@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/init.mjs
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Wire Forge into a project: commands, thin rules, hooks, .forge gitignore.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* forge init # interactive agent picker
|
|
7
|
+
* forge init --cursor --claude
|
|
8
|
+
* forge init --all
|
|
9
|
+
* forge init --overlay # also apply OpenSpec vendor overlays
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import readline from 'node:readline/promises';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_ADR_DIR,
|
|
20
|
+
disableProjectAdr,
|
|
21
|
+
loadUserConfig,
|
|
22
|
+
normalizeAdrDir,
|
|
23
|
+
scaffoldAdr,
|
|
24
|
+
} from './adr.mjs';
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_SPECS_DIR,
|
|
27
|
+
hasOpenSpecConfig,
|
|
28
|
+
loadUserPlanEngine,
|
|
29
|
+
scaffoldSpecs,
|
|
30
|
+
setupOpenSpec,
|
|
31
|
+
writeProjectPlanConfig,
|
|
32
|
+
} from './plan-engine.mjs';
|
|
33
|
+
import { resolveAsset } from './paths.mjs';
|
|
34
|
+
|
|
35
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string[]} argv
|
|
39
|
+
*/
|
|
40
|
+
export function parseArgs(argv) {
|
|
41
|
+
const opts = {
|
|
42
|
+
all: false,
|
|
43
|
+
help: false,
|
|
44
|
+
force: false,
|
|
45
|
+
overlay: false,
|
|
46
|
+
agents: /** @type {string[]} */ ([]),
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
/** @type {boolean | null} */
|
|
49
|
+
adr: /** @type {boolean | null} */ (null),
|
|
50
|
+
adrDir: /** @type {string | null} */ (null),
|
|
51
|
+
/** @type {boolean | null} true=openspec, false=specs, null=detect/prompt */
|
|
52
|
+
openspec: /** @type {boolean | null} */ (null),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
56
|
+
const arg = argv[i];
|
|
57
|
+
if (arg === '--all') opts.all = true;
|
|
58
|
+
else if (arg === '--force' || arg === '-f') opts.force = true;
|
|
59
|
+
else if (arg === '--overlay') opts.overlay = true;
|
|
60
|
+
else if (arg === '--cwd') opts.cwd = argv[++i];
|
|
61
|
+
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
62
|
+
else if (arg === '--cursor') opts.agents.push('cursor');
|
|
63
|
+
else if (arg === '--claude' || arg === '--claude-code') opts.agents.push('claude');
|
|
64
|
+
else if (arg === '--codex') opts.agents.push('codex');
|
|
65
|
+
else if (arg === '--adr') opts.adr = true;
|
|
66
|
+
else if (arg === '--no-adr') opts.adr = false;
|
|
67
|
+
else if (arg === '--adr-dir') opts.adrDir = argv[++i];
|
|
68
|
+
else if (arg === '--openspec') opts.openspec = true;
|
|
69
|
+
else if (arg === '--no-openspec') opts.openspec = false;
|
|
70
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return opts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printHelp() {
|
|
77
|
+
process.stdout.write(`Usage: forge init [options]
|
|
78
|
+
|
|
79
|
+
Wire Forge commands, thin rules, and hooks into the current project.
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
--cursor Cursor (.cursor/commands, rules, hooks)
|
|
83
|
+
--claude Claude Code (.claude/commands, rules, hooks)
|
|
84
|
+
--codex Codex CLI (.codex/rules)
|
|
85
|
+
--all All of the above
|
|
86
|
+
--openspec Plan with OpenSpec (offer install + \`openspec init\` if missing)
|
|
87
|
+
--no-openspec Plan with the built-in specs engine (${DEFAULT_SPECS_DIR}/changes/)
|
|
88
|
+
--adr Enable ADRs (scaffold decisions.md + ADR dir + hooks)
|
|
89
|
+
--no-adr Disable ADRs for this project
|
|
90
|
+
--adr-dir <path> ADR directory (default: ${DEFAULT_ADR_DIR} or ~/.forgekit preference)
|
|
91
|
+
--overlay Also run \`forge overlay\` (OpenSpec vendor patches)
|
|
92
|
+
--force, -f Overwrite existing template files
|
|
93
|
+
--cwd <path> Project root (default: cwd)
|
|
94
|
+
--help
|
|
95
|
+
|
|
96
|
+
Requires the Forge skill already installed (\`forge install\`) for agents
|
|
97
|
+
to load skill content. Init only adds project-local wiring.
|
|
98
|
+
|
|
99
|
+
Interactive (TTY): when --openspec/--no-openspec omitted and OpenSpec is not
|
|
100
|
+
already set up, offers to install + set it up (decline = built-in specs
|
|
101
|
+
engine). When --adr/--no-adr omitted, asks whether to use ADRs and for the
|
|
102
|
+
directory inside the repo.
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
export function resolveTemplatesRoot() {
|
|
110
|
+
return resolveAsset('templates/project');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} src
|
|
115
|
+
* @param {string} dest
|
|
116
|
+
* @param {{ force?: boolean }} opts
|
|
117
|
+
*/
|
|
118
|
+
function copyFile(src, dest, opts) {
|
|
119
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
120
|
+
if (fs.existsSync(dest) && !opts.force) {
|
|
121
|
+
return 'skipped';
|
|
122
|
+
}
|
|
123
|
+
fs.copyFileSync(src, dest);
|
|
124
|
+
return 'written';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} srcDir
|
|
129
|
+
* @param {string} destDir
|
|
130
|
+
* @param {{ force?: boolean }} opts
|
|
131
|
+
*/
|
|
132
|
+
function copyDirFiles(srcDir, destDir, opts) {
|
|
133
|
+
/** @type {{ file: string, status: string }[]} */
|
|
134
|
+
const out = [];
|
|
135
|
+
if (!fs.existsSync(srcDir)) return out;
|
|
136
|
+
for (const name of fs.readdirSync(srcDir)) {
|
|
137
|
+
const from = path.join(srcDir, name);
|
|
138
|
+
if (!fs.statSync(from).isFile()) continue;
|
|
139
|
+
const to = path.join(destDir, name);
|
|
140
|
+
out.push({ file: path.relative(opts.cwd ?? destDir, to) || to, status: copyFile(from, to, opts) });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ensure .forge/.gitignore exists.
|
|
147
|
+
* @param {string} cwd
|
|
148
|
+
*/
|
|
149
|
+
export function ensureForgeGitignore(cwd) {
|
|
150
|
+
const forgeDir = path.join(cwd, '.forge');
|
|
151
|
+
fs.mkdirSync(forgeDir, { recursive: true });
|
|
152
|
+
const gi = path.join(forgeDir, '.gitignore');
|
|
153
|
+
const body = `# Forge session scratch — keep layout docs + committed project config
|
|
154
|
+
*
|
|
155
|
+
!.gitignore
|
|
156
|
+
!README.md
|
|
157
|
+
!config.json
|
|
158
|
+
`;
|
|
159
|
+
if (!fs.existsSync(gi)) {
|
|
160
|
+
fs.writeFileSync(gi, body, 'utf8');
|
|
161
|
+
return 'written';
|
|
162
|
+
}
|
|
163
|
+
// Upgrade older scaffolds that omit config.json
|
|
164
|
+
const existing = fs.readFileSync(gi, 'utf8');
|
|
165
|
+
if (!existing.includes('!config.json')) {
|
|
166
|
+
const next = existing.trimEnd().endsWith('!README.md')
|
|
167
|
+
? `${existing.trimEnd()}\n!config.json\n`
|
|
168
|
+
: `${existing.trimEnd()}\n!config.json\n`;
|
|
169
|
+
fs.writeFileSync(gi, next, 'utf8');
|
|
170
|
+
return 'updated';
|
|
171
|
+
}
|
|
172
|
+
return 'exists';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Write a short project README under .forge/ if missing.
|
|
177
|
+
* @param {string} cwd
|
|
178
|
+
*/
|
|
179
|
+
export function ensureForgeReadme(cwd) {
|
|
180
|
+
const readme = path.join(cwd, '.forge', 'README.md');
|
|
181
|
+
if (fs.existsSync(readme)) return 'exists';
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
readme,
|
|
184
|
+
`# \`.forge/\` — Forge session scratch
|
|
185
|
+
|
|
186
|
+
Per-checkout, **gitignored** workspace for Forge session orchestration.
|
|
187
|
+
Canonical specs live in \`openspec/\`; this directory holds session-local artefacts only.
|
|
188
|
+
|
|
189
|
+
\`\`\`bash
|
|
190
|
+
forge new <slug>
|
|
191
|
+
forge status
|
|
192
|
+
forge prefs
|
|
193
|
+
forge models
|
|
194
|
+
forge cleanup
|
|
195
|
+
\`\`\`
|
|
196
|
+
|
|
197
|
+
See the Forge skill and forgekit docs for the full workflow.
|
|
198
|
+
`,
|
|
199
|
+
'utf8',
|
|
200
|
+
);
|
|
201
|
+
return 'written';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Append hook registrations into Claude settings.json if present / create stub note.
|
|
206
|
+
* @param {string} cwd
|
|
207
|
+
* @param {{ force?: boolean }} opts
|
|
208
|
+
*/
|
|
209
|
+
export function ensureClaudeHookHints(cwd, opts) {
|
|
210
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
211
|
+
const notePath = path.join(cwd, '.claude', 'forge-hooks.snippet.json');
|
|
212
|
+
const snippet = {
|
|
213
|
+
_comment:
|
|
214
|
+
'Merge these hooks into .claude/settings.json (SessionStart + UserPromptSubmit). Paths assume forge CLI is on PATH.',
|
|
215
|
+
hooks: {
|
|
216
|
+
SessionStart: [
|
|
217
|
+
{
|
|
218
|
+
hooks: [
|
|
219
|
+
{
|
|
220
|
+
type: 'command',
|
|
221
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-session-start.mjs"',
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
UserPromptSubmit: [
|
|
227
|
+
{
|
|
228
|
+
hooks: [
|
|
229
|
+
{
|
|
230
|
+
type: 'command',
|
|
231
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-triage-hook.mjs"',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
type: 'command',
|
|
235
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-prompt-hook.mjs"',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
fs.mkdirSync(path.dirname(notePath), { recursive: true });
|
|
243
|
+
if (!fs.existsSync(notePath) || opts.force) {
|
|
244
|
+
fs.writeFileSync(notePath, `${JSON.stringify(snippet, null, 2)}\n`, 'utf8');
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
settingsExists: fs.existsSync(settingsPath),
|
|
248
|
+
snippet: notePath,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Write Cursor hooks.json snippet.
|
|
254
|
+
* @param {string} cwd
|
|
255
|
+
* @param {{ force?: boolean }} opts
|
|
256
|
+
*/
|
|
257
|
+
export function ensureCursorHookHints(cwd, opts) {
|
|
258
|
+
const notePath = path.join(cwd, '.cursor', 'forge-hooks.snippet.json');
|
|
259
|
+
const snippet = {
|
|
260
|
+
_comment:
|
|
261
|
+
'Merge into .cursor/hooks.json. Requires Node on PATH (forge-session-start.mjs).',
|
|
262
|
+
version: 1,
|
|
263
|
+
hooks: {
|
|
264
|
+
sessionStart: [
|
|
265
|
+
{
|
|
266
|
+
command: 'node .cursor/hooks/forge-session-start.mjs',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
fs.mkdirSync(path.dirname(notePath), { recursive: true });
|
|
272
|
+
if (!fs.existsSync(notePath) || opts.force) {
|
|
273
|
+
fs.writeFileSync(notePath, `${JSON.stringify(snippet, null, 2)}\n`, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
return notePath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {string[]} selected
|
|
280
|
+
* @param {{ cwd: string, force?: boolean, overlay?: boolean, templatesRoot?: string, adr?: boolean | null, adrDir?: string | null, home?: string, planEngine?: string | null }} opts
|
|
281
|
+
*/
|
|
282
|
+
export function initProject(selected, opts) {
|
|
283
|
+
const templates = opts.templatesRoot ?? resolveTemplatesRoot();
|
|
284
|
+
const cwd = opts.cwd;
|
|
285
|
+
/** @type {Record<string, unknown>} */
|
|
286
|
+
const report = {
|
|
287
|
+
cwd,
|
|
288
|
+
gitignore: ensureForgeGitignore(cwd),
|
|
289
|
+
readme: ensureForgeReadme(cwd),
|
|
290
|
+
files: /** @type {{ file: string, status: string }[]} */ ([]),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const copyOpts = { force: opts.force, cwd };
|
|
294
|
+
|
|
295
|
+
if (selected.includes('cursor')) {
|
|
296
|
+
report.files.push(
|
|
297
|
+
...copyDirFiles(
|
|
298
|
+
path.join(templates, 'cursor', 'commands'),
|
|
299
|
+
path.join(cwd, '.cursor', 'commands'),
|
|
300
|
+
copyOpts,
|
|
301
|
+
),
|
|
302
|
+
...copyDirFiles(
|
|
303
|
+
path.join(templates, 'cursor', 'rules'),
|
|
304
|
+
path.join(cwd, '.cursor', 'rules'),
|
|
305
|
+
copyOpts,
|
|
306
|
+
),
|
|
307
|
+
...copyDirFiles(
|
|
308
|
+
path.join(templates, 'cursor', 'hooks'),
|
|
309
|
+
path.join(cwd, '.cursor', 'hooks'),
|
|
310
|
+
copyOpts,
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
report.cursorHookSnippet = ensureCursorHookHints(cwd, copyOpts);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (selected.includes('claude')) {
|
|
317
|
+
report.files.push(
|
|
318
|
+
...copyDirFiles(
|
|
319
|
+
path.join(templates, 'claude', 'commands'),
|
|
320
|
+
path.join(cwd, '.claude', 'commands'),
|
|
321
|
+
copyOpts,
|
|
322
|
+
),
|
|
323
|
+
...copyDirFiles(
|
|
324
|
+
path.join(templates, 'claude', 'rules'),
|
|
325
|
+
path.join(cwd, '.claude', 'rules'),
|
|
326
|
+
copyOpts,
|
|
327
|
+
),
|
|
328
|
+
...copyDirFiles(
|
|
329
|
+
path.join(templates, 'claude', 'hooks'),
|
|
330
|
+
path.join(cwd, '.claude', 'hooks'),
|
|
331
|
+
copyOpts,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
report.claudeHooks = ensureClaudeHookHints(cwd, copyOpts);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (selected.includes('codex')) {
|
|
338
|
+
report.files.push(
|
|
339
|
+
...copyDirFiles(
|
|
340
|
+
path.join(templates, 'codex', 'rules'),
|
|
341
|
+
path.join(cwd, '.codex', 'rules'),
|
|
342
|
+
copyOpts,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (opts.planEngine === 'specs') {
|
|
348
|
+
const scaffold = scaffoldSpecs(cwd, { force: opts.force });
|
|
349
|
+
const config = writeProjectPlanConfig(cwd, {
|
|
350
|
+
engine: 'specs',
|
|
351
|
+
dir: scaffold.dir,
|
|
352
|
+
});
|
|
353
|
+
report.plan = { engine: 'specs', dir: scaffold.dir, files: scaffold.files, config };
|
|
354
|
+
} else if (opts.planEngine === 'openspec') {
|
|
355
|
+
const config = writeProjectPlanConfig(cwd, { engine: 'openspec' });
|
|
356
|
+
report.plan = {
|
|
357
|
+
engine: 'openspec',
|
|
358
|
+
configured: hasOpenSpecConfig(cwd),
|
|
359
|
+
config,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (opts.adr === true) {
|
|
364
|
+
const user = loadUserConfig(opts.home);
|
|
365
|
+
const dir = normalizeAdrDir(
|
|
366
|
+
opts.adrDir ?? user.adr?.dir ?? DEFAULT_ADR_DIR,
|
|
367
|
+
);
|
|
368
|
+
report.adr = scaffoldAdr(cwd, {
|
|
369
|
+
dir,
|
|
370
|
+
force: opts.force,
|
|
371
|
+
hooks: true,
|
|
372
|
+
});
|
|
373
|
+
} else if (opts.adr === false) {
|
|
374
|
+
report.adr = { config: disableProjectAdr(cwd), enabled: false };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (opts.overlay) {
|
|
378
|
+
const overlayScript = path.join(__dirname, 'vendor-openspec-overlays.mjs');
|
|
379
|
+
const r = spawnSync(process.execPath, [overlayScript], {
|
|
380
|
+
cwd,
|
|
381
|
+
encoding: 'utf8',
|
|
382
|
+
});
|
|
383
|
+
report.overlay = {
|
|
384
|
+
status: r.status,
|
|
385
|
+
stdout: r.stdout?.trim() || '',
|
|
386
|
+
stderr: r.stderr?.trim() || '',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return report;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function promptAgents() {
|
|
394
|
+
const rl = readline.createInterface({ input, output });
|
|
395
|
+
try {
|
|
396
|
+
process.stdout.write(`Init Forge project wiring for which environments?\n`);
|
|
397
|
+
process.stdout.write(` 1) Cursor\n`);
|
|
398
|
+
process.stdout.write(` 2) Claude Code\n`);
|
|
399
|
+
process.stdout.write(` 3) Codex CLI\n`);
|
|
400
|
+
process.stdout.write(` 4) All\n`);
|
|
401
|
+
process.stdout.write(`Enter numbers separated by commas (e.g. 1,2) or 4 for all: `);
|
|
402
|
+
const answer = (await rl.question('')).trim();
|
|
403
|
+
if (!answer || answer === '4') return ['cursor', 'claude', 'codex'];
|
|
404
|
+
const map = { 1: 'cursor', 2: 'claude', 3: 'codex' };
|
|
405
|
+
const picked = [
|
|
406
|
+
...new Set(
|
|
407
|
+
answer
|
|
408
|
+
.split(/[,\s]+/)
|
|
409
|
+
.map((s) => map[s.trim()])
|
|
410
|
+
.filter(Boolean),
|
|
411
|
+
),
|
|
412
|
+
];
|
|
413
|
+
if (picked.length === 0) throw new Error('No agents selected');
|
|
414
|
+
return picked;
|
|
415
|
+
} finally {
|
|
416
|
+
rl.close();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Offer to install + set up OpenSpec in this project.
|
|
422
|
+
* @returns {Promise<boolean>} true = user accepted OpenSpec setup
|
|
423
|
+
*/
|
|
424
|
+
async function promptOpenSpecSetup() {
|
|
425
|
+
const rl = readline.createInterface({ input, output });
|
|
426
|
+
try {
|
|
427
|
+
const yn = (
|
|
428
|
+
await rl.question(
|
|
429
|
+
'OpenSpec is not set up in this project. Install and set it up now? [Y/n] (n = built-in specs engine) ',
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
.trim()
|
|
433
|
+
.toLowerCase();
|
|
434
|
+
return !(yn === 'n' || yn === 'no');
|
|
435
|
+
} finally {
|
|
436
|
+
rl.close();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Resolve the planning engine for `forge init`, offering OpenSpec setup when needed.
|
|
442
|
+
* @param {{ cwd: string, openspec: boolean | null }} opts
|
|
443
|
+
* @returns {Promise<string>} 'openspec' | 'specs'
|
|
444
|
+
*/
|
|
445
|
+
async function resolveInitPlanEngine(opts) {
|
|
446
|
+
const configured = hasOpenSpecConfig(opts.cwd);
|
|
447
|
+
|
|
448
|
+
if (opts.openspec === false) return 'specs';
|
|
449
|
+
|
|
450
|
+
if (opts.openspec === true) {
|
|
451
|
+
if (!configured && process.stdin.isTTY) {
|
|
452
|
+
const setup = setupOpenSpec(opts.cwd);
|
|
453
|
+
for (const s of setup.steps) {
|
|
454
|
+
process.stdout.write(` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`);
|
|
455
|
+
}
|
|
456
|
+
if (!setup.ok) {
|
|
457
|
+
process.stderr.write(
|
|
458
|
+
'OpenSpec setup failed — engine recorded as openspec; re-run `forge doctor --install` or `openspec init` manually.\n',
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return 'openspec';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (configured) return 'openspec';
|
|
466
|
+
|
|
467
|
+
const userDefault = loadUserPlanEngine();
|
|
468
|
+
if (userDefault === 'specs') return 'specs';
|
|
469
|
+
|
|
470
|
+
// Default (or user prefers openspec) but project has no OpenSpec yet
|
|
471
|
+
if (!process.stdin.isTTY) {
|
|
472
|
+
return userDefault === 'openspec' ? 'openspec' : 'specs';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const accepted = await promptOpenSpecSetup();
|
|
476
|
+
if (!accepted) return 'specs';
|
|
477
|
+
|
|
478
|
+
const setup = setupOpenSpec(opts.cwd);
|
|
479
|
+
for (const s of setup.steps) {
|
|
480
|
+
process.stdout.write(` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`);
|
|
481
|
+
}
|
|
482
|
+
if (!setup.ok) {
|
|
483
|
+
process.stderr.write(
|
|
484
|
+
'OpenSpec setup failed — falling back to the built-in specs engine. You can switch later with `forge init --openspec --force`.\n',
|
|
485
|
+
);
|
|
486
|
+
return 'specs';
|
|
487
|
+
}
|
|
488
|
+
return 'openspec';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @param {string} [defaultDir]
|
|
493
|
+
* @returns {Promise<{ enabled: boolean, dir: string }>}
|
|
494
|
+
*/
|
|
495
|
+
async function promptAdrForInit(defaultDir = DEFAULT_ADR_DIR) {
|
|
496
|
+
const rl = readline.createInterface({ input, output });
|
|
497
|
+
let enabled = false;
|
|
498
|
+
try {
|
|
499
|
+
const yn = (
|
|
500
|
+
await rl.question(
|
|
501
|
+
'Use Architecture Decision Records (ADRs) in this project? [y/N] ',
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
.trim()
|
|
505
|
+
.toLowerCase();
|
|
506
|
+
enabled = yn === 'y' || yn === 'yes';
|
|
507
|
+
if (!enabled) return { enabled: false, dir: defaultDir };
|
|
508
|
+
const dirAnswer = (
|
|
509
|
+
await rl.question(`ADR directory inside the repo [${defaultDir}]: `)
|
|
510
|
+
).trim();
|
|
511
|
+
return { enabled: true, dir: normalizeAdrDir(dirAnswer || defaultDir) };
|
|
512
|
+
} finally {
|
|
513
|
+
rl.close();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function main(argv = process.argv.slice(2)) {
|
|
518
|
+
const opts = parseArgs(argv);
|
|
519
|
+
if (opts.help) {
|
|
520
|
+
printHelp();
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let selected = opts.all ? ['cursor', 'claude', 'codex'] : [...new Set(opts.agents)];
|
|
525
|
+
if (selected.length === 0) {
|
|
526
|
+
if (!process.stdin.isTTY) {
|
|
527
|
+
process.stderr.write(
|
|
528
|
+
'No agents specified. Pass --cursor/--claude/--codex/--all, or run in a TTY.\n',
|
|
529
|
+
);
|
|
530
|
+
return 1;
|
|
531
|
+
}
|
|
532
|
+
selected = await promptAgents();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const planEngine = await resolveInitPlanEngine({
|
|
536
|
+
cwd: opts.cwd,
|
|
537
|
+
openspec: opts.openspec,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
let adr = opts.adr;
|
|
541
|
+
let adrDir = opts.adrDir;
|
|
542
|
+
if (adr === null) {
|
|
543
|
+
const user = loadUserConfig();
|
|
544
|
+
const defaultDir = user.adr?.dir ?? DEFAULT_ADR_DIR;
|
|
545
|
+
if (process.stdin.isTTY) {
|
|
546
|
+
const picked = await promptAdrForInit(defaultDir);
|
|
547
|
+
adr = picked.enabled;
|
|
548
|
+
adrDir = picked.dir;
|
|
549
|
+
} else if (user.adr?.enabled === true) {
|
|
550
|
+
adr = true;
|
|
551
|
+
adrDir = user.adr.dir ?? DEFAULT_ADR_DIR;
|
|
552
|
+
} else {
|
|
553
|
+
adr = false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const report = initProject(selected, { ...opts, adr, adrDir, planEngine });
|
|
558
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
559
|
+
process.stdout.write(
|
|
560
|
+
`\nMerge hook snippets into settings if needed, ensure \`forge\` is on PATH, then open the project in your agent.\n`,
|
|
561
|
+
);
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const isDirect =
|
|
566
|
+
process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
|
|
567
|
+
|
|
568
|
+
if (isDirect) {
|
|
569
|
+
main()
|
|
570
|
+
.then((code) => process.exit(code))
|
|
571
|
+
.catch((err) => {
|
|
572
|
+
process.stderr.write(`${err.message || err}\n`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
});
|
|
575
|
+
}
|