@merittdev/horus-lens 0.3.0 → 0.4.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/dist/browser.cjs CHANGED
@@ -29082,7 +29082,7 @@ var ApiErrorSchema = external_exports.object({
29082
29082
 
29083
29083
  // ../core/dist/index.js
29084
29084
  var SDK_NAME = "@merittdev/horus-lens";
29085
- var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
29085
+ var SDK_VERSION = true ? "0.4.0" : "0.0.0-dev";
29086
29086
  var DEFAULT_ENDPOINT = "https://api.horus.sh";
29087
29087
  var DEFAULT_REPLAY_WINDOW_MS = 15e3;
29088
29088
  var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
package/dist/browser.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  isGateActive,
8
8
  loaderSnippet,
9
9
  processQueue
10
- } from "./chunk-PNY6BL4C.js";
11
- import "./chunk-LIYEDZV7.js";
10
+ } from "./chunk-GQREYSHC.js";
11
+ import "./chunk-B7QI3T3E.js";
12
12
  export {
13
13
  DEFAULT_CDN_URL,
14
14
  LOADER_SNIPPET,
@@ -29063,7 +29063,7 @@ async function domToBlob(node2, options) {
29063
29063
 
29064
29064
  // ../core/dist/index.js
29065
29065
  var SDK_NAME = "@merittdev/horus-lens";
29066
- var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
29066
+ var SDK_VERSION = true ? "0.4.0" : "0.0.0-dev";
29067
29067
  var DEFAULT_ENDPOINT = "https://api.horus.sh";
29068
29068
  var DEFAULT_REPLAY_WINDOW_MS = 15e3;
29069
29069
  var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
@@ -30241,4 +30241,4 @@ rrweb/dist/rrweb.js:
30241
30241
  PERFORMANCE OF THIS SOFTWARE.
30242
30242
  ***************************************************************************** *)
30243
30243
  */
30244
- //# sourceMappingURL=chunk-LIYEDZV7.js.map
30244
+ //# sourceMappingURL=chunk-B7QI3T3E.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LENS_OVERLAY_HOST_ID,
3
3
  LensClient
4
- } from "./chunk-LIYEDZV7.js";
4
+ } from "./chunk-B7QI3T3E.js";
5
5
 
6
6
  // ../browser/dist/index.js
7
7
  var DEFAULT_PARAM_NAME = "lens";
@@ -2529,4 +2529,4 @@ export {
2529
2529
  boot,
2530
2530
  autoBoot
2531
2531
  };
