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/0000-output-discipline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cdot65/prisma-airs-cli": minor
---

Machine-readable output is now pipe-safe: `--output json|yaml|csv` emits only the payload on stdout, with progress, banners, and rate-limit warnings moved to stderr. Exit codes are standardized (0 success, 1 runtime/API failure, 2 usage error) across every command group. API errors show the HTTP status and a `--debug` hint. Async command errors are handled properly via parseAsync.
32 changes: 32 additions & 0 deletions docs-site/docs/getting-started/exit-codes-and-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Exit Codes & Output Streams

The CLI follows one contract everywhere:

## Exit codes

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | Runtime failure — API error, network failure, missing credentials, partial batch failure |
| `2` | Usage error — invalid flag value, unparsable input file, missing required flag combination |

Scripts can rely on `2` meaning "fix the invocation" and `1` meaning "fix the
environment or retry".

## stdout vs stderr

- **stdout** carries data only: pretty layouts, tables, and the raw payload for
`--output json|yaml|csv`.
- **stderr** carries everything else: progress/status lines, rate-limit retry
warnings, deprecation notices, and errors.

This means machine-readable output always pipes cleanly:

```bash
airs runtime profiles list --output json | jq '.[].profile_name'
airs runtime bulk-scan --profile demo --input prompts.txt --output results.csv
# progress lines appear on the terminal (stderr) without corrupting the CSV
```

API errors include the HTTP status when available, plus a reminder that
`--debug` captures the full (secret-redacted) request/response traffic.
1 change: 1 addition & 0 deletions docs-site/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const sidebars: SidebarsConfig = {
'getting-started/installation',
'getting-started/configuration',
'getting-started/quick-start',
'getting-started/exit-codes-and-output',
],
},
'concepts/how-prisma-airs-works',
Expand Down
20 changes: 7 additions & 13 deletions src/cli/commands/dlp/dictionaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { basename } from 'node:path';
import type { Command } from 'commander';
import { SdkDictionariesService } from '../../../airs/dlp/dictionaries.js';
import type { DictionaryRequest } from '../../../airs/dlp/types.js';
import { dlpDictionaries, type OutputFormat, renderError } from '../../renderer/index.js';
import { dlpDictionaries, fail, type OutputFormat, usageError } from '../../renderer/index.js';
import { buildMergePatch, parseBody } from './patch.js';

// biome-ignore lint/suspicious/noExplicitAny: opts object from commander
Expand Down Expand Up @@ -49,8 +49,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand Down Expand Up @@ -78,8 +77,7 @@ export function register(dlp: Command): void {
});
dlpDictionaries.renderCreated(r, opts.output as OutputFormat);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

Expand All @@ -96,8 +94,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand Down Expand Up @@ -132,8 +129,7 @@ export function register(dlp: Command): void {
dlpDictionaries.renderReplaced(r, opts.output as OutputFormat);
}
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

Expand All @@ -157,8 +153,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

Expand All @@ -170,8 +165,7 @@ export function register(dlp: Command): void {
await new SdkDictionariesService().delete(id);
dlpDictionaries.renderDeleted(id);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});
}
11 changes: 4 additions & 7 deletions src/cli/commands/dlp/filtering-profiles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from 'commander';
import { SdkDataFilteringProfilesService } from '../../../airs/dlp/data-filtering-profiles.js';
import { dlpFilteringProfiles, type OutputFormat, renderError } from '../../renderer/index.js';
import { dlpFilteringProfiles, fail, type OutputFormat, usageError } from '../../renderer/index.js';
import { buildFilteringProfileBody, repeatable } from './build-body.js';
import { parseBody } from './patch.js';

Expand Down Expand Up @@ -35,8 +35,7 @@ export function register(dlp: Command): void {
const r = await svc.list({ page: opts.page, size: opts.size, sort: opts.sort });
dlpFilteringProfiles.renderList(r, opts.output as OutputFormat);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand All @@ -49,8 +48,7 @@ export function register(dlp: Command): void {
const svc = new SdkDataFilteringProfilesService();
dlpFilteringProfiles.renderGet(await svc.get(id), opts.output as OutputFormat);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand Down Expand Up @@ -81,8 +79,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});
}
45 changes: 28 additions & 17 deletions src/cli/commands/dlp/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import type { Command } from 'commander';
import type { Format } from '../../../dlp/types.js';
import { fail, ui, usageError } from '../../renderer/index.js';

const ALL_FORMATS: Format[] = ['pdf', 'png', 'jpeg', 'svg', 'docx'];

