skillmux 0.1.0 → 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 +373 -61
- package/dist/cli.js +1483 -475
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1483 -475
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
|
|
4
|
-
// src/commands/agents.ts
|
|
5
|
-
import { homedir } from "os";
|
|
6
|
-
|
|
7
|
-
// src/config/resolve-skillmux-home.ts
|
|
8
|
-
import { join, resolve } from "path";
|
|
9
|
-
function buildConfigPath(skillmuxHome) {
|
|
10
|
-
return join(resolve(skillmuxHome), "config.json");
|
|
11
|
-
}
|
|
12
|
-
function resolveSkillmuxHome(homeDir) {
|
|
13
|
-
const resolvedHomeDir = resolve(homeDir);
|
|
14
|
-
const skillmuxHome = join(resolvedHomeDir, ".skillmux");
|
|
15
|
-
return {
|
|
16
|
-
skillmuxHome,
|
|
17
|
-
configPath: buildConfigPath(skillmuxHome)
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// src/discovery/discover-agents.ts
|
|
22
|
-
import * as fs2 from "fs/promises";
|
|
23
|
-
import { join as join2, resolve as resolve2 } from "path";
|
|
24
|
-
|
|
25
4
|
// src/config/default-agent-rules.ts
|
|
26
5
|
var supportedPlatforms = ["win32", "linux", "darwin"];
|
|
27
6
|
var builtInAgentIds = [
|
|
@@ -82,9 +61,9 @@ var defaultAgentRuleMap = Object.fromEntries(
|
|
|
82
61
|
defaultAgentRules.map((rule) => [rule.id, rule])
|
|
83
62
|
);
|
|
84
63
|
|
|
85
|
-
// src/
|
|
86
|
-
import
|
|
87
|
-
import {
|
|
64
|
+
// src/commands/adopt.ts
|
|
65
|
+
import { homedir } from "os";
|
|
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 {
|
|
@@ -93,6 +72,15 @@ var SkillMuxError = class extends Error {
|
|
|
93
72
|
this.name = new.target.name;
|
|
94
73
|
}
|
|
95
74
|
};
|
|
75
|
+
var InvalidIdentifierError = class extends SkillMuxError {
|
|
76
|
+
constructor(kind, value) {
|
|
77
|
+
super(`Invalid ${kind}: ${value}`);
|
|
78
|
+
this.kind = kind;
|
|
79
|
+
this.value = value;
|
|
80
|
+
}
|
|
81
|
+
kind;
|
|
82
|
+
value;
|
|
83
|
+
};
|
|
96
84
|
var ManifestValidationError = class extends SkillMuxError {
|
|
97
85
|
constructor(message) {
|
|
98
86
|
super(message);
|
|
@@ -103,8 +91,43 @@ var UserConfigValidationError = class extends SkillMuxError {
|
|
|
103
91
|
super(message);
|
|
104
92
|
}
|
|
105
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";
|
|
106
127
|
|
|
107
128
|
// src/config/load-user-config.ts
|
|
129
|
+
import * as fs from "fs/promises";
|
|
130
|
+
import { z } from "zod";
|
|
108
131
|
var supportedPlatformSchema = z.enum(supportedPlatforms);
|
|
109
132
|
var agentOverrideSchema = z.object({
|
|
110
133
|
stableName: z.string().min(1).optional(),
|
|
@@ -123,6 +146,9 @@ function createEmptyUserConfig() {
|
|
|
123
146
|
agents: {}
|
|
124
147
|
};
|
|
125
148
|
}
|
|
149
|
+
function stripUtf8Bom(contents) {
|
|
150
|
+
return contents.charCodeAt(0) === 65279 ? contents.slice(1) : contents;
|
|
151
|
+
}
|
|
126
152
|
function formatValidationIssues(error) {
|
|
127
153
|
return error.issues.map((issue) => {
|
|
128
154
|
const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
@@ -133,7 +159,7 @@ async function loadUserConfig(skillmuxHome) {
|
|
|
133
159
|
const configPath = buildConfigPath(skillmuxHome);
|
|
134
160
|
try {
|
|
135
161
|
const contents = await fs.readFile(configPath, "utf8");
|
|
136
|
-
const parsed = JSON.parse(contents);
|
|
162
|
+
const parsed = JSON.parse(stripUtf8Bom(contents));
|
|
137
163
|
const validated = userConfigSchema.safeParse(parsed);
|
|
138
164
|
if (!validated.success) {
|
|
139
165
|
throw new UserConfigValidationError(
|
|
@@ -237,127 +263,9 @@ async function discoverAgents(options) {
|
|
|
237
263
|
return discoveredAgents;
|
|
238
264
|
}
|
|
239
265
|
|
|
240
|
-
// src/output/print-json.ts
|
|
241
|
-
function printJson(value) {
|
|
242
|
-
return `${JSON.stringify(value, null, 2)}
|
|
243
|
-
`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// src/output/print-table.ts
|
|
247
|
-
function printTable(rows, columns) {
|
|
248
|
-
const renderedRows = rows.map(
|
|
249
|
-
(row) => columns.map((column) => String(row[column.key] ?? ""))
|
|
250
|
-
);
|
|
251
|
-
const widths = columns.map(
|
|
252
|
-
(column, index) => Math.max(
|
|
253
|
-
column.label.length,
|
|
254
|
-
...renderedRows.map((row) => row[index]?.length ?? 0)
|
|
255
|
-
)
|
|
256
|
-
);
|
|
257
|
-
const header = columns.map((column, index) => column.label.padEnd(widths[index])).join(" ");
|
|
258
|
-
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
259
|
-
const body = renderedRows.map(
|
|
260
|
-
(row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ")
|
|
261
|
-
);
|
|
262
|
-
return `${[header, separator, ...body].join("\n")}
|
|
263
|
-
`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// src/commands/agents.ts
|
|
267
|
-
function buildTableOutput(agents) {
|
|
268
|
-
return printTable(
|
|
269
|
-
agents.map((agent) => ({
|
|
270
|
-
id: agent.id,
|
|
271
|
-
name: agent.stableName,
|
|
272
|
-
path: agent.absoluteSkillsDirectoryPath,
|
|
273
|
-
exists: String(agent.exists),
|
|
274
|
-
supported: String(agent.supportedOnPlatform),
|
|
275
|
-
discovery: agent.discovery
|
|
276
|
-
})),
|
|
277
|
-
[
|
|
278
|
-
{ key: "id", label: "Agent" },
|
|
279
|
-
{ key: "name", label: "Name" },
|
|
280
|
-
{ key: "path", label: "Path" },
|
|
281
|
-
{ key: "exists", label: "Exists" },
|
|
282
|
-
{ key: "supported", label: "Supported" },
|
|
283
|
-
{ key: "discovery", label: "Discovery" }
|
|
284
|
-
]
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
async function runAgents(options = {}) {
|
|
288
|
-
const homeDir = options.homeDir ?? homedir();
|
|
289
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
290
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
291
|
-
const agents = await discoverAgents({
|
|
292
|
-
homeDir,
|
|
293
|
-
skillmuxHome,
|
|
294
|
-
platform: options.platform
|
|
295
|
-
});
|
|
296
|
-
return {
|
|
297
|
-
agents,
|
|
298
|
-
output: options.json === true ? printJson(agents) : buildTableOutput(agents)
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// src/commands/config.ts
|
|
303
|
-
import { homedir as homedir2 } from "os";
|
|
304
|
-
function buildTableOutput2(result) {
|
|
305
|
-
const summary = printTable(
|
|
306
|
-
[
|
|
307
|
-
{
|
|
308
|
-
skillmuxHome: result.skillmuxHome,
|
|
309
|
-
configPath: result.configPath,
|
|
310
|
-
overrides: String(Object.keys(result.config.agents).length)
|
|
311
|
-
}
|
|
312
|
-
],
|
|
313
|
-
[
|
|
314
|
-
{ key: "skillmuxHome", label: "SkillMux Home" },
|
|
315
|
-
{ key: "configPath", label: "Config Path" },
|
|
316
|
-
{ key: "overrides", label: "Overrides" }
|
|
317
|
-
]
|
|
318
|
-
);
|
|
319
|
-
const agentRows = Object.entries(result.config.agents).sort(([left], [right]) => left.localeCompare(right)).map(([agentId, agent]) => ({
|
|
320
|
-
agentId,
|
|
321
|
-
stableName: agent.stableName ?? "",
|
|
322
|
-
root: agent.homeRelativeRootPath ?? "",
|
|
323
|
-
skills: agent.skillsDirectoryPath ?? ""
|
|
324
|
-
}));
|
|
325
|
-
if (agentRows.length === 0) {
|
|
326
|
-
return `${summary}
|
|
327
|
-
No user overrides configured.
|
|
328
|
-
`;
|
|
329
|
-
}
|
|
330
|
-
return `${summary}
|
|
331
|
-
${printTable(agentRows, [
|
|
332
|
-
{ key: "agentId", label: "Agent" },
|
|
333
|
-
{ key: "stableName", label: "Name" },
|
|
334
|
-
{ key: "root", label: "Root" },
|
|
335
|
-
{ key: "skills", label: "Skills Dir" }
|
|
336
|
-
])}`;
|
|
337
|
-
}
|
|
338
|
-
async function runConfig(options = {}) {
|
|
339
|
-
const homeDir = options.homeDir ?? homedir2();
|
|
340
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
341
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
342
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
343
|
-
const resultWithoutOutput = {
|
|
344
|
-
skillmuxHome,
|
|
345
|
-
configPath: resolvedPaths.configPath,
|
|
346
|
-
config
|
|
347
|
-
};
|
|
348
|
-
return {
|
|
349
|
-
...resultWithoutOutput,
|
|
350
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// src/commands/doctor.ts
|
|
355
|
-
import * as fs8 from "fs/promises";
|
|
356
|
-
import { homedir as homedir3 } from "os";
|
|
357
|
-
import { join as join5 } from "path";
|
|
358
|
-
|
|
359
266
|
// src/discovery/scan-agent-skills.ts
|
|
360
267
|
import * as fs5 from "fs/promises";
|
|
268
|
+
import { join as join3 } from "path";
|
|
361
269
|
|
|
362
270
|
// src/discovery/infer-skill-entry.ts
|
|
363
271
|
import * as fs4 from "fs/promises";
|
|
@@ -440,16 +348,10 @@ async function inferSkillEntry(options) {
|
|
|
440
348
|
agentId: options.agentId,
|
|
441
349
|
agentName: options.agentName,
|
|
442
350
|
skillName,
|
|
443
|
-
kind: "
|
|
351
|
+
kind: "unmanaged-link",
|
|
444
352
|
path: absolutePath,
|
|
445
353
|
targetPath
|
|
446
|
-
}
|
|
447
|
-
issue: buildIssue(
|
|
448
|
-
"unknown-entry",
|
|
449
|
-
"warning",
|
|
450
|
-
"Skill entry is a link that points outside the SkillMux managed store",
|
|
451
|
-
absolutePath
|
|
452
|
-
)
|
|
354
|
+
}
|
|
453
355
|
};
|
|
454
356
|
} catch (error) {
|
|
455
357
|
if (error.code !== "ENOENT") {
|
|
@@ -520,7 +422,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
520
422
|
const result = await inferSkillEntry({
|
|
521
423
|
agentId: agent.id,
|
|
522
424
|
agentName: agent.stableName,
|
|
523
|
-
path:
|
|
425
|
+
path: join3(agent.absoluteSkillsDirectoryPath, directoryEntry.name),
|
|
524
426
|
skillmuxHome
|
|
525
427
|
});
|
|
526
428
|
entries.push(result.entry);
|
|
@@ -534,39 +436,219 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
534
436
|
};
|
|
535
437
|
}
|
|
536
438
|
|
|
537
|
-
// src/
|
|
538
|
-
import * as
|
|
539
|
-
import { join as join4, resolve as resolve5 } from "path";
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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;
|
|
552
461
|
}
|
|
553
|
-
}
|
|
462
|
+
}
|
|
554
463
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
+
}
|
|
564
485
|
}
|
|
565
|
-
function
|
|
566
|
-
|
|
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
|
+
}
|
|
567
499
|
}
|
|
568
|
-
|
|
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";
|
|
634
|
+
|
|
635
|
+
// src/manifest/build-empty-manifest.ts
|
|
636
|
+
function buildEmptyManifest(skillmuxHome) {
|
|
637
|
+
return {
|
|
638
|
+
version: 1,
|
|
639
|
+
skillmuxHome,
|
|
640
|
+
skills: {},
|
|
641
|
+
agents: {},
|
|
642
|
+
activations: [],
|
|
643
|
+
lastScan: {
|
|
644
|
+
at: null,
|
|
645
|
+
issues: []
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
569
650
|
// src/manifest/manifest-schema.ts
|
|
651
|
+
import { z as z2 } from "zod";
|
|
570
652
|
var idSchema = z2.string().min(1).refine(isValidId, "Expected a canonical lowercase slug identifier");
|
|
571
653
|
var scanIssueSchema = z2.object({
|
|
572
654
|
code: z2.string().min(1),
|
|
@@ -673,35 +755,35 @@ var manifestSchema = z2.object({
|
|
|
673
755
|
|
|
674
756
|
// src/manifest/write-manifest.ts
|
|
675
757
|
import { randomUUID } from "crypto";
|
|
676
|
-
import * as
|
|
677
|
-
import { join as
|
|
758
|
+
import * as fs8 from "fs/promises";
|
|
759
|
+
import { join as join5 } from "path";
|
|
678
760
|
function getManifestPath(home) {
|
|
679
|
-
return
|
|
761
|
+
return join5(home, "manifest.json");
|
|
680
762
|
}
|
|
681
763
|
function createManifestTempPath(manifestPath) {
|
|
682
764
|
return `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
683
765
|
}
|
|
684
766
|
async function writeManifest(home, manifest) {
|
|
685
|
-
await
|
|
767
|
+
await fs8.mkdir(home, { recursive: true });
|
|
686
768
|
const manifestPath = getManifestPath(home);
|
|
687
769
|
const tempPath = createManifestTempPath(manifestPath);
|
|
688
770
|
const contents = `${JSON.stringify(manifest, null, 2)}
|
|
689
771
|
`;
|
|
690
|
-
await
|
|
772
|
+
await fs8.writeFile(tempPath, contents, "utf8");
|
|
691
773
|
try {
|
|
692
|
-
await
|
|
774
|
+
await fs8.rename(tempPath, manifestPath);
|
|
693
775
|
} catch (error) {
|
|
694
|
-
await
|
|
776
|
+
await fs8.unlink(tempPath).catch(() => void 0);
|
|
695
777
|
throw error;
|
|
696
778
|
}
|
|
697
779
|
}
|
|
698
780
|
|
|
699
781
|
// src/manifest/read-manifest.ts
|
|
700
782
|
function getManifestPath2(home) {
|
|
701
|
-
return
|
|
783
|
+
return join6(home, "manifest.json");
|
|
702
784
|
}
|
|
703
785
|
function normalizeHomePath(home) {
|
|
704
|
-
const resolvedHome =
|
|
786
|
+
const resolvedHome = resolve7(home);
|
|
705
787
|
return process.platform === "win32" ? resolvedHome.toLowerCase() : resolvedHome;
|
|
706
788
|
}
|
|
707
789
|
function formatValidationIssues2(error) {
|
|
@@ -713,7 +795,7 @@ function formatValidationIssues2(error) {
|
|
|
713
795
|
async function readManifest(home) {
|
|
714
796
|
const manifestPath = getManifestPath2(home);
|
|
715
797
|
try {
|
|
716
|
-
const contents = await
|
|
798
|
+
const contents = await fs9.readFile(manifestPath, "utf8");
|
|
717
799
|
const parsedJson = JSON.parse(contents);
|
|
718
800
|
const parsedManifest = manifestSchema.safeParse(parsedJson);
|
|
719
801
|
if (!parsedManifest.success) {
|
|
@@ -742,191 +824,690 @@ async function readManifest(home) {
|
|
|
742
824
|
}
|
|
743
825
|
}
|
|
744
826
|
|
|
745
|
-
// src/
|
|
746
|
-
function
|
|
747
|
-
return
|
|
827
|
+
// src/output/print-json.ts
|
|
828
|
+
function printJson(value) {
|
|
829
|
+
return `${JSON.stringify(value, null, 2)}
|
|
830
|
+
`;
|
|
748
831
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
} catch (error) {
|
|
754
|
-
if (error.code === "ENOENT") {
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
throw error;
|
|
758
|
-
}
|
|
832
|
+
|
|
833
|
+
// src/commands/adopt.ts
|
|
834
|
+
function buildManagedSkillPath(skillmuxHome, skillId) {
|
|
835
|
+
return resolve8(skillmuxHome, "skills", skillId);
|
|
759
836
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
"warning",
|
|
770
|
-
`Unmanaged skill directory is present for ${entry.agentId}/${entry.skillName}`,
|
|
771
|
-
entry.path
|
|
772
|
-
)
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
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
|
+
};
|
|
776
846
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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;
|
|
790
863
|
}
|
|
864
|
+
manifest.activations[index] = activation;
|
|
791
865
|
}
|
|
792
|
-
function
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
pathToAgents.set(key, current);
|
|
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}`);
|
|
799
872
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
const agentIds = conflictedAgents.map((agent) => agent.id).sort((left, right) => left.localeCompare(right));
|
|
805
|
-
issues.push(
|
|
806
|
-
buildIssue2(
|
|
807
|
-
"conflicting-agent-path",
|
|
808
|
-
"warning",
|
|
809
|
-
`Multiple agents resolve to the same skills directory: ${agentIds.join(", ")}`,
|
|
810
|
-
conflictedAgents[0].absoluteSkillsDirectoryPath
|
|
811
|
-
)
|
|
873
|
+
if (!agent.supportedOnPlatform) {
|
|
874
|
+
throw new AdoptionError(
|
|
875
|
+
`Agent ${agent.id} is not supported on ${process.platform}`
|
|
812
876
|
);
|
|
813
877
|
}
|
|
878
|
+
return agent;
|
|
814
879
|
}
|
|
815
|
-
function
|
|
816
|
-
if (
|
|
817
|
-
return
|
|
880
|
+
function filterEntries(scannedAgent, skillFilter) {
|
|
881
|
+
if (skillFilter === void 0) {
|
|
882
|
+
return scannedAgent.entries;
|
|
818
883
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
code: issue.code,
|
|
823
|
-
path: issue.path ?? "",
|
|
824
|
-
message: issue.message
|
|
825
|
-
})),
|
|
826
|
-
[
|
|
827
|
-
{ key: "severity", label: "Severity" },
|
|
828
|
-
{ key: "code", label: "Code" },
|
|
829
|
-
{ key: "path", label: "Path" },
|
|
830
|
-
{ key: "message", label: "Message" }
|
|
831
|
-
]
|
|
884
|
+
const skillId = normalizeId(skillFilter);
|
|
885
|
+
return scannedAgent.entries.filter(
|
|
886
|
+
(entry) => normalizeId(entry.skillName) === skillId
|
|
832
887
|
);
|
|
833
888
|
}
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
})),
|
|
843
|
-
entries: result.entries
|
|
844
|
-
});
|
|
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;
|
|
845
897
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
for (const agent of agents) {
|
|
862
|
-
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
863
|
-
entries.push(...scannedAgent.entries);
|
|
864
|
-
issues.push(...scannedAgent.issues);
|
|
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}`);
|
|
865
913
|
}
|
|
866
|
-
await
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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);
|
|
874
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,
|
|
875
1140
|
skillmuxHome,
|
|
876
|
-
|
|
877
|
-
|
|
1141
|
+
platform: options.platform
|
|
1142
|
+
});
|
|
1143
|
+
return {
|
|
878
1144
|
agents,
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
881
1279
|
};
|
|
882
1280
|
return {
|
|
883
1281
|
...resultWithoutOutput,
|
|
884
|
-
output: options.json === true ?
|
|
1282
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
885
1283
|
};
|
|
886
1284
|
}
|
|
887
1285
|
|
|
888
|
-
// src/commands/
|
|
889
|
-
import * as fs11 from "fs/promises";
|
|
1286
|
+
// src/commands/config-remove-agent.ts
|
|
890
1287
|
import { homedir as homedir4 } from "os";
|
|
891
|
-
|
|
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
|
+
}
|
|
892
1343
|
|
|
893
|
-
// src/
|
|
894
|
-
import
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
|
918
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
|
+
`;
|
|
919
1475
|
}
|
|
920
|
-
|
|
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
|
+
])}`;
|
|
921
1483
|
}
|
|
922
|
-
async function
|
|
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
|
+
|
|
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";
|
|
1504
|
+
function buildIssue2(code, severity, message, path) {
|
|
1505
|
+
return path === void 0 ? { code, severity, message } : { code, severity, message, path };
|
|
1506
|
+
}
|
|
1507
|
+
async function pathExists2(path) {
|
|
923
1508
|
try {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
return false;
|
|
927
|
-
}
|
|
928
|
-
const resolvedTargetPath = await fs9.realpath(linkPath);
|
|
929
|
-
return pathsAreEqual(resolvedTargetPath, targetPath);
|
|
1509
|
+
await fs11.access(path);
|
|
1510
|
+
return true;
|
|
930
1511
|
} catch (error) {
|
|
931
1512
|
if (error.code === "ENOENT") {
|
|
932
1513
|
return false;
|
|
@@ -934,16 +1515,148 @@ async function isLinkPointingToTarget(linkPath, targetPath) {
|
|
|
934
1515
|
throw error;
|
|
935
1516
|
}
|
|
936
1517
|
}
|
|
1518
|
+
async function addUnmanagedDirectoryIssues(entries, issues) {
|
|
1519
|
+
for (const entry of entries) {
|
|
1520
|
+
if (entry.kind !== "unmanaged-directory") {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (await pathExists2(join8(entry.path, "SKILL.md"))) {
|
|
1524
|
+
issues.push(
|
|
1525
|
+
buildIssue2(
|
|
1526
|
+
"unmanaged-skill-directory",
|
|
1527
|
+
"warning",
|
|
1528
|
+
`Unmanaged skill directory is present for ${entry.agentId}/${entry.skillName}`,
|
|
1529
|
+
entry.path
|
|
1530
|
+
)
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async function addMissingManagedSkillIssues(manifest, issues) {
|
|
1536
|
+
for (const skill of Object.values(manifest.skills)) {
|
|
1537
|
+
if (await pathExists2(skill.path)) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
issues.push(
|
|
1541
|
+
buildIssue2(
|
|
1542
|
+
"missing-managed-skill-path",
|
|
1543
|
+
"error",
|
|
1544
|
+
`Managed skill path is missing for ${skill.id}`,
|
|
1545
|
+
skill.path
|
|
1546
|
+
)
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function addConflictingAgentPathIssues(agents, issues) {
|
|
1551
|
+
const pathToAgents = /* @__PURE__ */ new Map();
|
|
1552
|
+
for (const agent of agents) {
|
|
1553
|
+
const key = normalizeAbsolutePath(agent.absoluteSkillsDirectoryPath);
|
|
1554
|
+
const current = pathToAgents.get(key) ?? [];
|
|
1555
|
+
current.push(agent);
|
|
1556
|
+
pathToAgents.set(key, current);
|
|
1557
|
+
}
|
|
1558
|
+
for (const conflictedAgents of pathToAgents.values()) {
|
|
1559
|
+
if (conflictedAgents.length < 2) {
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const agentIds = conflictedAgents.map((agent) => agent.id).sort((left, right) => left.localeCompare(right));
|
|
1563
|
+
issues.push(
|
|
1564
|
+
buildIssue2(
|
|
1565
|
+
"conflicting-agent-path",
|
|
1566
|
+
"warning",
|
|
1567
|
+
`Multiple agents resolve to the same skills directory: ${agentIds.join(", ")}`,
|
|
1568
|
+
conflictedAgents[0].absoluteSkillsDirectoryPath
|
|
1569
|
+
)
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function buildTableOutput6(issues) {
|
|
1574
|
+
if (issues.length === 0) {
|
|
1575
|
+
return "No doctor issues found.\n";
|
|
1576
|
+
}
|
|
1577
|
+
return printTable(
|
|
1578
|
+
issues.map((issue) => ({
|
|
1579
|
+
severity: issue.severity,
|
|
1580
|
+
code: issue.code,
|
|
1581
|
+
path: issue.path ?? "",
|
|
1582
|
+
message: issue.message
|
|
1583
|
+
})),
|
|
1584
|
+
[
|
|
1585
|
+
{ key: "severity", label: "Severity" },
|
|
1586
|
+
{ key: "code", label: "Code" },
|
|
1587
|
+
{ key: "path", label: "Path" },
|
|
1588
|
+
{ key: "message", label: "Message" }
|
|
1589
|
+
]
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
function buildJsonOutput(result) {
|
|
1593
|
+
return printJson({
|
|
1594
|
+
skillmuxHome: result.skillmuxHome,
|
|
1595
|
+
issues: result.issues,
|
|
1596
|
+
agents: result.agents.map((agent) => ({
|
|
1597
|
+
id: agent.id,
|
|
1598
|
+
path: agent.absoluteSkillsDirectoryPath,
|
|
1599
|
+
supportedOnPlatform: agent.supportedOnPlatform
|
|
1600
|
+
})),
|
|
1601
|
+
entries: result.entries
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
async function runDoctor(options = {}) {
|
|
1605
|
+
const homeDir = options.homeDir ?? homedir7();
|
|
1606
|
+
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1607
|
+
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1608
|
+
const [manifest, config, agents] = await Promise.all([
|
|
1609
|
+
readManifest(skillmuxHome),
|
|
1610
|
+
loadUserConfig(skillmuxHome),
|
|
1611
|
+
discoverAgents({
|
|
1612
|
+
homeDir,
|
|
1613
|
+
skillmuxHome,
|
|
1614
|
+
platform: options.platform
|
|
1615
|
+
})
|
|
1616
|
+
]);
|
|
1617
|
+
const entries = [];
|
|
1618
|
+
const issues = [];
|
|
1619
|
+
for (const agent of agents) {
|
|
1620
|
+
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
1621
|
+
entries.push(...scannedAgent.entries);
|
|
1622
|
+
issues.push(...scannedAgent.issues);
|
|
1623
|
+
}
|
|
1624
|
+
await addUnmanagedDirectoryIssues(entries, issues);
|
|
1625
|
+
await addMissingManagedSkillIssues(manifest, issues);
|
|
1626
|
+
addConflictingAgentPathIssues(agents, issues);
|
|
1627
|
+
const dedupedIssues = [...issues].sort((left, right) => {
|
|
1628
|
+
const leftKey = `${left.severity}:${left.code}:${left.path ?? ""}:${left.message}`;
|
|
1629
|
+
const rightKey = `${right.severity}:${right.code}:${right.path ?? ""}:${right.message}`;
|
|
1630
|
+
return leftKey.localeCompare(rightKey);
|
|
1631
|
+
});
|
|
1632
|
+
const resultWithoutOutput = {
|
|
1633
|
+
skillmuxHome,
|
|
1634
|
+
manifest,
|
|
1635
|
+
config,
|
|
1636
|
+
agents,
|
|
1637
|
+
entries,
|
|
1638
|
+
issues: dedupedIssues
|
|
1639
|
+
};
|
|
1640
|
+
return {
|
|
1641
|
+
...resultWithoutOutput,
|
|
1642
|
+
output: options.json === true ? buildJsonOutput(resultWithoutOutput) : buildTableOutput6(dedupedIssues)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/commands/disable.ts
|
|
1647
|
+
import * as fs13 from "fs/promises";
|
|
1648
|
+
import { homedir as homedir8 } from "os";
|
|
1649
|
+
import { join as join9, resolve as resolve9 } from "path";
|
|
937
1650
|
|
|
938
1651
|
// src/fs/safe-remove-link.ts
|
|
939
|
-
import * as
|
|
1652
|
+
import * as fs12 from "fs/promises";
|
|
940
1653
|
async function safeRemoveLink(path) {
|
|
941
1654
|
try {
|
|
942
|
-
const entry = await
|
|
1655
|
+
const entry = await fs12.lstat(path);
|
|
943
1656
|
if (!entry.isSymbolicLink()) {
|
|
944
1657
|
return false;
|
|
945
1658
|
}
|
|
946
|
-
await
|
|
1659
|
+
await fs12.rm(path, { recursive: true, force: false });
|
|
947
1660
|
return true;
|
|
948
1661
|
} catch (error) {
|
|
949
1662
|
if (error.code === "ENOENT") {
|
|
@@ -954,7 +1667,7 @@ async function safeRemoveLink(path) {
|
|
|
954
1667
|
}
|
|
955
1668
|
|
|
956
1669
|
// src/commands/disable.ts
|
|
957
|
-
function
|
|
1670
|
+
function buildAgentRecord2(agent, timestamp) {
|
|
958
1671
|
return {
|
|
959
1672
|
id: agent.id,
|
|
960
1673
|
name: agent.stableName,
|
|
@@ -964,7 +1677,7 @@ function buildAgentRecord(agent, timestamp) {
|
|
|
964
1677
|
lastSeenAt: agent.exists ? timestamp : null
|
|
965
1678
|
};
|
|
966
1679
|
}
|
|
967
|
-
function
|
|
1680
|
+
function buildActivationRecord2(skillId, agentId, linkPath, timestamp) {
|
|
968
1681
|
return {
|
|
969
1682
|
skillId,
|
|
970
1683
|
agentId,
|
|
@@ -973,7 +1686,7 @@ function buildActivationRecord(skillId, agentId, linkPath, timestamp) {
|
|
|
973
1686
|
updatedAt: timestamp
|
|
974
1687
|
};
|
|
975
1688
|
}
|
|
976
|
-
function
|
|
1689
|
+
function upsertActivation2(manifest, activation) {
|
|
977
1690
|
const index = manifest.activations.findIndex(
|
|
978
1691
|
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
979
1692
|
);
|
|
@@ -983,7 +1696,39 @@ function upsertActivation(manifest, activation) {
|
|
|
983
1696
|
}
|
|
984
1697
|
manifest.activations[index] = activation;
|
|
985
1698
|
}
|
|
986
|
-
|
|
1699
|
+
function buildManagedSkillPath2(skillmuxHome, skillId) {
|
|
1700
|
+
return resolve9(skillmuxHome, "skills", skillId);
|
|
1701
|
+
}
|
|
1702
|
+
async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName, linkPath, timestamp) {
|
|
1703
|
+
try {
|
|
1704
|
+
const entry = await fs13.lstat(linkPath);
|
|
1705
|
+
if (!entry.isSymbolicLink()) {
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
if (error.code === "ENOENT") {
|
|
1710
|
+
return void 0;
|
|
1711
|
+
}
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
const sourcePath = await fs13.realpath(linkPath);
|
|
1715
|
+
await assertSkillSourceLayout(sourcePath);
|
|
1716
|
+
const managedSkillPath = buildManagedSkillPath2(skillmuxHome, skillId);
|
|
1717
|
+
await copySkillContentsToManagedStore(sourcePath, managedSkillPath);
|
|
1718
|
+
const skill = {
|
|
1719
|
+
id: skillId,
|
|
1720
|
+
name: skillName,
|
|
1721
|
+
path: managedSkillPath,
|
|
1722
|
+
source: {
|
|
1723
|
+
kind: "imported",
|
|
1724
|
+
path: sourcePath
|
|
1725
|
+
},
|
|
1726
|
+
importedAt: timestamp
|
|
1727
|
+
};
|
|
1728
|
+
manifest.skills[skillId] = skill;
|
|
1729
|
+
return { skill, sourcePath };
|
|
1730
|
+
}
|
|
1731
|
+
async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
987
1732
|
const agentId = normalizeId(agentName);
|
|
988
1733
|
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
989
1734
|
const agent = agents.find((entry) => entry.id === agentId);
|
|
@@ -997,7 +1742,7 @@ async function resolveTargetAgent(homeDir, skillmuxHome, agentName) {
|
|
|
997
1742
|
}
|
|
998
1743
|
async function pathExists3(path) {
|
|
999
1744
|
try {
|
|
1000
|
-
await
|
|
1745
|
+
await fs13.lstat(path);
|
|
1001
1746
|
return true;
|
|
1002
1747
|
} catch (error) {
|
|
1003
1748
|
if (error.code === "ENOENT") {
|
|
@@ -1006,29 +1751,42 @@ async function pathExists3(path) {
|
|
|
1006
1751
|
throw error;
|
|
1007
1752
|
}
|
|
1008
1753
|
}
|
|
1009
|
-
async function
|
|
1010
|
-
|
|
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();
|
|
1011
1759
|
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1012
1760
|
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1013
1761
|
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1014
1762
|
const manifest = await readManifest(skillmuxHome);
|
|
1015
1763
|
const skillId = normalizeId(options.skill);
|
|
1016
|
-
const
|
|
1764
|
+
const agent = await resolveTargetAgent2(homeDir, skillmuxHome, options.agent);
|
|
1765
|
+
const linkPath = join9(agent.absoluteSkillsDirectoryPath, skillId);
|
|
1766
|
+
const adoption = manifest.skills[skillId] ? void 0 : await tryAdoptManagedSkill(
|
|
1767
|
+
manifest,
|
|
1768
|
+
skillmuxHome,
|
|
1769
|
+
skillId,
|
|
1770
|
+
options.skill,
|
|
1771
|
+
linkPath,
|
|
1772
|
+
timestamp
|
|
1773
|
+
);
|
|
1774
|
+
const skill = manifest.skills[skillId] ?? adoption?.skill;
|
|
1017
1775
|
if (skill === void 0) {
|
|
1018
1776
|
throw new Error(`Managed skill not found: ${skillId}`);
|
|
1019
1777
|
}
|
|
1020
|
-
const agent = await resolveTargetAgent(homeDir, skillmuxHome, options.agent);
|
|
1021
1778
|
const currentActivation = manifest.activations.find(
|
|
1022
1779
|
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1023
1780
|
);
|
|
1024
|
-
const
|
|
1025
|
-
const agentRecord =
|
|
1781
|
+
const activationLinkPath = currentActivation?.linkPath ?? linkPath;
|
|
1782
|
+
const agentRecord = buildAgentRecord2(agent, timestamp);
|
|
1026
1783
|
manifest.agents[agent.id] = agentRecord;
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1784
|
+
const adoptedLinkRemoved = adoption !== void 0 ? await safeRemoveLink(linkPath) : false;
|
|
1785
|
+
const linkMatchesSkill = adoption === void 0 ? await isLinkPointingToTarget(activationLinkPath, skill.path) : false;
|
|
1786
|
+
if (adoption === void 0 && !linkMatchesSkill && await pathExists3(activationLinkPath)) {
|
|
1029
1787
|
throw new Error(`Refusing to disable non-managed entry at ${linkPath}`);
|
|
1030
1788
|
}
|
|
1031
|
-
const removedLink = linkMatchesSkill ? await safeRemoveLink(
|
|
1789
|
+
const removedLink = adoptedLinkRemoved ? true : linkMatchesSkill ? await safeRemoveLink(activationLinkPath) : false;
|
|
1032
1790
|
if (removedLink === false && currentActivation?.state !== "enabled") {
|
|
1033
1791
|
return {
|
|
1034
1792
|
changed: false,
|
|
@@ -1040,8 +1798,8 @@ async function runDisable(options) {
|
|
|
1040
1798
|
`
|
|
1041
1799
|
};
|
|
1042
1800
|
}
|
|
1043
|
-
const activation =
|
|
1044
|
-
|
|
1801
|
+
const activation = buildActivationRecord2(skill.id, agent.id, linkPath, timestamp);
|
|
1802
|
+
upsertActivation2(manifest, activation);
|
|
1045
1803
|
await writeManifest(skillmuxHome, manifest);
|
|
1046
1804
|
return {
|
|
1047
1805
|
changed: true,
|
|
@@ -1053,12 +1811,44 @@ async function runDisable(options) {
|
|
|
1053
1811
|
`
|
|
1054
1812
|
};
|
|
1055
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
|
+
}
|
|
1056
1846
|
|
|
1057
1847
|
// src/commands/enable.ts
|
|
1058
|
-
import * as
|
|
1059
|
-
import { homedir as
|
|
1060
|
-
import { join as
|
|
1061
|
-
function
|
|
1848
|
+
import * as fs14 from "fs/promises";
|
|
1849
|
+
import { homedir as homedir9 } from "os";
|
|
1850
|
+
import { join as join10 } from "path";
|
|
1851
|
+
function buildAgentRecord3(agent, timestamp) {
|
|
1062
1852
|
return {
|
|
1063
1853
|
id: agent.id,
|
|
1064
1854
|
name: agent.stableName,
|
|
@@ -1068,7 +1858,7 @@ function buildAgentRecord2(agent, timestamp) {
|
|
|
1068
1858
|
lastSeenAt: timestamp
|
|
1069
1859
|
};
|
|
1070
1860
|
}
|
|
1071
|
-
function
|
|
1861
|
+
function buildActivationRecord3(skillId, agentId, linkPath, timestamp, state) {
|
|
1072
1862
|
return {
|
|
1073
1863
|
skillId,
|
|
1074
1864
|
agentId,
|
|
@@ -1077,7 +1867,7 @@ function buildActivationRecord2(skillId, agentId, linkPath, timestamp, state) {
|
|
|
1077
1867
|
updatedAt: timestamp
|
|
1078
1868
|
};
|
|
1079
1869
|
}
|
|
1080
|
-
function
|
|
1870
|
+
function upsertActivation3(manifest, activation) {
|
|
1081
1871
|
const index = manifest.activations.findIndex(
|
|
1082
1872
|
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
1083
1873
|
);
|
|
@@ -1087,7 +1877,7 @@ function upsertActivation2(manifest, activation) {
|
|
|
1087
1877
|
}
|
|
1088
1878
|
manifest.activations[index] = activation;
|
|
1089
1879
|
}
|
|
1090
|
-
async function
|
|
1880
|
+
async function resolveTargetAgent3(homeDir, skillmuxHome, agentName) {
|
|
1091
1881
|
const agentId = normalizeId(agentName);
|
|
1092
1882
|
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
1093
1883
|
const agent = agents.find((entry) => entry.id === agentId);
|
|
@@ -1099,8 +1889,11 @@ async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
|
1099
1889
|
}
|
|
1100
1890
|
return agent;
|
|
1101
1891
|
}
|
|
1102
|
-
async function
|
|
1103
|
-
|
|
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();
|
|
1104
1897
|
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1105
1898
|
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1106
1899
|
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1110,14 +1903,14 @@ async function runEnable(options) {
|
|
|
1110
1903
|
if (skill === void 0) {
|
|
1111
1904
|
throw new Error(`Managed skill not found: ${skillId}`);
|
|
1112
1905
|
}
|
|
1113
|
-
const agent = await
|
|
1114
|
-
const linkPath =
|
|
1906
|
+
const agent = await resolveTargetAgent3(homeDir, skillmuxHome, options.agent);
|
|
1907
|
+
const linkPath = join10(agent.absoluteSkillsDirectoryPath, skill.id);
|
|
1115
1908
|
const currentActivation = manifest.activations.find(
|
|
1116
1909
|
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1117
1910
|
);
|
|
1118
|
-
const agentRecord =
|
|
1911
|
+
const agentRecord = buildAgentRecord3(agent, timestamp);
|
|
1119
1912
|
manifest.agents[agent.id] = agentRecord;
|
|
1120
|
-
await
|
|
1913
|
+
await fs14.mkdir(agent.absoluteSkillsDirectoryPath, { recursive: true });
|
|
1121
1914
|
const linkAlreadyEnabled = await isLinkPointingToTarget(linkPath, skill.path);
|
|
1122
1915
|
const activationAlreadyEnabled = currentActivation?.state === "enabled" && currentActivation.linkPath === linkPath;
|
|
1123
1916
|
if (linkAlreadyEnabled && activationAlreadyEnabled) {
|
|
@@ -1132,14 +1925,14 @@ async function runEnable(options) {
|
|
|
1132
1925
|
};
|
|
1133
1926
|
}
|
|
1134
1927
|
await createManagedLink(linkPath, skill.path);
|
|
1135
|
-
const activation =
|
|
1928
|
+
const activation = buildActivationRecord3(
|
|
1136
1929
|
skill.id,
|
|
1137
1930
|
agent.id,
|
|
1138
1931
|
linkPath,
|
|
1139
1932
|
timestamp,
|
|
1140
1933
|
"enabled"
|
|
1141
1934
|
);
|
|
1142
|
-
|
|
1935
|
+
upsertActivation3(manifest, activation);
|
|
1143
1936
|
await writeManifest(skillmuxHome, manifest);
|
|
1144
1937
|
return {
|
|
1145
1938
|
changed: true,
|
|
@@ -1151,100 +1944,54 @@ async function runEnable(options) {
|
|
|
1151
1944
|
`
|
|
1152
1945
|
};
|
|
1153
1946
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
if (!entry.isFile()) {
|
|
1171
|
-
throw new Error(`Expected ${label} to be a regular file at ${path}`);
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
async function assertTargetDoesNotExist(path) {
|
|
1175
|
-
try {
|
|
1176
|
-
await fs13.lstat(path);
|
|
1177
|
-
throw new Error(`Refusing to overwrite existing path at ${path}`);
|
|
1178
|
-
} catch (error) {
|
|
1179
|
-
if (error.code !== "ENOENT") {
|
|
1180
|
-
throw error;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
async function copyDirectoryContents(sourcePath, targetPath) {
|
|
1185
|
-
await fs13.mkdir(targetPath, { recursive: true });
|
|
1186
|
-
const entries = await fs13.readdir(sourcePath, { withFileTypes: true });
|
|
1187
|
-
for (const entry of entries) {
|
|
1188
|
-
const sourceEntryPath = join8(sourcePath, entry.name);
|
|
1189
|
-
const targetEntryPath = join8(targetPath, entry.name);
|
|
1190
|
-
const entryStats = await fs13.lstat(sourceEntryPath);
|
|
1191
|
-
if (entryStats.isSymbolicLink()) {
|
|
1192
|
-
throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
|
|
1193
|
-
}
|
|
1194
|
-
if (entryStats.isDirectory()) {
|
|
1195
|
-
await copyDirectoryContents(sourceEntryPath, targetEntryPath);
|
|
1196
|
-
continue;
|
|
1197
|
-
}
|
|
1198
|
-
if (entryStats.isFile()) {
|
|
1199
|
-
await fs13.mkdir(dirname3(targetEntryPath), { recursive: true });
|
|
1200
|
-
await fs13.copyFile(sourceEntryPath, targetEntryPath);
|
|
1201
|
-
continue;
|
|
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
|
+
}
|
|
1202
1963
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
async function assertSkillSourceLayout(sourcePath) {
|
|
1207
|
-
const resolvedSourcePath = resolve7(sourcePath);
|
|
1208
|
-
const skillFilePath = join8(resolvedSourcePath, "SKILL.md");
|
|
1209
|
-
await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
|
|
1210
|
-
await assertDirectory(resolvedSourcePath);
|
|
1211
|
-
try {
|
|
1212
|
-
await assertRegularFile(skillFilePath, "SKILL.md");
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
if (error.code === "ENOENT") {
|
|
1215
|
-
throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
|
|
1964
|
+
if (results.length === 0) {
|
|
1965
|
+
throw new Error("Enable requires at least one target agent");
|
|
1216
1966
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
|
|
1227
|
-
throw new Error("Refusing to copy into a child of the source directory");
|
|
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
|
+
};
|
|
1228
1976
|
}
|
|
1229
|
-
|
|
1230
|
-
await assertNoSymlinkAncestors(resolvedTargetPath);
|
|
1231
|
-
await assertTargetDoesNotExist(resolvedTargetPath);
|
|
1232
|
-
await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
|
|
1977
|
+
return runEnableSingle(options);
|
|
1233
1978
|
}
|
|
1234
1979
|
|
|
1235
1980
|
// src/commands/import.ts
|
|
1236
|
-
|
|
1237
|
-
|
|
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);
|
|
1238
1985
|
}
|
|
1239
1986
|
async function runImport(options) {
|
|
1240
|
-
const homeDir = options.homeDir ??
|
|
1987
|
+
const homeDir = options.homeDir ?? homedir10();
|
|
1241
1988
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1242
1989
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1243
|
-
const sourcePath =
|
|
1990
|
+
const sourcePath = resolve10(options.sourcePath);
|
|
1244
1991
|
const skillId = normalizeId(options.skillName);
|
|
1245
1992
|
const importedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1246
1993
|
const manifest = await readManifest(skillmuxHome);
|
|
1247
|
-
const managedSkillPath =
|
|
1994
|
+
const managedSkillPath = buildManagedSkillPath3(skillmuxHome, skillId);
|
|
1248
1995
|
await assertSkillSourceLayout(sourcePath);
|
|
1249
1996
|
if (manifest.skills[skillId] !== void 0) {
|
|
1250
1997
|
throw new Error(`Managed skill already exists for ${skillId}`);
|
|
@@ -1271,7 +2018,7 @@ async function runImport(options) {
|
|
|
1271
2018
|
}
|
|
1272
2019
|
|
|
1273
2020
|
// src/commands/scan.ts
|
|
1274
|
-
import { homedir as
|
|
2021
|
+
import { homedir as homedir11 } from "os";
|
|
1275
2022
|
|
|
1276
2023
|
// src/output/format-issue.ts
|
|
1277
2024
|
function formatIssue(issue) {
|
|
@@ -1282,7 +2029,7 @@ function formatIssue(issue) {
|
|
|
1282
2029
|
}
|
|
1283
2030
|
|
|
1284
2031
|
// src/commands/scan.ts
|
|
1285
|
-
function
|
|
2032
|
+
function buildAgentRecord4(agent, timestamp, previousRecord) {
|
|
1286
2033
|
const lastSeenAt = agent.exists ? timestamp : previousRecord?.lastSeenAt ?? null;
|
|
1287
2034
|
return {
|
|
1288
2035
|
id: agent.id,
|
|
@@ -1332,7 +2079,7 @@ ${result.issues.map(formatIssue).join("\n")}
|
|
|
1332
2079
|
`;
|
|
1333
2080
|
}
|
|
1334
2081
|
async function runScan(options = {}) {
|
|
1335
|
-
const homeDir = options.homeDir ??
|
|
2082
|
+
const homeDir = options.homeDir ?? homedir11();
|
|
1336
2083
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1337
2084
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1338
2085
|
const manifest = await readManifest(skillmuxHome);
|
|
@@ -1348,7 +2095,7 @@ async function runScan(options = {}) {
|
|
|
1348
2095
|
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
1349
2096
|
entries.push(...scannedAgent.entries);
|
|
1350
2097
|
issues.push(...scannedAgent.issues);
|
|
1351
|
-
manifest.agents[agent.id] =
|
|
2098
|
+
manifest.agents[agent.id] = buildAgentRecord4(
|
|
1352
2099
|
agent,
|
|
1353
2100
|
timestamp,
|
|
1354
2101
|
manifest.agents[agent.id]
|
|
@@ -1381,6 +2128,13 @@ function buildRecordsView(scanResult) {
|
|
|
1381
2128
|
}
|
|
1382
2129
|
function buildAgentsView(scanResult) {
|
|
1383
2130
|
const groups = /* @__PURE__ */ new Map();
|
|
2131
|
+
for (const agent of scanResult.agents) {
|
|
2132
|
+
groups.set(agent.id, {
|
|
2133
|
+
agentId: agent.id,
|
|
2134
|
+
agentName: agent.stableName,
|
|
2135
|
+
entries: []
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
1384
2138
|
for (const entry of scanResult.entries) {
|
|
1385
2139
|
const current = groups.get(entry.agentId) ?? {
|
|
1386
2140
|
agentId: entry.agentId,
|
|
@@ -1400,6 +2154,12 @@ function buildAgentsView(scanResult) {
|
|
|
1400
2154
|
}
|
|
1401
2155
|
function buildSkillsView(scanResult) {
|
|
1402
2156
|
const groups = /* @__PURE__ */ new Map();
|
|
2157
|
+
for (const skill of Object.values(scanResult.manifest.skills)) {
|
|
2158
|
+
groups.set(skill.id, {
|
|
2159
|
+
skillName: skill.id,
|
|
2160
|
+
entries: []
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
1403
2163
|
for (const entry of scanResult.entries) {
|
|
1404
2164
|
const current = groups.get(entry.skillName) ?? {
|
|
1405
2165
|
skillName: entry.skillName,
|
|
@@ -1425,7 +2185,7 @@ function buildListData(scanResult, view) {
|
|
|
1425
2185
|
}
|
|
1426
2186
|
return buildRecordsView(scanResult);
|
|
1427
2187
|
}
|
|
1428
|
-
function
|
|
2188
|
+
function buildTableOutput7(data, view) {
|
|
1429
2189
|
if (view === "agents") {
|
|
1430
2190
|
const agentRows = data.agents;
|
|
1431
2191
|
return printTable(
|
|
@@ -1475,14 +2235,204 @@ async function runList(options = {}) {
|
|
|
1475
2235
|
const data = buildListData(scanResult, view);
|
|
1476
2236
|
return {
|
|
1477
2237
|
data,
|
|
1478
|
-
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)
|
|
1479
2365
|
};
|
|
1480
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
|
+
}
|
|
1481
2401
|
|
|
1482
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
|
+
}
|
|
1483
2418
|
function buildCli() {
|
|
1484
2419
|
const program = new Command();
|
|
1485
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
|
+
});
|
|
1486
2436
|
program.command("scan").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1487
2437
|
const result = await runScan({ json: options.json === true });
|
|
1488
2438
|
process.stdout.write(result.output);
|
|
@@ -1509,21 +2459,79 @@ function buildCli() {
|
|
|
1509
2459
|
const result = await runDoctor({ json: options.json === true });
|
|
1510
2460
|
process.stdout.write(result.output);
|
|
1511
2461
|
});
|
|
1512
|
-
program.command("config")
|
|
2462
|
+
const configCommand = program.command("config");
|
|
2463
|
+
configCommand.option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1513
2464
|
const result = await runConfig({ json: options.json === true });
|
|
1514
2465
|
process.stdout.write(result.output);
|
|
1515
2466
|
});
|
|
1516
|
-
|
|
2467
|
+
configCommand.command("add-agent").requiredOption("--id <id>", "Agent id").requiredOption("--root <path>", "Home-relative root path").option("--skills <path>", "Skills directory path relative to the agent root", "skills").option("--name <name>", "Stable display name").option(
|
|
2468
|
+
"--platform <platform>",
|
|
2469
|
+
`Supported platform (${supportedPlatforms.join(", ")})`,
|
|
2470
|
+
(value, previous = []) => [...previous, value],
|
|
2471
|
+
[]
|
|
2472
|
+
).option("--disabled-by-default", "Mark this custom agent as disabled by default").option("--json", "Emit structured JSON output").action(
|
|
2473
|
+
async (options) => {
|
|
2474
|
+
const result = await runConfigAddAgent({
|
|
2475
|
+
id: options.id,
|
|
2476
|
+
root: options.root,
|
|
2477
|
+
skills: options.skills,
|
|
2478
|
+
name: options.name,
|
|
2479
|
+
platforms: options.platform,
|
|
2480
|
+
disabledByDefault: options.disabledByDefault === true,
|
|
2481
|
+
json: options.json === true
|
|
2482
|
+
});
|
|
2483
|
+
process.stdout.write(result.output);
|
|
2484
|
+
}
|
|
2485
|
+
);
|
|
2486
|
+
configCommand.command("remove-agent").requiredOption("--id <id>", "Agent id").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
2487
|
+
const result = await runConfigRemoveAgent({
|
|
2488
|
+
id: options.id,
|
|
2489
|
+
json: options.json === true
|
|
2490
|
+
});
|
|
2491
|
+
process.stdout.write(result.output);
|
|
2492
|
+
});
|
|
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) => {
|
|
1517
2514
|
const result = await runEnable({
|
|
1518
|
-
skill: options.skill,
|
|
1519
|
-
|
|
2515
|
+
skill: requireSingleValue(options.skill, "skill"),
|
|
2516
|
+
agents: requireAtLeastOneValue(options.agent, "agent")
|
|
1520
2517
|
});
|
|
1521
2518
|
process.stdout.write(result.output);
|
|
1522
2519
|
});
|
|
1523
|
-
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) => {
|
|
1524
2521
|
const result = await runDisable({
|
|
1525
|
-
skill: options.skill,
|
|
1526
|
-
|
|
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
|
|
1527
2535
|
});
|
|
1528
2536
|
process.stdout.write(result.output);
|
|
1529
2537
|
});
|