unitup 0.1.5 → 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 -3
- 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 +1 -1
- package/src/runtimes/common.js +24 -6
- package/src/runtimes/deno.js +1 -1
- package/src/runtimes/elixir.js +1 -1
- package/src/runtimes/go.js +1 -1
- package/src/runtimes/index.js +7 -7
- package/src/runtimes/native.js +2 -4
- package/src/runtimes/node.js +1 -1
- package/src/runtimes/php.js +1 -1
- package/src/runtimes/python.js +1 -1
- package/src/runtimes/ruby.js +1 -1
- package/src/runtimes/shell.js +1 -1
- 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
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
package/src/runtimes/elixir.js
CHANGED
package/src/runtimes/go.js
CHANGED
package/src/runtimes/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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
14
|
export function detectRuntime(filepath, opts = {}) {
|
|
15
15
|
if (!filepath) {
|
package/src/runtimes/native.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
const baseDir = opts.cwd || process.cwd();
|
|
@@ -23,9 +23,7 @@ export async function createNativeAdapter(opts = {}) {
|
|
|
23
23
|
try {
|
|
24
24
|
fs.accessSync(absPath, fs.constants.X_OK);
|
|
25
25
|
} catch {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`
|
|
28
|
-
);
|
|
26
|
+
throw new Error(`The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`);
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
package/src/runtimes/node.js
CHANGED
package/src/runtimes/php.js
CHANGED
package/src/runtimes/python.js
CHANGED
package/src/runtimes/ruby.js
CHANGED