@solcreek/cli 0.4.14 → 0.4.16

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.
@@ -0,0 +1,2 @@
1
+ export declare const cacheCommand: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { createResourceCommand } from "./resource-cmd.js";
2
+ export const cacheCommand = createResourceCommand({
3
+ kind: "cache",
4
+ label: "cache namespace",
5
+ defaultBinding: "KV",
6
+ });
7
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,2 @@
1
+ export declare const dbCommand: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1,442 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { createInterface } from "node:readline";
5
+ import { resolve } from "node:path";
6
+ import { CreekClient } from "@solcreek/sdk";
7
+ import { createResourceCommand } from "./resource-cmd.js";
8
+ import { detectMigrationDir, parseMigrationFiles, splitStatements, computePending } from "./migrate.js";
9
+ import { getToken, getApiUrl } from "../utils/config.js";
10
+ import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS } from "../utils/output.js";
11
+ const base = createResourceCommand({
12
+ kind: "database",
13
+ label: "database",
14
+ defaultBinding: "DB",
15
+ });
16
+ const shellCommand = defineCommand({
17
+ meta: {
18
+ name: "shell",
19
+ description: "Execute SQL against a team database. Interactive REPL or single query with --sql.",
20
+ },
21
+ args: {
22
+ name: {
23
+ type: "positional",
24
+ description: "Database name (as shown by `creek db ls`)",
25
+ required: true,
26
+ },
27
+ sql: {
28
+ type: "string",
29
+ description: "SQL query to execute (non-interactive mode). Omit for interactive REPL.",
30
+ required: false,
31
+ },
32
+ ...globalArgs,
33
+ },
34
+ async run({ args }) {
35
+ const jsonMode = resolveJsonMode(args);
36
+ const token = getToken();
37
+ if (!token) {
38
+ if (jsonMode)
39
+ jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
40
+ consola.error("Not authenticated. Run `creek login` first.");
41
+ process.exit(1);
42
+ }
43
+ const client = new CreekClient(getApiUrl(), token);
44
+ // Resolve name → resource ID
45
+ const { resources } = await client.listResources();
46
+ const db = resources.find((r) => r.name === args.name && r.kind === "database");
47
+ if (!db) {
48
+ if (jsonMode)
49
+ jsonOutput({ ok: false, error: "not_found", message: `No database named "${args.name}"` }, 1);
50
+ consola.error(`No database named "${args.name}"`);
51
+ process.exit(1);
52
+ }
53
+ // Single query mode
54
+ if (args.sql) {
55
+ await executeAndPrint(client, db.id, args.sql, jsonMode);
56
+ return;
57
+ }
58
+ // Interactive REPL
59
+ consola.info(`Connected to ${args.name} (${db.id.slice(0, 8)})`);
60
+ consola.info("Type SQL and press Enter. Use .exit to quit.\n");
61
+ const rl = createInterface({
62
+ input: process.stdin,
63
+ output: process.stdout,
64
+ prompt: "sql> ",
65
+ });
66
+ rl.prompt();
67
+ let buffer = "";
68
+ rl.on("line", async (line) => {
69
+ const trimmed = line.trim();
70
+ if (trimmed === ".exit" || trimmed === ".quit") {
71
+ rl.close();
72
+ return;
73
+ }
74
+ if (trimmed === ".tables") {
75
+ buffer = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
76
+ }
77
+ else if (trimmed === ".schema") {
78
+ buffer = "SELECT sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY name";
79
+ }
80
+ else {
81
+ buffer += (buffer ? "\n" : "") + line;
82
+ }
83
+ // Execute when line ends with semicolon or is a dot-command
84
+ if (!buffer.endsWith(";") && !trimmed.startsWith(".")) {
85
+ rl.setPrompt("...> ");
86
+ rl.prompt();
87
+ return;
88
+ }
89
+ await executeAndPrint(client, db.id, buffer, false);
90
+ buffer = "";
91
+ rl.setPrompt("sql> ");
92
+ rl.prompt();
93
+ });
94
+ rl.on("close", () => {
95
+ consola.info("Bye.");
96
+ process.exit(0);
97
+ });
98
+ },
99
+ });
100
+ async function executeAndPrint(client, resourceId, sql, jsonMode) {
101
+ try {
102
+ const result = await client.queryDatabase(resourceId, sql);
103
+ if (jsonMode) {
104
+ jsonOutput({ ok: true, ...result }, 0);
105
+ return;
106
+ }
107
+ if (result.rows.length === 0) {
108
+ if (result.meta.changes > 0) {
109
+ consola.info(`${result.meta.changes} row(s) changed (${result.meta.duration.toFixed(1)}ms)`);
110
+ }
111
+ else {
112
+ consola.info("No results.");
113
+ }
114
+ return;
115
+ }
116
+ // Print table
117
+ const cols = result.columns;
118
+ const widths = cols.map((c) => c.length);
119
+ for (const row of result.rows) {
120
+ for (let i = 0; i < cols.length; i++) {
121
+ const val = String(row[cols[i]] ?? "NULL");
122
+ widths[i] = Math.max(widths[i], val.length);
123
+ }
124
+ }
125
+ // Cap column widths
126
+ const maxWidth = 40;
127
+ const cappedWidths = widths.map((w) => Math.min(w, maxWidth));
128
+ const header = cols.map((c, i) => c.padEnd(cappedWidths[i])).join(" ");
129
+ const separator = cappedWidths.map((w) => "─".repeat(w)).join("──");
130
+ console.log(header);
131
+ console.log(separator);
132
+ for (const row of result.rows) {
133
+ const line = cols
134
+ .map((c, i) => {
135
+ const val = String(row[c] ?? "NULL");
136
+ return val.length > maxWidth ? val.slice(0, maxWidth - 1) + "…" : val.padEnd(cappedWidths[i]);
137
+ })
138
+ .join(" ");
139
+ console.log(line);
140
+ }
141
+ consola.info(`\n${result.rows.length} row(s) · ${result.meta.duration.toFixed(1)}ms`);
142
+ }
143
+ catch (err) {
144
+ const msg = err instanceof Error ? err.message : String(err);
145
+ if (jsonMode) {
146
+ jsonOutput({ ok: false, error: "query_failed", message: msg }, 1);
147
+ }
148
+ else {
149
+ consola.error(msg);
150
+ }
151
+ }
152
+ }
153
+ const migrateCommand = defineCommand({
154
+ meta: {
155
+ name: "migrate",
156
+ description: "Apply pending SQL migrations to a team database. Reads .sql files from a migration directory, tracks applied state, executes in order.",
157
+ },
158
+ args: {
159
+ name: {
160
+ type: "positional",
161
+ description: "Database name (as shown by `creek db ls`)",
162
+ required: true,
163
+ },
164
+ dir: {
165
+ type: "string",
166
+ description: "Migration directory path. Default: auto-detect drizzle/, drizzle/migrations/, migrations/, sql/",
167
+ required: false,
168
+ },
169
+ "dry-run": {
170
+ type: "boolean",
171
+ description: "Show pending migrations without executing them.",
172
+ default: false,
173
+ },
174
+ ...globalArgs,
175
+ },
176
+ async run({ args }) {
177
+ const jsonMode = resolveJsonMode(args);
178
+ const token = getToken();
179
+ if (!token) {
180
+ if (jsonMode)
181
+ jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
182
+ consola.error("Not authenticated. Run `creek login` first.");
183
+ process.exit(1);
184
+ }
185
+ const client = new CreekClient(getApiUrl(), token);
186
+ // 1. Resolve database name → resource ID
187
+ const { resources } = await client.listResources();
188
+ const db = resources.find((r) => r.name === args.name && r.kind === "database");
189
+ if (!db) {
190
+ if (jsonMode)
191
+ jsonOutput({ ok: false, error: "not_found", message: `No database named "${args.name}"` }, 1);
192
+ consola.error(`No database named "${args.name}"`);
193
+ process.exit(1);
194
+ }
195
+ // 2. Find migration directory
196
+ const cwd = process.cwd();
197
+ const migrationDir = args.dir ? resolve(cwd, args.dir) : detectMigrationDir(cwd);
198
+ if (!migrationDir) {
199
+ const msg = args.dir
200
+ ? `Migration directory not found: ${args.dir}`
201
+ : "No migration directory found. Looked for: drizzle/, drizzle/migrations/, migrations/, sql/. Use --dir to specify.";
202
+ if (jsonMode)
203
+ jsonOutput({ ok: false, error: "no_migration_dir", message: msg }, 1);
204
+ consola.error(msg);
205
+ process.exit(1);
206
+ }
207
+ // 3. Read migration files
208
+ const files = parseMigrationFiles(migrationDir);
209
+ if (files.length === 0) {
210
+ if (jsonMode)
211
+ jsonOutput({ ok: true, message: "No .sql files found", applied: 0, pending: 0 }, 0);
212
+ consola.info(`No .sql files found in ${migrationDir}`);
213
+ return;
214
+ }
215
+ // 4. Create tracking table + query applied migrations
216
+ try {
217
+ await client.queryDatabase(db.id, `CREATE TABLE IF NOT EXISTS _creek_migrations (
218
+ name TEXT PRIMARY KEY,
219
+ applied_at INTEGER NOT NULL
220
+ );`);
221
+ }
222
+ catch (err) {
223
+ const msg = `Failed to create migration tracking table: ${err instanceof Error ? err.message : String(err)}`;
224
+ if (jsonMode)
225
+ jsonOutput({ ok: false, error: "tracking_table_failed", message: msg }, 1);
226
+ consola.error(msg);
227
+ process.exit(1);
228
+ }
229
+ let appliedRows;
230
+ try {
231
+ const result = await client.queryDatabase(db.id, "SELECT name FROM _creek_migrations ORDER BY name;");
232
+ appliedRows = result.rows;
233
+ }
234
+ catch (err) {
235
+ const msg = `Failed to query applied migrations: ${err instanceof Error ? err.message : String(err)}`;
236
+ if (jsonMode)
237
+ jsonOutput({ ok: false, error: "query_failed", message: msg }, 1);
238
+ consola.error(msg);
239
+ process.exit(1);
240
+ }
241
+ const appliedSet = new Set(appliedRows.map((r) => r.name));
242
+ const pending = computePending(files, appliedSet);
243
+ // 5. Dry-run
244
+ if (args["dry-run"]) {
245
+ if (jsonMode) {
246
+ jsonOutput({
247
+ ok: true,
248
+ dryRun: true,
249
+ total: files.length,
250
+ applied: appliedSet.size,
251
+ pending: pending.map((f) => f.name),
252
+ }, 0);
253
+ }
254
+ else if (pending.length === 0) {
255
+ consola.success(`Database "${args.name}" is up to date (${appliedSet.size} applied)`);
256
+ }
257
+ else {
258
+ consola.info(`${pending.length} pending migration(s):\n`);
259
+ for (const f of pending) {
260
+ consola.log(` ${f.name}`);
261
+ }
262
+ consola.info(`\nRun without --dry-run to apply.`);
263
+ }
264
+ return;
265
+ }
266
+ // 6. Apply pending
267
+ if (pending.length === 0) {
268
+ if (jsonMode)
269
+ jsonOutput({ ok: true, message: "up to date", applied: appliedSet.size, migrated: 0 }, 0);
270
+ consola.success(`Database "${args.name}" is up to date (${appliedSet.size} applied)`);
271
+ return;
272
+ }
273
+ consola.info(`Migrating "${args.name}": ${pending.length} pending of ${files.length} total\n`);
274
+ let migrated = 0;
275
+ for (const file of pending) {
276
+ const sql = readFileSync(file.path, "utf-8");
277
+ const statements = splitStatements(sql);
278
+ if (statements.length === 0) {
279
+ consola.warn(` ${file.name} — empty, skipping`);
280
+ continue;
281
+ }
282
+ consola.start(` ${file.name} (${statements.length} statement${statements.length > 1 ? "s" : ""})`);
283
+ let failed = false;
284
+ for (let i = 0; i < statements.length; i++) {
285
+ try {
286
+ await client.queryDatabase(db.id, statements[i]);
287
+ }
288
+ catch (err) {
289
+ const msg = err instanceof Error ? err.message : String(err);
290
+ consola.error(` ${file.name} — statement ${i + 1}/${statements.length} failed: ${msg}`);
291
+ if (jsonMode) {
292
+ jsonOutput({
293
+ ok: false,
294
+ error: "migration_failed",
295
+ file: file.name,
296
+ statement: i + 1,
297
+ totalStatements: statements.length,
298
+ message: msg,
299
+ migrated,
300
+ remaining: pending.length - migrated,
301
+ }, 1);
302
+ }
303
+ consola.error(`\n${migrated} migration(s) applied before failure. Fix the SQL and re-run.`);
304
+ process.exit(1);
305
+ }
306
+ }
307
+ // Record success
308
+ try {
309
+ await client.queryDatabase(db.id, "INSERT INTO _creek_migrations (name, applied_at) VALUES (?, ?);", [file.name, Date.now()]);
310
+ }
311
+ catch {
312
+ // Non-fatal — migration ran but tracking failed. It won't re-run
313
+ // because the schema changes already happened. Warn and continue.
314
+ consola.warn(` ${file.name} — applied but failed to record in tracking table`);
315
+ }
316
+ migrated++;
317
+ consola.success(` ${file.name}`);
318
+ }
319
+ if (jsonMode) {
320
+ jsonOutput({ ok: true, migrated, total: files.length, applied: appliedSet.size + migrated }, 0);
321
+ }
322
+ consola.success(`\n${migrated} migration(s) applied successfully`);
323
+ },
324
+ });
325
+ const SEED_CANDIDATES = [
326
+ "drizzle/seed.sql",
327
+ "drizzle/migrations/seed.sql",
328
+ "migrations/seed.sql",
329
+ "sql/seed.sql",
330
+ "seed.sql",
331
+ ];
332
+ const seedCommand = defineCommand({
333
+ meta: {
334
+ name: "seed",
335
+ description: "Execute a seed SQL file against a team database. Looks for seed.sql in common locations or use --file to specify.",
336
+ },
337
+ args: {
338
+ name: {
339
+ type: "positional",
340
+ description: "Database name (as shown by `creek db ls`)",
341
+ required: true,
342
+ },
343
+ file: {
344
+ type: "string",
345
+ description: "Path to seed SQL file. Default: auto-detect seed.sql in drizzle/, migrations/, sql/, or project root.",
346
+ required: false,
347
+ },
348
+ ...globalArgs,
349
+ },
350
+ async run({ args }) {
351
+ const jsonMode = resolveJsonMode(args);
352
+ const token = getToken();
353
+ if (!token) {
354
+ if (jsonMode)
355
+ jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
356
+ consola.error("Not authenticated. Run `creek login` first.");
357
+ process.exit(1);
358
+ }
359
+ const client = new CreekClient(getApiUrl(), token);
360
+ // Resolve database
361
+ const { resources } = await client.listResources();
362
+ const db = resources.find((r) => r.name === args.name && r.kind === "database");
363
+ if (!db) {
364
+ if (jsonMode)
365
+ jsonOutput({ ok: false, error: "not_found", message: `No database named "${args.name}"` }, 1);
366
+ consola.error(`No database named "${args.name}"`);
367
+ process.exit(1);
368
+ }
369
+ // Find seed file
370
+ const cwd = process.cwd();
371
+ let seedPath = null;
372
+ if (args.file) {
373
+ seedPath = resolve(cwd, args.file);
374
+ if (!existsSync(seedPath)) {
375
+ if (jsonMode)
376
+ jsonOutput({ ok: false, error: "file_not_found", message: `Seed file not found: ${args.file}` }, 1);
377
+ consola.error(`Seed file not found: ${args.file}`);
378
+ process.exit(1);
379
+ }
380
+ }
381
+ else {
382
+ for (const candidate of SEED_CANDIDATES) {
383
+ const abs = resolve(cwd, candidate);
384
+ if (existsSync(abs)) {
385
+ seedPath = abs;
386
+ break;
387
+ }
388
+ }
389
+ if (!seedPath) {
390
+ const msg = "No seed file found. Looked for: " + SEED_CANDIDATES.join(", ") + ". Use --file to specify.";
391
+ if (jsonMode)
392
+ jsonOutput({ ok: false, error: "no_seed_file", message: msg }, 1);
393
+ consola.error(msg);
394
+ process.exit(1);
395
+ }
396
+ }
397
+ // Read and execute
398
+ const sql = readFileSync(seedPath, "utf-8");
399
+ const statements = splitStatements(sql);
400
+ if (statements.length === 0) {
401
+ if (jsonMode)
402
+ jsonOutput({ ok: true, message: "Seed file is empty", executed: 0 }, 0);
403
+ consola.info("Seed file is empty — nothing to execute.");
404
+ return;
405
+ }
406
+ consola.info(`Seeding "${args.name}" from ${seedPath.replace(cwd + "/", "")}`);
407
+ consola.info(`${statements.length} statement(s)\n`);
408
+ for (let i = 0; i < statements.length; i++) {
409
+ try {
410
+ await client.queryDatabase(db.id, statements[i]);
411
+ }
412
+ catch (err) {
413
+ const msg = err instanceof Error ? err.message : String(err);
414
+ if (jsonMode) {
415
+ jsonOutput({
416
+ ok: false,
417
+ error: "seed_failed",
418
+ statement: i + 1,
419
+ totalStatements: statements.length,
420
+ message: msg,
421
+ }, 1);
422
+ }
423
+ consola.error(`Statement ${i + 1}/${statements.length} failed: ${msg}`);
424
+ process.exit(1);
425
+ }
426
+ }
427
+ if (jsonMode)
428
+ jsonOutput({ ok: true, executed: statements.length }, 0);
429
+ consola.success(`Seed complete (${statements.length} statement${statements.length > 1 ? "s" : ""})`);
430
+ },
431
+ });
432
+ // Merge subcommands into the base resource command
433
+ export const dbCommand = defineCommand({
434
+ meta: base.meta,
435
+ subCommands: {
436
+ ...base.subCommands,
437
+ shell: shellCommand,
438
+ migrate: migrateCommand,
439
+ seed: seedCommand,
440
+ },
441
+ });
442
+ //# sourceMappingURL=db.js.map
@@ -4,11 +4,13 @@ import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
4
4
  // ajv is lazy-imported only when --template --data is used (avoid top-level crash if deps missing)
