@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/install.mjs
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install forgekit skills into user-level agent skill directories.
|
|
4
|
+
*
|
|
5
|
+
* Canonical entry: `forgekit install`
|
|
6
|
+
* Aliases: `forge install` → --skills forge
|
|
7
|
+
* `review install` → --skills thorough-code-review
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* forgekit install
|
|
11
|
+
* forgekit install --skills forge,thorough-code-review --agents cursor,claude
|
|
12
|
+
* forgekit install --all-skills --all-agents --force
|
|
13
|
+
* forgekit list
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import readline from 'node:readline/promises';
|
|
20
|
+
import { pathToFileURL } from 'node:url';
|
|
21
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
22
|
+
import {
|
|
23
|
+
ADR_SKILLS,
|
|
24
|
+
DEFAULT_ADR_DIR,
|
|
25
|
+
isGitRepo,
|
|
26
|
+
normalizeAdrDir,
|
|
27
|
+
saveUserConfig,
|
|
28
|
+
scaffoldAdr,
|
|
29
|
+
disableProjectAdr,
|
|
30
|
+
} from './adr.mjs';
|
|
31
|
+
import { saveUserPlanEngine } from './plan-engine.mjs';
|
|
32
|
+
import { hashDirectory, packageVersion, resolveAsset } from './paths.mjs';
|
|
33
|
+
|
|
34
|
+
export const FORGEKIT_STAMP = '.forgekit.json';
|
|
35
|
+
|
|
36
|
+
/** @type {Record<string, { label: string, nextHint: string }>} */
|
|
37
|
+
export const SKILLS = {
|
|
38
|
+
forge: {
|
|
39
|
+
label: 'Forge',
|
|
40
|
+
nextHint:
|
|
41
|
+
'Forge: in each project, run `forge init` to add /forge commands, rules, and hooks.',
|
|
42
|
+
},
|
|
43
|
+
'thorough-code-review': {
|
|
44
|
+
label: 'Thorough Code Review',
|
|
45
|
+
nextHint:
|
|
46
|
+
'Thorough Code Review: invoke the skill explicitly (no auto-load). CLI: `review new|render|export|…`.',
|
|
47
|
+
},
|
|
48
|
+
'archive-to-adr': {
|
|
49
|
+
label: 'Archive → ADR',
|
|
50
|
+
nextHint:
|
|
51
|
+
'ADRs: after OpenSpec archive, run archive-to-adr (or stamp “No ADR”). Project path from `.forge/config.json`.',
|
|
52
|
+
},
|
|
53
|
+
'git-resolve-adr-conflict': {
|
|
54
|
+
label: 'Git: resolve ADR number conflict',
|
|
55
|
+
nextHint:
|
|
56
|
+
'ADR conflicts: invoke git-resolve-adr-conflict when two authors collide on the same NNNN.',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const SKILL_IDS = Object.freeze(Object.keys(SKILLS));
|
|
61
|
+
|
|
62
|
+
/** @type {Record<string, { label: string, skillDir: (home: string, skillId: string) => string }>} */
|
|
63
|
+
export const AGENTS = {
|
|
64
|
+
cursor: {
|
|
65
|
+
label: 'Cursor',
|
|
66
|
+
skillDir: (home, skillId) => path.join(home, '.cursor', 'skills', skillId),
|
|
67
|
+
},
|
|
68
|
+
claude: {
|
|
69
|
+
label: 'Claude Code',
|
|
70
|
+
skillDir: (home, skillId) => path.join(home, '.claude', 'skills', skillId),
|
|
71
|
+
},
|
|
72
|
+
codex: {
|
|
73
|
+
label: 'Codex CLI',
|
|
74
|
+
skillDir: (home, skillId) => path.join(home, '.codex', 'skills', skillId),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const AGENT_IDS = Object.freeze(Object.keys(AGENTS));
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} value
|
|
82
|
+
* @returns {string[]}
|
|
83
|
+
*/
|
|
84
|
+
function splitList(value) {
|
|
85
|
+
return String(value ?? '')
|
|
86
|
+
.split(/[,\s]+/)
|
|
87
|
+
.map((s) => s.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {string[]} argv
|
|
93
|
+
*/
|
|
94
|
+
export function parseArgs(argv) {
|
|
95
|
+
const opts = {
|
|
96
|
+
skills: /** @type {string[]} */ ([]),
|
|
97
|
+
agents: /** @type {string[]} */ ([]),
|
|
98
|
+
allSkills: false,
|
|
99
|
+
allAgents: false,
|
|
100
|
+
/** @deprecated use --all-agents; kept for forge/review install aliases */
|
|
101
|
+
all: false,
|
|
102
|
+
list: false,
|
|
103
|
+
help: false,
|
|
104
|
+
force: false,
|
|
105
|
+
update: false,
|
|
106
|
+
uninstall: false,
|
|
107
|
+
/** @type {boolean | null} null = unset (prompt / infer) */
|
|
108
|
+
adr: /** @type {boolean | null} */ (null),
|
|
109
|
+
adrDir: /** @type {string | null} */ (null),
|
|
110
|
+
/** @type {boolean | null} null = unset (prompt on TTY) */
|
|
111
|
+
openspec: /** @type {boolean | null} */ (null),
|
|
112
|
+
/** When true, also scaffold ADR files into cwd if it looks like a project */
|
|
113
|
+
adrProject: false,
|
|
114
|
+
noAdrProject: false,
|
|
115
|
+
cwd: process.cwd(),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
119
|
+
const arg = argv[i];
|
|
120
|
+
if (arg === '--skills' || arg === '--skill') {
|
|
121
|
+
opts.skills.push(...splitList(argv[++i]));
|
|
122
|
+
} else if (arg === '--agents' || arg === '--agent') {
|
|
123
|
+
opts.agents.push(...splitList(argv[++i]));
|
|
124
|
+
} else if (arg === '--all-skills') opts.allSkills = true;
|
|
125
|
+
else if (arg === '--all-agents') opts.allAgents = true;
|
|
126
|
+
else if (arg === '--all') opts.all = true;
|
|
127
|
+
else if (arg === '--list') opts.list = true;
|
|
128
|
+
else if (arg === '--update') opts.update = true;
|
|
129
|
+
else if (arg === '--uninstall') opts.uninstall = true;
|
|
130
|
+
else if (arg === '--force' || arg === '-f') opts.force = true;
|
|
131
|
+
else if (arg === '--help' || arg === '-h') opts.help = true;
|
|
132
|
+
else if (arg === '--cursor') opts.agents.push('cursor');
|
|
133
|
+
else if (arg === '--claude' || arg === '--claude-code') opts.agents.push('claude');
|
|
134
|
+
else if (arg === '--codex') opts.agents.push('codex');
|
|
135
|
+
else if (arg === '--adr') opts.adr = true;
|
|
136
|
+
else if (arg === '--no-adr') opts.adr = false;
|
|
137
|
+
else if (arg === '--adr-dir') opts.adrDir = argv[++i];
|
|
138
|
+
else if (arg === '--openspec') opts.openspec = true;
|
|
139
|
+
else if (arg === '--no-openspec') opts.openspec = false;
|
|
140
|
+
else if (arg === '--adr-project') opts.adrProject = true;
|
|
141
|
+
else if (arg === '--no-adr-project') opts.noAdrProject = true;
|
|
142
|
+
else if (arg === '--cwd') opts.cwd = argv[++i];
|
|
143
|
+
else if (
|
|
144
|
+
arg === 'forge' ||
|
|
145
|
+
arg === 'thorough-code-review' ||
|
|
146
|
+
arg === 'archive-to-adr' ||
|
|
147
|
+
arg === 'git-resolve-adr-conflict'
|
|
148
|
+
) {
|
|
149
|
+
opts.skills.push(arg);
|
|
150
|
+
} else throw new Error(`Unknown argument: ${arg}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
opts.skills = [...new Set(opts.skills)];
|
|
154
|
+
opts.agents = [...new Set(opts.agents)];
|
|
155
|
+
return opts;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function printHelp() {
|
|
159
|
+
process.stdout.write(`Usage: forgekit install [options]
|
|
160
|
+
|
|
161
|
+
Install one or more skills into user-level agent skill directories.
|
|
162
|
+
|
|
163
|
+
Options:
|
|
164
|
+
--skills <ids> Comma list: ${SKILL_IDS.join(', ')}
|
|
165
|
+
--agents <ids> Comma list: ${AGENT_IDS.join(', ')}
|
|
166
|
+
--all-skills Install every known skill
|
|
167
|
+
--all-agents Install for every agent environment
|
|
168
|
+
--cursor/--claude/--codex
|
|
169
|
+
Shorthand agent flags (same as --agents)
|
|
170
|
+
--openspec Prefer OpenSpec as the planning engine (save user default)
|
|
171
|
+
--no-openspec Prefer the built-in specs engine (save user default)
|
|
172
|
+
--adr Enable ADRs (install ADR skills; save user default)
|
|
173
|
+
--no-adr Disable ADRs (skip ADR skills; save user default)
|
|
174
|
+
--adr-dir <path> Default ADR directory inside repos (default: ${DEFAULT_ADR_DIR})
|
|
175
|
+
--adr-project Also scaffold ADR docs into --cwd when it is a git repo
|
|
176
|
+
--no-adr-project Never scaffold into cwd
|
|
177
|
+
--cwd <path> Project root for optional ADR scaffold (default: cwd)
|
|
178
|
+
--list Show installed vs missing (and outdated) for all skill×agent pairs
|
|
179
|
+
--update Reinstall outdated installed skills (same agents as present)
|
|
180
|
+
--uninstall Remove installed skill dirs for selected skills×agents
|
|
181
|
+
--force, -f Overwrite existing skill directories
|
|
182
|
+
--help
|
|
183
|
+
|
|
184
|
+
Interactive (TTY) when skills and/or agents are omitted. You are also asked
|
|
185
|
+
whether to plan with OpenSpec (vs the built-in specs engine), whether to use
|
|
186
|
+
ADRs, and for the ADR path inside the repo (default ${DEFAULT_ADR_DIR}).
|
|
187
|
+
|
|
188
|
+
Aliases:
|
|
189
|
+
forge install […] → forgekit install --skills forge […]
|
|
190
|
+
review install […] → forgekit install --skills thorough-code-review […]
|
|
191
|
+
|
|
192
|
+
Examples:
|
|
193
|
+
forgekit install
|
|
194
|
+
forgekit install --skills forge,thorough-code-review --agents cursor --adr
|
|
195
|
+
forgekit install --all-skills --all-agents --force
|
|
196
|
+
forgekit list
|
|
197
|
+
forgekit update
|
|
198
|
+
forgekit uninstall --skills forge --agents cursor
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {string} skillId
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
export function resolveSkillSource(skillId) {
|
|
207
|
+
if (!SKILLS[skillId]) {
|
|
208
|
+
throw new Error(`Unknown skill: ${skillId}. Known: ${SKILL_IDS.join(', ')}`);
|
|
209
|
+
}
|
|
210
|
+
return resolveAsset(path.join('skills', skillId), { requireFile: 'SKILL.md' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Write version + content hash stamp into an installed skill directory.
|
|
215
|
+
* @param {string} dest
|
|
216
|
+
* @param {string} skillId
|
|
217
|
+
* @param {string} skillSource
|
|
218
|
+
*/
|
|
219
|
+
export function writeInstallStamp(dest, skillId, skillSource) {
|
|
220
|
+
const stamp = {
|
|
221
|
+
skill: skillId,
|
|
222
|
+
version: packageVersion(),
|
|
223
|
+
contentHash: hashDirectory(skillSource),
|
|
224
|
+
installedAt: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
fs.writeFileSync(
|
|
227
|
+
path.join(dest, FORGEKIT_STAMP),
|
|
228
|
+
`${JSON.stringify(stamp, null, 2)}\n`,
|
|
229
|
+
'utf8',
|
|
230
|
+
);
|
|
231
|
+
return stamp;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {string} dest
|
|
236
|
+
* @returns {{ skill?: string, version?: string, contentHash?: string } | null}
|
|
237
|
+
*/
|
|
238
|
+
export function readInstallStamp(dest) {
|
|
239
|
+
const p = path.join(dest, FORGEKIT_STAMP);
|
|
240
|
+
if (!fs.existsSync(p)) return null;
|
|
241
|
+
try {
|
|
242
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {string} skillId
|
|
250
|
+
* @param {string} dest
|
|
251
|
+
* @returns {'missing' | 'present' | 'outdated' | 'unversioned'}
|
|
252
|
+
*/
|
|
253
|
+
export function skillInstallStatus(skillId, dest) {
|
|
254
|
+
if (!fs.existsSync(dest)) return 'missing';
|
|
255
|
+
const stamp = readInstallStamp(dest);
|
|
256
|
+
if (!stamp?.contentHash) return 'unversioned';
|
|
257
|
+
try {
|
|
258
|
+
const source = resolveSkillSource(skillId);
|
|
259
|
+
const current = hashDirectory(source);
|
|
260
|
+
if (stamp.contentHash !== current || stamp.version !== packageVersion()) {
|
|
261
|
+
return 'outdated';
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
return 'present';
|
|
265
|
+
}
|
|
266
|
+
return 'present';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {string} src
|
|
271
|
+
* @param {string} dest
|
|
272
|
+
*/
|
|
273
|
+
export function copyDirRecursive(src, dest) {
|
|
274
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
275
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
276
|
+
const from = path.join(src, entry.name);
|
|
277
|
+
const to = path.join(dest, entry.name);
|
|
278
|
+
if (entry.isDirectory()) copyDirRecursive(from, to);
|
|
279
|
+
else fs.copyFileSync(from, to);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @param {string} dir
|
|
285
|
+
*/
|
|
286
|
+
function removeDirRecursive(dir) {
|
|
287
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* @param {string[]} skillIds
|
|
292
|
+
* @param {string[]} agentIds
|
|
293
|
+
* @param {{ force?: boolean, home?: string }} [opts]
|
|
294
|
+
*/
|
|
295
|
+
export function installSkillsToAgents(skillIds, agentIds, opts = {}) {
|
|
296
|
+
const home = opts.home ?? os.homedir();
|
|
297
|
+
/** @type {{ skill: string, agent: string, dest: string, status: string, message?: string, skillSource?: string }[]} */
|
|
298
|
+
const results = [];
|
|
299
|
+
|
|
300
|
+
for (const skillId of skillIds) {
|
|
301
|
+
const skillSource = resolveSkillSource(skillId);
|
|
302
|
+
for (const agentId of agentIds) {
|
|
303
|
+
const agent = AGENTS[agentId];
|
|
304
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}`);
|
|
305
|
+
const dest = agent.skillDir(home, skillId);
|
|
306
|
+
const exists = fs.existsSync(dest);
|
|
307
|
+
if (exists && !opts.force) {
|
|
308
|
+
results.push({
|
|
309
|
+
skill: skillId,
|
|
310
|
+
agent: agentId,
|
|
311
|
+
dest,
|
|
312
|
+
status: 'exists',
|
|
313
|
+
message: 'already installed (use --force)',
|
|
314
|
+
skillSource,
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (exists) removeDirRecursive(dest);
|
|
319
|
+
copyDirRecursive(skillSource, dest);
|
|
320
|
+
writeInstallStamp(dest, skillId, skillSource);
|
|
321
|
+
results.push({
|
|
322
|
+
skill: skillId,
|
|
323
|
+
agent: agentId,
|
|
324
|
+
dest,
|
|
325
|
+
status: 'installed',
|
|
326
|
+
skillSource,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return results;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* @param {string[]} skillIds
|
|
336
|
+
* @param {string[]} agentIds
|
|
337
|
+
* @param {{ home?: string }} [opts]
|
|
338
|
+
*/
|
|
339
|
+
export function uninstallSkillsFromAgents(skillIds, agentIds, opts = {}) {
|
|
340
|
+
const home = opts.home ?? os.homedir();
|
|
341
|
+
/** @type {{ skill: string, agent: string, dest: string, status: string }[]} */
|
|
342
|
+
const results = [];
|
|
343
|
+
for (const skillId of skillIds) {
|
|
344
|
+
for (const agentId of agentIds) {
|
|
345
|
+
const agent = AGENTS[agentId];
|
|
346
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}`);
|
|
347
|
+
const dest = agent.skillDir(home, skillId);
|
|
348
|
+
if (!fs.existsSync(dest)) {
|
|
349
|
+
results.push({ skill: skillId, agent: agentId, dest, status: 'missing' });
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
removeDirRecursive(dest);
|
|
353
|
+
results.push({ skill: skillId, agent: agentId, dest, status: 'removed' });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return results;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Reinstall skills that are outdated or unversioned for agents that already have them.
|
|
361
|
+
* @param {{ home?: string, skills?: string[], agents?: string[] }} [opts]
|
|
362
|
+
*/
|
|
363
|
+
export function updateOutdatedSkills(opts = {}) {
|
|
364
|
+
const home = opts.home ?? os.homedir();
|
|
365
|
+
const skillFilter = opts.skills?.length ? new Set(opts.skills) : null;
|
|
366
|
+
const agentFilter = opts.agents?.length ? new Set(opts.agents) : null;
|
|
367
|
+
/** @type {string[]} */
|
|
368
|
+
const skills = [];
|
|
369
|
+
/** @type {string[]} */
|
|
370
|
+
const agents = [];
|
|
371
|
+
/** @type {Set<string>} */
|
|
372
|
+
const skillSet = new Set();
|
|
373
|
+
/** @type {Set<string>} */
|
|
374
|
+
const agentSet = new Set();
|
|
375
|
+
|
|
376
|
+
for (const skillId of SKILL_IDS) {
|
|
377
|
+
if (skillFilter && !skillFilter.has(skillId)) continue;
|
|
378
|
+
for (const agentId of AGENT_IDS) {
|
|
379
|
+
if (agentFilter && !agentFilter.has(agentId)) continue;
|
|
380
|
+
const dest = AGENTS[agentId].skillDir(home, skillId);
|
|
381
|
+
const status = skillInstallStatus(skillId, dest);
|
|
382
|
+
if (status === 'outdated' || status === 'unversioned') {
|
|
383
|
+
skillSet.add(skillId);
|
|
384
|
+
agentSet.add(agentId);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
skills.push(...skillSet);
|
|
389
|
+
agents.push(...agentSet);
|
|
390
|
+
if (skills.length === 0) return { results: [], skills, agents };
|
|
391
|
+
const results = installSkillsToAgents(skills, agents, { home, force: true });
|
|
392
|
+
return { results, skills, agents };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @param {{ home?: string }} [opts]
|
|
397
|
+
*/
|
|
398
|
+
export function listInstallStatus(opts = {}) {
|
|
399
|
+
const home = opts.home ?? os.homedir();
|
|
400
|
+
/** @type {{ skill: string, agent: string, dest: string, status: string }[]} */
|
|
401
|
+
const rows = [];
|
|
402
|
+
for (const skillId of SKILL_IDS) {
|
|
403
|
+
for (const agentId of AGENT_IDS) {
|
|
404
|
+
const dest = AGENTS[agentId].skillDir(home, skillId);
|
|
405
|
+
rows.push({
|
|
406
|
+
skill: skillId,
|
|
407
|
+
agent: agentId,
|
|
408
|
+
dest,
|
|
409
|
+
status: skillInstallStatus(skillId, dest),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return rows;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {string} question
|
|
418
|
+
* @param {Record<string, string>} map number → id
|
|
419
|
+
* @param {string[]} allIds
|
|
420
|
+
* @returns {Promise<string[]>}
|
|
421
|
+
*/
|
|
422
|
+
async function promptMulti(question, map, allIds) {
|
|
423
|
+
const rl = readline.createInterface({ input, output });
|
|
424
|
+
try {
|
|
425
|
+
process.stdout.write(`${question}\n`);
|
|
426
|
+
const entries = Object.entries(map);
|
|
427
|
+
for (const [num, id] of entries) {
|
|
428
|
+
const label =
|
|
429
|
+
SKILLS[id]?.label ?? AGENTS[id]?.label ?? id;
|
|
430
|
+
process.stdout.write(` ${num}) ${label}\n`);
|
|
431
|
+
}
|
|
432
|
+
const allNum = String(entries.length + 1);
|
|
433
|
+
process.stdout.write(` ${allNum}) All\n`);
|
|
434
|
+
process.stdout.write(
|
|
435
|
+
`Enter numbers separated by commas (e.g. 1,2) or ${allNum} for all: `,
|
|
436
|
+
);
|
|
437
|
+
const answer = (await rl.question('')).trim();
|
|
438
|
+
if (!answer || answer === allNum) return [...allIds];
|
|
439
|
+
const picked = [
|
|
440
|
+
...new Set(
|
|
441
|
+
answer
|
|
442
|
+
.split(/[,\s]+/)
|
|
443
|
+
.map((s) => map[s.trim()])
|
|
444
|
+
.filter(Boolean),
|
|
445
|
+
),
|
|
446
|
+
];
|
|
447
|
+
if (picked.length === 0) throw new Error('Nothing selected');
|
|
448
|
+
return picked;
|
|
449
|
+
} finally {
|
|
450
|
+
rl.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function promptSkills() {
|
|
455
|
+
const map = Object.fromEntries(SKILL_IDS.map((id, i) => [String(i + 1), id]));
|
|
456
|
+
return promptMulti('Install which skills?', map, SKILL_IDS);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function promptAgents() {
|
|
460
|
+
const map = Object.fromEntries(AGENT_IDS.map((id, i) => [String(i + 1), id]));
|
|
461
|
+
return promptMulti('Install for which environments?', map, AGENT_IDS);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* @param {string} [defaultDir]
|
|
466
|
+
* @returns {Promise<string>}
|
|
467
|
+
*/
|
|
468
|
+
export async function promptAdrDir(defaultDir = DEFAULT_ADR_DIR) {
|
|
469
|
+
const rl = readline.createInterface({ input, output });
|
|
470
|
+
try {
|
|
471
|
+
const dirAnswer = (
|
|
472
|
+
await rl.question(`ADR directory inside each repo [${defaultDir}]: `)
|
|
473
|
+
).trim();
|
|
474
|
+
return normalizeAdrDir(dirAnswer || defaultDir);
|
|
475
|
+
} finally {
|
|
476
|
+
rl.close();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* @returns {Promise<{ enabled: boolean, dir: string }>}
|
|
482
|
+
*/
|
|
483
|
+
export async function promptAdrOptions() {
|
|
484
|
+
const rl = readline.createInterface({ input, output });
|
|
485
|
+
let enabled = false;
|
|
486
|
+
try {
|
|
487
|
+
const yn = (
|
|
488
|
+
await rl.question(
|
|
489
|
+
'Use Architecture Decision Records (ADRs) after OpenSpec archive? [y/N] ',
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
.trim()
|
|
493
|
+
.toLowerCase();
|
|
494
|
+
enabled = yn === 'y' || yn === 'yes';
|
|
495
|
+
} finally {
|
|
496
|
+
rl.close();
|
|
497
|
+
}
|
|
498
|
+
if (!enabled) return { enabled: false, dir: DEFAULT_ADR_DIR };
|
|
499
|
+
const dir = await promptAdrDir(DEFAULT_ADR_DIR);
|
|
500
|
+
return { enabled: true, dir };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* @returns {Promise<boolean>} true = OpenSpec, false = built-in specs engine
|
|
505
|
+
*/
|
|
506
|
+
export async function promptOpenSpec() {
|
|
507
|
+
const rl = readline.createInterface({ input, output });
|
|
508
|
+
try {
|
|
509
|
+
const yn = (
|
|
510
|
+
await rl.question(
|
|
511
|
+
'Plan with OpenSpec (vendor CLI)? [Y/n] (n = built-in specs engine) ',
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
.trim()
|
|
515
|
+
.toLowerCase();
|
|
516
|
+
return !(yn === 'n' || yn === 'no');
|
|
517
|
+
} finally {
|
|
518
|
+
rl.close();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Merge ADR skills into the skill list when ADRs are enabled.
|
|
524
|
+
* @param {string[]} skills
|
|
525
|
+
* @param {boolean} adrEnabled
|
|
526
|
+
* @returns {string[]}
|
|
527
|
+
*/
|
|
528
|
+
export function applyAdrSkills(skills, adrEnabled) {
|
|
529
|
+
const next = [...skills];
|
|
530
|
+
if (adrEnabled) {
|
|
531
|
+
for (const id of ADR_SKILLS) {
|
|
532
|
+
if (!next.includes(id)) next.push(id);
|
|
533
|
+
}
|
|
534
|
+
return next;
|
|
535
|
+
}
|
|
536
|
+
return next.filter((id) => !ADR_SKILLS.includes(id));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Infer ADR enablement from explicit skill picks when --adr/--no-adr omitted.
|
|
541
|
+
* @param {string[]} skills
|
|
542
|
+
* @param {boolean | null} adrFlag
|
|
543
|
+
* @returns {boolean | null} null = still unknown
|
|
544
|
+
*/
|
|
545
|
+
export function inferAdrFromSkills(skills, adrFlag) {
|
|
546
|
+
if (adrFlag !== null) return adrFlag;
|
|
547
|
+
if (skills.some((id) => ADR_SKILLS.includes(id))) return true;
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Resolve ADR enablement + directory from flags / prompts.
|
|
553
|
+
* @param {{ adr: boolean | null, adrDir: string | null, skills: string[] }} opts
|
|
554
|
+
* @returns {Promise<{ enabled: boolean, dir: string }>}
|
|
555
|
+
*/
|
|
556
|
+
export async function resolveAdrInstallOptions(opts) {
|
|
557
|
+
const adrDecision = inferAdrFromSkills(opts.skills, opts.adr);
|
|
558
|
+
/** @type {{ enabled: boolean, dir: string }} */
|
|
559
|
+
let adrOpts = { enabled: false, dir: DEFAULT_ADR_DIR };
|
|
560
|
+
|
|
561
|
+
if (adrDecision === null && process.stdin.isTTY) {
|
|
562
|
+
adrOpts = await promptAdrOptions();
|
|
563
|
+
} else if (adrDecision === true) {
|
|
564
|
+
adrOpts.enabled = true;
|
|
565
|
+
adrOpts.dir = opts.adrDir
|
|
566
|
+
? normalizeAdrDir(opts.adrDir)
|
|
567
|
+
: process.stdin.isTTY
|
|
568
|
+
? await promptAdrDir(DEFAULT_ADR_DIR)
|
|
569
|
+
: DEFAULT_ADR_DIR;
|
|
570
|
+
} else if (adrDecision === false) {
|
|
571
|
+
adrOpts = {
|
|
572
|
+
enabled: false,
|
|
573
|
+
dir: opts.adrDir ? normalizeAdrDir(opts.adrDir) : DEFAULT_ADR_DIR,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (opts.adrDir && adrOpts.enabled) {
|
|
578
|
+
adrOpts.dir = normalizeAdrDir(opts.adrDir);
|
|
579
|
+
}
|
|
580
|
+
return adrOpts;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @param {string[]} skills
|
|
585
|
+
* @param {string[]} agents
|
|
586
|
+
* @returns {Promise<{ skills: string[], agents: string[] } | number>}
|
|
587
|
+
*/
|
|
588
|
+
async function resolveSkillsAndAgents(skillsIn, agentsIn) {
|
|
589
|
+
let skills = [...skillsIn];
|
|
590
|
+
let agents = [...agentsIn];
|
|
591
|
+
|
|
592
|
+
if (skills.length === 0) {
|
|
593
|
+
if (!process.stdin.isTTY) {
|
|
594
|
+
process.stderr.write(
|
|
595
|
+
'No skills specified. Pass --skills / --all-skills, or run in a TTY.\n',
|
|
596
|
+
);
|
|
597
|
+
return 1;
|
|
598
|
+
}
|
|
599
|
+
skills = await promptSkills();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const id of skills) {
|
|
603
|
+
if (!SKILLS[id]) {
|
|
604
|
+
process.stderr.write(`Unknown skill: ${id}. Known: ${SKILL_IDS.join(', ')}\n`);
|
|
605
|
+
return 1;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (agents.length === 0) {
|
|
610
|
+
if (!process.stdin.isTTY) {
|
|
611
|
+
process.stderr.write(
|
|
612
|
+
'No agents specified. Pass --agents / --all-agents / --cursor…, or run in a TTY.\n',
|
|
613
|
+
);
|
|
614
|
+
return 1;
|
|
615
|
+
}
|
|
616
|
+
agents = await promptAgents();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (const id of agents) {
|
|
620
|
+
if (!AGENTS[id]) {
|
|
621
|
+
process.stderr.write(`Unknown agent: ${id}. Known: ${AGENT_IDS.join(', ')}\n`);
|
|
622
|
+
return 1;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { skills, agents };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* @param {string[]} argv
|
|
631
|
+
* @returns {Promise<number>}
|
|
632
|
+
*/
|
|
633
|
+
export async function runInstall(argv = process.argv.slice(2)) {
|
|
634
|
+
const opts = parseArgs(argv);
|
|
635
|
+
if (opts.help) {
|
|
636
|
+
printHelp();
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (opts.list) {
|
|
641
|
+
for (const row of listInstallStatus()) {
|
|
642
|
+
process.stdout.write(
|
|
643
|
+
`${row.skill.padEnd(28)} ${row.agent.padEnd(8)} ${row.status.padEnd(12)} ${row.dest}\n`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (opts.update) {
|
|
650
|
+
const updated = updateOutdatedSkills({
|
|
651
|
+
skills: opts.skills.length ? opts.skills : undefined,
|
|
652
|
+
agents: opts.agents.length || opts.allAgents || opts.all ? (
|
|
653
|
+
opts.allAgents || opts.all ? [...AGENT_IDS] : opts.agents
|
|
654
|
+
) : undefined,
|
|
655
|
+
});
|
|
656
|
+
if (updated.results.length === 0) {
|
|
657
|
+
process.stdout.write('All installed skills are up to date.\n');
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
for (const r of updated.results) {
|
|
661
|
+
process.stdout.write(
|
|
662
|
+
`${r.skill} × ${r.agent}: ${r.status} → ${r.dest}\n`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let skills = opts.allSkills ? [...SKILL_IDS] : [...opts.skills];
|
|
669
|
+
let agents =
|
|
670
|
+
opts.allAgents || opts.all ? [...AGENT_IDS] : [...opts.agents];
|
|
671
|
+
|
|
672
|
+
const resolved = await resolveSkillsAndAgents(skills, agents);
|
|
673
|
+
if (typeof resolved === 'number') return resolved;
|
|
674
|
+
skills = resolved.skills;
|
|
675
|
+
agents = resolved.agents;
|
|
676
|
+
|
|
677
|
+
if (opts.uninstall) {
|
|
678
|
+
const results = uninstallSkillsFromAgents(skills, agents);
|
|
679
|
+
for (const r of results) {
|
|
680
|
+
process.stdout.write(`${r.skill} × ${r.agent}: ${r.status} → ${r.dest}\n`);
|
|
681
|
+
}
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** @type {boolean | null} */
|
|
686
|
+
let useOpenSpec = opts.openspec;
|
|
687
|
+
if (useOpenSpec === null && skills.includes('forge') && process.stdin.isTTY) {
|
|
688
|
+
useOpenSpec = await promptOpenSpec();
|
|
689
|
+
}
|
|
690
|
+
if (useOpenSpec !== null) {
|
|
691
|
+
saveUserPlanEngine(useOpenSpec ? 'openspec' : 'specs');
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const adrOpts = await resolveAdrInstallOptions({
|
|
695
|
+
adr: opts.adr,
|
|
696
|
+
adrDir: opts.adrDir,
|
|
697
|
+
skills,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
skills = applyAdrSkills(skills, adrOpts.enabled);
|
|
701
|
+
|
|
702
|
+
saveUserConfig({
|
|
703
|
+
adr: { enabled: adrOpts.enabled, dir: adrOpts.dir },
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const results = installSkillsToAgents(skills, agents, { force: opts.force });
|
|
707
|
+
const sources = new Map();
|
|
708
|
+
for (const r of results) {
|
|
709
|
+
if (r.skillSource) sources.set(r.skill, r.skillSource);
|
|
710
|
+
}
|
|
711
|
+
for (const [skill, src] of sources) {
|
|
712
|
+
process.stdout.write(`Skill ${skill}: ${src}\n`);
|
|
713
|
+
}
|
|
714
|
+
for (const r of results) {
|
|
715
|
+
process.stdout.write(
|
|
716
|
+
`${r.skill} × ${r.agent}: ${r.status}${r.message ? ` — ${r.message}` : ''} → ${r.dest}\n`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (useOpenSpec !== null) {
|
|
721
|
+
process.stdout.write(
|
|
722
|
+
`\nPlanning engine saved (~/.forgekit/config.json): ${
|
|
723
|
+
useOpenSpec ? 'openspec' : 'specs (built-in)'
|
|
724
|
+
} — per-project setup happens at \`forge init\`.\n`,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
process.stdout.write(
|
|
728
|
+
`${useOpenSpec !== null ? '' : '\n'}ADR preference saved (~/.forgekit/config.json): ${
|
|
729
|
+
adrOpts.enabled ? `enabled, dir=${adrOpts.dir}` : 'disabled'
|
|
730
|
+
}\n`,
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const inRepo = isGitRepo(opts.cwd);
|
|
734
|
+
const shouldScaffold =
|
|
735
|
+
!opts.noAdrProject &&
|
|
736
|
+
adrOpts.enabled &&
|
|
737
|
+
(opts.adrProject || (inRepo && process.stdin.isTTY));
|
|
738
|
+
|
|
739
|
+
if (shouldScaffold) {
|
|
740
|
+
const scaffold = scaffoldAdr(opts.cwd, {
|
|
741
|
+
dir: adrOpts.dir,
|
|
742
|
+
force: opts.force,
|
|
743
|
+
hooks: true,
|
|
744
|
+
});
|
|
745
|
+
process.stdout.write(
|
|
746
|
+
`ADR project scaffold in ${opts.cwd}: ${scaffold.decisionsDoc}, ${scaffold.dir}/README.md, .forge/config.json\n`,
|
|
747
|
+
);
|
|
748
|
+
for (const f of scaffold.files) {
|
|
749
|
+
process.stdout.write(` ${f.status.padEnd(8)} ${f.file}\n`);
|
|
750
|
+
}
|
|
751
|
+
} else if (inRepo && !adrOpts.enabled && opts.adr === false) {
|
|
752
|
+
disableProjectAdr(opts.cwd);
|
|
753
|
+
process.stdout.write(
|
|
754
|
+
`ADRs disabled in project (.forge/config.json) under ${opts.cwd}\n`,
|
|
755
|
+
);
|
|
756
|
+
} else if (adrOpts.enabled) {
|
|
757
|
+
process.stdout.write(
|
|
758
|
+
`Tip: in each repo run \`forge init --adr\` (or \`forgekit install --adr --adr-project\`) to scaffold ${adrOpts.dir}/ and decisions.md.\n`,
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const hints = [...new Set(skills.map((id) => SKILLS[id].nextHint))];
|
|
763
|
+
process.stdout.write(`\n${hints.join('\n')}\n`);
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const isDirect =
|
|
768
|
+
process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
|
|
769
|
+
|
|
770
|
+
if (isDirect) {
|
|
771
|
+
runInstall()
|
|
772
|
+
.then((code) => process.exit(code))
|
|
773
|
+
.catch((err) => {
|
|
774
|
+
process.stderr.write(`${err.message || err}\n`);
|
|
775
|
+
process.exit(1);
|
|
776
|
+
});
|
|
777
|
+
}
|