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/autosync.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
6
6
|
import { syncManagedTools } from "./manage";
|
|
7
7
|
import {
|
|
8
|
+
facultMachineStateDir,
|
|
8
9
|
facultRootDir,
|
|
9
10
|
facultStateDir,
|
|
10
11
|
legacyFacultStateDirForRoot,
|
|
@@ -84,6 +85,10 @@ interface GitSyncOutcome {
|
|
|
84
85
|
message?: string;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
let launchctlRunnerForTests:
|
|
89
|
+
| ((args: string[]) => Promise<CommandResult>)
|
|
90
|
+
| null = null;
|
|
91
|
+
|
|
87
92
|
function nowIso(): string {
|
|
88
93
|
return new Date().toISOString();
|
|
89
94
|
}
|
|
@@ -100,6 +105,10 @@ function runDetached(context: string, promise: Promise<void>) {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
function autosyncDir(home: string, rootDir?: string): string {
|
|
108
|
+
return join(facultMachineStateDir(home, rootDir), "autosync");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function canonicalAutosyncDir(home: string, rootDir?: string): string {
|
|
103
112
|
return join(facultStateDir(home, rootDir), "autosync");
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -155,11 +164,21 @@ function autosyncServiceName(
|
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
function autosyncLabel(serviceName: string): string {
|
|
167
|
+
return serviceName === "all"
|
|
168
|
+
? "com.fclt.autosync"
|
|
169
|
+
: `com.fclt.autosync.${serviceName}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function legacyAutosyncLabel(serviceName: string): string {
|
|
158
173
|
return serviceName === "all"
|
|
159
174
|
? "com.facult.autosync"
|
|
160
175
|
: `com.facult.autosync.${serviceName}`;
|
|
161
176
|
}
|
|
162
177
|
|
|
178
|
+
function autosyncLabelCandidates(serviceName: string): string[] {
|
|
179
|
+
return [autosyncLabel(serviceName), legacyAutosyncLabel(serviceName)];
|
|
180
|
+
}
|
|
181
|
+
|
|
163
182
|
function autosyncPlistPath(home: string, serviceName: string): string {
|
|
164
183
|
return join(
|
|
165
184
|
home,
|
|
@@ -331,6 +350,11 @@ export async function loadAutosyncConfig(
|
|
|
331
350
|
): Promise<AutosyncServiceConfig | null> {
|
|
332
351
|
const candidates = [
|
|
333
352
|
autosyncConfigPath(homeDir, serviceName, rootDir),
|
|
353
|
+
join(
|
|
354
|
+
canonicalAutosyncDir(homeDir, rootDir),
|
|
355
|
+
"services",
|
|
356
|
+
`${serviceName}.json`
|
|
357
|
+
),
|
|
334
358
|
legacyAutosyncConfigPath(homeDir, serviceName, rootDir),
|
|
335
359
|
];
|
|
336
360
|
for (const candidate of candidates) {
|
|
@@ -359,6 +383,11 @@ export async function loadAutosyncRuntimeState(
|
|
|
359
383
|
): Promise<AutosyncRuntimeState | null> {
|
|
360
384
|
const candidates = [
|
|
361
385
|
autosyncRuntimeStatePath(homeDir, serviceName, rootDir),
|
|
386
|
+
join(
|
|
387
|
+
canonicalAutosyncDir(homeDir, rootDir),
|
|
388
|
+
"state",
|
|
389
|
+
`${serviceName}.json`
|
|
390
|
+
),
|
|
362
391
|
legacyAutosyncRuntimeStatePath(homeDir, serviceName, rootDir),
|
|
363
392
|
];
|
|
364
393
|
for (const candidate of candidates) {
|
|
@@ -399,9 +428,18 @@ async function runCommand(
|
|
|
399
428
|
}
|
|
400
429
|
|
|
401
430
|
async function runLaunchctl(args: string[]): Promise<CommandResult> {
|
|
431
|
+
if (launchctlRunnerForTests) {
|
|
432
|
+
return await launchctlRunnerForTests(args);
|
|
433
|
+
}
|
|
402
434
|
return await runCommand(["launchctl", ...args]);
|
|
403
435
|
}
|
|
404
436
|
|
|
437
|
+
export function setLaunchctlRunnerForTests(
|
|
438
|
+
runner: ((args: string[]) => Promise<CommandResult>) | null
|
|
439
|
+
) {
|
|
440
|
+
launchctlRunnerForTests = runner;
|
|
441
|
+
}
|
|
442
|
+
|
|
405
443
|
function launchdDomain(): string {
|
|
406
444
|
return `gui/${process.getuid?.() ?? process.geteuid?.() ?? 0}`;
|
|
407
445
|
}
|
|
@@ -470,6 +508,122 @@ async function ensureGitRepo(repoDir: string): Promise<boolean> {
|
|
|
470
508
|
return await pathExists(join(repoDir, ".git"));
|
|
471
509
|
}
|
|
472
510
|
|
|
511
|
+
async function cleanupAutosyncLaunchAgentArtifacts(args: {
|
|
512
|
+
homeDir: string;
|
|
513
|
+
serviceName: string;
|
|
514
|
+
}) {
|
|
515
|
+
const domain = launchdDomain();
|
|
516
|
+
for (const label of autosyncLabelCandidates(args.serviceName)) {
|
|
517
|
+
await runLaunchctl(["bootout", `${domain}/${label}`]).catch(() => null);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const legacyPlistPath = join(
|
|
521
|
+
args.homeDir,
|
|
522
|
+
"Library",
|
|
523
|
+
"LaunchAgents",
|
|
524
|
+
`${legacyAutosyncLabel(args.serviceName)}.plist`
|
|
525
|
+
);
|
|
526
|
+
if (legacyPlistPath !== autosyncPlistPath(args.homeDir, args.serviceName)) {
|
|
527
|
+
await rm(legacyPlistPath, { force: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function cleanupLegacyAutosyncFiles(args: {
|
|
532
|
+
homeDir: string;
|
|
533
|
+
serviceName: string;
|
|
534
|
+
rootDir: string;
|
|
535
|
+
}) {
|
|
536
|
+
const legacyPaths = [
|
|
537
|
+
join(
|
|
538
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
539
|
+
"services",
|
|
540
|
+
`${args.serviceName}.json`
|
|
541
|
+
),
|
|
542
|
+
join(
|
|
543
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
544
|
+
"state",
|
|
545
|
+
`${args.serviceName}.json`
|
|
546
|
+
),
|
|
547
|
+
join(
|
|
548
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
549
|
+
"logs",
|
|
550
|
+
`${args.serviceName}.log`
|
|
551
|
+
),
|
|
552
|
+
join(
|
|
553
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
554
|
+
"logs",
|
|
555
|
+
`${args.serviceName}.err.log`
|
|
556
|
+
),
|
|
557
|
+
legacyAutosyncConfigPath(args.homeDir, args.serviceName, args.rootDir),
|
|
558
|
+
legacyAutosyncRuntimeStatePath(
|
|
559
|
+
args.homeDir,
|
|
560
|
+
args.serviceName,
|
|
561
|
+
args.rootDir
|
|
562
|
+
),
|
|
563
|
+
];
|
|
564
|
+
for (const candidate of legacyPaths) {
|
|
565
|
+
await rm(candidate, { force: true }).catch(() => null);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const AUTOSYNC_REBUILDABLE_PATHS = [
|
|
570
|
+
".facult/ai/index.json",
|
|
571
|
+
".facult/ai/graph.json",
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
const AUTOSYNC_MACHINE_LOCAL_LEGACY_PATHS = [
|
|
575
|
+
".facult/managed.json",
|
|
576
|
+
".facult/install.json",
|
|
577
|
+
".facult/autosync",
|
|
578
|
+
".facult/runtime",
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
async function gitListTrackedPaths(
|
|
582
|
+
repoDir: string,
|
|
583
|
+
pathValue: string
|
|
584
|
+
): Promise<string[]> {
|
|
585
|
+
const result = await runCommand(["git", "ls-files", "-z", "--", pathValue], {
|
|
586
|
+
cwd: repoDir,
|
|
587
|
+
});
|
|
588
|
+
if (result.exitCode !== 0 || !result.stdout) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
return result.stdout
|
|
592
|
+
.split("\0")
|
|
593
|
+
.map((value) => value.trim())
|
|
594
|
+
.filter(Boolean);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function cleanupAutosyncProtectedPaths(repoDir: string): Promise<void> {
|
|
598
|
+
const tracked = new Set<string>();
|
|
599
|
+
for (const pathValue of [
|
|
600
|
+
...AUTOSYNC_REBUILDABLE_PATHS,
|
|
601
|
+
...AUTOSYNC_MACHINE_LOCAL_LEGACY_PATHS,
|
|
602
|
+
]) {
|
|
603
|
+
for (const entry of await gitListTrackedPaths(repoDir, pathValue)) {
|
|
604
|
+
tracked.add(entry);
|
|
605
|
+
}
|
|
606
|
+
await rm(join(repoDir, pathValue), { force: true, recursive: true }).catch(
|
|
607
|
+
() => null
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (tracked.size > 0) {
|
|
612
|
+
await runCommand(
|
|
613
|
+
[
|
|
614
|
+
"git",
|
|
615
|
+
"restore",
|
|
616
|
+
"--staged",
|
|
617
|
+
"--worktree",
|
|
618
|
+
"--source=HEAD",
|
|
619
|
+
"--",
|
|
620
|
+
...tracked,
|
|
621
|
+
],
|
|
622
|
+
{ cwd: repoDir }
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
473
627
|
export async function runGitAutosyncOnce(args: {
|
|
474
628
|
config: AutosyncServiceConfig;
|
|
475
629
|
}): Promise<GitSyncOutcome> {
|
|
@@ -503,6 +657,8 @@ export async function runGitAutosyncOnce(args: {
|
|
|
503
657
|
};
|
|
504
658
|
}
|
|
505
659
|
|
|
660
|
+
await cleanupAutosyncProtectedPaths(repoDir);
|
|
661
|
+
|
|
506
662
|
const fetch = await runCommand(
|
|
507
663
|
["git", "fetch", config.git.remote, config.git.branch],
|
|
508
664
|
{ cwd: repoDir }
|
|
@@ -849,10 +1005,18 @@ export async function installAutosyncService(args: {
|
|
|
849
1005
|
await mkdir(dirname(spec.plistPath), { recursive: true });
|
|
850
1006
|
await mkdir(autosyncLogsDir(home, rootDir), { recursive: true });
|
|
851
1007
|
await saveAutosyncConfig(config, home);
|
|
1008
|
+
await cleanupLegacyAutosyncFiles({
|
|
1009
|
+
homeDir: home,
|
|
1010
|
+
serviceName,
|
|
1011
|
+
rootDir: config.rootDir,
|
|
1012
|
+
});
|
|
1013
|
+
await cleanupAutosyncLaunchAgentArtifacts({
|
|
1014
|
+
homeDir: home,
|
|
1015
|
+
serviceName,
|
|
1016
|
+
});
|
|
852
1017
|
await writeFile(spec.plistPath, plist, "utf8");
|
|
853
1018
|
|
|
854
1019
|
const domain = launchdDomain();
|
|
855
|
-
await runLaunchctl(["bootout", `${domain}/${spec.label}`]).catch(() => null);
|
|
856
1020
|
await runLaunchctl(["bootstrap", domain, spec.plistPath]);
|
|
857
1021
|
await runLaunchctl(["kickstart", "-k", `${domain}/${spec.label}`]);
|
|
858
1022
|
return config;
|
|
@@ -868,10 +1032,16 @@ export async function uninstallAutosyncService(args: {
|
|
|
868
1032
|
args.rootDir ??
|
|
869
1033
|
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
870
1034
|
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
871
|
-
const label = autosyncLabel(serviceName);
|
|
872
|
-
const domain = launchdDomain();
|
|
873
1035
|
|
|
874
|
-
await
|
|
1036
|
+
await cleanupAutosyncLaunchAgentArtifacts({
|
|
1037
|
+
homeDir: home,
|
|
1038
|
+
serviceName,
|
|
1039
|
+
});
|
|
1040
|
+
await cleanupLegacyAutosyncFiles({
|
|
1041
|
+
homeDir: home,
|
|
1042
|
+
serviceName,
|
|
1043
|
+
rootDir,
|
|
1044
|
+
});
|
|
875
1045
|
await rm(autosyncPlistPath(home, serviceName), { force: true });
|
|
876
1046
|
await rm(autosyncConfigPath(home, serviceName, rootDir), { force: true });
|
|
877
1047
|
}
|
|
@@ -883,6 +1053,7 @@ export async function repairAutosyncServices(
|
|
|
883
1053
|
const activeRoot = rootDir ?? facultRootDir(homeDir);
|
|
884
1054
|
const serviceDirs = [
|
|
885
1055
|
autosyncServicesDir(homeDir, activeRoot),
|
|
1056
|
+
join(canonicalAutosyncDir(homeDir, activeRoot), "services"),
|
|
886
1057
|
legacyAutosyncServicesDir(homeDir, activeRoot),
|
|
887
1058
|
];
|
|
888
1059
|
const seen = new Set<string>();
|
|
@@ -916,6 +1087,11 @@ export async function repairAutosyncServices(
|
|
|
916
1087
|
await saveAutosyncConfig(config, homeDir);
|
|
917
1088
|
changed = true;
|
|
918
1089
|
}
|
|
1090
|
+
await cleanupLegacyAutosyncFiles({
|
|
1091
|
+
homeDir,
|
|
1092
|
+
serviceName,
|
|
1093
|
+
rootDir: config.rootDir,
|
|
1094
|
+
});
|
|
919
1095
|
|
|
920
1096
|
const spec = buildLaunchAgentSpec({
|
|
921
1097
|
homeDir,
|
|
@@ -923,19 +1099,27 @@ export async function repairAutosyncServices(
|
|
|
923
1099
|
rootDir: config.rootDir,
|
|
924
1100
|
});
|
|
925
1101
|
const desired = buildLaunchAgentPlist(spec);
|
|
1102
|
+
const legacyPlistPath = join(
|
|
1103
|
+
homeDir,
|
|
1104
|
+
"Library",
|
|
1105
|
+
"LaunchAgents",
|
|
1106
|
+
`${legacyAutosyncLabel(serviceName)}.plist`
|
|
1107
|
+
);
|
|
926
1108
|
const currentText = await readFile(spec.plistPath, "utf8").catch(
|
|
927
1109
|
() => null
|
|
928
1110
|
);
|
|
929
|
-
|
|
1111
|
+
const legacyExists = await pathExists(legacyPlistPath);
|
|
1112
|
+
if (currentText !== desired || legacyExists) {
|
|
930
1113
|
await mkdir(dirname(spec.plistPath), { recursive: true });
|
|
931
1114
|
await mkdir(autosyncLogsDir(homeDir, config.rootDir), {
|
|
932
1115
|
recursive: true,
|
|
933
1116
|
});
|
|
1117
|
+
await cleanupAutosyncLaunchAgentArtifacts({
|
|
1118
|
+
homeDir,
|
|
1119
|
+
serviceName,
|
|
1120
|
+
});
|
|
934
1121
|
await writeFile(spec.plistPath, desired, "utf8");
|
|
935
1122
|
const domain = launchdDomain();
|
|
936
|
-
await runLaunchctl(["bootout", `${domain}/${spec.label}`]).catch(
|
|
937
|
-
() => null
|
|
938
|
-
);
|
|
939
1123
|
await runLaunchctl(["bootstrap", domain, spec.plistPath]).catch(
|
|
940
1124
|
() => null
|
|
941
1125
|
);
|
package/src/index-builder.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readdir } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { getAdapter } from "./adapters";
|
|
4
5
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
5
6
|
import {
|
|
6
7
|
type AssetScope,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
import {
|
|
15
16
|
facultAiGraphPath,
|
|
16
17
|
facultAiIndexPath,
|
|
17
|
-
|
|
18
|
+
facultMachineStateDir,
|
|
18
19
|
facultRootDir,
|
|
19
20
|
projectRootFromAiRoot,
|
|
20
21
|
projectSlugFromAiRoot,
|
|
@@ -31,6 +32,10 @@ interface AssetEntryBase {
|
|
|
31
32
|
shadow?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
function managedAgentFileExtension(tool: string): string {
|
|
36
|
+
return getAdapter(tool)?.agentFileExtension ?? ".toml";
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export interface SkillEntry {
|
|
35
40
|
name: string;
|
|
36
41
|
path: string;
|
|
@@ -987,7 +992,7 @@ async function readManagedState(
|
|
|
987
992
|
rootDir: string
|
|
988
993
|
): Promise<ManagedStateLite | null> {
|
|
989
994
|
const statePath = join(
|
|
990
|
-
|
|
995
|
+
facultMachineStateDir(homeDir, rootDir),
|
|
991
996
|
"managed.json"
|
|
992
997
|
);
|
|
993
998
|
try {
|
|
@@ -1213,6 +1218,7 @@ function registerManagedRenderedTargets(args: {
|
|
|
1213
1218
|
const nodes = args.graph.nodes;
|
|
1214
1219
|
for (const toolState of toolStates) {
|
|
1215
1220
|
if (toolState.agentsDir) {
|
|
1221
|
+
const extension = managedAgentFileExtension(toolState.tool);
|
|
1216
1222
|
for (const entry of Object.values(args.index.agents)) {
|
|
1217
1223
|
const sourceNodeId = sourceNodeIdForEntry({
|
|
1218
1224
|
kind: "agent",
|
|
@@ -1221,7 +1227,10 @@ function registerManagedRenderedTargets(args: {
|
|
|
1221
1227
|
if (!nodes[sourceNodeId]) {
|
|
1222
1228
|
continue;
|
|
1223
1229
|
}
|
|
1224
|
-
const targetPath = join(
|
|
1230
|
+
const targetPath = join(
|
|
1231
|
+
toolState.agentsDir,
|
|
1232
|
+
`${entry.name}${extension}`
|
|
1233
|
+
);
|
|
1225
1234
|
registerRenderedTargetNode({
|
|
1226
1235
|
graph: args.graph,
|
|
1227
1236
|
currentScope: args.currentScope,
|