unitup 0.0.8 → 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/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,
@@ -13,7 +14,12 @@ import {
13
14
  runJournalctlLogs,
14
15
  isLinux,
15
16
  isSystemctlAvailable,
16
- findNodeExecutable
17
+ findNodeExecutable,
18
+ getServicesByGroup,
19
+ inspectService,
20
+ getServiceFailures,
21
+ setServiceLimits,
22
+ executeJournalctlMaintenance
17
23
  } from './systemd.js';
18
24
  import { sanitizeServiceName, formatTable } from './utils.js';
19
25
 
@@ -28,6 +34,10 @@ export function parseArgs(argv) {
28
34
  positionals: [],
29
35
  flags: {
30
36
  name: '',
37
+ group: '',
38
+ runtime: '',
39
+ runtimeArgs: [],
40
+ command: '',
31
41
  node: '',
32
42
  cwd: '',
33
43
  env: [],
@@ -42,7 +52,23 @@ export function parseArgs(argv) {
42
52
  output: '',
43
53
  lines: 100,
44
54
  help: false,
45
- 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
46
72
  }
47
73
  };
48
74
 
@@ -56,6 +82,24 @@ export function parseArgs(argv) {
56
82
  } else if (arg === '-v' || arg === '--version') {
57
83
  result.flags.version = true;
58
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++;
59
103
  } else if (arg === '--node') {
60
104
  result.flags.node = argv[i + 1] || '';
61
105
  i += 2;
@@ -68,6 +112,12 @@ export function parseArgs(argv) {
68
112
  } else if (arg.startsWith('--name=')) {
69
113
  result.flags.name = arg.slice(7);
70
114
  i++;
115
+ } else if (arg === '--group') {
116
+ result.flags.group = argv[i + 1] || '';
117
+ i += 2;
118
+ } else if (arg.startsWith('--group=')) {
119
+ result.flags.group = arg.slice(8);
120
+ i++;
71
121
  } else if (arg === '--cwd') {
72
122
  result.flags.cwd = argv[i + 1] || '';
73
123
  i += 2;
@@ -125,6 +175,84 @@ export function parseArgs(argv) {
125
175
  } else if (arg.startsWith('--output=')) {
126
176
  result.flags.output = arg.slice(9);
127
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++;
128
256
  } else if (!arg.startsWith('-')) {
129
257
  if (!result.command) {
130
258
  result.command = arg;
@@ -143,36 +271,53 @@ export function parseArgs(argv) {
143
271
 
144
272
  export function printHelp() {
145
273
  console.log(`
146
- unitup - Minimal systemd user service wrapper for Node.js scripts
274
+ unitup - Minimal systemd user service wrapper for any executable & runtime
147
275
 
148
276
  Usage:
149
- unitup doctor Run system readiness check
150
- unitup add <script> [options] Add script as systemd user service
151
- unitup start <name> Start a service (--enable to enable on boot)
152
- unitup stop <name> Stop a service
153
- unitup restart <name> Restart a service
154
- unitup status <name> Show status summary (--raw for full status)
155
- unitup logs <name> Show journalctl logs (--follow, --lines N)
156
- unitup remove <name> Stop, disable and delete a service
157
- unitup list List all unitup user services
277
+ unitup doctor Run system readiness and runtime check
278
+ unitup add <script> [options] Add script or executable as systemd user service
279
+ unitup start <name|@group> Start a service (--enable to enable on boot)
280
+ unitup stop <name|@group> Stop a service
281
+ unitup restart <name|@group> Restart a service
282
+ unitup status <name|@group> Show status summary (--raw for full status)
283
+ unitup logs <name|@group> Show journalctl logs (-f/--follow, -n/--lines N, -c/--cat)
284
+ unitup inspect <name> View detailed app configuration and status
285
+ unitup failures List all failed services with exit code & restarts
286
+ unitup remove <name|@group> Stop, disable and delete a service
287
+ unitup list / unitup ls List all services (--group <group>)
158
288
 
159
289
  Add Options:
160
- --name <name> Service name (default: script name)
161
- --cwd <path> Working directory
162
- --restart <policy> Restart policy (on-failure, always, etc. Default: on-failure)
163
- --env KEY=value Environment variable (can be specified multiple times)
164
- --env-file <file> Path to environment file
165
- --arg <value> Script argument (can be specified multiple times)
166
- --start Enable and start service immediately after creation
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
167
301
 
168
302
  Examples:
169
- unitup add server.js --name api --env NODE_ENV=production --start
170
- unitup status api
171
- unitup logs api --follow
172
- unitup remove api
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
173
307
  `);
174
308
  }
175
309
 
310
+ async function resolveTargetNames(target) {
311
+ if (target && target.startsWith('@')) {
312
+ const list = await getServicesByGroup(target);
313
+ if (list.length === 0) {
314
+ throw new Error(`No services found in group "${target}".`);
315
+ }
316
+ return list;
317
+ }
318
+ return [target];
319
+ }
320
+
176
321
  /**
177
322
  * Main CLI runner entrypoint.
178
323
  *
@@ -208,25 +353,39 @@ export async function runCli(argv = process.argv.slice(2)) {
208
353
 
209
354
  case 'add': {
210
355
  const scriptArg = positionals[0];
211
- if (!scriptArg) {
212
- throw new Error('Script file path is required.\nExample: unitup add app.js');
213
- }
214
-
215
- const resolvedNode = await findNodeExecutable(flags.node);
216
- if (!resolvedNode) {
217
- throw new Error('Node.js is required but not found.\nRun: unitup doctor');
218
- }
356
+ let name = flags.name;
357
+ let absScriptPath = scriptArg ? path.resolve(process.cwd(), scriptArg) : undefined;
219
358
 
220
- const absScriptPath = path.resolve(process.cwd(), scriptArg);
221
- if (!fs.existsSync(absScriptPath)) {
222
- throw new Error(`Script file does not exist: ${absScriptPath}`);
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
+ }
223
364
  }
224
365
 
225
- let name = flags.name;
226
- if (!name) {
227
- const baseName = path.basename(scriptArg);
228
- const ext = path.extname(baseName);
229
- name = ext ? baseName.slice(0, -ext.length) : baseName;
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
+ }
230
389
  }
231
390
 
232
391
  const envObj = {};
@@ -241,14 +400,21 @@ export async function runCli(argv = process.argv.slice(2)) {
241
400
 
242
401
  const res = await addService({
243
402
  name,
403
+ group: flags.group || 'default',
244
404
  script: absScriptPath,
245
- nodePath: resolvedNode,
246
- cwd: flags.cwd ? path.resolve(process.cwd(), flags.cwd) : path.dirname(absScriptPath),
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()),
247
410
  env: envObj,
248
411
  envFile: flags.envFile ? path.resolve(process.cwd(), flags.envFile) : undefined,
249
412
  restart: flags.restart,
250
413
  args: flags.args,
251
- start: flags.start
414
+ start: flags.start,
415
+ memoryHigh: flags.memoryHigh,
416
+ memoryMax: flags.memoryMax,
417
+ memorySwapMax: flags.memorySwapMax
252
418
  });
253
419
 
254
420
  console.log(`✓ Service "${res.name}" created at ${res.unitPath}`);
@@ -260,91 +426,228 @@ export async function runCli(argv = process.argv.slice(2)) {
260
426
  break;
261
427
  }
262
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
+
263
447
  case 'start': {
264
- const name = positionals[0];
265
- if (!name) {
266
- throw new Error('Service name is required.\nExample: unitup start api');
448
+ const nameArg = positionals[0];
449
+ if (!nameArg) {
450
+ throw new Error('Service name or @group is required.\nExample: unitup start api or unitup start @myproject');
451
+ }
452
+ const targetNames = await resolveTargetNames(nameArg);
453
+ for (const name of targetNames) {
454
+ await startService(name, flags.enable);
455
+ console.log(`✓ Service "${sanitizeServiceName(name)}" ${flags.enable ? 'enabled & ' : ''}started.`);
267
456
  }
268
- await startService(name, flags.enable);
269
- console.log(`✓ Service "${sanitizeServiceName(name)}" ${flags.enable ? 'enabled & ' : ''}started.`);
270
457
  break;
271
458
  }
272
459
 
273
460
  case 'stop': {
274
- const name = positionals[0];
275
- if (!name) {
276
- throw new Error('Service name is required.\nExample: unitup stop api');
461
+ const nameArg = positionals[0];
462
+ if (!nameArg) {
463
+ throw new Error('Service name or @group is required.\nExample: unitup stop api or unitup stop @myproject');
464
+ }
465
+ const targetNames = await resolveTargetNames(nameArg);
466
+ for (const name of targetNames) {
467
+ await stopService(name);
468
+ console.log(`✓ Service "${sanitizeServiceName(name)}" stopped.`);
277
469
  }
278
- await stopService(name);
279
- console.log(`✓ Service "${sanitizeServiceName(name)}" stopped.`);
280
470
  break;
281
471
  }
282
472
 
283
473
  case 'restart': {
284
- const name = positionals[0];
285
- if (!name) {
286
- throw new Error('Service name is required.\nExample: unitup restart api');
474
+ const nameArg = positionals[0];
475
+ if (!nameArg) {
476
+ throw new Error('Service name or @group is required.\nExample: unitup restart api or unitup restart @myproject');
477
+ }
478
+ const targetNames = await resolveTargetNames(nameArg);
479
+ for (const name of targetNames) {
480
+ await restartService(name);
481
+ console.log(`✓ Service "${sanitizeServiceName(name)}" restarted.`);
287
482
  }
288
- await restartService(name);
289
- console.log(`✓ Service "${sanitizeServiceName(name)}" restarted.`);
290
483
  break;
291
484
  }
292
485
 
293
486
  case 'status': {
294
- const name = positionals[0];
295
- if (!name) {
296
- throw new Error('Service name is required.\nExample: unitup status api');
487
+ const nameArg = positionals[0];
488
+ if (!nameArg) {
489
+ throw new Error('Service name or @group is required.\nExample: unitup status api or unitup status @myproject');
297
490
  }
298
491
 
299
- if (flags.raw) {
300
- const raw = await getServiceStatusRaw(name);
301
- console.log(raw);
302
- } else {
303
- const status = await getServiceStatus(name);
304
- console.log(`${status.name}\n`);
305
- console.log(`Status: ${status.status}`);
306
- console.log(`PID: ${status.pid}`);
307
- console.log(`Started: ${status.started}`);
308
- console.log(`Restarts: ${status.restarts}`);
309
- console.log(`Script: ${status.script}`);
310
- console.log(`Working directory: ${status.cwd}`);
492
+ const targetNames = await resolveTargetNames(nameArg);
493
+ for (const name of targetNames) {
494
+ if (flags.raw) {
495
+ const raw = await getServiceStatusRaw(name);
496
+ console.log(raw);
497
+ } else {
498
+ const status = await getServiceStatus(name);
499
+ console.log(`${status.name}\n`);
500
+ console.log(`Status: ${status.status}`);
501
+ console.log(`PID: ${status.pid}`);
502
+ console.log(`Started: ${status.started}`);
503
+ console.log(`Restarts: ${status.restarts}`);
504
+ console.log(`Command: ${status.command}`);
505
+ console.log(`Arguments: ${status.arguments}`);
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}`);
512
+ }
311
513
  }
312
514
  break;
313
515
  }
314
516
 
315
517
  case 'logs': {
316
- const name = positionals[0];
317
- if (!name) {
318
- throw new Error('Service name is required.\nExample: unitup logs api');
518
+ const nameArg = positionals[0];
519
+ if (!nameArg) {
520
+ throw new Error('Service name or @group is required.\nExample: unitup logs api or unitup logs @myproject');
319
521
  }
320
- const output = await runJournalctlLogs(name, {
321
- follow: flags.follow,
322
- lines: flags.lines,
323
- cat: flags.cat,
324
- output: flags.output
325
- });
326
- if (typeof output === 'string') {
327
- console.log(output);
522
+
523
+ const targetNames = await resolveTargetNames(nameArg);
524
+ for (const name of targetNames) {
525
+ if (targetNames.length > 1) {
526
+ console.log(`=== Logs for ${name} ===`);
527
+ }
528
+ const output = await runJournalctlLogs(name, {
529
+ follow: flags.follow,
530
+ lines: flags.lines,
531
+ cat: flags.cat,
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
540
+ });
541
+ if (typeof output === 'string') {
542
+ console.log(output);
543
+ }
328
544
  }
329
545
  break;
330
546
  }
331
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
+
332
579
  case 'remove': {
333
- const name = positionals[0];
334
- if (!name) {
335
- throw new Error('Service name is required.\nExample: unitup remove api');
580
+ const nameArg = positionals[0];
581
+ if (!nameArg) {
582
+ throw new Error('Service name or @group is required.\nExample: unitup remove api or unitup remove @myproject');
583
+ }
584
+ const targetNames = await resolveTargetNames(nameArg);
585
+ for (const name of targetNames) {
586
+ await removeService(name);
587
+ console.log(`✓ Service "${sanitizeServiceName(name)}" removed.`);
336
588
  }
337
- await removeService(name);
338
- console.log(`✓ Service "${sanitizeServiceName(name)}" removed.`);
339
589
  break;
340
590
  }
341
591
 
592
+ case 'ls':
342
593
  case 'list': {
343
- const services = await listServices();
594
+ const services = await listServices({ group: flags.group });
595
+ if (services.length === 0) {
596
+ if (flags.group) {
597
+ console.log(`No services found in group "${flags.group}".`);
598
+ } else {
599
+ console.log('No unitup user services found.');
600
+ }
601
+ break;
602
+ }
603
+
344
604
  const table = formatTable(services, [
345
605
  { key: 'name', label: 'NAME' },
606
+ { key: 'runtime', label: 'RUNTIME' },
607
+ { key: 'status', label: 'STATUS' },
608
+ { key: 'pid', label: 'PID' },
609
+ { key: 'command', label: 'COMMAND' }
610
+ ]);
611
+ console.log(table);
612
+ break;
613
+ }
614
+
615
+ case 'inspect': {
616
+ const nameArg = positionals[0];
617
+ if (!nameArg) {
618
+ throw new Error('Service name is required.\nExample: unitup inspect api');
619
+ }
620
+ const info = await inspectService(nameArg);
621
+ console.log(`Name: ${info.name}`);
622
+ console.log(`Runtime: ${info.runtime}`);
623
+ console.log(`Group: ${info.group}`);
624
+ console.log(`Status: ${info.status}`);
625
+ console.log(`Command: ${info.command}`);
626
+ console.log(`Arguments: ${info.arguments}`);
627
+ console.log(`Working directory: ${info.cwd}`);
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}`);
634
+ break;
635
+ }
636
+
637
+ case 'failures': {
638
+ const failures = await getServiceFailures();
639
+ if (failures.length === 0) {
640
+ console.log('✓ No failed services.');
641
+ break;
642
+ }
643
+
644
+ const table = formatTable(failures, [
645
+ { key: 'name', label: 'NAME' },
646
+ { key: 'group', label: 'GROUP' },
346
647
  { key: 'status', label: 'STATUS' },
347
- { key: 'enabled', label: 'ENABLED' }
648
+ { key: 'exitCode', label: 'EXIT_CODE' },
649
+ { key: 'restarts', label: 'RESTARTS' },
650
+ { key: 'uptime', label: 'UPTIME' }
348
651
  ]);
349
652
  console.log(table);
350
653
  break;