skillmux 0.1.1 → 0.1.2
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 +423 -284
- package/dist/cli.js +1333 -608
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1333 -608
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,30 +61,9 @@ var defaultAgentRuleMap = Object.fromEntries(
|
|
|
61
61
|
defaultAgentRules.map((rule) => [rule.id, rule])
|
|
62
62
|
);
|
|
63
63
|
|
|
64
|
-
// src/commands/
|
|
64
|
+
// src/commands/adopt.ts
|
|
65
65
|
import { homedir } from "os";
|
|
66
|
-
|
|
67
|
-
// src/config/resolve-skillmux-home.ts
|
|
68
|
-
import { join, resolve } from "path";
|
|
69
|
-
function buildConfigPath(skillmuxHome) {
|
|
70
|
-
return join(resolve(skillmuxHome), "config.json");
|
|
71
|
-
}
|
|
72
|
-
function resolveSkillmuxHome(homeDir) {
|
|
73
|
-
const resolvedHomeDir = resolve(homeDir);
|
|
74
|
-
const skillmuxHome = join(resolvedHomeDir, ".skillmux");
|
|
75
|
-
return {
|
|
76
|
-
skillmuxHome,
|
|
77
|
-
configPath: buildConfigPath(skillmuxHome)
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// src/discovery/discover-agents.ts
|
|
82
|
-
import * as fs2 from "fs/promises";
|
|
83
|
-
import { join as join2, resolve as resolve2 } from "path";
|
|
84
|
-
|
|
85
|
-
// src/config/load-user-config.ts
|
|
86
|
-
import * as fs from "fs/promises";
|
|
87
|
-
import { z } from "zod";
|
|
66
|
+
import { join as join7, resolve as resolve8 } from "path";
|
|
88
67
|
|
|
89
68
|
// src/core/errors.ts
|
|
90
69
|
var SkillMuxError = class extends Error {
|
|
@@ -112,8 +91,43 @@ var UserConfigValidationError = class extends SkillMuxError {
|
|
|
112
91
|
super(message);
|
|
113
92
|
}
|
|
114
93
|
};
|
|
94
|
+
var AdoptionError = class extends SkillMuxError {
|
|
95
|
+
constructor(message) {
|
|
96
|
+
super(message);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/core/ids.ts
|
|
101
|
+
var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
102
|
+
function normalizeId(value) {
|
|
103
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
104
|
+
return normalized.length > 0 ? normalized : "skill";
|
|
105
|
+
}
|
|
106
|
+
function isValidId(value) {
|
|
107
|
+
return ID_PATTERN.test(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/config/resolve-skillmux-home.ts
|
|
111
|
+
import { join, resolve } from "path";
|
|
112
|
+
function buildConfigPath(skillmuxHome) {
|
|
113
|
+
return join(resolve(skillmuxHome), "config.json");
|
|
114
|
+
}
|
|
115
|
+
function resolveSkillmuxHome(homeDir) {
|
|
116
|
+
const resolvedHomeDir = resolve(homeDir);
|
|
117
|
+
const skillmuxHome = join(resolvedHomeDir, ".skillmux");
|
|
118
|
+
return {
|
|
119
|
+
skillmuxHome,
|
|
120
|
+
configPath: buildConfigPath(skillmuxHome)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/discovery/discover-agents.ts
|
|
125
|
+
import * as fs2 from "fs/promises";
|
|
126
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
115
127
|
|
|
116
128
|
// src/config/load-user-config.ts
|
|
129
|
+
import * as fs from "fs/promises";
|
|
130
|
+
import { z } from "zod";
|
|
117
131
|
var supportedPlatformSchema = z.enum(supportedPlatforms);
|
|
118
132
|
var agentOverrideSchema = z.object({
|
|
119
133
|
stableName: z.string().min(1).optional(),
|
|
@@ -249,422 +263,95 @@ async function discoverAgents(options) {
|
|
|
249
263
|
return discoveredAgents;
|
|
250
264
|
}
|
|
251
265
|
|
|
252
|
-
// src/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
`;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// src/output/print-table.ts
|
|
259
|
-
function printTable(rows, columns) {
|
|
260
|
-
const renderedRows = rows.map(
|
|
261
|
-
(row) => columns.map((column) => String(row[column.key] ?? ""))
|
|
262
|
-
);
|
|
263
|
-
const widths = columns.map(
|
|
264
|
-
(column, index) => Math.max(
|
|
265
|
-
column.label.length,
|
|
266
|
-
...renderedRows.map((row) => row[index]?.length ?? 0)
|
|
267
|
-
)
|
|
268
|
-
);
|
|
269
|
-
const header = columns.map((column, index) => column.label.padEnd(widths[index])).join(" ");
|
|
270
|
-
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
271
|
-
const body = renderedRows.map(
|
|
272
|
-
(row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ")
|
|
273
|
-
);
|
|
274
|
-
return `${[header, separator, ...body].join("\n")}
|
|
275
|
-
`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// src/commands/agents.ts
|
|
279
|
-
function buildTableOutput(agents) {
|
|
280
|
-
return printTable(
|
|
281
|
-
agents.map((agent) => ({
|
|
282
|
-
id: agent.id,
|
|
283
|
-
name: agent.stableName,
|
|
284
|
-
path: agent.absoluteSkillsDirectoryPath,
|
|
285
|
-
exists: String(agent.exists),
|
|
286
|
-
supported: String(agent.supportedOnPlatform),
|
|
287
|
-
discovery: agent.discovery
|
|
288
|
-
})),
|
|
289
|
-
[
|
|
290
|
-
{ key: "id", label: "Agent" },
|
|
291
|
-
{ key: "name", label: "Name" },
|
|
292
|
-
{ key: "path", label: "Path" },
|
|
293
|
-
{ key: "exists", label: "Exists" },
|
|
294
|
-
{ key: "supported", label: "Supported" },
|
|
295
|
-
{ key: "discovery", label: "Discovery" }
|
|
296
|
-
]
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
async function runAgents(options = {}) {
|
|
300
|
-
const homeDir = options.homeDir ?? homedir();
|
|
301
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
302
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
303
|
-
const agents = await discoverAgents({
|
|
304
|
-
homeDir,
|
|
305
|
-
skillmuxHome,
|
|
306
|
-
platform: options.platform
|
|
307
|
-
});
|
|
308
|
-
return {
|
|
309
|
-
agents,
|
|
310
|
-
output: options.json === true ? printJson(agents) : buildTableOutput(agents)
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// src/commands/config-add-agent.ts
|
|
315
|
-
import { homedir as homedir2 } from "os";
|
|
316
|
-
import { isAbsolute } from "path";
|
|
266
|
+
// src/discovery/scan-agent-skills.ts
|
|
267
|
+
import * as fs5 from "fs/promises";
|
|
268
|
+
import { join as join3 } from "path";
|
|
317
269
|
|
|
318
|
-
// src/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
322
|
-
return normalized.length > 0 ? normalized : "skill";
|
|
323
|
-
}
|
|
324
|
-
function isValidId(value) {
|
|
325
|
-
return ID_PATTERN.test(value);
|
|
326
|
-
}
|
|
270
|
+
// src/discovery/infer-skill-entry.ts
|
|
271
|
+
import * as fs4 from "fs/promises";
|
|
272
|
+
import { basename, resolve as resolve4 } from "path";
|
|
327
273
|
|
|
328
|
-
// src/
|
|
274
|
+
// src/fs/path-utils.ts
|
|
329
275
|
import * as fs3 from "fs/promises";
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
`, "utf8");
|
|
335
|
-
return {
|
|
336
|
-
skillmuxHome,
|
|
337
|
-
configPath
|
|
338
|
-
};
|
|
276
|
+
import { dirname, parse, relative, resolve as resolve3, sep } from "path";
|
|
277
|
+
function normalizeAbsolutePath(path) {
|
|
278
|
+
const normalized = resolve3(path);
|
|
279
|
+
return process.platform === "win32" ? normalized.replaceAll("/", "\\").toLowerCase() : normalized;
|
|
339
280
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
281
|
+
function pathsAreEqual(left, right) {
|
|
282
|
+
return normalizeAbsolutePath(left) === normalizeAbsolutePath(right);
|
|
283
|
+
}
|
|
284
|
+
function isPathInside(parentPath, childPath) {
|
|
285
|
+
const parent = normalizeAbsolutePath(parentPath);
|
|
286
|
+
const child = normalizeAbsolutePath(childPath);
|
|
287
|
+
if (parse(parent).root !== parse(child).root) {
|
|
288
|
+
return false;
|
|
346
289
|
}
|
|
347
|
-
|
|
348
|
-
|
|
290
|
+
const relativePath = relative(parent, child);
|
|
291
|
+
if (relativePath === "") {
|
|
292
|
+
return true;
|
|
349
293
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
throw new UserConfigValidationError(`${field} must stay within the configured home-relative tree`);
|
|
294
|
+
if (relativePath === "..") {
|
|
295
|
+
return false;
|
|
353
296
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
function normalizeAgentId(value) {
|
|
357
|
-
const trimmed = value.trim();
|
|
358
|
-
if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
|
|
359
|
-
throw new InvalidIdentifierError("agent id", value);
|
|
297
|
+
if (relativePath.startsWith(`..${sep}`)) {
|
|
298
|
+
return false;
|
|
360
299
|
}
|
|
361
|
-
return
|
|
300
|
+
return true;
|
|
362
301
|
}
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
302
|
+
async function assertNoSymlinkAncestors(path, options) {
|
|
303
|
+
let current = options?.includeLeaf === true ? resolve3(path) : dirname(resolve3(path));
|
|
304
|
+
while (true) {
|
|
305
|
+
try {
|
|
306
|
+
const entry = await fs3.lstat(current);
|
|
307
|
+
if (entry.isSymbolicLink()) {
|
|
308
|
+
throw new Error(`Refusing to use path with symlink ancestor at ${current}`);
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (error.code !== "ENOENT") {
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const parent = dirname(current);
|
|
316
|
+
if (parent === current) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
current = parent;
|
|
375
320
|
}
|
|
376
|
-
return normalized;
|
|
377
321
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
homeRelativeRootPath: normalizeRelativePath(options.root, "root"),
|
|
383
|
-
skillsDirectoryPath: normalizeRelativePath(options.skills ?? "skills", "skills")
|
|
384
|
-
};
|
|
385
|
-
if (options.name !== void 0 && options.name.trim().length > 0) {
|
|
386
|
-
agent.stableName = options.name.trim();
|
|
387
|
-
}
|
|
388
|
-
if (options.disabledByDefault === true) {
|
|
389
|
-
agent.enabledByDefault = false;
|
|
390
|
-
}
|
|
391
|
-
return { agentId, agent };
|
|
322
|
+
|
|
323
|
+
// src/discovery/infer-skill-entry.ts
|
|
324
|
+
function buildIssue(code, severity, message, path) {
|
|
325
|
+
return { code, severity, message, path };
|
|
392
326
|
}
|
|
393
|
-
function
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
stableName: result.agent.stableName ?? "",
|
|
412
|
-
platforms: (result.agent.supportedPlatforms ?? []).join(","),
|
|
413
|
-
root: result.agent.homeRelativeRootPath ?? "",
|
|
414
|
-
skills: result.agent.skillsDirectoryPath ?? "",
|
|
415
|
-
enabledByDefault: result.agent.enabledByDefault === void 0 ? "" : String(result.agent.enabledByDefault)
|
|
416
|
-
}
|
|
417
|
-
],
|
|
418
|
-
[
|
|
419
|
-
{ key: "stableName", label: "Name" },
|
|
420
|
-
{ key: "platforms", label: "Platforms" },
|
|
421
|
-
{ key: "root", label: "Root" },
|
|
422
|
-
{ key: "skills", label: "Skills Dir" },
|
|
423
|
-
{ key: "enabledByDefault", label: "Enabled By Default" }
|
|
424
|
-
]
|
|
425
|
-
);
|
|
426
|
-
return `${summary}${detail}`;
|
|
427
|
-
}
|
|
428
|
-
async function runConfigAddAgent(options) {
|
|
429
|
-
const homeDir = options.homeDir ?? homedir2();
|
|
430
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
431
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
432
|
-
const configPath = buildConfigPath(skillmuxHome);
|
|
433
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
434
|
-
const { agentId, agent } = buildAgentOverride(options);
|
|
435
|
-
const previous = config.agents[agentId];
|
|
436
|
-
const changed = JSON.stringify(previous ?? null) !== JSON.stringify(agent);
|
|
437
|
-
const nextConfig = {
|
|
438
|
-
...config,
|
|
439
|
-
agents: {
|
|
440
|
-
...config.agents,
|
|
441
|
-
[agentId]: agent
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
await writeUserConfig(skillmuxHome, nextConfig);
|
|
445
|
-
const resultWithoutOutput = {
|
|
446
|
-
skillmuxHome,
|
|
447
|
-
configPath,
|
|
448
|
-
agentId,
|
|
449
|
-
changed,
|
|
450
|
-
agent,
|
|
451
|
-
config: nextConfig
|
|
452
|
-
};
|
|
453
|
-
return {
|
|
454
|
-
...resultWithoutOutput,
|
|
455
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// src/commands/config-remove-agent.ts
|
|
460
|
-
import { homedir as homedir3 } from "os";
|
|
461
|
-
function normalizeAgentId2(value) {
|
|
462
|
-
const trimmed = value.trim();
|
|
463
|
-
if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
|
|
464
|
-
throw new InvalidIdentifierError("agent id", value);
|
|
465
|
-
}
|
|
466
|
-
return normalizeId(trimmed);
|
|
467
|
-
}
|
|
468
|
-
function buildTableOutput3(result) {
|
|
469
|
-
return printTable(
|
|
470
|
-
[
|
|
471
|
-
{
|
|
472
|
-
agentId: result.agentId,
|
|
473
|
-
configPath: result.configPath,
|
|
474
|
-
changed: String(result.changed),
|
|
475
|
-
removed: String(result.removed)
|
|
476
|
-
}
|
|
477
|
-
],
|
|
478
|
-
[
|
|
479
|
-
{ key: "agentId", label: "Agent" },
|
|
480
|
-
{ key: "configPath", label: "Config Path" },
|
|
481
|
-
{ key: "changed", label: "Changed" },
|
|
482
|
-
{ key: "removed", label: "Removed" }
|
|
483
|
-
]
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
async function runConfigRemoveAgent(options) {
|
|
487
|
-
const homeDir = options.homeDir ?? homedir3();
|
|
488
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
489
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
490
|
-
const configPath = buildConfigPath(skillmuxHome);
|
|
491
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
492
|
-
const agentId = normalizeAgentId2(options.id);
|
|
493
|
-
const removed = agentId in config.agents;
|
|
494
|
-
const nextConfig = {
|
|
495
|
-
...config,
|
|
496
|
-
agents: Object.fromEntries(
|
|
497
|
-
Object.entries(config.agents).filter(([currentAgentId]) => currentAgentId !== agentId)
|
|
498
|
-
)
|
|
499
|
-
};
|
|
500
|
-
if (removed) {
|
|
501
|
-
await writeUserConfig(skillmuxHome, nextConfig);
|
|
502
|
-
}
|
|
503
|
-
const resultWithoutOutput = {
|
|
504
|
-
skillmuxHome,
|
|
505
|
-
configPath,
|
|
506
|
-
agentId,
|
|
507
|
-
changed: removed,
|
|
508
|
-
removed,
|
|
509
|
-
config: nextConfig
|
|
510
|
-
};
|
|
511
|
-
return {
|
|
512
|
-
...resultWithoutOutput,
|
|
513
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput3(resultWithoutOutput)
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// src/commands/config.ts
|
|
518
|
-
import { homedir as homedir4 } from "os";
|
|
519
|
-
function buildTableOutput4(result) {
|
|
520
|
-
const summary = printTable(
|
|
521
|
-
[
|
|
522
|
-
{
|
|
523
|
-
skillmuxHome: result.skillmuxHome,
|
|
524
|
-
configPath: result.configPath,
|
|
525
|
-
overrides: String(Object.keys(result.config.agents).length)
|
|
526
|
-
}
|
|
527
|
-
],
|
|
528
|
-
[
|
|
529
|
-
{ key: "skillmuxHome", label: "SkillMux Home" },
|
|
530
|
-
{ key: "configPath", label: "Config Path" },
|
|
531
|
-
{ key: "overrides", label: "Overrides" }
|
|
532
|
-
]
|
|
533
|
-
);
|
|
534
|
-
const agentRows = Object.entries(result.config.agents).sort(([left], [right]) => left.localeCompare(right)).map(([agentId, agent]) => ({
|
|
535
|
-
agentId,
|
|
536
|
-
stableName: agent.stableName ?? "",
|
|
537
|
-
root: agent.homeRelativeRootPath ?? "",
|
|
538
|
-
skills: agent.skillsDirectoryPath ?? ""
|
|
539
|
-
}));
|
|
540
|
-
if (agentRows.length === 0) {
|
|
541
|
-
return `${summary}
|
|
542
|
-
No user overrides configured.
|
|
543
|
-
`;
|
|
544
|
-
}
|
|
545
|
-
return `${summary}
|
|
546
|
-
${printTable(agentRows, [
|
|
547
|
-
{ key: "agentId", label: "Agent" },
|
|
548
|
-
{ key: "stableName", label: "Name" },
|
|
549
|
-
{ key: "root", label: "Root" },
|
|
550
|
-
{ key: "skills", label: "Skills Dir" }
|
|
551
|
-
])}`;
|
|
552
|
-
}
|
|
553
|
-
async function runConfig(options = {}) {
|
|
554
|
-
const homeDir = options.homeDir ?? homedir4();
|
|
555
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
556
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
557
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
558
|
-
const resultWithoutOutput = {
|
|
559
|
-
skillmuxHome,
|
|
560
|
-
configPath: buildConfigPath(skillmuxHome),
|
|
561
|
-
config
|
|
562
|
-
};
|
|
563
|
-
return {
|
|
564
|
-
...resultWithoutOutput,
|
|
565
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput4(resultWithoutOutput)
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// src/commands/doctor.ts
|
|
570
|
-
import * as fs9 from "fs/promises";
|
|
571
|
-
import { homedir as homedir5 } from "os";
|
|
572
|
-
import { join as join5 } from "path";
|
|
573
|
-
|
|
574
|
-
// src/discovery/scan-agent-skills.ts
|
|
575
|
-
import * as fs6 from "fs/promises";
|
|
576
|
-
|
|
577
|
-
// src/discovery/infer-skill-entry.ts
|
|
578
|
-
import * as fs5 from "fs/promises";
|
|
579
|
-
import { basename, resolve as resolve4 } from "path";
|
|
580
|
-
|
|
581
|
-
// src/fs/path-utils.ts
|
|
582
|
-
import * as fs4 from "fs/promises";
|
|
583
|
-
import { dirname, parse, relative, resolve as resolve3, sep } from "path";
|
|
584
|
-
function normalizeAbsolutePath(path) {
|
|
585
|
-
const normalized = resolve3(path);
|
|
586
|
-
return process.platform === "win32" ? normalized.replaceAll("/", "\\").toLowerCase() : normalized;
|
|
587
|
-
}
|
|
588
|
-
function pathsAreEqual(left, right) {
|
|
589
|
-
return normalizeAbsolutePath(left) === normalizeAbsolutePath(right);
|
|
590
|
-
}
|
|
591
|
-
function isPathInside(parentPath, childPath) {
|
|
592
|
-
const parent = normalizeAbsolutePath(parentPath);
|
|
593
|
-
const child = normalizeAbsolutePath(childPath);
|
|
594
|
-
if (parse(parent).root !== parse(child).root) {
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
const relativePath = relative(parent, child);
|
|
598
|
-
if (relativePath === "") {
|
|
599
|
-
return true;
|
|
600
|
-
}
|
|
601
|
-
if (relativePath === "..") {
|
|
602
|
-
return false;
|
|
603
|
-
}
|
|
604
|
-
if (relativePath.startsWith(`..${sep}`)) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
|
-
return true;
|
|
608
|
-
}
|
|
609
|
-
async function assertNoSymlinkAncestors(path, options) {
|
|
610
|
-
let current = options?.includeLeaf === true ? resolve3(path) : dirname(resolve3(path));
|
|
611
|
-
while (true) {
|
|
612
|
-
try {
|
|
613
|
-
const entry = await fs4.lstat(current);
|
|
614
|
-
if (entry.isSymbolicLink()) {
|
|
615
|
-
throw new Error(`Refusing to use path with symlink ancestor at ${current}`);
|
|
616
|
-
}
|
|
617
|
-
} catch (error) {
|
|
618
|
-
if (error.code !== "ENOENT") {
|
|
619
|
-
throw error;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
const parent = dirname(current);
|
|
623
|
-
if (parent === current) {
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
current = parent;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// src/discovery/infer-skill-entry.ts
|
|
631
|
-
function buildIssue(code, severity, message, path) {
|
|
632
|
-
return { code, severity, message, path };
|
|
633
|
-
}
|
|
634
|
-
async function inferSkillEntry(options) {
|
|
635
|
-
const absolutePath = resolve4(options.path);
|
|
636
|
-
const skillName = basename(absolutePath);
|
|
637
|
-
const stats = await fs5.lstat(absolutePath);
|
|
638
|
-
if (stats.isSymbolicLink()) {
|
|
639
|
-
try {
|
|
640
|
-
const targetPath = await fs5.realpath(absolutePath);
|
|
641
|
-
if (isPathInside(options.skillmuxHome, targetPath)) {
|
|
642
|
-
return {
|
|
643
|
-
entry: {
|
|
644
|
-
agentId: options.agentId,
|
|
645
|
-
agentName: options.agentName,
|
|
646
|
-
skillName,
|
|
647
|
-
kind: "managed-link",
|
|
648
|
-
path: absolutePath,
|
|
649
|
-
targetPath
|
|
650
|
-
}
|
|
651
|
-
};
|
|
327
|
+
async function inferSkillEntry(options) {
|
|
328
|
+
const absolutePath = resolve4(options.path);
|
|
329
|
+
const skillName = basename(absolutePath);
|
|
330
|
+
const stats = await fs4.lstat(absolutePath);
|
|
331
|
+
if (stats.isSymbolicLink()) {
|
|
332
|
+
try {
|
|
333
|
+
const targetPath = await fs4.realpath(absolutePath);
|
|
334
|
+
if (isPathInside(options.skillmuxHome, targetPath)) {
|
|
335
|
+
return {
|
|
336
|
+
entry: {
|
|
337
|
+
agentId: options.agentId,
|
|
338
|
+
agentName: options.agentName,
|
|
339
|
+
skillName,
|
|
340
|
+
kind: "managed-link",
|
|
341
|
+
path: absolutePath,
|
|
342
|
+
targetPath
|
|
343
|
+
}
|
|
344
|
+
};
|
|
652
345
|
}
|
|
653
346
|
return {
|
|
654
347
|
entry: {
|
|
655
348
|
agentId: options.agentId,
|
|
656
349
|
agentName: options.agentName,
|
|
657
350
|
skillName,
|
|
658
|
-
kind: "
|
|
351
|
+
kind: "unmanaged-link",
|
|
659
352
|
path: absolutePath,
|
|
660
353
|
targetPath
|
|
661
|
-
}
|
|
662
|
-
issue: buildIssue(
|
|
663
|
-
"unknown-entry",
|
|
664
|
-
"warning",
|
|
665
|
-
"Skill entry is a link that points outside the SkillMux managed store",
|
|
666
|
-
absolutePath
|
|
667
|
-
)
|
|
354
|
+
}
|
|
668
355
|
};
|
|
669
356
|
} catch (error) {
|
|
670
357
|
if (error.code !== "ENOENT") {
|
|
@@ -723,7 +410,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
723
410
|
issues: []
|
|
724
411
|
};
|
|
725
412
|
}
|
|
726
|
-
const directoryEntries = await
|
|
413
|
+
const directoryEntries = await fs5.readdir(agent.absoluteSkillsDirectoryPath, {
|
|
727
414
|
withFileTypes: true
|
|
728
415
|
});
|
|
729
416
|
const sortedDirectoryEntries = [...directoryEntries].sort(
|
|
@@ -735,7 +422,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
735
422
|
const result = await inferSkillEntry({
|
|
736
423
|
agentId: agent.id,
|
|
737
424
|
agentName: agent.stableName,
|
|
738
|
-
path:
|
|
425
|
+
path: join3(agent.absoluteSkillsDirectoryPath, directoryEntry.name),
|
|
739
426
|
skillmuxHome
|
|
740
427
|
});
|
|
741
428
|
entries.push(result.entry);
|
|
@@ -749,9 +436,201 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
749
436
|
};
|
|
750
437
|
}
|
|
751
438
|
|
|
752
|
-
// src/
|
|
753
|
-
import * as
|
|
754
|
-
import { join as join4, resolve as resolve5 } from "path";
|
|
439
|
+
// src/fs/safe-copy.ts
|
|
440
|
+
import * as fs6 from "fs/promises";
|
|
441
|
+
import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
|
|
442
|
+
async function assertDirectory(path) {
|
|
443
|
+
const entry = await fs6.lstat(path);
|
|
444
|
+
if (!entry.isDirectory()) {
|
|
445
|
+
throw new Error(`Expected a directory at ${path}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function assertRegularFile(path, label) {
|
|
449
|
+
const entry = await fs6.lstat(path);
|
|
450
|
+
if (!entry.isFile()) {
|
|
451
|
+
throw new Error(`Expected ${label} to be a regular file at ${path}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function assertTargetDoesNotExist(path) {
|
|
455
|
+
try {
|
|
456
|
+
await fs6.lstat(path);
|
|
457
|
+
throw new Error(`Refusing to overwrite existing path at ${path}`);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error.code !== "ENOENT") {
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function copyDirectoryContents(sourcePath, targetPath) {
|
|
465
|
+
await fs6.mkdir(targetPath, { recursive: true });
|
|
466
|
+
const entries = await fs6.readdir(sourcePath, { withFileTypes: true });
|
|
467
|
+
for (const entry of entries) {
|
|
468
|
+
const sourceEntryPath = join4(sourcePath, entry.name);
|
|
469
|
+
const targetEntryPath = join4(targetPath, entry.name);
|
|
470
|
+
const entryStats = await fs6.lstat(sourceEntryPath);
|
|
471
|
+
if (entryStats.isSymbolicLink()) {
|
|
472
|
+
throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
|
|
473
|
+
}
|
|
474
|
+
if (entryStats.isDirectory()) {
|
|
475
|
+
await copyDirectoryContents(sourceEntryPath, targetEntryPath);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (entryStats.isFile()) {
|
|
479
|
+
await fs6.mkdir(dirname2(targetEntryPath), { recursive: true });
|
|
480
|
+
await fs6.copyFile(sourceEntryPath, targetEntryPath);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
throw new Error(`Unsupported filesystem entry at ${sourceEntryPath}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function assertSkillSourceLayout(sourcePath) {
|
|
487
|
+
const resolvedSourcePath = resolve5(sourcePath);
|
|
488
|
+
const skillFilePath = join4(resolvedSourcePath, "SKILL.md");
|
|
489
|
+
await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
|
|
490
|
+
await assertDirectory(resolvedSourcePath);
|
|
491
|
+
try {
|
|
492
|
+
await assertRegularFile(skillFilePath, "SKILL.md");
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (error.code === "ENOENT") {
|
|
495
|
+
throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
|
|
496
|
+
}
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function hasRootSkillFile(sourcePath) {
|
|
501
|
+
const resolvedSourcePath = resolve5(sourcePath);
|
|
502
|
+
const skillFilePath = join4(resolvedSourcePath, "SKILL.md");
|
|
503
|
+
try {
|
|
504
|
+
await assertDirectory(resolvedSourcePath);
|
|
505
|
+
await assertRegularFile(skillFilePath, "SKILL.md");
|
|
506
|
+
return true;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error.code === "ENOENT") {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function copySkillContentsToManagedStore(sourcePath, targetPath) {
|
|
515
|
+
const resolvedSourcePath = resolve5(sourcePath);
|
|
516
|
+
const resolvedTargetPath = resolve5(targetPath);
|
|
517
|
+
if (pathsAreEqual(resolvedSourcePath, resolvedTargetPath)) {
|
|
518
|
+
throw new Error("Source and target paths must differ");
|
|
519
|
+
}
|
|
520
|
+
if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
|
|
521
|
+
throw new Error("Refusing to copy into a child of the source directory");
|
|
522
|
+
}
|
|
523
|
+
await assertSkillSourceLayout(resolvedSourcePath);
|
|
524
|
+
await assertNoSymlinkAncestors(resolvedTargetPath);
|
|
525
|
+
await assertTargetDoesNotExist(resolvedTargetPath);
|
|
526
|
+
await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/core/batch-operation-error.ts
|
|
530
|
+
function getCauseMessage(cause) {
|
|
531
|
+
if (cause instanceof Error) {
|
|
532
|
+
return cause.message;
|
|
533
|
+
}
|
|
534
|
+
return String(cause);
|
|
535
|
+
}
|
|
536
|
+
function buildBatchOperationMessage(options) {
|
|
537
|
+
const completedSuffix = options.completedItems.length > 0 ? ` after ${options.completedAction}: ${options.completedItems.join(", ")}` : "";
|
|
538
|
+
return `Failed to ${options.failedAction}${completedSuffix}: ${getCauseMessage(options.cause)}`;
|
|
539
|
+
}
|
|
540
|
+
var BatchOperationError = class extends Error {
|
|
541
|
+
operation;
|
|
542
|
+
failedItem;
|
|
543
|
+
completedItems;
|
|
544
|
+
cause;
|
|
545
|
+
constructor(options) {
|
|
546
|
+
super(buildBatchOperationMessage(options), { cause: options.cause });
|
|
547
|
+
this.name = "BatchOperationError";
|
|
548
|
+
this.operation = options.operation;
|
|
549
|
+
this.failedItem = options.failedItem;
|
|
550
|
+
this.completedItems = [...options.completedItems];
|
|
551
|
+
this.cause = options.cause;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/fs/link-ops.ts
|
|
556
|
+
import * as fs7 from "fs/promises";
|
|
557
|
+
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
558
|
+
var directoryLinkType = process.platform === "win32" ? "junction" : "dir";
|
|
559
|
+
async function createManagedLink(linkPath, targetPath) {
|
|
560
|
+
const resolvedLinkPath = resolve6(linkPath);
|
|
561
|
+
const resolvedTargetPath = resolve6(targetPath);
|
|
562
|
+
await assertNoSymlinkAncestors(resolvedLinkPath);
|
|
563
|
+
await assertNoSymlinkAncestors(resolvedTargetPath, { includeLeaf: true });
|
|
564
|
+
await fs7.mkdir(dirname3(resolvedLinkPath), { recursive: true });
|
|
565
|
+
try {
|
|
566
|
+
const existingEntry = await fs7.lstat(resolvedLinkPath);
|
|
567
|
+
if (!existingEntry.isSymbolicLink()) {
|
|
568
|
+
throw new Error(`Refusing to replace non-link entry at ${resolvedLinkPath}`);
|
|
569
|
+
}
|
|
570
|
+
const currentTargetPath = await fs7.realpath(resolvedLinkPath);
|
|
571
|
+
if (pathsAreEqual(currentTargetPath, resolvedTargetPath)) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
throw new Error(`Refusing to replace link at ${resolvedLinkPath}`);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error.code === "ENOENT" && await fs7.lstat(resolvedLinkPath).then((entry) => entry.isSymbolicLink()).catch(() => false)) {
|
|
577
|
+
await fs7.rm(resolvedLinkPath, { recursive: true, force: false });
|
|
578
|
+
} else if (error.code !== "ENOENT") {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
await fs7.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
|
|
583
|
+
}
|
|
584
|
+
async function replaceEntryWithManagedLink(linkPath, targetPath, expectedCurrentPath) {
|
|
585
|
+
const resolvedLinkPath = resolve6(linkPath);
|
|
586
|
+
const resolvedTargetPath = resolve6(targetPath);
|
|
587
|
+
const resolvedExpectedCurrentPath = resolve6(expectedCurrentPath);
|
|
588
|
+
await assertNoSymlinkAncestors(resolvedLinkPath);
|
|
589
|
+
await assertNoSymlinkAncestors(resolvedTargetPath, { includeLeaf: true });
|
|
590
|
+
await fs7.mkdir(dirname3(resolvedLinkPath), { recursive: true });
|
|
591
|
+
const existingEntry = await fs7.lstat(resolvedLinkPath);
|
|
592
|
+
if (existingEntry.isSymbolicLink()) {
|
|
593
|
+
const currentTargetPath = await fs7.realpath(resolvedLinkPath);
|
|
594
|
+
if (pathsAreEqual(currentTargetPath, resolvedTargetPath)) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
if (!pathsAreEqual(currentTargetPath, resolvedExpectedCurrentPath)) {
|
|
598
|
+
throw new Error(`Refusing to replace unexpected link at ${resolvedLinkPath}`);
|
|
599
|
+
}
|
|
600
|
+
await fs7.rm(resolvedLinkPath, { recursive: true, force: false });
|
|
601
|
+
await fs7.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (!existingEntry.isDirectory()) {
|
|
605
|
+
throw new Error(`Refusing to replace non-directory entry at ${resolvedLinkPath}`);
|
|
606
|
+
}
|
|
607
|
+
const currentPath = await fs7.realpath(resolvedLinkPath);
|
|
608
|
+
if (!pathsAreEqual(currentPath, resolvedExpectedCurrentPath)) {
|
|
609
|
+
throw new Error(`Refusing to replace unexpected directory at ${resolvedLinkPath}`);
|
|
610
|
+
}
|
|
611
|
+
await fs7.rm(resolvedLinkPath, { recursive: true, force: false });
|
|
612
|
+
await fs7.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
async function isLinkPointingToTarget(linkPath, targetPath) {
|
|
616
|
+
try {
|
|
617
|
+
const entry = await fs7.lstat(linkPath);
|
|
618
|
+
if (!entry.isSymbolicLink()) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const resolvedTargetPath = await fs7.realpath(linkPath);
|
|
622
|
+
return pathsAreEqual(resolvedTargetPath, targetPath);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (error.code === "ENOENT") {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/manifest/read-manifest.ts
|
|
632
|
+
import * as fs9 from "fs/promises";
|
|
633
|
+
import { join as join6, resolve as resolve7 } from "path";
|
|
755
634
|
|
|
756
635
|
// src/manifest/build-empty-manifest.ts
|
|
757
636
|
function buildEmptyManifest(skillmuxHome) {
|
|
@@ -876,35 +755,35 @@ var manifestSchema = z2.object({
|
|
|
876
755
|
|
|
877
756
|
// src/manifest/write-manifest.ts
|
|
878
757
|
import { randomUUID } from "crypto";
|
|
879
|
-
import * as
|
|
880
|
-
import { join as
|
|
758
|
+
import * as fs8 from "fs/promises";
|
|
759
|
+
import { join as join5 } from "path";
|
|
881
760
|
function getManifestPath(home) {
|
|
882
|
-
return
|
|
761
|
+
return join5(home, "manifest.json");
|
|
883
762
|
}
|
|
884
763
|
function createManifestTempPath(manifestPath) {
|
|
885
764
|
return `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
886
765
|
}
|
|
887
766
|
async function writeManifest(home, manifest) {
|
|
888
|
-
await
|
|
767
|
+
await fs8.mkdir(home, { recursive: true });
|
|
889
768
|
const manifestPath = getManifestPath(home);
|
|
890
769
|
const tempPath = createManifestTempPath(manifestPath);
|
|
891
770
|
const contents = `${JSON.stringify(manifest, null, 2)}
|
|
892
771
|
`;
|
|
893
|
-
await
|
|
772
|
+
await fs8.writeFile(tempPath, contents, "utf8");
|
|
894
773
|
try {
|
|
895
|
-
await
|
|
774
|
+
await fs8.rename(tempPath, manifestPath);
|
|
896
775
|
} catch (error) {
|
|
897
|
-
await
|
|
776
|
+
await fs8.unlink(tempPath).catch(() => void 0);
|
|
898
777
|
throw error;
|
|
899
778
|
}
|
|
900
779
|
}
|
|
901
780
|
|
|
902
781
|
// src/manifest/read-manifest.ts
|
|
903
782
|
function getManifestPath2(home) {
|
|
904
|
-
return
|
|
783
|
+
return join6(home, "manifest.json");
|
|
905
784
|
}
|
|
906
785
|
function normalizeHomePath(home) {
|
|
907
|
-
const resolvedHome =
|
|
786
|
+
const resolvedHome = resolve7(home);
|
|
908
787
|
return process.platform === "win32" ? resolvedHome.toLowerCase() : resolvedHome;
|
|
909
788
|
}
|
|
910
789
|
function formatValidationIssues2(error) {
|
|
@@ -916,7 +795,7 @@ function formatValidationIssues2(error) {
|
|
|
916
795
|
async function readManifest(home) {
|
|
917
796
|
const manifestPath = getManifestPath2(home);
|
|
918
797
|
try {
|
|
919
|
-
const contents = await
|
|
798
|
+
const contents = await fs9.readFile(manifestPath, "utf8");
|
|
920
799
|
const parsedJson = JSON.parse(contents);
|
|
921
800
|
const parsedManifest = manifestSchema.safeParse(parsedJson);
|
|
922
801
|
if (!parsedManifest.success) {
|
|
@@ -945,13 +824,689 @@ async function readManifest(home) {
|
|
|
945
824
|
}
|
|
946
825
|
}
|
|
947
826
|
|
|
827
|
+
// src/output/print-json.ts
|
|
828
|
+
function printJson(value) {
|
|
829
|
+
return `${JSON.stringify(value, null, 2)}
|
|
830
|
+
`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/commands/adopt.ts
|
|
834
|
+
function buildManagedSkillPath(skillmuxHome, skillId) {
|
|
835
|
+
return resolve8(skillmuxHome, "skills", skillId);
|
|
836
|
+
}
|
|
837
|
+
function buildAgentRecord(agent, timestamp) {
|
|
838
|
+
return {
|
|
839
|
+
id: agent.id,
|
|
840
|
+
name: agent.stableName,
|
|
841
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
842
|
+
discovery: agent.discovery,
|
|
843
|
+
available: agent.exists && agent.supportedOnPlatform,
|
|
844
|
+
lastSeenAt: agent.exists ? timestamp : null
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function buildActivationRecord(skillId, agentId, linkPath, timestamp) {
|
|
848
|
+
return {
|
|
849
|
+
skillId,
|
|
850
|
+
agentId,
|
|
851
|
+
linkPath,
|
|
852
|
+
state: "enabled",
|
|
853
|
+
updatedAt: timestamp
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function upsertActivation(manifest, activation) {
|
|
857
|
+
const index = manifest.activations.findIndex(
|
|
858
|
+
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
859
|
+
);
|
|
860
|
+
if (index === -1) {
|
|
861
|
+
manifest.activations.push(activation);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
manifest.activations[index] = activation;
|
|
865
|
+
}
|
|
866
|
+
async function resolveTargetAgent(homeDir, skillmuxHome, agentName) {
|
|
867
|
+
const agentId = normalizeId(agentName);
|
|
868
|
+
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
869
|
+
const agent = agents.find((entry) => entry.id === agentId);
|
|
870
|
+
if (agent === void 0) {
|
|
871
|
+
throw new AdoptionError(`Unknown agent: ${agentName}`);
|
|
872
|
+
}
|
|
873
|
+
if (!agent.supportedOnPlatform) {
|
|
874
|
+
throw new AdoptionError(
|
|
875
|
+
`Agent ${agent.id} is not supported on ${process.platform}`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
return agent;
|
|
879
|
+
}
|
|
880
|
+
function filterEntries(scannedAgent, skillFilter) {
|
|
881
|
+
if (skillFilter === void 0) {
|
|
882
|
+
return scannedAgent.entries;
|
|
883
|
+
}
|
|
884
|
+
const skillId = normalizeId(skillFilter);
|
|
885
|
+
return scannedAgent.entries.filter(
|
|
886
|
+
(entry) => normalizeId(entry.skillName) === skillId
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
async function resolveAdoptionSource(entry) {
|
|
890
|
+
if (entry.kind === "unmanaged-link") {
|
|
891
|
+
return entry.targetPath;
|
|
892
|
+
}
|
|
893
|
+
if (entry.kind === "unmanaged-directory") {
|
|
894
|
+
return entry.path;
|
|
895
|
+
}
|
|
896
|
+
return void 0;
|
|
897
|
+
}
|
|
898
|
+
function buildManagedSkill(skillId, skillName, managedPath, sourcePath, timestamp) {
|
|
899
|
+
return {
|
|
900
|
+
id: skillId,
|
|
901
|
+
name: skillName,
|
|
902
|
+
path: managedPath,
|
|
903
|
+
source: {
|
|
904
|
+
kind: "imported",
|
|
905
|
+
path: sourcePath
|
|
906
|
+
},
|
|
907
|
+
importedAt: timestamp
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async function reconcileManagedLink(manifest, skillId, entry, agentId, timestamp) {
|
|
911
|
+
if (entry.targetPath === void 0) {
|
|
912
|
+
throw new AdoptionError(`Managed link target is missing for ${entry.path}`);
|
|
913
|
+
}
|
|
914
|
+
await assertSkillSourceLayout(entry.targetPath);
|
|
915
|
+
const skill = manifest.skills[skillId];
|
|
916
|
+
if (skill === void 0 || !pathsAreEqual(skill.path, entry.targetPath)) {
|
|
917
|
+
throw new AdoptionError(
|
|
918
|
+
`Managed link for ${agentId}/${skillId} has no matching manifest skill record`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
upsertActivation(
|
|
922
|
+
manifest,
|
|
923
|
+
buildActivationRecord(skillId, agentId, entry.path, timestamp)
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
function buildOutput(result, json) {
|
|
927
|
+
if (json) {
|
|
928
|
+
return printJson({
|
|
929
|
+
agent: result.agent,
|
|
930
|
+
adopted: result.adopted,
|
|
931
|
+
skipped: result.skipped
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
if (result.adopted.length === 0) {
|
|
935
|
+
return `No skills adopted for ${result.agent.id}.
|
|
936
|
+
`;
|
|
937
|
+
}
|
|
938
|
+
const adoptedSkills = result.adopted.map((entry) => entry.skillId).sort((left, right) => left.localeCompare(right)).join(", ");
|
|
939
|
+
return `Adopted ${adoptedSkills} for ${result.agent.id}
|
|
940
|
+
`;
|
|
941
|
+
}
|
|
942
|
+
async function runAdoptSingle(options) {
|
|
943
|
+
const homeDir = options.homeDir ?? homedir();
|
|
944
|
+
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
945
|
+
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
946
|
+
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
947
|
+
const manifest = await readManifest(skillmuxHome);
|
|
948
|
+
const agent = await resolveTargetAgent(homeDir, skillmuxHome, options.agent);
|
|
949
|
+
const agentRecord = buildAgentRecord(agent, timestamp);
|
|
950
|
+
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
951
|
+
const entries = filterEntries(scannedAgent, options.skill);
|
|
952
|
+
const adopted = [];
|
|
953
|
+
const skipped = [];
|
|
954
|
+
manifest.agents[agent.id] = agentRecord;
|
|
955
|
+
for (const entry of entries) {
|
|
956
|
+
const skillId = normalizeId(entry.skillName);
|
|
957
|
+
if (entry.kind === "managed-link") {
|
|
958
|
+
await reconcileManagedLink(
|
|
959
|
+
manifest,
|
|
960
|
+
skillId,
|
|
961
|
+
entry,
|
|
962
|
+
agent.id,
|
|
963
|
+
timestamp
|
|
964
|
+
);
|
|
965
|
+
skipped.push({
|
|
966
|
+
skillId,
|
|
967
|
+
agentId: agent.id,
|
|
968
|
+
path: entry.path,
|
|
969
|
+
reason: "already-managed"
|
|
970
|
+
});
|
|
971
|
+
await writeManifest(skillmuxHome, manifest);
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const sourcePath = await resolveAdoptionSource(entry);
|
|
975
|
+
if (sourcePath === void 0) {
|
|
976
|
+
skipped.push({
|
|
977
|
+
skillId,
|
|
978
|
+
agentId: agent.id,
|
|
979
|
+
path: entry.path,
|
|
980
|
+
reason: "not-adoptable"
|
|
981
|
+
});
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (!await hasRootSkillFile(sourcePath)) {
|
|
985
|
+
skipped.push({
|
|
986
|
+
skillId,
|
|
987
|
+
agentId: agent.id,
|
|
988
|
+
path: entry.path,
|
|
989
|
+
reason: "missing-skill-file"
|
|
990
|
+
});
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const managedPath = buildManagedSkillPath(skillmuxHome, skillId);
|
|
994
|
+
if (manifest.skills[skillId] === void 0) {
|
|
995
|
+
await copySkillContentsToManagedStore(sourcePath, managedPath);
|
|
996
|
+
manifest.skills[skillId] = buildManagedSkill(
|
|
997
|
+
skillId,
|
|
998
|
+
entry.skillName,
|
|
999
|
+
managedPath,
|
|
1000
|
+
sourcePath,
|
|
1001
|
+
timestamp
|
|
1002
|
+
);
|
|
1003
|
+
} else if (await isLinkPointingToTarget(entry.path, manifest.skills[skillId].path)) {
|
|
1004
|
+
skipped.push({
|
|
1005
|
+
skillId,
|
|
1006
|
+
agentId: agent.id,
|
|
1007
|
+
path: entry.path,
|
|
1008
|
+
reason: "already-managed"
|
|
1009
|
+
});
|
|
1010
|
+
continue;
|
|
1011
|
+
} else {
|
|
1012
|
+
await assertSkillSourceLayout(manifest.skills[skillId].path);
|
|
1013
|
+
}
|
|
1014
|
+
await replaceEntryWithManagedLink(
|
|
1015
|
+
entry.path,
|
|
1016
|
+
manifest.skills[skillId].path,
|
|
1017
|
+
sourcePath
|
|
1018
|
+
);
|
|
1019
|
+
const activation = buildActivationRecord(
|
|
1020
|
+
skillId,
|
|
1021
|
+
agent.id,
|
|
1022
|
+
join7(agent.absoluteSkillsDirectoryPath, entry.skillName),
|
|
1023
|
+
timestamp
|
|
1024
|
+
);
|
|
1025
|
+
upsertActivation(manifest, activation);
|
|
1026
|
+
adopted.push({
|
|
1027
|
+
skillId,
|
|
1028
|
+
agentId: agent.id,
|
|
1029
|
+
sourcePath,
|
|
1030
|
+
managedPath: manifest.skills[skillId].path,
|
|
1031
|
+
linkPath: activation.linkPath
|
|
1032
|
+
});
|
|
1033
|
+
await writeManifest(skillmuxHome, manifest);
|
|
1034
|
+
}
|
|
1035
|
+
await writeManifest(skillmuxHome, manifest);
|
|
1036
|
+
const resultWithoutOutput = {
|
|
1037
|
+
agent: agentRecord,
|
|
1038
|
+
adopted,
|
|
1039
|
+
skipped,
|
|
1040
|
+
manifest
|
|
1041
|
+
};
|
|
1042
|
+
return {
|
|
1043
|
+
...resultWithoutOutput,
|
|
1044
|
+
output: buildOutput(resultWithoutOutput, options.json === true)
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
async function runAdopt(options) {
|
|
1048
|
+
if (options.skills !== void 0) {
|
|
1049
|
+
const results = [];
|
|
1050
|
+
const completedSkills = [];
|
|
1051
|
+
for (const skill of options.skills) {
|
|
1052
|
+
try {
|
|
1053
|
+
results.push(await runAdoptSingle({ ...options, skill, skills: void 0 }));
|
|
1054
|
+
completedSkills.push(skill);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
throw new BatchOperationError({
|
|
1057
|
+
operation: "adopt",
|
|
1058
|
+
failedItem: skill,
|
|
1059
|
+
failedAction: `adopt ${skill} for ${options.agent}`,
|
|
1060
|
+
completedAction: "adopting",
|
|
1061
|
+
completedItems: completedSkills,
|
|
1062
|
+
cause: error
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (results.length === 0) {
|
|
1067
|
+
throw new AdoptionError("Adopt requires at least one target skill");
|
|
1068
|
+
}
|
|
1069
|
+
const lastResult = results[results.length - 1];
|
|
1070
|
+
const resultWithoutOutput = {
|
|
1071
|
+
agent: lastResult.agent,
|
|
1072
|
+
adopted: results.flatMap((result) => result.adopted),
|
|
1073
|
+
skipped: results.flatMap((result) => result.skipped),
|
|
1074
|
+
manifest: lastResult.manifest,
|
|
1075
|
+
results
|
|
1076
|
+
};
|
|
1077
|
+
return {
|
|
1078
|
+
...resultWithoutOutput,
|
|
1079
|
+
output: options.json === true ? printJson({
|
|
1080
|
+
agent: resultWithoutOutput.agent,
|
|
1081
|
+
adopted: resultWithoutOutput.adopted,
|
|
1082
|
+
skipped: resultWithoutOutput.skipped,
|
|
1083
|
+
results: resultWithoutOutput.results
|
|
1084
|
+
}) : results.map((result) => result.output).join("")
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
return runAdoptSingle(options);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/commands/agents.ts
|
|
1091
|
+
import { homedir as homedir2 } from "os";
|
|
1092
|
+
|
|
1093
|
+
// src/output/print-table.ts
|
|
1094
|
+
function printTable(rows, columns) {
|
|
1095
|
+
const renderedRows = rows.map(
|
|
1096
|
+
(row) => columns.map((column) => String(row[column.key] ?? ""))
|
|
1097
|
+
);
|
|
1098
|
+
const widths = columns.map(
|
|
1099
|
+
(column, index) => Math.max(
|
|
1100
|
+
column.label.length,
|
|
1101
|
+
...renderedRows.map((row) => row[index]?.length ?? 0)
|
|
1102
|
+
)
|
|
1103
|
+
);
|
|
1104
|
+
const header = columns.map((column, index) => column.label.padEnd(widths[index])).join(" ");
|
|
1105
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
1106
|
+
const body = renderedRows.map(
|
|
1107
|
+
(row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ")
|
|
1108
|
+
);
|
|
1109
|
+
return `${[header, separator, ...body].join("\n")}
|
|
1110
|
+
`;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/commands/agents.ts
|
|
1114
|
+
function buildTableOutput(agents) {
|
|
1115
|
+
return printTable(
|
|
1116
|
+
agents.map((agent) => ({
|
|
1117
|
+
id: agent.id,
|
|
1118
|
+
name: agent.stableName,
|
|
1119
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
1120
|
+
exists: String(agent.exists),
|
|
1121
|
+
supported: String(agent.supportedOnPlatform),
|
|
1122
|
+
discovery: agent.discovery
|
|
1123
|
+
})),
|
|
1124
|
+
[
|
|
1125
|
+
{ key: "id", label: "Agent" },
|
|
1126
|
+
{ key: "name", label: "Name" },
|
|
1127
|
+
{ key: "path", label: "Path" },
|
|
1128
|
+
{ key: "exists", label: "Exists" },
|
|
1129
|
+
{ key: "supported", label: "Supported" },
|
|
1130
|
+
{ key: "discovery", label: "Discovery" }
|
|
1131
|
+
]
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
async function runAgents(options = {}) {
|
|
1135
|
+
const homeDir = options.homeDir ?? homedir2();
|
|
1136
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1137
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1138
|
+
const agents = await discoverAgents({
|
|
1139
|
+
homeDir,
|
|
1140
|
+
skillmuxHome,
|
|
1141
|
+
platform: options.platform
|
|
1142
|
+
});
|
|
1143
|
+
return {
|
|
1144
|
+
agents,
|
|
1145
|
+
output: options.json === true ? printJson(agents) : buildTableOutput(agents)
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/commands/config-add-agent.ts
|
|
1150
|
+
import { homedir as homedir3 } from "os";
|
|
1151
|
+
|
|
1152
|
+
// src/config/agent-override-validation.ts
|
|
1153
|
+
import { isAbsolute } from "path";
|
|
1154
|
+
function normalizeRelativePath(value, field) {
|
|
1155
|
+
const trimmed = value.trim();
|
|
1156
|
+
if (trimmed.length === 0) {
|
|
1157
|
+
throw new UserConfigValidationError(`${field} must not be empty`);
|
|
1158
|
+
}
|
|
1159
|
+
if (isAbsolute(trimmed)) {
|
|
1160
|
+
throw new UserConfigValidationError(`${field} must be a relative path`);
|
|
1161
|
+
}
|
|
1162
|
+
const normalized = trimmed.replaceAll("\\", "/");
|
|
1163
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
1164
|
+
throw new UserConfigValidationError(`${field} must stay within the configured home-relative tree`);
|
|
1165
|
+
}
|
|
1166
|
+
return normalized.replace(/^\.\/+/, "");
|
|
1167
|
+
}
|
|
1168
|
+
function normalizeAgentId(value) {
|
|
1169
|
+
const trimmed = value.trim();
|
|
1170
|
+
if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
|
|
1171
|
+
throw new InvalidIdentifierError("agent id", value);
|
|
1172
|
+
}
|
|
1173
|
+
return normalizeId(trimmed);
|
|
1174
|
+
}
|
|
1175
|
+
function normalizePlatforms(value) {
|
|
1176
|
+
if (value === void 0 || value.length === 0) {
|
|
1177
|
+
return [process.platform];
|
|
1178
|
+
}
|
|
1179
|
+
const normalized = [...new Set(value.map((entry) => entry.trim().toLowerCase()))];
|
|
1180
|
+
const invalid = normalized.filter(
|
|
1181
|
+
(entry) => supportedPlatforms.includes(entry) === false
|
|
1182
|
+
);
|
|
1183
|
+
if (invalid.length > 0) {
|
|
1184
|
+
throw new UserConfigValidationError(
|
|
1185
|
+
`platform must be one of: ${supportedPlatforms.join(", ")}`
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
return normalized;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/config/write-user-config.ts
|
|
1192
|
+
import * as fs10 from "fs/promises";
|
|
1193
|
+
async function writeUserConfig(skillmuxHome, config) {
|
|
1194
|
+
const configPath = buildConfigPath(skillmuxHome);
|
|
1195
|
+
await fs10.mkdir(skillmuxHome, { recursive: true });
|
|
1196
|
+
await fs10.writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
1197
|
+
`, "utf8");
|
|
1198
|
+
return {
|
|
1199
|
+
skillmuxHome,
|
|
1200
|
+
configPath
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/commands/config-add-agent.ts
|
|
1205
|
+
function buildAgentOverride(options) {
|
|
1206
|
+
const agentId = normalizeAgentId(options.id);
|
|
1207
|
+
const agent = {
|
|
1208
|
+
supportedPlatforms: normalizePlatforms(options.platforms),
|
|
1209
|
+
homeRelativeRootPath: normalizeRelativePath(options.root, "root"),
|
|
1210
|
+
skillsDirectoryPath: normalizeRelativePath(options.skills ?? "skills", "skills")
|
|
1211
|
+
};
|
|
1212
|
+
if (options.name !== void 0 && options.name.trim().length > 0) {
|
|
1213
|
+
agent.stableName = options.name.trim();
|
|
1214
|
+
}
|
|
1215
|
+
if (options.disabledByDefault === true) {
|
|
1216
|
+
agent.enabledByDefault = false;
|
|
1217
|
+
}
|
|
1218
|
+
return { agentId, agent };
|
|
1219
|
+
}
|
|
1220
|
+
function buildTableOutput2(result) {
|
|
1221
|
+
const summary = printTable(
|
|
1222
|
+
[
|
|
1223
|
+
{
|
|
1224
|
+
agentId: result.agentId,
|
|
1225
|
+
configPath: result.configPath,
|
|
1226
|
+
changed: String(result.changed)
|
|
1227
|
+
}
|
|
1228
|
+
],
|
|
1229
|
+
[
|
|
1230
|
+
{ key: "agentId", label: "Agent" },
|
|
1231
|
+
{ key: "configPath", label: "Config Path" },
|
|
1232
|
+
{ key: "changed", label: "Changed" }
|
|
1233
|
+
]
|
|
1234
|
+
);
|
|
1235
|
+
const detail = printTable(
|
|
1236
|
+
[
|
|
1237
|
+
{
|
|
1238
|
+
stableName: result.agent.stableName ?? "",
|
|
1239
|
+
platforms: (result.agent.supportedPlatforms ?? []).join(","),
|
|
1240
|
+
root: result.agent.homeRelativeRootPath ?? "",
|
|
1241
|
+
skills: result.agent.skillsDirectoryPath ?? "",
|
|
1242
|
+
enabledByDefault: result.agent.enabledByDefault === void 0 ? "" : String(result.agent.enabledByDefault)
|
|
1243
|
+
}
|
|
1244
|
+
],
|
|
1245
|
+
[
|
|
1246
|
+
{ key: "stableName", label: "Name" },
|
|
1247
|
+
{ key: "platforms", label: "Platforms" },
|
|
1248
|
+
{ key: "root", label: "Root" },
|
|
1249
|
+
{ key: "skills", label: "Skills Dir" },
|
|
1250
|
+
{ key: "enabledByDefault", label: "Enabled By Default" }
|
|
1251
|
+
]
|
|
1252
|
+
);
|
|
1253
|
+
return `${summary}${detail}`;
|
|
1254
|
+
}
|
|
1255
|
+
async function runConfigAddAgent(options) {
|
|
1256
|
+
const homeDir = options.homeDir ?? homedir3();
|
|
1257
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1258
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1259
|
+
const configPath = buildConfigPath(skillmuxHome);
|
|
1260
|
+
const config = await loadUserConfig(skillmuxHome);
|
|
1261
|
+
const { agentId, agent } = buildAgentOverride(options);
|
|
1262
|
+
const previous = config.agents[agentId];
|
|
1263
|
+
const changed = JSON.stringify(previous ?? null) !== JSON.stringify(agent);
|
|
1264
|
+
const nextConfig = {
|
|
1265
|
+
...config,
|
|
1266
|
+
agents: {
|
|
1267
|
+
...config.agents,
|
|
1268
|
+
[agentId]: agent
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
await writeUserConfig(skillmuxHome, nextConfig);
|
|
1272
|
+
const resultWithoutOutput = {
|
|
1273
|
+
skillmuxHome,
|
|
1274
|
+
configPath,
|
|
1275
|
+
agentId,
|
|
1276
|
+
changed,
|
|
1277
|
+
agent,
|
|
1278
|
+
config: nextConfig
|
|
1279
|
+
};
|
|
1280
|
+
return {
|
|
1281
|
+
...resultWithoutOutput,
|
|
1282
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/commands/config-remove-agent.ts
|
|
1287
|
+
import { homedir as homedir4 } from "os";
|
|
1288
|
+
function normalizeAgentId2(value) {
|
|
1289
|
+
const trimmed = value.trim();
|
|
1290
|
+
if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
|
|
1291
|
+
throw new InvalidIdentifierError("agent id", value);
|
|
1292
|
+
}
|
|
1293
|
+
return normalizeId(trimmed);
|
|
1294
|
+
}
|
|
1295
|
+
function buildTableOutput3(result) {
|
|
1296
|
+
return printTable(
|
|
1297
|
+
[
|
|
1298
|
+
{
|
|
1299
|
+
agentId: result.agentId,
|
|
1300
|
+
configPath: result.configPath,
|
|
1301
|
+
changed: String(result.changed),
|
|
1302
|
+
removed: String(result.removed)
|
|
1303
|
+
}
|
|
1304
|
+
],
|
|
1305
|
+
[
|
|
1306
|
+
{ key: "agentId", label: "Agent" },
|
|
1307
|
+
{ key: "configPath", label: "Config Path" },
|
|
1308
|
+
{ key: "changed", label: "Changed" },
|
|
1309
|
+
{ key: "removed", label: "Removed" }
|
|
1310
|
+
]
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
async function runConfigRemoveAgent(options) {
|
|
1314
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
1315
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1316
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1317
|
+
const configPath = buildConfigPath(skillmuxHome);
|
|
1318
|
+
const config = await loadUserConfig(skillmuxHome);
|
|
1319
|
+
const agentId = normalizeAgentId2(options.id);
|
|
1320
|
+
const removed = agentId in config.agents;
|
|
1321
|
+
const nextConfig = {
|
|
1322
|
+
...config,
|
|
1323
|
+
agents: Object.fromEntries(
|
|
1324
|
+
Object.entries(config.agents).filter(([currentAgentId]) => currentAgentId !== agentId)
|
|
1325
|
+
)
|
|
1326
|
+
};
|
|
1327
|
+
if (removed) {
|
|
1328
|
+
await writeUserConfig(skillmuxHome, nextConfig);
|
|
1329
|
+
}
|
|
1330
|
+
const resultWithoutOutput = {
|
|
1331
|
+
skillmuxHome,
|
|
1332
|
+
configPath,
|
|
1333
|
+
agentId,
|
|
1334
|
+
changed: removed,
|
|
1335
|
+
removed,
|
|
1336
|
+
config: nextConfig
|
|
1337
|
+
};
|
|
1338
|
+
return {
|
|
1339
|
+
...resultWithoutOutput,
|
|
1340
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput3(resultWithoutOutput)
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/commands/config-update-agent.ts
|
|
1345
|
+
import { homedir as homedir5 } from "os";
|
|
1346
|
+
function buildAgentPatch(options) {
|
|
1347
|
+
const patch = {};
|
|
1348
|
+
if (options.root !== void 0) {
|
|
1349
|
+
patch.homeRelativeRootPath = normalizeRelativePath(options.root, "root");
|
|
1350
|
+
}
|
|
1351
|
+
if (options.skills !== void 0) {
|
|
1352
|
+
patch.skillsDirectoryPath = normalizeRelativePath(options.skills, "skills");
|
|
1353
|
+
}
|
|
1354
|
+
if (options.name !== void 0 && options.name.trim().length > 0) {
|
|
1355
|
+
patch.stableName = options.name.trim();
|
|
1356
|
+
}
|
|
1357
|
+
if (options.platforms !== void 0) {
|
|
1358
|
+
patch.supportedPlatforms = normalizePlatforms(options.platforms);
|
|
1359
|
+
}
|
|
1360
|
+
if (options.enabledByDefault !== void 0 && options.disabledByDefault === true) {
|
|
1361
|
+
throw new UserConfigValidationError(
|
|
1362
|
+
"enabled-by-default and disabled-by-default cannot both be set"
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
if (options.enabledByDefault !== void 0) {
|
|
1366
|
+
patch.enabledByDefault = options.enabledByDefault;
|
|
1367
|
+
}
|
|
1368
|
+
if (options.disabledByDefault === true) {
|
|
1369
|
+
patch.enabledByDefault = false;
|
|
1370
|
+
}
|
|
1371
|
+
return patch;
|
|
1372
|
+
}
|
|
1373
|
+
function buildTableOutput4(result) {
|
|
1374
|
+
const summary = printTable(
|
|
1375
|
+
[
|
|
1376
|
+
{
|
|
1377
|
+
agentId: result.agentId,
|
|
1378
|
+
configPath: result.configPath,
|
|
1379
|
+
changed: String(result.changed)
|
|
1380
|
+
}
|
|
1381
|
+
],
|
|
1382
|
+
[
|
|
1383
|
+
{ key: "agentId", label: "Agent" },
|
|
1384
|
+
{ key: "configPath", label: "Config Path" },
|
|
1385
|
+
{ key: "changed", label: "Changed" }
|
|
1386
|
+
]
|
|
1387
|
+
);
|
|
1388
|
+
const detail = printTable(
|
|
1389
|
+
[
|
|
1390
|
+
{
|
|
1391
|
+
stableName: result.agent.stableName ?? "",
|
|
1392
|
+
platforms: (result.agent.supportedPlatforms ?? []).join(","),
|
|
1393
|
+
root: result.agent.homeRelativeRootPath ?? "",
|
|
1394
|
+
skills: result.agent.skillsDirectoryPath ?? "",
|
|
1395
|
+
enabledByDefault: result.agent.enabledByDefault === void 0 ? "" : String(result.agent.enabledByDefault)
|
|
1396
|
+
}
|
|
1397
|
+
],
|
|
1398
|
+
[
|
|
1399
|
+
{ key: "stableName", label: "Name" },
|
|
1400
|
+
{ key: "platforms", label: "Platforms" },
|
|
1401
|
+
{ key: "root", label: "Root" },
|
|
1402
|
+
{ key: "skills", label: "Skills Dir" },
|
|
1403
|
+
{ key: "enabledByDefault", label: "Enabled By Default" }
|
|
1404
|
+
]
|
|
1405
|
+
);
|
|
1406
|
+
return `${summary}${detail}`;
|
|
1407
|
+
}
|
|
1408
|
+
async function runConfigUpdateAgent(options) {
|
|
1409
|
+
const homeDir = options.homeDir ?? homedir5();
|
|
1410
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1411
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1412
|
+
const configPath = buildConfigPath(skillmuxHome);
|
|
1413
|
+
const config = await loadUserConfig(skillmuxHome);
|
|
1414
|
+
const agentId = normalizeAgentId(options.id);
|
|
1415
|
+
const previous = config.agents[agentId];
|
|
1416
|
+
if (previous === void 0) {
|
|
1417
|
+
throw new UserConfigValidationError(`Agent override does not exist: ${agentId}`);
|
|
1418
|
+
}
|
|
1419
|
+
const agent = {
|
|
1420
|
+
...previous,
|
|
1421
|
+
...buildAgentPatch(options)
|
|
1422
|
+
};
|
|
1423
|
+
const changed = JSON.stringify(previous) !== JSON.stringify(agent);
|
|
1424
|
+
const nextConfig = {
|
|
1425
|
+
...config,
|
|
1426
|
+
agents: {
|
|
1427
|
+
...config.agents,
|
|
1428
|
+
[agentId]: agent
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
if (changed) {
|
|
1432
|
+
await writeUserConfig(skillmuxHome, nextConfig);
|
|
1433
|
+
}
|
|
1434
|
+
const resultWithoutOutput = {
|
|
1435
|
+
skillmuxHome,
|
|
1436
|
+
configPath,
|
|
1437
|
+
agentId,
|
|
1438
|
+
changed,
|
|
1439
|
+
agent,
|
|
1440
|
+
config: nextConfig
|
|
1441
|
+
};
|
|
1442
|
+
return {
|
|
1443
|
+
...resultWithoutOutput,
|
|
1444
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput4(resultWithoutOutput)
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/commands/config.ts
|
|
1449
|
+
import { homedir as homedir6 } from "os";
|
|
1450
|
+
function buildTableOutput5(result) {
|
|
1451
|
+
const summary = printTable(
|
|
1452
|
+
[
|
|
1453
|
+
{
|
|
1454
|
+
skillmuxHome: result.skillmuxHome,
|
|
1455
|
+
configPath: result.configPath,
|
|
1456
|
+
overrides: String(Object.keys(result.config.agents).length)
|
|
1457
|
+
}
|
|
1458
|
+
],
|
|
1459
|
+
[
|
|
1460
|
+
{ key: "skillmuxHome", label: "SkillMux Home" },
|
|
1461
|
+
{ key: "configPath", label: "Config Path" },
|
|
1462
|
+
{ key: "overrides", label: "Overrides" }
|
|
1463
|
+
]
|
|
1464
|
+
);
|
|
1465
|
+
const agentRows = Object.entries(result.config.agents).sort(([left], [right]) => left.localeCompare(right)).map(([agentId, agent]) => ({
|
|
1466
|
+
agentId,
|
|
1467
|
+
stableName: agent.stableName ?? "",
|
|
1468
|
+
root: agent.homeRelativeRootPath ?? "",
|
|
1469
|
+
skills: agent.skillsDirectoryPath ?? ""
|
|
1470
|
+
}));
|
|
1471
|
+
if (agentRows.length === 0) {
|
|
1472
|
+
return `${summary}
|
|
1473
|
+
No user overrides configured.
|
|
1474
|
+
`;
|
|
1475
|
+
}
|
|
1476
|
+
return `${summary}
|
|
1477
|
+
${printTable(agentRows, [
|
|
1478
|
+
{ key: "agentId", label: "Agent" },
|
|
1479
|
+
{ key: "stableName", label: "Name" },
|
|
1480
|
+
{ key: "root", label: "Root" },
|
|
1481
|
+
{ key: "skills", label: "Skills Dir" }
|
|
1482
|
+
])}`;
|
|
1483
|
+
}
|
|
1484
|
+
async function runConfig(options = {}) {
|
|
1485
|
+
const homeDir = options.homeDir ?? homedir6();
|
|
1486
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1487
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1488
|
+
const config = await loadUserConfig(skillmuxHome);
|
|
1489
|
+
const resultWithoutOutput = {
|
|
1490
|
+
skillmuxHome,
|
|
1491
|
+
configPath: buildConfigPath(skillmuxHome),
|
|
1492
|
+
config
|
|
1493
|
+
};
|
|
1494
|
+
return {
|
|
1495
|
+
...resultWithoutOutput,
|
|
1496
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput5(resultWithoutOutput)
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
|
|
948
1500
|
// src/commands/doctor.ts
|
|
1501
|
+
import * as fs11 from "fs/promises";
|
|
1502
|
+
import { homedir as homedir7 } from "os";
|
|
1503
|
+
import { join as join8 } from "path";
|
|
949
1504
|
function buildIssue2(code, severity, message, path) {
|
|
950
1505
|
return path === void 0 ? { code, severity, message } : { code, severity, message, path };
|
|
951
1506
|
}
|
|
952
1507
|
async function pathExists2(path) {
|
|
953
1508
|
try {
|
|
954
|
-
await
|
|
1509
|
+
await fs11.access(path);
|
|
955
1510
|
return true;
|
|
956
1511
|
} catch (error) {
|
|
957
1512
|
if (error.code === "ENOENT") {
|
|
@@ -965,7 +1520,7 @@ async function addUnmanagedDirectoryIssues(entries, issues) {
|
|
|
965
1520
|
if (entry.kind !== "unmanaged-directory") {
|
|
966
1521
|
continue;
|
|
967
1522
|
}
|
|
968
|
-
if (await pathExists2(
|
|
1523
|
+
if (await pathExists2(join8(entry.path, "SKILL.md"))) {
|
|
969
1524
|
issues.push(
|
|
970
1525
|
buildIssue2(
|
|
971
1526
|
"unmanaged-skill-directory",
|
|
@@ -1015,7 +1570,7 @@ function addConflictingAgentPathIssues(agents, issues) {
|
|
|
1015
1570
|
);
|
|
1016
1571
|
}
|
|
1017
1572
|
}
|
|
1018
|
-
function
|
|
1573
|
+
function buildTableOutput6(issues) {
|
|
1019
1574
|
if (issues.length === 0) {
|
|
1020
1575
|
return "No doctor issues found.\n";
|
|
1021
1576
|
}
|
|
@@ -1047,7 +1602,7 @@ function buildJsonOutput(result) {
|
|
|
1047
1602
|
});
|
|
1048
1603
|
}
|
|
1049
1604
|
async function runDoctor(options = {}) {
|
|
1050
|
-
const homeDir = options.homeDir ??
|
|
1605
|
+
const homeDir = options.homeDir ?? homedir7();
|
|
1051
1606
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1052
1607
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1053
1608
|
const [manifest, config, agents] = await Promise.all([
|
|
@@ -1084,135 +1639,14 @@ async function runDoctor(options = {}) {
|
|
|
1084
1639
|
};
|
|
1085
1640
|
return {
|
|
1086
1641
|
...resultWithoutOutput,
|
|
1087
|
-
output: options.json === true ? buildJsonOutput(resultWithoutOutput) :
|
|
1642
|
+
output: options.json === true ? buildJsonOutput(resultWithoutOutput) : buildTableOutput6(dedupedIssues)
|
|
1088
1643
|
};
|
|
1089
1644
|
}
|
|
1090
1645
|
|
|
1091
1646
|
// src/commands/disable.ts
|
|
1092
1647
|
import * as fs13 from "fs/promises";
|
|
1093
|
-
import { homedir as
|
|
1094
|
-
import { join as
|
|
1095
|
-
|
|
1096
|
-
// src/fs/safe-copy.ts
|
|
1097
|
-
import * as fs10 from "fs/promises";
|
|
1098
|
-
import { dirname as dirname2, join as join6, resolve as resolve6 } from "path";
|
|
1099
|
-
async function assertDirectory(path) {
|
|
1100
|
-
const entry = await fs10.lstat(path);
|
|
1101
|
-
if (!entry.isDirectory()) {
|
|
1102
|
-
throw new Error(`Expected a directory at ${path}`);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
async function assertRegularFile(path, label) {
|
|
1106
|
-
const entry = await fs10.lstat(path);
|
|
1107
|
-
if (!entry.isFile()) {
|
|
1108
|
-
throw new Error(`Expected ${label} to be a regular file at ${path}`);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
async function assertTargetDoesNotExist(path) {
|
|
1112
|
-
try {
|
|
1113
|
-
await fs10.lstat(path);
|
|
1114
|
-
throw new Error(`Refusing to overwrite existing path at ${path}`);
|
|
1115
|
-
} catch (error) {
|
|
1116
|
-
if (error.code !== "ENOENT") {
|
|
1117
|
-
throw error;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
async function copyDirectoryContents(sourcePath, targetPath) {
|
|
1122
|
-
await fs10.mkdir(targetPath, { recursive: true });
|
|
1123
|
-
const entries = await fs10.readdir(sourcePath, { withFileTypes: true });
|
|
1124
|
-
for (const entry of entries) {
|
|
1125
|
-
const sourceEntryPath = join6(sourcePath, entry.name);
|
|
1126
|
-
const targetEntryPath = join6(targetPath, entry.name);
|
|
1127
|
-
const entryStats = await fs10.lstat(sourceEntryPath);
|
|
1128
|
-
if (entryStats.isSymbolicLink()) {
|
|
1129
|
-
throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
|
|
1130
|
-
}
|
|
1131
|
-
if (entryStats.isDirectory()) {
|
|
1132
|
-
await copyDirectoryContents(sourceEntryPath, targetEntryPath);
|
|
1133
|
-
continue;
|
|
1134
|
-
}
|
|
1135
|
-
if (entryStats.isFile()) {
|
|
1136
|
-
await fs10.mkdir(dirname2(targetEntryPath), { recursive: true });
|
|
1137
|
-
await fs10.copyFile(sourceEntryPath, targetEntryPath);
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
throw new Error(`Unsupported filesystem entry at ${sourceEntryPath}`);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
async function assertSkillSourceLayout(sourcePath) {
|
|
1144
|
-
const resolvedSourcePath = resolve6(sourcePath);
|
|
1145
|
-
const skillFilePath = join6(resolvedSourcePath, "SKILL.md");
|
|
1146
|
-
await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
|
|
1147
|
-
await assertDirectory(resolvedSourcePath);
|
|
1148
|
-
try {
|
|
1149
|
-
await assertRegularFile(skillFilePath, "SKILL.md");
|
|
1150
|
-
} catch (error) {
|
|
1151
|
-
if (error.code === "ENOENT") {
|
|
1152
|
-
throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
|
|
1153
|
-
}
|
|
1154
|
-
throw error;
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
async function copySkillContentsToManagedStore(sourcePath, targetPath) {
|
|
1158
|
-
const resolvedSourcePath = resolve6(sourcePath);
|
|
1159
|
-
const resolvedTargetPath = resolve6(targetPath);
|
|
1160
|
-
if (pathsAreEqual(resolvedSourcePath, resolvedTargetPath)) {
|
|
1161
|
-
throw new Error("Source and target paths must differ");
|
|
1162
|
-
}
|
|
1163
|
-
if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
|
|
1164
|
-
throw new Error("Refusing to copy into a child of the source directory");
|
|
1165
|
-
}
|
|
1166
|
-
await assertSkillSourceLayout(resolvedSourcePath);
|
|
1167
|
-
await assertNoSymlinkAncestors(resolvedTargetPath);
|
|
1168
|
-
await assertTargetDoesNotExist(resolvedTargetPath);
|
|
1169
|
-
await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// src/fs/link-ops.ts
|
|
1173
|
-
import * as fs11 from "fs/promises";
|
|
1174
|
-
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
1175
|
-
var directoryLinkType = process.platform === "win32" ? "junction" : "dir";
|
|
1176
|
-
async function createManagedLink(linkPath, targetPath) {
|
|
1177
|
-
const resolvedLinkPath = resolve7(linkPath);
|
|
1178
|
-
const resolvedTargetPath = resolve7(targetPath);
|
|
1179
|
-
await assertNoSymlinkAncestors(resolvedLinkPath);
|
|
1180
|
-
await assertNoSymlinkAncestors(resolvedTargetPath, { includeLeaf: true });
|
|
1181
|
-
await fs11.mkdir(dirname3(resolvedLinkPath), { recursive: true });
|
|
1182
|
-
try {
|
|
1183
|
-
const existingEntry = await fs11.lstat(resolvedLinkPath);
|
|
1184
|
-
if (!existingEntry.isSymbolicLink()) {
|
|
1185
|
-
throw new Error(`Refusing to replace non-link entry at ${resolvedLinkPath}`);
|
|
1186
|
-
}
|
|
1187
|
-
const currentTargetPath = await fs11.realpath(resolvedLinkPath);
|
|
1188
|
-
if (pathsAreEqual(currentTargetPath, resolvedTargetPath)) {
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
throw new Error(`Refusing to replace link at ${resolvedLinkPath}`);
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
if (error.code === "ENOENT" && await fs11.lstat(resolvedLinkPath).then((entry) => entry.isSymbolicLink()).catch(() => false)) {
|
|
1194
|
-
await fs11.rm(resolvedLinkPath, { recursive: true, force: false });
|
|
1195
|
-
} else if (error.code !== "ENOENT") {
|
|
1196
|
-
throw error;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
await fs11.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
|
|
1200
|
-
}
|
|
1201
|
-
async function isLinkPointingToTarget(linkPath, targetPath) {
|
|
1202
|
-
try {
|
|
1203
|
-
const entry = await fs11.lstat(linkPath);
|
|
1204
|
-
if (!entry.isSymbolicLink()) {
|
|
1205
|
-
return false;
|
|
1206
|
-
}
|
|
1207
|
-
const resolvedTargetPath = await fs11.realpath(linkPath);
|
|
1208
|
-
return pathsAreEqual(resolvedTargetPath, targetPath);
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
if (error.code === "ENOENT") {
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
throw error;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1648
|
+
import { homedir as homedir8 } from "os";
|
|
1649
|
+
import { join as join9, resolve as resolve9 } from "path";
|
|
1216
1650
|
|
|
1217
1651
|
// src/fs/safe-remove-link.ts
|
|
1218
1652
|
import * as fs12 from "fs/promises";
|
|
@@ -1233,7 +1667,7 @@ async function safeRemoveLink(path) {
|
|
|
1233
1667
|
}
|
|
1234
1668
|
|
|
1235
1669
|
// src/commands/disable.ts
|
|
1236
|
-
function
|
|
1670
|
+
function buildAgentRecord2(agent, timestamp) {
|
|
1237
1671
|
return {
|
|
1238
1672
|
id: agent.id,
|
|
1239
1673
|
name: agent.stableName,
|
|
@@ -1243,7 +1677,7 @@ function buildAgentRecord(agent, timestamp) {
|
|
|
1243
1677
|
lastSeenAt: agent.exists ? timestamp : null
|
|
1244
1678
|
};
|
|
1245
1679
|
}
|
|
1246
|
-
function
|
|
1680
|
+
function buildActivationRecord2(skillId, agentId, linkPath, timestamp) {
|
|
1247
1681
|
return {
|
|
1248
1682
|
skillId,
|
|
1249
1683
|
agentId,
|
|
@@ -1252,7 +1686,7 @@ function buildActivationRecord(skillId, agentId, linkPath, timestamp) {
|
|
|
1252
1686
|
updatedAt: timestamp
|
|
1253
1687
|
};
|
|
1254
1688
|
}
|
|
1255
|
-
function
|
|
1689
|
+
function upsertActivation2(manifest, activation) {
|
|
1256
1690
|
const index = manifest.activations.findIndex(
|
|
1257
1691
|
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
1258
1692
|
);
|
|
@@ -1262,8 +1696,8 @@ function upsertActivation(manifest, activation) {
|
|
|
1262
1696
|
}
|
|
1263
1697
|
manifest.activations[index] = activation;
|
|
1264
1698
|
}
|
|
1265
|
-
function
|
|
1266
|
-
return
|
|
1699
|
+
function buildManagedSkillPath2(skillmuxHome, skillId) {
|
|
1700
|
+
return resolve9(skillmuxHome, "skills", skillId);
|
|
1267
1701
|
}
|
|
1268
1702
|
async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName, linkPath, timestamp) {
|
|
1269
1703
|
try {
|
|
@@ -1279,7 +1713,7 @@ async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName,
|
|
|
1279
1713
|
}
|
|
1280
1714
|
const sourcePath = await fs13.realpath(linkPath);
|
|
1281
1715
|
await assertSkillSourceLayout(sourcePath);
|
|
1282
|
-
const managedSkillPath =
|
|
1716
|
+
const managedSkillPath = buildManagedSkillPath2(skillmuxHome, skillId);
|
|
1283
1717
|
await copySkillContentsToManagedStore(sourcePath, managedSkillPath);
|
|
1284
1718
|
const skill = {
|
|
1285
1719
|
id: skillId,
|
|
@@ -1294,7 +1728,7 @@ async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName,
|
|
|
1294
1728
|
manifest.skills[skillId] = skill;
|
|
1295
1729
|
return { skill, sourcePath };
|
|
1296
1730
|
}
|
|
1297
|
-
async function
|
|
1731
|
+
async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
1298
1732
|
const agentId = normalizeId(agentName);
|
|
1299
1733
|
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
1300
1734
|
const agent = agents.find((entry) => entry.id === agentId);
|
|
@@ -1317,15 +1751,18 @@ async function pathExists3(path) {
|
|
|
1317
1751
|
throw error;
|
|
1318
1752
|
}
|
|
1319
1753
|
}
|
|
1320
|
-
async function
|
|
1321
|
-
|
|
1754
|
+
async function runDisableSingle(options) {
|
|
1755
|
+
if (options.agent === void 0) {
|
|
1756
|
+
throw new Error("Disable requires one target agent");
|
|
1757
|
+
}
|
|
1758
|
+
const homeDir = options.homeDir ?? homedir8();
|
|
1322
1759
|
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1323
1760
|
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1324
1761
|
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1325
1762
|
const manifest = await readManifest(skillmuxHome);
|
|
1326
1763
|
const skillId = normalizeId(options.skill);
|
|
1327
|
-
const agent = await
|
|
1328
|
-
const linkPath =
|
|
1764
|
+
const agent = await resolveTargetAgent2(homeDir, skillmuxHome, options.agent);
|
|
1765
|
+
const linkPath = join9(agent.absoluteSkillsDirectoryPath, skillId);
|
|
1329
1766
|
const adoption = manifest.skills[skillId] ? void 0 : await tryAdoptManagedSkill(
|
|
1330
1767
|
manifest,
|
|
1331
1768
|
skillmuxHome,
|
|
@@ -1342,7 +1779,7 @@ async function runDisable(options) {
|
|
|
1342
1779
|
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1343
1780
|
);
|
|
1344
1781
|
const activationLinkPath = currentActivation?.linkPath ?? linkPath;
|
|
1345
|
-
const agentRecord =
|
|
1782
|
+
const agentRecord = buildAgentRecord2(agent, timestamp);
|
|
1346
1783
|
manifest.agents[agent.id] = agentRecord;
|
|
1347
1784
|
const adoptedLinkRemoved = adoption !== void 0 ? await safeRemoveLink(linkPath) : false;
|
|
1348
1785
|
const linkMatchesSkill = adoption === void 0 ? await isLinkPointingToTarget(activationLinkPath, skill.path) : false;
|
|
@@ -1361,8 +1798,8 @@ async function runDisable(options) {
|
|
|
1361
1798
|
`
|
|
1362
1799
|
};
|
|
1363
1800
|
}
|
|
1364
|
-
const activation =
|
|
1365
|
-
|
|
1801
|
+
const activation = buildActivationRecord2(skill.id, agent.id, linkPath, timestamp);
|
|
1802
|
+
upsertActivation2(manifest, activation);
|
|
1366
1803
|
await writeManifest(skillmuxHome, manifest);
|
|
1367
1804
|
return {
|
|
1368
1805
|
changed: true,
|
|
@@ -1374,12 +1811,44 @@ async function runDisable(options) {
|
|
|
1374
1811
|
`
|
|
1375
1812
|
};
|
|
1376
1813
|
}
|
|
1814
|
+
async function runDisable(options) {
|
|
1815
|
+
if (options.agents !== void 0) {
|
|
1816
|
+
const results = [];
|
|
1817
|
+
for (const agent of options.agents) {
|
|
1818
|
+
try {
|
|
1819
|
+
results.push(await runDisableSingle({ ...options, agent, agents: void 0 }));
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
throw new BatchOperationError({
|
|
1822
|
+
operation: "disable",
|
|
1823
|
+
failedItem: agent,
|
|
1824
|
+
failedAction: `disable ${options.skill} for ${agent}`,
|
|
1825
|
+
completedAction: "disabling",
|
|
1826
|
+
completedItems: results.map((result) => result.agent.id),
|
|
1827
|
+
cause: error
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
if (results.length === 0) {
|
|
1832
|
+
throw new Error("Disable requires at least one target agent");
|
|
1833
|
+
}
|
|
1834
|
+
const lastResult = results[results.length - 1];
|
|
1835
|
+
return {
|
|
1836
|
+
changed: results.some((result) => result.changed),
|
|
1837
|
+
skill: lastResult.skill,
|
|
1838
|
+
results,
|
|
1839
|
+
changedAgents: results.filter((result) => result.changed).map((result) => result.agent.id),
|
|
1840
|
+
manifest: lastResult.manifest,
|
|
1841
|
+
output: results.map((result) => result.output).join("")
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
return runDisableSingle(options);
|
|
1845
|
+
}
|
|
1377
1846
|
|
|
1378
1847
|
// src/commands/enable.ts
|
|
1379
1848
|
import * as fs14 from "fs/promises";
|
|
1380
|
-
import { homedir as
|
|
1381
|
-
import { join as
|
|
1382
|
-
function
|
|
1849
|
+
import { homedir as homedir9 } from "os";
|
|
1850
|
+
import { join as join10 } from "path";
|
|
1851
|
+
function buildAgentRecord3(agent, timestamp) {
|
|
1383
1852
|
return {
|
|
1384
1853
|
id: agent.id,
|
|
1385
1854
|
name: agent.stableName,
|
|
@@ -1389,7 +1858,7 @@ function buildAgentRecord2(agent, timestamp) {
|
|
|
1389
1858
|
lastSeenAt: timestamp
|
|
1390
1859
|
};
|
|
1391
1860
|
}
|
|
1392
|
-
function
|
|
1861
|
+
function buildActivationRecord3(skillId, agentId, linkPath, timestamp, state) {
|
|
1393
1862
|
return {
|
|
1394
1863
|
skillId,
|
|
1395
1864
|
agentId,
|
|
@@ -1398,7 +1867,7 @@ function buildActivationRecord2(skillId, agentId, linkPath, timestamp, state) {
|
|
|
1398
1867
|
updatedAt: timestamp
|
|
1399
1868
|
};
|
|
1400
1869
|
}
|
|
1401
|
-
function
|
|
1870
|
+
function upsertActivation3(manifest, activation) {
|
|
1402
1871
|
const index = manifest.activations.findIndex(
|
|
1403
1872
|
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
1404
1873
|
);
|
|
@@ -1408,7 +1877,7 @@ function upsertActivation2(manifest, activation) {
|
|
|
1408
1877
|
}
|
|
1409
1878
|
manifest.activations[index] = activation;
|
|
1410
1879
|
}
|
|
1411
|
-
async function
|
|
1880
|
+
async function resolveTargetAgent3(homeDir, skillmuxHome, agentName) {
|
|
1412
1881
|
const agentId = normalizeId(agentName);
|
|
1413
1882
|
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
1414
1883
|
const agent = agents.find((entry) => entry.id === agentId);
|
|
@@ -1420,8 +1889,11 @@ async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
|
1420
1889
|
}
|
|
1421
1890
|
return agent;
|
|
1422
1891
|
}
|
|
1423
|
-
async function
|
|
1424
|
-
|
|
1892
|
+
async function runEnableSingle(options) {
|
|
1893
|
+
if (options.agent === void 0) {
|
|
1894
|
+
throw new Error("Enable requires one target agent");
|
|
1895
|
+
}
|
|
1896
|
+
const homeDir = options.homeDir ?? homedir9();
|
|
1425
1897
|
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1426
1898
|
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1427
1899
|
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1431,12 +1903,12 @@ async function runEnable(options) {
|
|
|
1431
1903
|
if (skill === void 0) {
|
|
1432
1904
|
throw new Error(`Managed skill not found: ${skillId}`);
|
|
1433
1905
|
}
|
|
1434
|
-
const agent = await
|
|
1435
|
-
const linkPath =
|
|
1906
|
+
const agent = await resolveTargetAgent3(homeDir, skillmuxHome, options.agent);
|
|
1907
|
+
const linkPath = join10(agent.absoluteSkillsDirectoryPath, skill.id);
|
|
1436
1908
|
const currentActivation = manifest.activations.find(
|
|
1437
1909
|
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1438
1910
|
);
|
|
1439
|
-
const agentRecord =
|
|
1911
|
+
const agentRecord = buildAgentRecord3(agent, timestamp);
|
|
1440
1912
|
manifest.agents[agent.id] = agentRecord;
|
|
1441
1913
|
await fs14.mkdir(agent.absoluteSkillsDirectoryPath, { recursive: true });
|
|
1442
1914
|
const linkAlreadyEnabled = await isLinkPointingToTarget(linkPath, skill.path);
|
|
@@ -1453,14 +1925,14 @@ async function runEnable(options) {
|
|
|
1453
1925
|
};
|
|
1454
1926
|
}
|
|
1455
1927
|
await createManagedLink(linkPath, skill.path);
|
|
1456
|
-
const activation =
|
|
1928
|
+
const activation = buildActivationRecord3(
|
|
1457
1929
|
skill.id,
|
|
1458
1930
|
agent.id,
|
|
1459
1931
|
linkPath,
|
|
1460
1932
|
timestamp,
|
|
1461
1933
|
"enabled"
|
|
1462
1934
|
);
|
|
1463
|
-
|
|
1935
|
+
upsertActivation3(manifest, activation);
|
|
1464
1936
|
await writeManifest(skillmuxHome, manifest);
|
|
1465
1937
|
return {
|
|
1466
1938
|
changed: true,
|
|
@@ -1472,22 +1944,54 @@ async function runEnable(options) {
|
|
|
1472
1944
|
`
|
|
1473
1945
|
};
|
|
1474
1946
|
}
|
|
1947
|
+
async function runEnable(options) {
|
|
1948
|
+
if (options.agents !== void 0) {
|
|
1949
|
+
const results = [];
|
|
1950
|
+
for (const agent of options.agents) {
|
|
1951
|
+
try {
|
|
1952
|
+
results.push(await runEnableSingle({ ...options, agent, agents: void 0 }));
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
throw new BatchOperationError({
|
|
1955
|
+
operation: "enable",
|
|
1956
|
+
failedItem: agent,
|
|
1957
|
+
failedAction: `enable ${options.skill} for ${agent}`,
|
|
1958
|
+
completedAction: "enabling",
|
|
1959
|
+
completedItems: results.map((result) => result.agent.id),
|
|
1960
|
+
cause: error
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (results.length === 0) {
|
|
1965
|
+
throw new Error("Enable requires at least one target agent");
|
|
1966
|
+
}
|
|
1967
|
+
const lastResult = results[results.length - 1];
|
|
1968
|
+
return {
|
|
1969
|
+
changed: results.some((result) => result.changed),
|
|
1970
|
+
skill: lastResult.skill,
|
|
1971
|
+
results,
|
|
1972
|
+
changedAgents: results.filter((result) => result.changed).map((result) => result.agent.id),
|
|
1973
|
+
manifest: lastResult.manifest,
|
|
1974
|
+
output: results.map((result) => result.output).join("")
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
return runEnableSingle(options);
|
|
1978
|
+
}
|
|
1475
1979
|
|
|
1476
1980
|
// src/commands/import.ts
|
|
1477
|
-
import { resolve as
|
|
1478
|
-
import { homedir as
|
|
1479
|
-
function
|
|
1480
|
-
return
|
|
1981
|
+
import { resolve as resolve10 } from "path";
|
|
1982
|
+
import { homedir as homedir10 } from "os";
|
|
1983
|
+
function buildManagedSkillPath3(skillmuxHome, skillId) {
|
|
1984
|
+
return resolve10(skillmuxHome, "skills", skillId);
|
|
1481
1985
|
}
|
|
1482
1986
|
async function runImport(options) {
|
|
1483
|
-
const homeDir = options.homeDir ??
|
|
1987
|
+
const homeDir = options.homeDir ?? homedir10();
|
|
1484
1988
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1485
1989
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1486
|
-
const sourcePath =
|
|
1990
|
+
const sourcePath = resolve10(options.sourcePath);
|
|
1487
1991
|
const skillId = normalizeId(options.skillName);
|
|
1488
1992
|
const importedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1489
1993
|
const manifest = await readManifest(skillmuxHome);
|
|
1490
|
-
const managedSkillPath =
|
|
1994
|
+
const managedSkillPath = buildManagedSkillPath3(skillmuxHome, skillId);
|
|
1491
1995
|
await assertSkillSourceLayout(sourcePath);
|
|
1492
1996
|
if (manifest.skills[skillId] !== void 0) {
|
|
1493
1997
|
throw new Error(`Managed skill already exists for ${skillId}`);
|
|
@@ -1514,7 +2018,7 @@ async function runImport(options) {
|
|
|
1514
2018
|
}
|
|
1515
2019
|
|
|
1516
2020
|
// src/commands/scan.ts
|
|
1517
|
-
import { homedir as
|
|
2021
|
+
import { homedir as homedir11 } from "os";
|
|
1518
2022
|
|
|
1519
2023
|
// src/output/format-issue.ts
|
|
1520
2024
|
function formatIssue(issue) {
|
|
@@ -1525,7 +2029,7 @@ function formatIssue(issue) {
|
|
|
1525
2029
|
}
|
|
1526
2030
|
|
|
1527
2031
|
// src/commands/scan.ts
|
|
1528
|
-
function
|
|
2032
|
+
function buildAgentRecord4(agent, timestamp, previousRecord) {
|
|
1529
2033
|
const lastSeenAt = agent.exists ? timestamp : previousRecord?.lastSeenAt ?? null;
|
|
1530
2034
|
return {
|
|
1531
2035
|
id: agent.id,
|
|
@@ -1575,7 +2079,7 @@ ${result.issues.map(formatIssue).join("\n")}
|
|
|
1575
2079
|
`;
|
|
1576
2080
|
}
|
|
1577
2081
|
async function runScan(options = {}) {
|
|
1578
|
-
const homeDir = options.homeDir ??
|
|
2082
|
+
const homeDir = options.homeDir ?? homedir11();
|
|
1579
2083
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1580
2084
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1581
2085
|
const manifest = await readManifest(skillmuxHome);
|
|
@@ -1591,7 +2095,7 @@ async function runScan(options = {}) {
|
|
|
1591
2095
|
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
1592
2096
|
entries.push(...scannedAgent.entries);
|
|
1593
2097
|
issues.push(...scannedAgent.issues);
|
|
1594
|
-
manifest.agents[agent.id] =
|
|
2098
|
+
manifest.agents[agent.id] = buildAgentRecord4(
|
|
1595
2099
|
agent,
|
|
1596
2100
|
timestamp,
|
|
1597
2101
|
manifest.agents[agent.id]
|
|
@@ -1681,7 +2185,7 @@ function buildListData(scanResult, view) {
|
|
|
1681
2185
|
}
|
|
1682
2186
|
return buildRecordsView(scanResult);
|
|
1683
2187
|
}
|
|
1684
|
-
function
|
|
2188
|
+
function buildTableOutput7(data, view) {
|
|
1685
2189
|
if (view === "agents") {
|
|
1686
2190
|
const agentRows = data.agents;
|
|
1687
2191
|
return printTable(
|
|
@@ -1731,14 +2235,204 @@ async function runList(options = {}) {
|
|
|
1731
2235
|
const data = buildListData(scanResult, view);
|
|
1732
2236
|
return {
|
|
1733
2237
|
data,
|
|
1734
|
-
output: format === "json" ? printJson(data) :
|
|
2238
|
+
output: format === "json" ? printJson(data) : buildTableOutput7(data, view)
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/commands/remove.ts
|
|
2243
|
+
import * as fs15 from "fs/promises";
|
|
2244
|
+
import { homedir as homedir12 } from "os";
|
|
2245
|
+
import { resolve as resolve11 } from "path";
|
|
2246
|
+
function buildManagedSkillPath4(skillmuxHome, skillId) {
|
|
2247
|
+
return resolve11(skillmuxHome, "skills", skillId);
|
|
2248
|
+
}
|
|
2249
|
+
function buildManifestPath(skillmuxHome) {
|
|
2250
|
+
return resolve11(skillmuxHome, "manifest.json");
|
|
2251
|
+
}
|
|
2252
|
+
function buildConfigPath2(skillmuxHome) {
|
|
2253
|
+
return resolve11(skillmuxHome, "config.json");
|
|
2254
|
+
}
|
|
2255
|
+
function resolveManagedSkill(manifest, skillNameOrId) {
|
|
2256
|
+
const skillId = normalizeId(skillNameOrId);
|
|
2257
|
+
const directMatch = manifest.skills[skillId];
|
|
2258
|
+
if (directMatch !== void 0) {
|
|
2259
|
+
return directMatch;
|
|
2260
|
+
}
|
|
2261
|
+
const nameMatches = Object.values(manifest.skills).filter(
|
|
2262
|
+
(skill) => normalizeId(skill.name) === skillId
|
|
2263
|
+
);
|
|
2264
|
+
if (nameMatches.length === 1) {
|
|
2265
|
+
return nameMatches[0];
|
|
2266
|
+
}
|
|
2267
|
+
if (nameMatches.length > 1) {
|
|
2268
|
+
const candidateIds = nameMatches.map((skill) => skill.id).sort((left, right) => left.localeCompare(right));
|
|
2269
|
+
throw new Error(
|
|
2270
|
+
`Ambiguous managed skill name ${skillNameOrId}: ${candidateIds.join(", ")}`
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
throw new Error(`Managed skill not found: ${skillId}`);
|
|
2274
|
+
}
|
|
2275
|
+
function buildHumanOutput(skill, managedSkillPath) {
|
|
2276
|
+
return `Removed ${skill.id} from ${managedSkillPath}
|
|
2277
|
+
`;
|
|
2278
|
+
}
|
|
2279
|
+
function buildJsonOutput2(result) {
|
|
2280
|
+
return printJson({
|
|
2281
|
+
changed: result.changed,
|
|
2282
|
+
removedSkillId: result.removedSkillId,
|
|
2283
|
+
skill: result.skill,
|
|
2284
|
+
location: result.location,
|
|
2285
|
+
manifest: result.manifest
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
async function pathExists4(path) {
|
|
2289
|
+
try {
|
|
2290
|
+
await fs15.lstat(path);
|
|
2291
|
+
return true;
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
if (error.code === "ENOENT") {
|
|
2294
|
+
return false;
|
|
2295
|
+
}
|
|
2296
|
+
throw error;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async function assertManagedSkillRemovalSafety(skillmuxHome, skillPath, skillId) {
|
|
2300
|
+
const expectedSkillPath = buildManagedSkillPath4(skillmuxHome, skillId);
|
|
2301
|
+
if (!pathsAreEqual(skillPath, expectedSkillPath)) {
|
|
2302
|
+
throw new Error(`Refusing to remove unmanaged skill path at ${skillPath}`);
|
|
2303
|
+
}
|
|
2304
|
+
await assertNoSymlinkAncestors(skillPath, { includeLeaf: true });
|
|
2305
|
+
if (!await pathExists4(skillPath)) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const entry = await fs15.lstat(skillPath);
|
|
2309
|
+
if (entry.isSymbolicLink()) {
|
|
2310
|
+
throw new Error(`Refusing to remove symlinked managed skill path at ${skillPath}`);
|
|
2311
|
+
}
|
|
2312
|
+
if (!entry.isDirectory()) {
|
|
2313
|
+
throw new Error(`Refusing to remove non-directory managed skill path at ${skillPath}`);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
async function runRemoveSingle(options) {
|
|
2317
|
+
if (options.skill === void 0) {
|
|
2318
|
+
throw new Error("Remove requires one target skill");
|
|
2319
|
+
}
|
|
2320
|
+
const homeDir = options.homeDir ?? homedir12();
|
|
2321
|
+
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
2322
|
+
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
2323
|
+
const manifestPath = buildManifestPath(skillmuxHome);
|
|
2324
|
+
const configPath = buildConfigPath2(skillmuxHome);
|
|
2325
|
+
const manifest = await readManifest(skillmuxHome);
|
|
2326
|
+
const skill = resolveManagedSkill(manifest, options.skill);
|
|
2327
|
+
const managedSkillsDirectory = resolve11(skillmuxHome, "skills");
|
|
2328
|
+
const managedSkillPath = skill.path;
|
|
2329
|
+
const enabledActivations = manifest.activations.filter(
|
|
2330
|
+
(activation) => activation.skillId === skill.id && activation.state === "enabled"
|
|
2331
|
+
);
|
|
2332
|
+
if (enabledActivations.length > 0) {
|
|
2333
|
+
const enabledAgents = [...new Set(enabledActivations.map((activation) => activation.agentId))].sort(
|
|
2334
|
+
(left, right) => left.localeCompare(right)
|
|
2335
|
+
);
|
|
2336
|
+
throw new Error(
|
|
2337
|
+
`Cannot remove ${skill.id}; it is still enabled for: ${enabledAgents.join(", ")}`
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
await assertManagedSkillRemovalSafety(skillmuxHome, managedSkillPath, skill.id);
|
|
2341
|
+
if (await pathExists4(managedSkillPath)) {
|
|
2342
|
+
await fs15.rm(managedSkillPath, { recursive: true, force: false });
|
|
2343
|
+
}
|
|
2344
|
+
delete manifest.skills[skill.id];
|
|
2345
|
+
manifest.activations = manifest.activations.filter(
|
|
2346
|
+
(activation) => activation.skillId !== skill.id
|
|
2347
|
+
);
|
|
2348
|
+
await writeManifest(skillmuxHome, manifest);
|
|
2349
|
+
const resultWithoutOutput = {
|
|
2350
|
+
changed: true,
|
|
2351
|
+
removedSkillId: skill.id,
|
|
2352
|
+
skill,
|
|
2353
|
+
location: {
|
|
2354
|
+
skillmuxHome,
|
|
2355
|
+
configPath,
|
|
2356
|
+
manifestPath,
|
|
2357
|
+
managedSkillsDirectory,
|
|
2358
|
+
managedSkillPath
|
|
2359
|
+
},
|
|
2360
|
+
manifest
|
|
2361
|
+
};
|
|
2362
|
+
return {
|
|
2363
|
+
...resultWithoutOutput,
|
|
2364
|
+
output: options.json === true ? buildJsonOutput2(resultWithoutOutput) : buildHumanOutput(skill, managedSkillPath)
|
|
1735
2365
|
};
|
|
1736
2366
|
}
|
|
2367
|
+
async function runRemove(options) {
|
|
2368
|
+
if (options.skills !== void 0) {
|
|
2369
|
+
const results = [];
|
|
2370
|
+
for (const skill of options.skills) {
|
|
2371
|
+
try {
|
|
2372
|
+
results.push(await runRemoveSingle({ ...options, skill, skills: void 0 }));
|
|
2373
|
+
} catch (error) {
|
|
2374
|
+
throw new BatchOperationError({
|
|
2375
|
+
operation: "remove",
|
|
2376
|
+
failedItem: skill,
|
|
2377
|
+
failedAction: `remove ${skill}`,
|
|
2378
|
+
completedAction: "removing",
|
|
2379
|
+
completedItems: results.map((result) => result.removedSkillId),
|
|
2380
|
+
cause: error
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
if (results.length === 0) {
|
|
2385
|
+
throw new Error("Remove requires at least one target skill");
|
|
2386
|
+
}
|
|
2387
|
+
const lastResult = results[results.length - 1];
|
|
2388
|
+
const resultWithoutOutput = {
|
|
2389
|
+
changed: results.some((result) => result.changed),
|
|
2390
|
+
removedSkillIds: results.map((result) => result.removedSkillId),
|
|
2391
|
+
results,
|
|
2392
|
+
manifest: lastResult.manifest
|
|
2393
|
+
};
|
|
2394
|
+
return {
|
|
2395
|
+
...resultWithoutOutput,
|
|
2396
|
+
output: options.json === true ? printJson(resultWithoutOutput) : results.map((result) => result.output).join("")
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
return runRemoveSingle(options);
|
|
2400
|
+
}
|
|
1737
2401
|
|
|
1738
2402
|
// src/index.ts
|
|
2403
|
+
function collectValues(value, previous = []) {
|
|
2404
|
+
return [...previous, value];
|
|
2405
|
+
}
|
|
2406
|
+
function requireSingleValue(values, label) {
|
|
2407
|
+
if (values.length !== 1) {
|
|
2408
|
+
throw new Error(`Expected exactly one ${label}`);
|
|
2409
|
+
}
|
|
2410
|
+
return values[0];
|
|
2411
|
+
}
|
|
2412
|
+
function requireAtLeastOneValue(values, label) {
|
|
2413
|
+
if (values.length === 0) {
|
|
2414
|
+
throw new Error(`Expected at least one ${label}`);
|
|
2415
|
+
}
|
|
2416
|
+
return values;
|
|
2417
|
+
}
|
|
1739
2418
|
function buildCli() {
|
|
1740
2419
|
const program = new Command();
|
|
1741
2420
|
program.name("skillmux");
|
|
2421
|
+
program.command("adopt").requiredOption("--agent <agent>", "Source agent id").option("--skill <skill>", "Repeatable installed skill to adopt", collectValues, []).option("--json", "Emit structured JSON output").action(async (options) => {
|
|
2422
|
+
const result = options.skill.length === 0 ? await runAdopt({
|
|
2423
|
+
agent: options.agent,
|
|
2424
|
+
json: options.json === true
|
|
2425
|
+
}) : options.skill.length === 1 ? await runAdopt({
|
|
2426
|
+
agent: options.agent,
|
|
2427
|
+
skill: options.skill[0],
|
|
2428
|
+
json: options.json === true
|
|
2429
|
+
}) : await runAdopt({
|
|
2430
|
+
agent: options.agent,
|
|
2431
|
+
skills: options.skill,
|
|
2432
|
+
json: options.json === true
|
|
2433
|
+
});
|
|
2434
|
+
process.stdout.write(result.output);
|
|
2435
|
+
});
|
|
1742
2436
|
program.command("scan").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1743
2437
|
const result = await runScan({ json: options.json === true });
|
|
1744
2438
|
process.stdout.write(result.output);
|
|
@@ -1796,17 +2490,48 @@ function buildCli() {
|
|
|
1796
2490
|
});
|
|
1797
2491
|
process.stdout.write(result.output);
|
|
1798
2492
|
});
|
|
1799
|
-
|
|
2493
|
+
configCommand.command("update-agent").requiredOption("--id <id>", "Agent id").option("--root <path>", "Home-relative root path").option("--skills <path>", "Skills directory path relative to the agent root").option("--name <name>", "Stable display name").option(
|
|
2494
|
+
"--platform <platform>",
|
|
2495
|
+
`Supported platform (${supportedPlatforms.join(", ")})`,
|
|
2496
|
+
(value, previous = []) => [...previous, value],
|
|
2497
|
+
[]
|
|
2498
|
+
).option("--enabled-by-default", "Mark this custom agent as enabled by default").option("--disabled-by-default", "Mark this custom agent as disabled by default").option("--json", "Emit structured JSON output").action(
|
|
2499
|
+
async (options) => {
|
|
2500
|
+
const result = await runConfigUpdateAgent({
|
|
2501
|
+
id: options.id,
|
|
2502
|
+
root: options.root,
|
|
2503
|
+
skills: options.skills,
|
|
2504
|
+
name: options.name,
|
|
2505
|
+
platforms: options.platform !== void 0 && options.platform.length > 0 ? options.platform : void 0,
|
|
2506
|
+
enabledByDefault: options.enabledByDefault === true ? true : void 0,
|
|
2507
|
+
disabledByDefault: options.disabledByDefault === true,
|
|
2508
|
+
json: options.json === true
|
|
2509
|
+
});
|
|
2510
|
+
process.stdout.write(result.output);
|
|
2511
|
+
}
|
|
2512
|
+
);
|
|
2513
|
+
program.command("enable").requiredOption("--skill <skill>", "Managed skill name or id", collectValues, []).requiredOption("--agent <agent>", "Repeatable target agent", collectValues, []).action(async (options) => {
|
|
1800
2514
|
const result = await runEnable({
|
|
1801
|
-
skill: options.skill,
|
|
1802
|
-
|
|
2515
|
+
skill: requireSingleValue(options.skill, "skill"),
|
|
2516
|
+
agents: requireAtLeastOneValue(options.agent, "agent")
|
|
1803
2517
|
});
|
|
1804
2518
|
process.stdout.write(result.output);
|
|
1805
2519
|
});
|
|
1806
|
-
program.command("disable").requiredOption("--skill <skill>", "Managed skill name or id").requiredOption("--agent <agent>", "
|
|
2520
|
+
program.command("disable").requiredOption("--skill <skill>", "Managed skill name or id", collectValues, []).requiredOption("--agent <agent>", "Repeatable target agent", collectValues, []).action(async (options) => {
|
|
1807
2521
|
const result = await runDisable({
|
|
1808
|
-
skill: options.skill,
|
|
1809
|
-
|
|
2522
|
+
skill: requireSingleValue(options.skill, "skill"),
|
|
2523
|
+
agents: requireAtLeastOneValue(options.agent, "agent")
|
|
2524
|
+
});
|
|
2525
|
+
process.stdout.write(result.output);
|
|
2526
|
+
});
|
|
2527
|
+
program.command("remove").requiredOption("--skill <skill>", "Repeatable managed skill name or id", collectValues, []).option("--json", "Emit structured JSON output").action(async (options) => {
|
|
2528
|
+
const skills = requireAtLeastOneValue(options.skill, "skill");
|
|
2529
|
+
const result = skills.length === 1 ? await runRemove({
|
|
2530
|
+
skill: skills[0],
|
|
2531
|
+
json: options.json === true
|
|
2532
|
+
}) : await runRemove({
|
|
2533
|
+
skills,
|
|
2534
|
+
json: options.json === true
|
|
1810
2535
|
});
|
|
1811
2536
|
process.stdout.write(result.output);
|
|
1812
2537
|
});
|