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/CHANGELOG.md +5 -0
- package/README.md +89 -8
- package/biome.json +0 -1
- package/docs/index.html +188 -234
- package/index.d.ts +336 -445
- package/package.json +9 -3
- package/src/cli.js +187 -101
- package/src/doctor.js +123 -56
- package/src/errors.js +131 -0
- package/src/index.js +132 -59
- package/src/logs.js +148 -0
- package/src/platform/base.js +211 -0
- package/src/platform/index.js +43 -0
- package/src/platform/linux.js +155 -0
- package/src/platform/macos.js +723 -0
- package/src/platform/windows-host.js +246 -0
- package/src/platform/windows.js +495 -0
- package/src/runtimes/bun.js +4 -3
- package/src/runtimes/common.js +24 -6
- package/src/runtimes/deno.js +4 -3
- package/src/runtimes/elixir.js +4 -3
- package/src/runtimes/go.js +4 -3
- package/src/runtimes/index.js +14 -11
- package/src/runtimes/native.js +4 -5
- package/src/runtimes/node.js +4 -3
- package/src/runtimes/php.js +4 -3
- package/src/runtimes/python.js +4 -3
- package/src/runtimes/ruby.js +4 -3
- package/src/runtimes/shell.js +4 -3
- package/src/schedule.js +31 -24
- package/src/service/config.js +55 -0
- package/src/service/manager.js +233 -0
- package/src/service/normalize.js +216 -0
- package/src/systemd.js +84 -38
- package/src/unit.js +12 -18
- package/src/utils.js +32 -18
- package/tsconfig.json +1 -1
- package/types-test/index.ts +158 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
PermissionRequiredError,
|
|
7
|
+
ServiceAlreadyExistsError,
|
|
8
|
+
ServiceNotFoundError,
|
|
9
|
+
ServiceStartError,
|
|
10
|
+
ServiceStopError
|
|
11
|
+
} from '../errors.js';
|
|
12
|
+
import { readServiceLogs } from '../logs.js';
|
|
13
|
+
import {
|
|
14
|
+
deleteAppMetadata,
|
|
15
|
+
formatRelativeTime,
|
|
16
|
+
getAppsDir,
|
|
17
|
+
getUnitupDir,
|
|
18
|
+
readAppMetadata,
|
|
19
|
+
sanitizeServiceName,
|
|
20
|
+
saveAppMetadata
|
|
21
|
+
} from '../utils.js';
|
|
22
|
+
import { ServiceAdapter } from './base.js';
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = path.dirname(__filename);
|
|
26
|
+
const WINDOWS_HOST_PATH = path.resolve(__dirname, 'windows-host.js');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Windows Service Adapter using Windows SCM (sc.exe) and Unitup Windows Service Host.
|
|
30
|
+
*/
|
|
31
|
+
export class WindowsAdapter extends ServiceAdapter {
|
|
32
|
+
get name() {
|
|
33
|
+
return 'windows';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get capabilities() {
|
|
37
|
+
return {
|
|
38
|
+
serviceManager: 'windows',
|
|
39
|
+
supports: {
|
|
40
|
+
install: true,
|
|
41
|
+
uninstall: true,
|
|
42
|
+
start: true,
|
|
43
|
+
stop: true,
|
|
44
|
+
restart: true,
|
|
45
|
+
enable: true,
|
|
46
|
+
disable: true,
|
|
47
|
+
status: true,
|
|
48
|
+
logs: true,
|
|
49
|
+
restartPolicy: true,
|
|
50
|
+
userServices: false,
|
|
51
|
+
systemServices: true,
|
|
52
|
+
memoryLimits: false,
|
|
53
|
+
schedule: false
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns the Windows service name.
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
getServiceName(name) {
|
|
64
|
+
const safeName = sanitizeServiceName(name);
|
|
65
|
+
return `unitup-${safeName}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generates Windows service creation command parameters.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} config - NormalizedServiceConfig
|
|
72
|
+
* @returns {object}
|
|
73
|
+
*/
|
|
74
|
+
generateService(config) {
|
|
75
|
+
const safeName = sanitizeServiceName(config.name);
|
|
76
|
+
const serviceName = this.getServiceName(safeName);
|
|
77
|
+
const nodeExec = process.execPath;
|
|
78
|
+
const binPath = `"${nodeExec}" "${WINDOWS_HOST_PATH}" "${safeName}"`;
|
|
79
|
+
const startType = config.autostart !== false ? 'auto' : 'demand';
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
serviceName,
|
|
83
|
+
displayName: config.displayName && config.displayName !== safeName ? config.displayName : `Unitup - ${safeName}`,
|
|
84
|
+
description: config.description || `unitup service: ${safeName}`,
|
|
85
|
+
binPath,
|
|
86
|
+
startType,
|
|
87
|
+
command: config.command,
|
|
88
|
+
args: config.args,
|
|
89
|
+
cwd: config.cwd,
|
|
90
|
+
env: config.env
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Installs a Windows Service.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} config - NormalizedServiceConfig
|
|
98
|
+
* @param {object} [options]
|
|
99
|
+
* @returns {Promise<object>}
|
|
100
|
+
*/
|
|
101
|
+
async install(config, options = {}) {
|
|
102
|
+
const safeName = sanitizeServiceName(config.name);
|
|
103
|
+
const serviceName = this.getServiceName(safeName);
|
|
104
|
+
const generated = this.generateService(config);
|
|
105
|
+
|
|
106
|
+
const isExisting = await this.isInstalled(safeName);
|
|
107
|
+
let wasRunning = false;
|
|
108
|
+
|
|
109
|
+
if (isExisting) {
|
|
110
|
+
const currentStatus = await this.status(safeName);
|
|
111
|
+
if (currentStatus.state === 'running') {
|
|
112
|
+
wasRunning = true;
|
|
113
|
+
if (!options.force && !config.raw?.force) {
|
|
114
|
+
throw new ServiceAlreadyExistsError(safeName);
|
|
115
|
+
}
|
|
116
|
+
await this.stop(safeName);
|
|
117
|
+
}
|
|
118
|
+
await this.uninstall(safeName, { force: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Save app metadata before creating service
|
|
122
|
+
saveAppMetadata({
|
|
123
|
+
...config,
|
|
124
|
+
name: safeName,
|
|
125
|
+
serviceName,
|
|
126
|
+
hostPath: WINDOWS_HOST_PATH,
|
|
127
|
+
installedAt: new Date().toISOString()
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const scArgs = [
|
|
131
|
+
'create',
|
|
132
|
+
serviceName,
|
|
133
|
+
`binPath= ${generated.binPath}`,
|
|
134
|
+
`displayName= ${generated.displayName}`,
|
|
135
|
+
`start= ${generated.startType}`
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const res = await this.run('sc.exe', scArgs);
|
|
139
|
+
if (res.code !== 0) {
|
|
140
|
+
if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied') || res.code === 5) {
|
|
141
|
+
throw new PermissionRequiredError(
|
|
142
|
+
`Installing a Windows service requires Administrator privileges.\n\nPlease open PowerShell or Command Prompt as Administrator and run:\n unitup install ${config.name}`,
|
|
143
|
+
'install'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Failed to create Windows service "${serviceName}": ${res.stderr || res.stdout}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Set description if supported
|
|
150
|
+
try {
|
|
151
|
+
await this.run('sc.exe', ['description', serviceName, generated.description]);
|
|
152
|
+
} catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options.start || config.raw?.start || wasRunning) {
|
|
157
|
+
await this.start(safeName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
name: safeName,
|
|
162
|
+
serviceName,
|
|
163
|
+
displayName: generated.displayName,
|
|
164
|
+
unitPath: generated.binPath
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Uninstalls a Windows Service.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} name
|
|
172
|
+
* @param {object} [options]
|
|
173
|
+
* @returns {Promise<boolean>}
|
|
174
|
+
*/
|
|
175
|
+
async uninstall(name, options = {}) {
|
|
176
|
+
const safeName = sanitizeServiceName(name);
|
|
177
|
+
const serviceName = this.getServiceName(safeName);
|
|
178
|
+
|
|
179
|
+
const isExisting = await this.isInstalled(safeName);
|
|
180
|
+
if (!isExisting) {
|
|
181
|
+
throw new ServiceNotFoundError(safeName);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const currentStatus = await this.status(safeName);
|
|
185
|
+
if (currentStatus.state === 'running' && !options.force) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Service "${safeName}" is currently running.\n` +
|
|
188
|
+
`Use --force (-f) to remove running services, or stop it first:\n` +
|
|
189
|
+
` unitup stop ${safeName}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (currentStatus.state === 'running') {
|
|
194
|
+
try {
|
|
195
|
+
await this.stop(safeName);
|
|
196
|
+
} catch {
|
|
197
|
+
// ignore
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const res = await this.run('sc.exe', ['delete', serviceName]);
|
|
202
|
+
if (res.code !== 0) {
|
|
203
|
+
if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied') || res.code === 5) {
|
|
204
|
+
throw new PermissionRequiredError(
|
|
205
|
+
`Uninstalling a Windows service requires Administrator privileges.`,
|
|
206
|
+
'uninstall'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Failed to delete Windows service "${serviceName}": ${res.stderr || res.stdout}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
deleteAppMetadata(safeName);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Starts a Windows Service.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} name
|
|
220
|
+
* @param {object} [options]
|
|
221
|
+
* @returns {Promise<boolean>}
|
|
222
|
+
*/
|
|
223
|
+
async start(name, options = {}) {
|
|
224
|
+
const safeName = sanitizeServiceName(name);
|
|
225
|
+
const serviceName = this.getServiceName(safeName);
|
|
226
|
+
|
|
227
|
+
const res = await this.run('sc.exe', ['start', serviceName]);
|
|
228
|
+
if (res.code !== 0) {
|
|
229
|
+
if (res.stdout.includes('already running') || res.stdout.includes('1056')) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied')) {
|
|
233
|
+
throw new PermissionRequiredError('Starting a Windows service requires Administrator privileges.', 'start');
|
|
234
|
+
}
|
|
235
|
+
throw new ServiceStartError(safeName, res.stderr || res.stdout);
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stops a Windows Service.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} name
|
|
244
|
+
* @param {object} [options]
|
|
245
|
+
* @returns {Promise<boolean>}
|
|
246
|
+
*/
|
|
247
|
+
async stop(name, options = {}) {
|
|
248
|
+
const safeName = sanitizeServiceName(name);
|
|
249
|
+
const serviceName = this.getServiceName(safeName);
|
|
250
|
+
|
|
251
|
+
const res = await this.run('sc.exe', ['stop', serviceName]);
|
|
252
|
+
if (res.code !== 0) {
|
|
253
|
+
if (res.stdout.includes('not started') || res.stdout.includes('1062')) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (res.stderr.includes('Access is denied') || res.stdout.includes('Access is denied')) {
|
|
257
|
+
throw new PermissionRequiredError('Stopping a Windows service requires Administrator privileges.', 'stop');
|
|
258
|
+
}
|
|
259
|
+
throw new ServiceStopError(safeName, res.stderr || res.stdout);
|
|
260
|
+
}
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Restarts a Windows Service.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} name
|
|
268
|
+
* @param {object} [options]
|
|
269
|
+
* @returns {Promise<boolean>}
|
|
270
|
+
*/
|
|
271
|
+
async restart(name, options = {}) {
|
|
272
|
+
await this.stop(name, options);
|
|
273
|
+
// Give SCM a brief pause
|
|
274
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
275
|
+
return this.start(name, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Enables automatic startup for a Windows service.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} name
|
|
282
|
+
* @param {object} [options]
|
|
283
|
+
* @returns {Promise<boolean>}
|
|
284
|
+
*/
|
|
285
|
+
async enable(name, options = {}) {
|
|
286
|
+
const safeName = sanitizeServiceName(name);
|
|
287
|
+
const serviceName = this.getServiceName(safeName);
|
|
288
|
+
|
|
289
|
+
const res = await this.run('sc.exe', ['config', serviceName, 'start=', 'auto']);
|
|
290
|
+
return res.code === 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Disables automatic startup for a Windows service.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} name
|
|
297
|
+
* @param {object} [options]
|
|
298
|
+
* @returns {Promise<boolean>}
|
|
299
|
+
*/
|
|
300
|
+
async disable(name, options = {}) {
|
|
301
|
+
const safeName = sanitizeServiceName(name);
|
|
302
|
+
const serviceName = this.getServiceName(safeName);
|
|
303
|
+
|
|
304
|
+
const res = await this.run('sc.exe', ['config', serviceName, 'start=', 'disabled']);
|
|
305
|
+
return res.code === 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Gets normalized status for a Windows service.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} name
|
|
312
|
+
* @param {object} [options]
|
|
313
|
+
* @returns {Promise<object>}
|
|
314
|
+
*/
|
|
315
|
+
async status(name, options = {}) {
|
|
316
|
+
const safeName = sanitizeServiceName(name);
|
|
317
|
+
const serviceName = this.getServiceName(safeName);
|
|
318
|
+
const meta = readAppMetadata(safeName) || {};
|
|
319
|
+
|
|
320
|
+
let state = 'stopped';
|
|
321
|
+
const pid = meta.pid || '-';
|
|
322
|
+
let installed = false;
|
|
323
|
+
const details = {};
|
|
324
|
+
|
|
325
|
+
const res = await this.run('sc.exe', ['query', serviceName]);
|
|
326
|
+
if (res.code === 0 && res.stdout) {
|
|
327
|
+
installed = true;
|
|
328
|
+
details.raw = res.stdout;
|
|
329
|
+
|
|
330
|
+
if (res.stdout.includes('RUNNING')) {
|
|
331
|
+
state = 'running';
|
|
332
|
+
} else if (res.stdout.includes('STOP_PENDING')) {
|
|
333
|
+
state = 'stopping';
|
|
334
|
+
} else if (res.stdout.includes('START_PENDING')) {
|
|
335
|
+
state = 'starting';
|
|
336
|
+
} else if (res.stdout.includes('STOPPED')) {
|
|
337
|
+
state = 'stopped';
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Check if metadata exists even if sc query failed
|
|
341
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
342
|
+
installed = true;
|
|
343
|
+
state = 'stopped';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const command = meta.command || process.execPath;
|
|
348
|
+
const argsList = meta.args || (meta.script ? [meta.script] : []);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
name: safeName,
|
|
352
|
+
installed,
|
|
353
|
+
state,
|
|
354
|
+
status: state,
|
|
355
|
+
enabled: installed,
|
|
356
|
+
pid,
|
|
357
|
+
restarts: meta.restarts || '0',
|
|
358
|
+
started: meta.started ? formatRelativeTime(meta.started) : state === 'running' ? 'active' : 'never',
|
|
359
|
+
startedRaw: meta.started || null,
|
|
360
|
+
command,
|
|
361
|
+
arguments: argsList.join(' '),
|
|
362
|
+
args: argsList,
|
|
363
|
+
script: meta.script || argsList[0] || command,
|
|
364
|
+
cwd: meta.cwd || process.cwd(),
|
|
365
|
+
unitFile: serviceName,
|
|
366
|
+
unitPath: serviceName,
|
|
367
|
+
platform: 'win32',
|
|
368
|
+
manager: 'windows',
|
|
369
|
+
details
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Inspects detailed configuration and runtime info for a service.
|
|
375
|
+
*
|
|
376
|
+
* @param {string} name
|
|
377
|
+
* @param {object} [options]
|
|
378
|
+
* @returns {Promise<object>}
|
|
379
|
+
*/
|
|
380
|
+
async inspect(name, options = {}) {
|
|
381
|
+
const statusObj = await this.status(name, options);
|
|
382
|
+
const meta = readAppMetadata(name) || {};
|
|
383
|
+
return {
|
|
384
|
+
...statusObj,
|
|
385
|
+
runtime: meta.runtime || 'node',
|
|
386
|
+
group: meta.group || 'default',
|
|
387
|
+
node: statusObj.command
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Retrieves logs for a Windows service.
|
|
393
|
+
*
|
|
394
|
+
* @param {string} name
|
|
395
|
+
* @param {object} [options]
|
|
396
|
+
* @returns {Promise<any>}
|
|
397
|
+
*/
|
|
398
|
+
async logs(name, options = {}) {
|
|
399
|
+
const safeName = sanitizeServiceName(name);
|
|
400
|
+
const meta = readAppMetadata(safeName);
|
|
401
|
+
const defaultStdout = path.join(getUnitupDir(), 'logs', `${safeName}.log`);
|
|
402
|
+
const defaultStderr = path.join(getUnitupDir(), 'logs', `${safeName}-error.log`);
|
|
403
|
+
|
|
404
|
+
const stdoutPath = meta?.logs?.stdout || defaultStdout;
|
|
405
|
+
const stderrPath = meta?.logs?.stderr || defaultStderr;
|
|
406
|
+
|
|
407
|
+
let paths = [stdoutPath, stderrPath];
|
|
408
|
+
if (options.output === 'stdout' || options.stdout) {
|
|
409
|
+
paths = [stdoutPath];
|
|
410
|
+
} else if (options.output === 'stderr' || options.stderr) {
|
|
411
|
+
paths = [stderrPath];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return readServiceLogs(paths, options);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Lists all unitup Windows services.
|
|
419
|
+
*
|
|
420
|
+
* @param {object} [options]
|
|
421
|
+
* @returns {Promise<Array<object>>}
|
|
422
|
+
*/
|
|
423
|
+
async list(options = {}) {
|
|
424
|
+
const appsDir = getAppsDir();
|
|
425
|
+
if (!fs.existsSync(appsDir)) return [];
|
|
426
|
+
|
|
427
|
+
const files = fs.readdirSync(appsDir).filter((f) => f.endsWith('.json'));
|
|
428
|
+
const result = [];
|
|
429
|
+
const targetGroup = options.group
|
|
430
|
+
? (options.group.startsWith('@') ? options.group.slice(1) : options.group).toLowerCase()
|
|
431
|
+
: null;
|
|
432
|
+
|
|
433
|
+
for (const file of files) {
|
|
434
|
+
const serviceName = file.replace(/\.json$/, '');
|
|
435
|
+
const meta = readAppMetadata(serviceName);
|
|
436
|
+
if (!meta) continue;
|
|
437
|
+
|
|
438
|
+
const group = meta.group || 'default';
|
|
439
|
+
if (targetGroup && group.toLowerCase() !== targetGroup) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const stat = await this.status(serviceName);
|
|
445
|
+
const cmdBase = path.basename(stat.command || 'node');
|
|
446
|
+
const argsStr = (stat.args || []).map((a) => path.basename(a)).join(' ');
|
|
447
|
+
const commandSummary = (cmdBase + (argsStr ? ` ${argsStr}` : '')).trim();
|
|
448
|
+
|
|
449
|
+
result.push({
|
|
450
|
+
name: serviceName,
|
|
451
|
+
runtime: meta.runtime || 'node',
|
|
452
|
+
group,
|
|
453
|
+
status: stat.state,
|
|
454
|
+
enabled: stat.enabled ? 'yes' : 'no',
|
|
455
|
+
pid: stat.pid,
|
|
456
|
+
command: commandSummary,
|
|
457
|
+
uptime: stat.started,
|
|
458
|
+
restarts: stat.restarts || '0',
|
|
459
|
+
platform: 'win32',
|
|
460
|
+
manager: 'windows'
|
|
461
|
+
});
|
|
462
|
+
} catch {
|
|
463
|
+
result.push({
|
|
464
|
+
name: serviceName,
|
|
465
|
+
runtime: meta.runtime || 'node',
|
|
466
|
+
group,
|
|
467
|
+
status: 'unknown',
|
|
468
|
+
enabled: 'unknown',
|
|
469
|
+
pid: '-',
|
|
470
|
+
command: 'unknown',
|
|
471
|
+
uptime: 'never',
|
|
472
|
+
restarts: '0',
|
|
473
|
+
platform: 'win32',
|
|
474
|
+
manager: 'windows'
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async isInstalled(name, options = {}) {
|
|
483
|
+
const safeName = sanitizeServiceName(name);
|
|
484
|
+
const serviceName = this.getServiceName(safeName);
|
|
485
|
+
const res = await this.run('sc.exe', ['query', serviceName]);
|
|
486
|
+
if (res.code === 0) return true;
|
|
487
|
+
const meta = readAppMetadata(safeName);
|
|
488
|
+
return !!meta;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async failures(options = {}) {
|
|
492
|
+
const all = await this.list(options);
|
|
493
|
+
return all.filter((i) => i.status === 'failed');
|
|
494
|
+
}
|
|
495
|
+
}
|
package/src/runtimes/bun.js
CHANGED
|
@@ -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 createBunAdapter(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(
|
|
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 createBunAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
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/runtimes/common.js
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
import { runCommand } from '../systemd.js';
|
|
4
4
|
|
|
5
5
|
export function findExecutableInPath(binaryName) {
|
|
6
6
|
// 1. Check standard PATH env
|
|
7
7
|
const envPath = process.env.PATH || '';
|
|
8
8
|
const dirs = envPath.split(path.delimiter).filter(Boolean);
|
|
9
|
-
|
|
9
|
+
const isWin = process.platform === 'win32';
|
|
10
|
+
const pathext = isWin ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean) : [''];
|
|
11
|
+
|
|
10
12
|
for (const dir of dirs) {
|
|
11
|
-
const
|
|
13
|
+
const directFull = path.join(dir, binaryName);
|
|
12
14
|
try {
|
|
13
|
-
if (fs.existsSync(
|
|
14
|
-
fs.accessSync(
|
|
15
|
-
return
|
|
15
|
+
if (fs.existsSync(directFull)) {
|
|
16
|
+
if (!isWin) fs.accessSync(directFull, fs.constants.X_OK);
|
|
17
|
+
return directFull;
|
|
16
18
|
}
|
|
17
19
|
} catch {
|
|
18
20
|
// ignore
|
|
19
21
|
}
|
|
22
|
+
|
|
23
|
+
if (isWin) {
|
|
24
|
+
for (const ext of pathext) {
|
|
25
|
+
const withExt = path.join(
|
|
26
|
+
dir,
|
|
27
|
+
binaryName.toLowerCase().endsWith(ext.toLowerCase()) ? binaryName : `${binaryName}${ext.toLowerCase()}`
|
|
28
|
+
);
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(withExt)) {
|
|
31
|
+
return withExt;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
20
38
|
}
|
|
21
39
|
return null;
|
|
22
40
|
}
|
package/src/runtimes/deno.js
CHANGED
|
@@ -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 createDenoAdapter(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(
|
|
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 createDenoAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '--version');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
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/runtimes/elixir.js
CHANGED
|
@@ -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 createElixirAdapter(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(
|
|
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 createElixirAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
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/runtimes/go.js
CHANGED
|
@@ -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 createGoAdapter(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(
|
|
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 createGoAdapter(opts = {}) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const version = await getBinaryVersion(execPath, 'version');
|
|
30
|
-
const scriptPath = opts.script ? path.resolve(
|
|
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/runtimes/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createBunAdapter } from './bun.js';
|
|
4
|
+
import { createDenoAdapter } from './deno.js';
|
|
5
|
+
import { createElixirAdapter } from './elixir.js';
|
|
6
|
+
import { createGoAdapter } from './go.js';
|
|
7
|
+
import { createNativeAdapter } from './native.js';
|
|
3
8
|
import { createNodeAdapter } from './node.js';
|
|
9
|
+
import { createPhpAdapter } from './php.js';
|
|
4
10
|
import { createPythonAdapter } from './python.js';
|
|
5
11
|
import { createRubyAdapter } from './ruby.js';
|
|
6
|
-
import { createPhpAdapter } from './php.js';
|
|
7
|
-
import { createBunAdapter } from './bun.js';
|
|
8
|
-
import { createDenoAdapter } from './deno.js';
|
|
9
12
|
import { createShellAdapter } from './shell.js';
|
|
10
|
-
import { createNativeAdapter } from './native.js';
|
|
11
|
-
import { createGoAdapter } from './go.js';
|
|
12
|
-
import { createElixirAdapter } from './elixir.js';
|
|
13
13
|
|
|
14
|
-
export function detectRuntime(filepath) {
|
|
14
|
+
export function detectRuntime(filepath, opts = {}) {
|
|
15
15
|
if (!filepath) {
|
|
16
16
|
throw new Error('Script path or command is required for runtime detection.');
|
|
17
17
|
}
|
|
@@ -42,7 +42,8 @@ export function detectRuntime(filepath) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// Inspect shebang header if file exists
|
|
45
|
-
const
|
|
45
|
+
const baseDir = opts.cwd || process.cwd();
|
|
46
|
+
const absPath = path.resolve(baseDir, filepath);
|
|
46
47
|
if (fs.existsSync(absPath)) {
|
|
47
48
|
try {
|
|
48
49
|
const fd = fs.openSync(absPath, 'r');
|
|
@@ -85,9 +86,11 @@ export function detectRuntime(filepath) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export async function resolveRuntimeConfig(opts = {}) {
|
|
89
|
+
const baseDir = opts.cwd || process.cwd();
|
|
90
|
+
|
|
88
91
|
// 1. If explicit --command is provided, bypass runtime auto-detection
|
|
89
92
|
if (opts.command) {
|
|
90
|
-
const absCommand = path.resolve(
|
|
93
|
+
const absCommand = path.resolve(baseDir, opts.command);
|
|
91
94
|
if (!fs.existsSync(absCommand)) {
|
|
92
95
|
// If binary doesn't exist as exact file path, check if it's in PATH or native executable
|
|
93
96
|
const { findRuntimeExecutable } = await import('./common.js');
|
|
@@ -116,7 +119,7 @@ export async function resolveRuntimeConfig(opts = {}) {
|
|
|
116
119
|
// 2. Resolve target runtime name
|
|
117
120
|
let runtimeName = opts.runtime;
|
|
118
121
|
if (!runtimeName) {
|
|
119
|
-
runtimeName = detectRuntime(opts.script);
|
|
122
|
+
runtimeName = detectRuntime(opts.script, opts);
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
const cleanRuntime = runtimeName.toLowerCase();
|