@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,81 @@
1
+ /**
2
+ * `runly db with`'s watchdog (0.19): it runs the command, and takes it down
3
+ * when the CLI that started it dies.
4
+ *
5
+ * The CLI drops the database copy after the command exits. If the CLI itself
6
+ * is killed (SIGKILL, OOM, a `kill` of its process group that missed a command
7
+ * in a group of its own — `setsid runly db with …`), the daemon drops the copy
8
+ * once it notices, and the command used to keep working against a database
9
+ * that no longer exists. Node cannot set PR_SET_PDEATHSIG, so the tie is a
10
+ * pipe: the CLI holds the write end of fd 3, the kernel closes it when the CLI
11
+ * dies however it dies, and this process sees EOF.
12
+ *
13
+ * argv: <shell|argv> <group|nogroup> <command…>. `group`: this process leads
14
+ * the command's process group (no terminal), and the whole group goes. At a
15
+ * terminal the command stays in the caller's job and only it is signalled.
16
+ *
17
+ * The command's exit code (or the signal that ended it) is this process's, so
18
+ * the CLI passes it through unchanged.
19
+ */
20
+ import { spawn } from 'node:child_process';
21
+ import { Socket } from 'node:net';
22
+ const [mode, grouping, ...parts] = process.argv.slice(2);
23
+ const group = grouping === 'group';
24
+ const command = parts[0];
25
+ if (command === undefined) {
26
+ console.error('runly db with: watchdog started without a command');
27
+ process.exit(64);
28
+ }
29
+ const child = mode === 'shell'
30
+ ? spawn(command, { stdio: 'inherit', shell: true })
31
+ : spawn(command, parts.slice(1), { stdio: 'inherit' });
32
+ const signalCommand = (sig) => {
33
+ try {
34
+ if (group)
35
+ process.kill(-process.pid, sig);
36
+ else
37
+ child.kill(sig);
38
+ }
39
+ catch {
40
+ /* already gone */
41
+ }
42
+ };
43
+ // A signal the CLI passes on: in a group of our own the whole group (the
44
+ // command included) already received it; at a terminal only this process did.
45
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
46
+ process.on(sig, () => {
47
+ if (!group)
48
+ child.kill(sig);
49
+ });
50
+ }
51
+ let cliGone = false;
52
+ const onCliGone = () => {
53
+ if (cliGone)
54
+ return;
55
+ cliGone = true;
56
+ signalCommand('SIGTERM');
57
+ // A command that ignores SIGTERM goes anyway: nothing may go on working
58
+ // against a copy that is about to be dropped.
59
+ setTimeout(() => signalCommand('SIGKILL'), 3000).unref();
60
+ };
61
+ // A stream handle, not an fs read: a blocking read on fd 3 would sit in the
62
+ // threadpool and keep process.exit() from returning.
63
+ const pipe = new Socket({ fd: 3, readable: true, writable: false });
64
+ pipe.on('data', () => undefined);
65
+ pipe.on('end', onCliGone);
66
+ pipe.on('close', onCliGone);
67
+ pipe.on('error', onCliGone);
68
+ child.on('error', (err) => {
69
+ console.error(`runly db with: could not start '${command}': ${err.message}`);
70
+ process.exit(127);
71
+ });
72
+ child.on('exit', (code, signal) => {
73
+ if (signal) {
74
+ // Die of the same signal, so the CLI reports 128+n like a shell would.
75
+ process.removeAllListeners(signal);
76
+ process.kill(process.pid, signal);
77
+ return;
78
+ }
79
+ process.exit(code ?? 1);
80
+ });
81
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../../src/cli/watchdog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { Manifest } from './manifest.js';
2
+ /**
3
+ * The key a build is recorded under: its (templated) command plus path, size
4
+ * and mtime of every file its `when:` globs match.
5
+ */
6
+ export declare function buildInputsKey(root: string, manifest: Manifest, cmd: string, when: string[]): Promise<string>;
7
+ /**
8
+ * Was this service last built SUCCESSFULLY from exactly these inputs, and are
9
+ * its outputs still what that build left? `outputs` is the current stat
10
+ * snapshot of the declared outputs (undefined when none are declared).
11
+ */
12
+ export declare function buildIsCurrent(stackId: string, service: string, key: string, outputs?: string): boolean;
13
+ /** Forget a service's build before running it again (a failure must not vouch for anything). */
14
+ export declare function forgetBuild(stackId: string, root: string, service: string): void;
15
+ /** Record a successful build, with the stat snapshot of its outputs when it declares any. */
16
+ export declare function recordBuild(stackId: string, root: string, service: string, key: string, outputs?: string): void;
17
+ /** Has this service ever been built successfully in this worktree (any inputs)? */
18
+ export declare function everBuilt(stackId: string, service: string): boolean;
19
+ /** `--pristine`: nothing is trusted, so every build runs again. */
20
+ export declare function clearBuilds(stackId: string): void;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Build skip (decision 0038): a service whose `build:` declares `when:` globs
3
+ * is not rebuilt while the files they match are unchanged since its last
4
+ * SUCCESSFUL build in this worktree.
5
+ *
6
+ * The inputs are listed the way upkeep triggers are (decision 0032): git's
7
+ * file list filtered by the globs before anything is stat'ed, minus declared
8
+ * `caches:` — so a `frontend/**` glob never walks node_modules. A file's
9
+ * identity is path + size + mtime; the build line itself is part of the key,
10
+ * so editing the command rebuilds. The ledger lives in the state root
11
+ * (`worktrees/<stack>/builds.json`), never in the worktree, and is the
12
+ * worktree's: `runly warm` writes it as well as `up`.
13
+ *
14
+ * An entry is dropped BEFORE its build runs and written only after it
15
+ * succeeds, so a build that fails half-way is never vouched for.
16
+ *
17
+ * When the service declares `outputs:`, the entry also records a stat
18
+ * snapshot of them taken right after the build. A skip needs that snapshot
19
+ * to still hold: outputs that were deleted or overwritten since (a `git
20
+ * clean`, a manual `rm -rf dist`, another tool) would otherwise leave the
21
+ * service with nothing, or the wrong thing, to run.
22
+ */
23
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { sha256 } from './util.js';
26
+ import { enumerateSourceStats } from './worktree.js';
27
+ import { worktreeStateDir } from './tree-ledger.js';
28
+ const ledgerPath = (stackId) => join(worktreeStateDir(stackId), 'builds.json');
29
+ function read(stackId) {
30
+ try {
31
+ const raw = JSON.parse(readFileSync(ledgerPath(stackId), 'utf8'));
32
+ return raw.builds && typeof raw.builds === 'object' ? { ...raw.builds } : {};
33
+ }
34
+ catch {
35
+ return {};
36
+ }
37
+ }
38
+ function write(stackId, root, builds) {
39
+ const dir = worktreeStateDir(stackId);
40
+ mkdirSync(dir, { recursive: true });
41
+ const tmp = join(dir, `builds.json.${process.pid}.tmp`);
42
+ writeFileSync(tmp, JSON.stringify({ root, builds }));
43
+ renameSync(tmp, ledgerPath(stackId));
44
+ }
45
+ /**
46
+ * The key a build is recorded under: its (templated) command plus path, size
47
+ * and mtime of every file its `when:` globs match.
48
+ */
49
+ export async function buildInputsKey(root, manifest, cmd, when) {
50
+ // The listing stats every file once, and this key is made of those stats.
51
+ const files = await enumerateSourceStats(root, manifest, when);
52
+ return sha256(`${cmd}\n${files.map((f) => `${f.path}:${f.size}:${f.mtimeMs}`).join('\n')}`);
53
+ }
54
+ /**
55
+ * Was this service last built SUCCESSFULLY from exactly these inputs, and are
56
+ * its outputs still what that build left? `outputs` is the current stat
57
+ * snapshot of the declared outputs (undefined when none are declared).
58
+ */
59
+ export function buildIsCurrent(stackId, service, key, outputs) {
60
+ const entry = read(stackId)[service];
61
+ if (entry?.key !== key)
62
+ return false;
63
+ if (outputs === undefined)
64
+ return true;
65
+ // An entry written before outputs were recorded vouches for its inputs only;
66
+ // the caller has already checked that outputs exist at all.
67
+ if (entry.outputs === undefined)
68
+ return true;
69
+ return entry.outputs === outputs;
70
+ }
71
+ /** Forget a service's build before running it again (a failure must not vouch for anything). */
72
+ export function forgetBuild(stackId, root, service) {
73
+ const builds = read(stackId);
74
+ if (!(service in builds))
75
+ return;
76
+ delete builds[service];
77
+ write(stackId, root, builds);
78
+ }
79
+ /** Record a successful build, with the stat snapshot of its outputs when it declares any. */
80
+ export function recordBuild(stackId, root, service, key, outputs) {
81
+ const builds = read(stackId);
82
+ builds[service] = outputs === undefined ? { key, at: Date.now() } : { key, at: Date.now(), outputs };
83
+ write(stackId, root, builds);
84
+ }
85
+ /** Has this service ever been built successfully in this worktree (any inputs)? */
86
+ export function everBuilt(stackId, service) {
87
+ return read(stackId)[service] !== undefined;
88
+ }
89
+ /** `--pristine`: nothing is trusted, so every build runs again. */
90
+ export function clearBuilds(stackId) {
91
+ rmSync(ledgerPath(stackId), { force: true });
92
+ }
93
+ //# sourceMappingURL=builds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builds.js","sourceRoot":"","sources":["../../src/core/builds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Caller environment transport is allowlisted by service, never journaled. */
2
+ import type { Manifest, ServiceSpec } from './manifest.js';
3
+ export declare function collectCallerEnv(cwd: string, source?: NodeJS.ProcessEnv): Record<string, string>;
4
+ export declare function selectCallerEnv(manifest: Manifest, source: NodeJS.ProcessEnv): Record<string, string>;
5
+ /** Validate the socket payload without echoing values into error messages. */
6
+ export declare function validateCallerEnv(manifest: Manifest, input: unknown): Record<string, string>;
7
+ export declare function requireCallerEnv(manifest: Manifest, active: Iterable<string>, values: Record<string, string>): void;
8
+ /**
9
+ * A supplied value wins; an omitted one keeps the service's explicit `env`
10
+ * default, and undefined deliberately masks a same-named inherited daemon
11
+ * value when there is no such default.
12
+ */
13
+ export declare function serviceCallerEnv(spec: ServiceSpec, values: Record<string, string>): NodeJS.ProcessEnv;
14
+ /** Declaration identity only: safe to compare without hashing secret values. */
15
+ export declare function callerEnvSpec(manifest: Manifest): string;
16
+ /** Preserve chunk boundaries without ever writing a prefix of a secret first. */
17
+ export declare function redactStream(values: string[]): (chunk: string, final?: boolean) => string;
@@ -0,0 +1,77 @@
1
+ import { loadStack } from './manifest.js';
2
+ import { BrokerError } from './util.js';
3
+ export function collectCallerEnv(cwd, source = process.env) {
4
+ return selectCallerEnv(loadStack(cwd).manifest, source);
5
+ }
6
+ export function selectCallerEnv(manifest, source) {
7
+ return Object.fromEntries(Object.values(manifest.services).flatMap((spec) => Object.keys(spec.env_from ?? {}).flatMap((name) => source[name] === undefined ? [] : [[name, source[name]]])));
8
+ }
9
+ /** Validate the socket payload without echoing values into error messages. */
10
+ export function validateCallerEnv(manifest, input) {
11
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
12
+ throw new BrokerError('work-error', 'callerEnv must be an object containing only declared environment inputs', 'env_from');
13
+ }
14
+ const allowed = new Set(Object.values(manifest.services).flatMap((s) => Object.keys(s.env_from ?? {})));
15
+ const values = Object.create(null);
16
+ for (const [name, value] of Object.entries(input)) {
17
+ if (!allowed.has(name) || typeof value !== 'string' || value.includes('\0')) {
18
+ throw new BrokerError('work-error', 'callerEnv contains an undeclared name or invalid value; only manifest env_from inputs are accepted', 'env_from');
19
+ }
20
+ values[name] = value;
21
+ }
22
+ return values;
23
+ }
24
+ export function requireCallerEnv(manifest, active, values) {
25
+ for (const service of active) {
26
+ for (const [name, mode] of Object.entries(manifest.services[service].env_from ?? {})) {
27
+ if (mode === 'required' && values[name] === undefined) {
28
+ throw new BrokerError('work-error', `service '${service}' requires caller environment '${name}'; export it and run 'runly up' again (inputs are not retained across daemon restarts)`, 'env_from');
29
+ }
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * A supplied value wins; an omitted one keeps the service's explicit `env`
35
+ * default, and undefined deliberately masks a same-named inherited daemon
36
+ * value when there is no such default.
37
+ */
38
+ export function serviceCallerEnv(spec, values) {
39
+ return Object.fromEntries(Object.keys(spec.env_from ?? {}).flatMap((name) => values[name] !== undefined ? [[name, values[name]]] : name in (spec.env ?? {}) ? [] : [[name, undefined]]));
40
+ }
41
+ /** Declaration identity only: safe to compare without hashing secret values. */
42
+ export function callerEnvSpec(manifest) {
43
+ return JSON.stringify(Object.entries(manifest.services)
44
+ .filter(([, spec]) => Object.keys(spec.env_from ?? {}).length > 0)
45
+ .map(([name, spec]) => [name, Object.entries(spec.env_from).sort(([a], [b]) => a.localeCompare(b))])
46
+ .sort(([a], [b]) => String(a).localeCompare(String(b))));
47
+ }
48
+ /** Preserve chunk boundaries without ever writing a prefix of a secret first. */
49
+ export function redactStream(values) {
50
+ const secrets = [...new Set(values.filter(Boolean))].sort((a, b) => b.length - a.length);
51
+ if (secrets.length === 0)
52
+ return (chunk) => chunk;
53
+ let pending = '';
54
+ return (chunk, final = false) => {
55
+ const input = pending + chunk;
56
+ pending = '';
57
+ let output = '';
58
+ for (let i = 0; i < input.length;) {
59
+ const remaining = input.slice(i);
60
+ if (!final && secrets.some((secret) => remaining.length < secret.length && secret.startsWith(remaining))) {
61
+ pending = remaining;
62
+ break;
63
+ }
64
+ const match = secrets.find((secret) => input.startsWith(secret, i));
65
+ if (match) {
66
+ output += '[redacted]';
67
+ i += match.length;
68
+ }
69
+ else {
70
+ output += input[i];
71
+ i++;
72
+ }
73
+ }
74
+ return output;
75
+ };
76
+ }
77
+ //# sourceMappingURL=caller-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caller-env.js","sourceRoot":"","sources":["../../src/core/caller-env.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /** Request-local bind explanations. Never persist commands or environment values. */
2
+ export interface BindDiagnostics {
3
+ durationMs: number;
4
+ /** Wall time per phase. On a full bind `data` and `build` overlap (decision 0039), so they may add up to more than `durationMs`. */
5
+ phasesMs: Record<BindPhase, number>;
6
+ /**
7
+ * `reused`: no running service was stopped — every build ran and none
8
+ * changed a service's declared outputs (services in `started` were added
9
+ * next to them); `restarted`: the services kept running except `restarted`;
10
+ * `rebound`: the full stop/data/build/start path, for the `reasons` given.
11
+ */
12
+ reuse: 'reused' | 'restarted' | 'rebound';
13
+ /**
14
+ * The running services an `up` restarted: their build output changed, or
15
+ * they use a datastore this operation reloaded (decision 0034); on a full
16
+ * rebind, every one that was running (0.19).
17
+ */
18
+ restarted: string[];
19
+ /** Services this `up` added to the ones already running (decision 0034: `up` is additive); on a full rebind, the ones not running before it (0.19). */
20
+ started: string[];
21
+ /** Datastores reloaded from their template because the caller named a preset for them (decision 0034). */
22
+ reloaded: string[];
23
+ /**
24
+ * Why the full path ran (empty when it did not): `new-environment` (its
25
+ * first bind), `upkeep-required`, `environment-not-running`,
26
+ * `service-process-unhealthy`, `environment-inputs-changed`,
27
+ * `manifest-changed`, `hygiene-<reset-data|pristine>`, `public-port-moved`.
28
+ * The plain `up` summary prints them in words (0.19).
29
+ */
30
+ reasons: string[];
31
+ upkeep: {
32
+ ran: number;
33
+ skipped: number;
34
+ };
35
+ /**
36
+ * Every `build:` that ran in this operation — all of them, every `up`
37
+ * (decision 0032: runly keeps no build cache; the build tool decides what is
38
+ * current) — and whether its service was restarted, with why:
39
+ * `outputs-changed`, `outputs-unchanged`, `no-outputs-declared` (always
40
+ * restarted), `not-running` (a service this `up` adds), `full-rebind`,
41
+ * `not-named` (`up <service>` left this running one alone, 0.19), or
42
+ * `when-unchanged` (decision 0038: a `build: { when: }` whose inputs did
43
+ * not change since its last successful build — skipped, durationMs 0).
44
+ */
45
+ builds: Array<{
46
+ service: string;
47
+ durationMs: number;
48
+ restart: boolean;
49
+ reason: 'outputs-changed' | 'outputs-unchanged' | 'no-outputs-declared' | 'not-running' | 'full-rebind' | 'not-named' | 'when-unchanged';
50
+ }>;
51
+ }
52
+ type BindPhase = 'queue' | 'prepare' | 'appliances' | 'upkeep' | 'stop' | 'data' | 'build' | 'ready' | 'finalize';
53
+ export declare class BindTrace {
54
+ readonly result: BindDiagnostics;
55
+ private started;
56
+ private marked;
57
+ private current;
58
+ phase(next: BindPhase): void;
59
+ /**
60
+ * Phases that ran side by side (data and build on a full bind, decision
61
+ * 0039): each gets its own wall time, and the span they shared is not
62
+ * counted again for the phase that was current. `durationMs` is then less
63
+ * than the sum of the phases.
64
+ */
65
+ overlapped(ms: Partial<Record<BindPhase, number>>): void;
66
+ finish(): BindDiagnostics;
67
+ }
68
+ export {};
@@ -0,0 +1,34 @@
1
+ export class BindTrace {
2
+ result = {
3
+ durationMs: 0,
4
+ phasesMs: { queue: 0, prepare: 0, appliances: 0, upkeep: 0, stop: 0, data: 0, build: 0, ready: 0, finalize: 0 },
5
+ reuse: 'rebound', restarted: [], started: [], reloaded: [], reasons: [],
6
+ upkeep: { ran: 0, skipped: 0 }, builds: [],
7
+ };
8
+ started = performance.now();
9
+ marked = this.started;
10
+ current = 'prepare';
11
+ phase(next) {
12
+ const at = performance.now();
13
+ this.result.phasesMs[this.current] += at - this.marked;
14
+ this.marked = at;
15
+ this.current = next;
16
+ }
17
+ /**
18
+ * Phases that ran side by side (data and build on a full bind, decision
19
+ * 0039): each gets its own wall time, and the span they shared is not
20
+ * counted again for the phase that was current. `durationMs` is then less
21
+ * than the sum of the phases.
22
+ */
23
+ overlapped(ms) {
24
+ for (const [phase, v] of Object.entries(ms))
25
+ this.result.phasesMs[phase] += v;
26
+ this.marked = performance.now();
27
+ }
28
+ finish() {
29
+ this.phase('finalize');
30
+ this.result.durationMs = performance.now() - this.started;
31
+ return this.result;
32
+ }
33
+ }
34
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/core/diagnostics.ts"],"names":[],"mappings":"AA8CA,MAAM,OAAO,SAAS;IACX,MAAM,GAAoB;QACjC,UAAU,EAAE,CAAC;QACb,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE;QAC/G,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE;QACvE,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE;KAC3C,CAAC;IACM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAC5B,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;IACtB,OAAO,GAAc,SAAS,CAAC;IAEvC,KAAK,CAAC,IAAe;QACnB,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;QACvD,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACtB,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,EAAsC;QAC/C,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAA+B;YAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5G,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAClC,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC;QAC1D,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;CACF"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The RUNLY_* variables that describe an environment (decision 0032): what
3
+ * `runly ctx --env` prints and what `runly exec` exports, one set of names.
4
+ *
5
+ * RUNLY_ENV_ID, RUNLY_PORT_<PORT>, RUNLY_URL_<SERVICE>,
6
+ * RUNLY_DATASTORE_<NAME>_URL, RUNLY_DATASTORE_<NAME>_PRESET (decision 0034),
7
+ * RUNLY_LOGIN_USER, RUNLY_LOGIN_PASSWORD
8
+ *
9
+ * Names are uppercased, any other character becomes `_`.
10
+ */
11
+ export interface EnvVarSource {
12
+ envId: string;
13
+ ports?: Record<string, number>;
14
+ urls?: Record<string, string>;
15
+ datastores?: Record<string, {
16
+ url: string;
17
+ preset?: string | null;
18
+ }>;
19
+ logins?: {
20
+ user: string;
21
+ password: string;
22
+ } | null;
23
+ }
24
+ /** `web-audit` -> `WEB_AUDIT`: a valid, stable shell variable suffix. */
25
+ export declare const envName: (s: string) => string;
26
+ /** The variables, in a stable order. */
27
+ export declare function runlyEnvVars(c: EnvVarSource): Record<string, string>;
28
+ /** Quoted only when it has to be, so plain values stay plain. */
29
+ export declare const shellValue: (v: string) => string;
30
+ /**
31
+ * `export KEY=value` lines. `export` is what makes them reach a child:
32
+ * `eval "$(runly ctx --env)" && npm test` with bare `KEY=value` lines set
33
+ * shell variables that `npm test` never saw.
34
+ */
35
+ export declare function exportLines(c: EnvVarSource): string[];
@@ -0,0 +1,32 @@
1
+ /** `web-audit` -> `WEB_AUDIT`: a valid, stable shell variable suffix. */
2
+ export const envName = (s) => s.toUpperCase().replace(/[^A-Z0-9]/g, '_');
3
+ /** The variables, in a stable order. */
4
+ export function runlyEnvVars(c) {
5
+ const out = { RUNLY_ENV_ID: c.envId };
6
+ for (const [k, v] of Object.entries(c.ports ?? {}).sort())
7
+ out[`RUNLY_PORT_${envName(k)}`] = String(v);
8
+ for (const [k, v] of Object.entries(c.urls ?? {}).sort())
9
+ out[`RUNLY_URL_${envName(k)}`] = v;
10
+ for (const [k, v] of Object.entries(c.datastores ?? {}).sort()) {
11
+ out[`RUNLY_DATASTORE_${envName(k)}_URL`] = v.url;
12
+ // What the datastore holds right now (decision 0034); absent before its first restore.
13
+ if (v.preset)
14
+ out[`RUNLY_DATASTORE_${envName(k)}_PRESET`] = v.preset;
15
+ }
16
+ if (c.logins) {
17
+ out.RUNLY_LOGIN_USER = c.logins.user;
18
+ out.RUNLY_LOGIN_PASSWORD = c.logins.password;
19
+ }
20
+ return out;
21
+ }
22
+ /** Quoted only when it has to be, so plain values stay plain. */
23
+ export const shellValue = (v) => (/^[A-Za-z0-9_./:@%+,=-]*$/.test(v) ? v : `'${v.replace(/'/g, `'\\''`)}'`);
24
+ /**
25
+ * `export KEY=value` lines. `export` is what makes them reach a child:
26
+ * `eval "$(runly ctx --env)" && npm test` with bare `KEY=value` lines set
27
+ * shell variables that `npm test` never saw.
28
+ */
29
+ export function exportLines(c) {
30
+ return Object.entries(runlyEnvVars(c)).map(([k, v]) => `export ${k}=${shellValue(v)}`);
31
+ }
32
+ //# sourceMappingURL=env-vars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-vars.js","sourceRoot":"","sources":["../../src/core/env-vars.ts"],"names":[],"mappings":"AAkBA,yEAAyE;AACzE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;AAEzF,wCAAwC;AACxC,MAAM,UAAU,YAAY,CAAC,CAAe;IAC1C,MAAM,GAAG,GAA2B,EAAE,YAAY,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAC9D,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;QAAE,GAAG,CAAC,cAAc,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACvG,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;QAAE,GAAG,CAAC,aAAa,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAC7F,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/D,GAAG,CAAC,mBAAmB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;QACjD,uFAAuF;QACvF,IAAI,CAAC,CAAC,MAAM;YAAE,GAAG,CAAC,mBAAmB,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IACvE,CAAC;IACD,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;QACb,GAAG,CAAC,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC;QACrC,GAAG,CAAC,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;AAE5H;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,CAAe;IACzC,OAAO,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -0,0 +1,10 @@
1
+ export type EventLevel = 'info' | 'warn' | 'error';
2
+ export interface DaemonEvent {
3
+ at: number;
4
+ level: EventLevel;
5
+ kind: string;
6
+ envId?: string;
7
+ detail?: string;
8
+ }
9
+ export declare function logEvent(e: Omit<DaemonEvent, 'at'>): void;
10
+ export declare function recentEvents(limit?: number): DaemonEvent[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Structured daemon event log (task: observability). Failures used to vanish
3
+ * into swallowed catches; this makes them visible. Append-only JSONL under the
4
+ * state dir, size-capped in place, read back by `status`/`doctor`.
5
+ */
6
+ import { appendFileSync, readFileSync, statSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { stateRoot } from './paths.js';
9
+ const logPath = () => join(stateRoot(), 'events.jsonl');
10
+ const CAP = 512 * 1024;
11
+ export function logEvent(e) {
12
+ const line = JSON.stringify({ at: Date.now(), ...e }) + '\n';
13
+ try {
14
+ const p = logPath();
15
+ try {
16
+ if (statSync(p).size > CAP) {
17
+ const kept = readFileSync(p, 'utf8').split('\n').slice(-500).join('\n');
18
+ writeFileSync(p, kept);
19
+ }
20
+ }
21
+ catch {
22
+ /* no file yet */
23
+ }
24
+ appendFileSync(p, line);
25
+ }
26
+ catch {
27
+ /* logging must never throw into a caller */
28
+ }
29
+ }
30
+ export function recentEvents(limit = 30) {
31
+ try {
32
+ return readFileSync(logPath(), 'utf8')
33
+ .split('\n')
34
+ .filter(Boolean)
35
+ .slice(-limit)
36
+ .map((l) => JSON.parse(l));
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }
42
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/core/events.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAWvC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,cAAc,CAAC,CAAC;AACxD,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAEvB,MAAM,UAAU,QAAQ,CAAC,CAA0B;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;QACpB,IAAI,CAAC;YACH,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACxE,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,iBAAiB;QACnB,CAAC;QACD,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAK,GAAG,EAAE;IACrC,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC;aACnC,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,OAAO,CAAC;aACf,KAAK,CAAC,CAAC,KAAK,CAAC;aACb,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAgB,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ /** Repo-declared commands are IO-bound setup steps, not builds. */
2
+ export declare const DEFAULT_CMD_TIMEOUT_S = 300;
3
+ /** Build/exec/check-grade work gets a longer leash than setup steps. */
4
+ export declare const LONG_CMD_TIMEOUT_S = 600;
5
+ /**
6
+ * The deadline for a repo-declared command. Callers pass the default that fits
7
+ * their site's class (setup steps 300s, or an upkeep rule's own `timeout`;
8
+ * build/exec-grade work 600s); BACKLOT_CMD_TIMEOUT_S overrides ALL of them —
9
+ * the escape hatch for a repo whose legitimate commands outrun the class default.
10
+ */
11
+ export declare function cmdTimeoutS(fallback?: number): number;
12
+ export interface CmdResult {
13
+ code: number;
14
+ output: string;
15
+ timedOut: boolean;
16
+ }
17
+ export declare function runBounded(cmd: string, cwd: string, timeoutS?: number, env?: NodeJS.ProcessEnv,
18
+ /** Sees every chunk as it arrives (a build log, decision 0038); `end` once per stream. */
19
+ onOutput?: {
20
+ data: (stream: 'out' | 'err', chunk: string) => void;
21
+ end: (stream: 'out' | 'err') => void;
22
+ }): Promise<CmdResult>;