@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.
- package/dist/commands/cache.d.ts +2 -0
- package/dist/commands/cache.js +7 -0
- package/dist/commands/db.d.ts +2 -0
- package/dist/commands/db.js +442 -0
- package/dist/commands/deploy.js +81 -7
- package/dist/commands/deployments.js +210 -15
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +175 -44
- package/dist/commands/init.js +1 -5
- package/dist/commands/migrate.d.ts +37 -0
- package/dist/commands/migrate.js +99 -0
- package/dist/commands/resource-cmd.d.ts +18 -0
- package/dist/commands/resource-cmd.js +216 -0
- package/dist/commands/storage.d.ts +2 -0
- package/dist/commands/storage.js +7 -0
- package/dist/index.js +6 -0
- package/dist/utils/build-log.d.ts +36 -0
- package/dist/utils/build-log.js +40 -0
- package/dist/utils/doctor-context.d.ts +3 -0
- package/dist/utils/doctor-context.js +48 -0
- package/package.json +2 -2
|
@@ -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
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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,
|
|
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:
|
|
123
|
-
? "
|
|
124
|
-
:
|
|
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
|
-
//
|
|
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" },
|