@izkac/forgekit 0.1.2 → 0.1.3
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 +1 -1
- package/package.json +5 -2
- package/src/init.mjs +26 -55
- package/src/install.mjs +31 -67
- package/src/menu-select.mjs +0 -49
- package/src/menu-select.test.mjs +0 -52
package/bin/forge.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izkac/forgekit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
"claude",
|
|
43
43
|
"codex"
|
|
44
44
|
],
|
|
45
|
-
"license": "MIT"
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@inquirer/prompts": "^8.5.2"
|
|
48
|
+
}
|
|
46
49
|
}
|
package/src/init.mjs
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
import readline from 'node:readline/promises';
|
|
15
14
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
15
|
import { spawnSync } from 'node:child_process';
|
|
17
|
-
import {
|
|
16
|
+
import { checkbox, confirm, input } from '@inquirer/prompts';
|
|
18
17
|
import {
|
|
19
18
|
DEFAULT_ADR_DIR,
|
|
20
19
|
disableProjectAdr,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
normalizeAdrDir,
|
|
23
22
|
scaffoldAdr,
|
|
24
23
|
} from './adr.mjs';
|
|
25
|
-
import { parseMenuSelection } from './menu-select.mjs';
|
|
26
24
|
import {
|
|
27
25
|
DEFAULT_SPECS_DIR,
|
|
28
26
|
hasOpenSpecConfig,
|
|
@@ -392,26 +390,15 @@ export function initProject(selected, opts) {
|
|
|
392
390
|
}
|
|
393
391
|
|
|
394
392
|
async function promptAgents() {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
process.stdout.write(` 3) Codex CLI\n`);
|
|
405
|
-
process.stdout.write(` ${allNum}) All\n`);
|
|
406
|
-
for (;;) {
|
|
407
|
-
const answer = await rl.question(`Choice(s) [1-${allNum}]: `);
|
|
408
|
-
const parsed = parseMenuSelection(answer, map, allIds, allNum);
|
|
409
|
-
if (parsed.ok) return parsed.ids;
|
|
410
|
-
process.stdout.write(`${parsed.error}\n`);
|
|
411
|
-
}
|
|
412
|
-
} finally {
|
|
413
|
-
rl.close();
|
|
414
|
-
}
|
|
393
|
+
return checkbox({
|
|
394
|
+
message: 'Init Forge project wiring for which environments?',
|
|
395
|
+
choices: [
|
|
396
|
+
{ value: 'cursor', name: 'Cursor' },
|
|
397
|
+
{ value: 'claude', name: 'Claude Code' },
|
|
398
|
+
{ value: 'codex', name: 'Codex CLI' },
|
|
399
|
+
],
|
|
400
|
+
required: true,
|
|
401
|
+
});
|
|
415
402
|
}
|
|
416
403
|
|
|
417
404
|
/**
|
|
@@ -419,19 +406,11 @@ async function promptAgents() {
|
|
|
419
406
|
* @returns {Promise<boolean>} true = user accepted OpenSpec setup
|
|
420
407
|
*/
|
|
421
408
|
async function promptOpenSpecSetup() {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
)
|
|
428
|
-
)
|
|
429
|
-
.trim()
|
|
430
|
-
.toLowerCase();
|
|
431
|
-
return !(yn === 'n' || yn === 'no');
|
|
432
|
-
} finally {
|
|
433
|
-
rl.close();
|
|
434
|
-
}
|
|
409
|
+
return confirm({
|
|
410
|
+
message:
|
|
411
|
+
'OpenSpec is not set up in this project. Install and set it up now? (No = built-in specs engine)',
|
|
412
|
+
default: true,
|
|
413
|
+
});
|
|
435
414
|
}
|
|
436
415
|
|
|
437
416
|
/**
|
|
@@ -490,25 +469,16 @@ async function resolveInitPlanEngine(opts) {
|
|
|
490
469
|
* @returns {Promise<{ enabled: boolean, dir: string }>}
|
|
491
470
|
*/
|
|
492
471
|
async function promptAdrForInit(defaultDir = DEFAULT_ADR_DIR) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
enabled = yn === 'y' || yn === 'yes';
|
|
504
|
-
if (!enabled) return { enabled: false, dir: defaultDir };
|
|
505
|
-
const dirAnswer = (
|
|
506
|
-
await rl.question(`ADR directory inside the repo [${defaultDir}]: `)
|
|
507
|
-
).trim();
|
|
508
|
-
return { enabled: true, dir: normalizeAdrDir(dirAnswer || defaultDir) };
|
|
509
|
-
} finally {
|
|
510
|
-
rl.close();
|
|
511
|
-
}
|
|
472
|
+
const enabled = await confirm({
|
|
473
|
+
message: 'Use Architecture Decision Records (ADRs) in this project?',
|
|
474
|
+
default: false,
|
|
475
|
+
});
|
|
476
|
+
if (!enabled) return { enabled: false, dir: defaultDir };
|
|
477
|
+
const dir = await input({
|
|
478
|
+
message: 'ADR directory inside the repo',
|
|
479
|
+
default: defaultDir,
|
|
480
|
+
});
|
|
481
|
+
return { enabled: true, dir: normalizeAdrDir(dir.trim() || defaultDir) };
|
|
512
482
|
}
|
|
513
483
|
|
|
514
484
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -566,6 +536,7 @@ if (isDirect) {
|
|
|
566
536
|
main()
|
|
567
537
|
.then((code) => process.exit(code))
|
|
568
538
|
.catch((err) => {
|
|
539
|
+
if (err?.name === 'ExitPromptError') process.exit(130);
|
|
569
540
|
process.stderr.write(`${err.message || err}\n`);
|
|
570
541
|
process.exit(1);
|
|
571
542
|
});
|
package/src/install.mjs
CHANGED
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
-
import readline from 'node:readline/promises';
|
|
20
19
|
import { pathToFileURL } from 'node:url';
|
|
21
|
-
import {
|
|
20
|
+
import { checkbox, confirm, input, select } from '@inquirer/prompts';
|
|
22
21
|
import {
|
|
23
22
|
ADR_SKILLS,
|
|
24
23
|
DEFAULT_ADR_DIR,
|
|
@@ -28,7 +27,6 @@ import {
|
|
|
28
27
|
scaffoldAdr,
|
|
29
28
|
disableProjectAdr,
|
|
30
29
|
} from './adr.mjs';
|
|
31
|
-
import { parseMenuSelection } from './menu-select.mjs';
|
|
32
30
|
import { saveUserPlanEngine } from './plan-engine.mjs';
|
|
33
31
|
import { hashDirectory, packageVersion, resolveAsset } from './paths.mjs';
|
|
34
32
|
|
|
@@ -415,42 +413,27 @@ export function listInstallStatus(opts = {}) {
|
|
|
415
413
|
}
|
|
416
414
|
|
|
417
415
|
/**
|
|
418
|
-
* @param {string}
|
|
419
|
-
* @param {
|
|
420
|
-
* @param {string[]} allIds
|
|
416
|
+
* @param {string} message
|
|
417
|
+
* @param {string[]} ids
|
|
421
418
|
* @returns {Promise<string[]>}
|
|
422
419
|
*/
|
|
423
|
-
async function promptMulti(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
process.stdout.write(` ${num}) ${label}\n`);
|
|
433
|
-
}
|
|
434
|
-
process.stdout.write(` ${allNum}) All\n`);
|
|
435
|
-
for (;;) {
|
|
436
|
-
const answer = await rl.question(`Choice(s) [1-${allNum}]: `);
|
|
437
|
-
const parsed = parseMenuSelection(answer, map, allIds, allNum);
|
|
438
|
-
if (parsed.ok) return parsed.ids;
|
|
439
|
-
process.stdout.write(`${parsed.error}\n`);
|
|
440
|
-
}
|
|
441
|
-
} finally {
|
|
442
|
-
rl.close();
|
|
443
|
-
}
|
|
420
|
+
async function promptMulti(message, ids) {
|
|
421
|
+
return checkbox({
|
|
422
|
+
message,
|
|
423
|
+
choices: ids.map((id) => ({
|
|
424
|
+
value: id,
|
|
425
|
+
name: SKILLS[id]?.label ?? AGENTS[id]?.label ?? id,
|
|
426
|
+
})),
|
|
427
|
+
required: true,
|
|
428
|
+
});
|
|
444
429
|
}
|
|
445
430
|
|
|
446
431
|
async function promptSkills() {
|
|
447
|
-
|
|
448
|
-
return promptMulti('Install which skills?', map, SKILL_IDS);
|
|
432
|
+
return promptMulti('Install which skills?', SKILL_IDS);
|
|
449
433
|
}
|
|
450
434
|
|
|
451
435
|
async function promptAgents() {
|
|
452
|
-
|
|
453
|
-
return promptMulti('Install for which environments?', map, AGENT_IDS);
|
|
436
|
+
return promptMulti('Install for which environments?', AGENT_IDS);
|
|
454
437
|
}
|
|
455
438
|
|
|
456
439
|
/**
|
|
@@ -458,35 +441,21 @@ async function promptAgents() {
|
|
|
458
441
|
* @returns {Promise<string>}
|
|
459
442
|
*/
|
|
460
443
|
export async function promptAdrDir(defaultDir = DEFAULT_ADR_DIR) {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
return normalizeAdrDir(dirAnswer || defaultDir);
|
|
467
|
-
} finally {
|
|
468
|
-
rl.close();
|
|
469
|
-
}
|
|
444
|
+
const dir = await input({
|
|
445
|
+
message: 'ADR directory inside each repo',
|
|
446
|
+
default: defaultDir,
|
|
447
|
+
});
|
|
448
|
+
return normalizeAdrDir(dir.trim() || defaultDir);
|
|
470
449
|
}
|
|
471
450
|
|
|
472
451
|
/**
|
|
473
452
|
* @returns {Promise<{ enabled: boolean, dir: string }>}
|
|
474
453
|
*/
|
|
475
454
|
export async function promptAdrOptions() {
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
await rl.question(
|
|
481
|
-
'Use Architecture Decision Records (ADRs) after OpenSpec archive? [y/N] ',
|
|
482
|
-
)
|
|
483
|
-
)
|
|
484
|
-
.trim()
|
|
485
|
-
.toLowerCase();
|
|
486
|
-
enabled = yn === 'y' || yn === 'yes';
|
|
487
|
-
} finally {
|
|
488
|
-
rl.close();
|
|
489
|
-
}
|
|
455
|
+
const enabled = await confirm({
|
|
456
|
+
message: 'Use Architecture Decision Records (ADRs) after OpenSpec archive?',
|
|
457
|
+
default: false,
|
|
458
|
+
});
|
|
490
459
|
if (!enabled) return { enabled: false, dir: DEFAULT_ADR_DIR };
|
|
491
460
|
const dir = await promptAdrDir(DEFAULT_ADR_DIR);
|
|
492
461
|
return { enabled: true, dir };
|
|
@@ -496,19 +465,13 @@ export async function promptAdrOptions() {
|
|
|
496
465
|
* @returns {Promise<boolean>} true = OpenSpec, false = built-in specs engine
|
|
497
466
|
*/
|
|
498
467
|
export async function promptOpenSpec() {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
.trim()
|
|
507
|
-
.toLowerCase();
|
|
508
|
-
return !(yn === 'n' || yn === 'no');
|
|
509
|
-
} finally {
|
|
510
|
-
rl.close();
|
|
511
|
-
}
|
|
468
|
+
return select({
|
|
469
|
+
message: 'Planning engine?',
|
|
470
|
+
choices: [
|
|
471
|
+
{ value: true, name: 'OpenSpec (vendor CLI)' },
|
|
472
|
+
{ value: false, name: 'Built-in specs engine' },
|
|
473
|
+
],
|
|
474
|
+
});
|
|
512
475
|
}
|
|
513
476
|
|
|
514
477
|
/**
|
|
@@ -763,6 +726,7 @@ if (isDirect) {
|
|
|
763
726
|
runInstall()
|
|
764
727
|
.then((code) => process.exit(code))
|
|
765
728
|
.catch((err) => {
|
|
729
|
+
if (err?.name === 'ExitPromptError') process.exit(130);
|
|
766
730
|
process.stderr.write(`${err.message || err}\n`);
|
|
767
731
|
process.exit(1);
|
|
768
732
|
});
|
package/src/menu-select.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared numbered-menu selection (one, many, or all).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parse a menu answer into selected ids.
|
|
7
|
-
* Accepts one number, several (comma/space), or the All option.
|
|
8
|
-
*
|
|
9
|
-
* @param {string} answer
|
|
10
|
-
* @param {Record<string, string>} map number → id
|
|
11
|
-
* @param {string[]} allIds
|
|
12
|
-
* @param {string} allNum
|
|
13
|
-
* @returns {{ ok: true, ids: string[] } | { ok: false, error: string }}
|
|
14
|
-
*/
|
|
15
|
-
export function parseMenuSelection(answer, map, allIds, allNum) {
|
|
16
|
-
const raw = String(answer ?? '').trim();
|
|
17
|
-
if (!raw) {
|
|
18
|
-
return {
|
|
19
|
-
ok: false,
|
|
20
|
-
error: `Enter one or more numbers (e.g. 1 or 1,3) or ${allNum} for all`,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
if (raw === allNum || /^all$/i.test(raw)) {
|
|
24
|
-
return { ok: true, ids: [...allIds] };
|
|
25
|
-
}
|
|
26
|
-
const tokens = raw.split(/[,\s]+/).filter(Boolean);
|
|
27
|
-
/** @type {string[]} */
|
|
28
|
-
const ids = [];
|
|
29
|
-
/** @type {string[]} */
|
|
30
|
-
const bad = [];
|
|
31
|
-
for (const t of tokens) {
|
|
32
|
-
if (t === allNum || /^all$/i.test(t)) {
|
|
33
|
-
return { ok: true, ids: [...allIds] };
|
|
34
|
-
}
|
|
35
|
-
const id = map[t];
|
|
36
|
-
if (!id) bad.push(t);
|
|
37
|
-
else if (!ids.includes(id)) ids.push(id);
|
|
38
|
-
}
|
|
39
|
-
if (bad.length) {
|
|
40
|
-
return {
|
|
41
|
-
ok: false,
|
|
42
|
-
error: `Unknown choice(s): ${bad.join(', ')}. Use listed numbers or ${allNum} for all`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (ids.length === 0) {
|
|
46
|
-
return { ok: false, error: 'Nothing selected' };
|
|
47
|
-
}
|
|
48
|
-
return { ok: true, ids };
|
|
49
|
-
}
|
package/src/menu-select.test.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { parseMenuSelection } from './menu-select.mjs';
|
|
4
|
-
|
|
5
|
-
const map = { 1: 'forge', 2: 'review', 3: 'adr', 4: 'git' };
|
|
6
|
-
const allIds = ['forge', 'review', 'adr', 'git'];
|
|
7
|
-
const allNum = '5';
|
|
8
|
-
|
|
9
|
-
test('parseMenuSelection: single choice', () => {
|
|
10
|
-
assert.deepEqual(parseMenuSelection('1', map, allIds, allNum), {
|
|
11
|
-
ok: true,
|
|
12
|
-
ids: ['forge'],
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('parseMenuSelection: multiple comma-separated', () => {
|
|
17
|
-
assert.deepEqual(parseMenuSelection('1,3', map, allIds, allNum), {
|
|
18
|
-
ok: true,
|
|
19
|
-
ids: ['forge', 'adr'],
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('parseMenuSelection: multiple space-separated', () => {
|
|
24
|
-
assert.deepEqual(parseMenuSelection('2 4', map, allIds, allNum), {
|
|
25
|
-
ok: true,
|
|
26
|
-
ids: ['review', 'git'],
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('parseMenuSelection: all via number or word', () => {
|
|
31
|
-
assert.deepEqual(parseMenuSelection('5', map, allIds, allNum), {
|
|
32
|
-
ok: true,
|
|
33
|
-
ids: [...allIds],
|
|
34
|
-
});
|
|
35
|
-
assert.deepEqual(parseMenuSelection('all', map, allIds, allNum), {
|
|
36
|
-
ok: true,
|
|
37
|
-
ids: [...allIds],
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('parseMenuSelection: empty and unknown rejected', () => {
|
|
42
|
-
assert.equal(parseMenuSelection('', map, allIds, allNum).ok, false);
|
|
43
|
-
assert.equal(parseMenuSelection('9', map, allIds, allNum).ok, false);
|
|
44
|
-
assert.equal(parseMenuSelection('1,9', map, allIds, allNum).ok, false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('parseMenuSelection: dedupes repeats', () => {
|
|
48
|
-
assert.deepEqual(parseMenuSelection('1,1,2', map, allIds, allNum), {
|
|
49
|
-
ok: true,
|
|
50
|
-
ids: ['forge', 'review'],
|
|
51
|
-
});
|
|
52
|
-
});
|