@jr2/cli 0.3.0 → 0.4.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 +1 -1
- package/bin/jr2.js +3 -3
- package/manifests/operator.yaml +265 -23
- package/package.json +5 -5
- package/src/assembly.ts +432 -0
- package/src/build.ts +125 -287
- package/src/cli.ts +6 -2
- package/src/client.ts +126 -33
- package/src/commands/down.ts +2 -1
- package/src/commands/init.ts +9 -2
- package/src/commands/kit.ts +3 -3
- package/src/commands/logs.ts +9 -0
- package/src/commands/run.ts +58 -4
- package/src/commands/runs.ts +96 -4
- package/src/commands/send.ts +20 -7
- package/src/commands/status.ts +90 -14
- package/src/commands/up.ts +1058 -207
- package/src/commands/version.ts +17 -12
- package/src/deploy.ts +657 -78
- package/src/effector-image.ts +133 -0
- package/src/kube.ts +107 -5
- package/src/nodes.ts +27 -2
- package/src/sweep.ts +24 -10
package/src/commands/status.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// `jr2 status [runId|abbrev]` (ADR-0009): print a run's status as JSON on stdout, reading through to the
|
|
2
2
|
// store so a completed run still reports its terminal status + final context. A genuinely unknown run → 1.
|
|
3
3
|
//
|
|
4
|
-
// With NO run named, the verb answers about the INSTANCE instead (ADR-0048/0051):
|
|
5
|
-
//
|
|
4
|
+
// With NO run named, the verb answers about the INSTANCE instead (ADR-0048/0051/0069): every
|
|
5
|
+
// Assembly a live run holds, the current one first, whether it has a data plane at all, and every Repo
|
|
6
|
+
// resource's per-node state, so a Repo that will not clone — an
|
|
6
7
|
// unregistered deploy key (ADR-0047), a wrong url, a git host outage — is discoverable by asking
|
|
7
8
|
// the thing that knows, rather than by tailing pod logs. The Orchestrator serves either way; only
|
|
8
9
|
// the Repo is degraded, and the cache agent retries it on its own.
|
|
@@ -10,10 +11,13 @@
|
|
|
10
11
|
// Two output classes (ADR-0009 as amended): `status <runId>` is a RESULT verb — the RunStatus as one
|
|
11
12
|
// JSON line on stdout, for the pipe. Bare `status` is a REPORT verb — a human table on stdout, or the
|
|
12
13
|
// one object with `--json`.
|
|
14
|
+
//
|
|
15
|
+
// A run that binds a Chart (ADR-0070) carries `bound: true`; the verb says so beside the result as
|
|
16
|
+
// `chart: bound`, with the route that draws the Machine the run binds, its Charts compiled in.
|
|
13
17
|
|
|
14
18
|
import { parseArgs } from "node:util";
|
|
15
|
-
import type { RepoStatus } from "@jr2/orchestrator";
|
|
16
|
-
import { JR2Client } from "../client.ts";
|
|
19
|
+
import type { AssemblyView, RepoStatus } from "@jr2/orchestrator";
|
|
20
|
+
import { JR2Client, type FetchLike, type RunStatus, workflowPath } from "../client.ts";
|
|
17
21
|
import { resolveTarget, TARGET_ARGS, targetOptions } from "../instance.ts";
|
|
18
22
|
import { activity, result, type Io } from "../output.ts";
|
|
19
23
|
import { resolveRunId } from "../run-id.ts";
|
|
@@ -32,7 +36,7 @@ export async function status(args: string[], io: Io): Promise<number> {
|
|
|
32
36
|
const target = await resolveTarget(io, targetOptions(values));
|
|
33
37
|
try {
|
|
34
38
|
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
35
|
-
if (given === undefined) return await instanceStatus(client, io, values.json === true);
|
|
39
|
+
if (given === undefined) return await instanceStatus(client, io, target.url, values.json === true);
|
|
36
40
|
const ref = await resolveRunId(client, given);
|
|
37
41
|
if (!ref.ok) {
|
|
38
42
|
activity(io, ref.message);
|
|
@@ -44,18 +48,32 @@ export async function status(args: string[], io: Io): Promise<number> {
|
|
|
44
48
|
return 1;
|
|
45
49
|
}
|
|
46
50
|
result(io, s);
|
|
51
|
+
const note = boundActivity(s);
|
|
52
|
+
if (note) activity(io, note);
|
|
47
53
|
return 0;
|
|
48
54
|
} finally {
|
|
49
55
|
target.close?.();
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/** The `chart: bound` line for a run that binds a Chart (ADR-0070), or undefined for one that binds
|
|
60
|
+
* none. The Workflow name rides the route as one encoded segment. */
|
|
61
|
+
export function boundActivity(s: RunStatus): string | undefined {
|
|
62
|
+
if (s.bound !== true) return undefined;
|
|
63
|
+
const route = `GET ${workflowPath(s.workflow)}/runs/${encodeURIComponent(s.runId)}/machine`;
|
|
64
|
+
// A drifted run's Chart may no longer admit (D-B): the route then draws the slot's own subgraph
|
|
65
|
+
// and names why in `chartsNotDrawn`, so the line does not claim more than the route draws.
|
|
66
|
+
if (s.status === "drifted") return `chart: bound, drifted — ${route} names each Chart it cannot draw`;
|
|
67
|
+
return `chart: bound — ${route} draws the Machine this run binds`;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
/**
|
|
54
|
-
* The instance's own status: the data-plane switch and every Repo resource. As a table on stdout —
|
|
71
|
+
* The instance's own status: the held Assemblies, the data-plane switch and every Repo resource. As a table on stdout —
|
|
55
72
|
* every node's state per Repo, and every node whose last attempt FAILED spelled out with git's own
|
|
56
73
|
* error, since that is the line a human acts on — or, with `--json`, as the one object (extensible:
|
|
57
|
-
* the instance has more to report eventually). An instance with no data plane says so — "
|
|
58
|
-
*
|
|
74
|
+
* the instance has more to report eventually). An instance with no data plane says so — "it runs
|
|
75
|
+
* outside a cluster" is the answer, not an empty list. A deployed Orchestrator always has one: it
|
|
76
|
+
* wires its kube client whatever the current Assembly composes (ADR-0069).
|
|
59
77
|
*
|
|
60
78
|
* Two failures, two consequences (ADR-0004/0051): a cache that is ABSENT on the node (a cold clone
|
|
61
79
|
* failed) parks every Workspace that needs it there; a cache that is PRESENT but whose last fetch
|
|
@@ -65,18 +83,76 @@ export async function status(args: string[], io: Io): Promise<number> {
|
|
|
65
83
|
* Exit 0 even with Repos failing: this is a report, and a degraded Repo is a state the instance
|
|
66
84
|
* is serving in, not a failure of the asking.
|
|
67
85
|
*/
|
|
68
|
-
async function instanceStatus(client: JR2Client, io: Io, json: boolean): Promise<number> {
|
|
69
|
-
const report = await client.repos();
|
|
86
|
+
async function instanceStatus(client: JR2Client, io: Io, url: string, json: boolean): Promise<number> {
|
|
87
|
+
const report = { assembly: await readAssembly(io.fetch ?? globalThis.fetch, url), ...(await client.repos()) };
|
|
70
88
|
if (json) result(io, report);
|
|
71
89
|
else io.stdout(renderInstanceStatus(report));
|
|
72
90
|
return 0;
|
|
73
91
|
}
|
|
74
92
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
/** `GET /assembly` (ADR-0069): open, like `GET /workflows`, so it needs no token. An answer that
|
|
94
|
+
* cannot be read is reported as the reason, never as a failure of the verb. */
|
|
95
|
+
async function readAssembly(fetchImpl: FetchLike, url: string): Promise<AssemblyView | { error: string }> {
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetchImpl(`${url.replace(/\/+$/, "")}/assembly`);
|
|
98
|
+
if (!res.ok) return { error: `GET /assembly answered HTTP ${res.status}` };
|
|
99
|
+
// Read loosely: a `held` that is absent or not a list reads as none held, so a report never
|
|
100
|
+
// fails on the shape of one field (ADR-0048: a report is not a failure of the asking).
|
|
101
|
+
const view = (await res.json()) as Partial<AssemblyView>;
|
|
102
|
+
return {
|
|
103
|
+
kitVersion: view.kitVersion ?? "",
|
|
104
|
+
current: view.current ?? null,
|
|
105
|
+
held: Array.isArray(view.held) ? view.held : [],
|
|
106
|
+
};
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The Assembly lines (ADR-0069): one per Assembly a live run holds, the current one first and so
|
|
114
|
+
* marked, each with how many live runs hold it — or why there is none. A run holds the Assembly it
|
|
115
|
+
* started under, so after a publish the previous one stays on the volume while a run still holds it.
|
|
116
|
+
* The current Assembly prints first, whatever order `held` uses; the other held Assemblies follow
|
|
117
|
+
* in the server's order.
|
|
118
|
+
*/
|
|
119
|
+
function assemblyLines(view: AssemblyView | { error: string } | undefined): string {
|
|
120
|
+
if (view === undefined) return "";
|
|
121
|
+
if ("error" in view) return `assembly: unknown — ${view.error}\n`;
|
|
122
|
+
const current = view.current;
|
|
123
|
+
const heldList = view.held;
|
|
124
|
+
const line = (hash: string, kitVersion: string, runs: number, isCurrent: boolean): string =>
|
|
125
|
+
`assembly: ${hash} (kit v${kitVersion}${isCurrent ? ", current" : ""}), ` +
|
|
126
|
+
`${runs} live run${runs === 1 ? "" : "s"} hold${runs === 1 ? "s" : ""} it\n`;
|
|
127
|
+
let out = "";
|
|
128
|
+
if (current) {
|
|
129
|
+
const runs = heldList.find((h) => h.hash === current.hash)?.runs.length ?? 0;
|
|
130
|
+
out += line(current.hash, current.kitVersion, runs, true);
|
|
131
|
+
} else {
|
|
132
|
+
out += "assembly: none — `jr2 up` publishes\n";
|
|
133
|
+
}
|
|
134
|
+
for (const held of heldList) {
|
|
135
|
+
if (held.hash === current?.hash) continue;
|
|
136
|
+
out += line(held.hash, held.kitVersion, held.runs.length, false);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The human table — the held Assemblies, current first, one line per Repo, one per node under it, then the
|
|
142
|
+
* hints for what was seen. */
|
|
143
|
+
export function renderInstanceStatus({
|
|
144
|
+
assembly,
|
|
145
|
+
dataPlane,
|
|
146
|
+
repos,
|
|
147
|
+
}: {
|
|
148
|
+
assembly?: AssemblyView | { error: string };
|
|
149
|
+
dataPlane: boolean;
|
|
150
|
+
repos: RepoStatus[];
|
|
151
|
+
}): string {
|
|
152
|
+
let out = assemblyLines(assembly);
|
|
153
|
+
out += dataPlane
|
|
78
154
|
? "data plane: yes\n"
|
|
79
|
-
: "data plane: none — this
|
|
155
|
+
: "data plane: none — this Orchestrator runs outside a cluster, so it provisions no Sandbox (ADR-0069)\n";
|
|
80
156
|
let absent = 0;
|
|
81
157
|
let stale = 0;
|
|
82
158
|
for (const repo of repos) {
|