facult 1.1.0 → 1.2.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 +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 +1870 -188
- 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/autosync.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { watch as fsWatch } from "node:fs";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
5
6
|
import { syncManagedTools } from "./manage";
|
|
6
|
-
import { facultRootDir, facultStateDir } from "./paths";
|
|
7
|
+
import { facultRootDir, facultStateDir, projectRootFromAiRoot } from "./paths";
|
|
7
8
|
|
|
8
9
|
const AUTOSYNC_VERSION = 1 as const;
|
|
9
10
|
const DEFAULT_DEBOUNCE_MS = 1500;
|
|
@@ -109,8 +110,30 @@ function autosyncLogsDir(home: string): string {
|
|
|
109
110
|
return join(autosyncDir(home), "logs");
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
function
|
|
113
|
-
|
|
113
|
+
function serviceSuffix(
|
|
114
|
+
rootDir: string | undefined,
|
|
115
|
+
home: string
|
|
116
|
+
): string | null {
|
|
117
|
+
if (!rootDir) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const projectRoot = projectRootFromAiRoot(rootDir, home);
|
|
121
|
+
if (!projectRoot) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const base = basename(projectRoot).trim().toLowerCase();
|
|
125
|
+
const slug = base.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
126
|
+
return slug || "project";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function autosyncServiceName(
|
|
130
|
+
tool?: string,
|
|
131
|
+
rootDir?: string,
|
|
132
|
+
home: string = homedir()
|
|
133
|
+
): string {
|
|
134
|
+
const base = tool?.trim() ? tool.trim() : "all";
|
|
135
|
+
const suffix = serviceSuffix(rootDir, home);
|
|
136
|
+
return suffix ? `${base}-${suffix}` : base;
|
|
114
137
|
}
|
|
115
138
|
|
|
116
139
|
function autosyncLabel(serviceName: string): string {
|
|
@@ -318,6 +341,7 @@ function defaultAutosyncConfig(args: {
|
|
|
318
341
|
serviceName: string;
|
|
319
342
|
tool?: string;
|
|
320
343
|
homeDir: string;
|
|
344
|
+
rootDir?: string;
|
|
321
345
|
remote?: string;
|
|
322
346
|
branch?: string;
|
|
323
347
|
intervalMinutes?: number;
|
|
@@ -328,7 +352,7 @@ function defaultAutosyncConfig(args: {
|
|
|
328
352
|
version: AUTOSYNC_VERSION,
|
|
329
353
|
name: args.serviceName,
|
|
330
354
|
tool: args.tool,
|
|
331
|
-
rootDir: facultRootDir(args.homeDir),
|
|
355
|
+
rootDir: args.rootDir ?? facultRootDir(args.homeDir),
|
|
332
356
|
debounceMs: DEFAULT_DEBOUNCE_MS,
|
|
333
357
|
git: {
|
|
334
358
|
enabled: args.gitEnabled ?? true,
|
|
@@ -715,6 +739,9 @@ Options:
|
|
|
715
739
|
--git-branch <name> Git branch for canonical repo sync (default: main)
|
|
716
740
|
--git-interval-minutes <n> Remote git sync interval in minutes (default: 60)
|
|
717
741
|
--git-disable Disable remote git sync for this service
|
|
742
|
+
--root <path> Select a canonical .ai root explicitly
|
|
743
|
+
--global Force the global canonical root
|
|
744
|
+
--project Force the nearest repo-local .ai root
|
|
718
745
|
--once Run one local+remote sync cycle and exit
|
|
719
746
|
`;
|
|
720
747
|
}
|
|
@@ -722,17 +749,22 @@ Options:
|
|
|
722
749
|
export async function installAutosyncService(args: {
|
|
723
750
|
tool?: string;
|
|
724
751
|
homeDir?: string;
|
|
752
|
+
rootDir?: string;
|
|
725
753
|
gitRemote?: string;
|
|
726
754
|
gitBranch?: string;
|
|
727
755
|
gitIntervalMinutes?: number;
|
|
728
756
|
gitEnabled?: boolean;
|
|
729
757
|
}): Promise<AutosyncServiceConfig> {
|
|
730
758
|
const home = args.homeDir ?? homedir();
|
|
731
|
-
const
|
|
759
|
+
const rootDir =
|
|
760
|
+
args.rootDir ??
|
|
761
|
+
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
762
|
+
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
732
763
|
const config = defaultAutosyncConfig({
|
|
733
764
|
serviceName,
|
|
734
765
|
tool: args.tool,
|
|
735
766
|
homeDir: home,
|
|
767
|
+
rootDir,
|
|
736
768
|
remote: args.gitRemote,
|
|
737
769
|
branch: args.gitBranch,
|
|
738
770
|
intervalMinutes: args.gitIntervalMinutes,
|
|
@@ -760,9 +792,13 @@ export async function installAutosyncService(args: {
|
|
|
760
792
|
export async function uninstallAutosyncService(args: {
|
|
761
793
|
tool?: string;
|
|
762
794
|
homeDir?: string;
|
|
795
|
+
rootDir?: string;
|
|
763
796
|
}): Promise<void> {
|
|
764
797
|
const home = args.homeDir ?? homedir();
|
|
765
|
-
const
|
|
798
|
+
const rootDir =
|
|
799
|
+
args.rootDir ??
|
|
800
|
+
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
801
|
+
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
766
802
|
const label = autosyncLabel(serviceName);
|
|
767
803
|
const domain = launchdDomain();
|
|
768
804
|
|
|
@@ -787,7 +823,9 @@ export async function repairAutosyncServices(
|
|
|
787
823
|
if (!config) {
|
|
788
824
|
continue;
|
|
789
825
|
}
|
|
790
|
-
const desiredRoot =
|
|
826
|
+
const desiredRoot = projectRootFromAiRoot(config.rootDir, homeDir)
|
|
827
|
+
? config.rootDir
|
|
828
|
+
: facultRootDir(homeDir);
|
|
791
829
|
if (config.rootDir !== desiredRoot) {
|
|
792
830
|
config.rootDir = desiredRoot;
|
|
793
831
|
await saveAutosyncConfig(config, homeDir);
|
|
@@ -827,9 +865,13 @@ export async function repairAutosyncServices(
|
|
|
827
865
|
export async function autosyncStatus(args: {
|
|
828
866
|
tool?: string;
|
|
829
867
|
homeDir?: string;
|
|
868
|
+
rootDir?: string;
|
|
830
869
|
}): Promise<AutosyncStatus> {
|
|
831
870
|
const home = args.homeDir ?? homedir();
|
|
832
|
-
const
|
|
871
|
+
const rootDir =
|
|
872
|
+
args.rootDir ??
|
|
873
|
+
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
874
|
+
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
833
875
|
const config = await loadAutosyncConfig(serviceName, home);
|
|
834
876
|
const state = await loadAutosyncRuntimeState(serviceName, home);
|
|
835
877
|
const plistPath = autosyncPlistPath(home, serviceName);
|
|
@@ -853,8 +895,13 @@ export async function autosyncStatus(args: {
|
|
|
853
895
|
|
|
854
896
|
export async function restartAutosyncService(args: {
|
|
855
897
|
tool?: string;
|
|
898
|
+
rootDir?: string;
|
|
856
899
|
}): Promise<void> {
|
|
857
|
-
const
|
|
900
|
+
const home = homedir();
|
|
901
|
+
const rootDir =
|
|
902
|
+
args.rootDir ??
|
|
903
|
+
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
904
|
+
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
858
905
|
const label = autosyncLabel(serviceName);
|
|
859
906
|
await runLaunchctl(["kickstart", "-k", `${launchdDomain()}/${label}`]);
|
|
860
907
|
}
|
|
@@ -867,21 +914,37 @@ export async function autosyncCommand(argv: string[]) {
|
|
|
867
914
|
}
|
|
868
915
|
|
|
869
916
|
try {
|
|
917
|
+
const parsed = parseCliContextArgs(rest);
|
|
918
|
+
if (
|
|
919
|
+
parsed.argv.includes("--help") ||
|
|
920
|
+
parsed.argv.includes("-h") ||
|
|
921
|
+
parsed.argv[0] === "help"
|
|
922
|
+
) {
|
|
923
|
+
console.log(autosyncHelp());
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const rootDir = resolveCliContextRoot({
|
|
927
|
+
rootArg: parsed.rootArg,
|
|
928
|
+
scope: parsed.scope,
|
|
929
|
+
cwd: process.cwd(),
|
|
930
|
+
});
|
|
931
|
+
|
|
870
932
|
if (sub === "install") {
|
|
871
|
-
const tool = parseAutosyncPositionals(
|
|
933
|
+
const tool = parseAutosyncPositionals(parsed.argv, [
|
|
872
934
|
"--git-remote",
|
|
873
935
|
"--git-branch",
|
|
874
936
|
"--git-interval-minutes",
|
|
875
937
|
])[0];
|
|
876
|
-
const gitRemote = parseAutosyncStringFlag(
|
|
877
|
-
const gitBranch = parseAutosyncStringFlag(
|
|
938
|
+
const gitRemote = parseAutosyncStringFlag(parsed.argv, "--git-remote");
|
|
939
|
+
const gitBranch = parseAutosyncStringFlag(parsed.argv, "--git-branch");
|
|
878
940
|
const gitIntervalMinutes = parseAutosyncIntFlag(
|
|
879
|
-
|
|
941
|
+
parsed.argv,
|
|
880
942
|
"--git-interval-minutes"
|
|
881
943
|
);
|
|
882
|
-
const gitEnabled = !
|
|
944
|
+
const gitEnabled = !parsed.argv.includes("--git-disable");
|
|
883
945
|
const config = await installAutosyncService({
|
|
884
946
|
tool,
|
|
947
|
+
rootDir,
|
|
885
948
|
gitRemote,
|
|
886
949
|
gitBranch,
|
|
887
950
|
gitIntervalMinutes,
|
|
@@ -893,16 +956,18 @@ export async function autosyncCommand(argv: string[]) {
|
|
|
893
956
|
}
|
|
894
957
|
|
|
895
958
|
if (sub === "uninstall") {
|
|
896
|
-
const tool = parseAutosyncPositionals(
|
|
897
|
-
await uninstallAutosyncService({ tool });
|
|
898
|
-
console.log(
|
|
959
|
+
const tool = parseAutosyncPositionals(parsed.argv, [])[0];
|
|
960
|
+
await uninstallAutosyncService({ tool, rootDir });
|
|
961
|
+
console.log(
|
|
962
|
+
`Removed autosync service: ${autosyncServiceName(tool, rootDir)}`
|
|
963
|
+
);
|
|
899
964
|
return;
|
|
900
965
|
}
|
|
901
966
|
|
|
902
967
|
if (sub === "status") {
|
|
903
|
-
const tool = parseAutosyncPositionals(
|
|
904
|
-
const status = await autosyncStatus({ tool });
|
|
905
|
-
console.log(`Service: ${autosyncServiceName(tool)}`);
|
|
968
|
+
const tool = parseAutosyncPositionals(parsed.argv, [])[0];
|
|
969
|
+
const status = await autosyncStatus({ tool, rootDir });
|
|
970
|
+
console.log(`Service: ${autosyncServiceName(tool, rootDir)}`);
|
|
906
971
|
console.log(`Plist: ${status.plistPath}`);
|
|
907
972
|
console.log(`Installed: ${status.plistExists ? "yes" : "no"}`);
|
|
908
973
|
console.log(`Loaded: ${status.loaded ? "yes" : "no"}`);
|
|
@@ -933,21 +998,25 @@ export async function autosyncCommand(argv: string[]) {
|
|
|
933
998
|
}
|
|
934
999
|
|
|
935
1000
|
if (sub === "restart") {
|
|
936
|
-
const tool = parseAutosyncPositionals(
|
|
937
|
-
await restartAutosyncService({ tool });
|
|
938
|
-
console.log(
|
|
1001
|
+
const tool = parseAutosyncPositionals(parsed.argv, [])[0];
|
|
1002
|
+
await restartAutosyncService({ tool, rootDir });
|
|
1003
|
+
console.log(
|
|
1004
|
+
`Restarted autosync service: ${autosyncServiceName(tool, rootDir)}`
|
|
1005
|
+
);
|
|
939
1006
|
return;
|
|
940
1007
|
}
|
|
941
1008
|
|
|
942
1009
|
if (sub === "run") {
|
|
943
|
-
const service = parseAutosyncStringFlag(
|
|
944
|
-
const tool = parseAutosyncPositionals(
|
|
945
|
-
const serviceName = service ?? autosyncServiceName(tool);
|
|
1010
|
+
const service = parseAutosyncStringFlag(parsed.argv, "--service");
|
|
1011
|
+
const tool = parseAutosyncPositionals(parsed.argv, ["--service"])[0];
|
|
1012
|
+
const serviceName = service ?? autosyncServiceName(tool, rootDir);
|
|
946
1013
|
const config = await loadAutosyncConfig(serviceName);
|
|
947
1014
|
if (!config) {
|
|
948
1015
|
throw new Error(`Autosync service not configured: ${serviceName}`);
|
|
949
1016
|
}
|
|
950
|
-
await runAutosyncService(config, {
|
|
1017
|
+
await runAutosyncService(config, {
|
|
1018
|
+
once: parsed.argv.includes("--once"),
|
|
1019
|
+
});
|
|
951
1020
|
return;
|
|
952
1021
|
}
|
|
953
1022
|
|
package/src/builtin.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function facultBuiltinPackRoot(
|
|
9
|
+
packName = "facult-operating-model"
|
|
10
|
+
): string {
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
return join(here, "..", "assets", "packs", packName);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function readTomlObject(
|
|
16
|
+
pathValue: string
|
|
17
|
+
): Promise<Record<string, unknown> | null> {
|
|
18
|
+
const file = Bun.file(pathValue);
|
|
19
|
+
if (!(await file.exists())) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const parsed = Bun.TOML.parse(await file.text());
|
|
23
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readBooleanConfig(
|
|
27
|
+
data: Record<string, unknown> | null,
|
|
28
|
+
key: string
|
|
29
|
+
): boolean | null {
|
|
30
|
+
if (!data) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const builtin = data.builtin;
|
|
34
|
+
if (!isPlainObject(builtin)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const value = builtin[key];
|
|
38
|
+
return typeof value === "boolean" ? value : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function builtinSyncDefaultsEnabled(
|
|
42
|
+
rootDir: string
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
const [tracked, local] = await Promise.all([
|
|
45
|
+
readTomlObject(join(rootDir, "config.toml")),
|
|
46
|
+
readTomlObject(join(rootDir, "config.local.toml")),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
for (const candidate of [tracked, local]) {
|
|
50
|
+
const direct = readBooleanConfig(candidate, "sync_defaults");
|
|
51
|
+
if (direct != null) {
|
|
52
|
+
return direct;
|
|
53
|
+
}
|
|
54
|
+
const legacy = readBooleanConfig(candidate, "sync_global_defaults");
|
|
55
|
+
if (legacy != null) {
|
|
56
|
+
return legacy;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { AssetSourceKind } from "./graph";
|
|
3
|
+
import {
|
|
4
|
+
facultContextRootDir,
|
|
5
|
+
facultRootDir,
|
|
6
|
+
findNearestProjectAiRoot,
|
|
7
|
+
projectRootFromAiRoot,
|
|
8
|
+
} from "./paths";
|
|
9
|
+
|
|
10
|
+
export type CapabilityScopeMode = "merged" | "global" | "project";
|
|
11
|
+
|
|
12
|
+
export interface ParsedCliContext {
|
|
13
|
+
argv: string[];
|
|
14
|
+
rootArg?: string;
|
|
15
|
+
scope: CapabilityScopeMode;
|
|
16
|
+
sourceKind?: AssetSourceKind;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function expandHomePath(pathValue: string, home: string): string {
|
|
20
|
+
if (pathValue === "~") {
|
|
21
|
+
return home;
|
|
22
|
+
}
|
|
23
|
+
if (pathValue.startsWith("~/")) {
|
|
24
|
+
return `${home}/${pathValue.slice(2)}`;
|
|
25
|
+
}
|
|
26
|
+
return pathValue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveRootArgument(pathValue: string, homeDir: string): string {
|
|
30
|
+
return resolve(expandHomePath(pathValue, homeDir));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseStringFlagValue(
|
|
34
|
+
arg: string,
|
|
35
|
+
nextArg: string | undefined,
|
|
36
|
+
flag: string
|
|
37
|
+
): { value: string; advance: number } | null {
|
|
38
|
+
if (arg === flag) {
|
|
39
|
+
if (!nextArg) {
|
|
40
|
+
throw new Error(`${flag} requires a value`);
|
|
41
|
+
}
|
|
42
|
+
return { value: nextArg, advance: 1 };
|
|
43
|
+
}
|
|
44
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
45
|
+
const value = arg.slice(flag.length + 1);
|
|
46
|
+
if (!value) {
|
|
47
|
+
throw new Error(`${flag} requires a value`);
|
|
48
|
+
}
|
|
49
|
+
return { value, advance: 0 };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseScopeValue(value: string): CapabilityScopeMode {
|
|
55
|
+
if (value === "merged" || value === "global" || value === "project") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`Unknown scope: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseSourceValue(value: string): AssetSourceKind {
|
|
62
|
+
if (value === "builtin" || value === "global" || value === "project") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Unknown source: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseCliContextArgs(
|
|
69
|
+
argv: string[],
|
|
70
|
+
opts?: { allowSource?: boolean; allowScope?: boolean }
|
|
71
|
+
): ParsedCliContext {
|
|
72
|
+
const rest: string[] = [];
|
|
73
|
+
let rootArg: string | undefined;
|
|
74
|
+
let scope: CapabilityScopeMode = "merged";
|
|
75
|
+
let sourceKind: AssetSourceKind | undefined;
|
|
76
|
+
let explicitRoot = false;
|
|
77
|
+
let explicitScope = false;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
const arg = argv[i];
|
|
81
|
+
if (!arg) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const root = parseStringFlagValue(arg, argv[i + 1], "--root");
|
|
86
|
+
if (root) {
|
|
87
|
+
if (explicitRoot) {
|
|
88
|
+
throw new Error("--root may only be provided once");
|
|
89
|
+
}
|
|
90
|
+
rootArg = root.value;
|
|
91
|
+
explicitRoot = true;
|
|
92
|
+
i += root.advance;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (arg === "--global") {
|
|
97
|
+
if (explicitScope && scope !== "global") {
|
|
98
|
+
throw new Error("Conflicting scope flags");
|
|
99
|
+
}
|
|
100
|
+
scope = "global";
|
|
101
|
+
explicitScope = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (arg === "--project") {
|
|
106
|
+
if (explicitScope && scope !== "project") {
|
|
107
|
+
throw new Error("Conflicting scope flags");
|
|
108
|
+
}
|
|
109
|
+
scope = "project";
|
|
110
|
+
explicitScope = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (opts?.allowScope !== false) {
|
|
115
|
+
const parsedScope = parseStringFlagValue(arg, argv[i + 1], "--scope");
|
|
116
|
+
if (parsedScope) {
|
|
117
|
+
const value = parseScopeValue(parsedScope.value);
|
|
118
|
+
if (explicitScope && scope !== value) {
|
|
119
|
+
throw new Error("Conflicting scope flags");
|
|
120
|
+
}
|
|
121
|
+
scope = value;
|
|
122
|
+
explicitScope = true;
|
|
123
|
+
i += parsedScope.advance;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (opts?.allowSource) {
|
|
129
|
+
const parsedSource = parseStringFlagValue(arg, argv[i + 1], "--source");
|
|
130
|
+
if (parsedSource) {
|
|
131
|
+
sourceKind = parseSourceValue(parsedSource.value);
|
|
132
|
+
i += parsedSource.advance;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
rest.push(arg);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
argv: rest,
|
|
142
|
+
rootArg,
|
|
143
|
+
scope,
|
|
144
|
+
sourceKind,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function coerceCanonicalRoot(pathValue: string, homeDir: string): string {
|
|
149
|
+
const resolved = resolveRootArgument(pathValue, homeDir);
|
|
150
|
+
const nearestProjectAi = findNearestProjectAiRoot(resolved);
|
|
151
|
+
if (nearestProjectAi) {
|
|
152
|
+
const projectRoot = projectRootFromAiRoot(nearestProjectAi, homeDir);
|
|
153
|
+
if (
|
|
154
|
+
resolved === nearestProjectAi ||
|
|
155
|
+
resolved === resolve(projectRoot ?? "")
|
|
156
|
+
) {
|
|
157
|
+
return nearestProjectAi;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return resolved;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveCliContextRoot(args?: {
|
|
164
|
+
homeDir?: string;
|
|
165
|
+
cwd?: string;
|
|
166
|
+
rootArg?: string;
|
|
167
|
+
scope?: CapabilityScopeMode;
|
|
168
|
+
}): string {
|
|
169
|
+
const homeDir = args?.homeDir ?? process.env.HOME ?? "";
|
|
170
|
+
const cwd = args?.cwd ?? process.cwd();
|
|
171
|
+
const scope = args?.scope ?? "merged";
|
|
172
|
+
|
|
173
|
+
if (args?.rootArg) {
|
|
174
|
+
const rootDir = coerceCanonicalRoot(args.rootArg, homeDir);
|
|
175
|
+
if (scope === "project" && !projectRootFromAiRoot(rootDir, homeDir)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Project scope requires a repo-local .ai root: ${rootDir}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return rootDir;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (scope === "global") {
|
|
184
|
+
return facultRootDir(homeDir);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (scope === "project") {
|
|
188
|
+
const projectRoot = findNearestProjectAiRoot(cwd);
|
|
189
|
+
if (!projectRoot) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"No project-local .ai root found from the current directory"
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return projectRoot;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return facultContextRootDir({ home: homeDir, cwd });
|
|
198
|
+
}
|
package/src/enable-disable.ts
CHANGED
package/src/global-docs.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { mkdir, readdir, rm } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { renderCanonicalText } from "./agents";
|
|
4
|
+
import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
|
|
5
|
+
import { projectRootFromAiRoot } from "./paths";
|
|
4
6
|
import { renderSnippetText } from "./snippets";
|
|
5
7
|
|
|
6
8
|
export interface GlobalDocPlan {
|
|
7
9
|
write: string[];
|
|
8
10
|
remove: string[];
|
|
9
11
|
contents: Map<string, string>;
|
|
12
|
+
sources: Map<string, string>;
|
|
10
13
|
managedTargets: string[];
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -14,6 +17,7 @@ export interface RulesPlan {
|
|
|
14
17
|
write: string[];
|
|
15
18
|
remove: string[];
|
|
16
19
|
contents: Map<string, string>;
|
|
20
|
+
sources: Map<string, string>;
|
|
17
21
|
managedRulesDir: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -22,6 +26,7 @@ export interface ToolConfigPlan {
|
|
|
22
26
|
write: boolean;
|
|
23
27
|
remove: boolean;
|
|
24
28
|
contents: string | null;
|
|
29
|
+
sourcePath?: string;
|
|
25
30
|
managedConfig: boolean;
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -30,6 +35,11 @@ interface SourceTarget {
|
|
|
30
35
|
targetPath: string;
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
interface GlobalDocTargetPaths {
|
|
39
|
+
primary: string;
|
|
40
|
+
override?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
const TOML_BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
34
44
|
|
|
35
45
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -151,30 +161,53 @@ async function listGlobalDocSources(args: {
|
|
|
151
161
|
toolHome: string;
|
|
152
162
|
}): Promise<SourceTarget[]> {
|
|
153
163
|
const { rootDir, tool, toolHome } = args;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
164
|
+
const targets = globalDocTargetPaths(tool, toolHome);
|
|
165
|
+
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(rootDir);
|
|
157
166
|
|
|
158
167
|
const candidates: SourceTarget[] = [];
|
|
159
168
|
const base = join(rootDir, "AGENTS.global.md");
|
|
160
169
|
if (await fileExists(base)) {
|
|
161
170
|
candidates.push({
|
|
162
171
|
sourcePath: base,
|
|
163
|
-
targetPath:
|
|
172
|
+
targetPath: targets.primary,
|
|
164
173
|
});
|
|
174
|
+
} else if (useBuiltinDefaults) {
|
|
175
|
+
const builtinBase = join(facultBuiltinPackRoot(), "AGENTS.global.md");
|
|
176
|
+
if (await fileExists(builtinBase)) {
|
|
177
|
+
candidates.push({
|
|
178
|
+
sourcePath: builtinBase,
|
|
179
|
+
targetPath: targets.primary,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
const override = join(rootDir, "AGENTS.override.global.md");
|
|
168
|
-
if (await fileExists(override)) {
|
|
185
|
+
if (targets.override && (await fileExists(override))) {
|
|
169
186
|
candidates.push({
|
|
170
187
|
sourcePath: override,
|
|
171
|
-
targetPath:
|
|
188
|
+
targetPath: targets.override,
|
|
172
189
|
});
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
return candidates;
|
|
176
193
|
}
|
|
177
194
|
|
|
195
|
+
export function globalDocTargetPaths(
|
|
196
|
+
tool: string,
|
|
197
|
+
toolHome: string
|
|
198
|
+
): GlobalDocTargetPaths {
|
|
199
|
+
if (tool === "claude") {
|
|
200
|
+
return {
|
|
201
|
+
primary: join(toolHome, "CLAUDE.md"),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
primary: join(toolHome, "AGENTS.md"),
|
|
207
|
+
override: join(toolHome, "AGENTS.override.md"),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
178
211
|
async function renderSourceTarget(args: {
|
|
179
212
|
homeDir: string;
|
|
180
213
|
rootDir: string;
|
|
@@ -194,6 +227,7 @@ async function renderSourceTarget(args: {
|
|
|
194
227
|
return await renderCanonicalText(withSnippets.text, {
|
|
195
228
|
homeDir: args.homeDir,
|
|
196
229
|
rootDir: args.rootDir,
|
|
230
|
+
projectRoot: projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
|
|
197
231
|
targetTool: args.tool,
|
|
198
232
|
targetPath: args.targetPath,
|
|
199
233
|
});
|
|
@@ -208,6 +242,7 @@ export async function planToolGlobalDocsSync(args: {
|
|
|
208
242
|
}): Promise<GlobalDocPlan> {
|
|
209
243
|
const docs = await listGlobalDocSources(args);
|
|
210
244
|
const contents = new Map<string, string>();
|
|
245
|
+
const sources = new Map<string, string>();
|
|
211
246
|
const managedTargets = docs.map((doc) => doc.targetPath).sort();
|
|
212
247
|
|
|
213
248
|
for (const doc of docs) {
|
|
@@ -219,6 +254,7 @@ export async function planToolGlobalDocsSync(args: {
|
|
|
219
254
|
tool: args.tool,
|
|
220
255
|
});
|
|
221
256
|
contents.set(doc.targetPath, rendered);
|
|
257
|
+
sources.set(doc.targetPath, doc.sourcePath);
|
|
222
258
|
}
|
|
223
259
|
|
|
224
260
|
const write: string[] = [];
|
|
@@ -238,6 +274,7 @@ export async function planToolGlobalDocsSync(args: {
|
|
|
238
274
|
write: write.sort(),
|
|
239
275
|
remove,
|
|
240
276
|
contents,
|
|
277
|
+
sources,
|
|
241
278
|
managedTargets,
|
|
242
279
|
};
|
|
243
280
|
}
|
|
@@ -301,6 +338,7 @@ export async function planToolRulesSync(args: {
|
|
|
301
338
|
}): Promise<RulesPlan> {
|
|
302
339
|
const rules = await listToolRules(args);
|
|
303
340
|
const contents = new Map<string, string>();
|
|
341
|
+
const sources = new Map<string, string>();
|
|
304
342
|
|
|
305
343
|
for (const rule of rules) {
|
|
306
344
|
const targetPath = join(args.rulesDir, rule.targetPath);
|
|
@@ -308,10 +346,13 @@ export async function planToolRulesSync(args: {
|
|
|
308
346
|
const rendered = await renderCanonicalText(raw, {
|
|
309
347
|
homeDir: args.homeDir,
|
|
310
348
|
rootDir: args.rootDir,
|
|
349
|
+
projectRoot:
|
|
350
|
+
projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
|
|
311
351
|
targetTool: args.tool,
|
|
312
352
|
targetPath,
|
|
313
353
|
});
|
|
314
354
|
contents.set(targetPath, rendered);
|
|
355
|
+
sources.set(targetPath, rule.sourcePath);
|
|
315
356
|
}
|
|
316
357
|
|
|
317
358
|
const write: string[] = [];
|
|
@@ -342,6 +383,7 @@ export async function planToolRulesSync(args: {
|
|
|
342
383
|
write: write.sort(),
|
|
343
384
|
remove: remove.sort(),
|
|
344
385
|
contents,
|
|
386
|
+
sources,
|
|
345
387
|
managedRulesDir: rules.length > 0,
|
|
346
388
|
};
|
|
347
389
|
}
|
|
@@ -390,6 +432,7 @@ export async function planToolConfigSync(args: {
|
|
|
390
432
|
write: false,
|
|
391
433
|
remove: false,
|
|
392
434
|
contents: null,
|
|
435
|
+
sourcePath,
|
|
393
436
|
managedConfig: false,
|
|
394
437
|
};
|
|
395
438
|
}
|
|
@@ -419,6 +462,7 @@ export async function planToolConfigSync(args: {
|
|
|
419
462
|
write: current !== `${nextContents}\n`,
|
|
420
463
|
remove: false,
|
|
421
464
|
contents: nextContents,
|
|
465
|
+
sourcePath,
|
|
422
466
|
managedConfig: true,
|
|
423
467
|
};
|
|
424
468
|
}
|