@keplog/cli 0.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +495 -0
  3. package/bin/keplog +2 -0
  4. package/dist/commands/delete.d.ts +3 -0
  5. package/dist/commands/delete.d.ts.map +1 -0
  6. package/dist/commands/delete.js +158 -0
  7. package/dist/commands/delete.js.map +1 -0
  8. package/dist/commands/init.d.ts +3 -0
  9. package/dist/commands/init.d.ts.map +1 -0
  10. package/dist/commands/init.js +131 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/commands/issues.d.ts +3 -0
  13. package/dist/commands/issues.d.ts.map +1 -0
  14. package/dist/commands/issues.js +543 -0
  15. package/dist/commands/issues.js.map +1 -0
  16. package/dist/commands/list.d.ts +3 -0
  17. package/dist/commands/list.d.ts.map +1 -0
  18. package/dist/commands/list.js +104 -0
  19. package/dist/commands/list.js.map +1 -0
  20. package/dist/commands/releases.d.ts +3 -0
  21. package/dist/commands/releases.d.ts.map +1 -0
  22. package/dist/commands/releases.js +100 -0
  23. package/dist/commands/releases.js.map +1 -0
  24. package/dist/commands/upload.d.ts +3 -0
  25. package/dist/commands/upload.d.ts.map +1 -0
  26. package/dist/commands/upload.js +76 -0
  27. package/dist/commands/upload.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +28 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/lib/config.d.ts +57 -0
  33. package/dist/lib/config.d.ts.map +1 -0
  34. package/dist/lib/config.js +155 -0
  35. package/dist/lib/config.js.map +1 -0
  36. package/dist/lib/uploader.d.ts +11 -0
  37. package/dist/lib/uploader.d.ts.map +1 -0
  38. package/dist/lib/uploader.js +171 -0
  39. package/dist/lib/uploader.js.map +1 -0
  40. package/jest.config.js +16 -0
  41. package/package.json +58 -0
  42. package/src/commands/delete.ts +186 -0
  43. package/src/commands/init.ts +137 -0
  44. package/src/commands/issues.ts +695 -0
  45. package/src/commands/list.ts +124 -0
  46. package/src/commands/releases.ts +122 -0
  47. package/src/commands/upload.ts +76 -0
  48. package/src/index.ts +31 -0
  49. package/src/lib/config.ts +138 -0
  50. package/src/lib/uploader.ts +168 -0
  51. package/tests/README.md +380 -0
  52. package/tests/config.test.ts +397 -0
  53. package/tests/uploader.test.ts +524 -0
  54. package/tsconfig.json +20 -0
