Skip to content

Commit dac8f8c

Browse files
committed
refactor(safegres): use inquirerer for CLI plumbing
Drop the hand-rolled argv parser in favor of inquirerer's CLI class + @inquirerer/utils helpers — same convention as cnc, pgpm, and csdk. - Remove src/util/args.ts - Add src/cli/commands.ts (router via extractFirst) - Add src/cli/pg.ts (pg command handler) - src/cli.ts now constructs the inquirerer CLI CLI surface is unchanged (`safegres pg --connection ...`).
1 parent 0922b73 commit dac8f8c

7 files changed

Lines changed: 179 additions & 158 deletions

File tree

graphql/safegres/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"constructive"
4242
],
4343
"dependencies": {
44+
"@inquirerer/utils": "^3.3.5",
4445
"@pgsql/types": "^17.6.2",
46+
"inquirerer": "^4.7.0",
4547
"node-type-registry": "workspace:^",
4648
"pg": "^8.16.0",
4749
"pgsql-deparser": "^17.18.2",

graphql/safegres/src/cli.ts

Lines changed: 20 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,28 @@
11
#!/usr/bin/env node
22
/* eslint-disable no-console */
3-
import { Client } from 'pg';
3+
import { CLI, CLIOptions, getPackageJson } from 'inquirerer';
44

5-
import { auditPg } from './commands/pg';
6-
import { renderJson } from './report/json';
7-
import { renderPretty } from './report/pretty';
8-
import type { Severity } from './types';
9-
import { meetsThreshold, SEVERITY_ORDER } from './types';
10-
import { boolFlag, listFlag, parseArgs, stringFlag } from './util/args';
5+
import { commands } from './cli/commands';
116

12-
const HELP = `\
13-
safegres — pure-PostgreSQL RLS auditor
14-
15-
Usage:
16-
safegres pg [options]
17-
safegres help
18-
19-
Options (pg):
20-
--connection <url> PostgreSQL connection string (or env DATABASE_URL)
21-
--schemas <csv> Limit to these schemas (default: all non-system)
22-
--exclude-schemas <csv> Skip these schemas (default: pg_catalog,information_schema,pg_toast)
23-
--roles <csv> Audit grants only for these roles (default: all)
24-
--exclude-roles <csv> Skip grants for these roles
25-
--format <fmt> "pretty" (default) | "json" | "json-pretty"
26-
--fail-on <severity> Exit non-zero if any finding >= severity
27-
(critical|high|medium|low|info; default: none)
28-
--skip-ast Skip AST-level anti-pattern checks (faster)
29-
--no-color Disable ANSI colors in pretty output
30-
`;
31-
32-
async function main(): Promise<number> {
33-
const args = parseArgs(process.argv.slice(2));
34-
const cmd = args.command ?? 'help';
35-
36-
if (cmd === 'help' || cmd === '-h' || cmd === '--help' || args.flags.help === true) {
37-
process.stdout.write(HELP);
38-
return 0;
39-
}
40-
41-
if (cmd !== 'pg') {
42-
process.stderr.write(`Unknown command: ${cmd}\n\n${HELP}`);
43-
return 2;
44-
}
7+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
8+
const pkg = getPackageJson(__dirname);
9+
console.log(pkg.version);
10+
process.exit(0);
11+
}
4512

46-
const connection = stringFlag(args.flags, 'connection') ?? process.env.DATABASE_URL;
47-
if (!connection) {
48-
process.stderr.write('error: --connection <url> or DATABASE_URL env required\n');
49-
return 2;
13+
const options: Partial<CLIOptions> = {
14+
minimistOpts: {
15+
alias: {
16+
v: 'version',
17+
h: 'help'
18+
},
19+
boolean: ['skip-ast', 'no-color', 'color', 'help']
5020
}
21+
};
5122

52-
const client = new Client({ connectionString: connection });
53-
await client.connect();
54-
try {
55-
const report = await auditPg(client, {
56-
schemas: listFlag(args.flags, 'schemas'),
57-
excludeSchemas: listFlag(args.flags, 'exclude-schemas'),
58-
includeRoles: listFlag(args.flags, 'roles'),
59-
excludeRoles: listFlag(args.flags, 'exclude-roles'),
60-
skipAstChecks: boolFlag(args.flags, 'skip-ast')
61-
});
62-
63-
const fmt = stringFlag(args.flags, 'format') ?? 'pretty';
64-
let output: string;
65-
switch (fmt) {
66-
case 'json':
67-
output = renderJson(report);
68-
break;
69-
case 'json-pretty':
70-
output = renderJson(report, { pretty: true });
71-
break;
72-
case 'pretty':
73-
output = renderPretty(report, { color: !boolFlag(args.flags, 'no-color') });
74-
break;
75-
default:
76-
process.stderr.write(`Unknown --format: ${fmt}\n`);
77-
return 2;
78-
}
79-
process.stdout.write(output);
80-
process.stdout.write('\n');
81-
82-
const failOn = stringFlag(args.flags, 'fail-on') as Severity | undefined;
83-
if (failOn) {
84-
if (!(failOn in SEVERITY_ORDER)) {
85-
process.stderr.write(`Unknown --fail-on severity: ${failOn}\n`);
86-
return 2;
87-
}
88-
if (report.findings.some((f) => meetsThreshold(f.severity, failOn))) {
89-
return 1;
90-
}
91-
}
92-
return 0;
93-
} finally {
94-
await client.end();
95-
}
96-
}
23+
const app = new CLI(commands, options);
9724

98-
if (require.main === module) {
99-
main().then((code) => process.exit(code)).catch((err) => {
100-
// eslint-disable-next-line no-console
101-
console.error(err);
102-
process.exit(3);
103-
});
104-
}
25+
app.run().catch((error) => {
26+
console.error(error);
27+
process.exit(1);
28+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-disable no-console */
2+
import { CLIOptions, extractFirst, Inquirerer, ParsedArgs } from 'inquirerer';
3+
4+
import pg from './pg';
5+
6+
const usage = `
7+
safegres — pure-PostgreSQL RLS auditor
8+
9+
Usage:
10+
safegres <command> [OPTIONS]
11+
12+
Commands:
13+
pg Audit grants, RLS flags, policy coverage, and anti-patterns
14+
help Show this help message
15+
16+
Run \`safegres <command> --help\` for command-specific options.
17+
`;
18+
19+
const commandMap: Record<string, (argv: ParsedArgs, prompter: Inquirerer, options: CLIOptions) => unknown> = {
20+
pg
21+
};
22+
23+
export const commands = async (
24+
argv: ParsedArgs,
25+
prompter: Inquirerer,
26+
options: CLIOptions
27+
): Promise<void> => {
28+
const { first: command, newArgv } = extractFirst(argv);
29+
30+
if (!command || command === 'help' || argv.help || argv.h) {
31+
console.log(usage);
32+
return;
33+
}
34+
35+
const handler = commandMap[command];
36+
if (!handler) {
37+
console.error(`Unknown command: ${command}\n${usage}`);
38+
process.exit(2);
39+
}
40+
41+
await handler(newArgv as ParsedArgs, prompter, options);
42+
};

graphql/safegres/src/cli/pg.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/* eslint-disable no-console */
2+
import { CLIOptions, Inquirerer, ParsedArgs } from 'inquirerer';
3+
import { Client } from 'pg';
4+
5+
import { auditPg } from '../commands/pg';
6+
import { renderJson } from '../report/json';
7+
import { renderPretty } from '../report/pretty';
8+
import type { Severity } from '../types';
9+
import { meetsThreshold, SEVERITY_ORDER } from '../types';
10+
11+
const usage = `
12+
safegres pg — pure-PostgreSQL RLS auditor
13+
14+
safegres pg [OPTIONS]
15+
16+
Options:
17+
--connection <url> PostgreSQL connection string (or env DATABASE_URL)
18+
--schemas <csv> Limit to these schemas (default: all non-system)
19+
--exclude-schemas <csv> Skip these schemas (default: pg_catalog,information_schema,pg_toast)
20+
--roles <csv> Audit grants only for these roles (default: all)
21+
--exclude-roles <csv> Skip grants for these roles
22+
--format <fmt> "pretty" (default) | "json" | "json-pretty"
23+
--fail-on <severity> Exit non-zero if any finding >= severity
24+
(critical|high|medium|low|info; default: none)
25+
--skip-ast Skip AST-level anti-pattern checks (faster)
26+
--no-color Disable ANSI colors in pretty output
27+
--help, -h Show this help message
28+
`;
29+
30+
function csv(value: unknown): string[] | undefined {
31+
if (typeof value !== 'string' || value.length === 0) return undefined;
32+
return value
33+
.split(',')
34+
.map((p) => p.trim())
35+
.filter(Boolean);
36+
}
37+
38+
function string(value: unknown): string | undefined {
39+
return typeof value === 'string' && value.length > 0 ? value : undefined;
40+
}
41+
42+
function bool(value: unknown): boolean {
43+
return value === true || value === 'true';
44+
}
45+
46+
export default async (
47+
argv: ParsedArgs,
48+
_prompter: Inquirerer,
49+
_options: CLIOptions
50+
): Promise<void> => {
51+
if (argv.help || argv.h) {
52+
console.log(usage);
53+
return;
54+
}
55+
56+
const connection = string(argv.connection) ?? process.env.DATABASE_URL;
57+
if (!connection) {
58+
console.error('error: --connection <url> or DATABASE_URL env required');
59+
process.exit(2);
60+
}
61+
62+
// `--no-color` is parsed by minimist as `color: false` (negation of `--color`).
63+
const colorFlag = 'color' in argv ? argv.color !== false : !bool(argv['no-color']);
64+
65+
const client = new Client({ connectionString: connection });
66+
await client.connect();
67+
try {
68+
const report = await auditPg(client, {
69+
schemas: csv(argv.schemas),
70+
excludeSchemas: csv(argv['exclude-schemas']),
71+
includeRoles: csv(argv.roles),
72+
excludeRoles: csv(argv['exclude-roles']),
73+
skipAstChecks: bool(argv['skip-ast'])
74+
});
75+
76+
const fmt = string(argv.format) ?? 'pretty';
77+
let output: string;
78+
switch (fmt) {
79+
case 'json':
80+
output = renderJson(report);
81+
break;
82+
case 'json-pretty':
83+
output = renderJson(report, { pretty: true });
84+
break;
85+
case 'pretty':
86+
output = renderPretty(report, { color: colorFlag });
87+
break;
88+
default:
89+
console.error(`Unknown --format: ${fmt}`);
90+
process.exit(2);
91+
}
92+
process.stdout.write(output);
93+
process.stdout.write('\n');
94+
95+
const failOn = string(argv['fail-on']) as Severity | undefined;
96+
if (failOn) {
97+
if (!(failOn in SEVERITY_ORDER)) {
98+
console.error(`Unknown --fail-on severity: ${failOn}`);
99+
process.exit(2);
100+
}
101+
if (report.findings.some((f) => meetsThreshold(f.severity, failOn))) {
102+
process.exit(1);
103+
}
104+
}
105+
} finally {
106+
await client.end();
107+
}
108+
};

graphql/safegres/src/util/args.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

graphql/safegres/src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// Auto-synced from package.json during publish.
2-
export const version = '0.0.1';
2+
export const version = '0.1.0';

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)