fix: guard against newer synced resources (#445)
Co-authored-by: TÂCHES <afromanguy@me.com>
This commit is contained in:
parent
16364c7dba
commit
fe03743b08
4 changed files with 117 additions and 3 deletions
24
src/cli.ts
24
src/cli.ts
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { agentDir, sessionsDir, authFilePath } from './app-paths.js'
|
||||
import { initResources, buildResourceLoader } from './resource-loader.js'
|
||||
import { initResources, buildResourceLoader, getNewerManagedResourceVersion } from './resource-loader.js'
|
||||
import { ensureManagedTools } from './tool-bootstrap.js'
|
||||
import { loadStoredEnvKeys } from './wizard.js'
|
||||
import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migration.js'
|
||||
|
|
@ -35,6 +35,26 @@ interface CliFlags {
|
|||
messages: string[]
|
||||
}
|
||||
|
||||
function exitIfManagedResourcesAreNewer(currentAgentDir: string): void {
|
||||
const currentVersion = process.env.GSD_VERSION || '0.0.0'
|
||||
const managedVersion = getNewerManagedResourceVersion(currentAgentDir, currentVersion)
|
||||
if (!managedVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
const yellow = '\x1b[33m'
|
||||
const dim = '\x1b[2m'
|
||||
const reset = '\x1b[0m'
|
||||
const bold = '\x1b[1m'
|
||||
|
||||
process.stderr.write(
|
||||
`[gsd] ${yellow}Version mismatch detected${reset}\n` +
|
||||
`[gsd] Synced resources are from ${bold}v${managedVersion}${reset}, but this \`gsd\` binary is ${dim}v${currentVersion}${reset}.\n` +
|
||||
`[gsd] Run ${bold}npm install -g gsd-pi@latest${reset} or ${bold}gsd update${reset}, then try again.\n`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function parseCliArgs(argv: string[]): CliFlags {
|
||||
const flags: CliFlags = { extensions: [], messages: [] }
|
||||
const args = argv.slice(2) // skip node + script
|
||||
|
|
@ -232,6 +252,7 @@ if (isPrintMode) {
|
|||
}
|
||||
}
|
||||
|
||||
exitIfManagedResourcesAreNewer(agentDir)
|
||||
initResources(agentDir)
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
agentDir,
|
||||
|
|
@ -316,6 +337,7 @@ const sessionManager = cliFlags.continue
|
|||
? SessionManager.continueRecent(cwd, projectSessionsDir)
|
||||
: SessionManager.create(cwd, projectSessionsDir)
|
||||
|
||||
exitIfManagedResourcesAreNewer(agentDir)
|
||||
initResources(agentDir)
|
||||
const resourceLoader = buildResourceLoader(agentDir)
|
||||
await resourceLoader.reload()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { homedir } from 'node:os'
|
|||
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { compareSemver } from './update-check.js'
|
||||
|
||||
// Resolve resources directory — prefer dist/resources/ (stable, set at build time)
|
||||
// over src/resources/ (live working tree, changes with git branch).
|
||||
|
|
@ -17,6 +18,11 @@ const distResources = join(packageRoot, 'dist', 'resources')
|
|||
const srcResources = join(packageRoot, 'src', 'resources')
|
||||
const resourcesDir = existsSync(distResources) ? distResources : srcResources
|
||||
const bundledExtensionsDir = join(resourcesDir, 'extensions')
|
||||
const resourceVersionManifestName = 'managed-resources.json'
|
||||
|
||||
interface ManagedResourceManifest {
|
||||
gsdVersion: string
|
||||
}
|
||||
|
||||
function isExtensionFile(name: string): boolean {
|
||||
return name.endsWith('.ts') || name.endsWith('.js')
|
||||
|
|
@ -82,6 +88,41 @@ function getExtensionKey(entryPath: string, extensionsDir: string): string {
|
|||
return relPath.split(/[\\/]/)[0]
|
||||
}
|
||||
|
||||
function getManagedResourceManifestPath(agentDir: string): string {
|
||||
return join(agentDir, resourceVersionManifestName)
|
||||
}
|
||||
|
||||
function getBundledGsdVersion(): string {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8'))
|
||||
return typeof pkg?.version === 'string' ? pkg.version : '0.0.0'
|
||||
} catch {
|
||||
return process.env.GSD_VERSION || '0.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
function writeManagedResourceManifest(agentDir: string): void {
|
||||
const manifest: ManagedResourceManifest = { gsdVersion: getBundledGsdVersion() }
|
||||
writeFileSync(getManagedResourceManifestPath(agentDir), JSON.stringify(manifest))
|
||||
}
|
||||
|
||||
export function readManagedResourceVersion(agentDir: string): string | null {
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(getManagedResourceManifestPath(agentDir), 'utf-8')) as ManagedResourceManifest
|
||||
return typeof manifest?.gsdVersion === 'string' ? manifest.gsdVersion : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function getNewerManagedResourceVersion(agentDir: string, currentVersion: string): string | null {
|
||||
const managedVersion = readManagedResourceVersion(agentDir)
|
||||
if (!managedVersion) {
|
||||
return null
|
||||
}
|
||||
return compareSemver(managedVersion, currentVersion) > 0 ? managedVersion : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
||||
*
|
||||
|
|
@ -116,6 +157,8 @@ export function initResources(agentDir: string): void {
|
|||
if (existsSync(srcSkills)) {
|
||||
cpSync(srcSkills, destSkills, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
writeManagedResourceManifest(agentDir)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ test("loader sets all 4 GSD_ env vars and PI_PACKAGE_DIR", async () => {
|
|||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test("initResources syncs extensions, agents, and skills to target dir", async () => {
|
||||
const { initResources } = await import("../resource-loader.ts");
|
||||
const { initResources, readManagedResourceVersion } = await import("../resource-loader.ts");
|
||||
const tmp = mkdtempSync(join(tmpdir(), "gsd-resources-test-"));
|
||||
const fakeAgentDir = join(tmp, "agent");
|
||||
|
||||
|
|
@ -146,6 +146,10 @@ test("initResources syncs extensions, agents, and skills to target dir", async (
|
|||
// Skills synced
|
||||
assert.ok(existsSync(join(fakeAgentDir, "skills")), "skills directory synced");
|
||||
|
||||
// Version manifest synced
|
||||
const managedVersion = readManagedResourceVersion(fakeAgentDir);
|
||||
assert.ok(managedVersion, "managed resource version written");
|
||||
|
||||
// Idempotent: run again, no crash
|
||||
initResources(fakeAgentDir);
|
||||
assert.ok(existsSync(join(fakeAgentDir, "extensions", "gsd", "index.ts")), "idempotent re-sync works");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createGunzip } from "node:zlib";
|
||||
|
|
@ -229,3 +229,48 @@ test("gsd launches and loads extensions without errors", async () => {
|
|||
"no ERR_MODULE_NOT_FOUND",
|
||||
);
|
||||
});
|
||||
|
||||
test("gsd exits early with a clear message when synced resources are newer than the binary", async () => {
|
||||
const fakeHome = mkdtempSync(join(tmpdir(), "gsd-version-skew-"));
|
||||
const fakeAgentDir = join(fakeHome, ".gsd", "agent");
|
||||
mkdirSync(fakeAgentDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(fakeAgentDir, "managed-resources.json"),
|
||||
JSON.stringify({ gsdVersion: "999.0.0" }),
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await new Promise<{ code: number | null; stderr: string }>((resolve) => {
|
||||
let stderr = "";
|
||||
const child = spawn("node", ["dist/loader.js"], {
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: fakeHome,
|
||||
BRAVE_API_KEY: "test",
|
||||
BRAVE_ANSWERS_KEY: "test",
|
||||
CONTEXT7_API_KEY: "test",
|
||||
JINA_API_KEY: "test",
|
||||
TAVILY_API_KEY: "test",
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.stdin.end();
|
||||
child.on("close", (code) => {
|
||||
resolve({ code, stderr });
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(result.code, 1, "startup exits with code 1 on version skew");
|
||||
assert.match(result.stderr, /Version mismatch detected/, "prints a friendly skew header");
|
||||
assert.match(result.stderr, /npm install -g gsd-pi@latest|gsd update/, "prints upgrade guidance");
|
||||
assert.doesNotMatch(result.stderr, /\[gsd\] Extension load error/, "fails before extension loading");
|
||||
} finally {
|
||||
rmSync(fakeHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue