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