@solcreek/cli 0.4.14 → 0.4.15

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,334 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { 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
+ // Merge subcommands into the base resource command
326
+ export const dbCommand = defineCommand({
327
+ meta: base.meta,
328
+ subCommands: {
329
+ ...base.subCommands,
330
+ shell: shellCommand,
331
+ migrate: migrateCommand,
332
+ },
333
+ });
334
+ //# sourceMappingURL=db.js.map
@@ -4,11 +4,12 @@ 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, } from "@solcreek/sdk";
8
8
  import { getToken, getApiUrl } from "../utils/config.js";
9
9
  import { collectAssets } from "../utils/bundle.js";
10
10
  import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
11
11
  import { prepareDeployBundle } from "../utils/prepare-bundle.js";
12
+ import { BuildLogEmitter } from "../utils/build-log.js";
12
13
  import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
13
14
  import { ensureTosAccepted } from "../utils/tos.js";
14
15
  import { hasAdapterOutput } from "../utils/nextjs.js";
@@ -917,11 +918,30 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
917
918
  const prepared = await prepareDeployBundle({ cwd, resolved, skipBuild });
918
919
  const { plan, framework: detectedFramework, effectiveRenderMode, effectiveEntrypoint, fileList, assets: clientAssets, serverFiles, } = prepared;
919
920
  void detectedFramework; // framework var above is the source of truth here
921
+ // Resource / runtime anchors — inline lines that pre-empt the most
922
+ // common wrong assumptions an AI agent reads from Creek running on
923
+ // Cloudflare. Cheap to print, and they parse them directly.
924
+ const dbDeps = !!resolved.bindings.find((b) => b.type === "d1") ||
925
+ fileList.some((f) => /\.(db|sqlite)$/i.test(f));
926
+ if (!jsonMode && dbDeps) {
927
+ 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.");
928
+ }
920
929
  section("Upload");
921
930
  consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
922
931
  section("Deploy");
923
932
  consola.start(" Creating deployment...");
924
933
  const { deployment } = await client.createDeployment(project.id);
934
+ // Collect a minimal structured build log for the dashboard. We emit
935
+ // one line per high-level phase — verbose stdout capture is a
936
+ // Phase 2 concern. The log is POSTed once we reach a terminal
937
+ // status (success / failed), so the dashboard panel shows something
938
+ // useful for every authenticated deploy.
939
+ const buildLog = new BuildLogEmitter();
940
+ buildLog.info("detect", `framework=${framework ?? "none"} renderMode=${effectiveRenderMode} entrypoint=${effectiveEntrypoint ?? "none"}`);
941
+ if (resolved.buildCommand) {
942
+ buildLog.info("build", `ran: ${resolved.buildCommand}`);
943
+ }
944
+ buildLog.info("bundle", `${fileList.length} assets (${assetSummary(fileList)})`);
925
945
  consola.start(" Uploading bundle...");
926
946
  const effectiveHasWorker = serverFiles !== undefined;
927
947
  const bundle = {
@@ -935,9 +955,7 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
935
955
  workerScript: null,
936
956
  assets: clientAssets,
937
957
  serverFiles,
938
- // Backward compat: boolean flags
939
- resources: resolvedConfigToResources(resolved),
940
- // New: binding declarations with user-defined names
958
+ // Binding declarations with user-defined names
941
959
  bindings: resolvedConfigToBindingRequirements(resolved),
942
960
  // Pass through wrangler vars and compat settings
943
961
  ...(Object.keys(resolved.vars).length > 0 ? { vars: resolved.vars } : {}),
@@ -966,6 +984,7 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
966
984
  };
967
985
  let lastStatus = "";
968
986
  const start = Date.now();
987
+ buildLog.info("upload", `bundle uploaded (${fileList.length} files)`);
969
988
  while (Date.now() - start < POLL_TIMEOUT) {
970
989
  const res = await client.getDeploymentStatus(project.id, deployment.id);
971
990
  const { status, failed_step, error_message } = res.deployment;
@@ -976,9 +995,27 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
976
995
  if (!TERMINAL.has(status) && STEP_LABELS[status]) {
977
996
  consola.start(STEP_LABELS[status]);
978
997
  }
998
+ // Map server-side phase transitions into build-log steps so
999
+ // the dashboard timeline shows what happened after upload.
1000
+ if (status === "provisioning")
1001
+ buildLog.info("provision", "provisioning resources");
1002
+ if (status === "deploying")
1003
+ buildLog.info("activate", "activating at edge");
979
1004
  lastStatus = status;
980
1005
  }
981
1006
  if (status === "active") {
1007
+ buildLog.info("activate", `deployed: ${res.url ?? res.previewUrl}`);
1008
+ // Fire-and-forget the build log upload — don't block the user
1009
+ // on it or make a slow/failing log API take down a successful
1010
+ // deploy.
1011
+ void client
1012
+ .uploadBuildLog(deployment.id, buildLog.toNdjson(), {
1013
+ status: "success",
1014
+ startedAt: buildLog.startedAt,
1015
+ })
1016
+ .catch(() => {
1017
+ // Silent — build log is best-effort for now.
1018
+ });
982
1019
  if (jsonMode) {
983
1020
  jsonOutput({
984
1021
  ok: true,
@@ -1013,6 +1050,16 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1013
1050
  if (status === "failed") {
1014
1051
  const step = failed_step ? ` at ${failed_step}` : "";
1015
1052
  const msg = error_message ?? "Unknown error";
1053
+ buildLog.error(failed_step ?? "activate", msg);
1054
+ void client
1055
+ .uploadBuildLog(deployment.id, buildLog.toNdjson(), {
1056
+ status: "failed",
1057
+ startedAt: buildLog.startedAt,
1058
+ errorStep: failed_step ?? null,
1059
+ })
1060
+ .catch(() => {
1061
+ // Silent — build log is best-effort for now.
1062
+ });
1016
1063
  if (jsonMode)
1017
1064
  jsonOutput({ ok: false, error: "deploy_failed", message: msg, failedStep: failed_step }, 1, [
1018
1065
  { command: `creek deployments --project ${project.slug}`, description: "Check previous deployments" },