oh-skillhub 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -29,6 +29,7 @@ printf "1\n3 9\n" | npx oh-skillhub@latest
29
29
 
30
30
  - List bundled OpenHarmony skills by domain or stage.
31
31
  - Resolve profile/domain selections such as `arkui`, `app-dev`, `cicd`, `testing`, and `security`.
32
+ - Download the selected skill directories from the OpenHarmony skills `release` branch and install the full skill contents.
32
33
  - Install skills into Codex, Claude Code, and OpenCode target directories.
33
34
  - Support `--agent codex|claude|opencode|all`.
34
35
  - Support interactive target selection for `Codex`, `Claude`, `OpenCode`, and `All`.
@@ -37,6 +38,8 @@ printf "1\n3 9\n" | npx oh-skillhub@latest
37
38
  - Run a TUI matching the `skills/common/*` and `skills/domain/*` repository layout.
38
39
  - Keep anonymous telemetry events in a local retry queue.
39
40
 
41
+ The installer uses sparse Git checkout so it downloads only the selected skill directories instead of the whole repository.
42
+
40
43
  ## Commands
41
44
 
42
45
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-skillhub",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "OpenHarmony Skills installer for Codex, Claude Code, and OpenCode.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ const readlinePromises = require("node:readline/promises");
7
7
  const { resolveAgentTargets } = require("./agents");
8
8
  const { loadLocalManifest, loadProfiles, selectSkills } = require("./manifest");
9
9
  const { applyInstallPlan, planInstall } = require("./planner");
10
+ const { ensureSkillSourceRoot } = require("./source");
10
11
  const { buildTelemetryEvent, enqueueTelemetryEvent, telemetryStatus } = require("./telemetry");
11
12
 
12
13
  const packageJson = require("../package.json");
@@ -139,7 +140,8 @@ function renderInstall(options) {
139
140
  return renderInstallPlan("Dry run install plan", plan);
140
141
  }
141
142
 
142
- const applied = applyInstallPlan(plan);
143
+ const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills: selection.skills });
144
+ const applied = applyInstallPlan(plan, { sourceRoot });
143
145
  const telemetryEnabled =
144
146
  !options.noTelemetry && !options.offline && process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
145
147
  for (const operation of applied) {
@@ -184,7 +186,9 @@ async function runInteractiveInstaller(input = process.stdin, output = process.s
184
186
  throw new Error("No skills matched the selected groups.");
185
187
  }
186
188
  output.write(`\nInstalling ${selectedChoices.map((choice) => choice.path).join(", ")} for ${agent}:user...\n`);
189
+ output.write(`Preparing skill source from ${manifest.source}#${manifest.ref}...\n`);
187
190
  output.write(`${renderInstallForSkills(skills, { agent, scope: "user" })}\n`);
191
+ output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" })}\n`);
188
192
  }
189
193
 
190
194
  function readAll(input) {
@@ -526,7 +530,9 @@ function renderInstallForSkills(skills, targetOptions) {
526
530
  env: process.env,
527
531
  });
528
532
  const plan = planInstall(skills, targets);
529
- const applied = applyInstallPlan(plan);
533
+ const manifest = loadLocalManifest();
534
+ const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills });
535
+ const applied = applyInstallPlan(plan, { sourceRoot });
530
536
  const telemetryEnabled = process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
