Skip to content

Commit a028a47

Browse files
Copilotsroussey
andcommitted
fix: CLI v2 review feedback — input validation, escaping, type safety, and docs alignment (#66)
* Initial plan * fix: apply review feedback — validation, escaping, type correctness, and docs alignment Co-authored-by: sroussey <127349+sroussey@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sroussey <127349+sroussey@users.noreply.github.com>
1 parent de5d8af commit a028a47

12 files changed

Lines changed: 211 additions & 125 deletions

File tree

SPEC.md

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ All commands accept the following flags:
139139
| Flag | Short | Description |
140140
| --------------- | ----- | ------------------------------------------------- |
141141
| `--json` | | Output structured JSON to stdout |
142-
| `--verbose` | `-v` | Enable detailed log output |
142+
| `--verbose` | | Enable detailed log output |
143143
| `--dry-run` | | Show what would be done without making changes |
144144
| `--no-color` | | Disable colored output |
145-
| `--concurrency` | `-c` | Max parallel operations (default varies by command)|
145+
| `--concurrency` | | Max parallel operations (default varies by command)|
146146

147147
---
148148

@@ -200,14 +200,14 @@ Subcommands for running individual bootstrap phases.
200200

201201
Download and extract bulk SEC data archives.
202202

203-
| Option | Description |
203+
| Argument | Description |
204204
| -------- | ------------------------------------------------------------ |
205-
| `--type` | One of: `submissions`, `companyfacts`, `ciks`, `all` (default: `all`) |
205+
| `<type>` | One of: `submissions`, `facts`, `ciks`, `all` (default: `all`) |
206206

207207
**Behavior:**
208208

209209
- `submissions` — Downloads `submissions.zip`, extracts to `SEC_RAW_DATA_FOLDER/submissions/`
210-
- `companyfacts` — Downloads `companyfacts.zip`, extracts to `SEC_RAW_DATA_FOLDER/companyfacts/`
210+
- `facts` — Downloads `companyfacts.zip`, extracts to `SEC_RAW_DATA_FOLDER/facts/`
211211
- `ciks` — Downloads `cik-lookup-data.txt` to `SEC_RAW_DATA_FOLDER/ciks/`
212212
- `all` — Downloads all three
213213
- Validates extracted paths to prevent directory traversal
@@ -355,53 +355,59 @@ Process a single accession document.
355355

356356
Read-only commands for querying the database. All query commands support `--format` (`table`, `csv`, `json`; default: `table`) and `--limit`/`--offset` for pagination.
357357

358-
#### `sec query entities [cik]`
358+
#### `sec query entities [search]`
359359

360360
List or look up entities.
361361

362-
| Argument | Required | Description |
363-
| -------- | -------- | --------------------------------- |
364-
| `cik` | No | Specific CIK to look up |
362+
| Argument | Required | Description |
363+
| -------- | -------- | ---------------------------------------- |
364+
| `search` | No | Free-text search term to filter entities |
365365

366-
| Option | Description |
367-
| ------------------ | ----------------------------- |
368-
| `--name <pattern>` | Filter by name (LIKE pattern) |
369-
| `--sic <code>` | Filter by SIC code |
366+
| Option | Description |
367+
| ------------------ | -------------------------------- |
368+
| `--cik <cik>` | Filter by exact CIK |
369+
| `--sic <code>` | Filter by SIC code |
370370
| `--state <code>` | Filter by state of incorporation |
371-
| `--limit <n>` | Max rows (default: 25) |
372-
| `--offset <n>` | Skip rows (default: 0) |
373-
| `--format <fmt>` | Output format: table, csv, json |
371+
| `--sort <field>` | Sort by field name |
372+
| `--limit <n>` | Max rows (default: 25) |
373+
| `--offset <n>` | Skip rows (default: 0) |
374+
| `--format <fmt>` | Output format: table, csv, json |
374375

375-
#### `sec query filings [cik]`
376+
#### `sec query filings [search]`
376377

377378
List or look up filings.
378379

379-
| Argument | Required | Description |
380-
| -------- | -------- | ------------------------ |
381-
| `cik` | No | Filter by entity CIK |
380+
| Argument | Required | Description |
381+
| -------- | -------- | ----------------------------------------- |
382+
| `search` | No | Free-text search term to filter filings |
382383

383384
| Option | Description |
384385
| ------------------ | --------------------------------- |
386+
| `--cik <cik>` | Filter by entity CIK |
385387
| `--form <type>` | Filter by form type |
386-
| `--from <date>` | Filing date start (YYYY-MM-DD) |
387-
| `--to <date>` | Filing date end (YYYY-MM-DD) |
388+
| `--after <date>` | Filing date start (YYYY-MM-DD) |
389+
| `--before <date>` | Filing date end (YYYY-MM-DD) |
388390
| `--limit <n>` | Max rows (default: 25) |
389391
| `--offset <n>` | Skip rows (default: 0) |
390392
| `--format <fmt>` | Output format: table, csv, json |
391393

392-
#### `sec query offerings [cik]`
394+
#### `sec query offerings [search]`
393395

394396
List Form D investment offerings.
395397

396-
| Argument | Required | Description |
397-
| -------- | -------- | --------------------- |
398-
| `cik` | No | Filter by issuer CIK |
399-
400-
| Option | Description |
401-
| ----------------------- | ----------------------------- |
402-
| `--industry <group>` | Filter by industry group |
403-
| `--limit <n>` | Max rows (default: 25) |
404-
| `--offset <n>` | Skip rows (default: 0) |
398+
| Argument | Required | Description |
399+
| -------- | -------- | ------------------------------------------ |
400+
| `search` | No | Free-text search term to filter offerings |
401+
402+
| Option | Description |
403+
| ----------------------- | ------------------------------ |
404+
| `--cik <cik>` | Filter by issuer CIK |
405+
| `--industry <group>` | Filter by industry group |
406+
| `--exemption <type>` | Filter by exemption type |
407+
| `--after <date>` | Filter after date |
408+
| `--before <date>` | Filter before date |
409+
| `--limit <n>` | Max rows (default: 25) |
410+
| `--offset <n>` | Skip rows (default: 0) |
405411
| `--format <fmt>` | Output format: table, csv, json |
406412

407413
#### `sec query crowdfunding [cik]`
@@ -466,11 +472,11 @@ Show row counts for all tables and processing progress.
466472

467473
#### `sec db reset`
468474

469-
Drop all tables and re-create them. Prompts for confirmation unless `--force` is passed.
475+
Drop all tables and re-create them. Prompts for confirmation unless `--confirm` is passed.
470476

471-
| Option | Description |
472-
| --------- | ------------------------------ |
473-
| `--force` | Skip confirmation prompt |
477+
| Option | Description |
478+
| ----------- | ------------------------------ |
479+
| `--confirm` | Skip confirmation prompt |
474480

475481
---
476482

docs/plans/2026-03-05-cli-v2-design.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ Interactive wizard:
3232
5. Create directories
3333
6. Run `db setup`
3434

35-
Detects existing `.env.local` and offers to reconfigure or skip. PostgreSQL path prompts for connection string or individual parameters. Validates database connectivity before writing config.
36-
37-
When `--json` is passed: outputs config as JSON, no interactivity (for scripting). Non-zero exit if any step fails.
35+
Detects existing `.env.local` and warns before overwriting. PostgreSQL path prompts for connection string or individual parameters. Non-zero exit if any step fails.
3836

3937
### 1.2 Pipeline Commands
4038

src/cli/GlobalOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function parseGlobalOptions(cmd: Command): GlobalOptions {
2828
};
2929
}
3030

31-
function parseIntOption(value: string): number {
31+
export function parseIntOption(value: string): number {
3232
const parsed = parseInt(value, 10);
3333
if (isNaN(parsed)) {
3434
throw new Error(`"${value}" is not a valid number`);

src/cli/groups/init.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,28 @@ describe("buildEnvConfig", () => {
5757
expect(result).toContain('SEC_PG_DATABASE="sec_data"');
5858
expect(result).not.toContain("SEC_PG_URL");
5959
});
60+
61+
it("escapes double quotes in values", () => {
62+
const config: InitConfig = {
63+
dbType: "sqlite",
64+
dbFolder: '/path/with/"quotes"',
65+
dbName: 'name"with"quotes',
66+
rawDataFolder: "/simple/path",
67+
};
68+
const result = buildEnvConfig(config);
69+
expect(result).toContain('SEC_DB_FOLDER="/path/with/\\"quotes\\""');
70+
expect(result).toContain('SEC_DB_NAME="name\\"with\\"quotes"');
71+
});
72+
73+
it("escapes newlines in values", () => {
74+
const config: InitConfig = {
75+
dbType: "postgres",
76+
dbFolder: "/path",
77+
dbName: "edgar",
78+
rawDataFolder: "/raw",
79+
pgPassword: "pass\nword",
80+
};
81+
const result = buildEnvConfig(config);
82+
expect(result).toContain('SEC_PG_PASSWORD="pass\\nword"');
83+
});
6084
});

src/cli/groups/init.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,27 @@ export interface InitConfig {
1919
readonly pgDatabase?: string;
2020
}
2121

22+
function escapeEnvValue(value: string): string {
23+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
24+
}
25+
2226
export function buildEnvConfig(config: InitConfig): string {
2327
const lines: string[] = [
24-
`SEC_DB_TYPE="${config.dbType}"`,
25-
`SEC_DB_FOLDER="${config.dbFolder}"`,
26-
`SEC_DB_NAME="${config.dbName}"`,
27-
`SEC_RAW_DATA_FOLDER="${config.rawDataFolder}"`,
28+
`SEC_DB_TYPE="${escapeEnvValue(config.dbType)}"`,
29+
`SEC_DB_FOLDER="${escapeEnvValue(config.dbFolder)}"`,
30+
`SEC_DB_NAME="${escapeEnvValue(config.dbName)}"`,
31+
`SEC_RAW_DATA_FOLDER="${escapeEnvValue(config.rawDataFolder)}"`,
2832
];
2933

3034
if (config.dbType === "postgres") {
3135
if (config.pgUrl) {
32-
lines.push(`SEC_PG_URL="${config.pgUrl}"`);
36+
lines.push(`SEC_PG_URL="${escapeEnvValue(config.pgUrl)}"`);
3337
} else {
34-
if (config.pgHost) lines.push(`SEC_PG_HOST="${config.pgHost}"`);
35-
if (config.pgPort) lines.push(`SEC_PG_PORT="${config.pgPort}"`);
36-
if (config.pgUser) lines.push(`SEC_PG_USER="${config.pgUser}"`);
37-
if (config.pgPassword) lines.push(`SEC_PG_PASSWORD="${config.pgPassword}"`);
38-
if (config.pgDatabase) lines.push(`SEC_PG_DATABASE="${config.pgDatabase}"`);
38+
if (config.pgHost) lines.push(`SEC_PG_HOST="${escapeEnvValue(config.pgHost)}"`);
39+
if (config.pgPort) lines.push(`SEC_PG_PORT="${escapeEnvValue(config.pgPort)}"`);
40+
if (config.pgUser) lines.push(`SEC_PG_USER="${escapeEnvValue(config.pgUser)}"`);
41+
if (config.pgPassword) lines.push(`SEC_PG_PASSWORD="${escapeEnvValue(config.pgPassword)}"`);
42+
if (config.pgDatabase) lines.push(`SEC_PG_DATABASE="${escapeEnvValue(config.pgDatabase)}"`);
3943
}
4044
}
4145

@@ -117,8 +121,6 @@ export function addInitCommand(parent: Command): void {
117121
...pgFields,
118122
};
119123

120-
rl.close();
121-
122124
const envContent = buildEnvConfig(config);
123125
writeFileSync(envPath, envContent, "utf-8");
124126
console.log(`Wrote ${envPath}`);

0 commit comments

Comments
 (0)