@revturbine/cli 0.13.1 → 0.15.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 +241 -23
  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
  }
@@ -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,181 @@ 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 collectStrings(values) {
9558
+ const out = /* @__PURE__ */ new Set();
9559
+ for (const value of values) {
9560
+ const s = nonEmptyString(value);
9561
+ if (s) out.add(s);
9562
+ }
9563
+ return [...out].sort();
9564
+ }
9565
+ function extractPlanHandles(config) {
9566
+ if (!isRecord4(config) || !Array.isArray(config.plans)) return [];
9567
+ return collectStrings(
9568
+ config.plans.map(
9569
+ (p) => isRecord4(p) ? nonEmptyString(p.unique_handle) ?? nonEmptyString(p.handle) ?? p.id : null
9570
+ )
9571
+ );
9572
+ }
9573
+ function extractSegmentHandles(config) {
9574
+ if (!isRecord4(config) || !Array.isArray(config.segments)) return [];
9575
+ return collectStrings(
9576
+ config.segments.map((s) => isRecord4(s) ? nonEmptyString(s.handle) ?? s.id : null)
9577
+ );
9578
+ }
9579
+ function extractSurfaceTemplateIds(config) {
9580
+ if (!isRecord4(config)) return [];
9581
+ const ids = [];
9582
+ if (Array.isArray(config.surface_templates)) {
9583
+ for (const t of config.surface_templates) {
9584
+ if (isRecord4(t)) ids.push(t.id ?? t.template_id);
9585
+ }
9586
+ }
9587
+ const surfacesOf = (payload) => {
9588
+ if (!isRecord4(payload) || !Array.isArray(payload.surfaces)) return;
9589
+ for (const surface of payload.surfaces) {
9590
+ if (isRecord4(surface)) ids.push(surface.template_id);
9591
+ }
9592
+ };
9593
+ if (Array.isArray(config.placements)) {
9594
+ for (const placement of config.placements) {
9595
+ if (!isRecord4(placement) || !Array.isArray(placement.payloads)) continue;
9596
+ for (const payload of placement.payloads) surfacesOf(payload);
9597
+ }
9598
+ }
9599
+ if (Array.isArray(config.placement_payloads)) {
9600
+ for (const payload of config.placement_payloads) surfacesOf(payload);
9601
+ }
9602
+ return collectStrings(ids);
9603
+ }
9604
+ function extractUiPathActionTypes(config) {
9605
+ if (!isRecord4(config) || !Array.isArray(config.content_ui_paths)) return [];
9606
+ return collectStrings(
9607
+ config.content_ui_paths.map((p) => isRecord4(p) ? p.action_type : null)
9608
+ );
9609
+ }
9610
+ function renderFlatFamily(lines, opts) {
9611
+ lines.push("", `/** ${opts.doc} */`);
9612
+ if (opts.values.length === 0) {
9613
+ lines.push(`export const ${opts.constName} = {} as const;`);
9614
+ lines.push(`export type ${opts.typeName} = never;`);
9615
+ return;
9616
+ }
9617
+ lines.push(`export const ${opts.constName} = {`);
9618
+ for (const value of opts.values) {
9619
+ lines.push(` ${key(value)}: ${quote(value)},`);
9620
+ }
9621
+ lines.push("} as const;");
9622
+ lines.push(`export type ${opts.typeName} =`);
9623
+ opts.values.forEach((value, i) => {
9624
+ lines.push(` | ${quote(value)}${i === opts.values.length - 1 ? ";" : ""}`);
9625
+ });
9626
+ }
9627
+ function generateHandleTypes(config, opts) {
9628
+ const byType = extractEntitlementHandles(config);
9629
+ const handles = [...new Set(Object.values(byType).flat())].sort();
9630
+ const lines = [renderHandlesModule(byType, opts).trimEnd()];
9631
+ renderFlatFamily(lines, {
9632
+ doc: "Plan handles \u2014 the values plan targeting and planHandle options take.",
9633
+ constName: "Plans",
9634
+ typeName: "PlanHandle",
9635
+ values: extractPlanHandles(config)
9636
+ });
9637
+ renderFlatFamily(lines, {
9638
+ doc: "Segment handles.",
9639
+ constName: "Segments",
9640
+ typeName: "SegmentHandle",
9641
+ values: extractSegmentHandles(config)
9642
+ });
9643
+ renderFlatFamily(lines, {
9644
+ doc: "Surface-template ids \u2014 catalog entries plus every template a payload references.",
9645
+ constName: "SurfaceTemplates",
9646
+ typeName: "SurfaceTemplateId",
9647
+ values: extractSurfaceTemplateIds(config)
9648
+ });
9649
+ renderFlatFamily(lines, {
9650
+ doc: "Authored ui-path action types \u2014 exactly the key set uiPathResolvers must cover.",
9651
+ constName: "UiPathActionTypes",
9652
+ typeName: "UiPathActionType",
9653
+ values: extractUiPathActionTypes(config)
9654
+ });
9655
+ lines.push("");
9656
+ return { byType, handles, ts: lines.join("\n") };
9657
+ }
9658
+
9484
9659
  // src/lib/output.ts
