unitup 0.1.5 → 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/CHANGELOG.md +5 -3
- package/README.md +89 -8
- package/biome.json +0 -1
- package/docs/index.html +188 -234
- package/index.d.ts +467 -440
- package/package.json +9 -3
- package/src/cli.js +561 -103
- 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/doctor.js +123 -56
- package/src/errors.js +131 -0
- package/src/index.js +186 -59
- package/src/logs.js +148 -0
- package/src/platform/base.js +211 -0
- package/src/platform/index.js +43 -0
- package/src/platform/linux.js +155 -0
- package/src/platform/macos.js +726 -0
- package/src/platform/windows-host.js +246 -0
- package/src/platform/windows.js +498 -0
- package/src/runtimes/bun.js +1 -1
- package/src/runtimes/common.js +24 -6
- package/src/runtimes/deno.js +1 -1
- package/src/runtimes/elixir.js +1 -1
- package/src/runtimes/go.js +1 -1
- package/src/runtimes/index.js +7 -7
- package/src/runtimes/native.js +2 -4
- package/src/runtimes/node.js +1 -1
- package/src/runtimes/php.js +1 -1
- package/src/runtimes/python.js +1 -1
- package/src/runtimes/ruby.js +1 -1
- package/src/runtimes/shell.js +1 -1
- package/src/schedule.js +31 -24
- package/src/service/config.js +63 -0
- package/src/service/manager.js +233 -0
- package/src/service/normalize.js +245 -0
- package/src/systemd.js +84 -38
- package/src/unit.js +17 -19
- package/src/utils.js +112 -18
- package/tsconfig.json +1 -1
- package/types-test/index.ts +158 -0
package/src/systemd.js
CHANGED
|
@@ -3,22 +3,24 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import {
|
|
6
|
-
sanitizeServiceName,
|
|
7
|
-
getUnitFilename,
|
|
8
|
-
getTimerFilename,
|
|
9
|
-
formatRelativeTime,
|
|
10
|
-
readAppMetadata,
|
|
11
|
-
readScheduleMetadata
|
|
12
|
-
} from './utils.js';
|
|
13
|
-
import {
|
|
14
|
-
getUserUnitDir,
|
|
15
|
-
getUnitPath,
|
|
16
|
-
unitFileExists,
|
|
17
6
|
deleteUnitFile,
|
|
7
|
+
getUnitPath,
|
|
8
|
+
getUserUnitDir,
|
|
18
9
|
listUnitFiles,
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
parseUnitContent,
|
|
11
|
+
unitFileExists,
|
|
12
|
+
writeUnitFile
|
|
21
13
|
} from './unit.js';
|
|
14
|
+
import {
|
|
15
|
+
findProjectConfig,
|
|
16
|
+
formatRelativeTime,
|
|
17
|
+
getTimerFilename,
|
|
18
|
+
getUnitFilename,
|
|
19
|
+
readAppMetadata,
|
|
20
|
+
readProjectConfig,
|
|
21
|
+
readScheduleMetadata,
|
|
22
|
+
sanitizeServiceName
|
|
23
|
+
} from './utils.js';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Default runner for executing commands via child_process.execFile.
|
|
@@ -281,17 +283,44 @@ export async function resetFailed() {
|
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
export async function addService(opts) {
|
|
284
|
-
const
|
|
286
|
+
const targetCwd = opts.cwd ? path.resolve(process.cwd(), opts.cwd) : process.cwd();
|
|
287
|
+
const configPath = opts.config ? path.resolve(targetCwd, opts.config) : findProjectConfig(targetCwd);
|
|
288
|
+
const projectCfg = configPath ? readProjectConfig(configPath) : null;
|
|
289
|
+
|
|
290
|
+
let mergedOpts = { ...opts };
|
|
291
|
+
if (projectCfg) {
|
|
292
|
+
mergedOpts = {
|
|
293
|
+
name: opts.name || projectCfg.name,
|
|
294
|
+
group: opts.group || projectCfg.group || 'default',
|
|
295
|
+
script: opts.script || projectCfg.script,
|
|
296
|
+
command: opts.command || projectCfg.command,
|
|
297
|
+
runtime: opts.runtime || projectCfg.runtime,
|
|
298
|
+
runtimeArgs: opts.runtimeArgs && opts.runtimeArgs.length > 0 ? opts.runtimeArgs : projectCfg.runtimeArgs || [],
|
|
299
|
+
args: opts.args && opts.args.length > 0 ? opts.args : projectCfg.args || [],
|
|
300
|
+
envFile: opts.envFile || projectCfg.envFile,
|
|
301
|
+
restart: opts.restart && opts.restart !== 'on-failure' ? opts.restart : projectCfg.restart || 'on-failure',
|
|
302
|
+
memoryHigh: opts.memoryHigh || projectCfg?.resources?.memoryHigh || projectCfg?.memoryHigh || '',
|
|
303
|
+
memoryMax: opts.memoryMax || projectCfg?.resources?.memoryMax || projectCfg?.memoryMax || '',
|
|
304
|
+
memorySwapMax: opts.memorySwapMax || projectCfg?.resources?.memorySwapMax || projectCfg?.memorySwapMax || '',
|
|
305
|
+
...opts,
|
|
306
|
+
env: { ...(projectCfg.env || {}), ...(opts.env || {}) },
|
|
307
|
+
cwd: targetCwd
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const safeName = sanitizeServiceName(mergedOpts.name);
|
|
312
|
+
let wasActive = false;
|
|
285
313
|
|
|
286
314
|
if (unitFileExists(safeName)) {
|
|
287
315
|
try {
|
|
288
316
|
const show = await getServiceShow(safeName);
|
|
289
317
|
if (show.ActiveState === 'active') {
|
|
290
|
-
|
|
318
|
+
wasActive = true;
|
|
319
|
+
if (!mergedOpts.force) {
|
|
291
320
|
throw new Error(
|
|
292
321
|
`Service "${safeName}" is currently running.\n` +
|
|
293
|
-
|
|
294
|
-
|
|
322
|
+
`Use --force (-f) to overwrite running services, or stop it first:\n` +
|
|
323
|
+
` unitup stop ${safeName}`
|
|
295
324
|
);
|
|
296
325
|
}
|
|
297
326
|
}
|
|
@@ -302,20 +331,22 @@ export async function addService(opts) {
|
|
|
302
331
|
}
|
|
303
332
|
}
|
|
304
333
|
|
|
305
|
-
const runtimeConfig = await resolveRuntimeConfig({ ...
|
|
334
|
+
const runtimeConfig = await resolveRuntimeConfig({ ...mergedOpts, name: safeName });
|
|
306
335
|
|
|
307
336
|
const { path: unitPath } = writeUnitFile({
|
|
308
|
-
...
|
|
337
|
+
...mergedOpts,
|
|
309
338
|
name: safeName,
|
|
310
339
|
runtime: runtimeConfig.runtime,
|
|
311
340
|
command: runtimeConfig.command,
|
|
312
341
|
args: runtimeConfig.args,
|
|
313
|
-
cwd:
|
|
342
|
+
cwd: mergedOpts.cwd
|
|
314
343
|
});
|
|
315
344
|
|
|
316
345
|
await daemonReload();
|
|
317
346
|
|
|
318
|
-
if (
|
|
347
|
+
if (wasActive && mergedOpts.force) {
|
|
348
|
+
await restartService(safeName);
|
|
349
|
+
} else if (mergedOpts.start) {
|
|
319
350
|
await startService(safeName, true);
|
|
320
351
|
}
|
|
321
352
|
|
|
@@ -423,8 +454,8 @@ export async function removeService(name, opts = {}) {
|
|
|
423
454
|
if (!force) {
|
|
424
455
|
throw new Error(
|
|
425
456
|
`Service "${safeName}" is currently running.\n` +
|
|
426
|
-
|
|
427
|
-
|
|
457
|
+
`Use --force (-f) to remove running services, or stop it first:\n` +
|
|
458
|
+
` unitup stop ${safeName}`
|
|
428
459
|
);
|
|
429
460
|
}
|
|
430
461
|
}
|
|
@@ -514,9 +545,7 @@ export async function getServiceStatus(name) {
|
|
|
514
545
|
const pid = show.MainPID && show.MainPID !== '0' ? show.MainPID : '-';
|
|
515
546
|
const restarts = show.NRestarts ?? '0';
|
|
516
547
|
const startedRaw = show.ActiveEnterTimestamp;
|
|
517
|
-
const started = startedRaw && startedRaw !== '0' && startedRaw !== 'n/a'
|
|
518
|
-
? formatRelativeTime(startedRaw)
|
|
519
|
-
: 'never';
|
|
548
|
+
const started = startedRaw && startedRaw !== '0' && startedRaw !== 'n/a' ? formatRelativeTime(startedRaw) : 'never';
|
|
520
549
|
|
|
521
550
|
const meta = readAppMetadata(safeName) || readScheduleMetadata(safeName);
|
|
522
551
|
const command = meta?.command || parsed.command || meta?.node || process.execPath;
|
|
@@ -589,7 +618,7 @@ export async function listServices(filterOpts = {}) {
|
|
|
589
618
|
} else {
|
|
590
619
|
const cmdBase = path.basename(meta.command);
|
|
591
620
|
const argsStr = (meta.args || [])
|
|
592
|
-
.map(a => (a.startsWith('/') || a.startsWith('./') ? path.basename(a) : a))
|
|
621
|
+
.map((a) => (a.startsWith('/') || a.startsWith('./') ? path.basename(a) : a))
|
|
593
622
|
.join(' ');
|
|
594
623
|
commandSummary = (cmdBase + (argsStr ? ' ' + argsStr : '')).trim();
|
|
595
624
|
}
|
|
@@ -641,9 +670,8 @@ export async function listServices(filterOpts = {}) {
|
|
|
641
670
|
const pid = show.MainPID && show.MainPID !== '0' ? show.MainPID : '-';
|
|
642
671
|
const restarts = show.NRestarts ?? '0';
|
|
643
672
|
const startedRaw = show.ActiveEnterTimestamp;
|
|
644
|
-
const uptime =
|
|
645
|
-
? formatRelativeTime(startedRaw)
|
|
646
|
-
: 'never';
|
|
673
|
+
const uptime =
|
|
674
|
+
startedRaw && startedRaw !== '0' && startedRaw !== 'n/a' ? formatRelativeTime(startedRaw) : 'never';
|
|
647
675
|
|
|
648
676
|
result.push({
|
|
649
677
|
name: unit.name,
|
|
@@ -757,8 +785,13 @@ export async function runJournalctlLogs(name, opts = {}) {
|
|
|
757
785
|
|
|
758
786
|
if (opts.diskUsage) {
|
|
759
787
|
const res = await runCommand('journalctl', ['--disk-usage']);
|
|
760
|
-
if (
|
|
761
|
-
|
|
788
|
+
if (
|
|
789
|
+
res.code !== 0 &&
|
|
790
|
+
(res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root'))
|
|
791
|
+
) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
'Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.'
|
|
794
|
+
);
|
|
762
795
|
}
|
|
763
796
|
return res.stdout || res.stderr;
|
|
764
797
|
}
|
|
@@ -920,7 +953,7 @@ export async function executeJournalctlMaintenance(action, opts = {}) {
|
|
|
920
953
|
}
|
|
921
954
|
args.push(`--vacuum-time=${timeStr}`);
|
|
922
955
|
} else if (opts.files) {
|
|
923
|
-
const filesNum = parseInt(opts.files, 10);
|
|
956
|
+
const filesNum = Number.parseInt(opts.files, 10);
|
|
924
957
|
if (Number.isNaN(filesNum) || filesNum <= 0) {
|
|
925
958
|
throw new Error(`Invalid vacuum files count: "${opts.files}". Must be a positive integer.`);
|
|
926
959
|
}
|
|
@@ -937,8 +970,15 @@ export async function executeJournalctlMaintenance(action, opts = {}) {
|
|
|
937
970
|
}
|
|
938
971
|
|
|
939
972
|
const res = await runCommand('journalctl', args);
|
|
940
|
-
if (
|
|
941
|
-
|
|
973
|
+
if (
|
|
974
|
+
res.code !== 0 &&
|
|
975
|
+
(res.stderr.toLowerCase().includes('permission') ||
|
|
976
|
+
res.stderr.toLowerCase().includes('root') ||
|
|
977
|
+
res.stderr.toLowerCase().includes('access denied'))
|
|
978
|
+
) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
'Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.'
|
|
981
|
+
);
|
|
942
982
|
}
|
|
943
983
|
|
|
944
984
|
return (res.stdout || res.stderr || 'Journal maintenance completed successfully.').trim();
|
|
@@ -978,9 +1018,15 @@ export async function getServiceMemoryUsage(name) {
|
|
|
978
1018
|
const effective = resolveEffectiveMemoryLimits(meta || {});
|
|
979
1019
|
|
|
980
1020
|
const memoryCurrentBytes = Number(props.MemoryCurrent);
|
|
981
|
-
const rawBytes =
|
|
1021
|
+
const rawBytes =
|
|
1022
|
+
!isNaN(memoryCurrentBytes) && memoryCurrentBytes > 0 && memoryCurrentBytes < 1e15 ? memoryCurrentBytes : 0;
|
|
982
1023
|
|
|
983
|
-
const status =
|
|
1024
|
+
const status =
|
|
1025
|
+
props.ActiveState === 'active'
|
|
1026
|
+
? props.SubState === 'running'
|
|
1027
|
+
? 'running'
|
|
1028
|
+
: props.SubState || 'active'
|
|
1029
|
+
: props.ActiveState || 'stopped';
|
|
984
1030
|
|
|
985
1031
|
return {
|
|
986
1032
|
name: safeName,
|
|
@@ -1060,7 +1106,7 @@ export async function getAllServicesMemoryUsage(opts = {}) {
|
|
|
1060
1106
|
}
|
|
1061
1107
|
}
|
|
1062
1108
|
|
|
1063
|
-
const runningCount = items.filter(i => i.status === 'running' || i.status === 'active').length;
|
|
1109
|
+
const runningCount = items.filter((i) => i.status === 'running' || i.status === 'active').length;
|
|
1064
1110
|
|
|
1065
1111
|
return {
|
|
1066
1112
|
items,
|
package/src/unit.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
getTimerFilename,
|
|
8
|
-
getServiceNameFromUnit,
|
|
9
|
-
formatSystemdEnv,
|
|
5
|
+
deleteAppMetadata,
|
|
6
|
+
deleteScheduleMetadata,
|
|
10
7
|
escapeExecArg,
|
|
8
|
+
formatSystemdEnv,
|
|
9
|
+
getServiceNameFromUnit,
|
|
10
|
+
getTimerFilename,
|
|
11
|
+
getUnitFilename,
|
|
12
|
+
getUnitupBinPath,
|
|
11
13
|
resolveAbsolutePath,
|
|
14
|
+
resolveEffectiveMemoryLimits,
|
|
12
15
|
resolveWorkingDirectory,
|
|
16
|
+
sanitizeServiceName,
|
|
13
17
|
saveAppMetadata,
|
|
14
|
-
deleteAppMetadata,
|
|
15
18
|
saveScheduleMetadata,
|
|
16
|
-
|
|
17
|
-
validateDuration,
|
|
18
|
-
resolveEffectiveMemoryLimits
|
|
19
|
+
validateDuration
|
|
19
20
|
} from './utils.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -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) {
|
|
@@ -241,7 +245,7 @@ export function parseUnitContent(content) {
|
|
|
241
245
|
} else if (trimmed.startsWith('ExecStart=')) {
|
|
242
246
|
const execVal = trimmed.slice(10).trim();
|
|
243
247
|
const parts = execVal.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
244
|
-
const cleanParts = parts.map(p => p.replace(/^"|"$/g, ''));
|
|
248
|
+
const cleanParts = parts.map((p) => p.replace(/^"|"$/g, '').replace(/\\"/g, '"').replace(/\\\\/g, '\\'));
|
|
245
249
|
if (cleanParts.length > 0) {
|
|
246
250
|
result.command = cleanParts[0];
|
|
247
251
|
result.args = cleanParts.slice(1);
|
|
@@ -368,12 +372,7 @@ export function generateScheduleServiceContent(opts) {
|
|
|
368
372
|
export function generateTimerContent(opts) {
|
|
369
373
|
const safeName = sanitizeServiceName(opts.name);
|
|
370
374
|
|
|
371
|
-
const timerLines = [
|
|
372
|
-
'[Unit]',
|
|
373
|
-
`Description=unitup timer: ${safeName}`,
|
|
374
|
-
'',
|
|
375
|
-
'[Timer]'
|
|
376
|
-
];
|
|
375
|
+
const timerLines = ['[Unit]', `Description=unitup timer: ${safeName}`, '', '[Timer]'];
|
|
377
376
|
|
|
378
377
|
if (opts.every) {
|
|
379
378
|
const validEvery = validateDuration(opts.every, '--every');
|
|
@@ -482,4 +481,3 @@ export function deleteScheduleUnitFiles(name) {
|
|
|
482
481
|
}
|
|
483
482
|
return removed;
|
|
484
483
|
}
|
|
485
|
-
|
package/src/utils.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import os from 'node:os';
|
|
3
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
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).
|
|
@@ -75,7 +85,14 @@ export function validateMemorySize(val, paramName = 'Memory limit') {
|
|
|
75
85
|
* @returns {string} Formatted memory string e.g. "284 MB", "infinity", "unavailable"
|
|
76
86
|
*/
|
|
77
87
|
export function formatMemoryBytes(bytes) {
|
|
78
|
-
if (
|
|
88
|
+
if (
|
|
89
|
+
bytes === null ||
|
|
90
|
+
bytes === undefined ||
|
|
91
|
+
bytes === '' ||
|
|
92
|
+
bytes === 'unavailable' ||
|
|
93
|
+
bytes === '[unavailable]' ||
|
|
94
|
+
bytes === '-'
|
|
95
|
+
) {
|
|
79
96
|
return 'unavailable';
|
|
80
97
|
}
|
|
81
98
|
|
|
@@ -137,10 +154,16 @@ export function saveAppMetadata(meta) {
|
|
|
137
154
|
}
|
|
138
155
|
const filePath = getAppMetadataPath(safeName);
|
|
139
156
|
|
|
140
|
-
const command = meta.command
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
const command = meta.command
|
|
158
|
+
? resolveAbsolutePath(meta.command)
|
|
159
|
+
: meta.node
|
|
160
|
+
? resolveAbsolutePath(meta.node)
|
|
161
|
+
: process.execPath;
|
|
162
|
+
const rawArgs = Array.isArray(meta.args) ? meta.args : meta.script ? [meta.script] : [];
|
|
163
|
+
const args = rawArgs.map((a) =>
|
|
164
|
+
a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/') ? resolveAbsolutePath(a) : a
|
|
165
|
+
);
|
|
166
|
+
const scriptPath = meta.script ? resolveAbsolutePath(meta.script) : args[0] ? resolveAbsolutePath(args[0]) : command;
|
|
144
167
|
const cwd = resolveWorkingDirectory(meta);
|
|
145
168
|
|
|
146
169
|
// Extract memory resources if present
|
|
@@ -157,6 +180,8 @@ export function saveAppMetadata(meta) {
|
|
|
157
180
|
args,
|
|
158
181
|
cwd,
|
|
159
182
|
group: meta.group || 'default',
|
|
183
|
+
...(meta.port !== undefined ? { port: meta.port } : {}),
|
|
184
|
+
...(meta.deploy ? { deploy: meta.deploy } : {}),
|
|
160
185
|
// Optional resources section
|
|
161
186
|
...(Object.keys(resources).length > 0 ? { resources } : {}),
|
|
162
187
|
// Backward compatibility fields
|
|
@@ -402,6 +427,32 @@ export function formatRelativeTime(timestamp) {
|
|
|
402
427
|
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
403
428
|
}
|
|
404
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
|
+
|
|
405
456
|
/**
|
|
406
457
|
* Formats data rows into a text table.
|
|
407
458
|
*
|
|
@@ -414,7 +465,7 @@ export function formatTable(data, columns) {
|
|
|
414
465
|
return 'No services found.';
|
|
415
466
|
}
|
|
416
467
|
|
|
417
|
-
const widths = columns.map(col => col.label.length);
|
|
468
|
+
const widths = columns.map((col) => col.label.length);
|
|
418
469
|
|
|
419
470
|
for (const row of data) {
|
|
420
471
|
columns.forEach((col, idx) => {
|
|
@@ -429,9 +480,7 @@ export function formatTable(data, columns) {
|
|
|
429
480
|
const lines = [headerLine];
|
|
430
481
|
|
|
431
482
|
for (const row of data) {
|
|
432
|
-
const line = columns
|
|
433
|
-
.map((col, idx) => String(row[col.key] ?? '').padEnd(widths[idx]))
|
|
434
|
-
.join(' ');
|
|
483
|
+
const line = columns.map((col, idx) => String(row[col.key] ?? '').padEnd(widths[idx])).join(' ');
|
|
435
484
|
lines.push(line);
|
|
436
485
|
}
|
|
437
486
|
|
|
@@ -471,7 +520,9 @@ export function saveScheduleMetadata(meta) {
|
|
|
471
520
|
|
|
472
521
|
const command = meta.command ? resolveAbsolutePath(meta.command) : process.execPath;
|
|
473
522
|
const rawArgs = Array.isArray(meta.args) ? meta.args : [];
|
|
474
|
-
const args = rawArgs.map(
|
|
523
|
+
const args = rawArgs.map((a) =>
|
|
524
|
+
a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/') ? resolveAbsolutePath(a) : a
|
|
525
|
+
);
|
|
475
526
|
|
|
476
527
|
const resources = meta.resources || {};
|
|
477
528
|
if (meta.memoryHigh) resources.memoryHigh = validateMemorySize(meta.memoryHigh, 'MemoryHigh');
|
|
@@ -578,7 +629,8 @@ export function validateDuration(val, paramName = 'Duration') {
|
|
|
578
629
|
}
|
|
579
630
|
|
|
580
631
|
const tokens = str.split(/\s+/);
|
|
581
|
-
const tokenRegex =
|
|
632
|
+
const tokenRegex =
|
|
633
|
+
/^(\d+(?:\.\d+)?)(us|usec|ms|msec|s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|day|days?|w|week|weeks?|mth|months?|y|year|years?)$/i;
|
|
582
634
|
|
|
583
635
|
for (const token of tokens) {
|
|
584
636
|
if (!tokenRegex.test(token)) {
|
|
@@ -589,6 +641,48 @@ export function validateDuration(val, paramName = 'Duration') {
|
|
|
589
641
|
return str;
|
|
590
642
|
}
|
|
591
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
|
+
|
|
592
686
|
/**
|
|
593
687
|
* Formats a future date/timestamp into relative human readable string (e.g. "in 12 minutes", "tomorrow").
|
|
594
688
|
* @param {string|number|Date} dateVal
|
|
@@ -598,7 +692,9 @@ export function formatFutureTime(dateVal) {
|
|
|
598
692
|
if (!dateVal || dateVal === 'n/a' || dateVal === '0' || dateVal === 'infinity') {
|
|
599
693
|
return 'n/a';
|
|
600
694
|
}
|
|
601
|
-
const date = new Date(
|
|
695
|
+
const date = new Date(
|
|
696
|
+
typeof dateVal === 'number' && dateVal > 1e12 ? dateVal : typeof dateVal === 'number' ? dateVal / 1000 : dateVal
|
|
697
|
+
);
|
|
602
698
|
if (isNaN(date.getTime())) {
|
|
603
699
|
return String(dateVal);
|
|
604
700
|
}
|
|
@@ -691,7 +787,8 @@ export function resolveEffectiveMemoryLimits(opts = {}) {
|
|
|
691
787
|
}
|
|
692
788
|
|
|
693
789
|
if (defaultMem) {
|
|
694
|
-
const sizeStr =
|
|
790
|
+
const sizeStr =
|
|
791
|
+
typeof defaultMem === 'boolean' || defaultMem === 'true' || defaultMem === '' ? '1G' : String(defaultMem);
|
|
695
792
|
const validSize = validateMemorySize(sizeStr, 'Default memory limit');
|
|
696
793
|
return { memoryMax: validSize };
|
|
697
794
|
}
|
|
@@ -757,6 +854,3 @@ export function saveProjectConfig(dirPathOrFile = process.cwd(), config = {}) {
|
|
|
757
854
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
758
855
|
return filePath;
|
|
759
856
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
package/tsconfig.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppMetadata,
|
|
3
|
+
CreateServiceOptions,
|
|
4
|
+
DoctorInfo,
|
|
5
|
+
InstallServiceOptions,
|
|
6
|
+
ListServiceItem,
|
|
7
|
+
LogOptions,
|
|
8
|
+
NodeDiagnostics,
|
|
9
|
+
NormalizedServiceConfig,
|
|
10
|
+
PlatformCapabilities,
|
|
11
|
+
ServiceFailure,
|
|
12
|
+
ServiceStatus
|
|
13
|
+
} from '../index.d.ts';
|
|
14
|
+
|
|
15
|
+
// Compile-time type assertion checks for all exported interfaces
|
|
16
|
+
function typeCheckTest() {
|
|
17
|
+
const options: CreateServiceOptions = {
|
|
18
|
+
name: 'test-app',
|
|
19
|
+
script: './server.js',
|
|
20
|
+
runtime: 'python',
|
|
21
|
+
runtimeArgs: ['-u'],
|
|
22
|
+
command: '/usr/bin/python3',
|
|
23
|
+
cwd: '/path/to/cwd',
|
|
24
|
+
nodePath: '/usr/bin/node',
|
|
25
|
+
env: { NODE_ENV: 'production' },
|
|
26
|
+
envFile: '.env',
|
|
27
|
+
restart: 'always',
|
|
28
|
+
args: ['--port', '3000'],
|
|
29
|
+
start: true,
|
|
30
|
+
memoryMax: '512M',
|
|
31
|
+
memoryHigh: '256M'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const installOpts: InstallServiceOptions = {
|
|
35
|
+
...options,
|
|
36
|
+
system: false,
|
|
37
|
+
force: true
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const status: ServiceStatus = {
|
|
41
|
+
name: 'test-app',
|
|
42
|
+
installed: true,
|
|
43
|
+
state: 'running',
|
|
44
|
+
status: 'active (running)',
|
|
45
|
+
enabled: true,
|
|
46
|
+
pid: '1234',
|
|
47
|
+
restarts: '0',
|
|
48
|
+
started: '2026-08-23T12:00:00Z',
|
|
49
|
+
command: '/usr/bin/node',
|
|
50
|
+
arguments: 'server.js',
|
|
51
|
+
args: ['server.js'],
|
|
52
|
+
cwd: '/path/to/cwd',
|
|
53
|
+
platform: 'darwin',
|
|
54
|
+
manager: 'launchd'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const doctor: DoctorInfo = {
|
|
58
|
+
platform: 'darwin',
|
|
59
|
+
systemdAvailable: false,
|
|
60
|
+
systemctlAvailable: false,
|
|
61
|
+
linux: false,
|
|
62
|
+
systemdRunning: false,
|
|
63
|
+
userSystemdAvailable: false,
|
|
64
|
+
lingering: false,
|
|
65
|
+
nodePath: '/usr/bin/node',
|
|
66
|
+
nodeVersion: 'v20.0.0',
|
|
67
|
+
nodeValid: true,
|
|
68
|
+
username: 'user',
|
|
69
|
+
unitDir: '/tmp',
|
|
70
|
+
unitDirWritable: true
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const nodeDiag: NodeDiagnostics = {
|
|
74
|
+
found: true,
|
|
75
|
+
path: '/usr/bin/node',
|
|
76
|
+
version: '20.0.0',
|
|
77
|
+
valid: true
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const list: ListServiceItem[] = [{ name: 'test-app', status: 'running', pid: '1234' }];
|
|
81
|
+
|
|
82
|
+
const failures: ServiceFailure[] = [
|
|
83
|
+
{
|
|
84
|
+
name: 'api',
|
|
85
|
+
group: 'default',
|
|
86
|
+
status: 'failed',
|
|
87
|
+
exitCode: '1',
|
|
88
|
+
restarts: '3',
|
|
89
|
+
uptime: '10s'
|
|
90
|
+
}
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const caps: PlatformCapabilities = {
|
|
94
|
+
serviceManager: 'launchd',
|
|
95
|
+
supports: {
|
|
96
|
+
install: true,
|
|
97
|
+
uninstall: true,
|
|
98
|
+
start: true,
|
|
99
|
+
stop: true,
|
|
100
|
+
restart: true,
|
|
101
|
+
enable: true,
|
|
102
|
+
disable: true,
|
|
103
|
+
status: true,
|
|
104
|
+
logs: true,
|
|
105
|
+
restartPolicy: true,
|
|
106
|
+
userServices: true,
|
|
107
|
+
systemServices: true,
|
|
108
|
+
memoryLimits: false,
|
|
109
|
+
schedule: false
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const logs: LogOptions = {
|
|
114
|
+
follow: true,
|
|
115
|
+
lines: 50,
|
|
116
|
+
grep: 'ERROR',
|
|
117
|
+
json: true
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const meta: AppMetadata = {
|
|
121
|
+
name: 'test-app',
|
|
122
|
+
runtime: 'node',
|
|
123
|
+
command: '/usr/bin/node',
|
|
124
|
+
args: ['app.js'],
|
|
125
|
+
cwd: '/tmp'
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const norm: NormalizedServiceConfig = {
|
|
129
|
+
name: 'test-app',
|
|
130
|
+
displayName: 'Test App',
|
|
131
|
+
description: 'Unitup test',
|
|
132
|
+
runtime: 'node',
|
|
133
|
+
command: '/usr/bin/node',
|
|
134
|
+
args: ['app.js'],
|
|
135
|
+
cwd: '/tmp',
|
|
136
|
+
env: { FOO: 'bar' },
|
|
137
|
+
autostart: true,
|
|
138
|
+
restart: {
|
|
139
|
+
enabled: true,
|
|
140
|
+
policy: 'on-failure',
|
|
141
|
+
delay: 3000
|
|
142
|
+
},
|
|
143
|
+
logs: {
|
|
144
|
+
stdout: '/tmp/test.log',
|
|
145
|
+
stderr: '/tmp/test-error.log'
|
|
146
|
+
},
|
|
147
|
+
group: 'default',
|
|
148
|
+
system: false,
|
|
149
|
+
shutdownTimeout: 10000,
|
|
150
|
+
resources: {
|
|
151
|
+
memoryMax: '512M'
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
console.log(options, installOpts, status, doctor, nodeDiag, list, failures, caps, logs, meta, norm);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
typeCheckTest();
|