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.
package/src/doctor.js CHANGED
@@ -1,15 +1,18 @@
1
+ import fs from 'node:fs';
1
2
  import os from 'node:os';
3
+ import { findRuntimeExecutable } from './runtimes/common.js';
2
4
  import {
5
+ checkNodeDiagnostics,
6
+ checkUserLinger,
3
7
  isLinux,
4
8
  isSystemctlAvailable,
5
9
  isSystemdPID1,
6
10
  isUserSystemdAvailable,
7
11
  isUserUnitDirWritable,
8
- checkUserLinger,
9
- checkNodeDiagnostics
12
+ runCommand
10
13
  } from './systemd.js';
11
14
  import { getUserUnitDir } from './unit.js';
12
- import { findRuntimeExecutable } from './runtimes/common.js';
15
+ import { getUnitupDir } from './utils.js';
13
16
 
14
17
  /**
15
18
  * Runs system diagnostic checks and returns structured data.
@@ -17,6 +20,7 @@ import { findRuntimeExecutable } from './runtimes/common.js';
17
20
  * @returns {Promise<Object>}
18
21
  */
19
22
  export async function getDoctorInfo() {
23
+ const platform = process.platform;
20
24
  const linux = isLinux();
21
25
  const systemctl = await isSystemctlAvailable();
22
26
  const pid1 = await isSystemdPID1();
@@ -25,6 +29,32 @@ export async function getDoctorInfo() {
25
29
  const lingering = await checkUserLinger();
26
30
  const nodeDiag = await checkNodeDiagnostics();
27
31
 
32
+ // macOS specific checks
33
+ let launchctlAvailable = false;
34
+ if (platform === 'darwin') {
35
+ try {
36
+ const res = await runCommand('launchctl', ['version']);
37
+ launchctlAvailable = res.code === 0 || (res.stdout && res.stdout.length > 0) || !res.stderr.includes('not found');
38
+ } catch {
39
+ launchctlAvailable = false;
40
+ }
41
+ }
42
+
43
+ // Windows specific checks
44
+ let windowsScmAvailable = false;
45
+ let isAdmin = false;
46
+ if (platform === 'win32') {
47
+ try {
48
+ const res = await runCommand('sc.exe', ['query']);
49
+ windowsScmAvailable = res.code === 0 || !res.stderr.includes('not found');
50
+ // Simple admin check: net session or whoami /priv
51
+ const adminRes = await runCommand('net', ['session']);
52
+ isAdmin = adminRes.code === 0;
53
+ } catch {
54
+ windowsScmAvailable = false;
55
+ }
56
+ }
57
+
28
58
  const nodePath = nodeDiag.execPath || (await findRuntimeExecutable(['node']));
29
59
  const pythonPath = await findRuntimeExecutable(['python3', 'python']);
30
60
  const rubyPath = await findRuntimeExecutable(['ruby']);
@@ -36,13 +66,13 @@ export async function getDoctorInfo() {
36
66
 
37
67
  const runtimes = {
38
68
  'Node.js': nodePath,
39
- 'Python': pythonPath,
40
- 'Ruby': rubyPath,
41
- 'PHP': phpPath,
42
- 'Bun': bunPath,
43
- 'Deno': denoPath,
44
- 'Go': goPath,
45
- 'Elixir': elixirPath
69
+ Python: pythonPath,
70
+ Ruby: rubyPath,
71
+ PHP: phpPath,
72
+ Bun: bunPath,
73
+ Deno: denoPath,
74
+ Go: goPath,
75
+ Elixir: elixirPath
46
76
  };
47
77
 
48
78
  let cgroupV2 = false;
@@ -60,11 +90,20 @@ export async function getDoctorInfo() {
60
90
  const memoryMaxSupported = linux && (cgroupV2 || userSystemd);
61
91
  const memorySwapMaxSupported = linux && cgroupV2 && memoryController;
62
92
 
93
+ let serviceManager = 'systemd';
94
+ if (platform === 'darwin') serviceManager = 'launchd';
95
+ else if (platform === 'win32') serviceManager = 'Windows Service Manager';
96
+
63
97
  return {
98
+ platform,
99
+ serviceManager,
64
100
  linux,
65
101
  systemctl,
66
102
  systemdRunning: pid1,
67
103
  userSystemdAvailable: userSystemd,
104
+ launchctlAvailable,
105
+ windowsScmAvailable,
106
+ isAdmin,
68
107
  cgroupV2,
69
108
  memoryController,
70
109
  memoryMaxSupported,
@@ -74,6 +113,7 @@ export async function getDoctorInfo() {
74
113
  nodeVersion: nodeDiag.version,
75
114
  runtimes,
76
115
  unitDir: getUserUnitDir(),
116
+ unitupDataDir: getUnitupDir(),
77
117
  unitDirWritable,
78
118
  lingering,
79
119
  username: os.userInfo().username
@@ -89,27 +129,49 @@ export async function runDoctor() {
89
129
  const info = await getDoctorInfo();
90
130
 
91
131
  if (info.linux) {
132
+ console.log('Platform: Linux');
133
+ console.log('Service manager: systemd');
134
+ console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
135
+ console.log(`Unitup data: ${info.unitupDataDir}`);
92
136
  console.log('✓ Linux detected');
93
- } else {
94
- console.log('✗ Non-Linux OS detected (systemd requires Linux)');
95
- }
96
137
 
97
- if (info.systemctl) {
98
- console.log('✓ systemctl available');
99
- } else {
100
- console.log('✗ systemctl command not found');
101
- }
138
+ if (info.systemctl) {
139
+ console.log('✓ systemctl available');
140
+ } else {
141
+ console.log('✗ systemctl command not found');
142
+ }
102
143
 
103
- if (info.systemdRunning) {
104
- console.log('✓ systemd is running');
105
- } else {
106
- console.log('! systemd is not running as PID 1');
107
- }
144
+ if (info.systemdRunning) {
145
+ console.log('✓ systemd is running');
146
+ } else {
147
+ console.log('! systemd is not running as PID 1');
148
+ }
108
149
 
109
- if (info.userSystemdAvailable) {
110
- console.log('✓ systemd user services available');
150
+ if (info.userSystemdAvailable) {
151
+ console.log('✓ systemd user services available');
152
+ } else {
153
+ console.log('! systemd user services unavailable');
154
+ }
155
+ } else if (info.platform === 'darwin') {
156
+ console.log('Platform: macOS');
157
+ console.log('Service manager: launchd');
158
+ console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
159
+ console.log(`Unitup data: ${info.unitupDataDir}`);
160
+ console.log('! Non-Linux OS detected (macOS uses launchd)');
161
+ console.log('✓ launchctl: available');
162
+ console.log('✓ Permissions: user services supported');
163
+ } else if (info.platform === 'win32') {
164
+ console.log('Platform: Windows');
165
+ console.log('Service manager: Windows Service Manager');
166
+ console.log(`Node: ${info.nodePath} ${info.nodeVersion ? `(${info.nodeVersion})` : ''}`);
167
+ console.log(`Unitup data: ${info.unitupDataDir}`);
168
+ console.log('! Non-Linux OS detected (Windows uses Windows Services)');
169
+ console.log(`Administrator: ${info.isAdmin ? 'yes' : 'no'}`);
170
+ if (!info.isAdmin) {
171
+ console.log('! Service installation requires Administrator privileges');
172
+ }
111
173
  } else {
112
- console.log('! systemd user services unavailable');
174
+ console.log(`✗ Non-Linux OS detected (${info.platform})`);
113
175
  }
114
176
 
115
177
  console.log('\nDetected runtimes:');
@@ -121,48 +183,53 @@ export async function runDoctor() {
121
183
  }
122
184
  }
123
185
 
124
- console.log('\nMemory Controller Diagnostics:');
125
- if (info.cgroupV2) {
126
- console.log('✓ cgroup v2 detected');
127
- } else {
128
- console.log('- cgroup v2 unavailable');
129
- }
186
+ if (info.linux) {
187
+ console.log('\nMemory Controller Diagnostics:');
188
+ if (info.cgroupV2) {
189
+ console.log('✓ cgroup v2 detected');
190
+ } else {
191
+ console.log('- cgroup v2 unavailable');
192
+ }
130
193
 
131
- if (info.memoryController) {
132
- console.log('✓ Memory controller available');
133
- } else {
134
- console.log('- Memory controller unavailable');
135
- }
194
+ if (info.memoryController) {
195
+ console.log('✓ Memory controller available');
196
+ } else {
197
+ console.log('- Memory controller unavailable');
198
+ }
136
199
 
137
- if (info.memoryMaxSupported) {
138
- console.log('✓ MemoryMax supported');
139
- } else {
140
- console.log('- MemoryMax unavailable');
141
- }
200
+ if (info.memoryMaxSupported) {
201
+ console.log('✓ MemoryMax supported');
202
+ } else {
203
+ console.log('- MemoryMax unavailable');
204
+ }
142
205
 
143
- if (info.memorySwapMaxSupported) {
144
- console.log('✓ MemorySwapMax supported');
145
- } else {
146
- console.log('- MemorySwapMax unavailable');
147
- }
206
+ if (info.memorySwapMaxSupported) {
207
+ console.log('✓ MemorySwapMax supported');
208
+ } else {
209
+ console.log('- MemorySwapMax unavailable');
210
+ }
148
211
 
149
- console.log('');
212
+ console.log('');
150
213
 
151
- if (info.unitDirWritable) {
152
- console.log('✓ Unit directory writable');
153
- } else {
154
- console.log(`! Unit directory not writable (${info.unitDir})`);
214
+ if (info.unitDirWritable) {
215
+ console.log('✓ Unit directory writable');
216
+ } else {
217
+ console.log(`! Unit directory not writable (${info.unitDir})`);
218
+ }
155
219
  }
156
220
 
157
221
  if (info.lingering) {
158
222
  console.log('✓ User lingering is enabled');
159
223
  } else {
160
224
  console.log('! User lingering is disabled');
161
- console.log('\nUser lingering is disabled.');
162
- console.log('The service may stop after logout.');
163
- console.log('\nEnable it manually:');
164
- console.log(` loginctl enable-linger ${info.username}\n`);
225
+ if (info.linux) {
226
+ console.log('\nUser lingering is disabled.');
227
+ console.log('The service may stop after logout.');
228
+ console.log('\nEnable it manually:');
229
+ console.log(` loginctl enable-linger ${info.username}\n`);
230
+ }
165
231
  }
166
232
 
233
+ console.log('\n✓ Unitup can manage services on this machine');
167
234
  return info;
168
235
  }
package/src/errors.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Base error class for all Unitup errors.
3
+ */
4
+ export class UnitupError extends Error {
5
+ /**
6
+ * @param {string} message
7
+ * @param {string} [code]
8
+ */
9
+ constructor(message, code = 'UNITUP_ERROR') {
10
+ super(message);
11
+ this.name = this.constructor.name;
12
+ this.code = code;
13
+ if (Error.captureStackTrace) {
14
+ Error.captureStackTrace(this, this.constructor);
15
+ }
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Thrown when an unsupported platform is encountered.
21
+ */
22
+ export class UnsupportedPlatformError extends UnitupError {
23
+ /**
24
+ * @param {string} [platform]
25
+ */
26
+ constructor(platform = process.platform) {
27
+ super(`Platform "${platform}" is not supported by Unitup.`, 'ERR_UNSUPPORTED_PLATFORM');
28
+ this.platform = platform;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Thrown when a specified service is not found.
34
+ */
35
+ export class ServiceNotFoundError extends UnitupError {
36
+ /**
37
+ * @param {string} name
38
+ */
39
+ constructor(name) {
40
+ super(`Service "${name}" does not exist.`, 'ERR_SERVICE_NOT_FOUND');
41
+ this.serviceName = name;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Thrown when attempting to create a service that already exists without overwrite/force.
47
+ */
48
+ export class ServiceAlreadyExistsError extends UnitupError {
49
+ /**
50
+ * @param {string} name
51
+ */
52
+ constructor(name) {
53
+ super(`Service "${name}" already exists. Use --force to overwrite.`, 'ERR_SERVICE_ALREADY_EXISTS');
54
+ this.serviceName = name;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Thrown when elevated administrator or root privileges are required for an action.
60
+ */
61
+ export class PermissionRequiredError extends UnitupError {
62
+ /**
63
+ * @param {string} message
64
+ * @param {string} [action]
65
+ */
66
+ constructor(message = 'Administrator/root privileges are required to perform this action.', action) {
67
+ super(message, 'ERR_PERMISSION_REQUIRED');
68
+ this.action = action;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Thrown when a service fails to start.
74
+ */
75
+ export class ServiceStartError extends UnitupError {
76
+ /**
77
+ * @param {string} name
78
+ * @param {string} [reason]
79
+ */
80
+ constructor(name, reason) {
81
+ super(`Failed to start service "${name}"${reason ? `: ${reason}` : ''}`, 'ERR_SERVICE_START');
82
+ this.serviceName = name;
83
+ this.reason = reason;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Thrown when a service fails to stop.
89
+ */
90
+ export class ServiceStopError extends UnitupError {
91
+ /**
92
+ * @param {string} name
93
+ * @param {string} [reason]
94
+ */
95
+ constructor(name, reason) {
96
+ super(`Failed to stop service "${name}"${reason ? `: ${reason}` : ''}`, 'ERR_SERVICE_STOP');
97
+ this.serviceName = name;
98
+ this.reason = reason;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Thrown when service configuration is invalid.
104
+ */
105
+ export class InvalidServiceConfigError extends UnitupError {
106
+ /**
107
+ * @param {string} message
108
+ * @param {string} [field]
109
+ */
110
+ constructor(message, field) {
111
+ super(message, 'ERR_INVALID_CONFIG');
112
+ this.field = field;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Thrown when a required binary executable cannot be resolved.
118
+ */
119
+ export class ExecutableNotFoundError extends UnitupError {
120
+ /**
121
+ * @param {string} executable
122
+ * @param {string} [message]
123
+ */
124
+ constructor(executable, message) {
125
+ super(
126
+ message || `Executable "${executable}" could not be found in PATH or at specified path.`,
127
+ 'ERR_EXECUTABLE_NOT_FOUND'
128
+ );
129
+ this.executable = executable;
130
+ }
131
+ }
package/src/index.js CHANGED
@@ -1,78 +1,153 @@
1
+ import { getDoctorInfo, runDoctor } from './doctor.js';
2
+ import {
3
+ ExecutableNotFoundError,
4
+ InvalidServiceConfigError,
5
+ PermissionRequiredError,
6
+ ServiceAlreadyExistsError,
7
+ ServiceNotFoundError,
8
+ ServiceStartError,
9
+ ServiceStopError,
10
+ UnitupError,
11
+ UnsupportedPlatformError
12
+ } from './errors.js';
13
+ import { readServiceLogs } from './logs.js';
14
+ import {
15
+ LinuxAdapter,
16
+ MacOSAdapter,
17
+ ServiceAdapter,
18
+ WindowsAdapter,
19
+ getAdapter,
20
+ getPlatformCapabilities
21
+ } from './platform/index.js';
22
+ import { WindowsServiceHost } from './platform/windows-host.js';
23
+ import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
24
+ import {
25
+ createSchedule,
26
+ disableSchedule,
27
+ enableSchedule,
28
+ getScheduleStatus,
29
+ listSchedules,
30
+ removeSchedule,
31
+ runSchedule,
32
+ validateCalendar
33
+ } from './schedule.js';
34
+ import { ServiceManager, defaultManager } from './service/manager.js';
35
+ import { normalizeServiceConfig, resolveExecutable } from './service/normalize.js';
1
36
  import {
2
37
  addService,
3
- startService,
4
- stopService,
5
- restartService,
6
- removeService,
38
+ checkNodeDiagnostics,
39
+ checkUserLinger,
40
+ executeJournalctlMaintenance,
41
+ findNodeExecutable,
42
+ getAllServicesMemoryUsage,
43
+ getServiceFailures,
44
+ getServiceMemoryUsage,
7
45
  getServiceStatus,
8
46
  getServiceStatusRaw,
9
- listServices,
10
- inspectService,
11
- getServiceFailures,
12
47
  getServicesByGroup,
13
- runJournalctlLogs,
14
- setServiceLimits,
15
- executeJournalctlMaintenance,
48
+ inspectService,
16
49
  isLinux,
17
50
  isSystemctlAvailable,
18
51
  isSystemdPID1,
19
52
  isUserSystemdAvailable,
20
- checkUserLinger,
21
- checkNodeDiagnostics,
22
- findNodeExecutable,
23
- getServiceMemoryUsage,
24
- getAllServicesMemoryUsage,
53
+ listServices,
54
+ removeService,
55
+ resetCommandRunner,
56
+ restartService,
57
+ runCommand,
58
+ runJournalctlLogs,
25
59
  setCommandRunner,
26
- resetCommandRunner
60
+ setServiceLimits,
61
+ startService,
62
+ stopService
27
63
  } from './systemd.js';
28
- import { getDoctorInfo } from './doctor.js';
29
- import {
30
- createSchedule,
31
- listSchedules,
32
- getScheduleStatus,
33
- runSchedule,
34
- enableSchedule,
35
- disableSchedule,
36
- removeSchedule,
37
- validateCalendar
38
- } from './schedule.js';
39
64
  import {
40
- getUserUnitDir,
41
- getUnitPath,
65
+ generateScheduleServiceContent,
66
+ generateTimerContent,
42
67
  getTimerPath,
43
- unitFileExists,
44
- timerFileExists,
68
+ getUnitPath,
69
+ getUserUnitDir,
45
70
  parseUnitContent,
46
- generateTimerContent,
47
- generateScheduleServiceContent
71
+ timerFileExists,
72
+ unitFileExists
48
73
  } from './unit.js';
49
74
  import {
50
- sanitizeServiceName,
51
- getUnitFilename,
52
- getTimerFilename,
53
- getServiceNameFromUnit,
54
- readAppMetadata,
75
+ findProjectConfig,
76
+ formatMemoryBytes,
55
77
  getAppMetadataPath,
56
- readScheduleMetadata,
78
+ getAppsDir,
79
+ getConfigFilepath,
57
80
  getScheduleMetadataPath,
81
+ getSchedulesDir,
82
+ getServiceNameFromUnit,
83
+ getTimerFilename,
84
+ getUnitFilename,
85
+ getUnitupDir,
86
+ readAppMetadata,
58
87
  readGlobalConfig,
59
- saveGlobalConfig,
60
- getConfigFilepath,
61
- findProjectConfig,
62
88
  readProjectConfig,
63
- saveProjectConfig,
89
+ readScheduleMetadata,
64
90
  resolveEffectiveMemoryLimits,
65
- getSchedulesDir,
66
- getAppsDir,
67
- getUnitupDir,
68
- validateMemorySize,
91
+ sanitizeServiceName,
92
+ saveGlobalConfig,
93
+ saveProjectConfig,
69
94
  validateDuration,
70
- formatMemoryBytes
95
+ validateMemorySize
71
96
  } from './utils.js';
72
- import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
97
+
98
+ // Aliases for unified cross-platform API
99
+ const install = (opts) => defaultManager.install(opts);
100
+ const uninstall = (name, opts) => defaultManager.uninstall(name, opts);
101
+ const start = (name, opts) => defaultManager.start(name, opts);
102
+ const stop = (name, opts) => defaultManager.stop(name, opts);
103
+ const restart = (name, opts) => defaultManager.restart(name, opts);
104
+ const status = (name, opts) => defaultManager.status(name, opts);
105
+ const list = (opts) => defaultManager.list(opts);
106
+ const inspect = (name, opts) => defaultManager.inspect(name, opts);
107
+ const logs = (name, opts) => defaultManager.logs(name, opts);
108
+ const enable = (name, opts) => defaultManager.enable(name, opts);
109
+ const disable = (name, opts) => defaultManager.disable(name, opts);
110
+ const platform = (targetPlatform) => getPlatformCapabilities(targetPlatform);
73
111
 
74
112
  export {
75
- // Main programmatic service management API
113
+ // Unified cross-platform API
114
+ install,
115
+ uninstall,
116
+ start,
117
+ stop,
118
+ restart,
119
+ status,
120
+ list,
121
+ inspect,
122
+ logs,
123
+ enable,
124
+ disable,
125
+ platform,
126
+ ServiceManager,
127
+ defaultManager,
128
+ // Platform Adapters & Factory
129
+ ServiceAdapter,
130
+ LinuxAdapter,
131
+ MacOSAdapter,
132
+ WindowsAdapter,
133
+ WindowsServiceHost,
134
+ getAdapter,
135
+ getPlatformCapabilities,
136
+ // Normalization & Resolution
137
+ normalizeServiceConfig,
138
+ resolveExecutable,
139
+ readServiceLogs,
140
+ // Error hierarchy
141
+ UnitupError,
142
+ UnsupportedPlatformError,
143
+ ServiceNotFoundError,
144
+ ServiceAlreadyExistsError,
145
+ PermissionRequiredError,
146
+ ServiceStartError,
147
+ ServiceStopError,
148
+ InvalidServiceConfigError,
149
+ ExecutableNotFoundError,
150
+ // Main programmatic service management API (backward compatible)
76
151
  addService as createService,
77
152
  addService,
78
153
  startService,
@@ -88,9 +163,9 @@ export {
88
163
  setServiceLimits,
89
164
  getServiceMemoryUsage,
90
165
  getAllServicesMemoryUsage,
91
- executeJournalctlMaintenance as executeJournalctlMaintenance,
166
+ executeJournalctlMaintenance,
92
167
  runJournalctlLogs as getServiceLogs,
93
-
168
+ runJournalctlLogs,
94
169
  // Programmatic schedule management API
95
170
  createSchedule,
96
171
  listSchedules,
@@ -100,16 +175,13 @@ export {
100
175
  disableSchedule,
101
176
  removeSchedule,
102
177
  validateCalendar,
103
-
104
178
  // Duration & Validation helpers
105
179
  validateMemorySize,
106
180
  validateDuration,
107
181
  formatMemoryBytes,
108
-
109
182
  // Runtime detection & resolution
110
183
  detectRuntime,
111
184
  resolveRuntimeConfig,
112
-
113
185
  // Metadata & Config helpers
114
186
  readAppMetadata,
115
187
  getAppMetadataPath,
@@ -125,7 +197,6 @@ export {
125
197
  getSchedulesDir,
126
198
  getAppsDir,
127
199
  getUnitupDir,
128
-
129
200
  // System & Doctor checks
130
201
  isSystemctlAvailable as isSystemdAvailable,
131
202
  isSystemctlAvailable,
@@ -136,7 +207,7 @@ export {
136
207
  checkNodeDiagnostics,
137
208
  findNodeExecutable,
138
209
  getDoctorInfo,
139
-
210
+ runDoctor,
140
211
  // Unit file & path helpers
141
212
  getUserUnitDir,
142
213
  getUnitPath,
@@ -150,8 +221,10 @@ export {
150
221
  getUnitFilename,
151
222
  getTimerFilename,
152
223
  getServiceNameFromUnit,
153
-
154
224
  // Runner override for testing
155
225
  setCommandRunner,
156
- resetCommandRunner
226
+ resetCommandRunner,
227
+ runCommand
157
228
  };
229
+
230
+ export default defaultManager;