@cotal-ai/cli 0.11.2 → 0.11.4
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/dist/command.d.ts.map +1 -1
- package/dist/command.js +58 -0
- package/dist/command.js.map +1 -1
- package/dist/commands/ext.d.ts +6 -0
- package/dist/commands/ext.d.ts.map +1 -1
- package/dist/commands/ext.js +50 -47
- package/dist/commands/ext.js.map +1 -1
- package/dist/commands/models.d.ts.map +1 -1
- package/dist/commands/models.js +3 -2
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/runtimes.d.ts +9 -0
- package/dist/commands/runtimes.d.ts.map +1 -0
- package/dist/commands/runtimes.js +52 -0
- package/dist/commands/runtimes.js.map +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +24 -7
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn-manifest.js +1 -1
- package/dist/commands/spawn-manifest.js.map +1 -1
- package/dist/commands/spawn.d.ts +7 -1
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +11 -0
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/up.d.ts +10 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +66 -10
- package/dist/commands/up.js.map +1 -1
- package/dist/ext-loader.d.ts +12 -1
- package/dist/ext-loader.d.ts.map +1 -1
- package/dist/ext-loader.js +36 -62
- package/dist/ext-loader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -18
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-proc.d.ts.map +1 -1
- package/dist/lib/auth-proc.js +3 -0
- package/dist/lib/auth-proc.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +3 -1
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/ext-mutation.d.ts +17 -0
- package/dist/lib/ext-mutation.d.ts.map +1 -0
- package/dist/lib/ext-mutation.js +23 -0
- package/dist/lib/ext-mutation.js.map +1 -0
- package/dist/lib/manager-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.js +3 -1
- package/dist/lib/manager-proc.js.map +1 -1
- package/dist/lib/manifest/apply.d.ts +4 -3
- package/dist/lib/manifest/apply.d.ts.map +1 -1
- package/dist/lib/manifest/apply.js +9 -7
- package/dist/lib/manifest/apply.js.map +1 -1
- package/dist/lib/up-report.d.ts +9 -0
- package/dist/lib/up-report.d.ts.map +1 -0
- package/dist/lib/up-report.js +12 -0
- package/dist/lib/up-report.js.map +1 -0
- package/dist/seed/authority.d.ts +54 -0
- package/dist/seed/authority.d.ts.map +1 -0
- package/dist/seed/authority.js +94 -0
- package/dist/seed/authority.js.map +1 -0
- package/dist/seed/lock.d.ts +109 -0
- package/dist/seed/lock.d.ts.map +1 -0
- package/dist/seed/lock.js +168 -0
- package/dist/seed/lock.js.map +1 -0
- package/dist/seed/paths.d.ts +66 -0
- package/dist/seed/paths.d.ts.map +1 -0
- package/dist/seed/paths.js +149 -0
- package/dist/seed/paths.js.map +1 -0
- package/dist/seed/reconcile.d.ts +16 -0
- package/dist/seed/reconcile.d.ts.map +1 -0
- package/dist/seed/reconcile.js +431 -0
- package/dist/seed/reconcile.js.map +1 -0
- package/dist/seed/store.d.ts +18 -0
- package/dist/seed/store.d.ts.map +1 -0
- package/dist/seed/store.js +58 -0
- package/dist/seed/store.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface DetachedStatus {
|
|
2
|
+
pid: number;
|
|
3
|
+
delivery: boolean;
|
|
4
|
+
authService: boolean;
|
|
5
|
+
manager: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Render only components confirmed running for this detached boot. */
|
|
8
|
+
export declare function renderDetachedSummary(status: DetachedStatus): string;
|
|
9
|
+
//# sourceMappingURL=up-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"up-report.d.ts","sourceRoot":"","sources":["../../src/lib/up-report.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAMpE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Render only components confirmed running for this detached boot. */
|
|
2
|
+
export function renderDetachedSummary(status) {
|
|
3
|
+
const components = [`nats-server (pid ${status.pid})`];
|
|
4
|
+
if (status.delivery)
|
|
5
|
+
components.push("delivery daemon");
|
|
6
|
+
if (status.authService)
|
|
7
|
+
components.push("user-auth service");
|
|
8
|
+
if (status.manager)
|
|
9
|
+
components.push("manager");
|
|
10
|
+
return `✓ running in the background: ${components.join(", ")} - stop with: cotal down`;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=up-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"up-report.js","sourceRoot":"","sources":["../../src/lib/up-report.ts"],"names":[],"mappings":"AAOA,uEAAuE;AACvE,MAAM,UAAU,qBAAqB,CAAC,MAAsB;IAC1D,MAAM,UAAU,GAAG,CAAC,oBAAoB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IACvD,IAAI,MAAM,CAAC,QAAQ;QAAE,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACxD,IAAI,MAAM,CAAC,WAAW;QAAE,UAAU,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAC7D,IAAI,MAAM,CAAC,OAAO;QAAE,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/C,OAAO,gCAAgC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC;AACzF,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three named seed-state files plus the durable authority backup, and the monotonic contract
|
|
3
|
+
* that keeps a deliberately-removed connector removed across a version bump.
|
|
4
|
+
*
|
|
5
|
+
* `ever-seeded` is the authority: the set of built-ins that were EVER seeded. It only grows. A name
|
|
6
|
+
* in it but absent from the extensions manifest was removed on purpose (leave it removed); a name
|
|
7
|
+
* NOT in it was never seeded (seed it). Losing this set would resurrect removals, so it is mirrored
|
|
8
|
+
* into an append-only backup — the only thing `--repair` can recover from. Write order per reconcile:
|
|
9
|
+
* authority (with its backup) first, then the witness, then the stamp LAST, so a stamp is never
|
|
10
|
+
* current ahead of the authority it certifies.
|
|
11
|
+
*/
|
|
12
|
+
export interface Authority {
|
|
13
|
+
/** Built-in connector names ever seeded on this machine. Monotonic — never shrinks. */
|
|
14
|
+
readonly everSeeded: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface Witness {
|
|
17
|
+
readonly initialized: true;
|
|
18
|
+
/** The generation that first initialized this prefix (diagnostic; the stamp tracks the latest). */
|
|
19
|
+
readonly firstGeneration: string;
|
|
20
|
+
}
|
|
21
|
+
export interface Stamp {
|
|
22
|
+
/** The generation the last completed reconcile ran for. */
|
|
23
|
+
readonly generation: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function readAuthority(): Authority | undefined;
|
|
26
|
+
export declare function readAuthorityBackup(): Authority | undefined;
|
|
27
|
+
export declare function readWitness(): Witness | undefined;
|
|
28
|
+
export declare function readStamp(): Stamp | undefined;
|
|
29
|
+
/** The ever-seeded set unioned with its monotonic backup. A live authority that lost ids (a truncated
|
|
30
|
+
* or partially-corrupt write that still parsed) can only SHRINK the set; the backup is a superset, so
|
|
31
|
+
* unioning it on read means a deliberately-removed connector is never resurrected by such a loss.
|
|
32
|
+
* Returns undefined only when BOTH are absent (the genuinely-lost case the caller must fail loud on). */
|
|
33
|
+
export declare function everSeededUnion(): Set<string> | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Persist the ever-seeded authority. The backup is updated FIRST as a monotonic UNION (old backup ∪
|
|
36
|
+
* new set), so the recovery source is always a superset of the live authority and a crash between the
|
|
37
|
+
* two writes can only lose ids from the authority (which the backup then restores), never from both.
|
|
38
|
+
* An older binary that reads this must union unknown ids, never drop them.
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeAuthority(everSeeded: Set<string>): void;
|
|
41
|
+
/** Write the initialization witness once (idempotent; refreshing it would erase the first-seen gen). */
|
|
42
|
+
export declare function ensureWitness(generation: string): void;
|
|
43
|
+
/** Write the version stamp LAST — the only thing that flips the fast path to "reconcile complete". */
|
|
44
|
+
export declare function writeStamp(generation: string): void;
|
|
45
|
+
/** Recover a lost authority from the durable backup (for `--repair`). Undefined when even the backup
|
|
46
|
+
* is gone — the caller REFUSES, because a witness alone cannot tell a removed connector from a
|
|
47
|
+
* never-seeded future one. */
|
|
48
|
+
export declare function recoverAuthorityFromBackup(): Set<string> | undefined;
|
|
49
|
+
/** Move aside any seed-state file that no longer reads/validates, so a `--reset` can rebuild clean
|
|
50
|
+
* state instead of wedging when a commit helper re-reads the same corrupt file. Returns the
|
|
51
|
+
* quarantine paths (for reporting). `--reset` overwrites authority/backup/witness/stamp anyway; this
|
|
52
|
+
* just guarantees the pre-write reads (e.g. {@link ensureWitness}) can't throw on a corrupt one. */
|
|
53
|
+
export declare function quarantineCorruptSeedState(): string[];
|
|
54
|
+
//# sourceMappingURL=authority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authority.d.ts","sourceRoot":"","sources":["../../src/seed/authority.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,SAAS;IACxB,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;IAC3B,mGAAmG;IACnG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,KAAK;IACpB,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAeD,wBAAgB,aAAa,IAAI,SAAS,GAAG,SAAS,CAErD;AAED,wBAAgB,mBAAmB,IAAI,SAAS,GAAG,SAAS,CAE3D;AAED,wBAAgB,WAAW,IAAI,OAAO,GAAG,SAAS,CAEjD;AAED,wBAAgB,SAAS,IAAI,KAAK,GAAG,SAAS,CAE7C;AAED;;;0GAG0G;AAC1G,wBAAgB,eAAe,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAKzD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAK5D;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAEtD;AAED,sGAAsG;AACtG,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED;;+BAE+B;AAC/B,wBAAgB,0BAA0B,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAGpE;AAED;;;qGAGqG;AACrG,wBAAgB,0BAA0B,IAAI,MAAM,EAAE,CAmBrD"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, renameSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { authorityBackupPath, authorityPath, readJsonFile, stampPath, witnessPath, writeJsonAtomic, } from "./paths.js";
|
|
4
|
+
/** Every seed-state file is schema-validated on read: a syntactically-valid but wrong-shaped file is
|
|
5
|
+
* as dangerous as corrupt JSON (a truncated authority would resurrect removed connectors), so it is
|
|
6
|
+
* rejected the same way — loud, with the repair hint — never silently treated as a smaller set. */
|
|
7
|
+
function validate(path, value, ok) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (!ok(value))
|
|
11
|
+
throw new Error(`corrupt seed state ${path}: unexpected shape - repair with \`cotal ext seed --repair\` (or --reset)`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function isStringArray(v) {
|
|
15
|
+
return Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
16
|
+
}
|
|
17
|
+
export function readAuthority() {
|
|
18
|
+
return validate(authorityPath(), readJsonFile(authorityPath()), (v) => isStringArray(v.everSeeded));
|
|
19
|
+
}
|
|
20
|
+
export function readAuthorityBackup() {
|
|
21
|
+
return validate(authorityBackupPath(), readJsonFile(authorityBackupPath()), (v) => isStringArray(v.everSeeded));
|
|
22
|
+
}
|
|
23
|
+
export function readWitness() {
|
|
24
|
+
return validate(witnessPath(), readJsonFile(witnessPath()), (v) => v.initialized === true && typeof v.firstGeneration === "string");
|
|
25
|
+
}
|
|
26
|
+
export function readStamp() {
|
|
27
|
+
return validate(stampPath(), readJsonFile(stampPath()), (v) => typeof v.generation === "string");
|
|
28
|
+
}
|
|
29
|
+
/** The ever-seeded set unioned with its monotonic backup. A live authority that lost ids (a truncated
|
|
30
|
+
* or partially-corrupt write that still parsed) can only SHRINK the set; the backup is a superset, so
|
|
31
|
+
* unioning it on read means a deliberately-removed connector is never resurrected by such a loss.
|
|
32
|
+
* Returns undefined only when BOTH are absent (the genuinely-lost case the caller must fail loud on). */
|
|
33
|
+
export function everSeededUnion() {
|
|
34
|
+
const live = readAuthority()?.everSeeded;
|
|
35
|
+
const backup = readAuthorityBackup()?.everSeeded;
|
|
36
|
+
if (live === undefined && backup === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
return new Set([...(live ?? []), ...(backup ?? [])]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Persist the ever-seeded authority. The backup is updated FIRST as a monotonic UNION (old backup ∪
|
|
42
|
+
* new set), so the recovery source is always a superset of the live authority and a crash between the
|
|
43
|
+
* two writes can only lose ids from the authority (which the backup then restores), never from both.
|
|
44
|
+
* An older binary that reads this must union unknown ids, never drop them.
|
|
45
|
+
*/
|
|
46
|
+
export function writeAuthority(everSeeded) {
|
|
47
|
+
const prior = readAuthorityBackup()?.everSeeded ?? [];
|
|
48
|
+
const union = [...new Set([...prior, ...everSeeded])].sort();
|
|
49
|
+
writeJsonAtomic(authorityBackupPath(), { everSeeded: union });
|
|
50
|
+
writeJsonAtomic(authorityPath(), { everSeeded: [...everSeeded].sort() });
|
|
51
|
+
}
|
|
52
|
+
/** Write the initialization witness once (idempotent; refreshing it would erase the first-seen gen). */
|
|
53
|
+
export function ensureWitness(generation) {
|
|
54
|
+
if (!readWitness())
|
|
55
|
+
writeJsonAtomic(witnessPath(), { initialized: true, firstGeneration: generation });
|
|
56
|
+
}
|
|
57
|
+
/** Write the version stamp LAST — the only thing that flips the fast path to "reconcile complete". */
|
|
58
|
+
export function writeStamp(generation) {
|
|
59
|
+
writeJsonAtomic(stampPath(), { generation });
|
|
60
|
+
}
|
|
61
|
+
/** Recover a lost authority from the durable backup (for `--repair`). Undefined when even the backup
|
|
62
|
+
* is gone — the caller REFUSES, because a witness alone cannot tell a removed connector from a
|
|
63
|
+
* never-seeded future one. */
|
|
64
|
+
export function recoverAuthorityFromBackup() {
|
|
65
|
+
const backup = readAuthorityBackup();
|
|
66
|
+
return backup ? new Set(backup.everSeeded) : undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Move aside any seed-state file that no longer reads/validates, so a `--reset` can rebuild clean
|
|
69
|
+
* state instead of wedging when a commit helper re-reads the same corrupt file. Returns the
|
|
70
|
+
* quarantine paths (for reporting). `--reset` overwrites authority/backup/witness/stamp anyway; this
|
|
71
|
+
* just guarantees the pre-write reads (e.g. {@link ensureWitness}) can't throw on a corrupt one. */
|
|
72
|
+
export function quarantineCorruptSeedState() {
|
|
73
|
+
const readers = [
|
|
74
|
+
[authorityPath(), readAuthority],
|
|
75
|
+
[authorityBackupPath(), readAuthorityBackup],
|
|
76
|
+
[witnessPath(), readWitness],
|
|
77
|
+
[stampPath(), readStamp],
|
|
78
|
+
];
|
|
79
|
+
const quarantined = [];
|
|
80
|
+
for (const [path, read] of readers) {
|
|
81
|
+
if (!existsSync(path))
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
read();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
const aside = `${path}.corrupt.${randomBytes(4).toString("hex")}`;
|
|
88
|
+
renameSync(path, aside);
|
|
89
|
+
quarantined.push(aside);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return quarantined;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authority.js","sourceRoot":"","sources":["../../src/seed/authority.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reconcile-wide lock and the crash cursor.
|
|
3
|
+
*
|
|
4
|
+
* ONE lock guards a whole reconcile (not each `ext add` child) so an operator `ext add`/`remove`
|
|
5
|
+
* cannot interleave between seed children and strand a half-applied refresh decision. It rides the
|
|
6
|
+
* shared crash-safe advisory-lock primitive (atomic link publish so a racer never sees a canonical
|
|
7
|
+
* lock before its owner record; serialized reclaim so no inspect→remove targets a later generation;
|
|
8
|
+
* PID + process-start liveness so a recycled PID isn't mistaken for the holder; a bounded wait on a
|
|
9
|
+
* live holder rather than a false "interrupted"; nonce-guarded release).
|
|
10
|
+
*
|
|
11
|
+
* The cursor is the SIGKILL floor: before each connector mutation the reconcile journals
|
|
12
|
+
* `{nonce, package, phase}`, and clears it only after the stamp commits. A crash mid-reconcile leaves
|
|
13
|
+
* the cursor behind; the next boot distinguishes a LIVE reconcile (lock still actively held → wait)
|
|
14
|
+
* from a DEAD one (lock stale + cursor present → fail loud, `--repair`).
|
|
15
|
+
*/
|
|
16
|
+
export interface ReconcileLock {
|
|
17
|
+
readonly nonce: string;
|
|
18
|
+
release(): void;
|
|
19
|
+
}
|
|
20
|
+
/** Acquire the reconcile lock, waiting (bounded) on a live reconcile and reclaiming a dead owner's.
|
|
21
|
+
* A reconcile installs four connectors via npm, so the wait bound is generous. */
|
|
22
|
+
export declare function acquireReconcileLock(): ReconcileLock;
|
|
23
|
+
/** True while a LIVE process holds the reconcile lock (distinguishes an in-flight reconcile from a
|
|
24
|
+
* crashed one when a cursor is present). */
|
|
25
|
+
export declare function reconcileLockActive(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* True iff THIS process is a genuine seed child: it carries `COTAL_EXT_SEEDING=<nonce>` +
|
|
28
|
+
* `COTAL_EXT_SEEDING_PARENT=<pid>`, and a LIVE reconcile actually holds the lock under exactly that
|
|
29
|
+
* PID and nonce. A user cannot forge this to skip the mutation lock for an arbitrary `ext add` — the
|
|
30
|
+
* bare-`=1` bypass is gone; the marker must match the live lock the parent published.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isAuthenticSeedChild(): boolean;
|
|
33
|
+
/** The mid-reconcile journal: which package is being mutated, in which phase, under which lock nonce. */
|
|
34
|
+
export interface ReconcileCursor {
|
|
35
|
+
readonly nonce: string;
|
|
36
|
+
readonly package: string;
|
|
37
|
+
readonly phase: "copy" | "add";
|
|
38
|
+
}
|
|
39
|
+
export declare function writeCursor(cursor: ReconcileCursor): void;
|
|
40
|
+
/** Read + SCHEMA-validate the cursor: a torn write, or a parsed-but-malformed one (missing/invalid
|
|
41
|
+
* nonce, a non-official package, an unknown phase — e.g. `{}`) is corrupt, never a silent "no target"
|
|
42
|
+
* that lets a repair clear the journal and report success while a connector is still torn. */
|
|
43
|
+
export declare function readCursor(): ReconcileCursor | undefined;
|
|
44
|
+
export declare function clearCursor(): void;
|
|
45
|
+
/**
|
|
46
|
+
* A seed child's liveness marker. The PARENT records `pending` BEFORE it spawns (so the window never
|
|
47
|
+
* opens ownerless); the CHILD upgrades it to `live` with its own PID + start token as its first act;
|
|
48
|
+
* the parent clears it after the child exits. A `pending` marker whose parent has died is the ambiguous
|
|
49
|
+
* window — the child may or may not have started — and is resolved fail-loud, never by guessing.
|
|
50
|
+
*/
|
|
51
|
+
export type ChildMarker = {
|
|
52
|
+
readonly state: "pending";
|
|
53
|
+
readonly parentPid: number;
|
|
54
|
+
readonly nonce: string;
|
|
55
|
+
readonly ts: number;
|
|
56
|
+
} | {
|
|
57
|
+
readonly state: "live";
|
|
58
|
+
readonly childPid: number;
|
|
59
|
+
readonly childStart?: string;
|
|
60
|
+
readonly parentPid: number;
|
|
61
|
+
readonly nonce: string;
|
|
62
|
+
readonly ts: number;
|
|
63
|
+
};
|
|
64
|
+
/** Parent: record intent to spawn a seed child before `spawnSync`, so a crash never leaves a gap a
|
|
65
|
+
* concurrent repair could race through unaware. */
|
|
66
|
+
export declare function writePendingChildMarker(nonce: string): void;
|
|
67
|
+
/** Child: upgrade the parent's pending marker to a live one carrying this child's identity. */
|
|
68
|
+
export declare function markSeedChildLive(nonce: string, parentPid: number): void;
|
|
69
|
+
export declare function clearChildMarker(): void;
|
|
70
|
+
export type SeedChildStatus = {
|
|
71
|
+
readonly kind: "none";
|
|
72
|
+
} | {
|
|
73
|
+
readonly kind: "live";
|
|
74
|
+
readonly pid: number;
|
|
75
|
+
} | {
|
|
76
|
+
readonly kind: "ambiguous";
|
|
77
|
+
readonly path: string;
|
|
78
|
+
};
|
|
79
|
+
/** The state of any recorded seed child, for the orphan-vs-repair decision. A `live` child (its parent
|
|
80
|
+
* SIGKILL'd mid-install → orphan) must not be raced. A `pending` marker whose parent is gone, or a
|
|
81
|
+
* corrupt marker, is `ambiguous` (cannot tell "never spawned" from "child delayed") and is resolved
|
|
82
|
+
* fail-loud. A dead/recycled child is `none` (safe to proceed). */
|
|
83
|
+
export declare function seedChildStatus(): SeedChildStatus;
|
|
84
|
+
/**
|
|
85
|
+
* The durable maintenance-recovery obligation. A `--repair`/`--reset` writes it BEFORE it
|
|
86
|
+
* destructively quarantines a corrupt manifest/cursor, and clears it only at the final commit — so a
|
|
87
|
+
* SIGKILL after the quarantine but before the reinstalls is not forgotten: the next boot fails loud
|
|
88
|
+
* and the next repair re-derives the same obligation (rebuild the built-ins from on-disk truth, and/or
|
|
89
|
+
* reinstall+verify every seeded built-in) instead of stamping success over missing connectors.
|
|
90
|
+
*/
|
|
91
|
+
export interface RecoveryObligation {
|
|
92
|
+
readonly rebuildFromDisk?: boolean;
|
|
93
|
+
readonly repairAllSeeded?: boolean;
|
|
94
|
+
}
|
|
95
|
+
export declare function writeRecovery(obligation: RecoveryObligation): void;
|
|
96
|
+
/** Read + SCHEMA-validate the recovery obligation. A parsed-but-malformed marker (`{}`, `null`, an
|
|
97
|
+
* array, non-boolean fields, or an all-false obligation the writer never emits) is CORRUPT, not an
|
|
98
|
+
* empty valid obligation — it must throw so `recoveryPending()` stays true and maintenance recovers
|
|
99
|
+
* conservatively, never silently clearing the journal over still-damaged state. */
|
|
100
|
+
export declare function readRecovery(): RecoveryObligation | undefined;
|
|
101
|
+
/** True iff a recovery obligation is outstanding (any read failure counts — never silently ignored). */
|
|
102
|
+
export declare function recoveryPending(): boolean;
|
|
103
|
+
export declare function clearRecovery(): void;
|
|
104
|
+
/** Quarantine a CORRUPT (unparseable) crash CURSOR aside, so a `--repair`/`--reset` read of it can't
|
|
105
|
+
* wedge. The cursor is a journal, not a liveness signal, so moving it aside is safe; a corrupt CHILD
|
|
106
|
+
* MARKER is deliberately NOT touched here — it is resolved by {@link seedChildStatus}'s fail-loud
|
|
107
|
+
* (a marker we can't read might mean an installer is running). Returns the quarantine path(s). */
|
|
108
|
+
export declare function sanitizeCorruptCrashState(): string[];
|
|
109
|
+
//# sourceMappingURL=lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../src/seed/lock.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;mFACmF;AACnF,wBAAgB,oBAAoB,IAAI,aAAa,CAUpD;AAED;6CAC6C;AAC7C,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAM9C;AAED,yGAAyG;AACzG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;CAChC;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAEzD;AAED;;+FAE+F;AAC/F,wBAAgB,UAAU,IAAI,eAAe,GAAG,SAAS,CAWxD;AAED,wBAAgB,WAAW,IAAI,IAAI,CAElC;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACtG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjK;oDACoD;AACpD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CASxE;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D;;;oEAGoE;AACpE,wBAAgB,eAAe,IAAI,eAAe,CAmBjD;AAgBD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAElE;AAED;;;oFAGoF;AACpF,wBAAgB,YAAY,IAAI,kBAAkB,GAAG,SAAS,CAe7D;AAED,wGAAwG;AACxG,wBAAgB,eAAe,IAAI,OAAO,CAMzC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED;;;mGAGmG;AACnG,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAapD"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { acquireLock, inspectLock, processStartToken } from "@cotal-ai/workspace";
|
|
4
|
+
import { SEED_BUILTINS, reconcileChildPath, reconcileCursorPath, reconcileLockPath, reconcileRecoveryPath, readJsonFile, writeJsonAtomic } from "./paths.js";
|
|
5
|
+
/** Acquire the reconcile lock, waiting (bounded) on a live reconcile and reclaiming a dead owner's.
|
|
6
|
+
* A reconcile installs four connectors via npm, so the wait bound is generous. */
|
|
7
|
+
export function acquireReconcileLock() {
|
|
8
|
+
const held = acquireLock(reconcileLockPath(), {
|
|
9
|
+
label: "a connector reconcile",
|
|
10
|
+
waitMs: 300000,
|
|
11
|
+
onTimeout: (owner) => new Error(`another connector reconcile has held the lock for over 5 minutes (pid ${owner.pid}) - retry once it finishes, or \`cotal ext seed --repair\` if it is wedged`),
|
|
12
|
+
});
|
|
13
|
+
return { nonce: held.nonce, release: () => held.release() };
|
|
14
|
+
}
|
|
15
|
+
/** True while a LIVE process holds the reconcile lock (distinguishes an in-flight reconcile from a
|
|
16
|
+
* crashed one when a cursor is present). */
|
|
17
|
+
export function reconcileLockActive() {
|
|
18
|
+
return inspectLock(reconcileLockPath()).state === "active";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* True iff THIS process is a genuine seed child: it carries `COTAL_EXT_SEEDING=<nonce>` +
|
|
22
|
+
* `COTAL_EXT_SEEDING_PARENT=<pid>`, and a LIVE reconcile actually holds the lock under exactly that
|
|
23
|
+
* PID and nonce. A user cannot forge this to skip the mutation lock for an arbitrary `ext add` — the
|
|
24
|
+
* bare-`=1` bypass is gone; the marker must match the live lock the parent published.
|
|
25
|
+
*/
|
|
26
|
+
export function isAuthenticSeedChild() {
|
|
27
|
+
const nonce = process.env.COTAL_EXT_SEEDING;
|
|
28
|
+
const parent = Number(process.env.COTAL_EXT_SEEDING_PARENT);
|
|
29
|
+
if (!nonce || nonce === "1" || !Number.isInteger(parent) || parent <= 0)
|
|
30
|
+
return false;
|
|
31
|
+
const found = inspectLock(reconcileLockPath());
|
|
32
|
+
return found.state === "active" && found.owner.pid === parent && found.owner.nonce === nonce;
|
|
33
|
+
}
|
|
34
|
+
export function writeCursor(cursor) {
|
|
35
|
+
writeJsonAtomic(reconcileCursorPath(), cursor);
|
|
36
|
+
}
|
|
37
|
+
/** Read + SCHEMA-validate the cursor: a torn write, or a parsed-but-malformed one (missing/invalid
|
|
38
|
+
* nonce, a non-official package, an unknown phase — e.g. `{}`) is corrupt, never a silent "no target"
|
|
39
|
+
* that lets a repair clear the journal and report success while a connector is still torn. */
|
|
40
|
+
export function readCursor() {
|
|
41
|
+
const cursor = readJsonFile(reconcileCursorPath());
|
|
42
|
+
if (cursor === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (typeof cursor.nonce !== "string" ||
|
|
45
|
+
!SEED_BUILTINS.includes(cursor.package) ||
|
|
46
|
+
(cursor.phase !== "copy" && cursor.phase !== "add")) {
|
|
47
|
+
throw new Error(`corrupt seed cursor ${reconcileCursorPath()}: unexpected shape - repair with \`cotal ext seed --repair\` (or --reset)`);
|
|
48
|
+
}
|
|
49
|
+
return cursor;
|
|
50
|
+
}
|
|
51
|
+
export function clearCursor() {
|
|
52
|
+
rmSync(reconcileCursorPath(), { force: true });
|
|
53
|
+
}
|
|
54
|
+
/** Parent: record intent to spawn a seed child before `spawnSync`, so a crash never leaves a gap a
|
|
55
|
+
* concurrent repair could race through unaware. */
|
|
56
|
+
export function writePendingChildMarker(nonce) {
|
|
57
|
+
writeJsonAtomic(reconcileChildPath(), { state: "pending", parentPid: process.pid, nonce, ts: Date.now() });
|
|
58
|
+
}
|
|
59
|
+
/** Child: upgrade the parent's pending marker to a live one carrying this child's identity. */
|
|
60
|
+
export function markSeedChildLive(nonce, parentPid) {
|
|
61
|
+
writeJsonAtomic(reconcileChildPath(), {
|
|
62
|
+
state: "live",
|
|
63
|
+
childPid: process.pid,
|
|
64
|
+
childStart: processStartToken(process.pid),
|
|
65
|
+
parentPid,
|
|
66
|
+
nonce,
|
|
67
|
+
ts: Date.now(),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function clearChildMarker() {
|
|
71
|
+
rmSync(reconcileChildPath(), { force: true });
|
|
72
|
+
}
|
|
73
|
+
/** The state of any recorded seed child, for the orphan-vs-repair decision. A `live` child (its parent
|
|
74
|
+
* SIGKILL'd mid-install → orphan) must not be raced. A `pending` marker whose parent is gone, or a
|
|
75
|
+
* corrupt marker, is `ambiguous` (cannot tell "never spawned" from "child delayed") and is resolved
|
|
76
|
+
* fail-loud. A dead/recycled child is `none` (safe to proceed). */
|
|
77
|
+
export function seedChildStatus() {
|
|
78
|
+
const path = reconcileChildPath();
|
|
79
|
+
let marker;
|
|
80
|
+
try {
|
|
81
|
+
marker = readJsonFile(path);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { kind: "ambiguous", path }; // corrupt marker — never silently ignore
|
|
85
|
+
}
|
|
86
|
+
if (!marker)
|
|
87
|
+
return { kind: "none" };
|
|
88
|
+
if (marker.state === "live") {
|
|
89
|
+
if (typeof marker.childPid !== "number")
|
|
90
|
+
return { kind: "ambiguous", path };
|
|
91
|
+
return pidAliveMatching(marker.childPid, marker.childStart) ? { kind: "live", pid: marker.childPid } : { kind: "none" };
|
|
92
|
+
}
|
|
93
|
+
if (marker.state === "pending" && typeof marker.parentPid === "number") {
|
|
94
|
+
// Parent still alive ⇒ a reconcile is starting (the caller waits on the lock); parent gone ⇒ the
|
|
95
|
+
// ambiguous SIGKILL/delayed-start window.
|
|
96
|
+
return pidAliveMatching(marker.parentPid, undefined) ? { kind: "live", pid: marker.parentPid } : { kind: "ambiguous", path };
|
|
97
|
+
}
|
|
98
|
+
return { kind: "ambiguous", path };
|
|
99
|
+
}
|
|
100
|
+
function pidAliveMatching(pid, start) {
|
|
101
|
+
try {
|
|
102
|
+
process.kill(pid, 0);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (e.code === "ESRCH")
|
|
106
|
+
return false;
|
|
107
|
+
// EPERM ⇒ alive under another user; fall through to the start-token check.
|
|
108
|
+
}
|
|
109
|
+
if (start !== undefined) {
|
|
110
|
+
const now = processStartToken(pid);
|
|
111
|
+
if (now !== undefined && now !== start)
|
|
112
|
+
return false; // recycled PID
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
export function writeRecovery(obligation) {
|
|
117
|
+
writeJsonAtomic(reconcileRecoveryPath(), obligation);
|
|
118
|
+
}
|
|
119
|
+
/** Read + SCHEMA-validate the recovery obligation. A parsed-but-malformed marker (`{}`, `null`, an
|
|
120
|
+
* array, non-boolean fields, or an all-false obligation the writer never emits) is CORRUPT, not an
|
|
121
|
+
* empty valid obligation — it must throw so `recoveryPending()` stays true and maintenance recovers
|
|
122
|
+
* conservatively, never silently clearing the journal over still-damaged state. */
|
|
123
|
+
export function readRecovery() {
|
|
124
|
+
const recovery = readJsonFile(reconcileRecoveryPath());
|
|
125
|
+
if (recovery === undefined)
|
|
126
|
+
return undefined;
|
|
127
|
+
const shapeOk = typeof recovery === "object" &&
|
|
128
|
+
recovery !== null &&
|
|
129
|
+
!Array.isArray(recovery) &&
|
|
130
|
+
(recovery.rebuildFromDisk === undefined || typeof recovery.rebuildFromDisk === "boolean") &&
|
|
131
|
+
(recovery.repairAllSeeded === undefined || typeof recovery.repairAllSeeded === "boolean");
|
|
132
|
+
if (!shapeOk || !(recovery.rebuildFromDisk === true || recovery.repairAllSeeded === true)) {
|
|
133
|
+
throw new Error(`corrupt recovery journal ${reconcileRecoveryPath()}: expected { rebuildFromDisk?/repairAllSeeded?: boolean, at least one true } - repair with \`cotal ext seed --repair\` (or --reset)`);
|
|
134
|
+
}
|
|
135
|
+
return recovery;
|
|
136
|
+
}
|
|
137
|
+
/** True iff a recovery obligation is outstanding (any read failure counts — never silently ignored). */
|
|
138
|
+
export function recoveryPending() {
|
|
139
|
+
try {
|
|
140
|
+
return readRecovery() !== undefined;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function clearRecovery() {
|
|
147
|
+
rmSync(reconcileRecoveryPath(), { force: true });
|
|
148
|
+
}
|
|
149
|
+
/** Quarantine a CORRUPT (unparseable) crash CURSOR aside, so a `--repair`/`--reset` read of it can't
|
|
150
|
+
* wedge. The cursor is a journal, not a liveness signal, so moving it aside is safe; a corrupt CHILD
|
|
151
|
+
* MARKER is deliberately NOT touched here — it is resolved by {@link seedChildStatus}'s fail-loud
|
|
152
|
+
* (a marker we can't read might mean an installer is running). Returns the quarantine path(s). */
|
|
153
|
+
export function sanitizeCorruptCrashState() {
|
|
154
|
+
const aside = [];
|
|
155
|
+
const path = reconcileCursorPath();
|
|
156
|
+
if (existsSync(path)) {
|
|
157
|
+
try {
|
|
158
|
+
readCursor(); // schema-validating read: catches torn JSON AND parsed-but-malformed (e.g. `{}`)
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
const dest = `${path}.corrupt.${randomBytes(4).toString("hex")}`;
|
|
162
|
+
renameSync(path, dest);
|
|
163
|
+
aside.push(dest);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return aside;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/seed/lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem layout + version identity for the connector seeding engine. Everything the reconcile
|
|
3
|
+
* writes lives under `<config>/cotal/seed/`, a sibling of the `extensions/` npm prefix — the prefix
|
|
4
|
+
* is npm-owned (a stray dir there would confuse `npm install`), the seed state is ours.
|
|
5
|
+
*
|
|
6
|
+
* The seed store is DURABLE by design: `npm install --install-links <dir>` normalizes a `file:` dep
|
|
7
|
+
* pointing at `<dir>`, so a volatile source (an `npx` `_npx` cache) would later fail to reify. We
|
|
8
|
+
* copy each shipped connector payload into `store/<generation>/<name>` and `ext add` from THAT stable
|
|
9
|
+
* path, keyed by the binary's version so a `cotal-ai` upgrade lands a fresh generation.
|
|
10
|
+
*/
|
|
11
|
+
/** The four first-party connectors, in a stable seed order. Keys of {@link OFFICIAL_CONNECTORS}. */
|
|
12
|
+
export declare const SEED_BUILTINS: readonly string[];
|
|
13
|
+
/** `<config>/cotal/seed` — the seed engine's own state root (NOT inside the npm prefix). */
|
|
14
|
+
export declare function seedDir(): string;
|
|
15
|
+
/** The monotonic ever-seeded authority: which built-ins were ever seeded (never shrinks). */
|
|
16
|
+
export declare function authorityPath(): string;
|
|
17
|
+
/** The durable append-only backup of the authority — the ONLY source `ext seed --repair` can
|
|
18
|
+
* recover a lost {@link authorityPath} from (a witness alone can't tell removed from never-seeded). */
|
|
19
|
+
export declare function authorityBackupPath(): string;
|
|
20
|
+
/** The initialization witness: proof the seeder ever ran here (distinguishes a pristine prefix from
|
|
21
|
+
* one whose authority was lost). Written after the authority, before the stamp. */
|
|
22
|
+
export declare function witnessPath(): string;
|
|
23
|
+
/** The version stamp, written LAST: the generation the last reconcile completed for. A boot whose
|
|
24
|
+
* stamp equals the current generation short-circuits (after the health preamble). */
|
|
25
|
+
export declare function stampPath(): string;
|
|
26
|
+
/** The reconcile-wide lock DIRECTORY (shared advisory-lock primitive: atomic `mkdir` publish, PID +
|
|
27
|
+
* process-start liveness, bounded wait, dead-owner reclaim). */
|
|
28
|
+
export declare function reconcileLockPath(): string;
|
|
29
|
+
/** The durable reconcile cursor `{nonce, package, phase}`: a partial-reconcile journal a crashed
|
|
30
|
+
* (SIGKILL) run leaves behind, so the next boot fails loud instead of silently proceeding. */
|
|
31
|
+
export declare function reconcileCursorPath(): string;
|
|
32
|
+
/** The seed-child liveness marker `{pid, start, ts}`: a validated `ext add` seed child writes it
|
|
33
|
+
* before it mutates the prefix and clears it on exit, so a reclaim/repair after a parent SIGKILL can
|
|
34
|
+
* refuse to race an orphaned child that is still installing. */
|
|
35
|
+
export declare function reconcileChildPath(): string;
|
|
36
|
+
/** The durable maintenance-recovery obligation `{rebuildFromDisk?, repairAllSeeded?}`: a `--repair`/
|
|
37
|
+
* `--reset` writes it BEFORE it destructively quarantines a corrupt manifest/cursor, so a SIGKILL
|
|
38
|
+
* after the quarantine but before the reinstalls doesn't forget that the built-ins must be rebuilt
|
|
39
|
+
* from disk (or all reinstalled). Cleared only at the final commit. */
|
|
40
|
+
export declare function reconcileRecoveryPath(): string;
|
|
41
|
+
/** The durable seed store root: `<config>/cotal/seed/store`. */
|
|
42
|
+
export declare function seedStoreDir(): string;
|
|
43
|
+
/** A seeded connector's stable copy for one generation: `store/<generation>/<name>`. */
|
|
44
|
+
export declare function seedStorePath(generation: string, name: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* The seed generation: this `cotal-ai` binary's version. Ties a reconcile-refresh to the shipped
|
|
47
|
+
* payload changing — including the bundled `connector-core`, so a `cotal-ai` upgrade re-seeds the
|
|
48
|
+
* connectors that carry version-coupled behavior (e.g. the v0.4 lifecycle parse). Read from the
|
|
49
|
+
* nearest `package.json` above the entry script (`cotal-ai` published, or `bin/package.json` in a
|
|
50
|
+
* dev `tsx bin/cotal.ts` run). Fails loud if unreadable — a generation-less seed can't gate refresh.
|
|
51
|
+
*/
|
|
52
|
+
export declare function seedGeneration(): string;
|
|
53
|
+
/**
|
|
54
|
+
* The shipped payload directory for one built-in connector — the source the reconcile copies into the
|
|
55
|
+
* durable store. Dev resolves `extensions/<pkg-dir>` from this module's location (NEVER `cwd`);
|
|
56
|
+
* published resolves `<cotal-ai>/seeded-connectors/<name>` beside the entry (the `bin` prepublish
|
|
57
|
+
* copy). Fails loud if neither carries a `package.json` — a build missing its seeded connectors.
|
|
58
|
+
*/
|
|
59
|
+
export declare function shippedSourceDir(name: string): string;
|
|
60
|
+
/** Atomically write JSON (temp-then-rename, exclusive temp — never follows a pre-planted symlink),
|
|
61
|
+
* the same durability `writeLaunchSpec`/manifest writes use. Creates the parent dir. */
|
|
62
|
+
export declare function writeJsonAtomic(path: string, value: unknown, mode?: number): void;
|
|
63
|
+
/** Read + parse a JSON state file. Returns undefined when absent; throws (loud) on corrupt JSON — a
|
|
64
|
+
* seed-state file is never silently treated as missing (that would resurrect removed connectors). */
|
|
65
|
+
export declare function readJsonFile<T>(path: string): T | undefined;
|
|
66
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/seed/paths.ts"],"names":[],"mappings":"AAMA;;;;;;;;;GASG;AAEH,oGAAoG;AACpG,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAAqC,CAAC;AAEjF,4FAA4F;AAC5F,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,6FAA6F;AAC7F,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;wGACwG;AACxG,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;oFACoF;AACpF,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;sFACsF;AACtF,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;iEACiE;AACjE,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;+FAC+F;AAC/F,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;iEAEiE;AACjE,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;wEAGwE;AACxE,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,gEAAgE;AAChE,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAcvC;AASD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED;yFACyF;AACzF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,SAAQ,GAAG,IAAI,CAMhF;AAED;sGACsG;AACtG,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAa3D"}
|