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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
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 = loadLocalManifest();
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 = loadLocalManifest();
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
  };