unitup 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unitup",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Minimal, zero-dependency systemd-native service manager for any runtime, script, or executable",
5
5
  "main": "src/index.js",
6
6
  "types": "./index.d.ts",
package/src/cli.js CHANGED
@@ -108,12 +108,15 @@ export function parseArgs(argv) {
108
108
  } else if (arg.startsWith('--runtime=')) {
109
109
  result.flags.runtime = arg.slice(10);
110
110
  i++;
111
- } else if (arg === '--runtime-arg') {
111
+ } else if (arg === '--runtime-arg' || arg === '--runtime-args') {
112
112
  if (argv[i + 1]) result.flags.runtimeArgs.push(argv[i + 1]);
113
113
  i += 2;
114
114
  } else if (arg.startsWith('--runtime-arg=')) {
115
115
  result.flags.runtimeArgs.push(arg.slice(14));
116
116
  i++;
117
+ } else if (arg.startsWith('--runtime-args=')) {
118
+ result.flags.runtimeArgs.push(arg.slice(15));
119
+ i++;
117
120
  } else if (arg === '--command') {
118
121
  result.flags.command = argv[i + 1] || '';
119
122
  i += 2;
@@ -162,12 +165,15 @@ export function parseArgs(argv) {
162
165
  } else if (arg.startsWith('--restart=')) {
163
166
  result.flags.restart = arg.slice(10);
164
167
  i++;
165
- } else if (arg === '--arg') {
168
+ } else if (arg === '--arg' || arg === '--args') {
166
169
  if (argv[i + 1]) result.flags.args.push(argv[i + 1]);
167
170
  i += 2;
168
171
  } else if (arg.startsWith('--arg=')) {
169
172
  result.flags.args.push(arg.slice(6));
170
173
  i++;
174
+ } else if (arg.startsWith('--args=')) {
175
+ result.flags.args.push(arg.slice(7));
176
+ i++;
171
177
  } else if (arg === '--lines' || arg === '-n') {
172
178
  result.flags.lines = parseInt(argv[i + 1] || '100', 10);
173
179
  i += 2;
package/src/schedule.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  getUnitFilename,
7
7
  getTimerFilename,
8
8
  resolveAbsolutePath,
9
+ resolveWorkingDirectory,
9
10
  validateDuration,
10
11
  formatFutureTime,
11
12
  formatRelativeTime,
@@ -125,11 +126,7 @@ export async function createSchedule(opts = {}) {
125
126
  const execArgs = resolved.args;
126
127
  const detectedRuntime = resolved.runtime;
127
128
 
128
- const cwd = opts.cwd
129
- ? resolveAbsolutePath(opts.cwd)
130
- : scriptPath
131
- ? path.dirname(scriptPath)
132
- : process.cwd();
129
+ const cwd = resolveWorkingDirectory({ cwd: opts.cwd, script: scriptPath });
133
130
 
134
131
  const envObj = {};
135
132
  if (opts.env) {
@@ -173,6 +170,7 @@ export async function createSchedule(opts = {}) {
173
170
 
174
171
  if (opts.start || opts.enable) {
175
172
  await runCommand('systemctl', ['--user', 'daemon-reload']);
173
+ await runCommand('systemctl', ['--user', 'reset-failed', getUnitFilename(safeName), getTimerFilename(safeName)]);
176
174
  await runCommand('systemctl', ['--user', 'enable', getTimerFilename(safeName)]);
177
175
  await runCommand('systemctl', ['--user', 'start', getTimerFilename(safeName)]);
178
176
  }
@@ -212,6 +210,7 @@ export async function enableSchedule(name) {
212
210
  throw new Error(`Schedule timer unit up for "${name}" does not exist.`);
213
211
  }
214
212
  await runCommand('systemctl', ['--user', 'daemon-reload']);
213
+ await runCommand('systemctl', ['--user', 'reset-failed', getUnitFilename(safeName), getTimerFilename(safeName)]);
215
214
  const res = await runCommand('systemctl', ['--user', 'enable', '--now', getTimerFilename(safeName)]);
216
215
  return res.code === 0;
217
216
  }
package/src/unit.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  formatSystemdEnv,
10
10
  escapeExecArg,
11
11
  resolveAbsolutePath,
12
+ resolveWorkingDirectory,
12
13
  saveAppMetadata,
13
14
  deleteAppMetadata,
14
15
  saveScheduleMetadata,
@@ -83,11 +84,7 @@ export function generateUnitContent(opts) {
83
84
  throw new Error('Either "command" or "script" must be provided to generate systemd unit file.');
84
85
  }
85
86
 
86
- const cwd = opts.cwd
87
- ? resolveAbsolutePath(opts.cwd)
88
- : opts.script
89
- ? path.dirname(resolveAbsolutePath(opts.script))
90
- : process.cwd();
87
+ const cwd = resolveWorkingDirectory(opts);
91
88
  const restartPolicy = opts.restart || 'on-failure';
92
89
 
93
90
  const execStartTokens = [commandExec, ...execArgs];
@@ -309,11 +306,7 @@ export function generateScheduleServiceContent(opts) {
309
306
  throw new Error('Either "command" or "script" must be provided to generate scheduled unit file.');
310
307
  }
311
308
 
312
- const cwd = opts.cwd
313
- ? resolveAbsolutePath(opts.cwd)
314
- : opts.script
315
- ? path.dirname(resolveAbsolutePath(opts.script))
316
- : process.cwd();
309
+ const cwd = resolveWorkingDirectory(opts);
317
310
 
318
311
  const execStartTokens = [commandExec, ...execArgs];
319
312
  const execStartLine = execStartTokens.map(escapeExecArg).join(' ');
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) : (meta.script ? path.dirname(resolveAbsolutePath(meta.script)) : process.cwd());
144
+ const cwd = resolveWorkingDirectory(meta);
145
145
 
146
146
  // Extract memory resources if present
147
147
  const resources = meta.resources || {};
@@ -180,10 +180,17 @@ export function readAppMetadata(name) {
180
180
  const data = JSON.parse(content);
181
181
  if (!data) return null;
182
182
 
183
- // Normalization for legacy metadata format
184
183
  const command = data.command || data.node || process.execPath;
185
184
  const args = data.args || (data.script ? [data.script] : []);
186
- const runtime = data.runtime || 'node';
185
+ let runtime = data.runtime;
186
+ if (!runtime || (runtime === 'node' && data.command)) {
187
+ const baseCmd = path.basename(command).toLowerCase();
188
+ if (baseCmd !== 'node' && baseCmd !== 'nodejs') {
189
+ runtime = 'custom';
190
+ } else {
191
+ runtime = 'node';
192
+ }
193
+ }
187
194
  const script = data.script || args[0] || command;
188
195
  const cwd = data.cwd || (script ? path.dirname(script) : process.cwd());
189
196
 
@@ -302,6 +309,31 @@ export function resolveAbsolutePath(filepath, baseDir = process.cwd()) {
302
309
  return path.resolve(baseDir, p);
303
310
  }
304
311
 
312
+ /**
313
+ * Safely resolves the working directory for a service or schedule unit.
314
+ *
315
+ * @param {Object} [opts]
316
+ * @returns {string}
317
+ */
318
+ export function resolveWorkingDirectory(opts = {}) {
319
+ if (opts.cwd) {
320
+ return resolveAbsolutePath(opts.cwd);
321
+ }
322
+ if (opts.script) {
323
+ return path.dirname(resolveAbsolutePath(opts.script));
324
+ }
325
+ try {
326
+ const cwd = process.cwd();
327
+ if (fs.existsSync(cwd)) {
328
+ fs.accessSync(cwd, fs.constants.R_OK | fs.constants.X_OK);
329
+ return cwd;
330
+ }
331
+ } catch {
332
+ // fallback if process.cwd() is inaccessible
333
+ }
334
+ return os.homedir();
335
+ }
336
+
305
337
  /**
306
338
  * Escapes values for systemd Environment= line entries.
307
339
  * E.g. KEY="val with spaces"
@@ -447,7 +479,7 @@ export function saveScheduleMetadata(meta) {
447
479
  name: safeName,
448
480
  group: meta.group || 'default',
449
481
  type: 'timer',
450
- runtime: meta.runtime || 'node',
482
+ runtime: meta.runtime || (meta.command ? 'custom' : 'node'),
451
483
  command,
452
484
  args,
453
485
  cwd: meta.cwd ? resolveAbsolutePath(meta.cwd) : process.cwd(),
@@ -477,7 +509,17 @@ export function readScheduleMetadata(name) {
477
509
  const filePath = getScheduleMetadataPath(name);
478
510
  if (!fs.existsSync(filePath)) return null;
479
511
  const content = fs.readFileSync(filePath, 'utf8');
480
- return JSON.parse(content);
512
+ const meta = JSON.parse(content);
513
+ if (!meta) return null;
514
+
515
+ // Normalization for legacy metadata format where runtime defaulted to 'node' for custom commands
516
+ if (meta.runtime === 'node' && meta.command) {
517
+ const baseCmd = path.basename(meta.command).toLowerCase();
518
+ if (baseCmd !== 'node' && baseCmd !== 'nodejs') {
519
+ meta.runtime = 'custom';
520
+ }
521
+ }
522
+ return meta;
481
523
  } catch {
482
524
  return null;
483
525
  }