omnikey-cli 1.0.27 → 1.0.28

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,789 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.grantBrowserAccess = grantBrowserAccess;
7
+ exports.reopenBrowserDebugProfile = reopenBrowserDebugProfile;
8
+ const inquirer_1 = __importDefault(require("inquirer"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const net_1 = __importDefault(require("net"));
12
+ const http_1 = __importDefault(require("http"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const child_process_1 = require("child_process");
15
+ const utils_1 = require("./utils");
16
+ const home = os_1.default.homedir();
17
+ const WINDOWS_BROWSERS = [
18
+ {
19
+ name: 'Chrome',
20
+ executablePaths: [
21
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
22
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
23
+ path_1.default.join(home, 'AppData', 'Local', 'Google', 'Chrome', 'Application', 'chrome.exe'),
24
+ ],
25
+ userDataDir: path_1.default.join(home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'),
26
+ },
27
+ {
28
+ name: 'Edge',
29
+ executablePaths: [
30
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
31
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
32
+ path_1.default.join(home, 'AppData', 'Local', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
33
+ ],
34
+ userDataDir: path_1.default.join(home, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data'),
35
+ },
36
+ {
37
+ name: 'Brave',
38
+ executablePaths: [
39
+ 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
40
+ path_1.default.join(home, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
41
+ ],
42
+ userDataDir: path_1.default.join(home, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'User Data'),
43
+ },
44
+ ];
45
+ const MACOS_BROWSERS = [
46
+ {
47
+ name: 'Chrome',
48
+ executablePaths: [
49
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
50
+ `${home}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
51
+ ],
52
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
53
+ },
54
+ {
55
+ name: 'Brave',
56
+ executablePaths: [
57
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
58
+ `${home}/Applications/Brave Browser.app/Contents/MacOS/Brave Browser`,
59
+ ],
60
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'),
61
+ },
62
+ {
63
+ name: 'Edge',
64
+ executablePaths: [
65
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
66
+ `${home}/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge`,
67
+ ],
68
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'Microsoft Edge'),
69
+ },
70
+ {
71
+ name: 'Arc',
72
+ executablePaths: [
73
+ '/Applications/Arc.app/Contents/MacOS/Arc',
74
+ `${home}/Applications/Arc.app/Contents/MacOS/Arc`,
75
+ ],
76
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'Arc', 'User Data'),
77
+ },
78
+ {
79
+ name: 'Vivaldi',
80
+ executablePaths: [
81
+ '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi',
82
+ `${home}/Applications/Vivaldi.app/Contents/MacOS/Vivaldi`,
83
+ ],
84
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'Vivaldi'),
85
+ },
86
+ {
87
+ name: 'Opera',
88
+ executablePaths: [
89
+ '/Applications/Opera.app/Contents/MacOS/Opera',
90
+ `${home}/Applications/Opera.app/Contents/MacOS/Opera`,
91
+ ],
92
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'com.operasoftware.Opera'),
93
+ },
94
+ {
95
+ name: 'Chromium',
96
+ executablePaths: [
97
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
98
+ `${home}/Applications/Chromium.app/Contents/MacOS/Chromium`,
99
+ ],
100
+ userDataDir: path_1.default.join(home, 'Library', 'Application Support', 'Chromium'),
101
+ },
102
+ ];
103
+ function resolveExecutable(paths) {
104
+ for (const p of paths) {
105
+ try {
106
+ if (fs_1.default.existsSync(p))
107
+ return p;
108
+ }
109
+ catch { }
110
+ }
111
+ return null;
112
+ }
113
+ function getInstalledBrowsers(catalogue) {
114
+ return catalogue
115
+ .map((b) => {
116
+ const executablePath = resolveExecutable(b.executablePaths);
117
+ return executablePath ? { ...b, executablePath } : null;
118
+ })
119
+ .filter((b) => b !== null);
120
+ }
121
+ function isPortAvailable(port) {
122
+ return new Promise((resolve) => {
123
+ const server = net_1.default.createServer();
124
+ server.once('error', () => resolve(false));
125
+ server.once('listening', () => server.close(() => resolve(true)));
126
+ server.listen(port, '127.0.0.1');
127
+ });
128
+ }
129
+ async function findAvailablePort(startPort = 9222) {
130
+ for (let port = startPort; port < startPort + 100; port++) {
131
+ if (await isPortAvailable(port))
132
+ return port;
133
+ }
134
+ throw new Error('No available port found in range 9222–9321');
135
+ }
136
+ function persistDebugPort(port) {
137
+ const configDir = (0, utils_1.getConfigDir)();
138
+ const configPath = (0, utils_1.getConfigPath)();
139
+ fs_1.default.mkdirSync(configDir, { recursive: true });
140
+ const cfg = (0, utils_1.readConfig)();
141
+ cfg['BROWSER_DEBUG_PORT'] = port;
142
+ fs_1.default.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
143
+ }
144
+ function persistDebugConfig(params) {
145
+ const configDir = (0, utils_1.getConfigDir)();
146
+ const configPath = (0, utils_1.getConfigPath)();
147
+ fs_1.default.mkdirSync(configDir, { recursive: true });
148
+ const cfg = (0, utils_1.readConfig)();
149
+ cfg['BROWSER_DEBUG_PORT'] = params.port;
150
+ cfg['BROWSER_DEBUG_BROWSER_NAME'] = params.browserName;
151
+ cfg['BROWSER_DEBUG_EXECUTABLE'] = params.executablePath;
152
+ cfg['BROWSER_DEBUG_USER_DATA_DIR'] = params.userDataDir;
153
+ fs_1.default.writeFileSync(configPath, JSON.stringify(cfg, null, 2), 'utf-8');
154
+ }
155
+ function hasExistingStartupEntry() {
156
+ if (utils_1.isWindows) {
157
+ try {
158
+ const ps = `(Get-ItemProperty -Path '${WINDOWS_RUN_KEY}' ` +
159
+ `-Name '${WINDOWS_RUN_VALUE_NAME}' -ErrorAction SilentlyContinue)` +
160
+ `.${WINDOWS_RUN_VALUE_NAME}`;
161
+ const out = (0, child_process_1.execSync)(`powershell -NoProfile -NonInteractive -Command "${ps.replace(/"/g, '\\"')}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
162
+ return out.length > 0 && out !== '$null';
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
168
+ return fs_1.default.existsSync(MACOS_LAUNCH_AGENT_PATH);
169
+ }
170
+ function quoteArgWindows(arg) {
171
+ if (!/[ \t"]/.test(arg))
172
+ return arg;
173
+ return `"${arg.replace(/"/g, '\\"')}"`;
174
+ }
175
+ function quoteArgPosix(arg) {
176
+ if (!/[^\w./:=+-]/.test(arg))
177
+ return arg;
178
+ return `"${arg.replace(/(["\\$`])/g, '\\$1')}"`;
179
+ }
180
+ function launchBrowserDebugProfile(executablePath, launchArgs) {
181
+ return new Promise((resolve) => {
182
+ let spawnErrorMsg = null;
183
+ const child = (0, child_process_1.spawn)(executablePath, launchArgs, {
184
+ detached: true,
185
+ stdio: 'ignore',
186
+ });
187
+ child.on('error', (err) => {
188
+ spawnErrorMsg = err.message;
189
+ });
190
+ child.unref();
191
+ setTimeout(() => resolve(spawnErrorMsg), 500);
192
+ });
193
+ }
194
+ async function setupDebuggingPort(browser) {
195
+ if (hasExistingStartupEntry()) {
196
+ const location = utils_1.isWindows
197
+ ? `Registry: ${WINDOWS_RUN_KEY}\\${WINDOWS_RUN_VALUE_NAME}`
198
+ : MACOS_LAUNCH_AGENT_PATH;
199
+ console.log(`\nA permanent browser debug startup entry already exists:\n ${location}`);
200
+ const { action } = await inquirer_1.default.prompt([
201
+ {
202
+ type: 'list',
203
+ name: 'action',
204
+ message: 'What would you like to do?',
205
+ choices: [
206
+ { name: 'Update browser / debug profile / port settings', value: 'update' },
207
+ { name: 'Remove it (disable permanent startup)', value: 'remove' },
208
+ { name: 'Cancel', value: 'cancel' },
209
+ ],
210
+ },
211
+ ]);
212
+ if (action === 'cancel')
213
+ return;
214
+ if (action === 'remove') {
215
+ try {
216
+ if (utils_1.isWindows) {
217
+ removeWindowsStartup();
218
+ }
219
+ else {
220
+ removeMacOSLaunchAgent();
221
+ }
222
+ console.log('Startup entry removed.');
223
+ }
224
+ catch (err) {
225
+ console.error('Failed to remove entry:', err instanceof Error ? err.message : String(err));
226
+ }
227
+ return;
228
+ }
229
+ }
230
+ const configDir = (0, utils_1.getConfigDir)();
231
+ const debugRootDir = path_1.default.join(configDir, 'browser-debug-profiles');
232
+ fs_1.default.mkdirSync(debugRootDir, { recursive: true });
233
+ const safeBrowserName = browser.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
234
+ const existingDebugProfiles = fs_1.default
235
+ .readdirSync(debugRootDir, { withFileTypes: true })
236
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${safeBrowserName}-`))
237
+ .map((entry) => ({
238
+ dirName: entry.name,
239
+ displayName: entry.name.replace(`${safeBrowserName}-`, ''),
240
+ fullPath: path_1.default.join(debugRootDir, entry.name),
241
+ }));
242
+ console.log(`\nOmnikey will launch ${browser.name} with its own dedicated debug profile.\n` +
243
+ `This is separate from your normal ${browser.name} profile, so you may need to sign in again.\n`);
244
+ let debugUserDataDir;
245
+ let debugProfileLabel;
246
+ if (existingDebugProfiles.length > 0) {
247
+ const { profileMode } = await inquirer_1.default.prompt([
248
+ {
249
+ type: 'list',
250
+ name: 'profileMode',
251
+ message: `Choose an Omnikey debug profile for ${browser.name}:`,
252
+ choices: [
253
+ { name: 'Create a new debug profile', value: 'new' },
254
+ { name: 'Reuse an existing debug profile', value: 'existing' },
255
+ ],
256
+ },
257
+ ]);
258
+ if (profileMode === 'existing') {
259
+ const { existingProfile } = await inquirer_1.default.prompt([
260
+ {
261
+ type: 'list',
262
+ name: 'existingProfile',
263
+ message: 'Select an existing Omnikey debug profile:',
264
+ choices: existingDebugProfiles.map((p) => ({
265
+ name: `${p.displayName} (${p.dirName})`,
266
+ value: p,
267
+ })),
268
+ },
269
+ ]);
270
+ debugUserDataDir = existingProfile.fullPath;
271
+ debugProfileLabel = existingProfile.displayName;
272
+ }
273
+ else {
274
+ const { profileName } = await inquirer_1.default.prompt([
275
+ {
276
+ type: 'input',
277
+ name: 'profileName',
278
+ message: 'Name for the new Omnikey debug profile:',
279
+ default: 'default',
280
+ validate: (input) => {
281
+ const trimmed = input.trim();
282
+ if (!trimmed)
283
+ return 'Enter a profile name.';
284
+ return true;
285
+ },
286
+ filter: (input) => input.trim(),
287
+ },
288
+ ]);
289
+ const safeProfileName = profileName.toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
290
+ debugProfileLabel = profileName;
291
+ debugUserDataDir = path_1.default.join(debugRootDir, `${safeBrowserName}-${safeProfileName}`);
292
+ fs_1.default.mkdirSync(debugUserDataDir, { recursive: true });
293
+ }
294
+ }
295
+ else {
296
+ const { profileName } = await inquirer_1.default.prompt([
297
+ {
298
+ type: 'input',
299
+ name: 'profileName',
300
+ message: `Name for the Omnikey debug profile for ${browser.name}:`,
301
+ default: 'default',
302
+ validate: (input) => {
303
+ const trimmed = input.trim();
304
+ if (!trimmed)
305
+ return 'Enter a profile name.';
306
+ return true;
307
+ },
308
+ filter: (input) => input.trim(),
309
+ },
310
+ ]);
311
+ const safeProfileName = profileName.toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
312
+ debugProfileLabel = profileName;
313
+ debugUserDataDir = path_1.default.join(debugRootDir, `${safeBrowserName}-${safeProfileName}`);
314
+ fs_1.default.mkdirSync(debugUserDataDir, { recursive: true });
315
+ }
316
+ try {
317
+ fs_1.default.writeFileSync(path_1.default.join(debugUserDataDir, 'omnikey-profile.json'), JSON.stringify({
318
+ browser: browser.name,
319
+ profileLabel: debugProfileLabel,
320
+ createdAt: new Date().toISOString(),
321
+ }, null, 2), 'utf-8');
322
+ }
323
+ catch { }
324
+ const port = await findAvailablePort(9222);
325
+ console.log(`\nAvailable debug port: ${port}`);
326
+ persistDebugPort(port);
327
+ persistDebugConfig({
328
+ browserName: browser.name,
329
+ executablePath: browser.executablePath,
330
+ userDataDir: debugUserDataDir,
331
+ port,
332
+ });
333
+ console.log(`Saved browser debug configuration to config.\n`);
334
+ const launchArgs = [
335
+ `--remote-debugging-port=${port}`,
336
+ `--user-data-dir=${debugUserDataDir}`,
337
+ '--no-first-run',
338
+ '--no-default-browser-check',
339
+ ];
340
+ console.log(`Omnikey debug profile:\n ${debugProfileLabel}\n` + `Profile path:\n ${debugUserDataDir}\n`);
341
+ console.log(`Close any open ${browser.name} windows, then press Enter.`);
342
+ await inquirer_1.default.prompt([{ type: 'input', name: '_', message: 'Press Enter when ready…' }]);
343
+ console.log(`Closing any remaining ${browser.name} processes…`);
344
+ killBrowserProcesses(browser.name);
345
+ if (utils_1.isWindows) {
346
+ const exe = WINDOWS_EXE_NAMES[browser.name];
347
+ if (exe)
348
+ await waitUntilProcessDead(exe);
349
+ }
350
+ else {
351
+ await new Promise((r) => setTimeout(r, 2000));
352
+ }
353
+ for (const fileName of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
354
+ const p = path_1.default.join(debugUserDataDir, fileName);
355
+ if (fs_1.default.existsSync(p)) {
356
+ try {
357
+ fs_1.default.unlinkSync(p);
358
+ }
359
+ catch {
360
+ console.warn(`Could not remove stale ${fileName} from debug profile.`);
361
+ }
362
+ }
363
+ }
364
+ try {
365
+ if (utils_1.isWindows) {
366
+ registerWindowsStartup(browser.executablePath, launchArgs);
367
+ console.log(`Startup entry saved to Registry (${WINDOWS_RUN_KEY}\\${WINDOWS_RUN_VALUE_NAME}).`);
368
+ }
369
+ else {
370
+ registerMacOSLaunchAgent(browser.executablePath, launchArgs);
371
+ console.log(`LaunchAgent written to:\n ${MACOS_LAUNCH_AGENT_PATH}`);
372
+ }
373
+ }
374
+ catch (err) {
375
+ console.error('Failed to register startup entry:', err instanceof Error ? err.message : String(err));
376
+ printLaunchHint(browser.executablePath, launchArgs);
377
+ return;
378
+ }
379
+ console.log(`\nLaunching ${browser.name}…`);
380
+ console.log(` Command: "${browser.executablePath}" ${utils_1.isWindows
381
+ ? launchArgs.map(quoteArgWindows).join(' ')
382
+ : launchArgs.map(quoteArgPosix).join(' ')}`);
383
+ const spawnErrorMsg = await launchBrowserDebugProfile(browser.executablePath, launchArgs);
384
+ if (spawnErrorMsg) {
385
+ console.error(`Failed to launch browser: ${spawnErrorMsg}`);
386
+ printLaunchHint(browser.executablePath, launchArgs);
387
+ return;
388
+ }
389
+ console.log(`Waiting for debug port ${port} to become active…`);
390
+ const portUp = await waitForDebugPort(port);
391
+ if (portUp) {
392
+ console.log(`\nDebug port ${port} is active.\n` +
393
+ `Verify at: http://localhost:${port}/json\n` +
394
+ `Omnikey can now access tabs opened in the Omnikey-managed ${browser.name} debug profile.\n` +
395
+ `${browser.name} will start automatically on every future login using this debug profile.`);
396
+ }
397
+ else {
398
+ console.error(`\nCould not reach localhost:${port} after 15 s.\n` +
399
+ `Possible causes:\n` +
400
+ ` 1. A background ${browser.name} process is still alive — open Task Manager → Details,\n` +
401
+ ` end all "${WINDOWS_EXE_NAMES[browser.name] ?? browser.name.toLowerCase()}" entries, then run this command again.\n` +
402
+ ` 2. Security software or policy is blocking the remote-debugging-port flag.\n` +
403
+ ` 3. The browser opened, but failed to initialize the Omnikey debug profile.\n\n` +
404
+ `Try launching manually:\n` +
405
+ (utils_1.isWindows
406
+ ? ` & "${browser.executablePath}" ${launchArgs.map(quoteArgWindows).join(' ')}`
407
+ : ` "${browser.executablePath}" ${launchArgs.map(quoteArgPosix).join(' ')}`));
408
+ }
409
+ }
410
+ function printLaunchHint(executablePath, launchArgs) {
411
+ console.log('\nTo enable browser access, start your browser with:');
412
+ if (utils_1.isWindows) {
413
+ console.log(` & "${executablePath}" ${launchArgs.map(quoteArgWindows).join(' ')}`);
414
+ }
415
+ else {
416
+ const quotedExe = executablePath.includes(' ') ? `"${executablePath}"` : executablePath;
417
+ console.log(` ${quotedExe} ${launchArgs.map(quoteArgPosix).join(' ')}`);
418
+ }
419
+ }
420
+ const WINDOWS_EXE_NAMES = {
421
+ Chrome: 'chrome.exe',
422
+ Edge: 'msedge.exe',
423
+ Brave: 'brave.exe',
424
+ };
425
+ function killBrowserProcesses(browserName) {
426
+ if (utils_1.isWindows) {
427
+ const exe = WINDOWS_EXE_NAMES[browserName];
428
+ if (!exe)
429
+ return;
430
+ const baseName = exe.replace('.exe', '');
431
+ try {
432
+ (0, child_process_1.execSync)(`taskkill /F /IM "${exe}" /T`, { stdio: ['pipe', 'pipe', 'pipe'] });
433
+ }
434
+ catch { }
435
+ try {
436
+ (0, child_process_1.execSync)(`powershell -NoProfile -NonInteractive -Command "Stop-Process -Name '${baseName}' -Force -ErrorAction SilentlyContinue"`, { stdio: ['pipe', 'pipe', 'pipe'] });
437
+ }
438
+ catch { }
439
+ }
440
+ else {
441
+ const processName = MACOS_PROCESS_NAMES[browserName];
442
+ if (!processName)
443
+ return;
444
+ try {
445
+ (0, child_process_1.execSync)(`pkill -x "${processName}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
446
+ }
447
+ catch { }
448
+ }
449
+ }
450
+ async function waitUntilProcessDead(exe, timeoutMs = 8000) {
451
+ const deadline = Date.now() + timeoutMs;
452
+ while (Date.now() < deadline) {
453
+ try {
454
+ const out = (0, child_process_1.execSync)(`tasklist /FI "IMAGENAME eq ${exe}" /FO CSV /NH 2>nul`, {
455
+ encoding: 'utf8',
456
+ stdio: ['pipe', 'pipe', 'pipe'],
457
+ });
458
+ if (!out.toLowerCase().includes(exe.toLowerCase()))
459
+ return;
460
+ }
461
+ catch {
462
+ return;
463
+ }
464
+ await new Promise((r) => setTimeout(r, 400));
465
+ }
466
+ }
467
+ function waitForDebugPort(port, timeoutMs = 15000) {
468
+ return new Promise((resolve) => {
469
+ const deadline = Date.now() + timeoutMs;
470
+ const probe = () => {
471
+ const req = http_1.default.get({ hostname: '127.0.0.1', port, path: '/json/version', timeout: 1500 }, (res) => {
472
+ res.resume();
473
+ if (res.statusCode === 200)
474
+ return resolve(true);
475
+ retry();
476
+ });
477
+ req.on('error', retry);
478
+ req.on('timeout', () => {
479
+ req.destroy();
480
+ retry();
481
+ });
482
+ };
483
+ const retry = () => {
484
+ if (Date.now() >= deadline)
485
+ return resolve(false);
486
+ setTimeout(probe, 600);
487
+ };
488
+ probe();
489
+ });
490
+ }
491
+ const WINDOWS_RUN_KEY = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
492
+ const WINDOWS_RUN_VALUE_NAME = 'OmnikeyBrowserDebug';
493
+ const MACOS_LAUNCH_AGENT_LABEL = 'com.omnikey.browser-debug';
494
+ const MACOS_LAUNCH_AGENT_PATH = path_1.default.join(home, 'Library', 'LaunchAgents', `${MACOS_LAUNCH_AGENT_LABEL}.plist`);
495
+ function registerWindowsStartup(executablePath, launchArgs) {
496
+ const cmd = `"${executablePath}" ${launchArgs.map(quoteArgWindows).join(' ')}`;
497
+ const ps = `Set-ItemProperty -Path '${WINDOWS_RUN_KEY}' ` +
498
+ `-Name '${WINDOWS_RUN_VALUE_NAME}' ` +
499
+ `-Value '${cmd.replace(/'/g, "''")}'`;
500
+ (0, child_process_1.execSync)(`powershell -NoProfile -NonInteractive -Command "${ps.replace(/"/g, '\\"')}"`, {
501
+ stdio: ['pipe', 'pipe', 'pipe'],
502
+ });
503
+ }
504
+ function removeWindowsStartup() {
505
+ const ps = `Remove-ItemProperty -Path '${WINDOWS_RUN_KEY}' ` +
506
+ `-Name '${WINDOWS_RUN_VALUE_NAME}' -ErrorAction SilentlyContinue`;
507
+ (0, child_process_1.execSync)(`powershell -NoProfile -NonInteractive -Command "${ps.replace(/"/g, '\\"')}"`, {
508
+ stdio: ['pipe', 'pipe', 'pipe'],
509
+ });
510
+ }
511
+ function registerMacOSLaunchAgent(executablePath, launchArgs) {
512
+ const plist = [
513
+ '<?xml version="1.0" encoding="UTF-8"?>',
514
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"',
515
+ ' "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
516
+ '<plist version="1.0">',
517
+ '<dict>',
518
+ ` <key>Label</key><string>${MACOS_LAUNCH_AGENT_LABEL}</string>`,
519
+ ' <key>ProgramArguments</key>',
520
+ ' <array>',
521
+ ` <string>${executablePath}</string>`,
522
+ ...launchArgs.map((a) => ` <string>${a}</string>`),
523
+ ' </array>',
524
+ ' <key>RunAtLoad</key><true/>',
525
+ ' <key>KeepAlive</key><false/>',
526
+ ` <key>StandardOutPath</key><string>${path_1.default.join(os_1.default.tmpdir(), 'omnikey-browser-debug.log')}</string>`,
527
+ ` <key>StandardErrorPath</key><string>${path_1.default.join(os_1.default.tmpdir(), 'omnikey-browser-debug.err')}</string>`,
528
+ '</dict>',
529
+ '</plist>',
530
+ ].join('\n');
531
+ fs_1.default.mkdirSync(path_1.default.dirname(MACOS_LAUNCH_AGENT_PATH), { recursive: true });
532
+ fs_1.default.writeFileSync(MACOS_LAUNCH_AGENT_PATH, plist, 'utf-8');
533
+ try {
534
+ (0, child_process_1.execSync)(`launchctl unload "${MACOS_LAUNCH_AGENT_PATH}" 2>/dev/null`, {
535
+ stdio: ['pipe', 'pipe', 'pipe'],
536
+ });
537
+ }
538
+ catch { }
539
+ (0, child_process_1.execSync)(`launchctl load "${MACOS_LAUNCH_AGENT_PATH}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
540
+ }
541
+ function removeMacOSLaunchAgent() {
542
+ if (!fs_1.default.existsSync(MACOS_LAUNCH_AGENT_PATH))
543
+ return;
544
+ try {
545
+ (0, child_process_1.execSync)(`launchctl unload "${MACOS_LAUNCH_AGENT_PATH}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
546
+ }
547
+ catch { }
548
+ fs_1.default.unlinkSync(MACOS_LAUNCH_AGENT_PATH);
549
+ }
550
+ const MACOS_PROCESS_NAMES = {
551
+ Chrome: 'Google Chrome',
552
+ Brave: 'Brave Browser',
553
+ Edge: 'Microsoft Edge',
554
+ Arc: 'Arc',
555
+ Vivaldi: 'Vivaldi',
556
+ Opera: 'Opera',
557
+ Chromium: 'Chromium',
558
+ Safari: 'Safari',
559
+ };
560
+ async function setupAppleScript() {
561
+ const chromiumInstalled = getInstalledBrowsers(MACOS_BROWSERS);
562
+ const safariPresent = fs_1.default.existsSync('/Applications/Safari.app');
563
+ const choices = [
564
+ ...chromiumInstalled.map((b) => ({ name: b.name, value: b.name })),
565
+ ...(safariPresent ? [{ name: 'Safari', value: 'Safari' }] : []),
566
+ ];
567
+ if (choices.length === 0) {
568
+ console.log('No supported browsers found on this system.');
569
+ return;
570
+ }
571
+ const { selectedNames } = await inquirer_1.default.prompt([
572
+ {
573
+ type: 'checkbox',
574
+ name: 'selectedNames',
575
+ message: 'Select browsers to enable "Allow JavaScript from Apple Events":',
576
+ choices,
577
+ validate: (input) => input.length > 0 || 'Select at least one browser.',
578
+ },
579
+ ]);
580
+ console.log('\nFollow these steps for each selected browser:\n');
581
+ for (const name of selectedNames) {
582
+ if (name === 'Safari') {
583
+ console.log('Safari:\n' +
584
+ ' 1. Open Safari → Settings (Cmd + ,) → Advanced tab\n' +
585
+ ' 2. Check "Show features for web developers" (enables the Develop menu)\n' +
586
+ ' 3. In the menu bar choose Develop → Allow JavaScript from Apple Events\n');
587
+ }
588
+ else {
589
+ console.log(`${name}:\n` +
590
+ ' 1. Open the browser\n' +
591
+ ' 2. Open DevTools: Cmd + Option + I\n' +
592
+ ' 3. Click the gear icon (⚙) in the top-right corner of the DevTools panel\n' +
593
+ ' 4. Under the "Preferences" tab scroll to the "Sources" section\n' +
594
+ ' 5. Check "Allow JavaScript from Apple Events"\n' +
595
+ ' 6. Close DevTools — the setting takes effect immediately\n');
596
+ }
597
+ }
598
+ console.log('Once enabled, Omnikey can read content directly from the active tab.');
599
+ }
600
+ async function grantBrowserAccess() {
601
+ if (!utils_1.isWindows) {
602
+ const { method } = await inquirer_1.default.prompt([
603
+ {
604
+ type: 'list',
605
+ name: 'method',
606
+ message: 'How should Omnikey access authenticated browser tabs?',
607
+ choices: [
608
+ {
609
+ name: 'Remote Debugging Port — launch browser with CDP; works on all Chromium browsers',
610
+ value: 'debugging-port',
611
+ },
612
+ {
613
+ name: 'AppleScript — read live tabs without relaunching; requires "Allow JavaScript from Apple Events"',
614
+ value: 'applescript',
615
+ },
616
+ ],
617
+ },
618
+ ]);
619
+ if (method === 'applescript') {
620
+ await setupAppleScript();
621
+ return;
622
+ }
623
+ }
624
+ const catalogue = utils_1.isWindows ? WINDOWS_BROWSERS : MACOS_BROWSERS;
625
+ const installed = getInstalledBrowsers(catalogue);
626
+ if (installed.length === 0) {
627
+ console.log('No supported Chromium browsers found.\n' +
628
+ 'Supported: Chrome, Edge, Brave' +
629
+ (utils_1.isWindows ? '.' : ', Arc, Vivaldi, Opera, Chromium.'));
630
+ return;
631
+ }
632
+ const { browser } = await inquirer_1.default.prompt([
633
+ {
634
+ type: 'list',
635
+ name: 'browser',
636
+ message: 'Select the browser to set up:',
637
+ choices: installed.map((b) => ({ name: b.name, value: b })),
638
+ },
639
+ ]);
640
+ await setupDebuggingPort(browser);
641
+ }
642
+ async function recoverDebugConfig() {
643
+ const debugRootDir = path_1.default.join((0, utils_1.getConfigDir)(), 'browser-debug-profiles');
644
+ if (!fs_1.default.existsSync(debugRootDir))
645
+ return null;
646
+ const profileDirs = fs_1.default
647
+ .readdirSync(debugRootDir, { withFileTypes: true })
648
+ .filter((e) => e.isDirectory())
649
+ .map((e) => e.name);
650
+ if (profileDirs.length === 0)
651
+ return null;
652
+ const catalogue = utils_1.isWindows ? WINDOWS_BROWSERS : MACOS_BROWSERS;
653
+ const installed = getInstalledBrowsers(catalogue);
654
+ // Pair each profile directory with its browser by matching the name prefix.
655
+ const candidates = [];
656
+ for (const profileDir of profileDirs) {
657
+ const browser = installed.find((b) => profileDir.startsWith(b.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '-'));
658
+ if (browser)
659
+ candidates.push({ profileDir, browser });
660
+ }
661
+ if (candidates.length === 0)
662
+ return null;
663
+ let chosen;
664
+ if (candidates.length === 1) {
665
+ chosen = candidates[0];
666
+ console.log(`Recovered debug profile: ${chosen.browser.name} › ${chosen.profileDir}`);
667
+ }
668
+ else {
669
+ const { selection } = await inquirer_1.default.prompt([
670
+ {
671
+ type: 'list',
672
+ name: 'selection',
673
+ message: 'Multiple Omnikey debug profiles found — which one should open?',
674
+ choices: candidates.map((c) => ({
675
+ name: `${c.browser.name} (${c.profileDir})`,
676
+ value: c,
677
+ })),
678
+ },
679
+ ]);
680
+ chosen = selection;
681
+ }
682
+ const cfg = (0, utils_1.readConfig)();
683
+ const port = Number.isFinite(Number(cfg['BROWSER_DEBUG_PORT'])) && Number(cfg['BROWSER_DEBUG_PORT']) > 0
684
+ ? Number(cfg['BROWSER_DEBUG_PORT'])
685
+ : await findAvailablePort(9222);
686
+ return {
687
+ browserName: chosen.browser.name,
688
+ executablePath: chosen.browser.executablePath,
689
+ userDataDir: path_1.default.join(debugRootDir, chosen.profileDir),
690
+ port,
691
+ };
692
+ }
693
+ async function reopenBrowserDebugProfile() {
694
+ let cfg = (0, utils_1.readConfig)();
695
+ let executablePath = cfg['BROWSER_DEBUG_EXECUTABLE'] || '';
696
+ let userDataDir = cfg['BROWSER_DEBUG_USER_DATA_DIR'] || '';
697
+ let port = Number(cfg['BROWSER_DEBUG_PORT']);
698
+ let browserName = cfg['BROWSER_DEBUG_BROWSER_NAME'] || '';
699
+ // If the config is incomplete (e.g. created by an older version of grant-browser-access),
700
+ // try to recover by scanning the browser-debug-profiles directory.
701
+ if (!executablePath || !userDataDir) {
702
+ const recovered = await recoverDebugConfig();
703
+ if (!recovered) {
704
+ console.error('No saved browser debug profile found.\n' +
705
+ 'Run `omnikey grant-browser-access` first to set one up.');
706
+ return;
707
+ }
708
+ executablePath = recovered.executablePath;
709
+ userDataDir = recovered.userDataDir;
710
+ browserName = browserName || recovered.browserName;
711
+ if (!Number.isFinite(port) || port <= 0)
712
+ port = recovered.port;
713
+ // Persist the recovered values so future runs skip this step.
714
+ persistDebugConfig({ browserName, executablePath, userDataDir, port });
715
+ cfg = (0, utils_1.readConfig)();
716
+ }
717
+ if (!Number.isFinite(port) || port <= 0) {
718
+ console.error('No valid saved browser debug port found. Run browser setup first.');
719
+ return;
720
+ }
721
+ if (!fs_1.default.existsSync(executablePath)) {
722
+ console.error(`Saved browser executable does not exist:\n ${executablePath}`);
723
+ return;
724
+ }
725
+ fs_1.default.mkdirSync(userDataDir, { recursive: true });
726
+ const launchArgs = [
727
+ `--remote-debugging-port=${port}`,
728
+ `--user-data-dir=${userDataDir}`,
729
+ '--no-first-run',
730
+ '--no-default-browser-check',
731
+ ];
732
+ // If the debug endpoint is already up, do not kill/relaunch anything.
733
+ const alreadyUp = await waitForDebugPort(port, 1200);
734
+ if (alreadyUp) {
735
+ console.log(`Debug port ${port} is already active.\n` +
736
+ `Verify at: http://localhost:${port}/json\n` +
737
+ `Omnikey can already access tabs opened in the saved ${browserName} debug profile.`);
738
+ return;
739
+ }
740
+ console.log(`Closing any running ${browserName} processes…`);
741
+ killBrowserProcesses(browserName);
742
+ if (utils_1.isWindows) {
743
+ const exe = WINDOWS_EXE_NAMES[browserName];
744
+ if (exe) {
745
+ await waitUntilProcessDead(exe);
746
+ }
747
+ else {
748
+ await new Promise((r) => setTimeout(r, 1500));
749
+ }
750
+ }
751
+ else {
752
+ await new Promise((r) => setTimeout(r, 2000));
753
+ }
754
+ for (const fileName of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
755
+ const p = path_1.default.join(userDataDir, fileName);
756
+ if (fs_1.default.existsSync(p)) {
757
+ try {
758
+ fs_1.default.unlinkSync(p);
759
+ }
760
+ catch {
761
+ console.warn(`Could not remove stale ${fileName} from debug profile.`);
762
+ }
763
+ }
764
+ }
765
+ console.log(`Reopening ${browserName} with Omnikey debug profile…`);
766
+ console.log(` Command: "${executablePath}" ${utils_1.isWindows
767
+ ? launchArgs.map(quoteArgWindows).join(' ')
768
+ : launchArgs.map(quoteArgPosix).join(' ')}`);
769
+ const spawnErrorMsg = await launchBrowserDebugProfile(executablePath, launchArgs);
770
+ if (spawnErrorMsg) {
771
+ console.error(`Failed to launch browser: ${spawnErrorMsg}`);
772
+ printLaunchHint(executablePath, launchArgs);
773
+ return;
774
+ }
775
+ console.log(`Waiting for debug port ${port} to become active…`);
776
+ const portUp = await waitForDebugPort(port);
777
+ if (portUp) {
778
+ console.log(`\nDebug port ${port} is active.\n` +
779
+ `Verify at: http://localhost:${port}/json\n` +
780
+ `Omnikey can now access tabs opened in the saved ${browserName} debug profile.`);
781
+ }
782
+ else {
783
+ console.error(`\nCould not reach localhost:${port} after relaunch.\n` +
784
+ `Manual launch:\n` +
785
+ (utils_1.isWindows
786
+ ? ` & "${executablePath}" ${launchArgs.map(quoteArgWindows).join(' ')}`
787
+ : ` "${executablePath}" ${launchArgs.map(quoteArgPosix).join(' ')}`));
788
+ }
789
+ }