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