@revturbine/cli 0.19.5 → 0.20.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 +33 -3
- package/dist/cli.js +170 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,13 +65,13 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
65
65
|
|---|---|
|
|
66
66
|
| `init` (alias `create`) | Set up RevTurbine while preserving existing integrations: install missing packages, pin the CLI exactly, create a starter for a fresh integration, and install skills for one agent. Use `--scaffold` to create a missing root starter explicitly; existing files are never overwritten. `--agent claude-code\|cursor\|codex` overrides environment detection; an unknown agent skips skills and prints the selection command. Also supports `--dir`, `--dry-run`, `--no-skills`, `--json`. With no `package.json`, offers a new project (`--yes` for noninteractive creation). Runs the invoked CLI even inside a repo that pins a different one. |
|
|
67
67
|
| `signup` | Create an account headlessly: email + password, then an emailed one-time code to verify, then a token is stored. |
|
|
68
|
-
| `login` / `logout` | Device-flow auth; tokens stored at `~/.revturbine/credentials.json` (mode 0600). |
|
|
69
|
-
| `whoami` | Authentication state first: not logged in, credentials accepted, credentials rejected for the selected instance/tenant, or verification unavailable. Then shows instance, tenant, and credentials source; configured tenant information alone does not prove login. Verification and subsequent telemetry each time out after 3 seconds. Status reports exit 0. `--json` retains its seven fields: `instance`, `tenant
|
|
68
|
+
| `login` / `logout` | Device-flow auth; tokens stored at `~/.revturbine/credentials.json` (mode 0600). For CI, skip login and set `REVTURBINE_TOKEN` (see [CI / non-interactive use](#ci--non-interactive-use)). |
|
|
69
|
+
| `whoami` | Authentication state first: not logged in, credentials accepted, credentials rejected for the selected instance/tenant, or verification unavailable. Then shows instance, tenant, and credentials source; configured tenant information alone does not prove login. Verification and subsequent telemetry each time out after 3 seconds. Status reports exit 0. `--json` retains its seven fields: `instance`, `tenant` (`null` when `REVTURBINE_TOKEN` is set without `-t`: the server resolves the token user's own tenant, and `tenant_source` names the variable), `tenant_source`, `credentials_dir`, `credentials_source`, `token_present` (boolean), and `token_valid` (`true` for 2xx, `false` for 401/403, `null` when absent or unverified, including redirects, other HTTP errors, timeouts and network failures). |
|
|
70
70
|
| `schema` | Emit the bundled `RevTurbineConfig` JSON schema (for agents to author against). |
|
|
71
71
|
| `docs` | Print the canonical documentation URL. |
|
|
72
72
|
| `download` | Fetch a config version (`--live` / `--draft` / `--release <id>`); `--save`. |
|
|
73
73
|
| `validate` | Offline schema validation of a `<file>`, or the full server catalog against the open draft (`--draft`). |
|
|
74
|
-
| `diff` | Compare any two versions (dry-run, no writes). A file vs `--draft`/`--live`/`--release` previews the launch — the server side is the base, so `+`/`-` read as created/pruned on launch. |
|
|
74
|
+
| `diff` | Compare any two versions (dry-run, no writes). A file vs `--draft`/`--live`/`--release` previews the launch — the server side is the base, so `+`/`-` read as created/pruned on launch. `--exit-code` makes the status machine-readable: `0` no differences, `1` differences, anything else an error (an unexpected internal error exits `70` so `1` always means "differences"). Every top-level Playbook collection is compared. |
|
|
75
75
|
| `show <kind>` | Summary tables: `plans` · `entitlements` · `segments` · `placements` · `trials` for any version. |
|
|
76
76
|
| `upload` | Stage a Playbook file as the open draft. |
|
|
77
77
|
| `launch` | Take a config live: validate (launch gate) → submit → approve → deploy. `launch <file>` or `launch --draft`. |
|
|
@@ -145,6 +145,33 @@ Public ingest tokens are intentionally embeddable but remain tenant-bound,
|
|
|
145
145
|
origin-restricted, and optionally IP-restricted. They cannot mint other tokens
|
|
146
146
|
or access private control-plane APIs.
|
|
147
147
|
|
|
148
|
+
## CI / non-interactive use
|
|
149
|
+
|
|
150
|
+
`login` and `signup` are interactive. In CI, supply the token through the
|
|
151
|
+
environment instead:
|
|
152
|
+
|
|
153
|
+
| Variable | Meaning |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `REVTURBINE_TOKEN` | Bearer token for every request. Wins over any stored credential (which is then ignored). With it set and no `-t/--tenant-id`, **no `x-tenant-id` header is sent** and the server resolves the token user's own tenant; `-t` still sends one (it must be that tenant). |
|
|
156
|
+
| `REVTURBINE_HTTP_HEADERS` | A JSON object of extra headers added to every request to the RevTurbine instance, e.g. `{"x-vercel-protection-bypass":"…"}` to reach a protection-gated preview/staging deployment. It may not set `Authorization`, `x-tenant-id`, `x-rt-tenant-id` or `Content-Type`. A malformed value exits `2`. |
|
|
157
|
+
|
|
158
|
+
Neither the token nor any header value is ever logged, echoed in an error, or
|
|
159
|
+
sent as telemetry: diagnostics name only the variable and the header names, and
|
|
160
|
+
any stderr line that would contain a registered secret shows `<redacted>`.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
export REVTURBINE_TOKEN="$RT_TOKEN"
|
|
164
|
+
export REVTURBINE_HTTP_HEADERS="{\"x-vercel-protection-bypass\":\"$VERCEL_BYPASS\"}"
|
|
165
|
+
URL=https://staging.example.com/app
|
|
166
|
+
if revturbine diff ./seed.playbook.json --live --url "$URL" --exit-code; then
|
|
167
|
+
echo "no changes: skip launch"
|
|
168
|
+
else
|
|
169
|
+
status=$?
|
|
170
|
+
[ "$status" -eq 1 ] || exit "$status" # 2-7 / 70 = a real error
|
|
171
|
+
revturbine launch ./seed.playbook.json --url "$URL"
|
|
172
|
+
fi
|
|
173
|
+
```
|
|
174
|
+
|
|
148
175
|
## Exit-code classes
|
|
149
176
|
|
|
150
177
|
```
|
|
@@ -158,6 +185,9 @@ or access private control-plane APIs.
|
|
|
158
185
|
7 server error
|
|
159
186
|
```
|
|
160
187
|
|
|
188
|
+
`diff --exit-code` is the one exception: `1` means "differences" there, and an
|
|
189
|
+
unexpected internal error exits `70` instead.
|
|
190
|
+
|
|
161
191
|
## Schema validation
|
|
162
192
|
|
|
163
193
|
Validation is **mandatory** and runs fully offline against a vendored,
|
package/dist/cli.js
CHANGED
|
@@ -15609,8 +15609,8 @@ function openBrowser(url) {
|
|
|
15609
15609
|
} catch {
|
|
15610
15610
|
}
|
|
15611
15611
|
}
|
|
15612
|
-
async function deviceLogin(baseUrl, log = console.log) {
|
|
15613
|
-
const start = await startDeviceAuth(baseUrl, os2.hostname(),
|
|
15612
|
+
async function deviceLogin(baseUrl, log = console.log, fetchImpl = fetch) {
|
|
15613
|
+
const start = await startDeviceAuth(baseUrl, os2.hostname(), fetchImpl);
|
|
15614
15614
|
log("");
|
|
15615
15615
|
log(`To authorize this device, visit:
|
|
15616
15616
|
${start.verification_uri}`);
|
|
@@ -15622,7 +15622,7 @@ async function deviceLogin(baseUrl, log = console.log) {
|
|
|
15622
15622
|
deviceCode: start.device_code,
|
|
15623
15623
|
intervalSeconds: start.interval,
|
|
15624
15624
|
expiresInSeconds: start.expires_in,
|
|
15625
|
-
fetchImpl
|
|
15625
|
+
fetchImpl,
|
|
15626
15626
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
15627
15627
|
onPending: () => process.stdout.write(".")
|
|
15628
15628
|
});
|
|
@@ -15717,6 +15717,77 @@ Account created. Enter the verification code emailed to ${opts.email}.`);
|
|
|
15717
15717
|
return { status: "signed_in", token };
|
|
15718
15718
|
}
|
|
15719
15719
|
|
|
15720
|
+
// src/lib/env-auth.ts
|
|
15721
|
+
var TOKEN_ENV = "REVTURBINE_TOKEN";
|
|
15722
|
+
var HEADERS_ENV = "REVTURBINE_HTTP_HEADERS";
|
|
15723
|
+
var DEFAULT_TENANT_ID = "dev-tenant-001";
|
|
15724
|
+
function resolveAuth(params) {
|
|
15725
|
+
const envToken = params.envToken?.trim();
|
|
15726
|
+
const explicit = params.explicitTenantId;
|
|
15727
|
+
if (envToken) {
|
|
15728
|
+
return {
|
|
15729
|
+
token: envToken,
|
|
15730
|
+
tokenSource: "env",
|
|
15731
|
+
tenantId: explicit ?? null,
|
|
15732
|
+
tenantSource: explicit ? "--tenant-id" : `${TOKEN_ENV}; resolved server-side`
|
|
15733
|
+
};
|
|
15734
|
+
}
|
|
15735
|
+
const stored = params.stored;
|
|
15736
|
+
return {
|
|
15737
|
+
token: stored?.token ?? null,
|
|
15738
|
+
tokenSource: stored ? "stored" : "none",
|
|
15739
|
+
tenantId: explicit ?? stored?.tenant_id ?? DEFAULT_TENANT_ID,
|
|
15740
|
+
tenantSource: explicit ? "--tenant-id" : stored?.tenant_id ? "stored token" : "default"
|
|
15741
|
+
};
|
|
15742
|
+
}
|
|
15743
|
+
var RESERVED = /* @__PURE__ */ new Map([
|
|
15744
|
+
["authorization", `use ${TOKEN_ENV}`],
|
|
15745
|
+
["x-tenant-id", "use -t/--tenant-id"],
|
|
15746
|
+
["x-rt-tenant-id", "use -t/--tenant-id"],
|
|
15747
|
+
["content-type", "set by the CLI"]
|
|
15748
|
+
]);
|
|
15749
|
+
var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
15750
|
+
function parseExtraHeaders(raw) {
|
|
15751
|
+
if (raw === void 0 || raw.trim() === "") return { ok: true, headers: {} };
|
|
15752
|
+
let parsed;
|
|
15753
|
+
try {
|
|
15754
|
+
parsed = JSON.parse(raw);
|
|
15755
|
+
} catch {
|
|
15756
|
+
return { ok: false, error: `${HEADERS_ENV} is not valid JSON (expected an object of header name \u2192 string value)` };
|
|
15757
|
+
}
|
|
15758
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
15759
|
+
return { ok: false, error: `${HEADERS_ENV} must be a JSON object of header name \u2192 string value` };
|
|
15760
|
+
}
|
|
15761
|
+
const headers = {};
|
|
15762
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
15763
|
+
if (!HEADER_NAME.test(name)) {
|
|
15764
|
+
return { ok: false, error: `${HEADERS_ENV} has an invalid header name (${JSON.stringify(name.slice(0, 64))})` };
|
|
15765
|
+
}
|
|
15766
|
+
const reserved = RESERVED.get(name.toLowerCase());
|
|
15767
|
+
if (reserved) {
|
|
15768
|
+
return { ok: false, error: `${HEADERS_ENV} may not set ${name} (${reserved})` };
|
|
15769
|
+
}
|
|
15770
|
+
if (typeof value !== "string") {
|
|
15771
|
+
return { ok: false, error: `${HEADERS_ENV} header ${name} must have a string value` };
|
|
15772
|
+
}
|
|
15773
|
+
if (/[\r\n\0]/.test(value)) {
|
|
15774
|
+
return { ok: false, error: `${HEADERS_ENV} header ${name} has a value with a control character` };
|
|
15775
|
+
}
|
|
15776
|
+
headers[name] = value;
|
|
15777
|
+
}
|
|
15778
|
+
return { ok: true, headers };
|
|
15779
|
+
}
|
|
15780
|
+
function withExtraHeaders(fetchImpl, extra) {
|
|
15781
|
+
if (Object.keys(extra).length === 0) return fetchImpl;
|
|
15782
|
+
return (input, init) => {
|
|
15783
|
+
const headers = new Headers(init?.headers);
|
|
15784
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
15785
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
15786
|
+
}
|
|
15787
|
+
return fetchImpl(input, { ...init, headers });
|
|
15788
|
+
};
|
|
15789
|
+
}
|
|
15790
|
+
|
|
15720
15791
|
// src/lib/track.ts
|
|
15721
15792
|
var CLI_UNTRACKED_COMMANDS = /* @__PURE__ */ new Set(["login", "signup", "logout"]);
|
|
15722
15793
|
function shouldTrackCommandExecution(name, hasUrl) {
|
|
@@ -15725,14 +15796,16 @@ function shouldTrackCommandExecution(name, hasUrl) {
|
|
|
15725
15796
|
async function trackEvent(rawUrl, explicitTenantId, eventType, payload, signal) {
|
|
15726
15797
|
try {
|
|
15727
15798
|
const url = normalizeBaseUrl(rawUrl);
|
|
15728
|
-
const
|
|
15729
|
-
if (!
|
|
15799
|
+
const auth = resolveAuth({ envToken: process.env[TOKEN_ENV], stored: getCredential(url), explicitTenantId });
|
|
15800
|
+
if (!auth.token) return;
|
|
15801
|
+
const extra = parseExtraHeaders(process.env[HEADERS_ENV]);
|
|
15730
15802
|
await fetch(`${url}/api/events`, {
|
|
15731
15803
|
method: "POST",
|
|
15732
15804
|
headers: {
|
|
15805
|
+
...extra.ok ? extra.headers : {},
|
|
15733
15806
|
"Content-Type": "application/json",
|
|
15734
|
-
"x-tenant-id":
|
|
15735
|
-
Authorization: `Bearer ${
|
|
15807
|
+
...auth.tenantId ? { "x-tenant-id": auth.tenantId } : {},
|
|
15808
|
+
Authorization: `Bearer ${auth.token}`
|
|
15736
15809
|
},
|
|
15737
15810
|
body: JSON.stringify({ event_type: eventType, payload }),
|
|
15738
15811
|
...signal ? { signal } : {}
|
|
@@ -15755,12 +15828,21 @@ var COLLECTIONS = [
|
|
|
15755
15828
|
// BL-0176 / BL-0178: first-class objectives, keyed by handle.
|
|
15756
15829
|
"objectives"
|
|
15757
15830
|
];
|
|
15831
|
+
function collectionsOf(current, next) {
|
|
15832
|
+
const named = new Set(COLLECTIONS);
|
|
15833
|
+
const extra = /* @__PURE__ */ new Set();
|
|
15834
|
+
for (const side of [current, next]) {
|
|
15835
|
+
if (!side || typeof side !== "object") continue;
|
|
15836
|
+
for (const [k, v] of Object.entries(side)) if (Array.isArray(v) && !named.has(k)) extra.add(k);
|
|
15837
|
+
}
|
|
15838
|
+
return [...COLLECTIONS, ...[...extra].sort()];
|
|
15839
|
+
}
|
|
15758
15840
|
function keyOf(item) {
|
|
15759
15841
|
return item?.handle ?? item?.unique_handle ?? item?.name ?? item?.id ?? JSON.stringify(item);
|
|
15760
15842
|
}
|
|
15761
15843
|
function diffExportedConfig(current, next) {
|
|
15762
15844
|
const diff = {};
|
|
15763
|
-
for (const coll of
|
|
15845
|
+
for (const coll of collectionsOf(current, next)) {
|
|
15764
15846
|
const cur = Array.isArray(current?.[coll]) ? current[coll] : [];
|
|
15765
15847
|
const nxt = Array.isArray(next?.[coll]) ? next[coll] : [];
|
|
15766
15848
|
if (cur.length === 0 && nxt.length === 0) continue;
|
|
@@ -16814,11 +16896,21 @@ function isNetworkError(err) {
|
|
|
16814
16896
|
return err instanceof TypeError || err instanceof Error && /fetch failed|ECONN|ENOTFOUND|ETIMEDOUT|EAI_AGAIN/i.test(err.message);
|
|
16815
16897
|
}
|
|
16816
16898
|
var LOG = "[revturbine]";
|
|
16899
|
+
var secrets = /* @__PURE__ */ new Set();
|
|
16900
|
+
function registerSecrets(values) {
|
|
16901
|
+
for (const v of values) if (v.length >= 4) secrets.add(v);
|
|
16902
|
+
}
|
|
16903
|
+
function redact(message) {
|
|
16904
|
+
if (secrets.size === 0) return message;
|
|
16905
|
+
let out = message;
|
|
16906
|
+
for (const s of [...secrets].sort((a, b) => b.length - a.length)) out = out.split(s).join("<redacted>");
|
|
16907
|
+
return out;
|
|
16908
|
+
}
|
|
16817
16909
|
function diag(message) {
|
|
16818
|
-
console.error(`${LOG} ${message}`);
|
|
16910
|
+
console.error(redact(`${LOG} ${message}`));
|
|
16819
16911
|
}
|
|
16820
16912
|
function diagRaw(message) {
|
|
16821
|
-
console.error(message);
|
|
16913
|
+
console.error(redact(message));
|
|
16822
16914
|
}
|
|
16823
16915
|
function emit(data, json, humanText) {
|
|
16824
16916
|
if (json) {
|
|
@@ -16830,9 +16922,13 @@ function emit(data, json, humanText) {
|
|
|
16830
16922
|
}
|
|
16831
16923
|
}
|
|
16832
16924
|
function fail(cls, message) {
|
|
16833
|
-
console.error(`${LOG} \u2717 ${message}`);
|
|
16925
|
+
console.error(redact(`${LOG} \u2717 ${message}`));
|
|
16834
16926
|
process.exit(cls);
|
|
16835
16927
|
}
|
|
16928
|
+
var DIFF_EXIT = { NO_DIFFERENCES: 0, DIFFERENCES: 1, UNEXPECTED: 70 };
|
|
16929
|
+
function diffExitCode(diff) {
|
|
16930
|
+
return Object.keys(diff).length === 0 ? DIFF_EXIT.NO_DIFFERENCES : DIFF_EXIT.DIFFERENCES;
|
|
16931
|
+
}
|
|
16836
16932
|
|
|
16837
16933
|
// src/lib/pin-drift.ts
|
|
16838
16934
|
function isRecord7(value) {
|
|
@@ -17102,14 +17198,33 @@ function verifyConfig(file) {
|
|
|
17102
17198
|
if (check.advisories.length > 0) diagRaw(formatFindings(check.advisories));
|
|
17103
17199
|
return check.body;
|
|
17104
17200
|
}
|
|
17201
|
+
var extraHeadersCache;
|
|
17202
|
+
function extraHeaders() {
|
|
17203
|
+
if (extraHeadersCache) return extraHeadersCache;
|
|
17204
|
+
const parsed = parseExtraHeaders(process.env[HEADERS_ENV]);
|
|
17205
|
+
if (!parsed.ok) fail(EXIT.USAGE, parsed.error);
|
|
17206
|
+
registerSecrets(Object.values(parsed.headers));
|
|
17207
|
+
extraHeadersCache = parsed.headers;
|
|
17208
|
+
return parsed.headers;
|
|
17209
|
+
}
|
|
17210
|
+
function instanceFetch() {
|
|
17211
|
+
return withExtraHeaders(fetch, extraHeaders());
|
|
17212
|
+
}
|
|
17105
17213
|
function connect(rawUrl, explicitTenantId, diagnostic = diag) {
|
|
17106
17214
|
const url = normalizeBaseUrl(rawUrl);
|
|
17107
17215
|
const { dir, source } = resolveConfigDir();
|
|
17108
|
-
const
|
|
17109
|
-
const
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17216
|
+
const extra = extraHeaders();
|
|
17217
|
+
const envToken = process.env[TOKEN_ENV];
|
|
17218
|
+
if (envToken?.trim()) registerSecrets([envToken.trim()]);
|
|
17219
|
+
const auth = resolveAuth({ envToken, stored: getCredential(url), explicitTenantId });
|
|
17220
|
+
const { tenantId, tenantSource } = auth;
|
|
17221
|
+
const extraNote = Object.keys(extra).length ? `; extra headers: ${Object.keys(extra).join(", ")} [${HEADERS_ENV}, values redacted]` : "";
|
|
17222
|
+
if (auth.tokenSource === "env") {
|
|
17223
|
+
diagnostic(`Tenant ${tenantId ?? "(the token user's own)"} (${tenantSource}); token: ${TOKEN_ENV}${extraNote}.`);
|
|
17224
|
+
} else {
|
|
17225
|
+
diagnostic(`Tenant ${tenantId} (${tenantSource}); credentials: ${dir} [${source}]${extraNote}.`);
|
|
17226
|
+
}
|
|
17227
|
+
if (source === "global" && auth.tokenSource !== "env") {
|
|
17113
17228
|
diagnostic(
|
|
17114
17229
|
`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.`
|
|
17115
17230
|
);
|
|
@@ -17120,20 +17235,27 @@ function connect(rawUrl, explicitTenantId, diagnostic = diag) {
|
|
|
17120
17235
|
tenantSource,
|
|
17121
17236
|
credentialsDir: dir,
|
|
17122
17237
|
credentialsSource: source,
|
|
17123
|
-
hasToken:
|
|
17238
|
+
hasToken: auth.token !== null,
|
|
17239
|
+
tokenSource: auth.tokenSource,
|
|
17124
17240
|
headers: {
|
|
17241
|
+
...extra,
|
|
17125
17242
|
"Content-Type": "application/json",
|
|
17126
|
-
"x-tenant-id": tenantId,
|
|
17127
|
-
...
|
|
17243
|
+
...tenantId ? { "x-tenant-id": tenantId } : {},
|
|
17244
|
+
...auth.token ? { Authorization: `Bearer ${auth.token}` } : {}
|
|
17128
17245
|
}
|
|
17129
17246
|
};
|
|
17130
17247
|
}
|
|
17131
17248
|
function uploadTenantFor(rawUrl, config, explicit) {
|
|
17249
|
+
const embedded = config?.tenant_id;
|
|
17250
|
+
if (process.env[TOKEN_ENV]?.trim()) {
|
|
17251
|
+
if (!explicit && embedded) diag(`sending to the config's embedded tenant ${embedded} (pass -t to retarget).`);
|
|
17252
|
+
return explicit ?? embedded ?? void 0;
|
|
17253
|
+
}
|
|
17132
17254
|
const cred = getCredential(normalizeBaseUrl(rawUrl));
|
|
17133
17255
|
const target = resolveUploadTarget({
|
|
17134
|
-
embedded
|
|
17256
|
+
embedded,
|
|
17135
17257
|
explicit,
|
|
17136
|
-
session: cred?.tenant_id ??
|
|
17258
|
+
session: cred?.tenant_id ?? DEFAULT_TENANT_ID
|
|
17137
17259
|
});
|
|
17138
17260
|
if (!target.ok) fail(EXIT.VALIDATION, target.error);
|
|
17139
17261
|
if (target.note) diag(target.note);
|
|
@@ -17141,8 +17263,10 @@ function uploadTenantFor(rawUrl, config, explicit) {
|
|
|
17141
17263
|
}
|
|
17142
17264
|
function authHint(url, status) {
|
|
17143
17265
|
if (status === 401 || status === 403) {
|
|
17144
|
-
diag(
|
|
17145
|
-
|
|
17266
|
+
diag(
|
|
17267
|
+
process.env[TOKEN_ENV]?.trim() ? `Authentication failed for ${url} with ${TOKEN_ENV}: check the token and its tenant membership.` : `Authentication required for ${url}. Log in with:
|
|
17268
|
+
revturbine login ${url}`
|
|
17269
|
+
);
|
|
17146
17270
|
}
|
|
17147
17271
|
}
|
|
17148
17272
|
function httpFail(conn, what, status, detail) {
|
|
@@ -17378,6 +17502,16 @@ Auth:
|
|
|
17378
17502
|
~/.revturbine/credentials.json (0600); the token's tenant is used by default,
|
|
17379
17503
|
override with -t/--tenant-id. Mutating commands (discard, restore) prompt
|
|
17380
17504
|
for confirmation unless --yes.
|
|
17505
|
+
|
|
17506
|
+
CI / non-interactive:
|
|
17507
|
+
REVTURBINE_TOKEN bearer token; wins over the stored credential. Without
|
|
17508
|
+
-t no x-tenant-id is sent (the server resolves the
|
|
17509
|
+
token user's own tenant).
|
|
17510
|
+
REVTURBINE_HTTP_HEADERS JSON object of extra headers for every request, e.g.
|
|
17511
|
+
{"x-vercel-protection-bypass":"..."}.
|
|
17512
|
+
Neither the token nor any header value is ever logged or echoed.
|
|
17513
|
+
\`diff ... --exit-code\` exits 0 = no differences, 1 = differences, other = error
|
|
17514
|
+
(an unexpected error exits 70 there).
|
|
17381
17515
|
\`whoami\` leads with absent, accepted, rejected, or unverifiable credentials;
|
|
17382
17516
|
a configured tenant alone does not prove login. Verification and subsequent
|
|
17383
17517
|
telemetry each time out after 3 seconds. Status reports exit 0; --json keeps
|
|
@@ -17392,6 +17526,7 @@ SDK updates:
|
|
|
17392
17526
|
Full reference: ${DOCS_URL}
|
|
17393
17527
|
`;
|
|
17394
17528
|
var program = new Command();
|
|
17529
|
+
var unexpectedExit = EXIT.UNEXPECTED;
|
|
17395
17530
|
program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
17396
17531
|
const opts = actionCommand.opts();
|
|
17397
17532
|
if (actionCommand.name() === "validate" && !opts.draft) return;
|
|
@@ -17564,7 +17699,7 @@ program.command("init").alias("create").description("Set up RevTurbine while pre
|
|
|
17564
17699
|
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) => {
|
|
17565
17700
|
try {
|
|
17566
17701
|
const base = normalizeBaseUrl(url ?? DEFAULT_URL);
|
|
17567
|
-
await deviceLogin(base);
|
|
17702
|
+
await deviceLogin(base, console.log, instanceFetch());
|
|
17568
17703
|
await trackEvent(base, void 0, "cli_signed_in");
|
|
17569
17704
|
} catch (err) {
|
|
17570
17705
|
fail(EXIT.AUTH, `Login failed: ${err.message}`);
|
|
@@ -17635,7 +17770,8 @@ program.command("signup").description("Create a RevTurbine account and log in (e
|
|
|
17635
17770
|
name,
|
|
17636
17771
|
email,
|
|
17637
17772
|
password,
|
|
17638
|
-
promptOtp: (attempt) => promptLine(attempt > 1 ? "Verification code (try again): " : "Verification code: ")
|
|
17773
|
+
promptOtp: (attempt) => promptLine(attempt > 1 ? "Verification code (try again): " : "Verification code: "),
|
|
17774
|
+
fetchImpl: instanceFetch()
|
|
17639
17775
|
});
|
|
17640
17776
|
if (result.status === "awaiting_invitation") process.exit(0);
|
|
17641
17777
|
await trackEvent(baseUrl, void 0, "cli_signed_up");
|
|
@@ -17679,8 +17815,8 @@ program.command("whoami").description("Show authentication state first (absent,
|
|
|
17679
17815
|
[
|
|
17680
17816
|
formatAuthentication(conn.hasToken, tokenValid),
|
|
17681
17817
|
`instance: ${data.instance}`,
|
|
17682
|
-
`tenant: ${data.tenant} (${data.tenant_source})`,
|
|
17683
|
-
`credentials: ${data.credentials_dir} [${data.credentials_source}]`
|
|
17818
|
+
`tenant: ${data.tenant ?? "(the token user's own)"} (${data.tenant_source})`,
|
|
17819
|
+
conn.tokenSource === "env" ? `credentials: ${TOKEN_ENV} (env; stored credentials ignored)` : `credentials: ${data.credentials_dir} [${data.credentials_source}]`
|
|
17684
17820
|
].join("\n")
|
|
17685
17821
|
);
|
|
17686
17822
|
for (const message of diagnostics) diag(message);
|
|
@@ -17765,15 +17901,18 @@ program.command("validate").description("Validate a Playbook file offline (schem
|
|
|
17765
17901
|
}
|
|
17766
17902
|
diag("\u2713 No blocking findings.");
|
|
17767
17903
|
});
|
|
17768
|
-
program.command("diff").description("Compare two config versions (first \u2192 second; a file vs --draft/--live/--release previews the launch \u2014 the server side is the base). Dry-run, no writes.").argument("[file...]", "Local Playbook 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) => {
|
|
17904
|
+
program.command("diff").description("Compare two config versions (first \u2192 second; a file vs --draft/--live/--release previews the launch \u2014 the server side is the base). Dry-run, no writes.").argument("[file...]", "Local Playbook 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)").option("--exit-code", "Exit 0 when there are no differences, 1 when there are (any other code is an error); for CI no-op detection").action(async (files, opts) => {
|
|
17905
|
+
if (opts.exitCode) unexpectedExit = DIFF_EXIT.UNEXPECTED;
|
|
17769
17906
|
const sels = requireSelectors(opts, files, { count: 2, allowed: ["file", "draft", "live", "release"], command: "diff" });
|
|
17770
17907
|
const ordered = orderDiffSelectors(sels);
|
|
17771
17908
|
const needsServer = ordered.some((s) => s.kind !== "file");
|
|
17772
17909
|
const conn = needsServer ? connect(opts.url, opts.tenantId) : null;
|
|
17773
17910
|
const [a, b] = await Promise.all(ordered.map((s) => loadVersion(conn, s)));
|
|
17774
17911
|
diag(`Diff (${ordered.map((s) => s.kind === "file" ? s.path : s.kind === "release" ? `--release ${s.id}` : `--${s.kind}`).join(" \u2192 ")}):`);
|
|
17775
|
-
|
|
17912
|
+
const diff = diffExportedConfig(a, b);
|
|
17913
|
+
process.stdout.write(`${formatDiff(diff)}
|
|
17776
17914
|
`);
|
|
17915
|
+
if (opts.exitCode) process.exitCode = diffExitCode(diff);
|
|
17777
17916
|
});
|
|
17778
17917
|
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 Playbook 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(
|
|
17779
17918
|
async (kind, opts) => {
|
|
@@ -18365,7 +18504,7 @@ if (process.argv.includes("-V") || process.argv.includes("--version")) {
|
|
|
18365
18504
|
console.error(`[revturbine] \u2717 ${err.message}`);
|
|
18366
18505
|
process.exit(EXIT.USAGE);
|
|
18367
18506
|
}
|
|
18368
|
-
console.error(`[revturbine] \u2717 ${err.message}`);
|
|
18369
|
-
process.exit(
|
|
18507
|
+
console.error(redact(`[revturbine] \u2717 ${err.message}`));
|
|
18508
|
+
process.exit(unexpectedExit);
|
|
18370
18509
|
}
|
|
18371
18510
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revturbine/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "revturbine — validate RevTurbine Playbooks and ship them to a RevTurbine instance through the playbook-version lifecycle (draft → Release).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|