runtimedev-link 1.0.10 → 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.
Files changed (2) hide show
  1. package/lib/persistence.js +194 -30
  2. package/package.json +1 -1
@@ -10,6 +10,7 @@ const SERVICE_NAME = 'runtimedev-link';
10
10
  const NPM_PACKAGE = 'runtimedev-link@latest';
11
11
  const LAUNCH_LABEL = 'com.runtimedev.link';
12
12
  const WINDOWS_TASK_NAME = 'runtimedev-link';
13
+ const SYSTEMD_UNIT = `${SERVICE_NAME}.service`;
13
14
 
14
15
  function homeDir() {
15
16
  return process.env.HOME || process.env.USERPROFILE || '';
@@ -31,6 +32,10 @@ function dataDir() {
31
32
  return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
32
33
  }
33
34
 
35
+ function systemdUnitPath() {
36
+ return path.join(homeDir(), '.config', 'systemd', 'user', SYSTEMD_UNIT);
37
+ }
38
+
34
39
  function startScriptPath() {
35
40
  return process.platform === 'win32'
36
41
  ? path.join(dataDir(), 'start.vbs')
@@ -48,6 +53,11 @@ function quoteSh(value) {
48
53
  return `"${String(value || '').replace(/"/g, '\\"')}"`;
49
54
  }
50
55
 
56
+ function exportEnvLine(key, value) {
57
+ const v = String(value || '').replace(/'/g, `'\\''`);
58
+ return `export ${key}='${v}'`;
59
+ }
60
+
51
61
  function xmlEscape(value) {
52
62
  return String(value || '')
53
63
  .replace(/&/g, '&')
@@ -64,9 +74,37 @@ function mkdirp(dir) {
64
74
  }
65
75
  }
66
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
+
67
106
  function writeConfig(cfg) {
68
- const apiBase = String(cfg.apiBase || '').trim();
69
- const hash = String(cfg.hash || '').trim();
107
+ const { apiBase, hash } = resolveCfg(cfg);
70
108
  if (!apiBase || !hash) {
71
109
  throw new Error('API base and deployment hash are required for install');
72
110
  }
@@ -75,7 +113,9 @@ function writeConfig(cfg) {
75
113
 
76
114
  fs.writeFileSync(
77
115
  configFile(),
78
- [`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
+ ),
79
119
  { mode: 0o600 }
80
120
  );
81
121
 
@@ -97,10 +137,10 @@ function writeStartScript(cfg) {
97
137
  mkdirp(dataDir());
98
138
  const script = startScriptPath();
99
139
  const log = logPath();
140
+ const { apiBase, hash } = resolveCfg(cfg);
141
+ const token = launchToken(cfg);
100
142
 
101
143
  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
144
  const launch = resolveNodeLaunch(process.execPath);
105
145
  if (!launch) {
106
146
  throw new Error('Could not find npx-cli.js next to Node.js');
@@ -110,7 +150,7 @@ function writeStartScript(cfg) {
110
150
  [
111
151
  {
112
152
  exe: launch.node,
113
- args: [launch.npxCli, '-y', NPM_PACKAGE, '--token', hash],
153
+ args: [launch.npxCli, '-y', NPM_PACKAGE, '--token', token],
114
154
  delayAfterMs: 0,
115
155
  },
116
156
  ],
@@ -123,21 +163,26 @@ function writeStartScript(cfg) {
123
163
  return script;
124
164
  }
125
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();
126
173
  const body = [
127
174
  '#!/bin/sh',
128
- 'ENV_FILE="$HOME/.config/runtimedev-link/agent.env"',
129
- '[ -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)}`,
130
180
  `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',
181
+ `ENV_FILE=${quoteSh(envFile)}`,
182
+ 'export NPM_CONFIG_YES=true',
183
+ '[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
139
184
  'cd "$HOME" 2>/dev/null || cd /',
140
- `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',
141
186
  '',
142
187
  ].join('\n');
143
188
  fs.writeFileSync(script, body, { mode: 0o755 });
@@ -153,29 +198,120 @@ function run(cmd, args) {
153
198
  }
154
199
  }
155
200
 
156
- function installCrontab(scriptPath) {
157
- const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
158
- let existing = '';
201
+ function readCrontab() {
159
202
  try {
160
- existing = execSync('crontab -l', {
203
+ return execSync('crontab -l', {
161
204
  encoding: 'utf8',
162
205
  stdio: ['ignore', 'pipe', 'ignore'],
163
206
  });
164
207
  } catch {
165
- existing = '';
208
+ return '';
166
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();
167
248
  if (existing.includes(scriptPath) && existing.includes('@reboot')) {
168
249
  return { ok: true, method: 'crontab', path: 'existing' };
169
250
  }
170
- const next = `${existing.trim()}\n${line}\n`.trim() + '\n';
171
- execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
251
+ const next = `${existing.trim()}\n${line}\n`.trim();
252
+ writeCrontab(next);
172
253
  return { ok: true, method: 'crontab', path: 'crontab -l' };
173
254
  }
174
255
 
175
- 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
+
176
311
  const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
177
312
  mkdirp(agentsDir);
178
313
  const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
314
+ const token = launchToken(cfg);
179
315
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
180
316
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
181
317
  <plist version="1.0">
@@ -184,11 +320,32 @@ function installLaunchd(scriptPath) {
184
320
  <string>${LAUNCH_LABEL}</string>
185
321
  <key>ProgramArguments</key>
186
322
  <array>
187
- <string>/bin/sh</string>
188
- <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>
189
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>
190
343
  <key>RunAtLoad</key>
191
344
  <true/>
345
+ <key>KeepAlive</key>
346
+ <true/>
347
+ <key>ThrottleInterval</key>
348
+ <integer>30</integer>
192
349
  <key>StandardOutPath</key>
193
350
  <string>${xmlEscape(logPath())}</string>
194
351
  <key>StandardErrorPath</key>
@@ -246,7 +403,7 @@ function writeWindowsTaskXml(scriptPath) {
246
403
  const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
247
404
  const userId = xmlEscape(windowsUserId());
248
405
  const vbsArgs = xmlEscape(`//B "${scriptPath.replace(/"/g, '""')}"`);
249
- const xml = `<?xml version="1.0" encoding="UTF-16"?>
406
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
250
407
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
251
408
  <RegistrationInfo>
252
409
  <Description>RuntimeDev Link Agent</Description>
@@ -303,6 +460,13 @@ function installWindowsTaskScheduler(scriptPath) {
303
460
  return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
304
461
  }
305
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
+
306
470
  function installPersistence(cfg) {
307
471
  if (!homeDir()) {
308
472
  throw new Error('Could not resolve home directory');
@@ -314,9 +478,9 @@ function installPersistence(cfg) {
314
478
  case 'win32':
315
479
  return installWindowsTaskScheduler(scriptPath);
316
480
  case 'darwin':
317
- return installLaunchd(scriptPath);
481
+ return installLaunchd(scriptPath, cfg);
318
482
  default:
319
- return installCrontab(scriptPath);
483
+ return installLinuxPersistence(scriptPath);
320
484
  }
321
485
  }
322
486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtimedev-link",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {