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.
@@ -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 createPhpAdapter(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(['php']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'PHP runtime could not be found.\n\nInstall PHP or specify its path:\n unitup add index.php --command /usr/bin/php'
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: 'php',
44
+ version
45
+ };
46
+ }
@@ -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 createPythonAdapter(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(['python3', 'python']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'Python runtime could not be found.\n\nInstall Python or specify its path:\n unitup add worker.py --command /usr/bin/python3'
26
+ );
27
+ }
28
+
29
+ const version = await getBinaryVersion(execPath, '--version');
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: 'python',
44
+ version
45
+ };
46
+ }
@@ -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 createRubyAdapter(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(['ruby']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'Ruby runtime could not be found.\n\nInstall Ruby or specify its path:\n unitup add app.rb --command /usr/bin/ruby'
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: 'ruby',
44
+ version
45
+ };
46
+ }
@@ -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 createShellAdapter(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(['bash', 'sh']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'Shell runtime could not be found.\n\nInstall bash/sh or specify its path:\n unitup add script.sh --command /bin/bash'
26
+ );
27
+ }
28
+
29
+ const version = await getBinaryVersion(execPath, '--version');
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: 'shell',
44
+ version
45
+ };
46
+ }
package/src/systemd.js CHANGED
@@ -5,7 +5,8 @@ import path from 'node:path';
5
5
  import {
6
6
  sanitizeServiceName,
7
7
  getUnitFilename,
8
- formatRelativeTime
8
+ formatRelativeTime,
9
+ readAppMetadata
9
10
  } from './utils.js';
