@revturbine/cli 0.4.0 → 0.5.1
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 +373 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
63
63
|
| `upload` | Stage a Config File as the open draft. |
|
|
64
64
|
| `launch` | Take a config live: validate (launch gate) → submit → approve → deploy. `launch <file>` or `launch --draft`. |
|
|
65
65
|
| `discard` | Archive the open draft (`--yes`). |
|
|
66
|
-
| `
|
|
66
|
+
| `restore` | Stage a draft that restores a past release from its frozen snapshot; `--launch` takes it live. Halts if a draft is open. |
|
|
67
67
|
| `status` | The live Release and the open draft, side by side. |
|
|
68
68
|
| `history` | The Release Version Log, newest first. |
|
|
69
69
|
| `preview` | The open draft's staged changes. |
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from "module";
|
|
|
7
7
|
import { createInterface } from "readline/promises";
|
|
8
8
|
import { spawn as spawn2 } from "child_process";
|
|
9
9
|
import { Command, CommanderError, Option } from "commander";
|
|
10
|
-
import { z as
|
|
10
|
+
import { z as z29 } from "zod";
|
|
11
11
|
|
|
12
12
|
// src/schema/exported-config.snapshot.mjs
|
|
13
13
|
import { z as z2 } from "zod";
|
|
@@ -5223,6 +5223,344 @@ var AuthSsoProviderSchema = IdField.extend({
|
|
|
5223
5223
|
domain: z26.string().min(1).meta(Unrestricted23)
|
|
5224
5224
|
}).meta({ id: "AuthSsoProvider", "x-revturbine-schema-persistence": Persisted23, "x-revturbine-schema-exposure": Internal19 });
|
|
5225
5225
|
|
|
5226
|
+
// src/schema/validators.snapshot.mjs
|
|
5227
|
+
import { z as z27 } from "zod";
|
|
5228
|
+
import { z as z28 } from "zod";
|
|
5229
|
+
var SeveritySchema2 = z27.enum([
|
|
5230
|
+
"error_draft",
|
|
5231
|
+
"error_launch",
|
|
5232
|
+
"warning",
|
|
5233
|
+
"ai_check"
|
|
5234
|
+
]);
|
|
5235
|
+
var CallSiteSchema = z27.enum([
|
|
5236
|
+
"studio",
|
|
5237
|
+
// inline studio editing / modal commit
|
|
5238
|
+
"publish",
|
|
5239
|
+
// the launch gate (literal kept as 'publish' — see NOTE above)
|
|
5240
|
+
"ingestion",
|
|
5241
|
+
// external ingestion — CLI / MCP / agents
|
|
5242
|
+
"compile"
|
|
5243
|
+
// compile / activate backstop
|
|
5244
|
+
]);
|
|
5245
|
+
var TargetRefSchema = z27.object({
|
|
5246
|
+
object_type: z27.string().optional(),
|
|
5247
|
+
object_id: z27.string().optional(),
|
|
5248
|
+
field: z27.string().optional(),
|
|
5249
|
+
/** Studio key the UI resolves a deep-link against. */
|
|
5250
|
+
studio: z27.string().optional(),
|
|
5251
|
+
/** A free-form deep-link target the UI can resolve into a URL. */
|
|
5252
|
+
href: z27.string().optional(),
|
|
5253
|
+
/** Zod issue path, for structural-tier findings. */
|
|
5254
|
+
path: z27.array(z27.union([z27.string(), z27.number()])).optional()
|
|
5255
|
+
});
|
|
5256
|
+
var ValidationFindingSchema = z27.object({
|
|
5257
|
+
/** Stable rule ID (e.g. `VAL-PLN-01`); for schema findings, the Zod issue code. */
|
|
5258
|
+
code: z27.string(),
|
|
5259
|
+
severity: SeveritySchema2,
|
|
5260
|
+
targetRef: TargetRefSchema,
|
|
5261
|
+
/** Single user-facing message — same string at the gate, modal, and CLI. */
|
|
5262
|
+
message: z27.string(),
|
|
5263
|
+
/** Pointer to the originating spec rule. Beta inspection aid — remove post-beta. */
|
|
5264
|
+
specRef: z27.string().optional(),
|
|
5265
|
+
/** Optional extended context (preserved from the old `ValidationIssue.detail`). */
|
|
5266
|
+
detail: z27.string().optional(),
|
|
5267
|
+
/**
|
|
5268
|
+
* Set by `evaluate` when a `focus` is supplied and this finding touches the
|
|
5269
|
+
* focused object — surfaces use it to spotlight inline. Never narrows what is
|
|
5270
|
+
* checked (spec §3.1).
|
|
5271
|
+
*/
|
|
5272
|
+
spotlight: z27.boolean().optional()
|
|
5273
|
+
});
|
|
5274
|
+
var CATALOG = {
|
|
5275
|
+
// no-Stripe-price. Interim `warning` (does not block): a plan price that has
|
|
5276
|
+
// all its billing info but is entered statically rather than synced from
|
|
5277
|
+
// Stripe is valid to launch — it should warn, not block. The stricter
|
|
5278
|
+
// three-way model (block if billing info is INCOMPLETE; warn if complete-but-
|
|
5279
|
+
// static; pass if complete-and-Stripe-connected) is a tracked follow-up
|
|
5280
|
+
// (static-pricing plan). Spec §5.3's blanket error_launch is superseded by
|
|
5281
|
+
// that follow-up; see the plan.
|
|
5282
|
+
"VAL-PLN-01": {
|
|
5283
|
+
id: "VAL-PLN-01",
|
|
5284
|
+
severity: "warning",
|
|
5285
|
+
message: "Plan '{plan}' has no Stripe price linked.",
|
|
5286
|
+
specRef: "optimization-studio-ui.md \xA73.1; plans-entitlements-studio-ui.md \xA72.2"
|
|
5287
|
+
},
|
|
5288
|
+
// entitlement-rule overlap. Origin: the `rule.overlap` check (plan 40).
|
|
5289
|
+
"VAL-PLN-05": {
|
|
5290
|
+
id: "VAL-PLN-05",
|
|
5291
|
+
severity: "warning",
|
|
5292
|
+
message: "These entitlement rules target the same plan and segment \u2014 only one will apply.",
|
|
5293
|
+
specRef: "config-validation.md \xA75.3 (provisional \u2014 plan 73 Q-1)"
|
|
5294
|
+
},
|
|
5295
|
+
// multiple public variations. Origin: `*_variation.multiple_public`.
|
|
5296
|
+
"VAL-PLN-06": {
|
|
5297
|
+
id: "VAL-PLN-06",
|
|
5298
|
+
severity: "error_draft",
|
|
5299
|
+
message: "Only one public variation is allowed per billing period and segment.",
|
|
5300
|
+
specRef: "config-validation.md \xA75.3 (provisional \u2014 plan 73 Q-1)"
|
|
5301
|
+
}
|
|
5302
|
+
};
|
|
5303
|
+
function getCatalogEntry(id) {
|
|
5304
|
+
return CATALOG[id];
|
|
5305
|
+
}
|
|
5306
|
+
function runSemanticRules(graph) {
|
|
5307
|
+
return [
|
|
5308
|
+
...checkPlansHaveStripePrice(graph),
|
|
5309
|
+
...checkRuleOverlaps(graph),
|
|
5310
|
+
...checkPublicVariationCollisions(graph)
|
|
5311
|
+
];
|
|
5312
|
+
}
|
|
5313
|
+
function finding(catalogId, target, opts = {}) {
|
|
5314
|
+
const entry = getCatalogEntry(catalogId);
|
|
5315
|
+
const severity = entry?.severity ?? "warning";
|
|
5316
|
+
return {
|
|
5317
|
+
code: catalogId,
|
|
5318
|
+
severity,
|
|
5319
|
+
targetRef: target,
|
|
5320
|
+
message: opts.message ?? entry?.message ?? catalogId,
|
|
5321
|
+
...entry?.specRef ? { specRef: entry.specRef } : {},
|
|
5322
|
+
...opts.detail ? { detail: opts.detail } : {}
|
|
5323
|
+
};
|
|
5324
|
+
}
|
|
5325
|
+
function checkPlansHaveStripePrice(graph) {
|
|
5326
|
+
const plans = graph.plans;
|
|
5327
|
+
if (!plans?.length) return [];
|
|
5328
|
+
const plansWithVariations = /* @__PURE__ */ new Set();
|
|
5329
|
+
const pricedPlanIds = /* @__PURE__ */ new Set();
|
|
5330
|
+
for (const v of graph.plan_variations ?? []) {
|
|
5331
|
+
const planId = String(v.plan_id ?? "");
|
|
5332
|
+
if (!planId) continue;
|
|
5333
|
+
plansWithVariations.add(planId);
|
|
5334
|
+
const priceId = v.stripe_price_id;
|
|
5335
|
+
if (typeof priceId === "string" && priceId.length > 0) pricedPlanIds.add(planId);
|
|
5336
|
+
}
|
|
5337
|
+
const findings = [];
|
|
5338
|
+
for (const row of plans) {
|
|
5339
|
+
const planKeys = [String(row.handle ?? ""), String(row.id ?? "")].filter(Boolean);
|
|
5340
|
+
const hasVariations = planKeys.some((k) => plansWithVariations.has(k));
|
|
5341
|
+
if (!hasVariations) continue;
|
|
5342
|
+
const hasStripePrice = planKeys.some((k) => pricedPlanIds.has(k));
|
|
5343
|
+
if (hasStripePrice) continue;
|
|
5344
|
+
const id = String(row.handle ?? row.id ?? "");
|
|
5345
|
+
const name = String(row.name ?? id);
|
|
5346
|
+
findings.push(
|
|
5347
|
+
finding(
|
|
5348
|
+
"VAL-PLN-01",
|
|
5349
|
+
{
|
|
5350
|
+
object_type: "plan",
|
|
5351
|
+
object_id: id,
|
|
5352
|
+
field: "plan_variations",
|
|
5353
|
+
studio: "plans-entitlements"
|
|
5354
|
+
},
|
|
5355
|
+
{
|
|
5356
|
+
message: `Plan '${name}' has no Stripe price linked.`,
|
|
5357
|
+
detail: "Link a Plan Variation to a Stripe Price before activating the plan for paid customers."
|
|
5358
|
+
}
|
|
5359
|
+
)
|
|
5360
|
+
);
|
|
5361
|
+
}
|
|
5362
|
+
return findings;
|
|
5363
|
+
}
|
|
5364
|
+
function checkRuleOverlaps(graph) {
|
|
5365
|
+
const rules = graph.entitlement_rules;
|
|
5366
|
+
if (!rules?.length) return [];
|
|
5367
|
+
const segmentDimensions = /* @__PURE__ */ new Map();
|
|
5368
|
+
for (const seg of graph.segments ?? []) {
|
|
5369
|
+
const segId = String(seg.handle ?? seg.id ?? "");
|
|
5370
|
+
const dim = typeof seg.dimension_id === "string" ? seg.dimension_id : "";
|
|
5371
|
+
if (segId) segmentDimensions.set(segId, dim);
|
|
5372
|
+
}
|
|
5373
|
+
const sigs = [];
|
|
5374
|
+
for (const rule of rules) {
|
|
5375
|
+
const id = String(rule.handle ?? rule.id ?? "");
|
|
5376
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
5377
|
+
if (Array.isArray(rule.targets)) {
|
|
5378
|
+
for (const t of rule.targets) {
|
|
5379
|
+
if (t && typeof t === "object") {
|
|
5380
|
+
const obj = t;
|
|
5381
|
+
if (typeof obj.id === "string") {
|
|
5382
|
+
const kind = typeof obj.kind === "string" ? obj.kind : "plan";
|
|
5383
|
+
targetIds.add(`${kind}:${obj.id}`);
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
} else if (Array.isArray(rule.plan_ids)) {
|
|
5388
|
+
for (const pid of rule.plan_ids) {
|
|
5389
|
+
if (typeof pid === "string") targetIds.add(`plan:${pid}`);
|
|
5390
|
+
}
|
|
5391
|
+
} else if (rule.plan_id !== void 0 && rule.plan_id !== null) {
|
|
5392
|
+
targetIds.add(`plan:${String(rule.plan_id)}`);
|
|
5393
|
+
}
|
|
5394
|
+
const segmentIds = Array.isArray(rule.segment_ids) ? rule.segment_ids.filter((s) => typeof s === "string") : [];
|
|
5395
|
+
const segmentPairs = /* @__PURE__ */ new Set();
|
|
5396
|
+
for (const segId of segmentIds) {
|
|
5397
|
+
const dim = segmentDimensions.get(segId) ?? "";
|
|
5398
|
+
segmentPairs.add(`${dim}::${segId}`);
|
|
5399
|
+
}
|
|
5400
|
+
sigs.push({
|
|
5401
|
+
rule,
|
|
5402
|
+
id,
|
|
5403
|
+
targetIds,
|
|
5404
|
+
segmentPairs,
|
|
5405
|
+
matchesAllSegments: segmentIds.length === 0
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
const overlapping = /* @__PURE__ */ new Set();
|
|
5409
|
+
for (let i = 0; i < sigs.length; i++) {
|
|
5410
|
+
for (let j = i + 1; j < sigs.length; j++) {
|
|
5411
|
+
const a = sigs[i];
|
|
5412
|
+
const b = sigs[j];
|
|
5413
|
+
let sharedTarget = false;
|
|
5414
|
+
for (const t of a.targetIds) {
|
|
5415
|
+
if (b.targetIds.has(t)) {
|
|
5416
|
+
sharedTarget = true;
|
|
5417
|
+
break;
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
if (!sharedTarget) continue;
|
|
5421
|
+
let overlaps = false;
|
|
5422
|
+
if (a.matchesAllSegments || b.matchesAllSegments) {
|
|
5423
|
+
overlaps = true;
|
|
5424
|
+
} else {
|
|
5425
|
+
for (const pair of a.segmentPairs) {
|
|
5426
|
+
if (b.segmentPairs.has(pair)) {
|
|
5427
|
+
overlaps = true;
|
|
5428
|
+
break;
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
if (overlaps) {
|
|
5433
|
+
overlapping.add(i);
|
|
5434
|
+
overlapping.add(j);
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
const findings = [];
|
|
5439
|
+
for (const idx of overlapping) {
|
|
5440
|
+
const sig = sigs[idx];
|
|
5441
|
+
const name = String(sig.rule.name ?? sig.id);
|
|
5442
|
+
findings.push(
|
|
5443
|
+
finding(
|
|
5444
|
+
"VAL-PLN-05",
|
|
5445
|
+
{
|
|
5446
|
+
object_type: "entitlement_rule",
|
|
5447
|
+
object_id: sig.id,
|
|
5448
|
+
studio: "plans-entitlements"
|
|
5449
|
+
},
|
|
5450
|
+
{
|
|
5451
|
+
message: `Entitlement rule '${name}' overlaps another rule on a shared target and segment \u2014 only one will apply.`,
|
|
5452
|
+
detail: "Where rules overlap, the most permissive value applies (plans-entitlements-studio-ui.md \xA72.3.2). Adjust the segment selection if this is unintended."
|
|
5453
|
+
}
|
|
5454
|
+
)
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
5457
|
+
return findings;
|
|
5458
|
+
}
|
|
5459
|
+
function checkPublicVariationCollisions(graph) {
|
|
5460
|
+
return [
|
|
5461
|
+
...collectPublicCollisions(graph.plan_variations, "plan_variation", "plan_id"),
|
|
5462
|
+
...collectPublicCollisions(graph.addon_variations, "addon_variation", "addon_id")
|
|
5463
|
+
];
|
|
5464
|
+
}
|
|
5465
|
+
function collectPublicCollisions(rows, objectType, parentField) {
|
|
5466
|
+
if (!rows?.length) return [];
|
|
5467
|
+
const publicByTuple = /* @__PURE__ */ new Map();
|
|
5468
|
+
for (const row of rows) {
|
|
5469
|
+
if (row.is_current === false) continue;
|
|
5470
|
+
if (String(row.visibility ?? "public") !== "public") continue;
|
|
5471
|
+
const tenant = String(row.tenant_id ?? "");
|
|
5472
|
+
const parent = String(row[parentField] ?? "");
|
|
5473
|
+
const period = String(row.billing_period ?? "");
|
|
5474
|
+
const segment = row.segment_id == null ? "_default" : String(row.segment_id);
|
|
5475
|
+
const key = `${tenant} ${parent} ${period} ${segment}`;
|
|
5476
|
+
const id = String(row.handle ?? row.id ?? "");
|
|
5477
|
+
const bucket = publicByTuple.get(key);
|
|
5478
|
+
if (bucket) bucket.ids.push(id);
|
|
5479
|
+
else publicByTuple.set(key, { ids: [id], parent, period, segment });
|
|
5480
|
+
}
|
|
5481
|
+
const findings = [];
|
|
5482
|
+
for (const bucket of publicByTuple.values()) {
|
|
5483
|
+
if (bucket.ids.length < 2) continue;
|
|
5484
|
+
const allIds = [...bucket.ids].sort();
|
|
5485
|
+
const segmentLabel = bucket.segment === "_default" ? "no segment" : `segment ${bucket.segment}`;
|
|
5486
|
+
const parentLabel = parentField === "plan_id" ? "plan" : "add-on";
|
|
5487
|
+
for (const id of bucket.ids) {
|
|
5488
|
+
findings.push(
|
|
5489
|
+
finding(
|
|
5490
|
+
"VAL-PLN-06",
|
|
5491
|
+
{
|
|
5492
|
+
object_type: objectType,
|
|
5493
|
+
object_id: id,
|
|
5494
|
+
field: "visibility",
|
|
5495
|
+
studio: "plans-entitlements"
|
|
5496
|
+
},
|
|
5497
|
+
{
|
|
5498
|
+
message: `Variation '${id}' is one of ${bucket.ids.length} marked Public for the same ${parentLabel}, billing period '${bucket.period}', and ${segmentLabel} \u2014 only one may be Public.`,
|
|
5499
|
+
detail: `Set all but one of [${allIds.join(", ")}] to Unlisted or Legacy before deploying.`
|
|
5500
|
+
}
|
|
5501
|
+
)
|
|
5502
|
+
);
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
return findings;
|
|
5506
|
+
}
|
|
5507
|
+
function fieldLabel(path3) {
|
|
5508
|
+
if (!path3 || path3.length === 0) return "This value";
|
|
5509
|
+
return String(path3[path3.length - 1]);
|
|
5510
|
+
}
|
|
5511
|
+
function messageForZodIssue(issue) {
|
|
5512
|
+
const field = fieldLabel(issue.path);
|
|
5513
|
+
switch (issue.code) {
|
|
5514
|
+
case "invalid_type":
|
|
5515
|
+
return issue.input === void 0 ? `${field} is required.` : `${field} must be a ${issue.expected ?? "valid value"}.`;
|
|
5516
|
+
case "invalid_value":
|
|
5517
|
+
return `${field} must be one of the allowed values.`;
|
|
5518
|
+
case "invalid_format":
|
|
5519
|
+
return `${field} isn't in a valid format.`;
|
|
5520
|
+
case "too_small":
|
|
5521
|
+
return `${field} is too small.`;
|
|
5522
|
+
case "too_big":
|
|
5523
|
+
return `${field} is too large.`;
|
|
5524
|
+
case "unrecognized_keys":
|
|
5525
|
+
return `${field} isn't a recognized setting.`;
|
|
5526
|
+
default:
|
|
5527
|
+
return issue.message ?? `${field} is invalid.`;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
function targetRefForIssue(issue, basePath) {
|
|
5531
|
+
const path3 = [...basePath, ...issue.path ?? []].map(
|
|
5532
|
+
(seg) => typeof seg === "number" ? seg : String(seg)
|
|
5533
|
+
);
|
|
5534
|
+
return { path: path3 };
|
|
5535
|
+
}
|
|
5536
|
+
function zodErrorToFindings(error, opts = {}) {
|
|
5537
|
+
const basePath = opts.basePath ?? [];
|
|
5538
|
+
return error.issues.map((issue) => ({
|
|
5539
|
+
code: issue.code ?? "invalid",
|
|
5540
|
+
severity: "error_draft",
|
|
5541
|
+
targetRef: targetRefForIssue(issue, basePath),
|
|
5542
|
+
message: messageForZodIssue(issue),
|
|
5543
|
+
...opts.specRef ? { specRef: opts.specRef } : {}
|
|
5544
|
+
}));
|
|
5545
|
+
}
|
|
5546
|
+
function spotlights(finding2, focus) {
|
|
5547
|
+
if (!focus) return false;
|
|
5548
|
+
const { object_type, object_id } = finding2.targetRef;
|
|
5549
|
+
if (focus.object_id != null && object_id !== focus.object_id) return false;
|
|
5550
|
+
if (focus.object_type != null && object_type !== focus.object_type) return false;
|
|
5551
|
+
return focus.object_id != null || focus.object_type != null;
|
|
5552
|
+
}
|
|
5553
|
+
function evaluate(graph, opts = {}) {
|
|
5554
|
+
const findings = [];
|
|
5555
|
+
if (opts.structuralErrors) {
|
|
5556
|
+
const errors = Array.isArray(opts.structuralErrors) ? opts.structuralErrors : [opts.structuralErrors];
|
|
5557
|
+
for (const error of errors) findings.push(...zodErrorToFindings(error));
|
|
5558
|
+
}
|
|
5559
|
+
findings.push(...runSemanticRules(graph));
|
|
5560
|
+
if (!opts.focus) return findings;
|
|
5561
|
+
return findings.map((f) => spotlights(f, opts.focus) ? { ...f, spotlight: true } : f);
|
|
5562
|
+
}
|
|
5563
|
+
|
|
5226
5564
|
// src/schema/version.ts
|
|
5227
5565
|
var SCHEMA_VERSION = "0.1.107";
|
|
5228
5566
|
|
|
@@ -5556,8 +5894,8 @@ function formatDiff(diff) {
|
|
|
5556
5894
|
|
|
5557
5895
|
// src/lib/config-validate.ts
|
|
5558
5896
|
var BLOCKING_SEVERITIES = /* @__PURE__ */ new Set(["error_draft", "error_launch"]);
|
|
5559
|
-
function isBlockingFinding(
|
|
5560
|
-
return BLOCKING_SEVERITIES.has(
|
|
5897
|
+
function isBlockingFinding(finding2) {
|
|
5898
|
+
return BLOCKING_SEVERITIES.has(finding2.severity);
|
|
5561
5899
|
}
|
|
5562
5900
|
function hasBlockingFindings(findings) {
|
|
5563
5901
|
return findings.some(isBlockingFinding);
|
|
@@ -5890,7 +6228,7 @@ Command groups:
|
|
|
5890
6228
|
Auth & meta login, logout, signup, whoami, schema, docs
|
|
5891
6229
|
Download download
|
|
5892
6230
|
Check validate, diff, show
|
|
5893
|
-
Stage/launch upload, launch, discard,
|
|
6231
|
+
Stage/launch upload, launch, discard, restore
|
|
5894
6232
|
Inspect status, history, preview, evaluate
|
|
5895
6233
|
|
|
5896
6234
|
Version selectors (no defaults \u2014 a command that reads a config requires one):
|
|
@@ -5916,7 +6254,7 @@ Common workflows:
|
|
|
5916
6254
|
# Inspect and roll back
|
|
5917
6255
|
revturbine status
|
|
5918
6256
|
revturbine history
|
|
5919
|
-
revturbine
|
|
6257
|
+
revturbine restore <playbook-version-id> --launch
|
|
5920
6258
|
|
|
5921
6259
|
Exit-code classes: 0 ok \xB7 1 unexpected \xB7 2 usage \xB7 3 auth \xB7 4 validation
|
|
5922
6260
|
blocked \xB7 5 conflict/stale \xB7 6 network \xB7 7 server error.
|
|
@@ -5924,7 +6262,7 @@ blocked \xB7 5 conflict/stale \xB7 6 network \xB7 7 server error.
|
|
|
5924
6262
|
Auth:
|
|
5925
6263
|
Most commands need a token \u2014 run \`login\` first. Credentials live at
|
|
5926
6264
|
~/.revturbine/credentials.json (0600); the token's tenant is used by default,
|
|
5927
|
-
override with -t/--tenant-id. Mutating commands (discard,
|
|
6265
|
+
override with -t/--tenant-id. Mutating commands (discard, restore) prompt
|
|
5928
6266
|
for confirmation unless --yes.
|
|
5929
6267
|
|
|
5930
6268
|
Full reference: ${DOCS_URL}
|
|
@@ -6055,7 +6393,7 @@ program.command("whoami").description("Show the resolved instance, tenant, crede
|
|
|
6055
6393
|
});
|
|
6056
6394
|
program.command("schema").description("Emit the bundled RevTurbineConfig JSON schema for an agent to author against.").option("--json", "Accepted for symmetry; output is always JSON").action(() => {
|
|
6057
6395
|
try {
|
|
6058
|
-
const jsonSchema =
|
|
6396
|
+
const jsonSchema = z29.toJSONSchema(RevTurbineConfigSchema, { unrepresentable: "any" });
|
|
6059
6397
|
emit({ schema_version: SCHEMA_VERSION, schema: jsonSchema }, true);
|
|
6060
6398
|
} catch (err) {
|
|
6061
6399
|
fail(EXIT.UNEXPECTED, `could not render the bundled schema (${SCHEMA_VERSION}): ${err.message}`);
|
|
@@ -6112,9 +6450,23 @@ program.command("validate").description("Validate a Config File offline (schema)
|
|
|
6112
6450
|
throw new SelectorError("STATE_REQUIRED \u2014 validate needs a version: <file> (offline) or --draft (server catalog).");
|
|
6113
6451
|
}
|
|
6114
6452
|
if (!opts.draft) {
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6453
|
+
let blockedFiles = 0;
|
|
6454
|
+
for (const file of files) {
|
|
6455
|
+
const raw = loadConfig(file);
|
|
6456
|
+
const parsed = schema.safeParse(raw);
|
|
6457
|
+
const findings = evaluate(parsed.success ? parsed.data : raw, {
|
|
6458
|
+
structuralErrors: parsed.success ? void 0 : parsed.error
|
|
6459
|
+
});
|
|
6460
|
+
diag(`Validation for ${file} (offline, schema ${SCHEMA_VERSION}):`);
|
|
6461
|
+
process.stdout.write(`${formatFindings(findings)}
|
|
6462
|
+
`);
|
|
6463
|
+
if (hasBlockingFindings(findings)) blockedFiles += 1;
|
|
6464
|
+
}
|
|
6465
|
+
if (files.length > 1) diag(`${files.length - blockedFiles}/${files.length} passed.`);
|
|
6466
|
+
if (blockedFiles > 0) {
|
|
6467
|
+
fail(EXIT.VALIDATION, `${blockedFiles} file(s) have blocking findings.`);
|
|
6468
|
+
}
|
|
6469
|
+
process.exit(0);
|
|
6118
6470
|
}
|
|
6119
6471
|
const conn = connect(opts.url, opts.tenantId);
|
|
6120
6472
|
const playbookVersionId = await requireOpenDraft(conn);
|
|
@@ -6172,7 +6524,7 @@ program.command("upload").description("Stage a Config File as the open draft (PO
|
|
|
6172
6524
|
if (playbookVersionId) diagRaw(` playbook_version_id: ${playbookVersionId}`);
|
|
6173
6525
|
diag("Launch it with `revturbine launch --draft`, or from the UI (Drafts & Releases).");
|
|
6174
6526
|
});
|
|
6175
|
-
program.command("launch").description("Take a config live as a new Release: validate (launch gate), then submit \u2192 approve \u2192 deploy. Synchronous.").argument("[file]", "Config File to upload and launch directly").option("--draft", "Launch the already-open draft").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--yes", "Accepted for parity with discard/
|
|
6527
|
+
program.command("launch").description("Take a config live as a new Release: validate (launch gate), then submit \u2192 approve \u2192 deploy. Synchronous.").argument("[file]", "Config File to upload and launch directly").option("--draft", "Launch the already-open draft").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--yes", "Accepted for parity with discard/restore; launch has no confirmation prompt").action(async (file, opts) => {
|
|
6176
6528
|
const [sel] = requireSelectors(opts, file ? [file] : [], { count: 1, allowed: ["file", "draft"], command: "launch" });
|
|
6177
6529
|
const conn = connect(opts.url, opts.tenantId);
|
|
6178
6530
|
let playbookVersionId;
|
|
@@ -6201,17 +6553,22 @@ program.command("discard").description("Discard (archive) the open draft so a fr
|
|
|
6201
6553
|
if (!res.ok) httpFail(conn, "discard", res.status, json);
|
|
6202
6554
|
diag(`\u2713 Discarded draft ${playbookVersionId}.`);
|
|
6203
6555
|
});
|
|
6204
|
-
program.command("
|
|
6556
|
+
program.command("restore").description("Stage a draft that restores a past release (from its frozen snapshot); `--launch` takes it live. Halts if a draft is already open.").argument("<playbook-version-id>", "The deployed playbook version to restore (see `history`)").option("--launch", "Launch the restoring draft immediately (gate + submit \u2192 approve \u2192 deploy)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--yes", "Skip the confirmation prompt").action(async (playbookVersionId, opts) => {
|
|
6205
6557
|
const conn = connect(opts.url, opts.tenantId);
|
|
6206
6558
|
await confirmOrExit(
|
|
6207
|
-
`
|
|
6559
|
+
`Restore playbook version ${playbookVersionId} on ${conn.url}?${opts.launch ? " --launch will take it LIVE." : " (stages a draft; launch separately)"}`,
|
|
6208
6560
|
!!opts.yes
|
|
6209
6561
|
);
|
|
6210
6562
|
const { res, json } = await postJson(conn, `/api/playbook-versions/${playbookVersionId}/rollback`, {});
|
|
6211
|
-
if (!res.ok) httpFail(conn, "
|
|
6212
|
-
diag(`\u2713 Rollback requested for playbook version ${playbookVersionId}.`);
|
|
6563
|
+
if (!res.ok) httpFail(conn, "restore", res.status, json);
|
|
6213
6564
|
const reverting = json?.item?.id ?? json?.playbook_version_id;
|
|
6214
|
-
|
|
6565
|
+
diag(`\u2713 Staged a restoring draft from ${playbookVersionId}${reverting ? ` (${reverting})` : ""}.`);
|
|
6566
|
+
if (!opts.launch) {
|
|
6567
|
+
diag("Launch it with `revturbine launch --draft`, or discard it with `revturbine discard --yes`.");
|
|
6568
|
+
return;
|
|
6569
|
+
}
|
|
6570
|
+
if (!reverting) fail(EXIT.SERVER, "No reverting draft id returned \u2014 cannot launch.");
|
|
6571
|
+
await launchDraft(conn, reverting);
|
|
6215
6572
|
});
|
|
6216
6573
|
program.command("status").description("The current live Release and the open draft, side by side.").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").action(async (opts) => {
|
|
6217
6574
|
const conn = connect(opts.url, opts.tenantId);
|
package/package.json
CHANGED