@theford/runly 0.0.0-stage → 0.20.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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/dist/cli/client.d.ts +80 -0
  4. package/dist/cli/client.js +294 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/daemon-unit.d.ts +58 -0
  7. package/dist/cli/daemon-unit.js +289 -0
  8. package/dist/cli/daemon-unit.js.map +1 -0
  9. package/dist/cli/index.d.ts +2 -0
  10. package/dist/cli/index.js +1482 -0
  11. package/dist/cli/index.js.map +1 -0
  12. package/dist/cli/install.d.ts +9 -0
  13. package/dist/cli/install.js +53 -0
  14. package/dist/cli/install.js.map +1 -0
  15. package/dist/cli/logs.d.ts +43 -0
  16. package/dist/cli/logs.js +232 -0
  17. package/dist/cli/logs.js.map +1 -0
  18. package/dist/cli/tether.d.ts +3 -0
  19. package/dist/cli/tether.js +65 -0
  20. package/dist/cli/tether.js.map +1 -0
  21. package/dist/cli/watchdog.d.ts +1 -0
  22. package/dist/cli/watchdog.js +81 -0
  23. package/dist/cli/watchdog.js.map +1 -0
  24. package/dist/core/builds.d.ts +20 -0
  25. package/dist/core/builds.js +93 -0
  26. package/dist/core/builds.js.map +1 -0
  27. package/dist/core/caller-env.d.ts +17 -0
  28. package/dist/core/caller-env.js +77 -0
  29. package/dist/core/caller-env.js.map +1 -0
  30. package/dist/core/diagnostics.d.ts +68 -0
  31. package/dist/core/diagnostics.js +34 -0
  32. package/dist/core/diagnostics.js.map +1 -0
  33. package/dist/core/env-vars.d.ts +35 -0
  34. package/dist/core/env-vars.js +32 -0
  35. package/dist/core/env-vars.js.map +1 -0
  36. package/dist/core/events.d.ts +10 -0
  37. package/dist/core/events.js +42 -0
  38. package/dist/core/events.js.map +1 -0
  39. package/dist/core/exec.d.ts +22 -0
  40. package/dist/core/exec.js +84 -0
  41. package/dist/core/exec.js.map +1 -0
  42. package/dist/core/journal.d.ts +227 -0
  43. package/dist/core/journal.js +395 -0
  44. package/dist/core/journal.js.map +1 -0
  45. package/dist/core/logs.d.ts +56 -0
  46. package/dist/core/logs.js +259 -0
  47. package/dist/core/logs.js.map +1 -0
  48. package/dist/core/manifest.d.ts +292 -0
  49. package/dist/core/manifest.js +179 -0
  50. package/dist/core/manifest.js.map +1 -0
  51. package/dist/core/namespaces.d.ts +7 -0
  52. package/dist/core/namespaces.js +59 -0
  53. package/dist/core/namespaces.js.map +1 -0
  54. package/dist/core/paths.d.ts +31 -0
  55. package/dist/core/paths.js +65 -0
  56. package/dist/core/paths.js.map +1 -0
  57. package/dist/core/policy.d.ts +94 -0
  58. package/dist/core/policy.js +122 -0
  59. package/dist/core/policy.js.map +1 -0
  60. package/dist/core/ports.d.ts +52 -0
  61. package/dist/core/ports.js +94 -0
  62. package/dist/core/ports.js.map +1 -0
  63. package/dist/core/presets.d.ts +25 -0
  64. package/dist/core/presets.js +86 -0
  65. package/dist/core/presets.js.map +1 -0
  66. package/dist/core/procscan.d.ts +111 -0
  67. package/dist/core/procscan.js +342 -0
  68. package/dist/core/procscan.js.map +1 -0
  69. package/dist/core/retention.d.ts +97 -0
  70. package/dist/core/retention.js +322 -0
  71. package/dist/core/retention.js.map +1 -0
  72. package/dist/core/sleep.d.ts +51 -0
  73. package/dist/core/sleep.js +99 -0
  74. package/dist/core/sleep.js.map +1 -0
  75. package/dist/core/tree-ledger.d.ts +39 -0
  76. package/dist/core/tree-ledger.js +92 -0
  77. package/dist/core/tree-ledger.js.map +1 -0
  78. package/dist/core/types.d.ts +21 -0
  79. package/dist/core/types.js +6 -0
  80. package/dist/core/types.js.map +1 -0
  81. package/dist/core/units.d.ts +20 -0
  82. package/dist/core/units.js +69 -0
  83. package/dist/core/units.js.map +1 -0
  84. package/dist/core/upkeep.d.ts +90 -0
  85. package/dist/core/upkeep.js +176 -0
  86. package/dist/core/upkeep.js.map +1 -0
  87. package/dist/core/util.d.ts +46 -0
  88. package/dist/core/util.js +168 -0
  89. package/dist/core/util.js.map +1 -0
  90. package/dist/core/version.d.ts +39 -0
  91. package/dist/core/version.js +144 -0
  92. package/dist/core/version.js.map +1 -0
  93. package/dist/core/worktree.d.ts +51 -0
  94. package/dist/core/worktree.js +309 -0
  95. package/dist/core/worktree.js.map +1 -0
  96. package/dist/daemon/budget.d.ts +155 -0
  97. package/dist/daemon/budget.js +288 -0
  98. package/dist/daemon/budget.js.map +1 -0
  99. package/dist/daemon/election.d.ts +9 -0
  100. package/dist/daemon/election.js +140 -0
  101. package/dist/daemon/election.js.map +1 -0
  102. package/dist/daemon/engine.d.ts +1703 -0
  103. package/dist/daemon/engine.js +5305 -0
  104. package/dist/daemon/engine.js.map +1 -0
  105. package/dist/daemon/index.d.ts +2 -0
  106. package/dist/daemon/index.js +412 -0
  107. package/dist/daemon/index.js.map +1 -0
  108. package/dist/daemon/proxy.d.ts +108 -0
  109. package/dist/daemon/proxy.js +615 -0
  110. package/dist/daemon/proxy.js.map +1 -0
  111. package/dist/daemon/supervisor.d.ts +101 -0
  112. package/dist/daemon/supervisor.js +493 -0
  113. package/dist/daemon/supervisor.js.map +1 -0
  114. package/dist/drivers/appliances.d.ts +11 -0
  115. package/dist/drivers/appliances.js +174 -0
  116. package/dist/drivers/appliances.js.map +1 -0
  117. package/dist/drivers/datastores.d.ts +144 -0
  118. package/dist/drivers/datastores.js +775 -0
  119. package/dist/drivers/datastores.js.map +1 -0
  120. package/dist/drivers/preview.d.ts +69 -0
  121. package/dist/drivers/preview.js +605 -0
  122. package/dist/drivers/preview.js.map +1 -0
  123. package/dist/drivers/types.d.ts +70 -0
  124. package/dist/drivers/types.js +13 -0
  125. package/dist/drivers/types.js.map +1 -0
  126. package/package.json +63 -4
  127. package/schema/backlot.schema.json +549 -0
  128. package/schema/runly.schema.json +556 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Appliances: singular, machine-shared backing services (a Postgres server, a
3
+ * Redis, a MinIO) that environments point at but never own.
4
+ *
5
+ * The contract is ENSURE, NOT OWN (decision 0018). An appliance's identity is
6
+ * its probe address: whoever answers `host:port` IS the appliance, no matter
7
+ * who started it — a compose file yesterday, a teammate's shell, or runly a
8
+ * minute ago. runly never supervises the process (the `start:` command is
9
+ * expected to daemonize, e.g. `docker run -d`), never stops one automatically,
10
+ * and blames nobody's code when one is missing: every failure here is an
11
+ * infra-error by construction.
12
+ */
13
+ import { runBounded, DEFAULT_CMD_TIMEOUT_S } from '../core/exec.js';
14
+ import { connect } from 'node:net';
15
+ import { mkdirSync, rmdirSync, statSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { stateRoot } from '../core/paths.js';
18
+ import { BrokerError, now, sha256 } from '../core/util.js';
19
+ const PROBE_TIMEOUT_MS = 3000;
20
+ const START_POLL_MS = 500;
21
+ /** A start lock older than this is a crashed starter, not a live one. */
22
+ /**
23
+ * A start lock is stale once nobody could still be legitimately holding it.
24
+ * A fixed 5 minutes was WRONG for an appliance declaring a longer timeout: a
25
+ * second caller stole the lock mid-start and raced the first one's container.
26
+ */
27
+ const LOCK_STALE_MS = 5 * 60 * 1000;
28
+ const lockStaleMs = (timeoutS) => Math.max(LOCK_STALE_MS, (timeoutS ?? 0) * 1000 + 60_000);
29
+ export function probeTcp(target, timeoutMs = PROBE_TIMEOUT_MS) {
30
+ const [host, portStr] = target.split(':');
31
+ const port = Number(portStr);
32
+ return new Promise((resolve) => {
33
+ const sock = connect({ host: host || 'localhost', port, timeout: timeoutMs });
34
+ sock.once('connect', () => {
35
+ sock.end();
36
+ resolve(true);
37
+ });
38
+ const fail = () => {
39
+ sock.destroy();
40
+ resolve(false);
41
+ };
42
+ sock.once('error', fail);
43
+ sock.once('timeout', fail);
44
+ });
45
+ }
46
+ /**
47
+ * Appliance commands are the likeliest to hang: `start: docker run ...` without
48
+ * `-d` never returns, and an unbounded wait there wedges the bind AND leaves
49
+ * the environment permanently busy (so its lease never expires and teardown
50
+ * refuses it). Bounded, in its own process group.
51
+ */
52
+ const sh = async (cmd, cwd) => {
53
+ const r = await runBounded(cmd, cwd);
54
+ return {
55
+ code: r.code,
56
+ output: r.timedOut ? `${r.output}\n[killed: exceeded ${DEFAULT_CMD_TIMEOUT_S}s]` : r.output,
57
+ };
58
+ };
59
+ /**
60
+ * One start attempt per probe target machine-wide, whatever stack asked.
61
+ * Two stacks declaring "the postgres on 5433" race to the same lock, and the
62
+ * loser adopts what the winner started.
63
+ */
64
+ function lockDir(target) {
65
+ const dir = join(stateRoot(), 'appliances');
66
+ mkdirSync(dir, { recursive: true });
67
+ return join(dir, `${target.replace(/[^A-Za-z0-9._-]/g, '_')}-${sha256(target).slice(0, 8)}.lock`);
68
+ }
69
+ function acquireLock(target, timeoutS) {
70
+ const lock = lockDir(target);
71
+ try {
72
+ mkdirSync(lock);
73
+ return true;
74
+ }
75
+ catch {
76
+ try {
77
+ if (now() - statSync(lock).mtimeMs > lockStaleMs(timeoutS)) {
78
+ rmdirSync(lock);
79
+ mkdirSync(lock);
80
+ return true;
81
+ }
82
+ }
83
+ catch {
84
+ /* raced — treat as not acquired */
85
+ }
86
+ return false;
87
+ }
88
+ }
89
+ function releaseLock(target) {
90
+ try {
91
+ rmdirSync(lockDir(target));
92
+ }
93
+ catch {
94
+ /* already gone */
95
+ }
96
+ }
97
+ async function awaitUp(name, spec, root, deadline) {
98
+ while (now() < deadline) {
99
+ if (await probeTcp(spec.probe)) {
100
+ if (!spec.ready)
101
+ return true;
102
+ const r = await sh(spec.ready, root);
103
+ if (r.code === 0)
104
+ return true;
105
+ }
106
+ await new Promise((r) => setTimeout(r, START_POLL_MS));
107
+ }
108
+ return false;
109
+ }
110
+ /**
111
+ * Probe; if down and a `start:` is declared, start it (once, machine-wide) and
112
+ * wait for the probe (and optional `ready:` gate) to accept. Throws infra-error
113
+ * when the appliance stays unreachable — with the start output when we tried.
114
+ */
115
+ export async function ensureAppliance(name, spec, root, say) {
116
+ const timeoutMsEarly = (spec.timeout ?? 60) * 1000;
117
+ if (await probeTcp(spec.probe)) {
118
+ // An open TCP port is NOT readiness. Postgres accepts connections while
119
+ // still recovering; a container forwards the port before the server is
120
+ // listening. Returning 'up' here skipped the declared ready: gate entirely,
121
+ // so the bind proceeded against a half-started appliance and the failure
122
+ // surfaced later as an unrelated work-error.
123
+ if (!spec.ready)
124
+ return 'up';
125
+ const first = await sh(spec.ready, root);
126
+ if (first.code === 0)
127
+ return 'up';
128
+ say(`appliance '${name}' answers at ${spec.probe} but is not ready yet — waiting`);
129
+ if (await awaitUp(name, spec, root, now() + timeoutMsEarly))
130
+ return 'adopted';
131
+ throw new BrokerError('infra-error', `appliance '${name}' answers at ${spec.probe} but its ready: gate never passed within ${spec.timeout ?? 60}s`, 'appliance', first.output.slice(0, 800));
132
+ }
133
+ if (!spec.start) {
134
+ throw new BrokerError('infra-error', `appliance '${name}' unreachable at ${spec.probe} and declares no start: — bring it up yourself`, 'appliance');
135
+ }
136
+ const timeoutMs = (spec.timeout ?? 60) * 1000;
137
+ const deadline = now() + timeoutMs;
138
+ if (!acquireLock(spec.probe, spec.timeout)) {
139
+ // Someone else is starting it right now; wait for their result.
140
+ say(`appliance '${name}' is being started by another consumer — waiting`);
141
+ if (await awaitUp(name, spec, root, deadline))
142
+ return 'adopted';
143
+ throw new BrokerError('infra-error', `appliance '${name}' still unreachable at ${spec.probe} after waiting ${spec.timeout ?? 60}s for a concurrent start`, 'appliance');
144
+ }
145
+ try {
146
+ // Locked. Re-probe: it may have come up between our probe and the lock.
147
+ // Same rule as above — the ready: gate decides, not the open port.
148
+ if (await probeTcp(spec.probe) && (await awaitUp(name, spec, root, now() + 1000)))
149
+ return 'up';
150
+ say(`starting appliance '${name}'`);
151
+ const r = await sh(spec.start, root);
152
+ if (r.code !== 0) {
153
+ throw new BrokerError('infra-error', `appliance '${name}' start command failed`, 'appliance', r.output.slice(0, 800));
154
+ }
155
+ if (!(await awaitUp(name, spec, root, deadline))) {
156
+ throw new BrokerError('infra-error', `appliance '${name}' started but never answered at ${spec.probe} within ${spec.timeout ?? 60}s`, 'appliance', r.output.slice(0, 800));
157
+ }
158
+ return 'started';
159
+ }
160
+ finally {
161
+ releaseLock(spec.probe);
162
+ }
163
+ }
164
+ /** Explicit stop verb only — runly never stops an appliance on its own. */
165
+ export async function stopAppliance(name, spec, root) {
166
+ if (!spec.stop) {
167
+ throw new BrokerError('work-error', `appliance '${name}' declares no stop: command`, 'appliance');
168
+ }
169
+ const r = await sh(spec.stop, root);
170
+ if (r.code !== 0) {
171
+ throw new BrokerError('infra-error', `appliance '${name}' stop command failed`, 'appliance', r.output.slice(0, 800));
172
+ }
173
+ }
174
+ //# sourceMappingURL=appliances.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appliances.js","sourceRoot":"","sources":["../../src/drivers/appliances.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ import { type DatastoreSpec } from '../core/manifest.js';
2
+ export interface DsHandle {
3
+ envId: string;
4
+ /**
5
+ * Where the datastore's repo commands (create/drop/template_restore) run:
6
+ * the caller's worktree, which is where the environment runs (decision
7
+ * 0032). Teardown falls back to the environment's own directory when the
8
+ * worktree is already gone.
9
+ */
10
+ cwd: string;
11
+ dataDir: string;
12
+ }
13
+ export interface DsDriver {
14
+ readonly name: string;
15
+ /** The ns for an environment: sqlite = file path; server = SQL-safe db name. */
16
+ ns(h: DsHandle): string;
17
+ /** The connection string services/checks receive. */
18
+ url(h: DsHandle): string;
19
+ /** infra-error (never code blame) when the external server is unreachable. */
20
+ probe(): Promise<void>;
21
+ /** Create/restore the namespace at the preset. force = recreate even if present. */
22
+ ensure(h: DsHandle, preset: string, force: boolean, exists: boolean): Promise<void>;
23
+ /** Best-effort removal (recycle). */
24
+ drop(h: DsHandle): Promise<void>;
25
+ /**
26
+ * The drop as data, for a row that must be reapable without the manifest
27
+ * (a `runly db` copy, decision 0034): the already-templated command for a
28
+ * server datastore, or null when the namespace is a file under the state
29
+ * root (sqlite) — removing that is the engine's to do.
30
+ */
31
+ dropCommand(h: DsHandle): string | null;
32
+ /** True when the namespace is a local file (sqlite), false for a server database. */
33
+ readonly fileBased: boolean;
34
+ /**
35
+ * How to drop this namespace later without the manifest (decision 0037):
36
+ * the templated command and where to run it, or the file to delete.
37
+ */
38
+ dropRecipe(h: DsHandle): {
39
+ cmd?: string;
40
+ cwd?: string;
41
+ path?: string;
42
+ ns?: string;
43
+ };
44
+ /**
45
+ * The template a restore at `preset` uses, as `<stack>/<marker file>` under
46
+ * the templates root, or null when this datastore bakes none. Retention
47
+ * keeps a template while a row references it (decision 0037).
48
+ */
49
+ templateRef(preset: string): string | null;
50
+ /**
51
+ * Drop this datastore's CURRENT templates (every preset, the current
52
+ * template key) and their server-side databases, so the next restore bakes
53
+ * afresh. Only `--pristine` asks for this (decision 0039): a template whose
54
+ * key matches was baked from the same create command and trigger content,
55
+ * so an `@rebake-template` rule that fires only says which template is
56
+ * current — it never rebakes one that already exists.
57
+ */
58
+ rebake(cwd?: string): void | Promise<void>;
59
+ /** Is the template a restore at `preset` would use already baked? Always false without templates. */
60
+ templateBaked(preset: string): boolean;
61
+ }
62
+ /** Exclusive: bake, rebake, prune or drop the stack's templates. */
63
+ export declare function withBakeLock<T>(key: string, fn: () => Promise<T>): Promise<T>;
64
+ /** Shared: restore from one of the stack's templates; no writer runs meanwhile. */
65
+ export declare function withTemplateRead<T>(key: string, fn: () => Promise<T>): Promise<T>;
66
+ /**
67
+ * One datastore's templates: the stack lock shared, then this datastore's lock
68
+ * — exclusive (`write`) to bake or drop one, shared to restore from one. The
69
+ * order is always stack, then datastore, and nothing waits for the stack lock
70
+ * while holding a datastore lock, so the two levels cannot deadlock.
71
+ */
72
+ export declare function withDatastoreLock<T>(stackId: string, ds: string, write: boolean, fn: () => Promise<T>): Promise<T>;
73
+ /**
74
+ * Baked-template markers are self-describing (vetbill-1i49): they carry the
75
+ * server-side template ns AND the already-templated drop command, so
76
+ * retention/rebake can DROP the actual database when the marker is pruned —
77
+ * previously only the marker file was deleted and `backlot_tpl_*` databases
78
+ * leaked on the appliance forever. Legacy markers (bare ns string) still
79
+ * parse; they just can't be dropped server-side.
80
+ */
81
+ export interface BakedMarker {
82
+ v: 1;
83
+ ns: string;
84
+ drop: string | null;
85
+ /** The datastore it was baked for (0.18+), so a rebake drops only that datastore's. */
86
+ ds?: string;
87
+ /**
88
+ * When, and by which bake (0.19): two bakes of one key write different
89
+ * markers, so a restore that failed can tell a sibling's rebake from the
90
+ * template it failed on — the content alone is deterministic.
91
+ */
92
+ bakedAt?: number;
93
+ nonce?: string;
94
+ /** In `<name>@shared/` (0.20, decision 0044). */
95
+ shared?: boolean;
96
+ /** A `--pristine` bake private to one worktree of a shared datastore (0.20). */
97
+ private?: boolean;
98
+ /** The stack dir a shared marker was adopted from, naming its database (0.20). */
99
+ adoptedFrom?: string;
100
+ }
101
+ export declare function parseBakedMarker(content: string): BakedMarker;
102
+ export declare function hasOtherTemplateOwner(full: string, ns: string): boolean;
103
+ /**
104
+ * Shared templates (decision 0044). A template whose key folds in the content
105
+ * of its `@rebake-template` trigger files is a function of (manifest name,
106
+ * datastore, preset, create command, trigger content) — never of the worktree
107
+ * path — so every worktree of one manifest name on this daemon can restore
108
+ * from the same one. They live in `<name>@shared/` next to the per-stack dirs
109
+ * (`<name>-<8 chars>`); a manifest name cannot contain `@`, so the two never
110
+ * collide.
111
+ */
112
+ export declare const SHARED_TEMPLATE_SUFFIX = "@shared";
113
+ export declare const sharedTemplateDir: (project: string) => string;
114
+ export declare const isSharedTemplateDir: (dir: string) => boolean;
115
+ /** The manifest name a templates dir belongs to: `<name>@shared`, or a stack id `<name>-<8-char hash>`. */
116
+ export declare function templateProject(dir: string): string;
117
+ /**
118
+ * A template baked by `--pristine` for one worktree of a shared datastore
119
+ * (decision 0044): `<ds>-<preset>@<key>.own.<ext>` in the stack's own dir. It
120
+ * outranks the shared one for that worktree while it exists, and retention
121
+ * keeps it only while a row references it.
122
+ */
123
+ export declare const PRIVATE_TEMPLATE_TAG = ".own";
124
+ export declare const isPrivateTemplateFile: (file: string) => boolean;
125
+ /** How a datastore's templates are scoped. */
126
+ export interface TemplateScope {
127
+ /** Content bake key: the `@rebake-template` trigger content (vetbill-1i49). */
128
+ bakeKey?: string;
129
+ /**
130
+ * The manifest name. With a bake key and without `share_templates: false`,
131
+ * the templates are shared by every worktree of this name (decision 0044).
132
+ * Omitted: per stack, as before 0.20.
133
+ */
134
+ project?: string;
135
+ /** `--pristine` on a shared datastore: bake a template private to this worktree instead of dropping the shared one. */
136
+ privateBake?: boolean;
137
+ }
138
+ /** Whether a datastore's templates are shared across worktrees (decision 0044). */
139
+ export declare function sharesTemplates(spec: DatastoreSpec, scope: TemplateScope): boolean;
140
+ /**
141
+ * `scope` is the template scope; a bare string is the bake key alone (per
142
+ * stack, as before 0.20 — unit tests and callers that only read the url).
143
+ */
144
+ export declare function makeDatastore(name: string, spec: DatastoreSpec, stackId: string, scope?: string | TemplateScope): DsDriver;