@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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Forge CLI — session orchestration, prefs, models, project init.
4
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izkac/forgekit",
3
- "version": "0.1.2",
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 { stdin as input, stdout as output } from 'node:process';
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
- const rl = readline.createInterface({ input, output });
396
- const map = { 1: 'cursor', 2: 'claude', 3: 'codex' };
397
- const allIds = ['cursor', 'claude', 'codex'];
398
- const allNum = '4';
399
- try {
400
- process.stdout.write(`Init Forge project wiring for which environments?\n`);
401
- process.stdout.write(`(pick one or more — e.g. 1 or 1,3 — or ${allNum} for all)\n`);
402
- process.stdout.write(` 1) Cursor\n`);
403
- process.stdout.write(` 2) Claude Code\n`);
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
- const rl = readline.createInterface({ input, output });
423
- try {
424
- const yn = (
425
- await rl.question(
426
- 'OpenSpec is not set up in this project. Install and set it up now? [Y/n] (n = built-in specs engine) ',
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 rl = readline.createInterface({ input, output });
494
- let enabled = false;
495
- try {
496
- const yn = (
497
- await rl.question(
498
- 'Use Architecture Decision Records (ADRs) in this project? [y/N] ',
499
- )
500
- )
501
- .trim()
502
- .toLowerCase();
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 { stdin as input, stdout as output } from 'node:process';
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} question
419
- * @param {Record<string, string>} map number → id
420
- * @param {string[]} allIds
416
+ * @param {string} message
417
+ * @param {string[]} ids
421
418
  * @returns {Promise<string[]>}
422
419
  */
423
- async function promptMulti(question, map, allIds) {
424
- const rl = readline.createInterface({ input, output });
425
- try {
426
- const entries = Object.entries(map);
427
- const allNum = String(entries.length + 1);
428
- process.stdout.write(`${question}\n`);
429
- process.stdout.write(`(pick one or more — e.g. 1 or 1,3 — or ${allNum} for all)\n`);
430
- for (const [num, id] of entries) {
431
- const label = SKILLS[id]?.label ?? AGENTS[id]?.label ?? id;
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
- const map = Object.fromEntries(SKILL_IDS.map((id, i) => [String(i + 1), id]));
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
- const map = Object.fromEntries(AGENT_IDS.map((id, i) => [String(i + 1), id]));
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 rl = readline.createInterface({ input, output });
462
- try {
463
- const dirAnswer = (
464
- await rl.question(`ADR directory inside each repo [${defaultDir}]: `)
465
- ).trim();
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 rl = readline.createInterface({ input, output });
477
- let enabled = false;
478
- try {
479
- const yn = (
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
- const rl = readline.createInterface({ input, output });
500
- try {
501
- const yn = (
502
- await rl.question(
503
- 'Plan with OpenSpec (vendor CLI)? [Y/n] (n = built-in specs engine) ',
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
  });
@@ -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
- }
@@ -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
- });