@amalgm/apps 0.1.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 (98) hide show
  1. package/PURPOSE.md +98 -0
  2. package/README.md +61 -0
  3. package/dist/apps.d.ts +58 -0
  4. package/dist/apps.js +274 -0
  5. package/dist/apps.js.map +1 -0
  6. package/dist/bin/apps.d.ts +2 -0
  7. package/dist/bin/apps.js +6 -0
  8. package/dist/bin/apps.js.map +1 -0
  9. package/dist/bin/mcp.d.ts +2 -0
  10. package/dist/bin/mcp.js +15 -0
  11. package/dist/bin/mcp.js.map +1 -0
  12. package/dist/boundaries.d.ts +11 -0
  13. package/dist/boundaries.js +55 -0
  14. package/dist/boundaries.js.map +1 -0
  15. package/dist/changes.d.ts +14 -0
  16. package/dist/changes.js +23 -0
  17. package/dist/changes.js.map +1 -0
  18. package/dist/cli.d.ts +24 -0
  19. package/dist/cli.js +138 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/command.d.ts +3 -0
  22. package/dist/command.js +40 -0
  23. package/dist/command.js.map +1 -0
  24. package/dist/faults.d.ts +21 -0
  25. package/dist/faults.js +59 -0
  26. package/dist/faults.js.map +1 -0
  27. package/dist/generations.d.ts +28 -0
  28. package/dist/generations.js +83 -0
  29. package/dist/generations.js.map +1 -0
  30. package/dist/health.d.ts +35 -0
  31. package/dist/health.js +116 -0
  32. package/dist/health.js.map +1 -0
  33. package/dist/identity.d.ts +10 -0
  34. package/dist/identity.js +59 -0
  35. package/dist/identity.js.map +1 -0
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/lease.d.ts +30 -0
  40. package/dist/lease.js +82 -0
  41. package/dist/lease.js.map +1 -0
  42. package/dist/logs.d.ts +11 -0
  43. package/dist/logs.js +40 -0
  44. package/dist/logs.js.map +1 -0
  45. package/dist/manifest.d.ts +6 -0
  46. package/dist/manifest.js +105 -0
  47. package/dist/manifest.js.map +1 -0
  48. package/dist/mcp-server.d.ts +3 -0
  49. package/dist/mcp-server.js +65 -0
  50. package/dist/mcp-server.js.map +1 -0
  51. package/dist/mcp.d.ts +4 -0
  52. package/dist/mcp.js +171 -0
  53. package/dist/mcp.js.map +1 -0
  54. package/dist/operations.d.ts +10 -0
  55. package/dist/operations.js +31 -0
  56. package/dist/operations.js.map +1 -0
  57. package/dist/ownership.d.ts +11 -0
  58. package/dist/ownership.js +75 -0
  59. package/dist/ownership.js.map +1 -0
  60. package/dist/package.d.ts +20 -0
  61. package/dist/package.js +204 -0
  62. package/dist/package.js.map +1 -0
  63. package/dist/presentation.d.ts +9 -0
  64. package/dist/presentation.js +63 -0
  65. package/dist/presentation.js.map +1 -0
  66. package/dist/process.d.ts +10 -0
  67. package/dist/process.js +68 -0
  68. package/dist/process.js.map +1 -0
  69. package/dist/reconciler.d.ts +27 -0
  70. package/dist/reconciler.js +103 -0
  71. package/dist/reconciler.js.map +1 -0
  72. package/dist/releases.d.ts +26 -0
  73. package/dist/releases.js +108 -0
  74. package/dist/releases.js.map +1 -0
  75. package/dist/restarts.d.ts +40 -0
  76. package/dist/restarts.js +137 -0
  77. package/dist/restarts.js.map +1 -0
  78. package/dist/runtime.d.ts +19 -0
  79. package/dist/runtime.js +57 -0
  80. package/dist/runtime.js.map +1 -0
  81. package/dist/schema.d.ts +3 -0
  82. package/dist/schema.js +80 -0
  83. package/dist/schema.js.map +1 -0
  84. package/dist/source-import.d.ts +4 -0
  85. package/dist/source-import.js +77 -0
  86. package/dist/source-import.js.map +1 -0
  87. package/dist/store.d.ts +29 -0
  88. package/dist/store.js +221 -0
  89. package/dist/store.js.map +1 -0
  90. package/dist/supervisor.d.ts +29 -0
  91. package/dist/supervisor.js +279 -0
  92. package/dist/supervisor.js.map +1 -0
  93. package/dist/types.d.ts +264 -0
  94. package/dist/types.js +2 -0
  95. package/dist/types.js.map +1 -0
  96. package/docs/ENGINE_INTEGRATION.md +68 -0
  97. package/docs/SUPERVISION.md +73 -0
  98. package/package.json +59 -0
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import { allocatePort, allocateRef, assertAppRef } from './identity.js';
3
+ import { normalizeManifest, readManifest } from './manifest.js';
4
+ import { processIdentity } from './process.js';
5
+ import { errorMessage } from './boundaries.js';
6
+ function sourceManifest(input, cwd) {
7
+ try {
8
+ return readManifest(cwd);
9
+ }
10
+ catch (error) {
11
+ if (!input.startCommand && !input.start_command)
12
+ throw error;
13
+ return normalizeManifest({
14
+ schemaVersion: 1,
15
+ name: input.name || input.id,
16
+ description: input.description || '',
17
+ buildCommand: input.buildCommand || input.build_command || null,
18
+ startCommand: input.startCommand || input.start_command,
19
+ toolIds: input.toolIds || input.tool_ids || [],
20
+ package: { include: ['**'], exclude: [] },
21
+ });
22
+ }
23
+ }
24
+ async function importExisting(apps, input) {
25
+ if (!input?.id || !input?.cwd)
26
+ throw new Error('Existing app id and cwd are required');
27
+ const existing = apps.store.getApp(input.id);
28
+ if (existing)
29
+ return apps.view(existing);
30
+ const cwd = path.resolve(input.cwd);
31
+ const manifest = sourceManifest(input, cwd);
32
+ const appRef = assertAppRef(input.appRef || input.app_ref || allocateRef(apps.store));
33
+ const now = new Date().toISOString();
34
+ let app = apps.store.createApp({
35
+ id: input.id,
36
+ name: input.name || manifest.name,
37
+ description: input.description || manifest.description,
38
+ appRef,
39
+ publicUrl: input.publicUrl || input.appUrl || `https://${appRef}.${apps.domain}/`,
40
+ port: allocatePort(apps.store, input.port, apps.portMin, apps.portMax),
41
+ sourceDir: cwd,
42
+ dataDir: path.join(apps.stateDir, 'data', input.id),
43
+ desiredState: input.desiredState || (input.status === 'stopped' ? 'stopped' : 'running'),
44
+ status: 'registered',
45
+ dnsConnected: input.dnsConnected !== false,
46
+ autostart: input.autostart !== false,
47
+ keepAlive: input.keepAlive !== false,
48
+ createdAt: input.createdAt || now,
49
+ updatedAt: now,
50
+ });
51
+ try {
52
+ const release = await apps.releases.prepare(app, cwd, manifest);
53
+ const deployment = await apps.releases.createDeployment(app, release, 'import');
54
+ apps.store.activate(app.id, deployment.id);
55
+ if (input.pid && await processIdentity(input.pid)) {
56
+ app = apps.store.updateApp(app.id, {
57
+ pid: input.pid,
58
+ status: input.status === 'unresponsive' ? 'unresponsive' : 'running',
59
+ desiredState: 'running',
60
+ });
61
+ }
62
+ else if (app.desiredState === 'running') {
63
+ app = await apps.supervisor.start(app.id);
64
+ }
65
+ else {
66
+ app = apps.store.updateApp(app.id, { status: 'stopped', pid: null });
67
+ }
68
+ return apps.view(app);
69
+ }
70
+ catch (error) {
71
+ apps.store.deleteApp(input.id);
72
+ apps.removeAppFiles(input.id);
73
+ throw new Error(`Existing app import failed: ${errorMessage(error)}`);
74
+ }
75
+ }
76
+ export { importExisting };
77
+ //# sourceMappingURL=source-import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-import.js","sourceRoot":"","sources":["../src/source-import.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { Generations } from './generations.js';
2
+ import type DatabaseType from 'better-sqlite3';
3
+ import type { AppRecord, Deployment, Release } from './types.js';
4
+ type AppPatch = Partial<Pick<AppRecord, 'name' | 'description' | 'sourceDir' | 'activeReleaseId' | 'activeDeploymentId' | 'activeGenerationId' | 'desiredState' | 'status' | 'pid' | 'dnsConnected' | 'autostart' | 'keepAlive' | 'error' | 'readyAt' | 'lastHeartbeatAt' | 'heartbeatIntervalMs'>>;
5
+ type NewApp = Omit<AppRecord, 'activeReleaseId' | 'activeDeploymentId' | 'activeGenerationId' | 'error' | 'pid' | 'readyAt' | 'lastHeartbeatAt' | 'heartbeatIntervalMs'> & Partial<Pick<AppRecord, 'description' | 'desiredState' | 'status' | 'dnsConnected' | 'autostart' | 'keepAlive'>>;
6
+ type NewRelease = Release;
7
+ type NewDeployment = Omit<Deployment, 'sequence' | 'status' | 'error' | 'activatedAt'>;
8
+ declare class Store {
9
+ readonly db: DatabaseType.Database;
10
+ readonly generations: Generations;
11
+ constructor(file: string);
12
+ createApp(app: NewApp): AppRecord;
13
+ getApp(id: string): AppRecord | null;
14
+ listApps(): AppRecord[];
15
+ updateApp(id: string, patch: AppPatch): AppRecord | null;
16
+ deleteApp(id: string): AppRecord | null;
17
+ addRelease(release: NewRelease): Release;
18
+ getRelease(id: string): Release | null;
19
+ listReleases(appId: string): Release[];
20
+ createDeployment(deployment: NewDeployment): Deployment;
21
+ getDeployment(id: string): Deployment | null;
22
+ listDeployments(appId: string): Deployment[];
23
+ activate(appId: string, deploymentId: string, now?: string): AppRecord | null;
24
+ failDeployment(id: string, error: unknown): void;
25
+ previousDeployment(appId: string, activeReleaseId: string | null): Deployment | null;
26
+ close(): void;
27
+ }
28
+ export { Store };
29
+ export type { AppPatch, NewApp, NewDeployment, NewRelease };
package/dist/store.js ADDED
@@ -0,0 +1,221 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import { Generations } from './generations.js';
5
+ import { migrate } from './schema.js';
6
+ const APP_COLUMNS = new Map([
7
+ ['name', 'name'],
8
+ ['description', 'description'],
9
+ ['sourceDir', 'source_dir'],
10
+ ['activeReleaseId', 'active_release_id'],
11
+ ['activeDeploymentId', 'active_deployment_id'],
12
+ ['activeGenerationId', 'active_generation_id'],
13
+ ['desiredState', 'desired_state'],
14
+ ['status', 'status'],
15
+ ['pid', 'pid'],
16
+ ['dnsConnected', 'dns_connected'],
17
+ ['autostart', 'autostart'],
18
+ ['keepAlive', 'keep_alive'],
19
+ ['error', 'error'],
20
+ ['readyAt', 'ready_at'],
21
+ ['lastHeartbeatAt', 'last_heartbeat_at'],
22
+ ['heartbeatIntervalMs', 'heartbeat_interval_ms'],
23
+ ]);
24
+ function boolean(value) {
25
+ return Boolean(value);
26
+ }
27
+ // 'starting' predates the kernel status vocabulary, whose word is
28
+ // 'building'; rows written by older versions normalize at the read
29
+ // boundary so the legacy spelling never escapes the store.
30
+ function appStatus(value) {
31
+ return value === 'starting' ? 'building' : value;
32
+ }
33
+ function appRow(row) {
34
+ if (!row)
35
+ return null;
36
+ return {
37
+ id: row.id,
38
+ name: row.name,
39
+ description: row.description,
40
+ appRef: row.app_ref,
41
+ publicUrl: row.public_url,
42
+ port: row.port,
43
+ sourceDir: row.source_dir,
44
+ dataDir: row.data_dir,
45
+ activeReleaseId: row.active_release_id,
46
+ activeDeploymentId: row.active_deployment_id,
47
+ activeGenerationId: row.active_generation_id,
48
+ desiredState: row.desired_state,
49
+ status: appStatus(row.status),
50
+ pid: row.pid,
51
+ dnsConnected: boolean(row.dns_connected),
52
+ autostart: boolean(row.autostart),
53
+ keepAlive: boolean(row.keep_alive),
54
+ error: row.error,
55
+ readyAt: row.ready_at,
56
+ lastHeartbeatAt: row.last_heartbeat_at,
57
+ heartbeatIntervalMs: row.heartbeat_interval_ms,
58
+ createdAt: row.created_at,
59
+ updatedAt: row.updated_at,
60
+ };
61
+ }
62
+ function releaseRow(row) {
63
+ if (!row)
64
+ return null;
65
+ return {
66
+ id: row.id,
67
+ appId: row.app_id,
68
+ digest: row.digest,
69
+ directory: row.directory,
70
+ manifest: JSON.parse(row.manifest_json),
71
+ createdAt: row.created_at,
72
+ };
73
+ }
74
+ function deploymentRow(row) {
75
+ if (!row)
76
+ return null;
77
+ return {
78
+ sequence: row.sequence,
79
+ id: row.id,
80
+ appId: row.app_id,
81
+ releaseId: row.release_id,
82
+ directory: row.directory,
83
+ reason: row.reason,
84
+ status: row.status,
85
+ error: row.error,
86
+ createdAt: row.created_at,
87
+ activatedAt: row.activated_at,
88
+ };
89
+ }
90
+ class Store {
91
+ db;
92
+ generations;
93
+ constructor(file) {
94
+ fs.mkdirSync(path.dirname(file), { recursive: true });
95
+ this.db = new Database(file);
96
+ this.db.pragma('journal_mode = WAL');
97
+ this.db.pragma('foreign_keys = ON');
98
+ this.db.pragma('busy_timeout = 5000');
99
+ migrate(this.db);
100
+ this.generations = new Generations(this.db);
101
+ }
102
+ createApp(app) {
103
+ this.db.prepare(`
104
+ INSERT INTO apps (
105
+ id, name, description, app_ref, public_url, port, source_dir, data_dir,
106
+ desired_state, status, dns_connected, autostart, keep_alive,
107
+ created_at, updated_at
108
+ ) VALUES (
109
+ @id, @name, @description, @appRef, @publicUrl, @port, @sourceDir, @dataDir,
110
+ @desiredState, @status, @dnsConnected, @autostart, @keepAlive,
111
+ @createdAt, @updatedAt
112
+ )
113
+ `).run({
114
+ ...app,
115
+ description: app.description || '',
116
+ desiredState: app.desiredState || 'running',
117
+ status: app.status || 'registered',
118
+ dnsConnected: app.dnsConnected === false ? 0 : 1,
119
+ autostart: app.autostart === false ? 0 : 1,
120
+ keepAlive: app.keepAlive === false ? 0 : 1,
121
+ });
122
+ return this.getApp(app.id);
123
+ }
124
+ getApp(id) {
125
+ return appRow(this.db.prepare('SELECT * FROM apps WHERE id = ?').get(id));
126
+ }
127
+ listApps() {
128
+ return this.db.prepare('SELECT * FROM apps ORDER BY created_at, id').all().map((value) => appRow(value)).filter(Boolean);
129
+ }
130
+ updateApp(id, patch) {
131
+ const entries = Object.entries(patch)
132
+ .filter(([key, value]) => APP_COLUMNS.has(key) && value !== undefined);
133
+ if (!entries.length)
134
+ return this.getApp(id);
135
+ const values = { id, updatedAt: new Date().toISOString() };
136
+ const assignments = entries.map(([key, value]) => {
137
+ values[key] = typeof value === 'boolean' ? Number(value) : value;
138
+ return `${APP_COLUMNS.get(key)} = @${key}`;
139
+ });
140
+ assignments.push('updated_at = @updatedAt');
141
+ this.db.prepare(`UPDATE apps SET ${assignments.join(', ')} WHERE id = @id`).run(values);
142
+ return this.getApp(id);
143
+ }
144
+ deleteApp(id) {
145
+ const app = this.getApp(id);
146
+ if (app)
147
+ this.db.prepare('DELETE FROM apps WHERE id = ?').run(id);
148
+ return app;
149
+ }
150
+ addRelease(release) {
151
+ this.db.prepare(`
152
+ INSERT OR IGNORE INTO releases
153
+ (id, app_id, digest, directory, manifest_json, created_at)
154
+ VALUES (@id, @appId, @digest, @directory, @manifestJson, @createdAt)
155
+ `).run({ ...release, manifestJson: JSON.stringify(release.manifest) });
156
+ return this.getRelease(release.id);
157
+ }
158
+ getRelease(id) {
159
+ return releaseRow(this.db.prepare('SELECT * FROM releases WHERE id = ?').get(id));
160
+ }
161
+ listReleases(appId) {
162
+ return this.db.prepare(`
163
+ SELECT * FROM releases WHERE app_id = ? ORDER BY created_at DESC, id DESC
164
+ `).all(appId).map((value) => releaseRow(value)).filter(Boolean);
165
+ }
166
+ createDeployment(deployment) {
167
+ this.db.prepare(`
168
+ INSERT INTO deployments
169
+ (id, app_id, release_id, directory, reason, status, created_at)
170
+ VALUES (@id, @appId, @releaseId, @directory, @reason, 'prepared', @createdAt)
171
+ `).run(deployment);
172
+ return this.getDeployment(deployment.id);
173
+ }
174
+ getDeployment(id) {
175
+ return deploymentRow(this.db.prepare('SELECT * FROM deployments WHERE id = ?').get(id));
176
+ }
177
+ listDeployments(appId) {
178
+ return this.db.prepare(`
179
+ SELECT * FROM deployments WHERE app_id = ? ORDER BY sequence DESC
180
+ `).all(appId).map((value) => deploymentRow(value)).filter(Boolean);
181
+ }
182
+ activate(appId, deploymentId, now = new Date().toISOString()) {
183
+ this.db.transaction(() => {
184
+ const deployment = this.db.prepare('SELECT * FROM deployments WHERE id = ? AND app_id = ?').get(deploymentId, appId);
185
+ if (!deployment)
186
+ throw new Error(`Deployment not found: ${deploymentId}`);
187
+ this.db.prepare(`
188
+ UPDATE deployments SET status = 'superseded'
189
+ WHERE app_id = ? AND status = 'active'
190
+ `).run(appId);
191
+ this.db.prepare(`
192
+ UPDATE deployments SET status = 'active', error = NULL, activated_at = ?
193
+ WHERE id = ?
194
+ `).run(now, deploymentId);
195
+ this.db.prepare(`
196
+ UPDATE apps SET active_release_id = ?, active_deployment_id = ?,
197
+ status = 'building', error = NULL, updated_at = ?
198
+ WHERE id = ?
199
+ `).run(deployment.release_id, deploymentId, now, appId);
200
+ })();
201
+ return this.getApp(appId);
202
+ }
203
+ failDeployment(id, error) {
204
+ this.db.prepare(`
205
+ UPDATE deployments SET status = 'failed', error = ? WHERE id = ?
206
+ `).run(String(error), id);
207
+ }
208
+ previousDeployment(appId, activeReleaseId) {
209
+ return deploymentRow(this.db.prepare(`
210
+ SELECT * FROM deployments
211
+ WHERE app_id = ? AND status IN ('active', 'superseded')
212
+ AND release_id != ?
213
+ ORDER BY sequence DESC LIMIT 1
214
+ `).get(appId, activeReleaseId));
215
+ }
216
+ close() {
217
+ this.db.close();
218
+ }
219
+ }
220
+ export { Store };
221
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { DEFAULT_RESTART_POLICY } from './restarts.js';
2
+ import type { AppRecord, StartOptions, StopOptions, SupervisorOptions } from './types.js';
3
+ declare class Supervisor {
4
+ private readonly store;
5
+ private readonly stateDir;
6
+ private readonly environment;
7
+ private readonly changed;
8
+ private readonly faults;
9
+ private readonly spawn;
10
+ private readonly settleMs;
11
+ private readonly stopGraceMs;
12
+ private readonly killGraceMs;
13
+ private readonly maxLogBytes;
14
+ private readonly reconcileIntervalMs;
15
+ private readonly children;
16
+ private readonly stopping;
17
+ private readonly restarts;
18
+ private readonly reconciler;
19
+ constructor(options: SupervisorOptions);
20
+ logPath(appId: string): string;
21
+ start(appId: string, options?: StartOptions): Promise<AppRecord>;
22
+ private onExit;
23
+ stop(appId: string, options?: StopOptions): Promise<AppRecord>;
24
+ boot(): Promise<PromiseSettledResult<AppRecord>[]>;
25
+ close(): Promise<void>;
26
+ detach(): void;
27
+ }
28
+ export { DEFAULT_RESTART_POLICY, Supervisor };
29
+ export type { StartOptions, StopOptions, SupervisorOptions } from './types.js';
@@ -0,0 +1,279 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { CappedLog, DEFAULT_MAX_LOG_BYTES } from './logs.js';
6
+ import { appProcessEnvironment, replacePort } from './presentation.js';
7
+ import { isAlive, killTree, processIdentity, waitForDeath, } from './process.js';
8
+ import { Reconciler } from './reconciler.js';
9
+ import { DEFAULT_RESTART_POLICY, Restarts } from './restarts.js';
10
+ import { errorMessage } from './boundaries.js';
11
+ class Supervisor {
12
+ store;
13
+ stateDir;
14
+ environment;
15
+ changed;
16
+ faults;
17
+ spawn;
18
+ settleMs;
19
+ stopGraceMs;
20
+ killGraceMs;
21
+ maxLogBytes;
22
+ reconcileIntervalMs;
23
+ children = new Map();
24
+ stopping = new Set();
25
+ restarts;
26
+ reconciler;
27
+ constructor(options) {
28
+ this.store = options.store;
29
+ this.stateDir = options.stateDir;
30
+ this.environment = options.environment || (() => ({}));
31
+ this.changed = options.changed || (() => { });
32
+ this.faults = options.faults;
33
+ this.spawn = options.spawn || spawn;
34
+ this.settleMs = options.settleMs ?? 1_000;
35
+ this.stopGraceMs = options.stopGraceMs ?? 2_000;
36
+ this.killGraceMs = options.killGraceMs ?? 2_000;
37
+ this.maxLogBytes = options.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
38
+ this.reconcileIntervalMs = options.reconcileIntervalMs ?? 5_000;
39
+ this.restarts = new Restarts({
40
+ store: this.store,
41
+ changed: this.changed,
42
+ restart: options.restart || ((appId) => this.start(appId, { automatic: true })),
43
+ policy: options.restartPolicy,
44
+ faults: this.faults,
45
+ });
46
+ this.reconciler = new Reconciler({
47
+ store: this.store,
48
+ changed: this.changed,
49
+ intervalMs: this.reconcileIntervalMs,
50
+ failed: (appId, error) => this.restarts.afterFailure(appId, error),
51
+ faults: this.faults,
52
+ });
53
+ fs.mkdirSync(path.join(this.stateDir, 'logs'), { recursive: true });
54
+ }
55
+ logPath(appId) {
56
+ return path.join(this.stateDir, 'logs', `${appId}.log`);
57
+ }
58
+ async start(appId, options = {}) {
59
+ let app = this.store.getApp(appId);
60
+ if (!app)
61
+ throw new Error(`App not found: ${appId}`);
62
+ if (!app.activeDeploymentId)
63
+ throw new Error(`App has no active deployment: ${appId}`);
64
+ const deployment = this.store.getDeployment(app.activeDeploymentId);
65
+ const release = this.store.getRelease(app.activeReleaseId);
66
+ if (!deployment || !release)
67
+ throw new Error(`Active release is incomplete: ${appId}`);
68
+ const existing = this.children.get(appId);
69
+ if (existing && existing.exitCode === null) {
70
+ return this.store.updateApp(appId, { desiredState: 'running', status: 'running', error: null });
71
+ }
72
+ if (app.pid && isAlive(app.pid)) {
73
+ return this.reconciler.adopt(app);
74
+ }
75
+ if (app.pid) {
76
+ if (app.activeGenerationId) {
77
+ this.store.generations.finish(app.activeGenerationId, {
78
+ status: 'crashed',
79
+ error: 'Recorded process was no longer alive',
80
+ });
81
+ }
82
+ app = this.store.updateApp(appId, {
83
+ activeGenerationId: null,
84
+ pid: null,
85
+ });
86
+ }
87
+ if (options.automatic && !this.restarts.permit(appId))
88
+ return this.store.getApp(appId);
89
+ this.restarts.cancel(appId);
90
+ fs.mkdirSync(app.dataDir, { recursive: true });
91
+ const generation = this.store.generations.create({
92
+ id: `gen-${crypto.randomUUID()}`,
93
+ appId,
94
+ deploymentId: deployment.id,
95
+ createdAt: new Date().toISOString(),
96
+ });
97
+ const output = new CappedLog(this.logPath(appId), this.maxLogBytes);
98
+ output.on('error', (error) => {
99
+ this.faults.degrade(appId, error, 'App log stream failed');
100
+ });
101
+ const command = replacePort(release.manifest.startCommand, app.port);
102
+ let child;
103
+ try {
104
+ child = this.spawn(command, {
105
+ cwd: deployment.directory,
106
+ env: appProcessEnvironment(app, this.environment(app)),
107
+ shell: true,
108
+ detached: process.platform !== 'win32',
109
+ windowsHide: true,
110
+ stdio: ['ignore', 'pipe', 'pipe'],
111
+ });
112
+ }
113
+ catch (error) {
114
+ output.end();
115
+ this.store.generations.finish(generation.id, { status: 'failed', error: errorMessage(error) });
116
+ throw error;
117
+ }
118
+ child.stdout.pipe(output, { end: false });
119
+ child.stderr.pipe(output, { end: false });
120
+ child.once('close', () => output.end());
121
+ this.children.set(appId, child);
122
+ let settled = false;
123
+ const startupExit = new Promise((resolve) => {
124
+ child.on('error', (error) => {
125
+ if (!settled)
126
+ resolve(error);
127
+ else
128
+ this.faults.degrade(appId, error, 'Managed child emitted an error');
129
+ });
130
+ child.once('exit', (code, signal) => {
131
+ this.faults.guard(appId, 'Managed child exit handler failed', () => {
132
+ this.onExit(appId, generation.id, child, code, signal, settled);
133
+ });
134
+ resolve(settled ? null : new Error(`App exited during startup (${signal || `code ${code}`})`));
135
+ });
136
+ });
137
+ const identity = await processIdentity(child.pid ?? null);
138
+ if (!identity || child.exitCode !== null) {
139
+ const startupError = child.exitCode === null ? null : await startupExit;
140
+ if (startupError)
141
+ throw startupError;
142
+ throw new Error('App process identity could not be established');
143
+ }
144
+ this.store.generations.attach(generation.id, child.pid, identity);
145
+ app = this.store.updateApp(appId, {
146
+ activeGenerationId: generation.id,
147
+ desiredState: 'running',
148
+ status: 'building',
149
+ pid: child.pid || null,
150
+ error: null,
151
+ readyAt: null,
152
+ lastHeartbeatAt: null,
153
+ });
154
+ const survived = new Promise((resolve) => setTimeout(() => resolve(null), this.settleMs));
155
+ const startupError = await Promise.race([startupExit, survived]);
156
+ if (startupError)
157
+ throw startupError;
158
+ if (await processIdentity(child.pid ?? null) !== identity)
159
+ throw new Error('App exited during startup before stability was proven');
160
+ settled = true;
161
+ child.unref?.();
162
+ this.store.generations.stable(generation.id);
163
+ const current = this.store.updateApp(appId, {
164
+ activeGenerationId: generation.id,
165
+ status: 'running',
166
+ error: null,
167
+ pid: child.pid,
168
+ });
169
+ this.faults.clear(appId);
170
+ this.changed(current);
171
+ return current;
172
+ }
173
+ onExit(appId, generationId, child, code, signal, survivedStartup) {
174
+ if (this.children.get(appId) !== child)
175
+ return;
176
+ this.children.delete(appId);
177
+ const expected = this.stopping.delete(appId);
178
+ this.store.generations.finish(generationId, {
179
+ status: expected ? 'exited' : 'crashed',
180
+ exitCode: code,
181
+ exitSignal: signal,
182
+ });
183
+ if (expected || survivedStartup === false)
184
+ return;
185
+ const app = this.store.getApp(appId);
186
+ if (!app)
187
+ return;
188
+ if (app.desiredState !== 'running' || app.keepAlive === false) {
189
+ this.changed(this.store.updateApp(appId, {
190
+ status: code === 0 ? 'stopped' : 'error',
191
+ activeGenerationId: null,
192
+ pid: null,
193
+ error: code === 0 ? null : `Process exited with ${signal || `code ${code}`}`,
194
+ }));
195
+ return;
196
+ }
197
+ this.restarts.afterFailure(appId, `Process exited with ${signal || `code ${code}`}`);
198
+ }
199
+ async stop(appId, options = {}) {
200
+ const app = this.store.getApp(appId);
201
+ if (!app)
202
+ throw new Error(`App not found: ${appId}`);
203
+ this.restarts.cancel(appId);
204
+ const child = this.children.get(appId);
205
+ const pid = child?.pid || app.pid;
206
+ if (pid) {
207
+ this.stopping.add(appId);
208
+ try {
209
+ killTree(pid, 'SIGTERM');
210
+ let dead = await waitForDeath(pid, this.stopGraceMs, { group: true });
211
+ if (!dead) {
212
+ killTree(pid, 'SIGKILL');
213
+ dead = await waitForDeath(pid, this.killGraceMs, { group: true });
214
+ }
215
+ if (!dead)
216
+ throw new Error(`Process generation ${pid} did not stop`);
217
+ }
218
+ catch (error) {
219
+ this.stopping.delete(appId);
220
+ const current = this.store.updateApp(appId, {
221
+ desiredState: options.preserveDesiredState ? app.desiredState : 'stopped',
222
+ status: 'error',
223
+ pid,
224
+ error: errorMessage(error),
225
+ });
226
+ this.changed(current);
227
+ throw error;
228
+ }
229
+ }
230
+ this.children.delete(appId);
231
+ this.reconciler.forget(appId);
232
+ if (app.activeGenerationId) {
233
+ this.store.generations.finish(app.activeGenerationId, { status: 'exited' });
234
+ }
235
+ const current = this.store.updateApp(appId, {
236
+ activeGenerationId: null,
237
+ desiredState: options.preserveDesiredState ? app.desiredState : 'stopped',
238
+ status: 'stopped',
239
+ pid: null,
240
+ error: null,
241
+ });
242
+ this.faults.clear(appId);
243
+ this.changed(current);
244
+ return current;
245
+ }
246
+ async boot() {
247
+ const results = await Promise.allSettled(this.store.listApps()
248
+ .filter((app) => app.autostart && app.desiredState === 'running' && app.status !== 'degraded')
249
+ .map(async (app) => {
250
+ try {
251
+ return await this.start(app.id, { automatic: true });
252
+ }
253
+ catch (error) {
254
+ this.restarts.afterFailure(app.id, errorMessage(error));
255
+ throw error;
256
+ }
257
+ }));
258
+ this.reconciler.start();
259
+ return results;
260
+ }
261
+ async close() {
262
+ this.reconciler.stop();
263
+ this.restarts.close();
264
+ await Promise.allSettled(this.store.listApps()
265
+ .filter((app) => this.children.has(app.id) || isAlive(app.pid))
266
+ .map((app) => this.stop(app.id, { preserveDesiredState: true })));
267
+ }
268
+ detach() {
269
+ this.reconciler.stop();
270
+ this.restarts.close();
271
+ for (const child of this.children.values()) {
272
+ child.removeAllListeners();
273
+ child.unref?.();
274
+ }
275
+ this.children.clear();
276
+ }
277
+ }
278
+ export { DEFAULT_RESTART_POLICY, Supervisor };
279
+ //# sourceMappingURL=supervisor.js.map