@lizard-build/cli 0.3.41 → 0.3.43
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.js +103 -32
- package/dist/commands/logs.js.map +1 -1
- package/dist/lib/api.d.ts +13 -2
- package/dist/lib/api.js +143 -62
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/src/commands/logs.ts +110 -34
- package/src/lib/api.ts +158 -70
- package/src/lib/updater.ts +1 -1
- package/test/unit/sse.test.ts +173 -0
package/src/commands/logs.ts
CHANGED
|
@@ -2,8 +2,9 @@ import chalk from "chalk";
|
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { streamSSE, api, withScope, withQuery, type ResourceScope } from "../lib/api.js";
|
|
5
|
-
import { resolveProjectScope, resolveService,
|
|
6
|
-
import {
|
|
5
|
+
import { resolveProjectScope, resolveService, getActiveServiceWithKind } from "../lib/resolve.js";
|
|
6
|
+
import { getProjectLink } from "../lib/config.js";
|
|
7
|
+
import { info, error, warn, isTTY, isJSONMode, printJSON, table, statusColor, timeAgo } from "../lib/format.js";
|
|
7
8
|
|
|
8
9
|
export function registerLogs(program: Command) {
|
|
9
10
|
program
|
|
@@ -20,6 +21,10 @@ export function registerLogs(program: Command) {
|
|
|
20
21
|
error("Use --restarts (list) or --restart <id> (detail), not both");
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
24
|
+
if (opts.tail !== undefined && (opts.restarts !== undefined || opts.restart !== undefined)) {
|
|
25
|
+
error("--tail cannot be combined with --restarts/--restart");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
25
30
|
|
|
@@ -50,14 +55,22 @@ export function registerLogs(program: Command) {
|
|
|
50
55
|
// branch below talks to the API with a real service ID.
|
|
51
56
|
let serviceId: string | undefined;
|
|
52
57
|
let serviceName: string | undefined;
|
|
58
|
+
let serviceKind: "app" | "addon" | undefined;
|
|
53
59
|
if (opts.service) {
|
|
54
60
|
const svc = await resolveService(projectId, opts.service);
|
|
55
61
|
serviceId = svc.id;
|
|
56
62
|
serviceName = svc.name;
|
|
63
|
+
serviceKind = svc.kind;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
// --tail: fetch historical logs and exit
|
|
60
67
|
if (tailN !== undefined) {
|
|
68
|
+
if (opts.service && !serviceName) {
|
|
69
|
+
// An empty name would be dropped from the query string and the
|
|
70
|
+
// filter would silently match every service.
|
|
71
|
+
error(`Service "${opts.service}" has no name to filter logs by`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
61
74
|
// The project log stream tags entries with the service *name*
|
|
62
75
|
// (not ID), so the historical filter must use the name too.
|
|
63
76
|
const entries = await api.get<any[]>(
|
|
@@ -75,12 +88,13 @@ export function registerLogs(program: Command) {
|
|
|
75
88
|
|
|
76
89
|
if (!serviceId && isTTY() && !isJSONMode()) {
|
|
77
90
|
// Offer to pick a specific service or stream all
|
|
78
|
-
const data = await api.get<{ apps: any[] }>(
|
|
91
|
+
const data = await api.get<{ apps: any[]; addons?: any[] }>(
|
|
79
92
|
withScope(`/api/projects/${projectId}/services`, scope),
|
|
80
93
|
);
|
|
81
94
|
const apps = data.apps || [];
|
|
95
|
+
const addons = data.addons || [];
|
|
82
96
|
|
|
83
|
-
if (apps.length > 1) {
|
|
97
|
+
if (apps.length + addons.length > 1) {
|
|
84
98
|
const choices = [
|
|
85
99
|
{ value: "all", label: "All services", hint: "stream combined logs" },
|
|
86
100
|
...apps.map((a: any) => ({
|
|
@@ -88,24 +102,42 @@ export function registerLogs(program: Command) {
|
|
|
88
102
|
label: a.name || a.id,
|
|
89
103
|
hint: a.status,
|
|
90
104
|
})),
|
|
105
|
+
...addons.map((a: any) => ({
|
|
106
|
+
value: a.id,
|
|
107
|
+
label: a.name || a.addonType || a.id,
|
|
108
|
+
hint: a.status,
|
|
109
|
+
})),
|
|
91
110
|
];
|
|
92
111
|
const selected = await p.select({ message: "Show logs for", options: choices });
|
|
93
112
|
if (p.isCancel(selected)) process.exit(5);
|
|
94
|
-
if (selected !== "all")
|
|
113
|
+
if (selected !== "all") {
|
|
114
|
+
serviceId = selected as string;
|
|
115
|
+
serviceKind = addons.some((a: any) => a.id === selected) ? "addon" : "app";
|
|
116
|
+
}
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
120
|
+
const onReconnect = (attempt: number) =>
|
|
121
|
+
warn(`log stream lost — reconnecting (attempt ${attempt}/5)`);
|
|
122
|
+
const streamHandler = (event: string, data: string) => {
|
|
123
|
+
if (event === "error") {
|
|
124
|
+
error(data);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
printLogLine(data);
|
|
129
|
+
return true;
|
|
130
|
+
};
|
|
131
|
+
|
|
98
132
|
if (serviceId) {
|
|
99
|
-
// Stream logs for a specific
|
|
133
|
+
// Stream logs for a specific service. Addons live on a different
|
|
134
|
+
// endpoint — the app one 404s for them.
|
|
135
|
+
const streamPath =
|
|
136
|
+
serviceKind === "addon"
|
|
137
|
+
? withScope(`/api/projects/${projectId}/addons/${serviceId}/logs`, scope)
|
|
138
|
+
: `/api/apps/${serviceId}/logs`;
|
|
100
139
|
info(chalk.dim("Streaming logs... (Ctrl+C to stop)\n"));
|
|
101
|
-
await streamSSE(
|
|
102
|
-
if (event === "error") {
|
|
103
|
-
error(data);
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
printLogLine(data);
|
|
107
|
-
return true;
|
|
108
|
-
});
|
|
140
|
+
await streamSSE(streamPath, streamHandler, { reconnect: true, onReconnect });
|
|
109
141
|
return;
|
|
110
142
|
}
|
|
111
143
|
|
|
@@ -113,14 +145,8 @@ export function registerLogs(program: Command) {
|
|
|
113
145
|
info(chalk.dim("Streaming project logs... (Ctrl+C to stop)\n"));
|
|
114
146
|
await streamSSE(
|
|
115
147
|
withScope(`/api/projects/${projectId}/logs/stream`, scope),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
error(data);
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
printLogLine(data);
|
|
122
|
-
return true;
|
|
123
|
-
},
|
|
148
|
+
streamHandler,
|
|
149
|
+
{ reconnect: true, onReconnect },
|
|
124
150
|
);
|
|
125
151
|
});
|
|
126
152
|
}
|
|
@@ -174,6 +200,19 @@ type FlatEvent = DeployEvent["events"][number] & {
|
|
|
174
200
|
|
|
175
201
|
async function fetchFlatRestarts(appId: string): Promise<FlatEvent[]> {
|
|
176
202
|
const builds = await api.get<DeployEvent[]>(`/api/apps/${appId}/deploy-events`);
|
|
203
|
+
// The plain listing is DB-only; crash events still held by vm-agent are
|
|
204
|
+
// merged in by the server only when a buildId is given. Refetch the newest
|
|
205
|
+
// build so `--restart latest` sees crashes that haven't been persisted yet.
|
|
206
|
+
if (builds.length > 0) {
|
|
207
|
+
try {
|
|
208
|
+
const [withLive] = await api.get<DeployEvent[]>(
|
|
209
|
+
withQuery(`/api/apps/${appId}/deploy-events`, { buildId: builds[0].buildId }),
|
|
210
|
+
);
|
|
211
|
+
if (withLive && withLive.buildId === builds[0].buildId) builds[0] = withLive;
|
|
212
|
+
} catch {
|
|
213
|
+
// node unreachable — the DB-only list is still useful
|
|
214
|
+
}
|
|
215
|
+
}
|
|
177
216
|
const flat: FlatEvent[] = [];
|
|
178
217
|
for (const b of builds) {
|
|
179
218
|
for (const e of b.events) {
|
|
@@ -228,12 +267,27 @@ function printLogLine(data: string) {
|
|
|
228
267
|
}
|
|
229
268
|
}
|
|
230
269
|
|
|
270
|
+
/** Resolve the target for app-only subcommands; addons have no builds or
|
|
271
|
+
* restart events, so fail with a clear message instead of a server 404. */
|
|
272
|
+
async function getActiveApp(
|
|
273
|
+
serviceRef: string | undefined,
|
|
274
|
+
projectId: string,
|
|
275
|
+
what: string,
|
|
276
|
+
): Promise<{ id: string; name: string }> {
|
|
277
|
+
const svc = await getActiveServiceWithKind(serviceRef, projectId);
|
|
278
|
+
if (svc.kind === "addon") {
|
|
279
|
+
error(`${what} are only available for apps — "${svc.name}" is an addon`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
return { id: svc.id, name: svc.name };
|
|
283
|
+
}
|
|
284
|
+
|
|
231
285
|
async function showRestartList(
|
|
232
286
|
serviceRef: string | undefined,
|
|
233
287
|
projectId: string,
|
|
234
288
|
n: number,
|
|
235
289
|
) {
|
|
236
|
-
const svc = await
|
|
290
|
+
const svc = await getActiveApp(serviceRef, projectId, "Restart events");
|
|
237
291
|
const events = await fetchFlatRestarts(svc.id);
|
|
238
292
|
const slice = events.slice(0, n);
|
|
239
293
|
|
|
@@ -272,7 +326,7 @@ async function showRestartLogTail(
|
|
|
272
326
|
projectId: string,
|
|
273
327
|
ref: string,
|
|
274
328
|
) {
|
|
275
|
-
const svc = await
|
|
329
|
+
const svc = await getActiveApp(serviceRef, projectId, "Restart events");
|
|
276
330
|
const events = await fetchFlatRestarts(svc.id);
|
|
277
331
|
|
|
278
332
|
let evt: FlatEvent | undefined;
|
|
@@ -319,13 +373,11 @@ async function showBuildLogs(
|
|
|
319
373
|
tailN?: number,
|
|
320
374
|
) {
|
|
321
375
|
let appId: string | undefined;
|
|
322
|
-
if (serviceRef) {
|
|
323
|
-
|
|
376
|
+
if (serviceRef || getProjectLink()?.serviceId) {
|
|
377
|
+
// Explicit -s flag, or the service linked to this cwd.
|
|
378
|
+
const svc = await getActiveApp(serviceRef, projectId, "Build logs");
|
|
324
379
|
appId = svc.id;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (!appId) {
|
|
328
|
-
// Get first app in project
|
|
380
|
+
} else {
|
|
329
381
|
const data = await api.get<{ apps: Array<{ id: string; name: string }> }>(
|
|
330
382
|
withScope(`/api/projects/${projectId}/services`, scope),
|
|
331
383
|
);
|
|
@@ -335,6 +387,9 @@ async function showBuildLogs(
|
|
|
335
387
|
);
|
|
336
388
|
}
|
|
337
389
|
appId = data.apps[0].id;
|
|
390
|
+
if (data.apps.length > 1) {
|
|
391
|
+
info(chalk.dim(`Multiple apps — showing ${data.apps[0].name}. Use -s to pick another.`));
|
|
392
|
+
}
|
|
338
393
|
}
|
|
339
394
|
|
|
340
395
|
// Get latest build
|
|
@@ -354,24 +409,45 @@ async function showBuildLogs(
|
|
|
354
409
|
// Snapshot semantics: the server replays history immediately; if the
|
|
355
410
|
// build is still running the stream would otherwise follow it forever.
|
|
356
411
|
// Stop after 3s without new events and print what we have.
|
|
357
|
-
|
|
412
|
+
//
|
|
413
|
+
// Each SSE event carries a multi-line *chunk* (a logSnippet delta), not a
|
|
414
|
+
// single line — for a finished build the whole log arrives as one event.
|
|
415
|
+
// Reassemble the text first, then tail by line.
|
|
416
|
+
const chunks: string[] = [];
|
|
358
417
|
await streamSSE(
|
|
359
418
|
`/api/builds/${buildId}/logs`,
|
|
360
419
|
(event, data) => {
|
|
361
420
|
if (event === "done" || event === "error") return false;
|
|
362
|
-
|
|
421
|
+
try {
|
|
422
|
+
const parsed = JSON.parse(data);
|
|
423
|
+
chunks.push(typeof parsed === "string" ? parsed : data);
|
|
424
|
+
} catch {
|
|
425
|
+
chunks.push(data);
|
|
426
|
+
}
|
|
363
427
|
return true;
|
|
364
428
|
},
|
|
365
429
|
{ idleTimeoutMs: 3000 },
|
|
366
430
|
);
|
|
367
|
-
|
|
431
|
+
const lines = chunks.join("").split("\n");
|
|
432
|
+
while (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
433
|
+
for (const line of lines.slice(-tailN)) {
|
|
434
|
+
if (isJSONMode()) {
|
|
435
|
+
process.stdout.write(JSON.stringify({ message: line }) + "\n");
|
|
436
|
+
} else {
|
|
437
|
+
process.stdout.write(line + "\n");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
368
440
|
return;
|
|
369
441
|
}
|
|
370
442
|
|
|
371
443
|
await streamSSE(`/api/builds/${buildId}/logs`, (event, data) => {
|
|
372
|
-
if (event === "
|
|
444
|
+
if (event === "error") {
|
|
445
|
+
// The human-readable "--- Build failed ---" line already arrived as a
|
|
446
|
+
// data event; just make the failure visible to scripts.
|
|
447
|
+
process.exitCode = 1;
|
|
373
448
|
return false;
|
|
374
449
|
}
|
|
450
|
+
if (event === "done") return false;
|
|
375
451
|
printLogLine(data);
|
|
376
452
|
return true;
|
|
377
453
|
});
|
package/src/lib/api.ts
CHANGED
|
@@ -129,90 +129,178 @@ export const api = {
|
|
|
129
129
|
delete: <T = any>(path: string) => request<T>("DELETE", path),
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
/**
|
|
132
|
+
/** Compare two Redis-stream-style event ids (`<ms>-<seq>`). Returns true when
|
|
133
|
+
* `id` is at or before `last` — i.e. a replayed event we've already shown.
|
|
134
|
+
* Ids in any other format never count as replays. */
|
|
135
|
+
function isReplayedId(id: string, last: string): boolean {
|
|
136
|
+
const a = id.split("-").map(Number);
|
|
137
|
+
const b = last.split("-").map(Number);
|
|
138
|
+
if (a.length !== 2 || b.length !== 2 || a.some(Number.isNaN) || b.some(Number.isNaN)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return a[0] < b[0] || (a[0] === b[0] && a[1] <= b[1]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
145
|
+
|
|
146
|
+
/** Stream SSE and call handler for each event. Return false to stop.
|
|
133
147
|
*
|
|
134
148
|
* `opts.idleTimeoutMs` — stop (resolve) when no *event* arrives for that
|
|
135
149
|
* long. Heartbeat comments don't reset the timer. Used by `--tail`-style
|
|
136
|
-
* snapshot reads that must not follow a live stream forever.
|
|
150
|
+
* snapshot reads that must not follow a live stream forever.
|
|
151
|
+
*
|
|
152
|
+
* `opts.reconnect` — re-establish the connection when the server drops it
|
|
153
|
+
* (API deploys, proxy idle timeouts). Resumes via `Last-Event-ID` and
|
|
154
|
+
* suppresses events the server replays from before the drop. Rejects after
|
|
155
|
+
* MAX_RECONNECT_ATTEMPTS consecutive failures so callers exit non-zero
|
|
156
|
+
* instead of pretending the stream ended cleanly. `opts.onReconnect` fires
|
|
157
|
+
* before each attempt. */
|
|
137
158
|
export function streamSSE(
|
|
138
159
|
path: string,
|
|
139
160
|
handler: (event: string, data: string) => boolean | void,
|
|
140
|
-
opts: {
|
|
161
|
+
opts: {
|
|
162
|
+
idleTimeoutMs?: number;
|
|
163
|
+
reconnect?: boolean;
|
|
164
|
+
onReconnect?: (attempt: number) => void;
|
|
165
|
+
/** Base backoff between reconnect attempts; scaled by attempt number. */
|
|
166
|
+
reconnectBaseDelayMs?: number;
|
|
167
|
+
} = {},
|
|
141
168
|
): Promise<void> {
|
|
142
169
|
return new Promise((resolve, reject) => {
|
|
143
170
|
const url = new URL(baseURL + path);
|
|
144
171
|
const token = _accessToken || getToken();
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
172
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
173
|
+
|
|
174
|
+
let finished = false;
|
|
175
|
+
let attempts = 0;
|
|
176
|
+
let lastEventId: string | undefined;
|
|
177
|
+
// Id of the last event handed to the handler — replay marker across reconnects.
|
|
178
|
+
let lastDispatchedId: string | undefined;
|
|
179
|
+
|
|
180
|
+
const settle = (err?: unknown) => {
|
|
181
|
+
if (finished) return;
|
|
182
|
+
finished = true;
|
|
183
|
+
if (err) reject(err);
|
|
184
|
+
else resolve();
|
|
148
185
|
};
|
|
149
|
-
if (token) reqHeaders["Authorization"] = `Bearer ${token}`;
|
|
150
186
|
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
// Connection dropped without the handler asking to stop.
|
|
188
|
+
const dropped = (err?: unknown) => {
|
|
189
|
+
if (finished) return;
|
|
190
|
+
if (!opts.reconnect) {
|
|
191
|
+
settle(err);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
attempts++;
|
|
195
|
+
if (attempts > MAX_RECONNECT_ATTEMPTS) {
|
|
196
|
+
settle(
|
|
197
|
+
err instanceof Error
|
|
198
|
+
? err
|
|
199
|
+
: new Error("SSE stream disconnected and reconnect attempts failed"),
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
opts.onReconnect?.(attempts);
|
|
204
|
+
const base = opts.reconnectBaseDelayMs ?? 1000;
|
|
205
|
+
setTimeout(connect, Math.min(base * attempts, base * 5));
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const connect = () => {
|
|
209
|
+
if (finished) return;
|
|
210
|
+
const reqHeaders: Record<string, string> = {
|
|
211
|
+
"User-Agent": USER_AGENT,
|
|
212
|
+
Accept: "text/event-stream",
|
|
213
|
+
};
|
|
214
|
+
if (token) reqHeaders["Authorization"] = `Bearer ${token}`;
|
|
215
|
+
if (lastEventId) reqHeaders["Last-Event-ID"] = lastEventId;
|
|
216
|
+
|
|
217
|
+
const req = transport.request(
|
|
218
|
+
{ hostname: url.hostname, port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
219
|
+
path: url.pathname + url.search, method: "GET", headers: reqHeaders },
|
|
220
|
+
(res) => {
|
|
221
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
222
|
+
let body = "";
|
|
223
|
+
res.on("data", (c: Buffer) => body += c.toString());
|
|
224
|
+
res.on("end", () => settle(new APIError(res.statusCode!, `SSE failed: ${body}`)));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
229
|
+
const finish = () => {
|
|
230
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
231
|
+
finished = true;
|
|
232
|
+
req.destroy();
|
|
233
|
+
resolve();
|
|
234
|
+
};
|
|
235
|
+
const armIdleTimer = () => {
|
|
236
|
+
if (!opts.idleTimeoutMs) return;
|
|
237
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
238
|
+
idleTimer = setTimeout(finish, opts.idleTimeoutMs);
|
|
239
|
+
};
|
|
240
|
+
armIdleTimer();
|
|
241
|
+
|
|
242
|
+
let buffer = "";
|
|
243
|
+
let currentEvent = "";
|
|
244
|
+
let currentId: string | undefined;
|
|
245
|
+
let dataLines: string[] = [];
|
|
246
|
+
|
|
247
|
+
res.setEncoding("utf8");
|
|
248
|
+
res.on("data", (chunk: string) => {
|
|
249
|
+
buffer += chunk;
|
|
250
|
+
const lines = buffer.split("\n");
|
|
251
|
+
buffer = lines.pop() ?? "";
|
|
252
|
+
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const trimmed = line.replace(/\r$/, "");
|
|
255
|
+
if (trimmed === "") {
|
|
256
|
+
const data = dataLines.join("\n");
|
|
257
|
+
if (data) {
|
|
258
|
+
attempts = 0; // stream is healthy — reset the reconnect budget
|
|
259
|
+
armIdleTimer();
|
|
260
|
+
// After a reconnect the app-log endpoint replays recent
|
|
261
|
+
// history; skip entries we already printed.
|
|
262
|
+
const replay =
|
|
263
|
+
opts.reconnect && currentId && lastDispatchedId
|
|
264
|
+
? isReplayedId(currentId, lastDispatchedId)
|
|
265
|
+
: false;
|
|
266
|
+
if (!replay) {
|
|
267
|
+
if (currentId) lastDispatchedId = currentId;
|
|
268
|
+
const cont = handler(currentEvent, data);
|
|
269
|
+
if (cont === false) {
|
|
270
|
+
finish();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
195
274
|
}
|
|
275
|
+
currentEvent = "";
|
|
276
|
+
currentId = undefined;
|
|
277
|
+
dataLines = [];
|
|
278
|
+
} else if (trimmed.startsWith("event:")) {
|
|
279
|
+
currentEvent = trimmed.slice(6).trim();
|
|
280
|
+
} else if (trimmed.startsWith("data:")) {
|
|
281
|
+
dataLines.push(trimmed.slice(5).trimStart());
|
|
282
|
+
} else if (trimmed.startsWith("id:")) {
|
|
283
|
+
currentId = trimmed.slice(3).trim();
|
|
284
|
+
lastEventId = currentId;
|
|
196
285
|
}
|
|
197
|
-
currentEvent = "";
|
|
198
|
-
currentData = "";
|
|
199
|
-
} else if (trimmed.startsWith("event:")) {
|
|
200
|
-
currentEvent = trimmed.slice(6).trim();
|
|
201
|
-
} else if (trimmed.startsWith("data:")) {
|
|
202
|
-
currentData = trimmed.slice(5).trimStart();
|
|
203
286
|
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
res.on("end", () => {
|
|
290
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
291
|
+
dropped();
|
|
292
|
+
});
|
|
293
|
+
res.on("error", (err) => {
|
|
294
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
295
|
+
dropped(err);
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
req.on("error", dropped);
|
|
301
|
+
req.end();
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
connect();
|
|
217
305
|
});
|
|
218
306
|
}
|
package/src/lib/updater.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
export const CURRENT_VERSION = "0.3.
|
|
8
|
+
export const CURRENT_VERSION = "0.3.43";
|
|
9
9
|
const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
|
|
10
10
|
const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
|
|
11
11
|
|