@ulpi/browse 1.4.0 → 1.4.4
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/README.md +5 -1
- package/dist/browse.cjs +663 -104
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -327,6 +327,10 @@ browse perf-audit [url] # Full performance audit with actionabl
|
|
|
327
327
|
browse perf-audit [url] --no-coverage # Skip JS/CSS coverage (faster)
|
|
328
328
|
browse perf-audit [url] --no-detect # Skip stack detection
|
|
329
329
|
browse perf-audit [url] --json # Structured JSON output
|
|
330
|
+
browse perf-audit save [name] # Save audit report for later comparison
|
|
331
|
+
browse perf-audit compare <base> [curr] # Compare saved baseline vs current or saved audit
|
|
332
|
+
browse perf-audit list # List saved audit reports
|
|
333
|
+
browse perf-audit delete <name> # Delete a saved audit
|
|
330
334
|
browse detect # Tech stack fingerprint (frameworks, SaaS, CDN, infra)
|
|
331
335
|
browse coverage start # Start JS/CSS code coverage collection
|
|
332
336
|
browse coverage stop # Stop and report per-file used/unused bytes
|
|
@@ -631,7 +635,7 @@ Use browse to test the login flow. Run browse --help to see available commands.
|
|
|
631
635
|
|
|
632
636
|
## MCP Server Mode
|
|
633
637
|
|
|
634
|
-
Run browse as an [MCP](https://modelcontextprotocol.io/) server for editors that support the Model Context Protocol.
|
|
638
|
+
Run browse as an [MCP](https://modelcontextprotocol.io/) server for editors that support the Model Context Protocol. All 99 CLI commands are available as MCP tools, including `perf-audit`, `detect`, `coverage`, and `initscript`.
|
|
635
639
|
|
|
636
640
|
```bash
|
|
637
641
|
browse --mcp
|
package/dist/browse.cjs
CHANGED
|
@@ -828,7 +828,8 @@ var require_package = __commonJS({
|
|
|
828
828
|
"package.json"(exports2, module2) {
|
|
829
829
|
module2.exports = {
|
|
830
830
|
name: "@ulpi/browse",
|
|
831
|
-
version: "1.4.
|
|
831
|
+
version: "1.4.3",
|
|
832
|
+
homepage: "https://browse.ulpi.io",
|
|
832
833
|
repository: {
|
|
833
834
|
type: "git",
|
|
834
835
|
url: "https://github.com/ulpi-io/browse"
|
|
@@ -1372,9 +1373,9 @@ var init_browser_manager = __esm({
|
|
|
1372
1373
|
});
|
|
1373
1374
|
} catch (err) {
|
|
1374
1375
|
if (err.message?.includes("Failed to launch") || err.message?.includes("Target closed")) {
|
|
1375
|
-
const
|
|
1376
|
+
const fs19 = await import("fs");
|
|
1376
1377
|
console.error(`[browse] Profile directory corrupted, recreating: ${profileDir}`);
|
|
1377
|
-
|
|
1378
|
+
fs19.rmSync(profileDir, { recursive: true, force: true });
|
|
1378
1379
|
context = await import_playwright.chromium.launchPersistentContext(profileDir, {
|
|
1379
1380
|
headless: process.env.BROWSE_HEADED !== "1",
|
|
1380
1381
|
viewport: { width: 1920, height: 1080 }
|
|
@@ -2161,8 +2162,8 @@ var init_browser_manager = __esm({
|
|
|
2161
2162
|
// ─── Video Recording ──────────────────────────────────────
|
|
2162
2163
|
async startVideoRecording(dir) {
|
|
2163
2164
|
if (this.videoRecording) throw new Error("Video recording already active");
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2165
|
+
const fs19 = await import("fs");
|
|
2166
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
2166
2167
|
this.videoRecording = { dir, startedAt: Date.now() };
|
|
2167
2168
|
const viewport = this.currentDevice?.viewport || { width: 1920, height: 1080 };
|
|
2168
2169
|
await this.recreateContext({
|
|
@@ -4257,11 +4258,11 @@ var init_lib = __esm({
|
|
|
4257
4258
|
}
|
|
4258
4259
|
}
|
|
4259
4260
|
},
|
|
4260
|
-
addToPath: function addToPath(
|
|
4261
|
-
var last =
|
|
4261
|
+
addToPath: function addToPath(path16, added, removed, oldPosInc, options) {
|
|
4262
|
+
var last = path16.lastComponent;
|
|
4262
4263
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
4263
4264
|
return {
|
|
4264
|
-
oldPos:
|
|
4265
|
+
oldPos: path16.oldPos + oldPosInc,
|
|
4265
4266
|
lastComponent: {
|
|
4266
4267
|
count: last.count + 1,
|
|
4267
4268
|
added,
|
|
@@ -4271,7 +4272,7 @@ var init_lib = __esm({
|
|
|
4271
4272
|
};
|
|
4272
4273
|
} else {
|
|
4273
4274
|
return {
|
|
4274
|
-
oldPos:
|
|
4275
|
+
oldPos: path16.oldPos + oldPosInc,
|
|
4275
4276
|
lastComponent: {
|
|
4276
4277
|
count: 1,
|
|
4277
4278
|
added,
|
|
@@ -5474,15 +5475,15 @@ async function getSuspense(bm, page) {
|
|
|
5474
5475
|
if (!roots || roots.size === 0) return "No fiber roots found";
|
|
5475
5476
|
const root = roots.values().next().value;
|
|
5476
5477
|
const boundaries = [];
|
|
5477
|
-
const walk = (fiber,
|
|
5478
|
+
const walk = (fiber, path16) => {
|
|
5478
5479
|
if (!fiber) return;
|
|
5479
5480
|
if (fiber.tag === 13) {
|
|
5480
5481
|
const resolved = fiber.memoizedState === null;
|
|
5481
|
-
const parent =
|
|
5482
|
+
const parent = path16.length > 0 ? path16[path16.length - 1] : "root";
|
|
5482
5483
|
boundaries.push(`Suspense in ${parent} \u2014 ${resolved ? "resolved (children visible)" : "pending (showing fallback)"}`);
|
|
5483
5484
|
}
|
|
5484
5485
|
const name = fiber.type?.displayName || fiber.type?.name || null;
|
|
5485
|
-
const newPath = name ? [...
|
|
5486
|
+
const newPath = name ? [...path16, name] : path16;
|
|
5486
5487
|
let child = fiber.child;
|
|
5487
5488
|
while (child) {
|
|
5488
5489
|
walk(child, newPath);
|
|
@@ -12649,6 +12650,7 @@ var init_recommendations = __esm({
|
|
|
12649
12650
|
// src/perf-audit/formatter.ts
|
|
12650
12651
|
var formatter_exports = {};
|
|
12651
12652
|
__export(formatter_exports, {
|
|
12653
|
+
formatAuditDiff: () => formatAuditDiff,
|
|
12652
12654
|
formatPerfAudit: () => formatPerfAudit
|
|
12653
12655
|
});
|
|
12654
12656
|
function ratingFor(metric, value) {
|
|
@@ -13062,7 +13064,84 @@ function formatTopRecommendations(report) {
|
|
|
13062
13064
|
}
|
|
13063
13065
|
return lines.join("\n");
|
|
13064
13066
|
}
|
|
13065
|
-
|
|
13067
|
+
function formatAuditDiff(diff2, json = false) {
|
|
13068
|
+
if (json) return JSON.stringify(diff2, null, 2);
|
|
13069
|
+
return formatTextDiff(diff2);
|
|
13070
|
+
}
|
|
13071
|
+
function formatTextDiff(diff2) {
|
|
13072
|
+
const sections = [];
|
|
13073
|
+
if (diff2.webVitals.length > 0) {
|
|
13074
|
+
const lines = ["Web Vitals:"];
|
|
13075
|
+
lines.push(
|
|
13076
|
+
` ${padRight("Metric", 10)} ${padLeft("Baseline", 10)} ${padLeft("Current", 10)} ${padLeft("Delta", 10)} Verdict`
|
|
13077
|
+
);
|
|
13078
|
+
for (const d of diff2.webVitals) {
|
|
13079
|
+
const baseStr = d.baseline != null ? fmtVal(d.metric, d.baseline) : "---";
|
|
13080
|
+
const currStr = d.current != null ? fmtVal(d.metric, d.current) : "---";
|
|
13081
|
+
const deltaStr = d.deltaMs != null ? fmtDelta(d.metric, d.deltaMs) : "---";
|
|
13082
|
+
const symbol = VERDICT_SYMBOLS[d.verdict];
|
|
13083
|
+
lines.push(
|
|
13084
|
+
` ${padRight(d.metric.toUpperCase(), 10)} ${padLeft(baseStr, 10)} ${padLeft(currStr, 10)} ${padLeft(deltaStr, 10)} ${symbol} ${d.verdict}`
|
|
13085
|
+
);
|
|
13086
|
+
}
|
|
13087
|
+
sections.push(lines.join("\n"));
|
|
13088
|
+
}
|
|
13089
|
+
if (diff2.resourceSize.length > 0) {
|
|
13090
|
+
const lines = ["Resources:"];
|
|
13091
|
+
lines.push(
|
|
13092
|
+
` ${padRight("Type", 10)} ${padLeft("Baseline", 10)} ${padLeft("Current", 10)} ${padLeft("Delta", 10)} ${padLeft("Change", 8)} Verdict`
|
|
13093
|
+
);
|
|
13094
|
+
for (const d of diff2.resourceSize) {
|
|
13095
|
+
const baseStr = d.baseline != null ? formatBytes2(d.baseline) : "---";
|
|
13096
|
+
const currStr = d.current != null ? formatBytes2(d.current) : "---";
|
|
13097
|
+
const deltaStr = d.deltaMs != null ? fmtBytesDelta(d.deltaMs) : "---";
|
|
13098
|
+
const pctStr = d.deltaPct != null ? `${d.deltaPct > 0 ? "+" : ""}${d.deltaPct.toFixed(1)}%` : "---";
|
|
13099
|
+
const symbol = VERDICT_SYMBOLS[d.verdict];
|
|
13100
|
+
lines.push(
|
|
13101
|
+
` ${padRight(d.metric, 10)} ${padLeft(baseStr, 10)} ${padLeft(currStr, 10)} ${padLeft(deltaStr, 10)} ${padLeft(pctStr, 8)} ${symbol} ${d.verdict}`
|
|
13102
|
+
);
|
|
13103
|
+
}
|
|
13104
|
+
sections.push(lines.join("\n"));
|
|
13105
|
+
}
|
|
13106
|
+
const nonTrivialCoverage = diff2.coverage.filter(
|
|
13107
|
+
(d) => d.verdict !== "unchanged" || d.baseline != null || d.current != null
|
|
13108
|
+
);
|
|
13109
|
+
if (nonTrivialCoverage.length > 0) {
|
|
13110
|
+
const lines = ["Coverage:"];
|
|
13111
|
+
lines.push(
|
|
13112
|
+
` ${padRight("Metric", 16)} ${padLeft("Baseline", 10)} ${padLeft("Current", 10)} ${padLeft("Delta", 10)} Verdict`
|
|
13113
|
+
);
|
|
13114
|
+
for (const d of nonTrivialCoverage) {
|
|
13115
|
+
const baseStr = d.baseline != null ? `${d.baseline.toFixed(1)}%` : "---";
|
|
13116
|
+
const currStr = d.current != null ? `${d.current.toFixed(1)}%` : "---";
|
|
13117
|
+
const deltaStr = d.deltaMs != null ? `${d.deltaMs > 0 ? "+" : ""}${d.deltaMs.toFixed(1)}pp` : "---";
|
|
13118
|
+
const symbol = VERDICT_SYMBOLS[d.verdict];
|
|
13119
|
+
lines.push(
|
|
13120
|
+
` ${padRight(d.metric, 16)} ${padLeft(baseStr, 10)} ${padLeft(currStr, 10)} ${padLeft(deltaStr, 10)} ${symbol} ${d.verdict}`
|
|
13121
|
+
);
|
|
13122
|
+
}
|
|
13123
|
+
sections.push(lines.join("\n"));
|
|
13124
|
+
}
|
|
13125
|
+
const s = diff2.summary;
|
|
13126
|
+
sections.push(
|
|
13127
|
+
`${s.regressions} regression${s.regressions !== 1 ? "s" : ""}, ${s.improvements} improvement${s.improvements !== 1 ? "s" : ""}, ${s.unchanged} unchanged`
|
|
13128
|
+
);
|
|
13129
|
+
return sections.join("\n\n");
|
|
13130
|
+
}
|
|
13131
|
+
function fmtVal(metric, value) {
|
|
13132
|
+
if (metric === "cls") return value.toFixed(3);
|
|
13133
|
+
return formatMs(value);
|
|
13134
|
+
}
|
|
13135
|
+
function fmtDelta(metric, delta) {
|
|
13136
|
+
const sign = delta > 0 ? "+" : "";
|
|
13137
|
+
if (metric === "cls") return `${sign}${delta.toFixed(3)}`;
|
|
13138
|
+
return `${sign}${formatMs(delta)}`;
|
|
13139
|
+
}
|
|
13140
|
+
function fmtBytesDelta(delta) {
|
|
13141
|
+
const sign = delta > 0 ? "+" : "";
|
|
13142
|
+
return `${sign}${formatBytes2(Math.abs(delta))}`;
|
|
13143
|
+
}
|
|
13144
|
+
var THRESHOLDS, VERDICT_SYMBOLS;
|
|
13066
13145
|
var init_formatter = __esm({
|
|
13067
13146
|
"src/perf-audit/formatter.ts"() {
|
|
13068
13147
|
"use strict";
|
|
@@ -13075,10 +13154,261 @@ var init_formatter = __esm({
|
|
|
13075
13154
|
ttfb: { good: 800, needsImprovement: 1800 },
|
|
13076
13155
|
tbt: { good: 200, needsImprovement: 600 }
|
|
13077
13156
|
};
|
|
13157
|
+
VERDICT_SYMBOLS = {
|
|
13158
|
+
regression: "\u2191",
|
|
13159
|
+
improvement: "\u2193",
|
|
13160
|
+
unchanged: "=",
|
|
13161
|
+
new: "+",
|
|
13162
|
+
missing: "\u2212"
|
|
13163
|
+
};
|
|
13164
|
+
}
|
|
13165
|
+
});
|
|
13166
|
+
|
|
13167
|
+
// src/perf-audit/persist.ts
|
|
13168
|
+
var persist_exports = {};
|
|
13169
|
+
__export(persist_exports, {
|
|
13170
|
+
deleteAudit: () => deleteAudit,
|
|
13171
|
+
listAudits: () => listAudits,
|
|
13172
|
+
loadAudit: () => loadAudit,
|
|
13173
|
+
saveAudit: () => saveAudit
|
|
13174
|
+
});
|
|
13175
|
+
function auditsDir(localDir) {
|
|
13176
|
+
return path12.join(localDir, "audits");
|
|
13177
|
+
}
|
|
13178
|
+
function auditPath(localDir, name) {
|
|
13179
|
+
return path12.join(auditsDir(localDir), `${sanitizeName(name)}.json`);
|
|
13180
|
+
}
|
|
13181
|
+
function saveAudit(localDir, name, report) {
|
|
13182
|
+
const dir = auditsDir(localDir);
|
|
13183
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
13184
|
+
const filePath = auditPath(localDir, name);
|
|
13185
|
+
fs14.writeFileSync(filePath, JSON.stringify(report, null, 2), { mode: 384 });
|
|
13186
|
+
return filePath;
|
|
13187
|
+
}
|
|
13188
|
+
function loadAudit(localDir, name) {
|
|
13189
|
+
const filePath = auditPath(localDir, name);
|
|
13190
|
+
if (!fs14.existsSync(filePath)) {
|
|
13191
|
+
throw new Error(
|
|
13192
|
+
`Audit not found: ${filePath}. Run "browse perf-audit save ${name}" first.`
|
|
13193
|
+
);
|
|
13194
|
+
}
|
|
13195
|
+
return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
|
|
13196
|
+
}
|
|
13197
|
+
function listAudits(localDir) {
|
|
13198
|
+
const dir = auditsDir(localDir);
|
|
13199
|
+
if (!fs14.existsSync(dir)) return [];
|
|
13200
|
+
const files = fs14.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
13201
|
+
if (files.length === 0) return [];
|
|
13202
|
+
const entries = files.map((file) => {
|
|
13203
|
+
const fp = path12.join(dir, file);
|
|
13204
|
+
const stat = fs14.statSync(fp);
|
|
13205
|
+
return {
|
|
13206
|
+
name: file.replace(".json", ""),
|
|
13207
|
+
sizeBytes: stat.size,
|
|
13208
|
+
date: new Date(stat.mtimeMs).toISOString()
|
|
13209
|
+
};
|
|
13210
|
+
});
|
|
13211
|
+
entries.sort((a, b) => b.date.localeCompare(a.date));
|
|
13212
|
+
return entries;
|
|
13213
|
+
}
|
|
13214
|
+
function deleteAudit(localDir, name) {
|
|
13215
|
+
const filePath = auditPath(localDir, name);
|
|
13216
|
+
if (!fs14.existsSync(filePath)) {
|
|
13217
|
+
throw new Error(
|
|
13218
|
+
`Audit not found: ${filePath}. Nothing to delete.`
|
|
13219
|
+
);
|
|
13220
|
+
}
|
|
13221
|
+
fs14.unlinkSync(filePath);
|
|
13222
|
+
}
|
|
13223
|
+
var fs14, path12;
|
|
13224
|
+
var init_persist = __esm({
|
|
13225
|
+
"src/perf-audit/persist.ts"() {
|
|
13226
|
+
"use strict";
|
|
13227
|
+
fs14 = __toESM(require("fs"), 1);
|
|
13228
|
+
path12 = __toESM(require("path"), 1);
|
|
13229
|
+
init_sanitize();
|
|
13230
|
+
}
|
|
13231
|
+
});
|
|
13232
|
+
|
|
13233
|
+
// src/perf-audit/diff.ts
|
|
13234
|
+
var diff_exports = {};
|
|
13235
|
+
__export(diff_exports, {
|
|
13236
|
+
diffAuditReports: () => diffAuditReports
|
|
13237
|
+
});
|
|
13238
|
+
function compareMetric(metric, baseline, current, threshold) {
|
|
13239
|
+
if (baseline === null && current === null) {
|
|
13240
|
+
return { metric, baseline, current, verdict: "unchanged" };
|
|
13241
|
+
}
|
|
13242
|
+
if (baseline === null) {
|
|
13243
|
+
return { metric, baseline, current, verdict: "new" };
|
|
13244
|
+
}
|
|
13245
|
+
if (current === null) {
|
|
13246
|
+
return { metric, baseline, current, verdict: "missing" };
|
|
13247
|
+
}
|
|
13248
|
+
const deltaMs = current - baseline;
|
|
13249
|
+
const deltaPct = baseline !== 0 ? deltaMs / baseline * 100 : current !== 0 ? 100 : 0;
|
|
13250
|
+
let verdict = "unchanged";
|
|
13251
|
+
if (deltaMs > threshold) {
|
|
13252
|
+
verdict = "regression";
|
|
13253
|
+
} else if (deltaMs < -threshold) {
|
|
13254
|
+
verdict = "improvement";
|
|
13255
|
+
}
|
|
13256
|
+
return {
|
|
13257
|
+
metric,
|
|
13258
|
+
baseline,
|
|
13259
|
+
current,
|
|
13260
|
+
deltaMs: round(deltaMs),
|
|
13261
|
+
deltaPct: round(deltaPct),
|
|
13262
|
+
verdict
|
|
13263
|
+
};
|
|
13264
|
+
}
|
|
13265
|
+
function compareResourceSize(metric, baseline, current) {
|
|
13266
|
+
if (baseline === null && current === null) {
|
|
13267
|
+
return { metric, baseline, current, verdict: "unchanged" };
|
|
13268
|
+
}
|
|
13269
|
+
if (baseline === null) {
|
|
13270
|
+
return { metric, baseline, current, verdict: "new" };
|
|
13271
|
+
}
|
|
13272
|
+
if (current === null) {
|
|
13273
|
+
return { metric, baseline, current, verdict: "missing" };
|
|
13274
|
+
}
|
|
13275
|
+
const deltaMs = current - baseline;
|
|
13276
|
+
const deltaPct = baseline !== 0 ? deltaMs / baseline * 100 : current !== 0 ? 100 : 0;
|
|
13277
|
+
let verdict = "unchanged";
|
|
13278
|
+
if (deltaPct > RESOURCE_SIZE_PCT_THRESHOLD) {
|
|
13279
|
+
verdict = "regression";
|
|
13280
|
+
} else if (deltaPct < -RESOURCE_SIZE_PCT_THRESHOLD) {
|
|
13281
|
+
verdict = "improvement";
|
|
13282
|
+
}
|
|
13283
|
+
return {
|
|
13284
|
+
metric,
|
|
13285
|
+
baseline,
|
|
13286
|
+
current,
|
|
13287
|
+
deltaMs: round(deltaMs),
|
|
13288
|
+
deltaPct: round(deltaPct),
|
|
13289
|
+
verdict
|
|
13290
|
+
};
|
|
13291
|
+
}
|
|
13292
|
+
function compareCoverage(metric, baseline, current) {
|
|
13293
|
+
if (baseline === null && current === null) {
|
|
13294
|
+
return { metric, baseline, current, verdict: "unchanged" };
|
|
13295
|
+
}
|
|
13296
|
+
if (baseline === null) {
|
|
13297
|
+
return { metric, baseline, current, verdict: "new" };
|
|
13298
|
+
}
|
|
13299
|
+
if (current === null) {
|
|
13300
|
+
return { metric, baseline, current, verdict: "missing" };
|
|
13301
|
+
}
|
|
13302
|
+
const deltaPP = current - baseline;
|
|
13303
|
+
const deltaPct = baseline !== 0 ? deltaPP / baseline * 100 : current !== 0 ? 100 : 0;
|
|
13304
|
+
let verdict = "unchanged";
|
|
13305
|
+
if (deltaPP > COVERAGE_PP_THRESHOLD) {
|
|
13306
|
+
verdict = "regression";
|
|
13307
|
+
} else if (deltaPP < -COVERAGE_PP_THRESHOLD) {
|
|
13308
|
+
verdict = "improvement";
|
|
13309
|
+
}
|
|
13310
|
+
return {
|
|
13311
|
+
metric,
|
|
13312
|
+
baseline,
|
|
13313
|
+
current,
|
|
13314
|
+
deltaMs: round(deltaPP),
|
|
13315
|
+
deltaPct: round(deltaPct),
|
|
13316
|
+
verdict
|
|
13317
|
+
};
|
|
13318
|
+
}
|
|
13319
|
+
function avgUnusedPct(entries) {
|
|
13320
|
+
if (entries.length === 0) return null;
|
|
13321
|
+
let totalBytes = 0;
|
|
13322
|
+
let unusedBytes = 0;
|
|
13323
|
+
for (const e of entries) {
|
|
13324
|
+
totalBytes += e.totalBytes;
|
|
13325
|
+
unusedBytes += e.unusedBytes;
|
|
13326
|
+
}
|
|
13327
|
+
return totalBytes > 0 ? round(unusedBytes / totalBytes * 100) : 0;
|
|
13328
|
+
}
|
|
13329
|
+
function round(n) {
|
|
13330
|
+
return Math.round(n * 100) / 100;
|
|
13331
|
+
}
|
|
13332
|
+
function diffAuditReports(baseline, current) {
|
|
13333
|
+
const webVitals = [
|
|
13334
|
+
compareMetric("ttfb", baseline.webVitals.ttfb, current.webVitals.ttfb, WEB_VITALS_THRESHOLDS.ttfb.absolute),
|
|
13335
|
+
compareMetric("fcp", baseline.webVitals.fcp, current.webVitals.fcp, WEB_VITALS_THRESHOLDS.fcp.absolute),
|
|
13336
|
+
compareMetric("lcp", baseline.webVitals.lcp, current.webVitals.lcp, WEB_VITALS_THRESHOLDS.lcp.absolute),
|
|
13337
|
+
compareMetric("cls", baseline.webVitals.cls, current.webVitals.cls, WEB_VITALS_THRESHOLDS.cls.absolute),
|
|
13338
|
+
compareMetric("tbt", baseline.webVitals.tbt, current.webVitals.tbt, WEB_VITALS_THRESHOLDS.tbt.absolute),
|
|
13339
|
+
compareMetric("inp", baseline.webVitals.inp, current.webVitals.inp, WEB_VITALS_THRESHOLDS.inp.absolute)
|
|
13340
|
+
];
|
|
13341
|
+
const allCategoryKeys = /* @__PURE__ */ new Set([
|
|
13342
|
+
...Object.keys(baseline.resources.categories),
|
|
13343
|
+
...Object.keys(current.resources.categories)
|
|
13344
|
+
]);
|
|
13345
|
+
const resourceSize = [];
|
|
13346
|
+
for (const key of allCategoryKeys) {
|
|
13347
|
+
const baselineBytes = baseline.resources.categories[key]?.totalSizeBytes ?? null;
|
|
13348
|
+
const currentBytes = current.resources.categories[key]?.totalSizeBytes ?? null;
|
|
13349
|
+
resourceSize.push(compareResourceSize(key, baselineBytes, currentBytes));
|
|
13350
|
+
}
|
|
13351
|
+
const coverageDeltas = [];
|
|
13352
|
+
const baselineJsUnused = baseline.coverage ? avgUnusedPct(baseline.coverage.js) : null;
|
|
13353
|
+
const currentJsUnused = current.coverage ? avgUnusedPct(current.coverage.js) : null;
|
|
13354
|
+
coverageDeltas.push(compareCoverage("js-unused-pct", baselineJsUnused, currentJsUnused));
|
|
13355
|
+
const baselineCssUnused = baseline.coverage ? avgUnusedPct(baseline.coverage.css) : null;
|
|
13356
|
+
const currentCssUnused = current.coverage ? avgUnusedPct(current.coverage.css) : null;
|
|
13357
|
+
coverageDeltas.push(compareCoverage("css-unused-pct", baselineCssUnused, currentCssUnused));
|
|
13358
|
+
const fixableCount = {
|
|
13359
|
+
baseline: baseline.fixable.length,
|
|
13360
|
+
current: current.fixable.length,
|
|
13361
|
+
delta: current.fixable.length - baseline.fixable.length
|
|
13362
|
+
};
|
|
13363
|
+
const allDeltas = [...webVitals, ...resourceSize, ...coverageDeltas];
|
|
13364
|
+
let regressions = 0;
|
|
13365
|
+
let improvements = 0;
|
|
13366
|
+
let unchanged = 0;
|
|
13367
|
+
for (const d of allDeltas) {
|
|
13368
|
+
switch (d.verdict) {
|
|
13369
|
+
case "regression":
|
|
13370
|
+
regressions++;
|
|
13371
|
+
break;
|
|
13372
|
+
case "improvement":
|
|
13373
|
+
improvements++;
|
|
13374
|
+
break;
|
|
13375
|
+
case "unchanged":
|
|
13376
|
+
unchanged++;
|
|
13377
|
+
break;
|
|
13378
|
+
}
|
|
13379
|
+
}
|
|
13380
|
+
return {
|
|
13381
|
+
webVitals,
|
|
13382
|
+
resourceSize,
|
|
13383
|
+
coverage: coverageDeltas,
|
|
13384
|
+
fixableCount,
|
|
13385
|
+
summary: { regressions, improvements, unchanged }
|
|
13386
|
+
};
|
|
13387
|
+
}
|
|
13388
|
+
var WEB_VITALS_THRESHOLDS, RESOURCE_SIZE_PCT_THRESHOLD, COVERAGE_PP_THRESHOLD;
|
|
13389
|
+
var init_diff = __esm({
|
|
13390
|
+
"src/perf-audit/diff.ts"() {
|
|
13391
|
+
"use strict";
|
|
13392
|
+
WEB_VITALS_THRESHOLDS = {
|
|
13393
|
+
ttfb: { absolute: 100 },
|
|
13394
|
+
fcp: { absolute: 100 },
|
|
13395
|
+
lcp: { absolute: 200 },
|
|
13396
|
+
cls: { absolute: 0.05 },
|
|
13397
|
+
tbt: { absolute: 100 },
|
|
13398
|
+
inp: { absolute: 50 }
|
|
13399
|
+
};
|
|
13400
|
+
RESOURCE_SIZE_PCT_THRESHOLD = 10;
|
|
13401
|
+
COVERAGE_PP_THRESHOLD = 5;
|
|
13078
13402
|
}
|
|
13079
13403
|
});
|
|
13080
13404
|
|
|
13081
13405
|
// src/commands/meta.ts
|
|
13406
|
+
function padR(str, w) {
|
|
13407
|
+
return str.length >= w ? str : str + " ".repeat(w - str.length);
|
|
13408
|
+
}
|
|
13409
|
+
function padL(str, w) {
|
|
13410
|
+
return str.length >= w ? str : " ".repeat(w - str.length) + str;
|
|
13411
|
+
}
|
|
13082
13412
|
async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2, currentSession) {
|
|
13083
13413
|
switch (command) {
|
|
13084
13414
|
// ─── Tabs ──────────────────────────────────────────
|
|
@@ -13160,7 +13490,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
13160
13490
|
const lines = entries.map(
|
|
13161
13491
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
13162
13492
|
).join("\n") + "\n";
|
|
13163
|
-
|
|
13493
|
+
fs15.appendFileSync(consolePath, lines);
|
|
13164
13494
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
13165
13495
|
}
|
|
13166
13496
|
const newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -13170,7 +13500,7 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
13170
13500
|
const lines = entries.map(
|
|
13171
13501
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
13172
13502
|
).join("\n") + "\n";
|
|
13173
|
-
|
|
13503
|
+
fs15.appendFileSync(networkPath, lines);
|
|
13174
13504
|
buffers.lastNetworkFlushed = buffers.networkTotalAdded;
|
|
13175
13505
|
}
|
|
13176
13506
|
}
|
|
@@ -13187,22 +13517,22 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
13187
13517
|
const statesDir = `${LOCAL_DIR}/states`;
|
|
13188
13518
|
const statePath = `${statesDir}/${name}.json`;
|
|
13189
13519
|
if (subcommand === "list") {
|
|
13190
|
-
if (!
|
|
13191
|
-
const files =
|
|
13520
|
+
if (!fs15.existsSync(statesDir)) return "(no saved states)";
|
|
13521
|
+
const files = fs15.readdirSync(statesDir).filter((f) => f.endsWith(".json"));
|
|
13192
13522
|
if (files.length === 0) return "(no saved states)";
|
|
13193
13523
|
const lines = [];
|
|
13194
13524
|
for (const file of files) {
|
|
13195
13525
|
const fp = `${statesDir}/${file}`;
|
|
13196
|
-
const stat =
|
|
13526
|
+
const stat = fs15.statSync(fp);
|
|
13197
13527
|
lines.push(` ${file.replace(".json", "")} ${stat.size}B ${new Date(stat.mtimeMs).toISOString()}`);
|
|
13198
13528
|
}
|
|
13199
13529
|
return lines.join("\n");
|
|
13200
13530
|
}
|
|
13201
13531
|
if (subcommand === "show") {
|
|
13202
|
-
if (!
|
|
13532
|
+
if (!fs15.existsSync(statePath)) {
|
|
13203
13533
|
throw new Error(`State file not found: ${statePath}`);
|
|
13204
13534
|
}
|
|
13205
|
-
const data = JSON.parse(
|
|
13535
|
+
const data = JSON.parse(fs15.readFileSync(statePath, "utf-8"));
|
|
13206
13536
|
const cookieCount = data.cookies?.length || 0;
|
|
13207
13537
|
const originCount = data.origins?.length || 0;
|
|
13208
13538
|
const storageItems = (data.origins || []).reduce((sum, o) => sum + (o.localStorage?.length || 0), 0);
|
|
@@ -13227,15 +13557,15 @@ async function handleMetaCommand(command, args, bm, shutdown2, sessionManager2,
|
|
|
13227
13557
|
const context = bm.getContext();
|
|
13228
13558
|
if (!context) throw new Error("No browser context");
|
|
13229
13559
|
const state = await context.storageState();
|
|
13230
|
-
|
|
13231
|
-
|
|
13560
|
+
fs15.mkdirSync(statesDir, { recursive: true });
|
|
13561
|
+
fs15.writeFileSync(statePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
13232
13562
|
return `State saved: ${statePath}`;
|
|
13233
13563
|
}
|
|
13234
13564
|
if (subcommand === "load") {
|
|
13235
|
-
if (!
|
|
13565
|
+
if (!fs15.existsSync(statePath)) {
|
|
13236
13566
|
throw new Error(`State file not found: ${statePath}. Run "browse state save ${name}" first.`);
|
|
13237
13567
|
}
|
|
13238
|
-
const stateData = JSON.parse(
|
|
13568
|
+
const stateData = JSON.parse(fs15.readFileSync(statePath, "utf-8"));
|
|
13239
13569
|
const context = bm.getContext();
|
|
13240
13570
|
if (!context) throw new Error("No browser context");
|
|
13241
13571
|
const warnings = [];
|
|
@@ -13394,9 +13724,9 @@ ${legend.join("\n")}`;
|
|
|
13394
13724
|
try {
|
|
13395
13725
|
for (const vp of viewports) {
|
|
13396
13726
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
13397
|
-
const
|
|
13398
|
-
await page.screenshot({ path:
|
|
13399
|
-
results.push(`${vp.name} (${vp.width}x${vp.height}): ${
|
|
13727
|
+
const path16 = `${prefix}-${vp.name}.png`;
|
|
13728
|
+
await page.screenshot({ path: path16, fullPage: true });
|
|
13729
|
+
results.push(`${vp.name} (${vp.width}x${vp.height}): ${path16}`);
|
|
13400
13730
|
}
|
|
13401
13731
|
} finally {
|
|
13402
13732
|
if (originalViewport) {
|
|
@@ -13540,13 +13870,13 @@ ${legend.join("\n")}`;
|
|
|
13540
13870
|
const diffArgs = args.filter((a) => a !== "--full");
|
|
13541
13871
|
const baseline = diffArgs[0];
|
|
13542
13872
|
if (!baseline) throw new Error("Usage: browse screenshot-diff <baseline> [current] [--threshold 0.1] [--full]");
|
|
13543
|
-
if (!
|
|
13873
|
+
if (!fs15.existsSync(baseline)) throw new Error(`Baseline file not found: ${baseline}`);
|
|
13544
13874
|
let thresholdPct = 0.1;
|
|
13545
13875
|
const threshIdx = diffArgs.indexOf("--threshold");
|
|
13546
13876
|
if (threshIdx !== -1 && diffArgs[threshIdx + 1]) {
|
|
13547
13877
|
thresholdPct = parseFloat(diffArgs[threshIdx + 1]);
|
|
13548
13878
|
}
|
|
13549
|
-
const baselineBuffer =
|
|
13879
|
+
const baselineBuffer = fs15.readFileSync(baseline);
|
|
13550
13880
|
let currentBuffer;
|
|
13551
13881
|
let currentPath;
|
|
13552
13882
|
for (let i = 1; i < diffArgs.length; i++) {
|
|
@@ -13560,8 +13890,8 @@ ${legend.join("\n")}`;
|
|
|
13560
13890
|
}
|
|
13561
13891
|
}
|
|
13562
13892
|
if (currentPath) {
|
|
13563
|
-
if (!
|
|
13564
|
-
currentBuffer =
|
|
13893
|
+
if (!fs15.existsSync(currentPath)) throw new Error(`Current screenshot not found: ${currentPath}`);
|
|
13894
|
+
currentBuffer = fs15.readFileSync(currentPath);
|
|
13565
13895
|
} else {
|
|
13566
13896
|
const page = bm.getPage();
|
|
13567
13897
|
currentBuffer = await page.screenshot({ fullPage: isFullPageDiff });
|
|
@@ -13571,7 +13901,7 @@ ${legend.join("\n")}`;
|
|
|
13571
13901
|
const extIdx = baseline.lastIndexOf(".");
|
|
13572
13902
|
const diffPath = extIdx > 0 ? baseline.slice(0, extIdx) + "-diff" + baseline.slice(extIdx) : baseline + "-diff.png";
|
|
13573
13903
|
if (!result.passed && result.diffImage) {
|
|
13574
|
-
|
|
13904
|
+
fs15.writeFileSync(diffPath, result.diffImage);
|
|
13575
13905
|
}
|
|
13576
13906
|
return [
|
|
13577
13907
|
`Pixels: ${result.totalPixels}`,
|
|
@@ -13707,7 +14037,7 @@ ${legend.join("\n")}`;
|
|
|
13707
14037
|
const { formatAsHar: formatAsHar2 } = await Promise.resolve().then(() => (init_har(), har_exports));
|
|
13708
14038
|
const har = formatAsHar2(sessionBuffers.networkBuffer, recording.startTime);
|
|
13709
14039
|
const harPath = args[1] || (currentSession ? `${currentSession.outputDir}/recording.har` : `${LOCAL_DIR}/browse-recording.har`);
|
|
13710
|
-
|
|
14040
|
+
fs15.writeFileSync(harPath, JSON.stringify(har, null, 2));
|
|
13711
14041
|
const entryCount = har.log.entries.length;
|
|
13712
14042
|
return `HAR saved: ${harPath} (${entryCount} entries)`;
|
|
13713
14043
|
}
|
|
@@ -13957,7 +14287,7 @@ ${snapshot}`;
|
|
|
13957
14287
|
throw new Error(`Unknown format: ${format}. Use "browse" (chain JSON) or "replay" (Playwright/Puppeteer).`);
|
|
13958
14288
|
}
|
|
13959
14289
|
if (filePath) {
|
|
13960
|
-
|
|
14290
|
+
fs15.writeFileSync(filePath, output);
|
|
13961
14291
|
return `Exported ${steps.length} steps as ${format}: ${filePath}`;
|
|
13962
14292
|
}
|
|
13963
14293
|
return output;
|
|
@@ -14199,29 +14529,119 @@ ${lines.join("\n")}
|
|
|
14199
14529
|
return lines.join("\n");
|
|
14200
14530
|
}
|
|
14201
14531
|
case "perf-audit": {
|
|
14202
|
-
const
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14532
|
+
const subcommand = args[0];
|
|
14533
|
+
if (subcommand === "save") {
|
|
14534
|
+
const subArgs = args.slice(1);
|
|
14535
|
+
const flags = new Set(subArgs.filter((a) => a.startsWith("--")));
|
|
14536
|
+
const positionalArgs = subArgs.filter((a) => !a.startsWith("--"));
|
|
14537
|
+
const jsonOutput = flags.has("--json");
|
|
14538
|
+
const options = {
|
|
14539
|
+
includeCoverage: !flags.has("--no-coverage"),
|
|
14540
|
+
includeDetection: !flags.has("--no-detect")
|
|
14541
|
+
};
|
|
14542
|
+
let name;
|
|
14543
|
+
let url;
|
|
14544
|
+
for (const arg of positionalArgs) {
|
|
14545
|
+
if (arg.startsWith("http://") || arg.startsWith("https://")) {
|
|
14546
|
+
url = arg;
|
|
14547
|
+
} else {
|
|
14548
|
+
name = arg;
|
|
14549
|
+
}
|
|
14550
|
+
}
|
|
14551
|
+
if (url) {
|
|
14552
|
+
const { handleWriteCommand: handleWriteCommand2 } = await Promise.resolve().then(() => (init_write(), write_exports));
|
|
14553
|
+
await handleWriteCommand2("goto", [url], bm, currentSession?.domainFilter);
|
|
14554
|
+
}
|
|
14555
|
+
const { runPerfAudit: runPerfAudit2 } = await Promise.resolve().then(() => (init_perf_audit(), perf_audit_exports));
|
|
14556
|
+
const { formatPerfAudit: formatPerfAudit2 } = await Promise.resolve().then(() => (init_formatter(), formatter_exports));
|
|
14557
|
+
const { saveAudit: saveAudit2 } = await Promise.resolve().then(() => (init_persist(), persist_exports));
|
|
14558
|
+
const networkEntries = currentSession?.buffers?.networkBuffer || [];
|
|
14559
|
+
const report = await runPerfAudit2(bm, networkEntries, options);
|
|
14560
|
+
if (!name) {
|
|
14561
|
+
try {
|
|
14562
|
+
const pageUrl = new URL(bm.getCurrentUrl());
|
|
14563
|
+
const host = pageUrl.hostname.replace(/\./g, "-");
|
|
14564
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
14565
|
+
name = `${host}-${date}`;
|
|
14566
|
+
} catch {
|
|
14567
|
+
name = `audit-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
14568
|
+
}
|
|
14569
|
+
}
|
|
14570
|
+
const filePath = saveAudit2(LOCAL_DIR, name, report);
|
|
14571
|
+
const formatted = formatPerfAudit2(report, jsonOutput);
|
|
14572
|
+
return `${formatted}
|
|
14573
|
+
|
|
14574
|
+
Audit saved: ${filePath}`;
|
|
14575
|
+
}
|
|
14576
|
+
if (subcommand === "compare") {
|
|
14577
|
+
if (!args[1]) throw new Error("Usage: browse perf-audit compare <baseline> [current]");
|
|
14578
|
+
const subArgs = args.slice(1);
|
|
14579
|
+
const flags = new Set(subArgs.filter((a) => a.startsWith("--")));
|
|
14580
|
+
const positionalArgs = subArgs.filter((a) => !a.startsWith("--"));
|
|
14581
|
+
const jsonOutput = flags.has("--json");
|
|
14582
|
+
const { loadAudit: loadAudit2 } = await Promise.resolve().then(() => (init_persist(), persist_exports));
|
|
14583
|
+
const { diffAuditReports: diffAuditReports2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
|
|
14584
|
+
const { formatAuditDiff: formatAuditDiff2 } = await Promise.resolve().then(() => (init_formatter(), formatter_exports));
|
|
14585
|
+
const baseline = loadAudit2(LOCAL_DIR, positionalArgs[0]);
|
|
14586
|
+
let current;
|
|
14587
|
+
if (positionalArgs[1]) {
|
|
14588
|
+
current = loadAudit2(LOCAL_DIR, positionalArgs[1]);
|
|
14589
|
+
} else {
|
|
14590
|
+
const options = {
|
|
14591
|
+
includeCoverage: !flags.has("--no-coverage"),
|
|
14592
|
+
includeDetection: !flags.has("--no-detect")
|
|
14593
|
+
};
|
|
14594
|
+
const { runPerfAudit: runPerfAudit2 } = await Promise.resolve().then(() => (init_perf_audit(), perf_audit_exports));
|
|
14595
|
+
const networkEntries = currentSession?.buffers?.networkBuffer || [];
|
|
14596
|
+
current = await runPerfAudit2(bm, networkEntries, options);
|
|
14597
|
+
}
|
|
14598
|
+
const diff2 = diffAuditReports2(baseline, current);
|
|
14599
|
+
return formatAuditDiff2(diff2, jsonOutput);
|
|
14600
|
+
}
|
|
14601
|
+
if (subcommand === "list") {
|
|
14602
|
+
const { listAudits: listAudits2 } = await Promise.resolve().then(() => (init_persist(), persist_exports));
|
|
14603
|
+
const entries = listAudits2(LOCAL_DIR);
|
|
14604
|
+
if (entries.length === 0) return "(no saved audits)";
|
|
14605
|
+
const lines = [];
|
|
14606
|
+
lines.push(`${padR("Name", 40)} ${padL("Size", 10)} ${"Date"}`);
|
|
14607
|
+
for (const e of entries) {
|
|
14608
|
+
const sizeStr = e.sizeBytes < 1024 ? `${e.sizeBytes}B` : `${Math.round(e.sizeBytes / 1024)}KB`;
|
|
14609
|
+
const dateStr = e.date.slice(0, 19).replace("T", " ");
|
|
14610
|
+
lines.push(`${padR(e.name, 40)} ${padL(sizeStr, 10)} ${dateStr}`);
|
|
14611
|
+
}
|
|
14612
|
+
return lines.join("\n");
|
|
14613
|
+
}
|
|
14614
|
+
if (subcommand === "delete") {
|
|
14615
|
+
if (!args[1]) throw new Error("Usage: browse perf-audit delete <name>");
|
|
14616
|
+
const { deleteAudit: deleteAudit2 } = await Promise.resolve().then(() => (init_persist(), persist_exports));
|
|
14617
|
+
deleteAudit2(LOCAL_DIR, args[1]);
|
|
14618
|
+
return `Audit deleted: ${args[1]}`;
|
|
14619
|
+
}
|
|
14620
|
+
{
|
|
14621
|
+
const flags = new Set(args.filter((a) => a.startsWith("--")));
|
|
14622
|
+
const positionalArgs = args.filter((a) => !a.startsWith("--"));
|
|
14623
|
+
const url = positionalArgs[0];
|
|
14624
|
+
const options = {
|
|
14625
|
+
includeCoverage: !flags.has("--no-coverage"),
|
|
14626
|
+
includeDetection: !flags.has("--no-detect")
|
|
14627
|
+
};
|
|
14628
|
+
const jsonOutput = flags.has("--json");
|
|
14629
|
+
if (url) {
|
|
14630
|
+
const { handleWriteCommand: handleWriteCommand2 } = await Promise.resolve().then(() => (init_write(), write_exports));
|
|
14631
|
+
await handleWriteCommand2("goto", [url], bm, currentSession?.domainFilter);
|
|
14632
|
+
}
|
|
14633
|
+
const { runPerfAudit: runPerfAudit2 } = await Promise.resolve().then(() => (init_perf_audit(), perf_audit_exports));
|
|
14634
|
+
const { formatPerfAudit: formatPerfAudit2 } = await Promise.resolve().then(() => (init_formatter(), formatter_exports));
|
|
14635
|
+
const networkEntries = currentSession?.buffers?.networkBuffer || [];
|
|
14636
|
+
const report = await runPerfAudit2(bm, networkEntries, options);
|
|
14637
|
+
return formatPerfAudit2(report, jsonOutput);
|
|
14213
14638
|
}
|
|
14214
|
-
const { runPerfAudit: runPerfAudit2 } = await Promise.resolve().then(() => (init_perf_audit(), perf_audit_exports));
|
|
14215
|
-
const { formatPerfAudit: formatPerfAudit2 } = await Promise.resolve().then(() => (init_formatter(), formatter_exports));
|
|
14216
|
-
const networkEntries = currentSession?.buffers?.networkBuffer || [];
|
|
14217
|
-
const report = await runPerfAudit2(bm, networkEntries, options);
|
|
14218
|
-
return formatPerfAudit2(report, jsonOutput);
|
|
14219
14639
|
}
|
|
14220
14640
|
default:
|
|
14221
14641
|
throw new Error(`Unknown meta command: ${command}`);
|
|
14222
14642
|
}
|
|
14223
14643
|
}
|
|
14224
|
-
var
|
|
14644
|
+
var fs15, LOCAL_DIR;
|
|
14225
14645
|
var init_meta = __esm({
|
|
14226
14646
|
"src/commands/meta.ts"() {
|
|
14227
14647
|
"use strict";
|
|
@@ -14229,7 +14649,7 @@ var init_meta = __esm({
|
|
|
14229
14649
|
init_constants();
|
|
14230
14650
|
init_sanitize();
|
|
14231
14651
|
init_lib();
|
|
14232
|
-
|
|
14652
|
+
fs15 = __toESM(require("fs"), 1);
|
|
14233
14653
|
LOCAL_DIR = process.env.BROWSE_LOCAL_DIR || "/tmp";
|
|
14234
14654
|
}
|
|
14235
14655
|
});
|
|
@@ -14631,6 +15051,44 @@ function mapToolCallToCommand(toolName, params) {
|
|
|
14631
15051
|
if (params.api_key) args.push(String(params.api_key));
|
|
14632
15052
|
return { command: "provider", args };
|
|
14633
15053
|
}
|
|
15054
|
+
// ─── PERFORMANCE AUDIT ──────────────────────────────
|
|
15055
|
+
case "perf-audit": {
|
|
15056
|
+
const args = [];
|
|
15057
|
+
if (params.url) args.push(String(params.url));
|
|
15058
|
+
if (params.no_coverage) args.push("--no-coverage");
|
|
15059
|
+
if (params.no_detect) args.push("--no-detect");
|
|
15060
|
+
if (params.json) args.push("--json");
|
|
15061
|
+
return { command: "perf-audit", args };
|
|
15062
|
+
}
|
|
15063
|
+
case "perf-audit-save": {
|
|
15064
|
+
const args = ["save"];
|
|
15065
|
+
if (params.name) args.push(String(params.name));
|
|
15066
|
+
if (params.url) args.push(String(params.url));
|
|
15067
|
+
if (params.no_coverage) args.push("--no-coverage");
|
|
15068
|
+
if (params.no_detect) args.push("--no-detect");
|
|
15069
|
+
return { command: "perf-audit", args };
|
|
15070
|
+
}
|
|
15071
|
+
case "perf-audit-compare": {
|
|
15072
|
+
const args = ["compare", String(params.baseline)];
|
|
15073
|
+
if (params.current) args.push(String(params.current));
|
|
15074
|
+
if (params.no_coverage) args.push("--no-coverage");
|
|
15075
|
+
if (params.no_detect) args.push("--no-detect");
|
|
15076
|
+
if (params.json) args.push("--json");
|
|
15077
|
+
return { command: "perf-audit", args };
|
|
15078
|
+
}
|
|
15079
|
+
case "perf-audit-list":
|
|
15080
|
+
return { command: "perf-audit", args: ["list"] };
|
|
15081
|
+
case "perf-audit-delete":
|
|
15082
|
+
return { command: "perf-audit", args: ["delete", String(params.name)] };
|
|
15083
|
+
case "detect":
|
|
15084
|
+
return { command: "detect", args: [] };
|
|
15085
|
+
case "coverage":
|
|
15086
|
+
return { command: "coverage", args: [String(params.action)] };
|
|
15087
|
+
case "initscript": {
|
|
15088
|
+
const args = [String(params.action)];
|
|
15089
|
+
if (params.action === "set" && params.code) args.push(String(params.code));
|
|
15090
|
+
return { command: "initscript", args };
|
|
15091
|
+
}
|
|
14634
15092
|
default:
|
|
14635
15093
|
throw new Error(`Unknown MCP tool: ${toolName}`);
|
|
14636
15094
|
}
|
|
@@ -15605,6 +16063,103 @@ var init_mcp_tools = __esm({
|
|
|
15605
16063
|
},
|
|
15606
16064
|
required: ["action"]
|
|
15607
16065
|
}
|
|
16066
|
+
},
|
|
16067
|
+
// ─── Performance Audit ────────────────────────────────────────────
|
|
16068
|
+
{
|
|
16069
|
+
name: "browse_perf_audit",
|
|
16070
|
+
description: "Run a full performance audit on the current page (or a URL). Returns Core Web Vitals (LCP, CLS, TBT, FCP, TTFB, INP), LCP critical path reconstruction, layout shift attribution, long task script attribution, resource breakdown, render-blocking detection, image audit, font audit, DOM complexity, tech stack detection (108 frameworks, 55 SaaS platforms), third-party impact analysis, JS/CSS coverage, and prioritized recommendations. The page is reloaded as part of the audit.",
|
|
16071
|
+
inputSchema: {
|
|
16072
|
+
type: "object",
|
|
16073
|
+
properties: {
|
|
16074
|
+
url: { type: "string", description: "URL to audit. If provided, navigates there first. If omitted, audits the current page." },
|
|
16075
|
+
no_coverage: { type: "boolean", description: "Skip JS/CSS coverage collection (faster audit)." },
|
|
16076
|
+
no_detect: { type: "boolean", description: "Skip framework/SaaS/infrastructure detection." },
|
|
16077
|
+
json: { type: "boolean", description: "Return structured JSON instead of formatted text." }
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
},
|
|
16081
|
+
{
|
|
16082
|
+
name: "browse_perf_audit_save",
|
|
16083
|
+
description: "Run a performance audit and save the report to .browse/audits/ for later comparison.",
|
|
16084
|
+
inputSchema: {
|
|
16085
|
+
type: "object",
|
|
16086
|
+
properties: {
|
|
16087
|
+
name: { type: "string", description: "Name for the saved report. Auto-generated from URL + timestamp if omitted." },
|
|
16088
|
+
url: { type: "string", description: "URL to audit. If omitted, audits the current page." },
|
|
16089
|
+
no_coverage: { type: "boolean", description: "Skip JS/CSS coverage collection (faster audit)." },
|
|
16090
|
+
no_detect: { type: "boolean", description: "Skip framework/SaaS/infrastructure detection." }
|
|
16091
|
+
}
|
|
16092
|
+
}
|
|
16093
|
+
},
|
|
16094
|
+
{
|
|
16095
|
+
name: "browse_perf_audit_compare",
|
|
16096
|
+
description: "Compare a saved audit report against the current page or another saved report. Shows regressions and improvements.",
|
|
16097
|
+
inputSchema: {
|
|
16098
|
+
type: "object",
|
|
16099
|
+
properties: {
|
|
16100
|
+
baseline: { type: "string", description: "Name of the saved baseline audit report." },
|
|
16101
|
+
current: { type: "string", description: "Name of a second saved report to compare against. If omitted, runs a live audit for comparison." },
|
|
16102
|
+
no_coverage: { type: "boolean", description: "Skip JS/CSS coverage collection when running a live audit." },
|
|
16103
|
+
no_detect: { type: "boolean", description: "Skip framework/SaaS/infrastructure detection when running a live audit." },
|
|
16104
|
+
json: { type: "boolean", description: "Return structured JSON instead of formatted text." }
|
|
16105
|
+
},
|
|
16106
|
+
required: ["baseline"]
|
|
16107
|
+
}
|
|
16108
|
+
},
|
|
16109
|
+
{
|
|
16110
|
+
name: "browse_perf_audit_list",
|
|
16111
|
+
description: "List all saved performance audit reports in .browse/audits/.",
|
|
16112
|
+
inputSchema: { type: "object", properties: {} }
|
|
16113
|
+
},
|
|
16114
|
+
{
|
|
16115
|
+
name: "browse_perf_audit_delete",
|
|
16116
|
+
description: "Delete a saved performance audit report from .browse/audits/.",
|
|
16117
|
+
inputSchema: {
|
|
16118
|
+
type: "object",
|
|
16119
|
+
properties: {
|
|
16120
|
+
name: { type: "string", description: "Name of the saved audit report to delete." }
|
|
16121
|
+
},
|
|
16122
|
+
required: ["name"]
|
|
16123
|
+
}
|
|
16124
|
+
},
|
|
16125
|
+
{
|
|
16126
|
+
name: "browse_detect",
|
|
16127
|
+
description: "Detect the technology stack of the current page. Returns frameworks (React, Vue, Angular, Next.js, Laravel, WordPress, Magento, etc. \u2014 108 total with version, build mode, config depth), SaaS platforms (Shopify, Wix, Squarespace, etc. \u2014 55 total with app enumeration and constraints), infrastructure (CDN, protocol, compression, caching, Service Worker), DOM complexity, and third-party inventory (88 known domains classified by category).",
|
|
16128
|
+
inputSchema: { type: "object", properties: {} }
|
|
16129
|
+
},
|
|
16130
|
+
{
|
|
16131
|
+
name: "browse_coverage",
|
|
16132
|
+
description: "Collect JS and CSS code coverage. Start collection, navigate/interact with the page, then stop to see per-file used/unused bytes sorted by wasted bytes descending.",
|
|
16133
|
+
inputSchema: {
|
|
16134
|
+
type: "object",
|
|
16135
|
+
properties: {
|
|
16136
|
+
action: { type: "string", description: "Coverage operation.", enum: ["start", "stop"] }
|
|
16137
|
+
},
|
|
16138
|
+
required: ["action"]
|
|
16139
|
+
}
|
|
16140
|
+
},
|
|
16141
|
+
{
|
|
16142
|
+
name: "browse_initscript",
|
|
16143
|
+
description: "Inject JavaScript that runs before every page load (via context.addInitScript). Useful for mocking APIs, injecting polyfills, or setting up performance observers. Scripts persist across navigations and survive device emulation.",
|
|
16144
|
+
inputSchema: {
|
|
16145
|
+
type: "object",
|
|
16146
|
+
properties: {
|
|
16147
|
+
action: { type: "string", description: "Operation to perform.", enum: ["set", "show", "clear"] },
|
|
16148
|
+
code: { type: "string", description: 'JavaScript code to inject (required for "set").' }
|
|
16149
|
+
},
|
|
16150
|
+
required: ["action"]
|
|
16151
|
+
}
|
|
16152
|
+
},
|
|
16153
|
+
{
|
|
16154
|
+
name: "browse_scrollintoview",
|
|
16155
|
+
description: "Scroll an element into view. Alias for scrollinto.",
|
|
16156
|
+
inputSchema: {
|
|
16157
|
+
type: "object",
|
|
16158
|
+
properties: {
|
|
16159
|
+
selector: { type: "string", description: "CSS selector or @ref of the element to scroll into view." }
|
|
16160
|
+
},
|
|
16161
|
+
required: ["selector"]
|
|
16162
|
+
}
|
|
15608
16163
|
}
|
|
15609
16164
|
];
|
|
15610
16165
|
}
|
|
@@ -16025,7 +16580,7 @@ var init_domain_filter = __esm({
|
|
|
16025
16580
|
});
|
|
16026
16581
|
|
|
16027
16582
|
// src/session-manager.ts
|
|
16028
|
-
var
|
|
16583
|
+
var fs16, path13, SessionManager;
|
|
16029
16584
|
var init_session_manager = __esm({
|
|
16030
16585
|
"src/session-manager.ts"() {
|
|
16031
16586
|
"use strict";
|
|
@@ -16035,8 +16590,8 @@ var init_session_manager = __esm({
|
|
|
16035
16590
|
init_sanitize();
|
|
16036
16591
|
init_session_persist();
|
|
16037
16592
|
init_encryption();
|
|
16038
|
-
|
|
16039
|
-
|
|
16593
|
+
fs16 = __toESM(require("fs"), 1);
|
|
16594
|
+
path13 = __toESM(require("path"), 1);
|
|
16040
16595
|
SessionManager = class {
|
|
16041
16596
|
sessions = /* @__PURE__ */ new Map();
|
|
16042
16597
|
browser;
|
|
@@ -16091,8 +16646,8 @@ var init_session_manager = __esm({
|
|
|
16091
16646
|
}
|
|
16092
16647
|
return session;
|
|
16093
16648
|
}
|
|
16094
|
-
const outputDir =
|
|
16095
|
-
|
|
16649
|
+
const outputDir = path13.join(this.localDir, "sessions", sanitizeName(sessionId));
|
|
16650
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
16096
16651
|
const buffers = new SessionBuffers();
|
|
16097
16652
|
const manager = new BrowserManager(buffers);
|
|
16098
16653
|
await manager.launchWithBrowser(this.browser, this.reuseContext && this.sessions.size === 0);
|
|
@@ -16276,7 +16831,7 @@ function flushSessionBuffers(session, final) {
|
|
|
16276
16831
|
const lines = newEntries.map(
|
|
16277
16832
|
(e) => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
|
|
16278
16833
|
).join("\n") + "\n";
|
|
16279
|
-
|
|
16834
|
+
fs17.appendFileSync(consolePath, lines);
|
|
16280
16835
|
buffers.lastConsoleFlushed = buffers.consoleTotalAdded;
|
|
16281
16836
|
}
|
|
16282
16837
|
let newNetworkCount = buffers.networkTotalAdded - buffers.lastNetworkFlushed;
|
|
@@ -16301,7 +16856,7 @@ function flushSessionBuffers(session, final) {
|
|
|
16301
16856
|
const lines = prefix.map(
|
|
16302
16857
|
(e) => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} \u2192 ${e.status || "pending"} (${e.duration || "?"}ms, ${e.size || "?"}B)`
|
|
16303
16858
|
).join("\n") + "\n";
|
|
16304
|
-
|
|
16859
|
+
fs17.appendFileSync(networkPath, lines);
|
|
16305
16860
|
buffers.lastNetworkFlushed += prefixLen;
|
|
16306
16861
|
}
|
|
16307
16862
|
}
|
|
@@ -16509,9 +17064,9 @@ async function shutdown() {
|
|
|
16509
17064
|
await activeRuntime?.close?.().catch(() => {
|
|
16510
17065
|
});
|
|
16511
17066
|
try {
|
|
16512
|
-
const currentState = JSON.parse(
|
|
17067
|
+
const currentState = JSON.parse(fs17.readFileSync(STATE_FILE, "utf-8"));
|
|
16513
17068
|
if (currentState.pid === process.pid || currentState.token === AUTH_TOKEN) {
|
|
16514
|
-
|
|
17069
|
+
fs17.unlinkSync(STATE_FILE);
|
|
16515
17070
|
}
|
|
16516
17071
|
} catch {
|
|
16517
17072
|
}
|
|
@@ -16528,15 +17083,15 @@ async function start() {
|
|
|
16528
17083
|
const { BrowserManager: BrowserManager2, getProfileDir: getProfileDir2 } = await Promise.resolve().then(() => (init_browser_manager(), browser_manager_exports));
|
|
16529
17084
|
const { SessionBuffers: SessionBuffers2 } = await Promise.resolve().then(() => (init_buffers(), buffers_exports));
|
|
16530
17085
|
const profileDir = getProfileDir2(LOCAL_DIR2, profileName);
|
|
16531
|
-
|
|
17086
|
+
fs17.mkdirSync(profileDir, { recursive: true });
|
|
16532
17087
|
const bm = new BrowserManager2();
|
|
16533
17088
|
await bm.launchPersistent(profileDir, () => {
|
|
16534
17089
|
if (isShuttingDown) return;
|
|
16535
17090
|
console.error("[browse] Chromium disconnected (profile mode). Shutting down.");
|
|
16536
17091
|
shutdown();
|
|
16537
17092
|
});
|
|
16538
|
-
const outputDir =
|
|
16539
|
-
|
|
17093
|
+
const outputDir = path14.join(LOCAL_DIR2, "sessions", profileName);
|
|
17094
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
16540
17095
|
profileSession = {
|
|
16541
17096
|
id: profileName,
|
|
16542
17097
|
manager: bm,
|
|
@@ -16631,7 +17186,7 @@ async function start() {
|
|
|
16631
17186
|
const context = session.manager.getContext();
|
|
16632
17187
|
if (context) {
|
|
16633
17188
|
try {
|
|
16634
|
-
const stateData = JSON.parse(
|
|
17189
|
+
const stateData = JSON.parse(fs17.readFileSync(stateFilePath, "utf-8"));
|
|
16635
17190
|
if (stateData.cookies?.length) {
|
|
16636
17191
|
await context.addCookies(stateData.cookies);
|
|
16637
17192
|
}
|
|
@@ -16658,7 +17213,7 @@ async function start() {
|
|
|
16658
17213
|
port,
|
|
16659
17214
|
token: AUTH_TOKEN,
|
|
16660
17215
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16661
|
-
serverPath:
|
|
17216
|
+
serverPath: path14.resolve(path14.dirname((0, import_url2.fileURLToPath)(__import_meta_url)), "server.ts")
|
|
16662
17217
|
};
|
|
16663
17218
|
if (profileName) {
|
|
16664
17219
|
state.profile = profileName;
|
|
@@ -16666,12 +17221,12 @@ async function start() {
|
|
|
16666
17221
|
if (DEBUG_PORT > 0) {
|
|
16667
17222
|
state.debugPort = DEBUG_PORT;
|
|
16668
17223
|
}
|
|
16669
|
-
|
|
17224
|
+
fs17.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
|
|
16670
17225
|
console.log(`[browse] Server running on http://127.0.0.1:${port} (PID: ${process.pid})`);
|
|
16671
17226
|
console.log(`[browse] State file: ${STATE_FILE}`);
|
|
16672
17227
|
console.log(`[browse] Idle timeout: ${IDLE_TIMEOUT_MS / 1e3}s`);
|
|
16673
17228
|
}
|
|
16674
|
-
var
|
|
17229
|
+
var fs17, path14, crypto3, http, import_url2, net3, AUTH_TOKEN, DEBUG_PORT, BROWSE_PORT, BROWSE_INSTANCE, INSTANCE_SUFFIX, LOCAL_DIR2, STATE_FILE, IDLE_TIMEOUT_MS, sessionManager, browser, profileSession, activeRuntime, isShuttingDown, isRemoteBrowser, policyChecker, READ_COMMANDS2, WRITE_COMMANDS2, META_COMMANDS2, RECORDING_SKIP, PAGE_CONTENT_COMMANDS, BOUNDARY_NONCE, flushInterval, sessionCleanupInterval;
|
|
16675
17230
|
var init_server = __esm({
|
|
16676
17231
|
"src/server.ts"() {
|
|
16677
17232
|
"use strict";
|
|
@@ -16682,8 +17237,8 @@ var init_server = __esm({
|
|
|
16682
17237
|
init_meta();
|
|
16683
17238
|
init_policy();
|
|
16684
17239
|
init_constants();
|
|
16685
|
-
|
|
16686
|
-
|
|
17240
|
+
fs17 = __toESM(require("fs"), 1);
|
|
17241
|
+
path14 = __toESM(require("path"), 1);
|
|
16687
17242
|
crypto3 = __toESM(require("crypto"), 1);
|
|
16688
17243
|
http = __toESM(require("http"), 1);
|
|
16689
17244
|
import_url2 = require("url");
|
|
@@ -16873,8 +17428,8 @@ __export(cli_exports, {
|
|
|
16873
17428
|
resolveServerScript: () => resolveServerScript
|
|
16874
17429
|
});
|
|
16875
17430
|
module.exports = __toCommonJS(cli_exports);
|
|
16876
|
-
var
|
|
16877
|
-
var
|
|
17431
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
17432
|
+
var path15 = __toESM(require("path"), 1);
|
|
16878
17433
|
var import_child_process4 = require("child_process");
|
|
16879
17434
|
var import_url3 = require("url");
|
|
16880
17435
|
init_constants();
|
|
@@ -16930,50 +17485,50 @@ var INSTANCE_SUFFIX2 = BROWSE_PORT2 ? `-${BROWSE_PORT2}` : BROWSE_INSTANCE2 ? `-
|
|
|
16930
17485
|
function resolveLocalDir() {
|
|
16931
17486
|
if (process.env.BROWSE_LOCAL_DIR) {
|
|
16932
17487
|
try {
|
|
16933
|
-
|
|
17488
|
+
fs18.mkdirSync(process.env.BROWSE_LOCAL_DIR, { recursive: true });
|
|
16934
17489
|
} catch {
|
|
16935
17490
|
}
|
|
16936
17491
|
return process.env.BROWSE_LOCAL_DIR;
|
|
16937
17492
|
}
|
|
16938
17493
|
let dir = process.cwd();
|
|
16939
17494
|
for (let i = 0; i < 20; i++) {
|
|
16940
|
-
if (
|
|
16941
|
-
const browseDir =
|
|
17495
|
+
if (fs18.existsSync(path15.join(dir, ".git")) || fs18.existsSync(path15.join(dir, ".claude"))) {
|
|
17496
|
+
const browseDir = path15.join(dir, ".browse");
|
|
16942
17497
|
try {
|
|
16943
|
-
|
|
16944
|
-
const gi =
|
|
16945
|
-
if (!
|
|
16946
|
-
|
|
17498
|
+
fs18.mkdirSync(browseDir, { recursive: true });
|
|
17499
|
+
const gi = path15.join(browseDir, ".gitignore");
|
|
17500
|
+
if (!fs18.existsSync(gi)) {
|
|
17501
|
+
fs18.writeFileSync(gi, "*\n");
|
|
16947
17502
|
}
|
|
16948
17503
|
} catch {
|
|
16949
17504
|
}
|
|
16950
17505
|
return browseDir;
|
|
16951
17506
|
}
|
|
16952
|
-
const parent =
|
|
17507
|
+
const parent = path15.dirname(dir);
|
|
16953
17508
|
if (parent === dir) break;
|
|
16954
17509
|
dir = parent;
|
|
16955
17510
|
}
|
|
16956
17511
|
return "/tmp";
|
|
16957
17512
|
}
|
|
16958
17513
|
var LOCAL_DIR3 = resolveLocalDir();
|
|
16959
|
-
var STATE_FILE2 = process.env.BROWSE_STATE_FILE ||
|
|
17514
|
+
var STATE_FILE2 = process.env.BROWSE_STATE_FILE || path15.join(LOCAL_DIR3, `browse-server${INSTANCE_SUFFIX2}.json`);
|
|
16960
17515
|
var MAX_START_WAIT = parseInt(process.env.BROWSE_START_TIMEOUT || "0", 10) || 8e3;
|
|
16961
17516
|
var LOCK_FILE = STATE_FILE2 + ".lock";
|
|
16962
17517
|
var LOCK_STALE_MS = DEFAULTS.LOCK_STALE_THRESHOLD_MS;
|
|
16963
17518
|
var __filename_cli = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
16964
|
-
var __dirname_cli =
|
|
17519
|
+
var __dirname_cli = path15.dirname(__filename_cli);
|
|
16965
17520
|
function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
16966
17521
|
if (env.BROWSE_SERVER_SCRIPT) {
|
|
16967
17522
|
return env.BROWSE_SERVER_SCRIPT;
|
|
16968
17523
|
}
|
|
16969
17524
|
if (metaDir.startsWith("/")) {
|
|
16970
|
-
const direct =
|
|
16971
|
-
if (
|
|
17525
|
+
const direct = path15.resolve(metaDir, "server.ts");
|
|
17526
|
+
if (fs18.existsSync(direct)) {
|
|
16972
17527
|
return direct;
|
|
16973
17528
|
}
|
|
16974
17529
|
}
|
|
16975
17530
|
const selfPath = (0, import_url3.fileURLToPath)(__import_meta_url);
|
|
16976
|
-
if (
|
|
17531
|
+
if (fs18.existsSync(selfPath)) {
|
|
16977
17532
|
return "__self__";
|
|
16978
17533
|
}
|
|
16979
17534
|
throw new Error(
|
|
@@ -16983,7 +17538,7 @@ function resolveServerScript(env = process.env, metaDir = __dirname_cli) {
|
|
|
16983
17538
|
var SERVER_SCRIPT = resolveServerScript();
|
|
16984
17539
|
function readState() {
|
|
16985
17540
|
try {
|
|
16986
|
-
const data =
|
|
17541
|
+
const data = fs18.readFileSync(STATE_FILE2, "utf-8");
|
|
16987
17542
|
return JSON.parse(data);
|
|
16988
17543
|
} catch {
|
|
16989
17544
|
return null;
|
|
@@ -16999,7 +17554,7 @@ function isProcessAlive(pid) {
|
|
|
16999
17554
|
}
|
|
17000
17555
|
async function listInstances() {
|
|
17001
17556
|
try {
|
|
17002
|
-
const files =
|
|
17557
|
+
const files = fs18.readdirSync(LOCAL_DIR3).filter(
|
|
17003
17558
|
(f) => f.startsWith("browse-server") && f.endsWith(".json") && !f.endsWith(".lock")
|
|
17004
17559
|
);
|
|
17005
17560
|
if (files.length === 0) {
|
|
@@ -17009,7 +17564,7 @@ async function listInstances() {
|
|
|
17009
17564
|
let found = false;
|
|
17010
17565
|
for (const file of files) {
|
|
17011
17566
|
try {
|
|
17012
|
-
const data = JSON.parse(
|
|
17567
|
+
const data = JSON.parse(fs18.readFileSync(path15.join(LOCAL_DIR3, file), "utf-8"));
|
|
17013
17568
|
if (!data.pid || !data.port) continue;
|
|
17014
17569
|
const alive = isProcessAlive(data.pid);
|
|
17015
17570
|
let status = "dead";
|
|
@@ -17032,7 +17587,7 @@ async function listInstances() {
|
|
|
17032
17587
|
found = true;
|
|
17033
17588
|
if (!alive) {
|
|
17034
17589
|
try {
|
|
17035
|
-
|
|
17590
|
+
fs18.unlinkSync(path15.join(LOCAL_DIR3, file));
|
|
17036
17591
|
} catch {
|
|
17037
17592
|
}
|
|
17038
17593
|
}
|
|
@@ -17055,15 +17610,15 @@ function isBrowseProcess(pid) {
|
|
|
17055
17610
|
}
|
|
17056
17611
|
function acquireLock() {
|
|
17057
17612
|
try {
|
|
17058
|
-
|
|
17613
|
+
fs18.writeFileSync(LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
17059
17614
|
return true;
|
|
17060
17615
|
} catch (err) {
|
|
17061
17616
|
if (err.code === "EEXIST") {
|
|
17062
17617
|
try {
|
|
17063
|
-
const stat =
|
|
17618
|
+
const stat = fs18.statSync(LOCK_FILE);
|
|
17064
17619
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
17065
17620
|
try {
|
|
17066
|
-
|
|
17621
|
+
fs18.unlinkSync(LOCK_FILE);
|
|
17067
17622
|
} catch {
|
|
17068
17623
|
}
|
|
17069
17624
|
return acquireLock();
|
|
@@ -17077,7 +17632,7 @@ function acquireLock() {
|
|
|
17077
17632
|
}
|
|
17078
17633
|
function releaseLock() {
|
|
17079
17634
|
try {
|
|
17080
|
-
|
|
17635
|
+
fs18.unlinkSync(LOCK_FILE);
|
|
17081
17636
|
} catch {
|
|
17082
17637
|
}
|
|
17083
17638
|
}
|
|
@@ -17094,7 +17649,7 @@ async function startServer() {
|
|
|
17094
17649
|
}
|
|
17095
17650
|
await sleep(100);
|
|
17096
17651
|
}
|
|
17097
|
-
if (!
|
|
17652
|
+
if (!fs18.existsSync(LOCK_FILE) || fs18.readFileSync(LOCK_FILE, "utf-8").trim() !== String(process.pid)) {
|
|
17098
17653
|
const state = readState();
|
|
17099
17654
|
if (state && isProcessAlive(state.pid)) return state;
|
|
17100
17655
|
throw new Error("Server failed to start (another process is starting it)");
|
|
@@ -17104,7 +17659,7 @@ async function startServer() {
|
|
|
17104
17659
|
try {
|
|
17105
17660
|
const oldState = readState();
|
|
17106
17661
|
if (oldState && !isProcessAlive(oldState.pid)) {
|
|
17107
|
-
|
|
17662
|
+
fs18.unlinkSync(STATE_FILE2);
|
|
17108
17663
|
}
|
|
17109
17664
|
} catch {
|
|
17110
17665
|
}
|
|
@@ -17190,7 +17745,7 @@ async function ensureServer() {
|
|
|
17190
17745
|
}
|
|
17191
17746
|
if (state) {
|
|
17192
17747
|
try {
|
|
17193
|
-
|
|
17748
|
+
fs18.unlinkSync(STATE_FILE2);
|
|
17194
17749
|
} catch {
|
|
17195
17750
|
}
|
|
17196
17751
|
}
|
|
@@ -17200,21 +17755,21 @@ async function ensureServer() {
|
|
|
17200
17755
|
}
|
|
17201
17756
|
function cleanOrphanedServers() {
|
|
17202
17757
|
try {
|
|
17203
|
-
const files =
|
|
17758
|
+
const files = fs18.readdirSync(LOCAL_DIR3);
|
|
17204
17759
|
for (const file of files) {
|
|
17205
17760
|
if (!file.startsWith("browse-server") || !file.endsWith(".json") || file.endsWith(".lock")) continue;
|
|
17206
|
-
const filePath =
|
|
17761
|
+
const filePath = path15.join(LOCAL_DIR3, file);
|
|
17207
17762
|
if (filePath === STATE_FILE2) continue;
|
|
17208
17763
|
try {
|
|
17209
|
-
const data = JSON.parse(
|
|
17764
|
+
const data = JSON.parse(fs18.readFileSync(filePath, "utf-8"));
|
|
17210
17765
|
if (!data.pid) {
|
|
17211
|
-
|
|
17766
|
+
fs18.unlinkSync(filePath);
|
|
17212
17767
|
continue;
|
|
17213
17768
|
}
|
|
17214
17769
|
const suffixMatch = file.match(/browse-server-(\d+)\.json$/);
|
|
17215
17770
|
if (suffixMatch && data.port === parseInt(suffixMatch[1], 10) && isProcessAlive(data.pid)) continue;
|
|
17216
17771
|
if (!isProcessAlive(data.pid)) {
|
|
17217
|
-
|
|
17772
|
+
fs18.unlinkSync(filePath);
|
|
17218
17773
|
continue;
|
|
17219
17774
|
}
|
|
17220
17775
|
if (isBrowseProcess(data.pid)) {
|
|
@@ -17225,7 +17780,7 @@ function cleanOrphanedServers() {
|
|
|
17225
17780
|
}
|
|
17226
17781
|
} catch {
|
|
17227
17782
|
try {
|
|
17228
|
-
|
|
17783
|
+
fs18.unlinkSync(filePath);
|
|
17229
17784
|
} catch {
|
|
17230
17785
|
}
|
|
17231
17786
|
}
|
|
@@ -17331,7 +17886,7 @@ async function sendCommand(state, command, args, retries = 0, sessionId) {
|
|
|
17331
17886
|
await sleep(300);
|
|
17332
17887
|
}
|
|
17333
17888
|
try {
|
|
17334
|
-
|
|
17889
|
+
fs18.unlinkSync(STATE_FILE2);
|
|
17335
17890
|
} catch {
|
|
17336
17891
|
}
|
|
17337
17892
|
if (command === "restart") {
|
|
@@ -17650,6 +18205,10 @@ React: react-devtools enable|disable|tree|props|suspense|errors
|
|
|
17650
18205
|
Providers: provider save|list|delete <name> [api-key]
|
|
17651
18206
|
Detect: detect (frameworks, CDN, third-party, SaaS, infra)
|
|
17652
18207
|
Performance: perf-audit [url] [--no-coverage] [--no-detect] [--json]
|
|
18208
|
+
perf-audit save [name] Save audit report to .browse/audits/
|
|
18209
|
+
perf-audit compare <base> [curr] Compare saved audit vs current or another
|
|
18210
|
+
perf-audit list List saved audit reports
|
|
18211
|
+
perf-audit delete <name> Delete a saved audit report
|
|
17653
18212
|
Debug: inspect (requires BROWSE_DEBUG_PORT)
|
|
17654
18213
|
Server: status | instances | stop | restart | doctor | upgrade
|
|
17655
18214
|
Setup: install-skill [path]
|
|
@@ -17704,7 +18263,7 @@ if (process.argv.includes("--mcp")) {
|
|
|
17704
18263
|
Promise.resolve().then(() => (init_mcp(), mcp_exports)).then((m) => m.startMcpServer(jsonMode));
|
|
17705
18264
|
} else if (process.env.__BROWSE_SERVER_MODE === "1") {
|
|
17706
18265
|
Promise.resolve().then(() => init_server());
|
|
17707
|
-
} else if (process.argv[1] &&
|
|
18266
|
+
} else if (process.argv[1] && fs18.realpathSync(process.argv[1]) === fs18.realpathSync(__filename_cli)) {
|
|
17708
18267
|
main().catch((err) => {
|
|
17709
18268
|
console.error(`[browse] ${err.message}`);
|
|
17710
18269
|
process.exit(1);
|