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.
Files changed (64) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +106 -0
  3. package/dist/cli/client.d.ts +22 -0
  4. package/dist/cli/client.js +83 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +308 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/core/events.d.ts +10 -0
  10. package/dist/core/events.js +42 -0
  11. package/dist/core/events.js.map +1 -0
  12. package/dist/core/journal.d.ts +80 -0
  13. package/dist/core/journal.js +186 -0
  14. package/dist/core/journal.js.map +1 -0
  15. package/dist/core/manifest.d.ts +76 -0
  16. package/dist/core/manifest.js +46 -0
  17. package/dist/core/manifest.js.map +1 -0
  18. package/dist/core/paths.d.ts +17 -0
  19. package/dist/core/paths.js +32 -0
  20. package/dist/core/paths.js.map +1 -0
  21. package/dist/core/policy.d.ts +15 -0
  22. package/dist/core/policy.js +45 -0
  23. package/dist/core/policy.js.map +1 -0
  24. package/dist/core/ports.d.ts +3 -0
  25. package/dist/core/ports.js +22 -0
  26. package/dist/core/ports.js.map +1 -0
  27. package/dist/core/retention.d.ts +19 -0
  28. package/dist/core/retention.js +101 -0
  29. package/dist/core/retention.js.map +1 -0
  30. package/dist/core/sync.d.ts +15 -0
  31. package/dist/core/sync.js +150 -0
  32. package/dist/core/sync.js.map +1 -0
  33. package/dist/core/types.d.ts +82 -0
  34. package/dist/core/types.js +6 -0
  35. package/dist/core/types.js.map +1 -0
  36. package/dist/core/upkeep.d.ts +11 -0
  37. package/dist/core/upkeep.js +47 -0
  38. package/dist/core/upkeep.js.map +1 -0
  39. package/dist/core/util.d.ts +36 -0
  40. package/dist/core/util.js +100 -0
  41. package/dist/core/util.js.map +1 -0
  42. package/dist/daemon/engine.d.ts +284 -0
  43. package/dist/daemon/engine.js +858 -0
  44. package/dist/daemon/engine.js.map +1 -0
  45. package/dist/daemon/index.d.ts +2 -0
  46. package/dist/daemon/index.js +183 -0
  47. package/dist/daemon/index.js.map +1 -0
  48. package/dist/daemon/supervisor.d.ts +36 -0
  49. package/dist/daemon/supervisor.js +189 -0
  50. package/dist/daemon/supervisor.js.map +1 -0
  51. package/dist/drivers/datastore-sqlite.d.ts +27 -0
  52. package/dist/drivers/datastore-sqlite.js +83 -0
  53. package/dist/drivers/datastore-sqlite.js.map +1 -0
  54. package/dist/drivers/datastores.d.ts +22 -0
  55. package/dist/drivers/datastores.js +190 -0
  56. package/dist/drivers/datastores.js.map +1 -0
  57. package/dist/drivers/types.d.ts +71 -0
  58. package/dist/drivers/types.js +14 -0
  59. package/dist/drivers/types.js.map +1 -0
  60. package/dist/mcp/index.d.ts +2 -0
  61. package/dist/mcp/index.js +144 -0
  62. package/dist/mcp/index.js.map +1 -0
  63. package/package.json +57 -0
  64. package/schema/stack.schema.json +172 -0
