@keplog/cli 0.2.1

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