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.
- package/README.md +3 -2
- package/package.json +1 -1
- 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
|
|
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 "
|
|
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
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
162
|
-
output.write(`${renderInstallForSkills(skills, { agent
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
398
|
+
rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
|
|
399
|
+
rawHeaderLine("OpenHarmony Skills Installer", width, ANSI.dim),
|
|
279
400
|
line,
|
|
280
401
|
"",
|
|
281
|
-
"
|
|
282
|
-
"
|
|
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
|
|
292
|
-
|
|
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
|
|
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,
|