@izkac/forgekit 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/forge.mjs +107 -107
- package/bin/forgekit.mjs +83 -83
- package/bin/review.mjs +81 -81
- package/package.json +1 -1
- package/scripts/prepack.mjs +78 -78
- package/scripts/run-tests.mjs +50 -50
- package/src/adr.mjs +236 -236
- package/src/adr.test.mjs +170 -170
- package/src/change.mjs +327 -234
- package/src/change.test.mjs +145 -83
- package/src/cleanup-sessions.mjs +84 -84
- package/src/config.mjs +103 -103
- package/src/defer.mjs +75 -75
- package/src/doctor.mjs +350 -341
- package/src/doctor.test.mjs +114 -114
- package/src/init.mjs +680 -621
- package/src/install.mjs +815 -815
- package/src/install.test.mjs +180 -180
- package/src/integrity-check.mjs +60 -60
- package/src/integrity.mjs +682 -682
- package/src/integrity.test.mjs +566 -566
- package/src/lib.mjs +143 -143
- package/src/models.defaults.json +41 -41
- package/src/new-session.mjs +99 -99
- package/src/openspec-overlays/README.md +19 -19
- package/src/openspec-overlays/openspec-apply-change-footer.md +14 -14
- package/src/openspec-overlays/opsx-apply-completion-step.md +1 -1
- package/src/openspec-overlays/opsx-apply-implement-step.md +11 -11
- package/src/paths.mjs +92 -92
- package/src/plan-engine.mjs +321 -278
- package/src/plan-engine.test.mjs +447 -283
- package/src/preferences.defaults.json +78 -78
- package/src/preferences.mjs +438 -438
- package/src/preferences.test.mjs +174 -174
- package/src/record-evidence.mjs +204 -204
- package/src/resolve-model.mjs +312 -312
- package/src/resolve-model.test.mjs +194 -194
- package/src/review/cli.test.mjs +117 -117
- package/src/review/export.mjs +172 -172
- package/src/review/export.test.mjs +197 -197
- package/src/review/fixtures/valid-review.json +42 -42
- package/src/review/lib.mjs +894 -894
- package/src/review/lib.test.mjs +266 -266
- package/src/review/schema.json +196 -196
- package/src/review/signals.test.mjs +62 -62
- package/src/score-cli.mjs +68 -68
- package/src/score.mjs +568 -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 +234 -0
- package/src/specs-sync.test.mjs +114 -0
- package/src/spine.mjs +93 -93
- package/src/triage-prompt.mjs +175 -175
- package/src/triage-prompt.test.mjs +50 -50
- package/src/vendor-openspec-overlays.mjs +176 -176
- package/src/vendor-openspec-overlays.test.mjs +62 -62
- package/vendor/skills/archive-to-adr/SKILL.md +149 -149
- package/vendor/skills/forge/SKILL.md +136 -136
- package/vendor/skills/forge/docs/forge.md +650 -647
- package/vendor/skills/forge/phases/brainstorm.md +23 -23
- package/vendor/skills/forge/phases/finish.md +90 -87
- package/vendor/skills/forge/phases/implement.md +77 -77
- package/vendor/skills/forge/phases/plan-openspec.md +60 -60
- package/vendor/skills/forge/phases/plan-specs.md +163 -117
- package/vendor/skills/forge/phases/review.md +25 -25
- package/vendor/skills/forge/phases/verify.md +124 -124
- package/vendor/skills/forge/references/forge-layout.md +85 -85
- package/vendor/skills/forge/references/pace.md +115 -115
- package/vendor/skills/forge/references/plan-routing.md +52 -51
- package/vendor/skills/forge/references/runtime-integrity.md +232 -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/change.mjs
CHANGED
|
@@ -1,234 +1,327 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Specs-engine change scaffolding.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* forge change new <name> [--cwd <path>] [--force]
|
|
7
|
-
* forge change archive <name> [--cwd <path>] [--date YYYY-MM-DD]
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { pathToFileURL } from 'node:url';
|
|
13
|
-
import {
|
|
14
|
-
DEFAULT_SPECS_DIR,
|
|
15
|
-
resolveProjectPlanEngine,
|
|
16
|
-
} from './plan-engine.mjs';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else if (
|
|
38
|
-
else if (
|
|
39
|
-
else
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Specs-engine change scaffolding.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* forge change new <name> [--capability <id>]… [--cwd <path>] [--force]
|
|
7
|
+
* forge change archive <name> [--cwd <path>] [--date YYYY-MM-DD] [--no-sync]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_SPECS_DIR,
|
|
15
|
+
resolveProjectPlanEngine,
|
|
16
|
+
} from './plan-engine.mjs';
|
|
17
|
+
import { deltaSpecTemplate, mergeChangeDeltas } from './specs-sync.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string[]} argv
|
|
21
|
+
*/
|
|
22
|
+
export function parseArgs(argv) {
|
|
23
|
+
const opts = {
|
|
24
|
+
help: false,
|
|
25
|
+
force: false,
|
|
26
|
+
noSync: false,
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
date: /** @type {string | null} */ (null),
|
|
29
|
+
action: /** @type {string | null} */ (null),
|
|
30
|
+
name: /** @type {string | null} */ (null),
|
|
31
|
+
capabilities: /** @type {string[]} */ ([]),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35
|
+
const arg = argv[i];
|
|
36
|
+
if (arg === '--help' || arg === '-h') opts.help = true;
|
|
37
|
+
else if (arg === '--force' || arg === '-f') opts.force = true;
|
|
38
|
+
else if (arg === '--no-sync') opts.noSync = true;
|
|
39
|
+
else if (arg === '--cwd') opts.cwd = argv[++i];
|
|
40
|
+
else if (arg === '--date') opts.date = argv[++i];
|
|
41
|
+
else if (arg === '--capability' || arg === '--cap') {
|
|
42
|
+
const cap = argv[++i];
|
|
43
|
+
if (!cap) throw new Error(`${arg} requires a capability id`);
|
|
44
|
+
opts.capabilities.push(cap);
|
|
45
|
+
} else if (!opts.action && (arg === 'new' || arg === 'archive')) opts.action = arg;
|
|
46
|
+
else if (!opts.name && !arg.startsWith('-')) opts.name = arg;
|
|
47
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
48
|
+
}
|
|
49
|
+
return opts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function printHelp() {
|
|
53
|
+
process.stdout.write(`Usage: forge change <new|archive> <name> [options]
|
|
54
|
+
|
|
55
|
+
Scaffold or archive a change for the built-in specs planning engine.
|
|
56
|
+
Layout matches OpenSpec: proposal / design / tasks / delta specs under
|
|
57
|
+
\`changes/<name>/specs/<capability>/spec.md\`, main catalog at
|
|
58
|
+
\`<plan.dir>/specs/\`.
|
|
59
|
+
|
|
60
|
+
Commands:
|
|
61
|
+
new <name> Create <plan.dir>/changes/<name>/{proposal,design,tasks}.md
|
|
62
|
+
plus delta stubs for each --capability
|
|
63
|
+
archive <name> Merge deltas → <plan.dir>/specs/, then move change to
|
|
64
|
+
changes/archive/YYYY-MM-DD-<name>
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
--capability <id> Delta capability to stub (repeatable). kebab-case id
|
|
68
|
+
matching OpenSpec domains (e.g. auth, payments)
|
|
69
|
+
--cwd <path> Project root (default: cwd)
|
|
70
|
+
--date YYYY-MM-DD Archive date prefix (default: today UTC)
|
|
71
|
+
--no-sync Archive without merging deltas into the main catalog
|
|
72
|
+
--force, -f Overwrite existing scaffold files on new
|
|
73
|
+
--help
|
|
74
|
+
|
|
75
|
+
Requires \`.forge/config.json\` → plan.engine: specs (or run \`forge init --no-openspec\`).
|
|
76
|
+
Engine root is \`plan.dir\` (default \`specs\`; use \`openspec\` to reuse an
|
|
77
|
+
OpenSpec tree).
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} name
|
|
83
|
+
*/
|
|
84
|
+
export function assertChangeName(name) {
|
|
85
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Change name must be kebab-case (got: ${name}). Example: add-stripe-refunds`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} capability
|
|
95
|
+
*/
|
|
96
|
+
export function assertCapabilityName(capability) {
|
|
97
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(capability)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Capability id must be kebab-case (got: ${capability}). Example: auth`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return capability;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const PROPOSAL_TMPL = (title, capabilities) => {
|
|
106
|
+
const caps =
|
|
107
|
+
capabilities.length > 0
|
|
108
|
+
? capabilities.map((c) => `- \`${c}\`: … (delta: \`specs/${c}/spec.md\`)`).join('\n')
|
|
109
|
+
: `- \`<capability>\`: … — add with \`forge change new … --capability <id>\` or create \`specs/<id>/spec.md\``;
|
|
110
|
+
return `# ${title}
|
|
111
|
+
|
|
112
|
+
## Why
|
|
113
|
+
|
|
114
|
+
One or two paragraphs: problem / pressure.
|
|
115
|
+
|
|
116
|
+
## What Changes
|
|
117
|
+
|
|
118
|
+
- …
|
|
119
|
+
|
|
120
|
+
## Capabilities
|
|
121
|
+
|
|
122
|
+
${caps}
|
|
123
|
+
|
|
124
|
+
## Impact
|
|
125
|
+
|
|
126
|
+
Affected code/areas, risks, migration notes.
|
|
127
|
+
`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const DESIGN_TMPL = `# Design
|
|
131
|
+
|
|
132
|
+
## Context
|
|
133
|
+
|
|
134
|
+
…
|
|
135
|
+
|
|
136
|
+
## Decisions
|
|
137
|
+
|
|
138
|
+
- Decision: …
|
|
139
|
+
- Alternatives considered: …
|
|
140
|
+
- Rationale: …
|
|
141
|
+
|
|
142
|
+
## Risks / Trade-offs
|
|
143
|
+
|
|
144
|
+
- …
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
const TASKS_TMPL = `# Tasks
|
|
148
|
+
|
|
149
|
+
## 1. First group
|
|
150
|
+
- [ ] 1.1 Bite-sized task — exact files, expected tests
|
|
151
|
+
|
|
152
|
+
## 2. Second group
|
|
153
|
+
- [ ] 2.1 …
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} cwd
|
|
158
|
+
* @param {string} name
|
|
159
|
+
* @param {{ force?: boolean, capabilities?: string[] }} [opts]
|
|
160
|
+
*/
|
|
161
|
+
export function createSpecsChange(cwd, name, opts = {}) {
|
|
162
|
+
assertChangeName(name);
|
|
163
|
+
const capabilities = (opts.capabilities ?? []).map(assertCapabilityName);
|
|
164
|
+
const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
|
|
165
|
+
if (engine.engine !== 'specs') {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Project plan engine is "${engine.engine}", not "specs". ` +
|
|
168
|
+
`Use \`openspec-propose\` / \`/opsx:propose\` for OpenSpec projects, ` +
|
|
169
|
+
`or \`forge init --no-openspec\` to switch.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const dir = engine.dir || DEFAULT_SPECS_DIR;
|
|
173
|
+
const changeDir = path.join(cwd, dir, 'changes', name);
|
|
174
|
+
fs.mkdirSync(changeDir, { recursive: true });
|
|
175
|
+
fs.mkdirSync(path.join(changeDir, 'specs'), { recursive: true });
|
|
176
|
+
|
|
177
|
+
/** @type {{ file: string, status: string }[]} */
|
|
178
|
+
const files = [];
|
|
179
|
+
const write = (rel, body) => {
|
|
180
|
+
const dest = path.join(changeDir, rel);
|
|
181
|
+
if (fs.existsSync(dest) && !opts.force) {
|
|
182
|
+
files.push({ file: `${dir}/changes/${name}/${rel}`, status: 'skipped' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
186
|
+
fs.writeFileSync(dest, body, 'utf8');
|
|
187
|
+
files.push({ file: `${dir}/changes/${name}/${rel}`, status: 'written' });
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const title = name
|
|
191
|
+
.split('-')
|
|
192
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
193
|
+
.join(' ');
|
|
194
|
+
write('proposal.md', PROPOSAL_TMPL(title, capabilities));
|
|
195
|
+
write('design.md', DESIGN_TMPL);
|
|
196
|
+
write('tasks.md', TASKS_TMPL);
|
|
197
|
+
|
|
198
|
+
for (const cap of capabilities) {
|
|
199
|
+
write(`specs/${cap}/spec.md`, deltaSpecTemplate(cap));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { dir, changeDir, name, files, capabilities };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} cwd
|
|
207
|
+
* @param {string} name
|
|
208
|
+
* @param {{ date?: string | null, noSync?: boolean }} [opts]
|
|
209
|
+
*/
|
|
210
|
+
export function archiveSpecsChange(cwd, name, opts = {}) {
|
|
211
|
+
assertChangeName(name);
|
|
212
|
+
const engine = resolveProjectPlanEngine(cwd, { useUserDefault: false });
|
|
213
|
+
if (engine.engine !== 'specs') {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Project plan engine is "${engine.engine}", not "specs". ` +
|
|
216
|
+
`Use \`openspec archive\` / \`/opsx:archive\` for OpenSpec projects.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const dir = engine.dir || DEFAULT_SPECS_DIR;
|
|
220
|
+
const src = path.join(cwd, dir, 'changes', name);
|
|
221
|
+
if (!fs.existsSync(src)) {
|
|
222
|
+
throw new Error(`Change not found: ${dir}/changes/${name}`);
|
|
223
|
+
}
|
|
224
|
+
const date = opts.date || new Date().toISOString().slice(0, 10);
|
|
225
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
226
|
+
throw new Error(`Invalid --date (want YYYY-MM-DD): ${date}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @type {{ capability: string, status: string, file: string }[]} */
|
|
230
|
+
let synced = [];
|
|
231
|
+
if (!opts.noSync) {
|
|
232
|
+
const mainSpecsDir = path.join(cwd, dir, 'specs');
|
|
233
|
+
synced = mergeChangeDeltas(src, mainSpecsDir);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const archiveParent = path.join(cwd, dir, 'changes', 'archive');
|
|
237
|
+
fs.mkdirSync(archiveParent, { recursive: true });
|
|
238
|
+
const destName = `${date}-${name}`;
|
|
239
|
+
const dest = path.join(archiveParent, destName);
|
|
240
|
+
if (fs.existsSync(dest)) {
|
|
241
|
+
throw new Error(`Archive already exists: ${dir}/changes/archive/${destName}`);
|
|
242
|
+
}
|
|
243
|
+
fs.renameSync(src, dest);
|
|
244
|
+
return {
|
|
245
|
+
dir,
|
|
246
|
+
from: `${dir}/changes/${name}`,
|
|
247
|
+
to: `${dir}/changes/archive/${destName}`,
|
|
248
|
+
synced,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @param {string[]} argv
|
|
254
|
+
*/
|
|
255
|
+
export function runChange(argv) {
|
|
256
|
+
const opts = parseArgs(argv);
|
|
257
|
+
if (opts.help || !opts.action) {
|
|
258
|
+
printHelp();
|
|
259
|
+
return opts.help ? 0 : 1;
|
|
260
|
+
}
|
|
261
|
+
if (!opts.name) {
|
|
262
|
+
process.stderr.write('Missing change name.\n');
|
|
263
|
+
printHelp();
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (opts.action === 'new') {
|
|
268
|
+
const result = createSpecsChange(opts.cwd, opts.name, {
|
|
269
|
+
force: opts.force,
|
|
270
|
+
capabilities: opts.capabilities,
|
|
271
|
+
});
|
|
272
|
+
process.stdout.write(
|
|
273
|
+
`Created specs change "${result.name}" under ${result.dir}/changes/${result.name}/\n`,
|
|
274
|
+
);
|
|
275
|
+
for (const f of result.files) {
|
|
276
|
+
process.stdout.write(` ${f.status.padEnd(8)} ${f.file}\n`);
|
|
277
|
+
}
|
|
278
|
+
if (result.capabilities.length === 0) {
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`\nNo delta stubs yet — add with:\n` +
|
|
281
|
+
` forge change new ${result.name} --capability <domain> --force\n` +
|
|
282
|
+
`or create ${result.dir}/changes/${result.name}/specs/<domain>/spec.md\n`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
process.stdout.write(
|
|
286
|
+
`\nNext: edit proposal.md / design.md / tasks.md / specs/, then:\n` +
|
|
287
|
+
` forge phase plan --plan-type specs --openspec ${result.name}\n`,
|
|
288
|
+
);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (opts.action === 'archive') {
|
|
293
|
+
const result = archiveSpecsChange(opts.cwd, opts.name, {
|
|
294
|
+
date: opts.date,
|
|
295
|
+
noSync: opts.noSync,
|
|
296
|
+
});
|
|
297
|
+
for (const s of result.synced) {
|
|
298
|
+
process.stdout.write(
|
|
299
|
+
` sync ${s.status.padEnd(8)} ${result.dir}/specs/${s.capability}/spec.md\n`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (!opts.noSync && result.synced.length === 0) {
|
|
303
|
+
process.stdout.write(' sync (no delta specs to merge)\n');
|
|
304
|
+
}
|
|
305
|
+
process.stdout.write(`Archived ${result.from} → ${result.to}\n`);
|
|
306
|
+
process.stdout.write(
|
|
307
|
+
`If ADRs are enabled, run archive-to-adr on the archived folder.\n`,
|
|
308
|
+
);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
process.stderr.write(`Unknown action: ${opts.action}\n`);
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const isMain =
|
|
317
|
+
process.argv[1] &&
|
|
318
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
319
|
+
|
|
320
|
+
if (isMain) {
|
|
321
|
+
try {
|
|
322
|
+
process.exitCode = runChange(process.argv.slice(2));
|
|
323
|
+
} catch (err) {
|
|
324
|
+
process.stderr.write(`${err instanceof Error ? err.message : err}\n`);
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
}
|
|
327
|
+
}
|