oh-skillhub 0.1.4 → 0.1.6

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 +3 -2
  2. package/package.json +1 -1
  3. package/src/cli.js +197 -25
package/README.md CHANGED
@@ -11,7 +11,7 @@ npx oh-skillhub install --domain arkui --agent codex
11
11
  npx oh-skillhub install --domain arkui --agent all --scope user
12
12
  ```
13
13
 
14
- Running without arguments starts a TUI selector and installs the selected skill set to the default Codex user skills directory.
14
+ 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
15
 
16
16
  In an interactive terminal:
17
17
 
@@ -22,7 +22,7 @@ In an interactive terminal:
22
22
  When input is piped or the terminal does not support raw key input, enter numbers separated by spaces:
23
23
 
24
24
  ```bash
25
- printf "3 9\n" | npx oh-skillhub@latest
25
+ printf "1\n3 9\n" | npx oh-skillhub@latest
26
26
  ```
27
27
 
28
28
  ## Current Capabilities
@@ -31,6 +31,7 @@ printf "3 9\n" | npx oh-skillhub@latest
31
31
  - Resolve profile/domain selections such as `arkui`, `app-dev`, `cicd`, `testing`, and `security`.
32
32
  - Install skills into Codex, Claude Code, and OpenCode target directories.
33
33
  - Support `--agent codex|claude|opencode|all`.
34
+ - Support interactive target selection for `Codex`, `Claude`, `OpenCode`, and `All`.
34
35
  - Support `--scope user|project`.
35
36
  - Support `--dry-run` install plans.
36
37
  - Run a TUI matching the `skills/common/*` and `skills/domain/*` repository layout.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -21,6 +21,20 @@ const DOMAIN_NAMES = [
21
21
  "kernel",
22
22
  "security",
23
23
  ];
24
+ const ANSI = {
25
+ reset: "\x1b[0m",
26
+ bold: "\x1b[1m",
27
+ dim: "\x1b[2m",
28
+ cyan: "\x1b[36m",
29
+ green: "\x1b[32m",
30
+ reverse: "\x1b[7m",
31
+ };
32
+ const AGENT_CHOICES = [
33
+ { agent: "codex", label: "Codex", hint: "~/.codex/skills" },
34
+ { agent: "claude", label: "Claude", hint: "~/.claude/skills" },
35
+ { agent: "opencode", label: "OpenCode", hint: "~/.config/opencode/skill" },
36
+ { agent: "all", label: "All", hint: "Codex + Claude + OpenCode" },
37
+ ];
24
38
 
25
39
  async function main(argv = []) {
26
40
  if (argv.length === 0) {
@@ -149,17 +163,46 @@ function renderInstall(options) {
149
163
  async function runInteractiveInstaller(input = process.stdin, output = process.stdout) {
150
164
  const manifest = loadLocalManifest();
151
165
  const choices = buildRepositoryChoices(manifest);
152
- const selectedIndexes =
153
- input.isTTY && output.isTTY
154
- ? await runRawTuiSelection(input, output, choices)
155
- : await runPromptSelection(input, output, choices);
166
+ let agent;
167
+ let selectedIndexes;
168
+ if (input.isTTY && output.isTTY) {
169
+ agent = await runRawAgentSelection(input, output);
170
+ selectedIndexes = await runRawTuiSelection(input, output, choices, agent);
171
+ } else {
172
+ const answers = splitPromptAnswers(await readAll(input));
173
+ const [agentAnswer, groupAnswer] = answers.length > 1 ? [answers[0], answers.slice(1).join(" ")] : ["", answers[0] || ""];
174
+ output.write(renderAgentMenu());
175
+ output.write("Select target [1]: \n");
176
+ agent = AGENT_CHOICES[parseSingleSelection(agentAnswer, AGENT_CHOICES.length, 1)].agent;
177
+ output.write(renderTuiMenu(choices, agent));
178
+ output.write("Select groups [9]: \n");
179
+ selectedIndexes = parseSelection(groupAnswer, choices.length, 9);
180
+ }
156
181
  const selectedChoices = selectedIndexes.map((index) => choices[index]);
157
182
  const skills = selectSkillsForChoices(manifest, selectedChoices);
158
183
  if (!skills.length) {
159
184
  throw new Error("No skills matched the selected groups.");
160
185
  }
161
- output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for codex:user...\n`);
162
- output.write(`${renderInstallForSkills(skills, { agent: "codex", scope: "user" })}\n`);
186
+ output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for ${agent}:user...\n`);
187
+ output.write(`${renderInstallForSkills(skills, { agent, scope: "user" })}\n`);
188
+ }
189
+
190
+ function readAll(input) {
191
+ return new Promise((resolve, reject) => {
192
+ let text = "";
193
+ input.setEncoding("utf8");
194
+ input.on("data", (chunk) => {
195
+ text += chunk;
196
+ });
197
+ input.on("end", () => resolve(text));
198
+ input.on("error", reject);
199
+ });
200
+ }
201
+
202
+ function splitPromptAnswers(text) {
203
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").filter((line, index, lines) => {
204
+ return line.length > 0 || index < lines.length - 1;
205
+ });
163
206
  }
164
207
 
165
208
  function buildRepositoryChoices(manifest) {
@@ -175,7 +218,7 @@ function buildRepositoryChoices(manifest) {
175
218
  return choices;
176
219
  }
177
220
 
178
- function renderTuiMenu(choices) {
221
+ function renderTuiMenu(choices, agent = "codex") {
179
222
  const width = 72;
180
223
  const line = `+${"-".repeat(width - 2)}+`;
181
224
  const lines = [
@@ -185,7 +228,7 @@ function renderTuiMenu(choices) {
185
228
  line,
186
229
  "",
187
230
  "Target",
188
- " Agent: codex",
231
+ ` Agent: ${agent}`,
189
232
  " Scope: user",
190
233
  "",
191
234
  "Choose skill groups",
@@ -204,18 +247,49 @@ function renderTuiMenu(choices) {
204
247
  return `${lines.join("\n")}\n`;
205
248
  }
206
249
 
207
- async function runPromptSelection(input, output, choices) {
208
- output.write(renderTuiMenu(choices));
209
- const rl = readlinePromises.createInterface({ input, output });
250
+ function renderAgentMenu() {
251
+ const width = 72;
252
+ const line = `+${"-".repeat(width - 2)}+`;
253
+ const lines = [
254
+ line,
255
+ `| ${padRight("OH SkillHub", width - 4)} |`,
256
+ `| ${padRight("OpenHarmony Skills Installer", width - 4)} |`,
257
+ line,
258
+ "",
259
+ "Choose install target",
260
+ " Pick where the selected skills should be installed.",
261
+ "",
262
+ ];
263
+ AGENT_CHOICES.forEach((choice, index) => {
264
+ lines.push(` ${index + 1}. [ ] ${choice.label.padEnd(10, " ")} ${choice.hint}`);
265
+ });
266
+ lines.push("");
267
+ return `${lines.join("\n")}\n`;
268
+ }
269
+
270
+ async function runPromptAgentSelection(input, output, existingInterface) {
271
+ output.write(renderAgentMenu());
272
+ const rl = existingInterface || readlinePromises.createInterface({ input, output });
273
+ try {
274
+ const answer = await rl.question("Select target [1]: ");
275
+ return AGENT_CHOICES[parseSingleSelection(answer, AGENT_CHOICES.length, 1)].agent;
276
+ } finally {
277
+ if (!existingInterface) rl.close();
278
+ }
279
+ }
280
+
281
+ async function runPromptSelection(input, output, choices, agent = "codex", existingInterface) {
282
+ output.write(renderTuiMenu(choices, agent));
283
+ const rl = existingInterface || readlinePromises.createInterface({ input, output });
210
284
  try {
211
285
  const answer = await rl.question("Select groups [9]: ");
212
286
  return parseSelection(answer, choices.length, 9);
213
287
  } finally {
214
- rl.close();
288
+ if (!existingInterface) rl.close();
215
289
  }
216
290
  }
217
291
 
218
- function runRawTuiSelection(input, output, choices) {
292
+ function runRawTuiSelection(input, output, choices, agent = "codex") {
219
293
  return new Promise((resolve, reject) => {
220
294
  let cursor = 8;
221
295
  const selected = new Set([cursor]);
@@ -226,7 +300,7 @@ function runRawTuiSelection(input, output, choices) {
226
300
 
227
301
  function render() {
228
302
  output.write("\x1b[2J\x1b[H");
229
- output.write(renderRawTuiMenu(choices, cursor, selected));
303
+ output.write(renderRawTuiMenu(choices, cursor, selected, agent));
230
304
  }
231
305
 
232
306
  function cleanup() {
@@ -269,35 +343,123 @@ function runRawTuiSelection(input, output, choices) {
269
343
  });
270
344
  }
271
345
 
272
- function renderRawTuiMenu(choices, cursor, selected) {
346
+ function runRawAgentSelection(input, output) {
347
+ return new Promise((resolve, reject) => {
348
+ let cursor = 0;
349
+ const wasRaw = input.isRaw;
350
+
351
+ readline.emitKeypressEvents(input);
352
+ input.setRawMode(true);
353
+
354
+ function render() {
355
+ output.write("\x1b[2J\x1b[H");
356
+ output.write(renderRawAgentMenu(cursor));
357
+ }
358
+
359
+ function cleanup() {
360
+ input.removeListener("keypress", onKeypress);
361
+ input.setRawMode(Boolean(wasRaw));
362
+ output.write("\x1b[?25h");
363
+ }
364
+
365
+ function onKeypress(_str, key) {
366
+ if (key && key.ctrl && key.name === "c") {
367
+ cleanup();
368
+ reject(new Error("Cancelled."));
369
+ return;
370
+ }
371
+ if (key && (key.name === "down" || key.name === "j")) {
372
+ cursor = (cursor + 1) % AGENT_CHOICES.length;
373
+ render();
374
+ return;
375
+ }
376
+ if (key && (key.name === "up" || key.name === "k")) {
377
+ cursor = (cursor - 1 + AGENT_CHOICES.length) % AGENT_CHOICES.length;
378
+ render();
379
+ return;
380
+ }
381
+ if (key && (key.name === "space" || key.name === "return")) {
382
+ cleanup();
383
+ resolve(AGENT_CHOICES[cursor].agent);
384
+ }
385
+ }
386
+
387
+ output.write("\x1b[?25l");
388
+ render();
389
+ input.on("keypress", onKeypress);
390
+ });
391
+ }
392
+
393
+ function renderRawAgentMenu(cursor) {
273
394
  const width = 76;
274
395
  const line = `+${"-".repeat(width - 2)}+`;
275
396
  const lines = [
276
397
  line,
277
- `| ${padRight("OH SkillHub", width - 4)} |`,
278
- `| ${padRight("OpenHarmony Skills Installer", width - 4)} |`,
398
+ rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
399
+ rawHeaderLine("OpenHarmony Skills Installer", width, ANSI.dim),
279
400
  line,
280
401
  "",
281
- "Target",
282
- " Agent: codex",
402
+ colorize("Choose install target", ANSI.bold),
403
+ colorize(" Up/Down or j/k: move Space/Enter: select Ctrl+C: cancel", ANSI.dim),
404
+ "",
405
+ ];
406
+ AGENT_CHOICES.forEach((choice, index) => {
407
+ const pointer = index === cursor ? ">" : " ";
408
+ const row = `${pointer} [ ] ${choice.label.padEnd(10, " ")} ${choice.hint}`;
409
+ lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
410
+ });
411
+ lines.push("");
412
+ return `${lines.join("\n")}\n`;
413
+ }
414
+
415
+ function renderRawTuiMenu(choices, cursor, selected, agent = "codex") {
416
+ const width = 76;
417
+ const line = `+${"-".repeat(width - 2)}+`;
418
+ const lines = [
419
+ line,
420
+ rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
421
+ rawHeaderLine("OpenHarmony Skills Installer", width, ANSI.dim),
422
+ line,
423
+ "",
424
+ colorize("Target", ANSI.bold),
425
+ ` Agent: ${agent}`,
283
426
  " Scope: user",
284
427
  "",
285
- "Choose skill groups",
286
- " Up/Down or j/k: move Space: select Enter: install Ctrl+C: cancel",
428
+ colorize("Choose skill groups", ANSI.bold),
429
+ colorize(" Up/Down or j/k: move Space: select Enter: install Ctrl+C: cancel", ANSI.dim),
287
430
  "",
288
431
  ];
289
432
  choices.forEach((choice, index) => {
290
433
  const pointer = index === cursor ? ">" : " ";
291
- const checkbox = selected.has(index) ? "[✓]" : "[ ]";
292
- lines.push(`${pointer} ${checkbox} ${choice.path.padEnd(28, " ")} ${choice.count} skill(s)`);
434
+ const highlighted = index === cursor;
435
+ const checkbox = rawCheckbox(selected.has(index), highlighted);
436
+ const row = `${pointer} ${checkbox} ${choice.path.padEnd(28, " ")} ${choice.count} skill(s)`;
437
+ lines.push(highlighted ? colorize(row, ANSI.reverse, ANSI.bold) : row);
293
438
  for (const leaf of renderSkillLeaves(choice)) {
294
- lines.push(` - ${leaf}`);
439
+ lines.push(colorize(` - ${leaf}`, ANSI.dim));
295
440
  }
296
441
  });
297
442
  lines.push("");
298
443
  return `${lines.join("\n")}\n`;
299
444
  }
300
445
 
446
+ function colorize(value, ...codes) {
447
+ return `${codes.join("")}${value}${ANSI.reset}`;
448
+ }
449
+
450
+ function rawHeaderLine(value, width, ...codes) {
451
+ const styled = colorize(value, ...codes);
452
+ return `| ${styled}${" ".repeat(width - 4 - value.length)} |`;
453
+ }
454
+
455
+ function rawCheckbox(isSelected, highlighted) {
456
+ if (!isSelected) {
457
+ return "[ ]";
458
+ }
459
+ const restore = highlighted ? `${ANSI.reverse}${ANSI.bold}` : "";
460
+ return `${colorize("[✓]", ANSI.green, ANSI.bold)}${restore}`;
461
+ }
462
+
301
463
  function renderSkillLeaves(choice) {
302
464
  return (choice.skills || []).map((skill) => {
303
465
  if (choice.scope === "common") {
@@ -332,6 +494,14 @@ function parseSelection(answer, max, defaultNumber) {
332
494
  return indexes;
333
495
  }
334
496
 
497
+ function parseSingleSelection(answer, max, defaultNumber) {
498
+ const indexes = parseSelection(answer, max, defaultNumber);
499
+ if (indexes.length !== 1) {
500
+ throw new Error("Choose one target.");
501
+ }
502
+ return indexes[0];
503
+ }
504
+
335
505
  function selectSkillsForChoices(manifest, choices) {
336
506
  const selected = new Map();
337
507
  for (const choice of choices) {
@@ -435,7 +605,7 @@ function helpText() {
435
605
  return [
436
606
  "oh-skillhub",
437
607
  "",
438
- "Run without arguments to choose a skill domain interactively.",
608
+ "Run without arguments to choose an agent target and skill domain interactively.",
439
609
  "",
440
610
  "Commands:",
441
611
  " list [--domain <name>] [--stage <name>]",
@@ -457,6 +627,8 @@ module.exports = {
457
627
  renderList,
458
628
  renderTelemetry,
459
629
  buildRepositoryChoices,
630
+ renderAgentMenu,
631
+ renderRawAgentMenu,
460
632
  parseSelection,
461
633
  renderRawTuiMenu,
462
634
  renderTuiMenu,