skillmux 0.1.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/README.md +111 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1537 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1535 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/commands/agents.ts
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
|
|
7
|
+
// src/config/resolve-skillmux-home.ts
|
|
8
|
+
import { join, resolve } from "path";
|
|
9
|
+
function buildConfigPath(skillmuxHome) {
|
|
10
|
+
return join(resolve(skillmuxHome), "config.json");
|
|
11
|
+
}
|
|
12
|
+
function resolveSkillmuxHome(homeDir) {
|
|
13
|
+
const resolvedHomeDir = resolve(homeDir);
|
|
14
|
+
const skillmuxHome = join(resolvedHomeDir, ".skillmux");
|
|
15
|
+
return {
|
|
16
|
+
skillmuxHome,
|
|
17
|
+
configPath: buildConfigPath(skillmuxHome)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/discovery/discover-agents.ts
|
|
22
|
+
import * as fs2 from "fs/promises";
|
|
23
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
24
|
+
|
|
25
|
+
// src/config/default-agent-rules.ts
|
|
26
|
+
var supportedPlatforms = ["win32", "linux", "darwin"];
|
|
27
|
+
var builtInAgentIds = [
|
|
28
|
+
"codex",
|
|
29
|
+
"claude",
|
|
30
|
+
"gemini",
|
|
31
|
+
"agents",
|
|
32
|
+
"openclaw"
|
|
33
|
+
];
|
|
34
|
+
var defaultAgentRules = [
|
|
35
|
+
{
|
|
36
|
+
id: "codex",
|
|
37
|
+
stableName: "OpenAI Codex",
|
|
38
|
+
supportedPlatforms: [...supportedPlatforms],
|
|
39
|
+
homeRelativeRootPath: ".codex",
|
|
40
|
+
skillsDirectoryPath: "skills",
|
|
41
|
+
enabledByDefault: true,
|
|
42
|
+
discovery: "builtin"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "claude",
|
|
46
|
+
stableName: "Claude Code",
|
|
47
|
+
supportedPlatforms: [...supportedPlatforms],
|
|
48
|
+
homeRelativeRootPath: ".claude",
|
|
49
|
+
skillsDirectoryPath: "skills",
|
|
50
|
+
enabledByDefault: true,
|
|
51
|
+
discovery: "builtin"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "gemini",
|
|
55
|
+
stableName: "Gemini CLI",
|
|
56
|
+
supportedPlatforms: [...supportedPlatforms],
|
|
57
|
+
homeRelativeRootPath: ".gemini",
|
|
58
|
+
skillsDirectoryPath: "skills",
|
|
59
|
+
enabledByDefault: true,
|
|
60
|
+
discovery: "builtin"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "agents",
|
|
64
|
+
stableName: "Agents",
|
|
65
|
+
supportedPlatforms: [...supportedPlatforms],
|
|
66
|
+
homeRelativeRootPath: ".agents",
|
|
67
|
+
skillsDirectoryPath: "skills",
|
|
68
|
+
enabledByDefault: true,
|
|
69
|
+
discovery: "builtin"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "openclaw",
|
|
73
|
+
stableName: "OpenClaw",
|
|
74
|
+
supportedPlatforms: [...supportedPlatforms],
|
|
75
|
+
homeRelativeRootPath: ".openclaw",
|
|
76
|
+
skillsDirectoryPath: "skills",
|
|
77
|
+
enabledByDefault: true,
|
|
78
|
+
discovery: "builtin"
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
var defaultAgentRuleMap = Object.fromEntries(
|
|
82
|
+
defaultAgentRules.map((rule) => [rule.id, rule])
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// src/config/load-user-config.ts
|
|
86
|
+
import * as fs from "fs/promises";
|
|
87
|
+
import { z } from "zod";
|
|
88
|
+
|
|
89
|
+
// src/core/errors.ts
|
|
90
|
+
var SkillMuxError = class extends Error {
|
|
91
|
+
constructor(message) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.name = new.target.name;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var ManifestValidationError = class extends SkillMuxError {
|
|
97
|
+
constructor(message) {
|
|
98
|
+
super(message);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var UserConfigValidationError = class extends SkillMuxError {
|
|
102
|
+
constructor(message) {
|
|
103
|
+
super(message);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/config/load-user-config.ts
|
|
108
|
+
var supportedPlatformSchema = z.enum(supportedPlatforms);
|
|
109
|
+
var agentOverrideSchema = z.object({
|
|
110
|
+
stableName: z.string().min(1).optional(),
|
|
111
|
+
supportedPlatforms: z.array(supportedPlatformSchema).min(1).optional(),
|
|
112
|
+
homeRelativeRootPath: z.string().min(1).optional(),
|
|
113
|
+
skillsDirectoryPath: z.string().min(1).optional(),
|
|
114
|
+
enabledByDefault: z.boolean().optional()
|
|
115
|
+
}).strict();
|
|
116
|
+
var userConfigSchema = z.object({
|
|
117
|
+
version: z.literal(1),
|
|
118
|
+
agents: z.record(z.string().min(1), agentOverrideSchema)
|
|
119
|
+
}).strict();
|
|
120
|
+
function createEmptyUserConfig() {
|
|
121
|
+
return {
|
|
122
|
+
version: 1,
|
|
123
|
+
agents: {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function formatValidationIssues(error) {
|
|
127
|
+
return error.issues.map((issue) => {
|
|
128
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
129
|
+
return `${path}: ${issue.message}`;
|
|
130
|
+
}).join("; ");
|
|
131
|
+
}
|
|
132
|
+
async function loadUserConfig(skillmuxHome) {
|
|
133
|
+
const configPath = buildConfigPath(skillmuxHome);
|
|
134
|
+
try {
|
|
135
|
+
const contents = await fs.readFile(configPath, "utf8");
|
|
136
|
+
const parsed = JSON.parse(contents);
|
|
137
|
+
const validated = userConfigSchema.safeParse(parsed);
|
|
138
|
+
if (!validated.success) {
|
|
139
|
+
throw new UserConfigValidationError(
|
|
140
|
+
`Invalid config at ${configPath}: ${formatValidationIssues(validated.error)}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return validated.data;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
146
|
+
return createEmptyUserConfig();
|
|
147
|
+
}
|
|
148
|
+
if (error instanceof SyntaxError) {
|
|
149
|
+
throw new UserConfigValidationError(
|
|
150
|
+
`Invalid config at ${configPath}: malformed JSON`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/discovery/discover-agents.ts
|
|
158
|
+
function mergeRule(rule, override) {
|
|
159
|
+
if (override === void 0) {
|
|
160
|
+
return rule;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
...rule,
|
|
164
|
+
...override
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function buildCustomRule(id, override) {
|
|
168
|
+
if (override.homeRelativeRootPath === void 0 || override.skillsDirectoryPath === void 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Custom agent override "${id}" must define homeRelativeRootPath and skillsDirectoryPath`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
id,
|
|
175
|
+
stableName: override.stableName ?? id,
|
|
176
|
+
supportedPlatforms: override.supportedPlatforms ?? [...supportedPlatforms],
|
|
177
|
+
homeRelativeRootPath: override.homeRelativeRootPath,
|
|
178
|
+
skillsDirectoryPath: override.skillsDirectoryPath,
|
|
179
|
+
enabledByDefault: override.enabledByDefault ?? true,
|
|
180
|
+
discovery: "custom"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function pathExists(path) {
|
|
184
|
+
try {
|
|
185
|
+
await fs2.access(path);
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function resolveAgentRulePaths(homeDir, rule) {
|
|
192
|
+
const absoluteRootPath = resolve2(homeDir, rule.homeRelativeRootPath);
|
|
193
|
+
return {
|
|
194
|
+
absoluteRootPath,
|
|
195
|
+
absoluteSkillsDirectoryPath: join2(
|
|
196
|
+
absoluteRootPath,
|
|
197
|
+
rule.skillsDirectoryPath
|
|
198
|
+
)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function discoverAgents(options) {
|
|
202
|
+
const platform = options.platform ?? process.platform;
|
|
203
|
+
const homeDir = resolve2(options.homeDir);
|
|
204
|
+
const skillmuxHome = options.skillmuxHome ?? resolveSkillmuxHome(homeDir).skillmuxHome;
|
|
205
|
+
const userConfig = await loadUserConfig(skillmuxHome);
|
|
206
|
+
const discoveredAgents = [];
|
|
207
|
+
for (const agentId of builtInAgentIds) {
|
|
208
|
+
const mergedRule = mergeRule(
|
|
209
|
+
defaultAgentRuleMap[agentId],
|
|
210
|
+
userConfig.agents[agentId]
|
|
211
|
+
);
|
|
212
|
+
const resolvedPaths = resolveAgentRulePaths(homeDir, mergedRule);
|
|
213
|
+
discoveredAgents.push({
|
|
214
|
+
...mergedRule,
|
|
215
|
+
...resolvedPaths,
|
|
216
|
+
exists: await pathExists(resolvedPaths.absoluteSkillsDirectoryPath),
|
|
217
|
+
supportedOnPlatform: mergedRule.supportedPlatforms.some(
|
|
218
|
+
(supportedPlatform) => supportedPlatform === platform
|
|
219
|
+
)
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
for (const [agentId, override] of Object.entries(userConfig.agents)) {
|
|
223
|
+
if (Object.hasOwn(defaultAgentRuleMap, agentId)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const customRule = buildCustomRule(agentId, override);
|
|
227
|
+
const resolvedPaths = resolveAgentRulePaths(homeDir, customRule);
|
|
228
|
+
discoveredAgents.push({
|
|
229
|
+
...customRule,
|
|
230
|
+
...resolvedPaths,
|
|
231
|
+
exists: await pathExists(resolvedPaths.absoluteSkillsDirectoryPath),
|
|
232
|
+
supportedOnPlatform: customRule.supportedPlatforms.some(
|
|
233
|
+
(supportedPlatform) => supportedPlatform === platform
|
|
234
|
+
)
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return discoveredAgents;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/output/print-json.ts
|
|
241
|
+
function printJson(value) {
|
|
242
|
+
return `${JSON.stringify(value, null, 2)}
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/output/print-table.ts
|
|
247
|
+
function printTable(rows, columns) {
|
|
248
|
+
const renderedRows = rows.map(
|
|
249
|
+
(row) => columns.map((column) => String(row[column.key] ?? ""))
|
|
250
|
+
);
|
|
251
|
+
const widths = columns.map(
|
|
252
|
+
(column, index) => Math.max(
|
|
253
|
+
column.label.length,
|
|
254
|
+
...renderedRows.map((row) => row[index]?.length ?? 0)
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
const header = columns.map((column, index) => column.label.padEnd(widths[index])).join(" ");
|
|
258
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
259
|
+
const body = renderedRows.map(
|
|
260
|
+
(row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ")
|
|
261
|
+
);
|
|
262
|
+
return `${[header, separator, ...body].join("\n")}
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/commands/agents.ts
|
|
267
|
+
function buildTableOutput(agents) {
|
|
268
|
+
return printTable(
|
|
269
|
+
agents.map((agent) => ({
|
|
270
|
+
id: agent.id,
|
|
271
|
+
name: agent.stableName,
|
|
272
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
273
|
+
exists: String(agent.exists),
|
|
274
|
+
supported: String(agent.supportedOnPlatform),
|
|
275
|
+
discovery: agent.discovery
|
|
276
|
+
})),
|
|
277
|
+
[
|
|
278
|
+
{ key: "id", label: "Agent" },
|
|
279
|
+
{ key: "name", label: "Name" },
|
|
280
|
+
{ key: "path", label: "Path" },
|
|
281
|
+
{ key: "exists", label: "Exists" },
|
|
282
|
+
{ key: "supported", label: "Supported" },
|
|
283
|
+
{ key: "discovery", label: "Discovery" }
|
|
284
|
+
]
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
async function runAgents(options = {}) {
|
|
288
|
+
const homeDir = options.homeDir ?? homedir();
|
|
289
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
290
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
291
|
+
const agents = await discoverAgents({
|
|
292
|
+
homeDir,
|
|
293
|
+
skillmuxHome,
|
|
294
|
+
platform: options.platform
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
agents,
|
|
298
|
+
output: options.json === true ? printJson(agents) : buildTableOutput(agents)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/commands/config.ts
|
|
303
|
+
import { homedir as homedir2 } from "os";
|
|
304
|
+
function buildTableOutput2(result) {
|
|
305
|
+
const summary = printTable(
|
|
306
|
+
[
|
|
307
|
+
{
|
|
308
|
+
skillmuxHome: result.skillmuxHome,
|
|
309
|
+
configPath: result.configPath,
|
|
310
|
+
overrides: String(Object.keys(result.config.agents).length)
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
[
|
|
314
|
+
{ key: "skillmuxHome", label: "SkillMux Home" },
|
|
315
|
+
{ key: "configPath", label: "Config Path" },
|
|
316
|
+
{ key: "overrides", label: "Overrides" }
|
|
317
|
+
]
|
|
318
|
+
);
|
|
319
|
+
const agentRows = Object.entries(result.config.agents).sort(([left], [right]) => left.localeCompare(right)).map(([agentId, agent]) => ({
|
|
320
|
+
agentId,
|
|
321
|
+
stableName: agent.stableName ?? "",
|
|
322
|
+
root: agent.homeRelativeRootPath ?? "",
|
|
323
|
+
skills: agent.skillsDirectoryPath ?? ""
|
|
324
|
+
}));
|
|
325
|
+
if (agentRows.length === 0) {
|
|
326
|
+
return `${summary}
|
|
327
|
+
No user overrides configured.
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
return `${summary}
|
|
331
|
+
${printTable(agentRows, [
|
|
332
|
+
{ key: "agentId", label: "Agent" },
|
|
333
|
+
{ key: "stableName", label: "Name" },
|
|
334
|
+
{ key: "root", label: "Root" },
|
|
335
|
+
{ key: "skills", label: "Skills Dir" }
|
|
336
|
+
])}`;
|
|
337
|
+
}
|
|
338
|
+
async function runConfig(options = {}) {
|
|
339
|
+
const homeDir = options.homeDir ?? homedir2();
|
|
340
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
341
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
342
|
+
const config = await loadUserConfig(skillmuxHome);
|
|
343
|
+
const resultWithoutOutput = {
|
|
344
|
+
skillmuxHome,
|
|
345
|
+
configPath: resolvedPaths.configPath,
|
|
346
|
+
config
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
...resultWithoutOutput,
|
|
350
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/commands/doctor.ts
|
|
355
|
+
import * as fs8 from "fs/promises";
|
|
356
|
+
import { homedir as homedir3 } from "os";
|
|
357
|
+
import { join as join5 } from "path";
|
|
358
|
+
|
|
359
|
+
// src/discovery/scan-agent-skills.ts
|
|
360
|
+
import * as fs5 from "fs/promises";
|
|
361
|
+
|
|
362
|
+
// src/discovery/infer-skill-entry.ts
|
|
363
|
+
import * as fs4 from "fs/promises";
|
|
364
|
+
import { basename, resolve as resolve4 } from "path";
|
|
365
|
+
|
|
366
|
+
// src/fs/path-utils.ts
|
|
367
|
+
import * as fs3 from "fs/promises";
|
|
368
|
+
import { dirname, parse, relative, resolve as resolve3, sep } from "path";
|
|
369
|
+
function normalizeAbsolutePath(path) {
|
|
370
|
+
const normalized = resolve3(path);
|
|
371
|
+
return process.platform === "win32" ? normalized.replaceAll("/", "\\").toLowerCase() : normalized;
|
|
372
|
+
}
|
|
373
|
+
function pathsAreEqual(left, right) {
|
|
374
|
+
return normalizeAbsolutePath(left) === normalizeAbsolutePath(right);
|
|
375
|
+
}
|
|
376
|
+
function isPathInside(parentPath, childPath) {
|
|
377
|
+
const parent = normalizeAbsolutePath(parentPath);
|
|
378
|
+
const child = normalizeAbsolutePath(childPath);
|
|
379
|
+
if (parse(parent).root !== parse(child).root) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const relativePath = relative(parent, child);
|
|
383
|
+
if (relativePath === "") {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if (relativePath === "..") {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
if (relativePath.startsWith(`..${sep}`)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
async function assertNoSymlinkAncestors(path, options) {
|
|
395
|
+
let current = options?.includeLeaf === true ? resolve3(path) : dirname(resolve3(path));
|
|
396
|
+
while (true) {
|
|
397
|
+
try {
|
|
398
|
+
const entry = await fs3.lstat(current);
|
|
399
|
+
if (entry.isSymbolicLink()) {
|
|
400
|
+
throw new Error(`Refusing to use path with symlink ancestor at ${current}`);
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error.code !== "ENOENT") {
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const parent = dirname(current);
|
|
408
|
+
if (parent === current) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
current = parent;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/discovery/infer-skill-entry.ts
|
|
416
|
+
function buildIssue(code, severity, message, path) {
|
|
417
|
+
return { code, severity, message, path };
|
|
418
|
+
}
|
|
419
|
+
async function inferSkillEntry(options) {
|
|
420
|
+
const absolutePath = resolve4(options.path);
|
|
421
|
+
const skillName = basename(absolutePath);
|
|
422
|
+
const stats = await fs4.lstat(absolutePath);
|
|
423
|
+
if (stats.isSymbolicLink()) {
|
|
424
|
+
try {
|
|
425
|
+
const targetPath = await fs4.realpath(absolutePath);
|
|
426
|
+
if (isPathInside(options.skillmuxHome, targetPath)) {
|
|
427
|
+
return {
|
|
428
|
+
entry: {
|
|
429
|
+
agentId: options.agentId,
|
|
430
|
+
agentName: options.agentName,
|
|
431
|
+
skillName,
|
|
432
|
+
kind: "managed-link",
|
|
433
|
+
path: absolutePath,
|
|
434
|
+
targetPath
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
entry: {
|
|
440
|
+
agentId: options.agentId,
|
|
441
|
+
agentName: options.agentName,
|
|
442
|
+
skillName,
|
|
443
|
+
kind: "unknown",
|
|
444
|
+
path: absolutePath,
|
|
445
|
+
targetPath
|
|
446
|
+
},
|
|
447
|
+
issue: buildIssue(
|
|
448
|
+
"unknown-entry",
|
|
449
|
+
"warning",
|
|
450
|
+
"Skill entry is a link that points outside the SkillMux managed store",
|
|
451
|
+
absolutePath
|
|
452
|
+
)
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (error.code !== "ENOENT") {
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
entry: {
|
|
460
|
+
agentId: options.agentId,
|
|
461
|
+
agentName: options.agentName,
|
|
462
|
+
skillName,
|
|
463
|
+
kind: "broken-link",
|
|
464
|
+
path: absolutePath
|
|
465
|
+
},
|
|
466
|
+
issue: buildIssue(
|
|
467
|
+
"broken-link",
|
|
468
|
+
"error",
|
|
469
|
+
"Skill entry points to a missing target",
|
|
470
|
+
absolutePath
|
|
471
|
+
)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (stats.isDirectory()) {
|
|
476
|
+
return {
|
|
477
|
+
entry: {
|
|
478
|
+
agentId: options.agentId,
|
|
479
|
+
agentName: options.agentName,
|
|
480
|
+
skillName,
|
|
481
|
+
kind: "unmanaged-directory",
|
|
482
|
+
path: absolutePath
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
entry: {
|
|
488
|
+
agentId: options.agentId,
|
|
489
|
+
agentName: options.agentName,
|
|
490
|
+
skillName,
|
|
491
|
+
kind: "unknown",
|
|
492
|
+
path: absolutePath
|
|
493
|
+
},
|
|
494
|
+
issue: buildIssue(
|
|
495
|
+
"unknown-entry",
|
|
496
|
+
"warning",
|
|
497
|
+
"Skill entry is neither a managed link nor a directory",
|
|
498
|
+
absolutePath
|
|
499
|
+
)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/discovery/scan-agent-skills.ts
|
|
504
|
+
async function scanAgentSkills(agent, skillmuxHome) {
|
|
505
|
+
if (!agent.exists || !agent.supportedOnPlatform) {
|
|
506
|
+
return {
|
|
507
|
+
entries: [],
|
|
508
|
+
issues: []
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const directoryEntries = await fs5.readdir(agent.absoluteSkillsDirectoryPath, {
|
|
512
|
+
withFileTypes: true
|
|
513
|
+
});
|
|
514
|
+
const sortedDirectoryEntries = [...directoryEntries].sort(
|
|
515
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
516
|
+
);
|
|
517
|
+
const entries = [];
|
|
518
|
+
const issues = [];
|
|
519
|
+
for (const directoryEntry of sortedDirectoryEntries) {
|
|
520
|
+
const result = await inferSkillEntry({
|
|
521
|
+
agentId: agent.id,
|
|
522
|
+
agentName: agent.stableName,
|
|
523
|
+
path: `${agent.absoluteSkillsDirectoryPath}/${directoryEntry.name}`,
|
|
524
|
+
skillmuxHome
|
|
525
|
+
});
|
|
526
|
+
entries.push(result.entry);
|
|
527
|
+
if (result.issue !== void 0) {
|
|
528
|
+
issues.push(result.issue);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
entries,
|
|
533
|
+
issues
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/manifest/read-manifest.ts
|
|
538
|
+
import * as fs7 from "fs/promises";
|
|
539
|
+
import { join as join4, resolve as resolve5 } from "path";
|
|
540
|
+
|
|
541
|
+
// src/manifest/build-empty-manifest.ts
|
|
542
|
+
function buildEmptyManifest(skillmuxHome) {
|
|
543
|
+
return {
|
|
544
|
+
version: 1,
|
|
545
|
+
skillmuxHome,
|
|
546
|
+
skills: {},
|
|
547
|
+
agents: {},
|
|
548
|
+
activations: [],
|
|
549
|
+
lastScan: {
|
|
550
|
+
at: null,
|
|
551
|
+
issues: []
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/manifest/manifest-schema.ts
|
|
557
|
+
import { z as z2 } from "zod";
|
|
558
|
+
|
|
559
|
+
// src/core/ids.ts
|
|
560
|
+
var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
561
|
+
function normalizeId(value) {
|
|
562
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
563
|
+
return normalized.length > 0 ? normalized : "skill";
|
|
564
|
+
}
|
|
565
|
+
function isValidId(value) {
|
|
566
|
+
return ID_PATTERN.test(value);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/manifest/manifest-schema.ts
|
|
570
|
+
var idSchema = z2.string().min(1).refine(isValidId, "Expected a canonical lowercase slug identifier");
|
|
571
|
+
var scanIssueSchema = z2.object({
|
|
572
|
+
code: z2.string().min(1),
|
|
573
|
+
severity: z2.enum(["info", "warning", "error"]),
|
|
574
|
+
message: z2.string().min(1),
|
|
575
|
+
path: z2.string().min(1).optional()
|
|
576
|
+
}).strict();
|
|
577
|
+
var managedSkillSchema = z2.object({
|
|
578
|
+
id: idSchema,
|
|
579
|
+
name: z2.string().min(1),
|
|
580
|
+
path: z2.string().min(1),
|
|
581
|
+
source: z2.object({
|
|
582
|
+
kind: z2.enum(["local", "imported"]),
|
|
583
|
+
path: z2.string().min(1)
|
|
584
|
+
}).strict(),
|
|
585
|
+
importedAt: z2.string().min(1)
|
|
586
|
+
}).strict();
|
|
587
|
+
var agentRecordSchema = z2.object({
|
|
588
|
+
id: idSchema,
|
|
589
|
+
name: z2.string().min(1),
|
|
590
|
+
path: z2.string().min(1),
|
|
591
|
+
discovery: z2.enum(["builtin", "custom"]),
|
|
592
|
+
available: z2.boolean(),
|
|
593
|
+
lastSeenAt: z2.string().min(1).nullable()
|
|
594
|
+
}).strict();
|
|
595
|
+
var activationRecordSchema = z2.object({
|
|
596
|
+
skillId: idSchema,
|
|
597
|
+
agentId: idSchema,
|
|
598
|
+
linkPath: z2.string().min(1),
|
|
599
|
+
state: z2.enum(["enabled", "disabled"]),
|
|
600
|
+
updatedAt: z2.string().min(1)
|
|
601
|
+
}).strict();
|
|
602
|
+
var manifestSchema = z2.object({
|
|
603
|
+
version: z2.literal(1),
|
|
604
|
+
skillmuxHome: z2.string().min(1),
|
|
605
|
+
skills: z2.record(z2.string(), managedSkillSchema),
|
|
606
|
+
agents: z2.record(z2.string(), agentRecordSchema),
|
|
607
|
+
activations: z2.array(activationRecordSchema),
|
|
608
|
+
lastScan: z2.object({
|
|
609
|
+
at: z2.string().min(1).nullable(),
|
|
610
|
+
issues: z2.array(scanIssueSchema)
|
|
611
|
+
}).strict()
|
|
612
|
+
}).strict().superRefine((manifest, ctx) => {
|
|
613
|
+
for (const [skillId, skill] of Object.entries(manifest.skills)) {
|
|
614
|
+
if (!isValidId(skillId)) {
|
|
615
|
+
ctx.addIssue({
|
|
616
|
+
code: z2.ZodIssueCode.custom,
|
|
617
|
+
path: ["skills", skillId],
|
|
618
|
+
message: `Invalid skill id key: ${skillId}`
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (skill.id !== skillId) {
|
|
622
|
+
ctx.addIssue({
|
|
623
|
+
code: z2.ZodIssueCode.custom,
|
|
624
|
+
path: ["skills", skillId, "id"],
|
|
625
|
+
message: `Skill id must match its record key: ${skillId}`
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const [agentId, agent] of Object.entries(manifest.agents)) {
|
|
630
|
+
if (!isValidId(agentId)) {
|
|
631
|
+
ctx.addIssue({
|
|
632
|
+
code: z2.ZodIssueCode.custom,
|
|
633
|
+
path: ["agents", agentId],
|
|
634
|
+
message: `Invalid agent id key: ${agentId}`
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (agent.id !== agentId) {
|
|
638
|
+
ctx.addIssue({
|
|
639
|
+
code: z2.ZodIssueCode.custom,
|
|
640
|
+
path: ["agents", agentId, "id"],
|
|
641
|
+
message: `Agent id must match its record key: ${agentId}`
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const activationPairs = /* @__PURE__ */ new Set();
|
|
646
|
+
manifest.activations.forEach((activation, index) => {
|
|
647
|
+
if (!(activation.skillId in manifest.skills)) {
|
|
648
|
+
ctx.addIssue({
|
|
649
|
+
code: z2.ZodIssueCode.custom,
|
|
650
|
+
path: ["activations", index, "skillId"],
|
|
651
|
+
message: `Unknown skill reference: ${activation.skillId}`
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (!(activation.agentId in manifest.agents)) {
|
|
655
|
+
ctx.addIssue({
|
|
656
|
+
code: z2.ZodIssueCode.custom,
|
|
657
|
+
path: ["activations", index, "agentId"],
|
|
658
|
+
message: `Unknown agent reference: ${activation.agentId}`
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const pairKey = `${activation.skillId}:${activation.agentId}`;
|
|
662
|
+
if (activationPairs.has(pairKey)) {
|
|
663
|
+
ctx.addIssue({
|
|
664
|
+
code: z2.ZodIssueCode.custom,
|
|
665
|
+
path: ["activations", index],
|
|
666
|
+
message: `Duplicate activation for ${pairKey}`
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
activationPairs.add(pairKey);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// src/manifest/write-manifest.ts
|
|
675
|
+
import { randomUUID } from "crypto";
|
|
676
|
+
import * as fs6 from "fs/promises";
|
|
677
|
+
import { join as join3 } from "path";
|
|
678
|
+
function getManifestPath(home) {
|
|
679
|
+
return join3(home, "manifest.json");
|
|
680
|
+
}
|
|
681
|
+
function createManifestTempPath(manifestPath) {
|
|
682
|
+
return `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
683
|
+
}
|
|
684
|
+
async function writeManifest(home, manifest) {
|
|
685
|
+
await fs6.mkdir(home, { recursive: true });
|
|
686
|
+
const manifestPath = getManifestPath(home);
|
|
687
|
+
const tempPath = createManifestTempPath(manifestPath);
|
|
688
|
+
const contents = `${JSON.stringify(manifest, null, 2)}
|
|
689
|
+
`;
|
|
690
|
+
await fs6.writeFile(tempPath, contents, "utf8");
|
|
691
|
+
try {
|
|
692
|
+
await fs6.rename(tempPath, manifestPath);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
await fs6.unlink(tempPath).catch(() => void 0);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/manifest/read-manifest.ts
|
|
700
|
+
function getManifestPath2(home) {
|
|
701
|
+
return join4(home, "manifest.json");
|
|
702
|
+
}
|
|
703
|
+
function normalizeHomePath(home) {
|
|
704
|
+
const resolvedHome = resolve5(home);
|
|
705
|
+
return process.platform === "win32" ? resolvedHome.toLowerCase() : resolvedHome;
|
|
706
|
+
}
|
|
707
|
+
function formatValidationIssues2(error) {
|
|
708
|
+
return error.issues.map((issue) => {
|
|
709
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
710
|
+
return `${path}: ${issue.message}`;
|
|
711
|
+
}).join("; ");
|
|
712
|
+
}
|
|
713
|
+
async function readManifest(home) {
|
|
714
|
+
const manifestPath = getManifestPath2(home);
|
|
715
|
+
try {
|
|
716
|
+
const contents = await fs7.readFile(manifestPath, "utf8");
|
|
717
|
+
const parsedJson = JSON.parse(contents);
|
|
718
|
+
const parsedManifest = manifestSchema.safeParse(parsedJson);
|
|
719
|
+
if (!parsedManifest.success) {
|
|
720
|
+
throw new ManifestValidationError(
|
|
721
|
+
`Invalid manifest at ${manifestPath}: ${formatValidationIssues2(parsedManifest.error)}`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
if (normalizeHomePath(parsedManifest.data.skillmuxHome) !== normalizeHomePath(home)) {
|
|
725
|
+
throw new ManifestValidationError(
|
|
726
|
+
`Invalid manifest at ${manifestPath}: skillmuxHome must match ${home}`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
return parsedManifest.data;
|
|
730
|
+
} catch (error) {
|
|
731
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
732
|
+
const emptyManifest = buildEmptyManifest(home);
|
|
733
|
+
await writeManifest(home, emptyManifest);
|
|
734
|
+
return emptyManifest;
|
|
735
|
+
}
|
|
736
|
+
if (error instanceof SyntaxError) {
|
|
737
|
+
throw new ManifestValidationError(
|
|
738
|
+
`Invalid manifest at ${manifestPath}: malformed JSON`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/commands/doctor.ts
|
|
746
|
+
function buildIssue2(code, severity, message, path) {
|
|
747
|
+
return path === void 0 ? { code, severity, message } : { code, severity, message, path };
|
|
748
|
+
}
|
|
749
|
+
async function pathExists2(path) {
|
|
750
|
+
try {
|
|
751
|
+
await fs8.access(path);
|
|
752
|
+
return true;
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (error.code === "ENOENT") {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function addUnmanagedDirectoryIssues(entries, issues) {
|
|
761
|
+
for (const entry of entries) {
|
|
762
|
+
if (entry.kind !== "unmanaged-directory") {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (await pathExists2(join5(entry.path, "SKILL.md"))) {
|
|
766
|
+
issues.push(
|
|
767
|
+
buildIssue2(
|
|
768
|
+
"unmanaged-skill-directory",
|
|
769
|
+
"warning",
|
|
770
|
+
`Unmanaged skill directory is present for ${entry.agentId}/${entry.skillName}`,
|
|
771
|
+
entry.path
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function addMissingManagedSkillIssues(manifest, issues) {
|
|
778
|
+
for (const skill of Object.values(manifest.skills)) {
|
|
779
|
+
if (await pathExists2(skill.path)) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
issues.push(
|
|
783
|
+
buildIssue2(
|
|
784
|
+
"missing-managed-skill-path",
|
|
785
|
+
"error",
|
|
786
|
+
`Managed skill path is missing for ${skill.id}`,
|
|
787
|
+
skill.path
|
|
788
|
+
)
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function addConflictingAgentPathIssues(agents, issues) {
|
|
793
|
+
const pathToAgents = /* @__PURE__ */ new Map();
|
|
794
|
+
for (const agent of agents) {
|
|
795
|
+
const key = normalizeAbsolutePath(agent.absoluteSkillsDirectoryPath);
|
|
796
|
+
const current = pathToAgents.get(key) ?? [];
|
|
797
|
+
current.push(agent);
|
|
798
|
+
pathToAgents.set(key, current);
|
|
799
|
+
}
|
|
800
|
+
for (const conflictedAgents of pathToAgents.values()) {
|
|
801
|
+
if (conflictedAgents.length < 2) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const agentIds = conflictedAgents.map((agent) => agent.id).sort((left, right) => left.localeCompare(right));
|
|
805
|
+
issues.push(
|
|
806
|
+
buildIssue2(
|
|
807
|
+
"conflicting-agent-path",
|
|
808
|
+
"warning",
|
|
809
|
+
`Multiple agents resolve to the same skills directory: ${agentIds.join(", ")}`,
|
|
810
|
+
conflictedAgents[0].absoluteSkillsDirectoryPath
|
|
811
|
+
)
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function buildTableOutput3(issues) {
|
|
816
|
+
if (issues.length === 0) {
|
|
817
|
+
return "No doctor issues found.\n";
|
|
818
|
+
}
|
|
819
|
+
return printTable(
|
|
820
|
+
issues.map((issue) => ({
|
|
821
|
+
severity: issue.severity,
|
|
822
|
+
code: issue.code,
|
|
823
|
+
path: issue.path ?? "",
|
|
824
|
+
message: issue.message
|
|
825
|
+
})),
|
|
826
|
+
[
|
|
827
|
+
{ key: "severity", label: "Severity" },
|
|
828
|
+
{ key: "code", label: "Code" },
|
|
829
|
+
{ key: "path", label: "Path" },
|
|
830
|
+
{ key: "message", label: "Message" }
|
|
831
|
+
]
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
function buildJsonOutput(result) {
|
|
835
|
+
return printJson({
|
|
836
|
+
skillmuxHome: result.skillmuxHome,
|
|
837
|
+
issues: result.issues,
|
|
838
|
+
agents: result.agents.map((agent) => ({
|
|
839
|
+
id: agent.id,
|
|
840
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
841
|
+
supportedOnPlatform: agent.supportedOnPlatform
|
|
842
|
+
})),
|
|
843
|
+
entries: result.entries
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
async function runDoctor(options = {}) {
|
|
847
|
+
const homeDir = options.homeDir ?? homedir3();
|
|
848
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
849
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
850
|
+
const [manifest, config, agents] = await Promise.all([
|
|
851
|
+
readManifest(skillmuxHome),
|
|
852
|
+
loadUserConfig(skillmuxHome),
|
|
853
|
+
discoverAgents({
|
|
854
|
+
homeDir,
|
|
855
|
+
skillmuxHome,
|
|
856
|
+
platform: options.platform
|
|
857
|
+
})
|
|
858
|
+
]);
|
|
859
|
+
const entries = [];
|
|
860
|
+
const issues = [];
|
|
861
|
+
for (const agent of agents) {
|
|
862
|
+
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
863
|
+
entries.push(...scannedAgent.entries);
|
|
864
|
+
issues.push(...scannedAgent.issues);
|
|
865
|
+
}
|
|
866
|
+
await addUnmanagedDirectoryIssues(entries, issues);
|
|
867
|
+
await addMissingManagedSkillIssues(manifest, issues);
|
|
868
|
+
addConflictingAgentPathIssues(agents, issues);
|
|
869
|
+
const dedupedIssues = [...issues].sort((left, right) => {
|
|
870
|
+
const leftKey = `${left.severity}:${left.code}:${left.path ?? ""}:${left.message}`;
|
|
871
|
+
const rightKey = `${right.severity}:${right.code}:${right.path ?? ""}:${right.message}`;
|
|
872
|
+
return leftKey.localeCompare(rightKey);
|
|
873
|
+
});
|
|
874
|
+
const resultWithoutOutput = {
|
|
875
|
+
skillmuxHome,
|
|
876
|
+
manifest,
|
|
877
|
+
config,
|
|
878
|
+
agents,
|
|
879
|
+
entries,
|
|
880
|
+
issues: dedupedIssues
|
|
881
|
+
};
|
|
882
|
+
return {
|
|
883
|
+
...resultWithoutOutput,
|
|
884
|
+
output: options.json === true ? buildJsonOutput(resultWithoutOutput) : buildTableOutput3(dedupedIssues)
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/commands/disable.ts
|
|
889
|
+
import * as fs11 from "fs/promises";
|
|
890
|
+
import { homedir as homedir4 } from "os";
|
|
891
|
+
import { join as join6 } from "path";
|
|
892
|
+
|
|
893
|
+
// src/fs/link-ops.ts
|
|
894
|
+
import * as fs9 from "fs/promises";
|
|
895
|
+
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
896
|
+
var directoryLinkType = process.platform === "win32" ? "junction" : "dir";
|
|
897
|
+
async function createManagedLink(linkPath, targetPath) {
|
|
898
|
+
const resolvedLinkPath = resolve6(linkPath);
|
|
899
|
+
const resolvedTargetPath = resolve6(targetPath);
|
|
900
|
+
await assertNoSymlinkAncestors(resolvedLinkPath);
|
|
901
|
+
await assertNoSymlinkAncestors(resolvedTargetPath, { includeLeaf: true });
|
|
902
|
+
await fs9.mkdir(dirname2(resolvedLinkPath), { recursive: true });
|
|
903
|
+
try {
|
|
904
|
+
const existingEntry = await fs9.lstat(resolvedLinkPath);
|
|
905
|
+
if (!existingEntry.isSymbolicLink()) {
|
|
906
|
+
throw new Error(`Refusing to replace non-link entry at ${resolvedLinkPath}`);
|
|
907
|
+
}
|
|
908
|
+
const currentTargetPath = await fs9.realpath(resolvedLinkPath);
|
|
909
|
+
if (pathsAreEqual(currentTargetPath, resolvedTargetPath)) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
throw new Error(`Refusing to replace link at ${resolvedLinkPath}`);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (error.code === "ENOENT" && await fs9.lstat(resolvedLinkPath).then((entry) => entry.isSymbolicLink()).catch(() => false)) {
|
|
915
|
+
await fs9.rm(resolvedLinkPath, { recursive: true, force: false });
|
|
916
|
+
} else if (error.code !== "ENOENT") {
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
await fs9.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
|
|
921
|
+
}
|
|
922
|
+
async function isLinkPointingToTarget(linkPath, targetPath) {
|
|
923
|
+
try {
|
|
924
|
+
const entry = await fs9.lstat(linkPath);
|
|
925
|
+
if (!entry.isSymbolicLink()) {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
const resolvedTargetPath = await fs9.realpath(linkPath);
|
|
929
|
+
return pathsAreEqual(resolvedTargetPath, targetPath);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (error.code === "ENOENT") {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/fs/safe-remove-link.ts
|
|
939
|
+
import * as fs10 from "fs/promises";
|
|
940
|
+
async function safeRemoveLink(path) {
|
|
941
|
+
try {
|
|
942
|
+
const entry = await fs10.lstat(path);
|
|
943
|
+
if (!entry.isSymbolicLink()) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
await fs10.rm(path, { recursive: true, force: false });
|
|
947
|
+
return true;
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (error.code === "ENOENT") {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/commands/disable.ts
|
|
957
|
+
function buildAgentRecord(agent, timestamp) {
|
|
958
|
+
return {
|
|
959
|
+
id: agent.id,
|
|
960
|
+
name: agent.stableName,
|
|
961
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
962
|
+
discovery: agent.discovery,
|
|
963
|
+
available: agent.exists && agent.supportedOnPlatform,
|
|
964
|
+
lastSeenAt: agent.exists ? timestamp : null
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function buildActivationRecord(skillId, agentId, linkPath, timestamp) {
|
|
968
|
+
return {
|
|
969
|
+
skillId,
|
|
970
|
+
agentId,
|
|
971
|
+
linkPath,
|
|
972
|
+
state: "disabled",
|
|
973
|
+
updatedAt: timestamp
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function upsertActivation(manifest, activation) {
|
|
977
|
+
const index = manifest.activations.findIndex(
|
|
978
|
+
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
979
|
+
);
|
|
980
|
+
if (index === -1) {
|
|
981
|
+
manifest.activations.push(activation);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
manifest.activations[index] = activation;
|
|
985
|
+
}
|
|
986
|
+
async function resolveTargetAgent(homeDir, skillmuxHome, agentName) {
|
|
987
|
+
const agentId = normalizeId(agentName);
|
|
988
|
+
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
989
|
+
const agent = agents.find((entry) => entry.id === agentId);
|
|
990
|
+
if (agent === void 0) {
|
|
991
|
+
throw new Error(`Unknown agent: ${agentName}`);
|
|
992
|
+
}
|
|
993
|
+
if (!agent.supportedOnPlatform) {
|
|
994
|
+
throw new Error(`Agent ${agent.id} is not supported on ${process.platform}`);
|
|
995
|
+
}
|
|
996
|
+
return agent;
|
|
997
|
+
}
|
|
998
|
+
async function pathExists3(path) {
|
|
999
|
+
try {
|
|
1000
|
+
await fs11.lstat(path);
|
|
1001
|
+
return true;
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (error.code === "ENOENT") {
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async function runDisable(options) {
|
|
1010
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
1011
|
+
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1012
|
+
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1013
|
+
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1014
|
+
const manifest = await readManifest(skillmuxHome);
|
|
1015
|
+
const skillId = normalizeId(options.skill);
|
|
1016
|
+
const skill = manifest.skills[skillId];
|
|
1017
|
+
if (skill === void 0) {
|
|
1018
|
+
throw new Error(`Managed skill not found: ${skillId}`);
|
|
1019
|
+
}
|
|
1020
|
+
const agent = await resolveTargetAgent(homeDir, skillmuxHome, options.agent);
|
|
1021
|
+
const currentActivation = manifest.activations.find(
|
|
1022
|
+
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1023
|
+
);
|
|
1024
|
+
const linkPath = currentActivation?.linkPath ?? join6(agent.absoluteSkillsDirectoryPath, skill.id);
|
|
1025
|
+
const agentRecord = buildAgentRecord(agent, timestamp);
|
|
1026
|
+
manifest.agents[agent.id] = agentRecord;
|
|
1027
|
+
const linkMatchesSkill = await isLinkPointingToTarget(linkPath, skill.path);
|
|
1028
|
+
if (!linkMatchesSkill && await pathExists3(linkPath)) {
|
|
1029
|
+
throw new Error(`Refusing to disable non-managed entry at ${linkPath}`);
|
|
1030
|
+
}
|
|
1031
|
+
const removedLink = linkMatchesSkill ? await safeRemoveLink(linkPath) : false;
|
|
1032
|
+
if (removedLink === false && currentActivation?.state !== "enabled") {
|
|
1033
|
+
return {
|
|
1034
|
+
changed: false,
|
|
1035
|
+
skill,
|
|
1036
|
+
agent: agentRecord,
|
|
1037
|
+
activation: currentActivation ?? null,
|
|
1038
|
+
manifest,
|
|
1039
|
+
output: `${skill.id} is already disabled for ${agent.id}
|
|
1040
|
+
`
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const activation = buildActivationRecord(skill.id, agent.id, linkPath, timestamp);
|
|
1044
|
+
upsertActivation(manifest, activation);
|
|
1045
|
+
await writeManifest(skillmuxHome, manifest);
|
|
1046
|
+
return {
|
|
1047
|
+
changed: true,
|
|
1048
|
+
skill,
|
|
1049
|
+
agent: agentRecord,
|
|
1050
|
+
activation,
|
|
1051
|
+
manifest,
|
|
1052
|
+
output: `Disabled ${skill.id} for ${agent.id}
|
|
1053
|
+
`
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/commands/enable.ts
|
|
1058
|
+
import * as fs12 from "fs/promises";
|
|
1059
|
+
import { homedir as homedir5 } from "os";
|
|
1060
|
+
import { join as join7 } from "path";
|
|
1061
|
+
function buildAgentRecord2(agent, timestamp) {
|
|
1062
|
+
return {
|
|
1063
|
+
id: agent.id,
|
|
1064
|
+
name: agent.stableName,
|
|
1065
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
1066
|
+
discovery: agent.discovery,
|
|
1067
|
+
available: agent.supportedOnPlatform,
|
|
1068
|
+
lastSeenAt: timestamp
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function buildActivationRecord2(skillId, agentId, linkPath, timestamp, state) {
|
|
1072
|
+
return {
|
|
1073
|
+
skillId,
|
|
1074
|
+
agentId,
|
|
1075
|
+
linkPath,
|
|
1076
|
+
state,
|
|
1077
|
+
updatedAt: timestamp
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function upsertActivation2(manifest, activation) {
|
|
1081
|
+
const index = manifest.activations.findIndex(
|
|
1082
|
+
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
1083
|
+
);
|
|
1084
|
+
if (index === -1) {
|
|
1085
|
+
manifest.activations.push(activation);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
manifest.activations[index] = activation;
|
|
1089
|
+
}
|
|
1090
|
+
async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
1091
|
+
const agentId = normalizeId(agentName);
|
|
1092
|
+
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
1093
|
+
const agent = agents.find((entry) => entry.id === agentId);
|
|
1094
|
+
if (agent === void 0) {
|
|
1095
|
+
throw new Error(`Unknown agent: ${agentName}`);
|
|
1096
|
+
}
|
|
1097
|
+
if (!agent.supportedOnPlatform) {
|
|
1098
|
+
throw new Error(`Agent ${agent.id} is not supported on ${process.platform}`);
|
|
1099
|
+
}
|
|
1100
|
+
return agent;
|
|
1101
|
+
}
|
|
1102
|
+
async function runEnable(options) {
|
|
1103
|
+
const homeDir = options.homeDir ?? homedir5();
|
|
1104
|
+
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1105
|
+
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1106
|
+
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1107
|
+
const manifest = await readManifest(skillmuxHome);
|
|
1108
|
+
const skillId = normalizeId(options.skill);
|
|
1109
|
+
const skill = manifest.skills[skillId];
|
|
1110
|
+
if (skill === void 0) {
|
|
1111
|
+
throw new Error(`Managed skill not found: ${skillId}`);
|
|
1112
|
+
}
|
|
1113
|
+
const agent = await resolveTargetAgent2(homeDir, skillmuxHome, options.agent);
|
|
1114
|
+
const linkPath = join7(agent.absoluteSkillsDirectoryPath, skill.id);
|
|
1115
|
+
const currentActivation = manifest.activations.find(
|
|
1116
|
+
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1117
|
+
);
|
|
1118
|
+
const agentRecord = buildAgentRecord2(agent, timestamp);
|
|
1119
|
+
manifest.agents[agent.id] = agentRecord;
|
|
1120
|
+
await fs12.mkdir(agent.absoluteSkillsDirectoryPath, { recursive: true });
|
|
1121
|
+
const linkAlreadyEnabled = await isLinkPointingToTarget(linkPath, skill.path);
|
|
1122
|
+
const activationAlreadyEnabled = currentActivation?.state === "enabled" && currentActivation.linkPath === linkPath;
|
|
1123
|
+
if (linkAlreadyEnabled && activationAlreadyEnabled) {
|
|
1124
|
+
return {
|
|
1125
|
+
changed: false,
|
|
1126
|
+
skill,
|
|
1127
|
+
agent: agentRecord,
|
|
1128
|
+
activation: currentActivation,
|
|
1129
|
+
manifest,
|
|
1130
|
+
output: `${skill.id} is already enabled for ${agent.id}
|
|
1131
|
+
`
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
await createManagedLink(linkPath, skill.path);
|
|
1135
|
+
const activation = buildActivationRecord2(
|
|
1136
|
+
skill.id,
|
|
1137
|
+
agent.id,
|
|
1138
|
+
linkPath,
|
|
1139
|
+
timestamp,
|
|
1140
|
+
"enabled"
|
|
1141
|
+
);
|
|
1142
|
+
upsertActivation2(manifest, activation);
|
|
1143
|
+
await writeManifest(skillmuxHome, manifest);
|
|
1144
|
+
return {
|
|
1145
|
+
changed: true,
|
|
1146
|
+
skill,
|
|
1147
|
+
agent: agentRecord,
|
|
1148
|
+
activation,
|
|
1149
|
+
manifest,
|
|
1150
|
+
output: `Enabled ${skill.id} for ${agent.id}
|
|
1151
|
+
`
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/commands/import.ts
|
|
1156
|
+
import { resolve as resolve8 } from "path";
|
|
1157
|
+
import { homedir as homedir6 } from "os";
|
|
1158
|
+
|
|
1159
|
+
// src/fs/safe-copy.ts
|
|
1160
|
+
import * as fs13 from "fs/promises";
|
|
1161
|
+
import { dirname as dirname3, join as join8, resolve as resolve7 } from "path";
|
|
1162
|
+
async function assertDirectory(path) {
|
|
1163
|
+
const entry = await fs13.lstat(path);
|
|
1164
|
+
if (!entry.isDirectory()) {
|
|
1165
|
+
throw new Error(`Expected a directory at ${path}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
async function assertRegularFile(path, label) {
|
|
1169
|
+
const entry = await fs13.lstat(path);
|
|
1170
|
+
if (!entry.isFile()) {
|
|
1171
|
+
throw new Error(`Expected ${label} to be a regular file at ${path}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async function assertTargetDoesNotExist(path) {
|
|
1175
|
+
try {
|
|
1176
|
+
await fs13.lstat(path);
|
|
1177
|
+
throw new Error(`Refusing to overwrite existing path at ${path}`);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
if (error.code !== "ENOENT") {
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function copyDirectoryContents(sourcePath, targetPath) {
|
|
1185
|
+
await fs13.mkdir(targetPath, { recursive: true });
|
|
1186
|
+
const entries = await fs13.readdir(sourcePath, { withFileTypes: true });
|
|
1187
|
+
for (const entry of entries) {
|
|
1188
|
+
const sourceEntryPath = join8(sourcePath, entry.name);
|
|
1189
|
+
const targetEntryPath = join8(targetPath, entry.name);
|
|
1190
|
+
const entryStats = await fs13.lstat(sourceEntryPath);
|
|
1191
|
+
if (entryStats.isSymbolicLink()) {
|
|
1192
|
+
throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
|
|
1193
|
+
}
|
|
1194
|
+
if (entryStats.isDirectory()) {
|
|
1195
|
+
await copyDirectoryContents(sourceEntryPath, targetEntryPath);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (entryStats.isFile()) {
|
|
1199
|
+
await fs13.mkdir(dirname3(targetEntryPath), { recursive: true });
|
|
1200
|
+
await fs13.copyFile(sourceEntryPath, targetEntryPath);
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
throw new Error(`Unsupported filesystem entry at ${sourceEntryPath}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
async function assertSkillSourceLayout(sourcePath) {
|
|
1207
|
+
const resolvedSourcePath = resolve7(sourcePath);
|
|
1208
|
+
const skillFilePath = join8(resolvedSourcePath, "SKILL.md");
|
|
1209
|
+
await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
|
|
1210
|
+
await assertDirectory(resolvedSourcePath);
|
|
1211
|
+
try {
|
|
1212
|
+
await assertRegularFile(skillFilePath, "SKILL.md");
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
if (error.code === "ENOENT") {
|
|
1215
|
+
throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
|
|
1216
|
+
}
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function copySkillContentsToManagedStore(sourcePath, targetPath) {
|
|
1221
|
+
const resolvedSourcePath = resolve7(sourcePath);
|
|
1222
|
+
const resolvedTargetPath = resolve7(targetPath);
|
|
1223
|
+
if (pathsAreEqual(resolvedSourcePath, resolvedTargetPath)) {
|
|
1224
|
+
throw new Error("Source and target paths must differ");
|
|
1225
|
+
}
|
|
1226
|
+
if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
|
|
1227
|
+
throw new Error("Refusing to copy into a child of the source directory");
|
|
1228
|
+
}
|
|
1229
|
+
await assertSkillSourceLayout(resolvedSourcePath);
|
|
1230
|
+
await assertNoSymlinkAncestors(resolvedTargetPath);
|
|
1231
|
+
await assertTargetDoesNotExist(resolvedTargetPath);
|
|
1232
|
+
await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/commands/import.ts
|
|
1236
|
+
function buildManagedSkillPath(skillmuxHome, skillId) {
|
|
1237
|
+
return resolve8(skillmuxHome, "skills", skillId);
|
|
1238
|
+
}
|
|
1239
|
+
async function runImport(options) {
|
|
1240
|
+
const homeDir = options.homeDir ?? homedir6();
|
|
1241
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1242
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1243
|
+
const sourcePath = resolve8(options.sourcePath);
|
|
1244
|
+
const skillId = normalizeId(options.skillName);
|
|
1245
|
+
const importedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1246
|
+
const manifest = await readManifest(skillmuxHome);
|
|
1247
|
+
const managedSkillPath = buildManagedSkillPath(skillmuxHome, skillId);
|
|
1248
|
+
await assertSkillSourceLayout(sourcePath);
|
|
1249
|
+
if (manifest.skills[skillId] !== void 0) {
|
|
1250
|
+
throw new Error(`Managed skill already exists for ${skillId}`);
|
|
1251
|
+
}
|
|
1252
|
+
await copySkillContentsToManagedStore(sourcePath, managedSkillPath);
|
|
1253
|
+
const skill = {
|
|
1254
|
+
id: skillId,
|
|
1255
|
+
name: options.skillName,
|
|
1256
|
+
path: managedSkillPath,
|
|
1257
|
+
source: {
|
|
1258
|
+
kind: "local",
|
|
1259
|
+
path: sourcePath
|
|
1260
|
+
},
|
|
1261
|
+
importedAt
|
|
1262
|
+
};
|
|
1263
|
+
manifest.skills[skillId] = skill;
|
|
1264
|
+
await writeManifest(skillmuxHome, manifest);
|
|
1265
|
+
return {
|
|
1266
|
+
skill,
|
|
1267
|
+
manifest,
|
|
1268
|
+
output: `Imported ${skillId} to ${managedSkillPath}
|
|
1269
|
+
`
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/commands/scan.ts
|
|
1274
|
+
import { homedir as homedir7 } from "os";
|
|
1275
|
+
|
|
1276
|
+
// src/output/format-issue.ts
|
|
1277
|
+
function formatIssue(issue) {
|
|
1278
|
+
if (issue.path === void 0) {
|
|
1279
|
+
return `[${issue.severity}] ${issue.code}: ${issue.message}`;
|
|
1280
|
+
}
|
|
1281
|
+
return `[${issue.severity}] ${issue.code} @ ${issue.path}: ${issue.message}`;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/commands/scan.ts
|
|
1285
|
+
function buildAgentRecord3(agent, timestamp, previousRecord) {
|
|
1286
|
+
const lastSeenAt = agent.exists ? timestamp : previousRecord?.lastSeenAt ?? null;
|
|
1287
|
+
return {
|
|
1288
|
+
id: agent.id,
|
|
1289
|
+
name: agent.stableName,
|
|
1290
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
1291
|
+
discovery: agent.discovery,
|
|
1292
|
+
available: agent.exists && agent.supportedOnPlatform,
|
|
1293
|
+
lastSeenAt
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function buildScanOutput(result, json) {
|
|
1297
|
+
if (json) {
|
|
1298
|
+
return printJson({
|
|
1299
|
+
lastScan: result.manifest.lastScan,
|
|
1300
|
+
agents: result.agents.map((agent) => ({
|
|
1301
|
+
id: agent.id,
|
|
1302
|
+
name: agent.stableName,
|
|
1303
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
1304
|
+
exists: agent.exists,
|
|
1305
|
+
supportedOnPlatform: agent.supportedOnPlatform
|
|
1306
|
+
})),
|
|
1307
|
+
entries: result.entries
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
if (result.entries.length === 0) {
|
|
1311
|
+
return "No skill entries found.\n";
|
|
1312
|
+
}
|
|
1313
|
+
const table = printTable(
|
|
1314
|
+
result.entries.map((entry) => ({
|
|
1315
|
+
agent: entry.agentId,
|
|
1316
|
+
skill: entry.skillName,
|
|
1317
|
+
kind: entry.kind,
|
|
1318
|
+
path: entry.path
|
|
1319
|
+
})),
|
|
1320
|
+
[
|
|
1321
|
+
{ key: "agent", label: "Agent" },
|
|
1322
|
+
{ key: "skill", label: "Skill" },
|
|
1323
|
+
{ key: "kind", label: "Kind" },
|
|
1324
|
+
{ key: "path", label: "Path" }
|
|
1325
|
+
]
|
|
1326
|
+
);
|
|
1327
|
+
if (result.issues.length === 0) {
|
|
1328
|
+
return table;
|
|
1329
|
+
}
|
|
1330
|
+
return `${table}
|
|
1331
|
+
${result.issues.map(formatIssue).join("\n")}
|
|
1332
|
+
`;
|
|
1333
|
+
}
|
|
1334
|
+
async function runScan(options = {}) {
|
|
1335
|
+
const homeDir = options.homeDir ?? homedir7();
|
|
1336
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1337
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1338
|
+
const manifest = await readManifest(skillmuxHome);
|
|
1339
|
+
const agents = await discoverAgents({
|
|
1340
|
+
homeDir,
|
|
1341
|
+
platform: options.platform,
|
|
1342
|
+
skillmuxHome
|
|
1343
|
+
});
|
|
1344
|
+
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1345
|
+
const entries = [];
|
|
1346
|
+
const issues = [];
|
|
1347
|
+
for (const agent of agents) {
|
|
1348
|
+
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
1349
|
+
entries.push(...scannedAgent.entries);
|
|
1350
|
+
issues.push(...scannedAgent.issues);
|
|
1351
|
+
manifest.agents[agent.id] = buildAgentRecord3(
|
|
1352
|
+
agent,
|
|
1353
|
+
timestamp,
|
|
1354
|
+
manifest.agents[agent.id]
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
manifest.lastScan = {
|
|
1358
|
+
at: timestamp,
|
|
1359
|
+
issues
|
|
1360
|
+
};
|
|
1361
|
+
await writeManifest(skillmuxHome, manifest);
|
|
1362
|
+
const resultWithoutOutput = {
|
|
1363
|
+
manifest,
|
|
1364
|
+
agents,
|
|
1365
|
+
entries,
|
|
1366
|
+
issues
|
|
1367
|
+
};
|
|
1368
|
+
return {
|
|
1369
|
+
...resultWithoutOutput,
|
|
1370
|
+
output: buildScanOutput(resultWithoutOutput, options.json === true)
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/commands/list.ts
|
|
1375
|
+
function buildRecordsView(scanResult) {
|
|
1376
|
+
return {
|
|
1377
|
+
view: "records",
|
|
1378
|
+
records: scanResult.entries,
|
|
1379
|
+
issues: scanResult.issues
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function buildAgentsView(scanResult) {
|
|
1383
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1384
|
+
for (const entry of scanResult.entries) {
|
|
1385
|
+
const current = groups.get(entry.agentId) ?? {
|
|
1386
|
+
agentId: entry.agentId,
|
|
1387
|
+
agentName: entry.agentName,
|
|
1388
|
+
entries: []
|
|
1389
|
+
};
|
|
1390
|
+
current.entries.push(entry);
|
|
1391
|
+
groups.set(entry.agentId, current);
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
view: "agents",
|
|
1395
|
+
agents: [...groups.values()].sort(
|
|
1396
|
+
(left, right) => left.agentId.localeCompare(right.agentId)
|
|
1397
|
+
),
|
|
1398
|
+
issues: scanResult.issues
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function buildSkillsView(scanResult) {
|
|
1402
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1403
|
+
for (const entry of scanResult.entries) {
|
|
1404
|
+
const current = groups.get(entry.skillName) ?? {
|
|
1405
|
+
skillName: entry.skillName,
|
|
1406
|
+
entries: []
|
|
1407
|
+
};
|
|
1408
|
+
current.entries.push(entry);
|
|
1409
|
+
groups.set(entry.skillName, current);
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
view: "skills",
|
|
1413
|
+
skills: [...groups.values()].sort(
|
|
1414
|
+
(left, right) => left.skillName.localeCompare(right.skillName)
|
|
1415
|
+
),
|
|
1416
|
+
issues: scanResult.issues
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
function buildListData(scanResult, view) {
|
|
1420
|
+
if (view === "agents") {
|
|
1421
|
+
return buildAgentsView(scanResult);
|
|
1422
|
+
}
|
|
1423
|
+
if (view === "skills") {
|
|
1424
|
+
return buildSkillsView(scanResult);
|
|
1425
|
+
}
|
|
1426
|
+
return buildRecordsView(scanResult);
|
|
1427
|
+
}
|
|
1428
|
+
function buildTableOutput4(data, view) {
|
|
1429
|
+
if (view === "agents") {
|
|
1430
|
+
const agentRows = data.agents;
|
|
1431
|
+
return printTable(
|
|
1432
|
+
agentRows.map((agent) => ({
|
|
1433
|
+
agent: agent.agentId,
|
|
1434
|
+
name: agent.agentName,
|
|
1435
|
+
entries: String(agent.entries.length)
|
|
1436
|
+
})),
|
|
1437
|
+
[
|
|
1438
|
+
{ key: "agent", label: "Agent" },
|
|
1439
|
+
{ key: "name", label: "Name" },
|
|
1440
|
+
{ key: "entries", label: "Entries" }
|
|
1441
|
+
]
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
if (view === "skills") {
|
|
1445
|
+
const skillRows = data.skills;
|
|
1446
|
+
return printTable(
|
|
1447
|
+
skillRows.map((skill) => ({
|
|
1448
|
+
skill: skill.skillName,
|
|
1449
|
+
entries: String(skill.entries.length)
|
|
1450
|
+
})),
|
|
1451
|
+
[
|
|
1452
|
+
{ key: "skill", label: "Skill" },
|
|
1453
|
+
{ key: "entries", label: "Entries" }
|
|
1454
|
+
]
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
const records = data.records;
|
|
1458
|
+
return printTable(
|
|
1459
|
+
records.map((record) => ({
|
|
1460
|
+
agent: record.agentId,
|
|
1461
|
+
skill: record.skillName,
|
|
1462
|
+
kind: record.kind
|
|
1463
|
+
})),
|
|
1464
|
+
[
|
|
1465
|
+
{ key: "agent", label: "Agent" },
|
|
1466
|
+
{ key: "skill", label: "Skill" },
|
|
1467
|
+
{ key: "kind", label: "Kind" }
|
|
1468
|
+
]
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
async function runList(options = {}) {
|
|
1472
|
+
const view = options.view ?? "records";
|
|
1473
|
+
const format = options.format ?? "table";
|
|
1474
|
+
const scanResult = await runScan(options);
|
|
1475
|
+
const data = buildListData(scanResult, view);
|
|
1476
|
+
return {
|
|
1477
|
+
data,
|
|
1478
|
+
output: format === "json" ? printJson(data) : buildTableOutput4(data, view)
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/index.ts
|
|
1483
|
+
function buildCli() {
|
|
1484
|
+
const program = new Command();
|
|
1485
|
+
program.name("skillmux");
|
|
1486
|
+
program.command("scan").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1487
|
+
const result = await runScan({ json: options.json === true });
|
|
1488
|
+
process.stdout.write(result.output);
|
|
1489
|
+
});
|
|
1490
|
+
program.command("agents").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1491
|
+
const result = await runAgents({ json: options.json === true });
|
|
1492
|
+
process.stdout.write(result.output);
|
|
1493
|
+
});
|
|
1494
|
+
program.command("list").option("--view <view>", "Select records, agents, or skills view", "records").option("--format <format>", "Select table or json output", "table").action(async (options) => {
|
|
1495
|
+
const result = await runList({
|
|
1496
|
+
view: options.view,
|
|
1497
|
+
format: options.format
|
|
1498
|
+
});
|
|
1499
|
+
process.stdout.write(result.output);
|
|
1500
|
+
});
|
|
1501
|
+
program.command("import").requiredOption("--source <path>", "Local skill source directory").requiredOption("--name <name>", "Managed skill name").action(async (options) => {
|
|
1502
|
+
const result = await runImport({
|
|
1503
|
+
sourcePath: options.source,
|
|
1504
|
+
skillName: options.name
|
|
1505
|
+
});
|
|
1506
|
+
process.stdout.write(result.output);
|
|
1507
|
+
});
|
|
1508
|
+
program.command("doctor").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1509
|
+
const result = await runDoctor({ json: options.json === true });
|
|
1510
|
+
process.stdout.write(result.output);
|
|
1511
|
+
});
|
|
1512
|
+
program.command("config").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1513
|
+
const result = await runConfig({ json: options.json === true });
|
|
1514
|
+
process.stdout.write(result.output);
|
|
1515
|
+
});
|
|
1516
|
+
program.command("enable").requiredOption("--skill <skill>", "Managed skill name or id").requiredOption("--agent <agent>", "Target agent id").action(async (options) => {
|
|
1517
|
+
const result = await runEnable({
|
|
1518
|
+
skill: options.skill,
|
|
1519
|
+
agent: options.agent
|
|
1520
|
+
});
|
|
1521
|
+
process.stdout.write(result.output);
|
|
1522
|
+
});
|
|
1523
|
+
program.command("disable").requiredOption("--skill <skill>", "Managed skill name or id").requiredOption("--agent <agent>", "Target agent id").action(async (options) => {
|
|
1524
|
+
const result = await runDisable({
|
|
1525
|
+
skill: options.skill,
|
|
1526
|
+
agent: options.agent
|
|
1527
|
+
});
|
|
1528
|
+
process.stdout.write(result.output);
|
|
1529
|
+
});
|
|
1530
|
+
return program;
|
|
1531
|
+
}
|
|
1532
|
+
export {
|
|
1533
|
+
buildCli
|
|
1534
|
+
};
|
|
1535
|
+
//# sourceMappingURL=index.js.map
|