Expand Down Expand Up @@ -58,33 +58,44 @@ export function register(parent: Command): void {
.option('--seed <n>', 'Seed for reproducible payloads')
.option('--output <format>', 'Summary format: pretty or json', 'pretty')
.action(async (opts) => {
const types = parseTypes(opts.types);
let types: Format[];
try {
types = parseTypes(opts.types);
} catch (err) {
usageError(err instanceof Error ? err.message : String(err));
}
const count = Number.parseInt(opts.count, 10);
if (!Number.isInteger(count) || count < 1) {
throw new Error('--count must be a positive integer');
usageError('--count must be a positive integer');
}
const techniques =
opts.techniques === 'all'
? 'all'
: (opts.techniques as string).split(',').map((t) => t.trim());
const seed = opts.seed === undefined ? undefined : Number.parseInt(opts.seed, 10);

const generateCorpus = await loadGenerateCorpus();
const summary = await generateCorpus({ types, count, out: opts.out, techniques, seed });
try {
const generateCorpus = await loadGenerateCorpus();
const summary = await generateCorpus({ types, count, out: opts.out, techniques, seed });

if (opts.output === 'json') {
console.log(JSON.stringify(summary, null, 2));
return;
}
if (opts.output === 'json') {
console.log(JSON.stringify(summary, null, 2));
return;
}

console.log(chalk.bold('\n DLP Test-File Generation'));
console.log(` Output: ${summary.out}`);
console.log(` Seed: ${summary.seed}`);
console.log(` Clean: ${summary.clean} Dirty: ${summary.dirty}`);
console.log(` Manifest: ${summary.manifestPath}\n`);
for (const [fmt, counts] of Object.entries(summary.byFormat)) {
console.log(` ${fmt.padEnd(5)} clean=${counts.clean} dirty=${counts.dirty}`);
ui.header('DLP Test-File Generation');
ui.keyValue([
['Output', summary.out],
['Seed', summary.seed],
['Clean', summary.clean],
['Dirty', summary.dirty],
['Manifest', summary.manifestPath],
]);
for (const [fmt, counts] of Object.entries(summary.byFormat)) {
ui.dim(`${fmt.padEnd(5)} clean=${counts.clean} dirty=${counts.dirty}`);
}
} catch (err) {
fail(err);
}
console.log('');
});
}
20 changes: 7 additions & 13 deletions src/cli/commands/dlp/patterns.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from 'commander';
import { SdkDataPatternsService } from '../../../airs/dlp/data-patterns.js';
import { dlpPatterns, type OutputFormat, renderError } from '../../renderer/index.js';
import { dlpPatterns, fail, type OutputFormat, usageError } from '../../renderer/index.js';
import { buildPatternBody, repeatable } from './build-body.js';
import { buildMergePatch, parseBody } from './patch.js';

Expand Down Expand Up @@ -50,8 +50,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand All @@ -64,8 +63,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

Expand All @@ -80,8 +78,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand All @@ -95,8 +92,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
},
);
Expand Down Expand Up @@ -126,8 +122,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

Expand All @@ -139,8 +134,7 @@ export function register(dlp: Command): void {
await new SdkDataPatternsService().delete(id);
dlpPatterns.renderArchived(id);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});
}
33 changes: 13 additions & 20 deletions src/cli/commands/dlp/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Command } from 'commander';
import { SdkDataProfilesService } from '../../../airs/dlp/data-profiles.js';
import { dlpProfiles, type OutputFormat, renderError } from '../../renderer/index.js';
import { dlpProfiles, fail, type OutputFormat, usageError } from '../../renderer/index.js';
import { buildProfileBody, repeatable } from './build-body.js';
import { buildMergePatch, parseBody } from './patch.js';

Expand Down Expand Up @@ -60,8 +60,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand All @@ -74,8 +73,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

Expand All @@ -90,8 +88,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(1);
fail(err);
}
});

Expand All @@ -105,8 +102,7 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
},
);
Expand Down Expand Up @@ -136,23 +132,20 @@ export function register(dlp: Command): void {
opts.output as OutputFormat,
);
} catch (err) {
renderError(err instanceof Error ? err.message : String(err));
process.exit(2);
usageError(err instanceof Error ? err.message : String(err));
}
});

group
.command('delete <id>')
.description('Not supported — prints the patch idiom and exits 2')
.action((id) => {
console.error(`
This DLP API has no DELETE for data profiles.
To soft-delete, fetch the profile to get its name + profile_type, then patch:

airs runtime dlp profiles get ${id} --output json
airs runtime dlp profiles patch ${id} --set profile_status='"deleted"' \\
--set name='"<existing-name>"' --set profile_type='"<existing-type>"'
`);
process.exit(2);
usageError(
`This DLP API has no DELETE for data profiles.\n` +
` To soft-delete, fetch the profile to get its name + profile_type, then patch:\n\n` +
` airs runtime dlp profiles get ${id} --output json\n` +
` airs runtime dlp profiles patch ${id} --set profile_status='"deleted"' \\\n` +
` --set name='"<existing-name>"' --set profile_type='"<existing-type>"'`,
);
});
}
Loading
Loading