perf: skip initResources when version matches, consolidate startup I/O (#1052)

- Add version-match early return to initResources() — skips ~800ms of
  synchronous rmSync + cpSync when managed-resources.json already matches
  the running GSD version (steady-state on every launch)
- Consolidate package.json reads in loader.ts from 3 to 1 — single read
  reused for --version, --help, banner, and GSD_VERSION env var
- Replace blocking checkAndPromptForUpdates() with passive checkForUpdates()
  to avoid blocking startup on npm registry fetch + user prompt (up to 5s)
- Cache bundled extension keys in resource-loader to avoid redundant
  filesystem scan in buildResourceLoader()
- Use GSD_VERSION env var in getBundledGsdVersion() to skip package.json
  re-read from resource-loader.ts
- Add test verifying version-skip behavior: marker file survives when
  versions match, gets cleaned on mismatch
This commit is contained in:
Jeremy McSpadden 2026-03-17 22:57:13 -05:00 committed by GitHub
parent 326cef0b2d
commit ce1ad35706
4 changed files with 76 additions and 31 deletions

View file

@ -18,7 +18,7 @@ import { loadStoredEnvKeys } from './wizard.js'
import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migration.js'
import { shouldRunOnboarding, runOnboarding } from './onboarding.js'
import chalk from 'chalk'
import { checkForUpdates, checkAndPromptForUpdates } from './update-check.js'
import { checkForUpdates } from './update-check.js'
import { printHelp, printSubcommandHelp } from './help-text.js'
// ---------------------------------------------------------------------------
@ -211,17 +211,11 @@ if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultP
process.stdin.pause()
}
// Update check — interactive prompt when stdin is a TTY, passive banner otherwise
// Update check — non-blocking banner check; interactive prompt deferred to avoid
// blocking startup. The passive checkForUpdates() prints a banner if an update is
// available (using cached data or a background fetch) without blocking the TUI.
if (!isPrintMode) {
if (process.stdin.isTTY) {
const updated = await checkAndPromptForUpdates().catch(() => false)
if (updated) {
// User chose to update — exit so they relaunch with the new version
process.exit(0)
}
} else {
checkForUpdates().catch(() => {})
}
checkForUpdates().catch(() => {})
}
// Warn if terminal is too narrow for readable output

View file

@ -12,27 +12,21 @@ const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const args = process.argv.slice(2)
const firstArg = args[0]
let _cachedVersion: string | undefined
function getVersion(): string {
if (_cachedVersion === undefined) {
try {
const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'))
_cachedVersion = pkg.version || '0.0.0'
} catch {
_cachedVersion = '0.0.0'
}
}
return _cachedVersion as string
}
// Read package.json once — reused for version, banner, and GSD_VERSION below
let gsdVersion = '0.0.0'
try {
const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'))
gsdVersion = pkg.version || '0.0.0'
} catch { /* ignore */ }
if (firstArg === '--version' || firstArg === '-v') {
process.stdout.write(getVersion() + '\n')
process.stdout.write(gsdVersion + '\n')
process.exit(0)
}
if (firstArg === '--help' || firstArg === '-h') {
const { printHelp } = await import('./help-text.js')
printHelp(getVersion())
printHelp(gsdVersion)
process.exit(0)
}
@ -64,7 +58,7 @@ if (!existsSync(appRoot)) {
process.stderr.write(
renderLogo(colorCyan) +
'\n' +
` Get Shit Done ${dim}v${getVersion()}${reset}\n` +
` Get Shit Done ${dim}v${gsdVersion}${reset}\n` +
` ${green}Welcome.${reset} Setting up your environment...\n\n`
)
}
@ -87,7 +81,7 @@ const { Module } = await import('module');
(Module as any)._initPaths?.()
// GSD_VERSION — expose package version so extensions can display it
process.env.GSD_VERSION = getVersion()
process.env.GSD_VERSION = gsdVersion
// GSD_BIN_PATH — absolute path to this loader (dist/loader.js), used by patched subagent
// to spawn gsd instead of pi when dispatching workflow tasks

View file

