facult 1.1.0 → 1.2.1
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 +312 -26
- package/package.json +1 -1
- package/src/agents.ts +26 -1
- package/src/ai-state.ts +27 -2
- package/src/ai.ts +1763 -0
- package/src/audit/update-index.ts +1 -0
- package/src/autosync.ts +96 -27
- package/src/builtin.ts +61 -0
- package/src/cli-context.ts +198 -0
- package/src/enable-disable.ts +1 -0
- package/src/global-docs.ts +50 -6
- package/src/graph-query.ts +175 -0
- package/src/graph.ts +119 -0
- package/src/index-builder.ts +1099 -41
- package/src/index.ts +445 -23
- package/src/manage.ts +1904 -187
- package/src/paths.ts +137 -5
- package/src/query.ts +135 -4
- package/src/remote.ts +140 -9
- package/src/trust-list.ts +1 -0
- package/src/trust.ts +1 -0
package/src/manage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
cp,
|
|
2
3
|
lstat,
|
|
3
4
|
mkdir,
|
|
4
5
|
readdir,
|
|
@@ -12,12 +13,29 @@ import { basename, dirname, join } from "node:path";
|
|
|
12
13
|
import { getAdapter } from "./adapters";
|
|
13
14
|
import { renderCanonicalText } from "./agents";
|
|
14
15
|
import { ensureAiIndexPath } from "./ai-state";
|
|
16
|
+
import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
|
|
17
|
+
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
18
|
+
import { contentHash, normalizeText } from "./conflicts";
|
|
15
19
|
import {
|
|
20
|
+
globalDocTargetPaths,
|
|
21
|
+
planToolConfigSync,
|
|
22
|
+
planToolGlobalDocsSync,
|
|
23
|
+
planToolRulesSync,
|
|
16
24
|
syncToolConfig,
|
|
17
25
|
syncToolGlobalDocs,
|
|
18
26
|
syncToolRules,
|
|
19
27
|
} from "./global-docs";
|
|
20
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
type AgentEntry,
|
|
30
|
+
buildIndex,
|
|
31
|
+
type FacultIndex,
|
|
32
|
+
type SkillEntry,
|
|
33
|
+
} from "./index-builder";
|
|
34
|
+
import {
|
|
35
|
+
facultGeneratedStateDir,
|
|
36
|
+
facultRootDir,
|
|
37
|
+
projectRootFromAiRoot,
|
|
38
|
+
} from "./paths";
|
|
21
39
|
|
|
22
40
|
export interface ManagedToolState {
|
|
23
41
|
tool: string;
|
|
@@ -37,6 +55,13 @@ export interface ManagedToolState {
|
|
|
37
55
|
globalAgentsOverrideBackup?: string | null;
|
|
38
56
|
rulesBackup?: string | null;
|
|
39
57
|
toolConfigBackup?: string | null;
|
|
58
|
+
renderedTargets?: Record<string, ManagedRenderedTargetState>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ManagedRenderedTargetState {
|
|
62
|
+
hash: string;
|
|
63
|
+
sourcePath: string;
|
|
64
|
+
sourceKind: "builtin" | "canonical";
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
export interface ManagedState {
|
|
@@ -59,6 +84,10 @@ export interface ManageOptions {
|
|
|
59
84
|
rootDir?: string;
|
|
60
85
|
toolPaths?: Record<string, ToolPaths>;
|
|
61
86
|
now?: () => Date;
|
|
87
|
+
dryRun?: boolean;
|
|
88
|
+
adoptExisting?: boolean;
|
|
89
|
+
existingConflictMode?: "keep-canonical" | "keep-existing";
|
|
90
|
+
builtinConflictMode?: "warn" | "overwrite";
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
export interface SyncOptions {
|
|
@@ -66,6 +95,7 @@ export interface SyncOptions {
|
|
|
66
95
|
rootDir?: string;
|
|
67
96
|
tool?: string;
|
|
68
97
|
dryRun?: boolean;
|
|
98
|
+
builtinConflictMode?: "warn" | "overwrite";
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
const MANAGED_VERSION = 1 as const;
|
|
@@ -101,26 +131,48 @@ async function ensureDir(p: string) {
|
|
|
101
131
|
await mkdir(p, { recursive: true });
|
|
102
132
|
}
|
|
103
133
|
|
|
104
|
-
function
|
|
134
|
+
function renderedSourceKindForPath(
|
|
135
|
+
sourcePath: string
|
|
136
|
+
): ManagedRenderedTargetState["sourceKind"] {
|
|
137
|
+
return sourcePath.startsWith(facultBuiltinPackRoot())
|
|
138
|
+
? "builtin"
|
|
139
|
+
: "canonical";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderedHash(text: string): string {
|
|
143
|
+
return contentHash(normalizeText(text));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function defaultToolPaths(
|
|
147
|
+
home: string,
|
|
148
|
+
rootDir?: string
|
|
149
|
+
): Record<string, ToolPaths> {
|
|
150
|
+
const projectRoot = rootDir ? projectRootFromAiRoot(rootDir, home) : null;
|
|
151
|
+
const toolBase = (...parts: string[]) =>
|
|
152
|
+
projectRoot ? join(projectRoot, ...parts) : homePath(home, ...parts);
|
|
105
153
|
const defaults: Record<string, ToolPaths> = {
|
|
106
154
|
cursor: {
|
|
107
155
|
tool: "cursor",
|
|
108
|
-
skillsDir:
|
|
109
|
-
|
|
156
|
+
skillsDir: toolBase(".cursor", "skills"),
|
|
157
|
+
toolHome: toolBase(".cursor"),
|
|
158
|
+
mcpConfig: toolBase(".cursor", "mcp.json"),
|
|
110
159
|
},
|
|
111
160
|
codex: {
|
|
112
161
|
tool: "codex",
|
|
113
|
-
skillsDir:
|
|
114
|
-
mcpConfig:
|
|
115
|
-
agentsDir:
|
|
116
|
-
toolHome:
|
|
117
|
-
rulesDir:
|
|
118
|
-
toolConfig:
|
|
162
|
+
skillsDir: toolBase(".codex", "skills"),
|
|
163
|
+
mcpConfig: toolBase(".codex", "mcp.json"),
|
|
164
|
+
agentsDir: toolBase(".codex", "agents"),
|
|
165
|
+
toolHome: toolBase(".codex"),
|
|
166
|
+
rulesDir: toolBase(".codex", "rules"),
|
|
167
|
+
toolConfig: toolBase(".codex", "config.toml"),
|
|
119
168
|
},
|
|
120
169
|
claude: {
|
|
121
170
|
tool: "claude",
|
|
122
|
-
skillsDir:
|
|
123
|
-
|
|
171
|
+
skillsDir: toolBase(".claude", "skills"),
|
|
172
|
+
toolHome: toolBase(".claude"),
|
|
173
|
+
mcpConfig: projectRoot
|
|
174
|
+
? toolBase(".claude", "mcp.json")
|
|
175
|
+
: homePath(home, ".claude.json"),
|
|
124
176
|
},
|
|
125
177
|
"claude-desktop": {
|
|
126
178
|
tool: "claude-desktop",
|
|
@@ -134,22 +186,25 @@ function defaultToolPaths(home: string): Record<string, ToolPaths> {
|
|
|
134
186
|
},
|
|
135
187
|
clawdbot: {
|
|
136
188
|
tool: "clawdbot",
|
|
137
|
-
skillsDir:
|
|
138
|
-
mcpConfig:
|
|
189
|
+
skillsDir: toolBase(".clawdbot", "skills"),
|
|
190
|
+
mcpConfig: toolBase(".clawdbot", "mcp.json"),
|
|
139
191
|
},
|
|
140
192
|
gemini: {
|
|
141
193
|
tool: "gemini",
|
|
142
|
-
skillsDir:
|
|
143
|
-
mcpConfig:
|
|
194
|
+
skillsDir: toolBase(".gemini", "skills"),
|
|
195
|
+
mcpConfig: toolBase(".gemini", "mcp.json"),
|
|
144
196
|
},
|
|
145
197
|
antigravity: {
|
|
146
198
|
tool: "antigravity",
|
|
147
|
-
skillsDir:
|
|
148
|
-
mcpConfig:
|
|
199
|
+
skillsDir: toolBase(".antigravity", "skills"),
|
|
200
|
+
mcpConfig: toolBase(".antigravity", "mcp.json"),
|
|
149
201
|
},
|
|
150
202
|
};
|
|
151
203
|
|
|
152
204
|
const adapterDefaults = (tool: string): ToolPaths | null => {
|
|
205
|
+
if (projectRoot) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
153
208
|
const adapter = getAdapter(tool);
|
|
154
209
|
if (!adapter?.getDefaultPaths) {
|
|
155
210
|
return null;
|
|
@@ -188,17 +243,23 @@ function defaultToolPaths(home: string): Record<string, ToolPaths> {
|
|
|
188
243
|
async function resolveToolPaths(
|
|
189
244
|
tool: string,
|
|
190
245
|
home: string,
|
|
246
|
+
rootDir?: string,
|
|
191
247
|
override?: Record<string, ToolPaths>
|
|
192
248
|
): Promise<ToolPaths | null> {
|
|
249
|
+
const defaults = defaultToolPaths(home, rootDir);
|
|
193
250
|
if (override?.[tool]) {
|
|
194
|
-
|
|
251
|
+
const base = defaults[tool] ?? null;
|
|
252
|
+
return base ? { ...base, ...override[tool] } : (override[tool] ?? null);
|
|
195
253
|
}
|
|
196
|
-
const defaults = defaultToolPaths(home);
|
|
197
254
|
const base = defaults[tool] ?? null;
|
|
198
255
|
if (!base) {
|
|
199
256
|
return null;
|
|
200
257
|
}
|
|
201
258
|
if (tool !== "codex") {
|
|
259
|
+
// Codex has built-in default global-doc, rules, and tool-config
|
|
260
|
+
// locations. Claude and Cursor have built-in global doc locations through
|
|
261
|
+
// the base defaults above. Other tools can still opt into additional
|
|
262
|
+
// file-backed surfaces explicitly via toolPaths overrides.
|
|
202
263
|
return base;
|
|
203
264
|
}
|
|
204
265
|
|
|
@@ -224,13 +285,21 @@ async function resolveToolPaths(
|
|
|
224
285
|
}
|
|
225
286
|
|
|
226
287
|
export function managedStatePath(home: string = homedir()): string {
|
|
227
|
-
return
|
|
288
|
+
return managedStatePathForRoot(home);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function managedStatePathForRoot(
|
|
292
|
+
home: string = homedir(),
|
|
293
|
+
rootDir?: string
|
|
294
|
+
): string {
|
|
295
|
+
return join(facultGeneratedStateDir({ home, rootDir }), "managed.json");
|
|
228
296
|
}
|
|
229
297
|
|
|
230
298
|
export async function loadManagedState(
|
|
231
|
-
home: string = homedir()
|
|
299
|
+
home: string = homedir(),
|
|
300
|
+
rootDir?: string
|
|
232
301
|
): Promise<ManagedState> {
|
|
233
|
-
const p =
|
|
302
|
+
const p = managedStatePathForRoot(home, rootDir);
|
|
234
303
|
if (!(await fileExists(p))) {
|
|
235
304
|
return { version: MANAGED_VERSION, tools: {} };
|
|
236
305
|
}
|
|
@@ -248,12 +317,13 @@ export async function loadManagedState(
|
|
|
248
317
|
|
|
249
318
|
export async function saveManagedState(
|
|
250
319
|
state: ManagedState,
|
|
251
|
-
home: string = homedir()
|
|
320
|
+
home: string = homedir(),
|
|
321
|
+
rootDir?: string
|
|
252
322
|
) {
|
|
253
|
-
const dir =
|
|
323
|
+
const dir = facultGeneratedStateDir({ home, rootDir });
|
|
254
324
|
await ensureDir(dir);
|
|
255
325
|
await Bun.write(
|
|
256
|
-
|
|
326
|
+
managedStatePathForRoot(home, rootDir),
|
|
257
327
|
`${JSON.stringify(state, null, 2)}\n`
|
|
258
328
|
);
|
|
259
329
|
}
|
|
@@ -293,10 +363,20 @@ async function readTextIfExists(p: string): Promise<string | null> {
|
|
|
293
363
|
return await Bun.file(p).text();
|
|
294
364
|
}
|
|
295
365
|
|
|
296
|
-
async function
|
|
297
|
-
|
|
366
|
+
async function readTomlFile(
|
|
367
|
+
pathValue: string
|
|
368
|
+
): Promise<Record<string, unknown> | null> {
|
|
369
|
+
const text = await readTextIfExists(pathValue);
|
|
370
|
+
if (text == null) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const parsed = Bun.TOML.parse(text);
|
|
374
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function loadAgentsFromRoot(
|
|
378
|
+
agentsRoot: string
|
|
298
379
|
): Promise<{ name: string; sourcePath: string; raw: string }[]> {
|
|
299
|
-
const agentsRoot = homePath(rootDir, "agents");
|
|
300
380
|
const entries = await readdir(agentsRoot, { withFileTypes: true }).catch(
|
|
301
381
|
() => [] as import("node:fs").Dirent[]
|
|
302
382
|
);
|
|
@@ -331,6 +411,95 @@ async function loadCanonicalAgents(
|
|
|
331
411
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
332
412
|
}
|
|
333
413
|
|
|
414
|
+
async function loadCanonicalAgents(
|
|
415
|
+
rootDir: string
|
|
416
|
+
): Promise<{ name: string; sourcePath: string; raw: string }[]> {
|
|
417
|
+
return await loadAgentsFromRoot(homePath(rootDir, "agents"));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function loadMergedIndex(
|
|
421
|
+
homeDir: string,
|
|
422
|
+
rootDir: string
|
|
423
|
+
): Promise<FacultIndex> {
|
|
424
|
+
const { path: indexPath } = await ensureAiIndexPath({
|
|
425
|
+
homeDir,
|
|
426
|
+
rootDir,
|
|
427
|
+
repair: true,
|
|
428
|
+
});
|
|
429
|
+
if (!(await fileExists(indexPath))) {
|
|
430
|
+
await buildIndex({ homeDir, rootDir, force: false });
|
|
431
|
+
}
|
|
432
|
+
return JSON.parse(await Bun.file(indexPath).text()) as FacultIndex;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function loadEnabledSkillEntries(args: {
|
|
436
|
+
homeDir: string;
|
|
437
|
+
rootDir: string;
|
|
438
|
+
tool: string;
|
|
439
|
+
}): Promise<{ name: string; path: string }[]> {
|
|
440
|
+
const index = await loadMergedIndex(args.homeDir, args.rootDir);
|
|
441
|
+
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(args.rootDir);
|
|
442
|
+
const out: { name: string; path: string }[] = [];
|
|
443
|
+
|
|
444
|
+
for (const [name, entry] of Object.entries(index.skills)) {
|
|
445
|
+
const skill = entry as SkillEntry;
|
|
446
|
+
if (
|
|
447
|
+
!useBuiltinDefaults &&
|
|
448
|
+
skill.sourceKind === "builtin" &&
|
|
449
|
+
skill.sourceRoot?.includes("facult-operating-model")
|
|
450
|
+
) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (
|
|
454
|
+
Array.isArray(skill.enabledFor) &&
|
|
455
|
+
!skill.enabledFor.includes(args.tool)
|
|
456
|
+
) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
out.push({ name, path: skill.path });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (out.length > 0) {
|
|
463
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return (await listSkillDirs(join(args.rootDir, "skills"))).map((name) => ({
|
|
467
|
+
name,
|
|
468
|
+
path: join(args.rootDir, "skills", name),
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function loadManagedAgentEntries(args: {
|
|
473
|
+
homeDir: string;
|
|
474
|
+
rootDir: string;
|
|
475
|
+
}): Promise<{ name: string; sourcePath: string; raw: string }[]> {
|
|
476
|
+
const index = await loadMergedIndex(args.homeDir, args.rootDir);
|
|
477
|
+
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(args.rootDir);
|
|
478
|
+
const out: { name: string; sourcePath: string; raw: string }[] = [];
|
|
479
|
+
|
|
480
|
+
for (const [name, entry] of Object.entries(index.agents)) {
|
|
481
|
+
const agent = entry as AgentEntry;
|
|
482
|
+
if (
|
|
483
|
+
!useBuiltinDefaults &&
|
|
484
|
+
agent.sourceKind === "builtin" &&
|
|
485
|
+
agent.sourceRoot?.includes("facult-operating-model")
|
|
486
|
+
) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const raw = await readTextIfExists(agent.path);
|
|
490
|
+
if (raw == null) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
out.push({ name, sourcePath: agent.path, raw });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (out.length > 0) {
|
|
497
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return await loadCanonicalAgents(args.rootDir);
|
|
501
|
+
}
|
|
502
|
+
|
|
334
503
|
async function planAgentFileChanges({
|
|
335
504
|
agentsDir,
|
|
336
505
|
homeDir,
|
|
@@ -345,9 +514,11 @@ async function planAgentFileChanges({
|
|
|
345
514
|
add: string[];
|
|
346
515
|
remove: string[];
|
|
347
516
|
contents: Map<string, string>;
|
|
517
|
+
sources: Map<string, string>;
|
|
348
518
|
}> {
|
|
349
|
-
const agents = await
|
|
519
|
+
const agents = await loadManagedAgentEntries({ homeDir, rootDir });
|
|
350
520
|
const contents = new Map<string, string>();
|
|
521
|
+
const sources = new Map<string, string>();
|
|
351
522
|
const desiredPaths = new Set<string>();
|
|
352
523
|
|
|
353
524
|
for (const agent of agents) {
|
|
@@ -355,11 +526,13 @@ async function planAgentFileChanges({
|
|
|
355
526
|
const rendered = await renderCanonicalText(agent.raw, {
|
|
356
527
|
homeDir,
|
|
357
528
|
rootDir,
|
|
529
|
+
projectRoot: projectRootFromAiRoot(rootDir, homeDir) ?? undefined,
|
|
358
530
|
targetTool: tool,
|
|
359
531
|
targetPath: target,
|
|
360
532
|
});
|
|
361
533
|
desiredPaths.add(target);
|
|
362
534
|
contents.set(target, rendered);
|
|
535
|
+
sources.set(target, agent.sourcePath);
|
|
363
536
|
}
|
|
364
537
|
|
|
365
538
|
const existing = await readdir(agentsDir, { withFileTypes: true }).catch(
|
|
@@ -396,6 +569,7 @@ async function planAgentFileChanges({
|
|
|
396
569
|
add: Array.from(add).sort(),
|
|
397
570
|
remove: Array.from(remove).sort(),
|
|
398
571
|
contents,
|
|
572
|
+
sources,
|
|
399
573
|
};
|
|
400
574
|
}
|
|
401
575
|
|
|
@@ -446,31 +620,6 @@ async function listSkillDirs(skillsRoot: string): Promise<string[]> {
|
|
|
446
620
|
}
|
|
447
621
|
}
|
|
448
622
|
|
|
449
|
-
function skillNamesFromIndex(
|
|
450
|
-
indexData: Record<string, unknown>,
|
|
451
|
-
tool: string
|
|
452
|
-
): string[] {
|
|
453
|
-
const skills = indexData.skills as Record<string, unknown> | undefined;
|
|
454
|
-
if (!skills) {
|
|
455
|
-
return [];
|
|
456
|
-
}
|
|
457
|
-
const names: string[] = [];
|
|
458
|
-
for (const [name, entry] of Object.entries(skills)) {
|
|
459
|
-
if (!isPlainObject(entry)) {
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
const enabledFor = entry.enabledFor;
|
|
463
|
-
if (Array.isArray(enabledFor)) {
|
|
464
|
-
if (enabledFor.includes(tool)) {
|
|
465
|
-
names.push(name);
|
|
466
|
-
}
|
|
467
|
-
} else {
|
|
468
|
-
names.push(name);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return names.sort();
|
|
472
|
-
}
|
|
473
|
-
|
|
474
623
|
async function loadEnabledSkillNames({
|
|
475
624
|
homeDir,
|
|
476
625
|
rootDir,
|
|
@@ -480,24 +629,8 @@ async function loadEnabledSkillNames({
|
|
|
480
629
|
rootDir: string;
|
|
481
630
|
tool: string;
|
|
482
631
|
}): Promise<string[]> {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
rootDir,
|
|
486
|
-
repair: true,
|
|
487
|
-
});
|
|
488
|
-
if (await fileExists(indexPath)) {
|
|
489
|
-
try {
|
|
490
|
-
const txt = await Bun.file(indexPath).text();
|
|
491
|
-
const parsed = JSON.parse(txt) as Record<string, unknown>;
|
|
492
|
-
const names = skillNamesFromIndex(parsed, tool);
|
|
493
|
-
if (names.length) {
|
|
494
|
-
return names;
|
|
495
|
-
}
|
|
496
|
-
} catch {
|
|
497
|
-
// fallthrough to directory listing
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return await listSkillDirs(join(rootDir, "skills"));
|
|
632
|
+
const entries = await loadEnabledSkillEntries({ homeDir, rootDir, tool });
|
|
633
|
+
return entries.map((entry) => entry.name);
|
|
501
634
|
}
|
|
502
635
|
|
|
503
636
|
function extractServersObject(parsed: unknown): Record<string, unknown> | null {
|
|
@@ -589,6 +722,773 @@ async function ensureEmptyDir(p: string) {
|
|
|
589
722
|
await ensureDir(p);
|
|
590
723
|
}
|
|
591
724
|
|
|
725
|
+
async function adoptExistingToolSkills({
|
|
726
|
+
rootDir,
|
|
727
|
+
toolSkillsDir,
|
|
728
|
+
conflictMode,
|
|
729
|
+
}: {
|
|
730
|
+
rootDir: string;
|
|
731
|
+
toolSkillsDir: string;
|
|
732
|
+
conflictMode?: "keep-canonical" | "keep-existing";
|
|
733
|
+
}): Promise<{ adopted: string[]; skipped: string[] }> {
|
|
734
|
+
const adopted: string[] = [];
|
|
735
|
+
const skipped: string[] = [];
|
|
736
|
+
|
|
737
|
+
const entries = await readdir(toolSkillsDir, { withFileTypes: true }).catch(
|
|
738
|
+
() => [] as import("node:fs").Dirent[]
|
|
739
|
+
);
|
|
740
|
+
if (entries.length === 0) {
|
|
741
|
+
return { adopted, skipped };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const canonicalSkillsDir = join(rootDir, "skills");
|
|
745
|
+
await ensureDir(canonicalSkillsDir);
|
|
746
|
+
|
|
747
|
+
for (const entry of entries) {
|
|
748
|
+
if (!entry.isDirectory()) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (entry.name.startsWith(".")) {
|
|
752
|
+
skipped.push(entry.name);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const existingSkillDir = join(toolSkillsDir, entry.name);
|
|
757
|
+
const existingSkillFile = join(existingSkillDir, "SKILL.md");
|
|
758
|
+
if (!(await fileExists(existingSkillFile))) {
|
|
759
|
+
skipped.push(entry.name);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const canonicalSkillDir = join(canonicalSkillsDir, entry.name);
|
|
764
|
+
const canonicalSkillFile = join(canonicalSkillDir, "SKILL.md");
|
|
765
|
+
if (await fileExists(canonicalSkillFile)) {
|
|
766
|
+
if (conflictMode !== "keep-existing") {
|
|
767
|
+
skipped.push(entry.name);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
await rm(canonicalSkillDir, { recursive: true, force: true });
|
|
771
|
+
await cp(existingSkillDir, canonicalSkillDir, { recursive: true });
|
|
772
|
+
adopted.push(entry.name);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
await cp(existingSkillDir, canonicalSkillDir, { recursive: true });
|
|
777
|
+
adopted.push(entry.name);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return {
|
|
781
|
+
adopted: adopted.sort(),
|
|
782
|
+
skipped: skipped.sort(),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function adoptSkillsIntoCanonicalStore(args: {
|
|
787
|
+
homeDir: string;
|
|
788
|
+
rootDir: string;
|
|
789
|
+
skillSourceDirs: string[];
|
|
790
|
+
}): Promise<string[]> {
|
|
791
|
+
const adopted = new Set<string>();
|
|
792
|
+
|
|
793
|
+
for (const dir of args.skillSourceDirs) {
|
|
794
|
+
if (!dir) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
const result = await adoptExistingToolSkills({
|
|
798
|
+
rootDir: args.rootDir,
|
|
799
|
+
toolSkillsDir: dir,
|
|
800
|
+
conflictMode: "keep-canonical",
|
|
801
|
+
});
|
|
802
|
+
for (const name of result.adopted) {
|
|
803
|
+
adopted.add(name);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (adopted.size > 0) {
|
|
808
|
+
await buildIndex({
|
|
809
|
+
homeDir: args.homeDir,
|
|
810
|
+
rootDir: args.rootDir,
|
|
811
|
+
force: false,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return Array.from(adopted).sort();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
interface ExistingSkillConflict {
|
|
819
|
+
name: string;
|
|
820
|
+
livePath: string;
|
|
821
|
+
canonicalPath: string;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
interface ExistingSkillPlan {
|
|
825
|
+
adopt: string[];
|
|
826
|
+
identical: string[];
|
|
827
|
+
conflicts: ExistingSkillConflict[];
|
|
828
|
+
ignored: string[];
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function readTextOrNull(pathValue: string): Promise<string | null> {
|
|
832
|
+
if (!(await fileExists(pathValue))) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
return await Bun.file(pathValue).text();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function planExistingToolSkillAdoption(args: {
|
|
839
|
+
rootDir: string;
|
|
840
|
+
toolSkillsDir: string;
|
|
841
|
+
}): Promise<ExistingSkillPlan> {
|
|
842
|
+
const adopt: string[] = [];
|
|
843
|
+
const identical: string[] = [];
|
|
844
|
+
const conflicts: ExistingSkillConflict[] = [];
|
|
845
|
+
const ignored: string[] = [];
|
|
846
|
+
|
|
847
|
+
const entries = await readdir(args.toolSkillsDir, {
|
|
848
|
+
withFileTypes: true,
|
|
849
|
+
}).catch(() => [] as import("node:fs").Dirent[]);
|
|
850
|
+
|
|
851
|
+
for (const entry of entries) {
|
|
852
|
+
if (!entry.isDirectory()) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (entry.name.startsWith(".")) {
|
|
856
|
+
ignored.push(entry.name);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const liveSkillDir = join(args.toolSkillsDir, entry.name);
|
|
861
|
+
const liveSkillFile = join(liveSkillDir, "SKILL.md");
|
|
862
|
+
if (!(await fileExists(liveSkillFile))) {
|
|
863
|
+
ignored.push(entry.name);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const canonicalSkillDir = join(args.rootDir, "skills", entry.name);
|
|
868
|
+
const canonicalSkillFile = join(canonicalSkillDir, "SKILL.md");
|
|
869
|
+
if (!(await fileExists(canonicalSkillFile))) {
|
|
870
|
+
adopt.push(entry.name);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const [liveText, canonicalText] = await Promise.all([
|
|
875
|
+
readTextOrNull(liveSkillFile),
|
|
876
|
+
readTextOrNull(canonicalSkillFile),
|
|
877
|
+
]);
|
|
878
|
+
if (liveText === canonicalText) {
|
|
879
|
+
identical.push(entry.name);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
conflicts.push({
|
|
884
|
+
name: entry.name,
|
|
885
|
+
livePath: liveSkillDir,
|
|
886
|
+
canonicalPath: canonicalSkillDir,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
adopt: adopt.sort(),
|
|
892
|
+
identical: identical.sort(),
|
|
893
|
+
conflicts: conflicts.sort((a, b) => a.name.localeCompare(b.name)),
|
|
894
|
+
ignored: ignored.sort(),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function logManagePreflight(tool: string, plan: ExistingSkillPlan) {
|
|
899
|
+
if (
|
|
900
|
+
plan.adopt.length === 0 &&
|
|
901
|
+
plan.conflicts.length === 0 &&
|
|
902
|
+
plan.identical.length === 0 &&
|
|
903
|
+
plan.ignored.length === 0
|
|
904
|
+
) {
|
|
905
|
+
console.log(`${tool}: no existing tool-native skills detected`);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
for (const name of plan.adopt) {
|
|
909
|
+
console.log(
|
|
910
|
+
`${tool}: would adopt existing skill ${name} into canonical store`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
for (const name of plan.identical) {
|
|
914
|
+
console.log(
|
|
915
|
+
`${tool}: existing skill ${name} already matches canonical store`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
for (const conflict of plan.conflicts) {
|
|
919
|
+
console.log(
|
|
920
|
+
`${tool}: conflict for skill ${conflict.name} (live ${conflict.livePath} vs canonical ${conflict.canonicalPath})`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
for (const name of plan.ignored) {
|
|
924
|
+
console.log(`${tool}: would ignore existing entry ${name}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
interface ExistingManagedItem {
|
|
929
|
+
kind:
|
|
930
|
+
| "skill"
|
|
931
|
+
| "agent"
|
|
932
|
+
| "global-doc"
|
|
933
|
+
| "rule"
|
|
934
|
+
| "tool-config"
|
|
935
|
+
| "mcp-server";
|
|
936
|
+
name: string;
|
|
937
|
+
livePath: string;
|
|
938
|
+
canonicalPath: string;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
interface ExistingManagedImportPlan {
|
|
942
|
+
adopt: ExistingManagedItem[];
|
|
943
|
+
identical: ExistingManagedItem[];
|
|
944
|
+
conflicts: ExistingManagedItem[];
|
|
945
|
+
ignored: ExistingManagedItem[];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function emptyManagedImportPlan(): ExistingManagedImportPlan {
|
|
949
|
+
return {
|
|
950
|
+
adopt: [],
|
|
951
|
+
identical: [],
|
|
952
|
+
conflicts: [],
|
|
953
|
+
ignored: [],
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function mergeManagedImportPlans(
|
|
958
|
+
...plans: ExistingManagedImportPlan[]
|
|
959
|
+
): ExistingManagedImportPlan {
|
|
960
|
+
const merged = emptyManagedImportPlan();
|
|
961
|
+
for (const plan of plans) {
|
|
962
|
+
merged.adopt.push(...plan.adopt);
|
|
963
|
+
merged.identical.push(...plan.identical);
|
|
964
|
+
merged.conflicts.push(...plan.conflicts);
|
|
965
|
+
merged.ignored.push(...plan.ignored);
|
|
966
|
+
}
|
|
967
|
+
const sortItems = (items: ExistingManagedItem[]) =>
|
|
968
|
+
items.sort((a, b) =>
|
|
969
|
+
`${a.kind}:${a.name}`.localeCompare(`${b.kind}:${b.name}`)
|
|
970
|
+
);
|
|
971
|
+
return {
|
|
972
|
+
adopt: sortItems(merged.adopt),
|
|
973
|
+
identical: sortItems(merged.identical),
|
|
974
|
+
conflicts: sortItems(merged.conflicts),
|
|
975
|
+
ignored: sortItems(merged.ignored),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function asManagedSkillPlan(
|
|
980
|
+
plan: ExistingSkillPlan
|
|
981
|
+
): ExistingManagedImportPlan {
|
|
982
|
+
return {
|
|
983
|
+
adopt: plan.adopt.map((name) => ({
|
|
984
|
+
kind: "skill" as const,
|
|
985
|
+
name,
|
|
986
|
+
livePath: "",
|
|
987
|
+
canonicalPath: "",
|
|
988
|
+
})),
|
|
989
|
+
identical: plan.identical.map((name) => ({
|
|
990
|
+
kind: "skill" as const,
|
|
991
|
+
name,
|
|
992
|
+
livePath: "",
|
|
993
|
+
canonicalPath: "",
|
|
994
|
+
})),
|
|
995
|
+
conflicts: plan.conflicts.map((item) => ({
|
|
996
|
+
kind: "skill" as const,
|
|
997
|
+
name: item.name,
|
|
998
|
+
livePath: item.livePath,
|
|
999
|
+
canonicalPath: item.canonicalPath,
|
|
1000
|
+
})),
|
|
1001
|
+
ignored: plan.ignored.map((name) => ({
|
|
1002
|
+
kind: "skill" as const,
|
|
1003
|
+
name,
|
|
1004
|
+
livePath: "",
|
|
1005
|
+
canonicalPath: "",
|
|
1006
|
+
})),
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function formatManagedItem(item: ExistingManagedItem): string {
|
|
1011
|
+
return item.kind === "global-doc" || item.kind === "tool-config"
|
|
1012
|
+
? `${item.kind}:${item.name}`
|
|
1013
|
+
: `${item.kind}:${item.name}`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function logManagedImportPlan(tool: string, plan: ExistingManagedImportPlan) {
|
|
1017
|
+
if (
|
|
1018
|
+
plan.adopt.length === 0 &&
|
|
1019
|
+
plan.identical.length === 0 &&
|
|
1020
|
+
plan.conflicts.length === 0 &&
|
|
1021
|
+
plan.ignored.length === 0
|
|
1022
|
+
) {
|
|
1023
|
+
console.log(`${tool}: no existing managed content detected`);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
for (const item of plan.adopt) {
|
|
1027
|
+
console.log(
|
|
1028
|
+
`${tool}: would adopt existing ${formatManagedItem(item)} into canonical store`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
for (const item of plan.identical) {
|
|
1032
|
+
console.log(
|
|
1033
|
+
`${tool}: existing ${formatManagedItem(item)} already matches canonical store`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
for (const item of plan.conflicts) {
|
|
1037
|
+
console.log(
|
|
1038
|
+
`${tool}: conflict for ${formatManagedItem(item)} (live ${item.livePath} vs canonical ${item.canonicalPath})`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
for (const item of plan.ignored) {
|
|
1042
|
+
console.log(`${tool}: would ignore existing ${formatManagedItem(item)}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function planExistingToolAgentAdoption(args: {
|
|
1047
|
+
rootDir: string;
|
|
1048
|
+
agentsDir: string;
|
|
1049
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1050
|
+
const plan = emptyManagedImportPlan();
|
|
1051
|
+
const agents = await loadAgentsFromRoot(args.agentsDir);
|
|
1052
|
+
for (const agent of agents) {
|
|
1053
|
+
const canonicalPath = join(
|
|
1054
|
+
args.rootDir,
|
|
1055
|
+
"agents",
|
|
1056
|
+
agent.name,
|
|
1057
|
+
"agent.toml"
|
|
1058
|
+
);
|
|
1059
|
+
const canonicalRaw = await readTextOrNull(canonicalPath);
|
|
1060
|
+
const item: ExistingManagedItem = {
|
|
1061
|
+
kind: "agent",
|
|
1062
|
+
name: agent.name,
|
|
1063
|
+
livePath: agent.sourcePath,
|
|
1064
|
+
canonicalPath,
|
|
1065
|
+
};
|
|
1066
|
+
if (canonicalRaw == null) {
|
|
1067
|
+
plan.adopt.push(item);
|
|
1068
|
+
} else if (canonicalRaw === agent.raw) {
|
|
1069
|
+
plan.identical.push(item);
|
|
1070
|
+
} else {
|
|
1071
|
+
plan.conflicts.push(item);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return mergeManagedImportPlans(plan);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function adoptExistingToolAgents(args: {
|
|
1078
|
+
rootDir: string;
|
|
1079
|
+
agentsDir: string;
|
|
1080
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1081
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1082
|
+
const adopted: ExistingManagedItem[] = [];
|
|
1083
|
+
const agents = await loadAgentsFromRoot(args.agentsDir);
|
|
1084
|
+
for (const agent of agents) {
|
|
1085
|
+
const canonicalPath = join(
|
|
1086
|
+
args.rootDir,
|
|
1087
|
+
"agents",
|
|
1088
|
+
agent.name,
|
|
1089
|
+
"agent.toml"
|
|
1090
|
+
);
|
|
1091
|
+
const canonicalRaw = await readTextOrNull(canonicalPath);
|
|
1092
|
+
if (canonicalRaw != null && args.conflictMode !== "keep-existing") {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
await ensureDir(dirname(canonicalPath));
|
|
1096
|
+
await Bun.write(
|
|
1097
|
+
canonicalPath,
|
|
1098
|
+
agent.raw.endsWith("\n") ? agent.raw : `${agent.raw}\n`
|
|
1099
|
+
);
|
|
1100
|
+
adopted.push({
|
|
1101
|
+
kind: "agent",
|
|
1102
|
+
name: agent.name,
|
|
1103
|
+
livePath: agent.sourcePath,
|
|
1104
|
+
canonicalPath,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return adopted;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function planExistingGlobalDocAdoption(args: {
|
|
1111
|
+
rootDir: string;
|
|
1112
|
+
tool: string;
|
|
1113
|
+
toolHome: string;
|
|
1114
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1115
|
+
const targets = globalDocTargetPaths(args.tool, args.toolHome);
|
|
1116
|
+
const mappings = [
|
|
1117
|
+
{
|
|
1118
|
+
name: basename(targets.primary),
|
|
1119
|
+
livePath: targets.primary,
|
|
1120
|
+
canonicalPath: join(args.rootDir, "AGENTS.global.md"),
|
|
1121
|
+
},
|
|
1122
|
+
...(targets.override
|
|
1123
|
+
? [
|
|
1124
|
+
{
|
|
1125
|
+
name: basename(targets.override),
|
|
1126
|
+
livePath: targets.override,
|
|
1127
|
+
canonicalPath: join(args.rootDir, "AGENTS.override.global.md"),
|
|
1128
|
+
},
|
|
1129
|
+
]
|
|
1130
|
+
: []),
|
|
1131
|
+
];
|
|
1132
|
+
const plan = emptyManagedImportPlan();
|
|
1133
|
+
for (const mapping of mappings) {
|
|
1134
|
+
const liveRaw = await readTextOrNull(mapping.livePath);
|
|
1135
|
+
if (liveRaw == null) {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
const canonicalRaw = await readTextOrNull(mapping.canonicalPath);
|
|
1139
|
+
const item: ExistingManagedItem = {
|
|
1140
|
+
kind: "global-doc",
|
|
1141
|
+
name: mapping.name,
|
|
1142
|
+
livePath: mapping.livePath,
|
|
1143
|
+
canonicalPath: mapping.canonicalPath,
|
|
1144
|
+
};
|
|
1145
|
+
if (canonicalRaw == null) {
|
|
1146
|
+
plan.adopt.push(item);
|
|
1147
|
+
} else if (canonicalRaw === liveRaw) {
|
|
1148
|
+
plan.identical.push(item);
|
|
1149
|
+
} else {
|
|
1150
|
+
plan.conflicts.push(item);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return mergeManagedImportPlans(plan);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function adoptExistingGlobalDocs(args: {
|
|
1157
|
+
rootDir: string;
|
|
1158
|
+
tool: string;
|
|
1159
|
+
toolHome: string;
|
|
1160
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1161
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1162
|
+
const adopted: ExistingManagedItem[] = [];
|
|
1163
|
+
const targets = globalDocTargetPaths(args.tool, args.toolHome);
|
|
1164
|
+
const mappings = [
|
|
1165
|
+
{
|
|
1166
|
+
name: basename(targets.primary),
|
|
1167
|
+
livePath: targets.primary,
|
|
1168
|
+
canonicalPath: join(args.rootDir, "AGENTS.global.md"),
|
|
1169
|
+
},
|
|
1170
|
+
...(targets.override
|
|
1171
|
+
? [
|
|
1172
|
+
{
|
|
1173
|
+
name: basename(targets.override),
|
|
1174
|
+
livePath: targets.override,
|
|
1175
|
+
canonicalPath: join(args.rootDir, "AGENTS.override.global.md"),
|
|
1176
|
+
},
|
|
1177
|
+
]
|
|
1178
|
+
: []),
|
|
1179
|
+
];
|
|
1180
|
+
for (const mapping of mappings) {
|
|
1181
|
+
const liveRaw = await readTextOrNull(mapping.livePath);
|
|
1182
|
+
if (liveRaw == null) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
if (
|
|
1186
|
+
(await readTextOrNull(mapping.canonicalPath)) != null &&
|
|
1187
|
+
args.conflictMode !== "keep-existing"
|
|
1188
|
+
) {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
await ensureDir(dirname(mapping.canonicalPath));
|
|
1192
|
+
await Bun.write(
|
|
1193
|
+
mapping.canonicalPath,
|
|
1194
|
+
liveRaw.endsWith("\n") ? liveRaw : `${liveRaw}\n`
|
|
1195
|
+
);
|
|
1196
|
+
adopted.push({
|
|
1197
|
+
kind: "global-doc",
|
|
1198
|
+
name: mapping.name,
|
|
1199
|
+
livePath: mapping.livePath,
|
|
1200
|
+
canonicalPath: mapping.canonicalPath,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
return adopted;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async function adoptExistingGlobalDocFile(args: {
|
|
1207
|
+
sourcePath: string;
|
|
1208
|
+
canonicalPath: string;
|
|
1209
|
+
name: string;
|
|
1210
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1211
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1212
|
+
const liveRaw = await readTextOrNull(args.sourcePath);
|
|
1213
|
+
if (liveRaw == null) {
|
|
1214
|
+
return [];
|
|
1215
|
+
}
|
|
1216
|
+
if (
|
|
1217
|
+
(await readTextOrNull(args.canonicalPath)) != null &&
|
|
1218
|
+
args.conflictMode !== "keep-existing"
|
|
1219
|
+
) {
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
await ensureDir(dirname(args.canonicalPath));
|
|
1223
|
+
await Bun.write(
|
|
1224
|
+
args.canonicalPath,
|
|
1225
|
+
liveRaw.endsWith("\n") ? liveRaw : `${liveRaw}\n`
|
|
1226
|
+
);
|
|
1227
|
+
return [
|
|
1228
|
+
{
|
|
1229
|
+
kind: "global-doc",
|
|
1230
|
+
name: args.name,
|
|
1231
|
+
livePath: args.sourcePath,
|
|
1232
|
+
canonicalPath: args.canonicalPath,
|
|
1233
|
+
},
|
|
1234
|
+
];
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function planExistingRuleAdoption(args: {
|
|
1238
|
+
rootDir: string;
|
|
1239
|
+
tool: string;
|
|
1240
|
+
rulesDir: string;
|
|
1241
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1242
|
+
const plan = emptyManagedImportPlan();
|
|
1243
|
+
const entries = await readdir(args.rulesDir, { withFileTypes: true }).catch(
|
|
1244
|
+
() => [] as import("node:fs").Dirent[]
|
|
1245
|
+
);
|
|
1246
|
+
for (const entry of entries) {
|
|
1247
|
+
if (!(entry.isFile() && entry.name.endsWith(".rules"))) {
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
const livePath = join(args.rulesDir, entry.name);
|
|
1251
|
+
const canonicalPath = join(
|
|
1252
|
+
args.rootDir,
|
|
1253
|
+
"tools",
|
|
1254
|
+
args.tool,
|
|
1255
|
+
"rules",
|
|
1256
|
+
entry.name
|
|
1257
|
+
);
|
|
1258
|
+
const liveRaw = await readTextOrNull(livePath);
|
|
1259
|
+
if (liveRaw == null) {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const canonicalRaw = await readTextOrNull(canonicalPath);
|
|
1263
|
+
const item: ExistingManagedItem = {
|
|
1264
|
+
kind: "rule",
|
|
1265
|
+
name: entry.name,
|
|
1266
|
+
livePath,
|
|
1267
|
+
canonicalPath,
|
|
1268
|
+
};
|
|
1269
|
+
if (canonicalRaw == null) {
|
|
1270
|
+
plan.adopt.push(item);
|
|
1271
|
+
} else if (canonicalRaw === liveRaw) {
|
|
1272
|
+
plan.identical.push(item);
|
|
1273
|
+
} else {
|
|
1274
|
+
plan.conflicts.push(item);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return mergeManagedImportPlans(plan);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function adoptExistingRules(args: {
|
|
1281
|
+
rootDir: string;
|
|
1282
|
+
tool: string;
|
|
1283
|
+
rulesDir: string;
|
|
1284
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1285
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1286
|
+
const adopted: ExistingManagedItem[] = [];
|
|
1287
|
+
const entries = await readdir(args.rulesDir, { withFileTypes: true }).catch(
|
|
1288
|
+
() => [] as import("node:fs").Dirent[]
|
|
1289
|
+
);
|
|
1290
|
+
for (const entry of entries) {
|
|
1291
|
+
if (!(entry.isFile() && entry.name.endsWith(".rules"))) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
const livePath = join(args.rulesDir, entry.name);
|
|
1295
|
+
const canonicalPath = join(
|
|
1296
|
+
args.rootDir,
|
|
1297
|
+
"tools",
|
|
1298
|
+
args.tool,
|
|
1299
|
+
"rules",
|
|
1300
|
+
entry.name
|
|
1301
|
+
);
|
|
1302
|
+
const liveRaw = await readTextOrNull(livePath);
|
|
1303
|
+
if (liveRaw == null) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (
|
|
1307
|
+
(await readTextOrNull(canonicalPath)) != null &&
|
|
1308
|
+
args.conflictMode !== "keep-existing"
|
|
1309
|
+
) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
await ensureDir(dirname(canonicalPath));
|
|
1313
|
+
await Bun.write(
|
|
1314
|
+
canonicalPath,
|
|
1315
|
+
liveRaw.endsWith("\n") ? liveRaw : `${liveRaw}\n`
|
|
1316
|
+
);
|
|
1317
|
+
adopted.push({
|
|
1318
|
+
kind: "rule",
|
|
1319
|
+
name: entry.name,
|
|
1320
|
+
livePath,
|
|
1321
|
+
canonicalPath,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
return adopted;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function planExistingToolConfigAdoption(args: {
|
|
1328
|
+
rootDir: string;
|
|
1329
|
+
tool: string;
|
|
1330
|
+
toolConfigPath: string;
|
|
1331
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1332
|
+
const plan = emptyManagedImportPlan();
|
|
1333
|
+
const liveRaw = await readTextOrNull(args.toolConfigPath);
|
|
1334
|
+
if (liveRaw == null) {
|
|
1335
|
+
return plan;
|
|
1336
|
+
}
|
|
1337
|
+
const canonicalPath = join(args.rootDir, "tools", args.tool, "config.toml");
|
|
1338
|
+
const canonicalRaw = await readTextOrNull(canonicalPath);
|
|
1339
|
+
const item: ExistingManagedItem = {
|
|
1340
|
+
kind: "tool-config",
|
|
1341
|
+
name: `${args.tool}/config.toml`,
|
|
1342
|
+
livePath: args.toolConfigPath,
|
|
1343
|
+
canonicalPath,
|
|
1344
|
+
};
|
|
1345
|
+
if (canonicalRaw == null) {
|
|
1346
|
+
plan.adopt.push(item);
|
|
1347
|
+
} else if (canonicalRaw === liveRaw) {
|
|
1348
|
+
plan.identical.push(item);
|
|
1349
|
+
} else {
|
|
1350
|
+
plan.conflicts.push(item);
|
|
1351
|
+
}
|
|
1352
|
+
return plan;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
async function adoptExistingToolConfig(args: {
|
|
1356
|
+
rootDir: string;
|
|
1357
|
+
tool: string;
|
|
1358
|
+
toolConfigPath: string;
|
|
1359
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1360
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1361
|
+
const liveRaw = await readTextOrNull(args.toolConfigPath);
|
|
1362
|
+
if (liveRaw == null) {
|
|
1363
|
+
return [];
|
|
1364
|
+
}
|
|
1365
|
+
const canonicalPath = join(args.rootDir, "tools", args.tool, "config.toml");
|
|
1366
|
+
if (
|
|
1367
|
+
(await readTextOrNull(canonicalPath)) != null &&
|
|
1368
|
+
args.conflictMode !== "keep-existing"
|
|
1369
|
+
) {
|
|
1370
|
+
return [];
|
|
1371
|
+
}
|
|
1372
|
+
await ensureDir(dirname(canonicalPath));
|
|
1373
|
+
await Bun.write(
|
|
1374
|
+
canonicalPath,
|
|
1375
|
+
liveRaw.endsWith("\n") ? liveRaw : `${liveRaw}\n`
|
|
1376
|
+
);
|
|
1377
|
+
return [
|
|
1378
|
+
{
|
|
1379
|
+
kind: "tool-config",
|
|
1380
|
+
name: `${args.tool}/config.toml`,
|
|
1381
|
+
livePath: args.toolConfigPath,
|
|
1382
|
+
canonicalPath,
|
|
1383
|
+
},
|
|
1384
|
+
];
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function normalizeCanonicalMcpServers(
|
|
1388
|
+
servers: Record<string, unknown>
|
|
1389
|
+
): string {
|
|
1390
|
+
return JSON.stringify({ servers }, null, 2);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
async function planExistingMcpAdoption(args: {
|
|
1394
|
+
rootDir: string;
|
|
1395
|
+
tool: string;
|
|
1396
|
+
mcpConfigPath: string;
|
|
1397
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1398
|
+
const plan = emptyManagedImportPlan();
|
|
1399
|
+
const liveConfig = await readTomlFile(args.mcpConfigPath).catch(() => null);
|
|
1400
|
+
const liveRawJson = await readTextIfExists(args.mcpConfigPath);
|
|
1401
|
+
let liveServers: Record<string, unknown> | null = null;
|
|
1402
|
+
if (liveConfig) {
|
|
1403
|
+
liveServers = extractServersObject(liveConfig);
|
|
1404
|
+
}
|
|
1405
|
+
if (!liveServers && liveRawJson != null) {
|
|
1406
|
+
try {
|
|
1407
|
+
liveServers = extractServersObject(JSON.parse(liveRawJson));
|
|
1408
|
+
} catch {
|
|
1409
|
+
liveServers = null;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (!liveServers || Object.keys(liveServers).length === 0) {
|
|
1413
|
+
return plan;
|
|
1414
|
+
}
|
|
1415
|
+
const canonical = await loadCanonicalServers(args.rootDir);
|
|
1416
|
+
const canonicalPath =
|
|
1417
|
+
canonical.sourcePath ?? join(args.rootDir, "mcp", "servers.json");
|
|
1418
|
+
for (const [name, definition] of Object.entries(liveServers)) {
|
|
1419
|
+
const item: ExistingManagedItem = {
|
|
1420
|
+
kind: "mcp-server",
|
|
1421
|
+
name,
|
|
1422
|
+
livePath: args.mcpConfigPath,
|
|
1423
|
+
canonicalPath,
|
|
1424
|
+
};
|
|
1425
|
+
if (!(name in canonical.servers)) {
|
|
1426
|
+
plan.adopt.push(item);
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (
|
|
1430
|
+
JSON.stringify(canonical.servers[name]) === JSON.stringify(definition)
|
|
1431
|
+
) {
|
|
1432
|
+
plan.identical.push(item);
|
|
1433
|
+
} else {
|
|
1434
|
+
plan.conflicts.push(item);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return plan;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function adoptExistingMcpServers(args: {
|
|
1441
|
+
rootDir: string;
|
|
1442
|
+
tool: string;
|
|
1443
|
+
mcpConfigPath: string;
|
|
1444
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1445
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1446
|
+
const liveRaw = await readTextIfExists(args.mcpConfigPath);
|
|
1447
|
+
if (liveRaw == null) {
|
|
1448
|
+
return [];
|
|
1449
|
+
}
|
|
1450
|
+
let liveServers: Record<string, unknown> | null = null;
|
|
1451
|
+
try {
|
|
1452
|
+
liveServers = extractServersObject(Bun.TOML.parse(liveRaw));
|
|
1453
|
+
} catch {
|
|
1454
|
+
liveServers = null;
|
|
1455
|
+
}
|
|
1456
|
+
if (!liveServers) {
|
|
1457
|
+
try {
|
|
1458
|
+
liveServers = extractServersObject(JSON.parse(liveRaw));
|
|
1459
|
+
} catch {
|
|
1460
|
+
liveServers = null;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
if (!liveServers) {
|
|
1464
|
+
return [];
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const canonical = await loadCanonicalServers(args.rootDir);
|
|
1468
|
+
const merged = { ...canonical.servers };
|
|
1469
|
+
const adopted: ExistingManagedItem[] = [];
|
|
1470
|
+
for (const [name, definition] of Object.entries(liveServers)) {
|
|
1471
|
+
if (!(name in merged) || args.conflictMode === "keep-existing") {
|
|
1472
|
+
merged[name] = definition;
|
|
1473
|
+
adopted.push({
|
|
1474
|
+
kind: "mcp-server",
|
|
1475
|
+
name,
|
|
1476
|
+
livePath: args.mcpConfigPath,
|
|
1477
|
+
canonicalPath:
|
|
1478
|
+
canonical.sourcePath ?? join(args.rootDir, "mcp", "servers.json"),
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (adopted.length === 0) {
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
const canonicalPath =
|
|
1486
|
+
canonical.sourcePath ?? join(args.rootDir, "mcp", "servers.json");
|
|
1487
|
+
await ensureDir(dirname(canonicalPath));
|
|
1488
|
+
await Bun.write(canonicalPath, `${normalizeCanonicalMcpServers(merged)}\n`);
|
|
1489
|
+
return adopted;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
592
1492
|
async function createSkillSymlinks({
|
|
593
1493
|
homeDir,
|
|
594
1494
|
toolSkillsDir,
|
|
@@ -601,17 +1501,17 @@ async function createSkillSymlinks({
|
|
|
601
1501
|
tool: string;
|
|
602
1502
|
}) {
|
|
603
1503
|
await ensureDir(toolSkillsDir);
|
|
604
|
-
const
|
|
1504
|
+
const skills = await loadEnabledSkillEntries({
|
|
605
1505
|
homeDir,
|
|
606
1506
|
rootDir,
|
|
607
1507
|
tool,
|
|
608
1508
|
});
|
|
609
|
-
for (const
|
|
610
|
-
const target =
|
|
1509
|
+
for (const skill of skills) {
|
|
1510
|
+
const target = skill.path;
|
|
611
1511
|
if (!(await fileExists(target))) {
|
|
612
1512
|
continue;
|
|
613
1513
|
}
|
|
614
|
-
const linkPath = join(toolSkillsDir, name);
|
|
1514
|
+
const linkPath = join(toolSkillsDir, skill.name);
|
|
615
1515
|
try {
|
|
616
1516
|
const st = await lstat(linkPath);
|
|
617
1517
|
if (st.isSymbolicLink()) {
|
|
@@ -625,6 +1525,34 @@ async function createSkillSymlinks({
|
|
|
625
1525
|
}
|
|
626
1526
|
}
|
|
627
1527
|
|
|
1528
|
+
function isPreservedToolSkillEntry(name: string): boolean {
|
|
1529
|
+
return name.startsWith(".");
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
async function restorePreservedToolSkillEntries({
|
|
1533
|
+
backupDir,
|
|
1534
|
+
toolSkillsDir,
|
|
1535
|
+
}: {
|
|
1536
|
+
backupDir: string | null | undefined;
|
|
1537
|
+
toolSkillsDir: string;
|
|
1538
|
+
}) {
|
|
1539
|
+
if (!(backupDir && (await fileExists(backupDir)))) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const entries = await readdir(backupDir, { withFileTypes: true }).catch(
|
|
1543
|
+
() => [] as import("node:fs").Dirent[]
|
|
1544
|
+
);
|
|
1545
|
+
for (const entry of entries) {
|
|
1546
|
+
if (!isPreservedToolSkillEntry(entry.name)) {
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
const source = join(backupDir, entry.name);
|
|
1550
|
+
const target = join(toolSkillsDir, entry.name);
|
|
1551
|
+
await rm(target, { recursive: true, force: true });
|
|
1552
|
+
await cp(source, target, { recursive: true });
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
628
1556
|
async function planSkillSymlinkChanges({
|
|
629
1557
|
homeDir,
|
|
630
1558
|
toolSkillsDir,
|
|
@@ -636,8 +1564,15 @@ async function planSkillSymlinkChanges({
|
|
|
636
1564
|
rootDir: string;
|
|
637
1565
|
tool: string;
|
|
638
1566
|
}): Promise<{ add: string[]; remove: string[] }> {
|
|
639
|
-
const
|
|
640
|
-
|
|
1567
|
+
const desiredEntries = await loadEnabledSkillEntries({
|
|
1568
|
+
homeDir,
|
|
1569
|
+
rootDir,
|
|
1570
|
+
tool,
|
|
1571
|
+
});
|
|
1572
|
+
const desiredTargets = new Map(
|
|
1573
|
+
desiredEntries.map((entry) => [entry.name, entry.path])
|
|
1574
|
+
);
|
|
1575
|
+
const desiredSet = new Set(desiredEntries.map((entry) => entry.name));
|
|
641
1576
|
const existing = await readdir(toolSkillsDir, { withFileTypes: true }).catch(
|
|
642
1577
|
() => [] as import("node:fs").Dirent[]
|
|
643
1578
|
);
|
|
@@ -646,12 +1581,19 @@ async function planSkillSymlinkChanges({
|
|
|
646
1581
|
const add: string[] = [];
|
|
647
1582
|
|
|
648
1583
|
for (const entry of existing) {
|
|
1584
|
+
if (isPreservedToolSkillEntry(entry.name)) {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
649
1587
|
if (!desiredSet.has(entry.name)) {
|
|
650
1588
|
remove.push(entry.name);
|
|
651
1589
|
continue;
|
|
652
1590
|
}
|
|
653
1591
|
const linkPath = join(toolSkillsDir, entry.name);
|
|
654
|
-
const target =
|
|
1592
|
+
const target = desiredTargets.get(entry.name);
|
|
1593
|
+
if (!target) {
|
|
1594
|
+
remove.push(entry.name);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
655
1597
|
try {
|
|
656
1598
|
const st = await lstat(linkPath);
|
|
657
1599
|
if (!st.isSymbolicLink()) {
|
|
@@ -669,12 +1611,11 @@ async function planSkillSymlinkChanges({
|
|
|
669
1611
|
}
|
|
670
1612
|
}
|
|
671
1613
|
|
|
672
|
-
for (const name of
|
|
1614
|
+
for (const { name, path } of desiredEntries) {
|
|
673
1615
|
if (existing.find((entry) => entry.name === name)) {
|
|
674
1616
|
continue;
|
|
675
1617
|
}
|
|
676
|
-
|
|
677
|
-
if (await fileExists(target)) {
|
|
1618
|
+
if (await fileExists(path)) {
|
|
678
1619
|
add.push(name);
|
|
679
1620
|
}
|
|
680
1621
|
}
|
|
@@ -708,14 +1649,24 @@ async function syncSkillSymlinks({
|
|
|
708
1649
|
return plan;
|
|
709
1650
|
}
|
|
710
1651
|
|
|
1652
|
+
const desiredSkills = new Map(
|
|
1653
|
+
(
|
|
1654
|
+
await loadEnabledSkillEntries({
|
|
1655
|
+
homeDir,
|
|
1656
|
+
rootDir,
|
|
1657
|
+
tool,
|
|
1658
|
+
})
|
|
1659
|
+
).map((entry) => [entry.name, entry.path])
|
|
1660
|
+
);
|
|
1661
|
+
|
|
711
1662
|
await ensureDir(toolSkillsDir);
|
|
712
1663
|
for (const name of plan.remove) {
|
|
713
1664
|
const linkPath = join(toolSkillsDir, name);
|
|
714
1665
|
await rm(linkPath, { recursive: true, force: true });
|
|
715
1666
|
}
|
|
716
1667
|
for (const name of plan.add) {
|
|
717
|
-
const target =
|
|
718
|
-
if (!(await fileExists(target))) {
|
|
1668
|
+
const target = desiredSkills.get(name);
|
|
1669
|
+
if (!(target && (await fileExists(target)))) {
|
|
719
1670
|
continue;
|
|
720
1671
|
}
|
|
721
1672
|
const linkPath = join(toolSkillsDir, name);
|
|
@@ -788,44 +1739,233 @@ async function writeToolMcpConfig({
|
|
|
788
1739
|
);
|
|
789
1740
|
}
|
|
790
1741
|
|
|
791
|
-
export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
792
|
-
const home = opts.homeDir ?? homedir();
|
|
793
|
-
const rootDir = opts.rootDir ?? facultRootDir(home);
|
|
794
|
-
const state = await loadManagedState(home);
|
|
1742
|
+
export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
1743
|
+
const home = opts.homeDir ?? homedir();
|
|
1744
|
+
const rootDir = opts.rootDir ?? facultRootDir(home);
|
|
1745
|
+
const state = await loadManagedState(home, rootDir);
|
|
1746
|
+
|
|
1747
|
+
if (state.tools[tool]) {
|
|
1748
|
+
throw new Error(`${tool} is already managed`);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const toolPaths = await resolveToolPaths(tool, home, rootDir, opts.toolPaths);
|
|
1752
|
+
if (!toolPaths) {
|
|
1753
|
+
throw new Error(`Unknown tool: ${tool}`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const existingSkillPlan = toolPaths.skillsDir
|
|
1757
|
+
? await planExistingToolSkillAdoption({
|
|
1758
|
+
rootDir,
|
|
1759
|
+
toolSkillsDir: toolPaths.skillsDir,
|
|
1760
|
+
})
|
|
1761
|
+
: {
|
|
1762
|
+
adopt: [],
|
|
1763
|
+
identical: [],
|
|
1764
|
+
conflicts: [],
|
|
1765
|
+
ignored: [],
|
|
1766
|
+
};
|
|
1767
|
+
const existingImportPlan = mergeManagedImportPlans(
|
|
1768
|
+
asManagedSkillPlan(existingSkillPlan),
|
|
1769
|
+
toolPaths.agentsDir
|
|
1770
|
+
? await planExistingToolAgentAdoption({
|
|
1771
|
+
rootDir,
|
|
1772
|
+
agentsDir: toolPaths.agentsDir,
|
|
1773
|
+
})
|
|
1774
|
+
: emptyManagedImportPlan(),
|
|
1775
|
+
toolPaths.toolHome
|
|
1776
|
+
? await planExistingGlobalDocAdoption({
|
|
1777
|
+
rootDir,
|
|
1778
|
+
tool,
|
|
1779
|
+
toolHome: toolPaths.toolHome,
|
|
1780
|
+
})
|
|
1781
|
+
: emptyManagedImportPlan(),
|
|
1782
|
+
toolPaths.rulesDir
|
|
1783
|
+
? await planExistingRuleAdoption({
|
|
1784
|
+
rootDir,
|
|
1785
|
+
tool,
|
|
1786
|
+
rulesDir: toolPaths.rulesDir,
|
|
1787
|
+
})
|
|
1788
|
+
: emptyManagedImportPlan(),
|
|
1789
|
+
toolPaths.toolConfig
|
|
1790
|
+
? await planExistingToolConfigAdoption({
|
|
1791
|
+
rootDir,
|
|
1792
|
+
tool,
|
|
1793
|
+
toolConfigPath: toolPaths.toolConfig,
|
|
1794
|
+
})
|
|
1795
|
+
: emptyManagedImportPlan(),
|
|
1796
|
+
toolPaths.mcpConfig
|
|
1797
|
+
? await planExistingMcpAdoption({
|
|
1798
|
+
rootDir,
|
|
1799
|
+
tool,
|
|
1800
|
+
mcpConfigPath: toolPaths.mcpConfig,
|
|
1801
|
+
})
|
|
1802
|
+
: emptyManagedImportPlan()
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
if (opts.dryRun) {
|
|
1806
|
+
logManagedImportPlan(tool, existingImportPlan);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
795
1809
|
|
|
796
|
-
if (
|
|
797
|
-
|
|
1810
|
+
if (
|
|
1811
|
+
(toolPaths.skillsDir ||
|
|
1812
|
+
toolPaths.agentsDir ||
|
|
1813
|
+
toolPaths.toolHome ||
|
|
1814
|
+
toolPaths.rulesDir ||
|
|
1815
|
+
toolPaths.toolConfig ||
|
|
1816
|
+
toolPaths.mcpConfig) &&
|
|
1817
|
+
!opts.adoptExisting &&
|
|
1818
|
+
(existingImportPlan.adopt.length > 0 ||
|
|
1819
|
+
existingImportPlan.conflicts.length > 0)
|
|
1820
|
+
) {
|
|
1821
|
+
const summary = [
|
|
1822
|
+
`${tool} has existing managed content that must be reviewed before entering managed mode.`,
|
|
1823
|
+
existingImportPlan.adopt.length
|
|
1824
|
+
? `Adoptable items: ${existingImportPlan.adopt
|
|
1825
|
+
.map((item) => formatManagedItem(item))
|
|
1826
|
+
.join(", ")}`
|
|
1827
|
+
: null,
|
|
1828
|
+
existingImportPlan.conflicts.length
|
|
1829
|
+
? `Conflicting items: ${existingImportPlan.conflicts
|
|
1830
|
+
.map((item) => formatManagedItem(item))
|
|
1831
|
+
.join(", ")}`
|
|
1832
|
+
: null,
|
|
1833
|
+
`Run "facult manage ${tool} --dry-run" to review the plan, then rerun with "--adopt-existing"`,
|
|
1834
|
+
existingImportPlan.conflicts.length > 0
|
|
1835
|
+
? ' and "--existing-conflicts keep-canonical|keep-existing".'
|
|
1836
|
+
: ".",
|
|
1837
|
+
]
|
|
1838
|
+
.filter(Boolean)
|
|
1839
|
+
.join(" ");
|
|
1840
|
+
throw new Error(summary);
|
|
798
1841
|
}
|
|
799
1842
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1843
|
+
if (
|
|
1844
|
+
opts.adoptExisting &&
|
|
1845
|
+
existingImportPlan.conflicts.length > 0 &&
|
|
1846
|
+
!opts.existingConflictMode
|
|
1847
|
+
) {
|
|
1848
|
+
throw new Error(
|
|
1849
|
+
`${tool} has conflicting existing content (${existingImportPlan.conflicts
|
|
1850
|
+
.map((item) => formatManagedItem(item))
|
|
1851
|
+
.join(
|
|
1852
|
+
", "
|
|
1853
|
+
)}). Rerun with "--existing-conflicts keep-canonical" or "--existing-conflicts keep-existing".`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
const importConflictMode = opts.existingConflictMode ?? "keep-canonical";
|
|
1857
|
+
|
|
1858
|
+
const adoptedSkills = toolPaths.skillsDir
|
|
1859
|
+
? await adoptSkillsIntoCanonicalStore({
|
|
1860
|
+
homeDir: home,
|
|
1861
|
+
rootDir,
|
|
1862
|
+
skillSourceDirs: [toolPaths.skillsDir],
|
|
1863
|
+
})
|
|
1864
|
+
: [];
|
|
1865
|
+
|
|
1866
|
+
if (toolPaths.skillsDir && opts.adoptExisting) {
|
|
1867
|
+
const result = await adoptExistingToolSkills({
|
|
1868
|
+
rootDir,
|
|
1869
|
+
toolSkillsDir: toolPaths.skillsDir,
|
|
1870
|
+
conflictMode: importConflictMode,
|
|
1871
|
+
});
|
|
1872
|
+
for (const name of result.adopted) {
|
|
1873
|
+
if (!adoptedSkills.includes(name)) {
|
|
1874
|
+
adoptedSkills.push(name);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (result.adopted.length > 0) {
|
|
1878
|
+
await buildIndex({
|
|
1879
|
+
homeDir: home,
|
|
1880
|
+
rootDir,
|
|
1881
|
+
force: false,
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (toolPaths.agentsDir && opts.adoptExisting) {
|
|
1886
|
+
const result = await adoptExistingToolAgents({
|
|
1887
|
+
rootDir,
|
|
1888
|
+
agentsDir: toolPaths.agentsDir,
|
|
1889
|
+
conflictMode: importConflictMode,
|
|
1890
|
+
});
|
|
1891
|
+
adoptedSkills.push(...result.map((item) => item.name));
|
|
1892
|
+
}
|
|
1893
|
+
if (toolPaths.toolHome && opts.adoptExisting) {
|
|
1894
|
+
const result = await adoptExistingGlobalDocs({
|
|
1895
|
+
rootDir,
|
|
1896
|
+
tool,
|
|
1897
|
+
toolHome: toolPaths.toolHome,
|
|
1898
|
+
conflictMode: importConflictMode,
|
|
1899
|
+
});
|
|
1900
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
1901
|
+
}
|
|
1902
|
+
if (toolPaths.rulesDir && opts.adoptExisting) {
|
|
1903
|
+
const result = await adoptExistingRules({
|
|
1904
|
+
rootDir,
|
|
1905
|
+
tool,
|
|
1906
|
+
rulesDir: toolPaths.rulesDir,
|
|
1907
|
+
conflictMode: importConflictMode,
|
|
1908
|
+
});
|
|
1909
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
1910
|
+
}
|
|
1911
|
+
if (toolPaths.toolConfig && opts.adoptExisting) {
|
|
1912
|
+
const result = await adoptExistingToolConfig({
|
|
1913
|
+
rootDir,
|
|
1914
|
+
tool,
|
|
1915
|
+
toolConfigPath: toolPaths.toolConfig,
|
|
1916
|
+
conflictMode: importConflictMode,
|
|
1917
|
+
});
|
|
1918
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
1919
|
+
}
|
|
1920
|
+
if (toolPaths.mcpConfig && opts.adoptExisting) {
|
|
1921
|
+
const result = await adoptExistingMcpServers({
|
|
1922
|
+
rootDir,
|
|
1923
|
+
tool,
|
|
1924
|
+
mcpConfigPath: toolPaths.mcpConfig,
|
|
1925
|
+
conflictMode: importConflictMode,
|
|
1926
|
+
});
|
|
1927
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
1928
|
+
}
|
|
1929
|
+
if (adoptedSkills.length > 0) {
|
|
1930
|
+
await buildIndex({
|
|
1931
|
+
homeDir: home,
|
|
1932
|
+
rootDir,
|
|
1933
|
+
force: false,
|
|
1934
|
+
});
|
|
803
1935
|
}
|
|
1936
|
+
const agentPreview = toolPaths.agentsDir
|
|
1937
|
+
? await planAgentFileChanges({
|
|
1938
|
+
agentsDir: toolPaths.agentsDir,
|
|
1939
|
+
homeDir: home,
|
|
1940
|
+
rootDir,
|
|
1941
|
+
tool,
|
|
1942
|
+
})
|
|
1943
|
+
: null;
|
|
804
1944
|
const globalDocsPreview = toolPaths.toolHome
|
|
805
|
-
? await
|
|
1945
|
+
? await planToolGlobalDocsSync({
|
|
806
1946
|
homeDir: home,
|
|
807
1947
|
rootDir,
|
|
808
1948
|
tool,
|
|
809
1949
|
toolHome: toolPaths.toolHome,
|
|
810
|
-
dryRun: true,
|
|
811
1950
|
})
|
|
812
1951
|
: null;
|
|
1952
|
+
const globalDocTargets = toolPaths.toolHome
|
|
1953
|
+
? globalDocTargetPaths(tool, toolPaths.toolHome)
|
|
1954
|
+
: null;
|
|
813
1955
|
const rulesPreview = toolPaths.rulesDir
|
|
814
|
-
? await
|
|
1956
|
+
? await planToolRulesSync({
|
|
815
1957
|
homeDir: home,
|
|
816
1958
|
rootDir,
|
|
817
1959
|
tool,
|
|
818
1960
|
rulesDir: toolPaths.rulesDir,
|
|
819
|
-
dryRun: true,
|
|
820
1961
|
})
|
|
821
1962
|
: null;
|
|
822
1963
|
const toolConfigPreview = toolPaths.toolConfig
|
|
823
|
-
? await
|
|
1964
|
+
? await planToolConfigSync({
|
|
824
1965
|
homeDir: home,
|
|
825
1966
|
rootDir,
|
|
826
1967
|
tool,
|
|
827
1968
|
toolConfigPath: toolPaths.toolConfig,
|
|
828
|
-
dryRun: true,
|
|
829
1969
|
})
|
|
830
1970
|
: null;
|
|
831
1971
|
|
|
@@ -840,20 +1980,15 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
840
1980
|
: null;
|
|
841
1981
|
const globalAgentsBackup =
|
|
842
1982
|
toolPaths.toolHome &&
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
? await backupPath(join(toolPaths.toolHome, "AGENTS.md"), opts.now)
|
|
1983
|
+
globalDocTargets &&
|
|
1984
|
+
globalDocsPreview?.managedTargets.includes(globalDocTargets.primary)
|
|
1985
|
+
? await backupPath(globalDocTargets.primary, opts.now)
|
|
847
1986
|
: null;
|
|
848
1987
|
const globalAgentsOverrideBackup =
|
|
849
1988
|
toolPaths.toolHome &&
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
? await backupPath(
|
|
854
|
-
join(toolPaths.toolHome, "AGENTS.override.md"),
|
|
855
|
-
opts.now
|
|
856
|
-
)
|
|
1989
|
+
globalDocTargets?.override &&
|
|
1990
|
+
globalDocsPreview?.managedTargets.includes(globalDocTargets.override)
|
|
1991
|
+
? await backupPath(globalDocTargets.override, opts.now)
|
|
857
1992
|
: null;
|
|
858
1993
|
const rulesBackup =
|
|
859
1994
|
toolPaths.rulesDir && rulesPreview?.managedRulesDir
|
|
@@ -866,6 +2001,10 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
866
2001
|
|
|
867
2002
|
if (toolPaths.skillsDir) {
|
|
868
2003
|
await ensureEmptyDir(toolPaths.skillsDir);
|
|
2004
|
+
await restorePreservedToolSkillEntries({
|
|
2005
|
+
backupDir: skillsBackup,
|
|
2006
|
+
toolSkillsDir: toolPaths.skillsDir,
|
|
2007
|
+
});
|
|
869
2008
|
await createSkillSymlinks({
|
|
870
2009
|
homeDir: home,
|
|
871
2010
|
toolSkillsDir: toolPaths.skillsDir,
|
|
@@ -886,43 +2025,42 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
886
2025
|
});
|
|
887
2026
|
}
|
|
888
2027
|
|
|
889
|
-
if (toolPaths.agentsDir) {
|
|
890
|
-
await
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
rootDir,
|
|
894
|
-
tool,
|
|
2028
|
+
if (toolPaths.agentsDir && agentPreview) {
|
|
2029
|
+
await applyRenderedWrites({
|
|
2030
|
+
contents: agentPreview.contents,
|
|
2031
|
+
targets: Array.from(agentPreview.contents.keys()),
|
|
895
2032
|
});
|
|
896
2033
|
}
|
|
897
2034
|
|
|
898
2035
|
if (toolPaths.toolHome && globalDocsPreview) {
|
|
899
2036
|
await ensureDir(toolPaths.toolHome);
|
|
900
|
-
await
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
toolHome: toolPaths.toolHome,
|
|
2037
|
+
await applyRenderedRemoves(globalDocsPreview.remove);
|
|
2038
|
+
await applyRenderedWrites({
|
|
2039
|
+
contents: globalDocsPreview.contents,
|
|
2040
|
+
targets: Array.from(globalDocsPreview.contents.keys()),
|
|
905
2041
|
});
|
|
906
2042
|
}
|
|
907
2043
|
|
|
908
2044
|
if (toolPaths.rulesDir && rulesPreview?.managedRulesDir) {
|
|
909
2045
|
await ensureEmptyDir(toolPaths.rulesDir);
|
|
910
|
-
await
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
rulesDir: toolPaths.rulesDir,
|
|
915
|
-
previouslyManaged: true,
|
|
2046
|
+
await applyRenderedRemoves(rulesPreview.remove);
|
|
2047
|
+
await applyRenderedWrites({
|
|
2048
|
+
contents: rulesPreview.contents,
|
|
2049
|
+
targets: Array.from(rulesPreview.contents.keys()),
|
|
916
2050
|
});
|
|
917
2051
|
}
|
|
918
2052
|
|
|
919
2053
|
if (toolPaths.toolConfig && toolConfigPreview?.managedConfig) {
|
|
920
|
-
await
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
2054
|
+
await applyRenderedWrites({
|
|
2055
|
+
contents: new Map(
|
|
2056
|
+
toolConfigPreview.contents != null
|
|
2057
|
+
? [[toolConfigPreview.targetPath, toolConfigPreview.contents]]
|
|
2058
|
+
: []
|
|
2059
|
+
),
|
|
2060
|
+
targets:
|
|
2061
|
+
toolConfigPreview.managedConfig && toolConfigPreview.contents != null
|
|
2062
|
+
? [toolConfigPreview.targetPath]
|
|
2063
|
+
: [],
|
|
926
2064
|
});
|
|
927
2065
|
}
|
|
928
2066
|
|
|
@@ -935,16 +2073,16 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
935
2073
|
toolHome: globalDocsPreview?.managedTargets.length
|
|
936
2074
|
? toolPaths.toolHome
|
|
937
2075
|
: undefined,
|
|
938
|
-
globalAgentsPath:
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
globalAgentsOverridePath:
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
2076
|
+
globalAgentsPath:
|
|
2077
|
+
globalDocTargets &&
|
|
2078
|
+
globalDocsPreview?.managedTargets.includes(globalDocTargets.primary)
|
|
2079
|
+
? globalDocTargets.primary
|
|
2080
|
+
: undefined,
|
|
2081
|
+
globalAgentsOverridePath:
|
|
2082
|
+
globalDocTargets?.override &&
|
|
2083
|
+
globalDocsPreview?.managedTargets.includes(globalDocTargets.override)
|
|
2084
|
+
? globalDocTargets.override
|
|
2085
|
+
: undefined,
|
|
948
2086
|
rulesDir: rulesPreview?.managedRulesDir ? toolPaths.rulesDir : undefined,
|
|
949
2087
|
toolConfig: toolConfigPreview?.managedConfig
|
|
950
2088
|
? toolPaths.toolConfig
|
|
@@ -956,9 +2094,65 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
956
2094
|
globalAgentsOverrideBackup,
|
|
957
2095
|
rulesBackup,
|
|
958
2096
|
toolConfigBackup,
|
|
2097
|
+
renderedTargets: {},
|
|
959
2098
|
};
|
|
960
2099
|
|
|
961
|
-
|
|
2100
|
+
const managedEntry = state.tools[tool]!;
|
|
2101
|
+
if (agentPreview) {
|
|
2102
|
+
updateRenderedTargetState({
|
|
2103
|
+
entry: managedEntry,
|
|
2104
|
+
writtenTargets: Array.from(agentPreview.contents.keys()),
|
|
2105
|
+
removedTargets: agentPreview.remove,
|
|
2106
|
+
contents: agentPreview.contents,
|
|
2107
|
+
sources: agentPreview.sources,
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
if (globalDocsPreview) {
|
|
2111
|
+
updateRenderedTargetState({
|
|
2112
|
+
entry: managedEntry,
|
|
2113
|
+
writtenTargets: Array.from(globalDocsPreview.contents.keys()),
|
|
2114
|
+
removedTargets: globalDocsPreview.remove,
|
|
2115
|
+
contents: globalDocsPreview.contents,
|
|
2116
|
+
sources: globalDocsPreview.sources,
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
if (rulesPreview) {
|
|
2120
|
+
updateRenderedTargetState({
|
|
2121
|
+
entry: managedEntry,
|
|
2122
|
+
writtenTargets: Array.from(rulesPreview.contents.keys()),
|
|
2123
|
+
removedTargets: rulesPreview.remove,
|
|
2124
|
+
contents: rulesPreview.contents,
|
|
2125
|
+
sources: rulesPreview.sources,
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
if (toolConfigPreview?.managedConfig && toolConfigPreview.contents != null) {
|
|
2129
|
+
updateRenderedTargetState({
|
|
2130
|
+
entry: managedEntry,
|
|
2131
|
+
writtenTargets:
|
|
2132
|
+
toolConfigPreview.managedConfig && toolConfigPreview.contents != null
|
|
2133
|
+
? [toolConfigPreview.targetPath]
|
|
2134
|
+
: [],
|
|
2135
|
+
removedTargets: toolConfigPreview.remove
|
|
2136
|
+
? [toolConfigPreview.targetPath]
|
|
2137
|
+
: [],
|
|
2138
|
+
contents: new Map([
|
|
2139
|
+
[toolConfigPreview.targetPath, toolConfigPreview.contents],
|
|
2140
|
+
]),
|
|
2141
|
+
sources: new Map(
|
|
2142
|
+
toolConfigPreview.sourcePath
|
|
2143
|
+
? [[toolConfigPreview.targetPath, toolConfigPreview.sourcePath]]
|
|
2144
|
+
: []
|
|
2145
|
+
),
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
await saveManagedState(state, home, rootDir);
|
|
2150
|
+
|
|
2151
|
+
for (const name of adoptedSkills) {
|
|
2152
|
+
console.log(
|
|
2153
|
+
`${tool}: adopted existing content ${name} into canonical store`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
962
2156
|
}
|
|
963
2157
|
|
|
964
2158
|
async function restoreBackup({
|
|
@@ -999,7 +2193,8 @@ async function removeSymlinks(skillsDir: string) {
|
|
|
999
2193
|
|
|
1000
2194
|
export async function unmanageTool(tool: string, opts: ManageOptions = {}) {
|
|
1001
2195
|
const home = opts.homeDir ?? homedir();
|
|
1002
|
-
const
|
|
2196
|
+
const rootDir = opts.rootDir ?? facultRootDir(home);
|
|
2197
|
+
const state = await loadManagedState(home, rootDir);
|
|
1003
2198
|
const entry = state.tools[tool];
|
|
1004
2199
|
if (!entry) {
|
|
1005
2200
|
throw new Error(`${tool} is not managed`);
|
|
@@ -1063,13 +2258,15 @@ export async function unmanageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1063
2258
|
nextTools[name] = config;
|
|
1064
2259
|
}
|
|
1065
2260
|
state.tools = nextTools;
|
|
1066
|
-
await saveManagedState(state, home);
|
|
2261
|
+
await saveManagedState(state, home, rootDir);
|
|
1067
2262
|
}
|
|
1068
2263
|
|
|
1069
2264
|
export async function listManagedTools(
|
|
1070
|
-
opts: { homeDir?: string } = {}
|
|
2265
|
+
opts: { homeDir?: string; rootDir?: string } = {}
|
|
1071
2266
|
): Promise<string[]> {
|
|
1072
|
-
const
|
|
2267
|
+
const home = opts.homeDir ?? homedir();
|
|
2268
|
+
const rootDir = opts.rootDir ?? facultRootDir(home);
|
|
2269
|
+
const state = await loadManagedState(home, rootDir);
|
|
1073
2270
|
return Object.keys(state.tools).sort();
|
|
1074
2271
|
}
|
|
1075
2272
|
|
|
@@ -1089,7 +2286,7 @@ async function repairManagedToolEntry(args: {
|
|
|
1089
2286
|
entry: ManagedToolState;
|
|
1090
2287
|
}): Promise<{ entry: ManagedToolState; changed: boolean }> {
|
|
1091
2288
|
const { homeDir, rootDir, tool } = args;
|
|
1092
|
-
const toolPaths = await resolveToolPaths(tool, homeDir);
|
|
2289
|
+
const toolPaths = await resolveToolPaths(tool, homeDir, rootDir);
|
|
1093
2290
|
if (!toolPaths) {
|
|
1094
2291
|
return { entry: args.entry, changed: false };
|
|
1095
2292
|
}
|
|
@@ -1117,8 +2314,9 @@ async function repairManagedToolEntry(args: {
|
|
|
1117
2314
|
});
|
|
1118
2315
|
if (preview.managedTargets.length > 0) {
|
|
1119
2316
|
next.toolHome = toolPaths.toolHome;
|
|
1120
|
-
const
|
|
1121
|
-
const
|
|
2317
|
+
const targets = globalDocTargetPaths(tool, toolPaths.toolHome);
|
|
2318
|
+
const agentsPath = targets.primary;
|
|
2319
|
+
const overridePath = targets.override;
|
|
1122
2320
|
if (
|
|
1123
2321
|
preview.managedTargets.includes(agentsPath) &&
|
|
1124
2322
|
!next.globalAgentsPath
|
|
@@ -1128,6 +2326,7 @@ async function repairManagedToolEntry(args: {
|
|
|
1128
2326
|
changed = true;
|
|
1129
2327
|
}
|
|
1130
2328
|
if (
|
|
2329
|
+
overridePath &&
|
|
1131
2330
|
preview.managedTargets.includes(overridePath) &&
|
|
1132
2331
|
!next.globalAgentsOverridePath
|
|
1133
2332
|
) {
|
|
@@ -1171,24 +2370,197 @@ async function repairManagedToolEntry(args: {
|
|
|
1171
2370
|
return { entry: next, changed };
|
|
1172
2371
|
}
|
|
1173
2372
|
|
|
2373
|
+
interface RenderedConflict {
|
|
2374
|
+
targetPath: string;
|
|
2375
|
+
sourcePath: string;
|
|
2376
|
+
sourceKind: ManagedRenderedTargetState["sourceKind"];
|
|
2377
|
+
reason: "modified" | "unknown_state";
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
interface RenderedApplyPlan {
|
|
2381
|
+
write: string[];
|
|
2382
|
+
remove: string[];
|
|
2383
|
+
conflicts: RenderedConflict[];
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
async function planRenderedTargetConflicts(args: {
|
|
2387
|
+
entry: ManagedToolState;
|
|
2388
|
+
desiredWrites: string[];
|
|
2389
|
+
desiredRemoves: string[];
|
|
2390
|
+
desiredContents: Map<string, string>;
|
|
2391
|
+
desiredSources: Map<string, string>;
|
|
2392
|
+
conflictMode?: "warn" | "overwrite";
|
|
2393
|
+
}): Promise<RenderedApplyPlan> {
|
|
2394
|
+
if (args.conflictMode === "overwrite") {
|
|
2395
|
+
return {
|
|
2396
|
+
write: args.desiredWrites,
|
|
2397
|
+
remove: args.desiredRemoves,
|
|
2398
|
+
conflicts: [],
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
const previous = args.entry.renderedTargets ?? {};
|
|
2403
|
+
const write: string[] = [];
|
|
2404
|
+
const remove: string[] = [];
|
|
2405
|
+
const conflicts: RenderedConflict[] = [];
|
|
2406
|
+
const allTargets = new Set([...args.desiredWrites, ...args.desiredRemoves]);
|
|
2407
|
+
|
|
2408
|
+
for (const targetPath of allTargets) {
|
|
2409
|
+
const sourcePath =
|
|
2410
|
+
args.desiredSources.get(targetPath) ?? previous[targetPath]?.sourcePath;
|
|
2411
|
+
if (!sourcePath) {
|
|
2412
|
+
if (args.desiredWrites.includes(targetPath)) {
|
|
2413
|
+
write.push(targetPath);
|
|
2414
|
+
} else {
|
|
2415
|
+
remove.push(targetPath);
|
|
2416
|
+
}
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
const sourceKind = renderedSourceKindForPath(sourcePath);
|
|
2420
|
+
if (sourceKind !== "builtin") {
|
|
2421
|
+
if (args.desiredWrites.includes(targetPath)) {
|
|
2422
|
+
write.push(targetPath);
|
|
2423
|
+
} else {
|
|
2424
|
+
remove.push(targetPath);
|
|
2425
|
+
}
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
const prior = previous[targetPath];
|
|
2430
|
+
const current = await readTextIfExists(targetPath);
|
|
2431
|
+
if (current == null) {
|
|
2432
|
+
if (args.desiredWrites.includes(targetPath)) {
|
|
2433
|
+
write.push(targetPath);
|
|
2434
|
+
}
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const currentHash = renderedHash(current);
|
|
2439
|
+
if (prior?.hash) {
|
|
2440
|
+
if (currentHash === prior.hash) {
|
|
2441
|
+
if (args.desiredWrites.includes(targetPath)) {
|
|
2442
|
+
write.push(targetPath);
|
|
2443
|
+
} else {
|
|
2444
|
+
remove.push(targetPath);
|
|
2445
|
+
}
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
conflicts.push({
|
|
2449
|
+
targetPath,
|
|
2450
|
+
sourcePath,
|
|
2451
|
+
sourceKind,
|
|
2452
|
+
reason: "modified",
|
|
2453
|
+
});
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
conflicts.push({
|
|
2458
|
+
targetPath,
|
|
2459
|
+
sourcePath,
|
|
2460
|
+
sourceKind,
|
|
2461
|
+
reason: "unknown_state",
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
return {
|
|
2466
|
+
write: write.sort(),
|
|
2467
|
+
remove: remove.sort(),
|
|
2468
|
+
conflicts,
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
function logRenderedConflicts(
|
|
2473
|
+
tool: string,
|
|
2474
|
+
conflicts: RenderedConflict[],
|
|
2475
|
+
dryRun?: boolean
|
|
2476
|
+
) {
|
|
2477
|
+
for (const conflict of conflicts) {
|
|
2478
|
+
const verb = dryRun ? "would skip" : "skipped";
|
|
2479
|
+
const state =
|
|
2480
|
+
conflict.reason === "unknown_state"
|
|
2481
|
+
? "no prior managed hash is recorded"
|
|
2482
|
+
: "local edits were detected";
|
|
2483
|
+
console.warn(
|
|
2484
|
+
`${tool}: ${verb} builtin-backed target ${conflict.targetPath} because ${state}. Rerun with "--builtin-conflicts overwrite" to replace it with the latest packaged default.`
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
async function applyRenderedWrites(args: {
|
|
2490
|
+
contents: Map<string, string>;
|
|
2491
|
+
targets: string[];
|
|
2492
|
+
}) {
|
|
2493
|
+
for (const pathValue of args.targets) {
|
|
2494
|
+
const desired = args.contents.get(pathValue);
|
|
2495
|
+
if (desired == null) {
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
await mkdir(dirname(pathValue), { recursive: true });
|
|
2499
|
+
await Bun.write(
|
|
2500
|
+
pathValue,
|
|
2501
|
+
desired.endsWith("\n") ? desired : `${desired}\n`
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
async function applyRenderedRemoves(targets: string[]) {
|
|
2507
|
+
for (const pathValue of targets) {
|
|
2508
|
+
await rm(pathValue, { force: true });
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function updateRenderedTargetState(args: {
|
|
2513
|
+
entry: ManagedToolState;
|
|
2514
|
+
writtenTargets: string[];
|
|
2515
|
+
removedTargets: string[];
|
|
2516
|
+
contents: Map<string, string>;
|
|
2517
|
+
sources: Map<string, string>;
|
|
2518
|
+
}) {
|
|
2519
|
+
const next = { ...(args.entry.renderedTargets ?? {}) };
|
|
2520
|
+
for (const pathValue of args.removedTargets) {
|
|
2521
|
+
delete next[pathValue];
|
|
2522
|
+
}
|
|
2523
|
+
for (const pathValue of args.writtenTargets) {
|
|
2524
|
+
const contents = args.contents.get(pathValue);
|
|
2525
|
+
const sourcePath = args.sources.get(pathValue);
|
|
2526
|
+
if (!(contents && sourcePath)) {
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
next[pathValue] = {
|
|
2530
|
+
hash: renderedHash(contents),
|
|
2531
|
+
sourcePath,
|
|
2532
|
+
sourceKind: renderedSourceKindForPath(sourcePath),
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
args.entry.renderedTargets = next;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
1174
2538
|
function logSyncDryRun({
|
|
1175
2539
|
tool,
|
|
1176
2540
|
entry,
|
|
1177
2541
|
skillPlan,
|
|
1178
2542
|
mcpPlan,
|
|
1179
2543
|
agentPlan,
|
|
2544
|
+
agentConflicts,
|
|
1180
2545
|
globalDocsPlan,
|
|
2546
|
+
globalDocsConflicts,
|
|
1181
2547
|
rulesPlan,
|
|
2548
|
+
rulesConflicts,
|
|
1182
2549
|
configPlan,
|
|
2550
|
+
configConflicts,
|
|
1183
2551
|
}: {
|
|
1184
2552
|
tool: string;
|
|
1185
2553
|
entry: ManagedToolState;
|
|
1186
2554
|
skillPlan: { add: string[]; remove: string[] };
|
|
1187
2555
|
mcpPlan: { needsWrite: boolean };
|
|
1188
2556
|
agentPlan: { add: string[]; remove: string[] };
|
|
2557
|
+
agentConflicts: RenderedConflict[];
|
|
1189
2558
|
globalDocsPlan: { write: string[]; remove: string[] };
|
|
2559
|
+
globalDocsConflicts: RenderedConflict[];
|
|
1190
2560
|
rulesPlan: { write: string[]; remove: string[] };
|
|
2561
|
+
rulesConflicts: RenderedConflict[];
|
|
1191
2562
|
configPlan: { write: boolean; remove: boolean; targetPath: string };
|
|
2563
|
+
configConflicts: RenderedConflict[];
|
|
1192
2564
|
}) {
|
|
1193
2565
|
for (const name of skillPlan.add) {
|
|
1194
2566
|
console.log(`${tool}: would add skill ${name}`);
|
|
@@ -1202,24 +2574,28 @@ function logSyncDryRun({
|
|
|
1202
2574
|
for (const p of agentPlan.remove) {
|
|
1203
2575
|
console.log(`${tool}: would remove agent ${p}`);
|
|
1204
2576
|
}
|
|
2577
|
+
logRenderedConflicts(tool, agentConflicts, true);
|
|
1205
2578
|
for (const p of globalDocsPlan.write) {
|
|
1206
2579
|
console.log(`${tool}: would write global doc ${p}`);
|
|
1207
2580
|
}
|
|
1208
2581
|
for (const p of globalDocsPlan.remove) {
|
|
1209
2582
|
console.log(`${tool}: would remove global doc ${p}`);
|
|
1210
2583
|
}
|
|
2584
|
+
logRenderedConflicts(tool, globalDocsConflicts, true);
|
|
1211
2585
|
for (const p of rulesPlan.write) {
|
|
1212
2586
|
console.log(`${tool}: would write rule ${p}`);
|
|
1213
2587
|
}
|
|
1214
2588
|
for (const p of rulesPlan.remove) {
|
|
1215
2589
|
console.log(`${tool}: would remove rule ${p}`);
|
|
1216
2590
|
}
|
|
2591
|
+
logRenderedConflicts(tool, rulesConflicts, true);
|
|
1217
2592
|
if (configPlan.write) {
|
|
1218
2593
|
console.log(`${tool}: would write tool config ${configPlan.targetPath}`);
|
|
1219
2594
|
}
|
|
1220
2595
|
if (configPlan.remove) {
|
|
1221
2596
|
console.log(`${tool}: would remove tool config ${configPlan.targetPath}`);
|
|
1222
2597
|
}
|
|
2598
|
+
logRenderedConflicts(tool, configConflicts, true);
|
|
1223
2599
|
if (mcpPlan.needsWrite && entry.mcpConfig) {
|
|
1224
2600
|
console.log(`${tool}: would update mcp config ${entry.mcpConfig}`);
|
|
1225
2601
|
}
|
|
@@ -1234,25 +2610,129 @@ function logSyncDryRun({
|
|
|
1234
2610
|
rulesPlan.remove.length === 0 &&
|
|
1235
2611
|
!configPlan.write &&
|
|
1236
2612
|
!configPlan.remove &&
|
|
1237
|
-
!mcpPlan.needsWrite
|
|
2613
|
+
!mcpPlan.needsWrite &&
|
|
2614
|
+
agentConflicts.length === 0 &&
|
|
2615
|
+
globalDocsConflicts.length === 0 &&
|
|
2616
|
+
rulesConflicts.length === 0 &&
|
|
2617
|
+
configConflicts.length === 0
|
|
1238
2618
|
) {
|
|
1239
2619
|
console.log(`${tool}: no changes`);
|
|
1240
2620
|
}
|
|
1241
2621
|
}
|
|
1242
2622
|
|
|
2623
|
+
async function repairManagedCanonicalContent(args: {
|
|
2624
|
+
homeDir: string;
|
|
2625
|
+
rootDir: string;
|
|
2626
|
+
tool: string;
|
|
2627
|
+
entry: ManagedToolState;
|
|
2628
|
+
}): Promise<string[]> {
|
|
2629
|
+
const adopted: string[] = [];
|
|
2630
|
+
|
|
2631
|
+
for (const name of await adoptSkillsIntoCanonicalStore({
|
|
2632
|
+
homeDir: args.homeDir,
|
|
2633
|
+
rootDir: args.rootDir,
|
|
2634
|
+
skillSourceDirs: [
|
|
2635
|
+
args.entry.skillsBackup ?? "",
|
|
2636
|
+
args.entry.skillsDir ?? "",
|
|
2637
|
+
],
|
|
2638
|
+
})) {
|
|
2639
|
+
adopted.push(name);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
if (args.entry.agentsBackup) {
|
|
2643
|
+
const items = await adoptExistingToolAgents({
|
|
2644
|
+
rootDir: args.rootDir,
|
|
2645
|
+
agentsDir: args.entry.agentsBackup,
|
|
2646
|
+
conflictMode: "keep-canonical",
|
|
2647
|
+
});
|
|
2648
|
+
adopted.push(...items.map((item) => `agent:${item.name}`));
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
if (args.entry.globalAgentsBackup) {
|
|
2652
|
+
const items = await adoptExistingGlobalDocFile({
|
|
2653
|
+
sourcePath: args.entry.globalAgentsBackup,
|
|
2654
|
+
canonicalPath: join(args.rootDir, "AGENTS.global.md"),
|
|
2655
|
+
name: "AGENTS.md",
|
|
2656
|
+
conflictMode: "keep-canonical",
|
|
2657
|
+
});
|
|
2658
|
+
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
if (args.entry.globalAgentsOverrideBackup) {
|
|
2662
|
+
const items = await adoptExistingGlobalDocFile({
|
|
2663
|
+
sourcePath: args.entry.globalAgentsOverrideBackup,
|
|
2664
|
+
canonicalPath: join(args.rootDir, "AGENTS.override.global.md"),
|
|
2665
|
+
name: "AGENTS.override.md",
|
|
2666
|
+
conflictMode: "keep-canonical",
|
|
2667
|
+
});
|
|
2668
|
+
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
if (args.entry.rulesBackup) {
|
|
2672
|
+
const items = await adoptExistingRules({
|
|
2673
|
+
rootDir: args.rootDir,
|
|
2674
|
+
tool: args.tool,
|
|
2675
|
+
rulesDir: args.entry.rulesBackup,
|
|
2676
|
+
conflictMode: "keep-canonical",
|
|
2677
|
+
});
|
|
2678
|
+
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
if (args.entry.toolConfigBackup) {
|
|
2682
|
+
const items = await adoptExistingToolConfig({
|
|
2683
|
+
rootDir: args.rootDir,
|
|
2684
|
+
tool: args.tool,
|
|
2685
|
+
toolConfigPath: args.entry.toolConfigBackup,
|
|
2686
|
+
conflictMode: "keep-canonical",
|
|
2687
|
+
});
|
|
2688
|
+
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if (args.entry.mcpBackup) {
|
|
2692
|
+
const items = await adoptExistingMcpServers({
|
|
2693
|
+
rootDir: args.rootDir,
|
|
2694
|
+
tool: args.tool,
|
|
2695
|
+
mcpConfigPath: args.entry.mcpBackup,
|
|
2696
|
+
conflictMode: "keep-canonical",
|
|
2697
|
+
});
|
|
2698
|
+
adopted.push(...items.map((item) => `${item.kind}:${item.name}`));
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
if (adopted.length > 0) {
|
|
2702
|
+
await buildIndex({
|
|
2703
|
+
homeDir: args.homeDir,
|
|
2704
|
+
rootDir: args.rootDir,
|
|
2705
|
+
force: false,
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
return adopted;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
1243
2712
|
async function syncManagedToolEntry({
|
|
1244
2713
|
homeDir,
|
|
1245
2714
|
tool,
|
|
1246
2715
|
entry,
|
|
1247
2716
|
rootDir,
|
|
1248
2717
|
dryRun,
|
|
2718
|
+
builtinConflictMode,
|
|
1249
2719
|
}: {
|
|
1250
2720
|
homeDir: string;
|
|
1251
2721
|
tool: string;
|
|
1252
2722
|
entry: ManagedToolState;
|
|
1253
2723
|
rootDir: string;
|
|
1254
2724
|
dryRun?: boolean;
|
|
2725
|
+
builtinConflictMode?: "warn" | "overwrite";
|
|
1255
2726
|
}) {
|
|
2727
|
+
const adoptedSkills = dryRun
|
|
2728
|
+
? []
|
|
2729
|
+
: await repairManagedCanonicalContent({
|
|
2730
|
+
homeDir,
|
|
2731
|
+
rootDir,
|
|
2732
|
+
tool,
|
|
2733
|
+
entry,
|
|
2734
|
+
});
|
|
2735
|
+
|
|
1256
2736
|
const skillPlan = entry.skillsDir
|
|
1257
2737
|
? await syncSkillSymlinks({
|
|
1258
2738
|
homeDir,
|
|
@@ -1264,14 +2744,13 @@ async function syncManagedToolEntry({
|
|
|
1264
2744
|
: { add: [], remove: [] };
|
|
1265
2745
|
|
|
1266
2746
|
const agentPlan = entry.agentsDir
|
|
1267
|
-
? await
|
|
2747
|
+
? await planAgentFileChanges({
|
|
1268
2748
|
agentsDir: entry.agentsDir,
|
|
1269
2749
|
homeDir,
|
|
1270
2750
|
rootDir,
|
|
1271
2751
|
tool,
|
|
1272
|
-
dryRun,
|
|
1273
2752
|
})
|
|
1274
|
-
: { add: [], remove: [] };
|
|
2753
|
+
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
1275
2754
|
|
|
1276
2755
|
const mcpPlan = entry.mcpConfig
|
|
1277
2756
|
? await syncMcpConfig({
|
|
@@ -1283,7 +2762,7 @@ async function syncManagedToolEntry({
|
|
|
1283
2762
|
: { needsWrite: false };
|
|
1284
2763
|
|
|
1285
2764
|
const globalDocsPlan = entry.toolHome
|
|
1286
|
-
? await
|
|
2765
|
+
? await planToolGlobalDocsSync({
|
|
1287
2766
|
homeDir,
|
|
1288
2767
|
rootDir,
|
|
1289
2768
|
tool,
|
|
@@ -1292,51 +2771,175 @@ async function syncManagedToolEntry({
|
|
|
1292
2771
|
entry.globalAgentsPath,
|
|
1293
2772
|
entry.globalAgentsOverridePath,
|
|
1294
2773
|
].filter((value): value is string => Boolean(value)),
|
|
1295
|
-
dryRun,
|
|
1296
2774
|
})
|
|
1297
|
-
: {
|
|
2775
|
+
: {
|
|
2776
|
+
write: [],
|
|
2777
|
+
remove: [],
|
|
2778
|
+
contents: new Map(),
|
|
2779
|
+
sources: new Map(),
|
|
2780
|
+
managedTargets: [],
|
|
2781
|
+
};
|
|
1298
2782
|
|
|
1299
2783
|
const rulesPlan = entry.rulesDir
|
|
1300
|
-
? await
|
|
2784
|
+
? await planToolRulesSync({
|
|
1301
2785
|
homeDir,
|
|
1302
2786
|
rootDir,
|
|
1303
2787
|
tool,
|
|
1304
2788
|
rulesDir: entry.rulesDir,
|
|
1305
2789
|
previouslyManaged: true,
|
|
1306
|
-
dryRun,
|
|
1307
2790
|
})
|
|
1308
|
-
: {
|
|
2791
|
+
: {
|
|
2792
|
+
write: [],
|
|
2793
|
+
remove: [],
|
|
2794
|
+
contents: new Map(),
|
|
2795
|
+
sources: new Map(),
|
|
2796
|
+
managedRulesDir: false,
|
|
2797
|
+
};
|
|
1309
2798
|
|
|
1310
2799
|
const configPlan = entry.toolConfig
|
|
1311
|
-
? await
|
|
2800
|
+
? await planToolConfigSync({
|
|
1312
2801
|
homeDir,
|
|
1313
2802
|
rootDir,
|
|
1314
2803
|
tool,
|
|
1315
2804
|
toolConfigPath: entry.toolConfig,
|
|
1316
2805
|
existingConfigPath: entry.toolConfigBackup ?? undefined,
|
|
1317
2806
|
previouslyManaged: true,
|
|
1318
|
-
dryRun,
|
|
1319
2807
|
})
|
|
1320
2808
|
: {
|
|
1321
2809
|
write: false,
|
|
1322
2810
|
remove: false,
|
|
1323
2811
|
contents: null,
|
|
2812
|
+
sourcePath: undefined,
|
|
1324
2813
|
managedConfig: false,
|
|
1325
2814
|
targetPath: "",
|
|
1326
2815
|
};
|
|
1327
2816
|
|
|
2817
|
+
const agentRendered = await planRenderedTargetConflicts({
|
|
2818
|
+
entry,
|
|
2819
|
+
desiredWrites: agentPlan.add,
|
|
2820
|
+
desiredRemoves: agentPlan.remove,
|
|
2821
|
+
desiredContents: agentPlan.contents,
|
|
2822
|
+
desiredSources: agentPlan.sources,
|
|
2823
|
+
conflictMode: builtinConflictMode,
|
|
2824
|
+
});
|
|
2825
|
+
const globalDocsRendered = await planRenderedTargetConflicts({
|
|
2826
|
+
entry,
|
|
2827
|
+
desiredWrites: globalDocsPlan.write,
|
|
2828
|
+
desiredRemoves: globalDocsPlan.remove,
|
|
2829
|
+
desiredContents: globalDocsPlan.contents,
|
|
2830
|
+
desiredSources: globalDocsPlan.sources,
|
|
2831
|
+
conflictMode: builtinConflictMode,
|
|
2832
|
+
});
|
|
2833
|
+
const rulesRendered = await planRenderedTargetConflicts({
|
|
2834
|
+
entry,
|
|
2835
|
+
desiredWrites: rulesPlan.write,
|
|
2836
|
+
desiredRemoves: rulesPlan.remove,
|
|
2837
|
+
desiredContents: rulesPlan.contents,
|
|
2838
|
+
desiredSources: rulesPlan.sources,
|
|
2839
|
+
conflictMode: builtinConflictMode,
|
|
2840
|
+
});
|
|
2841
|
+
const configContents =
|
|
2842
|
+
configPlan.contents != null
|
|
2843
|
+
? new Map([[configPlan.targetPath, configPlan.contents]])
|
|
2844
|
+
: new Map<string, string>();
|
|
2845
|
+
const configSources = new Map<string, string>(
|
|
2846
|
+
configPlan.sourcePath
|
|
2847
|
+
? [[configPlan.targetPath, configPlan.sourcePath]]
|
|
2848
|
+
: []
|
|
2849
|
+
);
|
|
2850
|
+
const configRendered = await planRenderedTargetConflicts({
|
|
2851
|
+
entry,
|
|
2852
|
+
desiredWrites:
|
|
2853
|
+
configPlan.write && configPlan.targetPath ? [configPlan.targetPath] : [],
|
|
2854
|
+
desiredRemoves:
|
|
2855
|
+
configPlan.remove && configPlan.targetPath ? [configPlan.targetPath] : [],
|
|
2856
|
+
desiredContents: configContents,
|
|
2857
|
+
desiredSources: configSources,
|
|
2858
|
+
conflictMode: builtinConflictMode,
|
|
2859
|
+
});
|
|
2860
|
+
|
|
1328
2861
|
if (dryRun) {
|
|
1329
2862
|
logSyncDryRun({
|
|
1330
2863
|
tool,
|
|
1331
2864
|
entry,
|
|
1332
2865
|
skillPlan,
|
|
1333
2866
|
mcpPlan,
|
|
1334
|
-
agentPlan,
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
2867
|
+
agentPlan: { add: agentRendered.write, remove: agentRendered.remove },
|
|
2868
|
+
agentConflicts: agentRendered.conflicts,
|
|
2869
|
+
globalDocsPlan: {
|
|
2870
|
+
write: globalDocsRendered.write,
|
|
2871
|
+
remove: globalDocsRendered.remove,
|
|
2872
|
+
},
|
|
2873
|
+
globalDocsConflicts: globalDocsRendered.conflicts,
|
|
2874
|
+
rulesPlan: { write: rulesRendered.write, remove: rulesRendered.remove },
|
|
2875
|
+
rulesConflicts: rulesRendered.conflicts,
|
|
2876
|
+
configPlan: {
|
|
2877
|
+
write: configRendered.write.length > 0,
|
|
2878
|
+
remove: configRendered.remove.length > 0,
|
|
2879
|
+
targetPath: configPlan.targetPath,
|
|
2880
|
+
},
|
|
2881
|
+
configConflicts: configRendered.conflicts,
|
|
1338
2882
|
});
|
|
1339
2883
|
} else {
|
|
2884
|
+
await applyRenderedRemoves(agentRendered.remove);
|
|
2885
|
+
await applyRenderedWrites({
|
|
2886
|
+
contents: agentPlan.contents,
|
|
2887
|
+
targets: agentRendered.write,
|
|
2888
|
+
});
|
|
2889
|
+
await applyRenderedRemoves(globalDocsRendered.remove);
|
|
2890
|
+
await applyRenderedWrites({
|
|
2891
|
+
contents: globalDocsPlan.contents,
|
|
2892
|
+
targets: globalDocsRendered.write,
|
|
2893
|
+
});
|
|
2894
|
+
await applyRenderedRemoves(rulesRendered.remove);
|
|
2895
|
+
await applyRenderedWrites({
|
|
2896
|
+
contents: rulesPlan.contents,
|
|
2897
|
+
targets: rulesRendered.write,
|
|
2898
|
+
});
|
|
2899
|
+
await applyRenderedRemoves(configRendered.remove);
|
|
2900
|
+
await applyRenderedWrites({
|
|
2901
|
+
contents: configContents,
|
|
2902
|
+
targets: configRendered.write,
|
|
2903
|
+
});
|
|
2904
|
+
logRenderedConflicts(tool, agentRendered.conflicts);
|
|
2905
|
+
logRenderedConflicts(tool, globalDocsRendered.conflicts);
|
|
2906
|
+
logRenderedConflicts(tool, rulesRendered.conflicts);
|
|
2907
|
+
logRenderedConflicts(tool, configRendered.conflicts);
|
|
2908
|
+
|
|
2909
|
+
updateRenderedTargetState({
|
|
2910
|
+
entry,
|
|
2911
|
+
writtenTargets: agentRendered.write,
|
|
2912
|
+
removedTargets: agentRendered.remove,
|
|
2913
|
+
contents: agentPlan.contents,
|
|
2914
|
+
sources: agentPlan.sources,
|
|
2915
|
+
});
|
|
2916
|
+
updateRenderedTargetState({
|
|
2917
|
+
entry,
|
|
2918
|
+
writtenTargets: globalDocsRendered.write,
|
|
2919
|
+
removedTargets: globalDocsRendered.remove,
|
|
2920
|
+
contents: globalDocsPlan.contents,
|
|
2921
|
+
sources: globalDocsPlan.sources,
|
|
2922
|
+
});
|
|
2923
|
+
updateRenderedTargetState({
|
|
2924
|
+
entry,
|
|
2925
|
+
writtenTargets: rulesRendered.write,
|
|
2926
|
+
removedTargets: rulesRendered.remove,
|
|
2927
|
+
contents: rulesPlan.contents,
|
|
2928
|
+
sources: rulesPlan.sources,
|
|
2929
|
+
});
|
|
2930
|
+
updateRenderedTargetState({
|
|
2931
|
+
entry,
|
|
2932
|
+
writtenTargets: configRendered.write,
|
|
2933
|
+
removedTargets: configRendered.remove,
|
|
2934
|
+
contents: configContents,
|
|
2935
|
+
sources: configSources,
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
for (const name of adoptedSkills) {
|
|
2939
|
+
console.log(
|
|
2940
|
+
`${tool}: adopted existing content ${name} into canonical store`
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
1340
2943
|
console.log(`${tool} synced`);
|
|
1341
2944
|
}
|
|
1342
2945
|
}
|
|
@@ -1344,7 +2947,7 @@ async function syncManagedToolEntry({
|
|
|
1344
2947
|
export async function syncManagedTools(opts: SyncOptions = {}) {
|
|
1345
2948
|
const home = opts.homeDir ?? homedir();
|
|
1346
2949
|
const rootDir = opts.rootDir ?? facultRootDir(home);
|
|
1347
|
-
const state = await loadManagedState(home);
|
|
2950
|
+
const state = await loadManagedState(home, rootDir);
|
|
1348
2951
|
const tools = opts.tool ? [opts.tool] : Object.keys(state.tools).sort();
|
|
1349
2952
|
|
|
1350
2953
|
if (!tools.length) {
|
|
@@ -1370,7 +2973,7 @@ export async function syncManagedTools(opts: SyncOptions = {}) {
|
|
|
1370
2973
|
}
|
|
1371
2974
|
}
|
|
1372
2975
|
if (changed) {
|
|
1373
|
-
await saveManagedState(state, home);
|
|
2976
|
+
await saveManagedState(state, home, rootDir);
|
|
1374
2977
|
}
|
|
1375
2978
|
}
|
|
1376
2979
|
|
|
@@ -1385,28 +2988,94 @@ export async function syncManagedTools(opts: SyncOptions = {}) {
|
|
|
1385
2988
|
entry,
|
|
1386
2989
|
rootDir,
|
|
1387
2990
|
dryRun: opts.dryRun,
|
|
2991
|
+
builtinConflictMode: opts.builtinConflictMode,
|
|
1388
2992
|
});
|
|
1389
2993
|
}
|
|
2994
|
+
|
|
2995
|
+
if (!opts.dryRun) {
|
|
2996
|
+
await saveManagedState(state, home, rootDir);
|
|
2997
|
+
}
|
|
1390
2998
|
}
|
|
1391
2999
|
|
|
1392
3000
|
export async function manageCommand(argv: string[]) {
|
|
1393
|
-
|
|
3001
|
+
const parsed = parseCliContextArgs(argv);
|
|
3002
|
+
const args = [...parsed.argv];
|
|
3003
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
1394
3004
|
console.log(`facult manage — enter managed mode for a tool (backup + symlinks + MCP generation)
|
|
1395
3005
|
|
|
1396
3006
|
Usage:
|
|
1397
|
-
facult manage <tool>
|
|
3007
|
+
facult manage <tool> [--dry-run] [--adopt-existing] [--existing-conflicts keep-canonical|keep-existing] [--builtin-conflicts overwrite] [--root PATH|--global|--project]
|
|
1398
3008
|
`);
|
|
1399
3009
|
return;
|
|
1400
3010
|
}
|
|
1401
|
-
const
|
|
3011
|
+
const dryRun = args.includes("--dry-run");
|
|
3012
|
+
const adoptExisting = args.includes("--adopt-existing");
|
|
3013
|
+
const conflictIndex = args.indexOf("--existing-conflicts");
|
|
3014
|
+
const builtinConflictIndex = args.indexOf("--builtin-conflicts");
|
|
3015
|
+
let existingConflictMode: "keep-canonical" | "keep-existing" | undefined;
|
|
3016
|
+
let builtinConflictMode: "warn" | "overwrite" | undefined;
|
|
3017
|
+
if (conflictIndex !== -1) {
|
|
3018
|
+
const value = args[conflictIndex + 1];
|
|
3019
|
+
if (value !== "keep-canonical" && value !== "keep-existing") {
|
|
3020
|
+
console.error(
|
|
3021
|
+
'--existing-conflicts requires "keep-canonical" or "keep-existing"'
|
|
3022
|
+
);
|
|
3023
|
+
process.exitCode = 1;
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
existingConflictMode = value;
|
|
3027
|
+
}
|
|
3028
|
+
if (builtinConflictIndex !== -1) {
|
|
3029
|
+
const value = args[builtinConflictIndex + 1];
|
|
3030
|
+
if (value !== "overwrite") {
|
|
3031
|
+
console.error('--builtin-conflicts currently supports only "overwrite"');
|
|
3032
|
+
process.exitCode = 1;
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
builtinConflictMode = value;
|
|
3036
|
+
}
|
|
3037
|
+
const positional: string[] = [];
|
|
3038
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
3039
|
+
const value = args[i];
|
|
3040
|
+
if (!value) {
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
if (value === "--existing-conflicts") {
|
|
3044
|
+
i += 1;
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
if (value === "--builtin-conflicts") {
|
|
3048
|
+
i += 1;
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
if (value.startsWith("--")) {
|
|
3052
|
+
continue;
|
|
3053
|
+
}
|
|
3054
|
+
positional.push(value);
|
|
3055
|
+
}
|
|
3056
|
+
const tool = positional[0];
|
|
1402
3057
|
if (!tool) {
|
|
1403
3058
|
console.error("manage requires a tool name");
|
|
1404
3059
|
process.exitCode = 1;
|
|
1405
3060
|
return;
|
|
1406
3061
|
}
|
|
1407
3062
|
try {
|
|
1408
|
-
await manageTool(tool
|
|
1409
|
-
|
|
3063
|
+
await manageTool(tool, {
|
|
3064
|
+
rootDir: resolveCliContextRoot({
|
|
3065
|
+
rootArg: parsed.rootArg,
|
|
3066
|
+
scope: parsed.scope,
|
|
3067
|
+
cwd: process.cwd(),
|
|
3068
|
+
}),
|
|
3069
|
+
dryRun,
|
|
3070
|
+
adoptExisting,
|
|
3071
|
+
existingConflictMode,
|
|
3072
|
+
builtinConflictMode,
|
|
3073
|
+
});
|
|
3074
|
+
if (dryRun) {
|
|
3075
|
+
console.log(`${tool}: preflight complete`);
|
|
3076
|
+
} else {
|
|
3077
|
+
console.log(`${tool} is now managed`);
|
|
3078
|
+
}
|
|
1410
3079
|
} catch (err) {
|
|
1411
3080
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1412
3081
|
process.exitCode = 1;
|
|
@@ -1414,22 +3083,33 @@ Usage:
|
|
|
1414
3083
|
}
|
|
1415
3084
|
|
|
1416
3085
|
export async function unmanageCommand(argv: string[]) {
|
|
1417
|
-
|
|
3086
|
+
const parsed = parseCliContextArgs(argv);
|
|
3087
|
+
if (
|
|
3088
|
+
parsed.argv.includes("--help") ||
|
|
3089
|
+
parsed.argv.includes("-h") ||
|
|
3090
|
+
parsed.argv[0] === "help"
|
|
3091
|
+
) {
|
|
1418
3092
|
console.log(`facult unmanage — exit managed mode for a tool (restore backups)
|
|
1419
3093
|
|
|
1420
3094
|
Usage:
|
|
1421
|
-
facult unmanage <tool>
|
|
3095
|
+
facult unmanage <tool> [--root PATH|--global|--project]
|
|
1422
3096
|
`);
|
|
1423
3097
|
return;
|
|
1424
3098
|
}
|
|
1425
|
-
const tool = argv[0];
|
|
3099
|
+
const tool = parsed.argv[0];
|
|
1426
3100
|
if (!tool) {
|
|
1427
3101
|
console.error("unmanage requires a tool name");
|
|
1428
3102
|
process.exitCode = 1;
|
|
1429
3103
|
return;
|
|
1430
3104
|
}
|
|
1431
3105
|
try {
|
|
1432
|
-
await unmanageTool(tool
|
|
3106
|
+
await unmanageTool(tool, {
|
|
3107
|
+
rootDir: resolveCliContextRoot({
|
|
3108
|
+
rootArg: parsed.rootArg,
|
|
3109
|
+
scope: parsed.scope,
|
|
3110
|
+
cwd: process.cwd(),
|
|
3111
|
+
}),
|
|
3112
|
+
});
|
|
1433
3113
|
console.log(`${tool} is no longer managed`);
|
|
1434
3114
|
} catch (err) {
|
|
1435
3115
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -1438,15 +3118,26 @@ Usage:
|
|
|
1438
3118
|
}
|
|
1439
3119
|
|
|
1440
3120
|
export async function managedCommand(argv: string[] = []) {
|
|
1441
|
-
|
|
3121
|
+
const parsed = parseCliContextArgs(argv);
|
|
3122
|
+
if (
|
|
3123
|
+
parsed.argv.includes("--help") ||
|
|
3124
|
+
parsed.argv.includes("-h") ||
|
|
3125
|
+
parsed.argv[0] === "help"
|
|
3126
|
+
) {
|
|
1442
3127
|
console.log(`facult managed — list tools currently in managed mode
|
|
1443
3128
|
|
|
1444
3129
|
Usage:
|
|
1445
|
-
facult managed
|
|
3130
|
+
facult managed [--root PATH|--global|--project]
|
|
1446
3131
|
`);
|
|
1447
3132
|
return;
|
|
1448
3133
|
}
|
|
1449
|
-
const tools = await listManagedTools(
|
|
3134
|
+
const tools = await listManagedTools({
|
|
3135
|
+
rootDir: resolveCliContextRoot({
|
|
3136
|
+
rootArg: parsed.rootArg,
|
|
3137
|
+
scope: parsed.scope,
|
|
3138
|
+
cwd: process.cwd(),
|
|
3139
|
+
}),
|
|
3140
|
+
});
|
|
1450
3141
|
if (!tools.length) {
|
|
1451
3142
|
console.log("No managed tools.");
|
|
1452
3143
|
return;
|
|
@@ -1457,21 +3148,47 @@ Usage:
|
|
|
1457
3148
|
}
|
|
1458
3149
|
|
|
1459
3150
|
export async function syncCommand(argv: string[]) {
|
|
1460
|
-
|
|
3151
|
+
const parsed = parseCliContextArgs(argv);
|
|
3152
|
+
if (
|
|
3153
|
+
parsed.argv.includes("--help") ||
|
|
3154
|
+
parsed.argv.includes("-h") ||
|
|
3155
|
+
parsed.argv[0] === "help"
|
|
3156
|
+
) {
|
|
1461
3157
|
console.log(`facult sync — sync managed tools with canonical state
|
|
1462
3158
|
|
|
1463
3159
|
Usage:
|
|
1464
|
-
facult sync [tool] [--dry-run]
|
|
3160
|
+
facult sync [tool] [--dry-run] [--builtin-conflicts overwrite] [--root PATH|--global|--project]
|
|
1465
3161
|
|
|
1466
3162
|
Options:
|
|
1467
3163
|
--dry-run Show what would change
|
|
3164
|
+
--builtin-conflicts overwrite Replace locally modified builtin-backed rendered files
|
|
1468
3165
|
`);
|
|
1469
3166
|
return;
|
|
1470
3167
|
}
|
|
1471
|
-
const tool = argv.find((arg) => !arg.startsWith("-"));
|
|
1472
|
-
const dryRun = argv.includes("--dry-run");
|
|
3168
|
+
const tool = parsed.argv.find((arg) => !arg.startsWith("-"));
|
|
3169
|
+
const dryRun = parsed.argv.includes("--dry-run");
|
|
3170
|
+
const builtinConflictIndex = parsed.argv.indexOf("--builtin-conflicts");
|
|
3171
|
+
let builtinConflictMode: "warn" | "overwrite" | undefined;
|
|
3172
|
+
if (builtinConflictIndex !== -1) {
|
|
3173
|
+
const value = parsed.argv[builtinConflictIndex + 1];
|
|
3174
|
+
if (value !== "overwrite") {
|
|
3175
|
+
console.error('--builtin-conflicts currently supports only "overwrite"');
|
|
3176
|
+
process.exitCode = 1;
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
builtinConflictMode = value;
|
|
3180
|
+
}
|
|
1473
3181
|
try {
|
|
1474
|
-
await syncManagedTools({
|
|
3182
|
+
await syncManagedTools({
|
|
3183
|
+
tool,
|
|
3184
|
+
dryRun,
|
|
3185
|
+
builtinConflictMode,
|
|
3186
|
+
rootDir: resolveCliContextRoot({
|
|
3187
|
+
rootArg: parsed.rootArg,
|
|
3188
|
+
scope: parsed.scope,
|
|
3189
|
+
cwd: process.cwd(),
|
|
3190
|
+
}),
|
|
3191
|
+
});
|
|
1475
3192
|
} catch (err) {
|
|
1476
3193
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1477
3194
|
process.exitCode = 1;
|