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:
parent
326cef0b2d
commit
ce1ad35706
4 changed files with 76 additions and 31 deletions
16
src/cli.ts
16
src/cli.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue