oh-skillhub 0.1.17 → 0.1.18

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/package.json +1 -1
  2. package/src/agents.js +12 -3
  3. package/src/cli.js +78 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -13,11 +13,10 @@ function resolveAgentTargets(options = {}) {
13
13
  if (!["user", "project"].includes(scope)) {
14
14
  throw new Error(`Unsupported scope "${scope}". Use user or project.`);
15
15
  }
16
- if (agent === "all" && options.target) {
16
+ const agents = normalizeAgentSelection(agent);
17
+ if (agents.length > 1 && options.target) {
17
18
  throw new Error("--agent all cannot be combined with --target");
18
19
  }
19
-
20
- const agents = agent === "all" ? SUPPORTED_AGENTS : [agent];
21
20
  for (const item of agents) {
22
21
  if (!SUPPORTED_AGENTS.includes(item)) {
23
22
  throw new Error(`Unsupported agent "${item}". Use codex, claude, opencode, or all.`);
@@ -31,6 +30,16 @@ function resolveAgentTargets(options = {}) {
31
30
  }));
32
31
  }
33
32
 
33
+ function normalizeAgentSelection(agent) {
34
+ if (agent === "all") {
35
+ return SUPPORTED_AGENTS;
36
+ }
37
+ if (Array.isArray(agent)) {
38
+ return Array.from(new Set(agent));
39
+ }
40
+ return [agent];
41
+ }
42
+
34
43
  function defaultDirFor(agent, scope, cwd, homeDir, env) {
35
44
  if (scope === "project") {
36
45
  if (agent === "codex") return path.join(cwd, ".codex", "skills");
package/src/cli.js CHANGED
@@ -38,6 +38,8 @@ const AGENT_CHOICES = [
38
38
  { agent: "opencode", label: "OpenCode", hint: "~/.config/opencode/skill" },
39
39
  { agent: "all", label: "All", hint: "Codex + Claude + OpenCode" },
40
40
  ];
41
+ const INDIVIDUAL_AGENT_INDEXES = [0, 1, 2];
42
+ const ALL_AGENT_INDEX = 3;
41
43
  const ACTION_CHOICES = [
42
44
  { action: "install", label: "Install skills", hint: "Choose OpenHarmony skill groups to install" },
43
45
  { action: "clean", label: "Clean installed skills", hint: "Scan an agent target and remove selected skills" },
@@ -156,7 +158,7 @@ async function runCleanWithAnswers(options, input, output, scriptedAnswers = nul
156
158
  } else {
157
159
  output.write(renderAgentMenu());
158
160
  output.write("Select target [1]: \n");
159
- agent = AGENT_CHOICES[parseSingleSelection(takeAnswer() || "", AGENT_CHOICES.length, 1)].agent;
161
+ agent = parseAgentSelection(takeAnswer() || "");
160
162
  }
161
163
  }
162
164
  const targets = resolveAgentTargets({
@@ -379,7 +381,7 @@ async function runInteractiveInstallerFromAnswers(answers, output = process.stdo
379
381
  const [agentAnswer, groupAnswer] = answers.length > 1 ? [answers[0], answers.slice(1).join(" ")] : ["", answers[0] || ""];
380
382
  output.write(renderAgentMenu());
381
383
  output.write("Select target [1]: \n");
382
- const agent = AGENT_CHOICES[parseSingleSelection(agentAnswer, AGENT_CHOICES.length, 1)].agent;
384
+ const agent = parseAgentSelection(agentAnswer);
383
385
  output.write(renderTuiMenu(choices, agent));
384
386
  output.write("Select groups [9]: \n");
385
387
  const selectedIndexes = parseSelection(groupAnswer, choices.length, 9);
@@ -392,7 +394,7 @@ async function installInteractiveSelection(manifest, choices, agent, selectedInd
392
394
  if (!skills.length) {
393
395
  throw new Error("No skills matched the selected groups.");
394
396
  }
395
- output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for ${agent}:user...\n`);
397
+ output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for ${formatAgentSelection(agent)}:user...\n`);
396
398
  const sourceRoot = await withSpinner(output, `Preparing skill source from ${manifest.source}#${manifest.ref}`, () =>
397
399
  ensureSkillSourceRootAsync(manifest, { env: process.env, skills }),
398
400
  );
@@ -469,7 +471,7 @@ function renderTuiMenu(choices, agent = "codex") {
469
471
  line,
470
472
  "",
471
473
  "Target",
472
- ` Agent: ${agent}`,
474
+ ` Agent: ${formatAgentSelection(agent)}`,
473
475
  " Scope: user",
474
476
  "",
475
477
  "Choose skill groups",
@@ -607,7 +609,7 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
607
609
  function runRawAgentSelection(input, output) {
608
610
  return new Promise((resolve, reject) => {
609
611
  let cursor = 0;
610
- let selected = 0;
612
+ const selected = new Set([0]);
611
613
  const wasRaw = input.isRaw;
612
614
 
613
615
  readline.emitKeypressEvents(input);
@@ -641,13 +643,13 @@ function runRawAgentSelection(input, output) {
641
643
  return;
642
644
  }
643
645
  if (key && key.name === "space") {
644
- selected = cursor;
646
+ toggleAgentSelection(selected, cursor);
645
647
  render();
646
648
  return;
647
649
  }
648
650
  if (key && key.name === "return") {
649
651
  cleanup();
650
- resolve(AGENT_CHOICES[selected].agent);
652
+ resolve(agentSelectionFromIndexes(selected, cursor));
651
653
  }
652
654
  }
653
655
 
@@ -733,9 +735,10 @@ function renderRawActionMenu(cursor, selected = cursor) {
733
735
  return `${lines.join("\n")}\n`;
734
736
  }
735
737
 
736
- function renderRawAgentMenu(cursor, selected = cursor) {
738
+ function renderRawAgentMenu(cursor, selected = new Set([cursor])) {
737
739
  const width = 76;
738
740
  const line = `+${"-".repeat(width - 2)}+`;
741
+ const selectedIndexes = normalizeAgentIndexes(selected);
739
742
  const lines = [
740
743
  line,
741
744
  rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
@@ -749,7 +752,7 @@ function renderRawAgentMenu(cursor, selected = cursor) {
749
752
  AGENT_CHOICES.forEach((choice, index) => {
750
753
  const pointer = index === cursor ? ">" : " ";
751
754
  const highlighted = index === cursor;
752
- const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
755
+ const row = `${pointer} ${rawCheckbox(isAgentIndexSelected(selectedIndexes, index), highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
753
756
  lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
754
757
  });
755
758
  lines.push("");
@@ -925,6 +928,15 @@ function parseSingleSelection(answer, max, defaultNumber) {
925
928
  return indexes[0];
926
929
  }
927
930
 
931
+ function parseAgentSelection(answer) {
932
+ const indexes = parseSelection(answer, AGENT_CHOICES.length, 1);
933
+ if (indexes.includes(ALL_AGENT_INDEX)) {
934
+ return "all";
935
+ }
936
+ const agents = indexes.map((index) => AGENT_CHOICES[index].agent);
937
+ return agents.length === 1 ? agents[0] : agents;
938
+ }
939
+
928
940
  function isSingleSelection(answer, max) {
929
941
  try {
930
942
  parseSingleSelection(answer, max, 1);
@@ -934,6 +946,62 @@ function isSingleSelection(answer, max) {
934
946
  }
935
947
  }
936
948
 
949
+ function normalizeAgentIndexes(selected) {
950
+ const indexes = selected instanceof Set ? Array.from(selected) : [selected];
951
+ if (indexes.includes(ALL_AGENT_INDEX)) {
952
+ return new Set(INDIVIDUAL_AGENT_INDEXES);
953
+ }
954
+ return new Set(indexes.filter((index) => INDIVIDUAL_AGENT_INDEXES.includes(index)));
955
+ }
956
+
957
+ function isAgentIndexSelected(selected, index) {
958
+ if (index === ALL_AGENT_INDEX) {
959
+ return INDIVIDUAL_AGENT_INDEXES.every((item) => selected.has(item));
960
+ }
961
+ return selected.has(index);
962
+ }
963
+
964
+ function toggleAgentSelection(selected, cursor) {
965
+ if (cursor === ALL_AGENT_INDEX) {
966
+ if (INDIVIDUAL_AGENT_INDEXES.every((index) => selected.has(index))) {
967
+ selected.clear();
968
+ selected.add(0);
969
+ return;
970
+ }
971
+ selected.clear();
972
+ for (const index of INDIVIDUAL_AGENT_INDEXES) {
973
+ selected.add(index);
974
+ }
975
+ return;
976
+ }
977
+ if (selected.has(cursor) && selected.size > 1) {
978
+ selected.delete(cursor);
979
+ return;
980
+ }
981
+ selected.add(cursor);
982
+ }
983
+
984
+ function agentSelectionFromIndexes(selected, cursor = 0) {
985
+ const normalized = normalizeAgentIndexes(selected);
986
+ if (!normalized.size) {
987
+ normalized.add(INDIVIDUAL_AGENT_INDEXES.includes(cursor) ? cursor : 0);
988
+ }
989
+ if (INDIVIDUAL_AGENT_INDEXES.every((index) => normalized.has(index))) {
990
+ return "all";
991
+ }
992
+ const agents = Array.from(normalized)
993
+ .sort((left, right) => left - right)
994
+ .map((index) => AGENT_CHOICES[index].agent);
995
+ return agents.length === 1 ? agents[0] : agents;
996
+ }
997
+
998
+ function formatAgentSelection(agent) {
999
+ if (Array.isArray(agent)) {
1000
+ return agent.join(", ");
1001
+ }
1002
+ return agent;
1003
+ }
1004
+
937
1005
  function selectSkillsForChoices(manifest, choices) {
938
1006
  const selected = new Map();
939
1007
  for (const choice of choices) {
@@ -987,7 +1055,7 @@ function renderInteractiveCompletion(skills, targetOptions, output = process.std
987
1055
  renderStatusLine(
988
1056
  "success",
989
1057
  "INSTALL SUCCESS",
990
- `Installed ${skills.length} skill(s) for ${targetOptions.agent}:${targetOptions.scope}.`,
1058
+ `Installed ${skills.length} skill(s) for ${formatAgentSelection(targetOptions.agent)}:${targetOptions.scope}.`,
991
1059
  output,
992
1060
  ),
993
1061
  ].join("\n");