beth-copilot 1.0.18 → 1.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 +41 -28
- package/README.md +87 -247
- package/bin/cli.js +158 -358
- package/dist/__tests__/smoke.test.d.ts +8 -0
- package/dist/__tests__/smoke.test.d.ts.map +1 -0
- package/dist/__tests__/smoke.test.js +49 -0
- package/dist/__tests__/smoke.test.js.map +1 -0
- package/dist/cli/commands/beads.e2e.test.d.ts +13 -0
- package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/beads.e2e.test.js +526 -0
- package/dist/cli/commands/beads.e2e.test.js.map +1 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts +32 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.js +162 -0
- package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -0
- package/dist/cli/commands/close.d.ts +89 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.e2e.test.d.ts +27 -0
- package/dist/cli/commands/close.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/close.e2e.test.js +252 -0
- package/dist/cli/commands/close.e2e.test.js.map +1 -0
- package/dist/cli/commands/close.js +309 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/close.test.d.ts +15 -0
- package/dist/cli/commands/close.test.d.ts.map +1 -0
- package/dist/cli/commands/close.test.js +634 -0
- package/dist/cli/commands/close.test.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +23 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +93 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/doctor.test.js +209 -0
- package/dist/cli/commands/doctor.test.js.map +1 -1
- package/dist/cli/commands/framework-isolation.test.d.ts +30 -0
- package/dist/cli/commands/framework-isolation.test.d.ts.map +1 -0
- package/dist/cli/commands/framework-isolation.test.js +119 -0
- package/dist/cli/commands/framework-isolation.test.js.map +1 -0
- package/dist/cli/commands/help.e2e.test.js +4 -4
- package/dist/cli/commands/help.e2e.test.js.map +1 -1
- package/dist/cli/commands/init-logic.e2e.test.d.ts +37 -0
- package/dist/cli/commands/init-logic.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/init-logic.e2e.test.js +305 -0
- package/dist/cli/commands/init-logic.e2e.test.js.map +1 -0
- package/dist/cli/commands/land.d.ts +142 -0
- package/dist/cli/commands/land.d.ts.map +1 -0
- package/dist/cli/commands/land.js +647 -0
- package/dist/cli/commands/land.js.map +1 -0
- package/dist/cli/commands/land.test.d.ts +20 -0
- package/dist/cli/commands/land.test.d.ts.map +1 -0
- package/dist/cli/commands/land.test.js +622 -0
- package/dist/cli/commands/land.test.js.map +1 -0
- package/dist/cli/commands/mcp.e2e.test.js +22 -29
- package/dist/cli/commands/mcp.e2e.test.js.map +1 -1
- package/dist/cli/commands/pipeline.e2e.test.js +20 -20
- package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
- package/dist/cli/commands/pre-push-guard.d.ts +84 -0
- package/dist/cli/commands/pre-push-guard.d.ts.map +1 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.d.ts +24 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.js +171 -0
- package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -0
- package/dist/cli/commands/pre-push-guard.js +257 -0
- package/dist/cli/commands/pre-push-guard.js.map +1 -0
- package/dist/cli/commands/pre-push-guard.test.d.ts +15 -0
- package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -0
- package/dist/cli/commands/pre-push-guard.test.js +397 -0
- package/dist/cli/commands/pre-push-guard.test.js.map +1 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +23 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts.map +1 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.js +179 -0
- package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -0
- package/dist/cli/commands/quickstart.d.ts.map +1 -1
- package/dist/cli/commands/quickstart.js +7 -23
- package/dist/cli/commands/quickstart.js.map +1 -1
- package/dist/cli/commands/quickstart.test.js +40 -67
- package/dist/cli/commands/quickstart.test.js.map +1 -1
- package/dist/core/agents/suite.test.js +4 -2
- package/dist/core/agents/suite.test.js.map +1 -1
- package/dist/core/agents/tools.test.js +5 -1
- package/dist/core/agents/tools.test.js.map +1 -1
- package/dist/index.d.ts +3 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -10
- package/dist/index.js.map +1 -1
- package/package.json +15 -9
- package/sbom.json +2011 -819
- package/templates/.github/agents/beth.agent.md +220 -66
- package/templates/.github/agents/developer.agent.md +53 -90
- package/templates/.github/agents/product-manager.agent.md +15 -68
- package/templates/.github/agents/researcher.agent.md +20 -71
- package/templates/.github/agents/security-reviewer.agent.md +29 -81
- package/templates/.github/agents/tester.agent.md +40 -69
- package/templates/.github/agents/ux-designer.agent.md +20 -74
- package/templates/.github/copilot-instructions.md +217 -225
- package/templates/AGENTS.md +108 -20
- package/templates/mcp.json.example +0 -3
- package/dist/cli/commands/client-config.d.ts +0 -31
- package/dist/cli/commands/client-config.d.ts.map +0 -1
- package/dist/cli/commands/client-config.e2e.test.d.ts +0 -15
- package/dist/cli/commands/client-config.e2e.test.d.ts.map +0 -1
- package/dist/cli/commands/client-config.e2e.test.js +0 -556
- package/dist/cli/commands/client-config.e2e.test.js.map +0 -1
- package/dist/cli/commands/client-config.js +0 -73
- package/dist/cli/commands/client-config.js.map +0 -1
- package/dist/cli/commands/client-config.test.d.ts +0 -6
- package/dist/cli/commands/client-config.test.d.ts.map +0 -1
- package/dist/cli/commands/client-config.test.js +0 -133
- package/dist/cli/commands/client-config.test.js.map +0 -1
- package/dist/cli/commands/init-quickstart.e2e.test.d.ts +0 -11
- package/dist/cli/commands/init-quickstart.e2e.test.d.ts.map +0 -1
- package/dist/cli/commands/init-quickstart.e2e.test.js +0 -221
- package/dist/cli/commands/init-quickstart.e2e.test.js.map +0 -1
- package/dist/core/context.d.ts +0 -171
- package/dist/core/context.d.ts.map +0 -1
- package/dist/core/context.js +0 -353
- package/dist/core/context.js.map +0 -1
- package/dist/core/context.test.d.ts +0 -8
- package/dist/core/context.test.d.ts.map +0 -1
- package/dist/core/context.test.js +0 -253
- package/dist/core/context.test.js.map +0 -1
- package/dist/core/handoffs.d.ts +0 -151
- package/dist/core/handoffs.d.ts.map +0 -1
- package/dist/core/handoffs.js +0 -220
- package/dist/core/handoffs.js.map +0 -1
- package/dist/core/handoffs.test.d.ts +0 -8
- package/dist/core/handoffs.test.d.ts.map +0 -1
- package/dist/core/handoffs.test.js +0 -231
- package/dist/core/handoffs.test.js.map +0 -1
- package/dist/core/orchestrator.d.ts +0 -246
- package/dist/core/orchestrator.d.ts.map +0 -1
- package/dist/core/orchestrator.js +0 -514
- package/dist/core/orchestrator.js.map +0 -1
- package/dist/core/orchestrator.test.d.ts +0 -8
- package/dist/core/orchestrator.test.d.ts.map +0 -1
- package/dist/core/orchestrator.test.js +0 -517
- package/dist/core/orchestrator.test.js.map +0 -1
- package/dist/core/router.d.ts +0 -102
- package/dist/core/router.d.ts.map +0 -1
- package/dist/core/router.js +0 -178
- package/dist/core/router.js.map +0 -1
- package/dist/core/router.test.d.ts +0 -8
- package/dist/core/router.test.d.ts.map +0 -1
- package/dist/core/router.test.js +0 -215
- package/dist/core/router.test.js.map +0 -1
- package/dist/init.test.js +0 -288
- package/dist/providers/azure.d.ts +0 -147
- package/dist/providers/azure.d.ts.map +0 -1
- package/dist/providers/azure.js +0 -491
- package/dist/providers/azure.js.map +0 -1
- package/dist/providers/azure.test.d.ts +0 -11
- package/dist/providers/azure.test.d.ts.map +0 -1
- package/dist/providers/azure.test.js +0 -330
- package/dist/providers/azure.test.js.map +0 -1
- package/dist/providers/config.d.ts +0 -87
- package/dist/providers/config.d.ts.map +0 -1
- package/dist/providers/config.js +0 -193
- package/dist/providers/config.js.map +0 -1
- package/dist/providers/config.test.d.ts +0 -7
- package/dist/providers/config.test.d.ts.map +0 -1
- package/dist/providers/config.test.js +0 -370
- package/dist/providers/config.test.js.map +0 -1
- package/dist/providers/index.d.ts +0 -18
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -14
- package/dist/providers/index.js.map +0 -1
- package/dist/providers/interface.d.ts +0 -191
- package/dist/providers/interface.d.ts.map +0 -1
- package/dist/providers/interface.js +0 -94
- package/dist/providers/interface.js.map +0 -1
- package/dist/providers/retry.d.ts +0 -128
- package/dist/providers/retry.d.ts.map +0 -1
- package/dist/providers/retry.js +0 -205
- package/dist/providers/retry.js.map +0 -1
- package/dist/providers/retry.test.d.ts +0 -7
- package/dist/providers/retry.test.d.ts.map +0 -1
- package/dist/providers/retry.test.js +0 -439
- package/dist/providers/retry.test.js.map +0 -1
- package/dist/providers/streaming.d.ts +0 -157
- package/dist/providers/streaming.d.ts.map +0 -1
- package/dist/providers/streaming.js +0 -233
- package/dist/providers/streaming.js.map +0 -1
- package/dist/providers/streaming.test.d.ts +0 -7
- package/dist/providers/streaming.test.d.ts.map +0 -1
- package/dist/providers/streaming.test.js +0 -372
- package/dist/providers/streaming.test.js.map +0 -1
- package/dist/providers/types.d.ts +0 -209
- package/dist/providers/types.d.ts.map +0 -1
- package/dist/providers/types.js +0 -53
- package/dist/providers/types.js.map +0 -1
- package/dist/providers/types.test.d.ts +0 -7
- package/dist/providers/types.test.d.ts.map +0 -1
- package/dist/providers/types.test.js +0 -141
- package/dist/providers/types.test.js.map +0 -1
- package/dist/tools/cli/beads.d.ts +0 -27
- package/dist/tools/cli/beads.d.ts.map +0 -1
- package/dist/tools/cli/beads.js +0 -172
- package/dist/tools/cli/beads.js.map +0 -1
- package/dist/tools/cli/beads.test.d.ts +0 -8
- package/dist/tools/cli/beads.test.d.ts.map +0 -1
- package/dist/tools/cli/beads.test.js +0 -264
- package/dist/tools/cli/beads.test.js.map +0 -1
- package/dist/tools/cli/editFile.d.ts +0 -17
- package/dist/tools/cli/editFile.d.ts.map +0 -1
- package/dist/tools/cli/editFile.js +0 -125
- package/dist/tools/cli/editFile.js.map +0 -1
- package/dist/tools/cli/editFile.test.d.ts +0 -8
- package/dist/tools/cli/editFile.test.d.ts.map +0 -1
- package/dist/tools/cli/editFile.test.js +0 -177
- package/dist/tools/cli/editFile.test.js.map +0 -1
- package/dist/tools/cli/readFile.d.ts +0 -25
- package/dist/tools/cli/readFile.d.ts.map +0 -1
- package/dist/tools/cli/readFile.js +0 -118
- package/dist/tools/cli/readFile.js.map +0 -1
- package/dist/tools/cli/readFile.test.d.ts +0 -8
- package/dist/tools/cli/readFile.test.d.ts.map +0 -1
- package/dist/tools/cli/readFile.test.js +0 -194
- package/dist/tools/cli/readFile.test.js.map +0 -1
- package/dist/tools/cli/search.d.ts +0 -16
- package/dist/tools/cli/search.d.ts.map +0 -1
- package/dist/tools/cli/search.js +0 -261
- package/dist/tools/cli/search.js.map +0 -1
- package/dist/tools/cli/search.test.d.ts +0 -8
- package/dist/tools/cli/search.test.d.ts.map +0 -1
- package/dist/tools/cli/search.test.js +0 -172
- package/dist/tools/cli/search.test.js.map +0 -1
- package/dist/tools/cli/subagent.d.ts +0 -43
- package/dist/tools/cli/subagent.d.ts.map +0 -1
- package/dist/tools/cli/subagent.js +0 -99
- package/dist/tools/cli/subagent.js.map +0 -1
- package/dist/tools/cli/subagent.test.d.ts +0 -8
- package/dist/tools/cli/subagent.test.d.ts.map +0 -1
- package/dist/tools/cli/subagent.test.js +0 -190
- package/dist/tools/cli/subagent.test.js.map +0 -1
- package/dist/tools/cli/terminal.d.ts +0 -19
- package/dist/tools/cli/terminal.d.ts.map +0 -1
- package/dist/tools/cli/terminal.js +0 -164
- package/dist/tools/cli/terminal.js.map +0 -1
- package/dist/tools/cli/terminal.test.d.ts +0 -8
- package/dist/tools/cli/terminal.test.d.ts.map +0 -1
- package/dist/tools/cli/terminal.test.js +0 -161
- package/dist/tools/cli/terminal.test.js.map +0 -1
- package/dist/tools/index.d.ts +0 -25
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -41
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/interface.d.ts +0 -64
- package/dist/tools/interface.d.ts.map +0 -1
- package/dist/tools/interface.js +0 -37
- package/dist/tools/interface.js.map +0 -1
- package/dist/tools/interface.test.d.ts +0 -7
- package/dist/tools/interface.test.d.ts.map +0 -1
- package/dist/tools/interface.test.js +0 -179
- package/dist/tools/interface.test.js.map +0 -1
- package/dist/tools/mcp/bridge.d.ts +0 -48
- package/dist/tools/mcp/bridge.d.ts.map +0 -1
- package/dist/tools/mcp/bridge.js +0 -128
- package/dist/tools/mcp/bridge.js.map +0 -1
- package/dist/tools/mcp/bridge.test.d.ts +0 -8
- package/dist/tools/mcp/bridge.test.d.ts.map +0 -1
- package/dist/tools/mcp/bridge.test.js +0 -300
- package/dist/tools/mcp/bridge.test.js.map +0 -1
- package/dist/tools/mcp/client.d.ts +0 -135
- package/dist/tools/mcp/client.d.ts.map +0 -1
- package/dist/tools/mcp/client.js +0 -263
- package/dist/tools/mcp/client.js.map +0 -1
- package/dist/tools/mcp/client.test.d.ts +0 -8
- package/dist/tools/mcp/client.test.d.ts.map +0 -1
- package/dist/tools/mcp/client.test.js +0 -390
- package/dist/tools/mcp/client.test.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -82
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -99
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/registry.test.d.ts +0 -7
- package/dist/tools/registry.test.d.ts.map +0 -1
- package/dist/tools/registry.test.js +0 -199
- package/dist/tools/registry.test.js.map +0 -1
- package/dist/tools/suite.test.d.ts +0 -11
- package/dist/tools/suite.test.d.ts.map +0 -1
- package/dist/tools/suite.test.js +0 -119
- package/dist/tools/suite.test.js.map +0 -1
- package/dist/tools/types.d.ts +0 -75
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -30
- package/dist/tools/types.js.map +0 -1
- package/dist/tools/types.test.d.ts +0 -7
- package/dist/tools/types.test.d.ts.map +0 -1
- package/dist/tools/types.test.js +0 -178
- package/dist/tools/types.test.js.map +0 -1
- package/templates/.vscode/mcp.json +0 -20
- package/templates/CLAUDE.md +0 -129
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E tests for pre-push-guard command.
|
|
3
|
+
*
|
|
4
|
+
* Runs the actual `beth-copilot pre-push-guard` binary and validates output/behavior.
|
|
5
|
+
*
|
|
6
|
+
* beth-ywg.2: pre-push-guard has 56 unit tests but zero E2E — this fills the gap.
|
|
7
|
+
*
|
|
8
|
+
* Repro steps:
|
|
9
|
+
* 1. Build: npm run build
|
|
10
|
+
* 2. Run: npx vitest run src/cli/commands/pre-push-guard.e2e.test.ts
|
|
11
|
+
* OR: npx vitest run --config vitest.e2e.config.ts
|
|
12
|
+
*
|
|
13
|
+
* Test cases:
|
|
14
|
+
* - Running on an epic branch → exit 0, no errors
|
|
15
|
+
* - Running with BETH_SKIP_PUSH_GUARD=1 → exit 0, bypass
|
|
16
|
+
* - Running on a protected branch (main) → exit 1, error message
|
|
17
|
+
* - Running on unrecognized branch name → exit 0 with warning
|
|
18
|
+
* - Running outside git repo → graceful handling
|
|
19
|
+
* - Output contains expected formatting
|
|
20
|
+
*
|
|
21
|
+
* Expected outcomes documented inline per test case.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
24
|
+
import assert from 'node:assert';
|
|
25
|
+
import { execSync, spawnSync } from 'child_process';
|
|
26
|
+
import { resolve, join } from 'path';
|
|
27
|
+
import { mkdirSync, rmSync, existsSync, mkdtempSync } from 'fs';
|
|
28
|
+
import { tmpdir } from 'os';
|
|
29
|
+
const CLI_PATH = resolve(join(import.meta.dirname, '..', '..', '..', 'bin', 'cli.js'));
|
|
30
|
+
/**
|
|
31
|
+
* Run pre-push-guard via the CLI binary.
|
|
32
|
+
* Accepts optional env overrides and optional stdin for git ref simulation.
|
|
33
|
+
*/
|
|
34
|
+
function runGuard(options = {}) {
|
|
35
|
+
const env = { ...process.env, NO_COLOR: '1', ...options.env };
|
|
36
|
+
const result = spawnSync('node', [CLI_PATH, 'pre-push-guard'], {
|
|
37
|
+
cwd: options.cwd || process.cwd(),
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
env,
|
|
40
|
+
input: options.stdin || '',
|
|
41
|
+
timeout: 15000,
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
stdout: result.stdout || '',
|
|
45
|
+
stderr: result.stderr || '',
|
|
46
|
+
code: result.status ?? 1,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createGitRepoOnBranch(branchName, prefix) {
|
|
50
|
+
const tmpDir = mkdtempSync(join(tmpdir(), prefix));
|
|
51
|
+
execSync('git init', { cwd: tmpDir, stdio: 'ignore' });
|
|
52
|
+
execSync('git config user.name "Test"', { cwd: tmpDir, stdio: 'ignore' });
|
|
53
|
+
execSync('git config user.email "test@test.com"', { cwd: tmpDir, stdio: 'ignore' });
|
|
54
|
+
execSync(`git checkout --orphan ${branchName}`, { cwd: tmpDir, stdio: 'ignore' });
|
|
55
|
+
execSync('git commit --allow-empty -m "init"', { cwd: tmpDir, stdio: 'ignore' });
|
|
56
|
+
return tmpDir;
|
|
57
|
+
}
|
|
58
|
+
describe('pre-push-guard command E2E', () => {
|
|
59
|
+
describe('valid epic branch repo', () => {
|
|
60
|
+
let tmpDir;
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
tmpDir = createGitRepoOnBranch('epic/beth-ywg', 'beth-guard-epic-');
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
if (existsSync(tmpDir)) {
|
|
66
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// Expected: the guard should pass on a valid epic branch.
|
|
70
|
+
it('should exit 0 on the current epic branch', () => {
|
|
71
|
+
const result = runGuard({ cwd: tmpDir });
|
|
72
|
+
assert.strictEqual(result.code, 0, 'Should exit 0 on a valid epic branch');
|
|
73
|
+
});
|
|
74
|
+
it('should not produce error output on a valid branch', () => {
|
|
75
|
+
const result = runGuard({ cwd: tmpDir });
|
|
76
|
+
// Errors go to stderr — should be empty or just warnings
|
|
77
|
+
const hasBlockingError = result.stderr.includes('blocked') || result.stderr.includes('BLOCKED');
|
|
78
|
+
assert.ok(!hasBlockingError, 'Should not have blocking errors on epic branch');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('bypass with environment variable', () => {
|
|
82
|
+
// Expected: exit 0, skip all checks
|
|
83
|
+
it('should exit 0 when BETH_SKIP_PUSH_GUARD=1', () => {
|
|
84
|
+
const result = runGuard({ env: { BETH_SKIP_PUSH_GUARD: '1' } });
|
|
85
|
+
assert.strictEqual(result.code, 0, 'Should exit 0 when bypass env is set');
|
|
86
|
+
});
|
|
87
|
+
it('should indicate bypass in output', () => {
|
|
88
|
+
const result = runGuard({ env: { BETH_SKIP_PUSH_GUARD: '1' } });
|
|
89
|
+
const combined = result.stdout + result.stderr;
|
|
90
|
+
assert.ok(combined.includes('bypass') || combined.includes('skip') || combined.includes('SKIP') || result.code === 0, 'Should indicate bypass or just exit cleanly');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('outside git repo', () => {
|
|
94
|
+
let tmpDir;
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'beth-guard-test-'));
|
|
97
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
98
|
+
});
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
if (existsSync(tmpDir)) {
|
|
101
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Expected: graceful handling — no crash on missing git
|
|
105
|
+
it('should handle non-git directory gracefully', () => {
|
|
106
|
+
const result = runGuard({ cwd: tmpDir });
|
|
107
|
+
// Should not crash with unhandled exception
|
|
108
|
+
assert.ok(typeof result.code === 'number', 'Should exit with a numeric code, not crash');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('protected branch simulation', () => {
|
|
112
|
+
let tmpDir;
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
tmpDir = createGitRepoOnBranch('main', 'beth-guard-main-');
|
|
115
|
+
});
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
if (existsSync(tmpDir)) {
|
|
118
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Expected: exit 1, error about protected branch
|
|
122
|
+
it('should block push from main branch', () => {
|
|
123
|
+
const result = runGuard({ cwd: tmpDir });
|
|
124
|
+
assert.strictEqual(result.code, 1, 'Should exit 1 when on main branch');
|
|
125
|
+
const combined = result.stdout + result.stderr;
|
|
126
|
+
assert.ok(combined.includes('blocked') || combined.includes('main') || combined.includes('BLOCK'), 'Should mention main is blocked');
|
|
127
|
+
});
|
|
128
|
+
// Expected: exit 1, suggest using epic branch
|
|
129
|
+
it('should suggest using an epic branch', () => {
|
|
130
|
+
const result = runGuard({ cwd: tmpDir });
|
|
131
|
+
const combined = result.stdout + result.stderr;
|
|
132
|
+
assert.ok(combined.includes('epic') || combined.includes('PR'), 'Should suggest using an epic branch or PR');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('unrecognized branch naming', () => {
|
|
136
|
+
let tmpDir;
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
tmpDir = createGitRepoOnBranch('my-random-branch', 'beth-guard-weird-');
|
|
139
|
+
});
|
|
140
|
+
afterEach(() => {
|
|
141
|
+
if (existsSync(tmpDir)) {
|
|
142
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Expected: exit 0 (warnings only, not blocking), with naming convention suggestion
|
|
146
|
+
it('should exit 0 but warn about non-epic branch naming', () => {
|
|
147
|
+
const result = runGuard({ cwd: tmpDir });
|
|
148
|
+
assert.strictEqual(result.code, 0, 'Should exit 0 for unrecognized but non-protected branch');
|
|
149
|
+
});
|
|
150
|
+
it('should produce a warning about naming convention', () => {
|
|
151
|
+
const result = runGuard({ cwd: tmpDir });
|
|
152
|
+
const combined = result.stdout + result.stderr;
|
|
153
|
+
assert.ok(combined.includes('convention') || combined.includes('warning') || combined.includes('Warning') || combined.includes('doesn\'t follow'), 'Should warn about naming convention');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('stdin ref parsing (simulated git push input)', () => {
|
|
157
|
+
// Git sends refs on stdin during pre-push. Test that parsing works end-to-end.
|
|
158
|
+
it('should process refs from stdin without crashing', () => {
|
|
159
|
+
const stdin = 'refs/heads/epic/beth-ywg abc123 refs/heads/epic/beth-ywg def456\n';
|
|
160
|
+
const result = runGuard({ stdin });
|
|
161
|
+
// Should not crash — the exit code depends on current branch
|
|
162
|
+
assert.ok(typeof result.code === 'number', 'Should handle stdin refs gracefully');
|
|
163
|
+
});
|
|
164
|
+
it('should block when stdin refs target main', () => {
|
|
165
|
+
const stdin = 'refs/heads/epic/beth-ywg abc123 refs/heads/main def456\n';
|
|
166
|
+
const result = runGuard({ stdin });
|
|
167
|
+
assert.strictEqual(result.code, 1, 'Should block push targeting main via stdin refs');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=pre-push-guard.e2e.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-push-guard.e2e.test.js","sourceRoot":"","sources":["../../../src/cli/commands/pre-push-guard.e2e.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Push Guard — Branch discipline enforcement
|
|
3
|
+
*
|
|
4
|
+
* Validates before push:
|
|
5
|
+
* - No direct pushes to main/master (BLOCKS)
|
|
6
|
+
* - Current branch follows epic/<id> convention (WARNING)
|
|
7
|
+
* - Open in-progress beads issues reported (WARNING)
|
|
8
|
+
* - Bypassed with BETH_SKIP_PUSH_GUARD=1 environment variable
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
const COLORS = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bright: '\x1b[1m',
|
|
14
|
+
red: '\x1b[31m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
cyan: '\x1b[36m',
|
|
17
|
+
};
|
|
18
|
+
/** Protected branch names that cannot receive direct pushes. */
|
|
19
|
+
const PROTECTED_BRANCHES = ['main', 'master'];
|
|
20
|
+
/** Epic branch naming convention: epic/<rig>-<hash> */
|
|
21
|
+
const EPIC_BRANCH_PATTERN = /^epic\/[a-z]+-[a-z0-9]+$/;
|
|
22
|
+
/** Release branches are also valid push targets. */
|
|
23
|
+
const RELEASE_BRANCH_PATTERN = /^release\/v?\d+/;
|
|
24
|
+
/**
|
|
25
|
+
* Parse pre-push stdin input into structured refs.
|
|
26
|
+
* Git sends: <local ref> <local SHA> <remote ref> <remote SHA>
|
|
27
|
+
* One line per ref being pushed.
|
|
28
|
+
*/
|
|
29
|
+
export function parsePushRefs(stdin) {
|
|
30
|
+
return stdin
|
|
31
|
+
.trim()
|
|
32
|
+
.split('\n')
|
|
33
|
+
.filter((line) => line.trim().length > 0)
|
|
34
|
+
.map((line) => {
|
|
35
|
+
const parts = line.trim().split(/\s+/);
|
|
36
|
+
return {
|
|
37
|
+
localRef: parts[0] || '',
|
|
38
|
+
localSha: parts[1] || '',
|
|
39
|
+
remoteRef: parts[2] || '',
|
|
40
|
+
remoteSha: parts[3] || '',
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract branch name from a Git ref.
|
|
46
|
+
* refs/heads/main → main
|
|
47
|
+
* refs/heads/epic/beth-abc123 → epic/beth-abc123
|
|
48
|
+
*/
|
|
49
|
+
export function extractBranchName(ref) {
|
|
50
|
+
const prefix = 'refs/heads/';
|
|
51
|
+
if (ref.startsWith(prefix)) {
|
|
52
|
+
return ref.slice(prefix.length);
|
|
53
|
+
}
|
|
54
|
+
return ref;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if a branch name is protected (main, master).
|
|
58
|
+
*/
|
|
59
|
+
export function isProtectedBranch(branch) {
|
|
60
|
+
return PROTECTED_BRANCHES.includes(branch);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if the branch follows the epic/<id> convention.
|
|
64
|
+
*/
|
|
65
|
+
export function isEpicBranch(branch) {
|
|
66
|
+
return EPIC_BRANCH_PATTERN.test(branch);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if the branch is a release branch.
|
|
70
|
+
*/
|
|
71
|
+
export function isReleaseBranch(branch) {
|
|
72
|
+
return RELEASE_BRANCH_PATTERN.test(branch);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if the branch follows any recognized naming convention.
|
|
76
|
+
*/
|
|
77
|
+
export function isRecognizedBranch(branch) {
|
|
78
|
+
return isEpicBranch(branch) || isReleaseBranch(branch) || isProtectedBranch(branch);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the current Git branch name.
|
|
82
|
+
* Returns null if not in a git repo or in detached HEAD state.
|
|
83
|
+
*/
|
|
84
|
+
export function getCurrentBranch() {
|
|
85
|
+
try {
|
|
86
|
+
const result = execFileSync('git', ['branch', '--show-current'], {
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
timeout: 5000,
|
|
89
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
90
|
+
}).trim();
|
|
91
|
+
return result || null;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get beads issues with in_progress status.
|
|
99
|
+
* Returns empty array if bd is unavailable or errors.
|
|
100
|
+
*/
|
|
101
|
+
export function getInProgressIssues() {
|
|
102
|
+
try {
|
|
103
|
+
const output = execFileSync('bd', ['list', '--json'], {
|
|
104
|
+
encoding: 'utf-8',
|
|
105
|
+
timeout: 10000,
|
|
106
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
107
|
+
});
|
|
108
|
+
const parsed = JSON.parse(output);
|
|
109
|
+
if (!Array.isArray(parsed))
|
|
110
|
+
return [];
|
|
111
|
+
return parsed
|
|
112
|
+
.filter((item) => typeof item === 'object' &&
|
|
113
|
+
item !== null &&
|
|
114
|
+
'id' in item &&
|
|
115
|
+
'title' in item &&
|
|
116
|
+
'status' in item &&
|
|
117
|
+
item.status === 'in_progress')
|
|
118
|
+
.map((item) => ({ id: item.id, title: item.title }));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run all pre-push guard checks.
|
|
126
|
+
*
|
|
127
|
+
* @param currentBranch - Current Git branch name
|
|
128
|
+
* @param refs - Optional parsed push refs from Git stdin (for remote ref validation)
|
|
129
|
+
* @param checkBeads - Whether to check for in-progress beads issues
|
|
130
|
+
* @returns GuardResult with allowed status and diagnostics
|
|
131
|
+
*/
|
|
132
|
+
export function runGuard(currentBranch, refs, checkBeads = true) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
const warnings = [];
|
|
135
|
+
// Check 1: No pushing to protected branches via remote refs
|
|
136
|
+
if (refs && refs.length > 0) {
|
|
137
|
+
for (const ref of refs) {
|
|
138
|
+
const targetBranch = extractBranchName(ref.remoteRef);
|
|
139
|
+
if (isProtectedBranch(targetBranch)) {
|
|
140
|
+
errors.push(`Direct push to '${targetBranch}' is blocked. Use a PR from your epic branch.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Check 1b: Also block if current branch IS a protected branch
|
|
145
|
+
// (catches the common case without needing stdin refs)
|
|
146
|
+
if (currentBranch && isProtectedBranch(currentBranch)) {
|
|
147
|
+
const msg = `Pushing from '${currentBranch}' is blocked. Work on an epic branch.`;
|
|
148
|
+
if (!errors.some((e) => e.includes(currentBranch))) {
|
|
149
|
+
errors.push(msg);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Check 2: Current branch should follow epic convention
|
|
153
|
+
if (currentBranch && !isRecognizedBranch(currentBranch)) {
|
|
154
|
+
warnings.push(`Branch '${currentBranch}' doesn't follow the epic/<id> convention. Consider renaming.`);
|
|
155
|
+
}
|
|
156
|
+
// Check 3: Warn about in-progress beads issues (soft check)
|
|
157
|
+
if (checkBeads) {
|
|
158
|
+
try {
|
|
159
|
+
const inProgress = getInProgressIssues();
|
|
160
|
+
if (inProgress.length > 0) {
|
|
161
|
+
warnings.push(`${inProgress.length} issue${inProgress.length === 1 ? '' : 's'} still in_progress:`);
|
|
162
|
+
for (const issue of inProgress) {
|
|
163
|
+
warnings.push(` ◐ ${issue.id}: ${issue.title}`);
|
|
164
|
+
}
|
|
165
|
+
warnings.push('Consider closing completed work before pushing.');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// bd not available — skip silently
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
allowed: errors.length === 0,
|
|
174
|
+
errors,
|
|
175
|
+
warnings,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Main entry point for the pre-push guard command.
|
|
180
|
+
* Reads refs from stdin (if available) and runs all checks.
|
|
181
|
+
*/
|
|
182
|
+
export async function prePushGuard(stdinInput) {
|
|
183
|
+
// Check bypass
|
|
184
|
+
if (process.env.BETH_SKIP_PUSH_GUARD === '1') {
|
|
185
|
+
console.error(`${COLORS.yellow}⚠ Pre-push guard bypassed (BETH_SKIP_PUSH_GUARD=1)${COLORS.reset}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Read stdin if not provided (Git pre-push pipes refs via stdin)
|
|
189
|
+
let stdin = stdinInput;
|
|
190
|
+
if (stdin === undefined) {
|
|
191
|
+
const fs = await import('fs');
|
|
192
|
+
try {
|
|
193
|
+
stdin = fs.readFileSync(0, 'utf-8');
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
stdin = '';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const refs = stdin.trim() ? parsePushRefs(stdin) : undefined;
|
|
200
|
+
const currentBranch = getCurrentBranch();
|
|
201
|
+
const result = runGuard(currentBranch, refs);
|
|
202
|
+
// Print warnings
|
|
203
|
+
for (const warning of result.warnings) {
|
|
204
|
+
console.error(`${COLORS.yellow}⚠ ${warning}${COLORS.reset}`);
|
|
205
|
+
}
|
|
206
|
+
// Print errors and exit non-zero to block push
|
|
207
|
+
if (!result.allowed) {
|
|
208
|
+
console.error('');
|
|
209
|
+
for (const error of result.errors) {
|
|
210
|
+
console.error(`${COLORS.red}✗ ${error}${COLORS.reset}`);
|
|
211
|
+
}
|
|
212
|
+
console.error(`\n${COLORS.yellow}Set BETH_SKIP_PUSH_GUARD=1 to bypass.${COLORS.reset}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Generate the shell script content to append to .beads/hooks/pre-push.
|
|
218
|
+
* Pure shell — no Node dependency at hook time for speed.
|
|
219
|
+
*/
|
|
220
|
+
export function generateHookScript() {
|
|
221
|
+
return `
|
|
222
|
+
# --- BEGIN BETH GUARD ---
|
|
223
|
+
# Branch discipline enforcement — installed by beth-copilot
|
|
224
|
+
# Bypass: BETH_SKIP_PUSH_GUARD=1 git push
|
|
225
|
+
if [ "\$BETH_SKIP_PUSH_GUARD" = "1" ]; then
|
|
226
|
+
echo "⚠ Pre-push guard bypassed (BETH_SKIP_PUSH_GUARD=1)" >&2
|
|
227
|
+
else
|
|
228
|
+
_beth_branch=\$(git branch --show-current 2>/dev/null)
|
|
229
|
+
|
|
230
|
+
# Block pushes from protected branches
|
|
231
|
+
case "\$_beth_branch" in
|
|
232
|
+
main|master)
|
|
233
|
+
echo "✗ Pushing from '\$_beth_branch' is blocked. Work on an epic branch." >&2
|
|
234
|
+
echo " Set BETH_SKIP_PUSH_GUARD=1 to bypass." >&2
|
|
235
|
+
exit 1
|
|
236
|
+
;;
|
|
237
|
+
esac
|
|
238
|
+
|
|
239
|
+
# Warn if not on an epic or release branch
|
|
240
|
+
case "\$_beth_branch" in
|
|
241
|
+
epic/*) ;;
|
|
242
|
+
release/*) ;;
|
|
243
|
+
"")
|
|
244
|
+
echo "⚠ Detached HEAD — no branch name. Proceeding anyway." >&2
|
|
245
|
+
;;
|
|
246
|
+
*)
|
|
247
|
+
echo "⚠ Branch '\$_beth_branch' doesn't follow the epic/<id> convention." >&2
|
|
248
|
+
;;
|
|
249
|
+
esac
|
|
250
|
+
fi
|
|
251
|
+
# --- END BETH GUARD ---
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
/** Marker used to detect if bethguard is already installed in a hook file. */
|
|
255
|
+
export const BETH_GUARD_BEGIN = '# --- BEGIN BETH GUARD ---';
|
|
256
|
+
export const BETH_GUARD_END = '# --- END BETH GUARD ---';
|
|
257
|
+
//# sourceMappingURL=pre-push-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-push-guard.js","sourceRoot":"","sources":["../../../src/cli/commands/pre-push-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Push Guard Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests branch discipline enforcement:
|
|
5
|
+
* - Push ref parsing from Git stdin
|
|
6
|
+
* - Branch name extraction from refs
|
|
7
|
+
* - Protected branch detection
|
|
8
|
+
* - Epic branch convention validation
|
|
9
|
+
* - Release branch recognition
|
|
10
|
+
* - Guard logic: errors vs warnings
|
|
11
|
+
* - Hook script generation
|
|
12
|
+
* - Beads in-progress issue detection
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=pre-push-guard.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pre-push-guard.test.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/pre-push-guard.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|