unitup 0.0.12 → 0.1.1
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/README.md +125 -6
- package/SECURITY.md +21 -0
- package/docs/index.html +68 -4
- package/index.d.ts +137 -0
- package/package.json +1 -1
- package/src/cli.js +318 -20
- package/src/index.js +53 -3
- package/src/schedule.js +459 -0
- package/src/systemd.js +126 -0
- package/src/unit.js +238 -6
- package/src/utils.js +254 -1
package/src/unit.js
CHANGED
|
@@ -4,12 +4,17 @@ import os from 'node:os';
|
|
|
4
4
|
import {
|
|
5
5
|
sanitizeServiceName,
|
|
6
6
|
getUnitFilename,
|
|
7
|
+
getTimerFilename,
|
|
7
8
|
getServiceNameFromUnit,
|
|
8
9
|
formatSystemdEnv,
|
|
9
10
|
escapeExecArg,
|
|
10
11
|
resolveAbsolutePath,
|
|
11
12
|
saveAppMetadata,
|
|
12
|
-
deleteAppMetadata
|
|
13
|
+
deleteAppMetadata,
|
|
14
|
+
saveScheduleMetadata,
|
|
15
|
+
deleteScheduleMetadata,
|
|
16
|
+
validateDuration,
|
|
17
|
+
resolveEffectiveMemoryLimits
|
|
13
18
|
} from './utils.js';
|
|
14
19
|
|
|
15
20
|
/**
|
|
@@ -78,8 +83,11 @@ export function generateUnitContent(opts) {
|
|
|
78
83
|
throw new Error('Either "command" or "script" must be provided to generate systemd unit file.');
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
const
|
|
82
|
-
|
|
86
|
+
const cwd = opts.cwd
|
|
87
|
+
? resolveAbsolutePath(opts.cwd)
|
|
88
|
+
: opts.script
|
|
89
|
+
? path.dirname(resolveAbsolutePath(opts.script))
|
|
90
|
+
: process.cwd();
|
|
83
91
|
const restartPolicy = opts.restart || 'on-failure';
|
|
84
92
|
|
|
85
93
|
const execStartTokens = [commandExec, ...execArgs];
|
|
@@ -106,9 +114,10 @@ export function generateUnitContent(opts) {
|
|
|
106
114
|
'StandardError=journal'
|
|
107
115
|
];
|
|
108
116
|
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
117
|
+
const effectiveLimits = resolveEffectiveMemoryLimits(opts);
|
|
118
|
+
const memHigh = effectiveLimits.memoryHigh;
|
|
119
|
+
const memMax = effectiveLimits.memoryMax;
|
|
120
|
+
const memSwapMax = effectiveLimits.memorySwapMax;
|
|
112
121
|
|
|
113
122
|
if (memHigh || memMax || memSwapMax) {
|
|
114
123
|
serviceLines.push('MemoryAccounting=yes');
|
|
@@ -258,3 +267,226 @@ export function parseUnitContent(content) {
|
|
|
258
267
|
|
|
259
268
|
return result;
|
|
260
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Returns the full file path for a given timer unit.
|
|
273
|
+
* @param {string} name
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
export function getTimerPath(name) {
|
|
277
|
+
const filename = getTimerFilename(name);
|
|
278
|
+
return path.join(getUserUnitDir(), filename);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Checks if a timer unit file exists for the given schedule name.
|
|
283
|
+
* @param {string} name
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
export function timerFileExists(name) {
|
|
287
|
+
const timerPath = getTimerPath(name);
|
|
288
|
+
return fs.existsSync(timerPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Generates systemd service unit content for a scheduled oneshot task.
|
|
293
|
+
* @param {Object} opts
|
|
294
|
+
* @returns {string}
|
|
295
|
+
*/
|
|
296
|
+
export function generateScheduleServiceContent(opts) {
|
|
297
|
+
const safeName = sanitizeServiceName(opts.name);
|
|
298
|
+
let commandExec = '';
|
|
299
|
+
let execArgs = [];
|
|
300
|
+
|
|
301
|
+
if (opts.command) {
|
|
302
|
+
commandExec = resolveAbsolutePath(opts.command);
|
|
303
|
+
execArgs = Array.isArray(opts.args) ? [...opts.args] : [];
|
|
304
|
+
} else if (opts.script) {
|
|
305
|
+
const scriptPath = resolveAbsolutePath(opts.script);
|
|
306
|
+
commandExec = opts.nodePath ? resolveAbsolutePath(opts.nodePath) : process.execPath;
|
|
307
|
+
execArgs = [scriptPath, ...(Array.isArray(opts.args) ? opts.args : [])];
|
|
308
|
+
} else {
|
|
309
|
+
throw new Error('Either "command" or "script" must be provided to generate scheduled unit file.');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const cwd = opts.cwd
|
|
313
|
+
? resolveAbsolutePath(opts.cwd)
|
|
314
|
+
: opts.script
|
|
315
|
+
? path.dirname(resolveAbsolutePath(opts.script))
|
|
316
|
+
: process.cwd();
|
|
317
|
+
|
|
318
|
+
const execStartTokens = [commandExec, ...execArgs];
|
|
319
|
+
const execStartLine = execStartTokens.map(escapeExecArg).join(' ');
|
|
320
|
+
|
|
321
|
+
const binDir = path.dirname(commandExec);
|
|
322
|
+
const defaultPath = [binDir, '/usr/local/bin', '/usr/bin', '/bin']
|
|
323
|
+
.filter((p, i, self) => p && self.indexOf(p) === i)
|
|
324
|
+
.join(':');
|
|
325
|
+
|
|
326
|
+
const serviceLines = [
|
|
327
|
+
'[Unit]',
|
|
328
|
+
`Description=unitup scheduled task: ${safeName}`,
|
|
329
|
+
'After=network.target',
|
|
330
|
+
'',
|
|
331
|
+
'[Service]',
|
|
332
|
+
'Type=oneshot',
|
|
333
|
+
`SyslogIdentifier=unitup-${safeName}`,
|
|
334
|
+
`WorkingDirectory=${cwd}`,
|
|
335
|
+
`ExecStart=${execStartLine}`,
|
|
336
|
+
'StandardOutput=journal',
|
|
337
|
+
'StandardError=journal'
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
const effectiveLimits = resolveEffectiveMemoryLimits(opts);
|
|
341
|
+
const memHigh = effectiveLimits.memoryHigh;
|
|
342
|
+
const memMax = effectiveLimits.memoryMax;
|
|
343
|
+
const memSwapMax = effectiveLimits.memorySwapMax;
|
|
344
|
+
|
|
345
|
+
if (memHigh || memMax || memSwapMax) {
|
|
346
|
+
serviceLines.push('MemoryAccounting=yes');
|
|
347
|
+
if (memHigh) serviceLines.push(`MemoryHigh=${memHigh}`);
|
|
348
|
+
if (memMax) serviceLines.push(`MemoryMax=${memMax}`);
|
|
349
|
+
if (memSwapMax) serviceLines.push(`MemorySwapMax=${memSwapMax}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const envObj = opts.env && typeof opts.env === 'object' ? { ...opts.env } : {};
|
|
353
|
+
if (!envObj.PATH) {
|
|
354
|
+
envObj.PATH = defaultPath;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (opts.envFile) {
|
|
358
|
+
const absEnvFile = resolveAbsolutePath(opts.envFile);
|
|
359
|
+
serviceLines.push(`EnvironmentFile=${absEnvFile}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const [key, val] of Object.entries(envObj)) {
|
|
363
|
+
serviceLines.push(`Environment=${formatSystemdEnv(key, val)}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
serviceLines.push('');
|
|
367
|
+
return serviceLines.join('\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Generates systemd timer unit content.
|
|
372
|
+
* @param {Object} opts
|
|
373
|
+
* @returns {string}
|
|
374
|
+
*/
|
|
375
|
+
export function generateTimerContent(opts) {
|
|
376
|
+
const safeName = sanitizeServiceName(opts.name);
|
|
377
|
+
|
|
378
|
+
const timerLines = [
|
|
379
|
+
'[Unit]',
|
|
380
|
+
`Description=unitup timer: ${safeName}`,
|
|
381
|
+
'',
|
|
382
|
+
'[Timer]'
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
if (opts.every) {
|
|
386
|
+
const validEvery = validateDuration(opts.every, '--every');
|
|
387
|
+
const onActive = opts.onActive ? validateDuration(opts.onActive, '--on-active') : validEvery;
|
|
388
|
+
timerLines.push(`OnActiveSec=${onActive}`);
|
|
389
|
+
timerLines.push(`OnUnitActiveSec=${validEvery}`);
|
|
390
|
+
} else if (opts.onActive) {
|
|
391
|
+
const validActive = validateDuration(opts.onActive, '--on-active');
|
|
392
|
+
timerLines.push(`OnActiveSec=${validActive}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (opts.calendar) {
|
|
396
|
+
if (typeof opts.calendar !== 'string' || !opts.calendar.trim()) {
|
|
397
|
+
throw new Error('Calendar expression cannot be empty.');
|
|
398
|
+
}
|
|
399
|
+
timerLines.push(`OnCalendar=${opts.calendar.trim()}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (opts.onBoot) {
|
|
403
|
+
const validBoot = validateDuration(opts.onBoot, '--on-boot');
|
|
404
|
+
timerLines.push(`OnBootSec=${validBoot}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (opts.randomDelay) {
|
|
408
|
+
const validDelay = validateDuration(opts.randomDelay, '--random-delay');
|
|
409
|
+
timerLines.push(`RandomizedDelaySec=${validDelay}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (opts.persistent) {
|
|
413
|
+
timerLines.push('Persistent=true');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const serviceFilename = getUnitFilename(safeName);
|
|
417
|
+
timerLines.push(`Unit=${serviceFilename}`);
|
|
418
|
+
|
|
419
|
+
timerLines.push('', '[Install]', 'WantedBy=timers.target', '');
|
|
420
|
+
|
|
421
|
+
return timerLines.join('\n');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Creates or updates systemd service and timer files for a schedule on disk.
|
|
426
|
+
* @param {Object} opts
|
|
427
|
+
* @returns {{ servicePath: string, timerPath: string, serviceContent: string, timerContent: string }}
|
|
428
|
+
*/
|
|
429
|
+
export function writeScheduleUnitFiles(opts) {
|
|
430
|
+
const safeName = sanitizeServiceName(opts.name);
|
|
431
|
+
const unitDir = getUserUnitDir();
|
|
432
|
+
|
|
433
|
+
if (!fs.existsSync(unitDir)) {
|
|
434
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const servicePath = getUnitPath(safeName);
|
|
438
|
+
const timerPath = getTimerPath(safeName);
|
|
439
|
+
|
|
440
|
+
const serviceContent = generateScheduleServiceContent({ ...opts, name: safeName });
|
|
441
|
+
const timerContent = generateTimerContent({ ...opts, name: safeName });
|
|
442
|
+
|
|
443
|
+
fs.writeFileSync(servicePath, serviceContent, 'utf8');
|
|
444
|
+
fs.writeFileSync(timerPath, timerContent, 'utf8');
|
|
445
|
+
|
|
446
|
+
saveScheduleMetadata({
|
|
447
|
+
name: safeName,
|
|
448
|
+
group: opts.group || 'default',
|
|
449
|
+
runtime: opts.runtime || (opts.command ? 'custom' : 'node'),
|
|
450
|
+
command: opts.command,
|
|
451
|
+
args: opts.args,
|
|
452
|
+
cwd: opts.cwd,
|
|
453
|
+
memoryHigh: opts.memoryHigh || opts.resources?.memoryHigh,
|
|
454
|
+
memoryMax: opts.memoryMax || opts.resources?.memoryMax,
|
|
455
|
+
memorySwapMax: opts.memorySwapMax || opts.resources?.memorySwapMax,
|
|
456
|
+
resources: opts.resources,
|
|
457
|
+
schedule: {
|
|
458
|
+
every: opts.every || null,
|
|
459
|
+
calendar: opts.calendar || null,
|
|
460
|
+
onBoot: opts.onBoot || null,
|
|
461
|
+
onActive: opts.onActive || null,
|
|
462
|
+
persistent: Boolean(opts.persistent),
|
|
463
|
+
randomDelay: opts.randomDelay || null
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return { servicePath, timerPath, serviceContent, timerContent };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Removes service, timer, and metadata files for a schedule.
|
|
472
|
+
* @param {string} name
|
|
473
|
+
* @returns {boolean}
|
|
474
|
+
*/
|
|
475
|
+
export function deleteScheduleUnitFiles(name) {
|
|
476
|
+
const safeName = sanitizeServiceName(name);
|
|
477
|
+
deleteScheduleMetadata(safeName);
|
|
478
|
+
const servicePath = getUnitPath(safeName);
|
|
479
|
+
const timerPath = getTimerPath(safeName);
|
|
480
|
+
|
|
481
|
+
let removed = false;
|
|
482
|
+
if (fs.existsSync(timerPath)) {
|
|
483
|
+
fs.unlinkSync(timerPath);
|
|
484
|
+
removed = true;
|
|
485
|
+
}
|
|
486
|
+
if (fs.existsSync(servicePath)) {
|
|
487
|
+
fs.unlinkSync(servicePath);
|
|
488
|
+
removed = true;
|
|
489
|
+
}
|
|
490
|
+
return removed;
|
|
491
|
+
}
|
|
492
|
+
|
package/src/utils.js
CHANGED
|
@@ -141,7 +141,7 @@ export function saveAppMetadata(meta) {
|
|
|
141
141
|
const rawArgs = Array.isArray(meta.args) ? meta.args : (meta.script ? [meta.script] : []);
|
|
142
142
|
const args = rawArgs.map(a => (a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/')) ? resolveAbsolutePath(a) : a);
|
|
143
143
|
const scriptPath = meta.script ? resolveAbsolutePath(meta.script) : (args[0] ? resolveAbsolutePath(args[0]) : command);
|
|
144
|
-
const cwd = meta.cwd ? resolveAbsolutePath(meta.cwd) : path.dirname(
|
|
144
|
+
const cwd = meta.cwd ? resolveAbsolutePath(meta.cwd) : (meta.script ? path.dirname(resolveAbsolutePath(meta.script)) : process.cwd());
|
|
145
145
|
|
|
146
146
|
// Extract memory resources if present
|
|
147
147
|
const resources = meta.resources || {};
|
|
@@ -402,3 +402,256 @@ export function formatTable(data, columns) {
|
|
|
402
402
|
|
|
403
403
|
return lines.join('\n');
|
|
404
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Returns the directory for schedule metadata (~/.config/unitup/schedules).
|
|
408
|
+
* @returns {string}
|
|
409
|
+
*/
|
|
410
|
+
export function getSchedulesDir() {
|
|
411
|
+
return path.join(getUnitupDir(), 'schedules');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Returns the filepath for a specific schedule's metadata JSON file.
|
|
416
|
+
* @param {string} name
|
|
417
|
+
* @returns {string}
|
|
418
|
+
*/
|
|
419
|
+
export function getScheduleMetadataPath(name) {
|
|
420
|
+
const safeName = sanitizeServiceName(name);
|
|
421
|
+
return path.join(getSchedulesDir(), `${safeName}.json`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Saves metadata JSON for a schedule.
|
|
426
|
+
* @param {object} meta
|
|
427
|
+
* @returns {object}
|
|
428
|
+
*/
|
|
429
|
+
export function saveScheduleMetadata(meta) {
|
|
430
|
+
const safeName = sanitizeServiceName(meta.name);
|
|
431
|
+
const dir = getSchedulesDir();
|
|
432
|
+
if (!fs.existsSync(dir)) {
|
|
433
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
434
|
+
}
|
|
435
|
+
const filePath = getScheduleMetadataPath(safeName);
|
|
436
|
+
|
|
437
|
+
const command = meta.command ? resolveAbsolutePath(meta.command) : process.execPath;
|
|
438
|
+
const rawArgs = Array.isArray(meta.args) ? meta.args : [];
|
|
439
|
+
const args = rawArgs.map(a => (a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/')) ? resolveAbsolutePath(a) : a);
|
|
440
|
+
|
|
441
|
+
const resources = meta.resources || {};
|
|
442
|
+
if (meta.memoryHigh) resources.memoryHigh = validateMemorySize(meta.memoryHigh, 'MemoryHigh');
|
|
443
|
+
if (meta.memoryMax) resources.memoryMax = validateMemorySize(meta.memoryMax, 'MemoryMax');
|
|
444
|
+
if (meta.memorySwapMax) resources.memorySwapMax = validateMemorySize(meta.memorySwapMax, 'MemorySwapMax');
|
|
445
|
+
|
|
446
|
+
const payload = {
|
|
447
|
+
name: safeName,
|
|
448
|
+
group: meta.group || 'default',
|
|
449
|
+
type: 'timer',
|
|
450
|
+
runtime: meta.runtime || 'node',
|
|
451
|
+
command,
|
|
452
|
+
args,
|
|
453
|
+
cwd: meta.cwd ? resolveAbsolutePath(meta.cwd) : process.cwd(),
|
|
454
|
+
schedule: {
|
|
455
|
+
every: meta.schedule?.every || null,
|
|
456
|
+
calendar: meta.schedule?.calendar || null,
|
|
457
|
+
onBoot: meta.schedule?.onBoot || null,
|
|
458
|
+
onActive: meta.schedule?.onActive || null,
|
|
459
|
+
persistent: Boolean(meta.schedule?.persistent),
|
|
460
|
+
randomDelay: meta.schedule?.randomDelay || null
|
|
461
|
+
},
|
|
462
|
+
...(Object.keys(resources).length > 0 ? { resources } : {}),
|
|
463
|
+
serviceUnit: `unitup-${safeName}.service`,
|
|
464
|
+
timerUnit: `unitup-${safeName}.timer`
|
|
465
|
+
};
|
|
466
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
|
|
467
|
+
return payload;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Reads metadata JSON for a schedule.
|
|
472
|
+
* @param {string} name
|
|
473
|
+
* @returns {object|null}
|
|
474
|
+
*/
|
|
475
|
+
export function readScheduleMetadata(name) {
|
|
476
|
+
try {
|
|
477
|
+
const filePath = getScheduleMetadataPath(name);
|
|
478
|
+
if (!fs.existsSync(filePath)) return null;
|
|
479
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
480
|
+
return JSON.parse(content);
|
|
481
|
+
} catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Deletes metadata JSON for a schedule if it exists.
|
|
488
|
+
* @param {string} name
|
|
489
|
+
* @returns {boolean}
|
|
490
|
+
*/
|
|
491
|
+
export function deleteScheduleMetadata(name) {
|
|
492
|
+
try {
|
|
493
|
+
const filePath = getScheduleMetadataPath(name);
|
|
494
|
+
if (fs.existsSync(filePath)) {
|
|
495
|
+
fs.unlinkSync(filePath);
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
} catch {}
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Returns the full systemd timer filename for a schedule name.
|
|
504
|
+
* @param {string} name
|
|
505
|
+
* @returns {string}
|
|
506
|
+
*/
|
|
507
|
+
export function getTimerFilename(name) {
|
|
508
|
+
const safeName = sanitizeServiceName(name);
|
|
509
|
+
return `unitup-${safeName}.timer`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Validates systemd time span duration strings (e.g., 30s, 10m, 2h, 1d).
|
|
514
|
+
* @param {string|number} val
|
|
515
|
+
* @param {string} [paramName]
|
|
516
|
+
* @returns {string}
|
|
517
|
+
*/
|
|
518
|
+
export function validateDuration(val, paramName = 'Duration') {
|
|
519
|
+
if (val === null || val === undefined || val === '') {
|
|
520
|
+
throw new Error(`${paramName} cannot be empty.`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const str = String(val).trim();
|
|
524
|
+
|
|
525
|
+
// Prevent shell injection and unsafe characters
|
|
526
|
+
if (/[;&|$`"'\n\r\t]/.test(str)) {
|
|
527
|
+
throw new Error(`${paramName} contains invalid characters or shell injection attempt: "${str}".`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Reject negative numbers
|
|
531
|
+
if (str.startsWith('-')) {
|
|
532
|
+
throw new Error(`${paramName} cannot be negative: "${str}".`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const tokens = str.split(/\s+/);
|
|
536
|
+
const tokenRegex = /^(\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;
|
|
537
|
+
|
|
538
|
+
for (const token of tokens) {
|
|
539
|
+
if (!tokenRegex.test(token)) {
|
|
540
|
+
throw new Error(`Invalid ${paramName} format: "${str}". Expected values like 30s, 10m, 2h, 1d.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return str;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Formats a future date/timestamp into relative human readable string (e.g. "in 12 minutes", "tomorrow").
|
|
549
|
+
* @param {string|number|Date} dateVal
|
|
550
|
+
* @returns {string}
|
|
551
|
+
*/
|
|
552
|
+
export function formatFutureTime(dateVal) {
|
|
553
|
+
if (!dateVal || dateVal === 'n/a' || dateVal === '0' || dateVal === 'infinity') {
|
|
554
|
+
return 'n/a';
|
|
555
|
+
}
|
|
556
|
+
const date = new Date(typeof dateVal === 'number' && dateVal > 1e12 ? dateVal : (typeof dateVal === 'number' ? dateVal / 1000 : dateVal));
|
|
557
|
+
if (isNaN(date.getTime())) {
|
|
558
|
+
return String(dateVal);
|
|
559
|
+
}
|
|
560
|
+
const diffMs = date.getTime() - Date.now();
|
|
561
|
+
if (diffMs <= 0) return 'imminent';
|
|
562
|
+
|
|
563
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
564
|
+
if (seconds < 60) return `in ${seconds} seconds`;
|
|
565
|
+
|
|
566
|
+
const minutes = Math.floor(seconds / 60);
|
|
567
|
+
if (minutes < 60) return `in ${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
568
|
+
|
|
569
|
+
const hours = Math.floor(minutes / 60);
|
|
570
|
+
if (hours < 24) return `in ${hours} hour${hours === 1 ? '' : 's'}`;
|
|
571
|
+
|
|
572
|
+
const days = Math.floor(hours / 24);
|
|
573
|
+
if (days === 1) return 'tomorrow';
|
|
574
|
+
return `in ${days} days`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Returns the filepath to the global unitup config JSON file (~/.config/unitup/config.json).
|
|
579
|
+
* @returns {string}
|
|
580
|
+
*/
|
|
581
|
+
export function getConfigFilepath() {
|
|
582
|
+
return path.join(getUnitupDir(), 'config.json');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Reads global unitup configuration JSON file.
|
|
587
|
+
* @returns {object}
|
|
588
|
+
*/
|
|
589
|
+
export function readGlobalConfig() {
|
|
590
|
+
try {
|
|
591
|
+
const file = getConfigFilepath();
|
|
592
|
+
if (!fs.existsSync(file)) return {};
|
|
593
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
594
|
+
return JSON.parse(content) || {};
|
|
595
|
+
} catch {
|
|
596
|
+
return {};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Saves global unitup configuration JSON file.
|
|
602
|
+
* @param {object} config
|
|
603
|
+
* @returns {object}
|
|
604
|
+
*/
|
|
605
|
+
export function saveGlobalConfig(config) {
|
|
606
|
+
const dir = getUnitupDir();
|
|
607
|
+
if (!fs.existsSync(dir)) {
|
|
608
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
609
|
+
}
|
|
610
|
+
const file = getConfigFilepath();
|
|
611
|
+
const current = readGlobalConfig();
|
|
612
|
+
const updated = { ...current, ...config };
|
|
613
|
+
fs.writeFileSync(file, JSON.stringify(updated, null, 2), 'utf8');
|
|
614
|
+
return updated;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Resolves effective memory limits considering explicit options, env vars, and global config defaults (default: 1G).
|
|
619
|
+
* @param {object} opts
|
|
620
|
+
* @returns {{ memoryHigh?: string, memoryMax?: string, memorySwapMax?: string }}
|
|
621
|
+
*/
|
|
622
|
+
export function resolveEffectiveMemoryLimits(opts = {}) {
|
|
623
|
+
const memHigh = opts.memoryHigh || opts.resources?.memoryHigh;
|
|
624
|
+
const memMax = opts.memoryMax || opts.resources?.memoryMax;
|
|
625
|
+
const memSwapMax = opts.memorySwapMax || opts.resources?.memorySwapMax;
|
|
626
|
+
|
|
627
|
+
if (memHigh || memMax || memSwapMax) {
|
|
628
|
+
return {
|
|
629
|
+
...(memHigh ? { memoryHigh: validateMemorySize(memHigh, 'MemoryHigh') } : {}),
|
|
630
|
+
...(memMax ? { memoryMax: validateMemorySize(memMax, 'MemoryMax') } : {}),
|
|
631
|
+
...(memSwapMax ? { memorySwapMax: validateMemorySize(memSwapMax, 'MemorySwapMax') } : {})
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
let defaultMem = opts.defaultMemory;
|
|
636
|
+
|
|
637
|
+
if (!defaultMem && process.env.UNITUP_DEFAULT_MEMORY) {
|
|
638
|
+
defaultMem = process.env.UNITUP_DEFAULT_MEMORY;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!defaultMem) {
|
|
642
|
+
const cfg = readGlobalConfig();
|
|
643
|
+
if (cfg.defaultMemory) {
|
|
644
|
+
defaultMem = cfg.defaultMemory;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (defaultMem) {
|
|
649
|
+
const sizeStr = (typeof defaultMem === 'boolean' || defaultMem === 'true' || defaultMem === '') ? '1G' : String(defaultMem);
|
|
650
|
+
const validSize = validateMemorySize(sizeStr, 'Default memory limit');
|
|
651
|
+
return { memoryMax: validSize };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return {};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
|