Skip to content

Commit bb11cbd

Browse files
srousseyclaude
andcommitted
feat(cli-v2): add interactive init wizard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6c306de commit bb11cbd

3 files changed

Lines changed: 212 additions & 0 deletions

File tree

src/cli/groups/init.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { buildEnvConfig } from "./init";
3+
import type { InitConfig } from "./init";
4+
5+
describe("buildEnvConfig", () => {
6+
it("generates SQLite config with required vars", () => {
7+
const config: InitConfig = {
8+
dbType: "sqlite",
9+
dbFolder: "/home/user/.sec/data",
10+
dbName: "edgar",
11+
rawDataFolder: "/home/user/.sec/raw",
12+
};
13+
const result = buildEnvConfig(config);
14+
expect(result).toContain('SEC_DB_TYPE="sqlite"');
15+
expect(result).toContain('SEC_DB_FOLDER="/home/user/.sec/data"');
16+
expect(result).toContain('SEC_DB_NAME="edgar"');
17+
expect(result).toContain('SEC_RAW_DATA_FOLDER="/home/user/.sec/raw"');
18+
expect(result).not.toContain("SEC_PG_");
19+
});
20+
21+
it("generates Postgres config with URL when pgUrl provided", () => {
22+
const config: InitConfig = {
23+
dbType: "postgres",
24+
dbFolder: "/home/user/.sec/data",
25+
dbName: "edgar",
26+
rawDataFolder: "/home/user/.sec/raw",
27+
pgUrl: "postgres://user:pass@localhost:5432/edgar",
28+
};
29+
const result = buildEnvConfig(config);
30+
expect(result).toContain('SEC_DB_TYPE="postgres"');
31+
expect(result).toContain('SEC_PG_URL="postgres://user:pass@localhost:5432/edgar"');
32+
expect(result).not.toContain("SEC_PG_HOST");
33+
expect(result).not.toContain("SEC_PG_PORT");
34+
expect(result).not.toContain("SEC_PG_USER");
35+
expect(result).not.toContain("SEC_PG_PASSWORD");
36+
expect(result).not.toContain("SEC_PG_DATABASE");
37+
});
38+
39+
it("generates Postgres config with individual params when no pgUrl", () => {
40+
const config: InitConfig = {
41+
dbType: "postgres",
42+
dbFolder: "/home/user/.sec/data",
43+
dbName: "edgar",
44+
rawDataFolder: "/home/user/.sec/raw",
45+
pgHost: "db.example.com",
46+
pgPort: "5433",
47+
pgUser: "admin",
48+
pgPassword: "secret",
49+
pgDatabase: "sec_data",
50+
};
51+
const result = buildEnvConfig(config);
52+
expect(result).toContain('SEC_DB_TYPE="postgres"');
53+
expect(result).toContain('SEC_PG_HOST="db.example.com"');
54+
expect(result).toContain('SEC_PG_PORT="5433"');
55+
expect(result).toContain('SEC_PG_USER="admin"');
56+
expect(result).toContain('SEC_PG_PASSWORD="secret"');
57+
expect(result).toContain('SEC_PG_DATABASE="sec_data"');
58+
expect(result).not.toContain("SEC_PG_URL");
59+
});
60+
});

src/cli/groups/init.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import type { Command } from "commander";
2+
import { createInterface } from "readline";
3+
import { existsSync, mkdirSync, writeFileSync } from "fs";
4+
import { resolve } from "path";
5+
import { homedir } from "os";
6+
import { setupAllDatabases } from "../../config/setupAllDatabases";
7+
import { runCommand } from "../runCommand";
8+
9+
export interface InitConfig {
10+
readonly dbType: "sqlite" | "postgres";
11+
readonly dbFolder: string;
12+
readonly dbName: string;
13+
readonly rawDataFolder: string;
14+
readonly pgUrl?: string;
15+
readonly pgHost?: string;
16+
readonly pgPort?: string;
17+
readonly pgUser?: string;
18+
readonly pgPassword?: string;
19+
readonly pgDatabase?: string;
20+
}
21+
22+
export function buildEnvConfig(config: InitConfig): string {
23+
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+
];
29+
30+
if (config.dbType === "postgres") {
31+
if (config.pgUrl) {
32+
lines.push(`SEC_PG_URL="${config.pgUrl}"`);
33+
} 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}"`);
39+
}
40+
}
41+
42+
return lines.join("\n") + "\n";
43+
}
44+
45+
function prompt(
46+
rl: ReturnType<typeof createInterface>,
47+
question: string
48+
): Promise<string> {
49+
return new Promise((resolve) => {
50+
rl.question(question, (answer) => {
51+
resolve(answer.trim());
52+
});
53+
});
54+
}
55+
56+
export function addInitCommand(parent: Command): void {
57+
parent
58+
.command("init")
59+
.description("Interactive first-run setup wizard")
60+
.action(async () => {
61+
await runCommand(async () => {
62+
const envPath = resolve(process.cwd(), ".env.local");
63+
64+
if (existsSync(envPath)) {
65+
console.warn("Warning: .env.local already exists. Continuing will overwrite it.");
66+
}
67+
68+
const rl = createInterface({ input: process.stdin, output: process.stdout });
69+
70+
try {
71+
const defaultDbFolder = resolve(homedir(), ".sec/data");
72+
const defaultRawFolder = resolve(homedir(), ".sec/raw");
73+
74+
const dbTypeAnswer = await prompt(
75+
rl,
76+
"Database type (sqlite or postgres) [sqlite]: "
77+
);
78+
const dbType = dbTypeAnswer === "postgres" ? "postgres" : "sqlite";
79+
80+
const dbFolder =
81+
(await prompt(rl, `Database folder [${defaultDbFolder}]: `)) || defaultDbFolder;
82+
83+
const dbName = (await prompt(rl, 'Database name [edgar]: ')) || "edgar";
84+
85+
const rawDataFolder =
86+
(await prompt(rl, `Raw data folder [${defaultRawFolder}]: `)) || defaultRawFolder;
87+
88+
let pgFields: Partial<InitConfig> = {};
89+
90+
if (dbType === "postgres") {
91+
const useUrl = await prompt(
92+
rl,
93+
"Use a connection string? (y/n) [n]: "
94+
);
95+
96+
if (useUrl.toLowerCase() === "y") {
97+
const pgUrl = await prompt(rl, "PostgreSQL connection string: ");
98+
pgFields = { pgUrl };
99+
} else {
100+
const pgHost =
101+
(await prompt(rl, "PostgreSQL host [localhost]: ")) || "localhost";
102+
const pgPort = (await prompt(rl, "PostgreSQL port [5432]: ")) || "5432";
103+
const pgUser = await prompt(rl, "PostgreSQL user: ");
104+
const pgPassword = await prompt(rl, "PostgreSQL password: ");
105+
const pgDatabase =
106+
(await prompt(rl, "PostgreSQL database [edgar]: ")) || "edgar";
107+
108+
pgFields = { pgHost, pgPort, pgUser, pgPassword, pgDatabase };
109+
}
110+
}
111+
112+
const config: InitConfig = {
113+
dbType,
114+
dbFolder,
115+
dbName,
116+
rawDataFolder,
117+
...pgFields,
118+
};
119+
120+
rl.close();
121+
122+
const envContent = buildEnvConfig(config);
123+
writeFileSync(envPath, envContent, "utf-8");
124+
console.log(`Wrote ${envPath}`);
125+
126+
mkdirSync(dbFolder, { recursive: true });
127+
console.log(`Created directory: ${dbFolder}`);
128+
129+
mkdirSync(rawDataFolder, { recursive: true });
130+
console.log(`Created directory: ${rawDataFolder}`);
131+
132+
// Re-read env so DI picks up new values
133+
process.env.SEC_DB_TYPE = config.dbType;
134+
process.env.SEC_DB_FOLDER = config.dbFolder;
135+
process.env.SEC_DB_NAME = config.dbName;
136+
process.env.SEC_RAW_DATA_FOLDER = config.rawDataFolder;
137+
138+
await setupAllDatabases();
139+
console.log("Database tables created.");
140+
141+
console.log("\nSetup complete! Next steps:");
142+
console.log(" sec db status — verify database connection");
143+
console.log(" sec bootstrap cik — download CIK name lookup");
144+
console.log(" sec bootstrap index — download filing indexes");
145+
} finally {
146+
rl.close();
147+
}
148+
});
149+
});
150+
}

src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { addUpdateCommands } from "../cli/groups/update";
1515
import { addFetchCommands } from "../cli/groups/fetch";
1616
import { addQueryCommands } from "../cli/groups/query";
1717
import { addDbCommands } from "../cli/groups/db";
18+
import { addInitCommand } from "../cli/groups/init";
1819

1920
export const AddCommands = (program: Command): void => {
2021
EnvToDI();
@@ -33,4 +34,5 @@ export const AddCommands = (program: Command): void => {
3334
addFetchCommands(program);
3435
addQueryCommands(program);
3536
addDbCommands(program);
37+
addInitCommand(program);
3638
};

0 commit comments

Comments
 (0)