@@ -0,0 +1,76 @@
1
+ export interface ReadySpec {
2
+ http?: string;
3
+ log?: string;
4
+ cmd?: string;
5
+ timeout?: number;
6
+ }
7
+ export interface ServiceSpec {
8
+ run: string;
9
+ build?: string;
10
+ watch_run?: string;
11
+ cwd?: string;
12
+ port?: string;
13
+ env?: Record<string, string>;
14
+ ready?: ReadySpec;
15
+ fatal_logs?: string;
16
+ depends_on?: string[];
17
+ }
18
+ export interface DatastoreSpec {
19
+ driver: 'sqlite' | 'postgres' | 'mssql' | 'mysql' | 'redis';
20
+ server?: 'external';
21
+ probe?: string;
22
+ url?: string;
23
+ create?: string;
24
+ drop?: string;
25
+ template_restore?: string;
26
+ presets?: string[];
27
+ default_preset?: {
28
+ run?: string;
29
+ session?: string;
30
+ };
31
+ template?: boolean;
32
+ ephemeral?: boolean;
33
+ }
34
+ export interface CheckSpec {
35
+ run: string;
36
+ cwd?: string;
37
+ env?: Record<string, string>;
38
+ artifacts?: string[];
39
+ /** Hard kill (whole process group) after this many seconds. Default 600. */
40
+ timeout?: number;
41
+ }
42
+ export interface UpkeepRule {
43
+ when: string;
44
+ run: string;
45
+ }
46
+ export interface Manifest {
47
+ name: string;
48
+ services: Record<string, ServiceSpec>;
49
+ datastores?: Record<string, DatastoreSpec>;
50
+ caches?: string[];
51
+ sync?: {
52
+ keep?: string[];
53
+ include?: string[];
54
+ };
55
+ outputs?: string[];
56
+ upkeep?: UpkeepRule[];
57
+ auth?: {
58
+ logins?: {
59
+ user: string;
60
+ password: string;
61
+ };
62
+ token?: string;
63
+ };
64
+ checks?: Record<string, CheckSpec>;
65
+ }
66
+ export interface Stack {
67
+ manifest: Manifest;
68
+ /** Directory containing stack.yaml — the sync source root. */
69
+ root: string;
70
+ /** Stable identity: pools are keyed by this. */
71
+ id: string;
72
+ }
73
+ /** Walk upward from cwd to the nearest stack.yaml. */
74
+ export declare function findStackRoot(from: string): string;
75
+ export declare function loadStack(from: string): Stack;
76
+ export declare function defaultPreset(ds: DatastoreSpec, kind: 'run' | 'session'): string;
@@ -0,0 +1,46 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import Ajv2020 from 'ajv/dist/2020.js';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { BrokerError } from './util.js';
7
+ const schemaPath = () => join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schema', 'stack.schema.json');
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ let validator;
10
+ function validate(data) {
11
+ if (!validator) {
12
+ // ajv is CJS; the constructor lands on .default under real ESM interop.
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const AjvCtor = Ajv2020.default ?? Ajv2020;
15
+ const ajv = new AjvCtor({ allErrors: true });
16
+ validator = ajv.compile(JSON.parse(readFileSync(schemaPath(), 'utf8')));
17
+ }
18
+ if (!validator(data)) {
19
+ throw new BrokerError('work-error', `stack.yaml is invalid: ${JSON.stringify(validator.errors)}`, 'manifest');
20
+ }
21
+ }
22
+ /** Walk upward from cwd to the nearest stack.yaml. */
23
+ export function findStackRoot(from) {
24
+ let dir = resolve(from);
25
+ for (;;) {
26
+ if (existsSync(join(dir, 'stack.yaml')))
27
+ return dir;
28
+ const parent = dirname(dir);
29
+ if (parent === dir) {
30
+ throw new BrokerError('work-error', `no stack.yaml found from ${from} upward`, 'manifest');
31
+ }
32
+ dir = parent;
33
+ }
34
+ }
35
+ export function loadStack(from) {
36
+ const root = findStackRoot(from);
37
+ const manifest = parse(readFileSync(join(root, 'stack.yaml'), 'utf8'));
38
+ validate(manifest);
39
+ // Identity = absolute root + declared name; filesystem-safe.
40
+ const id = `${manifest.name}-${Buffer.from(root).toString('base64url').slice(-8)}`;
41
+ return { manifest, root, id };
42
+ }
43
+ export function defaultPreset(ds, kind) {
44
+ return ds.default_preset?.[kind] ?? ds.presets?.[0] ?? 'default';
45
+ }
46
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/core/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAC7B,OAAO,OAAO,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAqExC,MAAM,UAAU,GAAG,GAAG,EAAE,CACtB,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAC;AAE3F,8DAA8D;AAC9D,IAAI,SAAc,CAAC;AACnB,SAAS,QAAQ,CAAC,IAAa;IAC7B,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,wEAAwE;QACxE,8DAA8D;QAC9D,MAAM,OAAO,GAAS,OAAe,CAAC,OAAO,IAAI,OAAO,CAAC;QACzD,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,WAAW,CAAC,YAAY,EAAE,0BAA0B,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAChH,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,SAAS,CAAC;QACR,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QACpD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,IAAI,WAAW,CAAC,YAAY,EAAE,4BAA4B,IAAI,SAAS,EAAE,UAAU,CAAC,CAAC;QAC7F,CAAC;QACD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAa,CAAC;IACnF,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnB,6DAA6D;IAC7D,MAAM,EAAE,GAAG,GAAG,QAAQ,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAiB,EAAE,IAAuB;IACtE,OAAO,EAAE,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;AACnE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-machine state root (decision 0009). BACKLOT_STATE_DIR overrides — the
3
+ * isolation knob every integration test uses.
4
+ *
5
+ * Created 0700 and chmod-enforced every call: the daemon socket lives here and
6
+ * has NO application-level auth (README "Security model" — filesystem
7
+ * permissions ARE the auth), so no other local user may traverse in. We chmod
8
+ * even when the dir already exists, to repair a too-open pre-existing dir
9
+ * (e.g. one made before this hardening, or via a loose umask).
10
+ */
11
+ export declare function stateRoot(): string;
12
+ export declare const socketPath: () => string;
13
+ export declare const pidPath: () => string;
14
+ export declare const journalPath: () => string;
15
+ export declare const envsRoot: () => string;
16
+ export declare const templatesRoot: () => string;
17
+ export declare const artifactsRoot: () => string;
@@ -0,0 +1,32 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync, chmodSync } from 'node:fs';
4
+ /**
5
+ * Per-machine state root (decision 0009). BACKLOT_STATE_DIR overrides — the
6
+ * isolation knob every integration test uses.
7
+ *
8
+ * Created 0700 and chmod-enforced every call: the daemon socket lives here and
9
+ * has NO application-level auth (README "Security model" — filesystem
10
+ * permissions ARE the auth), so no other local user may traverse in. We chmod
11
+ * even when the dir already exists, to repair a too-open pre-existing dir
12
+ * (e.g. one made before this hardening, or via a loose umask).
13
+ */
14
+ export function stateRoot() {
15
+ const root = process.env.BACKLOT_STATE_DIR ??
16
+ join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'backlot');
17
+ mkdirSync(root, { recursive: true, mode: 0o700 });
18
+ try {
19
+ chmodSync(root, 0o700);
20
+ }
21
+ catch {
22
+ /* not the owner — leave it; the socket chmod below is the real gate */
23
+ }
24
+ return root;
25
+ }
26
+ export const socketPath = () => join(stateRoot(), 'daemon.sock');
27
+ export const pidPath = () => join(stateRoot(), 'daemon.pid');
28
+ export const journalPath = () => join(stateRoot(), 'journal.db');
29
+ export const envsRoot = () => join(stateRoot(), 'envs');
30
+ export const templatesRoot = () => join(stateRoot(), 'templates');
31
+ export const artifactsRoot = () => join(stateRoot(), 'artifacts');
32
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS;IACvB,MAAM,IAAI,GACR,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAC7B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC;IACpF,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;IACzE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;AACzE,MAAM,CAAC,MAAM,OAAO,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACzE,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,WAAW,CAAC,CAAC;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,WAAW,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ export interface Policy {
2
+ poolMax: number;
3
+ sessionTtlMs: number;
4
+ runTtlMs: number;
5
+ idleTtlMs: number;
6
+ waitMs: number;
7
+ /** Retention knobs (task: disk sweep). */
8
+ artifactDays: number;
9
+ jobDays: number;
10
+ logCapBytes: number;
11
+ templatesKeep: number;
12
+ }
13
+ /** The designed capacity heuristic: min(cores/2, memGB/4), clamped to [1, 8]. */
14
+ export declare function poolMaxHeuristic(): number;
15
+ export declare function policy(): Policy;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Engine policy — NEVER repo knowledge (the manifest carries none of this).
3
+ * Precedence per knob: env var > $STATE_DIR/config.json > heuristic/default.
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { cpus, totalmem } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { stateRoot } from './paths.js';
9
+ function configFile() {
10
+ try {
11
+ return JSON.parse(readFileSync(join(stateRoot(), 'config.json'), 'utf8'));
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ /** The designed capacity heuristic: min(cores/2, memGB/4), clamped to [1, 8]. */
18
+ export function poolMaxHeuristic() {
19
+ const byCores = Math.floor(cpus().length / 2);
20
+ const byMem = Math.floor(totalmem() / (4 * 1024 ** 3));
21
+ return Math.max(1, Math.min(8, Math.min(byCores || 1, byMem || 1)));
22
+ }
23
+ const num = (envVar, fileVal, fallback) => {
24
+ const e = process.env[envVar];
25
+ if (e !== undefined && e !== '')
26
+ return Number(e);
27
+ if (fileVal !== undefined)
28
+ return fileVal;
29
+ return fallback;
30
+ };
31
+ export function policy() {
32
+ const f = configFile();
33
+ return {
34
+ poolMax: num('BACKLOT_POOL_MAX', f.poolMax, poolMaxHeuristic()),
35
+ sessionTtlMs: num('BACKLOT_LEASE_TTL_MS', f.sessionTtlMs, 30 * 60_000),
36
+ runTtlMs: num('BACKLOT_LEASE_TTL_MS', f.runTtlMs, 10 * 60_000),
37
+ idleTtlMs: num('BACKLOT_IDLE_TTL_MS', f.idleTtlMs, 30 * 60_000),
38
+ waitMs: num('BACKLOT_WAIT_MS', f.waitMs, 60_000),
39
+ artifactDays: num('BACKLOT_ARTIFACT_DAYS', f.artifactDays, 7),
40
+ jobDays: num('BACKLOT_JOB_DAYS', f.jobDays, 7),
41
+ logCapBytes: num('BACKLOT_LOG_CAP_BYTES', f.logCapBytes, 5 * 1024 * 1024),
42
+ templatesKeep: num('BACKLOT_TEMPLATES_KEEP', f.templatesKeep, 4),
43
+ };
44
+ }
45
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/core/policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA2BvC,SAAS,UAAU;IACjB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAe,CAAC;IAC1F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IACvD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,OAA2B,EAAE,QAAgB,EAAU,EAAE;IACpF,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAC1C,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,UAAU,MAAM;IACpB,MAAM,CAAC,GAAG,UAAU,EAAE,CAAC;IACvB,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE,gBAAgB,EAAE,CAAC;QAC/D,YAAY,EAAE,GAAG,CAAC,sBAAsB,EAAE,CAAC,CAAC,YAAY,EAAE,EAAE,GAAG,MAAM,CAAC;QACtE,QAAQ,EAAE,GAAG,CAAC,sBAAsB,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,GAAG,MAAM,CAAC;QAC9D,SAAS,EAAE,GAAG,CAAC,qBAAqB,EAAE,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,MAAM,CAAC;QAC/D,MAAM,EAAE,GAAG,CAAC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC;QAChD,YAAY,EAAE,GAAG,CAAC,uBAAuB,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;QAC7D,OAAO,EAAE,GAAG,CAAC,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC9C,WAAW,EAAE,GAAG,CAAC,uBAAuB,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;QACzE,aAAa,EAAE,GAAG,CAAC,wBAAwB,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;KACjE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function probeFree(port: number): Promise<boolean>;
2
+ /** OS-allocated free port — stable per environment once recorded (decision 0004). */
3
+ export declare function freePort(): Promise<number>;
@@ -0,0 +1,22 @@
1
+ import { createServer } from 'node:net';
2
+ export function probeFree(port) {
3
+ return new Promise((resolve) => {
4
+ const srv = createServer();
5
+ srv.once('error', () => resolve(false));
6
+ srv.listen(port, '127.0.0.1', () => srv.close(() => resolve(true)));
7
+ });
8
+ }
9
+ /** OS-allocated free port — stable per environment once recorded (decision 0004). */
10
+ export function freePort() {
11
+ return new Promise((resolve, reject) => {
12
+ const srv = createServer();
13
+ srv.listen(0, '127.0.0.1', () => {
14
+ const addr = srv.address();
15
+ if (addr && typeof addr === 'object')
16
+ srv.close(() => resolve(addr.port));
17
+ else
18
+ srv.close(() => reject(new Error('no port allocated')));
19
+ });
20
+ });
21
+ }
22
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../../src/core/ports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;QAC3B,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACxC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,QAAQ;IACtB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;QAC3B,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;YAC3B,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;;gBACrE,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Journal } from './journal.js';
2
+ import type { Policy } from './policy.js';
3
+ /** Artifacts older than N days are pruned (verdict dirs are timestamped). */
4
+ export declare function pruneArtifacts(p: Policy, root?: string): number;
5
+ /** Service log files past the cap keep only their tail (in-place truncate). */
6
+ export declare function truncateLogs(p: Policy, root?: string): number;
7
+ /** Done jobs older than N days leave the journal. */
8
+ export declare function pruneJobs(journal: Journal, p: Policy): number;
9
+ /**
10
+ * Templates: keep the newest M per stack (a template whose seed-hash key is
11
+ * still current keeps being touched by binds; stale keys age out naturally).
12
+ */
13
+ export declare function pruneTemplates(p: Policy, root?: string): number;
14
+ export declare function retentionSweep(journal: Journal, p: Policy): {
15
+ artifacts: number;
16
+ logs: number;
17
+ jobs: number;
18
+ templates: number;
19
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Disk retention: nothing backlot writes may grow forever. Called from the
3
+ * daemon sweeper (~10 min cadence); every function is idempotent, best-effort,
4
+ * and unit-testable in isolation.
5
+ */
6
+ import { readdirSync, statSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { artifactsRoot, templatesRoot, envsRoot } from './paths.js';
9
+ const dayMs = 24 * 60 * 60 * 1000;
10
+ const entriesOf = (dir) => {
11
+ try {
12
+ return readdirSync(dir);
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ };
18
+ /** Artifacts older than N days are pruned (verdict dirs are timestamped). */
19
+ export function pruneArtifacts(p, root = artifactsRoot()) {
20
+ let pruned = 0;
21
+ for (const envDir of entriesOf(root)) {
22
+ for (const runDir of entriesOf(join(root, envDir))) {
23
+ const full = join(root, envDir, runDir);
24
+ try {
25
+ if (Date.now() - statSync(full).mtimeMs > p.artifactDays * dayMs) {
26
+ rmSync(full, { recursive: true, force: true });
27
+ pruned++;
28
+ }
29
+ }
30
+ catch {
31
+ /* raced */
32
+ }
33
+ }
34
+ if (entriesOf(join(root, envDir)).length === 0)
35
+ rmSync(join(root, envDir), { recursive: true, force: true });
36
+ }
37
+ return pruned;
38
+ }
39
+ /** Service log files past the cap keep only their tail (in-place truncate). */
40
+ export function truncateLogs(p, root = envsRoot()) {
41
+ let truncated = 0;
42
+ for (const envDir of entriesOf(root)) {
43
+ const logDir = join(root, envDir, 'logs');
44
+ for (const logFile of entriesOf(logDir)) {
45
+ const full = join(logDir, logFile);
46
+ try {
47
+ if (statSync(full).size > p.logCapBytes) {
48
+ const content = readFileSync(full, 'utf8');
49
+ const keep = content.slice(-Math.floor(p.logCapBytes / 4));
50
+ writeFileSync(full, `[backlot: truncated by retention sweep]\n${keep}`);
51
+ truncated++;
52
+ }
53
+ }
54
+ catch {
55
+ /* raced */
56
+ }
57
+ }
58
+ }
59
+ return truncated;
60
+ }
61
+ /** Done jobs older than N days leave the journal. */
62
+ export function pruneJobs(journal, p) {
63
+ return journal.pruneJobs(Date.now() - p.jobDays * dayMs);
64
+ }
65
+ /**
66
+ * Templates: keep the newest M per stack (a template whose seed-hash key is
67
+ * still current keeps being touched by binds; stale keys age out naturally).
68
+ */
69
+ export function pruneTemplates(p, root = templatesRoot()) {
70
+ let pruned = 0;
71
+ for (const stackDir of entriesOf(root)) {
72
+ const dir = join(root, stackDir);
73
+ if (!existsSync(dir))
74
+ continue;
75
+ const files = entriesOf(dir)
76
+ .map((f) => {
77
+ try {
78
+ return { f, mtime: statSync(join(dir, f)).mtimeMs };
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ })
84
+ .filter((x) => x !== null)
85
+ .sort((a, b) => b.mtime - a.mtime);
86
+ for (const { f } of files.slice(p.templatesKeep)) {
87
+ rmSync(join(dir, f), { force: true });
88
+ pruned++;
89
+ }
90
+ }
91
+ return pruned;
92
+ }
93
+ export function retentionSweep(journal, p) {
94
+ return {
95
+ artifacts: pruneArtifacts(p),
96
+ logs: truncateLogs(p),
97
+ jobs: pruneJobs(journal, p),
98
+ templates: pruneTemplates(p),
99
+ };
100
+ }
101
+ //# sourceMappingURL=retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Manifest } from './manifest.js';
2
+ export interface SyncResult {
3
+ copied: number;
4
+ deleted: number;
5
+ files: string[];
6
+ /** Hash of the full (path -> content hash) map: the binding's source identity. */
7
+ sourceHash: string;
8
+ }
9
+ export declare function syncIntoEnv(stackRoot: string, envTree: string, manifest: Manifest): SyncResult;
10
+ /**
11
+ * Outputs contract (decision 0011): report env-side changes to declared
12
+ * outputs; copy back only on explicit pull.
13
+ */
14
+ export declare function changedOutputs(stackRoot: string, envTree: string, manifest: Manifest): string[];
15
+ export declare function pullOutputs(stackRoot: string, envTree: string, manifest: Manifest): string[];
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Bind-by-sync (decision 0005): project the consumer's worktree into the
3
+ * environment's own tree. Git decides what belongs to a binding (tracked +
4
+ * untracked-unignored under the stack root, plus sync.include); the copy is
5
+ * hash-gated. Deletions are mirrored from the previous binding's file list;
6
+ * caches and sync.keep survive. v0.1 transport is enumerate+copy — the
7
+ * fetch/patch optimization arrives with remote substrates (0.3).
8
+ *
9
+ * Performance: hashing is (size, mtime)-gated on BOTH sides. A file whose
10
+ * stat matches the per-env sync cache reuses its recorded hash, so a warm
11
+ * rebind of an unchanged 28k-file repo stats instead of re-hashing 2 GB.
12
+ * Correctness is preserved: any stat drift re-hashes, and the env-side stat
13
+ * check is what keeps the "tracked files restored hard" reset guarantee —
14
+ * a check that mutated a tracked env file is detected and overwritten.
15
+ */
16
+ import { execFileSync } from 'node:child_process';
17
+ import { copyFileSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, readdirSync, statSync, constants as fsConstants, } from 'node:fs';
18
+ import { dirname, join } from 'node:path';
19
+ import { fileHash, matchesAny, sha256, isFile, safeJoin } from './util.js';
20
+ function enumerate(stackRoot, manifest) {
21
+ let listed;
22
+ try {
23
+ const out = execFileSync('git', ['-C', stackRoot, 'ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
24
+ listed = out.split('\0').filter(Boolean);
25
+ }
26
+ catch {
27
+ // Not a git repo: fall back to everything (minus default noise).
28
+ listed = walkAll(stackRoot);
29
+ }
30
+ const include = manifest.sync?.include ?? [];
31
+ for (const inc of include) {
32
+ safeJoin(stackRoot, inc, 'sync.include'); // reject ../ or absolute before use
33
+ if (isFile(join(stackRoot, inc)) && !listed.includes(inc))
34
+ listed.push(inc);
35
+ }
36
+ // Tracked-but-deleted files still appear in ls-files --cached.
37
+ return listed.filter((f) => isFile(join(stackRoot, f)));
38
+ }
39
+ function walkAll(root, prefix = '') {
40
+ const out = [];
41
+ for (const name of readdirSync(join(root, prefix))) {
42
+ if (name === '.git' || name === 'node_modules')
43
+ continue;
44
+ const rel = prefix ? `${prefix}/${name}` : name;
45
+ if (statSync(join(root, rel)).isDirectory())
46
+ out.push(...walkAll(root, rel));
47
+ else
48
+ out.push(rel);
49
+ }
50
+ return out;
51
+ }
52
+ const cachePath = (envRoot) => join(envRoot, '.backlot-synced.json');
53
+ function loadCache(envTree) {
54
+ try {
55
+ const raw = JSON.parse(readFileSync(cachePath(envTree), 'utf8'));
56
+ return Array.isArray(raw) ? {} : raw; // migrate from the v0.1 list format
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
62
+ const statOf = (p) => {
63
+ try {
64
+ const s = statSync(p);
65
+ return { size: s.size, mtime: s.mtimeMs };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ };
71
+ export function syncIntoEnv(stackRoot, envTree, manifest) {
72
+ mkdirSync(envTree, { recursive: true });
73
+ const files = enumerate(stackRoot, manifest).sort();
74
+ const protectedPatterns = [...(manifest.caches ?? []), ...(manifest.sync?.keep ?? [])];
75
+ const prev = loadCache(envTree);
76
+ const next = {};
77
+ let copied = 0;
78
+ for (const rel of files) {
79
+ // git ls-files can't emit ../ but the sync.include path can — belt-and-suspenders
80
+ // so neither the copy nor the deletion-mirror below can ever leave envTree.
81
+ const dst = safeJoin(envTree, rel, 'synced file');
82
+ const src = join(stackRoot, rel);
83
+ const cached = prev[rel];
84
+ const srcStat = statOf(src);
85
+ // Source hash: stat-gated.
86
+ const srcHash = cached && cached.srcSize === srcStat.size && cached.srcMtime === srcStat.mtime
87
+ ? cached.hash
88
+ : fileHash(src);
89
+ // Env-side hash: stat-gated too (this is the reset guarantee — a mutated
90
+ // tracked file in the env has a drifted stat or hash and gets overwritten).
91
+ const dstStat = statOf(dst);
92
+ let dstHash = null;
93
+ if (dstStat) {
94
+ dstHash =
95
+ cached && cached.dstSize === dstStat.size && cached.dstMtime === dstStat.mtime
96
+ ? cached.hash
97
+ : fileHash(dst);
98
+ }
99
+ if (dstHash !== srcHash) {
100
+ mkdirSync(dirname(dst), { recursive: true });
101
+ copyFileSync(src, dst, fsConstants.COPYFILE_FICLONE); // CoW clone on APFS/reflink fs; falls back to copy
102
+ copied++;
103
+ }
104
+ const newDstStat = dstHash !== srcHash ? statOf(dst) : dstStat;
105
+ next[rel] = { hash: srcHash, srcSize: srcStat.size, srcMtime: srcStat.mtime, dstSize: newDstStat.size, dstMtime: newDstStat.mtime };
106
+ }
107
+ // Mirror deletions relative to the PREVIOUS binding, never touching caches/keep.
108
+ let deleted = 0;
109
+ for (const rel of Object.keys(prev)) {
110
+ if (next[rel] || matchesAny(rel, protectedPatterns))
111
+ continue;
112
+ let victim;
113
+ try {
114
+ victim = safeJoin(envTree, rel, 'synced file'); // never rm outside the env tree
115
+ }
116
+ catch {
117
+ continue; // a poisoned cache entry can't make us delete outside envTree
118
+ }
119
+ if (existsSync(victim)) {
120
+ rmSync(victim, { force: true });
121
+ deleted++;
122
+ }
123
+ }
124
+ writeFileSync(cachePath(envTree), JSON.stringify(next));
125
+ const sourceHash = sha256(files.map((f) => `${f}:${next[f].hash}`).join('\n'));
126
+ return { copied, deleted, files, sourceHash };
127
+ }
128
+ /**
129
+ * Outputs contract (decision 0011): report env-side changes to declared
130
+ * outputs; copy back only on explicit pull.
131
+ */
132
+ export function changedOutputs(stackRoot, envTree, manifest) {
133
+ return (manifest.outputs ?? []).filter((rel) => {
134
+ const envH = fileHash(join(envTree, rel));
135
+ return envH !== null && envH !== fileHash(join(stackRoot, rel));
136
+ });
137
+ }
138
+ export function pullOutputs(stackRoot, envTree, manifest) {
139
+ const changed = changedOutputs(stackRoot, envTree, manifest);
140
+ for (const rel of changed) {
141
+ // outputs write BACK into the worktree — must never escape it (a rogue
142
+ // '../../.bashrc' output would otherwise be overwritten from env content).
143
+ const dst = safeJoin(stackRoot, rel, 'outputs');
144
+ const src = safeJoin(envTree, rel, 'outputs');
145
+ mkdirSync(dirname(dst), { recursive: true });
146
+ copyFileSync(src, dst, fsConstants.COPYFILE_FICLONE);
147
+ }
148
+ return changed;
149
+ }
150
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/core/sync.ts"],"names":[],"mappings":"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"}