@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 +1 -1
- package/src/init.mjs +65 -16
- package/src/init.test.mjs +53 -0
- package/src/install.mjs +3 -0
package/package.json
CHANGED
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
|
-
--
|
|
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):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
446
|
+
const remembered = rememberedAgents(cwd);
|
|
413
447
|
return checkbox({
|
|
414
448
|
message: 'Init Forge project wiring for which environments?',
|
|
415
|
-
choices:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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:
|
|
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 ? [
|
|
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
|
-
|
|
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
|