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.
- package/bin/oh-skillhub.js +2 -2
- package/package.json +1 -1
- package/src/cli.js +16 -4
- package/src/source.js +65 -2
package/bin/oh-skillhub.js
CHANGED
|
@@ -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
|
-
|
|
6
|
+
process.stderr.write(`${renderStatusLine("failure", "INSTALL FAILED", error.message, process.stderr)}\n`);
|
|
7
7
|
process.exitCode = 1;
|
|
8
8
|
});
|
package/package.json
CHANGED
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
|
-
|
|
987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|