oh-skillhub 0.1.20 → 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 +10 -3
- 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);
|
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
|
};
|