@topmindspace/topmind-skills 4.13.2 → 4.13.4
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/INSTALL.md +94 -51
- package/README.en.md +12 -6
- package/README.md +15 -9
- package/bin/install-skills.mjs +446 -51
- package/evals/evals.json +5 -5
- package/install-targets/claude-code.json +9 -5
- package/install-targets/codex.json +9 -5
- package/install-targets/hermes.json +9 -5
- package/install-targets/mimocode.json +59 -0
- package/install-targets/opencode.json +9 -5
- package/install-targets/universal.json +58 -0
- package/integrations/codex/README.md +4 -4
- package/integrations/hermes/README.md +4 -4
- package/integrations/opencode/README.md +3 -3
- package/integrations/opencode/plugins/topmind-plugin.ts +1 -1
- package/integrations/opencode/tests/opencode-config-shape.test.mjs +3 -1
- package/package.json +5 -2
- package/shared/document-ingest.md +1 -1
- package/shared/host-loading.md +7 -7
- package/topmind/SKILL.md +1 -1
- package/topmind-capture/SKILL.md +1 -1
- package/topmind-ledger/SKILL.md +1 -1
- package/topmind-loop/SKILL.md +1 -1
- package/topmind-maintain/SKILL.md +1 -1
- package/topmind-memory/SKILL.md +1 -1
- package/topmind-organize/SKILL.md +1 -1
- package/topmind-pack.json +14 -6
- package/topmind-wechat/DESIGN.md +1 -1
- package/topmind-wechat/SKILL.md +1 -1
- package/topmind-wechat/references/workflow.md +9 -9
- package/topmind-weread/SKILL.md +1 -1
- package/topmind-write/SKILL.md +1 -1
- package/topmind-x/SKILL.md +1 -1
package/bin/install-skills.mjs
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
* Private GitHub: export GH_TOKEN or GITHUB_TOKEN.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
import { promises as fs } from "node:fs";
|
|
56
|
+
import { promises as fs, existsSync } from "node:fs";
|
|
57
57
|
import path from "node:path";
|
|
58
58
|
import os from "node:os";
|
|
59
59
|
import { fileURLToPath } from "node:url";
|
|
@@ -70,14 +70,152 @@ const DEFAULT_GIT = `https://github.com/${DEFAULT_OWNER_REPO}.git`;
|
|
|
70
70
|
const DEFAULT_PATH = ".";
|
|
71
71
|
const RECEIPT_NAME = ".topmind-skills-install.json";
|
|
72
72
|
|
|
73
|
+
const AGENTS_SUB = path.join(".agents", "skills");
|
|
74
|
+
|
|
75
|
+
function homeDir() {
|
|
76
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function configHome() {
|
|
80
|
+
return process.env.XDG_CONFIG_HOME || path.join(homeDir(), ".config");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ecosystem agent matrix (aligned with `npx skills` / skills.sh):
|
|
85
|
+
* canonical <base>/.agents/skills (project base=cwd, global base=~)
|
|
86
|
+
* universal agents whose skillsDir is .agents/skills read it directly
|
|
87
|
+
* private others get relative symlink (default) or copy into their own root
|
|
88
|
+
*/
|
|
89
|
+
function agentCatalog() {
|
|
90
|
+
const home = homeDir();
|
|
91
|
+
const cfg = configHome();
|
|
92
|
+
return {
|
|
93
|
+
universal: {
|
|
94
|
+
id: "universal",
|
|
95
|
+
label: "Universal (.agents/skills)",
|
|
96
|
+
projectDir: AGENTS_SUB,
|
|
97
|
+
globalDir: path.join(home, AGENTS_SUB),
|
|
98
|
+
detect: () => true,
|
|
99
|
+
universal: true,
|
|
100
|
+
},
|
|
101
|
+
"claude-code": {
|
|
102
|
+
id: "claude-code",
|
|
103
|
+
label: "Claude Code",
|
|
104
|
+
projectDir: path.join(".claude", "skills"),
|
|
105
|
+
globalDir: path.join(home, ".claude", "skills"),
|
|
106
|
+
detect: () => fsSyncExists(path.join(home, ".claude")),
|
|
107
|
+
universal: false,
|
|
108
|
+
},
|
|
109
|
+
mimocode: {
|
|
110
|
+
id: "mimocode",
|
|
111
|
+
label: "MiMoCode / MiMo Desktop",
|
|
112
|
+
projectDir: path.join(".mimocode", "skills"),
|
|
113
|
+
globalDir: path.join(cfg, "mimocode", "skills"),
|
|
114
|
+
detect: () => fsSyncExists(path.join(cfg, "mimocode")) || fsSyncExists(path.join(home, ".mimocode")),
|
|
115
|
+
universal: false,
|
|
116
|
+
},
|
|
117
|
+
codex: {
|
|
118
|
+
id: "codex",
|
|
119
|
+
label: "Codex",
|
|
120
|
+
projectDir: AGENTS_SUB,
|
|
121
|
+
globalDir: path.join(home, ".codex", "skills"),
|
|
122
|
+
detect: () => fsSyncExists(path.join(home, ".codex")),
|
|
123
|
+
universal: false,
|
|
124
|
+
},
|
|
125
|
+
cursor: {
|
|
126
|
+
id: "cursor",
|
|
127
|
+
label: "Cursor",
|
|
128
|
+
projectDir: AGENTS_SUB,
|
|
129
|
+
globalDir: path.join(home, ".cursor", "skills"),
|
|
130
|
+
detect: () => fsSyncExists(path.join(home, ".cursor")),
|
|
131
|
+
universal: false,
|
|
132
|
+
},
|
|
133
|
+
"gemini-cli": {
|
|
134
|
+
id: "gemini-cli",
|
|
135
|
+
label: "Gemini CLI",
|
|
136
|
+
projectDir: AGENTS_SUB,
|
|
137
|
+
globalDir: path.join(home, ".gemini", "skills"),
|
|
138
|
+
detect: () => fsSyncExists(path.join(home, ".gemini")),
|
|
139
|
+
universal: false,
|
|
140
|
+
},
|
|
141
|
+
opencode: {
|
|
142
|
+
id: "opencode",
|
|
143
|
+
label: "OpenCode",
|
|
144
|
+
projectDir: AGENTS_SUB,
|
|
145
|
+
globalDir: path.join(cfg, "opencode", "skills"),
|
|
146
|
+
detect: () => fsSyncExists(path.join(cfg, "opencode")) || fsSyncExists(path.join(home, ".opencode")),
|
|
147
|
+
universal: false,
|
|
148
|
+
},
|
|
149
|
+
hermes: {
|
|
150
|
+
id: "hermes",
|
|
151
|
+
label: "Hermes",
|
|
152
|
+
projectDir: path.join(".hermes", "skills"),
|
|
153
|
+
globalDir: path.join(home, ".hermes", "skills"),
|
|
154
|
+
detect: () => fsSyncExists(path.join(home, ".hermes")),
|
|
155
|
+
universal: false,
|
|
156
|
+
},
|
|
157
|
+
"github-copilot": {
|
|
158
|
+
id: "github-copilot",
|
|
159
|
+
label: "GitHub Copilot",
|
|
160
|
+
projectDir: AGENTS_SUB,
|
|
161
|
+
globalDir: path.join(home, ".copilot", "skills"),
|
|
162
|
+
detect: () => fsSyncExists(path.join(home, ".copilot")),
|
|
163
|
+
universal: false,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Legacy --host names → agent ids + single-dir default dest. */
|
|
73
169
|
const HOST_DEFAULTS = {
|
|
74
|
-
"claude-code": () => path.join(
|
|
75
|
-
codex: () => path.join(
|
|
76
|
-
hermes: () => path.join(
|
|
170
|
+
"claude-code": () => path.join(homeDir(), ".claude", "skills"),
|
|
171
|
+
codex: () => path.join(homeDir(), ".codex", "skills"),
|
|
172
|
+
hermes: () => path.join(homeDir(), ".hermes", "skills"),
|
|
77
173
|
opencode: () => path.join(process.cwd(), ".opencode", "skills"),
|
|
78
174
|
generic: () => path.join(process.cwd(), "topmind-skills"),
|
|
175
|
+
mimocode: () => path.join(configHome(), "mimocode", "skills"),
|
|
79
176
|
};
|
|
80
177
|
|
|
178
|
+
function fsSyncExists(p) {
|
|
179
|
+
return existsSync(p);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function canonicalBase(global) {
|
|
183
|
+
return path.join(global ? homeDir() : process.cwd(), AGENTS_SUB);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function agentSkillsRoot(agent, global) {
|
|
187
|
+
return global ? agent.globalDir : path.join(process.cwd(), agent.projectDir);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function samePath(a, b) {
|
|
191
|
+
return path.resolve(a) === path.resolve(b);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function detectedAgentIds() {
|
|
195
|
+
return Object.values(agentCatalog())
|
|
196
|
+
.filter((a) => a.detect())
|
|
197
|
+
.map((a) => a.id);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveAgentSelection(requested, global) {
|
|
201
|
+
const cat = agentCatalog();
|
|
202
|
+
const all = Object.keys(cat);
|
|
203
|
+
let ids;
|
|
204
|
+
if (!requested || !requested.length) ids = detectedAgentIds();
|
|
205
|
+
else if (requested.includes("*")) ids = all;
|
|
206
|
+
else ids = [...new Set(requested)];
|
|
207
|
+
for (const id of ids) {
|
|
208
|
+
if (!cat[id]) fail(`unknown agent: ${id}\nknown: ${all.join(", ")}`);
|
|
209
|
+
}
|
|
210
|
+
ids.sort((a, b) => (a === "universal" ? -1 : b === "universal" ? 1 : a.localeCompare(b)));
|
|
211
|
+
if (!ids.includes("universal")) ids.unshift("universal");
|
|
212
|
+
return ids.map((id) => {
|
|
213
|
+
const agent = cat[id];
|
|
214
|
+
const root = agentSkillsRoot(agent, global);
|
|
215
|
+
return { agent, root, sharesCanonical: samePath(root, canonicalBase(global)) };
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
81
219
|
function log(msg) {
|
|
82
220
|
process.stdout.write(`[install-skills] ${msg}\n`);
|
|
83
221
|
}
|
|
@@ -92,21 +230,34 @@ function printHelp() {
|
|
|
92
230
|
node bin/install-skills.mjs add <source> [options]
|
|
93
231
|
node bin/install-skills.mjs update [--dest <dir>]
|
|
94
232
|
node bin/install-skills.mjs list <source> [options]
|
|
233
|
+
node bin/install-skills.mjs agents | paths | doctor [--repair]
|
|
95
234
|
|
|
96
235
|
Source: owner/repo | owner/repo@ref | git-url | local-path | release:<tag>
|
|
97
236
|
|
|
98
|
-
|
|
99
|
-
|
|
237
|
+
Layout (aligned with npx skills / skills.sh):
|
|
238
|
+
canonical <base>/.agents/skills pack body lives here (one copy)
|
|
239
|
+
universal agents that read .agents/skills need nothing else
|
|
240
|
+
private Claude Code / MiMoCode / Hermes … get symlink (or --copy)
|
|
100
241
|
|
|
101
242
|
Options:
|
|
102
|
-
--
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
--
|
|
106
|
-
--
|
|
107
|
-
--
|
|
108
|
-
--
|
|
109
|
-
--
|
|
243
|
+
-g, --global Global scope (canonical ~/.agents/skills + agent global roots)
|
|
244
|
+
-p, --project Project scope (canonical ./.agents/skills + agent project roots)
|
|
245
|
+
-a, --agent <ids> Target agents (repeatable / comma-separated; '*' = all)
|
|
246
|
+
--copy Copy into each private agent dir (default: symlink)
|
|
247
|
+
--dest <dir> Install ONLY into this skills root (single-dir / legacy)
|
|
248
|
+
--host <name> Legacy single-host dest: claude-code|codex|hermes|opencode|mimocode|generic
|
|
249
|
+
--mode copy|symlink Mode for --dest/--host single-dir installs (default: copy)
|
|
250
|
+
--skill <id> Only install these skill ids (repeatable / comma-separated)
|
|
251
|
+
--locale <code> Install locale overlay (e.g. en-US); falls back to topmind_LOCALE env
|
|
252
|
+
-n, --dry-run Print plan only
|
|
253
|
+
--force Replace non-symlink destinations when using symlink mode
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
node bin/install-skills.mjs add topmindspace/topmind-skills -g
|
|
257
|
+
node bin/install-skills.mjs add topmindspace/topmind-skills -a claude-code -a mimocode
|
|
258
|
+
node bin/install-skills.mjs add . --dest ~/.claude/skills
|
|
259
|
+
node bin/install-skills.mjs update -g
|
|
260
|
+
node bin/install-skills.mjs doctor --repair
|
|
110
261
|
|
|
111
262
|
See INSTALL.md for full docs.
|
|
112
263
|
`);
|
|
@@ -121,11 +272,15 @@ function parseArgs(argv) {
|
|
|
121
272
|
dest: null,
|
|
122
273
|
repoPath: DEFAULT_PATH,
|
|
123
274
|
mode: "copy",
|
|
124
|
-
host:
|
|
125
|
-
|
|
275
|
+
host: null,
|
|
276
|
+
agents: [],
|
|
277
|
+
scope: null, // 'global' | 'project' | null
|
|
278
|
+
linkMode: "symlink", // for agent matrix: symlink | copy
|
|
279
|
+
skillsFilter: null,
|
|
126
280
|
dryRun: false,
|
|
127
281
|
force: false,
|
|
128
282
|
locale: null,
|
|
283
|
+
repair: false,
|
|
129
284
|
};
|
|
130
285
|
|
|
131
286
|
if (argv.length === 0) {
|
|
@@ -133,7 +288,6 @@ function parseArgs(argv) {
|
|
|
133
288
|
process.exit(0);
|
|
134
289
|
}
|
|
135
290
|
|
|
136
|
-
// Legacy flag-only form (no subcommand) → treat as add with flags
|
|
137
291
|
const first = argv[0];
|
|
138
292
|
if (first === "add" || first === "install" || first === "a") {
|
|
139
293
|
out.command = "add";
|
|
@@ -144,14 +298,21 @@ function parseArgs(argv) {
|
|
|
144
298
|
} else if (first === "list" || first === "ls") {
|
|
145
299
|
out.command = "list";
|
|
146
300
|
argv = argv.slice(1);
|
|
301
|
+
} else if (first === "agents") {
|
|
302
|
+
out.command = "agents";
|
|
303
|
+
argv = argv.slice(1);
|
|
304
|
+
} else if (first === "paths") {
|
|
305
|
+
out.command = "paths";
|
|
306
|
+
argv = argv.slice(1);
|
|
307
|
+
} else if (first === "doctor") {
|
|
308
|
+
out.command = "doctor";
|
|
309
|
+
argv = argv.slice(1);
|
|
147
310
|
} else if (first === "--help" || first === "-h") {
|
|
148
311
|
printHelp();
|
|
149
312
|
process.exit(0);
|
|
150
313
|
} else if (first.startsWith("-")) {
|
|
151
|
-
// npm run skills:install -- --from-git (legacy)
|
|
152
314
|
out.command = "add";
|
|
153
315
|
} else {
|
|
154
|
-
// bare source: install-skills.mjs topmindspace/topmind-skills
|
|
155
316
|
out.command = "add";
|
|
156
317
|
}
|
|
157
318
|
|
|
@@ -167,7 +328,6 @@ function parseArgs(argv) {
|
|
|
167
328
|
else if (a === "--path" || a === "--subdir") out.repoPath = next().replace(/^\/+|\/+$/g, "") || ".";
|
|
168
329
|
else if (a === "--source" || a === "-s") out.source = next();
|
|
169
330
|
else if (a === "--from-git") {
|
|
170
|
-
// legacy: --from-git [url]
|
|
171
331
|
const peek = argv[i + 1];
|
|
172
332
|
if (peek && !peek.startsWith("-")) out.source = next();
|
|
173
333
|
else out.source = DEFAULT_OWNER_REPO;
|
|
@@ -176,23 +336,31 @@ function parseArgs(argv) {
|
|
|
176
336
|
out.source = peek && !peek.startsWith("-") ? `release:${next()}` : "release:latest";
|
|
177
337
|
} else if (a === "--mode" || a === "-m") out.mode = next();
|
|
178
338
|
else if (a === "--host") out.host = next();
|
|
179
|
-
else if (a === "--global" || a === "-g")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
339
|
+
else if (a === "--global" || a === "-g") out.scope = "global";
|
|
340
|
+
else if (a === "--project" || a === "-p") out.scope = "project";
|
|
341
|
+
else if (a === "--agent" || a === "-a") {
|
|
342
|
+
const raw = next();
|
|
343
|
+
for (const part of raw.split(",")) {
|
|
344
|
+
const id = part.trim();
|
|
345
|
+
if (id) out.agents.push(id);
|
|
346
|
+
}
|
|
347
|
+
} else if (a === "--copy") {
|
|
348
|
+
out.linkMode = "copy";
|
|
349
|
+
out.mode = "copy";
|
|
350
|
+
} else if (a === "--link") out.linkMode = "symlink";
|
|
351
|
+
else if (a === "--skill") {
|
|
183
352
|
const raw = next();
|
|
184
353
|
out.skillsFilter = out.skillsFilter || new Set();
|
|
185
354
|
for (const part of raw.split(",")) {
|
|
186
355
|
const id = part.trim();
|
|
187
356
|
if (id) out.skillsFilter.add(id);
|
|
188
357
|
}
|
|
189
|
-
} else if (a === "--dry-run") out.dryRun = true;
|
|
358
|
+
} else if (a === "--dry-run" || a === "-n") out.dryRun = true;
|
|
190
359
|
else if (a === "--locale") out.locale = next();
|
|
191
360
|
else if (a === "--force" || a === "-f") out.force = true;
|
|
192
|
-
else if (a === "--
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
} else if (a === "--help" || a === "-h") {
|
|
361
|
+
else if (a === "--repair") out.repair = true;
|
|
362
|
+
else if (a === "--update" || a === "-u") out.command = "update";
|
|
363
|
+
else if (a === "--help" || a === "-h") {
|
|
196
364
|
printHelp();
|
|
197
365
|
process.exit(0);
|
|
198
366
|
} else if (a.startsWith("-")) fail(`unknown option: ${a}`);
|
|
@@ -201,17 +369,24 @@ function parseArgs(argv) {
|
|
|
201
369
|
}
|
|
202
370
|
|
|
203
371
|
if (!["copy", "symlink"].includes(out.mode)) fail(`--mode must be copy|symlink`);
|
|
204
|
-
if (!out.
|
|
372
|
+
if (!["copy", "symlink"].includes(out.linkMode)) fail(`--copy/--link must be copy|symlink`);
|
|
373
|
+
|
|
374
|
+
// Scope default: project (matches npx skills); -g flips to global.
|
|
375
|
+
if (!out.scope) out.scope = "project";
|
|
376
|
+
out.global = out.scope === "global";
|
|
377
|
+
|
|
378
|
+
// Single-dir path: --dest or legacy --host (not the multi-agent matrix).
|
|
379
|
+
if (out.dest) {
|
|
380
|
+
out.dest = path.resolve(out.dest.replace(/^~(?=\/|$)/, os.homedir()));
|
|
381
|
+
} else if (out.host) {
|
|
205
382
|
if (!HOST_DEFAULTS[out.host]) {
|
|
206
383
|
fail(`unknown --host ${out.host}; pass --dest or use: ${Object.keys(HOST_DEFAULTS).join("|")}`);
|
|
207
384
|
}
|
|
208
385
|
out.dest = HOST_DEFAULTS[out.host]();
|
|
386
|
+
out.dest = path.resolve(out.dest.replace(/^~(?=\/|$)/, os.homedir()));
|
|
209
387
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!out.locale) {
|
|
213
|
-
out.locale = process.env.topmind_LOCALE || null;
|
|
214
|
-
}
|
|
388
|
+
|
|
389
|
+
if (!out.locale) out.locale = process.env.topmind_LOCALE || null;
|
|
215
390
|
return out;
|
|
216
391
|
}
|
|
217
392
|
|
|
@@ -551,7 +726,7 @@ async function installSymlink(skillsRoot, dest, entries, { force }) {
|
|
|
551
726
|
|
|
552
727
|
async function writeInstallReceipt(dest, meta) {
|
|
553
728
|
const receipt = {
|
|
554
|
-
schema:
|
|
729
|
+
schema: 2,
|
|
555
730
|
package: meta.name || "topmind",
|
|
556
731
|
version: meta.version,
|
|
557
732
|
installed_at: new Date().toISOString(),
|
|
@@ -559,22 +734,196 @@ async function writeInstallReceipt(dest, meta) {
|
|
|
559
734
|
source_raw: meta.sourceRaw,
|
|
560
735
|
repo_path: meta.repoPath,
|
|
561
736
|
mode: meta.mode,
|
|
562
|
-
host: meta.host,
|
|
737
|
+
host: meta.host || null,
|
|
738
|
+
scope: meta.scope || null,
|
|
739
|
+
agents: meta.agents || null,
|
|
563
740
|
skill_ids: meta.skillIds,
|
|
564
741
|
locale: meta.locale || null,
|
|
565
742
|
repository: `https://github.com/${DEFAULT_OWNER_REPO}`,
|
|
566
|
-
// Enough to re-run update without remembering flags
|
|
567
743
|
update: {
|
|
568
744
|
command: "add",
|
|
569
745
|
source: meta.sourceRaw,
|
|
570
746
|
path: meta.repoPath,
|
|
571
747
|
mode: meta.mode,
|
|
572
|
-
host: meta.host,
|
|
748
|
+
host: meta.host || null,
|
|
749
|
+
scope: meta.scope || null,
|
|
750
|
+
agents: meta.agents || null,
|
|
573
751
|
},
|
|
574
752
|
};
|
|
575
753
|
await fs.writeFile(path.join(dest, RECEIPT_NAME), JSON.stringify(receipt, null, 2) + "\n", "utf8");
|
|
576
754
|
}
|
|
577
755
|
|
|
756
|
+
/** Relative-symlink each pack entry from an agent root back to the canonical body. */
|
|
757
|
+
async function linkPackEntries(canonicalRoot, agentRoot, entryNames, { force }) {
|
|
758
|
+
await fs.mkdir(agentRoot, { recursive: true });
|
|
759
|
+
for (const name of entryNames) {
|
|
760
|
+
const src = path.join(canonicalRoot, name);
|
|
761
|
+
const dst = path.join(agentRoot, name);
|
|
762
|
+
if (!(await pathExists(src))) continue;
|
|
763
|
+
if (samePath(agentRoot, canonicalRoot)) continue;
|
|
764
|
+
if ((await pathExists(dst)) || (await isBrokenSymlink(dst))) {
|
|
765
|
+
const st = await fs.lstat(dst).catch(() => null);
|
|
766
|
+
if (st && st.isSymbolicLink()) await fs.rm(dst, { recursive: true, force: true });
|
|
767
|
+
else if (force) await fs.rm(dst, { recursive: true, force: true });
|
|
768
|
+
else {
|
|
769
|
+
log(` ! ${dst} exists (not symlink) — skip (pass --force to replace)`);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const rel = path.relative(agentRoot, src);
|
|
774
|
+
await fs.symlink(rel, dst, "junction");
|
|
775
|
+
log(` ~ ${path.join(agentRoot, name)} → ${rel}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function isBrokenSymlink(p) {
|
|
780
|
+
let st;
|
|
781
|
+
try {
|
|
782
|
+
st = await fs.lstat(p);
|
|
783
|
+
} catch {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
if (!st.isSymbolicLink()) return false;
|
|
787
|
+
try {
|
|
788
|
+
await fs.stat(p);
|
|
789
|
+
return false;
|
|
790
|
+
} catch {
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Install pack into canonical `.agents/skills`, then link/copy into agent-private roots.
|
|
797
|
+
* Returns the canonical dest (receipt lives there).
|
|
798
|
+
*/
|
|
799
|
+
async function installViaMatrix(skillsRoot, plan, opts) {
|
|
800
|
+
const global = !!opts.global;
|
|
801
|
+
const canonical = canonicalBase(global);
|
|
802
|
+
const targets = resolveAgentSelection(opts.agents, global);
|
|
803
|
+
const linkMode = opts.linkMode || "symlink";
|
|
804
|
+
|
|
805
|
+
log(`matrix ${global ? "global" : "project"} · ${linkMode} · canonical ${canonical}`);
|
|
806
|
+
|
|
807
|
+
if (opts.dryRun) {
|
|
808
|
+
log(` · ${canonical} [canonical pack]`);
|
|
809
|
+
for (const t of targets) {
|
|
810
|
+
if (t.sharesCanonical) continue;
|
|
811
|
+
log(` · ${t.root} [${t.agent.label} → ${linkMode}]`);
|
|
812
|
+
}
|
|
813
|
+
log("(dry-run)");
|
|
814
|
+
return { canonical, targets, plan };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 1) Full pack body → canonical
|
|
818
|
+
await installCopy(skillsRoot, canonical, plan.entries, opts.locale);
|
|
819
|
+
|
|
820
|
+
// 2) Private agent dirs: symlink entries (default) or copy
|
|
821
|
+
for (const t of targets) {
|
|
822
|
+
if (t.sharesCanonical) {
|
|
823
|
+
log(` · shared via .agents/skills: ${t.agent.id}`);
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (linkMode === "copy") {
|
|
827
|
+
await installCopy(skillsRoot, t.root, plan.entries, opts.locale);
|
|
828
|
+
} else {
|
|
829
|
+
await linkPackEntries(canonical, t.root, plan.entries, opts);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
await writeInstallReceipt(canonical, {
|
|
834
|
+
name: plan.name,
|
|
835
|
+
version: plan.version,
|
|
836
|
+
sourceLabel: "matrix",
|
|
837
|
+
sourceRaw: opts.source,
|
|
838
|
+
repoPath: opts.repoPath,
|
|
839
|
+
mode: linkMode,
|
|
840
|
+
host: null,
|
|
841
|
+
scope: opts.scope,
|
|
842
|
+
agents: targets.map((t) => t.agent.id),
|
|
843
|
+
skillIds: plan.skillIds,
|
|
844
|
+
locale: opts.locale,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
log(`done → ${canonical}`);
|
|
848
|
+
log(`daily entry: ${path.join(canonical, "topmind", "SKILL.md")}`);
|
|
849
|
+
log(`update later: node bin/install-skills.mjs update -g`);
|
|
850
|
+
return { canonical, targets, plan };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function cmdAgents() {
|
|
854
|
+
const cat = agentCatalog();
|
|
855
|
+
const det = new Set(detectedAgentIds());
|
|
856
|
+
log("Agents (id · kind · project · global · status):");
|
|
857
|
+
for (const a of Object.values(cat)) {
|
|
858
|
+
const kind = a.projectDir === AGENTS_SUB ? "universal-project" : "private-project";
|
|
859
|
+
const st = det.has(a.id) ? "detected" : "-";
|
|
860
|
+
log(` ${a.id.padEnd(16)} ${kind}`);
|
|
861
|
+
log(` ${"".padEnd(16)} project ${a.projectDir}`);
|
|
862
|
+
log(` ${"".padEnd(16)} global ${a.globalDir}`);
|
|
863
|
+
log(` ${"".padEnd(16)} ${st}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function cmdPaths(opts) {
|
|
868
|
+
const global = !!opts.global;
|
|
869
|
+
log(`Canonical (${global ? "global" : "project"}):`);
|
|
870
|
+
log(` ${canonicalBase(global)}`);
|
|
871
|
+
log("Agents:");
|
|
872
|
+
for (const a of Object.values(agentCatalog())) {
|
|
873
|
+
const root = agentSkillsRoot(a, global);
|
|
874
|
+
const share = samePath(root, canonicalBase(global)) ? " (shares .agents/skills)" : "";
|
|
875
|
+
const det = a.detect() ? "detected" : "not detected";
|
|
876
|
+
log(` ${a.id.padEnd(16)} ${root}${share} [${det}]`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function cmdDoctor(opts) {
|
|
881
|
+
const cat = agentCatalog();
|
|
882
|
+
let problems = 0;
|
|
883
|
+
log("Skill root health:");
|
|
884
|
+
for (const a of Object.values(cat)) {
|
|
885
|
+
for (const [scopeName, root] of [
|
|
886
|
+
["project", path.join(process.cwd(), a.projectDir)],
|
|
887
|
+
["global", a.globalDir],
|
|
888
|
+
]) {
|
|
889
|
+
if (!(await pathExists(root)) && !(await pathExists(path.dirname(root)))) continue;
|
|
890
|
+
let st = null;
|
|
891
|
+
try {
|
|
892
|
+
st = await fs.lstat(root);
|
|
893
|
+
} catch {
|
|
894
|
+
st = null;
|
|
895
|
+
}
|
|
896
|
+
if (st && st.isSymbolicLink()) {
|
|
897
|
+
let ok = true;
|
|
898
|
+
try {
|
|
899
|
+
await fs.stat(root);
|
|
900
|
+
} catch {
|
|
901
|
+
ok = false;
|
|
902
|
+
}
|
|
903
|
+
if (!ok) {
|
|
904
|
+
problems++;
|
|
905
|
+
log(` ✗ ${root} [${a.label} · ${scopeName}] broken symlink`);
|
|
906
|
+
if (opts.repair) {
|
|
907
|
+
await fs.rm(root, { force: true });
|
|
908
|
+
await fs.mkdir(root, { recursive: true });
|
|
909
|
+
log(" repaired → real directory");
|
|
910
|
+
}
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (st && !st.isDirectory() && !(st.isSymbolicLink())) {
|
|
915
|
+
problems++;
|
|
916
|
+
log(` ✗ ${root} [${a.label} · ${scopeName}] not a directory`);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (st) log(` ✓ ${root} [${a.label} · ${scopeName}]`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (!problems) log(" all clear");
|
|
923
|
+
else if (!opts.repair) log("Run `topmind-skills doctor --repair` to replace broken symlinks.");
|
|
924
|
+
if (problems && !opts.repair) process.exitCode = 1;
|
|
925
|
+
}
|
|
926
|
+
|
|
578
927
|
async function readReceipt(dest) {
|
|
579
928
|
const p = path.join(dest, RECEIPT_NAME);
|
|
580
929
|
if (!(await pathExists(p))) return null;
|
|
@@ -590,10 +939,15 @@ async function readReceipt(dest) {
|
|
|
590
939
|
async function cmdAdd(opts, sourceOverride) {
|
|
591
940
|
const sourceRaw = sourceOverride || opts.source || DEFAULT_OWNER_REPO;
|
|
592
941
|
const sourceDesc = parseSource(sourceRaw, opts.repoPath);
|
|
593
|
-
const
|
|
942
|
+
const useMatrix = !opts.dest;
|
|
943
|
+
const dest = opts.dest || canonicalBase(opts.global);
|
|
594
944
|
|
|
595
945
|
log(`add source=${sourceDesc.label}`);
|
|
596
|
-
log(
|
|
946
|
+
log(
|
|
947
|
+
useMatrix
|
|
948
|
+
? ` path=${opts.repoPath} scope=${opts.scope} link=${opts.linkMode} agents=${opts.agents.length ? opts.agents.join(",") : "detected"}`
|
|
949
|
+
: ` path=${opts.repoPath} mode=${opts.mode} dest=${dest}`,
|
|
950
|
+
);
|
|
597
951
|
|
|
598
952
|
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "topmind-skills-"));
|
|
599
953
|
try {
|
|
@@ -601,9 +955,21 @@ async function cmdAdd(opts, sourceOverride) {
|
|
|
601
955
|
const plan = await listInstallEntries(skillsRoot, opts.skillsFilter);
|
|
602
956
|
log(`pack ${plan.name}@${plan.version} · ${plan.skillIds.length} skill(s) · ${plan.entries.length} entries`);
|
|
603
957
|
|
|
604
|
-
if (opts.command === "list"
|
|
958
|
+
if (opts.command === "list") {
|
|
605
959
|
for (const e of plan.entries) log(` - ${e}`);
|
|
606
|
-
log(
|
|
960
|
+
log("(list only)");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (useMatrix) {
|
|
965
|
+
await installViaMatrix(skillsRoot, plan, opts);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Legacy / explicit single-dir install
|
|
970
|
+
if (opts.dryRun) {
|
|
971
|
+
for (const e of plan.entries) log(` - ${e}`);
|
|
972
|
+
log(`(dry-run) would install → ${dest}`);
|
|
607
973
|
return;
|
|
608
974
|
}
|
|
609
975
|
|
|
@@ -624,6 +990,8 @@ async function cmdAdd(opts, sourceOverride) {
|
|
|
624
990
|
repoPath: opts.repoPath,
|
|
625
991
|
mode: opts.mode,
|
|
626
992
|
host: opts.host,
|
|
993
|
+
scope: opts.scope,
|
|
994
|
+
agents: opts.agents,
|
|
627
995
|
skillIds: plan.skillIds,
|
|
628
996
|
locale: opts.locale,
|
|
629
997
|
});
|
|
@@ -631,19 +999,29 @@ async function cmdAdd(opts, sourceOverride) {
|
|
|
631
999
|
log(`done → ${dest}`);
|
|
632
1000
|
log(`daily entry: ${path.join(dest, "topmind", "SKILL.md")}`);
|
|
633
1001
|
log(`update later: node bin/install-skills.mjs update --dest ${dest}`);
|
|
634
|
-
log(`community CLI (optional): npx skills add ${sourceRaw.includes("/") && !sourceRaw.includes("://") ? sourceRaw.split("@")[0] : DEFAULT_OWNER_REPO} -g -y`);
|
|
635
1002
|
} finally {
|
|
636
1003
|
await fs.rm(tmpRoot, { recursive: true, force: true }).catch(() => {});
|
|
637
1004
|
}
|
|
638
1005
|
}
|
|
639
1006
|
|
|
640
1007
|
async function cmdUpdate(opts) {
|
|
641
|
-
|
|
642
|
-
const
|
|
1008
|
+
// Prefer receipt at matrix canonical; fall back to explicit --dest receipt.
|
|
1009
|
+
const destCandidates = opts.dest
|
|
1010
|
+
? [opts.dest]
|
|
1011
|
+
: [canonicalBase(opts.global), canonicalBase(!opts.global), HOST_DEFAULTS["claude-code"]()];
|
|
1012
|
+
let dest = null;
|
|
1013
|
+
let receipt = null;
|
|
1014
|
+
for (const d of destCandidates) {
|
|
1015
|
+
receipt = await readReceipt(d);
|
|
1016
|
+
if (receipt) {
|
|
1017
|
+
dest = d;
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
643
1021
|
if (!receipt) {
|
|
644
1022
|
fail(
|
|
645
|
-
`no ${RECEIPT_NAME} in ${
|
|
646
|
-
` First install: node bin/install-skills.mjs add ${DEFAULT_OWNER_REPO}
|
|
1023
|
+
`no ${RECEIPT_NAME} in ${destCandidates.join(", ")}.\n` +
|
|
1024
|
+
` First install: node bin/install-skills.mjs add ${DEFAULT_OWNER_REPO} -g\n` +
|
|
647
1025
|
` Or community: npx skills update -g -y`,
|
|
648
1026
|
);
|
|
649
1027
|
}
|
|
@@ -654,11 +1032,17 @@ async function cmdUpdate(opts) {
|
|
|
654
1032
|
source: receipt.source_raw || receipt.update?.source || DEFAULT_OWNER_REPO,
|
|
655
1033
|
repoPath: receipt.repo_path || receipt.update?.path || DEFAULT_PATH,
|
|
656
1034
|
mode: receipt.mode === "symlink" ? "symlink" : "copy",
|
|
1035
|
+
linkMode: receipt.agents ? "symlink" : opts.linkMode,
|
|
657
1036
|
host: receipt.host || opts.host,
|
|
1037
|
+
scope: receipt.scope || opts.scope,
|
|
1038
|
+
agents: receipt.agents || opts.agents,
|
|
658
1039
|
locale: receipt.locale || opts.locale,
|
|
659
1040
|
};
|
|
660
|
-
//
|
|
661
|
-
if (next.
|
|
1041
|
+
// Keep matrix mode when the receipt was a matrix install (no single dest host).
|
|
1042
|
+
if (receipt.agents) next.dest = null;
|
|
1043
|
+
else if (!next.dest) next.dest = dest;
|
|
1044
|
+
|
|
1045
|
+
if (next.mode === "symlink" && next.dest) {
|
|
662
1046
|
const local = parseSource(next.source, next.repoPath);
|
|
663
1047
|
if (local.kind !== "local" || !(await pathExists(local.value))) {
|
|
664
1048
|
log("original symlink source gone — falling back to copy from default git source");
|
|
@@ -672,6 +1056,18 @@ async function cmdUpdate(opts) {
|
|
|
672
1056
|
async function main() {
|
|
673
1057
|
const opts = parseArgs(process.argv.slice(2));
|
|
674
1058
|
|
|
1059
|
+
if (opts.command === "agents") {
|
|
1060
|
+
cmdAgents();
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (opts.command === "paths") {
|
|
1064
|
+
cmdPaths(opts);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (opts.command === "doctor") {
|
|
1068
|
+
await cmdDoctor(opts);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
675
1071
|
if (opts.command === "list") {
|
|
676
1072
|
opts.dryRun = true;
|
|
677
1073
|
if (!opts.source) opts.source = DEFAULT_OWNER_REPO;
|
|
@@ -682,7 +1078,6 @@ async function main() {
|
|
|
682
1078
|
await cmdUpdate(opts);
|
|
683
1079
|
return;
|
|
684
1080
|
}
|
|
685
|
-
// add
|
|
686
1081
|
if (!opts.source) opts.source = DEFAULT_OWNER_REPO;
|
|
687
1082
|
await cmdAdd(opts);
|
|
688
1083
|
}
|