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 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. Generated state lives next to the active canonical root:
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
- .facult/
369
- ai/
370
- index.json
371
- graph.json
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. `~/.ai/config.toml`
412
- 3. `~/.ai/config.local.toml`
413
- 4. `~/.ai/projects/<slug>/config.toml`
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: `~/.facult/ai/projects/<slug>/...`
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, and proposal records stay outside the canonical git-backed tree by default
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Manage coding-agent skills and MCP configs across tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 { facultAiGraphPath, facultAiIndexPath } from "./paths";
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,
@@ -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, ".facult", "audit");
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(), ".facult", "audit", "agent-latest.json")}`
1043
+ `Wrote ${join(facultStateDir(homedir()), "audit", "agent-latest.json")}`
1044
1044
  );
1045
1045
  }
1046
1046
 
@@ -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, ".facult", "audit-rules.yaml");
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, ".facult", "audit");
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(), ".facult", "audit", "static-latest.json")}`
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(), ".facult", "audit", "static-latest.json")}`
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(), ".facult", "audit", "agent-latest.json")}`
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(), ".facult", "quarantine", stamp);
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 { facultRootDir, facultStateDir, projectRootFromAiRoot } from "./paths";
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 autosyncServicesDir(home: string): string {
102
- return join(autosyncDir(home), "services");
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 autosyncStateDir(home: string): string {
106
- return join(autosyncDir(home), "state");
111
+ function autosyncServicesDir(home: string, rootDir?: string): string {
112
+ return join(autosyncDir(home, rootDir), "services");
107
113
  }
108
114
 
109
- function autosyncLogsDir(home: string): string {
110
- return join(autosyncDir(home), "logs");
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(home: string, serviceName: string): string {
155
- return join(autosyncServicesDir(home), `${serviceName}.json`);
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 autosyncRuntimeStatePath(home: string, serviceName: string): string {
159
- return join(autosyncStateDir(home), `${serviceName}.json`);
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
- return await readJsonFile<AutosyncServiceConfig>(
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(autosyncConfigPath(homeDir, config.name), config);
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
- return await readJsonFile<AutosyncRuntimeState>(
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(autosyncRuntimeStatePath(homeDir, state.service), state);
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 servicesDir = autosyncServicesDir(homeDir);
814
- const files = await readdir(servicesDir).catch(() => [] as string[]);
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), { recursive: true });
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);