oh-skillhub 0.1.14 → 0.1.16

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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { main } = require("../src/cli");
3
+ const { main, renderStatusLine } = require("../src/cli");
4
4
 
5
5
  main(process.argv.slice(2)).catch((error) => {
6
- console.error(error.message);
6
+ process.stderr.write(`${renderStatusLine("failure", "INSTALL FAILED", error.message, process.stderr)}\n`);
7
7
  process.exitCode = 1;
8
8
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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");
@@ -29,6 +29,7 @@ const ANSI = {
29
29
  dim: "\x1b[2m",
30
30
  cyan: "\x1b[36m",
31
31
  green: "\x1b[32m",
32
+ red: "\x1b[31m",
32
33
  reverse: "\x1b[7m",
33
34
  };
34
35
  const AGENT_CHOICES = [
@@ -392,9 +393,39 @@ async function installInteractiveSelection(manifest, choices, agent, selectedInd
392
393
  throw new Error("No skills matched the selected groups.");
393
394
  }
394
395
  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`);
397
- output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" })}\n`);
396
+ const sourceRoot = await withSpinner(output, `Preparing skill source from ${manifest.source}#${manifest.ref}`, () =>
397
+ ensureSkillSourceRootAsync(manifest, { env: process.env, skills }),
398
+ );
399
+ output.write(`${renderInstallForSkills(skills, { agent, scope: "user" }, sourceRoot)}\n`);
400
+ output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" }, output)}\n`);
401
+ }
402
+
403
+ async function withSpinner(output, message, task, options = {}) {
404
+ if (!output.isTTY) {
405
+ output.write(`${message}...\n`);
406
+ return task();
407
+ }
408
+
409
+ const frames = options.frames || ["|", "/", "-", "\\"];
410
+ const intervalMs = options.intervalMs || 120;
411
+ let index = 0;
412
+ const render = () => {
413
+ output.write(`\r${frames[index % frames.length]} ${message}...`);
414
+ index += 1;
415
+ };
416
+
417
+ render();
418
+ const timer = setInterval(render, intervalMs);
419
+ try {
420
+ const result = await task();
421
+ clearInterval(timer);
422
+ output.write(`\r${" ".repeat(message.length + 8)}\r✓ ${message}\n`);
423
+ return result;
424
+ } catch (error) {
425
+ clearInterval(timer);
426
+ output.write(`\r${" ".repeat(message.length + 8)}\r! ${message}\n`);
427
+ throw error;
428
+ }
398
429
  }
399
430
 
400
431
  function readAll(input) {
@@ -918,7 +949,7 @@ function selectSkillsForChoices(manifest, choices) {
918
949
  return Array.from(selected.values()).sort((left, right) => left.name.localeCompare(right.name));
919
950
  }
920
951
 
921
- function renderInstallForSkills(skills, targetOptions) {
952
+ function renderInstallForSkills(skills, targetOptions, sourceRootOverride = null) {
922
953
  const targets = resolveAgentTargets({
923
954
  agent: targetOptions.agent,
924
955
  scope: targetOptions.scope,
@@ -928,7 +959,7 @@ function renderInstallForSkills(skills, targetOptions) {
928
959
  });
929
960
  const plan = planInstall(skills, targets);
930
961
  const manifest = loadLocalManifest();
931
- const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills });
962
+ const sourceRoot = sourceRootOverride || ensureSkillSourceRoot(manifest, { env: process.env, skills });
932
963
  const applied = applyInstallPlan(plan, { sourceRoot });
933
964
  const telemetryEnabled = process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
934
965
  for (const operation of applied) {
@@ -950,14 +981,24 @@ function renderInstallForSkills(skills, targetOptions) {
950
981
  return renderInstallPlan("Install summary", plan);
951
982
  }
952
983
 
953
- function renderInteractiveCompletion(skills, targetOptions) {
984
+ function renderInteractiveCompletion(skills, targetOptions, output = process.stdout) {
954
985
  return [
955
986
  "",
956
- `Done. Installed ${skills.length} skill(s) for ${targetOptions.agent}:${targetOptions.scope}.`,
957
- "You can close this terminal or start using the installed skills.",
987
+ renderStatusLine(
988
+ "success",
989
+ "INSTALL SUCCESS",
990
+ `Installed ${skills.length} skill(s) for ${targetOptions.agent}:${targetOptions.scope}.`,
991
+ output,
992
+ ),
958
993
  ].join("\n");
959
994
  }
960
995
 
996
+ function renderStatusLine(kind, title, detail, output = process.stdout) {
997
+ const color = kind === "failure" ? ANSI.red : ANSI.green;
998
+ const heading = output.isTTY ? colorize(title, color, ANSI.bold) : title;
999
+ return detail ? `${heading}\n${detail}` : heading;
1000
+ }
1001
+
961
1002
  function renderInstallPlan(title, plan) {
962
1003
  const lines = [title];
963
1004
  for (const operation of plan.operations) {
@@ -1038,6 +1079,7 @@ module.exports = {
1038
1079
  renderInstall,
1039
1080
  renderList,
1040
1081
  renderTelemetry,
1082
+ renderStatusLine,
1041
1083
  buildRepositoryChoices,
1042
1084
  renderAgentMenu,
1043
1085
  renderActionMenu,
@@ -1048,5 +1090,6 @@ module.exports = {
1048
1090
  parseSelection,
1049
1091
  renderRawTuiMenu,
1050
1092
  renderTuiMenu,
1093
+ withSpinner,
1051
1094
  selectSkillsForChoices,
1052
1095
  };
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
  };