facult 1.2.1 → 1.3.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 +28 -30
- package/package.json +1 -1
- package/src/ai-state.ts +49 -1
- package/src/audit/agent.ts +3 -3
- package/src/audit/static.ts +4 -4
- package/src/audit/tui.ts +8 -8
- package/src/autosync.ts +118 -34
- package/src/consolidate.ts +38 -33
- package/src/doctor.ts +232 -16
- package/src/index.ts +2 -2
- package/src/manage.ts +27 -11
- package/src/migrate.ts +6 -6
- package/src/paths.ts +128 -70
- package/src/quarantine.ts +2 -1
- package/src/remote-sources.ts +18 -4
- package/src/remote.ts +1 -1
- package/src/scan.ts +7 -5
- package/src/self-update.ts +32 -10
- package/src/source-trust.ts +50 -42
- package/src/trust-list.ts +19 -10
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Put that in `config.toml` or `config.local.toml` under the active canonical root
|
|
|
104
104
|
|
|
105
105
|
- canonical source lives in `~/.ai` or `<repo>/.ai`
|
|
106
106
|
- rendered outputs live in tool homes like `~/.codex`, `<repo>/.codex`, `~/.claude`, or `~/.cursor`
|
|
107
|
-
- generated state lives in `~/.facult` or `<repo>/.facult`
|
|
107
|
+
- generated Facult-owned state lives in `~/.ai/.facult` or `<repo>/.ai/.facult`
|
|
108
108
|
|
|
109
109
|
This keeps authored capability portable and reviewable while still producing the exact files each tool expects.
|
|
110
110
|
|
|
@@ -217,9 +217,9 @@ facult index
|
|
|
217
217
|
|
|
218
218
|
Why `keep-current`: it is deterministic and non-interactive for duplicate sources.
|
|
219
219
|
|
|
220
|
-
Canonical source root: `~/.ai` for global work, or `<repo>/.ai` for project-local work.
|
|
221
|
-
- global: `~/.facult`
|
|
222
|
-
- project: `<repo>/.facult`
|
|
220
|
+
Canonical source root: `~/.ai` for global work, or `<repo>/.ai` for project-local work. Facult-owned generated/config/runtime state lives inside the active canonical root:
|
|
221
|
+
- global: `~/.ai/.facult`
|
|
222
|
+
- project: `<repo>/.ai/.facult`
|
|
223
223
|
|
|
224
224
|
### 3b. Bootstrap a repo-local `.ai`
|
|
225
225
|
|
|
@@ -229,7 +229,7 @@ bunx facult templates init project-ai
|
|
|
229
229
|
bunx facult index
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
This seeds `<repo>/.ai` from the built-in Facult operating-model pack and writes a merged project index/graph under `<repo>/.facult/ai/`.
|
|
232
|
+
This seeds `<repo>/.ai` from the built-in Facult operating-model pack and writes a merged project index/graph under `<repo>/.ai/.facult/ai/`.
|
|
233
233
|
|
|
234
234
|
### 4. Inspect what you have
|
|
235
235
|
|
|
@@ -365,19 +365,19 @@ Typical layout:
|
|
|
365
365
|
agents/
|
|
366
366
|
skills/
|
|
367
367
|
tools/
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
.facult/
|
|
369
|
+
ai/
|
|
370
|
+
index.json
|
|
371
|
+
graph.json
|
|
372
372
|
.codex/
|
|
373
373
|
.claude/
|
|
374
374
|
```
|
|
375
375
|
|
|
376
376
|
Important split:
|
|
377
377
|
- `.ai/` is canonical source
|
|
378
|
-
- `.facult/` is generated state, trust state, managed tool state, autosync state, and caches
|
|
378
|
+
- `.ai/.facult/` is Facult-owned generated state, trust state, managed tool state, autosync state, and caches
|
|
379
379
|
- tool homes such as `.codex/` and `.claude/` are rendered outputs
|
|
380
|
-
- the generated capability graph lives at `.facult/ai/graph.json`
|
|
380
|
+
- the generated capability graph lives at `.ai/.facult/ai/graph.json`
|
|
381
381
|
|
|
382
382
|
### Asset types
|
|
383
383
|
|
|
@@ -408,11 +408,9 @@ Not every asset syncs directly to a tool. Some exist primarily to support render
|
|
|
408
408
|
|
|
409
409
|
Canonical render context is layered explicitly:
|
|
410
410
|
1. built-ins injected by `facult`
|
|
411
|
-
2.
|
|
412
|
-
3.
|
|
413
|
-
4.
|
|
414
|
-
5. `~/.ai/projects/<slug>/config.local.toml`
|
|
415
|
-
6. explicit runtime overrides
|
|
411
|
+
2. active canonical root `config.toml`
|
|
412
|
+
3. active canonical root `config.local.toml`
|
|
413
|
+
4. explicit runtime overrides
|
|
416
414
|
|
|
417
415
|
Built-ins currently include:
|
|
418
416
|
- `AI_ROOT`
|
|
@@ -423,8 +421,8 @@ Built-ins currently include:
|
|
|
423
421
|
- `TARGET_PATH`
|
|
424
422
|
|
|
425
423
|
Recommended split:
|
|
426
|
-
- `config.toml`: tracked, portable, non-secret refs/defaults
|
|
427
|
-
- `config.local.toml`: ignored, machine-local paths and secrets
|
|
424
|
+
- `~/.ai/config.toml` or `<repo>/.ai/config.toml`: tracked, portable, non-secret refs/defaults
|
|
425
|
+
- `~/.ai/config.local.toml` or `<repo>/.ai/config.local.toml`: ignored, machine-local paths and secrets
|
|
428
426
|
- `[builtin].sync_defaults = false`: disable builtin default sync/materialization for this root
|
|
429
427
|
- `facult sync --builtin-conflicts overwrite`: allow packaged builtin defaults to overwrite locally modified generated targets
|
|
430
428
|
|
|
@@ -453,7 +451,7 @@ Snippets are already used during global Codex `AGENTS.md` rendering.
|
|
|
453
451
|
|
|
454
452
|
### Graph inspection
|
|
455
453
|
|
|
456
|
-
The generated graph in `.facult/ai/graph.json` is queryable directly:
|
|
454
|
+
The generated graph in `.ai/.facult/ai/graph.json` is queryable directly:
|
|
457
455
|
|
|
458
456
|
```bash
|
|
459
457
|
facult graph show instruction:WRITING
|
|
@@ -496,13 +494,13 @@ facult ai evolve apply EV-00001
|
|
|
496
494
|
facult ai evolve promote EV-00003 --to global --project
|
|
497
495
|
```
|
|
498
496
|
|
|
499
|
-
Runtime state stays generated and local:
|
|
500
|
-
- global writeback state: `~/.facult/ai/global/...`
|
|
501
|
-
- project writeback state:
|
|
497
|
+
Runtime state stays generated and local inside the active canonical root:
|
|
498
|
+
- global writeback state: `~/.ai/.facult/ai/global/...`
|
|
499
|
+
- project writeback state: `<repo>/.ai/.facult/ai/project/...`
|
|
502
500
|
|
|
503
501
|
That split is intentional:
|
|
504
502
|
- canonical source remains in `~/.ai` or `<repo>/.ai`
|
|
505
|
-
- writeback queues, journals,
|
|
503
|
+
- writeback queues, journals, proposal records, trust state, autosync state, and other Facult-owned runtime/config state stay inside `.ai/.facult/` rather than inside the tool homes
|
|
506
504
|
|
|
507
505
|
Use writeback when:
|
|
508
506
|
- a task exposed a weak or misleading verification loop
|
|
@@ -652,7 +650,7 @@ facult <command> --help
|
|
|
652
650
|
`facult` resolves the canonical root in this order:
|
|
653
651
|
1. `FACULT_ROOT_DIR`
|
|
654
652
|
2. nearest project `.ai` from the current working directory for CLI-facing commands
|
|
655
|
-
3. `~/.facult/config.json` (`rootDir`)
|
|
653
|
+
3. `~/.ai/.facult/config.json` (`rootDir`)
|
|
656
654
|
4. `~/.ai`
|
|
657
655
|
5. `~/agents/.facult` (or a detected legacy store under `~/agents/`)
|
|
658
656
|
|
|
@@ -660,12 +658,12 @@ facult <command> --help
|
|
|
660
658
|
|
|
661
659
|
- `FACULT_ROOT_DIR`: override canonical store location
|
|
662
660
|
- `FACULT_VERSION`: version selector for `scripts/install.sh` (`latest` by default)
|
|
663
|
-
- `FACULT_INSTALL_DIR`: install target dir for `scripts/install.sh` (`~/.facult/bin` by default)
|
|
661
|
+
- `FACULT_INSTALL_DIR`: install target dir for `scripts/install.sh` (`~/.ai/.facult/bin` by default)
|
|
664
662
|
- `FACULT_INSTALL_PM`: force package manager detection for npm bootstrap launcher (`npm` or `bun`)
|
|
665
663
|
|
|
666
664
|
### State and report files
|
|
667
665
|
|
|
668
|
-
Under `~/.facult/`:
|
|
666
|
+
Under `~/.ai/.facult/`:
|
|
669
667
|
- `sources.json` (latest inventory scan state)
|
|
670
668
|
- `consolidated.json` (consolidation state)
|
|
671
669
|
- `managed.json` (managed tool state)
|
|
@@ -679,7 +677,7 @@ Under `~/.facult/`:
|
|
|
679
677
|
|
|
680
678
|
### Config reference
|
|
681
679
|
|
|
682
|
-
`~/.facult/config.json` supports:
|
|
680
|
+
`~/.ai/.facult/config.json` supports:
|
|
683
681
|
- `rootDir`
|
|
684
682
|
- `scanFrom`
|
|
685
683
|
- `scanFromIgnore`
|
|
@@ -710,7 +708,7 @@ Default source aliases:
|
|
|
710
708
|
- `skills.sh`
|
|
711
709
|
- `clawhub`
|
|
712
710
|
|
|
713
|
-
Custom remote sources can be defined in `~/.facult/indices.json` (manifest URL, optional integrity, optional signature keys/signature verification settings).
|
|
711
|
+
Custom remote sources can be defined in `~/.ai/.facult/indices.json` (manifest URL, optional integrity, optional signature keys/signature verification settings).
|
|
714
712
|
|
|
715
713
|
## Local Install Modes
|
|
716
714
|
|
|
@@ -722,7 +720,7 @@ bun run install:bin
|
|
|
722
720
|
bun run install:status
|
|
723
721
|
```
|
|
724
722
|
|
|
725
|
-
Default install path is `~/.facult/bin/facult`. You can pass a custom target dir via `--dir=/path`.
|
|
723
|
+
Default install path is `~/.ai/.facult/bin/facult`. You can pass a custom target dir via `--dir=/path`.
|
|
726
724
|
|
|
727
725
|
## Autosync
|
|
728
726
|
|
|
@@ -780,7 +778,7 @@ Release behavior:
|
|
|
780
778
|
3. The same release workflow then builds platform binaries and uploads them to that GitHub release.
|
|
781
779
|
4. npm publish runs only after binary asset upload succeeds (`publish-npm` depends on `publish-assets`).
|
|
782
780
|
5. Published release assets include platform binaries, `facult-install.sh`, and `SHA256SUMS`.
|
|
783
|
-
6. The npm package launcher resolves your platform, downloads the matching release binary, caches it under `~/.facult/runtime/<version>/<platform-arch>/`, and runs it.
|
|
781
|
+
6. The npm package launcher resolves your platform, downloads the matching release binary, caches it under `~/.ai/.facult/runtime/<version>/<platform-arch>/`, and runs it.
|
|
784
782
|
|
|
785
783
|
Current prebuilt binary targets:
|
|
786
784
|
- `darwin-x64`
|
package/package.json
CHANGED
package/src/ai-state.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { copyFile, mkdir, stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { buildIndex } from "./index-builder";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
facultAiGraphPath,
|
|
6
|
+
facultAiIndexPath,
|
|
7
|
+
legacyFacultStateDirForRoot,
|
|
8
|
+
} from "./paths";
|
|
5
9
|
|
|
6
10
|
async function fileExists(path: string): Promise<boolean> {
|
|
7
11
|
try {
|
|
@@ -15,6 +19,22 @@ export function legacyAiIndexPath(rootDir: string): string {
|
|
|
15
19
|
return join(rootDir, "index.json");
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
function legacyGeneratedAiIndexPath(homeDir: string, rootDir: string): string {
|
|
23
|
+
return join(
|
|
24
|
+
legacyFacultStateDirForRoot(rootDir, homeDir),
|
|
25
|
+
"ai",
|
|
26
|
+
"index.json"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function legacyGeneratedAiGraphPath(homeDir: string, rootDir: string): string {
|
|
31
|
+
return join(
|
|
32
|
+
legacyFacultStateDirForRoot(rootDir, homeDir),
|
|
33
|
+
"ai",
|
|
34
|
+
"graph.json"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
export async function ensureAiIndexPath(args: {
|
|
19
39
|
homeDir: string;
|
|
20
40
|
rootDir: string;
|
|
@@ -29,6 +49,22 @@ export async function ensureAiIndexPath(args: {
|
|
|
29
49
|
return { path: generatedPath, repaired: false, source: "generated" };
|
|
30
50
|
}
|
|
31
51
|
|
|
52
|
+
const legacyGeneratedPath = legacyGeneratedAiIndexPath(
|
|
53
|
+
args.homeDir,
|
|
54
|
+
args.rootDir
|
|
55
|
+
);
|
|
56
|
+
if (await fileExists(legacyGeneratedPath)) {
|
|
57
|
+
if (args.repair !== false) {
|
|
58
|
+
await mkdir(dirname(generatedPath), { recursive: true });
|
|
59
|
+
await copyFile(legacyGeneratedPath, generatedPath);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
path: generatedPath,
|
|
63
|
+
repaired: args.repair !== false,
|
|
64
|
+
source: "legacy",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
const legacyPath = legacyAiIndexPath(args.rootDir);
|
|
33
69
|
if (await fileExists(legacyPath)) {
|
|
34
70
|
if (args.repair !== false) {
|
|
@@ -67,6 +103,18 @@ export async function ensureAiGraphPath(args: {
|
|
|
67
103
|
return { path: generatedPath, rebuilt: false };
|
|
68
104
|
}
|
|
69
105
|
|
|
106
|
+
const legacyGeneratedPath = legacyGeneratedAiGraphPath(
|
|
107
|
+
args.homeDir,
|
|
108
|
+
args.rootDir
|
|
109
|
+
);
|
|
110
|
+
if (await fileExists(legacyGeneratedPath)) {
|
|
111
|
+
if (args.repair !== false) {
|
|
112
|
+
await mkdir(dirname(generatedPath), { recursive: true });
|
|
113
|
+
await copyFile(legacyGeneratedPath, generatedPath);
|
|
114
|
+
}
|
|
115
|
+
return { path: generatedPath, rebuilt: args.repair !== false };
|
|
116
|
+
}
|
|
117
|
+
|
|
70
118
|
if (args.repair !== false) {
|
|
71
119
|
const { graphPath } = await buildIndex({
|
|
72
120
|
rootDir: args.rootDir,
|
package/src/audit/agent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { basename, join, sep } from "node:path";
|
|
4
|
-
import { facultRootDir, readFacultConfig } from "../paths";
|
|
4
|
+
import { facultRootDir, facultStateDir, readFacultConfig } from "../paths";
|
|
5
5
|
import type { AssetFile, ScanResult } from "../scan";
|
|
6
6
|
import { scan } from "../scan";
|
|
7
7
|
import {
|
|
@@ -978,7 +978,7 @@ export async function runAgentAudit(opts?: {
|
|
|
978
978
|
},
|
|
979
979
|
};
|
|
980
980
|
|
|
981
|
-
const auditDir = join(home, "
|
|
981
|
+
const auditDir = join(facultStateDir(home), "audit");
|
|
982
982
|
await mkdir(auditDir, { recursive: true });
|
|
983
983
|
await Bun.write(
|
|
984
984
|
join(auditDir, "agent-latest.json"),
|
|
@@ -1040,7 +1040,7 @@ function printHuman(report: AgentAuditReport) {
|
|
|
1040
1040
|
`By severity: critical=${report.summary.bySeverity.critical}, high=${report.summary.bySeverity.high}, medium=${report.summary.bySeverity.medium}, low=${report.summary.bySeverity.low}`
|
|
1041
1041
|
);
|
|
1042
1042
|
console.log(
|
|
1043
|
-
`Wrote ${join(homedir(), "
|
|
1043
|
+
`Wrote ${join(facultStateDir(homedir()), "audit", "agent-latest.json")}`
|
|
1044
1044
|
);
|
|
1045
1045
|
}
|
|
1046
1046
|
|
package/src/audit/static.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
-
import { readFacultConfig } from "../paths";
|
|
5
|
+
import { facultStateDir, readFacultConfig } from "../paths";
|
|
6
6
|
import type { ScanResult } from "../scan";
|
|
7
7
|
import { scan } from "../scan";
|
|
8
8
|
import {
|
|
@@ -649,7 +649,7 @@ export async function runStaticAudit(opts?: {
|
|
|
649
649
|
const argv = opts?.argv ?? [];
|
|
650
650
|
const home = opts?.homeDir ?? homedir();
|
|
651
651
|
const rulesPath =
|
|
652
|
-
opts?.rulesPath ?? join(home, "
|
|
652
|
+
opts?.rulesPath ?? join(facultStateDir(home), "audit-rules.yaml");
|
|
653
653
|
|
|
654
654
|
const overrides = await loadRuleOverrides(rulesPath);
|
|
655
655
|
const rules = compileRules(mergeRules(DEFAULT_RULES, overrides));
|
|
@@ -1040,7 +1040,7 @@ export async function runStaticAudit(opts?: {
|
|
|
1040
1040
|
},
|
|
1041
1041
|
};
|
|
1042
1042
|
|
|
1043
|
-
const auditDir = join(home, "
|
|
1043
|
+
const auditDir = join(facultStateDir(home), "audit");
|
|
1044
1044
|
await ensureDir(auditDir);
|
|
1045
1045
|
await Bun.write(
|
|
1046
1046
|
join(auditDir, "static-latest.json"),
|
|
@@ -1091,7 +1091,7 @@ function printHuman(report: StaticAuditReport) {
|
|
|
1091
1091
|
`By severity: critical=${report.summary.bySeverity.critical}, high=${report.summary.bySeverity.high}, medium=${report.summary.bySeverity.medium}, low=${report.summary.bySeverity.low}`
|
|
1092
1092
|
);
|
|
1093
1093
|
console.log(
|
|
1094
|
-
`Wrote ${join(homedir(), "
|
|
1094
|
+
`Wrote ${join(facultStateDir(homedir()), "audit", "static-latest.json")}`
|
|
1095
1095
|
);
|
|
1096
1096
|
}
|
|
1097
1097
|
|
package/src/audit/tui.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
text,
|
|
13
13
|
} from "@clack/prompts";
|
|
14
14
|
import { buildIndex } from "../index-builder";
|
|
15
|
-
import { facultRootDir, readFacultConfig } from "../paths";
|
|
15
|
+
import { facultRootDir, facultStateDir, readFacultConfig } from "../paths";
|
|
16
16
|
import { type QuarantineMode, quarantineItems } from "../quarantine";
|
|
17
17
|
import { type AgentAuditReport, runAgentAudit } from "./agent";
|
|
18
18
|
import { runStaticAudit } from "./static";
|
|
@@ -206,7 +206,7 @@ Usage:
|
|
|
206
206
|
|
|
207
207
|
Notes:
|
|
208
208
|
- This is an interactive wizard (TTY required).
|
|
209
|
-
- Quarantine will move/copy files into ~/.facult/quarantine/<timestamp>/ and write a manifest.json.
|
|
209
|
+
- Quarantine will move/copy files into ~/.ai/.facult/quarantine/<timestamp>/ and write a manifest.json.
|
|
210
210
|
- For non-interactive runs, use: facult audit --non-interactive ...
|
|
211
211
|
`);
|
|
212
212
|
}
|
|
@@ -228,7 +228,7 @@ export async function auditTuiCommand(argv: string[]) {
|
|
|
228
228
|
if (cfgRoots.length) {
|
|
229
229
|
note(
|
|
230
230
|
`Configured scanFrom roots:\n- ${cfgRoots.join("\n- ")}`,
|
|
231
|
-
"~/.facult/config.json"
|
|
231
|
+
"~/.ai/.facult/config.json"
|
|
232
232
|
);
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -285,7 +285,7 @@ export async function auditTuiCommand(argv: string[]) {
|
|
|
285
285
|
{
|
|
286
286
|
value: "config",
|
|
287
287
|
label: "Use configured scanFrom",
|
|
288
|
-
hint: "from ~/.facult/config.json",
|
|
288
|
+
hint: "from ~/.ai/.facult/config.json",
|
|
289
289
|
},
|
|
290
290
|
]
|
|
291
291
|
: []),
|
|
@@ -448,13 +448,13 @@ export async function auditTuiCommand(argv: string[]) {
|
|
|
448
448
|
if (reports.static) {
|
|
449
449
|
summaries.push(`Static: ${summarizeReportStatic(reports.static)}`);
|
|
450
450
|
summaries.push(
|
|
451
|
-
`Wrote ${join(homedir(), "
|
|
451
|
+
`Wrote ${join(facultStateDir(homedir()), "audit", "static-latest.json")}`
|
|
452
452
|
);
|
|
453
453
|
}
|
|
454
454
|
if (reports.agent) {
|
|
455
455
|
summaries.push(`Agent: ${summarizeReportAgent(reports.agent)}`);
|
|
456
456
|
summaries.push(
|
|
457
|
-
`Wrote ${join(homedir(), "
|
|
457
|
+
`Wrote ${join(facultStateDir(homedir()), "audit", "agent-latest.json")}`
|
|
458
458
|
);
|
|
459
459
|
}
|
|
460
460
|
if (summaries.length) {
|
|
@@ -524,7 +524,7 @@ export async function auditTuiCommand(argv: string[]) {
|
|
|
524
524
|
{
|
|
525
525
|
value: "quarantine",
|
|
526
526
|
label: "Quarantine items",
|
|
527
|
-
hint: "move/copy to ~/.facult/quarantine",
|
|
527
|
+
hint: "move/copy to ~/.ai/.facult/quarantine",
|
|
528
528
|
},
|
|
529
529
|
{ value: "view", label: "View item details", hint: "inspect findings" },
|
|
530
530
|
{ value: "exit", label: "Exit", hint: "leave files unchanged" },
|
|
@@ -617,7 +617,7 @@ export async function auditTuiCommand(argv: string[]) {
|
|
|
617
617
|
|
|
618
618
|
const ts = new Date().toISOString();
|
|
619
619
|
const stamp = ts.replace(/[:.]/g, "-");
|
|
620
|
-
const destDir = join(homedir(), "
|
|
620
|
+
const destDir = join(facultStateDir(homedir()), "quarantine", stamp);
|
|
621
621
|
|
|
622
622
|
const plan = await quarantineItems({
|
|
623
623
|
items,
|
package/src/autosync.ts
CHANGED
|
@@ -4,7 +4,12 @@ import { homedir, hostname } from "node:os";
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
6
6
|
import { syncManagedTools } from "./manage";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
facultRootDir,
|
|
9
|
+
facultStateDir,
|
|
10
|
+
legacyFacultStateDirForRoot,
|
|
11
|
+
projectRootFromAiRoot,
|
|
12
|
+
} from "./paths";
|
|
8
13
|
|
|
9
14
|
const AUTOSYNC_VERSION = 1 as const;
|
|
10
15
|
const DEFAULT_DEBOUNCE_MS = 1500;
|
|
@@ -94,20 +99,33 @@ function runDetached(context: string, promise: Promise<void>) {
|
|
|
94
99
|
});
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function autosyncDir(home: string): string {
|
|
98
|
-
return join(facultStateDir(home), "autosync");
|
|
102
|
+
function autosyncDir(home: string, rootDir?: string): string {
|
|
103
|
+
return join(facultStateDir(home, rootDir), "autosync");
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
function
|
|
102
|
-
|
|
106
|
+
function legacyAutosyncDir(home: string, rootDir?: string): string {
|
|
107
|
+
const resolvedRoot = rootDir ?? facultRootDir(home);
|
|
108
|
+
return join(legacyFacultStateDirForRoot(resolvedRoot, home), "autosync");
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
function
|
|
106
|
-
return join(autosyncDir(home), "
|
|
111
|
+
function autosyncServicesDir(home: string, rootDir?: string): string {
|
|
112
|
+
return join(autosyncDir(home, rootDir), "services");
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
function
|
|
110
|
-
return join(
|
|
115
|
+
function legacyAutosyncServicesDir(home: string, rootDir?: string): string {
|
|
116
|
+
return join(legacyAutosyncDir(home, rootDir), "services");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function autosyncStateDir(home: string, rootDir?: string): string {
|
|
120
|
+
return join(autosyncDir(home, rootDir), "state");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function legacyAutosyncStateDir(home: string, rootDir?: string): string {
|
|
124
|
+
return join(legacyAutosyncDir(home, rootDir), "state");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function autosyncLogsDir(home: string, rootDir?: string): string {
|
|
128
|
+
return join(autosyncDir(home, rootDir), "logs");
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
function serviceSuffix(
|
|
@@ -151,12 +169,36 @@ function autosyncPlistPath(home: string, serviceName: string): string {
|
|
|
151
169
|
);
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
function autosyncConfigPath(
|
|
155
|
-
|
|
172
|
+
function autosyncConfigPath(
|
|
173
|
+
home: string,
|
|
174
|
+
serviceName: string,
|
|
175
|
+
rootDir?: string
|
|
176
|
+
): string {
|
|
177
|
+
return join(autosyncServicesDir(home, rootDir), `${serviceName}.json`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function legacyAutosyncConfigPath(
|
|
181
|
+
home: string,
|
|
182
|
+
serviceName: string,
|
|
183
|
+
rootDir?: string
|
|
184
|
+
): string {
|
|
185
|
+
return join(legacyAutosyncServicesDir(home, rootDir), `${serviceName}.json`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function autosyncRuntimeStatePath(
|
|
189
|
+
home: string,
|
|
190
|
+
serviceName: string,
|
|
191
|
+
rootDir?: string
|
|
192
|
+
): string {
|
|
193
|
+
return join(autosyncStateDir(home, rootDir), `${serviceName}.json`);
|
|
156
194
|
}
|
|
157
195
|
|
|
158
|
-
function
|
|
159
|
-
|
|
196
|
+
function legacyAutosyncRuntimeStatePath(
|
|
197
|
+
home: string,
|
|
198
|
+
serviceName: string,
|
|
199
|
+
rootDir?: string
|
|
200
|
+
): string {
|
|
201
|
+
return join(legacyAutosyncStateDir(home, rootDir), `${serviceName}.json`);
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
function escapeXml(value: string): string {
|
|
@@ -204,7 +246,7 @@ export function buildLaunchAgentSpec(args: {
|
|
|
204
246
|
const { homeDir, rootDir, serviceName } = args;
|
|
205
247
|
const label = autosyncLabel(serviceName);
|
|
206
248
|
const invocation = args.invocation ?? resolveAutosyncInvocation();
|
|
207
|
-
const logsDir = autosyncLogsDir(homeDir);
|
|
249
|
+
const logsDir = autosyncLogsDir(homeDir, rootDir);
|
|
208
250
|
|
|
209
251
|
return {
|
|
210
252
|
label,
|
|
@@ -281,34 +323,58 @@ async function writeJsonFile(pathValue: string, data: unknown): Promise<void> {
|
|
|
281
323
|
|
|
282
324
|
export async function loadAutosyncConfig(
|
|
283
325
|
serviceName: string,
|
|
284
|
-
homeDir: string = homedir()
|
|
326
|
+
homeDir: string = homedir(),
|
|
327
|
+
rootDir?: string
|
|
285
328
|
): Promise<AutosyncServiceConfig | null> {
|
|
286
|
-
|
|
287
|
-
autosyncConfigPath(homeDir, serviceName)
|
|
288
|
-
|
|
329
|
+
const candidates = [
|
|
330
|
+
autosyncConfigPath(homeDir, serviceName, rootDir),
|
|
331
|
+
legacyAutosyncConfigPath(homeDir, serviceName, rootDir),
|
|
332
|
+
];
|
|
333
|
+
for (const candidate of candidates) {
|
|
334
|
+
const config = await readJsonFile<AutosyncServiceConfig>(candidate);
|
|
335
|
+
if (config) {
|
|
336
|
+
return config;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
289
340
|
}
|
|
290
341
|
|
|
291
342
|
async function saveAutosyncConfig(
|
|
292
343
|
config: AutosyncServiceConfig,
|
|
293
344
|
homeDir: string
|
|
294
345
|
): Promise<void> {
|
|
295
|
-
await writeJsonFile(
|
|
346
|
+
await writeJsonFile(
|
|
347
|
+
autosyncConfigPath(homeDir, config.name, config.rootDir),
|
|
348
|
+
config
|
|
349
|
+
);
|
|
296
350
|
}
|
|
297
351
|
|
|
298
352
|
export async function loadAutosyncRuntimeState(
|
|
299
353
|
serviceName: string,
|
|
300
|
-
homeDir: string = homedir()
|
|
354
|
+
homeDir: string = homedir(),
|
|
355
|
+
rootDir?: string
|
|
301
356
|
): Promise<AutosyncRuntimeState | null> {
|
|
302
|
-
|
|
303
|
-
autosyncRuntimeStatePath(homeDir, serviceName)
|
|
304
|
-
|
|
357
|
+
const candidates = [
|
|
358
|
+
autosyncRuntimeStatePath(homeDir, serviceName, rootDir),
|
|
359
|
+
legacyAutosyncRuntimeStatePath(homeDir, serviceName, rootDir),
|
|
360
|
+
];
|
|
361
|
+
for (const candidate of candidates) {
|
|
362
|
+
const state = await readJsonFile<AutosyncRuntimeState>(candidate);
|
|
363
|
+
if (state) {
|
|
364
|
+
return state;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
305
368
|
}
|
|
306
369
|
|
|
307
370
|
async function saveAutosyncRuntimeState(
|
|
308
371
|
state: AutosyncRuntimeState,
|
|
309
372
|
homeDir: string
|
|
310
373
|
): Promise<void> {
|
|
311
|
-
await writeJsonFile(
|
|
374
|
+
await writeJsonFile(
|
|
375
|
+
autosyncRuntimeStatePath(homeDir, state.service, state.rootDir),
|
|
376
|
+
state
|
|
377
|
+
);
|
|
312
378
|
}
|
|
313
379
|
|
|
314
380
|
async function runCommand(
|
|
@@ -535,7 +601,7 @@ export async function runAutosyncService(
|
|
|
535
601
|
|
|
536
602
|
const persistState = async (patch: Partial<AutosyncRuntimeState>) => {
|
|
537
603
|
const current =
|
|
538
|
-
(await loadAutosyncRuntimeState(config.name, home)) ??
|
|
604
|
+
(await loadAutosyncRuntimeState(config.name, home, config.rootDir)) ??
|
|
539
605
|
({
|
|
540
606
|
version: AUTOSYNC_VERSION,
|
|
541
607
|
service: config.name,
|
|
@@ -778,7 +844,7 @@ export async function installAutosyncService(args: {
|
|
|
778
844
|
const plist = buildLaunchAgentPlist(spec);
|
|
779
845
|
|
|
780
846
|
await mkdir(dirname(spec.plistPath), { recursive: true });
|
|
781
|
-
await mkdir(autosyncLogsDir(home), { recursive: true });
|
|
847
|
+
await mkdir(autosyncLogsDir(home, rootDir), { recursive: true });
|
|
782
848
|
await saveAutosyncConfig(config, home);
|
|
783
849
|
await writeFile(spec.plistPath, plist, "utf8");
|
|
784
850
|
|
|
@@ -804,14 +870,30 @@ export async function uninstallAutosyncService(args: {
|
|
|
804
870
|
|
|
805
871
|
await runLaunchctl(["bootout", `${domain}/${label}`]).catch(() => null);
|
|
806
872
|
await rm(autosyncPlistPath(home, serviceName), { force: true });
|
|
807
|
-
await rm(autosyncConfigPath(home, serviceName), { force: true });
|
|
873
|
+
await rm(autosyncConfigPath(home, serviceName, rootDir), { force: true });
|
|
808
874
|
}
|
|
809
875
|
|
|
810
876
|
export async function repairAutosyncServices(
|
|
811
|
-
homeDir: string = homedir()
|
|
877
|
+
homeDir: string = homedir(),
|
|
878
|
+
rootDir?: string
|
|
812
879
|
): Promise<boolean> {
|
|
813
|
-
const
|
|
814
|
-
const
|
|
880
|
+
const activeRoot = rootDir ?? facultRootDir(homeDir);
|
|
881
|
+
const serviceDirs = [
|
|
882
|
+
autosyncServicesDir(homeDir, activeRoot),
|
|
883
|
+
legacyAutosyncServicesDir(homeDir, activeRoot),
|
|
884
|
+
];
|
|
885
|
+
const seen = new Set<string>();
|
|
886
|
+
const files: string[] = [];
|
|
887
|
+
for (const dir of serviceDirs) {
|
|
888
|
+
const entries = await readdir(dir).catch(() => [] as string[]);
|
|
889
|
+
for (const entry of entries) {
|
|
890
|
+
if (seen.has(entry)) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
seen.add(entry);
|
|
894
|
+
files.push(entry);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
815
897
|
let changed = false;
|
|
816
898
|
|
|
817
899
|
for (const entry of files) {
|
|
@@ -819,7 +901,7 @@ export async function repairAutosyncServices(
|
|
|
819
901
|
continue;
|
|
820
902
|
}
|
|
821
903
|
const serviceName = basename(entry, ".json");
|
|
822
|
-
const config = await loadAutosyncConfig(serviceName, homeDir);
|
|
904
|
+
const config = await loadAutosyncConfig(serviceName, homeDir, activeRoot);
|
|
823
905
|
if (!config) {
|
|
824
906
|
continue;
|
|
825
907
|
}
|
|
@@ -843,7 +925,9 @@ export async function repairAutosyncServices(
|
|
|
843
925
|
);
|
|
844
926
|
if (currentText !== desired) {
|
|
845
927
|
await mkdir(dirname(spec.plistPath), { recursive: true });
|
|
846
|
-
await mkdir(autosyncLogsDir(homeDir), {
|
|
928
|
+
await mkdir(autosyncLogsDir(homeDir, config.rootDir), {
|
|
929
|
+
recursive: true,
|
|
930
|
+
});
|
|
847
931
|
await writeFile(spec.plistPath, desired, "utf8");
|
|
848
932
|
const domain = launchdDomain();
|
|
849
933
|
await runLaunchctl(["bootout", `${domain}/${spec.label}`]).catch(
|
|
@@ -872,8 +956,8 @@ export async function autosyncStatus(args: {
|
|
|
872
956
|
args.rootDir ??
|
|
873
957
|
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
874
958
|
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
875
|
-
const config = await loadAutosyncConfig(serviceName, home);
|
|
876
|
-
const state = await loadAutosyncRuntimeState(serviceName, home);
|
|
959
|
+
const config = await loadAutosyncConfig(serviceName, home, rootDir);
|
|
960
|
+
const state = await loadAutosyncRuntimeState(serviceName, home, rootDir);
|
|
877
961
|
const plistPath = autosyncPlistPath(home, serviceName);
|
|
878
962
|
const plistExists = await pathExists(plistPath);
|
|
879
963
|
const label = autosyncLabel(serviceName);
|