@revturbine/cli 0.16.4 → 0.17.0

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +70 -35
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -69,7 +69,7 @@ Commands that read a config name the version explicitly — there is no default:
69
69
  | `whoami` | The resolved instance, tenant, credentials source, and whether the stored token works. |
70
70
  | `schema` | Emit the bundled `RevTurbineConfig` JSON schema (for agents to author against). |
71
71
  | `docs` | Print the canonical documentation URL. |
72
- | `download` | Fetch a config version (`--live` / `--draft` / `--release <id>`); `--save`, `--format flatbuffer`. |
72
+ | `download` | Fetch a config version (`--live` / `--draft` / `--release <id>`); `--save`. |
73
73
  | `validate` | Offline schema validation of a `<file>`, or the full server catalog against the open draft (`--draft`). |
74
74
  | `diff` | Compare any two versions (dry-run, no writes). A file vs `--draft`/`--live`/`--release` previews the launch — the server side is the base, so `+`/`-` read as created/pruned on launch. |
75
75
  | `show <kind>` | Summary tables: `plans` · `entitlements` · `segments` · `placements` · `trials` for any version. |
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { createRequire } from "module";
7
7
  import { fileURLToPath } from "url";
8
8
  import { createInterface } from "readline/promises";
9
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
- import { Command, CommanderError, Option } from "commander";
10
+ import { Command, CommanderError } from "commander";
11
11
  import { z as z29 } from "zod";
12
12
 
13
13
  // src/schema/exported-config.snapshot.mjs
@@ -9405,6 +9405,34 @@ async function resolveActiveDraft(baseUrl, headers, fetchImpl = fetch) {
9405
9405
  return { ok: res.ok, status: res.status, draft: res.ok ? json.active ?? null : null };
9406
9406
  }
9407
9407
 
9408
+ // src/lib/evaluate-local.ts
9409
+ import { initRevTurbine } from "@revturbine/sdk/headless";
9410
+ async function evaluateLocal(config, input) {
9411
+ const user = { id: input.userId };
9412
+ if (input.planHandle) user.plan_handle = input.planHandle;
9413
+ if (input.traits && Object.keys(input.traits).length > 0) user.custom = input.traits;
9414
+ const session = await initRevTurbine({
9415
+ user,
9416
+ localRuntime: { playbook: config }
9417
+ });
9418
+ const entitlements = {};
9419
+ for (const handle of input.entitlementHandles ?? []) {
9420
+ entitlements[handle] = await session.checkEntitlement(handle);
9421
+ }
9422
+ const decisions = [];
9423
+ for (const placementId of input.placementIds ?? []) {
9424
+ const controller = session.placement({ placement: { name: placementId } });
9425
+ await controller.load();
9426
+ decisions.push(controller.state.decision);
9427
+ }
9428
+ const placement = input.slot && (input.slot.slotId || input.slot.surfaceType) ? await session.getPlacement({
9429
+ ...input.slot.slotId ? { slotId: input.slot.slotId } : {},
9430
+ ...input.slot.surfaceType ? { surfaceType: input.slot.surfaceType } : {},
9431
+ ...input.planHandle ? { planHandle: input.planHandle } : {}
9432
+ }) : null;
9433
+ return { decisions, entitlements, placement };
9434
+ }
9435
+
9408
9436
  // src/lib/ingest-keys.ts
9409
9437
  async function readJson(res) {
9410
9438
  return await res.json().catch(() => ({}));
@@ -10652,27 +10680,11 @@ program.command("docs").description("Print the canonical documentation URL (and
10652
10680
  }
10653
10681
  }
10654
10682
  });
10655
- program.command("download").description("Fetch a config version from the server. Requires --live, --draft, or --release <id>.").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--draft", "The tenant's open draft (rendered on demand)").option("--live", "The current live Release").option("--release <id>", "A specific playbook version / Release").addOption(new Option("-f, --format <format>", "Representation").choices(["json", "flatbuffer"]).default("json")).option("--save <file>", "Write to <file> instead of stdout").action(
10683
+ program.command("download").description("Fetch a config version from the server. Requires --live, --draft, or --release <id>.").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--draft", "The tenant's open draft (rendered on demand)").option("--live", "The current live Release").option("--release <id>", "A specific playbook version / Release").option("--save <file>", "Write to <file> instead of stdout").action(
10656
10684
  async (opts) => {
10657
10685
  const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "download" });
10658
10686
  const conn = connect(opts.url, opts.tenantId);
10659
10687
  const playbookVersionId = sel.kind === "release" ? sel.id : sel.kind === "draft" ? await requireOpenDraft(conn) : void 0;
