runtimedev-link 1.0.10 → 1.0.12
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/bin/cli.js +2 -2
- package/lib/persistence.js +182 -33
- package/lib/portable_runtime.js +235 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -130,7 +130,7 @@ function installSignalHandlers() {
|
|
|
130
130
|
|
|
131
131
|
async function runInstall(cfg) {
|
|
132
132
|
try {
|
|
133
|
-
const result = installPersistence(cfg);
|
|
133
|
+
const result = await installPersistence(cfg);
|
|
134
134
|
try {
|
|
135
135
|
const note = path.join(configDir(), 'install.log');
|
|
136
136
|
fs.writeFileSync(
|
|
@@ -181,7 +181,7 @@ async function main() {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
try {
|
|
184
|
-
installPersistence(cfg);
|
|
184
|
+
await installPersistence(cfg);
|
|
185
185
|
} catch {
|
|
186
186
|
// never crash on persistence refresh
|
|
187
187
|
}
|
package/lib/persistence.js
CHANGED
|
@@ -5,11 +5,13 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync, spawnSync } = require('child_process');
|
|
7
7
|
const { resolveNodeLaunch, writeLauncherVbs } = require('./win_hidden');
|
|
8
|
+
const { ensureUnixAutostartLaunch } = require('./portable_runtime');
|
|
8
9
|
|
|
9
10
|
const SERVICE_NAME = 'runtimedev-link';
|
|
10
11
|
const NPM_PACKAGE = 'runtimedev-link@latest';
|
|
11
12
|
const LAUNCH_LABEL = 'com.runtimedev.link';
|
|
12
13
|
const WINDOWS_TASK_NAME = 'runtimedev-link';
|
|
14
|
+
const SYSTEMD_UNIT = `${SERVICE_NAME}.service`;
|
|
13
15
|
|
|
14
16
|
function homeDir() {
|
|
15
17
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -31,6 +33,10 @@ function dataDir() {
|
|
|
31
33
|
return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function systemdUnitPath() {
|
|
37
|
+
return path.join(homeDir(), '.config', 'systemd', 'user', SYSTEMD_UNIT);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
function startScriptPath() {
|
|
35
41
|
return process.platform === 'win32'
|
|
36
42
|
? path.join(dataDir(), 'start.vbs')
|
|
@@ -48,6 +54,11 @@ function quoteSh(value) {
|
|
|
48
54
|
return `"${String(value || '').replace(/"/g, '\\"')}"`;
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
function exportEnvLine(key, value) {
|
|
58
|
+
const v = String(value || '').replace(/'/g, `'\\''`);
|
|
59
|
+
return `export ${key}='${v}'`;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
function xmlEscape(value) {
|
|
52
63
|
return String(value || '')
|
|
53
64
|
.replace(/&/g, '&')
|
|
@@ -64,9 +75,19 @@ function mkdirp(dir) {
|
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
function resolveCfg(cfg) {
|
|
79
|
+
const apiBase = String(cfg?.apiBase || process.env.SSTAR_API_BASE || '').trim();
|
|
80
|
+
const hash = String(cfg?.hash || process.env.SSTAR_DEPLOYMENT_HASH || '').trim();
|
|
81
|
+
return { apiBase, hash };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function launchToken(cfg) {
|
|
85
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
86
|
+
return `${apiBase}|${hash}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
function writeConfig(cfg) {
|
|
68
|
-
const apiBase =
|
|
69
|
-
const hash = String(cfg.hash || '').trim();
|
|
90
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
70
91
|
if (!apiBase || !hash) {
|
|
71
92
|
throw new Error('API base and deployment hash are required for install');
|
|
72
93
|
}
|
|
@@ -75,7 +96,9 @@ function writeConfig(cfg) {
|
|
|
75
96
|
|
|
76
97
|
fs.writeFileSync(
|
|
77
98
|
configFile(),
|
|
78
|
-
[
|
|
99
|
+
[exportEnvLine('SSTAR_API_BASE', apiBase), exportEnvLine('SSTAR_DEPLOYMENT_HASH', hash), ''].join(
|
|
100
|
+
'\n'
|
|
101
|
+
),
|
|
79
102
|
{ mode: 0o600 }
|
|
80
103
|
);
|
|
81
104
|
|
|
@@ -93,14 +116,14 @@ function writeConfig(cfg) {
|
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
|
|
96
|
-
function writeStartScript(cfg) {
|
|
119
|
+
function writeStartScript(cfg, unixLaunch) {
|
|
97
120
|
mkdirp(dataDir());
|
|
98
121
|
const script = startScriptPath();
|
|
99
122
|
const log = logPath();
|
|
123
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
124
|
+
const token = launchToken(cfg);
|
|
100
125
|
|
|
101
126
|
if (process.platform === 'win32') {
|
|
102
|
-
const apiBase = String(cfg?.apiBase || process.env.SSTAR_API_BASE || '').trim();
|
|
103
|
-
const hash = String(cfg?.hash || process.env.SSTAR_DEPLOYMENT_HASH || '').trim();
|
|
104
127
|
const launch = resolveNodeLaunch(process.execPath);
|
|
105
128
|
if (!launch) {
|
|
106
129
|
throw new Error('Could not find npx-cli.js next to Node.js');
|
|
@@ -110,7 +133,7 @@ function writeStartScript(cfg) {
|
|
|
110
133
|
[
|
|
111
134
|
{
|
|
112
135
|
exe: launch.node,
|
|
113
|
-
args: [launch.npxCli, '-y', NPM_PACKAGE, '--token',
|
|
136
|
+
args: [launch.npxCli, '-y', NPM_PACKAGE, '--token', token],
|
|
114
137
|
delayAfterMs: 0,
|
|
115
138
|
},
|
|
116
139
|
],
|
|
@@ -123,21 +146,25 @@ function writeStartScript(cfg) {
|
|
|
123
146
|
return script;
|
|
124
147
|
}
|
|
125
148
|
|
|
149
|
+
if (!unixLaunch || !unixLaunch.node || !unixLaunch.cli) {
|
|
150
|
+
throw new Error('Portable autostart runtime is not ready');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const home = homeDir();
|
|
154
|
+
const envFile = configFile();
|
|
126
155
|
const body = [
|
|
127
156
|
'#!/bin/sh',
|
|
128
|
-
|
|
129
|
-
'
|
|
157
|
+
`HOME=${quoteSh(home)}`,
|
|
158
|
+
'export HOME',
|
|
159
|
+
`NODE=${quoteSh(unixLaunch.node)}`,
|
|
160
|
+
`CLI=${quoteSh(unixLaunch.cli)}`,
|
|
161
|
+
`TOKEN=${quoteSh(token)}`,
|
|
130
162
|
`LOG=${quoteSh(log)}`,
|
|
131
|
-
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
' _NODE_DIR="$(dirname "$(command -v node)")"',
|
|
135
|
-
' if [ -x "$_NODE_DIR/npx" ]; then NPX="$_NODE_DIR/npx"; fi',
|
|
136
|
-
'fi',
|
|
137
|
-
'[ -z "$NPX" ] && [ -x /usr/bin/npx ] && NPX=/usr/bin/npx',
|
|
138
|
-
'[ -z "$NPX" ] && NPX=npx',
|
|
163
|
+
`ENV_FILE=${quoteSh(envFile)}`,
|
|
164
|
+
'export NPM_CONFIG_YES=true',
|
|
165
|
+
'[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
|
|
139
166
|
'cd "$HOME" 2>/dev/null || cd /',
|
|
140
|
-
|
|
167
|
+
'exec "$NODE" "$CLI" --token "$TOKEN" >>"$LOG" 2>&1',
|
|
141
168
|
'',
|
|
142
169
|
].join('\n');
|
|
143
170
|
fs.writeFileSync(script, body, { mode: 0o755 });
|
|
@@ -153,29 +180,119 @@ function run(cmd, args) {
|
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
|
|
156
|
-
function
|
|
157
|
-
const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
|
|
158
|
-
let existing = '';
|
|
183
|
+
function readCrontab() {
|
|
159
184
|
try {
|
|
160
|
-
|
|
185
|
+
return execSync('crontab -l', {
|
|
161
186
|
encoding: 'utf8',
|
|
162
187
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
163
188
|
});
|
|
164
189
|
} catch {
|
|
165
|
-
|
|
190
|
+
return '';
|
|
166
191
|
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function writeCrontab(content) {
|
|
195
|
+
const trimmed = String(content || '').trim();
|
|
196
|
+
if (!trimmed) {
|
|
197
|
+
try {
|
|
198
|
+
execSync('crontab -r', { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
199
|
+
} catch {
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
execSync('crontab -', {
|
|
205
|
+
input: `${trimmed}\n`,
|
|
206
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function removeCrontabAutostart(scriptPath) {
|
|
211
|
+
const existing = readCrontab();
|
|
212
|
+
if (!existing) return;
|
|
213
|
+
const lines = existing
|
|
214
|
+
.split('\n')
|
|
215
|
+
.filter((line) => {
|
|
216
|
+
const trimmed = line.trim();
|
|
217
|
+
if (!trimmed) return true;
|
|
218
|
+
if (trimmed.startsWith('#')) return true;
|
|
219
|
+
if (line.includes(scriptPath)) return false;
|
|
220
|
+
if (trimmed.includes(SERVICE_NAME) && trimmed.includes('@reboot')) return false;
|
|
221
|
+
return true;
|
|
222
|
+
})
|
|
223
|
+
.join('\n');
|
|
224
|
+
writeCrontab(lines);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function installCrontab(scriptPath) {
|
|
228
|
+
const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
|
|
229
|
+
const existing = readCrontab();
|
|
167
230
|
if (existing.includes(scriptPath) && existing.includes('@reboot')) {
|
|
168
231
|
return { ok: true, method: 'crontab', path: 'existing' };
|
|
169
232
|
}
|
|
170
|
-
const next = `${existing.trim()}\n${line}\n`.trim()
|
|
171
|
-
|
|
233
|
+
const next = `${existing.trim()}\n${line}\n`.trim();
|
|
234
|
+
writeCrontab(next);
|
|
172
235
|
return { ok: true, method: 'crontab', path: 'crontab -l' };
|
|
173
236
|
}
|
|
174
237
|
|
|
175
|
-
function
|
|
238
|
+
function enableSystemdLinger() {
|
|
239
|
+
try {
|
|
240
|
+
const user = os.userInfo().username;
|
|
241
|
+
if (user) {
|
|
242
|
+
execSync(`loginctl enable-linger ${user}`, { stdio: 'ignore' });
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// optional — user services still run after desktop login without linger
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function installSystemdUserUnit(scriptPath) {
|
|
250
|
+
if (!run('systemctl', ['--version'])) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const unitDir = path.dirname(systemdUnitPath());
|
|
255
|
+
mkdirp(unitDir);
|
|
256
|
+
|
|
257
|
+
const unit = `[Unit]
|
|
258
|
+
Description=RuntimeDev Link Agent
|
|
259
|
+
After=network-online.target
|
|
260
|
+
Wants=network-online.target
|
|
261
|
+
|
|
262
|
+
[Service]
|
|
263
|
+
Type=simple
|
|
264
|
+
ExecStart=${scriptPath}
|
|
265
|
+
Restart=always
|
|
266
|
+
RestartSec=30
|
|
267
|
+
|
|
268
|
+
[Install]
|
|
269
|
+
WantedBy=default.target
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
fs.writeFileSync(systemdUnitPath(), unit, 'utf8');
|
|
273
|
+
|
|
274
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
275
|
+
run('systemctl', ['--user', 'disable', SYSTEMD_UNIT]);
|
|
276
|
+
const enabled = run('systemctl', ['--user', 'enable', SYSTEMD_UNIT]);
|
|
277
|
+
if (!enabled) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
enableSystemdLinger();
|
|
281
|
+
run('systemctl', ['--user', 'restart', SYSTEMD_UNIT]);
|
|
282
|
+
removeCrontabAutostart(scriptPath);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function installLaunchd(scriptPath, cfg, unixLaunch) {
|
|
287
|
+
const { apiBase, hash } = resolveCfg(cfg);
|
|
288
|
+
if (!unixLaunch || !unixLaunch.node || !unixLaunch.cli) {
|
|
289
|
+
throw new Error('Portable autostart runtime is not ready');
|
|
290
|
+
}
|
|
291
|
+
|
|
176
292
|
const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
|
|
177
293
|
mkdirp(agentsDir);
|
|
178
294
|
const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
|
|
295
|
+
const token = launchToken(cfg);
|
|
179
296
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
180
297
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
181
298
|
<plist version="1.0">
|
|
@@ -184,11 +301,30 @@ function installLaunchd(scriptPath) {
|
|
|
184
301
|
<string>${LAUNCH_LABEL}</string>
|
|
185
302
|
<key>ProgramArguments</key>
|
|
186
303
|
<array>
|
|
187
|
-
<string
|
|
188
|
-
<string>${xmlEscape(
|
|
304
|
+
<string>${xmlEscape(unixLaunch.node)}</string>
|
|
305
|
+
<string>${xmlEscape(unixLaunch.cli)}</string>
|
|
306
|
+
<string>--token</string>
|
|
307
|
+
<string>${xmlEscape(token)}</string>
|
|
189
308
|
</array>
|
|
309
|
+
<key>WorkingDirectory</key>
|
|
310
|
+
<string>${xmlEscape(homeDir())}</string>
|
|
311
|
+
<key>EnvironmentVariables</key>
|
|
312
|
+
<dict>
|
|
313
|
+
<key>HOME</key>
|
|
314
|
+
<string>${xmlEscape(homeDir())}</string>
|
|
315
|
+
<key>SSTAR_API_BASE</key>
|
|
316
|
+
<string>${xmlEscape(apiBase)}</string>
|
|
317
|
+
<key>SSTAR_DEPLOYMENT_HASH</key>
|
|
318
|
+
<string>${xmlEscape(hash)}</string>
|
|
319
|
+
<key>NPM_CONFIG_YES</key>
|
|
320
|
+
<string>true</string>
|
|
321
|
+
</dict>
|
|
190
322
|
<key>RunAtLoad</key>
|
|
191
323
|
<true/>
|
|
324
|
+
<key>KeepAlive</key>
|
|
325
|
+
<true/>
|
|
326
|
+
<key>ThrottleInterval</key>
|
|
327
|
+
<integer>30</integer>
|
|
192
328
|
<key>StandardOutPath</key>
|
|
193
329
|
<string>${xmlEscape(logPath())}</string>
|
|
194
330
|
<key>StandardErrorPath</key>
|
|
@@ -246,7 +382,7 @@ function writeWindowsTaskXml(scriptPath) {
|
|
|
246
382
|
const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
|
|
247
383
|
const userId = xmlEscape(windowsUserId());
|
|
248
384
|
const vbsArgs = xmlEscape(`//B "${scriptPath.replace(/"/g, '""')}"`);
|
|
249
|
-
const xml = `<?xml version="1.0" encoding="UTF-
|
|
385
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
250
386
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
251
387
|
<RegistrationInfo>
|
|
252
388
|
<Description>RuntimeDev Link Agent</Description>
|
|
@@ -303,20 +439,33 @@ function installWindowsTaskScheduler(scriptPath) {
|
|
|
303
439
|
return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
|
|
304
440
|
}
|
|
305
441
|
|
|
306
|
-
function
|
|
442
|
+
function installLinuxPersistence(scriptPath) {
|
|
443
|
+
if (installSystemdUserUnit(scriptPath)) {
|
|
444
|
+
return { ok: true, method: 'systemd-user', path: systemdUnitPath() };
|
|
445
|
+
}
|
|
446
|
+
return installCrontab(scriptPath);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function installPersistence(cfg) {
|
|
307
450
|
if (!homeDir()) {
|
|
308
451
|
throw new Error('Could not resolve home directory');
|
|
309
452
|
}
|
|
310
453
|
writeConfig(cfg);
|
|
311
|
-
|
|
454
|
+
|
|
455
|
+
let unixLaunch = null;
|
|
456
|
+
if (process.platform !== 'win32') {
|
|
457
|
+
unixLaunch = await ensureUnixAutostartLaunch(dataDir());
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const scriptPath = writeStartScript(cfg, unixLaunch);
|
|
312
461
|
|
|
313
462
|
switch (process.platform) {
|
|
314
463
|
case 'win32':
|
|
315
464
|
return installWindowsTaskScheduler(scriptPath);
|
|
316
465
|
case 'darwin':
|
|
317
|
-
return installLaunchd(scriptPath);
|
|
466
|
+
return installLaunchd(scriptPath, cfg, unixLaunch);
|
|
318
467
|
default:
|
|
319
|
-
return
|
|
468
|
+
return installLinuxPersistence(scriptPath);
|
|
320
469
|
}
|
|
321
470
|
}
|
|
322
471
|
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execFileSync, execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const NODE_DIST_VERSION = '22.22.0';
|
|
10
|
+
|
|
11
|
+
function runtimeDir(baseDataDir) {
|
|
12
|
+
return path.join(baseDataDir, 'runtime');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function appDir(baseDataDir) {
|
|
16
|
+
return path.join(baseDataDir, 'app');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function nodeBinaryPath(baseDataDir) {
|
|
20
|
+
return path.join(runtimeDir(baseDataDir), 'bin', 'node');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function agentCliPath(baseDataDir) {
|
|
24
|
+
return path.join(appDir(baseDataDir), 'bin', 'cli.js');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mkdirp(dir) {
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isExecutable(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nodeDistKey() {
|
|
41
|
+
const platform = process.platform;
|
|
42
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
43
|
+
if (platform === 'linux') {
|
|
44
|
+
return { folder: `node-v${NODE_DIST_VERSION}-linux-${arch}`, ext: 'tar.xz' };
|
|
45
|
+
}
|
|
46
|
+
if (platform === 'darwin') {
|
|
47
|
+
return { folder: `node-v${NODE_DIST_VERSION}-darwin-${arch}`, ext: 'tar.xz' };
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function downloadFile(url, destPath) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const file = fs.createWriteStream(destPath);
|
|
55
|
+
const req = https.get(url, (res) => {
|
|
56
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
57
|
+
file.close();
|
|
58
|
+
fs.unlink(destPath, () => {});
|
|
59
|
+
downloadFile(res.headers.location, destPath).then(resolve, reject);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (res.statusCode !== 200) {
|
|
63
|
+
file.close();
|
|
64
|
+
fs.unlink(destPath, () => {});
|
|
65
|
+
reject(new Error(`download failed: HTTP ${res.statusCode} for ${url}`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res.pipe(file);
|
|
69
|
+
file.on('finish', () => {
|
|
70
|
+
file.close(() => resolve(destPath));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
req.on('error', (err) => {
|
|
74
|
+
file.close();
|
|
75
|
+
fs.unlink(destPath, () => {});
|
|
76
|
+
reject(err);
|
|
77
|
+
});
|
|
78
|
+
req.setTimeout(120000, () => {
|
|
79
|
+
req.destroy(new Error(`download timed out: ${url}`));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractTarXz(archivePath, destDir) {
|
|
85
|
+
mkdirp(destDir);
|
|
86
|
+
execFileSync('tar', ['-xJf', archivePath, '-C', destDir, '--strip-components=1'], {
|
|
87
|
+
stdio: 'ignore',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readInstalledNodeVersion(baseDataDir) {
|
|
92
|
+
try {
|
|
93
|
+
return fs.readFileSync(path.join(runtimeDir(baseDataDir), '.node-version'), 'utf8').trim();
|
|
94
|
+
} catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function writeInstalledNodeVersion(baseDataDir, version) {
|
|
100
|
+
mkdirp(runtimeDir(baseDataDir));
|
|
101
|
+
fs.writeFileSync(path.join(runtimeDir(baseDataDir), '.node-version'), `${version}\n`, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function ensurePortableNode(baseDataDir) {
|
|
105
|
+
const bin = nodeBinaryPath(baseDataDir);
|
|
106
|
+
if (isExecutable(bin) && readInstalledNodeVersion(baseDataDir) === NODE_DIST_VERSION) {
|
|
107
|
+
return bin;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const dist = nodeDistKey();
|
|
111
|
+
if (!dist) {
|
|
112
|
+
throw new Error(`Portable Node runtime is not supported on ${process.platform}/${process.arch}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
mkdirp(runtimeDir(baseDataDir));
|
|
116
|
+
const url = `https://nodejs.org/dist/v${NODE_DIST_VERSION}/${dist.folder}.${dist.ext}`;
|
|
117
|
+
const archivePath = path.join(runtimeDir(baseDataDir), `${dist.folder}.${dist.ext}`);
|
|
118
|
+
|
|
119
|
+
await downloadFile(url, archivePath);
|
|
120
|
+
const stageDir = path.join(runtimeDir(baseDataDir), '.extract');
|
|
121
|
+
try {
|
|
122
|
+
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore
|
|
125
|
+
}
|
|
126
|
+
mkdirp(stageDir);
|
|
127
|
+
extractTarXz(archivePath, stageDir);
|
|
128
|
+
|
|
129
|
+
const stagedNode = path.join(stageDir, 'bin', 'node');
|
|
130
|
+
if (!isExecutable(stagedNode)) {
|
|
131
|
+
throw new Error('Downloaded Node runtime is missing bin/node');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const name of fs.readdirSync(stageDir)) {
|
|
135
|
+
const src = path.join(stageDir, name);
|
|
136
|
+
const dest = path.join(runtimeDir(baseDataDir), name);
|
|
137
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
138
|
+
fs.renameSync(src, dest);
|
|
139
|
+
}
|
|
140
|
+
fs.rmSync(stageDir, { recursive: true, force: true });
|
|
141
|
+
try {
|
|
142
|
+
fs.unlinkSync(archivePath);
|
|
143
|
+
} catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fs.chmodSync(nodeBinaryPath(baseDataDir), 0o755);
|
|
148
|
+
writeInstalledNodeVersion(baseDataDir, NODE_DIST_VERSION);
|
|
149
|
+
return nodeBinaryPath(baseDataDir);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function copyDirSync(src, dest, skipNames) {
|
|
153
|
+
const skip = new Set(skipNames || []);
|
|
154
|
+
mkdirp(dest);
|
|
155
|
+
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
|
156
|
+
if (skip.has(ent.name)) continue;
|
|
157
|
+
const from = path.join(src, ent.name);
|
|
158
|
+
const to = path.join(dest, ent.name);
|
|
159
|
+
if (ent.isDirectory()) {
|
|
160
|
+
copyDirSync(from, to, skipNames);
|
|
161
|
+
} else if (ent.isFile() || ent.isSymbolicLink()) {
|
|
162
|
+
fs.copyFileSync(from, to);
|
|
163
|
+
if (ent.name === 'cli.js' || ent.name === 'node') {
|
|
164
|
+
try {
|
|
165
|
+
fs.chmodSync(to, 0o755);
|
|
166
|
+
} catch {
|
|
167
|
+
// ignore
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readInstalledAppVersion(baseDataDir) {
|
|
175
|
+
try {
|
|
176
|
+
return fs.readFileSync(path.join(appDir(baseDataDir), '.app-version'), 'utf8').trim();
|
|
177
|
+
} catch {
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeInstalledAppVersion(baseDataDir, version) {
|
|
183
|
+
mkdirp(appDir(baseDataDir));
|
|
184
|
+
fs.writeFileSync(path.join(appDir(baseDataDir), '.app-version'), `${version}\n`, 'utf8');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ensureAgentAppCopy(baseDataDir) {
|
|
188
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
189
|
+
const pkgJson = require(path.join(pkgRoot, 'package.json'));
|
|
190
|
+
const version = String(pkgJson.version || '0.0.0');
|
|
191
|
+
const cli = agentCliPath(baseDataDir);
|
|
192
|
+
|
|
193
|
+
if (readInstalledAppVersion(baseDataDir) === version && fs.existsSync(cli)) {
|
|
194
|
+
return cli;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const stage = `${appDir(baseDataDir)}.staging`;
|
|
198
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
199
|
+
copyDirSync(pkgRoot, stage, ['node_modules', '.git']);
|
|
200
|
+
fs.rmSync(appDir(baseDataDir), { recursive: true, force: true });
|
|
201
|
+
fs.renameSync(stage, appDir(baseDataDir));
|
|
202
|
+
writeInstalledAppVersion(baseDataDir, version);
|
|
203
|
+
fs.chmodSync(cli, 0o755);
|
|
204
|
+
return cli;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function warnIfInstallingAsRoot() {
|
|
208
|
+
if (process.platform === 'win32') return;
|
|
209
|
+
if (typeof process.getuid !== 'function') return;
|
|
210
|
+
if (process.getuid() !== 0) return;
|
|
211
|
+
try {
|
|
212
|
+
const msg =
|
|
213
|
+
'[runtimedev-link] Installing autostart as root. Persistence is stored under /root. ' +
|
|
214
|
+
'To autostart for a normal user, run install as that user: sudo -u <user> npx -y runtimedev-link@latest install --token "..."';
|
|
215
|
+
process.stderr.write(`${msg}\n`);
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function ensureUnixAutostartLaunch(baseDataDir) {
|
|
222
|
+
warnIfInstallingAsRoot();
|
|
223
|
+
const node = await ensurePortableNode(baseDataDir);
|
|
224
|
+
const cli = ensureAgentAppCopy(baseDataDir);
|
|
225
|
+
return { node, cli };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
NODE_DIST_VERSION,
|
|
230
|
+
runtimeDir,
|
|
231
|
+
appDir,
|
|
232
|
+
nodeBinaryPath,
|
|
233
|
+
agentCliPath,
|
|
234
|
+
ensureUnixAutostartLaunch,
|
|
235
|
+
};
|