@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,524 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import axios from 'axios';
5
+ import MockAdapter from 'axios-mock-adapter';
6
+ import { uploadSourceMaps } from '../src/lib/uploader';
7
+
8
+ // Mock ora to avoid spinner output during tests
9
+ jest.mock('ora', () => {
10
+ return jest.fn(() => ({
11
+ start: jest.fn().mockReturnThis(),
12
+ succeed: jest.fn().mockReturnThis(),
13
+ fail: jest.fn().mockReturnThis(),
14
+ info: jest.fn().mockReturnThis(),
15
+ }));
16
+ });
17
+
18
+ describe('uploadSourceMaps', () => {
19
+ let mock: MockAdapter;
20
+ let testDir: string;
21
+ let originalProcessExit: any;
22
+
23
+ beforeEach(() => {
24
+ // Create axios mock
25
+ mock = new MockAdapter(axios);
26
+
27
+ // Create temporary test directory
28
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'keplog-upload-test-'));
29
+
30
+ // Mock process.exit to prevent tests from actually exiting
31
+ originalProcessExit = process.exit;
32
+ process.exit = jest.fn() as any;
33
+
34
+ // Suppress console output during tests
35
+ jest.spyOn(console, 'log').mockImplementation();
36
+ jest.spyOn(console, 'error').mockImplementation();
37
+ jest.spyOn(console, 'warn').mockImplementation();
38
+ });
39
+
40
+ afterEach(() => {
41
+ // Restore mocks
42
+ mock.restore();
43
+ process.exit = originalProcessExit;
44
+
45
+ // Clean up test directory
46
+ if (fs.existsSync(testDir)) {
47
+ fs.rmSync(testDir, { recursive: true, force: true });
48
+ }
49
+
50
+ // Restore console
51
+ jest.restoreAllMocks();
52
+ });
53
+
54
+ describe('file discovery', () => {
55
+ it('should find source map files matching pattern', async () => {
56
+ // Create test .map files
57
+ const mapFiles = ['app.js.map', 'vendor.js.map', 'style.css.map'];
58
+ mapFiles.forEach(file => {
59
+ fs.writeFileSync(path.join(testDir, file), '{}', 'utf-8');
60
+ });
61
+
62
+ // Mock successful upload
63
+ mock.onPost().reply(200, {
64
+ uploaded: mapFiles,
65
+ errors: [],
66
+ release: 'v1.0.0',
67
+ count: mapFiles.length,
68
+ });
69
+
70
+ await uploadSourceMaps({
71
+ release: 'v1.0.0',
72
+ filePatterns: [path.join(testDir, '*.map')],
73
+ projectId: 'test-project',
74
+ apiKey: 'test-key',
75
+ apiUrl: 'https://api.keplog.com',
76
+ verbose: false,
77
+ });
78
+
79
+ // Should have called the API
80
+ expect(mock.history.post.length).toBe(1);
81
+ });
82
+
83
+ it('should find nested source map files with glob pattern', async () => {
84
+ // Create nested directory structure
85
+ const nestedDir = path.join(testDir, 'dist', 'js');
86
+ fs.mkdirSync(nestedDir, { recursive: true });
87
+
88
+ fs.writeFileSync(path.join(nestedDir, 'app.js.map'), '{}', 'utf-8');
89
+ fs.writeFileSync(path.join(testDir, 'dist', 'style.css.map'), '{}', 'utf-8');
90
+
91
+ mock.onPost().reply(200, {
92
+ uploaded: ['app.js.map', 'style.css.map'],
93
+ errors: [],
94
+ release: 'v1.0.0',
95
+ count: 2,
96
+ });
97
+
98
+ await uploadSourceMaps({
99
+ release: 'v1.0.0',
100
+ filePatterns: [path.join(testDir, '**/*.map')],
101
+ projectId: 'test-project',
102
+ apiKey: 'test-key',
103
+ apiUrl: 'https://api.keplog.com',
104
+ verbose: false,
105
+ });
106
+
107
+ expect(mock.history.post.length).toBe(1);
108
+ });
109
+
110
+ it('should handle multiple file patterns', async () => {
111
+ // Create files in different directories
112
+ fs.mkdirSync(path.join(testDir, 'dist'), { recursive: true });
113
+ fs.mkdirSync(path.join(testDir, 'build'), { recursive: true });
114
+
115
+ fs.writeFileSync(path.join(testDir, 'dist', 'app.js.map'), '{}', 'utf-8');
116
+ fs.writeFileSync(path.join(testDir, 'build', 'vendor.js.map'), '{}', 'utf-8');
117
+
118
+ mock.onPost().reply(200, {
119
+ uploaded: ['app.js.map', 'vendor.js.map'],
120
+ errors: [],
121
+ release: 'v1.0.0',
122
+ count: 2,
123
+ });
124
+
125
+ await uploadSourceMaps({
126
+ release: 'v1.0.0',
127
+ filePatterns: [
128
+ path.join(testDir, 'dist/*.map'),
129
+ path.join(testDir, 'build/*.map'),
130
+ ],
131
+ projectId: 'test-project',
132
+ apiKey: 'test-key',
133
+ apiUrl: 'https://api.keplog.com',
134
+ verbose: false,
135
+ });
136
+
137
+ expect(mock.history.post.length).toBe(1);
138
+ });
139
+
140
+ it('should remove duplicate files from multiple patterns', async () => {
141
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{}', 'utf-8');
142
+
143
+ mock.onPost().reply(200, {
144
+ uploaded: ['app.js.map'],
145
+ errors: [],
146
+ release: 'v1.0.0',
147
+ count: 1,
148
+ });
149
+
150
+ await uploadSourceMaps({
151
+ release: 'v1.0.0',
152
+ // Same file matched by two patterns
153
+ filePatterns: [
154
+ path.join(testDir, '*.map'),
155
+ path.join(testDir, 'app.js.map'),
156
+ ],
157
+ projectId: 'test-project',
158
+ apiKey: 'test-key',
159
+ apiUrl: 'https://api.keplog.com',
160
+ verbose: false,
161
+ });
162
+
163
+ // Should only upload once
164
+ expect(mock.history.post.length).toBe(1);
165
+ });
166
+
167
+ it('should exit with error when no files found', async () => {
168
+ try {
169
+ await uploadSourceMaps({
170
+ release: 'v1.0.0',
171
+ filePatterns: [path.join(testDir, '*.map')],
172
+ projectId: 'test-project',
173
+ apiKey: 'test-key',
174
+ apiUrl: 'https://api.keplog.com',
175
+ verbose: false,
176
+ });
177
+ } catch (error) {
178
+ // May throw or exit
179
+ }
180
+
181
+ expect(process.exit).toHaveBeenCalledWith(1);
182
+ });
183
+
184
+ it('should filter out non-.map files', async () => {
185
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{}', 'utf-8');
186
+ fs.writeFileSync(path.join(testDir, 'app.js'), 'code', 'utf-8');
187
+
188
+ mock.onPost().reply(200, {
189
+ uploaded: ['app.js.map'],
190
+ errors: [],
191
+ release: 'v1.0.0',
192
+ count: 1,
193
+ });
194
+
195
+ await uploadSourceMaps({
196
+ release: 'v1.0.0',
197
+ filePatterns: [path.join(testDir, '*')],
198
+ projectId: 'test-project',
199
+ apiKey: 'test-key',
200
+ apiUrl: 'https://api.keplog.com',
201
+ verbose: false,
202
+ });
203
+
204
+ expect(mock.history.post.length).toBe(1);
205
+ });
206
+ });
207
+
208
+ describe('API communication', () => {
209
+ beforeEach(() => {
210
+ // Create a test .map file for upload tests
211
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{"version":3}', 'utf-8');
212
+ });
213
+
214
+ it('should send correct API request', async () => {
215
+ mock.onPost().reply(config => {
216
+ // Verify URL and headers
217
+ expect(config.url).toContain('/api/v1/cli/projects/test-project/sourcemaps');
218
+ expect(config.headers?.['X-API-Key']).toBe('test-api-key');
219
+
220
+ return [200, {
221
+ uploaded: ['app.js.map'],
222
+ errors: [],
223
+ release: 'v1.0.0',
224
+ count: 1,
225
+ }];
226
+ });
227
+
228
+ await uploadSourceMaps({
229
+ release: 'v1.0.0',
230
+ filePatterns: [path.join(testDir, '*.map')],
231
+ projectId: 'test-project',
232
+ apiKey: 'test-api-key',
233
+ apiUrl: 'https://api.keplog.com',
234
+ verbose: false,
235
+ });
236
+
237
+ expect(mock.history.post.length).toBe(1);
238
+ });
239
+
240
+ it('should include release in form data', async () => {
241
+ mock.onPost().reply(200, {
242
+ uploaded: ['app.js.map'],
243
+ errors: [],
244
+ release: 'v2.0.0',
245
+ count: 1,
246
+ });
247
+
248
+ await uploadSourceMaps({
249
+ release: 'v2.0.0',
250
+ filePatterns: [path.join(testDir, '*.map')],
251
+ projectId: 'test-project',
252
+ apiKey: 'test-key',
253
+ apiUrl: 'https://api.keplog.com',
254
+ verbose: false,
255
+ });
256
+
257
+ // Verify request was made
258
+ expect(mock.history.post.length).toBe(1);
259
+ });
260
+
261
+ it('should handle successful upload', async () => {
262
+ mock.onPost().reply(200, {
263
+ uploaded: ['app.js.map'],
264
+ errors: [],
265
+ release: 'v1.0.0',
266
+ count: 1,
267
+ });
268
+
269
+ await expect(uploadSourceMaps({
270
+ release: 'v1.0.0',
271
+ filePatterns: [path.join(testDir, '*.map')],
272
+ projectId: 'test-project',
273
+ apiKey: 'test-key',
274
+ apiUrl: 'https://api.keplog.com',
275
+ verbose: false,
276
+ })).resolves.not.toThrow();
277
+ });
278
+
279
+ it('should exit with error when upload has errors', async () => {
280
+ mock.onPost().reply(200, {
281
+ uploaded: [],
282
+ errors: ['File too large', 'Invalid format'],
283
+ release: 'v1.0.0',
284
+ count: 0,
285
+ });
286
+
287
+ await uploadSourceMaps({
288
+ release: 'v1.0.0',
289
+ filePatterns: [path.join(testDir, '*.map')],
290
+ projectId: 'test-project',
291
+ apiKey: 'test-key',
292
+ apiUrl: 'https://api.keplog.com',
293
+ verbose: false,
294
+ });
295
+
296
+ expect(process.exit).toHaveBeenCalledWith(1);
297
+ });
298
+ });
299
+
300
+ describe('error handling', () => {
301
+ beforeEach(() => {
302
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{}', 'utf-8');
303
+ });
304
+
305
+ it('should handle 401 authentication error', async () => {
306
+ mock.onPost().reply(401, {
307
+ error: 'Invalid API key',
308
+ });
309
+
310
+ await expect(uploadSourceMaps({
311
+ release: 'v1.0.0',
312
+ filePatterns: [path.join(testDir, '*.map')],
313
+ projectId: 'test-project',
314
+ apiKey: 'invalid-key',
315
+ apiUrl: 'https://api.keplog.com',
316
+ verbose: false,
317
+ })).rejects.toThrow('Invalid API key');
318
+ });
319
+
320
+ it('should handle 404 project not found', async () => {
321
+ mock.onPost().reply(404, {
322
+ error: 'Project not found',
323
+ });
324
+
325
+ await expect(uploadSourceMaps({
326
+ release: 'v1.0.0',
327
+ filePatterns: [path.join(testDir, '*.map')],
328
+ projectId: 'nonexistent',
329
+ apiKey: 'test-key',
330
+ apiUrl: 'https://api.keplog.com',
331
+ verbose: false,
332
+ })).rejects.toThrow('Project not found');
333
+ });
334
+
335
+ it('should handle network errors', async () => {
336
+ mock.onPost().networkError();
337
+
338
+ await expect(uploadSourceMaps({
339
+ release: 'v1.0.0',
340
+ filePatterns: [path.join(testDir, '*.map')],
341
+ projectId: 'test-project',
342
+ apiKey: 'test-key',
343
+ apiUrl: 'https://api.keplog.com',
344
+ verbose: false,
345
+ })).rejects.toThrow();
346
+ });
347
+
348
+ it('should handle timeout errors', async () => {
349
+ mock.onPost().timeout();
350
+
351
+ await expect(uploadSourceMaps({
352
+ release: 'v1.0.0',
353
+ filePatterns: [path.join(testDir, '*.map')],
354
+ projectId: 'test-project',
355
+ apiKey: 'test-key',
356
+ apiUrl: 'https://api.keplog.com',
357
+ verbose: false,
358
+ })).rejects.toThrow();
359
+ });
360
+
361
+ it('should handle server connection errors', async () => {
362
+ mock.onPost().reply(() => {
363
+ const error: any = new Error('connect ECONNREFUSED');
364
+ error.code = 'ECONNREFUSED';
365
+ error.isAxiosError = true;
366
+ throw error;
367
+ });
368
+
369
+ await expect(uploadSourceMaps({
370
+ release: 'v1.0.0',
371
+ filePatterns: [path.join(testDir, '*.map')],
372
+ projectId: 'test-project',
373
+ apiKey: 'test-key',
374
+ apiUrl: 'https://api.keplog.com',
375
+ verbose: false,
376
+ })).rejects.toThrow();
377
+ });
378
+
379
+ it('should handle DNS resolution errors', async () => {
380
+ mock.onPost().reply(() => {
381
+ const error: any = new Error('getaddrinfo ENOTFOUND');
382
+ error.code = 'ENOTFOUND';
383
+ error.isAxiosError = true;
384
+ throw error;
385
+ });
386
+
387
+ await expect(uploadSourceMaps({
388
+ release: 'v1.0.0',
389
+ filePatterns: [path.join(testDir, '*.map')],
390
+ projectId: 'test-project',
391
+ apiKey: 'test-key',
392
+ apiUrl: 'https://invalid-domain.keplog.com',
393
+ verbose: false,
394
+ })).rejects.toThrow();
395
+ });
396
+
397
+ it('should handle invalid glob patterns', async () => {
398
+ await expect(uploadSourceMaps({
399
+ release: 'v1.0.0',
400
+ filePatterns: ['[invalid-pattern'],
401
+ projectId: 'test-project',
402
+ apiKey: 'test-key',
403
+ apiUrl: 'https://api.keplog.com',
404
+ verbose: false,
405
+ })).rejects.toThrow();
406
+ });
407
+ });
408
+
409
+ describe('verbose mode', () => {
410
+ beforeEach(() => {
411
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{}', 'utf-8');
412
+ fs.writeFileSync(path.join(testDir, 'vendor.js.map'), '{}', 'utf-8');
413
+
414
+ mock.onPost().reply(200, {
415
+ uploaded: ['app.js.map', 'vendor.js.map'],
416
+ errors: [],
417
+ release: 'v1.0.0',
418
+ count: 2,
419
+ });
420
+ });
421
+
422
+ it('should display detailed information in verbose mode', async () => {
423
+ const consoleSpy = jest.spyOn(console, 'log');
424
+
425
+ await uploadSourceMaps({
426
+ release: 'v1.0.0',
427
+ filePatterns: [path.join(testDir, '*.map')],
428
+ projectId: 'test-project',
429
+ apiKey: 'test-key',
430
+ apiUrl: 'https://api.keplog.com',
431
+ verbose: true,
432
+ });
433
+
434
+ expect(consoleSpy).toHaveBeenCalled();
435
+ });
436
+
437
+ it('should not display extra information when verbose is false', async () => {
438
+ await uploadSourceMaps({
439
+ release: 'v1.0.0',
440
+ filePatterns: [path.join(testDir, '*.map')],
441
+ projectId: 'test-project',
442
+ apiKey: 'test-key',
443
+ apiUrl: 'https://api.keplog.com',
444
+ verbose: false,
445
+ });
446
+
447
+ // Should still complete successfully
448
+ expect(mock.history.post.length).toBe(1);
449
+ });
450
+ });
451
+
452
+ describe('edge cases', () => {
453
+ it('should handle empty release string', async () => {
454
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{}', 'utf-8');
455
+
456
+ mock.onPost().reply(200, {
457
+ uploaded: ['app.js.map'],
458
+ errors: [],
459
+ release: '',
460
+ count: 1,
461
+ });
462
+
463
+ await uploadSourceMaps({
464
+ release: '',
465
+ filePatterns: [path.join(testDir, '*.map')],
466
+ projectId: 'test-project',
467
+ apiKey: 'test-key',
468
+ apiUrl: 'https://api.keplog.com',
469
+ verbose: false,
470
+ });
471
+
472
+ expect(mock.history.post.length).toBe(1);
473
+ });
474
+
475
+ it('should handle very large number of files', async () => {
476
+ // Create 100 test files
477
+ for (let i = 0; i < 100; i++) {
478
+ fs.writeFileSync(path.join(testDir, `file${i}.js.map`), '{}', 'utf-8');
479
+ }
480
+
481
+ const files = Array.from({ length: 100 }, (_, i) => `file${i}.js.map`);
482
+ mock.onPost().reply(200, {
483
+ uploaded: files,
484
+ errors: [],
485
+ release: 'v1.0.0',
486
+ count: 100,
487
+ });
488
+
489
+ await uploadSourceMaps({
490
+ release: 'v1.0.0',
491
+ filePatterns: [path.join(testDir, '*.map')],
492
+ projectId: 'test-project',
493
+ apiKey: 'test-key',
494
+ apiUrl: 'https://api.keplog.com',
495
+ verbose: false,
496
+ });
497
+
498
+ expect(mock.history.post.length).toBe(1);
499
+ });
500
+
501
+ it('should handle custom API URL', async () => {
502
+ fs.writeFileSync(path.join(testDir, 'app.js.map'), '{}', 'utf-8');
503
+
504
+ mock.onPost('https://custom.keplog.io/api/v1/cli/projects/test-project/sourcemaps')
505
+ .reply(200, {
506
+ uploaded: ['app.js.map'],
507
+ errors: [],
508
+ release: 'v1.0.0',
509
+ count: 1,
510
+ });
511
+
512
+ await uploadSourceMaps({
513
+ release: 'v1.0.0',
514
+ filePatterns: [path.join(testDir, '*.map')],
515
+ projectId: 'test-project',
516
+ apiKey: 'test-key',
517
+ apiUrl: 'https://custom.keplog.io',
518
+ verbose: false,
519
+ });
520
+
521
+ expect(mock.history.post[0].url).toContain('custom.keplog.io');
522
+ });
523
+ });
524
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }