@revturbine/cli 0.17.1 → 0.18.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 +45 -1
- package/dist/cli.js +301 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
63
63
|
|
|
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
69
|
| `whoami` | The resolved instance, tenant, credentials source, and whether the stored token works. |
|
|
@@ -83,10 +83,54 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
83
83
|
| `evaluate` | Run a config version's placement/entitlement decisions locally for a user context. Use `--slot` with optional `--component-type`; deprecated `--surface-type` remains an alias. |
|
|
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
|
+
| `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.
|
|
86
98
|
|
|
87
99
|
`--json` on read commands emits machine-readable output. Results go to stdout,
|
|
88
100
|
diagnostics to stderr.
|
|
89
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
|
+
|
|
121
|
+
### Mint a publishable ingest token
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
revturbine login
|
|
125
|
+
revturbine ingest-keys mint --origin https://app.example.com --json
|
|
126
|
+
# Store the once-only token, then use the returned id when it is no longer needed:
|
|
127
|
+
revturbine ingest-keys revoke <ingest-key-id> --yes
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Public ingest tokens are intentionally embeddable but remain tenant-bound,
|
|
131
|
+
origin-restricted, and optionally IP-restricted. They cannot mint other tokens
|
|
132
|
+
or access private control-plane APIs.
|
|
133
|
+
|
|
90
134
|
## Exit-code classes
|
|
91
135
|
|
|
92
136
|
```
|
package/dist/cli.js
CHANGED
|
@@ -10981,6 +10981,130 @@ function formatIngestKeyLine(key2) {
|
|
|
10981
10981
|
return `${key2.id} ${key2.tokenPreview} origins: ${origins}${ips}${last}`;
|
|
10982
10982
|
}
|
|
10983
10983
|
|
|
10984
|
+
// src/lib/events-ingest.ts
|
|
10985
|
+
var MAX_EVENTS_PER_BATCH = 500;
|
|
10986
|
+
var REQUIRED_FIELDS = [
|
|
10987
|
+
"environment_id",
|
|
10988
|
+
"user_id",
|
|
10989
|
+
"account_id",
|
|
10990
|
+
"event_name",
|
|
10991
|
+
"event_ts"
|
|
10992
|
+
];
|
|
10993
|
+
function parseEventBatch(text) {
|
|
10994
|
+
const trimmed = text.trim();
|
|
10995
|
+
if (trimmed === "") return { events: [], errors: ["input is empty"] };
|
|
10996
|
+
const errors = [];
|
|
10997
|
+
let raw;
|
|
10998
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
10999
|
+
let parsed;
|
|
11000
|
+
try {
|
|
11001
|
+
parsed = JSON.parse(trimmed);
|
|
11002
|
+
} catch {
|
|
11003
|
+
return parseNdjson(trimmed);
|
|
11004
|
+
}
|
|
11005
|
+
if (Array.isArray(parsed)) {
|
|
11006
|
+
raw = parsed;
|
|
11007
|
+
} else if (parsed !== null && typeof parsed === "object" && Array.isArray(parsed.events)) {
|
|
11008
|
+
raw = parsed.events;
|
|
11009
|
+
} else if (parsed !== null && typeof parsed === "object") {
|
|
11010
|
+
raw = [parsed];
|
|
11011
|
+
} else {
|
|
11012
|
+
return { events: [], errors: ["input must be an object, an array, or an { events: [...] } envelope"] };
|
|
11013
|
+
}
|
|
11014
|
+
} else {
|
|
11015
|
+
return parseNdjson(trimmed);
|
|
11016
|
+
}
|
|
11017
|
+
const events2 = validateAll(raw, errors);
|
|
11018
|
+
return { events: events2, errors };
|
|
11019
|
+
}
|
|
11020
|
+
function parseNdjson(text) {
|
|
11021
|
+
const errors = [];
|
|
11022
|
+
const raw = [];
|
|
11023
|
+
text.split("\n").forEach((line, i) => {
|
|
11024
|
+
const t = line.trim();
|
|
11025
|
+
if (t === "") return;
|
|
11026
|
+
try {
|
|
11027
|
+
raw.push(JSON.parse(t));
|
|
11028
|
+
} catch {
|
|
11029
|
+
errors.push(`line ${i + 1}: not valid JSON`);
|
|
11030
|
+
}
|
|
11031
|
+
});
|
|
11032
|
+
const events2 = validateAll(raw, errors);
|
|
11033
|
+
return { events: events2, errors };
|
|
11034
|
+
}
|
|
11035
|
+
function validateAll(raw, errors) {
|
|
11036
|
+
const events2 = [];
|
|
11037
|
+
raw.forEach((item, i) => {
|
|
11038
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
11039
|
+
errors.push(`event ${i}: not a JSON object`);
|
|
11040
|
+
return;
|
|
11041
|
+
}
|
|
11042
|
+
const bag = item;
|
|
11043
|
+
const missing = REQUIRED_FIELDS.filter(
|
|
11044
|
+
(f) => typeof bag[f] !== "string" || bag[f].trim() === ""
|
|
11045
|
+
);
|
|
11046
|
+
if (missing.length > 0) {
|
|
11047
|
+
errors.push(`event ${i}: missing or empty ${missing.join(", ")}`);
|
|
11048
|
+
return;
|
|
11049
|
+
}
|
|
11050
|
+
events2.push(bag);
|
|
11051
|
+
});
|
|
11052
|
+
return events2;
|
|
11053
|
+
}
|
|
11054
|
+
function chunkEvents(events2, size = MAX_EVENTS_PER_BATCH) {
|
|
11055
|
+
const limit = Math.max(1, Math.min(size, MAX_EVENTS_PER_BATCH));
|
|
11056
|
+
const out = [];
|
|
11057
|
+
for (let i = 0; i < events2.length; i += limit) out.push(events2.slice(i, i + limit));
|
|
11058
|
+
return out;
|
|
11059
|
+
}
|
|
11060
|
+
function readResult(json) {
|
|
11061
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
11062
|
+
return {
|
|
11063
|
+
accepted: num(json.accepted),
|
|
11064
|
+
quarantined: num(json.quarantined),
|
|
11065
|
+
dropped_unscoped: num(json.dropped_unscoped),
|
|
11066
|
+
rejected: Array.isArray(json.rejected) ? json.rejected : [],
|
|
11067
|
+
request_id: typeof json.request_id === "string" ? json.request_id : null
|
|
11068
|
+
};
|
|
11069
|
+
}
|
|
11070
|
+
async function postBatch(baseUrl, headers, events2, fetchImpl = fetch) {
|
|
11071
|
+
const res = await fetchImpl(`${baseUrl}/api/track`, {
|
|
11072
|
+
method: "POST",
|
|
11073
|
+
headers,
|
|
11074
|
+
body: JSON.stringify({ events: events2 })
|
|
11075
|
+
});
|
|
11076
|
+
const json = await res.json().catch(() => ({}));
|
|
11077
|
+
if (!res.ok) return { ok: false, status: res.status, result: null, error: json };
|
|
11078
|
+
return { ok: true, status: res.status, result: readResult(json), error: {} };
|
|
11079
|
+
}
|
|
11080
|
+
function summarize(results, eventCount) {
|
|
11081
|
+
return {
|
|
11082
|
+
batches: results.length,
|
|
11083
|
+
// @revturbine-graph gref:3f4d51af0a0af98edea0
|
|
11084
|
+
events: eventCount,
|
|
11085
|
+
accepted: results.reduce((n, r) => n + r.accepted, 0),
|
|
11086
|
+
quarantined: results.reduce((n, r) => n + r.quarantined, 0),
|
|
11087
|
+
dropped_unscoped: results.reduce((n, r) => n + r.dropped_unscoped, 0),
|
|
11088
|
+
rejected: results.reduce((n, r) => n + r.rejected.length, 0),
|
|
11089
|
+
results
|
|
11090
|
+
};
|
|
11091
|
+
}
|
|
11092
|
+
function formatSummary(summary) {
|
|
11093
|
+
const lines = [
|
|
11094
|
+
`ingested ${summary.accepted}/${summary.events} event(s) in ${summary.batches} batch(es)`
|
|
11095
|
+
];
|
|
11096
|
+
if (summary.quarantined > 0) {
|
|
11097
|
+
lines.push(` quarantined: ${summary.quarantined} (payload failed its platform contract)`);
|
|
11098
|
+
}
|
|
11099
|
+
if (summary.rejected > 0) {
|
|
11100
|
+
lines.push(` rejected: ${summary.rejected} (malformed on the wire)`);
|
|
11101
|
+
}
|
|
11102
|
+
if (summary.dropped_unscoped > 0) {
|
|
11103
|
+
lines.push(` dropped (unscoped): ${summary.dropped_unscoped} (simulation rows without a dataset id)`);
|
|
11104
|
+
}
|
|
11105
|
+
return lines.join("\n");
|
|
11106
|
+
}
|
|
11107
|
+
|
|
10984
11108
|
// src/lib/delegate.ts
|
|
10985
11109
|
var DELEGATION_ENV = "REVTURBINE_DELEGATED";
|
|
10986
11110
|
var NO_LOCAL_FLAG = "--no-local";
|
|
@@ -11116,6 +11240,29 @@ function detectStack(signals) {
|
|
|
11116
11240
|
}
|
|
11117
11241
|
var SDK_PACKAGE = "@revturbine/sdk";
|
|
11118
11242
|
var CLI_PACKAGE = "@revturbine/cli";
|
|
11243
|
+
function declaredSdk(manifest) {
|
|
11244
|
+
return Boolean(manifest.dependencies?.[SDK_PACKAGE] || manifest.devDependencies?.[SDK_PACKAGE]);
|
|
11245
|
+
}
|
|
11246
|
+
function integrationFileReason(file, content) {
|
|
11247
|
+
if (file.endsWith("revturbine.playbook.json")) return `existing Playbook (${file})`;
|
|
11248
|
+
if (/\.[cm]?[jt]sx?$/.test(file) && /['"]@revturbine\/sdk(?:\/[^'"]*)?['"]/.test(content)) {
|
|
11249
|
+
return `SDK integration (${file})`;
|
|
11250
|
+
}
|
|
11251
|
+
if (file.endsWith(".json")) {
|
|
11252
|
+
try {
|
|
11253
|
+
const value = JSON.parse(content);
|
|
11254
|
+
if (value && typeof value === "object" && "artifact_type" in value && value.artifact_type === "playbook") {
|
|
11255
|
+
return `existing Playbook (${file})`;
|
|
11256
|
+
}
|
|
11257
|
+
} catch {
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
return void 0;
|
|
11261
|
+
}
|
|
11262
|
+
function planStarter(params) {
|
|
11263
|
+
if (params.rootExists) return "present";
|
|
11264
|
+
return params.integrationReason && !params.scaffold ? "skipped" : "create";
|
|
11265
|
+
}
|
|
11119
11266
|
function projectNameFromDir(dirName) {
|
|
11120
11267
|
const slug = dirName.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
|
|
11121
11268
|
return slug || "my-app";
|
|
@@ -11208,20 +11355,34 @@ function validatePlaybook(config) {
|
|
|
11208
11355
|
// src/lib/init-skills.ts
|
|
11209
11356
|
var SKILLS_SOURCE = "revt-eng/revturbine-skills";
|
|
11210
11357
|
var START_HERE_SKILL = "revturbine-start-here";
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11358
|
+
var SUPPORTED_AGENTS = ["claude-code", "cursor", "codex"];
|
|
11359
|
+
function isAgentId(value) {
|
|
11360
|
+
return SUPPORTED_AGENTS.some((id) => id === value);
|
|
11361
|
+
}
|
|
11362
|
+
function detectHarness(env, explicit) {
|
|
11363
|
+
let agentId = explicit;
|
|
11364
|
+
if (!agentId) {
|
|
11365
|
+
if (env["CLAUDECODE"] || env["CLAUDE_CODE_SESSION_ID"] || env["CLAUDE_CODE_ENTRYPOINT"]) agentId = "claude-code";
|
|
11366
|
+
else if (env["CURSOR_TRACE_ID"] || env["CURSOR"]) agentId = "cursor";
|
|
11367
|
+
else if (env["CODEX_THREAD_ID"]) agentId = "codex";
|
|
11214
11368
|
}
|
|
11215
|
-
if (
|
|
11216
|
-
return { label: "
|
|
11369
|
+
if (agentId === "claude-code") {
|
|
11370
|
+
return { agentId, label: "Claude Code", invocation: `Run the skill: /${START_HERE_SKILL}` };
|
|
11371
|
+
}
|
|
11372
|
+
if (agentId === "cursor") {
|
|
11373
|
+
return { agentId, label: "Cursor", invocation: `Mention the skill: @${START_HERE_SKILL}` };
|
|
11374
|
+
}
|
|
11375
|
+
if (agentId === "codex") {
|
|
11376
|
+
return { agentId, label: "Codex", invocation: `Run the skill: $${START_HERE_SKILL}` };
|
|
11217
11377
|
}
|
|
11218
11378
|
return {
|
|
11379
|
+
agentId: null,
|
|
11219
11380
|
label: null,
|
|
11220
11381
|
invocation: `Ask your coding agent to run the "${START_HERE_SKILL}" skill.`
|
|
11221
11382
|
};
|
|
11222
11383
|
}
|
|
11223
|
-
function skillsAddArgs(
|
|
11224
|
-
return ["--yes", "skills", "add",
|
|
11384
|
+
function skillsAddArgs(agent) {
|
|
11385
|
+
return ["--yes", "skills", "add", SKILLS_SOURCE, "-y", "--copy", "-a", agent];
|
|
11225
11386
|
}
|
|
11226
11387
|
function finalOutputLines(params) {
|
|
11227
11388
|
const lines = ["", "RevTurbine is set up.", ""];
|
|
@@ -11235,12 +11396,14 @@ function finalOutputLines(params) {
|
|
|
11235
11396
|
lines.push(" \u2713 Installed the Agent Skills");
|
|
11236
11397
|
} else if (params.skills === "skipped") {
|
|
11237
11398
|
lines.push(" \u2022 Skipped the Agent Skills (--no-skills)");
|
|
11399
|
+
} else if (params.skills === "unknown") {
|
|
11400
|
+
lines.push(" \u2022 Agent Skills not installed \u2014 select a target with --agent");
|
|
11238
11401
|
} else {
|
|
11239
11402
|
lines.push(" \u26A0 Agent Skills not installed \u2014 see above to add them by hand");
|
|
11240
11403
|
}
|
|
11241
11404
|
lines.push("", "Next step:");
|
|
11242
11405
|
lines.push(` ${params.harness.invocation}`);
|
|
11243
|
-
if (params.harness.label) lines.push(` (
|
|
11406
|
+
if (params.harness.label) lines.push(` (${params.harness.label})`);
|
|
11244
11407
|
lines.push("", "Your setup path:");
|
|
11245
11408
|
lines.push(" create playbook \u2192 app wiring \u2192 billing \u2192 verify \u2192 launch");
|
|
11246
11409
|
lines.push("", "Docs: https://revturbine.com/docs", "");
|
|
@@ -11878,7 +12041,8 @@ Command groups:
|
|
|
11878
12041
|
Inspect status, history, preview, evaluate
|
|
11879
12042
|
Codegen generate types
|
|
11880
12043
|
Analytics analytics catalog|templates|views|view|create|preview|query
|
|
11881
|
-
Keys ingest-keys create|list|revoke
|
|
12044
|
+
Keys ingest-keys create (alias: mint)|list|revoke
|
|
12045
|
+
Events events track
|
|
11882
12046
|
|
|
11883
12047
|
Version selectors (no defaults \u2014 a command that reads a config requires one):
|
|
11884
12048
|
<file> a local Playbook file (positional, or --file <path>)
|
|
@@ -11889,6 +12053,10 @@ Version selectors (no defaults \u2014 a command that reads a config requires one
|
|
|
11889
12053
|
Common workflows:
|
|
11890
12054
|
# Add RevTurbine to an app (same routine as \`npm create revturbine@latest\`)
|
|
11891
12055
|
revturbine init
|
|
12056
|
+
# Preserve an existing integration; select one skills target explicitly
|
|
12057
|
+
revturbine init --agent codex
|
|
12058
|
+
# Opt into a missing root starter; existing Playbooks are never overwritten
|
|
12059
|
+
revturbine init --scaffold --no-skills
|
|
11892
12060
|
|
|
11893
12061
|
# Author, validate, and ship against the default instance (revturbine.com/app)
|
|
11894
12062
|
revturbine login
|
|
@@ -11908,6 +12076,14 @@ Common workflows:
|
|
|
11908
12076
|
revturbine history
|
|
11909
12077
|
revturbine restore <playbook-version-id> --launch
|
|
11910
12078
|
|
|
12079
|
+
# Mint an origin-restricted browser token (shown once), then revoke it
|
|
12080
|
+
revturbine ingest-keys mint --origin https://app.example.com --json
|
|
12081
|
+
|
|
12082
|
+
# Send analytics events with your own login (no ingest key, no browser origin)
|
|
12083
|
+
revturbine events track --file ./events.ndjson
|
|
12084
|
+
revturbine ingest-keys list
|
|
12085
|
+
revturbine ingest-keys revoke <ingest-key-id> --yes
|
|
12086
|
+
|
|
11911
12087
|
Exit-code classes: 0 ok \xB7 1 unexpected \xB7 2 usage \xB7 3 auth \xB7 4 validation
|
|
11912
12088
|
blocked \xB7 5 conflict/stale \xB7 6 network \xB7 7 server error.
|
|
11913
12089
|
|
|
@@ -11936,7 +12112,26 @@ function runInstall(manager, args, cwd) {
|
|
|
11936
12112
|
child.on("close", (code) => resolve(code ?? 1));
|
|
11937
12113
|
});
|
|
11938
12114
|
}
|
|
11939
|
-
|
|
12115
|
+
function findIntegration(dir, root = dir) {
|
|
12116
|
+
if (!existsSync2(dir)) return void 0;
|
|
12117
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
12118
|
+
if (entry.name.startsWith(".") || ["node_modules", "dist", "build", "coverage", "target"].includes(entry.name)) continue;
|
|
12119
|
+
const file = path2.join(dir, entry.name);
|
|
12120
|
+
if (entry.isDirectory()) {
|
|
12121
|
+
const reason = findIntegration(file, root);
|
|
12122
|
+
if (reason) return reason;
|
|
12123
|
+
} else if (entry.isFile() && /\.(?:[cm]?[jt]sx?|json)$/.test(entry.name) && !entry.name.endsWith("lock.json")) {
|
|
12124
|
+
const reason = integrationFileReason(path2.relative(root, file), readFileSync2(file, "utf8"));
|
|
12125
|
+
if (reason) return reason;
|
|
12126
|
+
}
|
|
12127
|
+
}
|
|
12128
|
+
return void 0;
|
|
12129
|
+
}
|
|
12130
|
+
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) => {
|
|
12131
|
+
if (opts.agent !== void 0 && !isAgentId(opts.agent)) {
|
|
12132
|
+
fail(EXIT.USAGE, `unknown skills agent '${opts.agent}' \u2014 choose ${SUPPORTED_AGENTS.join(", ")}.`);
|
|
12133
|
+
}
|
|
12134
|
+
const harness = detectHarness(process.env, opts.agent !== void 0 && isAgentId(opts.agent) ? opts.agent : void 0);
|
|
11940
12135
|
const dir = path2.resolve(opts.dir ?? process.cwd());
|
|
11941
12136
|
const manifestPath = path2.join(dir, "package.json");
|
|
11942
12137
|
let manifest;
|
|
@@ -11974,8 +12169,14 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
11974
12169
|
for (const note of plan.skipped) diag(`\u2022 ${note}`);
|
|
11975
12170
|
const playbookPath = path2.join(dir, STARTER_PLAYBOOK_FILENAME);
|
|
11976
12171
|
const playbookExists = existsSync2(playbookPath);
|
|
11977
|
-
|
|
11978
|
-
const
|
|
12172
|
+
const integrationReason = playbookExists || opts.scaffold ? void 0 : declaredSdk(manifest) ? "@revturbine/sdk already declared" : findIntegration(dir);
|
|
12173
|
+
const starter = planStarter({ rootExists: playbookExists, integrationReason, scaffold: opts.scaffold === true });
|
|
12174
|
+
if (starter === "present") diag(`\u2022 ${STARTER_PLAYBOOK_FILENAME} already present \u2014 left as-is`);
|
|
12175
|
+
if (starter === "skipped") diag(`\u2022 Skipping starter Playbook: ${integrationReason}. Use --scaffold to create a missing ${STARTER_PLAYBOOK_FILENAME}.`);
|
|
12176
|
+
const skillsArgs = opts.skills !== false && harness.agentId ? skillsAddArgs(harness.agentId) : null;
|
|
12177
|
+
if (opts.skills !== false && !harness.agentId) {
|
|
12178
|
+
diag(`\u2022 Skipping Agent Skills: no supported agent detected. Choose a target, for example: revturbine init --agent codex (${SUPPORTED_AGENTS.join(", ")}).`);
|
|
12179
|
+
}
|
|
11979
12180
|
if (opts.json) {
|
|
11980
12181
|
emit(
|
|
11981
12182
|
{
|
|
@@ -11985,8 +12186,9 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
11985
12186
|
stack,
|
|
11986
12187
|
install: plan.install,
|
|
11987
12188
|
skipped: plan.skipped,
|
|
11988
|
-
playbook:
|
|
11989
|
-
skills:
|
|
12189
|
+
playbook: starter === "create" ? STARTER_PLAYBOOK_FILENAME : starter,
|
|
12190
|
+
skills: skillsArgs ? SKILLS_SOURCE : "skipped",
|
|
12191
|
+
skills_agent: skillsArgs ? harness.agentId : null
|
|
11990
12192
|
},
|
|
11991
12193
|
true
|
|
11992
12194
|
);
|
|
@@ -11996,9 +12198,9 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
11996
12198
|
for (const step of plan.install) {
|
|
11997
12199
|
diag(`would run: ${manager.name} ${installArgs(manager.name, step).join(" ")}`);
|
|
11998
12200
|
}
|
|
11999
|
-
if (
|
|
12000
|
-
if (
|
|
12001
|
-
if (plan.install.length === 0 &&
|
|
12201
|
+
if (starter === "create") diag(`would write: ${STARTER_PLAYBOOK_FILENAME}`);
|
|
12202
|
+
if (skillsArgs) diag(`would run: npx ${skillsArgs.join(" ")}`);
|
|
12203
|
+
if (plan.install.length === 0 && starter !== "create" && !skillsArgs) diag("\u2713 No setup changes planned.");
|
|
12002
12204
|
return;
|
|
12003
12205
|
}
|
|
12004
12206
|
for (const step of plan.install) {
|
|
@@ -12015,7 +12217,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12015
12217
|
if (plan.install.length > 0) {
|
|
12016
12218
|
diag(`\u2713 Installed the RevTurbine SDK and CLI (CLI pinned to ${pkgVersion})`);
|
|
12017
12219
|
}
|
|
12018
|
-
const playbookAdded =
|
|
12220
|
+
const playbookAdded = starter === "create";
|
|
12019
12221
|
if (playbookAdded) {
|
|
12020
12222
|
const check = validatePlaybook(STARTER_PLAYBOOK);
|
|
12021
12223
|
if (!check.ok) {
|
|
@@ -12025,13 +12227,13 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12025
12227
|
);
|
|
12026
12228
|
}
|
|
12027
12229
|
writeFileSync2(playbookPath, `${JSON.stringify(STARTER_PLAYBOOK, null, 2)}
|
|
12028
|
-
`, "utf8");
|
|
12230
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
12029
12231
|
diag(`\u2713 Added a starter playbook (${STARTER_PLAYBOOK_FILENAME} \u2014 local mode, no account needed)`);
|
|
12030
12232
|
}
|
|
12031
|
-
let skillsOutcome =
|
|
12032
|
-
if (
|
|
12233
|
+
let skillsOutcome = skillsArgs ? "installed" : opts.skills === false ? "skipped" : "unknown";
|
|
12234
|
+
if (skillsArgs) {
|
|
12033
12235
|
diag("Installing the RevTurbine Agent Skills (npx skills)\u2026");
|
|
12034
|
-
const args =
|
|
12236
|
+
const args = skillsArgs;
|
|
12035
12237
|
let code;
|
|
12036
12238
|
try {
|
|
12037
12239
|
code = await runInstall("npx", args, dir);
|
|
@@ -12045,7 +12247,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12045
12247
|
diag("\u26A0 Could not install the Agent Skills automatically. Add them by hand:");
|
|
12046
12248
|
diag(` npx ${args.join(" ")}`);
|
|
12047
12249
|
}
|
|
12048
|
-
} else {
|
|
12250
|
+
} else if (opts.skills === false) {
|
|
12049
12251
|
diag("\u2022 Skipping the Agent Skills (--no-skills)");
|
|
12050
12252
|
}
|
|
12051
12253
|
if (!opts.json) {
|
|
@@ -12055,7 +12257,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
|
|
|
12055
12257
|
cliVersion: pkgVersion,
|
|
12056
12258
|
playbookAdded,
|
|
12057
12259
|
skills: skillsOutcome,
|
|
12058
|
-
harness
|
|
12260
|
+
harness
|
|
12059
12261
|
});
|
|
12060
12262
|
process.stdout.write(`${lines.join("\n")}
|
|
12061
12263
|
`);
|
|
@@ -12579,7 +12781,18 @@ analyticsConnectionOptions(
|
|
|
12579
12781
|
emitAnalyticsResult(conn, "analytics query", result, opts.json);
|
|
12580
12782
|
});
|
|
12581
12783
|
var ingestKeys = program.command("ingest-keys").description("Manage public ingest keys (embeddable SDK telemetry credentials) for the tenant.");
|
|
12582
|
-
ingestKeys.command("create").description("Mint a public ingest key. The full token is printed ONCE and cannot be retrieved again.").requiredOption("--origin <url...>", "Allowed browser origin(s), e.g. https://app.example.com (repeatable; at least one required)").option("--ip <cidr...>", "Optional IP / CIDR allowlist (empty = no IP restriction)").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 (token on stdout for scripted capture)").
|
|
12784
|
+
ingestKeys.command("create").alias("mint").description("Mint a public ingest key. The full token is printed ONCE and cannot be retrieved again.").requiredOption("--origin <url...>", "Allowed browser origin(s), e.g. https://app.example.com (repeatable; at least one required)").option("--ip <cidr...>", "Optional IP / CIDR allowlist (empty = no IP restriction)").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 (token on stdout for scripted capture)").addHelpText(
|
|
12785
|
+
"after",
|
|
12786
|
+
[
|
|
12787
|
+
"",
|
|
12788
|
+
"Examples:",
|
|
12789
|
+
" revturbine ingest-keys mint --origin https://app.example.com",
|
|
12790
|
+
" revturbine ingest-keys create --origin https://app.example.com --ip 203.0.113.0/24 --json",
|
|
12791
|
+
"",
|
|
12792
|
+
"The full token is returned only by this command. Store it securely or",
|
|
12793
|
+
"capture --json output, and revoke it by id when the integration or test ends."
|
|
12794
|
+
].join("\n")
|
|
12795
|
+
).action(async (opts) => {
|
|
12583
12796
|
const conn = connect(opts.url, opts.tenantId);
|
|
12584
12797
|
const result = await createIngestKey(conn.url, conn.headers, { originAllowlist: opts.origin, ipAllowlist: opts.ip });
|
|
12585
12798
|
if (!result.ok || !result.key) httpFail(conn, "ingest-keys create", result.status, result.error);
|
|
@@ -12617,6 +12830,69 @@ ingestKeys.command("revoke").description("Revoke a public ingest key by id. The
|
|
|
12617
12830
|
if (!result.ok) httpFail(conn, "ingest-keys revoke", result.status);
|
|
12618
12831
|
diag(`\u2713 Revoked ingest key ${id}.`);
|
|
12619
12832
|
});
|
|
12833
|
+
var events = program.command("events").description("Send analytics events to the RevTurbine ingest pipeline.");
|
|
12834
|
+
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(
|
|
12835
|
+
"after",
|
|
12836
|
+
[
|
|
12837
|
+
"",
|
|
12838
|
+
"Examples:",
|
|
12839
|
+
" revturbine events track --file ./events.ndjson",
|
|
12840
|
+
" revturbine events track --file ./batch.json --json",
|
|
12841
|
+
` 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"}'`,
|
|
12842
|
+
"",
|
|
12843
|
+
`Every event needs environment_id, user_id, account_id, event_name and`,
|
|
12844
|
+
`event_ts. Batches larger than ${MAX_EVENTS_PER_BATCH} are chunked automatically.`,
|
|
12845
|
+
"",
|
|
12846
|
+
"This authenticates with your device-auth token, so it needs no ingest key",
|
|
12847
|
+
"and no browser origin. Quarantined and rejected counts are always reported",
|
|
12848
|
+
'separately from accepted \u2014 a load is not "clean" because it returned 202.'
|
|
12849
|
+
].join("\n")
|
|
12850
|
+
).action(async (opts) => {
|
|
12851
|
+
if (!opts.file && !opts.event) fail(EXIT.USAGE, "events track needs --file <path> or --event <json>");
|
|
12852
|
+
if (opts.file && opts.event) fail(EXIT.USAGE, "events track takes --file or --event, not both");
|
|
12853
|
+
let text;
|
|
12854
|
+
if (opts.file) {
|
|
12855
|
+
try {
|
|
12856
|
+
text = readFileSync2(path2.resolve(opts.file), "utf8");
|
|
12857
|
+
} catch (err) {
|
|
12858
|
+
fail(EXIT.USAGE, `cannot read ${opts.file}: ${err.message}`);
|
|
12859
|
+
}
|
|
12860
|
+
} else {
|
|
12861
|
+
text = opts.event;
|
|
12862
|
+
}
|
|
12863
|
+
const parsed = parseEventBatch(text);
|
|
12864
|
+
if (parsed.errors.length > 0) {
|
|
12865
|
+
for (const e of parsed.errors) diagRaw(` ${e}`);
|
|
12866
|
+
fail(EXIT.VALIDATION, `${parsed.errors.length} event(s) failed local validation; nothing was sent`);
|
|
12867
|
+
}
|
|
12868
|
+
if (parsed.events.length === 0) fail(EXIT.VALIDATION, "no events to send");
|
|
12869
|
+
const conn = connect(opts.url, opts.tenantId);
|
|
12870
|
+
const chunks = chunkEvents(parsed.events);
|
|
12871
|
+
const results = [];
|
|
12872
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
12873
|
+
if (chunks.length > 1 && !opts.json) {
|
|
12874
|
+
diag(`sending batch ${i + 1}/${chunks.length} (${chunk.length} event(s))\u2026`);
|
|
12875
|
+
}
|
|
12876
|
+
let posted;
|
|
12877
|
+
try {
|
|
12878
|
+
posted = await postBatch(conn.url, conn.headers, chunk);
|
|
12879
|
+
} catch (err) {
|
|
12880
|
+
if (isNetworkError(err)) {
|
|
12881
|
+
fail(EXIT.NETWORK, `network failure reaching ${conn.url}: ${err.message}`);
|
|
12882
|
+
}
|
|
12883
|
+
throw err;
|
|
12884
|
+
}
|
|
12885
|
+
if (!posted.ok || !posted.result) {
|
|
12886
|
+
if (results.length > 0) {
|
|
12887
|
+
diag(`${summarize(results, parsed.events.length).accepted} event(s) were accepted before this failure.`);
|
|
12888
|
+
}
|
|
12889
|
+
httpFail(conn, `events track (batch ${i + 1}/${chunks.length})`, posted.status, posted.error);
|
|
12890
|
+
}
|
|
12891
|
+
results.push(posted.result);
|
|
12892
|
+
}
|
|
12893
|
+
const summary = summarize(results, parsed.events.length);
|
|
12894
|
+
emit(summary, !!opts.json, formatSummary(summary));
|
|
12895
|
+
});
|
|
12620
12896
|
var COMMAND_EXAMPLES = {
|
|
12621
12897
|
download: [
|
|
12622
12898
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revturbine/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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": {
|