@revturbine/cli 0.3.0 → 0.4.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 +54 -22
- package/dist/cli.js +530 -313
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# revturbine-cli (`revturbine`)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
terminal, the same operations the in-app
|
|
3
|
+
Validate [RevTurbine](https://revturbine.com) **Config Files** and ship them to
|
|
4
|
+
a RevTurbine instance through the playbook-version lifecycle (draft → Release) —
|
|
5
|
+
from the terminal, the same operations the in-app studios perform.
|
|
6
6
|
|
|
7
7
|
`revturbine` is the command-line counterpart to the RevTurbine control plane. It
|
|
8
8
|
schema-validates a config offline, authenticates to an instance via the browser
|
|
9
|
-
(RFC 8628 device flow), and stages /
|
|
9
|
+
(RFC 8628 device flow), and stages / launches configs as playbook versions so
|
|
10
10
|
every change is reviewable and rollback-able.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
@@ -25,38 +25,70 @@ Requires Node ≥ 20.
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
revturbine signup
|
|
29
|
-
revturbine
|
|
30
|
-
revturbine
|
|
31
|
-
|
|
32
|
-
revturbine
|
|
33
|
-
revturbine
|
|
28
|
+
revturbine signup # create an account (email + password + emailed code)
|
|
29
|
+
revturbine login # authorize this machine (device flow)
|
|
30
|
+
revturbine download --live --save ./config.json
|
|
31
|
+
# …edit ./config.json…
|
|
32
|
+
revturbine validate ./config.json # schema-validate locally (no network)
|
|
33
|
+
revturbine diff ./config.json --live # what would change (no writes)
|
|
34
|
+
revturbine launch ./config.json # upload, gate, and go live
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
`revturbine <command> --help` documents every flag.
|
|
37
38
|
|
|
39
|
+
## Version selectors
|
|
40
|
+
|
|
41
|
+
Commands that read a config name the version explicitly — there is no default:
|
|
42
|
+
|
|
43
|
+
| Selector | Meaning |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `<file>` / `--file <path>` | a local Config File |
|
|
46
|
+
| `--draft` | the tenant's single open draft (resolved automatically) |
|
|
47
|
+
| `--live` | the current live Release |
|
|
48
|
+
| `--release <id>` | a specific playbook version / Release |
|
|
49
|
+
|
|
38
50
|
## Commands
|
|
39
51
|
|
|
40
52
|
| Command | What it does |
|
|
41
53
|
|---|---|
|
|
42
|
-
| `signup` | Create an account headlessly: email + password, then an emailed one-time code to verify, then a token is stored.
|
|
54
|
+
| `signup` | Create an account headlessly: email + password, then an emailed one-time code to verify, then a token is stored. |
|
|
43
55
|
| `login` / `logout` | Device-flow auth; tokens stored at `~/.revturbine/credentials.json` (mode 0600). |
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `validate` |
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
56
|
+
| `whoami` | The resolved instance, tenant, credentials source, and whether the stored token works. |
|
|
57
|
+
| `schema` | Emit the bundled `RevTurbineConfig` JSON schema (for agents to author against). |
|
|
58
|
+
| `docs` | Print the canonical documentation URL. |
|
|
59
|
+
| `download` | Fetch a config version (`--live` / `--draft` / `--release <id>`); `--save`, `--format flatbuffer`. |
|
|
60
|
+
| `validate` | Offline schema validation of a `<file>`, or the full server catalog against the open draft (`--draft`). |
|
|
61
|
+
| `diff` | Compare any two versions, first → second (dry-run, no writes). |
|
|
62
|
+
| `show <kind>` | Summary tables: `plans` · `entitlements` · `segments` · `placements` · `trials` for any version. |
|
|
63
|
+
| `upload` | Stage a Config File as the open draft. |
|
|
64
|
+
| `launch` | Take a config live: validate (launch gate) → submit → approve → deploy. `launch <file>` or `launch --draft`. |
|
|
65
|
+
| `discard` | Archive the open draft (`--yes`). |
|
|
66
|
+
| `rollback` | Revert a deployed playbook version (creates a reverting draft; becomes `restore` once server-side entry copying lands). |
|
|
67
|
+
| `status` | The live Release and the open draft, side by side. |
|
|
68
|
+
| `history` | The Release Version Log, newest first. |
|
|
69
|
+
| `preview` | The open draft's staged changes. |
|
|
52
70
|
| `evaluate` | Run the live config's placement/entitlement decisions for a user context. |
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
|
|
72
|
+
`--json` on read commands emits machine-readable output. Results go to stdout,
|
|
73
|
+
diagnostics to stderr.
|
|
74
|
+
|
|
75
|
+
## Exit-code classes
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
0 success
|
|
79
|
+
1 unexpected error (catch-all)
|
|
80
|
+
2 bad usage — unknown command, flag, argument, or missing version selector
|
|
81
|
+
3 authentication or permission denied
|
|
82
|
+
4 validation blocked (schema failure, blocking findings, unknown id)
|
|
83
|
+
5 conflict or stale state (e.g. a draft is already open)
|
|
84
|
+
6 network or transient failure
|
|
85
|
+
7 server error
|
|
86
|
+
```
|
|
55
87
|
|
|
56
88
|
## Schema validation
|
|
57
89
|
|
|
58
90
|
Validation is **mandatory** and runs fully offline against a vendored,
|
|
59
|
-
version-stamped snapshot of RevTurbine's `
|
|
91
|
+
version-stamped snapshot of RevTurbine's `RevTurbineConfigSchema`
|
|
60
92
|
(`src/schema/`, regenerated from the canonical schema via
|
|
61
93
|
`npm run generate:schema`). The CLI never uploads a config it could not
|
|
62
94
|
validate. `revturbine --version` reports both the CLI version and the schema
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,9 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as rea
|
|
|
5
5
|
import path2 from "path";
|
|
6
6
|
import { createRequire } from "module";
|
|
7
7
|
import { createInterface } from "readline/promises";
|
|
8
|
-
import {
|
|
8
|
+
import { spawn as spawn2 } from "child_process";
|
|
9
|
+
import { Command, CommanderError, Option } from "commander";
|
|
10
|
+
import { z as z27 } from "zod";
|
|
9
11
|
|
|
10
12
|
// src/schema/exported-config.snapshot.mjs
|
|
11
13
|
import { z as z2 } from "zod";
|
|
@@ -5222,7 +5224,7 @@ var AuthSsoProviderSchema = IdField.extend({
|
|
|
5222
5224
|
}).meta({ id: "AuthSsoProvider", "x-revturbine-schema-persistence": Persisted23, "x-revturbine-schema-exposure": Internal19 });
|
|
5223
5225
|
|
|
5224
5226
|
// src/schema/version.ts
|
|
5225
|
-
var SCHEMA_VERSION = "0.1.
|
|
5227
|
+
var SCHEMA_VERSION = "0.1.107";
|
|
5226
5228
|
|
|
5227
5229
|
// src/lib/credentials.ts
|
|
5228
5230
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
@@ -5553,7 +5555,7 @@ function formatDiff(diff) {
|
|
|
5553
5555
|
}
|
|
5554
5556
|
|
|
5555
5557
|
// src/lib/config-validate.ts
|
|
5556
|
-
var BLOCKING_SEVERITIES = /* @__PURE__ */ new Set(["error_draft", "
|
|
5558
|
+
var BLOCKING_SEVERITIES = /* @__PURE__ */ new Set(["error_draft", "error_launch"]);
|
|
5557
5559
|
function isBlockingFinding(finding) {
|
|
5558
5560
|
return BLOCKING_SEVERITIES.has(finding.severity);
|
|
5559
5561
|
}
|
|
@@ -5579,33 +5581,134 @@ async function fetchValidation(baseUrl, playbookVersionId, headers, fetchImpl =
|
|
|
5579
5581
|
};
|
|
5580
5582
|
}
|
|
5581
5583
|
|
|
5582
|
-
// src/
|
|
5584
|
+
// src/lib/drafts.ts
|
|
5585
|
+
async function resolveActiveDraft(baseUrl, headers, fetchImpl = fetch) {
|
|
5586
|
+
const res = await fetchImpl(`${baseUrl}/api/optimization/drafts`, { headers });
|
|
5587
|
+
const json = await res.json().catch(() => ({}));
|
|
5588
|
+
return { ok: res.ok, status: res.status, draft: res.ok ? json.active ?? null : null };
|
|
5589
|
+
}
|
|
5590
|
+
|
|
5591
|
+
// src/lib/output.ts
|
|
5592
|
+
var EXIT = {
|
|
5593
|
+
OK: 0,
|
|
5594
|
+
/** Unexpected error (catch-all). */
|
|
5595
|
+
UNEXPECTED: 1,
|
|
5596
|
+
/** Bad usage — unknown command, flag, argument, or missing version selector. */
|
|
5597
|
+
USAGE: 2,
|
|
5598
|
+
/** Authentication or permission denied. */
|
|
5599
|
+
AUTH: 3,
|
|
5600
|
+
/** Validation blocked (schema failure, blocking findings, rejected config, unknown id). */
|
|
5601
|
+
VALIDATION: 4,
|
|
5602
|
+
/** Conflict or stale state (open draft exists, base moved). */
|
|
5603
|
+
CONFLICT: 5,
|
|
5604
|
+
/** Network or transient failure. */
|
|
5605
|
+
NETWORK: 6,
|
|
5606
|
+
/** Server error. */
|
|
5607
|
+
SERVER: 7
|
|
5608
|
+
};
|
|
5609
|
+
function classFromStatus(status) {
|
|
5610
|
+
if (status === 401 || status === 403) return EXIT.AUTH;
|
|
5611
|
+
if (status === 409) return EXIT.CONFLICT;
|
|
5612
|
+
if (status === 400 || status === 404 || status === 422) return EXIT.VALIDATION;
|
|
5613
|
+
if (status >= 500) return EXIT.SERVER;
|
|
5614
|
+
return EXIT.UNEXPECTED;
|
|
5615
|
+
}
|
|
5616
|
+
function isNetworkError(err) {
|
|
5617
|
+
return err instanceof TypeError || err instanceof Error && /fetch failed|ECONN|ENOTFOUND|ETIMEDOUT|EAI_AGAIN/i.test(err.message);
|
|
5618
|
+
}
|
|
5583
5619
|
var LOG = "[revturbine]";
|
|
5620
|
+
function diag(message) {
|
|
5621
|
+
console.error(`${LOG} ${message}`);
|
|
5622
|
+
}
|
|
5623
|
+
function diagRaw(message) {
|
|
5624
|
+
console.error(message);
|
|
5625
|
+
}
|
|
5626
|
+
function emit(data, json, humanText) {
|
|
5627
|
+
if (json) {
|
|
5628
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
5629
|
+
`);
|
|
5630
|
+
} else {
|
|
5631
|
+
process.stdout.write(`${humanText ?? JSON.stringify(data, null, 2)}
|
|
5632
|
+
`);
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
function fail(cls, message) {
|
|
5636
|
+
console.error(`${LOG} \u2717 ${message}`);
|
|
5637
|
+
process.exit(cls);
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
// src/lib/selectors.ts
|
|
5641
|
+
var SelectorError = class extends Error {
|
|
5642
|
+
code = "STATE_REQUIRED";
|
|
5643
|
+
constructor(message) {
|
|
5644
|
+
super(message);
|
|
5645
|
+
}
|
|
5646
|
+
};
|
|
5647
|
+
function collectSelectors(opts, positionalFiles = []) {
|
|
5648
|
+
const found = positionalFiles.map((path3) => ({ kind: "file", path: path3 }));
|
|
5649
|
+
if (opts.file) found.push({ kind: "file", path: opts.file });
|
|
5650
|
+
if (opts.draft) found.push({ kind: "draft" });
|
|
5651
|
+
if (opts.live) found.push({ kind: "live" });
|
|
5652
|
+
if (opts.release) found.push({ kind: "release", id: opts.release });
|
|
5653
|
+
return found;
|
|
5654
|
+
}
|
|
5655
|
+
function describeSelector(sel) {
|
|
5656
|
+
switch (sel.kind) {
|
|
5657
|
+
case "file":
|
|
5658
|
+
return sel.path;
|
|
5659
|
+
case "draft":
|
|
5660
|
+
return "--draft";
|
|
5661
|
+
case "live":
|
|
5662
|
+
return "--live";
|
|
5663
|
+
case "release":
|
|
5664
|
+
return `--release ${sel.id}`;
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5667
|
+
function requireSelectors(opts, positionalFiles, {
|
|
5668
|
+
count,
|
|
5669
|
+
allowed,
|
|
5670
|
+
command
|
|
5671
|
+
}) {
|
|
5672
|
+
const found = collectSelectors(opts, positionalFiles);
|
|
5673
|
+
const choices = allowed.map((k) => k === "file" ? "<file>" : k === "release" ? "--release <id>" : `--${k}`).join(" / ");
|
|
5674
|
+
const disallowed = found.find((s) => !allowed.includes(s.kind));
|
|
5675
|
+
if (disallowed) {
|
|
5676
|
+
throw new SelectorError(`${command} does not accept ${describeSelector(disallowed)} \u2014 use ${choices}.`);
|
|
5677
|
+
}
|
|
5678
|
+
if (found.length === 0) {
|
|
5679
|
+
throw new SelectorError(
|
|
5680
|
+
`STATE_REQUIRED \u2014 ${command} needs ${count === 1 ? "a version" : `${count} versions`}: ${choices}. There is no default.`
|
|
5681
|
+
);
|
|
5682
|
+
}
|
|
5683
|
+
if (found.length !== count) {
|
|
5684
|
+
throw new SelectorError(`${command} takes exactly ${count} version${count === 1 ? "" : "s"} (${choices}); got ${found.length}.`);
|
|
5685
|
+
}
|
|
5686
|
+
return found;
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
// src/cli.ts
|
|
5690
|
+
var DOCS_URL = "https://github.com/revt-eng/revturbine-cli#readme";
|
|
5584
5691
|
var DEFAULT_URL = "https://revturbine.com/app";
|
|
5585
5692
|
var schema = RevTurbineConfigSchema;
|
|
5586
5693
|
function loadConfig(file) {
|
|
5587
5694
|
const resolved = path2.resolve(file);
|
|
5588
|
-
if (!existsSync2(resolved)) {
|
|
5589
|
-
console.error(`${LOG} Config not found: ${resolved}`);
|
|
5590
|
-
process.exit(1);
|
|
5591
|
-
}
|
|
5695
|
+
if (!existsSync2(resolved)) fail(EXIT.USAGE, `Config not found: ${resolved}`);
|
|
5592
5696
|
try {
|
|
5593
5697
|
return JSON.parse(readFileSync2(resolved, "utf8"));
|
|
5594
5698
|
} catch (err) {
|
|
5595
|
-
|
|
5596
|
-
process.exit(1);
|
|
5699
|
+
fail(EXIT.VALIDATION, `invalid JSON in ${resolved}: ${err.message}`);
|
|
5597
5700
|
}
|
|
5598
5701
|
}
|
|
5599
5702
|
function verifyConfig(file) {
|
|
5600
5703
|
const parsed = loadConfig(file);
|
|
5601
5704
|
const result = schema.safeParse(parsed);
|
|
5602
5705
|
if (result.success) {
|
|
5603
|
-
|
|
5706
|
+
diag(`\u2713 ${file}: schema validation passed`);
|
|
5604
5707
|
return result.data;
|
|
5605
5708
|
}
|
|
5606
|
-
|
|
5709
|
+
diag(`\u2717 ${file}: schema validation FAILED:`);
|
|
5607
5710
|
for (const issue of result.error.issues) {
|
|
5608
|
-
|
|
5711
|
+
diagRaw(` ${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
5609
5712
|
}
|
|
5610
5713
|
return null;
|
|
5611
5714
|
}
|
|
@@ -5615,15 +5718,19 @@ function connect(rawUrl, explicitTenantId) {
|
|
|
5615
5718
|
const cred = getCredential(url);
|
|
5616
5719
|
const tenantId = explicitTenantId ?? cred?.tenant_id ?? "dev-tenant-001";
|
|
5617
5720
|
const tenantSource = explicitTenantId ? "--tenant-id" : cred?.tenant_id ? "stored token" : "default";
|
|
5618
|
-
|
|
5721
|
+
diag(`Tenant ${tenantId} (${tenantSource}); credentials: ${dir} [${source}].`);
|
|
5619
5722
|
if (source === "global") {
|
|
5620
|
-
|
|
5621
|
-
|
|
5723
|
+
diag(
|
|
5724
|
+
`WARNING: using the global ${dir} - NOT worktree-scoped. If this session is for a specific customer, run from that customer's worktree (with its own .revturbine/) so a stale login can't target the wrong tenant.`
|
|
5622
5725
|
);
|
|
5623
5726
|
}
|
|
5624
5727
|
return {
|
|
5625
5728
|
url,
|
|
5626
5729
|
tenantId,
|
|
5730
|
+
tenantSource,
|
|
5731
|
+
credentialsDir: dir,
|
|
5732
|
+
credentialsSource: source,
|
|
5733
|
+
hasToken: Boolean(cred),
|
|
5627
5734
|
headers: {
|
|
5628
5735
|
"Content-Type": "application/json",
|
|
5629
5736
|
"x-tenant-id": tenantId,
|
|
@@ -5633,129 +5740,216 @@ function connect(rawUrl, explicitTenantId) {
|
|
|
5633
5740
|
}
|
|
5634
5741
|
function authHint(url, status) {
|
|
5635
5742
|
if (status === 401 || status === 403) {
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5743
|
+
diag(`Authentication required for ${url}. Log in with:
|
|
5744
|
+
revturbine login ${url}`);
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
function httpFail(conn, what, status, detail) {
|
|
5748
|
+
authHint(conn.url, status);
|
|
5749
|
+
fail(classFromStatus(status), `${what} failed (${status})${detail ? `: ${JSON.stringify(detail)}` : ""}`);
|
|
5750
|
+
}
|
|
5751
|
+
async function request(conn, pathname, init) {
|
|
5752
|
+
try {
|
|
5753
|
+
return await fetch(`${conn.url}${pathname}`, { headers: conn.headers, ...init });
|
|
5754
|
+
} catch (err) {
|
|
5755
|
+
if (isNetworkError(err)) fail(EXIT.NETWORK, `network failure reaching ${conn.url}: ${err.message}`);
|
|
5756
|
+
throw err;
|
|
5641
5757
|
}
|
|
5642
5758
|
}
|
|
5643
5759
|
async function postJson(conn, pathname, body) {
|
|
5644
|
-
const res = await
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5760
|
+
const res = await request(conn, pathname, { method: "POST", body: JSON.stringify(body) });
|
|
5761
|
+
const json = await res.json().catch(() => ({}));
|
|
5762
|
+
return { res, json };
|
|
5763
|
+
}
|
|
5764
|
+
async function getJson(conn, pathname) {
|
|
5765
|
+
const res = await request(conn, pathname);
|
|
5649
5766
|
const json = await res.json().catch(() => ({}));
|
|
5650
5767
|
return { res, json };
|
|
5651
5768
|
}
|
|
5652
|
-
async function
|
|
5653
|
-
|
|
5654
|
-
|
|
5769
|
+
async function downloadConfig(conn, playbookVersionId) {
|
|
5770
|
+
const qs = playbookVersionId ? `?playbookVersionId=${encodeURIComponent(playbookVersionId)}` : "";
|
|
5771
|
+
const res = await request(conn, `/api/config/export${qs}`);
|
|
5772
|
+
if (!res.ok) httpFail(conn, "download", res.status);
|
|
5773
|
+
return res.json().catch(() => ({}));
|
|
5774
|
+
}
|
|
5775
|
+
async function requireOpenDraft(conn) {
|
|
5776
|
+
const { ok, status, draft } = await resolveActiveDraft(conn.url, conn.headers);
|
|
5777
|
+
if (!ok) httpFail(conn, "draft lookup", status);
|
|
5778
|
+
if (!draft) fail(EXIT.VALIDATION, "No open draft for this tenant \u2014 stage one with `revturbine upload <file>`.");
|
|
5779
|
+
return draft.id;
|
|
5780
|
+
}
|
|
5781
|
+
async function loadVersion(conn, sel) {
|
|
5782
|
+
switch (sel.kind) {
|
|
5783
|
+
case "file":
|
|
5784
|
+
return loadConfig(sel.path);
|
|
5785
|
+
case "live":
|
|
5786
|
+
return downloadConfig(conn);
|
|
5787
|
+
case "release":
|
|
5788
|
+
return downloadConfig(conn, sel.id);
|
|
5789
|
+
case "draft":
|
|
5790
|
+
return downloadConfig(conn, await requireOpenDraft(conn));
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
async function launchDraft(conn, playbookVersionId) {
|
|
5794
|
+
diag(`Launch gate: validating draft ${playbookVersionId} \u2026`);
|
|
5795
|
+
const validation = await fetchValidation(conn.url, playbookVersionId, conn.headers);
|
|
5796
|
+
if (!validation.ok) httpFail(conn, "validate", validation.status, validation.error);
|
|
5797
|
+
if (validation.findings.length > 0) diagRaw(formatFindings(validation.findings));
|
|
5798
|
+
if (hasBlockingFindings(validation.findings)) {
|
|
5799
|
+
fail(EXIT.VALIDATION, "blocking findings \u2014 this draft cannot launch.");
|
|
5800
|
+
}
|
|
5801
|
+
diag(`Launching playbook version ${playbookVersionId} (submit \u2192 approve \u2192 deploy) \u2026`);
|
|
5655
5802
|
for (const step of ["submit", "approve", "deploy"]) {
|
|
5656
5803
|
const { res, json } = await postJson(conn, `/api/playbook-versions/${playbookVersionId}/${step}`, {});
|
|
5657
5804
|
if (!res.ok) {
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5805
|
+
diag(`\u2717 ${step} failed (${res.status}). The draft is staged but NOT live.`);
|
|
5806
|
+
diagRaw(` ${JSON.stringify(json)}`);
|
|
5807
|
+
diag(`Finish it from the UI (Drafts & Releases): playbook version ${playbookVersionId}`);
|
|
5661
5808
|
authHint(conn.url, res.status);
|
|
5662
|
-
process.exit(
|
|
5809
|
+
process.exit(classFromStatus(res.status));
|
|
5663
5810
|
}
|
|
5664
|
-
|
|
5811
|
+
diag(` \u2713 ${step}`);
|
|
5665
5812
|
}
|
|
5666
|
-
|
|
5667
|
-
}
|
|
5668
|
-
async function getJson(conn, pathname) {
|
|
5669
|
-
const res = await fetch(`${conn.url}${pathname}`, { headers: conn.headers });
|
|
5670
|
-
const json = await res.json().catch(() => ({}));
|
|
5671
|
-
return { res, json };
|
|
5672
|
-
}
|
|
5673
|
-
async function downloadLiveConfig(conn, playbookVersionId) {
|
|
5674
|
-
const qs = playbookVersionId ? `?playbookVersionId=${encodeURIComponent(playbookVersionId)}` : "";
|
|
5675
|
-
const res = await fetch(`${conn.url}/api/config/export${qs}`, { headers: conn.headers });
|
|
5676
|
-
const config = res.ok ? await res.json().catch(() => ({})) : {};
|
|
5677
|
-
return { res, config };
|
|
5813
|
+
diag("\u2713 Launched \u2014 the playbook version is now the live configuration.");
|
|
5678
5814
|
}
|
|
5679
5815
|
async function confirmOrExit(promptText, yes) {
|
|
5680
5816
|
if (yes) return;
|
|
5681
5817
|
if (!process.stdin.isTTY) {
|
|
5682
|
-
|
|
5683
|
-
process.exit(1);
|
|
5818
|
+
fail(EXIT.USAGE, "Refusing a destructive action without confirmation (no TTY). Pass --yes.");
|
|
5684
5819
|
}
|
|
5685
|
-
const rl = createInterface({ input: process.stdin, output: process.
|
|
5820
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
5686
5821
|
const answer = (await rl.question(`${promptText} [y/N] `)).trim().toLowerCase();
|
|
5687
5822
|
rl.close();
|
|
5688
5823
|
if (answer !== "y" && answer !== "yes") {
|
|
5689
|
-
|
|
5824
|
+
diag("Aborted.");
|
|
5690
5825
|
process.exit(0);
|
|
5691
5826
|
}
|
|
5692
5827
|
}
|
|
5828
|
+
function table(rows) {
|
|
5829
|
+
if (rows.length === 0) return " (none)";
|
|
5830
|
+
const widths = rows[0].map((_, col) => Math.max(...rows.map((r) => (r[col] ?? "").length)));
|
|
5831
|
+
return rows.map((r) => " " + r.map((cell, i) => (cell ?? "").padEnd(widths[i])).join(" ")).join("\n");
|
|
5832
|
+
}
|
|
5833
|
+
var SHOW_KINDS = ["plans", "entitlements", "segments", "placements", "trials"];
|
|
5834
|
+
function renderShow(kind, config) {
|
|
5835
|
+
const arr = (key) => Array.isArray(config[key]) ? config[key] : [];
|
|
5836
|
+
const handleOf = (item) => item.handle ?? item.unique_handle ?? item.id ?? "";
|
|
5837
|
+
switch (kind) {
|
|
5838
|
+
case "plans": {
|
|
5839
|
+
const plans = arr("plans");
|
|
5840
|
+
const addons = arr("addons");
|
|
5841
|
+
const ents = arr("entitlements");
|
|
5842
|
+
const data = { plans, addons, entitlements: ents };
|
|
5843
|
+
const text = [
|
|
5844
|
+
`plans (${plans.length}):`,
|
|
5845
|
+
table(plans.map((p) => [handleOf(p), p.name ?? "", `tier ${p.tier_position ?? "-"}`])),
|
|
5846
|
+
`addons (${addons.length}):`,
|
|
5847
|
+
table(addons.map((a) => [handleOf(a), a.name ?? ""])),
|
|
5848
|
+
`entitlements (${ents.length}):`,
|
|
5849
|
+
table(ents.map((e) => [handleOf(e), e.name ?? "", e.type ?? ""]))
|
|
5850
|
+
].join("\n");
|
|
5851
|
+
return { data, text };
|
|
5852
|
+
}
|
|
5853
|
+
case "entitlements": {
|
|
5854
|
+
const ents = arr("entitlements");
|
|
5855
|
+
return { data: ents, text: table(ents.map((e) => [handleOf(e), e.name ?? "", e.type ?? ""])) };
|
|
5856
|
+
}
|
|
5857
|
+
case "segments": {
|
|
5858
|
+
const segs = arr("segments");
|
|
5859
|
+
return {
|
|
5860
|
+
data: segs,
|
|
5861
|
+
text: table(segs.map((s) => [handleOf(s), s.name ?? "", `${(s.predicates ?? []).length} predicate(s)`]))
|
|
5862
|
+
};
|
|
5863
|
+
}
|
|
5864
|
+
case "placements": {
|
|
5865
|
+
const pls = arr("placements");
|
|
5866
|
+
return {
|
|
5867
|
+
data: pls,
|
|
5868
|
+
text: table(
|
|
5869
|
+
pls.map((p) => [p.name ?? handleOf(p), p.category ?? "", p.trigger?.type ?? "", `${(p.payloads ?? []).length} payload(s)`])
|
|
5870
|
+
)
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
case "trials": {
|
|
5874
|
+
const free = arr("free_trial_rules");
|
|
5875
|
+
const reverse = arr("reverse_trial_rules");
|
|
5876
|
+
const data = { free_trial_rules: free, reverse_trial_rules: reverse };
|
|
5877
|
+
const text = [
|
|
5878
|
+
`free_trial_rules (${free.length}):`,
|
|
5879
|
+
table(free.map((r) => [handleOf(r), r.name ?? ""])),
|
|
5880
|
+
`reverse_trial_rules (${reverse.length}):`,
|
|
5881
|
+
table(reverse.map((r) => [handleOf(r), r.name ?? ""]))
|
|
5882
|
+
].join("\n");
|
|
5883
|
+
return { data, text };
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5693
5887
|
var pkgVersion = createRequire(import.meta.url)("../package.json").version ?? "0.0.0";
|
|
5694
5888
|
var HELP_AFTER = `
|
|
5695
5889
|
Command groups:
|
|
5696
|
-
Auth
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5890
|
+
Auth & meta login, logout, signup, whoami, schema, docs
|
|
5891
|
+
Download download
|
|
5892
|
+
Check validate, diff, show
|
|
5893
|
+
Stage/launch upload, launch, discard, rollback
|
|
5894
|
+
Inspect status, history, preview, evaluate
|
|
5895
|
+
|
|
5896
|
+
Version selectors (no defaults \u2014 a command that reads a config requires one):
|
|
5897
|
+
<file> a local Config File (positional, or --file <path>)
|
|
5898
|
+
--draft the tenant's single open draft (resolved automatically)
|
|
5899
|
+
--live the current live Release
|
|
5900
|
+
--release <id> a specific playbook version / Release
|
|
5702
5901
|
|
|
5703
5902
|
Common workflows:
|
|
5704
5903
|
# Author, validate, and ship against the default instance (revturbine.com/app)
|
|
5705
5904
|
revturbine login
|
|
5706
|
-
revturbine
|
|
5707
|
-
revturbine
|
|
5708
|
-
revturbine
|
|
5905
|
+
revturbine download --live --save ./config.json
|
|
5906
|
+
revturbine validate ./config.json
|
|
5907
|
+
revturbine diff ./config.json --live
|
|
5908
|
+
revturbine launch ./config.json
|
|
5709
5909
|
|
|
5710
|
-
#
|
|
5711
|
-
revturbine
|
|
5712
|
-
revturbine
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
revturbine upload ./export-config.json # stage a draft playbook version
|
|
5716
|
-
revturbine preview <playbook-version-id> # inspect runtime impact
|
|
5717
|
-
revturbine submit <playbook-version-id>
|
|
5718
|
-
revturbine approve <playbook-version-id>
|
|
5719
|
-
revturbine deploy <playbook-version-id>
|
|
5910
|
+
# Manual review flow (stage \u2192 inspect \u2192 launch)
|
|
5911
|
+
revturbine upload ./config.json # stage as the open draft
|
|
5912
|
+
revturbine preview # inspect the open draft
|
|
5913
|
+
revturbine validate --draft # full server catalog
|
|
5914
|
+
revturbine launch --draft
|
|
5720
5915
|
|
|
5721
5916
|
# Inspect and roll back
|
|
5722
5917
|
revturbine status
|
|
5918
|
+
revturbine history
|
|
5723
5919
|
revturbine rollback <playbook-version-id>
|
|
5724
5920
|
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
the /app subfolder is required for API routing. Pass an explicit URL (with its
|
|
5728
|
-
/app path) to target another instance.
|
|
5921
|
+
Exit-code classes: 0 ok \xB7 1 unexpected \xB7 2 usage \xB7 3 auth \xB7 4 validation
|
|
5922
|
+
blocked \xB7 5 conflict/stale \xB7 6 network \xB7 7 server error.
|
|
5729
5923
|
|
|
5730
5924
|
Auth:
|
|
5731
5925
|
Most commands need a token \u2014 run \`login\` first. Credentials live at
|
|
5732
5926
|
~/.revturbine/credentials.json (0600); the token's tenant is used by default,
|
|
5733
|
-
override with -t/--tenant-id. Mutating commands (discard, rollback
|
|
5734
|
-
|
|
5927
|
+
override with -t/--tenant-id. Mutating commands (discard, rollback) prompt
|
|
5928
|
+
for confirmation unless --yes.
|
|
5735
5929
|
|
|
5736
|
-
Full reference:
|
|
5930
|
+
Full reference: ${DOCS_URL}
|
|
5737
5931
|
`;
|
|
5738
5932
|
var program = new Command();
|
|
5739
5933
|
program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
5740
5934
|
const opts = actionCommand.opts();
|
|
5935
|
+
if (actionCommand.name() === "validate" && !opts.draft) return;
|
|
5741
5936
|
if (!shouldTrackCommandExecution(actionCommand.name(), Boolean(opts.url))) return;
|
|
5742
5937
|
await trackEvent(opts.url, opts.tenantId, "cli_command_executed", {
|
|
5743
5938
|
command: actionCommand.name()
|
|
5744
5939
|
});
|
|
5745
5940
|
});
|
|
5746
|
-
program.name("revturbine").description("
|
|
5941
|
+
program.name("revturbine").description("Validate RevTurbine Config Files and ship them through the playbook-version lifecycle (draft \u2192 Release).").version(`${pkgVersion} (schema ${SCHEMA_VERSION})`, "-V, --version", "Print the revturbine and bundled schema versions").showHelpAfterError().addHelpText("after", HELP_AFTER);
|
|
5747
5942
|
program.command("login").description("Authorize this machine via the browser (device flow) and store a token for the instance (default: the production instance).").argument("[url]", `RevTurbine instance URL (default: ${DEFAULT_URL})`).action(async (url) => {
|
|
5748
5943
|
try {
|
|
5749
5944
|
const base = normalizeBaseUrl(url ?? DEFAULT_URL);
|
|
5750
5945
|
await deviceLogin(base);
|
|
5751
5946
|
await trackEvent(base, void 0, "cli_signed_in");
|
|
5752
5947
|
} catch (err) {
|
|
5753
|
-
|
|
5754
|
-
process.exit(1);
|
|
5948
|
+
fail(EXIT.AUTH, `Login failed: ${err.message}`);
|
|
5755
5949
|
}
|
|
5756
5950
|
});
|
|
5757
5951
|
async function promptLine(question) {
|
|
5758
|
-
const rl = createInterface({ input: process.stdin, output: process.
|
|
5952
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
5759
5953
|
try {
|
|
5760
5954
|
return (await rl.question(question)).trim();
|
|
5761
5955
|
} finally {
|
|
@@ -5763,10 +5957,10 @@ async function promptLine(question) {
|
|
|
5763
5957
|
}
|
|
5764
5958
|
}
|
|
5765
5959
|
function promptHidden(question) {
|
|
5766
|
-
const { stdin,
|
|
5960
|
+
const { stdin, stderr } = process;
|
|
5767
5961
|
if (!stdin.isTTY) return promptLine(question);
|
|
5768
5962
|
return new Promise((resolve) => {
|
|
5769
|
-
|
|
5963
|
+
stderr.write(question);
|
|
5770
5964
|
const wasRaw = stdin.isRaw;
|
|
5771
5965
|
stdin.setRawMode(true);
|
|
5772
5966
|
stdin.resume();
|
|
@@ -5782,24 +5976,24 @@ function promptHidden(question) {
|
|
|
5782
5976
|
const code = ch.charCodeAt(0);
|
|
5783
5977
|
if (code === 13 || code === 10 || code === 4) {
|
|
5784
5978
|
cleanup();
|
|
5785
|
-
|
|
5979
|
+
stderr.write("\n");
|
|
5786
5980
|
resolve(input.trim());
|
|
5787
5981
|
return;
|
|
5788
5982
|
}
|
|
5789
5983
|
if (code === 3) {
|
|
5790
5984
|
cleanup();
|
|
5791
|
-
|
|
5985
|
+
stderr.write("\n");
|
|
5792
5986
|
process.exit(130);
|
|
5793
5987
|
}
|
|
5794
5988
|
if (code === 127 || code === 8) {
|
|
5795
5989
|
if (input.length > 0) {
|
|
5796
5990
|
input = input.slice(0, -1);
|
|
5797
|
-
|
|
5991
|
+
stderr.write("\b \b");
|
|
5798
5992
|
}
|
|
5799
5993
|
continue;
|
|
5800
5994
|
}
|
|
5801
5995
|
input += ch;
|
|
5802
|
-
|
|
5996
|
+
stderr.write("*");
|
|
5803
5997
|
}
|
|
5804
5998
|
};
|
|
5805
5999
|
stdin.on("data", onData);
|
|
@@ -5812,8 +6006,7 @@ program.command("signup").description("Create a RevTurbine account and log in (e
|
|
|
5812
6006
|
const email = opts.email ?? await promptLine("Email: ");
|
|
5813
6007
|
const password = opts.password ?? await promptHidden("Password: ");
|
|
5814
6008
|
if (!name || !email || password.length < 8) {
|
|
5815
|
-
|
|
5816
|
-
process.exit(1);
|
|
6009
|
+
fail(EXIT.USAGE, "Name, email, and a password of at least 8 characters are required.");
|
|
5817
6010
|
}
|
|
5818
6011
|
const result = await signup({
|
|
5819
6012
|
baseUrl,
|
|
@@ -5825,290 +6018,314 @@ program.command("signup").description("Create a RevTurbine account and log in (e
|
|
|
5825
6018
|
if (result.status === "awaiting_invitation") process.exit(0);
|
|
5826
6019
|
await trackEvent(baseUrl, void 0, "cli_signed_up");
|
|
5827
6020
|
} catch (err) {
|
|
5828
|
-
|
|
5829
|
-
process.exit(1);
|
|
6021
|
+
fail(EXIT.AUTH, `Signup failed: ${err.message}`);
|
|
5830
6022
|
}
|
|
5831
6023
|
});
|
|
5832
6024
|
program.command("logout").description("Remove the stored token for <url>.").argument("[url]", `RevTurbine instance URL (default: ${DEFAULT_URL})`).action((url) => {
|
|
5833
6025
|
const normalized = normalizeBaseUrl(url ?? DEFAULT_URL);
|
|
5834
6026
|
const removed = removeCredential(normalized);
|
|
5835
|
-
|
|
5836
|
-
});
|
|
5837
|
-
program.command("verify").description("Schema-validate one or more ExportedConfig files (offline).").argument("<config...>", "Path(s) to an export-config.json file").action((configs) => {
|
|
5838
|
-
const failures = configs.filter((file) => verifyConfig(file) === null).length;
|
|
5839
|
-
if (configs.length > 1) console.log(`
|
|
5840
|
-
${LOG} ${configs.length - failures}/${configs.length} passed.`);
|
|
5841
|
-
process.exit(failures > 0 ? 1 : 0);
|
|
6027
|
+
diag(removed ? `Logged out of ${normalized}.` : `No stored credential for ${normalized}.`);
|
|
5842
6028
|
});
|
|
5843
|
-
program.command("
|
|
5844
|
-
const config = verifyConfig(configFile2);
|
|
5845
|
-
if (config === null) process.exit(1);
|
|
6029
|
+
program.command("whoami").description("Show the resolved instance, tenant, credentials source, and whether the stored token works.").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) => {
|
|
5846
6030
|
const conn = connect(opts.url, opts.tenantId);
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
${
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
}
|
|
5871
|
-
}
|
|
5872
|
-
if (!dryRes.ok || dry.ok === false) {
|
|
5873
|
-
authHint(conn.url, dryRes.status);
|
|
5874
|
-
console.error(`
|
|
5875
|
-
${LOG} Dry-run found problems. Fix them before uploading.`);
|
|
5876
|
-
process.exit(1);
|
|
5877
|
-
}
|
|
5878
|
-
console.log(
|
|
5879
|
-
`
|
|
5880
|
-
${LOG} \u2713 No conflicts. Stage it with:
|
|
5881
|
-
revturbine upload ${configFile2} --url ${conn.url} --deploy`
|
|
6031
|
+
let tokenValid = null;
|
|
6032
|
+
if (conn.hasToken) {
|
|
6033
|
+
const probe = await resolveActiveDraft(conn.url, conn.headers).catch(() => ({ ok: false, status: 0, draft: null }));
|
|
6034
|
+
tokenValid = probe.ok || probe.status !== 401 && probe.status !== 403 && probe.status !== 0;
|
|
6035
|
+
}
|
|
6036
|
+
const data = {
|
|
6037
|
+
instance: conn.url,
|
|
6038
|
+
tenant: conn.tenantId,
|
|
6039
|
+
tenant_source: conn.tenantSource,
|
|
6040
|
+
credentials_dir: conn.credentialsDir,
|
|
6041
|
+
credentials_source: conn.credentialsSource,
|
|
6042
|
+
token_present: conn.hasToken,
|
|
6043
|
+
token_valid: tokenValid
|
|
6044
|
+
};
|
|
6045
|
+
emit(
|
|
6046
|
+
data,
|
|
6047
|
+
Boolean(opts.json),
|
|
6048
|
+
[
|
|
6049
|
+
`instance: ${data.instance}`,
|
|
6050
|
+
`tenant: ${data.tenant} (${data.tenant_source})`,
|
|
6051
|
+
`credentials: ${data.credentials_dir} [${data.credentials_source}]`,
|
|
6052
|
+
`token: ${data.token_present ? tokenValid ? "present, valid" : "present, NOT accepted" : "absent \u2014 run `revturbine login`"}`
|
|
6053
|
+
].join("\n")
|
|
5882
6054
|
);
|
|
5883
6055
|
});
|
|
5884
|
-
program.command("
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
6056
|
+
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
|
+
try {
|
|
6058
|
+
const jsonSchema = z27.toJSONSchema(RevTurbineConfigSchema, { unrepresentable: "any" });
|
|
6059
|
+
emit({ schema_version: SCHEMA_VERSION, schema: jsonSchema }, true);
|
|
6060
|
+
} catch (err) {
|
|
6061
|
+
fail(EXIT.UNEXPECTED, `could not render the bundled schema (${SCHEMA_VERSION}): ${err.message}`);
|
|
5890
6062
|
}
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
if (
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
6063
|
+
});
|
|
6064
|
+
program.command("docs").description("Print the canonical documentation URL (and open it in a browser when interactive).").action(() => {
|
|
6065
|
+
process.stdout.write(`${DOCS_URL}
|
|
6066
|
+
`);
|
|
6067
|
+
if (process.stdout.isTTY) {
|
|
6068
|
+
const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", DOCS_URL]] : process.platform === "darwin" ? ["open", [DOCS_URL]] : ["xdg-open", [DOCS_URL]];
|
|
6069
|
+
try {
|
|
6070
|
+
spawn2(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
6071
|
+
} catch {
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
});
|
|
6075
|
+
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").addOption(new Option("-f, --format <format>", "Representation").choices(["json", "flatbuffer"]).default("json")).option("--save <file>", "Write to <file> instead of stdout").action(
|
|
6076
|
+
async (opts) => {
|
|
6077
|
+
const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["draft", "live", "release"], command: "download" });
|
|
6078
|
+
const conn = connect(opts.url, opts.tenantId);
|
|
6079
|
+
const playbookVersionId = sel.kind === "release" ? sel.id : sel.kind === "draft" ? await requireOpenDraft(conn) : void 0;
|
|
6080
|
+
if (opts.format === "flatbuffer") {
|
|
6081
|
+
const qs = playbookVersionId ? `?playbookVersionId=${encodeURIComponent(playbookVersionId)}` : "";
|
|
6082
|
+
const res = await request(conn, `/api/config/bundle${qs}`);
|
|
6083
|
+
if (!res.ok) httpFail(conn, "bundle download", res.status);
|
|
6084
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
6085
|
+
if (opts.save) {
|
|
6086
|
+
const out2 = path2.resolve(opts.save);
|
|
6087
|
+
mkdirSync2(path2.dirname(out2), { recursive: true });
|
|
6088
|
+
writeFileSync2(out2, bytes);
|
|
6089
|
+
diag(`\u2713 Wrote ${bytes.length} bytes \u2192 ${out2}`);
|
|
6090
|
+
} else {
|
|
6091
|
+
process.stdout.write(`${bytes.toString("base64")}
|
|
6092
|
+
`);
|
|
6093
|
+
}
|
|
6094
|
+
return;
|
|
6095
|
+
}
|
|
6096
|
+
const config = await downloadConfig(conn, playbookVersionId);
|
|
6097
|
+
const out = `${JSON.stringify(config, null, 2)}
|
|
6098
|
+
`;
|
|
6099
|
+
if (opts.save) {
|
|
6100
|
+
const file = path2.resolve(opts.save);
|
|
6101
|
+
mkdirSync2(path2.dirname(file), { recursive: true });
|
|
6102
|
+
writeFileSync2(file, out);
|
|
6103
|
+
diag(`\u2713 Wrote ${file}`);
|
|
5900
6104
|
} else {
|
|
5901
|
-
|
|
6105
|
+
process.stdout.write(out);
|
|
5902
6106
|
}
|
|
5903
|
-
authHint(conn.url, res.status);
|
|
5904
|
-
process.exit(1);
|
|
5905
6107
|
}
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
if (
|
|
5910
|
-
|
|
5911
|
-
console.log(
|
|
5912
|
-
`
|
|
5913
|
-
${LOG} Staged as a draft. Activate it with:
|
|
5914
|
-
` + (playbookVersionId ? ` revturbine deploy ${playbookVersionId} --url ${conn.url}
|
|
5915
|
-
` : "") + " \u2026or from the RevTurbine UI (Drafts & Releases)."
|
|
5916
|
-
);
|
|
5917
|
-
return;
|
|
6108
|
+
);
|
|
6109
|
+
program.command("validate").description("Validate a Config File offline (schema), or the open draft against the full server catalog (--draft).").argument("[file...]", "Path(s) to a Config File (offline mode)").option("--draft", "Validate the open draft server-side (full catalog)").option("-u, --url <url>", "RevTurbine instance URL (used with --draft)", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(async (files, opts) => {
|
|
6110
|
+
if (opts.draft && files.length > 0) fail(EXIT.USAGE, "validate takes either <file> or --draft, not both.");
|
|
6111
|
+
if (!opts.draft && files.length === 0) {
|
|
6112
|
+
throw new SelectorError("STATE_REQUIRED \u2014 validate needs a version: <file> (offline) or --draft (server catalog).");
|
|
5918
6113
|
}
|
|
5919
|
-
if (!
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
);
|
|
5923
|
-
return;
|
|
6114
|
+
if (!opts.draft) {
|
|
6115
|
+
const failures = files.filter((file) => verifyConfig(file) === null).length;
|
|
6116
|
+
if (files.length > 1) diag(`${files.length - failures}/${files.length} passed.`);
|
|
6117
|
+
process.exit(failures > 0 ? EXIT.VALIDATION : 0);
|
|
5924
6118
|
}
|
|
5925
|
-
await activatePlaybookVersion(conn, playbookVersionId);
|
|
5926
|
-
});
|
|
5927
|
-
program.command("deploy").description("Activate a staged draft playbook version: submit \u2192 approve \u2192 deploy.").argument("<playbook-version-id>", "The playbook_version_id returned by `upload`").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/rollback; deploy has no confirmation prompt").action(async (playbookVersionId, opts) => {
|
|
5928
|
-
const conn = connect(opts.url, opts.tenantId);
|
|
5929
|
-
await activatePlaybookVersion(conn, playbookVersionId);
|
|
5930
|
-
});
|
|
5931
|
-
program.command("validate").description("Run config-validation against a staged draft. Prints findings; exits non-zero if any block.").argument("<playbook-version-id>", "The playbook_version_id to validate").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(async (playbookVersionId, opts) => {
|
|
5932
6119
|
const conn = connect(opts.url, opts.tenantId);
|
|
6120
|
+
const playbookVersionId = await requireOpenDraft(conn);
|
|
5933
6121
|
const result = await fetchValidation(conn.url, playbookVersionId, conn.headers);
|
|
5934
|
-
if (!result.ok)
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
}
|
|
5939
|
-
console.log(`
|
|
5940
|
-
${LOG} Validation for playbook version ${playbookVersionId}:`);
|
|
5941
|
-
console.log(formatFindings(result.findings));
|
|
6122
|
+
if (!result.ok) httpFail(conn, "validate", result.status, result.error);
|
|
6123
|
+
diag(`Validation for the open draft (${playbookVersionId}):`);
|
|
6124
|
+
process.stdout.write(`${formatFindings(result.findings)}
|
|
6125
|
+
`);
|
|
5942
6126
|
if (hasBlockingFindings(result.findings)) {
|
|
5943
|
-
const blocking = result.findings.filter((f) => f.severity === "error_draft" || f.severity === "
|
|
5944
|
-
|
|
5945
|
-
process.exit(1);
|
|
6127
|
+
const blocking = result.findings.filter((f) => f.severity === "error_draft" || f.severity === "error_launch");
|
|
6128
|
+
fail(EXIT.VALIDATION, `${blocking.length} blocking finding(s) \u2014 this draft cannot launch.`);
|
|
5946
6129
|
}
|
|
5947
|
-
|
|
6130
|
+
diag("\u2713 No blocking findings.");
|
|
5948
6131
|
});
|
|
5949
|
-
program.command("
|
|
5950
|
-
const
|
|
5951
|
-
const
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
authHint(conn.url, res2.status);
|
|
5957
|
-
process.exit(1);
|
|
5958
|
-
}
|
|
5959
|
-
const bytes = Buffer.from(await res2.arrayBuffer());
|
|
5960
|
-
if (opts.save) {
|
|
5961
|
-
const out2 = path2.resolve(opts.save);
|
|
5962
|
-
mkdirSync2(path2.dirname(out2), { recursive: true });
|
|
5963
|
-
writeFileSync2(out2, bytes);
|
|
5964
|
-
console.log(`${LOG} \u2713 Wrote ${bytes.length} bytes \u2192 ${out2}`);
|
|
5965
|
-
} else {
|
|
5966
|
-
process.stdout.write(`${bytes.toString("base64")}
|
|
6132
|
+
program.command("diff").description("Compare two config versions (first \u2192 second). Dry-run, no writes.").argument("[file...]", "Local Config File path(s)").option("--draft", "The tenant's open draft").option("--live", "The current live Release").option("--release <id>", "A specific playbook version / Release").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(async (files, opts) => {
|
|
6133
|
+
const sels = requireSelectors(opts, files, { count: 2, allowed: ["file", "draft", "live", "release"], command: "diff" });
|
|
6134
|
+
const needsServer = sels.some((s) => s.kind !== "file");
|
|
6135
|
+
const conn = needsServer ? connect(opts.url, opts.tenantId) : null;
|
|
6136
|
+
const [a, b] = await Promise.all(sels.map((s) => loadVersion(conn, s)));
|
|
6137
|
+
diag(`Diff (${sels.map((s) => s.kind === "file" ? s.path : s.kind === "release" ? `--release ${s.id}` : `--${s.kind}`).join(" \u2192 ")}):`);
|
|
6138
|
+
process.stdout.write(`${formatDiff(diffExportedConfig(a, b))}
|
|
5967
6139
|
`);
|
|
6140
|
+
});
|
|
6141
|
+
program.command("show").description(`Render a summary view of a config version. <kind> \u2208 ${SHOW_KINDS.join(" | ")}.`).argument("<kind>", `One of: ${SHOW_KINDS.join(", ")}`).option("--file <path>", "A local Config File").option("--draft", "The tenant's open draft").option("--live", "The current live Release").option("--release <id>", "A specific playbook version / Release").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(
|
|
6142
|
+
async (kind, opts) => {
|
|
6143
|
+
if (!SHOW_KINDS.includes(kind)) {
|
|
6144
|
+
fail(EXIT.USAGE, `unknown kind '${kind}' \u2014 use one of: ${SHOW_KINDS.join(", ")}.`);
|
|
5968
6145
|
}
|
|
5969
|
-
|
|
6146
|
+
const [sel] = requireSelectors(opts, [], { count: 1, allowed: ["file", "draft", "live", "release"], command: "show" });
|
|
6147
|
+
const needsServer = sel.kind !== "file";
|
|
6148
|
+
const conn = needsServer ? connect(opts.url, opts.tenantId) : null;
|
|
6149
|
+
const config = await loadVersion(conn, sel);
|
|
6150
|
+
const { data, text } = renderShow(kind, config);
|
|
6151
|
+
emit(data, Boolean(opts.json), text);
|
|
5970
6152
|
}
|
|
5971
|
-
|
|
6153
|
+
);
|
|
6154
|
+
program.command("upload").description("Stage a Config File as the open draft (POST /api/config/import).").argument("<config>", "Path to a Config File").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(async (configFile2, opts) => {
|
|
6155
|
+
const config = verifyConfig(configFile2);
|
|
6156
|
+
if (config === null) fail(EXIT.VALIDATION, `Fix the issues above in ${configFile2}, then re-run.`);
|
|
6157
|
+
const conn = connect(opts.url, opts.tenantId);
|
|
6158
|
+
diag(`Staging ${configFile2} as the open draft (${conn.url}/api/config/import) \u2026`);
|
|
6159
|
+
const { res, json } = await postJson(conn, "/api/config/import", config);
|
|
5972
6160
|
if (!res.ok) {
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
const file = path2.resolve(opts.save);
|
|
5981
|
-
mkdirSync2(path2.dirname(file), { recursive: true });
|
|
5982
|
-
writeFileSync2(file, out);
|
|
5983
|
-
console.log(`${LOG} \u2713 Wrote ${file}`);
|
|
5984
|
-
} else {
|
|
5985
|
-
process.stdout.write(out);
|
|
6161
|
+
if (json && typeof json === "object" && json.stage) {
|
|
6162
|
+
diagRaw(` stage: ${json.stage}`);
|
|
6163
|
+
diagRaw(` detail: ${JSON.stringify(json.detail)}`);
|
|
6164
|
+
} else {
|
|
6165
|
+
diagRaw(` ${JSON.stringify(json)}`);
|
|
6166
|
+
}
|
|
6167
|
+
httpFail(conn, "upload", res.status);
|
|
5986
6168
|
}
|
|
6169
|
+
const playbookVersionId = json?.playbook_version_id;
|
|
6170
|
+
diag(`\u2713 Staged ${configFile2} as the open draft (${res.status})`);
|
|
6171
|
+
diagRaw(` imported: ${JSON.stringify(json.imported ?? {})}`);
|
|
6172
|
+
if (playbookVersionId) diagRaw(` playbook_version_id: ${playbookVersionId}`);
|
|
6173
|
+
diag("Launch it with `revturbine launch --draft`, or from the UI (Drafts & Releases).");
|
|
5987
6174
|
});
|
|
5988
|
-
program.command("
|
|
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/rollback; launch has no confirmation prompt").action(async (file, opts) => {
|
|
6176
|
+
const [sel] = requireSelectors(opts, file ? [file] : [], { count: 1, allowed: ["file", "draft"], command: "launch" });
|
|
5989
6177
|
const conn = connect(opts.url, opts.tenantId);
|
|
5990
|
-
|
|
5991
|
-
if (
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
${
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6178
|
+
let playbookVersionId;
|
|
6179
|
+
if (sel.kind === "file") {
|
|
6180
|
+
const config = verifyConfig(sel.path);
|
|
6181
|
+
if (config === null) fail(EXIT.VALIDATION, `Fix the issues above in ${sel.path}, then re-run.`);
|
|
6182
|
+
diag(`Staging ${sel.path} as the open draft \u2026`);
|
|
6183
|
+
const { res, json } = await postJson(conn, "/api/config/import", config);
|
|
6184
|
+
if (!res.ok) {
|
|
6185
|
+
diagRaw(` ${JSON.stringify(json)}`);
|
|
6186
|
+
httpFail(conn, "upload", res.status);
|
|
6187
|
+
}
|
|
6188
|
+
playbookVersionId = json?.playbook_version_id;
|
|
6189
|
+
if (!playbookVersionId) fail(EXIT.SERVER, "No playbook_version_id returned by the import \u2014 cannot launch.");
|
|
6190
|
+
diag(`\u2713 Staged (${playbookVersionId})`);
|
|
6191
|
+
} else {
|
|
6192
|
+
playbookVersionId = await requireOpenDraft(conn);
|
|
6193
|
+
}
|
|
6194
|
+
await launchDraft(conn, playbookVersionId);
|
|
6003
6195
|
});
|
|
6004
|
-
program.command("discard").description("Discard (archive)
|
|
6196
|
+
program.command("discard").description("Discard (archive) the open draft so a fresh one can start.").argument("[playbook-version-id]", "A specific draft id (default: the open draft)").option("--draft", "The open draft (explicit form of the default)").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 (id, opts) => {
|
|
6005
6197
|
const conn = connect(opts.url, opts.tenantId);
|
|
6006
|
-
|
|
6198
|
+
const playbookVersionId = id ?? await requireOpenDraft(conn);
|
|
6199
|
+
await confirmOrExit(`Discard draft ${playbookVersionId} on ${conn.url}?`, !!opts.yes);
|
|
6007
6200
|
const { res, json } = await postJson(conn, `/api/playbook-versions/${playbookVersionId}/archive`, {});
|
|
6008
|
-
if (!res.ok)
|
|
6009
|
-
|
|
6010
|
-
authHint(conn.url, res.status);
|
|
6011
|
-
process.exit(1);
|
|
6012
|
-
}
|
|
6013
|
-
console.log(`${LOG} \u2713 Discarded playbook version ${playbookVersionId}.`);
|
|
6201
|
+
if (!res.ok) httpFail(conn, "discard", res.status, json);
|
|
6202
|
+
diag(`\u2713 Discarded draft ${playbookVersionId}.`);
|
|
6014
6203
|
});
|
|
6015
|
-
program.command("rollback").description("Roll back a deployed playbook version
|
|
6204
|
+
program.command("rollback").description("Roll back a deployed playbook version (creates a reverting draft). Becomes `restore <release>` once server-side entry copying lands (plan 131 TASK-6).").argument("<playbook-version-id>", "The deployed playbook_version_id to revert").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) => {
|
|
6016
6205
|
const conn = connect(opts.url, opts.tenantId);
|
|
6017
6206
|
await confirmOrExit(
|
|
6018
6207
|
`Roll back playbook version ${playbookVersionId} on ${conn.url}? This changes the LIVE configuration.`,
|
|
6019
6208
|
!!opts.yes
|
|
6020
6209
|
);
|
|
6021
6210
|
const { res, json } = await postJson(conn, `/api/playbook-versions/${playbookVersionId}/rollback`, {});
|
|
6022
|
-
if (!res.ok)
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
}
|
|
6027
|
-
console.log(`${LOG} \u2713 Rollback requested for playbook version ${playbookVersionId}.`);
|
|
6028
|
-
if (json?.playbook_version_id) console.log(` reverting playbook_version_id: ${json.playbook_version_id}`);
|
|
6211
|
+
if (!res.ok) httpFail(conn, "rollback", res.status, json);
|
|
6212
|
+
diag(`\u2713 Rollback requested for playbook version ${playbookVersionId}.`);
|
|
6213
|
+
const reverting = json?.item?.id ?? json?.playbook_version_id;
|
|
6214
|
+
if (reverting) diagRaw(` reverting draft: ${reverting}`);
|
|
6029
6215
|
});
|
|
6030
|
-
program.command("
|
|
6216
|
+
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) => {
|
|
6031
6217
|
const conn = connect(opts.url, opts.tenantId);
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6218
|
+
const drafts = await getJson(conn, "/api/optimization/drafts");
|
|
6219
|
+
if (!drafts.res.ok) httpFail(conn, "draft lookup", drafts.res.status);
|
|
6220
|
+
const releases = await getJson(conn, "/api/optimization/releases");
|
|
6221
|
+
const items = releases.res.ok && Array.isArray(releases.json?.items) ? releases.json.items : [];
|
|
6222
|
+
const live = items.find((r) => r.status === "live") ?? items[0] ?? null;
|
|
6223
|
+
const draft = drafts.json?.active ?? null;
|
|
6224
|
+
const parked = Array.isArray(drafts.json?.parked) ? drafts.json.parked : [];
|
|
6225
|
+
const data = { live, draft, parked };
|
|
6226
|
+
const liveLine = live ? `v${live.version ?? "?"} "${live.playbook_version_name ?? live.name ?? ""}" (${live.playbook_version_id ?? live.id}) released ${live.released_at ?? "?"}` : "(none)";
|
|
6227
|
+
const draftLine = draft ? `"${draft.name ?? ""}" (${draft.id}) status ${draft.status ?? "draft"}` : "(none)";
|
|
6228
|
+
emit(
|
|
6229
|
+
data,
|
|
6230
|
+
Boolean(opts.json),
|
|
6231
|
+
[
|
|
6232
|
+
`Live release: ${liveLine}`,
|
|
6233
|
+
`Open draft: ${draftLine}`,
|
|
6234
|
+
`Parked: ${parked.length === 0 ? "(none)" : `${parked.length} draft(s)`}`
|
|
6235
|
+
].join("\n")
|
|
6236
|
+
);
|
|
6050
6237
|
});
|
|
6051
|
-
program.command("
|
|
6238
|
+
program.command("history").description("The Release Version Log: recent Releases, newest first.").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) => {
|
|
6052
6239
|
const conn = connect(opts.url, opts.tenantId);
|
|
6240
|
+
const { res, json } = await getJson(conn, "/api/optimization/releases");
|
|
6241
|
+
if (!res.ok) httpFail(conn, "history", res.status);
|
|
6242
|
+
const items = Array.isArray(json?.items) ? json.items : [];
|
|
6243
|
+
emit(
|
|
6244
|
+
items,
|
|
6245
|
+
Boolean(opts.json),
|
|
6246
|
+
table(
|
|
6247
|
+
items.map((r) => [
|
|
6248
|
+
`v${r.version ?? "?"}`,
|
|
6249
|
+
r.status ?? "",
|
|
6250
|
+
(r.released_at ?? "").slice(0, 19),
|
|
6251
|
+
r.playbook_version_name ?? r.name ?? "",
|
|
6252
|
+
r.playbook_version_id ?? r.id ?? ""
|
|
6253
|
+
])
|
|
6254
|
+
)
|
|
6255
|
+
);
|
|
6256
|
+
});
|
|
6257
|
+
program.command("preview").description("The open draft's staged changes (runtime-impact summary once plan 78 lands).").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 (default shape is already JSON)").action(async (opts) => {
|
|
6258
|
+
const conn = connect(opts.url, opts.tenantId);
|
|
6259
|
+
const playbookVersionId = await requireOpenDraft(conn);
|
|
6053
6260
|
const { res, json } = await getJson(conn, `/api/playbook-versions/${playbookVersionId}/preview`);
|
|
6054
|
-
if (!res.ok)
|
|
6055
|
-
|
|
6056
|
-
authHint(conn.url, res.status);
|
|
6057
|
-
process.exit(1);
|
|
6058
|
-
}
|
|
6059
|
-
console.log(JSON.stringify(json, null, 2));
|
|
6261
|
+
if (!res.ok) httpFail(conn, "preview", res.status, json);
|
|
6262
|
+
emit(json, true);
|
|
6060
6263
|
});
|
|
6061
|
-
for (
|
|
6062
|
-
program.command(step).description(`Transition a playbook version: ${step}.`).argument("<playbook-version-id>", "The playbook_version_id").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(async (playbookVersionId, opts) => {
|
|
6063
|
-
const conn = connect(opts.url, opts.tenantId);
|
|
6064
|
-
const { res, json } = await postJson(conn, `/api/playbook-versions/${playbookVersionId}/${step}`, {});
|
|
6065
|
-
if (!res.ok) {
|
|
6066
|
-
console.error(`${LOG} \u2717 ${step} failed (${res.status}): ${JSON.stringify(json)}`);
|
|
6067
|
-
authHint(conn.url, res.status);
|
|
6068
|
-
process.exit(1);
|
|
6069
|
-
}
|
|
6070
|
-
console.log(`${LOG} \u2713 ${step} \u2192 playbook version ${playbookVersionId}`);
|
|
6071
|
-
});
|
|
6072
|
-
}
|
|
6073
|
-
program.command("promote").description("Promote a compiled config from one environment to another.").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--from <env>", "Source environment id").requiredOption("--to <env>", "Target environment id").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--yes", "Skip the confirmation prompt").action(async (opts) => {
|
|
6264
|
+
program.command("evaluate").description("Run the live config's placement/entitlement decisions for a user context (from a JSON file). Draft/Release-targeted evaluation lands with plan 131 TASK-7.").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).requiredOption("--user <file>", "JSON file: { user_id, customer_id?, plan_handle?, placement_ids?, entitlement_handles?, traits?, now_iso? }").option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(async (opts) => {
|
|
6074
6265
|
const conn = connect(opts.url, opts.tenantId);
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
})
|
|
6080
|
-
|
|
6081
|
-
console.error(`${LOG} \u2717 promote failed (${res.status}): ${JSON.stringify(json)}`);
|
|
6082
|
-
authHint(conn.url, res.status);
|
|
6083
|
-
process.exit(1);
|
|
6266
|
+
if (!existsSync2(opts.user)) fail(EXIT.USAGE, `user file not found: ${opts.user}`);
|
|
6267
|
+
let body;
|
|
6268
|
+
try {
|
|
6269
|
+
body = JSON.parse(readFileSync2(opts.user, "utf8"));
|
|
6270
|
+
} catch (err) {
|
|
6271
|
+
fail(EXIT.VALIDATION, `invalid JSON in ${opts.user}: ${err.message}`);
|
|
6084
6272
|
}
|
|
6085
|
-
|
|
6273
|
+
const { res, json } = await postJson(conn, "/api/sdk/evaluate", body);
|
|
6274
|
+
if (!res.ok) httpFail(conn, "evaluate", res.status, json);
|
|
6275
|
+
emit(json, true);
|
|
6086
6276
|
});
|
|
6087
6277
|
var COMMAND_EXAMPLES = {
|
|
6088
|
-
|
|
6278
|
+
download: [
|
|
6279
|
+
"",
|
|
6280
|
+
"Examples:",
|
|
6281
|
+
" revturbine download --live --save ./config.json The live config \u2192 file",
|
|
6282
|
+
" revturbine download --draft The open draft (rendered on demand) \u2192 stdout",
|
|
6283
|
+
" revturbine download --release cs_1a2b3c --format flatbuffer --save ./bundle.fb"
|
|
6284
|
+
].join("\n"),
|
|
6285
|
+
validate: [
|
|
6286
|
+
"",
|
|
6287
|
+
"Examples:",
|
|
6288
|
+
" revturbine validate ./config.json Offline schema validation (no network)",
|
|
6289
|
+
" revturbine validate --draft Full server catalog against the open draft"
|
|
6290
|
+
].join("\n"),
|
|
6291
|
+
diff: [
|
|
6089
6292
|
"",
|
|
6090
6293
|
"Examples:",
|
|
6091
|
-
" revturbine
|
|
6092
|
-
" revturbine
|
|
6294
|
+
" revturbine diff ./config.json --live Local edits vs the live config",
|
|
6295
|
+
" revturbine diff --live --draft What the open draft would change"
|
|
6093
6296
|
].join("\n"),
|
|
6094
|
-
|
|
6095
|
-
|
|
6297
|
+
show: ["", "Examples:", " revturbine show plans --live", " revturbine show segments --file ./config.json"].join("\n"),
|
|
6298
|
+
upload: ["", "Example:", " revturbine upload ./config.json Stage as the open draft"].join("\n"),
|
|
6299
|
+
launch: [
|
|
6096
6300
|
"",
|
|
6097
6301
|
"Examples:",
|
|
6098
|
-
" revturbine
|
|
6099
|
-
" revturbine
|
|
6100
|
-
" revturbine export --url <url> --format flatbuffer --save ./bundle.fb Compiled bundle \u2192 file",
|
|
6101
|
-
" revturbine export --url <url> --playbook-version cs_1a2b3c A specific playbook version\u2019s frozen snapshot"
|
|
6302
|
+
" revturbine launch ./config.json Upload, gate, and go live",
|
|
6303
|
+
" revturbine launch --draft Launch the already-open draft"
|
|
6102
6304
|
].join("\n"),
|
|
6103
6305
|
evaluate: [
|
|
6104
6306
|
"",
|
|
6105
6307
|
"Example:",
|
|
6106
6308
|
" revturbine evaluate --url <url> --user ./ctx.json",
|
|
6107
6309
|
' ctx.json: { "user_id": "u1", "plan_handle": "pro", "entitlement_handles": ["seats"] }'
|
|
6108
|
-
].join("\n")
|
|
6109
|
-
promote: ["", "Example:", " revturbine promote --url <url> --from production --to staging"].join("\n")
|
|
6310
|
+
].join("\n")
|
|
6110
6311
|
};
|
|
6111
6312
|
for (const [name, text] of Object.entries(COMMAND_EXAMPLES)) {
|
|
6112
6313
|
program.commands.find((c) => c.name() === name)?.addHelpText("after", text);
|
|
6113
6314
|
}
|
|
6114
|
-
|
|
6315
|
+
program.exitOverride();
|
|
6316
|
+
try {
|
|
6317
|
+
await program.parseAsync(process.argv);
|
|
6318
|
+
} catch (err) {
|
|
6319
|
+
if (err instanceof CommanderError) {
|
|
6320
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version" || err.code === "commander.help") {
|
|
6321
|
+
process.exit(0);
|
|
6322
|
+
}
|
|
6323
|
+
process.exit(EXIT.USAGE);
|
|
6324
|
+
}
|
|
6325
|
+
if (err instanceof SelectorError) {
|
|
6326
|
+
console.error(`[revturbine] \u2717 ${err.message}`);
|
|
6327
|
+
process.exit(EXIT.USAGE);
|
|
6328
|
+
}
|
|
6329
|
+
console.error(`[revturbine] \u2717 ${err.message}`);
|
|
6330
|
+
process.exit(EXIT.UNEXPECTED);
|
|
6331
|
+
}
|
package/package.json
CHANGED