@izkac/forgekit 0.1.4 → 0.1.5

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.4",
3
+ "version": "0.1.5",
4
4
  "description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/init.mjs CHANGED
@@ -30,6 +30,11 @@ import {
30
30
  writeProjectPlanConfig,
31
31
  } from './plan-engine.mjs';
32
32
  import { resolveAsset } from './paths.mjs';
33
+ import { AGENT_IDS, AGENTS, installedManagedPairs } from './install.mjs';
34
+
35
+ // Environments with project-local command/rule/hook templates. Others are
36
+ // driven by the globally-installed skill alone (no per-project wiring).
37
+ const WIRED_AGENTS = Object.freeze(['cursor', 'claude', 'codex']);
33
38
 
34
39
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
40
 
@@ -61,6 +66,10 @@ export function parseArgs(argv) {
61
66
  else if (arg === '--cursor') opts.agents.push('cursor');
62
67
  else if (arg === '--claude' || arg === '--claude-code') opts.agents.push('claude');
63
68
  else if (arg === '--codex') opts.agents.push('codex');
69
+ else if (arg === '--copilot') opts.agents.push('copilot');
70
+ else if (arg === '--gemini') opts.agents.push('gemini');
71
+ else if (arg === '--windsurf') opts.agents.push('windsurf');
72
+ else if (arg === '--opencode') opts.agents.push('opencode');
64
73
  else if (arg === '--adr') opts.adr = true;
65
74
  else if (arg === '--no-adr') opts.adr = false;
66
75
  else if (arg === '--adr-dir') opts.adrDir = argv[++i];
@@ -81,7 +90,10 @@ Options:
81
90
  --cursor Cursor (.cursor/commands, rules, hooks)
82
91
  --claude Claude Code (.claude/commands, rules, hooks)
83
92
  --codex Codex CLI (.codex/rules)
84
- --all All of the above
93
+ --copilot/--gemini/--windsurf/--opencode
94
+ Offered in the picker for parity with \`forgekit install\`;
95
+ driven by the global skill (no per-project wiring yet)
96
+ --all Every offered environment
85
97
  --openspec Plan with OpenSpec (offer install + \`openspec init\` if missing)
86
98
  --no-openspec Plan with the built-in specs engine (${DEFAULT_SPECS_DIR}/changes/)
87
99
  --adr Enable ADRs (scaffold decisions.md + ADR dir + hooks)
@@ -95,10 +107,12 @@ Options:
95
107
  Requires the Forge skill already installed (\`forge install\`) for agents
96
108
  to load skill content. Init only adds project-local wiring.
97
109
 
98
- Interactive (TTY): when --openspec/--no-openspec omitted and OpenSpec is not
99
- already set up, offers to install + set it up (decline = built-in specs
100
- engine). When --adr/--no-adr omitted, asks whether to use ADRs and for the
101
- directory inside the repo.
110
+ Interactive (TTY): the environment picker matches \`forgekit install\` and is
111
+ pre-checked with what you installed there (saved in ~/.forgekit/config.json),
112
+ so you don't pick twice. When --openspec/--no-openspec omitted and OpenSpec is
113
+ not already set up, offers to install + set it up (decline = built-in specs
114
+ engine). When --adr/--no-adr omitted, asks whether to use ADRs (default Yes)
115
+ and for the directory inside the repo.
102
116
  `);
103
117
  }
104
118
 
@@ -343,6 +357,10 @@ export function initProject(selected, opts) {
343
357
  );
344
358
  }
345
359
 
