@monocle.sh/cli 0.1.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 ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2024-present, Julien Ripouteau
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # @monocle.sh/cli
2
+
3
+ CLI for Monocle - upload source maps to enable readable stack traces in your error monitoring.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @monocle.sh/cli
9
+ # or use npx
10
+ npx @monocle.sh/cli sourcemaps upload ...
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Upload Source Maps
16
+
17
+ Upload source maps after your build process:
18
+
19
+ ```bash
20
+ monocle-cli sourcemaps upload \
21
+ --api-key=mk_live_xxx \
22
+ --release=v1.2.3 \
23
+ ./dist/**/*.map
24
+ ```
25
+
26
+ With environment variable:
27
+
28
+ ```bash
29
+ MONOCLE_API_KEY=mk_live_xxx monocle-cli sourcemaps upload \
30
+ --release=$(git rev-parse HEAD) \
31
+ ./dist/**/*.map
32
+ ```
33
+
34
+ ### Options
35
+
36
+ | Option | Description | Default |
37
+ | --------------------- | -------------------------------------------------- | ------------------------ |
38
+ | `--api-key <key>` | Monocle API key (or set `MONOCLE_API_KEY` env var) | - |
39
+ | `--release <version>` | Release version (e.g., `v1.2.3` or git SHA) | - |
40
+ | `--url <url>` | Monocle API URL | `https://api.monocle.sh` |
41
+ | `--dry-run` | Show what would be uploaded without uploading | `false` |
42
+
43
+ ### Release Identifier
44
+
45
+ The release identifier links exceptions to their source maps. It must match the `serviceVersion` configured in your Monocle agent:
46
+
47
+ ```typescript
48
+ // config/monocle.ts
49
+ export default defineConfig({
50
+ serviceVersion: env.get('APP_VERSION'), // e.g., "v1.2.3" or git SHA
51
+ })
52
+ ```
53
+
54
+ Best practices:
55
+
56
+ - Use git commit SHA for precise matching: `$(git rev-parse HEAD)`
57
+ - Use semver for human-readable releases: `v1.2.3`
58
+ - Must match exactly between upload and runtime config
59
+
60
+ ## CI/CD Integration
61
+
62
+ ### GitHub Actions
63
+
64
+ ```yaml
65
+ name: Deploy
66
+
67
+ on:
68
+ push:
69
+ branches: [main]
70
+
71
+ jobs:
72
+ deploy:
73
+ runs-on: ubuntu-latest
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+
77
+ - name: Setup Node.js
78
+ uses: actions/setup-node@v4
79
+ with:
80
+ node-version: '20'
81
+
82
+ - name: Install dependencies
83
+ run: npm ci
84
+
85
+ - name: Build
86
+ run: npm run build
87
+
88
+ - name: Upload Source Maps
89
+ run: |
90
+ npx @monocle.sh/cli sourcemaps upload \
91
+ --api-key=${{ secrets.MONOCLE_API_KEY }} \
92
+ --release=${{ github.sha }} \
93
+ ./dist/**/*.map
94
+
95
+ - name: Deploy
96
+ run: # your deploy command
97
+ ```
98
+
99
+ ### GitLab CI
100
+
101
+ ```yaml
102
+ deploy:
103
+ stage: deploy
104
+ script:
105
+ - npm ci
106
+ - npm run build
107
+ - npx @monocle.sh/cli sourcemaps upload
108
+ --api-key=$MONOCLE_API_KEY
109
+ --release=$CI_COMMIT_SHA
110
+ ./dist/**/*.map
111
+ - # your deploy command
112
+ ```
113
+
114
+ ### CircleCI
115
+
116
+ ```yaml
117
+ version: 2.1
118
+
119
+ jobs:
120
+ deploy:
121
+ docker:
122
+ - image: cimg/node:20.0
123
+ steps:
124
+ - checkout
125
+ - run: npm ci
126
+ - run: npm run build
127
+ - run:
128
+ name: Upload Source Maps
129
+ command: |
130
+ npx @monocle.sh/cli sourcemaps upload \
131
+ --api-key=$MONOCLE_API_KEY \
132
+ --release=$CIRCLE_SHA1 \
133
+ ./dist/**/*.map
134
+ - run: # your deploy command
135
+ ```
136
+
137
+ ## AdonisJS Configuration
138
+
139
+ For AdonisJS projects using `tsc` and Node.js (no bundler):
140
+
141
+ ### 1. Enable source maps in tsconfig.json
142
+
143
+ ```json
144
+ {
145
+ "compilerOptions": {
146
+ "sourceMap": true,
147
+ "inlineSources": true,
148
+ "sourceRoot": "/"
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### 2. Configure Monocle agent with release version
154
+
155
+ ```typescript
156
+ // config/monocle.ts
157
+ import env from '#start/env'
158
+
159
+ export default defineConfig({
160
+ serviceVersion: env.get('APP_VERSION'),
161
+ })
162
+ ```
163
+
164
+ ### 3. Set APP_VERSION in your deployment
165
+
166
+ ```bash
167
+ # In your CI/CD or .env
168
+ APP_VERSION=$(git rev-parse HEAD)
169
+ ```
170
+
171
+ ### 4. Upload source maps after build
172
+
173
+ ```yaml
174
+ # GitHub Actions example
175
+ - name: Build
176
+ run: node ace build
177
+
178
+ - name: Upload Source Maps
179
+ run: |
180
+ npx @monocle.sh/cli sourcemaps upload \
181
+ --api-key=${{ secrets.MONOCLE_API_KEY }} \
182
+ --release=${{ github.sha }} \
183
+ ./build/**/*.map
184
+
185
+ - name: Deploy
186
+ run: # rsync, docker push, etc.
187
+ ```
188
+
189
+ The `--release` value must match `APP_VERSION` used at runtime.
190
+
191
+ ### 5. (Optional) Delete source maps before deploy
192
+
193
+ Source maps can expose your source code. Delete them after upload:
194
+
195
+ ```yaml
196
+ - name: Upload Source Maps
197
+ run: npx @monocle.sh/cli sourcemaps upload ...
198
+
199
+ - name: Remove source maps from build
200
+ run: find ./build -name "*.map" -delete
201
+
202
+ - name: Deploy
203
+ run: # deploy without .map files
204
+ ```
205
+
206
+ ## Troubleshooting
207
+
208
+ ### No source map files found
209
+
210
+ - Check that your build generates `.map` files
211
+ - Verify your glob patterns match the output location
212
+ - Try using `--dry-run` to see what would be matched
213
+
214
+ ### Authentication failed
215
+
216
+ - Verify your API key is correct
217
+ - Ensure the API key has write permissions for source maps
218
+ - Check that `MONOCLE_API_KEY` environment variable is set correctly
219
+
220
+ ### Stack traces not resolving
221
+
222
+ - Ensure the `--release` value matches `serviceVersion` in your Monocle agent config
223
+ - Verify source maps were uploaded successfully
224
+ - Check that the source map filenames match the ones referenced in your minified code
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import { i as uploadSourcemaps, t as UploadError } from "./upload-DJfE9Qcx.mjs";
3
+ import { join } from "node:path";
4
+ import { readFileSync, readdirSync, rmSync } from "node:fs";
5
+ import pc from "picocolors";
6
+ import { program } from "commander";
7
+ import { homedir } from "node:os";
8
+ import { exec } from "node:child_process";
9
+ //#region src/cli.ts
10
+ const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
11
+ console.log();
12
+ console.log(pc.magenta(pc.bold(" monocle.sh")), pc.dim(`v${pkg.version}`));
13
+ console.log();
14
+ program.name("monocle").description("Monocle CLI - upload source maps and manage your Monocle projects").version(pkg.version);
15
+ program.command("sourcemaps").description("Manage source maps").command("upload").description("Upload source maps to Monocle").argument("<patterns...>", "Glob patterns for source map files (e.g., ./dist/**/*.map)").option("--api-key <key>", "Monocle API key (or set MONOCLE_API_KEY env var)").option("--release <version>", "Release version (e.g., v1.2.3 or git SHA)").option("--url <url>", "Monocle API URL", "https://api.monocle.sh").option("--dry-run", "Show what would be uploaded without uploading").action(async (patterns, options) => {
16
+ const apiKey = options.apiKey || process.env.MONOCLE_API_KEY;
17
+ if (!apiKey) {
18
+ console.error(pc.red("Error: Missing API key"));
19
+ console.error("Provide --api-key option or set MONOCLE_API_KEY environment variable");
20
+ process.exit(1);
21
+ }
22
+ if (!options.release) {
23
+ console.error(pc.red("Error: Missing --release option"));
24
+ console.error("Provide a release version (e.g., --release=v1.2.3 or --release=$(git rev-parse HEAD))");
25
+ process.exit(1);
26
+ }
27
+ try {
28
+ await uploadSourcemaps({
29
+ patterns,
30
+ apiKey,
31
+ release: options.release,
32
+ apiUrl: options.url,
33
+ dryRun: options.dryRun
34
+ });
35
+ } catch (error) {
36
+ if (error instanceof UploadError) console.error(pc.red(`Error: ${error.message}`));
37
+ else console.error(pc.red("Error: An unexpected error occurred"));
38
+ process.exit(1);
39
+ }
40
+ });
41
+ program.command("dev").description("Start local DevTools server for development observability").option("--port <port>", "Port for the DevTools server", "4200").option("--host <host>", "Host to bind to", "0.0.0.0").option("--open", "Open browser automatically").option("--clean", "Reset all stored data before starting").option("--db-path <path>", "Custom path for DuckDB database").action(async (options) => {
42
+ const port = Number.parseInt(options.port, 10);
43
+ if (options.clean) {
44
+ const configDir = join(homedir(), ".config", "monocle");
45
+ try {
46
+ for (const file of readdirSync(configDir)) if (file.endsWith(".db") || file.endsWith(".db-wal") || file.endsWith(".db-shm")) rmSync(join(configDir, file));
47
+ console.log(pc.green("Data cleared."));
48
+ } catch {}
49
+ }
50
+ let devtools;
51
+ try {
52
+ devtools = await import("@monocle.sh/studio");
53
+ } catch {
54
+ console.error(pc.red("Error: @monocle.sh/studio is not installed"));
55
+ console.error("Install it with: pnpm add -D @monocle.sh/studio");
56
+ process.exit(1);
57
+ }
58
+ try {
59
+ await devtools.startServer({
60
+ port,
61
+ host: options.host,
62
+ dbPath: options.dbPath
63
+ });
64
+ if (options.open) exec(`open http://localhost:${port}`);
65
+ } catch (error) {
66
+ console.error(pc.red("Failed to start DevTools server"));
67
+ if (error instanceof Error) console.error(error.message);
68
+ process.exit(1);
69
+ }
70
+ });
71
+ program.parse();
72
+ //#endregion
73
+ export {};
@@ -0,0 +1,49 @@
1
+ //#region src/upload.d.ts
2
+ interface UploadSourcemapsOptions {
3
+ patterns: string[];
4
+ apiKey: string;
5
+ release: string;
6
+ apiUrl: string;
7
+ dryRun?: boolean;
8
+ }
9
+ interface UploadResult {
10
+ uploaded: Array<{
11
+ filename: string;
12
+ release: string;
13
+ sizeBytes: number;
14
+ }>;
15
+ count: number;
16
+ }
17
+ interface SourcemapFile {
18
+ path: string;
19
+ filename: string;
20
+ content: Buffer;
21
+ size: number;
22
+ }
23
+ /**
24
+ * Upload source maps to Monocle API.
25
+ */
26
+ declare function uploadSourcemaps(options: UploadSourcemapsOptions): Promise<UploadResult>;
27
+ /**
28
+ * Find source map files matching the given glob patterns.
29
+ */
30
+ declare function findSourcemapFiles(patterns: string[]): Promise<string[]>;
31
+ /**
32
+ * Load and validate a source map file. Returns the file with its content.
33
+ */
34
+ declare function loadAndValidateSourcemap(filePath: string): SourcemapFile;
35
+ /**
36
+ * Validate that a file is a valid source map. (Legacy function for tests)
37
+ */
38
+ declare function validateSourcemap(filePath: string): {
39
+ valid: boolean;
40
+ error?: string;
41
+ };
42
+ /**
43
+ * Custom error class for upload errors. Does not expose sensitive data.
44
+ */
45
+ declare class UploadError extends Error {
46
+ constructor(message: string);
47
+ }
48
+ //#endregion
49
+ export { type SourcemapFile, UploadError, type UploadResult, type UploadSourcemapsOptions, findSourcemapFiles, loadAndValidateSourcemap, uploadSourcemaps, validateSourcemap };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as validateSourcemap, i as uploadSourcemaps, n as findSourcemapFiles, r as loadAndValidateSourcemap, t as UploadError } from "./upload-DJfE9Qcx.mjs";
2
+ export { UploadError, findSourcemapFiles, loadAndValidateSourcemap, uploadSourcemaps, validateSourcemap };
@@ -0,0 +1,167 @@
1
+ import * as path from "node:path";
2
+ import * as fs from "node:fs";
3
+ import pc from "picocolors";
4
+ import ora from "ora";
5
+ import ky, { HTTPError, TimeoutError } from "ky";
6
+ import fg from "fast-glob";
7
+ //#region src/upload.ts
8
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
9
+ const UPLOAD_TIMEOUT = 12e4;
10
+ /**
11
+ * Upload source maps to Monocle API.
12
+ */
13
+ async function uploadSourcemaps(options) {
14
+ const { patterns, apiKey, release, apiUrl, dryRun } = options;
15
+ const spinner = ora("Finding source map files...").start();
16
+ const filePaths = await findSourcemapFiles(patterns);
17
+ if (filePaths.length === 0) {
18
+ spinner.fail("No source map files found");
19
+ console.error(pc.yellow(`Searched patterns: ${patterns.join(", ")}`));
20
+ console.error(pc.dim("Make sure the glob patterns match .map files in your build output"));
21
+ throw new UploadError("No source map files found");
22
+ }
23
+ spinner.text = `Validating ${filePaths.length} source map file(s)...`;
24
+ const files = [];
25
+ for (const filePath of filePaths) {
26
+ const file = loadAndValidateSourcemap(filePath);
27
+ files.push(file);
28
+ }
29
+ spinner.succeed(`Found ${files.length} valid source map file(s)`);
30
+ for (const file of files) {
31
+ const sizeKb = (file.size / 1024).toFixed(1);
32
+ console.log(pc.dim(` ${file.path} (${sizeKb} KB)`));
33
+ }
34
+ if (dryRun) {
35
+ console.log(pc.yellow("\nDry run mode - no files uploaded"));
36
+ return {
37
+ uploaded: [],
38
+ count: 0
39
+ };
40
+ }
41
+ spinner.start(`Uploading to ${sanitizeUrl(apiUrl)}...`);
42
+ const result = await uploadFiles({
43
+ files,
44
+ apiKey,
45
+ release,
46
+ apiUrl,
47
+ onProgress: (current, total) => {
48
+ spinner.text = `Uploading file ${current}/${total} to ${sanitizeUrl(apiUrl)}...`;
49
+ }
50
+ });
51
+ spinner.succeed(`Uploaded ${result.count} source map(s) for release ${pc.cyan(release)}`);
52
+ return result;
53
+ }
54
+ /**
55
+ * Find source map files matching the given glob patterns.
56
+ */
57
+ async function findSourcemapFiles(patterns) {
58
+ return (await fg(patterns, {
59
+ onlyFiles: true,
60
+ absolute: true,
61
+ ignore: ["**/node_modules/**"]
62
+ })).filter((file) => file.endsWith(".map"));
63
+ }
64
+ /**
65
+ * Load and validate a source map file. Returns the file with its content.
66
+ */
67
+ function loadAndValidateSourcemap(filePath) {
68
+ const stats = fs.statSync(filePath);
69
+ if (stats.size > MAX_FILE_SIZE) throw new UploadError(`File ${filePath} exceeds 50MB limit (${(stats.size / 1024 / 1024).toFixed(1)}MB)`);
70
+ const content = fs.readFileSync(filePath);
71
+ const filename = path.basename(filePath);
72
+ try {
73
+ const parsed = JSON.parse(content.toString("utf-8"));
74
+ if (typeof parsed.version !== "number") throw new UploadError(`Invalid source map ${filePath}: missing version field`);
75
+ if (!Array.isArray(parsed.sources)) throw new UploadError(`Invalid source map ${filePath}: missing sources array`);
76
+ if (typeof parsed.mappings !== "string") throw new UploadError(`Invalid source map ${filePath}: missing mappings field`);
77
+ } catch (error) {
78
+ if (error instanceof SyntaxError) throw new UploadError(`Invalid source map ${filePath}: invalid JSON`);
79
+ throw error;
80
+ }
81
+ return {
82
+ path: filePath,
83
+ filename,
84
+ content,
85
+ size: stats.size
86
+ };
87
+ }
88
+ /**
89
+ * Validate that a file is a valid source map. (Legacy function for tests)
90
+ */
91
+ function validateSourcemap(filePath) {
92
+ try {
93
+ loadAndValidateSourcemap(filePath);
94
+ return { valid: true };
95
+ } catch (error) {
96
+ return {
97
+ valid: false,
98
+ error: error instanceof Error ? error.message : "Unknown error"
99
+ };
100
+ }
101
+ }
102
+ /**
103
+ * Upload files to the Monocle API.
104
+ */
105
+ async function uploadFiles(options) {
106
+ const { files, apiKey, release, apiUrl, onProgress } = options;
107
+ const formData = new FormData();
108
+ formData.append("release", release);
109
+ let current = 0;
110
+ for (const file of files) {
111
+ current++;
112
+ onProgress?.(current, files.length);
113
+ const blob = new Blob([new Uint8Array(file.content)], { type: "application/json" });
114
+ formData.append("files", blob, file.filename);
115
+ }
116
+ try {
117
+ return (await (await ky.post(`${apiUrl}/sourcemaps`, {
118
+ headers: { "x-api-key": apiKey },
119
+ body: formData,
120
+ timeout: UPLOAD_TIMEOUT
121
+ })).json()).data;
122
+ } catch (error) {
123
+ throw handleUploadError(error);
124
+ }
125
+ }
126
+ /**
127
+ * Custom error class for upload errors. Does not expose sensitive data.
128
+ */
129
+ var UploadError = class extends Error {
130
+ constructor(message) {
131
+ super(message);
132
+ this.name = "UploadError";
133
+ }
134
+ };
135
+ /**
136
+ * Handle upload errors and return a sanitized error.
137
+ */
138
+ function handleUploadError(error) {
139
+ if (error instanceof TimeoutError) return new UploadError("Upload timed out. Check your network connection and try again.");
140
+ if (error instanceof HTTPError) {
141
+ const status = error.response.status;
142
+ if (status === 401) return new UploadError("Authentication failed. Check your API key.");
143
+ if (status === 413) return new UploadError("Upload too large. Maximum total size is 50MB per file.");
144
+ if (status === 429) return new UploadError("Rate limited. Please wait and try again.");
145
+ if (status >= 500) return new UploadError(`Server error (${status}). Please try again later.`);
146
+ return new UploadError(`Upload failed with status ${status}`);
147
+ }
148
+ if (error instanceof Error) {
149
+ if (error.message.includes("ECONNREFUSED")) return new UploadError("Connection refused. Check the API URL and your network.");
150
+ if (error.message.includes("ENOTFOUND")) return new UploadError("DNS resolution failed. Check the API URL.");
151
+ if (error.message.includes("ETIMEDOUT")) return new UploadError("Connection timed out. Check your network connection.");
152
+ }
153
+ return new UploadError("Upload failed. Check your network connection and try again.");
154
+ }
155
+ /**
156
+ * Sanitize URL to remove sensitive parts for display.
157
+ */
158
+ function sanitizeUrl(url) {
159
+ try {
160
+ const parsed = new URL(url);
161
+ return `${parsed.protocol}//${parsed.host}`;
162
+ } catch {
163
+ return url;
164
+ }
165
+ }
166
+ //#endregion
167
+ export { validateSourcemap as a, uploadSourcemaps as i, findSourcemapFiles as n, loadAndValidateSourcemap as r, UploadError as t };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@monocle.sh/cli",
3
+ "version": "0.1.0",
4
+ "description": "Monocle CLI - upload source maps and manage your Monocle projects",
5
+ "keywords": [
6
+ "cli",
7
+ "error-tracking",
8
+ "monocle",
9
+ "observability",
10
+ "sourcemaps"
11
+ ],
12
+ "license": "ISC",
13
+ "author": "Julien Ripouteau <julien@ripouteau.com>",
14
+ "bin": {
15
+ "monocle": "./dist/cli.mjs"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./dist/index.mjs",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "tag": "beta"
28
+ },
29
+ "dependencies": {
30
+ "commander": "^14.0.3",
31
+ "fast-glob": "^3.3.3",
32
+ "ky": "^1.14.3",
33
+ "ora": "^9.3.0",
34
+ "picocolors": "^1.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@adonisjs/tsconfig": "^2.0.0",
38
+ "@japa/assert": "^4.2.0",
39
+ "@japa/file-system": "^3.0.0",
40
+ "@japa/runner": "^5.3.0",
41
+ "@poppinss/ts-exec": "^1.4.4",
42
+ "release-it": "^19.2.4",
43
+ "@monocle.sh/studio": "0.1.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=22.0.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "dev": "tsdown --watch",
51
+ "typecheck": "tsgo --noEmit",
52
+ "test": "node --import @poppinss/ts-exec bin/test.ts",
53
+ "release": "release-it --preRelease=beta"
54
+ }
55
+ }