@jr2/cli 0.3.0 → 0.5.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/README.md +4 -3
- package/bin/jr2.js +3 -3
- package/manifests/operator.yaml +265 -23
- package/package.json +5 -5
- package/src/agents-md.ts +149 -0
- package/src/assembly.ts +432 -0
- package/src/build.ts +145 -290
- package/src/cli.ts +20 -41
- package/src/client.ts +144 -33
- package/src/command-table.ts +641 -0
- package/src/commands/check.ts +37 -0
- package/src/commands/down.ts +7 -16
- package/src/commands/gc.ts +2 -11
- package/src/commands/init.ts +82 -19
- package/src/commands/kit.ts +6 -11
- package/src/commands/logs.ts +13 -9
- package/src/commands/run.ts +70 -17
- package/src/commands/runs.ts +83 -6
- package/src/commands/send.ts +22 -23
- package/src/commands/status.ts +134 -22
- package/src/commands/up.ts +1704 -386
- package/src/commands/version.ts +26 -25
- package/src/deploy.ts +880 -101
- package/src/effector-image.ts +133 -0
- package/src/index.ts +26 -0
- package/src/instance-check.ts +309 -0
- package/src/instance.ts +3 -9
- package/src/kit-version.ts +27 -17
- package/src/kube.ts +439 -70
- package/src/nodes.ts +33 -2
- package/src/output.ts +1 -1
- package/src/repo-sweep.ts +1 -1
- package/src/sweep.ts +30 -15
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Top-level dispatch (ADR-0009). `argv[0]` is the verb; the rest is handed to that command, which does
|
|
2
|
-
// its own `parseArgs
|
|
3
|
-
//
|
|
2
|
+
// its own `parseArgs` against its entry in the command table (`command-table.ts`). Every help prints
|
|
3
|
+
// from that table too: `jr2 --help` the whole of it, `jr2 <verb> --help` (or `-h`) one entry — and
|
|
4
|
+
// then the verb does NOT run, so `jr2 up --help` never converges a cluster. `main` returns an exit
|
|
5
|
+
// code (the bin sets `process.exitCode`) and never throws: a command error becomes an `error: …` line
|
|
6
|
+
// on stderr + code 1. Codes: 0 ok, 1 runtime error, 2 usage.
|
|
4
7
|
|
|
5
8
|
import { activity, defaultIo, type Io } from "./output.ts";
|
|
6
9
|
import { JR2HttpError } from "./client.ts";
|
|
@@ -12,42 +15,12 @@ import { status } from "./commands/status.ts";
|
|
|
12
15
|
import { logs } from "./commands/logs.ts";
|
|
13
16
|
import { send } from "./commands/send.ts";
|
|
14
17
|
import { up } from "./commands/up.ts";
|
|
18
|
+
import { check } from "./commands/check.ts";
|
|
15
19
|
import { down } from "./commands/down.ts";
|
|
16
20
|
import { gc } from "./commands/gc.ts";
|
|
17
21
|
import { kit } from "./commands/kit.ts";
|
|
18
22
|
import { version } from "./commands/version.ts";
|
|
19
|
-
|
|
20
|
-
const USAGE = `jr2 — orchestrate agentic workflows (ADR-0009)
|
|
21
|
-
|
|
22
|
-
usage: jr2 <command> [args]
|
|
23
|
-
|
|
24
|
-
version [--local] [--json] what runs here (this jr2, the global that handed off, the
|
|
25
|
-
Instance's Kit) and what the cluster runs; --local skips the cluster
|
|
26
|
-
init [dir] [--name <n>] scaffold a new instance folder
|
|
27
|
-
up [--yes] [--force] converge the current kube context to this instance (ADR-0019)
|
|
28
|
-
down [--all] remove the instance from the cluster (--all: operator too)
|
|
29
|
-
gc [--dry-run] [--repo-ttl 7d] remove jr2's images that no live instance names (ADR-0039), and
|
|
30
|
-
Repo resources no Machine binds and no run attached lately (ADR-0051)
|
|
31
|
-
kit push <registry> mirror the published kit images into a registry (ADR-0044)
|
|
32
|
-
run <workflow> [--input <json>] start a run; stream activity, print terminal result
|
|
33
|
-
[--detach] ...or just print the runId and return
|
|
34
|
-
runs list live runs
|
|
35
|
-
status [runId] print a run's current status (read-through),
|
|
36
|
-
[--json] or the instance's Repos, per node, when given none (ADR-0048/0051)
|
|
37
|
-
logs <runId> [-f] replay a run's status; -f to follow until it settles
|
|
38
|
-
send <runId> --event CANCEL abandon a live run
|
|
39
|
-
send <runId> --gate <gate> --event <name> [--input <json>]
|
|
40
|
-
deliver a workflow event to an open gate
|
|
41
|
-
|
|
42
|
-
run ids: any <runId> above may be abbreviated to a unique prefix (4+ chars, git-style);
|
|
43
|
-
an ambiguous prefix lists the candidates and fails rather than guessing
|
|
44
|
-
|
|
45
|
-
global (run verbs): -n/--namespace <ns>, --context <ctx> address the deployment (ADR-0019);
|
|
46
|
-
--url <u> / JR2_URL attaches to a specific orchestrator (skips kube entirely)
|
|
47
|
-
|
|
48
|
-
output: result verbs (run, runs, status <runId>, send, logs) print one JSON result on stdout and
|
|
49
|
-
activity on stderr, so \`jr2 run … | jq\` yields the result; report verbs (version, bare
|
|
50
|
-
status) print a table on stdout, or the one object with --json`;
|
|
23
|
+
import { asksForHelp, commandsFor, GLOBAL, renderCommand, renderHelp } from "./command-table.ts";
|
|
51
24
|
|
|
52
25
|
/**
|
|
53
26
|
* The catch-all half of skew reporting: a route-shaped failure gets ONE extra line naming what the
|
|
@@ -74,6 +47,15 @@ export async function main(argv: string[], io: Io = defaultIo): Promise<number>
|
|
|
74
47
|
// Before any verb, so the instance's `.env` is in `process.env` by the time a command imports
|
|
75
48
|
// `jr2.config.ts` (whose deployment-varying values are read from there — ADR-0019).
|
|
76
49
|
loadDotenv(io);
|
|
50
|
+
const entries = cmd === undefined ? [] : commandsFor(cmd);
|
|
51
|
+
if (entries.length && asksForHelp(entries, rest)) {
|
|
52
|
+
activity(io, entries.map(renderCommand).join("\n\n"));
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (cmd !== undefined && (GLOBAL.help as readonly string[]).includes(cmd)) {
|
|
56
|
+
activity(io, renderHelp());
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
77
59
|
switch (cmd) {
|
|
78
60
|
case "init":
|
|
79
61
|
return await init(rest, io);
|
|
@@ -87,6 +69,8 @@ export async function main(argv: string[], io: Io = defaultIo): Promise<number>
|
|
|
87
69
|
return await logs(rest, io);
|
|
88
70
|
case "send":
|
|
89
71
|
return await send(rest, io);
|
|
72
|
+
case "check":
|
|
73
|
+
return await check(rest, io);
|
|
90
74
|
case "up":
|
|
91
75
|
return await up(rest, io);
|
|
92
76
|
case "down":
|
|
@@ -98,17 +82,12 @@ export async function main(argv: string[], io: Io = defaultIo): Promise<number>
|
|
|
98
82
|
case "version":
|
|
99
83
|
case "--version":
|
|
100
84
|
return await version(rest, io);
|
|
101
|
-
case "help":
|
|
102
|
-
case "--help":
|
|
103
|
-
case "-h":
|
|
104
|
-
activity(io, USAGE);
|
|
105
|
-
return 0;
|
|
106
85
|
case undefined:
|
|
107
|
-
activity(io,
|
|
86
|
+
activity(io, renderHelp());
|
|
108
87
|
return 2;
|
|
109
88
|
default:
|
|
110
89
|
activity(io, `unknown command: ${cmd}`);
|
|
111
|
-
activity(io,
|
|
90
|
+
activity(io, renderHelp());
|
|
112
91
|
return 2;
|
|
113
92
|
}
|
|
114
93
|
} catch (err) {
|
package/src/client.ts
CHANGED
|
@@ -6,7 +6,15 @@
|
|
|
6
6
|
// `fetchImpl` is injectable so tests drive it with a hono `app.request` (no socket) the same way the
|
|
7
7
|
// orchestrator's own http tests do; in production it defaults to the global `fetch`.
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ChildErrorMarker,
|
|
11
|
+
DispatchedBy,
|
|
12
|
+
MachineDoc,
|
|
13
|
+
PlacingMarker,
|
|
14
|
+
RepoStatus,
|
|
15
|
+
RunChild,
|
|
16
|
+
RunWaiting,
|
|
17
|
+
} from "@jr2/orchestrator/internal";
|
|
10
18
|
import { parseSSE } from "./sse.ts";
|
|
11
19
|
|
|
12
20
|
/** A run's current observable state — mirrors the orchestrator's `RunStatus` (run-host.ts). */
|
|
@@ -17,19 +25,66 @@ export type RunStatus = {
|
|
|
17
25
|
status: string;
|
|
18
26
|
value: unknown;
|
|
19
27
|
context: unknown;
|
|
20
|
-
/**
|
|
21
|
-
|
|
28
|
+
/** The live child machines beneath the root, context-free — where most of a run is. */
|
|
29
|
+
children?: RunChild[];
|
|
30
|
+
/** Every Workspace waiting for capacity, and why (ADR-0064). Optional: an orchestrator older
|
|
31
|
+
* than ADR-0064 does not send it. */
|
|
32
|
+
waiting?: RunWaiting[];
|
|
33
|
+
/** Why the host set this status, for statuses the Machine did not choose — `drifted` names the
|
|
34
|
+
* Assembly hash and why this Orchestrator cannot read it: not on the volume, published for
|
|
35
|
+
* another Kit version, or does not load (ADR-0030, ADR-0069). */
|
|
22
36
|
reason?: string;
|
|
37
|
+
/** The hash of the Assembly the run started under, and holds while it is live (ADR-0069).
|
|
38
|
+
* Absent for a run no Assembly started. */
|
|
39
|
+
assembly?: string;
|
|
40
|
+
/** The run binds at least one Chart (ADR-0070): its Machine is the Chart wrapper's with each bound
|
|
41
|
+
* Open body's Chart compiled in, which `GET /workflows/:name/runs/:runId/machine` serves.
|
|
42
|
+
* Absent when the run binds none. */
|
|
43
|
+
bound?: true;
|
|
44
|
+
/** The hashes of the Charts the run binds now, distinct and sorted (ADR-0070). */
|
|
45
|
+
chartHashes?: string[];
|
|
46
|
+
/** The run that dispatched this one through a `dispatch` Effect, the caller's slot, its ledger
|
|
47
|
+
* row id (the idempotency key) and this run's depth in the chain (ADR-0071); `instance` names a
|
|
48
|
+
* foreign caller, as opaque text. Absent for a run nobody dispatched. */
|
|
49
|
+
dispatchedBy?: DispatchedBy;
|
|
50
|
+
/** The Machine's output, for a run that is no longer live (ADR-0071): what a `dispatch` with
|
|
51
|
+
* `wait` settles with. Never for a live run. */
|
|
52
|
+
output?: unknown;
|
|
53
|
+
/** The runs this run's `dispatch` Effects started, oldest first: the slot, the run, and when its
|
|
54
|
+
* ledger row was written (ADR-0071). Absent when it started none. */
|
|
55
|
+
dispatched?: Array<{ slot: string; run: string; at: string }>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Where a Workflow came from (ADR-0070): a Machine in the Assembly, a Chart file in the Assembly, or
|
|
59
|
+
* a runtime Chart put by `PUT /workflows/:wrapper/charts/:name`. */
|
|
60
|
+
export type WorkflowOrigin = "machine" | "file" | "runtime";
|
|
61
|
+
|
|
62
|
+
/** One entry of `GET /workflows` (ADR-0070) — mirrors the orchestrator's `WorkflowListing`
|
|
63
|
+
* (run-host.ts). A Chart wrapper's entry carries its Open body slot, whether its door takes a Chart
|
|
64
|
+
* one-shot, and its named Charts, inside its entry and not beside it. `unrunnable` is why a
|
|
65
|
+
* Workflow cannot start now (a runtime Chart a publish no longer admits). */
|
|
66
|
+
export type WorkflowListing = {
|
|
67
|
+
name: string;
|
|
68
|
+
origin: WorkflowOrigin;
|
|
69
|
+
wrapper?: {
|
|
70
|
+
body: string;
|
|
71
|
+
oneShot: boolean;
|
|
72
|
+
charts: Array<{ name: string; origin: "file" | "runtime"; unrunnable?: string }>;
|
|
73
|
+
};
|
|
74
|
+
unrunnable?: string;
|
|
23
75
|
};
|
|
24
76
|
|
|
25
77
|
/** One item on a run's observation feed — mirrors the orchestrator's `RunFeedEvent`. */
|
|
26
78
|
export type RunFeedEvent =
|
|
27
79
|
| { kind: "status"; status: RunStatus }
|
|
28
80
|
| { kind: "emit"; event: { type: string } & Record<string, unknown> }
|
|
29
|
-
| { kind: "retry"; child: string; attempt: number; reason: string }
|
|
81
|
+
| { kind: "retry"; child: string; attempt: number; reason: string }
|
|
82
|
+
| PlacingMarker
|
|
83
|
+
| ChildErrorMarker;
|
|
30
84
|
|
|
31
|
-
/** A run-control event posted to a live run (ADR-0013): CANCEL is the vocabulary that is left.
|
|
32
|
-
|
|
85
|
+
/** A run-control event posted to a live run (ADR-0013): CANCEL is the vocabulary that is left.
|
|
86
|
+
* `keep` leaves a cancelled run's Workspaces instead of destroying them (ADR-0025). */
|
|
87
|
+
export type RunEvent = { type: string; keep?: boolean };
|
|
33
88
|
|
|
34
89
|
/** The subset of `fetch` the client uses. `globalThis.fetch` and hono's `app.request` both satisfy it. */
|
|
35
90
|
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
@@ -57,16 +112,27 @@ export class JR2HttpError extends Error {
|
|
|
57
112
|
readonly status: number;
|
|
58
113
|
readonly path: string;
|
|
59
114
|
readonly instance?: InstanceIdentity;
|
|
115
|
+
/** The door's report lines, when the answer carried them: a refused Chart is
|
|
116
|
+
* `422 { error: "refused", report }` (ADR-0070), one line per rule it fails. */
|
|
117
|
+
readonly report?: string[];
|
|
60
118
|
|
|
61
|
-
constructor(message: string, status: number, path: string, instance?: InstanceIdentity) {
|
|
119
|
+
constructor(message: string, status: number, path: string, instance?: InstanceIdentity, report?: string[]) {
|
|
62
120
|
super(message);
|
|
63
121
|
this.name = "JR2HttpError";
|
|
64
122
|
this.status = status;
|
|
65
123
|
this.path = path;
|
|
66
124
|
this.instance = instance;
|
|
125
|
+
if (report) this.report = report;
|
|
67
126
|
}
|
|
68
127
|
}
|
|
69
128
|
|
|
129
|
+
/** The path of a Workflow's routes. The name is ONE path segment, encoded: a named Chart's
|
|
130
|
+
* `<wrapper>/<name>` rides as `<wrapper>%2F<name>` (ADR-0070), which the Orchestrator reads as the
|
|
131
|
+
* same Workflow as `/workflows/<wrapper>/charts/<name>`. */
|
|
132
|
+
export function workflowPath(workflow: string): string {
|
|
133
|
+
return `/workflows/${encodeURIComponent(workflow)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
70
136
|
export class JR2Client {
|
|
71
137
|
readonly baseUrl: string;
|
|
72
138
|
private readonly fetchImpl: FetchLike;
|
|
@@ -85,20 +151,40 @@ export class JR2Client {
|
|
|
85
151
|
return this.token ? { ...extra, authorization: `Bearer ${this.token}` } : extra;
|
|
86
152
|
}
|
|
87
153
|
|
|
88
|
-
/** `GET /workflows` —
|
|
89
|
-
|
|
90
|
-
|
|
154
|
+
/** `GET /workflows` — every registered Workflow with its origin, each Chart wrapper's Charts inside
|
|
155
|
+
* its entry (ADR-0070). An orchestrator older than ADR-0070 answers bare names; each reads as a
|
|
156
|
+
* Machine, so the listing has one shape for every caller. */
|
|
157
|
+
async workflows(): Promise<WorkflowListing[]> {
|
|
158
|
+
const body = await this.json(await this.fetchImpl(`${this.baseUrl}/workflows`), "/workflows");
|
|
159
|
+
// An answer that is no listing is not "none registered": say what came back (`jr2 runs` prints
|
|
160
|
+
// `workflows: unknown — ...` for it, never "none registered").
|
|
161
|
+
if (!Array.isArray(body)) throw new Error("GET /workflows: the answer is not a listing");
|
|
162
|
+
return body.map((w: unknown) =>
|
|
163
|
+
typeof w === "string" ? { name: w, origin: "machine" as const } : (w as WorkflowListing),
|
|
164
|
+
);
|
|
91
165
|
}
|
|
92
166
|
|
|
93
167
|
/** `GET /workflows/:name/machine` — the workflow's Machine as the Console's DTO. */
|
|
94
168
|
async machine(workflow: string): Promise<MachineDoc> {
|
|
95
|
-
const res = await this.fetchImpl(`${this.baseUrl}
|
|
169
|
+
const res = await this.fetchImpl(`${this.baseUrl}${workflowPath(workflow)}/machine`);
|
|
96
170
|
return (await this.json(res, "/workflows/:name/machine")) as MachineDoc;
|
|
97
171
|
}
|
|
98
172
|
|
|
99
|
-
/** `
|
|
173
|
+
/** `GET /workflows/:name/runs/:runId/machine` — the run's bound Machine (ADR-0070): the Chart
|
|
174
|
+
* wrapper's Machine with each bound Open body's Chart compiled in. An unknown run → undefined. */
|
|
175
|
+
async runMachine(workflow: string, runId: string): Promise<MachineDoc | undefined> {
|
|
176
|
+
const res = await this.fetchImpl(
|
|
177
|
+
`${this.baseUrl}${workflowPath(workflow)}/runs/${encodeURIComponent(runId)}/machine`,
|
|
178
|
+
);
|
|
179
|
+
if (res.status === 404) return undefined;
|
|
180
|
+
return (await this.json(res, "/workflows/:name/runs/:runId/machine")) as MachineDoc;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `POST /workflows/:name/runs` — start a run; unknown workflow → 404 → throws. A named Chart's
|
|
184
|
+
* name `<wrapper>/<name>` rides the path as one encoded segment (ADR-0070); a refused Chart in
|
|
185
|
+
* the input → 422 → throws with the door's `report`. */
|
|
100
186
|
async start(workflow: string, input: Record<string, unknown> = {}): Promise<{ runId: string; instanceId: string }> {
|
|
101
|
-
const res = await this.fetchImpl(`${this.baseUrl}
|
|
187
|
+
const res = await this.fetchImpl(`${this.baseUrl}${workflowPath(workflow)}/runs`, {
|
|
102
188
|
method: "POST",
|
|
103
189
|
headers: this.headers(JSON_HEADERS),
|
|
104
190
|
body: JSON.stringify(input),
|
|
@@ -142,7 +228,8 @@ export class JR2Client {
|
|
|
142
228
|
|
|
143
229
|
/** `POST /runs/:runId/gates/:gate/events` — deliver a workflow-defined event to an open gate
|
|
144
230
|
* (ADR-0011). The body is `{ type, ...input }`, validated against the event's schema host-side;
|
|
145
|
-
* an unknown gate (404)
|
|
231
|
+
* an unknown gate (404), a rejected payload (400) or an answer the run would not take now (409,
|
|
232
|
+
* ADR-0013) surfaces as a throw. */
|
|
146
233
|
async sendToGate(runId: string, gate: string, event: { type: string } & Record<string, unknown>): Promise<void> {
|
|
147
234
|
const res = await this.fetchImpl(
|
|
148
235
|
`${this.baseUrl}/runs/${encodeURIComponent(runId)}/gates/${encodeURIComponent(gate)}/events`,
|
|
@@ -165,26 +252,48 @@ export class JR2Client {
|
|
|
165
252
|
* `GET /runs/:runId/events` — the SSE feed as an async-iterable of `RunFeedEvent`. The current status
|
|
166
253
|
* is replayed first (attach), then deltas + author `emit`s until the terminal status (or the consumer
|
|
167
254
|
* breaks, which cancels the stream). A settled run streams its final status once and ends.
|
|
255
|
+
*
|
|
256
|
+
* A feed that ends BEFORE a terminal status is re-attached, never read as the run's end: the
|
|
257
|
+
* server closes a reader that fell behind (a paused pager on `jr2 run | less`), and the feed is
|
|
258
|
+
* level-triggered, so the re-attach opens with where the run stands now (ADR-0022). What falls in
|
|
259
|
+
* the gap is activity — Emits, retries — never state. The one end that IS the run's own without
|
|
260
|
+
* a terminal status is `gone`: the run was stopped and is parked (ADR-0025); the generator ends.
|
|
168
261
|
*/
|
|
169
262
|
async *events(runId: string): AsyncGenerator<RunFeedEvent> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
263
|
+
for (;;) {
|
|
264
|
+
const res = await this.fetchImpl(`${this.baseUrl}/runs/${encodeURIComponent(runId)}/events`, {
|
|
265
|
+
headers: this.headers({ accept: "text/event-stream" }),
|
|
266
|
+
});
|
|
267
|
+
if (res.status === 404) {
|
|
268
|
+
throw new JR2HttpError(`no run "${runId}"`, 404, "/runs/:runId/events", await this.identify());
|
|
269
|
+
}
|
|
270
|
+
if (!res.body) return;
|
|
271
|
+
let settled = false;
|
|
272
|
+
for await (const frame of parseSSE(res.body)) {
|
|
273
|
+
if (frame.event === "emit") {
|
|
274
|
+
yield { kind: "emit", event: JSON.parse(frame.data) as { type: string } & Record<string, unknown> };
|
|
275
|
+
} else if (frame.event === "retry") {
|
|
276
|
+
yield { kind: "retry", ...(JSON.parse(frame.data) as { child: string; attempt: number; reason: string }) };
|
|
277
|
+
} else if (frame.event === "status") {
|
|
278
|
+
const status = JSON.parse(frame.data) as RunStatus;
|
|
279
|
+
settled = status.status !== "active";
|
|
280
|
+
yield { kind: "status", status };
|
|
281
|
+
} else if (frame.event === "placing" || frame.event === "placed") {
|
|
282
|
+
// A wait for capacity starting and ending (ADR-0064) — activity, never a status.
|
|
283
|
+
yield { ...(JSON.parse(frame.data) as object), kind: frame.event } as PlacingMarker;
|
|
284
|
+
} else if (frame.event === "child-error") {
|
|
285
|
+
// A child's error below an Agent's state, which the Agent's answer does not carry (ADR-0024).
|
|
286
|
+
yield { ...(JSON.parse(frame.data) as object), kind: "child-error" } as ChildErrorMarker;
|
|
287
|
+
} else if (frame.event === "gone") {
|
|
288
|
+
// The run left the live set without settling — stopped, parked for the next boot's
|
|
289
|
+
// restore (ADR-0025). This end is the run's own (ADR-0022): nothing to re-attach to.
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Any other frame (a Turn marker — ADR-0023 — or a kind this CLI predates) is skipped, not
|
|
293
|
+
// misread as a status: `jr2 logs -f` decides "settled" off `status.status`, and a marker
|
|
294
|
+
// parsed as a status would end the follow mid-run.
|
|
184
295
|
}
|
|
185
|
-
|
|
186
|
-
// misread as a status: `jr2 logs -f` decides "settled" off `status.status`, and a marker
|
|
187
|
-
// parsed as a status would end the follow mid-run.
|
|
296
|
+
if (settled) return;
|
|
188
297
|
}
|
|
189
298
|
}
|
|
190
299
|
|
|
@@ -209,10 +318,12 @@ export class JR2Client {
|
|
|
209
318
|
const text = await res.text();
|
|
210
319
|
const body = text ? JSON.parse(text) : undefined;
|
|
211
320
|
if (!res.ok) {
|
|
212
|
-
const
|
|
321
|
+
const { error, report } = (body ?? {}) as { error?: string; report?: unknown };
|
|
322
|
+
const message = error ?? `HTTP ${res.status}`;
|
|
323
|
+
const lines = Array.isArray(report) ? report.map(String) : undefined;
|
|
213
324
|
// Probe HERE, not at the catch site: by the time the error reaches `cli.ts` the command's
|
|
214
325
|
// `finally` has closed the port-forward, and there is nothing left to ask.
|
|
215
|
-
throw new JR2HttpError(message, res.status, path, await this.identify());
|
|
326
|
+
throw new JR2HttpError(message, res.status, path, await this.identify(), lines);
|
|
216
327
|
}
|
|
217
328
|
return body;
|
|
218
329
|
}
|