10
11
  import {
11
12
  getUserUnitDir,
@@ -263,6 +264,8 @@ export async function checkUserLinger() {
263
264
  // Systemd Operations
264
265
  // ---------------------------------------------------------------------------
265
266
 
267
+ import { resolveRuntimeConfig } from './runtimes/index.js';
268
+
266
269
  export async function daemonReload() {
267
270
  const res = await runCommand('systemctl', ['--user', 'daemon-reload']);
268
271
  if (res.code !== 0) {
@@ -277,7 +280,16 @@ export async function resetFailed() {
277
280
 
278
281
  export async function addService(opts) {
279
282
  const safeName = sanitizeServiceName(opts.name);
280
- const { path: unitPath } = writeUnitFile({ ...opts, name: safeName });
283
+ const runtimeConfig = await resolveRuntimeConfig({ ...opts, name: safeName });
284
+
285
+ const { path: unitPath } = writeUnitFile({
286
+ ...opts,
287
+ name: safeName,
288
+ runtime: runtimeConfig.runtime,
289
+ command: runtimeConfig.command,
290
+ args: runtimeConfig.args,
291
+ cwd: opts.cwd
292
+ });
281
293
 
282
294
  await daemonReload();
283
295
 
@@ -289,6 +301,17 @@ export async function addService(opts) {
289
301
  }
290
302
 
291
303
  export async function startService(name, enable = false) {
304
+ if (name && typeof name === 'string' && name.startsWith('@')) {
305
+ const list = await getServicesByGroup(name);
306
+ if (list.length === 0) {
307
+ throw new Error(`No services found in group "${name}".`);
308
+ }
309
+ for (const s of list) {
310
+ await startService(s, enable);
311
+ }
312
+ return true;
313
+ }
314
+
292
315
  const safeName = sanitizeServiceName(name);
293
316
  if (!unitFileExists(safeName)) {
294
317
  throw new Error(`Service "${safeName}" does not exist.\nCreate it with: unitup add <script> --name ${safeName}`);
@@ -306,6 +329,17 @@ export async function startService(name, enable = false) {
306
329
  }
307
330
 
308
331
  export async function stopService(name) {
332
+ if (name && typeof name === 'string' && name.startsWith('@')) {
333
+ const list = await getServicesByGroup(name);
334
+ if (list.length === 0) {
335
+ throw new Error(`No services found in group "${name}".`);
336
+ }
337
+ for (const s of list) {
338
+ await stopService(s);
339
+ }
340
+ return true;
341
+ }
342
+
309
343
  const safeName = sanitizeServiceName(name);
310
344
  if (!unitFileExists(safeName)) {
311
345
  throw new Error(`Service "${safeName}" does not exist.`);
@@ -319,6 +353,17 @@ export async function stopService(name) {
319
353
  }
320
354
 
321
355
  export async function restartService(name) {
356
+ if (name && typeof name === 'string' && name.startsWith('@')) {
357
+ const list = await getServicesByGroup(name);
358
+ if (list.length === 0) {
359
+ throw new Error(`No services found in group "${name}".`);
360
+ }
361
+ for (const s of list) {
362
+ await restartService(s);
363
+ }
364
+ return true;
365
+ }
366
+
322
367
  const safeName = sanitizeServiceName(name);
323
368
  if (!unitFileExists(safeName)) {
324
369
  throw new Error(`Service "${safeName}" does not exist.`);
@@ -332,6 +377,17 @@ export async function restartService(name) {
332
377
  }
333
378
 
334
379
  export async function removeService(name) {
380
+ if (name && typeof name === 'string' && name.startsWith('@')) {
381
+ const list = await getServicesByGroup(name);
382
+ if (list.length === 0) {
383
+ throw new Error(`No services found in group "${name}".`);
384
+ }
385
+ for (const s of list) {
386
+ await removeService(s);
387
+ }
388
+ return true;
389
+ }
390
+
335
391
  const safeName = sanitizeServiceName(name);
336
392
  if (!unitFileExists(safeName)) {
337
393
  throw new Error(`Service "${safeName}" does not exist.`);
@@ -386,11 +442,12 @@ export async function getServiceStatus(name) {
386
442
  // Read unit file content for script and working dir
387
443
  let script = 'unknown';
388
444
  let cwd = 'unknown';
445
+ let parsed = {};
389
446
 
390
447
  const unitPath = getUnitPath(safeName);
391
448
  try {
392
449
  const content = fs.readFileSync(unitPath, 'utf8');
393
- const parsed = parseUnitContent(content);
450
+ parsed = parseUnitContent(content);
394
451
  if (parsed.script) script = parsed.script;
395
452
  if (parsed.cwd) cwd = parsed.cwd;
396
453
  } catch {
@@ -420,6 +477,17 @@ export async function getServiceStatus(name) {
420
477
  ? formatRelativeTime(startedRaw)
421
478
  : 'never';
422
479
 
480
+ const meta = readAppMetadata(safeName);
481
+ const command = meta?.command || parsed.command || meta?.node || process.execPath;
482
+ const argsList = meta?.args || (parsed.script ? [parsed.script] : []);
483
+
484
+ const { formatMemoryBytes } = await import('./utils.js');
485
+ const memoryCurrent = formatMemoryBytes(show.MemoryCurrent);
486
+ const memoryPeak = formatMemoryBytes(show.MemoryPeak);
487
+ const memoryHigh = formatMemoryBytes(show.MemoryHigh || meta?.resources?.memoryHigh || parsed.memoryHigh);
488
+ const memoryMax = formatMemoryBytes(show.MemoryMax || meta?.resources?.memoryMax || parsed.memoryMax);
489
+ const memorySwapMax = formatMemoryBytes(show.MemorySwapMax || meta?.resources?.memorySwapMax || parsed.memorySwapMax);
490
+
423
491
  return {
424
492
  name: safeName,
425
493
  unitFile: getUnitFilename(safeName),
@@ -430,16 +498,66 @@ export async function getServiceStatus(name) {
430
498
  restarts,
431
499
  started,
432
500
  startedRaw,
501
+ command,
502
+ arguments: argsList.join(' '),
503
+ args: argsList,
433
504
  script,
434
- cwd
505
+ cwd,
506
+ memory: memoryCurrent,
507
+ memoryPeak,
508
+ memoryHigh,
509
+ memoryMax,
510
+ memorySwapMax
435
511
  };
436
512
  }
437
513
 
438
- export async function listServices() {
514
+ export async function getServicesByGroup(groupName) {
515
+ const cleanGroup = groupName.startsWith('@') ? groupName.slice(1) : groupName;
516
+ const units = listUnitFiles();
517
+ const matched = [];
518
+ for (const unit of units) {
519
+ const meta = readAppMetadata(unit.name);
520
+ const itemGroup = meta?.group || 'default';
521
+ if (itemGroup.toLowerCase() === cleanGroup.toLowerCase()) {
522
+ matched.push(unit.name);
523
+ }
524
+ }
525
+ return matched;
526
+ }
527
+
528
+ export async function listServices(filterOpts = {}) {
439
529
  const units = listUnitFiles();
440
530
  const result = [];
531
+ const targetGroup = filterOpts.group
532
+ ? (filterOpts.group.startsWith('@') ? filterOpts.group.slice(1) : filterOpts.group).toLowerCase()
533
+ : null;
441
534
 
442
535
  for (const unit of units) {
536
+ const meta = readAppMetadata(unit.name);
537
+ const group = meta?.group || 'default';
538
+ const runtime = meta?.runtime || 'node';
539
+
540
+ if (targetGroup && group.toLowerCase() !== targetGroup) {
541
+ continue;
542
+ }
543
+
544
+ let commandSummary = '';
545
+ if (meta?.command) {
546
+ if (runtime === 'native') {
547
+ commandSummary = './' + path.basename(meta.command);
548
+ } else {
549
+ const cmdBase = path.basename(meta.command);
550
+ const argsStr = (meta.args || [])
551
+ .map(a => (a.startsWith('/') || a.startsWith('./') ? path.basename(a) : a))
552
+ .join(' ');
553
+ commandSummary = (cmdBase + (argsStr ? ' ' + argsStr : '')).trim();
554
+ }
555
+ } else if (meta?.script) {
556
+ commandSummary = `node ${path.basename(meta.script)}`;
557
+ } else {
558
+ commandSummary = 'unknown';
559
+ }
560
+
443
561
  try {
444
562
  const show = await getServiceShow(unit.name);
445
563
  const activeState = show.ActiveState || 'inactive';
@@ -458,17 +576,35 @@ export async function listServices() {
458
576
  }
459
577
 
460
578
  const enabled = unitFileState.startsWith('enabled') ? 'yes' : 'no';
579
+ const pid = show.MainPID && show.MainPID !== '0' ? show.MainPID : '-';
580
+ const restarts = show.NRestarts ?? '0';
581
+ const startedRaw = show.ActiveEnterTimestamp;
582
+ const uptime = startedRaw && startedRaw !== '0' && startedRaw !== 'n/a'
583
+ ? formatRelativeTime(startedRaw)
584
+ : 'never';
461
585
 
462
586
  result.push({
463
587
  name: unit.name,
588
+ runtime,
589
+ group,
464
590
  status,
465
- enabled
591
+ enabled,
592
+ pid,
593
+ command: commandSummary,
594
+ uptime,
595
+ restarts
466
596
  });
467
597
  } catch {
468
598
  result.push({
469
599
  name: unit.name,
600
+ runtime,
601
+ group,
470
602
  status: 'unknown',
471
- enabled: 'unknown'
603
+ enabled: 'unknown',
604
+ pid: '-',
605
+ command: commandSummary,
606
+ uptime: 'never',
607
+ restarts: '0'
472
608
  });
473
609
  }
474
610
  }
@@ -476,9 +612,79 @@ export async function listServices() {
476
612
  return result;
477
613
  }
478
614
 
615
+ export async function inspectService(name) {
616
+ const safeName = sanitizeServiceName(name);
617
+ if (!unitFileExists(safeName)) {
618
+ throw new Error(`Service "${safeName}" does not exist.`);
619
+ }
620
+
621
+ const meta = readAppMetadata(safeName);
622
+ const statusObj = await getServiceStatus(safeName);
623
+
624
+ const command = meta?.command || statusObj.command || statusObj.node || process.execPath;
625
+ const argsList = meta?.args || (statusObj.script ? [statusObj.script] : []);
626
+
627
+ return {
628
+ name: safeName,
629
+ runtime: meta?.runtime || 'node',
630
+ status: statusObj.status,
631
+ activeState: statusObj.activeState,
632
+ subState: statusObj.subState,
633
+ command,
634
+ arguments: argsList.join(' '),
635
+ args: argsList,
636
+ cwd: meta?.cwd || statusObj.cwd,
637
+ unit: getUnitFilename(safeName),
638
+ unitPath: getUnitPath(safeName),
639
+ group: meta?.group || 'default',
640
+ pid: statusObj.pid,
641
+ restarts: statusObj.restarts,
642
+ started: statusObj.started,
643
+ memory: statusObj.memory,
644
+ memoryPeak: statusObj.memoryPeak,
645
+ memoryHigh: statusObj.memoryHigh,
646
+ memoryMax: statusObj.memoryMax,
647
+ memorySwapMax: statusObj.memorySwapMax,
648
+ // Legacy fields for backward compatibility
649
+ script: meta?.script || statusObj.script,
650
+ node: meta?.node || command
651
+ };
652
+ }
653
+
654
+ export async function getServiceFailures() {
655
+ const allServices = await listServices();
656
+ const failures = [];
657
+
658
+ for (const item of allServices) {
659
+ if (item.status === 'failed') {
660
+ const show = await getServiceShow(item.name);
661
+ const exitCode = show.ExecMainStatus || show.ExecMainCode || '1';
662
+ failures.push({
663
+ name: item.name,
664
+ group: item.group,
665
+ status: 'failed',
666
+ exitCode: String(exitCode),
667
+ restarts: item.restarts,
668
+ uptime: item.uptime
669
+ });
670
+ }
671
+ }
672
+
673
+ return failures;
674
+ }
675
+
479
676
  export async function runJournalctlLogs(name, opts = {}) {
480
677
  const safeName = sanitizeServiceName(name);
481
678
  const unitFilename = getUnitFilename(safeName);
679
+
680
+ if (opts.diskUsage) {
681
+ const res = await runCommand('journalctl', ['--disk-usage']);
682
+ if (res.code !== 0 && (res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root'))) {
683
+ throw new Error('Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.');
684
+ }
685
+ return res.stdout || res.stderr;
686
+ }
687
+
482
688
  const syslogId = `unitup-${safeName}`;
483
689
 
484
690
  const candidateArgSets = [
@@ -509,19 +715,27 @@ export async function runJournalctlLogs(name, opts = {}) {
509
715
  }
510
716
 
511
717
  const workingCandidate = chosenCandidate || ['--user', '-u', unitFilename];
718
+ const filterArgs = [...workingCandidate];
719
+
720
+ if (opts.since) filterArgs.push(`--since=${opts.since}`);
721
+ if (opts.until) filterArgs.push(`--until=${opts.until}`);
722
+ if (opts.priority) filterArgs.push(`--priority=${opts.priority}`);
723
+ if (opts.grep) filterArgs.push(`--grep=${opts.grep}`);
724
+ if (opts.boot) filterArgs.push('--boot');
725
+
726
+ if (opts.json) {
727
+ filterArgs.push('-o', 'json');
728
+ } else if (opts.cat || opts.output === 'cat') {
729
+ filterArgs.push('-o', 'cat');
730
+ } else if (opts.output) {
731
+ filterArgs.push('-o', opts.output);
732
+ }
512
733
 
513
734
  if (opts.follow) {
514
- const followArgs = [...workingCandidate, '-f'];
515
- if (opts.lines) {
516
- followArgs.push('-n', String(opts.lines));
517
- }
518
- if (opts.cat || opts.output === 'cat') {
519
- followArgs.push('-o', 'cat');
520
- } else if (opts.output) {
521
- followArgs.push('-o', opts.output);
522
- }
735
+ filterArgs.push('-f');
736
+ if (opts.lines) filterArgs.push('-n', String(opts.lines));
523
737
  try {
524
- const child = spawn('journalctl', followArgs, { stdio: 'inherit', shell: false });
738
+ const child = spawn('journalctl', filterArgs, { stdio: 'inherit', shell: false });
525
739
  child.on('error', () => {});
526
740
  return child;
527
741
  } catch {
@@ -529,15 +743,125 @@ export async function runJournalctlLogs(name, opts = {}) {
529
743
  }
530
744
  }
531
745
 
532
- const staticArgs = [...workingCandidate, '--no-pager'];
533
- if (opts.cat || opts.output === 'cat') {
534
- staticArgs.push('-o', 'cat');
535
- } else if (opts.output) {
536
- staticArgs.push('-o', opts.output);
537
- }
746
+ filterArgs.push('--no-pager');
538
747
  const lines = opts.lines ? String(opts.lines) : '100';
539
- staticArgs.push('-n', lines);
748
+ filterArgs.push('-n', lines);
540
749
 
541
- const res = await runCommand('journalctl', staticArgs);
750
+ const res = await runCommand('journalctl', filterArgs);
542
751
  return res.stdout || res.stderr || 'No logs found.';
543
752
  }
753
+
754
+ export async function setServiceLimits(name, options = {}) {
755
+ const safeName = sanitizeServiceName(name);
756
+ if (!unitFileExists(safeName)) {
757
+ throw new Error(`Service "${safeName}" does not exist.`);
758
+ }
759
+
760
+ const meta = readAppMetadata(safeName) || {};
761
+ const { validateMemorySize } = await import('./utils.js');
762
+
763
+ let memoryHigh = options.memoryHigh;
764
+ let memoryMax = options.memoryMax;
765
+ let memorySwapMax = options.memorySwapMax;
766
+
767
+ const resources = { ...(meta.resources || {}) };
768
+
769
+ if (options.resetMemory) {
770
+ delete resources.memoryHigh;
771
+ delete resources.memoryMax;
772
+ delete resources.memorySwapMax;
773
+ memoryHigh = undefined;
774
+ memoryMax = undefined;
775
+ memorySwapMax = undefined;
776
+ } else {
777
+ if (memoryHigh !== undefined) {
778
+ memoryHigh = validateMemorySize(memoryHigh, 'MemoryHigh');
779
+ resources.memoryHigh = memoryHigh;
780
+ }
781
+ if (memoryMax !== undefined) {
782
+ memoryMax = validateMemorySize(memoryMax, 'MemoryMax');
783
+ resources.memoryMax = memoryMax;
784
+ }
785
+ if (memorySwapMax !== undefined) {
786
+ memorySwapMax = validateMemorySize(memorySwapMax, 'MemorySwapMax');
787
+ resources.memorySwapMax = memorySwapMax;
788
+ }
789
+ }
790
+
791
+ const updatedMeta = {
792
+ ...meta,
793
+ name: safeName,
794
+ resources: Object.keys(resources).length > 0 ? resources : undefined,
795
+ memoryHigh: resources.memoryHigh,
796
+ memoryMax: resources.memoryMax,
797
+ memorySwapMax: resources.memorySwapMax
798
+ };
799
+
800
+ writeUnitFile(updatedMeta);
801
+
802
+ await runCommand('systemctl', ['--user', 'daemon-reload']);
803
+
804
+ const status = await getServiceStatus(safeName);
805
+ if (status.status === 'running') {
806
+ const setPropsArgs = ['--user', 'set-property', getUnitFilename(safeName)];
807
+ if (options.resetMemory) {
808
+ setPropsArgs.push('MemoryHigh=infinity', 'MemoryMax=infinity', 'MemorySwapMax=infinity');
809
+ } else {
810
+ if (resources.memoryHigh) setPropsArgs.push(`MemoryHigh=${resources.memoryHigh}`);
811
+ if (resources.memoryMax) setPropsArgs.push(`MemoryMax=${resources.memoryMax}`);
812
+ if (resources.memorySwapMax) setPropsArgs.push(`MemorySwapMax=${resources.memorySwapMax}`);
813
+ }
814
+
815
+ if (setPropsArgs.length > 3) {
816
+ try {
817
+ await runCommand('systemctl', setPropsArgs);
818
+ } catch {
819
+ // Graceful fallback if runtime property setting fails
820
+ }
821
+ }
822
+ }
823
+
824
+ return await inspectService(safeName);
825
+ }
826
+
827
+ export async function executeJournalctlMaintenance(action, opts = {}) {
828
+ const args = [];
829
+ if (action === 'disk-usage') {
830
+ args.push('--disk-usage');
831
+ } else if (action === 'rotate') {
832
+ args.push('--rotate');
833
+ } else if (action === 'vacuum') {
834
+ const { validateMemorySize } = await import('./utils.js');
835
+ if (opts.size) {
836
+ const validSize = validateMemorySize(opts.size, 'Vacuum size');
837
+ args.push(`--vacuum-size=${validSize}`);
838
+ } else if (opts.time) {
839
+ const timeStr = String(opts.time).trim();
840
+ if (/[;&|$`"'\n\r\t ]/.test(timeStr)) {
841
+ throw new Error(`Vacuum time contains invalid characters or shell injection: "${timeStr}".`);
842
+ }
843
+ args.push(`--vacuum-time=${timeStr}`);
844
+ } else if (opts.files) {
845
+ const filesNum = parseInt(opts.files, 10);
846
+ if (Number.isNaN(filesNum) || filesNum <= 0) {
847
+ throw new Error(`Invalid vacuum files count: "${opts.files}". Must be a positive integer.`);
848
+ }
849
+ args.push(`--vacuum-files=${filesNum}`);
850
+ } else {
851
+ throw new Error('Vacuum action requires one of --size, --time, or --files.');
852
+ }
853
+ } else {
854
+ throw new Error(`Unknown journal maintenance action: "${action}".`);
855
+ }
856
+
857
+ if (opts.dryRun) {
858
+ return `[dry-run] Would execute: journalctl ${args.join(' ')}`;
859
+ }
860
+
861
+ const res = await runCommand('journalctl', args);
862
+ if (res.code !== 0 && (res.stderr.toLowerCase().includes('permission') || res.stderr.toLowerCase().includes('root') || res.stderr.toLowerCase().includes('access denied'))) {
863
+ throw new Error('Journal maintenance requires additional privileges on this system.\nRun the command manually with the appropriate permissions.');
864
+ }
865
+
866
+ return (res.stdout || res.stderr || 'Journal maintenance completed successfully.').trim();
867
+ }