@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,124 @@
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 SourceMap {
7
+ Filename: string;
8
+ Size: number;
9
+ UploadedAt: string;
10
+ }
11
+
12
+ interface ListResponse {
13
+ source_maps: SourceMap[];
14
+ release: string;
15
+ count: number;
16
+ }
17
+
18
+ export const listCommand = new Command('list')
19
+ .description('List uploaded source maps for a specific release')
20
+ .option('-r, --release <version>', 'Release version to list source maps for')
21
+ .option('-p, --project-id <id>', 'Project ID (overrides config)')
22
+ .option('-k, --api-key <key>', 'API key (overrides config)')
23
+ .option('-u, --api-url <url>', 'API URL (overrides config)')
24
+ .action(async (options) => {
25
+ try {
26
+ // Read config from file (priority: local > global > env)
27
+ const config = ConfigManager.getConfig();
28
+
29
+ const release = options.release || process.env.KEPLOG_RELEASE;
30
+ const projectId = options.projectId || config.projectId;
31
+ const apiKey = options.apiKey || config.apiKey;
32
+ const apiUrl = options.apiUrl || config.apiUrl || 'https://api.keplog.com';
33
+
34
+ // Validate required parameters
35
+ if (!projectId) {
36
+ console.error(chalk.red('\n✗ Error: Project ID is required\n'));
37
+ console.log('Options:');
38
+ console.log(' 1. Run: keplog init (recommended)');
39
+ console.log(' 2. Use flag: --project-id=<your-project-id>');
40
+ console.log(' 3. Set env: KEPLOG_PROJECT_ID=<your-project-id>\n');
41
+ process.exit(1);
42
+ }
43
+
44
+ if (!apiKey) {
45
+ console.error(chalk.red('\n✗ Error: API key is required\n'));
46
+ console.log('Options:');
47
+ console.log(' 1. Run: keplog init (recommended)');
48
+ console.log(' 2. Use flag: --api-key=<your-api-key>');
49
+ console.log(' 3. Set env: KEPLOG_API_KEY=<your-api-key>\n');
50
+ process.exit(1);
51
+ }
52
+
53
+ if (!release) {
54
+ console.error(chalk.red('\n✗ Error: Release version is required\n'));
55
+ console.log('Options:');
56
+ console.log(' 1. Use flag: --release=v1.0.0');
57
+ console.log(' 2. Set env: KEPLOG_RELEASE=v1.0.0\n');
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log(chalk.bold.cyan('\n📁 Keplog Source Maps - List\n'));
62
+ console.log(`Release: ${chalk.green(release)}`);
63
+ console.log(`Project ID: ${chalk.gray(projectId)}`);
64
+ console.log(`API URL: ${chalk.gray(apiUrl)}\n`);
65
+
66
+ const spinner = ora('Fetching source maps...').start();
67
+
68
+ // Fetch source maps from API
69
+ const url = `${apiUrl}/api/v1/cli/projects/${projectId}/sourcemaps?release=${encodeURIComponent(release)}`;
70
+ const response = await fetch(url, {
71
+ method: 'GET',
72
+ headers: {
73
+ 'X-API-Key': apiKey,
74
+ },
75
+ });
76
+
77
+ if (!response.ok) {
78
+ const error = await response.json() as any;
79
+ spinner.fail(chalk.red('Failed to fetch source maps'));
80
+ console.error(chalk.red(`\n✗ Error: ${error.error || 'Unknown error'}\n`));
81
+ process.exit(1);
82
+ }
83
+
84
+ const data = await response.json() as ListResponse;
85
+ spinner.succeed(chalk.green('Source maps fetched'));
86
+
87
+ // Display results
88
+ console.log(chalk.bold(`\n📦 Source Maps for ${chalk.cyan(data.release)}`));
89
+ console.log(chalk.gray(`Found ${data.count} file${data.count !== 1 ? 's' : ''}\n`));
90
+
91
+ if (data.count === 0) {
92
+ console.log(chalk.yellow('No source maps found for this release.\n'));
93
+ console.log(chalk.gray('Upload source maps using: keplog upload --release=' + release + '\n'));
94
+ return;
95
+ }
96
+
97
+ // Calculate total size
98
+ let totalSize = 0;
99
+ for (const file of data.source_maps) {
100
+ totalSize += file.Size;
101
+ }
102
+
103
+ // Display files
104
+ for (const file of data.source_maps) {
105
+ const size = formatFileSize(file.Size);
106
+ const date = new Date(file.UploadedAt).toLocaleString();
107
+ console.log(`${chalk.green('✓')} ${chalk.white(file.Filename.padEnd(40))} ${chalk.gray(size.padEnd(12))} ${chalk.dim(date)}`);
108
+ }
109
+
110
+ console.log(chalk.gray(`\nTotal: ${data.count} files (${formatFileSize(totalSize)})\n`));
111
+
112
+ } catch (error: any) {
113
+ console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
114
+ process.exit(1);
115
+ }
116
+ });
117
+
118
+ function formatFileSize(bytes: number): string {
119
+ if (bytes === 0) return '0 B';
120
+ const k = 1024;
121
+ const sizes = ['B', 'KB', 'MB', 'GB'];
122
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
123
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
124
+ }
@@ -0,0 +1,122 @@
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 ReleaseInfo {
7
+ Release: string;
8
+ FileCount: number;
9
+ TotalSize: number;
10
+ LastModified: string;
11
+ }
12
+
13
+ interface ReleasesResponse {
14
+ releases: ReleaseInfo[];
15
+ count: number;
16
+ }
17
+
18
+ export const releasesCommand = new Command('releases')
19
+ .description('List all releases with uploaded source maps')
20
+ .option('-p, --project-id <id>', 'Project ID (overrides config)')
21
+ .option('-k, --api-key <key>', 'API key (overrides config)')
22
+ .option('-u, --api-url <url>', 'API URL (overrides config)')
23
+ .action(async (options) => {
24
+ try {
25
+ // Read config from file (priority: local > global > env)
26
+ const config = ConfigManager.getConfig();
27
+
28
+ const projectId = options.projectId || config.projectId;
29
+ const apiKey = options.apiKey || config.apiKey;
30
+ const apiUrl = options.apiUrl || config.apiUrl || 'https://api.keplog.com';
31
+
32
+ // Validate required parameters
33
+ if (!projectId) {
34
+ console.error(chalk.red('\n✗ Error: Project ID is required\n'));
35
+ console.log('Options:');
36
+ console.log(' 1. Run: keplog init (recommended)');
37
+ console.log(' 2. Use flag: --project-id=<your-project-id>');
38
+ console.log(' 3. Set env: KEPLOG_PROJECT_ID=<your-project-id>\n');
39
+ process.exit(1);
40
+ }
41
+
42
+ if (!apiKey) {
43
+ console.error(chalk.red('\n✗ Error: API key is required\n'));
44
+ console.log('Options:');
45
+ console.log(' 1. Run: keplog init (recommended)');
46
+ console.log(' 2. Use flag: --api-key=<your-api-key>');
47
+ console.log(' 3. Set env: KEPLOG_API_KEY=<your-api-key>\n');
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(chalk.bold.cyan('\n📦 Keplog Releases\n'));
52
+ console.log(`Project ID: ${chalk.gray(projectId)}`);
53
+ console.log(`API URL: ${chalk.gray(apiUrl)}\n`);
54
+
55
+ const spinner = ora('Fetching releases...').start();
56
+
57
+ // Fetch releases from API
58
+ const url = `${apiUrl}/api/v1/cli/projects/${projectId}/sourcemaps/releases`;
59
+ const response = await fetch(url, {
60
+ method: 'GET',
61
+ headers: {
62
+ 'X-API-Key': apiKey,
63
+ },
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const error = await response.json() as any;
68
+ spinner.fail(chalk.red('Failed to fetch releases'));
69
+ console.error(chalk.red(`\n✗ Error: ${error.error || 'Unknown error'}\n`));
70
+ process.exit(1);
71
+ }
72
+
73
+ const data = await response.json() as ReleasesResponse;
74
+ spinner.succeed(chalk.green('Releases fetched'));
75
+
76
+ // Display results
77
+ console.log(chalk.bold(`\n📋 Releases with Source Maps`));
78
+ console.log(chalk.gray(`Found ${data.count} release${data.count !== 1 ? 's' : ''}\n`));
79
+
80
+ if (data.count === 0) {
81
+ console.log(chalk.yellow('No releases found.\n'));
82
+ console.log(chalk.gray('Upload source maps using: keplog upload --release=v1.0.0 --files="dist/**/*.map"\n'));
83
+ return;
84
+ }
85
+
86
+ // Calculate total size across all releases
87
+ let totalSize = 0;
88
+ let totalFiles = 0;
89
+ for (const release of data.releases) {
90
+ totalSize += release.TotalSize;
91
+ totalFiles += release.FileCount;
92
+ }
93
+
94
+ // Display releases
95
+ for (const release of data.releases) {
96
+ const size = formatFileSize(release.TotalSize);
97
+ const date = new Date(release.LastModified).toLocaleDateString();
98
+ const fileText = `${release.FileCount} file${release.FileCount !== 1 ? 's' : ''}`;
99
+
100
+ console.log(
101
+ `${chalk.cyan(release.Release.padEnd(30))} ` +
102
+ `${chalk.gray(fileText.padEnd(12))} ` +
103
+ `${chalk.yellow(size.padEnd(12))} ` +
104
+ `${chalk.dim(date)}`
105
+ );
106
+ }
107
+
108
+ console.log(chalk.gray(`\nTotal: ${data.count} releases, ${totalFiles} files (${formatFileSize(totalSize)})\n`));
109
+
110
+ } catch (error: any) {
111
+ console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
112
+ process.exit(1);
113
+ }
114
+ });
115
+
116
+ function formatFileSize(bytes: number): string {
117
+ if (bytes === 0) return '0 B';
118
+ const k = 1024;
119
+ const sizes = ['B', 'KB', 'MB', 'GB'];
120
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
121
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
122
+ }
@@ -0,0 +1,76 @@
1
+ import { Command } from 'commander';
2
+ import { uploadSourceMaps } from '../lib/uploader';
3
+ import { ConfigManager } from '../lib/config';
4
+ import chalk from 'chalk';
5
+
6
+ export const uploadCommand = new Command('upload')
7
+ .description('Upload source maps for a specific release')
8
+ .option('-r, --release <version>', 'Release version (e.g., v1.0.0)')
9
+ .option('-f, --files <patterns...>', 'Source map file patterns (supports glob)')
10
+ .option('-p, --project-id <id>', 'Project ID (overrides config)')
11
+ .option('-k, --api-key <key>', 'API Key (overrides config)')
12
+ .option('-u, --api-url <url>', 'API URL (overrides config)')
13
+ .option('-v, --verbose', 'Verbose output')
14
+ .action(async (options) => {
15
+ try {
16
+ // Read config from file (priority: local > global > env)
17
+ const config = ConfigManager.getConfig();
18
+
19
+ // Validate required options
20
+ const release = options.release;
21
+ const files = options.files || [];
22
+ const projectId = options.projectId || config.projectId;
23
+ const apiKey = options.apiKey || config.apiKey;
24
+ const apiUrl = options.apiUrl || config.apiUrl || 'https://api.keplog.com';
25
+ const verbose = options.verbose || false;
26
+
27
+ if (!release) {
28
+ console.error(chalk.red('❌ Error: --release is required'));
29
+ console.log(chalk.gray('\nExample:'));
30
+ console.log(chalk.gray(' keplog upload --release=v1.0.0 --files="dist/**/*.map"'));
31
+ process.exit(1);
32
+ }
33
+
34
+ if (!files || files.length === 0) {
35
+ console.error(chalk.red('❌ Error: --files is required'));
36
+ console.log(chalk.gray('\nExample:'));
37
+ console.log(chalk.gray(' keplog upload --release=v1.0.0 --files="dist/**/*.map"'));
38
+ process.exit(1);
39
+ }
40
+
41
+ if (!projectId) {
42
+ console.error(chalk.red('❌ Error: Project ID is required'));
43
+ console.log(chalk.gray('\nOptions:'));
44
+ console.log(chalk.gray(' 1. Run: keplog init (recommended)'));
45
+ console.log(chalk.gray(' 2. Use flag: --project-id=<your-project-id>'));
46
+ console.log(chalk.gray(' 3. Set env: KEPLOG_PROJECT_ID=<your-project-id>'));
47
+ process.exit(1);
48
+ }
49
+
50
+ if (!apiKey) {
51
+ console.error(chalk.red('❌ Error: API Key is required'));
52
+ console.log(chalk.gray('\nOptions:'));
53
+ console.log(chalk.gray(' 1. Run: keplog init (recommended)'));
54
+ console.log(chalk.gray(' 2. Use flag: --api-key=<your-api-key>'));
55
+ console.log(chalk.gray(' 3. Set env: KEPLOG_API_KEY=<your-api-key>'));
56
+ process.exit(1);
57
+ }
58
+
59
+ // Upload source maps
60
+ await uploadSourceMaps({
61
+ release,
62
+ filePatterns: files,
63
+ projectId,
64
+ apiKey,
65
+ apiUrl,
66
+ verbose,
67
+ });
68
+
69
+ } catch (error: any) {
70
+ console.error(chalk.red(`\n❌ Error: ${error.message}`));
71
+ if (error.stack && process.env.DEBUG) {
72
+ console.error(chalk.gray(error.stack));
73
+ }
74
+ process.exit(1);
75
+ }
76
+ });
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { initCommand } from './commands/init.js';
5
+ import { uploadCommand } from './commands/upload.js';
6
+ import { listCommand } from './commands/list.js';
7
+ import { deleteCommand } from './commands/delete.js';
8
+ import { releasesCommand } from './commands/releases.js';
9
+ import { issuesCommand } from './commands/issues.js';
10
+ import { config } from 'dotenv';
11
+
12
+ // Load environment variables from .env file
13
+ config();
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name('keplog')
19
+ .description('Official Keplog CLI for error tracking and source map management')
20
+ .version('1.0.0');
21
+
22
+ // Add commands
23
+ program.addCommand(initCommand);
24
+ program.addCommand(uploadCommand);
25
+ program.addCommand(listCommand);
26
+ program.addCommand(deleteCommand);
27
+ program.addCommand(releasesCommand);
28
+ program.addCommand(issuesCommand);
29
+
30
+ // Parse command line arguments
31
+ program.parse(process.argv);
@@ -0,0 +1,138 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ export interface KeplogConfig {
6
+ projectId?: string;
7
+ apiKey?: string;
8
+ apiUrl?: string;
9
+ projectName?: string;
10
+ }
11
+
12
+ const CONFIG_FILENAME = '.keplog.json';
13
+ const GLOBAL_CONFIG_PATH = path.join(homedir(), '.keplogrc');
14
+
15
+ /**
16
+ * Config Manager for Keplog CLI
17
+ *
18
+ * Priority order:
19
+ * 1. Local .keplog.json (project-specific)
20
+ * 2. Global ~/.keplogrc (user-level)
21
+ * 3. Environment variables (KEPLOG_PROJECT_ID, KEPLOG_API_KEY)
22
+ */
23
+ export class ConfigManager {
24
+ /**
25
+ * Find the nearest .keplog.json file by walking up the directory tree
26
+ */
27
+ private static findLocalConfigPath(startDir: string = process.cwd()): string | null {
28
+ let currentDir = startDir;
29
+ const root = path.parse(currentDir).root;
30
+
31
+ while (currentDir !== root) {
32
+ const configPath = path.join(currentDir, CONFIG_FILENAME);
33
+ if (fs.existsSync(configPath)) {
34
+ return configPath;
35
+ }
36
+ currentDir = path.dirname(currentDir);
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * Read configuration from local or global config file
44
+ */
45
+ static readConfig(): KeplogConfig {
46
+ // Try local config first
47
+ const localConfigPath = this.findLocalConfigPath();
48
+ if (localConfigPath) {
49
+ try {
50
+ const content = fs.readFileSync(localConfigPath, 'utf-8');
51
+ return JSON.parse(content);
52
+ } catch (error) {
53
+ console.warn(`Warning: Failed to read config from ${localConfigPath}`);
54
+ }
55
+ }
56
+
57
+ // Try global config
58
+ if (fs.existsSync(GLOBAL_CONFIG_PATH)) {
59
+ try {
60
+ const content = fs.readFileSync(GLOBAL_CONFIG_PATH, 'utf-8');
61
+ return JSON.parse(content);
62
+ } catch (error) {
63
+ console.warn(`Warning: Failed to read global config from ${GLOBAL_CONFIG_PATH}`);
64
+ }
65
+ }
66
+
67
+ return {};
68
+ }
69
+
70
+ /**
71
+ * Write configuration to local .keplog.json file
72
+ */
73
+ static writeLocalConfig(config: KeplogConfig, dir: string = process.cwd()): void {
74
+ const configPath = path.join(dir, CONFIG_FILENAME);
75
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
76
+ }
77
+
78
+ /**
79
+ * Write configuration to global ~/.keplogrc file
80
+ */
81
+ static writeGlobalConfig(config: KeplogConfig): void {
82
+ fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
83
+ }
84
+
85
+ /**
86
+ * Get configuration with priority: local > global > env vars
87
+ */
88
+ static getConfig(): KeplogConfig {
89
+ const fileConfig = this.readConfig();
90
+
91
+ return {
92
+ projectId: fileConfig.projectId || process.env.KEPLOG_PROJECT_ID,
93
+ apiKey: fileConfig.apiKey || process.env.KEPLOG_API_KEY,
94
+ apiUrl: fileConfig.apiUrl || process.env.KEPLOG_API_URL || 'https://api.keplog.com',
95
+ projectName: fileConfig.projectName,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Check if local config exists in current directory or parent directories
101
+ */
102
+ static hasLocalConfig(): boolean {
103
+ return this.findLocalConfigPath() !== null;
104
+ }
105
+
106
+ /**
107
+ * Check if global config exists
108
+ */
109
+ static hasGlobalConfig(): boolean {
110
+ return fs.existsSync(GLOBAL_CONFIG_PATH);
111
+ }
112
+
113
+ /**
114
+ * Get the path of the local config file (if it exists)
115
+ */
116
+ static getLocalConfigPath(): string | null {
117
+ return this.findLocalConfigPath();
118
+ }
119
+
120
+ /**
121
+ * Delete local config
122
+ */
123
+ static deleteLocalConfig(dir: string = process.cwd()): void {
124
+ const configPath = path.join(dir, CONFIG_FILENAME);
125
+ if (fs.existsSync(configPath)) {
126
+ fs.unlinkSync(configPath);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Delete global config
132
+ */
133
+ static deleteGlobalConfig(): void {
134
+ if (fs.existsSync(GLOBAL_CONFIG_PATH)) {
135
+ fs.unlinkSync(GLOBAL_CONFIG_PATH);
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,168 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { glob } from 'glob';
4
+ import FormData from 'form-data';
5
+ import axios from 'axios';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+
9
+ interface UploadOptions {
10
+ release: string;
11
+ filePatterns: string[];
12
+ projectId: string;
13
+ apiKey: string;
14
+ apiUrl: string;
15
+ verbose: boolean;
16
+ }
17
+
18
+ interface UploadResponse {
19
+ uploaded: string[];
20
+ errors: string[];
21
+ release: string;
22
+ count: number;
23
+ error?: string;
24
+ }
25
+
26
+ export async function uploadSourceMaps(options: UploadOptions): Promise<void> {
27
+ const { release, filePatterns, projectId, apiKey, apiUrl, verbose } = options;
28
+
29
+ console.log(chalk.bold.cyan('\n📦 Keplog Source Map Uploader\n'));
30
+ console.log(chalk.gray(`Release: ${release}`));
31
+ console.log(chalk.gray(`Project ID: ${projectId}`));
32
+ console.log(chalk.gray(`API URL: ${apiUrl}\n`));
33
+
34
+ // Find all matching files
35
+ const spinner = ora('Finding source map files...').start();
36
+ const allFiles: string[] = [];
37
+
38
+ for (const pattern of filePatterns) {
39
+ try {
40
+ const matches = await glob(pattern, { nodir: true });
41
+ if (verbose) {
42
+ spinner.info(`Pattern "${pattern}" matched ${matches.length} file(s)`);
43
+ }
44
+ allFiles.push(...matches);
45
+ } catch (error: any) {
46
+ spinner.fail(`Invalid glob pattern: ${pattern}`);
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ // Remove duplicates
52
+ const uniqueFiles = [...new Set(allFiles)];
53
+
54
+ if (uniqueFiles.length === 0) {
55
+ spinner.fail('No source map files found');
56
+ console.log(chalk.yellow('\n⚠️ No files matched the specified patterns'));
57
+ console.log(chalk.gray('\nTip: Make sure your patterns are correct:'));
58
+ console.log(chalk.gray(' --files="dist/**/*.map"'));
59
+ console.log(chalk.gray(' --files="build/*.map"'));
60
+ process.exit(1);
61
+ }
62
+
63
+ spinner.succeed(`Found ${uniqueFiles.length} source map file(s)`);
64
+
65
+ // Filter only .map files
66
+ const mapFiles = uniqueFiles.filter(file => file.endsWith('.map'));
67
+
68
+ if (mapFiles.length === 0) {
69
+ console.log(chalk.yellow('\n⚠️ No .map files found'));
70
+ process.exit(1);
71
+ }
72
+
73
+ if (mapFiles.length !== uniqueFiles.length) {
74
+ console.log(chalk.yellow(`\n⚠️ Skipped ${uniqueFiles.length - mapFiles.length} non-.map file(s)`));
75
+ }
76
+
77
+ // Display files to upload
78
+ if (verbose) {
79
+ console.log(chalk.cyan('\n📁 Files to upload:'));
80
+ mapFiles.forEach((file, index) => {
81
+ const stats = fs.statSync(file);
82
+ const size = formatFileSize(stats.size);
83
+ console.log(chalk.gray(` ${index + 1}. ${file} (${size})`));
84
+ });
85
+ console.log();
86
+ }
87
+
88
+ // Create form data
89
+ const uploadSpinner = ora(`Uploading ${mapFiles.length} file(s) to Keplog...`).start();
90
+
91
+ try {
92
+ const formData = new FormData();
93
+ formData.append('release', release);
94
+
95
+ // Add all files
96
+ for (const filePath of mapFiles) {
97
+ const fileName = path.basename(filePath);
98
+ const fileStream = fs.createReadStream(filePath);
99
+ formData.append('files', fileStream, fileName);
100
+ }
101
+
102
+ // Upload to API
103
+ const url = `${apiUrl}/api/v1/cli/projects/${projectId}/sourcemaps`;
104
+
105
+ const response = await axios.post(url, formData, {
106
+ headers: {
107
+ 'X-API-Key': apiKey,
108
+ ...formData.getHeaders(),
109
+ },
110
+ maxContentLength: Infinity,
111
+ maxBodyLength: Infinity,
112
+ });
113
+
114
+ const data = response.data as UploadResponse;
115
+
116
+ uploadSpinner.succeed('Upload complete!');
117
+
118
+ // Display results
119
+ console.log(chalk.bold.green('\n✅ Upload Complete!\n'));
120
+ console.log(chalk.gray(`Release: ${data.release}`));
121
+ console.log(chalk.gray(`Uploaded: ${data.count} file(s)\n`));
122
+
123
+ if (data.uploaded && data.uploaded.length > 0) {
124
+ console.log(chalk.cyan('📁 Successfully uploaded:'));
125
+ data.uploaded.forEach(filename => {
126
+ console.log(chalk.green(` ✓ ${filename}`));
127
+ });
128
+ }
129
+
130
+ if (data.errors && data.errors.length > 0) {
131
+ console.log(chalk.yellow('\n⚠️ Errors:'));
132
+ data.errors.forEach(error => {
133
+ console.log(chalk.red(` ✗ ${error}`));
134
+ });
135
+ process.exit(1);
136
+ }
137
+
138
+ console.log(chalk.cyan(`\n💡 Source maps will be used automatically when processing errors for release ${data.release}\n`));
139
+
140
+ } catch (error: any) {
141
+ uploadSpinner.fail('Upload failed');
142
+
143
+ if (axios.isAxiosError(error)) {
144
+ if (error.code === 'ENOTFOUND') {
145
+ throw new Error(`Could not connect to ${apiUrl}. Please check your internet connection.`);
146
+ } else if (error.code === 'ECONNREFUSED') {
147
+ throw new Error(`Connection refused to ${apiUrl}. Please check the API URL.`);
148
+ } else if (error.response) {
149
+ // Server responded with error
150
+ const data = error.response.data;
151
+ throw new Error(data.error || `HTTP ${error.response.status}: ${error.response.statusText}`);
152
+ } else if (error.request) {
153
+ // Request made but no response
154
+ throw new Error(`No response from server. Please check your internet connection.`);
155
+ }
156
+ }
157
+
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ function formatFileSize(bytes: number): string {
163
+ if (bytes === 0) return '0 B';
164
+ const k = 1024;
165
+ const sizes = ['B', 'KB', 'MB', 'GB'];
166
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
167
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
168
+ }