@revturbine/cli 0.13.0 → 0.14.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 -0
  2. package/dist/cli.js +155 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -81,6 +81,7 @@ Commands that read a config name the version explicitly — there is no default:
81
81
  | `history` | The Release Version Log, newest first. |
82
82
  | `preview` | The open draft's staged changes. |
83
83
  | `evaluate` | Run the live config's placement/entitlement decisions for a user context. |
84
+ | `generate types` | Generate a TypeScript module of entitlement handles (namespaced by entitlement type) from any config version, for type-safe `can()`/`gate()` call sites. `--out <path>` writes the file; the generated header records the exact command to regenerate it. `--json` for the raw handle map. |
84
85
 
85
86
  `--json` on read commands emits machine-readable output. Results go to stdout,
86
87
  diagnostics to stderr.
package/dist/cli.js CHANGED
@@ -6014,11 +6014,11 @@ function collectPublicCollisions(rows, objectType, parentField) {
6014
6014
  const parent = String(row[parentField] ?? "");
6015
6015
  const period = String(row.billing_period ?? "");
6016
6016
  const segment = row.segment_id == null ? "_default" : String(row.segment_id);
6017
- const key = `${tenant} ${parent} ${period} ${segment}`;
6017
+ const key2 = `${tenant} ${parent} ${period} ${segment}`;
6018
6018
  const id = String(row.handle ?? row.id ?? "");
6019
- const bucket = publicByTuple.get(key);
6019
+ const bucket = publicByTuple.get(key2);
6020
6020
  if (bucket) bucket.ids.push(id);
6021
- else publicByTuple.set(key, { ids: [id], parent, period, segment });
6021
+ else publicByTuple.set(key2, { ids: [id], parent, period, segment });
6022
6022
  }
6023
6023
  const findings = [];
6024
6024
  for (const bucket of publicByTuple.values()) {
@@ -8785,9 +8785,9 @@ function getCredential(baseUrl) {
8785
8785
  }
8786
8786
  function removeCredential(baseUrl) {
8787
8787
  const data = read();
8788
- const key = urlKey(baseUrl);
8789
- if (!(key in data.credentials)) return false;
8790
- delete data.credentials[key];
8788
+ const key2 = urlKey(baseUrl);
8789
+ if (!(key2 in data.credentials)) return false;
8790
+ delete data.credentials[key2];
8791
8791
  write(data);
8792
8792
  return true;
8793
8793
  }
@@ -9032,7 +9032,7 @@ function formatDiff(diff) {
9032
9032
  const parts = [];
9033
9033
  if (d.added.length) parts.push(`+${d.added.length} added`);
9034
9034
  if (d.changed.length) parts.push(`~${d.changed.length} changed`);
9035
- if (d.removed.length) parts.push(`-${d.removed.length} only in tenant (import never deletes)`);
9035
+ if (d.removed.length) parts.push(`-${d.removed.length} only in tenant (pruned on launch unless --no-prune)`);
9036
9036
  lines.push(` ${coll}: ${parts.join(", ")}`);
9037
9037
  for (const k of d.added) lines.push(` + ${k}`);
9038
9038
  for (const k of d.changed) lines.push(` ~ ${k}`);
@@ -9208,11 +9208,11 @@ async function revokeIngestKey(baseUrl, headers, id, fetchImpl = fetch) {
9208
9208
  });
9209
9209
  return { ok: res.ok, status: res.status };
9210
9210
  }
9211
- function formatIngestKeyLine(key) {
9212
- const origins = key.originAllowlist.length ? key.originAllowlist.join(", ") : "(none)";
9213
- const ips = key.ipAllowlist.length ? ` ips: ${key.ipAllowlist.join(", ")}` : "";
9214
- const last = key.lastUsedAt ? ` last used ${key.lastUsedAt}` : "";
9215
- return `${key.id} ${key.tokenPreview} origins: ${origins}${ips}${last}`;
9211
+ function formatIngestKeyLine(key2) {
9212
+ const origins = key2.originAllowlist.length ? key2.originAllowlist.join(", ") : "(none)";
9213
+ const ips = key2.ipAllowlist.length ? ` ips: ${key2.ipAllowlist.join(", ")}` : "";
9214
+ const last = key2.lastUsedAt ? ` last used ${key2.lastUsedAt}` : "";
9215
+ return `${key2.id} ${key2.tokenPreview} origins: ${origins}${ips}${last}`;
9216
9216
  }
9217
9217
 
9218
9218
  // src/lib/delegate.ts
@@ -9280,7 +9280,7 @@ function unknownTopLevelKeys(raw) {
9280
9280
  const parsed = schema.safeParse(raw);
9281
9281
  if (!parsed.success || !isRecord3(parsed.data)) return [];
9282
9282
  const recognized = new Set(Object.keys(parsed.data));
9283
- return Object.keys(raw).filter((key) => !recognized.has(key) && !LEGACY_HEADER_ALIASES.has(key));
9283
+ return Object.keys(raw).filter((key2) => !recognized.has(key2) && !LEGACY_HEADER_ALIASES.has(key2));
9284
9284
  }
9285
9285
  function offlineAdvisories(raw) {
9286
9286
  const advisories = [];
@@ -9292,12 +9292,12 @@ function offlineAdvisories(raw) {
9292
9292
  targetRef: { ...finding2.targetRef ?? {} }
9293
9293
  });
9294
9294
  }
