From 6c0eba551dfbfd23d9d1d96fd1cfc17f35641f46 Mon Sep 17 00:00:00 2001 From: ROHAN <123131rkorohan@gmail.com> Date: Sun, 16 Aug 2026 21:18:06 +0530 Subject: [PATCH] feat(cli): add -f/--format to webcmd profile list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local `profile list` rejected `-f` entirely and only printed prose, while hosted `profile list` already supports `-f json|yaml|csv|md` (src/hosted/runner.ts) — the same command name behaving differently between modes with no indication why (#175). Add `-f, --format` (default `table`, prose output unchanged). Other formats render one row per profile through the shared output path. Row shape aligns with hosted's where the concept overlaps (`default`), plus locally-meaningful fields hosted doesn't have (`connected`, `runtimeVersion`) since Cloak profiles are live runtime connections, not persisted Cloud records. Disconnected saved aliases (already shown in the prose output) are included as `connected: false` rows so the structured view doesn't silently drop information the text view shows. The daemon-not-running/stale/no-profiles branches render `[]` for non-table formats rather than swallowing output, mirroring the existing `adapter status` empty-list convention. Scope note: same slice-of-#175 approach as the `validate` and `daemon status` PRs. Co-Authored-By: Claude Sonnet 5 --- src/cli.test.ts | 57 ++++++++++++++++++++++++ src/cli.ts | 116 +++++++++++++++++++++++++++++++----------------- 2 files changed, 133 insertions(+), 40 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index b4831fca..8ebe05df 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -17,6 +17,7 @@ import { } from './command-presentation.js'; import { parseOutputFormat } from './command-surface.js'; import { render as renderOutput } from './output.js'; +import { saveProfileConfig } from './browser/profile.js'; import * as pluginModule from './plugin.js'; import * as discoveryModule from './discovery.js'; @@ -1722,6 +1723,62 @@ describe('profile list', () => { expect(output).not.toContain(`Webcmd ${'extension'}`); expect(output).not.toContain('webcmd daemon restart'); }); + + it('-f json renders an empty array instead of prose when no profiles are active (#175)', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ + ok: true, + pid: 123, + uptime: 1, + daemonVersion: PKG_VERSION, + runtimeConnected: false, + runtimeName: 'Cloak', + profiles: [], + pending: 0, + memoryMB: 20, + port: 9777, + }), + } as Response); + const program = createProgram('', ''); + + await program.parseAsync(['node', 'webcmd', 'profile', 'list', '-f', 'json']); + + const output = stdoutSpy.mock.calls.flat().join('\n'); + expect(JSON.parse(output)).toEqual([]); + }); + + it('-f json renders connected and disconnected-saved profiles as structured rows (#175)', async () => { + saveProfileConfig({ + version: 1, + defaultContextId: 'work', + aliases: { work: 'work', archived: 'gone' }, + }); + vi.mocked(fetch).mockResolvedValue({ + ok: true, + json: async () => ({ + ok: true, + pid: 123, + uptime: 1, + daemonVersion: PKG_VERSION, + runtimeConnected: true, + runtimeName: 'Cloak', + profiles: [{ contextId: 'work', runtimeConnected: true, runtimeVersion: '1.2.3', pending: 0 }], + pending: 0, + memoryMB: 20, + port: 9777, + }), + } as Response); + const program = createProgram('', ''); + + await program.parseAsync(['node', 'webcmd', 'profile', 'list', '-f', 'json']); + + const output = stdoutSpy.mock.calls.flat().join('\n'); + expect(JSON.parse(output)).toEqual([ + { contextId: 'work', alias: 'work', default: true, connected: true, runtimeVersion: '1.2.3' }, + { contextId: 'gone', alias: 'archived', default: false, connected: false, runtimeVersion: null }, + ]); + }); }); describe('browser raw session commands', () => { diff --git a/src/cli.ts b/src/cli.ts index c7b2a09b..ae6e0568 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1775,54 +1775,90 @@ cli({ // Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing. const originalProfileDescription = profileCmd.description(); - profileCmd + const profileListCmd = profileCmd .command('list') .description('List Chrome and Chromium profiles available through the Cloak runtime') - .action(async () => { - const status = await fetchDaemonStatus(); - const config = loadProfileConfig(); - const profiles = status?.profiles ?? []; - if (!status) { - console.log('Daemon is not running. Run webcmd doctor after opening Chrome.'); - return; - } - if (isDaemonStale(status, PKG_VERSION) || !Array.isArray(status.profiles)) { - console.log(`Daemon ${formatDaemonVersion(status)} is stale for CLI v${PKG_VERSION}.`); - console.log('Run: webcmd daemon restart'); - return; + .option('-f, --format ', OUTPUT_FORMAT_HELP, 'table'); + profileListCmd.action(async (opts) => { + const fmt = resolveOutputFormat(opts.format); + if (fmt === null) return; + const status = await fetchDaemonStatus(); + const config = loadProfileConfig(); + const profiles = status?.profiles ?? []; + if (!status) { + if (fmt !== 'table') { renderOutput([], { fmt }); return; } + console.log('Daemon is not running. Run webcmd doctor after opening Chrome.'); + return; + } + if (isDaemonStale(status, PKG_VERSION) || !Array.isArray(status.profiles)) { + if (fmt !== 'table') { renderOutput([], { fmt }); return; } + console.log(`Daemon ${formatDaemonVersion(status)} is stale for CLI v${PKG_VERSION}.`); + console.log('Run: webcmd daemon restart'); + return; + } + if (profiles.length === 0) { + if (fmt !== 'table') { renderOutput([], { fmt }); return; } + console.log('No Cloak runtime profiles are active.'); + console.log('Run a browser-backed command or webcmd login to create one.'); + return; + } + + const knownContextIds = new Set(profiles.map((profile) => profile.contextId)); + const disconnectedAliases = Object.entries(config.aliases) + .filter(([, contextId]) => !knownContextIds.has(contextId)); + + if (fmt !== 'table') { + // Aligns with the hosted `profile list` row shape (`default`); local rows also + // carry `connected`/`runtimeVersion` since Cloak profiles are live runtime + // connections, not persisted Cloud records. + const rows = profiles.map((profile) => ({ + contextId: profile.contextId, + alias: aliasForContextId(config, profile.contextId) ?? null, + default: config.defaultContextId === profile.contextId, + connected: true, + runtimeVersion: profile.runtimeVersion ?? null, + })); + const shown = new Set(); + for (const [alias, contextId] of disconnectedAliases) { + shown.add(contextId); + rows.push({ + contextId, + alias, + default: config.defaultContextId === contextId, + connected: false, + runtimeVersion: null, + }); } - if (profiles.length === 0) { - console.log('No Cloak runtime profiles are active.'); - console.log('Run a browser-backed command or webcmd login to create one.'); - return; + if (config.defaultContextId && !shown.has(config.defaultContextId) && !knownContextIds.has(config.defaultContextId)) { + rows.push({ contextId: config.defaultContextId, alias: null, default: true, connected: false, runtimeVersion: null }); } + renderOutput(rows, { fmt }); + return; + } + + console.log('Available Cloak profiles'); + console.log(); + for (const profile of profiles) { + const alias = aliasForContextId(config, profile.contextId); + const defaultMark = config.defaultContextId === profile.contextId ? ' default' : ''; + const aliasText = alias ? ` ${alias}` : ''; + const version = profile.runtimeVersion ? ` v${profile.runtimeVersion}` : ' version unknown'; + console.log(` ${profile.contextId}${aliasText}${defaultMark} — connected${version}`); + } - const knownContextIds = new Set(profiles.map((profile) => profile.contextId)); - console.log('Available Cloak profiles'); + if (disconnectedAliases.length > 0 || (config.defaultContextId && !knownContextIds.has(config.defaultContextId))) { console.log(); - for (const profile of profiles) { - const alias = aliasForContextId(config, profile.contextId); - const defaultMark = config.defaultContextId === profile.contextId ? ' default' : ''; - const aliasText = alias ? ` ${alias}` : ''; - const version = profile.runtimeVersion ? ` v${profile.runtimeVersion}` : ' version unknown'; - console.log(` ${profile.contextId}${aliasText}${defaultMark} — connected${version}`); + console.log('Disconnected saved profiles:'); + const shown = new Set(); + for (const [alias, contextId] of disconnectedAliases) { + shown.add(contextId); + console.log(` ${contextId} ${alias} — not connected`); } - - const disconnectedAliases = Object.entries(config.aliases) - .filter(([, contextId]) => !knownContextIds.has(contextId)); - if (disconnectedAliases.length > 0 || (config.defaultContextId && !knownContextIds.has(config.defaultContextId))) { - console.log(); - console.log('Disconnected saved profiles:'); - const shown = new Set(); - for (const [alias, contextId] of disconnectedAliases) { - shown.add(contextId); - console.log(` ${contextId} ${alias} — not connected`); - } - if (config.defaultContextId && !shown.has(config.defaultContextId) && !knownContextIds.has(config.defaultContextId)) { - console.log(` ${config.defaultContextId} — default, not connected`); - } + if (config.defaultContextId && !shown.has(config.defaultContextId) && !knownContextIds.has(config.defaultContextId)) { + console.log(` ${config.defaultContextId} — default, not connected`); } - }); + } + }); profileCmd .command('rename')