facult 1.0.3 → 1.1.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 +200 -10
- package/package.json +1 -1
- package/src/adapters/codex.ts +1 -0
- package/src/adapters/types.ts +1 -0
- package/src/agents.ts +180 -0
- package/src/ai-state.ts +55 -0
- package/src/audit/update-index.ts +12 -10
- package/src/autosync.ts +959 -0
- package/src/doctor.ts +128 -0
- package/src/enable-disable.ts +12 -7
- package/src/global-docs.ts +461 -0
- package/src/index-builder.ts +7 -5
- package/src/index.ts +13 -1
- package/src/manage.ts +591 -6
- package/src/paths.ts +48 -16
- package/src/query.ts +15 -6
- package/src/remote.ts +5 -1
- package/src/snippets.ts +106 -0
- package/src/trust.ts +12 -11
package/README.md
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
# facult
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a aria-label="NPM version" href="https://www.npmjs.com/package/facult">
|
|
5
|
+
<img alt="facult npm version" src="https://img.shields.io/npm/v/facult.svg?style=flat-square&logo=npm&labelColor=000000&label=facult">
|
|
6
|
+
</a>
|
|
7
|
+
<a aria-label="CI status" href="https://github.com/hack-dance/facult/actions/workflows/ci.yml">
|
|
8
|
+
<img alt="CI" src="https://img.shields.io/github/actions/workflow/status/hack-dance/facult/ci.yml?branch=main&style=flat-square&logo=github&label=ci&labelColor=000000">
|
|
9
|
+
</a>
|
|
10
|
+
<a aria-label="hack.dance" href="https://hack.dance">
|
|
11
|
+
<img alt="Made by hack.dance" src="https://img.shields.io/badge/MADE%20BY%20HACK.DANCE-000000.svg?style=flat-square&labelColor=000000">
|
|
12
|
+
</a>
|
|
13
|
+
<a aria-label="X" href="https://x.com/dimitrikennedy">
|
|
14
|
+
<img alt="Follow on X" src="https://img.shields.io/twitter/follow/dimitrikennedy?style=social">
|
|
15
|
+
</a>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
`facult` is a CLI for managing coding-agent configuration across tools.
|
|
4
19
|
|
|
5
20
|
It helps you:
|
|
6
21
|
- discover what is installed on your machine
|
|
7
22
|
- consolidate everything into one canonical store
|
|
8
23
|
- review trust/security before installing remote content
|
|
9
|
-
-
|
|
24
|
+
- sync managed outputs into Codex, Cursor, and Claude
|
|
25
|
+
- manage a git-backed personal AI store under `~/.ai`
|
|
10
26
|
|
|
11
27
|
## What facult Is
|
|
12
28
|
|
|
@@ -16,6 +32,7 @@ Think of it as:
|
|
|
16
32
|
- inventory + auditing for agent assets
|
|
17
33
|
- package manager interface for skill/MCP catalogs
|
|
18
34
|
- sync layer that applies your chosen setup to each tool
|
|
35
|
+
- canonical source manager for global AI instructions, agents, snippets, tool configs, and rules
|
|
19
36
|
|
|
20
37
|
## Quick Start
|
|
21
38
|
|
|
@@ -60,7 +77,17 @@ Pin to a specific version:
|
|
|
60
77
|
facult self-update --version 0.0.1
|
|
61
78
|
```
|
|
62
79
|
|
|
63
|
-
### 2.
|
|
80
|
+
### 2. Start with a read-only inventory (recommended first)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
facult scan --show-duplicates
|
|
84
|
+
# optional machine-readable output
|
|
85
|
+
facult scan --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`scan` is read-only. It inspects local configs and reports what `facult` found without changing files.
|
|
89
|
+
|
|
90
|
+
### 3. Import existing skills/configs
|
|
64
91
|
|
|
65
92
|
```bash
|
|
66
93
|
facult consolidate --auto keep-current --from ~/.codex/skills --from ~/.agents/skills
|
|
@@ -69,9 +96,9 @@ facult index
|
|
|
69
96
|
|
|
70
97
|
Why `keep-current`: it is deterministic and non-interactive for duplicate sources.
|
|
71
98
|
|
|
72
|
-
|
|
99
|
+
Canonical source root: `~/.ai`. Generated state remains under `~/.facult`.
|
|
73
100
|
|
|
74
|
-
###
|
|
101
|
+
### 4. Inspect what you have
|
|
75
102
|
|
|
76
103
|
```bash
|
|
77
104
|
facult list skills
|
|
@@ -80,7 +107,7 @@ facult show requesting-code-review
|
|
|
80
107
|
facult show mcp:github
|
|
81
108
|
```
|
|
82
109
|
|
|
83
|
-
###
|
|
110
|
+
### 5. Enable managed mode for your tools
|
|
84
111
|
|
|
85
112
|
```bash
|
|
86
113
|
facult manage codex
|
|
@@ -93,7 +120,21 @@ facult sync
|
|
|
93
120
|
|
|
94
121
|
At this point, your selected skills are actively synced to all managed tools.
|
|
95
122
|
|
|
96
|
-
###
|
|
123
|
+
### 6. Turn on background autosync
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
facult autosync install --git-remote origin --git-branch main --git-interval-minutes 60
|
|
127
|
+
facult autosync status
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This installs a per-user macOS LaunchAgent that:
|
|
131
|
+
- watches `~/.ai` for local changes and syncs managed tool outputs automatically
|
|
132
|
+
- tracks dirty state for the canonical repo
|
|
133
|
+
- runs a slower git autosync loop that batches changes, auto-commits them, rebases on the configured remote branch, and pushes on success
|
|
134
|
+
|
|
135
|
+
If the repo hits a rebase conflict, remote autosync stops and reports the blocked state, but local tool sync continues.
|
|
136
|
+
|
|
137
|
+
### 7. Turn on source trust and strict install flow
|
|
97
138
|
|
|
98
139
|
```bash
|
|
99
140
|
facult sources list
|
|
@@ -134,6 +175,94 @@ facult sync
|
|
|
134
175
|
|
|
135
176
|
Note: `templates init mcp ...` is a scaffold, not a running server by itself.
|
|
136
177
|
|
|
178
|
+
## The `~/.ai` Model
|
|
179
|
+
|
|
180
|
+
`facult` now treats `~/.ai` as the canonical, git-backed source of truth for personal AI configuration.
|
|
181
|
+
|
|
182
|
+
Typical layout:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
~/.ai/
|
|
186
|
+
AGENTS.global.md
|
|
187
|
+
AGENTS.override.global.md
|
|
188
|
+
config.toml
|
|
189
|
+
config.local.toml
|
|
190
|
+
instructions/
|
|
191
|
+
snippets/
|
|
192
|
+
agents/
|
|
193
|
+
skills/
|
|
194
|
+
mcp/
|
|
195
|
+
templates/
|
|
196
|
+
tools/
|
|
197
|
+
codex/
|
|
198
|
+
config.toml
|
|
199
|
+
rules/
|
|
200
|
+
projects/
|
|
201
|
+
<slug>/
|
|
202
|
+
config.toml
|
|
203
|
+
config.local.toml
|
|
204
|
+
snippets/
|
|
205
|
+
instructions/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Important split:
|
|
209
|
+
- `~/.ai` is canonical source
|
|
210
|
+
- `~/.facult` is generated state, trust state, managed tool state, autosync state, and caches
|
|
211
|
+
- tool homes such as `~/.codex` are rendered outputs
|
|
212
|
+
|
|
213
|
+
### Canonical conventions
|
|
214
|
+
|
|
215
|
+
- Use `instructions/` for reusable markdown documents
|
|
216
|
+
- Use `snippets/` for composable partial blocks injected into markdown templates
|
|
217
|
+
- Use `tools/codex/rules/*.rules` for actual Codex approval-policy rules
|
|
218
|
+
- Use logical refs such as `@ai/instructions/WRITING.md` in tracked source
|
|
219
|
+
- Use config-backed refs in prompts where you want stable named references such as `${refs.writing_rule}`
|
|
220
|
+
|
|
221
|
+
### Config and env layering
|
|
222
|
+
|
|
223
|
+
Canonical render context is layered explicitly:
|
|
224
|
+
1. built-ins injected by `facult`
|
|
225
|
+
2. `~/.ai/config.toml`
|
|
226
|
+
3. `~/.ai/config.local.toml`
|
|
227
|
+
4. `~/.ai/projects/<slug>/config.toml`
|
|
228
|
+
5. `~/.ai/projects/<slug>/config.local.toml`
|
|
229
|
+
6. explicit runtime overrides
|
|
230
|
+
|
|
231
|
+
Built-ins currently include:
|
|
232
|
+
- `AI_ROOT`
|
|
233
|
+
- `HOME`
|
|
234
|
+
- `PROJECT_ROOT`
|
|
235
|
+
- `PROJECT_SLUG`
|
|
236
|
+
- `TARGET_TOOL`
|
|
237
|
+
- `TARGET_PATH`
|
|
238
|
+
|
|
239
|
+
Recommended split:
|
|
240
|
+
- `config.toml`: tracked, portable, non-secret refs/defaults
|
|
241
|
+
- `config.local.toml`: ignored, machine-local paths and secrets
|
|
242
|
+
|
|
243
|
+
### Snippets
|
|
244
|
+
|
|
245
|
+
Snippets use HTML comment markers:
|
|
246
|
+
|
|
247
|
+
```md
|
|
248
|
+
<!-- fclty:global/codex/baseline -->
|
|
249
|
+
<!-- /fclty:global/codex/baseline -->
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Resolution rules:
|
|
253
|
+
- unscoped marker `codingstyle` prefers `snippets/projects/<project>/codingstyle.md`, then falls back to `snippets/global/codingstyle.md`
|
|
254
|
+
- explicit marker `global/codex/baseline` resolves directly to `snippets/global/codex/baseline.md`
|
|
255
|
+
|
|
256
|
+
Commands:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
facult snippets list
|
|
260
|
+
facult snippets show global/codex/baseline
|
|
261
|
+
facult snippets sync [--dry-run] [file...]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Snippets are already used during global Codex `AGENTS.md` rendering.
|
|
265
|
+
|
|
137
266
|
## Security and Trust
|
|
138
267
|
|
|
139
268
|
`facult` has two trust layers:
|
|
@@ -168,10 +297,11 @@ Recommended security flow:
|
|
|
168
297
|
### Capability categories
|
|
169
298
|
|
|
170
299
|
- Inventory: discover local skills, MCP configs, hooks, and instruction files
|
|
171
|
-
- Management: consolidate, index, manage/unmanage tools, enable/disable entries
|
|
300
|
+
- Management: consolidate, index, manage/unmanage tools, enable/disable entries, manage canonical AI config
|
|
172
301
|
- Security: static audit, agent audit, item trust, source trust, source verification
|
|
173
302
|
- Distribution: search/install/update from catalogs and verified manifests
|
|
174
303
|
- DX: scaffold templates and sync snippets into instruction/config files
|
|
304
|
+
- Automation: background autosync for local tool propagation and canonical repo git sync
|
|
175
305
|
|
|
176
306
|
### Command categories
|
|
177
307
|
|
|
@@ -199,6 +329,10 @@ facult enable <name> [--for <tool1,tool2,...>]
|
|
|
199
329
|
facult enable mcp:<name> [--for <tool1,tool2,...>]
|
|
200
330
|
facult disable <name> [--for <tool1,tool2,...>]
|
|
201
331
|
facult sync [tool] [--dry-run]
|
|
332
|
+
facult autosync install [tool] [--git-remote <name>] [--git-branch <name>] [--git-interval-minutes <n>] [--git-disable]
|
|
333
|
+
facult autosync status [tool]
|
|
334
|
+
facult autosync restart [tool]
|
|
335
|
+
facult autosync uninstall [tool]
|
|
202
336
|
```
|
|
203
337
|
|
|
204
338
|
- Remote catalogs and policies
|
|
@@ -241,7 +375,8 @@ facult <command> --help
|
|
|
241
375
|
`facult` resolves the canonical root in this order:
|
|
242
376
|
1. `FACULT_ROOT_DIR`
|
|
243
377
|
2. `~/.facult/config.json` (`rootDir`)
|
|
244
|
-
3.
|
|
378
|
+
3. `~/.ai`
|
|
379
|
+
4. `~/agents/.facult` (or a detected legacy store under `~/agents/`)
|
|
245
380
|
|
|
246
381
|
### Runtime env vars
|
|
247
382
|
|
|
@@ -256,9 +391,13 @@ Under `~/.facult/`:
|
|
|
256
391
|
- `sources.json` (latest inventory scan state)
|
|
257
392
|
- `consolidated.json` (consolidation state)
|
|
258
393
|
- `managed.json` (managed tool state)
|
|
394
|
+
- `ai/index.json` (generated canonical AI inventory)
|
|
259
395
|
- `audit/static-latest.json` (latest static audit report)
|
|
260
396
|
- `audit/agent-latest.json` (latest agent audit report)
|
|
261
397
|
- `trust/sources.json` (source trust policy state)
|
|
398
|
+
- `autosync/services/*.json` (autosync service configs)
|
|
399
|
+
- `autosync/state/*.json` (autosync runtime state)
|
|
400
|
+
- `autosync/logs/*` (autosync service logs)
|
|
262
401
|
|
|
263
402
|
### Config reference
|
|
264
403
|
|
|
@@ -275,7 +414,7 @@ Under `~/.facult/`:
|
|
|
275
414
|
Example:
|
|
276
415
|
```json
|
|
277
416
|
{
|
|
278
|
-
"rootDir": "
|
|
417
|
+
"rootDir": "~/.ai",
|
|
279
418
|
"scanFrom": ["~/dev", "~/work"],
|
|
280
419
|
"scanFromIgnore": ["vendor", ".venv"],
|
|
281
420
|
"scanFromNoDefaultIgnore": false,
|
|
@@ -307,6 +446,42 @@ bun run install:status
|
|
|
307
446
|
|
|
308
447
|
Default install path is `~/.facult/bin/facult`. You can pass a custom target dir via `--dir=/path`.
|
|
309
448
|
|
|
449
|
+
## Autosync
|
|
450
|
+
|
|
451
|
+
`facult autosync` is the background propagation layer for managed installs.
|
|
452
|
+
|
|
453
|
+
Current v1 behavior:
|
|
454
|
+
- macOS LaunchAgent-backed
|
|
455
|
+
- immediate local managed-tool sync on `~/.ai` file changes
|
|
456
|
+
- periodic git autosync for the canonical repo
|
|
457
|
+
- automatic autosync commits with source-tagged commit messages such as:
|
|
458
|
+
- `chore(facult-autosync): sync canonical ai changes from <host> [service:all]`
|
|
459
|
+
|
|
460
|
+
Recommended usage:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
facult autosync install
|
|
464
|
+
facult autosync status
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Tool-scoped service:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
facult autosync install codex
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
One-shot runner for verification/debugging:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
facult autosync run --service all --once
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Remote git policy:
|
|
480
|
+
- do not sync on every file event
|
|
481
|
+
- mark the canonical repo dirty on local changes
|
|
482
|
+
- on the configured timer, fetch, auto-commit local canonical changes if needed, pull `--rebase`, then push
|
|
483
|
+
- if rebase conflicts occur, remote autosync is blocked and reported, but local managed-tool sync keeps running
|
|
484
|
+
|
|
310
485
|
## CI and Release Automation
|
|
311
486
|
|
|
312
487
|
- CI workflow: `.github/workflows/ci.yml`
|
|
@@ -373,3 +548,18 @@ bun run release:dry-run
|
|
|
373
548
|
Not as a first-party `facult mcp serve` runtime.
|
|
374
549
|
|
|
375
550
|
`facult` currently focuses on inventory, trust/audit, install/update, and managed sync of skills/MCP configs.
|
|
551
|
+
|
|
552
|
+
### Does facult now manage global AI config, not just skills and MCP?
|
|
553
|
+
|
|
554
|
+
Yes. The core model now includes:
|
|
555
|
+
- canonical personal AI source in `~/.ai`
|
|
556
|
+
- rendered managed outputs in tool homes such as `~/.codex`
|
|
557
|
+
- global instruction docs such as `AGENTS.global.md`
|
|
558
|
+
- tool-native configs such as `~/.codex/config.toml`
|
|
559
|
+
- tool-native rule files such as `~/.codex/rules/*.rules`
|
|
560
|
+
|
|
561
|
+
### Do I still need to run `facult sync` manually?
|
|
562
|
+
|
|
563
|
+
If autosync is not installed, yes.
|
|
564
|
+
|
|
565
|
+
If autosync is installed, local changes under `~/.ai` propagate automatically to managed tools. Manual `facult sync` is still useful for explicit repair, dry-runs, and non-daemon workflows.
|
package/package.json
CHANGED
package/src/adapters/codex.ts
CHANGED
package/src/adapters/types.ts
CHANGED
package/src/agents.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
const AI_REF_RE = /(?<![\w@])@ai\/([^\s"'`<>]+)/g;
|
|
4
|
+
const INTERPOLATION_RE = /\$\{([^}]+)\}/g;
|
|
5
|
+
const TRAILING_PUNCTUATION_RE = /[.,;:!?)}\]]+$/;
|
|
6
|
+
const MAX_RENDER_PASSES = 10;
|
|
7
|
+
|
|
8
|
+
export interface RenderCanonicalTextOptions {
|
|
9
|
+
homeDir?: string;
|
|
10
|
+
rootDir: string;
|
|
11
|
+
projectSlug?: string;
|
|
12
|
+
projectRoot?: string;
|
|
13
|
+
targetTool?: string;
|
|
14
|
+
targetPath?: string;
|
|
15
|
+
overrides?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type RenderContext = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
function trimTrailingPunctuation(refPath: string): {
|
|
21
|
+
path: string;
|
|
22
|
+
suffix: string;
|
|
23
|
+
} {
|
|
24
|
+
const match = TRAILING_PUNCTUATION_RE.exec(refPath);
|
|
25
|
+
if (!match) {
|
|
26
|
+
return { path: refPath, suffix: "" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const suffix = match[0];
|
|
30
|
+
return {
|
|
31
|
+
path: refPath.slice(0, -suffix.length),
|
|
32
|
+
suffix,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderAiRefs(input: string, canonicalRoot: string): string {
|
|
37
|
+
return input.replace(AI_REF_RE, (_match, refPath: string) => {
|
|
38
|
+
const { path, suffix } = trimTrailingPunctuation(refPath);
|
|
39
|
+
return `${join(canonicalRoot, path)}${suffix}`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeContexts(
|
|
48
|
+
base: Record<string, unknown>,
|
|
49
|
+
override: Record<string, unknown>
|
|
50
|
+
): Record<string, unknown> {
|
|
51
|
+
const merged: Record<string, unknown> = { ...base };
|
|
52
|
+
|
|
53
|
+
for (const [key, value] of Object.entries(override)) {
|
|
54
|
+
const current = merged[key];
|
|
55
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
56
|
+
merged[key] = mergeContexts(current, value);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
merged[key] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return merged;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readTomlFile(
|
|
66
|
+
pathValue: string
|
|
67
|
+
): Promise<Record<string, unknown> | null> {
|
|
68
|
+
const file = Bun.file(pathValue);
|
|
69
|
+
if (!(await file.exists())) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const text = await file.text();
|
|
74
|
+
const parsed = Bun.TOML.parse(text);
|
|
75
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getContextValue(
|
|
79
|
+
context: Record<string, unknown>,
|
|
80
|
+
dottedPath: string
|
|
81
|
+
): unknown {
|
|
82
|
+
const segments = dottedPath
|
|
83
|
+
.split(".")
|
|
84
|
+
.map((segment) => segment.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
if (segments.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let current: unknown = context;
|
|
91
|
+
for (const segment of segments) {
|
|
92
|
+
if (!(isPlainObject(current) && segment in current)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
current = current[segment];
|
|
96
|
+
}
|
|
97
|
+
return current;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function interpolateString(
|
|
101
|
+
input: string,
|
|
102
|
+
context: Record<string, unknown>
|
|
103
|
+
): string {
|
|
104
|
+
return input.replace(INTERPOLATION_RE, (match, keyPath: string) => {
|
|
105
|
+
const value = getContextValue(context, keyPath.trim());
|
|
106
|
+
return typeof value === "string" ? value : match;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function loadRenderContext(
|
|
111
|
+
options: RenderCanonicalTextOptions
|
|
112
|
+
): Promise<RenderContext> {
|
|
113
|
+
const {
|
|
114
|
+
homeDir,
|
|
115
|
+
overrides,
|
|
116
|
+
projectRoot,
|
|
117
|
+
projectSlug,
|
|
118
|
+
rootDir,
|
|
119
|
+
targetPath,
|
|
120
|
+
targetTool,
|
|
121
|
+
} = options;
|
|
122
|
+
const contextBase: RenderContext = {
|
|
123
|
+
AI_ROOT: rootDir,
|
|
124
|
+
HOME: homeDir ?? "",
|
|
125
|
+
PROJECT_ROOT: projectRoot ?? "",
|
|
126
|
+
PROJECT_SLUG: projectSlug ?? "",
|
|
127
|
+
TARGET_PATH: targetPath ?? "",
|
|
128
|
+
TARGET_TOOL: targetTool ?? "",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let context = contextBase;
|
|
132
|
+
const layers = [
|
|
133
|
+
await readTomlFile(join(rootDir, "config.toml")),
|
|
134
|
+
await readTomlFile(join(rootDir, "config.local.toml")),
|
|
135
|
+
projectSlug
|
|
136
|
+
? await readTomlFile(
|
|
137
|
+
join(rootDir, "projects", projectSlug, "config.toml")
|
|
138
|
+
)
|
|
139
|
+
: null,
|
|
140
|
+
projectSlug
|
|
141
|
+
? await readTomlFile(
|
|
142
|
+
join(rootDir, "projects", projectSlug, "config.local.toml")
|
|
143
|
+
)
|
|
144
|
+
: null,
|
|
145
|
+
overrides && isPlainObject(overrides) ? overrides : null,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
for (const layer of layers) {
|
|
149
|
+
if (layer) {
|
|
150
|
+
context = mergeContexts(context, layer);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return context;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function renderCanonicalText(
|
|
158
|
+
input: string,
|
|
159
|
+
options: RenderCanonicalTextOptions
|
|
160
|
+
): Promise<string> {
|
|
161
|
+
const context = await loadRenderContext(options);
|
|
162
|
+
let rendered = input;
|
|
163
|
+
const seen = new Set<string>();
|
|
164
|
+
|
|
165
|
+
for (let pass = 0; pass < MAX_RENDER_PASSES; pass += 1) {
|
|
166
|
+
if (seen.has(rendered)) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
seen.add(rendered);
|
|
170
|
+
|
|
171
|
+
const interpolated = interpolateString(rendered, context);
|
|
172
|
+
const withRefs = renderAiRefs(interpolated, options.rootDir);
|
|
173
|
+
if (withRefs === rendered) {
|
|
174
|
+
return withRefs;
|
|
175
|
+
}
|
|
176
|
+
rendered = withRefs;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return rendered;
|
|
180
|
+
}
|
package/src/ai-state.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { buildIndex } from "./index-builder";
|
|
4
|
+
import { facultAiIndexPath } from "./paths";
|
|
5
|
+
|
|
6
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
return (await stat(path)).isFile();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function legacyAiIndexPath(rootDir: string): string {
|
|
15
|
+
return join(rootDir, "index.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function ensureAiIndexPath(args: {
|
|
19
|
+
homeDir: string;
|
|
20
|
+
rootDir: string;
|
|
21
|
+
repair?: boolean;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
path: string;
|
|
24
|
+
repaired: boolean;
|
|
25
|
+
source: "generated" | "legacy" | "rebuilt" | "missing";
|
|
26
|
+
}> {
|
|
27
|
+
const generatedPath = facultAiIndexPath(args.homeDir);
|
|
28
|
+
if (await fileExists(generatedPath)) {
|
|
29
|
+
return { path: generatedPath, repaired: false, source: "generated" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const legacyPath = legacyAiIndexPath(args.rootDir);
|
|
33
|
+
if (await fileExists(legacyPath)) {
|
|
34
|
+
if (args.repair !== false) {
|
|
35
|
+
await mkdir(dirname(generatedPath), { recursive: true });
|
|
36
|
+
await copyFile(legacyPath, generatedPath);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
path: generatedPath,
|
|
40
|
+
repaired: args.repair !== false,
|
|
41
|
+
source: "legacy",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (args.repair !== false) {
|
|
46
|
+
const { outputPath } = await buildIndex({
|
|
47
|
+
rootDir: args.rootDir,
|
|
48
|
+
homeDir: args.homeDir,
|
|
49
|
+
force: false,
|
|
50
|
+
});
|
|
51
|
+
return { path: outputPath, repaired: true, source: "rebuilt" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { path: generatedPath, repaired: false, source: "missing" };
|
|
55
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import {
|
|
2
|
+
import { ensureAiIndexPath } from "../ai-state";
|
|
3
3
|
import type { FacultIndex } from "../index-builder";
|
|
4
|
-
import { facultRootDir } from "../paths";
|
|
4
|
+
import { facultAiIndexPath, facultRootDir } from "../paths";
|
|
5
5
|
import type { AuditItemResult, Severity } from "./types";
|
|
6
6
|
import { SEVERITY_ORDER } from "./types";
|
|
7
7
|
|
|
@@ -33,8 +33,12 @@ function computeAuditStatus(
|
|
|
33
33
|
return worst >= SEVERITY_ORDER.high ? "flagged" : "passed";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
async function loadIndex(
|
|
37
|
-
const indexPath =
|
|
36
|
+
async function loadIndex(homeDir: string): Promise<FacultIndex | null> {
|
|
37
|
+
const { path: indexPath } = await ensureAiIndexPath({
|
|
38
|
+
homeDir,
|
|
39
|
+
rootDir: facultRootDir(homeDir),
|
|
40
|
+
repair: true,
|
|
41
|
+
});
|
|
38
42
|
const file = Bun.file(indexPath);
|
|
39
43
|
if (!(await file.exists())) {
|
|
40
44
|
return null;
|
|
@@ -46,8 +50,8 @@ async function loadIndex(rootDir: string): Promise<FacultIndex | null> {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
async function writeIndex(
|
|
50
|
-
const indexPath =
|
|
53
|
+
async function writeIndex(homeDir: string, index: FacultIndex) {
|
|
54
|
+
const indexPath = facultAiIndexPath(homeDir);
|
|
51
55
|
await Bun.write(indexPath, `${JSON.stringify(index, null, 2)}\n`);
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -57,9 +61,7 @@ export async function updateIndexFromAuditReport(opts: {
|
|
|
57
61
|
results: AuditItemResult[];
|
|
58
62
|
}): Promise<{ updated: boolean; reason?: string }> {
|
|
59
63
|
const home = opts.homeDir ?? homedir();
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const loaded = await loadIndex(rootDir);
|
|
64
|
+
const loaded = await loadIndex(home);
|
|
63
65
|
if (!loaded) {
|
|
64
66
|
return { updated: false, reason: "index-missing" };
|
|
65
67
|
}
|
|
@@ -110,6 +112,6 @@ export async function updateIndexFromAuditReport(opts: {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
index.updatedAt = new Date().toISOString();
|
|
113
|
-
await writeIndex(
|
|
115
|
+
await writeIndex(home, index);
|
|
114
116
|
return { updated: true };
|
|
115
117
|
}
|