5
5
  import { join, resolve } from "node:path";
6
6
  import { execSync, execFileSync } from "node:child_process";
7
- import { CreekClient, CreekAuthError, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToResources, resolvedConfigToBindingRequirements, ConfigNotFoundError, detectNextjsMode, detectMonorepo, } from "@solcreek/sdk";
7
+ import { CreekClient, CreekAuthError, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToBindingRequirements, ConfigNotFoundError, detectNextjsMode, detectMonorepo, runDoctor, } from "@solcreek/sdk";
8
+ import { buildDoctorContext } from "../utils/doctor-context.js";
8
9
  import { getToken, getApiUrl } from "../utils/config.js";
9
10
  import { collectAssets } from "../utils/bundle.js";
10
11
  import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
11
12
  import { prepareDeployBundle } from "../utils/prepare-bundle.js";
13
+ import { BuildLogEmitter } from "../utils/build-log.js";
12
14
  import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
13
15
  import { ensureTosAccepted } from "../utils/tos.js";
14
16
  import { hasAdapterOutput } from "../utils/nextjs.js";
@@ -88,6 +90,13 @@ async function dryRunPlan(cwd, args, jsonMode) {
88
90
  type: b.type,
89
91
  }))
90
92
  : [];
93
+ // Run the same rule engine as `creek doctor` so dry-run surfaces
94
+ // config errors (e.g. CK-RESOURCES-KEYS) that would otherwise only
95
+ // reveal themselves at runtime when the missing binding crashes the
96
+ // worker. Agents following the SKILL.md "dry-run first" rule need
97
+ // these findings here, not after a 500.
98
+ const doctorReport = runDoctor(buildDoctorContext(cwd));
99
+ const blockingFindings = doctorReport.findings.filter((f) => f.severity === "error");
91
100
  const plan = {
92
101
  mode: "dry-run",
93
102
  supported: true,
@@ -112,6 +121,7 @@ async function dryRunPlan(cwd, args, jsonMode) {
112
121
  : null,
113
122
  buildOutputFallback,
114
123
  bindings,
124
+ findings: doctorReport.findings,
115
125
  wouldDeploy,
116
126
  sideEffects: {
117
127
  networkCalls: false,
@@ -119,9 +129,11 @@ async function dryRunPlan(cwd, args, jsonMode) {
119
129
  buildExecuted: false,
120
130
  tosPromptShown: false,
121
131
  },
122
- nextStep: wouldDeploy
123
- ? "Run without --dry-run to execute: npx creek deploy"
124
- : "No project config or build output found. Run `creek init` or `npm create vite@latest` first.",
132
+ nextStep: blockingFindings.length > 0
133
+ ? `Fix ${blockingFindings.length} blocking issue${blockingFindings.length === 1 ? "" : "s"} first (see findings), then re-run. For details: creek doctor --json`
134
+ : wouldDeploy
135
+ ? "Run without --dry-run to execute: npx creek deploy"
136
+ : "No project config or build output found. Run `creek init` or `npm create vite@latest` first.",
125
137
  };
126
138
  if (jsonMode) {
127
139
  jsonOutput(plan, 0, []);
@@ -154,6 +166,22 @@ async function dryRunPlan(cwd, args, jsonMode) {
154
166
  .map((b) => `${b.name} (${b.type})`)
155
167
  .join(", ")} — would be skipped`);
156
168
  }
169
+ if (doctorReport.findings.length > 0) {
170
+ const errCount = doctorReport.summary.error;
171
+ const warnCount = doctorReport.summary.warn;
172
+ const infoCount = doctorReport.summary.info;
173
+ const parts = [];
174
+ if (errCount)
175
+ parts.push(`${errCount} error${errCount === 1 ? "" : "s"}`);
176
+ if (warnCount)
177
+ parts.push(`${warnCount} warning${warnCount === 1 ? "" : "s"}`);
178
+ if (infoCount)
179
+ parts.push(`${infoCount} info`);
180
+ consola.log(` Doctor findings: ${parts.join(", ")} — run \`creek doctor\` for details`);
181
+ for (const f of doctorReport.findings.filter((f) => f.severity === "error")) {
182
+ consola.log(` \x1b[31m✗\x1b[0m [${f.code}] ${f.title}`);
183
+ }
184
+ }
157
185
  }
