unitup 0.0.9 → 0.0.11

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,
@@ -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,24 @@ export function parseArgs(argv) {
46
52
  output: '',
47
53
  lines: 100,
48
54
  help: false,
49
- version: false
55
+ version: false,
56
+ force: false,
57
+ memoryHigh: '',
58
+ memoryMax: '',
59
+ memorySwapMax: '',
60
+ resetMemory: false,
61
+ since: '',
62
+ until: '',
63
+ priority: '',
64
+ grep: '',
65
+ boot: false,
66
+ json: false,
67
+ diskUsage: false,
68
+ size: '',
69
+ time: '',
70
+ files: '',
71
+ yes: false,
72
+ dryRun: false
50
73
  }
51
74
  };
52
75
 
@@ -60,6 +83,24 @@ export function parseArgs(argv) {
60
83
  } else if (arg === '-v' || arg === '--version') {
61
84
  result.flags.version = true;
62
85
  i++;
86
+ } else if (arg === '--runtime') {
87
+ result.flags.runtime = argv[i + 1] || '';
88
+ i += 2;
89
+ } else if (arg.startsWith('--runtime=')) {
90
+ result.flags.runtime = arg.slice(10);
91
+ i++;
92
+ } else if (arg === '--runtime-arg') {
93
+ if (argv[i + 1]) result.flags.runtimeArgs.push(argv[i + 1]);
94
+ i += 2;
95
+ } else if (arg.startsWith('--runtime-arg=')) {
96
+ result.flags.runtimeArgs.push(arg.slice(14));
97
+ i++;
98
+ } else if (arg === '--command') {
99
+ result.flags.command = argv[i + 1] || '';
100
+ i += 2;
101
+ } else if (arg.startsWith('--command=')) {
102
+ result.flags.command = arg.slice(10);
103
+ i++;
63
104
  } else if (arg === '--node') {
64
105
  result.flags.node = argv[i + 1] || '';
65
106
  i += 2;
@@ -123,8 +164,9 @@ export function parseArgs(argv) {
123
164
  } else if (arg === '--raw') {
124
165
  result.flags.raw = true;
125
166
  i++;
126
- } else if (arg === '-f' || arg === '--follow') {
167
+ } else if (arg === '-f' || arg === '--follow' || arg === '--force') {
127
168
  result.flags.follow = true;
169
+ result.flags.force = true;
128
170
  i++;
129
171
  } else if (arg === '-c' || arg === '--cat') {
130
172
  result.flags.cat = true;
@@ -135,6 +177,84 @@ export function parseArgs(argv) {
135
177
  } else if (arg.startsWith('--output=')) {
136
178
  result.flags.output = arg.slice(9);
137
179
  i++;
180
+ } else if (arg === '--memory-high') {
181
+ result.flags.memoryHigh = argv[i + 1] || '';
182
+ i += 2;
183
+ } else if (arg.startsWith('--memory-high=')) {
184
+ result.flags.memoryHigh = arg.slice(14);
185
+ i++;
186
+ } else if (arg === '--memory-max') {
187
+ result.flags.memoryMax = argv[i + 1] || '';
188
+ i += 2;
189
+ } else if (arg.startsWith('--memory-max=')) {
190
+ result.flags.memoryMax = arg.slice(13);
191
+ i++;
192
+ } else if (arg === '--memory-swap-max') {
193
+ result.flags.memorySwapMax = argv[i + 1] || '';
194
+ i += 2;
195
+ } else if (arg.startsWith('--memory-swap-max=')) {
196
+ result.flags.memorySwapMax = arg.slice(18);
197
+ i++;
198
+ } else if (arg === '--reset-memory') {
199
+ result.flags.resetMemory = true;
200
+ i++;
201
+ } else if (arg === '--since') {
202
+ result.flags.since = argv[i + 1] || '';
203
+ i += 2;
204
+ } else if (arg.startsWith('--since=')) {
205
+ result.flags.since = arg.slice(8);
206
+ i++;
207
+ } else if (arg === '--until') {
208
+ result.flags.until = argv[i + 1] || '';
209
+ i += 2;
210
+ } else if (arg.startsWith('--until=')) {
211
+ result.flags.until = arg.slice(8);
212
+ i++;
213
+ } else if (arg === '--priority') {
214
+ result.flags.priority = argv[i + 1] || '';
215
+ i += 2;
216
+ } else if (arg.startsWith('--priority=')) {
217
+ result.flags.priority = arg.slice(11);
218
+ i++;
219
+ } else if (arg === '--grep') {
220
+ result.flags.grep = argv[i + 1] || '';
221
+ i += 2;
222
+ } else if (arg.startsWith('--grep=')) {
223
+ result.flags.grep = arg.slice(7);
224
+ i++;
225
+ } else if (arg === '--boot' || arg === '-b') {
226
+ result.flags.boot = true;
227
+ i++;
228
+ } else if (arg === '--json') {
229
+ result.flags.json = true;
230
+ i++;
231
+ } else if (arg === '--disk-usage') {
232
+ result.flags.diskUsage = true;
233
+ i++;
234
+ } else if (arg === '--size') {
235
+ result.flags.size = argv[i + 1] || '';
236
+ i += 2;
237
+ } else if (arg.startsWith('--size=')) {
238
+ result.flags.size = arg.slice(7);
239
+ i++;
240
+ } else if (arg === '--time') {
241
+ result.flags.time = argv[i + 1] || '';
242
+ i += 2;
243
+ } else if (arg.startsWith('--time=')) {
244
+ result.flags.time = arg.slice(7);
245
+ i++;
246
+ } else if (arg === '--files') {
247
+ result.flags.files = argv[i + 1] || '';
248
+ i += 2;
249
+ } else if (arg.startsWith('--files=')) {
250
+ result.flags.files = arg.slice(8);
251
+ i++;
252
+ } else if (arg === '--yes' || arg === '-y') {
253
+ result.flags.yes = true;
254
+ i++;
255
+ } else if (arg === '--dry-run') {
256
+ result.flags.dryRun = true;
257
+ i++;
138
258
  } else if (!arg.startsWith('-')) {
139
259
  if (!result.command) {
140
260
  result.command = arg;
@@ -153,11 +273,11 @@ export function parseArgs(argv) {
153
273
 
154
274
  export function printHelp() {
155
275
  console.log(`
156
- unitup - Minimal systemd user service wrapper for Node.js scripts
276
+ unitup - Minimal systemd user service wrapper for any executable & runtime
157
277
 
158
278
  Usage:
159
- unitup doctor Run system readiness check
160
- unitup add <script> [options] Add script as systemd user service
279
+ unitup doctor Run system readiness and runtime check
280
+ unitup add <script> [options] Add script or executable as systemd user service
161
281
  unitup start <name|@group> Start a service (--enable to enable on boot)
162
282
  unitup stop <name|@group> Stop a service
163
283
  unitup restart <name|@group> Restart a service
@@ -169,23 +289,23 @@ Usage:
169
289
  unitup list / unitup ls List all services (--group <group>)
170
290
 
171
291
  Add Options:
172
- --name <name> Service name (default: script name)
173
- --group <group> Assign service to a group (default: default)
174
- --node <path> Custom Node.js executable path
175
- --cwd <path> Working directory
176
- --restart <policy> Restart policy (on-failure, always, etc. Default: on-failure)
177
- --env KEY=value Environment variable (can be specified multiple times)
178
- --env-file <file> Path to environment file
179
- --arg <value> Script argument (can be specified multiple times)
180
- --start Enable and start service immediately after creation
292
+ --name <name> Service name (default: script/executable name)
293
+ --runtime <name> Runtime (node, python, ruby, php, bun, deno, shell, go, elixir, native)
294
+ --runtime-arg <val> Runtime argument (can be specified multiple times)
295
+ --command <path> Custom executable command path (bypasses auto-detection)
296
+ --arg <value> Argument to pass to command (can be specified multiple times)
297
+ --group <group> Assign service to a group (default: default)
298
+ --cwd <path> Working directory
299
+ --restart <policy> Restart policy (on-failure, always, etc. Default: on-failure)
300
+ --env KEY=value Environment variable (can be specified multiple times)
301
+ --env-file <file> Path to environment file
302
+ --start Enable and start service immediately after creation
181
303
 
182
304
  Examples:
183
- unitup add server.js --name api --group myproject --start
184
- unitup list --group myproject
185
- unitup start @myproject
186
- unitup inspect api
187
- unitup failures
188
- unitup remove @myproject
305
+ unitup add server.js --runtime node
306
+ unitup add worker.py --runtime python
307
+ unitup add ./server --runtime native
308
+ unitup add --name worker --command /usr/bin/python3 --arg worker.py --arg --port --arg 3000
189
309
  `);
190
310
  }
191
311
 
@@ -235,25 +355,39 @@ export async function runCli(argv = process.argv.slice(2)) {
235
355
 
236
356
  case 'add': {
237
357
  const scriptArg = positionals[0];
238
- if (!scriptArg) {
239
- throw new Error('Script file path is required.\nExample: unitup add app.js');
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
- }
358
+ let name = flags.name;
359
+ let absScriptPath = scriptArg ? path.resolve(process.cwd(), scriptArg) : undefined;
246
360
 
247
- const absScriptPath = path.resolve(process.cwd(), scriptArg);
248
- if (!fs.existsSync(absScriptPath)) {
249
- throw new Error(`Script file does not exist: ${absScriptPath}`);
361
+ if (flags.node) {
362
+ const resolvedNode = await findNodeExecutable(flags.node);
363
+ if (!resolvedNode) {
364
+ throw new Error('Node.js is required but not found.\nRun: unitup doctor');
365
+ }
250
366
  }
251
367
 
252
- let name = flags.name;
253
- if (!name) {
254
- const baseName = path.basename(scriptArg);
255
- const ext = path.extname(baseName);
256
- name = ext ? baseName.slice(0, -ext.length) : baseName;
368
+ if (flags.command) {
369
+ if (!name) {
370
+ if (scriptArg) {
371
+ const baseName = path.basename(scriptArg);
372
+ const ext = path.extname(baseName);
373
+ name = ext ? baseName.slice(0, -ext.length) : baseName;
374
+ } else {
375
+ const baseCmd = path.basename(flags.command);
376
+ name = baseCmd;
377
+ }
378
+ }
379
+ } else {
380
+ if (!scriptArg) {
381
+ throw new Error('Script file path or --command is required.\nExample: unitup add app.py --runtime python');
382
+ }
383
+ if (!fs.existsSync(absScriptPath)) {
384
+ throw new Error(`Script file does not exist: ${absScriptPath}`);
385
+ }
386
+ if (!name) {
387
+ const baseName = path.basename(scriptArg);
388
+ const ext = path.extname(baseName);
389
+ name = ext ? baseName.slice(0, -ext.length) : baseName;
390
+ }
257
391
  }
258
392
 
259
393
  const envObj = {};
@@ -270,13 +404,20 @@ export async function runCli(argv = process.argv.slice(2)) {
270
404
  name,
271
405
  group: flags.group || 'default',
272
406
  script: absScriptPath,
273
- nodePath: resolvedNode,
274
- cwd: flags.cwd ? path.resolve(process.cwd(), flags.cwd) : path.dirname(absScriptPath),
407
+ command: flags.command,
408
+ runtime: flags.runtime,
409
+ runtimeArgs: flags.runtimeArgs,
410
+ nodePath: flags.node,
411
+ cwd: flags.cwd ? path.resolve(process.cwd(), flags.cwd) : (absScriptPath ? path.dirname(absScriptPath) : process.cwd()),
275
412
  env: envObj,
276
413
  envFile: flags.envFile ? path.resolve(process.cwd(), flags.envFile) : undefined,
277
414
  restart: flags.restart,
278
415
  args: flags.args,
279
- start: flags.start
416
+ start: flags.start,
417
+ memoryHigh: flags.memoryHigh,
418
+ memoryMax: flags.memoryMax,
419
+ memorySwapMax: flags.memorySwapMax,
420
+ force: flags.force
280
421
  });
281
422
 
282
423
  console.log(`✓ Service "${res.name}" created at ${res.unitPath}`);
@@ -288,6 +429,24 @@ export async function runCli(argv = process.argv.slice(2)) {
288
429
  break;
289
430
  }
290
431
 
432
+ case 'limits': {
433
+ const nameArg = positionals[0];
434
+ if (!nameArg) {
435
+ throw new Error('Service name is required.\nExample: unitup limits api --memory-high 400M --memory-max 512M');
436
+ }
437
+ const info = await setServiceLimits(nameArg, {
438
+ memoryHigh: flags.memoryHigh,
439
+ memoryMax: flags.memoryMax,
440
+ memorySwapMax: flags.memorySwapMax,
441
+ resetMemory: flags.resetMemory
442
+ });
443
+ console.log(`✓ Service "${info.name}" limits updated.`);
444
+ console.log(`Memory High: ${info.memoryHigh}`);
445
+ console.log(`Memory Max: ${info.memoryMax}`);
446
+ console.log(`Swap Max: ${info.memorySwapMax}`);
447
+ break;
448
+ }
449
+
291
450
  case 'start': {
292
451
  const nameArg = positionals[0];
293
452
  if (!nameArg) {
@@ -345,8 +504,14 @@ export async function runCli(argv = process.argv.slice(2)) {
345
504
  console.log(`PID: ${status.pid}`);
346
505
  console.log(`Started: ${status.started}`);
347
506
  console.log(`Restarts: ${status.restarts}`);
348
- console.log(`Script: ${status.script}`);
507
+ console.log(`Command: ${status.command}`);
508
+ console.log(`Arguments: ${status.arguments}`);
349
509
  console.log(`Working directory: ${status.cwd}`);
510
+ console.log(`Memory: ${status.memory}`);
511
+ console.log(`Memory Peak: ${status.memoryPeak}`);
512
+ console.log(`Memory High: ${status.memoryHigh}`);
513
+ console.log(`Memory Max: ${status.memoryMax}`);
514
+ console.log(`Swap Max: ${status.memorySwapMax}`);
350
515
  }
351
516
  }
352
517
  break;
@@ -367,7 +532,14 @@ export async function runCli(argv = process.argv.slice(2)) {
367
532
  follow: flags.follow,
368
533
  lines: flags.lines,
369
534
  cat: flags.cat,
370
- output: flags.output
535
+ output: flags.output,
536
+ since: flags.since,
537
+ until: flags.until,
538
+ priority: flags.priority,
539
+ grep: flags.grep,
540
+ boot: flags.boot,
541
+ json: flags.json,
542
+ diskUsage: flags.diskUsage
371
543
  });
372
544
  if (typeof output === 'string') {
373
545
  console.log(output);
@@ -376,6 +548,37 @@ export async function runCli(argv = process.argv.slice(2)) {
376
548
  break;
377
549
  }
378
550
 
551
+ case 'journal': {
552
+ const action = positionals[0];
553
+ if (!action) {
554
+ throw new Error('Journal action is required (disk-usage, rotate, vacuum).\nExample: unitup journal vacuum --size 500M');
555
+ }
556
+
557
+ if (action === 'vacuum' && !flags.yes && !flags.dryRun) {
558
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
559
+ const answer = await new Promise((resolve) => {
560
+ rl.question('This operation affects archived journal logs system-wide,\nnot only services managed by unitup.\n\nContinue? [y/N] ', (ans) => {
561
+ rl.close();
562
+ resolve(ans.trim());
563
+ });
564
+ });
565
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
566
+ console.log('Aborted.');
567
+ break;
568
+ }
569
+ }
570
+
571
+ const result = await executeJournalctlMaintenance(action, {
572
+ size: flags.size,
573
+ time: flags.time,
574
+ files: flags.files,
575
+ yes: flags.yes,
576
+ dryRun: flags.dryRun
577
+ });
578
+ console.log(result);
579
+ break;
580
+ }
581
+
379
582
  case 'remove': {
380
583
  const nameArg = positionals[0];
381
584
  if (!nameArg) {
@@ -383,7 +586,7 @@ export async function runCli(argv = process.argv.slice(2)) {
383
586
  }
384
587
  const targetNames = await resolveTargetNames(nameArg);
385
588
  for (const name of targetNames) {
386
- await removeService(name);
589
+ await removeService(name, { force: flags.force });
387
590
  console.log(`✓ Service "${sanitizeServiceName(name)}" removed.`);
388
591
  }
389
592
  break;
@@ -403,11 +606,10 @@ export async function runCli(argv = process.argv.slice(2)) {
403
606
 
404
607
  const table = formatTable(services, [
405
608
  { key: 'name', label: 'NAME' },
406
- { key: 'group', label: 'GROUP' },
609
+ { key: 'runtime', label: 'RUNTIME' },
407
610
  { key: 'status', label: 'STATUS' },
408
611
  { key: 'pid', label: 'PID' },
409
- { key: 'uptime', label: 'UPTIME' },
410
- { key: 'restarts', label: 'RESTARTS' }
612
+ { key: 'command', label: 'COMMAND' }
411
613
  ]);
412
614
  console.log(table);
413
615
  break;
@@ -419,17 +621,19 @@ export async function runCli(argv = process.argv.slice(2)) {
419
621
  throw new Error('Service name is required.\nExample: unitup inspect api');
420
622
  }
421
623
  const info = await inspectService(nameArg);
422
- console.log(`App: ${info.name}`);
624
+ console.log(`Name: ${info.name}`);
625
+ console.log(`Runtime: ${info.runtime}`);
423
626
  console.log(`Group: ${info.group}`);
424
- console.log(`Status: ${info.status} (${info.activeState}/${info.subState})`);
425
- console.log(`PID: ${info.pid}`);
426
- console.log(`Restarts: ${info.restarts}`);
427
- console.log(`Uptime: ${info.started}`);
428
- console.log(`Script: ${info.script}`);
627
+ console.log(`Status: ${info.status}`);
628
+ console.log(`Command: ${info.command}`);
629
+ console.log(`Arguments: ${info.arguments}`);
429
630
  console.log(`Working directory: ${info.cwd}`);
430
- console.log(`Node path: ${info.node}`);
431
- console.log(`Unit file: ${info.unit}`);
432
- console.log(`Unit path: ${info.unitPath}`);
631
+ console.log(`Unit: ${info.unit}`);
632
+ console.log(`Memory: ${info.memory}`);
633
+ console.log(`Memory Peak: ${info.memoryPeak}`);
634
+ console.log(`Memory High: ${info.memoryHigh}`);
635
+ console.log(`Memory Max: ${info.memoryMax}`);
636
+ console.log(`Swap Max: ${info.memorySwapMax}`);
433
637
  break;
434
638
  }
435
639
 
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: nodeDiag.execPath || process.execPath,
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
- const n = info.nodeDiag;
75
- if (n.found && n.executable) {
76
- console.log(`✓ Node.js: ${n.execPath}`);
77
- if (n.version) {
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('! node is not in standard PATH');
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(`✗ ${n.error || 'Node.js executable could not be resolved'}`);
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
+ }