oh-skillhub 0.1.14 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.14",
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 = "";
@@ -918,7 +948,7 @@ function selectSkillsForChoices(manifest, choices) {
918
948
  return Array.from(selected.values()).sort((left, right) => left.name.localeCompare(right.name));
919
949
  }
920
950
 
921
- function renderInstallForSkills(skills, targetOptions) {
951
+ function renderInstallForSkills(skills, targetOptions, sourceRootOverride = null) {
922
952
  const targets = resolveAgentTargets({
923
953
  agent: targetOptions.agent,
924
954
  scope: targetOptions.scope,
@@ -928,7 +958,7 @@ function renderInstallForSkills(skills, targetOptions) {
928
958
  });
929
959
  const plan = planInstall(skills, targets);
930
960
  const manifest = loadLocalManifest();
931
- const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills });
961
+ const sourceRoot = sourceRootOverride || ensureSkillSourceRoot(manifest, { env: process.env, skills });
932
962
  const applied = applyInstallPlan(plan, { sourceRoot });
933
963
  const telemetryEnabled = process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
934
964
  for (const operation of applied) {
@@ -1048,5 +1078,6 @@ module.exports = {
1048
1078
  parseSelection,
1049
1079
  renderRawTuiMenu,
1050
1080
  renderTuiMenu,
1081
+ withSpinner,
1051
1082
  selectSkillsForChoices,
1052
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
  };