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
5 changes: 5 additions & 0 deletions .changeset/fix-redteam-output-flag-prompt-sets-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cdot65/prisma-airs-cli": patch
---

Add `--output <pretty|json|yaml>` to `airs redteam prompt-sets get` and `airs redteam properties values`. Both previously only emitted the pretty renderer, blocking `jq`-pipe workflows and the docs sidecar's pretty+json+yaml triplet requirement.
6 changes: 6 additions & 0 deletions docs/cli/redteam/prompt-sets.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ airs redteam prompt-sets get [options] <uuid>

- `uuid` (required) —

#### Options

| Flag | Required | Default | Description |
|------|:--------:|---------|-------------|
| `--output <format>` | No | `pretty` | Output format: pretty, json, yaml |

#### Examples

!!! warning "Example needed"
Expand Down
6 changes: 6 additions & 0 deletions docs/cli/redteam/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ airs redteam properties values [options] <name>

- `name` (required) —

#### Options

| Flag | Required | Default | Description |
|------|:--------:|---------|-------------|
| `--output <format>` | No | `pretty` | Output format: pretty, json, yaml |

#### Examples

!!! warning "Example needed"
Expand Down
22 changes: 15 additions & 7 deletions src/cli/commands/redteam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,14 +433,20 @@ export function registerRedteamCommand(program: Command): void {
promptSets
.command('get <uuid>')
.description('Get prompt set details')
.action(async (uuid: string) => {
.option('--output <format>', 'Output format: pretty, json, yaml', 'pretty')
.action(async (uuid: string, opts) => {
try {
renderRedteamHeader();
const fmt = opts.output as OutputFormat;
if (fmt === 'pretty') renderRedteamHeader();
const service = await createPromptSetService();
const ps = await service.getPromptSet(uuid);
renderPromptSetDetail(ps);
const info = await service.getPromptSetVersionInfo(uuid);
renderVersionInfo(info);
if (fmt === 'pretty') {
renderPromptSetDetail(ps);
renderVersionInfo(info);
} else {
renderPromptSetDetail(ps, fmt, info);
}
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
Expand Down Expand Up @@ -669,12 +675,14 @@ export function registerRedteamCommand(program: Command): void {
properties
.command('values <name>')
.description('List values for a property')
.action(async (name: string) => {
.option('--output <format>', 'Output format: pretty, json, yaml', 'pretty')
.action(async (name: string, opts) => {
try {
renderRedteamHeader();
const fmt = opts.output as OutputFormat;
if (fmt === 'pretty') renderRedteamHeader();
const service = await createPromptSetService();
const values = await service.getPropertyValues(name);
renderPropertyValues(values);
renderPropertyValues(values, fmt);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
Expand Down
49 changes: 39 additions & 10 deletions src/cli/renderer/redteam.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chalk from 'chalk';
import { dump as yamlDump } from 'js-yaml';
import { formatOutput, type OutputFormat } from './common.js';

/** Render the red team banner. */
Expand Down Expand Up @@ -400,15 +401,32 @@ export function renderTargetDetail(target: {
}

/** Render prompt set detail. */
export function renderPromptSetDetail(ps: {
uuid: string;
name: string;
active: boolean;
archive: boolean;
description?: string;
createdAt?: string;
updatedAt?: string;
}): void {
export function renderPromptSetDetail(
ps: {
uuid: string;
name: string;
active: boolean;
archive: boolean;
description?: string;
createdAt?: string;
updatedAt?: string;
},
format: OutputFormat = 'pretty',
info?: {
uuid: string;
version: number;
stats: { total: number; active: number; inactive: number };
},
): void {
if (format !== 'pretty') {
const payload = info ? { ...ps, versionInfo: info } : { ...ps };
if (format === 'json') {
console.log(JSON.stringify(payload, null, 2));
} else if (format === 'yaml') {
console.log(yamlDump(payload));
}
return;
}
console.log(chalk.bold('\n Prompt Set Detail:\n'));
console.log(` UUID: ${chalk.dim(ps.uuid)}`);
console.log(` Name: ${ps.name}`);
Expand Down Expand Up @@ -543,7 +561,18 @@ export function renderEulaContent(content: { content: string }): void {
}

/** Render property values. */
export function renderPropertyValues(values: Array<{ name: string; value: string }>): void {
export function renderPropertyValues(
values: Array<{ name: string; value: string }>,
format: OutputFormat = 'pretty',
): void {
if (format !== 'pretty') {
if (format === 'json') {
console.log(JSON.stringify(values, null, 2));
} else if (format === 'yaml') {
console.log(yamlDump(values));
}
return;
}
if (values.length === 0) {
console.log(chalk.dim(' No property values found.\n'));
return;
Expand Down
95 changes: 95 additions & 0 deletions tests/unit/cli/redteam-output-flag.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { load as yamlLoad } from 'js-yaml';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

let output: string[];
const originalLog = console.log;

beforeEach(() => {
output = [];
console.log = (...args: unknown[]) => output.push(args.join(' '));
});

afterEach(() => {
console.log = originalLog;
});

const samplePromptSet = {
uuid: 'ps-uuid-001',
name: 'sample-set',
active: true,
archive: false,
description: 'sample description',
createdAt: '2026-01-02T03:04:05Z',
updatedAt: '2026-01-02T03:04:05Z',
};

const sampleVersionInfo = {
uuid: 'ps-uuid-001',
version: 7,
stats: { total: 42, active: 40, inactive: 2 },
};

const sampleValues = [
{ name: 'persona', value: 'pirate' },
{ name: 'persona', value: 'doctor' },
];

describe('renderPromptSetDetail --output', () => {
it('emits JSON with combined detail + versionInfo when format=json', async () => {
const { renderPromptSetDetail } = await import('../../../src/cli/renderer/redteam.js');
renderPromptSetDetail(samplePromptSet, 'json', sampleVersionInfo);
const parsed = JSON.parse(output.join('\n'));
expect(parsed.uuid).toBe('ps-uuid-001');
expect(parsed.name).toBe('sample-set');
expect(parsed.versionInfo.version).toBe(7);
expect(parsed.versionInfo.stats.total).toBe(42);
});

it('emits YAML with combined detail + versionInfo when format=yaml', async () => {
const { renderPromptSetDetail } = await import('../../../src/cli/renderer/redteam.js');
renderPromptSetDetail(samplePromptSet, 'yaml', sampleVersionInfo);
const parsed = yamlLoad(output.join('\n')) as Record<string, unknown>;
expect(parsed.uuid).toBe('ps-uuid-001');
expect(parsed.name).toBe('sample-set');
expect((parsed.versionInfo as { version: number }).version).toBe(7);
});

it('still renders pretty form when format=pretty', async () => {
const { renderPromptSetDetail } = await import('../../../src/cli/renderer/redteam.js');
renderPromptSetDetail(samplePromptSet, 'pretty');
const text = output.join('\n');
expect(text).toContain('Prompt Set Detail');
expect(text).toContain('sample-set');
});
});

describe('renderPropertyValues --output', () => {
it('emits JSON array when format=json', async () => {
const { renderPropertyValues } = await import('../../../src/cli/renderer/redteam.js');
renderPropertyValues(sampleValues, 'json');
const parsed = JSON.parse(output.join('\n'));
expect(Array.isArray(parsed)).toBe(true);
expect(parsed).toEqual(sampleValues);
});

it('emits YAML when format=yaml', async () => {
const { renderPropertyValues } = await import('../../../src/cli/renderer/redteam.js');
renderPropertyValues(sampleValues, 'yaml');
const parsed = yamlLoad(output.join('\n'));
expect(parsed).toEqual(sampleValues);
});

it('still renders pretty form when format=pretty', async () => {
const { renderPropertyValues } = await import('../../../src/cli/renderer/redteam.js');
renderPropertyValues(sampleValues, 'pretty');
const text = output.join('\n');
expect(text).toContain('Property Values');
expect(text).toContain('persona');
});

it('prints empty-state message for empty list in pretty mode', async () => {
const { renderPropertyValues } = await import('../../../src/cli/renderer/redteam.js');
renderPropertyValues([], 'pretty');
expect(output.join('\n')).toContain('No property values');
});
});
Loading