facult 2.3.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -10
- package/bin/fclt.cjs +86 -3
- package/package.json +1 -1
- package/src/adapters/factory.ts +228 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/types.ts +22 -0
- package/src/autosync.ts +192 -8
- package/src/index-builder.ts +12 -3
- package/src/manage.ts +487 -13
- package/src/paths.ts +69 -0
- package/src/scan.ts +26 -1
- package/src/self-update.ts +4 -2
package/src/manage.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type SkillEntry,
|
|
33
33
|
} from "./index-builder";
|
|
34
34
|
import {
|
|
35
|
-
|
|
35
|
+
facultMachineStateDir,
|
|
36
36
|
facultRootDir,
|
|
37
37
|
legacyFacultStateDirForRoot,
|
|
38
38
|
projectRootFromAiRoot,
|
|
@@ -44,6 +44,7 @@ export interface ManagedToolState {
|
|
|
44
44
|
skillsDir?: string;
|
|
45
45
|
mcpConfig?: string;
|
|
46
46
|
agentsDir?: string;
|
|
47
|
+
automationDir?: string;
|
|
47
48
|
toolHome?: string;
|
|
48
49
|
globalAgentsPath?: string;
|
|
49
50
|
globalAgentsOverridePath?: string;
|
|
@@ -75,6 +76,7 @@ export interface ToolPaths {
|
|
|
75
76
|
skillsDir?: string;
|
|
76
77
|
mcpConfig?: string;
|
|
77
78
|
agentsDir?: string;
|
|
79
|
+
automationDir?: string;
|
|
78
80
|
toolHome?: string;
|
|
79
81
|
rulesDir?: string;
|
|
80
82
|
toolConfig?: string;
|
|
@@ -163,6 +165,7 @@ function defaultToolPaths(
|
|
|
163
165
|
skillsDir: toolBase(".codex", "skills"),
|
|
164
166
|
mcpConfig: toolBase(".codex", "mcp.json"),
|
|
165
167
|
agentsDir: toolBase(".codex", "agents"),
|
|
168
|
+
automationDir: homePath(home, ".codex", "automations"),
|
|
166
169
|
toolHome: toolBase(".codex"),
|
|
167
170
|
rulesDir: toolBase(".codex", "rules"),
|
|
168
171
|
toolConfig: toolBase(".codex", "config.toml"),
|
|
@@ -200,6 +203,19 @@ function defaultToolPaths(
|
|
|
200
203
|
skillsDir: toolBase(".antigravity", "skills"),
|
|
201
204
|
mcpConfig: toolBase(".antigravity", "mcp.json"),
|
|
202
205
|
},
|
|
206
|
+
factory: {
|
|
207
|
+
tool: "factory",
|
|
208
|
+
skillsDir: projectRoot
|
|
209
|
+
? join(projectRoot, ".factory", "skills")
|
|
210
|
+
: homePath(home, ".factory", "skills"),
|
|
211
|
+
mcpConfig: projectRoot
|
|
212
|
+
? join(projectRoot, ".factory", "mcp.json")
|
|
213
|
+
: homePath(home, ".factory", "mcp.json"),
|
|
214
|
+
agentsDir: projectRoot
|
|
215
|
+
? join(projectRoot, ".factory", "droids")
|
|
216
|
+
: homePath(home, ".factory", "droids"),
|
|
217
|
+
toolHome: projectRoot ? undefined : homePath(home, ".factory"),
|
|
218
|
+
},
|
|
203
219
|
};
|
|
204
220
|
|
|
205
221
|
const adapterDefaults = (tool: string): ToolPaths | null => {
|
|
@@ -293,7 +309,7 @@ export function managedStatePathForRoot(
|
|
|
293
309
|
home: string = homedir(),
|
|
294
310
|
rootDir?: string
|
|
295
311
|
): string {
|
|
296
|
-
return join(
|
|
312
|
+
return join(facultMachineStateDir(home, rootDir), "managed.json");
|
|
297
313
|
}
|
|
298
314
|
|
|
299
315
|
function legacyManagedStatePathForRoot(
|
|
@@ -336,7 +352,7 @@ export async function saveManagedState(
|
|
|
336
352
|
home: string = homedir(),
|
|
337
353
|
rootDir?: string
|
|
338
354
|
) {
|
|
339
|
-
const dir =
|
|
355
|
+
const dir = facultMachineStateDir(home, rootDir);
|
|
340
356
|
await ensureDir(dir);
|
|
341
357
|
await Bun.write(
|
|
342
358
|
managedStatePathForRoot(home, rootDir),
|
|
@@ -433,6 +449,167 @@ async function loadCanonicalAgents(
|
|
|
433
449
|
return await loadAgentsFromRoot(homePath(rootDir, "agents"));
|
|
434
450
|
}
|
|
435
451
|
|
|
452
|
+
function managedAgentFileExtension(tool: string): string {
|
|
453
|
+
return getAdapter(tool)?.agentFileExtension ?? ".toml";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function renderManagedAgentFile(args: {
|
|
457
|
+
agent: { name: string; sourcePath: string; raw: string };
|
|
458
|
+
homeDir: string;
|
|
459
|
+
rootDir: string;
|
|
460
|
+
tool: string;
|
|
461
|
+
targetPath: string;
|
|
462
|
+
}): Promise<string> {
|
|
463
|
+
const adapter = getAdapter(args.tool);
|
|
464
|
+
if (adapter?.renderAgent) {
|
|
465
|
+
return await adapter.renderAgent({
|
|
466
|
+
raw: args.agent.raw,
|
|
467
|
+
homeDir: args.homeDir,
|
|
468
|
+
rootDir: args.rootDir,
|
|
469
|
+
projectRoot:
|
|
470
|
+
projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
|
|
471
|
+
tool: args.tool,
|
|
472
|
+
targetPath: args.targetPath,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return await renderCanonicalText(args.agent.raw, {
|
|
477
|
+
homeDir: args.homeDir,
|
|
478
|
+
rootDir: args.rootDir,
|
|
479
|
+
projectRoot: projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
|
|
480
|
+
targetTool: args.tool,
|
|
481
|
+
targetPath: args.targetPath,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function loadManagedAgentsFromTool(args: {
|
|
486
|
+
tool: string;
|
|
487
|
+
agentsDir: string;
|
|
488
|
+
}): Promise<{ name: string; sourcePath: string; raw: string }[]> {
|
|
489
|
+
const adapter = getAdapter(args.tool);
|
|
490
|
+
if (!adapter?.parseManagedAgentFile) {
|
|
491
|
+
return await loadAgentsFromRoot(args.agentsDir);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const extension = managedAgentFileExtension(args.tool);
|
|
495
|
+
const entries = await readdir(args.agentsDir, { withFileTypes: true }).catch(
|
|
496
|
+
() => [] as import("node:fs").Dirent[]
|
|
497
|
+
);
|
|
498
|
+
const out: { name: string; sourcePath: string; raw: string }[] = [];
|
|
499
|
+
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
if (!(entry.isFile() && entry.name.endsWith(extension))) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const sourcePath = join(args.agentsDir, entry.name);
|
|
505
|
+
const parsed = await adapter.parseManagedAgentFile(sourcePath);
|
|
506
|
+
if (!parsed) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
out.push(parsed);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
interface AutomationEntry {
|
|
516
|
+
name: string;
|
|
517
|
+
sourceDir: string;
|
|
518
|
+
files: Map<string, string>;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function listRelativeFiles(root: string): Promise<string[]> {
|
|
522
|
+
const out: string[] = [];
|
|
523
|
+
|
|
524
|
+
async function visit(currentDir: string, prefix = ""): Promise<void> {
|
|
525
|
+
const entries = await readdir(currentDir, { withFileTypes: true }).catch(
|
|
526
|
+
() => [] as import("node:fs").Dirent[]
|
|
527
|
+
);
|
|
528
|
+
for (const entry of entries) {
|
|
529
|
+
if (entry.name.startsWith(".")) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const relPath = prefix ? join(prefix, entry.name) : entry.name;
|
|
533
|
+
const fullPath = join(currentDir, entry.name);
|
|
534
|
+
if (entry.isDirectory()) {
|
|
535
|
+
await visit(fullPath, relPath);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (entry.isFile()) {
|
|
539
|
+
out.push(relPath);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
await visit(root);
|
|
545
|
+
return out.sort();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function loadAutomationEntries(
|
|
549
|
+
automationsRoot: string
|
|
550
|
+
): Promise<AutomationEntry[]> {
|
|
551
|
+
const entries = await readdir(automationsRoot, { withFileTypes: true }).catch(
|
|
552
|
+
() => [] as import("node:fs").Dirent[]
|
|
553
|
+
);
|
|
554
|
+
const out: AutomationEntry[] = [];
|
|
555
|
+
|
|
556
|
+
for (const entry of entries) {
|
|
557
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const sourceDir = join(automationsRoot, entry.name);
|
|
561
|
+
const relativeFiles = await listRelativeFiles(sourceDir);
|
|
562
|
+
const files = new Map<string, string>();
|
|
563
|
+
for (const relPath of relativeFiles) {
|
|
564
|
+
const raw = await readTextIfExists(join(sourceDir, relPath));
|
|
565
|
+
if (raw == null) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
files.set(relPath, raw);
|
|
569
|
+
}
|
|
570
|
+
if (!files.has("automation.toml")) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
out.push({
|
|
574
|
+
name: entry.name,
|
|
575
|
+
sourceDir,
|
|
576
|
+
files,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function loadCanonicalAutomations(
|
|
584
|
+
rootDir: string
|
|
585
|
+
): Promise<AutomationEntry[]> {
|
|
586
|
+
return await loadAutomationEntries(join(rootDir, "automations"));
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function automationEntriesEqual(
|
|
590
|
+
left: AutomationEntry,
|
|
591
|
+
right: AutomationEntry
|
|
592
|
+
): boolean {
|
|
593
|
+
if (left.files.size !== right.files.size) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
for (const [relPath, leftRaw] of left.files.entries()) {
|
|
597
|
+
if (right.files.get(relPath) !== leftRaw) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function canonicalAutomationsExist(rootDir: string): Promise<boolean> {
|
|
605
|
+
try {
|
|
606
|
+
const automations = await loadCanonicalAutomations(rootDir);
|
|
607
|
+
return automations.length > 0;
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
436
613
|
async function loadMergedIndex(
|
|
437
614
|
homeDir: string,
|
|
438
615
|
rootDir: string
|
|
@@ -536,14 +713,15 @@ async function planAgentFileChanges({
|
|
|
536
713
|
const contents = new Map<string, string>();
|
|
537
714
|
const sources = new Map<string, string>();
|
|
538
715
|
const desiredPaths = new Set<string>();
|
|
716
|
+
const extension = managedAgentFileExtension(tool);
|
|
539
717
|
|
|
540
718
|
for (const agent of agents) {
|
|
541
|
-
const target = homePath(agentsDir, `${agent.name}
|
|
542
|
-
const rendered = await
|
|
719
|
+
const target = homePath(agentsDir, `${agent.name}${extension}`);
|
|
720
|
+
const rendered = await renderManagedAgentFile({
|
|
721
|
+
agent,
|
|
543
722
|
homeDir,
|
|
544
723
|
rootDir,
|
|
545
|
-
|
|
546
|
-
targetTool: tool,
|
|
724
|
+
tool,
|
|
547
725
|
targetPath: target,
|
|
548
726
|
});
|
|
549
727
|
desiredPaths.add(target);
|
|
@@ -558,7 +736,7 @@ async function planAgentFileChanges({
|
|
|
558
736
|
const remove = new Set<string>();
|
|
559
737
|
|
|
560
738
|
for (const entry of existing) {
|
|
561
|
-
if (!(entry.isFile() && entry.name.endsWith(
|
|
739
|
+
if (!(entry.isFile() && entry.name.endsWith(extension))) {
|
|
562
740
|
continue;
|
|
563
741
|
}
|
|
564
742
|
const p = homePath(agentsDir, entry.name);
|
|
@@ -624,6 +802,58 @@ async function syncAgentFiles({
|
|
|
624
802
|
return { add: plan.add, remove: plan.remove };
|
|
625
803
|
}
|
|
626
804
|
|
|
805
|
+
async function planAutomationFileChanges(args: {
|
|
806
|
+
automationDir: string;
|
|
807
|
+
rootDir: string;
|
|
808
|
+
previouslyManagedTargets?: string[];
|
|
809
|
+
}): Promise<{
|
|
810
|
+
add: string[];
|
|
811
|
+
remove: string[];
|
|
812
|
+
contents: Map<string, string>;
|
|
813
|
+
sources: Map<string, string>;
|
|
814
|
+
}> {
|
|
815
|
+
const automations = await loadCanonicalAutomations(args.rootDir);
|
|
816
|
+
const contents = new Map<string, string>();
|
|
817
|
+
const sources = new Map<string, string>();
|
|
818
|
+
const desiredPaths = new Set<string>();
|
|
819
|
+
|
|
820
|
+
for (const automation of automations) {
|
|
821
|
+
for (const [relPath, raw] of automation.files.entries()) {
|
|
822
|
+
const targetPath = join(args.automationDir, automation.name, relPath);
|
|
823
|
+
const sourcePath = join(automation.sourceDir, relPath);
|
|
824
|
+
desiredPaths.add(targetPath);
|
|
825
|
+
contents.set(targetPath, raw);
|
|
826
|
+
sources.set(targetPath, sourcePath);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const add = new Set<string>();
|
|
831
|
+
for (const targetPath of desiredPaths) {
|
|
832
|
+
const current = await readTextIfExists(targetPath);
|
|
833
|
+
const desired = contents.get(targetPath);
|
|
834
|
+
if (desired != null && current !== desired) {
|
|
835
|
+
add.add(targetPath);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const remove = Array.from(
|
|
840
|
+
new Set(
|
|
841
|
+
(args.previouslyManagedTargets ?? []).filter(
|
|
842
|
+
(targetPath) =>
|
|
843
|
+
targetPath.startsWith(join(args.automationDir, "")) &&
|
|
844
|
+
!desiredPaths.has(targetPath)
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
).sort();
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
add: Array.from(add).sort(),
|
|
851
|
+
remove,
|
|
852
|
+
contents,
|
|
853
|
+
sources,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
627
857
|
async function listSkillDirs(skillsRoot: string): Promise<string[]> {
|
|
628
858
|
try {
|
|
629
859
|
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
@@ -945,6 +1175,7 @@ interface ExistingManagedItem {
|
|
|
945
1175
|
kind:
|
|
946
1176
|
| "skill"
|
|
947
1177
|
| "agent"
|
|
1178
|
+
| "automation"
|
|
948
1179
|
| "global-doc"
|
|
949
1180
|
| "rule"
|
|
950
1181
|
| "tool-config"
|
|
@@ -1060,11 +1291,15 @@ function logManagedImportPlan(tool: string, plan: ExistingManagedImportPlan) {
|
|
|
1060
1291
|
}
|
|
1061
1292
|
|
|
1062
1293
|
async function planExistingToolAgentAdoption(args: {
|
|
1294
|
+
tool: string;
|
|
1063
1295
|
rootDir: string;
|
|
1064
1296
|
agentsDir: string;
|
|
1065
1297
|
}): Promise<ExistingManagedImportPlan> {
|
|
1066
1298
|
const plan = emptyManagedImportPlan();
|
|
1067
|
-
const agents = await
|
|
1299
|
+
const agents = await loadManagedAgentsFromTool({
|
|
1300
|
+
tool: args.tool,
|
|
1301
|
+
agentsDir: args.agentsDir,
|
|
1302
|
+
});
|
|
1068
1303
|
for (const agent of agents) {
|
|
1069
1304
|
const canonicalPath = join(
|
|
1070
1305
|
args.rootDir,
|
|
@@ -1091,12 +1326,16 @@ async function planExistingToolAgentAdoption(args: {
|
|
|
1091
1326
|
}
|
|
1092
1327
|
|
|
1093
1328
|
async function adoptExistingToolAgents(args: {
|
|
1329
|
+
tool: string;
|
|
1094
1330
|
rootDir: string;
|
|
1095
1331
|
agentsDir: string;
|
|
1096
1332
|
conflictMode: "keep-canonical" | "keep-existing";
|
|
1097
1333
|
}): Promise<ExistingManagedItem[]> {
|
|
1098
1334
|
const adopted: ExistingManagedItem[] = [];
|
|
1099
|
-
const agents = await
|
|
1335
|
+
const agents = await loadManagedAgentsFromTool({
|
|
1336
|
+
tool: args.tool,
|
|
1337
|
+
agentsDir: args.agentsDir,
|
|
1338
|
+
});
|
|
1100
1339
|
for (const agent of agents) {
|
|
1101
1340
|
const canonicalPath = join(
|
|
1102
1341
|
args.rootDir,
|
|
@@ -1123,6 +1362,87 @@ async function adoptExistingToolAgents(args: {
|
|
|
1123
1362
|
return adopted;
|
|
1124
1363
|
}
|
|
1125
1364
|
|
|
1365
|
+
async function planExistingAutomationAdoption(args: {
|
|
1366
|
+
rootDir: string;
|
|
1367
|
+
automationDir: string;
|
|
1368
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1369
|
+
const plan = emptyManagedImportPlan();
|
|
1370
|
+
const liveAutomations = await loadAutomationEntries(args.automationDir);
|
|
1371
|
+
const canonicalAutomations = new Map(
|
|
1372
|
+
(await loadCanonicalAutomations(args.rootDir)).map((entry) => [
|
|
1373
|
+
entry.name,
|
|
1374
|
+
entry,
|
|
1375
|
+
])
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
for (const liveAutomation of liveAutomations) {
|
|
1379
|
+
const canonicalAutomation = canonicalAutomations.get(liveAutomation.name);
|
|
1380
|
+
if (!canonicalAutomation) {
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const item: ExistingManagedItem = {
|
|
1384
|
+
kind: "automation",
|
|
1385
|
+
name: liveAutomation.name,
|
|
1386
|
+
livePath: liveAutomation.sourceDir,
|
|
1387
|
+
canonicalPath: join(args.rootDir, "automations", liveAutomation.name),
|
|
1388
|
+
};
|
|
1389
|
+
if (automationEntriesEqual(liveAutomation, canonicalAutomation)) {
|
|
1390
|
+
plan.identical.push(item);
|
|
1391
|
+
} else {
|
|
1392
|
+
plan.conflicts.push(item);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return mergeManagedImportPlans(plan);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async function adoptExistingAutomations(args: {
|
|
1400
|
+
rootDir: string;
|
|
1401
|
+
automationDir: string;
|
|
1402
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1403
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1404
|
+
if (args.conflictMode !== "keep-existing") {
|
|
1405
|
+
return [];
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const adopted: ExistingManagedItem[] = [];
|
|
1409
|
+
const liveAutomations = await loadAutomationEntries(args.automationDir);
|
|
1410
|
+
const canonicalAutomations = new Map(
|
|
1411
|
+
(await loadCanonicalAutomations(args.rootDir)).map((entry) => [
|
|
1412
|
+
entry.name,
|
|
1413
|
+
entry,
|
|
1414
|
+
])
|
|
1415
|
+
);
|
|
1416
|
+
|
|
1417
|
+
for (const liveAutomation of liveAutomations) {
|
|
1418
|
+
const canonicalAutomation = canonicalAutomations.get(liveAutomation.name);
|
|
1419
|
+
if (
|
|
1420
|
+
!(
|
|
1421
|
+
canonicalAutomation &&
|
|
1422
|
+
!automationEntriesEqual(liveAutomation, canonicalAutomation)
|
|
1423
|
+
)
|
|
1424
|
+
) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const canonicalPath = join(
|
|
1428
|
+
args.rootDir,
|
|
1429
|
+
"automations",
|
|
1430
|
+
liveAutomation.name
|
|
1431
|
+
);
|
|
1432
|
+
await ensureDir(dirname(canonicalPath));
|
|
1433
|
+
await rm(canonicalPath, { recursive: true, force: true });
|
|
1434
|
+
await cp(liveAutomation.sourceDir, canonicalPath, { recursive: true });
|
|
1435
|
+
adopted.push({
|
|
1436
|
+
kind: "automation",
|
|
1437
|
+
name: liveAutomation.name,
|
|
1438
|
+
livePath: liveAutomation.sourceDir,
|
|
1439
|
+
canonicalPath,
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return adopted;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1126
1446
|
async function planExistingGlobalDocAdoption(args: {
|
|
1127
1447
|
rootDir: string;
|
|
1128
1448
|
tool: string;
|
|
@@ -1784,10 +2104,17 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1784
2104
|
asManagedSkillPlan(existingSkillPlan),
|
|
1785
2105
|
toolPaths.agentsDir
|
|
1786
2106
|
? await planExistingToolAgentAdoption({
|
|
2107
|
+
tool,
|
|
1787
2108
|
rootDir,
|
|
1788
2109
|
agentsDir: toolPaths.agentsDir,
|
|
1789
2110
|
})
|
|
1790
2111
|
: emptyManagedImportPlan(),
|
|
2112
|
+
toolPaths.automationDir
|
|
2113
|
+
? await planExistingAutomationAdoption({
|
|
2114
|
+
rootDir,
|
|
2115
|
+
automationDir: toolPaths.automationDir,
|
|
2116
|
+
})
|
|
2117
|
+
: emptyManagedImportPlan(),
|
|
1791
2118
|
toolPaths.toolHome
|
|
1792
2119
|
? await planExistingGlobalDocAdoption({
|
|
1793
2120
|
rootDir,
|
|
@@ -1826,6 +2153,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1826
2153
|
if (
|
|
1827
2154
|
(toolPaths.skillsDir ||
|
|
1828
2155
|
toolPaths.agentsDir ||
|
|
2156
|
+
toolPaths.automationDir ||
|
|
1829
2157
|
toolPaths.toolHome ||
|
|
1830
2158
|
toolPaths.rulesDir ||
|
|
1831
2159
|
toolPaths.toolConfig ||
|
|
@@ -1900,12 +2228,21 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1900
2228
|
}
|
|
1901
2229
|
if (toolPaths.agentsDir && opts.adoptExisting) {
|
|
1902
2230
|
const result = await adoptExistingToolAgents({
|
|
2231
|
+
tool,
|
|
1903
2232
|
rootDir,
|
|
1904
2233
|
agentsDir: toolPaths.agentsDir,
|
|
1905
2234
|
conflictMode: importConflictMode,
|
|
1906
2235
|
});
|
|
1907
2236
|
adoptedSkills.push(...result.map((item) => item.name));
|
|
1908
2237
|
}
|
|
2238
|
+
if (toolPaths.automationDir && opts.adoptExisting) {
|
|
2239
|
+
const result = await adoptExistingAutomations({
|
|
2240
|
+
rootDir,
|
|
2241
|
+
automationDir: toolPaths.automationDir,
|
|
2242
|
+
conflictMode: importConflictMode,
|
|
2243
|
+
});
|
|
2244
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
2245
|
+
}
|
|
1909
2246
|
if (toolPaths.toolHome && opts.adoptExisting) {
|
|
1910
2247
|
const result = await adoptExistingGlobalDocs({
|
|
1911
2248
|
rootDir,
|
|
@@ -1957,6 +2294,12 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1957
2294
|
tool,
|
|
1958
2295
|
})
|
|
1959
2296
|
: null;
|
|
2297
|
+
const automationPreview = toolPaths.automationDir
|
|
2298
|
+
? await planAutomationFileChanges({
|
|
2299
|
+
automationDir: toolPaths.automationDir,
|
|
2300
|
+
rootDir,
|
|
2301
|
+
})
|
|
2302
|
+
: null;
|
|
1960
2303
|
const globalDocsPreview = toolPaths.toolHome
|
|
1961
2304
|
? await planToolGlobalDocsSync({
|
|
1962
2305
|
homeDir: home,
|
|
@@ -2048,6 +2391,16 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2048
2391
|
});
|
|
2049
2392
|
}
|
|
2050
2393
|
|
|
2394
|
+
if (toolPaths.automationDir && automationPreview) {
|
|
2395
|
+
await ensureDir(toolPaths.automationDir);
|
|
2396
|
+
await applyRenderedRemoves(automationPreview.remove);
|
|
2397
|
+
await applyRenderedWrites({
|
|
2398
|
+
contents: automationPreview.contents,
|
|
2399
|
+
targets: Array.from(automationPreview.contents.keys()),
|
|
2400
|
+
});
|
|
2401
|
+
await pruneEmptyParents(automationPreview.remove, toolPaths.automationDir);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2051
2404
|
if (toolPaths.toolHome && globalDocsPreview) {
|
|
2052
2405
|
await ensureDir(toolPaths.toolHome);
|
|
2053
2406
|
await applyRenderedRemoves(globalDocsPreview.remove);
|
|
@@ -2086,6 +2439,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2086
2439
|
skillsDir: toolPaths.skillsDir,
|
|
2087
2440
|
mcpConfig: toolPaths.mcpConfig,
|
|
2088
2441
|
agentsDir: toolPaths.agentsDir,
|
|
2442
|
+
automationDir: toolPaths.automationDir,
|
|
2089
2443
|
toolHome: globalDocsPreview?.managedTargets.length
|
|
2090
2444
|
? toolPaths.toolHome
|
|
2091
2445
|
: undefined,
|
|
@@ -2123,6 +2477,15 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2123
2477
|
sources: agentPreview.sources,
|
|
2124
2478
|
});
|
|
2125
2479
|
}
|
|
2480
|
+
if (automationPreview) {
|
|
2481
|
+
updateRenderedTargetState({
|
|
2482
|
+
entry: managedEntry,
|
|
2483
|
+
writtenTargets: Array.from(automationPreview.contents.keys()),
|
|
2484
|
+
removedTargets: automationPreview.remove,
|
|
2485
|
+
contents: automationPreview.contents,
|
|
2486
|
+
sources: automationPreview.sources,
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2126
2489
|
if (globalDocsPreview) {
|
|
2127
2490
|
updateRenderedTargetState({
|
|
2128
2491
|
entry: managedEntry,
|
|
@@ -2238,6 +2601,14 @@ export async function unmanageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2238
2601
|
});
|
|
2239
2602
|
}
|
|
2240
2603
|
|
|
2604
|
+
if (entry.automationDir) {
|
|
2605
|
+
const automationTargets = Object.keys(entry.renderedTargets ?? {}).filter(
|
|
2606
|
+
(targetPath) => targetPath.startsWith(join(entry.automationDir!, ""))
|
|
2607
|
+
);
|
|
2608
|
+
await applyRenderedRemoves(automationTargets);
|
|
2609
|
+
await pruneEmptyParents(automationTargets, entry.automationDir);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2241
2612
|
if (entry.globalAgentsPath) {
|
|
2242
2613
|
await restoreBackup({
|
|
2243
2614
|
original: entry.globalAgentsPath,
|
|
@@ -2320,6 +2691,15 @@ async function repairManagedToolEntry(args: {
|
|
|
2320
2691
|
changed = true;
|
|
2321
2692
|
}
|
|
2322
2693
|
|
|
2694
|
+
if (
|
|
2695
|
+
!next.automationDir &&
|
|
2696
|
+
toolPaths.automationDir &&
|
|
2697
|
+
(await canonicalAutomationsExist(rootDir))
|
|
2698
|
+
) {
|
|
2699
|
+
next.automationDir = toolPaths.automationDir;
|
|
2700
|
+
changed = true;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2323
2703
|
if (toolPaths.toolHome && !next.toolHome) {
|
|
2324
2704
|
const preview = await syncToolGlobalDocs({
|
|
2325
2705
|
homeDir,
|
|
@@ -2406,6 +2786,7 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2406
2786
|
desiredContents: Map<string, string>;
|
|
2407
2787
|
desiredSources: Map<string, string>;
|
|
2408
2788
|
conflictMode?: "warn" | "overwrite";
|
|
2789
|
+
protectAllSources?: boolean;
|
|
2409
2790
|
}): Promise<RenderedApplyPlan> {
|
|
2410
2791
|
if (args.conflictMode === "overwrite") {
|
|
2411
2792
|
return {
|
|
@@ -2433,7 +2814,7 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2433
2814
|
continue;
|
|
2434
2815
|
}
|
|
2435
2816
|
const sourceKind = renderedSourceKindForPath(sourcePath);
|
|
2436
|
-
if (sourceKind !== "builtin") {
|
|
2817
|
+
if (sourceKind !== "builtin" && !args.protectAllSources) {
|
|
2437
2818
|
if (args.desiredWrites.includes(targetPath)) {
|
|
2438
2819
|
write.push(targetPath);
|
|
2439
2820
|
} else {
|
|
@@ -2452,8 +2833,16 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2452
2833
|
}
|
|
2453
2834
|
|
|
2454
2835
|
const currentHash = renderedHash(current);
|
|
2836
|
+
const desiredHash = args.desiredContents.get(targetPath)
|
|
2837
|
+
? renderedHash(args.desiredContents.get(targetPath)!)
|
|
2838
|
+
: null;
|
|
2455
2839
|
if (prior?.hash) {
|
|
2456
|
-
if (
|
|
2840
|
+
if (
|
|
2841
|
+
currentHash === prior.hash ||
|
|
2842
|
+
(args.desiredWrites.includes(targetPath) &&
|
|
2843
|
+
desiredHash != null &&
|
|
2844
|
+
currentHash === desiredHash)
|
|
2845
|
+
) {
|
|
2457
2846
|
if (args.desiredWrites.includes(targetPath)) {
|
|
2458
2847
|
write.push(targetPath);
|
|
2459
2848
|
} else {
|
|
@@ -2470,6 +2859,15 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2470
2859
|
continue;
|
|
2471
2860
|
}
|
|
2472
2861
|
|
|
2862
|
+
if (
|
|
2863
|
+
args.desiredWrites.includes(targetPath) &&
|
|
2864
|
+
desiredHash != null &&
|
|
2865
|
+
currentHash === desiredHash
|
|
2866
|
+
) {
|
|
2867
|
+
write.push(targetPath);
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2473
2871
|
conflicts.push({
|
|
2474
2872
|
targetPath,
|
|
2475
2873
|
sourcePath,
|
|
@@ -2496,8 +2894,14 @@ function logRenderedConflicts(
|
|
|
2496
2894
|
conflict.reason === "unknown_state"
|
|
2497
2895
|
? "no prior managed hash is recorded"
|
|
2498
2896
|
: "local edits were detected";
|
|
2897
|
+
const surface =
|
|
2898
|
+
conflict.sourceKind === "builtin"
|
|
2899
|
+
? "builtin-backed target"
|
|
2900
|
+
: "managed target";
|
|
2499
2901
|
console.warn(
|
|
2500
|
-
|
|
2902
|
+
conflict.sourceKind === "builtin"
|
|
2903
|
+
? `${tool}: ${verb} ${surface} ${conflict.targetPath} because ${state}. Rerun with "--builtin-conflicts overwrite" to replace it with the latest packaged default.`
|
|
2904
|
+
: `${tool}: ${verb} ${surface} ${conflict.targetPath} because ${state}.`
|
|
2501
2905
|
);
|
|
2502
2906
|
}
|
|
2503
2907
|
}
|
|
@@ -2525,6 +2929,24 @@ async function applyRenderedRemoves(targets: string[]) {
|
|
|
2525
2929
|
}
|
|
2526
2930
|
}
|
|
2527
2931
|
|
|
2932
|
+
async function pruneEmptyParents(targets: string[], stopDir: string) {
|
|
2933
|
+
const candidateDirs = Array.from(
|
|
2934
|
+
new Set(targets.map((pathValue) => dirname(pathValue)))
|
|
2935
|
+
).sort((a, b) => b.length - a.length);
|
|
2936
|
+
|
|
2937
|
+
for (const startDir of candidateDirs) {
|
|
2938
|
+
let currentDir = startDir;
|
|
2939
|
+
while (currentDir.startsWith(join(stopDir, "")) && currentDir !== stopDir) {
|
|
2940
|
+
const entries = await readdir(currentDir).catch(() => null);
|
|
2941
|
+
if (!(entries && entries.length === 0)) {
|
|
2942
|
+
break;
|
|
2943
|
+
}
|
|
2944
|
+
await rm(currentDir, { recursive: true, force: true });
|
|
2945
|
+
currentDir = dirname(currentDir);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2528
2950
|
function updateRenderedTargetState(args: {
|
|
2529
2951
|
entry: ManagedToolState;
|
|
2530
2952
|
writtenTargets: string[];
|
|
@@ -2558,6 +2980,8 @@ function logSyncDryRun({
|
|
|
2558
2980
|
mcpPlan,
|
|
2559
2981
|
agentPlan,
|
|
2560
2982
|
agentConflicts,
|
|
2983
|
+
automationPlan,
|
|
2984
|
+
automationConflicts,
|
|
2561
2985
|
globalDocsPlan,
|
|
2562
2986
|
globalDocsConflicts,
|
|
2563
2987
|
rulesPlan,
|
|
@@ -2571,6 +2995,8 @@ function logSyncDryRun({
|
|
|
2571
2995
|
mcpPlan: { needsWrite: boolean };
|
|
2572
2996
|
agentPlan: { add: string[]; remove: string[] };
|
|
2573
2997
|
agentConflicts: RenderedConflict[];
|
|
2998
|
+
automationPlan: { write: string[]; remove: string[] };
|
|
2999
|
+
automationConflicts: RenderedConflict[];
|
|
2574
3000
|
globalDocsPlan: { write: string[]; remove: string[] };
|
|
2575
3001
|
globalDocsConflicts: RenderedConflict[];
|
|
2576
3002
|
rulesPlan: { write: string[]; remove: string[] };
|
|
@@ -2591,6 +3017,13 @@ function logSyncDryRun({
|
|
|
2591
3017
|
console.log(`${tool}: would remove agent ${p}`);
|
|
2592
3018
|
}
|
|
2593
3019
|
logRenderedConflicts(tool, agentConflicts, true);
|
|
3020
|
+
for (const p of automationPlan.write) {
|
|
3021
|
+
console.log(`${tool}: would write automation ${p}`);
|
|
3022
|
+
}
|
|
3023
|
+
for (const p of automationPlan.remove) {
|
|
3024
|
+
console.log(`${tool}: would remove automation ${p}`);
|
|
3025
|
+
}
|
|
3026
|
+
logRenderedConflicts(tool, automationConflicts, true);
|
|
2594
3027
|
for (const p of globalDocsPlan.write) {
|
|
2595
3028
|
console.log(`${tool}: would write global doc ${p}`);
|
|
2596
3029
|
}
|
|
@@ -2620,6 +3053,8 @@ function logSyncDryRun({
|
|
|
2620
3053
|
skillPlan.remove.length === 0 &&
|
|
2621
3054
|
agentPlan.add.length === 0 &&
|
|
2622
3055
|
agentPlan.remove.length === 0 &&
|
|
3056
|
+
automationPlan.write.length === 0 &&
|
|
3057
|
+
automationPlan.remove.length === 0 &&
|
|
2623
3058
|
globalDocsPlan.write.length === 0 &&
|
|
2624
3059
|
globalDocsPlan.remove.length === 0 &&
|
|
2625
3060
|
rulesPlan.write.length === 0 &&
|
|
@@ -2628,6 +3063,7 @@ function logSyncDryRun({
|
|
|
2628
3063
|
!configPlan.remove &&
|
|
2629
3064
|
!mcpPlan.needsWrite &&
|
|
2630
3065
|
agentConflicts.length === 0 &&
|
|
3066
|
+
automationConflicts.length === 0 &&
|
|
2631
3067
|
globalDocsConflicts.length === 0 &&
|
|
2632
3068
|
rulesConflicts.length === 0 &&
|
|
2633
3069
|
configConflicts.length === 0
|
|
@@ -2657,6 +3093,7 @@ async function repairManagedCanonicalContent(args: {
|
|
|
2657
3093
|
|
|
2658
3094
|
if (args.entry.agentsBackup) {
|
|
2659
3095
|
const items = await adoptExistingToolAgents({
|
|
3096
|
+
tool: args.entry.tool,
|
|
2660
3097
|
rootDir: args.rootDir,
|
|
2661
3098
|
agentsDir: args.entry.agentsBackup,
|
|
2662
3099
|
conflictMode: "keep-canonical",
|
|
@@ -2767,6 +3204,13 @@ async function syncManagedToolEntry({
|
|
|
2767
3204
|
tool,
|
|
2768
3205
|
})
|
|
2769
3206
|
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
3207
|
+
const automationPlan = entry.automationDir
|
|
3208
|
+
? await planAutomationFileChanges({
|
|
3209
|
+
automationDir: entry.automationDir,
|
|
3210
|
+
rootDir,
|
|
3211
|
+
previouslyManagedTargets: Object.keys(entry.renderedTargets ?? {}),
|
|
3212
|
+
})
|
|
3213
|
+
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
2770
3214
|
|
|
2771
3215
|
const mcpPlan = entry.mcpConfig
|
|
2772
3216
|
? await syncMcpConfig({
|
|
@@ -2846,6 +3290,15 @@ async function syncManagedToolEntry({
|
|
|
2846
3290
|
desiredSources: globalDocsPlan.sources,
|
|
2847
3291
|
conflictMode: builtinConflictMode,
|
|
2848
3292
|
});
|
|
3293
|
+
const automationRendered = await planRenderedTargetConflicts({
|
|
3294
|
+
entry,
|
|
3295
|
+
desiredWrites: automationPlan.add,
|
|
3296
|
+
desiredRemoves: automationPlan.remove,
|
|
3297
|
+
desiredContents: automationPlan.contents,
|
|
3298
|
+
desiredSources: automationPlan.sources,
|
|
3299
|
+
conflictMode: builtinConflictMode,
|
|
3300
|
+
protectAllSources: true,
|
|
3301
|
+
});
|
|
2849
3302
|
const rulesRendered = await planRenderedTargetConflicts({
|
|
2850
3303
|
entry,
|
|
2851
3304
|
desiredWrites: rulesPlan.write,
|
|
@@ -2882,6 +3335,11 @@ async function syncManagedToolEntry({
|
|
|
2882
3335
|
mcpPlan,
|
|
2883
3336
|
agentPlan: { add: agentRendered.write, remove: agentRendered.remove },
|
|
2884
3337
|
agentConflicts: agentRendered.conflicts,
|
|
3338
|
+
automationPlan: {
|
|
3339
|
+
write: automationRendered.write,
|
|
3340
|
+
remove: automationRendered.remove,
|
|
3341
|
+
},
|
|
3342
|
+
automationConflicts: automationRendered.conflicts,
|
|
2885
3343
|
globalDocsPlan: {
|
|
2886
3344
|
write: globalDocsRendered.write,
|
|
2887
3345
|
remove: globalDocsRendered.remove,
|
|
@@ -2902,6 +3360,14 @@ async function syncManagedToolEntry({
|
|
|
2902
3360
|
contents: agentPlan.contents,
|
|
2903
3361
|
targets: agentRendered.write,
|
|
2904
3362
|
});
|
|
3363
|
+
await applyRenderedRemoves(automationRendered.remove);
|
|
3364
|
+
await applyRenderedWrites({
|
|
3365
|
+
contents: automationPlan.contents,
|
|
3366
|
+
targets: automationRendered.write,
|
|
3367
|
+
});
|
|
3368
|
+
if (entry.automationDir) {
|
|
3369
|
+
await pruneEmptyParents(automationRendered.remove, entry.automationDir);
|
|
3370
|
+
}
|
|
2905
3371
|
await applyRenderedRemoves(globalDocsRendered.remove);
|
|
2906
3372
|
await applyRenderedWrites({
|
|
2907
3373
|
contents: globalDocsPlan.contents,
|
|
@@ -2918,6 +3384,7 @@ async function syncManagedToolEntry({
|
|
|
2918
3384
|
targets: configRendered.write,
|
|
2919
3385
|
});
|
|
2920
3386
|
logRenderedConflicts(tool, agentRendered.conflicts);
|
|
3387
|
+
logRenderedConflicts(tool, automationRendered.conflicts);
|
|
2921
3388
|
logRenderedConflicts(tool, globalDocsRendered.conflicts);
|
|
2922
3389
|
logRenderedConflicts(tool, rulesRendered.conflicts);
|
|
2923
3390
|
logRenderedConflicts(tool, configRendered.conflicts);
|
|
@@ -2929,6 +3396,13 @@ async function syncManagedToolEntry({
|
|
|
2929
3396
|
contents: agentPlan.contents,
|
|
2930
3397
|
sources: agentPlan.sources,
|
|
2931
3398
|
});
|
|
3399
|
+
updateRenderedTargetState({
|
|
3400
|
+
entry,
|
|
3401
|
+
writtenTargets: automationRendered.write,
|
|
3402
|
+
removedTargets: automationRendered.remove,
|
|
3403
|
+
contents: automationPlan.contents,
|
|
3404
|
+
sources: automationPlan.sources,
|
|
3405
|
+
});
|
|
2932
3406
|
updateRenderedTargetState({
|
|
2933
3407
|
entry,
|
|
2934
3408
|
writtenTargets: globalDocsRendered.write,
|