becky-cli 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +102 -0
  2. package/dist/ai/openai.d.ts +17 -0
  3. package/dist/ai/openai.js +67 -0
  4. package/dist/commands/config.d.ts +3 -0
  5. package/dist/commands/config.js +30 -0
  6. package/dist/commands/connect.d.ts +11 -0
  7. package/dist/commands/connect.js +36 -0
  8. package/dist/commands/doctor.d.ts +10 -0
  9. package/dist/commands/doctor.js +166 -0
  10. package/dist/commands/migrate.d.ts +9 -0
  11. package/dist/commands/migrate.js +208 -0
  12. package/dist/commands/postgres-setup.d.ts +12 -0
  13. package/dist/commands/postgres-setup.js +132 -0
  14. package/dist/commands/up.d.ts +13 -0
  15. package/dist/commands/up.js +58 -0
  16. package/dist/config/store.d.ts +19 -0
  17. package/dist/config/store.js +65 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +197 -0
  20. package/dist/platform/detect.d.ts +10 -0
  21. package/dist/platform/detect.js +47 -0
  22. package/dist/platform/linux-ubuntu.d.ts +2 -0
  23. package/dist/platform/linux-ubuntu.js +48 -0
  24. package/dist/postgres/admin.d.ts +19 -0
  25. package/dist/postgres/admin.js +77 -0
  26. package/dist/project/alembicScaffold.d.ts +7 -0
  27. package/dist/project/alembicScaffold.js +161 -0
  28. package/dist/project/env.d.ts +25 -0
  29. package/dist/project/env.js +124 -0
  30. package/dist/project/fastapiPins.d.ts +3 -0
  31. package/dist/project/fastapiPins.js +13 -0
  32. package/dist/project/identity.d.ts +13 -0
  33. package/dist/project/identity.js +37 -0
  34. package/dist/project/load.d.ts +27 -0
  35. package/dist/project/load.js +67 -0
  36. package/dist/util/exec.d.ts +15 -0
  37. package/dist/util/exec.js +45 -0
  38. package/dist/util/log.d.ts +6 -0
  39. package/dist/util/log.js +27 -0
  40. package/dist/util/prompt.d.ts +2 -0
  41. package/dist/util/prompt.js +22 -0
  42. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # becky-cli
