@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,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
|
+
}
|