moshcode 0.69.0 → 0.70.0
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/integrations.mjs +5 -2
- package/src/skills.mjs +78 -6
package/package.json
CHANGED
package/src/integrations.mjs
CHANGED
|
@@ -253,7 +253,7 @@ export async function mcpCommand(tokens, { run, installedSet } = {}) {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
/** Run `/skill …`. `tokens` are the words after `skill`. `run`/`installedSet` are injectable for tests. */
|
|
256
|
-
export async function skillCommand(tokens, { run, installedSet } = {}) {
|
|
256
|
+
export async function skillCommand(tokens, { run, installedSet, settle } = {}) {
|
|
257
257
|
const verb = tokens[0];
|
|
258
258
|
if (!verb || verb === "list") { printSkillTargets(tokens.slice(1).includes("--json")); return 0; }
|
|
259
259
|
if (verb !== "install") {
|
|
@@ -287,7 +287,10 @@ export async function skillCommand(tokens, { run, installedSet } = {}) {
|
|
|
287
287
|
|
|
288
288
|
const spec = { source, name: skillName(source, name) };
|
|
289
289
|
console.log(info(`installing skill ${bone(spec.name)} → ${ash(source)} across skills engines…`));
|
|
290
|
-
const results = await runSkillInstall(planSkillInstall(spec, { installedSet }),
|
|
290
|
+
const results = await runSkillInstall(planSkillInstall(spec, { installedSet }), {
|
|
291
|
+
...(run ? { run } : {}),
|
|
292
|
+
...(settle ? { settle } : {}),
|
|
293
|
+
});
|
|
291
294
|
summarize(results);
|
|
292
295
|
return anyFailed(results) ? 1 : 0;
|
|
293
296
|
}
|
package/src/skills.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Install Agent Skills across every engine that has a skills primitive, from one
|
|
2
2
|
// source (a git URL or local path). Gemini installs natively; Claude clones the
|
|
3
3
|
// source into its personal skills dir. See prd/0003.
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { ENGINES, isInstalled, ranOk, runCmd } from "./engines.mjs";
|
|
@@ -38,6 +39,61 @@ export function skillName(source, override) {
|
|
|
38
39
|
return named(sanitize(path.basename(path.resolve(raw)))) || "skill";
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* What a freshly cloned skill source actually contains.
|
|
44
|
+
*
|
|
45
|
+
* A repository is not always one skill. `SKILL.md` at the root is the common
|
|
46
|
+
* shape and the one this module assumed. But a repository can equally be a
|
|
47
|
+
* *collection* — subdirectories that each hold a `SKILL.md` — and every engine
|
|
48
|
+
* that discovers skills by scanning looks exactly one level deep. Cloning a
|
|
49
|
+
* collection whole therefore lands every skill one level too deep, where
|
|
50
|
+
* nothing will ever find them, while `git clone` still exits 0 and the install
|
|
51
|
+
* reports success. Detecting the shape is what makes that failure impossible.
|
|
52
|
+
*/
|
|
53
|
+
export function skillCollection(dir) {
|
|
54
|
+
if (!fs.existsSync(dir)) return { kind: "empty", names: [] };
|
|
55
|
+
if (fs.existsSync(path.join(dir, "SKILL.md"))) return { kind: "single", names: [] };
|
|
56
|
+
const names = fs
|
|
57
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
58
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith("."))
|
|
59
|
+
.filter((d) => fs.existsSync(path.join(dir, d.name, "SKILL.md")))
|
|
60
|
+
.map((d) => d.name)
|
|
61
|
+
.sort();
|
|
62
|
+
return names.length ? { kind: "collection", names } : { kind: "empty", names: [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Settle a fresh clone into the shape the engine scans, and report what it was.
|
|
67
|
+
*
|
|
68
|
+
* `single` is left exactly as cloned. `collection` has each skill moved up
|
|
69
|
+
* beside its siblings and the wrapper removed — the wrapper holds the
|
|
70
|
+
* repository's own README, tooling and CI, none of which is a skill. `empty`
|
|
71
|
+
* removes the clone rather than leaving a directory that can never resolve.
|
|
72
|
+
*
|
|
73
|
+
* A skill whose name is already taken is left alone and reported in `kept`:
|
|
74
|
+
* this runs inside the user's real skills directory, so a name collision must
|
|
75
|
+
* never silently replace a skill they already had.
|
|
76
|
+
*/
|
|
77
|
+
export function settleSkillClone(dir) {
|
|
78
|
+
const { kind, names } = skillCollection(dir);
|
|
79
|
+
if (kind === "single") return { kind, installed: [path.basename(dir)], kept: [] };
|
|
80
|
+
if (kind === "empty") {
|
|
81
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
return { kind, installed: [], kept: [] };
|
|
83
|
+
}
|
|
84
|
+
const parent = path.dirname(dir);
|
|
85
|
+
const installed = [];
|
|
86
|
+
const kept = [];
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const dest = path.join(parent, name);
|
|
89
|
+
if (fs.existsSync(dest)) { kept.push(name); continue; }
|
|
90
|
+
fs.renameSync(path.join(dir, name), dest);
|
|
91
|
+
installed.push(name);
|
|
92
|
+
}
|
|
93
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
return { kind, installed, kept };
|
|
95
|
+
}
|
|
96
|
+
|
|
41
97
|
/**
|
|
42
98
|
* The install action for one engine: a spawnable { cmd, args } or a { skip }
|
|
43
99
|
* reason. `spec: { source, name }`.
|
|
@@ -47,13 +103,19 @@ export function skillInstallAction(key, spec) {
|
|
|
47
103
|
switch (key) {
|
|
48
104
|
case "gemini":
|
|
49
105
|
return { cmd: "gemini", args: ["skills", "install", source, "--scope", "user"] };
|
|
50
|
-
case "claude":
|
|
106
|
+
case "claude": {
|
|
51
107
|
// Claude has no `skill install`; clone the source into its skills dir.
|
|
52
|
-
|
|
53
|
-
|
|
108
|
+
// `settle` is the cloned path: a scanning engine needs the clone resolved
|
|
109
|
+
// into one-level-deep skills afterwards (see settleSkillClone).
|
|
110
|
+
const dir = path.join(claudeSkillsDir(), name);
|
|
111
|
+
return { cmd: "git", args: ["clone", "--depth", "1", source, dir], settle: dir };
|
|
112
|
+
}
|
|
113
|
+
case "kimi": {
|
|
54
114
|
// Kimi Code discovers skills by scanning directories, with no install
|
|
55
115
|
// command of its own — so clone into the one it scans, as Claude does.
|
|
56
|
-
|
|
116
|
+
const dir = path.join(kimiSkillsDir(), name);
|
|
117
|
+
return { cmd: "git", args: ["clone", "--depth", "1", source, dir], settle: dir };
|
|
118
|
+
}
|
|
57
119
|
default:
|
|
58
120
|
return { skip: "no skills primitive" };
|
|
59
121
|
}
|
|
@@ -81,13 +143,23 @@ export function planSkillInstall(spec, { installedSet } = {}) {
|
|
|
81
143
|
* [{ key, status: "installed"|"skipped"|"failed"|"not-installed", reason? }].
|
|
82
144
|
* `run` is injectable for tests.
|
|
83
145
|
*/
|
|
84
|
-
export async function runSkillInstall(plan, { run = runCmd } = {}) {
|
|
146
|
+
export async function runSkillInstall(plan, { run = runCmd, settle = settleSkillClone } = {}) {
|
|
85
147
|
const results = [];
|
|
86
148
|
for (const item of plan) {
|
|
87
149
|
if (item.skip) { results.push({ key: item.key, status: "skipped", reason: item.skip }); continue; }
|
|
88
150
|
if (!item.installed) { results.push({ key: item.key, status: "not-installed" }); continue; }
|
|
89
151
|
const r = await run(item.cmd, item.args);
|
|
90
|
-
|
|
152
|
+
const base = { key: item.key, code: r.code, signal: r.signal ?? null };
|
|
153
|
+
if (!ranOk(r)) { results.push({ ...base, status: "failed" }); continue; }
|
|
154
|
+
if (!item.settle) { results.push({ ...base, status: "installed" }); continue; }
|
|
155
|
+
|
|
156
|
+
// The clone succeeded, which is not the same as a skill being installed.
|
|
157
|
+
const { kind, installed, kept } = settle(item.settle);
|
|
158
|
+
if (kind === "empty") {
|
|
159
|
+
results.push({ ...base, status: "failed", reason: "no SKILL.md at the root or in any subdirectory" });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
results.push({ ...base, status: "installed", kind, skills: installed, ...(kept.length ? { kept } : {}) });
|
|
91
163
|
}
|
|
92
164
|
return results;
|
|
93
165
|
}
|