2
+
3
+ CLI for local Postgres setup and wiring Becky–generated backends.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g becky-cli
9
+ # or without installing:
10
+ npx becky-cli --help
11
+ ```
12
+
13
+ ## Quick start (after unzipping a generated project)
14
+
15
+ ```bash
16
+ cd my-backend
17
+ npx becky-cli up .
18
+ ```
19
+
20
+ That will:
21
+
22
+ 1. Install/start Postgres if needed (Ubuntu/Debian)
23
+ 2. Create user + database
24
+ 3. Write `.env` with `DATABASE_URL`
25
+ 4. Run stack migrations (Prisma / Alembic / scripts)
26
+
27
+ Then start your API as described in the project README.
28
+
29
+ ## Commands
30
+
31
+ | Command | What it does | AI? |
32
+ |---------|--------------|-----|
33
+ | `becky postgres status` | Check install + service | No |
34
+ | `becky postgres setup` | Install Postgres, create user/DB | No |
35
+ | `becky connect [dir]` | Write `.env`, test connection | No |
36
+ | `becky migrate [dir]` | Run migrations for the stack | No |
37
+ | `becky up [dir]` | setup + connect + migrate | No |
38
+ | `becky doctor [dir]` | Health checks; OpenAI diagnosis if issues | Yes* |
39
+ | `becky config set-key` | Save OpenAI API key | — |
40
+ | `becky config show` | Show masked config | — |
41
+
42
+ \* AI only runs when checks fail **and** a key is available.
43
+
44
+ ## OpenAI (for `doctor`)
45
+
46
+ The CLI does **not** need OpenAI for setup/connect/migrate.
47
+ Only `doctor` uses it when something is wrong.
48
+
49
+ Set a key one of these ways:
50
+
51
+ ```bash
52
+ # Option A — save locally (chmod 600 under ~/.becky/)
53
+ becky config set-key
54
+
55
+ # Option B — environment
56
+ export OPENAI_API_KEY=sk-...
57
+
58
+ # Option C — one-shot flag
59
+ becky doctor . --api-key sk-...
60
+ ```
61
+
62
+ Optional model:
63
+
64
+ ```bash
65
+ becky config set-model gpt-4o-mini
66
+ becky doctor . --model gpt-4o
67
+ ```
68
+
69
+ Checks only (no AI):
70
+
71
+ ```bash
72
+ becky doctor . --skip-ai
73
+ ```
74
+
75
+ ## Examples
76
+
77
+ ```bash
78
+ becky postgres status
79
+ becky postgres setup --spec ./project-spec.json -y
80
+ # creates user + database both named after the project (e.g. bicycle_marketplace)
81
+ becky connect . --force
82
+ becky migrate .
83
+ becky up . -y
84
+ becky doctor .
85
+ ```
86
+
87
+ Credentials from `postgres setup` / `connect` are stored in `~/.becky/credentials.json` so you are not re-prompted every time.
88
+
89
+ ## Requirements
90
+
91
+ - Node.js 18+
92
+ - Linux (Ubuntu/Debian) for `postgres setup` — macOS/Windows later
93
+ - `sudo` for package install and postgres admin commands
94
+ - OpenAI key only for AI diagnosis in `doctor`
95
+
96
+ ## Generated projects
97
+
98
+ Every Becky ZIP includes `project-spec.json`. Point the CLI at that folder:
99
+
100
+ ```bash
101
+ npx becky-cli up ./path/to/unzipped-project
102
+ ```
@@ -0,0 +1,17 @@
1
+ export type DoctorAiInput = {
2
+ projectName: string;
3
+ stack: string;
4
+ checks: {
5
+ id: string;
6
+ ok: boolean;
7
+ detail: string;
8
+ }[];
9
+ apiKey?: string;
10
+ model?: string;
11
+ };
12
+ export type DoctorAiAdvice = {
13
+ summary: string;
14
+ suggestedCommands: string[];
15
+ notes: string[];
16
+ };
17
+ export declare function askDoctorAdvice(input: DoctorAiInput): Promise<DoctorAiAdvice>;
@@ -0,0 +1,67 @@
1
+ import { resolveOpenAiKey, loadConfig } from "../config/store.js";
2
+ export async function askDoctorAdvice(input) {
3
+ const apiKey = resolveOpenAiKey(input.apiKey);
4
+ if (!apiKey) {
5
+ throw new Error("OpenAI API key required for AI diagnosis. Set OPENAI_API_KEY, run `becky config set-key`, or pass --api-key.");
6
+ }
7
+ const model = input.model || loadConfig().defaultModel || "gpt-4o-mini";
8
+ const failed = input.checks.filter((c) => !c.ok);
9
+ const passed = input.checks.filter((c) => c.ok);
10
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
11
+ method: "POST",
12
+ headers: {
13
+ Authorization: `Bearer ${apiKey}`,
14
+ "Content-Type": "application/json",
15
+ },
16
+ body: JSON.stringify({
17
+ model,
18
+ response_format: { type: "json_object" },
19
+ temperature: 0.2,
20
+ max_tokens: 1200,
21
+ messages: [
22
+ {
23
+ role: "system",
24
+ content: `You are Becky doctor — a local backend/Postgres troubleshooter.
25
+ Return ONLY JSON:
26
+ {
27
+ "summary": "1-3 sentences",
28
+ "suggestedCommands": ["shell commands the user can run"],
29
+ "notes": ["short tips"]
30
+ }
31
+ Prefer becky CLI commands (postgres setup, connect, migrate, up) and standard stack tools.
32
+ Do not invent credentials. Keep commands safe for local Ubuntu/Debian.`,
33
+ },
34
+ {
35
+ role: "user",
36
+ content: `Project: ${input.projectName}
37
+ Stack: ${input.stack}
38
+
39
+ Passed checks:
40
+ ${passed.map((c) => `- ${c.id}: ${c.detail}`).join("\n") || "(none)"}
41
+
42
+ Failed checks:
43
+ ${failed.map((c) => `- ${c.id}: ${c.detail}`).join("\n") || "(none)"}
44
+
45
+ Suggest concrete next steps.`,
46
+ },
47
+ ],
48
+ }),
49
+ });
50
+ const data = (await res.json());
51
+ if (!res.ok) {
52
+ throw new Error(data.error?.message || `OpenAI request failed (${res.status})`);
53
+ }
54
+ const content = data.choices?.[0]?.message?.content;
55
+ if (!content)
56
+ throw new Error("Empty response from OpenAI");
57
+ const parsed = JSON.parse(content);
58
+ return {
59
+ summary: typeof parsed.summary === "string" ? parsed.summary : "AI diagnosis complete.",
60
+ suggestedCommands: Array.isArray(parsed.suggestedCommands)
61
+ ? parsed.suggestedCommands.filter((c) => typeof c === "string")
62
+ : [],
63
+ notes: Array.isArray(parsed.notes)
64
+ ? parsed.notes.filter((n) => typeof n === "string")
65
+ : [],
66
+ };
67
+ }
@@ -0,0 +1,3 @@
1
+ export declare function runConfigSetKey(apiKey?: string): Promise<void>;
2
+ export declare function runConfigShow(): void;
3
+ export declare function runConfigSetModel(model: string): void;
@@ -0,0 +1,30 @@
1
+ import { loadConfig, resolveOpenAiKey, saveConfig, getConfigDir } from "../config/store.js";
2
+ import { promptHidden } from "../util/prompt.js";
3
+ import { detail, info, success, warn } from "../util/log.js";
4
+ export async function runConfigSetKey(apiKey) {
5
+ let key = apiKey?.trim();
6
+ if (!key) {
7
+ key = await promptHidden("OpenAI API key (sk-...)");
8
+ }
9
+ if (!key) {
10
+ throw new Error("API key is required");
11
+ }
12
+ saveConfig({ openaiApiKey: key });
13
+ success(`Saved OpenAI key to ${getConfigDir()}/config.json`);
14
+ detail("Used by: becky doctor (and future AI commands)");
15
+ info("You can also use OPENAI_API_KEY in the environment instead.");
16
+ }
17
+ export function runConfigShow() {
18
+ const config = loadConfig();
19
+ const key = resolveOpenAiKey();
20
+ console.log(`Config dir: ${getConfigDir()}`);
21
+ console.log(`OpenAI key: ${key ? `${key.slice(0, 7)}…${key.slice(-4)}` : "(not set)"}`);
22
+ console.log(`Default model: ${config.defaultModel || "gpt-4o-mini"}`);
23
+ if (!key) {
24
+ warn("Set a key with: becky config set-key");
25
+ }
26
+ }
27
+ export function runConfigSetModel(model) {
28
+ saveConfig({ defaultModel: model.trim() });
29
+ success(`Default model set to ${model.trim()}`);
30
+ }
@@ -0,0 +1,11 @@
1
+ export type ConnectOptions = {
2
+ dir: string;
3
+ user?: string;
4
+ password?: string;
5
+ database?: string;
6
+ host?: string;
7
+ port?: number;
8
+ force?: boolean;
9
+ skipTest?: boolean;
10
+ };
11
+ export declare function runConnect(options: ConnectOptions): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { saveCredentials } from "../config/store.js";
2
+ import { resolveConnection, testDatabaseConnection, writeEnvFile } from "../project/env.js";
3
+ import { loadProject } from "../project/load.js";
4
+ import { detail, info, step, success } from "../util/log.js";
5
+ export async function runConnect(options) {
6
+ const project = loadProject(options.dir);
7
+ step(`Becky — connect (${project.spec.projectName})`);
8
+ detail(`Project: ${project.root}`);
9
+ detail(`Stack: ${project.spec.stack}`);
10
+ const connection = await resolveConnection(project, {
11
+ user: options.user,
12
+ password: options.password,
13
+ database: options.database,
14
+ host: options.host,
15
+ port: options.port,
16
+ });
17
+ writeEnvFile(project, connection, options.force);
18
+ saveCredentials({
19
+ host: connection.host,
20
+ port: connection.port,
21
+ user: connection.user,
22
+ password: connection.password,
23
+ database: connection.database,
24
+ databaseUrl: connection.databaseUrl,
25
+ });
26
+ info("Saved credentials to ~/.becky/credentials.json");
27
+ if (!options.skipTest) {
28
+ await testDatabaseConnection(connection);
29
+ }
30
+ step("Done");
31
+ success("Backend is wired to Postgres via .env");
32
+ console.log("");
33
+ console.log(`DATABASE_URL=${connection.databaseUrl}`);
34
+ console.log("");
35
+ info("Next: becky migrate . (or becky up .)");
36
+ }
@@ -0,0 +1,10 @@
1
+ import { type LoadedProject } from "../project/load.js";
2
+ export type DoctorOptions = {
3
+ dir: string;
4
+ apiKey?: string;
5
+ model?: string;
6
+ noAi?: boolean;
7
+ };
8
+ export declare function runDoctor(options: DoctorOptions): Promise<void>;
9
+ /** Read DATABASE_URL from stack or project .env (Prisma uses stackRoot/.env). */
10
+ export declare function peekEnvDatabaseUrl(project: LoadedProject): string | null;
@@ -0,0 +1,166 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { askDoctorAdvice } from "../ai/openai.js";
4
+ import { resolveOpenAiKey } from "../config/store.js";
5
+ import { getPostgresStatus, databaseExists } from "../postgres/admin.js";
6
+ import { resolveConnection, testDatabaseConnection, envFilePaths } from "../project/env.js";
7
+ import { loadProject } from "../project/load.js";
8
+ import { run } from "../util/exec.js";
9
+ import { detail, error, info, step, success, warn } from "../util/log.js";
10
+ async function checkHealthEndpoint(projectRoot, stack) {
11
+ const ports = stack === "fastapi-postgres" ? [8000, 3000] : [3000, 8000];
12
+ for (const port of ports) {
13
+ const result = await run("curl", ["-sf", `http://127.0.0.1:${port}/health`]);
14
+ if (result.code === 0) {
15
+ return { id: "api-health", ok: true, detail: `GET :${port}/health responded` };
16
+ }
17
+ }
18
+ return {
19
+ id: "api-health",
20
+ ok: false,
21
+ detail: "API /health not reachable (start the server from the project README)",
22
+ };
23
+ }
24
+ export async function runDoctor(options) {
25
+ const project = loadProject(options.dir);
26
+ const checks = [];
27
+ step(`Becky — doctor (${project.spec.projectName})`);
28
+ detail(`Stack: ${project.spec.stack}`);
29
+ const pg = await getPostgresStatus();
30
+ checks.push({
31
+ id: "postgres-installed",
32
+ ok: pg.installed,
33
+ detail: pg.installed ? pg.version || "psql found" : "PostgreSQL / psql not found",
34
+ });
35
+ checks.push({
36
+ id: "postgres-service",
37
+ ok: pg.serviceActive,
38
+ detail: pg.serviceActive ? "postgresql service active" : "postgresql service not running",
39
+ });
40
+ const envPaths = envFilePaths(project);
41
+ const hasEnv = envPaths.some((p) => existsSync(p));
42
+ checks.push({
43
+ id: "env-file",
44
+ ok: hasEnv,
45
+ detail: hasEnv
46
+ ? `.env present (${envPaths.filter((p) => existsSync(p)).join(", ")})`
47
+ : `.env missing — run becky connect .`,
48
+ });
49
+ let dbOk = false;
50
+ try {
51
+ if (pg.installed) {
52
+ const connection = await resolveConnection(project, { promptIfMissing: false });
53
+ const exists = await databaseExists(connection.database);
54
+ checks.push({
55
+ id: "database-exists",
56
+ ok: exists,
57
+ detail: exists
58
+ ? `Database "${connection.database}" exists`
59
+ : `Database "${connection.database}" missing — run becky postgres setup`,
60
+ });
61
+ if (hasEnv) {
62
+ try {
63
+ await testDatabaseConnection(connection);
64
+ dbOk = true;
65
+ checks.push({ id: "database-connect", ok: true, detail: "DATABASE_URL connection OK" });
66
+ }
67
+ catch (err) {
68
+ checks.push({
69
+ id: "database-connect",
70
+ ok: false,
71
+ detail: err instanceof Error ? err.message : String(err),
72
+ });
73
+ }
74
+ }
75
+ }
76
+ }
77
+ catch (err) {
78
+ checks.push({
79
+ id: "credentials",
80
+ ok: false,
81
+ detail: err instanceof Error ? err.message : String(err),
82
+ });
83
+ }
84
+ const health = await checkHealthEndpoint(project.root, project.spec.stack);
85
+ checks.push(health);
86
+ // Optional: look for migration stubs still TODO
87
+ if (project.spec.outputs?.migrations !== false) {
88
+ const migrateScript = join(project.root, "scripts", "migrate.sh");
89
+ checks.push({
90
+ id: "migrate-script",
91
+ ok: existsSync(migrateScript),
92
+ detail: existsSync(migrateScript)
93
+ ? "scripts/migrate.sh present"
94
+ : "No scripts/migrate.sh (migrations may be stubs)",
95
+ });
96
+ }
97
+ console.log("");
98
+ for (const check of checks) {
99
+ if (check.ok)
100
+ success(`${check.id}: ${check.detail}`);
101
+ else
102
+ error(`${check.id}: ${check.detail}`);
103
+ }
104
+ const failed = checks.filter((c) => !c.ok);
105
+ if (failed.length === 0) {
106
+ step("Healthy");
107
+ success("All checks passed");
108
+ if (dbOk)
109
+ detail("Database is connected and reachable");
110
+ return;
111
+ }
112
+ step(`${failed.length} issue(s) found`);
113
+ if (options.noAi) {
114
+ warn("Skipping AI diagnosis (--skip-ai)");
115
+ info("Try: becky up .");
116
+ return;
117
+ }
118
+ if (!resolveOpenAiKey(options.apiKey)) {
119
+ warn("No OpenAI API key — skipping AI diagnosis");
120
+ info("Set a key with: becky config set-key");
121
+ info("Or: export OPENAI_API_KEY=sk-...");
122
+ info("Then re-run: becky doctor .");
123
+ info("Quick fix: becky up .");
124
+ return;
125
+ }
126
+ info("Asking OpenAI for fix suggestions…");
127
+ try {
128
+ const advice = await askDoctorAdvice({
129
+ projectName: project.spec.projectName,
130
+ stack: project.spec.stack,
131
+ checks,
132
+ apiKey: options.apiKey,
133
+ model: options.model,
134
+ });
135
+ console.log("");
136
+ console.log(advice.summary);
137
+ if (advice.suggestedCommands.length) {
138
+ console.log("\nSuggested commands:");
139
+ for (const cmd of advice.suggestedCommands) {
140
+ console.log(` ${cmd}`);
141
+ }
142
+ }
143
+ if (advice.notes.length) {
144
+ console.log("\nNotes:");
145
+ for (const note of advice.notes) {
146
+ console.log(` - ${note}`);
147
+ }
148
+ }
149
+ }
150
+ catch (err) {
151
+ error(err instanceof Error ? err.message : String(err));
152
+ info("Fallback: becky up .");
153
+ }
154
+ }
155
+ /** Read DATABASE_URL from stack or project .env (Prisma uses stackRoot/.env). */
156
+ export function peekEnvDatabaseUrl(project) {
157
+ for (const envPath of envFilePaths(project)) {
158
+ if (!existsSync(envPath))
159
+ continue;
160
+ const match = readFileSync(envPath, "utf8").match(/^DATABASE_URL=(.+)$/m);
161
+ const url = match?.[1]?.trim();
162
+ if (url)
163
+ return url;
164
+ }
165
+ return null;
166
+ }
@@ -0,0 +1,9 @@
1
+ export type MigrateOptions = {
2
+ dir: string;
3
+ user?: string;
4
+ password?: string;
5
+ database?: string;
6
+ host?: string;
7
+ port?: number;
8
+ };
9
+ export declare function runMigrate(options: MigrateOptions): Promise<void>;
@@ -0,0 +1,208 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { loadCredentials } from "../config/store.js";
4
+ import { resolveConnection } from "../project/env.js";
5
+ import { ensureAlembicScaffold } from "../project/alembicScaffold.js";
6
+ import { FASTAPI_REQUIREMENTS, isLegacyFastapiRequirements } from "../project/fastapiPins.js";
7
+ import { loadProject } from "../project/load.js";
8
+ import { run, runOrThrow } from "../util/exec.js";
9
+ import { detail, info, step, success, warn } from "../util/log.js";
10
+ function combinedOutput(result) {
11
+ return [result.stderr, result.stdout].filter(Boolean).join("\n");
12
+ }
13
+ function isPrismaBinaryTargetError(text) {
14
+ return /Unknown binaryTarget|debian-openssl-3\.\d+\.x|no custom engine files/i.test(text);
15
+ }
16
+ function pythonBin(project) {
17
+ const venvPython = join(project.stackRoot, ".venv", "bin", "python");
18
+ return existsSync(venvPython) ? venvPython : "python3";
19
+ }
20
+ async function ensurePythonDepsInstalled(project) {
21
+ const requirementsPath = join(project.stackRoot, "requirements.txt");
22
+ if (!existsSync(requirementsPath)) {
23
+ writeFileSync(requirementsPath, `${FASTAPI_REQUIREMENTS}\n`, "utf8");
24
+ info(`Created ${requirementsPath}`);
25
+ }
26
+ else {
27
+ const current = readFileSync(requirementsPath, "utf8");
28
+ if (isLegacyFastapiRequirements(current)) {
29
+ writeFileSync(requirementsPath, `${FASTAPI_REQUIREMENTS}\n`, "utf8");
30
+ warn("Replaced legacy Python pins in requirements.txt with compatible versions");
31
+ }
32
+ }
33
+ const venvDir = join(project.stackRoot, ".venv");
34
+ if (!existsSync(venvDir)) {
35
+ info("Creating Python venv in backend/");
36
+ const venvResult = await run("python3", ["-m", "venv", ".venv"], { cwd: project.stackRoot });
37
+ if (venvResult.code !== 0) {
38
+ const out = combinedOutput(venvResult);
39
+ if (/ensurepip|python3-venv/i.test(out)) {
40
+ throw new Error(`venv creation failed — install Python venv support:\n sudo apt install python3-venv python3-pip\n${out}`);
41
+ }
42
+ throw new Error(`venv creation failed:\n${out}`);
43
+ }
44
+ }
45
+ const pip = join(project.stackRoot, ".venv", "bin", "pip");
46
+ const pipCmd = existsSync(pip) ? pip : "pip3";
47
+ info("Installing Python dependencies (pip)");
48
+ await runOrThrow(pipCmd, ["install", "--upgrade", "pip"], { cwd: project.stackRoot }, "pip upgrade failed");
49
+ await runOrThrow(pipCmd, ["install", "-r", "requirements.txt"], { cwd: project.stackRoot }, "pip install failed");
50
+ writeFileSync(join(project.stackRoot, ".venv", ".becky-deps"), new Date().toISOString(), "utf8");
51
+ }
52
+ async function ensureDepsInstalled(project) {
53
+ if (project.spec.stack === "fastapi-postgres") {
54
+ await ensurePythonDepsInstalled(project);
55
+ return;
56
+ }
57
+ const packageJson = join(project.stackRoot, "package.json");
58
+ const nodeModules = join(project.stackRoot, "node_modules");
59
+ if (!existsSync(packageJson)) {
60
+ warn(`No package.json at ${project.stackRoot} — skipping npm install`);
61
+ return;
62
+ }
63
+ if (existsSync(nodeModules)) {
64
+ detail("Dependencies already installed");
65
+ return;
66
+ }
67
+ info(`Installing npm dependencies in ${project.stackRoot}`);
68
+ await runOrThrow("npm", ["install"], { cwd: project.stackRoot }, "npm install failed");
69
+ }
70
+ /** Ubuntu/Debian OpenSSL 3.5+ often needs an explicit 3.0.x engine target on Prisma 6. */
71
+ function patchPrismaBinaryTargets(project) {
72
+ const schemaPath = join(project.stackRoot, "prisma", "schema.prisma");
73
+ if (!existsSync(schemaPath))
74
+ return false;
75
+ let schema = readFileSync(schemaPath, "utf8");
76
+ if (schema.includes("binaryTargets"))
77
+ return false;
78
+ const patched = schema.replace(/generator\s+client\s*\{([^}]*)\}/m, (block, inner) => {
79
+ if (String(inner).includes("binaryTargets"))
80
+ return block;
81
+ return `generator client {${inner.trimEnd()}\n binaryTargets = ["native", "debian-openssl-3.0.x"]\n}`;
82
+ });
83
+ if (patched === schema)
84
+ return false;
85
+ writeFileSync(schemaPath, patched, "utf8");
86
+ info('Patched prisma/schema.prisma with binaryTargets ["native", "debian-openssl-3.0.x"]');
87
+ return true;
88
+ }
89
+ async function upgradePrisma(project) {
90
+ info("Upgrading Prisma to latest 6.x (OpenSSL 3.5 compatibility)");
91
+ await runOrThrow("npm", ["install", "prisma@^6.19.0", "@prisma/client@^6.19.0"], { cwd: project.stackRoot }, "Failed to upgrade Prisma");
92
+ }
93
+ async function prismaDbPush(project, env) {
94
+ return run("npx", ["prisma", "db", "push", "--accept-data-loss"], {
95
+ cwd: project.stackRoot,
96
+ env,
97
+ });
98
+ }
99
+ async function migrateExpress(project, databaseUrl) {
100
+ await ensureDepsInstalled(project);
101
+ const env = { ...process.env, DATABASE_URL: databaseUrl };
102
+ // Prefer db push for scaffold stubs (empty/TODO migrations often break migrate deploy)
103
+ info("Running Prisma db push");
104
+ let push = await prismaDbPush(project, env);
105
+ if (push.code === 0) {
106
+ success("Prisma schema pushed to database");
107
+ return;
108
+ }
109
+ let output = combinedOutput(push);
110
+ detail(output);
111
+ if (isPrismaBinaryTargetError(output)) {
112
+ warn("Prisma OpenSSL/binary target mismatch — applying workaround");
113
+ patchPrismaBinaryTargets(project);
114
+ await upgradePrisma(project);
115
+ info("Regenerating Prisma client");
116
+ await run("npx", ["prisma", "generate"], { cwd: project.stackRoot, env });
117
+ info("Retrying Prisma db push");
118
+ push = await prismaDbPush(project, env);
119
+ if (push.code === 0) {
120
+ success("Prisma schema pushed to database");
121
+ return;
122
+ }
123
+ output = combinedOutput(push);
124
+ }
125
+ // Last resort: migrate deploy
126
+ warn("db push failed — trying prisma migrate deploy");
127
+ detail(output);
128
+ const deploy = await run("npx", ["prisma", "migrate", "deploy"], {
129
+ cwd: project.stackRoot,
130
+ env,
131
+ });
132
+ if (deploy.code === 0) {
133
+ success("Prisma migrations applied");
134
+ return;
135
+ }
136
+ throw new Error(`Prisma migrate failed:\n${combinedOutput(deploy)}\n\nManual fix:\n cd ${project.stackRoot}\n npm install prisma@latest @prisma/client@latest\n npx prisma db push`);
137
+ }
138
+ async function migrateFastapi(project, databaseUrl) {
139
+ await ensurePythonDepsInstalled(project);
140
+ ensureAlembicScaffold(project);
141
+ const env = { ...process.env, DATABASE_URL: databaseUrl };
142
+ const py = pythonBin(project);
143
+ info("Running alembic upgrade head");
144
+ const result = await run(py, ["-m", "alembic", "upgrade", "head"], {
145
+ cwd: project.stackRoot,
146
+ env,
147
+ });
148
+ if (result.code === 0) {
149
+ success("Alembic migrations applied");
150
+ return;
151
+ }
152
+ const output = combinedOutput(result);
153
+ throw new Error(`Alembic migrate failed:\n${output}\n\nManual fix:\n cd ${project.stackRoot}\n source .venv/bin/activate\n export DATABASE_URL='...'\n python -m alembic upgrade head`);
154
+ }
155
+ async function migrateNest(project, databaseUrl) {
156
+ await ensureDepsInstalled(project);
157
+ const env = { ...process.env, DATABASE_URL: databaseUrl };
158
+ const script = join(project.root, "scripts", "migrate.sh");
159
+ const packageJson = join(project.stackRoot, "package.json");
160
+ if (existsSync(packageJson)) {
161
+ info("Trying npm run migration:run (if defined)");
162
+ const result = await run("npm", ["run", "migration:run", "--if-present"], {
163
+ cwd: project.stackRoot,
164
+ env,
165
+ });
166
+ if (result.code === 0 && !result.stdout.includes("Missing script")) {
167
+ success("Nest migrations applied");
168
+ return;
169
+ }
170
+ }
171
+ if (existsSync(script)) {
172
+ info("Running scripts/migrate.sh");
173
+ await runOrThrow("bash", [script], { cwd: project.root, env }, "migrate.sh failed");
174
+ success("Migration script finished");
175
+ return;
176
+ }
177
+ warn("No Nest migration command found — scaffold may still be stubs");
178
+ }
179
+ export async function runMigrate(options) {
180
+ const project = loadProject(options.dir);
181
+ const connection = await resolveConnection(project, {
182
+ user: options.user,
183
+ password: options.password,
184
+ database: options.database,
185
+ host: options.host,
186
+ port: options.port,
187
+ promptIfMissing: !loadCredentials(),
188
+ });
189
+ step(`Becky — migrate (${project.spec.projectName})`);
190
+ detail(`Stack: ${project.spec.stack}`);
191
+ detail(`Root: ${project.stackRoot}`);
192
+ if (!project.spec.outputs?.migrations) {
193
+ warn("This project was generated without migrations output enabled");
194
+ }
195
+ switch (project.spec.stack) {
196
+ case "express-postgres":
197
+ await migrateExpress(project, connection.databaseUrl);
198
+ break;
199
+ case "fastapi-postgres":
200
+ await migrateFastapi(project, connection.databaseUrl);
201
+ break;
202
+ case "nest-postgres":
203
+ await migrateNest(project, connection.databaseUrl);
204
+ break;
205
+ }
206
+ step("Done");
207
+ success("Migration step finished");
208
+ }
@@ -0,0 +1,12 @@
1
+ export type PostgresSetupOptions = {
2
+ /** When omitted, derived from --spec project name or database name */
3
+ user?: string;
4
+ password?: string;
5
+ database?: string;
6
+ host: string;
7
+ port: number;
8
+ yes: boolean;
9
+ specPath?: string;
10
+ };
11
+ export declare function runPostgresSetup(options: PostgresSetupOptions): Promise<void>;
12
+ export declare function printUnsupportedPlatform(): void;