@merittdev/horus-lens 0.2.1 → 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 +99 -2
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -2
- package/dist/{chunk-U6GBOUCE.js → chunk-B7QI3T3E.js} +99 -2
- package/dist/chunk-B7QI3T3E.js.map +1 -0
- package/dist/{chunk-SDMTZ5Y6.js → chunk-GQREYSHC.js} +4 -3
- package/dist/{chunk-SDMTZ5Y6.js.map → chunk-GQREYSHC.js.map} +1 -1
- package/dist/cli.cjs +334 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/index.cjs +99 -1
- package/dist/index.cjs.map +1 -1
- package/dist/{index.d-QzZn4EMM.d.cts → index.d-BYG_Mjs-.d.cts} +31 -1
- package/dist/{index.d-Cyvb07sp.d.cts → index.d-DTx4C384.d.cts} +18 -1
- package/dist/{index.d-Cyvb07sp.d.ts → index.d-DTx4C384.d.ts} +18 -1
- package/dist/{index.d-BtnbCqUx.d.ts → index.d-Dyzc1E9T.d.ts} +31 -1
- package/dist/index.d.cts +59 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +3 -1
- package/dist/lens.min.js +24 -24
- package/dist/lens.min.js.map +1 -1
- package/dist/react.cjs +100 -3
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +3 -3
- package/dist/react.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-U6GBOUCE.js.map +0 -1
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
|
package/dist/cli.cjs.map
ADDED
|
@@ -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
|
@@ -43,6 +43,7 @@ __export(src_exports, {
|
|
|
43
43
|
ReportSchema: () => ReportSchema,
|
|
44
44
|
ReportStatusSchema: () => ReportStatusSchema,
|
|
45
45
|
ReportSummarySchema: () => ReportSummarySchema,
|
|
46
|
+
ReportTriggerSchema: () => ReportTriggerSchema,
|
|
46
47
|
SDK_NAME: () => SDK_NAME,
|
|
47
48
|
SDK_VERSION: () => SDK_VERSION,
|
|
48
49
|
TargetElementSchema: () => TargetElementSchema,
|
|
@@ -14721,6 +14722,7 @@ var AssetRefSchema = external_exports.object({
|
|
|
14721
14722
|
byteSize: external_exports.number().optional()
|
|
14722
14723
|
});
|
|
14723
14724
|
var ReportStatusSchema = external_exports.enum(["initiated", "submitted", "triaged", "linked", "resolved"]);
|
|
14725
|
+
var ReportTriggerSchema = external_exports.enum(["user", "api", "error"]);
|
|
14724
14726
|
var ReportMetadataSchema = external_exports.object({
|
|
14725
14727
|
comment: external_exports.string().optional(),
|
|
14726
14728
|
annotations: external_exports.array(AnnotationSchema).default([]),
|
|
@@ -14731,6 +14733,13 @@ var ReportMetadataSchema = external_exports.object({
|
|
|
14731
14733
|
/** Small inline tails so the report list can render without fetching assets. */
|
|
14732
14734
|
consoleTail: external_exports.array(ConsoleEntrySchema).default([]),
|
|
14733
14735
|
networkTail: external_exports.array(NetworkEntrySchema).default([]),
|
|
14736
|
+
/**
|
|
14737
|
+
* How the report was triggered. Optional + non-breaking: this schema is a
|
|
14738
|
+
* plain `z.object` (NOT `.strict()`), so a cloud running an older shared
|
|
14739
|
+
* schema silently strips this unknown key on parse rather than rejecting.
|
|
14740
|
+
* Absent → treat as `user`.
|
|
14741
|
+
*/
|
|
14742
|
+
trigger: ReportTriggerSchema.optional(),
|
|
14734
14743
|
sdk: external_exports.object({ name: external_exports.string(), version: external_exports.string() })
|
|
14735
14744
|
});
|
|
14736
14745
|
var ReportSchema = external_exports.object({
|
|
@@ -29102,7 +29111,7 @@ async function domToBlob(node2, options) {
|
|
|
29102
29111
|
|
|
29103
29112
|
// ../core/dist/index.js
|
|
29104
29113
|
var SDK_NAME = "@merittdev/horus-lens";
|
|
29105
|
-
var SDK_VERSION = true ? "0.
|
|
29114
|
+
var SDK_VERSION = true ? "0.4.0" : "0.0.0-dev";
|
|
29106
29115
|
var DEFAULT_ENDPOINT = "https://api.horus.sh";
|
|
29107
29116
|
var DEFAULT_REPLAY_WINDOW_MS = 15e3;
|
|
29108
29117
|
var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
|
|
@@ -29116,9 +29125,85 @@ var MAX_TARGET_TEXT_CHARS = 200;
|
|
|
29116
29125
|
var SCREENSHOT_TIMEOUT_MS = 2e4;
|
|
29117
29126
|
var SCREENSHOT_MAX_SCALE = 2;
|
|
29118
29127
|
var SCREENSHOT_MAX_PIXELS = 25e5;
|
|
29128
|
+
var AUTO_CAPTURE_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
29129
|
+
var AUTO_CAPTURE_SESSION_CAP = 5;
|
|
29119
29130
|
var MAX_MANUAL_RECORDING_MS = 12e4;
|
|
29120
29131
|
var MAX_MANUAL_RECORDING_EVENTS = 1e4;
|
|
29121
29132
|
var LENS_OVERLAY_HOST_ID = "__lens_root__";
|
|
29133
|
+
function autoCaptureSignature(message) {
|
|
29134
|
+
return message.toLowerCase().replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, "<uuid>").replace(/\d+/g, "<n>").replace(/\s+/g, " ").trim();
|
|
29135
|
+
}
|
|
29136
|
+
function reasonToMessage(reason) {
|
|
29137
|
+
if (reason instanceof Error) return reason.message || String(reason);
|
|
29138
|
+
if (typeof reason === "string") return reason;
|
|
29139
|
+
try {
|
|
29140
|
+
const json2 = JSON.stringify(reason);
|
|
29141
|
+
if (json2 && json2 !== "{}") return json2;
|
|
29142
|
+
} catch {
|
|
29143
|
+
}
|
|
29144
|
+
return String(reason);
|
|
29145
|
+
}
|
|
29146
|
+
function isIgnored2(message, ignore) {
|
|
29147
|
+
for (const entry of ignore) {
|
|
29148
|
+
if (typeof entry === "string") {
|
|
29149
|
+
if (message.includes(entry)) return true;
|
|
29150
|
+
} else if (entry.test(message)) {
|
|
29151
|
+
return true;
|
|
29152
|
+
}
|
|
29153
|
+
}
|
|
29154
|
+
return false;
|
|
29155
|
+
}
|
|
29156
|
+
function startAutoCapture(client, options = {}) {
|
|
29157
|
+
if (typeof window === "undefined") return () => {
|
|
29158
|
+
};
|
|
29159
|
+
const sampleRate = options.sampleRate ?? 1;
|
|
29160
|
+
const ignore = options.ignore ?? [];
|
|
29161
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
29162
|
+
let sessionCount = 0;
|
|
29163
|
+
let submitting = false;
|
|
29164
|
+
const consider = (rawMessage) => {
|
|
29165
|
+
if (submitting) return;
|
|
29166
|
+
if (!rawMessage) return;
|
|
29167
|
+
if (isIgnored2(rawMessage, ignore)) return;
|
|
29168
|
+
if (sampleRate <= 0) return;
|
|
29169
|
+
if (sampleRate < 1 && Math.random() >= sampleRate) return;
|
|
29170
|
+
const signature = autoCaptureSignature(rawMessage);
|
|
29171
|
+
const now = Date.now();
|
|
29172
|
+
const last = lastSeen.get(signature);
|
|
29173
|
+
if (last !== void 0 && now - last < AUTO_CAPTURE_COOLDOWN_MS) return;
|
|
29174
|
+
if (sessionCount >= AUTO_CAPTURE_SESSION_CAP) return;
|
|
29175
|
+
lastSeen.set(signature, now);
|
|
29176
|
+
sessionCount += 1;
|
|
29177
|
+
submitting = true;
|
|
29178
|
+
options.onCapture?.(signature);
|
|
29179
|
+
void Promise.resolve().then(() => client.submitReport({ trigger: "error", include: { screenshot: false } })).catch(() => {
|
|
29180
|
+
}).finally(() => {
|
|
29181
|
+
submitting = false;
|
|
29182
|
+
});
|
|
29183
|
+
};
|
|
29184
|
+
const onError = (event) => {
|
|
29185
|
+
try {
|
|
29186
|
+
const e = event;
|
|
29187
|
+
const error51 = e.error;
|
|
29188
|
+
const message = e.message || (error51 instanceof Error ? error51.message : error51 != null ? String(error51) : "");
|
|
29189
|
+
consider(message);
|
|
29190
|
+
} catch {
|
|
29191
|
+
}
|
|
29192
|
+
};
|
|
29193
|
+
const onRejection = (event) => {
|
|
29194
|
+
try {
|
|
29195
|
+
const e = event;
|
|
29196
|
+
consider(reasonToMessage(e.reason));
|
|
29197
|
+
} catch {
|
|
29198
|
+
}
|
|
29199
|
+
};
|
|
29200
|
+
window.addEventListener("error", onError);
|
|
29201
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
29202
|
+
return () => {
|
|
29203
|
+
window.removeEventListener("error", onError);
|
|
29204
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
29205
|
+
};
|
|
29206
|
+
}
|
|
29122
29207
|
var RingBuffer = class {
|
|
29123
29208
|
constructor(capacity) {
|
|
29124
29209
|
this.capacity = capacity;
|
|
@@ -29964,6 +30049,7 @@ var LensClient = class {
|
|
|
29964
30049
|
replayWindowMs;
|
|
29965
30050
|
debug;
|
|
29966
30051
|
screenshotEnabled;
|
|
30052
|
+
autoCapture;
|
|
29967
30053
|
beforeSend;
|
|
29968
30054
|
app;
|
|
29969
30055
|
consoleBuffer;
|
|
@@ -29978,6 +30064,7 @@ var LensClient = class {
|
|
|
29978
30064
|
this.endpoint = (config2.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
29979
30065
|
this.debug = config2.debug ?? false;
|
|
29980
30066
|
this.screenshotEnabled = config2.screenshot ?? true;
|
|
30067
|
+
this.autoCapture = config2.autoCapture;
|
|
29981
30068
|
this.beforeSend = config2.beforeSend;
|
|
29982
30069
|
this.app = config2.app ?? {};
|
|
29983
30070
|
this.replayWindowMs = config2.replay === false ? null : config2.replay?.windowMs ?? DEFAULT_REPLAY_WINDOW_MS;
|
|
@@ -30006,6 +30093,15 @@ var LensClient = class {
|
|
|
30006
30093
|
this.teardowns.push(installNetwork(this.networkBuffer, this.endpoint));
|
|
30007
30094
|
this.teardowns.push(installErrors(this.errorBuffer));
|
|
30008
30095
|
this.replay?.start();
|
|
30096
|
+
if (this.autoCapture?.errors) {
|
|
30097
|
+
this.teardowns.push(
|
|
30098
|
+
startAutoCapture(this, {
|
|
30099
|
+
sampleRate: this.autoCapture.sampleRate,
|
|
30100
|
+
ignore: this.autoCapture.ignore,
|
|
30101
|
+
onCapture: (signature) => this.warn(`auto-capture: submitting report for "${signature}"`)
|
|
30102
|
+
})
|
|
30103
|
+
);
|
|
30104
|
+
}
|
|
30009
30105
|
}
|
|
30010
30106
|
stop() {
|
|
30011
30107
|
if (!this.started) return;
|
|
@@ -30066,6 +30162,7 @@ var LensClient = class {
|
|
|
30066
30162
|
errors: wantErrors ? errorEntries : [],
|
|
30067
30163
|
consoleTail: wantConsole ? consoleTail : [],
|
|
30068
30164
|
networkTail: wantNetwork ? networkTail : [],
|
|
30165
|
+
trigger: input2.trigger ?? "user",
|
|
30069
30166
|
sdk: { name: SDK_NAME, version: SDK_VERSION }
|
|
30070
30167
|
};
|
|
30071
30168
|
if (input2.comment !== void 0) metadata.comment = input2.comment;
|
|
@@ -30166,6 +30263,7 @@ var LensClient = class {
|
|
|
30166
30263
|
ReportSchema,
|
|
30167
30264
|
ReportStatusSchema,
|
|
30168
30265
|
ReportSummarySchema,
|
|
30266
|
+
ReportTriggerSchema,
|
|
30169
30267
|
SDK_NAME,
|
|
30170
30268
|
SDK_VERSION,
|
|
30171
30269
|
TargetElementSchema,
|