@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.
- package/README.md +1 -0
- package/dist/cli.js +155 -28
- 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
|
|
6017
|
+
const key2 = `${tenant} ${parent} ${period} ${segment}`;
|
|
6018
6018
|
const id = String(row.handle ?? row.id ?? "");
|
|
6019
|
-
const bucket = publicByTuple.get(
|
|
6019
|
+
const bucket = publicByTuple.get(key2);
|
|
6020
6020
|
if (bucket) bucket.ids.push(id);
|
|
6021
|
-
else publicByTuple.set(
|
|
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
|
|
8789
|
-
if (!(
|
|
8790
|
-
delete data.credentials[
|
|
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 (
|
|
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(
|
|
9212
|
-
const origins =
|
|
9213
|
-
const ips =
|
|
9214
|
-
const last =
|
|
9215
|
-
return `${
|
|
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((
|
|
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
|
|
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 '${
|
|
9300
|
-
targetRef: { field:
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
10436
|
-
body.
|
|
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
|
|
10567
|
+
const key2 = result.key;
|
|
10452
10568
|
if (opts.json) {
|
|
10453
|
-
emit(
|
|
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
|
-
|
|
10574
|
+
key2,
|
|
10459
10575
|
false,
|
|
10460
10576
|
[
|
|
10461
|
-
` id: ${
|
|
10462
|
-
` token: ${
|
|
10463
|
-
` origins: ${
|
|
10464
|
-
|
|
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