@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 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
@@ -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
- if (args.follow) {
142
- // Track the newest historical timestamp so we can drop any
143
- // duplicates that the WS would otherwise replay (R2 → realtime
144
- // race window — the same event can appear on both within a
145
- // ~second of being captured).
146
- const seenAfter = response.entries.length > 0
147
- ? Math.max(...response.entries.map((e) => e.timestamp))
148
- : 0;
149
- await follow(client, projectSlug, filters, seenAfter, jsonMode);
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.12",
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.5"
36
+ "@solcreek/sdk": "0.4.6"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@testing-library/dom": "^10.4.1",