360
+ // Selected environments without project-wiring templates: the globally
361
+ // installed skill is their interface — nothing to scaffold per project.
362
+ report.skillOnly = selected.filter((id) => !WIRED_AGENTS.includes(id));
363
+
346
364
  if (opts.planEngine === 'specs') {
347
365
  const scaffold = scaffoldSpecs(cwd, { force: opts.force });
348
366
  const config = writeProjectPlanConfig(cwd, {
@@ -407,16 +425,32 @@ function wiredAgents(cwd) {
407
425
  );
408
426
  }
409
427
 
428
+ /**
429
+ * Environments to pre-check: those chosen during `forgekit install`
430
+ * (saved in ~/.forgekit/config.json), plus what is already installed or wired.
431
+ * @param {string} cwd
432
+ * @param {string} [home]
433
+ * @returns {Set<string>}
434
+ */
435
+ export function rememberedAgents(cwd, home) {
436
+ const user = loadUserConfig(home);
437
+ return new Set([
438
+ ...(Array.isArray(user.agents) ? user.agents : []),
439
+ ...installedManagedPairs(home).map((p) => p.agent),
440
+ ...wiredAgents(cwd),
441
+ ]);
442
+ }
443
+
410
444
  /** @param {string} cwd */
411
445
  async function promptAgents(cwd) {
412
- const wired = wiredAgents(cwd);
446
+ const remembered = rememberedAgents(cwd);
413
447
  return checkbox({
414
448
  message: 'Init Forge project wiring for which environments?',
415
- choices: [
416
- { value: 'cursor', name: 'Cursor', checked: wired.has('cursor') },
417
- { value: 'claude', name: 'Claude Code', checked: wired.has('claude') },
418
- { value: 'codex', name: 'Codex CLI', checked: wired.has('codex') },
419
- ],
449
+ choices: AGENT_IDS.map((id) => ({
450
+ value: id,
451
+ name: AGENTS[id].label,
452
+ checked: remembered.has(id),
453
+ })),
420
454
  required: true,
421
455
  });
422
456
  }
@@ -486,12 +520,13 @@ async function resolveInitPlanEngine(opts) {
486
520
 
487
521
  /**
488
522
  * @param {string} [defaultDir]
523
+ * @param {boolean} [defaultEnabled]
489
524
  * @returns {Promise<{ enabled: boolean, dir: string }>}
490
525
  */
491
- async function promptAdrForInit(defaultDir = DEFAULT_ADR_DIR) {
526
+ async function promptAdrForInit(defaultDir = DEFAULT_ADR_DIR, defaultEnabled = true) {
492
527
  const enabled = await confirm({
493
528
  message: 'Use Architecture Decision Records (ADRs) in this project?',
494
- default: false,
529
+ default: defaultEnabled,
495
530
  });
496
531
  if (!enabled) return { enabled: false, dir: defaultDir };
497
532
  const dir = await input({
@@ -508,17 +543,24 @@ async function main(argv = process.argv.slice(2)) {
508
543
  return 0;
509
544
  }
510
545
 
511
- let selected = opts.all ? ['cursor', 'claude', 'codex'] : [...new Set(opts.agents)];
546
+ let selected = opts.all ? [...AGENT_IDS] : [...new Set(opts.agents)];
512
547
  if (selected.length === 0) {
513
548
  if (!process.stdin.isTTY) {
514
549
  process.stderr.write(
515
- 'No agents specified. Pass --cursor/--claude/--codex/--all, or run in a TTY.\n',
550
+ 'No agents specified. Pass --cursor/--claude/--codex/--copilot/--gemini/--windsurf/--opencode/--all, or run in a TTY.\n',
516
551
  );
517
552
  return 1;
518
553
  }
519
554
  selected = await promptAgents(opts.cwd);
520
555
  }
521
556
 
557
+ for (const id of selected) {
558
+ if (!AGENTS[id]) {
559
+ process.stderr.write(`Unknown environment: ${id}. Known: ${AGENT_IDS.join(', ')}\n`);
560
+ return 1;
561
+ }
562
+ }
563
+
522
564
  const planEngine = await resolveInitPlanEngine({
523
565
  cwd: opts.cwd,
524
566
  openspec: opts.openspec,
@@ -530,7 +572,8 @@ async function main(argv = process.argv.slice(2)) {
530
572
  const user = loadUserConfig();
531
573
  const defaultDir = user.adr?.dir ?? DEFAULT_ADR_DIR;
532
574
  if (process.stdin.isTTY) {
533
- const picked = await promptAdrForInit(defaultDir);
575
+ // Default Yes, unless the user globally opted out of ADRs.
576
+ const picked = await promptAdrForInit(defaultDir, user.adr?.enabled !== false);
534
577
  adr = picked.enabled;
535
578
  adrDir = picked.dir;
536
579
  } else if (user.adr?.enabled === true) {
@@ -543,6 +586,12 @@ async function main(argv = process.argv.slice(2)) {
543
586
 
544
587
  const report = initProject(selected, { ...opts, adr, adrDir, planEngine });
545
588
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
589
+ if (Array.isArray(report.skillOnly) && report.skillOnly.length) {
590
+ const labels = report.skillOnly.map((id) => AGENTS[id].label).join(', ');
591
+ process.stdout.write(
592
+ `\nNo project wiring for: ${labels} — they use the globally installed Forge skill directly (run \`forgekit install\` if not yet installed).\n`,
593
+ );
594
+ }
546
595
  process.stdout.write(
547
596
  `\nMerge hook snippets into settings if needed, ensure \`forge\` is on PATH, then open the project in your agent.\n`,
548
597
  );
@@ -0,0 +1,53 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { parseArgs, initProject, rememberedAgents } from './init.mjs';
7
+ import { installSkillsToAgents } from './install.mjs';
8
+ import { saveUserConfig } from './config.mjs';
9
+
10
+ test('init parseArgs accepts the expanded environment shorthands', () => {
11
+ const opts = parseArgs(['--cursor', '--copilot', '--gemini', '--windsurf', '--opencode']);
12
+ assert.deepEqual(opts.agents, ['cursor', 'copilot', 'gemini', 'windsurf', 'opencode']);
13
+ });
14
+
15
+ test('rememberedAgents unions install config, installed skills, and project wiring', () => {
16
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'forgekit-home-'));
17
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'forgekit-proj-'));
18
+ try {
19
+ // Chosen during `forgekit install` (saved to user config).
20
+ saveUserConfig({ agents: ['claude', 'gemini'] }, home);
21
+ // Actually installed skill for another env.
22
+ installSkillsToAgents(['forge'], ['copilot'], { home, force: true });
23
+ // Project already wired for cursor.
24
+ fs.mkdirSync(path.join(cwd, '.cursor', 'commands'), { recursive: true });
25
+
26
+ const remembered = rememberedAgents(cwd, home);
27
+ assert.ok(remembered.has('claude'), 'from install config');
28
+ assert.ok(remembered.has('gemini'), 'from install config');
29
+ assert.ok(remembered.has('copilot'), 'from installed skill dir');
30
+ assert.ok(remembered.has('cursor'), 'from project wiring marker');
31
+ assert.ok(!remembered.has('codex'));
32
+ } finally {
33
+ fs.rmSync(home, { recursive: true, force: true });
34
+ fs.rmSync(cwd, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ test('initProject wires templated envs and marks the rest skill-only', () => {
39
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'forgekit-init-'));
40
+ try {
41
+ const report = initProject(['cursor', 'copilot', 'gemini'], {
42
+ cwd,
43
+ force: true,
44
+ adr: false,
45
+ planEngine: null,
46
+ });
47
+ assert.ok(report.files.some((f) => f.file.includes('.cursor')));
48
+ assert.deepEqual(report.skillOnly, ['copilot', 'gemini']);
49
+ assert.ok(!fs.existsSync(path.join(cwd, '.copilot')));
50
+ } finally {
51
+ fs.rmSync(cwd, { recursive: true, force: true });
52
+ }
53
+ });
package/src/install.mjs CHANGED
@@ -730,6 +730,9 @@ export async function runInstall(argv = process.argv.slice(2)) {
730
730
 
731
731
  saveUserConfig({
732
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 } : {}),
733
736
  });
734
737
 
735
738
  const { results, removed } = prune