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 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,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 = String(cfg.apiBase || '').trim();
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
- [`SSTAR_API_BASE=${apiBase}`, `SSTAR_DEPLOYMENT_HASH=${hash}`, ''].join('\n'),
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', hash],
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
- 'ENV_FILE="$HOME/.config/runtimedev-link/agent.env"',
129
- '[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
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
- 'export PATH="${PATH:-/usr/local/bin:/usr/bin:/bin}"',
132
- 'NPX="$(command -v npx 2>/dev/null || true)"',
133
- 'if [ -z "$NPX" ] && command -v node >/dev/null 2>&1; then',
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
- `nohup "$NPX" -y ${NPM_PACKAGE} --token "$SSTAR_DEPLOYMENT_HASH" >> "$LOG" 2>&1 &`,
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 installCrontab(scriptPath) {
157
- const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
158
- let existing = '';
183
+ function readCrontab() {
159
184
  try {
160
- existing = execSync('crontab -l', {
185
+ return execSync('crontab -l', {
161
186
  encoding: 'utf8',
162
187
  stdio: ['ignore', 'pipe', 'ignore'],
163
188
  });
164
189
  } catch {
165
- existing = '';
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() + '\n';
171
- execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
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 installLaunchd(scriptPath) {
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>/bin/sh</string>
188
- <string>${xmlEscape(scriptPath)}</string>
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-16"?>
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 installPersistence(cfg) {
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
- 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);
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 installCrontab(scriptPath);
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtimedev-link",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {