forge-workflow 0.0.4 → 0.0.6
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/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Forge Sync Command
|
|
7
|
+
* Syncs Beads issue data by running dolt pull + push.
|
|
8
|
+
* Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
|
|
9
|
+
*
|
|
10
|
+
* @module commands/sync
|
|
11
|
+
*/
|
|
12
|
+
module.exports = {
|
|
13
|
+
name: 'sync',
|
|
14
|
+
description: 'Sync Beads issue data (dolt pull + push)',
|
|
15
|
+
usage: 'forge sync',
|
|
16
|
+
flags: {},
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run beads dolt pull + push to sync issue data.
|
|
20
|
+
* @param {string[]} _args - Positional arguments (unused)
|
|
21
|
+
* @param {object} _flags - CLI flags (unused)
|
|
22
|
+
* @param {string} _projectRoot - Project root path (unused)
|
|
23
|
+
* @param {object} [opts] - Options for dependency injection
|
|
24
|
+
* @param {Function} [opts._exec] - Override for execFileSync (testing)
|
|
25
|
+
* @returns {Promise<{success: boolean, synced: boolean, message?: string, error?: string}>}
|
|
26
|
+
*/
|
|
27
|
+
handler: async (_args, _flags, _projectRoot, opts = {}) => {
|
|
28
|
+
const exec = opts._exec || execFileSync;
|
|
29
|
+
|
|
30
|
+
// Step 1: Check if bd binary exists
|
|
31
|
+
try {
|
|
32
|
+
exec('bd', ['--version'], { stdio: 'pipe' });
|
|
33
|
+
} catch (_checkErr) { /* intentional: bd not installed, skip sync gracefully */ // NOSONAR S2486
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
synced: false,
|
|
37
|
+
message: 'Beads not installed — nothing to sync',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 2: Run dolt pull + push
|
|
42
|
+
try {
|
|
43
|
+
exec('bd', ['dolt', 'pull'], { stdio: 'pipe' });
|
|
44
|
+
exec('bd', ['dolt', 'push'], { stdio: 'pipe' });
|
|
45
|
+
} catch (syncErr) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
synced: false,
|
|
49
|
+
error: syncErr.message,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { success: true, synced: true };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { existsSync } = require('node:fs');
|
|
2
|
+
const { execFileSync } = require('node:child_process');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
// Resolve bash binary from fixed, known locations only (no PATH search).
|
|
6
|
+
// SonarCloud S4036: PATH must not be searched for OS commands.
|
|
7
|
+
function _resolveBash() {
|
|
8
|
+
if (process.platform === 'win32') {
|
|
9
|
+
// Fixed locations for Git Bash on Windows (no PATH search)
|
|
10
|
+
const candidates = [
|
|
11
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
12
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
13
|
+
`${process.env.LOCALAPPDATA || ''}\\Programs\\Git\\bin\\bash.exe`,
|
|
14
|
+
];
|
|
15
|
+
for (const p of candidates) {
|
|
16
|
+
if (p && existsSync(p)) return p;
|
|
17
|
+
}
|
|
18
|
+
// Fallback: assume Git Bash is in PATH (less secure but functional)
|
|
19
|
+
return 'bash';
|
|
20
|
+
}
|
|
21
|
+
return '/usr/bin/bash';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function handleTeam(args) {
|
|
25
|
+
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'forge-team', 'index.sh');
|
|
26
|
+
const bashPath = _resolveBash();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
execFileSync(bashPath, [scriptPath, ...args], {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
process.exit(err.status || 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { handleTeam };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test Command — Smart Test Runner
|
|
5
|
+
*
|
|
6
|
+
* Wraps test execution with smart defaults:
|
|
7
|
+
* - Auto-detects package manager from lockfiles
|
|
8
|
+
* - Checks Beads (Dolt) connectivity, sets BEADS_SKIP_TESTS if unavailable
|
|
9
|
+
* - Supports --affected flag to run only tests for changed files
|
|
10
|
+
*
|
|
11
|
+
* Security: Uses execFileSync for subprocess calls (OWASP A03)
|
|
12
|
+
*
|
|
13
|
+
* @module commands/test
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { execFileSync: defaultExecFileSync, spawnSync: defaultSpawnSync } = require('node:child_process');
|
|
17
|
+
const defaultFs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
/** @type {Array<[string, string]>} Lockfile → package manager mapping (order matters) */
|
|
21
|
+
const LOCKFILE_MAP = [
|
|
22
|
+
['bun.lockb', 'bun'],
|
|
23
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
24
|
+
['yarn.lock', 'yarn'],
|
|
25
|
+
['package-lock.json', 'npm'],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIMEOUT = 120000;
|
|
29
|
+
const BEADS_CHECK_TIMEOUT = 3000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect the package manager by checking which lockfile exists.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
35
|
+
* @param {Object} fs - Injected fs module
|
|
36
|
+
* @returns {string} Package manager command ('bun' | 'pnpm' | 'yarn' | 'npm')
|
|
37
|
+
*/
|
|
38
|
+
function detectPackageManager(projectRoot, fs) {
|
|
39
|
+
for (const [lockfile, manager] of LOCKFILE_MAP) {
|
|
40
|
+
if (fs.existsSync(path.join(projectRoot, lockfile))) {
|
|
41
|
+
return manager;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return 'npm';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if Beads (bd CLI) is reachable.
|
|
49
|
+
*
|
|
50
|
+
* @param {Function} execFileSync - Injected execFileSync
|
|
51
|
+
* @returns {boolean} true if bd is available
|
|
52
|
+
*/
|
|
53
|
+
function checkBeadsConnectivity(execFileSync) {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync('bd', ['list', '--limit=1'], { timeout: BEADS_CHECK_TIMEOUT });
|
|
56
|
+
return true;
|
|
57
|
+
} catch (_e) { // NOSONAR S2486
|
|
58
|
+
/* intentional: bd not installed or unreachable */
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get changed files relative to main branch, mapped to test file paths.
|
|
65
|
+
*
|
|
66
|
+
* Falls back to `git diff --name-only HEAD` if merge-base fails.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} _projectRoot - Project root (unused, git uses cwd)
|
|
69
|
+
* @param {Function} execFileSync - Injected execFileSync
|
|
70
|
+
* @returns {string[]} Array of test file paths (e.g. ['test/foo.test.js'])
|
|
71
|
+
*/
|
|
72
|
+
function getAffectedTestFiles(_projectRoot, execFileSync) {
|
|
73
|
+
let diffRef;
|
|
74
|
+
|
|
75
|
+
// Detect default branch, then try merge-base
|
|
76
|
+
let baseBranch = 'main';
|
|
77
|
+
try {
|
|
78
|
+
baseBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'origin/HEAD'], {
|
|
79
|
+
encoding: 'utf8', timeout: 3000,
|
|
80
|
+
}).trim().replace('origin/', '');
|
|
81
|
+
} catch (_e) { // NOSONAR S2486
|
|
82
|
+
/* intentional: origin/HEAD not set, detect base branch manually */
|
|
83
|
+
for (const name of ['main', 'master']) {
|
|
84
|
+
try {
|
|
85
|
+
execFileSync('git', ['rev-parse', '--verify', name], { stdio: 'pipe', timeout: 3000 });
|
|
86
|
+
baseBranch = name;
|
|
87
|
+
break;
|
|
88
|
+
} catch (_e2) { /* intentional: branch doesn't exist, try next name */ } // NOSONAR S2486
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const mergeBase = execFileSync('git', ['merge-base', 'HEAD', baseBranch], {
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
timeout: 5000,
|
|
96
|
+
}).trim();
|
|
97
|
+
diffRef = `${mergeBase}...HEAD`;
|
|
98
|
+
} catch (_e) { /* intentional: merge-base failed, fallback to diff against HEAD */ // NOSONAR S2486
|
|
99
|
+
diffRef = 'HEAD';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let output;
|
|
103
|
+
try {
|
|
104
|
+
output = execFileSync('git', ['diff', '--name-only', diffRef], {
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
timeout: 5000,
|
|
107
|
+
}).trim();
|
|
108
|
+
} catch (_e) { // NOSONAR S2486
|
|
109
|
+
/* intentional: git diff failed, no affected files to report */
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!output) return [];
|
|
114
|
+
|
|
115
|
+
const changedFiles = output.split('\n').filter(Boolean);
|
|
116
|
+
|
|
117
|
+
// Map lib/*.js files to test/*.test.js
|
|
118
|
+
const testFiles = [];
|
|
119
|
+
for (const file of changedFiles) {
|
|
120
|
+
if (file.startsWith('lib/') && file.endsWith('.js')) {
|
|
121
|
+
const relative = file.slice('lib/'.length);
|
|
122
|
+
const testFile = `test/${relative.replace(/\.js$/, '.test.js')}`;
|
|
123
|
+
testFiles.push(testFile);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return testFiles;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
name: 'test',
|
|
132
|
+
description: 'Run tests with smart defaults (timeout, Beads skip, affected-only)',
|
|
133
|
+
usage: 'forge test [--affected]',
|
|
134
|
+
flags: {
|
|
135
|
+
'--affected': 'Run only tests for changed files',
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run tests with smart defaults.
|
|
140
|
+
*
|
|
141
|
+
* @param {string[]} _args - Positional arguments (unused)
|
|
142
|
+
* @param {Object} flags - Parsed flags ({ affected?: boolean })
|
|
143
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
144
|
+
* @param {Object} [deps] - Dependency injection for testability
|
|
145
|
+
* @param {Object} [deps.fs] - fs module
|
|
146
|
+
* @param {Function} [deps.execFileSync] - child_process.execFileSync
|
|
147
|
+
* @param {Function} [deps.spawnSync] - child_process.spawnSync
|
|
148
|
+
* @returns {Promise<{ success: boolean, exitCode: number, beadsSkipped: boolean }>}
|
|
149
|
+
*/
|
|
150
|
+
async handler(_args, flags, projectRoot, deps = {}) {
|
|
151
|
+
const fs = deps.fs || defaultFs;
|
|
152
|
+
const execFileSync = deps.execFileSync || defaultExecFileSync;
|
|
153
|
+
const spawnSync = deps.spawnSync || defaultSpawnSync;
|
|
154
|
+
|
|
155
|
+
// 1. Detect package manager
|
|
156
|
+
const pkgManager = detectPackageManager(projectRoot, fs);
|
|
157
|
+
|
|
158
|
+
// 2. Read timeout from package.json or use default
|
|
159
|
+
let timeout = DEFAULT_TIMEOUT;
|
|
160
|
+
try {
|
|
161
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
163
|
+
const testScript = pkg.scripts?.test || '';
|
|
164
|
+
const timeoutMatch = testScript.match(/--timeout\s+(\d+)/);
|
|
165
|
+
if (timeoutMatch) {
|
|
166
|
+
timeout = Number.parseInt(timeoutMatch[1], 10);
|
|
167
|
+
}
|
|
168
|
+
} catch (_e) { /* intentional: package.json missing or unreadable, use default timeout */ } // NOSONAR S2486
|
|
169
|
+
|
|
170
|
+
// 3. Check Beads connectivity
|
|
171
|
+
const beadsAvailable = checkBeadsConnectivity(execFileSync);
|
|
172
|
+
const beadsSkipped = !beadsAvailable;
|
|
173
|
+
|
|
174
|
+
const extraEnv = {};
|
|
175
|
+
if (beadsSkipped) {
|
|
176
|
+
extraEnv.BEADS_SKIP_TESTS = '1';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 4. Build test command args
|
|
180
|
+
let testArgs = ['run', 'test'];
|
|
181
|
+
|
|
182
|
+
// 5. --affected flag: find changed test files
|
|
183
|
+
if (flags['--affected'] || flags.affected) {
|
|
184
|
+
const affectedTests = getAffectedTestFiles(projectRoot, execFileSync);
|
|
185
|
+
if (affectedTests.length > 0) {
|
|
186
|
+
testArgs = ['run', 'test', ...affectedTests];
|
|
187
|
+
}
|
|
188
|
+
// If no affected tests found, fall back to running all tests
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 6. Run tests
|
|
192
|
+
const result = spawnSync(pkgManager, testArgs, {
|
|
193
|
+
env: { ...process.env, ...extraEnv },
|
|
194
|
+
timeout,
|
|
195
|
+
stdio: 'inherit',
|
|
196
|
+
shell: process.platform === 'win32',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const exitCode = result.status ?? 1;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
success: exitCode === 0,
|
|
203
|
+
exitCode,
|
|
204
|
+
beadsSkipped,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
};
|