facult 2.3.0 → 2.4.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 +30 -10
- package/bin/fclt.cjs +96 -9
- package/package.json +1 -1
- package/src/autosync.ts +192 -8
- package/src/index-builder.ts +2 -2
- package/src/manage.ts +392 -6
- package/src/paths.ts +69 -0
- package/src/self-update.ts +4 -2
package/README.md
CHANGED
|
@@ -260,9 +260,17 @@ fclt index
|
|
|
260
260
|
|
|
261
261
|
Why `keep-current`: it is deterministic and non-interactive for duplicate sources.
|
|
262
262
|
|
|
263
|
-
Canonical source root: `~/.ai` for global work, or `<repo>/.ai` for project-local work.
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
Canonical source root: `~/.ai` for global work, or `<repo>/.ai` for project-local work.
|
|
264
|
+
|
|
265
|
+
Generated AI state that belongs with the canonical root lives inside that root:
|
|
266
|
+
- global: `~/.ai/.facult/ai/...`
|
|
267
|
+
- project: `<repo>/.ai/.facult/ai/...`
|
|
268
|
+
|
|
269
|
+
Machine-local operational state lives outside the canonical root:
|
|
270
|
+
- macOS state: `~/Library/Application Support/fclt/...`
|
|
271
|
+
- macOS cache: `~/Library/Caches/fclt/...`
|
|
272
|
+
- Linux/other state: `${XDG_STATE_HOME:-~/.local/state}/fclt/...`
|
|
273
|
+
- Linux/other cache: `${XDG_CACHE_HOME:-~/.cache}/fclt/...`
|
|
266
274
|
|
|
267
275
|
### 3b. Bootstrap a repo-local `.ai`
|
|
268
276
|
|
|
@@ -420,7 +428,8 @@ Typical layout:
|
|
|
420
428
|
|
|
421
429
|
Important split:
|
|
422
430
|
- `.ai/` is canonical source
|
|
423
|
-
- `.ai/.facult/` is
|
|
431
|
+
- `.ai/.facult/ai/` is generated AI state that belongs with the canonical root
|
|
432
|
+
- machine-local Facult state such as managed-tool state, autosync runtime/config, install metadata, and launcher caches lives outside `.ai/`
|
|
424
433
|
- tool homes such as `.codex/` and `.claude/` are rendered outputs
|
|
425
434
|
- the generated capability graph lives at `.ai/.facult/ai/graph.json`
|
|
426
435
|
|
|
@@ -714,6 +723,13 @@ Files are written to:
|
|
|
714
723
|
- `~/.codex/automations/<name>/automation.toml`
|
|
715
724
|
- `~/.codex/automations/<name>/memory.md`
|
|
716
725
|
|
|
726
|
+
When Codex is in managed mode, canonical automation sources live under:
|
|
727
|
+
|
|
728
|
+
- `~/.ai/automations/<name>/...` for global automation state
|
|
729
|
+
- `<repo>/.ai/automations/<name>/...` for project-scoped canonical state
|
|
730
|
+
|
|
731
|
+
Managed sync renders those canonical automation directories into the shared live Codex automation store at `~/.codex/automations/` and only removes automation files that were previously rendered by the same canonical root.
|
|
732
|
+
|
|
717
733
|
Example project automation:
|
|
718
734
|
|
|
719
735
|
```bash
|
|
@@ -774,17 +790,21 @@ fclt <command> --help
|
|
|
774
790
|
|
|
775
791
|
### State and report files
|
|
776
792
|
|
|
777
|
-
Under `~/.ai/.facult
|
|
793
|
+
Under canonical generated AI state (`~/.ai/.facult/` or `<repo>/.ai/.facult/`):
|
|
778
794
|
- `sources.json` (latest inventory scan state)
|
|
779
795
|
- `consolidated.json` (consolidation state)
|
|
780
|
-
- `managed.json` (managed tool state)
|
|
781
796
|
- `ai/index.json` (generated canonical AI inventory)
|
|
782
797
|
- `audit/static-latest.json` (latest static audit report)
|
|
783
798
|
- `audit/agent-latest.json` (latest agent audit report)
|
|
784
799
|
- `trust/sources.json` (source trust policy state)
|
|
785
|
-
|
|
786
|
-
-
|
|
787
|
-
- `
|
|
800
|
+
|
|
801
|
+
Under machine-local Facult state:
|
|
802
|
+
- `install.json` (machine-local install metadata)
|
|
803
|
+
- `global/managed.json` or `projects/<slug-hash>/managed.json` (managed tool state)
|
|
804
|
+
- `.../autosync/services/*.json` (autosync service configs)
|
|
805
|
+
- `.../autosync/state/*.json` (autosync runtime state)
|
|
806
|
+
- `.../autosync/logs/*` (autosync service logs)
|
|
807
|
+
- `runtime/<version>/<platform-arch>/...` under the machine-local cache root (npm launcher binary cache)
|
|
788
808
|
|
|
789
809
|
### Config reference
|
|
790
810
|
|
|
@@ -890,7 +910,7 @@ Release behavior:
|
|
|
890
910
|
4. npm publish runs only after binary asset upload succeeds (`publish-npm` depends on `publish-assets`).
|
|
891
911
|
5. Published release assets include platform binaries, `fclt-install.sh`, `facult-install.sh`, and `SHA256SUMS`.
|
|
892
912
|
6. When `HOMEBREW_TAP_TOKEN` is configured, the release workflow also updates the Homebrew tap at `hack-dance/homebrew-tap`.
|
|
893
|
-
7. The npm package launcher resolves your platform, downloads the matching release binary, caches it under
|
|
913
|
+
7. The npm package launcher resolves your platform, downloads the matching release binary, caches it under the machine-local cache root (`~/Library/Caches/fclt/runtime/...` on macOS or `${XDG_CACHE_HOME:-~/.cache}/fclt/runtime/...` elsewhere), and runs it.
|
|
894
914
|
|
|
895
915
|
Current prebuilt binary targets:
|
|
896
916
|
- `darwin-x64`
|
package/bin/fclt.cjs
CHANGED
|
@@ -16,6 +16,43 @@ const PACKAGE_NAME = "facult";
|
|
|
16
16
|
const DOWNLOAD_RETRIES = 12;
|
|
17
17
|
const DOWNLOAD_RETRY_DELAY_MS = 5000;
|
|
18
18
|
|
|
19
|
+
function isHelpLikeArgs(args) {
|
|
20
|
+
return (
|
|
21
|
+
args.length === 0 ||
|
|
22
|
+
args.includes("--help") ||
|
|
23
|
+
args.includes("-h") ||
|
|
24
|
+
args[0] === "help"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function localStateRoot(home) {
|
|
29
|
+
const override = String(process.env.FACULT_LOCAL_STATE_DIR || "").trim();
|
|
30
|
+
if (override) {
|
|
31
|
+
return path.resolve(override);
|
|
32
|
+
}
|
|
33
|
+
if (process.platform === "darwin") {
|
|
34
|
+
return path.join(home, "Library", "Application Support", "fclt");
|
|
35
|
+
}
|
|
36
|
+
const xdg = String(process.env.XDG_STATE_HOME || "").trim();
|
|
37
|
+
return xdg
|
|
38
|
+
? path.join(path.resolve(xdg), "fclt")
|
|
39
|
+
: path.join(home, ".local", "state", "fclt");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function localCacheRoot(home) {
|
|
43
|
+
const override = String(process.env.FACULT_CACHE_DIR || "").trim();
|
|
44
|
+
if (override) {
|
|
45
|
+
return path.resolve(override);
|
|
46
|
+
}
|
|
47
|
+
if (process.platform === "darwin") {
|
|
48
|
+
return path.join(home, "Library", "Caches", "fclt");
|
|
49
|
+
}
|
|
50
|
+
const xdg = String(process.env.XDG_CACHE_HOME || "").trim();
|
|
51
|
+
return xdg
|
|
52
|
+
? path.join(path.resolve(xdg), "fclt")
|
|
53
|
+
: path.join(home, ".cache", "fclt");
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
async function main() {
|
|
20
57
|
const resolved = resolveTarget();
|
|
21
58
|
if (!resolved.ok) {
|
|
@@ -30,7 +67,7 @@ async function main() {
|
|
|
30
67
|
}
|
|
31
68
|
|
|
32
69
|
const home = os.homedir();
|
|
33
|
-
const cacheRoot = path.join(home, "
|
|
70
|
+
const cacheRoot = path.join(localCacheRoot(home), "runtime");
|
|
34
71
|
const installDir = path.join(
|
|
35
72
|
cacheRoot,
|
|
36
73
|
version,
|
|
@@ -39,8 +76,34 @@ async function main() {
|
|
|
39
76
|
const binaryName = resolved.platform === "windows" ? "fclt.exe" : "fclt";
|
|
40
77
|
const binaryPath = path.join(installDir, binaryName);
|
|
41
78
|
const sourceEntry = path.join(__dirname, "..", "src", "index.ts");
|
|
79
|
+
const args = process.argv.slice(2);
|
|
80
|
+
let installedBinaryThisRun = false;
|
|
42
81
|
|
|
43
82
|
if (!(await fileExists(binaryPath))) {
|
|
83
|
+
const packageManager = detectPackageManager();
|
|
84
|
+
const hasSourceFallback = await canUseSourceFallback(sourceEntry);
|
|
85
|
+
const incompleteCache = await hasIncompleteRuntimeCache({
|
|
86
|
+
installDir,
|
|
87
|
+
binaryName,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (incompleteCache) {
|
|
91
|
+
await removeIncompleteRuntimeTemps({ installDir, binaryName });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (hasSourceFallback && (incompleteCache || isHelpLikeArgs(args))) {
|
|
95
|
+
return runSourceFallback({
|
|
96
|
+
sourceEntry,
|
|
97
|
+
version,
|
|
98
|
+
packageManager,
|
|
99
|
+
reason: new Error(
|
|
100
|
+
incompleteCache
|
|
101
|
+
? "incomplete cached runtime download"
|
|
102
|
+
: "runtime binary missing for help-like command"
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
44
107
|
const tag = `v${version}`;
|
|
45
108
|
const assetName = `${PACKAGE_NAME}-${version}-${resolved.platform}-${resolved.arch}${resolved.ext}`;
|
|
46
109
|
const url = `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/download/${tag}/${assetName}`;
|
|
@@ -56,6 +119,7 @@ async function main() {
|
|
|
56
119
|
await fsp.chmod(tmpPath, 0o755);
|
|
57
120
|
}
|
|
58
121
|
await fsp.rename(tmpPath, binaryPath);
|
|
122
|
+
installedBinaryThisRun = true;
|
|
59
123
|
} catch (error) {
|
|
60
124
|
await safeUnlink(tmpPath);
|
|
61
125
|
if (await canUseSourceFallback(sourceEntry)) {
|
|
@@ -84,14 +148,15 @@ async function main() {
|
|
|
84
148
|
}
|
|
85
149
|
|
|
86
150
|
const packageManager = detectPackageManager();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
151
|
+
if (installedBinaryThisRun) {
|
|
152
|
+
await bestEffortWriteInstallState({
|
|
153
|
+
method: "npm-binary-cache",
|
|
154
|
+
version,
|
|
155
|
+
binaryPath,
|
|
156
|
+
packageManager,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
93
159
|
|
|
94
|
-
const args = process.argv.slice(2);
|
|
95
160
|
const result = spawnSync(binaryPath, args, {
|
|
96
161
|
stdio: "inherit",
|
|
97
162
|
env: {
|
|
@@ -277,6 +342,28 @@ async function fileExists(filePath) {
|
|
|
277
342
|
}
|
|
278
343
|
}
|
|
279
344
|
|
|
345
|
+
async function hasIncompleteRuntimeCache({ installDir, binaryName }) {
|
|
346
|
+
try {
|
|
347
|
+
const entries = await fsp.readdir(installDir);
|
|
348
|
+
return entries.some((entry) => entry.startsWith(`${binaryName}.tmp-`));
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function removeIncompleteRuntimeTemps({ installDir, binaryName }) {
|
|
355
|
+
try {
|
|
356
|
+
const entries = await fsp.readdir(installDir);
|
|
357
|
+
await Promise.all(
|
|
358
|
+
entries
|
|
359
|
+
.filter((entry) => entry.startsWith(`${binaryName}.tmp-`))
|
|
360
|
+
.map((entry) => safeUnlink(path.join(installDir, entry)))
|
|
361
|
+
);
|
|
362
|
+
} catch {
|
|
363
|
+
// Ignore missing runtime dirs while cleaning stale temp files.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
280
367
|
async function safeUnlink(filePath) {
|
|
281
368
|
try {
|
|
282
369
|
await fsp.unlink(filePath);
|
|
@@ -291,7 +378,7 @@ function sleep(ms) {
|
|
|
291
378
|
|
|
292
379
|
async function writeInstallState(state) {
|
|
293
380
|
const home = os.homedir();
|
|
294
|
-
const installStateDir =
|
|
381
|
+
const installStateDir = localStateRoot(home);
|
|
295
382
|
const installStatePath = path.join(installStateDir, "install.json");
|
|
296
383
|
await fsp.mkdir(installStateDir, { recursive: true });
|
|
297
384
|
await fsp.writeFile(
|
package/package.json
CHANGED
package/src/autosync.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
|
|
6
6
|
import { syncManagedTools } from "./manage";
|
|
7
7
|
import {
|
|
8
|
+
facultMachineStateDir,
|
|
8
9
|
facultRootDir,
|
|
9
10
|
facultStateDir,
|
|
10
11
|
legacyFacultStateDirForRoot,
|
|
@@ -84,6 +85,10 @@ interface GitSyncOutcome {
|
|
|
84
85
|
message?: string;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
let launchctlRunnerForTests:
|
|
89
|
+
| ((args: string[]) => Promise<CommandResult>)
|
|
90
|
+
| null = null;
|
|
91
|
+
|
|
87
92
|
function nowIso(): string {
|
|
88
93
|
return new Date().toISOString();
|
|
89
94
|
}
|
|
@@ -100,6 +105,10 @@ function runDetached(context: string, promise: Promise<void>) {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
function autosyncDir(home: string, rootDir?: string): string {
|
|
108
|
+
return join(facultMachineStateDir(home, rootDir), "autosync");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function canonicalAutosyncDir(home: string, rootDir?: string): string {
|
|
103
112
|
return join(facultStateDir(home, rootDir), "autosync");
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -155,11 +164,21 @@ function autosyncServiceName(
|
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
function autosyncLabel(serviceName: string): string {
|
|
167
|
+
return serviceName === "all"
|
|
168
|
+
? "com.fclt.autosync"
|
|
169
|
+
: `com.fclt.autosync.${serviceName}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function legacyAutosyncLabel(serviceName: string): string {
|
|
158
173
|
return serviceName === "all"
|
|
159
174
|
? "com.facult.autosync"
|
|
160
175
|
: `com.facult.autosync.${serviceName}`;
|
|
161
176
|
}
|
|
162
177
|
|
|
178
|
+
function autosyncLabelCandidates(serviceName: string): string[] {
|
|
179
|
+
return [autosyncLabel(serviceName), legacyAutosyncLabel(serviceName)];
|
|
180
|
+
}
|
|
181
|
+
|
|
163
182
|
function autosyncPlistPath(home: string, serviceName: string): string {
|
|
164
183
|
return join(
|
|
165
184
|
home,
|
|
@@ -331,6 +350,11 @@ export async function loadAutosyncConfig(
|
|
|
331
350
|
): Promise<AutosyncServiceConfig | null> {
|
|
332
351
|
const candidates = [
|
|
333
352
|
autosyncConfigPath(homeDir, serviceName, rootDir),
|
|
353
|
+
join(
|
|
354
|
+
canonicalAutosyncDir(homeDir, rootDir),
|
|
355
|
+
"services",
|
|
356
|
+
`${serviceName}.json`
|
|
357
|
+
),
|
|
334
358
|
legacyAutosyncConfigPath(homeDir, serviceName, rootDir),
|
|
335
359
|
];
|
|
336
360
|
for (const candidate of candidates) {
|
|
@@ -359,6 +383,11 @@ export async function loadAutosyncRuntimeState(
|
|
|
359
383
|
): Promise<AutosyncRuntimeState | null> {
|
|
360
384
|
const candidates = [
|
|
361
385
|
autosyncRuntimeStatePath(homeDir, serviceName, rootDir),
|
|
386
|
+
join(
|
|
387
|
+
canonicalAutosyncDir(homeDir, rootDir),
|
|
388
|
+
"state",
|
|
389
|
+
`${serviceName}.json`
|
|
390
|
+
),
|
|
362
391
|
legacyAutosyncRuntimeStatePath(homeDir, serviceName, rootDir),
|
|
363
392
|
];
|
|
364
393
|
for (const candidate of candidates) {
|
|
@@ -399,9 +428,18 @@ async function runCommand(
|
|
|
399
428
|
}
|
|
400
429
|
|
|
401
430
|
async function runLaunchctl(args: string[]): Promise<CommandResult> {
|
|
431
|
+
if (launchctlRunnerForTests) {
|
|
432
|
+
return await launchctlRunnerForTests(args);
|
|
433
|
+
}
|
|
402
434
|
return await runCommand(["launchctl", ...args]);
|
|
403
435
|
}
|
|
404
436
|
|
|
437
|
+
export function setLaunchctlRunnerForTests(
|
|
438
|
+
runner: ((args: string[]) => Promise<CommandResult>) | null
|
|
439
|
+
) {
|
|
440
|
+
launchctlRunnerForTests = runner;
|
|
441
|
+
}
|
|
442
|
+
|
|
405
443
|
function launchdDomain(): string {
|
|
406
444
|
return `gui/${process.getuid?.() ?? process.geteuid?.() ?? 0}`;
|
|
407
445
|
}
|
|
@@ -470,6 +508,122 @@ async function ensureGitRepo(repoDir: string): Promise<boolean> {
|
|
|
470
508
|
return await pathExists(join(repoDir, ".git"));
|
|
471
509
|
}
|
|
472
510
|
|
|
511
|
+
async function cleanupAutosyncLaunchAgentArtifacts(args: {
|
|
512
|
+
homeDir: string;
|
|
513
|
+
serviceName: string;
|
|
514
|
+
}) {
|
|
515
|
+
const domain = launchdDomain();
|
|
516
|
+
for (const label of autosyncLabelCandidates(args.serviceName)) {
|
|
517
|
+
await runLaunchctl(["bootout", `${domain}/${label}`]).catch(() => null);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const legacyPlistPath = join(
|
|
521
|
+
args.homeDir,
|
|
522
|
+
"Library",
|
|
523
|
+
"LaunchAgents",
|
|
524
|
+
`${legacyAutosyncLabel(args.serviceName)}.plist`
|
|
525
|
+
);
|
|
526
|
+
if (legacyPlistPath !== autosyncPlistPath(args.homeDir, args.serviceName)) {
|
|
527
|
+
await rm(legacyPlistPath, { force: true });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function cleanupLegacyAutosyncFiles(args: {
|
|
532
|
+
homeDir: string;
|
|
533
|
+
serviceName: string;
|
|
534
|
+
rootDir: string;
|
|
535
|
+
}) {
|
|
536
|
+
const legacyPaths = [
|
|
537
|
+
join(
|
|
538
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
539
|
+
"services",
|
|
540
|
+
`${args.serviceName}.json`
|
|
541
|
+
),
|
|
542
|
+
join(
|
|
543
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
544
|
+
"state",
|
|
545
|
+
`${args.serviceName}.json`
|
|
546
|
+
),
|
|
547
|
+
join(
|
|
548
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
549
|
+
"logs",
|
|
550
|
+
`${args.serviceName}.log`
|
|
551
|
+
),
|
|
552
|
+
join(
|
|
553
|
+
canonicalAutosyncDir(args.homeDir, args.rootDir),
|
|
554
|
+
"logs",
|
|
555
|
+
`${args.serviceName}.err.log`
|
|
556
|
+
),
|
|
557
|
+
legacyAutosyncConfigPath(args.homeDir, args.serviceName, args.rootDir),
|
|
558
|
+
legacyAutosyncRuntimeStatePath(
|
|
559
|
+
args.homeDir,
|
|
560
|
+
args.serviceName,
|
|
561
|
+
args.rootDir
|
|
562
|
+
),
|
|
563
|
+
];
|
|
564
|
+
for (const candidate of legacyPaths) {
|
|
565
|
+
await rm(candidate, { force: true }).catch(() => null);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const AUTOSYNC_REBUILDABLE_PATHS = [
|
|
570
|
+
".facult/ai/index.json",
|
|
571
|
+
".facult/ai/graph.json",
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
const AUTOSYNC_MACHINE_LOCAL_LEGACY_PATHS = [
|
|
575
|
+
".facult/managed.json",
|
|
576
|
+
".facult/install.json",
|
|
577
|
+
".facult/autosync",
|
|
578
|
+
".facult/runtime",
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
async function gitListTrackedPaths(
|
|
582
|
+
repoDir: string,
|
|
583
|
+
pathValue: string
|
|
584
|
+
): Promise<string[]> {
|
|
585
|
+
const result = await runCommand(["git", "ls-files", "-z", "--", pathValue], {
|
|
586
|
+
cwd: repoDir,
|
|
587
|
+
});
|
|
588
|
+
if (result.exitCode !== 0 || !result.stdout) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
return result.stdout
|
|
592
|
+
.split("\0")
|
|
593
|
+
.map((value) => value.trim())
|
|
594
|
+
.filter(Boolean);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function cleanupAutosyncProtectedPaths(repoDir: string): Promise<void> {
|
|
598
|
+
const tracked = new Set<string>();
|
|
599
|
+
for (const pathValue of [
|
|
600
|
+
...AUTOSYNC_REBUILDABLE_PATHS,
|
|
601
|
+
...AUTOSYNC_MACHINE_LOCAL_LEGACY_PATHS,
|
|
602
|
+
]) {
|
|
603
|
+
for (const entry of await gitListTrackedPaths(repoDir, pathValue)) {
|
|
604
|
+
tracked.add(entry);
|
|
605
|
+
}
|
|
606
|
+
await rm(join(repoDir, pathValue), { force: true, recursive: true }).catch(
|
|
607
|
+
() => null
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (tracked.size > 0) {
|
|
612
|
+
await runCommand(
|
|
613
|
+
[
|
|
614
|
+
"git",
|
|
615
|
+
"restore",
|
|
616
|
+
"--staged",
|
|
617
|
+
"--worktree",
|
|
618
|
+
"--source=HEAD",
|
|
619
|
+
"--",
|
|
620
|
+
...tracked,
|
|
621
|
+
],
|
|
622
|
+
{ cwd: repoDir }
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
473
627
|
export async function runGitAutosyncOnce(args: {
|
|
474
628
|
config: AutosyncServiceConfig;
|
|
475
629
|
}): Promise<GitSyncOutcome> {
|
|
@@ -503,6 +657,8 @@ export async function runGitAutosyncOnce(args: {
|
|
|
503
657
|
};
|
|
504
658
|
}
|
|
505
659
|
|
|
660
|
+
await cleanupAutosyncProtectedPaths(repoDir);
|
|
661
|
+
|
|
506
662
|
const fetch = await runCommand(
|
|
507
663
|
["git", "fetch", config.git.remote, config.git.branch],
|
|
508
664
|
{ cwd: repoDir }
|
|
@@ -849,10 +1005,18 @@ export async function installAutosyncService(args: {
|
|
|
849
1005
|
await mkdir(dirname(spec.plistPath), { recursive: true });
|
|
850
1006
|
await mkdir(autosyncLogsDir(home, rootDir), { recursive: true });
|
|
851
1007
|
await saveAutosyncConfig(config, home);
|
|
1008
|
+
await cleanupLegacyAutosyncFiles({
|
|
1009
|
+
homeDir: home,
|
|
1010
|
+
serviceName,
|
|
1011
|
+
rootDir: config.rootDir,
|
|
1012
|
+
});
|
|
1013
|
+
await cleanupAutosyncLaunchAgentArtifacts({
|
|
1014
|
+
homeDir: home,
|
|
1015
|
+
serviceName,
|
|
1016
|
+
});
|
|
852
1017
|
await writeFile(spec.plistPath, plist, "utf8");
|
|
853
1018
|
|
|
854
1019
|
const domain = launchdDomain();
|
|
855
|
-
await runLaunchctl(["bootout", `${domain}/${spec.label}`]).catch(() => null);
|
|
856
1020
|
await runLaunchctl(["bootstrap", domain, spec.plistPath]);
|
|
857
1021
|
await runLaunchctl(["kickstart", "-k", `${domain}/${spec.label}`]);
|
|
858
1022
|
return config;
|
|
@@ -868,10 +1032,16 @@ export async function uninstallAutosyncService(args: {
|
|
|
868
1032
|
args.rootDir ??
|
|
869
1033
|
resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
|
|
870
1034
|
const serviceName = autosyncServiceName(args.tool, rootDir, home);
|
|
871
|
-
const label = autosyncLabel(serviceName);
|
|
872
|
-
const domain = launchdDomain();
|
|
873
1035
|
|
|
874
|
-
await
|
|
1036
|
+
await cleanupAutosyncLaunchAgentArtifacts({
|
|
1037
|
+
homeDir: home,
|
|
1038
|
+
serviceName,
|
|
1039
|
+
});
|
|
1040
|
+
await cleanupLegacyAutosyncFiles({
|
|
1041
|
+
homeDir: home,
|
|
1042
|
+
serviceName,
|
|
1043
|
+
rootDir,
|
|
1044
|
+
});
|
|
875
1045
|
await rm(autosyncPlistPath(home, serviceName), { force: true });
|
|
876
1046
|
await rm(autosyncConfigPath(home, serviceName, rootDir), { force: true });
|
|
877
1047
|
}
|
|
@@ -883,6 +1053,7 @@ export async function repairAutosyncServices(
|
|
|
883
1053
|
const activeRoot = rootDir ?? facultRootDir(homeDir);
|
|
884
1054
|
const serviceDirs = [
|
|
885
1055
|
autosyncServicesDir(homeDir, activeRoot),
|
|
1056
|
+
join(canonicalAutosyncDir(homeDir, activeRoot), "services"),
|
|
886
1057
|
legacyAutosyncServicesDir(homeDir, activeRoot),
|
|
887
1058
|
];
|
|
888
1059
|
const seen = new Set<string>();
|
|
@@ -916,6 +1087,11 @@ export async function repairAutosyncServices(
|
|
|
916
1087
|
await saveAutosyncConfig(config, homeDir);
|
|
917
1088
|
changed = true;
|
|
918
1089
|
}
|
|
1090
|
+
await cleanupLegacyAutosyncFiles({
|
|
1091
|
+
homeDir,
|
|
1092
|
+
serviceName,
|
|
1093
|
+
rootDir: config.rootDir,
|
|
1094
|
+
});
|
|
919
1095
|
|
|
920
1096
|
const spec = buildLaunchAgentSpec({
|
|
921
1097
|
homeDir,
|
|
@@ -923,19 +1099,27 @@ export async function repairAutosyncServices(
|
|
|
923
1099
|
rootDir: config.rootDir,
|
|
924
1100
|
});
|
|
925
1101
|
const desired = buildLaunchAgentPlist(spec);
|
|
1102
|
+
const legacyPlistPath = join(
|
|
1103
|
+
homeDir,
|
|
1104
|
+
"Library",
|
|
1105
|
+
"LaunchAgents",
|
|
1106
|
+
`${legacyAutosyncLabel(serviceName)}.plist`
|
|
1107
|
+
);
|
|
926
1108
|
const currentText = await readFile(spec.plistPath, "utf8").catch(
|
|
927
1109
|
() => null
|
|
928
1110
|
);
|
|
929
|
-
|
|
1111
|
+
const legacyExists = await pathExists(legacyPlistPath);
|
|
1112
|
+
if (currentText !== desired || legacyExists) {
|
|
930
1113
|
await mkdir(dirname(spec.plistPath), { recursive: true });
|
|
931
1114
|
await mkdir(autosyncLogsDir(homeDir, config.rootDir), {
|
|
932
1115
|
recursive: true,
|
|
933
1116
|
});
|
|
1117
|
+
await cleanupAutosyncLaunchAgentArtifacts({
|
|
1118
|
+
homeDir,
|
|
1119
|
+
serviceName,
|
|
1120
|
+
});
|
|
934
1121
|
await writeFile(spec.plistPath, desired, "utf8");
|
|
935
1122
|
const domain = launchdDomain();
|
|
936
|
-
await runLaunchctl(["bootout", `${domain}/${spec.label}`]).catch(
|
|
937
|
-
() => null
|
|
938
|
-
);
|
|
939
1123
|
await runLaunchctl(["bootstrap", domain, spec.plistPath]).catch(
|
|
940
1124
|
() => null
|
|
941
1125
|
);
|
package/src/index-builder.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
facultAiGraphPath,
|
|
16
16
|
facultAiIndexPath,
|
|
17
|
-
|
|
17
|
+
facultMachineStateDir,
|
|
18
18
|
facultRootDir,
|
|
19
19
|
projectRootFromAiRoot,
|
|
20
20
|
projectSlugFromAiRoot,
|
|
@@ -987,7 +987,7 @@ async function readManagedState(
|
|
|
987
987
|
rootDir: string
|
|
988
988
|
): Promise<ManagedStateLite | null> {
|
|
989
989
|
const statePath = join(
|
|
990
|
-
|
|
990
|
+
facultMachineStateDir(homeDir, rootDir),
|
|
991
991
|
"managed.json"
|
|
992
992
|
);
|
|
993
993
|
try {
|
package/src/manage.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type SkillEntry,
|
|
33
33
|
} from "./index-builder";
|
|
34
34
|
import {
|
|
35
|
-
|
|
35
|
+
facultMachineStateDir,
|
|
36
36
|
facultRootDir,
|
|
37
37
|
legacyFacultStateDirForRoot,
|
|
38
38
|
projectRootFromAiRoot,
|
|
@@ -44,6 +44,7 @@ export interface ManagedToolState {
|
|
|
44
44
|
skillsDir?: string;
|
|
45
45
|
mcpConfig?: string;
|
|
46
46
|
agentsDir?: string;
|
|
47
|
+
automationDir?: string;
|
|
47
48
|
toolHome?: string;
|
|
48
49
|
globalAgentsPath?: string;
|
|
49
50
|
globalAgentsOverridePath?: string;
|
|
@@ -75,6 +76,7 @@ export interface ToolPaths {
|
|
|
75
76
|
skillsDir?: string;
|
|
76
77
|
mcpConfig?: string;
|
|
77
78
|
agentsDir?: string;
|
|
79
|
+
automationDir?: string;
|
|
78
80
|
toolHome?: string;
|
|
79
81
|
rulesDir?: string;
|
|
80
82
|
toolConfig?: string;
|
|
@@ -163,6 +165,7 @@ function defaultToolPaths(
|
|
|
163
165
|
skillsDir: toolBase(".codex", "skills"),
|
|
164
166
|
mcpConfig: toolBase(".codex", "mcp.json"),
|
|
165
167
|
agentsDir: toolBase(".codex", "agents"),
|
|
168
|
+
automationDir: homePath(home, ".codex", "automations"),
|
|
166
169
|
toolHome: toolBase(".codex"),
|
|
167
170
|
rulesDir: toolBase(".codex", "rules"),
|
|
168
171
|
toolConfig: toolBase(".codex", "config.toml"),
|
|
@@ -293,7 +296,7 @@ export function managedStatePathForRoot(
|
|
|
293
296
|
home: string = homedir(),
|
|
294
297
|
rootDir?: string
|
|
295
298
|
): string {
|
|
296
|
-
return join(
|
|
299
|
+
return join(facultMachineStateDir(home, rootDir), "managed.json");
|
|
297
300
|
}
|
|
298
301
|
|
|
299
302
|
function legacyManagedStatePathForRoot(
|
|
@@ -336,7 +339,7 @@ export async function saveManagedState(
|
|
|
336
339
|
home: string = homedir(),
|
|
337
340
|
rootDir?: string
|
|
338
341
|
) {
|
|
339
|
-
const dir =
|
|
342
|
+
const dir = facultMachineStateDir(home, rootDir);
|
|
340
343
|
await ensureDir(dir);
|
|
341
344
|
await Bun.write(
|
|
342
345
|
managedStatePathForRoot(home, rootDir),
|
|
@@ -433,6 +436,104 @@ async function loadCanonicalAgents(
|
|
|
433
436
|
return await loadAgentsFromRoot(homePath(rootDir, "agents"));
|
|
434
437
|
}
|
|
435
438
|
|
|
439
|
+
interface AutomationEntry {
|
|
440
|
+
name: string;
|
|
441
|
+
sourceDir: string;
|
|
442
|
+
files: Map<string, string>;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function listRelativeFiles(root: string): Promise<string[]> {
|
|
446
|
+
const out: string[] = [];
|
|
447
|
+
|
|
448
|
+
async function visit(currentDir: string, prefix = ""): Promise<void> {
|
|
449
|
+
const entries = await readdir(currentDir, { withFileTypes: true }).catch(
|
|
450
|
+
() => [] as import("node:fs").Dirent[]
|
|
451
|
+
);
|
|
452
|
+
for (const entry of entries) {
|
|
453
|
+
if (entry.name.startsWith(".")) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const relPath = prefix ? join(prefix, entry.name) : entry.name;
|
|
457
|
+
const fullPath = join(currentDir, entry.name);
|
|
458
|
+
if (entry.isDirectory()) {
|
|
459
|
+
await visit(fullPath, relPath);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (entry.isFile()) {
|
|
463
|
+
out.push(relPath);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await visit(root);
|
|
469
|
+
return out.sort();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function loadAutomationEntries(
|
|
473
|
+
automationsRoot: string
|
|
474
|
+
): Promise<AutomationEntry[]> {
|
|
475
|
+
const entries = await readdir(automationsRoot, { withFileTypes: true }).catch(
|
|
476
|
+
() => [] as import("node:fs").Dirent[]
|
|
477
|
+
);
|
|
478
|
+
const out: AutomationEntry[] = [];
|
|
479
|
+
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const sourceDir = join(automationsRoot, entry.name);
|
|
485
|
+
const relativeFiles = await listRelativeFiles(sourceDir);
|
|
486
|
+
const files = new Map<string, string>();
|
|
487
|
+
for (const relPath of relativeFiles) {
|
|
488
|
+
const raw = await readTextIfExists(join(sourceDir, relPath));
|
|
489
|
+
if (raw == null) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
files.set(relPath, raw);
|
|
493
|
+
}
|
|
494
|
+
if (!files.has("automation.toml")) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
out.push({
|
|
498
|
+
name: entry.name,
|
|
499
|
+
sourceDir,
|
|
500
|
+
files,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function loadCanonicalAutomations(
|
|
508
|
+
rootDir: string
|
|
509
|
+
): Promise<AutomationEntry[]> {
|
|
510
|
+
return await loadAutomationEntries(join(rootDir, "automations"));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function automationEntriesEqual(
|
|
514
|
+
left: AutomationEntry,
|
|
515
|
+
right: AutomationEntry
|
|
516
|
+
): boolean {
|
|
517
|
+
if (left.files.size !== right.files.size) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
for (const [relPath, leftRaw] of left.files.entries()) {
|
|
521
|
+
if (right.files.get(relPath) !== leftRaw) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function canonicalAutomationsExist(rootDir: string): Promise<boolean> {
|
|
529
|
+
try {
|
|
530
|
+
const automations = await loadCanonicalAutomations(rootDir);
|
|
531
|
+
return automations.length > 0;
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
436
537
|
async function loadMergedIndex(
|
|
437
538
|
homeDir: string,
|
|
438
539
|
rootDir: string
|
|
@@ -624,6 +725,58 @@ async function syncAgentFiles({
|
|
|
624
725
|
return { add: plan.add, remove: plan.remove };
|
|
625
726
|
}
|
|
626
727
|
|
|
728
|
+
async function planAutomationFileChanges(args: {
|
|
729
|
+
automationDir: string;
|
|
730
|
+
rootDir: string;
|
|
731
|
+
previouslyManagedTargets?: string[];
|
|
732
|
+
}): Promise<{
|
|
733
|
+
add: string[];
|
|
734
|
+
remove: string[];
|
|
735
|
+
contents: Map<string, string>;
|
|
736
|
+
sources: Map<string, string>;
|
|
737
|
+
}> {
|
|
738
|
+
const automations = await loadCanonicalAutomations(args.rootDir);
|
|
739
|
+
const contents = new Map<string, string>();
|
|
740
|
+
const sources = new Map<string, string>();
|
|
741
|
+
const desiredPaths = new Set<string>();
|
|
742
|
+
|
|
743
|
+
for (const automation of automations) {
|
|
744
|
+
for (const [relPath, raw] of automation.files.entries()) {
|
|
745
|
+
const targetPath = join(args.automationDir, automation.name, relPath);
|
|
746
|
+
const sourcePath = join(automation.sourceDir, relPath);
|
|
747
|
+
desiredPaths.add(targetPath);
|
|
748
|
+
contents.set(targetPath, raw);
|
|
749
|
+
sources.set(targetPath, sourcePath);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const add = new Set<string>();
|
|
754
|
+
for (const targetPath of desiredPaths) {
|
|
755
|
+
const current = await readTextIfExists(targetPath);
|
|
756
|
+
const desired = contents.get(targetPath);
|
|
757
|
+
if (desired != null && current !== desired) {
|
|
758
|
+
add.add(targetPath);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const remove = Array.from(
|
|
763
|
+
new Set(
|
|
764
|
+
(args.previouslyManagedTargets ?? []).filter(
|
|
765
|
+
(targetPath) =>
|
|
766
|
+
targetPath.startsWith(join(args.automationDir, "")) &&
|
|
767
|
+
!desiredPaths.has(targetPath)
|
|
768
|
+
)
|
|
769
|
+
)
|
|
770
|
+
).sort();
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
add: Array.from(add).sort(),
|
|
774
|
+
remove,
|
|
775
|
+
contents,
|
|
776
|
+
sources,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
627
780
|
async function listSkillDirs(skillsRoot: string): Promise<string[]> {
|
|
628
781
|
try {
|
|
629
782
|
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
|
@@ -945,6 +1098,7 @@ interface ExistingManagedItem {
|
|
|
945
1098
|
kind:
|
|
946
1099
|
| "skill"
|
|
947
1100
|
| "agent"
|
|
1101
|
+
| "automation"
|
|
948
1102
|
| "global-doc"
|
|
949
1103
|
| "rule"
|
|
950
1104
|
| "tool-config"
|
|
@@ -1123,6 +1277,87 @@ async function adoptExistingToolAgents(args: {
|
|
|
1123
1277
|
return adopted;
|
|
1124
1278
|
}
|
|
1125
1279
|
|
|
1280
|
+
async function planExistingAutomationAdoption(args: {
|
|
1281
|
+
rootDir: string;
|
|
1282
|
+
automationDir: string;
|
|
1283
|
+
}): Promise<ExistingManagedImportPlan> {
|
|
1284
|
+
const plan = emptyManagedImportPlan();
|
|
1285
|
+
const liveAutomations = await loadAutomationEntries(args.automationDir);
|
|
1286
|
+
const canonicalAutomations = new Map(
|
|
1287
|
+
(await loadCanonicalAutomations(args.rootDir)).map((entry) => [
|
|
1288
|
+
entry.name,
|
|
1289
|
+
entry,
|
|
1290
|
+
])
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
for (const liveAutomation of liveAutomations) {
|
|
1294
|
+
const canonicalAutomation = canonicalAutomations.get(liveAutomation.name);
|
|
1295
|
+
if (!canonicalAutomation) {
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
const item: ExistingManagedItem = {
|
|
1299
|
+
kind: "automation",
|
|
1300
|
+
name: liveAutomation.name,
|
|
1301
|
+
livePath: liveAutomation.sourceDir,
|
|
1302
|
+
canonicalPath: join(args.rootDir, "automations", liveAutomation.name),
|
|
1303
|
+
};
|
|
1304
|
+
if (automationEntriesEqual(liveAutomation, canonicalAutomation)) {
|
|
1305
|
+
plan.identical.push(item);
|
|
1306
|
+
} else {
|
|
1307
|
+
plan.conflicts.push(item);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return mergeManagedImportPlans(plan);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async function adoptExistingAutomations(args: {
|
|
1315
|
+
rootDir: string;
|
|
1316
|
+
automationDir: string;
|
|
1317
|
+
conflictMode: "keep-canonical" | "keep-existing";
|
|
1318
|
+
}): Promise<ExistingManagedItem[]> {
|
|
1319
|
+
if (args.conflictMode !== "keep-existing") {
|
|
1320
|
+
return [];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const adopted: ExistingManagedItem[] = [];
|
|
1324
|
+
const liveAutomations = await loadAutomationEntries(args.automationDir);
|
|
1325
|
+
const canonicalAutomations = new Map(
|
|
1326
|
+
(await loadCanonicalAutomations(args.rootDir)).map((entry) => [
|
|
1327
|
+
entry.name,
|
|
1328
|
+
entry,
|
|
1329
|
+
])
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
for (const liveAutomation of liveAutomations) {
|
|
1333
|
+
const canonicalAutomation = canonicalAutomations.get(liveAutomation.name);
|
|
1334
|
+
if (
|
|
1335
|
+
!(
|
|
1336
|
+
canonicalAutomation &&
|
|
1337
|
+
!automationEntriesEqual(liveAutomation, canonicalAutomation)
|
|
1338
|
+
)
|
|
1339
|
+
) {
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
const canonicalPath = join(
|
|
1343
|
+
args.rootDir,
|
|
1344
|
+
"automations",
|
|
1345
|
+
liveAutomation.name
|
|
1346
|
+
);
|
|
1347
|
+
await ensureDir(dirname(canonicalPath));
|
|
1348
|
+
await rm(canonicalPath, { recursive: true, force: true });
|
|
1349
|
+
await cp(liveAutomation.sourceDir, canonicalPath, { recursive: true });
|
|
1350
|
+
adopted.push({
|
|
1351
|
+
kind: "automation",
|
|
1352
|
+
name: liveAutomation.name,
|
|
1353
|
+
livePath: liveAutomation.sourceDir,
|
|
1354
|
+
canonicalPath,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
return adopted;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1126
1361
|
async function planExistingGlobalDocAdoption(args: {
|
|
1127
1362
|
rootDir: string;
|
|
1128
1363
|
tool: string;
|
|
@@ -1788,6 +2023,12 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1788
2023
|
agentsDir: toolPaths.agentsDir,
|
|
1789
2024
|
})
|
|
1790
2025
|
: emptyManagedImportPlan(),
|
|
2026
|
+
toolPaths.automationDir
|
|
2027
|
+
? await planExistingAutomationAdoption({
|
|
2028
|
+
rootDir,
|
|
2029
|
+
automationDir: toolPaths.automationDir,
|
|
2030
|
+
})
|
|
2031
|
+
: emptyManagedImportPlan(),
|
|
1791
2032
|
toolPaths.toolHome
|
|
1792
2033
|
? await planExistingGlobalDocAdoption({
|
|
1793
2034
|
rootDir,
|
|
@@ -1826,6 +2067,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1826
2067
|
if (
|
|
1827
2068
|
(toolPaths.skillsDir ||
|
|
1828
2069
|
toolPaths.agentsDir ||
|
|
2070
|
+
toolPaths.automationDir ||
|
|
1829
2071
|
toolPaths.toolHome ||
|
|
1830
2072
|
toolPaths.rulesDir ||
|
|
1831
2073
|
toolPaths.toolConfig ||
|
|
@@ -1906,6 +2148,14 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1906
2148
|
});
|
|
1907
2149
|
adoptedSkills.push(...result.map((item) => item.name));
|
|
1908
2150
|
}
|
|
2151
|
+
if (toolPaths.automationDir && opts.adoptExisting) {
|
|
2152
|
+
const result = await adoptExistingAutomations({
|
|
2153
|
+
rootDir,
|
|
2154
|
+
automationDir: toolPaths.automationDir,
|
|
2155
|
+
conflictMode: importConflictMode,
|
|
2156
|
+
});
|
|
2157
|
+
adoptedSkills.push(...result.map((item) => `${item.kind}:${item.name}`));
|
|
2158
|
+
}
|
|
1909
2159
|
if (toolPaths.toolHome && opts.adoptExisting) {
|
|
1910
2160
|
const result = await adoptExistingGlobalDocs({
|
|
1911
2161
|
rootDir,
|
|
@@ -1957,6 +2207,12 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
1957
2207
|
tool,
|
|
1958
2208
|
})
|
|
1959
2209
|
: null;
|
|
2210
|
+
const automationPreview = toolPaths.automationDir
|
|
2211
|
+
? await planAutomationFileChanges({
|
|
2212
|
+
automationDir: toolPaths.automationDir,
|
|
2213
|
+
rootDir,
|
|
2214
|
+
})
|
|
2215
|
+
: null;
|
|
1960
2216
|
const globalDocsPreview = toolPaths.toolHome
|
|
1961
2217
|
? await planToolGlobalDocsSync({
|
|
1962
2218
|
homeDir: home,
|
|
@@ -2048,6 +2304,16 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2048
2304
|
});
|
|
2049
2305
|
}
|
|
2050
2306
|
|
|
2307
|
+
if (toolPaths.automationDir && automationPreview) {
|
|
2308
|
+
await ensureDir(toolPaths.automationDir);
|
|
2309
|
+
await applyRenderedRemoves(automationPreview.remove);
|
|
2310
|
+
await applyRenderedWrites({
|
|
2311
|
+
contents: automationPreview.contents,
|
|
2312
|
+
targets: Array.from(automationPreview.contents.keys()),
|
|
2313
|
+
});
|
|
2314
|
+
await pruneEmptyParents(automationPreview.remove, toolPaths.automationDir);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2051
2317
|
if (toolPaths.toolHome && globalDocsPreview) {
|
|
2052
2318
|
await ensureDir(toolPaths.toolHome);
|
|
2053
2319
|
await applyRenderedRemoves(globalDocsPreview.remove);
|
|
@@ -2086,6 +2352,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2086
2352
|
skillsDir: toolPaths.skillsDir,
|
|
2087
2353
|
mcpConfig: toolPaths.mcpConfig,
|
|
2088
2354
|
agentsDir: toolPaths.agentsDir,
|
|
2355
|
+
automationDir: toolPaths.automationDir,
|
|
2089
2356
|
toolHome: globalDocsPreview?.managedTargets.length
|
|
2090
2357
|
? toolPaths.toolHome
|
|
2091
2358
|
: undefined,
|
|
@@ -2123,6 +2390,15 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2123
2390
|
sources: agentPreview.sources,
|
|
2124
2391
|
});
|
|
2125
2392
|
}
|
|
2393
|
+
if (automationPreview) {
|
|
2394
|
+
updateRenderedTargetState({
|
|
2395
|
+
entry: managedEntry,
|
|
2396
|
+
writtenTargets: Array.from(automationPreview.contents.keys()),
|
|
2397
|
+
removedTargets: automationPreview.remove,
|
|
2398
|
+
contents: automationPreview.contents,
|
|
2399
|
+
sources: automationPreview.sources,
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2126
2402
|
if (globalDocsPreview) {
|
|
2127
2403
|
updateRenderedTargetState({
|
|
2128
2404
|
entry: managedEntry,
|
|
@@ -2238,6 +2514,14 @@ export async function unmanageTool(tool: string, opts: ManageOptions = {}) {
|
|
|
2238
2514
|
});
|
|
2239
2515
|
}
|
|
2240
2516
|
|
|
2517
|
+
if (entry.automationDir) {
|
|
2518
|
+
const automationTargets = Object.keys(entry.renderedTargets ?? {}).filter(
|
|
2519
|
+
(targetPath) => targetPath.startsWith(join(entry.automationDir!, ""))
|
|
2520
|
+
);
|
|
2521
|
+
await applyRenderedRemoves(automationTargets);
|
|
2522
|
+
await pruneEmptyParents(automationTargets, entry.automationDir);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2241
2525
|
if (entry.globalAgentsPath) {
|
|
2242
2526
|
await restoreBackup({
|
|
2243
2527
|
original: entry.globalAgentsPath,
|
|
@@ -2320,6 +2604,15 @@ async function repairManagedToolEntry(args: {
|
|
|
2320
2604
|
changed = true;
|
|
2321
2605
|
}
|
|
2322
2606
|
|
|
2607
|
+
if (
|
|
2608
|
+
!next.automationDir &&
|
|
2609
|
+
toolPaths.automationDir &&
|
|
2610
|
+
(await canonicalAutomationsExist(rootDir))
|
|
2611
|
+
) {
|
|
2612
|
+
next.automationDir = toolPaths.automationDir;
|
|
2613
|
+
changed = true;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2323
2616
|
if (toolPaths.toolHome && !next.toolHome) {
|
|
2324
2617
|
const preview = await syncToolGlobalDocs({
|
|
2325
2618
|
homeDir,
|
|
@@ -2406,6 +2699,7 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2406
2699
|
desiredContents: Map<string, string>;
|
|
2407
2700
|
desiredSources: Map<string, string>;
|
|
2408
2701
|
conflictMode?: "warn" | "overwrite";
|
|
2702
|
+
protectAllSources?: boolean;
|
|
2409
2703
|
}): Promise<RenderedApplyPlan> {
|
|
2410
2704
|
if (args.conflictMode === "overwrite") {
|
|
2411
2705
|
return {
|
|
@@ -2433,7 +2727,7 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2433
2727
|
continue;
|
|
2434
2728
|
}
|
|
2435
2729
|
const sourceKind = renderedSourceKindForPath(sourcePath);
|
|
2436
|
-
if (sourceKind !== "builtin") {
|
|
2730
|
+
if (sourceKind !== "builtin" && !args.protectAllSources) {
|
|
2437
2731
|
if (args.desiredWrites.includes(targetPath)) {
|
|
2438
2732
|
write.push(targetPath);
|
|
2439
2733
|
} else {
|
|
@@ -2452,8 +2746,16 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2452
2746
|
}
|
|
2453
2747
|
|
|
2454
2748
|
const currentHash = renderedHash(current);
|
|
2749
|
+
const desiredHash = args.desiredContents.get(targetPath)
|
|
2750
|
+
? renderedHash(args.desiredContents.get(targetPath)!)
|
|
2751
|
+
: null;
|
|
2455
2752
|
if (prior?.hash) {
|
|
2456
|
-
if (
|
|
2753
|
+
if (
|
|
2754
|
+
currentHash === prior.hash ||
|
|
2755
|
+
(args.desiredWrites.includes(targetPath) &&
|
|
2756
|
+
desiredHash != null &&
|
|
2757
|
+
currentHash === desiredHash)
|
|
2758
|
+
) {
|
|
2457
2759
|
if (args.desiredWrites.includes(targetPath)) {
|
|
2458
2760
|
write.push(targetPath);
|
|
2459
2761
|
} else {
|
|
@@ -2470,6 +2772,15 @@ async function planRenderedTargetConflicts(args: {
|
|
|
2470
2772
|
continue;
|
|
2471
2773
|
}
|
|
2472
2774
|
|
|
2775
|
+
if (
|
|
2776
|
+
args.desiredWrites.includes(targetPath) &&
|
|
2777
|
+
desiredHash != null &&
|
|
2778
|
+
currentHash === desiredHash
|
|
2779
|
+
) {
|
|
2780
|
+
write.push(targetPath);
|
|
2781
|
+
continue;
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2473
2784
|
conflicts.push({
|
|
2474
2785
|
targetPath,
|
|
2475
2786
|
sourcePath,
|
|
@@ -2496,8 +2807,14 @@ function logRenderedConflicts(
|
|
|
2496
2807
|
conflict.reason === "unknown_state"
|
|
2497
2808
|
? "no prior managed hash is recorded"
|
|
2498
2809
|
: "local edits were detected";
|
|
2810
|
+
const surface =
|
|
2811
|
+
conflict.sourceKind === "builtin"
|
|
2812
|
+
? "builtin-backed target"
|
|
2813
|
+
: "managed target";
|
|
2499
2814
|
console.warn(
|
|
2500
|
-
|
|
2815
|
+
conflict.sourceKind === "builtin"
|
|
2816
|
+
? `${tool}: ${verb} ${surface} ${conflict.targetPath} because ${state}. Rerun with "--builtin-conflicts overwrite" to replace it with the latest packaged default.`
|
|
2817
|
+
: `${tool}: ${verb} ${surface} ${conflict.targetPath} because ${state}.`
|
|
2501
2818
|
);
|
|
2502
2819
|
}
|
|
2503
2820
|
}
|
|
@@ -2525,6 +2842,24 @@ async function applyRenderedRemoves(targets: string[]) {
|
|
|
2525
2842
|
}
|
|
2526
2843
|
}
|
|
2527
2844
|
|
|
2845
|
+
async function pruneEmptyParents(targets: string[], stopDir: string) {
|
|
2846
|
+
const candidateDirs = Array.from(
|
|
2847
|
+
new Set(targets.map((pathValue) => dirname(pathValue)))
|
|
2848
|
+
).sort((a, b) => b.length - a.length);
|
|
2849
|
+
|
|
2850
|
+
for (const startDir of candidateDirs) {
|
|
2851
|
+
let currentDir = startDir;
|
|
2852
|
+
while (currentDir.startsWith(join(stopDir, "")) && currentDir !== stopDir) {
|
|
2853
|
+
const entries = await readdir(currentDir).catch(() => null);
|
|
2854
|
+
if (!(entries && entries.length === 0)) {
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
await rm(currentDir, { recursive: true, force: true });
|
|
2858
|
+
currentDir = dirname(currentDir);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2528
2863
|
function updateRenderedTargetState(args: {
|
|
2529
2864
|
entry: ManagedToolState;
|
|
2530
2865
|
writtenTargets: string[];
|
|
@@ -2558,6 +2893,8 @@ function logSyncDryRun({
|
|
|
2558
2893
|
mcpPlan,
|
|
2559
2894
|
agentPlan,
|
|
2560
2895
|
agentConflicts,
|
|
2896
|
+
automationPlan,
|
|
2897
|
+
automationConflicts,
|
|
2561
2898
|
globalDocsPlan,
|
|
2562
2899
|
globalDocsConflicts,
|
|
2563
2900
|
rulesPlan,
|
|
@@ -2571,6 +2908,8 @@ function logSyncDryRun({
|
|
|
2571
2908
|
mcpPlan: { needsWrite: boolean };
|
|
2572
2909
|
agentPlan: { add: string[]; remove: string[] };
|
|
2573
2910
|
agentConflicts: RenderedConflict[];
|
|
2911
|
+
automationPlan: { write: string[]; remove: string[] };
|
|
2912
|
+
automationConflicts: RenderedConflict[];
|
|
2574
2913
|
globalDocsPlan: { write: string[]; remove: string[] };
|
|
2575
2914
|
globalDocsConflicts: RenderedConflict[];
|
|
2576
2915
|
rulesPlan: { write: string[]; remove: string[] };
|
|
@@ -2591,6 +2930,13 @@ function logSyncDryRun({
|
|
|
2591
2930
|
console.log(`${tool}: would remove agent ${p}`);
|
|
2592
2931
|
}
|
|
2593
2932
|
logRenderedConflicts(tool, agentConflicts, true);
|
|
2933
|
+
for (const p of automationPlan.write) {
|
|
2934
|
+
console.log(`${tool}: would write automation ${p}`);
|
|
2935
|
+
}
|
|
2936
|
+
for (const p of automationPlan.remove) {
|
|
2937
|
+
console.log(`${tool}: would remove automation ${p}`);
|
|
2938
|
+
}
|
|
2939
|
+
logRenderedConflicts(tool, automationConflicts, true);
|
|
2594
2940
|
for (const p of globalDocsPlan.write) {
|
|
2595
2941
|
console.log(`${tool}: would write global doc ${p}`);
|
|
2596
2942
|
}
|
|
@@ -2620,6 +2966,8 @@ function logSyncDryRun({
|
|
|
2620
2966
|
skillPlan.remove.length === 0 &&
|
|
2621
2967
|
agentPlan.add.length === 0 &&
|
|
2622
2968
|
agentPlan.remove.length === 0 &&
|
|
2969
|
+
automationPlan.write.length === 0 &&
|
|
2970
|
+
automationPlan.remove.length === 0 &&
|
|
2623
2971
|
globalDocsPlan.write.length === 0 &&
|
|
2624
2972
|
globalDocsPlan.remove.length === 0 &&
|
|
2625
2973
|
rulesPlan.write.length === 0 &&
|
|
@@ -2628,6 +2976,7 @@ function logSyncDryRun({
|
|
|
2628
2976
|
!configPlan.remove &&
|
|
2629
2977
|
!mcpPlan.needsWrite &&
|
|
2630
2978
|
agentConflicts.length === 0 &&
|
|
2979
|
+
automationConflicts.length === 0 &&
|
|
2631
2980
|
globalDocsConflicts.length === 0 &&
|
|
2632
2981
|
rulesConflicts.length === 0 &&
|
|
2633
2982
|
configConflicts.length === 0
|
|
@@ -2767,6 +3116,13 @@ async function syncManagedToolEntry({
|
|
|
2767
3116
|
tool,
|
|
2768
3117
|
})
|
|
2769
3118
|
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
3119
|
+
const automationPlan = entry.automationDir
|
|
3120
|
+
? await planAutomationFileChanges({
|
|
3121
|
+
automationDir: entry.automationDir,
|
|
3122
|
+
rootDir,
|
|
3123
|
+
previouslyManagedTargets: Object.keys(entry.renderedTargets ?? {}),
|
|
3124
|
+
})
|
|
3125
|
+
: { add: [], remove: [], contents: new Map(), sources: new Map() };
|
|
2770
3126
|
|
|
2771
3127
|
const mcpPlan = entry.mcpConfig
|
|
2772
3128
|
? await syncMcpConfig({
|
|
@@ -2846,6 +3202,15 @@ async function syncManagedToolEntry({
|
|
|
2846
3202
|
desiredSources: globalDocsPlan.sources,
|
|
2847
3203
|
conflictMode: builtinConflictMode,
|
|
2848
3204
|
});
|
|
3205
|
+
const automationRendered = await planRenderedTargetConflicts({
|
|
3206
|
+
entry,
|
|
3207
|
+
desiredWrites: automationPlan.add,
|
|
3208
|
+
desiredRemoves: automationPlan.remove,
|
|
3209
|
+
desiredContents: automationPlan.contents,
|
|
3210
|
+
desiredSources: automationPlan.sources,
|
|
3211
|
+
conflictMode: builtinConflictMode,
|
|
3212
|
+
protectAllSources: true,
|
|
3213
|
+
});
|
|
2849
3214
|
const rulesRendered = await planRenderedTargetConflicts({
|
|
2850
3215
|
entry,
|
|
2851
3216
|
desiredWrites: rulesPlan.write,
|
|
@@ -2882,6 +3247,11 @@ async function syncManagedToolEntry({
|
|
|
2882
3247
|
mcpPlan,
|
|
2883
3248
|
agentPlan: { add: agentRendered.write, remove: agentRendered.remove },
|
|
2884
3249
|
agentConflicts: agentRendered.conflicts,
|
|
3250
|
+
automationPlan: {
|
|
3251
|
+
write: automationRendered.write,
|
|
3252
|
+
remove: automationRendered.remove,
|
|
3253
|
+
},
|
|
3254
|
+
automationConflicts: automationRendered.conflicts,
|
|
2885
3255
|
globalDocsPlan: {
|
|
2886
3256
|
write: globalDocsRendered.write,
|
|
2887
3257
|
remove: globalDocsRendered.remove,
|
|
@@ -2902,6 +3272,14 @@ async function syncManagedToolEntry({
|
|
|
2902
3272
|
contents: agentPlan.contents,
|
|
2903
3273
|
targets: agentRendered.write,
|
|
2904
3274
|
});
|
|
3275
|
+
await applyRenderedRemoves(automationRendered.remove);
|
|
3276
|
+
await applyRenderedWrites({
|
|
3277
|
+
contents: automationPlan.contents,
|
|
3278
|
+
targets: automationRendered.write,
|
|
3279
|
+
});
|
|
3280
|
+
if (entry.automationDir) {
|
|
3281
|
+
await pruneEmptyParents(automationRendered.remove, entry.automationDir);
|
|
3282
|
+
}
|
|
2905
3283
|
await applyRenderedRemoves(globalDocsRendered.remove);
|
|
2906
3284
|
await applyRenderedWrites({
|
|
2907
3285
|
contents: globalDocsPlan.contents,
|
|
@@ -2918,6 +3296,7 @@ async function syncManagedToolEntry({
|
|
|
2918
3296
|
targets: configRendered.write,
|
|
2919
3297
|
});
|
|
2920
3298
|
logRenderedConflicts(tool, agentRendered.conflicts);
|
|
3299
|
+
logRenderedConflicts(tool, automationRendered.conflicts);
|
|
2921
3300
|
logRenderedConflicts(tool, globalDocsRendered.conflicts);
|
|
2922
3301
|
logRenderedConflicts(tool, rulesRendered.conflicts);
|
|
2923
3302
|
logRenderedConflicts(tool, configRendered.conflicts);
|
|
@@ -2929,6 +3308,13 @@ async function syncManagedToolEntry({
|
|
|
2929
3308
|
contents: agentPlan.contents,
|
|
2930
3309
|
sources: agentPlan.sources,
|
|
2931
3310
|
});
|
|
3311
|
+
updateRenderedTargetState({
|
|
3312
|
+
entry,
|
|
3313
|
+
writtenTargets: automationRendered.write,
|
|
3314
|
+
removedTargets: automationRendered.remove,
|
|
3315
|
+
contents: automationPlan.contents,
|
|
3316
|
+
sources: automationPlan.sources,
|
|
3317
|
+
});
|
|
2932
3318
|
updateRenderedTargetState({
|
|
2933
3319
|
entry,
|
|
2934
3320
|
writtenTargets: globalDocsRendered.write,
|
package/src/paths.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -93,6 +94,34 @@ export function preferredGlobalFacultStateDir(
|
|
|
93
94
|
return join(preferredGlobalAiRoot(home), ".facult");
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
export function facultLocalStateRoot(home: string = defaultHomeDir()): string {
|
|
98
|
+
const override = process.env.FACULT_LOCAL_STATE_DIR?.trim();
|
|
99
|
+
if (override) {
|
|
100
|
+
return resolvePath(override, home);
|
|
101
|
+
}
|
|
102
|
+
if (process.platform === "darwin") {
|
|
103
|
+
return join(home, "Library", "Application Support", "fclt");
|
|
104
|
+
}
|
|
105
|
+
const xdg = process.env.XDG_STATE_HOME?.trim();
|
|
106
|
+
return xdg
|
|
107
|
+
? join(resolvePath(xdg, home), "fclt")
|
|
108
|
+
: join(home, ".local", "state", "fclt");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function facultLocalCacheRoot(home: string = defaultHomeDir()): string {
|
|
112
|
+
const override = process.env.FACULT_CACHE_DIR?.trim();
|
|
113
|
+
if (override) {
|
|
114
|
+
return resolvePath(override, home);
|
|
115
|
+
}
|
|
116
|
+
if (process.platform === "darwin") {
|
|
117
|
+
return join(home, "Library", "Caches", "fclt");
|
|
118
|
+
}
|
|
119
|
+
const xdg = process.env.XDG_CACHE_HOME?.trim();
|
|
120
|
+
return xdg
|
|
121
|
+
? join(resolvePath(xdg, home), "fclt")
|
|
122
|
+
: join(home, ".cache", "fclt");
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
export function legacyExternalFacultStateDir(
|
|
97
126
|
home: string = defaultHomeDir()
|
|
98
127
|
): string {
|
|
@@ -186,6 +215,46 @@ export function facultStateDir(
|
|
|
186
215
|
return join(resolvedRoot, ".facult");
|
|
187
216
|
}
|
|
188
217
|
|
|
218
|
+
function machineStateProjectKey(
|
|
219
|
+
rootDir: string,
|
|
220
|
+
home: string = defaultHomeDir()
|
|
221
|
+
): string {
|
|
222
|
+
const projectRoot = projectRootFromAiRoot(rootDir, home);
|
|
223
|
+
const labelSource = projectRoot ?? rootDir;
|
|
224
|
+
const label = basename(labelSource).trim().toLowerCase();
|
|
225
|
+
const slug = label.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
226
|
+
const digest = createHash("sha256")
|
|
227
|
+
.update(resolve(rootDir))
|
|
228
|
+
.digest("hex")
|
|
229
|
+
.slice(0, 12);
|
|
230
|
+
return `${slug || "project"}-${digest}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function facultMachineStateDir(
|
|
234
|
+
home: string = defaultHomeDir(),
|
|
235
|
+
rootDir?: string
|
|
236
|
+
): string {
|
|
237
|
+
const resolvedRoot = rootDir ?? facultRootDir(home);
|
|
238
|
+
const projectRoot = projectRootFromAiRoot(resolvedRoot, home);
|
|
239
|
+
return projectRoot
|
|
240
|
+
? join(
|
|
241
|
+
facultLocalStateRoot(home),
|
|
242
|
+
"projects",
|
|
243
|
+
machineStateProjectKey(resolvedRoot, home)
|
|
244
|
+
)
|
|
245
|
+
: join(facultLocalStateRoot(home), "global");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function facultInstallStatePath(
|
|
249
|
+
home: string = defaultHomeDir()
|
|
250
|
+
): string {
|
|
251
|
+
return join(facultLocalStateRoot(home), "install.json");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function facultRuntimeCacheDir(home: string = defaultHomeDir()): string {
|
|
255
|
+
return join(facultLocalCacheRoot(home), "runtime");
|
|
256
|
+
}
|
|
257
|
+
|
|
189
258
|
export function projectRootFromAiRoot(
|
|
190
259
|
rootDir: string,
|
|
191
260
|
home: string = defaultHomeDir()
|
package/src/self-update.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, rename } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
facultInstallStatePath,
|
|
5
6
|
legacyExternalFacultStateDir,
|
|
6
7
|
preferredGlobalFacultStateDir,
|
|
7
8
|
} from "./paths";
|
|
@@ -88,6 +89,7 @@ export function parseSelfUpdateArgs(argv: string[]): ParsedArgs {
|
|
|
88
89
|
|
|
89
90
|
async function loadInstallState(home: string): Promise<InstallState | null> {
|
|
90
91
|
const paths = [
|
|
92
|
+
facultInstallStatePath(home),
|
|
91
93
|
join(preferredGlobalFacultStateDir(home), "install.json"),
|
|
92
94
|
join(legacyExternalFacultStateDir(home), "install.json"),
|
|
93
95
|
];
|
|
@@ -208,7 +210,7 @@ async function writeInstallState(args: {
|
|
|
208
210
|
packageVersion?: string;
|
|
209
211
|
binaryPath?: string;
|
|
210
212
|
}) {
|
|
211
|
-
const dir =
|
|
213
|
+
const dir = dirname(facultInstallStatePath(args.home));
|
|
212
214
|
await mkdir(dir, { recursive: true });
|
|
213
215
|
const payload: InstallState = {
|
|
214
216
|
version: 1,
|
|
@@ -219,7 +221,7 @@ async function writeInstallState(args: {
|
|
|
219
221
|
installedAt: new Date().toISOString(),
|
|
220
222
|
};
|
|
221
223
|
await Bun.write(
|
|
222
|
-
|
|
224
|
+
facultInstallStatePath(args.home),
|
|
223
225
|
`${JSON.stringify(payload, null, 2)}\n`
|
|
224
226
|
);
|
|
225
227
|
}
|