@izkac/forgekit 0.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/bin/forge.mjs +100 -0
- package/bin/forgekit.mjs +84 -0
- package/bin/review.mjs +82 -0
- package/package.json +46 -0
- package/scripts/prepack.mjs +78 -0
- package/scripts/run-tests.mjs +43 -0
- package/src/adr.mjs +236 -0
- package/src/adr.test.mjs +170 -0
- package/src/change.mjs +234 -0
- package/src/change.test.mjs +83 -0
- package/src/cleanup-sessions.mjs +70 -0
- package/src/config.mjs +103 -0
- package/src/defer.mjs +75 -0
- package/src/doctor.mjs +341 -0
- package/src/doctor.test.mjs +114 -0
- package/src/init.mjs +575 -0
- package/src/install.mjs +777 -0
- package/src/install.test.mjs +104 -0
- package/src/integrity-check.mjs +58 -0
- package/src/integrity.mjs +317 -0
- package/src/integrity.test.mjs +296 -0
- package/src/lib/workspaces.mjs +55 -0
- package/src/lib.mjs +138 -0
- package/src/models.defaults.json +41 -0
- package/src/new-session.mjs +82 -0
- package/src/openspec-overlays/README.md +19 -0
- package/src/openspec-overlays/openspec-apply-change-footer.md +14 -0
- package/src/openspec-overlays/opsx-apply-completion-step.md +1 -0
- package/src/openspec-overlays/opsx-apply-implement-step.md +11 -0
- package/src/paths.mjs +92 -0
- package/src/plan-engine.mjs +260 -0
- package/src/plan-engine.test.mjs +245 -0
- package/src/preferences.defaults.json +78 -0
- package/src/preferences.mjs +438 -0
- package/src/preferences.test.mjs +174 -0
- package/src/record-evidence.mjs +204 -0
- package/src/record-evidence.test.mjs +260 -0
- package/src/resolve-model.mjs +312 -0
- package/src/resolve-model.test.mjs +194 -0
- package/src/review/carryforward.mjs +413 -0
- package/src/review/carryforward.test.mjs +587 -0
- package/src/review/cli.test.mjs +117 -0
- package/src/review/export.mjs +172 -0
- package/src/review/export.test.mjs +197 -0
- package/src/review/fixtures/valid-review.json +42 -0
- package/src/review/lib.mjs +894 -0
- package/src/review/lib.test.mjs +266 -0
- package/src/review/merge-tentative.mjs +292 -0
- package/src/review/merge-tentative.test.mjs +363 -0
- package/src/review/new-review.mjs +200 -0
- package/src/review/render.mjs +108 -0
- package/src/review/schema-consistency.test.mjs +83 -0
- package/src/review/schema.json +196 -0
- package/src/review/signals.mjs +144 -0
- package/src/review/signals.test.mjs +62 -0
- package/src/score-cli.mjs +68 -0
- package/src/score.mjs +489 -0
- package/src/score.test.mjs +253 -0
- package/src/session-reminder.mjs +168 -0
- package/src/session-status.mjs +70 -0
- package/src/set-models.mjs +186 -0
- package/src/set-phase.mjs +177 -0
- package/src/set-phase.test.mjs +317 -0
- package/src/set-prefs.mjs +294 -0
- package/src/spine.mjs +91 -0
- package/src/triage-prompt.mjs +175 -0
- package/src/triage-prompt.test.mjs +50 -0
- package/src/vendor-openspec-overlays.mjs +176 -0
- package/src/vendor-openspec-overlays.test.mjs +62 -0
- package/vendor/skills/archive-to-adr/SKILL.md +149 -0
- package/vendor/skills/forge/SKILL.md +136 -0
- package/vendor/skills/forge/phases/brainstorm.md +23 -0
- package/vendor/skills/forge/phases/finish.md +87 -0
- package/vendor/skills/forge/phases/implement.md +76 -0
- package/vendor/skills/forge/phases/plan-openspec.md +40 -0
- package/vendor/skills/forge/phases/plan-specs.md +97 -0
- package/vendor/skills/forge/phases/review.md +25 -0
- package/vendor/skills/forge/phases/verify.md +120 -0
- package/vendor/skills/forge/references/forge-layout.md +85 -0
- package/vendor/skills/forge/references/pace.md +115 -0
- package/vendor/skills/forge/references/plan-routing.md +51 -0
- package/vendor/skills/forge/references/runtime-integrity.md +157 -0
- package/vendor/skills/forge/references/substantial-work.md +37 -0
- package/vendor/skills/forge/references/tdd-core.md +29 -0
- package/vendor/skills/forge/references/test-evidence.md +30 -0
- package/vendor/skills/forge/references/test-strategy.md +68 -0
- package/vendor/skills/forge/skills/NOTICE.md +17 -0
- package/vendor/skills/forge/skills/brainstorming/SKILL.md +120 -0
- package/vendor/skills/forge/skills/requesting-code-review/SKILL.md +67 -0
- package/vendor/skills/forge/skills/requesting-code-review/code-reviewer.md +146 -0
- package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -0
- package/vendor/skills/forge/skills/systematic-debugging/SKILL.md +234 -0
- package/vendor/skills/forge/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/vendor/skills/forge/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/vendor/skills/forge/skills/systematic-debugging/find-polluter.sh +63 -0
- package/vendor/skills/forge/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/vendor/skills/forge/skills/test-driven-development/SKILL.md +290 -0
- package/vendor/skills/forge/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/vendor/skills/forge/skills/verification-before-completion/SKILL.md +59 -0
- package/vendor/skills/forge/subagents/final-reviewer-prompt.md +53 -0
- package/vendor/skills/forge/subagents/implementer-prompt.md +38 -0
- package/vendor/skills/forge/subagents/task-reviewer-prompt.md +61 -0
- package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -0
- package/vendor/skills/thorough-code-review/SKILL.md +290 -0
- package/vendor/skills/thorough-code-review/examples/accepted-risks-janus.md +32 -0
- package/vendor/skills/thorough-code-review/examples.md +133 -0
- package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -0
- package/vendor/skills/thorough-code-review/reference/lenses.md +96 -0
- package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -0
- package/vendor/skills/thorough-code-review/reference/phase1c-coverage.md +44 -0
- package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -0
- package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -0
- package/vendor/skills/thorough-code-review/reference/report-template.md +115 -0
- package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -0
- package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -0
- package/vendor/templates/adr/README.md +7 -0
- package/vendor/templates/adr/decisions.md +141 -0
- package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -0
- package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -0
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -0
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -0
- package/vendor/templates/project/claude/commands/forge-apply.md +75 -0
- package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -0
- package/vendor/templates/project/claude/commands/forge-build.md +17 -0
- package/vendor/templates/project/claude/commands/forge-plan.md +12 -0
- package/vendor/templates/project/claude/commands/forge-skip.md +14 -0
- package/vendor/templates/project/claude/commands/forge-status.md +16 -0
- package/vendor/templates/project/claude/commands/forge.md +16 -0
- package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -0
- package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -0
- package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -0
- package/vendor/templates/project/claude/rules/forge.md +16 -0
- package/vendor/templates/project/codex/rules/forge.md +10 -0
- package/vendor/templates/project/cursor/commands/forge-apply.md +75 -0
- package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -0
- package/vendor/templates/project/cursor/commands/forge-build.md +17 -0
- package/vendor/templates/project/cursor/commands/forge-plan.md +15 -0
- package/vendor/templates/project/cursor/commands/forge-skip.md +14 -0
- package/vendor/templates/project/cursor/commands/forge-status.md +16 -0
- package/vendor/templates/project/cursor/commands/forge.md +16 -0
- package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -0
- package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -0
- package/vendor/templates/project/cursor/rules/forge.mdc +21 -0
package/src/defer.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forge deferral registry — deferred wiring is tracked debt, not a checkbox.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* forge defer add --task <id> --reason "<why deferred>"
|
|
7
|
+
* forge defer resolve --task <id>
|
|
8
|
+
* forge defer list
|
|
9
|
+
* [--session <id>]
|
|
10
|
+
*
|
|
11
|
+
* Unresolved deferrals block `forge phase done|finish`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { loadSession, readActive } from './lib.mjs';
|
|
15
|
+
import { addDeferral, loadDeferrals, resolveDeferral } from './integrity.mjs';
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const sub = args[0] && !args[0].startsWith('--') ? args[0] : 'list';
|
|
19
|
+
|
|
20
|
+
if (args[0] === '--help' || sub === 'help') {
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
'Usage: forge defer [add --task <id> --reason "<why>" | resolve --task <id> | list] [--session <id>]\n',
|
|
23
|
+
);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let sessionId = null;
|
|
28
|
+
let task = null;
|
|
29
|
+
let reason = null;
|
|
30
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
31
|
+
if (args[i] === '--session' && args[i + 1]) {
|
|
32
|
+
sessionId = args[i + 1];
|
|
33
|
+
i += 1;
|
|
34
|
+
} else if (args[i] === '--task' && args[i + 1]) {
|
|
35
|
+
task = args[i + 1];
|
|
36
|
+
i += 1;
|
|
37
|
+
} else if (args[i] === '--reason' && args[i + 1]) {
|
|
38
|
+
reason = args[i + 1];
|
|
39
|
+
i += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!sessionId) {
|
|
44
|
+
const active = readActive();
|
|
45
|
+
sessionId = active?.sessionId;
|
|
46
|
+
}
|
|
47
|
+
if (!sessionId) {
|
|
48
|
+
process.stderr.write('No active session. Run forge new first.\n');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { dir } = loadSession(sessionId);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
if (sub === 'add') {
|
|
56
|
+
const doc = addDeferral(dir, { task: task ?? '', reason: reason ?? '' });
|
|
57
|
+
process.stdout.write(JSON.stringify(doc, null, 2));
|
|
58
|
+
process.stdout.write('\n');
|
|
59
|
+
} else if (sub === 'resolve') {
|
|
60
|
+
if (!task) throw new Error('resolve requires --task <id>');
|
|
61
|
+
const doc = resolveDeferral(dir, task);
|
|
62
|
+
process.stdout.write(JSON.stringify(doc, null, 2));
|
|
63
|
+
process.stdout.write('\n');
|
|
64
|
+
} else if (sub === 'list') {
|
|
65
|
+
const doc = loadDeferrals(dir);
|
|
66
|
+
const open = doc.deferrals.filter((d) => !d.resolvedAt).length;
|
|
67
|
+
process.stdout.write(JSON.stringify({ open, ...doc }, null, 2));
|
|
68
|
+
process.stdout.write('\n');
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Unknown subcommand: ${sub}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
process.stderr.write(`${err instanceof Error ? err.message : err}\n`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Forge readiness checks (OpenSpec project + CLI).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* forge doctor
|
|
7
|
+
* forge doctor --json
|
|
8
|
+
* forge doctor --install
|
|
9
|
+
* forge doctor --warn-only # always exit 0 (for forge:new)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import {
|
|
17
|
+
OPENSPEC_PACKAGE,
|
|
18
|
+
OPENSPEC_INSTALL_CMD,
|
|
19
|
+
resolveProjectPlanEngine,
|
|
20
|
+
} from './plan-engine.mjs';
|
|
21
|
+
|
|
22
|
+
export { OPENSPEC_PACKAGE, OPENSPEC_INSTALL_CMD };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string[]} argv
|
|
26
|
+
*/
|
|
27
|
+
export function parseArgs(argv) {
|
|
28
|
+
const opts = {
|
|
29
|
+
json: false,
|
|
30
|
+
install: false,
|
|
31
|
+
warnOnly: false,
|
|
32
|
+
cwd: null,
|
|
33
|
+
help: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
37
|
+
const arg = argv[i];
|
|
38
|
+
if (arg === '--json') opts.json = true;
|
|
39
|
+
else if (arg === '--install') opts.install = true;
|
|
40
|
+
else if (arg === '--warn-only') opts.warnOnly = true;
|
|
41
|
+
else if (arg === '--cwd') opts.cwd = argv[++i];
|
|
42
|
+
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
43
|
+
else if (arg === '--') continue;
|
|
44
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return opts;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printHelp() {
|
|
51
|
+
process.stdout.write(`Usage: forge doctor [options]
|
|
52
|
+
|
|
53
|
+
Check planning-engine readiness. OpenSpec projects: config + CLI availability.
|
|
54
|
+
Specs-engine projects (.forge/config.json → plan.engine: specs): specs/changes/ layout.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--json Machine-readable report
|
|
58
|
+
--install Attempt: ${OPENSPEC_INSTALL_CMD}
|
|
59
|
+
--warn-only Print warnings but exit 0 (used by forge:new)
|
|
60
|
+
--cwd <path> Project root (default: process.cwd())
|
|
61
|
+
--help
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {{ cwd: string, existsSync?: typeof fs.existsSync }} opts
|
|
67
|
+
*/
|
|
68
|
+
export function checkOpenSpecProject(opts) {
|
|
69
|
+
const existsSync = opts.existsSync ?? fs.existsSync;
|
|
70
|
+
const configPath = path.join(opts.cwd, 'openspec', 'config.yaml');
|
|
71
|
+
const ok = existsSync(configPath);
|
|
72
|
+
return {
|
|
73
|
+
id: 'openspec-project',
|
|
74
|
+
ok,
|
|
75
|
+
configPath,
|
|
76
|
+
message: ok
|
|
77
|
+
? 'openspec/config.yaml found'
|
|
78
|
+
: 'openspec/config.yaml missing — run openspec init in the repo root if this is a new project',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {{
|
|
84
|
+
* runCommand?: (cmd: string, args: string[], opts?: { cwd?: string }) => { status: number | null, stdout: string, stderr: string },
|
|
85
|
+
* cwd?: string,
|
|
86
|
+
* }} [opts]
|
|
87
|
+
*/
|
|
88
|
+
export function checkOpenSpecCli(opts = {}) {
|
|
89
|
+
const run =
|
|
90
|
+
opts.runCommand ??
|
|
91
|
+
((cmd, args, runOpts = {}) => {
|
|
92
|
+
// Prefer argv form without shell. On Windows, fall back to a single
|
|
93
|
+
// shell string so `.cmd` shims resolve without DEP0190 (args+shell).
|
|
94
|
+
let result = spawnSync(cmd, args, {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
shell: false,
|
|
97
|
+
cwd: runOpts.cwd,
|
|
98
|
+
});
|
|
99
|
+
if (result.error && process.platform === 'win32') {
|
|
100
|
+
const line = [cmd, ...args].join(' ');
|
|
101
|
+
result = spawnSync(line, {
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
shell: true,
|
|
104
|
+
cwd: runOpts.cwd,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
status: result.status,
|
|
109
|
+
stdout: result.stdout || '',
|
|
110
|
+
stderr: result.stderr || '',
|
|
111
|
+
error: result.error,
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const attempt = run('openspec', ['--version'], { cwd: opts.cwd });
|
|
116
|
+
if (attempt.status === 0) {
|
|
117
|
+
const version = String(attempt.stdout || '').trim().split(/\r?\n/)[0] || 'unknown';
|
|
118
|
+
return {
|
|
119
|
+
id: 'openspec-cli',
|
|
120
|
+
ok: true,
|
|
121
|
+
version,
|
|
122
|
+
message: `openspec CLI available (${version})`,
|
|
123
|
+
installCommand: OPENSPEC_INSTALL_CMD,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
id: 'openspec-cli',
|
|
129
|
+
ok: false,
|
|
130
|
+
version: null,
|
|
131
|
+
message:
|
|
132
|
+
`openspec CLI not found on PATH. Install with:\n ${OPENSPEC_INSTALL_CMD}\n` +
|
|
133
|
+
`Then re-run: forge doctor`,
|
|
134
|
+
installCommand: OPENSPEC_INSTALL_CMD,
|
|
135
|
+
detail: String(attempt.stderr || attempt.stdout || attempt.error || '').trim() || null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {{ cwd: string, dir: string, existsSync?: typeof fs.existsSync }} opts
|
|
141
|
+
*/
|
|
142
|
+
export function checkSpecsProject(opts) {
|
|
143
|
+
const existsSync = opts.existsSync ?? fs.existsSync;
|
|
144
|
+
const changesPath = path.join(opts.cwd, opts.dir, 'changes');
|
|
145
|
+
const ok = existsSync(changesPath);
|
|
146
|
+
return {
|
|
147
|
+
id: 'specs-project',
|
|
148
|
+
ok,
|
|
149
|
+
changesPath,
|
|
150
|
+
message: ok
|
|
151
|
+
? `${opts.dir}/changes/ found (built-in specs engine)`
|
|
152
|
+
: `${opts.dir}/changes/ missing — run \`forge init --no-openspec\` to scaffold the specs engine`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {{
|
|
158
|
+
* cwd?: string,
|
|
159
|
+
* install?: boolean,
|
|
160
|
+
* existsSync?: typeof fs.existsSync,
|
|
161
|
+
* runCommand?: Function,
|
|
162
|
+
* }} [opts]
|
|
163
|
+
*/
|
|
164
|
+
export function runDoctorChecks(opts = {}) {
|
|
165
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
166
|
+
const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
|
|
167
|
+
|
|
168
|
+
if (engine.engine === 'specs') {
|
|
169
|
+
const project = checkSpecsProject({
|
|
170
|
+
cwd,
|
|
171
|
+
dir: engine.dir,
|
|
172
|
+
existsSync: opts.existsSync,
|
|
173
|
+
});
|
|
174
|
+
const cli = {
|
|
175
|
+
id: 'openspec-cli',
|
|
176
|
+
ok: true,
|
|
177
|
+
skipped: true,
|
|
178
|
+
version: null,
|
|
179
|
+
message: 'built-in specs engine — OpenSpec CLI not required',
|
|
180
|
+
installCommand: OPENSPEC_INSTALL_CMD,
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
ok: project.ok,
|
|
184
|
+
engine: engine.engine,
|
|
185
|
+
checks: { project, cli },
|
|
186
|
+
installCommand: OPENSPEC_INSTALL_CMD,
|
|
187
|
+
actions: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const project = checkOpenSpecProject({ cwd, existsSync: opts.existsSync });
|
|
192
|
+
let cli = checkOpenSpecCli({ cwd, runCommand: opts.runCommand });
|
|
193
|
+
|
|
194
|
+
/** @type {string[]} */
|
|
195
|
+
const actions = [];
|
|
196
|
+
|
|
197
|
+
if (opts.install && !cli.ok) {
|
|
198
|
+
actions.push(OPENSPEC_INSTALL_CMD);
|
|
199
|
+
const run =
|
|
200
|
+
opts.runCommand ??
|
|
201
|
+
((cmd, args) => {
|
|
202
|
+
let result = spawnSync(cmd, args, {
|
|
203
|
+
encoding: 'utf8',
|
|
204
|
+
shell: false,
|
|
205
|
+
cwd,
|
|
206
|
+
});
|
|
207
|
+
if (result.error && process.platform === 'win32') {
|
|
208
|
+
result = spawnSync([cmd, ...args].join(' '), {
|
|
209
|
+
encoding: 'utf8',
|
|
210
|
+
shell: true,
|
|
211
|
+
cwd,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
status: result.status,
|
|
216
|
+
stdout: result.stdout || '',
|
|
217
|
+
stderr: result.stderr || '',
|
|
218
|
+
error: result.error,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const installResult = run('npm', ['install', '-g', OPENSPEC_PACKAGE], { cwd });
|
|
222
|
+
actions.push(
|
|
223
|
+
installResult.status === 0
|
|
224
|
+
? 'install: ok'
|
|
225
|
+
: `install: failed (${String(installResult.stderr || installResult.stdout || '').trim() || installResult.status})`,
|
|
226
|
+
);
|
|
227
|
+
cli = checkOpenSpecCli({ cwd, runCommand: opts.runCommand });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const ok = project.ok && cli.ok;
|
|
231
|
+
return {
|
|
232
|
+
ok,
|
|
233
|
+
engine: engine.engine,
|
|
234
|
+
checks: { project, cli },
|
|
235
|
+
installCommand: OPENSPEC_INSTALL_CMD,
|
|
236
|
+
actions,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string[]} argv
|
|
242
|
+
* @param {{
|
|
243
|
+
* cwd?: string,
|
|
244
|
+
* stdout?: NodeJS.WritableStream,
|
|
245
|
+
* stderr?: NodeJS.WritableStream,
|
|
246
|
+
* existsSync?: typeof fs.existsSync,
|
|
247
|
+
* runCommand?: Function,
|
|
248
|
+
* }} [io]
|
|
249
|
+
*/
|
|
250
|
+
export function runDoctor(argv, io = {}) {
|
|
251
|
+
const stdout = io.stdout ?? process.stdout;
|
|
252
|
+
const stderr = io.stderr ?? process.stderr;
|
|
253
|
+
const cwd = io.cwd ?? process.cwd();
|
|
254
|
+
|
|
255
|
+
let opts;
|
|
256
|
+
try {
|
|
257
|
+
opts = parseArgs(argv);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
260
|
+
stderr.write(`${msg}\n`);
|
|
261
|
+
return 2;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (opts.help) {
|
|
265
|
+
printHelp();
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const report = runDoctorChecks({
|
|
270
|
+
cwd: opts.cwd ?? cwd,
|
|
271
|
+
install: opts.install,
|
|
272
|
+
existsSync: io.existsSync,
|
|
273
|
+
runCommand: io.runCommand,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (opts.json) {
|
|
277
|
+
stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
278
|
+
} else {
|
|
279
|
+
const { project, cli } = report.checks;
|
|
280
|
+
stdout.write(`Forge doctor (plan engine: ${report.engine ?? 'openspec'})\n`);
|
|
281
|
+
stdout.write(` [${project.ok ? 'ok' : 'FAIL'}] ${project.message}\n`);
|
|
282
|
+
stdout.write(` [${cli.ok ? 'ok' : 'FAIL'}] ${cli.message}\n`);
|
|
283
|
+
if (!cli.ok) {
|
|
284
|
+
stdout.write(`\nOffer: install OpenSpec CLI?\n ${cli.installCommand}\n`);
|
|
285
|
+
stdout.write(`Or re-run with: forge doctor --install\n`);
|
|
286
|
+
}
|
|
287
|
+
for (const action of report.actions) {
|
|
288
|
+
stdout.write(` action: ${action}\n`);
|
|
289
|
+
}
|
|
290
|
+
stdout.write(report.ok ? '\nAll checks passed.\n' : '\nDoctor found issues.\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (opts.warnOnly) return 0;
|
|
294
|
+
return report.ok ? 0 : 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Warn-only helper for forge:new (never throws).
|
|
299
|
+
* @param {{
|
|
300
|
+
* cwd?: string,
|
|
301
|
+
* stderr?: NodeJS.WritableStream,
|
|
302
|
+
* existsSync?: typeof fs.existsSync,
|
|
303
|
+
* runCommand?: Function,
|
|
304
|
+
* }} [opts]
|
|
305
|
+
*/
|
|
306
|
+
export function warnIfDoctorFails(opts = {}) {
|
|
307
|
+
const stderr = opts.stderr ?? process.stderr;
|
|
308
|
+
try {
|
|
309
|
+
const report = runDoctorChecks({
|
|
310
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
311
|
+
existsSync: opts.existsSync,
|
|
312
|
+
runCommand: opts.runCommand,
|
|
313
|
+
});
|
|
314
|
+
if (!report.ok) {
|
|
315
|
+
stderr.write('[forge:doctor] plan-engine readiness check failed:\n');
|
|
316
|
+
if (!report.checks.project.ok) {
|
|
317
|
+
stderr.write(` - ${report.checks.project.message}\n`);
|
|
318
|
+
}
|
|
319
|
+
if (!report.checks.cli.ok) {
|
|
320
|
+
stderr.write(` - ${report.checks.cli.message}\n`);
|
|
321
|
+
}
|
|
322
|
+
if (!report.checks.cli.ok) {
|
|
323
|
+
stderr.write(` Install: ${report.installCommand}\n`);
|
|
324
|
+
stderr.write(' Or: forge doctor --install\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return report;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
330
|
+
stderr.write(`[forge:doctor] check error: ${msg}\n`);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const isMain =
|
|
336
|
+
process.argv[1] &&
|
|
337
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
338
|
+
|
|
339
|
+
if (isMain) {
|
|
340
|
+
process.exitCode = runDoctor(process.argv.slice(2));
|
|
341
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Writable } from 'node:stream';
|
|
5
|
+
import {
|
|
6
|
+
OPENSPEC_INSTALL_CMD,
|
|
7
|
+
checkOpenSpecCli,
|
|
8
|
+
checkOpenSpecProject,
|
|
9
|
+
runDoctor,
|
|
10
|
+
runDoctorChecks,
|
|
11
|
+
warnIfDoctorFails,
|
|
12
|
+
} from './doctor.mjs';
|
|
13
|
+
|
|
14
|
+
function capture() {
|
|
15
|
+
let text = '';
|
|
16
|
+
const stream = new Writable({
|
|
17
|
+
write(chunk, _enc, cb) {
|
|
18
|
+
text += String(chunk);
|
|
19
|
+
cb();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
return { stream, text: () => text };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('checkOpenSpecProject ok when config exists', () => {
|
|
26
|
+
const result = checkOpenSpecProject({
|
|
27
|
+
cwd: '/repo',
|
|
28
|
+
existsSync: (p) => p.replace(/\\/g, '/') === '/repo/openspec/config.yaml',
|
|
29
|
+
});
|
|
30
|
+
assert.equal(result.ok, true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('checkOpenSpecProject fails when missing', () => {
|
|
34
|
+
const result = checkOpenSpecProject({
|
|
35
|
+
cwd: '/repo',
|
|
36
|
+
existsSync: () => false,
|
|
37
|
+
});
|
|
38
|
+
assert.equal(result.ok, false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('checkOpenSpecCli ok when version exits 0', () => {
|
|
42
|
+
const result = checkOpenSpecCli({
|
|
43
|
+
runCommand: () => ({ status: 0, stdout: '1.2.0\n', stderr: '' }),
|
|
44
|
+
});
|
|
45
|
+
assert.equal(result.ok, true);
|
|
46
|
+
assert.equal(result.version, '1.2.0');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('checkOpenSpecCli missing offers install', () => {
|
|
50
|
+
const result = checkOpenSpecCli({
|
|
51
|
+
runCommand: () => ({ status: 1, stdout: '', stderr: 'not found' }),
|
|
52
|
+
});
|
|
53
|
+
assert.equal(result.ok, false);
|
|
54
|
+
assert.match(result.message, /@fission-ai\/openspec/);
|
|
55
|
+
assert.equal(result.installCommand, OPENSPEC_INSTALL_CMD);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('runDoctorChecks aggregates', () => {
|
|
59
|
+
const report = runDoctorChecks({
|
|
60
|
+
cwd: '/repo',
|
|
61
|
+
existsSync: () => true,
|
|
62
|
+
runCommand: () => ({ status: 0, stdout: '1.0.0', stderr: '' }),
|
|
63
|
+
});
|
|
64
|
+
assert.equal(report.ok, true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('runDoctor --warn-only exits 0 on failure', () => {
|
|
68
|
+
const out = capture();
|
|
69
|
+
const err = capture();
|
|
70
|
+
const code = runDoctor(['--warn-only'], {
|
|
71
|
+
cwd: '/repo',
|
|
72
|
+
stdout: out.stream,
|
|
73
|
+
stderr: err.stream,
|
|
74
|
+
existsSync: () => true,
|
|
75
|
+
runCommand: () => ({ status: 1, stdout: '', stderr: 'missing' }),
|
|
76
|
+
});
|
|
77
|
+
assert.equal(code, 0);
|
|
78
|
+
assert.match(out.text(), /FAIL|not found|Install/i);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('runDoctor exits 1 when CLI missing', () => {
|
|
82
|
+
const out = capture();
|
|
83
|
+
const err = capture();
|
|
84
|
+
const code = runDoctor([], {
|
|
85
|
+
cwd: '/repo',
|
|
86
|
+
stdout: out.stream,
|
|
87
|
+
stderr: err.stream,
|
|
88
|
+
existsSync: () => true,
|
|
89
|
+
runCommand: () => ({ status: 1, stdout: '', stderr: 'missing' }),
|
|
90
|
+
});
|
|
91
|
+
assert.equal(code, 1);
|
|
92
|
+
assert.match(out.text(), /npm install -g @fission-ai\/openspec/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('warnIfDoctorFails writes to stderr', () => {
|
|
96
|
+
const err = capture();
|
|
97
|
+
const report = warnIfDoctorFails({
|
|
98
|
+
cwd: '/repo',
|
|
99
|
+
stderr: err.stream,
|
|
100
|
+
existsSync: () => false,
|
|
101
|
+
runCommand: () => ({ status: 1, stdout: '', stderr: '' }),
|
|
102
|
+
});
|
|
103
|
+
assert.equal(report.ok, false);
|
|
104
|
+
assert.match(err.text(), /forge:doctor/);
|
|
105
|
+
assert.match(err.text(), /install/i);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('path join uses openspec/config.yaml', () => {
|
|
109
|
+
const result = checkOpenSpecProject({
|
|
110
|
+
cwd: path.join('S:', 'Projects', 'janus'),
|
|
111
|
+
existsSync: (p) => p.endsWith(path.join('openspec', 'config.yaml')),
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result.ok, true);
|
|
114
|
+
});
|