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/README.md +398 -171
- package/docs/index.html +531 -0
- package/index.d.ts +204 -134
- package/package.json +4 -1
- package/src/cli.js +393 -90
- package/src/doctor.js +73 -14
- package/src/index.js +32 -1
- package/src/runtimes/bun.js +46 -0
- package/src/runtimes/common.js +73 -0
- package/src/runtimes/deno.js +46 -0
- package/src/runtimes/elixir.js +46 -0
- package/src/runtimes/go.js +46 -0
- package/src/runtimes/index.js +155 -0
- package/src/runtimes/native.js +38 -0
- package/src/runtimes/node.js +57 -0
- package/src/runtimes/php.js +46 -0
- package/src/runtimes/python.js +46 -0
- package/src/runtimes/ruby.js +46 -0
- package/src/runtimes/shell.js +46 -0
- package/src/systemd.js +349 -25
- package/src/unit.js +72 -22
- package/src/utils.js +220 -0
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:
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
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(
|
|
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
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
getServiceStatus,
|
|
8
8
|
getServiceStatusRaw,
|
|
9
9
|
listServices,
|
|
10
|
+
inspectService,
|
|
11
|
+
getServiceFailures,
|
|
12
|
+
getServicesByGroup,
|
|
10
13
|
runJournalctlLogs,
|
|
14
|
+
setServiceLimits,
|
|
15
|
+
executeJournalctlMaintenance,
|
|
11
16
|
isLinux,
|
|
12
17
|
isSystemctlAvailable,
|
|
13
18
|
isSystemdPID1,
|
|
@@ -28,8 +33,15 @@ import {
|
|
|
28
33
|
import {
|
|
29
34
|
sanitizeServiceName,
|
|
30
35
|
getUnitFilename,
|
|
31
|
-
getServiceNameFromUnit
|
|
36
|
+
getServiceNameFromUnit,
|
|
37
|
+
readAppMetadata,
|
|
38
|
+
getAppMetadataPath,
|
|
39
|
+
getAppsDir,
|
|
40
|
+
getUnitupDir,
|
|
41
|
+
validateMemorySize,
|
|
42
|
+
formatMemoryBytes
|
|
32
43
|
} from './utils.js';
|
|
44
|
+
import { detectRuntime, resolveRuntimeConfig } from './runtimes/index.js';
|
|
33
45
|
|
|
34
46
|
export {
|
|
35
47
|
// Main programmatic service management API
|
|
@@ -42,8 +54,27 @@ export {
|
|
|
42
54
|
getServiceStatus,
|
|
43
55
|
getServiceStatusRaw,
|
|
44
56
|
listServices,
|
|
57
|
+
inspectService,
|
|
58
|
+
getServiceFailures,
|
|
59
|
+
getServicesByGroup,
|
|
60
|
+
setServiceLimits,
|
|
61
|
+
executeJournalctlMaintenance as executeJournalctlMaintenance,
|
|
45
62
|
runJournalctlLogs as getServiceLogs,
|
|
46
63
|
|
|
64
|
+
// Memory & Validation helpers
|
|
65
|
+
validateMemorySize,
|
|
66
|
+
formatMemoryBytes,
|
|
67
|
+
|
|
68
|
+
// Runtime detection & resolution
|
|
69
|
+
detectRuntime,
|
|
70
|
+
resolveRuntimeConfig,
|
|
71
|
+
|
|
72
|
+
// Metadata helpers
|
|
73
|
+
readAppMetadata,
|
|
74
|
+
getAppMetadataPath,
|
|
75
|
+
getAppsDir,
|
|
76
|
+
getUnitupDir,
|
|
77
|
+
|
|
47
78
|
// System & Doctor checks
|
|
48
79
|
isSystemctlAvailable as isSystemdAvailable,
|
|
49
80
|
isSystemctlAvailable,
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|