unitup 0.0.9 → 0.0.10
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/README.md +395 -180
- package/docs/index.html +531 -0
- package/index.d.ts +133 -152
- package/package.json +4 -1
- package/src/cli.js +254 -53
- package/src/doctor.js +73 -14
- package/src/index.js +16 -1
- package/src/runtimes/bun.js +46 -0
- package/src/runtimes/common.js +73 -0
- package/src/runtimes/deno.js +46 -0
- package/src/runtimes/elixir.js +46 -0
- package/src/runtimes/go.js +46 -0
- package/src/runtimes/index.js +155 -0
- package/src/runtimes/native.js +38 -0
- package/src/runtimes/node.js +57 -0
- package/src/runtimes/php.js +46 -0
- package/src/runtimes/python.js +46 -0
- package/src/runtimes/ruby.js +46 -0
- package/src/runtimes/shell.js +46 -0
- package/src/systemd.js +221 -28
- package/src/unit.js +59 -21
- package/src/utils.js +139 -4
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import readline from 'node:readline';
|
|
3
4
|
import { runDoctor } from './doctor.js';
|
|
4
5
|
import {
|
|
5
6
|
addService,
|
|
@@ -16,7 +17,9 @@ import {
|
|
|
16
17
|
findNodeExecutable,
|
|
17
18
|
getServicesByGroup,
|
|
18
19
|
inspectService,
|
|
19
|
-
getServiceFailures
|
|
20
|
+
getServiceFailures,
|
|
21
|
+
setServiceLimits,
|
|
22
|
+
executeJournalctlMaintenance
|
|
20
23
|
} from './systemd.js';
|
|
21
24
|
import { sanitizeServiceName, formatTable } from './utils.js';
|
|
22
25
|
|
|
@@ -32,6 +35,9 @@ export function parseArgs(argv) {
|
|
|
32
35
|
flags: {
|
|
33
36
|
name: '',
|
|
34
37
|
group: '',
|
|
38
|
+
runtime: '',
|
|
39
|
+
runtimeArgs: [],
|
|
40
|
+
command: '',
|
|
35
41
|
node: '',
|
|
36
42
|
cwd: '',
|
|
37
43
|
env: [],
|
|
@@ -46,7 +52,23 @@ export function parseArgs(argv) {
|
|
|
46
52
|
output: '',
|
|
47
53
|
lines: 100,
|
|
48
54
|
help: false,
|
|
49
|
-
version: false
|
|
55
|
+
version: false,
|
|
56
|
+
memoryHigh: '',
|
|
57
|
+
memoryMax: '',
|
|
58
|
+
memorySwapMax: '',
|
|
59
|
+
resetMemory: false,
|
|
60
|
+
since: '',
|
|
61
|
+
until: '',
|
|
62
|
+
priority: '',
|
|
63
|
+
grep: '',
|
|
64
|
+
boot: false,
|
|
65
|
+
json: false,
|
|
66
|
+
diskUsage: false,
|
|
67
|
+
size: '',
|
|
68
|
+
time: '',
|
|
69
|
+
files: '',
|
|
70
|
+
yes: false,
|
|
71
|
+
dryRun: false
|
|
50
72
|
}
|
|
51
73
|
};
|
|
52
74
|
|
|
@@ -60,6 +82,24 @@ export function parseArgs(argv) {
|
|
|
60
82
|
} else if (arg === '-v' || arg === '--version') {
|
|
61
83
|
result.flags.version = true;
|
|
62
84
|
i++;
|
|
85
|
+
} else if (arg === '--runtime') {
|
|
86
|
+
result.flags.runtime = argv[i + 1] || '';
|
|
87
|
+
i += 2;
|
|
88
|
+
} else if (arg.startsWith('--runtime=')) {
|
|
89
|
+
result.flags.runtime = arg.slice(10);
|
|
90
|
+
i++;
|
|
91
|
+
} else if (arg === '--runtime-arg') {
|
|
92
|
+
if (argv[i + 1]) result.flags.runtimeArgs.push(argv[i + 1]);
|
|
93
|
+
i += 2;
|
|
94
|
+
} else if (arg.startsWith('--runtime-arg=')) {
|
|
95
|
+
result.flags.runtimeArgs.push(arg.slice(14));
|
|
96
|
+
i++;
|
|
97
|
+
} else if (arg === '--command') {
|
|
98
|
+
result.flags.command = argv[i + 1] || '';
|
|
99
|
+
i += 2;
|
|
100
|
+
} else if (arg.startsWith('--command=')) {
|
|
101
|
+
result.flags.command = arg.slice(10);
|
|
102
|
+
i++;
|
|
63
103
|
} else if (arg === '--node') {
|
|
64
104
|
result.flags.node = argv[i + 1] || '';
|
|
65
105
|
i += 2;
|
|
@@ -135,6 +175,84 @@ export function parseArgs(argv) {
|
|
|
135
175
|
} else if (arg.startsWith('--output=')) {
|
|
136
176
|
result.flags.output = arg.slice(9);
|
|
137
177
|
i++;
|
|
178
|
+
} else if (arg === '--memory-high') {
|
|
179
|
+
result.flags.memoryHigh = argv[i + 1] || '';
|
|
180
|
+
i += 2;
|
|
181
|
+
} else if (arg.startsWith('--memory-high=')) {
|
|
182
|
+
result.flags.memoryHigh = arg.slice(14);
|
|
183
|
+
i++;
|
|
184
|
+
} else if (arg === '--memory-max') {
|
|
185
|
+
result.flags.memoryMax = argv[i + 1] || '';
|
|
186
|
+
i += 2;
|
|
187
|
+
} else if (arg.startsWith('--memory-max=')) {
|
|
188
|
+
result.flags.memoryMax = arg.slice(13);
|
|
189
|
+
i++;
|
|
190
|
+
} else if (arg === '--memory-swap-max') {
|
|
191
|
+
result.flags.memorySwapMax = argv[i + 1] || '';
|
|
192
|
+
i += 2;
|
|
193
|
+
} else if (arg.startsWith('--memory-swap-max=')) {
|
|
194
|
+
result.flags.memorySwapMax = arg.slice(18);
|
|
195
|
+
i++;
|
|
196
|
+
} else if (arg === '--reset-memory') {
|
|
197
|
+
result.flags.resetMemory = true;
|
|
198
|
+
i++;
|
|
199
|
+
} else if (arg === '--since') {
|
|
200
|
+
result.flags.since = argv[i + 1] || '';
|
|
201
|
+
i += 2;
|
|
202
|
+
} else if (arg.startsWith('--since=')) {
|
|
203
|
+
result.flags.since = arg.slice(8);
|
|
204
|
+
i++;
|
|
205
|
+
} else if (arg === '--until') {
|
|
206
|
+
result.flags.until = argv[i + 1] || '';
|
|
207
|
+
i += 2;
|
|
208
|
+
} else if (arg.startsWith('--until=')) {
|
|
209
|
+
result.flags.until = arg.slice(8);
|
|
210
|
+
i++;
|
|
211
|
+
} else if (arg === '--priority') {
|
|
212
|
+
result.flags.priority = argv[i + 1] || '';
|
|
213
|
+
i += 2;
|
|
214
|
+
} else if (arg.startsWith('--priority=')) {
|
|
215
|
+
result.flags.priority = arg.slice(11);
|
|
216
|
+
i++;
|
|
217
|
+
} else if (arg === '--grep') {
|
|
218
|
+
result.flags.grep = argv[i + 1] || '';
|
|
219
|
+
i += 2;
|
|
220
|
+
} else if (arg.startsWith('--grep=')) {
|
|
221
|
+
result.flags.grep = arg.slice(7);
|
|
222
|
+
i++;
|
|
223
|
+
} else if (arg === '--boot' || arg === '-b') {
|
|
224
|
+
result.flags.boot = true;
|
|
225
|
+
i++;
|
|
226
|
+
} else if (arg === '--json') {
|
|
227
|
+
result.flags.json = true;
|
|
228
|
+
i++;
|
|
229
|
+
} else if (arg === '--disk-usage') {
|
|
230
|
+
result.flags.diskUsage = true;
|
|
231
|
+
i++;
|
|
232
|
+
} else if (arg === '--size') {
|
|
233
|
+
result.flags.size = argv[i + 1] || '';
|
|
234
|
+
i += 2;
|
|
235
|
+
} else if (arg.startsWith('--size=')) {
|
|
236
|
+
result.flags.size = arg.slice(7);
|
|
237
|
+
i++;
|
|
238
|
+
} else if (arg === '--time') {
|
|
239
|
+
result.flags.time = argv[i + 1] || '';
|
|
240
|
+
i += 2;
|
|
241
|
+
} else if (arg.startsWith('--time=')) {
|
|
242
|
+
result.flags.time = arg.slice(7);
|
|
243
|
+
i++;
|
|
244
|
+
} else if (arg === '--files') {
|
|
245
|
+
result.flags.files = argv[i + 1] || '';
|
|
246
|
+
i += 2;
|
|
247
|
+
} else if (arg.startsWith('--files=')) {
|
|
248
|
+
result.flags.files = arg.slice(8);
|
|
249
|
+
i++;
|
|
250
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
251
|
+
result.flags.yes = true;
|
|
252
|
+
i++;
|
|
253
|
+
} else if (arg === '--dry-run') {
|
|
254
|
+
result.flags.dryRun = true;
|
|
255
|
+
i++;
|
|
138
256
|
} else if (!arg.startsWith('-')) {
|
|
139
257
|
if (!result.command) {
|
|
140
258
|
result.command = arg;
|
|
@@ -153,11 +271,11 @@ export function parseArgs(argv) {
|
|
|
153
271
|
|
|
154
272
|
export function printHelp() {
|
|
155
273
|
console.log(`
|
|
156
|
-
unitup - Minimal systemd user service wrapper for
|
|
274
|
+
unitup - Minimal systemd user service wrapper for any executable & runtime
|
|
157
275
|
|
|
158
276
|
Usage:
|
|
159
|
-
unitup doctor Run system readiness check
|
|
160
|
-
unitup add <script> [options] Add script as systemd user service
|
|
277
|
+
unitup doctor Run system readiness and runtime check
|
|
278
|
+
unitup add <script> [options] Add script or executable as systemd user service
|
|
161
279
|
unitup start <name|@group> Start a service (--enable to enable on boot)
|
|
162
280
|
unitup stop <name|@group> Stop a service
|
|
163
281
|
unitup restart <name|@group> Restart a service
|
|
@@ -169,23 +287,23 @@ Usage:
|
|
|
169
287
|
unitup list / unitup ls List all services (--group <group>)
|
|
170
288
|
|
|
171
289
|
Add Options:
|
|
172
|
-
--name <name>
|
|
173
|
-
--
|
|
174
|
-
--
|
|
175
|
-
--
|
|
176
|
-
--
|
|
177
|
-
--
|
|
178
|
-
--
|
|
179
|
-
--
|
|
180
|
-
--
|
|
290
|
+
--name <name> Service name (default: script/executable name)
|
|
291
|
+
--runtime <name> Runtime (node, python, ruby, php, bun, deno, shell, go, elixir, native)
|
|
292
|
+
--runtime-arg <val> Runtime argument (can be specified multiple times)
|
|
293
|
+
--command <path> Custom executable command path (bypasses auto-detection)
|
|
294
|
+
--arg <value> Argument to pass to command (can be specified multiple times)
|
|
295
|
+
--group <group> Assign service to a group (default: default)
|
|
296
|
+
--cwd <path> Working directory
|
|
297
|
+
--restart <policy> Restart policy (on-failure, always, etc. Default: on-failure)
|
|
298
|
+
--env KEY=value Environment variable (can be specified multiple times)
|
|
299
|
+
--env-file <file> Path to environment file
|
|
300
|
+
--start Enable and start service immediately after creation
|
|
181
301
|
|
|
182
302
|
Examples:
|
|
183
|
-
unitup add server.js --
|
|
184
|
-
unitup
|
|
185
|
-
unitup
|
|
186
|
-
unitup
|
|
187
|
-
unitup failures
|
|
188
|
-
unitup remove @myproject
|
|
303
|
+
unitup add server.js --runtime node
|
|
304
|
+
unitup add worker.py --runtime python
|
|
305
|
+
unitup add ./server --runtime native
|
|
306
|
+
unitup add --name worker --command /usr/bin/python3 --arg worker.py --arg --port --arg 3000
|
|
189
307
|
`);
|
|
190
308
|
}
|
|
191
309
|
|
|
@@ -235,25 +353,39 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
235
353
|
|
|
236
354
|
case 'add': {
|
|
237
355
|
const scriptArg = positionals[0];
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const resolvedNode = await findNodeExecutable(flags.node);
|
|
243
|
-
if (!resolvedNode) {
|
|
244
|
-
throw new Error('Node.js is required but not found.\nRun: unitup doctor');
|
|
245
|
-
}
|
|
356
|
+
let name = flags.name;
|
|
357
|
+
let absScriptPath = scriptArg ? path.resolve(process.cwd(), scriptArg) : undefined;
|
|
246
358
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
359
|
+
if (flags.node) {
|
|
360
|
+
const resolvedNode = await findNodeExecutable(flags.node);
|
|
361
|
+
if (!resolvedNode) {
|
|
362
|
+
throw new Error('Node.js is required but not found.\nRun: unitup doctor');
|
|
363
|
+
}
|
|
250
364
|
}
|
|
251
365
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
366
|
+
if (flags.command) {
|
|
367
|
+
if (!name) {
|
|
368
|
+
if (scriptArg) {
|
|
369
|
+
const baseName = path.basename(scriptArg);
|
|
370
|
+
const ext = path.extname(baseName);
|
|
371
|
+
name = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
372
|
+
} else {
|
|
373
|
+
const baseCmd = path.basename(flags.command);
|
|
374
|
+
name = baseCmd;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
if (!scriptArg) {
|
|
379
|
+
throw new Error('Script file path or --command is required.\nExample: unitup add app.py --runtime python');
|
|
380
|
+
}
|
|
381
|
+
if (!fs.existsSync(absScriptPath)) {
|
|
382
|
+
throw new Error(`Script file does not exist: ${absScriptPath}`);
|
|
383
|
+
}
|
|
384
|
+
if (!name) {
|
|
385
|
+
const baseName = path.basename(scriptArg);
|
|
386
|
+
const ext = path.extname(baseName);
|
|
387
|
+
name = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
388
|
+
}
|
|
257
389
|
}
|
|
258
390
|
|
|
259
391
|
const envObj = {};
|
|
@@ -270,13 +402,19 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
270
402
|
name,
|
|
271
403
|
group: flags.group || 'default',
|
|
272
404
|
script: absScriptPath,
|
|
273
|
-
|
|
274
|
-
|
|
405
|
+
command: flags.command,
|
|
406
|
+
runtime: flags.runtime,
|
|
407
|
+
runtimeArgs: flags.runtimeArgs,
|
|
408
|
+
nodePath: flags.node,
|
|
409
|
+
cwd: flags.cwd ? path.resolve(process.cwd(), flags.cwd) : (absScriptPath ? path.dirname(absScriptPath) : process.cwd()),
|
|
275
410
|
env: envObj,
|
|
276
411
|
envFile: flags.envFile ? path.resolve(process.cwd(), flags.envFile) : undefined,
|
|
277
412
|
restart: flags.restart,
|
|
278
413
|
args: flags.args,
|
|
279
|
-
start: flags.start
|
|
414
|
+
start: flags.start,
|
|
415
|
+
memoryHigh: flags.memoryHigh,
|
|
416
|
+
memoryMax: flags.memoryMax,
|
|
417
|
+
memorySwapMax: flags.memorySwapMax
|
|
280
418
|
});
|
|
281
419
|
|
|
282
420
|
console.log(`✓ Service "${res.name}" created at ${res.unitPath}`);
|
|
@@ -288,6 +426,24 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
288
426
|
break;
|
|
289
427
|
}
|
|
290
428
|
|
|
429
|
+
case 'limits': {
|
|
430
|
+
const nameArg = positionals[0];
|
|
431
|
+
if (!nameArg) {
|
|
432
|
+
throw new Error('Service name is required.\nExample: unitup limits api --memory-high 400M --memory-max 512M');
|
|
433
|
+
}
|
|
434
|
+
const info = await setServiceLimits(nameArg, {
|
|
435
|
+
memoryHigh: flags.memoryHigh,
|
|
436
|
+
memoryMax: flags.memoryMax,
|
|
437
|
+
memorySwapMax: flags.memorySwapMax,
|
|
438
|
+
resetMemory: flags.resetMemory
|
|
439
|
+
});
|
|
440
|
+
console.log(`✓ Service "${info.name}" limits updated.`);
|
|
441
|
+
console.log(`Memory High: ${info.memoryHigh}`);
|
|
442
|
+
console.log(`Memory Max: ${info.memoryMax}`);
|
|
443
|
+
console.log(`Swap Max: ${info.memorySwapMax}`);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
|
|
291
447
|
case 'start': {
|
|
292
448
|
const nameArg = positionals[0];
|
|
293
449
|
if (!nameArg) {
|
|
@@ -345,8 +501,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
345
501
|
console.log(`PID: ${status.pid}`);
|
|
346
502
|
console.log(`Started: ${status.started}`);
|
|
347
503
|
console.log(`Restarts: ${status.restarts}`);
|
|
348
|
-
console.log(`
|
|
504
|
+
console.log(`Command: ${status.command}`);
|
|
505
|
+
console.log(`Arguments: ${status.arguments}`);
|
|
349
506
|
console.log(`Working directory: ${status.cwd}`);
|
|
507
|
+
console.log(`Memory: ${status.memory}`);
|
|
508
|
+
console.log(`Memory Peak: ${status.memoryPeak}`);
|
|
509
|
+
console.log(`Memory High: ${status.memoryHigh}`);
|
|
510
|
+
console.log(`Memory Max: ${status.memoryMax}`);
|
|
511
|
+
console.log(`Swap Max: ${status.memorySwapMax}`);
|
|
350
512
|
}
|
|
351
513
|
}
|
|
352
514
|
break;
|
|
@@ -367,7 +529,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
367
529
|
follow: flags.follow,
|
|
368
530
|
lines: flags.lines,
|
|
369
531
|
cat: flags.cat,
|
|
370
|
-
output: flags.output
|
|
532
|
+
output: flags.output,
|
|
533
|
+
since: flags.since,
|
|
534
|
+
until: flags.until,
|
|
535
|
+
priority: flags.priority,
|
|
536
|
+
grep: flags.grep,
|
|
537
|
+
boot: flags.boot,
|
|
538
|
+
json: flags.json,
|
|
539
|
+
diskUsage: flags.diskUsage
|
|
371
540
|
});
|
|
372
541
|
if (typeof output === 'string') {
|
|
373
542
|
console.log(output);
|
|
@@ -376,6 +545,37 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
376
545
|
break;
|
|
377
546
|
}
|
|
378
547
|
|
|
548
|
+
case 'journal': {
|
|
549
|
+
const action = positionals[0];
|
|
550
|
+
if (!action) {
|
|
551
|
+
throw new Error('Journal action is required (disk-usage, rotate, vacuum).\nExample: unitup journal vacuum --size 500M');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (action === 'vacuum' && !flags.yes && !flags.dryRun) {
|
|
555
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
556
|
+
const answer = await new Promise((resolve) => {
|
|
557
|
+
rl.question('This operation affects archived journal logs system-wide,\nnot only services managed by unitup.\n\nContinue? [y/N] ', (ans) => {
|
|
558
|
+
rl.close();
|
|
559
|
+
resolve(ans.trim());
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
563
|
+
console.log('Aborted.');
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const result = await executeJournalctlMaintenance(action, {
|
|
569
|
+
size: flags.size,
|
|
570
|
+
time: flags.time,
|
|
571
|
+
files: flags.files,
|
|
572
|
+
yes: flags.yes,
|
|
573
|
+
dryRun: flags.dryRun
|
|
574
|
+
});
|
|
575
|
+
console.log(result);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
|
|
379
579
|
case 'remove': {
|
|
380
580
|
const nameArg = positionals[0];
|
|
381
581
|
if (!nameArg) {
|
|
@@ -403,11 +603,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
403
603
|
|
|
404
604
|
const table = formatTable(services, [
|
|
405
605
|
{ key: 'name', label: 'NAME' },
|
|
406
|
-
{ key: '
|
|
606
|
+
{ key: 'runtime', label: 'RUNTIME' },
|
|
407
607
|
{ key: 'status', label: 'STATUS' },
|
|
408
608
|
{ key: 'pid', label: 'PID' },
|
|
409
|
-
{ key: '
|
|
410
|
-
{ key: 'restarts', label: 'RESTARTS' }
|
|
609
|
+
{ key: 'command', label: 'COMMAND' }
|
|
411
610
|
]);
|
|
412
611
|
console.log(table);
|
|
413
612
|
break;
|
|
@@ -419,17 +618,19 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
419
618
|
throw new Error('Service name is required.\nExample: unitup inspect api');
|
|
420
619
|
}
|
|
421
620
|
const info = await inspectService(nameArg);
|
|
422
|
-
console.log(`
|
|
621
|
+
console.log(`Name: ${info.name}`);
|
|
622
|
+
console.log(`Runtime: ${info.runtime}`);
|
|
423
623
|
console.log(`Group: ${info.group}`);
|
|
424
|
-
console.log(`Status: ${info.status}
|
|
425
|
-
console.log(`
|
|
426
|
-
console.log(`
|
|
427
|
-
console.log(`Uptime: ${info.started}`);
|
|
428
|
-
console.log(`Script: ${info.script}`);
|
|
624
|
+
console.log(`Status: ${info.status}`);
|
|
625
|
+
console.log(`Command: ${info.command}`);
|
|
626
|
+
console.log(`Arguments: ${info.arguments}`);
|
|
429
627
|
console.log(`Working directory: ${info.cwd}`);
|
|
430
|
-
console.log(`
|
|
431
|
-
console.log(`
|
|
432
|
-
console.log(`
|
|
628
|
+
console.log(`Unit: ${info.unit}`);
|
|
629
|
+
console.log(`Memory: ${info.memory}`);
|
|
630
|
+
console.log(`Memory Peak: ${info.memoryPeak}`);
|
|
631
|
+
console.log(`Memory High: ${info.memoryHigh}`);
|
|
632
|
+
console.log(`Memory Max: ${info.memoryMax}`);
|
|
633
|
+
console.log(`Swap Max: ${info.memorySwapMax}`);
|
|
433
634
|
break;
|
|
434
635
|
}
|
|
435
636
|
|
package/src/doctor.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
checkNodeDiagnostics
|
|
10
10
|
} from './systemd.js';
|
|
11
11
|
import { getUserUnitDir } from './unit.js';
|
|
12
|
+
import { findRuntimeExecutable } from './runtimes/common.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Runs system diagnostic checks and returns structured data.
|
|
@@ -24,14 +25,54 @@ export async function getDoctorInfo() {
|
|
|
24
25
|
const lingering = await checkUserLinger();
|
|
25
26
|
const nodeDiag = await checkNodeDiagnostics();
|
|
26
27
|
|
|
28
|
+
const nodePath = nodeDiag.execPath || (await findRuntimeExecutable(['node']));
|
|
29
|
+
const pythonPath = await findRuntimeExecutable(['python3', 'python']);
|
|
30
|
+
const rubyPath = await findRuntimeExecutable(['ruby']);
|
|
31
|
+
const phpPath = await findRuntimeExecutable(['php']);
|
|
32
|
+
const bunPath = await findRuntimeExecutable(['bun']);
|
|
33
|
+
const denoPath = await findRuntimeExecutable(['deno']);
|
|
34
|
+
const goPath = await findRuntimeExecutable(['go']);
|
|
35
|
+
const elixirPath = await findRuntimeExecutable(['elixir']);
|
|
36
|
+
|
|
37
|
+
const runtimes = {
|
|
38
|
+
'Node.js': nodePath,
|
|
39
|
+
'Python': pythonPath,
|
|
40
|
+
'Ruby': rubyPath,
|
|
41
|
+
'PHP': phpPath,
|
|
42
|
+
'Bun': bunPath,
|
|
43
|
+
'Deno': denoPath,
|
|
44
|
+
'Go': goPath,
|
|
45
|
+
'Elixir': elixirPath
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let cgroupV2 = false;
|
|
49
|
+
let memoryController = false;
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync('/sys/fs/cgroup/cgroup.controllers')) {
|
|
52
|
+
cgroupV2 = true;
|
|
53
|
+
const controllers = fs.readFileSync('/sys/fs/cgroup/cgroup.controllers', 'utf8');
|
|
54
|
+
memoryController = controllers.includes('memory');
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const memoryMaxSupported = linux && (cgroupV2 || userSystemd);
|
|
61
|
+
const memorySwapMaxSupported = linux && cgroupV2 && memoryController;
|
|
62
|
+
|
|
27
63
|
return {
|
|
28
64
|
linux,
|
|
29
65
|
systemctl,
|
|
30
66
|
systemdRunning: pid1,
|
|
31
67
|
userSystemdAvailable: userSystemd,
|
|
68
|
+
cgroupV2,
|
|
69
|
+
memoryController,
|
|
70
|
+
memoryMaxSupported,
|
|
71
|
+
memorySwapMaxSupported,
|
|
32
72
|
nodeDiag,
|
|
33
|
-
nodePath:
|
|
73
|
+
nodePath: nodePath || process.execPath,
|
|
34
74
|
nodeVersion: nodeDiag.version,
|
|
75
|
+
runtimes,
|
|
35
76
|
unitDir: getUserUnitDir(),
|
|
36
77
|
unitDirWritable,
|
|
37
78
|
lingering,
|
|
@@ -71,24 +112,42 @@ export async function runDoctor() {
|
|
|
71
112
|
console.log('! systemd user services unavailable');
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.log(`✓ Node version: ${n.version}`);
|
|
79
|
-
}
|
|
80
|
-
if (n.inPath) {
|
|
81
|
-
console.log('✓ PATH includes node');
|
|
115
|
+
console.log('\nDetected runtimes:');
|
|
116
|
+
for (const [name, path] of Object.entries(info.runtimes)) {
|
|
117
|
+
if (path) {
|
|
118
|
+
console.log(`✓ ${name}: ${path}`);
|
|
82
119
|
} else {
|
|
83
|
-
console.log(
|
|
120
|
+
console.log(`- ${name}: not found`);
|
|
84
121
|
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('\nMemory Controller Diagnostics:');
|
|
125
|
+
if (info.cgroupV2) {
|
|
126
|
+
console.log('✓ cgroup v2 detected');
|
|
85
127
|
} else {
|
|
86
|
-
console.log(
|
|
87
|
-
if (n.solution) {
|
|
88
|
-
console.log(`\n${n.solution}\n`);
|
|
89
|
-
}
|
|
128
|
+
console.log('- cgroup v2 unavailable');
|
|
90
129
|
}
|
|
91
130
|
|
|
131
|
+
if (info.memoryController) {
|
|
132
|
+
console.log('✓ Memory controller available');
|
|
133
|
+
} else {
|
|
134
|
+
console.log('- Memory controller unavailable');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (info.memoryMaxSupported) {
|
|
138
|
+
console.log('✓ MemoryMax supported');
|
|
139
|
+
} else {
|
|
140
|
+
console.log('- MemoryMax unavailable');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (info.memorySwapMaxSupported) {
|
|
144
|
+
console.log('✓ MemorySwapMax supported');
|
|
145
|
+
} else {
|
|
146
|
+
console.log('- MemorySwapMax unavailable');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
92
151
|
if (info.unitDirWritable) {
|
|
93
152
|
console.log('✓ Unit directory writable');
|
|
94
153
|
} else {
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
getServiceFailures,
|
|
12
12
|
getServicesByGroup,
|
|
13
13
|
runJournalctlLogs,
|
|
14
|
+
setServiceLimits,
|
|
15
|
+
executeJournalctlMaintenance,
|
|
14
16
|
isLinux,
|
|
15
17
|
isSystemctlAvailable,
|
|
16
18
|
isSystemdPID1,
|
|
@@ -35,8 +37,11 @@ import {
|
|
|
35
37
|
readAppMetadata,
|
|
36
38
|
getAppMetadataPath,
|
|
37
39
|
getAppsDir,
|
|
38
|
-
getUnitupDir
|
|
40
|
+
getUnitupDir,
|
|
41
|
+
validateMemorySize,
|
|
42
|
+
formatMemoryBytes
|
|
39
43
|
} from './utils.js';
|
|
44
|
+
import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
|
|
40
45
|
|
|
41
46
|
export {
|
|
42
47
|
// Main programmatic service management API
|
|
@@ -52,8 +57,18 @@ export {
|
|
|
52
57
|
inspectService,
|
|
53
58
|
getServiceFailures,
|
|
54
59
|
getServicesByGroup,
|
|
60
|
+
setServiceLimits,
|
|
61
|
+
executeJournalctlMaintenance as executeJournalctlMaintenance,
|
|
55
62
|
runJournalctlLogs as getServiceLogs,
|
|
56
63
|
|
|
64
|
+
// Memory & Validation helpers
|
|
65
|
+
validateMemorySize,
|
|
66
|
+
formatMemoryBytes,
|
|
67
|
+
|
|
68
|
+
// Runtime detection & resolution
|
|
69
|
+
detectRuntime,
|
|
70
|
+
resolveRuntimeConfig,
|
|
71
|
+
|
|
57
72
|
// Metadata helpers
|
|
58
73
|
readAppMetadata,
|
|
59
74
|
getAppMetadataPath,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { findRuntimeExecutable, getBinaryVersion } from './common.js';
|
|
4
|
+
|
|
5
|
+
export async function createBunAdapter(opts = {}) {
|
|
6
|
+
const customPath = opts.command;
|
|
7
|
+
let execPath = null;
|
|
8
|
+
|
|
9
|
+
if (customPath) {
|
|
10
|
+
const absPath = path.resolve(process.cwd(), customPath);
|
|
11
|
+
if (fs.existsSync(absPath)) {
|
|
12
|
+
try {
|
|
13
|
+
fs.accessSync(absPath, fs.constants.X_OK);
|
|
14
|
+
execPath = absPath;
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!execPath) {
|
|
20
|
+
execPath = await findRuntimeExecutable(['bun']);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!execPath) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Bun runtime could not be found.\n\nInstall Bun or specify its path:\n unitup add server.ts --command /usr/bin/bun'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = await getBinaryVersion(execPath, '-v');
|
|
30
|
+
const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
|
|
31
|
+
const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
|
|
32
|
+
const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
|
|
33
|
+
|
|
34
|
+
const args = [...runtimeArgs];
|
|
35
|
+
if (scriptPath) {
|
|
36
|
+
args.push(scriptPath);
|
|
37
|
+
}
|
|
38
|
+
args.push(...scriptArgs);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
command: execPath,
|
|
42
|
+
args,
|
|
43
|
+
runtime: 'bun',
|
|
44
|
+
version
|
|
45
|
+
};
|
|
46
|
+
}
|