facult 2.7.3 → 2.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -1
- package/src/ai.ts +34 -3
- package/src/doctor.ts +81 -6
- package/src/index.ts +116 -21
- package/src/manage.ts +724 -71
- package/src/paths.ts +21 -1
- package/src/project-sync.ts +15 -2
- package/src/scan.ts +5 -1
package/src/manage.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
|
+
appendFile,
|
|
3
4
|
cp,
|
|
4
5
|
lstat,
|
|
5
6
|
mkdir,
|
|
@@ -83,6 +84,29 @@ export interface ManagedState {
|
|
|
83
84
|
tools: Record<string, ManagedToolState>;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
type SyncLedgerAction = "add" | "adopt" | "remove" | "skip" | "write";
|
|
88
|
+
|
|
89
|
+
interface SyncLedgerEvent {
|
|
90
|
+
version: 1;
|
|
91
|
+
id: string;
|
|
92
|
+
correlationId: string;
|
|
93
|
+
ts: string;
|
|
94
|
+
command: "sync";
|
|
95
|
+
phase: "plan" | "apply";
|
|
96
|
+
dryRun: boolean;
|
|
97
|
+
tool: string;
|
|
98
|
+
rootDir: string;
|
|
99
|
+
scope: "global" | "project";
|
|
100
|
+
actor: string;
|
|
101
|
+
action: SyncLedgerAction;
|
|
102
|
+
targetPath?: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
sourcePath?: string;
|
|
105
|
+
oldHash?: string;
|
|
106
|
+
newHash?: string;
|
|
107
|
+
reason: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
export interface ToolPaths {
|
|
87
111
|
tool: string;
|
|
88
112
|
skillsDir?: string;
|
|
@@ -186,6 +210,51 @@ async function ensureDir(p: string) {
|
|
|
186
210
|
await mkdir(p, { recursive: true });
|
|
187
211
|
}
|
|
188
212
|
|
|
213
|
+
async function hasCanonicalSource(rootDir: string): Promise<boolean> {
|
|
214
|
+
const fileCandidates = [
|
|
215
|
+
"config.toml",
|
|
216
|
+
"config.local.toml",
|
|
217
|
+
"AGENTS.global.md",
|
|
218
|
+
"AGENTS.override.global.md",
|
|
219
|
+
];
|
|
220
|
+
for (const relPath of fileCandidates) {
|
|
221
|
+
if (await fileExists(join(rootDir, relPath))) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const dirCandidates = [
|
|
227
|
+
"agents",
|
|
228
|
+
"automations",
|
|
229
|
+
"instructions",
|
|
230
|
+
"mcp",
|
|
231
|
+
"rules",
|
|
232
|
+
"skills",
|
|
233
|
+
"snippets",
|
|
234
|
+
"tools",
|
|
235
|
+
];
|
|
236
|
+
for (const relPath of dirCandidates) {
|
|
237
|
+
const entries = await readdir(join(rootDir, relPath)).catch(
|
|
238
|
+
() => [] as string[]
|
|
239
|
+
);
|
|
240
|
+
if (entries.some((entry) => !entry.startsWith("."))) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function isGeneratedOnlyProjectRoot(args: {
|
|
249
|
+
homeDir: string;
|
|
250
|
+
rootDir: string;
|
|
251
|
+
}): Promise<boolean> {
|
|
252
|
+
if (projectRootFromAiRoot(args.rootDir, args.homeDir) == null) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
return !(await hasCanonicalSource(args.rootDir));
|
|
256
|
+
}
|
|
257
|
+
|
|
189
258
|
function renderedSourceKindForPath(
|
|
190
259
|
sourcePath: string
|
|
191
260
|
): ManagedRenderedTargetState["sourceKind"] {
|
|
@@ -414,13 +483,7 @@ async function resolveToolPaths(
|
|
|
414
483
|
return base;
|
|
415
484
|
}
|
|
416
485
|
|
|
417
|
-
const adapterPaths = getAdapter("codex")?.getDefaultPaths?.();
|
|
418
|
-
const adapterConfig = adapterPaths?.config
|
|
419
|
-
? expandHomePath(adapterPaths.config, home)
|
|
420
|
-
: null;
|
|
421
|
-
|
|
422
486
|
const candidates = [
|
|
423
|
-
adapterConfig,
|
|
424
487
|
homePath(home, ".config", "openai", "codex.json"),
|
|
425
488
|
homePath(home, ".codex", "config.json"),
|
|
426
489
|
homePath(home, ".codex", "mcp.json"),
|
|
@@ -446,6 +509,30 @@ export function managedStatePathForRoot(
|
|
|
446
509
|
return join(facultMachineStateDir(home, rootDir), "managed.json");
|
|
447
510
|
}
|
|
448
511
|
|
|
512
|
+
function syncLedgerPathForRoot(
|
|
513
|
+
home: string = homedir(),
|
|
514
|
+
rootDir?: string
|
|
515
|
+
): string {
|
|
516
|
+
return join(facultMachineStateDir(home, rootDir), "ledger", "sync.jsonl");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function appendSyncLedgerEvents(args: {
|
|
520
|
+
homeDir: string;
|
|
521
|
+
rootDir: string;
|
|
522
|
+
events: SyncLedgerEvent[];
|
|
523
|
+
}) {
|
|
524
|
+
if (args.events.length === 0) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const pathValue = syncLedgerPathForRoot(args.homeDir, args.rootDir);
|
|
528
|
+
await ensureDir(dirname(pathValue));
|
|
529
|
+
await appendFile(
|
|
530
|
+
pathValue,
|
|
531
|
+
`${args.events.map((event) => JSON.stringify(event)).join("\n")}\n`,
|
|
532
|
+
"utf8"
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
449
536
|
function legacyManagedStatePathForRoot(
|
|
450
537
|
home: string = homedir(),
|
|
451
538
|
rootDir?: string
|
|
@@ -720,6 +807,23 @@ async function loadCanonicalAutomations(
|
|
|
720
807
|
return await loadAutomationEntries(join(rootDir, "automations"));
|
|
721
808
|
}
|
|
722
809
|
|
|
810
|
+
async function loadRenderableCanonicalAutomations(args: {
|
|
811
|
+
homeDir: string;
|
|
812
|
+
rootDir: string;
|
|
813
|
+
tool: string;
|
|
814
|
+
}): Promise<AutomationEntry[]> {
|
|
815
|
+
const policy = await loadProjectToolSyncPolicy(args);
|
|
816
|
+
const automations = await loadCanonicalAutomations(args.rootDir);
|
|
817
|
+
if (!policy) {
|
|
818
|
+
return automations;
|
|
819
|
+
}
|
|
820
|
+
return automations.filter(
|
|
821
|
+
(automation) =>
|
|
822
|
+
policy.automations.includes("*") ||
|
|
823
|
+
policy.automations.includes(automation.name)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
723
827
|
function isAutomationRuntimeRelativePath(relPath: string): boolean {
|
|
724
828
|
return relPath === "memory.md";
|
|
725
829
|
}
|
|
@@ -839,7 +943,12 @@ async function loadCanonicalCodexMarketplaceText(
|
|
|
839
943
|
}
|
|
840
944
|
|
|
841
945
|
async function hashDirectoryTree(root: string): Promise<string | null> {
|
|
842
|
-
|
|
946
|
+
try {
|
|
947
|
+
const st = await lstat(root);
|
|
948
|
+
if (!st.isDirectory()) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
843
952
|
return null;
|
|
844
953
|
}
|
|
845
954
|
const files = await listRelativeFilesWithDotfiles(root);
|
|
@@ -1082,7 +1191,9 @@ async function syncAgentFiles({
|
|
|
1082
1191
|
|
|
1083
1192
|
async function planAutomationFileChanges(args: {
|
|
1084
1193
|
automationDir: string;
|
|
1194
|
+
homeDir: string;
|
|
1085
1195
|
rootDir: string;
|
|
1196
|
+
tool: string;
|
|
1086
1197
|
previouslyManagedTargets?: string[];
|
|
1087
1198
|
}): Promise<{
|
|
1088
1199
|
add: string[];
|
|
@@ -1090,7 +1201,7 @@ async function planAutomationFileChanges(args: {
|
|
|
1090
1201
|
contents: Map<string, string>;
|
|
1091
1202
|
sources: Map<string, string>;
|
|
1092
1203
|
}> {
|
|
1093
|
-
const automations = await
|
|
1204
|
+
const automations = await loadRenderableCanonicalAutomations(args);
|
|
1094
1205
|
const contents = new Map<string, string>();
|
|
1095
1206
|
const sources = new Map<string, string>();
|
|
1096
1207
|
const desiredPaths = new Set<string>();
|
|
@@ -1640,13 +1751,15 @@ async function adoptExistingToolAgents(args: {
|
|
|
1640
1751
|
}
|
|
1641
1752
|
|
|
1642
1753
|
async function planExistingAutomationAdoption(args: {
|
|
1754
|
+
homeDir: string;
|
|
1643
1755
|
rootDir: string;
|
|
1756
|
+
tool: string;
|
|
1644
1757
|
automationDir: string;
|
|
1645
1758
|
}): Promise<ExistingManagedImportPlan> {
|
|
1646
1759
|
const plan = emptyManagedImportPlan();
|
|
1647
1760
|
const liveAutomations = await loadAutomationEntries(args.automationDir);
|
|
1648
1761
|
const canonicalAutomations = new Map(
|
|
1649
|
-
(await
|
|
1762
|
+
(await loadRenderableCanonicalAutomations(args)).map((entry) => [
|
|
1650
1763
|
entry.name,
|
|
1651
1764
|
entry,
|
|
1652
1765
|
])
|
|
@@ -1674,7 +1787,9 @@ async function planExistingAutomationAdoption(args: {
|
|
|
1674
1787
|
}
|
|
1675
1788
|
|
|
1676
1789
|
async function adoptExistingAutomations(args: {
|
|
1790
|
+
homeDir: string;
|
|
1677
1791
|
rootDir: string;
|
|
1792
|
+
tool: string;
|
|
1678
1793
|
automationDir: string;
|
|
1679
1794
|
conflictMode: "keep-canonical" | "keep-existing";
|
|
1680
1795
|
}): Promise<ExistingManagedItem[]> {
|
|
@@ -1685,7 +1800,7 @@ async function adoptExistingAutomations(args: {
|
|
|
1685
1800
|
const adopted: ExistingManagedItem[] = [];
|
|
1686
1801
|
const liveAutomations = await loadAutomationEntries(args.automationDir);
|
|
1687
1802
|
const canonicalAutomations = new Map(
|
|
1688
|
-
(await
|
|
1803
|
+
(await loadRenderableCanonicalAutomations(args)).map((entry) => [
|
|
1689
1804
|
entry.name,
|
|
1690
1805
|
entry,
|
|
1691
1806
|
])
|
|
@@ -2136,6 +2251,20 @@ function isPreservedToolSkillEntry(name: string): boolean {
|
|
|
2136
2251
|
return name.startsWith(".");
|
|
2137
2252
|
}
|
|
2138
2253
|
|
|
2254
|
+
interface SkillSymlinkConflict {
|
|
2255
|
+
name: string;
|
|
2256
|
+
livePath: string;
|
|
2257
|
+
canonicalPath?: string;
|
|
2258
|
+
reason: "modified" | "unmanaged" | "disabled";
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
interface SkillSymlinkPlan {
|
|
2262
|
+
add: string[];
|
|
2263
|
+
remove: string[];
|
|
2264
|
+
conflicts: SkillSymlinkConflict[];
|
|
2265
|
+
adopted: SkillSymlinkConflict[];
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2139
2268
|
async function restorePreservedToolSkillEntries({
|
|
2140
2269
|
backupDir,
|
|
2141
2270
|
toolSkillsDir,
|
|
@@ -2170,7 +2299,7 @@ async function planSkillSymlinkChanges({
|
|
|
2170
2299
|
toolSkillsDir: string;
|
|
2171
2300
|
rootDir: string;
|
|
2172
2301
|
tool: string;
|
|
2173
|
-
}): Promise<
|
|
2302
|
+
}): Promise<SkillSymlinkPlan> {
|
|
2174
2303
|
const desiredEntries = await loadEnabledSkillEntries({
|
|
2175
2304
|
homeDir,
|
|
2176
2305
|
rootDir,
|
|
@@ -2186,16 +2315,46 @@ async function planSkillSymlinkChanges({
|
|
|
2186
2315
|
|
|
2187
2316
|
const remove: string[] = [];
|
|
2188
2317
|
const add: string[] = [];
|
|
2318
|
+
const conflicts: SkillSymlinkConflict[] = [];
|
|
2189
2319
|
|
|
2190
2320
|
for (const entry of existing) {
|
|
2191
2321
|
if (isPreservedToolSkillEntry(entry.name)) {
|
|
2192
2322
|
continue;
|
|
2193
2323
|
}
|
|
2324
|
+
const linkPath = join(toolSkillsDir, entry.name);
|
|
2194
2325
|
if (!desiredSet.has(entry.name)) {
|
|
2326
|
+
if (
|
|
2327
|
+
entry.isDirectory() &&
|
|
2328
|
+
(await fileExists(join(linkPath, "SKILL.md")))
|
|
2329
|
+
) {
|
|
2330
|
+
const disabledCanonicalPath = join(rootDir, "skills", entry.name);
|
|
2331
|
+
const [liveHash, canonicalHash] = await Promise.all([
|
|
2332
|
+
hashDirectoryTree(linkPath),
|
|
2333
|
+
hashDirectoryTree(disabledCanonicalPath),
|
|
2334
|
+
]);
|
|
2335
|
+
if (canonicalHash != null) {
|
|
2336
|
+
if (liveHash === canonicalHash) {
|
|
2337
|
+
remove.push(entry.name);
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
conflicts.push({
|
|
2341
|
+
name: entry.name,
|
|
2342
|
+
livePath: linkPath,
|
|
2343
|
+
canonicalPath: disabledCanonicalPath,
|
|
2344
|
+
reason: "disabled",
|
|
2345
|
+
});
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
conflicts.push({
|
|
2349
|
+
name: entry.name,
|
|
2350
|
+
livePath: linkPath,
|
|
2351
|
+
reason: "unmanaged",
|
|
2352
|
+
});
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2195
2355
|
remove.push(entry.name);
|
|
2196
2356
|
continue;
|
|
2197
2357
|
}
|
|
2198
|
-
const linkPath = join(toolSkillsDir, entry.name);
|
|
2199
2358
|
const target = desiredTargets.get(entry.name);
|
|
2200
2359
|
if (!target) {
|
|
2201
2360
|
remove.push(entry.name);
|
|
@@ -2204,6 +2363,27 @@ async function planSkillSymlinkChanges({
|
|
|
2204
2363
|
try {
|
|
2205
2364
|
const st = await lstat(linkPath);
|
|
2206
2365
|
if (!st.isSymbolicLink()) {
|
|
2366
|
+
if (
|
|
2367
|
+
st.isDirectory() &&
|
|
2368
|
+
(await fileExists(join(linkPath, "SKILL.md")))
|
|
2369
|
+
) {
|
|
2370
|
+
const [liveHash, canonicalHash] = await Promise.all([
|
|
2371
|
+
hashDirectoryTree(linkPath),
|
|
2372
|
+
hashDirectoryTree(target),
|
|
2373
|
+
]);
|
|
2374
|
+
if (
|
|
2375
|
+
liveHash != null &&
|
|
2376
|
+
(canonicalHash == null || liveHash !== canonicalHash)
|
|
2377
|
+
) {
|
|
2378
|
+
conflicts.push({
|
|
2379
|
+
name: entry.name,
|
|
2380
|
+
livePath: linkPath,
|
|
2381
|
+
canonicalPath: target,
|
|
2382
|
+
reason: canonicalHash == null ? "unmanaged" : "modified",
|
|
2383
|
+
});
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2207
2387
|
remove.push(entry.name);
|
|
2208
2388
|
add.push(entry.name);
|
|
2209
2389
|
continue;
|
|
@@ -2230,6 +2410,8 @@ async function planSkillSymlinkChanges({
|
|
|
2230
2410
|
return {
|
|
2231
2411
|
add: Array.from(new Set(add)).sort(),
|
|
2232
2412
|
remove: Array.from(new Set(remove)).sort(),
|
|
2413
|
+
conflicts: conflicts.sort((a, b) => a.name.localeCompare(b.name)),
|
|
2414
|
+
adopted: [],
|
|
2233
2415
|
};
|
|
2234
2416
|
}
|
|
2235
2417
|
|
|
@@ -2245,7 +2427,7 @@ async function syncSkillSymlinks({
|
|
|
2245
2427
|
rootDir: string;
|
|
2246
2428
|
tool: string;
|
|
2247
2429
|
dryRun?: boolean;
|
|
2248
|
-
}): Promise<
|
|
2430
|
+
}): Promise<SkillSymlinkPlan> {
|
|
2249
2431
|
const plan = await planSkillSymlinkChanges({
|
|
2250
2432
|
homeDir,
|
|
2251
2433
|
toolSkillsDir,
|
|
@@ -2256,6 +2438,23 @@ async function syncSkillSymlinks({
|
|
|
2256
2438
|
return plan;
|
|
2257
2439
|
}
|
|
2258
2440
|
|
|
2441
|
+
const adoptedConflicts = await adoptSkillSymlinkConflictsIntoCanonical({
|
|
2442
|
+
rootDir,
|
|
2443
|
+
conflicts: plan.conflicts,
|
|
2444
|
+
});
|
|
2445
|
+
const adoptedNames = new Set(
|
|
2446
|
+
adoptedConflicts.map((conflict) => conflict.name)
|
|
2447
|
+
);
|
|
2448
|
+
const unresolvedConflicts = plan.conflicts.filter(
|
|
2449
|
+
(conflict) => !adoptedNames.has(conflict.name)
|
|
2450
|
+
);
|
|
2451
|
+
if (adoptedConflicts.length > 0) {
|
|
2452
|
+
await buildIndex({
|
|
2453
|
+
homeDir,
|
|
2454
|
+
rootDir,
|
|
2455
|
+
force: false,
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2259
2458
|
const desiredSkills = new Map(
|
|
2260
2459
|
(
|
|
2261
2460
|
await loadEnabledSkillEntries({
|
|
@@ -2267,19 +2466,30 @@ async function syncSkillSymlinks({
|
|
|
2267
2466
|
);
|
|
2268
2467
|
|
|
2269
2468
|
await ensureDir(toolSkillsDir);
|
|
2270
|
-
|
|
2469
|
+
const remove = Array.from(
|
|
2470
|
+
new Set([...plan.remove, ...adoptedConflicts.map(({ name }) => name)])
|
|
2471
|
+
).sort();
|
|
2472
|
+
const add = Array.from(
|
|
2473
|
+
new Set([...plan.add, ...adoptedConflicts.map(({ name }) => name)])
|
|
2474
|
+
).sort();
|
|
2475
|
+
for (const name of remove) {
|
|
2271
2476
|
const linkPath = join(toolSkillsDir, name);
|
|
2272
2477
|
await rm(linkPath, { recursive: true, force: true });
|
|
2273
2478
|
}
|
|
2274
|
-
for (const name of
|
|
2275
|
-
const target = desiredSkills.get(name);
|
|
2479
|
+
for (const name of add) {
|
|
2480
|
+
const target = desiredSkills.get(name) ?? join(rootDir, "skills", name);
|
|
2276
2481
|
if (!(target && (await fileExists(target)))) {
|
|
2277
2482
|
continue;
|
|
2278
2483
|
}
|
|
2279
2484
|
const linkPath = join(toolSkillsDir, name);
|
|
2280
2485
|
await symlink(target, linkPath, "dir");
|
|
2281
2486
|
}
|
|
2282
|
-
return
|
|
2487
|
+
return {
|
|
2488
|
+
add,
|
|
2489
|
+
remove,
|
|
2490
|
+
conflicts: unresolvedConflicts,
|
|
2491
|
+
adopted: adoptedConflicts,
|
|
2492
|
+
};
|
|
2283
2493
|
}
|
|
2284
2494
|
|
|
2285
2495
|
async function planMcpWrite({
|
|
@@ -2292,10 +2502,14 @@ async function planMcpWrite({
|
|
|
2292
2502
|
mcpConfigPath: string;
|
|
2293
2503
|
rootDir: string;
|
|
2294
2504
|
tool: string;
|
|
2295
|
-
}): Promise<{ needsWrite: boolean; contents: string }> {
|
|
2296
|
-
const { servers } = await loadCanonicalMcpState(
|
|
2297
|
-
|
|
2298
|
-
|
|
2505
|
+
}): Promise<{ needsWrite: boolean; contents: string; sourcePath: string }> {
|
|
2506
|
+
const { localPath, servers, trackedPath } = await loadCanonicalMcpState(
|
|
2507
|
+
rootDir,
|
|
2508
|
+
{
|
|
2509
|
+
includeLocal: true,
|
|
2510
|
+
}
|
|
2511
|
+
);
|
|
2512
|
+
const sourcePath = (await fileExists(localPath)) ? localPath : trackedPath;
|
|
2299
2513
|
const filtered = await filterServersForTool({
|
|
2300
2514
|
homeDir,
|
|
2301
2515
|
rootDir,
|
|
@@ -2305,38 +2519,40 @@ async function planMcpWrite({
|
|
|
2305
2519
|
const contents = `${JSON.stringify({ mcpServers: filtered }, null, 2)}\n`;
|
|
2306
2520
|
|
|
2307
2521
|
if (!(await fileExists(mcpConfigPath))) {
|
|
2308
|
-
return {
|
|
2522
|
+
return {
|
|
2523
|
+
needsWrite: true,
|
|
2524
|
+
contents,
|
|
2525
|
+
sourcePath,
|
|
2526
|
+
};
|
|
2309
2527
|
}
|
|
2310
2528
|
try {
|
|
2311
2529
|
const current = await Bun.file(mcpConfigPath).text();
|
|
2312
|
-
return {
|
|
2530
|
+
return {
|
|
2531
|
+
needsWrite: current !== contents,
|
|
2532
|
+
contents,
|
|
2533
|
+
sourcePath,
|
|
2534
|
+
};
|
|
2313
2535
|
} catch {
|
|
2314
|
-
return {
|
|
2536
|
+
return {
|
|
2537
|
+
needsWrite: true,
|
|
2538
|
+
contents,
|
|
2539
|
+
sourcePath,
|
|
2540
|
+
};
|
|
2315
2541
|
}
|
|
2316
2542
|
}
|
|
2317
2543
|
|
|
2318
|
-
async function
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
tool,
|
|
2323
|
-
dryRun,
|
|
2324
|
-
}: {
|
|
2325
|
-
homeDir: string;
|
|
2326
|
-
mcpConfigPath: string;
|
|
2327
|
-
rootDir: string;
|
|
2328
|
-
tool: string;
|
|
2329
|
-
dryRun?: boolean;
|
|
2330
|
-
}): Promise<{ needsWrite: boolean }> {
|
|
2331
|
-
const plan = await planMcpWrite({ homeDir, mcpConfigPath, rootDir, tool });
|
|
2332
|
-
if (dryRun) {
|
|
2333
|
-
return { needsWrite: plan.needsWrite };
|
|
2544
|
+
async function isEmptyGeneratedMcpConfig(pathValue: string): Promise<boolean> {
|
|
2545
|
+
const text = await readTextIfExists(pathValue);
|
|
2546
|
+
if (text == null) {
|
|
2547
|
+
return false;
|
|
2334
2548
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2549
|
+
try {
|
|
2550
|
+
const parsed = JSON.parse(text) as unknown;
|
|
2551
|
+
const servers = extractServersObject(parsed);
|
|
2552
|
+
return servers != null && Object.keys(servers).length === 0;
|
|
2553
|
+
} catch {
|
|
2554
|
+
return false;
|
|
2338
2555
|
}
|
|
2339
|
-
return { needsWrite: plan.needsWrite };
|
|
2340
2556
|
}
|
|
2341
2557
|
|
|
2342
2558
|
async function writeToolMcpConfig({
|
|
@@ -2417,7 +2633,9 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2417
2633
|
: emptyManagedImportPlan(),
|
|
2418
2634
|
toolPaths.automationDir
|
|
2419
2635
|
? await planExistingAutomationAdoption({
|
|
2636
|
+
homeDir: home,
|
|
2420
2637
|
rootDir,
|
|
2638
|
+
tool,
|
|
2421
2639
|
automationDir: toolPaths.automationDir,
|
|
2422
2640
|
})
|
|
2423
2641
|
: emptyManagedImportPlan(),
|
|
@@ -2577,7 +2795,9 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2577
2795
|
}
|
|
2578
2796
|
if (toolPaths.automationDir && opts.adoptExisting) {
|
|
2579
2797
|
const result = await adoptExistingAutomations({
|
|
2798
|
+
homeDir: home,
|
|
2580
2799
|
rootDir,
|
|
2800
|
+
tool,
|
|
2581
2801
|
automationDir: toolPaths.automationDir,
|
|
2582
2802
|
conflictMode: importConflictMode,
|
|
2583
2803
|
});
|
|
@@ -2648,10 +2868,20 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2648
2868
|
tool,
|
|
2649
2869
|
})
|
|
2650
2870
|
: null;
|
|
2871
|
+
const mcpPreview = toolPaths.mcpConfig
|
|
2872
|
+
? await planMcpWrite({
|
|
2873
|
+
homeDir: home,
|
|
2874
|
+
mcpConfigPath: toolPaths.mcpConfig,
|
|
2875
|
+
rootDir,
|
|
2876
|
+
tool,
|
|
2877
|
+
})
|
|
2878
|
+
: null;
|
|
2651
2879
|
const automationPreview = toolPaths.automationDir
|
|
2652
2880
|
? await planAutomationFileChanges({
|
|
2653
2881
|
automationDir: toolPaths.automationDir,
|
|
2882
|
+
homeDir: home,
|
|
2654
2883
|
rootDir,
|
|
2884
|
+
tool,
|
|
2655
2885
|
})
|
|
2656
2886
|
: null;
|
|
2657
2887
|
const globalDocsPreview = toolPaths.toolHome
|
|
@@ -2927,6 +3157,15 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2927
3157
|
),
|
|
2928
3158
|
});
|
|
2929
3159
|
}
|
|
3160
|
+
if (toolPaths.mcpConfig && mcpPreview) {
|
|
3161
|
+
updateRenderedTargetState({
|
|
3162
|
+
entry: managedEntry,
|
|
3163
|
+
writtenTargets: [toolPaths.mcpConfig],
|
|
3164
|
+
removedTargets: [],
|
|
3165
|
+
contents: new Map([[toolPaths.mcpConfig, mcpPreview.contents]]),
|
|
3166
|
+
sources: new Map([[toolPaths.mcpConfig, mcpPreview.sourcePath]]),
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
2930
3169
|
|
|
2931
3170
|
if (pluginPreview) {
|
|
2932
3171
|
updateRenderedTargetState({
|
|
@@ -3248,7 +3487,7 @@ interface RenderedConflict {
|
|
|
3248
3487
|
targetPath: string;
|
|
3249
3488
|
sourcePath: string;
|
|
3250
3489
|
sourceKind: ManagedRenderedTargetState["sourceKind"];
|
|
3251
|
-
reason: "modified" | "unknown_state";
|
|
3490
|
+
reason: "modified" | "unknown_state" | "missing_source";
|
|
3252
3491
|
}
|
|
3253
3492
|
|
|
3254
3493
|
interface RenderedApplyPlan {
|
|
@@ -3265,16 +3504,9 @@ async function planRenderedTargetConflicts(args: {
|
|
|
3265
3504
|
desiredSources: Map<string, string>;
|
|
3266
3505
|
conflictMode?: "warn" | "overwrite";
|
|
3267
3506
|
protectAllSources?: boolean;
|
|
3507
|
+
protectMissingSources?: boolean;
|
|
3268
3508
|
normalizeText?: boolean;
|
|
3269
3509
|
}): Promise<RenderedApplyPlan> {
|
|
3270
|
-
if (args.conflictMode === "overwrite") {
|
|
3271
|
-
return {
|
|
3272
|
-
write: args.desiredWrites,
|
|
3273
|
-
remove: args.desiredRemoves,
|
|
3274
|
-
conflicts: [],
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
3510
|
const previous = args.entry.renderedTargets ?? {};
|
|
3279
3511
|
const write: string[] = [];
|
|
3280
3512
|
const remove: string[] = [];
|
|
@@ -3293,7 +3525,15 @@ async function planRenderedTargetConflicts(args: {
|
|
|
3293
3525
|
continue;
|
|
3294
3526
|
}
|
|
3295
3527
|
const sourceKind = renderedSourceKindForPath(sourcePath);
|
|
3296
|
-
if (
|
|
3528
|
+
if (args.conflictMode === "overwrite" && sourceKind === "builtin") {
|
|
3529
|
+
if (args.desiredWrites.includes(targetPath)) {
|
|
3530
|
+
write.push(targetPath);
|
|
3531
|
+
} else {
|
|
3532
|
+
remove.push(targetPath);
|
|
3533
|
+
}
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
if (sourceKind !== "builtin" && args.protectAllSources === false) {
|
|
3297
3537
|
if (args.desiredWrites.includes(targetPath)) {
|
|
3298
3538
|
write.push(targetPath);
|
|
3299
3539
|
} else {
|
|
@@ -3303,6 +3543,20 @@ async function planRenderedTargetConflicts(args: {
|
|
|
3303
3543
|
}
|
|
3304
3544
|
|
|
3305
3545
|
const prior = previous[targetPath];
|
|
3546
|
+
if (
|
|
3547
|
+
args.protectMissingSources &&
|
|
3548
|
+
args.desiredRemoves.includes(targetPath) &&
|
|
3549
|
+
prior?.sourcePath &&
|
|
3550
|
+
!(await fileExists(prior.sourcePath))
|
|
3551
|
+
) {
|
|
3552
|
+
conflicts.push({
|
|
3553
|
+
targetPath,
|
|
3554
|
+
sourcePath: prior.sourcePath,
|
|
3555
|
+
sourceKind,
|
|
3556
|
+
reason: "missing_source",
|
|
3557
|
+
});
|
|
3558
|
+
continue;
|
|
3559
|
+
}
|
|
3306
3560
|
const currentHash = await readTargetHash(targetPath, {
|
|
3307
3561
|
normalizeText: args.normalizeText,
|
|
3308
3562
|
});
|
|
@@ -3372,9 +3626,11 @@ function logRenderedConflicts(
|
|
|
3372
3626
|
for (const conflict of conflicts) {
|
|
3373
3627
|
const verb = dryRun ? "would skip" : "skipped";
|
|
3374
3628
|
const state =
|
|
3375
|
-
conflict.reason === "
|
|
3376
|
-
?
|
|
3377
|
-
:
|
|
3629
|
+
conflict.reason === "missing_source"
|
|
3630
|
+
? `canonical source is missing at ${conflict.sourcePath}`
|
|
3631
|
+
: conflict.reason === "unknown_state"
|
|
3632
|
+
? "no prior managed hash is recorded"
|
|
3633
|
+
: "local edits were detected";
|
|
3378
3634
|
const surface =
|
|
3379
3635
|
conflict.sourceKind === "builtin"
|
|
3380
3636
|
? "builtin-backed target"
|
|
@@ -3387,6 +3643,57 @@ function logRenderedConflicts(
|
|
|
3387
3643
|
}
|
|
3388
3644
|
}
|
|
3389
3645
|
|
|
3646
|
+
function logSkillSymlinkConflicts(
|
|
3647
|
+
tool: string,
|
|
3648
|
+
conflicts: SkillSymlinkConflict[],
|
|
3649
|
+
dryRun?: boolean
|
|
3650
|
+
) {
|
|
3651
|
+
for (const conflict of conflicts) {
|
|
3652
|
+
const verb =
|
|
3653
|
+
conflict.reason === "disabled"
|
|
3654
|
+
? dryRun
|
|
3655
|
+
? "would preserve"
|
|
3656
|
+
: "preserved"
|
|
3657
|
+
: dryRun
|
|
3658
|
+
? "would adopt"
|
|
3659
|
+
: "adopted";
|
|
3660
|
+
const state =
|
|
3661
|
+
conflict.reason === "unmanaged"
|
|
3662
|
+
? "it is not in canonical skill state"
|
|
3663
|
+
: conflict.reason === "disabled"
|
|
3664
|
+
? "it maps to a disabled canonical skill"
|
|
3665
|
+
: "local skill content differs from canonical state";
|
|
3666
|
+
const canonical = conflict.canonicalPath
|
|
3667
|
+
? ` (canonical ${conflict.canonicalPath})`
|
|
3668
|
+
: "";
|
|
3669
|
+
console.warn(
|
|
3670
|
+
`${tool}: ${verb} skill ${conflict.name} from ${conflict.livePath}${canonical} because ${state}.`
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
async function adoptSkillSymlinkConflictsIntoCanonical(args: {
|
|
3676
|
+
rootDir: string;
|
|
3677
|
+
conflicts: SkillSymlinkConflict[];
|
|
3678
|
+
}): Promise<SkillSymlinkConflict[]> {
|
|
3679
|
+
const adopted: SkillSymlinkConflict[] = [];
|
|
3680
|
+
for (const conflict of args.conflicts) {
|
|
3681
|
+
if (conflict.reason === "disabled") {
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
const canonicalPath =
|
|
3685
|
+
conflict.canonicalPath ?? join(args.rootDir, "skills", conflict.name);
|
|
3686
|
+
if (!(await fileExists(join(conflict.livePath, "SKILL.md")))) {
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
await rm(canonicalPath, { recursive: true, force: true });
|
|
3690
|
+
await ensureDir(dirname(canonicalPath));
|
|
3691
|
+
await cp(conflict.livePath, canonicalPath, { recursive: true });
|
|
3692
|
+
adopted.push({ ...conflict, canonicalPath });
|
|
3693
|
+
}
|
|
3694
|
+
return adopted.sort((a, b) => a.name.localeCompare(b.name));
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3390
3697
|
async function applyRenderedWrites(args: {
|
|
3391
3698
|
contents: Map<string, ManagedTargetContent>;
|
|
3392
3699
|
targets: string[];
|
|
@@ -3485,9 +3792,9 @@ function pruneAutomationRuntimeRenderedTargets(args: {
|
|
|
3485
3792
|
|
|
3486
3793
|
function logSyncDryRun({
|
|
3487
3794
|
tool,
|
|
3488
|
-
entry,
|
|
3489
3795
|
skillPlan,
|
|
3490
3796
|
mcpPlan,
|
|
3797
|
+
mcpConflicts,
|
|
3491
3798
|
agentPlan,
|
|
3492
3799
|
agentConflicts,
|
|
3493
3800
|
automationPlan,
|
|
@@ -3502,9 +3809,9 @@ function logSyncDryRun({
|
|
|
3502
3809
|
pluginConflicts,
|
|
3503
3810
|
}: {
|
|
3504
3811
|
tool: string;
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3812
|
+
skillPlan: SkillSymlinkPlan;
|
|
3813
|
+
mcpPlan: { write: boolean; targetPath: string };
|
|
3814
|
+
mcpConflicts: RenderedConflict[];
|
|
3508
3815
|
agentPlan: { add: string[]; remove: string[] };
|
|
3509
3816
|
agentConflicts: RenderedConflict[];
|
|
3510
3817
|
automationPlan: { write: string[]; remove: string[] };
|
|
@@ -3524,6 +3831,7 @@ function logSyncDryRun({
|
|
|
3524
3831
|
for (const name of skillPlan.remove) {
|
|
3525
3832
|
console.log(`${tool}: would remove skill ${name}`);
|
|
3526
3833
|
}
|
|
3834
|
+
logSkillSymlinkConflicts(tool, skillPlan.conflicts, true);
|
|
3527
3835
|
for (const p of agentPlan.add) {
|
|
3528
3836
|
console.log(`${tool}: would write agent ${p}`);
|
|
3529
3837
|
}
|
|
@@ -3566,12 +3874,14 @@ function logSyncDryRun({
|
|
|
3566
3874
|
console.log(`${tool}: would remove plugin asset ${p}`);
|
|
3567
3875
|
}
|
|
3568
3876
|
logRenderedConflicts(tool, pluginConflicts, true);
|
|
3569
|
-
if (mcpPlan.
|
|
3570
|
-
console.log(`${tool}: would update mcp config ${
|
|
3877
|
+
if (mcpPlan.write) {
|
|
3878
|
+
console.log(`${tool}: would update mcp config ${mcpPlan.targetPath}`);
|
|
3571
3879
|
}
|
|
3880
|
+
logRenderedConflicts(tool, mcpConflicts, true);
|
|
3572
3881
|
if (
|
|
3573
3882
|
skillPlan.add.length === 0 &&
|
|
3574
3883
|
skillPlan.remove.length === 0 &&
|
|
3884
|
+
skillPlan.conflicts.length === 0 &&
|
|
3575
3885
|
agentPlan.add.length === 0 &&
|
|
3576
3886
|
agentPlan.remove.length === 0 &&
|
|
3577
3887
|
automationPlan.write.length === 0 &&
|
|
@@ -3584,7 +3894,8 @@ function logSyncDryRun({
|
|
|
3584
3894
|
!configPlan.remove &&
|
|
3585
3895
|
pluginPlan.write.length === 0 &&
|
|
3586
3896
|
pluginPlan.remove.length === 0 &&
|
|
3587
|
-
!mcpPlan.
|
|
3897
|
+
!mcpPlan.write &&
|
|
3898
|
+
mcpConflicts.length === 0 &&
|
|
3588
3899
|
agentConflicts.length === 0 &&
|
|
3589
3900
|
automationConflicts.length === 0 &&
|
|
3590
3901
|
globalDocsConflicts.length === 0 &&
|
|
@@ -3596,6 +3907,171 @@ function logSyncDryRun({
|
|
|
3596
3907
|
}
|
|
3597
3908
|
}
|
|
3598
3909
|
|
|
3910
|
+
function syncLedgerBase(args: {
|
|
3911
|
+
correlationId: string;
|
|
3912
|
+
dryRun: boolean;
|
|
3913
|
+
homeDir: string;
|
|
3914
|
+
rootDir: string;
|
|
3915
|
+
tool: string;
|
|
3916
|
+
}): Omit<
|
|
3917
|
+
SyncLedgerEvent,
|
|
3918
|
+
| "action"
|
|
3919
|
+
| "id"
|
|
3920
|
+
| "name"
|
|
3921
|
+
| "newHash"
|
|
3922
|
+
| "oldHash"
|
|
3923
|
+
| "reason"
|
|
3924
|
+
| "sourcePath"
|
|
3925
|
+
| "targetPath"
|
|
3926
|
+
> {
|
|
3927
|
+
return {
|
|
3928
|
+
version: 1,
|
|
3929
|
+
correlationId: args.correlationId,
|
|
3930
|
+
ts: new Date().toISOString(),
|
|
3931
|
+
command: "sync",
|
|
3932
|
+
phase: args.dryRun ? "plan" : "apply",
|
|
3933
|
+
dryRun: args.dryRun,
|
|
3934
|
+
tool: args.tool,
|
|
3935
|
+
rootDir: args.rootDir,
|
|
3936
|
+
scope: projectRootFromAiRoot(args.rootDir, args.homeDir)
|
|
3937
|
+
? "project"
|
|
3938
|
+
: "global",
|
|
3939
|
+
actor: process.env.USER ?? "unknown",
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
function renderedDesiredHash(args: {
|
|
3944
|
+
contents: Map<string, ManagedTargetContent>;
|
|
3945
|
+
normalizeText?: boolean;
|
|
3946
|
+
targetPath: string;
|
|
3947
|
+
}): string | undefined {
|
|
3948
|
+
const contents = args.contents.get(args.targetPath);
|
|
3949
|
+
return contents
|
|
3950
|
+
? targetContentHash(contents, { normalizeText: args.normalizeText })
|
|
3951
|
+
: undefined;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
function collectRenderedLedgerEvents(args: {
|
|
3955
|
+
base: Omit<
|
|
3956
|
+
SyncLedgerEvent,
|
|
3957
|
+
| "action"
|
|
3958
|
+
| "id"
|
|
3959
|
+
| "name"
|
|
3960
|
+
| "newHash"
|
|
3961
|
+
| "oldHash"
|
|
3962
|
+
| "reason"
|
|
3963
|
+
| "sourcePath"
|
|
3964
|
+
| "targetPath"
|
|
3965
|
+
>;
|
|
3966
|
+
conflicts: RenderedConflict[];
|
|
3967
|
+
contents: Map<string, ManagedTargetContent>;
|
|
3968
|
+
entry: ManagedToolState;
|
|
3969
|
+
normalizeText?: boolean;
|
|
3970
|
+
plan: RenderedApplyPlan;
|
|
3971
|
+
reason: string;
|
|
3972
|
+
sources: Map<string, string>;
|
|
3973
|
+
}): SyncLedgerEvent[] {
|
|
3974
|
+
const previous = args.entry.renderedTargets ?? {};
|
|
3975
|
+
const events: SyncLedgerEvent[] = [];
|
|
3976
|
+
for (const targetPath of args.plan.write) {
|
|
3977
|
+
events.push({
|
|
3978
|
+
...args.base,
|
|
3979
|
+
id: randomUUID(),
|
|
3980
|
+
action: "write",
|
|
3981
|
+
targetPath,
|
|
3982
|
+
sourcePath: args.sources.get(targetPath),
|
|
3983
|
+
oldHash: previous[targetPath]?.hash,
|
|
3984
|
+
newHash: renderedDesiredHash({
|
|
3985
|
+
contents: args.contents,
|
|
3986
|
+
normalizeText: args.normalizeText,
|
|
3987
|
+
targetPath,
|
|
3988
|
+
}),
|
|
3989
|
+
reason: args.reason,
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
for (const targetPath of args.plan.remove) {
|
|
3993
|
+
events.push({
|
|
3994
|
+
...args.base,
|
|
3995
|
+
id: randomUUID(),
|
|
3996
|
+
action: "remove",
|
|
3997
|
+
targetPath,
|
|
3998
|
+
sourcePath: previous[targetPath]?.sourcePath,
|
|
3999
|
+
oldHash: previous[targetPath]?.hash,
|
|
4000
|
+
reason: args.reason,
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
for (const conflict of args.conflicts) {
|
|
4004
|
+
events.push({
|
|
4005
|
+
...args.base,
|
|
4006
|
+
id: randomUUID(),
|
|
4007
|
+
action: "skip",
|
|
4008
|
+
targetPath: conflict.targetPath,
|
|
4009
|
+
sourcePath: conflict.sourcePath,
|
|
4010
|
+
oldHash: previous[conflict.targetPath]?.hash,
|
|
4011
|
+
reason: conflict.reason,
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
return events;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
function collectSkillLedgerEvents(args: {
|
|
4018
|
+
base: Omit<
|
|
4019
|
+
SyncLedgerEvent,
|
|
4020
|
+
| "action"
|
|
4021
|
+
| "id"
|
|
4022
|
+
| "name"
|
|
4023
|
+
| "newHash"
|
|
4024
|
+
| "oldHash"
|
|
4025
|
+
| "reason"
|
|
4026
|
+
| "sourcePath"
|
|
4027
|
+
| "targetPath"
|
|
4028
|
+
>;
|
|
4029
|
+
plan: SkillSymlinkPlan;
|
|
4030
|
+
}): SyncLedgerEvent[] {
|
|
4031
|
+
const events: SyncLedgerEvent[] = [];
|
|
4032
|
+
for (const name of args.plan.add) {
|
|
4033
|
+
events.push({
|
|
4034
|
+
...args.base,
|
|
4035
|
+
id: randomUUID(),
|
|
4036
|
+
action: "add",
|
|
4037
|
+
name,
|
|
4038
|
+
reason: "skill_symlink",
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
for (const name of args.plan.remove) {
|
|
4042
|
+
events.push({
|
|
4043
|
+
...args.base,
|
|
4044
|
+
id: randomUUID(),
|
|
4045
|
+
action: "remove",
|
|
4046
|
+
name,
|
|
4047
|
+
reason: "skill_symlink",
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
for (const conflict of args.plan.conflicts) {
|
|
4051
|
+
events.push({
|
|
4052
|
+
...args.base,
|
|
4053
|
+
id: randomUUID(),
|
|
4054
|
+
action: "skip",
|
|
4055
|
+
name: conflict.name,
|
|
4056
|
+
targetPath: conflict.livePath,
|
|
4057
|
+
sourcePath: conflict.canonicalPath,
|
|
4058
|
+
reason: `skill_${conflict.reason}`,
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
for (const conflict of args.plan.adopted) {
|
|
4062
|
+
events.push({
|
|
4063
|
+
...args.base,
|
|
4064
|
+
id: randomUUID(),
|
|
4065
|
+
action: "adopt",
|
|
4066
|
+
name: conflict.name,
|
|
4067
|
+
targetPath: conflict.livePath,
|
|
4068
|
+
sourcePath: conflict.canonicalPath,
|
|
4069
|
+
reason: `skill_${conflict.reason}`,
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
return events;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
3599
4075
|
async function repairManagedCanonicalContent(args: {
|
|
3600
4076
|
homeDir: string;
|
|
3601
4077
|
rootDir: string;
|
|
@@ -4002,6 +4478,38 @@ async function syncManagedToolEntry({
|
|
|
4002
4478
|
dryRun?: boolean;
|
|
4003
4479
|
builtinConflictMode?: "warn" | "overwrite";
|
|
4004
4480
|
}) {
|
|
4481
|
+
const correlationId = randomUUID();
|
|
4482
|
+
const baseLedger = syncLedgerBase({
|
|
4483
|
+
correlationId,
|
|
4484
|
+
dryRun: Boolean(dryRun),
|
|
4485
|
+
homeDir,
|
|
4486
|
+
rootDir,
|
|
4487
|
+
tool,
|
|
4488
|
+
});
|
|
4489
|
+
if (await isGeneratedOnlyProjectRoot({ homeDir, rootDir })) {
|
|
4490
|
+
const message = `${tool}: ${dryRun ? "would skip" : "skipped"} sync because project .ai contains generated state only and no canonical source. Initialize or restore project .ai source before syncing managed project output.`;
|
|
4491
|
+
if (dryRun) {
|
|
4492
|
+
console.log(message);
|
|
4493
|
+
} else {
|
|
4494
|
+
console.warn(message);
|
|
4495
|
+
}
|
|
4496
|
+
if (!dryRun) {
|
|
4497
|
+
await appendSyncLedgerEvents({
|
|
4498
|
+
homeDir,
|
|
4499
|
+
rootDir,
|
|
4500
|
+
events: [
|
|
4501
|
+
{
|
|
4502
|
+
...baseLedger,
|
|
4503
|
+
id: randomUUID(),
|
|
4504
|
+
action: "skip",
|
|
4505
|
+
reason: "generated_only_project_root",
|
|
4506
|
+
},
|
|
4507
|
+
],
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4005
4513
|
pruneAutomationRuntimeRenderedTargets({
|
|
4006
4514
|
entry,
|
|
4007
4515
|
automationDir: entry.automationDir,
|
|
@@ -4024,7 +4532,18 @@ async function syncManagedToolEntry({
|
|
|
4024
4532
|
tool,
|
|
4025
4533
|
dryRun,
|
|
4026
4534
|
})
|
|
4027
|
-
: { add: [], remove: [] };
|
|
4535
|
+
: { add: [], remove: [], conflicts: [], adopted: [] };
|
|
4536
|
+
const skillLedgerEvents = collectSkillLedgerEvents({
|
|
4537
|
+
base: baseLedger,
|
|
4538
|
+
plan: skillPlan,
|
|
4539
|
+
});
|
|
4540
|
+
if (!dryRun) {
|
|
4541
|
+
await appendSyncLedgerEvents({
|
|
4542
|
+
homeDir,
|
|
4543
|
+
rootDir,
|
|
4544
|
+
events: skillLedgerEvents,
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4028
4547
|
|
|
4029
4548
|
const agentPlan = entry.agentsDir
|
|
4030
4549
|
? await planAgentFileChanges({
|
|
@@ -4037,20 +4556,21 @@ async function syncManagedToolEntry({
|
|
|
4037
4556
|
const automationPlan = entry.automationDir
|
|
4038
4557
|
? await planAutomationFileChanges({
|
|
4039
4558
|
automationDir: entry.automationDir,
|
|
4559
|
+
homeDir,
|
|
4040
4560
|
rootDir,
|
|
4561
|
+
tool,
|
|
4041
4562
|
previouslyManagedTargets: Object.keys(entry.renderedTargets ?? {}),
|
|
4042
4563
|
})
|
|
4043
4564
|
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
4044
4565
|
|
|
4045
4566
|
const mcpPlan = entry.mcpConfig
|
|
4046
|
-
? await
|
|
4567
|
+
? await planMcpWrite({
|
|
4047
4568
|
homeDir,
|
|
4048
4569
|
mcpConfigPath: entry.mcpConfig,
|
|
4049
4570
|
rootDir,
|
|
4050
4571
|
tool,
|
|
4051
|
-
dryRun,
|
|
4052
4572
|
})
|
|
4053
|
-
:
|
|
4573
|
+
: null;
|
|
4054
4574
|
|
|
4055
4575
|
const globalDocsPlan = entry.toolHome
|
|
4056
4576
|
? await planToolGlobalDocsSync({
|
|
@@ -4113,6 +4633,7 @@ async function syncManagedToolEntry({
|
|
|
4113
4633
|
previouslyManagedTargets: Object.keys(entry.renderedTargets ?? {}),
|
|
4114
4634
|
})
|
|
4115
4635
|
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
4636
|
+
const protectMissingSources = projectRootFromAiRoot(rootDir, homeDir) != null;
|
|
4116
4637
|
|
|
4117
4638
|
const agentRendered = await planRenderedTargetConflicts({
|
|
4118
4639
|
entry,
|
|
@@ -4121,6 +4642,7 @@ async function syncManagedToolEntry({
|
|
|
4121
4642
|
desiredContents: agentPlan.contents,
|
|
4122
4643
|
desiredSources: agentPlan.sources,
|
|
4123
4644
|
conflictMode: builtinConflictMode,
|
|
4645
|
+
protectMissingSources,
|
|
4124
4646
|
});
|
|
4125
4647
|
const globalDocsRendered = await planRenderedTargetConflicts({
|
|
4126
4648
|
entry,
|
|
@@ -4129,6 +4651,7 @@ async function syncManagedToolEntry({
|
|
|
4129
4651
|
desiredContents: globalDocsPlan.contents,
|
|
4130
4652
|
desiredSources: globalDocsPlan.sources,
|
|
4131
4653
|
conflictMode: builtinConflictMode,
|
|
4654
|
+
protectMissingSources,
|
|
4132
4655
|
});
|
|
4133
4656
|
const automationRendered = await planRenderedTargetConflicts({
|
|
4134
4657
|
entry,
|
|
@@ -4138,6 +4661,7 @@ async function syncManagedToolEntry({
|
|
|
4138
4661
|
desiredSources: automationPlan.sources,
|
|
4139
4662
|
conflictMode: builtinConflictMode,
|
|
4140
4663
|
protectAllSources: true,
|
|
4664
|
+
protectMissingSources,
|
|
4141
4665
|
});
|
|
4142
4666
|
const rulesRendered = await planRenderedTargetConflicts({
|
|
4143
4667
|
entry,
|
|
@@ -4146,6 +4670,7 @@ async function syncManagedToolEntry({
|
|
|
4146
4670
|
desiredContents: rulesPlan.contents,
|
|
4147
4671
|
desiredSources: rulesPlan.sources,
|
|
4148
4672
|
conflictMode: builtinConflictMode,
|
|
4673
|
+
protectMissingSources,
|
|
4149
4674
|
});
|
|
4150
4675
|
const configContents =
|
|
4151
4676
|
configPlan.contents != null
|
|
@@ -4165,6 +4690,7 @@ async function syncManagedToolEntry({
|
|
|
4165
4690
|
desiredContents: configContents,
|
|
4166
4691
|
desiredSources: configSources,
|
|
4167
4692
|
conflictMode: builtinConflictMode,
|
|
4693
|
+
protectMissingSources,
|
|
4168
4694
|
});
|
|
4169
4695
|
const pluginRendered = await planRenderedTargetConflicts({
|
|
4170
4696
|
entry,
|
|
@@ -4175,14 +4701,126 @@ async function syncManagedToolEntry({
|
|
|
4175
4701
|
conflictMode: builtinConflictMode,
|
|
4176
4702
|
protectAllSources: true,
|
|
4177
4703
|
normalizeText: false,
|
|
4704
|
+
protectMissingSources,
|
|
4178
4705
|
});
|
|
4706
|
+
const mcpContents =
|
|
4707
|
+
entry.mcpConfig && mcpPlan
|
|
4708
|
+
? new Map<string, ManagedTargetContent>([
|
|
4709
|
+
[entry.mcpConfig, mcpPlan.contents],
|
|
4710
|
+
])
|
|
4711
|
+
: new Map<string, ManagedTargetContent>();
|
|
4712
|
+
const mcpSources =
|
|
4713
|
+
entry.mcpConfig && mcpPlan
|
|
4714
|
+
? new Map<string, string>([[entry.mcpConfig, mcpPlan.sourcePath]])
|
|
4715
|
+
: new Map<string, string>();
|
|
4716
|
+
const mcpRendered = await planRenderedTargetConflicts({
|
|
4717
|
+
entry,
|
|
4718
|
+
desiredWrites:
|
|
4719
|
+
entry.mcpConfig && mcpPlan?.needsWrite ? [entry.mcpConfig] : [],
|
|
4720
|
+
desiredRemoves: [],
|
|
4721
|
+
desiredContents: mcpContents,
|
|
4722
|
+
desiredSources: mcpSources,
|
|
4723
|
+
conflictMode: builtinConflictMode,
|
|
4724
|
+
protectMissingSources,
|
|
4725
|
+
});
|
|
4726
|
+
if (
|
|
4727
|
+
entry.mcpConfig &&
|
|
4728
|
+
mcpRendered.conflicts.some(
|
|
4729
|
+
(conflict) =>
|
|
4730
|
+
conflict.targetPath === entry.mcpConfig &&
|
|
4731
|
+
conflict.reason === "unknown_state"
|
|
4732
|
+
) &&
|
|
4733
|
+
(await isEmptyGeneratedMcpConfig(entry.mcpConfig))
|
|
4734
|
+
) {
|
|
4735
|
+
mcpRendered.conflicts = mcpRendered.conflicts.filter(
|
|
4736
|
+
(conflict) => conflict.targetPath !== entry.mcpConfig
|
|
4737
|
+
);
|
|
4738
|
+
mcpRendered.write.push(entry.mcpConfig);
|
|
4739
|
+
mcpRendered.write.sort();
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
const ledgerEvents = [
|
|
4743
|
+
...collectRenderedLedgerEvents({
|
|
4744
|
+
base: baseLedger,
|
|
4745
|
+
conflicts: agentRendered.conflicts,
|
|
4746
|
+
contents: agentPlan.contents,
|
|
4747
|
+
entry,
|
|
4748
|
+
normalizeText: true,
|
|
4749
|
+
plan: agentRendered,
|
|
4750
|
+
reason: "agent_render",
|
|
4751
|
+
sources: agentPlan.sources,
|
|
4752
|
+
}),
|
|
4753
|
+
...collectRenderedLedgerEvents({
|
|
4754
|
+
base: baseLedger,
|
|
4755
|
+
conflicts: automationRendered.conflicts,
|
|
4756
|
+
contents: automationPlan.contents,
|
|
4757
|
+
entry,
|
|
4758
|
+
normalizeText: true,
|
|
4759
|
+
plan: automationRendered,
|
|
4760
|
+
reason: "automation_render",
|
|
4761
|
+
sources: automationPlan.sources,
|
|
4762
|
+
}),
|
|
4763
|
+
...collectRenderedLedgerEvents({
|
|
4764
|
+
base: baseLedger,
|
|
4765
|
+
conflicts: globalDocsRendered.conflicts,
|
|
4766
|
+
contents: globalDocsPlan.contents,
|
|
4767
|
+
entry,
|
|
4768
|
+
normalizeText: true,
|
|
4769
|
+
plan: globalDocsRendered,
|
|
4770
|
+
reason: "global_doc_render",
|
|
4771
|
+
sources: globalDocsPlan.sources,
|
|
4772
|
+
}),
|
|
4773
|
+
...collectRenderedLedgerEvents({
|
|
4774
|
+
base: baseLedger,
|
|
4775
|
+
conflicts: rulesRendered.conflicts,
|
|
4776
|
+
contents: rulesPlan.contents,
|
|
4777
|
+
entry,
|
|
4778
|
+
normalizeText: true,
|
|
4779
|
+
plan: rulesRendered,
|
|
4780
|
+
reason: "rule_render",
|
|
4781
|
+
sources: rulesPlan.sources,
|
|
4782
|
+
}),
|
|
4783
|
+
...collectRenderedLedgerEvents({
|
|
4784
|
+
base: baseLedger,
|
|
4785
|
+
conflicts: configRendered.conflicts,
|
|
4786
|
+
contents: configContents,
|
|
4787
|
+
entry,
|
|
4788
|
+
normalizeText: true,
|
|
4789
|
+
plan: configRendered,
|
|
4790
|
+
reason: "tool_config_render",
|
|
4791
|
+
sources: configSources,
|
|
4792
|
+
}),
|
|
4793
|
+
...collectRenderedLedgerEvents({
|
|
4794
|
+
base: baseLedger,
|
|
4795
|
+
conflicts: mcpRendered.conflicts,
|
|
4796
|
+
contents: mcpContents,
|
|
4797
|
+
entry,
|
|
4798
|
+
normalizeText: true,
|
|
4799
|
+
plan: mcpRendered,
|
|
4800
|
+
reason: "mcp_render",
|
|
4801
|
+
sources: mcpSources,
|
|
4802
|
+
}),
|
|
4803
|
+
...collectRenderedLedgerEvents({
|
|
4804
|
+
base: baseLedger,
|
|
4805
|
+
conflicts: pluginRendered.conflicts,
|
|
4806
|
+
contents: pluginPlan.contents,
|
|
4807
|
+
entry,
|
|
4808
|
+
normalizeText: false,
|
|
4809
|
+
plan: pluginRendered,
|
|
4810
|
+
reason: "plugin_render",
|
|
4811
|
+
sources: pluginPlan.sources,
|
|
4812
|
+
}),
|
|
4813
|
+
];
|
|
4179
4814
|
|
|
4180
4815
|
if (dryRun) {
|
|
4181
4816
|
logSyncDryRun({
|
|
4182
4817
|
tool,
|
|
4183
|
-
entry,
|
|
4184
4818
|
skillPlan,
|
|
4185
|
-
mcpPlan
|
|
4819
|
+
mcpPlan: {
|
|
4820
|
+
write: mcpRendered.write.length > 0,
|
|
4821
|
+
targetPath: entry.mcpConfig ?? "",
|
|
4822
|
+
},
|
|
4823
|
+
mcpConflicts: mcpRendered.conflicts,
|
|
4186
4824
|
agentPlan: { add: agentRendered.write, remove: agentRendered.remove },
|
|
4187
4825
|
agentConflicts: agentRendered.conflicts,
|
|
4188
4826
|
automationPlan: {
|
|
@@ -4238,6 +4876,10 @@ async function syncManagedToolEntry({
|
|
|
4238
4876
|
contents: configContents,
|
|
4239
4877
|
targets: configRendered.write,
|
|
4240
4878
|
});
|
|
4879
|
+
await applyRenderedWrites({
|
|
4880
|
+
contents: mcpContents,
|
|
4881
|
+
targets: mcpRendered.write,
|
|
4882
|
+
});
|
|
4241
4883
|
await applyRenderedRemoves(pluginRendered.remove);
|
|
4242
4884
|
await applyRenderedWrites({
|
|
4243
4885
|
contents: pluginPlan.contents,
|
|
@@ -4251,7 +4893,10 @@ async function syncManagedToolEntry({
|
|
|
4251
4893
|
logRenderedConflicts(tool, globalDocsRendered.conflicts);
|
|
4252
4894
|
logRenderedConflicts(tool, rulesRendered.conflicts);
|
|
4253
4895
|
logRenderedConflicts(tool, configRendered.conflicts);
|
|
4896
|
+
logRenderedConflicts(tool, mcpRendered.conflicts);
|
|
4254
4897
|
logRenderedConflicts(tool, pluginRendered.conflicts);
|
|
4898
|
+
logSkillSymlinkConflicts(tool, skillPlan.adopted);
|
|
4899
|
+
logSkillSymlinkConflicts(tool, skillPlan.conflicts);
|
|
4255
4900
|
|
|
4256
4901
|
updateRenderedTargetState({
|
|
4257
4902
|
entry,
|
|
@@ -4288,6 +4933,13 @@ async function syncManagedToolEntry({
|
|
|
4288
4933
|
contents: configContents,
|
|
4289
4934
|
sources: configSources,
|
|
4290
4935
|
});
|
|
4936
|
+
updateRenderedTargetState({
|
|
4937
|
+
entry,
|
|
4938
|
+
writtenTargets: mcpRendered.write,
|
|
4939
|
+
removedTargets: mcpRendered.remove,
|
|
4940
|
+
contents: mcpContents,
|
|
4941
|
+
sources: mcpSources,
|
|
4942
|
+
});
|
|
4291
4943
|
updateRenderedTargetState({
|
|
4292
4944
|
entry,
|
|
4293
4945
|
writtenTargets: pluginRendered.write,
|
|
@@ -4302,6 +4954,7 @@ async function syncManagedToolEntry({
|
|
|
4302
4954
|
`${tool}: adopted existing content ${name} into canonical store`
|
|
4303
4955
|
);
|
|
4304
4956
|
}
|
|
4957
|
+
await appendSyncLedgerEvents({ homeDir, rootDir, events: ledgerEvents });
|
|
4305
4958
|
console.log(`${tool} synced`);
|
|
4306
4959
|
}
|
|
4307
4960
|
}
|