@@ -0,0 +1,695 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { ConfigManager } from '../lib/config.js';
5
+
6
+ interface Issue {
7
+ id: string;
8
+ title: string;
9
+ status: string;
10
+ level: string;
11
+ first_seen: string;
12
+ last_seen: string;
13
+ occurrences: number;
14
+ project_id: string;
15
+ assigned_user_name?: string;
16
+ assigned_team_name?: string;
17
+ snoozed_until?: string;
18
+ }
19
+
20
+ interface Frame {
21
+ file?: string;
22
+ line?: number;
23
+ function?: string;
24
+ class?: string;
25
+ type?: string;
26
+ code_snippet?: { [lineNumber: string]: string } | null;
27
+ }
28
+
29
+ interface MappedFrame {
30
+ filename?: string;
31
+ line_number?: number;
32
+ column_number?: number;
33
+ function?: string;
34
+ source_filename?: string;
35
+ source_line_number?: number;
36
+ source_column?: number;
37
+ source_function?: string;
38
+ source_code?: string;
39
+ pre_context?: string[];
40
+ post_context?: string[];
41
+ mapped?: boolean;
42
+ }
43
+
44
+ interface ErrorContext {
45
+ frames?: Frame[];
46
+ [key: string]: any;
47
+ }
48
+
49
+ interface MappedStackTrace {
50
+ frames?: MappedFrame[];
51
+ release?: string;
52
+ applied_at?: string;
53
+ }
54
+
55
+ interface IssueDetails extends Issue {
56
+ fingerprint: string;
57
+ created_at: string;
58
+ updated_at: string;
59
+ context?: ErrorContext;
60
+ stack_trace?: string;
61
+ mapped_stack_trace?: MappedStackTrace;
62
+ }
63
+
64
+ interface ErrorEvent {
65
+ id: string;
66
+ issue_id: string;
67
+ stack_trace?: string;
68
+ context?: ErrorContext;
69
+ mapped_stack_trace?: MappedStackTrace;
70
+ environment?: string;
71
+ release_version?: string;
72
+ timestamp: string;
73
+ }
74
+
75
+ interface ListIssuesResponse {
76
+ issues: Issue[];
77
+ }
78
+
79
+ interface GetIssueResponse {
80
+ issue: IssueDetails;
81
+ latest_event?: ErrorEvent;
82
+ }
83
+
84
+ // List subcommand
85
+ const listSubcommand = new Command('list')
86
+ .description('List issues for a project')
87
+ .option('-p, --project-id <id>', 'Project ID (overrides config)')
88
+ .option('-k, --api-key <key>', 'API key (overrides config)')
89
+ .option('-u, --api-url <url>', 'API URL (overrides config)')
90
+ .option('-s, --status <status>', 'Filter by status (open, in_progress, resolved, ignored)', 'open')
91
+ .option('-l, --limit <number>', 'Number of issues to fetch', '50')
92
+ .option('--offset <number>', 'Offset for pagination', '0')
93
+ .option('--from <date>', 'Filter from date (YYYY-MM-DD)')
94
+ .option('--to <date>', 'Filter to date (YYYY-MM-DD)')
95
+ .option('-f, --format <format>', 'Output format (table, json)', 'table')
96
+ .action(async (options) => {
97
+ try {
98
+ // Read config from file
99
+ const config = ConfigManager.getConfig();
100
+
101
+ const projectId = options.projectId || config.projectId;
102
+ const apiKey = options.apiKey || config.apiKey;
103
+ const apiUrl = options.apiUrl || config.apiUrl || 'https://api.keplog.com';
104
+
105
+ // Validate required parameters
106
+ if (!projectId) {
107
+ console.error(chalk.red('\nāœ— Error: Project ID is required\n'));
108
+ console.log('Options:');
109
+ console.log(' 1. Run: keplog init (recommended)');
110
+ console.log(' 2. Use flag: --project-id=<your-project-id>');
111
+ console.log(' 3. Set env: KEPLOG_PROJECT_ID=<your-project-id>\n');
112
+ process.exit(1);
113
+ }
114
+
115
+ if (!apiKey) {
116
+ console.error(chalk.red('\nāœ— Error: API key is required\n'));
117
+ console.log('Options:');
118
+ console.log(' 1. Run: keplog init (recommended)');
119
+ console.log(' 2. Use flag: --api-key=<your-api-key>');
120
+ console.log(' 3. Set env: KEPLOG_API_KEY=<your-api-key>\n');
121
+ process.exit(1);
122
+ }
123
+
124
+ console.log(chalk.bold.cyan('\nšŸ› Keplog Issues - List\n'));
125
+ console.log(`Project ID: ${chalk.gray(projectId)}`);
126
+ console.log(`Status: ${chalk.yellow(options.status)}`);
127
+ console.log(`Limit: ${chalk.gray(options.limit)}\n`);
128
+
129
+ const spinner = ora('Fetching issues...').start();
130
+
131
+ // Build query parameters
132
+ const params = new URLSearchParams({
133
+ status: options.status,
134
+ limit: options.limit,
135
+ offset: options.offset,
136
+ });
137
+
138
+ if (options.from) {
139
+ params.append('date_from', options.from);
140
+ }
141
+ if (options.to) {
142
+ params.append('date_to', options.to);
143
+ }
144
+
145
+ // Fetch issues from API (CLI endpoint)
146
+ const url = `${apiUrl}/api/v1/cli/projects/${projectId}/issues?${params.toString()}`;
147
+ const response = await fetch(url, {
148
+ method: 'GET',
149
+ headers: {
150
+ 'X-API-Key': apiKey,
151
+ },
152
+ });
153
+
154
+ if (!response.ok) {
155
+ const error = await response.json() as any;
156
+ spinner.fail(chalk.red('Failed to fetch issues'));
157
+ console.error(chalk.red(`\nāœ— Error: ${error.error || 'Unknown error'}\n`));
158
+ process.exit(1);
159
+ }
160
+
161
+ const data = await response.json() as ListIssuesResponse;
162
+ spinner.succeed(chalk.green('Issues fetched'));
163
+
164
+ // Display results
165
+ if (!data.issues || data.issues.length === 0) {
166
+ if (options.format === 'json') {
167
+ console.log(JSON.stringify({ issues: [] }, null, 2));
168
+ } else {
169
+ console.log(chalk.yellow('\nNo issues found.\n'));
170
+ }
171
+ return;
172
+ }
173
+
174
+ // JSON format output
175
+ if (options.format === 'json') {
176
+ console.log(JSON.stringify(data, null, 2));
177
+ return;
178
+ }
179
+
180
+ // Table format output
181
+ console.log(chalk.bold(`\nšŸ“‹ Found ${chalk.cyan(data.issues.length)} issue${data.issues.length !== 1 ? 's' : ''}\n`));
182
+
183
+ // Display table header
184
+ const idWidth = 10;
185
+ const titleWidth = 50;
186
+ const statusWidth = 12;
187
+ const levelWidth = 10;
188
+ const occWidth = 8;
189
+ const lastSeenWidth = 20;
190
+
191
+ console.log(
192
+ chalk.gray(
193
+ 'ID'.padEnd(idWidth) +
194
+ 'TITLE'.padEnd(titleWidth) +
195
+ 'STATUS'.padEnd(statusWidth) +
196
+ 'LEVEL'.padEnd(levelWidth) +
197
+ 'COUNT'.padEnd(occWidth) +
198
+ 'LAST SEEN'
199
+ )
200
+ );
201
+ console.log(chalk.gray('─'.repeat(idWidth + titleWidth + statusWidth + levelWidth + occWidth + lastSeenWidth)));
202
+
203
+ // Display issues
204
+ for (const issue of data.issues) {
205
+ const shortId = issue.id.substring(0, 8);
206
+ const title = truncate(issue.title, titleWidth - 2);
207
+ const statusText = issue.status.padEnd(statusWidth);
208
+ const levelText = issue.level.padEnd(levelWidth);
209
+ const count = issue.occurrences.toString().padEnd(occWidth);
210
+ const lastSeen = formatDate(issue.last_seen);
211
+
212
+ console.log(
213
+ chalk.white(shortId.padEnd(idWidth)) +
214
+ title.padEnd(titleWidth) +
215
+ colorStatus(statusText) +
216
+ colorLevel(levelText) +
217
+ chalk.cyan(count) +
218
+ chalk.gray(lastSeen)
219
+ );
220
+ }
221
+
222
+ console.log(chalk.gray(`\nShowing ${data.issues.length} issue${data.issues.length !== 1 ? 's' : ''} (offset: ${options.offset})\n`));
223
+ console.log(chalk.dim('View details: keplog issues show <issue-id>\n'));
224
+
225
+ } catch (error: any) {
226
+ console.error(chalk.red(`\nāœ— Error: ${error.message}\n`));
227
+ process.exit(1);
228
+ }
229
+ });
230
+
231
+ // Show subcommand
232
+ const showSubcommand = new Command('show')
233
+ .description('Show detailed information about an issue')
234
+ .argument('<issue-id>', 'Issue ID to display (short or full UUID)')
235
+ .option('-p, --project-id <id>', 'Project ID (overrides config)')
236
+ .option('-k, --api-key <key>', 'API key (overrides config)')
237
+ .option('-u, --api-url <url>', 'API URL (overrides config)')
238
+ .option('--show-minified', 'Show minified stack trace (if source maps available)')
239
+ .option('-f, --format <format>', 'Output format (pretty, json)', 'pretty')
240
+ .action(async (issueId: string, options) => {
241
+ try {
242
+ // Read config from file
243
+ const config = ConfigManager.getConfig();
244
+
245
+ const projectId = options.projectId || config.projectId;
246
+ const apiKey = options.apiKey || config.apiKey;
247
+ const apiUrl = options.apiUrl || config.apiUrl || 'https://api.keplog.com';
248
+
249
+ // Validate required parameters
250
+ if (!apiKey) {
251
+ console.error(chalk.red('\nāœ— Error: API key is required\n'));
252
+ console.log('Options:');
253
+ console.log(' 1. Run: keplog init (recommended)');
254
+ console.log(' 2. Use flag: --api-key=<your-api-key>');
255
+ console.log(' 3. Set env: KEPLOG_API_KEY=<your-api-key>\n');
256
+ process.exit(1);
257
+ }
258
+
259
+ console.log(chalk.bold.cyan('\nšŸ› Keplog Issue Details\n'));
260
+
261
+ let fullIssueId = issueId;
262
+
263
+ // If issue ID is short (8 chars), fetch full UUID from list
264
+ if (issueId.length === 8) {
265
+ if (!projectId) {
266
+ console.error(chalk.red('\nāœ— Error: Project ID is required for short issue IDs\n'));
267
+ console.log('Options:');
268
+ console.log(' 1. Run: keplog init (recommended)');
269
+ console.log(' 2. Use flag: --project-id=<your-project-id>');
270
+ console.log(' 3. Use full UUID instead of short ID\n');
271
+ process.exit(1);
272
+ }
273
+
274
+ const spinner = ora('Resolving short issue ID...').start();
275
+
276
+ // Fetch issues to find the full UUID
277
+ const listUrl = `${apiUrl}/api/v1/cli/projects/${projectId}/issues?limit=1000`;
278
+ const listResponse = await fetch(listUrl, {
279
+ method: 'GET',
280
+ headers: {
281
+ 'X-API-Key': apiKey,
282
+ },
283
+ });
284
+
285
+ if (!listResponse.ok) {
286
+ spinner.fail(chalk.red('Failed to resolve issue ID'));
287
+ console.error(chalk.red(`\nāœ— Error: Could not fetch issues list\n`));
288
+ process.exit(1);
289
+ }
290
+
291
+ const listData = await listResponse.json() as ListIssuesResponse;
292
+ const matchingIssue = listData.issues.find(issue => issue.id.startsWith(issueId));
293
+
294
+ if (!matchingIssue) {
295
+ spinner.fail(chalk.red('Issue not found'));
296
+ console.error(chalk.red(`\nāœ— Error: No issue found with ID starting with '${issueId}'\n`));
297
+ process.exit(1);
298
+ }
299
+
300
+ fullIssueId = matchingIssue.id;
301
+ spinner.succeed(chalk.green(`Resolved to ${fullIssueId}`));
302
+ }
303
+
304
+ const spinner = ora('Fetching issue details...').start();
305
+
306
+ // Fetch issue from API (CLI endpoint)
307
+ const url = `${apiUrl}/api/v1/cli/issues/${fullIssueId}`;
308
+ const response = await fetch(url, {
309
+ method: 'GET',
310
+ headers: {
311
+ 'X-API-Key': apiKey,
312
+ },
313
+ });
314
+
315
+ if (!response.ok) {
316
+ const error = await response.json() as any;
317
+ spinner.fail(chalk.red('Failed to fetch issue'));
318
+ console.error(chalk.red(`\nāœ— Error: ${error.error || 'Unknown error'}\n`));
319
+ process.exit(1);
320
+ }
321
+
322
+ const data = await response.json() as GetIssueResponse;
323
+ const issue = data.issue;
324
+
325
+ // Merge latest_event data into issue if available
326
+ if (data.latest_event) {
327
+ issue.stack_trace = data.latest_event.stack_trace;
328
+ issue.context = data.latest_event.context;
329
+ issue.mapped_stack_trace = data.latest_event.mapped_stack_trace;
330
+ }
331
+
332
+ spinner.succeed(chalk.green('Issue fetched'));
333
+
334
+ // JSON format output
335
+ if (options.format === 'json') {
336
+ console.log(JSON.stringify(data, null, 2));
337
+ return;
338
+ }
339
+
340
+ // Display issue details
341
+ console.log(chalk.bold(`\nšŸ“Œ ${issue.title}\n`));
342
+ console.log(`${chalk.gray('ID:')} ${chalk.white(issue.id)}`);
343
+ console.log(`${chalk.gray('Status:')} ${colorStatus(issue.status)}`);
344
+ console.log(`${chalk.gray('Level:')} ${colorLevel(issue.level)}`);
345
+ console.log(`${chalk.gray('Occurrences:')} ${chalk.cyan(issue.occurrences)}`);
346
+ console.log(`${chalk.gray('First Seen:')} ${chalk.white(formatDateLong(issue.first_seen))}`);
347
+ console.log(`${chalk.gray('Last Seen:')} ${chalk.white(formatDateLong(issue.last_seen))}`);
348
+
349
+ // Display environment and version from latest event
350
+ if (data.latest_event?.environment) {
351
+ console.log(`${chalk.gray('Environment:')} ${chalk.cyan(data.latest_event.environment)}`);
352
+ }
353
+ if (data.latest_event?.release_version) {
354
+ console.log(`${chalk.gray('Version:')} ${chalk.cyan(data.latest_event.release_version)}`);
355
+ }
356
+
357
+ if (issue.assigned_user_name) {
358
+ console.log(`${chalk.gray('Assigned To:')} ${chalk.white(issue.assigned_user_name)}`);
359
+ } else if (issue.assigned_team_name) {
360
+ console.log(`${chalk.gray('Assigned To:')} ${chalk.white(issue.assigned_team_name)} ${chalk.dim('(team)')}`);
361
+ }
362
+
363
+ if (issue.snoozed_until) {
364
+ console.log(`${chalk.gray('Snoozed:')} ${chalk.yellow('Until ' + formatDateLong(issue.snoozed_until))}`);
365
+ }
366
+
367
+ // Display source-mapped stack trace if available
368
+ const hasMappedTrace = issue.mapped_stack_trace?.frames && issue.mapped_stack_trace.frames.length > 0;
369
+ const showOriginal = hasMappedTrace && !options.showMinified;
370
+
371
+ if (showOriginal) {
372
+ console.log(chalk.bold.green('\nāœ“ Source Maps Applied'));
373
+ if (issue.mapped_stack_trace?.release) {
374
+ console.log(chalk.gray(`Release: ${issue.mapped_stack_trace.release}`));
375
+ }
376
+ console.log(chalk.bold('\nšŸ“š Stack Trace (Original Source):\n'));
377
+
378
+ for (const frame of issue.mapped_stack_trace!.frames!) {
379
+ if (frame.mapped && frame.source_filename) {
380
+ // Source-mapped frame
381
+ console.log(chalk.green(' āœ“ [MAPPED]'));
382
+ console.log(` ${chalk.white(frame.source_function || 'anonymous')}`);
383
+ console.log(chalk.gray(` ${frame.source_filename}:${frame.source_line_number}:${frame.source_column || 0}`));
384
+
385
+ // Show source code if available
386
+ if (frame.source_code) {
387
+ console.log(chalk.dim('\n Source Code:'));
388
+
389
+ // Pre-context
390
+ if (frame.pre_context && frame.pre_context.length > 0) {
391
+ const startLine = (frame.source_line_number || 0) - frame.pre_context.length;
392
+ frame.pre_context.forEach((line, i) => {
393
+ const lineNum = startLine + i;
394
+ console.log(chalk.gray(` ${String(lineNum).padStart(4)} │ ${line}`));
395
+ });
396
+ }
397
+
398
+ // Error line
399
+ console.log(chalk.red(` → ${String(frame.source_line_number).padStart(4)} │ ${frame.source_code}`));
400
+
401
+ // Post-context
402
+ if (frame.post_context && frame.post_context.length > 0) {
403
+ frame.post_context.forEach((line, i) => {
404
+ const lineNum = (frame.source_line_number || 0) + i + 1;
405
+ console.log(chalk.gray(` ${String(lineNum).padStart(4)} │ ${line}`));
406
+ });
407
+ }
408
+ console.log('');
409
+ }
410
+ } else {
411
+ // Unmapped frame
412
+ console.log(chalk.yellow(' ā—‹'));
413
+ console.log(` ${chalk.white(frame.function || 'anonymous')}`);
414
+ console.log(chalk.gray(` ${frame.filename || 'unknown'}:${frame.line_number || 0}`));
415
+ }
416
+ console.log('');
417
+ }
418
+ } else if (issue.context?.frames && issue.context.frames.length > 0) {
419
+ // Laravel/PHP style frames
420
+ console.log(chalk.bold('\nšŸ“š Stack Trace:\n'));
421
+
422
+ for (const frame of issue.context.frames) {
423
+ const funcName = frame.class
424
+ ? `${frame.class}${frame.type || '::'}${frame.function}`
425
+ : (frame.function || 'anonymous');
426
+
427
+ console.log(chalk.white(` ${funcName}`));
428
+ console.log(chalk.gray(` ${frame.file || 'unknown'}:${frame.line || '?'}`));
429
+
430
+ // Show code snippet if available
431
+ if (frame.code_snippet) {
432
+ const lines = Object.entries(frame.code_snippet).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
433
+
434
+ if (lines.length > 0) {
435
+ console.log(chalk.dim('\n Code:'));
436
+ for (const [lineNum, code] of lines) {
437
+ const isErrorLine = parseInt(lineNum) === frame.line;
438
+ if (isErrorLine) {
439
+ console.log(chalk.red(`→ ${lineNum.padStart(4)} │ ${code}`));
440
+ } else {
441
+ console.log(chalk.gray(` ${lineNum.padStart(4)} │ ${code}`));
442
+ }
443
+ }
444
+ console.log('');
445
+ }
446
+ }
447
+ console.log('');
448
+ }
449
+ } else if (issue.stack_trace) {
450
+ // Plain text stack trace
451
+ console.log(chalk.bold('\nšŸ“š Stack Trace:\n'));
452
+ console.log(chalk.gray(issue.stack_trace));
453
+ } else {
454
+ // No stack trace available
455
+ console.log(chalk.bold('\nšŸ“š Stack Trace:\n'));
456
+ console.log(chalk.yellow(' No stack trace available for this issue.\n'));
457
+ }
458
+
459
+ if (hasMappedTrace && !showOriginal) {
460
+ console.log(chalk.dim('\nšŸ’” Tip: Remove --show-minified to see original source code\n'));
461
+ }
462
+
463
+ console.log('');
464
+
465
+ } catch (error: any) {
466
+ console.error(chalk.red(`\nāœ— Error: ${error.message}\n`));
467
+ process.exit(1);
468
+ }
469
+ });
470
+
471
+ // Events subcommand
472
+ const eventsSubcommand = new Command('events')
473
+ .description('List all events for an issue')
474
+ .argument('<issue-id>', 'Issue ID (short or full UUID)')
475
+ .option('-p, --project-id <id>', 'Project ID (overrides config)')
476
+ .option('-k, --api-key <key>', 'API key (overrides config)')
477
+ .option('-u, --api-url <url>', 'API URL (overrides config)')
478
+ .option('-l, --limit <number>', 'Number of events to fetch', '50')
479
+ .option('--offset <number>', 'Offset for pagination', '0')
480
+ .option('-f, --format <format>', 'Output format (table, json)', 'table')
481
+ .action(async (issueId: string, options) => {
482
+ try {
483
+ // Read config from file
484
+ const config = ConfigManager.getConfig();
485
+
486
+ const projectId = options.projectId || config.projectId;
487
+ const apiKey = options.apiKey || config.apiKey;
488
+ const apiUrl = options.apiUrl || config.apiUrl || 'https://api.keplog.com';
489
+
490
+ // Validate required parameters
491
+ if (!apiKey) {
492
+ console.error(chalk.red('\nāœ— Error: API key is required\n'));
493
+ console.log('Options:');
494
+ console.log(' 1. Run: keplog init (recommended)');
495
+ console.log(' 2. Use flag: --api-key=<your-api-key>');
496
+ console.log(' 3. Set env: KEPLOG_API_KEY=<your-api-key>\n');
497
+ process.exit(1);
498
+ }
499
+
500
+ let fullIssueId = issueId;
501
+
502
+ // If issue ID is short (8 chars), fetch full UUID from list
503
+ if (issueId.length === 8) {
504
+ if (!projectId) {
505
+ console.error(chalk.red('\nāœ— Error: Project ID is required for short issue IDs\n'));
506
+ console.log('Options:');
507
+ console.log(' 1. Run: keplog init (recommended)');
508
+ console.log(' 2. Use flag: --project-id=<your-project-id>');
509
+ console.log(' 3. Use full UUID instead of short ID\n');
510
+ process.exit(1);
511
+ }
512
+
513
+ const spinner = ora('Resolving short issue ID...').start();
514
+
515
+ // Fetch issues to find the full UUID
516
+ const listUrl = `${apiUrl}/api/v1/cli/projects/${projectId}/issues?limit=1000`;
517
+ const listResponse = await fetch(listUrl, {
518
+ method: 'GET',
519
+ headers: {
520
+ 'X-API-Key': apiKey,
521
+ },
522
+ });
523
+
524
+ if (!listResponse.ok) {
525
+ spinner.fail(chalk.red('Failed to resolve issue ID'));
526
+ console.error(chalk.red(`\nāœ— Error: Could not fetch issues list\n`));
527
+ process.exit(1);
528
+ }
529
+
530
+ const listData = await listResponse.json() as ListIssuesResponse;
531
+ const matchingIssue = listData.issues.find(issue => issue.id.startsWith(issueId));
532
+
533
+ if (!matchingIssue) {
534
+ spinner.fail(chalk.red('Issue not found'));
535
+ console.error(chalk.red(`\nāœ— Error: No issue found with ID starting with '${issueId}'\n`));
536
+ process.exit(1);
537
+ }
538
+
539
+ fullIssueId = matchingIssue.id;
540
+ spinner.succeed(chalk.green(`Resolved to ${fullIssueId}`));
541
+ }
542
+
543
+ const spinner = ora('Fetching issue events...').start();
544
+
545
+ // Build query parameters
546
+ const params = new URLSearchParams({
547
+ limit: options.limit,
548
+ offset: options.offset,
549
+ });
550
+
551
+ // Fetch events from API (CLI endpoint)
552
+ const url = `${apiUrl}/api/v1/cli/issues/${fullIssueId}/events?${params.toString()}`;
553
+ const response = await fetch(url, {
554
+ method: 'GET',
555
+ headers: {
556
+ 'X-API-Key': apiKey,
557
+ },
558
+ });
559
+
560
+ if (!response.ok) {
561
+ const error = await response.json() as any;
562
+ spinner.fail(chalk.red('Failed to fetch events'));
563
+ console.error(chalk.red(`\nāœ— Error: ${error.error || 'Unknown error'}\n`));
564
+ process.exit(1);
565
+ }
566
+
567
+ const data = await response.json() as { events: ErrorEvent[], issue: IssueDetails };
568
+ spinner.succeed(chalk.green('Events fetched'));
569
+
570
+ if (!data.events || data.events.length === 0) {
571
+ if (options.format === 'json') {
572
+ console.log(JSON.stringify({ events: [], issue: data.issue }, null, 2));
573
+ } else {
574
+ console.log(chalk.yellow('\nNo events found for this issue.\n'));
575
+ }
576
+ return;
577
+ }
578
+
579
+ // JSON format output
580
+ if (options.format === 'json') {
581
+ console.log(JSON.stringify(data, null, 2));
582
+ return;
583
+ }
584
+
585
+ // Table format output
586
+ console.log(chalk.bold.cyan('\nšŸ› Issue Events\n'));
587
+ console.log(`${chalk.gray('Issue:')} ${chalk.white(data.issue.title)}`);
588
+ console.log(`${chalk.gray('ID:')} ${chalk.white(fullIssueId)}\n`);
589
+ console.log(chalk.bold(`šŸ“‹ Found ${chalk.cyan(data.events.length)} event${data.events.length !== 1 ? 's' : ''}\n`));
590
+
591
+ // Display events
592
+ for (let i = 0; i < data.events.length; i++) {
593
+ const event = data.events[i];
594
+ console.log(chalk.bold(`\n${i + 1}. Event ${chalk.gray(event.id.substring(0, 8))}`));
595
+ console.log(` ${chalk.gray('Timestamp:')} ${formatDateLong(event.timestamp)}`);
596
+
597
+ if (event.environment) {
598
+ console.log(` ${chalk.gray('Environment:')} ${chalk.cyan(event.environment)}`);
599
+ }
600
+ if (event.release_version) {
601
+ console.log(` ${chalk.gray('Version:')} ${chalk.cyan(event.release_version)}`);
602
+ }
603
+
604
+ // Show context data if available
605
+ if (event.context && Object.keys(event.context).length > 0) {
606
+ console.log(` ${chalk.gray('Context:')}`);
607
+ const contextKeys = Object.keys(event.context).filter(k => k !== 'frames');
608
+ if (contextKeys.length > 0) {
609
+ for (const key of contextKeys.slice(0, 5)) {
610
+ const value = event.context[key];
611
+ const valueStr = typeof value === 'object' ? JSON.stringify(value).substring(0, 50) + '...' : String(value).substring(0, 50);
612
+ console.log(` ${chalk.dim(key)}: ${chalk.white(valueStr)}`);
613
+ }
614
+ if (contextKeys.length > 5) {
615
+ console.log(chalk.dim(` ... and ${contextKeys.length - 5} more fields`));
616
+ }
617
+ }
618
+ }
619
+
620
+ console.log(chalk.gray(' ─'.repeat(40)));
621
+ }
622
+
623
+ console.log(chalk.gray(`\nShowing ${data.events.length} event${data.events.length !== 1 ? 's' : ''} (offset: ${options.offset})`));
624
+ console.log(chalk.dim('View full details: keplog issues show <issue-id> --format json\n'));
625
+
626
+ } catch (error: any) {
627
+ console.error(chalk.red(`\nāœ— Error: ${error.message}\n`));
628
+ process.exit(1);
629
+ }
630
+ });
631
+
632
+ // Main issues command
633
+ export const issuesCommand = new Command('issues')
634
+ .description('Manage and view issues')
635
+ .addCommand(listSubcommand)
636
+ .addCommand(showSubcommand)
637
+ .addCommand(eventsSubcommand);
638
+
639
+ // Helper functions
640
+ function truncate(str: string, maxLength: number): string {
641
+ if (str.length <= maxLength) return str;
642
+ return str.substring(0, maxLength - 3) + '...';
643
+ }
644
+
645
+ function colorStatus(status: string): string {
646
+ switch (status) {
647
+ case 'open':
648
+ return chalk.red(status);
649
+ case 'in_progress':
650
+ return chalk.yellow(status);
651
+ case 'resolved':
652
+ return chalk.green(status);
653
+ case 'ignored':
654
+ return chalk.gray(status);
655
+ default:
656
+ return chalk.white(status);
657
+ }
658
+ }
659
+
660
+ function colorLevel(level: string): string {
661
+ switch (level) {
662
+ case 'critical':
663
+ return chalk.red.bold(level);
664
+ case 'error':
665
+ return chalk.red(level);
666
+ case 'warning':
667
+ return chalk.yellow(level);
668
+ case 'info':
669
+ return chalk.blue(level);
670
+ default:
671
+ return chalk.white(level);
672
+ }
673
+ }
674
+
675
+ function formatDate(dateStr: string): string {
676
+ const date = new Date(dateStr);
677
+ const now = new Date();
678
+ const diff = now.getTime() - date.getTime();
679
+
680
+ const minutes = Math.floor(diff / 60000);
681
+ const hours = Math.floor(diff / 3600000);
682
+ const days = Math.floor(diff / 86400000);
683
+
684
+ if (minutes < 1) return 'just now';
685
+ if (minutes < 60) return `${minutes}m ago`;
686
+ if (hours < 24) return `${hours}h ago`;
687
+ if (days < 7) return `${days}d ago`;
688
+
689
+ return date.toLocaleDateString();
690
+ }
691
+
692
+ function formatDateLong(dateStr: string): string {
693
+ const date = new Date(dateStr);
694
+ return date.toLocaleString();
695
+ }