oh-skillhub 0.1.13 → 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 +60 -17
- 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 = "";
|
|
@@ -576,6 +606,7 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
|
|
|
576
606
|
function runRawAgentSelection(input, output) {
|
|
577
607
|
return new Promise((resolve, reject) => {
|
|
578
608
|
let cursor = 0;
|
|
609
|
+
let selected = 0;
|
|
579
610
|
const wasRaw = input.isRaw;
|
|
580
611
|
|
|
581
612
|
readline.emitKeypressEvents(input);
|
|
@@ -583,7 +614,7 @@ function runRawAgentSelection(input, output) {
|
|
|
583
614
|
|
|
584
615
|
function render() {
|
|
585
616
|
output.write("\x1b[2J\x1b[H");
|
|
586
|
-
output.write(renderRawAgentMenu(cursor));
|
|
617
|
+
output.write(renderRawAgentMenu(cursor, selected));
|
|
587
618
|
}
|
|
588
619
|
|
|
589
620
|
function cleanup() {
|
|
@@ -608,9 +639,14 @@ function runRawAgentSelection(input, output) {
|
|
|
608
639
|
render();
|
|
609
640
|
return;
|
|
610
641
|
}
|
|
611
|
-
if (key &&
|
|
642
|
+
if (key && key.name === "space") {
|
|
643
|
+
selected = cursor;
|
|
644
|
+
render();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (key && key.name === "return") {
|
|
612
648
|
cleanup();
|
|
613
|
-
resolve(AGENT_CHOICES[
|
|
649
|
+
resolve(AGENT_CHOICES[selected].agent);
|
|
614
650
|
}
|
|
615
651
|
}
|
|
616
652
|
|
|
@@ -623,6 +659,7 @@ function runRawAgentSelection(input, output) {
|
|
|
623
659
|
function runRawActionSelection(input, output) {
|
|
624
660
|
return new Promise((resolve, reject) => {
|
|
625
661
|
let cursor = 0;
|
|
662
|
+
let selected = 0;
|
|
626
663
|
const wasRaw = input.isRaw;
|
|
627
664
|
|
|
628
665
|
readline.emitKeypressEvents(input);
|
|
@@ -630,7 +667,7 @@ function runRawActionSelection(input, output) {
|
|
|
630
667
|
|
|
631
668
|
function render() {
|
|
632
669
|
output.write("\x1b[2J\x1b[H");
|
|
633
|
-
output.write(renderRawActionMenu(cursor));
|
|
670
|
+
output.write(renderRawActionMenu(cursor, selected));
|
|
634
671
|
}
|
|
635
672
|
|
|
636
673
|
function cleanup() {
|
|
@@ -655,9 +692,14 @@ function runRawActionSelection(input, output) {
|
|
|
655
692
|
render();
|
|
656
693
|
return;
|
|
657
694
|
}
|
|
658
|
-
if (key &&
|
|
695
|
+
if (key && key.name === "space") {
|
|
696
|
+
selected = cursor;
|
|
697
|
+
render();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (key && key.name === "return") {
|
|
659
701
|
cleanup();
|
|
660
|
-
resolve(ACTION_CHOICES[
|
|
702
|
+
resolve(ACTION_CHOICES[selected].action);
|
|
661
703
|
}
|
|
662
704
|
}
|
|
663
705
|
|
|
@@ -667,7 +709,7 @@ function runRawActionSelection(input, output) {
|
|
|
667
709
|
});
|
|
668
710
|
}
|
|
669
711
|
|
|
670
|
-
function renderRawActionMenu(cursor) {
|
|
712
|
+
function renderRawActionMenu(cursor, selected = cursor) {
|
|
671
713
|
const width = 76;
|
|
672
714
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
673
715
|
const lines = [
|
|
@@ -677,20 +719,20 @@ function renderRawActionMenu(cursor) {
|
|
|
677
719
|
line,
|
|
678
720
|
"",
|
|
679
721
|
colorize("Choose action", ANSI.bold),
|
|
680
|
-
colorize(" Up/Down or j/k: move Space
|
|
722
|
+
colorize(" Up/Down or j/k: move Space: select Enter: confirm Ctrl+C: cancel", ANSI.dim),
|
|
681
723
|
"",
|
|
682
724
|
];
|
|
683
725
|
ACTION_CHOICES.forEach((choice, index) => {
|
|
684
726
|
const pointer = index === cursor ? ">" : " ";
|
|
685
727
|
const highlighted = index === cursor;
|
|
686
|
-
const row = `${pointer} ${rawCheckbox(
|
|
728
|
+
const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(22, " ")} ${choice.hint}`;
|
|
687
729
|
lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
|
|
688
730
|
});
|
|
689
731
|
lines.push("");
|
|
690
732
|
return `${lines.join("\n")}\n`;
|
|
691
733
|
}
|
|
692
734
|
|
|
693
|
-
function renderRawAgentMenu(cursor) {
|
|
735
|
+
function renderRawAgentMenu(cursor, selected = cursor) {
|
|
694
736
|
const width = 76;
|
|
695
737
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
696
738
|
const lines = [
|
|
@@ -700,13 +742,13 @@ function renderRawAgentMenu(cursor) {
|
|
|
700
742
|
line,
|
|
701
743
|
"",
|
|
702
744
|
colorize("Choose install target", ANSI.bold),
|
|
703
|
-
colorize(" Up/Down or j/k: move Space
|
|
745
|
+
colorize(" Up/Down or j/k: move Space: select Enter: confirm Ctrl+C: cancel", ANSI.dim),
|
|
704
746
|
"",
|
|
705
747
|
];
|
|
706
748
|
AGENT_CHOICES.forEach((choice, index) => {
|
|
707
749
|
const pointer = index === cursor ? ">" : " ";
|
|
708
750
|
const highlighted = index === cursor;
|
|
709
|
-
const row = `${pointer} ${rawCheckbox(
|
|
751
|
+
const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
|
|
710
752
|
lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
|
|
711
753
|
});
|
|
712
754
|
lines.push("");
|
|
@@ -906,7 +948,7 @@ function selectSkillsForChoices(manifest, choices) {
|
|
|
906
948
|
return Array.from(selected.values()).sort((left, right) => left.name.localeCompare(right.name));
|
|
907
949
|
}
|
|
908
950
|
|
|
909
|
-
function renderInstallForSkills(skills, targetOptions) {
|
|
951
|
+
function renderInstallForSkills(skills, targetOptions, sourceRootOverride = null) {
|
|
910
952
|
const targets = resolveAgentTargets({
|
|
911
953
|
agent: targetOptions.agent,
|
|
912
954
|
scope: targetOptions.scope,
|
|
@@ -916,7 +958,7 @@ function renderInstallForSkills(skills, targetOptions) {
|
|
|
916
958
|
});
|
|
917
959
|
const plan = planInstall(skills, targets);
|
|
918
960
|
const manifest = loadLocalManifest();
|
|
919
|
-
const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills });
|
|
961
|
+
const sourceRoot = sourceRootOverride || ensureSkillSourceRoot(manifest, { env: process.env, skills });
|
|
920
962
|
const applied = applyInstallPlan(plan, { sourceRoot });
|
|
921
963
|
const telemetryEnabled = process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
|
|
922
964
|
for (const operation of applied) {
|
|
@@ -1036,5 +1078,6 @@ module.exports = {
|
|
|
1036
1078
|
parseSelection,
|
|
1037
1079
|
renderRawTuiMenu,
|
|
1038
1080
|
renderTuiMenu,
|
|
1081
|
+
withSpinner,
|
|
1039
1082
|
selectSkillsForChoices,
|
|
1040
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
|
};
|