oh-skillhub 0.1.13 → 0.1.15

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/cli.js +60 -17
  3. package/src/source.js +79 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -8,7 +8,7 @@ const { resolveAgentTargets } = require("./agents");
8
8
  const { applyCleanPlan, planClean, scanInstalledSkills } = require("./cleaner");
9
9
  const { loadLocalManifest, loadProfiles, selectSkills } = require("./manifest");
10
10
  const { applyInstallPlan, planInstall } = require("./planner");
11
- const { ensureSkillSourceRoot } = require("./source");
11
+ const { ensureSkillSourceRoot, ensureSkillSourceRootAsync } = require("./source");
12
12
  const { buildTelemetryEvent, enqueueTelemetryEvent, telemetryStatus } = require("./telemetry");
13
13
 
14
14
  const packageJson = require("../package.json");
@@ -392,11 +392,41 @@ async function installInteractiveSelection(manifest, choices, agent, selectedInd
392
392
  throw new Error("No skills matched the selected groups.");
393
393
  }
394
394
  output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for ${agent}:user...\n`);
395
- output.write(`Preparing skill source from ${manifest.source}#${manifest.ref}...\n`);
396
- output.write(`${renderInstallForSkills(skills, { agent, scope: "user" })}\n`);
395
+ const sourceRoot = await withSpinner(output, `Preparing skill source from ${manifest.source}#${manifest.ref}`, () =>
396
+ ensureSkillSourceRootAsync(manifest, { env: process.env, skills }),
397
+ );
398
+ output.write(`${renderInstallForSkills(skills, { agent, scope: "user" }, sourceRoot)}\n`);
397
399
  output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" })}\n`);
398
400
  }
399
401
 
