@revturbine/cli 0.17.2 → 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.
Files changed (3) hide show
  1. package/README.md +31 -1
  2. package/dist/cli.js +283 -23
  3. 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`) | Scaffold RevTurbine into this app: detect the package manager and stack, install the SDK, pin the CLI exactly, drop a starter Playbook, and install the Agent Skills. In a directory with no `package.json` it offers to start a new project (`--yes` to skip the prompt); `--dir`, `--dry-run`, `--no-skills`, `--json`. Runs the invoked CLI even inside a repo that pins a different one — setup establishes the pin, so it never delegates. |
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. |
@@ -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
@@ -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
- function detectHarness(env) {
11212
- if (env["CLAUDECODE"] || env["CLAUDE_CODE_SESSION_ID"] || env["CLAUDE_CODE_ENTRYPOINT"]) {
11213
- return { label: "Claude Code", invocation: `Run the skill: /${START_HERE_SKILL}` };
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";
11368
+ }
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}` };
11214
11374
  }
11215
- if (env["CURSOR_TRACE_ID"] || env["CURSOR"]) {
11216
- return { label: "Cursor", invocation: `Mention the skill: @${START_HERE_SKILL}` };
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(source = SKILLS_SOURCE) {
11224
- return ["--yes", "skills", "add", source, "-y", "--copy"];
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(` (detected ${params.harness.label})`);
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", "");
@@ -11879,6 +12042,7 @@ Command groups:
11879
12042
  Codegen generate types
11880
12043
  Analytics analytics catalog|templates|views|view|create|preview|query
11881
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
@@ -11910,6 +12078,9 @@ Common workflows:
11910
12078
 
11911
12079
  # Mint an origin-restricted browser token (shown once), then revoke it
11912
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
11913
12084
  revturbine ingest-keys list
11914
12085
  revturbine ingest-keys revoke <ingest-key-id> --yes
11915
12086
 
@@ -11941,7 +12112,26 @@ function runInstall(manager, args, cwd) {
11941
12112
  child.on("close", (code) => resolve(code ?? 1));
11942
12113
  });
11943
12114
  }
11944
- program.command("init").alias("create").description("Scaffold RevTurbine into this app: detect the stack, install the SDK, pin the CLI, drop a starter Playbook, and install the Agent Skills. Offers to start a new project when the directory has no package.json.").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("--json", "Emit the scaffold plan as JSON").action(async (opts) => {
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);
11945
12135
  const dir = path2.resolve(opts.dir ?? process.cwd());
11946
12136
  const manifestPath = path2.join(dir, "package.json");
11947
12137
  let manifest;
@@ -11979,8 +12169,14 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
11979
12169
  for (const note of plan.skipped) diag(`\u2022 ${note}`);
11980
12170
  const playbookPath = path2.join(dir, STARTER_PLAYBOOK_FILENAME);
11981
12171
  const playbookExists = existsSync2(playbookPath);
11982
- if (playbookExists) diag(`\u2022 ${STARTER_PLAYBOOK_FILENAME} already present \u2014 left as-is`);
11983
- const installSkills = opts.skills !== false;
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
+ }
11984
12180
  if (opts.json) {
11985
12181
  emit(
11986
12182
  {
@@ -11990,8 +12186,9 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
11990
12186
  stack,
11991
12187
  install: plan.install,
11992
12188
  skipped: plan.skipped,
11993
- playbook: playbookExists ? "present" : STARTER_PLAYBOOK_FILENAME,
11994
- skills: installSkills ? SKILLS_SOURCE : "skipped"
12189
+ playbook: starter === "create" ? STARTER_PLAYBOOK_FILENAME : starter,
12190
+ skills: skillsArgs ? SKILLS_SOURCE : "skipped",
12191
+ skills_agent: skillsArgs ? harness.agentId : null
11995
12192
  },
11996
12193
  true
11997
12194
  );
@@ -12001,9 +12198,9 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
12001
12198
  for (const step of plan.install) {
12002
12199
  diag(`would run: ${manager.name} ${installArgs(manager.name, step).join(" ")}`);
12003
12200
  }
12004
- if (!playbookExists) diag(`would write: ${STARTER_PLAYBOOK_FILENAME}`);
12005
- if (installSkills) diag(`would run: npx ${skillsAddArgs().join(" ")}`);
12006
- if (plan.install.length === 0 && playbookExists) diag("\u2713 Already set up \u2014 nothing to do.");
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.");
12007
12204
  return;
12008
12205
  }
12009
12206
  for (const step of plan.install) {
@@ -12020,7 +12217,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
12020
12217
  if (plan.install.length > 0) {
12021
12218
  diag(`\u2713 Installed the RevTurbine SDK and CLI (CLI pinned to ${pkgVersion})`);
12022
12219
  }
12023
- const playbookAdded = !playbookExists;
12220
+ const playbookAdded = starter === "create";
12024
12221
  if (playbookAdded) {
12025
12222
  const check = validatePlaybook(STARTER_PLAYBOOK);
12026
12223
  if (!check.ok) {
@@ -12030,13 +12227,13 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
12030
12227
  );
12031
12228
  }
12032
12229
  writeFileSync2(playbookPath, `${JSON.stringify(STARTER_PLAYBOOK, null, 2)}
12033
- `, "utf8");
12230
+ `, { encoding: "utf8", flag: "wx" });
12034
12231
  diag(`\u2713 Added a starter playbook (${STARTER_PLAYBOOK_FILENAME} \u2014 local mode, no account needed)`);
12035
12232
  }
12036
- let skillsOutcome = installSkills ? "installed" : "skipped";
12037
- if (installSkills) {
12233
+ let skillsOutcome = skillsArgs ? "installed" : opts.skills === false ? "skipped" : "unknown";
12234
+ if (skillsArgs) {
12038
12235
  diag("Installing the RevTurbine Agent Skills (npx skills)\u2026");
12039
- const args = skillsAddArgs();
12236
+ const args = skillsArgs;
12040
12237
  let code;
12041
12238
  try {
12042
12239
  code = await runInstall("npx", args, dir);
@@ -12050,7 +12247,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
12050
12247
  diag("\u26A0 Could not install the Agent Skills automatically. Add them by hand:");
12051
12248
  diag(` npx ${args.join(" ")}`);
12052
12249
  }
12053
- } else {
12250
+ } else if (opts.skills === false) {
12054
12251
  diag("\u2022 Skipping the Agent Skills (--no-skills)");
12055
12252
  }
12056
12253
  if (!opts.json) {
@@ -12060,7 +12257,7 @@ program.command("init").alias("create").description("Scaffold RevTurbine into th
12060
12257
  cliVersion: pkgVersion,
12061
12258
  playbookAdded,
12062
12259
  skills: skillsOutcome,
12063
- harness: detectHarness(process.env)
12260
+ harness
12064
12261
  });
12065
12262
  process.stdout.write(`${lines.join("\n")}
12066
12263
  `);
@@ -12633,6 +12830,69 @@ ingestKeys.command("revoke").description("Revoke a public ingest key by id. The
12633
12830
  if (!result.ok) httpFail(conn, "ingest-keys revoke", result.status);
12634
12831
  diag(`\u2713 Revoked ingest key ${id}.`);
12635
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
+ });
12636
12896
  var COMMAND_EXAMPLES = {
12637
12897
  download: [
12638
12898
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revturbine/cli",
3
- "version": "0.17.2",
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": {