@izkac/forgekit 0.1.5 → 0.1.7
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/package.json +1 -1
- package/src/init.mjs +18 -9
- package/src/init.test.mjs +32 -1
- package/src/plan-engine.mjs +22 -4
- package/src/plan-engine.test.mjs +38 -0
- package/vendor/templates/project/claude/commands/forge.md +1 -1
- package/vendor/templates/project/claude/rules/forge.md +20 -16
- package/vendor/templates/project/codex/rules/forge.md +12 -10
- package/vendor/templates/project/cursor/rules/forge.mdc +25 -21
package/package.json
CHANGED
package/src/init.mjs
CHANGED
|
@@ -100,7 +100,8 @@ Options:
|
|
|
100
100
|
--no-adr Disable ADRs for this project
|
|
101
101
|
--adr-dir <path> ADR directory (default: ${DEFAULT_ADR_DIR} or ~/.forgekit preference)
|
|
102
102
|
--overlay Also run \`forge overlay\` (OpenSpec vendor patches)
|
|
103
|
-
--force, -f
|
|
103
|
+
--force, -f Force re-scaffold of ADR/specs docs (managed command,
|
|
104
|
+
rule, and hook files always refresh to the latest template)
|
|
104
105
|
--cwd <path> Project root (default: cwd)
|
|
105
106
|
--help
|
|
106
107
|
|
|
@@ -124,16 +125,22 @@ export function resolveTemplatesRoot() {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
|
128
|
+
* Copy a forgekit-managed template file. These are regenerated pointers
|
|
129
|
+
* (forge-* commands/rules/hooks) with no user-owned content, so re-running
|
|
130
|
+
* `forge init` refreshes them in place — that's how template fixes propagate.
|
|
127
131
|
* @param {string} src
|
|
128
132
|
* @param {string} dest
|
|
129
|
-
* @param {{ force?: boolean }}
|
|
133
|
+
* @param {{ force?: boolean }} _opts
|
|
130
134
|
*/
|
|
131
|
-
function copyFile(src, dest,
|
|
135
|
+
function copyFile(src, dest, _opts) {
|
|
132
136
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
const next = fs.readFileSync(src);
|
|
138
|
+
if (fs.existsSync(dest)) {
|
|
139
|
+
if (fs.readFileSync(dest).equals(next)) return 'unchanged';
|
|
140
|
+
fs.writeFileSync(dest, next);
|
|
141
|
+
return 'updated';
|
|
135
142
|
}
|
|
136
|
-
fs.
|
|
143
|
+
fs.writeFileSync(dest, next);
|
|
137
144
|
return 'written';
|
|
138
145
|
}
|
|
139
146
|
|
|
@@ -469,17 +476,18 @@ async function promptOpenSpecSetup() {
|
|
|
469
476
|
|
|
470
477
|
/**
|
|
471
478
|
* Resolve the planning engine for `forge init`, offering OpenSpec setup when needed.
|
|
472
|
-
* @param {{ cwd: string, openspec: boolean | null }} opts
|
|
479
|
+
* @param {{ cwd: string, openspec: boolean | null, agents?: string[] }} opts
|
|
473
480
|
* @returns {Promise<string>} 'openspec' | 'specs'
|
|
474
481
|
*/
|
|
475
482
|
async function resolveInitPlanEngine(opts) {
|
|
476
483
|
const configured = hasOpenSpecConfig(opts.cwd);
|
|
484
|
+
const tools = opts.agents;
|
|
477
485
|
|
|
478
486
|
if (opts.openspec === false) return 'specs';
|
|
479
487
|
|
|
480
488
|
if (opts.openspec === true) {
|
|
481
489
|
if (!configured && process.stdin.isTTY) {
|
|
482
|
-
const setup = setupOpenSpec(opts.cwd);
|
|
490
|
+
const setup = setupOpenSpec(opts.cwd, { tools });
|
|
483
491
|
for (const s of setup.steps) {
|
|
484
492
|
process.stdout.write(` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`);
|
|
485
493
|
}
|
|
@@ -505,7 +513,7 @@ async function resolveInitPlanEngine(opts) {
|
|
|
505
513
|
const accepted = await promptOpenSpecSetup();
|
|
506
514
|
if (!accepted) return 'specs';
|
|
507
515
|
|
|
508
|
-
const setup = setupOpenSpec(opts.cwd);
|
|
516
|
+
const setup = setupOpenSpec(opts.cwd, { tools });
|
|
509
517
|
for (const s of setup.steps) {
|
|
510
518
|
process.stdout.write(` [${s.ok ? 'ok' : 'FAIL'}] ${s.step}${s.detail ? ` — ${s.detail}` : ''}\n`);
|
|
511
519
|
}
|
|
@@ -564,6 +572,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
564
572
|
const planEngine = await resolveInitPlanEngine({
|
|
565
573
|
cwd: opts.cwd,
|
|
566
574
|
openspec: opts.openspec,
|
|
575
|
+
agents: selected,
|
|
567
576
|
});
|
|
568
577
|
|
|
569
578
|
let adr = opts.adr;
|
package/src/init.test.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { parseArgs, initProject, rememberedAgents } from './init.mjs';
|
|
6
|
+
import { parseArgs, initProject, rememberedAgents, resolveTemplatesRoot } from './init.mjs';
|
|
7
7
|
import { installSkillsToAgents } from './install.mjs';
|
|
8
8
|
import { saveUserConfig } from './config.mjs';
|
|
9
9
|
|
|
@@ -35,6 +35,37 @@ test('rememberedAgents unions install config, installed skills, and project wiri
|
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test('re-running init refreshes stale managed rule files in place', () => {
|
|
39
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'forgekit-refresh-'));
|
|
40
|
+
try {
|
|
41
|
+
initProject(['claude'], { cwd, adr: false, planEngine: null });
|
|
42
|
+
const rule = path.join(cwd, '.claude', 'rules', 'forge.md');
|
|
43
|
+
// Simulate an older install with a stale reference.
|
|
44
|
+
fs.writeFileSync(rule, 'Full workflow: forgekit `docs/forge.md`\n', 'utf8');
|
|
45
|
+
|
|
46
|
+
const report = initProject(['claude'], { cwd, adr: false, planEngine: null });
|
|
47
|
+
const updated = fs.readFileSync(rule, 'utf8');
|
|
48
|
+
assert.ok(!updated.includes('forgekit `docs/forge.md`'), 'stale ref replaced');
|
|
49
|
+
assert.ok(updated.includes('~/.claude/skills/forge/docs/forge.md'), 'points to global skill doc');
|
|
50
|
+
assert.ok(
|
|
51
|
+
report.files.some((f) => f.file.includes('forge.md') && f.status === 'updated'),
|
|
52
|
+
'reports the refresh as updated',
|
|
53
|
+
);
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('thin-rule templates are engine-neutral (no hardcoded OpenSpec-only flow)', () => {
|
|
60
|
+
const root = resolveTemplatesRoot();
|
|
61
|
+
for (const rel of ['claude/rules/forge.md', 'cursor/rules/forge.mdc', 'codex/rules/forge.md']) {
|
|
62
|
+
const body = fs.readFileSync(path.join(root, ...rel.split('/')), 'utf8');
|
|
63
|
+
assert.ok(!/Forge = OpenSpec/.test(body), `${rel} still says "Forge = OpenSpec"`);
|
|
64
|
+
assert.ok(body.includes('forge change new'), `${rel} missing built-in specs command`);
|
|
65
|
+
assert.ok(body.includes('/opsx:propose'), `${rel} missing OpenSpec command`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
38
69
|
test('initProject wires templated envs and marks the rest skill-only', () => {
|
|
39
70
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'forgekit-init-'));
|
|
40
71
|
try {
|
package/src/plan-engine.mjs
CHANGED
|
@@ -204,12 +204,28 @@ export function checkOpenSpecCliQuick(runCommand) {
|
|
|
204
204
|
return { ok: false, version: null };
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Map forgekit environment ids → OpenSpec tool ids (mostly identity).
|
|
209
|
+
* @type {Record<string, string>}
|
|
210
|
+
*/
|
|
211
|
+
const OPENSPEC_TOOL_ID = { copilot: 'github-copilot' };
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {string[]} agentIds forgekit environment ids
|
|
215
|
+
* @returns {string[]} OpenSpec tool ids
|
|
216
|
+
*/
|
|
217
|
+
export function toOpenSpecToolIds(agentIds) {
|
|
218
|
+
return agentIds.map((id) => OPENSPEC_TOOL_ID[id] ?? id);
|
|
219
|
+
}
|
|
220
|
+
|
|
207
221
|
/**
|
|
208
222
|
* Install the OpenSpec CLI (if missing) and run `openspec init` in the project.
|
|
209
|
-
* Interactive: inherits stdio so `openspec init` can prompt
|
|
223
|
+
* Interactive: inherits stdio so `openspec init` can prompt — unless `tools`
|
|
224
|
+
* is given, in which case those tools are configured non-interactively
|
|
225
|
+
* (`openspec init --tools <ids>`), skipping OpenSpec's own tool picker.
|
|
210
226
|
*
|
|
211
227
|
* @param {string} cwd
|
|
212
|
-
* @param {{ runCommand?: Function, interactive?: boolean }} [opts]
|
|
228
|
+
* @param {{ runCommand?: Function, interactive?: boolean, tools?: string[] }} [opts]
|
|
213
229
|
* @returns {{ ok: boolean, steps: { step: string, ok: boolean, detail?: string }[] }}
|
|
214
230
|
*/
|
|
215
231
|
export function setupOpenSpec(cwd, opts = {}) {
|
|
@@ -249,10 +265,12 @@ export function setupOpenSpec(cwd, opts = {}) {
|
|
|
249
265
|
return { ok: true, steps };
|
|
250
266
|
}
|
|
251
267
|
|
|
252
|
-
const
|
|
268
|
+
const tools = opts.tools?.length ? toOpenSpecToolIds(opts.tools) : null;
|
|
269
|
+
const initArgs = tools ? ['init', '--tools', tools.join(',')] : ['init'];
|
|
270
|
+
const init = runInherit('openspec', initArgs);
|
|
253
271
|
const initOk = init.status === 0;
|
|
254
272
|
steps.push({
|
|
255
|
-
step:
|
|
273
|
+
step: `openspec ${initArgs.join(' ')}`,
|
|
256
274
|
ok: initOk,
|
|
257
275
|
detail: initOk ? undefined : String(init.error ?? `exit ${init.status}`),
|
|
258
276
|
});
|
package/src/plan-engine.test.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
saveUserPlanEngine,
|
|
13
13
|
scaffoldSpecs,
|
|
14
14
|
setupOpenSpec,
|
|
15
|
+
toOpenSpecToolIds,
|
|
15
16
|
writeProjectPlanConfig,
|
|
16
17
|
} from './plan-engine.mjs';
|
|
17
18
|
import { loadProjectConfig, loadUserConfig, saveUserConfig } from './adr.mjs';
|
|
@@ -148,6 +149,43 @@ test('setupOpenSpec runs install + init via injected runner', () => {
|
|
|
148
149
|
}
|
|
149
150
|
});
|
|
150
151
|
|
|
152
|
+
test('toOpenSpecToolIds maps copilot → github-copilot, else identity', () => {
|
|
153
|
+
assert.deepEqual(
|
|
154
|
+
toOpenSpecToolIds(['claude', 'cursor', 'codex', 'opencode', 'copilot', 'gemini', 'windsurf']),
|
|
155
|
+
['claude', 'cursor', 'codex', 'opencode', 'github-copilot', 'gemini', 'windsurf'],
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('setupOpenSpec passes selected tools to `openspec init --tools`', () => {
|
|
160
|
+
const cwd = tmpdir('forgekit-openspec-tools-');
|
|
161
|
+
try {
|
|
162
|
+
/** @type {string[]} */
|
|
163
|
+
const calls = [];
|
|
164
|
+
const runCommand = (cmd, args) => {
|
|
165
|
+
const line = [cmd, ...args].join(' ');
|
|
166
|
+
calls.push(line);
|
|
167
|
+
if (line === 'openspec --version') return { status: 0, stdout: '1.0.0' };
|
|
168
|
+
if (line.startsWith('openspec init')) {
|
|
169
|
+
fs.mkdirSync(path.join(cwd, 'openspec'), { recursive: true });
|
|
170
|
+
fs.writeFileSync(path.join(cwd, 'openspec', 'config.yaml'), 'x: 1\n', 'utf8');
|
|
171
|
+
return { status: 0, stdout: '' };
|
|
172
|
+
}
|
|
173
|
+
return { status: 1, stdout: '' };
|
|
174
|
+
};
|
|
175
|
+
const result = setupOpenSpec(cwd, {
|
|
176
|
+
runCommand,
|
|
177
|
+
tools: ['claude', 'cursor', 'codex', 'copilot'],
|
|
178
|
+
});
|
|
179
|
+
assert.equal(result.ok, true);
|
|
180
|
+
assert.ok(
|
|
181
|
+
calls.includes('openspec init --tools claude,cursor,codex,github-copilot'),
|
|
182
|
+
`expected --tools call, got: ${calls.join(' | ')}`,
|
|
183
|
+
);
|
|
184
|
+
} finally {
|
|
185
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
151
189
|
test('setupOpenSpec reports failure when install fails', () => {
|
|
152
190
|
const cwd = tmpdir('forgekit-openspec-fail-');
|
|
153
191
|
try {
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
# Forge (thin rule)
|
|
2
|
-
|
|
3
|
-
Full workflow: `~/.claude/skills/forge/docs/forge.md` · Skill: user-installed **Forge** (`~/.claude/skills/forge/SKILL.md` after `forge install`).
|
|
4
|
-
|
|
5
|
-
**Default:** triage before implementation. Substantial work → **Forge**
|
|
6
|
-
(brainstorm →
|
|
7
|
-
|
|
8
|
-
**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
# Forge (thin rule)
|
|
2
|
+
|
|
3
|
+
Full workflow: `~/.claude/skills/forge/docs/forge.md` · Skill: user-installed **Forge** (`~/.claude/skills/forge/SKILL.md` after `forge install`).
|
|
4
|
+
|
|
5
|
+
**Default:** triage before implementation. Substantial work → **Forge**
|
|
6
|
+
(brainstorm → plan → subagent TDD implement → review).
|
|
7
|
+
|
|
8
|
+
**Planning engine:** this project's engine is recorded in `.forge/config.json` (`plan.engine`). After brainstorm, create the change spec directly — do not ask for a plan mode:
|
|
9
|
+
- OpenSpec → `/opsx:propose`
|
|
10
|
+
- built-in specs → `forge change new <slug>`
|
|
11
|
+
|
|
12
|
+
Work too small to spec should skip Forge (`/forge:skip` or direct execution).
|
|
13
|
+
|
|
14
|
+
**Skip Forge** only when work is trivial OR user sends **`/forge:skip`**.
|
|
15
|
+
|
|
16
|
+
Scratch sessions: `.forge/sessions/` (14-day retention). Active pointer: `.forge/active.json`.
|
|
17
|
+
|
|
18
|
+
CLI: `forge new`, `forge status`, `forge prefs`, `forge models`, `forge phase`, `forge doctor`.
|
|
19
|
+
|
|
20
|
+
Do not edit vendor planning-engine skills — Forge orchestrates them. Workflow skills are bundled under the Forge skill's `skills/` folder.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# Forge (thin rule)
|
|
2
|
-
|
|
3
|
-
Full workflow: `~/.codex/skills/forge/docs/forge.md` · Skill: user-installed **Forge** (`~/.codex/skills/forge/SKILL.md` after `forge install`).
|
|
4
|
-
|
|
5
|
-
**Default:** triage before implementation. Substantial work → **Forge**
|
|
6
|
-
(brainstorm →
|
|
7
|
-
|
|
8
|
-
**
|
|
9
|
-
|
|
10
|
-
Scratch: `.forge/` · CLI: `forge new|status|prefs|models|phase|doctor`
|
|
1
|
+
# Forge (thin rule)
|
|
2
|
+
|
|
3
|
+
Full workflow: `~/.codex/skills/forge/docs/forge.md` · Skill: user-installed **Forge** (`~/.codex/skills/forge/SKILL.md` after `forge install`).
|
|
4
|
+
|
|
5
|
+
**Default:** triage before implementation. Substantial work → **Forge**
|
|
6
|
+
(brainstorm → plan → subagent TDD implement → review).
|
|
7
|
+
|
|
8
|
+
**Planning engine:** recorded in `.forge/config.json` (`plan.engine`). After brainstorm, create the change directly — OpenSpec → `/opsx:propose`, built-in specs → `forge change new <slug>`. Skip with `/forge:skip` or when work is trivial.
|
|
9
|
+
|
|
10
|
+
Scratch: `.forge/` · CLI: `forge new|status|prefs|models|phase|doctor`
|
|
11
|
+
|
|
12
|
+
Do not edit vendor planning-engine skills — Forge orchestrates them.
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Forge triage and workflow (thin rule)
|
|
3
|
-
alwaysApply: true
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Forge (thin rule)
|
|
7
|
-
|
|
8
|
-
Full workflow: `~/.cursor/skills/forge/docs/forge.md` · Skill: user-installed **Forge** (`~/.cursor/skills/forge/SKILL.md` after `forge install`).
|
|
9
|
-
|
|
10
|
-
**Default:** triage before implementation. Substantial work → **Forge**
|
|
11
|
-
(brainstorm →
|
|
12
|
-
|
|
13
|
-
**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
---
|
|
2
|
+
description: Forge triage and workflow (thin rule)
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Forge (thin rule)
|
|
7
|
+
|
|
8
|
+
Full workflow: `~/.cursor/skills/forge/docs/forge.md` · Skill: user-installed **Forge** (`~/.cursor/skills/forge/SKILL.md` after `forge install`).
|
|
9
|
+
|
|
10
|
+
**Default:** triage before implementation. Substantial work → **Forge**
|
|
11
|
+
(brainstorm → plan → subagent TDD implement → review).
|
|
12
|
+
|
|
13
|
+
**Planning engine:** this project's engine is recorded in `.forge/config.json` (`plan.engine`). After brainstorm, create the change spec directly — do not ask for a plan mode:
|
|
14
|
+
- OpenSpec → `/opsx:propose`
|
|
15
|
+
- built-in specs → `forge change new <slug>`
|
|
16
|
+
|
|
17
|
+
Work too small to spec should skip Forge (`/forge:skip` or direct execution).
|
|
18
|
+
|
|
19
|
+
**Skip Forge** only when work is trivial OR user sends **`/forge:skip`**.
|
|
20
|
+
|
|
21
|
+
Scratch sessions: `.forge/sessions/` (14-day retention). Active pointer: `.forge/active.json`.
|
|
22
|
+
|
|
23
|
+
CLI: `forge new`, `forge status`, `forge prefs`, `forge models`, `forge phase`, `forge doctor`.
|
|
24
|
+
|
|
25
|
+
Do not edit vendor planning-engine skills — Forge orchestrates them. Workflow skills are bundled under the Forge skill's `skills/` folder.
|