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.
@@ -1,14 +1,15 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
 
4
4
  export async function createNativeAdapter(opts = {}) {
5
+ const baseDir = opts.cwd || process.cwd();
5
6
  const targetPath = opts.script || opts.command;
6
7
 
7
8
  if (!targetPath) {
8
9
  throw new Error('Native executable path is required.');
9
10
  }
10
11
 
11
- const absPath = path.resolve(process.cwd(), targetPath);
12
+ const absPath = path.resolve(baseDir, targetPath);
12
13
 
13
14
  if (!fs.existsSync(absPath)) {
14
15
  throw new Error(`Executable file does not exist: ${absPath}`);
@@ -22,9 +23,7 @@ export async function createNativeAdapter(opts = {}) {
22
23
  try {
23
24
  fs.accessSync(absPath, fs.constants.X_OK);
24
25
  } catch {
25
- throw new Error(
26
- `The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`
27
- );
26
+ throw new Error(`The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`);
28
27
  }
29
28
 
30
29
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
@@ -1,13 +1,14 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createNodeAdapter(opts = {}) {
6
+ const baseDir = opts.cwd || process.cwd();
6
7
  const customPath = opts.command || opts.nodePath;
7
8
  let execPath = null;
8
9
 
9
10
  if (customPath) {
10
- const absPath = path.resolve(process.cwd(), customPath);
11
+ const absPath = path.resolve(baseDir, customPath);
11
12
  if (fs.existsSync(absPath)) {
12
13
  try {
13
14
  fs.accessSync(absPath, fs.constants.X_OK);
@@ -38,7 +39,7 @@ export async function createNodeAdapter(opts = {}) {
38
39
  }
39
40
 
40
41
  const version = await getBinaryVersion(execPath, '-v');
41
- const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
42
+ const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
42
43
  const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
43
44
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
44
45
 
@@ -1,13 +1,14 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createPhpAdapter(opts = {}) {
6
+ const baseDir = opts.cwd || process.cwd();
6
7
  const customPath = opts.command;
7
8
  let execPath = null;
8
9
 
9
10
  if (customPath) {
10
- const absPath = path.resolve(process.cwd(), customPath);
11
+ const absPath = path.resolve(baseDir, customPath);
11
12
  if (fs.existsSync(absPath)) {
12
13
  try {
13
14
  fs.accessSync(absPath, fs.constants.X_OK);
@@ -27,7 +28,7 @@ export async function createPhpAdapter(opts = {}) {
27
28
  }
28
29
 
29
30
  const version = await getBinaryVersion(execPath, '-v');
30
- const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
31
+ const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
31
32
  const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
32
33
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
33
34
 
@@ -1,13 +1,14 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createPythonAdapter(opts = {}) {
6
+ const baseDir = opts.cwd || process.cwd();
6
7
  const customPath = opts.command;
7
8
  let execPath = null;
8
9
 
9
10
  if (customPath) {
10
- const absPath = path.resolve(process.cwd(), customPath);
11
+ const absPath = path.resolve(baseDir, customPath);
11
12
  if (fs.existsSync(absPath)) {
12
13
  try {
13
14
  fs.accessSync(absPath, fs.constants.X_OK);
@@ -27,7 +28,7 @@ export async function createPythonAdapter(opts = {}) {
27
28
  }
28
29
 
29
30
  const version = await getBinaryVersion(execPath, '--version');
30
- const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
31
+ const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
31
32
  const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
32
33
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
33
34
 
@@ -1,13 +1,14 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createRubyAdapter(opts = {}) {
6
+ const baseDir = opts.cwd || process.cwd();
6
7
  const customPath = opts.command;
7
8
  let execPath = null;
8
9
 
9
10
  if (customPath) {
10
- const absPath = path.resolve(process.cwd(), customPath);
11
+ const absPath = path.resolve(baseDir, customPath);
11
12
  if (fs.existsSync(absPath)) {
12
13
  try {
13
14
  fs.accessSync(absPath, fs.constants.X_OK);
@@ -27,7 +28,7 @@ export async function createRubyAdapter(opts = {}) {
27
28
  }
28
29
 
29
30
  const version = await getBinaryVersion(execPath, '-v');
30
- const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
31
+ const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
31
32
  const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
32
33
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
33
34
 
@@ -1,13 +1,14 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
3
3
  import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
4
 
5
5
  export async function createShellAdapter(opts = {}) {
6
+ const baseDir = opts.cwd || process.cwd();
6
7
  const customPath = opts.command;
7
8
  let execPath = null;
8
9
 
9
10
  if (customPath) {
10
- const absPath = path.resolve(process.cwd(), customPath);
11
+ const absPath = path.resolve(baseDir, customPath);
11
12
  if (fs.existsSync(absPath)) {
12
13
  try {
13
14
  fs.accessSync(absPath, fs.constants.X_OK);
@@ -27,7 +28,7 @@ export async function createShellAdapter(opts = {}) {
27
28
  }
28
29
 
29
30
  const version = await getBinaryVersion(execPath, '--version');
30
- const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
31
+ const scriptPath = opts.script ? path.resolve(baseDir, opts.script) : null;
31
32
  const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
32
33
  const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
33
34
 
package/src/schedule.js CHANGED
@@ -1,31 +1,31 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs';
3
2
  import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
5
+ import { runCommand } from './systemd.js';
4
6
  import {
5
- sanitizeServiceName,
6
- getUnitFilename,
7
+ deleteScheduleUnitFiles,
8
+ getTimerPath,
9
+ getUnitPath,
10
+ getUserUnitDir,
11
+ timerFileExists,
12
+ unitFileExists,
13
+ writeScheduleUnitFiles
14
+ } from './unit.js';
15
+ import {
16
+ deleteScheduleMetadata,
17
+ formatFutureTime,
18
+ formatRelativeTime,
19
+ getSchedulesDir,
7
20
  getTimerFilename,
21
+ getUnitFilename,
22
+ readScheduleMetadata,
8
23
  resolveAbsolutePath,
9
24
  resolveWorkingDirectory,
10
- validateDuration,
11
- formatFutureTime,
12
- formatRelativeTime,
25
+ sanitizeServiceName,
13
26
  saveScheduleMetadata,
14
- readScheduleMetadata,
15
- deleteScheduleMetadata,
16
- getSchedulesDir
27
+ validateDuration
17
28
  } from './utils.js';
18
- import {
19
- getUserUnitDir,
20
- getUnitPath,
21
- getTimerPath,
22
- timerFileExists,
23
- unitFileExists,
24
- writeScheduleUnitFiles,
25
- deleteScheduleUnitFiles
26
- } from './unit.js';
27
- import { runCommand } from './systemd.js';
28
- import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
29
29
 
30
30
  /**
31
31
  * Validates a systemd calendar expression using systemd-analyze if available.
@@ -53,7 +53,10 @@ export async function validateCalendar(expression) {
53
53
  }
54
54
  // If systemd-analyze binary is missing
55
55
  if (res.code === 127 || (res.stderr && res.stderr.includes('not found'))) {
56
- return { valid: true, warning: 'systemd-analyze is not available; calendar expression could not be fully validated by systemd.' };
56
+ return {
57
+ valid: true,
58
+ warning: 'systemd-analyze is not available; calendar expression could not be fully validated by systemd.'
59
+ };
57
60
  }
58
61
  } catch (err) {
59
62
  if (err.message && err.message.startsWith('Invalid calendar expression')) {
@@ -360,7 +363,7 @@ export async function getScheduleStatus(name) {
360
363
  }
361
364
 
362
365
  // Parse NextElapse timestamp
363
- let rawNext = timerProps.NextElapseUSecRealtime || timerProps.NextElapseUSec;
366
+ const rawNext = timerProps.NextElapseUSecRealtime || timerProps.NextElapseUSec;
364
367
  let nextRun = 'n/a';
365
368
 
366
369
  if (rawNext && rawNext !== '0' && rawNext !== '18446744073709551615') {
@@ -370,7 +373,11 @@ export async function getScheduleStatus(name) {
370
373
  } else {
371
374
  nextRun = formatFutureTime(rawNext);
372
375
  }
373
- } else if (timerProps.NextElapseUSecMonotonic && timerProps.NextElapseUSecMonotonic !== '0' && timerProps.NextElapseUSecMonotonic !== '18446744073709551615') {
376
+ } else if (
377
+ timerProps.NextElapseUSecMonotonic &&
378
+ timerProps.NextElapseUSecMonotonic !== '0' &&
379
+ timerProps.NextElapseUSecMonotonic !== '18446744073709551615'
380
+ ) {
374
381
  const monoNum = Number(timerProps.NextElapseUSecMonotonic);
375
382
  if (!isNaN(monoNum) && monoNum > 0) {
376
383
  const currentMonoUSec = os.uptime() * 1000000;
@@ -385,7 +392,7 @@ export async function getScheduleStatus(name) {
385
392
  }
386
393
 
387
394
  // Parse LastTrigger timestamp
388
- let rawLast = timerProps.LastTriggerUSec;
395
+ const rawLast = timerProps.LastTriggerUSec;
389
396
  let lastRun = 'never';
390
397
  if (rawLast && rawLast !== '0' && rawLast !== 'n/a') {
391
398
  const num = Number(rawLast);
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ findProjectConfig,
5
+ getConfigFilepath,
6
+ getUnitupDir,
7
+ readGlobalConfig,
8
+ readProjectConfig,
9
+ saveGlobalConfig,
10
+ saveProjectConfig
11
+ } from '../utils.js';
12
+
13
+ export {
14
+ findProjectConfig,
15
+ readProjectConfig,
16
+ saveProjectConfig,
17
+ readGlobalConfig,
18
+ saveGlobalConfig,
19
+ getConfigFilepath,
20
+ getUnitupDir
21
+ };
22
+
23
+ /**
24
+ * Merges raw CLI options with any found project configuration.
25
+ *
26
+ * @param {object} opts
27
+ * @returns {object} Merged options
28
+ */
29
+ export function mergeConfigWithOptions(opts = {}) {
30
+ const targetCwd = opts.cwd ? path.resolve(process.cwd(), opts.cwd) : process.cwd();
31
+ const configPath = opts.config ? path.resolve(targetCwd, opts.config) : findProjectConfig(targetCwd);
32
+ const projectCfg = configPath ? readProjectConfig(configPath) : null;
33
+
34
+ if (!projectCfg) {
35
+ return { ...opts, cwd: targetCwd };
36
+ }
37
+
38
+ return {
39
+ name: opts.name || projectCfg.name,
40
+ group: opts.group || projectCfg.group || 'default',
41
+ script: opts.script || projectCfg.script,
42
+ command: opts.command || projectCfg.command,
43
+ runtime: opts.runtime || projectCfg.runtime,
44
+ runtimeArgs: opts.runtimeArgs && opts.runtimeArgs.length > 0 ? opts.runtimeArgs : projectCfg.runtimeArgs || [],
45
+ args: opts.args && opts.args.length > 0 ? opts.args : projectCfg.args || [],
46
+ envFile: opts.envFile || projectCfg.envFile,
47
+ restart: opts.restart && opts.restart !== 'on-failure' ? opts.restart : projectCfg.restart || 'on-failure',
48
+ memoryHigh: opts.memoryHigh || projectCfg?.resources?.memoryHigh || projectCfg?.memoryHigh || '',
49
+ memoryMax: opts.memoryMax || projectCfg?.resources?.memoryMax || projectCfg?.memoryMax || '',
50
+ memorySwapMax: opts.memorySwapMax || projectCfg?.resources?.memorySwapMax || projectCfg?.memorySwapMax || '',
51
+ ...opts,
52
+ env: { ...(projectCfg.env || {}), ...(opts.env || {}) },
53
+ cwd: targetCwd
54
+ };
55
+ }
@@ -0,0 +1,233 @@
1
+ import { getAdapter } from '../platform/index.js';
2
+ import { readAppMetadata, readScheduleMetadata, sanitizeServiceName } from '../utils.js';
3
+ import { mergeConfigWithOptions } from './config.js';
4
+ import { normalizeServiceConfig } from './normalize.js';
5
+
6
+ /**
7
+ * ServiceManager orchestrates service operations across platforms.
8
+ */
9
+ export class ServiceManager {
10
+ /**
11
+ * @param {object} [opts]
12
+ * @param {string} [opts.platform]
13
+ * @param {import('../platform/base.js').ServiceAdapter} [opts.adapter]
14
+ */
15
+ constructor(opts = {}) {
16
+ this.platformName = opts.platform;
17
+ this._adapter = opts.adapter;
18
+ }
19
+
20
+ get adapter() {
21
+ return this._adapter || getAdapter(this.platformName);
22
+ }
23
+
24
+ /**
25
+ * Gets the platform capabilities.
26
+ */
27
+ get capabilities() {
28
+ return this.adapter.capabilities;
29
+ }
30
+
31
+ /**
32
+ * Generates the native service file/config from options (dry-run mode).
33
+ *
34
+ * @param {object} rawOpts
35
+ * @returns {Promise<string|object>}
36
+ */
37
+ async generate(rawOpts = {}) {
38
+ const merged = mergeConfigWithOptions(rawOpts);
39
+ const normalized = await normalizeServiceConfig(merged);
40
+ return this.adapter.generateService(normalized);
41
+ }
42
+
43
+ /**
44
+ * Installs/registers a service.
45
+ *
46
+ * @param {object} rawOpts
47
+ * @returns {Promise<object>}
48
+ */
49
+ async install(rawOpts = {}) {
50
+ const merged = mergeConfigWithOptions(rawOpts);
51
+ const normalized = await normalizeServiceConfig(merged);
52
+ return this.adapter.install(normalized, {
53
+ force: !!rawOpts.force,
54
+ start: !!rawOpts.start,
55
+ system: !!rawOpts.system
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Uninstalls/removes a service.
61
+ *
62
+ * @param {string} name
63
+ * @param {object} [options]
64
+ * @returns {Promise<boolean>}
65
+ */
66
+ async uninstall(name, options = {}) {
67
+ if (name && typeof name === 'string' && name.startsWith('@')) {
68
+ const all = await this.list({ group: name });
69
+ if (all.length === 0) {
70
+ throw new Error(`No services found in group "${name}".`);
71
+ }
72
+ for (const item of all) {
73
+ await this.uninstall(item.name, options);
74
+ }
75
+ return true;
76
+ }
77
+ return this.adapter.uninstall(name, options);
78
+ }
79
+
80
+ /**
81
+ * Starts a service.
82
+ *
83
+ * @param {string} name
84
+ * @param {object} [options]
85
+ * @returns {Promise<boolean>}
86
+ */
87
+ async start(name, options = {}) {
88
+ if (name && typeof name === 'string' && name.startsWith('@')) {
89
+ const all = await this.list({ group: name });
90
+ if (all.length === 0) {
91
+ throw new Error(`No services found in group "${name}".`);
92
+ }
93
+ for (const item of all) {
94
+ await this.start(item.name, options);
95
+ }
96
+ return true;
97
+ }
98
+ return this.adapter.start(name, options);
99
+ }
100
+
101
+ /**
102
+ * Stops a service.
103
+ *
104
+ * @param {string} name
105
+ * @param {object} [options]
106
+ * @returns {Promise<boolean>}
107
+ */
108
+ async stop(name, options = {}) {
109
+ if (name && typeof name === 'string' && name.startsWith('@')) {
110
+ const all = await this.list({ group: name });
111
+ if (all.length === 0) {
112
+ throw new Error(`No services found in group "${name}".`);
113
+ }
114
+ for (const item of all) {
115
+ await this.stop(item.name, options);
116
+ }
117
+ return true;
118
+ }
119
+ return this.adapter.stop(name, options);
120
+ }
121
+
122
+ /**
123
+ * Restarts a service.
124
+ *
125
+ * @param {string} name
126
+ * @param {object} [options]
127
+ * @returns {Promise<boolean>}
128
+ */
129
+ async restart(name, options = {}) {
130
+ if (name && typeof name === 'string' && name.startsWith('@')) {
131
+ const all = await this.list({ group: name });
132
+ if (all.length === 0) {
133
+ throw new Error(`No services found in group "${name}".`);
134
+ }
135
+ for (const item of all) {
136
+ await this.restart(item.name, options);
137
+ }
138
+ return true;
139
+ }
140
+ return this.adapter.restart(name, options);
141
+ }
142
+
143
+ /**
144
+ * Enables a service for automatic startup.
145
+ *
146
+ * @param {string} name
147
+ * @param {object} [options]
148
+ * @returns {Promise<boolean>}
149
+ */
150
+ async enable(name, options = {}) {
151
+ return this.adapter.enable(name, options);
152
+ }
153
+
154
+ /**
155
+ * Disables automatic startup for a service.
156
+ *
157
+ * @param {string} name
158
+ * @param {object} [options]
159
+ * @returns {Promise<boolean>}
160
+ */
161
+ async disable(name, options = {}) {
162
+ return this.adapter.disable(name, options);
163
+ }
164
+
165
+ /**
166
+ * Gets normalized status for a service.
167
+ *
168
+ * @param {string} name
169
+ * @param {object} [options]
170
+ * @returns {Promise<object>}
171
+ */
172
+ async status(name, options = {}) {
173
+ return this.adapter.status(name, options);
174
+ }
175
+
176
+ /**
177
+ * Inspects detailed configuration and runtime info for a service.
178
+ *
179
+ * @param {string} name
180
+ * @param {object} [options]
181
+ * @returns {Promise<object>}
182
+ */
183
+ async inspect(name, options = {}) {
184
+ return this.adapter.inspect(name, options);
185
+ }
186
+
187
+ /**
188
+ * Retrieves or streams logs for a service.
189
+ *
190
+ * @param {string} name
191
+ * @param {object} [options]
192
+ * @returns {Promise<any>}
193
+ */
194
+ async logs(name, options = {}) {
195
+ return this.adapter.logs(name, options);
196
+ }
197
+
198
+ /**
199
+ * Lists all services.
200
+ *
201
+ * @param {object} [options]
202
+ * @returns {Promise<Array<object>>}
203
+ */
204
+ async list(options = {}) {
205
+ return this.adapter.list(options);
206
+ }
207
+
208
+ /**
209
+ * Lists failed services.
210
+ *
211
+ * @param {object} [options]
212
+ * @returns {Promise<Array<object>>}
213
+ */
214
+ async failures(options = {}) {
215
+ return this.adapter.failures(options);
216
+ }
217
+
218
+ /**
219
+ * Checks if a service is installed.
220
+ *
221
+ * @param {string} name
222
+ * @param {object} [options]
223
+ * @returns {Promise<boolean>|boolean}
224
+ */
225
+ isInstalled(name, options = {}) {
226
+ return this.adapter.isInstalled(name, options);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Default global ServiceManager instance.
232
+ */
233
+ export const defaultManager = new ServiceManager();