beth-copilot 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +121 -132
  3. package/assets/beth-questioning.png +0 -0
  4. package/assets/yellowstone-beth.png +0 -0
  5. package/bin/cli.js +359 -445
  6. package/dist/__tests__/inject-skills.test.d.ts +9 -0
  7. package/dist/__tests__/inject-skills.test.d.ts.map +1 -0
  8. package/dist/__tests__/inject-skills.test.js +143 -0
  9. package/dist/__tests__/inject-skills.test.js.map +1 -0
  10. package/dist/__tests__/skills/disambiguation.test.d.ts +10 -0
  11. package/dist/__tests__/skills/disambiguation.test.d.ts.map +1 -0
  12. package/dist/__tests__/skills/disambiguation.test.js +192 -0
  13. package/dist/__tests__/skills/disambiguation.test.js.map +1 -0
  14. package/dist/__tests__/skills/hook-injection.test.d.ts +11 -0
  15. package/dist/__tests__/skills/hook-injection.test.d.ts.map +1 -0
  16. package/dist/__tests__/skills/hook-injection.test.js +173 -0
  17. package/dist/__tests__/skills/hook-injection.test.js.map +1 -0
  18. package/dist/__tests__/skills/mapping-completeness.test.d.ts +17 -0
  19. package/dist/__tests__/skills/mapping-completeness.test.d.ts.map +1 -0
  20. package/dist/__tests__/skills/mapping-completeness.test.js +281 -0
  21. package/dist/__tests__/skills/mapping-completeness.test.js.map +1 -0
  22. package/dist/__tests__/skills/pipeline-integration.test.d.ts +18 -0
  23. package/dist/__tests__/skills/pipeline-integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/skills/pipeline-integration.test.js +234 -0
  25. package/dist/__tests__/skills/pipeline-integration.test.js.map +1 -0
  26. package/dist/__tests__/skills/skill-routing.test.d.ts +15 -0
  27. package/dist/__tests__/skills/skill-routing.test.d.ts.map +1 -0
  28. package/dist/__tests__/skills/skill-routing.test.js +723 -0
  29. package/dist/__tests__/skills/skill-routing.test.js.map +1 -0
  30. package/dist/__tests__/skills/trigger-coverage.test.d.ts +24 -0
  31. package/dist/__tests__/skills/trigger-coverage.test.d.ts.map +1 -0
  32. package/dist/__tests__/skills/trigger-coverage.test.js +746 -0
  33. package/dist/__tests__/skills/trigger-coverage.test.js.map +1 -0
  34. package/dist/__tests__/smoke.test.js +13 -0
  35. package/dist/__tests__/smoke.test.js.map +1 -1
  36. package/dist/__tests__/verify-skills.test.d.ts +9 -0
  37. package/dist/__tests__/verify-skills.test.d.ts.map +1 -0
  38. package/dist/__tests__/verify-skills.test.js +78 -0
  39. package/dist/__tests__/verify-skills.test.js.map +1 -0
  40. package/dist/cli/commands/beads.e2e.test.d.ts +4 -2
  41. package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -1
  42. package/dist/cli/commands/beads.e2e.test.js +97 -38
  43. package/dist/cli/commands/beads.e2e.test.js.map +1 -1
  44. package/dist/cli/commands/cli-edge-cases.e2e.test.js +1 -1
  45. package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -1
  46. package/dist/cli/commands/close.d.ts +11 -46
  47. package/dist/cli/commands/close.d.ts.map +1 -1
  48. package/dist/cli/commands/close.e2e.test.d.ts +4 -20
  49. package/dist/cli/commands/close.e2e.test.d.ts.map +1 -1
  50. package/dist/cli/commands/close.e2e.test.js +23 -204
  51. package/dist/cli/commands/close.e2e.test.js.map +1 -1
  52. package/dist/cli/commands/close.js +26 -240
  53. package/dist/cli/commands/close.js.map +1 -1
  54. package/dist/cli/commands/close.test.d.ts +7 -9
  55. package/dist/cli/commands/close.test.d.ts.map +1 -1
  56. package/dist/cli/commands/close.test.js +44 -424
  57. package/dist/cli/commands/close.test.js.map +1 -1
  58. package/dist/cli/commands/doctor.d.ts +16 -22
  59. package/dist/cli/commands/doctor.d.ts.map +1 -1
  60. package/dist/cli/commands/doctor.e2e.test.js +3 -59
  61. package/dist/cli/commands/doctor.e2e.test.js.map +1 -1
  62. package/dist/cli/commands/doctor.js +87 -103
  63. package/dist/cli/commands/doctor.js.map +1 -1
  64. package/dist/cli/commands/doctor.test.js +120 -229
  65. package/dist/cli/commands/doctor.test.js.map +1 -1
  66. package/dist/cli/commands/framework-isolation.test.d.ts +1 -1
  67. package/dist/cli/commands/framework-isolation.test.js +2 -3
  68. package/dist/cli/commands/framework-isolation.test.js.map +1 -1
  69. package/dist/cli/commands/help.e2e.test.js +1 -5
  70. package/dist/cli/commands/help.e2e.test.js.map +1 -1
  71. package/dist/cli/commands/init-logic.e2e.test.js +114 -2
  72. package/dist/cli/commands/init-logic.e2e.test.js.map +1 -1
  73. package/dist/cli/commands/init.test.js +4 -21
  74. package/dist/cli/commands/init.test.js.map +1 -1
  75. package/dist/cli/commands/land.d.ts +3 -15
  76. package/dist/cli/commands/land.d.ts.map +1 -1
  77. package/dist/cli/commands/land.js +13 -68
  78. package/dist/cli/commands/land.js.map +1 -1
  79. package/dist/cli/commands/land.test.d.ts +0 -1
  80. package/dist/cli/commands/land.test.d.ts.map +1 -1
  81. package/dist/cli/commands/land.test.js +2 -57
  82. package/dist/cli/commands/land.test.js.map +1 -1
  83. package/dist/cli/commands/mcp.e2e.test.js +28 -3
  84. package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
  85. package/dist/cli/commands/pipeline.e2e.test.js +23 -26
  86. package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
  87. package/dist/cli/commands/pre-push-guard.d.ts +2 -12
  88. package/dist/cli/commands/pre-push-guard.d.ts.map +1 -1
  89. package/dist/cli/commands/pre-push-guard.e2e.test.js +1 -1
  90. package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -1
  91. package/dist/cli/commands/pre-push-guard.js +2 -47
  92. package/dist/cli/commands/pre-push-guard.js.map +1 -1
  93. package/dist/cli/commands/pre-push-guard.test.d.ts +0 -1
  94. package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -1
  95. package/dist/cli/commands/pre-push-guard.test.js +15 -98
  96. package/dist/cli/commands/pre-push-guard.test.js.map +1 -1
  97. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +1 -1
  98. package/dist/cli/commands/quickstart-expanded.e2e.test.js +3 -30
  99. package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -1
  100. package/dist/cli/commands/quickstart.d.ts +0 -1
  101. package/dist/cli/commands/quickstart.d.ts.map +1 -1
  102. package/dist/cli/commands/quickstart.js +2 -60
  103. package/dist/cli/commands/quickstart.js.map +1 -1
  104. package/dist/cli/commands/quickstart.test.js +10 -104
  105. package/dist/cli/commands/quickstart.test.js.map +1 -1
  106. package/dist/cli/commands/uninstall.test.d.ts +5 -0
  107. package/dist/cli/commands/uninstall.test.d.ts.map +1 -0
  108. package/dist/cli/commands/uninstall.test.js +223 -0
  109. package/dist/cli/commands/uninstall.test.js.map +1 -0
  110. package/dist/cli/commands/update.d.ts +35 -0
  111. package/dist/cli/commands/update.d.ts.map +1 -0
  112. package/dist/cli/commands/update.e2e.test.d.ts +24 -0
  113. package/dist/cli/commands/update.e2e.test.d.ts.map +1 -0
  114. package/dist/cli/commands/update.e2e.test.js +238 -0
  115. package/dist/cli/commands/update.e2e.test.js.map +1 -0
  116. package/dist/cli/commands/update.js +255 -0
  117. package/dist/cli/commands/update.js.map +1 -0
  118. package/dist/core/agents/frontmatter.test.js +1 -1
  119. package/dist/core/agents/frontmatter.test.js.map +1 -1
  120. package/dist/core/agents/handoffs.test.js +1 -1
  121. package/dist/core/agents/handoffs.test.js.map +1 -1
  122. package/dist/core/agents/loader.d.ts +4 -2
  123. package/dist/core/agents/loader.d.ts.map +1 -1
  124. package/dist/core/agents/loader.js +5 -3
  125. package/dist/core/agents/loader.js.map +1 -1
  126. package/dist/core/agents/loader.test.js +42 -4
  127. package/dist/core/agents/loader.test.js.map +1 -1
  128. package/dist/core/agents/suite.test.js +8 -7
  129. package/dist/core/agents/suite.test.js.map +1 -1
  130. package/dist/core/agents/tools.test.js +10 -8
  131. package/dist/core/agents/tools.test.js.map +1 -1
  132. package/dist/core/agents/types.test.js +1 -1
  133. package/dist/core/agents/types.test.js.map +1 -1
  134. package/dist/core/skills/loader.test.js +1 -1
  135. package/dist/core/skills/loader.test.js.map +1 -1
  136. package/dist/index.d.ts +0 -1
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +0 -2
  139. package/dist/index.js.map +1 -1
  140. package/dist/lib/pathValidation.d.ts +0 -5
  141. package/dist/lib/pathValidation.d.ts.map +1 -1
  142. package/dist/lib/pathValidation.js +0 -11
  143. package/dist/lib/pathValidation.js.map +1 -1
  144. package/dist/lib/pathValidation.test.js +2 -14
  145. package/dist/lib/pathValidation.test.js.map +1 -1
  146. package/package.json +3 -6
  147. package/sbom.json +259 -371
  148. package/templates/.github/agents/beth.agent.md +194 -122
  149. package/templates/.github/agents/developer.agent.md +30 -22
  150. package/templates/.github/agents/product-manager.agent.md +15 -6
  151. package/templates/.github/agents/researcher.agent.md +10 -7
  152. package/templates/.github/agents/security-reviewer.agent.md +16 -7
  153. package/templates/.github/agents/tester.agent.md +16 -8
  154. package/templates/.github/agents/ux-designer.agent.md +12 -9
  155. package/templates/.github/copilot-instructions.md +33 -4
  156. package/templates/.github/copilot-mcp-config.json +12 -0
  157. package/templates/.github/dependabot.yml +68 -0
  158. package/templates/.github/hooks/scripts/inject-skills.mjs +139 -0
  159. package/templates/.github/hooks/scripts/verify-skills.mjs +47 -0
  160. package/templates/.github/hooks/skill-enforcement.json +18 -0
  161. package/templates/.github/pull_request_template.md +48 -0
  162. package/templates/.github/skills/framer-components/SKILL.md +0 -0
  163. package/templates/.github/skills/prd/SKILL.md +0 -0
  164. package/templates/.github/skills/security-analysis/SKILL.md +798 -798
  165. package/templates/.github/skills/shadcn-ui/SKILL.md +561 -561
  166. package/templates/.github/skills/vercel-react-best-practices/AGENTS.md +0 -0
  167. package/templates/.github/skills/vercel-react-best-practices/SKILL.md +0 -0
  168. package/templates/.github/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -0
  169. package/templates/.github/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -0
  170. package/templates/.github/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -0
  171. package/templates/.github/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -0
  172. package/templates/.github/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -0
  173. package/templates/.github/skills/vercel-react-best-practices/rules/async-parallel.md +0 -0
  174. package/templates/.github/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -0
  175. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -0
  176. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -0
  177. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -0
  178. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -0
  179. package/templates/.github/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -0
  180. package/templates/.github/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -0
  181. package/templates/.github/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -0
  182. package/templates/.github/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -0
  183. package/templates/.github/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -0
  184. package/templates/.github/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -0
  185. package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -0
  186. package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -0
  187. package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -0
  188. package/templates/.github/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -0
  189. package/templates/.github/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -0
  190. package/templates/.github/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -0
  191. package/templates/.github/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -0
  192. package/templates/.github/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -0
  193. package/templates/.github/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -0
  194. package/templates/.github/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -0
  195. package/templates/.github/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -0
  196. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -0
  197. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -0
  198. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -0
  199. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -0
  200. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -0
  201. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -0
  202. package/templates/.github/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -0
  203. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -0
  204. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -0
  205. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -0
  206. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -0
  207. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -0
  208. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -0
  209. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -0
  210. package/templates/.github/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -0
  211. package/templates/.github/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -0
  212. package/templates/.github/skills/vercel-react-best-practices/rules/server-auth-actions.md +0 -0
  213. package/templates/.github/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -0
  214. package/templates/.github/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -0
  215. package/templates/.github/skills/vercel-react-best-practices/rules/server-dedup-props.md +0 -0
  216. package/templates/.github/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -0
  217. package/templates/.github/skills/vercel-react-best-practices/rules/server-serialization.md +0 -0
  218. package/templates/.github/skills/web-design-guidelines/SKILL.md +0 -0
  219. package/templates/.vscode/settings.json +16 -16
  220. package/templates/AGENTS.md +59 -98
  221. package/templates/Backlog.md +80 -80
  222. package/templates/mcp.json.example +8 -0
  223. package/assets/beth-portrait-small.txt +0 -13
  224. package/assets/beth-portrait.txt +0 -60
  225. package/bin/beth-animation.sh +0 -155
  226. package/bin/lib/animation.js +0 -189
  227. package/bin/lib/pathValidation.js +0 -233
  228. package/bin/lib/pathValidation.test.js +0 -280
package/bin/cli.js CHANGED
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { fileURLToPath } from 'url';
4
- import { dirname, join, relative } from 'path';
5
- import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
4
+ import { basename, dirname, join, relative } from 'path';
5
+ import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, chmodSync, rmSync } from 'fs';
6
6
  import { createRequire } from 'module';
7
- import { execSync, spawn } from 'child_process';
8
- import { validateBeadsPath, validateBinaryPath } from './lib/pathValidation.js';
7
+ import { execSync, execFileSync, spawn } from 'child_process';
9
8
 
10
9
  const require = createRequire(import.meta.url);
11
10
  const __filename = fileURLToPath(import.meta.url);
@@ -227,18 +226,12 @@ async function animateBethBanner() {
227
226
  console.log('"' + COLORS.reset);
228
227
  console.log('');
229
228
 
230
- // Show version and quick help
229
+ // Show version line only — commands are shown after install completes
231
230
  console.log(`${COLORS.dim}v${CURRENT_VERSION}${COLORS.reset} ${COLORS.dim}AI Orchestrator for GitHub Copilot${COLORS.reset}`);
232
231
  console.log('');
233
- console.log(`${COLORS.bright}Commands:${COLORS.reset}`);
234
- console.log(` ${COLORS.cyan}npx beth-copilot init${COLORS.reset} Install Beth in your project`);
235
- console.log(` ${COLORS.cyan}npx beth-copilot help${COLORS.reset} Show full documentation`);
236
- console.log('');
237
- console.log(`${COLORS.bright}After install:${COLORS.reset} Open VS Code → Copilot Chat → ${COLORS.cyan}@Beth${COLORS.reset}`);
238
- console.log('');
239
232
  }
