fix(update): fetch latest version from registry
This commit is contained in:
parent
335535b506
commit
c1f732fae8
5 changed files with 89 additions and 48 deletions
|
|
@ -25,6 +25,26 @@ import { getAutoWorktreePath } from "./auto-worktree.js";
|
|||
import { projectRoot } from "./commands/context.js";
|
||||
import { loadPrompt } from "./prompt-loader.js";
|
||||
|
||||
const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
|
||||
const UPDATE_FETCH_TIMEOUT_MS = 5000;
|
||||
|
||||
async function fetchLatestVersionForCommand(): Promise<string | null> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const res = await fetch(UPDATE_REGISTRY_URL, { signal: controller.signal });
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as { version?: string };
|
||||
const latest = typeof data.version === "string" ? data.version.trim().replace(/^v/, "") : "";
|
||||
return latest.length > 0 ? latest : null;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
|
||||
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
||||
const workflow = readFileSync(workflowPath, "utf-8");
|
||||
|
|
@ -394,13 +414,8 @@ export async function handleUpdate(ctx: ExtensionCommandContext): Promise<void>
|
|||
|
||||
ctx.ui.notify(`Current version: v${current}\nChecking npm registry...`, "info");
|
||||
|
||||
let latest: string;
|
||||
try {
|
||||
latest = execSync(`npm view ${NPM_PACKAGE} version`, {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
const latest = await fetchLatestVersionForCommand();
|
||||
if (!latest) {
|
||||
ctx.ui.notify("Failed to reach npm registry. Check your network connection.", "error");
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { join } from 'node:path'
|
|||
import { tmpdir } from 'node:os'
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { compareSemver, readUpdateCache, writeUpdateCache, checkForUpdates } from '../update-check.js'
|
||||
import { compareSemver, readUpdateCache, writeUpdateCache, checkForUpdates, fetchLatestVersionFromRegistry } from '../update-check.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// compareSemver
|
||||
|
|
@ -315,3 +315,23 @@ test('checkForUpdates handles missing version field in response', async (t) => {
|
|||
|
||||
assert.ok(!called, 'onUpdate should not be called when response has no version')
|
||||
})
|
||||
|
||||
test('fetchLatestVersionFromRegistry returns the registry version string', async (t) => {
|
||||
const registry = await startMockRegistry({ version: '2.67.0' })
|
||||
t.after(async () => {
|
||||
await registry.close()
|
||||
})
|
||||
|
||||
const latest = await fetchLatestVersionFromRegistry(registry.url, 5000)
|
||||
assert.equal(latest, '2.67.0')
|
||||
})
|
||||
|
||||
test('fetchLatestVersionFromRegistry returns null for blank version strings', async (t) => {
|
||||
const registry = await startMockRegistry({ version: '' })
|
||||
t.after(async () => {
|
||||
await registry.close()
|
||||
})
|
||||
|
||||
const latest = await fetchLatestVersionFromRegistry(registry.url, 5000)
|
||||
assert.equal(latest, null)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,10 +18,17 @@ test("update-cmd prints latest version before comparison (#3445)", () => {
|
|||
assert.ok(latestPrintIdx < comparisonIdx, "Must print latest BEFORE comparison");
|
||||
});
|
||||
|
||||
test("update-cmd bypasses npm cache (#3445)", () => {
|
||||
test("update commands use the registry fetch helper instead of npm view (#3806)", () => {
|
||||
const src = readFileSync(join(__dirname, "..", "update-cmd.ts"), "utf-8");
|
||||
const handlerSrc = readFileSync(join(__dirname, "..", "resources", "extensions", "gsd", "commands-handlers.ts"), "utf-8");
|
||||
assert.ok(
|
||||
src.includes("npm_config_cache"),
|
||||
"Must clear npm cache env to bypass stale registry data",
|
||||
src.includes("fetchLatestVersionFromRegistry"),
|
||||
"update-cmd should use the shared registry fetch helper",
|
||||
);
|
||||
assert.ok(!src.includes("npm view "), "update-cmd should no longer shell out to npm view");
|
||||
assert.ok(
|
||||
handlerSrc.includes("fetchLatestVersionForCommand"),
|
||||
"/gsd update should fetch the latest version through a registry helper too",
|
||||
);
|
||||
assert.ok(!handlerSrc.includes("npm view "), "/gsd update should no longer shell out to npm view");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const CACHE_FILE = join(appRoot, '.update-check')
|
|||
const NPM_PACKAGE_NAME = 'gsd-pi'
|
||||
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
||||
const FETCH_TIMEOUT_MS = 5000
|
||||
const DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`
|
||||
|
||||
interface UpdateCheckCache {
|
||||
lastCheck: number
|
||||
|
|
@ -47,6 +48,32 @@ export function writeUpdateCache(cache: UpdateCheckCache, cachePath: string = CA
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeLatestVersion(version: unknown): string | null {
|
||||
if (typeof version !== 'string') return null
|
||||
const trimmed = version.trim().replace(/^v/, '')
|
||||
return trimmed.length > 0 ? trimmed : null
|
||||
}
|
||||
|
||||
export async function fetchLatestVersionFromRegistry(
|
||||
registryUrl: string = DEFAULT_REGISTRY_URL,
|
||||
fetchTimeoutMs: number = FETCH_TIMEOUT_MS,
|
||||
): Promise<string | null> {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs)
|
||||
|
||||
try {
|
||||
const res = await fetch(registryUrl, { signal: controller.signal })
|
||||
if (!res.ok) return null
|
||||
|
||||
const data = (await res.json()) as { version?: string }
|
||||
return normalizeLatestVersion(data.version)
|
||||
} catch {
|
||||
return null
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
function printUpdateBanner(current: string, latest: string): void {
|
||||
process.stderr.write(
|
||||
` ${chalk.yellow('Update available:')} ${chalk.dim(`v${current}`)} → ${chalk.bold(`v${latest}`)}\n` +
|
||||
|
|
@ -70,7 +97,7 @@ export interface UpdateCheckOptions {
|
|||
export async function checkForUpdates(options: UpdateCheckOptions = {}): Promise<void> {
|
||||
const currentVersion = options.currentVersion || process.env.GSD_VERSION || '0.0.0'
|
||||
const cachePath = options.cachePath || CACHE_FILE
|
||||
const registryUrl = options.registryUrl || `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`
|
||||
const registryUrl = options.registryUrl || DEFAULT_REGISTRY_URL
|
||||
const checkIntervalMs = options.checkIntervalMs ?? CHECK_INTERVAL_MS
|
||||
const fetchTimeoutMs = options.fetchTimeoutMs ?? FETCH_TIMEOUT_MS
|
||||
const onUpdate = options.onUpdate || printUpdateBanner
|
||||
|
|
@ -84,18 +111,8 @@ export async function checkForUpdates(options: UpdateCheckOptions = {}): Promise
|
|||
return
|
||||
}
|
||||
|
||||
// Fetch latest version from npm registry
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs)
|
||||
|
||||
try {
|
||||
const res = await fetch(registryUrl, { signal: controller.signal })
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (!res.ok) return
|
||||
|
||||
const data = (await res.json()) as { version?: string }
|
||||
const latestVersion = data.version
|
||||
const latestVersion = await fetchLatestVersionFromRegistry(registryUrl, fetchTimeoutMs)
|
||||
if (!latestVersion) return
|
||||
|
||||
writeUpdateCache({ lastCheck: Date.now(), latestVersion }, cachePath)
|
||||
|
|
@ -105,8 +122,6 @@ export async function checkForUpdates(options: UpdateCheckOptions = {}): Promise
|
|||
}
|
||||
} catch {
|
||||
// Network error or timeout — silently ignore, don't block startup
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +138,7 @@ const PROMPT_TIMEOUT_MS = 30_000
|
|||
export async function checkAndPromptForUpdates(options: UpdateCheckOptions = {}): Promise<boolean> {
|
||||
const currentVersion = options.currentVersion || process.env.GSD_VERSION || '0.0.0'
|
||||
const cachePath = options.cachePath || CACHE_FILE
|
||||
const registryUrl = options.registryUrl || `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`
|
||||
const registryUrl = options.registryUrl || DEFAULT_REGISTRY_URL
|
||||
const checkIntervalMs = options.checkIntervalMs ?? CHECK_INTERVAL_MS
|
||||
const fetchTimeoutMs = options.fetchTimeoutMs ?? FETCH_TIMEOUT_MS
|
||||
|
||||
|
|
@ -134,22 +149,13 @@ export async function checkAndPromptForUpdates(options: UpdateCheckOptions = {})
|
|||
if (cache && Date.now() - cache.lastCheck < checkIntervalMs) {
|
||||
latestVersion = cache.latestVersion
|
||||
} else {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs)
|
||||
try {
|
||||
const res = await fetch(registryUrl, { signal: controller.signal })
|
||||
clearTimeout(timeout)
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as { version?: string }
|
||||
if (data.version) {
|
||||
latestVersion = data.version
|
||||
writeUpdateCache({ lastCheck: Date.now(), latestVersion }, cachePath)
|
||||
}
|
||||
latestVersion = await fetchLatestVersionFromRegistry(registryUrl, fetchTimeoutMs)
|
||||
if (latestVersion) {
|
||||
writeUpdateCache({ lastCheck: Date.now(), latestVersion }, cachePath)
|
||||
}
|
||||
} catch {
|
||||
// Network unavailable — silently skip
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { execSync } from 'node:child_process'
|
||||
import { compareSemver } from './update-check.js'
|
||||
import { compareSemver, fetchLatestVersionFromRegistry } from './update-check.js'
|
||||
|
||||
const NPM_PACKAGE = 'gsd-pi'
|
||||
|
||||
|
|
@ -14,15 +14,8 @@ export async function runUpdate(): Promise<void> {
|
|||
process.stdout.write(`${dim}Current version:${reset} v${current}\n`)
|
||||
process.stdout.write(`${dim}Checking npm registry...${reset}\n`)
|
||||
|
||||
// Fetch latest version — bypass npm client cache to avoid stale results (#3445)
|
||||
let latest: string
|
||||
try {
|
||||
latest = execSync(`npm view ${NPM_PACKAGE} version --fetch-retry-mintimeout=3000`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
env: { ...process.env, npm_config_cache: '' },
|
||||
}).trim()
|
||||
} catch {
|
||||
const latest = await fetchLatestVersionFromRegistry()
|
||||
if (!latest) {
|
||||
process.stderr.write(`${yellow}Failed to reach npm registry.${reset}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue