@revturbine/cli 0.19.2 → 0.19.3
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 +223 -31
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
80
80
|
| `status` | The live Release and the open draft, side by side. |
|
|
81
81
|
| `history` | The Release Version Log, newest first. |
|
|
82
82
|
| `preview` | The open draft's staged changes. |
|
|
83
|
-
| `evaluate` | Run a config version's placement/entitlement decisions locally for a user context. Use `--slot` with optional `--component-type`; deprecated `--surface-type` remains an alias. |
|
|
83
|
+
| `evaluate` | Run a config version's placement/entitlement decisions locally for a user context. Use `--slot` with optional `--component-type`; deprecated `--surface-type` remains an alias. The ctx file carries the whole decision context — `plan_handle`, `traits`, `usage`, `trial` (or `trial_instances` + `now_iso`) — and `--usage` / `--trial-status` / `--now` override those keys, so usage-threshold and trial placements decide here exactly as they do in an app. Output reports `payload_ids`: which authored payload the resolver chose per placement, plus the SDK's `personalization_tokens`. |
|
|
84
84
|
| `generate types` | Generate a TypeScript module of typed Playbook handles from any config version — `Entitlements` (namespaced by type, with the `EntitlementHandle` union for type-safe `can()`/`gate()`/`checkEntitlement()` call sites), plus `Plans`, `Segments`, `SurfaceTemplates`, and `UiPathActionTypes`. Const objects + literal-union types (erasable — no enums). `--out <path>` writes the file; the generated header records the exact command to regenerate it. `--json` for the raw handle map. |
|
|
85
85
|
| `analytics catalog\|templates\|views\|view\|create\|preview\|query` | Work with the hosted Semantic Catalog and canonical analytics-view documents. Create and preview pass the document through unchanged to the same server contract used by the web editor and MCP tools; preview remains subject to the server's query limits. |
|
|
86
86
|
| `ingest-keys create` (alias `mint`) | Mint a tenant-bound public ingest token for browser SDK use. Requires one or more `--origin` values; optional `--ip` restrictions. The full token is returned once. `ingest-keys list` shows ids/previews and `ingest-keys revoke <id>` invalidates one. |
|
package/dist/cli.js
CHANGED
|
@@ -14390,28 +14390,181 @@ function formatAuthentication(hasToken, tokenValid) {
|
|
|
14390
14390
|
return "authentication: verification unavailable \u2014 credentials present; retry when the instance is reachable";
|
|
14391
14391
|
}
|
|
14392
14392
|
|
|
14393
|
+
// src/lib/evaluate-context.ts
|
|
14394
|
+
function isRecord4(value) {
|
|
14395
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14396
|
+
}
|
|
14397
|
+
var DEFAULT_USAGE_UNIT = "unit";
|
|
14398
|
+
function normalizeUsageEntry(handle, value, errors) {
|
|
14399
|
+
if (typeof value === "number") {
|
|
14400
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
14401
|
+
errors.push(`usage.${handle} must be a finite number \u2265 0`);
|
|
14402
|
+
return void 0;
|
|
14403
|
+
}
|
|
14404
|
+
return { entitlement_handle: handle, unit: DEFAULT_USAGE_UNIT, amount: value };
|
|
14405
|
+
}
|
|
14406
|
+
if (!isRecord4(value)) {
|
|
14407
|
+
errors.push(`usage.${handle} must be a number or an object with "amount"`);
|
|
14408
|
+
return void 0;
|
|
14409
|
+
}
|
|
14410
|
+
const amount = value.amount;
|
|
14411
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
14412
|
+
errors.push(`usage.${handle}.amount must be a finite number \u2265 0`);
|
|
14413
|
+
return void 0;
|
|
14414
|
+
}
|
|
14415
|
+
const entry = {
|
|
14416
|
+
entitlement_handle: typeof value.entitlement_handle === "string" ? value.entitlement_handle : handle,
|
|
14417
|
+
unit: typeof value.unit === "string" && value.unit.length > 0 ? value.unit : DEFAULT_USAGE_UNIT,
|
|
14418
|
+
amount
|
|
14419
|
+
};
|
|
14420
|
+
if (value.limit !== void 0) {
|
|
14421
|
+
if (typeof value.limit !== "number" || !Number.isFinite(value.limit) || value.limit < 0) {
|
|
14422
|
+
errors.push(`usage.${handle}.limit must be a finite number \u2265 0`);
|
|
14423
|
+
} else {
|
|
14424
|
+
entry.limit = value.limit;
|
|
14425
|
+
}
|
|
14426
|
+
}
|
|
14427
|
+
if (value.reset_date !== void 0) {
|
|
14428
|
+
if (typeof value.reset_date !== "string") {
|
|
14429
|
+
errors.push(`usage.${handle}.reset_date must be a string`);
|
|
14430
|
+
} else {
|
|
14431
|
+
entry.reset_date = value.reset_date;
|
|
14432
|
+
}
|
|
14433
|
+
}
|
|
14434
|
+
return entry;
|
|
14435
|
+
}
|
|
14436
|
+
function normalizeUsage(value, errors) {
|
|
14437
|
+
if (value === void 0 || value === null) return void 0;
|
|
14438
|
+
if (!isRecord4(value)) {
|
|
14439
|
+
errors.push("usage must be an object keyed by entitlement handle");
|
|
14440
|
+
return void 0;
|
|
14441
|
+
}
|
|
14442
|
+
const out = {};
|
|
14443
|
+
for (const [handle, raw] of Object.entries(value)) {
|
|
14444
|
+
const entry = normalizeUsageEntry(handle, raw, errors);
|
|
14445
|
+
if (entry) out[handle] = entry;
|
|
14446
|
+
}
|
|
14447
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
14448
|
+
}
|
|
14449
|
+
function normalizeTrialStatus(value, errors) {
|
|
14450
|
+
if (value === void 0 || value === null) return void 0;
|
|
14451
|
+
if (!isRecord4(value)) {
|
|
14452
|
+
errors.push("trial status must be an object (the SDK's UserTrialStatus)");
|
|
14453
|
+
return void 0;
|
|
14454
|
+
}
|
|
14455
|
+
if (typeof value.in_trial !== "boolean") {
|
|
14456
|
+
errors.push('trial status must carry a boolean "in_trial"');
|
|
14457
|
+
return void 0;
|
|
14458
|
+
}
|
|
14459
|
+
return value;
|
|
14460
|
+
}
|
|
14461
|
+
function normalizeTrialInstances(value, errors) {
|
|
14462
|
+
if (value === void 0 || value === null) return void 0;
|
|
14463
|
+
if (!Array.isArray(value) || value.some((item) => !isRecord4(item))) {
|
|
14464
|
+
errors.push("trial_instances must be an array of TrialInstance objects");
|
|
14465
|
+
return void 0;
|
|
14466
|
+
}
|
|
14467
|
+
return value.length > 0 ? value : void 0;
|
|
14468
|
+
}
|
|
14469
|
+
function normalizeNowIso(value, source, errors) {
|
|
14470
|
+
if (value === void 0 || value === null) return void 0;
|
|
14471
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
14472
|
+
errors.push(`${source} must be an ISO-8601 timestamp (e.g. 2026-09-21T00:00:00Z)`);
|
|
14473
|
+
return void 0;
|
|
14474
|
+
}
|
|
14475
|
+
return new Date(value).toISOString();
|
|
14476
|
+
}
|
|
14477
|
+
function parseJsonFlag(raw, flag, errors) {
|
|
14478
|
+
try {
|
|
14479
|
+
return JSON.parse(raw);
|
|
14480
|
+
} catch (err) {
|
|
14481
|
+
errors.push(`${flag} must be valid JSON: ${err.message}`);
|
|
14482
|
+
return void 0;
|
|
14483
|
+
}
|
|
14484
|
+
}
|
|
14485
|
+
function parseDecisionContext(input) {
|
|
14486
|
+
const errors = [];
|
|
14487
|
+
const usageSource = input.usage !== void 0 ? parseJsonFlag(input.usage, "--usage", errors) : input.ctx.usage;
|
|
14488
|
+
const usage = normalizeUsage(usageSource, errors);
|
|
14489
|
+
const trialSource = input.trialStatus !== void 0 ? parseJsonFlag(input.trialStatus, "--trial-status", errors) : input.ctx.trial;
|
|
14490
|
+
const trialStatus = normalizeTrialStatus(trialSource, errors);
|
|
14491
|
+
const trialInstances = normalizeTrialInstances(input.ctx.trial_instances, errors);
|
|
14492
|
+
const nowIso = input.now !== void 0 ? normalizeNowIso(input.now, "--now", errors) : normalizeNowIso(input.ctx.now_iso, "now_iso", errors);
|
|
14493
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
14494
|
+
return {
|
|
14495
|
+
ok: true,
|
|
14496
|
+
context: {
|
|
14497
|
+
...usage ? { usage } : {},
|
|
14498
|
+
...trialStatus ? { trialStatus } : {},
|
|
14499
|
+
...trialInstances ? { trialInstances } : {},
|
|
14500
|
+
...nowIso ? { nowIso } : {}
|
|
14501
|
+
}
|
|
14502
|
+
};
|
|
14503
|
+
}
|
|
14504
|
+
|
|
14393
14505
|
// src/lib/evaluate-local.ts
|
|
14394
14506
|
import { initRevTurbine } from "@revturbine/sdk/headless";
|
|
14395
14507
|
function resolvePlacementComponentType(input) {
|
|
14396
14508
|
return input.componentType ?? input.surfaceType;
|
|
14397
14509
|
}
|
|
14510
|
+
function payloadIdOf(decision) {
|
|
14511
|
+
if (typeof decision !== "object" || decision === null) return null;
|
|
14512
|
+
const output = decision.output;
|
|
14513
|
+
if (typeof output !== "object" || output === null) return null;
|
|
14514
|
+
const id = output.output_id;
|
|
14515
|
+
return typeof id === "string" ? id : null;
|
|
14516
|
+
}
|
|
14517
|
+
async function completeUsageLimits(session, usage) {
|
|
14518
|
+
let completed = false;
|
|
14519
|
+
const filled = {};
|
|
14520
|
+
for (const [handle, entry] of Object.entries(usage)) {
|
|
14521
|
+
if (entry.limit !== void 0) {
|
|
14522
|
+
filled[handle] = entry;
|
|
14523
|
+
continue;
|
|
14524
|
+
}
|
|
14525
|
+
const result = await session.checkEntitlement(entry.entitlement_handle);
|
|
14526
|
+
if (typeof result?.limit === "number" && Number.isFinite(result.limit)) {
|
|
14527
|
+
filled[handle] = { ...entry, limit: result.limit };
|
|
14528
|
+
completed = true;
|
|
14529
|
+
} else {
|
|
14530
|
+
filled[handle] = entry;
|
|
14531
|
+
}
|
|
14532
|
+
}
|
|
14533
|
+
if (completed) session.setUserContext({ usage: filled });
|
|
14534
|
+
}
|
|
14398
14535
|
async function evaluateLocal(config, input) {
|
|
14399
14536
|
const user = { id: input.userId };
|
|
14400
14537
|
if (input.planHandle) user.plan_handle = input.planHandle;
|
|
14401
14538
|
if (input.traits && Object.keys(input.traits).length > 0) user.custom = input.traits;
|
|
14539
|
+
if (input.usage) user.usage = input.usage;
|
|
14402
14540
|
const session = await initRevTurbine({
|
|
14403
14541
|
user,
|
|
14404
|
-
localRuntime: {
|
|
14542
|
+
localRuntime: {
|
|
14543
|
+
playbook: config,
|
|
14544
|
+
...input.trialStatus ? { initialData: { trialStatus: input.trialStatus } } : {}
|
|
14545
|
+
}
|
|
14405
14546
|
});
|
|
14547
|
+
if (input.trialInstances) {
|
|
14548
|
+
await session.sdk.setTrialInstances(
|
|
14549
|
+
input.trialInstances,
|
|
14550
|
+
input.nowIso ? { nowIso: input.nowIso } : void 0
|
|
14551
|
+
);
|
|
14552
|
+
}
|
|
14553
|
+
const trialAsked = Boolean(input.trialInstances || input.trialStatus);
|
|
14554
|
+
const trialStatus = trialAsked ? await session.getTrialStatus() : void 0;
|
|
14555
|
+
if (input.usage) await completeUsageLimits(session, input.usage);
|
|
14406
14556
|
const entitlements = {};
|
|
14407
14557
|
for (const handle of input.entitlementHandles ?? []) {
|
|
14408
14558
|
entitlements[handle] = await session.checkEntitlement(handle);
|
|
14409
14559
|
}
|
|
14410
14560
|
const decisions = [];
|
|
14561
|
+
const payloadIds = {};
|
|
14411
14562
|
for (const placementId of input.placementIds ?? []) {
|
|
14412
14563
|
const controller = session.placement({ placement: { name: placementId } });
|
|
14413
14564
|
await controller.load();
|
|
14414
|
-
|
|
14565
|
+
const decision = controller.state.decision;
|
|
14566
|
+
decisions.push(decision);
|
|
14567
|
+
payloadIds[placementId] = payloadIdOf(decision);
|
|
14415
14568
|
}
|
|
14416
14569
|
const componentType = input.slot ? resolvePlacementComponentType(input.slot) : void 0;
|
|
14417
14570
|
const placement = input.slot && (input.slot.slotId || componentType) ? await session.getPlacement({
|
|
@@ -14419,7 +14572,14 @@ async function evaluateLocal(config, input) {
|
|
|
14419
14572
|
...componentType ? { componentType } : {},
|
|
14420
14573
|
...input.planHandle ? { planHandle: input.planHandle } : {}
|
|
14421
14574
|
}) : null;
|
|
14422
|
-
return {
|
|
14575
|
+
return {
|
|
14576
|
+
decisions,
|
|
14577
|
+
payloadIds,
|
|
14578
|
+
entitlements,
|
|
14579
|
+
placement,
|
|
14580
|
+
personalizationTokens: session.sdk.getPersonalizationTokens(),
|
|
14581
|
+
...trialStatus !== void 0 ? { trialStatus } : {}
|
|
14582
|
+
};
|
|
14423
14583
|
}
|
|
14424
14584
|
|
|
14425
14585
|
// src/lib/ingest-keys.ts
|
|
@@ -14665,14 +14825,14 @@ function schemaForConfig(raw) {
|
|
|
14665
14825
|
var repairDeprecatedFields2 = repairDeprecatedFields;
|
|
14666
14826
|
var LEGACY_HEADER_ALIASES = /* @__PURE__ */ new Set(["version", "change_set_id"]);
|
|
14667
14827
|
var UNKNOWN_FIELD_CODE = "cli/unknown-field";
|
|
14668
|
-
function
|
|
14828
|
+
function isRecord5(value) {
|
|
14669
14829
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14670
14830
|
}
|
|
14671
14831
|
function unknownTopLevelKeys(raw) {
|
|
14672
|
-
if (!
|
|
14832
|
+
if (!isRecord5(raw)) return [];
|
|
14673
14833
|
const { schema } = schemaForConfig(raw);
|
|
14674
14834
|
const parsed = schema.safeParse(raw);
|
|
14675
|
-
if (!parsed.success || !
|
|
14835
|
+
if (!parsed.success || !isRecord5(parsed.data)) return [];
|
|
14676
14836
|
const recognized = new Set(Object.keys(parsed.data));
|
|
14677
14837
|
return Object.keys(raw).filter((key2) => !recognized.has(key2) && !LEGACY_HEADER_ALIASES.has(key2));
|
|
14678
14838
|
}
|
|
@@ -14916,17 +15076,17 @@ function finalOutputLines(params) {
|
|
|
14916
15076
|
|
|
14917
15077
|
// src/lib/handles-codegen.ts
|
|
14918
15078
|
var UNTYPED_GROUP = "untyped";
|
|
14919
|
-
function
|
|
15079
|
+
function isRecord6(value) {
|
|
14920
15080
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14921
15081
|
}
|
|
14922
15082
|
function nonEmptyString(value) {
|
|
14923
15083
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
14924
15084
|
}
|
|
14925
15085
|
function extractEntitlementHandles(config) {
|
|
14926
|
-
if (!
|
|
15086
|
+
if (!isRecord6(config) || !Array.isArray(config.entitlements)) return {};
|
|
14927
15087
|
const groups = /* @__PURE__ */ new Map();
|
|
14928
15088
|
for (const entry of config.entitlements) {
|
|
14929
|
-
if (!
|
|
15089
|
+
if (!isRecord6(entry)) continue;
|
|
14930
15090
|
const handle = nonEmptyString(entry.unique_handle) ?? nonEmptyString(entry.handle);
|
|
14931
15091
|
if (!handle) continue;
|
|
14932
15092
|
const type = nonEmptyString(entry.type) ?? UNTYPED_GROUP;
|
|
@@ -14996,36 +15156,36 @@ function collectStrings(values) {
|
|
|
14996
15156
|
return [...out].sort();
|
|
14997
15157
|
}
|
|
14998
15158
|
function extractPlanHandles(config) {
|
|
14999
|
-
if (!
|
|
15159
|
+
if (!isRecord6(config) || !Array.isArray(config.plans)) return [];
|
|
15000
15160
|
return collectStrings(
|
|
15001
15161
|
config.plans.map(
|
|
15002
|
-
(p) =>
|
|
15162
|
+
(p) => isRecord6(p) ? nonEmptyString(p.unique_handle) ?? nonEmptyString(p.handle) ?? p.id : null
|
|
15003
15163
|
)
|
|
15004
15164
|
);
|
|
15005
15165
|
}
|
|
15006
15166
|
function extractSegmentHandles(config) {
|
|
15007
|
-
if (!
|
|
15167
|
+
if (!isRecord6(config) || !Array.isArray(config.segments)) return [];
|
|
15008
15168
|
return collectStrings(
|
|
15009
|
-
config.segments.map((s) =>
|
|
15169
|
+
config.segments.map((s) => isRecord6(s) ? nonEmptyString(s.handle) ?? s.id : null)
|
|
15010
15170
|
);
|
|
15011
15171
|
}
|
|
15012
15172
|
function extractSurfaceTemplateIds(config) {
|
|
15013
|
-
if (!
|
|
15173
|
+
if (!isRecord6(config)) return [];
|
|
15014
15174
|
const ids = [];
|
|
15015
15175
|
if (Array.isArray(config.surface_templates)) {
|
|
15016
15176
|
for (const t of config.surface_templates) {
|
|
15017
|
-
if (
|
|
15177
|
+
if (isRecord6(t)) ids.push(t.id ?? t.template_id);
|
|
15018
15178
|
}
|
|
15019
15179
|
}
|
|
15020
15180
|
const surfacesOf = (payload) => {
|
|
15021
|
-
if (!
|
|
15181
|
+
if (!isRecord6(payload) || !Array.isArray(payload.surfaces)) return;
|
|
15022
15182
|
for (const surface of payload.surfaces) {
|
|
15023
|
-
if (
|
|
15183
|
+
if (isRecord6(surface)) ids.push(surface.template_id);
|
|
15024
15184
|
}
|
|
15025
15185
|
};
|
|
15026
15186
|
if (Array.isArray(config.placements)) {
|
|
15027
15187
|
for (const placement of config.placements) {
|
|
15028
|
-
if (!
|
|
15188
|
+
if (!isRecord6(placement) || !Array.isArray(placement.payloads)) continue;
|
|
15029
15189
|
for (const payload of placement.payloads) surfacesOf(payload);
|
|
15030
15190
|
}
|
|
15031
15191
|
}
|
|
@@ -15035,9 +15195,9 @@ function extractSurfaceTemplateIds(config) {
|
|
|
15035
15195
|
return collectStrings(ids);
|
|
15036
15196
|
}
|
|
15037
15197
|
function extractUiPathActionTypes(config) {
|
|
15038
|
-
if (!
|
|
15198
|
+
if (!isRecord6(config) || !Array.isArray(config.content_ui_paths)) return [];
|
|
15039
15199
|
return collectStrings(
|
|
15040
|
-
config.content_ui_paths.map((p) =>
|
|
15200
|
+
config.content_ui_paths.map((p) => isRecord6(p) ? p.action_type : null)
|
|
15041
15201
|
);
|
|
15042
15202
|
}
|
|
15043
15203
|
function renderFlatFamily(lines, opts) {
|
|
@@ -15139,19 +15299,19 @@ function fail(cls, message) {
|
|
|
15139
15299
|
}
|
|
15140
15300
|
|
|
15141
15301
|
// src/lib/pin-drift.ts
|
|
15142
|
-
function
|
|
15302
|
+
function isRecord7(value) {
|
|
15143
15303
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15144
15304
|
}
|
|
15145
15305
|
function pinOf(pkg, name) {
|
|
15146
15306
|
for (const field of ["devDependencies", "dependencies"]) {
|
|
15147
15307
|
const deps = pkg[field];
|
|
15148
|
-
if (
|
|
15308
|
+
if (isRecord7(deps) && typeof deps[name] === "string") return deps[name];
|
|
15149
15309
|
}
|
|
15150
15310
|
return void 0;
|
|
15151
15311
|
}
|
|
15152
15312
|
var EXACT = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
15153
15313
|
function checkPinDrift(pkg) {
|
|
15154
|
-
if (!
|
|
15314
|
+
if (!isRecord7(pkg)) return [];
|
|
15155
15315
|
const warnings = [];
|
|
15156
15316
|
const cliPin = pinOf(pkg, "@revturbine/cli");
|
|
15157
15317
|
if (cliPin !== void 0 && !EXACT.test(cliPin)) {
|
|
@@ -16223,8 +16383,8 @@ program.command("preview").description("The open draft's Runtime Impact summary
|
|
|
16223
16383
|
emit(data, Boolean(opts.json), text);
|
|
16224
16384
|
});
|
|
16225
16385
|
program.command("evaluate").description(
|
|
16226
|
-
"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>."
|
|
16227
|
-
).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("--component-type <type>", "Disambiguate a slot that can render more than one component, or resolve by component type alone").option("--surface-type <type>", "Deprecated alias for --component-type").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,
|
|
16386
|
+
"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). The ctx file carries the full decision context \u2014 plan, traits, usage, trial state and a clock pin \u2014 so usage-threshold and trial placements decide here as they do in an app. Requires --live, --draft, or --release <id>."
|
|
16387
|
+
).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("--component-type <type>", "Disambiguate a slot that can render more than one component, or resolve by component type alone").option("--surface-type <type>", "Deprecated alias for --component-type").option("--plan-handle <handle>", "Evaluate as if the user were on this plan (overrides the ctx file)").option("--usage <json>", `Usage entries keyed by entitlement handle, e.g. '{"api_calls":{"amount":95}}' (overrides the ctx file) \u2014 required to decide usage/credit-threshold placements`).option("--trial-status <json>", `Trial status, e.g. '{"in_trial":true,"trial_limit_type":"time","days_remaining":3}' (overrides the ctx file) \u2014 required to decide trial placements`).option("--now <iso>", "Pin the clock (ISO-8601) for trial derivation from the ctx file's trial_instances (overrides ctx now_iso)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--user <file>", "JSON file: { user_id, plan_handle?, traits?, usage?, trial?, trial_instances?, now_iso? }").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
|
|
16228
16388
|
async (opts) => {
|
|
16229
16389
|
const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "evaluate" });
|
|
16230
16390
|
const conn = connect(opts.url, opts.tenantId);
|
|
@@ -16242,8 +16402,18 @@ program.command("evaluate").description(
|
|
|
16242
16402
|
}
|
|
16243
16403
|
const userId = typeof ctx.user_id === "string" && ctx.user_id.length > 0 ? ctx.user_id : void 0;
|
|
16244
16404
|
if (!userId) fail(EXIT.VALIDATION, `user file must carry a non-empty string "user_id" (${opts.user})`);
|
|
16245
|
-
|
|
16246
|
-
|
|
16405
|
+
const parsed = parseDecisionContext({
|
|
16406
|
+
ctx,
|
|
16407
|
+
usage: opts.usage,
|
|
16408
|
+
trialStatus: opts.trialStatus,
|
|
16409
|
+
now: opts.now
|
|
16410
|
+
});
|
|
16411
|
+
if (!parsed.ok) fail(EXIT.VALIDATION, `invalid decision context in ${opts.user}: ${parsed.errors.join("; ")}`);
|
|
16412
|
+
const decisionContext = parsed.context;
|
|
16413
|
+
if (decisionContext.nowIso && !decisionContext.trialInstances) {
|
|
16414
|
+
diag(
|
|
16415
|
+
'NOTE: the pinned clock only moves trial derivation from trial_instances; an explicit "trial" status is already fixed in time.'
|
|
16416
|
+
);
|
|
16247
16417
|
}
|
|
16248
16418
|
if (typeof ctx.customer_id === "string") {
|
|
16249
16419
|
diag(
|
|
@@ -16262,6 +16432,7 @@ program.command("evaluate").description(
|
|
|
16262
16432
|
userId,
|
|
16263
16433
|
planHandle,
|
|
16264
16434
|
traits,
|
|
16435
|
+
...decisionContext,
|
|
16265
16436
|
entitlementHandles: opts.entitlement ? [opts.entitlement] : bulk ? asStringArray2(ctx.entitlement_handles) : [],
|
|
16266
16437
|
placementIds: bulk ? asStringArray2(ctx.placement_ids) : [],
|
|
16267
16438
|
slot: wantsSlot ? { slotId: opts.slot, componentType } : void 0
|
|
@@ -16271,10 +16442,18 @@ program.command("evaluate").description(
|
|
|
16271
16442
|
tenant_id: conn.tenantId,
|
|
16272
16443
|
user_id: userId,
|
|
16273
16444
|
...playbookVersionId ? { playbook_version_id: playbookVersionId } : {},
|
|
16274
|
-
|
|
16445
|
+
// The pinned clock when one was given, so the stamp matches the
|
|
16446
|
+
// instant the decisions were made against.
|
|
16447
|
+
evaluated_at: decisionContext.nowIso ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
16275
16448
|
// Provenance: decided in-process by the SDK engine, not by a server.
|
|
16276
16449
|
evaluation: "local",
|
|
16277
|
-
|
|
16450
|
+
decisions: result.decisions,
|
|
16451
|
+
// Which authored payload the resolver chose, per requested placement.
|
|
16452
|
+
payload_ids: result.payloadIds,
|
|
16453
|
+
entitlements: result.entitlements,
|
|
16454
|
+
placement: result.placement,
|
|
16455
|
+
personalization_tokens: result.personalizationTokens,
|
|
16456
|
+
...result.trialStatus !== void 0 ? { trial_status: result.trialStatus } : {}
|
|
16278
16457
|
},
|
|
16279
16458
|
true
|
|
16280
16459
|
);
|
|
@@ -16546,10 +16725,23 @@ var COMMAND_EXAMPLES = {
|
|
|
16546
16725
|
].join("\n"),
|
|
16547
16726
|
evaluate: [
|
|
16548
16727
|
"",
|
|
16549
|
-
"
|
|
16728
|
+
"Examples:",
|
|
16550
16729
|
" revturbine evaluate --live --user ./ctx.json --entitlement seats",
|
|
16551
16730
|
" revturbine evaluate --draft --user ./ctx.json --slot upgrade_banner --component-type banner --plan-handle pro",
|
|
16552
|
-
|
|
16731
|
+
` revturbine evaluate --live --user ./ctx.json --usage '{"api_calls":{"amount":95}}'`,
|
|
16732
|
+
` revturbine evaluate --live --user ./ctx.json --trial-status '{"in_trial":true,"trial_limit_type":"time","days_remaining":3}'`,
|
|
16733
|
+
" revturbine evaluate --live --user ./ctx.json --now 2026-09-21T00:00:00Z",
|
|
16734
|
+
"",
|
|
16735
|
+
"ctx.json \u2014 the whole decision context:",
|
|
16736
|
+
' { "user_id": "u1", "plan_handle": "pro", "traits": { "tier": "smb" },',
|
|
16737
|
+
' "usage": { "api_calls": { "amount": 95, "limit": 100 } },',
|
|
16738
|
+
' "trial": { "in_trial": true, "trial_limit_type": "time", "days_remaining": 3 },',
|
|
16739
|
+
' "trial_instances": [ ... ], "now_iso": "2026-09-21T00:00:00Z",',
|
|
16740
|
+
' "entitlement_handles": ["seats"], "placement_ids": ["pl_usage_warn"] }',
|
|
16741
|
+
"",
|
|
16742
|
+
` usage \u2014 omit "limit" and it is taken from the Playbook's entitlement rule.`,
|
|
16743
|
+
" trial \u2014 an explicit status; trial_instances + now_iso derive one at that clock.",
|
|
16744
|
+
" Output carries payload_ids: which authored payload won, per placement."
|
|
16553
16745
|
].join("\n")
|
|
16554
16746
|
};
|
|
16555
16747
|
for (const [name, text] of Object.entries(COMMAND_EXAMPLES)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revturbine/cli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
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": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"verify": "npm run typecheck && npm run lint && npm run check:schema && npm run test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@revturbine/sdk": "
|
|
33
|
+
"@revturbine/sdk": ">=0.10.9 <1",
|
|
34
34
|
"commander": "^15.0.0",
|
|
35
35
|
"semver": "^7.8.5",
|
|
36
36
|
"zod": "4.4.3"
|