9295
- for (const key of unknownTopLevelKeys(raw)) {
9295
+ for (const key2 of unknownTopLevelKeys(raw)) {
9296
9296
  advisories.push({
9297
9297
  code: UNKNOWN_FIELD_CODE,
9298
9298
  severity: "warning",
9299
- message: `Unknown top-level field '${key}' is not part of the RevTurbine config schema (bundled offline snapshot). The CLI uploads it unchanged so the server can validate it \u2014 remove it if it was a typo.`,
9300
- targetRef: { field: key, path: [key] }
9299
+ message: `Unknown top-level field '${key2}' is not part of the RevTurbine config schema (bundled offline snapshot). The CLI uploads it unchanged so the server can validate it \u2014 remove it if it was a typo.`,
9300
+ targetRef: { field: key2, path: [key2] }
9301
9301
  });
9302
9302
  }
9303
9303
  return advisories;
@@ -9481,6 +9481,85 @@ function finalOutputLines(params) {
9481
9481
  return lines;
9482
9482
  }
9483
9483
 
9484
+ // src/lib/handles-codegen.ts
9485
+ var UNTYPED_GROUP = "untyped";
9486
+ function isRecord4(value) {
9487
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9488
+ }
9489
+ function nonEmptyString(value) {
9490
+ return typeof value === "string" && value.length > 0 ? value : null;
9491
+ }
9492
+ function extractEntitlementHandles(config) {
9493
+ if (!isRecord4(config) || !Array.isArray(config.entitlements)) return {};
9494
+ const groups = /* @__PURE__ */ new Map();
9495
+ for (const entry of config.entitlements) {
9496
+ if (!isRecord4(entry)) continue;
9497
+ const handle = nonEmptyString(entry.unique_handle) ?? nonEmptyString(entry.handle);
9498
+ if (!handle) continue;
9499
+ const type = nonEmptyString(entry.type) ?? UNTYPED_GROUP;
9500
+ const set = groups.get(type) ?? /* @__PURE__ */ new Set();
9501
+ set.add(handle);
9502
+ groups.set(type, set);
9503
+ }
9504
+ const byType = {};
9505
+ for (const type of [...groups.keys()].sort()) {
9506
+ byType[type] = [...groups.get(type)].sort();
9507
+ }
9508
+ return byType;
9509
+ }
9510
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
9511
+ var quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
9512
+ var key = (s) => IDENTIFIER.test(s) ? s : quote(s);
9513
+ function renderHandlesModule(byType, opts) {
9514
+ const lines = [
9515
+ "// Generated by `revturbine generate types` \u2014 do not edit by hand.",
9516
+ `// Source: ${opts.source}`,
9517
+ "// Regenerate (rerun after any `launch` that changes entitlements):",
9518
+ `// ${opts.regenerate}`,
9519
+ "",
9520
+ "/** Entitlement handles, namespaced by entitlement type. */"
9521
+ ];
9522
+ const types = Object.keys(byType);
9523
+ if (types.length === 0) {
9524
+ lines.push("export const Entitlements = {} as const;");
9525
+ } else {
9526
+ lines.push("export const Entitlements = {");
9527
+ for (const type of types) {
9528
+ lines.push(` ${key(type)}: {`);
9529
+ for (const handle of byType[type]) {
9530
+ lines.push(` ${key(handle)}: ${quote(handle)},`);
9531
+ }
9532
+ lines.push(" },");
9533
+ }
9534
+ lines.push("} as const;");
9535
+ }
9536
+ const flat = [...new Set(types.flatMap((t) => byType[t]))].sort();
9537
+ lines.push(
9538
+ "",
9539
+ "/** Every entitlement handle \u2014 the argument type for can() / gate() / checkEntitlement(). */"
9540
+ );
9541
+ if (flat.length === 0) {
9542
+ lines.push("export type EntitlementHandle = never;");
9543
+ } else {
9544
+ lines.push("export type EntitlementHandle =");
9545
+ flat.forEach((handle, i) => {
9546
+ lines.push(` | ${quote(handle)}${i === flat.length - 1 ? ";" : ""}`);
9547
+ });
9548
+ }
9549
+ lines.push(
9550
+ "",
9551
+ "/** The entitlement-type namespaces present in this Playbook. */",
9552
+ "export type EntitlementTypeName = keyof typeof Entitlements;",
9553
+ ""
9554
+ );
9555
+ return lines.join("\n");
9556
+ }
9557
+ function generateHandleTypes(config, opts) {
9558
+ const byType = extractEntitlementHandles(config);
9559
+ const handles = [...new Set(Object.values(byType).flat())].sort();
9560
+ return { byType, handles, ts: renderHandlesModule(byType, opts) };
9561
+ }
9562
+
9484
9563
  // src/lib/output.ts
9485
9564
  var EXIT = {
9486
9565
  OK: 0,
@@ -9809,7 +9888,7 @@ function table(rows) {
9809
9888
  }
9810
9889
  var SHOW_KINDS = ["plans", "entitlements", "segments", "placements", "trials"];
9811
9890
  function renderShow(kind, config) {
9812
- const arr = (key) => Array.isArray(config[key]) ? config[key] : [];
9891
+ const arr = (key2) => Array.isArray(config[key2]) ? config[key2] : [];
9813
9892
  const handleOf = (item) => item.handle ?? item.unique_handle ?? item.id ?? "";
9814
9893
  switch (kind) {
9815
9894
  case "plans": {
@@ -9870,6 +9949,8 @@ Command groups:
9870
9949
  Check validate, diff, show
9871
9950
  Stage/launch upload, launch, discard, restore
9872
9951
  Inspect status, history, preview, evaluate
9952
+ Codegen generate types
9953
+ Keys ingest-keys create|list|revoke
9873
9954
 
9874
9955
  Version selectors (no defaults \u2014 a command that reads a config requires one):
9875
9956
  <file> a local Config File (positional, or --file <path>)
@@ -10414,7 +10495,7 @@ program.command("preview").description("The open draft's Runtime Impact summary
10414
10495
  ].join("\n");
10415
10496
  emit(data, Boolean(opts.json), text);
10416
10497
  });
10417
- 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 placement/slot (the getPlacement decision)").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(
10498
+ 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(
10418
10499
  async (opts) => {
10419
10500
  const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "evaluate" });
10420
10501
  const conn = connect(opts.url, opts.tenantId);
@@ -10427,13 +10508,18 @@ program.command("evaluate").description("Run placement/entitlement decisions for
10427
10508
  }
10428
10509
  const body = { ...ctx };
10429
10510
  if (opts.planHandle) body.plan_handle = opts.planHandle;
10430
- if (opts.entitlement && opts.slot) fail(EXIT.USAGE, "pass exactly one of --entitlement or --slot (or neither for the ctx file lists).");
10511
+ const wantsSlot = Boolean(opts.slot || opts.surfaceType);
10512
+ if (opts.entitlement && wantsSlot) {
10513
+ fail(EXIT.USAGE, "pass exactly one of --entitlement or --slot/--surface-type (or neither for the ctx file lists).");
10514
+ }
10431
10515
  if (opts.entitlement) {
10432
10516
  body.entitlement_handles = [opts.entitlement];
10433
10517
  body.placement_ids = [];
10434
10518
  }
10435
- if (opts.slot) {
10436
- body.placement_ids = [opts.slot];
10519
+ if (wantsSlot) {
10520
+ if (opts.slot) body.slot_id = opts.slot;
10521
+ if (opts.surfaceType) body.surface_type = opts.surfaceType;
10522
+ body.placement_ids = [];
10437
10523
  body.entitlement_handles = [];
10438
10524
  }
10439
10525
  if (sel.kind === "release") body.playbook_version_id = sel.id;
@@ -10443,25 +10529,55 @@ program.command("evaluate").description("Run placement/entitlement decisions for
10443
10529
  emit(json, true);
10444
10530
  }
10445
10531
  );
10532
+ var generateCmd = program.command("generate").description("Code generation from a config version (see: generate types).");
10533
+ generateCmd.command("types").description(
10534
+ "Generate a TypeScript module of entitlement handles (namespaced by entitlement type) for type-safe can()/gate() calls."
10535
+ ).argument("[file...]", "Path to a Config File").option("--draft", "The tenant's open draft").option("--live", "The current live Release").option("--release <id>", "A specific playbook version / Release").option("-o, --out <path>", "Write the generated module to this path (parent dirs created); default stdout").option("--json", "Emit the handle map as JSON instead of TypeScript").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
10536
+ async (files, opts) => {
10537
+ const [sel] = requireSelectors(opts, files, {
10538
+ count: 1,
10539
+ allowed: ["file", "draft", "live", "release"],
10540
+ command: "generate types"
10541
+ });
10542
+ const conn = sel.kind !== "file" ? connect(opts.url, opts.tenantId) : null;
10543
+ const config = await loadVersion(conn, sel);
10544
+ const selectorArg = sel.kind === "file" ? sel.path : sel.kind === "release" ? `--release ${sel.id}` : `--${sel.kind}`;
10545
+ const regenerate = ["revturbine generate types", selectorArg, ...opts.out ? ["--out", opts.out] : []].join(" ");
10546
+ const result = generateHandleTypes(config, { source: describeSelector(sel), regenerate });
10547
+ if (result.handles.length === 0) diag("No entitlement handles found in this config version.");
10548
+ const content = opts.json ? `${JSON.stringify({ source: describeSelector(sel), entitlements: result.byType }, null, 2)}
10549
+ ` : result.ts;
10550
+ if (opts.out) {
10551
+ const resolved = path2.resolve(opts.out);
10552
+ mkdirSync2(path2.dirname(resolved), { recursive: true });
10553
+ writeFileSync2(resolved, content);
10554
+ diag(
10555
+ `\u2713 Wrote ${result.handles.length} handle(s) across ${Object.keys(result.byType).length} type(s) to ${opts.out}`
10556
+ );
10557
+ } else {
10558
+ process.stdout.write(content);
10559
+ }
10560
+ }
10561
+ );
10446
10562
  var ingestKeys = program.command("ingest-keys").description("Manage public ingest keys (embeddable SDK telemetry credentials) for the tenant.");
10447
10563
  ingestKeys.command("create").description("Mint a public ingest key. The full token is printed ONCE and cannot be retrieved again.").requiredOption("--origin <url...>", "Allowed browser origin(s), e.g. https://app.example.com (repeatable; at least one required)").option("--ip <cidr...>", "Optional IP / CIDR allowlist (empty = no IP restriction)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--json", "Machine-readable output (token on stdout for scripted capture)").action(async (opts) => {
10448
10564
  const conn = connect(opts.url, opts.tenantId);
10449
10565
  const result = await createIngestKey(conn.url, conn.headers, { originAllowlist: opts.origin, ipAllowlist: opts.ip });
10450
10566
  if (!result.ok || !result.key) httpFail(conn, "ingest-keys create", result.status, result.error);
10451
- const key = result.key;
10567
+ const key2 = result.key;
10452
10568
  if (opts.json) {
10453
- emit(key, true);
10569
+ emit(key2, true);
10454
10570
  return;
10455
10571
  }
10456
10572
  diag("\u2713 Minted a public ingest key. STORE THE TOKEN NOW \u2014 it is shown once and cannot be retrieved again.");
10457
10573
  emit(
10458
- key,
10574
+ key2,
10459
10575
  false,
10460
10576
  [
10461
- ` id: ${key.id}`,
10462
- ` token: ${key.token}`,
10463
- ` origins: ${key.originAllowlist.join(", ")}`,
10464
- key.ipAllowlist.length ? ` ips: ${key.ipAllowlist.join(", ")}` : void 0
10577
+ ` id: ${key2.id}`,
10578
+ ` token: ${key2.token}`,
10579
+ ` origins: ${key2.originAllowlist.join(", ")}`,
10580
+ key2.ipAllowlist.length ? ` ips: ${key2.ipAllowlist.join(", ")}` : void 0
10465
10581
  ].filter((line) => Boolean(line)).join("\n")
10466
10582
  );
10467
10583
  });
@@ -10503,6 +10619,17 @@ var COMMAND_EXAMPLES = {
10503
10619
  " revturbine diff --live --draft What the open draft would change"
10504
10620
  ].join("\n"),
10505
10621
  show: ["", "Examples:", " revturbine show plans --live", " revturbine show segments --file ./config.json"].join("\n"),
10622
+ generate: [
10623
+ "",
10624
+ "Examples:",
10625
+ " revturbine generate types revturbine.playbook.json --out src/revturbine-handles.ts",
10626
+ " revturbine generate types --live --out src/lib/revturbine-handles.gen.ts",
10627
+ " revturbine generate types --draft --json",
10628
+ "",
10629
+ "The generated module exports `Entitlements` (handles namespaced by",
10630
+ "entitlement type) and the `EntitlementHandle` union for can()/gate()",
10631
+ "call sites. Its header records the exact command to regenerate it."
10632
+ ].join("\n"),
10506
10633
  upload: ["", "Example:", " revturbine upload ./config.json Stage as the open draft"].join("\n"),
10507
10634
  launch: [
10508
10635
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revturbine/cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "revturbine — verify RevTurbine ExportedConfig files and ship them to a RevTurbine instance through the Change Set lifecycle.",
5
5
  "license": "MIT",
6
6
  "repository": {