@izkac/forgekit 0.3.11 → 0.3.13
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 +107 -107
- package/bin/forgekit.mjs +83 -83
- package/bin/review.mjs +81 -81
- package/package.json +1 -1
- package/scripts/prepack.mjs +78 -78
- package/scripts/run-tests.mjs +50 -50
- package/src/adr.mjs +236 -236
- package/src/adr.test.mjs +170 -170
- package/src/change.mjs +327 -234
- package/src/change.test.mjs +145 -83
- package/src/cleanup-sessions.mjs +84 -84
- package/src/config.mjs +103 -103
- package/src/defer.mjs +75 -75
- package/src/doctor.mjs +350 -341
- package/src/doctor.test.mjs +114 -114
- package/src/init.mjs +680 -621
- package/src/install.mjs +815 -815
- package/src/install.test.mjs +180 -180
- package/src/integrity-check.mjs +60 -60
- package/src/integrity.mjs +682 -682
- package/src/integrity.test.mjs +566 -566
- package/src/lib.mjs +143 -143
- package/src/models.defaults.json +41 -41
- package/src/new-session.mjs +99 -99
- package/src/openspec-overlays/README.md +19 -19
- package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
- package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
- package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
- package/src/paths.mjs +92 -92
- package/src/plan-engine.mjs +321 -278
- package/src/plan-engine.test.mjs +447 -283
- package/src/preferences.defaults.json +78 -78
- package/src/preferences.mjs +438 -438
- package/src/preferences.test.mjs +174 -174
- package/src/record-evidence.mjs +204 -204
- package/src/resolve-model.mjs +312 -312
- package/src/resolve-model.test.mjs +194 -194
- package/src/review/cli.test.mjs +117 -117
- package/src/review/export.mjs +172 -172
- package/src/review/export.test.mjs +197 -197
- package/src/review/fixtures/valid-review.json +42 -42
- package/src/review/lib.mjs +894 -894
- package/src/review/lib.test.mjs +266 -266
- package/src/review/schema.json +196 -196
- package/src/review/signals.test.mjs +62 -62
- package/src/score-cli.mjs +68 -68
- package/src/score.mjs +568 -566
- package/src/score.test.mjs +366 -340
- package/src/session-reminder.mjs +207 -207
- package/src/session-status.mjs +70 -70
- package/src/set-models.mjs +186 -186
- package/src/set-phase.mjs +205 -205
- package/src/set-prefs.mjs +294 -294
- package/src/specs-sync.mjs +234 -0
- package/src/specs-sync.test.mjs +114 -0
- package/src/spine.mjs +93 -93
- package/src/triage-prompt.mjs +175 -175
- package/src/triage-prompt.test.mjs +50 -50
- package/src/vendor-openspec-overlays.mjs +176 -176
- package/src/vendor-openspec-overlays.test.mjs +62 -62
- package/vendor/skills/archive-to-adr/SKILL.md +149 -149
- package/vendor/skills/forge/SKILL.md +136 -136
- package/vendor/skills/forge/docs/forge.md +650 -647
- package/vendor/skills/forge/phases/brainstorm.md +23 -23
- package/vendor/skills/forge/phases/finish.md +90 -87
- package/vendor/skills/forge/phases/implement.md +77 -77
- package/vendor/skills/forge/phases/plan-openspec.md +60 -60
- package/vendor/skills/forge/phases/plan-specs.md +163 -117
- package/vendor/skills/forge/phases/review.md +25 -25
- package/vendor/skills/forge/phases/verify.md +124 -124
- package/vendor/skills/forge/references/forge-layout.md +85 -85
- package/vendor/skills/forge/references/pace.md +115 -115
- package/vendor/skills/forge/references/plan-routing.md +52 -51
- package/vendor/skills/forge/references/runtime-integrity.md +232 -225
- package/vendor/skills/forge/references/substantial-work.md +37 -37
- package/vendor/skills/forge/references/test-evidence.md +30 -30
- package/vendor/skills/forge/references/test-strategy.md +68 -68
- package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -87
- package/vendor/skills/forge/subagents/final-reviewer-prompt.md +56 -56
- package/vendor/skills/forge/subagents/implementer-prompt.md +38 -38
- package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -132
- package/vendor/skills/thorough-code-review/SKILL.md +290 -290
- package/vendor/skills/thorough-code-review/examples.md +133 -133
- package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -26
- package/vendor/skills/thorough-code-review/reference/lenses.md +96 -96
- package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -62
- package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -105
- package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -222
- package/vendor/skills/thorough-code-review/reference/report-template.md +115 -115
- package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -49
- package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -55
- package/vendor/templates/adr/README.md +7 -7
- package/vendor/templates/adr/decisions.md +141 -141
- package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -74
- package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -3
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -52
- package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -3
- package/vendor/templates/project/claude/commands/forge-apply.md +75 -75
- package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -7
- package/vendor/templates/project/claude/commands/forge-build.md +17 -17
- package/vendor/templates/project/claude/commands/forge-plan.md +12 -12
- package/vendor/templates/project/claude/commands/forge-skip.md +14 -14
- package/vendor/templates/project/claude/commands/forge-status.md +16 -16
- package/vendor/templates/project/claude/commands/forge.md +16 -16
- package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -73
- package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -19
- package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -77
- package/vendor/templates/project/cursor/commands/forge-apply.md +75 -75
- package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -10
- package/vendor/templates/project/cursor/commands/forge-build.md +17 -17
- package/vendor/templates/project/cursor/commands/forge-plan.md +15 -15
- package/vendor/templates/project/cursor/commands/forge-skip.md +14 -14
- package/vendor/templates/project/cursor/commands/forge-status.md +16 -16
- package/vendor/templates/project/cursor/commands/forge.md +16 -16
- package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -30
- package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -3
package/src/init.mjs
CHANGED
|
@@ -1,621 +1,680 @@
|
|
|
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 { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
-
import { spawnSync } from 'node:child_process';
|
|
16
|
-
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_ADR_DIR,
|
|
19
|
-
disableProjectAdr,
|
|
20
|
-
loadUserConfig,
|
|
21
|
-
normalizeAdrDir,
|
|
22
|
-
scaffoldAdr,
|
|
23
|
-
} from './adr.mjs';
|
|
24
|
-
import {
|
|
25
|
-
DEFAULT_SPECS_DIR,
|
|
26
|
-
hasOpenSpecConfig,
|
|
27
|
-
loadUserPlanEngine,
|
|
28
|
-
scaffoldSpecs,
|
|
29
|
-
setupOpenSpec,
|
|
30
|
-
writeProjectPlanConfig,
|
|
31
|
-
} from './plan-engine.mjs';
|
|
32
|
-
import { resolveAsset } from './paths.mjs';
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
/** @type {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
else if (arg === '--
|
|
70
|
-
else if (arg === '--
|
|
71
|
-
else if (arg === '--
|
|
72
|
-
else if (arg === '--
|
|
73
|
-
else if (arg === '--
|
|
74
|
-
else if (arg === '--
|
|
75
|
-
else if (arg === '--
|
|
76
|
-
else if (arg === '--
|
|
77
|
-
else if (arg === '--
|
|
78
|
-
else
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
--
|
|
106
|
-
--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* @
|
|
134
|
-
*/
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
forge
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
UserPromptSubmit
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
path.join(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
path.join(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
);
|
|
392
|
-
report.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
*
|
|
439
|
-
* @param {string}
|
|
440
|
-
* @returns {Set<string>}
|
|
441
|
-
*/
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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 { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_ADR_DIR,
|
|
19
|
+
disableProjectAdr,
|
|
20
|
+
loadUserConfig,
|
|
21
|
+
normalizeAdrDir,
|
|
22
|
+
scaffoldAdr,
|
|
23
|
+
} from './adr.mjs';
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_SPECS_DIR,
|
|
26
|
+
hasOpenSpecConfig,
|
|
27
|
+
loadUserPlanEngine,
|
|
28
|
+
scaffoldSpecs,
|
|
29
|
+
setupOpenSpec,
|
|
30
|
+
writeProjectPlanConfig,
|
|
31
|
+
} from './plan-engine.mjs';
|
|
32
|
+
import { resolveAsset } from './paths.mjs';
|
|
33
|
+
import {
|
|
34
|
+
AGENT_IDS,
|
|
35
|
+
AGENTS,
|
|
36
|
+
installedManagedPairs,
|
|
37
|
+
promptOpenSpec,
|
|
38
|
+
} from './install.mjs';
|
|
39
|
+
|
|
40
|
+
// Environments with project-local command/rule/hook templates. Others are
|
|
41
|
+
// driven by the globally-installed skill alone (no per-project wiring).
|
|
42
|
+
const WIRED_AGENTS = Object.freeze(['cursor', 'claude', 'codex']);
|
|
43
|
+
|
|
44
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string[]} argv
|
|
48
|
+
*/
|
|
49
|
+
export function parseArgs(argv) {
|
|
50
|
+
const opts = {
|
|
51
|
+
all: false,
|
|
52
|
+
help: false,
|
|
53
|
+
force: false,
|
|
54
|
+
overlay: false,
|
|
55
|
+
agents: /** @type {string[]} */ ([]),
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
/** @type {boolean | null} */
|
|
58
|
+
adr: /** @type {boolean | null} */ (null),
|
|
59
|
+
adrDir: /** @type {string | null} */ (null),
|
|
60
|
+
/** @type {boolean | null} true=openspec, false=specs, null=detect/prompt */
|
|
61
|
+
openspec: /** @type {boolean | null} */ (null),
|
|
62
|
+
/** @type {string | null} specs-engine root (plan.dir) */
|
|
63
|
+
planDir: /** @type {string | null} */ (null),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
67
|
+
const arg = argv[i];
|
|
68
|
+
if (arg === '--all') opts.all = true;
|
|
69
|
+
else if (arg === '--force' || arg === '-f') opts.force = true;
|
|
70
|
+
else if (arg === '--overlay') opts.overlay = true;
|
|
71
|
+
else if (arg === '--cwd') opts.cwd = argv[++i];
|
|
72
|
+
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
73
|
+
else if (arg === '--cursor') opts.agents.push('cursor');
|
|
74
|
+
else if (arg === '--claude' || arg === '--claude-code') opts.agents.push('claude');
|
|
75
|
+
else if (arg === '--codex') opts.agents.push('codex');
|
|
76
|
+
else if (arg === '--copilot') opts.agents.push('copilot');
|
|
77
|
+
else if (arg === '--gemini') opts.agents.push('gemini');
|
|
78
|
+
else if (arg === '--windsurf') opts.agents.push('windsurf');
|
|
79
|
+
else if (arg === '--opencode') opts.agents.push('opencode');
|
|
80
|
+
else if (arg === '--adr') opts.adr = true;
|
|
81
|
+
else if (arg === '--no-adr') opts.adr = false;
|
|
82
|
+
else if (arg === '--adr-dir') opts.adrDir = argv[++i];
|
|
83
|
+
else if (arg === '--openspec') opts.openspec = true;
|
|
84
|
+
else if (arg === '--no-openspec') opts.openspec = false;
|
|
85
|
+
else if (arg === '--plan-dir') opts.planDir = argv[++i];
|
|
86
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return opts;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function printHelp() {
|
|
93
|
+
process.stdout.write(`Usage: forge init [options]
|
|
94
|
+
|
|
95
|
+
Wire Forge commands, thin rules, and hooks into the current project.
|
|
96
|
+
|
|
97
|
+
Options:
|
|
98
|
+
--cursor Cursor (.cursor/commands, rules, hooks)
|
|
99
|
+
--claude Claude Code (.claude/commands, rules, hooks)
|
|
100
|
+
--codex Codex CLI (.codex/rules)
|
|
101
|
+
--copilot/--gemini/--windsurf/--opencode
|
|
102
|
+
Offered in the picker for parity with \`forgekit install\`;
|
|
103
|
+
driven by the global skill (no per-project wiring yet)
|
|
104
|
+
--all Every offered environment
|
|
105
|
+
--openspec Plan with OpenSpec (offer install + \`openspec init\` if missing)
|
|
106
|
+
--no-openspec Plan with the built-in specs engine (${DEFAULT_SPECS_DIR}/changes/)
|
|
107
|
+
--plan-dir <path> Specs-engine root (plan.dir). Default: ${DEFAULT_SPECS_DIR}.
|
|
108
|
+
Use \`openspec\` to reuse an existing OpenSpec tree without moving files.
|
|
109
|
+
--adr Enable ADRs (scaffold decisions.md + ADR dir + hooks)
|
|
110
|
+
--no-adr Disable ADRs for this project
|
|
111
|
+
--adr-dir <path> ADR directory (default: ${DEFAULT_ADR_DIR} or ~/.forgekit preference)
|
|
112
|
+
--overlay Also run \`forge overlay\` (OpenSpec vendor patches)
|
|
113
|
+
--force, -f Force re-scaffold of ADR/specs docs (managed command,
|
|
114
|
+
rule, and hook files always refresh to the latest template)
|
|
115
|
+
--cwd <path> Project root (default: cwd)
|
|
116
|
+
--help
|
|
117
|
+
|
|
118
|
+
Requires the Forge skill already installed (\`forge install\`) for agents
|
|
119
|
+
to load skill content. Init only adds project-local wiring.
|
|
120
|
+
|
|
121
|
+
Interactive (TTY): the environment picker matches \`forgekit install\` and is
|
|
122
|
+
pre-checked with what you installed there (saved in ~/.forgekit/config.json),
|
|
123
|
+
so you don't pick twice. When --openspec/--no-openspec omitted: uses the user
|
|
124
|
+
default from install when set; otherwise asks Planning engine?. Choosing
|
|
125
|
+
OpenSpec always writes plan.engine=openspec (setup failure or declining
|
|
126
|
+
immediate \`openspec init\` does not fall back to the built-in specs engine).
|
|
127
|
+
When --adr/--no-adr omitted, asks whether to use ADRs (default Yes) and for
|
|
128
|
+
the directory inside the repo.
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
export function resolveTemplatesRoot() {
|
|
136
|
+
return resolveAsset('templates/project');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Copy a forgekit-managed template file. These are regenerated pointers
|
|
141
|
+
* (forge-* commands/rules/hooks) with no user-owned content, so re-running
|
|
142
|
+
* `forge init` refreshes them in place — that's how template fixes propagate.
|
|
143
|
+
* @param {string} src
|
|
144
|
+
* @param {string} dest
|
|
145
|
+
* @param {{ force?: boolean }} _opts
|
|
146
|
+
*/
|
|
147
|
+
function copyFile(src, dest, _opts) {
|
|
148
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
149
|
+
const next = fs.readFileSync(src);
|
|
150
|
+
if (fs.existsSync(dest)) {
|
|
151
|
+
if (fs.readFileSync(dest).equals(next)) return 'unchanged';
|
|
152
|
+
fs.writeFileSync(dest, next);
|
|
153
|
+
return 'updated';
|
|
154
|
+
}
|
|
155
|
+
fs.writeFileSync(dest, next);
|
|
156
|
+
return 'written';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} srcDir
|
|
161
|
+
* @param {string} destDir
|
|
162
|
+
* @param {{ force?: boolean }} opts
|
|
163
|
+
*/
|
|
164
|
+
function copyDirFiles(srcDir, destDir, opts) {
|
|
165
|
+
/** @type {{ file: string, status: string }[]} */
|
|
166
|
+
const out = [];
|
|
167
|
+
if (!fs.existsSync(srcDir)) return out;
|
|
168
|
+
for (const name of fs.readdirSync(srcDir)) {
|
|
169
|
+
const from = path.join(srcDir, name);
|
|
170
|
+
if (!fs.statSync(from).isFile()) continue;
|
|
171
|
+
const to = path.join(destDir, name);
|
|
172
|
+
out.push({ file: path.relative(opts.cwd ?? destDir, to) || to, status: copyFile(from, to, opts) });
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Ensure .forge/.gitignore exists.
|
|
179
|
+
* @param {string} cwd
|
|
180
|
+
*/
|
|
181
|
+
export function ensureForgeGitignore(cwd) {
|
|
182
|
+
const forgeDir = path.join(cwd, '.forge');
|
|
183
|
+
fs.mkdirSync(forgeDir, { recursive: true });
|
|
184
|
+
const gi = path.join(forgeDir, '.gitignore');
|
|
185
|
+
const body = `# Forge session scratch — keep layout docs + committed project config
|
|
186
|
+
*
|
|
187
|
+
!.gitignore
|
|
188
|
+
!README.md
|
|
189
|
+
!config.json
|
|
190
|
+
`;
|
|
191
|
+
if (!fs.existsSync(gi)) {
|
|
192
|
+
fs.writeFileSync(gi, body, 'utf8');
|
|
193
|
+
return 'written';
|
|
194
|
+
}
|
|
195
|
+
// Upgrade older scaffolds that omit config.json
|
|
196
|
+
const existing = fs.readFileSync(gi, 'utf8');
|
|
197
|
+
if (!existing.includes('!config.json')) {
|
|
198
|
+
const next = existing.trimEnd().endsWith('!README.md')
|
|
199
|
+
? `${existing.trimEnd()}\n!config.json\n`
|
|
200
|
+
: `${existing.trimEnd()}\n!config.json\n`;
|
|
201
|
+
fs.writeFileSync(gi, next, 'utf8');
|
|
202
|
+
return 'updated';
|
|
203
|
+
}
|
|
204
|
+
return 'exists';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Write a short project README under .forge/ if missing.
|
|
209
|
+
* @param {string} cwd
|
|
210
|
+
*/
|
|
211
|
+
export function ensureForgeReadme(cwd) {
|
|
212
|
+
const readme = path.join(cwd, '.forge', 'README.md');
|
|
213
|
+
if (fs.existsSync(readme)) return 'exists';
|
|
214
|
+
fs.writeFileSync(
|
|
215
|
+
readme,
|
|
216
|
+
`# \`.forge/\` — Forge session scratch
|
|
217
|
+
|
|
218
|
+
Per-checkout, **gitignored** workspace for Forge session orchestration.
|
|
219
|
+
Canonical specs live in \`openspec/\`; this directory holds session-local artefacts only.
|
|
220
|
+
|
|
221
|
+
\`\`\`bash
|
|
222
|
+
forge new <slug>
|
|
223
|
+
forge status
|
|
224
|
+
forge prefs
|
|
225
|
+
forge models
|
|
226
|
+
forge cleanup
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
See the Forge skill and forgekit docs for the full workflow.
|
|
230
|
+
`,
|
|
231
|
+
'utf8',
|
|
232
|
+
);
|
|
233
|
+
return 'written';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Append hook registrations into Claude settings.json if present / create stub note.
|
|
238
|
+
* @param {string} cwd
|
|
239
|
+
* @param {{ force?: boolean }} opts
|
|
240
|
+
*/
|
|
241
|
+
export function ensureClaudeHookHints(cwd, opts) {
|
|
242
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
243
|
+
const notePath = path.join(cwd, '.claude', 'forge-hooks.snippet.json');
|
|
244
|
+
const snippet = {
|
|
245
|
+
_comment:
|
|
246
|
+
'Merge these hooks into .claude/settings.json (SessionStart + UserPromptSubmit). Paths assume forge CLI is on PATH.',
|
|
247
|
+
hooks: {
|
|
248
|
+
SessionStart: [
|
|
249
|
+
{
|
|
250
|
+
hooks: [
|
|
251
|
+
{
|
|
252
|
+
type: 'command',
|
|
253
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-session-start.mjs"',
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
UserPromptSubmit: [
|
|
259
|
+
{
|
|
260
|
+
hooks: [
|
|
261
|
+
{
|
|
262
|
+
type: 'command',
|
|
263
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-triage-hook.mjs"',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: 'command',
|
|
267
|
+
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/hooks/forge-prompt-hook.mjs"',
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
fs.mkdirSync(path.dirname(notePath), { recursive: true });
|
|
275
|
+
if (!fs.existsSync(notePath) || opts.force) {
|
|
276
|
+
fs.writeFileSync(notePath, `${JSON.stringify(snippet, null, 2)}\n`, 'utf8');
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
settingsExists: fs.existsSync(settingsPath),
|
|
280
|
+
snippet: notePath,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Write Cursor hooks.json snippet.
|
|
286
|
+
* @param {string} cwd
|
|
287
|
+
* @param {{ force?: boolean }} opts
|
|
288
|
+
*/
|
|
289
|
+
export function ensureCursorHookHints(cwd, opts) {
|
|
290
|
+
const notePath = path.join(cwd, '.cursor', 'forge-hooks.snippet.json');
|
|
291
|
+
const snippet = {
|
|
292
|
+
_comment:
|
|
293
|
+
'Merge into .cursor/hooks.json. Requires Node on PATH (forge-session-start.mjs).',
|
|
294
|
+
version: 1,
|
|
295
|
+
hooks: {
|
|
296
|
+
sessionStart: [
|
|
297
|
+
{
|
|
298
|
+
command: 'node .cursor/hooks/forge-session-start.mjs',
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
fs.mkdirSync(path.dirname(notePath), { recursive: true });
|
|
304
|
+
if (!fs.existsSync(notePath) || opts.force) {
|
|
305
|
+
fs.writeFileSync(notePath, `${JSON.stringify(snippet, null, 2)}\n`, 'utf8');
|
|
306
|
+
}
|
|
307
|
+
return notePath;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {string[]} selected
|
|
312
|
+
* @param {{ cwd: string, force?: boolean, overlay?: boolean, templatesRoot?: string, adr?: boolean | null, adrDir?: string | null, home?: string, planEngine?: string | null, planDir?: string | null }} opts
|
|
313
|
+
*/
|
|
314
|
+
export function initProject(selected, opts) {
|
|
315
|
+
const templates = opts.templatesRoot ?? resolveTemplatesRoot();
|
|
316
|
+
const cwd = opts.cwd;
|
|
317
|
+
/** @type {Record<string, unknown>} */
|
|
318
|
+
const report = {
|
|
319
|
+
cwd,
|
|
320
|
+
gitignore: ensureForgeGitignore(cwd),
|
|
321
|
+
readme: ensureForgeReadme(cwd),
|
|
322
|
+
files: /** @type {{ file: string, status: string }[]} */ ([]),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const copyOpts = { force: opts.force, cwd };
|
|
326
|
+
|
|
327
|
+
if (selected.includes('cursor')) {
|
|
328
|
+
report.files.push(
|
|
329
|
+
...copyDirFiles(
|
|
330
|
+
path.join(templates, 'cursor', 'commands'),
|
|
331
|
+
path.join(cwd, '.cursor', 'commands'),
|
|
332
|
+
copyOpts,
|
|
333
|
+
),
|
|
334
|
+
...copyDirFiles(
|
|
335
|
+
path.join(templates, 'cursor', 'rules'),
|
|
336
|
+
path.join(cwd, '.cursor', 'rules'),
|
|
337
|
+
copyOpts,
|
|
338
|
+
),
|
|
339
|
+
...copyDirFiles(
|
|
340
|
+
path.join(templates, 'cursor', 'hooks'),
|
|
341
|
+
path.join(cwd, '.cursor', 'hooks'),
|
|
342
|
+
copyOpts,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
report.cursorHookSnippet = ensureCursorHookHints(cwd, copyOpts);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (selected.includes('claude')) {
|
|
349
|
+
report.files.push(
|
|
350
|
+
...copyDirFiles(
|
|
351
|
+
path.join(templates, 'claude', 'commands'),
|
|
352
|
+
path.join(cwd, '.claude', 'commands'),
|
|
353
|
+
copyOpts,
|
|
354
|
+
),
|
|
355
|
+
...copyDirFiles(
|
|
356
|
+
path.join(templates, 'claude', 'rules'),
|
|
357
|
+
path.join(cwd, '.claude', 'rules'),
|
|
358
|
+
copyOpts,
|
|
359
|
+
),
|
|
360
|
+
...copyDirFiles(
|
|
361
|
+
path.join(templates, 'claude', 'hooks'),
|
|
362
|
+
path.join(cwd, '.claude', 'hooks'),
|
|
363
|
+
copyOpts,
|
|
364
|
+
),
|
|
365
|
+
);
|
|
366
|
+
report.claudeHooks = ensureClaudeHookHints(cwd, copyOpts);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (selected.includes('codex')) {
|
|
370
|
+
report.files.push(
|
|
371
|
+
...copyDirFiles(
|
|
372
|
+
path.join(templates, 'codex', 'rules'),
|
|
373
|
+
path.join(cwd, '.codex', 'rules'),
|
|
374
|
+
copyOpts,
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Selected environments without project-wiring templates: the globally
|
|
380
|
+
// installed skill is their interface — nothing to scaffold per project.
|
|
381
|
+
report.skillOnly = selected.filter((id) => !WIRED_AGENTS.includes(id));
|
|
382
|
+
|
|
383
|
+
if (opts.planEngine === 'specs') {
|
|
384
|
+
const scaffold = scaffoldSpecs(cwd, {
|
|
385
|
+
force: opts.force,
|
|
386
|
+
dir: opts.planDir ?? undefined,
|
|
387
|
+
});
|
|
388
|
+
const config = writeProjectPlanConfig(cwd, {
|
|
389
|
+
engine: 'specs',
|
|
390
|
+
dir: scaffold.dir,
|
|
391
|
+
});
|
|
392
|
+
report.plan = { engine: 'specs', dir: scaffold.dir, files: scaffold.files, config };
|
|
393
|
+
} else if (opts.planEngine === 'openspec') {
|
|
394
|
+
if (opts.planDir) {
|
|
395
|
+
process.stderr.write(
|
|
396
|
+
'Note: --plan-dir applies to the built-in specs engine; ignoring for openspec.\n',
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const config = writeProjectPlanConfig(cwd, { engine: 'openspec' });
|
|
400
|
+
report.plan = {
|
|
401
|
+
engine: 'openspec',
|
|
402
|
+
configured: hasOpenSpecConfig(cwd),
|
|
403
|
+
config,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (opts.adr === true) {
|
|
408
|
+
const user = loadUserConfig(opts.home);
|
|
409
|
+
const dir = normalizeAdrDir(
|
|
410
|
+
opts.adrDir ?? user.adr?.dir ?? DEFAULT_ADR_DIR,
|
|
411
|
+
);
|
|
412
|
+
report.adr = scaffoldAdr(cwd, {
|
|
413
|
+
dir,
|
|
414
|
+
force: opts.force,
|
|
415
|
+
hooks: true,
|
|
416
|
+
});
|
|
417
|
+
} else if (opts.adr === false) {
|
|
418
|
+
report.adr = { config: disableProjectAdr(cwd), enabled: false };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (opts.overlay) {
|
|
422
|
+
const overlayScript = path.join(__dirname, 'vendor-openspec-overlays.mjs');
|
|
423
|
+
const r = spawnSync(process.execPath, [overlayScript], {
|
|
424
|
+
cwd,
|
|
425
|
+
encoding: 'utf8',
|
|
426
|
+
});
|
|
427
|
+
report.overlay = {
|
|
428
|
+
status: r.status,
|
|
429
|
+
stdout: r.stdout?.trim() || '',
|
|
430
|
+
stderr: r.stderr?.trim() || '',
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return report;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Environments this project already has Forge wiring for (marker dir present).
|
|
439
|
+
* @param {string} cwd
|
|
440
|
+
* @returns {Set<string>}
|
|
441
|
+
*/
|
|
442
|
+
function wiredAgents(cwd) {
|
|
443
|
+
const markers = {
|
|
444
|
+
cursor: path.join(cwd, '.cursor', 'commands'),
|
|
445
|
+
claude: path.join(cwd, '.claude', 'commands'),
|
|
446
|
+
codex: path.join(cwd, '.codex', 'rules'),
|
|
447
|
+
};
|
|
448
|
+
return new Set(
|
|
449
|
+
Object.entries(markers)
|
|
450
|
+
.filter(([, dir]) => fs.existsSync(dir))
|
|
451
|
+
.map(([id]) => id),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Environments to pre-check: those chosen during `forgekit install`
|
|
457
|
+
* (saved in ~/.forgekit/config.json), plus what is already installed or wired.
|
|
458
|
+
* @param {string} cwd
|
|
459
|
+
* @param {string} [home]
|
|
460
|
+
* @returns {Set<string>}
|
|
461
|
+
*/
|
|
462
|
+
export function rememberedAgents(cwd, home) {
|
|
463
|
+
const user = loadUserConfig(home);
|
|
464
|
+
return new Set([
|
|
465
|
+
...(Array.isArray(user.agents) ? user.agents : []),
|
|
466
|
+
...installedManagedPairs(home).map((p) => p.agent),
|
|
467
|
+
...wiredAgents(cwd),
|
|
468
|
+
]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** @param {string} cwd */
|
|
472
|
+
async function promptAgents(cwd) {
|
|
473
|
+
const remembered = rememberedAgents(cwd);
|
|
474
|
+
return checkbox({
|
|
475
|
+
message: 'Init Forge project wiring for which environments?',
|
|
476
|
+
choices: AGENT_IDS.map((id) => ({
|
|
477
|
+
value: id,
|
|
478
|
+
name: AGENTS[id].label,
|
|
479
|
+
checked: remembered.has(id),
|
|
480
|
+
})),
|
|
481
|
+
required: true,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Offer to install + set up OpenSpec in this project (engine already chosen).
|
|
487
|
+
* @returns {Promise<boolean>} true = user accepted OpenSpec setup now
|
|
488
|
+
*/
|
|
489
|
+
async function promptOpenSpecSetup() {
|
|
490
|
+
return confirm({
|
|
491
|
+
message: 'OpenSpec is not set up in this project. Install and set it up now?',
|
|
492
|
+
default: true,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Resolve the planning engine for `forge init`, offering OpenSpec setup when needed.
|
|
498
|
+
*
|
|
499
|
+
* Choosing OpenSpec (flag, user default, or interactive pick) always records
|
|
500
|
+
* `plan.engine: openspec`. Immediate `openspec init` is best-effort — failure
|
|
501
|
+
* or declining setup must not fall back to the built-in specs engine.
|
|
502
|
+
*
|
|
503
|
+
* @param {{
|
|
504
|
+
* cwd: string,
|
|
505
|
+
* openspec: boolean | null,
|
|
506
|
+
* agents?: string[],
|
|
507
|
+
* home?: string,
|
|
508
|
+
* isTTY?: boolean,
|
|
509
|
+
* confirmSetup?: () => Promise<boolean>,
|
|
510
|
+
* promptEngine?: () => Promise<boolean>,
|
|
511
|
+
* setup?: typeof setupOpenSpec,
|
|
512
|
+
* loadUser?: (home?: string) => string | null,
|
|
513
|
+
* }} opts
|
|
514
|
+
* @returns {Promise<string>} 'openspec' | 'specs'
|
|
515
|
+
*/
|
|
516
|
+
export async function resolveInitPlanEngine(opts) {
|
|
517
|
+
const configured = hasOpenSpecConfig(opts.cwd);
|
|
518
|
+
const tools = opts.agents;
|
|
519
|
+
const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY);
|
|
520
|
+
const runSetup = opts.setup ?? setupOpenSpec;
|
|
521
|
+
const loadUser = opts.loadUser ?? ((home) => loadUserPlanEngine(home));
|
|
522
|
+
const confirmSetup = opts.confirmSetup ?? promptOpenSpecSetup;
|
|
523
|
+
const promptEngine = opts.promptEngine ?? promptOpenSpec;
|
|
524
|
+
|
|
525
|
+
const reportSetup = (setup) => {
|
|
526
|
+
for (const s of setup.steps) {
|
|
527
|
+
process.stdout.write(
|
|
528
|
+
` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (!setup.ok) {
|
|
532
|
+
process.stderr.write(
|
|
533
|
+
'OpenSpec setup failed — engine recorded as openspec; re-run `forge doctor --install` or `openspec init` manually.\n',
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/** Best-effort setup; never changes the chosen engine. */
|
|
539
|
+
const ensureOpenSpecSetup = async ({ ask }) => {
|
|
540
|
+
if (configured) return;
|
|
541
|
+
if (!isTTY) return;
|
|
542
|
+
if (ask) {
|
|
543
|
+
const accepted = await confirmSetup();
|
|
544
|
+
if (!accepted) {
|
|
545
|
+
process.stdout.write(
|
|
546
|
+
'OpenSpec engine recorded; run `openspec init` (or `forge doctor --install`) when ready.\n',
|
|
547
|
+
);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
reportSetup(runSetup(opts.cwd, { tools }));
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
if (opts.openspec === false) return 'specs';
|
|
555
|
+
|
|
556
|
+
if (opts.openspec === true) {
|
|
557
|
+
// Flag means engine=openspec; attempt setup without a second prompt.
|
|
558
|
+
await ensureOpenSpecSetup({ ask: false });
|
|
559
|
+
return 'openspec';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (configured) return 'openspec';
|
|
563
|
+
|
|
564
|
+
const userDefault = loadUser(opts.home);
|
|
565
|
+
if (userDefault === 'specs') return 'specs';
|
|
566
|
+
|
|
567
|
+
if (userDefault === 'openspec') {
|
|
568
|
+
await ensureOpenSpecSetup({ ask: true });
|
|
569
|
+
return 'openspec';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// No user default — ask on TTY, otherwise leave project on built-in specs.
|
|
573
|
+
if (!isTTY) return 'specs';
|
|
574
|
+
|
|
575
|
+
const wantOpenSpec = await promptEngine();
|
|
576
|
+
if (!wantOpenSpec) return 'specs';
|
|
577
|
+
|
|
578
|
+
await ensureOpenSpecSetup({ ask: true });
|
|
579
|
+
return 'openspec';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* @param {string} [defaultDir]
|
|
584
|
+
* @param {boolean} [defaultEnabled]
|
|
585
|
+
* @returns {Promise<{ enabled: boolean, dir: string }>}
|
|
586
|
+
*/
|
|
587
|
+
async function promptAdrForInit(defaultDir = DEFAULT_ADR_DIR, defaultEnabled = true) {
|
|
588
|
+
const enabled = await confirm({
|
|
589
|
+
message: 'Use Architecture Decision Records (ADRs) in this project?',
|
|
590
|
+
default: defaultEnabled,
|
|
591
|
+
});
|
|
592
|
+
if (!enabled) return { enabled: false, dir: defaultDir };
|
|
593
|
+
const dir = await input({
|
|
594
|
+
message: 'ADR directory inside the repo',
|
|
595
|
+
default: defaultDir,
|
|
596
|
+
});
|
|
597
|
+
return { enabled: true, dir: normalizeAdrDir(dir.trim() || defaultDir) };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function main(argv = process.argv.slice(2)) {
|
|
601
|
+
const opts = parseArgs(argv);
|
|
602
|
+
if (opts.help) {
|
|
603
|
+
printHelp();
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let selected = opts.all ? [...AGENT_IDS] : [...new Set(opts.agents)];
|
|
608
|
+
if (selected.length === 0) {
|
|
609
|
+
if (!process.stdin.isTTY) {
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
'No agents specified. Pass --cursor/--claude/--codex/--copilot/--gemini/--windsurf/--opencode/--all, or run in a TTY.\n',
|
|
612
|
+
);
|
|
613
|
+
return 1;
|
|
614
|
+
}
|
|
615
|
+
selected = await promptAgents(opts.cwd);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const id of selected) {
|
|
619
|
+
if (!AGENTS[id]) {
|
|
620
|
+
process.stderr.write(`Unknown environment: ${id}. Known: ${AGENT_IDS.join(', ')}\n`);
|
|
621
|
+
return 1;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const planEngine = await resolveInitPlanEngine({
|
|
626
|
+
cwd: opts.cwd,
|
|
627
|
+
openspec: opts.openspec,
|
|
628
|
+
agents: selected,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
let adr = opts.adr;
|
|
632
|
+
let adrDir = opts.adrDir;
|
|
633
|
+
if (adr === null) {
|
|
634
|
+
const user = loadUserConfig();
|
|
635
|
+
const defaultDir = user.adr?.dir ?? DEFAULT_ADR_DIR;
|
|
636
|
+
if (process.stdin.isTTY) {
|
|
637
|
+
// Default Yes, unless the user globally opted out of ADRs.
|
|
638
|
+
const picked = await promptAdrForInit(defaultDir, user.adr?.enabled !== false);
|
|
639
|
+
adr = picked.enabled;
|
|
640
|
+
adrDir = picked.dir;
|
|
641
|
+
} else if (user.adr?.enabled === true) {
|
|
642
|
+
adr = true;
|
|
643
|
+
adrDir = user.adr.dir ?? DEFAULT_ADR_DIR;
|
|
644
|
+
} else {
|
|
645
|
+
adr = false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const report = initProject(selected, {
|
|
650
|
+
...opts,
|
|
651
|
+
adr,
|
|
652
|
+
adrDir,
|
|
653
|
+
planEngine,
|
|
654
|
+
planDir: opts.planDir,
|
|
655
|
+
});
|
|
656
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
657
|
+
if (Array.isArray(report.skillOnly) && report.skillOnly.length) {
|
|
658
|
+
const labels = report.skillOnly.map((id) => AGENTS[id].label).join(', ');
|
|
659
|
+
process.stdout.write(
|
|
660
|
+
`\nNo project wiring for: ${labels} — they use the globally installed Forge skill directly (run \`forgekit install\` if not yet installed).\n`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
process.stdout.write(
|
|
664
|
+
`\nMerge hook snippets into settings if needed, ensure \`forge\` is on PATH, then open the project in your agent.\n`,
|
|
665
|
+
);
|
|
666
|
+
return 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const isDirect =
|
|
670
|
+
process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
|
|
671
|
+
|
|
672
|
+
if (isDirect) {
|
|
673
|
+
main()
|
|
674
|
+
.then((code) => process.exit(code))
|
|
675
|
+
.catch((err) => {
|
|
676
|
+
if (err?.name === 'ExitPromptError') process.exit(130);
|
|
677
|
+
process.stderr.write(`${err.message || err}\n`);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
});
|
|
680
|
+
}
|