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 +1 -1
- package/src/cli.js +36 -5
- package/src/source.js +79 -1
package/package.json
CHANGED
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
|
|
396
|
-
|
|
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
|
};
|