Perf/gsd startup speed (#497)
* docs: add startup performance analysis and optimization plan Profiled GSD CLI startup finding 2.2s for --version and ~3.8s for interactive mode. Identified 5 root causes with measured timings and created a phased optimization plan targeting <0.2s for --version and ~0.8s for interactive startup. * perf: speed up GSD startup with lazy loading and fast paths - Fast-path --version/-v and --help/-h in loader.ts before importing any heavy dependencies (2.2s → 0.15s, 14x faster) - Lazy-load undici (~200ms) only when HTTP_PROXY env vars are set - Skip initResources cpSync when managed-resources.json version matches current GSD version (~128ms saved per launch) - Lazy-load Mistral SDK (~369ms) on first API call instead of startup - Lazy-load Google GenAI SDK (~186ms) on first API call instead of startup - Parallelize extension loading with Promise.all() instead of sequential for-loop --------- Co-authored-by: TÂCHES <afromanguy@me.com>
This commit is contained in:
parent
ed47018496
commit
e6d55f8aaf
6 changed files with 272 additions and 33 deletions
157
.plans/startup-performance.md
Normal file
157
.plans/startup-performance.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# GSD Startup Performance Analysis & Optimization Plan
|
||||
|
||||
## Measured Baseline (macOS, Node v25.6.1)
|
||||
|
||||
### `gsd --version` (simplest possible path): **2.2 seconds**
|
||||
|
||||
| Phase | Time | Notes |
|
||||
|-------|------|-------|
|
||||
| Node.js process startup | ~160ms | Unavoidable |
|
||||
| loader.js top-level imports | ~13ms | fs, app-paths, logo |
|
||||
| undici import + proxy setup | ~200ms | EnvHttpProxyAgent |
|
||||
| **@gsd/pi-coding-agent barrel import** | **~970ms** | THE BOTTLENECK |
|
||||
| cli.js other imports | ~3ms | resource-loader, wizard, etc. |
|
||||
| Arg parsing + version print | ~0ms | |
|
||||
| Measured wall time overhead | ~700ms | ESM resolution, gc, etc. |
|
||||
|
||||
### Full interactive startup: **~3.6 seconds** (post-node)
|
||||
|
||||
| Phase | Time | Notes |
|
||||
|-------|------|-------|
|
||||
| @gsd/pi-coding-agent import | ~750ms | (cached from loader measurement) |
|
||||
| ensureManagedTools | ~0ms | No-op after first run |
|
||||
| AuthStorage + env keys | ~3ms | |
|
||||
| ModelRegistry | ~1ms | |
|
||||
| SettingsManager | ~1ms | |
|
||||
| **initResources (cpSync)** | **~128ms** | Copies all extensions/skills/agents on every launch |
|
||||
| **resourceLoader.reload()** | **~2535ms** | jiti-compiles 17+ extensions from TypeScript |
|
||||
|
||||
### Inside @gsd/pi-coding-agent (barrel import breakdown)
|
||||
|
||||
| Sub-module | Time | Notes |
|
||||
|------------|------|-------|
|
||||
| Mistral SDK (@mistralai/mistralai) | 369ms | Loaded even if unused |
|
||||
| Google GenAI SDK (@google/genai) | 186ms | Loaded even if unused |
|
||||
| extensions/index.js (circular → index.js) | 497ms | Pulls in everything |
|
||||
| tools/index.js | 124ms | Tool definitions |
|
||||
| @sinclair/typebox | 64ms | Schema validation |
|
||||
| OpenAI SDK | 52ms | |
|
||||
| Anthropic SDK | 50ms | |
|
||||
|
||||
---
|
||||
|
||||
## Root Causes (Priority Order)
|
||||
|
||||
### 1. Extension JIT compilation via jiti (~2.5s)
|
||||
Every launch compiles 17+ TypeScript extensions to JavaScript using jiti. No caching (`moduleCache: false` is explicitly set). This is the single largest cost.
|
||||
|
||||
### 2. Barrel import of @gsd/pi-coding-agent (~1s)
|
||||
`cli.js` line 1 does a barrel import pulling in ALL exports including all LLM provider SDKs, TUI components, theme system, compaction, blob store, etc.
|
||||
|
||||
### 3. Eager LLM SDK loading (~660ms inside barrel)
|
||||
All provider SDKs are imported at module evaluation time in `pi-ai/index.js`, even though only one provider is typically configured.
|
||||
|
||||
### 4. initResources copies files every launch (~128ms)
|
||||
`cpSync` with `force: true` copies all bundled resources to `~/.gsd/agent/` on every startup, even when nothing changed.
|
||||
|
||||
### 5. undici import (~200ms)
|
||||
Imported in loader.js for proxy support. Not needed for most users.
|
||||
|
||||
---
|
||||
|
||||
## Optimization Plan
|
||||
|
||||
### Phase 1: Quick Wins (est. save ~1-1.5s on --version, ~0.5s interactive)
|
||||
|
||||
#### 1A. Fast-path for `--version` and `--help`
|
||||
Parse argv BEFORE importing cli.js. In loader.js, check for `--version`/`-v` and `--help`/`-h` and exit immediately without loading any dependencies.
|
||||
|
||||
**File**: `src/loader.ts`
|
||||
**Change**: Add arg check before `await import('./cli.js')`
|
||||
**Impact**: `gsd --version` goes from 2.2s → ~0.2s
|
||||
|
||||
#### 1B. Skip initResources when unchanged
|
||||
Compare `managed-resources.json` version against current `GSD_VERSION`. If they match, skip the `cpSync` entirely.
|
||||
|
||||
**File**: `src/resource-loader.ts` → `initResources()`
|
||||
**Change**: Early return if versions match
|
||||
**Impact**: Save ~128ms per launch
|
||||
|
||||
#### 1C. Lazy-load undici
|
||||
Only import undici when HTTP_PROXY/HTTPS_PROXY env vars are actually set.
|
||||
|
||||
**File**: `src/loader.ts`
|
||||
**Change**: Wrap undici import in proxy env check
|
||||
**Impact**: Save ~200ms for most users
|
||||
|
||||
### Phase 2: Lazy Provider Loading (est. save ~600ms interactive)
|
||||
|
||||
#### 2A. Lazy-load LLM provider SDKs
|
||||
Instead of importing all providers at module level in `pi-ai/index.js`, use dynamic `import()` in the provider factory functions. Only load the SDK when a model from that provider is actually requested.
|
||||
|
||||
**Files**: `packages/pi-ai/src/providers/*.ts`
|
||||
**Change**: Move `import { Anthropic } from '@anthropic-ai/sdk'` etc. to dynamic imports inside `complete()` / `stream()` functions
|
||||
**Impact**: Save ~600ms (Mistral 369ms + Google 186ms + extras) for users who only use one provider
|
||||
|
||||
#### 2B. Selective re-exports in pi-ai barrel
|
||||
Instead of `export * from "./providers/mistral.js"` etc., only export the registration function. Provider internals stay private.
|
||||
|
||||
**File**: `packages/pi-ai/src/index.ts`
|
||||
|
||||
### Phase 3: Extension Loading Optimization (est. save ~1.5-2s interactive)
|
||||
|
||||
#### 3A. Enable jiti module caching
|
||||
Remove `moduleCache: false` from the jiti config, or use a persistent cache directory.
|
||||
|
||||
**File**: `packages/pi-coding-agent/src/core/extensions/loader.ts`
|
||||
**Change**: Set `moduleCache: true` or configure `cacheDir`
|
||||
**Impact**: Second+ launches save ~1-2s on extension loading
|
||||
|
||||
#### 3B. Pre-compile extensions at build time
|
||||
Instead of JIT-compiling TypeScript extensions at runtime, compile them to JavaScript during `npm run build`. The runtime loader can then just `import()` the .js files directly without jiti.
|
||||
|
||||
**Files**: `package.json` build scripts, `src/resource-loader.ts`, extension loader
|
||||
**Change**: Add build step to compile extensions; loader checks for .js first
|
||||
**Impact**: Eliminate ~2.5s of jiti compilation entirely
|
||||
**Complexity**: HIGH — requires careful handling of extension resolution paths
|
||||
|
||||
#### 3C. Parallel extension loading
|
||||
Currently extensions load sequentially in a `for` loop. Load them in parallel with `Promise.all()`.
|
||||
|
||||
**File**: `packages/pi-coding-agent/src/core/extensions/loader.ts` → `loadExtensions()`
|
||||
**Change**: `await Promise.all(paths.map(...))` instead of sequential for-loop
|
||||
**Impact**: Wall time reduction depends on I/O overlap; est. 30-50% faster
|
||||
|
||||
### Phase 4: Bundle Optimization (est. save ~300-500ms)
|
||||
|
||||
#### 4A. Use esbuild/tsup for the main CLI bundle
|
||||
Replace plain `tsc` with a bundler that does tree-shaking. A single-file bundle eliminates ESM resolution overhead and removes unused code.
|
||||
|
||||
**Impact**: Faster module resolution, smaller output, tree-shaking removes unused exports
|
||||
**Complexity**: MEDIUM
|
||||
|
||||
#### 4B. Split pi-coding-agent into entry-point chunks
|
||||
Instead of one barrel export, provide separate entry points for core, interactive, tools.
|
||||
|
||||
**Impact**: cli.js can import only what it needs for each code path
|
||||
**Complexity**: HIGH — changes public API surface
|
||||
|
||||
---
|
||||
|
||||
## Recommended Implementation Order
|
||||
|
||||
1. **Phase 1A** — Fast-path --version/--help (trivial, huge UX impact)
|
||||
2. **Phase 1C** — Lazy undici (easy, 200ms saved)
|
||||
3. **Phase 1B** — Skip initResources (easy, 128ms saved)
|
||||
4. **Phase 3C** — Parallel extension loading (moderate, ~1s saved)
|
||||
5. **Phase 2A** — Lazy provider SDKs (moderate, ~600ms saved)
|
||||
6. **Phase 3A** — jiti caching (easy, ~1s saved on repeat launches)
|
||||
7. **Phase 3B** — Pre-compile extensions (hard, eliminates jiti entirely)
|
||||
8. **Phase 4A** — Bundle with esbuild (medium, ~300-500ms)
|
||||
|
||||
### Expected Results
|
||||
|
||||
| Scenario | Before | After (Phase 1-3) | After (All) |
|
||||
|----------|--------|-------------------|-------------|
|
||||
| `gsd --version` | 2.2s | **~0.2s** | ~0.2s |
|
||||
| Interactive startup | ~3.8s | **~1.5s** | **~0.8s** |
|
||||
|
|
@ -1,9 +1,20 @@
|
|||
import {
|
||||
type GenerateContentConfig,
|
||||
type GenerateContentParameters,
|
||||
// Lazy-loaded: Google GenAI SDK (~186ms) is imported on first use, not at startup.
|
||||
// This avoids penalizing users who don't use Google models.
|
||||
import type {
|
||||
GenerateContentConfig,
|
||||
GenerateContentParameters,
|
||||
GoogleGenAI,
|
||||
type ThinkingConfig,
|
||||
ThinkingConfig,
|
||||
} from "@google/genai";
|
||||
|
||||
let _GoogleGenAIClass: typeof GoogleGenAI | undefined;
|
||||
async function getGoogleGenAIClass(): Promise<typeof GoogleGenAI> {
|
||||
if (!_GoogleGenAIClass) {
|
||||
const mod = await import("@google/genai");
|
||||
_GoogleGenAIClass = mod.GoogleGenAI;
|
||||
}
|
||||
return _GoogleGenAIClass;
|
||||
}
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { calculateCost } from "../models.js";
|
||||
import type {
|
||||
|
|
@ -73,7 +84,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>
|
|||
|
||||
try {
|
||||
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
||||
const client = createClient(model, apiKey, options?.headers);
|
||||
const client = await createClient(model, apiKey, options?.headers);
|
||||
let params = buildParams(model, context, options);
|
||||
const nextParams = await options?.onPayload?.(params, model);
|
||||
if (nextParams !== undefined) {
|
||||
|
|
@ -308,11 +319,11 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt
|
|||
} satisfies GoogleOptions);
|
||||
};
|
||||
|
||||
function createClient(
|
||||
async function createClient(
|
||||
model: Model<"google-generative-ai">,
|
||||
apiKey?: string,
|
||||
optionsHeaders?: Record<string, string>,
|
||||
): GoogleGenAI {
|
||||
): Promise<GoogleGenAI> {
|
||||
const httpOptions: { baseUrl?: string; apiVersion?: string; headers?: Record<string, string> } = {};
|
||||
if (model.baseUrl) {
|
||||
httpOptions.baseUrl = model.baseUrl;
|
||||
|
|
@ -322,7 +333,8 @@ function createClient(
|
|||
httpOptions.headers = { ...model.headers, ...optionsHeaders };
|
||||
}
|
||||
|
||||
return new GoogleGenAI({
|
||||
const GoogleGenAIClass = await getGoogleGenAIClass();
|
||||
return new GoogleGenAIClass({
|
||||
apiKey,
|
||||
httpOptions: Object.keys(httpOptions).length > 0 ? httpOptions : undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Mistral } from "@mistralai/mistralai";
|
||||
// Lazy-loaded: Mistral SDK (~369ms) is imported on first use, not at startup.
|
||||
// This avoids penalizing users who don't use Mistral models.
|
||||
import type { Mistral } from "@mistralai/mistralai";
|
||||
import type { RequestOptions } from "@mistralai/mistralai/lib/sdks.js";
|
||||
import type {
|
||||
ChatCompletionStreamRequest,
|
||||
|
|
@ -7,6 +9,15 @@ import type {
|
|||
ContentChunk,
|
||||
FunctionTool,
|
||||
} from "@mistralai/mistralai/models/components/index.js";
|
||||
|
||||
let _MistralClass: typeof Mistral | undefined;
|
||||
async function getMistralClass(): Promise<typeof Mistral> {
|
||||
if (!_MistralClass) {
|
||||
const mod = await import("@mistralai/mistralai");
|
||||
_MistralClass = mod.Mistral;
|
||||
}
|
||||
return _MistralClass;
|
||||
}
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { calculateCost } from "../models.js";
|
||||
import type {
|
||||
|
|
@ -61,7 +72,8 @@ export const streamMistral: StreamFunction<"mistral-conversations", MistralOptio
|
|||
}
|
||||
|
||||
// Intentionally per-request: avoids shared SDK mutable state across concurrent consumers.
|
||||
const mistral = new Mistral({
|
||||
const MistralSDK = await getMistralClass();
|
||||
const mistral = new MistralSDK({
|
||||
apiKey,
|
||||
serverURL: model.baseUrl,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -369,22 +369,26 @@ export async function loadExtensionFromFactory(
|
|||
|
||||
/**
|
||||
* Load extensions from paths.
|
||||
*
|
||||
* Extensions are loaded in parallel to reduce wall-clock time (~30-50% faster
|
||||
* than sequential loading for I/O-bound jiti compilation).
|
||||
*/
|
||||
export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
|
||||
const extensions: Extension[] = [];
|
||||
const errors: Array<{ path: string; error: string }> = [];
|
||||
const resolvedEventBus = eventBus ?? createEventBus();
|
||||
const runtime = createExtensionRuntime();
|
||||
|
||||
for (const extPath of paths) {
|
||||
const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
|
||||
const results = await Promise.all(
|
||||
paths.map((extPath) => loadExtension(extPath, cwd, resolvedEventBus, runtime)),
|
||||
);
|
||||
|
||||
const extensions: Extension[] = [];
|
||||
const errors: Array<{ path: string; error: string }> = [];
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const { extension, error } = results[i];
|
||||
if (error) {
|
||||
errors.push({ path: extPath, error });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
errors.push({ path: paths[i], error });
|
||||
} else if (extension) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,52 @@
|
|||
#!/usr/bin/env node
|
||||
// GSD Startup Loader
|
||||
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, resolve, join, delimiter } from 'path'
|
||||
import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync } from 'fs'
|
||||
|
||||
// Fast-path: handle --version/-v and --help/-h before importing any heavy
|
||||
// dependencies. This avoids loading the entire pi-coding-agent barrel import
|
||||
// (~1s) just to print a version string.
|
||||
const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const args = process.argv.slice(2)
|
||||
const firstArg = args[0]
|
||||
|
||||
if (firstArg === '--version' || firstArg === '-v') {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'))
|
||||
process.stdout.write((pkg.version || '0.0.0') + '\n')
|
||||
} catch {
|
||||
process.stdout.write('0.0.0\n')
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (firstArg === '--help' || firstArg === '-h') {
|
||||
let version = '0.0.0'
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'))
|
||||
version = pkg.version || version
|
||||
} catch { /* ignore */ }
|
||||
process.stdout.write(`GSD v${version} — Get Shit Done\n\n`)
|
||||
process.stdout.write('Usage: gsd [options] [message...]\n\n')
|
||||
process.stdout.write('Options:\n')
|
||||
process.stdout.write(' --mode <text|json|rpc> Output mode (default: interactive)\n')
|
||||
process.stdout.write(' --print, -p Single-shot print mode\n')
|
||||
process.stdout.write(' --continue, -c Resume the most recent session\n')
|
||||
process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n')
|
||||
process.stdout.write(' --no-session Disable session persistence\n')
|
||||
process.stdout.write(' --extension <path> Load additional extension\n')
|
||||
process.stdout.write(' --tools <a,b,c> Restrict available tools\n')
|
||||
process.stdout.write(' --list-models [search] List available models and exit\n')
|
||||
process.stdout.write(' --version, -v Print version and exit\n')
|
||||
process.stdout.write(' --help, -h Print this help and exit\n')
|
||||
process.stdout.write('\nSubcommands:\n')
|
||||
process.stdout.write(' config Re-run the setup wizard\n')
|
||||
process.stdout.write(' update Update GSD to the latest version\n')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
import { agentDir, appRoot } from './app-paths.js'
|
||||
import { serializeBundledExtensionPaths } from './bundled-extension-paths.js'
|
||||
import { renderLogo } from './logo.js'
|
||||
|
|
@ -46,7 +91,6 @@ process.env.GSD_CODING_AGENT_DIR = agentDir
|
|||
// Without this, extensions (e.g. browser-tools) can't resolve dependencies like
|
||||
// `playwright` because jiti resolves modules from pi-coding-agent's location, not gsd's.
|
||||
// Prepending gsd's node_modules to NODE_PATH fixes this for all extensions.
|
||||
const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const gsdNodeModules = join(gsdRoot, 'node_modules')
|
||||
process.env.NODE_PATH = [gsdNodeModules, process.env.NODE_PATH]
|
||||
.filter(Boolean)
|
||||
|
|
@ -72,9 +116,8 @@ process.env.GSD_BIN_PATH = process.argv[1]
|
|||
// GSD_WORKFLOW_PATH — absolute path to bundled GSD-WORKFLOW.md, used by patched gsd extension
|
||||
// when dispatching workflow prompts. Prefers dist/resources/ (stable, set at build time)
|
||||
// over src/resources/ (live working tree) — see resource-loader.ts for rationale.
|
||||
const loaderPackageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const distRes = join(loaderPackageRoot, 'dist', 'resources')
|
||||
const srcRes = join(loaderPackageRoot, 'src', 'resources')
|
||||
const distRes = join(gsdRoot, 'dist', 'resources')
|
||||
const srcRes = join(gsdRoot, 'src', 'resources')
|
||||
const resourcesDir = existsSync(distRes) ? distRes : srcRes
|
||||
process.env.GSD_WORKFLOW_PATH = join(resourcesDir, 'GSD-WORKFLOW.md')
|
||||
|
||||
|
|
@ -116,8 +159,11 @@ process.env.GSD_BUNDLED_EXTENSION_PATHS = serializeBundledExtensionPaths(discove
|
|||
// Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars for all outbound requests.
|
||||
// pi-coding-agent's cli.ts sets this, but GSD bypasses that entry point — so we
|
||||
// must set it here before any SDK clients are created.
|
||||
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent())
|
||||
// Lazy-load undici (~200ms) only when proxy env vars are actually set.
|
||||
if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy) {
|
||||
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici')
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent())
|
||||
}
|
||||
|
||||
// Ensure workspace packages are linked before importing cli.js (which imports @gsd/*).
|
||||
// npm postinstall handles this normally, but npx --ignore-scripts skips postinstall.
|
||||
|
|
|
|||
|
|
@ -126,21 +126,29 @@ export function getNewerManagedResourceVersion(agentDir: string, currentVersion:
|
|||
/**
|
||||
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
||||
*
|
||||
* - extensions/ → ~/.gsd/agent/extensions/ (always overwrite — ensures updates ship on next launch)
|
||||
* - agents/ → ~/.gsd/agent/agents/ (always overwrite)
|
||||
* - skills/ → ~/.gsd/agent/skills/ (always overwrite)
|
||||
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
||||
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
||||
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
||||
* - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
|
||||
*
|
||||
* Always-overwrite ensures `npm update -g @glittercowboy/gsd` takes effect immediately.
|
||||
* User customizations should go in ~/.gsd/agent/extensions/ subdirs with unique names,
|
||||
* not by editing the gsd-managed files.
|
||||
* Skips the copy when the managed-resources.json version matches the current
|
||||
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
||||
* After `npm update -g @glittercowboy/gsd`, versions will differ and the
|
||||
* copy runs once to land the new resources.
|
||||
*
|
||||
* Inspectable: `ls ~/.gsd/agent/extensions/`
|
||||
*/
|
||||
export function initResources(agentDir: string): void {
|
||||
mkdirSync(agentDir, { recursive: true })
|
||||
|
||||
// Sync extensions — always overwrite so updates land on next launch
|
||||
// Skip resource sync when versions match — saves ~128ms of cpSync per launch
|
||||
const currentVersion = getBundledGsdVersion()
|
||||
const managedVersion = readManagedResourceVersion(agentDir)
|
||||
if (managedVersion && managedVersion === currentVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sync extensions — overwrite so updates land on next launch
|
||||
const destExtensions = join(agentDir, 'extensions')
|
||||
cpSync(bundledExtensionsDir, destExtensions, { recursive: true, force: true })
|
||||
|
||||
|
|
@ -151,7 +159,7 @@ export function initResources(agentDir: string): void {
|
|||
cpSync(srcAgents, destAgents, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
// Sync skills — always overwrite so updates land on next launch
|
||||
// Sync skills — overwrite so updates land on next launch
|
||||
const destSkills = join(agentDir, 'skills')
|
||||
const srcSkills = join(resourcesDir, 'skills')
|
||||
if (existsSync(srcSkills)) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue