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.
- package/CHANGELOG.md +51 -1
- package/README.md +121 -132
- package/assets/beth-questioning.png +0 -0
- package/assets/yellowstone-beth.png +0 -0
- package/bin/cli.js +359 -445
- package/dist/__tests__/inject-skills.test.d.ts +9 -0
- package/dist/__tests__/inject-skills.test.d.ts.map +1 -0
- package/dist/__tests__/inject-skills.test.js +143 -0
- package/dist/__tests__/inject-skills.test.js.map +1 -0
- package/dist/__tests__/skills/disambiguation.test.d.ts +10 -0
- package/dist/__tests__/skills/disambiguation.test.d.ts.map +1 -0
- package/dist/__tests__/skills/disambiguation.test.js +192 -0
- package/dist/__tests__/skills/disambiguation.test.js.map +1 -0
- package/dist/__tests__/skills/hook-injection.test.d.ts +11 -0
- package/dist/__tests__/skills/hook-injection.test.d.ts.map +1 -0
- package/dist/__tests__/skills/hook-injection.test.js +173 -0
- package/dist/__tests__/skills/hook-injection.test.js.map +1 -0
- package/dist/__tests__/skills/mapping-completeness.test.d.ts +17 -0
- package/dist/__tests__/skills/mapping-completeness.test.d.ts.map +1 -0
- package/dist/__tests__/skills/mapping-completeness.test.js +281 -0
- package/dist/__tests__/skills/mapping-completeness.test.js.map +1 -0
- package/dist/__tests__/skills/pipeline-integration.test.d.ts +18 -0
- package/dist/__tests__/skills/pipeline-integration.test.d.ts.map +1 -0
- package/dist/__tests__/skills/pipeline-integration.test.js +234 -0
- package/dist/__tests__/skills/pipeline-integration.test.js.map +1 -0
- package/dist/__tests__/skills/skill-routing.test.d.ts +15 -0
- package/dist/__tests__/skills/skill-routing.test.d.ts.map +1 -0
- package/dist/__tests__/skills/skill-routing.test.js +723 -0
- package/dist/__tests__/skills/skill-routing.test.js.map +1 -0
- package/dist/__tests__/skills/trigger-coverage.test.d.ts +24 -0
- package/dist/__tests__/skills/trigger-coverage.test.d.ts.map +1 -0
- package/dist/__tests__/skills/trigger-coverage.test.js +746 -0
- package/dist/__tests__/skills/trigger-coverage.test.js.map +1 -0
- package/dist/__tests__/smoke.test.js +13 -0
- package/dist/__tests__/smoke.test.js.map +1 -1
- package/dist/__tests__/verify-skills.test.d.ts +9 -0
- package/dist/__tests__/verify-skills.test.d.ts.map +1 -0
- package/dist/__tests__/verify-skills.test.js +78 -0
- package/dist/__tests__/verify-skills.test.js.map +1 -0
- package/dist/cli/commands/beads.e2e.test.d.ts +4 -2
- package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -1
- package/dist/cli/commands/beads.e2e.test.js +97 -38
- package/dist/cli/commands/beads.e2e.test.js.map +1 -1
- package/dist/cli/commands/cli-edge-cases.e2e.test.js +1 -1
- package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -1
- package/dist/cli/commands/close.d.ts +11 -46
- package/dist/cli/commands/close.d.ts.map +1 -1
- package/dist/cli/commands/close.e2e.test.d.ts +4 -20
- package/dist/cli/commands/close.e2e.test.d.ts.map +1 -1
- package/dist/cli/commands/close.e2e.test.js +23 -204
- package/dist/cli/commands/close.e2e.test.js.map +1 -1
- package/dist/cli/commands/close.js +26 -240
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/close.test.d.ts +7 -9
- package/dist/cli/commands/close.test.d.ts.map +1 -1
- package/dist/cli/commands/close.test.js +44 -424
- package/dist/cli/commands/close.test.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +16 -22
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.e2e.test.js +3 -59
- package/dist/cli/commands/doctor.e2e.test.js.map +1 -1
- package/dist/cli/commands/doctor.js +87 -103
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/doctor.test.js +120 -229
- package/dist/cli/commands/doctor.test.js.map +1 -1
- package/dist/cli/commands/framework-isolation.test.d.ts +1 -1
- package/dist/cli/commands/framework-isolation.test.js +2 -3
- package/dist/cli/commands/framework-isolation.test.js.map +1 -1
- package/dist/cli/commands/help.e2e.test.js +1 -5
- package/dist/cli/commands/help.e2e.test.js.map +1 -1
- package/dist/cli/commands/init-logic.e2e.test.js +114 -2
- package/dist/cli/commands/init-logic.e2e.test.js.map +1 -1
- package/dist/cli/commands/init.test.js +4 -21
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/land.d.ts +3 -15
- package/dist/cli/commands/land.d.ts.map +1 -1
- package/dist/cli/commands/land.js +13 -68
- package/dist/cli/commands/land.js.map +1 -1
- package/dist/cli/commands/land.test.d.ts +0 -1
- package/dist/cli/commands/land.test.d.ts.map +1 -1
- package/dist/cli/commands/land.test.js +2 -57
- package/dist/cli/commands/land.test.js.map +1 -1
- package/dist/cli/commands/mcp.e2e.test.js +28 -3
- package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
- package/dist/cli/commands/pipeline.e2e.test.js +23 -26
- package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
- package/dist/cli/commands/pre-push-guard.d.ts +2 -12
- package/dist/cli/commands/pre-push-guard.d.ts.map +1 -1
- package/dist/cli/commands/pre-push-guard.e2e.test.js +1 -1
- package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -1
- package/dist/cli/commands/pre-push-guard.js +2 -47
- package/dist/cli/commands/pre-push-guard.js.map +1 -1
- package/dist/cli/commands/pre-push-guard.test.d.ts +0 -1
- package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -1
- package/dist/cli/commands/pre-push-guard.test.js +15 -98
- package/dist/cli/commands/pre-push-guard.test.js.map +1 -1
- package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +1 -1
- package/dist/cli/commands/quickstart-expanded.e2e.test.js +3 -30
- package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -1
- package/dist/cli/commands/quickstart.d.ts +0 -1
- package/dist/cli/commands/quickstart.d.ts.map +1 -1
- package/dist/cli/commands/quickstart.js +2 -60
- package/dist/cli/commands/quickstart.js.map +1 -1
- package/dist/cli/commands/quickstart.test.js +10 -104
- package/dist/cli/commands/quickstart.test.js.map +1 -1
- package/dist/cli/commands/uninstall.test.d.ts +5 -0
- package/dist/cli/commands/uninstall.test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall.test.js +223 -0
- package/dist/cli/commands/uninstall.test.js.map +1 -0
- package/dist/cli/commands/update.d.ts +35 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.e2e.test.d.ts +24 -0
- package/dist/cli/commands/update.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/update.e2e.test.js +238 -0
- package/dist/cli/commands/update.e2e.test.js.map +1 -0
- package/dist/cli/commands/update.js +255 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/core/agents/frontmatter.test.js +1 -1
- package/dist/core/agents/frontmatter.test.js.map +1 -1
- package/dist/core/agents/handoffs.test.js +1 -1
- package/dist/core/agents/handoffs.test.js.map +1 -1
- package/dist/core/agents/loader.d.ts +4 -2
- package/dist/core/agents/loader.d.ts.map +1 -1
- package/dist/core/agents/loader.js +5 -3
- package/dist/core/agents/loader.js.map +1 -1
- package/dist/core/agents/loader.test.js +42 -4
- package/dist/core/agents/loader.test.js.map +1 -1
- package/dist/core/agents/suite.test.js +8 -7
- package/dist/core/agents/suite.test.js.map +1 -1
- package/dist/core/agents/tools.test.js +10 -8
- package/dist/core/agents/tools.test.js.map +1 -1
- package/dist/core/agents/types.test.js +1 -1
- package/dist/core/agents/types.test.js.map +1 -1
- package/dist/core/skills/loader.test.js +1 -1
- package/dist/core/skills/loader.test.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/pathValidation.d.ts +0 -5
- package/dist/lib/pathValidation.d.ts.map +1 -1
- package/dist/lib/pathValidation.js +0 -11
- package/dist/lib/pathValidation.js.map +1 -1
- package/dist/lib/pathValidation.test.js +2 -14
- package/dist/lib/pathValidation.test.js.map +1 -1
- package/package.json +3 -6
- package/sbom.json +259 -371
- package/templates/.github/agents/beth.agent.md +194 -122
- package/templates/.github/agents/developer.agent.md +30 -22
- package/templates/.github/agents/product-manager.agent.md +15 -6
- package/templates/.github/agents/researcher.agent.md +10 -7
- package/templates/.github/agents/security-reviewer.agent.md +16 -7
- package/templates/.github/agents/tester.agent.md +16 -8
- package/templates/.github/agents/ux-designer.agent.md +12 -9
- package/templates/.github/copilot-instructions.md +33 -4
- package/templates/.github/copilot-mcp-config.json +12 -0
- package/templates/.github/dependabot.yml +68 -0
- package/templates/.github/hooks/scripts/inject-skills.mjs +139 -0
- package/templates/.github/hooks/scripts/verify-skills.mjs +47 -0
- package/templates/.github/hooks/skill-enforcement.json +18 -0
- package/templates/.github/pull_request_template.md +48 -0
- package/templates/.github/skills/framer-components/SKILL.md +0 -0
- package/templates/.github/skills/prd/SKILL.md +0 -0
- package/templates/.github/skills/security-analysis/SKILL.md +798 -798
- package/templates/.github/skills/shadcn-ui/SKILL.md +561 -561
- package/templates/.github/skills/vercel-react-best-practices/AGENTS.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/SKILL.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/async-parallel.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-auth-actions.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-dedup-props.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -0
- package/templates/.github/skills/vercel-react-best-practices/rules/server-serialization.md +0 -0
- package/templates/.github/skills/web-design-guidelines/SKILL.md +0 -0
- package/templates/.vscode/settings.json +16 -16
- package/templates/AGENTS.md +59 -98
- package/templates/Backlog.md +80 -80
- package/templates/mcp.json.example +8 -0
- package/assets/beth-portrait-small.txt +0 -13
- package/assets/beth-portrait.txt +0 -60
- package/bin/beth-animation.sh +0 -155
- package/bin/lib/animation.js +0 -189
- package/bin/lib/pathValidation.js +0 -233
- 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
|
|
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(
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
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(
|
|
579
|
+
showBethBannerStatic();
|
|
851
580
|
console.log(`${COLORS.bright}Beth${COLORS.reset} - AI Orchestrator for GitHub Copilot
|
|
852
581
|
|
|
853
|
-
${COLORS.bright}
|
|
854
|
-
npx beth-copilot init [options] Initialize Beth in current directory
|
|
855
|
-
npx beth-copilot
|
|
856
|
-
npx beth-copilot
|
|
857
|
-
npx beth-copilot land [
|
|
858
|
-
npx beth-copilot
|
|
859
|
-
npx beth-copilot
|
|
860
|
-
npx beth-copilot
|
|
861
|
-
|
|
862
|
-
|
|
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/
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1168
|
-
if (!
|
|
1169
|
-
|
|
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
|
-
//
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
${COLORS.cyan}
|
|
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', '
|
|
1211
|
-
const ALLOWED_FLAGS = ['--force', '--skip-backlog', '--skip-mcp', '--
|
|
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 '
|
|
1121
|
+
// The 'land' and 'update' commands handle their own arg validation
|
|
1217
1122
|
const command = args[0]?.toLowerCase();
|
|
1218
|
-
if (command === '
|
|
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 '
|
|
1253
|
-
if (command !== '
|
|
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':
|