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