@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/commands/down.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// `jr2 down [--all] [-n <ns>] [--context <ctx>]` (ADR-0019): remove the instance from the
|
|
2
|
-
// its namespace and everything `jr2 up` converged into it.
|
|
3
|
-
// takes the runs, the store PVC,
|
|
1
|
+
// `jr2 down [--all] [--yes] [-n <ns>] [--context <ctx>]` (ADR-0019): remove the instance from the
|
|
2
|
+
// cluster — its namespace and everything `jr2 up` converged into it. Confirms unless `--yes`
|
|
3
|
+
// (deleting a namespace takes the runs, the store PVC, the Instance Harness's conversations PVC
|
|
4
|
+
// (ADR-0031), and every live Sandbox with it). `--all` also uninstalls the
|
|
4
5
|
// per-cluster operator — only sane when this was the cluster's last instance, which is the
|
|
5
6
|
// caller's judgment, not derivable here.
|
|
6
7
|
//
|
|
@@ -21,27 +22,17 @@
|
|
|
21
22
|
// bounded by the node's disk; on kind they live inside the node container and go with the cluster.
|
|
22
23
|
|
|
23
24
|
import { basename } from "node:path";
|
|
24
|
-
import {
|
|
25
|
-
import { loadConfig } from "@jr2/orchestrator";
|
|
25
|
+
import { loadConfig } from "@jr2/orchestrator/internal";
|
|
26
26
|
import { pnpmDockerBuild } from "../build.ts";
|
|
27
27
|
import { KIT_VERSION, LABEL_INSTANCE, operatorManifest } from "../deploy.ts";
|
|
28
28
|
import { resolveRoot } from "../instance.ts";
|
|
29
29
|
import { kubectlAdmin } from "../kube.ts";
|
|
30
30
|
import { activity, confirmOrBail, type Io } from "../output.ts";
|
|
31
31
|
import { sweepImages } from "../sweep.ts";
|
|
32
|
+
import { parseCommand } from "../command-table.ts";
|
|
32
33
|
|
|
33
34
|
export async function down(args: string[], io: Io): Promise<number> {
|
|
34
|
-
const { values } =
|
|
35
|
-
args,
|
|
36
|
-
allowPositionals: true,
|
|
37
|
-
strict: false,
|
|
38
|
-
options: {
|
|
39
|
-
all: { type: "boolean" },
|
|
40
|
-
yes: { type: "boolean" },
|
|
41
|
-
namespace: { type: "string", short: "n" },
|
|
42
|
-
context: { type: "string" },
|
|
43
|
-
},
|
|
44
|
-
});
|
|
35
|
+
const { values } = parseCommand("down", args);
|
|
45
36
|
|
|
46
37
|
const root = resolveRoot(io.cwd);
|
|
47
38
|
const config = (await loadConfig(root)) ?? {};
|
package/src/commands/gc.ts
CHANGED
|
@@ -15,24 +15,15 @@
|
|
|
15
15
|
// resolves no instance folder: "disk is full now" has to work from anywhere, and a kube context is
|
|
16
16
|
// the only address this command needs.
|
|
17
17
|
|
|
18
|
-
import { parseArgs } from "node:util";
|
|
19
18
|
import { pnpmDockerBuild } from "../build.ts";
|
|
20
19
|
import { kubectlAdmin } from "../kube.ts";
|
|
21
20
|
import { activity, type Io } from "../output.ts";
|
|
22
21
|
import { DEFAULT_REPO_TTL, parseRepoTtl, sweepRepos } from "../repo-sweep.ts";
|
|
23
22
|
import { sweepImages } from "../sweep.ts";
|
|
23
|
+
import { parseCommand } from "../command-table.ts";
|
|
24
24
|
|
|
25
25
|
export async function gc(args: string[], io: Io): Promise<number> {
|
|
26
|
-
const { values } =
|
|
27
|
-
args,
|
|
28
|
-
allowPositionals: true,
|
|
29
|
-
strict: false,
|
|
30
|
-
options: {
|
|
31
|
-
"dry-run": { type: "boolean" },
|
|
32
|
-
"repo-ttl": { type: "string" },
|
|
33
|
-
context: { type: "string" },
|
|
34
|
-
},
|
|
35
|
-
});
|
|
26
|
+
const { values } = parseCommand("gc", args);
|
|
36
27
|
const ttl = (values["repo-ttl"] as string | undefined) ?? DEFAULT_REPO_TTL;
|
|
37
28
|
// Refused before anything is read: a TTL that does not parse must not become "sweep now".
|
|
38
29
|
parseRepoTtl(ttl);
|
package/src/commands/init.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// does `jr2 up`, which refuses to converge a folder the compiler rejects — ADR-0050), a
|
|
5
5
|
// `.gitignore` for the runtime `.jr2/`, ONE starter workflow (`ping`) that runs end-to-end with no
|
|
6
6
|
// Workspace/Agent — a small "respond directly" Machine to trim down and build on — and
|
|
7
|
-
// `images/default/Dockerfile`, the Sandbox Image every Workspace falls back to (ADR-0037)
|
|
7
|
+
// `images/default/Dockerfile`, the Sandbox Image every Workspace falls back to (ADR-0037), and
|
|
8
|
+
// the `AGENTS.md` an Assistant reads first, with a `CLAUDE.md` that imports it (ADR-0072).
|
|
8
9
|
//
|
|
9
10
|
// What it does NOT scaffold is as deliberate: there is no `agents/` folder and no `images/` scan,
|
|
10
11
|
// because a Machine carries its own Agents and Sandbox Images (ADR-0049) and only what the CLI
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
// `test/init.test.ts` enforces that; without it the two drift silently, and since the manifest
|
|
16
17
|
// carries the kit version that same test is the version-bump tripwire (bump the kit, re-render the
|
|
17
18
|
// template). Existing files are left untouched (init is additive); created paths are reported on
|
|
18
|
-
// stderr.
|
|
19
|
+
// stderr. A user's own AGENTS.md or CLAUDE.md that keeps an Assistant from the docs index gets one
|
|
20
|
+
// hint line under its skip line (ADR-0072).
|
|
19
21
|
//
|
|
20
22
|
// ONE template serves both checkout and installed mode (ADR-0043) — a branch there would mean the
|
|
21
23
|
// tested output and the shipped output diverge. So the scaffold names no package manager, and pins
|
|
@@ -24,20 +26,32 @@
|
|
|
24
26
|
// not the orchestrator's `KIT_VERSION`: a global `jr2` with no Instance around it must scaffold
|
|
25
27
|
// without a peer resolved, and the two numbers are equal by construction (one release train,
|
|
26
28
|
// lockstep — ADR-0055).
|
|
29
|
+
//
|
|
30
|
+
// The `AGENTS.md` block is the one scaffolded text that comes from the Kit, not from this file: the
|
|
31
|
+
// bundle of the CLI's own peer `@jr2/orchestrator`, when that peer is the version the scaffold pins.
|
|
32
|
+
// A global `jr2` has that peer (npm installs a global's peers with it, and a checkout links it), so
|
|
33
|
+
// a fresh folder indexes its docs before the first install — an Assistant can write the first
|
|
34
|
+
// Machine before the first `jr2 up`. Without the bundle (a checkout before `docs:build`), the
|
|
35
|
+
// markers stay empty and `jr2 up` fills them. The template mirrors the file with the block taken
|
|
36
|
+
// out, since the block is generated and nothing generated is committed (ADR-0072).
|
|
27
37
|
|
|
28
38
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
29
39
|
import { basename, dirname, join, resolve } from "node:path";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
40
|
+
import { parseCommand } from "../command-table.ts";
|
|
41
|
+
import {
|
|
42
|
+
AGENTS_MD,
|
|
43
|
+
BEGIN_MARKER,
|
|
44
|
+
CLAUDE_MD,
|
|
45
|
+
END_MARKER,
|
|
46
|
+
placeAgentsBlock,
|
|
47
|
+
readAgentsBlock,
|
|
48
|
+
scaffoldAgentsMd,
|
|
49
|
+
} from "../agents-md.ts";
|
|
50
|
+
import { CLI_ROOT, CLI_VERSION, resolvePackage, type ResolvedPackage } from "../kit-version.ts";
|
|
32
51
|
import { activity, type Io } from "../output.ts";
|
|
33
52
|
|
|
34
53
|
export async function init(args: string[], io: Io): Promise<number> {
|
|
35
|
-
const { values, positionals } =
|
|
36
|
-
args,
|
|
37
|
-
allowPositionals: true,
|
|
38
|
-
strict: false,
|
|
39
|
-
options: { name: { type: "string" } },
|
|
40
|
-
});
|
|
54
|
+
const { values, positionals } = parseCommand("init", args);
|
|
41
55
|
|
|
42
56
|
const dir = positionals[0] ? resolve(io.cwd, String(positionals[0])) : io.cwd;
|
|
43
57
|
const name = (values.name as string | undefined) ?? basename(dir);
|
|
@@ -49,6 +63,8 @@ export async function init(args: string[], io: Io): Promise<number> {
|
|
|
49
63
|
{ path: ".gitignore", content: GITIGNORE },
|
|
50
64
|
{ path: join("workflows", "ping.ts"), content: PING_TS },
|
|
51
65
|
{ path: join("images", "default", "Dockerfile"), content: IMAGE_DOCKERFILE },
|
|
66
|
+
{ path: AGENTS_MD, content: scaffoldAgentsMd(scaffoldBlock(resolvePackage("@jr2/orchestrator", CLI_ROOT))) },
|
|
67
|
+
{ path: "CLAUDE.md", content: CLAUDE_MD },
|
|
52
68
|
];
|
|
53
69
|
|
|
54
70
|
activity(io, `jr2 init — scaffolding ${name} in ${dir}`);
|
|
@@ -56,6 +72,8 @@ export async function init(args: string[], io: Io): Promise<number> {
|
|
|
56
72
|
const full = join(dir, file.path);
|
|
57
73
|
if (await exists(full)) {
|
|
58
74
|
activity(io, ` skip ${file.path} (exists)`);
|
|
75
|
+
const hint = await existingHint(file.path, full);
|
|
76
|
+
if (hint) activity(io, ` ${hint}`);
|
|
59
77
|
continue;
|
|
60
78
|
}
|
|
61
79
|
await mkdir(dirname(full), { recursive: true });
|
|
@@ -66,6 +84,29 @@ export async function init(args: string[], io: Io): Promise<number> {
|
|
|
66
84
|
return 0;
|
|
67
85
|
}
|
|
68
86
|
|
|
87
|
+
/** The block `init` writes between the markers: the bundle of `peer` when it is the exact version
|
|
88
|
+
* the scaffold pins, else nothing (`jr2 up` fills it once the Instance resolves its Kit). */
|
|
89
|
+
export function scaffoldBlock(peer: ResolvedPackage | undefined, pinned: string = CLI_VERSION): string | undefined {
|
|
90
|
+
return peer && peer.version === pinned ? readAgentsBlock(peer.root) : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One line for a user's own `AGENTS.md` or `CLAUDE.md` that init left alone and that keeps an
|
|
94
|
+
* Assistant from the docs index (ADR-0072): a file with no markers has opted out, and Claude Code
|
|
95
|
+
* reads `AGENTS.md` only through a `CLAUDE.md` that imports it. Said, never fixed: the file is the
|
|
96
|
+
* user's. */
|
|
97
|
+
export async function existingHint(path: string, full: string): Promise<string | undefined> {
|
|
98
|
+
if (path === AGENTS_MD) {
|
|
99
|
+
const place = placeAgentsBlock(await readFile(full, "utf8"));
|
|
100
|
+
if (place.kind === "opted-out")
|
|
101
|
+
return `AGENTS.md has no jr2-docs markers: add the lines ${BEGIN_MARKER} and ${END_MARKER}, and jr2 up fills them`;
|
|
102
|
+
if (place.kind === "broken") return `AGENTS.md: ${place.reason}, so jr2 up does not update the jr2 block`;
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (path === "CLAUDE.md" && !/^@AGENTS\.md\s*$/m.test(await readFile(full, "utf8")))
|
|
106
|
+
return "CLAUDE.md does not import AGENTS.md: add the line @AGENTS.md, so that Claude Code reads it";
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
69
110
|
async function exists(path: string): Promise<boolean> {
|
|
70
111
|
try {
|
|
71
112
|
await readFile(path);
|
|
@@ -85,6 +126,17 @@ async function exists(path: string): Promise<boolean> {
|
|
|
85
126
|
// rule for `@jr2/*`, applied to the checker): the instance's PROGRAM includes the kit's `.ts`
|
|
86
127
|
// sources — zero-build, `exports` point at source — so the compiler is part of the contract, not
|
|
87
128
|
// the user's choice, and it moves when the kit moves.
|
|
129
|
+
//
|
|
130
|
+
// `zod` is the other import the scaffold's own files do not make but every Machine an author writes
|
|
131
|
+
// does: each defineEvent and each input schema is a zod object, and the Reference's examples start
|
|
132
|
+
// with `import { z } from "zod"`. A strict node_modules (pnpm) does not let the Instance import a
|
|
133
|
+
// package it does not list, so without it a Machine copied from the bundled Reference is refused by
|
|
134
|
+
// `jr2 up`'s typecheck (ADR-0050). npm hoisted it, by accident. The range is the kit's own.
|
|
135
|
+
//
|
|
136
|
+
// `check` runs `jr2 check`: the typecheck AND the load of each Workflow, which `jr2 up` runs first
|
|
137
|
+
// (ADR-0050). `typecheck` alone does not load a module, so it misses each refusal of
|
|
138
|
+
// `createMachine`, `defineEvent` and `workspace()`. The `jr2` it runs is the Instance's own
|
|
139
|
+
// `@jr2/cli`, which strips the types of the kit sources under `node_modules`.
|
|
88
140
|
function packageJson(name: string): string {
|
|
89
141
|
return `${JSON.stringify(
|
|
90
142
|
{
|
|
@@ -92,8 +144,8 @@ function packageJson(name: string): string {
|
|
|
92
144
|
version: "0.0.0",
|
|
93
145
|
private: true,
|
|
94
146
|
type: "module",
|
|
95
|
-
scripts: { typecheck: "tsc --noEmit" },
|
|
96
|
-
dependencies: { "@jr2/orchestrator": CLI_VERSION, xstate: "^5.18.0" },
|
|
147
|
+
scripts: { check: "jr2 check", typecheck: "tsc --noEmit" },
|
|
148
|
+
dependencies: { "@jr2/orchestrator": CLI_VERSION, xstate: "^5.18.0", zod: "^4.4.3" },
|
|
97
149
|
devDependencies: { "@jr2/cli": CLI_VERSION, "@types/node": "^26.0.1", typescript: "^5.6.0" },
|
|
98
150
|
},
|
|
99
151
|
null,
|
|
@@ -117,9 +169,16 @@ export default defineConfig({
|
|
|
117
169
|
// attach refuses it, so nothing can spend this cluster's credential against an arbitrary host. \`*\` is
|
|
118
170
|
// today's two implicit defaults made visible (JR2_GIT_TOKEN from .env for https, the jr2-git-ssh Secret
|
|
119
171
|
// for ssh). Narrow it to your hosts (\`match: "github.com/yourorg/"\`) before anything untrusted can start
|
|
120
|
-
// a run. The longest match wins; the url's scheme picks token vs sshKey.
|
|
172
|
+
// a run. The longest match wins; the url's scheme picks token vs sshKey. It also PUSHES: a \`push()\` whose
|
|
173
|
+
// Repo Slot url it matches — a per-run url included — spends the same token or key (ADR-0051, ADR-0067).
|
|
121
174
|
credentials: [{ match: "*", token: "JR2_GIT_TOKEN", sshKey: "jr2-git-ssh" }],
|
|
122
175
|
},
|
|
176
|
+
// Every secret of the Instance, one list (ADR-0068). Where each goes is read, never declared — a pod gets
|
|
177
|
+
// what something in it names. A model key goes to the pods whose Agents' provider reads it, and a
|
|
178
|
+
// \`workspace()\` that lists a name under \`secrets\` gets that one: its Custodian holds it and the Agent sees
|
|
179
|
+
// only a Stand-in (ADR-0059). One an Effect names with \`secret("name")\` is a file in that Effect's Effector
|
|
180
|
+
// alone. \`hosts\` says where a held value may go. Read values from .env, never commit them.
|
|
181
|
+
// secrets: [{ name: "ANTHROPIC_API_KEY", value: process.env.ANTHROPIC_API_KEY }],
|
|
123
182
|
// Where Sandboxes land (ADR-0052): by default wherever an ordinary pod lands — not cordoned, no taint — and
|
|
124
183
|
// the Repo cache agent follows the same set. No node label is needed. To admit a tainted pool or narrow to
|
|
125
184
|
// a labeled one, name it in raw pod-spec shapes; \`jr2 up\` reports the Sandbox nodes it sees.
|
|
@@ -152,7 +211,9 @@ const IMAGE_DOCKERFILE = `# A Sandbox Image (ADR-0037): the tools your agents ca
|
|
|
152
211
|
# (\`user:\` on \`workspace()\`, ADR-0005).
|
|
153
212
|
#
|
|
154
213
|
# Two things jr2 cannot vendor, both proven by \`jr2 up\` before it converges:
|
|
155
|
-
# - \`git\` — the agent clones, worktrees, and commits with the git you chose
|
|
214
|
+
# - \`git\` — the agent clones, worktrees, and commits with the git you chose. With no identity
|
|
215
|
+
# here, the agent commits as \`jr2 Agent <agent@jr2.invalid>\`, jr2's default at the lowest rung
|
|
216
|
+
# of git's config: an identity you set at any level wins over it (ADR-0037);
|
|
156
217
|
# - a glibc base no older than jr2's node. alpine/musl cannot run it at all.
|
|
157
218
|
#
|
|
158
219
|
# This one folder is a path convention; there is no \`images/\` scan (ADR-0049/0050). A SECOND Sandbox
|
|
@@ -182,16 +243,18 @@ node_modules/
|
|
|
182
243
|
`;
|
|
183
244
|
|
|
184
245
|
const PING_TS = `// The simplest jr2 workflow: no Agent, no Workspace, no data plane at all. A Machine is free to "just
|
|
185
|
-
// respond to the request" with a plain actor (
|
|
246
|
+
// respond to the request" with a plain actor (ADR-0001: a worktree is one piece a coding Machine bolts on) —
|
|
186
247
|
// this is that case, and the one workflow that runs end-to-end on a fresh instance before any Sandbox /
|
|
187
248
|
// Harness infrastructure exists. Filename \`ping.ts\` → workflow "ping".
|
|
188
249
|
//
|
|
189
250
|
// Shape: take the run input, invoke a plain \`fromPromise\` actor, fold its result into context, finish.
|
|
190
251
|
// \`jr2 run ping --input '{"message":"hi"}'\` → the run reaches \`done\` and \`jr2 status\` shows the reply.
|
|
191
252
|
//
|
|
192
|
-
// Module contract (ADR-0011/0015): one named export — \`machine\`.
|
|
193
|
-
//
|
|
194
|
-
//
|
|
253
|
+
// Module contract (ADR-0011/0015): one named export — \`machine\`. ping takes no workflow event and
|
|
254
|
+
// declares no door, so plain xstate \`setup()\` is enough. A Machine with a door (a zod \`input\` on
|
|
255
|
+
// \`createMachine\`, ADR-0033) uses \`jr2Setup\`, also when it takes no event: then it leaves out
|
|
256
|
+
// \`events\`, as in \`jr2Setup()\`. \`setup()\` does not read that \`input\`, so \`jr2 up\` refuses a
|
|
257
|
+
// zod \`input\` on a Machine that \`setup()\` made.
|
|
195
258
|
//
|
|
196
259
|
// The next step is a Machine the kit already ships (ADR-0054). \`@jr2/machines\` exports \`task\` — one
|
|
197
260
|
// prompt, one Workspace, one human says done — and a Workflow is only the name an Instance
|
|
@@ -202,7 +265,7 @@ const PING_TS = `// The simplest jr2 workflow: no Agent, no Workspace, no data p
|
|
|
202
265
|
//
|
|
203
266
|
// export const machine = customize(task, {
|
|
204
267
|
// repos: { target: { url: "https://github.com/you/repo.git" } },
|
|
205
|
-
// agents: { coder: { model: "anthropic/claude-
|
|
268
|
+
// agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
|
|
206
269
|
// });
|
|
207
270
|
//
|
|
208
271
|
// A packaged Machine leaves the parts it cannot honestly fill OPEN: it does not know your
|
package/src/commands/kit.ts
CHANGED
|
@@ -24,19 +24,14 @@
|
|
|
24
24
|
// swap — it is the decision.
|
|
25
25
|
|
|
26
26
|
import { execFile } from "node:child_process";
|
|
27
|
-
import {
|
|
28
|
-
import { custodianRef, KIT_VERSION } from "@jr2/orchestrator";
|
|
27
|
+
import { promisify } from "node:util";
|
|
28
|
+
import { custodianRef, KIT_VERSION } from "@jr2/orchestrator/internal";
|
|
29
29
|
import { KIT_IMAGES, publishedKitRefs, type KitImageName } from "../build.ts";
|
|
30
30
|
import { activity, result, type Io } from "../output.ts";
|
|
31
|
+
import { commandNamed, parseCommand, renderCommand } from "../command-table.ts";
|
|
31
32
|
|
|
32
33
|
const exec = promisify(execFile);
|
|
33
34
|
|
|
34
|
-
const USAGE = `usage: jr2 kit push <registry>
|
|
35
|
-
|
|
36
|
-
mirror the v${KIT_VERSION} kit images (harness, operator) from their canonical home, and the
|
|
37
|
-
Custodian's pinned Envoy from its own, into <registry>, for a cluster whose \`kitRegistry\`
|
|
38
|
-
names it (ADR-0044, ADR-0059)`;
|
|
39
|
-
|
|
40
35
|
/** Run one `docker` invocation, rejecting on a non-zero exit. The seam this command is tested
|
|
41
36
|
* through: the claims are which argv each ref produces and what a failure means, and both are
|
|
42
37
|
* decidable with no registry anywhere near the test. Narrower than `build.ts`'s `RunCommand` on
|
|
@@ -51,17 +46,17 @@ const dockerCli: RunDocker = async (args) => {
|
|
|
51
46
|
type Mirrored = { image: KitImageName | "custodian"; source: string; target: string; copied: boolean };
|
|
52
47
|
|
|
53
48
|
export async function kit(args: string[], io: Io, docker: RunDocker = dockerCli): Promise<number> {
|
|
54
|
-
const { positionals } =
|
|
49
|
+
const { positionals } = parseCommand("kit push", args);
|
|
55
50
|
const [sub, registry] = positionals;
|
|
56
51
|
|
|
57
52
|
if (sub !== "push") {
|
|
58
53
|
activity(io, sub === undefined ? "jr2 kit: no subcommand" : `unknown kit subcommand: ${sub}`);
|
|
59
|
-
activity(io,
|
|
54
|
+
activity(io, renderCommand(commandNamed("kit push")));
|
|
60
55
|
return 2;
|
|
61
56
|
}
|
|
62
57
|
if (!registry) {
|
|
63
58
|
activity(io, "jr2 kit push: no target registry");
|
|
64
|
-
activity(io,
|
|
59
|
+
activity(io, renderCommand(commandNamed("kit push")));
|
|
65
60
|
return 2;
|
|
66
61
|
}
|
|
67
62
|
|
package/src/commands/logs.ts
CHANGED
|
@@ -3,22 +3,18 @@
|
|
|
3
3
|
// author emits → stderr). `-f`/`--follow` keeps streaming status deltas until the run settles; without
|
|
4
4
|
// it, we print the replayed status and stop. A run that already settled streams its final status once.
|
|
5
5
|
|
|
6
|
-
import { parseArgs } from "node:util";
|
|
7
6
|
import { JR2Client } from "../client.ts";
|
|
8
|
-
import { resolveTarget,
|
|
7
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
9
8
|
import { activity, result, type Io } from "../output.ts";
|
|
10
9
|
import { resolveRunId } from "../run-id.ts";
|
|
10
|
+
import { childErrorActivity, placingActivity } from "./run.ts";
|
|
11
|
+
import { parseCommand, usageLine } from "../command-table.ts";
|
|
11
12
|
|
|
12
13
|
export async function logs(args: string[], io: Io): Promise<number> {
|
|
13
|
-
const { values, positionals } =
|
|
14
|
-
args,
|
|
15
|
-
allowPositionals: true,
|
|
16
|
-
strict: false,
|
|
17
|
-
options: { follow: { type: "boolean", short: "f" }, ...TARGET_ARGS },
|
|
18
|
-
});
|
|
14
|
+
const { values, positionals } = parseCommand("logs", args);
|
|
19
15
|
const given = positionals[0];
|
|
20
16
|
if (!given) {
|
|
21
|
-
activity(io, "
|
|
17
|
+
activity(io, usageLine("logs"));
|
|
22
18
|
return 2;
|
|
23
19
|
}
|
|
24
20
|
const target = await resolveTarget(io, targetOptions(values));
|
|
@@ -39,6 +35,14 @@ export async function logs(args: string[], io: Io): Promise<number> {
|
|
|
39
35
|
activity(io, `retry ${ev.child} attempt ${ev.attempt} (${ev.reason})`);
|
|
40
36
|
continue;
|
|
41
37
|
}
|
|
38
|
+
if (ev.kind === "placing" || ev.kind === "placed") {
|
|
39
|
+
activity(io, placingActivity(ev));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ev.kind === "child-error") {
|
|
43
|
+
activity(io, childErrorActivity(ev));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
42
46
|
result(io, ev.status);
|
|
43
47
|
if (!values.follow) break;
|
|
44
48
|
if (ev.status.status !== "active") break;
|
package/src/commands/run.ts
CHANGED
|
@@ -7,27 +7,29 @@
|
|
|
7
7
|
// kube context — ADR-0019) rather than a per-invocation runtime, because jr2 runs are durable and may
|
|
8
8
|
// park indefinitely on an approval gate. `--detach` prints the runId and returns, leaving the run
|
|
9
9
|
// going server-side.
|
|
10
|
+
//
|
|
11
|
+
// A named Chart is a Workflow (ADR-0070): `jr2 run <wrapper>/<name>` starts it as any other run, the
|
|
12
|
+
// name riding the path as one encoded segment. A Chart the door refuses (one-shot, in `--input`)
|
|
13
|
+
// is a 422 whose report lines this prints, one per rule the Chart fails.
|
|
14
|
+
//
|
|
15
|
+
// The exit code (ADR-0073): 0 only for a run that ended `done`. 1 for every other end — `failed`,
|
|
16
|
+
// `error`, `cancelled`, `lost`, `drifted` — and for a run that was parked before it settled. 2 is a
|
|
17
|
+
// usage error, as for every verb. The status line on stderr says which end it was, and a failed
|
|
18
|
+
// run's reason follows it.
|
|
10
19
|
|
|
11
|
-
import {
|
|
12
|
-
import { JR2Client } from "../client.ts";
|
|
13
|
-
import {
|
|
20
|
+
import type { ChildErrorMarker, PlacingMarker } from "@jr2/orchestrator/internal";
|
|
21
|
+
import { JR2Client, JR2HttpError } from "../client.ts";
|
|
22
|
+
import { failureActivity } from "./status.ts";
|
|
23
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
14
24
|
import { activity, result, type Io } from "../output.ts";
|
|
25
|
+
import { parseCommand, usageLine } from "../command-table.ts";
|
|
15
26
|
|
|
16
27
|
export async function run(args: string[], io: Io): Promise<number> {
|
|
17
|
-
const { values, positionals } =
|
|
18
|
-
args,
|
|
19
|
-
allowPositionals: true,
|
|
20
|
-
strict: false,
|
|
21
|
-
options: {
|
|
22
|
-
input: { type: "string" },
|
|
23
|
-
detach: { type: "boolean" },
|
|
24
|
-
...TARGET_ARGS,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
28
|
+
const { values, positionals } = parseCommand("run", args);
|
|
27
29
|
|
|
28
30
|
const workflow = positionals[0];
|
|
29
31
|
if (!workflow) {
|
|
30
|
-
activity(io, "
|
|
32
|
+
activity(io, usageLine("run"));
|
|
31
33
|
return 2;
|
|
32
34
|
}
|
|
33
35
|
const input = values.input ? (JSON.parse(String(values.input)) as Record<string, unknown>) : {};
|
|
@@ -35,7 +37,14 @@ export async function run(args: string[], io: Io): Promise<number> {
|
|
|
35
37
|
try {
|
|
36
38
|
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
let runId: string;
|
|
41
|
+
try {
|
|
42
|
+
({ runId } = await client.start(workflow, input));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (!(err instanceof JR2HttpError) || !err.report) throw err;
|
|
45
|
+
activity(io, refusalActivity(workflow, err.report));
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
39
48
|
if (values.detach) {
|
|
40
49
|
result(io, { runId });
|
|
41
50
|
return 0;
|
|
@@ -51,14 +60,58 @@ export async function run(args: string[], io: Io): Promise<number> {
|
|
|
51
60
|
activity(io, ` retry ${ev.child} attempt ${ev.attempt} (${ev.reason})`);
|
|
52
61
|
continue;
|
|
53
62
|
}
|
|
63
|
+
if (ev.kind === "placing" || ev.kind === "placed") {
|
|
64
|
+
activity(io, ` ${placingActivity(ev)}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ev.kind === "child-error") {
|
|
68
|
+
activity(io, ` ${childErrorActivity(ev)}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
54
71
|
activity(io, ` → ${ev.status.status} ${JSON.stringify(ev.status.value)}`);
|
|
55
72
|
if (ev.status.status !== "active") {
|
|
73
|
+
const failed = failureActivity(ev.status);
|
|
74
|
+
if (failed) activity(io, failed);
|
|
56
75
|
result(io, ev.status);
|
|
57
|
-
return ev.status.status === "
|
|
76
|
+
return ev.status.status === "done" ? 0 : 1;
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
|
-
|
|
79
|
+
// `events()` re-attaches until the run settles (ADR-0022), so reaching here means the run left
|
|
80
|
+
// the live set without settling: stopped, parked for the next boot's restore (ADR-0025). Never
|
|
81
|
+
// a success, and nothing on stdout to be mistaken for a result.
|
|
82
|
+
activity(
|
|
83
|
+
io,
|
|
84
|
+
`run ${runId} is parked: stopped before it settled — \`jr2 logs -f ${runId}\` follows it once restored`,
|
|
85
|
+
);
|
|
86
|
+
return 1;
|
|
61
87
|
} finally {
|
|
62
88
|
target.close?.();
|
|
63
89
|
}
|
|
64
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The door's refusal of a Chart (ADR-0070) as activity: the Workflow, then each report line, which
|
|
94
|
+
* names the path in the Chart and the rule it fails.
|
|
95
|
+
*/
|
|
96
|
+
export function refusalActivity(workflow: string, report: readonly string[]): string {
|
|
97
|
+
return [`error: the door refused the Chart for "${workflow}"`, ...report.map((line) => ` ${line}`)].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A wait for capacity as one activity line (ADR-0064): the Workspace, what it waits for, and the
|
|
102
|
+
* scheduler's or the quota's own words — then, once it has a node, how long it waited. The run's
|
|
103
|
+
* root Workspace has no child path, so its line names none.
|
|
104
|
+
*/
|
|
105
|
+
export function placingActivity(ev: PlacingMarker): string {
|
|
106
|
+
const who = ev.child ? ` ${ev.child}` : "";
|
|
107
|
+
if (ev.kind === "placed") return `placed${who} after ${Math.round(ev.after / 1000)}s`;
|
|
108
|
+
return `placing${who}: waits for ${ev.on === "quota" ? "quota" : "a node"} — ${ev.message}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A child's error below an Agent's state as one activity line (ADR-0024): the pick's answer carries
|
|
113
|
+
* no reply for it, so the Agent is not told, and this line is where the operator reads it.
|
|
114
|
+
*/
|
|
115
|
+
export function childErrorActivity(ev: ChildErrorMarker): string {
|
|
116
|
+
return `child-error: \`${ev.child}\` failed below ${ev.agent}'s pick \`${ev.pick}\` — ${ev.error}`;
|
|
117
|
+
}
|
package/src/commands/runs.ts
CHANGED
|
@@ -1,19 +1,96 @@
|
|
|
1
1
|
// `jr2 runs` (ADR-0009): list the live runs as JSON on stdout. (Settled runs leave the live registry;
|
|
2
2
|
// read a specific one through to the store with `jr2 status <runId>`.)
|
|
3
|
+
//
|
|
4
|
+
// A run with a Workspace in `placing` (ADR-0064) carries `placing`: the child paths of those
|
|
5
|
+
// Workspaces, in `RunWaiting.child`'s form. It is the at-a-glance marker for a run that waits for
|
|
6
|
+
// capacity — why it waits is `jr2 status <runId>`'s `waiting`. A run with none has no `placing` key.
|
|
7
|
+
// A dispatched run (ADR-0071) carries `dispatchedBy` after its id, as the Orchestrator sends it: the
|
|
8
|
+
// run that started it, the caller's slot and the chain's depth, so the chain reads off the listing.
|
|
9
|
+
//
|
|
10
|
+
// The verb also lists the Workflows a run can start (ADR-0070), from `GET /workflows`, as activity on
|
|
11
|
+
// stderr so the result on stdout stays the live runs: one line per Workflow with its origin
|
|
12
|
+
// (`machine`, `chart file`, `chart`), a Chart wrapper's Charts indented under it, and `unrunnable`
|
|
13
|
+
// with its reason. `--workflows` prints that listing as the result instead. No verb puts or deletes
|
|
14
|
+
// a runtime Chart in this step: `PUT`/`DELETE /workflows/<wrapper>/charts/<name>` is for an MCP or curl.
|
|
3
15
|
|
|
4
|
-
import {
|
|
5
|
-
import { JR2Client } from "../client.ts";
|
|
6
|
-
import { resolveTarget,
|
|
7
|
-
import { result, type Io } from "../output.ts";
|
|
16
|
+
import type { RunChild } from "@jr2/orchestrator/internal";
|
|
17
|
+
import { JR2Client, type RunStatus, type WorkflowListing, type WorkflowOrigin } from "../client.ts";
|
|
18
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
19
|
+
import { activity, result, type Io } from "../output.ts";
|
|
20
|
+
import { parseCommand } from "../command-table.ts";
|
|
8
21
|
|
|
9
22
|
export async function runs(args: string[], io: Io): Promise<number> {
|
|
10
|
-
const { values } =
|
|
23
|
+
const { values } = parseCommand("runs", args);
|
|
11
24
|
const target = await resolveTarget(io, targetOptions(values));
|
|
12
25
|
try {
|
|
13
26
|
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
14
|
-
|
|
27
|
+
if (values.workflows) {
|
|
28
|
+
result(io, await client.workflows());
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
const live = (await client.list()).map(markListed);
|
|
32
|
+
// A report beside the result: a listing that cannot be read says why, and the runs still print.
|
|
33
|
+
try {
|
|
34
|
+
activity(io, renderWorkflows(await client.workflows()));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
activity(io, `workflows: unknown — ${e instanceof Error ? e.message : String(e)}`);
|
|
37
|
+
}
|
|
38
|
+
result(io, live);
|
|
15
39
|
return 0;
|
|
16
40
|
} finally {
|
|
17
41
|
target.close?.();
|
|
18
42
|
}
|
|
19
43
|
}
|
|
44
|
+
|
|
45
|
+
/** How a listing names an origin (ADR-0070): a runtime Chart is plainly a `chart`. */
|
|
46
|
+
const ORIGIN_LABEL: Record<WorkflowOrigin, string> = { machine: "machine", file: "chart file", runtime: "chart" };
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The Workflow listing as lines: each Workflow with its origin, a Chart wrapper's Open body and
|
|
50
|
+
* whether its door takes a Chart one-shot, its Charts indented under it, and `unrunnable` with the
|
|
51
|
+
* reason. A Chart the listing also gives at the top level (or whose wrapper it does not list) prints
|
|
52
|
+
* once: under its wrapper when the wrapper is listed, else at the top level.
|
|
53
|
+
*/
|
|
54
|
+
export function renderWorkflows(listing: readonly WorkflowListing[]): string {
|
|
55
|
+
const underWrapper = new Set<string>();
|
|
56
|
+
for (const w of listing) for (const c of w.wrapper?.charts ?? []) underWrapper.add(chartFullName(w.name, c.name));
|
|
57
|
+
const line = (indent: string, name: string, label: string, unrunnable?: string): string =>
|
|
58
|
+
`${indent}${name} (${label})${unrunnable ? ` unrunnable: ${unrunnable}` : ""}`;
|
|
59
|
+
const out = ["workflows:"];
|
|
60
|
+
for (const w of listing) {
|
|
61
|
+
if (w.origin !== "machine" && underWrapper.has(w.name)) continue;
|
|
62
|
+
const wrapper = w.wrapper;
|
|
63
|
+
const label = wrapper
|
|
64
|
+
? `${ORIGIN_LABEL[w.origin]}, Chart wrapper: body "${wrapper.body}"${wrapper.oneShot ? ", one-shot" : ""}`
|
|
65
|
+
: ORIGIN_LABEL[w.origin];
|
|
66
|
+
out.push(line(" ", w.name, label, w.unrunnable));
|
|
67
|
+
for (const c of wrapper?.charts ?? []) {
|
|
68
|
+
out.push(line(" ", chartFullName(w.name, c.name), ORIGIN_LABEL[c.origin], c.unrunnable));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (out.length === 1) out.push(" none registered — `jr2 up` publishes the Assembly");
|
|
72
|
+
return out.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A Chart's Workflow name, `<wrapper>/<name>` (ADR-0070), whether the listing gives it short or whole. */
|
|
76
|
+
function chartFullName(wrapper: string, name: string): string {
|
|
77
|
+
return name.includes("/") ? name : `${wrapper}/${name}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The run as `jr2 runs` lists it: `dispatchedBy` after its id when a `dispatch` Effect started it
|
|
81
|
+
* (ADR-0071), and `placing` after that when any Workspace in it is in `placing`. */
|
|
82
|
+
export function markListed(run: RunStatus): RunStatus & { placing?: string[] } {
|
|
83
|
+
const placing = [...(run.value === "placing" ? [""] : []), ...placingIn(run.children ?? [], [])];
|
|
84
|
+
if (!placing.length && !run.dispatchedBy) return run;
|
|
85
|
+
const { runId, dispatchedBy, ...rest } = run;
|
|
86
|
+
return { runId, ...(dispatchedBy ? { dispatchedBy } : {}), ...(placing.length ? { placing } : {}), ...rest };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The paths of every child in `placing`, at any depth — read off the state values alone, which
|
|
90
|
+
* are public (a Workspace's value is the flat state key). */
|
|
91
|
+
function placingIn(children: RunChild[], path: string[]): string[] {
|
|
92
|
+
return children.flatMap((child) => {
|
|
93
|
+
const here = [...path, child.id];
|
|
94
|
+
return [...(child.value === "placing" ? [here.join("/")] : []), ...placingIn(child.children, here)];
|
|
95
|
+
});
|
|
96
|
+
}
|