spawn/packages/cli/src/shared/paths.ts
A fef312cd47
fix(update): cache successful update checks for 1 hour (#2755)
checkForUpdates() previously fetched the latest version from GitHub on
every single CLI invocation, blocking for up to 10s on slow/offline
connections. Now it writes a timestamp to ~/.config/spawn/.update-checked
after a successful check and skips the network call if the cache is
less than 1 hour old.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 23:08:05 -07:00

99 lines
3.6 KiB
TypeScript

// shared/paths.ts — Centralized filesystem path resolution
//
// All path helpers live here. Production code imports from this module;
// no other module should call homedir() or construct spawn-specific paths directly.
import { homedir, tmpdir } from "node:os";
import { isAbsolute, join, resolve } from "node:path";
/** Return the user's home directory, preferring $HOME over os.homedir(). */
export function getUserHome(): string {
return process.env.HOME || homedir();
}
/** Returns the directory for spawn data, respecting SPAWN_HOME env var.
* SPAWN_HOME must be an absolute path if set; relative paths are rejected
* to prevent unintended file writes. */
export function getSpawnDir(): string {
const spawnHome = process.env.SPAWN_HOME;
if (!spawnHome) {
return join(getUserHome(), ".spawn");
}
// Require absolute path to prevent path traversal via relative paths
if (!isAbsolute(spawnHome)) {
throw new Error(
`SPAWN_HOME must be an absolute path (got "${spawnHome}").\n` + "Example: export SPAWN_HOME=/home/user/.spawn",
);
}
// Resolve to canonical form (collapses .. segments)
const resolved = resolve(spawnHome);
// SECURITY: Prevent path traversal to system directories
// Even though the path is absolute, resolve() can normalize paths like
// /tmp/../../root/.spawn to /root/.spawn, potentially allowing unauthorized
// file writes to sensitive directories.
const userHome = getUserHome();
if (!resolved.startsWith(userHome + "/") && resolved !== userHome) {
throw new Error("SPAWN_HOME must be within your home directory.\n" + `Got: ${resolved}\n` + `Home: ${userHome}`);
}
return resolved;
}
/** Path to the spawn history file. */
export function getHistoryPath(): string {
return join(getSpawnDir(), "history.json");
}
/**
* Return the path to the per-cloud config file: ~/.config/spawn/{cloud}.json
* Shared by all cloud modules to avoid repeating the same path construction.
*/
export function getSpawnCloudConfigPath(cloud: string): string {
return join(getUserHome(), ".config", "spawn", `${cloud}.json`);
}
/** Return the path to the spawn preferences file: ~/.config/spawn/preferences.json */
export function getSpawnPreferencesPath(): string {
return join(getUserHome(), ".config", "spawn", "preferences.json");
}
/** Return the cache directory for spawn, respecting XDG_CACHE_HOME. */
export function getCacheDir(): string {
return join(process.env.XDG_CACHE_HOME || join(getUserHome(), ".cache"), "spawn");
}
/** Return the path to the cached manifest file. */
export function getCacheFile(): string {
return join(getCacheDir(), "manifest.json");
}
/** Return the path to the update-failed sentinel file. */
export function getUpdateFailedPath(): string {
return join(getUserHome(), ".config", "spawn", ".update-failed");
}
/** Return the path to the last-successful-update-check sentinel file. */
export function getUpdateCheckedPath(): string {
return join(getUserHome(), ".config", "spawn", ".update-checked");
}
/** Return the path to the user's ~/.ssh directory. */
export function getSshDir(): string {
return join(getUserHome(), ".ssh");
}
/** Return the system temp directory (wraps os.tmpdir()). */
export function getTmpDir(): string {
return tmpdir();
}
/**
* Shell RC marker comments used by install.sh and uninstall.ts.
* Keep in sync with sh/cli/install.sh — both files use these exact strings.
*/
export const RC_MARKER_START = "# >>> spawn >>>";
export const RC_MARKER_END = "# <<< spawn <<<";
/** Legacy single-line marker written by installer versions before start/end markers. */
export const RC_MARKER_LEGACY = "# Added by spawn installer";