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 +3 -0
- package/package.json +1 -1
- package/src/cli.js +16 -2
- package/src/planner.js +14 -21
- package/src/source.js +91 -0
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
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
|
|
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
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
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
|
+
};
|