402
+ async function withSpinner(output, message, task, options = {}) {
403
+ if (!output.isTTY) {
404
+ output.write(`${message}...\n`);
405
+ return task();
406
+ }
407
+
408
+ const frames = options.frames || ["|", "/", "-", "\\"];
409
+ const intervalMs = options.intervalMs || 120;
410
+ let index = 0;
411
+ const render = () => {
412
+ output.write(`\r${frames[index % frames.length]} ${message}...`);
413
+ index += 1;
414
+ };
415
+
416
+ render();
417
+ const timer = setInterval(render, intervalMs);
418
+ try {
419
+ const result = await task();
420
+ clearInterval(timer);
421
+ output.write(`\r${" ".repeat(message.length + 8)}\r✓ ${message}\n`);
422
+ return result;
423
+ } catch (error) {
424
+ clearInterval(timer);
425
+ output.write(`\r${" ".repeat(message.length + 8)}\r! ${message}\n`);
426
+ throw error;
427
+ }
428
+ }
429
+
400
430
  function readAll(input) {
401
431
  return new Promise((resolve, reject) => {
402
432
  let text = "";
@@ -576,6 +606,7 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
576
606
  function runRawAgentSelection(input, output) {
577
607
  return new Promise((resolve, reject) => {
578
608
  let cursor = 0;
609
+ let selected = 0;
579
610
  const wasRaw = input.isRaw;
580
611
 
581
612
  readline.emitKeypressEvents(input);
@@ -583,7 +614,7 @@ function runRawAgentSelection(input, output) {
583
614
 
584
615
  function render() {
585
616
  output.write("\x1b[2J\x1b[H");
586
- output.write(renderRawAgentMenu(cursor));
617
+ output.write(renderRawAgentMenu(cursor, selected));
587
618
  }
588
619
 
589
620
  function cleanup() {
@@ -608,9 +639,14 @@ function runRawAgentSelection(input, output) {
608
639
  render();
609
640
  return;
610
641
  }
611
- if (key && (key.name === "space" || key.name === "return")) {
642
+ if (key && key.name === "space") {
643
+ selected = cursor;
644
+ render();
645
+ return;
646
+ }
647
+ if (key && key.name === "return") {
612
648
  cleanup();
613
- resolve(AGENT_CHOICES[cursor].agent);
649
+ resolve(AGENT_CHOICES[selected].agent);
614
650
  }
615
651
  }
616
652
 
@@ -623,6 +659,7 @@ function runRawAgentSelection(input, output) {
623
659
  function runRawActionSelection(input, output) {
624
660
  return new Promise((resolve, reject) => {
625
661
  let cursor = 0;
662
+ let selected = 0;
626
663
  const wasRaw = input.isRaw;
627
664
 
628
665
  readline.emitKeypressEvents(input);
@@ -630,7 +667,7 @@ function runRawActionSelection(input, output) {
630
667
 
631
668
  function render() {
632
669
  output.write("\x1b[2J\x1b[H");
633
- output.write(renderRawActionMenu(cursor));
670
+ output.write(renderRawActionMenu(cursor, selected));
634
671
  }
635
672
 
636
673
  function cleanup() {
@@ -655,9 +692,14 @@ function runRawActionSelection(input, output) {
655
692
  render();
656
693
  return;
657
694
  }
658
- if (key && (key.name === "space" || key.name === "return")) {
695
+ if (key && key.name === "space") {
696
+ selected = cursor;
697
+ render();
698
+ return;
699
+ }
700
+ if (key && key.name === "return") {
659
701
  cleanup();
660
- resolve(ACTION_CHOICES[cursor].action);
702
+ resolve(ACTION_CHOICES[selected].action);
661
703
  }
662
704
  }
663
705
 
@@ -667,7 +709,7 @@ function runRawActionSelection(input, output) {
667
709
  });
668
710
  }
669
711
 
670
- function renderRawActionMenu(cursor) {
712
+ function renderRawActionMenu(cursor, selected = cursor) {
671
713
  const width = 76;
672
714
  const line = `+${"-".repeat(width - 2)}+`;
673
715
  const lines = [
@@ -677,20 +719,20 @@ function renderRawActionMenu(cursor) {
677
719
  line,
678
720
  "",
679
721
  colorize("Choose action", ANSI.bold),
680
- colorize(" Up/Down or j/k: move Space/Enter: select Ctrl+C: cancel", ANSI.dim),
722
+ colorize(" Up/Down or j/k: move Space: select Enter: confirm Ctrl+C: cancel", ANSI.dim),
681
723
  "",
682
724
  ];
683
725
  ACTION_CHOICES.forEach((choice, index) => {
684
726
  const pointer = index === cursor ? ">" : " ";
685
727
  const highlighted = index === cursor;
686
- const row = `${pointer} ${rawCheckbox(highlighted, highlighted)} ${choice.label.padEnd(22, " ")} ${choice.hint}`;
728
+ const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(22, " ")} ${choice.hint}`;
687
729
  lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
688
730
  });
689
731
  lines.push("");
690
732
  return `${lines.join("\n")}\n`;
691
733
  }
692
734
 
693
- function renderRawAgentMenu(cursor) {
735
+ function renderRawAgentMenu(cursor, selected = cursor) {
694
736
  const width = 76;
695
737
  const line = `+${"-".repeat(width - 2)}+`;
696
738
  const lines = [
@@ -700,13 +742,13 @@ function renderRawAgentMenu(cursor) {
700
742
  line,
701
743
  "",
702
744
  colorize("Choose install target", ANSI.bold),
703
- colorize(" Up/Down or j/k: move Space/Enter: select Ctrl+C: cancel", ANSI.dim),
745
+ colorize(" Up/Down or j/k: move Space: select Enter: confirm Ctrl+C: cancel", ANSI.dim),
704
746
  "",
705
747
  ];
706
748
  AGENT_CHOICES.forEach((choice, index) => {
707
749
  const pointer = index === cursor ? ">" : " ";
708
750
  const highlighted = index === cursor;
709
- const row = `${pointer} ${rawCheckbox(highlighted, highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
751
+ const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
710
752
  lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
711
753
  });
712
754
  lines.push("");
@@ -906,7 +948,7 @@ function selectSkillsForChoices(manifest, choices) {
906
948
  return Array.from(selected.values()).sort((left, right) => left.name.localeCompare(right.name));
907
949
  }
908
950
 
909
- function renderInstallForSkills(skills, targetOptions) {
951
+ function renderInstallForSkills(skills, targetOptions, sourceRootOverride = null) {
910
952
  const targets = resolveAgentTargets({
911
953
  agent: targetOptions.agent,
912
954
  scope: targetOptions.scope,
@@ -916,7 +958,7 @@ function renderInstallForSkills(skills, targetOptions) {
916
958
  });
917
959
  const plan = planInstall(skills, targets);
918
960
  const manifest = loadLocalManifest();
919
- const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills });
961
+ const sourceRoot = sourceRootOverride || ensureSkillSourceRoot(manifest, { env: process.env, skills });
920
962
  const applied = applyInstallPlan(plan, { sourceRoot });
921
963
  const telemetryEnabled = process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
922
964
  for (const operation of applied) {
@@ -1036,5 +1078,6 @@ module.exports = {
1036
1078
  parseSelection,
1037
1079
  renderRawTuiMenu,
1038
1080
  renderTuiMenu,
1081
+ withSpinner,
1039
1082
  selectSkillsForChoices,
1040
1083
  };
package/src/source.js CHANGED
@@ -1,4 +1,4 @@
1
- const { spawnSync } = require("node:child_process");
1
+ const { spawn, spawnSync } = require("node:child_process");
2
2
  const crypto = require("node:crypto");
3
3
  const fs = require("node:fs");
4
4
  const os = require("node:os");
@@ -48,6 +48,61 @@ function ensureSkillSourceRoot(manifest, options = {}) {
48
48
  return checkout;
49
49
  }
50
50
 
51
+ async function ensureSkillSourceRootAsync(manifest, options = {}) {
52
+ const env = options.env || process.env;
53
+ if (env.OH_SKILLHUB_SOURCE_DIR) {
54
+ return requireDirectory(env.OH_SKILLHUB_SOURCE_DIR, "OH_SKILLHUB_SOURCE_DIR");
55
+ }
56
+
57
+ const source = manifest.source;
58
+ const ref = manifest.ref || "release";
59
+ if (!source) {
60
+ throw new Error("Manifest does not define a skill source repository.");
61
+ }
62
+
63
+ const cacheRoot = env.OH_SKILLHUB_CACHE_DIR || path.join(os.homedir(), ".oh-skillhub", "cache");
64
+ const sparsePaths = selectedSparsePaths(options.skills);
65
+ const checkout = path.join(cacheRoot, `${repositorySlug(source)}-${sanitize(ref)}-${sparseKey(sparsePaths)}`);
66
+ if (isUsableCheckout(checkout, sparsePaths)) {
67
+ return checkout;
68
+ }
69
+
70
+ fs.mkdirSync(cacheRoot, { recursive: true });
71
+ fs.rmSync(checkout, { recursive: true, force: true });
72
+ const tempCheckout = path.join(cacheRoot, `.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`);
73
+ const clone = await runGitAsync([
74
+ "-c",
75
+ "core.longpaths=true",
76
+ "clone",
77
+ "--depth",
78
+ "1",
79
+ "--branch",
80
+ ref,
81
+ "--no-checkout",
82
+ source,
83
+ tempCheckout,
84
+ ]);
85
+ if (clone.status !== 0) {
86
+ fs.rmSync(tempCheckout, { recursive: true, force: true });
87
+ const detail = gitDetail(clone);
88
+ throw new Error(`Failed to download skill source from ${source}#${ref}.${detail ? ` ${detail}` : ""}`);
89
+ }
90
+ const sparseInit = await runGitAsync(["-C", tempCheckout, "-c", "core.longpaths=true", "sparse-checkout", "init", "--no-cone"]);
91
+ const sparseSet = sparseInit.status === 0
92
+ ? await runGitAsync(["-C", tempCheckout, "-c", "core.longpaths=true", "sparse-checkout", "set", ...sparsePaths])
93
+ : sparseInit;
94
+ const checkoutResult = sparseSet.status === 0
95
+ ? await runGitAsync(["-C", tempCheckout, "-c", "core.longpaths=true", "checkout"])
96
+ : sparseSet;
97
+ if (checkoutResult.status !== 0) {
98
+ fs.rmSync(tempCheckout, { recursive: true, force: true });
99
+ const detail = gitDetail(checkoutResult);
100
+ throw new Error(`Failed to checkout selected skill source from ${source}#${ref}.${detail ? ` ${detail}` : ""}`);
101
+ }
102
+ fs.renameSync(tempCheckout, checkout);
103
+ return checkout;
104
+ }
105
+
51
106
  function requireDirectory(value, label) {
52
107
  const dir = path.resolve(value);
53
108
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
@@ -82,10 +137,33 @@ function runGit(args) {
82
137
  return spawnSync("git", args, { encoding: "utf8", shell: false });
83
138
  }
84
139
 
140
+ function runGitAsync(args) {
141
+ return new Promise((resolve) => {
142
+ const child = spawn("git", args, { shell: false });
143
+ let stdout = "";
144
+ let stderr = "";
145
+ child.stdout.setEncoding("utf8");
146
+ child.stderr.setEncoding("utf8");
147
+ child.stdout.on("data", (chunk) => {
148
+ stdout += chunk;
149
+ });
150
+ child.stderr.on("data", (chunk) => {
151
+ stderr += chunk;
152
+ });
153
+ child.on("error", (error) => {
154
+ resolve({ status: 1, stdout, stderr: `${stderr}${error.message}` });
155
+ });
156
+ child.on("close", (status) => {
157
+ resolve({ status, stdout, stderr });
158
+ });
159
+ });
160
+ }
161
+
85
162
  function gitDetail(result) {
86
163
  return (result.stderr || result.stdout || "").trim();
87
164
  }
88
165
 
89
166
  module.exports = {
90
167
  ensureSkillSourceRoot,
168
+ ensureSkillSourceRootAsync,
91
169
  };