@solcreek/cli 0.4.29 → 0.4.31

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.
@@ -8,6 +8,7 @@ import { CreekClient, CreekAuthError, detectFramework, resolveConfig, formatDete
8
8
  import { buildDoctorContext } from "../utils/doctor-context.js";
9
9
  import { getToken, getApiUrl } from "../utils/config.js";
10
10
  import { collectAssets } from "../utils/bundle.js";
11
+ import { runDatabasePreflight, makePreflightIO, readProjectDeps } from "../utils/db-preflight.js";
11
12
  import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess, expiresInMinutes } from "../utils/sandbox.js";
12
13
  import { prepareDeployBundle } from "../utils/prepare-bundle.js";
13
14
  import { BuildLogEmitter } from "../utils/build-log.js";
@@ -345,6 +346,19 @@ export const deployCommand = defineCommand({
345
346
  throw err;
346
347
  }
347
348
  if (resolved) {
349
+ // Database deploy preflight: when the app uses Prisma/Drizzle on SQLite
350
+ // but creek.toml hasn't declared a database, confirm provisioning a D1
351
+ // (a cloud instance, separate from the local file) and re-resolve so the
352
+ // binding is included in this deploy. Confirm-first; no-op when already
353
+ // decided or non-interactive without --yes.
354
+ const dbPreflight = await runDatabasePreflight({
355
+ deps: readProjectDeps(cwd),
356
+ projectName: resolved.projectName,
357
+ tty: isTTY && !jsonMode,
358
+ autoYes: shouldAutoConfirm(args),
359
+ }, makePreflightIO(cwd));
360
+ if (dbPreflight.wroteToml)
361
+ resolved = resolveConfig(cwd);
348
362
  if (!jsonMode) {
349
363
  consola.info(` Detected: ${formatDetectionSummary(resolved)}`);
350
364
  for (const ub of resolved.unsupportedBindings) {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Database deploy preflight: detect → confirm helpers.
3
+ *
4
+ * An app that uses Prisma or Drizzle on local SQLite runs on a Cloudflare D1
5
+ * database on Creek (a separate cloud instance from the local file). Getting
6
+ * there needs `database = true` under `[resources]` in creek.toml and, for
7
+ * Prisma, a generated client. These pure helpers detect that situation and
8
+ * patch creek.toml; the actual prompts, file writes, and `creek db migrate`
9
+ * are wired by the deploy command so the consequential steps stay behind
10
+ * explicit consent (see the deploy flow).
11
+ *
12
+ * Design split by risk: client generation is safe to automate; provisioning a
13
+ * (billed) database is confirm-first; applying migrations is never silent.
14
+ *
15
+ * Pure functions are exported for testing.
16
+ */
17
+ export type SqliteOrm = "prisma" | "drizzle";
18
+ /**
19
+ * Detect an ORM configured against local SQLite — the case Creek swaps onto
20
+ * D1. The signal is the presence of the better-sqlite3 driver/adapter the
21
+ * adapter-creek build-time swap targets:
22
+ * - Prisma: `@prisma/adapter-better-sqlite3`
23
+ * - Drizzle: `drizzle-orm` + `better-sqlite3`
24
+ *
25
+ * Returns null when neither is present (e.g. an app on an external DB, or
26
+ * Drizzle pointed straight at `drizzle-orm/d1`), so we never guess a database
27
+ * the project didn't ask for.
28
+ */
29
+ export declare function detectSqliteOrm(deps: Record<string, string | undefined>): SqliteOrm | null;
30
+ export type DatabaseDirective = "enabled" | "disabled" | "absent";
31
+ /**
32
+ * Read the `[resources].database` directive from raw creek.toml text — NOT the
33
+ * SDK-parsed config, because the parser defaults `database` to `false` and so
34
+ * can't tell "explicitly opted out" from "never decided". The distinction
35
+ * drives whether we may prompt:
36
+ * - "enabled" → already on, nothing to do
37
+ * - "disabled" → explicit opt-out, never prompt
38
+ * - "absent" → undecided, eligible to prompt
39
+ */
40
+ export declare function databaseDirectiveState(rawToml: string | null): DatabaseDirective;
41
+ /**
42
+ * Patch raw creek.toml to enable the database resource, preserving existing
43
+ * content (so the file stays reviewable). Only called when the directive is
44
+ * "absent". Handles: no file, `[resources]` present (insert the key), and
45
+ * `[resources]` missing (append the section, prepending `[project]` when the
46
+ * file has none so the result is a valid creek.toml).
47
+ */
48
+ export declare function enableDatabaseResource(rawToml: string | null, projectName: string): string;
49
+ /**
50
+ * Whether the project uses Prisma and its client hasn't been generated yet.
51
+ * Safe to act on automatically (generation is idempotent, no external effect).
52
+ * Conservative: only true when a schema exists, declares an `output`, and that
53
+ * directory is missing — otherwise we leave the user's setup untouched.
54
+ */
55
+ export declare function prismaNeedsGenerate(cwd: string): boolean;
56
+ /**
57
+ * Side-effect surface for the database preflight, injected so the decision flow
58
+ * is testable without a real terminal or filesystem. `confirm` resolves the
59
+ * interactive yes/no; `readToml`/`writeToml` are the raw creek.toml.
60
+ */
61
+ export interface PreflightIO {
62
+ readToml(): string | null;
63
+ writeToml(content: string): void;
64
+ confirm(message: string): Promise<boolean>;
65
+ log(message: string): void;
66
+ warn(message: string): void;
67
+ }
68
+ export interface PreflightOptions {
69
+ deps: Record<string, string | undefined>;
70
+ projectName: string;
71
+ /** Interactive terminal — may prompt. */
72
+ tty: boolean;
73
+ /** `--yes`: auto-accept the (low-risk) DB-resource provisioning. */
74
+ autoYes: boolean;
75
+ }
76
+ export interface PreflightResult {
77
+ /** creek.toml was patched — the caller must re-resolve config before deploy. */
78
+ wroteToml: boolean;
79
+ }
80
+ /**
81
+ * Confirm-first database provisioning. When the project uses an ORM on SQLite
82
+ * but creek.toml hasn't decided on a database resource, prompt (or auto-accept
83
+ * under --yes) to add `database = true`. Never overrides an explicit opt-out,
84
+ * never prompts in non-interactive contexts (warns and continues — the deploy
85
+ * still succeeds and DB routes return a self-documenting hint at runtime).
86
+ */
87
+ export declare function runDatabasePreflight(opts: PreflightOptions, io: PreflightIO): Promise<PreflightResult>;
88
+ /**
89
+ * What to do about a detected migration directory after the database exists.
90
+ * Migrations can be destructive, so this is never silent:
91
+ * - "run" → explicit opt-in (`--migrate`)
92
+ * - "prompt" → interactive (ask, default no)
93
+ * - "suggest" → non-interactive (print the command, don't run)
94
+ * - "none" → no migrations to apply
95
+ */
96
+ export declare function migrationOfferPlan(opts: {
97
+ migrationDir: string | null;
98
+ tty: boolean;
99
+ autoMigrate: boolean;
100
+ }): "run" | "prompt" | "suggest" | "none";
101
+ /** Merged dependencies + devDependencies from the project's package.json. */
102
+ export declare function readProjectDeps(cwd: string): Record<string, string | undefined>;
103
+ /** Real (consola + filesystem) PreflightIO for the deploy command. */
104
+ export declare function makePreflightIO(cwd: string): PreflightIO;
105
+ //# sourceMappingURL=db-preflight.d.ts.map
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Database deploy preflight: detect → confirm helpers.
3
+ *
4
+ * An app that uses Prisma or Drizzle on local SQLite runs on a Cloudflare D1
5
+ * database on Creek (a separate cloud instance from the local file). Getting
6
+ * there needs `database = true` under `[resources]` in creek.toml and, for
7
+ * Prisma, a generated client. These pure helpers detect that situation and
8
+ * patch creek.toml; the actual prompts, file writes, and `creek db migrate`
9
+ * are wired by the deploy command so the consequential steps stay behind
10
+ * explicit consent (see the deploy flow).
11
+ *
12
+ * Design split by risk: client generation is safe to automate; provisioning a
13
+ * (billed) database is confirm-first; applying migrations is never silent.
14
+ *
15
+ * Pure functions are exported for testing.
16
+ */
17
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import consola from "consola";
20
+ /**
21
+ * Detect an ORM configured against local SQLite — the case Creek swaps onto
22
+ * D1. The signal is the presence of the better-sqlite3 driver/adapter the
23
+ * adapter-creek build-time swap targets:
24
+ * - Prisma: `@prisma/adapter-better-sqlite3`
25
+ * - Drizzle: `drizzle-orm` + `better-sqlite3`
26
+ *
27
+ * Returns null when neither is present (e.g. an app on an external DB, or
28
+ * Drizzle pointed straight at `drizzle-orm/d1`), so we never guess a database
29
+ * the project didn't ask for.
30
+ */
31
+ export function detectSqliteOrm(deps) {
32
+ if (deps["@prisma/adapter-better-sqlite3"])
33
+ return "prisma";
34
+ if (deps["drizzle-orm"] && deps["better-sqlite3"])
35
+ return "drizzle";
36
+ return null;
37
+ }
38
+ /**
39
+ * Read the `[resources].database` directive from raw creek.toml text — NOT the
40
+ * SDK-parsed config, because the parser defaults `database` to `false` and so
41
+ * can't tell "explicitly opted out" from "never decided". The distinction
42
+ * drives whether we may prompt:
43
+ * - "enabled" → already on, nothing to do
44
+ * - "disabled" → explicit opt-out, never prompt
45
+ * - "absent" → undecided, eligible to prompt
46
+ */
47
+ export function databaseDirectiveState(rawToml) {
48
+ if (!rawToml)
49
+ return "absent";
50
+ let inResources = false;
51
+ for (const rawLine of rawToml.split(/\r?\n/)) {
52
+ const line = rawLine.trim();
53
+ if (line.startsWith("[")) {
54
+ // A new table header. `[resources]` exactly; `[resources.x]` is a
55
+ // different (sub)table and doesn't carry the boolean directive.
56
+ inResources = line === "[resources]";
57
+ continue;
58
+ }
59
+ if (!inResources)
60
+ continue;
61
+ const m = line.match(/^database\s*=\s*(true|false)\b/);
62
+ if (m)
63
+ return m[1] === "true" ? "enabled" : "disabled";
64
+ }
65
+ return "absent";
66
+ }
67
+ /**
68
+ * Patch raw creek.toml to enable the database resource, preserving existing
69
+ * content (so the file stays reviewable). Only called when the directive is
70
+ * "absent". Handles: no file, `[resources]` present (insert the key), and
71
+ * `[resources]` missing (append the section, prepending `[project]` when the
72
+ * file has none so the result is a valid creek.toml).
73
+ */
74
+ export function enableDatabaseResource(rawToml, projectName) {
75
+ const resourcesBlock = "[resources]\ndatabase = true\n";
76
+ if (rawToml === null || rawToml.trim() === "") {
77
+ return `[project]\nname = "${projectName}"\n\n${resourcesBlock}`;
78
+ }
79
+ const lines = rawToml.split(/\r?\n/);
80
+ const resourcesIdx = lines.findIndex((l) => l.trim() === "[resources]");
81
+ if (resourcesIdx !== -1) {
82
+ // Insert the key directly under the existing header.
83
+ lines.splice(resourcesIdx + 1, 0, "database = true");
84
+ return lines.join("\n");
85
+ }
86
+ // No [resources] table — append one, ensuring a [project] table exists.
87
+ const hasProject = lines.some((l) => l.trim() === "[project]");
88
+ const trimmed = rawToml.replace(/\s*$/, "");
89
+ const prefix = hasProject ? "" : `[project]\nname = "${projectName}"\n\n`;
90
+ const sep = trimmed === "" ? "" : "\n\n";
91
+ return `${prefix}${trimmed}${sep}${resourcesBlock}`;
92
+ }
93
+ /**
94
+ * Read the generator `output` path from a Prisma schema (Prisma 7's
95
+ * `prisma-client` generator requires one). Returns an absolute path or null.
96
+ */
97
+ function prismaClientOutput(cwd, schemaPath) {
98
+ let schema;
99
+ try {
100
+ schema = readFileSync(schemaPath, "utf-8");
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ const m = schema.match(/\boutput\s*=\s*"([^"]+)"/);
106
+ if (!m)
107
+ return null;
108
+ const out = m[1];
109
+ return out.startsWith("/") ? out : join(cwd, "prisma", out);
110
+ }
111
+ /**
112
+ * Whether the project uses Prisma and its client hasn't been generated yet.
113
+ * Safe to act on automatically (generation is idempotent, no external effect).
114
+ * Conservative: only true when a schema exists, declares an `output`, and that
115
+ * directory is missing — otherwise we leave the user's setup untouched.
116
+ */
117
+ export function prismaNeedsGenerate(cwd) {
118
+ const schemaPath = join(cwd, "prisma", "schema.prisma");
119
+ if (!existsSync(schemaPath))
120
+ return false;
121
+ const out = prismaClientOutput(cwd, schemaPath);
122
+ if (!out)
123
+ return false;
124
+ return !existsSync(out);
125
+ }
126
+ const ORM_LABEL = {
127
+ prisma: "Prisma",
128
+ drizzle: "Drizzle",
129
+ };
130
+ /**
131
+ * Confirm-first database provisioning. When the project uses an ORM on SQLite
132
+ * but creek.toml hasn't decided on a database resource, prompt (or auto-accept
133
+ * under --yes) to add `database = true`. Never overrides an explicit opt-out,
134
+ * never prompts in non-interactive contexts (warns and continues — the deploy
135
+ * still succeeds and DB routes return a self-documenting hint at runtime).
136
+ */
137
+ export async function runDatabasePreflight(opts, io) {
138
+ const orm = detectSqliteOrm(opts.deps);
139
+ if (!orm)
140
+ return { wroteToml: false };
141
+ const state = databaseDirectiveState(io.readToml());
142
+ if (state !== "absent")
143
+ return { wroteToml: false }; // already enabled or opted out
144
+ if (!opts.tty && !opts.autoYes) {
145
+ io.warn(`Detected ${ORM_LABEL[orm]} on better-sqlite3 but creek.toml has no [resources] database. ` +
146
+ "Database routes will fail until you add `database = true` (run deploy in a terminal, or pass --yes).");
147
+ return { wroteToml: false };
148
+ }
149
+ const accepted = opts.autoYes
150
+ ? true
151
+ : await io.confirm(`Detected ${ORM_LABEL[orm]} on better-sqlite3. On Creek this runs on a Cloudflare D1 ` +
152
+ "database in the cloud — a separate instance from your local file (local data is not " +
153
+ "copied). Add `database = true` under [resources] to creek.toml?");
154
+ if (!accepted) {
155
+ io.log("Skipped. Database routes will return a setup hint until you add `database = true` to creek.toml.");
156
+ return { wroteToml: false };
157
+ }
158
+ io.writeToml(enableDatabaseResource(io.readToml(), opts.projectName));
159
+ io.log("Enabled [resources] database in creek.toml (cloud D1 — separate from your local file).");
160
+ return { wroteToml: true };
161
+ }
162
+ /**
163
+ * What to do about a detected migration directory after the database exists.
164
+ * Migrations can be destructive, so this is never silent:
165
+ * - "run" → explicit opt-in (`--migrate`)
166
+ * - "prompt" → interactive (ask, default no)
167
+ * - "suggest" → non-interactive (print the command, don't run)
168
+ * - "none" → no migrations to apply
169
+ */
170
+ export function migrationOfferPlan(opts) {
171
+ if (!opts.migrationDir)
172
+ return "none";
173
+ if (opts.autoMigrate)
174
+ return "run";
175
+ return opts.tty ? "prompt" : "suggest";
176
+ }
177
+ /** Merged dependencies + devDependencies from the project's package.json. */
178
+ export function readProjectDeps(cwd) {
179
+ try {
180
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
181
+ return { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
182
+ }
183
+ catch {
184
+ return {};
185
+ }
186
+ }
187
+ /** Real (consola + filesystem) PreflightIO for the deploy command. */
188
+ export function makePreflightIO(cwd) {
189
+ const tomlPath = join(cwd, "creek.toml");
190
+ return {
191
+ readToml: () => (existsSync(tomlPath) ? readFileSync(tomlPath, "utf-8") : null),
192
+ writeToml: (content) => writeFileSync(tomlPath, content),
193
+ // DB provisioning is low-risk → default to yes.
194
+ confirm: async (message) => (await consola.prompt(message, { type: "confirm", initial: true })),
195
+ log: (message) => consola.success(` ${message}`),
196
+ warn: (message) => consola.warn(` ${message}`),
197
+ };
198
+ }
199
+ //# sourceMappingURL=db-preflight.js.map
@@ -15,6 +15,7 @@ import { join, dirname, resolve } from "node:path";
15
15
  import { createRequire } from "node:module";
16
16
  import { execSync, execFileSync } from "node:child_process";
17
17
  import consola from "consola";
18
+ import { prismaNeedsGenerate } from "./db-preflight.js";
18
19
  // ---------------------------------------------------------------------------
19
20
  // Version detection + unified entry point
20
21
  // ---------------------------------------------------------------------------
@@ -128,6 +129,7 @@ export function buildNextjs(cwd, isMonorepo, projectName) {
128
129
  if (version && semverGte(version, "16.2.3")) {
129
130
  const adapterPath = ensureAdapter(cwd);
130
131
  if (adapterPath) {
132
+ ensurePrismaClient(cwd);
131
133
  ensurePrismaD1(cwd);
132
134
  buildWithAdapter(cwd, adapterPath);
133
135
  return;
@@ -210,12 +212,17 @@ const ADAPTER_VERSION = "^0.2.2";
210
212
  // adapter and imports @prisma/adapter-d1 (an optional peer it doesn't ship).
211
213
  const PRISMA_BSQLITE_PKG = "@prisma/adapter-better-sqlite3";
212
214
  const PRISMA_D1_PKG = "@prisma/adapter-d1";
213
- // Adapter < 0.2.2 resolves its dependencies against paths that don't exist
214
- // under the .creek lazy install (0.2.0: the cache handler; 0.2.1: the
215
- // wrangler bin) npm hoists them to the top of the tree, so the adapter's
216
- // guessed nested paths fail every Next.js build. Installs below this are
217
- // re-installed, not reused.
218
- const ADAPTER_MIN_VERSION = "0.2.2";
215
+ // Minimum adapter the CLI will REUSE from a prior .creek install; older
216
+ // cached copies are force-reinstalled. Each bump tracks a deploy-critical
217
+ // adapter fix that a stale cache would silently miss:
218
+ // 0.2.2 wrangler resolved via module resolution (not a nested .bin guess)
219
+ // 0.2.6 — better-sqlite3 stubbed (else the native module inlines → a ~200MB
220
+ // worker that only fails at upload with "Payload Too Large")
221
+ // 0.2.7 — zero-change Prisma-on-D1 swap
222
+ // 0.2.10 — oversized-bundle fail-fast
223
+ // Kept at the latest because the reinstall cost is trivial and a cached copy
224
+ // in the 0.2.2–0.2.5 window builds successfully but produces a broken worker.
225
+ const ADAPTER_MIN_VERSION = "0.2.10";
219
226
  /**
220
227
  * Merge a dependency into .creek/package.json without clobbering deps that
221
228
  * a previous install (adapter or opennext) may have already written.
@@ -288,6 +295,29 @@ function ensureAdapter(cwd) {
288
295
  consola.success(` ${ADAPTER_PKG} installed`);
289
296
  return resolved;
290
297
  }
298
+ /**
299
+ * Generate the Prisma client before the Next build when it's missing.
300
+ * Prisma 7's `prisma-client` generator emits to a configured `output` dir that
301
+ * the app imports; without it the build fails. Safe to run automatically —
302
+ * generation is idempotent and has no external/persistent effect. No-op unless
303
+ * a Prisma schema exists and its output dir is absent (see prismaNeedsGenerate).
304
+ */
305
+ function ensurePrismaClient(cwd) {
306
+ if (!prismaNeedsGenerate(cwd))
307
+ return;
308
+ consola.start(" Generating Prisma client (prisma generate)...");
309
+ try {
310
+ // `--no-install` uses the project's own prisma (a dependency) rather than
311
+ // fetching one; the schema's datasource needs no URL to generate.
312
+ execSync("npx --no-install prisma generate", { cwd, stdio: "pipe" });
313
+ consola.success(" Prisma client generated");
314
+ }
315
+ catch (err) {
316
+ // Non-fatal: the build will surface a clearer "client not found" error if
317
+ // generation was genuinely required.
318
+ consola.warn(` prisma generate failed (${err instanceof Error ? err.message : String(err)}); continuing`);
319
+ }
320
+ }
291
321
  /**
292
322
  * Ensure @prisma/adapter-d1 is resolvable at build time for the zero-change
293
323
  * Prisma-on-D1 swap. adapter-creek aliases `@prisma/adapter-better-sqlite3` to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/cli",
3
- "version": "0.4.29",
3
+ "version": "0.4.31",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",