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.
@@ -0,0 +1,73 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { runCommand } from '../systemd.js';
4
+
5
+ export function findExecutableInPath(binaryName) {
6
+ // 1. Check standard PATH env
7
+ const envPath = process.env.PATH || '';
8
+ const dirs = envPath.split(path.delimiter).filter(Boolean);
9
+
10
+ for (const dir of dirs) {
11
+ const full = path.join(dir, binaryName);
12
+ try {
13
+ if (fs.existsSync(full)) {
14
+ fs.accessSync(full, fs.constants.X_OK);
15
+ return full;
16
+ }
17
+ } catch {
18
+ // ignore
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export async function findRuntimeExecutable(binaryNames) {
25
+ const names = Array.isArray(binaryNames) ? binaryNames : [binaryNames];
26
+
27
+ for (const name of names) {
28
+ if (path.isAbsolute(name)) {
29
+ if (fs.existsSync(name)) {
30
+ try {
31
+ fs.accessSync(name, fs.constants.X_OK);
32
+ return name;
33
+ } catch {
34
+ // ignore
35
+ }
36
+ }
37
+ continue;
38
+ }
39
+
40
+ // Try finding in system PATH directly
41
+ const foundInPath = findExecutableInPath(name);
42
+ if (foundInPath) return foundInPath;
43
+
44
+ // Fallback to which/where system call
45
+ try {
46
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
47
+ const res = await runCommand(whichCmd, [name]);
48
+ if (res.code === 0 && res.stdout.trim()) {
49
+ const p = res.stdout.trim().split('\n')[0].trim();
50
+ if (fs.existsSync(p)) {
51
+ fs.accessSync(p, fs.constants.X_OK);
52
+ return p;
53
+ }
54
+ }
55
+ } catch {
56
+ // ignore
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ export async function getBinaryVersion(execPath, versionFlag = '-v') {
64
+ try {
65
+ const res = await runCommand(execPath, [versionFlag]);
66
+ if (res.code === 0 && res.stdout.trim()) {
67
+ return res.stdout.trim().split('\n')[0].trim();
68
+ }
69
+ } catch {
70
+ // ignore
71
+ }
72
+ return 'unknown';
73
+ }
@@ -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 createDenoAdapter(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(['deno']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'Deno runtime could not be found.\n\nInstall Deno or specify its path:\n unitup add server.ts --command /usr/bin/deno'
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 = ['run', ...runtimeArgs];
35
+ if (scriptPath) {
36
+ args.push(scriptPath);
37
+ }
38
+ args.push(...scriptArgs);
39
+
40
+ return {
41
+ command: execPath,
42
+ args,
43
+ runtime: 'deno',
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 createElixirAdapter(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(['elixir']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'Elixir runtime could not be found.\n\nInstall Elixir or specify its path:\n unitup add app.exs --command /usr/bin/elixir'
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: 'elixir',
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 createGoAdapter(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(['go']);
21
+ }
22
+
23
+ if (!execPath) {
24
+ throw new Error(
25
+ 'Go runtime could not be found.\n\nInstall Go or specify its path:\n unitup add main.go --command /usr/local/go/bin/go'
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 = ['run', ...runtimeArgs];
35
+ if (scriptPath) {
36
+ args.push(scriptPath);
37
+ }
38
+ args.push(...scriptArgs);
39
+
40
+ return {
41
+ command: execPath,
42
+ args,
43
+ runtime: 'go',
44
+ version
45
+ };
46
+ }
@@ -0,0 +1,155 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { createNodeAdapter } from './node.js';
4
+ import { createPythonAdapter } from './python.js';
5
+ import { createRubyAdapter } from './ruby.js';
6
+ import { createPhpAdapter } from './php.js';
7
+ import { createBunAdapter } from './bun.js';
8
+ import { createDenoAdapter } from './deno.js';
9
+ import { createShellAdapter } from './shell.js';
10
+ import { createNativeAdapter } from './native.js';
11
+ import { createGoAdapter } from './go.js';
12
+ import { createElixirAdapter } from './elixir.js';
13
+
14
+ export function detectRuntime(filepath) {
15
+ if (!filepath) {
16
+ throw new Error('Script path or command is required for runtime detection.');
17
+ }
18
+
19
+ const baseName = path.basename(filepath);
20
+ const ext = path.extname(baseName).toLowerCase();
21
+
22
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
23
+ return 'node';
24
+ }
25
+ if (ext === '.py') {
26
+ return 'python';
27
+ }
28
+ if (ext === '.rb') {
29
+ return 'ruby';
30
+ }
31
+ if (ext === '.php') {
32
+ return 'php';
33
+ }
34
+ if (ext === '.sh') {
35
+ return 'shell';
36
+ }
37
+ if (ext === '.go') {
38
+ return 'go';
39
+ }
40
+ if (ext === '.ex' || ext === '.exs') {
41
+ return 'elixir';
42
+ }
43
+
44
+ // Inspect shebang header if file exists
45
+ const absPath = path.resolve(process.cwd(), filepath);
46
+ if (fs.existsSync(absPath)) {
47
+ try {
48
+ const fd = fs.openSync(absPath, 'r');
49
+ const buffer = Buffer.alloc(256);
50
+ const bytesRead = fs.readSync(fd, buffer, 0, 256, 0);
51
+ fs.closeSync(fd);
52
+
53
+ const header = buffer.toString('utf8', 0, bytesRead);
54
+ const firstLine = header.split('\n')[0].trim();
55
+
56
+ if (firstLine.startsWith('#!')) {
57
+ if (firstLine.includes('python')) return 'python';
58
+ if (firstLine.includes('node')) return 'node';
59
+ if (firstLine.includes('ruby')) return 'ruby';
60
+ if (firstLine.includes('bash') || firstLine.includes('/sh')) return 'shell';
61
+ if (firstLine.includes('bun')) return 'bun';
62
+ if (firstLine.includes('deno')) return 'deno';
63
+ if (firstLine.includes('php')) return 'php';
64
+ if (firstLine.includes('go')) return 'go';
65
+ if (firstLine.includes('elixir')) return 'elixir';
66
+ }
67
+ } catch {
68
+ // ignore
69
+ }
70
+ }
71
+
72
+ if (ext === '.ts') {
73
+ throw new Error(
74
+ `Could not determine runtime for ${baseName}.\n\nSpecify one:\n unitup add ${baseName} --runtime bun\n unitup add ${baseName} --runtime deno\n unitup add ${baseName} --runtime node`
75
+ );
76
+ }
77
+
78
+ if (ext === '') {
79
+ return 'node';
80
+ }
81
+
82
+ throw new Error(
83
+ `Could not determine runtime for ${baseName}.\n\nSpecify runtime with --runtime <name> or custom executable with --command <path>`
84
+ );
85
+ }
86
+
87
+ export async function resolveRuntimeConfig(opts = {}) {
88
+ // 1. If explicit --command is provided, bypass runtime auto-detection
89
+ if (opts.command) {
90
+ const absCommand = path.resolve(process.cwd(), opts.command);
91
+ if (!fs.existsSync(absCommand)) {
92
+ // If binary doesn't exist as exact file path, check if it's in PATH or native executable
93
+ const { findRuntimeExecutable } = await import('./common.js');
94
+ const resolved = await findRuntimeExecutable([opts.command]);
95
+ if (!resolved) {
96
+ throw new Error(`Command executable could not be resolved: ${opts.command}`);
97
+ }
98
+ const args = Array.isArray(opts.args) ? opts.args : [];
99
+ return {
100
+ command: resolved,
101
+ args,
102
+ runtime: opts.runtime || 'custom',
103
+ version: 'custom'
104
+ };
105
+ }
106
+
107
+ const args = Array.isArray(opts.args) ? opts.args : [];
108
+ return {
109
+ command: absCommand,
110
+ args,
111
+ runtime: opts.runtime || 'custom',
112
+ version: 'custom'
113
+ };
114
+ }
115
+
116
+ // 2. Resolve target runtime name
117
+ let runtimeName = opts.runtime;
118
+ if (!runtimeName) {
119
+ runtimeName = detectRuntime(opts.script);
120
+ }
121
+
122
+ const cleanRuntime = runtimeName.toLowerCase();
123
+
124
+ switch (cleanRuntime) {
125
+ case 'node':
126
+ case 'nodejs':
127
+ return createNodeAdapter(opts);
128
+ case 'python':
129
+ case 'python3':
130
+ return createPythonAdapter(opts);
131
+ case 'ruby':
132
+ return createRubyAdapter(opts);
133
+ case 'php':
134
+ return createPhpAdapter(opts);
135
+ case 'bun':
136
+ return createBunAdapter(opts);
137
+ case 'deno':
138
+ return createDenoAdapter(opts);
139
+ case 'shell':
140
+ case 'bash':
141
+ case 'sh':
142
+ return createShellAdapter(opts);
143
+ case 'go':
144
+ case 'golang':
145
+ return createGoAdapter(opts);
146
+ case 'elixir':
147
+ case 'ex':
148
+ case 'exs':
149
+ return createElixirAdapter(opts);
150
+ case 'native':
151
+ return createNativeAdapter(opts);
152
+ default:
153
+ throw new Error(`Unsupported runtime: "${runtimeName}"`);
154
+ }
155
+ }
@@ -0,0 +1,38 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ export async function createNativeAdapter(opts = {}) {
5
+ const targetPath = opts.script || opts.command;
6
+
7
+ if (!targetPath) {
8
+ throw new Error('Native executable path is required.');
9
+ }
10
+
11
+ const absPath = path.resolve(process.cwd(), targetPath);
12
+
13
+ if (!fs.existsSync(absPath)) {
14
+ throw new Error(`Executable file does not exist: ${absPath}`);
15
+ }
16
+
17
+ const stat = fs.statSync(absPath);
18
+ if (!stat.isFile()) {
19
+ throw new Error(`Executable path is not a file: ${absPath}`);
20
+ }
21
+
22
+ try {
23
+ fs.accessSync(absPath, fs.constants.X_OK);
24
+ } catch {
25
+ throw new Error(
26
+ `The executable is not runnable.\n\nRun:\n chmod +x ${absPath}`
27
+ );
28
+ }
29
+
30
+ const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
31
+
32
+ return {
33
+ command: absPath,
34
+ args: scriptArgs,
35
+ runtime: 'native',
36
+ version: 'native'
37
+ };
38
+ }
@@ -0,0 +1,57 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { findRuntimeExecutable, getBinaryVersion } from './common.js';
4
+
5
+ export async function createNodeAdapter(opts = {}) {
6
+ const customPath = opts.command || opts.nodePath;
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
+ execPath = null;
17
+ }
18
+ }
19
+ }
20
+
21
+ if (!execPath) {
22
+ if (process.execPath && fs.existsSync(process.execPath)) {
23
+ try {
24
+ fs.accessSync(process.execPath, fs.constants.X_OK);
25
+ execPath = process.execPath;
26
+ } catch {}
27
+ }
28
+ }
29
+
30
+ if (!execPath) {
31
+ execPath = await findRuntimeExecutable(['node']);
32
+ }
33
+
34
+ if (!execPath) {
35
+ throw new Error(
36
+ 'Node.js runtime could not be found.\n\nInstall Node.js or specify its path:\n unitup add script.js --command /usr/bin/node'
37
+ );
38
+ }
39
+
40
+ const version = await getBinaryVersion(execPath, '-v');
41
+ const scriptPath = opts.script ? path.resolve(process.cwd(), opts.script) : null;
42
+ const runtimeArgs = Array.isArray(opts.runtimeArgs) ? opts.runtimeArgs : [];
43
+ const scriptArgs = Array.isArray(opts.args) ? opts.args : [];
44
+
45
+ const args = [...runtimeArgs];
46
+ if (scriptPath) {
47
+ args.push(scriptPath);
48
+ }
49
+ args.push(...scriptArgs);
50
+
51
+ return {
52
+ command: execPath,
53
+ args,
54
+ runtime: 'node',
55
+ version
56
+ };
57
+ }
@@ -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
+ }