@eventra_dev/eventra-cli 0.0.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.
@@ -0,0 +1,40 @@
1
+ name: Release CLI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ concurrency:
8
+ group: npm-publish
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+
15
+ permissions:
16
+ contents: read
17
+ id-token: write
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: pnpm/action-setup@v4
23
+ with:
24
+ version: 10
25
+
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
29
+ registry-url: https://registry.npmjs.org/
30
+
31
+ - run: pnpm install --no-frozen-lockfile
32
+
33
+ - name: Build CLI
34
+ run: pnpm build
35
+
36
+ - name: Publish to npm
37
+ run: npm publish --access public
38
+ env:
39
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
40
+ EVENTRA_ENDPOINT: ${{ secrets.EVENTRA_ENDPOINT }}
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eventra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software.
package/README.md ADDED
@@ -0,0 +1,310 @@
1
+ # Eventra CLI
2
+
3
+ Eventra CLI automatically discovers feature usage events in your codebase and syncs them with Eventra.
4
+
5
+ It helps you:
6
+
7
+ - Automatically discover events
8
+ - Detect wrapper components
9
+ - Keep events in sync
10
+ - Register features in Eventra
11
+ - Works with any JS framework (React, Vue, Svelte, Node, etc.)
12
+
13
+ ---
14
+
15
+ # Installation
16
+
17
+ Using npm
18
+
19
+ ```bash
20
+ npm install -D @eventra_dev/eventra-cli
21
+ ```
22
+
23
+ Using pnpm
24
+
25
+ ```bash
26
+ pnpm add -D @eventra_dev/eventra-cli
27
+ ```
28
+
29
+ Using npx
30
+
31
+ ```bash
32
+ npx eventra init
33
+ ```
34
+
35
+ ---
36
+
37
+ # Quick Start
38
+
39
+ ```bash
40
+ eventra init
41
+ eventra sync
42
+ eventra send
43
+ ```
44
+
45
+ ---
46
+
47
+ # Commands
48
+
49
+ ## eventra init
50
+
51
+ Creates `eventra.json` config file.
52
+
53
+ ```bash
54
+ eventra init
55
+ ```
56
+
57
+ Example:
58
+
59
+ ```json
60
+ {
61
+ "apiKey": "",
62
+ "events": [],
63
+ "wrappers": [],
64
+ "sync": {
65
+ "include": ["**/*.{ts,tsx,js,jsx}"],
66
+ "exclude": ["node_modules", "dist", ".next", ".git"]
67
+ }
68
+ }
69
+ ```
70
+
71
+ ---
72
+
73
+ ## eventra sync
74
+
75
+ Scans your project and finds all tracking events automatically.
76
+
77
+ ```bash
78
+ eventra sync
79
+ ```
80
+
81
+ Eventra CLI searches for:
82
+
83
+ ### Direct tracking calls
84
+
85
+ ```ts
86
+ tracker.track("feature_created")
87
+ ```
88
+
89
+ ### Wrapper components
90
+
91
+ ```tsx
92
+ <TrackedButton event="feature_created" />
93
+ ```
94
+
95
+ ### Custom wrappers
96
+
97
+ ```tsx
98
+ <MyComponent event="user_signup" />
99
+ ```
100
+
101
+ ---
102
+
103
+ # Wrapper detection
104
+
105
+ If you use wrapper components, Eventra CLI will ask:
106
+
107
+ ```
108
+ Use wrapper components? (Y/n)
109
+ ```
110
+
111
+ Then:
112
+
113
+ ```
114
+ Wrapper component name:
115
+ > TrackedButton
116
+ ```
117
+
118
+ ```
119
+ Event prop name:
120
+ > event
121
+ ```
122
+
123
+ You can add multiple wrappers:
124
+
125
+ ```
126
+ Add another wrapper? (y/N)
127
+ ```
128
+
129
+ Example config:
130
+
131
+ ```json
132
+ {
133
+ "wrappers": [
134
+ {
135
+ "name": "TrackedButton",
136
+ "prop": "event"
137
+ },
138
+ {
139
+ "name": "Feature",
140
+ "prop": "name"
141
+ }
142
+ ]
143
+ }
144
+ ```
145
+
146
+ ---
147
+
148
+ # Example sync output
149
+
150
+ ```bash
151
+ eventra sync
152
+ ```
153
+
154
+ Output:
155
+
156
+ ```
157
+ Scanning project...
158
+
159
+ Found events:
160
+
161
+ - landing_login_clicked
162
+ - feature_created
163
+ - user_signup
164
+ ```
165
+
166
+ ---
167
+
168
+ # Diff detection
169
+
170
+ Eventra CLI automatically detects changes:
171
+
172
+ ```
173
+ Changes:
174
+
175
+ New events:
176
+ + landing_signup_clicked
177
+
178
+ Removed events:
179
+ - old_event
180
+ ```
181
+
182
+ ---
183
+
184
+ ## eventra send
185
+
186
+ Send events to Eventra backend.
187
+
188
+ ```bash
189
+ eventra send
190
+ ```
191
+
192
+ If API key is missing:
193
+
194
+ ```
195
+ API key is not configured
196
+ Enter your API key:
197
+ ```
198
+
199
+ Events are registered in Eventra.
200
+
201
+ Example output:
202
+
203
+ ```
204
+ Events registered successfully
205
+
206
+ New events:
207
+ + feature_created
208
+ ```
209
+
210
+ ---
211
+
212
+ # Example workflow
213
+
214
+ ```bash
215
+ eventra init
216
+ eventra sync
217
+ eventra send
218
+ ```
219
+
220
+ ---
221
+
222
+ # Configuration
223
+
224
+ eventra.json
225
+
226
+ ```json
227
+ {
228
+ "apiKey": "",
229
+ "events": [],
230
+ "wrappers": [],
231
+ "sync": {
232
+ "include": ["**/*.{ts,tsx,js,jsx}"],
233
+ "exclude": ["node_modules", "dist", ".next", ".git"]
234
+ }
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ # Supported patterns
241
+
242
+ ## Direct tracking
243
+
244
+ ```ts
245
+ tracker.track("feature_created")
246
+ ```
247
+
248
+ ## Wrapper components
249
+
250
+ ```tsx
251
+ <TrackedButton event="feature_created" />
252
+ ```
253
+
254
+ ## Custom wrapper props
255
+
256
+ ```tsx
257
+ <MyComponent eventName="user_signup" />
258
+ ```
259
+
260
+ ---
261
+
262
+ # Supported frameworks
263
+
264
+ Eventra CLI works with:
265
+
266
+ - React
267
+ - Next.js
268
+ - Vue
269
+ - Nuxt
270
+ - Svelte
271
+ - Astro
272
+ - Node.js
273
+ - Express
274
+ - NestJS
275
+ - Vanilla JavaScript
276
+
277
+ ---
278
+
279
+ # Important
280
+
281
+ Eventra CLI detects only string literals:
282
+
283
+ Supported:
284
+
285
+ ```ts
286
+ track("event_name")
287
+ ```
288
+
289
+ Not supported:
290
+
291
+ ```ts
292
+ track(eventName)
293
+ track(EVENTS.event)
294
+ track(getEvent())
295
+ ```
296
+
297
+ This ensures reliable and predictable event detection.
298
+
299
+ ---
300
+
301
+ # Requirements
302
+
303
+ - Node.js 18+
304
+ - JavaScript / TypeScript project
305
+
306
+ ---
307
+
308
+ # License
309
+
310
+ MIT
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.init = init;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const config_1 = require("../utils/config");
12
+ async function init() {
13
+ console.log(chalk_1.default.blue("Initializing Eventra..."));
14
+ const configPath = path_1.default.join(process.cwd(), config_1.CONFIG_NAME);
15
+ if (await fs_extra_1.default.pathExists(configPath)) {
16
+ console.log(chalk_1.default.yellow("eventra.json already exists"));
17
+ return;
18
+ }
19
+ const answers = await inquirer_1.default.prompt([
20
+ {
21
+ type: "input",
22
+ name: "apiKey",
23
+ message: "API Key (optional):"
24
+ }
25
+ ]);
26
+ const config = {
27
+ apiKey: answers.apiKey || "",
28
+ events: [],
29
+ wrappers: [],
30
+ sync: {
31
+ include: ["**/*.{ts,tsx,js,jsx}"],
32
+ exclude: [
33
+ "node_modules",
34
+ "dist",
35
+ ".next",
36
+ ".git"
37
+ ]
38
+ }
39
+ };
40
+ await (0, config_1.saveConfig)(config);
41
+ console.log(chalk_1.default.green("eventra.json created"));
42
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.send = send;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const inquirer_1 = __importDefault(require("inquirer"));
9
+ const config_1 = require("../utils/config");
10
+ const EVENTRA_ENDPOINT = process.env.EVENTRA_ENDPOINT ?? "";
11
+ const CLI_VERSION = "0.0.1";
12
+ async function send() {
13
+ let config = await (0, config_1.loadConfig)();
14
+ if (!config) {
15
+ console.log(chalk_1.default.red("eventra.json not found. Run 'eventra init'"));
16
+ return;
17
+ }
18
+ let apiKey = config.apiKey;
19
+ if (!apiKey) {
20
+ const answers = await inquirer_1.default.prompt([
21
+ {
22
+ type: "input",
23
+ name: "apiKey",
24
+ message: "Enter your API key:"
25
+ }
26
+ ]);
27
+ apiKey = answers.apiKey;
28
+ config.apiKey = apiKey;
29
+ await (0, config_1.saveConfig)(config);
30
+ console.log(chalk_1.default.green("API key saved"));
31
+ }
32
+ if (!config.events.length) {
33
+ console.log(chalk_1.default.yellow("No events found. Run 'eventra sync'"));
34
+ return;
35
+ }
36
+ console.log(chalk_1.default.blue("Sending events..."));
37
+ try {
38
+ const res = await fetch(EVENTRA_ENDPOINT, {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ "x-api-key": apiKey
43
+ },
44
+ body: JSON.stringify({
45
+ events: config.events,
46
+ cli: {
47
+ name: "@eventra_dev/eventra-cli",
48
+ version: CLI_VERSION,
49
+ runtime: "node"
50
+ }
51
+ })
52
+ });
53
+ if (res.status >= 400) {
54
+ console.log(chalk_1.default.red(`Failed (${res.status})`));
55
+ return;
56
+ }
57
+ const data = await res.json();
58
+ console.log(chalk_1.default.green("Events registered successfully"));
59
+ if (data.created?.length) {
60
+ console.log(chalk_1.default.green("\nNew events:"));
61
+ data.created.forEach((e) => console.log(chalk_1.default.green(`+ ${e}`)));
62
+ }
63
+ }
64
+ catch {
65
+ console.log(chalk_1.default.red("Network error"));
66
+ }
67
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sync = sync;
7
+ const ts_morph_1 = require("ts-morph");
8
+ const fast_glob_1 = __importDefault(require("fast-glob"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const config_1 = require("../utils/config");
12
+ async function sync() {
13
+ let config = await (0, config_1.loadConfig)();
14
+ if (!config) {
15
+ console.log(chalk_1.default.red("Run 'eventra init' first"));
16
+ return;
17
+ }
18
+ const events = new Set();
19
+ const project = new ts_morph_1.Project();
20
+ console.log(chalk_1.default.blue("Scanning project..."));
21
+ const files = await (0, fast_glob_1.default)(config.sync.include, {
22
+ ignore: config.sync.exclude
23
+ });
24
+ // track()
25
+ for (const file of files) {
26
+ const sourceFile = project.addSourceFileAtPath(file);
27
+ const calls = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
28
+ for (const call of calls) {
29
+ const expression = call.getExpression();
30
+ if (expression.getKind() ===
31
+ ts_morph_1.SyntaxKind.PropertyAccessExpression) {
32
+ const prop = expression.asKind(ts_morph_1.SyntaxKind.PropertyAccessExpression);
33
+ if (!prop)
34
+ continue;
35
+ if (prop.getName() !== "track")
36
+ continue;
37
+ const args = call.getArguments();
38
+ const eventArg = args[0];
39
+ if (!eventArg)
40
+ continue;
41
+ let event = null;
42
+ // "event"
43
+ if (eventArg.getKind() ===
44
+ ts_morph_1.SyntaxKind.StringLiteral) {
45
+ event =
46
+ eventArg
47
+ .asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral)
48
+ .getLiteralText();
49
+ }
50
+ // `event`
51
+ if (eventArg.getKind() ===
52
+ ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
53
+ event =
54
+ eventArg
55
+ .asKindOrThrow(ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral)
56
+ .getLiteralText();
57
+ }
58
+ if (event) {
59
+ events.add(event);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ console.log(chalk_1.default.green(`Found ${events.size} track events`));
65
+ // wrappers setup
66
+ if (!config.wrappers.length) {
67
+ const { useWrapper } = await inquirer_1.default.prompt([
68
+ {
69
+ type: "confirm",
70
+ name: "useWrapper",
71
+ message: "Use wrapper components?",
72
+ default: true
73
+ }
74
+ ]);
75
+ if (useWrapper) {
76
+ const wrappers = [];
77
+ let addMore = true;
78
+ while (addMore) {
79
+ const answers = await inquirer_1.default.prompt([
80
+ {
81
+ type: "input",
82
+ name: "name",
83
+ message: "Wrapper component name:"
84
+ },
85
+ {
86
+ type: "input",
87
+ name: "prop",
88
+ message: "Event prop name:"
89
+ }
90
+ ]);
91
+ wrappers.push({
92
+ name: answers.name,
93
+ prop: answers.prop
94
+ });
95
+ const more = await inquirer_1.default.prompt([
96
+ {
97
+ type: "confirm",
98
+ name: "more",
99
+ message: "Add another wrapper?",
100
+ default: false
101
+ }
102
+ ]);
103
+ addMore = more.more;
104
+ }
105
+ config.wrappers = wrappers;
106
+ }
107
+ }
108
+ // scan wrappers
109
+ if (config.wrappers.length) {
110
+ console.log(chalk_1.default.blue("Scanning wrappers..."));
111
+ for (const file of files) {
112
+ const sourceFile = project.addSourceFileAtPath(file);
113
+ const elements = [
114
+ ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxOpeningElement),
115
+ ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxSelfClosingElement)
116
+ ];
117
+ for (const element of elements) {
118
+ const tagName = element
119
+ .getTagNameNode()
120
+ .getText()
121
+ .toLowerCase();
122
+ for (const wrapper of config.wrappers) {
123
+ if (tagName !==
124
+ wrapper.name.toLowerCase())
125
+ continue;
126
+ const attrs = element.getAttributes();
127
+ for (const attr of attrs) {
128
+ if (attr.getKind() !==
129
+ ts_morph_1.SyntaxKind.JsxAttribute)
130
+ continue;
131
+ const attrNode = attr.asKind(ts_morph_1.SyntaxKind.JsxAttribute);
132
+ if (!attrNode)
133
+ continue;
134
+ const attrName = attrNode
135
+ .getNameNode()
136
+ .getText()
137
+ .toLowerCase();
138
+ if (attrName !==
139
+ wrapper.prop.toLowerCase())
140
+ continue;
141
+ const initializer = attrNode.getInitializer();
142
+ if (!initializer)
143
+ continue;
144
+ let value = null;
145
+ // event="signup"
146
+ if (initializer.getKind() ===
147
+ ts_morph_1.SyntaxKind.StringLiteral) {
148
+ value =
149
+ initializer
150
+ .asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral)
151
+ .getLiteralText();
152
+ }
153
+ // event={"signup"}
154
+ if (initializer.getKind() ===
155
+ ts_morph_1.SyntaxKind.JsxExpression) {
156
+ const expr = initializer
157
+ .asKindOrThrow(ts_morph_1.SyntaxKind.JsxExpression)
158
+ .getExpression();
159
+ if (expr?.getKind() ===
160
+ ts_morph_1.SyntaxKind.StringLiteral) {
161
+ value =
162
+ expr
163
+ .asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral)
164
+ .getLiteralText();
165
+ }
166
+ }
167
+ if (value) {
168
+ events.add(value);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ // results
176
+ const list = [...events].sort();
177
+ console.log("");
178
+ console.log(chalk_1.default.green("Found events:"));
179
+ list.forEach((e) => console.log(chalk_1.default.gray(`- ${e}`)));
180
+ console.log("");
181
+ // diff
182
+ const previous = config.events ?? [];
183
+ const added = list.filter((e) => !previous.includes(e));
184
+ const removed = previous.filter((e) => !list.includes(e));
185
+ if (added.length || removed.length) {
186
+ console.log(chalk_1.default.blue("Changes:"));
187
+ if (added.length) {
188
+ console.log(chalk_1.default.green("New events:"));
189
+ added.forEach((e) => console.log(chalk_1.default.green(`+ ${e}`)));
190
+ }
191
+ if (removed.length) {
192
+ console.log(chalk_1.default.red("Removed events:"));
193
+ removed.forEach((e) => console.log(chalk_1.default.red(`- ${e}`)));
194
+ }
195
+ console.log("");
196
+ }
197
+ else {
198
+ console.log(chalk_1.default.gray("No changes detected"));
199
+ }
200
+ // confirm
201
+ const { confirm } = await inquirer_1.default.prompt([
202
+ {
203
+ type: "confirm",
204
+ name: "confirm",
205
+ message: "Sync these events?",
206
+ default: true
207
+ }
208
+ ]);
209
+ if (!confirm) {
210
+ console.log(chalk_1.default.yellow("Sync cancelled"));
211
+ return;
212
+ }
213
+ config.events = list;
214
+ await (0, config_1.saveConfig)(config);
215
+ console.log(chalk_1.default.green("eventra.json updated"));
216
+ }