Skip to content
Merged
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
81 changes: 76 additions & 5 deletions src/cli/dashboard/_shared/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { TRPCClientError } from '@trpc/client';
import chalk from 'chalk';
import { type DashboardClient, createDashboardClient } from './client.js';
import { type CliConfig, loadConfig } from './config.js';
import { printDetail, printTable } from './format.js';
import { printCompact, printCsv, printDetail, printTable } from './format.js';
import { withSpinner } from './spinner.js';

export type OutputFormat = 'table' | 'json' | 'csv' | 'compact';

export function extractBaseFlags(argv: string[]): { server?: string; org?: string } | undefined {
let server: string | undefined;
let org: string | undefined;
Expand All @@ -28,7 +30,18 @@ export function extractBaseFlags(argv: string[]): { server?: string; org?: strin

export abstract class DashboardCommand extends Command {
static override baseFlags = {
json: Flags.boolean({ description: 'Output as JSON', default: false }),
format: Flags.string({
description: 'Output format (table, json, csv, compact)',
options: ['table', 'json', 'csv', 'compact'],
default: 'table',
}),
json: Flags.boolean({
description: 'Output as JSON (alias for --format json)',
default: false,
}),
columns: Flags.string({
description: 'Comma-separated list of columns to display (e.g. --columns id,status,agent)',
}),
server: Flags.string({ description: 'Override server URL' }),
org: Flags.string({ description: 'Override organization context (admin/superadmin only)' }),
};
Expand Down Expand Up @@ -67,15 +80,24 @@ export abstract class DashboardCommand extends Command {
return extractBaseFlags(this.argv);
}

/**
* Resolve the effective output format. --json flag takes precedence as alias for json format.
*/
protected resolveFormat(flags: { format?: string; json?: boolean }): OutputFormat {
if (flags.json) return 'json';
return (flags.format as OutputFormat | undefined) ?? 'table';
}

protected outputJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
}

protected outputTable(
rows: Record<string, unknown>[],
columns: { key: string; header: string; format?: (v: unknown) => string }[],
emptyMessage?: string,
): void {
printTable(rows, columns);
printTable(rows, columns, emptyMessage);
}

protected outputDetail(
Expand All @@ -85,6 +107,50 @@ export abstract class DashboardCommand extends Command {
printDetail(obj, fields);
}

/**
* Filter columns based on the --columns flag value.
* Returns the original columns if no filter is specified.
*/
protected filterColumns<T extends { key: string }>(columns: T[], columnsFlag?: string): T[] {
if (!columnsFlag) return columns;
const keys = columnsFlag
.split(',')
.map((k) => k.trim())
.filter(Boolean);
if (keys.length === 0) return columns;
return columns.filter((col) => keys.includes(col.key));
}

/**
* Output rows in the format specified by the --format / --json flags.
* Handles column filtering via --columns flag automatically.
*/
protected outputFormatted(
rows: Record<string, unknown>[],
columns: { key: string; header: string; format?: (v: unknown) => string }[],
flags: { format?: string; json?: boolean; columns?: string },
data?: unknown,
emptyMessage?: string,
): void {
const fmt = this.resolveFormat(flags);
const filteredColumns = this.filterColumns(columns, flags.columns);

switch (fmt) {
case 'json':
this.outputJson(data ?? rows);
break;
case 'csv':
printCsv(rows, filteredColumns);
break;
case 'compact':
printCompact(rows, filteredColumns);
break;
default:
printTable(rows, filteredColumns, emptyMessage);
break;
}
}

/**
* Print a success message with a green ✓ prefix.
*/
Expand All @@ -104,9 +170,14 @@ export abstract class DashboardCommand extends Command {
* Automatically suppressed when --json flag is active, NO_COLOR=1, or CI=1.
*/
protected withSpinner<T>(message: string, fn: () => Promise<T>): Promise<T> {
// Suppress spinner when --json flag is present
// Suppress spinner when --json flag or non-table format is present
const isJson = this.argv.includes('--json');
return withSpinner(message, fn, { silent: isJson });
const hasFormat = this.argv.some((a) => a === '--format' || a.startsWith('--format='));
const formatVal =
this.argv.find((a) => a.startsWith('--format='))?.slice('--format='.length) ??
(hasFormat ? this.argv[this.argv.indexOf('--format') + 1] : undefined);
const silent = isJson || (formatVal !== undefined && formatVal !== 'table');
return withSpinner(message, fn, { silent });
}

protected handleError(err: unknown): never {
Expand Down
45 changes: 43 additions & 2 deletions src/cli/dashboard/_shared/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ interface Column {
format?: (value: unknown) => string;
}

export function printTable(rows: Record<string, unknown>[], columns: Column[]): void {
export function printTable(
rows: Record<string, unknown>[],
columns: Column[],
emptyMessage?: string,
): void {
if (rows.length === 0) {
console.log(' (no results)');
console.log(` ${emptyMessage ?? '(no results)'}`);
return;
}

Expand Down Expand Up @@ -47,6 +51,43 @@ export function printTable(rows: Record<string, unknown>[], columns: Column[]):
}
}

export function printCsv(rows: Record<string, unknown>[], columns: Column[]): void {
// Print header row
const headers = columns.map((col) => csvQuote(col.header));
console.log(headers.join(','));

// Print data rows
for (const row of columns.length === 0 ? [] : rows) {
const values = columns.map((col) => {
const raw = col.format ? col.format(row[col.key]) : String(row[col.key] ?? '');
// Strip ANSI escape codes for CSV output
const plain = raw.replace(ANSI_STRIP_RE, '');
return csvQuote(plain);
});
console.log(values.join(','));
}
}

function csvQuote(value: string): string {
// Quote the value if it contains commas, quotes, or newlines
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}

export function printCompact(rows: Record<string, unknown>[], columns: Column[]): void {
for (const row of rows) {
const parts = columns.map((col) => {
const raw = col.format ? col.format(row[col.key]) : String(row[col.key] ?? '');
// Strip ANSI escape codes for compact output
const plain = raw.replace(ANSI_STRIP_RE, '');
return `${col.key}=${plain}`;
});
console.log(parts.join(' '));
}
}

interface FieldMap {
label: string;
format?: (value: unknown) => string;
Expand Down
24 changes: 11 additions & 13 deletions src/cli/dashboard/agents/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,23 @@ export default class AgentsList extends DashboardCommand {
projectId: flags['project-id'],
});

if (flags.json) {
this.outputJson(configs);
return;
}

if (configs.length === 0) {
this.log('No agents enabled for this project. Use `cascade agents create` to enable one.');
return;
}

this.outputTable(configs as unknown as Record<string, unknown>[], [
const columns = [
{ key: 'id', header: 'ID' },
{ key: 'agentType', header: 'Agent Type' },
{ key: 'projectId', header: 'Project' },
{ key: 'model', header: 'Model' },
{ key: 'maxIterations', header: 'Max Iter' },
{ key: 'agentEngine', header: 'Engine' },
{ key: 'prompt', header: 'Prompt', format: (v) => (v ? 'custom' : '-') },
]);
{ key: 'prompt', header: 'Prompt', format: (v: unknown) => (v ? 'custom' : '-') },
];

this.outputFormatted(
configs as unknown as Record<string, unknown>[],
columns,
flags,
configs,
'No agents enabled for this project. Use `cascade agents create` to enable one.',
);
} catch (err) {
this.handleError(err);
}
Expand Down
44 changes: 23 additions & 21 deletions src/cli/dashboard/definitions/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,30 @@ export default class DefinitionsList extends DashboardCommand {
try {
const definitions = await this.client.agentDefinitions.list.query();

if (flags.json) {
this.outputJson(definitions);
return;
}
const rows = definitions.map((d) => ({
agentType: d.agentType,
label: d.definition.identity.label,
emoji: d.definition.identity.emoji,
isBuiltin: d.isBuiltin,
}));

this.outputTable(
definitions.map((d) => ({
agentType: d.agentType,
label: d.definition.identity.label,
emoji: d.definition.identity.emoji,
isBuiltin: d.isBuiltin,
})),
[
{ key: 'agentType', header: 'Agent Type' },
{ key: 'label', header: 'Label' },
{ key: 'emoji', header: 'Emoji' },
{
key: 'isBuiltin',
header: 'Built-in',
format: (v) => (v ? 'yes' : 'no'),
},
],
const columns = [
{ key: 'agentType', header: 'Agent Type' },
{ key: 'label', header: 'Label' },
{ key: 'emoji', header: 'Emoji' },
{
key: 'isBuiltin',
header: 'Built-in',
format: (v: unknown) => (v ? 'yes' : 'no'),
},
];

this.outputFormatted(
rows,
columns,
flags,
definitions,
'No agent definitions found. Import one with: cascade definitions import --file <definition.yaml>',
);
} catch (err) {
this.handleError(err);
Expand Down
17 changes: 10 additions & 7 deletions src/cli/dashboard/projects/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@ export default class ProjectsList extends DashboardCommand {
try {
const projects = await this.client.projects.listFull.query();

if (flags.json) {
this.outputJson(projects);
return;
}

this.outputTable(projects as unknown as Record<string, unknown>[], [
const columns = [
{ key: 'id', header: 'ID' },
{ key: 'name', header: 'Name' },
{ key: 'repo', header: 'Repo' },
{ key: 'baseBranch', header: 'Base Branch' },
{ key: 'model', header: 'Model' },
{ key: 'agentEngine', header: 'Engine' },
]);
];

this.outputFormatted(
projects as unknown as Record<string, unknown>[],
columns,
flags,
projects,
'No projects found. Create one with: cascade projects create --id <id> --name <name> --repo <owner/repo>',
);
} catch (err) {
this.handleError(err);
}
Expand Down
22 changes: 13 additions & 9 deletions src/cli/dashboard/runs/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,28 @@ export default class RunsList extends DashboardCommand {
order: flags.order as 'asc' | 'desc',
});

if (flags.json) {
this.outputJson(runs);
return;
}

const { data, total } = runs as { data: Record<string, unknown>[]; total: number };

this.outputTable(data, [
{ key: 'id', header: 'ID', format: (v) => String(v ?? '').slice(0, 8) },
const columns = [
{ key: 'id', header: 'ID', format: (v: unknown) => String(v ?? '').slice(0, 8) },
{ key: 'projectId', header: 'Project' },
{ key: 'agentType', header: 'Agent' },
{ key: 'status', header: 'Status', format: formatStatus },
{ key: 'startedAt', header: 'Started', format: formatDate },
{ key: 'durationMs', header: 'Duration', format: formatDuration },
{ key: 'costUsd', header: 'Cost', format: formatCost },
]);
];

this.outputFormatted(
data,
columns,
flags,
runs,
'No runs found. Try `cascade runs trigger --project <id> --agent-type <type>`',
);

if (total > data.length) {
const fmt = this.resolveFormat(flags);
if (fmt === 'table' && total > data.length) {
this.log(`\nShowing ${data.length} of ${total} runs.`);
}
} catch (err) {
Expand Down
17 changes: 10 additions & 7 deletions src/cli/dashboard/users/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ export default class UsersList extends DashboardCommand {
try {
const users = await this.client.users.list.query();

if (flags.json) {
this.outputJson(users);
return;
}

this.outputTable(users as unknown as Record<string, unknown>[], [
const columns = [
{ key: 'id', header: 'ID' },
{ key: 'email', header: 'Email' },
{ key: 'name', header: 'Name' },
{ key: 'role', header: 'Role' },
{ key: 'createdAt', header: 'Created', format: formatDate },
]);
];

this.outputFormatted(
users as unknown as Record<string, unknown>[],
columns,
flags,
users,
'No users found. Create one with: cascade users create --email <email> --password <pass>',
);
} catch (err) {
this.handleError(err);
}
Expand Down
Loading
Loading