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