@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.
@@ -13,17 +13,17 @@
13
13
  // A REPORT verb, not a result verb: the human table goes to stdout, `--json` swaps in the one object.
14
14
 
15
15
  import { basename } from "node:path";
16
- import { parseArgs } from "node:util";
17
- import { IMAGES_CONFIGMAP, IMAGES_KEY, INSTANCE_SECRET, loadConfig, type ImageRefs } from "@jr2/orchestrator";
16
+ import { IMAGES_CONFIGMAP, IMAGES_KEY, INSTANCE_SECRET, loadConfig, type ImageRefs } from "@jr2/orchestrator/internal";
18
17
  import { detectKitCheckout, KIT_IMAGE_HOME } from "../build.ts";
19
18
  import type { InstanceIdentity } from "../client.ts";
20
19
  import { compareVersions, LABEL_VERSION, OPERATOR_DEPLOYMENT, OPERATOR_NAMESPACE } from "../deploy.ts";
21
20
  import { handedOffFrom } from "../handoff.ts";
22
- import { TARGET_ARGS, targetOptions, type TargetOptions } from "../instance.ts";
21
+ import { targetOptions, type TargetOptions } from "../instance.ts";
23
22
  import { checkKitVersion, CLI_ROOT, CLI_VERSION } from "../kit-version.ts";
24
23
  import { kubectlAdmin, kubectlKube, ORCHESTRATOR_PORT, ORCHESTRATOR_SERVICE, type KubeObject } from "../kube.ts";
25
24
  import { activity, result, type Io } from "../output.ts";
26
25
  import { findRoot } from "../root.ts";
26
+ import { parseCommand } from "../command-table.ts";
27
27
 
28
28
  /** A package as the report names it: the number and the real path, so the path says WHICH copy. */
29
29
  export type Copy = { version: string; path: string };
