facult 1.0.3 → 1.1.0

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/src/manage.ts CHANGED
@@ -8,8 +8,15 @@ import {
8
8
  symlink,
9
9
  } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
- import { dirname, join } from "node:path";
11
+ import { basename, dirname, join } from "node:path";
12
12
  import { getAdapter } from "./adapters";
13
+ import { renderCanonicalText } from "./agents";
14
+ import { ensureAiIndexPath } from "./ai-state";
15
+ import {
16
+ syncToolConfig,
17
+ syncToolGlobalDocs,
18
+ syncToolRules,
19
+ } from "./global-docs";
13
20
  import { facultRootDir } from "./paths";
14
21
 
15
22
  export interface ManagedToolState {
@@ -17,8 +24,19 @@ export interface ManagedToolState {
17
24
  managedAt: string;
18
25
  skillsDir?: string;
19
26
  mcpConfig?: string;
27
+ agentsDir?: string;
28
+ toolHome?: string;
29
+ globalAgentsPath?: string;
30
+ globalAgentsOverridePath?: string;
31
+ rulesDir?: string;
32
+ toolConfig?: string;
20
33
  skillsBackup?: string | null;
21
34
  mcpBackup?: string | null;
35
+ agentsBackup?: string | null;
36
+ globalAgentsBackup?: string | null;
37
+ globalAgentsOverrideBackup?: string | null;
38
+ rulesBackup?: string | null;
39
+ toolConfigBackup?: string | null;
22
40
  }
23
41
 
24
42
  export interface ManagedState {
@@ -30,6 +48,10 @@ export interface ToolPaths {
30
48
  tool: string;
31
49
  skillsDir?: string;
32
50
  mcpConfig?: string;
51
+ agentsDir?: string;
52
+ toolHome?: string;
53
+ rulesDir?: string;
54
+ toolConfig?: string;
33
55
  }
34
56
 
35
57
  export interface ManageOptions {
@@ -90,6 +112,10 @@ function defaultToolPaths(home: string): Record<string, ToolPaths> {
90
112
  tool: "codex",
91
113
  skillsDir: homePath(home, ".codex", "skills"),
92
114
  mcpConfig: homePath(home, ".codex", "mcp.json"),
115
+ agentsDir: homePath(home, ".codex", "agents"),
116
+ toolHome: homePath(home, ".codex"),
117
+ rulesDir: homePath(home, ".codex", "rules"),
118
+ toolConfig: homePath(home, ".codex", "config.toml"),
93
119
  },
94
120
  claude: {
95
121
  tool: "claude",
@@ -130,13 +156,18 @@ function defaultToolPaths(home: string): Record<string, ToolPaths> {
130
156
  }
131
157
  const paths = adapter.getDefaultPaths();
132
158
  const rawSkills = paths?.skills;
159
+ const rawAgents = paths?.agents;
133
160
  const skillsDir = Array.isArray(rawSkills)
134
161
  ? rawSkills[0]
135
162
  : (rawSkills ?? undefined);
163
+ const agentsDir = Array.isArray(rawAgents)
164
+ ? rawAgents[0]
165
+ : (rawAgents ?? undefined);
136
166
 
137
167
  return {
138
168
  tool,
139
169
  skillsDir: skillsDir ? expandHomePath(skillsDir, home) : undefined,
170
+ agentsDir: agentsDir ? expandHomePath(agentsDir, home) : undefined,
140
171
  mcpConfig: paths?.mcp ? expandHomePath(paths.mcp, home) : undefined,
141
172
  };
142
173
  };
@@ -255,6 +286,154 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
255
286
  return !!v && typeof v === "object" && !Array.isArray(v);
256
287
  }
257
288
 
289
+ async function readTextIfExists(p: string): Promise<string | null> {
290
+ if (!(await fileExists(p))) {
291
+ return null;
292
+ }
293
+ return await Bun.file(p).text();
294
+ }
295
+
296
+ async function loadCanonicalAgents(
297
+ rootDir: string
298
+ ): Promise<{ name: string; sourcePath: string; raw: string }[]> {
299
+ const agentsRoot = homePath(rootDir, "agents");
300
+ const entries = await readdir(agentsRoot, { withFileTypes: true }).catch(
301
+ () => [] as import("node:fs").Dirent[]
302
+ );
303
+ const out: { name: string; sourcePath: string; raw: string }[] = [];
304
+
305
+ for (const entry of entries) {
306
+ if (entry.name.startsWith(".")) {
307
+ continue;
308
+ }
309
+ const sourcePath = entry.isDirectory()
310
+ ? homePath(agentsRoot, entry.name, "agent.toml")
311
+ : entry.isFile() && entry.name.endsWith(".toml")
312
+ ? homePath(agentsRoot, entry.name)
313
+ : null;
314
+ if (!sourcePath) {
315
+ continue;
316
+ }
317
+ const raw = await readTextIfExists(sourcePath);
318
+ if (raw == null) {
319
+ continue;
320
+ }
321
+ const name = entry.isDirectory()
322
+ ? entry.name
323
+ : basename(entry.name, ".toml");
324
+ out.push({
325
+ name,
326
+ sourcePath,
327
+ raw,
328
+ });
329
+ }
330
+
331
+ return out.sort((a, b) => a.name.localeCompare(b.name));
332
+ }
333
+
334
+ async function planAgentFileChanges({
335
+ agentsDir,
336
+ homeDir,
337
+ rootDir,
338
+ tool,
339
+ }: {
340
+ agentsDir: string;
341
+ homeDir: string;
342
+ rootDir: string;
343
+ tool: string;
344
+ }): Promise<{
345
+ add: string[];
346
+ remove: string[];
347
+ contents: Map<string, string>;
348
+ }> {
349
+ const agents = await loadCanonicalAgents(rootDir);
350
+ const contents = new Map<string, string>();
351
+ const desiredPaths = new Set<string>();
352
+
353
+ for (const agent of agents) {
354
+ const target = homePath(agentsDir, `${agent.name}.toml`);
355
+ const rendered = await renderCanonicalText(agent.raw, {
356
+ homeDir,
357
+ rootDir,
358
+ targetTool: tool,
359
+ targetPath: target,
360
+ });
361
+ desiredPaths.add(target);
362
+ contents.set(target, rendered);
363
+ }
364
+
365
+ const existing = await readdir(agentsDir, { withFileTypes: true }).catch(
366
+ () => [] as import("node:fs").Dirent[]
367
+ );
368
+ const add = new Set<string>();
369
+ const remove = new Set<string>();
370
+
371
+ for (const entry of existing) {
372
+ if (!(entry.isFile() && entry.name.endsWith(".toml"))) {
373
+ continue;
374
+ }
375
+ const p = homePath(agentsDir, entry.name);
376
+ if (!desiredPaths.has(p)) {
377
+ remove.add(p);
378
+ continue;
379
+ }
380
+ const desired = contents.get(p);
381
+ const current = await readTextIfExists(p);
382
+ if (desired != null && current !== desired) {
383
+ add.add(p);
384
+ }
385
+ }
386
+
387
+ for (const p of desiredPaths) {
388
+ const current = await readTextIfExists(p);
389
+ const desired = contents.get(p);
390
+ if (desired != null && current !== desired) {
391
+ add.add(p);
392
+ }
393
+ }
394
+
395
+ return {
396
+ add: Array.from(add).sort(),
397
+ remove: Array.from(remove).sort(),
398
+ contents,
399
+ };
400
+ }
401
+
402
+ async function syncAgentFiles({
403
+ agentsDir,
404
+ homeDir,
405
+ rootDir,
406
+ tool,
407
+ dryRun,
408
+ }: {
409
+ agentsDir: string;
410
+ homeDir: string;
411
+ rootDir: string;
412
+ tool: string;
413
+ dryRun?: boolean;
414
+ }): Promise<{ add: string[]; remove: string[] }> {
415
+ const plan = await planAgentFileChanges({
416
+ agentsDir,
417
+ homeDir,
418
+ rootDir,
419
+ tool,
420
+ });
421
+ if (dryRun) {
422
+ return { add: plan.add, remove: plan.remove };
423
+ }
424
+ await ensureDir(agentsDir);
425
+ for (const p of plan.remove) {
426
+ await rm(p, { force: true });
427
+ }
428
+ for (const p of plan.add) {
429
+ const desired = plan.contents.get(p);
430
+ if (desired != null) {
431
+ await Bun.write(p, desired.endsWith("\n") ? desired : `${desired}\n`);
432
+ }
433
+ }
434
+ return { add: plan.add, remove: plan.remove };
435
+ }
436
+
258
437
  async function listSkillDirs(skillsRoot: string): Promise<string[]> {
259
438
  try {
260
439
  const entries = await readdir(skillsRoot, { withFileTypes: true });
@@ -293,13 +472,19 @@ function skillNamesFromIndex(
293
472
  }
294
473
 
295
474
  async function loadEnabledSkillNames({
475
+ homeDir,
296
476
  rootDir,
297
477
  tool,
298
478
  }: {
479
+ homeDir: string;
299
480
  rootDir: string;
300
481
  tool: string;
301
482
  }): Promise<string[]> {
302
- const indexPath = join(rootDir, "index.json");
483
+ const { path: indexPath } = await ensureAiIndexPath({
484
+ homeDir,
485
+ rootDir,
486
+ repair: true,
487
+ });
303
488
  if (await fileExists(indexPath)) {
304
489
  try {
305
490
  const txt = await Bun.file(indexPath).text();
@@ -405,16 +590,22 @@ async function ensureEmptyDir(p: string) {
405
590
  }
406
591
 
407
592
  async function createSkillSymlinks({
593
+ homeDir,
408
594
  toolSkillsDir,
409
595
  rootDir,
410
596
  tool,
411
597
  }: {
598
+ homeDir: string;
412
599
  toolSkillsDir: string;
413
600
  rootDir: string;
414
601
  tool: string;
415
602
  }) {
416
603
  await ensureDir(toolSkillsDir);
417
- const skillNames = await loadEnabledSkillNames({ rootDir, tool });
604
+ const skillNames = await loadEnabledSkillNames({
605
+ homeDir,
606
+ rootDir,
607
+ tool,
608
+ });
418
609
  for (const name of skillNames) {
419
610
  const target = join(rootDir, "skills", name);
420
611
  if (!(await fileExists(target))) {
@@ -435,15 +626,17 @@ async function createSkillSymlinks({
435
626
  }
436
627
 
437
628
  async function planSkillSymlinkChanges({
629
+ homeDir,
438
630
  toolSkillsDir,
439
631
  rootDir,
440
632
  tool,
441
633
  }: {
634
+ homeDir: string;
442
635
  toolSkillsDir: string;
443
636
  rootDir: string;
444
637
  tool: string;
445
638
  }): Promise<{ add: string[]; remove: string[] }> {
446
- const desired = await loadEnabledSkillNames({ rootDir, tool });
639
+ const desired = await loadEnabledSkillNames({ homeDir, rootDir, tool });
447
640
  const desiredSet = new Set(desired);
448
641
  const existing = await readdir(toolSkillsDir, { withFileTypes: true }).catch(
449
642
  () => [] as import("node:fs").Dirent[]
@@ -493,17 +686,24 @@ async function planSkillSymlinkChanges({
493
686
  }
494
687
 
495
688
  async function syncSkillSymlinks({
689
+ homeDir,
496
690
  toolSkillsDir,
497
691
  rootDir,
498
692
  tool,
499
693
  dryRun,
500
694
  }: {
695
+ homeDir: string;
501
696
  toolSkillsDir: string;
502
697
  rootDir: string;
503
698
  tool: string;
504
699
  dryRun?: boolean;
505
700
  }): Promise<{ add: string[]; remove: string[] }> {
506
- const plan = await planSkillSymlinkChanges({ toolSkillsDir, rootDir, tool });
701
+ const plan = await planSkillSymlinkChanges({
702
+ homeDir,
703
+ toolSkillsDir,
704
+ rootDir,
705
+ tool,
706
+ });
507
707
  if (dryRun) {
508
708
  return plan;
509
709
  }
@@ -601,6 +801,33 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
601
801
  if (!toolPaths) {
602
802
  throw new Error(`Unknown tool: ${tool}`);
603
803
  }
804
+ const globalDocsPreview = toolPaths.toolHome
805
+ ? await syncToolGlobalDocs({
806
+ homeDir: home,
807
+ rootDir,
808
+ tool,
809
+ toolHome: toolPaths.toolHome,
810
+ dryRun: true,
811
+ })
812
+ : null;
813
+ const rulesPreview = toolPaths.rulesDir
814
+ ? await syncToolRules({
815
+ homeDir: home,
816
+ rootDir,
817
+ tool,
818
+ rulesDir: toolPaths.rulesDir,
819
+ dryRun: true,
820
+ })
821
+ : null;
822
+ const toolConfigPreview = toolPaths.toolConfig
823
+ ? await syncToolConfig({
824
+ homeDir: home,
825
+ rootDir,
826
+ tool,
827
+ toolConfigPath: toolPaths.toolConfig,
828
+ dryRun: true,
829
+ })
830
+ : null;
604
831
 
605
832
  const skillsBackup = toolPaths.skillsDir
606
833
  ? await backupPath(toolPaths.skillsDir, opts.now)
@@ -608,16 +835,49 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
608
835
  const mcpBackup = toolPaths.mcpConfig
609
836
  ? await backupPath(toolPaths.mcpConfig, opts.now)
610
837
  : null;
838
+ const agentsBackup = toolPaths.agentsDir
839
+ ? await backupPath(toolPaths.agentsDir, opts.now)
840
+ : null;
841
+ const globalAgentsBackup =
842
+ toolPaths.toolHome &&
843
+ globalDocsPreview?.managedTargets.includes(
844
+ join(toolPaths.toolHome, "AGENTS.md")
845
+ )
846
+ ? await backupPath(join(toolPaths.toolHome, "AGENTS.md"), opts.now)
847
+ : null;
848
+ const globalAgentsOverrideBackup =
849
+ toolPaths.toolHome &&
850
+ globalDocsPreview?.managedTargets.includes(
851
+ join(toolPaths.toolHome, "AGENTS.override.md")
852
+ )
853
+ ? await backupPath(
854
+ join(toolPaths.toolHome, "AGENTS.override.md"),
855
+ opts.now
856
+ )
857
+ : null;
858
+ const rulesBackup =
859
+ toolPaths.rulesDir && rulesPreview?.managedRulesDir
860
+ ? await backupPath(toolPaths.rulesDir, opts.now)
861
+ : null;
862
+ const toolConfigBackup =
863
+ toolPaths.toolConfig && toolConfigPreview?.managedConfig
864
+ ? await backupPath(toolPaths.toolConfig, opts.now)
865
+ : null;
611
866
 
612
867
  if (toolPaths.skillsDir) {
613
868
  await ensureEmptyDir(toolPaths.skillsDir);
614
869
  await createSkillSymlinks({
870
+ homeDir: home,
615
871
  toolSkillsDir: toolPaths.skillsDir,
616
872
  rootDir,
617
873
  tool,
618
874
  });
619
875
  }
620
876
 
877
+ if (toolPaths.agentsDir) {
878
+ await ensureEmptyDir(toolPaths.agentsDir);
879
+ }
880
+
621
881
  if (toolPaths.mcpConfig) {
622
882
  await writeToolMcpConfig({
623
883
  mcpConfigPath: toolPaths.mcpConfig,
@@ -626,13 +886,76 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
626
886
  });
627
887
  }
628
888
 
889
+ if (toolPaths.agentsDir) {
890
+ await syncAgentFiles({
891
+ agentsDir: toolPaths.agentsDir,
892
+ homeDir: home,
893
+ rootDir,
894
+ tool,
895
+ });
896
+ }
897
+
898
+ if (toolPaths.toolHome && globalDocsPreview) {
899
+ await ensureDir(toolPaths.toolHome);
900
+ await syncToolGlobalDocs({
901
+ homeDir: home,
902
+ rootDir,
903
+ tool,
904
+ toolHome: toolPaths.toolHome,
905
+ });
906
+ }
907
+
908
+ if (toolPaths.rulesDir && rulesPreview?.managedRulesDir) {
909
+ await ensureEmptyDir(toolPaths.rulesDir);
910
+ await syncToolRules({
911
+ homeDir: home,
912
+ rootDir,
913
+ tool,
914
+ rulesDir: toolPaths.rulesDir,
915
+ previouslyManaged: true,
916
+ });
917
+ }
918
+
919
+ if (toolPaths.toolConfig && toolConfigPreview?.managedConfig) {
920
+ await syncToolConfig({
921
+ homeDir: home,
922
+ rootDir,
923
+ tool,
924
+ toolConfigPath: toolPaths.toolConfig,
925
+ existingConfigPath: toolConfigBackup ?? undefined,
926
+ });
927
+ }
928
+
629
929
  state.tools[tool] = {
630
930
  tool,
631
931
  managedAt: nowIso(opts.now),
632
932
  skillsDir: toolPaths.skillsDir,
633
933
  mcpConfig: toolPaths.mcpConfig,
934
+ agentsDir: toolPaths.agentsDir,
935
+ toolHome: globalDocsPreview?.managedTargets.length
936
+ ? toolPaths.toolHome
937
+ : undefined,
938
+ globalAgentsPath: globalDocsPreview?.managedTargets.includes(
939
+ join(toolPaths.toolHome ?? "", "AGENTS.md")
940
+ )
941
+ ? join(toolPaths.toolHome ?? "", "AGENTS.md")
942
+ : undefined,
943
+ globalAgentsOverridePath: globalDocsPreview?.managedTargets.includes(
944
+ join(toolPaths.toolHome ?? "", "AGENTS.override.md")
945
+ )
946
+ ? join(toolPaths.toolHome ?? "", "AGENTS.override.md")
947
+ : undefined,
948
+ rulesDir: rulesPreview?.managedRulesDir ? toolPaths.rulesDir : undefined,
949
+ toolConfig: toolConfigPreview?.managedConfig
950
+ ? toolPaths.toolConfig
951
+ : undefined,
634
952
  skillsBackup,
635
953
  mcpBackup,
954
+ agentsBackup,
955
+ globalAgentsBackup,
956
+ globalAgentsOverrideBackup,
957
+ rulesBackup,
958
+ toolConfigBackup,
636
959
  };
637
960
 
638
961
  await saveManagedState(state, home);
@@ -697,6 +1020,41 @@ export async function unmanageTool(tool: string, opts: ManageOptions = {}) {
697
1020
  });
698
1021
  }
699
1022
 
1023
+ if (entry.agentsDir) {
1024
+ await restoreBackup({
1025
+ original: entry.agentsDir,
1026
+ backup: entry.agentsBackup ?? null,
1027
+ });
1028
+ }
1029
+
1030
+ if (entry.globalAgentsPath) {
1031
+ await restoreBackup({
1032
+ original: entry.globalAgentsPath,
1033
+ backup: entry.globalAgentsBackup ?? null,
1034
+ });
1035
+ }
1036
+
1037
+ if (entry.globalAgentsOverridePath) {
1038
+ await restoreBackup({
1039
+ original: entry.globalAgentsOverridePath,
1040
+ backup: entry.globalAgentsOverrideBackup ?? null,
1041
+ });
1042
+ }
1043
+
1044
+ if (entry.rulesDir) {
1045
+ await restoreBackup({
1046
+ original: entry.rulesDir,
1047
+ backup: entry.rulesBackup ?? null,
1048
+ });
1049
+ }
1050
+
1051
+ if (entry.toolConfig) {
1052
+ await restoreBackup({
1053
+ original: entry.toolConfig,
1054
+ backup: entry.toolConfigBackup ?? null,
1055
+ });
1056
+ }
1057
+
700
1058
  const nextTools: ManagedState["tools"] = {};
701
1059
  for (const [name, config] of Object.entries(state.tools)) {
702
1060
  if (name === tool) {
@@ -715,16 +1073,122 @@ export async function listManagedTools(
715
1073
  return Object.keys(state.tools).sort();
716
1074
  }
717
1075
 
1076
+ async function canonicalAgentsExist(rootDir: string): Promise<boolean> {
1077
+ try {
1078
+ const agents = await loadCanonicalAgents(rootDir);
1079
+ return agents.length > 0;
1080
+ } catch {
1081
+ return false;
1082
+ }
1083
+ }
1084
+
1085
+ async function repairManagedToolEntry(args: {
1086
+ homeDir: string;
1087
+ rootDir: string;
1088
+ tool: string;
1089
+ entry: ManagedToolState;
1090
+ }): Promise<{ entry: ManagedToolState; changed: boolean }> {
1091
+ const { homeDir, rootDir, tool } = args;
1092
+ const toolPaths = await resolveToolPaths(tool, homeDir);
1093
+ if (!toolPaths) {
1094
+ return { entry: args.entry, changed: false };
1095
+ }
1096
+
1097
+ const next: ManagedToolState = { ...args.entry };
1098
+ let changed = false;
1099
+
1100
+ if (
1101
+ !next.agentsDir &&
1102
+ toolPaths.agentsDir &&
1103
+ (await canonicalAgentsExist(rootDir))
1104
+ ) {
1105
+ next.agentsBackup = await backupPath(toolPaths.agentsDir);
1106
+ next.agentsDir = toolPaths.agentsDir;
1107
+ changed = true;
1108
+ }
1109
+
1110
+ if (toolPaths.toolHome && !next.toolHome) {
1111
+ const preview = await syncToolGlobalDocs({
1112
+ homeDir,
1113
+ rootDir,
1114
+ tool,
1115
+ toolHome: toolPaths.toolHome,
1116
+ dryRun: true,
1117
+ });
1118
+ if (preview.managedTargets.length > 0) {
1119
+ next.toolHome = toolPaths.toolHome;
1120
+ const agentsPath = join(toolPaths.toolHome, "AGENTS.md");
1121
+ const overridePath = join(toolPaths.toolHome, "AGENTS.override.md");
1122
+ if (
1123
+ preview.managedTargets.includes(agentsPath) &&
1124
+ !next.globalAgentsPath
1125
+ ) {
1126
+ next.globalAgentsBackup = await backupPath(agentsPath);
1127
+ next.globalAgentsPath = agentsPath;
1128
+ changed = true;
1129
+ }
1130
+ if (
1131
+ preview.managedTargets.includes(overridePath) &&
1132
+ !next.globalAgentsOverridePath
1133
+ ) {
1134
+ next.globalAgentsOverrideBackup = await backupPath(overridePath);
1135
+ next.globalAgentsOverridePath = overridePath;
1136
+ changed = true;
1137
+ }
1138
+ }
1139
+ }
1140
+
1141
+ if (!next.rulesDir && toolPaths.rulesDir) {
1142
+ const preview = await syncToolRules({
1143
+ homeDir,
1144
+ rootDir,
1145
+ tool,
1146
+ rulesDir: toolPaths.rulesDir,
1147
+ dryRun: true,
1148
+ });
1149
+ if (preview.managedRulesDir) {
1150
+ next.rulesBackup = await backupPath(toolPaths.rulesDir);
1151
+ next.rulesDir = toolPaths.rulesDir;
1152
+ changed = true;
1153
+ }
1154
+ }
1155
+
1156
+ if (!next.toolConfig && toolPaths.toolConfig) {
1157
+ const preview = await syncToolConfig({
1158
+ homeDir,
1159
+ rootDir,
1160
+ tool,
1161
+ toolConfigPath: toolPaths.toolConfig,
1162
+ dryRun: true,
1163
+ });
1164
+ if (preview.managedConfig) {
1165
+ next.toolConfigBackup = await backupPath(toolPaths.toolConfig);
1166
+ next.toolConfig = toolPaths.toolConfig;
1167
+ changed = true;
1168
+ }
1169
+ }
1170
+
1171
+ return { entry: next, changed };
1172
+ }
1173
+
718
1174
  function logSyncDryRun({
719
1175
  tool,
720
1176
  entry,
721
1177
  skillPlan,
722
1178
  mcpPlan,
1179
+ agentPlan,
1180
+ globalDocsPlan,
1181
+ rulesPlan,
1182
+ configPlan,
723
1183
  }: {
724
1184
  tool: string;
725
1185
  entry: ManagedToolState;
726
1186
  skillPlan: { add: string[]; remove: string[] };
727
1187
  mcpPlan: { needsWrite: boolean };
1188
+ agentPlan: { add: string[]; remove: string[] };
1189
+ globalDocsPlan: { write: string[]; remove: string[] };
1190
+ rulesPlan: { write: string[]; remove: string[] };
1191
+ configPlan: { write: boolean; remove: boolean; targetPath: string };
728
1192
  }) {
729
1193
  for (const name of skillPlan.add) {
730
1194
  console.log(`${tool}: would add skill ${name}`);
@@ -732,12 +1196,44 @@ function logSyncDryRun({
732
1196
  for (const name of skillPlan.remove) {
733
1197
  console.log(`${tool}: would remove skill ${name}`);
734
1198
  }
1199
+ for (const p of agentPlan.add) {
1200
+ console.log(`${tool}: would write agent ${p}`);
1201
+ }
1202
+ for (const p of agentPlan.remove) {
1203
+ console.log(`${tool}: would remove agent ${p}`);
1204
+ }
1205
+ for (const p of globalDocsPlan.write) {
1206
+ console.log(`${tool}: would write global doc ${p}`);
1207
+ }
1208
+ for (const p of globalDocsPlan.remove) {
1209
+ console.log(`${tool}: would remove global doc ${p}`);
1210
+ }
1211
+ for (const p of rulesPlan.write) {
1212
+ console.log(`${tool}: would write rule ${p}`);
1213
+ }
1214
+ for (const p of rulesPlan.remove) {
1215
+ console.log(`${tool}: would remove rule ${p}`);
1216
+ }
1217
+ if (configPlan.write) {
1218
+ console.log(`${tool}: would write tool config ${configPlan.targetPath}`);
1219
+ }
1220
+ if (configPlan.remove) {
1221
+ console.log(`${tool}: would remove tool config ${configPlan.targetPath}`);
1222
+ }
735
1223
  if (mcpPlan.needsWrite && entry.mcpConfig) {
736
1224
  console.log(`${tool}: would update mcp config ${entry.mcpConfig}`);
737
1225
  }
738
1226
  if (
739
1227
  skillPlan.add.length === 0 &&
740
1228
  skillPlan.remove.length === 0 &&
1229
+ agentPlan.add.length === 0 &&
1230
+ agentPlan.remove.length === 0 &&
1231
+ globalDocsPlan.write.length === 0 &&
1232
+ globalDocsPlan.remove.length === 0 &&
1233
+ rulesPlan.write.length === 0 &&
1234
+ rulesPlan.remove.length === 0 &&
1235
+ !configPlan.write &&
1236
+ !configPlan.remove &&
741
1237
  !mcpPlan.needsWrite
742
1238
  ) {
743
1239
  console.log(`${tool}: no changes`);
@@ -745,11 +1241,13 @@ function logSyncDryRun({
745
1241
  }
746
1242
 
747
1243
  async function syncManagedToolEntry({
1244
+ homeDir,
748
1245
  tool,
749
1246
  entry,
750
1247
  rootDir,
751
1248
  dryRun,
752
1249
  }: {
1250
+ homeDir: string;
753
1251
  tool: string;
754
1252
  entry: ManagedToolState;
755
1253
  rootDir: string;
@@ -757,6 +1255,7 @@ async function syncManagedToolEntry({
757
1255
  }) {
758
1256
  const skillPlan = entry.skillsDir
759
1257
  ? await syncSkillSymlinks({
1258
+ homeDir,
760
1259
  toolSkillsDir: entry.skillsDir,
761
1260
  rootDir,
762
1261
  tool,
@@ -764,6 +1263,16 @@ async function syncManagedToolEntry({
764
1263
  })
765
1264
  : { add: [], remove: [] };
766
1265
 
1266
+ const agentPlan = entry.agentsDir
1267
+ ? await syncAgentFiles({
1268
+ agentsDir: entry.agentsDir,
1269
+ homeDir,
1270
+ rootDir,
1271
+ tool,
1272
+ dryRun,
1273
+ })
1274
+ : { add: [], remove: [] };
1275
+
767
1276
  const mcpPlan = entry.mcpConfig
768
1277
  ? await syncMcpConfig({
769
1278
  mcpConfigPath: entry.mcpConfig,
@@ -773,8 +1282,60 @@ async function syncManagedToolEntry({
773
1282
  })
774
1283
  : { needsWrite: false };
775
1284
 
1285
+ const globalDocsPlan = entry.toolHome
1286
+ ? await syncToolGlobalDocs({
1287
+ homeDir,
1288
+ rootDir,
1289
+ tool,
1290
+ toolHome: entry.toolHome,
1291
+ previouslyManagedTargets: [
1292
+ entry.globalAgentsPath,
1293
+ entry.globalAgentsOverridePath,
1294
+ ].filter((value): value is string => Boolean(value)),
1295
+ dryRun,
1296
+ })
1297
+ : { write: [], remove: [], contents: new Map(), managedTargets: [] };
1298
+
1299
+ const rulesPlan = entry.rulesDir
1300
+ ? await syncToolRules({
1301
+ homeDir,
1302
+ rootDir,
1303
+ tool,
1304
+ rulesDir: entry.rulesDir,
1305
+ previouslyManaged: true,
1306
+ dryRun,
1307
+ })
1308
+ : { write: [], remove: [], contents: new Map(), managedRulesDir: false };
1309
+
1310
+ const configPlan = entry.toolConfig
1311
+ ? await syncToolConfig({
1312
+ homeDir,
1313
+ rootDir,
1314
+ tool,
1315
+ toolConfigPath: entry.toolConfig,
1316
+ existingConfigPath: entry.toolConfigBackup ?? undefined,
1317
+ previouslyManaged: true,
1318
+ dryRun,
1319
+ })
1320
+ : {
1321
+ write: false,
1322
+ remove: false,
1323
+ contents: null,
1324
+ managedConfig: false,
1325
+ targetPath: "",
1326
+ };
1327
+
776
1328
  if (dryRun) {
777
- logSyncDryRun({ tool, entry, skillPlan, mcpPlan });
1329
+ logSyncDryRun({
1330
+ tool,
1331
+ entry,
1332
+ skillPlan,
1333
+ mcpPlan,
1334
+ agentPlan,
1335
+ globalDocsPlan,
1336
+ rulesPlan,
1337
+ configPlan,
1338
+ });
778
1339
  } else {
779
1340
  console.log(`${tool} synced`);
780
1341
  }
@@ -790,12 +1351,36 @@ export async function syncManagedTools(opts: SyncOptions = {}) {
790
1351
  throw new Error("No managed tools to sync.");
791
1352
  }
792
1353
 
1354
+ if (!opts.dryRun) {
1355
+ let changed = false;
1356
+ for (const tool of tools) {
1357
+ const entry = state.tools[tool];
1358
+ if (!entry) {
1359
+ throw new Error(`${tool} is not managed`);
1360
+ }
1361
+ const repaired = await repairManagedToolEntry({
1362
+ homeDir: home,
1363
+ rootDir,
1364
+ tool,
1365
+ entry,
1366
+ });
1367
+ if (repaired.changed) {
1368
+ state.tools[tool] = repaired.entry;
1369
+ changed = true;
1370
+ }
1371
+ }
1372
+ if (changed) {
1373
+ await saveManagedState(state, home);
1374
+ }
1375
+ }
1376
+
793
1377
  for (const tool of tools) {
794
1378
  const entry = state.tools[tool];
795
1379
  if (!entry) {
796
1380
  throw new Error(`${tool} is not managed`);
797
1381
  }
798
1382
  await syncManagedToolEntry({
1383
+ homeDir: home,
799
1384
  tool,
800
1385
  entry,
801
1386
  rootDir,