@skilly-hand/skilly-hand 0.4.0 → 0.5.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.
- package/CHANGELOG.md +18 -0
- package/README.md +10 -0
- package/package.json +4 -1
- package/packages/cli/src/bin.js +345 -85
- package/packages/core/src/index.js +42 -8
- package/packages/core/src/ui/brand.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,24 @@ All notable changes to this project are documented in this file.
|
|
|
16
16
|
### Removed
|
|
17
17
|
- _None._
|
|
18
18
|
|
|
19
|
+
## [0.5.0] - 2026-04-03
|
|
20
|
+
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.5.0)
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Interactive command launcher when running `npx skilly-hand` in a TTY, including install skill/agent selection flow.
|
|
24
|
+
- New `selectedSkillIds` install path for explicitly choosing portable skills.
|
|
25
|
+
- Comprehensive CLI interaction tests in `tests/interactive-cli.test.js`.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Help, docs, and install/uninstall confirmation messaging now reflect current behavior and naming (`skilly-hand` branding).
|
|
29
|
+
- CLI bin execution mode and command routing were refactored into testable `runCli`/service helpers.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Non-interactive invocation without a command now defaults to install output instead of opening prompts.
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
- _None._
|
|
36
|
+
|
|
19
37
|
## [0.4.0] - 2026-04-03
|
|
20
38
|
[View on npm](https://www.npmjs.com/package/@skilly-hand/skilly-hand/v/0.4.0)
|
|
21
39
|
|
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ npm install
|
|
|
37
37
|
npx skilly-hand
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
`npx skilly-hand` opens an interactive command launcher when running in a TTY.
|
|
41
|
+
|
|
40
42
|
---
|
|
41
43
|
|
|
42
44
|
## Commands
|
|
@@ -49,6 +51,14 @@ npx skilly-hand
|
|
|
49
51
|
| `npx skilly-hand doctor` | Diagnose installation and configuration issues |
|
|
50
52
|
| `npx skilly-hand uninstall` | Remove installed skills |
|
|
51
53
|
|
|
54
|
+
### Common Flags
|
|
55
|
+
|
|
56
|
+
| Flag | Description |
|
|
57
|
+
| ---- | ----------- |
|
|
58
|
+
| `--json` | Emit machine-readable output and disable interactive prompts |
|
|
59
|
+
| `--yes`, `-y` | Skip confirmation prompts for mutating commands (`install`, `uninstall`) |
|
|
60
|
+
| `--dry-run` | Preview install plan without writing files |
|
|
61
|
+
|
|
52
62
|
---
|
|
53
63
|
|
|
54
64
|
## Current Portable Catalog
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skilly-hand/skilly-hand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "CC-BY-NC-4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
"detect": "node ./packages/cli/src/bin.js detect",
|
|
41
41
|
"list": "node ./packages/cli/src/bin.js list",
|
|
42
42
|
"doctor": "node ./packages/cli/src/bin.js doctor"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@inquirer/prompts": "^7.10.1"
|
|
43
46
|
}
|
|
44
47
|
}
|
package/packages/cli/src/bin.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { checkbox as inquirerCheckbox, confirm as inquirerConfirm, select as inquirerSelect } from "@inquirer/prompts";
|
|
4
6
|
import { loadAllSkills } from "../../catalog/src/index.js";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_AGENTS,
|
|
9
|
+
installProject,
|
|
10
|
+
resolveSkillSelection,
|
|
11
|
+
runDoctor,
|
|
12
|
+
uninstallProject
|
|
13
|
+
} from "../../core/src/index.js";
|
|
6
14
|
import { createTerminalRenderer } from "../../core/src/terminal.js";
|
|
7
15
|
import { detectProject } from "../../detectors/src/index.js";
|
|
8
16
|
|
|
9
17
|
const require = createRequire(import.meta.url);
|
|
10
18
|
const { version } = require("../../../package.json");
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
function isExecutedDirectly(metaUrl, argv1) {
|
|
21
|
+
if (!argv1) return false;
|
|
22
|
+
return metaUrl === pathToFileURL(argv1).href;
|
|
23
|
+
}
|
|
13
24
|
|
|
14
|
-
function parseArgs(argv) {
|
|
25
|
+
export function parseArgs(argv) {
|
|
15
26
|
const args = [...argv];
|
|
16
27
|
const positional = [];
|
|
17
28
|
const flags = {
|
|
@@ -54,9 +65,10 @@ function parseArgs(argv) {
|
|
|
54
65
|
return { command: positional[0], flags };
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
function buildHelpText() {
|
|
68
|
+
function buildHelpText(renderer, appVersion) {
|
|
58
69
|
const usage = renderer.section("Usage", renderer.list([
|
|
59
|
-
"npx skilly-hand
|
|
70
|
+
"npx skilly-hand # interactive launcher when running in a TTY",
|
|
71
|
+
"npx skilly-hand install",
|
|
60
72
|
"npx skilly-hand detect",
|
|
61
73
|
"npx skilly-hand list",
|
|
62
74
|
"npx skilly-hand doctor",
|
|
@@ -66,7 +78,7 @@ function buildHelpText() {
|
|
|
66
78
|
const flags = renderer.section("Flags", renderer.list([
|
|
67
79
|
"--dry-run Show install plan without writing files",
|
|
68
80
|
"--json Emit stable JSON output for automation",
|
|
69
|
-
"--yes, -y
|
|
81
|
+
"--yes, -y Skip install/uninstall confirmations",
|
|
70
82
|
"--verbose, -v Reserved for future debug detail",
|
|
71
83
|
"--agent, -a <name> codex|claude|cursor|gemini|copilot (repeatable)",
|
|
72
84
|
"--cwd <path> Project root (defaults to current directory)",
|
|
@@ -76,21 +88,22 @@ function buildHelpText() {
|
|
|
76
88
|
], { bullet: "-" }));
|
|
77
89
|
|
|
78
90
|
const examples = renderer.section("Examples", renderer.list([
|
|
91
|
+
"npx skilly-hand",
|
|
79
92
|
"npx skilly-hand install --dry-run",
|
|
80
93
|
"npx skilly-hand detect --json",
|
|
81
94
|
"npx skilly-hand install --agent codex --agent claude",
|
|
82
|
-
"npx skilly-hand
|
|
95
|
+
"npx skilly-hand uninstall --yes"
|
|
83
96
|
], { bullet: "-" }));
|
|
84
97
|
|
|
85
98
|
return renderer.joinBlocks([
|
|
86
|
-
renderer.banner(
|
|
99
|
+
renderer.banner(appVersion),
|
|
87
100
|
usage,
|
|
88
101
|
flags,
|
|
89
102
|
examples
|
|
90
103
|
]);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
function printInstallResult(result, flags) {
|
|
106
|
+
function printInstallResult(renderer, appVersion, result, flags) {
|
|
94
107
|
const mode = flags.dryRun ? "dry-run" : "apply";
|
|
95
108
|
const preflight = renderer.section(
|
|
96
109
|
"Install Preflight",
|
|
@@ -135,19 +148,19 @@ function printInstallResult(result, flags) {
|
|
|
135
148
|
|
|
136
149
|
const nextSteps = result.applied
|
|
137
150
|
? renderer.nextSteps([
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
"Review generated AGENTS and assistant instruction files.",
|
|
152
|
+
"Run `npx skilly-hand doctor` to validate installation health.",
|
|
153
|
+
"Use `npx skilly-hand uninstall` to restore backed-up files if needed."
|
|
154
|
+
])
|
|
142
155
|
: renderer.nextSteps([
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
"Run `npx skilly-hand install` to apply this plan.",
|
|
157
|
+
"Adjust `--include` and `--exclude` tags to tune skill selection."
|
|
158
|
+
]);
|
|
146
159
|
|
|
147
|
-
renderer.write(renderer.joinBlocks([renderer.banner(
|
|
160
|
+
renderer.write(renderer.joinBlocks([renderer.banner(appVersion), preflight, detections, skills, status, nextSteps]));
|
|
148
161
|
}
|
|
149
162
|
|
|
150
|
-
function printDetectResult(cwd, detections) {
|
|
163
|
+
function printDetectResult(renderer, cwd, detections) {
|
|
151
164
|
const summary = renderer.section(
|
|
152
165
|
"Detection Summary",
|
|
153
166
|
renderer.kv([
|
|
@@ -166,7 +179,7 @@ function printDetectResult(cwd, detections) {
|
|
|
166
179
|
renderer.write(renderer.joinBlocks([summary, findings]));
|
|
167
180
|
}
|
|
168
181
|
|
|
169
|
-
function printListResult(skills) {
|
|
182
|
+
function printListResult(renderer, skills) {
|
|
170
183
|
const summary = renderer.section(
|
|
171
184
|
"Catalog Summary",
|
|
172
185
|
renderer.kv([["Skills available", String(skills.length)]])
|
|
@@ -193,7 +206,7 @@ function printListResult(skills) {
|
|
|
193
206
|
renderer.write(renderer.joinBlocks([summary, table]));
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
function printDoctorResult(result) {
|
|
209
|
+
function printDoctorResult(renderer, result) {
|
|
197
210
|
const badge = renderer.healthBadge(result.installed);
|
|
198
211
|
|
|
199
212
|
const summary = renderer.section(
|
|
@@ -239,7 +252,7 @@ function printDoctorResult(result) {
|
|
|
239
252
|
renderer.write(renderer.joinBlocks([badge, summary, lock, issues, probes]));
|
|
240
253
|
}
|
|
241
254
|
|
|
242
|
-
function printUninstallResult(result) {
|
|
255
|
+
function printUninstallResult(renderer, result) {
|
|
243
256
|
if (result.removed) {
|
|
244
257
|
renderer.write(
|
|
245
258
|
renderer.joinBlocks([
|
|
@@ -261,34 +274,193 @@ function printUninstallResult(result) {
|
|
|
261
274
|
);
|
|
262
275
|
}
|
|
263
276
|
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
export function buildErrorHint(message) {
|
|
278
|
+
if (message.startsWith("Unknown command:")) {
|
|
279
|
+
return "Run `npx skilly-hand --help` to see available commands.";
|
|
280
|
+
}
|
|
281
|
+
if (message.startsWith("Unknown flag:") || message.startsWith("Missing value")) {
|
|
282
|
+
return "Check command flags with `npx skilly-hand --help`.";
|
|
283
|
+
}
|
|
284
|
+
return "Retry with `--verbose` for expanded context if needed.";
|
|
285
|
+
}
|
|
266
286
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
export function createPromptAdapter({ selectImpl, checkboxImpl, confirmImpl } = {}) {
|
|
288
|
+
return {
|
|
289
|
+
select: selectImpl || inquirerSelect,
|
|
290
|
+
checkbox: checkboxImpl || inquirerCheckbox,
|
|
291
|
+
confirm: confirmImpl || inquirerConfirm
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createServices(overrides = {}) {
|
|
296
|
+
return {
|
|
297
|
+
loadAllSkills,
|
|
298
|
+
installProject,
|
|
299
|
+
resolveSkillSelection,
|
|
300
|
+
runDoctor,
|
|
301
|
+
uninstallProject,
|
|
302
|
+
detectProject,
|
|
303
|
+
defaultAgents: DEFAULT_AGENTS,
|
|
304
|
+
...overrides
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isInteractiveLauncherMode({ command, flags, stdout }) {
|
|
309
|
+
return !command && !flags.json && Boolean(stdout?.isTTY);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function runInteractiveInstall({
|
|
313
|
+
cwd,
|
|
314
|
+
renderer,
|
|
315
|
+
prompt,
|
|
316
|
+
services,
|
|
317
|
+
appVersion
|
|
318
|
+
}) {
|
|
319
|
+
const [catalog, detections] = await Promise.all([
|
|
320
|
+
services.loadAllSkills(),
|
|
321
|
+
services.detectProject(cwd)
|
|
322
|
+
]);
|
|
323
|
+
const portableCatalog = catalog.filter((skill) => skill.portable).sort((a, b) => a.id.localeCompare(b.id));
|
|
324
|
+
const preselected = new Set(
|
|
325
|
+
services
|
|
326
|
+
.resolveSkillSelection({ catalog, detections, includeTags: [], excludeTags: [] })
|
|
327
|
+
.map((skill) => skill.id)
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const selectedSkillIds = await prompt.checkbox({
|
|
331
|
+
message: "Select skills to install",
|
|
332
|
+
choices: portableCatalog.map((skill) => ({
|
|
333
|
+
value: skill.id,
|
|
334
|
+
name: `${skill.id} - ${skill.title}`,
|
|
335
|
+
checked: preselected.has(skill.id)
|
|
336
|
+
}))
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const selectedAgents = await prompt.checkbox({
|
|
340
|
+
message: "Select AI assistants to configure",
|
|
341
|
+
choices: services.defaultAgents.map((agent) => ({
|
|
342
|
+
value: agent,
|
|
343
|
+
name: agent,
|
|
344
|
+
checked: true
|
|
345
|
+
}))
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const preview = await services.installProject({
|
|
349
|
+
cwd,
|
|
350
|
+
agents: selectedAgents,
|
|
351
|
+
dryRun: true,
|
|
352
|
+
selectedSkillIds
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
printInstallResult(renderer, appVersion, preview, {
|
|
356
|
+
dryRun: true,
|
|
357
|
+
include: [],
|
|
358
|
+
exclude: []
|
|
359
|
+
});
|
|
282
360
|
|
|
283
|
-
|
|
361
|
+
const shouldApply = await prompt.confirm({
|
|
362
|
+
message: "Apply installation changes now?",
|
|
363
|
+
default: true
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!shouldApply) {
|
|
367
|
+
renderer.write(renderer.status("info", "Installation cancelled.", "No files were written."));
|
|
284
368
|
return;
|
|
285
369
|
}
|
|
286
370
|
|
|
287
|
-
const
|
|
288
|
-
|
|
371
|
+
const applied = await services.installProject({
|
|
372
|
+
cwd,
|
|
373
|
+
agents: selectedAgents,
|
|
374
|
+
dryRun: false,
|
|
375
|
+
selectedSkillIds
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
printInstallResult(renderer, appVersion, applied, {
|
|
379
|
+
dryRun: false,
|
|
380
|
+
include: [],
|
|
381
|
+
exclude: []
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function runInteractiveSession({
|
|
386
|
+
cwd,
|
|
387
|
+
renderer,
|
|
388
|
+
prompt,
|
|
389
|
+
services,
|
|
390
|
+
appVersion
|
|
391
|
+
}) {
|
|
392
|
+
renderer.write(renderer.banner(appVersion));
|
|
393
|
+
|
|
394
|
+
while (true) {
|
|
395
|
+
const selection = await prompt.select({
|
|
396
|
+
message: "Select a command",
|
|
397
|
+
choices: [
|
|
398
|
+
{ value: "install", name: "Install" },
|
|
399
|
+
{ value: "detect", name: "Detect" },
|
|
400
|
+
{ value: "list", name: "List" },
|
|
401
|
+
{ value: "doctor", name: "Doctor" },
|
|
402
|
+
{ value: "uninstall", name: "Uninstall" },
|
|
403
|
+
{ value: "exit", name: "Exit" }
|
|
404
|
+
]
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (selection === "exit") {
|
|
408
|
+
renderer.write(renderer.status("info", "Exited skilly-hand interactive mode."));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (selection === "install") {
|
|
413
|
+
await runInteractiveInstall({ cwd, renderer, prompt, services, appVersion });
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (selection === "detect") {
|
|
418
|
+
const detections = await services.detectProject(cwd);
|
|
419
|
+
printDetectResult(renderer, cwd, detections);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (selection === "list") {
|
|
424
|
+
const skills = await services.loadAllSkills();
|
|
425
|
+
printListResult(renderer, skills);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (selection === "doctor") {
|
|
430
|
+
const result = await services.runDoctor(cwd);
|
|
431
|
+
printDoctorResult(renderer, result);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
289
434
|
|
|
290
|
-
|
|
291
|
-
|
|
435
|
+
if (selection === "uninstall") {
|
|
436
|
+
const confirmed = await prompt.confirm({
|
|
437
|
+
message: "Remove the skilly-hand installation from this project?",
|
|
438
|
+
default: false
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (!confirmed) {
|
|
442
|
+
renderer.write(renderer.status("info", "Uninstall cancelled."));
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await services.uninstallProject(cwd);
|
|
447
|
+
printUninstallResult(renderer, result);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function runCommand({
|
|
453
|
+
command,
|
|
454
|
+
flags,
|
|
455
|
+
cwd,
|
|
456
|
+
stdout,
|
|
457
|
+
renderer,
|
|
458
|
+
prompt,
|
|
459
|
+
services,
|
|
460
|
+
appVersion
|
|
461
|
+
}) {
|
|
462
|
+
if (command === "detect") {
|
|
463
|
+
const detections = await services.detectProject(cwd);
|
|
292
464
|
if (flags.json) {
|
|
293
465
|
renderer.writeJson({
|
|
294
466
|
command: "detect",
|
|
@@ -298,12 +470,12 @@ async function main() {
|
|
|
298
470
|
});
|
|
299
471
|
return;
|
|
300
472
|
}
|
|
301
|
-
printDetectResult(cwd, detections);
|
|
473
|
+
printDetectResult(renderer, cwd, detections);
|
|
302
474
|
return;
|
|
303
475
|
}
|
|
304
476
|
|
|
305
|
-
if (
|
|
306
|
-
const skills = await loadAllSkills();
|
|
477
|
+
if (command === "list") {
|
|
478
|
+
const skills = await services.loadAllSkills();
|
|
307
479
|
if (flags.json) {
|
|
308
480
|
renderer.writeJson({
|
|
309
481
|
command: "list",
|
|
@@ -312,12 +484,12 @@ async function main() {
|
|
|
312
484
|
});
|
|
313
485
|
return;
|
|
314
486
|
}
|
|
315
|
-
printListResult(skills);
|
|
487
|
+
printListResult(renderer, skills);
|
|
316
488
|
return;
|
|
317
489
|
}
|
|
318
490
|
|
|
319
|
-
if (
|
|
320
|
-
const result = await runDoctor(cwd);
|
|
491
|
+
if (command === "doctor") {
|
|
492
|
+
const result = await services.runDoctor(cwd);
|
|
321
493
|
if (flags.json) {
|
|
322
494
|
renderer.writeJson({
|
|
323
495
|
command: "doctor",
|
|
@@ -325,12 +497,23 @@ async function main() {
|
|
|
325
497
|
});
|
|
326
498
|
return;
|
|
327
499
|
}
|
|
328
|
-
printDoctorResult(result);
|
|
500
|
+
printDoctorResult(renderer, result);
|
|
329
501
|
return;
|
|
330
502
|
}
|
|
331
503
|
|
|
332
|
-
if (
|
|
333
|
-
|
|
504
|
+
if (command === "uninstall") {
|
|
505
|
+
if (!flags.json && !flags.yes && Boolean(stdout?.isTTY)) {
|
|
506
|
+
const confirmed = await prompt.confirm({
|
|
507
|
+
message: "Remove the skilly-hand installation from this project?",
|
|
508
|
+
default: false
|
|
509
|
+
});
|
|
510
|
+
if (!confirmed) {
|
|
511
|
+
renderer.write(renderer.status("info", "Uninstall cancelled."));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const result = await services.uninstallProject(cwd);
|
|
334
517
|
if (flags.json) {
|
|
335
518
|
renderer.writeJson({
|
|
336
519
|
command: "uninstall",
|
|
@@ -338,12 +521,23 @@ async function main() {
|
|
|
338
521
|
});
|
|
339
522
|
return;
|
|
340
523
|
}
|
|
341
|
-
printUninstallResult(result);
|
|
524
|
+
printUninstallResult(renderer, result);
|
|
342
525
|
return;
|
|
343
526
|
}
|
|
344
527
|
|
|
345
|
-
if (
|
|
346
|
-
|
|
528
|
+
if (command === "install") {
|
|
529
|
+
if (!flags.dryRun && !flags.json && !flags.yes && Boolean(stdout?.isTTY)) {
|
|
530
|
+
const confirmed = await prompt.confirm({
|
|
531
|
+
message: "Apply installation changes to this project?",
|
|
532
|
+
default: true
|
|
533
|
+
});
|
|
534
|
+
if (!confirmed) {
|
|
535
|
+
renderer.write(renderer.status("info", "Installation cancelled.", "No files were written."));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const result = await services.installProject({
|
|
347
541
|
cwd,
|
|
348
542
|
agents: flags.agents,
|
|
349
543
|
dryRun: flags.dryRun,
|
|
@@ -361,43 +555,109 @@ async function main() {
|
|
|
361
555
|
return;
|
|
362
556
|
}
|
|
363
557
|
|
|
364
|
-
printInstallResult(result, flags);
|
|
558
|
+
printInstallResult(renderer, appVersion, result, flags);
|
|
365
559
|
return;
|
|
366
560
|
}
|
|
367
561
|
|
|
368
|
-
throw new Error(`Unknown command: ${
|
|
562
|
+
throw new Error(`Unknown command: ${command}`);
|
|
369
563
|
}
|
|
370
564
|
|
|
371
|
-
|
|
565
|
+
export async function runCli({
|
|
566
|
+
argv = process.argv.slice(2),
|
|
567
|
+
stdout = process.stdout,
|
|
568
|
+
stderr = process.stderr,
|
|
569
|
+
env = process.env,
|
|
570
|
+
platform = process.platform,
|
|
571
|
+
prompt = createPromptAdapter(),
|
|
572
|
+
services: providedServices = {},
|
|
573
|
+
appVersion = version,
|
|
574
|
+
cwdResolver = process.cwd
|
|
575
|
+
} = {}) {
|
|
576
|
+
const renderer = createTerminalRenderer({ stdout, stderr, env, platform });
|
|
577
|
+
const services = createServices(providedServices);
|
|
578
|
+
const { command, flags } = parseArgs(argv);
|
|
372
579
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
580
|
+
if (flags.help) {
|
|
581
|
+
if (flags.json) {
|
|
582
|
+
renderer.writeJson({
|
|
583
|
+
command: command || "install",
|
|
584
|
+
help: true,
|
|
585
|
+
usage: [
|
|
586
|
+
"npx skilly-hand",
|
|
587
|
+
"npx skilly-hand install",
|
|
588
|
+
"npx skilly-hand detect",
|
|
589
|
+
"npx skilly-hand list",
|
|
590
|
+
"npx skilly-hand doctor",
|
|
591
|
+
"npx skilly-hand uninstall"
|
|
592
|
+
]
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
380
596
|
|
|
381
|
-
|
|
382
|
-
renderer.writeErrorJson({
|
|
383
|
-
ok: false,
|
|
384
|
-
error: {
|
|
385
|
-
what: "skilly-hand command failed",
|
|
386
|
-
why: error.message,
|
|
387
|
-
hint
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
process.exitCode = 1;
|
|
597
|
+
renderer.write(buildHelpText(renderer, appVersion));
|
|
391
598
|
return;
|
|
392
599
|
}
|
|
393
600
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
601
|
+
const cwd = path.resolve(flags.cwd || cwdResolver());
|
|
602
|
+
|
|
603
|
+
if (isInteractiveLauncherMode({ command, flags, stdout })) {
|
|
604
|
+
try {
|
|
605
|
+
await runInteractiveSession({
|
|
606
|
+
cwd,
|
|
607
|
+
renderer,
|
|
608
|
+
prompt,
|
|
609
|
+
services,
|
|
610
|
+
appVersion
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error?.name === "ExitPromptError") {
|
|
615
|
+
renderer.write(renderer.status("info", "Interactive session cancelled."));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const effectiveCommand = command || "install";
|
|
623
|
+
await runCommand({
|
|
624
|
+
command: effectiveCommand,
|
|
625
|
+
flags,
|
|
626
|
+
cwd,
|
|
627
|
+
stdout,
|
|
628
|
+
renderer,
|
|
629
|
+
prompt,
|
|
630
|
+
services,
|
|
631
|
+
appVersion
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (isExecutedDirectly(import.meta.url, process.argv[1])) {
|
|
636
|
+
const jsonRequested = process.argv.includes("--json");
|
|
637
|
+
const renderer = createTerminalRenderer();
|
|
638
|
+
|
|
639
|
+
runCli().catch((error) => {
|
|
640
|
+
if (jsonRequested) {
|
|
641
|
+
renderer.writeErrorJson({
|
|
642
|
+
ok: false,
|
|
643
|
+
error: {
|
|
644
|
+
what: "skilly-hand command failed",
|
|
645
|
+
why: error.message,
|
|
646
|
+
hint: buildErrorHint(error.message)
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
process.exitCode = 1;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
renderer.writeError(
|
|
654
|
+
renderer.error({
|
|
655
|
+
what: "skilly-hand command failed",
|
|
656
|
+
why: error.message,
|
|
657
|
+
hint: buildErrorHint(error.message),
|
|
658
|
+
exitCode: 1
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
process.exitCode = 1;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { copySkillTo, loadAllSkills, renderAgentsMarkdown, verifyCatalogFiles } from "../../catalog/src/index.js";
|
|
4
4
|
import { detectProject, inspectProjectFiles } from "../../detectors/src/index.js";
|
|
5
5
|
|
|
6
|
-
const DEFAULT_AGENTS = ["codex", "claude", "cursor", "gemini", "copilot"];
|
|
6
|
+
export const DEFAULT_AGENTS = ["codex", "claude", "cursor", "gemini", "copilot"];
|
|
7
7
|
const MANAGED_MARKER = "<!-- Managed by skilly-hand.";
|
|
8
8
|
|
|
9
9
|
function uniq(values) {
|
|
@@ -44,6 +44,37 @@ function parseTags(input) {
|
|
|
44
44
|
return uniq((input || []).flatMap((value) => String(value).split(",")).map((value) => value.trim()).filter(Boolean));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function parseSkillIds(input) {
|
|
48
|
+
return uniq((input || []).flatMap((value) => String(value).split(",")).map((value) => value.trim()).filter(Boolean));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveSkillSelectionByIds({ catalog, selectedSkillIds = [] }) {
|
|
52
|
+
const ids = parseSkillIds(selectedSkillIds);
|
|
53
|
+
const portableById = new Map(
|
|
54
|
+
catalog
|
|
55
|
+
.filter((skill) => skill.portable)
|
|
56
|
+
.map((skill) => [skill.id, skill])
|
|
57
|
+
);
|
|
58
|
+
const allById = new Map(catalog.map((skill) => [skill.id, skill]));
|
|
59
|
+
|
|
60
|
+
const invalid = [];
|
|
61
|
+
for (const id of ids) {
|
|
62
|
+
if (!allById.has(id)) {
|
|
63
|
+
invalid.push(`Unknown skill id: ${id}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!portableById.has(id)) {
|
|
67
|
+
invalid.push(`Skill is not portable: ${id}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (invalid.length > 0) {
|
|
72
|
+
throw new Error(invalid.join("; "));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return ids.map((id) => portableById.get(id)).sort((a, b) => a.id.localeCompare(b.id));
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
export function resolveSkillSelection({ catalog, detections, includeTags = [], excludeTags = [] }) {
|
|
48
79
|
const coreSkills = catalog.filter((skill) => skill.tags.includes("core"));
|
|
49
80
|
const requested = new Set(coreSkills.map((skill) => skill.id));
|
|
@@ -168,17 +199,20 @@ export async function installProject({
|
|
|
168
199
|
agents,
|
|
169
200
|
dryRun = false,
|
|
170
201
|
includeTags = [],
|
|
171
|
-
excludeTags = []
|
|
202
|
+
excludeTags = [],
|
|
203
|
+
selectedSkillIds
|
|
172
204
|
}) {
|
|
173
205
|
const selectedAgents = normalizeAgentList(agents);
|
|
174
206
|
const catalog = await loadAllSkills();
|
|
175
207
|
const detections = await detectProject(cwd);
|
|
176
|
-
const skills =
|
|
177
|
-
catalog,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
const skills = selectedSkillIds !== undefined && selectedSkillIds !== null
|
|
209
|
+
? resolveSkillSelectionByIds({ catalog, selectedSkillIds })
|
|
210
|
+
: resolveSkillSelection({
|
|
211
|
+
catalog,
|
|
212
|
+
detections,
|
|
213
|
+
includeTags: parseTags(includeTags),
|
|
214
|
+
excludeTags: parseTags(excludeTags)
|
|
215
|
+
});
|
|
182
216
|
const plan = buildInstallPlan({ cwd, detections, skills, agents: selectedAgents });
|
|
183
217
|
|
|
184
218
|
if (dryRun) {
|