Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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([]);
});
Comment on lines +1743 to +1749

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', () => {
Expand Down
116 changes: 76 additions & 40 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fmt>', 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 <site> 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.
Comment on lines +1811 to +1813
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<string>();
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 <site> 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<string>();
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<string>();
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')
Expand Down