oh-skillhub 0.1.16 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/source.js +65 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.16",
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/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
  };