@solcreek/cli 0.4.13 → 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.
- package/README.md +3 -0
- 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 +334 -0
- package/dist/commands/deploy.js +51 -4
- package/dist/commands/deployments.js +210 -15
- package/dist/commands/doctor.d.ts +39 -0
- package/dist/commands/doctor.js +344 -0
- 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 +8 -0
- package/dist/utils/build-log.d.ts +36 -0
- package/dist/utils/build-log.js +40 -0
- package/package.json +2 -2
|
@@ -6,10 +6,28 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { parseConfig } from "@solcreek/sdk";
|
|
8
8
|
import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
|
|
9
|
-
|
|
9
|
+
function resolveSlug(argSlug, jsonMode) {
|
|
10
|
+
if (argSlug)
|
|
11
|
+
return argSlug;
|
|
12
|
+
const configPath = join(process.cwd(), "creek.toml");
|
|
13
|
+
if (!existsSync(configPath)) {
|
|
14
|
+
if (jsonMode) {
|
|
15
|
+
jsonOutput({
|
|
16
|
+
ok: false,
|
|
17
|
+
error: "no_project",
|
|
18
|
+
message: "No creek.toml found. Use --project <slug> or run from a project directory.",
|
|
19
|
+
}, 1, NO_PROJECT_BREADCRUMBS);
|
|
20
|
+
}
|
|
21
|
+
consola.error("No creek.toml found. Use --project <slug> or run from a project directory.");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
return parseConfig(readFileSync(configPath, "utf-8")).project.name;
|
|
25
|
+
}
|
|
26
|
+
// --- List (default behaviour) ---
|
|
27
|
+
const deploymentsList = defineCommand({
|
|
10
28
|
meta: {
|
|
11
|
-
name: "
|
|
12
|
-
description: "List recent deployments
|
|
29
|
+
name: "list",
|
|
30
|
+
description: "List recent deployments",
|
|
13
31
|
},
|
|
14
32
|
args: {
|
|
15
33
|
project: {
|
|
@@ -28,18 +46,7 @@ export const deploymentsCommand = defineCommand({
|
|
|
28
46
|
consola.error("Not authenticated. Run `creek login` first.");
|
|
29
47
|
process.exit(1);
|
|
30
48
|
}
|
|
31
|
-
|
|
32
|
-
let slug = args.project;
|
|
33
|
-
if (!slug) {
|
|
34
|
-
const configPath = join(process.cwd(), "creek.toml");
|
|
35
|
-
if (!existsSync(configPath)) {
|
|
36
|
-
if (jsonMode)
|
|
37
|
-
jsonOutput({ ok: false, error: "no_project", message: "No creek.toml found. Use --project <slug> or run from a project directory." }, 1, NO_PROJECT_BREADCRUMBS);
|
|
38
|
-
consola.error("No creek.toml found. Use --project <slug> or run from a project directory.");
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
slug = parseConfig(readFileSync(configPath, "utf-8")).project.name;
|
|
42
|
-
}
|
|
49
|
+
const slug = resolveSlug(args.project, jsonMode);
|
|
43
50
|
const client = new CreekClient(getApiUrl(), token);
|
|
44
51
|
let deployments;
|
|
45
52
|
try {
|
|
@@ -74,6 +81,194 @@ export const deploymentsCommand = defineCommand({
|
|
|
74
81
|
}
|
|
75
82
|
},
|
|
76
83
|
});
|
|
84
|
+
// --- Logs subcommand ---
|
|
85
|
+
const deploymentsLogs = defineCommand({
|
|
86
|
+
meta: {
|
|
87
|
+
name: "logs",
|
|
88
|
+
description: "Read the build log for a deployment (production + branch + preview). Use --project to target a different project; --raw to print ndjson for piping. Designed so AI coding agents can diagnose failed deploys without a human relay.",
|
|
89
|
+
},
|
|
90
|
+
args: {
|
|
91
|
+
id: {
|
|
92
|
+
type: "positional",
|
|
93
|
+
description: "Deployment id (8-char short id or full uuid)",
|
|
94
|
+
required: true,
|
|
95
|
+
},
|
|
96
|
+
project: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Project slug (default: from creek.toml)",
|
|
99
|
+
required: false,
|
|
100
|
+
},
|
|
101
|
+
raw: {
|
|
102
|
+
type: "boolean",
|
|
103
|
+
description: "Print raw ndjson lines instead of step-grouped output",
|
|
104
|
+
default: false,
|
|
105
|
+
},
|
|
106
|
+
...globalArgs,
|
|
107
|
+
},
|
|
108
|
+
async run({ args }) {
|
|
109
|
+
const jsonMode = resolveJsonMode(args);
|
|
110
|
+
const token = getToken();
|
|
111
|
+
if (!token) {
|
|
112
|
+
if (jsonMode)
|
|
113
|
+
jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
|
|
114
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const slug = resolveSlug(args.project, jsonMode);
|
|
118
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
119
|
+
// If the user passed a short id, look up the full uuid. GET endpoint
|
|
120
|
+
// requires the full id.
|
|
121
|
+
let fullId = args.id;
|
|
122
|
+
if (fullId.length < 36) {
|
|
123
|
+
try {
|
|
124
|
+
const list = await client.listDeployments(slug);
|
|
125
|
+
const match = list.find((d) => d.id.startsWith(fullId));
|
|
126
|
+
if (!match) {
|
|
127
|
+
if (jsonMode)
|
|
128
|
+
jsonOutput({ ok: false, error: "not_found", message: `No deployment matches id prefix '${fullId}'` }, 1);
|
|
129
|
+
consola.error(`No deployment matches id prefix '${fullId}'`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
fullId = match.id;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const msg = err instanceof Error ? err.message : "Failed to resolve deployment id";
|
|
136
|
+
if (jsonMode)
|
|
137
|
+
jsonOutput({ ok: false, error: "api_error", message: msg }, 1);
|
|
138
|
+
consola.error(msg);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let log;
|
|
143
|
+
try {
|
|
144
|
+
log = await client.getBuildLog(slug, fullId);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const msg = err instanceof Error ? err.message : "Failed to read build log";
|
|
148
|
+
if (jsonMode)
|
|
149
|
+
jsonOutput({ ok: false, error: "api_error", message: msg }, 1);
|
|
150
|
+
consola.error(msg);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
if (jsonMode) {
|
|
154
|
+
jsonOutput({ ok: true, deploymentId: fullId, ...log }, 0);
|
|
155
|
+
}
|
|
156
|
+
if (!log.metadata) {
|
|
157
|
+
consola.info(log.message ?? "No build log available for this deployment.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (args.raw) {
|
|
161
|
+
for (const e of log.entries)
|
|
162
|
+
consola.log(JSON.stringify(e));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Grouped printout: step ▸ lines. Headers carry status + duration + CK-code.
|
|
166
|
+
const grouped = groupByStep(log.entries);
|
|
167
|
+
const headerLabels = {
|
|
168
|
+
clone: "Clone",
|
|
169
|
+
detect: "Detect",
|
|
170
|
+
install: "Install",
|
|
171
|
+
build: "Build",
|
|
172
|
+
bundle: "Bundle",
|
|
173
|
+
upload: "Upload",
|
|
174
|
+
provision: "Provision",
|
|
175
|
+
activate: "Activate",
|
|
176
|
+
cleanup: "Cleanup",
|
|
177
|
+
};
|
|
178
|
+
const order = [
|
|
179
|
+
"clone",
|
|
180
|
+
"detect",
|
|
181
|
+
"install",
|
|
182
|
+
"build",
|
|
183
|
+
"bundle",
|
|
184
|
+
"upload",
|
|
185
|
+
"provision",
|
|
186
|
+
"activate",
|
|
187
|
+
"cleanup",
|
|
188
|
+
];
|
|
189
|
+
const meta = log.metadata;
|
|
190
|
+
const statusColour = meta.status === "success" ? "\x1b[32m" :
|
|
191
|
+
meta.status === "failed" ? "\x1b[31m" :
|
|
192
|
+
"\x1b[33m";
|
|
193
|
+
consola.log(`\n deployment ${fullId.slice(0, 8)} ${statusColour}${meta.status}\x1b[0m`);
|
|
194
|
+
if (meta.errorCode)
|
|
195
|
+
consola.log(` error code: ${meta.errorCode}`);
|
|
196
|
+
if (meta.errorStep)
|
|
197
|
+
consola.log(` failed at: ${meta.errorStep}`);
|
|
198
|
+
consola.log("");
|
|
199
|
+
for (const step of order) {
|
|
200
|
+
const lines = grouped.get(step);
|
|
201
|
+
if (!lines)
|
|
202
|
+
continue;
|
|
203
|
+
const header = headerLabels[step] ?? step;
|
|
204
|
+
const duration = stepDuration(lines);
|
|
205
|
+
const failed = meta.status === "failed" && meta.errorStep === step;
|
|
206
|
+
const icon = failed ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
|
|
207
|
+
const label = failed ? `\x1b[31m${header}\x1b[0m` : header;
|
|
208
|
+
consola.log(` ${icon} ${label}${duration ? ` (${duration})` : ""}`);
|
|
209
|
+
for (const l of lines) {
|
|
210
|
+
const lineColour = l.level === "error" || l.level === "fatal" ? "\x1b[31m" :
|
|
211
|
+
l.level === "warn" ? "\x1b[33m" :
|
|
212
|
+
"\x1b[90m";
|
|
213
|
+
consola.log(` ${lineColour}${l.msg}\x1b[0m`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (meta.truncated) {
|
|
217
|
+
consola.warn(" (log was truncated at 5MB / 200k lines)");
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
function groupByStep(entries) {
|
|
222
|
+
const map = new Map();
|
|
223
|
+
for (const e of entries) {
|
|
224
|
+
let bucket = map.get(e.step);
|
|
225
|
+
if (!bucket) {
|
|
226
|
+
bucket = [];
|
|
227
|
+
map.set(e.step, bucket);
|
|
228
|
+
}
|
|
229
|
+
bucket.push(e);
|
|
230
|
+
}
|
|
231
|
+
return map;
|
|
232
|
+
}
|
|
233
|
+
function stepDuration(lines) {
|
|
234
|
+
if (lines.length < 2)
|
|
235
|
+
return null;
|
|
236
|
+
const sorted = [...lines].sort((a, b) => a.ts - b.ts);
|
|
237
|
+
const ms = sorted[sorted.length - 1].ts - sorted[0].ts;
|
|
238
|
+
if (ms < 1000)
|
|
239
|
+
return `${ms}ms`;
|
|
240
|
+
if (ms < 60_000)
|
|
241
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
242
|
+
return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
|
243
|
+
}
|
|
244
|
+
// --- Public export ---
|
|
245
|
+
export const deploymentsCommand = defineCommand({
|
|
246
|
+
meta: {
|
|
247
|
+
name: "deployments",
|
|
248
|
+
description: "List deployments and read build logs",
|
|
249
|
+
},
|
|
250
|
+
args: {
|
|
251
|
+
project: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "Project slug (default: from creek.toml)",
|
|
254
|
+
required: false,
|
|
255
|
+
},
|
|
256
|
+
...globalArgs,
|
|
257
|
+
},
|
|
258
|
+
subCommands: {
|
|
259
|
+
list: deploymentsList,
|
|
260
|
+
logs: deploymentsLogs,
|
|
261
|
+
},
|
|
262
|
+
// Default behaviour (no subcommand) = list. Citty resolves
|
|
263
|
+
// subcommands first, so this only fires when the user types
|
|
264
|
+
// `creek deployments` with no trailing verb.
|
|
265
|
+
async run(ctx) {
|
|
266
|
+
const run = deploymentsList.run;
|
|
267
|
+
if (!run)
|
|
268
|
+
return;
|
|
269
|
+
return run(ctx);
|
|
270
|
+
},
|
|
271
|
+
});
|
|
77
272
|
function timeAgo(dateStr) {
|
|
78
273
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
79
274
|
const mins = Math.floor(diff / 60_000);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `creek doctor` — pre-deploy sanity check.
|
|
3
|
+
*
|
|
4
|
+
* Runs the SDK rule engine against the current project, reports
|
|
5
|
+
* findings. Exits 0 if ok, 1 if any error-severity finding fires.
|
|
6
|
+
*
|
|
7
|
+
* This is the one command designed for LLM agents to invoke before
|
|
8
|
+
* `creek deploy`. With `--json` the output is ndjson-adjacent (pretty
|
|
9
|
+
* JSON, but parseable), letting an agent look up fixes by stable
|
|
10
|
+
* CK-* codes and apply them without re-reading the source.
|
|
11
|
+
*/
|
|
12
|
+
export declare const doctorCommand: import("citty").CommandDef<{
|
|
13
|
+
json: {
|
|
14
|
+
type: "boolean";
|
|
15
|
+
description: string;
|
|
16
|
+
default: boolean;
|
|
17
|
+
};
|
|
18
|
+
yes: {
|
|
19
|
+
type: "boolean";
|
|
20
|
+
description: string;
|
|
21
|
+
default: boolean;
|
|
22
|
+
};
|
|
23
|
+
path: {
|
|
24
|
+
type: "positional";
|
|
25
|
+
description: string;
|
|
26
|
+
required: false;
|
|
27
|
+
};
|
|
28
|
+
last: {
|
|
29
|
+
type: "boolean";
|
|
30
|
+
description: string;
|
|
31
|
+
default: false;
|
|
32
|
+
};
|
|
33
|
+
project: {
|
|
34
|
+
type: "string";
|
|
35
|
+
description: string;
|
|
36
|
+
required: false;
|
|
37
|
+
};
|
|
38
|
+
}>;
|
|
39
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { runDoctor, resolveConfig, ConfigNotFoundError, } from "@solcreek/sdk";
|
|
6
|
+
import { CreekClient } from "@solcreek/sdk";
|
|
7
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
8
|
+
import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, } from "../utils/output.js";
|
|
9
|
+
/**
|
|
10
|
+
* `creek doctor` — pre-deploy sanity check.
|
|
11
|
+
*
|
|
12
|
+
* Runs the SDK rule engine against the current project, reports
|
|
13
|
+
* findings. Exits 0 if ok, 1 if any error-severity finding fires.
|
|
14
|
+
*
|
|
15
|
+
* This is the one command designed for LLM agents to invoke before
|
|
16
|
+
* `creek deploy`. With `--json` the output is ndjson-adjacent (pretty
|
|
17
|
+
* JSON, but parseable), letting an agent look up fixes by stable
|
|
18
|
+
* CK-* codes and apply them without re-reading the source.
|
|
19
|
+
*/
|
|
20
|
+
export const doctorCommand = defineCommand({
|
|
21
|
+
meta: {
|
|
22
|
+
name: "doctor",
|
|
23
|
+
description: "Analyze the project for pre-deploy issues — missing build output, deprecated config keys, Workers-incompatible deps, portability leaks.",
|
|
24
|
+
},
|
|
25
|
+
args: {
|
|
26
|
+
path: {
|
|
27
|
+
type: "positional",
|
|
28
|
+
description: "Project directory to analyze. Defaults to cwd.",
|
|
29
|
+
required: false,
|
|
30
|
+
},
|
|
31
|
+
last: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
description: "Diagnose the most recent FAILED deployment instead of running pre-deploy checks. Fetches the build log and matches errorCode against the CK-* fix table.",
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
project: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Project slug for --last (default: read from creek.toml in cwd)",
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
...globalArgs,
|
|
42
|
+
},
|
|
43
|
+
async run({ args }) {
|
|
44
|
+
const jsonMode = resolveJsonMode(args);
|
|
45
|
+
if (args.last) {
|
|
46
|
+
await runLastFailureDiagnosis({
|
|
47
|
+
project: args.project,
|
|
48
|
+
cwd: resolve(args.path ?? process.cwd()),
|
|
49
|
+
jsonMode,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const cwd = resolve(args.path ?? process.cwd());
|
|
54
|
+
const ctx = buildContext(cwd);
|
|
55
|
+
const report = runDoctor(ctx);
|
|
56
|
+
if (jsonMode) {
|
|
57
|
+
jsonOutput({
|
|
58
|
+
ok: report.ok,
|
|
59
|
+
cwd,
|
|
60
|
+
archetype: report.archetype,
|
|
61
|
+
summary: report.summary,
|
|
62
|
+
findings: report.findings,
|
|
63
|
+
}, report.ok ? 0 : 1);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
printHuman(cwd, report);
|
|
67
|
+
if (!report.ok)
|
|
68
|
+
process.exit(1);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
function buildContext(cwd) {
|
|
72
|
+
const fileExists = (relPath) => existsSync(join(cwd, relPath));
|
|
73
|
+
const creekTomlPath = join(cwd, "creek.toml");
|
|
74
|
+
const creekTomlRaw = existsSync(creekTomlPath)
|
|
75
|
+
? safeRead(creekTomlPath)
|
|
76
|
+
: null;
|
|
77
|
+
const pkgPath = join(cwd, "package.json");
|
|
78
|
+
const packageJson = existsSync(pkgPath)
|
|
79
|
+
? safeParseJson(pkgPath)
|
|
80
|
+
: null;
|
|
81
|
+
const resolved = resolveConfigSafely(cwd);
|
|
82
|
+
const allDeps = {
|
|
83
|
+
...(packageJson?.dependencies ?? {}),
|
|
84
|
+
...(packageJson?.devDependencies ?? {}),
|
|
85
|
+
};
|
|
86
|
+
return { cwd, resolved, packageJson, creekTomlRaw, fileExists, allDeps };
|
|
87
|
+
}
|
|
88
|
+
function resolveConfigSafely(cwd) {
|
|
89
|
+
try {
|
|
90
|
+
return resolveConfig(cwd);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof ConfigNotFoundError)
|
|
94
|
+
return null;
|
|
95
|
+
// Other errors (parse failures) bubble as null — the rules will
|
|
96
|
+
// still pick up partial info from creekTomlRaw + packageJson.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function safeRead(path) {
|
|
101
|
+
try {
|
|
102
|
+
return readFileSync(path, "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function safeParseJson(path) {
|
|
109
|
+
const raw = safeRead(path);
|
|
110
|
+
if (raw === null)
|
|
111
|
+
return null;
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(raw);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ─── Human output ───────────────────────────────────────────────────────
|
|
120
|
+
const COLOR = {
|
|
121
|
+
reset: "\x1b[0m",
|
|
122
|
+
bold: "\x1b[1m",
|
|
123
|
+
dim: "\x1b[2m",
|
|
124
|
+
red: "\x1b[31m",
|
|
125
|
+
yellow: "\x1b[33m",
|
|
126
|
+
green: "\x1b[32m",
|
|
127
|
+
cyan: "\x1b[36m",
|
|
128
|
+
gray: "\x1b[90m",
|
|
129
|
+
};
|
|
130
|
+
function tty() {
|
|
131
|
+
return process.stdout.isTTY ?? false;
|
|
132
|
+
}
|
|
133
|
+
function c(s, color) {
|
|
134
|
+
return tty() ? `${COLOR[color]}${s}${COLOR.reset}` : s;
|
|
135
|
+
}
|
|
136
|
+
function printHuman(cwd, report) {
|
|
137
|
+
consola.log("");
|
|
138
|
+
consola.log(` ${c("⬡ creek doctor", "bold")} ${c(cwd, "dim")}`);
|
|
139
|
+
consola.log(` ${c("archetype:", "dim")} ${report.archetype ?? "unknown"}`);
|
|
140
|
+
consola.log("");
|
|
141
|
+
if (report.findings.length === 0) {
|
|
142
|
+
consola.log(` ${c("✓", "green")} No issues detected. Deploy is good to go.`);
|
|
143
|
+
consola.log("");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const order = ["error", "warn", "info"];
|
|
147
|
+
const grouped = groupBy(report.findings, (f) => f.severity);
|
|
148
|
+
for (const sev of order) {
|
|
149
|
+
const bucket = grouped.get(sev) ?? [];
|
|
150
|
+
for (const f of bucket) {
|
|
151
|
+
printFinding(f);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const parts = [];
|
|
155
|
+
if (report.summary.error)
|
|
156
|
+
parts.push(c(`${report.summary.error} error${s(report.summary.error)}`, "red"));
|
|
157
|
+
if (report.summary.warn)
|
|
158
|
+
parts.push(c(`${report.summary.warn} warning${s(report.summary.warn)}`, "yellow"));
|
|
159
|
+
if (report.summary.info)
|
|
160
|
+
parts.push(c(`${report.summary.info} info`, "cyan"));
|
|
161
|
+
consola.log(` Summary: ${parts.join(", ")}`);
|
|
162
|
+
consola.log("");
|
|
163
|
+
}
|
|
164
|
+
function printFinding(f) {
|
|
165
|
+
const icon = f.severity === "error" ? c("✗", "red")
|
|
166
|
+
: f.severity === "warn" ? c("⚠", "yellow")
|
|
167
|
+
: c("ℹ", "cyan");
|
|
168
|
+
consola.log(` ${icon} ${c(f.title, "bold")} ${c(`[${f.code}]`, "gray")}`);
|
|
169
|
+
for (const line of f.detail.split("\n")) {
|
|
170
|
+
consola.log(` ${c(line, "dim")}`);
|
|
171
|
+
}
|
|
172
|
+
consola.log(` ${c("→ fix:", "cyan")}`);
|
|
173
|
+
for (const line of f.fix.split("\n")) {
|
|
174
|
+
consola.log(` ${line}`);
|
|
175
|
+
}
|
|
176
|
+
if (f.references?.length) {
|
|
177
|
+
consola.log(` ${c("→ refs:", "dim")} ${f.references.join(", ")}`);
|
|
178
|
+
}
|
|
179
|
+
consola.log("");
|
|
180
|
+
}
|
|
181
|
+
function groupBy(arr, key) {
|
|
182
|
+
const out = new Map();
|
|
183
|
+
for (const v of arr) {
|
|
184
|
+
const k = key(v);
|
|
185
|
+
const bucket = out.get(k) ?? [];
|
|
186
|
+
bucket.push(v);
|
|
187
|
+
out.set(k, bucket);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function s(n) {
|
|
192
|
+
return n === 1 ? "" : "s";
|
|
193
|
+
}
|
|
194
|
+
// ─── `--last` failure diagnosis ─────────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// CK-code → one-line fix hint. Source of truth in product terms is
|
|
197
|
+
// skills/creek/references/diagnosis.md; the MCP server's get_build_log
|
|
198
|
+
// tool carries the same mapping (packages/mcp-server/src/tools.ts
|
|
199
|
+
// CK_FIX_HINTS). Keep all three in sync when adding a new CK-* rule.
|
|
200
|
+
const CK_FIX_HINTS = {
|
|
201
|
+
"CK-NO-CONFIG": "Run `creek init` to scaffold a creek.toml, or cd to a directory that contains creek.toml / wrangler.* / package.json / index.html.",
|
|
202
|
+
"CK-NOTHING-TO-DEPLOY": "Run the project's build command so there's output in [build].output, or set [build].command in creek.toml if the project needs one.",
|
|
203
|
+
"CK-DB-DUAL-DRIVER-SPLIT": "Consolidate the split db.local.ts + db.prod.ts files. Share schema.ts and routes.ts; keep only thin boot files (server/local.ts for dev, server/worker.ts for prod) that differ in driver setup. See examples/vite-react-drizzle.",
|
|
204
|
+
"CK-SYNC-SQLITE": "better-sqlite3 is synchronous and won't run on Workers. Migrate to an async ORM with a D1 adapter — Drizzle or Kysely are the drop-in paths.",
|
|
205
|
+
"CK-PRISMA-SQLITE": "Prisma's SQLite datasource isn't supported on Cloudflare Workers. Switch to Drizzle or Kysely with a D1 adapter.",
|
|
206
|
+
"CK-RUNTIME-LOCKIN": "The project imports from @solcreek/* runtime packages. For a portable build that can deploy outside Creek, replace those with driver-level imports (e.g. drizzle-orm/d1 instead of creek's db re-export).",
|
|
207
|
+
"CK-CONFIG-OVERLAP": "Both creek.toml and wrangler.* are present. Pick one as the source of truth — creek.toml is preferred; remove wrangler.* or update any shared fields to match.",
|
|
208
|
+
};
|
|
209
|
+
function suggestFix(code) {
|
|
210
|
+
if (!code)
|
|
211
|
+
return null;
|
|
212
|
+
return CK_FIX_HINTS[code] ?? null;
|
|
213
|
+
}
|
|
214
|
+
async function runLastFailureDiagnosis(opts) {
|
|
215
|
+
const token = getToken();
|
|
216
|
+
if (!token) {
|
|
217
|
+
if (opts.jsonMode) {
|
|
218
|
+
jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
|
|
219
|
+
}
|
|
220
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
// Resolve project slug — prefer --project, fall back to creek.toml
|
|
224
|
+
let slug = opts.project;
|
|
225
|
+
if (!slug) {
|
|
226
|
+
const creekToml = join(opts.cwd, "creek.toml");
|
|
227
|
+
if (existsSync(creekToml)) {
|
|
228
|
+
const raw = safeRead(creekToml);
|
|
229
|
+
const match = raw?.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
230
|
+
if (match)
|
|
231
|
+
slug = match[1];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!slug) {
|
|
235
|
+
const msg = "No project slug. Pass --project <slug> or run from a directory with creek.toml.";
|
|
236
|
+
if (opts.jsonMode)
|
|
237
|
+
jsonOutput({ ok: false, error: "no_project", message: msg }, 1);
|
|
238
|
+
consola.error(msg);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
242
|
+
// Find the most recent failed deployment.
|
|
243
|
+
let deployments;
|
|
244
|
+
try {
|
|
245
|
+
deployments = await client.listDeployments(slug);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
const msg = err instanceof Error ? err.message : "Failed to list deployments";
|
|
249
|
+
if (opts.jsonMode)
|
|
250
|
+
jsonOutput({ ok: false, error: "api_error", message: msg }, 1);
|
|
251
|
+
consola.error(msg);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
const failed = deployments.find((d) => d.status === "failed");
|
|
255
|
+
if (!failed) {
|
|
256
|
+
const msg = `No failed deployments for ${slug}. Last ${deployments.length} deploys succeeded.`;
|
|
257
|
+
if (opts.jsonMode)
|
|
258
|
+
jsonOutput({ ok: true, project: slug, failed: null, message: msg }, 0);
|
|
259
|
+
consola.log("");
|
|
260
|
+
consola.log(` ${c("⬡ creek doctor --last", "bold")} ${c(slug, "dim")}`);
|
|
261
|
+
consola.log(` ${c("✓", "green")} ${msg}`);
|
|
262
|
+
consola.log("");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// Pull the build log.
|
|
266
|
+
let log;
|
|
267
|
+
try {
|
|
268
|
+
log = await client.getBuildLog(slug, failed.id);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
const msg = err instanceof Error ? err.message : "Failed to read build log";
|
|
272
|
+
if (opts.jsonMode)
|
|
273
|
+
jsonOutput({ ok: false, error: "api_error", message: msg }, 1);
|
|
274
|
+
consola.error(msg);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const meta = log.metadata;
|
|
278
|
+
const errorCode = meta?.errorCode ?? null;
|
|
279
|
+
const errorStep = meta?.errorStep ?? null;
|
|
280
|
+
const fix = suggestFix(errorCode);
|
|
281
|
+
if (opts.jsonMode) {
|
|
282
|
+
jsonOutput({
|
|
283
|
+
ok: true,
|
|
284
|
+
project: slug,
|
|
285
|
+
failed: {
|
|
286
|
+
id: failed.id,
|
|
287
|
+
version: failed.version,
|
|
288
|
+
branch: failed.branch,
|
|
289
|
+
commitSha: failed.commit_sha,
|
|
290
|
+
errorCode,
|
|
291
|
+
errorStep,
|
|
292
|
+
suggestedFix: fix,
|
|
293
|
+
failedStep: failed.failed_step,
|
|
294
|
+
errorMessage: failed.error_message,
|
|
295
|
+
},
|
|
296
|
+
}, 0, [
|
|
297
|
+
{
|
|
298
|
+
command: `creek deployments logs ${failed.id.slice(0, 8)} --json`,
|
|
299
|
+
description: "Read the full build log",
|
|
300
|
+
},
|
|
301
|
+
{ command: `creek deploy --json`, description: "Redeploy after fixing" },
|
|
302
|
+
]);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Human output.
|
|
306
|
+
consola.log("");
|
|
307
|
+
consola.log(` ${c("⬡ creek doctor --last", "bold")} ${c(slug, "dim")}`);
|
|
308
|
+
consola.log(` ${c("failed deploy:", "dim")} ${failed.id.slice(0, 8)}${failed.branch ? ` (${failed.branch})` : ""}`);
|
|
309
|
+
consola.log("");
|
|
310
|
+
if (errorCode) {
|
|
311
|
+
consola.log(` ${c("✗", "red")} ${c(errorCode, "bold")} ${c(`at step: ${errorStep ?? "unknown"}`, "gray")}`);
|
|
312
|
+
}
|
|
313
|
+
else if (errorStep) {
|
|
314
|
+
consola.log(` ${c("✗", "red")} Failed at step: ${c(errorStep, "bold")}`);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
consola.log(` ${c("✗", "red")} Deploy failed — no structured errorCode available.`);
|
|
318
|
+
}
|
|
319
|
+
consola.log("");
|
|
320
|
+
if (fix) {
|
|
321
|
+
consola.log(` ${c("→ fix:", "cyan")}`);
|
|
322
|
+
for (const line of fix.split("\n"))
|
|
323
|
+
consola.log(` ${line}`);
|
|
324
|
+
consola.log("");
|
|
325
|
+
}
|
|
326
|
+
else if (errorCode) {
|
|
327
|
+
consola.log(` ${c("→ no mapped fix for", "dim")} ${c(errorCode, "bold")}${c(". Inspect the full log:", "dim")}`);
|
|
328
|
+
consola.log(` creek deployments logs ${failed.id.slice(0, 8)}`);
|
|
329
|
+
consola.log("");
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
consola.log(` ${c("→ inspect the full log:", "cyan")}`);
|
|
333
|
+
consola.log(` creek deployments logs ${failed.id.slice(0, 8)}`);
|
|
334
|
+
consola.log("");
|
|
335
|
+
}
|
|
336
|
+
if (failed.error_message) {
|
|
337
|
+
consola.log(` ${c("error message:", "dim")}`);
|
|
338
|
+
for (const line of failed.error_message.split("\n").slice(0, 8)) {
|
|
339
|
+
consola.log(` ${c(line, "gray")}`);
|
|
340
|
+
}
|
|
341
|
+
consola.log("");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration logic for `creek db migrate`.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions are exported for testing. The CLI command wires
|
|
5
|
+
* them together with the SDK client in db.ts.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Find the migration directory relative to cwd.
|
|
9
|
+
* Returns the absolute path of the first candidate that exists and
|
|
10
|
+
* contains at least one .sql file, or null if none found.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectMigrationDir(cwd: string): string | null;
|
|
13
|
+
export interface MigrationFile {
|
|
14
|
+
/** File name without directory (e.g. "0001_init.sql") */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Absolute path */
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read .sql files from a directory, sorted lexicographically.
|
|
21
|
+
* Non-.sql files and empty .sql files are skipped.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseMigrationFiles(dir: string): MigrationFile[];
|
|
24
|
+
/**
|
|
25
|
+
* Split a migration file's SQL content into individual statements.
|
|
26
|
+
*
|
|
27
|
+
* If the content contains Drizzle's `--> statement-breakpoint` marker,
|
|
28
|
+
* split on those. Otherwise split on semicolons. Empty statements are
|
|
29
|
+
* filtered out.
|
|
30
|
+
*/
|
|
31
|
+
export declare function splitStatements(sql: string): string[];
|
|
32
|
+
/**
|
|
33
|
+
* Given files on disk and names already applied, return the pending
|
|
34
|
+
* migrations in order.
|
|
35
|
+
*/
|
|
36
|
+
export declare function computePending(files: MigrationFile[], applied: Set<string>): MigrationFile[];
|
|
37
|
+
//# sourceMappingURL=migrate.d.ts.map
|