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/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 Node.js scripts
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> 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
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 --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
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
- 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
- }
356
+ let name = flags.name;
357
+ let absScriptPath = scriptArg ? path.resolve(process.cwd(), scriptArg) : undefined;
246
358
 
247
- const absScriptPath = path.resolve(process.cwd(), scriptArg);
248
- if (!fs.existsSync(absScriptPath)) {
249
- 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
+ }
250
364
  }
251
365
 
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;
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
- nodePath: resolvedNode,
274
- 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()),
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(`Script: ${status.script}`);
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: 'group', label: 'GROUP' },
606
+ { key: 'runtime', label: 'RUNTIME' },
407
607
  { key: 'status', label: 'STATUS' },
408
608
  { key: 'pid', label: 'PID' },
409
- { key: 'uptime', label: 'UPTIME' },
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(`App: ${info.name}`);
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} (${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}`);
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(`Node path: ${info.node}`);
431
- console.log(`Unit file: ${info.unit}`);
432
- console.log(`Unit path: ${info.unitPath}`);
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: 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
+ }