@reliableapp/frontend-cli 1.0.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 (3) hide show
  1. package/README.md +138 -0
  2. package/dist/index.js +271 -0
  3. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @reliableapp/frontend-cli
2
+
3
+ Reliable frontend CLI — upload sourcemaps from your CI / build pipeline so production stack traces resolve to real source code.
4
+
5
+ > Frontend-only. Backend / devops uploads will live in a separate `@reliableapp/backend-cli` package later.
6
+
7
+ ```bash
8
+ npx @reliableapp/frontend-cli sourcemaps upload \
9
+ --dist=./dist \
10
+ --url-prefix=https://app.example.com/
11
+ ```
12
+
13
+ That's it on a supported platform — release SHA auto-detects, project IDs come from env vars.
14
+
15
+ ## Installation
16
+
17
+ No install needed; use via `npx`:
18
+
19
+ ```bash
20
+ npx @reliableapp/frontend-cli sourcemaps upload [options]
21
+ ```
22
+
23
+ Or install globally:
24
+
25
+ ```bash
26
+ npm i -g @reliableapp/frontend-cli
27
+ reliableapp-frontend sourcemaps upload [options]
28
+ ```
29
+
30
+ Requires Node 18+.
31
+
32
+ ## Setup (one-time)
33
+
34
+ In the Reliable dashboard, generate an API token for your frontend project:
35
+
36
+ > Project → Frontend Project → Settings → API Tokens → **Create token**
37
+
38
+ Copy the `rl_fpt_...` value (shown once). Set it as a secret in your CI / deploy platform alongside the project IDs:
39
+
40
+ | Variable | Description |
41
+ |---|---|
42
+ | `RELIABLE_TOKEN` | The `rl_fpt_...` token. **Mark as secret.** |
43
+ | `RELIABLE_PROJECT_ID` | Master project UUID (from dashboard URL) |
44
+ | `RELIABLE_FRONTEND_PROJECT_ID` | Frontend project UUID (from dashboard URL) |
45
+
46
+ ## Usage
47
+
48
+ ### CI step (GitHub Actions, GitLab, CircleCI, Buildkite, Jenkins…)
49
+
50
+ ```yaml
51
+ - name: Upload sourcemaps
52
+ env:
53
+ RELIABLE_TOKEN: ${{ secrets.RELIABLE_TOKEN }}
54
+ RELIABLE_PROJECT_ID: ${{ vars.RELIABLE_PROJECT_ID }}
55
+ RELIABLE_FRONTEND_PROJECT_ID: ${{ vars.RELIABLE_FRONTEND_PROJECT_ID }}
56
+ run: |
57
+ npx @reliableapp/frontend-cli sourcemaps upload \
58
+ --dist=./dist \
59
+ --url-prefix=https://app.example.com/
60
+ ```
61
+
62
+ ### `package.json` script (Vercel, Netlify, Railway, Render, Coolify, Dokploy, anything Nixpacks-based)
63
+
64
+ PaaS platforms that auto-build don't have CI step injection — chain the CLI into your build script instead:
65
+
66
+ ```json
67
+ {
68
+ "scripts": {
69
+ "build": "vite build && reliableapp-frontend sourcemaps upload --dist=./dist --url-prefix=https://app.example.com/"
70
+ }
71
+ }
72
+ ```
73
+
74
+ Set `RELIABLE_TOKEN`, `RELIABLE_PROJECT_ID`, `RELIABLE_FRONTEND_PROJECT_ID` in the platform's environment variables UI. The CLI auto-detects the commit SHA from the platform's own env vars (no `--release` needed).
75
+
76
+ ## Required SDK config
77
+
78
+ In your application code, pass `release` to `init()` so events arrive tagged with the matching build:
79
+
80
+ ```ts
81
+ import { init } from '@reliableapp/react';
82
+
83
+ init({
84
+ publicKey: 'pk_live_...',
85
+ release: process.env.GIT_SHA, // same value the CLI uploads under
86
+ });
87
+ ```
88
+
89
+ Without `release`, events arrive but the resolver has no way to find the right map.
90
+
91
+ ## Options
92
+
93
+ | Flag | Default | Description |
94
+ |---|---|---|
95
+ | `--token <token>` | `$RELIABLE_TOKEN` | API token (`rl_fpt_...`) |
96
+ | `--project <id>` | `$RELIABLE_PROJECT_ID` | Master project UUID |
97
+ | `--frontend-project <id>` | `$RELIABLE_FRONTEND_PROJECT_ID` | Frontend project UUID |
98
+ | `--release <id>` | auto | Release identifier. Auto-detected on supported platforms |
99
+ | `--dist <path>` | required | Local path to the build output folder |
100
+ | `--url-prefix <url>` | required | Browser-visible URL prefix that maps to `--dist` root |
101
+ | `--environment <env>` | `production` | `production` / `staging` / `development` |
102
+ | `--api <url>` | Reliable backend | Override for self-hosted backends |
103
+ | `--concurrency <n>` | `4` | Parallel upload workers |
104
+ | `--force` | — | Bypass the CI safety check (use with care) |
105
+ | `--dry-run` | — | Walk and report what would be uploaded; don't POST |
106
+
107
+ ## How `--url-prefix` works
108
+
109
+ The CLI walks `--dist` for every `*.js.map` file. For each one, the corresponding JS file's URL is computed as:
110
+
111
+ ```
112
+ url-prefix + path-relative-to-dist (with .map stripped)
113
+ ```
114
+
115
+ Example:
116
+
117
+ | `--dist` | file found | `--url-prefix` | computed asset URL |
118
+ |---|---|---|---|
119
+ | `./dist` | `dist/assets/main-abc.js.map` | `https://app.example.com/` | `https://app.example.com/assets/main-abc.js` |
120
+ | `./.next/static` | `.next/static/chunks/page-x.js.map` | `https://app.example.com/_next/static/` | `https://app.example.com/_next/static/chunks/page-x.js` |
121
+
122
+ Match the prefix to whatever URL your CDN actually serves the JS from.
123
+
124
+ ## Auto-detected platforms
125
+
126
+ Release SHA is auto-resolved from these env vars (in order):
127
+
128
+ `GITHUB_SHA` · `CI_COMMIT_SHA` · `VERCEL_GIT_COMMIT_SHA` · `COMMIT_REF` (Netlify) · `RAILWAY_GIT_COMMIT_SHA` · `RENDER_GIT_COMMIT` · `CF_PAGES_COMMIT_SHA` · `SOURCE_COMMIT` (Coolify, Heroku) · `CIRCLE_SHA1` · `BUILDKITE_COMMIT` · `BITBUCKET_COMMIT` · `BUILD_SOURCEVERSION` (Azure) · `GIT_COMMIT` · `COMMIT_SHA`
129
+
130
+ Pass `--release` explicitly if your platform isn't on this list.
131
+
132
+ ## Safety
133
+
134
+ The CLI refuses to upload when no CI env var is detected, to prevent your local builds from polluting the release index. Override with `--force` only when you know what you're doing.
135
+
136
+ ## License
137
+
138
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/sourcemaps-upload.ts
7
+ import { promises as fs3 } from "fs";
8
+ import path3 from "path";
9
+ import { red, green, yellow, cyan, gray, bold } from "kleur/colors";
10
+
11
+ // src/lib/env.ts
12
+ var CI_ENV_VARS = [
13
+ "CI",
14
+ // generic, set by most CI systems
15
+ "GITHUB_ACTIONS",
16
+ "GITLAB_CI",
17
+ "BUILDKITE",
18
+ "CIRCLECI",
19
+ "VERCEL",
20
+ "NETLIFY",
21
+ "CLOUDFLARE_PAGES",
22
+ "BITBUCKET_BUILD_NUMBER",
23
+ "TF_BUILD",
24
+ // Azure Pipelines
25
+ "JENKINS_URL",
26
+ "TEAMCITY_VERSION"
27
+ ];
28
+ function detectCI() {
29
+ for (const k of CI_ENV_VARS) {
30
+ if (process.env[k]) return k;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ // src/lib/walk.ts
36
+ import { promises as fs } from "fs";
37
+ import path from "path";
38
+ async function walkForMaps(root) {
39
+ const out = [];
40
+ await walk(root, root, out);
41
+ return out;
42
+ }
43
+ async function walk(root, dir, out) {
44
+ const entries = await fs.readdir(dir, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ const full = path.join(dir, entry.name);
47
+ if (entry.isDirectory()) {
48
+ if (entry.name === "node_modules") continue;
49
+ await walk(root, full, out);
50
+ } else if (entry.isFile() && entry.name.endsWith(".js.map")) {
51
+ out.push({
52
+ absolutePath: full,
53
+ relativePath: path.relative(root, full)
54
+ });
55
+ }
56
+ }
57
+ }
58
+
59
+ // src/lib/upload.ts
60
+ import { promises as fs2 } from "fs";
61
+ import path2 from "path";
62
+ async function uploadSourcemap(input) {
63
+ const buffer = await fs2.readFile(input.mapPath);
64
+ const blob = new Blob([buffer], { type: "application/json" });
65
+ const form = new FormData();
66
+ form.append("release", input.release);
67
+ form.append("environment", input.environment);
68
+ form.append("asset_url", input.assetUrl);
69
+ form.append("sourcemap", blob, path2.basename(input.mapPath));
70
+ const url = `${input.api.replace(/\/+$/, "")}/v1/projects/${input.masterProjectId}/frontend-projects/${input.frontendProjectUuid}/sourcemaps`;
71
+ const res = await fetch(url, {
72
+ method: "POST",
73
+ headers: { Authorization: `Bearer ${input.token}` },
74
+ body: form
75
+ });
76
+ if (!res.ok) {
77
+ const text = await res.text().catch(() => "");
78
+ const snippet = text.slice(0, 300).replace(/\s+/g, " ").trim();
79
+ throw new Error(`HTTP ${res.status}${snippet ? ` \u2014 ${snippet}` : ""}`);
80
+ }
81
+ }
82
+
83
+ // src/lib/release.ts
84
+ var SHA_ENV_VARS = [
85
+ // GitHub Actions
86
+ "GITHUB_SHA",
87
+ // GitLab CI
88
+ "CI_COMMIT_SHA",
89
+ // Vercel
90
+ "VERCEL_GIT_COMMIT_SHA",
91
+ // Netlify
92
+ "COMMIT_REF",
93
+ // Railway
94
+ "RAILWAY_GIT_COMMIT_SHA",
95
+ // Render
96
+ "RENDER_GIT_COMMIT",
97
+ // Cloudflare Pages
98
+ "CF_PAGES_COMMIT_SHA",
99
+ // Coolify (older), Heroku (Nixpacks-derived)
100
+ "SOURCE_COMMIT",
101
+ // CircleCI
102
+ "CIRCLE_SHA1",
103
+ // Buildkite
104
+ "BUILDKITE_COMMIT",
105
+ // Bitbucket Pipelines
106
+ "BITBUCKET_COMMIT",
107
+ // Azure Pipelines
108
+ "BUILD_SOURCEVERSION",
109
+ // Generic
110
+ "GIT_COMMIT",
111
+ "COMMIT_SHA"
112
+ ];
113
+ function autodetectRelease() {
114
+ for (const k of SHA_ENV_VARS) {
115
+ const v = process.env[k];
116
+ if (v && v.trim()) {
117
+ return { release: v.trim(), source: k };
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ // src/commands/sourcemaps-upload.ts
124
+ async function sourcemapsUpload(opts) {
125
+ const ci = detectCI();
126
+ if (!ci && !opts.force) {
127
+ console.error(red("\u2717 Refusing to upload \u2014 not running in CI."));
128
+ console.error(gray(" No CI env var detected (CI, GITHUB_ACTIONS, VERCEL, etc)."));
129
+ console.error(gray(" Pass --force to override (e.g. for testing locally)."));
130
+ process.exit(2);
131
+ }
132
+ if (ci) console.log(gray(`CI detected via $${ci}`));
133
+ const explicit = opts.release?.trim();
134
+ const auto = explicit ? null : autodetectRelease();
135
+ const release = explicit ?? auto?.release ?? "";
136
+ const releaseSrc = explicit ? "--release flag" : auto ? `$${auto.source}` : "";
137
+ if (!release) {
138
+ console.error(red("\u2717 --release not provided and could not auto-detect."));
139
+ console.error(gray(" Pass --release explicitly, or run on a platform that sets one of:"));
140
+ console.error(gray(" GITHUB_SHA, VERCEL_GIT_COMMIT_SHA, RAILWAY_GIT_COMMIT_SHA,"));
141
+ console.error(gray(" RENDER_GIT_COMMIT, CF_PAGES_COMMIT_SHA, SOURCE_COMMIT, etc."));
142
+ process.exit(2);
143
+ }
144
+ try {
145
+ const stat = await fs3.stat(opts.dist);
146
+ if (!stat.isDirectory()) {
147
+ console.error(red(`\u2717 --dist is not a directory: ${opts.dist}`));
148
+ process.exit(2);
149
+ }
150
+ } catch {
151
+ console.error(red(`\u2717 --dist not found: ${opts.dist}`));
152
+ process.exit(2);
153
+ }
154
+ const maps = await walkForMaps(opts.dist);
155
+ if (maps.length === 0) {
156
+ console.error(yellow(`No .js.map files found under ${opts.dist}`));
157
+ console.error(gray(" Make sure your bundler emits sourcemaps in production."));
158
+ return;
159
+ }
160
+ console.log();
161
+ console.log(bold("Reliable sourcemap upload"));
162
+ console.log(` release ${cyan(release)} ${gray(`(${releaseSrc})`)}`);
163
+ console.log(` environment ${opts.environment}`);
164
+ console.log(` found ${maps.length} maps`);
165
+ console.log(` dist ${opts.dist}`);
166
+ console.log(` url-prefix ${opts.urlPrefix}`);
167
+ console.log(` api ${opts.api}`);
168
+ console.log();
169
+ if (opts.dryRun) {
170
+ for (const m of maps) {
171
+ const assetUrl = computeAssetUrl(opts.urlPrefix, m.relativePath);
172
+ console.log(gray(" [dry]"), assetUrl);
173
+ }
174
+ console.log(gray("\nDry run, no uploads sent."));
175
+ return;
176
+ }
177
+ const concurrency = Math.max(1, parseInt(opts.concurrency, 10) || 4);
178
+ const queue = [...maps];
179
+ const results = { ok: 0, fail: 0 };
180
+ async function worker() {
181
+ for (; ; ) {
182
+ const m = queue.shift();
183
+ if (!m) return;
184
+ const assetUrl = computeAssetUrl(opts.urlPrefix, m.relativePath);
185
+ try {
186
+ await uploadSourcemap({
187
+ api: opts.api,
188
+ token: opts.token,
189
+ masterProjectId: opts.project,
190
+ frontendProjectUuid: opts.frontendProject,
191
+ release,
192
+ environment: opts.environment,
193
+ assetUrl,
194
+ mapPath: m.absolutePath
195
+ });
196
+ results.ok++;
197
+ console.log(green(" \u2713"), assetUrl);
198
+ } catch (err) {
199
+ results.fail++;
200
+ const msg = err instanceof Error ? err.message : String(err);
201
+ console.log(red(" \u2717"), assetUrl, gray(`(${msg})`));
202
+ }
203
+ }
204
+ }
205
+ const workers = Array.from({ length: concurrency }, () => worker());
206
+ await Promise.all(workers);
207
+ console.log();
208
+ console.log(
209
+ `Uploaded ${green(String(results.ok))} \xB7 Failed ${results.fail > 0 ? red(String(results.fail)) : "0"}`
210
+ );
211
+ if (results.fail > 0) process.exit(1);
212
+ }
213
+ function computeAssetUrl(urlPrefix, relativePath) {
214
+ const cleanRel = relativePath.split(path3.sep).join("/");
215
+ const jsRel = cleanRel.replace(/\.map$/, "");
216
+ const prefix = urlPrefix.endsWith("/") ? urlPrefix : urlPrefix + "/";
217
+ const rel = jsRel.startsWith("/") ? jsRel.slice(1) : jsRel;
218
+ return prefix + rel;
219
+ }
220
+
221
+ // src/index.ts
222
+ var program = new Command();
223
+ program.name("reliableapp-frontend").description("Reliable frontend CLI \u2014 sourcemaps, releases, and other build-time uploads").version("1.0.0");
224
+ var sourcemaps = program.command("sourcemaps").description("Sourcemap operations");
225
+ sourcemaps.command("upload").description("Upload sourcemaps for a release to the Reliable backend").requiredOption(
226
+ "--token <token>",
227
+ "API token (rl_fpt_...). Falls back to $RELIABLE_TOKEN.",
228
+ process.env.RELIABLE_TOKEN
229
+ ).requiredOption(
230
+ "--project <id>",
231
+ "Master project UUID. Falls back to $RELIABLE_PROJECT_ID.",
232
+ process.env.RELIABLE_PROJECT_ID
233
+ ).requiredOption(
234
+ "--frontend-project <id>",
235
+ "Frontend project UUID. Falls back to $RELIABLE_FRONTEND_PROJECT_ID.",
236
+ process.env.RELIABLE_FRONTEND_PROJECT_ID
237
+ ).option(
238
+ "--release <id>",
239
+ "Release identifier. Auto-detected from common platform env vars if omitted (GITHUB_SHA, VERCEL_GIT_COMMIT_SHA, RAILWAY_GIT_COMMIT_SHA, RENDER_GIT_COMMIT, etc.)."
240
+ ).requiredOption(
241
+ "--dist <path>",
242
+ "Path to the build output folder containing .js + .js.map files."
243
+ ).requiredOption(
244
+ "--url-prefix <url>",
245
+ "URL prefix that maps to your dist root (e.g. https://app.example.com/)."
246
+ ).option(
247
+ "--environment <env>",
248
+ "production | staging | development",
249
+ "production"
250
+ ).option(
251
+ "--api <url>",
252
+ "API base URL.",
253
+ process.env.RELIABLE_API_URL ?? "https://reliablebackend.ziloris.com/api"
254
+ ).option(
255
+ "--concurrency <n>",
256
+ "How many uploads to run in parallel.",
257
+ "4"
258
+ ).option(
259
+ "--force",
260
+ "Bypass the CI detection check (use with care)."
261
+ ).option(
262
+ "--dry-run",
263
+ "Don't actually upload anything; just report what would be sent."
264
+ ).action(async (opts) => {
265
+ await sourcemapsUpload(opts);
266
+ });
267
+ program.parseAsync(process.argv).catch((err) => {
268
+ const msg = err instanceof Error ? err.message : String(err);
269
+ console.error(msg);
270
+ process.exit(1);
271
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@reliableapp/frontend-cli",
3
+ "version": "1.0.0",
4
+ "description": "Reliable frontend CLI — upload sourcemaps and manage frontend releases from CI. Built by Ziloris · https://ziloris.com",
5
+ "homepage": "https://reliable.ziloris.com/docs",
6
+ "bugs": {
7
+ "url": "https://reliable.ziloris.com/docs"
8
+ },
9
+ "author": {
10
+ "name": "Ziloris",
11
+ "url": "https://ziloris.com"
12
+ },
13
+ "keywords": [
14
+ "reliable",
15
+ "ziloris",
16
+ "sourcemaps",
17
+ "error-tracking",
18
+ "observability",
19
+ "frontend",
20
+ "cli",
21
+ "ci"
22
+ ],
23
+ "type": "module",
24
+ "bin": {
25
+ "reliableapp-frontend": "./dist/index.js"
26
+ },
27
+ "main": "./dist/index.js",
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "dev": "tsup --watch",
34
+ "typecheck": "tsc --noEmit",
35
+ "clean": "rm -rf dist"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "commander": "^12.1.0",
45
+ "kleur": "^4.1.5"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.0.0",
49
+ "tsup": "^8.5.1",
50
+ "typescript": "~5.7.0"
51
+ }
52
+ }