errorhub-cli 1.0.0 → 1.0.2

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 +111 -0
  2. package/package.json +1 -1
  3. package/src/index.ts +69 -76
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # errorhub-cli
2
+
3
+ Official CLI for [ErrorHub](https://errorhub.vercel.app) — authenticate, manage projects and configure error tracking directly from your terminal.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g errorhub-cli
11
+ ```
12
+
13
+ Or use without installing:
14
+
15
+ ```bash
16
+ npx errorhub-cli <command>
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Quick Start
22
+
23
+ ```bash
24
+ # 1. Login to your ErrorHub account
25
+ errorhub login
26
+
27
+ # 2. Link your project using its API key
28
+ errorhub project <your-api-key>
29
+
30
+ # 3. Verify your session
31
+ errorhub whoami
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Commands
37
+
38
+ ### `errorhub login`
39
+
40
+ Opens your browser and walks you through the authentication flow. Your JWT token is saved locally at `~/.token.json` and reused for all subsequent commands.
41
+
42
+ ```bash
43
+ errorhub login
44
+ ```
45
+
46
+ ---
47
+
48
+ ### `errorhub project <apiKey>`
49
+
50
+ Links a project to your local environment using its API key. The project ID is saved to `~/projectId.json` and used by the SDK automatically.
51
+
52
+ ```bash
53
+ errorhub project abc123yourkey
54
+ ```
55
+
56
+ ---
57
+
58
+ ### `errorhub whoami`
59
+
60
+ Displays your currently authenticated account and token details — including expiry, role, and email decoded from the JWT.
61
+
62
+ ```bash
63
+ errorhub whoami
64
+ ```
65
+
66
+ ---
67
+
68
+ ### `errorhub init`
69
+
70
+ Initializes your workspace and launches the auth flow for first-time setup.
71
+
72
+ ```bash
73
+ errorhub init
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Getting Your API Key
79
+
80
+ 1. Sign in at [errorhub.vercel.app](https://errorhub.vercel.app)
81
+ 2. Open or create a project
82
+ 3. Copy the API key from the project settings
83
+ 4. Run `errorhub project <your-api-key>`
84
+
85
+ ---
86
+
87
+ ## Token Storage
88
+
89
+ | File | Contents |
90
+ |---|---|
91
+ | `~/.token.json` | Your JWT auth token |
92
+ | `~/projectId.json` | Your linked project ID |
93
+
94
+ To log out, simply delete `~/.token.json`:
95
+
96
+ ```bash
97
+ rm ~/.token.json
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Requirements
103
+
104
+ - Node.js `v18` or higher
105
+ - An account at [errorhub.vercel.app](https://errorhub.vercel.app)
106
+
107
+ ---
108
+
109
+ ## License
110
+
111
+ MIT © [ErrorHub](https://errorhub.vercel.app)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errorhub-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -6,19 +6,17 @@ import express from 'express';
6
6
  import fs from 'fs';
7
7
  import os from 'os';
8
8
  import path from 'path';
9
+ import net from 'net';
9
10
  import { fileURLToPath } from 'url';
10
11
  import clc from 'cli-color';
11
- import { json } from 'stream/consumers';
12
12
 
13
13
  const program = new Command();
14
14
  const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = path.dirname(__filename);
16
16
  const CONFIG_PATH = path.join(os.homedir(), '.token.json');
17
- const PROJECTS = path.join(os.homedir(), 'projectId.json')
17
+ const PROJECTS = path.join(os.homedir(), 'projectId.json');
18
18
 
19
-
20
- // ─── Color Palette ──────────────────────────────────────────
21
- // ─────────────────
19
+ // ─── Color Palette ────────────────────────────────────────────────────────────
22
20
  const c = {
23
21
  cyan: clc.xterm(51).bold,
24
22
  cyanDim: clc.xterm(38),
@@ -35,16 +33,18 @@ const c = {
35
33
  bgGreen: clc.bgXterm(28),
36
34
  bgOrange: clc.bgXterm(208),
37
35
  };
36
+
38
37
  const FRONTENDBASE = "https://errorhub.vercel.app";
38
+
39
39
  // ─── Layout Helpers ───────────────────────────────────────────────────────────
40
- const W = 62; // box width
40
+ const W = 62;
41
41
 
42
42
  const line = (char = '─') => c.teal('─'.repeat(W));
43
43
  const dLine = () => c.teal('═'.repeat(W));
44
44
  const blank = () => console.log(c.teal('│') + ' '.repeat(W - 2) + c.teal('│'));
45
45
 
46
46
  const boxRow = (text: string, color = c.white) => {
47
- const inner = W - 4; // 2 border chars + 2 padding chars
47
+ const inner = W - 4;
48
48
  const stripped = text.replace(/\x1B\[[0-9;]*m/g, '');
49
49
  const pad = Math.max(0, inner - stripped.length);
50
50
  const left = Math.floor(pad / 2);
@@ -80,7 +80,6 @@ const LOGO = [
80
80
  const printLogo = () => {
81
81
  console.log('');
82
82
  LOGO.forEach((row, i) => {
83
- // Gradient: cyan → teal → green
84
83
  const color = i < 3 ? c.cyan : i < 6 ? c.teal : c.green;
85
84
  console.log(color(row));
86
85
  });
@@ -112,7 +111,7 @@ const badge = {
112
111
  loading: (msg: string) => console.log(c.cyan(' ⟳ ') + c.cyanDim(msg)),
113
112
  };
114
113
 
115
- // ─── Spinner (simple dot progress) ───────────────────────────────────────────
114
+ // ─── Spinner ──────────────────────────────────────────────────────────────────
116
115
  const spinner = (label: string) => {
117
116
  const frames = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
118
117
  let i = 0;
@@ -128,18 +127,32 @@ const spinner = (label: string) => {
128
127
  // ─── Section Divider ──────────────────────────────────────────────────────────
129
128
  const section = (title: string) => {
130
129
  console.log('');
131
- const line = '─'.repeat(4);
132
- console.log(c.teal(line) + ' ' + c.yellow(title.toUpperCase()) + ' ' + c.teal(line));
130
+ const l = '─'.repeat(4);
131
+ console.log(c.teal(l) + ' ' + c.yellow(title.toUpperCase()) + ' ' + c.teal(l));
133
132
  };
134
133
 
134
+ // ─── Free Port Helper ─────────────────────────────────────────────────────────
135
+ function getFreePort(startPort = 4000): Promise<number> {
136
+ return new Promise((resolve) => {
137
+ const server = net.createServer();
138
+ server.listen(startPort, () => {
139
+ const port = (server.address() as net.AddressInfo).port;
140
+ server.close(() => resolve(port));
141
+ });
142
+ server.on('error', () => {
143
+ resolve(getFreePort(startPort + 1));
144
+ });
145
+ });
146
+ }
147
+
135
148
  // ─────────────────────────────────────────────────────────────────────────────
136
149
  // CLI Setup
137
150
  // ─────────────────────────────────────────────────────────────────────────────
138
151
 
139
152
  program
140
- .name('errorhub')
153
+ .name('errorhub-cli')
141
154
  .description('CLI with JWT login')
142
- .version('1.0.0');
155
+ .version('1.0.1');
143
156
 
144
157
  // ─────────────────────────────────────────────────────────────────────────────
145
158
  // INIT
@@ -148,33 +161,29 @@ program
148
161
  program
149
162
  .command('init')
150
163
  .description('Initialize project')
151
- .action(() => {
164
+ .action(async () => {
152
165
  printLogo();
153
-
154
166
  printBanner('PROJECT INITIALIZER', 'Setting up your workspace...');
155
-
156
167
  console.log(c.teal('│'));
157
168
  labelRow('Version:', '1.0.0');
158
169
  labelRow('Node:', process.version);
159
170
  labelRow('Platform:', process.platform);
160
171
  labelRow('CWD:', process.cwd().slice(0, 40));
161
172
  console.log(c.teal('│'));
162
-
163
173
  closeBanner();
164
174
 
165
175
  section('Launching Auth Server');
166
176
  console.log('');
167
177
 
168
178
  const app = express();
169
- const port = 4000;
170
- const stop = spinner('Starting local server on port 4000...');
179
+ const port = await getFreePort();
180
+ const stop = spinner(`Starting local server on port ${port}...`);
171
181
 
172
182
  const server = app.listen(port, () => {
173
183
  stop();
174
184
  badge.ok(`Server listening on ${c.cyan(`http://localhost:${port}`)}`);
175
185
 
176
186
  const loginUrl = `${FRONTENDBASE}/login?callback=http://localhost:${port}/callback`;
177
-
178
187
  badge.info(`Opening: ${c.cyanDim(loginUrl)}`);
179
188
  console.log('');
180
189
  open(loginUrl);
@@ -195,28 +204,23 @@ program
195
204
  .description('Login and store JWT')
196
205
  .action(async () => {
197
206
  printLogo();
198
-
199
207
  printBanner('AUTHENTICATION', 'Secure JWT login flow');
200
-
201
- labelRow('Callback Port:', '4000');
202
208
  labelRow('Auth Server:', FRONTENDBASE);
203
209
  labelRow('Config Path:', CONFIG_PATH.replace(os.homedir(), '~'));
204
210
  console.log(c.teal('│'));
205
-
206
211
  closeBanner();
207
212
 
208
213
  section('Starting OAuth Flow');
209
214
  console.log('');
210
215
 
211
216
  const app = express();
212
- const port = 4000;
213
- const stop = spinner('Binding to port 4000...');
217
+ const port = await getFreePort();
218
+ const stop = spinner(`Binding to port ${port}...`);
214
219
 
215
220
  const server = app.listen(port, () => {
216
221
  stop();
217
222
  const loginUrl = `${FRONTENDBASE}/login?callback=http://localhost:${port}/callback`;
218
-
219
- badge.ok('Local callback server ready');
223
+ badge.ok(`Local callback server ready on port ${c.cyan(String(port))}`);
220
224
  badge.loading('Opening browser for login...');
221
225
  console.log('');
222
226
  open(loginUrl);
@@ -231,9 +235,7 @@ program
231
235
  return;
232
236
  }
233
237
 
234
- // Save token
235
238
  fs.writeFileSync(CONFIG_PATH, JSON.stringify({ token }, null, 2));
236
-
237
239
  res.send(renderHtmlPage('Success!', '✔ Login successful — you may close this tab.', '#00e5a0'));
238
240
 
239
241
  section('Login Complete');
@@ -272,11 +274,11 @@ program
272
274
 
273
275
  if (!fs.existsSync(CONFIG_PATH)) {
274
276
  console.log('');
275
- badge.error('No credentials found. Run ' + c.cyan('login-cli login') + ' first.');
277
+ badge.error('No credentials found. Run ' + c.cyan('errorhub login') + ' first.');
276
278
  console.log('');
277
279
  console.log(c.teal('┌' + '─'.repeat(W - 2) + '┐'));
278
280
  boxRow('NOT AUTHENTICATED', c.red);
279
- boxRow('Use: login-cli login', c.dim);
281
+ boxRow('Use: errorhub login', c.dim);
280
282
  console.log(c.teal('└' + '─'.repeat(W - 2) + '┘'));
281
283
  console.log('');
282
284
  return;
@@ -285,43 +287,38 @@ program
285
287
  const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
286
288
  const { token } = data;
287
289
 
288
- // Decode JWT header/payload for display (no verification)
289
290
  let jwtInfo: { header?: any; payload?: any } = {};
290
291
  try {
291
292
  const [h, p] = token.split('.');
292
293
  jwtInfo.header = JSON.parse(Buffer.from(h, 'base64').toString());
293
294
  jwtInfo.payload = JSON.parse(Buffer.from(p, 'base64').toString());
294
295
  } catch {
295
- // not a valid JWT, just show raw
296
+ // not a valid JWT
296
297
  }
297
298
 
298
299
  section('Stored Token');
299
300
  console.log('');
300
-
301
301
  console.log(c.teal('┌' + '─'.repeat(W - 2) + '┐'));
302
302
 
303
303
  if (jwtInfo.payload) {
304
304
  const p = jwtInfo.payload;
305
-
306
- // Header meta
307
305
  boxRow('JWT DECODED', c.yellow);
308
306
  console.log(c.teal('├' + '─'.repeat(W - 2) + '┤'));
309
307
 
310
308
  const fields: [string, any][] = Object.entries({
311
- sub: p.sub ?? '—',
312
- name: p.name ?? '—',
309
+ sub: p.sub ?? '—',
310
+ name: p.name ?? '—',
313
311
  email: p.email ?? '—',
314
- role: p.role ?? '—',
315
- iat: p.iat ? new Date(p.iat * 1000).toISOString() : '—',
316
- exp: p.exp ? new Date(p.exp * 1000).toISOString() : '—',
317
- alg: jwtInfo.header?.alg ?? '—',
312
+ role: p.role ?? '—',
313
+ iat: p.iat ? new Date(p.iat * 1000).toISOString() : '—',
314
+ exp: p.exp ? new Date(p.exp * 1000).toISOString() : '—',
315
+ alg: jwtInfo.header?.alg ?? '—',
318
316
  });
319
317
 
320
318
  for (const [k, v] of fields) {
321
319
  labelRow(`${k}:`, String(v));
322
320
  }
323
321
 
324
- // Expiry warning
325
322
  if (jwtInfo.payload.exp) {
326
323
  const expired = Date.now() / 1000 > jwtInfo.payload.exp;
327
324
  console.log(c.teal('├' + '─'.repeat(W - 2) + '┤'));
@@ -331,60 +328,56 @@ program
331
328
 
332
329
  console.log(c.teal('├' + '─'.repeat(W - 2) + '┤'));
333
330
  boxRow('RAW TOKEN', c.yellow);
334
-
335
- // Print token split at 58 chars per line
336
331
  const chunks = token.match(/.{1,54}/g) ?? [token];
337
332
  for (const chunk of chunks) {
338
333
  boxRow(chunk, c.cyanDim);
339
334
  }
340
-
341
335
  console.log(c.teal('└' + '─'.repeat(W - 2) + '┘'));
342
336
  console.log('');
343
337
  });
344
338
 
345
-
339
+ // ─────────────────────────────────────────────────────────────────────────────
340
+ // PROJECT
341
+ // ─────────────────────────────────────────────────────────────────────────────
346
342
 
347
343
  program
348
344
  .command('project <apiKey>')
349
345
  .description('Find and store project by API key')
350
346
  .action(async (apiKey) => {
351
- // 1. check if token exists
352
347
  if (!fs.existsSync(CONFIG_PATH)) {
353
- badge.error('Not logged in. Run errorhub login first.')
354
- return
348
+ badge.error('Not logged in. Run errorhub login first.');
349
+ return;
355
350
  }
356
351
 
357
- // 2. read token
358
- const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
359
- const token = data.token
352
+ const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
353
+ const token = data.token;
360
354
 
361
- // 3. fetch all projects
362
- const response = await fetch(`${FRONTENDBASE}/projects`, {
363
- headers: {
364
- Authorization: `Bearer ${token}`
365
- }
366
- })
367
- const projects = await response.json()
355
+ const stop = spinner('Fetching projects...');
368
356
 
369
- // 4. find matching project
370
- const match = projects.find((p: any) => p.api_key === apiKey)
357
+ try {
358
+ const response = await fetch(`${FRONTENDBASE}/api/projects`, {
359
+ headers: { Authorization: `Bearer ${token}` }
360
+ });
361
+ const projects = await response.json();
362
+ stop();
371
363
 
372
- // 5. no match found
373
- if (!match) {
374
- badge.error('No project found with that API key.')
375
- return
376
- }
364
+ const match = projects.find((p: any) => p.api_key === apiKey);
377
365
 
378
- // 6. save projectId
379
- fs.writeFileSync(PROJECTS, JSON.stringify({ projectId: match.id }, null, 2))
366
+ if (!match) {
367
+ badge.error('No project found with that API key.');
368
+ return;
369
+ }
380
370
 
381
- // 7. confirm
382
- badge.ok(`Project found and saved: ${match.id}`)
383
- })
371
+ fs.writeFileSync(PROJECTS, JSON.stringify({ projectId: match.id }, null, 2));
372
+ badge.ok(`Project found and saved: ${c.cyan(match.id)}`);
373
+ } catch (e) {
374
+ stop();
375
+ badge.error('Failed to fetch projects. Check your connection.');
376
+ }
377
+ });
384
378
 
385
-
386
- // ─────────────────────────=────────────────────────────────────────────────────
387
- // HTML response page helper
379
+ // ─────────────────────────────────────────────────────────────────────────────
380
+ // HTML Helper
388
381
  // ─────────────────────────────────────────────────────────────────────────────
389
382
 
390
383
  function renderHtmlPage(title: string, message: string, color: string): string {