skillmux 0.1.0 → 0.1.2

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