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.
- package/bin/oh-skillhub.js +2 -2
- package/package.json +1 -1
- package/src/cli.js +52 -9
- package/src/source.js +79 -1
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
|
@@ -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
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
957
|
-
|
|
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
|
};
|