oh-skillhub 0.1.7 → 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 +7 -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,6 +186,7 @@ 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`);
|
|
188
191
|
output.write(`${renderInteractiveCompletion(skills, { agent, scope: "user" })}\n`);
|
|
189
192
|
}
|
|
@@ -527,7 +530,9 @@ function renderInstallForSkills(skills, targetOptions) {
|
|
|
527
530
|
env: process.env,
|
|
528
531
|
});
|
|
529
532
|
const plan = planInstall(skills, targets);
|
|
530
|
-
const
|
|
533
|
+
const manifest = loadLocalManifest();
|
|
534
|
+
const sourceRoot = ensureSkillSourceRoot(manifest, { env: process.env, skills });
|
|
535
|
+
const applied = applyInstallPlan(plan, { sourceRoot });
|
|
531
536
|
const telemetryEnabled = process.env.OH_SKILLHUB_NO_TELEMETRY !== "1";
|
|
532
537
|
for (const operation of applied) {
|
|
533
538
|
enqueueTelemetryEvent(
|
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
|
+
};
|