oh-skillhub 0.1.19 → 0.1.21
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 +64 -17
- package/src/manifest.js +231 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ const readlinePromises = require("node:readline/promises");
|
|
|
6
6
|
|
|
7
7
|
const { resolveAgentTargets } = require("./agents");
|
|
8
8
|
const { applyCleanPlan, planClean, scanInstalledSkills } = require("./cleaner");
|
|
9
|
-
const { loadLocalManifest, loadProfiles, selectSkills } = require("./manifest");
|
|
9
|
+
const { loadLocalManifest, loadProfiles, loadRuntimeManifest, selectSkills } = require("./manifest");
|
|
10
10
|
const { applyInstallPlan, planInstall } = require("./planner");
|
|
11
11
|
const { ensureSkillSourceRoot, ensureSkillSourceRootAsync } = require("./source");
|
|
12
12
|
const { buildTelemetryEvent, enqueueTelemetryEvent, telemetryStatus } = require("./telemetry");
|
|
@@ -368,7 +368,7 @@ async function runInteractiveInstaller(input = process.stdin, output = process.s
|
|
|
368
368
|
return;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
const manifest =
|
|
371
|
+
const manifest = await loadInteractiveManifest(input, output);
|
|
372
372
|
const choices = buildRepositoryChoices(manifest);
|
|
373
373
|
const agent = await runRawAgentSelection(input, output);
|
|
374
374
|
const selectedIndexes = await runRawTuiSelection(input, output, choices, agent);
|
|
@@ -376,7 +376,7 @@ async function runInteractiveInstaller(input = process.stdin, output = process.s
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
async function runInteractiveInstallerFromAnswers(answers, output = process.stdout) {
|
|
379
|
-
const manifest =
|
|
379
|
+
const manifest = await loadRuntimeManifest({ env: process.env });
|
|
380
380
|
const choices = buildRepositoryChoices(manifest);
|
|
381
381
|
const [agentAnswer, groupAnswer] = answers.length > 1 ? [answers[0], answers.slice(1).join(" ")] : ["", answers[0] || ""];
|
|
382
382
|
output.write(renderAgentMenu());
|
|
@@ -388,6 +388,13 @@ async function runInteractiveInstallerFromAnswers(answers, output = process.stdo
|
|
|
388
388
|
await installInteractiveSelection(manifest, choices, agent, selectedIndexes, output);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
async function loadInteractiveManifest(_input, output) {
|
|
392
|
+
if (output.isTTY) {
|
|
393
|
+
return withSpinner(output, "Refreshing skill catalog", () => loadRuntimeManifest({ env: process.env }));
|
|
394
|
+
}
|
|
395
|
+
return loadRuntimeManifest({ env: process.env });
|
|
396
|
+
}
|
|
397
|
+
|
|
391
398
|
async function installInteractiveSelection(manifest, choices, agent, selectedIndexes, output) {
|
|
392
399
|
const selectedChoices = selectedIndexes.map((index) => choices[index]);
|
|
393
400
|
const skills = selectSkillsForChoices(manifest, selectedChoices);
|
|
@@ -555,14 +562,16 @@ async function runPromptSelection(input, output, choices, agent = "codex", exist
|
|
|
555
562
|
function runRawTuiSelection(input, output, choices, agent = "codex") {
|
|
556
563
|
return new Promise((resolve, reject) => {
|
|
557
564
|
let cursor = 8;
|
|
558
|
-
const selected = new Set(
|
|
565
|
+
const selected = new Set();
|
|
566
|
+
let notice = "";
|
|
559
567
|
const wasRaw = input.isRaw;
|
|
560
568
|
|
|
561
569
|
prepareRawInput(input);
|
|
562
570
|
|
|
563
571
|
function render() {
|
|
564
572
|
output.write("\x1b[2J\x1b[H");
|
|
565
|
-
output.write(renderRawTuiMenu(choices, cursor, selected, agent));
|
|
573
|
+
output.write(renderRawTuiMenu(choices, cursor, selected, agent, notice));
|
|
574
|
+
notice = "";
|
|
566
575
|
}
|
|
567
576
|
|
|
568
577
|
function cleanup() {
|
|
@@ -594,8 +603,13 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
|
|
|
594
603
|
return;
|
|
595
604
|
}
|
|
596
605
|
if (key && key.name === "return") {
|
|
606
|
+
if (!selected.size) {
|
|
607
|
+
notice = "Select at least one group with Space.";
|
|
608
|
+
render();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
597
611
|
cleanup();
|
|
598
|
-
resolve(
|
|
612
|
+
resolve(Array.from(selected).sort((a, b) => a - b));
|
|
599
613
|
}
|
|
600
614
|
}
|
|
601
615
|
|
|
@@ -608,14 +622,16 @@ function runRawTuiSelection(input, output, choices, agent = "codex") {
|
|
|
608
622
|
function runRawAgentSelection(input, output) {
|
|
609
623
|
return new Promise((resolve, reject) => {
|
|
610
624
|
let cursor = 0;
|
|
611
|
-
const selected = new Set(
|
|
625
|
+
const selected = new Set();
|
|
626
|
+
let notice = "";
|
|
612
627
|
const wasRaw = input.isRaw;
|
|
613
628
|
|
|
614
629
|
prepareRawInput(input);
|
|
615
630
|
|
|
616
631
|
function render() {
|
|
617
632
|
output.write("\x1b[2J\x1b[H");
|
|
618
|
-
output.write(renderRawAgentMenu(cursor, selected));
|
|
633
|
+
output.write(renderRawAgentMenu(cursor, selected, notice));
|
|
634
|
+
notice = "";
|
|
619
635
|
}
|
|
620
636
|
|
|
621
637
|
function cleanup() {
|
|
@@ -646,6 +662,11 @@ function runRawAgentSelection(input, output) {
|
|
|
646
662
|
return;
|
|
647
663
|
}
|
|
648
664
|
if (key && key.name === "return") {
|
|
665
|
+
if (!normalizeAgentIndexes(selected).size) {
|
|
666
|
+
notice = "Select at least one target with Space.";
|
|
667
|
+
render();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
649
670
|
cleanup();
|
|
650
671
|
resolve(agentSelectionFromIndexes(selected, cursor));
|
|
651
672
|
}
|
|
@@ -660,14 +681,16 @@ function runRawAgentSelection(input, output) {
|
|
|
660
681
|
function runRawActionSelection(input, output) {
|
|
661
682
|
return new Promise((resolve, reject) => {
|
|
662
683
|
let cursor = 0;
|
|
663
|
-
let selected =
|
|
684
|
+
let selected = null;
|
|
685
|
+
let notice = "";
|
|
664
686
|
const wasRaw = input.isRaw;
|
|
665
687
|
|
|
666
688
|
prepareRawInput(input);
|
|
667
689
|
|
|
668
690
|
function render() {
|
|
669
691
|
output.write("\x1b[2J\x1b[H");
|
|
670
|
-
output.write(renderRawActionMenu(cursor, selected));
|
|
692
|
+
output.write(renderRawActionMenu(cursor, selected, notice));
|
|
693
|
+
notice = "";
|
|
671
694
|
}
|
|
672
695
|
|
|
673
696
|
function cleanup() {
|
|
@@ -698,6 +721,11 @@ function runRawActionSelection(input, output) {
|
|
|
698
721
|
return;
|
|
699
722
|
}
|
|
700
723
|
if (key && key.name === "return") {
|
|
724
|
+
if (selected === null) {
|
|
725
|
+
notice = "Select an action with Space.";
|
|
726
|
+
render();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
701
729
|
cleanup();
|
|
702
730
|
resolve(ACTION_CHOICES[selected].action);
|
|
703
731
|
}
|
|
@@ -709,7 +737,7 @@ function runRawActionSelection(input, output) {
|
|
|
709
737
|
});
|
|
710
738
|
}
|
|
711
739
|
|
|
712
|
-
function renderRawActionMenu(cursor, selected =
|
|
740
|
+
function renderRawActionMenu(cursor, selected = null, notice = "") {
|
|
713
741
|
const width = 76;
|
|
714
742
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
715
743
|
const lines = [
|
|
@@ -728,6 +756,9 @@ function renderRawActionMenu(cursor, selected = cursor) {
|
|
|
728
756
|
const row = `${pointer} ${rawCheckbox(index === selected, highlighted)} ${choice.label.padEnd(22, " ")} ${choice.hint}`;
|
|
729
757
|
lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
|
|
730
758
|
});
|
|
759
|
+
if (notice) {
|
|
760
|
+
lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
|
|
761
|
+
}
|
|
731
762
|
lines.push("");
|
|
732
763
|
return `${lines.join("\n")}\n`;
|
|
733
764
|
}
|
|
@@ -749,7 +780,7 @@ function releaseRawInput(input, wasRaw) {
|
|
|
749
780
|
}
|
|
750
781
|
}
|
|
751
782
|
|
|
752
|
-
function renderRawAgentMenu(cursor, selected = new Set(
|
|
783
|
+
function renderRawAgentMenu(cursor, selected = new Set(), notice = "") {
|
|
753
784
|
const width = 76;
|
|
754
785
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
755
786
|
const selectedIndexes = normalizeAgentIndexes(selected);
|
|
@@ -769,11 +800,14 @@ function renderRawAgentMenu(cursor, selected = new Set([cursor])) {
|
|
|
769
800
|
const row = `${pointer} ${rawCheckbox(isAgentIndexSelected(selectedIndexes, index), highlighted)} ${choice.label.padEnd(10, " ")} ${choice.hint}`;
|
|
770
801
|
lines.push(index === cursor ? colorize(row, ANSI.reverse, ANSI.bold) : row);
|
|
771
802
|
});
|
|
803
|
+
if (notice) {
|
|
804
|
+
lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
|
|
805
|
+
}
|
|
772
806
|
lines.push("");
|
|
773
807
|
return `${lines.join("\n")}\n`;
|
|
774
808
|
}
|
|
775
809
|
|
|
776
|
-
function renderRawTuiMenu(choices, cursor, selected, agent = "codex") {
|
|
810
|
+
function renderRawTuiMenu(choices, cursor, selected, agent = "codex", notice = "") {
|
|
777
811
|
const width = 76;
|
|
778
812
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
779
813
|
const lines = [
|
|
@@ -800,6 +834,9 @@ function renderRawTuiMenu(choices, cursor, selected, agent = "codex") {
|
|
|
800
834
|
lines.push(colorize(` - ${leaf}`, ANSI.dim));
|
|
801
835
|
}
|
|
802
836
|
});
|
|
837
|
+
if (notice) {
|
|
838
|
+
lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
|
|
839
|
+
}
|
|
803
840
|
lines.push("");
|
|
804
841
|
return `${lines.join("\n")}\n`;
|
|
805
842
|
}
|
|
@@ -807,14 +844,16 @@ function renderRawTuiMenu(choices, cursor, selected, agent = "codex") {
|
|
|
807
844
|
function runRawCleanSelection(input, output, skills) {
|
|
808
845
|
return new Promise((resolve, reject) => {
|
|
809
846
|
let cursor = 0;
|
|
810
|
-
const selected = new Set(
|
|
847
|
+
const selected = new Set();
|
|
848
|
+
let notice = "";
|
|
811
849
|
const wasRaw = input.isRaw;
|
|
812
850
|
|
|
813
851
|
prepareRawInput(input);
|
|
814
852
|
|
|
815
853
|
function render() {
|
|
816
854
|
output.write("\x1b[2J\x1b[H");
|
|
817
|
-
output.write(renderRawCleanSelectionMenu(skills, cursor, selected));
|
|
855
|
+
output.write(renderRawCleanSelectionMenu(skills, cursor, selected, notice));
|
|
856
|
+
notice = "";
|
|
818
857
|
}
|
|
819
858
|
|
|
820
859
|
function cleanup() {
|
|
@@ -846,8 +885,13 @@ function runRawCleanSelection(input, output, skills) {
|
|
|
846
885
|
return;
|
|
847
886
|
}
|
|
848
887
|
if (key && key.name === "return") {
|
|
888
|
+
if (!selected.size) {
|
|
889
|
+
notice = "Select at least one skill with Space.";
|
|
890
|
+
render();
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
849
893
|
cleanup();
|
|
850
|
-
resolve(
|
|
894
|
+
resolve(Array.from(selected).sort((a, b) => a - b));
|
|
851
895
|
}
|
|
852
896
|
}
|
|
853
897
|
|
|
@@ -857,7 +901,7 @@ function runRawCleanSelection(input, output, skills) {
|
|
|
857
901
|
});
|
|
858
902
|
}
|
|
859
903
|
|
|
860
|
-
function renderRawCleanSelectionMenu(skills, cursor, selected) {
|
|
904
|
+
function renderRawCleanSelectionMenu(skills, cursor, selected, notice = "") {
|
|
861
905
|
const width = 92;
|
|
862
906
|
const line = `+${"-".repeat(width - 2)}+`;
|
|
863
907
|
const lines = [
|
|
@@ -878,6 +922,9 @@ function renderRawCleanSelectionMenu(skills, cursor, selected) {
|
|
|
878
922
|
const row = `${pointer} ${checkbox} ${skill.name.padEnd(36, " ")} ${skill.status.padEnd(9, " ")} ${group}`;
|
|
879
923
|
lines.push(highlighted ? colorize(row, ANSI.reverse, ANSI.bold) : row);
|
|
880
924
|
});
|
|
925
|
+
if (notice) {
|
|
926
|
+
lines.push("", colorize(notice, ANSI.cyan, ANSI.bold));
|
|
927
|
+
}
|
|
881
928
|
lines.push("");
|
|
882
929
|
return `${lines.join("\n")}\n`;
|
|
883
930
|
}
|
package/src/manifest.js
CHANGED
|
@@ -1,13 +1,242 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
1
4
|
const path = require("node:path");
|
|
2
5
|
|
|
3
6
|
function loadLocalManifest() {
|
|
4
7
|
return require(path.join(__dirname, "data", "manifest.json"));
|
|
5
8
|
}
|
|
6
9
|
|
|
10
|
+
async function loadRuntimeManifest(options = {}) {
|
|
11
|
+
const env = options.env || process.env;
|
|
12
|
+
const bundled = loadLocalManifest();
|
|
13
|
+
if (options.offline || env.OH_SKILLHUB_OFFLINE === "1") {
|
|
14
|
+
return loadCachedManifest(env) || bundled;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const manifest = env.OH_SKILLHUB_SOURCE_DIR
|
|
19
|
+
? buildManifestFromSourceTree(env.OH_SKILLHUB_SOURCE_DIR, bundled)
|
|
20
|
+
: await buildManifestFromGit(bundled, { env });
|
|
21
|
+
writeCachedManifest(manifest, env);
|
|
22
|
+
return manifest;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (env.OH_SKILLHUB_SOURCE_DIR) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return loadCachedManifest(env) || bundled;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
function loadProfiles() {
|
|
8
32
|
return require(path.join(__dirname, "data", "profiles.json"));
|
|
9
33
|
}
|
|
10
34
|
|
|
35
|
+
function buildManifestFromSourceTree(sourceRoot, baseManifest = loadLocalManifest()) {
|
|
36
|
+
const root = path.resolve(sourceRoot);
|
|
37
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
38
|
+
throw new Error(`OH_SKILLHUB_SOURCE_DIR is not a directory: ${root}`);
|
|
39
|
+
}
|
|
40
|
+
const skillFiles = findSkillFiles(path.join(root, "skills"));
|
|
41
|
+
if (!skillFiles.length) {
|
|
42
|
+
throw new Error(`No SKILL.md files found in source tree: ${root}`);
|
|
43
|
+
}
|
|
44
|
+
return buildManifestFromSkillFiles(root, skillFiles, baseManifest);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function buildManifestFromGit(baseManifest, options = {}) {
|
|
48
|
+
const source = baseManifest.source;
|
|
49
|
+
const ref = baseManifest.ref || "release";
|
|
50
|
+
const env = options.env || process.env;
|
|
51
|
+
const cacheRoot = env.OH_SKILLHUB_CACHE_DIR || path.join(os.homedir(), ".oh-skillhub", "cache");
|
|
52
|
+
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
53
|
+
const tempCheckout = path.join(cacheRoot, `.manifest-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
54
|
+
try {
|
|
55
|
+
const clone = await runGitAsync([
|
|
56
|
+
"-c",
|
|
57
|
+
"core.longpaths=true",
|
|
58
|
+
"clone",
|
|
59
|
+
"--depth",
|
|
60
|
+
"1",
|
|
61
|
+
"--branch",
|
|
62
|
+
ref,
|
|
63
|
+
"--no-checkout",
|
|
64
|
+
source,
|
|
65
|
+
tempCheckout,
|
|
66
|
+
]);
|
|
67
|
+
if (clone.status !== 0) {
|
|
68
|
+
throw new Error(gitDetail(clone) || `Failed to clone ${source}#${ref}`);
|
|
69
|
+
}
|
|
70
|
+
const tree = await runGitAsync(["-C", tempCheckout, "ls-tree", "-r", "--name-only", "HEAD", "skills"]);
|
|
71
|
+
if (tree.status !== 0) {
|
|
72
|
+
throw new Error(gitDetail(tree) || "Failed to list remote skills tree.");
|
|
73
|
+
}
|
|
74
|
+
const skillFiles = tree.stdout
|
|
75
|
+
.split(/\r?\n/)
|
|
76
|
+
.filter((item) => item.endsWith("/SKILL.md"))
|
|
77
|
+
.sort();
|
|
78
|
+
if (!skillFiles.length) {
|
|
79
|
+
throw new Error("Remote skills tree does not contain SKILL.md files.");
|
|
80
|
+
}
|
|
81
|
+
const loadedFiles = [];
|
|
82
|
+
for (const relativePath of skillFiles) {
|
|
83
|
+
const show = await runGitAsync(["-C", tempCheckout, "show", `HEAD:${relativePath}`]);
|
|
84
|
+
if (show.status === 0) {
|
|
85
|
+
loadedFiles.push({ relativePath, contents: show.stdout });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!loadedFiles.length) {
|
|
89
|
+
throw new Error("Remote skills tree could not load SKILL.md files.");
|
|
90
|
+
}
|
|
91
|
+
return buildManifestFromSkillFiles(null, loadedFiles, baseManifest);
|
|
92
|
+
} finally {
|
|
93
|
+
fs.rmSync(tempCheckout, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildManifestFromSkillFiles(root, skillFiles, baseManifest) {
|
|
98
|
+
const byPath = new Map((baseManifest.skills || []).map((skill) => [skill.path, skill]));
|
|
99
|
+
const byName = new Map((baseManifest.skills || []).map((skill) => [skill.name, skill]));
|
|
100
|
+
const skills = [];
|
|
101
|
+
for (const item of skillFiles) {
|
|
102
|
+
const relativePath = typeof item === "string" ? toPosixPath(path.relative(root, item)) : item.relativePath;
|
|
103
|
+
const contents = typeof item === "string" ? fs.readFileSync(item, "utf8") : item.contents;
|
|
104
|
+
const skillPath = relativePath.replace(/\/SKILL\.md$/, "");
|
|
105
|
+
const parsed = parseSkillFile(contents);
|
|
106
|
+
const inferred = inferSkillFromPath(skillPath, parsed);
|
|
107
|
+
const base = byPath.get(skillPath) || byName.get(inferred.name) || {};
|
|
108
|
+
skills.push({
|
|
109
|
+
name: parsed.name || base.name || inferred.name,
|
|
110
|
+
scope: inferred.scope || base.scope || "domain",
|
|
111
|
+
domain: parsed.domain || base.domain || inferred.domain || "unknown",
|
|
112
|
+
stage: parsed.stage || base.stage || inferred.stage || "unknown",
|
|
113
|
+
path: skillPath,
|
|
114
|
+
description: parsed.description || base.description || `Use ${parsed.name || inferred.name}.`,
|
|
115
|
+
version: parsed.version || base.version || "0.1.0",
|
|
116
|
+
status: parsed.status || base.status || "stable",
|
|
117
|
+
tags: parsed.tags || base.tags || [],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
source: baseManifest.source,
|
|
122
|
+
ref: baseManifest.ref || "release",
|
|
123
|
+
generatedAt: new Date().toISOString(),
|
|
124
|
+
skills: skills.sort((left, right) => left.path.localeCompare(right.path)),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findSkillFiles(root) {
|
|
129
|
+
if (!fs.existsSync(root)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const found = [];
|
|
133
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
134
|
+
const fullPath = path.join(root, entry.name);
|
|
135
|
+
if (entry.isDirectory()) {
|
|
136
|
+
found.push(...findSkillFiles(fullPath));
|
|
137
|
+
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
138
|
+
found.push(fullPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return found.sort();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseSkillFile(contents) {
|
|
145
|
+
const frontmatter = contents.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
146
|
+
if (!frontmatter) {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
const parsed = {};
|
|
150
|
+
let inMetadata = false;
|
|
151
|
+
for (const line of frontmatter[1].split(/\r?\n/)) {
|
|
152
|
+
if (/^\s*metadata\s*:\s*$/.test(line)) {
|
|
153
|
+
inMetadata = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const nested = line.match(/^\s+([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
157
|
+
if (inMetadata && nested) {
|
|
158
|
+
parsed[nested[1]] = cleanYamlValue(nested[2]);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (/^\S/.test(line)) {
|
|
162
|
+
inMetadata = false;
|
|
163
|
+
}
|
|
164
|
+
const topLevel = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
165
|
+
if (topLevel) {
|
|
166
|
+
parsed[topLevel[1]] = cleanYamlValue(topLevel[2]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function cleanYamlValue(value) {
|
|
173
|
+
return value.trim().replace(/^['"]|['"]$/g, "");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function inferSkillFromPath(skillPath, parsed = {}) {
|
|
177
|
+
const parts = skillPath.split("/");
|
|
178
|
+
const name = parsed.name || parts[parts.length - 1];
|
|
179
|
+
if (parts[1] === "common") {
|
|
180
|
+
return { name, scope: "common", stage: parts[2] || parsed.stage || "unknown", domain: parsed.domain || "unknown" };
|
|
181
|
+
}
|
|
182
|
+
if (parts[1] === "domain") {
|
|
183
|
+
return { name, scope: "domain", domain: parts[2] || parsed.domain || "unknown", stage: parsed.stage || parts[3] || "unknown" };
|
|
184
|
+
}
|
|
185
|
+
return { name, scope: parsed.scope || "domain", domain: parsed.domain || "unknown", stage: parsed.stage || "unknown" };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function runtimeManifestCachePath(env = process.env) {
|
|
189
|
+
return env.OH_SKILLHUB_MANIFEST_CACHE || path.join(env.OH_SKILLHUB_CACHE_DIR || path.join(os.homedir(), ".oh-skillhub", "cache"), "manifest-release.json");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadCachedManifest(env = process.env) {
|
|
193
|
+
const file = runtimeManifestCachePath(env);
|
|
194
|
+
if (!fs.existsSync(file)) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function writeCachedManifest(manifest, env = process.env) {
|
|
205
|
+
const file = runtimeManifestCachePath(env);
|
|
206
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
207
|
+
fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toPosixPath(value) {
|
|
211
|
+
return value.split(path.sep).join("/");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runGitAsync(args) {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const child = spawn("git", args, { shell: false });
|
|
217
|
+
let stdout = "";
|
|
218
|
+
let stderr = "";
|
|
219
|
+
child.stdout.setEncoding("utf8");
|
|
220
|
+
child.stderr.setEncoding("utf8");
|
|
221
|
+
child.stdout.on("data", (chunk) => {
|
|
222
|
+
stdout += chunk;
|
|
223
|
+
});
|
|
224
|
+
child.stderr.on("data", (chunk) => {
|
|
225
|
+
stderr += chunk;
|
|
226
|
+
});
|
|
227
|
+
child.on("error", (error) => {
|
|
228
|
+
resolve({ status: 1, stdout, stderr: `${stderr}${error.message}` });
|
|
229
|
+
});
|
|
230
|
+
child.on("close", (status) => {
|
|
231
|
+
resolve({ status, stdout, stderr });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function gitDetail(result) {
|
|
237
|
+
return (result.stderr || result.stdout || "").trim();
|
|
238
|
+
}
|
|
239
|
+
|
|
11
240
|
function selectSkills(manifest, profiles, options = {}) {
|
|
12
241
|
const selected = new Map();
|
|
13
242
|
const unknownNames = [];
|
|
@@ -88,7 +317,9 @@ function selectSkills(manifest, profiles, options = {}) {
|
|
|
88
317
|
}
|
|
89
318
|
|
|
90
319
|
module.exports = {
|
|
320
|
+
buildManifestFromSourceTree,
|
|
91
321
|
loadLocalManifest,
|
|
92
322
|
loadProfiles,
|
|
323
|
+
loadRuntimeManifest,
|
|
93
324
|
selectSkills,
|
|
94
325
|
};
|