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 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
  }
@@ -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
- const launch = resolveUnixLaunch();
167
- if (!launch) {
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(launch.node)}`,
178
- `NPX_CLI=${quoteSh(launch.npxCli)}`,
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" "$NPX_CLI" -y ' + NPM_PACKAGE + ' --token "$TOKEN" >>"$LOG" 2>&1',
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
- const launch = resolveUnixLaunch();
307
- if (!launch) {
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(launch.node)}</string>
324
- <string>${xmlEscape(launch.npxCli)}</string>
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
- const scriptPath = writeStartScript(cfg);
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtimedev-link",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {