@revturbine/cli 0.13.1 → 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 +145 -23
- 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
|
}
|
|
@@ -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>)
|
|
@@ -10448,25 +10529,55 @@ program.command("evaluate").description("Run placement/entitlement decisions for
|
|
|
10448
10529
|
emit(json, true);
|
|
10449
10530
|
}
|
|
10450
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
|
+
);
|
|
10451
10562
|
var ingestKeys = program.command("ingest-keys").description("Manage public ingest keys (embeddable SDK telemetry credentials) for the tenant.");
|
|
10452
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) => {
|
|
10453
10564
|
const conn = connect(opts.url, opts.tenantId);
|
|
10454
10565
|
const result = await createIngestKey(conn.url, conn.headers, { originAllowlist: opts.origin, ipAllowlist: opts.ip });
|
|
10455
10566
|
if (!result.ok || !result.key) httpFail(conn, "ingest-keys create", result.status, result.error);
|
|
10456
|
-
const
|
|
10567
|
+
const key2 = result.key;
|
|
10457
10568
|
if (opts.json) {
|
|
10458
|
-
emit(
|
|
10569
|
+
emit(key2, true);
|
|
10459
10570
|
return;
|
|
10460
10571
|
}
|
|
10461
10572
|
diag("\u2713 Minted a public ingest key. STORE THE TOKEN NOW \u2014 it is shown once and cannot be retrieved again.");
|
|
10462
10573
|
emit(
|
|
10463
|
-
|
|
10574
|
+
key2,
|
|
10464
10575
|
false,
|
|
10465
10576
|
[
|
|
10466
|
-
` id: ${
|
|
10467
|
-
` token: ${
|
|
10468
|
-
` origins: ${
|
|
10469
|
-
|
|
10577
|
+
` id: ${key2.id}`,
|
|
10578
|
+
` token: ${key2.token}`,
|
|
10579
|
+
` origins: ${key2.originAllowlist.join(", ")}`,
|
|
10580
|
+
key2.ipAllowlist.length ? ` ips: ${key2.ipAllowlist.join(", ")}` : void 0
|
|
10470
10581
|
].filter((line) => Boolean(line)).join("\n")
|
|
10471
10582
|
);
|
|
10472
10583
|
});
|
|
@@ -10508,6 +10619,17 @@ var COMMAND_EXAMPLES = {
|
|
|
10508
10619
|
" revturbine diff --live --draft What the open draft would change"
|
|
10509
10620
|
].join("\n"),
|
|
10510
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"),
|
|
10511
10633
|
upload: ["", "Example:", " revturbine upload ./config.json Stage as the open draft"].join("\n"),
|
|
10512
10634
|
launch: [
|
|
10513
10635
|
"",
|
package/package.json
CHANGED