mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix: keep browser setup config-only
This commit is contained in:
@@ -53,7 +53,6 @@ ccs help browser
|
||||
ccs browser setup
|
||||
ccs browser status
|
||||
ccs browser doctor
|
||||
ccs browser doctor --fix
|
||||
```
|
||||
|
||||
Use `ccs browser setup` for the primary one-command setup path. Use `ccs browser status` for
|
||||
@@ -124,21 +123,9 @@ That flow:
|
||||
1. enables Claude Browser Attach in the saved CCS browser config
|
||||
2. keeps the configured DevTools port normalized
|
||||
3. creates the configured browser user-data directory if needed
|
||||
4. tries to start a managed Chrome/Chromium attach session
|
||||
4. prints the exact browser launch command for the current platform
|
||||
5. re-checks readiness and reports the next step if Chrome still needs manual attention
|
||||
|
||||
If you want the same remediation flow from the diagnostic command, use:
|
||||
|
||||
```bash
|
||||
ccs browser doctor --fix
|
||||
```
|
||||
|
||||
If you only want to save config and browser-directory state without launching Chrome:
|
||||
|
||||
```bash
|
||||
ccs browser setup --no-launch
|
||||
```
|
||||
|
||||
## Launching Chrome For Claude Attach
|
||||
|
||||
Claude Browser Attach needs a browser launched with remote debugging.
|
||||
|
||||
@@ -41,7 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic
|
||||
|
||||
### Recent Fixes
|
||||
|
||||
- **2026-04-19**: **#1049** Browser setup now has a real remediation path instead of status/doctor-only guidance. CCS adds `ccs browser setup` as the primary one-command flow for Claude Browser Attach, supports `ccs browser doctor --fix` as a repair alias, shortens managed browser-path output to home-relative display paths where appropriate, and updates browser readiness guidance to point users at setup first while keeping browser doctor read-only by default.
|
||||
- **2026-04-19**: **#1049** Browser setup now has a real remediation path instead of status/doctor-only guidance. CCS adds `ccs browser setup` as the primary one-command flow for Claude Browser Attach, shortens managed browser-path output to home-relative display paths where appropriate, and updates browser readiness guidance to point users at setup first while keeping browser doctor read-only by default.
|
||||
- **2026-04-18**: **#1038** Legacy OpenAI-compatible provider writes no longer self-destruct on the next `ccs cliproxy restart`. CCS now preserves AI-provider-managed top-level sections such as `openai-compatibility` during CLIProxy config regeneration, and the legacy `openai-compat` manager now rewrites only its own YAML section instead of dumping the whole file and stripping the generated version header. Regression coverage now proves the legacy helper keeps the generated header intact and that OpenAI-compatible connectors survive regeneration.
|
||||
- **2026-04-16**: **#1030** Browser automation is now a first-class CCS surface instead of an env-only/runtime-only feature. CCS adds `ccs help browser`, `ccs browser status`, and `ccs browser doctor`; a dedicated `Settings -> Browser` dashboard tab for Claude Browser Attach and Codex Browser Tools; a new `browser` section in `~/.ccs/config.yaml`; explicit readiness/next-step messaging for attach-mode Chrome sessions; and Codex UI guidance that marks the managed `ccs_browser` entry as CCS-owned and redirects browser setup away from the generic MCP editor.
|
||||
- **2026-04-15**: **#969** Local CLIProxy bootstrap no longer depends on live GitHub reachability during normal dashboard and runtime startup. CCS now skips hidden auto-update lookups on standard CLIProxy bootstrap paths, fails fast with explicit `ccs cliproxy install` guidance when a service start needs a binary that is not installed locally, and keeps `ccs config` able to open the dashboard in limited mode instead of stalling behind blocked release downloads.
|
||||
|
||||
@@ -24,7 +24,7 @@ function summarizeBrowserHealth(status: browserUtils.BrowserStatusPayload): {
|
||||
function writeCommandTable(writeLine: HelpWriter): void {
|
||||
writeLine(subheader('Commands'));
|
||||
writeLine(
|
||||
` ${color('ccs browser setup', 'command')} Configure Claude Browser Attach and try to start the managed browser session`
|
||||
` ${color('ccs browser setup', 'command')} Configure Claude Browser Attach and print the manual launch command`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs browser status', 'command')} Show Claude attach and Codex browser readiness`
|
||||
@@ -32,9 +32,6 @@ function writeCommandTable(writeLine: HelpWriter): void {
|
||||
writeLine(
|
||||
` ${color('ccs browser doctor', 'command')} Explain what is missing and how to fix it`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs browser doctor --fix', 'command')} ${dim('# Alias for browser setup')}`
|
||||
);
|
||||
writeLine('');
|
||||
}
|
||||
|
||||
@@ -105,10 +102,6 @@ function writeSetupSummary(
|
||||
writeLine(` Config updated: ${result.configUpdated ? 'yes' : 'no'}`);
|
||||
writeLine(` Created user-data dir: ${result.createdUserDataDir ? 'yes' : 'no'}`);
|
||||
writeLine(` Browser MCP ready: ${result.mcpReady ? 'yes' : 'no'}`);
|
||||
if (result.launchAttempted) {
|
||||
writeLine(` Browser launch: ${result.launchStarted ? 'started' : 'failed'}`);
|
||||
}
|
||||
writeLine(` Launch command: ${result.launchCommand}`);
|
||||
if (result.notes.length > 0) {
|
||||
for (const note of result.notes) {
|
||||
writeLine(` Note: ${note}`);
|
||||
@@ -133,17 +126,11 @@ export async function showBrowserHelp(writeLine: HelpWriter = console.log): Prom
|
||||
writeLine('');
|
||||
writeLine(subheader('Examples'));
|
||||
writeLine(
|
||||
` ${color('ccs browser setup', 'command')} ${dim('# Configure and start the managed browser session')}`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs browser setup --no-launch', 'command')} ${dim('# Save setup only, do not start Chrome')}`
|
||||
` ${color('ccs browser setup', 'command')} ${dim('# Configure browser attach and print the manual launch command')}`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs browser doctor', 'command')} ${dim('# Detailed troubleshooting output')}`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs browser doctor --fix', 'command')} ${dim('# Run the setup flow from doctor')}`
|
||||
);
|
||||
writeLine(
|
||||
` ${color('ccs config', 'command')} ${dim('# Open Settings > Browser in the dashboard')}`
|
||||
);
|
||||
@@ -151,7 +138,7 @@ export async function showBrowserHelp(writeLine: HelpWriter = console.log): Prom
|
||||
}
|
||||
|
||||
function isHelpRequest(args: string[]): boolean {
|
||||
return args.length === 0 || args.includes('--help') || args.includes('-h');
|
||||
return args.length === 0 || args[0] === 'help' || args.includes('--help') || args.includes('-h');
|
||||
}
|
||||
|
||||
export async function handleBrowserCommand(
|
||||
@@ -164,13 +151,20 @@ export async function handleBrowserCommand(
|
||||
}
|
||||
|
||||
const subcommand = args[0];
|
||||
if (subcommand === 'setup' || (subcommand === 'doctor' && args.includes('--fix'))) {
|
||||
await initUI();
|
||||
const result = await browserUtils.runBrowserSetup({
|
||||
launch: !args.includes('--no-launch'),
|
||||
});
|
||||
if (subcommand === 'setup') {
|
||||
if (args.includes('--no-launch')) {
|
||||
await initUI();
|
||||
writeLine(color('`ccs browser setup` no longer supports `--no-launch`.', 'error'));
|
||||
writeLine(` ${dim('Setup is config-only and already prints the manual launch command.')}`);
|
||||
writeLine('');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const label = subcommand === 'setup' ? 'ccs browser setup' : 'ccs browser doctor --fix';
|
||||
await initUI();
|
||||
const result = await browserUtils.runBrowserSetup();
|
||||
|
||||
const label = 'ccs browser setup';
|
||||
writeLine(header(label));
|
||||
writeLine('');
|
||||
writeIntro(writeLine);
|
||||
@@ -181,6 +175,15 @@ export async function handleBrowserCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'doctor' && (args.includes('--fix') || args.includes('-f'))) {
|
||||
await initUI();
|
||||
writeLine(color('`ccs browser doctor` is read-only.', 'error'));
|
||||
writeLine(` ${dim('Run `ccs browser setup` for the browser remediation flow.')}`);
|
||||
writeLine('');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand !== 'status' && subcommand !== 'doctor') {
|
||||
await initUI();
|
||||
writeLine(color(`Unknown browser subcommand: ${subcommand}`, 'error'));
|
||||
|
||||
@@ -320,7 +320,7 @@ export const COMMAND_FLAG_SUGGESTIONS: Readonly<Record<string, readonly string[]
|
||||
config: ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'],
|
||||
cursor: ['--help', '-h'],
|
||||
doctor: ['--fix', '-f', '--help', '-h'],
|
||||
browser: ['setup', 'status', 'doctor', '--fix', '-f', '--no-launch', '--help', '-h'],
|
||||
browser: ['setup', 'status', 'doctor', '--help', '-h'],
|
||||
docker: ['--help', '-h', '--host'],
|
||||
env: ['--format', '--shell', '--ide', '--help', '-h'],
|
||||
migrate: MIGRATE_FLAGS,
|
||||
|
||||
@@ -192,11 +192,8 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
||||
if (!subcommand || subcommand.startsWith('-')) {
|
||||
return completeSubcommands(['setup', 'status', 'doctor'], ['--help', '-h']);
|
||||
}
|
||||
if (subcommand === 'setup') {
|
||||
return completeSubcommands([], ['--no-launch', '--help', '-h']);
|
||||
}
|
||||
if (subcommand === 'doctor') {
|
||||
return completeSubcommands([], ['--fix', '-f', '--help', '-h', '--no-launch']);
|
||||
if (subcommand === 'setup' || subcommand === 'doctor') {
|
||||
return completeSubcommands([], ['--help', '-h']);
|
||||
}
|
||||
return completeSubcommands([], ['--help', '-h']);
|
||||
case 'cursor':
|
||||
|
||||
@@ -1,34 +1,22 @@
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import { getBrowserConfig, mutateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import type { BrowserConfig } from '../../config/unified-config-types';
|
||||
import { getCcsPathDisplay } from '../config-manager';
|
||||
import { getNodePlatformKey } from './platform';
|
||||
import { type BrowserStatusPayload, getBrowserStatus } from './browser-status';
|
||||
import { type BrowserRuntimeEnv, resolveBrowserRuntimeEnv } from './chrome-reuse';
|
||||
import { ensureBrowserMcp } from './mcp-installer';
|
||||
import {
|
||||
buildBrowserLaunchCommands,
|
||||
ensureManagedBrowserUserDataDir,
|
||||
getEffectiveClaudeBrowserAttachConfig,
|
||||
getRecommendedBrowserUserDataDir,
|
||||
isManagedClaudeBrowserAttachConfig,
|
||||
type EffectiveClaudeBrowserAttachConfig,
|
||||
} from './browser-settings';
|
||||
|
||||
export interface BrowserSetupOptions {
|
||||
launch?: boolean;
|
||||
}
|
||||
|
||||
export interface BrowserSetupResult {
|
||||
configUpdated: boolean;
|
||||
createdUserDataDir: boolean;
|
||||
launchAttempted: boolean;
|
||||
launchStarted: boolean;
|
||||
launchCommand: string;
|
||||
launchError?: string;
|
||||
mcpReady: boolean;
|
||||
overrideActive: boolean;
|
||||
ready: boolean;
|
||||
runtimeEnv?: BrowserRuntimeEnv;
|
||||
launchCommand: string;
|
||||
status: BrowserStatusPayload;
|
||||
notes: string[];
|
||||
}
|
||||
@@ -37,39 +25,33 @@ export interface BrowserSetupDeps {
|
||||
getBrowserConfig: typeof getBrowserConfig;
|
||||
mutateUnifiedConfig: typeof mutateUnifiedConfig;
|
||||
ensureBrowserMcp: typeof ensureBrowserMcp;
|
||||
resolveBrowserRuntimeEnv: typeof resolveBrowserRuntimeEnv;
|
||||
getBrowserStatus: typeof getBrowserStatus;
|
||||
launchBrowserSession: (
|
||||
config: EffectiveClaudeBrowserAttachConfig
|
||||
) => Promise<{ launchCommand: string; started: boolean; error?: string }>;
|
||||
sleep: (ms: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const defaultBrowserSetupDeps: BrowserSetupDeps = {
|
||||
getBrowserConfig,
|
||||
mutateUnifiedConfig,
|
||||
ensureBrowserMcp,
|
||||
resolveBrowserRuntimeEnv,
|
||||
getBrowserStatus,
|
||||
launchBrowserSession,
|
||||
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
};
|
||||
|
||||
export async function runBrowserSetup(
|
||||
options: BrowserSetupOptions = {},
|
||||
deps: BrowserSetupDeps = defaultBrowserSetupDeps
|
||||
): Promise<BrowserSetupResult> {
|
||||
const initialConfig = deps.getBrowserConfig();
|
||||
const configUpdated = persistBrowserSetupConfig(deps, initialConfig);
|
||||
const persistedConfig = deps.getBrowserConfig();
|
||||
const effectiveConfig = getEffectiveClaudeBrowserAttachConfig(persistedConfig);
|
||||
const createdUserDataDir = ensureBrowserUserDataDir(effectiveConfig.userDataDir);
|
||||
const createdUserDataDir = isManagedClaudeBrowserAttachConfig(effectiveConfig)
|
||||
? ensureManagedBrowserUserDataDir(effectiveConfig).createdProfileDir
|
||||
: false;
|
||||
const mcpReady = deps.ensureBrowserMcp();
|
||||
const status = await deps.getBrowserStatus();
|
||||
const notes: string[] = [];
|
||||
|
||||
if (effectiveConfig.overrideActive) {
|
||||
notes.push(
|
||||
`Current session is using ${effectiveConfig.source}; saved config may still be shadowed until that override is removed.`
|
||||
`Current session is using ${effectiveConfig.source}; remove that override if you want saved browser settings to take effect in future shells.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,38 +59,20 @@ export async function runBrowserSetup(
|
||||
notes.push('CCS could not fully prepare the local browser MCP runtime.');
|
||||
}
|
||||
|
||||
let runtimeEnv = await tryResolveBrowserRuntime(effectiveConfig, deps);
|
||||
let launchAttempted = false;
|
||||
let launchStarted = false;
|
||||
let launchError: string | undefined;
|
||||
const { launchCommand } = getPreferredLaunchInfo(effectiveConfig);
|
||||
|
||||
if (!runtimeEnv && options.launch !== false) {
|
||||
launchAttempted = true;
|
||||
const launchResult = await deps.launchBrowserSession(effectiveConfig);
|
||||
launchStarted = launchResult.started;
|
||||
launchError = launchResult.error;
|
||||
runtimeEnv = launchStarted
|
||||
? await waitForBrowserRuntime(effectiveConfig, deps)
|
||||
: await tryResolveBrowserRuntime(effectiveConfig, deps);
|
||||
|
||||
if (launchError) {
|
||||
notes.push(launchError);
|
||||
}
|
||||
if (!isManagedClaudeBrowserAttachConfig(effectiveConfig)) {
|
||||
notes.push(
|
||||
'Setup did not create the current browser user-data dir because it is not the CCS-managed default path.'
|
||||
);
|
||||
}
|
||||
|
||||
const status = await deps.getBrowserStatus();
|
||||
const platform = getNodePlatformKey();
|
||||
return {
|
||||
configUpdated,
|
||||
createdUserDataDir,
|
||||
launchAttempted,
|
||||
launchStarted,
|
||||
launchCommand,
|
||||
launchError,
|
||||
mcpReady,
|
||||
overrideActive: effectiveConfig.overrideActive,
|
||||
ready: Boolean(runtimeEnv) && mcpReady,
|
||||
runtimeEnv,
|
||||
ready: status.claude.state === 'ready' && mcpReady,
|
||||
launchCommand: status.claude.launchCommands[platform],
|
||||
status,
|
||||
notes,
|
||||
};
|
||||
@@ -135,214 +99,3 @@ function persistBrowserSetupConfig(deps: BrowserSetupDeps, currentConfig: Browse
|
||||
|
||||
return JSON.stringify(deps.getBrowserConfig()) !== before;
|
||||
}
|
||||
|
||||
function ensureBrowserUserDataDir(userDataDir: string): boolean {
|
||||
try {
|
||||
fs.statSync(userDataDir);
|
||||
return false;
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code && code !== 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(userDataDir, { recursive: true, mode: 0o700 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryResolveBrowserRuntime(
|
||||
config: EffectiveClaudeBrowserAttachConfig,
|
||||
deps: BrowserSetupDeps
|
||||
): Promise<BrowserRuntimeEnv | undefined> {
|
||||
try {
|
||||
return await deps.resolveBrowserRuntimeEnv({
|
||||
profileDir: config.userDataDir,
|
||||
devtoolsPort: config.hasExplicitDevtoolsPort ? String(config.devtoolsPort) : undefined,
|
||||
});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForBrowserRuntime(
|
||||
config: EffectiveClaudeBrowserAttachConfig,
|
||||
deps: BrowserSetupDeps
|
||||
): Promise<BrowserRuntimeEnv | undefined> {
|
||||
const deadline = Date.now() + 10000;
|
||||
|
||||
while (Date.now() <= deadline) {
|
||||
const runtimeEnv = await tryResolveBrowserRuntime(config, deps);
|
||||
if (runtimeEnv) {
|
||||
return runtimeEnv;
|
||||
}
|
||||
|
||||
await deps.sleep(250);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getPreferredLaunchInfo(config: EffectiveClaudeBrowserAttachConfig): {
|
||||
launchCommand: string;
|
||||
} {
|
||||
const userDataDirDisplay = isManagedClaudeBrowserAttachConfig(config)
|
||||
? getCcsPathDisplay('browser', 'chrome-user-data')
|
||||
: config.userDataDir;
|
||||
const launchCommands = buildBrowserLaunchCommands(userDataDirDisplay, config.devtoolsPort);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return { launchCommand: launchCommands.win32 };
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return { launchCommand: launchCommands.darwin };
|
||||
}
|
||||
|
||||
return { launchCommand: launchCommands.linux };
|
||||
}
|
||||
|
||||
function getLaunchCandidates(config: EffectiveClaudeBrowserAttachConfig): Array<{
|
||||
command: string;
|
||||
args: string[];
|
||||
displayCommand: string;
|
||||
}> {
|
||||
const launchCommands = buildBrowserLaunchCommands(config.userDataDir, config.devtoolsPort);
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return [
|
||||
{
|
||||
command: 'open',
|
||||
args: [
|
||||
'-na',
|
||||
'Google Chrome',
|
||||
'--args',
|
||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
||||
`--user-data-dir=${config.userDataDir}`,
|
||||
],
|
||||
displayCommand: launchCommands.darwin,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return [
|
||||
{
|
||||
command: 'chrome.exe',
|
||||
args: [
|
||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
||||
`--user-data-dir=${config.userDataDir}`,
|
||||
],
|
||||
displayCommand: launchCommands.win32,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
command: 'google-chrome',
|
||||
args: [
|
||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
||||
`--user-data-dir=${config.userDataDir}`,
|
||||
],
|
||||
displayCommand: launchCommands.linux,
|
||||
},
|
||||
{
|
||||
command: 'google-chrome-stable',
|
||||
args: [
|
||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
||||
`--user-data-dir=${config.userDataDir}`,
|
||||
],
|
||||
displayCommand: launchCommands.linux.replace('google-chrome', 'google-chrome-stable'),
|
||||
},
|
||||
{
|
||||
command: 'chromium',
|
||||
args: [
|
||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
||||
`--user-data-dir=${config.userDataDir}`,
|
||||
],
|
||||
displayCommand: launchCommands.linux.replace('google-chrome', 'chromium'),
|
||||
},
|
||||
{
|
||||
command: 'chromium-browser',
|
||||
args: [
|
||||
`--remote-debugging-port=${config.devtoolsPort}`,
|
||||
`--user-data-dir=${config.userDataDir}`,
|
||||
],
|
||||
displayCommand: launchCommands.linux.replace('google-chrome', 'chromium-browser'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function launchBrowserSession(
|
||||
config: EffectiveClaudeBrowserAttachConfig
|
||||
): Promise<{ launchCommand: string; started: boolean; error?: string }> {
|
||||
let lastError: string | undefined;
|
||||
const candidates = getLaunchCandidates(config);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const result = await spawnDetached(candidate.command, candidate.args);
|
||||
if (result.started) {
|
||||
return {
|
||||
launchCommand: candidate.displayCommand,
|
||||
started: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.notFound) {
|
||||
return {
|
||||
launchCommand: candidate.displayCommand,
|
||||
started: false,
|
||||
error: `CCS could not start the browser automatically. ${result.error}`,
|
||||
};
|
||||
}
|
||||
|
||||
lastError = result.error;
|
||||
}
|
||||
|
||||
return {
|
||||
launchCommand: getPreferredLaunchInfo(config).launchCommand,
|
||||
started: false,
|
||||
error:
|
||||
lastError ??
|
||||
'CCS could not find a supported Chrome/Chromium executable to launch automatically.',
|
||||
};
|
||||
}
|
||||
|
||||
async function spawnDetached(
|
||||
command: string,
|
||||
args: string[]
|
||||
): Promise<{ started: boolean; notFound: boolean; error?: string }> {
|
||||
return await new Promise((resolve) => {
|
||||
try {
|
||||
const child = spawn(command, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
child.once('error', (error) => {
|
||||
const errno = error as NodeJS.ErrnoException;
|
||||
resolve({
|
||||
started: false,
|
||||
notFound: errno.code === 'ENOENT',
|
||||
error: errno.message,
|
||||
});
|
||||
});
|
||||
child.once('spawn', () => {
|
||||
child.unref();
|
||||
resolve({ started: true, notFound: false });
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
started: false,
|
||||
notFound: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,4 +51,4 @@ export type {
|
||||
} from './browser-status';
|
||||
|
||||
export { runBrowserSetup } from './browser-setup';
|
||||
export type { BrowserSetupOptions, BrowserSetupResult } from './browser-setup';
|
||||
export type { BrowserSetupResult } from './browser-setup';
|
||||
|
||||
@@ -187,19 +187,10 @@ describe('browser command', () => {
|
||||
const setupSpy = spyOn(browserUtils, 'runBrowserSetup').mockResolvedValue({
|
||||
configUpdated: true,
|
||||
createdUserDataDir: true,
|
||||
launchAttempted: true,
|
||||
launchStarted: true,
|
||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
||||
mcpReady: true,
|
||||
overrideActive: false,
|
||||
ready: true,
|
||||
runtimeEnv: {
|
||||
CCS_BROWSER_USER_DATA_DIR: '/tmp/browser-profile',
|
||||
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
||||
},
|
||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
||||
status: {
|
||||
claude: {
|
||||
enabled: true,
|
||||
@@ -248,67 +239,26 @@ describe('browser command', () => {
|
||||
expect(rendered.includes('ccs browser setup')).toBe(true);
|
||||
expect(rendered.includes('Result: ready')).toBe(true);
|
||||
expect(rendered.includes('Config updated: yes')).toBe(true);
|
||||
expect(rendered.includes('Browser launch: started')).toBe(true);
|
||||
expect(rendered.includes('Launch command: open -na "Google Chrome"')).toBe(true);
|
||||
expect(process.exitCode).toBe(0);
|
||||
} finally {
|
||||
setupSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
test('doctor --fix reuses the browser setup flow', async () => {
|
||||
const setupSpy = spyOn(browserUtils, 'runBrowserSetup').mockResolvedValue({
|
||||
configUpdated: false,
|
||||
createdUserDataDir: false,
|
||||
launchAttempted: false,
|
||||
launchStarted: false,
|
||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
||||
mcpReady: false,
|
||||
overrideActive: false,
|
||||
ready: false,
|
||||
status: {
|
||||
claude: {
|
||||
enabled: true,
|
||||
source: 'config',
|
||||
overrideActive: false,
|
||||
state: 'browser_not_running',
|
||||
title: 'Claude Browser Attach is not ready yet.',
|
||||
detail: 'No running attach-mode Chrome session is using the managed browser profile yet.',
|
||||
nextStep: 'Run `ccs browser setup` to configure and start the managed browser session.',
|
||||
effectiveUserDataDir: '/tmp/browser-profile',
|
||||
recommendedUserDataDir: '/tmp/browser-profile',
|
||||
devtoolsPort: 9222,
|
||||
managedMcpServerName: 'ccs-browser',
|
||||
managedMcpServerPath: '/tmp/ccs-browser-server.cjs',
|
||||
launchCommands: {
|
||||
darwin: 'open -na "Google Chrome" --args',
|
||||
linux: 'google-chrome --remote-debugging-port=9222',
|
||||
win32: 'chrome.exe --remote-debugging-port=9222',
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
state: 'enabled',
|
||||
title: 'Codex Browser Tools are enabled.',
|
||||
detail: 'CCS can inject the managed Playwright MCP overrides.',
|
||||
nextStep: 'Use a Codex-target launch.',
|
||||
serverName: 'ccs_browser',
|
||||
supportsConfigOverrides: true,
|
||||
binaryPath: '/usr/local/bin/codex',
|
||||
},
|
||||
},
|
||||
notes: ['CCS could not fully prepare the local browser MCP runtime.'],
|
||||
});
|
||||
test('doctor rejects --fix and points users to setup', async () => {
|
||||
const rendered = await renderLines(['doctor', '--fix']);
|
||||
|
||||
try {
|
||||
const rendered = await renderLines(['doctor', '--fix', '--no-launch']);
|
||||
expect(rendered.includes('`ccs browser doctor` is read-only.')).toBe(true);
|
||||
expect(rendered.includes('Run `ccs browser setup` for the browser remediation flow.')).toBe(
|
||||
true
|
||||
);
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
expect(rendered.includes('ccs browser doctor --fix')).toBe(true);
|
||||
expect(rendered.includes('Result: action required')).toBe(true);
|
||||
expect(rendered.includes('Note: CCS could not fully prepare the local browser MCP runtime.')).toBe(true);
|
||||
expect(process.exitCode).toBe(1);
|
||||
} finally {
|
||||
setupSpy.mockRestore();
|
||||
}
|
||||
test('literal browser help still renders the help page', async () => {
|
||||
const rendered = await renderLines(['help']);
|
||||
|
||||
expect(rendered.includes('CCS Browser Help')).toBe(true);
|
||||
expect(rendered.includes('ccs browser setup')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,11 +144,9 @@ describe('completion backend', () => {
|
||||
expect(suggestionValues(['browser'])).toEqual(
|
||||
expect.arrayContaining(['setup', 'status', 'doctor'])
|
||||
);
|
||||
expect(suggestionValues(['browser', 'setup'])).toEqual(
|
||||
expect.arrayContaining(['--no-launch', '--help', '-h'])
|
||||
);
|
||||
expect(suggestionValues(['browser', 'setup'])).toEqual(expect.arrayContaining(['--help', '-h']));
|
||||
expect(suggestionValues(['browser', 'doctor'])).toEqual(
|
||||
expect.arrayContaining(['--fix', '-f', '--no-launch'])
|
||||
expect.arrayContaining(['--help', '-h'])
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -104,9 +104,10 @@ describe('browser setup', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('enables Claude browser attach and returns ready without launching when runtime already resolves', async () => {
|
||||
it('enables Claude browser attach and prepares the managed default path', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
||||
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
||||
config.browser.claude.user_data_dir = '';
|
||||
|
||||
const deps: BrowserSetupDeps = {
|
||||
getBrowserConfig: () => config.browser,
|
||||
@@ -115,13 +116,6 @@ describe('browser setup', () => {
|
||||
return config;
|
||||
},
|
||||
ensureBrowserMcp: () => true,
|
||||
resolveBrowserRuntimeEnv: async () => ({
|
||||
CCS_BROWSER_USER_DATA_DIR: config.browser.claude.user_data_dir,
|
||||
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
||||
}),
|
||||
getBrowserStatus: async () =>
|
||||
({
|
||||
claude: {
|
||||
@@ -154,26 +148,21 @@ describe('browser setup', () => {
|
||||
binaryPath: '/usr/local/bin/codex',
|
||||
},
|
||||
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
||||
launchBrowserSession: async () => ({
|
||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
||||
started: false,
|
||||
}),
|
||||
sleep: async () => undefined,
|
||||
};
|
||||
|
||||
const result = await runBrowserSetup({ launch: false }, deps);
|
||||
const result = await runBrowserSetup(deps);
|
||||
|
||||
expect(result.configUpdated).toBe(true);
|
||||
expect(result.createdUserDataDir).toBe(true);
|
||||
expect(result.launchAttempted).toBe(false);
|
||||
expect(result.ready).toBe(true);
|
||||
expect(config.browser.claude.enabled).toBe(true);
|
||||
expect(config.browser.claude.user_data_dir).not.toBe('');
|
||||
});
|
||||
|
||||
it('launches the browser session when runtime is not ready initially', async () => {
|
||||
it('does not create a custom override path during setup', async () => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-setup-'));
|
||||
const config = createUnifiedConfig(join(tempDir, 'browser-profile'));
|
||||
let resolveCalls = 0;
|
||||
config.browser.claude.enabled = true;
|
||||
|
||||
const deps: BrowserSetupDeps = {
|
||||
getBrowserConfig: () => config.browser,
|
||||
@@ -182,30 +171,16 @@ describe('browser setup', () => {
|
||||
return config;
|
||||
},
|
||||
ensureBrowserMcp: () => true,
|
||||
resolveBrowserRuntimeEnv: async () => {
|
||||
resolveCalls += 1;
|
||||
if (resolveCalls < 2) {
|
||||
throw new Error('Chrome reuse metadata not found');
|
||||
}
|
||||
|
||||
return {
|
||||
CCS_BROWSER_USER_DATA_DIR: config.browser.claude.user_data_dir,
|
||||
CCS_BROWSER_DEVTOOLS_HOST: '127.0.0.1',
|
||||
CCS_BROWSER_DEVTOOLS_PORT: '9222',
|
||||
CCS_BROWSER_DEVTOOLS_HTTP_URL: 'http://127.0.0.1:9222',
|
||||
CCS_BROWSER_DEVTOOLS_WS_URL: 'ws://127.0.0.1/devtools/browser/test',
|
||||
};
|
||||
},
|
||||
getBrowserStatus: async () =>
|
||||
({
|
||||
claude: {
|
||||
enabled: true,
|
||||
source: 'config',
|
||||
overrideActive: false,
|
||||
state: 'ready',
|
||||
title: 'Claude Browser Attach is ready.',
|
||||
detail: 'ready',
|
||||
nextStep: 'Launch Claude.',
|
||||
source: 'CCS_BROWSER_USER_DATA_DIR',
|
||||
overrideActive: true,
|
||||
state: 'browser_not_running',
|
||||
title: 'Claude Browser Attach could not find a running browser session.',
|
||||
detail: 'override active',
|
||||
nextStep: 'Run `ccs browser setup` to configure and start the managed browser session.',
|
||||
effectiveUserDataDir: config.browser.claude.user_data_dir,
|
||||
recommendedUserDataDir: config.browser.claude.user_data_dir,
|
||||
devtoolsPort: 9222,
|
||||
@@ -228,18 +203,13 @@ describe('browser setup', () => {
|
||||
binaryPath: '/usr/local/bin/codex',
|
||||
},
|
||||
}) as Awaited<ReturnType<BrowserSetupDeps['getBrowserStatus']>>,
|
||||
launchBrowserSession: async () => ({
|
||||
launchCommand: 'open -na "Google Chrome" --args --remote-debugging-port=9222',
|
||||
started: true,
|
||||
}),
|
||||
sleep: async () => undefined,
|
||||
};
|
||||
|
||||
const result = await runBrowserSetup({}, deps);
|
||||
const result = await runBrowserSetup(deps);
|
||||
|
||||
expect(result.launchAttempted).toBe(true);
|
||||
expect(result.launchStarted).toBe(true);
|
||||
expect(result.ready).toBe(true);
|
||||
expect(resolveCalls).toBeGreaterThanOrEqual(2);
|
||||
expect(result.createdUserDataDir).toBe(false);
|
||||
expect(result.overrideActive).toBe(false);
|
||||
expect(result.ready).toBe(false);
|
||||
expect(result.notes[0]).toContain('did not create the current browser user-data dir');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user