unitup 0.1.0 → 0.1.2

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.0",
3
+ "version": "0.1.2",
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/schedule.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import {
4
5
  sanitizeServiceName,
5
6
  getUnitFilename,
6
7
  getTimerFilename,
7
8
  resolveAbsolutePath,
9
+ resolveWorkingDirectory,
8
10
  validateDuration,
9
11
  formatFutureTime,
10
12
  formatRelativeTime,
@@ -106,42 +108,25 @@ export async function createSchedule(opts = {}) {
106
108
 
107
109
  const safeName = sanitizeServiceName(rawName);
108
110
 
109
- let commandExec = '';
110
- let execArgs = [];
111
- let detectedRuntime = opts.runtime || 'node';
112
- let scriptPath = '';
113
-
114
- if (opts.command) {
115
- commandExec = resolveAbsolutePath(opts.command);
116
- execArgs = Array.isArray(opts.args) ? opts.args : [];
117
- if (opts.script) {
118
- scriptPath = resolveAbsolutePath(opts.script);
119
- }
120
- } else if (opts.script) {
121
- scriptPath = resolveAbsolutePath(opts.script);
122
- const runtimeInfo = detectRuntime(scriptPath, opts.runtime);
123
- detectedRuntime = runtimeInfo.runtime;
124
-
125
- const resolved = await resolveRuntimeConfig({
126
- runtime: detectedRuntime,
127
- script: scriptPath,
128
- command: opts.command,
129
- args: opts.args || [],
130
- runtimeArgs: opts.runtimeArgs || [],
131
- customNodePath: opts.node
132
- });
133
-
134
- commandExec = resolved.command;
135
- execArgs = resolved.args;
136
- } else {
111
+ if (!opts.script && !opts.command) {
137
112
  throw new Error('Either script or command must be provided to create schedule.');
138
113
  }
139
114
 
140
- const cwd = opts.cwd
141
- ? resolveAbsolutePath(opts.cwd)
142
- : scriptPath
143
- ? path.dirname(scriptPath)
144
- : path.dirname(commandExec);
115
+ const scriptPath = opts.script ? resolveAbsolutePath(opts.script) : '';
116
+ const resolved = await resolveRuntimeConfig({
117
+ runtime: opts.runtime,
118
+ script: scriptPath,
119
+ command: opts.command,
120
+ args: opts.args || [],
121
+ runtimeArgs: opts.runtimeArgs || [],
122
+ customNodePath: opts.node
123
+ });
124
+
125
+ const commandExec = resolved.command;
126
+ const execArgs = resolved.args;
127
+ const detectedRuntime = resolved.runtime;
128
+
129
+ const cwd = resolveWorkingDirectory({ cwd: opts.cwd, script: scriptPath });
145
130
 
146
131
  const envObj = {};
147
132
  if (opts.env) {
@@ -185,6 +170,7 @@ export async function createSchedule(opts = {}) {
185
170
 
186
171
  if (opts.start || opts.enable) {
187
172
  await runCommand('systemctl', ['--user', 'daemon-reload']);
173
+ await runCommand('systemctl', ['--user', 'reset-failed', getUnitFilename(safeName), getTimerFilename(safeName)]);
188
174
  await runCommand('systemctl', ['--user', 'enable', getTimerFilename(safeName)]);
189
175
  await runCommand('systemctl', ['--user', 'start', getTimerFilename(safeName)]);
190
176
  }
@@ -224,6 +210,7 @@ export async function enableSchedule(name) {
224
210
  throw new Error(`Schedule timer unit up for "${name}" does not exist.`);
225
211
  }
226
212
  await runCommand('systemctl', ['--user', 'daemon-reload']);
213
+ await runCommand('systemctl', ['--user', 'reset-failed', getUnitFilename(safeName), getTimerFilename(safeName)]);
227
214
  const res = await runCommand('systemctl', ['--user', 'enable', '--now', getTimerFilename(safeName)]);
228
215
  return res.code === 0;
229
216
  }
@@ -333,7 +320,7 @@ export async function getScheduleStatus(name) {
333
320
  '--user',
334
321
  'show',
335
322
  timerFilename,
336
- '--property=ActiveState,SubState,NextElapseUSecRealtime,NextElapseUSec,LastTriggerUSec,Unit,Triggers,UnitFileState'
323
+ '--property=ActiveState,SubState,NextElapseUSecRealtime,NextElapseUSecMonotonic,NextElapseUSec,LastTriggerUSec,Unit,Triggers,UnitFileState'
337
324
  ]);
338
325
 
339
326
  const showServiceRes = await runCommand('systemctl', [
@@ -374,18 +361,27 @@ export async function getScheduleStatus(name) {
374
361
 
375
362
  // Parse NextElapse timestamp
376
363
  let rawNext = timerProps.NextElapseUSecRealtime || timerProps.NextElapseUSec;
377
- if (!rawNext || rawNext === '0' || rawNext === '18446744073709551615') {
378
- rawNext = 'n/a';
379
- }
380
-
381
364
  let nextRun = 'n/a';
382
- if (rawNext !== 'n/a') {
365
+
366
+ if (rawNext && rawNext !== '0' && rawNext !== '18446744073709551615') {
383
367
  const num = Number(rawNext);
384
368
  if (!isNaN(num) && num > 0) {
385
369
  nextRun = formatFutureTime(Math.floor(num / 1000));
386
370
  } else {
387
371
  nextRun = formatFutureTime(rawNext);
388
372
  }
373
+ } else if (timerProps.NextElapseUSecMonotonic && timerProps.NextElapseUSecMonotonic !== '0' && timerProps.NextElapseUSecMonotonic !== '18446744073709551615') {
374
+ const monoNum = Number(timerProps.NextElapseUSecMonotonic);
375
+ if (!isNaN(monoNum) && monoNum > 0) {
376
+ const currentMonoUSec = os.uptime() * 1000000;
377
+ const diffUSec = monoNum - currentMonoUSec;
378
+ if (diffUSec > 0) {
379
+ const targetMs = Date.now() + Math.floor(diffUSec / 1000);
380
+ nextRun = formatFutureTime(targetMs);
381
+ } else {
382
+ nextRun = 'imminent';
383
+ }
384
+ }
389
385
  }
390
386
 
391
387
  // Parse LastTrigger timestamp
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,8 +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 scriptOrCmdPath = opts.script ? resolveAbsolutePath(opts.script) : (execArgs[0] && (execArgs[0].startsWith('/') || execArgs[0].startsWith('./')) ? resolveAbsolutePath(execArgs[0]) : commandExec);
87
- const cwd = opts.cwd ? resolveAbsolutePath(opts.cwd) : path.dirname(scriptOrCmdPath || commandExec);
87
+ const cwd = resolveWorkingDirectory(opts);
88
88
  const restartPolicy = opts.restart || 'on-failure';
89
89
 
90
90
  const execStartTokens = [commandExec, ...execArgs];
@@ -306,8 +306,7 @@ export function generateScheduleServiceContent(opts) {
306
306
  throw new Error('Either "command" or "script" must be provided to generate scheduled unit file.');
307
307
  }
308
308
 
309
- const scriptOrCmdPath = opts.script ? resolveAbsolutePath(opts.script) : (execArgs[0] && (execArgs[0].startsWith('/') || execArgs[0].startsWith('./')) ? resolveAbsolutePath(execArgs[0]) : commandExec);
310
- const cwd = opts.cwd ? resolveAbsolutePath(opts.cwd) : path.dirname(scriptOrCmdPath || commandExec);
309
+ const cwd = resolveWorkingDirectory(opts);
311
310
 
312
311
  const execStartTokens = [commandExec, ...execArgs];
313
312
  const execStartLine = execStartTokens.map(escapeExecArg).join(' ');
@@ -440,7 +439,7 @@ export function writeScheduleUnitFiles(opts) {
440
439
  saveScheduleMetadata({
441
440
  name: safeName,
442
441
  group: opts.group || 'default',
443
- runtime: opts.runtime || 'node',
442
+ runtime: opts.runtime || (opts.command ? 'custom' : 'node'),
444
443
  command: opts.command,
445
444
  args: opts.args,
446
445
  cwd: opts.cwd,
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(scriptPath || command);
144
+ const cwd = resolveWorkingDirectory(meta);
145
145
 
146
146
  // Extract memory resources if present
147
147
  const resources = meta.resources || {};
@@ -302,6 +302,31 @@ export function resolveAbsolutePath(filepath, baseDir = process.cwd()) {
302
302
  return path.resolve(baseDir, p);
303
303
  }
304
304
 
305
+ /**
306
+ * Safely resolves the working directory for a service or schedule unit.
307
+ *
308
+ * @param {Object} [opts]
309
+ * @returns {string}
310
+ */
311
+ export function resolveWorkingDirectory(opts = {}) {
312
+ if (opts.cwd) {
313
+ return resolveAbsolutePath(opts.cwd);
314
+ }
315
+ if (opts.script) {
316
+ return path.dirname(resolveAbsolutePath(opts.script));
317
+ }
318
+ try {
319
+ const cwd = process.cwd();
320
+ if (fs.existsSync(cwd)) {
321
+ fs.accessSync(cwd, fs.constants.R_OK | fs.constants.X_OK);
322
+ return cwd;
323
+ }
324
+ } catch {
325
+ // fallback if process.cwd() is inaccessible
326
+ }
327
+ return os.homedir();
328
+ }
329
+
305
330
  /**
306
331
  * Escapes values for systemd Environment= line entries.
307
332
  * E.g. KEY="val with spaces"