@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.
- package/LICENSE +21 -0
- package/README.md +495 -0
- package/bin/keplog +2 -0
- package/dist/commands/delete.d.ts +3 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +158 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/issues.d.ts +3 -0
- package/dist/commands/issues.d.ts.map +1 -0
- package/dist/commands/issues.js +543 -0
- package/dist/commands/issues.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +104 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/releases.d.ts +3 -0
- package/dist/commands/releases.d.ts.map +1 -0
- package/dist/commands/releases.js +100 -0
- package/dist/commands/releases.js.map +1 -0
- package/dist/commands/upload.d.ts +3 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +76 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +57 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +155 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/uploader.d.ts +11 -0
- package/dist/lib/uploader.d.ts.map +1 -0
- package/dist/lib/uploader.js +171 -0
- package/dist/lib/uploader.js.map +1 -0
- package/jest.config.js +16 -0
- package/package.json +58 -0
- package/src/commands/delete.ts +186 -0
- package/src/commands/init.ts +137 -0
- package/src/commands/issues.ts +695 -0
- package/src/commands/list.ts +124 -0
- package/src/commands/releases.ts +122 -0
- package/src/commands/upload.ts +76 -0
- package/src/index.ts +31 -0
- package/src/lib/config.ts +138 -0
- package/src/lib/uploader.ts +168 -0
- package/tests/README.md +380 -0
- package/tests/config.test.ts +397 -0
- package/tests/uploader.test.ts +524 -0
- 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
|
+
}
|