@revturbine/cli 0.17.2 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/cli.js +327 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,10 +63,10 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
63
63
|
|
|
64
64
|
| Command | What it does |
|
|
65
65
|
|---|---|
|
|
66
|
-
| `init` (alias `create`) |
|
|
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
68
|
| `login` / `logout` | Device-flow auth; tokens stored at `~/.revturbine/credentials.json` (mode 0600). |
|
|
69
|
-
| `whoami` |
|
|
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`, `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`. |
|
|
@@ -84,10 +84,40 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
84
84
|
| `generate types` | Generate a TypeScript module of typed Playbook handles from any config version — `Entitlements` (namespaced by type, with the `EntitlementHandle` union for type-safe `can()`/`gate()`/`checkEntitlement()` call sites), plus `Plans`, `Segments`, `SurfaceTemplates`, and `UiPathActionTypes`. Const objects + literal-union types (erasable — no enums). `--out <path>` writes the file; the generated header records the exact command to regenerate it. `--json` for the raw handle map. |
|
|
85
85
|
| `analytics catalog\|templates\|views\|view\|create\|preview\|query` | Work with the hosted Semantic Catalog and canonical analytics-view documents. Create and preview pass the document through unchanged to the same server contract used by the web editor and MCP tools; preview remains subject to the server's query limits. |
|
|
86
86
|
| `ingest-keys create` (alias `mint`) | Mint a tenant-bound public ingest token for browser SDK use. Requires one or more `--origin` values; optional `--ip` restrictions. The full token is returned once. `ingest-keys list` shows ids/previews and `ingest-keys revoke <id>` invalidates one. |
|
|
87
|
+
| `events track` | Send analytics events to the ingest pipeline with your own login — no ingest key and no browser origin required. Takes `--file <path>` (JSON array, `{ "events": [...] }` envelope, or NDJSON) or `--event <json>`. Batches over 500 events are chunked automatically. Reports accepted, quarantined, rejected and dropped counts separately. |
|
|
88
|
+
|
|
89
|
+
`init` preserves declared SDK/CLI versions and application code. It skips a new root
|
|
90
|
+
Playbook when the SDK is already declared in `dependencies` or `devDependencies`,
|
|
91
|
+
or project source imports the SDK, or a non-root canonical Playbook is found.
|
|
92
|
+
Detection ignores hidden directories, symlinks, dependencies and build outputs.
|
|
93
|
+
If the integration is intentionally incomplete, `--scaffold` creates only the missing
|
|
94
|
+
`revturbine.playbook.json`; it never replaces an existing one. `--dry-run --json`
|
|
95
|
+
reports `playbook: "present"`, `"skipped"`, or the filename to create, and
|
|
96
|
+
`skills_agent` identifies the single installer target (or `null` when skipped).
|
|
97
|
+
`--no-skills` takes precedence over a detected or explicitly selected valid agent.
|
|
87
98
|
|
|
88
99
|
`--json` on read commands emits machine-readable output. Results go to stdout,
|
|
89
100
|
diagnostics to stderr.
|
|
90
101
|
|
|
102
|
+
### Send analytics events
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
revturbine login
|
|
106
|
+
revturbine events track --file ./events.ndjson
|
|
107
|
+
|
|
108
|
+
# or a single event inline
|
|
109
|
+
revturbine events track --event '{"environment_id":"production","user_id":"u-1","account_id":"a-1","event_name":"product_used","event_ts":"2026-09-10T00:00:00.000Z"}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Every event needs `environment_id`, `user_id`, `account_id`, `event_name` and
|
|
113
|
+
`event_ts`; anything else you include is carried through. Local validation
|
|
114
|
+
reports every malformed event at once and sends nothing until they are fixed.
|
|
115
|
+
|
|
116
|
+
This authenticates with your device-auth token rather than an ingest key, so
|
|
117
|
+
it works from a terminal or CI where there is no browser origin to allowlist.
|
|
118
|
+
Quarantined and rejected counts are reported separately from accepted — a
|
|
119
|
+
load is not clean merely because the server returned `202`.
|
|
120
|
+
|
|
91
121
|
### Mint a publishable ingest token
|
|
92
122
|
|
|
93
123
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -10705,7 +10705,7 @@ var CLI_UNTRACKED_COMMANDS = /* @__PURE__ */ new Set(["login", "signup", "logout
|
|
|
10705
10705
|
function shouldTrackCommandExecution(name, hasUrl) {
|
|
10706
10706
|
return hasUrl && !CLI_UNTRACKED_COMMANDS.has(name);
|
|
10707
10707
|
}
|
|
10708
|
-
async function trackEvent(rawUrl, explicitTenantId, eventType, payload) {
|
|
10708
|
+
async function trackEvent(rawUrl, explicitTenantId, eventType, payload, signal) {
|
|
10709
10709
|
try {
|
|
10710
10710
|
const url = normalizeBaseUrl(rawUrl);
|
|
10711
10711
|
const cred = getCredential(url);
|
|
@@ -10717,7 +10717,8 @@ async function trackEvent(rawUrl, explicitTenantId, eventType, payload) {
|
|
|
10717
10717
|
"x-tenant-id": explicitTenantId ?? cred.tenant_id ?? "dev-tenant-001",
|
|
10718
10718
|
Authorization: `Bearer ${cred.token}`
|
|
10719
10719
|
},
|
|
10720
|
-
body: JSON.stringify({ event_type: eventType, payload })
|
|
10720
|
+
body: JSON.stringify({ event_type: eventType, payload }),
|
|
10721
|
+
...signal ? { signal } : {}
|
|
10721
10722
|
});
|
|
10722
10723
|
} catch {
|
|
10723
10724
|
}
|
|
@@ -10886,6 +10887,20 @@ async function resolveActiveDraft(baseUrl, headers, fetchImpl = fetch) {
|
|
|
10886
10887
|
return { ok: res.ok, status: res.status, draft: res.ok ? json.active ?? null : null };
|
|
10887
10888
|
}
|
|
10888
10889
|
|
|
10890
|
+
// src/lib/whoami.ts
|
|
10891
|
+
function classifyToken(hasToken, status) {
|
|
10892
|
+
if (!hasToken || status === null) return null;
|
|
10893
|
+
if (status >= 200 && status < 300) return true;
|
|
10894
|
+
if (status === 401 || status === 403) return false;
|
|
10895
|
+
return null;
|
|
10896
|
+
}
|
|
10897
|
+
function formatAuthentication(hasToken, tokenValid) {
|
|
10898
|
+
if (!hasToken) return "authentication: not logged in \u2014 run `revturbine login`";
|
|
10899
|
+
if (tokenValid === true) return "authentication: credentials accepted for this instance and tenant";
|
|
10900
|
+
if (tokenValid === false) return "authentication: credentials rejected for this instance or tenant \u2014 check access or run `revturbine login`";
|
|
10901
|
+
return "authentication: verification unavailable \u2014 credentials present; retry when the instance is reachable";
|
|
10902
|
+
}
|
|
10903
|
+
|
|
10889
10904
|
// src/lib/evaluate-local.ts
|
|
10890
10905
|
import { initRevTurbine } from "@revturbine/sdk/headless";
|
|
10891
10906
|
function resolvePlacementComponentType(input) {
|
|
@@ -10981,6 +10996,130 @@ function formatIngestKeyLine(key2) {
|
|
|
10981
10996
|
return `${key2.id} ${key2.tokenPreview} origins: ${origins}${ips}${last}`;
|
|
10982
10997
|
}
|
|
10983
10998
|
|
|
10999
|
+
// src/lib/events-ingest.ts
|
|
11000
|
+
var MAX_EVENTS_PER_BATCH = 500;
|
|
11001
|
+
var REQUIRED_FIELDS = [
|
|
11002
|
+
"environment_id",
|
|
11003
|
+
"user_id",
|
|
11004
|
+
"account_id",
|
|
11005
|
+
"event_name",
|
|
11006
|
+
"event_ts"
|
|
11007
|
+
];
|
|
11008
|
+
function parseEventBatch(text) {
|
|
11009
|
+
const trimmed = text.trim();
|
|
11010
|
+
if (trimmed === "") return { events: [], errors: ["input is empty"] };
|
|
11011
|
+
const errors = [];
|
|
11012
|
+
let raw;
|
|
11013
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
11014
|
+
let parsed;
|
|
11015
|
+
try {
|
|
11016
|
+
parsed = JSON.parse(trimmed);
|
|
11017
|
+
} catch {
|
|
11018
|
+
return parseNdjson(trimmed);
|
|
11019
|
+
}
|
|
11020
|
+
if (Array.isArray(parsed)) {
|
|
11021
|
+
raw = parsed;
|
|
11022
|
+
} else if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.events)) {
|
|
11023
|
+
raw = parsed.events;
|
|
11024
|
+
} else if (parsed !== null && typeof parsed === "object") {
|
|
11025
|
+
raw = [parsed];
|
|
11026
|
+
} else {
|
|
11027
|
+
return { events: [], errors: ["input must be an object, an array, or an { events: [...] } envelope"] };
|
|
11028
|
+
}
|
|
11029
|
+
} else {
|
|
11030
|
+
return parseNdjson(trimmed);
|
|
11031
|
+
}
|
|
11032
|
+
const events2 = validateAll(raw, errors);
|
|
11033
|
+
return { events: events2, errors };
|
|
11034
|
+
}
|
|
11035
|
+
function parseNdjson(text) {
|
|
11036
|
+
const errors = [];
|
|
11037
|
+
const raw = [];
|
|
11038
|
+
text.split("\n").forEach((line, i) => {
|
|
11039
|
+
const t = line.trim();
|
|
11040
|
+
if (t === "") return;
|
|
11041
|
+
try {
|
|
11042
|
+
raw.push(JSON.parse(t));
|
|
11043
|
+
} catch {
|
|
11044
|
+
errors.push(`line ${i + 1}: not valid JSON`);
|
|
11045
|
+
}
|
|
11046
|
+
});
|
|
11047
|
+
const events2 = validateAll(raw, errors);
|
|
11048
|
+
return { events: events2, errors };
|
|
11049
|
+
}
|
|
11050
|
+
function validateAll(raw, errors) {
|
|
11051
|
+
const events2 = [];
|
|
11052
|
+
raw.forEach((item, i) => {
|
|
11053
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
11054
|
+
errors.push(`event ${i}: not a JSON object`);
|
|
11055
|
+
return;
|
|
11056
|
+
}
|
|
11057
|
+
const bag = item;
|
|
11058
|
+
const missing = REQUIRED_FIELDS.filter(
|
|
11059
|
+
(f) => typeof bag[f] !== "string" || bag[f].trim() === ""
|
|
11060
|
+
);
|
|
11061
|
+
if (missing.length > 0) {
|
|
11062
|
+
errors.push(`event ${i}: missing or empty ${missing.join(", ")}`);
|
|
11063
|
+
return;
|
|
11064
|
+
}
|
|
11065
|
+
events2.push(bag);
|
|
11066
|
+
});
|
|
11067
|
+
return events2;
|
|
11068
|
+
}
|
|
11069
|
+
function chunkEvents(events2, size = MAX_EVENTS_PER_BATCH) {
|
|
11070
|
+
const limit = Math.max(1, Math.min(size, MAX_EVENTS_PER_BATCH));
|
|
11071
|
+
const out = [];
|
|
11072
|
+
for (let i = 0; i < events2.length; i += limit) out.push(events2.slice(i, i + limit));
|
|
11073
|
+
return out;
|
|
11074
|
+
}
|
|
11075
|
+
function readResult(json) {
|
|
11076
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
11077
|
+
return {
|
|
11078
|
+
accepted: num(json.accepted),
|
|
11079
|
+
quarantined: num(json.quarantined),
|
|
11080
|
+
dropped_unscoped: num(json.dropped_unscoped),
|
|
11081
|
+
rejected: Array.isArray(json.rejected) ? json.rejected : [],
|
|
11082
|
+
request_id: typeof json.request_id === "string" ? json.request_id : null
|
|
11083
|
+
};
|
|
11084
|
+
}
|
|
11085
|
+
async function postBatch(baseUrl, headers, events2, fetchImpl = fetch) {
|
|
11086
|
+
const res = await fetchImpl(`${baseUrl}/api/track`, {
|
|
11087
|
+
method: "POST",
|
|
11088
|
+
headers,
|
|
11089
|
+
body: JSON.stringify({ events: events2 })
|
|
11090
|
+
});
|
|
11091
|
+
const json = await res.json().catch(() => ({}));
|
|
11092
|
+
if (!res.ok) return { ok: false, status: res.status, result: null, error: json };
|
|
11093
|
+
return { ok: true, status: res.status, result: readResult(json), error: {} };
|
|
11094
|
+
}
|
|
11095
|
+
function summarize(results, eventCount) {
|
|
11096
|
+
return {
|
|
11097
|
+
batches: results.length,
|
|
11098
|
+
// @revturbine-graph gref:3f4d51af0a0af98edea0
|
|
11099
|
+
events: eventCount,
|
|
11100
|
+
accepted: results.reduce((n, r) => n + r.accepted, 0),
|
|
11101
|
+
quarantined: results.reduce((n, r) => n + r.quarantined, 0),
|
|
11102
|
+
dropped_unscoped: results.reduce((n, r) => n + r.dropped_unscoped, 0),
|
|
11103
|
+
rejected: results.reduce((n, r) => n + r.rejected.length, 0),
|
|
11104
|
+
results
|
|
11105
|
+
};
|
|
11106
|
+
}
|
|
11107
|
+
function formatSummary(summary) {
|
|
11108
|
+
const lines = [
|
|
11109
|
+
`ingested ${summary.accepted}/${summary.events} event(s) in ${summary.batches} batch(es)`
|
|
11110
|
+
];
|
|
11111
|
+
if (summary.quarantined > 0) {
|
|
11112
|
+
lines.push(` quarantined: ${summary.quarantined} (payload failed its platform contract)`);
|
|
11113
|
+
}
|
|
11114
|
+
if (summary.rejected > 0) {
|
|
11115
|
+
lines.push(` rejected: ${summary.rejected} (malformed on the wire)`);
|
|
11116
|
+
}
|
|
11117
|
+
if (summary.dropped_unscoped > 0) {
|
|
11118
|
+
lines.push(` dropped (unscoped): ${summary.dropped_unscoped} (simulation rows without a dataset id)`);
|
|
11119
|
+
}
|
|
11120
|
+
return lines.join("\n");
|
|
11121
|
+
}
|
|
11122
|
+
|
|
10984
11123
|
// src/lib/delegate.ts
|
|
10985
11124
|
var DELEGATION_ENV = "REVTURBINE_DELEGATED";
|
|
10986
11125
|
var NO_LOCAL_FLAG = "--no-local";
|
|
@@ -11116,6 +11255,29 @@ function detectStack(signals) {
|
|
|
11116
11255
|
}
|
|
11117
11256
|
var SDK_PACKAGE = "@revturbine/sdk";
|
|
11118
11257
|
var CLI_PACKAGE = "@revturbine/cli";
|
|
11258
|
+
function declaredSdk(manifest) {
|
|
11259
|
+
return Boolean(manifest.dependencies?.[SDK_PACKAGE] || manifest.devDependencies?.[SDK_PACKAGE]);
|
|
11260
|
+
}
|
|
11261
|
+
function integrationFileReason(file, content) {
|
|
11262
|
+
if (file.endsWith("revturbine.playbook.json")) return `existing Playbook (${file})`;
|
|
11263
|
+
if (/\.[cm]?[jt]sx?$/.test(file) && /['"]@revturbine\/sdk(?:\/[^'"]*)?['"]/.test(content)) {
|
|
11264
|
+
return `SDK integration (${file})`;
|
|
11265
|
+
}
|
|
11266
|
+
if (file.endsWith(".json")) {
|
|
11267
|
+
try {
|
|
11268
|
+
const value = JSON.parse(content);
|
|
11269
|
+
if (value && typeof value === "object" && "artifact_type" in value && value.artifact_type === "playbook") {
|
|
11270
|
+
return `existing Playbook (${file})`;
|
|
11271
|
+
}
|
|
11272
|
+
} catch {
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
return void 0;
|
|
11276
|
+
}
|
|
11277
|
+
function planStarter(params) {
|
|
11278
|
+
if (params.rootExists) return "present";
|
|
11279
|
+
return params.integrationReason && !params.scaffold ? "skipped" : "create";
|
|
11280
|
+
}
|
|
11119
11281
|
function projectNameFromDir(dirName) {
|
|
11120
11282
|
const slug = dirName.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
|
|
11121
11283
|
return slug || "my-app";
|
|
@@ -11208,20 +11370,34 @@ function validatePlaybook(config) {
|
|
|
11208
11370
|
// src/lib/init-skills.ts
|
|
11209
11371
|
var SKILLS_SOURCE = "revt-eng/revturbine-skills";
|
|
11210
11372
|
var START_HERE_SKILL = "revturbine-start-here";
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11373
|
+
var SUPPORTED_AGENTS = ["claude-code", "cursor", "codex"];
|
|
11374
|
+
function isAgentId(value) {
|
|
11375
|
+
return SUPPORTED_AGENTS.some((id) => id === value);
|
|
11376
|
+
}
|
|
11377
|
+
function detectHarness(env, explicit) {
|
|
11378
|
+
let agentId = explicit;
|
|
11379
|
+
if (!agentId) {
|
|
11380
|
+
if (env["CLAUDECODE"] || env["CLAUDE_CODE_SESSION_ID"] || env["CLAUDE_CODE_ENTRYPOINT"]) agentId = "claude-code";
|
|
11381
|
+
else if (env["CURSOR_TRACE_ID"] || env["CURSOR"]) agentId = "cursor";
|
|
11382
|
+
else if (env["CODEX_THREAD_ID"]) agentId = "codex";
|
|
11383
|
+
}
|
|
11384
|
+
if (agentId === "claude-code") {
|
|
11385
|
+
return { agentId, label: "Claude Code", invocation: `Run the skill: /${START_HERE_SKILL}` };
|
|
11386
|
+
}
|
|
11387
|
+
if (agentId === "cursor") {
|
|
11388
|
+
return { agentId, label: "Cursor", invocation: `Mention the skill: @${START_HERE_SKILL}` };
|
|
11214
11389
|
}
|
|
11215
|
-
if (
|
|
11216
|
-
return { label: "
|
|
11390
|
+
if (agentId === "codex") {
|
|
11391
|
+
return { agentId, label: "Codex", invocation: `Run the skill: $${START_HERE_SKILL}` };
|
|
11217
11392
|
}
|
|
11218
11393
|
return {
|
|
11394
|
+
agentId: null,
|
|
11219
11395
|
label: null,
|
|
11220
11396
|
invocation: `Ask your coding agent to run the "${START_HERE_SKILL}" skill.`
|
|
11221
11397
|
};
|
|
11222
11398
|
}
|
|
11223
|
-
function skillsAddArgs(
|
|
11224
|
-
return ["--yes", "skills", "add",
|
|
11399
|
+
function skillsAddArgs(agent) {
|
|
11400
|
+
return ["--yes", "skills", "add", SKILLS_SOURCE, "-y", "--copy", "-a", agent];
|
|
11225
11401
|
}
|
|
11226
11402
|
function finalOutputLines(params) {
|
|
11227
11403
|
const lines = ["", "RevTurbine is set up.", ""];
|
|
@@ -11235,12 +11411,14 @@ function finalOutputLines(params) {
|
|
|
11235
11411
|
lines.push(" \u2713 Installed the Agent Skills");
|
|
11236
11412
|
} else if (params.skills === "skipped") {
|
|
11237
11413
|
lines.push(" \u2022 Skipped the Agent Skills (--no-skills)");
|
|
11414
|
+
} else if (params.skills === "unknown") {
|
|
11415
|
+
lines.push(" \u2022 Agent Skills not installed \u2014 select a target with --agent");
|
|
11238
11416
|
} else {
|
|
11239
11417
|
lines.push(" \u26A0 Agent Skills not installed \u2014 see above to add them by hand");
|
|
11240
11418
|
}
|
|
11241
11419
|
lines.push("", "Next step:");
|
|
11242
11420
|
lines.push(` ${params.harness.invocation}`);
|
|
11243
|
-
if (params.harness.label) lines.push(` (
|
|
11421
|
+
if (params.harness.label) lines.push(` (${params.harness.label})`);
|
|
11244
11422
|
lines.push("", "Your setup path:");
|
|
11245
11423
|
lines.push(" create playbook \u2192 app wiring \u2192 billing \u2192 verify \u2192 launch");
|
|
11246
11424
|
lines.push("", "Docs: https://revturbine.com/docs", "");
|
|
@@ -11618,6 +11796,7 @@ function queryAnalyticsView(baseUrl, headers, body, fetchImpl) {
|
|
|
11618
11796
|
// src/cli.ts
|
|
11619
11797
|
var DOCS_URL = "https://github.com/revt-eng/revturbine-cli#readme";
|
|
11620
11798
|
var DEFAULT_URL = "https://revturbine.com/app";
|
|
11799
|
+
var WHOAMI_REQUEST_TIMEOUT_MS = 3e3;
|
|
11621
11800
|
function loadConfig(file) {
|
|
11622
11801
|
const resolved = path2.resolve(file);
|
|
11623
11802
|
if (!existsSync2(resolved)) fail(EXIT.USAGE, `Config not found: ${resolved}`);
|
|
@@ -11653,15 +11832,15 @@ function verifyConfig(file) {
|
|
|
11653
11832
|
if (check.advisories.length > 0) diagRaw(formatFindings(check.advisories));
|
|
11654
11833
|
return check.body;
|
|
11655
11834
|
}
|
|
11656
|
-
function connect(rawUrl, explicitTenantId) {
|
|
11835
|
+
function connect(rawUrl, explicitTenantId, diagnostic = diag) {
|
|
11657
11836
|
const url = normalizeBaseUrl(rawUrl);
|
|
11658
11837
|
const { dir, source } = resolveConfigDir();
|
|
11659
11838
|
const cred = getCredential(url);
|
|
11660
11839
|
const tenantId = explicitTenantId ?? cred?.tenant_id ?? "dev-tenant-001";
|
|
11661
11840
|
const tenantSource = explicitTenantId ? "--tenant-id" : cred?.tenant_id ? "stored token" : "default";
|
|
11662
|
-
|
|
11841
|
+
diagnostic(`Tenant ${tenantId} (${tenantSource}); credentials: ${dir} [${source}].`);
|
|
11663
11842
|
if (source === "global") {
|
|
11664
|
-
|
|
11843
|
+
diagnostic(
|
|
11665
11844
|
`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.`
|
|
11666
11845
|
);
|
|
11667
11846
|
}
|
|
@@ -11879,6 +12058,7 @@ Command groups:
|
|
|
11879
12058
|
Codegen generate types
|
|
11880
12059
|
Analytics analytics catalog|templates|views|view|create|preview|query
|
|
11881
12060
|
Keys ingest-keys create (alias: mint)|list|revoke
|
|
12061
|
+
Events events track
|
|
11882
12062
|
|
|
11883
12063
|
Version selectors (no defaults \u2014 a command that reads a config requires one):
|
|
11884
12064
|
<file> a local Playbook file (positional, or --file <path>)
|
|
@@ -11889,6 +12069,10 @@ Version selectors (no defaults \u2014 a command that reads a config requires one
|
|
|
11889
12069
|
Common workflows:
|
|
11890
12070
|
# Add RevTurbine to an app (same routine as \`npm create revturbine@latest\`)
|
|
11891
12071
|
revturbine init
|
|
12072
|
+
# Preserve an existing integration; select one skills target explicitly
|
|
12073
|
+
revturbine init --agent codex
|
|
12074
|
+
# Opt into a missing root starter; existing Playbooks are never overwritten
|
|
12075
|
+
revturbine init --scaffold --no-skills
|
|
11892
12076
|
|
|
11893
12077
|
# Author, validate, and ship against the default instance (revturbine.com/app)
|
|
11894
12078
|
revturbine login
|
|
@@ -11910,6 +12094,9 @@ Common workflows:
|
|
|
11910
12094
|
|
|
11911
12095
|
# Mint an origin-restricted browser token (shown once), then revoke it
|
|
11912
12096
|
revturbine ingest-keys mint --origin https://app.example.com --json
|
|
12097
|
+
|
|
12098
|
+
# Send analytics events with your own login (no ingest key, no browser origin)
|
|
12099
|
+
revturbine events track --file ./events.ndjson
|
|
11913
12100
|
revturbine ingest-keys list
|
|
11914
12101
|
revturbine ingest-keys revoke <ingest-key-id> --yes
|
|
11915
12102
|
|
|
@@ -11921,6 +12108,10 @@ Auth:
|
|
|
11921
12108
|
~/.revturbine/credentials.json (0600); the token's tenant is used by default,
|
|
11922
12109
|
override with -t/--tenant-id. Mutating commands (discard, restore) prompt
|
|
11923
12110
|
for confirmation unless --yes.
|
|
12111
|
+
\`whoami\` leads with absent, accepted, rejected, or unverifiable credentials;
|
|
12112
|
+
a configured tenant alone does not prove login. Verification and subsequent
|
|
12113
|
+
telemetry each time out after 3 seconds. Status reports exit 0; --json keeps
|
|
12114
|
+
token_present boolean and token_valid true/false/null (null = unverified).
|
|
11924
12115
|
|
|
11925
12116
|
Full reference: ${DOCS_URL}
|
|
11926
12117
|
`;
|
|
@@ -11931,7 +12122,7 @@ program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
|
11931
12122
|
if (!shouldTrackCommandExecution(actionCommand.name(), Boolean(opts.url))) return;
|
|
11932
12123
|
await trackEvent(opts.url, opts.tenantId, "cli_command_executed", {
|
|
11933
12124
|
command: actionCommand.name()
|
|
11934
|
-
});
|
|
12125
|
+
}, actionCommand.name() === "whoami" ? globalThis.AbortSignal.timeout(WHOAMI_REQUEST_TIMEOUT_MS) : void 0);
|
|
11935
12126
|
});
|
|
11936
12127
|
program.name("revturbine").description("Validate RevTurbine Playbooks 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").option(NO_LOCAL_FLAG, "Ignore the repo-pinned CLI and run this installation").showHelpAfterError().addHelpText("after", HELP_AFTER);
|
|
11937
12128
|
function runInstall(manager, args, cwd) {
|
|
@@ -11941,7 +12132,26 @@ function runInstall(manager, args, cwd) {
|
|
|
11941
12132
|
child.on("close", (code) => resolve(code ?? 1));
|
|
11942
12133
|
});
|
|
11943
12134
|
}
|
|
11944
|
-
|
|
12135
|
+
function findIntegration(dir, root = dir) {
|
|
12136
|
+
if (!existsSync2(dir)) return void 0;
|
|
12137
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
12138
|
+
if (entry.name.startsWith(".") || ["node_modules", "dist", "build", "coverage", "target"].includes(entry.name)) continue;
|
|
12139
|
+
const file = path2.join(dir, entry.name);
|
|
12140
|
+
if (entry.isDirectory()) {
|
|
12141
|
+
const reason = findIntegration(file, root);
|
|
12142
|
+
if (reason) return reason;
|
|
12143
|
+
} else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|json)$/.test(entry.name) && !entry.name.endsWith("lock.json")) {
|
|
12144
|
+
const reason = integrationFileReason(path2.relative(root, file), readFileSync2(file, "utf8"));
|
|
12145
|
+
if (reason) return reason;
|
|
12146
|
+
}
|
|
12147
|
+
}
|
|
12148
|
+
return void 0;
|
|
12149
|
+
}
|
|
12150
|
+
program.command("init").alias("create").description("Set up RevTurbine while preserving existing integrations: install missing packages, create a starter for a fresh integration, and install skills for one selected agent. Offers to start a new project when no package.json exists.").option("-d, --dir <path>", "Target directory (defaults to the current directory)").option("-y, --yes", "Skip prompts \u2014 create a new project non-interactively when the directory has none").option("--dry-run", "Report what would be installed without running the package manager").option("--no-skills", "Do not install the RevTurbine Agent Skills").option("--scaffold", "Create a missing root starter Playbook even when an integration exists; never overwrite a file").option("--agent <id>", `Install skills for one agent (${SUPPORTED_AGENTS.join(", ")}); overrides environment detection`).option("--json", "Emit the scaffold plan as JSON").action(async (opts) => {
|
|
12151
|
+
if (opts.agent !== void 0 && !isAgentId(opts.agent)) {
|
|
12152
|
+
fail(EXIT.USAGE, `unknown skills agent '${opts.agent}' \u2014 choose ${SUPPORTED_AGENTS.join(", ")}.`);
|
|
12153
|
+
}
|
|
12154
|
+
const harness = detectHarness(process.env, opts.agent !== void 0 && isAgentId(opts.agent) ? opts.agent : void 0);
|
|
11945
12155
|
const dir = path2.resolve(opts.dir ?? process.cwd());
|
|
11946
12156
|
const manifestPath = path2.join(dir, "package.json");
|
|
11947
12157
|
let manifest;
|
|
@@ -11979,8 +12189,14 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
11979
12189
|
for (const note of plan.skipped) diag(`\u2022 ${note}`);
|
|
11980
12190
|
const playbookPath = path2.join(dir, STARTER_PLAYBOOK_FILENAME);
|
|
11981
12191
|
const playbookExists = existsSync2(playbookPath);
|
|
11982
|
-
|
|
11983
|
-
const
|
|
12192
|
+
const integrationReason = playbookExists || opts.scaffold ? void 0 : declaredSdk(manifest) ? "@revturbine/sdk already declared" : findIntegration(dir);
|
|
12193
|
+
const starter = planStarter({ rootExists: playbookExists, integrationReason, scaffold: opts.scaffold === true });
|
|
12194
|
+
if (starter === "present") diag(`\u2022 ${STARTER_PLAYBOOK_FILENAME} already present \u2014 left as-is`);
|
|
12195
|
+
if (starter === "skipped") diag(`\u2022 Skipping starter Playbook: ${integrationReason}. Use --scaffold to create a missing ${STARTER_PLAYBOOK_FILENAME}.`);
|
|
12196
|
+
const skillsArgs = opts.skills !== false && harness.agentId ? skillsAddArgs(harness.agentId) : null;
|
|
12197
|
+
if (opts.skills !== false && !harness.agentId) {
|
|
12198
|
+
diag(`\u2022 Skipping Agent Skills: no supported agent detected. Choose a target, for example: revturbine init --agent codex (${SUPPORTED_AGENTS.join(", ")}).`);
|
|
12199
|
+
}
|
|
11984
12200
|
if (opts.json) {
|
|
11985
12201
|
emit(
|
|
11986
12202
|
{
|
|
@@ -11990,8 +12206,9 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
11990
12206
|
stack,
|
|
11991
12207
|
install: plan.install,
|
|
11992
12208
|
skipped: plan.skipped,
|
|
11993
|
-
playbook:
|
|
11994
|
-
skills:
|
|
12209
|
+
playbook: starter === "create" ? STARTER_PLAYBOOK_FILENAME : starter,
|
|
12210
|
+
skills: skillsArgs ? SKILLS_SOURCE : "skipped",
|
|
12211
|
+
skills_agent: skillsArgs ? harness.agentId : null
|
|
11995
12212
|
},
|
|
11996
12213
|
true
|
|
11997
12214
|
);
|
|
@@ -12001,9 +12218,9 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12001
12218
|
for (const step of plan.install) {
|
|
12002
12219
|
diag(`would run: ${manager.name} ${installArgs(manager.name, step).join(" ")}`);
|
|
12003
12220
|
}
|
|
12004
|
-
if (
|
|
12005
|
-
if (
|
|
12006
|
-
if (plan.install.length === 0 &&
|
|
12221
|
+
if (starter === "create") diag(`would write: ${STARTER_PLAYBOOK_FILENAME}`);
|
|
12222
|
+
if (skillsArgs) diag(`would run: npx ${skillsArgs.join(" ")}`);
|
|
12223
|
+
if (plan.install.length === 0 && starter !== "create" && !skillsArgs) diag("\u2713 No setup changes planned.");
|
|
12007
12224
|
return;
|
|
12008
12225
|
}
|
|
12009
12226
|
for (const step of plan.install) {
|
|
@@ -12020,7 +12237,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12020
12237
|
if (plan.install.length > 0) {
|
|
12021
12238
|
diag(`\u2713 Installed the RevTurbine SDK and CLI (CLI pinned to ${pkgVersion})`);
|
|
12022
12239
|
}
|
|
12023
|
-
const playbookAdded =
|
|
12240
|
+
const playbookAdded = starter === "create";
|
|
12024
12241
|
if (playbookAdded) {
|
|
12025
12242
|
const check = validatePlaybook(STARTER_PLAYBOOK);
|
|
12026
12243
|
if (!check.ok) {
|
|
@@ -12030,13 +12247,13 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12030
12247
|
);
|
|
12031
12248
|
}
|
|
12032
12249
|
writeFileSync2(playbookPath, `${JSON.stringify(STARTER_PLAYBOOK, null, 2)}
|
|
12033
|
-
`, "utf8");
|
|
12250
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
12034
12251
|
diag(`\u2713 Added a starter playbook (${STARTER_PLAYBOOK_FILENAME} \u2014 local mode, no account needed)`);
|
|
12035
12252
|
}
|
|
12036
|
-
let skillsOutcome =
|
|
12037
|
-
if (
|
|
12253
|
+
let skillsOutcome = skillsArgs ? "installed" : opts.skills === false ? "skipped" : "unknown";
|
|
12254
|
+
if (skillsArgs) {
|
|
12038
12255
|
diag("Installing the RevTurbine Agent Skills (npx skills)\u2026");
|
|
12039
|
-
const args =
|
|
12256
|
+
const args = skillsArgs;
|
|
12040
12257
|
let code;
|
|
12041
12258
|
try {
|
|
12042
12259
|
code = await runInstall("npx", args, dir);
|
|
@@ -12050,7 +12267,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12050
12267
|
diag("\u26A0 Could not install the Agent Skills automatically. Add them by hand:");
|
|
12051
12268
|
diag(` npx ${args.join(" ")}`);
|
|
12052
12269
|
}
|
|
12053
|
-
} else {
|
|
12270
|
+
} else if (opts.skills === false) {
|
|
12054
12271
|
diag("\u2022 Skipping the Agent Skills (--no-skills)");
|
|
12055
12272
|
}
|
|
12056
12273
|
if (!opts.json) {
|
|
@@ -12060,7 +12277,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12060
12277
|
cliVersion: pkgVersion,
|
|
12061
12278
|
playbookAdded,
|
|
12062
12279
|
skills: skillsOutcome,
|
|
12063
|
-
harness
|
|
12280
|
+
harness
|
|
12064
12281
|
});
|
|
12065
12282
|
process.stdout.write(`${lines.join("\n")}
|
|
12066
12283
|
`);
|
|
@@ -12153,13 +12370,22 @@ program.command("logout").description("Remove the stored token for <url>.").argu
|
|
|
12153
12370
|
const removed = removeCredential(normalized);
|
|
12154
12371
|
diag(removed ? `Logged out of ${normalized}.` : `No stored credential for ${normalized}.`);
|
|
12155
12372
|
});
|
|
12156
|
-
program.command("whoami").description("Show
|
|
12157
|
-
const
|
|
12158
|
-
|
|
12373
|
+
program.command("whoami").description("Show authentication state first (absent, accepted, rejected, or unverifiable), then instance, tenant, and credentials source.").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) => {
|
|
12374
|
+
const diagnostics = [];
|
|
12375
|
+
const conn = connect(opts.url, opts.tenantId, (message) => diagnostics.push(message));
|
|
12376
|
+
let probeStatus = null;
|
|
12159
12377
|
if (conn.hasToken) {
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12378
|
+
probeStatus = await fetch(`${conn.url}/api/optimization/drafts`, {
|
|
12379
|
+
headers: conn.headers,
|
|
12380
|
+
signal: globalThis.AbortSignal.timeout(WHOAMI_REQUEST_TIMEOUT_MS),
|
|
12381
|
+
// A redirected login page returning 200 is not proof of authentication.
|
|
12382
|
+
redirect: "manual"
|
|
12383
|
+
}).then(async (response) => {
|
|
12384
|
+
await response.body?.cancel();
|
|
12385
|
+
return response.status;
|
|
12386
|
+
}).catch(() => null);
|
|
12387
|
+
}
|
|
12388
|
+
const tokenValid = classifyToken(conn.hasToken, probeStatus);
|
|
12163
12389
|
const data = {
|
|
12164
12390
|
instance: conn.url,
|
|
12165
12391
|
tenant: conn.tenantId,
|
|
@@ -12173,12 +12399,13 @@ program.command("whoami").description("Show the resolved instance, tenant, crede
|
|
|
12173
12399
|
data,
|
|
12174
12400
|
Boolean(opts.json),
|
|
12175
12401
|
[
|
|
12402
|
+
formatAuthentication(conn.hasToken, tokenValid),
|
|
12176
12403
|
`instance: ${data.instance}`,
|
|
12177
12404
|
`tenant: ${data.tenant} (${data.tenant_source})`,
|
|
12178
|
-
`credentials: ${data.credentials_dir} [${data.credentials_source}]
|
|
12179
|
-
`token: ${data.token_present ? tokenValid ? "present, valid" : "present, NOT accepted" : "absent \u2014 run `revturbine login`"}`
|
|
12405
|
+
`credentials: ${data.credentials_dir} [${data.credentials_source}]`
|
|
12180
12406
|
].join("\n")
|
|
12181
12407
|
);
|
|
12408
|
+
for (const message of diagnostics) diag(message);
|
|
12182
12409
|
});
|
|
12183
12410
|
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(() => {
|
|
12184
12411
|
try {
|
|
@@ -12633,6 +12860,69 @@ ingestKeys.command("revoke").description("Revoke a public ingest key by id. The
|
|
|
12633
12860
|
if (!result.ok) httpFail(conn, "ingest-keys revoke", result.status);
|
|
12634
12861
|
diag(`\u2713 Revoked ingest key ${id}.`);
|
|
12635
12862
|
});
|
|
12863
|
+
var events = program.command("events").description("Send analytics events to the RevTurbine ingest pipeline.");
|
|
12864
|
+
events.command("track").description("Ingest a batch of analytics events from a file or inline JSON.").option("-f, --file <path>", 'Batch file: JSON array, { "events": [...] } envelope, or NDJSON').option("-e, --event <json>", "A single event as inline JSON").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (selects among your tenants; defaults to the stored token tenant)").option("--json", "Machine-readable per-batch results").addHelpText(
|
|
12865
|
+
"after",
|
|
12866
|
+
[
|
|
12867
|
+
"",
|
|
12868
|
+
"Examples:",
|
|
12869
|
+
" revturbine events track --file ./events.ndjson",
|
|
12870
|
+
" revturbine events track --file ./batch.json --json",
|
|
12871
|
+
` revturbine events track --event '{"environment_id":"production","user_id":"u1","account_id":"a1","event_name":"product_used","event_ts":"2026-09-10T00:00:00.000Z"}'`,
|
|
12872
|
+
"",
|
|
12873
|
+
`Every event needs environment_id, user_id, account_id, event_name and`,
|
|
12874
|
+
`event_ts. Batches larger than ${MAX_EVENTS_PER_BATCH} are chunked automatically.`,
|
|
12875
|
+
"",
|
|
12876
|
+
"This authenticates with your device-auth token, so it needs no ingest key",
|
|
12877
|
+
"and no browser origin. Quarantined and rejected counts are always reported",
|
|
12878
|
+
'separately from accepted \u2014 a load is not "clean" because it returned 202.'
|
|
12879
|
+
].join("\n")
|
|
12880
|
+
).action(async (opts) => {
|
|
12881
|
+
if (!opts.file && !opts.event) fail(EXIT.USAGE, "events track needs --file <path> or --event <json>");
|
|
12882
|
+
if (opts.file && opts.event) fail(EXIT.USAGE, "events track takes --file or --event, not both");
|
|
12883
|
+
let text;
|
|
12884
|
+
if (opts.file) {
|
|
12885
|
+
try {
|
|
12886
|
+
text = readFileSync2(path2.resolve(opts.file), "utf8");
|
|
12887
|
+
} catch (err) {
|
|
12888
|
+
fail(EXIT.USAGE, `cannot read ${opts.file}: ${err.message}`);
|
|
12889
|
+
}
|
|
12890
|
+
} else {
|
|
12891
|
+
text = opts.event;
|
|
12892
|
+
}
|
|
12893
|
+
const parsed = parseEventBatch(text);
|
|
12894
|
+
if (parsed.errors.length > 0) {
|
|
12895
|
+
for (const e of parsed.errors) diagRaw(` ${e}`);
|
|
12896
|
+
fail(EXIT.VALIDATION, `${parsed.errors.length} event(s) failed local validation; nothing was sent`);
|
|
12897
|
+
}
|
|
12898
|
+
if (parsed.events.length === 0) fail(EXIT.VALIDATION, "no events to send");
|
|
12899
|
+
const conn = connect(opts.url, opts.tenantId);
|
|
12900
|
+
const chunks = chunkEvents(parsed.events);
|
|
12901
|
+
const results = [];
|
|
12902
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
12903
|
+
if (chunks.length > 1 && !opts.json) {
|
|
12904
|
+
diag(`sending batch ${i + 1}/${chunks.length} (${chunk.length} event(s))\u2026`);
|
|
12905
|
+
}
|
|
12906
|
+
let posted;
|
|
12907
|
+
try {
|
|
12908
|
+
posted = await postBatch(conn.url, conn.headers, chunk);
|
|
12909
|
+
} catch (err) {
|
|
12910
|
+
if (isNetworkError(err)) {
|
|
12911
|
+
fail(EXIT.NETWORK, `network failure reaching ${conn.url}: ${err.message}`);
|
|
12912
|
+
}
|
|
12913
|
+
throw err;
|
|
12914
|
+
}
|
|
12915
|
+
if (!posted.ok || !posted.result) {
|
|
12916
|
+
if (results.length > 0) {
|
|
12917
|
+
diag(`${summarize(results, parsed.events.length).accepted} event(s) were accepted before this failure.`);
|
|
12918
|
+
}
|
|
12919
|
+
httpFail(conn, `events track (batch ${i + 1}/${chunks.length})`, posted.status, posted.error);
|
|
12920
|
+
}
|
|
12921
|
+
results.push(posted.result);
|
|
12922
|
+
}
|
|
12923
|
+
const summary = summarize(results, parsed.events.length);
|
|
12924
|
+
emit(summary, !!opts.json, formatSummary(summary));
|
|
12925
|
+
});
|
|
12636
12926
|
var COMMAND_EXAMPLES = {
|
|
12637
12927
|
download: [
|
|
12638
12928
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revturbine/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
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": {
|