backlot 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +106 -0
- package/dist/cli/client.d.ts +22 -0
- package/dist/cli/client.js +83 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +308 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/events.d.ts +10 -0
- package/dist/core/events.js +42 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/journal.d.ts +80 -0
- package/dist/core/journal.js +186 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/manifest.d.ts +76 -0
- package/dist/core/manifest.js +46 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/core/paths.d.ts +17 -0
- package/dist/core/paths.js +32 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/policy.d.ts +15 -0
- package/dist/core/policy.js +45 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/ports.d.ts +3 -0
- package/dist/core/ports.js +22 -0
- package/dist/core/ports.js.map +1 -0
- package/dist/core/retention.d.ts +19 -0
- package/dist/core/retention.js +101 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/sync.d.ts +15 -0
- package/dist/core/sync.js +150 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/types.d.ts +82 -0
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/upkeep.d.ts +11 -0
- package/dist/core/upkeep.js +47 -0
- package/dist/core/upkeep.js.map +1 -0
- package/dist/core/util.d.ts +36 -0
- package/dist/core/util.js +100 -0
- package/dist/core/util.js.map +1 -0
- package/dist/daemon/engine.d.ts +284 -0
- package/dist/daemon/engine.js +858 -0
- package/dist/daemon/engine.js.map +1 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +183 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +36 -0
- package/dist/daemon/supervisor.js +189 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/drivers/datastore-sqlite.d.ts +27 -0
- package/dist/drivers/datastore-sqlite.js +83 -0
- package/dist/drivers/datastore-sqlite.js.map +1 -0
- package/dist/drivers/datastores.d.ts +22 -0
- package/dist/drivers/datastores.js +190 -0
- package/dist/drivers/datastores.js.map +1 -0
- package/dist/drivers/types.d.ts +71 -0
- package/dist/drivers/types.js +14 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +144 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +57 -0
- package/schema/stack.schema.json +172 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core domain model (see docs/architecture.md §3).
|
|
3
|
+
* Persisted in the per-machine SQLite journal; the daemon's memory is a cache.
|
|
4
|
+
*/
|
|
5
|
+
export type EnvState = 'provisioning' | 'hot' | 'warm' | 'degraded' | 'recycling';
|
|
6
|
+
export type Hygiene = 'reuse' | 'reset-data' | 'pristine';
|
|
7
|
+
export type LeaseKind = 'session' | 'run';
|
|
8
|
+
/** The field an agent branches on mechanically (decision 0010). */
|
|
9
|
+
export type ErrorClass = 'work-error' | 'env-error' | 'infra-error';
|
|
10
|
+
export interface Environment {
|
|
11
|
+
id: string;
|
|
12
|
+
stack: string;
|
|
13
|
+
substrate: string;
|
|
14
|
+
state: EnvState;
|
|
15
|
+
root: string;
|
|
16
|
+
/** Symbolic name -> allocated port. Stable for the environment's lifetime. */
|
|
17
|
+
ports: Record<string, number>;
|
|
18
|
+
datastoreNs: Record<string, string>;
|
|
19
|
+
/** Per-upkeep-rule trigger hash as last applied IN THIS ENV (decision 0008). */
|
|
20
|
+
fingerprints: Record<string, string>;
|
|
21
|
+
bindCount: number;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
lastUsedAt: number;
|
|
24
|
+
}
|
|
25
|
+
/** An immutable snapshot of source + data state (decision 0005). */
|
|
26
|
+
export interface Binding {
|
|
27
|
+
envId: string;
|
|
28
|
+
revision: number;
|
|
29
|
+
ref: string;
|
|
30
|
+
dirtyPatchHash: string | null;
|
|
31
|
+
preset: string;
|
|
32
|
+
hygiene: Hygiene;
|
|
33
|
+
syncedAt: number;
|
|
34
|
+
}
|
|
35
|
+
export interface Lease {
|
|
36
|
+
id: string;
|
|
37
|
+
envId: string;
|
|
38
|
+
kind: LeaseKind;
|
|
39
|
+
/** Refreshed by any CLI touch; expiry releases the env WARM (decision 0003). */
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
holder: string;
|
|
42
|
+
}
|
|
43
|
+
export interface Failure {
|
|
44
|
+
class: ErrorClass;
|
|
45
|
+
message: string;
|
|
46
|
+
/** e.g. the upkeep rule or service that failed. */
|
|
47
|
+
source?: string;
|
|
48
|
+
logExcerpt?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface Verdict {
|
|
51
|
+
check: string;
|
|
52
|
+
ok: boolean;
|
|
53
|
+
exitCode: number;
|
|
54
|
+
failure?: Failure;
|
|
55
|
+
artifactsDir?: string;
|
|
56
|
+
/** Declared outputs the env offers back; pulled only explicitly (decision 0011). */
|
|
57
|
+
outputsChanged: string[];
|
|
58
|
+
binding: Binding;
|
|
59
|
+
durationMs: number;
|
|
60
|
+
}
|
|
61
|
+
/** What `backlot ctx --json` returns — the consumer's entire interface. */
|
|
62
|
+
export interface Context {
|
|
63
|
+
stack: string;
|
|
64
|
+
envId: string;
|
|
65
|
+
lease: Lease;
|
|
66
|
+
urls: Record<string, string>;
|
|
67
|
+
logins?: {
|
|
68
|
+
user: string;
|
|
69
|
+
password: string;
|
|
70
|
+
};
|
|
71
|
+
tokenCommand?: string;
|
|
72
|
+
datastores: Record<string, {
|
|
73
|
+
url: string;
|
|
74
|
+
}>;
|
|
75
|
+
artifactsDir: string;
|
|
76
|
+
hygiene: Hygiene;
|
|
77
|
+
events: Array<{
|
|
78
|
+
at: number;
|
|
79
|
+
service: string;
|
|
80
|
+
event: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Manifest } from './manifest.js';
|
|
2
|
+
export interface UpkeepOutcome {
|
|
3
|
+
ran: Array<{
|
|
4
|
+
when: string;
|
|
5
|
+
run: string;
|
|
6
|
+
}>;
|
|
7
|
+
fingerprints: Record<string, string>;
|
|
8
|
+
/** Names of datastores whose templates must be rebaked (@rebake-template). */
|
|
9
|
+
rebakeTemplates: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function runUpkeep(envTree: string, syncedFiles: string[], manifest: Manifest, previous: Record<string, string>): Promise<UpkeepOutcome>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fingerprint ledger (decision 0008): a closed list of (trigger -> action)
|
|
3
|
+
* rules, evaluated per environment at bind time, direction-agnostic. Actions
|
|
4
|
+
* are repo commands, or engine built-ins prefixed with @.
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { globToRegex, sha256, fileHash, BrokerError } from './util.js';
|
|
9
|
+
function triggerHash(envTree, files, when) {
|
|
10
|
+
const re = globToRegex(when);
|
|
11
|
+
const matching = files.filter((f) => re.test(f)).sort();
|
|
12
|
+
return sha256(matching.map((f) => `${f}:${fileHash(join(envTree, f)) ?? 'gone'}`).join('\n'));
|
|
13
|
+
}
|
|
14
|
+
export async function runUpkeep(envTree, syncedFiles, manifest, previous) {
|
|
15
|
+
const outcome = { ran: [], fingerprints: { ...previous }, rebakeTemplates: [] };
|
|
16
|
+
for (const rule of manifest.upkeep ?? []) {
|
|
17
|
+
const key = `${rule.when} -> ${rule.run}`;
|
|
18
|
+
const hash = triggerHash(envTree, syncedFiles, rule.when);
|
|
19
|
+
if (previous[key] === hash)
|
|
20
|
+
continue;
|
|
21
|
+
if (rule.run.startsWith('@')) {
|
|
22
|
+
const [builtin, ...args] = rule.run.slice(1).split(/\s+/);
|
|
23
|
+
if (builtin === 'rebake-template') {
|
|
24
|
+
outcome.rebakeTemplates.push(args[0] ?? 'main');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
throw new BrokerError('work-error', `unknown upkeep built-in '@${builtin}'`, 'upkeep');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
execFile('sh', ['-c', rule.run], { cwd: envTree, maxBuffer: 16 * 1024 * 1024 }, (err, _out, stderr) => {
|
|
33
|
+
if (err) {
|
|
34
|
+
// Triggered by the binding's own change -> work-error by default (decision 0008).
|
|
35
|
+
reject(new BrokerError('work-error', `upkeep rule failed: ${rule.run}`, rule.when, String(stderr).slice(0, 800)));
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
outcome.ran.push({ when: rule.when, run: rule.run });
|
|
43
|
+
outcome.fingerprints[key] = hash;
|
|
44
|
+
}
|
|
45
|
+
return outcome;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=upkeep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upkeep.js","sourceRoot":"","sources":["../../src/core/upkeep.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAUvE,SAAS,WAAW,CAAC,OAAe,EAAE,KAAe,EAAE,IAAY;IACjE,MAAM,EAAE,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAChG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,OAAe,EACf,WAAqB,EACrB,QAAkB,EAClB,QAAgC;IAEhC,MAAM,OAAO,GAAkB,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,GAAG,QAAQ,EAAE,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC;IAC/F,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,IAAI;YAAE,SAAS;QAErC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1D,IAAI,OAAO,KAAK,iBAAiB,EAAE,CAAC;gBAClC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;YAClD,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,WAAW,CAAC,YAAY,EAAE,6BAA6B,OAAO,GAAG,EAAE,QAAQ,CAAC,CAAC;YACzF,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC1C,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;oBACpG,IAAI,GAAG,EAAE,CAAC;wBACR,kFAAkF;wBAClF,MAAM,CACJ,IAAI,WAAW,CAAC,YAAY,EAAE,uBAAuB,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAC1G,CAAC;oBACJ,CAAC;;wBAAM,OAAO,EAAE,CAAC;gBACnB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACnC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const sha256: (data: string | Buffer) => string;
|
|
2
|
+
export declare const fileHash: (path: string) => string | null;
|
|
3
|
+
export declare const isFile: (p: string) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Minimal glob matcher for manifest patterns (caches, sync.keep, artifacts,
|
|
6
|
+
* upkeep when:). Supports **, *, ?. A bare name with no glob chars and no
|
|
7
|
+
* slash matches that path segment anywhere (node_modules). All patterns also
|
|
8
|
+
* protect their subtree (an implicit trailing /**).
|
|
9
|
+
*/
|
|
10
|
+
export declare function globToRegex(pattern: string): RegExp;
|
|
11
|
+
export declare const matchesAny: (path: string, patterns: string[]) => boolean;
|
|
12
|
+
/** Resolve {{...}} placeholders against a nested context object. */
|
|
13
|
+
export declare function template(str: string, ctx: Record<string, unknown>): string;
|
|
14
|
+
export declare const templateEnv: (env: Record<string, string> | undefined, ctx: Record<string, unknown>) => Record<string, string>;
|
|
15
|
+
export declare class BrokerError extends Error {
|
|
16
|
+
readonly klass: 'work-error' | 'env-error' | 'infra-error';
|
|
17
|
+
readonly source?: string | undefined;
|
|
18
|
+
readonly logExcerpt?: string | undefined;
|
|
19
|
+
constructor(klass: 'work-error' | 'env-error' | 'infra-error', message: string, source?: string | undefined, logExcerpt?: string | undefined);
|
|
20
|
+
toJSON(): {
|
|
21
|
+
class: "work-error" | "infra-error" | "env-error";
|
|
22
|
+
message: string;
|
|
23
|
+
source: string | undefined;
|
|
24
|
+
logExcerpt: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A manifest-supplied relative path (sync.include, outputs, a datastore key)
|
|
29
|
+
* must stay INSIDE its base after resolution — never escape via `..` or an
|
|
30
|
+
* absolute path. Returns the safe joined absolute path, or throws work-error.
|
|
31
|
+
* This is the guard that keeps file ops from leaving backlot's own dirs even
|
|
32
|
+
* on an honest `../shared/.env` typo, not only a malicious manifest.
|
|
33
|
+
*/
|
|
34
|
+
export declare function safeJoin(base: string, rel: string, what: string): string;
|
|
35
|
+
export declare const now: () => number;
|
|
36
|
+
export declare const shortId: () => string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
3
|
+
import * as pathMod from 'node:path';
|
|
4
|
+
export const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
5
|
+
export const fileHash = (path) => {
|
|
6
|
+
try {
|
|
7
|
+
return sha256(readFileSync(path));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const isFile = (p) => existsSync(p) && statSync(p).isFile();
|
|
14
|
+
/**
|
|
15
|
+
* Minimal glob matcher for manifest patterns (caches, sync.keep, artifacts,
|
|
16
|
+
* upkeep when:). Supports **, *, ?. A bare name with no glob chars and no
|
|
17
|
+
* slash matches that path segment anywhere (node_modules). All patterns also
|
|
18
|
+
* protect their subtree (an implicit trailing /**).
|
|
19
|
+
*/
|
|
20
|
+
export function globToRegex(pattern) {
|
|
21
|
+
const p = pattern.replace(/^glob\((.*)\)$/, '$1').replace(/^\.\//, '');
|
|
22
|
+
if (!/[*?[]/.test(p) && !p.includes('/')) {
|
|
23
|
+
return new RegExp(`(^|/)${p.replace(/[.+^${}()|\\]/g, '\\$&')}(/|$)`);
|
|
24
|
+
}
|
|
25
|
+
let re = '';
|
|
26
|
+
for (let i = 0; i < p.length; i++) {
|
|
27
|
+
const c = p[i];
|
|
28
|
+
if (c === '*') {
|
|
29
|
+
if (p[i + 1] === '*') {
|
|
30
|
+
re += '.*';
|
|
31
|
+
i++;
|
|
32
|
+
if (p[i + 1] === '/')
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
else
|
|
36
|
+
re += '[^/]*';
|
|
37
|
+
}
|
|
38
|
+
else if (c === '?')
|
|
39
|
+
re += '[^/]';
|
|
40
|
+
else
|
|
41
|
+
re += c.replace(/[.+^${}()|\\[\]]/g, '\\$&');
|
|
42
|
+
}
|
|
43
|
+
return new RegExp(`^${re}(/.*)?$`);
|
|
44
|
+
}
|
|
45
|
+
export const matchesAny = (path, patterns) => patterns.some((p) => globToRegex(p).test(path));
|
|
46
|
+
/** Resolve {{...}} placeholders against a nested context object. */
|
|
47
|
+
export function template(str, ctx) {
|
|
48
|
+
return str.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (_, expr) => {
|
|
49
|
+
const val = expr.split('.').reduce((acc, key) => {
|
|
50
|
+
if (acc && typeof acc === 'object' && key in acc) {
|
|
51
|
+
return acc[key];
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}, ctx);
|
|
55
|
+
if (val === undefined || val === null)
|
|
56
|
+
throw new Error(`unresolved template variable {{${expr}}}`);
|
|
57
|
+
return String(val);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export const templateEnv = (env, ctx) => {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(env ?? {}))
|
|
63
|
+
out[k] = template(v, ctx);
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
export class BrokerError extends Error {
|
|
67
|
+
klass;
|
|
68
|
+
source;
|
|
69
|
+
logExcerpt;
|
|
70
|
+
constructor(klass, message, source, logExcerpt) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.klass = klass;
|
|
73
|
+
this.source = source;
|
|
74
|
+
this.logExcerpt = logExcerpt;
|
|
75
|
+
}
|
|
76
|
+
toJSON() {
|
|
77
|
+
return { class: this.klass, message: this.message, source: this.source, logExcerpt: this.logExcerpt };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A manifest-supplied relative path (sync.include, outputs, a datastore key)
|
|
82
|
+
* must stay INSIDE its base after resolution — never escape via `..` or an
|
|
83
|
+
* absolute path. Returns the safe joined absolute path, or throws work-error.
|
|
84
|
+
* This is the guard that keeps file ops from leaving backlot's own dirs even
|
|
85
|
+
* on an honest `../shared/.env` typo, not only a malicious manifest.
|
|
86
|
+
*/
|
|
87
|
+
export function safeJoin(base, rel, what) {
|
|
88
|
+
const { join, resolve, isAbsolute } = pathMod;
|
|
89
|
+
if (isAbsolute(rel))
|
|
90
|
+
throw new BrokerError('work-error', `${what} must be a relative path, got absolute '${rel}'`, 'manifest');
|
|
91
|
+
const abs = resolve(base, rel);
|
|
92
|
+
const baseResolved = resolve(base);
|
|
93
|
+
if (abs !== baseResolved && !abs.startsWith(baseResolved + pathMod.sep)) {
|
|
94
|
+
throw new BrokerError('work-error', `${what} '${rel}' escapes its directory — path traversal is not allowed`, 'manifest');
|
|
95
|
+
}
|
|
96
|
+
return abs;
|
|
97
|
+
}
|
|
98
|
+
export const now = () => Date.now();
|
|
99
|
+
export const shortId = () => Math.random().toString(36).slice(2, 8);
|
|
100
|
+
//# sourceMappingURL=util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../../src/core/util.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Journal } from '../core/journal.js';
|
|
2
|
+
import type { Hygiene, LeaseKind } from '../core/types.js';
|
|
3
|
+
/** Streamed bind phases → human progress on stderr (never on the --json stdout). */
|
|
4
|
+
export type Progress = (phase: string) => void;
|
|
5
|
+
export interface UpOptions {
|
|
6
|
+
cwd: string;
|
|
7
|
+
holder?: string;
|
|
8
|
+
hygiene?: Hygiene;
|
|
9
|
+
kind?: LeaseKind;
|
|
10
|
+
watch?: boolean;
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
/** Bind from this directory instead of the worktree (bind --ref extraction). */
|
|
13
|
+
sourceRoot?: string;
|
|
14
|
+
/** Set by the daemon per-request; emits progress frames back to the client. */
|
|
15
|
+
onProgress?: Progress;
|
|
16
|
+
}
|
|
17
|
+
export declare class Engine {
|
|
18
|
+
readonly journal: Journal;
|
|
19
|
+
private supervisors;
|
|
20
|
+
private lastSweep;
|
|
21
|
+
private lastRetention;
|
|
22
|
+
private poolChain;
|
|
23
|
+
private envChains;
|
|
24
|
+
/** Envs with an operation in flight — the sweeper must not expire/quiesce these. */
|
|
25
|
+
readonly busy: Set<string>;
|
|
26
|
+
/** --watch: per-env worktree watchers ("verbs sync, watch streams", decision 0005). */
|
|
27
|
+
private watchers;
|
|
28
|
+
private poolLocked;
|
|
29
|
+
private envLocked;
|
|
30
|
+
/** Recovery (decision 0009): reap recorded PIDs from a previous daemon life; hot -> warm. */
|
|
31
|
+
recover(): void;
|
|
32
|
+
private envDirs;
|
|
33
|
+
private createEnv;
|
|
34
|
+
/** One atomic claim attempt — MUST run under the pool lock. */
|
|
35
|
+
private tryClaim;
|
|
36
|
+
/** Queue at capacity WITHOUT holding the pool lock while sleeping. */
|
|
37
|
+
private acquireEnv;
|
|
38
|
+
private templateCtx;
|
|
39
|
+
private supervisor;
|
|
40
|
+
private bindAndStart;
|
|
41
|
+
/**
|
|
42
|
+
* --watch: the daemon observes the CONSUMER's worktree (opt-in, per lease)
|
|
43
|
+
* and auto-syncs debounced. The environment's own dev servers then pick up
|
|
44
|
+
* the projected change — two-stage reload. Stopped on release/expiry/
|
|
45
|
+
* quiesce/recycle/shutdown.
|
|
46
|
+
*/
|
|
47
|
+
private startWatch;
|
|
48
|
+
private stopWatch;
|
|
49
|
+
up(opts: UpOptions): Promise<{
|
|
50
|
+
stack: string;
|
|
51
|
+
envId: string;
|
|
52
|
+
state: import("../core/types.js").EnvState;
|
|
53
|
+
lease: {
|
|
54
|
+
id: string;
|
|
55
|
+
kind: LeaseKind;
|
|
56
|
+
hygiene: Hygiene;
|
|
57
|
+
expiresAt: number;
|
|
58
|
+
} | null;
|
|
59
|
+
urls: Record<string, string>;
|
|
60
|
+
logins: {
|
|
61
|
+
user: string;
|
|
62
|
+
password: string;
|
|
63
|
+
} | null;
|
|
64
|
+
tokenCommand: string | null;
|
|
65
|
+
datastores: {
|
|
66
|
+
[k: string]: {
|
|
67
|
+
url: string;
|
|
68
|
+
ns: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
artifactsDir: string;
|
|
72
|
+
events: import("./supervisor.js").ServiceEvent[];
|
|
73
|
+
}>;
|
|
74
|
+
ctx(cwd: string, holder?: string, envId?: string): {
|
|
75
|
+
stack: string;
|
|
76
|
+
envId: string;
|
|
77
|
+
state: import("../core/types.js").EnvState;
|
|
78
|
+
lease: {
|
|
79
|
+
id: string;
|
|
80
|
+
kind: LeaseKind;
|
|
81
|
+
hygiene: Hygiene;
|
|
82
|
+
expiresAt: number;
|
|
83
|
+
} | null;
|
|
84
|
+
urls: Record<string, string>;
|
|
85
|
+
logins: {
|
|
86
|
+
user: string;
|
|
87
|
+
password: string;
|
|
88
|
+
} | null;
|
|
89
|
+
tokenCommand: string | null;
|
|
90
|
+
datastores: {
|
|
91
|
+
[k: string]: {
|
|
92
|
+
url: string;
|
|
93
|
+
ns: string;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
artifactsDir: string;
|
|
97
|
+
events: import("./supervisor.js").ServiceEvent[];
|
|
98
|
+
};
|
|
99
|
+
run(opts: UpOptions & {
|
|
100
|
+
check: string;
|
|
101
|
+
}): Promise<{
|
|
102
|
+
check: string;
|
|
103
|
+
ok: boolean;
|
|
104
|
+
exitCode: number;
|
|
105
|
+
failure: {
|
|
106
|
+
class: string;
|
|
107
|
+
message: string;
|
|
108
|
+
logExcerpt: string;
|
|
109
|
+
} | null;
|
|
110
|
+
output: string;
|
|
111
|
+
artifactsDir: string | null;
|
|
112
|
+
outputsChanged: string[];
|
|
113
|
+
envId: string;
|
|
114
|
+
durationMs: number;
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Detached submit-and-poll runs (decision 0015): the verdict outlives the
|
|
118
|
+
* client. Returns immediately with a jobId; the caller polls jobStatus.
|
|
119
|
+
* Execution is handed back to the daemon's serialized queue by the server.
|
|
120
|
+
*/
|
|
121
|
+
createJob(cwd: string, check: string): string;
|
|
122
|
+
executeJob(id: string, opts: UpOptions & {
|
|
123
|
+
check: string;
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
jobStatus(id: string): {
|
|
126
|
+
id: string;
|
|
127
|
+
check: string;
|
|
128
|
+
state: string;
|
|
129
|
+
verdict: unknown;
|
|
130
|
+
createdAt: number;
|
|
131
|
+
finishedAt: number | null;
|
|
132
|
+
};
|
|
133
|
+
private collectArtifacts;
|
|
134
|
+
syncLease(cwd: string, holder?: string, onProgress?: Progress): Promise<{
|
|
135
|
+
stack: string;
|
|
136
|
+
envId: string;
|
|
137
|
+
state: import("../core/types.js").EnvState;
|
|
138
|
+
lease: {
|
|
139
|
+
id: string;
|
|
140
|
+
kind: LeaseKind;
|
|
141
|
+
hygiene: Hygiene;
|
|
142
|
+
expiresAt: number;
|
|
143
|
+
} | null;
|
|
144
|
+
urls: Record<string, string>;
|
|
145
|
+
logins: {
|
|
146
|
+
user: string;
|
|
147
|
+
password: string;
|
|
148
|
+
} | null;
|
|
149
|
+
tokenCommand: string | null;
|
|
150
|
+
datastores: {
|
|
151
|
+
[k: string]: {
|
|
152
|
+
url: string;
|
|
153
|
+
ns: string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
artifactsDir: string;
|
|
157
|
+
events: import("./supervisor.js").ServiceEvent[];
|
|
158
|
+
}>;
|
|
159
|
+
/** bind --ref: project a COMMITTED ref (not the worktree state) into the env. */
|
|
160
|
+
bindRef(cwd: string, ref: string, holder?: string): Promise<{
|
|
161
|
+
stack: string;
|
|
162
|
+
envId: string;
|
|
163
|
+
state: import("../core/types.js").EnvState;
|
|
164
|
+
lease: {
|
|
165
|
+
id: string;
|
|
166
|
+
kind: LeaseKind;
|
|
167
|
+
hygiene: Hygiene;
|
|
168
|
+
expiresAt: number;
|
|
169
|
+
} | null;
|
|
170
|
+
urls: Record<string, string>;
|
|
171
|
+
logins: {
|
|
172
|
+
user: string;
|
|
173
|
+
password: string;
|
|
174
|
+
} | null;
|
|
175
|
+
tokenCommand: string | null;
|
|
176
|
+
datastores: {
|
|
177
|
+
[k: string]: {
|
|
178
|
+
url: string;
|
|
179
|
+
ns: string;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
artifactsDir: string;
|
|
183
|
+
events: import("./supervisor.js").ServiceEvent[];
|
|
184
|
+
}>;
|
|
185
|
+
jobList(): {
|
|
186
|
+
jobs: {
|
|
187
|
+
id: string;
|
|
188
|
+
check: string;
|
|
189
|
+
state: string;
|
|
190
|
+
ok: boolean | null;
|
|
191
|
+
createdAt: number;
|
|
192
|
+
finishedAt: number | null;
|
|
193
|
+
}[];
|
|
194
|
+
};
|
|
195
|
+
resetData(cwd: string, holder?: string, onProgress?: Progress): Promise<{
|
|
196
|
+
stack: string;
|
|
197
|
+
envId: string;
|
|
198
|
+
state: import("../core/types.js").EnvState;
|
|
199
|
+
lease: {
|
|
200
|
+
id: string;
|
|
201
|
+
kind: LeaseKind;
|
|
202
|
+
hygiene: Hygiene;
|
|
203
|
+
expiresAt: number;
|
|
204
|
+
} | null;
|
|
205
|
+
urls: Record<string, string>;
|
|
206
|
+
logins: {
|
|
207
|
+
user: string;
|
|
208
|
+
password: string;
|
|
209
|
+
} | null;
|
|
210
|
+
tokenCommand: string | null;
|
|
211
|
+
datastores: {
|
|
212
|
+
[k: string]: {
|
|
213
|
+
url: string;
|
|
214
|
+
ns: string;
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
artifactsDir: string;
|
|
218
|
+
events: import("./supervisor.js").ServiceEvent[];
|
|
219
|
+
}>;
|
|
220
|
+
exec(cwd: string, cmd: string, holder?: string): Promise<unknown>;
|
|
221
|
+
/** Resolve auth.token with {{role}} and run it in the env tree. */
|
|
222
|
+
token(cwd: string, role: string, holder?: string): Promise<unknown>;
|
|
223
|
+
logs(cwd: string, service: string, lines: number, holder?: string): {
|
|
224
|
+
service: string;
|
|
225
|
+
lines: string;
|
|
226
|
+
};
|
|
227
|
+
pull(cwd: string, holder?: string): {
|
|
228
|
+
pulled: string[];
|
|
229
|
+
};
|
|
230
|
+
release(cwd: string, holder?: string): Promise<{
|
|
231
|
+
released: boolean;
|
|
232
|
+
envId?: undefined;
|
|
233
|
+
} | {
|
|
234
|
+
released: boolean;
|
|
235
|
+
envId: string;
|
|
236
|
+
}>;
|
|
237
|
+
status(): {
|
|
238
|
+
pid: number;
|
|
239
|
+
envs: {
|
|
240
|
+
id: string;
|
|
241
|
+
stack: string;
|
|
242
|
+
state: import("../core/types.js").EnvState;
|
|
243
|
+
ports: Record<string, number>;
|
|
244
|
+
bindCount: number;
|
|
245
|
+
lease: import("../core/journal.js").LeaseRow | null;
|
|
246
|
+
idleMs: number;
|
|
247
|
+
}[];
|
|
248
|
+
poolMax: number;
|
|
249
|
+
events: import("../core/events.js").DaemonEvent[];
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* doctor: actively check for the failure shapes the review surfaced —
|
|
253
|
+
* orphaned ports, journal/reality pid divergence, envs stuck recycling.
|
|
254
|
+
*/
|
|
255
|
+
doctor(): Promise<{
|
|
256
|
+
ok: boolean;
|
|
257
|
+
issues: {
|
|
258
|
+
level: string;
|
|
259
|
+
envId?: string;
|
|
260
|
+
issue: string;
|
|
261
|
+
}[];
|
|
262
|
+
events: import("../core/events.js").DaemonEvent[];
|
|
263
|
+
}>;
|
|
264
|
+
/**
|
|
265
|
+
* Atomically claim an env for teardown UNDER THE POOL LOCK: re-read it, and
|
|
266
|
+
* (unless force) refuse if it's leased or busy, then flip it to the
|
|
267
|
+
* 'recycling' guard state so tryClaim/sweep skip it. Returns the row to tear
|
|
268
|
+
* down, or null if it slipped away. The slow teardown then runs OUTSIDE the
|
|
269
|
+
* lock, but no claim can touch a 'recycling' env.
|
|
270
|
+
*/
|
|
271
|
+
private claimForTeardown;
|
|
272
|
+
/** Slow teardown of an already-claimed ('recycling') env. */
|
|
273
|
+
private teardownClaimed;
|
|
274
|
+
private recycleOne;
|
|
275
|
+
poolRecycle(all: boolean): Promise<{
|
|
276
|
+
recycled: string[];
|
|
277
|
+
}>;
|
|
278
|
+
/** Reap the provably-dead (degraded) envs now, instead of waiting for the sweep. */
|
|
279
|
+
poolReconcile(): Promise<{
|
|
280
|
+
reaped: string[];
|
|
281
|
+
}>;
|
|
282
|
+
sweep(): Promise<void>;
|
|
283
|
+
shutdown(): Promise<void>;
|
|
284
|
+
}
|