diff --git a/src/completion-shared.ts b/src/completion-shared.ts index d9d4dc59..46cc1254 100644 --- a/src/completion-shared.ts +++ b/src/completion-shared.ts @@ -38,7 +38,7 @@ const HOSTED_CLIENT_ROOT_COMMANDS: readonly RootHelpCommand[] = [ { name: 'plugin', description: 'Manage Webcmd plugins' }, { name: 'profile', description: 'Manage hosted browser profiles' }, { name: 'session', description: 'Create, list, and close browser Sessions' }, - { name: 'setup', description: 'Configure local or hosted mode' }, + { name: 'setup', description: 'Configure local browser mode' }, { name: 'site', description: 'Read and write per-site memory: notes, endpoints, field maps, fixtures' }, { name: 'skills', description: 'Manage bundled Webcmd skills on this computer' }, { name: 'update', description: 'Update the installed Webcmd CLI on this computer' }, diff --git a/src/hosted/setup.test.ts b/src/hosted/setup.test.ts index b6a5bca2..961b3b5e 100644 --- a/src/hosted/setup.test.ts +++ b/src/hosted/setup.test.ts @@ -6,7 +6,6 @@ import { Writable } from 'node:stream'; import { fileURLToPath } from 'node:url'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { getConfigPath, makeLocalConfig, saveWebcmdConfig } from './config.js'; -import { getHostedCredentialPath } from './credentials.js'; import { runHostedSetup } from './setup.js'; import type { SlabSetupStatus } from '../slab/status.js'; @@ -21,7 +20,7 @@ afterEach(async () => { describe('webcmd setup', () => { it('writes local mode from interactive answer', async () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-')); - const answers = ['local', 'cloak']; + const answers = ['cloak']; const messages: string[] = []; const env = { WEBCMD_CONFIG_DIR: tempDir } as NodeJS.ProcessEnv; @@ -46,7 +45,7 @@ describe('webcmd setup', () => { it('shows installed Chrome in the interactive browser prompt and reuses its detection', async () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-interactive-chrome-')); const env = { WEBCMD_CONFIG_DIR: tempDir } as NodeJS.ProcessEnv; - const answers = ['local', 'chrome']; + const answers = ['chrome']; const prompts: string[] = []; const executablePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; const resolveGoogleChromeExecutable = vi.fn(async () => executablePath); @@ -73,7 +72,7 @@ describe('webcmd setup', () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-interactive-chrome-missing-')); const env = { WEBCMD_CONFIG_DIR: tempDir } as NodeJS.ProcessEnv; saveWebcmdConfig(makeLocalConfig(new Date('2026-08-31T00:00:00.000Z')), { env }); - const answers = ['local', 'chrome']; + const answers = ['chrome']; const prompts: string[] = []; const messages: string[] = []; const resolveGoogleChromeExecutable = vi.fn(async () => undefined); @@ -95,56 +94,7 @@ describe('webcmd setup', () => { expect(JSON.parse(await readFile(getConfigPath({ env }), 'utf8'))).toMatchObject({ browser: { kind: 'cloak' } }); }); - it('writes hosted mode and validates with /v1/me', async () => { - tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-')); - const answers = ['hosted', 'wcmd_live_test']; - const env = { - WEBCMD_CONFIG_DIR: tempDir, - WEBCMD_CREDENTIAL_BACKEND: 'file', - } as NodeJS.ProcessEnv; - const requests: Array<{ url: string; authorization: string | null }> = []; - const prompts: string[] = []; - const messages: string[] = []; - - const code = await runHostedSetup({ - env, - platform: 'linux', - now: () => new Date('2026-07-08T00:00:00.000Z'), - question: async (prompt) => { - prompts.push(prompt); - return answers.shift() ?? ''; - }, - fetchImpl: async (url, init) => { - requests.push({ - url: String(url), - authorization: new Headers(init?.headers).get('authorization'), - }); - return new Response(JSON.stringify({ ok: true, user: { id: 'user_demo' } }), { status: 200 }); - }, - write: (message) => { messages.push(message); }, - }); - - expect(code).toBe(0); - expect(prompts).toEqual([ - 'Use hosted Webcmd Cloud or local Webcmd? [hosted/local] ', - 'Webcmd API key: ', - ]); - expect(requests).toEqual([{ url: 'https://api.webcmd.dev/v1/me', authorization: 'Bearer wcmd_live_test' }]); - expect(JSON.parse(await readFile(getConfigPath({ env }), 'utf8'))).toMatchObject({ - mode: 'hosted', - hosted: { - apiBaseUrl: 'https://api.webcmd.dev', - apiKeyRef: expect.stringMatching(/^wcmd_cred_/), - credentialBackend: 'file-fallback', - }, - }); - expect(await readFile(getConfigPath({ env }), 'utf8')).not.toContain('wcmd_live_test'); - expect(await readFile(getHostedCredentialPath({ env }), 'utf8')).toContain('wcmd_live_test'); - expect(messages.join('')).toContain('Verified Webcmd Cloud account: user_demo'); - expect(messages.join('')).toContain('Credential backend: protected file fallback.'); - }); - - it('writes local mode from --mode without prompting', async () => { + it('writes local mode without prompting', async () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-flags-')); const messages: string[] = []; const env = { WEBCMD_CONFIG_DIR: tempDir } as NodeJS.ProcessEnv; @@ -154,7 +104,7 @@ describe('webcmd setup', () => { env, platform: 'linux', now: () => new Date('2026-07-08T00:00:00.000Z'), - argv: ['--mode', 'local'], + argv: [], isTTY: false, question, fetchDaemonStatus: async () => null, @@ -178,7 +128,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local', '--browser', 'cloak'], + argv: ['--browser', 'cloak'], isTTY: false, now: () => new Date('2026-08-31T00:00:00.000Z'), resolveCloakPackage: async () => { events.push('validate'); return 'file:///cloakbrowser/index.js'; }, @@ -203,7 +153,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local', '--browser', '/Applications/Chrome.app/Contents/MacOS/Google Chrome'], + argv: ['--browser', '/Applications/Chrome.app/Contents/MacOS/Google Chrome'], isTTY: false, realpath: async () => '/private/Applications/Chrome.app/Contents/MacOS/Google Chrome', stat: async () => ({ isFile: () => true }), @@ -225,7 +175,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local', '--browser', 'chrome'], + argv: ['--browser', 'chrome'], isTTY: false, resolveGoogleChromeExecutable: async () => executablePath, fetchDaemonStatus: async () => null, @@ -244,7 +194,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome'], + argv: ['--browser', 'chrome'], isTTY: false, resolveGoogleChromeExecutable: async () => undefined, fetchDaemonStatus: async () => null, @@ -264,7 +214,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local', '--browser', 'chrome', '--import-chrome-cookies'], + argv: ['--browser', 'chrome', '--import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => executablePath, listChromeCookieSources: () => [ @@ -298,7 +248,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome', '--no-import-chrome-cookies'], + argv: ['--browser', 'chrome', '--no-import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => executablePath, listChromeCookieSources, @@ -319,7 +269,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome', '--import-chrome-cookies'], + argv: ['--browser', 'chrome', '--import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => executablePath, listChromeCookieSources: () => [], @@ -335,7 +285,7 @@ describe('webcmd setup', () => { it('prompts to import Chrome cookies interactively and honors a "no" answer', async () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-chrome-cookies-prompt-')); const env = { WEBCMD_CONFIG_DIR: tempDir } as NodeJS.ProcessEnv; - const answers = ['local', 'chrome', 'n']; + const answers = ['chrome', 'n']; const prompts: string[] = []; const importChromeCookies = vi.fn(); @@ -369,7 +319,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome', '--chrome-profile', 'Work', '--import-chrome-cookies'], + argv: ['--browser', 'chrome', '--chrome-profile', 'Work', '--import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', listChromeCookieSources, @@ -391,7 +341,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome', '--chrome-profile', 'Profile 2', '--import-chrome-cookies'], + argv: ['--browser', 'chrome', '--chrome-profile', 'Profile 2', '--import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', listChromeCookieSources: () => [ @@ -417,7 +367,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome', '--import-chrome-cookies'], + argv: ['--browser', 'chrome', '--import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', listChromeCookieSources: () => [ @@ -445,7 +395,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'chrome', '--import-chrome-cookies'], + argv: ['--browser', 'chrome', '--import-chrome-cookies'], isTTY: false, resolveGoogleChromeExecutable: async () => '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', listChromeCookieSources: () => [ @@ -466,7 +416,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'cloak', '--chrome-profile', 'Work'], + argv: ['--browser', 'cloak', '--chrome-profile', 'Work'], isTTY: false, fetchDaemonStatus: async () => null, write: message => { messages.push(message); }, @@ -483,7 +433,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'darwin', homeDir: '/Users/me', @@ -508,7 +458,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'darwin', existsSync: () => false, @@ -528,7 +478,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'darwin', existsSync: () => false, @@ -549,7 +499,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'darwin', existsSync: () => false, @@ -573,7 +523,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'darwin', existsSync: () => false, @@ -593,7 +543,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'darwin', existsSync: () => false, @@ -611,7 +561,7 @@ describe('webcmd setup', () => { const restartDaemon = vi.fn(); await expect(runHostedSetup({ - argv: ['--mode', 'local'], + argv: [], isTTY: false, resolveCloakPackage: async () => 'file:///cloakbrowser/index.js', fetchDaemonStatus: async () => daemonStatus('cloak'), @@ -627,7 +577,7 @@ describe('webcmd setup', () => { const restartDaemon = vi.fn(async () => ({ previousStatus: daemonStatus('cloak'), status: daemonStatus('custom'), stopped: true, spawned: true })); await expect(runHostedSetup({ - argv: ['--mode', 'local', '--browser', '/custom/browser'], + argv: ['--browser', '/custom/browser'], isTTY: false, realpath: async () => '/custom/browser', stat: async () => ({ isFile: () => true }), @@ -644,7 +594,7 @@ describe('webcmd setup', () => { const restartDaemon = vi.fn(); await expect(runHostedSetup({ - argv: ['--mode', 'local'], + argv: [], isTTY: false, resolveCloakPackage: async () => 'file:///cloakbrowser/index.js', fetchDaemonStatus: async () => null, @@ -661,7 +611,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'local'], + argv: [], isTTY: false, resolveCloakPackage: async () => 'file:///cloakbrowser/index.js', fetchDaemonStatus: async () => daemonStatus('custom'), @@ -680,7 +630,7 @@ describe('webcmd setup', () => { await expect(runHostedSetup({ env, - argv: ['--mode', 'local'], + argv: [], isTTY: false, resolveCloakPackage: async () => 'file:///cloakbrowser/index.js', fetchDaemonStatus: async () => daemonStatus('custom'), @@ -696,7 +646,7 @@ describe('webcmd setup', () => { const installSlabMacos = vi.fn(); await expect(runHostedSetup({ - argv: ['--mode', 'local', '--browser', 'slab'], + argv: ['--browser', 'slab'], isTTY: false, platform: 'linux', installSlabMacos, @@ -707,9 +657,8 @@ describe('webcmd setup', () => { }); it.each([ - [['--mode', 'local', '--browser'], '--browser requires a value.'], - [['--mode', 'local', '--browser', 'relative/browser'], '--browser must be cloak, chrome, slab, or an absolute path'], - [['--mode', 'hosted', '--browser', 'slab', '--api-key', 'wcmd_live_test'], '--browser is only valid with --mode local.'], + [['--browser'], '--browser requires a value.'], + [['--browser', 'relative/browser'], '--browser must be cloak, chrome, slab, or an absolute path'], ])('rejects invalid browser arguments from %j', async (argv, message) => { const stderr = collectStderr(); @@ -732,10 +681,14 @@ describe('webcmd setup', () => { write: message => { messages.push(message); }, })).resolves.toBe(0); - expect(messages.join('')).toContain('--browser '); - expect(messages.join('')).toContain('Cloak is default, Chrome reuses an installed Google Chrome'); - expect(messages.join('')).toContain('Import only this Chrome profile'); - expect(messages.join('')).toContain('import all Chrome profiles without prompting'); + const help = messages.join(''); + expect(help).toContain('Configure local browser mode.'); + expect(help).toContain('--browser '); + expect(help).toContain('Cloak is default, Chrome reuses an installed Google Chrome'); + expect(help).toContain('Import only this Chrome profile'); + expect(help).toContain('import all Chrome profiles without prompting'); + expect(help).not.toContain('--mode'); + expect(help).not.toContain('--api-key'); }); it('reports the configured custom browser without probing SLAB', async () => { @@ -779,44 +732,9 @@ describe('webcmd setup', () => { expect(installSlabMacos).not.toHaveBeenCalled(); }); - it('rejects non-TTY setup without --mode and never prompts', async () => { - tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-nontty-')); - const messages: string[] = []; - const stderr = collectStderr(); - - const code = await runHostedSetup({ - env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: [], - isTTY: false, - stderr: stderr.stream, - write: (message) => { messages.push(message); }, - }); - - expect(code).toBe(2); - expect(messages.join('')).toBe(''); - expect(stderr.text()).toContain('setup requires --mode when stdin is not a TTY.'); - expect(stderr.text()).toContain('example: webcmd setup --mode local'); - }); - - it('rejects non-TTY hosted setup without --api-key', async () => { - tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-hosted-key-')); - const stderr = collectStderr(); - - const code = await runHostedSetup({ - env: { WEBCMD_CONFIG_DIR: tempDir }, - argv: ['--mode', 'hosted'], - isTTY: false, - stderr: stderr.stream, - write: () => undefined, - }); - - expect(code).toBe(2); - expect(stderr.text()).toContain('setup --mode hosted requires --api-key'); - }); - it('persists flag-driven local setup before the real CLI process completes', async () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-process-')); - const child = spawn(process.execPath, ['--import', 'tsx', 'src/main.ts', 'setup', '--mode', 'local'], { + const child = spawn(process.execPath, ['--import', 'tsx', 'src/main.ts', 'setup'], { cwd: packageRoot, env: { ...process.env, WEBCMD_CONFIG_DIR: tempDir, WEBCMD_NO_UPDATE_CHECK: '1' }, stdio: ['ignore', 'pipe', 'pipe'], @@ -845,7 +763,9 @@ describe('webcmd setup', () => { env: { ...process.env, WEBCMD_CONFIG_DIR: tempDir, WEBCMD_NO_UPDATE_CHECK: '1' }, stdio: ['ignore', 'pipe', 'pipe'], }); + const stdout: Buffer[] = []; const stderr: Buffer[] = []; + child.stdout.on('data', chunk => stdout.push(Buffer.from(chunk))); child.stderr.on('data', chunk => stderr.push(Buffer.from(chunk))); const status = await new Promise((resolve, reject) => { @@ -863,15 +783,16 @@ describe('webcmd setup', () => { }); }); - expect(status).toBe(2); - expect(Buffer.concat(stderr).toString('utf8')).toContain('setup requires --mode when stdin is not a TTY.'); + expect(status).toBe(0); + expect(Buffer.concat(stderr).toString('utf8')).toBe(''); + expect(Buffer.concat(stdout).toString('utf8')).toContain('Webcmd is now configured for local mode.'); }, 12_000); it('does not resolve until all caller-owned output writes complete', async () => { tempDir = await mkdtemp(join(tmpdir(), 'webcmd-setup-slow-output-')); const output = new SetupControlledWritable(); let settled = false; - const answers = ['local', 'cloak']; + const answers = ['cloak']; const run = runHostedSetup({ env: { WEBCMD_CONFIG_DIR: tempDir }, diff --git a/src/hosted/setup.ts b/src/hosted/setup.ts index 861f6bd2..f7339a6f 100644 --- a/src/hosted/setup.ts +++ b/src/hosted/setup.ts @@ -21,9 +21,7 @@ import { createSlabInstallerIo, installSlabMacos, verifySlabApp } from '../slab/ import { findSlabInstallation, type SlabInstallation } from '../slab/installation.js'; import { createSlabLaunchIo } from '../slab/launch.js'; import { inspectSlabStatus, slabStatusHasHello, type SlabSetupStatus } from '../slab/status.js'; -import { HostedClient } from './client.js'; import { - defaultHostedApiBaseUrl, getConfigPath, loadWebcmdConfig, makeLocalConfig, @@ -32,18 +30,12 @@ import { type LocalBrowserConfig, type WebcmdConfig, } from './config.js'; -import { - makeStoredHostedConfig, - storeHostedApiKey, - type HostedCredentialBackend, - type HostedCredentialIo, -} from './credentials.js'; -export interface SetupIo extends ConfigIo, HostedCredentialIo { +export interface SetupIo extends ConfigIo { input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream; stderr?: NodeJS.WritableStream; - fetchImpl?: typeof fetch; + platform?: NodeJS.Platform; question?: (prompt: string) => Promise; write?: (message: string) => void | Promise; argv?: readonly string[]; @@ -68,24 +60,21 @@ export interface SetupIo extends ConfigIo, HostedCredentialIo { type SetupMode = 'local' | 'hosted'; type LocalBrowserSelection = LocalBrowserConfig | { kind: 'chrome' }; -const SETUP_USAGE = `usage: ${CLI_COMMAND} setup --mode [--browser ] [--chrome-profile ] [--import-chrome-cookies|--no-import-chrome-cookies] [--api-key ]`; -const SETUP_EXAMPLE = `example: ${CLI_COMMAND} setup --mode local`; +const SETUP_USAGE = `usage: ${CLI_COMMAND} setup [--browser ] [--chrome-profile ] [--import-chrome-cookies|--no-import-chrome-cookies]`; +const SETUP_EXAMPLE = `example: ${CLI_COMMAND} setup`; const SETUP_HELP = [ `${CLI_COMMAND} setup`, '', - 'Configure local or hosted mode.', + 'Configure local browser mode.', '', - ' --mode Required when stdin is not a TTY', ' --browser Local browser; Cloak is default, Chrome reuses an installed Google Chrome, SLAB is macOS alpha', ' --chrome-profile Import only this Chrome profile (folder or display name)', ' --import-chrome-cookies With --browser chrome, import all Chrome profiles without prompting', ' --no-import-chrome-cookies With --browser chrome, skip cookie import without prompting', - ' --api-key Required for --mode hosted when stdin is not a TTY', ' --status Show the configured mode and local browser', ' -h, --help', '', SETUP_EXAMPLE, - `example: ${CLI_COMMAND} setup --mode hosted --api-key `, '', ].join('\n'); @@ -114,116 +103,37 @@ export async function runHostedSetup(io: SetupIo = {}): Promise { } const interactive = canPrompt(io); - let mode = parsed.mode; - if (!mode) { - if (!interactive) { - throw new ArgumentError( - 'setup requires --mode when stdin is not a TTY.', - `${SETUP_USAGE}\n${SETUP_EXAMPLE}`, - ); - } - await write('Webcmd setup\n'); - mode = (await ask('Use hosted Webcmd Cloud or local Webcmd? [hosted/local] ')).trim().toLowerCase().startsWith('l') - ? 'local' - : 'hosted'; - } else { - await write('Webcmd setup\n'); - } + await write('Webcmd setup\n'); - if (mode === 'local') { - if (parsed.apiKey) { - throw new ArgumentError( - '--api-key is only valid with --mode hosted.', - `${SETUP_USAGE}\n${SETUP_EXAMPLE}`, - ); - } - if ((parsed.chromeProfile || parsed.importChromeCookies !== undefined) && parsed.browser && parsed.browser.kind !== 'chrome') { - throw new ArgumentError( - '--chrome-profile and --import-chrome-cookies are only valid with --browser chrome.', - `${SETUP_USAGE}\n${SETUP_EXAMPLE}`, - ); - } - let chromeDiscovery: { executablePath: string | undefined } | undefined; - let browser = parsed.browser; - if (!browser && interactive) { - chromeDiscovery = { executablePath: await resolveGoogleChromeExecutable(io) }; - const chromeStatus = chromeDiscovery.executablePath ? 'installed' : 'install required'; - browser = parseLocalBrowser( - (await ask(`Local browser [cloak/chrome (${chromeStatus})/slab/absolute path] (cloak): `)).trim() || 'cloak', - ); - } - browser ??= { kind: 'cloak' }; - const before = await (io.fetchDaemonStatus ?? fetchDaemonStatus)(); - try { - const selected = await validateLocalBrowser(browser, io, chromeDiscovery); - if (selected.kind === 'chrome') await maybeImportChromeCookies(parsed, io, interactive, ask, write); - (io.saveConfig ?? saveWebcmdConfig)(makeLocalConfig(io.now?.() ?? new Date(), selected), io); - if (before) await restartConfiguredDaemon(selected, io); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - await write(`Local browser setup failed: ${message}\n`); - if (err instanceof DaemonRestartError) await write('Run `webcmd daemon restart` to apply the selected browser.\n'); - return 1; - } - await write('Webcmd is now configured for local mode.\n'); - return 0; - } - - if (parsed.browser) { - throw new ArgumentError( - '--browser is only valid with --mode local.', - `${SETUP_USAGE}\n${SETUP_EXAMPLE}`, - ); - } - if (parsed.chromeProfile || parsed.importChromeCookies !== undefined) { + if ((parsed.chromeProfile || parsed.importChromeCookies !== undefined) && parsed.browser && parsed.browser.kind !== 'chrome') { throw new ArgumentError( '--chrome-profile and --import-chrome-cookies are only valid with --browser chrome.', `${SETUP_USAGE}\n${SETUP_EXAMPLE}`, ); } - - let apiKey = parsed.apiKey?.trim(); - if (!apiKey) { - if (!interactive) { - throw new ArgumentError( - 'setup --mode hosted requires --api-key when stdin is not a TTY.', - `${SETUP_USAGE}\nexample: ${CLI_COMMAND} setup --mode hosted --api-key `, - ); - } - apiKey = (await ask('Webcmd API key: ')).trim(); - if (!apiKey) { - await write('A Webcmd API key is required for hosted mode.\n'); - return 2; - } + let chromeDiscovery: { executablePath: string | undefined } | undefined; + let browser = parsed.browser; + if (!browser && interactive) { + chromeDiscovery = { executablePath: await resolveGoogleChromeExecutable(io) }; + const chromeStatus = chromeDiscovery.executablePath ? 'installed' : 'install required'; + browser = parseLocalBrowser( + (await ask(`Local browser [cloak/chrome (${chromeStatus})/slab/absolute path] (cloak): `)).trim() || 'cloak', + ); } - - const apiBaseUrl = defaultHostedApiBaseUrl(io.env ?? process.env); - let accountLabel: string | undefined; + browser ??= { kind: 'cloak' }; + const before = await (io.fetchDaemonStatus ?? fetchDaemonStatus)(); try { - const me = await new HostedClient({ - apiBaseUrl, - apiKey, - fetchImpl: io.fetchImpl, - }).getMe(); - accountLabel = hostedAccountLabel(me); + const selected = await validateLocalBrowser(browser, io, chromeDiscovery); + if (selected.kind === 'chrome') await maybeImportChromeCookies(parsed, io, interactive, ask, write); + (io.saveConfig ?? saveWebcmdConfig)(makeLocalConfig(io.now?.() ?? new Date(), selected), io); + if (before) await restartConfiguredDaemon(selected, io); } catch (err) { const message = err instanceof Error ? err.message : String(err); - await write(`Warning: could not verify API key yet: ${message}\n`); - } - const credential = await storeHostedApiKey(apiKey, io); - const config = makeStoredHostedConfig({ - apiBaseUrl, - apiKeyRef: credential.apiKeyRef, - credentialBackend: credential.credentialBackend, - now: io.now?.() ?? new Date(), - }); - saveWebcmdConfig(config, io); - if (accountLabel) await write(`Verified Webcmd Cloud account: ${accountLabel}\n`); - if (credential.credentialBackend === 'file-fallback') { - await write('Warning: OS credential storage was unavailable; API key stored in a protected Webcmd credentials file.\n'); + await write(`Local browser setup failed: ${message}\n`); + if (err instanceof DaemonRestartError) await write('Run `webcmd daemon restart` to apply the selected browser.\n'); + return 1; } - await write(`Credential backend: ${credentialBackendLabel(credential.credentialBackend)}.\n`); - await write('Webcmd is now configured for hosted mode.\n'); + await write('Webcmd is now configured for local mode.\n'); return 0; } catch (err) { if (err instanceof ArgumentError) { @@ -265,7 +175,7 @@ async function validateLocalBrowser( : await resolveGoogleChromeExecutable(io); if (!executablePath) { throw new Error( - `Google Chrome is not installed. Install it from https://www.google.com/chrome/, then rerun ${CLI_COMMAND} setup --mode local --browser chrome.`, + `Google Chrome is not installed. Install it from https://www.google.com/chrome/, then rerun ${CLI_COMMAND} setup --browser chrome.`, ); } return { kind: 'chrome', executablePath }; @@ -440,15 +350,11 @@ export async function getSetupStatus(io: SetupIo = {}): Promise { function parseSetupArgs(argv: readonly string[]): { help?: true; status?: true; - mode?: SetupMode; browser?: LocalBrowserSelection; - apiKey?: string; chromeProfile?: string; importChromeCookies?: boolean; } { - let mode: SetupMode | undefined; let browser: LocalBrowserSelection | undefined; - let apiKey: string | undefined; let status: true | undefined; let chromeProfile: string | undefined; let importChromeCookies: boolean | undefined; @@ -475,30 +381,6 @@ function parseSetupArgs(argv: readonly string[]): { chromeProfile = value; continue; } - if (token === '--mode' || token.startsWith('--mode=')) { - const value = token.startsWith('--mode=') ? token.slice('--mode='.length) : argv[++i]; - if (value !== 'local' && value !== 'hosted') { - throw new ArgumentError( - `--mode must be one of: local, hosted${value ? ` (got: "${value}")` : ''}.`, - `${SETUP_USAGE}\n${SETUP_EXAMPLE}`, - ); - } - mode = value; - continue; - } - - if (token === '--api-key' || token.startsWith('--api-key=')) { - const value = token.startsWith('--api-key=') ? token.slice('--api-key='.length) : argv[++i]; - if (!value || value.startsWith('-')) { - throw new ArgumentError( - '--api-key requires a value.', - `${SETUP_USAGE}\nexample: ${CLI_COMMAND} setup --mode hosted --api-key `, - ); - } - apiKey = value; - continue; - } - if (token === '--browser' || token.startsWith('--browser=')) { const value = token.startsWith('--browser=') ? token.slice('--browser='.length) : argv[++i]; browser = parseLocalBrowser(value); @@ -507,10 +389,10 @@ function parseSetupArgs(argv: readonly string[]): { throw new ArgumentError( `unknown flag ${token} for \`setup\``, - `valid flags for \`setup\`: --mode, --browser, --chrome-profile, --import-chrome-cookies, --no-import-chrome-cookies, --api-key, --status, --help\n${SETUP_USAGE}`, + `valid flags for \`setup\`: --browser, --chrome-profile, --import-chrome-cookies, --no-import-chrome-cookies, --status, --help\n${SETUP_USAGE}`, ); } - return { ...(status ? { status } : {}), mode, browser, apiKey, chromeProfile, importChromeCookies }; + return { ...(status ? { status } : {}), browser, chromeProfile, importChromeCookies }; } function parseLocalBrowser(value: string | undefined): LocalBrowserSelection { @@ -527,16 +409,4 @@ function parseLocalBrowser(value: string | undefined): LocalBrowserSelection { ); } -function hostedAccountLabel(body: unknown): string | undefined { - if (!body || typeof body !== 'object' || Array.isArray(body)) return undefined; - const user = (body as { user?: unknown }).user; - if (!user || typeof user !== 'object' || Array.isArray(user)) return undefined; - const record = user as { email?: unknown; id?: unknown }; - if (typeof record.email === 'string' && record.email.trim()) return record.email.trim(); - if (typeof record.id === 'string' && record.id.trim()) return record.id.trim(); - return undefined; -} -function credentialBackendLabel(backend: HostedCredentialBackend): string { - return backend === 'os' ? 'OS credential store' : 'protected file fallback'; -}