@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izkac/forgekit",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
5
5
  "type": "module",
6
6
  "bin": {
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 Overwrite existing template files
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 }} opts
133
+ * @param {{ force?: boolean }} _opts
130
134
  */
131
- function copyFile(src, dest, opts) {
135
+ function copyFile(src, dest, _opts) {
132
136
  fs.mkdirSync(path.dirname(dest), { recursive: true });
133
- if (fs.existsSync(dest) && !opts.force) {
134
- return 'skipped';
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.copyFileSync(src, dest);
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 {
@@ -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 init = runInherit('openspec', ['init']);
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: 'openspec init',
273
+ step: `openspec ${initArgs.join(' ')}`,
256
274
  ok: initOk,
257
275
  detail: initOk ? undefined : String(init.error ?? `exit ${init.status}`),
258
276
  });
@@ -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 {
@@ -2,7 +2,7 @@
2
2
  name: /forge
3
3
  description: Forge — start or resume disciplined development (brainstorm → plan → build → review)
4
4
  category: Workflow
5
- tags: [workflow, forge, openspec]
5
+ tags: [workflow, forge, planning]
6
6
  ---
7
7
 
8
8
  Run the **Forge** workflow for substantial work.
@@ -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 → OpenSpec → subagent TDD implement → review).
7
-
8
- **Forge = OpenSpec:** after brainstorm, proceed directly to `/opsx:propose` — do not ask for a plan mode. Work too small for OpenSpec should skip Forge (`/forge:skip` or direct execution).
9
-
10
- **Skip Forge** only when work is trivial OR user sends **`/forge:skip`**.
11
-
12
- Scratch sessions: `.forge/sessions/` (14-day retention). Active pointer: `.forge/active.json`.
13
-
14
- CLI: `forge new`, `forge status`, `forge prefs`, `forge models`, `forge phase`, `forge doctor`.
15
-
16
- Do not edit vendor OpenSpec skills — Forge orchestrates them. Workflow skills are bundled under the Forge skill’s `skills/` folder.
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 → OpenSpec → subagent TDD implement → review).
7
-
8
- **Forge = OpenSpec:** after brainstorm, proceed directly to OpenSpec propose. Skip with `/forge:skip` or when work is trivial.
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 → OpenSpec → subagent TDD implement → review).
12
-
13
- **Forge = OpenSpec:** after brainstorm, proceed directly to `/opsx:propose` — do not ask for a plan mode. Work too small for OpenSpec should skip Forge (`/forge:skip` or direct execution).
14
-
15
- **Skip Forge** only when work is trivial OR user sends **`/forge:skip`**.
16
-
17
- Scratch sessions: `.forge/sessions/` (14-day retention). Active pointer: `.forge/active.json`.
18
-
19
- CLI: `forge new`, `forge status`, `forge prefs`, `forge models`, `forge phase`, `forge doctor`.
20
-
21
- Do not edit vendor OpenSpec skills — Forge orchestrates them. Workflow skills are bundled under the Forge skill’s `skills/` folder.
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.