skillmux 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +423 -284
- package/dist/cli.js +1333 -608
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1333 -608
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -63,30 +63,9 @@ var defaultAgentRuleMap = Object.fromEntries(
|
|
|
63
63
|
defaultAgentRules.map((rule) => [rule.id, rule])
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
// src/commands/
|
|
66
|
+
// src/commands/adopt.ts
|
|
67
67
|
import { homedir } from "os";
|
|
68
|
-
|
|
69
|
-
// src/config/resolve-skillmux-home.ts
|
|
70
|
-
import { join, resolve } from "path";
|
|
71
|
-
function buildConfigPath(skillmuxHome) {
|
|
72
|
-
return join(resolve(skillmuxHome), "config.json");
|
|
73
|
-
}
|
|
74
|
-
function resolveSkillmuxHome(homeDir) {
|
|
75
|
-
const resolvedHomeDir = resolve(homeDir);
|
|
76
|
-
const skillmuxHome = join(resolvedHomeDir, ".skillmux");
|
|
77
|
-
return {
|
|
78
|
-
skillmuxHome,
|
|
79
|
-
configPath: buildConfigPath(skillmuxHome)
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// src/discovery/discover-agents.ts
|
|
84
|
-
import * as fs2 from "fs/promises";
|
|
85
|
-
import { join as join2, resolve as resolve2 } from "path";
|
|
86
|
-
|
|
87
|
-
// src/config/load-user-config.ts
|
|
88
|
-
import * as fs from "fs/promises";
|
|
89
|
-
import { z } from "zod";
|
|
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 {
|
|
@@ -114,8 +93,43 @@ var UserConfigValidationError = class extends SkillMuxError {
|
|
|
114
93
|
super(message);
|
|
115
94
|
}
|
|
116
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";
|
|
117
129
|
|
|
118
130
|
// src/config/load-user-config.ts
|
|
131
|
+
import * as fs from "fs/promises";
|
|
132
|
+
import { z } from "zod";
|
|
119
133
|
var supportedPlatformSchema = z.enum(supportedPlatforms);
|
|
120
134
|
var agentOverrideSchema = z.object({
|
|
121
135
|
stableName: z.string().min(1).optional(),
|
|
@@ -251,422 +265,95 @@ async function discoverAgents(options) {
|
|
|
251
265
|
return discoveredAgents;
|
|
252
266
|
}
|
|
253
267
|
|
|
254
|
-
// src/
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// src/output/print-table.ts
|
|
261
|
-
function printTable(rows, columns) {
|
|
262
|
-
const renderedRows = rows.map(
|
|
263
|
-
(row) => columns.map((column) => String(row[column.key] ?? ""))
|
|
264
|
-
);
|
|
265
|
-
const widths = columns.map(
|
|
266
|
-
(column, index) => Math.max(
|
|
267
|
-
column.label.length,
|
|
268
|
-
...renderedRows.map((row) => row[index]?.length ?? 0)
|
|
269
|
-
)
|
|
270
|
-
);
|
|
271
|
-
const header = columns.map((column, index) => column.label.padEnd(widths[index])).join(" ");
|
|
272
|
-
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
273
|
-
const body = renderedRows.map(
|
|
274
|
-
(row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ")
|
|
275
|
-
);
|
|
276
|
-
return `${[header, separator, ...body].join("\n")}
|
|
277
|
-
`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// src/commands/agents.ts
|
|
281
|
-
function buildTableOutput(agents) {
|
|
282
|
-
return printTable(
|
|
283
|
-
agents.map((agent) => ({
|
|
284
|
-
id: agent.id,
|
|
285
|
-
name: agent.stableName,
|
|
286
|
-
path: agent.absoluteSkillsDirectoryPath,
|
|
287
|
-
exists: String(agent.exists),
|
|
288
|
-
supported: String(agent.supportedOnPlatform),
|
|
289
|
-
discovery: agent.discovery
|
|
290
|
-
})),
|
|
291
|
-
[
|
|
292
|
-
{ key: "id", label: "Agent" },
|
|
293
|
-
{ key: "name", label: "Name" },
|
|
294
|
-
{ key: "path", label: "Path" },
|
|
295
|
-
{ key: "exists", label: "Exists" },
|
|
296
|
-
{ key: "supported", label: "Supported" },
|
|
297
|
-
{ key: "discovery", label: "Discovery" }
|
|
298
|
-
]
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
async function runAgents(options = {}) {
|
|
302
|
-
const homeDir = options.homeDir ?? homedir();
|
|
303
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
304
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
305
|
-
const agents = await discoverAgents({
|
|
306
|
-
homeDir,
|
|
307
|
-
skillmuxHome,
|
|
308
|
-
platform: options.platform
|
|
309
|
-
});
|
|
310
|
-
return {
|
|
311
|
-
agents,
|
|
312
|
-
output: options.json === true ? printJson(agents) : buildTableOutput(agents)
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// src/commands/config-add-agent.ts
|
|
317
|
-
import { homedir as homedir2 } from "os";
|
|
318
|
-
import { isAbsolute } from "path";
|
|
268
|
+
// src/discovery/scan-agent-skills.ts
|
|
269
|
+
import * as fs5 from "fs/promises";
|
|
270
|
+
import { join as join3 } from "path";
|
|
319
271
|
|
|
320
|
-
// src/
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
324
|
-
return normalized.length > 0 ? normalized : "skill";
|
|
325
|
-
}
|
|
326
|
-
function isValidId(value) {
|
|
327
|
-
return ID_PATTERN.test(value);
|
|
328
|
-
}
|
|
272
|
+
// src/discovery/infer-skill-entry.ts
|
|
273
|
+
import * as fs4 from "fs/promises";
|
|
274
|
+
import { basename, resolve as resolve4 } from "path";
|
|
329
275
|
|
|
330
|
-
// src/
|
|
276
|
+
// src/fs/path-utils.ts
|
|
331
277
|
import * as fs3 from "fs/promises";
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
`, "utf8");
|
|
337
|
-
return {
|
|
338
|
-
skillmuxHome,
|
|
339
|
-
configPath
|
|
340
|
-
};
|
|
278
|
+
import { dirname, parse, relative, resolve as resolve3, sep } from "path";
|
|
279
|
+
function normalizeAbsolutePath(path) {
|
|
280
|
+
const normalized = resolve3(path);
|
|
281
|
+
return process.platform === "win32" ? normalized.replaceAll("/", "\\").toLowerCase() : normalized;
|
|
341
282
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
283
|
+
function pathsAreEqual(left, right) {
|
|
284
|
+
return normalizeAbsolutePath(left) === normalizeAbsolutePath(right);
|
|
285
|
+
}
|
|
286
|
+
function isPathInside(parentPath, childPath) {
|
|
287
|
+
const parent = normalizeAbsolutePath(parentPath);
|
|
288
|
+
const child = normalizeAbsolutePath(childPath);
|
|
289
|
+
if (parse(parent).root !== parse(child).root) {
|
|
290
|
+
return false;
|
|
348
291
|
}
|
|
349
|
-
|
|
350
|
-
|
|
292
|
+
const relativePath = relative(parent, child);
|
|
293
|
+
if (relativePath === "") {
|
|
294
|
+
return true;
|
|
351
295
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
throw new UserConfigValidationError(`${field} must stay within the configured home-relative tree`);
|
|
296
|
+
if (relativePath === "..") {
|
|
297
|
+
return false;
|
|
355
298
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
function normalizeAgentId(value) {
|
|
359
|
-
const trimmed = value.trim();
|
|
360
|
-
if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
|
|
361
|
-
throw new InvalidIdentifierError("agent id", value);
|
|
299
|
+
if (relativePath.startsWith(`..${sep}`)) {
|
|
300
|
+
return false;
|
|
362
301
|
}
|
|
363
|
-
return
|
|
302
|
+
return true;
|
|
364
303
|
}
|
|
365
|
-
function
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
304
|
+
async function assertNoSymlinkAncestors(path, options) {
|
|
305
|
+
let current = options?.includeLeaf === true ? resolve3(path) : dirname(resolve3(path));
|
|
306
|
+
while (true) {
|
|
307
|
+
try {
|
|
308
|
+
const entry = await fs3.lstat(current);
|
|
309
|
+
if (entry.isSymbolicLink()) {
|
|
310
|
+
throw new Error(`Refusing to use path with symlink ancestor at ${current}`);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error.code !== "ENOENT") {
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const parent = dirname(current);
|
|
318
|
+
if (parent === current) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
current = parent;
|
|
377
322
|
}
|
|
378
|
-
return normalized;
|
|
379
323
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
homeRelativeRootPath: normalizeRelativePath(options.root, "root"),
|
|
385
|
-
skillsDirectoryPath: normalizeRelativePath(options.skills ?? "skills", "skills")
|
|
386
|
-
};
|
|
387
|
-
if (options.name !== void 0 && options.name.trim().length > 0) {
|
|
388
|
-
agent.stableName = options.name.trim();
|
|
389
|
-
}
|
|
390
|
-
if (options.disabledByDefault === true) {
|
|
391
|
-
agent.enabledByDefault = false;
|
|
392
|
-
}
|
|
393
|
-
return { agentId, agent };
|
|
324
|
+
|
|
325
|
+
// src/discovery/infer-skill-entry.ts
|
|
326
|
+
function buildIssue(code, severity, message, path) {
|
|
327
|
+
return { code, severity, message, path };
|
|
394
328
|
}
|
|
395
|
-
function
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
stableName: result.agent.stableName ?? "",
|
|
414
|
-
platforms: (result.agent.supportedPlatforms ?? []).join(","),
|
|
415
|
-
root: result.agent.homeRelativeRootPath ?? "",
|
|
416
|
-
skills: result.agent.skillsDirectoryPath ?? "",
|
|
417
|
-
enabledByDefault: result.agent.enabledByDefault === void 0 ? "" : String(result.agent.enabledByDefault)
|
|
418
|
-
}
|
|
419
|
-
],
|
|
420
|
-
[
|
|
421
|
-
{ key: "stableName", label: "Name" },
|
|
422
|
-
{ key: "platforms", label: "Platforms" },
|
|
423
|
-
{ key: "root", label: "Root" },
|
|
424
|
-
{ key: "skills", label: "Skills Dir" },
|
|
425
|
-
{ key: "enabledByDefault", label: "Enabled By Default" }
|
|
426
|
-
]
|
|
427
|
-
);
|
|
428
|
-
return `${summary}${detail}`;
|
|
429
|
-
}
|
|
430
|
-
async function runConfigAddAgent(options) {
|
|
431
|
-
const homeDir = options.homeDir ?? homedir2();
|
|
432
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
433
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
434
|
-
const configPath = buildConfigPath(skillmuxHome);
|
|
435
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
436
|
-
const { agentId, agent } = buildAgentOverride(options);
|
|
437
|
-
const previous = config.agents[agentId];
|
|
438
|
-
const changed = JSON.stringify(previous ?? null) !== JSON.stringify(agent);
|
|
439
|
-
const nextConfig = {
|
|
440
|
-
...config,
|
|
441
|
-
agents: {
|
|
442
|
-
...config.agents,
|
|
443
|
-
[agentId]: agent
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
await writeUserConfig(skillmuxHome, nextConfig);
|
|
447
|
-
const resultWithoutOutput = {
|
|
448
|
-
skillmuxHome,
|
|
449
|
-
configPath,
|
|
450
|
-
agentId,
|
|
451
|
-
changed,
|
|
452
|
-
agent,
|
|
453
|
-
config: nextConfig
|
|
454
|
-
};
|
|
455
|
-
return {
|
|
456
|
-
...resultWithoutOutput,
|
|
457
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// src/commands/config-remove-agent.ts
|
|
462
|
-
import { homedir as homedir3 } from "os";
|
|
463
|
-
function normalizeAgentId2(value) {
|
|
464
|
-
const trimmed = value.trim();
|
|
465
|
-
if (trimmed.length === 0 || /[a-z0-9]/i.test(trimmed) === false) {
|
|
466
|
-
throw new InvalidIdentifierError("agent id", value);
|
|
467
|
-
}
|
|
468
|
-
return normalizeId(trimmed);
|
|
469
|
-
}
|
|
470
|
-
function buildTableOutput3(result) {
|
|
471
|
-
return printTable(
|
|
472
|
-
[
|
|
473
|
-
{
|
|
474
|
-
agentId: result.agentId,
|
|
475
|
-
configPath: result.configPath,
|
|
476
|
-
changed: String(result.changed),
|
|
477
|
-
removed: String(result.removed)
|
|
478
|
-
}
|
|
479
|
-
],
|
|
480
|
-
[
|
|
481
|
-
{ key: "agentId", label: "Agent" },
|
|
482
|
-
{ key: "configPath", label: "Config Path" },
|
|
483
|
-
{ key: "changed", label: "Changed" },
|
|
484
|
-
{ key: "removed", label: "Removed" }
|
|
485
|
-
]
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
async function runConfigRemoveAgent(options) {
|
|
489
|
-
const homeDir = options.homeDir ?? homedir3();
|
|
490
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
491
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
492
|
-
const configPath = buildConfigPath(skillmuxHome);
|
|
493
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
494
|
-
const agentId = normalizeAgentId2(options.id);
|
|
495
|
-
const removed = agentId in config.agents;
|
|
496
|
-
const nextConfig = {
|
|
497
|
-
...config,
|
|
498
|
-
agents: Object.fromEntries(
|
|
499
|
-
Object.entries(config.agents).filter(([currentAgentId]) => currentAgentId !== agentId)
|
|
500
|
-
)
|
|
501
|
-
};
|
|
502
|
-
if (removed) {
|
|
503
|
-
await writeUserConfig(skillmuxHome, nextConfig);
|
|
504
|
-
}
|
|
505
|
-
const resultWithoutOutput = {
|
|
506
|
-
skillmuxHome,
|
|
507
|
-
configPath,
|
|
508
|
-
agentId,
|
|
509
|
-
changed: removed,
|
|
510
|
-
removed,
|
|
511
|
-
config: nextConfig
|
|
512
|
-
};
|
|
513
|
-
return {
|
|
514
|
-
...resultWithoutOutput,
|
|
515
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput3(resultWithoutOutput)
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// src/commands/config.ts
|
|
520
|
-
import { homedir as homedir4 } from "os";
|
|
521
|
-
function buildTableOutput4(result) {
|
|
522
|
-
const summary = printTable(
|
|
523
|
-
[
|
|
524
|
-
{
|
|
525
|
-
skillmuxHome: result.skillmuxHome,
|
|
526
|
-
configPath: result.configPath,
|
|
527
|
-
overrides: String(Object.keys(result.config.agents).length)
|
|
528
|
-
}
|
|
529
|
-
],
|
|
530
|
-
[
|
|
531
|
-
{ key: "skillmuxHome", label: "SkillMux Home" },
|
|
532
|
-
{ key: "configPath", label: "Config Path" },
|
|
533
|
-
{ key: "overrides", label: "Overrides" }
|
|
534
|
-
]
|
|
535
|
-
);
|
|
536
|
-
const agentRows = Object.entries(result.config.agents).sort(([left], [right]) => left.localeCompare(right)).map(([agentId, agent]) => ({
|
|
537
|
-
agentId,
|
|
538
|
-
stableName: agent.stableName ?? "",
|
|
539
|
-
root: agent.homeRelativeRootPath ?? "",
|
|
540
|
-
skills: agent.skillsDirectoryPath ?? ""
|
|
541
|
-
}));
|
|
542
|
-
if (agentRows.length === 0) {
|
|
543
|
-
return `${summary}
|
|
544
|
-
No user overrides configured.
|
|
545
|
-
`;
|
|
546
|
-
}
|
|
547
|
-
return `${summary}
|
|
548
|
-
${printTable(agentRows, [
|
|
549
|
-
{ key: "agentId", label: "Agent" },
|
|
550
|
-
{ key: "stableName", label: "Name" },
|
|
551
|
-
{ key: "root", label: "Root" },
|
|
552
|
-
{ key: "skills", label: "Skills Dir" }
|
|
553
|
-
])}`;
|
|
554
|
-
}
|
|
555
|
-
async function runConfig(options = {}) {
|
|
556
|
-
const homeDir = options.homeDir ?? homedir4();
|
|
557
|
-
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
558
|
-
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
559
|
-
const config = await loadUserConfig(skillmuxHome);
|
|
560
|
-
const resultWithoutOutput = {
|
|
561
|
-
skillmuxHome,
|
|
562
|
-
configPath: buildConfigPath(skillmuxHome),
|
|
563
|
-
config
|
|
564
|
-
};
|
|
565
|
-
return {
|
|
566
|
-
...resultWithoutOutput,
|
|
567
|
-
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput4(resultWithoutOutput)
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// src/commands/doctor.ts
|
|
572
|
-
import * as fs9 from "fs/promises";
|
|
573
|
-
import { homedir as homedir5 } from "os";
|
|
574
|
-
import { join as join5 } from "path";
|
|
575
|
-
|
|
576
|
-
// src/discovery/scan-agent-skills.ts
|
|
577
|
-
import * as fs6 from "fs/promises";
|
|
578
|
-
|
|
579
|
-
// src/discovery/infer-skill-entry.ts
|
|
580
|
-
import * as fs5 from "fs/promises";
|
|
581
|
-
import { basename, resolve as resolve4 } from "path";
|
|
582
|
-
|
|
583
|
-
// src/fs/path-utils.ts
|
|
584
|
-
import * as fs4 from "fs/promises";
|
|
585
|
-
import { dirname, parse, relative, resolve as resolve3, sep } from "path";
|
|
586
|
-
function normalizeAbsolutePath(path) {
|
|
587
|
-
const normalized = resolve3(path);
|
|
588
|
-
return process.platform === "win32" ? normalized.replaceAll("/", "\\").toLowerCase() : normalized;
|
|
589
|
-
}
|
|
590
|
-
function pathsAreEqual(left, right) {
|
|
591
|
-
return normalizeAbsolutePath(left) === normalizeAbsolutePath(right);
|
|
592
|
-
}
|
|
593
|
-
function isPathInside(parentPath, childPath) {
|
|
594
|
-
const parent = normalizeAbsolutePath(parentPath);
|
|
595
|
-
const child = normalizeAbsolutePath(childPath);
|
|
596
|
-
if (parse(parent).root !== parse(child).root) {
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
const relativePath = relative(parent, child);
|
|
600
|
-
if (relativePath === "") {
|
|
601
|
-
return true;
|
|
602
|
-
}
|
|
603
|
-
if (relativePath === "..") {
|
|
604
|
-
return false;
|
|
605
|
-
}
|
|
606
|
-
if (relativePath.startsWith(`..${sep}`)) {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
async function assertNoSymlinkAncestors(path, options) {
|
|
612
|
-
let current = options?.includeLeaf === true ? resolve3(path) : dirname(resolve3(path));
|
|
613
|
-
while (true) {
|
|
614
|
-
try {
|
|
615
|
-
const entry = await fs4.lstat(current);
|
|
616
|
-
if (entry.isSymbolicLink()) {
|
|
617
|
-
throw new Error(`Refusing to use path with symlink ancestor at ${current}`);
|
|
618
|
-
}
|
|
619
|
-
} catch (error) {
|
|
620
|
-
if (error.code !== "ENOENT") {
|
|
621
|
-
throw error;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
const parent = dirname(current);
|
|
625
|
-
if (parent === current) {
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
current = parent;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/discovery/infer-skill-entry.ts
|
|
633
|
-
function buildIssue(code, severity, message, path) {
|
|
634
|
-
return { code, severity, message, path };
|
|
635
|
-
}
|
|
636
|
-
async function inferSkillEntry(options) {
|
|
637
|
-
const absolutePath = resolve4(options.path);
|
|
638
|
-
const skillName = basename(absolutePath);
|
|
639
|
-
const stats = await fs5.lstat(absolutePath);
|
|
640
|
-
if (stats.isSymbolicLink()) {
|
|
641
|
-
try {
|
|
642
|
-
const targetPath = await fs5.realpath(absolutePath);
|
|
643
|
-
if (isPathInside(options.skillmuxHome, targetPath)) {
|
|
644
|
-
return {
|
|
645
|
-
entry: {
|
|
646
|
-
agentId: options.agentId,
|
|
647
|
-
agentName: options.agentName,
|
|
648
|
-
skillName,
|
|
649
|
-
kind: "managed-link",
|
|
650
|
-
path: absolutePath,
|
|
651
|
-
targetPath
|
|
652
|
-
}
|
|
653
|
-
};
|
|
329
|
+
async function inferSkillEntry(options) {
|
|
330
|
+
const absolutePath = resolve4(options.path);
|
|
331
|
+
const skillName = basename(absolutePath);
|
|
332
|
+
const stats = await fs4.lstat(absolutePath);
|
|
333
|
+
if (stats.isSymbolicLink()) {
|
|
334
|
+
try {
|
|
335
|
+
const targetPath = await fs4.realpath(absolutePath);
|
|
336
|
+
if (isPathInside(options.skillmuxHome, targetPath)) {
|
|
337
|
+
return {
|
|
338
|
+
entry: {
|
|
339
|
+
agentId: options.agentId,
|
|
340
|
+
agentName: options.agentName,
|
|
341
|
+
skillName,
|
|
342
|
+
kind: "managed-link",
|
|
343
|
+
path: absolutePath,
|
|
344
|
+
targetPath
|
|
345
|
+
}
|
|
346
|
+
};
|
|
654
347
|
}
|
|
655
348
|
return {
|
|
656
349
|
entry: {
|
|
657
350
|
agentId: options.agentId,
|
|
658
351
|
agentName: options.agentName,
|
|
659
352
|
skillName,
|
|
660
|
-
kind: "
|
|
353
|
+
kind: "unmanaged-link",
|
|
661
354
|
path: absolutePath,
|
|
662
355
|
targetPath
|
|
663
|
-
}
|
|
664
|
-
issue: buildIssue(
|
|
665
|
-
"unknown-entry",
|
|
666
|
-
"warning",
|
|
667
|
-
"Skill entry is a link that points outside the SkillMux managed store",
|
|
668
|
-
absolutePath
|
|
669
|
-
)
|
|
356
|
+
}
|
|
670
357
|
};
|
|
671
358
|
} catch (error) {
|
|
672
359
|
if (error.code !== "ENOENT") {
|
|
@@ -725,7 +412,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
725
412
|
issues: []
|
|
726
413
|
};
|
|
727
414
|
}
|
|
728
|
-
const directoryEntries = await
|
|
415
|
+
const directoryEntries = await fs5.readdir(agent.absoluteSkillsDirectoryPath, {
|
|
729
416
|
withFileTypes: true
|
|
730
417
|
});
|
|
731
418
|
const sortedDirectoryEntries = [...directoryEntries].sort(
|
|
@@ -737,7 +424,7 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
737
424
|
const result = await inferSkillEntry({
|
|
738
425
|
agentId: agent.id,
|
|
739
426
|
agentName: agent.stableName,
|
|
740
|
-
path:
|
|
427
|
+
path: join3(agent.absoluteSkillsDirectoryPath, directoryEntry.name),
|
|
741
428
|
skillmuxHome
|
|
742
429
|
});
|
|
743
430
|
entries.push(result.entry);
|
|
@@ -751,9 +438,201 @@ async function scanAgentSkills(agent, skillmuxHome) {
|
|
|
751
438
|
};
|
|
752
439
|
}
|
|
753
440
|
|
|
754
|
-
// src/
|
|
755
|
-
import * as
|
|
756
|
-
import { join as join4, resolve as resolve5 } from "path";
|
|
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;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
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
|
+
}
|
|
487
|
+
}
|
|
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
|
+
}
|
|
501
|
+
}
|
|
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";
|
|
757
636
|
|
|
758
637
|
// src/manifest/build-empty-manifest.ts
|
|
759
638
|
function buildEmptyManifest(skillmuxHome) {
|
|
@@ -878,35 +757,35 @@ var manifestSchema = z2.object({
|
|
|
878
757
|
|
|
879
758
|
// src/manifest/write-manifest.ts
|
|
880
759
|
import { randomUUID } from "crypto";
|
|
881
|
-
import * as
|
|
882
|
-
import { join as
|
|
760
|
+
import * as fs8 from "fs/promises";
|
|
761
|
+
import { join as join5 } from "path";
|
|
883
762
|
function getManifestPath(home) {
|
|
884
|
-
return
|
|
763
|
+
return join5(home, "manifest.json");
|
|
885
764
|
}
|
|
886
765
|
function createManifestTempPath(manifestPath) {
|
|
887
766
|
return `${manifestPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
888
767
|
}
|
|
889
768
|
async function writeManifest(home, manifest) {
|
|
890
|
-
await
|
|
769
|
+
await fs8.mkdir(home, { recursive: true });
|
|
891
770
|
const manifestPath = getManifestPath(home);
|
|
892
771
|
const tempPath = createManifestTempPath(manifestPath);
|
|
893
772
|
const contents = `${JSON.stringify(manifest, null, 2)}
|
|
894
773
|
`;
|
|
895
|
-
await
|
|
774
|
+
await fs8.writeFile(tempPath, contents, "utf8");
|
|
896
775
|
try {
|
|
897
|
-
await
|
|
776
|
+
await fs8.rename(tempPath, manifestPath);
|
|
898
777
|
} catch (error) {
|
|
899
|
-
await
|
|
778
|
+
await fs8.unlink(tempPath).catch(() => void 0);
|
|
900
779
|
throw error;
|
|
901
780
|
}
|
|
902
781
|
}
|
|
903
782
|
|
|
904
783
|
// src/manifest/read-manifest.ts
|
|
905
784
|
function getManifestPath2(home) {
|
|
906
|
-
return
|
|
785
|
+
return join6(home, "manifest.json");
|
|
907
786
|
}
|
|
908
787
|
function normalizeHomePath(home) {
|
|
909
|
-
const resolvedHome =
|
|
788
|
+
const resolvedHome = resolve7(home);
|
|
910
789
|
return process.platform === "win32" ? resolvedHome.toLowerCase() : resolvedHome;
|
|
911
790
|
}
|
|
912
791
|
function formatValidationIssues2(error) {
|
|
@@ -918,7 +797,7 @@ function formatValidationIssues2(error) {
|
|
|
918
797
|
async function readManifest(home) {
|
|
919
798
|
const manifestPath = getManifestPath2(home);
|
|
920
799
|
try {
|
|
921
|
-
const contents = await
|
|
800
|
+
const contents = await fs9.readFile(manifestPath, "utf8");
|
|
922
801
|
const parsedJson = JSON.parse(contents);
|
|
923
802
|
const parsedManifest = manifestSchema.safeParse(parsedJson);
|
|
924
803
|
if (!parsedManifest.success) {
|
|
@@ -947,13 +826,689 @@ async function readManifest(home) {
|
|
|
947
826
|
}
|
|
948
827
|
}
|
|
949
828
|
|
|
829
|
+
// src/output/print-json.ts
|
|
830
|
+
function printJson(value) {
|
|
831
|
+
return `${JSON.stringify(value, null, 2)}
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/commands/adopt.ts
|
|
836
|
+
function buildManagedSkillPath(skillmuxHome, skillId) {
|
|
837
|
+
return resolve8(skillmuxHome, "skills", skillId);
|
|
838
|
+
}
|
|
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
|
+
};
|
|
848
|
+
}
|
|
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;
|
|
865
|
+
}
|
|
866
|
+
manifest.activations[index] = activation;
|
|
867
|
+
}
|
|
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}`);
|
|
874
|
+
}
|
|
875
|
+
if (!agent.supportedOnPlatform) {
|
|
876
|
+
throw new AdoptionError(
|
|
877
|
+
`Agent ${agent.id} is not supported on ${process.platform}`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return agent;
|
|
881
|
+
}
|
|
882
|
+
function filterEntries(scannedAgent, skillFilter) {
|
|
883
|
+
if (skillFilter === void 0) {
|
|
884
|
+
return scannedAgent.entries;
|
|
885
|
+
}
|
|
886
|
+
const skillId = normalizeId(skillFilter);
|
|
887
|
+
return scannedAgent.entries.filter(
|
|
888
|
+
(entry) => normalizeId(entry.skillName) === skillId
|
|
889
|
+
);
|
|
890
|
+
}
|
|
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;
|
|
899
|
+
}
|
|
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}`);
|
|
915
|
+
}
|
|
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);
|
|
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,
|
|
1142
|
+
skillmuxHome,
|
|
1143
|
+
platform: options.platform
|
|
1144
|
+
});
|
|
1145
|
+
return {
|
|
1146
|
+
agents,
|
|
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
|
|
1281
|
+
};
|
|
1282
|
+
return {
|
|
1283
|
+
...resultWithoutOutput,
|
|
1284
|
+
output: options.json === true ? printJson(resultWithoutOutput) : buildTableOutput2(resultWithoutOutput)
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/commands/config-remove-agent.ts
|
|
1289
|
+
import { homedir as homedir4 } from "os";
|
|
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
|
+
}
|
|
1345
|
+
|
|
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
|
|
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
|
+
`;
|
|
1477
|
+
}
|
|
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
|
+
])}`;
|
|
1485
|
+
}
|
|
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
|
+
|
|
950
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";
|
|
951
1506
|
function buildIssue2(code, severity, message, path) {
|
|
952
1507
|
return path === void 0 ? { code, severity, message } : { code, severity, message, path };
|
|
953
1508
|
}
|
|
954
1509
|
async function pathExists2(path) {
|
|
955
1510
|
try {
|
|
956
|
-
await
|
|
1511
|
+
await fs11.access(path);
|
|
957
1512
|
return true;
|
|
958
1513
|
} catch (error) {
|
|
959
1514
|
if (error.code === "ENOENT") {
|
|
@@ -967,7 +1522,7 @@ async function addUnmanagedDirectoryIssues(entries, issues) {
|
|
|
967
1522
|
if (entry.kind !== "unmanaged-directory") {
|
|
968
1523
|
continue;
|
|
969
1524
|
}
|
|
970
|
-
if (await pathExists2(
|
|
1525
|
+
if (await pathExists2(join8(entry.path, "SKILL.md"))) {
|
|
971
1526
|
issues.push(
|
|
972
1527
|
buildIssue2(
|
|
973
1528
|
"unmanaged-skill-directory",
|
|
@@ -1017,7 +1572,7 @@ function addConflictingAgentPathIssues(agents, issues) {
|
|
|
1017
1572
|
);
|
|
1018
1573
|
}
|
|
1019
1574
|
}
|
|
1020
|
-
function
|
|
1575
|
+
function buildTableOutput6(issues) {
|
|
1021
1576
|
if (issues.length === 0) {
|
|
1022
1577
|
return "No doctor issues found.\n";
|
|
1023
1578
|
}
|
|
@@ -1049,7 +1604,7 @@ function buildJsonOutput(result) {
|
|
|
1049
1604
|
});
|
|
1050
1605
|
}
|
|
1051
1606
|
async function runDoctor(options = {}) {
|
|
1052
|
-
const homeDir = options.homeDir ??
|
|
1607
|
+
const homeDir = options.homeDir ?? homedir7();
|
|
1053
1608
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1054
1609
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1055
1610
|
const [manifest, config, agents] = await Promise.all([
|
|
@@ -1086,135 +1641,14 @@ async function runDoctor(options = {}) {
|
|
|
1086
1641
|
};
|
|
1087
1642
|
return {
|
|
1088
1643
|
...resultWithoutOutput,
|
|
1089
|
-
output: options.json === true ? buildJsonOutput(resultWithoutOutput) :
|
|
1644
|
+
output: options.json === true ? buildJsonOutput(resultWithoutOutput) : buildTableOutput6(dedupedIssues)
|
|
1090
1645
|
};
|
|
1091
1646
|
}
|
|
1092
1647
|
|
|
1093
1648
|
// src/commands/disable.ts
|
|
1094
1649
|
import * as fs13 from "fs/promises";
|
|
1095
|
-
import { homedir as
|
|
1096
|
-
import { join as
|
|
1097
|
-
|
|
1098
|
-
// src/fs/safe-copy.ts
|
|
1099
|
-
import * as fs10 from "fs/promises";
|
|
1100
|
-
import { dirname as dirname2, join as join6, resolve as resolve6 } from "path";
|
|
1101
|
-
async function assertDirectory(path) {
|
|
1102
|
-
const entry = await fs10.lstat(path);
|
|
1103
|
-
if (!entry.isDirectory()) {
|
|
1104
|
-
throw new Error(`Expected a directory at ${path}`);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
async function assertRegularFile(path, label) {
|
|
1108
|
-
const entry = await fs10.lstat(path);
|
|
1109
|
-
if (!entry.isFile()) {
|
|
1110
|
-
throw new Error(`Expected ${label} to be a regular file at ${path}`);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
async function assertTargetDoesNotExist(path) {
|
|
1114
|
-
try {
|
|
1115
|
-
await fs10.lstat(path);
|
|
1116
|
-
throw new Error(`Refusing to overwrite existing path at ${path}`);
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
if (error.code !== "ENOENT") {
|
|
1119
|
-
throw error;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
async function copyDirectoryContents(sourcePath, targetPath) {
|
|
1124
|
-
await fs10.mkdir(targetPath, { recursive: true });
|
|
1125
|
-
const entries = await fs10.readdir(sourcePath, { withFileTypes: true });
|
|
1126
|
-
for (const entry of entries) {
|
|
1127
|
-
const sourceEntryPath = join6(sourcePath, entry.name);
|
|
1128
|
-
const targetEntryPath = join6(targetPath, entry.name);
|
|
1129
|
-
const entryStats = await fs10.lstat(sourceEntryPath);
|
|
1130
|
-
if (entryStats.isSymbolicLink()) {
|
|
1131
|
-
throw new Error(`Refusing to copy source symlink at ${sourceEntryPath}`);
|
|
1132
|
-
}
|
|
1133
|
-
if (entryStats.isDirectory()) {
|
|
1134
|
-
await copyDirectoryContents(sourceEntryPath, targetEntryPath);
|
|
1135
|
-
continue;
|
|
1136
|
-
}
|
|
1137
|
-
if (entryStats.isFile()) {
|
|
1138
|
-
await fs10.mkdir(dirname2(targetEntryPath), { recursive: true });
|
|
1139
|
-
await fs10.copyFile(sourceEntryPath, targetEntryPath);
|
|
1140
|
-
continue;
|
|
1141
|
-
}
|
|
1142
|
-
throw new Error(`Unsupported filesystem entry at ${sourceEntryPath}`);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
async function assertSkillSourceLayout(sourcePath) {
|
|
1146
|
-
const resolvedSourcePath = resolve6(sourcePath);
|
|
1147
|
-
const skillFilePath = join6(resolvedSourcePath, "SKILL.md");
|
|
1148
|
-
await assertNoSymlinkAncestors(resolvedSourcePath, { includeLeaf: true });
|
|
1149
|
-
await assertDirectory(resolvedSourcePath);
|
|
1150
|
-
try {
|
|
1151
|
-
await assertRegularFile(skillFilePath, "SKILL.md");
|
|
1152
|
-
} catch (error) {
|
|
1153
|
-
if (error.code === "ENOENT") {
|
|
1154
|
-
throw new Error(`Refusing to import ${resolvedSourcePath} without a root SKILL.md`);
|
|
1155
|
-
}
|
|
1156
|
-
throw error;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
async function copySkillContentsToManagedStore(sourcePath, targetPath) {
|
|
1160
|
-
const resolvedSourcePath = resolve6(sourcePath);
|
|
1161
|
-
const resolvedTargetPath = resolve6(targetPath);
|
|
1162
|
-
if (pathsAreEqual(resolvedSourcePath, resolvedTargetPath)) {
|
|
1163
|
-
throw new Error("Source and target paths must differ");
|
|
1164
|
-
}
|
|
1165
|
-
if (isPathInside(resolvedSourcePath, resolvedTargetPath)) {
|
|
1166
|
-
throw new Error("Refusing to copy into a child of the source directory");
|
|
1167
|
-
}
|
|
1168
|
-
await assertSkillSourceLayout(resolvedSourcePath);
|
|
1169
|
-
await assertNoSymlinkAncestors(resolvedTargetPath);
|
|
1170
|
-
await assertTargetDoesNotExist(resolvedTargetPath);
|
|
1171
|
-
await copyDirectoryContents(resolvedSourcePath, resolvedTargetPath);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// src/fs/link-ops.ts
|
|
1175
|
-
import * as fs11 from "fs/promises";
|
|
1176
|
-
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
1177
|
-
var directoryLinkType = process.platform === "win32" ? "junction" : "dir";
|
|
1178
|
-
async function createManagedLink(linkPath, targetPath) {
|
|
1179
|
-
const resolvedLinkPath = resolve7(linkPath);
|
|
1180
|
-
const resolvedTargetPath = resolve7(targetPath);
|
|
1181
|
-
await assertNoSymlinkAncestors(resolvedLinkPath);
|
|
1182
|
-
await assertNoSymlinkAncestors(resolvedTargetPath, { includeLeaf: true });
|
|
1183
|
-
await fs11.mkdir(dirname3(resolvedLinkPath), { recursive: true });
|
|
1184
|
-
try {
|
|
1185
|
-
const existingEntry = await fs11.lstat(resolvedLinkPath);
|
|
1186
|
-
if (!existingEntry.isSymbolicLink()) {
|
|
1187
|
-
throw new Error(`Refusing to replace non-link entry at ${resolvedLinkPath}`);
|
|
1188
|
-
}
|
|
1189
|
-
const currentTargetPath = await fs11.realpath(resolvedLinkPath);
|
|
1190
|
-
if (pathsAreEqual(currentTargetPath, resolvedTargetPath)) {
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
throw new Error(`Refusing to replace link at ${resolvedLinkPath}`);
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
if (error.code === "ENOENT" && await fs11.lstat(resolvedLinkPath).then((entry) => entry.isSymbolicLink()).catch(() => false)) {
|
|
1196
|
-
await fs11.rm(resolvedLinkPath, { recursive: true, force: false });
|
|
1197
|
-
} else if (error.code !== "ENOENT") {
|
|
1198
|
-
throw error;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
await fs11.symlink(resolvedTargetPath, resolvedLinkPath, directoryLinkType);
|
|
1202
|
-
}
|
|
1203
|
-
async function isLinkPointingToTarget(linkPath, targetPath) {
|
|
1204
|
-
try {
|
|
1205
|
-
const entry = await fs11.lstat(linkPath);
|
|
1206
|
-
if (!entry.isSymbolicLink()) {
|
|
1207
|
-
return false;
|
|
1208
|
-
}
|
|
1209
|
-
const resolvedTargetPath = await fs11.realpath(linkPath);
|
|
1210
|
-
return pathsAreEqual(resolvedTargetPath, targetPath);
|
|
1211
|
-
} catch (error) {
|
|
1212
|
-
if (error.code === "ENOENT") {
|
|
1213
|
-
return false;
|
|
1214
|
-
}
|
|
1215
|
-
throw error;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1650
|
+
import { homedir as homedir8 } from "os";
|
|
1651
|
+
import { join as join9, resolve as resolve9 } from "path";
|
|
1218
1652
|
|
|
1219
1653
|
// src/fs/safe-remove-link.ts
|
|
1220
1654
|
import * as fs12 from "fs/promises";
|
|
@@ -1235,7 +1669,7 @@ async function safeRemoveLink(path) {
|
|
|
1235
1669
|
}
|
|
1236
1670
|
|
|
1237
1671
|
// src/commands/disable.ts
|
|
1238
|
-
function
|
|
1672
|
+
function buildAgentRecord2(agent, timestamp) {
|
|
1239
1673
|
return {
|
|
1240
1674
|
id: agent.id,
|
|
1241
1675
|
name: agent.stableName,
|
|
@@ -1245,7 +1679,7 @@ function buildAgentRecord(agent, timestamp) {
|
|
|
1245
1679
|
lastSeenAt: agent.exists ? timestamp : null
|
|
1246
1680
|
};
|
|
1247
1681
|
}
|
|
1248
|
-
function
|
|
1682
|
+
function buildActivationRecord2(skillId, agentId, linkPath, timestamp) {
|
|
1249
1683
|
return {
|
|
1250
1684
|
skillId,
|
|
1251
1685
|
agentId,
|
|
@@ -1254,7 +1688,7 @@ function buildActivationRecord(skillId, agentId, linkPath, timestamp) {
|
|
|
1254
1688
|
updatedAt: timestamp
|
|
1255
1689
|
};
|
|
1256
1690
|
}
|
|
1257
|
-
function
|
|
1691
|
+
function upsertActivation2(manifest, activation) {
|
|
1258
1692
|
const index = manifest.activations.findIndex(
|
|
1259
1693
|
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
1260
1694
|
);
|
|
@@ -1264,8 +1698,8 @@ function upsertActivation(manifest, activation) {
|
|
|
1264
1698
|
}
|
|
1265
1699
|
manifest.activations[index] = activation;
|
|
1266
1700
|
}
|
|
1267
|
-
function
|
|
1268
|
-
return
|
|
1701
|
+
function buildManagedSkillPath2(skillmuxHome, skillId) {
|
|
1702
|
+
return resolve9(skillmuxHome, "skills", skillId);
|
|
1269
1703
|
}
|
|
1270
1704
|
async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName, linkPath, timestamp) {
|
|
1271
1705
|
try {
|
|
@@ -1281,7 +1715,7 @@ async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName,
|
|
|
1281
1715
|
}
|
|
1282
1716
|
const sourcePath = await fs13.realpath(linkPath);
|
|
1283
1717
|
await assertSkillSourceLayout(sourcePath);
|
|
1284
|
-
const managedSkillPath =
|
|
1718
|
+
const managedSkillPath = buildManagedSkillPath2(skillmuxHome, skillId);
|
|
1285
1719
|
await copySkillContentsToManagedStore(sourcePath, managedSkillPath);
|
|
1286
1720
|
const skill = {
|
|
1287
1721
|
id: skillId,
|
|
@@ -1296,7 +1730,7 @@ async function tryAdoptManagedSkill(manifest, skillmuxHome, skillId, skillName,
|
|
|
1296
1730
|
manifest.skills[skillId] = skill;
|
|
1297
1731
|
return { skill, sourcePath };
|
|
1298
1732
|
}
|
|
1299
|
-
async function
|
|
1733
|
+
async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
1300
1734
|
const agentId = normalizeId(agentName);
|
|
1301
1735
|
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
1302
1736
|
const agent = agents.find((entry) => entry.id === agentId);
|
|
@@ -1319,15 +1753,18 @@ async function pathExists3(path) {
|
|
|
1319
1753
|
throw error;
|
|
1320
1754
|
}
|
|
1321
1755
|
}
|
|
1322
|
-
async function
|
|
1323
|
-
|
|
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();
|
|
1324
1761
|
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1325
1762
|
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1326
1763
|
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1327
1764
|
const manifest = await readManifest(skillmuxHome);
|
|
1328
1765
|
const skillId = normalizeId(options.skill);
|
|
1329
|
-
const agent = await
|
|
1330
|
-
const linkPath =
|
|
1766
|
+
const agent = await resolveTargetAgent2(homeDir, skillmuxHome, options.agent);
|
|
1767
|
+
const linkPath = join9(agent.absoluteSkillsDirectoryPath, skillId);
|
|
1331
1768
|
const adoption = manifest.skills[skillId] ? void 0 : await tryAdoptManagedSkill(
|
|
1332
1769
|
manifest,
|
|
1333
1770
|
skillmuxHome,
|
|
@@ -1344,7 +1781,7 @@ async function runDisable(options) {
|
|
|
1344
1781
|
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1345
1782
|
);
|
|
1346
1783
|
const activationLinkPath = currentActivation?.linkPath ?? linkPath;
|
|
1347
|
-
const agentRecord =
|
|
1784
|
+
const agentRecord = buildAgentRecord2(agent, timestamp);
|
|
1348
1785
|
manifest.agents[agent.id] = agentRecord;
|
|
1349
1786
|
const adoptedLinkRemoved = adoption !== void 0 ? await safeRemoveLink(linkPath) : false;
|
|
1350
1787
|
const linkMatchesSkill = adoption === void 0 ? await isLinkPointingToTarget(activationLinkPath, skill.path) : false;
|
|
@@ -1363,8 +1800,8 @@ async function runDisable(options) {
|
|
|
1363
1800
|
`
|
|
1364
1801
|
};
|
|
1365
1802
|
}
|
|
1366
|
-
const activation =
|
|
1367
|
-
|
|
1803
|
+
const activation = buildActivationRecord2(skill.id, agent.id, linkPath, timestamp);
|
|
1804
|
+
upsertActivation2(manifest, activation);
|
|
1368
1805
|
await writeManifest(skillmuxHome, manifest);
|
|
1369
1806
|
return {
|
|
1370
1807
|
changed: true,
|
|
@@ -1376,12 +1813,44 @@ async function runDisable(options) {
|
|
|
1376
1813
|
`
|
|
1377
1814
|
};
|
|
1378
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
|
+
}
|
|
1379
1848
|
|
|
1380
1849
|
// src/commands/enable.ts
|
|
1381
1850
|
import * as fs14 from "fs/promises";
|
|
1382
|
-
import { homedir as
|
|
1383
|
-
import { join as
|
|
1384
|
-
function
|
|
1851
|
+
import { homedir as homedir9 } from "os";
|
|
1852
|
+
import { join as join10 } from "path";
|
|
1853
|
+
function buildAgentRecord3(agent, timestamp) {
|
|
1385
1854
|
return {
|
|
1386
1855
|
id: agent.id,
|
|
1387
1856
|
name: agent.stableName,
|
|
@@ -1391,7 +1860,7 @@ function buildAgentRecord2(agent, timestamp) {
|
|
|
1391
1860
|
lastSeenAt: timestamp
|
|
1392
1861
|
};
|
|
1393
1862
|
}
|
|
1394
|
-
function
|
|
1863
|
+
function buildActivationRecord3(skillId, agentId, linkPath, timestamp, state) {
|
|
1395
1864
|
return {
|
|
1396
1865
|
skillId,
|
|
1397
1866
|
agentId,
|
|
@@ -1400,7 +1869,7 @@ function buildActivationRecord2(skillId, agentId, linkPath, timestamp, state) {
|
|
|
1400
1869
|
updatedAt: timestamp
|
|
1401
1870
|
};
|
|
1402
1871
|
}
|
|
1403
|
-
function
|
|
1872
|
+
function upsertActivation3(manifest, activation) {
|
|
1404
1873
|
const index = manifest.activations.findIndex(
|
|
1405
1874
|
(entry) => entry.skillId === activation.skillId && entry.agentId === activation.agentId
|
|
1406
1875
|
);
|
|
@@ -1410,7 +1879,7 @@ function upsertActivation2(manifest, activation) {
|
|
|
1410
1879
|
}
|
|
1411
1880
|
manifest.activations[index] = activation;
|
|
1412
1881
|
}
|
|
1413
|
-
async function
|
|
1882
|
+
async function resolveTargetAgent3(homeDir, skillmuxHome, agentName) {
|
|
1414
1883
|
const agentId = normalizeId(agentName);
|
|
1415
1884
|
const agents = await discoverAgents({ homeDir, skillmuxHome });
|
|
1416
1885
|
const agent = agents.find((entry) => entry.id === agentId);
|
|
@@ -1422,8 +1891,11 @@ async function resolveTargetAgent2(homeDir, skillmuxHome, agentName) {
|
|
|
1422
1891
|
}
|
|
1423
1892
|
return agent;
|
|
1424
1893
|
}
|
|
1425
|
-
async function
|
|
1426
|
-
|
|
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();
|
|
1427
1899
|
const { skillmuxHome: defaultSkillmuxHome } = resolveSkillmuxHome(homeDir);
|
|
1428
1900
|
const skillmuxHome = options.skillmuxHome ?? defaultSkillmuxHome;
|
|
1429
1901
|
const timestamp = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1433,12 +1905,12 @@ async function runEnable(options) {
|
|
|
1433
1905
|
if (skill === void 0) {
|
|
1434
1906
|
throw new Error(`Managed skill not found: ${skillId}`);
|
|
1435
1907
|
}
|
|
1436
|
-
const agent = await
|
|
1437
|
-
const linkPath =
|
|
1908
|
+
const agent = await resolveTargetAgent3(homeDir, skillmuxHome, options.agent);
|
|
1909
|
+
const linkPath = join10(agent.absoluteSkillsDirectoryPath, skill.id);
|
|
1438
1910
|
const currentActivation = manifest.activations.find(
|
|
1439
1911
|
(entry) => entry.skillId === skill.id && entry.agentId === agent.id
|
|
1440
1912
|
);
|
|
1441
|
-
const agentRecord =
|
|
1913
|
+
const agentRecord = buildAgentRecord3(agent, timestamp);
|
|
1442
1914
|
manifest.agents[agent.id] = agentRecord;
|
|
1443
1915
|
await fs14.mkdir(agent.absoluteSkillsDirectoryPath, { recursive: true });
|
|
1444
1916
|
const linkAlreadyEnabled = await isLinkPointingToTarget(linkPath, skill.path);
|
|
@@ -1455,14 +1927,14 @@ async function runEnable(options) {
|
|
|
1455
1927
|
};
|
|
1456
1928
|
}
|
|
1457
1929
|
await createManagedLink(linkPath, skill.path);
|
|
1458
|
-
const activation =
|
|
1930
|
+
const activation = buildActivationRecord3(
|
|
1459
1931
|
skill.id,
|
|
1460
1932
|
agent.id,
|
|
1461
1933
|
linkPath,
|
|
1462
1934
|
timestamp,
|
|
1463
1935
|
"enabled"
|
|
1464
1936
|
);
|
|
1465
|
-
|
|
1937
|
+
upsertActivation3(manifest, activation);
|
|
1466
1938
|
await writeManifest(skillmuxHome, manifest);
|
|
1467
1939
|
return {
|
|
1468
1940
|
changed: true,
|
|
@@ -1474,22 +1946,54 @@ async function runEnable(options) {
|
|
|
1474
1946
|
`
|
|
1475
1947
|
};
|
|
1476
1948
|
}
|
|
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
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (results.length === 0) {
|
|
1967
|
+
throw new Error("Enable requires at least one target agent");
|
|
1968
|
+
}
|
|
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
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
return runEnableSingle(options);
|
|
1980
|
+
}
|
|
1477
1981
|
|
|
1478
1982
|
// src/commands/import.ts
|
|
1479
|
-
import { resolve as
|
|
1480
|
-
import { homedir as
|
|
1481
|
-
function
|
|
1482
|
-
return
|
|
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);
|
|
1483
1987
|
}
|
|
1484
1988
|
async function runImport(options) {
|
|
1485
|
-
const homeDir = options.homeDir ??
|
|
1989
|
+
const homeDir = options.homeDir ?? homedir10();
|
|
1486
1990
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1487
1991
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1488
|
-
const sourcePath =
|
|
1992
|
+
const sourcePath = resolve10(options.sourcePath);
|
|
1489
1993
|
const skillId = normalizeId(options.skillName);
|
|
1490
1994
|
const importedAt = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1491
1995
|
const manifest = await readManifest(skillmuxHome);
|
|
1492
|
-
const managedSkillPath =
|
|
1996
|
+
const managedSkillPath = buildManagedSkillPath3(skillmuxHome, skillId);
|
|
1493
1997
|
await assertSkillSourceLayout(sourcePath);
|
|
1494
1998
|
if (manifest.skills[skillId] !== void 0) {
|
|
1495
1999
|
throw new Error(`Managed skill already exists for ${skillId}`);
|
|
@@ -1516,7 +2020,7 @@ async function runImport(options) {
|
|
|
1516
2020
|
}
|
|
1517
2021
|
|
|
1518
2022
|
// src/commands/scan.ts
|
|
1519
|
-
import { homedir as
|
|
2023
|
+
import { homedir as homedir11 } from "os";
|
|
1520
2024
|
|
|
1521
2025
|
// src/output/format-issue.ts
|
|
1522
2026
|
function formatIssue(issue) {
|
|
@@ -1527,7 +2031,7 @@ function formatIssue(issue) {
|
|
|
1527
2031
|
}
|
|
1528
2032
|
|
|
1529
2033
|
// src/commands/scan.ts
|
|
1530
|
-
function
|
|
2034
|
+
function buildAgentRecord4(agent, timestamp, previousRecord) {
|
|
1531
2035
|
const lastSeenAt = agent.exists ? timestamp : previousRecord?.lastSeenAt ?? null;
|
|
1532
2036
|
return {
|
|
1533
2037
|
id: agent.id,
|
|
@@ -1577,7 +2081,7 @@ ${result.issues.map(formatIssue).join("\n")}
|
|
|
1577
2081
|
`;
|
|
1578
2082
|
}
|
|
1579
2083
|
async function runScan(options = {}) {
|
|
1580
|
-
const homeDir = options.homeDir ??
|
|
2084
|
+
const homeDir = options.homeDir ?? homedir11();
|
|
1581
2085
|
const resolvedPaths = resolveSkillmuxHome(homeDir);
|
|
1582
2086
|
const skillmuxHome = options.skillmuxHome ?? resolvedPaths.skillmuxHome;
|
|
1583
2087
|
const manifest = await readManifest(skillmuxHome);
|
|
@@ -1593,7 +2097,7 @@ async function runScan(options = {}) {
|
|
|
1593
2097
|
const scannedAgent = await scanAgentSkills(agent, skillmuxHome);
|
|
1594
2098
|
entries.push(...scannedAgent.entries);
|
|
1595
2099
|
issues.push(...scannedAgent.issues);
|
|
1596
|
-
manifest.agents[agent.id] =
|
|
2100
|
+
manifest.agents[agent.id] = buildAgentRecord4(
|
|
1597
2101
|
agent,
|
|
1598
2102
|
timestamp,
|
|
1599
2103
|
manifest.agents[agent.id]
|
|
@@ -1683,7 +2187,7 @@ function buildListData(scanResult, view) {
|
|
|
1683
2187
|
}
|
|
1684
2188
|
return buildRecordsView(scanResult);
|
|
1685
2189
|
}
|
|
1686
|
-
function
|
|
2190
|
+
function buildTableOutput7(data, view) {
|
|
1687
2191
|
if (view === "agents") {
|
|
1688
2192
|
const agentRows = data.agents;
|
|
1689
2193
|
return printTable(
|
|
@@ -1733,14 +2237,204 @@ async function runList(options = {}) {
|
|
|
1733
2237
|
const data = buildListData(scanResult, view);
|
|
1734
2238
|
return {
|
|
1735
2239
|
data,
|
|
1736
|
-
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)
|
|
1737
2367
|
};
|
|
1738
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
|
+
}
|
|
1739
2403
|
|
|
1740
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
|
+
}
|
|
1741
2420
|
function buildCli() {
|
|
1742
2421
|
const program = new Command();
|
|
1743
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
|
+
});
|
|
1744
2438
|
program.command("scan").option("--json", "Emit structured JSON output").action(async (options) => {
|
|
1745
2439
|
const result = await runScan({ json: options.json === true });
|
|
1746
2440
|
process.stdout.write(result.output);
|
|
@@ -1798,17 +2492,48 @@ function buildCli() {
|
|
|
1798
2492
|
});
|
|
1799
2493
|
process.stdout.write(result.output);
|
|
1800
2494
|
});
|
|
1801
|
-
|
|
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) => {
|
|
1802
2516
|
const result = await runEnable({
|
|
1803
|
-
skill: options.skill,
|
|
1804
|
-
|
|
2517
|
+
skill: requireSingleValue(options.skill, "skill"),
|
|
2518
|
+
agents: requireAtLeastOneValue(options.agent, "agent")
|
|
1805
2519
|
});
|
|
1806
2520
|
process.stdout.write(result.output);
|
|
1807
2521
|
});
|
|
1808
|
-
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) => {
|
|
1809
2523
|
const result = await runDisable({
|
|
1810
|
-
skill: options.skill,
|
|
1811
|
-
|
|
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
|
|
1812
2537
|
});
|
|
1813
2538
|
process.stdout.write(result.output);
|
|
1814
2539
|
});
|