9485
9660
  var EXIT = {
9486
9661
  OK: 0,
@@ -9809,7 +9984,7 @@ function table(rows) {
9809
9984
  }
9810
9985
  var SHOW_KINDS = ["plans", "entitlements", "segments", "placements", "trials"];
9811
9986
  function renderShow(kind, config) {
9812
- const arr = (key) => Array.isArray(config[key]) ? config[key] : [];
9987
+ const arr = (key2) => Array.isArray(config[key2]) ? config[key2] : [];
9813
9988
  const handleOf = (item) => item.handle ?? item.unique_handle ?? item.id ?? "";
9814
9989
  switch (kind) {
9815
9990
  case "plans": {
@@ -9870,6 +10045,8 @@ Command groups:
9870
10045
  Check validate, diff, show
9871
10046
  Stage/launch upload, launch, discard, restore
9872
10047
  Inspect status, history, preview, evaluate
10048
+ Codegen generate types
10049
+ Keys ingest-keys create|list|revoke
9873
10050
 
9874
10051
  Version selectors (no defaults \u2014 a command that reads a config requires one):
9875
10052
  <file> a local Config File (positional, or --file <path>)
@@ -10448,25 +10625,55 @@ program.command("evaluate").description("Run placement/entitlement decisions for
10448
10625
  emit(json, true);
10449
10626
  }
10450
10627
  );
10628
+ var generateCmd = program.command("generate").description("Code generation from a config version (see: generate types).");
10629
+ generateCmd.command("types").description(
10630
+ "Generate a TypeScript module of entitlement handles (namespaced by entitlement type) for type-safe can()/gate() calls."
10631
+ ).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(
10632
+ async (files, opts) => {
10633
+ const [sel] = requireSelectors(opts, files, {
10634
+ count: 1,
10635
+ allowed: ["file", "draft", "live", "release"],
10636
+ command: "generate types"
10637
+ });
10638
+ const conn = sel.kind !== "file" ? connect(opts.url, opts.tenantId) : null;
10639
+ const config = await loadVersion(conn, sel);
10640
+ const selectorArg = sel.kind === "file" ? sel.path : sel.kind === "release" ? `--release ${sel.id}` : `--${sel.kind}`;
10641
+ const regenerate = ["revturbine generate types", selectorArg, ...opts.out ? ["--out", opts.out] : []].join(" ");
10642
+ const result = generateHandleTypes(config, { source: describeSelector(sel), regenerate });
10643
+ if (result.handles.length === 0) diag("No entitlement handles found in this config version.");
10644
+ const content = opts.json ? `${JSON.stringify({ source: describeSelector(sel), entitlements: result.byType }, null, 2)}
10645
+ ` : result.ts;
10646
+ if (opts.out) {
10647
+ const resolved = path2.resolve(opts.out);
10648
+ mkdirSync2(path2.dirname(resolved), { recursive: true });
10649
+ writeFileSync2(resolved, content);
10650
+ diag(
10651
+ `\u2713 Wrote ${result.handles.length} handle(s) across ${Object.keys(result.byType).length} type(s) to ${opts.out}`
10652
+ );
10653
+ } else {
10654
+ process.stdout.write(content);
10655
+ }
10656
+ }
10657
+ );
10451
10658
  var ingestKeys = program.command("ingest-keys").description("Manage public ingest keys (embeddable SDK telemetry credentials) for the tenant.");
10452
10659
  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) => {
10453
10660
  const conn = connect(opts.url, opts.tenantId);
10454
10661
  const result = await createIngestKey(conn.url, conn.headers, { originAllowlist: opts.origin, ipAllowlist: opts.ip });
10455
10662
  if (!result.ok || !result.key) httpFail(conn, "ingest-keys create", result.status, result.error);
10456
- const key = result.key;
10663
+ const key2 = result.key;
10457
10664
  if (opts.json) {
10458
- emit(key, true);
10665
+ emit(key2, true);
10459
10666
  return;
10460
10667
  }
10461
10668
  diag("\u2713 Minted a public ingest key. STORE THE TOKEN NOW \u2014 it is shown once and cannot be retrieved again.");
10462
10669
  emit(
10463
- key,
10670
+ key2,
10464
10671
  false,
10465
10672
  [
10466
- ` id: ${key.id}`,
10467
- ` token: ${key.token}`,
10468
- ` origins: ${key.originAllowlist.join(", ")}`,
10469
- key.ipAllowlist.length ? ` ips: ${key.ipAllowlist.join(", ")}` : void 0
10673
+ ` id: ${key2.id}`,
10674
+ ` token: ${key2.token}`,
10675
+ ` origins: ${key2.originAllowlist.join(", ")}`,
10676
+ key2.ipAllowlist.length ? ` ips: ${key2.ipAllowlist.join(", ")}` : void 0
10470
10677
  ].filter((line) => Boolean(line)).join("\n")
10471
10678
  );
10472
10679
  });
@@ -10508,6 +10715,17 @@ var COMMAND_EXAMPLES = {
10508
10715
  " revturbine diff --live --draft What the open draft would change"
10509
10716
  ].join("\n"),
10510
10717
  show: ["", "Examples:", " revturbine show plans --live", " revturbine show segments --file ./config.json"].join("\n"),
10718
+ generate: [
10719
+ "",
10720
+ "Examples:",
10721
+ " revturbine generate types revturbine.playbook.json --out src/revturbine-handles.ts",
10722
+ " revturbine generate types --live --out src/lib/revturbine-handles.gen.ts",
10723
+ " revturbine generate types --draft --json",
10724
+ "",
10725
+ "The generated module exports `Entitlements` (handles namespaced by",
10726
+ "entitlement type) and the `EntitlementHandle` union for can()/gate()",
10727
+ "call sites. Its header records the exact command to regenerate it."
10728
+ ].join("\n"),
10511
10729
  upload: ["", "Example:", " revturbine upload ./config.json Stage as the open draft"].join("\n"),
10512
10730
  launch: [
10513
10731
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revturbine/cli",
3
- "version": "0.13.1",
3
+ "version": "0.15.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": {