2532
- //# sourceMappingURL=chunk-PNY6BL4C.js.map
2532
+ //# sourceMappingURL=chunk-GQREYSHC.js.map
package/dist/cli.cjs ADDED
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli.ts
5
+ var import_node_fs2 = require("fs");
6
+
7
+ // src/cli-lib.ts
8
+ var import_node_fs = require("fs");
9
+ var import_node_path = require("path");
10
+ var DEFAULT_ENDPOINT = "https://api.horus.sh";
11
+ var UPLOAD_KEY_ENV = "LENS_UPLOAD_KEY";
12
+ var MAX_FILES = 200;
13
+ var USAGE = `Usage: horus-lens upload-sourcemaps --release <release> [options] <dir...>
14
+
15
+ Upload JavaScript sourcemaps (*.map) for a release to Horus Lens.
16
+
17
+ Arguments:
18
+ <dir...> One or more directories to search recursively for .map files
19
+
20
+ Options:
21
+ --release <release> Release identifier, e.g. a git SHA [required]
22
+ --key <las_...> Per-site upload key (las_-prefixed).
23
+ Falls back to the ${UPLOAD_KEY_ENV} environment variable.
24
+ --endpoint <url> API endpoint (default: ${DEFAULT_ENDPOINT})
25
+ --dry-run List what would be uploaded, then exit without uploading
26
+ -h, --help Show this help
27
+
28
+ Example:
29
+ npx @merittdev/horus-lens upload-sourcemaps --release "$GIT_SHA" dist/`;
30
+ function parseArgs(argv, env = process.env) {
31
+ if (argv.length === 0) return { kind: "help" };
32
+ const [command, ...rest] = argv;
33
+ if (command === "-h" || command === "--help" || command === "help") {
34
+ return { kind: "help" };
35
+ }
36
+ if (command !== "upload-sourcemaps") {
37
+ return { kind: "error", message: `Unknown command: ${command}` };
38
+ }
39
+ let release;
40
+ let key;
41
+ let endpoint = DEFAULT_ENDPOINT;
42
+ let dryRun = false;
43
+ const dirs = [];
44
+ for (let i = 0; i < rest.length; i++) {
45
+ const token = rest[i];
46
+ if (token === "-h" || token === "--help") return { kind: "help" };
47
+ if (token.startsWith("--")) {
48
+ let flag = token;
49
+ let value;
50
+ const eq = token.indexOf("=");
51
+ if (eq !== -1) {
52
+ flag = token.slice(0, eq);
53
+ value = token.slice(eq + 1);
54
+ }
55
+ if (flag === "--dry-run") {
56
+ dryRun = true;
57
+ continue;
58
+ }
59
+ if (flag === "--release" || flag === "--key" || flag === "--endpoint") {
60
+ if (value === void 0) {
61
+ const next = rest[i + 1];
62
+ if (next === void 0 || next.startsWith("-")) {
63
+ return { kind: "error", message: `Option ${flag} requires a value.` };
64
+ }
65
+ value = next;
66
+ i++;
67
+ }
68
+ if (flag === "--release") release = value;
69
+ else if (flag === "--key") key = value;
70
+ else endpoint = value;
71
+ continue;
72
+ }
73
+ return { kind: "error", message: `Unknown option: ${flag}` };
74
+ }
75
+ dirs.push(token);
76
+ }
77
+ const resolvedKey = (key ?? env[UPLOAD_KEY_ENV] ?? "").trim();
78
+ const missing = [];
79
+ if (!release) missing.push("--release <release>");
80
+ if (dirs.length === 0) missing.push("<dir...>");
81
+ if (!dryRun && !resolvedKey) {
82
+ missing.push(`--key <las_...> (or set ${UPLOAD_KEY_ENV})`);
83
+ }
84
+ if (missing.length > 0) {
85
+ return {
86
+ kind: "error",
87
+ message: `Missing required argument(s): ${missing.join(", ")}`
88
+ };
89
+ }
90
+ return {
91
+ kind: "run",
92
+ args: { release, key: resolvedKey, endpoint, dryRun, dirs }
93
+ };
94
+ }
95
+ function discoverSourcemaps(dirs, opts = {}) {
96
+ const maxFiles = opts.maxFiles ?? MAX_FILES;
97
+ const warnings = [];
98
+ const seen = /* @__PURE__ */ new Map();
99
+ const files = [];
100
+ const walk = (dir) => {
101
+ let entries;
102
+ try {
103
+ entries = (0, import_node_fs.readdirSync)(dir, { withFileTypes: true });
104
+ } catch {
105
+ warnings.push(`could not read directory: ${dir}`);
106
+ return;
107
+ }
108
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
109
+ for (const entry of entries) {
110
+ if (entry.name === "node_modules") continue;
111
+ const full = (0, import_node_path.join)(dir, entry.name);
112
+ if (entry.isDirectory()) {
113
+ walk(full);
114
+ continue;
115
+ }
116
+ if (!entry.isFile() || !entry.name.endsWith(".map")) continue;
117
+ const name = entry.name;
118
+ const existing = seen.get(name);
119
+ if (existing !== void 0) {
120
+ warnings.push(
121
+ `duplicate sourcemap basename "${name}": keeping ${existing}, skipping ${full}`
122
+ );
123
+ continue;
124
+ }
125
+ let byteSize;
126
+ try {
127
+ byteSize = (0, import_node_fs.statSync)(full).size;
128
+ } catch {
129
+ warnings.push(`could not stat file: ${full}`);
130
+ continue;
131
+ }
132
+ seen.set(name, full);
133
+ files.push({ name, byteSize, absolutePath: full });
134
+ }
135
+ };
136
+ for (const dir of dirs) walk((0, import_node_path.resolve)(dir));
137
+ let truncated = false;
138
+ let result = files;
139
+ if (files.length > maxFiles) {
140
+ truncated = true;
141
+ warnings.push(
142
+ `found ${files.length} sourcemaps; capping at ${maxFiles} \u2014 the rest will be skipped.`
143
+ );
144
+ result = files.slice(0, maxFiles);
145
+ }
146
+ return { files: result, warnings, truncated };
147
+ }
148
+ function formatBytes(bytes) {
149
+ if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
150
+ if (bytes < 1024) return `${bytes} B`;
151
+ const units = ["KB", "MB", "GB", "TB"];
152
+ let value = bytes / 1024;
153
+ let unitIndex = 0;
154
+ while (value >= 1024 && unitIndex < units.length - 1) {
155
+ value /= 1024;
156
+ unitIndex++;
157
+ }
158
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
159
+ }
160
+
161
+ // src/cli.ts
162
+ var PUT_CONCURRENCY = 4;
163
+ async function postJson(url, key, body) {
164
+ try {
165
+ const res = await fetch(url, {
166
+ method: "POST",
167
+ headers: {
168
+ authorization: `Bearer ${key}`,
169
+ "content-type": "application/json"
170
+ },
171
+ body: JSON.stringify(body)
172
+ });
173
+ return { ok: res.ok, status: res.status, body: await res.text() };
174
+ } catch (err) {
175
+ return { ok: false, status: 0, body: `network error: ${err.message}` };
176
+ }
177
+ }
178
+ async function putFile(file, target) {
179
+ const bytes = (0, import_node_fs2.readFileSync)(file.absolutePath);
180
+ const method = target.method ?? "PUT";
181
+ const headers = target.headers ?? { "content-type": "application/json" };
182
+ let lastError = "";
183
+ for (let attempt = 0; attempt < 2; attempt++) {
184
+ try {
185
+ const res = await fetch(target.url, { method, headers, body: bytes });
186
+ if (res.ok) return;
187
+ lastError = `HTTP ${res.status}`;
188
+ } catch (err) {
189
+ lastError = err.message;
190
+ }
191
+ }
192
+ throw new Error(lastError || "upload failed");
193
+ }
194
+ async function runPool(items, concurrency, worker) {
195
+ const failures = [];
196
+ let index = 0;
197
+ const next = async () => {
198
+ while (index < items.length) {
199
+ const item = items[index++];
200
+ try {
201
+ await worker(item);
202
+ } catch (err) {
203
+ failures.push({ item, error: err.message });
204
+ }
205
+ }
206
+ };
207
+ const runners = [];
208
+ for (let i = 0; i < Math.min(concurrency, items.length); i++) runners.push(next());
209
+ await Promise.all(runners);
210
+ return failures;
211
+ }
212
+ async function main(argv) {
213
+ const parsed = parseArgs(argv);
214
+ if (parsed.kind === "help") {
215
+ process.stdout.write(`${USAGE}
216
+ `);
217
+ return 0;
218
+ }
219
+ if (parsed.kind === "error") {
220
+ process.stderr.write(`error: ${parsed.message}
221
+
222
+ ${USAGE}
223
+ `);
224
+ return 1;
225
+ }
226
+ const { release, key, endpoint, dryRun, dirs } = parsed.args;
227
+ const { files, warnings } = discoverSourcemaps(dirs);
228
+ for (const w of warnings) process.stderr.write(`warning: ${w}
229
+ `);
230
+ if (files.length === 0) {
231
+ process.stderr.write(
232
+ `warning: no .map files found in: ${dirs.join(", ")} \u2014 nothing to upload.
233
+ `
234
+ );
235
+ return 0;
236
+ }
237
+ const totalBytes = files.reduce((sum, f) => sum + f.byteSize, 0);
238
+ if (dryRun) {
239
+ process.stdout.write(
240
+ `Dry run \u2014 would upload ${files.length} sourcemap(s) for release ${release} (${formatBytes(totalBytes)}):
241
+ `
242
+ );
243
+ for (const f of files) {
244
+ process.stdout.write(` ${f.name} (${formatBytes(f.byteSize)})
245
+ `);
246
+ }
247
+ process.stdout.write("Nothing uploaded (--dry-run).\n");
248
+ return 0;
249
+ }
250
+ const base = endpoint.replace(/\/+$/, "");
251
+ process.stdout.write(
252
+ `Uploading ${files.length} sourcemap(s) for release ${release} to ${base} ...
253
+ `
254
+ );
255
+ const initiate = await postJson(`${base}/v1/lens/sourcemaps/initiate`, key, {
256
+ release,
257
+ files: files.map((f) => ({ name: f.name, byteSize: f.byteSize }))
258
+ });
259
+ if (!initiate.ok) {
260
+ process.stderr.write(
261
+ `error: initiate failed (HTTP ${initiate.status})
262
+ ${initiate.body}
263
+ `
264
+ );
265
+ return 1;
266
+ }
267
+ let initiateData;
268
+ try {
269
+ initiateData = JSON.parse(initiate.body);
270
+ } catch {
271
+ process.stderr.write(
272
+ `error: could not parse initiate response as JSON
273
+ ${initiate.body}
274
+ `
275
+ );
276
+ return 1;
277
+ }
278
+ const byName = new Map(files.map((f) => [f.name, f]));
279
+ const jobs = [];
280
+ for (const target of initiateData.uploads ?? []) {
281
+ const file = byName.get(target.name);
282
+ if (!file) {
283
+ process.stderr.write(
284
+ `warning: server returned an upload target for unknown file "${target.name}" \u2014 skipping
285
+ `
286
+ );
287
+ continue;
288
+ }
289
+ jobs.push({ file, target });
290
+ }
291
+ if (jobs.length === 0) {
292
+ process.stderr.write("error: initiate returned no upload targets.\n");
293
+ return 1;
294
+ }
295
+ const failures = await runPool(jobs, PUT_CONCURRENCY, async ({ file, target }) => {
296
+ await putFile(file, target);
297
+ process.stdout.write(` \u2713 ${file.name}
298
+ `);
299
+ });
300
+ if (failures.length > 0) {
301
+ for (const { item, error } of failures) {
302
+ process.stderr.write(`error: failed to upload ${item.file.name}: ${error}
303
+ `);
304
+ }
305
+ return 1;
306
+ }
307
+ const uploaded = jobs.map((j) => j.file);
308
+ const finalize = await postJson(`${base}/v1/lens/sourcemaps/finalize`, key, {
309
+ release,
310
+ files: uploaded.map((f) => ({ name: f.name, byteSize: f.byteSize }))
311
+ });
312
+ if (!finalize.ok) {
313
+ process.stderr.write(
314
+ `error: finalize failed (HTTP ${finalize.status})
315
+ ${finalize.body}
316
+ `
317
+ );
318
+ return 1;
319
+ }
320
+ const uploadedBytes = uploaded.reduce((sum, f) => sum + f.byteSize, 0);
321
+ process.stdout.write(
322
+ `\u2713 uploaded ${uploaded.length} sourcemaps for release ${release} (${formatBytes(uploadedBytes)})
323
+ `
324
+ );
325
+ return 0;
326
+ }
327
+ main(process.argv.slice(2)).then((code) => {
328
+ process.exitCode = code;
329
+ }).catch((err) => {
330
+ process.stderr.write(`error: ${err.message}
331
+ `);
332
+ process.exitCode = 1;
333
+ });
334
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/cli-lib.ts"],"sourcesContent":["/**\n * `horus-lens upload-sourcemaps` — CI-friendly sourcemap uploader.\n *\n * npx @merittdev/horus-lens upload-sourcemaps --release <sha> dist/\n *\n * Node >= 18: uses native `fetch` and `node:fs` recursion — no runtime deps.\n * Pure logic (arg parsing, discovery, byte formatting) lives in `./cli-lib`.\n */\nimport { readFileSync } from 'node:fs';\n\nimport {\n discoverSourcemaps,\n formatBytes,\n parseArgs,\n USAGE,\n type DiscoveredFile,\n} from './cli-lib.js';\n\nconst PUT_CONCURRENCY = 4;\n\ninterface UploadTarget {\n name: string;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\ninterface InitiateResponse {\n uploads?: UploadTarget[];\n}\n\ninterface HttpResult {\n ok: boolean;\n status: number;\n body: string;\n}\n\nasync function postJson(url: string, key: string, body: unknown): Promise<HttpResult> {\n try {\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${key}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n return { ok: res.ok, status: res.status, body: await res.text() };\n } catch (err) {\n return { ok: false, status: 0, body: `network error: ${(err as Error).message}` };\n }\n}\n\n/** PUT one file's raw bytes to its presigned URL, retrying once on failure. */\nasync function putFile(file: DiscoveredFile, target: UploadTarget): Promise<void> {\n const bytes = readFileSync(file.absolutePath);\n const method = target.method ?? 'PUT';\n const headers = target.headers ?? { 'content-type': 'application/json' };\n let lastError = '';\n\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const res = await fetch(target.url, { method, headers, body: bytes });\n if (res.ok) return;\n lastError = `HTTP ${res.status}`;\n } catch (err) {\n lastError = (err as Error).message;\n }\n }\n throw new Error(lastError || 'upload failed');\n}\n\n/** Run `worker` over `items` with bounded concurrency; collect failures. */\nasync function runPool<T>(\n items: T[],\n concurrency: number,\n worker: (item: T) => Promise<void>,\n): Promise<{ item: T; error: string }[]> {\n const failures: { item: T; error: string }[] = [];\n let index = 0;\n\n const next = async (): Promise<void> => {\n while (index < items.length) {\n const item = items[index++]!;\n try {\n await worker(item);\n } catch (err) {\n failures.push({ item, error: (err as Error).message });\n }\n }\n };\n\n const runners: Promise<void>[] = [];\n for (let i = 0; i < Math.min(concurrency, items.length); i++) runners.push(next());\n await Promise.all(runners);\n return failures;\n}\n\nasync function main(argv: string[]): Promise<number> {\n const parsed = parseArgs(argv);\n if (parsed.kind === 'help') {\n process.stdout.write(`${USAGE}\\n`);\n return 0;\n }\n if (parsed.kind === 'error') {\n process.stderr.write(`error: ${parsed.message}\\n\\n${USAGE}\\n`);\n return 1;\n }\n\n const { release, key, endpoint, dryRun, dirs } = parsed.args;\n\n const { files, warnings } = discoverSourcemaps(dirs);\n for (const w of warnings) process.stderr.write(`warning: ${w}\\n`);\n\n // Zero files is not a CI failure — a build may legitimately emit no maps.\n if (files.length === 0) {\n process.stderr.write(\n `warning: no .map files found in: ${dirs.join(', ')} — nothing to upload.\\n`,\n );\n return 0;\n }\n\n const totalBytes = files.reduce((sum, f) => sum + f.byteSize, 0);\n\n if (dryRun) {\n process.stdout.write(\n `Dry run — would upload ${files.length} sourcemap(s) for release ${release} (${formatBytes(totalBytes)}):\\n`,\n );\n for (const f of files) {\n process.stdout.write(` ${f.name} (${formatBytes(f.byteSize)})\\n`);\n }\n process.stdout.write('Nothing uploaded (--dry-run).\\n');\n return 0;\n }\n\n const base = endpoint.replace(/\\/+$/, '');\n process.stdout.write(\n `Uploading ${files.length} sourcemap(s) for release ${release} to ${base} ...\\n`,\n );\n\n // 1. initiate — exchange file list for presigned upload targets.\n const initiate = await postJson(`${base}/v1/lens/sourcemaps/initiate`, key, {\n release,\n files: files.map((f) => ({ name: f.name, byteSize: f.byteSize })),\n });\n if (!initiate.ok) {\n process.stderr.write(\n `error: initiate failed (HTTP ${initiate.status})\\n${initiate.body}\\n`,\n );\n return 1;\n }\n\n let initiateData: InitiateResponse;\n try {\n initiateData = JSON.parse(initiate.body) as InitiateResponse;\n } catch {\n process.stderr.write(\n `error: could not parse initiate response as JSON\\n${initiate.body}\\n`,\n );\n return 1;\n }\n\n const byName = new Map(files.map((f) => [f.name, f]));\n const jobs: { file: DiscoveredFile; target: UploadTarget }[] = [];\n for (const target of initiateData.uploads ?? []) {\n const file = byName.get(target.name);\n if (!file) {\n process.stderr.write(\n `warning: server returned an upload target for unknown file \"${target.name}\" — skipping\\n`,\n );\n continue;\n }\n jobs.push({ file, target });\n }\n if (jobs.length === 0) {\n process.stderr.write('error: initiate returned no upload targets.\\n');\n return 1;\n }\n\n // 2. PUT each file (concurrency-limited, one retry each).\n const failures = await runPool(jobs, PUT_CONCURRENCY, async ({ file, target }) => {\n await putFile(file, target);\n process.stdout.write(` ✓ ${file.name}\\n`);\n });\n if (failures.length > 0) {\n for (const { item, error } of failures) {\n process.stderr.write(`error: failed to upload ${item.file.name}: ${error}\\n`);\n }\n return 1;\n }\n\n // 3. finalize — commit the release with the files we actually uploaded.\n const uploaded = jobs.map((j) => j.file);\n const finalize = await postJson(`${base}/v1/lens/sourcemaps/finalize`, key, {\n release,\n files: uploaded.map((f) => ({ name: f.name, byteSize: f.byteSize })),\n });\n if (!finalize.ok) {\n process.stderr.write(\n `error: finalize failed (HTTP ${finalize.status})\\n${finalize.body}\\n`,\n );\n return 1;\n }\n\n const uploadedBytes = uploaded.reduce((sum, f) => sum + f.byteSize, 0);\n process.stdout.write(\n `✓ uploaded ${uploaded.length} sourcemaps for release ${release} (${formatBytes(uploadedBytes)})\\n`,\n );\n return 0;\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((err: unknown) => {\n process.stderr.write(`error: ${(err as Error).message}\\n`);\n process.exitCode = 1;\n });\n","/**\n * Pure, side-effect-light logic for the `upload-sourcemaps` CLI: argument\n * parsing, filesystem discovery of `*.map` files, and human byte formatting.\n *\n * Kept separate from `cli.ts` (which owns process/network I/O) so this logic\n * is unit-testable without spawning a subprocess or hitting the network.\n */\nimport { readdirSync, statSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\n\n/**\n * Default Horus Lens API endpoint. Mirrors `DEFAULT_ENDPOINT` in\n * `@horus-lens/core` (packages/core/src/constants.ts). Duplicated as a literal\n * rather than imported because core's only export entrypoint pulls in the full\n * browser SDK (rrweb / DOM globals), which has no place in a Node CLI bundle.\n * Keep this value in sync with core.\n */\nexport const DEFAULT_ENDPOINT = 'https://api.horus.sh';\n\n/** Environment variable consulted for the upload key when `--key` is omitted. */\nexport const UPLOAD_KEY_ENV = 'LENS_UPLOAD_KEY';\n\n/** Hard cap on the number of sourcemaps uploaded per invocation. */\nexport const MAX_FILES = 200;\n\nexport const USAGE = `Usage: horus-lens upload-sourcemaps --release <release> [options] <dir...>\n\nUpload JavaScript sourcemaps (*.map) for a release to Horus Lens.\n\nArguments:\n <dir...> One or more directories to search recursively for .map files\n\nOptions:\n --release <release> Release identifier, e.g. a git SHA [required]\n --key <las_...> Per-site upload key (las_-prefixed).\n Falls back to the ${UPLOAD_KEY_ENV} environment variable.\n --endpoint <url> API endpoint (default: ${DEFAULT_ENDPOINT})\n --dry-run List what would be uploaded, then exit without uploading\n -h, --help Show this help\n\nExample:\n npx @merittdev/horus-lens upload-sourcemaps --release \"$GIT_SHA\" dist/`;\n\nexport interface ParsedArgs {\n release: string;\n /** Resolved upload key (from --key or the env fallback). May be empty for --dry-run. */\n key: string;\n endpoint: string;\n dryRun: boolean;\n dirs: string[];\n}\n\nexport type ParseResult =\n | { kind: 'run'; args: ParsedArgs }\n | { kind: 'help' }\n | { kind: 'error'; message: string };\n\n/**\n * Parse the CLI argv (everything after the binary name) into a structured\n * result. Never throws — validation failures become `{ kind: 'error' }`.\n */\nexport function parseArgs(\n argv: string[],\n env: Record<string, string | undefined> = process.env,\n): ParseResult {\n if (argv.length === 0) return { kind: 'help' };\n\n const [command, ...rest] = argv;\n if (command === '-h' || command === '--help' || command === 'help') {\n return { kind: 'help' };\n }\n if (command !== 'upload-sourcemaps') {\n return { kind: 'error', message: `Unknown command: ${command}` };\n }\n\n let release: string | undefined;\n let key: string | undefined;\n let endpoint = DEFAULT_ENDPOINT;\n let dryRun = false;\n const dirs: string[] = [];\n\n for (let i = 0; i < rest.length; i++) {\n const token = rest[i]!;\n\n if (token === '-h' || token === '--help') return { kind: 'help' };\n\n if (token.startsWith('--')) {\n let flag = token;\n let value: string | undefined;\n const eq = token.indexOf('=');\n if (eq !== -1) {\n flag = token.slice(0, eq);\n value = token.slice(eq + 1);\n }\n\n if (flag === '--dry-run') {\n dryRun = true;\n continue;\n }\n\n if (flag === '--release' || flag === '--key' || flag === '--endpoint') {\n if (value === undefined) {\n const next = rest[i + 1];\n if (next === undefined || next.startsWith('-')) {\n return { kind: 'error', message: `Option ${flag} requires a value.` };\n }\n value = next;\n i++;\n }\n if (flag === '--release') release = value;\n else if (flag === '--key') key = value;\n else endpoint = value;\n continue;\n }\n\n return { kind: 'error', message: `Unknown option: ${flag}` };\n }\n\n dirs.push(token);\n }\n\n const resolvedKey = (key ?? env[UPLOAD_KEY_ENV] ?? '').trim();\n\n const missing: string[] = [];\n if (!release) missing.push('--release <release>');\n if (dirs.length === 0) missing.push('<dir...>');\n // The key is only needed to actually talk to the server; --dry-run is offline.\n if (!dryRun && !resolvedKey) {\n missing.push(`--key <las_...> (or set ${UPLOAD_KEY_ENV})`);\n }\n\n if (missing.length > 0) {\n return {\n kind: 'error',\n message: `Missing required argument(s): ${missing.join(', ')}`,\n };\n }\n\n return {\n kind: 'run',\n args: { release: release!, key: resolvedKey, endpoint, dryRun, dirs },\n };\n}\n\nexport interface DiscoveredFile {\n /** Basename, e.g. `main.js.map`. Sent to the server as the file identity. */\n name: string;\n byteSize: number;\n absolutePath: string;\n}\n\nexport interface DiscoveryResult {\n files: DiscoveredFile[];\n warnings: string[];\n truncated: boolean;\n}\n\n/**\n * Recursively discover `*.map` files under the given directories.\n *\n * - `node_modules` directories are skipped entirely.\n * - Duplicate basenames (across or within dirs) keep the first seen and warn.\n * - Results are capped at `maxFiles` (warns + truncates rather than failing).\n * - Unreadable dirs / unstattable files warn and are skipped.\n */\nexport function discoverSourcemaps(\n dirs: string[],\n opts: { maxFiles?: number } = {},\n): DiscoveryResult {\n const maxFiles = opts.maxFiles ?? MAX_FILES;\n const warnings: string[] = [];\n const seen = new Map<string, string>(); // basename -> first absolute path\n const files: DiscoveredFile[] = [];\n\n const walk = (dir: string): void => {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n warnings.push(`could not read directory: ${dir}`);\n return;\n }\n // Sort for deterministic ordering so \"keep the first\" is stable.\n entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n\n for (const entry of entries) {\n if (entry.name === 'node_modules') continue;\n const full = join(dir, entry.name);\n\n if (entry.isDirectory()) {\n walk(full);\n continue;\n }\n if (!entry.isFile() || !entry.name.endsWith('.map')) continue;\n\n const name = entry.name;\n const existing = seen.get(name);\n if (existing !== undefined) {\n warnings.push(\n `duplicate sourcemap basename \"${name}\": keeping ${existing}, skipping ${full}`,\n );\n continue;\n }\n\n let byteSize: number;\n try {\n byteSize = statSync(full).size;\n } catch {\n warnings.push(`could not stat file: ${full}`);\n continue;\n }\n\n seen.set(name, full);\n files.push({ name, byteSize, absolutePath: full });\n }\n };\n\n for (const dir of dirs) walk(resolve(dir));\n\n let truncated = false;\n let result = files;\n if (files.length > maxFiles) {\n truncated = true;\n warnings.push(\n `found ${files.length} sourcemaps; capping at ${maxFiles} — the rest will be skipped.`,\n );\n result = files.slice(0, maxFiles);\n }\n\n return { files: result, warnings, truncated };\n}\n\n/** Format a byte count as a short human string, e.g. `3.4 MB`, `512 B`. */\nexport function formatBytes(bytes: number): string {\n if (!Number.isFinite(bytes) || bytes < 0) return '0 B';\n if (bytes < 1024) return `${bytes} B`;\n const units = ['KB', 'MB', 'GB', 'TB'];\n let value = bytes / 1024;\n let unitIndex = 0;\n while (value >= 1024 && unitIndex < units.length - 1) {\n value /= 1024;\n unitIndex++;\n }\n return `${value.toFixed(1)} ${units[unitIndex]!}`;\n}\n"],"mappings":"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","names":["import_node_fs"]}
package/dist/index.cjs CHANGED
@@ -29111,7 +29111,7 @@ async function domToBlob(node2, options) {
29111
29111
 
29112
29112
  // ../core/dist/index.js
29113
29113
  var SDK_NAME = "@merittdev/horus-lens";
29114
- var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
29114
+ var SDK_VERSION = true ? "0.4.0" : "0.0.0-dev";
29115
29115
  var DEFAULT_ENDPOINT = "https://api.horus.sh";
29116
29116
  var DEFAULT_REPLAY_WINDOW_MS = 15e3;
29117
29117
  var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  TargetElementSchema,
29
29
  isAccessId,
30
30
  isAccessSecret
31
- } from "./chunk-LIYEDZV7.js";
31
+ } from "./chunk-B7QI3T3E.js";
32
32
  export {
33
33
  ACCESS_ID_PREFIX,
34
34
  ACCESS_SECRET_PREFIX,