unitup 0.1.4 → 0.2.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,216 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ExecutableNotFoundError, InvalidServiceConfigError } from '../errors.js';
4
+ import { detectRuntime, resolveRuntimeConfig } from '../runtimes/index.js';
5
+ import { getUnitupDir, resolveEffectiveMemoryLimits, resolveWorkingDirectory, sanitizeServiceName } from '../utils.js';
6
+
7
+ /**
8
+ * Resolves a binary or command name to an absolute executable path.
9
+ * Supports Windows PATHEXT extensions (.exe, .cmd, .bat, etc.) and Unix executable permissions.
10
+ *
11
+ * @param {string} binaryName
12
+ * @param {object} [opts]
13
+ * @param {string} [opts.cwd]
14
+ * @param {string} [opts.pathEnv]
15
+ * @returns {string|null}
16
+ */
17
+ export function resolveExecutable(binaryName, opts = {}) {
18
+ if (!binaryName || typeof binaryName !== 'string') {
19
+ return null;
20
+ }
21
+
22
+ const trimmed = binaryName.trim();
23
+ if (!trimmed) return null;
24
+
25
+ const baseDir = opts.cwd || process.cwd();
26
+ const isWin = process.platform === 'win32';
27
+ const pathext = isWin
28
+ ? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WSF;.WSC;.MSI').split(';').filter(Boolean)
29
+ : [''];
30
+
31
+ // Helper to check if a specific file is executable
32
+ function isExecutableFile(targetPath) {
33
+ try {
34
+ if (!fs.existsSync(targetPath)) return false;
35
+ const stat = fs.statSync(targetPath);
36
+ if (!stat.isFile()) return false;
37
+ if (isWin) return true;
38
+ fs.accessSync(targetPath, fs.constants.X_OK);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ // 1. If path is absolute or explicit relative path (starts with ./, ../, /)
46
+ if (path.isAbsolute(trimmed) || trimmed.startsWith('./') || trimmed.startsWith('../') || trimmed.startsWith('~/')) {
47
+ const expanded = trimmed.startsWith('~/')
48
+ ? path.join(process.env.HOME || process.env.USERPROFILE || '', trimmed.slice(2))
49
+ : path.resolve(baseDir, trimmed);
50
+
51
+ if (isExecutableFile(expanded)) {
52
+ return fs.realpathSync(expanded);
53
+ }
54
+
55
+ if (isWin) {
56
+ for (const ext of pathext) {
57
+ const withExt = expanded + ext.toLowerCase();
58
+ if (isExecutableFile(withExt)) {
59
+ return fs.realpathSync(withExt);
60
+ }
61
+ }
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ // 2. Look in system PATH
68
+ const envPath = opts.pathEnv || process.env.PATH || '';
69
+ const searchDirs = envPath.split(path.delimiter).filter(Boolean);
70
+
71
+ for (const dir of searchDirs) {
72
+ const candidate = path.join(dir, trimmed);
73
+ if (isExecutableFile(candidate)) {
74
+ try {
75
+ return fs.realpathSync(candidate);
76
+ } catch {
77
+ return candidate;
78
+ }
79
+ }
80
+
81
+ if (isWin) {
82
+ for (const ext of pathext) {
83
+ const candidateWithExt = candidate + ext.toLowerCase();
84
+ if (isExecutableFile(candidateWithExt)) {
85
+ try {
86
+ return fs.realpathSync(candidateWithExt);
87
+ } catch {
88
+ return candidateWithExt;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Normalizes input service options into a complete, validated NormalizedServiceConfig object.
100
+ *
101
+ * @param {object} rawOpts
102
+ * @returns {Promise<object>} NormalizedServiceConfig
103
+ */
104
+ export async function normalizeServiceConfig(rawOpts = {}) {
105
+ if (!rawOpts || typeof rawOpts !== 'object') {
106
+ throw new InvalidServiceConfigError('Service configuration options must be an object.');
107
+ }
108
+
109
+ const name = rawOpts.name ? sanitizeServiceName(rawOpts.name) : '';
110
+ if (!name) {
111
+ throw new InvalidServiceConfigError('Service name is required.', 'name');
112
+ }
113
+
114
+ const cwd = resolveWorkingDirectory(rawOpts);
115
+ const runtimeConfig = await resolveRuntimeConfig({
116
+ ...rawOpts,
117
+ name,
118
+ cwd
119
+ });
120
+
121
+ const command = runtimeConfig.command;
122
+ if (!command) {
123
+ throw new ExecutableNotFoundError(
124
+ rawOpts.command || rawOpts.script || 'executable',
125
+ `Executable could not be resolved for service "${name}".`
126
+ );
127
+ }
128
+
129
+ const args = Array.isArray(runtimeConfig.args) ? [...runtimeConfig.args] : [];
130
+ const displayName = rawOpts.displayName || rawOpts.name || name;
131
+ const description = rawOpts.description || `unitup service: ${name}`;
132
+ const group = rawOpts.group || 'default';
133
+ const system = !!rawOpts.system;
134
+ const autostart = rawOpts.autostart !== undefined ? !!rawOpts.autostart : true;
135
+ const shutdownTimeout =
136
+ rawOpts.shutdownTimeout !== undefined &&
137
+ !Number.isNaN(Number(rawOpts.shutdownTimeout)) &&
138
+ Number(rawOpts.shutdownTimeout) > 0
139
+ ? Number(rawOpts.shutdownTimeout)
140
+ : 10000;
141
+
142
+ // Environment normalization
143
+ const env = {};
144
+ if (rawOpts.env && typeof rawOpts.env === 'object') {
145
+ for (const [k, v] of Object.entries(rawOpts.env)) {
146
+ if (v !== undefined && v !== null) {
147
+ env[k] = String(v);
148
+ }
149
+ }
150
+ }
151
+
152
+ // Restart policy normalization
153
+ let restartPolicy = 'on-failure';
154
+ let restartEnabled = true;
155
+ let restartDelay = 3000;
156
+ let maxRetries = null;
157
+ let resetAfter = null;
158
+
159
+ if (typeof rawOpts.restart === 'string') {
160
+ restartPolicy = rawOpts.restart;
161
+ restartEnabled = restartPolicy !== 'no';
162
+ } else if (rawOpts.restart && typeof rawOpts.restart === 'object') {
163
+ restartEnabled = rawOpts.restart.enabled !== false;
164
+ restartPolicy = rawOpts.restart.policy || (restartEnabled ? 'on-failure' : 'no');
165
+ if (rawOpts.restart.delay !== undefined && !Number.isNaN(Number(rawOpts.restart.delay))) {
166
+ restartDelay = Number(rawOpts.restart.delay);
167
+ }
168
+ if (rawOpts.restart.maxRetries !== undefined && !Number.isNaN(Number(rawOpts.restart.maxRetries))) {
169
+ maxRetries = Number(rawOpts.restart.maxRetries);
170
+ }
171
+ if (rawOpts.restart.resetAfter !== undefined && !Number.isNaN(Number(rawOpts.restart.resetAfter))) {
172
+ resetAfter = Number(rawOpts.restart.resetAfter);
173
+ }
174
+ }
175
+
176
+ // Logs normalization
177
+ const unitupDir = getUnitupDir();
178
+ const logsDir = path.join(unitupDir, 'logs');
179
+ const defaultStdout = path.join(logsDir, `${name}.log`);
180
+ const defaultStderr = path.join(logsDir, `${name}-error.log`);
181
+
182
+ const logs = {
183
+ stdout: rawOpts.logs?.stdout || rawOpts.stdout || defaultStdout,
184
+ stderr: rawOpts.logs?.stderr || rawOpts.stderr || defaultStderr
185
+ };
186
+
187
+ // Memory and resource limits
188
+ const resources = resolveEffectiveMemoryLimits(rawOpts);
189
+
190
+ return {
191
+ name,
192
+ displayName,
193
+ description,
194
+ runtime: runtimeConfig.runtime || 'custom',
195
+ command,
196
+ args,
197
+ script: rawOpts.script ? path.resolve(cwd, rawOpts.script) : undefined,
198
+ cwd,
199
+ env,
200
+ envFile: rawOpts.envFile ? path.resolve(cwd, rawOpts.envFile) : undefined,
201
+ autostart,
202
+ restart: {
203
+ enabled: restartEnabled,
204
+ policy: restartPolicy,
205
+ delay: restartDelay,
206
+ maxRetries,
207
+ resetAfter
208
+ },
209
+ logs,
210
+ group,
211
+ system,
212
+ shutdownTimeout,
213
+ resources,
214
+ raw: rawOpts
215
+ };
216
+ }
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
- writeUnitFile,
20
- parseUnitContent
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 safeName = sanitizeServiceName(opts.name);
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
- if (!opts.force) {
318
+ wasActive = true;
319
+ if (!mergedOpts.force) {
291
320
  throw new Error(
292
321
  `Service "${safeName}" is currently running.\n` +
293
- `Use --force (-f) to overwrite running services, or stop it first:\n` +
294
- ` unitup stop ${safeName}`
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({ ...opts, name: safeName });
334
+ const runtimeConfig = await resolveRuntimeConfig({ ...mergedOpts, name: safeName });
306
335
 
307
336
  const { path: unitPath } = writeUnitFile({
308
- ...opts,
337
+ ...mergedOpts,
309
338
  name: safeName,
310
339
  runtime: runtimeConfig.runtime,
311
340
  command: runtimeConfig.command,
312
341
  args: runtimeConfig.args,
313
- cwd: opts.cwd
342
+ cwd: mergedOpts.cwd
314
343
  });
315
344
 
316
345
  await daemonReload();
317
346
 
318
- if (opts.start) {
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
- `Use --force (-f) to remove running services, or stop it first:\n` +
427
- ` unitup stop ${safeName}`
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 = startedRaw && startedRaw !== '0' && startedRaw !== 'n/a'
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 (res.code !== 0 && (res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root'))) {
761
- throw new Error('Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.');
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 (res.code !== 0 && (res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root') || res.stderr.toLowerCase().includes('access denied'))) {
941
- throw new Error('Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.');
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 = !isNaN(memoryCurrentBytes) && memoryCurrentBytes > 0 && memoryCurrentBytes < 1e15 ? memoryCurrentBytes : 0;
1021
+ const rawBytes =
1022
+ !isNaN(memoryCurrentBytes) && memoryCurrentBytes > 0 && memoryCurrentBytes < 1e15 ? memoryCurrentBytes : 0;
982
1023
 
983
- const status = props.ActiveState === 'active' ? (props.SubState === 'running' ? 'running' : props.SubState || 'active') : (props.ActiveState || 'stopped');
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,21 @@
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
- sanitizeServiceName,
6
- getUnitFilename,
7
- getTimerFilename,
8
- getServiceNameFromUnit,
9
- formatSystemdEnv,
5
+ deleteAppMetadata,
6
+ deleteScheduleMetadata,
10
7
  escapeExecArg,
8
+ formatSystemdEnv,
9
+ getServiceNameFromUnit,
10
+ getTimerFilename,
11
+ getUnitFilename,
11
12
  resolveAbsolutePath,
13
+ resolveEffectiveMemoryLimits,
12
14
  resolveWorkingDirectory,
15
+ sanitizeServiceName,
13
16
  saveAppMetadata,
14
- deleteAppMetadata,
15
17
  saveScheduleMetadata,
16
- deleteScheduleMetadata,
17
- validateDuration,
18
- resolveEffectiveMemoryLimits
18
+ validateDuration
19
19
  } from './utils.js';
20
20
 
21
21
  /**
@@ -241,7 +241,7 @@ export function parseUnitContent(content) {
241
241
  } else if (trimmed.startsWith('ExecStart=')) {
242
242
  const execVal = trimmed.slice(10).trim();
243
243
  const parts = execVal.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
244
- const cleanParts = parts.map(p => p.replace(/^"|"$/g, ''));
244
+ const cleanParts = parts.map((p) => p.replace(/^"|"$/g, '').replace(/\\"/g, '"').replace(/\\\\/g, '\\'));
245
245
  if (cleanParts.length > 0) {
246
246
  result.command = cleanParts[0];
247
247
  result.args = cleanParts.slice(1);
@@ -368,12 +368,7 @@ export function generateScheduleServiceContent(opts) {
368
368
  export function generateTimerContent(opts) {
369
369
  const safeName = sanitizeServiceName(opts.name);
370
370
 
371
- const timerLines = [
372
- '[Unit]',
373
- `Description=unitup timer: ${safeName}`,
374
- '',
375
- '[Timer]'
376
- ];
371
+ const timerLines = ['[Unit]', `Description=unitup timer: ${safeName}`, '', '[Timer]'];
377
372
 
378
373
  if (opts.every) {
379
374
  const validEvery = validateDuration(opts.every, '--every');
@@ -482,4 +477,3 @@ export function deleteScheduleUnitFiles(name) {
482
477
  }
483
478
  return removed;
484
479
  }
485
-
package/src/utils.js CHANGED
@@ -1,6 +1,6 @@
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
4
 
5
5
  /**
6
6
  * Returns the base unitup config directory (~/.config/unitup).
@@ -75,7 +75,14 @@ export function validateMemorySize(val, paramName = 'Memory limit') {
75
75
  * @returns {string} Formatted memory string e.g. "284 MB", "infinity", "unavailable"
76
76
  */
77
77
  export function formatMemoryBytes(bytes) {
78
- if (bytes === null || bytes === undefined || bytes === '' || bytes === 'unavailable' || bytes === '[unavailable]' || bytes === '-') {
78
+ if (
79
+ bytes === null ||
80
+ bytes === undefined ||
81
+ bytes === '' ||
82
+ bytes === 'unavailable' ||
83
+ bytes === '[unavailable]' ||
84
+ bytes === '-'
85
+ ) {
79
86
  return 'unavailable';
80
87
  }
81
88
 
@@ -137,10 +144,16 @@ export function saveAppMetadata(meta) {
137
144
  }
138
145
  const filePath = getAppMetadataPath(safeName);
139
146
 
140
- const command = meta.command ? resolveAbsolutePath(meta.command) : (meta.node ? resolveAbsolutePath(meta.node) : process.execPath);
141
- const rawArgs = Array.isArray(meta.args) ? meta.args : (meta.script ? [meta.script] : []);
142
- const args = rawArgs.map(a => (a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/')) ? resolveAbsolutePath(a) : a);
143
- const scriptPath = meta.script ? resolveAbsolutePath(meta.script) : (args[0] ? resolveAbsolutePath(args[0]) : command);
147
+ const command = meta.command
148
+ ? resolveAbsolutePath(meta.command)
149
+ : meta.node
150
+ ? resolveAbsolutePath(meta.node)
151
+ : process.execPath;
152
+ const rawArgs = Array.isArray(meta.args) ? meta.args : meta.script ? [meta.script] : [];
153
+ const args = rawArgs.map((a) =>
154
+ a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/') ? resolveAbsolutePath(a) : a
155
+ );
156
+ const scriptPath = meta.script ? resolveAbsolutePath(meta.script) : args[0] ? resolveAbsolutePath(args[0]) : command;
144
157
  const cwd = resolveWorkingDirectory(meta);
145
158
 
146
159
  // Extract memory resources if present
@@ -414,7 +427,7 @@ export function formatTable(data, columns) {
414
427
  return 'No services found.';
415
428
  }
416
429
 
417
- const widths = columns.map(col => col.label.length);
430
+ const widths = columns.map((col) => col.label.length);
418
431
 
419
432
  for (const row of data) {
420
433
  columns.forEach((col, idx) => {
@@ -429,9 +442,7 @@ export function formatTable(data, columns) {
429
442
  const lines = [headerLine];
430
443
 
431
444
  for (const row of data) {
432
- const line = columns
433
- .map((col, idx) => String(row[col.key] ?? '').padEnd(widths[idx]))
434
- .join(' ');
445
+ const line = columns.map((col, idx) => String(row[col.key] ?? '').padEnd(widths[idx])).join(' ');
435
446
  lines.push(line);
436
447
  }
437
448
 
@@ -471,7 +482,9 @@ export function saveScheduleMetadata(meta) {
471
482
 
472
483
  const command = meta.command ? resolveAbsolutePath(meta.command) : process.execPath;
473
484
  const rawArgs = Array.isArray(meta.args) ? meta.args : [];
474
- const args = rawArgs.map(a => (a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/')) ? resolveAbsolutePath(a) : a);
485
+ const args = rawArgs.map((a) =>
486
+ a.startsWith('/') || a.startsWith('./') || a.startsWith('../') || a.startsWith('~/') ? resolveAbsolutePath(a) : a
487
+ );
475
488
 
476
489
  const resources = meta.resources || {};
477
490
  if (meta.memoryHigh) resources.memoryHigh = validateMemorySize(meta.memoryHigh, 'MemoryHigh');
@@ -578,7 +591,8 @@ export function validateDuration(val, paramName = 'Duration') {
578
591
  }
579
592
 
580
593
  const tokens = str.split(/\s+/);
581
- 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;
594
+ const tokenRegex =
595
+ /^(\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
596
 
583
597
  for (const token of tokens) {
584
598
  if (!tokenRegex.test(token)) {
@@ -598,7 +612,9 @@ export function formatFutureTime(dateVal) {
598
612
  if (!dateVal || dateVal === 'n/a' || dateVal === '0' || dateVal === 'infinity') {
599
613
  return 'n/a';
600
614
  }
601
- const date = new Date(typeof dateVal === 'number' && dateVal > 1e12 ? dateVal : (typeof dateVal === 'number' ? dateVal / 1000 : dateVal));
615
+ const date = new Date(
616
+ typeof dateVal === 'number' && dateVal > 1e12 ? dateVal : typeof dateVal === 'number' ? dateVal / 1000 : dateVal
617
+ );
602
618
  if (isNaN(date.getTime())) {
603
619
  return String(dateVal);
604
620
  }
@@ -691,7 +707,8 @@ export function resolveEffectiveMemoryLimits(opts = {}) {
691
707
  }
692
708
 
693
709
  if (defaultMem) {
694
- const sizeStr = (typeof defaultMem === 'boolean' || defaultMem === 'true' || defaultMem === '') ? '1G' : String(defaultMem);
710
+ const sizeStr =
711
+ typeof defaultMem === 'boolean' || defaultMem === 'true' || defaultMem === '' ? '1G' : String(defaultMem);
695
712
  const validSize = validateMemorySize(sizeStr, 'Default memory limit');
696
713
  return { memoryMax: validSize };
697
714
  }
@@ -757,6 +774,3 @@ export function saveProjectConfig(dirPathOrFile = process.cwd(), config = {}) {
757
774
  fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
758
775
  return filePath;
759
776
  }
760
-
761
-
762
-
package/tsconfig.json CHANGED
@@ -9,5 +9,5 @@
9
9
  "allowJs": true,
10
10
  "checkJs": false
11
11
  },
12
- "include": ["index.d.ts"]
12
+ "include": ["index.d.ts", "types-test/index.ts"]
13
13
  }