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.
- package/.agents/AGENTS.md +35 -0
- package/.agents/skills/multi-runtime-management/SKILL.md +80 -0
- package/.agents/skills/platform-service-adapters/SKILL.md +108 -0
- package/.agents/skills/unitup-development-and-qa/SKILL.md +88 -0
- package/.agents/skills/zero-downtime-deployment/SKILL.md +135 -0
- package/index.d.ts +136 -0
- package/package.json +1 -1
- package/src/cli.js +374 -2
- package/src/core/drain-manager.js +70 -0
- package/src/core/generation-manager.js +414 -0
- package/src/core/index.js +8 -0
- package/src/core/ipc.js +236 -0
- package/src/core/lock.js +133 -0
- package/src/core/process-manager.js +313 -0
- package/src/core/readiness.js +243 -0
- package/src/core/router.js +390 -0
- package/src/core/supervisor.js +222 -0
- package/src/deploy/deployment-manager.js +407 -0
- package/src/deploy/index.js +9 -0
- package/src/deploy/rollback-manager.js +186 -0
- package/src/index.js +54 -0
- package/src/platform/macos.js +4 -1
- package/src/platform/windows.js +4 -1
- package/src/service/config.js +8 -0
- package/src/service/normalize.js +30 -1
- package/src/unit.js +5 -1
- package/src/utils.js +80 -0
package/src/platform/macos.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
deleteAppMetadata,
|
|
14
14
|
formatRelativeTime,
|
|
15
15
|
getAppsDir,
|
|
16
|
+
getUnitupBinPath,
|
|
16
17
|
getUnitupDir,
|
|
17
18
|
readAppMetadata,
|
|
18
19
|
sanitizeServiceName,
|
|
@@ -175,7 +176,9 @@ export class MacOSAdapter extends ServiceAdapter {
|
|
|
175
176
|
const safeName = sanitizeServiceName(config.name);
|
|
176
177
|
const label = this.getLabel(safeName);
|
|
177
178
|
|
|
178
|
-
const programArguments =
|
|
179
|
+
const programArguments = config.deploy?.zeroDowntime
|
|
180
|
+
? [process.execPath, getUnitupBinPath(), 'supervisor', safeName]
|
|
181
|
+
: [config.command, ...(config.args || [])];
|
|
179
182
|
|
|
180
183
|
// Ensure log directory exists
|
|
181
184
|
const stdoutLog = config.logs?.stdout || path.join(getUnitupDir(), 'logs', `${safeName}.log`);
|
package/src/platform/windows.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
deleteAppMetadata,
|
|
15
15
|
formatRelativeTime,
|
|
16
16
|
getAppsDir,
|
|
17
|
+
getUnitupBinPath,
|
|
17
18
|
getUnitupDir,
|
|
18
19
|
readAppMetadata,
|
|
19
20
|
sanitizeServiceName,
|
|
@@ -75,7 +76,9 @@ export class WindowsAdapter extends ServiceAdapter {
|
|
|
75
76
|
const safeName = sanitizeServiceName(config.name);
|
|
76
77
|
const serviceName = this.getServiceName(safeName);
|
|
77
78
|
const nodeExec = process.execPath;
|
|
78
|
-
const
|
|
79
|
+
const hostScript = config.deploy?.zeroDowntime ? getUnitupBinPath() : WINDOWS_HOST_PATH;
|
|
80
|
+
const hostArg = config.deploy?.zeroDowntime ? `supervisor "${safeName}"` : `"${safeName}"`;
|
|
81
|
+
const binPath = `"${nodeExec}" "${hostScript}" ${hostArg}`;
|
|
79
82
|
const startType = config.autostart !== false ? 'auto' : 'demand';
|
|
80
83
|
|
|
81
84
|
return {
|
package/src/service/config.js
CHANGED
|
@@ -48,6 +48,14 @@ export function mergeConfigWithOptions(opts = {}) {
|
|
|
48
48
|
memoryHigh: opts.memoryHigh || projectCfg?.resources?.memoryHigh || projectCfg?.memoryHigh || '',
|
|
49
49
|
memoryMax: opts.memoryMax || projectCfg?.resources?.memoryMax || projectCfg?.memoryMax || '',
|
|
50
50
|
memorySwapMax: opts.memorySwapMax || projectCfg?.resources?.memorySwapMax || projectCfg?.memorySwapMax || '',
|
|
51
|
+
port: opts.port !== undefined ? opts.port : projectCfg.port,
|
|
52
|
+
deploy: {
|
|
53
|
+
...(projectCfg.deploy && typeof projectCfg.deploy === 'object' ? projectCfg.deploy : {}),
|
|
54
|
+
...(opts.deploy && typeof opts.deploy === 'object' ? opts.deploy : {}),
|
|
55
|
+
...(opts.zeroDowntime !== undefined ? { zeroDowntime: !!opts.zeroDowntime } : {}),
|
|
56
|
+
...(opts.ready !== undefined ? { ready: opts.ready } : {}),
|
|
57
|
+
...(opts.drain !== undefined ? { drain: opts.drain } : {})
|
|
58
|
+
},
|
|
51
59
|
...opts,
|
|
52
60
|
env: { ...(projectCfg.env || {}), ...(opts.env || {}) },
|
|
53
61
|
cwd: targetCwd
|
package/src/service/normalize.js
CHANGED
|
@@ -2,7 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ExecutableNotFoundError, InvalidServiceConfigError } from '../errors.js';
|
|
4
4
|
import { detectRuntime, resolveRuntimeConfig } from '../runtimes/index.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getUnitupDir,
|
|
7
|
+
parseDurationMs,
|
|
8
|
+
resolveEffectiveMemoryLimits,
|
|
9
|
+
resolveWorkingDirectory,
|
|
10
|
+
sanitizeServiceName
|
|
11
|
+
} from '../utils.js';
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Resolves a binary or command name to an absolute executable path.
|
|
@@ -187,6 +193,27 @@ export async function normalizeServiceConfig(rawOpts = {}) {
|
|
|
187
193
|
// Memory and resource limits
|
|
188
194
|
const resources = resolveEffectiveMemoryLimits(rawOpts);
|
|
189
195
|
|
|
196
|
+
// Port and Deploy normalization
|
|
197
|
+
const port =
|
|
198
|
+
rawOpts.port !== undefined && rawOpts.port !== null && rawOpts.port !== '' ? Number(rawOpts.port) : undefined;
|
|
199
|
+
|
|
200
|
+
const rawDeploy = rawOpts.deploy && typeof rawOpts.deploy === 'object' ? rawOpts.deploy : {};
|
|
201
|
+
const zeroDowntime = !!(rawOpts.deploy === true || rawDeploy.zeroDowntime || rawOpts.zeroDowntime);
|
|
202
|
+
const readyPath =
|
|
203
|
+
typeof rawDeploy.ready === 'string'
|
|
204
|
+
? rawDeploy.ready
|
|
205
|
+
: typeof rawOpts.ready === 'string'
|
|
206
|
+
? rawOpts.ready
|
|
207
|
+
: undefined;
|
|
208
|
+
const drainRaw = rawDeploy.drain !== undefined ? rawDeploy.drain : rawOpts.drain;
|
|
209
|
+
const drainTimeout = drainRaw !== undefined ? parseDurationMs(drainRaw) : 10000;
|
|
210
|
+
const deploy = {
|
|
211
|
+
zeroDowntime,
|
|
212
|
+
ready: readyPath,
|
|
213
|
+
drain: drainRaw,
|
|
214
|
+
drainTimeout
|
|
215
|
+
};
|
|
216
|
+
|
|
190
217
|
return {
|
|
191
218
|
name,
|
|
192
219
|
displayName,
|
|
@@ -196,6 +223,8 @@ export async function normalizeServiceConfig(rawOpts = {}) {
|
|
|
196
223
|
args,
|
|
197
224
|
script: rawOpts.script ? path.resolve(cwd, rawOpts.script) : undefined,
|
|
198
225
|
cwd,
|
|
226
|
+
port,
|
|
227
|
+
deploy,
|
|
199
228
|
env,
|
|
200
229
|
envFile: rawOpts.envFile ? path.resolve(cwd, rawOpts.envFile) : undefined,
|
|
201
230
|
autostart,
|
package/src/unit.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getServiceNameFromUnit,
|
|
10
10
|
getTimerFilename,
|
|
11
11
|
getUnitFilename,
|
|
12
|
+
getUnitupBinPath,
|
|
12
13
|
resolveAbsolutePath,
|
|
13
14
|
resolveEffectiveMemoryLimits,
|
|
14
15
|
resolveWorkingDirectory,
|
|
@@ -73,7 +74,10 @@ export function generateUnitContent(opts) {
|
|
|
73
74
|
let commandExec = '';
|
|
74
75
|
let execArgs = [];
|
|
75
76
|
|
|
76
|
-
if (opts.
|
|
77
|
+
if (opts.deploy?.zeroDowntime) {
|
|
78
|
+
commandExec = process.execPath;
|
|
79
|
+
execArgs = [getUnitupBinPath(), 'supervisor', safeName];
|
|
80
|
+
} else if (opts.command) {
|
|
77
81
|
commandExec = resolveAbsolutePath(opts.command);
|
|
78
82
|
execArgs = Array.isArray(opts.args) ? [...opts.args] : [];
|
|
79
83
|
} else if (opts.script) {
|
package/src/utils.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the absolute path to the unitup CLI binary.
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function getUnitupBinPath() {
|
|
11
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
return path.resolve(currentDir, '../bin/unitup.js');
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
/**
|
|
6
16
|
* Returns the base unitup config directory (~/.config/unitup).
|
|
@@ -170,6 +180,8 @@ export function saveAppMetadata(meta) {
|
|
|
170
180
|
args,
|
|
171
181
|
cwd,
|
|
172
182
|
group: meta.group || 'default',
|
|
183
|
+
...(meta.port !== undefined ? { port: meta.port } : {}),
|
|
184
|
+
...(meta.deploy ? { deploy: meta.deploy } : {}),
|
|
173
185
|
// Optional resources section
|
|
174
186
|
...(Object.keys(resources).length > 0 ? { resources } : {}),
|
|
175
187
|
// Backward compatibility fields
|
|
@@ -415,6 +427,32 @@ export function formatRelativeTime(timestamp) {
|
|
|
415
427
|
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
416
428
|
}
|
|
417
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Formats duration in milliseconds into a concise human-readable string (e.g. "23s", "2 mins", "3 hours", "2 days").
|
|
432
|
+
*
|
|
433
|
+
* @param {number} ms
|
|
434
|
+
* @returns {string}
|
|
435
|
+
*/
|
|
436
|
+
export function formatDuration(ms) {
|
|
437
|
+
if (typeof ms !== 'number' || isNaN(ms) || ms < 0) {
|
|
438
|
+
return '-';
|
|
439
|
+
}
|
|
440
|
+
const seconds = Math.floor(ms / 1000);
|
|
441
|
+
if (seconds < 60) {
|
|
442
|
+
return `${seconds}s`;
|
|
443
|
+
}
|
|
444
|
+
const minutes = Math.floor(seconds / 60);
|
|
445
|
+
if (minutes < 60) {
|
|
446
|
+
return `${minutes} min${minutes === 1 ? '' : 's'}`;
|
|
447
|
+
}
|
|
448
|
+
const hours = Math.floor(minutes / 60);
|
|
449
|
+
if (hours < 24) {
|
|
450
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
451
|
+
}
|
|
452
|
+
const days = Math.floor(hours / 24);
|
|
453
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
418
456
|
/**
|
|
419
457
|
* Formats data rows into a text table.
|
|
420
458
|
*
|
|
@@ -603,6 +641,48 @@ export function validateDuration(val, paramName = 'Duration') {
|
|
|
603
641
|
return str;
|
|
604
642
|
}
|
|
605
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Parses a duration string (e.g. '30s', '500ms', '1m') into milliseconds.
|
|
646
|
+
* @param {string|number} val
|
|
647
|
+
* @param {number} [defaultMs=10000]
|
|
648
|
+
* @returns {number}
|
|
649
|
+
*/
|
|
650
|
+
export function parseDurationMs(val, defaultMs = 10000) {
|
|
651
|
+
if (typeof val === 'number') return val;
|
|
652
|
+
if (!val || typeof val !== 'string') return defaultMs;
|
|
653
|
+
const str = val.trim().toLowerCase();
|
|
654
|
+
const match = str.match(/^(\d+(?:\.\d+)?)\s*(ms|msec|s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|day|days?)?$/);
|
|
655
|
+
if (!match) return defaultMs;
|
|
656
|
+
const num = Number.parseFloat(match[1]);
|
|
657
|
+
const unit = match[2] || 's';
|
|
658
|
+
switch (unit) {
|
|
659
|
+
case 'ms':
|
|
660
|
+
case 'msec':
|
|
661
|
+
return num;
|
|
662
|
+
case 's':
|
|
663
|
+
case 'sec':
|
|
664
|
+
case 'second':
|
|
665
|
+
case 'seconds':
|
|
666
|
+
return num * 1000;
|
|
667
|
+
case 'm':
|
|
668
|
+
case 'min':
|
|
669
|
+
case 'minute':
|
|
670
|
+
case 'minutes':
|
|
671
|
+
return num * 60 * 1000;
|
|
672
|
+
case 'h':
|
|
673
|
+
case 'hr':
|
|
674
|
+
case 'hour':
|
|
675
|
+
case 'hours':
|
|
676
|
+
return num * 60 * 60 * 1000;
|
|
677
|
+
case 'd':
|
|
678
|
+
case 'day':
|
|
679
|
+
case 'days':
|
|
680
|
+
return num * 24 * 60 * 60 * 1000;
|
|
681
|
+
default:
|
|
682
|
+
return num * 1000;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
606
686
|
/**
|
|
607
687
|
* Formats a future date/timestamp into relative human readable string (e.g. "in 12 minutes", "tomorrow").
|
|
608
688
|
* @param {string|number|Date} dateVal
|