@jr2/cli 0.4.1 → 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 +3 -2
- package/package.json +3 -3
- package/src/agents-md.ts +149 -0
- package/src/assembly.ts +1 -1
- package/src/build.ts +24 -7
- package/src/cli.ts +20 -45
- package/src/client.ts +19 -1
- package/src/command-table.ts +641 -0
- package/src/commands/check.ts +37 -0
- package/src/commands/down.ts +7 -17
- package/src/commands/gc.ts +2 -11
- package/src/commands/init.ts +73 -17
- package/src/commands/kit.ts +6 -11
- package/src/commands/logs.ts +4 -9
- package/src/commands/run.ts +14 -15
- package/src/commands/runs.ts +13 -28
- package/src/commands/send.ts +4 -18
- package/src/commands/status.ts +47 -11
- package/src/commands/up.ts +841 -374
- package/src/commands/version.ts +9 -13
- package/src/deploy.ts +223 -23
- 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 +334 -67
- package/src/nodes.ts +7 -1
- package/src/output.ts +1 -1
- package/src/repo-sweep.ts +1 -1
- package/src/sweep.ts +6 -5
package/src/commands/down.ts
CHANGED
|
@@ -1,7 +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, the Instance Harness's conversations PVC
|
|
4
|
-
// live Sandbox with it). `--all` also uninstalls the
|
|
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
|
|
5
5
|
// per-cluster operator — only sane when this was the cluster's last instance, which is the
|
|
6
6
|
// caller's judgment, not derivable here.
|
|
7
7
|
//
|
|
@@ -22,27 +22,17 @@
|
|
|
22
22
|
// bounded by the node's disk; on kind they live inside the node container and go with the cluster.
|
|
23
23
|
|
|
24
24
|
import { basename } from "node:path";
|
|
25
|
-
import {
|
|
26
|
-
import { loadConfig } from "@jr2/orchestrator";
|
|
25
|
+
import { loadConfig } from "@jr2/orchestrator/internal";
|
|
27
26
|
import { pnpmDockerBuild } from "../build.ts";
|
|
28
27
|
import { KIT_VERSION, LABEL_INSTANCE, operatorManifest } from "../deploy.ts";
|
|
29
28
|
import { resolveRoot } from "../instance.ts";
|
|
30
29
|
import { kubectlAdmin } from "../kube.ts";
|
|
31
30
|
import { activity, confirmOrBail, type Io } from "../output.ts";
|
|
32
31
|
import { sweepImages } from "../sweep.ts";
|
|
32
|
+
import { parseCommand } from "../command-table.ts";
|
|
33
33
|
|
|
34
34
|
export async function down(args: string[], io: Io): Promise<number> {
|
|
35
|
-
const { values } =
|
|
36
|
-
args,
|
|
37
|
-
allowPositionals: true,
|
|
38
|
-
strict: false,
|
|
39
|
-
options: {
|
|
40
|
-
all: { type: "boolean" },
|
|
41
|
-
yes: { type: "boolean" },
|
|
42
|
-
namespace: { type: "string", short: "n" },
|
|
43
|
-
context: { type: "string" },
|
|
44
|
-
},
|
|
45
|
-
});
|
|
35
|
+
const { values } = parseCommand("down", args);
|
|
46
36
|
|
|
47
37
|
const root = resolveRoot(io.cwd);
|
|
48
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,
|
|
@@ -159,7 +211,9 @@ const IMAGE_DOCKERFILE = `# A Sandbox Image (ADR-0037): the tools your agents ca
|
|
|
159
211
|
# (\`user:\` on \`workspace()\`, ADR-0005).
|
|
160
212
|
#
|
|
161
213
|
# Two things jr2 cannot vendor, both proven by \`jr2 up\` before it converges:
|
|
162
|
-
# - \`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);
|
|
163
217
|
# - a glibc base no older than jr2's node. alpine/musl cannot run it at all.
|
|
164
218
|
#
|
|
165
219
|
# This one folder is a path convention; there is no \`images/\` scan (ADR-0049/0050). A SECOND Sandbox
|
|
@@ -196,9 +250,11 @@ const PING_TS = `// The simplest jr2 workflow: no Agent, no Workspace, no data p
|
|
|
196
250
|
// Shape: take the run input, invoke a plain \`fromPromise\` actor, fold its result into context, finish.
|
|
197
251
|
// \`jr2 run ping --input '{"message":"hi"}'\` → the run reaches \`done\` and \`jr2 status\` shows the reply.
|
|
198
252
|
//
|
|
199
|
-
// Module contract (ADR-0011/0015): one named export — \`machine\`.
|
|
200
|
-
//
|
|
201
|
-
//
|
|
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.
|
|
202
258
|
//
|
|
203
259
|
// The next step is a Machine the kit already ships (ADR-0054). \`@jr2/machines\` exports \`task\` — one
|
|
204
260
|
// prompt, one Workspace, one human says done — and a Workflow is only the name an Instance
|
|
@@ -209,7 +265,7 @@ const PING_TS = `// The simplest jr2 workflow: no Agent, no Workspace, no data p
|
|
|
209
265
|
//
|
|
210
266
|
// export const machine = customize(task, {
|
|
211
267
|
// repos: { target: { url: "https://github.com/you/repo.git" } },
|
|
212
|
-
// agents: { coder: { model: "anthropic/claude-
|
|
268
|
+
// agents: { coder: { model: "anthropic/claude-haiku-5-5" } },
|
|
213
269
|
// });
|
|
214
270
|
//
|
|
215
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, orchestrator) from their canonical
|
|
37
|
-
home, and the Custodian's pinned Envoy from its own, into <registry>, for a cluster whose
|
|
38
|
-
\`kitRegistry\` names it (ADR-0044, ADR-0059, ADR-0069)`;
|
|
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,23 +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";
|
|
11
10
|
import { childErrorActivity, placingActivity } from "./run.ts";
|
|
11
|
+
import { parseCommand, usageLine } from "../command-table.ts";
|
|
12
12
|
|
|
13
13
|
export async function logs(args: string[], io: Io): Promise<number> {
|
|
14
|
-
const { values, positionals } =
|
|
15
|
-
args,
|
|
16
|
-
allowPositionals: true,
|
|
17
|
-
strict: false,
|
|
18
|
-
options: { follow: { type: "boolean", short: "f" }, ...TARGET_ARGS },
|
|
19
|
-
});
|
|
14
|
+
const { values, positionals } = parseCommand("logs", args);
|
|
20
15
|
const given = positionals[0];
|
|
21
16
|
if (!given) {
|
|
22
|
-
activity(io, "
|
|
17
|
+
activity(io, usageLine("logs"));
|
|
23
18
|
return 2;
|
|
24
19
|
}
|
|
25
20
|
const target = await resolveTarget(io, targetOptions(values));
|
package/src/commands/run.ts
CHANGED
|
@@ -11,28 +11,25 @@
|
|
|
11
11
|
// A named Chart is a Workflow (ADR-0070): `jr2 run <wrapper>/<name>` starts it as any other run, the
|
|
12
12
|
// name riding the path as one encoded segment. A Chart the door refuses (one-shot, in `--input`)
|
|
13
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.
|
|
14
19
|
|
|
15
|
-
import {
|
|
16
|
-
import type { ChildErrorMarker, PlacingMarker } from "@jr2/orchestrator";
|
|
20
|
+
import type { ChildErrorMarker, PlacingMarker } from "@jr2/orchestrator/internal";
|
|
17
21
|
import { JR2Client, JR2HttpError } from "../client.ts";
|
|
18
|
-
import {
|
|
22
|
+
import { failureActivity } from "./status.ts";
|
|
23
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
19
24
|
import { activity, result, type Io } from "../output.ts";
|
|
25
|
+
import { parseCommand, usageLine } from "../command-table.ts";
|
|
20
26
|
|
|
21
27
|
export async function run(args: string[], io: Io): Promise<number> {
|
|
22
|
-
const { values, positionals } =
|
|
23
|
-
args,
|
|
24
|
-
allowPositionals: true,
|
|
25
|
-
strict: false,
|
|
26
|
-
options: {
|
|
27
|
-
input: { type: "string" },
|
|
28
|
-
detach: { type: "boolean" },
|
|
29
|
-
...TARGET_ARGS,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
28
|
+
const { values, positionals } = parseCommand("run", args);
|
|
32
29
|
|
|
33
30
|
const workflow = positionals[0];
|
|
34
31
|
if (!workflow) {
|
|
35
|
-
activity(io, "
|
|
32
|
+
activity(io, usageLine("run"));
|
|
36
33
|
return 2;
|
|
37
34
|
}
|
|
38
35
|
const input = values.input ? (JSON.parse(String(values.input)) as Record<string, unknown>) : {};
|
|
@@ -73,8 +70,10 @@ export async function run(args: string[], io: Io): Promise<number> {
|
|
|
73
70
|
}
|
|
74
71
|
activity(io, ` → ${ev.status.status} ${JSON.stringify(ev.status.value)}`);
|
|
75
72
|
if (ev.status.status !== "active") {
|
|
73
|
+
const failed = failureActivity(ev.status);
|
|
74
|
+
if (failed) activity(io, failed);
|
|
76
75
|
result(io, ev.status);
|
|
77
|
-
return ev.status.status === "
|
|
76
|
+
return ev.status.status === "done" ? 0 : 1;
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
// `events()` re-attaches until the run settles (ADR-0022), so reaching here means the run left
|
package/src/commands/runs.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// A run with a Workspace in `placing` (ADR-0064) carries `placing`: the child paths of those
|
|
5
5
|
// Workspaces, in `RunWaiting.child`'s form. It is the at-a-glance marker for a run that waits for
|
|
6
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.
|
|
7
9
|
//
|
|
8
10
|
// The verb also lists the Workflows a run can start (ADR-0070), from `GET /workflows`, as activity on
|
|
9
11
|
// stderr so the result on stdout stays the live runs: one line per Workflow with its origin
|
|
@@ -11,32 +13,14 @@
|
|
|
11
13
|
// with its reason. `--workflows` prints that listing as the result instead. No verb puts or deletes
|
|
12
14
|
// a runtime Chart in this step: `PUT`/`DELETE /workflows/<wrapper>/charts/<name>` is for an MCP or curl.
|
|
13
15
|
|
|
14
|
-
import {
|
|
15
|
-
import type { RunChild } from "@jr2/orchestrator";
|
|
16
|
+
import type { RunChild } from "@jr2/orchestrator/internal";
|
|
16
17
|
import { JR2Client, type RunStatus, type WorkflowListing, type WorkflowOrigin } from "../client.ts";
|
|
17
|
-
import { resolveTarget,
|
|
18
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
18
19
|
import { activity, result, type Io } from "../output.ts";
|
|
19
|
-
|
|
20
|
-
export const RUNS_USAGE = `usage: jr2 runs [--workflows]
|
|
21
|
-
|
|
22
|
-
the live runs as JSON on stdout; the Workflows a run can start on stderr, each with its origin
|
|
23
|
-
(machine, chart file, chart) and a Chart wrapper's Charts indented under it (ADR-0070)
|
|
24
|
-
--workflows print the Workflow listing as the JSON result instead
|
|
25
|
-
|
|
26
|
-
a runtime Chart has no verb here: PUT or DELETE /workflows/<wrapper>/charts/<name> with the
|
|
27
|
-
Instance token (an MCP or curl), then \`jr2 run <wrapper>/<name>\` starts it`;
|
|
20
|
+
import { parseCommand } from "../command-table.ts";
|
|
28
21
|
|
|
29
22
|
export async function runs(args: string[], io: Io): Promise<number> {
|
|
30
|
-
const { values } =
|
|
31
|
-
args,
|
|
32
|
-
allowPositionals: true,
|
|
33
|
-
strict: false,
|
|
34
|
-
options: { ...TARGET_ARGS, workflows: { type: "boolean" }, help: { type: "boolean", short: "h" } },
|
|
35
|
-
});
|
|
36
|
-
if (values.help) {
|
|
37
|
-
activity(io, RUNS_USAGE);
|
|
38
|
-
return 0;
|
|
39
|
-
}
|
|
23
|
+
const { values } = parseCommand("runs", args);
|
|
40
24
|
const target = await resolveTarget(io, targetOptions(values));
|
|
41
25
|
try {
|
|
42
26
|
const client = new JR2Client(target.url, io.fetch, target.token);
|
|
@@ -44,7 +28,7 @@ export async function runs(args: string[], io: Io): Promise<number> {
|
|
|
44
28
|
result(io, await client.workflows());
|
|
45
29
|
return 0;
|
|
46
30
|
}
|
|
47
|
-
const live = (await client.list()).map(
|
|
31
|
+
const live = (await client.list()).map(markListed);
|
|
48
32
|
// A report beside the result: a listing that cannot be read says why, and the runs still print.
|
|
49
33
|
try {
|
|
50
34
|
activity(io, renderWorkflows(await client.workflows()));
|
|
@@ -93,12 +77,13 @@ function chartFullName(wrapper: string, name: string): string {
|
|
|
93
77
|
return name.includes("/") ? name : `${wrapper}/${name}`;
|
|
94
78
|
}
|
|
95
79
|
|
|
96
|
-
/** The run
|
|
97
|
-
|
|
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[] } {
|
|
98
83
|
const placing = [...(run.value === "placing" ? [""] : []), ...placingIn(run.children ?? [], [])];
|
|
99
|
-
if (!placing.length) return run;
|
|
100
|
-
const { runId, ...rest } = run;
|
|
101
|
-
return { runId, placing, ...rest };
|
|
84
|
+
if (!placing.length && !run.dispatchedBy) return run;
|
|
85
|
+
const { runId, dispatchedBy, ...rest } = run;
|
|
86
|
+
return { runId, ...(dispatchedBy ? { dispatchedBy } : {}), ...(placing.length ? { placing } : {}), ...rest };
|
|
102
87
|
}
|
|
103
88
|
|
|
104
89
|
/** The paths of every child in `placing`, at any depth — read off the state values alone, which
|
package/src/commands/send.ts
CHANGED
|
@@ -20,33 +20,19 @@
|
|
|
20
20
|
// idle timeout (ADR-0025). It is a fact of this cancel, so it is a flag here and never a config key,
|
|
21
21
|
// and it means nothing on any other event — refused there rather than ignored.
|
|
22
22
|
|
|
23
|
-
import { parseArgs } from "node:util";
|
|
24
23
|
import { JR2Client } from "../client.ts";
|
|
25
|
-
import { resolveTarget,
|
|
24
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
26
25
|
import { activity, type Io } from "../output.ts";
|
|
27
26
|
import { resolveRunId } from "../run-id.ts";
|
|
28
|
-
|
|
29
|
-
const USAGE =
|
|
30
|
-
"usage: jr2 send <runId|abbrev> --event CANCEL [--keep] | jr2 send <runId|abbrev> --gate <gate> --event <name> [--input '<json>']";
|
|
27
|
+
import { commandNamed, parseCommand, renderCommand } from "../command-table.ts";
|
|
31
28
|
|
|
32
29
|
export async function send(args: string[], io: Io): Promise<number> {
|
|
33
|
-
const { values, positionals } =
|
|
34
|
-
args,
|
|
35
|
-
allowPositionals: true,
|
|
36
|
-
strict: false,
|
|
37
|
-
options: {
|
|
38
|
-
event: { type: "string" },
|
|
39
|
-
gate: { type: "string" },
|
|
40
|
-
input: { type: "string" },
|
|
41
|
-
keep: { type: "boolean" },
|
|
42
|
-
...TARGET_ARGS,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
30
|
+
const { values, positionals } = parseCommand("send", args);
|
|
45
31
|
const given = positionals[0];
|
|
46
32
|
const gate = values.gate as string | undefined;
|
|
47
33
|
const event = values.event as string | undefined;
|
|
48
34
|
if (!given || !event) {
|
|
49
|
-
activity(io,
|
|
35
|
+
activity(io, renderCommand(commandNamed("send")));
|
|
50
36
|
return 2;
|
|
51
37
|
}
|
|
52
38
|
const keep = values.keep === true;
|
package/src/commands/status.ts
CHANGED
|
@@ -14,21 +14,24 @@
|
|
|
14
14
|
//
|
|
15
15
|
// A run that binds a Chart (ADR-0070) carries `bound: true`; the verb says so beside the result as
|
|
16
16
|
// `chart: bound`, with the route that draws the Machine the run binds, its Charts compiled in.
|
|
17
|
+
//
|
|
18
|
+
// A dispatched run (ADR-0071) says beside the result who dispatched it: `dispatched by: <run>
|
|
19
|
+
// (<slot>), depth <n>`. A run that dispatched says the other direction, one line per run it
|
|
20
|
+
// started: `dispatched: <run> (<slot>)`. The result itself carries `dispatchedBy`, `dispatched`
|
|
21
|
+
// and, once the run is no longer live, its `output`, as the Orchestrator sends them.
|
|
22
|
+
//
|
|
23
|
+
// A failed run (ADR-0073) says its reason beside the result: `failed: <reason>`. The result's
|
|
24
|
+
// `output` holds the whole Failure, `{ reason, detail? }`.
|
|
17
25
|
|
|
18
|
-
import {
|
|
19
|
-
import type { AssemblyView, RepoStatus } from "@jr2/orchestrator";
|
|
26
|
+
import type { AssemblyView, RepoStatus } from "@jr2/orchestrator/internal";
|
|
20
27
|
import { JR2Client, type FetchLike, type RunStatus, workflowPath } from "../client.ts";
|
|
21
|
-
import { resolveTarget,
|
|
28
|
+
import { resolveTarget, targetOptions } from "../instance.ts";
|
|
22
29
|
import { activity, result, type Io } from "../output.ts";
|
|
23
30
|
import { resolveRunId } from "../run-id.ts";
|
|
31
|
+
import { parseCommand } from "../command-table.ts";
|
|
24
32
|
|
|
25
33
|
export async function status(args: string[], io: Io): Promise<number> {
|
|
26
|
-
const { values, positionals } =
|
|
27
|
-
args,
|
|
28
|
-
allowPositionals: true,
|
|
29
|
-
strict: false,
|
|
30
|
-
options: { ...TARGET_ARGS, json: { type: "boolean" } },
|
|
31
|
-
});
|
|
34
|
+
const { values, positionals } = parseCommand("status", args);
|
|
32
35
|
// NO run named, not an EMPTY one: `jr2 status "$RUNID"` with the variable unset asked about a run
|
|
33
36
|
// and got an empty string, and answering that with the instance's repos would report exit 0 on a
|
|
34
37
|
// question nobody asked. An empty positional falls through to the resolver, which says so.
|
|
@@ -48,14 +51,47 @@ export async function status(args: string[], io: Io): Promise<number> {
|
|
|
48
51
|
return 1;
|
|
49
52
|
}
|
|
50
53
|
result(io, s);
|
|
51
|
-
const
|
|
52
|
-
if (note) activity(io, note);
|
|
54
|
+
const notes = [failureActivity(s), dispatchedActivity(s), ...dispatchedLines(s), boundActivity(s)];
|
|
55
|
+
for (const note of notes) if (note) activity(io, note);
|
|
53
56
|
return 0;
|
|
54
57
|
} finally {
|
|
55
58
|
target.close?.();
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
/** The `failed:` line for a run that ended `failed` (ADR-0073): the Failure's reason, read from its
|
|
63
|
+
* output. Undefined for every other status, and for an output with no reason text. */
|
|
64
|
+
export function failureActivity(s: Pick<RunStatus, "status" | "output">): string | undefined {
|
|
65
|
+
if (s.status !== "failed") return undefined;
|
|
66
|
+
const reason = (s.output as { reason?: unknown } | undefined)?.reason;
|
|
67
|
+
return typeof reason === "string" ? `failed: ${reason}` : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The most `dispatched:` lines `jr2 status` prints: a watcher dispatches on every pass, and the
|
|
71
|
+
* result carries the whole list. */
|
|
72
|
+
const DISPATCHED_LINES = 10;
|
|
73
|
+
|
|
74
|
+
/** The `dispatched:` lines for the runs this run's `dispatch` Effects started (ADR-0071), newest
|
|
75
|
+
* last, the older ones counted. */
|
|
76
|
+
export function dispatchedLines(s: RunStatus): string[] {
|
|
77
|
+
const all = s.dispatched ?? [];
|
|
78
|
+
const shown = all.slice(-DISPATCHED_LINES);
|
|
79
|
+
const older = all.length - shown.length;
|
|
80
|
+
return [
|
|
81
|
+
...(older > 0 ? [`dispatched: ${older} older run(s); the result lists them all`] : []),
|
|
82
|
+
...shown.map((d) => `dispatched: ${d.run} (${d.slot})`),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The `dispatched by:` line for a run a `dispatch` Effect started (ADR-0071), or undefined for a
|
|
87
|
+
* run nobody dispatched. A foreign caller is named by its Orchestrator, as that one sent it: opaque
|
|
88
|
+
* text, which this Orchestrator could not verify. */
|
|
89
|
+
export function dispatchedActivity(s: RunStatus): string | undefined {
|
|
90
|
+
const by = s.dispatchedBy;
|
|
91
|
+
if (!by) return undefined;
|
|
92
|
+
return `dispatched by: ${by.run} (${by.slot}), depth ${by.depth}${by.instance ? `, from Orchestrator ${by.instance}` : ""}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
59
95
|
/** The `chart: bound` line for a run that binds a Chart (ADR-0070), or undefined for one that binds
|
|
60
96
|
* none. The Workflow name rides the route as one encoded segment. */
|
|
61
97
|
export function boundActivity(s: RunStatus): string | undefined {
|