10660
- if (opts.format === "flatbuffer") {
10661
- const qs = playbookVersionId ? `?playbookVersionId=${encodeURIComponent(playbookVersionId)}` : "";
10662
- const res = await request(conn, `/api/config/bundle${qs}`);
10663
- if (!res.ok) httpFail(conn, "bundle download", res.status);
10664
- const bytes = Buffer.from(await res.arrayBuffer());
10665
- if (opts.save) {
10666
- const out2 = path2.resolve(opts.save);
10667
- mkdirSync2(path2.dirname(out2), { recursive: true });
10668
- writeFileSync2(out2, bytes);
10669
- diag(`\u2713 Wrote ${bytes.length} bytes \u2192 ${out2}`);
10670
- } else {
10671
- process.stdout.write(`${bytes.toString("base64")}
10672
- `);
10673
- }
10674
- return;
10675
- }
10676
10688
  const config = await downloadConfig(conn, playbookVersionId);
10677
10689
  diag(`Downloaded: ${describePlaybookHeader(readPlaybookHeader(config))}`);
10678
10690
  const out = `${JSON.stringify(config, null, 2)}
@@ -10882,7 +10894,9 @@ program.command("preview").description("The open draft's Runtime Impact summary
10882
10894
  ].join("\n");
10883
10895
  emit(data, Boolean(opts.json), text);
10884
10896
  });
10885
- program.command("evaluate").description("Run placement/entitlement decisions for a user context against a config version. Requires --live, --draft, or --release <id>.").option("--live", "Evaluate the live configuration").option("--draft", "Evaluate the tenant's open draft (clean-room: no suppression/cap history)").option("--release <id>", "Evaluate a past release (from its frozen snapshot)").option("--entitlement <handle>", "Check one entitlement (the checkEntitlement result)").option("--slot <id>", "Evaluate one surface slot (the getPlacement decision for that slot)").option("--surface-type <type>", "Disambiguate a slot that can render more than one surface (with --slot), or resolve by surface type alone").option("--plan-handle <handle>", "Evaluate as if the user were on this plan (overrides the ctx file)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--user <file>", "JSON file: { user_id, customer_id?, plan_handle?, traits?, now_iso? }").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
10897
+ program.command("evaluate").description(
10898
+ "Run placement/entitlement decisions for a user context against a config version \u2014 evaluated LOCALLY: the CLI fetches the version's Playbook and runs the SDK engine in-process (plan 192; clean-room, no suppression/cap history). Requires --live, --draft, or --release <id>."
10899
+ ).option("--live", "Evaluate the live (deployed) configuration").option("--draft", "Evaluate the tenant's open draft").option("--release <id>", "Evaluate a past release (from its frozen snapshot)").option("--entitlement <handle>", "Check one entitlement (the checkEntitlement result)").option("--slot <id>", "Evaluate one surface slot (the getPlacement decision for that slot)").option("--surface-type <type>", "Disambiguate a slot that can render more than one surface (with --slot), or resolve by surface type alone").option("--plan-handle <handle>", "Evaluate as if the user were on this plan (overrides the ctx file)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--user <file>", "JSON file: { user_id, customer_id?, plan_handle?, traits?, now_iso? }").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
10886
10900
  async (opts) => {
10887
10901
  const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "evaluate" });
10888
10902
  const conn = connect(opts.url, opts.tenantId);
@@ -10893,27 +10907,48 @@ program.command("evaluate").description("Run placement/entitlement decisions for
10893
10907
  } catch (err) {
10894
10908
  fail(EXIT.VALIDATION, `invalid JSON in ${opts.user}: ${err.message}`);
10895
10909
  }
10896
- const body = { ...ctx };
10897
- if (opts.planHandle) body.plan_handle = opts.planHandle;
10898
10910
  const wantsSlot = Boolean(opts.slot || opts.surfaceType);
10899
10911
  if (opts.entitlement && wantsSlot) {
10900
10912
  fail(EXIT.USAGE, "pass exactly one of --entitlement or --slot/--surface-type (or neither for the ctx file lists).");
10901
10913
  }
10902
- if (opts.entitlement) {
10903
- body.entitlement_handles = [opts.entitlement];
10904
- body.placement_ids = [];
10914
+ const userId = typeof ctx.user_id === "string" && ctx.user_id.length > 0 ? ctx.user_id : void 0;
10915
+ if (!userId) fail(EXIT.VALIDATION, `user file must carry a non-empty string "user_id" (${opts.user})`);
10916
+ if (typeof ctx.now_iso === "string") {
10917
+ diag("WARNING: now_iso is ignored \u2014 local evaluation uses the real clock.");
10905
10918
  }
10906
- if (wantsSlot) {
10907
- if (opts.slot) body.slot_id = opts.slot;
10908
- if (opts.surfaceType) body.surface_type = opts.surfaceType;
10909
- body.placement_ids = [];
10910
- body.entitlement_handles = [];
10919
+ if (typeof ctx.customer_id === "string") {
10920
+ diag(
10921
+ "WARNING: customer_id is ignored \u2014 supply plan_handle in the ctx file (or --plan-handle). The server-side customer\u2192plan lookup was retired with the decision endpoint (plan 192)."
10922
+ );
10911
10923
  }
10912
- if (sel.kind === "release") body.playbook_version_id = sel.id;
10913
- if (sel.kind === "draft") body.playbook_version_id = await requireOpenDraft(conn);
10914
- const { res, json } = await postJson(conn, "/api/sdk/evaluate", body);
10915
- if (!res.ok) httpFail(conn, "evaluate", res.status, json);
10916
- emit(json, true);
10924
+ let playbookVersionId;
10925
+ if (sel.kind === "release") playbookVersionId = sel.id;
10926
+ if (sel.kind === "draft") playbookVersionId = await requireOpenDraft(conn);
10927
+ const config = await downloadConfig(conn, playbookVersionId);
10928
+ const planHandle = opts.planHandle ?? (typeof ctx.plan_handle === "string" ? ctx.plan_handle : void 0);
10929
+ const bulk = !opts.entitlement && !wantsSlot;
10930
+ const asStringArray2 = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
10931
+ const traits = typeof ctx.traits === "object" && ctx.traits !== null && !Array.isArray(ctx.traits) ? ctx.traits : void 0;
10932
+ const result = await evaluateLocal(config, {
10933
+ userId,
10934
+ planHandle,
10935
+ traits,
10936
+ entitlementHandles: opts.entitlement ? [opts.entitlement] : bulk ? asStringArray2(ctx.entitlement_handles) : [],
10937
+ placementIds: bulk ? asStringArray2(ctx.placement_ids) : [],
10938
+ slot: wantsSlot ? { slotId: opts.slot, surfaceType: opts.surfaceType } : void 0
10939
+ });
10940
+ emit(
10941
+ {
10942
+ tenant_id: conn.tenantId,
10943
+ user_id: userId,
10944
+ ...playbookVersionId ? { playbook_version_id: playbookVersionId } : {},
10945
+ evaluated_at: (/* @__PURE__ */ new Date()).toISOString(),
10946
+ // Provenance: decided in-process by the SDK engine, not by a server.
10947
+ evaluation: "local",
10948
+ ...result
10949
+ },
10950
+ true
10951
+ );
10917
10952
  }
10918
10953
  );
10919
10954
  var generateCmd = program.command("generate").description("Code generation from a config version (see: generate types).");
@@ -10991,7 +11026,7 @@ var COMMAND_EXAMPLES = {
10991
11026
  "Examples:",
10992
11027
  " revturbine download --live --save ./revturbine.playbook.json The live config \u2192 file",
10993
11028
  " revturbine download --draft The open draft (rendered on demand) \u2192 stdout",
10994
- " revturbine download --release cs_1a2b3c --format flatbuffer --save ./bundle.fb"
11029
+ " revturbine download --release cs_1a2b3c --save ./release.playbook.json"
10995
11030
  ].join("\n"),
10996
11031
  validate: [
10997
11032
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revturbine/cli",
3
- "version": "0.16.4",
3
+ "version": "0.17.0",
4
4
  "description": "revturbine — validate RevTurbine Playbooks and ship them to a RevTurbine instance through the playbook-version lifecycle (draft → Release).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,7 +23,6 @@
23
23
  "scripts": {
24
24
  "build": "tsup",
25
25
  "generate:schema": "node scripts/generate-schema-snapshot.mjs",
26
- "revt:sync": "node scripts/sync-revt-deps.mjs",
27
26
  "typecheck": "tsc --noEmit",
28
27
  "test": "vitest run",
29
28
  "lint": "eslint .",
@@ -31,6 +30,7 @@
31
30
  "verify": "npm run typecheck && npm run lint && npm run check:schema && npm run test"
32
31
  },
33
32
  "dependencies": {
33
+ "@revturbine/sdk": "^0.2.88",
34
34
  "commander": "^15.0.0",
35
35
  "zod": "4.4.3"
36
36
  },