@skilly-hand/skilly-hand 0.18.0 → 0.20.0

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.
@@ -3,7 +3,6 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createRequire } from "node:module";
5
5
  import { fileURLToPath } from "node:url";
6
- import { checkbox as inquirerCheckbox, confirm as inquirerConfirm, select as inquirerSelect } from "@inquirer/prompts";
7
6
  import { loadAllSkills } from "../../catalog/src/index.js";
8
7
  import {
9
8
  DEFAULT_AGENTS,
@@ -14,6 +13,7 @@ import {
14
13
  } from "../../core/src/index.js";
15
14
  import { createTerminalRenderer } from "../../core/src/terminal.js";
16
15
  import { detectProject } from "../../detectors/src/index.js";
16
+ import { confirmWithInk, launchInkApp } from "./ink-ui.js";
17
17
 
18
18
  const require = createRequire(import.meta.url);
19
19
  const { version } = require("../../../package.json");
@@ -41,6 +41,7 @@ export function parseArgs(argv) {
41
41
  yes: false,
42
42
  verbose: false,
43
43
  json: false,
44
+ classic: false,
44
45
  agents: [],
45
46
  include: [],
46
47
  exclude: []
@@ -65,6 +66,7 @@ export function parseArgs(argv) {
65
66
  else if (token === "--yes" || token === "-y") flags.yes = true;
66
67
  else if (token === "--verbose" || token === "-v") flags.verbose = true;
67
68
  else if (token === "--json") flags.json = true;
69
+ else if (token === "--classic") flags.classic = true;
68
70
  else if (token === "--agent" || token === "-a") flags.agents.push(takeFlagValue(token));
69
71
  else if (token === "--cwd") flags.cwd = takeFlagValue(token);
70
72
  else if (token === "--include") flags.include.push(takeFlagValue(token));
@@ -89,6 +91,7 @@ function buildHelpText(renderer, appVersion) {
89
91
  const flags = renderer.section("Flags", renderer.list([
90
92
  "--dry-run Show install plan without writing files",
91
93
  "--json Emit stable JSON output for automation",
94
+ "--classic Force plain text command mode (skip full-screen TUI)",
92
95
  "--yes, -y Skip install/uninstall confirmations",
93
96
  "--verbose, -v Reserved for future debug detail",
94
97
  "--agent, -a <name> standard|codex|claude|cursor|gemini|copilot|antigravity|windsurf|trae (repeatable)",
@@ -114,7 +117,7 @@ function buildHelpText(renderer, appVersion) {
114
117
  ]);
115
118
  }
116
119
 
117
- function printInstallResult(renderer, appVersion, result, flags) {
120
+ function buildInstallResultBlock(renderer, appVersion, result, flags) {
118
121
  const mode = flags.dryRun ? "dry-run" : "apply";
119
122
  const preflight = renderer.section(
120
123
  "Install Preflight",
@@ -168,10 +171,14 @@ function printInstallResult(renderer, appVersion, result, flags) {
168
171
  "Adjust `--include` and `--exclude` tags to tune skill selection."
169
172
  ]);
170
173
 
171
- renderer.write(renderer.joinBlocks([renderer.banner(appVersion), preflight, detections, skills, status, nextSteps]));
174
+ return renderer.joinBlocks([renderer.banner(appVersion), preflight, detections, skills, status, nextSteps]);
172
175
  }
173
176
 
174
- function printDetectResult(renderer, cwd, detections) {
177
+ function printInstallResult(renderer, appVersion, result, flags) {
178
+ renderer.write(buildInstallResultBlock(renderer, appVersion, result, flags));
179
+ }
180
+
181
+ function buildDetectResultBlock(renderer, cwd, detections) {
175
182
  const summary = renderer.section(
176
183
  "Detection Summary",
177
184
  renderer.kv([
@@ -187,10 +194,14 @@ function printDetectResult(renderer, cwd, detections) {
187
194
  : renderer.status("warn", "No technology signals were detected.", "Only core skills will be selected.")
188
195
  );
189
196
 
190
- renderer.write(renderer.joinBlocks([summary, findings]));
197
+ return renderer.joinBlocks([summary, findings]);
191
198
  }
192
199
 
193
- function printListResult(renderer, skills) {
200
+ function printDetectResult(renderer, cwd, detections) {
201
+ renderer.write(buildDetectResultBlock(renderer, cwd, detections));
202
+ }
203
+
204
+ function buildListResultBlock(renderer, skills) {
194
205
  const summary = renderer.section(
195
206
  "Catalog Summary",
196
207
  renderer.kv([["Skills available", String(skills.length)]])
@@ -214,10 +225,14 @@ function printListResult(renderer, skills) {
214
225
  )
215
226
  );
216
227
 
217
- renderer.write(renderer.joinBlocks([summary, table]));
228
+ return renderer.joinBlocks([summary, table]);
218
229
  }
219
230
 
220
- function printDoctorResult(renderer, result) {
231
+ function printListResult(renderer, skills) {
232
+ renderer.write(buildListResultBlock(renderer, skills));
233
+ }
234
+
235
+ function buildDoctorResultBlock(renderer, result) {
221
236
  const badge = renderer.healthBadge(result.installed);
222
237
 
223
238
  const summary = renderer.section(
@@ -260,29 +275,32 @@ function printDoctorResult(renderer, result) {
260
275
  )
261
276
  );
262
277
 
263
- renderer.write(renderer.joinBlocks([badge, summary, lock, issues, probes]));
278
+ return renderer.joinBlocks([badge, summary, lock, issues, probes]);
264
279
  }
265
280
 
266
- function printUninstallResult(renderer, result) {
281
+ function printDoctorResult(renderer, result) {
282
+ renderer.write(buildDoctorResultBlock(renderer, result));
283
+ }
284
+
285
+ function buildUninstallResultBlock(renderer, result) {
267
286
  if (result.removed) {
268
- renderer.write(
269
- renderer.joinBlocks([
270
- renderer.status("success", "skilly-hand installation removed."),
271
- renderer.nextSteps([
272
- "Run `npx skilly-hand install` if you want to reinstall managed files.",
273
- "Run `npx skilly-hand doctor` to confirm the project state."
274
- ])
287
+ return renderer.joinBlocks([
288
+ renderer.status("success", "skilly-hand installation removed."),
289
+ renderer.nextSteps([
290
+ "Run `npx skilly-hand install` if you want to reinstall managed files.",
291
+ "Run `npx skilly-hand doctor` to confirm the project state."
275
292
  ])
276
- );
277
- return;
293
+ ]);
278
294
  }
279
295
 
280
- renderer.write(
281
- renderer.joinBlocks([
282
- renderer.status("warn", "Nothing to uninstall.", result.reason),
283
- renderer.nextSteps(["Run `npx skilly-hand install` to create a managed installation first."])
284
- ])
285
- );
296
+ return renderer.joinBlocks([
297
+ renderer.status("warn", "Nothing to uninstall.", result.reason),
298
+ renderer.nextSteps(["Run `npx skilly-hand install` to create a managed installation first."])
299
+ ]);
300
+ }
301
+
302
+ function printUninstallResult(renderer, result) {
303
+ renderer.write(buildUninstallResultBlock(renderer, result));
286
304
  }
287
305
 
288
306
  export function buildErrorHint(message) {
@@ -295,14 +313,6 @@ export function buildErrorHint(message) {
295
313
  return "Retry with `--verbose` for expanded context if needed.";
296
314
  }
297
315
 
298
- export function createPromptAdapter({ selectImpl, checkboxImpl, confirmImpl } = {}) {
299
- return {
300
- select: selectImpl || inquirerSelect,
301
- checkbox: checkboxImpl || inquirerCheckbox,
302
- confirm: confirmImpl || inquirerConfirm
303
- };
304
- }
305
-
306
316
  function createServices(overrides = {}) {
307
317
  return {
308
318
  loadAllSkills,
@@ -316,16 +326,13 @@ function createServices(overrides = {}) {
316
326
  };
317
327
  }
318
328
 
319
- function isInteractiveLauncherMode({ command, flags, stdout }) {
320
- return !command && !flags.json && Boolean(stdout?.isTTY);
329
+ function isInteractiveLauncherMode({ command, flags, stdout, stdin }) {
330
+ return !command && !flags.json && !flags.classic && Boolean(stdout?.isTTY) && Boolean(stdin?.isTTY);
321
331
  }
322
332
 
323
- async function runInteractiveInstall({
333
+ async function getInteractiveInstallContext({
324
334
  cwd,
325
- renderer,
326
- prompt,
327
- services,
328
- appVersion
335
+ services
329
336
  }) {
330
337
  const [catalog, detections] = await Promise.all([
331
338
  services.loadAllSkills(),
@@ -338,137 +345,90 @@ async function runInteractiveInstall({
338
345
  .map((skill) => skill.id)
339
346
  );
340
347
 
341
- const selectedSkillIds = await prompt.checkbox({
342
- message: "Select skills to install",
343
- choices: portableCatalog.map((skill) => ({
344
- value: skill.id,
345
- name: `${skill.id} - ${skill.title}`,
348
+ return {
349
+ skills: portableCatalog.map((skill) => ({
350
+ ...skill,
346
351
  checked: preselected.has(skill.id)
347
- }))
348
- });
349
-
350
- const selectedAgents = await prompt.checkbox({
351
- message: "Select AI assistants to configure",
352
- choices: services.defaultAgents.map((agent) => ({
352
+ })),
353
+ agents: services.defaultAgents.map((agent) => ({
353
354
  value: agent,
354
- name: agent,
355
355
  checked: agent === "standard"
356
356
  }))
357
- });
358
-
359
- const preview = await services.installProject({
360
- cwd,
361
- agents: selectedAgents,
362
- dryRun: true,
363
- selectedSkillIds
364
- });
365
-
366
- printInstallResult(renderer, appVersion, preview, {
367
- dryRun: true,
368
- include: [],
369
- exclude: []
370
- });
371
-
372
- const shouldApply = await prompt.confirm({
373
- message: "Apply installation changes now?",
374
- default: true
375
- });
376
-
377
- if (!shouldApply) {
378
- renderer.write(renderer.status("info", "Installation cancelled.", "No files were written."));
379
- return;
380
- }
381
-
382
- const applied = await services.installProject({
383
- cwd,
384
- agents: selectedAgents,
385
- dryRun: false,
386
- selectedSkillIds
387
- });
388
-
389
- printInstallResult(renderer, appVersion, applied, {
390
- dryRun: false,
391
- include: [],
392
- exclude: []
393
- });
357
+ };
394
358
  }
395
359
 
396
360
  async function runInteractiveSession({
397
361
  cwd,
398
362
  renderer,
399
- prompt,
400
363
  services,
401
- appVersion
364
+ appVersion,
365
+ interactiveUi
402
366
  }) {
403
- renderer.write(renderer.banner(appVersion));
404
-
405
- while (true) {
406
- const selection = await prompt.select({
407
- message: "Select a command",
408
- choices: [
409
- { value: "install", name: "Install" },
410
- { value: "detect", name: "Detect" },
411
- { value: "list", name: "List" },
412
- { value: "doctor", name: "Doctor" },
413
- { value: "uninstall", name: "Uninstall" },
414
- { value: "exit", name: "Exit" }
415
- ]
416
- });
417
-
418
- if (selection === "exit") {
419
- renderer.write(renderer.status("info", "Exited skilly-hand interactive mode."));
420
- return;
421
- }
422
-
423
- if (selection === "install") {
424
- await runInteractiveInstall({ cwd, renderer, prompt, services, appVersion });
425
- continue;
426
- }
427
-
428
- if (selection === "detect") {
429
- const detections = await services.detectProject(cwd);
430
- printDetectResult(renderer, cwd, detections);
431
- continue;
432
- }
433
-
434
- if (selection === "list") {
435
- const skills = await services.loadAllSkills();
436
- printListResult(renderer, skills);
437
- continue;
438
- }
439
-
440
- if (selection === "doctor") {
441
- const result = await services.runDoctor(cwd);
442
- printDoctorResult(renderer, result);
443
- continue;
444
- }
445
-
446
- if (selection === "uninstall") {
447
- const confirmed = await prompt.confirm({
448
- message: "Remove the skilly-hand installation from this project?",
449
- default: false
450
- });
451
-
452
- if (!confirmed) {
453
- renderer.write(renderer.status("info", "Uninstall cancelled."));
454
- continue;
367
+ await interactiveUi.launch({
368
+ appVersion,
369
+ actions: {
370
+ async runCommand(command) {
371
+ if (command === "detect") {
372
+ const detections = await services.detectProject(cwd);
373
+ return buildDetectResultBlock(renderer, cwd, detections);
374
+ }
375
+ if (command === "list") {
376
+ const skills = await services.loadAllSkills();
377
+ return buildListResultBlock(renderer, skills);
378
+ }
379
+ if (command === "doctor") {
380
+ const result = await services.runDoctor(cwd);
381
+ return buildDoctorResultBlock(renderer, result);
382
+ }
383
+ if (command === "uninstall") {
384
+ const result = await services.uninstallProject(cwd);
385
+ return buildUninstallResultBlock(renderer, result);
386
+ }
387
+ return renderer.status("warn", `Unknown command: ${command}`);
388
+ },
389
+ async prepareInstall() {
390
+ return getInteractiveInstallContext({ cwd, services });
391
+ },
392
+ async previewInstall({ selectedSkillIds, selectedAgents }) {
393
+ const preview = await services.installProject({
394
+ cwd,
395
+ agents: selectedAgents,
396
+ dryRun: true,
397
+ selectedSkillIds
398
+ });
399
+ return buildInstallResultBlock(renderer, appVersion, preview, {
400
+ dryRun: true,
401
+ include: [],
402
+ exclude: []
403
+ });
404
+ },
405
+ async applyInstall({ selectedSkillIds, selectedAgents }) {
406
+ const applied = await services.installProject({
407
+ cwd,
408
+ agents: selectedAgents,
409
+ dryRun: false,
410
+ selectedSkillIds
411
+ });
412
+ return buildInstallResultBlock(renderer, appVersion, applied, {
413
+ dryRun: false,
414
+ include: [],
415
+ exclude: []
416
+ });
455
417
  }
456
-
457
- const result = await services.uninstallProject(cwd);
458
- printUninstallResult(renderer, result);
459
418
  }
460
- }
419
+ });
461
420
  }
462
421
 
463
422
  async function runCommand({
464
423
  command,
465
424
  flags,
466
425
  cwd,
426
+ stdin,
467
427
  stdout,
468
428
  renderer,
469
- prompt,
470
429
  services,
471
- appVersion
430
+ appVersion,
431
+ interactiveUi
472
432
  }) {
473
433
  if (command === "detect") {
474
434
  const detections = await services.detectProject(cwd);
@@ -513,10 +473,10 @@ async function runCommand({
513
473
  }
514
474
 
515
475
  if (command === "uninstall") {
516
- if (!flags.json && !flags.yes && Boolean(stdout?.isTTY)) {
517
- const confirmed = await prompt.confirm({
476
+ if (!flags.json && !flags.yes && Boolean(stdout?.isTTY) && Boolean(stdin?.isTTY)) {
477
+ const confirmed = await interactiveUi.confirm({
518
478
  message: "Remove the skilly-hand installation from this project?",
519
- default: false
479
+ defaultValue: false
520
480
  });
521
481
  if (!confirmed) {
522
482
  renderer.write(renderer.status("info", "Uninstall cancelled."));
@@ -537,10 +497,10 @@ async function runCommand({
537
497
  }
538
498
 
539
499
  if (command === "install") {
540
- if (!flags.dryRun && !flags.json && !flags.yes && Boolean(stdout?.isTTY)) {
541
- const confirmed = await prompt.confirm({
500
+ if (!flags.dryRun && !flags.json && !flags.yes && Boolean(stdout?.isTTY) && Boolean(stdin?.isTTY)) {
501
+ const confirmed = await interactiveUi.confirm({
542
502
  message: "Apply installation changes to this project?",
543
- default: true
503
+ defaultValue: true
544
504
  });
545
505
  if (!confirmed) {
546
506
  renderer.write(renderer.status("info", "Installation cancelled.", "No files were written."));
@@ -575,12 +535,16 @@ async function runCommand({
575
535
 
576
536
  export async function runCli({
577
537
  argv = process.argv.slice(2),
538
+ stdin = process.stdin,
578
539
  stdout = process.stdout,
579
540
  stderr = process.stderr,
580
541
  env = process.env,
581
542
  platform = process.platform,
582
- prompt = createPromptAdapter(),
583
543
  services: providedServices = {},
544
+ interactiveUi = {
545
+ launch: ({ appVersion, actions }) => launchInkApp({ appVersion, actions }),
546
+ confirm: ({ message, defaultValue }) => confirmWithInk({ message, defaultValue })
547
+ },
584
548
  appVersion = version,
585
549
  cwdResolver = process.cwd
586
550
  } = {}) {
@@ -611,14 +575,14 @@ export async function runCli({
611
575
 
612
576
  const cwd = path.resolve(flags.cwd || cwdResolver());
613
577
 
614
- if (isInteractiveLauncherMode({ command, flags, stdout })) {
578
+ if (isInteractiveLauncherMode({ command, flags, stdout, stdin })) {
615
579
  try {
616
580
  await runInteractiveSession({
617
581
  cwd,
618
582
  renderer,
619
- prompt,
620
583
  services,
621
- appVersion
584
+ appVersion,
585
+ interactiveUi
622
586
  });
623
587
  return;
624
588
  } catch (error) {
@@ -635,11 +599,12 @@ export async function runCli({
635
599
  command: effectiveCommand,
636
600
  flags,
637
601
  cwd,
602
+ stdin,
638
603
  stdout,
639
604
  renderer,
640
- prompt,
641
605
  services,
642
- appVersion
606
+ appVersion,
607
+ interactiveUi
643
608
  });
644
609
  }
645
610