oh-skillhub 0.1.15 → 0.1.17

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.15",
3
+ "version": "0.1.17",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -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 = [
@@ -396,7 +397,7 @@ async function installInteractiveSelection(manifest, choices, agent, selectedInd
396
397
  ensureSkillSourceRootAsync(manifest, { env: process.env, skills }),
397
398
  );
398
399
  output.write(`${renderInstallForSkills(skills, { agent, scope: "user" }, sourceRoot)}\n`);
399
- output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" })}\n`);
400
+ output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" }, output)}\n`);
400
401
  }
401
402
 
402
403
  async function withSpinner(output, message, task, options = {}) {
@@ -980,14 +981,24 @@ function renderInstallForSkills(skills, targetOptions, sourceRootOverride = null
980
981
  return renderInstallPlan("Install summary", plan);
981
982
  }
982
983
 
983
- function renderInteractiveCompletion(skills, targetOptions) {
984
+ function renderInteractiveCompletion(skills, targetOptions, output = process.stdout) {
984
985
  return [
985
986
  "",
986
- `Done. Installed ${skills.length} skill(s) for ${targetOptions.agent}:${targetOptions.scope}.`,
987
- "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
+ ),
988
993
  ].join("\n");
989
994
  }
990
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
+
991
1002
  function renderInstallPlan(title, plan) {
992
1003
  const lines = [title];
993
1004
  for (const operation of plan.operations) {
@@ -1068,6 +1079,7 @@ module.exports = {
1068
1079
  renderInstall,
1069
1080
  renderList,
1070
1081
  renderTelemetry,
1082
+ renderStatusLine,
1071
1083
  buildRepositoryChoices,
1072
1084
  renderAgentMenu,
1073
1085
  renderActionMenu,
package/src/source.js CHANGED
@@ -4,6 +4,10 @@ const fs = require("node:fs");
4
4
  const os = require("node:os");
5
5
  const path = require("node:path");
6
6
 
7
+ const DEFAULT_MOVE_RETRIES = 8;
8
+ const DEFAULT_MOVE_DELAY_MS = 120;
9
+ const RETRYABLE_MOVE_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
10
+
7
11
  function ensureSkillSourceRoot(manifest, options = {}) {
8
12
  const env = options.env || process.env;
9
13
  if (env.OH_SKILLHUB_SOURCE_DIR) {
@@ -44,7 +48,7 @@ function ensureSkillSourceRoot(manifest, options = {}) {
44
48
  const detail = gitDetail(checkoutResult);
45
49
  throw new Error(`Failed to checkout selected skill source from ${source}#${ref}.${detail ? ` ${detail}` : ""}`);
46
50
  }
47
- fs.renameSync(tempCheckout, checkout);
51
+ moveCheckoutIntoCache(tempCheckout, checkout);
48
52
  return checkout;
49
53
  }
50
54
 
@@ -99,10 +103,44 @@ async function ensureSkillSourceRootAsync(manifest, options = {}) {
99
103
  const detail = gitDetail(checkoutResult);
100
104
  throw new Error(`Failed to checkout selected skill source from ${source}#${ref}.${detail ? ` ${detail}` : ""}`);
101
105
  }
102
- fs.renameSync(tempCheckout, checkout);
106
+ await moveCheckoutIntoCacheAsync(tempCheckout, checkout);
103
107
  return checkout;
104
108
  }
105
109
 
110
+ function moveCheckoutIntoCache(tempCheckout, checkout, options = {}) {
111
+ const renameSync = options.renameSync || fs.renameSync;
112
+ const retries = options.retries ?? DEFAULT_MOVE_RETRIES;
113
+ const delayMs = options.delayMs ?? DEFAULT_MOVE_DELAY_MS;
114
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
115
+ try {
116
+ renameSync(tempCheckout, checkout);
117
+ return;
118
+ } catch (error) {
119
+ if (!isRetryableMoveError(error) || attempt === retries) {
120
+ throw cacheMoveError(error, tempCheckout, checkout);
121
+ }
122
+ sleepSync(delayMs * (attempt + 1));
123
+ }
124
+ }
125
+ }
126
+
127
+ async function moveCheckoutIntoCacheAsync(tempCheckout, checkout, options = {}) {
128
+ const renameSync = options.renameSync || fs.renameSync;
129
+ const retries = options.retries ?? DEFAULT_MOVE_RETRIES;
130
+ const delayMs = options.delayMs ?? DEFAULT_MOVE_DELAY_MS;
131
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
132
+ try {
133
+ renameSync(tempCheckout, checkout);
134
+ return;
135
+ } catch (error) {
136
+ if (!isRetryableMoveError(error) || attempt === retries) {
137
+ throw cacheMoveError(error, tempCheckout, checkout);
138
+ }
139
+ await sleep(delayMs * (attempt + 1));
140
+ }
141
+ }
142
+ }
143
+
106
144
  function requireDirectory(value, label) {
107
145
  const dir = path.resolve(value);
108
146
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
@@ -163,7 +201,32 @@ function gitDetail(result) {
163
201
  return (result.stderr || result.stdout || "").trim();
164
202
  }
165
203
 
204
+ function isRetryableMoveError(error) {
205
+ return RETRYABLE_MOVE_CODES.has(error && error.code);
206
+ }
207
+
208
+ function cacheMoveError(error, tempCheckout, checkout) {
209
+ const wrapped = new Error(
210
+ `Failed to finalize skill source cache because the checkout directory is busy or locked. ` +
211
+ `Close terminals/editors using the cache and retry. ${tempCheckout} -> ${checkout}. ${error.message}`,
212
+ );
213
+ wrapped.code = error.code;
214
+ wrapped.cause = error;
215
+ return wrapped;
216
+ }
217
+
218
+ function sleep(ms) {
219
+ return new Promise((resolve) => setTimeout(resolve, ms));
220
+ }
221
+
222
+ function sleepSync(ms) {
223
+ if (ms <= 0) return;
224
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
225
+ }
226
+
166
227
  module.exports = {
167
228
  ensureSkillSourceRoot,
168
229
  ensureSkillSourceRootAsync,
230
+ moveCheckoutIntoCache,
231
+ moveCheckoutIntoCacheAsync,
169
232
  };