@ -38,11 +38,15 @@ function getManagedResourceManifestPath(agentDir: string): string {
}
function getBundledGsdVersion(): string {
// Prefer GSD_VERSION env var (set once by loader.ts) to avoid re-reading package.json
if (process.env.GSD_VERSION && process.env.GSD_VERSION !== '0.0.0') {
return process.env.GSD_VERSION
}
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'
return '0.0.0'
}
}
@ -146,6 +150,14 @@ function syncResourceDir(srcDir: string, destDir: string): void {
export function initResources(agentDir: string): void {
mkdirSync(agentDir, { recursive: true })
// Skip the full copy when the synced version already matches the running version.
// This avoids ~800ms of synchronous rmSync + cpSync on every startup.
const currentVersion = getBundledGsdVersion()
const managedVersion = readManagedResourceVersion(agentDir)
if (managedVersion && managedVersion === currentVersion) {
return
}
syncResourceDir(bundledExtensionsDir, join(agentDir, 'extensions'))
syncResourceDir(join(resourcesDir, 'agents'), join(agentDir, 'agents'))
syncResourceDir(join(resourcesDir, 'skills'), join(agentDir, 'skills'))
@ -162,12 +174,22 @@ export function initResources(agentDir: string): void {
* ~/.gsd/agent/extensions/ (GSD's default) and ~/.pi/agent/extensions/ (pi's default).
* This allows users to use extensions from either location.
*/
// Cache bundled extension keys at module load — avoids re-scanning the extensions
// directory in buildResourceLoader() (already scanned by loader.ts for env var).
let _bundledExtensionKeys: Set<string> | null = null
function getBundledExtensionKeys(): Set<string> {
if (!_bundledExtensionKeys) {
_bundledExtensionKeys = new Set(
discoverExtensionEntryPaths(bundledExtensionsDir).map((entryPath) => getExtensionKey(entryPath, bundledExtensionsDir)),
)
}
return _bundledExtensionKeys
}
export function buildResourceLoader(agentDir: string): DefaultResourceLoader {
const piAgentDir = join(homedir(), '.pi', 'agent')
const piExtensionsDir = join(piAgentDir, 'extensions')
const bundledKeys = new Set(
discoverExtensionEntryPaths(bundledExtensionsDir).map((entryPath) => getExtensionKey(entryPath, bundledExtensionsDir)),
)
const bundledKeys = getBundledExtensionKeys()
const piExtensionPaths = discoverExtensionEntryPaths(piExtensionsDir).filter(
(entryPath) => !bundledKeys.has(getExtensionKey(entryPath, piExtensionsDir)),
)

View file

@ -157,6 +157,41 @@ test("initResources syncs extensions, agents, and skills to target dir", async (
}
});
test("initResources skips copy when managed version matches current version", async () => {
const { initResources, readManagedResourceVersion } = await import("../resource-loader.ts");
const tmp = mkdtempSync(join(tmpdir(), "gsd-resources-skip-"));
const fakeAgentDir = join(tmp, "agent");
try {
// First run: full sync (no manifest yet)
initResources(fakeAgentDir);
const version = readManagedResourceVersion(fakeAgentDir);
assert.ok(version, "manifest written after first sync");
// Add a marker file to detect whether sync runs again
const markerPath = join(fakeAgentDir, "extensions", "gsd", "_marker.txt");
writeFileSync(markerPath, "test-marker");
// Second run: version matches — should skip, marker survives
initResources(fakeAgentDir);
assert.ok(existsSync(markerPath), "marker file survives when version matches (sync skipped)");
// Simulate version mismatch by writing older version to manifest
const manifestPath = join(fakeAgentDir, "managed-resources.json");
writeFileSync(manifestPath, JSON.stringify({ gsdVersion: "0.0.1", syncedAt: Date.now() }));
// Third run: version mismatch — full sync, marker removed
initResources(fakeAgentDir);
assert.ok(!existsSync(markerPath), "marker file removed after version-mismatch sync");
// Manifest updated to current version
const updatedVersion = readManagedResourceVersion(fakeAgentDir);
assert.strictEqual(updatedVersion, version, "manifest updated to current version after sync");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
// ═══════════════════════════════════════════════════════════════════════════
// 4. wizard loadStoredEnvKeys hydration
// ═══════════════════════════════════════════════════════════════════════════