@solcreek/cli 0.4.10 → 0.4.12
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/logs-filter.d.ts +15 -0
- package/dist/commands/logs-filter.js +66 -0
- package/dist/commands/logs.d.ts +75 -0
- package/dist/commands/logs.js +355 -0
- package/dist/index.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure filter logic for `creek logs --follow`.
|
|
3
|
+
*
|
|
4
|
+
* Historical mode (`creek logs --since`) gets server-side filtering;
|
|
5
|
+
* live mode (`--follow`) receives ALL log events for the project's
|
|
6
|
+
* realtime room and must filter client-side. This module is the
|
|
7
|
+
* mirror of control-plane/src/modules/logs/query.ts:matchesQuery —
|
|
8
|
+
* if those drift, --follow shows different entries than --since
|
|
9
|
+
* for the same flags.
|
|
10
|
+
*/
|
|
11
|
+
import type { LogEntry, LogQueryFilters } from "@solcreek/sdk";
|
|
12
|
+
export declare function matchesClientSide(entry: LogEntry, filters: LogQueryFilters): boolean;
|
|
13
|
+
export declare function describeFilters(filters: LogQueryFilters): string;
|
|
14
|
+
export declare function safeStringify(v: unknown): string;
|
|
15
|
+
//# sourceMappingURL=logs-filter.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure filter logic for `creek logs --follow`.
|
|
3
|
+
*
|
|
4
|
+
* Historical mode (`creek logs --since`) gets server-side filtering;
|
|
5
|
+
* live mode (`--follow`) receives ALL log events for the project's
|
|
6
|
+
* realtime room and must filter client-side. This module is the
|
|
7
|
+
* mirror of control-plane/src/modules/logs/query.ts:matchesQuery —
|
|
8
|
+
* if those drift, --follow shows different entries than --since
|
|
9
|
+
* for the same flags.
|
|
10
|
+
*/
|
|
11
|
+
export function matchesClientSide(entry, filters) {
|
|
12
|
+
if (filters.outcomes?.length && !filters.outcomes.includes(entry.outcome))
|
|
13
|
+
return false;
|
|
14
|
+
if (filters.scriptTypes?.length && !filters.scriptTypes.includes(entry.scriptType))
|
|
15
|
+
return false;
|
|
16
|
+
if (filters.deployment && entry.deployId !== filters.deployment)
|
|
17
|
+
return false;
|
|
18
|
+
if (filters.branch && entry.branch !== filters.branch)
|
|
19
|
+
return false;
|
|
20
|
+
if (filters.levels?.length) {
|
|
21
|
+
const hit = entry.logs.some((l) => filters.levels.includes(l.level));
|
|
22
|
+
if (!hit)
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (filters.search) {
|
|
26
|
+
if (!searchMatches(entry, filters.search))
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function searchMatches(entry, needle) {
|
|
32
|
+
const n = needle.toLowerCase();
|
|
33
|
+
const haystack = entry.logs
|
|
34
|
+
.flatMap((l) => l.message.map((m) => (typeof m === "string" ? m : safeStringify(m))))
|
|
35
|
+
.join(" ") +
|
|
36
|
+
" " +
|
|
37
|
+
entry.exceptions.map((e) => `${e.name} ${e.message}`).join(" ") +
|
|
38
|
+
" " +
|
|
39
|
+
(entry.request?.url ?? "");
|
|
40
|
+
return haystack.toLowerCase().includes(n);
|
|
41
|
+
}
|
|
42
|
+
export function describeFilters(filters) {
|
|
43
|
+
const bits = [];
|
|
44
|
+
if (filters.outcomes?.length)
|
|
45
|
+
bits.push(`outcome=${filters.outcomes.join(",")}`);
|
|
46
|
+
if (filters.scriptTypes?.length)
|
|
47
|
+
bits.push(`scriptType=${filters.scriptTypes.join(",")}`);
|
|
48
|
+
if (filters.deployment)
|
|
49
|
+
bits.push(`deployment=${filters.deployment}`);
|
|
50
|
+
if (filters.branch)
|
|
51
|
+
bits.push(`branch=${filters.branch}`);
|
|
52
|
+
if (filters.levels?.length)
|
|
53
|
+
bits.push(`level=${filters.levels.join(",")}`);
|
|
54
|
+
if (filters.search)
|
|
55
|
+
bits.push(`search="${filters.search}"`);
|
|
56
|
+
return bits.length === 0 ? "(none)" : bits.join(" ");
|
|
57
|
+
}
|
|
58
|
+
export function safeStringify(v) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(v);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return String(v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=logs-filter.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `creek logs` — read structured tenant logs from R2 archive.
|
|
3
|
+
*
|
|
4
|
+
* Auth: requires `creek login`. Server is responsible for tenant
|
|
5
|
+
* isolation — the CLI simply targets the project slug, the
|
|
6
|
+
* authenticated session decides which team's logs are visible.
|
|
7
|
+
*
|
|
8
|
+
* Project resolution: --project flag wins; otherwise resolve from
|
|
9
|
+
* cwd creek.toml / wrangler.*. If neither, error with hint.
|
|
10
|
+
*
|
|
11
|
+
* `--follow` is reserved for Step 7 (WebSocket subscribe). For now
|
|
12
|
+
* the command is one-shot historical query.
|
|
13
|
+
*
|
|
14
|
+
* Output:
|
|
15
|
+
* default → human-friendly multi-line per entry, colored by
|
|
16
|
+
* outcome (ok=dim, exception=red, etc.)
|
|
17
|
+
* --json → newline-delimited LogEntry JSON, suitable for `| jq`
|
|
18
|
+
*/
|
|
19
|
+
export declare const logsCommand: import("citty").CommandDef<{
|
|
20
|
+
json: {
|
|
21
|
+
type: "boolean";
|
|
22
|
+
description: string;
|
|
23
|
+
default: boolean;
|
|
24
|
+
};
|
|
25
|
+
yes: {
|
|
26
|
+
type: "boolean";
|
|
27
|
+
description: string;
|
|
28
|
+
default: boolean;
|
|
29
|
+
};
|
|
30
|
+
project: {
|
|
31
|
+
type: "string";
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
34
|
+
since: {
|
|
35
|
+
type: "string";
|
|
36
|
+
description: string;
|
|
37
|
+
};
|
|
38
|
+
until: {
|
|
39
|
+
type: "string";
|
|
40
|
+
description: string;
|
|
41
|
+
};
|
|
42
|
+
outcome: {
|
|
43
|
+
type: "string";
|
|
44
|
+
description: string;
|
|
45
|
+
};
|
|
46
|
+
"script-type": {
|
|
47
|
+
type: "string";
|
|
48
|
+
description: string;
|
|
49
|
+
};
|
|
50
|
+
deployment: {
|
|
51
|
+
type: "string";
|
|
52
|
+
description: string;
|
|
53
|
+
};
|
|
54
|
+
branch: {
|
|
55
|
+
type: "string";
|
|
56
|
+
description: string;
|
|
57
|
+
};
|
|
58
|
+
level: {
|
|
59
|
+
type: "string";
|
|
60
|
+
description: string;
|
|
61
|
+
};
|
|
62
|
+
search: {
|
|
63
|
+
type: "string";
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
66
|
+
limit: {
|
|
67
|
+
type: "string";
|
|
68
|
+
description: string;
|
|
69
|
+
};
|
|
70
|
+
follow: {
|
|
71
|
+
type: "boolean";
|
|
72
|
+
description: string;
|
|
73
|
+
};
|
|
74
|
+
}>;
|
|
75
|
+
//# sourceMappingURL=logs.d.ts.map
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import WebSocket from "ws";
|
|
4
|
+
import { CreekClient, resolveConfig, ConfigNotFoundError, } from "@solcreek/sdk";
|
|
5
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
6
|
+
import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS, } from "../utils/output.js";
|
|
7
|
+
import { matchesClientSide, describeFilters, safeStringify as ssExport } from "./logs-filter.js";
|
|
8
|
+
/**
|
|
9
|
+
* `creek logs` — read structured tenant logs from R2 archive.
|
|
10
|
+
*
|
|
11
|
+
* Auth: requires `creek login`. Server is responsible for tenant
|
|
12
|
+
* isolation — the CLI simply targets the project slug, the
|
|
13
|
+
* authenticated session decides which team's logs are visible.
|
|
14
|
+
*
|
|
15
|
+
* Project resolution: --project flag wins; otherwise resolve from
|
|
16
|
+
* cwd creek.toml / wrangler.*. If neither, error with hint.
|
|
17
|
+
*
|
|
18
|
+
* `--follow` is reserved for Step 7 (WebSocket subscribe). For now
|
|
19
|
+
* the command is one-shot historical query.
|
|
20
|
+
*
|
|
21
|
+
* Output:
|
|
22
|
+
* default → human-friendly multi-line per entry, colored by
|
|
23
|
+
* outcome (ok=dim, exception=red, etc.)
|
|
24
|
+
* --json → newline-delimited LogEntry JSON, suitable for `| jq`
|
|
25
|
+
*/
|
|
26
|
+
export const logsCommand = defineCommand({
|
|
27
|
+
meta: {
|
|
28
|
+
name: "logs",
|
|
29
|
+
description: "Read recent log entries for a project",
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
project: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Project slug. Defaults to creek.toml in cwd.",
|
|
35
|
+
},
|
|
36
|
+
since: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Time window start. Relative (1h, 30m, 2d) or ISO. Default: 1h",
|
|
39
|
+
},
|
|
40
|
+
until: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: 'Time window end. "now" or ISO. Default: now',
|
|
43
|
+
},
|
|
44
|
+
outcome: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Filter by tail outcome. Repeatable via comma (ok,exception).",
|
|
47
|
+
},
|
|
48
|
+
"script-type": {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Filter by production/branch/deployment. Repeatable via comma.",
|
|
51
|
+
},
|
|
52
|
+
deployment: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: "8-hex deploy id — scopes to that single deployment preview.",
|
|
55
|
+
},
|
|
56
|
+
branch: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Branch name — scopes to that branch preview.",
|
|
59
|
+
},
|
|
60
|
+
level: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Filter by console level (error,warn,...). Entry needs at least one matching log line.",
|
|
63
|
+
},
|
|
64
|
+
search: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Substring match against console messages, exceptions, and request URLs.",
|
|
67
|
+
},
|
|
68
|
+
limit: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Max entries to print. Default 100, max 1000.",
|
|
71
|
+
},
|
|
72
|
+
follow: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
description: "Live tail via WebSocket. Prints recent context first, then streams new entries until Ctrl+C.",
|
|
75
|
+
},
|
|
76
|
+
...globalArgs,
|
|
77
|
+
},
|
|
78
|
+
async run({ args }) {
|
|
79
|
+
const jsonMode = resolveJsonMode(args);
|
|
80
|
+
const token = getToken();
|
|
81
|
+
if (!token) {
|
|
82
|
+
if (jsonMode)
|
|
83
|
+
jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
|
|
84
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const projectSlug = await resolveProjectSlug(args.project, jsonMode);
|
|
88
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
89
|
+
const filters = {
|
|
90
|
+
...(args.since ? { since: args.since } : {}),
|
|
91
|
+
...(args.until ? { until: args.until } : {}),
|
|
92
|
+
...(args.deployment ? { deployment: args.deployment } : {}),
|
|
93
|
+
...(args.branch ? { branch: args.branch } : {}),
|
|
94
|
+
...(args.search ? { search: args.search } : {}),
|
|
95
|
+
...(args.limit ? { limit: Number(args.limit) } : {}),
|
|
96
|
+
...(args.outcome
|
|
97
|
+
? { outcomes: parseList(args.outcome) }
|
|
98
|
+
: {}),
|
|
99
|
+
...(args["script-type"]
|
|
100
|
+
? {
|
|
101
|
+
scriptTypes: parseList(args["script-type"]),
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
...(args.level
|
|
105
|
+
? { levels: parseList(args.level) }
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
108
|
+
let response;
|
|
109
|
+
try {
|
|
110
|
+
response = await client.getLogs(projectSlug, filters);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
114
|
+
if (jsonMode)
|
|
115
|
+
jsonOutput({ ok: false, error: "logs_failed", message: msg }, 1, []);
|
|
116
|
+
consola.error(`Failed to read logs: ${msg}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (jsonMode) {
|
|
120
|
+
// ndjson — easy to pipe to jq
|
|
121
|
+
for (const entry of response.entries) {
|
|
122
|
+
process.stdout.write(JSON.stringify(entry) + "\n");
|
|
123
|
+
}
|
|
124
|
+
if (response.truncated) {
|
|
125
|
+
process.stderr.write(`# truncated — more entries match. Refine --since/--limit to narrow.\n`);
|
|
126
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* Live tail via WebSocket. Mints a 5-min token via control-plane,
|
|
155
|
+
* connects to realtime-worker, and prints incoming `type: "log"`
|
|
156
|
+
* messages through the same printEntry path used for historical mode.
|
|
157
|
+
*
|
|
158
|
+
* Reconnect: realtime-worker drops the connection on any 5xx upstream
|
|
159
|
+
* (DO eviction, DO error, etc.). We retry with capped exponential
|
|
160
|
+
* backoff, re-mint the token (it could have expired during downtime).
|
|
161
|
+
*
|
|
162
|
+
* Token refresh: a single token is good for 5 min. For long-running
|
|
163
|
+
* tails we re-mint at 4 min — well before expiry — to avoid a
|
|
164
|
+
* mid-window 401 race with the WS handshake.
|
|
165
|
+
*
|
|
166
|
+
* Filter parity: the same client-side filter we'd apply to historical
|
|
167
|
+
* entries is applied to live entries too. Realtime push doesn't know
|
|
168
|
+
* about query filters; we filter here so `--outcome exception
|
|
169
|
+
* --follow` shows only exceptions.
|
|
170
|
+
*/
|
|
171
|
+
async function follow(client, projectSlug, filters, initialSeenAfter, jsonMode) {
|
|
172
|
+
if (!jsonMode) {
|
|
173
|
+
consola.info(`Live tail — Ctrl+C to exit. Filtering: ${describeFilters(filters)}`);
|
|
174
|
+
}
|
|
175
|
+
let seenAfter = initialSeenAfter;
|
|
176
|
+
let stopped = false;
|
|
177
|
+
process.on("SIGINT", () => {
|
|
178
|
+
stopped = true;
|
|
179
|
+
if (!jsonMode) {
|
|
180
|
+
process.stderr.write("\n");
|
|
181
|
+
consola.info("Stopped.");
|
|
182
|
+
}
|
|
183
|
+
process.exit(0);
|
|
184
|
+
});
|
|
185
|
+
const TOKEN_REFRESH_MS = 4 * 60 * 1000;
|
|
186
|
+
let backoffMs = 500;
|
|
187
|
+
const BACKOFF_MAX_MS = 15_000;
|
|
188
|
+
while (!stopped) {
|
|
189
|
+
let mintedAt;
|
|
190
|
+
let wsUrl;
|
|
191
|
+
try {
|
|
192
|
+
const minted = await client.getLogsWsToken(projectSlug);
|
|
193
|
+
mintedAt = Date.now();
|
|
194
|
+
wsUrl = minted.wsUrl;
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (stopped)
|
|
198
|
+
return;
|
|
199
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
200
|
+
if (jsonMode)
|
|
201
|
+
process.stderr.write(`# ws-token failed: ${msg}\n`);
|
|
202
|
+
else
|
|
203
|
+
consola.warn(`Failed to mint subscribe token: ${msg}. Retrying in ${Math.round(backoffMs / 1000)}s.`);
|
|
204
|
+
await sleep(backoffMs);
|
|
205
|
+
backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const ws = new WebSocket(wsUrl);
|
|
209
|
+
let closed = false;
|
|
210
|
+
let refreshTimer = null;
|
|
211
|
+
await new Promise((resolve) => {
|
|
212
|
+
ws.on("open", () => {
|
|
213
|
+
backoffMs = 500; // reset on successful connect
|
|
214
|
+
// Force a clean reconnect just before token expiry.
|
|
215
|
+
const elapsed = Date.now() - mintedAt;
|
|
216
|
+
const remaining = Math.max(5_000, TOKEN_REFRESH_MS - elapsed);
|
|
217
|
+
refreshTimer = setTimeout(() => {
|
|
218
|
+
if (!closed) {
|
|
219
|
+
try {
|
|
220
|
+
ws.close(1000, "token refresh");
|
|
221
|
+
}
|
|
222
|
+
catch { /* already closed */ }
|
|
223
|
+
}
|
|
224
|
+
}, remaining);
|
|
225
|
+
});
|
|
226
|
+
ws.on("message", (data) => {
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = JSON.parse(data.toString());
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (parsed.type !== "log" || !parsed.entry)
|
|
235
|
+
return;
|
|
236
|
+
const entry = parsed.entry;
|
|
237
|
+
// Skip duplicates the historical R2 read already showed.
|
|
238
|
+
if (entry.timestamp <= seenAfter)
|
|
239
|
+
return;
|
|
240
|
+
// Apply client-side filters so flags work in --follow mode too.
|
|
241
|
+
if (!matchesClientSide(entry, filters))
|
|
242
|
+
return;
|
|
243
|
+
seenAfter = Math.max(seenAfter, entry.timestamp);
|
|
244
|
+
if (jsonMode) {
|
|
245
|
+
process.stdout.write(JSON.stringify(entry) + "\n");
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
printEntry(entry);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
ws.on("close", () => {
|
|
252
|
+
closed = true;
|
|
253
|
+
if (refreshTimer)
|
|
254
|
+
clearTimeout(refreshTimer);
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
ws.on("error", () => {
|
|
258
|
+
// The "close" event will fire after this; resolve happens there.
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
if (stopped)
|
|
262
|
+
return;
|
|
263
|
+
// Brief backoff before reconnect — only meaningful if the close
|
|
264
|
+
// wasn't from our own token-refresh timer.
|
|
265
|
+
await sleep(backoffMs);
|
|
266
|
+
backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function sleep(ms) {
|
|
270
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
271
|
+
}
|
|
272
|
+
async function resolveProjectSlug(override, jsonMode) {
|
|
273
|
+
if (override)
|
|
274
|
+
return override;
|
|
275
|
+
let resolved;
|
|
276
|
+
try {
|
|
277
|
+
resolved = resolveConfig(process.cwd());
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (err instanceof ConfigNotFoundError) {
|
|
281
|
+
if (jsonMode)
|
|
282
|
+
jsonOutput({ ok: false, error: "no_project", message: "No project config in cwd" }, 1, NO_PROJECT_BREADCRUMBS);
|
|
283
|
+
consola.error("No project config in cwd. Pass --project <slug>.");
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
return resolved.projectName;
|
|
289
|
+
}
|
|
290
|
+
function parseList(input) {
|
|
291
|
+
return input
|
|
292
|
+
.split(",")
|
|
293
|
+
.map((s) => s.trim())
|
|
294
|
+
.filter(Boolean);
|
|
295
|
+
}
|
|
296
|
+
const COLOR = {
|
|
297
|
+
reset: "\x1b[0m",
|
|
298
|
+
dim: "\x1b[2m",
|
|
299
|
+
red: "\x1b[31m",
|
|
300
|
+
yellow: "\x1b[33m",
|
|
301
|
+
green: "\x1b[32m",
|
|
302
|
+
cyan: "\x1b[36m",
|
|
303
|
+
gray: "\x1b[90m",
|
|
304
|
+
};
|
|
305
|
+
function color(s, c) {
|
|
306
|
+
return process.stdout.isTTY ? `${COLOR[c]}${s}${COLOR.reset}` : s;
|
|
307
|
+
}
|
|
308
|
+
function printEntry(entry) {
|
|
309
|
+
const ts = new Date(entry.timestamp).toISOString().replace("T", " ").slice(0, 19);
|
|
310
|
+
const outcomeColor = entry.outcome === "ok"
|
|
311
|
+
? "gray"
|
|
312
|
+
: entry.outcome === "exception"
|
|
313
|
+
? "red"
|
|
314
|
+
: "yellow";
|
|
315
|
+
const status = entry.request?.status;
|
|
316
|
+
const statusStr = status === undefined
|
|
317
|
+
? ""
|
|
318
|
+
: status >= 500
|
|
319
|
+
? color(String(status), "red")
|
|
320
|
+
: status >= 400
|
|
321
|
+
? color(String(status), "yellow")
|
|
322
|
+
: color(String(status), "green");
|
|
323
|
+
const variant = entry.scriptType === "production"
|
|
324
|
+
? ""
|
|
325
|
+
: entry.scriptType === "branch"
|
|
326
|
+
? ` [branch ${entry.branch}]`
|
|
327
|
+
: ` [deploy ${entry.deployId}]`;
|
|
328
|
+
const headline = [
|
|
329
|
+
color(ts, "dim"),
|
|
330
|
+
color(entry.outcome, outcomeColor),
|
|
331
|
+
entry.request?.method ?? "—",
|
|
332
|
+
entry.request?.url
|
|
333
|
+
? new URL(entry.request.url).pathname + new URL(entry.request.url).search
|
|
334
|
+
: "—",
|
|
335
|
+
statusStr,
|
|
336
|
+
color(variant, "dim"),
|
|
337
|
+
]
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
.join(" ");
|
|
340
|
+
process.stdout.write(headline + "\n");
|
|
341
|
+
for (const log of entry.logs) {
|
|
342
|
+
const levelColor = log.level === "error" ? "red" : log.level === "warn" ? "yellow" : "cyan";
|
|
343
|
+
const msg = log.message
|
|
344
|
+
.map((m) => (typeof m === "string" ? m : safeStringify(m)))
|
|
345
|
+
.join(" ");
|
|
346
|
+
process.stdout.write(` ${color(log.level.padEnd(5), levelColor)} ${msg}\n`);
|
|
347
|
+
}
|
|
348
|
+
for (const ex of entry.exceptions) {
|
|
349
|
+
process.stdout.write(` ${color("exc", "red")} ${color(ex.name, "red")}: ${ex.message}\n`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// safeStringify lives in logs-filter.js (re-exported for nested
|
|
353
|
+
// console.log message rendering in printEntry below).
|
|
354
|
+
const safeStringify = ssExport;
|
|
355
|
+
//# sourceMappingURL=logs.js.map
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { devCommand } from "./commands/dev.js";
|
|
|
18
18
|
import { rollbackCommand } from "./commands/rollback.js";
|
|
19
19
|
import { opsCommand } from "./commands/ops.js";
|
|
20
20
|
import { queueCommand } from "./commands/queue.js";
|
|
21
|
+
import { logsCommand } from "./commands/logs.js";
|
|
21
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
23
|
const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
23
24
|
// Read version from the "creek" facade package (what users install),
|
|
@@ -43,6 +44,7 @@ const main = defineCommand({
|
|
|
43
44
|
status: statusCommand,
|
|
44
45
|
projects: projectsCommand,
|
|
45
46
|
deployments: deploymentsCommand,
|
|
47
|
+
logs: logsCommand,
|
|
46
48
|
login: loginCommand,
|
|
47
49
|
whoami: whoamiCommand,
|
|
48
50
|
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.12",
|
|
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.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/dom": "^10.4.1",
|