oh-skillhub 0.1.17 → 0.1.19

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 +105 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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",
@@ -556,8 +558,7 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
556
558
  const selected = new Set([cursor]);
557
559
  const wasRaw = input.isRaw;
558
560
 
559
- readline.emitKeypressEvents(input);
560
- input.setRawMode(true);
561
+ prepareRawInput(input);
561
562
 
562
563
  function render() {
563
564
  output.write("\x1b[2J\x1b[H");
@@ -566,7 +567,7 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
566
567
 
567
568
  function cleanup() {
568
569
  input.removeListener("keypress", onKeypress);
569
- input.setRawMode(Boolean(wasRaw));
570
+ releaseRawInput(input, wasRaw);
570
571
  output.write("\x1b[?25h");
571
572
  }
572
573
 
@@ -607,11 +608,10 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
607
608
  function runRawAgentSelection(input, output) {
608
609
  return new Promise((resolve, reject) => {
609
610
  let cursor = 0;
610
- let selected = 0;
611
+ const selected = new Set([0]);
611
612
  const wasRaw = input.isRaw;
612
613
 
613
- readline.emitKeypressEvents(input);
614
- input.setRawMode(true);
614
+ prepareRawInput(input);
615
615
 
616
616
  function render() {
617
617
  output.write("\x1b[2J\x1b[H");
@@ -620,7 +620,7 @@ function runRawAgentSelection(input, output) {
620
620
 
621
621
  function cleanup() {
622
622
  input.removeListener("keypress", onKeypress);
623
- input.setRawMode(Boolean(wasRaw));
623
+ releaseRawInput(input, wasRaw);
624
624
  output.write("\x1b[?25h");
625
625
  }
626
626
 
@@ -641,13 +641,13 @@ function runRawAgentSelection(input, output) {
641
641
  return;
642
642
  }
643
643
  if (key && key.name === "space") {
644
- selected = cursor;
644
+ toggleAgentSelection(selected, cursor);
645
645
  render();
646
646
  return;
647
647
  }
648
648
  if (key && key.name === "return") {
649
649
  cleanup();
650
- resolve(AGENT_CHOICES[selected].agent);
650
+ resolve(agentSelectionFromIndexes(selected, cursor));
651
651
  }
652
652
  }
653
653
 
@@ -663,8 +663,7 @@ function runRawActionSelection(input, output) {
663
663
  let selected = 0;
664
664
  const wasRaw = input.isRaw;
665
665
 
666
- readline.emitKeypressEvents(input);
667
- input.setRawMode(true);
666
+ prepareRawInput(input);
668
667
 
669
668
  function render() {
670
669
  output.write("\x1b[2J\x1b[H");
@@ -673,7 +672,7 @@ function runRawActionSelection(input, output) {
673
672
 
674
673
  function cleanup() {
675
674
  input.removeListener("keypress", onKeypress);
676
- input.setRawMode(Boolean(wasRaw));
675
+ releaseRawInput(input, wasRaw);
677
676
  output.write("\x1b[?25h");
678
677
  }
679
678
 
@@ -733,9 +732,27 @@ function renderRawActionMenu(cursor, selected = cursor) {
733
732
  return `${lines.join("\n")}\n`;
734
733
  }
735
734
 
736
- function renderRawAgentMenu(cursor, selected = cursor) {
735
+ function prepareRawInput(input) {
736
+ if (typeof input.on === "function" && typeof input.listenerCount === "function") {
737
+ readline.emitKeypressEvents(input);
738
+ }
739
+ input.setRawMode(true);
740
+ if (typeof input.resume === "function") {
741
+ input.resume();
742
+ }
743
+ }
744
+
745
+ function releaseRawInput(input, wasRaw) {
746
+ input.setRawMode(Boolean(wasRaw));
747
+ if (!wasRaw && typeof input.pause === "function") {
748
+ input.pause();
749
+ }
750
+ }
751
+
752
+ function renderRawAgentMenu(cursor, selected = new Set([cursor])) {
737
753
  const width = 76;
738
754
  const line = `+${"-".repeat(width - 2)}+`;
755
+ const selectedIndexes = normalizeAgentIndexes(selected);
739
756
  const lines = [
740
757
  line,
741
758
  rawHeaderLine("OH SkillHub", width, ANSI.cyan, ANSI.bold),
@@ -749,7 +766,7 @@ function renderRawAgentMenu(cursor, selected = cursor) {
749
766
  AGENT_CHOICES.forEach((choice, index) => {
750
767
  const pointer = index === cursor ? ">" : " ";
751
768
  const highlighted = index === cursor;
752
- const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
769
+ const row = `${pointer} ${rawCheckbox(isAgentIndexSelected(selectedIndexes, index), highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
753
770
  lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
754
771
  });
755
772
  lines.push("");
@@ -793,8 +810,7 @@ function runRawCleanSelection(input, output, skills) {
793
810
  const selected = new Set([cursor]);
794
811
  const wasRaw = input.isRaw;
795
812
 
796
- readline.emitKeypressEvents(input);
797
- input.setRawMode(true);
813
+ prepareRawInput(input);
798
814
 
799
815
  function render() {
800
816
  output.write("\x1b[2J\x1b[H");
@@ -803,7 +819,7 @@ function runRawCleanSelection(input, output, skills) {
803
819
 
804
820
  function cleanup() {
805
821
  input.removeListener("keypress", onKeypress);
806
- input.setRawMode(Boolean(wasRaw));
822
+ releaseRawInput(input, wasRaw);
807
823
  output.write("\x1b[?25h");
808
824
  }
809
825
 
@@ -925,6 +941,15 @@ function parseSingleSelection(answer, max, defaultNumber) {
925
941
  return indexes[0];
926
942
  }
927
943
 
944
+ function parseAgentSelection(answer) {
945
+ const indexes = parseSelection(answer, AGENT_CHOICES.length, 1);
946
+ if (indexes.includes(ALL_AGENT_INDEX)) {
947
+ return "all";
948
+ }
949
+ const agents = indexes.map((index) => AGENT_CHOICES[index].agent);
950
+ return agents.length === 1 ? agents[0] : agents;
951
+ }
952
+
928
953
  function isSingleSelection(answer, max) {
929
954
  try {
930
955
  parseSingleSelection(answer, max, 1);
@@ -934,6 +959,62 @@ function isSingleSelection(answer, max) {
934
959
  }
935
960
  }
936
961
 
962
+ function normalizeAgentIndexes(selected) {
963
+ const indexes = selected instanceof Set ? Array.from(selected) : [selected];
964
+ if (indexes.includes(ALL_AGENT_INDEX)) {
965
+ return new Set(INDIVIDUAL_AGENT_INDEXES);
966
+ }
967
+ return new Set(indexes.filter((index) => INDIVIDUAL_AGENT_INDEXES.includes(index)));
968
+ }
969
+
970
+ function isAgentIndexSelected(selected, index) {
971
+ if (index === ALL_AGENT_INDEX) {
972
+ return INDIVIDUAL_AGENT_INDEXES.every((item) => selected.has(item));
973
+ }
974
+ return selected.has(index);
975
+ }
976
+
977
+ function toggleAgentSelection(selected, cursor) {
978
+ if (cursor === ALL_AGENT_INDEX) {
979
+ if (INDIVIDUAL_AGENT_INDEXES.every((index) => selected.has(index))) {
980
+ selected.clear();
981
+ selected.add(0);
982
+ return;
983
+ }
984
+ selected.clear();
985
+ for (const index of INDIVIDUAL_AGENT_INDEXES) {
986
+ selected.add(index);
987
+ }
988
+ return;
989
+ }
990
+ if (selected.has(cursor) && selected.size > 1) {
991
+ selected.delete(cursor);
992
+ return;
993
+ }
994
+ selected.add(cursor);
995
+ }
996
+
997
+ function agentSelectionFromIndexes(selected, cursor = 0) {
998
+ const normalized = normalizeAgentIndexes(selected);
999
+ if (!normalized.size) {
1000
+ normalized.add(INDIVIDUAL_AGENT_INDEXES.includes(cursor) ? cursor : 0);
1001
+ }
1002
+ if (INDIVIDUAL_AGENT_INDEXES.every((index) => normalized.has(index))) {
1003
+ return "all";
1004
+ }
1005
+ const agents = Array.from(normalized)
1006
+ .sort((left, right) => left - right)
1007
+ .map((index) => AGENT_CHOICES[index].agent);
1008
+ return agents.length === 1 ? agents[0] : agents;
1009
+ }
1010
+
1011
+ function formatAgentSelection(agent) {
1012
+ if (Array.isArray(agent)) {
1013
+ return agent.join(", ");
1014
+ }
1015
+ return agent;
1016
+ }
1017
+
937
1018
  function selectSkillsForChoices(manifest, choices) {
938
1019
  const selected = new Map();
939
1020
  for (const choice of choices) {
@@ -987,7 +1068,7 @@ function renderInteractiveCompletion(skills, targetOptions, output = process.std
987
1068
  renderStatusLine(
988
1069
  "success",
989
1070
  "INSTALL SUCCESS",
990
- `Installed ${skills.length} skill(s) for ${targetOptions.agent}:${targetOptions.scope}.`,
1071
+ `Installed ${skills.length} skill(s) for ${formatAgentSelection(targetOptions.agent)}:${targetOptions.scope}.`,
991
1072
  output,
992
1073
  ),
993
1074
  ].join("\n");
@@ -1088,6 +1169,8 @@ module.exports = {
1088
1169
  renderRawAgentMenu,
1089
1170
  renderRawCleanSelectionMenu,
1090
1171
  parseSelection,
1172
+ prepareRawInput,
1173
+ releaseRawInput,
1091
1174
  renderRawTuiMenu,
1092
1175
  renderTuiMenu,
1093
1176
  withSpinner,