@cotal-ai/cli 0.11.3 → 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/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 +2 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +14 -5
- package/dist/commands/up.js.map +1 -1
- package/dist/ext-loader.d.ts +3 -1
- package/dist/ext-loader.d.ts.map +1 -1
- package/dist/ext-loader.js +13 -62
- package/dist/ext-loader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- 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,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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { globalConfigDir } from "@cotal-ai/core";
|
|
5
|
+
import { OFFICIAL_CONNECTORS } from "@cotal-ai/workspace";
|
|
6
|
+
/**
|
|
7
|
+
* Filesystem layout + version identity for the connector seeding engine. Everything the reconcile
|
|
8
|
+
* writes lives under `<config>/cotal/seed/`, a sibling of the `extensions/` npm prefix — the prefix
|
|
9
|
+
* is npm-owned (a stray dir there would confuse `npm install`), the seed state is ours.
|
|
10
|
+
*
|
|
11
|
+
* The seed store is DURABLE by design: `npm install --install-links <dir>` normalizes a `file:` dep
|
|
12
|
+
* pointing at `<dir>`, so a volatile source (an `npx` `_npx` cache) would later fail to reify. We
|
|
13
|
+
* copy each shipped connector payload into `store/<generation>/<name>` and `ext add` from THAT stable
|
|
14
|
+
* path, keyed by the binary's version so a `cotal-ai` upgrade lands a fresh generation.
|
|
15
|
+
*/
|
|
16
|
+
/** The four first-party connectors, in a stable seed order. Keys of {@link OFFICIAL_CONNECTORS}. */
|
|
17
|
+
export const SEED_BUILTINS = Object.keys(OFFICIAL_CONNECTORS);
|
|
18
|
+
/** `<config>/cotal/seed` — the seed engine's own state root (NOT inside the npm prefix). */
|
|
19
|
+
export function seedDir() {
|
|
20
|
+
return join(globalConfigDir(), "seed");
|
|
21
|
+
}
|
|
22
|
+
/** The monotonic ever-seeded authority: which built-ins were ever seeded (never shrinks). */
|
|
23
|
+
export function authorityPath() {
|
|
24
|
+
return join(seedDir(), "authority.json");
|
|
25
|
+
}
|
|
26
|
+
/** The durable append-only backup of the authority — the ONLY source `ext seed --repair` can
|
|
27
|
+
* recover a lost {@link authorityPath} from (a witness alone can't tell removed from never-seeded). */
|
|
28
|
+
export function authorityBackupPath() {
|
|
29
|
+
return join(seedDir(), "authority.bak.json");
|
|
30
|
+
}
|
|
31
|
+
/** The initialization witness: proof the seeder ever ran here (distinguishes a pristine prefix from
|
|
32
|
+
* one whose authority was lost). Written after the authority, before the stamp. */
|
|
33
|
+
export function witnessPath() {
|
|
34
|
+
return join(seedDir(), "witness.json");
|
|
35
|
+
}
|
|
36
|
+
/** The version stamp, written LAST: the generation the last reconcile completed for. A boot whose
|
|
37
|
+
* stamp equals the current generation short-circuits (after the health preamble). */
|
|
38
|
+
export function stampPath() {
|
|
39
|
+
return join(seedDir(), "stamp.json");
|
|
40
|
+
}
|
|
41
|
+
/** The reconcile-wide lock DIRECTORY (shared advisory-lock primitive: atomic `mkdir` publish, PID +
|
|
42
|
+
* process-start liveness, bounded wait, dead-owner reclaim). */
|
|
43
|
+
export function reconcileLockPath() {
|
|
44
|
+
return join(seedDir(), "reconcile.lock");
|
|
45
|
+
}
|
|
46
|
+
/** The durable reconcile cursor `{nonce, package, phase}`: a partial-reconcile journal a crashed
|
|
47
|
+
* (SIGKILL) run leaves behind, so the next boot fails loud instead of silently proceeding. */
|
|
48
|
+
export function reconcileCursorPath() {
|
|
49
|
+
return join(seedDir(), "reconcile.cursor.json");
|
|
50
|
+
}
|
|
51
|
+
/** The seed-child liveness marker `{pid, start, ts}`: a validated `ext add` seed child writes it
|
|
52
|
+
* before it mutates the prefix and clears it on exit, so a reclaim/repair after a parent SIGKILL can
|
|
53
|
+
* refuse to race an orphaned child that is still installing. */
|
|
54
|
+
export function reconcileChildPath() {
|
|
55
|
+
return join(seedDir(), "reconcile.child.json");
|
|
56
|
+
}
|
|
57
|
+
/** The durable maintenance-recovery obligation `{rebuildFromDisk?, repairAllSeeded?}`: a `--repair`/
|
|
58
|
+
* `--reset` writes it BEFORE it destructively quarantines a corrupt manifest/cursor, so a SIGKILL
|
|
59
|
+
* after the quarantine but before the reinstalls doesn't forget that the built-ins must be rebuilt
|
|
60
|
+
* from disk (or all reinstalled). Cleared only at the final commit. */
|
|
61
|
+
export function reconcileRecoveryPath() {
|
|
62
|
+
return join(seedDir(), "reconcile.recovery.json");
|
|
63
|
+
}
|
|
64
|
+
/** The durable seed store root: `<config>/cotal/seed/store`. */
|
|
65
|
+
export function seedStoreDir() {
|
|
66
|
+
return join(seedDir(), "store");
|
|
67
|
+
}
|
|
68
|
+
/** A seeded connector's stable copy for one generation: `store/<generation>/<name>`. */
|
|
69
|
+
export function seedStorePath(generation, name) {
|
|
70
|
+
return join(seedStoreDir(), generation, name);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The seed generation: this `cotal-ai` binary's version. Ties a reconcile-refresh to the shipped
|
|
74
|
+
* payload changing — including the bundled `connector-core`, so a `cotal-ai` upgrade re-seeds the
|
|
75
|
+
* connectors that carry version-coupled behavior (e.g. the v0.4 lifecycle parse). Read from the
|
|
76
|
+
* nearest `package.json` above the entry script (`cotal-ai` published, or `bin/package.json` in a
|
|
77
|
+
* dev `tsx bin/cotal.ts` run). Fails loud if unreadable — a generation-less seed can't gate refresh.
|
|
78
|
+
*/
|
|
79
|
+
export function seedGeneration() {
|
|
80
|
+
const entry = process.argv[1];
|
|
81
|
+
if (!entry)
|
|
82
|
+
throw new Error("cannot determine the seed generation: no entry script (process.argv[1])");
|
|
83
|
+
let dir = dirname(entry);
|
|
84
|
+
for (;;) {
|
|
85
|
+
const pj = join(dir, "package.json");
|
|
86
|
+
if (existsSync(pj)) {
|
|
87
|
+
const version = JSON.parse(readFileSync(pj, "utf8")).version;
|
|
88
|
+
if (typeof version === "string" && version)
|
|
89
|
+
return version;
|
|
90
|
+
}
|
|
91
|
+
const parent = dirname(dir);
|
|
92
|
+
if (parent === dir)
|
|
93
|
+
throw new Error(`cannot determine the seed generation: no versioned package.json above ${entry}`);
|
|
94
|
+
dir = parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** This module's repo root in a source checkout: `implementations/cli/{src,dist}/seed` → up 4. Only
|
|
98
|
+
* meaningful in dev; a published `@cotal-ai/cli` resolves to `node_modules`, where the `extensions/`
|
|
99
|
+
* probe below simply misses and the published `seeded-connectors/` path wins. */
|
|
100
|
+
function repoRootFromHere() {
|
|
101
|
+
return join(import.meta.dirname, "..", "..", "..", "..");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The shipped payload directory for one built-in connector — the source the reconcile copies into the
|
|
105
|
+
* durable store. Dev resolves `extensions/<pkg-dir>` from this module's location (NEVER `cwd`);
|
|
106
|
+
* published resolves `<cotal-ai>/seeded-connectors/<name>` beside the entry (the `bin` prepublish
|
|
107
|
+
* copy). Fails loud if neither carries a `package.json` — a build missing its seeded connectors.
|
|
108
|
+
*/
|
|
109
|
+
export function shippedSourceDir(name) {
|
|
110
|
+
const pkg = OFFICIAL_CONNECTORS[name];
|
|
111
|
+
if (!pkg)
|
|
112
|
+
throw new Error(`"${name}" is not a first-party connector; only ${SEED_BUILTINS.join(", ")} are seeded`);
|
|
113
|
+
const devDir = join(repoRootFromHere(), "extensions", pkg.split("/")[1]);
|
|
114
|
+
if (existsSync(join(devDir, "package.json")))
|
|
115
|
+
return devDir;
|
|
116
|
+
const pubDir = join(dirname(process.argv[1] ?? ""), "..", "seeded-connectors", name);
|
|
117
|
+
if (existsSync(join(pubDir, "package.json")))
|
|
118
|
+
return pubDir;
|
|
119
|
+
throw new Error(`no shipped payload for connector "${name}" (looked in ${devDir} and ${pubDir}) - this cotal-ai build is missing its seeded connectors`);
|
|
120
|
+
}
|
|
121
|
+
/** Atomically write JSON (temp-then-rename, exclusive temp — never follows a pre-planted symlink),
|
|
122
|
+
* the same durability `writeLaunchSpec`/manifest writes use. Creates the parent dir. */
|
|
123
|
+
export function writeJsonAtomic(path, value, mode = 0o600) {
|
|
124
|
+
const dir = dirname(path);
|
|
125
|
+
mkdirSync(dir, { recursive: true });
|
|
126
|
+
const tmp = join(dir, `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
127
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode, flag: "wx" });
|
|
128
|
+
renameSync(tmp, path);
|
|
129
|
+
}
|
|
130
|
+
/** Read + parse a JSON state file. Returns undefined when absent; throws (loud) on corrupt JSON — a
|
|
131
|
+
* seed-state file is never silently treated as missing (that would resurrect removed connectors). */
|
|
132
|
+
export function readJsonFile(path) {
|
|
133
|
+
let raw;
|
|
134
|
+
try {
|
|
135
|
+
raw = readFileSync(path, "utf8");
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
if (e.code === "ENOENT")
|
|
139
|
+
return undefined;
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(raw);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
throw new Error(`corrupt seed state ${path}: ${e.message} - repair with \`cotal ext seed --repair\` (or --reset)`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/seed/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SeedFlags {
|
|
2
|
+
readonly repair?: boolean;
|
|
3
|
+
readonly reset?: boolean;
|
|
4
|
+
readonly force?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ReconcileResult {
|
|
7
|
+
readonly noop: boolean;
|
|
8
|
+
readonly seeded: string[];
|
|
9
|
+
readonly refreshed: string[];
|
|
10
|
+
readonly removedKept: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Auto reconcile for the boot gate: silent no-op when the stamp is current and healthy. */
|
|
13
|
+
export declare function reconcileSeededConnectors(): Promise<void>;
|
|
14
|
+
/** `cotal ext seed [--repair|--reset|--force]`: the maintenance entry. Prints a summary. */
|
|
15
|
+
export declare function runSeed(flags: SeedFlags): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/seed/reconcile.ts"],"names":[],"mappings":"AAoEA,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,4FAA4F;AAC5F,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/D;AAED,4FAA4F;AAC5F,wBAAsB,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAY7D"}
|