facult 2.7.0 → 2.7.2
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 +141 -337
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -1
- package/src/ai-state.ts +131 -1
- package/src/builtin.ts +7 -1
- package/src/doctor.ts +327 -0
- package/src/global-docs.ts +43 -2
- package/src/index-builder.ts +79 -8
- package/src/index.ts +60 -53
- package/src/manage.ts +880 -37
- package/src/project-sync.ts +288 -0
package/package.json
CHANGED
package/src/adapters/codex.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const codexAdapter: ToolAdapter = {
|
|
|
10
10
|
detectVersion: detectExplicitVersion,
|
|
11
11
|
getDefaultPaths: () => ({
|
|
12
12
|
mcp: "~/.codex/mcp.json",
|
|
13
|
-
skills: "~/.codex/skills",
|
|
13
|
+
skills: ["~/.agents/skills", "~/.codex/skills"],
|
|
14
14
|
agents: "~/.codex/agents",
|
|
15
15
|
config: "~/.config/openai/codex.json",
|
|
16
16
|
}),
|
package/src/ai-state.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { buildIndex } from "./index-builder";
|
|
4
5
|
import {
|
|
5
6
|
facultAiGraphPath,
|
|
6
7
|
facultAiIndexPath,
|
|
7
8
|
legacyFacultStateDirForRoot,
|
|
9
|
+
preferredGlobalAiRoot,
|
|
10
|
+
projectRootFromAiRoot,
|
|
8
11
|
} from "./paths";
|
|
9
12
|
|
|
10
13
|
async function fileExists(path: string): Promise<boolean> {
|
|
@@ -15,6 +18,99 @@ async function fileExists(path: string): Promise<boolean> {
|
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
async function newestPathMtime(path: string): Promise<number> {
|
|
22
|
+
try {
|
|
23
|
+
const st = await stat(path);
|
|
24
|
+
if (st.isFile()) {
|
|
25
|
+
return st.mtimeMs;
|
|
26
|
+
}
|
|
27
|
+
if (!st.isDirectory()) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let newest = st.mtimeMs;
|
|
31
|
+
let entries: Dirent<string>[] = [];
|
|
32
|
+
try {
|
|
33
|
+
entries = await readdir(path, { withFileTypes: true, encoding: "utf8" });
|
|
34
|
+
} catch {
|
|
35
|
+
return newest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const child = join(path, entry.name);
|
|
40
|
+
if (entry.isFile()) {
|
|
41
|
+
try {
|
|
42
|
+
const childStat = await stat(child);
|
|
43
|
+
newest = Math.max(newest, childStat.mtimeMs);
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore unreadable children
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
newest = Math.max(newest, await newestPathMtime(child));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return newest;
|
|
54
|
+
} catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function watchedPathMtime(path: string): Promise<number> {
|
|
60
|
+
const newest = await newestPathMtime(path);
|
|
61
|
+
if (newest > 0) {
|
|
62
|
+
return newest;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return (await stat(dirname(path))).mtimeMs;
|
|
66
|
+
} catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function canonicalAssetsNewerThanIndex(args: {
|
|
72
|
+
homeDir: string;
|
|
73
|
+
rootDir: string;
|
|
74
|
+
indexPath: string;
|
|
75
|
+
}): Promise<boolean> {
|
|
76
|
+
let indexMtimeMs = 0;
|
|
77
|
+
try {
|
|
78
|
+
indexMtimeMs = (await stat(args.indexPath)).mtimeMs;
|
|
79
|
+
} catch {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const watchedRelPaths = [
|
|
84
|
+
"AGENTS.global.md",
|
|
85
|
+
"AGENTS.override.global.md",
|
|
86
|
+
"agents",
|
|
87
|
+
"config.toml",
|
|
88
|
+
"instructions",
|
|
89
|
+
"mcp",
|
|
90
|
+
"skills",
|
|
91
|
+
"snippets",
|
|
92
|
+
"tools",
|
|
93
|
+
];
|
|
94
|
+
const watchedRoots = [args.rootDir];
|
|
95
|
+
|
|
96
|
+
if (projectRootFromAiRoot(args.rootDir, args.homeDir)) {
|
|
97
|
+
const globalRoot = preferredGlobalAiRoot(args.homeDir);
|
|
98
|
+
if (globalRoot !== args.rootDir) {
|
|
99
|
+
watchedRoots.push(globalRoot);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const root of watchedRoots) {
|
|
104
|
+
for (const rel of watchedRelPaths) {
|
|
105
|
+
if ((await watchedPathMtime(join(root, rel))) > indexMtimeMs) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
18
114
|
export function legacyAiIndexPath(rootDir: string): string {
|
|
19
115
|
return join(rootDir, "index.json");
|
|
20
116
|
}
|
|
@@ -46,6 +142,21 @@ export async function ensureAiIndexPath(args: {
|
|
|
46
142
|
}> {
|
|
47
143
|
const generatedPath = facultAiIndexPath(args.homeDir, args.rootDir);
|
|
48
144
|
if (await fileExists(generatedPath)) {
|
|
145
|
+
if (
|
|
146
|
+
args.repair !== false &&
|
|
147
|
+
(await canonicalAssetsNewerThanIndex({
|
|
148
|
+
homeDir: args.homeDir,
|
|
149
|
+
rootDir: args.rootDir,
|
|
150
|
+
indexPath: generatedPath,
|
|
151
|
+
}))
|
|
152
|
+
) {
|
|
153
|
+
const { outputPath } = await buildIndex({
|
|
154
|
+
rootDir: args.rootDir,
|
|
155
|
+
homeDir: args.homeDir,
|
|
156
|
+
force: false,
|
|
157
|
+
});
|
|
158
|
+
return { path: outputPath, repaired: true, source: "rebuilt" };
|
|
159
|
+
}
|
|
49
160
|
return { path: generatedPath, repaired: false, source: "generated" };
|
|
50
161
|
}
|
|
51
162
|
|
|
@@ -100,6 +211,25 @@ export async function ensureAiGraphPath(args: {
|
|
|
100
211
|
}> {
|
|
101
212
|
const generatedPath = facultAiGraphPath(args.homeDir, args.rootDir);
|
|
102
213
|
if (await fileExists(generatedPath)) {
|
|
214
|
+
const generatedIndexPath = facultAiIndexPath(args.homeDir, args.rootDir);
|
|
215
|
+
const freshnessAnchor = (await fileExists(generatedIndexPath))
|
|
216
|
+
? generatedIndexPath
|
|
217
|
+
: generatedPath;
|
|
218
|
+
if (
|
|
219
|
+
args.repair !== false &&
|
|
220
|
+
(await canonicalAssetsNewerThanIndex({
|
|
221
|
+
homeDir: args.homeDir,
|
|
222
|
+
rootDir: args.rootDir,
|
|
223
|
+
indexPath: freshnessAnchor,
|
|
224
|
+
}))
|
|
225
|
+
) {
|
|
226
|
+
const { graphPath } = await buildIndex({
|
|
227
|
+
rootDir: args.rootDir,
|
|
228
|
+
homeDir: args.homeDir,
|
|
229
|
+
force: false,
|
|
230
|
+
});
|
|
231
|
+
return { path: graphPath, rebuilt: true };
|
|
232
|
+
}
|
|
103
233
|
return { path: generatedPath, rebuilt: false };
|
|
104
234
|
}
|
|
105
235
|
|
package/src/builtin.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { projectRootFromAiRoot } from "./paths";
|
|
3
4
|
|
|
4
5
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
5
6
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -39,7 +40,8 @@ function readBooleanConfig(
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export async function builtinSyncDefaultsEnabled(
|
|
42
|
-
rootDir: string
|
|
43
|
+
rootDir: string,
|
|
44
|
+
homeDir?: string
|
|
43
45
|
): Promise<boolean> {
|
|
44
46
|
const [tracked, local] = await Promise.all([
|
|
45
47
|
readTomlObject(join(rootDir, "config.toml")),
|
|
@@ -57,5 +59,9 @@ export async function builtinSyncDefaultsEnabled(
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
if (projectRootFromAiRoot(rootDir, homeDir) != null) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
60
66
|
return true;
|
|
61
67
|
}
|
package/src/doctor.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from "./ai-state";
|
|
23
23
|
import { repairAutosyncServices } from "./autosync";
|
|
24
24
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
25
|
+
import { loadManagedState } from "./manage";
|
|
26
|
+
import { extractServersObject } from "./mcp-config";
|
|
25
27
|
import {
|
|
26
28
|
facultAiGraphPath,
|
|
27
29
|
facultAiIndexPath,
|
|
@@ -30,7 +32,14 @@ import {
|
|
|
30
32
|
facultStateDir,
|
|
31
33
|
legacyExternalFacultStateDir,
|
|
32
34
|
legacyFacultStateDirForRoot,
|
|
35
|
+
projectRootFromAiRoot,
|
|
33
36
|
} from "./paths";
|
|
37
|
+
import {
|
|
38
|
+
loadConfiguredProjectSyncTools,
|
|
39
|
+
writeProjectSyncPolicy,
|
|
40
|
+
} from "./project-sync";
|
|
41
|
+
|
|
42
|
+
const TOML_FILE_SUFFIX_RE = /\.toml$/;
|
|
34
43
|
|
|
35
44
|
function legacyDefaultRoot(home: string): string {
|
|
36
45
|
return join(home, "agents", ".facult");
|
|
@@ -223,6 +232,275 @@ async function repairLegacyState(args: {
|
|
|
223
232
|
return { changed, conflicts };
|
|
224
233
|
}
|
|
225
234
|
|
|
235
|
+
function normalizeCodexMarketplaceText(text: string): string {
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(text) as unknown;
|
|
238
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
239
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
240
|
+
}
|
|
241
|
+
const plugins = Array.isArray((parsed as { plugins?: unknown[] }).plugins)
|
|
242
|
+
? ((parsed as { plugins: unknown[] }).plugins ?? [])
|
|
243
|
+
: null;
|
|
244
|
+
if (plugins) {
|
|
245
|
+
(parsed as { plugins: unknown[] }).plugins = plugins.map((entry) => {
|
|
246
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
247
|
+
return entry;
|
|
248
|
+
}
|
|
249
|
+
const source =
|
|
250
|
+
"source" in entry &&
|
|
251
|
+
(entry as { source?: unknown }).source &&
|
|
252
|
+
typeof (entry as { source?: unknown }).source === "object" &&
|
|
253
|
+
!Array.isArray((entry as { source?: unknown }).source)
|
|
254
|
+
? {
|
|
255
|
+
...((entry as { source: Record<string, unknown> }).source ??
|
|
256
|
+
{}),
|
|
257
|
+
}
|
|
258
|
+
: null;
|
|
259
|
+
if (
|
|
260
|
+
source?.source === "local" &&
|
|
261
|
+
typeof source.path === "string" &&
|
|
262
|
+
source.path.startsWith("./.codex/plugins/")
|
|
263
|
+
) {
|
|
264
|
+
source.path = source.path.replace("./.codex/plugins/", "./plugins/");
|
|
265
|
+
}
|
|
266
|
+
return source
|
|
267
|
+
? { ...(entry as Record<string, unknown>), source }
|
|
268
|
+
: entry;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return `${JSON.stringify(parsed, null, 2)}\n`;
|
|
272
|
+
} catch {
|
|
273
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function repairLegacyCodexAuthoringLayout(args: {
|
|
278
|
+
home: string;
|
|
279
|
+
rootDir: string;
|
|
280
|
+
}): Promise<{ changed: boolean; conflicts: string[] }> {
|
|
281
|
+
const liveRoot = projectRootFromAiRoot(args.rootDir, args.home) ?? args.home;
|
|
282
|
+
const legacySkillsDir = join(liveRoot, ".codex", "skills");
|
|
283
|
+
const preferredSkillsDir = join(liveRoot, ".agents", "skills");
|
|
284
|
+
const legacyPluginsDir = join(liveRoot, ".codex", "plugins");
|
|
285
|
+
const preferredPluginsDir = join(liveRoot, "plugins");
|
|
286
|
+
const marketplacePath = join(
|
|
287
|
+
liveRoot,
|
|
288
|
+
".agents",
|
|
289
|
+
"plugins",
|
|
290
|
+
"marketplace.json"
|
|
291
|
+
);
|
|
292
|
+
const conflicts: string[] = [];
|
|
293
|
+
let changed = false;
|
|
294
|
+
|
|
295
|
+
if (await moveMissingTree(legacySkillsDir, preferredSkillsDir, conflicts)) {
|
|
296
|
+
changed = true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (await moveMissingTree(legacyPluginsDir, preferredPluginsDir, conflicts)) {
|
|
300
|
+
changed = true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const current = await readFile(marketplacePath, "utf8");
|
|
305
|
+
const normalized = normalizeCodexMarketplaceText(current);
|
|
306
|
+
if (normalized !== current) {
|
|
307
|
+
await mkdir(dirname(marketplacePath), { recursive: true });
|
|
308
|
+
await writeFile(marketplacePath, normalized, "utf8");
|
|
309
|
+
changed = true;
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// Ignore missing or unreadable marketplace files.
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { changed, conflicts };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function listProjectSkillNames(rootDir: string): Promise<string[]> {
|
|
319
|
+
const skillsDir = join(rootDir, "skills");
|
|
320
|
+
const entries = await readdir(skillsDir, { withFileTypes: true }).catch(
|
|
321
|
+
() => [] as import("node:fs").Dirent[]
|
|
322
|
+
);
|
|
323
|
+
return entries
|
|
324
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
325
|
+
.map((entry) => entry.name)
|
|
326
|
+
.sort((a, b) => a.localeCompare(b));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function listProjectAgentNames(rootDir: string): Promise<string[]> {
|
|
330
|
+
const agentsDir = join(rootDir, "agents");
|
|
331
|
+
const entries = await readdir(agentsDir, { withFileTypes: true }).catch(
|
|
332
|
+
() => [] as import("node:fs").Dirent[]
|
|
333
|
+
);
|
|
334
|
+
return entries
|
|
335
|
+
.flatMap((entry) => {
|
|
336
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
337
|
+
return [entry.name];
|
|
338
|
+
}
|
|
339
|
+
if (entry.isFile() && entry.name.endsWith(".toml")) {
|
|
340
|
+
return [entry.name.replace(TOML_FILE_SUFFIX_RE, "")];
|
|
341
|
+
}
|
|
342
|
+
return [];
|
|
343
|
+
})
|
|
344
|
+
.sort((a, b) => a.localeCompare(b));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function listProjectMcpNames(rootDir: string): Promise<string[]> {
|
|
348
|
+
const trackedPaths = [
|
|
349
|
+
join(rootDir, "mcp", "servers.json"),
|
|
350
|
+
join(rootDir, "mcp", "mcp.json"),
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
for (const candidate of trackedPaths) {
|
|
354
|
+
try {
|
|
355
|
+
const raw = await Bun.file(candidate).text();
|
|
356
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
357
|
+
const servers = extractServersObject(parsed) ?? {};
|
|
358
|
+
return Object.keys(servers).sort((a, b) => a.localeCompare(b));
|
|
359
|
+
} catch {
|
|
360
|
+
// Try next candidate.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function hasProjectGlobalDocs(rootDir: string): Promise<boolean> {
|
|
368
|
+
return (
|
|
369
|
+
(await pathExists(join(rootDir, "AGENTS.global.md"))) ||
|
|
370
|
+
(await pathExists(join(rootDir, "AGENTS.override.global.md")))
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function hasProjectToolRules(
|
|
375
|
+
rootDir: string,
|
|
376
|
+
tool: string
|
|
377
|
+
): Promise<boolean> {
|
|
378
|
+
const rulesDir = join(rootDir, "tools", tool, "rules");
|
|
379
|
+
const entries = await readdir(rulesDir, { withFileTypes: true }).catch(
|
|
380
|
+
() => [] as import("node:fs").Dirent[]
|
|
381
|
+
);
|
|
382
|
+
return entries.some(
|
|
383
|
+
(entry) => entry.isFile() && entry.name.endsWith(".rules")
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function hasProjectToolConfig(
|
|
388
|
+
rootDir: string,
|
|
389
|
+
tool: string
|
|
390
|
+
): Promise<boolean> {
|
|
391
|
+
return (
|
|
392
|
+
(await pathExists(join(rootDir, "tools", tool, "config.toml"))) ||
|
|
393
|
+
(await pathExists(join(rootDir, "tools", tool, "config.local.toml")))
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function planProjectSyncPolicyRepair(args: {
|
|
398
|
+
home: string;
|
|
399
|
+
rootDir: string;
|
|
400
|
+
}): Promise<{
|
|
401
|
+
needed: boolean;
|
|
402
|
+
toolPolicies: Record<
|
|
403
|
+
string,
|
|
404
|
+
{
|
|
405
|
+
skills?: string[];
|
|
406
|
+
agents?: string[];
|
|
407
|
+
mcpServers?: string[];
|
|
408
|
+
globalDocs?: boolean;
|
|
409
|
+
toolRules?: boolean;
|
|
410
|
+
toolConfig?: boolean;
|
|
411
|
+
}
|
|
412
|
+
>;
|
|
413
|
+
}> {
|
|
414
|
+
if (projectRootFromAiRoot(args.rootDir, args.home) == null) {
|
|
415
|
+
return { needed: false, toolPolicies: {} };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const managedState = await loadManagedState(args.home, args.rootDir);
|
|
419
|
+
const managedTools = Object.keys(managedState.tools).sort((a, b) =>
|
|
420
|
+
a.localeCompare(b)
|
|
421
|
+
);
|
|
422
|
+
if (managedTools.length === 0) {
|
|
423
|
+
return { needed: false, toolPolicies: {} };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const configuredTools = new Set(
|
|
427
|
+
await loadConfiguredProjectSyncTools({ rootDir: args.rootDir })
|
|
428
|
+
);
|
|
429
|
+
const [skills, agents, mcpServers, globalDocs] = await Promise.all([
|
|
430
|
+
listProjectSkillNames(args.rootDir),
|
|
431
|
+
listProjectAgentNames(args.rootDir),
|
|
432
|
+
listProjectMcpNames(args.rootDir),
|
|
433
|
+
hasProjectGlobalDocs(args.rootDir),
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
const toolPolicies: Record<
|
|
437
|
+
string,
|
|
438
|
+
{
|
|
439
|
+
skills?: string[];
|
|
440
|
+
agents?: string[];
|
|
441
|
+
mcpServers?: string[];
|
|
442
|
+
globalDocs?: boolean;
|
|
443
|
+
toolRules?: boolean;
|
|
444
|
+
toolConfig?: boolean;
|
|
445
|
+
}
|
|
446
|
+
> = {};
|
|
447
|
+
|
|
448
|
+
for (const tool of managedTools) {
|
|
449
|
+
if (configuredTools.has(tool)) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const [toolRules, toolConfig] = await Promise.all([
|
|
453
|
+
hasProjectToolRules(args.rootDir, tool),
|
|
454
|
+
hasProjectToolConfig(args.rootDir, tool),
|
|
455
|
+
]);
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
skills.length === 0 &&
|
|
459
|
+
agents.length === 0 &&
|
|
460
|
+
mcpServers.length === 0 &&
|
|
461
|
+
!globalDocs &&
|
|
462
|
+
!toolRules &&
|
|
463
|
+
!toolConfig
|
|
464
|
+
) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
toolPolicies[tool] = {
|
|
469
|
+
...(skills.length > 0 ? { skills } : {}),
|
|
470
|
+
...(agents.length > 0 ? { agents } : {}),
|
|
471
|
+
...(mcpServers.length > 0 ? { mcpServers } : {}),
|
|
472
|
+
...(globalDocs ? { globalDocs: true } : {}),
|
|
473
|
+
...(toolRules ? { toolRules: true } : {}),
|
|
474
|
+
...(toolConfig ? { toolConfig: true } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
needed: Object.keys(toolPolicies).length > 0,
|
|
480
|
+
toolPolicies,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function repairProjectSyncPolicy(args: {
|
|
485
|
+
home: string;
|
|
486
|
+
rootDir: string;
|
|
487
|
+
}): Promise<{ changed: boolean; path?: string; tools: string[] }> {
|
|
488
|
+
const plan = await planProjectSyncPolicyRepair(args);
|
|
489
|
+
if (!plan.needed) {
|
|
490
|
+
return { changed: false, tools: [] };
|
|
491
|
+
}
|
|
492
|
+
const result = await writeProjectSyncPolicy({
|
|
493
|
+
rootDir: args.rootDir,
|
|
494
|
+
toolPolicies: plan.toolPolicies,
|
|
495
|
+
targetFile: "config.local.toml",
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
changed: result.changed,
|
|
499
|
+
path: result.path,
|
|
500
|
+
tools: Object.keys(plan.toolPolicies).sort((a, b) => a.localeCompare(b)),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
226
504
|
function printHelp() {
|
|
227
505
|
console.log(`fclt doctor — inspect and repair local fclt state
|
|
228
506
|
|
|
@@ -258,6 +536,12 @@ export async function doctorCommand(argv: string[]) {
|
|
|
258
536
|
let stateRepaired = false;
|
|
259
537
|
let stateConflicts: string[] = [];
|
|
260
538
|
let autosyncRepaired = false;
|
|
539
|
+
let codexAuthoringRepaired = false;
|
|
540
|
+
let codexAuthoringConflicts: string[] = [];
|
|
541
|
+
let projectSyncRepairNeeded = false;
|
|
542
|
+
let projectSyncRepaired = false;
|
|
543
|
+
let projectSyncRepairTools: string[] = [];
|
|
544
|
+
let projectSyncRepairPath: string | undefined;
|
|
261
545
|
if (repair) {
|
|
262
546
|
rootConfigRepaired = await repairLegacyRootConfig(home);
|
|
263
547
|
}
|
|
@@ -266,6 +550,28 @@ export async function doctorCommand(argv: string[]) {
|
|
|
266
550
|
stateRepaired = stateRepair.changed;
|
|
267
551
|
stateConflicts = stateRepair.conflicts;
|
|
268
552
|
autosyncRepaired = await repairAutosyncServices(home, rootDir);
|
|
553
|
+
const authoringRepair = await repairLegacyCodexAuthoringLayout({
|
|
554
|
+
home,
|
|
555
|
+
rootDir,
|
|
556
|
+
});
|
|
557
|
+
codexAuthoringRepaired = authoringRepair.changed;
|
|
558
|
+
codexAuthoringConflicts = authoringRepair.conflicts;
|
|
559
|
+
const projectSyncRepair = await repairProjectSyncPolicy({
|
|
560
|
+
home,
|
|
561
|
+
rootDir,
|
|
562
|
+
});
|
|
563
|
+
projectSyncRepaired = projectSyncRepair.changed;
|
|
564
|
+
projectSyncRepairTools = projectSyncRepair.tools;
|
|
565
|
+
projectSyncRepairPath = projectSyncRepair.path;
|
|
566
|
+
} else {
|
|
567
|
+
const projectSyncPlan = await planProjectSyncPolicyRepair({
|
|
568
|
+
home,
|
|
569
|
+
rootDir,
|
|
570
|
+
});
|
|
571
|
+
projectSyncRepairNeeded = projectSyncPlan.needed;
|
|
572
|
+
projectSyncRepairTools = Object.keys(projectSyncPlan.toolPolicies).sort(
|
|
573
|
+
(a, b) => a.localeCompare(b)
|
|
574
|
+
);
|
|
269
575
|
}
|
|
270
576
|
const generated = facultAiIndexPath(home, rootDir);
|
|
271
577
|
const generatedGraph = facultAiGraphPath(home, rootDir);
|
|
@@ -300,6 +606,27 @@ export async function doctorCommand(argv: string[]) {
|
|
|
300
606
|
if (autosyncRepaired) {
|
|
301
607
|
console.log("Repaired autosync launch agent configuration.");
|
|
302
608
|
}
|
|
609
|
+
if (codexAuthoringRepaired) {
|
|
610
|
+
console.log(
|
|
611
|
+
"Migrated legacy Codex authoring paths to .agents/skills, .agents/plugins/marketplace.json, and plugins/."
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (codexAuthoringConflicts.length) {
|
|
615
|
+
console.log("Skipped conflicting Codex authoring paths:");
|
|
616
|
+
for (const conflict of codexAuthoringConflicts) {
|
|
617
|
+
console.log(`- ${conflict}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (projectSyncRepaired && projectSyncRepairPath) {
|
|
621
|
+
console.log(
|
|
622
|
+
`Materialized explicit project sync policy in ${projectSyncRepairPath} for: ${projectSyncRepairTools.join(", ")}`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if (!repair && projectSyncRepairNeeded) {
|
|
626
|
+
console.log(
|
|
627
|
+
`Project sync is still implicit for managed tools (${projectSyncRepairTools.join(", ")}). Run \`fclt doctor --repair\` to write explicit [project_sync.<tool>] entries.`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
303
630
|
|
|
304
631
|
if (result.source === "generated") {
|
|
305
632
|
console.log("AI index is healthy.");
|
package/src/global-docs.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { renderCanonicalText } from "./agents";
|
|
4
4
|
import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
|
|
5
5
|
import { projectRootFromAiRoot } from "./paths";
|
|
6
|
+
import { projectSyncAllowsToolSurface } from "./project-sync";
|
|
6
7
|
import { renderSnippetText } from "./snippets";
|
|
7
8
|
|
|
8
9
|
export interface GlobalDocPlan {
|
|
@@ -156,13 +157,24 @@ function stringifyTomlObject(obj: Record<string, unknown>): string {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
async function listGlobalDocSources(args: {
|
|
160
|
+
homeDir: string;
|
|
159
161
|
rootDir: string;
|
|
160
162
|
tool: string;
|
|
161
163
|
toolHome: string;
|
|
162
164
|
}): Promise<SourceTarget[]> {
|
|
163
|
-
const { rootDir, tool, toolHome } = args;
|
|
165
|
+
const { homeDir, rootDir, tool, toolHome } = args;
|
|
166
|
+
if (
|
|
167
|
+
!(await projectSyncAllowsToolSurface({
|
|
168
|
+
homeDir,
|
|
169
|
+
rootDir,
|
|
170
|
+
tool,
|
|
171
|
+
surface: "globalDocs",
|
|
172
|
+
}))
|
|
173
|
+
) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
164
176
|
const targets = globalDocTargetPaths(tool, toolHome);
|
|
165
|
-
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(rootDir);
|
|
177
|
+
const useBuiltinDefaults = await builtinSyncDefaultsEnabled(rootDir, homeDir);
|
|
166
178
|
|
|
167
179
|
const candidates: SourceTarget[] = [];
|
|
168
180
|
const base = join(rootDir, "AGENTS.global.md");
|
|
@@ -309,9 +321,20 @@ export async function syncToolGlobalDocs(args: {
|
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
async function listToolRules(args: {
|
|
324
|
+
homeDir: string;
|
|
312
325
|
rootDir: string;
|
|
313
326
|
tool: string;
|
|
314
327
|
}): Promise<{ sourcePath: string; targetPath: string }[]> {
|
|
328
|
+
if (
|
|
329
|
+
!(await projectSyncAllowsToolSurface({
|
|
330
|
+
homeDir: args.homeDir,
|
|
331
|
+
rootDir: args.rootDir,
|
|
332
|
+
tool: args.tool,
|
|
333
|
+
surface: "toolRules",
|
|
334
|
+
}))
|
|
335
|
+
) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
315
338
|
const sourceRoot = join(args.rootDir, "tools", args.tool, "rules");
|
|
316
339
|
const entries = await readdir(sourceRoot, { withFileTypes: true }).catch(
|
|
317
340
|
() => [] as import("node:fs").Dirent[]
|
|
@@ -425,6 +448,24 @@ export async function planToolConfigSync(args: {
|
|
|
425
448
|
existingConfigPath?: string;
|
|
426
449
|
previouslyManaged?: boolean;
|
|
427
450
|
}): Promise<ToolConfigPlan> {
|
|
451
|
+
if (
|
|
452
|
+
!(await projectSyncAllowsToolSurface({
|
|
453
|
+
homeDir: args.homeDir,
|
|
454
|
+
rootDir: args.rootDir,
|
|
455
|
+
tool: args.tool,
|
|
456
|
+
surface: "toolConfig",
|
|
457
|
+
}))
|
|
458
|
+
) {
|
|
459
|
+
return {
|
|
460
|
+
targetPath: args.toolConfigPath,
|
|
461
|
+
write: false,
|
|
462
|
+
remove: false,
|
|
463
|
+
contents: null,
|
|
464
|
+
sourcePath: join(args.rootDir, "tools", args.tool, "config.toml"),
|
|
465
|
+
managedConfig: false,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
428
469
|
const sourcePath = join(args.rootDir, "tools", args.tool, "config.toml");
|
|
429
470
|
const localSourcePath = join(
|
|
430
471
|
args.rootDir,
|