531
537
  for (const operation of applied) {
532
538
  enqueueTelemetryEvent(
@@ -547,6 +553,14 @@ function renderInstallForSkills(skills, targetOptions) {
547
553
  return renderInstallPlan("Install summary", plan);
548
554
  }
549
555
 
556
+ function renderInteractiveCompletion(skills, targetOptions) {
557
+ return [
558
+ "",
559
+ `Done. Installed ${skills.length} skill(s) for ${targetOptions.agent}:${targetOptions.scope}.`,
560
+ "You can close this terminal or start using the installed skills.",
561
+ ].join("\n");
562
+ }
563
+
550
564
  function renderInstallPlan(title, plan) {
551
565
  const lines = [title];
552
566
  for (const operation of plan.operations) {
package/src/planner.js CHANGED
@@ -36,33 +36,26 @@ function applyInstallPlan(plan, options = {}) {
36
36
  continue;
37
37
  }
38
38
  fs.mkdirSync(operation.destination, { recursive: true });
39
- writeSkillFile(operation.destination, operation.skill);
39
+ copySkillDirectory(options.sourceRoot, operation.destination, operation.skill);
40
40
  writeInstallRecord(operation.target.dir, operation.target, operation.skill);
41
41
  applied.push(operation);
42
42
  }
43
43
  return applied;
44
44
  }
45
45
 
46
- function writeSkillFile(destination, skill) {
47
- const contents = [
48
- "---",
49
- `name: ${skill.name}`,
50
- `description: ${skill.description}`,
51
- "metadata:",
52
- " author: openharmony",
53
- ` scope: ${skill.scope}`,
54
- ` stage: ${skill.stage}`,
55
- ` domain: ${skill.domain}`,
56
- ` version: ${skill.version}`,
57
- ` status: ${skill.status}`,
58
- "---",
59
- "",
60
- `# ${skill.name}`,
61
- "",
62
- skill.description,
63
- "",
64
- ].join("\n");
65
- fs.writeFileSync(path.join(destination, "SKILL.md"), contents, "utf8");
46
+ function copySkillDirectory(sourceRoot, destination, skill) {
47
+ if (!sourceRoot) {
48
+ throw new Error("Missing skill source root.");
49
+ }
50
+ const root = path.resolve(sourceRoot);
51
+ const source = path.resolve(root, skill.path);
52
+ if (source !== root && !source.startsWith(`${root}${path.sep}`)) {
53
+ throw new Error(`Invalid source path for ${skill.name}: ${skill.path}`);
54
+ }
55
+ if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
56
+ throw new Error(`Source skill directory not found for ${skill.name}: ${source}`);
57
+ }
58
+ fs.cpSync(source, destination, { recursive: true, force: true });
66
59
  }
67
60
 
68
61
  function writeInstallRecord(targetDir, target, skill) {
package/src/source.js ADDED
@@ -0,0 +1,91 @@
1
+ const { spawnSync } = require("node:child_process");
2
+ const crypto = require("node:crypto");
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+
7
+ function ensureSkillSourceRoot(manifest, options = {}) {
8
+ const env = options.env || process.env;
9
+ if (env.OH_SKILLHUB_SOURCE_DIR) {
10
+ return requireDirectory(env.OH_SKILLHUB_SOURCE_DIR, "OH_SKILLHUB_SOURCE_DIR");
11
+ }
12
+
13
+ const source = manifest.source;
14
+ const ref = manifest.ref || "release";
15
+ if (!source) {
16
+ throw new Error("Manifest does not define a skill source repository.");
17
+ }
18
+
19
+ const cacheRoot = env.OH_SKILLHUB_CACHE_DIR || path.join(os.homedir(), ".oh-skillhub", "cache");
20
+ const sparsePaths = selectedSparsePaths(options.skills);
21
+ const checkout = path.join(cacheRoot, `${repositorySlug(source)}-${sanitize(ref)}-${sparseKey(sparsePaths)}`);
22
+ if (isUsableCheckout(checkout, sparsePaths)) {
23
+ return checkout;
24
+ }
25
+
26
+ fs.mkdirSync(cacheRoot, { recursive: true });
27
+ fs.rmSync(checkout, { recursive: true, force: true });
28
+ const tempCheckout = path.join(cacheRoot, `.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`);
29
+ const clone = runGit(["-c", "core.longpaths=true", "clone", "--depth", "1", "--branch", ref, "--no-checkout", source, tempCheckout]);
30
+ if (clone.status !== 0) {
31
+ fs.rmSync(tempCheckout, { recursive: true, force: true });
32
+ const detail = gitDetail(clone);
33
+ throw new Error(`Failed to download skill source from ${source}#${ref}.${detail ? ` ${detail}` : ""}`);
34
+ }
35
+ const sparseInit = runGit(["-C", tempCheckout, "-c", "core.longpaths=true", "sparse-checkout", "init", "--no-cone"]);
36
+ const sparseSet = sparseInit.status === 0
37
+ ? runGit(["-C", tempCheckout, "-c", "core.longpaths=true", "sparse-checkout", "set", ...sparsePaths])
38
+ : sparseInit;
39
+ const checkoutResult = sparseSet.status === 0
40
+ ? runGit(["-C", tempCheckout, "-c", "core.longpaths=true", "checkout"])
41
+ : sparseSet;
42
+ if (checkoutResult.status !== 0) {
43
+ fs.rmSync(tempCheckout, { recursive: true, force: true });
44
+ const detail = gitDetail(checkoutResult);
45
+ throw new Error(`Failed to checkout selected skill source from ${source}#${ref}.${detail ? ` ${detail}` : ""}`);
46
+ }
47
+ fs.renameSync(tempCheckout, checkout);
48
+ return checkout;
49
+ }
50
+
51
+ function requireDirectory(value, label) {
52
+ const dir = path.resolve(value);
53
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
54
+ throw new Error(`${label} is not a directory: ${dir}`);
55
+ }
56
+ return dir;
57
+ }
58
+
59
+ function isUsableCheckout(dir, sparsePaths) {
60
+ return sparsePaths.every((item) => fs.existsSync(path.join(dir, item)));
61
+ }
62
+
63
+ function repositorySlug(source) {
64
+ const name = source.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || "skills";
65
+ return sanitize(name.replace(/\.git$/, ""));
66
+ }
67
+
68
+ function sanitize(value) {
69
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
70
+ }
71
+
72
+ function selectedSparsePaths(skills = []) {
73
+ const paths = skills.map((skill) => skill.path).filter(Boolean).sort();
74
+ return paths.length ? paths : ["skills"];
75
+ }
76
+
77
+ function sparseKey(sparsePaths) {
78
+ return crypto.createHash("sha1").update(sparsePaths.join("\n")).digest("hex").slice(0, 12);
79
+ }
80
+
81
+ function runGit(args) {
82
+ return spawnSync("git", args, { encoding: "utf8", shell: false });
83
+ }
84
+
85
+ function gitDetail(result) {
86
+ return (result.stderr || result.stdout || "").trim();
87
+ }
88
+
89
+ module.exports = {
90
+ ensureSkillSourceRoot,
91
+ };