From d1b6a8a6b1970d03b002b0f41e4f6f93567b8ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sat, 21 Mar 2026 09:19:48 -0600 Subject: [PATCH] fix: prevent getLoadedSkills crash and auto-build workspace packages (#1767) Add defensive fallback in auto-prompts.ts so a missing getLoadedSkills export degrades gracefully (empty skill list) instead of crashing every auto-mode dispatch iteration. Add ensure-workspace-builds.cjs postinstall script that detects missing dist/ directories in workspace packages and rebuilds them automatically. This prevents stale-build issues after fresh clones where dist/ is gitignored but required at runtime by jiti-loaded extensions. Closes #1734 Co-authored-by: Claude Opus 4.6 (1M context) --- package.json | 2 +- scripts/ensure-workspace-builds.cjs | 58 ++++++++++++++++++++ src/resources/extensions/gsd/auto-prompts.ts | 2 +- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 scripts/ensure-workspace-builds.cjs diff --git a/package.json b/package.json index 7c7624ac2..a93770648 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "build:native": "node native/scripts/build.js", "build:native:dev": "node native/scripts/build.js --dev", "dev": "node scripts/dev.js", - "postinstall": "node scripts/link-workspace-packages.cjs && node scripts/postinstall.js", + "postinstall": "node scripts/link-workspace-packages.cjs && node scripts/ensure-workspace-builds.cjs && node scripts/postinstall.js", "pi:install-global": "node scripts/install-pi-global.js", "pi:uninstall-global": "node scripts/uninstall-pi-global.js", "sync-pkg-version": "node scripts/sync-pkg-version.cjs", diff --git a/scripts/ensure-workspace-builds.cjs b/scripts/ensure-workspace-builds.cjs new file mode 100644 index 000000000..ddbba3488 --- /dev/null +++ b/scripts/ensure-workspace-builds.cjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +/** + * ensure-workspace-builds.cjs + * + * Checks whether workspace packages have been compiled (dist/ exists with + * index.js). If any are missing, runs the build for those packages. + * + * Designed for the postinstall hook so that `npm install` in a fresh clone + * produces a working runtime without a manual `npm run build` step. + * + * Skipped in CI (where the full build pipeline handles this) and when + * installing as an end-user dependency (no packages/ directory). + */ +const { existsSync } = require('fs') +const { resolve, join } = require('path') +const { execSync } = require('child_process') + +const root = resolve(__dirname, '..') +const packagesDir = join(root, 'packages') + +// Skip if packages/ doesn't exist (published tarball / end-user install) +if (!existsSync(packagesDir)) process.exit(0) + +// Skip in CI — the pipeline runs `npm run build` explicitly +if (process.env.CI === 'true' || process.env.CI === '1') process.exit(0) + +// Workspace packages that need dist/index.js at runtime. +// Order matters: dependencies must build before dependents. +const WORKSPACE_PACKAGES = [ + 'native', + 'pi-tui', + 'pi-ai', + 'pi-agent-core', + 'pi-coding-agent', +] + +const missing = [] +for (const pkg of WORKSPACE_PACKAGES) { + const distIndex = join(packagesDir, pkg, 'dist', 'index.js') + if (!existsSync(distIndex)) { + missing.push(pkg) + } +} + +if (missing.length === 0) process.exit(0) + +process.stderr.write(` Building ${missing.length} workspace package(s) missing dist/: ${missing.join(', ')}\n`) + +for (const pkg of missing) { + const pkgDir = join(packagesDir, pkg) + try { + execSync('npm run build', { cwd: pkgDir, stdio: 'pipe' }) + process.stderr.write(` ✓ ${pkg}\n`) + } catch (err) { + process.stderr.write(` ✗ ${pkg} build failed: ${err.message}\n`) + // Non-fatal — the user can run `npm run build` manually + } +} diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index 9cae54994..f891039f9 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -424,7 +424,7 @@ export function buildSkillActivationBlock(params: { params.taskPlanContent ?? undefined, ); - const visibleSkills = getLoadedSkills().filter(skill => !skill.disableModelInvocation); + const visibleSkills = (typeof getLoadedSkills === 'function' ? getLoadedSkills() : []).filter(skill => !skill.disableModelInvocation); const installedNames = new Set(visibleSkills.map(skill => normalizeSkillReference(skill.name))); const avoided = new Set(resolvePreferenceSkillNames(prefs?.avoid_skills ?? [], params.base)); const matched = new Set();