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.
Files changed (3) hide show
  1. package/README.md +12 -4
  2. package/package.json +1 -1
  3. 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 the install target first (`Codex`, `Claude`, `OpenCode`, or `All`), then choose the skill groups to install.
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 a group.
21
- - Press `Enter` to install selected groups.
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
- - Writes a generated `SKILL.md` from manifest metadata instead of downloading full remote skill directories.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
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 runInteractiveInstaller();
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 (!scriptedAnswers) return null;
146
+ if (!hasScriptedAnswers) return null;
109
147
  const answer = scriptedAnswers[answerIndex] || "";
110
148
  answerIndex += 1;
111
149
  return answer;
112
150
  };
113
- let agent = options.agent;
114
- if (!agent && !options.target) {
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: options.scope || "user",
126
- target: options.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 (!options.names.length && !options.dryRun) {
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, options.names);
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: ${options.names.join(", ")}`);
192
+ throw new Error(`No installed skills matched: ${cleanOptions.names.join(", ")}`);
155
193
  }
156
- const plan = planClean(selected, { mode: options.purge ? "purge" : "trash" });
157
- if (options.dryRun) {
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
- let agent;
324
- let selectedIndexes;
325
- if (input.isTTY && output.isTTY) {
326
- agent = await runRawAgentSelection(input, output);
327
- selectedIndexes = await runRawTuiSelection(input, output, choices, agent);
328
- } else {
329
- const answers = splitPromptAnswers(await readAll(input));
330
- const [agentAnswer, groupAnswer] = answers.length > 1 ? [answers[0], answers.slice(1).join(" ")] : ["", answers[0] || ""];
331
- output.write(renderAgentMenu());
332
- output.write("Select target [1]: \n");
333
- agent = AGENT_CHOICES[parseSingleSelection(agentAnswer, AGENT_CHOICES.length, 1)].agent;
334
- output.write(renderTuiMenu(choices, agent));
335
- output.write("Select groups [9]: \n");
336
- selectedIndexes = parseSelection(groupAnswer, choices.length, 9);
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 an agent target and skill domain interactively.",
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,