@revturbine/cli 0.16.3 → 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.
- package/README.md +1 -1
- package/dist/cli.js +113 -37
- 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
|
|
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
|
|
10
|
+
import { Command, CommanderError } from "commander";
|
|
11
11
|
import { z as z29 } from "zod";
|
|
12
12
|
|
|
13
13
|
// src/schema/exported-config.snapshot.mjs
|
|
@@ -1869,7 +1869,14 @@ var ClientContextCapabilitiesSchema = z9.object({
|
|
|
1869
1869
|
);
|
|
1870
1870
|
var ClientContextPlanSchema = z9.object({
|
|
1871
1871
|
/** The customer's current plan handle (`plans.unique_handle`). */
|
|
1872
|
-
handle: z9.string().min(1).optional().meta({ ...Unrestricted6, ...ClientSafe })
|
|
1872
|
+
handle: z9.string().min(1).optional().meta({ ...Unrestricted6, ...ClientSafe }),
|
|
1873
|
+
/**
|
|
1874
|
+
* The plan's display name, resolved server-side from the plan record so
|
|
1875
|
+
* client UI can render it without a Playbook lookup (plan 179 TASK-1 —
|
|
1876
|
+
* Q-2 ruling: `{ handle, name }`). Plan names are already client-visible
|
|
1877
|
+
* in every Playbook; no new exposure.
|
|
1878
|
+
*/
|
|
1879
|
+
name: z9.string().min(1).optional().meta({ ...Unrestricted6, ...ClientSafe })
|
|
1873
1880
|
}).meta(
|
|
1874
1881
|
{ id: "ClientContextPlan", "x-revturbine-schema-persistence": Transient6, "x-revturbine-schema-exposure": Internal5 }
|
|
1875
1882
|
);
|
|
@@ -5940,6 +5947,18 @@ var CATALOG = {
|
|
|
5940
5947
|
message: "These rules for the same entitlement target the same plan and segment \u2014 only one will apply.",
|
|
5941
5948
|
specRef: "config-validation.md \xA75.3 (provisional \u2014 plan 73 Q-1)"
|
|
5942
5949
|
},
|
|
5950
|
+
// limit rule with unset enforcement. Promoted from §5.8 (plan 179 Q-6,
|
|
5951
|
+
// Kent 2026-08-12): unset enforcement silently means "hard-block at the
|
|
5952
|
+
// cap" (evaluator default since plan 34) — an author who wanted degrade or
|
|
5953
|
+
// allow_overage gets a hard stop without being told. Explicitness warn on
|
|
5954
|
+
// the corrected premise (the old "never blocks" mechanism was the SDK
|
|
5955
|
+
// ignoring allowed:false, fixed in sdk 0.2.75).
|
|
5956
|
+
"VAL-PLN-07": {
|
|
5957
|
+
id: "VAL-PLN-07",
|
|
5958
|
+
severity: "warning",
|
|
5959
|
+
message: "This limit rule sets no enforcement \u2014 at the cap it blocks by default. Set enforcement explicitly (hard_block, soft_block, degrade, allow_overage) if that isn't the intent.",
|
|
5960
|
+
specRef: "config-validation.md \xA75.8 (promoted \u2014 plan 179 Q-6)"
|
|
5961
|
+
},
|
|
5943
5962
|
// multiple public variations. Origin: `*_variation.multiple_public`.
|
|
5944
5963
|
"VAL-PLN-06": {
|
|
5945
5964
|
id: "VAL-PLN-06",
|
|
@@ -5976,11 +5995,33 @@ function runSemanticRules(graph) {
|
|
|
5976
5995
|
return [
|
|
5977
5996
|
...checkPlansHaveStripePrice(graph),
|
|
5978
5997
|
...checkRuleOverlaps(graph),
|
|
5998
|
+
...checkLimitRuleEnforcement(graph),
|
|
5979
5999
|
...checkPublicVariationCollisions(graph),
|
|
5980
6000
|
...checkPayloadCtaOverflow(graph),
|
|
5981
6001
|
...checkTrialRuleWidestAudience(graph)
|
|
5982
6002
|
];
|
|
5983
6003
|
}
|
|
6004
|
+
function checkLimitRuleEnforcement(graph) {
|
|
6005
|
+
const findings = [];
|
|
6006
|
+
const CAP_FIELDS = ["limit_value", "allowance_value", "included_count"];
|
|
6007
|
+
for (const rule of graph.entitlement_rules ?? []) {
|
|
6008
|
+
if (rule.enforcement !== void 0 && rule.enforcement !== null) continue;
|
|
6009
|
+
const cappedField = CAP_FIELDS.find((f) => typeof rule[f] === "number" && Number.isFinite(rule[f]));
|
|
6010
|
+
if (!cappedField) continue;
|
|
6011
|
+
const id = String(rule.handle ?? rule.id ?? "");
|
|
6012
|
+
const name = String(rule.name ?? id);
|
|
6013
|
+
findings.push(
|
|
6014
|
+
finding(
|
|
6015
|
+
"VAL-PLN-07",
|
|
6016
|
+
{ object_type: "entitlement_rule", object_id: id, field: "enforcement", studio: "plans-entitlements" },
|
|
6017
|
+
{
|
|
6018
|
+
message: `Limit rule '${name}' sets no enforcement \u2014 at the cap it blocks by default. Set enforcement explicitly (hard_block, soft_block, degrade, allow_overage) if that isn't the intent.`
|
|
6019
|
+
}
|
|
6020
|
+
)
|
|
6021
|
+
);
|
|
6022
|
+
}
|
|
6023
|
+
return findings;
|
|
6024
|
+
}
|
|
5984
6025
|
function surfaceCtas(surface) {
|
|
5985
6026
|
if (!surface || typeof surface !== "object") return 0;
|
|
5986
6027
|
const ctas = surface.ctas;
|
|
@@ -8923,7 +8964,7 @@ function repairDeprecatedFields(config, schema = PlaybookObjectSchema2) {
|
|
|
8923
8964
|
}
|
|
8924
8965
|
|
|
8925
8966
|
// src/schema/version.ts
|
|
8926
|
-
var SCHEMA_VERSION = "0.1.
|
|
8967
|
+
var SCHEMA_VERSION = "0.1.168";
|
|
8927
8968
|
|
|
8928
8969
|
// src/lib/credentials.ts
|
|
8929
8970
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
@@ -9364,6 +9405,34 @@ async function resolveActiveDraft(baseUrl, headers, fetchImpl = fetch) {
|
|
|
9364
9405
|
return { ok: res.ok, status: res.status, draft: res.ok ? json.active ?? null : null };
|
|
9365
9406
|
}
|
|
9366
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
|
+
|
|
9367
9436
|
// src/lib/ingest-keys.ts
|
|
9368
9437
|
async function readJson(res) {
|
|
9369
9438
|
return await res.json().catch(() => ({}));
|
|
@@ -10611,27 +10680,11 @@ program.command("docs").description("Print the canonical documentation URL (and
|
|
|
10611
10680
|
}
|
|
10612
10681
|
}
|
|
10613
10682
|
});
|
|
10614
|
-
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").
|
|
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(
|
|
10615
10684
|
async (opts) => {
|
|
10616
10685
|
const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "download" });
|
|
10617
10686
|
const conn = connect(opts.url, opts.tenantId);
|
|
10618
10687
|
const playbookVersionId = sel.kind === "release" ? sel.id : sel.kind === "draft" ? await requireOpenDraft(conn) : void 0;
|
|
10619
|
-
if (opts.format === "flatbuffer") {
|
|
10620
|
-
const qs = playbookVersionId ? `?playbookVersionId=${encodeURIComponent(playbookVersionId)}` : "";
|
|
10621
|
-
const res = await request(conn, `/api/config/bundle${qs}`);
|
|
10622
|
-
if (!res.ok) httpFail(conn, "bundle download", res.status);
|
|
10623
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
10624
|
-
if (opts.save) {
|
|
10625
|
-
const out2 = path2.resolve(opts.save);
|
|
10626
|
-
mkdirSync2(path2.dirname(out2), { recursive: true });
|
|
10627
|
-
writeFileSync2(out2, bytes);
|
|
10628
|
-
diag(`\u2713 Wrote ${bytes.length} bytes \u2192 ${out2}`);
|
|
10629
|
-
} else {
|
|
10630
|
-
process.stdout.write(`${bytes.toString("base64")}
|
|
10631
|
-
`);
|
|
10632
|
-
}
|
|
10633
|
-
return;
|
|
10634
|
-
}
|
|
10635
10688
|
const config = await downloadConfig(conn, playbookVersionId);
|
|
10636
10689
|
diag(`Downloaded: ${describePlaybookHeader(readPlaybookHeader(config))}`);
|
|
10637
10690
|
const out = `${JSON.stringify(config, null, 2)}
|
|
@@ -10841,7 +10894,9 @@ program.command("preview").description("The open draft's Runtime Impact summary
|
|
|
10841
10894
|
].join("\n");
|
|
10842
10895
|
emit(data, Boolean(opts.json), text);
|
|
10843
10896
|
});
|
|
10844
|
-
program.command("evaluate").description(
|
|
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(
|
|
10845
10900
|
async (opts) => {
|
|
10846
10901
|
const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "evaluate" });
|
|
10847
10902
|
const conn = connect(opts.url, opts.tenantId);
|
|
@@ -10852,27 +10907,48 @@ program.command("evaluate").description("Run placement/entitlement decisions for
|
|
|
10852
10907
|
} catch (err) {
|
|
10853
10908
|
fail(EXIT.VALIDATION, `invalid JSON in ${opts.user}: ${err.message}`);
|
|
10854
10909
|
}
|
|
10855
|
-
const body = { ...ctx };
|
|
10856
|
-
if (opts.planHandle) body.plan_handle = opts.planHandle;
|
|
10857
10910
|
const wantsSlot = Boolean(opts.slot || opts.surfaceType);
|
|
10858
10911
|
if (opts.entitlement && wantsSlot) {
|
|
10859
10912
|
fail(EXIT.USAGE, "pass exactly one of --entitlement or --slot/--surface-type (or neither for the ctx file lists).");
|
|
10860
10913
|
}
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
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.");
|
|
10864
10918
|
}
|
|
10865
|
-
if (
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
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
|
+
);
|
|
10870
10923
|
}
|
|
10871
|
-
|
|
10872
|
-
if (sel.kind === "
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
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
|
+
);
|
|
10876
10952
|
}
|
|
10877
10953
|
);
|
|
10878
10954
|
var generateCmd = program.command("generate").description("Code generation from a config version (see: generate types).");
|
|
@@ -10950,7 +11026,7 @@ var COMMAND_EXAMPLES = {
|
|
|
10950
11026
|
"Examples:",
|
|
10951
11027
|
" revturbine download --live --save ./revturbine.playbook.json The live config \u2192 file",
|
|
10952
11028
|
" revturbine download --draft The open draft (rendered on demand) \u2192 stdout",
|
|
10953
|
-
" revturbine download --release cs_1a2b3c --
|
|
11029
|
+
" revturbine download --release cs_1a2b3c --save ./release.playbook.json"
|
|
10954
11030
|
].join("\n"),
|
|
10955
11031
|
validate: [
|
|
10956
11032
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revturbine/cli",
|
|
3
|
-
"version": "0.
|
|
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
|
},
|