@vybestack/llxprt-code-core 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7
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/dist/src/config/config.d.ts +20 -0
- package/dist/src/config/config.js +143 -23
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/configBaseCore.d.ts +6 -2
- package/dist/src/config/configBaseCore.js +8 -0
- package/dist/src/config/configBaseCore.js.map +1 -1
- package/dist/src/config/configConstructor.d.ts +2 -0
- package/dist/src/config/configConstructor.js +9 -1
- package/dist/src/config/configConstructor.js.map +1 -1
- package/dist/src/config/configTestHarness.d.ts +8 -6
- package/dist/src/config/configTestHarness.js +10 -4
- package/dist/src/config/configTestHarness.js.map +1 -1
- package/dist/src/config/configTypes.d.ts +7 -2
- package/dist/src/config/configTypes.js +2 -0
- package/dist/src/config/configTypes.js.map +1 -1
- package/dist/src/config/liveTrustTransitionLifecycle.d.ts +35 -0
- package/dist/src/config/liveTrustTransitionLifecycle.js +180 -0
- package/dist/src/config/liveTrustTransitionLifecycle.js.map +1 -0
- package/dist/src/config/schedulerSingleton.d.ts +2 -2
- package/dist/src/config/schedulerSingleton.js.map +1 -1
- package/dist/src/core/compression/types.d.ts +48 -1
- package/dist/src/core/compression/types.js +41 -0
- package/dist/src/core/compression/types.js.map +1 -1
- package/dist/src/core/model-limits.json +96 -0
- package/dist/src/core/model-limits.schema.d.ts +123 -0
- package/dist/src/core/model-limits.schema.js +75 -0
- package/dist/src/core/model-limits.schema.js.map +1 -0
- package/dist/src/core/tokenLimits.d.ts +6 -8
- package/dist/src/core/tokenLimits.js +56 -132
- package/dist/src/core/tokenLimits.js.map +1 -1
- package/dist/src/core/turn.d.ts +20 -3
- package/dist/src/core/turn.js +20 -3
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/hooks/hookRegistry.d.ts +1 -1
- package/dist/src/hooks/hookRegistry.js +3 -1
- package/dist/src/hooks/hookRegistry.js.map +1 -1
- package/dist/src/hooks/hookRunner.d.ts +6 -0
- package/dist/src/hooks/hookRunner.js +22 -6
- package/dist/src/hooks/hookRunner.js.map +1 -1
- package/dist/src/hooks/hookSystem.d.ts +10 -1
- package/dist/src/hooks/hookSystem.js +69 -12
- package/dist/src/hooks/hookSystem.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/llm-types/geminiContent.d.ts +3 -0
- package/dist/src/policy/config.d.ts +4 -2
- package/dist/src/policy/config.js +22 -18
- package/dist/src/policy/config.js.map +1 -1
- package/dist/src/recording/ReplayEngine.js +39 -2
- package/dist/src/recording/ReplayEngine.js.map +1 -1
- package/dist/src/recording/SessionDiscovery.d.ts +8 -0
- package/dist/src/recording/SessionDiscovery.js +88 -3
- package/dist/src/recording/SessionDiscovery.js.map +1 -1
- package/dist/src/recording/SessionLockManager.d.ts +3 -0
- package/dist/src/recording/SessionLockManager.js +9 -3
- package/dist/src/recording/SessionLockManager.js.map +1 -1
- package/dist/src/recording/SessionRecordingService.d.ts +14 -1
- package/dist/src/recording/SessionRecordingService.js +27 -3
- package/dist/src/recording/SessionRecordingService.js.map +1 -1
- package/dist/src/recording/index.d.ts +4 -3
- package/dist/src/recording/index.js +4 -3
- package/dist/src/recording/index.js.map +1 -1
- package/dist/src/recording/replay-test-helpers.d.ts +5 -0
- package/dist/src/recording/replay-test-helpers.js +13 -0
- package/dist/src/recording/replay-test-helpers.js.map +1 -1
- package/dist/src/recording/resumeNotFoundMessages.d.ts +56 -0
- package/dist/src/recording/resumeNotFoundMessages.js +64 -0
- package/dist/src/recording/resumeNotFoundMessages.js.map +1 -0
- package/dist/src/recording/resumeSession.js +3 -2
- package/dist/src/recording/resumeSession.js.map +1 -1
- package/dist/src/recording/sessionManagement.d.ts +3 -0
- package/dist/src/recording/sessionManagement.js +38 -19
- package/dist/src/recording/sessionManagement.js.map +1 -1
- package/dist/src/recording/types.d.ts +42 -3
- package/dist/src/recording/types.js +1 -1
- package/dist/src/recording/types.js.map +1 -1
- package/dist/src/runtime/AgentRuntimeLoader.js +2 -1
- package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -1
- package/dist/src/scheduler/liveOutput.d.ts +10 -19
- package/dist/src/scheduler/liveOutput.js +21 -21
- package/dist/src/scheduler/liveOutput.js.map +1 -1
- package/dist/src/scheduler/types.d.ts +2 -2
- package/dist/src/services/history/ContentConverters.d.ts +4 -0
- package/dist/src/services/history/ContentConverters.js +77 -59
- package/dist/src/services/history/ContentConverters.js.map +1 -1
- package/dist/src/services/history/HistoryService.d.ts +13 -1
- package/dist/src/services/history/HistoryService.js +71 -0
- package/dist/src/services/history/HistoryService.js.map +1 -1
- package/dist/src/services/history/IContent.d.ts +10 -0
- package/dist/src/services/history/IContent.js.map +1 -1
- package/dist/src/skills/skillManager.d.ts +11 -1
- package/dist/src/skills/skillManager.js +21 -1
- package/dist/src/skills/skillManager.js.map +1 -1
- package/dist/src/test-utils/mock-tool.d.ts +3 -3
- package/dist/src/test-utils/mock-tool.js.map +1 -1
- package/dist/src/test-utils/tools.d.ts +2 -2
- package/dist/src/test-utils/tools.js.map +1 -1
- package/dist/src/tools-adapters/CoreShellToolHostAdapter.js +1 -6
- package/dist/src/tools-adapters/CoreShellToolHostAdapter.js.map +1 -1
- package/dist/src/utils/browser-profile-discovery.d.ts +40 -0
- package/dist/src/utils/browser-profile-discovery.js +251 -0
- package/dist/src/utils/browser-profile-discovery.js.map +1 -0
- package/dist/src/utils/env.d.ts +12 -0
- package/dist/src/utils/env.js +15 -0
- package/dist/src/utils/env.js.map +1 -0
- package/dist/src/utils/events.d.ts +10 -1
- package/dist/src/utils/events.js +22 -0
- package/dist/src/utils/events.js.map +1 -1
- package/dist/src/utils/fileUtils.js +8 -1
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/retry.js +3 -4
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/secure-browser-launcher.d.ts +35 -1
- package/dist/src/utils/secure-browser-launcher.js +341 -1
- package/dist/src/utils/secure-browser-launcher.js.map +1 -1
- package/dist/src/utils/terminalSerializer.d.ts +12 -0
- package/dist/src/utils/terminalSerializer.js.map +1 -1
- package/dist/src/utils/thoughtUtils.d.ts +2 -0
- package/dist/src/utils/thoughtUtils.js.map +1 -1
- package/package.json +13 -9
- package/src/config/config.ts +161 -22
- package/src/config/configBaseCore.ts +10 -2
- package/src/config/configConstructor.ts +11 -0
- package/src/config/configTestHarness.ts +10 -4
- package/src/config/configTypes.ts +6 -1
- package/src/config/liveTrustTransitionLifecycle.ts +250 -0
- package/src/config/schedulerSingleton.ts +2 -5
- package/src/core/compression/types.ts +81 -1
- package/src/core/legacy-model-limits.expected.txt +96 -0
- package/src/core/model-limits.json +96 -0
- package/src/core/model-limits.schema.ts +87 -0
- package/src/core/tokenLimits.ts +73 -147
- package/src/core/turn.ts +20 -3
- package/src/hooks/hookRegistry.ts +3 -1
- package/src/hooks/hookRunner.ts +24 -7
- package/src/hooks/hookSystem.ts +73 -13
- package/src/index.ts +1 -0
- package/src/llm-types/geminiContent.ts +3 -0
- package/src/policy/config.ts +32 -19
- package/src/recording/ReplayEngine.ts +51 -2
- package/src/recording/SessionDiscovery.ts +99 -4
- package/src/recording/SessionLockManager.ts +10 -3
- package/src/recording/SessionRecordingService.ts +36 -3
- package/src/recording/index.ts +19 -2
- package/src/recording/replay-test-helpers.ts +14 -0
- package/src/recording/resumeNotFoundMessages.ts +75 -0
- package/src/recording/resumeSession.ts +3 -1
- package/src/recording/sessionManagement.ts +51 -21
- package/src/recording/types.ts +44 -2
- package/src/runtime/AgentRuntimeLoader.ts +3 -1
- package/src/scheduler/liveOutput.ts +25 -22
- package/src/scheduler/types.ts +5 -2
- package/src/services/history/ContentConverters.ts +91 -68
- package/src/services/history/HistoryService.ts +92 -1
- package/src/services/history/IContent.ts +13 -0
- package/src/skills/skillManager.ts +26 -1
- package/src/test-utils/mock-tool.ts +4 -3
- package/src/test-utils/tools.ts +4 -3
- package/src/tools-adapters/CoreShellToolHostAdapter.ts +1 -7
- package/src/utils/browser-profile-discovery.ts +358 -0
- package/src/utils/env.ts +15 -0
- package/src/utils/events.ts +31 -0
- package/src/utils/fileUtils.ts +11 -1
- package/src/utils/retry.ts +3 -4
- package/src/utils/secure-browser-launcher.ts +500 -2
- package/src/utils/terminalSerializer.ts +9 -0
- package/src/utils/thoughtUtils.ts +2 -0
|
@@ -7,11 +7,146 @@
|
|
|
7
7
|
import { execFile, type ExecFileOptions } from 'node:child_process';
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import { platform } from 'node:os';
|
|
10
|
+
import { stat } from 'node:fs/promises';
|
|
11
|
+
import { env } from 'node:process';
|
|
10
12
|
import { URL } from 'node:url';
|
|
13
|
+
import { envOr } from './env.js';
|
|
14
|
+
import * as nodePath from 'node:path';
|
|
11
15
|
|
|
12
16
|
const execFileAsync = promisify(execFile);
|
|
13
17
|
const WINDOWS_BROWSER_URL_ENV_VAR = 'LLXPRT_BROWSER_URL';
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Type for the options object passed to execFileAsync for browser launches.
|
|
21
|
+
* Node's ExecFileOptions does not declare `detached` or `stdio` (they live on
|
|
22
|
+
* SpawnOptions), but execFile forwards them to spawn at runtime. Declaring
|
|
23
|
+
* them explicitly here keeps TypeScript checking of the known keys while
|
|
24
|
+
* permitting the spawn-only fields we rely on.
|
|
25
|
+
*/
|
|
26
|
+
type BrowserExecOptions = ExecFileOptions & {
|
|
27
|
+
detached?: boolean;
|
|
28
|
+
stdio?: 'ignore' | 'pipe' | 'inherit';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared exec options for browser launches. Both the default-browser path
|
|
33
|
+
* and the specific-browser path use these so behavior stays consistent.
|
|
34
|
+
*/
|
|
35
|
+
function getBrowserExecOptions(): BrowserExecOptions {
|
|
36
|
+
const browserEnv = { ...env };
|
|
37
|
+
delete browserEnv.SHELL;
|
|
38
|
+
return {
|
|
39
|
+
env: browserEnv,
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: 'ignore',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Candidate Chrome/Chromium binary names on Linux/BSD, tried in order when
|
|
47
|
+
* the preferred name is not installed. Different distros ship the browser
|
|
48
|
+
* under different names (google-chrome, google-chrome-stable, chromium,
|
|
49
|
+
* chromium-browser).
|
|
50
|
+
*/
|
|
51
|
+
const LINUX_CHROME_BINARIES = [
|
|
52
|
+
'google-chrome',
|
|
53
|
+
'google-chrome-stable',
|
|
54
|
+
'chromium',
|
|
55
|
+
'chromium-browser',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Candidate Firefox binary names on Linux/BSD (firefox, firefox-esr on
|
|
60
|
+
* Debian/Ubuntu).
|
|
61
|
+
*/
|
|
62
|
+
const LINUX_FIREFOX_BINARIES = ['firefox', 'firefox-esr'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Typical Windows install locations for Chrome (relative to the system drive
|
|
66
|
+
* root, Program Files, and LOCALAPPDATA). Probed when a bare `chrome.exe` is
|
|
67
|
+
* not on PATH.
|
|
68
|
+
*/
|
|
69
|
+
const WINDOWS_CHROME_PATHS = [
|
|
70
|
+
() =>
|
|
71
|
+
nodePath.join(
|
|
72
|
+
envOr(env.PROGRAMFILES, 'C:\\Program Files'),
|
|
73
|
+
'Google',
|
|
74
|
+
'Chrome',
|
|
75
|
+
'Application',
|
|
76
|
+
'chrome.exe',
|
|
77
|
+
),
|
|
78
|
+
() =>
|
|
79
|
+
nodePath.join(
|
|
80
|
+
envOr(env['PROGRAMFILES(X86)'], 'C:\\Program Files (x86)'),
|
|
81
|
+
'Google',
|
|
82
|
+
'Chrome',
|
|
83
|
+
'Application',
|
|
84
|
+
'chrome.exe',
|
|
85
|
+
),
|
|
86
|
+
() =>
|
|
87
|
+
nodePath.join(
|
|
88
|
+
envOr(
|
|
89
|
+
env.LOCALAPPDATA,
|
|
90
|
+
nodePath.join(
|
|
91
|
+
envOr(env.USERPROFILE, 'C:\\Users\\Public'),
|
|
92
|
+
'AppData',
|
|
93
|
+
'Local',
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
'Google',
|
|
97
|
+
'Chrome',
|
|
98
|
+
'Application',
|
|
99
|
+
'chrome.exe',
|
|
100
|
+
),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Typical Windows install locations for Firefox, probed when a bare
|
|
105
|
+
* `firefox.exe` is not on PATH.
|
|
106
|
+
*/
|
|
107
|
+
const WINDOWS_FIREFOX_PATHS = [
|
|
108
|
+
() =>
|
|
109
|
+
nodePath.join(
|
|
110
|
+
envOr(env.PROGRAMFILES, 'C:\\Program Files'),
|
|
111
|
+
'Mozilla Firefox',
|
|
112
|
+
'firefox.exe',
|
|
113
|
+
),
|
|
114
|
+
() =>
|
|
115
|
+
nodePath.join(
|
|
116
|
+
envOr(env['PROGRAMFILES(X86)'] ?? undefined, 'C:\\Program Files (x86)'),
|
|
117
|
+
'Mozilla Firefox',
|
|
118
|
+
'firefox.exe',
|
|
119
|
+
),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Supported browser kinds for targeted launching.
|
|
124
|
+
*/
|
|
125
|
+
export type BrowserKind = 'chrome' | 'firefox' | 'safari';
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether the platform is Linux or a BSD variant. Centralized so the
|
|
129
|
+
* default-browser path and the specific-browser fallback path agree on which
|
|
130
|
+
* platforms count as "Linux-like".
|
|
131
|
+
*/
|
|
132
|
+
function isLinuxLike(platformName: NodeJS.Platform): boolean {
|
|
133
|
+
return (
|
|
134
|
+
platformName === 'linux' ||
|
|
135
|
+
platformName === 'freebsd' ||
|
|
136
|
+
platformName === 'openbsd'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Options for launching a specific browser binary with an optional profile.
|
|
142
|
+
* Chrome accepts a profile directory name. Firefox accepts either a profile
|
|
143
|
+
* name or an absolute profile path; names use `-P` and paths use `-profile`.
|
|
144
|
+
*/
|
|
145
|
+
export interface BrowserLaunchOptions {
|
|
146
|
+
browser?: BrowserKind;
|
|
147
|
+
profileDirectory?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
15
150
|
/**
|
|
16
151
|
* Validates that a URL is safe to open in a browser.
|
|
17
152
|
* Only allows HTTP and HTTPS URLs to prevent command injection.
|
|
@@ -95,6 +230,95 @@ function createBrowserLaunchPlan(
|
|
|
95
230
|
}
|
|
96
231
|
}
|
|
97
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Characters that must never appear in a profile directory name because they
|
|
235
|
+
* enable path traversal, act as path separators, or are shell/argument
|
|
236
|
+
* metacharacters. Everything else (including Unicode display names with
|
|
237
|
+
* parentheses, ampersands, or CJK characters) is permitted, because the
|
|
238
|
+
* launcher passes the value as a distinct argv element to execFile (no
|
|
239
|
+
* shell), so only traversal/separator characters are a real injection
|
|
240
|
+
* vector.
|
|
241
|
+
*/
|
|
242
|
+
const PROFILE_DIRECTORY_FORBIDDEN =
|
|
243
|
+
// \p{Cc} matches Unicode control characters (incl. NUL, newline, tab);
|
|
244
|
+
// backslash and forward slash are path separators; a bare or dotted ".."
|
|
245
|
+
// enables traversal. Using \p{Cc} (rather than a literal \x00-\x1f range)
|
|
246
|
+
// keeps the pattern free of embedded control characters.
|
|
247
|
+
/[\\/]|(?:^|[/\\])\.\.(?:[/\\]|$)|\p{Cc}/u;
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Validates a profile directory name against a denylist of dangerous
|
|
251
|
+
* characters.
|
|
252
|
+
*
|
|
253
|
+
* Security: This guard prevents path traversal and command injection by
|
|
254
|
+
* rejecting path separators (/, \), NUL/control characters, and traversal
|
|
255
|
+
* sequences (..). Because the launcher uses execFile (no shell) and passes
|
|
256
|
+
* the profile name as a discrete argv element, shell metacharacters are not
|
|
257
|
+
* an injection vector, so legitimate display names containing parentheses,
|
|
258
|
+
* ampersands, or Unicode characters are accepted.
|
|
259
|
+
*
|
|
260
|
+
* @param dir The profile directory name to validate
|
|
261
|
+
* @throws Error if the name contains disallowed characters
|
|
262
|
+
*/
|
|
263
|
+
export function validateProfileDirectory(dir: string): void {
|
|
264
|
+
if (
|
|
265
|
+
!dir ||
|
|
266
|
+
typeof dir !== 'string' ||
|
|
267
|
+
PROFILE_DIRECTORY_FORBIDDEN.test(dir)
|
|
268
|
+
) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Invalid profile directory: "${dir}". Path separators, control characters, and traversal sequences (..) are not allowed.`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const FIREFOX_PROFILE_PATH_FORBIDDEN = /\p{Cc}/u;
|
|
276
|
+
|
|
277
|
+
function isFirefoxProfilePath(
|
|
278
|
+
browser: BrowserKind,
|
|
279
|
+
profileDirectory: unknown,
|
|
280
|
+
): profileDirectory is string {
|
|
281
|
+
return (
|
|
282
|
+
browser === 'firefox' &&
|
|
283
|
+
typeof profileDirectory === 'string' &&
|
|
284
|
+
nodePath.isAbsolute(profileDirectory)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateBrowserProfileSelection(
|
|
289
|
+
browser: BrowserKind,
|
|
290
|
+
profileDirectory: unknown,
|
|
291
|
+
): void {
|
|
292
|
+
if (isFirefoxProfilePath(browser, profileDirectory)) {
|
|
293
|
+
if (FIREFOX_PROFILE_PATH_FORBIDDEN.test(profileDirectory)) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Invalid Firefox profile path: "${profileDirectory}". Control characters are not allowed.`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (typeof profileDirectory !== 'string') {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Invalid profile directory: "${String(
|
|
303
|
+
profileDirectory,
|
|
304
|
+
)}". Profile directory must be a string.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
validateProfileDirectory(profileDirectory);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function firefoxProfileArguments(
|
|
311
|
+
profileDirectory: string | undefined,
|
|
312
|
+
): string[] {
|
|
313
|
+
if (!profileDirectory) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
return [
|
|
317
|
+
nodePath.isAbsolute(profileDirectory) ? '-profile' : '-P',
|
|
318
|
+
profileDirectory,
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
98
322
|
/**
|
|
99
323
|
* Opens a URL in the default browser using platform-specific commands.
|
|
100
324
|
* This implementation avoids shell injection vulnerabilities by:
|
|
@@ -102,12 +326,41 @@ function createBrowserLaunchPlan(
|
|
|
102
326
|
* 2. Using execFile instead of exec to avoid shell interpretation
|
|
103
327
|
* 3. Keeping URL data out of PowerShell command source
|
|
104
328
|
*
|
|
329
|
+
* When `options.browser` is set, launches that specific browser binary
|
|
330
|
+
* directly (instead of the OS default browser), optionally with a specific
|
|
331
|
+
* profile directory. This ensures OAuth always uses the intended browser
|
|
332
|
+
* profile for the given bucket.
|
|
333
|
+
*
|
|
105
334
|
* @param url The URL to open
|
|
335
|
+
* @param options Optional browser and profile directory selection
|
|
106
336
|
* @throws Error if the URL is invalid or if opening the browser fails
|
|
107
337
|
*/
|
|
108
|
-
export async function openBrowserSecurely(
|
|
338
|
+
export async function openBrowserSecurely(
|
|
339
|
+
url: string,
|
|
340
|
+
options?: BrowserLaunchOptions,
|
|
341
|
+
): Promise<void> {
|
|
109
342
|
validateUrl(url);
|
|
110
343
|
|
|
344
|
+
if (options?.browser) {
|
|
345
|
+
if (options.profileDirectory !== undefined) {
|
|
346
|
+
validateBrowserProfileSelection(
|
|
347
|
+
options.browser,
|
|
348
|
+
options.profileDirectory,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
await openSpecificBrowser(url, options.browser, options.profileDirectory);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await openDefaultBrowser(url);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Open the OS-default browser for the given URL. This preserves the original
|
|
360
|
+
* platform-specific behaviour (including the Linux fallback chain) used when
|
|
361
|
+
* no particular browser/profile is requested.
|
|
362
|
+
*/
|
|
363
|
+
async function openDefaultBrowser(url: string): Promise<void> {
|
|
111
364
|
const platformName = platform();
|
|
112
365
|
const launchPlan = createBrowserLaunchPlan(platformName, url);
|
|
113
366
|
|
|
@@ -131,7 +384,6 @@ export async function openBrowserSecurely(url: string): Promise<void> {
|
|
|
131
384
|
'chromium',
|
|
132
385
|
'google-chrome',
|
|
133
386
|
];
|
|
134
|
-
|
|
135
387
|
const succeeded = await tryFallbackBrowserCommands(
|
|
136
388
|
fallbackCommands,
|
|
137
389
|
url,
|
|
@@ -216,3 +468,249 @@ async function tryFallbackBrowserCommands(
|
|
|
216
468
|
}
|
|
217
469
|
return false;
|
|
218
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Launch a specific browser binary with an optional profile directory.
|
|
474
|
+
*
|
|
475
|
+
* Uses execFile (no shell) throughout to prevent command injection.
|
|
476
|
+
* Profile directory names are validated by the caller via
|
|
477
|
+
* {@link validateProfileDirectory} before reaching this function.
|
|
478
|
+
*/
|
|
479
|
+
async function openSpecificBrowser(
|
|
480
|
+
url: string,
|
|
481
|
+
browser: BrowserKind,
|
|
482
|
+
profileDirectory: string | undefined,
|
|
483
|
+
): Promise<void> {
|
|
484
|
+
const platformName = platform();
|
|
485
|
+
let command: string;
|
|
486
|
+
let args: string[];
|
|
487
|
+
|
|
488
|
+
switch (browser) {
|
|
489
|
+
case 'chrome':
|
|
490
|
+
[command, args] = buildChromeLaunchArgs(
|
|
491
|
+
platformName,
|
|
492
|
+
profileDirectory,
|
|
493
|
+
url,
|
|
494
|
+
);
|
|
495
|
+
break;
|
|
496
|
+
|
|
497
|
+
case 'firefox':
|
|
498
|
+
[command, args] = buildFirefoxLaunchArgs(
|
|
499
|
+
platformName,
|
|
500
|
+
profileDirectory,
|
|
501
|
+
url,
|
|
502
|
+
);
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case 'safari':
|
|
506
|
+
if (platformName !== 'darwin') {
|
|
507
|
+
throw new Error('Safari is only available on macOS');
|
|
508
|
+
}
|
|
509
|
+
// Safari has no profile concept; launch the app with the URL
|
|
510
|
+
command = 'open';
|
|
511
|
+
args = ['-a', 'Safari', url];
|
|
512
|
+
break;
|
|
513
|
+
|
|
514
|
+
default:
|
|
515
|
+
throw new Error(`Unsupported browser: ${browser}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
await execFileAsync(command, args, getBrowserExecOptions());
|
|
520
|
+
} catch (error) {
|
|
521
|
+
const succeeded =
|
|
522
|
+
(await tryLinuxFallbackBinaries(platformName, browser, command, args)) ||
|
|
523
|
+
(await tryWindowsFallbackBinaries(platformName, browser, command, args));
|
|
524
|
+
if (succeeded) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
throw new Error(
|
|
528
|
+
`Failed to open ${browser}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
529
|
+
{ cause: error },
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* On Linux/BSD, the preferred Chrome/Firefox binary name varies by distro
|
|
536
|
+
* (google-chrome vs. chromium, firefox vs. firefox-esr). This helper tries
|
|
537
|
+
* the known fallback binaries for the given browser and resolves true if one
|
|
538
|
+
* succeeded (so the caller can suppress the original error).
|
|
539
|
+
*
|
|
540
|
+
* Resolves false when the platform is not Linux/BSD, the browser has no
|
|
541
|
+
* fallback list, or every fallback binary failed.
|
|
542
|
+
*/
|
|
543
|
+
async function tryLinuxFallbackBinaries(
|
|
544
|
+
platformName: NodeJS.Platform,
|
|
545
|
+
browser: BrowserKind,
|
|
546
|
+
primaryCommand: string,
|
|
547
|
+
args: string[],
|
|
548
|
+
): Promise<boolean> {
|
|
549
|
+
if (!isLinuxLike(platformName)) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
const fallbacks = linuxBrowserFallbacks(browser);
|
|
553
|
+
const tried = new Set<string>([primaryCommand]);
|
|
554
|
+
for (const fallback of fallbacks) {
|
|
555
|
+
if (tried.has(fallback)) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
tried.add(fallback);
|
|
559
|
+
try {
|
|
560
|
+
await execFileAsync(fallback, args, getBrowserExecOptions());
|
|
561
|
+
return true;
|
|
562
|
+
} catch {
|
|
563
|
+
// Try the next fallback binary.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Resolve the candidate fallback binary names for a browser on Linux/BSD.
|
|
571
|
+
* Returns an empty array for browsers without known alternates (e.g. Safari).
|
|
572
|
+
*/
|
|
573
|
+
function linuxBrowserFallbacks(browser: BrowserKind): string[] {
|
|
574
|
+
if (browser === 'chrome') {
|
|
575
|
+
return LINUX_CHROME_BINARIES;
|
|
576
|
+
}
|
|
577
|
+
if (browser === 'firefox') {
|
|
578
|
+
return LINUX_FIREFOX_BINARIES;
|
|
579
|
+
}
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Resolve the candidate fallback paths for a browser on Windows. Returns an
|
|
585
|
+
* empty array for browsers without known install locations (e.g. Safari).
|
|
586
|
+
*/
|
|
587
|
+
function windowsBrowserFallbackPaths(
|
|
588
|
+
browser: BrowserKind,
|
|
589
|
+
): Array<() => string> {
|
|
590
|
+
if (browser === 'chrome') {
|
|
591
|
+
return WINDOWS_CHROME_PATHS;
|
|
592
|
+
}
|
|
593
|
+
if (browser === 'firefox') {
|
|
594
|
+
return WINDOWS_FIREFOX_PATHS;
|
|
595
|
+
}
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* On Windows, a bare `chrome.exe`/`firefox.exe` is only on PATH if the user
|
|
601
|
+
* opted in during install. When it is missing (ENOENT), probe the typical
|
|
602
|
+
* install locations under Program Files and LOCALAPPDATA and launch the first
|
|
603
|
+
* one that exists. Resolves false when the platform is not Windows, the
|
|
604
|
+
* browser has no fallback list, or every candidate failed.
|
|
605
|
+
*/
|
|
606
|
+
async function tryWindowsFallbackBinaries(
|
|
607
|
+
platformName: NodeJS.Platform,
|
|
608
|
+
browser: BrowserKind,
|
|
609
|
+
primaryCommand: string,
|
|
610
|
+
args: string[],
|
|
611
|
+
): Promise<boolean> {
|
|
612
|
+
if (platformName !== 'win32') {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
const candidates = windowsBrowserFallbackPaths(browser);
|
|
616
|
+
const tried = new Set<string>([primaryCommand]);
|
|
617
|
+
for (const candidate of candidates) {
|
|
618
|
+
const resolved = candidate();
|
|
619
|
+
// Only attempt to execute real files so a directory at a candidate path is
|
|
620
|
+
// not mistaken for a browser binary (execFileAsync would emit a confusing
|
|
621
|
+
// EACCES/UNKNOWN error).
|
|
622
|
+
const isExecutableFile = await stat(resolved)
|
|
623
|
+
.then((candidateStat) => candidateStat.isFile())
|
|
624
|
+
.catch(() => false);
|
|
625
|
+
if (tried.has(resolved) || !isExecutableFile) {
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
tried.add(resolved);
|
|
629
|
+
try {
|
|
630
|
+
await execFileAsync(resolved, args, getBrowserExecOptions());
|
|
631
|
+
return true;
|
|
632
|
+
} catch {
|
|
633
|
+
// Try the next candidate path.
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Build the command and arguments to launch Chrome/Chromium with a profile.
|
|
641
|
+
*/
|
|
642
|
+
function buildChromeLaunchArgs(
|
|
643
|
+
platformName: NodeJS.Platform,
|
|
644
|
+
profileDirectory: string | undefined,
|
|
645
|
+
url: string,
|
|
646
|
+
): [string, string[]] {
|
|
647
|
+
switch (platformName) {
|
|
648
|
+
case 'darwin': {
|
|
649
|
+
const args = ['-a', 'Google Chrome'];
|
|
650
|
+
if (profileDirectory !== undefined) {
|
|
651
|
+
args.push('--args', `--profile-directory=${profileDirectory}`);
|
|
652
|
+
}
|
|
653
|
+
args.push(url);
|
|
654
|
+
return ['open', args];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
case 'win32': {
|
|
658
|
+
// Invoke the Chrome executable directly via execFileAsync — no shell,
|
|
659
|
+
// no PowerShell -Command string. Each argument is passed as a distinct
|
|
660
|
+
// argv element so spaces in the profile directory are preserved and
|
|
661
|
+
// there is no command-string boundary to inject through.
|
|
662
|
+
const args: string[] = [];
|
|
663
|
+
if (profileDirectory !== undefined) {
|
|
664
|
+
args.push(`--profile-directory=${profileDirectory}`);
|
|
665
|
+
}
|
|
666
|
+
args.push(url);
|
|
667
|
+
return ['chrome.exe', args];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
case 'linux':
|
|
671
|
+
case 'freebsd':
|
|
672
|
+
case 'openbsd': {
|
|
673
|
+
const args: string[] = [];
|
|
674
|
+
if (profileDirectory !== undefined) {
|
|
675
|
+
args.push(`--profile-directory=${profileDirectory}`);
|
|
676
|
+
}
|
|
677
|
+
args.push(url);
|
|
678
|
+
return ['google-chrome', args];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
default:
|
|
682
|
+
throw new Error(`Unsupported platform for Chrome: ${platformName}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Build the command and arguments to launch Firefox with a profile.
|
|
688
|
+
*/
|
|
689
|
+
function buildFirefoxLaunchArgs(
|
|
690
|
+
platformName: NodeJS.Platform,
|
|
691
|
+
profileDirectory: string | undefined,
|
|
692
|
+
url: string,
|
|
693
|
+
): [string, string[]] {
|
|
694
|
+
const profileArguments = firefoxProfileArguments(profileDirectory);
|
|
695
|
+
switch (platformName) {
|
|
696
|
+
case 'darwin': {
|
|
697
|
+
const args =
|
|
698
|
+
profileArguments.length > 0
|
|
699
|
+
? ['-n', '-a', 'Firefox', '--args', ...profileArguments]
|
|
700
|
+
: ['-a', 'Firefox'];
|
|
701
|
+
args.push(url);
|
|
702
|
+
return ['open', args];
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
case 'win32':
|
|
706
|
+
return ['firefox.exe', [...profileArguments, url]];
|
|
707
|
+
|
|
708
|
+
case 'linux':
|
|
709
|
+
case 'freebsd':
|
|
710
|
+
case 'openbsd':
|
|
711
|
+
return ['firefox', [...profileArguments, url]];
|
|
712
|
+
|
|
713
|
+
default:
|
|
714
|
+
throw new Error(`Unsupported platform for Firefox: ${platformName}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
@@ -19,6 +19,15 @@ export interface AnsiToken {
|
|
|
19
19
|
export type AnsiLine = AnsiToken[];
|
|
20
20
|
export type AnsiOutput = AnsiLine[];
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Explicit tagged update protocol for live-output streaming. Discriminated
|
|
24
|
+
* by `mode`: text stream producers emit `append` (incremental deltas);
|
|
25
|
+
* terminal-buffer producers emit `replace` (full snapshots).
|
|
26
|
+
*/
|
|
27
|
+
export type LiveOutputUpdate =
|
|
28
|
+
| { mode: 'append'; data: string }
|
|
29
|
+
| { mode: 'replace'; data: AnsiOutput };
|
|
30
|
+
|
|
22
31
|
const enum Attribute {
|
|
23
32
|
inverse = 1,
|
|
24
33
|
bold = 2,
|