unitup 0.2.0 → 0.3.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.
@@ -0,0 +1,407 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { DrainManager } from '../core/drain-manager.js';
3
+ import { GenerationManager } from '../core/generation-manager.js';
4
+ import { IPCClient } from '../core/ipc.js';
5
+ import { DeploymentLock } from '../core/lock.js';
6
+ import { ProcessManager } from '../core/process-manager.js';
7
+ import { ReadinessChecker } from '../core/readiness.js';
8
+ import { Router } from '../core/router.js';
9
+ import { readAppMetadata } from '../utils.js';
10
+
11
+ /**
12
+ * Deployment State Machine states.
13
+ */
14
+ export const DeploymentState = {
15
+ IDLE: 'IDLE',
16
+ CREATING_GENERATION: 'CREATING_GENERATION',
17
+ STARTING: 'STARTING',
18
+ WAITING_READY: 'WAITING_READY',
19
+ SWITCHING: 'SWITCHING',
20
+ STABILIZING: 'STABILIZING',
21
+ SETTING_CANARY: 'SETTING_CANARY',
22
+ PROMOTING: 'PROMOTING',
23
+ DRAINING: 'DRAINING',
24
+ STOPPING_PREVIOUS: 'STOPPING_PREVIOUS',
25
+ COMPLETE: 'COMPLETE',
26
+ FAILED: 'FAILED'
27
+ };
28
+
29
+ /**
30
+ * DeploymentManager is orchestration only.
31
+ * It strictly composes the Unitup core primitives to achieve zero-downtime deployment.
32
+ */
33
+ export class DeploymentManager extends EventEmitter {
34
+ /**
35
+ * @param {object} [options]
36
+ * @param {import('../core/generation-manager.js').GenerationManager} [options.generationManager]
37
+ * @param {import('../core/router.js').Router} [options.router]
38
+ * @param {import('../core/readiness.js').ReadinessChecker} [options.readiness]
39
+ * @param {import('../core/drain-manager.js').DrainManager} [options.drainManager]
40
+ * @param {import('../core/process-manager.js').ProcessManager} [options.processManager]
41
+ * @param {import('../core/lock.js').DeploymentLock} [options.lock]
42
+ * @param {number} [options.defaultStabilizationMs=5000]
43
+ */
44
+ constructor(options = {}) {
45
+ super();
46
+ this.generationManager = options.generationManager;
47
+ this.router = options.router;
48
+ this.readiness = options.readiness;
49
+ this.drainManager = options.drainManager;
50
+ this.processManager = options.processManager;
51
+ this.lock = options.lock;
52
+ this.defaultStabilizationMs = options.defaultStabilizationMs !== undefined ? options.defaultStabilizationMs : 5000;
53
+ this.state = DeploymentState.IDLE;
54
+ }
55
+
56
+ _ensurePrimitives(serviceName, options = {}) {
57
+ if (!this.processManager) {
58
+ this.processManager = new ProcessManager();
59
+ }
60
+ if (!this.generationManager) {
61
+ this.generationManager = new GenerationManager({
62
+ processManager: this.processManager,
63
+ stateDir: options.stateDir
64
+ });
65
+ }
66
+ if (!this.router) {
67
+ this.router = new Router();
68
+ }
69
+ if (!this.readiness) {
70
+ this.readiness = new ReadinessChecker({ processManager: this.processManager });
71
+ }
72
+ if (!this.drainManager) {
73
+ this.drainManager = new DrainManager({
74
+ router: this.router,
75
+ processManager: this.processManager
76
+ });
77
+ }
78
+ if (!this.lock) {
79
+ this.lock = new DeploymentLock({
80
+ lockDir: options.lockDir,
81
+ processManager: this.processManager
82
+ });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Emits state change event and optional progress notification.
88
+ * @param {string} newState
89
+ * @param {object} [details]
90
+ * @param {function} [onProgress]
91
+ */
92
+ _setState(newState, details = {}, onProgress) {
93
+ const oldState = this.state;
94
+ this.state = newState;
95
+ const payload = { from: oldState, to: newState, state: newState, ...details };
96
+ this.emit('stateChange', payload);
97
+ if (onProgress) {
98
+ onProgress(payload);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Executes a zero-downtime deployment for a service.
104
+ *
105
+ * @param {string} serviceName
106
+ * @param {object} [serviceConfig] - Config containing command, args, cwd, env, port, deploy opts
107
+ * @param {object} [options]
108
+ * @param {number} [options.publicPort]
109
+ * @param {number} [options.stabilizationWindowMs]
110
+ * @param {number} [options.readinessTimeout]
111
+ * @param {string} [options.readyPath]
112
+ * @param {number} [options.drainTimeout]
113
+ * @param {function(object): void} [options.onProgress]
114
+ * @returns {Promise<object>} Deployment summary
115
+ */
116
+ async deploy(serviceName, serviceConfig = {}, options = {}) {
117
+ if (!serviceName) {
118
+ throw new Error('Service name is required for deployment.');
119
+ }
120
+
121
+ // Check if a supervisor daemon is running for this service via IPC
122
+ const ipcClient = new IPCClient(serviceName, options.ipcDir);
123
+ const isRemote = await ipcClient.isAlive();
124
+ if (isRemote) {
125
+ return ipcClient.send(
126
+ {
127
+ action: 'deploy',
128
+ options: { ...options, ...serviceConfig }
129
+ },
130
+ options.onProgress
131
+ );
132
+ }
133
+
134
+ this._ensurePrimitives(serviceName, options);
135
+
136
+ const config =
137
+ serviceConfig && (serviceConfig.command || serviceConfig.script)
138
+ ? serviceConfig
139
+ : readAppMetadata(serviceName) || serviceConfig || {};
140
+
141
+ // 1. Acquire per-service deployment lock
142
+ await this.lock.acquire(serviceName);
143
+
144
+ let next = null;
145
+ let previous = null;
146
+
147
+ try {
148
+ this._setState(DeploymentState.IDLE, { service: serviceName }, options.onProgress);
149
+
150
+ previous = this.generationManager.getActive(serviceName);
151
+
152
+ // 2. CREATING_GENERATION
153
+ this._setState(
154
+ DeploymentState.CREATING_GENERATION,
155
+ { service: serviceName, previousGeneration: previous?.id },
156
+ options.onProgress
157
+ );
158
+
159
+ next = await this.generationManager.create(serviceName, config, options);
160
+
161
+ // 3. STARTING
162
+ this._setState(
163
+ DeploymentState.STARTING,
164
+ {
165
+ service: serviceName,
166
+ generation: next.id,
167
+ pid: next.pid,
168
+ internalPort: next.internalPort
169
+ },
170
+ options.onProgress
171
+ );
172
+
173
+ // 4. WAITING_READY
174
+ this._setState(
175
+ DeploymentState.WAITING_READY,
176
+ { service: serviceName, generation: next.id },
177
+ options.onProgress
178
+ );
179
+
180
+ try {
181
+ const readyPath = options.readyPath || config.deploy?.ready;
182
+ const readinessTimeout = options.readinessTimeout || 15000;
183
+ await this.readiness.wait(next, {
184
+ path: readyPath,
185
+ timeout: readinessTimeout,
186
+ configuredPort: options.publicPort || config.port,
187
+ router: this.router
188
+ });
189
+ } catch (readinessErr) {
190
+ // Readiness failed: clean up next generation, leave router pointing at previous
191
+ await this.processManager.stop(next.pid, { timeout: 2000 });
192
+ this.generationManager.remove(next, true);
193
+ this._setState(
194
+ DeploymentState.FAILED,
195
+ { service: serviceName, error: readinessErr.message },
196
+ options.onProgress
197
+ );
198
+ throw readinessErr;
199
+ }
200
+
201
+ const isCanary = Boolean(options.canary);
202
+ const rawWeight =
203
+ options.weight !== undefined ? options.weight : options.canaryWeight !== undefined ? options.canaryWeight : 0.1;
204
+ const parsedWeight = typeof rawWeight === 'string' ? Number.parseFloat(rawWeight.replace('%', '')) : Number(rawWeight);
205
+ const numWeight = Number.isNaN(parsedWeight) ? 0.1 : parsedWeight;
206
+ const canaryWeight = Math.max(0, Math.min(1, numWeight > 1 ? numWeight / 100 : numWeight));
207
+
208
+ if (isCanary) {
209
+ const publicPort = options.publicPort || config.port;
210
+ if (publicPort && (!this.router.server || this.router.listeningPort !== publicPort)) {
211
+ await this.router.listen(publicPort);
212
+ }
213
+
214
+ if (!previous) {
215
+ this.router.switchBackend(serviceName, next);
216
+ this.generationManager.markActive(next);
217
+ }
218
+
219
+ this._setState(
220
+ DeploymentState.SETTING_CANARY,
221
+ { service: serviceName, generation: next.id, weight: canaryWeight },
222
+ options.onProgress
223
+ );
224
+
225
+ this.router.setCanary(serviceName, next, canaryWeight);
226
+ this.generationManager.markCanary(next, canaryWeight);
227
+
228
+ const result = {
229
+ service: serviceName,
230
+ isCanary: true,
231
+ activeGeneration: previous?.id ?? next.id,
232
+ canaryGeneration: next.id,
233
+ canaryWeight,
234
+ downtimeMs: 0,
235
+ status: 'canary'
236
+ };
237
+
238
+ this._setState(DeploymentState.COMPLETE, result, options.onProgress);
239
+ return result;
240
+ }
241
+
242
+ // 5. SWITCHING
243
+ this._setState(
244
+ DeploymentState.SWITCHING,
245
+ { service: serviceName, generation: next.id },
246
+ options.onProgress
247
+ );
248
+
249
+ const publicPort = options.publicPort || config.port;
250
+ if (publicPort && (!this.router.server || this.router.listeningPort !== publicPort)) {
251
+ await this.router.listen(publicPort);
252
+ }
253
+
254
+ this.router.switchBackend(serviceName, next);
255
+ this.generationManager.markActive(next);
256
+
257
+ // 6. STABILIZING
258
+ this._setState(
259
+ DeploymentState.STABILIZING,
260
+ { service: serviceName, generation: next.id },
261
+ options.onProgress
262
+ );
263
+
264
+ const stabilizationMs =
265
+ options.stabilizationWindowMs !== undefined ? options.stabilizationWindowMs : this.defaultStabilizationMs;
266
+
267
+ if (stabilizationMs > 0) {
268
+ const checkInterval = 50;
269
+ const stabStart = Date.now();
270
+ while (Date.now() - stabStart < stabilizationMs) {
271
+ if (!this.processManager.isAlive(next.pid)) {
272
+ // New generation crashed during stabilization window! Revert to previous!
273
+ if (previous && this.processManager.isAlive(previous.pid)) {
274
+ this.router.switchBackend(serviceName, previous);
275
+ this.generationManager.markActive(previous);
276
+ }
277
+ this.generationManager.remove(next, true);
278
+ this._setState(
279
+ DeploymentState.FAILED,
280
+ {
281
+ service: serviceName,
282
+ error: `Generation #${next.id} crashed during stabilization window.`
283
+ },
284
+ options.onProgress
285
+ );
286
+ throw new Error(
287
+ `Generation #${next.id} crashed during stabilization window. Rolled back traffic to previous generation #${previous?.id || 'none'}.`
288
+ );
289
+ }
290
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
291
+ }
292
+ }
293
+
294
+ // 7. DRAINING
295
+ if (previous && previous.id !== next.id) {
296
+ this._setState(
297
+ DeploymentState.DRAINING,
298
+ { service: serviceName, generation: previous.id },
299
+ options.onProgress
300
+ );
301
+
302
+ this.generationManager.markDraining(previous);
303
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
304
+ await this.drainManager.drain(previous, { timeout: drainTimeout, stopProcess: false });
305
+ }
306
+
307
+ // 8. STOPPING_PREVIOUS
308
+ if (previous && previous.id !== next.id) {
309
+ this._setState(
310
+ DeploymentState.STOPPING_PREVIOUS,
311
+ { service: serviceName, generation: previous.id },
312
+ options.onProgress
313
+ );
314
+
315
+ await this.processManager.stop(previous.pid, { timeout: 3000 });
316
+ this.generationManager.remove(previous, false);
317
+ }
318
+
319
+ // 9. COMPLETE
320
+ const result = {
321
+ service: serviceName,
322
+ previousGeneration: previous?.id ?? null,
323
+ currentGeneration: next.id,
324
+ downtimeMs: 0,
325
+ status: 'complete'
326
+ };
327
+
328
+ this._setState(DeploymentState.COMPLETE, result, options.onProgress);
329
+ return result;
330
+ } finally {
331
+ await this.lock.release(serviceName);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Promotes an existing canary generation to 100% active primary and drains previous.
337
+ *
338
+ * @param {string} serviceName
339
+ * @param {object} [config]
340
+ * @param {object} [options]
341
+ * @returns {Promise<object>}
342
+ */
343
+ async promote(serviceName, config = {}, options = {}) {
344
+ this._ensurePrimitives(serviceName, options);
345
+
346
+ const ipcClient = new IPCClient(serviceName, options.ipcDir);
347
+ if (await ipcClient.isAlive()) {
348
+ return ipcClient.send({ action: 'promote', options }, options.onProgress);
349
+ }
350
+
351
+ await this.lock.acquire(serviceName, { timeout: options.lockTimeout || 10000 });
352
+
353
+ try {
354
+ const canaryGen = this.generationManager.getCanary(serviceName);
355
+ if (!canaryGen) {
356
+ throw new Error(`No canary generation found for service "${serviceName}" to promote.`);
357
+ }
358
+
359
+ const previous = this.generationManager.getActive(serviceName);
360
+
361
+ this._setState(
362
+ DeploymentState.PROMOTING,
363
+ { service: serviceName, generation: canaryGen.id, previousGeneration: previous?.id },
364
+ options.onProgress
365
+ );
366
+
367
+ // 1. Promote in Router (clears canary, makes it primary)
368
+ this.router.promoteCanary(serviceName);
369
+ this.generationManager.markActive(canaryGen);
370
+
371
+ // 2. Drain previous generation
372
+ if (previous && previous.id !== canaryGen.id) {
373
+ this._setState(
374
+ DeploymentState.DRAINING,
375
+ { service: serviceName, generation: previous.id },
376
+ options.onProgress
377
+ );
378
+ this.generationManager.markDraining(previous);
379
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
380
+ await this.drainManager.drain(previous, { timeout: drainTimeout, stopProcess: false });
381
+
382
+ this._setState(
383
+ DeploymentState.STOPPING_PREVIOUS,
384
+ { service: serviceName, generation: previous.id },
385
+ options.onProgress
386
+ );
387
+ await this.processManager.stop(previous.pid, { timeout: 3000 });
388
+ this.generationManager.remove(previous, false);
389
+ }
390
+
391
+ const result = {
392
+ service: serviceName,
393
+ promotedGeneration: canaryGen.id,
394
+ previousGeneration: previous?.id ?? null,
395
+ downtimeMs: 0,
396
+ status: 'promoted'
397
+ };
398
+
399
+ this._setState(DeploymentState.COMPLETE, result, options.onProgress);
400
+ return result;
401
+ } finally {
402
+ await this.lock.release(serviceName);
403
+ }
404
+ }
405
+ }
406
+
407
+ export const defaultDeploymentManager = new DeploymentManager();
@@ -0,0 +1,9 @@
1
+ export {
2
+ DeploymentManager,
3
+ DeploymentState,
4
+ defaultDeploymentManager
5
+ } from './deployment-manager.js';
6
+ export {
7
+ RollbackManager,
8
+ defaultRollbackManager
9
+ } from './rollback-manager.js';
@@ -0,0 +1,186 @@
1
+ import { DrainManager } from '../core/drain-manager.js';
2
+ import { GenerationManager } from '../core/generation-manager.js';
3
+ import { IPCClient } from '../core/ipc.js';
4
+ import { DeploymentLock } from '../core/lock.js';
5
+ import { ProcessManager } from '../core/process-manager.js';
6
+ import { ReadinessChecker } from '../core/readiness.js';
7
+ import { Router } from '../core/router.js';
8
+ import { readAppMetadata } from '../utils.js';
9
+
10
+ /**
11
+ * RollbackManager provides orchestration for reverting traffic to a previous generation.
12
+ * It strictly composes the Unitup core primitives.
13
+ */
14
+ export class RollbackManager {
15
+ /**
16
+ * @param {object} [options]
17
+ * @param {import('../core/generation-manager.js').GenerationManager} [options.generationManager]
18
+ * @param {import('../core/router.js').Router} [options.router]
19
+ * @param {import('../core/readiness.js').ReadinessChecker} [options.readiness]
20
+ * @param {import('../core/drain-manager.js').DrainManager} [options.drainManager]
21
+ * @param {import('../core/process-manager.js').ProcessManager} [options.processManager]
22
+ * @param {import('../core/lock.js').DeploymentLock} [options.lock]
23
+ */
24
+ constructor(options = {}) {
25
+ this.generationManager = options.generationManager;
26
+ this.router = options.router;
27
+ this.readiness = options.readiness;
28
+ this.drainManager = options.drainManager;
29
+ this.processManager = options.processManager;
30
+ this.lock = options.lock;
31
+ }
32
+
33
+ _ensurePrimitives(serviceName, options = {}) {
34
+ if (!this.processManager) {
35
+ this.processManager = new ProcessManager();
36
+ }
37
+ if (!this.generationManager) {
38
+ this.generationManager = new GenerationManager({
39
+ processManager: this.processManager,
40
+ stateDir: options.stateDir
41
+ });
42
+ }
43
+ if (!this.router) {
44
+ this.router = new Router();
45
+ }
46
+ if (!this.readiness) {
47
+ this.readiness = new ReadinessChecker({ processManager: this.processManager });
48
+ }
49
+ if (!this.drainManager) {
50
+ this.drainManager = new DrainManager({
51
+ router: this.router,
52
+ processManager: this.processManager
53
+ });
54
+ }
55
+ if (!this.lock) {
56
+ this.lock = new DeploymentLock({
57
+ lockDir: options.lockDir,
58
+ processManager: this.processManager
59
+ });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Rolls back a service to its previous generation.
65
+ *
66
+ * @param {string} serviceName
67
+ * @param {object} [serviceConfig]
68
+ * @param {object} [options]
69
+ * @param {string} [options.readyPath]
70
+ * @param {number} [options.drainTimeout]
71
+ * @param {function(object): void} [options.onProgress]
72
+ * @returns {Promise<object>} Rollback summary
73
+ */
74
+ async rollback(serviceName, serviceConfig = {}, options = {}) {
75
+ if (!serviceName) {
76
+ throw new Error('Service name is required for rollback.');
77
+ }
78
+
79
+ // Check if a supervisor daemon is running for this service via IPC
80
+ const ipcClient = new IPCClient(serviceName, options.ipcDir);
81
+ const isRemote = await ipcClient.isAlive();
82
+ if (isRemote) {
83
+ return ipcClient.send(
84
+ {
85
+ action: 'rollback',
86
+ options: { ...options, ...serviceConfig }
87
+ },
88
+ options.onProgress
89
+ );
90
+ }
91
+
92
+ this._ensurePrimitives(serviceName, options);
93
+
94
+ const config =
95
+ serviceConfig && (serviceConfig.command || serviceConfig.script)
96
+ ? serviceConfig
97
+ : readAppMetadata(serviceName) || serviceConfig || {};
98
+
99
+ await this.lock.acquire(serviceName);
100
+
101
+ try {
102
+ // Check if a canary generation is running: if so, abort canary and preserve active
103
+ const canary = this.generationManager.getCanary(serviceName);
104
+ if (canary) {
105
+ if (options.onProgress) {
106
+ options.onProgress({ state: 'ABORTING_CANARY', canaryGeneration: canary.id });
107
+ }
108
+ this.router.clearCanary(serviceName);
109
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 5000;
110
+ await this.drainManager.drain(canary, { timeout: drainTimeout, stopProcess: true });
111
+ this.generationManager.remove(canary, false);
112
+
113
+ const active = this.generationManager.getActive(serviceName);
114
+ return {
115
+ service: serviceName,
116
+ abortedCanary: canary.id,
117
+ activeGeneration: active ? active.id : null,
118
+ status: 'canary_aborted'
119
+ };
120
+ }
121
+
122
+ const current = this.generationManager.getActive(serviceName);
123
+ const previous = this.generationManager.getPrevious(serviceName);
124
+
125
+ if (!previous) {
126
+ throw new Error(`No previous generation available to roll back to for service "${serviceName}".`);
127
+ }
128
+
129
+ if (options.onProgress) {
130
+ options.onProgress({ state: 'CHECKING_PREVIOUS', previous: previous.id });
131
+ }
132
+
133
+ // Check if previous is alive
134
+ if (!this.processManager.isAlive(previous.pid)) {
135
+ throw new Error(
136
+ `Previous generation #${previous.id} (PID ${previous.pid}) is no longer running and cannot be rolled back to.`
137
+ );
138
+ }
139
+
140
+ // Ensure previous generation is still ready
141
+ const isReady = await this.readiness.ensure(previous, {
142
+ path: options.readyPath || config.deploy?.ready
143
+ });
144
+
145
+ if (!isReady) {
146
+ throw new Error(`Previous generation #${previous.id} failed readiness verification.`);
147
+ }
148
+
149
+ if (options.onProgress) {
150
+ options.onProgress({ state: 'SWITCHING', targetGeneration: previous.id });
151
+ }
152
+
153
+ // Switch router to previous generation
154
+ this.router.switchBackend(serviceName, previous);
155
+ this.generationManager.markActive(previous);
156
+
157
+ // Drain and stop the rolled-back current generation
158
+ if (current && current.id !== previous.id) {
159
+ if (options.onProgress) {
160
+ options.onProgress({ state: 'DRAINING_CURRENT', generation: current.id });
161
+ }
162
+ this.generationManager.markDraining(current);
163
+ const drainTimeout = options.drainTimeout || config.deploy?.drainTimeout || 10000;
164
+ await this.drainManager.drain(current, { timeout: drainTimeout, stopProcess: true });
165
+ this.generationManager.remove(current, false);
166
+ }
167
+
168
+ const result = {
169
+ service: serviceName,
170
+ rolledBackFrom: current?.id ?? null,
171
+ activeGeneration: previous.id,
172
+ status: 'rolled_back'
173
+ };
174
+
175
+ if (options.onProgress) {
176
+ options.onProgress({ state: 'COMPLETE', ...result });
177
+ }
178
+
179
+ return result;
180
+ } finally {
181
+ await this.lock.release(serviceName);
182
+ }
183
+ }
184
+ }
185
+
186
+ export const defaultRollbackManager = new RollbackManager();
package/src/index.js CHANGED
@@ -73,6 +73,7 @@ import {
73
73
  } from './unit.js';
74
74
  import {
75
75
  findProjectConfig,
76
+ formatDuration,
76
77
  formatMemoryBytes,
77
78
  getAppMetadataPath,
78
79
  getAppsDir,
@@ -94,6 +95,29 @@ import {
94
95
  validateDuration,
95
96
  validateMemorySize
96
97
  } from './utils.js';
98
+ import {
99
+ ProcessManager,
100
+ GenerationManager,
101
+ Router,
102
+ ReadinessChecker,
103
+ ReadinessTimeoutError,
104
+ ProcessStartupError,
105
+ PortMismatchError,
106
+ DrainManager,
107
+ DeploymentLock,
108
+ DeploymentLockError,
109
+ Supervisor,
110
+ IPCServer,
111
+ IPCClient,
112
+ findFreePort
113
+ } from './core/index.js';
114
+ import {
115
+ DeploymentManager,
116
+ DeploymentState,
117
+ defaultDeploymentManager,
118
+ RollbackManager,
119
+ defaultRollbackManager
120
+ } from './deploy/index.js';
97
121
 
98
122
  // Aliases for unified cross-platform API
99
123
  const install = (opts) => defaultManager.install(opts);
@@ -101,6 +125,10 @@ const uninstall = (name, opts) => defaultManager.uninstall(name, opts);
101
125
  const start = (name, opts) => defaultManager.start(name, opts);
102
126
  const stop = (name, opts) => defaultManager.stop(name, opts);
103
127
  const restart = (name, opts) => defaultManager.restart(name, opts);
128
+ const deploy = (name, opts) => defaultDeploymentManager.deploy(name, {}, opts);
129
+ const promote = (name, opts) => defaultDeploymentManager.promote(name, {}, opts);
130
+ const rollback = (name, opts) => defaultRollbackManager.rollback(name, {}, opts);
131
+ const generations = (name, opts) => new GenerationManager(opts).listGenerations(name);
104
132
  const status = (name, opts) => defaultManager.status(name, opts);
105
133
  const list = (opts) => defaultManager.list(opts);
106
134
  const inspect = (name, opts) => defaultManager.inspect(name, opts);
@@ -116,6 +144,10 @@ export {
116
144
  start,
117
145
  stop,
118
146
  restart,
147
+ deploy,
148
+ promote,
149
+ rollback,
150
+ generations,
119
151
  status,
120
152
  list,
121
153
  inspect,
@@ -125,6 +157,27 @@ export {
125
157
  platform,
126
158
  ServiceManager,
127
159
  defaultManager,
160
+ // Zero-Downtime Deployment & Orchestration
161
+ DeploymentManager,
162
+ DeploymentState,
163
+ defaultDeploymentManager,
164
+ RollbackManager,
165
+ defaultRollbackManager,
166
+ // Core Runtime Primitives
167
+ ProcessManager,
168
+ GenerationManager,
169
+ Router,
170
+ ReadinessChecker,
171
+ ReadinessTimeoutError,
172
+ ProcessStartupError,
173
+ PortMismatchError,
174
+ DrainManager,
175
+ DeploymentLock,
176
+ DeploymentLockError,
177
+ Supervisor,
178
+ IPCServer,
179
+ IPCClient,
180
+ findFreePort,
128
181
  // Platform Adapters & Factory
129
182
  ServiceAdapter,
130
183
  LinuxAdapter,
@@ -179,6 +232,7 @@ export {
179
232
  validateMemorySize,
180
233
  validateDuration,
181
234
  formatMemoryBytes,
235
+ formatDuration,
182
236
  // Runtime detection & resolution
183
237
  detectRuntime,
184
238
  resolveRuntimeConfig,