From bb4deaf24c6ee46737d9335930be32fb58ddae4e Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:08:38 -0800 Subject: [PATCH] fix: reset stale cache flag, guard gcloud null, validate DO config (#2073) - manifest.ts: Reset _staleCache on successful fetch/cache load so isStaleCache() doesn't falsely report stale data after reconnecting - gcp.ts: Replace getGcloudCmd()! with requireGcloudCmd() that throws a descriptive error instead of crashing with null dereference - digitalocean.ts: Replace unvalidated JSON.parse return with parseJsonObj() + isString()/isNumber() guards for type safety Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- packages/cli/package.json | 2 +- packages/cli/src/digitalocean/digitalocean.ts | 40 ++++++++++--------- packages/cli/src/gcp/gcp.ts | 20 ++++++++-- packages/cli/src/manifest.ts | 3 ++ 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index c2b08674..e0169471 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.11.21", + "version": "0.11.22", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/digitalocean/digitalocean.ts b/packages/cli/src/digitalocean/digitalocean.ts index 7848be32..aeaf6792 100644 --- a/packages/cli/src/digitalocean/digitalocean.ts +++ b/packages/cli/src/digitalocean/digitalocean.ts @@ -156,36 +156,28 @@ function getConfigPath(): string { return join(process.env.HOME || homedir(), ".config", "spawn", "digitalocean.json"); } -interface DoConfig { - api_key?: string; - token?: string; - refresh_token?: string; - expires_at?: number; - auth_method?: "oauth" | "manual"; -} - -function loadConfig(): DoConfig | null { +function loadConfig(): Record | null { try { - return JSON.parse(readFileSync(getConfigPath(), "utf-8")); + return parseJsonObj(readFileSync(getConfigPath(), "utf-8")); } catch { return null; } } -async function saveConfig(config: DoConfig): Promise { +async function saveConfig(values: Record): Promise { const configPath = getConfigPath(); const dir = configPath.replace(/\/[^/]+$/, ""); mkdirSync(dir, { recursive: true, mode: 0o700, }); - await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n", { + await Bun.write(configPath, JSON.stringify(values, null, 2) + "\n", { mode: 0o600, }); } async function saveTokenToConfig(token: string, refreshToken?: string, expiresIn?: number): Promise { - const config: DoConfig = { + const config: Record = { api_key: token, token, }; @@ -204,7 +196,9 @@ function loadTokenFromConfig(): string | null { if (!data) { return null; } - const token = data.api_key || data.token || ""; + const apiKey = isString(data.api_key) ? data.api_key : ""; + const tok = isString(data.token) ? data.token : ""; + const token = apiKey || tok; if (!token) { return null; } @@ -216,22 +210,30 @@ function loadTokenFromConfig(): string | null { function loadRefreshToken(): string | null { const data = loadConfig(); - if (!data?.refresh_token) { + if (!data) { return null; } - if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(data.refresh_token)) { + const refreshToken = isString(data.refresh_token) ? data.refresh_token : ""; + if (!refreshToken) { return null; } - return data.refresh_token; + if (!/^[a-zA-Z0-9._/@:+=, -]+$/.test(refreshToken)) { + return null; + } + return refreshToken; } function isTokenExpired(): boolean { const data = loadConfig(); - if (!data?.expires_at) { + if (!data) { + return false; + } + const expiresAt = isNumber(data.expires_at) ? data.expires_at : 0; + if (!expiresAt) { return false; } // Consider expired 5 minutes before actual expiry - return Math.floor(Date.now() / 1000) >= data.expires_at - 300; + return Math.floor(Date.now() / 1000) >= expiresAt - 300; } // ─── Token Validation ──────────────────────────────────────────────────────── diff --git a/packages/cli/src/gcp/gcp.ts b/packages/cli/src/gcp/gcp.ts index 1c4da8b6..7e8a350f 100644 --- a/packages/cli/src/gcp/gcp.ts +++ b/packages/cli/src/gcp/gcp.ts @@ -187,13 +187,27 @@ function getGcloudCmd(): string | null { return null; } +/** Get gcloud path or throw a descriptive error. */ +function requireGcloudCmd(): string { + const cmd = getGcloudCmd(); + if (!cmd) { + throw new Error( + "gcloud CLI not found. Install it first:\n" + + " macOS: brew install --cask google-cloud-sdk\n" + + " Linux: curl https://sdk.cloud.google.com | bash\n" + + " Or run: spawn gcp (auto-installs gcloud)", + ); + } + return cmd; +} + /** Run a gcloud command and return stdout. */ function gcloudSync(args: string[]): { stdout: string; stderr: string; exitCode: number; } { - const cmd = getGcloudCmd()!; + const cmd = requireGcloudCmd(); const proc = Bun.spawnSync( [ cmd, @@ -221,7 +235,7 @@ async function gcloud(args: string[]): Promise<{ stderr: string; exitCode: number; }> { - const cmd = getGcloudCmd()!; + const cmd = requireGcloudCmd(); const proc = Bun.spawn( [ cmd, @@ -250,7 +264,7 @@ async function gcloud(args: string[]): Promise<{ /** Run a gcloud command interactively (inheriting stdio). */ async function gcloudInteractive(args: string[]): Promise { - const cmd = getGcloudCmd()!; + const cmd = requireGcloudCmd(); const proc = Bun.spawn( [ cmd, diff --git a/packages/cli/src/manifest.ts b/packages/cli/src/manifest.ts index 1cef7f15..87bb41bb 100644 --- a/packages/cli/src/manifest.ts +++ b/packages/cli/src/manifest.ts @@ -198,6 +198,7 @@ function tryLoadFromDiskCache(): Manifest | null { function updateCache(manifest: Manifest): Manifest { writeCache(manifest); _cached = manifest; + _staleCache = false; return manifest; } @@ -233,6 +234,7 @@ export async function loadManifest(forceRefresh = false): Promise { const local = tryLoadLocalManifest(); if (local) { _cached = local; + _staleCache = false; return local; } @@ -241,6 +243,7 @@ export async function loadManifest(forceRefresh = false): Promise { const cached = tryLoadFromDiskCache(); if (cached) { _cached = cached; + _staleCache = false; return cached; } }