runtimedev-link 1.0.11 → 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 +21 -36
- 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,6 +5,7 @@ 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';
|
|
@@ -85,24 +86,6 @@ function launchToken(cfg) {
|
|
|
85
86
|
return `${apiBase}|${hash}`;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
function resolveUnixLaunch() {
|
|
89
|
-
const launch = resolveNodeLaunch(process.execPath);
|
|
90
|
-
if (launch) return launch;
|
|
91
|
-
try {
|
|
92
|
-
const npx = execSync('command -v npx', {
|
|
93
|
-
encoding: 'utf8',
|
|
94
|
-
shell: '/bin/sh',
|
|
95
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
96
|
-
}).trim();
|
|
97
|
-
if (npx) {
|
|
98
|
-
return { node: process.execPath, npxCli: npx };
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// ignore
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
89
|
function writeConfig(cfg) {
|
|
107
90
|
const { apiBase, hash } = resolveCfg(cfg);
|
|
108
91
|
if (!apiBase || !hash) {
|
|
@@ -133,7 +116,7 @@ function writeConfig(cfg) {
|
|
|
133
116
|
}
|
|
134
117
|
}
|
|
135
118
|
|
|
136
|
-
function writeStartScript(cfg) {
|
|
119
|
+
function writeStartScript(cfg, unixLaunch) {
|
|
137
120
|
mkdirp(dataDir());
|
|
138
121
|
const script = startScriptPath();
|
|
139
122
|
const log = logPath();
|
|
@@ -163,9 +146,8 @@ function writeStartScript(cfg) {
|
|
|
163
146
|
return script;
|
|
164
147
|
}
|
|
165
148
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
throw new Error('Could not resolve node/npx for autostart');
|
|
149
|
+
if (!unixLaunch || !unixLaunch.node || !unixLaunch.cli) {
|
|
150
|
+
throw new Error('Portable autostart runtime is not ready');
|
|
169
151
|
}
|
|
170
152
|
|
|
171
153
|
const home = homeDir();
|
|
@@ -174,15 +156,15 @@ function writeStartScript(cfg) {
|
|
|
174
156
|
'#!/bin/sh',
|
|
175
157
|
`HOME=${quoteSh(home)}`,
|
|
176
158
|
'export HOME',
|
|
177
|
-
`NODE=${quoteSh(
|
|
178
|
-
`
|
|
159
|
+
`NODE=${quoteSh(unixLaunch.node)}`,
|
|
160
|
+
`CLI=${quoteSh(unixLaunch.cli)}`,
|
|
179
161
|
`TOKEN=${quoteSh(token)}`,
|
|
180
162
|
`LOG=${quoteSh(log)}`,
|
|
181
163
|
`ENV_FILE=${quoteSh(envFile)}`,
|
|
182
164
|
'export NPM_CONFIG_YES=true',
|
|
183
165
|
'[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
|
|
184
166
|
'cd "$HOME" 2>/dev/null || cd /',
|
|
185
|
-
'exec "$NODE" "$
|
|
167
|
+
'exec "$NODE" "$CLI" --token "$TOKEN" >>"$LOG" 2>&1',
|
|
186
168
|
'',
|
|
187
169
|
].join('\n');
|
|
188
170
|
fs.writeFileSync(script, body, { mode: 0o755 });
|
|
@@ -301,11 +283,10 @@ WantedBy=default.target
|
|
|
301
283
|
return true;
|
|
302
284
|
}
|
|
303
285
|
|
|
304
|
-
function installLaunchd(scriptPath, cfg) {
|
|
286
|
+
function installLaunchd(scriptPath, cfg, unixLaunch) {
|
|
305
287
|
const { apiBase, hash } = resolveCfg(cfg);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
throw new Error('Could not resolve node/npx for autostart');
|
|
288
|
+
if (!unixLaunch || !unixLaunch.node || !unixLaunch.cli) {
|
|
289
|
+
throw new Error('Portable autostart runtime is not ready');
|
|
309
290
|
}
|
|
310
291
|
|
|
311
292
|
const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
|
|
@@ -320,10 +301,8 @@ function installLaunchd(scriptPath, cfg) {
|
|
|
320
301
|
<string>${LAUNCH_LABEL}</string>
|
|
321
302
|
<key>ProgramArguments</key>
|
|
322
303
|
<array>
|
|
323
|
-
<string>${xmlEscape(
|
|
324
|
-
<string>${xmlEscape(
|
|
325
|
-
<string>-y</string>
|
|
326
|
-
<string>${NPM_PACKAGE}</string>
|
|
304
|
+
<string>${xmlEscape(unixLaunch.node)}</string>
|
|
305
|
+
<string>${xmlEscape(unixLaunch.cli)}</string>
|
|
327
306
|
<string>--token</string>
|
|
328
307
|
<string>${xmlEscape(token)}</string>
|
|
329
308
|
</array>
|
|
@@ -467,18 +446,24 @@ function installLinuxPersistence(scriptPath) {
|
|
|
467
446
|
return installCrontab(scriptPath);
|
|
468
447
|
}
|
|
469
448
|
|
|
470
|
-
function installPersistence(cfg) {
|
|
449
|
+
async function installPersistence(cfg) {
|
|
471
450
|
if (!homeDir()) {
|
|
472
451
|
throw new Error('Could not resolve home directory');
|
|
473
452
|
}
|
|
474
453
|
writeConfig(cfg);
|
|
475
|
-
|
|
454
|
+
|
|
455
|
+
let unixLaunch = null;
|
|
456
|
+
if (process.platform !== 'win32') {
|
|
457
|
+
unixLaunch = await ensureUnixAutostartLaunch(dataDir());
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const scriptPath = writeStartScript(cfg, unixLaunch);
|
|
476
461
|
|
|
477
462
|
switch (process.platform) {
|
|
478
463
|
case 'win32':
|
|
479
464
|
return installWindowsTaskScheduler(scriptPath);
|
|
480
465
|
case 'darwin':
|
|
481
|
-
return installLaunchd(scriptPath, cfg);
|
|
466
|
+
return installLaunchd(scriptPath, cfg, unixLaunch);
|
|
482
467
|
default:
|
|
483
468
|
return installLinuxPersistence(scriptPath);
|
|
484
469
|
}
|
|
@@ -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
|
+
};
|