158
186
  else if (buildOutputFallback) {
159
187
  consola.log(` Detected: prebuilt assets in ${buildOutputFallback}/ (no creek.toml, no wrangler config)`);
@@ -917,11 +945,30 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
917
945
  const prepared = await prepareDeployBundle({ cwd, resolved, skipBuild });
918
946
  const { plan, framework: detectedFramework, effectiveRenderMode, effectiveEntrypoint, fileList, assets: clientAssets, serverFiles, } = prepared;
919
947
  void detectedFramework; // framework var above is the source of truth here
948
+ // Resource / runtime anchors — inline lines that pre-empt the most
949
+ // common wrong assumptions an AI agent reads from Creek running on
950
+ // Cloudflare. Cheap to print, and they parse them directly.
951
+ const dbDeps = !!resolved.bindings.find((b) => b.type === "d1") ||
952
+ fileList.some((f) => /\.(db|sqlite)$/i.test(f));
953
+ if (!jsonMode && dbDeps) {
954
+ consola.info(" ℹ Database: Creek uses the portable driver — better-sqlite3 locally, D1 remotely. Your code reads env.DB in both. Do NOT rewrite for D1 manually; `creek db attach` wires the binding.");
955
+ }
920
956
  section("Upload");
921
957
  consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
922
958
  section("Deploy");
923
959
  consola.start(" Creating deployment...");
924
960
  const { deployment } = await client.createDeployment(project.id);
961
+ // Collect a minimal structured build log for the dashboard. We emit
962
+ // one line per high-level phase — verbose stdout capture is a
963
+ // Phase 2 concern. The log is POSTed once we reach a terminal
964
+ // status (success / failed), so the dashboard panel shows something
965
+ // useful for every authenticated deploy.
966
+ const buildLog = new BuildLogEmitter();
967
+ buildLog.info("detect", `framework=${framework ?? "none"} renderMode=${effectiveRenderMode} entrypoint=${effectiveEntrypoint ?? "none"}`);
968
+ if (resolved.buildCommand) {
969
+ buildLog.info("build", `ran: ${resolved.buildCommand}`);
970
+ }
971
+ buildLog.info("bundle", `${fileList.length} assets (${assetSummary(fileList)})`);
925
972
  consola.start(" Uploading bundle...");
926
973
  const effectiveHasWorker = serverFiles !== undefined;
927
974
  const bundle = {
@@ -935,9 +982,7 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
935
982
  workerScript: null,
936
983
  assets: clientAssets,
937
984
  serverFiles,
938
- // Backward compat: boolean flags
939
- resources: resolvedConfigToResources(resolved),
940
- // New: binding declarations with user-defined names
985
+ // Binding declarations with user-defined names
941
986
  bindings: resolvedConfigToBindingRequirements(resolved),
942
987
  // Pass through wrangler vars and compat settings
943
988
  ...(Object.keys(resolved.vars).length > 0 ? { vars: resolved.vars } : {}),
@@ -966,6 +1011,7 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
966
1011
  };
967
1012
  let lastStatus = "";
968
1013
  const start = Date.now();
1014
+ buildLog.info("upload", `bundle uploaded (${fileList.length} files)`);
969
1015
  while (Date.now() - start < POLL_TIMEOUT) {
970
1016
  const res = await client.getDeploymentStatus(project.id, deployment.id);
971
1017
  const { status, failed_step, error_message } = res.deployment;
@@ -976,9 +1022,27 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
976
1022
  if (!TERMINAL.has(status) && STEP_LABELS[status]) {
977
1023
  consola.start(STEP_LABELS[status]);
978
1024
  }
1025
+ // Map server-side phase transitions into build-log steps so
1026
+ // the dashboard timeline shows what happened after upload.
1027
+ if (status === "provisioning")
1028
+ buildLog.info("provision", "provisioning resources");
1029
+ if (status === "deploying")
1030
+ buildLog.info("activate", "activating at edge");
979
1031
  lastStatus = status;
980
1032
  }
981
1033
  if (status === "active") {
1034
+ buildLog.info("activate", `deployed: ${res.url ?? res.previewUrl}`);
1035
+ // Fire-and-forget the build log upload — don't block the user
1036
+ // on it or make a slow/failing log API take down a successful
1037
+ // deploy.
1038
+ void client
1039
+ .uploadBuildLog(deployment.id, buildLog.toNdjson(), {
1040
+ status: "success",
1041
+ startedAt: buildLog.startedAt,
1042
+ })
1043
+ .catch(() => {
1044
+ // Silent — build log is best-effort for now.
1045
+ });
982
1046
  if (jsonMode) {
983
1047
  jsonOutput({
984
1048
  ok: true,
@@ -1013,6 +1077,16 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1013
1077
  if (status === "failed") {
1014
1078
  const step = failed_step ? ` at ${failed_step}` : "";
1015
1079
  const msg = error_message ?? "Unknown error";
1080
+ buildLog.error(failed_step ?? "activate", msg);
1081
+ void client
1082
+ .uploadBuildLog(deployment.id, buildLog.toNdjson(), {
1083
+ status: "failed",
1084
+ startedAt: buildLog.startedAt,
1085
+ errorStep: failed_step ?? null,
1086
+ })
1087
+ .catch(() => {
1088
+ // Silent — build log is best-effort for now.
1089
+ });
1016
1090
  if (jsonMode)
1017
1091
  jsonOutput({ ok: false, error: "deploy_failed", message: msg, failedStep: failed_step }, 1, [
1018
1092
  { command: `creek deployments --project ${project.slug}`, description: "Check previous deployments" },