@@ -79,12 +79,7 @@ export type VersionReport = {
79
79
  type Orchestrator = { version?: string; hash?: string; image?: string } | { error: string };
80
80
 
81
81
  export async function version(args: string[], io: Io): Promise<number> {
82
- const { values } = parseArgs({
83
- args,
84
- allowPositionals: true,
85
- strict: false,
86
- options: { ...TARGET_ARGS, local: { type: "boolean" }, json: { type: "boolean" } },
87
- });
82
+ const { values } = parseCommand("version", args);
88
83
  const opts = targetOptions(values);
89
84
  const report = await versionReport(io, opts, { local: values.local === true });
90
85
  if (values.json === true) result(io, report);
@@ -258,7 +253,7 @@ const copy = (c: Copy): string => `${c.version.padEnd(8)} ${c.path}`;
258
253
  export function render(r: VersionReport): string {
259
254
  const rows: Row[] = [["cli", copy(r.cli)]];
260
255
  if (r.global) rows.push(["global", copy(r.global)]);
261
- if (r.kit) rows.push(["kit", renderKit(r.kit)]);
256
+ if (r.kit) rows.push(["kit", renderKit(r.kit, r.cli)]);
262
257
  if (r.instance) {
263
258
  const i = r.instance;
264
259
  const mode = i.mode === "checkout" ? `kit checkout at ${i.kitCheckout}` : `installed kit → ${i.kitRegistry}`;
@@ -273,14 +268,15 @@ export function render(r: VersionReport): string {
273
268
  return rows.map(([label, value]) => (label ? `${`${label}:`.padEnd(width)}${value}` : "")).join("\n") + "\n";
274
269
  }
275
270
 
276
- function renderKit(k: NonNullable<VersionReport["kit"]>): string {
271
+ function renderKit(k: NonNullable<VersionReport["kit"]>, cli: Copy): string {
277
272
  const pinned = k.pinned !== undefined ? `; package.json pins ${k.pinned} — reinstall` : "";
278
273
  if (k.check === "unresolved") return `not installed${pinned}`;
279
274
  const head = copy({ version: k.version!, path: k.path! });
280
275
  if (k.check === "ok") return `${head} ok${pinned}`;
276
+ // The CLI's own number is named: one copy at another version is a mismatch too (ADR-0056).
281
277
  const against = k.against
282
- ? `this jr2 runs against ${k.against.version} (${k.against.path})`
283
- : "this jr2 has no @jr2/orchestrator beside it";
278
+ ? `this jr2 (${cli.version}) runs against ${k.against.version} (${k.against.path})`
279
+ : `this jr2 (${cli.version}) has no @jr2/orchestrator beside it`;
284
280
  return `${head} MISMATCH — ${against}${pinned}`;
285
281
  }
286
282
 
package/src/deploy.ts CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  type HeldManifest,
63
63
  type PriorityClasses,
64
64
  type SandboxPlacement,
65
- } from "@jr2/orchestrator";
65
+ } from "@jr2/orchestrator/internal";
66
66
  import type { Pem } from "./held-pki.ts";
67
67
 
68
68
  export {
@@ -163,6 +163,8 @@ export const KIT_POD_SIZES = {
163
163
  repoCache: { requests: { cpu: "50m", memory: "128Mi" }, limits: { memory: "1Gi" } },
164
164
  /** The Instance Effector (ADR-0068): one http call at a time per Effect, its text body bounded. */
165
165
  instanceEffector: { requests: { cpu: "50m", memory: "128Mi" }, limits: { memory: "128Mi" } },
166
+ /** The provider preflight's check container (ADR-0019): one Node.js process, pi-ai, one call at a time. */
167
+ checkContainer: { requests: { cpu: "100m", memory: "512Mi" }, limits: { memory: "512Mi" } },
166
168
  } as const;
167
169
 
168
170
  /** A fresh copy, so no manifest shares an object with another (or with the constant). */
@@ -366,28 +368,7 @@ export function instanceObjects(opts: {
366
368
  kind: "ConfigMap",
367
369
  metadata: meta(HARNESS_CONFIGMAP),
368
370
  data: {
369
- [HARNESS_CONFIG_KEY]: JSON.stringify(
370
- {
371
- // apiKey is deliberately dropped: it is a held secret (ADR-0059) — the Custodian holds
372
- // it, the Harness's env holds its Stand-in, and a ConfigMap is no place for either.
373
- provider: opts.harness?.provider
374
- ? {
375
- id: opts.harness.provider.id,
376
- api: opts.harness.provider.api,
377
- baseUrl: opts.harness.provider.baseUrl,
378
- // Token limits are model properties, not credentials — they ride the ConfigMap
379
- // so the Harness can register the provider with them.
380
- contextWindow: opts.harness.provider.contextWindow,
381
- maxTokens: opts.harness.provider.maxTokens,
382
- models: opts.harness.provider.models,
383
- }
384
- : undefined,
385
- // Where pi's catalog providers send their calls (ADR-0059): endpoints, not keys.
386
- catalog: opts.harness?.catalog,
387
- },
388
- null,
389
- 2,
390
- ),
371
+ [HARNESS_CONFIG_KEY]: harnessConfigJson(opts.harness),
391
372
  },
392
373
  },
393
374
  {
@@ -1062,6 +1043,225 @@ export function instanceHarnessObjects(opts: {
1062
1043
  return JSON.stringify({ apiVersion: "v1", kind: "List", items });
1063
1044
  }
1064
1045
 
1046
+ /** What the Instance can REACH (ADR-0018), as the `jr2-harness` ConfigMap carries it to every
1047
+ * Harness container: the custom provider without its key and the catalog gateways. The check pod
1048
+ * carries the same text in its own ConfigMap (ADR-0019). */
1049
+ export function harnessConfigJson(harness: HarnessConfig | undefined): string {
1050
+ return JSON.stringify(
1051
+ {
1052
+ // apiKey is deliberately dropped: it is a held secret (ADR-0059) — the Custodian holds
1053
+ // it, the Harness's env holds its Stand-in, and a ConfigMap is no place for either.
1054
+ provider: harness?.provider
1055
+ ? {
1056
+ id: harness.provider.id,
1057
+ api: harness.provider.api,
1058
+ baseUrl: harness.provider.baseUrl,
1059
+ // Token limits are model properties, not credentials — they ride the ConfigMap
1060
+ // so the Harness can register the provider with them.
1061
+ contextWindow: harness.provider.contextWindow,
1062
+ maxTokens: harness.provider.maxTokens,
1063
+ models: harness.provider.models,
1064
+ }
1065
+ : undefined,
1066
+ // Where pi's catalog providers send their calls (ADR-0059): endpoints, not keys.
1067
+ catalog: harness?.catalog,
1068
+ },
1069
+ null,
1070
+ 2,
1071
+ );
1072
+ }
1073
+
1074
+ /** The provider preflight's objects (ADR-0019): a check pod and its own copies of what it reads,
1075
+ * each named `jr2-provider-preflight-<run>-<n>` or that name and a suffix, unique to one `jr2 up`. */
1076
+ export const PROVIDER_PREFLIGHT = "jr2-provider-preflight";
1077
+ /** The label every one of them carries, its value the `jr2 up` run: how a later run finds the
1078
+ * leftovers of a run that did not end, and leaves another run's objects alone. */
1079
+ export const LABEL_PROVIDER_PREFLIGHT = "jr2.dev/provider-preflight";
1080
+ /** The check container in that pod, whose output `jr2 up` reads. An older `jr2 up` named it `probe`. */
1081
+ export const CHECK_CONTAINER = "check";
1082
+
1083
+ /** Every object of one check pod (`checkPodObjects`), for its deletion. */
1084
+ export function checkPodObjectNames(object: string): Array<{ kind: string; name: string }> {
1085
+ return [
1086
+ { kind: "pod", name: object },
1087
+ { kind: "configmap", name: object },
1088
+ { kind: "configmap", name: `${object}-ca` },
1089
+ { kind: "secret", name: object },
1090
+ { kind: "secret", name: `${object}-tls` },
1091
+ { kind: "secret", name: `${object}-env` },
1092
+ ];
1093
+ }
1094
+
1095
+ /**
1096
+ * One check pod of the provider preflight (ADR-0019, ADR-0059): a Harness pod with no Harness in
1097
+ * it, and its own copies of every object a Harness pod reads. So a check pod writes nothing that a
1098
+ * running pod of the Instance reads, and a check that fails leaves the Instance as it was.
1099
+ *
1100
+ * - `<object>` (ConfigMap): the Custodian's bootstrap and script, the program and the harness config
1101
+ * (`harnessConfigJson`).
1102
+ * - `<object>-ca` (ConfigMap): the trust bundles, as `jr2-ca` would carry them.
1103
+ * - `<object>` (Secret): the Custodian's token, a value the Orchestrator never signed (the check pod
1104
+ * reads no Menu), and the LITERAL values of the keys it holds, from the host. A `valueFrom` key
1105
+ * stays a reference to the user's Secret. The values are in this Secret for the check pod's life.
1106
+ * - `<object>-tls` (Secret): the leaves of the bound hosts. `<object>-env` (Secret): the literal
1107
+ * `harness.env` values, which `jr2-harness-env` would carry.
1108
+ *
1109
+ * The Custodian is composed by the function every Harness pod's is (`custodianComposition`). With
1110
+ * no binding, it holds nothing and the check pod's HTTPS goes direct, as in a Harness pod that holds no
1111
+ * secret. The check container runs the stock Harness image's `provider-preflight.ts` with the Harness
1112
+ * container's env, so the call is the one a Turn makes, and the key never enters that container,
1113
+ * its argv or its log. The argv is the model specifiers alone.
1114
+ *
1115
+ * The pod has the placement and the priority class of the Harness pods it stands for (ADR-0052,
1116
+ * ADR-0060). Its labels are its own and match no Harness pod's: the Instance Harness Service
1117
+ * selects by label, and would send Turns to it. The Custodian is a native sidecar with a startup
1118
+ * probe, so the check starts only once Envoy serves and the pod ends when the check does.
1119
+ */
1120
+ export function checkPodObjects(opts: {
1121
+ name: string;
1122
+ namespace: string;
1123
+ /** This check pod's object name (`jr2-provider-preflight-<run>-<n>`) and its run, for the label. */
1124
+ object: string;
1125
+ run: string;
1126
+ /** The resolved stock Harness ref: the image a Turn's Harness runs, with pi-ai at the pin. */
1127
+ harnessImage: string;
1128
+ custodianImage: string;
1129
+ /** The bindings of the keys this check pod holds: each a part of a held pod's manifest. */
1130
+ held: HeldManifest;
1131
+ /** The literal values of those keys, by name, from the host. */
1132
+ values: Record<string, string>;
1133
+ /** The leaves of their bound hosts, by file stem. */
1134
+ leaves: Record<string, Pem>;
1135
+ /** The trust bundles (`ca.crt`, `extra.crt`, `bundle.crt`, `upstream.crt`), as `jr2-ca` has them. */
1136
+ trust: Record<string, string>;
1137
+ /** The Custodian's two files, rendered from `held`. */
1138
+ custodian: CustodianFiles;
1139
+ token: string;
1140
+ /** The literal `harness.env` values. */
1141
+ harnessEnvData: Record<string, string>;
1142
+ /** The `<provider>/<model>` specifiers to check, in order. */
1143
+ models: readonly string[];
1144
+ /** The node predicate of the Harness pods this check pod stands for. */
1145
+ placement?: SandboxPlacement;
1146
+ priorityClassName: string;
1147
+ harness?: HarnessConfig;
1148
+ caBundle?: boolean;
1149
+ /** The pod's whole budget: the kubelet ends it at this deadline, even when `jr2 up` is gone. */
1150
+ deadlineSeconds: number;
1151
+ }): string {
1152
+ const labels = {
1153
+ [LABEL_INSTANCE]: opts.name,
1154
+ "app.kubernetes.io/managed-by": "jr2",
1155
+ [LABEL_PROVIDER_PREFLIGHT]: opts.run,
1156
+ };
1157
+ const meta = (name: string) => ({ name, namespace: opts.namespace, labels });
1158
+ const o = opts.object;
1159
+ const custodian = custodianComposition(opts.held, {
1160
+ image: opts.custodianImage,
1161
+ pod: o,
1162
+ reads: { literals: o, tls: `${o}-tls`, trust: `${o}-ca`, program: o },
1163
+ config: o,
1164
+ token: { secret: o, key: "token" },
1165
+ caBundle: opts.caBundle === true,
1166
+ resources: sized(KIT_POD_SIZES.instanceCustodian),
1167
+ });
1168
+ const ready = custodian.custodianContainer.readinessProbe as { httpGet: unknown };
1169
+ const pod = {
1170
+ apiVersion: "v1",
1171
+ kind: "Pod",
1172
+ metadata: { ...meta(o), labels: { ...labels, app: PROVIDER_PREFLIGHT } },
1173
+ spec: {
1174
+ restartPolicy: "Never",
1175
+ activeDeadlineSeconds: opts.deadlineSeconds,
1176
+ automountServiceAccountToken: false,
1177
+ priorityClassName: opts.priorityClassName,
1178
+ ...(opts.placement?.nodeSelector ? { nodeSelector: opts.placement.nodeSelector } : {}),
1179
+ ...(opts.placement?.tolerations?.length ? { tolerations: opts.placement.tolerations } : {}),
1180
+ // The Instance Harness pod's baseline, and its `fsGroup` for the same reason: the
1181
+ // Custodian's values are group-readable, and Envoy runs as its own uid.
1182
+ securityContext: {
1183
+ runAsNonRoot: true,
1184
+ seccompProfile: { type: "RuntimeDefault" },
1185
+ fsGroup: INSTANCE_HARNESS_FS_GROUP,
1186
+ },
1187
+ initContainers: [
1188
+ {
1189
+ ...custodian.custodianContainer,
1190
+ restartPolicy: "Always",
1191
+ startupProbe: { httpGet: ready.httpGet, periodSeconds: 1, failureThreshold: 60 },
1192
+ },
1193
+ ],
1194
+ containers: [
1195
+ {
1196
+ name: CHECK_CONTAINER,
1197
+ image: opts.harnessImage,
1198
+ imagePullPolicy: "IfNotPresent",
1199
+ // Under tini, as the Harness runs (ADR-0061): PID 1 reaps, and forwards a stop.
1200
+ command: [
1201
+ "/opt/jr2/bin/tini",
1202
+ "--",
1203
+ "/opt/jr2/bin/node",
1204
+ "/opt/jr2/src/provider-preflight.ts",
1205
+ ...opts.models,
1206
+ ],
1207
+ // The Harness container's env, in its order (`instanceHarnessObjects`): no credential.
1208
+ env: [
1209
+ { name: "JR2_HARNESS_JSON", valueFrom: { configMapKeyRef: { name: o, key: HARNESS_CONFIG_KEY } } },
1210
+ ...(opts.harness?.env ?? []).filter((v) => v.valueFrom !== undefined),
1211
+ ...custodian.harnessEnv,
1212
+ ],
1213
+ envFrom: [{ secretRef: { name: `${o}-env` } }, ...(opts.harness?.envFrom ?? [])],
1214
+ volumeMounts: custodian.harnessMounts,
1215
+ resources: sized(KIT_POD_SIZES.checkContainer),
1216
+ securityContext: {
1217
+ runAsNonRoot: true,
1218
+ allowPrivilegeEscalation: false,
1219
+ capabilities: { drop: ["ALL"] },
1220
+ seccompProfile: { type: "RuntimeDefault" },
1221
+ },
1222
+ },
1223
+ ],
1224
+ volumes: custodian.volumes,
1225
+ },
1226
+ };
1227
+ const secret = (name: string, stringData: Record<string, string>): KubeManifest => ({
1228
+ apiVersion: "v1",
1229
+ kind: "Secret",
1230
+ metadata: meta(name),
1231
+ type: "Opaque",
1232
+ stringData,
1233
+ });
1234
+ const leaves: Record<string, string> = {};
1235
+ for (const stem of new Set(opts.held.secrets.flatMap((s) => s.hosts.map((h) => h.leaf)))) {
1236
+ const pem = opts.leaves[stem];
1237
+ if (!pem) throw new Error(`no leaf for a bound host of the provider preflight (${stem}) — a jr2 defect`);
1238
+ leaves[`${stem}.crt`] = pem.cert;
1239
+ leaves[`${stem}.key`] = pem.key;
1240
+ }
1241
+ const literals = Object.fromEntries(
1242
+ opts.held.secrets.filter((s) => s.source.kind === "literal").map((s) => [s.name, opts.values[s.name] ?? ""]),
1243
+ );
1244
+ const items: KubeManifest[] = [
1245
+ {
1246
+ apiVersion: "v1",
1247
+ kind: "ConfigMap",
1248
+ metadata: meta(o),
1249
+ data: {
1250
+ [CUSTODIAN_BOOTSTRAP_KEY]: opts.custodian.bootstrap,
1251
+ [CUSTODIAN_SCRIPT_KEY]: opts.custodian.script,
1252
+ [CUSTODIAN_PROGRAM_KEY]: CUSTODIAN_PROGRAM,
1253
+ [HARNESS_CONFIG_KEY]: harnessConfigJson(opts.harness),
1254
+ },
1255
+ },
1256
+ { apiVersion: "v1", kind: "ConfigMap", metadata: meta(`${o}-ca`), data: opts.trust },
1257
+ secret(o, { token: opts.token, ...literals }),
1258
+ secret(`${o}-tls`, leaves),
1259
+ secret(`${o}-env`, opts.harnessEnvData),
1260
+ pod,
1261
+ ];
1262
+ return JSON.stringify({ apiVersion: "v1", kind: "List", items });
1263
+ }
1264
+
1065
1265
  /** The Orchestrator's pod template digest of the Instance Secret: it rolls the pod when a value
1066
1266
  * the process reads through `envFrom` changes (ADR-0069). */
1067
1267
  export const ANNOTATION_SECRET_DIGEST = "jr2.dev/secret-digest";
package/src/index.ts CHANGED
@@ -14,3 +14,29 @@ export type { KubePort } from "./kube.ts";
14
14
  // test can issue its own fixtures the same way (the @kind tier's HTTPS model provider).
15
15
  export { issueCa, issueLeaf } from "./held-pki.ts";
16
16
  export type { Pem } from "./held-pki.ts";
17
+ // The command table (ADR-0072): every verb as data, and the renderers `--help` prints with — what the
18
+ // docs' CLI Reference is generated from, so the help and the site cannot disagree.
19
+ export {
20
+ COMMANDS,
21
+ GLOBAL,
22
+ GLOBAL_OPTIONS,
23
+ HELP_FLAG,
24
+ SECTIONS,
25
+ EXIT_CODES,
26
+ nameOf,
27
+ synopsis,
28
+ renderHelp,
29
+ renderCommand,
30
+ } from "./command-table.ts";
31
+ export type {
32
+ Command,
33
+ CommandName,
34
+ Flag,
35
+ Arg,
36
+ EnvVar,
37
+ Form,
38
+ Output,
39
+ OutputKind,
40
+ GlobalOption,
41
+ Section,
42
+ } from "./command-table.ts";