patchrelay 0.7.0 → 0.7.1
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/build-info.json +3 -3
- package/dist/cli/commands/project.js +4 -4
- package/dist/cli/data.js +3 -133
- package/dist/cli/index.js +134 -13
- package/dist/cli/operator-client.js +140 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -91,7 +91,7 @@ export async function handleProjectCommand(params) {
|
|
|
91
91
|
if (!serviceState.ok) {
|
|
92
92
|
throw new Error(`Project was saved, but PatchRelay could not be reloaded: ${serviceState.error}`);
|
|
93
93
|
}
|
|
94
|
-
const cliData = params.options?.data ?? (await
|
|
94
|
+
const cliData = params.options?.data ?? (await createCliOperatorDataAccess(fullConfig));
|
|
95
95
|
try {
|
|
96
96
|
if (params.json) {
|
|
97
97
|
const connectResult = noConnect ? undefined : await cliData.connect(projectId);
|
|
@@ -140,7 +140,7 @@ export async function handleProjectCommand(params) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
async function
|
|
144
|
-
const {
|
|
145
|
-
return new
|
|
143
|
+
async function createCliOperatorDataAccess(config) {
|
|
144
|
+
const { CliOperatorApiClient } = await import("../operator-client.js");
|
|
145
|
+
return new CliOperatorApiClient(config);
|
|
146
146
|
}
|
package/dist/cli/data.js
CHANGED
|
@@ -3,6 +3,7 @@ import pino from "pino";
|
|
|
3
3
|
import { CodexAppServerClient } from "../codex-app-server.js";
|
|
4
4
|
import { PatchRelayDatabase } from "../db.js";
|
|
5
5
|
import { WorktreeManager } from "../worktree-manager.js";
|
|
6
|
+
import { CliOperatorApiClient } from "./operator-client.js";
|
|
6
7
|
import { resolveWorkflowStage } from "../workflow-policy.js";
|
|
7
8
|
function safeJsonParse(value) {
|
|
8
9
|
if (!value) {
|
|
@@ -40,12 +41,13 @@ function resolveStageFromState(config, projectId, stateName) {
|
|
|
40
41
|
}
|
|
41
42
|
return resolveWorkflowStage(project, stateName);
|
|
42
43
|
}
|
|
43
|
-
export class CliDataAccess {
|
|
44
|
+
export class CliDataAccess extends CliOperatorApiClient {
|
|
44
45
|
config;
|
|
45
46
|
db;
|
|
46
47
|
codex;
|
|
47
48
|
codexStarted = false;
|
|
48
49
|
constructor(config, options) {
|
|
50
|
+
super(config);
|
|
49
51
|
this.config = config;
|
|
50
52
|
this.db = options?.db ?? new PatchRelayDatabase(config.database.path, config.database.wal);
|
|
51
53
|
this.codex = options?.codex;
|
|
@@ -448,138 +450,6 @@ export class CliDataAccess {
|
|
|
448
450
|
const worktreeManager = new WorktreeManager(this.config);
|
|
449
451
|
await worktreeManager.ensureIssueWorktree(project.repoPath, project.worktreeRoot, worktree.workspace.worktreePath, worktree.workspace.branchName);
|
|
450
452
|
}
|
|
451
|
-
async connect(projectId) {
|
|
452
|
-
return await this.requestJson("/api/oauth/linear/start", {
|
|
453
|
-
...(projectId ? { projectId } : {}),
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
async connectStatus(state) {
|
|
457
|
-
if (!state) {
|
|
458
|
-
throw new Error("OAuth state is required.");
|
|
459
|
-
}
|
|
460
|
-
return await this.requestJson(`/api/oauth/linear/state/${encodeURIComponent(state)}`);
|
|
461
|
-
}
|
|
462
|
-
async listInstallations() {
|
|
463
|
-
return await this.requestJson("/api/installations");
|
|
464
|
-
}
|
|
465
|
-
async listOperatorFeed(options) {
|
|
466
|
-
return await this.requestJson("/api/feed", {
|
|
467
|
-
...(options?.limit && options.limit > 0 ? { limit: String(options.limit) } : {}),
|
|
468
|
-
...(options?.issueKey ? { issue: options.issueKey } : {}),
|
|
469
|
-
...(options?.projectId ? { project: options.projectId } : {}),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
async followOperatorFeed(onEvent, options) {
|
|
473
|
-
const url = new URL("/api/feed", this.getOperatorBaseUrl());
|
|
474
|
-
url.searchParams.set("follow", "1");
|
|
475
|
-
if (options?.limit && options.limit > 0) {
|
|
476
|
-
url.searchParams.set("limit", String(options.limit));
|
|
477
|
-
}
|
|
478
|
-
if (options?.issueKey) {
|
|
479
|
-
url.searchParams.set("issue", options.issueKey);
|
|
480
|
-
}
|
|
481
|
-
if (options?.projectId) {
|
|
482
|
-
url.searchParams.set("project", options.projectId);
|
|
483
|
-
}
|
|
484
|
-
const response = await fetch(url, {
|
|
485
|
-
method: "GET",
|
|
486
|
-
headers: {
|
|
487
|
-
accept: "text/event-stream",
|
|
488
|
-
...(this.config.operatorApi.bearerToken ? { authorization: `Bearer ${this.config.operatorApi.bearerToken}` } : {}),
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
if (!response.ok || !response.body) {
|
|
492
|
-
const body = await response.text().catch(() => "");
|
|
493
|
-
const message = this.readErrorMessage(body);
|
|
494
|
-
throw new Error(message ?? `Request failed: ${response.status}`);
|
|
495
|
-
}
|
|
496
|
-
const reader = response.body.getReader();
|
|
497
|
-
const decoder = new TextDecoder();
|
|
498
|
-
let buffer = "";
|
|
499
|
-
let dataLines = [];
|
|
500
|
-
while (true) {
|
|
501
|
-
const { done, value } = await reader.read();
|
|
502
|
-
if (done) {
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
buffer += decoder.decode(value, { stream: true });
|
|
506
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
507
|
-
while (newlineIndex !== -1) {
|
|
508
|
-
const rawLine = buffer.slice(0, newlineIndex);
|
|
509
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
510
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
511
|
-
if (!line) {
|
|
512
|
-
if (dataLines.length > 0) {
|
|
513
|
-
const parsed = JSON.parse(dataLines.join("\n"));
|
|
514
|
-
onEvent(parsed);
|
|
515
|
-
dataLines = [];
|
|
516
|
-
}
|
|
517
|
-
newlineIndex = buffer.indexOf("\n");
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (line.startsWith(":")) {
|
|
521
|
-
newlineIndex = buffer.indexOf("\n");
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
if (line.startsWith("data:")) {
|
|
525
|
-
dataLines.push(line.slice(5).trimStart());
|
|
526
|
-
}
|
|
527
|
-
newlineIndex = buffer.indexOf("\n");
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
getOperatorBaseUrl() {
|
|
532
|
-
const host = this.normalizeLocalHost(this.config.server.bind);
|
|
533
|
-
return `http://${host}:${this.config.server.port}/`;
|
|
534
|
-
}
|
|
535
|
-
normalizeLocalHost(bind) {
|
|
536
|
-
if (bind === "0.0.0.0") {
|
|
537
|
-
return "127.0.0.1";
|
|
538
|
-
}
|
|
539
|
-
if (bind === "::") {
|
|
540
|
-
return "[::1]";
|
|
541
|
-
}
|
|
542
|
-
if (bind.includes(":") && !bind.startsWith("[")) {
|
|
543
|
-
return `[${bind}]`;
|
|
544
|
-
}
|
|
545
|
-
return bind;
|
|
546
|
-
}
|
|
547
|
-
async requestJson(pathname, query, init) {
|
|
548
|
-
const url = new URL(pathname, this.getOperatorBaseUrl());
|
|
549
|
-
for (const [key, value] of Object.entries(query ?? {})) {
|
|
550
|
-
if (value) {
|
|
551
|
-
url.searchParams.set(key, value);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
const response = await fetch(url, {
|
|
555
|
-
method: init?.method ?? "GET",
|
|
556
|
-
headers: {
|
|
557
|
-
accept: "application/json",
|
|
558
|
-
...(init?.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
559
|
-
...(this.config.operatorApi.bearerToken ? { authorization: `Bearer ${this.config.operatorApi.bearerToken}` } : {}),
|
|
560
|
-
},
|
|
561
|
-
...(init?.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
562
|
-
});
|
|
563
|
-
const body = await response.text();
|
|
564
|
-
if (!response.ok) {
|
|
565
|
-
const message = this.readErrorMessage(body);
|
|
566
|
-
throw new Error(message ?? `Request failed: ${response.status}`);
|
|
567
|
-
}
|
|
568
|
-
const parsed = JSON.parse(body);
|
|
569
|
-
if (parsed.ok === false) {
|
|
570
|
-
throw new Error(this.readErrorMessage(body) ?? "Request failed.");
|
|
571
|
-
}
|
|
572
|
-
return parsed;
|
|
573
|
-
}
|
|
574
|
-
readErrorMessage(body) {
|
|
575
|
-
try {
|
|
576
|
-
const parsed = JSON.parse(body);
|
|
577
|
-
return parsed.message ?? parsed.reason;
|
|
578
|
-
}
|
|
579
|
-
catch {
|
|
580
|
-
return undefined;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
453
|
async readLiveSummary(threadId, latestTimestampSeen) {
|
|
584
454
|
const codex = await this.getCodex();
|
|
585
455
|
const thread = await codex.readThread(threadId, true);
|
package/dist/cli/index.js
CHANGED
|
@@ -202,6 +202,7 @@ export async function runCli(argv, options) {
|
|
|
202
202
|
profile: getCommandConfigProfile(command),
|
|
203
203
|
});
|
|
204
204
|
let data = options?.data;
|
|
205
|
+
let ownsData = false;
|
|
205
206
|
try {
|
|
206
207
|
if (command === "doctor") {
|
|
207
208
|
const { runPreflight } = await import("../preflight.js");
|
|
@@ -209,55 +210,111 @@ export async function runCli(argv, options) {
|
|
|
209
210
|
writeOutput(stdout, json ? formatJson(report) : formatDoctor(report));
|
|
210
211
|
return report.ok ? 0 : 1;
|
|
211
212
|
}
|
|
212
|
-
data ??= await createCliDataAccess(config);
|
|
213
213
|
if (command === "inspect") {
|
|
214
|
-
|
|
214
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
215
|
+
if (!data) {
|
|
216
|
+
data = issueData;
|
|
217
|
+
ownsData = true;
|
|
218
|
+
}
|
|
219
|
+
return await handleInspectCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
215
220
|
}
|
|
216
221
|
if (command === "live") {
|
|
217
|
-
|
|
222
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
223
|
+
if (!data) {
|
|
224
|
+
data = issueData;
|
|
225
|
+
ownsData = true;
|
|
226
|
+
}
|
|
227
|
+
return await handleLiveCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
218
228
|
}
|
|
219
229
|
if (command === "report") {
|
|
220
|
-
|
|
230
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
231
|
+
if (!data) {
|
|
232
|
+
data = issueData;
|
|
233
|
+
ownsData = true;
|
|
234
|
+
}
|
|
235
|
+
return await handleReportCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
221
236
|
}
|
|
222
237
|
if (command === "events") {
|
|
223
|
-
|
|
238
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
239
|
+
if (!data) {
|
|
240
|
+
data = issueData;
|
|
241
|
+
ownsData = true;
|
|
242
|
+
}
|
|
243
|
+
return await handleEventsCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
224
244
|
}
|
|
225
245
|
if (command === "worktree") {
|
|
226
|
-
|
|
246
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
247
|
+
if (!data) {
|
|
248
|
+
data = issueData;
|
|
249
|
+
ownsData = true;
|
|
250
|
+
}
|
|
251
|
+
return await handleWorktreeCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
227
252
|
}
|
|
228
253
|
if (command === "open") {
|
|
229
|
-
|
|
254
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
255
|
+
if (!data) {
|
|
256
|
+
data = issueData;
|
|
257
|
+
ownsData = true;
|
|
258
|
+
}
|
|
259
|
+
return await handleOpenCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
230
260
|
}
|
|
231
261
|
if (command === "connect") {
|
|
262
|
+
const operatorData = await ensureConnectDataAccess(data, config);
|
|
263
|
+
if (!data) {
|
|
264
|
+
data = operatorData;
|
|
265
|
+
ownsData = true;
|
|
266
|
+
}
|
|
232
267
|
return await handleConnectCommand({
|
|
233
268
|
parsed,
|
|
234
269
|
json,
|
|
235
270
|
stdout,
|
|
236
271
|
config,
|
|
237
|
-
data,
|
|
272
|
+
data: operatorData,
|
|
238
273
|
...(options ? { options } : {}),
|
|
239
274
|
});
|
|
240
275
|
}
|
|
241
276
|
if (command === "installations") {
|
|
277
|
+
const operatorData = await ensureInstallationsDataAccess(data, config);
|
|
278
|
+
if (!data) {
|
|
279
|
+
data = operatorData;
|
|
280
|
+
ownsData = true;
|
|
281
|
+
}
|
|
242
282
|
return await handleInstallationsCommand({
|
|
243
283
|
json,
|
|
244
284
|
stdout,
|
|
245
|
-
data,
|
|
285
|
+
data: operatorData,
|
|
246
286
|
});
|
|
247
287
|
}
|
|
248
288
|
if (command === "feed") {
|
|
289
|
+
const operatorData = parsed.flags.get("follow") === true
|
|
290
|
+
? await ensureFeedFollowDataAccess(data, config)
|
|
291
|
+
: await ensureFeedListDataAccess(data, config);
|
|
292
|
+
if (!data) {
|
|
293
|
+
data = operatorData;
|
|
294
|
+
ownsData = true;
|
|
295
|
+
}
|
|
249
296
|
return await handleFeedCommand({
|
|
250
297
|
parsed,
|
|
251
298
|
json,
|
|
252
299
|
stdout,
|
|
253
|
-
data,
|
|
300
|
+
data: operatorData,
|
|
254
301
|
});
|
|
255
302
|
}
|
|
256
303
|
if (command === "retry") {
|
|
257
|
-
|
|
304
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
305
|
+
if (!data) {
|
|
306
|
+
data = issueData;
|
|
307
|
+
ownsData = true;
|
|
308
|
+
}
|
|
309
|
+
return await handleRetryCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
258
310
|
}
|
|
259
311
|
if (command === "list") {
|
|
260
|
-
|
|
312
|
+
const issueData = await ensureIssueDataAccess(data, config);
|
|
313
|
+
if (!data) {
|
|
314
|
+
data = issueData;
|
|
315
|
+
ownsData = true;
|
|
316
|
+
}
|
|
317
|
+
return await handleListCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
|
|
261
318
|
}
|
|
262
319
|
throw new Error(`Unknown command: ${command}`);
|
|
263
320
|
}
|
|
@@ -270,7 +327,7 @@ export async function runCli(argv, options) {
|
|
|
270
327
|
return 1;
|
|
271
328
|
}
|
|
272
329
|
finally {
|
|
273
|
-
if (
|
|
330
|
+
if (ownsData && data) {
|
|
274
331
|
data.close();
|
|
275
332
|
}
|
|
276
333
|
}
|
|
@@ -279,3 +336,67 @@ async function createCliDataAccess(config) {
|
|
|
279
336
|
const { CliDataAccess } = await import("./data.js");
|
|
280
337
|
return new CliDataAccess(config);
|
|
281
338
|
}
|
|
339
|
+
async function createCliOperatorDataAccess(config) {
|
|
340
|
+
const { CliOperatorApiClient } = await import("./operator-client.js");
|
|
341
|
+
return new CliOperatorApiClient(config);
|
|
342
|
+
}
|
|
343
|
+
async function ensureIssueDataAccess(data, config) {
|
|
344
|
+
if (data) {
|
|
345
|
+
if (isIssueDataAccess(data)) {
|
|
346
|
+
return data;
|
|
347
|
+
}
|
|
348
|
+
throw new Error("Issue inspection commands require local SQLite-backed CLI data access.");
|
|
349
|
+
}
|
|
350
|
+
return await createCliDataAccess(config);
|
|
351
|
+
}
|
|
352
|
+
async function ensureConnectDataAccess(data, config) {
|
|
353
|
+
if (data) {
|
|
354
|
+
if (hasConnectDataAccess(data)) {
|
|
355
|
+
return data;
|
|
356
|
+
}
|
|
357
|
+
throw new Error("The connect command requires HTTP-backed OAuth CLI data access.");
|
|
358
|
+
}
|
|
359
|
+
return await createCliOperatorDataAccess(config);
|
|
360
|
+
}
|
|
361
|
+
function isIssueDataAccess(data) {
|
|
362
|
+
return !!data && typeof data === "object" && "inspect" in data && typeof data.inspect === "function";
|
|
363
|
+
}
|
|
364
|
+
async function ensureInstallationsDataAccess(data, config) {
|
|
365
|
+
if (data) {
|
|
366
|
+
if (hasInstallationsDataAccess(data)) {
|
|
367
|
+
return data;
|
|
368
|
+
}
|
|
369
|
+
throw new Error("The installations command requires HTTP-backed installation data access.");
|
|
370
|
+
}
|
|
371
|
+
return await createCliOperatorDataAccess(config);
|
|
372
|
+
}
|
|
373
|
+
async function ensureFeedListDataAccess(data, config) {
|
|
374
|
+
if (data) {
|
|
375
|
+
if (hasFeedListDataAccess(data)) {
|
|
376
|
+
return data;
|
|
377
|
+
}
|
|
378
|
+
throw new Error("The feed command requires listOperatorFeed() data access.");
|
|
379
|
+
}
|
|
380
|
+
return await createCliOperatorDataAccess(config);
|
|
381
|
+
}
|
|
382
|
+
function hasConnectDataAccess(data) {
|
|
383
|
+
return !!data && typeof data === "object" && "connect" in data && typeof data.connect === "function";
|
|
384
|
+
}
|
|
385
|
+
function hasInstallationsDataAccess(data) {
|
|
386
|
+
return !!data && typeof data === "object" && "listInstallations" in data && typeof data.listInstallations === "function";
|
|
387
|
+
}
|
|
388
|
+
async function ensureFeedFollowDataAccess(data, config) {
|
|
389
|
+
if (data) {
|
|
390
|
+
if (hasFeedFollowDataAccess(data)) {
|
|
391
|
+
return data;
|
|
392
|
+
}
|
|
393
|
+
throw new Error("The feed --follow command requires followOperatorFeed() data access.");
|
|
394
|
+
}
|
|
395
|
+
return await createCliOperatorDataAccess(config);
|
|
396
|
+
}
|
|
397
|
+
function hasFeedListDataAccess(data) {
|
|
398
|
+
return !!data && typeof data === "object" && "listOperatorFeed" in data && typeof data.listOperatorFeed === "function";
|
|
399
|
+
}
|
|
400
|
+
function hasFeedFollowDataAccess(data) {
|
|
401
|
+
return !!data && typeof data === "object" && "followOperatorFeed" in data && typeof data.followOperatorFeed === "function";
|
|
402
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export class CliOperatorApiClient {
|
|
2
|
+
config;
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
close() { }
|
|
7
|
+
async connect(projectId) {
|
|
8
|
+
return await this.requestJson("/api/oauth/linear/start", {
|
|
9
|
+
...(projectId ? { projectId } : {}),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
async connectStatus(state) {
|
|
13
|
+
if (!state) {
|
|
14
|
+
throw new Error("OAuth state is required.");
|
|
15
|
+
}
|
|
16
|
+
return await this.requestJson(`/api/oauth/linear/state/${encodeURIComponent(state)}`);
|
|
17
|
+
}
|
|
18
|
+
async listInstallations() {
|
|
19
|
+
return await this.requestJson("/api/installations");
|
|
20
|
+
}
|
|
21
|
+
async listOperatorFeed(options) {
|
|
22
|
+
return await this.requestJson("/api/feed", {
|
|
23
|
+
...(options?.limit && options.limit > 0 ? { limit: String(options.limit) } : {}),
|
|
24
|
+
...(options?.issueKey ? { issue: options.issueKey } : {}),
|
|
25
|
+
...(options?.projectId ? { project: options.projectId } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async followOperatorFeed(onEvent, options) {
|
|
29
|
+
const url = new URL("/api/feed", this.getOperatorBaseUrl());
|
|
30
|
+
url.searchParams.set("follow", "1");
|
|
31
|
+
if (options?.limit && options.limit > 0) {
|
|
32
|
+
url.searchParams.set("limit", String(options.limit));
|
|
33
|
+
}
|
|
34
|
+
if (options?.issueKey) {
|
|
35
|
+
url.searchParams.set("issue", options.issueKey);
|
|
36
|
+
}
|
|
37
|
+
if (options?.projectId) {
|
|
38
|
+
url.searchParams.set("project", options.projectId);
|
|
39
|
+
}
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
method: "GET",
|
|
42
|
+
headers: {
|
|
43
|
+
accept: "text/event-stream",
|
|
44
|
+
...(this.config.operatorApi.bearerToken ? { authorization: `Bearer ${this.config.operatorApi.bearerToken}` } : {}),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok || !response.body) {
|
|
48
|
+
const body = await response.text().catch(() => "");
|
|
49
|
+
const message = this.readErrorMessage(body);
|
|
50
|
+
throw new Error(message ?? `Request failed: ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const decoder = new TextDecoder();
|
|
54
|
+
let buffer = "";
|
|
55
|
+
let dataLines = [];
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
buffer += decoder.decode(value, { stream: true });
|
|
62
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
63
|
+
while (newlineIndex !== -1) {
|
|
64
|
+
const rawLine = buffer.slice(0, newlineIndex);
|
|
65
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
66
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
67
|
+
if (!line) {
|
|
68
|
+
if (dataLines.length > 0) {
|
|
69
|
+
const parsed = JSON.parse(dataLines.join("\n"));
|
|
70
|
+
onEvent(parsed);
|
|
71
|
+
dataLines = [];
|
|
72
|
+
}
|
|
73
|
+
newlineIndex = buffer.indexOf("\n");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (line.startsWith(":")) {
|
|
77
|
+
newlineIndex = buffer.indexOf("\n");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (line.startsWith("data:")) {
|
|
81
|
+
dataLines.push(line.slice(5).trimStart());
|
|
82
|
+
}
|
|
83
|
+
newlineIndex = buffer.indexOf("\n");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
getOperatorBaseUrl() {
|
|
88
|
+
const host = this.normalizeLocalHost(this.config.server.bind);
|
|
89
|
+
return `http://${host}:${this.config.server.port}/`;
|
|
90
|
+
}
|
|
91
|
+
normalizeLocalHost(bind) {
|
|
92
|
+
if (bind === "0.0.0.0") {
|
|
93
|
+
return "127.0.0.1";
|
|
94
|
+
}
|
|
95
|
+
if (bind === "::") {
|
|
96
|
+
return "[::1]";
|
|
97
|
+
}
|
|
98
|
+
if (bind.includes(":") && !bind.startsWith("[")) {
|
|
99
|
+
return `[${bind}]`;
|
|
100
|
+
}
|
|
101
|
+
return bind;
|
|
102
|
+
}
|
|
103
|
+
async requestJson(pathname, query, init) {
|
|
104
|
+
const url = new URL(pathname, this.getOperatorBaseUrl());
|
|
105
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
106
|
+
if (value) {
|
|
107
|
+
url.searchParams.set(key, value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const response = await fetch(url, {
|
|
111
|
+
method: init?.method ?? "GET",
|
|
112
|
+
headers: {
|
|
113
|
+
accept: "application/json",
|
|
114
|
+
connection: "close",
|
|
115
|
+
...(init?.body !== undefined ? { "content-type": "application/json" } : {}),
|
|
116
|
+
...(this.config.operatorApi.bearerToken ? { authorization: `Bearer ${this.config.operatorApi.bearerToken}` } : {}),
|
|
117
|
+
},
|
|
118
|
+
...(init?.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
|
119
|
+
});
|
|
120
|
+
const body = await response.text();
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const message = this.readErrorMessage(body);
|
|
123
|
+
throw new Error(message ?? `Request failed: ${response.status}`);
|
|
124
|
+
}
|
|
125
|
+
const parsed = JSON.parse(body);
|
|
126
|
+
if (parsed.ok === false) {
|
|
127
|
+
throw new Error(this.readErrorMessage(body) ?? "Request failed.");
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
readErrorMessage(body) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(body);
|
|
134
|
+
return parsed.message ?? parsed.reason;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|