errorhub-cli 1.0.1 → 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.
- package/package.json +1 -1
- package/src/index.ts +70 -77
package/package.json
CHANGED
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;
|
|
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;
|
|
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
|
|
114
|
+
// ─── Spinner ──────────────────────────────────────────────────────────────────
|
|
116
115
|
const spinner = (label: string) => {
|
|
117
116
|
const frames = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
118
117
|
let i = 0;
|
|
@@ -128,10 +127,24 @@ const spinner = (label: string) => {
|
|
|
128
127
|
// ─── Section Divider ──────────────────────────────────────────────────────────
|
|
129
128
|
const section = (title: string) => {
|
|
130
129
|
console.log('');
|
|
131
|
-
const
|
|
132
|
-
console.log(c.teal(
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -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 =
|
|
170
|
-
const stop = spinner(
|
|
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
|
-
badge.ok(`Server listening on ${c.cyan(`http://{
|
|
175
|
-
|
|
176
|
-
const loginUrl = `${FRONTENDBASE}/login?callback=http://{FRONTENDBASE}/callback`;
|
|
184
|
+
badge.ok(`Server listening on ${c.cyan(`http://localhost:${port}`)}`);
|
|
177
185
|
|
|
186
|
+
const loginUrl = `${FRONTENDBASE}/login?callback=http://localhost:${port}/callback`;
|
|
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 =
|
|
213
|
-
const stop = spinner(
|
|
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
|
-
const loginUrl = `${FRONTENDBASE}/login?callback=http://{
|
|
218
|
-
|
|
219
|
-
badge.ok('Local callback server ready');
|
|
222
|
+
const loginUrl = `${FRONTENDBASE}/login?callback=http://localhost:${port}/callback`;
|
|
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('
|
|
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:
|
|
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
|
|
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:
|
|
312
|
-
name:
|
|
309
|
+
sub: p.sub ?? '—',
|
|
310
|
+
name: p.name ?? '—',
|
|
313
311
|
email: p.email ?? '—',
|
|
314
|
-
role:
|
|
315
|
-
iat:
|
|
316
|
-
exp:
|
|
317
|
-
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
|
-
|
|
358
|
-
const
|
|
359
|
-
const token = data.token
|
|
352
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
353
|
+
const token = data.token;
|
|
360
354
|
|
|
361
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
379
|
-
|
|
366
|
+
if (!match) {
|
|
367
|
+
badge.error('No project found with that API key.');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
380
370
|
|
|
381
|
-
|
|
382
|
-
|
|
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 {
|