@solcreek/cli 0.4.12 → 0.4.14
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/doctor.d.ts +29 -0
- package/dist/commands/doctor.js +174 -0
- package/dist/commands/logs.js +28 -25
- package/dist/index.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,6 +67,9 @@ creek deploy https://github.com/user/repo/tree/main/packages/app
|
|
|
67
67
|
| `creek deploy --dry-run` | Show the deploy plan without executing (agent-safe) |
|
|
68
68
|
| `creek projects` | List your projects |
|
|
69
69
|
| `creek deployments` | List deployments for a project |
|
|
70
|
+
| `creek logs` | Read recent log entries (R2 archive) |
|
|
71
|
+
| `creek logs --follow` | Live tail via WebSocket until Ctrl+C |
|
|
72
|
+
| `creek logs --outcome exception` | Filter by tail outcome (or `--deployment`, `--branch`, `--level`, `--search`) |
|
|
70
73
|
| `creek status` | Show current project status |
|
|
71
74
|
| `creek login` | Authenticate with Creek |
|
|
72
75
|
| `creek login --token <key>` | Authenticate in CI/CD (non-interactive) |
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}>;
|
|
29
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1,174 @@
|
|
|
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 { globalArgs, resolveJsonMode, jsonOutput, } from "../utils/output.js";
|
|
7
|
+
/**
|
|
8
|
+
* `creek doctor` — pre-deploy sanity check.
|
|
9
|
+
*
|
|
10
|
+
* Runs the SDK rule engine against the current project, reports
|
|
11
|
+
* findings. Exits 0 if ok, 1 if any error-severity finding fires.
|
|
12
|
+
*
|
|
13
|
+
* This is the one command designed for LLM agents to invoke before
|
|
14
|
+
* `creek deploy`. With `--json` the output is ndjson-adjacent (pretty
|
|
15
|
+
* JSON, but parseable), letting an agent look up fixes by stable
|
|
16
|
+
* CK-* codes and apply them without re-reading the source.
|
|
17
|
+
*/
|
|
18
|
+
export const doctorCommand = defineCommand({
|
|
19
|
+
meta: {
|
|
20
|
+
name: "doctor",
|
|
21
|
+
description: "Analyze the project for pre-deploy issues — missing build output, deprecated config keys, Workers-incompatible deps, portability leaks.",
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
path: {
|
|
25
|
+
type: "positional",
|
|
26
|
+
description: "Project directory to analyze. Defaults to cwd.",
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
29
|
+
...globalArgs,
|
|
30
|
+
},
|
|
31
|
+
async run({ args }) {
|
|
32
|
+
const cwd = resolve(args.path ?? process.cwd());
|
|
33
|
+
const jsonMode = resolveJsonMode(args);
|
|
34
|
+
const ctx = buildContext(cwd);
|
|
35
|
+
const report = runDoctor(ctx);
|
|
36
|
+
if (jsonMode) {
|
|
37
|
+
jsonOutput({
|
|
38
|
+
ok: report.ok,
|
|
39
|
+
cwd,
|
|
40
|
+
archetype: report.archetype,
|
|
41
|
+
summary: report.summary,
|
|
42
|
+
findings: report.findings,
|
|
43
|
+
}, report.ok ? 0 : 1);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
printHuman(cwd, report);
|
|
47
|
+
if (!report.ok)
|
|
48
|
+
process.exit(1);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
function buildContext(cwd) {
|
|
52
|
+
const fileExists = (relPath) => existsSync(join(cwd, relPath));
|
|
53
|
+
const creekTomlPath = join(cwd, "creek.toml");
|
|
54
|
+
const creekTomlRaw = existsSync(creekTomlPath)
|
|
55
|
+
? safeRead(creekTomlPath)
|
|
56
|
+
: null;
|
|
57
|
+
const pkgPath = join(cwd, "package.json");
|
|
58
|
+
const packageJson = existsSync(pkgPath)
|
|
59
|
+
? safeParseJson(pkgPath)
|
|
60
|
+
: null;
|
|
61
|
+
const resolved = resolveConfigSafely(cwd);
|
|
62
|
+
const allDeps = {
|
|
63
|
+
...(packageJson?.dependencies ?? {}),
|
|
64
|
+
...(packageJson?.devDependencies ?? {}),
|
|
65
|
+
};
|
|
66
|
+
return { cwd, resolved, packageJson, creekTomlRaw, fileExists, allDeps };
|
|
67
|
+
}
|
|
68
|
+
function resolveConfigSafely(cwd) {
|
|
69
|
+
try {
|
|
70
|
+
return resolveConfig(cwd);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (err instanceof ConfigNotFoundError)
|
|
74
|
+
return null;
|
|
75
|
+
// Other errors (parse failures) bubble as null — the rules will
|
|
76
|
+
// still pick up partial info from creekTomlRaw + packageJson.
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function safeRead(path) {
|
|
81
|
+
try {
|
|
82
|
+
return readFileSync(path, "utf8");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function safeParseJson(path) {
|
|
89
|
+
const raw = safeRead(path);
|
|
90
|
+
if (raw === null)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ─── Human output ───────────────────────────────────────────────────────
|
|
100
|
+
const COLOR = {
|
|
101
|
+
reset: "\x1b[0m",
|
|
102
|
+
bold: "\x1b[1m",
|
|
103
|
+
dim: "\x1b[2m",
|
|
104
|
+
red: "\x1b[31m",
|
|
105
|
+
yellow: "\x1b[33m",
|
|
106
|
+
green: "\x1b[32m",
|
|
107
|
+
cyan: "\x1b[36m",
|
|
108
|
+
gray: "\x1b[90m",
|
|
109
|
+
};
|
|
110
|
+
function tty() {
|
|
111
|
+
return process.stdout.isTTY ?? false;
|
|
112
|
+
}
|
|
113
|
+
function c(s, color) {
|
|
114
|
+
return tty() ? `${COLOR[color]}${s}${COLOR.reset}` : s;
|
|
115
|
+
}
|
|
116
|
+
function printHuman(cwd, report) {
|
|
117
|
+
consola.log("");
|
|
118
|
+
consola.log(` ${c("⬡ creek doctor", "bold")} ${c(cwd, "dim")}`);
|
|
119
|
+
consola.log(` ${c("archetype:", "dim")} ${report.archetype ?? "unknown"}`);
|
|
120
|
+
consola.log("");
|
|
121
|
+
if (report.findings.length === 0) {
|
|
122
|
+
consola.log(` ${c("✓", "green")} No issues detected. Deploy is good to go.`);
|
|
123
|
+
consola.log("");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const order = ["error", "warn", "info"];
|
|
127
|
+
const grouped = groupBy(report.findings, (f) => f.severity);
|
|
128
|
+
for (const sev of order) {
|
|
129
|
+
const bucket = grouped.get(sev) ?? [];
|
|
130
|
+
for (const f of bucket) {
|
|
131
|
+
printFinding(f);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const parts = [];
|
|
135
|
+
if (report.summary.error)
|
|
136
|
+
parts.push(c(`${report.summary.error} error${s(report.summary.error)}`, "red"));
|
|
137
|
+
if (report.summary.warn)
|
|
138
|
+
parts.push(c(`${report.summary.warn} warning${s(report.summary.warn)}`, "yellow"));
|
|
139
|
+
if (report.summary.info)
|
|
140
|
+
parts.push(c(`${report.summary.info} info`, "cyan"));
|
|
141
|
+
consola.log(` Summary: ${parts.join(", ")}`);
|
|
142
|
+
consola.log("");
|
|
143
|
+
}
|
|
144
|
+
function printFinding(f) {
|
|
145
|
+
const icon = f.severity === "error" ? c("✗", "red")
|
|
146
|
+
: f.severity === "warn" ? c("⚠", "yellow")
|
|
147
|
+
: c("ℹ", "cyan");
|
|
148
|
+
consola.log(` ${icon} ${c(f.title, "bold")} ${c(`[${f.code}]`, "gray")}`);
|
|
149
|
+
for (const line of f.detail.split("\n")) {
|
|
150
|
+
consola.log(` ${c(line, "dim")}`);
|
|
151
|
+
}
|
|
152
|
+
consola.log(` ${c("→ fix:", "cyan")}`);
|
|
153
|
+
for (const line of f.fix.split("\n")) {
|
|
154
|
+
consola.log(` ${line}`);
|
|
155
|
+
}
|
|
156
|
+
if (f.references?.length) {
|
|
157
|
+
consola.log(` ${c("→ refs:", "dim")} ${f.references.join(", ")}`);
|
|
158
|
+
}
|
|
159
|
+
consola.log("");
|
|
160
|
+
}
|
|
161
|
+
function groupBy(arr, key) {
|
|
162
|
+
const out = new Map();
|
|
163
|
+
for (const v of arr) {
|
|
164
|
+
const k = key(v);
|
|
165
|
+
const bucket = out.get(k) ?? [];
|
|
166
|
+
bucket.push(v);
|
|
167
|
+
out.set(k, bucket);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
function s(n) {
|
|
172
|
+
return n === 1 ? "" : "s";
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=doctor.js.map
|
package/dist/commands/logs.js
CHANGED
|
@@ -116,38 +116,38 @@ export const logsCommand = defineCommand({
|
|
|
116
116
|
consola.error(`Failed to read logs: ${msg}`);
|
|
117
117
|
process.exit(1);
|
|
118
118
|
}
|
|
119
|
+
// Print historical first, in both JSON and human mode. Ordering:
|
|
120
|
+
// oldest-at-top so the newest entry is closest to the prompt (and
|
|
121
|
+
// when --follow streams in below, it continues chronologically).
|
|
119
122
|
if (jsonMode) {
|
|
120
|
-
// ndjson — easy to pipe to jq
|
|
121
123
|
for (const entry of response.entries) {
|
|
122
124
|
process.stdout.write(JSON.stringify(entry) + "\n");
|
|
123
125
|
}
|
|
124
|
-
if (response.truncated) {
|
|
126
|
+
if (response.truncated && !args.follow) {
|
|
125
127
|
process.stderr.write(`# truncated — more entries match. Refine --since/--limit to narrow.\n`);
|
|
126
128
|
}
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (response.entries.length === 0 && !args.follow) {
|
|
130
|
-
consola.info("No log entries match the query.");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// Human output: oldest at top so the latest entry is closest to the prompt.
|
|
134
|
-
const ordered = [...response.entries].reverse();
|
|
135
|
-
for (const entry of ordered) {
|
|
136
|
-
printEntry(entry);
|
|
137
|
-
}
|
|
138
|
-
if (response.truncated && !args.follow) {
|
|
139
|
-
consola.warn(`Truncated to ${response.entries.length} entries — refine --since/--limit to see more.`);
|
|
140
129
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
else {
|
|
131
|
+
if (response.entries.length === 0 && !args.follow) {
|
|
132
|
+
consola.info("No log entries match the query.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const ordered = [...response.entries].reverse();
|
|
136
|
+
for (const entry of ordered) {
|
|
137
|
+
printEntry(entry);
|
|
138
|
+
}
|
|
139
|
+
if (response.truncated && !args.follow) {
|
|
140
|
+
consola.warn(`Truncated to ${response.entries.length} entries — refine --since/--limit to see more.`);
|
|
141
|
+
}
|
|
150
142
|
}
|
|
143
|
+
// No --follow → done.
|
|
144
|
+
if (!args.follow)
|
|
145
|
+
return;
|
|
146
|
+
// --follow → switch to WebSocket live tail.
|
|
147
|
+
const seenAfter = response.entries.length > 0
|
|
148
|
+
? Math.max(...response.entries.map((e) => e.timestamp))
|
|
149
|
+
: 0;
|
|
150
|
+
await follow(client, projectSlug, filters, seenAfter, jsonMode);
|
|
151
151
|
},
|
|
152
152
|
});
|
|
153
153
|
/**
|
|
@@ -254,8 +254,11 @@ async function follow(client, projectSlug, filters, initialSeenAfter, jsonMode)
|
|
|
254
254
|
clearTimeout(refreshTimer);
|
|
255
255
|
resolve();
|
|
256
256
|
});
|
|
257
|
-
ws.on("error", () => {
|
|
257
|
+
ws.on("error", (err) => {
|
|
258
258
|
// The "close" event will fire after this; resolve happens there.
|
|
259
|
+
// Surface to stderr so reconnect storms aren't invisible.
|
|
260
|
+
if (!jsonMode)
|
|
261
|
+
consola.warn(`ws error: ${err.message}`);
|
|
259
262
|
});
|
|
260
263
|
});
|
|
261
264
|
if (stopped)
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { rollbackCommand } from "./commands/rollback.js";
|
|
|
19
19
|
import { opsCommand } from "./commands/ops.js";
|
|
20
20
|
import { queueCommand } from "./commands/queue.js";
|
|
21
21
|
import { logsCommand } from "./commands/logs.js";
|
|
22
|
+
import { doctorCommand } from "./commands/doctor.js";
|
|
22
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
24
25
|
// Read version from the "creek" facade package (what users install),
|
|
@@ -45,6 +46,7 @@ const main = defineCommand({
|
|
|
45
46
|
projects: projectsCommand,
|
|
46
47
|
deployments: deploymentsCommand,
|
|
47
48
|
logs: logsCommand,
|
|
49
|
+
doctor: doctorCommand,
|
|
48
50
|
login: loginCommand,
|
|
49
51
|
whoami: whoamiCommand,
|
|
50
52
|
init: initCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solcreek/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.14",
|
|
4
4
|
"description": "CLI for the Creek deployment platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"esbuild": "^0.25.0",
|
|
34
34
|
"smol-toml": "^1.3.1",
|
|
35
35
|
"ws": "^8.20.0",
|
|
36
|
-
"@solcreek/sdk": "0.4.
|
|
36
|
+
"@solcreek/sdk": "0.4.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/dom": "^10.4.1",
|