@revturbine/cli 0.5.0 → 0.5.2
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/dist/cli.js +380 -8
- package/package.json +1 -1
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);
|
|
@@ -5686,6 +6024,16 @@ function requireSelectors(opts, positionalFiles, {
|
|
|
5686
6024
|
return found;
|
|
5687
6025
|
}
|
|
5688
6026
|
|
|
6027
|
+
// src/lib/version-trail.ts
|
|
6028
|
+
function serverSchemaIsNewer(config, bundledVersion) {
|
|
6029
|
+
const server = config?.schema_version;
|
|
6030
|
+
if (typeof server !== "string" || !server) return null;
|
|
6031
|
+
const parse = (v) => v.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
6032
|
+
const [a, b] = [parse(server), parse(bundledVersion)];
|
|
6033
|
+
const newer = a[0] !== b[0] ? a[0] > b[0] : a[1] !== b[1] ? a[1] > b[1] : (a[2] ?? 0) > (b[2] ?? 0);
|
|
6034
|
+
return newer ? server : null;
|
|
6035
|
+
}
|
|
6036
|
+
|
|
5689
6037
|
// src/cli.ts
|
|
5690
6038
|
var DOCS_URL = "https://github.com/revt-eng/revturbine-cli#readme";
|
|
5691
6039
|
var DEFAULT_URL = "https://revturbine.com/app";
|
|
@@ -5766,11 +6114,21 @@ async function getJson(conn, pathname) {
|
|
|
5766
6114
|
const json = await res.json().catch(() => ({}));
|
|
5767
6115
|
return { res, json };
|
|
5768
6116
|
}
|
|
6117
|
+
function warnIfSchemaBehind(config) {
|
|
6118
|
+
const server = serverSchemaIsNewer(config, SCHEMA_VERSION);
|
|
6119
|
+
if (server) {
|
|
6120
|
+
diag(
|
|
6121
|
+
`WARNING: the server's schema (${server}) is newer than this CLI's bundled snapshot (${SCHEMA_VERSION}) \u2014 offline validation may be missing rules. Update: npm i -g @revturbine/cli`
|
|
6122
|
+
);
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
5769
6125
|
async function downloadConfig(conn, playbookVersionId) {
|
|
5770
6126
|
const qs = playbookVersionId ? `?playbookVersionId=${encodeURIComponent(playbookVersionId)}` : "";
|
|
5771
6127
|
const res = await request(conn, `/api/config/export${qs}`);
|
|
5772
6128
|
if (!res.ok) httpFail(conn, "download", res.status);
|
|
5773
|
-
|
|
6129
|
+
const config = await res.json().catch(() => ({}));
|
|
6130
|
+
warnIfSchemaBehind(config);
|
|
6131
|
+
return config;
|
|
5774
6132
|
}
|
|
5775
6133
|
async function requireOpenDraft(conn) {
|
|
5776
6134
|
const { ok, status, draft } = await resolveActiveDraft(conn.url, conn.headers);
|
|
@@ -6055,7 +6413,7 @@ program.command("whoami").description("Show the resolved instance, tenant, crede
|
|
|
6055
6413
|
});
|
|
6056
6414
|
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
6415
|
try {
|
|
6058
|
-
const jsonSchema =
|
|
6416
|
+
const jsonSchema = z29.toJSONSchema(RevTurbineConfigSchema, { unrepresentable: "any" });
|
|
6059
6417
|
emit({ schema_version: SCHEMA_VERSION, schema: jsonSchema }, true);
|
|
6060
6418
|
} catch (err) {
|
|
6061
6419
|
fail(EXIT.UNEXPECTED, `could not render the bundled schema (${SCHEMA_VERSION}): ${err.message}`);
|
|
@@ -6112,9 +6470,23 @@ program.command("validate").description("Validate a Config File offline (schema)
|
|
|
6112
6470
|
throw new SelectorError("STATE_REQUIRED \u2014 validate needs a version: <file> (offline) or --draft (server catalog).");
|
|
6113
6471
|
}
|
|
6114
6472
|
if (!opts.draft) {
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6473
|
+
let blockedFiles = 0;
|
|
6474
|
+
for (const file of files) {
|
|
6475
|
+
const raw = loadConfig(file);
|
|
6476
|
+
const parsed = schema.safeParse(raw);
|
|
6477
|
+
const findings = evaluate(parsed.success ? parsed.data : raw, {
|
|
6478
|
+
structuralErrors: parsed.success ? void 0 : parsed.error
|
|
6479
|
+
});
|
|
6480
|
+
diag(`Validation for ${file} (offline, schema ${SCHEMA_VERSION}):`);
|
|
6481
|
+
process.stdout.write(`${formatFindings(findings)}
|
|
6482
|
+
`);
|
|
6483
|
+
if (hasBlockingFindings(findings)) blockedFiles += 1;
|
|
6484
|
+
}
|
|
6485
|
+
if (files.length > 1) diag(`${files.length - blockedFiles}/${files.length} passed.`);
|
|
6486
|
+
if (blockedFiles > 0) {
|
|
6487
|
+
fail(EXIT.VALIDATION, `${blockedFiles} file(s) have blocking findings.`);
|
|
6488
|
+
}
|
|
6489
|
+
process.exit(0);
|
|
6118
6490
|
}
|
|
6119
6491
|
const conn = connect(opts.url, opts.tenantId);
|
|
6120
6492
|
const playbookVersionId = await requireOpenDraft(conn);
|
package/package.json
CHANGED