oh-skillhub 0.1.11 → 0.1.12
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/README.md +12 -4
- package/package.json +1 -1
- package/src/cli.js +177 -30
package/README.md
CHANGED
|
@@ -12,16 +12,23 @@ npx oh-skillhub install --domain arkui --agent all --scope user
|
|
|
12
12
|
npx oh-skillhub clean --agent claude --scope user --dry-run
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Running without arguments starts a TUI selector. Choose
|
|
15
|
+
Running without arguments starts a TUI selector. Choose an action first (`Install skills` or `Clean installed skills`). Install then asks for the target (`Codex`, `Claude`, `OpenCode`, or `All`) and skill groups. Clean asks for the target and installed skills to remove.
|
|
16
16
|
|
|
17
17
|
In an interactive terminal:
|
|
18
18
|
|
|
19
19
|
- Use `Up` / `Down` or `j` / `k` to move.
|
|
20
|
-
- Press `Space` to select or unselect
|
|
21
|
-
- Press `Enter` to
|
|
20
|
+
- Press `Space` to select or unselect items.
|
|
21
|
+
- Press `Enter` to confirm the current action.
|
|
22
22
|
|
|
23
23
|
When input is piped or the terminal does not support raw key input, enter numbers separated by spaces:
|
|
24
24
|
|
|
25
|
+
```bash
|
|
26
|
+
printf "1\n1\n3 9\n" | npx oh-skillhub@latest
|
|
27
|
+
printf "2\n2\n1\n\n" | npx oh-skillhub@latest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The old piped installer format is still accepted:
|
|
31
|
+
|
|
25
32
|
```bash
|
|
26
33
|
printf "1\n3 9\n" | npx oh-skillhub@latest
|
|
27
34
|
```
|
|
@@ -37,6 +44,7 @@ printf "1\n3 9\n" | npx oh-skillhub@latest
|
|
|
37
44
|
- Support `--scope user|project`.
|
|
38
45
|
- Support `--dry-run` install plans.
|
|
39
46
|
- Scan installed skills and clean selected skills with `clean`.
|
|
47
|
+
- Show clean as a first-class option in the default TUI.
|
|
40
48
|
- Move cleaned skills to `.oh-skillhub/trash` by default, with `--purge` for permanent deletion.
|
|
41
49
|
- Run a TUI matching the `skills/common/*` and `skills/domain/*` repository layout.
|
|
42
50
|
- Keep anonymous telemetry events in a local retry queue.
|
|
@@ -119,7 +127,7 @@ node bin/oh-skillhub.js install --domain arkui --agent all --scope user --dry-ru
|
|
|
119
127
|
|
|
120
128
|
- Uses a bundled manifest derived from the GitCode `release` branch examples.
|
|
121
129
|
- Does not yet refresh the remote GitCode manifest at runtime.
|
|
122
|
-
-
|
|
130
|
+
- Downloads and installs full skill directories for manifest entries, but does not yet support arbitrary remote skill discovery outside the bundled manifest.
|
|
123
131
|
- Telemetry collector upload is designed but not yet wired; events are queued locally.
|
|
124
132
|
|
|
125
133
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -37,10 +37,14 @@ const AGENT_CHOICES = [
|
|
|
37
37
|
{ agent: "opencode", label: "OpenCode", hint: "~/.config/opencode/skill" },
|
|
38
38
|
{ agent: "all", label: "All", hint: "Codex + Claude + OpenCode" },
|
|
39
39
|
];
|
|
40
|
+
const ACTION_CHOICES = [
|
|
41
|
+
{ action: "install", label: "Install skills", hint: "Choose OpenHarmony skill groups to install" },
|
|
42
|
+
{ action: "clean", label: "Clean installed skills", hint: "Scan an agent target and remove selected skills" },
|
|
43
|
+
];
|
|
40
44
|
|
|
41
45
|
async function main(argv = []) {
|
|
42
46
|
if (argv.length === 0) {
|
|
43
|
-
await
|
|
47
|
+
await runHome();
|
|
44
48
|
return;
|
|
45
49
|
}
|
|
46
50
|
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -101,18 +105,52 @@ function parseArgs(args) {
|
|
|
101
105
|
return options;
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
async function runHome(input = process.stdin, output = process.stdout) {
|
|
109
|
+
if (input.isTTY && output.isTTY) {
|
|
110
|
+
const action = await runRawActionSelection(input, output);
|
|
111
|
+
if (action === "clean") {
|
|
112
|
+
await runClean({}, input, output);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await runInteractiveInstaller(input, output);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const answers = splitPromptAnswers(await readAll(input));
|
|
120
|
+
const hasActionAnswer = answers.length >= 3 && isSingleSelection(answers[0], ACTION_CHOICES.length);
|
|
121
|
+
if (!hasActionAnswer) {
|
|
122
|
+
await runInteractiveInstallerFromAnswers(answers, output);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
output.write(renderActionMenu());
|
|
127
|
+
output.write("Select action [1]: \n");
|
|
128
|
+
const action = ACTION_CHOICES[parseSingleSelection(answers[0], ACTION_CHOICES.length, 1)].action;
|
|
129
|
+
if (action === "clean") {
|
|
130
|
+
await runCleanWithAnswers({}, input, output, answers.slice(1));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await runInteractiveInstallerFromAnswers(answers.slice(1), output);
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
async function runClean(options, input = process.stdin, output = process.stdout) {
|
|
105
137
|
const scriptedAnswers = input.isTTY ? null : splitPromptAnswers(await readAll(input));
|
|
138
|
+
await runCleanWithAnswers(options, input, output, scriptedAnswers);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function runCleanWithAnswers(options, input, output, scriptedAnswers = null) {
|
|
142
|
+
const cleanOptions = { names: [], ...options };
|
|
143
|
+
const hasScriptedAnswers = Array.isArray(scriptedAnswers);
|
|
106
144
|
let answerIndex = 0;
|
|
107
145
|
const takeAnswer = () => {
|
|
108
|
-
if (!
|
|
146
|
+
if (!hasScriptedAnswers) return null;
|
|
109
147
|
const answer = scriptedAnswers[answerIndex] || "";
|
|
110
148
|
answerIndex += 1;
|
|
111
149
|
return answer;
|
|
112
150
|
};
|
|
113
|
-
let agent =
|
|
114
|
-
if (!agent && !
|
|
115
|
-
if (input.isTTY && output.isTTY) {
|
|
151
|
+
let agent = cleanOptions.agent;
|
|
152
|
+
if (!agent && !cleanOptions.target) {
|
|
153
|
+
if (!hasScriptedAnswers && input.isTTY && output.isTTY) {
|
|
116
154
|
agent = await runRawAgentSelection(input, output);
|
|
117
155
|
} else {
|
|
118
156
|
output.write(renderAgentMenu());
|
|
@@ -122,8 +160,8 @@ async function runClean(options, input = process.stdin, output = process.stdout)
|
|
|
122
160
|
}
|
|
123
161
|
const targets = resolveAgentTargets({
|
|
124
162
|
agent: agent || "codex",
|
|
125
|
-
scope:
|
|
126
|
-
target:
|
|
163
|
+
scope: cleanOptions.scope || "user",
|
|
164
|
+
target: cleanOptions.target,
|
|
127
165
|
cwd: process.cwd(),
|
|
128
166
|
homeDir: os.homedir(),
|
|
129
167
|
env: process.env,
|
|
@@ -135,8 +173,8 @@ async function runClean(options, input = process.stdin, output = process.stdout)
|
|
|
135
173
|
}
|
|
136
174
|
let selected;
|
|
137
175
|
let prefilledConfirmation = null;
|
|
138
|
-
if (!
|
|
139
|
-
if (input.isTTY) {
|
|
176
|
+
if (!cleanOptions.names.length && !cleanOptions.dryRun) {
|
|
177
|
+
if (!hasScriptedAnswers && input.isTTY) {
|
|
140
178
|
selected = await promptCleanSelection(input, output, discovered);
|
|
141
179
|
} else {
|
|
142
180
|
output.write(renderCleanSelectionMenu(discovered));
|
|
@@ -145,16 +183,16 @@ async function runClean(options, input = process.stdin, output = process.stdout)
|
|
|
145
183
|
prefilledConfirmation = takeAnswer() || "";
|
|
146
184
|
}
|
|
147
185
|
} else {
|
|
148
|
-
selected = selectCleanSkills(discovered,
|
|
149
|
-
if (!input.isTTY) {
|
|
186
|
+
selected = selectCleanSkills(discovered, cleanOptions.names);
|
|
187
|
+
if (hasScriptedAnswers || !input.isTTY) {
|
|
150
188
|
prefilledConfirmation = takeAnswer();
|
|
151
189
|
}
|
|
152
190
|
}
|
|
153
191
|
if (!selected.length) {
|
|
154
|
-
throw new Error(`No installed skills matched: ${
|
|
192
|
+
throw new Error(`No installed skills matched: ${cleanOptions.names.join(", ")}`);
|
|
155
193
|
}
|
|
156
|
-
const plan = planClean(selected, { mode:
|
|
157
|
-
if (
|
|
194
|
+
const plan = planClean(selected, { mode: cleanOptions.purge ? "purge" : "trash" });
|
|
195
|
+
if (cleanOptions.dryRun) {
|
|
158
196
|
output.write(`${renderCleanPlan("Clean dry run", plan)}\n`);
|
|
159
197
|
return;
|
|
160
198
|
}
|
|
@@ -318,23 +356,32 @@ function renderInstall(options) {
|
|
|
318
356
|
}
|
|
319
357
|
|
|
320
358
|
async function runInteractiveInstaller(input = process.stdin, output = process.stdout) {
|
|
359
|
+
if (!(input.isTTY && output.isTTY)) {
|
|
360
|
+
await runInteractiveInstallerFromAnswers(splitPromptAnswers(await readAll(input)), output);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
321
364
|
const manifest = loadLocalManifest();
|
|
322
365
|
const choices = buildRepositoryChoices(manifest);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
366
|
+
const agent = await runRawAgentSelection(input, output);
|
|
367
|
+
const selectedIndexes = await runRawTuiSelection(input, output, choices, agent);
|
|
368
|
+
await installInteractiveSelection(manifest, choices, agent, selectedIndexes, output);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function runInteractiveInstallerFromAnswers(answers, output = process.stdout) {
|
|
372
|
+
const manifest = loadLocalManifest();
|
|
373
|
+
const choices = buildRepositoryChoices(manifest);
|
|
374
|
+
const [agentAnswer, groupAnswer] = answers.length > 1 ? [answers[0], answers.slice(1).join(" ")] : ["", answers[0] || ""];
|
|
375
|
+
output.write(renderAgentMenu());
|
|
376
|
+
output.write("Select target [1]: \n");
|
|
377
|
+
const agent = AGENT_CHOICES[parseSingleSelection(agentAnswer, AGENT_CHOICES.length, 1)].agent;
|
|
378
|
+
output.write(renderTuiMenu(choices, agent));
|
|
379
|
+
output.write("Select groups [9]: \n");
|
|
380
|
+
const selectedIndexes = parseSelection(groupAnswer, choices.length, 9);
|
|
381
|
+
await installInteractiveSelection(manifest, choices, agent, selectedIndexes, output);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function installInteractiveSelection(manifest, choices, agent, selectedIndexes, output) {
|
|
338
385
|
const selectedChoices = selectedIndexes.map((index) => choices[index]);
|
|
339
386
|
const skills = selectSkillsForChoices(manifest, selectedChoices);
|
|
340
387
|
if (!skills.length) {
|
|
@@ -426,6 +473,26 @@ function renderAgentMenu() {
|
|
|
426
473
|
return `${lines.join("\n")}\n`;
|
|
427
474
|
}
|
|
428
475
|
|
|
476
|
+
function renderActionMenu() {
|
|
477
|
+
const width = 72;
|
|
478
|
+
const line = `+${"-".repeat(width - 2)}+`;
|
|
479
|
+
const lines = [
|
|
480
|
+
line,
|
|
481
|
+
`| ${padRight("OH SkillHub", width - 4)} |`,
|
|
482
|
+
`| ${padRight("OpenHarmony Skills Manager", width - 4)} |`,
|
|
483
|
+
line,
|
|
484
|
+
"",
|
|
485
|
+
"Choose action",
|
|
486
|
+
" Install new skills or clean existing skills from an agent target.",
|
|
487
|
+
"",
|
|
488
|
+
];
|
|
489
|
+
ACTION_CHOICES.forEach((choice, index) => {
|
|
490
|
+
lines.push(` ${index + 1}. [ ] ${choice.label.padEnd(22, " ")} ${choice.hint}`);
|
|
491
|
+
});
|
|
492
|
+
lines.push("");
|
|
493
|
+
return `${lines.join("\n")}\n`;
|
|
494
|
+
}
|
|
495
|
+
|
|
429
496
|
async function runPromptAgentSelection(input, output, existingInterface) {
|
|
430
497
|
output.write(renderAgentMenu());
|
|
431
498
|
const rl = existingInterface || readlinePromises.createInterface({ input, output });
|
|
@@ -549,6 +616,75 @@ function runRawAgentSelection(input, output) {
|
|
|
549
616
|
});
|
|
550
617
|
}
|
|
551
618
|
|
|
619
|
+
function runRawActionSelection(input, output) {
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
let cursor = 0;
|
|
622
|
+
const wasRaw = input.isRaw;
|
|
623
|
+
|
|
624
|
+
readline.emitKeypressEvents(input);
|
|
625
|
+
input.setRawMode(true);
|
|
626
|
+
|
|
627
|
+
function render() {
|
|
628
|
+
output.write("\x1b[2J\x1b[H");
|
|
629
|
+
output.write(renderRawActionMenu(cursor));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function cleanup() {
|
|
633
|
+
input.removeListener("keypress", onKeypress);
|
|
634
|
+
input.setRawMode(Boolean(wasRaw));
|
|
635
|
+
output.write("\x1b[?25h");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function onKeypress(_str, key) {
|
|
639
|
+
if (key && key.ctrl && key.name === "c") {
|
|
640
|
+
cleanup();
|
|
641
|
+
reject(new Error("Cancelled."));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (key && (key.name === "down" || key.name === "j")) {
|
|
645
|
+
cursor = (cursor + 1) % ACTION_CHOICES.length;
|
|
646
|
+
render();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (key && (key.name === "up" || key.name === "k")) {
|
|
650
|
+
cursor = (cursor - 1 + ACTION_CHOICES.length) % ACTION_CHOICES.length;
|
|
651
|
+
render();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (key && (key.name === "space" || key.name === "return")) {
|
|
655
|
+
cleanup();
|
|
656
|
+
resolve(ACTION_CHOICES[cursor].action);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
output.write("\x1b[?25l");
|
|
661
|
+
render();
|
|
662
|
+
input.on("keypress", onKeypress);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function renderRawActionMenu(cursor) {
|
|
667
|
+
const width = 76;
|
|
668
|
+
const line = `+${"-".repeat(width - 2)}+`;
|
|
669
|
+
const lines = [
|
|
670
|
+
line,
|
|
671
|
+
rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
|
|
672
|
+
rawHeaderLine("OpenHarmony Skills Manager", width, ANSI.dim),
|
|
673
|
+
line,
|
|
674
|
+
"",
|
|
675
|
+
colorize("Choose action", ANSI.bold),
|
|
676
|
+
colorize(" Up/Down or j/k: move Space/Enter: select Ctrl+C: cancel", ANSI.dim),
|
|
677
|
+
"",
|
|
678
|
+
];
|
|
679
|
+
ACTION_CHOICES.forEach((choice, index) => {
|
|
680
|
+
const pointer = index === cursor ? ">" : " ";
|
|
681
|
+
const row = `${pointer} [ ] ${choice.label.padEnd(22, " ")} ${choice.hint}`;
|
|
682
|
+
lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
|
|
683
|
+
});
|
|
684
|
+
lines.push("");
|
|
685
|
+
return `${lines.join("\n")}\n`;
|
|
686
|
+
}
|
|
687
|
+
|
|
552
688
|
function renderRawAgentMenu(cursor) {
|
|
553
689
|
const width = 76;
|
|
554
690
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
@@ -661,6 +797,15 @@ function parseSingleSelection(answer, max, defaultNumber) {
|
|
|
661
797
|
return indexes[0];
|
|
662
798
|
}
|
|
663
799
|
|
|
800
|
+
function isSingleSelection(answer, max) {
|
|
801
|
+
try {
|
|
802
|
+
parseSingleSelection(answer, max, 1);
|
|
803
|
+
return true;
|
|
804
|
+
} catch {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
664
809
|
function selectSkillsForChoices(manifest, choices) {
|
|
665
810
|
const selected = new Map();
|
|
666
811
|
for (const choice of choices) {
|
|
@@ -774,7 +919,7 @@ function helpText() {
|
|
|
774
919
|
return [
|
|
775
920
|
"oh-skillhub",
|
|
776
921
|
"",
|
|
777
|
-
"Run without arguments to choose
|
|
922
|
+
"Run without arguments to choose install or clean interactively.",
|
|
778
923
|
"",
|
|
779
924
|
"Commands:",
|
|
780
925
|
" list [--domain <name>] [--stage <name>]",
|
|
@@ -798,7 +943,9 @@ module.exports = {
|
|
|
798
943
|
renderTelemetry,
|
|
799
944
|
buildRepositoryChoices,
|
|
800
945
|
renderAgentMenu,
|
|
946
|
+
renderActionMenu,
|
|
801
947
|
renderCleanPlan,
|
|
948
|
+
renderRawActionMenu,
|
|
802
949
|
renderRawAgentMenu,
|
|
803
950
|
parseSelection,
|
|
804
951
|
renderRawTuiMenu,
|