240
233
 
241
- function showBethBannerStatic({ showQuickHelp = true } = {}) {
234
+ function showBethBannerStatic() {
242
235
  const bethColors = [
243
236
  '\x1b[38;5;196m',
244
237
  '\x1b[38;5;202m',
@@ -277,17 +270,9 @@ function showBethBannerStatic({ showQuickHelp = true } = {}) {
277
270
  console.log(COLORS.cyan + COLORS.bright + '"' + tagline + '"' + COLORS.reset);
278
271
  console.log('');
279
272
 
280
- // Show version and quick help (optional)
281
- if (showQuickHelp) {
282
- console.log(`${COLORS.dim}v${CURRENT_VERSION}${COLORS.reset} ${COLORS.dim}AI Orchestrator for GitHub Copilot${COLORS.reset}`);
283
- console.log('');
284
- console.log(`${COLORS.bright}Commands:${COLORS.reset}`);
285
- console.log(` ${COLORS.cyan}npx beth-copilot init${COLORS.reset} Install Beth in your project`);
286
- console.log(` ${COLORS.cyan}npx beth-copilot help${COLORS.reset} Show full documentation`);
287
- console.log('');
288
- console.log(`${COLORS.bright}After install:${COLORS.reset} Open VS Code → Copilot Chat → ${COLORS.cyan}@Beth${COLORS.reset}`);
289
- console.log('');
290
- }
273
+ // Show version (always)
274
+ console.log(`${COLORS.dim}v${CURRENT_VERSION}${COLORS.reset} ${COLORS.dim}AI Orchestrator for GitHub Copilot${COLORS.reset}`);
275
+ console.log('');
291
276
  }
292
277
 
293
278
  // Compact Beth portrait with colors
@@ -518,59 +503,6 @@ async function checkForUpdates() {
518
503
  }
519
504
  }
520
505
 
521
- function getBeadsPath() {
522
- // Check if bd is available in PATH
523
- try {
524
- logDebug('Checking if bd is in PATH...');
525
- execSync('bd --version', { stdio: 'ignore' });
526
- logDebug('Found bd in PATH');
527
- return 'bd';
528
- } catch {
529
- logDebug('bd not in PATH, checking common locations...');
530
- // Check common installation paths based on platform
531
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
532
- const isWindows = process.platform === 'win32';
533
-
534
- const commonPaths = isWindows ? [
535
- // Windows: npm global, Go bin, local apps
536
- join(process.env.APPDATA || '', 'npm', 'bd.cmd'),
537
- join(homeDir, 'AppData', 'Roaming', 'npm', 'bd.cmd'),
538
- join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps', 'bd.exe'),
539
- join(homeDir, 'go', 'bin', 'bd.exe'),
540
- join(process.env.GOPATH || join(homeDir, 'go'), 'bin', 'bd.exe'),
541
- ] : [
542
- // Unix: homebrew, npm global, go bin, local bin
543
- '/opt/homebrew/bin/bd',
544
- '/usr/local/bin/bd',
545
- join(homeDir, '.local', 'bin', 'bd'),
546
- join(homeDir, 'bin', 'bd'),
547
- join(homeDir, '.npm-global', 'bin', 'bd'),
548
- join(homeDir, 'go', 'bin', 'bd'),
549
- join(process.env.GOPATH || join(homeDir, 'go'), 'bin', 'bd'),
550
- ];
551
-
552
- for (const bdPath of commonPaths) {
553
- logDebug(`Checking: ${bdPath}`);
554
- if (existsSync(bdPath)) {
555
- logDebug(`Found at: ${bdPath}`);
556
- return bdPath;
557
- }
558
- }
559
-
560
- logDebug('bd not found in any common location');
561
- return null;
562
- }
563
- }
564
-
565
- function isBeadsInstalled() {
566
- return getBeadsPath() !== null;
567
- }
568
-
569
- function isBeadsInitialized(cwd) {
570
- // Check if .beads directory exists in the project
571
- return existsSync(join(cwd, '.beads'));
572
- }
573
-
574
506
  async function promptYesNo(question) {
575
507
  const readline = await import('readline');
576
508
  const rl = readline.createInterface({
@@ -601,180 +533,6 @@ async function promptForInput(question) {
601
533
  });
602
534
  }
603
535
 
604
- /**
605
- * Installs the beads CLI globally via npm.
606
- *
607
- * SECURITY NOTE - shell:true usage:
608
- * - Required for cross-platform npm execution (npm.cmd on Windows, npm on Unix)
609
- * - Arguments are HARDCODED - no user input is passed to the shell
610
- * - Command injection risk: NONE (no dynamic/user-supplied values)
611
- *
612
- * Alternative considered: Using platform-specific binary names (npm.cmd vs npm)
613
- * would eliminate shell:true but adds complexity and edge cases for non-standard installs.
614
- *
615
- * @returns {Promise<boolean>} True if installation succeeded and was verified
616
- */
617
- async function installBeads() {
618
- const isWindows = process.platform === 'win32';
619
- const isMac = process.platform === 'darwin';
620
-
621
- log('\nInstalling beads CLI via npm...', COLORS.cyan);
622
- logInfo('npm install -g @beads/bd');
623
-
624
- // SECURITY: shell:true is required for cross-platform npm execution.
625
- // All arguments are hardcoded constants - no user input reaches the shell.
626
- return new Promise((resolve) => {
627
- const child = spawn('npm', ['install', '-g', '@beads/bd'], {
628
- stdio: 'inherit',
629
- shell: true
630
- });
631
-
632
- child.on('close', (code) => {
633
- if (code === 0) {
634
- // CRITICAL: Verify installation actually worked before claiming success
635
- // npm can exit 0 even when the package isn't properly installed
636
- const verifiedPath = getBeadsPath();
637
- if (verifiedPath) {
638
- logSuccess('beads CLI installed and verified!');
639
- resolve(true);
640
- } else {
641
- logWarning('npm reported success but beads CLI not found in PATH.');
642
- logInfo('This can happen if npm global bin is not in your PATH.');
643
- if (globalThis.VERBOSE) {
644
- showPathDiagnostics();
645
- } else {
646
- logInfo('Run with --verbose for PATH diagnostics.');
647
- }
648
- console.log('');
649
- showBeadsAlternatives(isWindows, isMac);
650
- resolve(false);
651
- }
652
- } else {
653
- logError('npm install failed.');
654
- console.log('');
655
- showBeadsAlternatives(isWindows, isMac);
656
- resolve(false);
657
- }
658
- });
659
-
660
- child.on('error', () => {
661
- logError('Failed to run npm.');
662
- logInfo('Make sure npm is installed and in your PATH.');
663
- resolve(false);
664
- });
665
- });
666
- }
667
-
668
- function showBeadsAlternatives(isWindows, isMac) {
669
- logInfo('Alternative installation methods:');
670
- if (isWindows) {
671
- logInfo(' PowerShell: irm https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1 | iex');
672
- logInfo(' Go: go install github.com/steveyegge/beads/cmd/bd@latest');
673
- } else {
674
- if (isMac) {
675
- logInfo(' Homebrew: brew install beads');
676
- }
677
- logInfo(' Script: curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
678
- logInfo(' Go: go install github.com/steveyegge/beads/cmd/bd@latest');
679
- }
680
- logInfo('');
681
- logInfo('Learn more: https://github.com/steveyegge/beads');
682
- }
683
-
684
- /**
685
- * Initializes beads in the current project directory.
686
- *
687
- * SECURITY NOTE - shell:true usage:
688
- * - bdPath is validated via getBeadsPath() which only returns paths that:
689
- * 1. Pass execSync('bd --version') verification, OR
690
- * 2. Exist on disk (verified via existsSync) from a HARDCODED list of paths
691
- * - Arguments are HARDCODED ('init') - no user input is passed to the shell
692
- * - Command injection risk: LOW (bdPath is validated, no user input in args)
693
- *
694
- * The shell:true is used for PATH resolution consistency, though it could be
695
- * eliminated since we have an absolute path. Kept for consistency with other
696
- * spawn calls and to handle edge cases in shell script wrappers.
697
- *
698
- * @param {string} cwd - Current working directory (validated by caller)
699
- * @returns {Promise<boolean>} True if initialization succeeded
700
- */
701
- async function initializeBeads(cwd) {
702
- log('\nInitializing beads in project...', COLORS.cyan);
703
-
704
- const bdPath = getBeadsPath();
705
- if (!bdPath) {
706
- logWarning('Failed to initialize beads. Run manually: bd init');
707
- return false;
708
- }
709
-
710
- // SECURITY: bdPath is validated by getBeadsPath() (existsSync check).
711
- // Only 'init' argument is passed - no user input reaches the shell.
712
- return new Promise((resolve) => {
713
- const child = spawn(bdPath, ['init'], {
714
- stdio: 'inherit',
715
- shell: true,
716
- cwd
717
- });
718
-
719
- child.on('close', (code) => {
720
- if (code === 0) {
721
- logSuccess('beads initialized successfully!');
722
- resolve(true);
723
- } else {
724
- logWarning('Failed to initialize beads. Run manually: bd init');
725
- resolve(false);
726
- }
727
- });
728
-
729
- child.on('error', () => {
730
- logWarning('Failed to initialize beads. Run manually: bd init');
731
- resolve(false);
732
- });
733
- });
734
- }
735
-
736
- /**
737
- * Runs `bd doctor` to verify beads configuration health.
738
- *
739
- * SECURITY NOTE - shell:true usage:
740
- * - bdPath is validated via getBeadsPath() (same as initializeBeads)
741
- * - Arguments are HARDCODED ('doctor') - no user input is passed to the shell
742
- * - Command injection risk: LOW (bdPath is validated, no user input in args)
743
- *
744
- * @returns {Promise<boolean>} True if bd doctor passed
745
- */
746
- async function runBeadsDoctor() {
747
- log('\nRunning beads doctor to verify configuration...', COLORS.cyan);
748
-
749
- const bdPath = getBeadsPath();
750
- if (!bdPath) {
751
- logWarning('Cannot run beads doctor: bd not found.');
752
- return false;
753
- }
754
-
755
- return new Promise((resolve) => {
756
- const child = spawn(bdPath, ['doctor'], {
757
- stdio: 'inherit',
758
- shell: true,
759
- });
760
-
761
- child.on('close', (code) => {
762
- if (code === 0) {
763
- logSuccess('beads doctor passed!');
764
- resolve(true);
765
- } else {
766
- logWarning('beads doctor reported issues. Run "bd doctor" manually to investigate.');
767
- resolve(false);
768
- }
769
- });
770
-
771
- child.on('error', () => {
772
- logWarning('Failed to run beads doctor. Run "bd doctor" manually.');
773
- resolve(false);
774
- });
775
- });
776
- }
777
-
778
536
  const BETH_GUARD_BEGIN = '# --- BEGIN BETH GUARD ---';
779
537
  const BETH_GUARD_END = '# --- END BETH GUARD ---';
780
538
 
@@ -817,63 +575,48 @@ ${BETH_GUARD_END}
817
575
  `;
818
576
  }
819
577
 
820
- /**
821
- * Install the pre-push guard into .beads/hooks/pre-push.
822
- * Appends the guard section after the beads integration section.
823
- * Idempotent — skips if guard is already installed.
824
- *
825
- * @param {string} cwd - Project root directory
826
- */
827
- function installPrePushGuard(cwd) {
828
- const hookPath = join(cwd, '.beads', 'hooks', 'pre-push');
829
-
830
- if (!existsSync(hookPath)) {
831
- logWarning('Pre-push hook not found (.beads/hooks/pre-push). Skipping guard installation.');
832
- return;
833
- }
834
-
835
- const content = readFileSync(hookPath, 'utf-8');
836
-
837
- // Already installed?
838
- if (content.includes(BETH_GUARD_BEGIN)) {
839
- logSuccess('Pre-push branch guard already installed');
840
- return;
841
- }
842
-
843
- // Append guard after existing content
844
- const guardScript = generateGuardScript();
845
- writeFileSync(hookPath, content.trimEnd() + '\n' + guardScript, 'utf-8');
846
- logSuccess('Installed pre-push branch guard (blocks direct pushes to main)');
847
- }
848
-
849
578
  function showHelp() {
850
- showBethBannerStatic({ showQuickHelp: false });
579
+ showBethBannerStatic();
851
580
  console.log(`${COLORS.bright}Beth${COLORS.reset} - AI Orchestrator for GitHub Copilot
852
581
 
853
- ${COLORS.bright}Usage:${COLORS.reset}
854
- npx beth-copilot init [options] Initialize Beth in current directory
855
- npx beth-copilot doctor Check system health and dependencies
856
- npx beth-copilot close <id> [opts] Close issue with dependency enforcement
857
- npx beth-copilot land [opts] Automated session completion (test, commit, push)
858
- npx beth-copilot pre-push-guard Run branch discipline checks (used by git hook)
859
- npx beth-copilot quickstart Run init + doctor + beads setup
860
- npx beth-copilot help Show this help message
861
-
862
- ${COLORS.bright}Options:${COLORS.reset}
582
+ ${COLORS.bright}Commands:${COLORS.reset}
583
+ ${COLORS.cyan}npx beth-copilot init${COLORS.reset} [options] Initialize Beth in current directory
584
+ ${COLORS.cyan}npx beth-copilot update${COLORS.reset} [options] Update project files to latest templates
585
+ ${COLORS.cyan}npx beth-copilot doctor${COLORS.reset} Check system health and dependencies
586
+ ${COLORS.cyan}npx beth-copilot land${COLORS.reset} [options] Automated session completion (test, commit, push)
587
+ ${COLORS.cyan}npx beth-copilot quickstart${COLORS.reset} Run init + doctor
588
+ ${COLORS.cyan}npx beth-copilot pre-push-guard${COLORS.reset} Run branch discipline checks (used by git hook)
589
+ ${COLORS.cyan}npx beth-copilot uninstall${COLORS.reset} Remove all Beth files from current project
590
+ ${COLORS.cyan}npx beth-copilot help${COLORS.reset} Show this help message
591
+
592
+ ${COLORS.bright}Init Options:${COLORS.reset}
863
593
  --force Overwrite existing files
864
594
  --skip-backlog Don't create Backlog.md
865
595
  --skip-mcp Don't create mcp.json.example
866
- --skip-beads Skip beads check (not recommended)
867
596
  --verbose Show detailed diagnostics on errors
868
597
 
598
+ ${COLORS.bright}Update Options:${COLORS.reset}
599
+ --check-only Report update status without modifying files
600
+ --force Overwrite user-modified files with templates
601
+ --verbose Show per-file detail
602
+
603
+ ${COLORS.bright}Land Options:${COLORS.reset}
604
+ --message, -m <msg> Custom commit message
605
+ --skip-tests Skip test execution (not recommended)
606
+ --force, -f Push even if tests fail (dangerous)
607
+ --dry-run Show what would happen without executing
608
+
869
609
  ${COLORS.bright}Examples:${COLORS.reset}
870
610
  npx beth-copilot init Set up Beth in current project
871
611
  npx beth-copilot init --force Overwrite existing Beth files
612
+ npx beth-copilot update Update to latest templates
613
+ npx beth-copilot update --check-only See what changed without modifying
872
614
  npx beth-copilot doctor Verify installation health
615
+ npx beth-copilot land -m "feat: new component" Commit and push session work
873
616
 
874
617
  ${COLORS.bright}What gets installed:${COLORS.reset}
875
618
  .github/agents/ 7 specialized AI agents
876
- .github/skills/ 8 domain knowledge modules
619
+ .github/skills/ Domain knowledge modules
877
620
  .github/copilot-instructions.md Copilot configuration
878
621
  .vscode/settings.json Recommended VS Code settings
879
622
  AGENTS.md Workflow documentation
@@ -938,8 +681,46 @@ function copyDirRecursive(src, dest, options = {}) {
938
681
  return copiedFiles;
939
682
  }
940
683
 
684
+ /**
685
+ * Derive a task prefix from the project name.
686
+ * Uses package.json "name" field, falls back to directory name.
687
+ * Takes the first segment (split on - _ . space), lowercased, up to 6 letters.
688
+ */
689
+ function deriveTaskPrefix(cwd) {
690
+ let projectName = '';
691
+
692
+ // Try package.json name field
693
+ const pkgPath = join(cwd, 'package.json');
694
+ if (existsSync(pkgPath)) {
695
+ try {
696
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
697
+ if (pkg.name && typeof pkg.name === 'string') {
698
+ projectName = pkg.name;
699
+ }
700
+ } catch {
701
+ // Ignore parse errors — fall through to directory name
702
+ }
703
+ }
704
+
705
+ // Fall back to directory name
706
+ if (!projectName) {
707
+ projectName = basename(cwd);
708
+ }
709
+
710
+ // Strip npm scope (e.g. @scope/package -> package)
711
+ projectName = projectName.replace(/^@[^/]+\//, '');
712
+
713
+ // Split on common delimiters, take first segment
714
+ const firstSegment = projectName.split(/[-_. ]+/)[0] || '';
715
+
716
+ // Lowercase, keep only letters, take up to 6
717
+ const prefix = firstSegment.toLowerCase().replace(/[^a-z]/g, '').slice(0, 6);
718
+
719
+ return prefix || 'task'; // fallback to 'task' if nothing usable
720
+ }
721
+
941
722
  async function init(options = {}) {
942
- const { force = false, skipBacklog = false, skipMcp = false, skipBeads = false } = options;
723
+ const { force = false, skipBacklog = false, skipMcp = false } = options;
943
724
  const cwd = process.cwd();
944
725
 
945
726
  // Check for updates
@@ -957,11 +738,9 @@ ${COLORS.yellow}╔════════════════════
957
738
  if (canAnimate()) {
958
739
  await animateBethBanner();
959
740
  } else {
960
- showBethBannerStatic({ showQuickHelp: false });
741
+ showBethBannerStatic();
961
742
  }
962
743
 
963
- log(`${COLORS.yellow}Tip: Run with --verbose for detailed diagnostics if you hit issues.${COLORS.reset}`);
964
-
965
744
  // Check if templates exist
966
745
  if (!existsSync(TEMPLATES_DIR)) {
967
746
  logError('Templates directory not found. Package may be corrupted.');
@@ -1044,151 +823,76 @@ ${COLORS.yellow}╔════════════════════
1044
823
  }
1045
824
  }
1046
825
 
1047
- // Summary
1048
- console.log('');
1049
- if (copiedFiles.length > 0) {
1050
- logSuccess(`Installed ${copiedFiles.length} files:`);
1051
- copiedFiles.forEach(f => logInfo(f));
1052
- } else {
1053
- logWarning('No files were copied. Use --force to overwrite existing files.');
1054
- }
1055
-
1056
- // Check for beads CLI (REQUIRED for Beth)
1057
- if (!skipBeads) {
1058
- console.log('');
1059
- log('Checking beads (required for task tracking)...', COLORS.cyan);
1060
-
1061
- let bdPath = getBeadsPath();
1062
-
1063
- // Loop until beads is installed
1064
- while (!bdPath) {
1065
- logWarning('beads CLI is not installed.');
1066
- logInfo('Beth requires beads for task tracking. Agents use it to coordinate work.');
1067
- logInfo('Learn more: https://github.com/steveyegge/beads');
1068
- console.log('');
1069
-
1070
- const shouldInstallBeads = await promptYesNo('Install beads CLI now? (required)');
1071
- if (shouldInstallBeads) {
1072
- const installed = await installBeads();
1073
- if (installed) {
1074
- // Re-check for beads after installation
1075
- bdPath = getBeadsPath();
1076
- if (!bdPath) {
1077
- console.log('');
1078
- logWarning('beads installed but not found in common paths.');
1079
- logInfo('The installer may have placed it in a custom location.');
1080
- console.log('');
1081
- logInfo('Please try one of these options:');
1082
- logInfo(' 1. Open a NEW terminal and run: npx beth-copilot init');
1083
- logInfo(' 2. Add ~/.local/bin to your PATH and retry');
1084
- logInfo(' 3. Run: source ~/.bashrc (or ~/.zshrc) then retry');
1085
- console.log('');
1086
-
1087
- const retryCheck = await promptYesNo('Retry detection? (select No to enter path manually)');
1088
- if (retryCheck) {
1089
- bdPath = getBeadsPath();
1090
- continue;
1091
- }
1092
-
1093
- // Allow manual path entry
1094
- const customPath = await promptForInput('Enter full path to bd binary (or press Enter to retry installation):');
1095
- if (customPath) {
1096
- const validation = validateBeadsPath(customPath);
1097
- if (validation.valid) {
1098
- bdPath = validation.normalizedPath;
1099
- logSuccess(`Found beads at: ${bdPath}`);
1100
- } else {
1101
- logError(`Invalid path: ${validation.error}`);
1102
- }
1103
- }
1104
- }
1105
- } else {
1106
- console.log('');
1107
- logError('Installation script failed.');
1108
- logInfo('You can try installing manually:');
1109
- logInfo(' curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash');
1110
- console.log('');
1111
- }
1112
- } else {
1113
- console.log('');
1114
- logError('beads is REQUIRED for Beth to function.');
1115
- logInfo('Beth agents use beads to track tasks, dependencies, and coordinate work.');
1116
- logInfo('Without beads, the multi-agent workflow will not work correctly.');
1117
- console.log('');
1118
-
1119
- const tryAgain = await promptYesNo('Would you like to try installing beads?');
1120
- if (!tryAgain) {
1121
- logError('Cannot continue without beads. Exiting.');
1122
- logInfo('Install beads manually and run "npx beth-copilot init" again:');
1123
- logInfo(' npm install -g @beads/bd');
1124
- process.exit(1);
1125
- }
1126
- }
1127
- }
1128
-
1129
- // Show path info if not in standard PATH
1130
- if (bdPath && bdPath !== 'bd') {
1131
- logSuccess(`beads CLI found at: ${bdPath}`);
1132
- const isWindows = process.platform === 'win32';
1133
- if (isWindows) {
1134
- logInfo('Tip: Ensure npm global bin is in your PATH to use "bd" directly.');
1135
- } else {
1136
- logInfo('Tip: Add ~/.local/bin or npm global bin to your PATH to use "bd" directly.');
1137
- }
1138
- } else {
1139
- logSuccess('beads CLI is installed');
826
+ // Install .vscode/mcp.json with required MCP servers (unless --skip-mcp)
827
+ if (!skipMcp) {
828
+ const mcpJsonDest = join(cwd, '.vscode', 'mcp.json');
829
+ if (!existsSync(join(cwd, '.vscode'))) {
830
+ mkdirSync(join(cwd, '.vscode'), { recursive: true });
1140
831
  }
1141
832
 
1142
- // Initialize beads in the project if not already done
1143
- if (!isBeadsInitialized(cwd)) {
1144
- logInfo('beads not initialized in this project.');
1145
- let initialized = false;
1146
-
1147
- while (!initialized) {
1148
- const shouldInitBeads = await promptYesNo('Initialize beads now? (required)');
1149
- if (shouldInitBeads) {
1150
- initialized = await initializeBeads(cwd);
1151
- if (!initialized) {
1152
- logWarning('Initialization failed. Let\'s try again.');
1153
- }
833
+ if (existsSync(mcpJsonDest) && !force) {
834
+ // Verify existing mcp.json has the required servers
835
+ try {
836
+ const existing = JSON.parse(readFileSync(mcpJsonDest, 'utf-8'));
837
+ const missing = [];
838
+ if (!existing.servers?.playwright) missing.push('playwright');
839
+ if (!existing.servers?.backlog) missing.push('backlog');
840
+
841
+ if (missing.length > 0) {
842
+ logWarning(`.vscode/mcp.json exists but missing required servers: ${missing.join(', ')}`);
843
+ logInfo('Add them manually or run with --force to overwrite');
1154
844
  } else {
1155
- logError('beads must be initialized for Beth to work correctly.');
1156
- logInfo('The .beads directory stores task tracking data used by all agents.');
1157
- console.log('');
845
+ logSuccess('.vscode/mcp.json already has required MCP servers');
1158
846
  }
847
+ } catch {
848
+ logWarning('.vscode/mcp.json exists but could not be parsed — verify it manually');
1159
849
  }
1160
850
  } else {
1161
- logSuccess('beads is initialized in this project');
851
+ const mcpTemplateSrc = join(TEMPLATES_DIR, 'mcp.json.example');
852
+ if (existsSync(mcpTemplateSrc)) {
853
+ copyFileSync(mcpTemplateSrc, mcpJsonDest);
854
+ copiedFiles.push('.vscode/mcp.json');
855
+ }
1162
856
  }
1163
- } else {
1164
- logWarning('Skipped beads check (--skip-beads). Beth may not function correctly.');
1165
857
  }
1166
858
 
1167
- // Run bd doctor to verify beads configuration
1168
- if (!skipBeads && getBeadsPath() && isBeadsInitialized(cwd)) {
1169
- await runBeadsDoctor();
859
+ // Initialize Backlog.md project with derived task prefix (unless skipped)
860
+ if (!skipBacklog) {
861
+ const backlogConfigPath = join(cwd, 'backlog', 'config.yml');
862
+ if (!existsSync(backlogConfigPath) || force) {
863
+ const taskPrefix = deriveTaskPrefix(cwd);
864
+ const dirName = basename(cwd);
865
+ try {
866
+ execFileSync(
867
+ 'backlog',
868
+ ['init', dirName, '--defaults', '--task-prefix', taskPrefix.toUpperCase(), '--integration-mode', 'mcp', '--auto-open-browser', 'false', '--bypass-git-hooks', 'true'],
869
+ { cwd, stdio: 'pipe', encoding: 'utf-8' }
870
+ );
871
+ logSuccess(`Initialized Backlog.md with task prefix: ${taskPrefix.toUpperCase()}`);
872
+ copiedFiles.push('backlog/config.yml');
873
+ } catch (err) {
874
+ logWarning('Could not initialize Backlog.md — is the backlog CLI installed?');
875
+ logInfo('Install with: npm install -g backlog-md');
876
+ logDebug(err.message || String(err));
877
+ }
878
+ } else {
879
+ logSuccess('Backlog.md already initialized (backlog/config.yml exists)');
880
+ }
1170
881
  }
1171
882
 
1172
- // Install pre-push guard hook
1173
- if (!skipBeads && isBeadsInitialized(cwd)) {
1174
- installPrePushGuard(cwd);
883
+ // Summary
884
+ console.log('');
885
+ if (copiedFiles.length > 0) {
886
+ logSuccess(`Installed ${copiedFiles.length} files:`);
887
+ copiedFiles.forEach(f => logInfo(f));
888
+ } else {
889
+ logWarning('No files were copied. Use --force to overwrite existing files.');
1175
890
  }
1176
891
 
1177
892
  // Final verification
1178
893
  console.log('');
1179
894
  log('Verifying installation...', COLORS.cyan);
1180
-
1181
- const finalBeadsOk = skipBeads || getBeadsPath();
1182
- const finalBeadsInit = skipBeads || isBeadsInitialized(cwd);
1183
-
1184
- if (finalBeadsOk && finalBeadsInit) {
1185
- logSuccess('All dependencies installed and configured!');
1186
- } else {
1187
- if (!finalBeadsOk) logError('beads CLI not found');
1188
- if (!finalBeadsInit) logError('beads not initialized in project');
1189
- logError('Setup incomplete. Please resolve issues above and run init again.');
1190
- process.exit(1);
1191
- }
895
+ logSuccess('All files installed and configured!');
1192
896
 
1193
897
  // Next steps
1194
898
  console.log(`
@@ -1197,25 +901,226 @@ ${COLORS.bright}Next steps:${COLORS.reset}
1197
901
  2. Open Copilot Chat (${COLORS.cyan}Ctrl+Alt+I${COLORS.reset} / ${COLORS.cyan}Cmd+Alt+I${COLORS.reset})
1198
902
  3. Type ${COLORS.cyan}@Beth${COLORS.reset} to start - she's your orchestrator
1199
903
 
1200
- ${COLORS.bright}Pro tip:${COLORS.reset} Start every session with ${COLORS.cyan}@Beth${COLORS.reset} and let her route work to the right specialists.
904
+ ${COLORS.bright}Pro tip:${COLORS.reset} Start every session with ${COLORS.cyan}@Beth${COLORS.reset} and let her route work to the right specialists.`);
1201
905
 
1202
- ${COLORS.bright}Documentation:${COLORS.reset}
1203
- https://github.com/stephschofield/beth
1204
-
1205
- ${COLORS.cyan}"They broke my wings and forgot I had claws."${COLORS.reset}
906
+ // Commands at the bottom — easy to find and copy-paste
907
+ console.log(`
908
+ ${COLORS.bright}Commands:${COLORS.reset}
909
+ ${COLORS.cyan}npx beth-copilot update${COLORS.reset} Update Beth to the latest templates
910
+ ${COLORS.cyan}npx beth-copilot doctor${COLORS.reset} Check system health and dependencies
911
+ ${COLORS.cyan}npx beth-copilot land${COLORS.reset} Automated session completion (test, commit, push)
912
+ ${COLORS.cyan}npx beth-copilot help${COLORS.reset} Show all commands, options, and documentation
1206
913
  `);
914
+
915
+ console.log(`${COLORS.dim}Tip: Run with --verbose for detailed diagnostics if you hit issues.${COLORS.reset}`);
916
+ console.log(`${COLORS.dim}Documentation: https://github.com/stephschofield/beth${COLORS.reset}`);
917
+ console.log(`${COLORS.cyan}"They broke my wings and forgot I had claws."${COLORS.reset}`);
918
+ console.log('');
919
+ }
920
+
921
+ /**
922
+ * Uninstall Beth from the current project.
923
+ * Removes all files/directories that init installed.
924
+ */
925
+ async function uninstall() {
926
+ const cwd = process.cwd();
927
+ const args = process.argv.slice(3);
928
+ const forceFlag = args.includes('--force') || args.includes('-f');
929
+
930
+ showBethBannerStatic();
931
+
932
+ console.log(`${COLORS.bright}${COLORS.red}Uninstalling Beth...${COLORS.reset}\n`);
933
+
934
+ // Verify there's actually a Beth installation here
935
+ const githubDir = join(cwd, '.github');
936
+ const agentsDir = join(githubDir, 'agents');
937
+ const hasInstallation = existsSync(agentsDir) || existsSync(join(cwd, 'AGENTS.md'));
938
+
939
+ if (!hasInstallation) {
940
+ logWarning('No Beth installation detected in this directory.');
941
+ console.log('Are you in the right project? Beth installs into .github/agents/ and AGENTS.md.');
942
+ process.exit(0);
943
+ }
944
+
945
+ // --- Build the removal manifest ---
946
+ // Only remove files/dirs that Beth actually installs (from templates)
947
+
948
+ // Directories Beth owns entirely
949
+ const bethOwnedDirs = [
950
+ join(githubDir, 'agents'),
951
+ join(githubDir, 'skills'),
952
+ join(githubDir, 'hooks'),
953
+ ];
954
+
955
+ // Individual files Beth installs
956
+ const bethOwnedFiles = [
957
+ join(githubDir, 'copilot-instructions.md'),
958
+ join(githubDir, 'copilot-mcp-config.json'),
959
+ join(githubDir, 'pull_request_template.md'),
960
+ join(githubDir, 'dependabot.yml'),
961
+ join(cwd, 'AGENTS.md'),
962
+ join(cwd, 'Backlog.md'),
963
+ join(cwd, 'mcp.json.example'),
964
+ join(cwd, '.vscode', 'settings.json'),
965
+ join(cwd, '.vscode', 'mcp.json'),
966
+ ];
967
+
968
+ // Git pre-push hook (Beth appends a guard block)
969
+ const prePushHook = join(cwd, '.git', 'hooks', 'pre-push');
970
+
971
+ // Backlog.md directory (created by `backlog init`)
972
+ const backlogDir = join(cwd, 'backlog');
973
+
974
+ // --- Collect what actually exists ---
975
+ const dirsToRemove = bethOwnedDirs.filter(d => existsSync(d));
976
+ const filesToRemove = bethOwnedFiles.filter(f => existsSync(f));
977
+ const hasBacklogDir = existsSync(backlogDir);
978
+ const hasPrePushGuard = existsSync(prePushHook) && readFileSync(prePushHook, 'utf-8').includes(BETH_GUARD_BEGIN);
979
+
980
+ if (dirsToRemove.length === 0 && filesToRemove.length === 0 && !hasBacklogDir && !hasPrePushGuard) {
981
+ logWarning('Nothing to remove — Beth files have already been cleaned up.');
982
+ process.exit(0);
983
+ }
984
+
985
+ // --- Show what will be removed ---
986
+ console.log(`${COLORS.bright}The following will be removed:${COLORS.reset}\n`);
987
+
988
+ for (const dir of dirsToRemove) {
989
+ logInfo(`${relative(cwd, dir)}/ (directory)`);
990
+ }
991
+ for (const file of filesToRemove) {
992
+ logInfo(`${relative(cwd, file)}`);
993
+ }
994
+ if (hasBacklogDir) {
995
+ logInfo('backlog/ (directory — Backlog.md task data)');
996
+ }
997
+ if (hasPrePushGuard) {
998
+ logInfo('.git/hooks/pre-push (Beth guard block will be removed)');
999
+ }
1000
+
1001
+ console.log('');
1002
+
1003
+ // --- Confirm unless --force ---
1004
+ if (!forceFlag) {
1005
+ const confirmed = await promptYesNo(`${COLORS.yellow}Are you sure you want to remove Beth from this project?${COLORS.reset}`);
1006
+ if (!confirmed) {
1007
+ console.log('\nUninstall cancelled. Beth lives to fight another day.');
1008
+ process.exit(0);
1009
+ }
1010
+ }
1011
+
1012
+ console.log('');
1013
+ const removed = [];
1014
+
1015
+ // --- Remove directories ---
1016
+ for (const dir of dirsToRemove) {
1017
+ try {
1018
+ rmSync(dir, { recursive: true, force: true });
1019
+ removed.push(relative(cwd, dir) + '/');
1020
+ logSuccess(`Removed ${relative(cwd, dir)}/`);
1021
+ } catch (err) {
1022
+ logError(`Failed to remove ${relative(cwd, dir)}/: ${err.message}`);
1023
+ }
1024
+ }
1025
+
1026
+ // --- Remove files ---
1027
+ for (const file of filesToRemove) {
1028
+ try {
1029
+ unlinkSync(file);
1030
+ removed.push(relative(cwd, file));
1031
+ logSuccess(`Removed ${relative(cwd, file)}`);
1032
+ } catch (err) {
1033
+ logError(`Failed to remove ${relative(cwd, file)}: ${err.message}`);
1034
+ }
1035
+ }
1036
+
1037
+ // --- Remove backlog directory ---
1038
+ if (hasBacklogDir) {
1039
+ try {
1040
+ rmSync(backlogDir, { recursive: true, force: true });
1041
+ removed.push('backlog/');
1042
+ logSuccess('Removed backlog/');
1043
+ } catch (err) {
1044
+ logError(`Failed to remove backlog/: ${err.message}`);
1045
+ }
1046
+ }
1047
+
1048
+ // --- Clean pre-push hook ---
1049
+ if (hasPrePushGuard) {
1050
+ try {
1051
+ const hookContent = readFileSync(prePushHook, 'utf-8');
1052
+ const beginIdx = hookContent.indexOf(BETH_GUARD_BEGIN);
1053
+ const endIdx = hookContent.indexOf(BETH_GUARD_END);
1054
+
1055
+ if (beginIdx !== -1 && endIdx !== -1) {
1056
+ const cleaned = hookContent.slice(0, beginIdx) + hookContent.slice(endIdx + BETH_GUARD_END.length + 1);
1057
+ const trimmed = cleaned.trim();
1058
+
1059
+ if (trimmed === '' || trimmed === '#!/bin/sh' || trimmed === '#!/bin/bash') {
1060
+ // Hook is now empty — remove the whole file
1061
+ unlinkSync(prePushHook);
1062
+ logSuccess('Removed .git/hooks/pre-push (was Beth-only)');
1063
+ } else {
1064
+ writeFileSync(prePushHook, cleaned);
1065
+ logSuccess('Removed Beth guard block from .git/hooks/pre-push');
1066
+ }
1067
+ removed.push('.git/hooks/pre-push (guard block)');
1068
+ }
1069
+ } catch (err) {
1070
+ logError(`Failed to clean pre-push hook: ${err.message}`);
1071
+ }
1072
+ }
1073
+
1074
+ // --- Clean up empty parent directories ---
1075
+ // If .github/ is now empty, remove it
1076
+ if (existsSync(githubDir)) {
1077
+ try {
1078
+ const remaining = readdirSync(githubDir);
1079
+ if (remaining.length === 0) {
1080
+ rmSync(githubDir, { recursive: true, force: true });
1081
+ logSuccess('Removed empty .github/');
1082
+ }
1083
+ } catch {
1084
+ // Not critical
1085
+ }
1086
+ }
1087
+
1088
+ // If .vscode/ is now empty, remove it
1089
+ const vscodeDir = join(cwd, '.vscode');
1090
+ if (existsSync(vscodeDir)) {
1091
+ try {
1092
+ const remaining = readdirSync(vscodeDir);
1093
+ if (remaining.length === 0) {
1094
+ rmSync(vscodeDir, { recursive: true, force: true });
1095
+ logSuccess('Removed empty .vscode/');
1096
+ }
1097
+ } catch {
1098
+ // Not critical
1099
+ }
1100
+ }
1101
+
1102
+ // --- Summary ---
1103
+ console.log('');
1104
+ if (removed.length > 0) {
1105
+ logSuccess(`Removed ${removed.length} items. Beth has left the building.`);
1106
+ console.log(`\n${COLORS.dim}To reinstall: npx beth-copilot init${COLORS.reset}`);
1107
+ } else {
1108
+ logWarning('No items were removed. Check file permissions.');
1109
+ }
1110
+
1111
+ console.log(`\n${COLORS.cyan}"I'm not leaving. I'm choosing to go."${COLORS.reset}\n`);
1207
1112
  }
1208
1113
 
1209
1114
  // Input validation constants
1210
- const ALLOWED_COMMANDS = ['init', 'help', '--help', '-h', 'doctor', 'quickstart', 'close', 'pre-push-guard'];
1211
- const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--skip-beads', '--verbose', '--reason', '-r', '-f', '--skip-tests', '--skip-backup', '--message', '-m', '--dry-run'];
1115
+ const ALLOWED_COMMANDS = ['init', 'help', '--help', '-h', 'doctor', 'quickstart', 'pre-push-guard', 'update', 'land', 'uninstall'];
1116
+ const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--verbose', '--reason', '-r', '-f', '--skip-tests', '--message', '-m', '--dry-run', '--check-only'];
1212
1117
  const MAX_ARG_LENGTH = 50;
1213
1118
 
1214
1119
  // Validate and sanitize input
1215
1120
  function validateArgs(args) {
1216
- // The 'close' and 'land' commands handle their own arg validation
1121
+ // The 'land' and 'update' commands handle their own arg validation
1217
1122
  const command = args[0]?.toLowerCase();
1218
- if (command === 'close' || command === 'land') return;
1123
+ if (command === 'land' || command === 'update' || command === 'uninstall') return;
1219
1124
 
1220
1125
  for (const arg of args) {
1221
1126
  // Prevent excessively long arguments (log injection, DoS)
@@ -1241,7 +1146,6 @@ const options = {
1241
1146
  force: args.includes('--force'),
1242
1147
  skipBacklog: args.includes('--skip-backlog'),
1243
1148
  skipMcp: args.includes('--skip-mcp'),
1244
- skipBeads: args.includes('--skip-beads'),
1245
1149
  verbose: args.includes('--verbose'),
1246
1150
  };
1247
1151
 
@@ -1249,8 +1153,8 @@ const options = {
1249
1153
  globalThis.VERBOSE = options.verbose;
1250
1154
 
1251
1155
  // Validate unknown flags (exclude --help which is handled as a command)
1252
- // Skip for 'close' and 'land' commands which handle their own arg parsing
1253
- if (command !== 'close' && command !== 'land') {
1156
+ // Skip for 'land' and 'update' commands which handle their own arg parsing
1157
+ if (command !== 'land' && command !== 'update' && command !== 'uninstall') {
1254
1158
  const unknownFlags = args.filter(arg => arg.startsWith('--') && !ALLOWED_FLAGS.includes(arg) && arg !== '--help');
1255
1159
  if (unknownFlags.length > 0) {
1256
1160
  logError(`Unknown flag: ${unknownFlags[0].slice(0, MAX_ARG_LENGTH)}`);
@@ -1283,14 +1187,6 @@ switch (command) {
1283
1187
  await quickstart(options);
1284
1188
  }
1285
1189
  break;
1286
- case 'close':
1287
- {
1288
- const { close } = await loadTsCommand('close');
1289
- // Pass raw args after 'close' — the command handles its own parsing
1290
- const closeArgs = process.argv.slice(3);
1291
- await close(closeArgs);
1292
- }
1293
- break;
1294
1190
  case 'land':
1295
1191
  {
1296
1192
  const { land } = await loadTsCommand('land');
@@ -1299,12 +1195,30 @@ switch (command) {
1299
1195
  await land(landArgs);
1300
1196
  }
1301
1197
  break;
1198
+ case 'update':
1199
+ {
1200
+ const { update } = await loadTsCommand('update');
1201
+ const updateArgs = process.argv.slice(3);
1202
+ await update(updateArgs);
1203
+ }
1204
+ break;
1302
1205
  case 'pre-push-guard':
1303
1206
  {
1304
1207
  const { prePushGuard } = await loadTsCommand('pre-push-guard');
1305
1208
  await prePushGuard();
1306
1209
  }
1307
1210
  break;
1211
+ case 'uninstall':
1212
+ try {
1213
+ await uninstall();
1214
+ } catch (error) {
1215
+ if (error instanceof UserError) {
1216
+ showUserError(error);
1217
+ process.exit(1);
1218
+ }
1219
+ throw error;
1220
+ }
1221
+ break;
1308
1222
  case 'help':
1309
1223
  case '--help':
1310
1224
  case '-h':