@seybel/breakhound 1.0.1 → 1.0.3
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 +18 -5
- package/dist/cli.js +164 -35
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/schema.json +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,13 @@ In your project folder:
|
|
|
45
45
|
npx @seybel/breakhound
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
That's it: no config.
|
|
48
|
+
That's it: no config. npx runs it without installing anything. To type `breakhound` directly instead, install it globally:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm i -g @seybel/breakhound
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or add it to your project with `npm i -D @seybel/breakhound`, so everyone gets the same version, and run it as `npx breakhound` or from an npm script.
|
|
49
55
|
|
|
50
56
|
If your dev server is already running it's used, otherwise breakhound starts it with your `dev` script and stops it when done. Chromium is downloaded automatically the first time (about 150 MB).
|
|
51
57
|
|
|
@@ -72,7 +78,13 @@ npx @seybel/breakhound --url https://staging.example.com # every route, on ano
|
|
|
72
78
|
| Angular | your route definitions, from `src/main.ts` on, with lazily loaded `loadChildren` routes under their path |
|
|
73
79
|
| Anything else | links followed from the home page (up to 50 pages, a few per section) |
|
|
74
80
|
|
|
75
|
-
**Dynamic routes** like `/blog/[slug]` are scanned once, using a real link found on your pages (routes that share a template share a layout).
|
|
81
|
+
**Dynamic routes** like `/blog/[slug]` are scanned once, using a real link found on your pages (routes that share a template share a layout). Routes below one reuse its values: once `/cohorts/css-101` is found, `/cohorts/[slug]/settings` is scanned as `/cohorts/css-101/settings`. If something is still missing, it's listed with a hint naming the parameters. Give them a real value once, by name, and every route that uses them is filled in:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
params: { slug: 'css-101', submissionId: '42' }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or give one route its own URL, which wins over `params`:
|
|
76
88
|
|
|
77
89
|
```ts
|
|
78
90
|
samples: { '/products/[id]': '/products/42' }
|
|
@@ -175,7 +187,7 @@ breakhound doesn't edit your code: your own agent does, and breakhound checks it
|
|
|
175
187
|
npx @seybel/breakhound mcp install
|
|
176
188
|
```
|
|
177
189
|
|
|
178
|
-
It finds Claude Code, Cursor, VS Code, Windsurf and Codex, shows what it will add to their MCP settings, and asks first. Then ask your agent to fix the responsive issues. It can:
|
|
190
|
+
It finds Claude Code, Cursor, VS Code, Antigravity, Windsurf and Codex, shows what it will add to their MCP settings, and asks first. Then ask your agent to fix the responsive issues. It can:
|
|
179
191
|
|
|
180
192
|
| Tool | What it does |
|
|
181
193
|
|---|---|
|
|
@@ -290,7 +302,8 @@ Running breakhound through `npx` without installing it, you get `breakhound.conf
|
|
|
290
302
|
| `baseUrl` | Scan this server instead of finding or starting the dev server |
|
|
291
303
|
| `routes` | Scan exactly these routes instead of discovering them |
|
|
292
304
|
| `include`, `exclude` | Route globs: `*` is one segment, `**` any number |
|
|
293
|
-
| `samples` | A real URL for
|
|
305
|
+
| `samples` | A real URL for a dynamic route |
|
|
306
|
+
| `params` | Real values for dynamic route parameters by name, for every route that uses them |
|
|
294
307
|
| `maxRoutes` | Most routes per run (default 100) |
|
|
295
308
|
| `server` | How to start the app: `{ command: 'npm run preview', port: 4173, timeout: 120 }` |
|
|
296
309
|
| `widths` | `{ min, max, step }` in CSS pixels |
|
|
@@ -312,7 +325,7 @@ You can also mark elements in your markup:
|
|
|
312
325
|
|
|
313
326
|
## CLI
|
|
314
327
|
|
|
315
|
-
Run these with `npx @seybel/breakhound`, or as `breakhound`
|
|
328
|
+
Run these with `npx @seybel/breakhound`, or as `breakhound` after `npm i -g @seybel/breakhound`. Installed in a project, it's `npx breakhound` or an npm script.
|
|
316
329
|
|
|
317
330
|
```
|
|
318
331
|
breakhound Find your app and scan every route
|
package/dist/cli.js
CHANGED
|
@@ -1303,6 +1303,24 @@ function plural(n, word) {
|
|
|
1303
1303
|
function routeLabel(route) {
|
|
1304
1304
|
return route.route || new URL(route.url).pathname;
|
|
1305
1305
|
}
|
|
1306
|
+
function leftOutHint(routes) {
|
|
1307
|
+
const skipped = routes.filter((r) => r.status === "skipped");
|
|
1308
|
+
if (!skipped.length) return null;
|
|
1309
|
+
const names = /* @__PURE__ */ new Set();
|
|
1310
|
+
for (const r of skipped) {
|
|
1311
|
+
const named = r.error?.match(/nothing gives (.+) a value/)?.[1] ?? "";
|
|
1312
|
+
for (const m of named.matchAll(/\[([^\]]+)\]/g)) names.add(m[1]);
|
|
1313
|
+
}
|
|
1314
|
+
const key = (n) => /^[A-Za-z_$][\w$]*$/.test(n) ? n : `'${n}'`;
|
|
1315
|
+
if (names.size) return `params: { ${[...names].map((n) => `${key(n)}: '1'`).join(", ")} }`;
|
|
1316
|
+
const first = skipped[0].route;
|
|
1317
|
+
const example = first.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, "a/b").replace(/\[\[?[^\]]+\]\]?/g, "1");
|
|
1318
|
+
return `samples: { '${first}': '${example}' }`;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ../report/src/brand.ts
|
|
1322
|
+
var MARK_PNG = "data:image/png;base64,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";
|
|
1323
|
+
var WORDMARK_PNG = "data:image/png;base64,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";
|
|
1306
1324
|
|
|
1307
1325
|
// ../report/src/markdown.ts
|
|
1308
1326
|
var SEVERITY_ICON = { error: "\u2716", warning: "\u26A0", info: "\u2139" };
|
|
@@ -1364,7 +1382,7 @@ function renderSummary(result) {
|
|
|
1364
1382
|
[
|
|
1365
1383
|
"Routes left out",
|
|
1366
1384
|
result.routes.filter((r) => r.status === "redirect" || r.status === "skipped"),
|
|
1367
|
-
result.routes
|
|
1385
|
+
leftOutHint(result.routes) ? `Give dynamic routes real values in your config, e.g. \`${leftOutHint(result.routes)}\` (replace the 1s with real ones).` : void 0
|
|
1368
1386
|
]
|
|
1369
1387
|
];
|
|
1370
1388
|
for (const [title, routes, hint] of sections) {
|
|
@@ -1547,6 +1565,7 @@ function buildData(result, options) {
|
|
|
1547
1565
|
baseUrl: result.project.baseUrl
|
|
1548
1566
|
},
|
|
1549
1567
|
summary: result.summary,
|
|
1568
|
+
leftOutHint: leftOutHint(result.routes),
|
|
1550
1569
|
routes: result.routes.map((r) => ({
|
|
1551
1570
|
route: routeLabel(r),
|
|
1552
1571
|
url: r.url,
|
|
@@ -1586,7 +1605,7 @@ function renderHtml(result, options = {}) {
|
|
|
1586
1605
|
<body>
|
|
1587
1606
|
<header class="top">
|
|
1588
1607
|
<div class="wrap top-inner">
|
|
1589
|
-
<div class="brand"
|
|
1608
|
+
<div class="brand" role="img" aria-label="breakhound"><span class="mark"></span><span class="word"></span></div>
|
|
1590
1609
|
<div class="top-actions">
|
|
1591
1610
|
<button type="button" class="btn primary" id="copy-all">Copy fix prompt</button>
|
|
1592
1611
|
</div>
|
|
@@ -1657,8 +1676,10 @@ code, .mono { font-family: var(--mono); font-size: 13px; }
|
|
|
1657
1676
|
|
|
1658
1677
|
.top { position: sticky; top: 0; z-index: 10; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid var(--border); }
|
|
1659
1678
|
.top-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 56px; }
|
|
1660
|
-
.brand { display: flex; align-items: center; gap:
|
|
1661
|
-
.mark {
|
|
1679
|
+
.brand { display: flex; align-items: center; gap: 7px; color: var(--text); }
|
|
1680
|
+
.brand .mark, .brand .word { display: inline-block; background: currentColor; mask-position: center; mask-size: contain; mask-repeat: no-repeat; }
|
|
1681
|
+
.brand .mark { width: 27px; height: 20px; mask-image: url("${MARK_PNG}"); }
|
|
1682
|
+
.brand .word { width: 95px; height: 18px; mask-image: url("${WORDMARK_PNG}"); }
|
|
1662
1683
|
.btn { border: 1px solid var(--border); background: var(--surface); border-radius: 8px; padding: 7px 12px; cursor: pointer; white-space: nowrap; }
|
|
1663
1684
|
.btn:hover { border-color: var(--tick); }
|
|
1664
1685
|
.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 600; }
|
|
@@ -1773,7 +1794,7 @@ dd code { background: var(--surface-2); padding: 1px 5px; border-radius: 4px; }
|
|
|
1773
1794
|
a { color: inherit; }
|
|
1774
1795
|
}
|
|
1775
1796
|
`;
|
|
1776
|
-
var JS =
|
|
1797
|
+
var JS = `
|
|
1777
1798
|
(() => {
|
|
1778
1799
|
const D = JSON.parse(document.getElementById('bh-data').textContent);
|
|
1779
1800
|
const $ = (id) => document.getElementById(id);
|
|
@@ -1792,7 +1813,7 @@ var JS = String.raw`
|
|
|
1792
1813
|
for (const kid of kids.flat(Infinity)) if (kid != null && kid !== false) n.append(kid);
|
|
1793
1814
|
return n;
|
|
1794
1815
|
};
|
|
1795
|
-
const range = (r) => (r.from === r.to ? r.from + 'px' : r.from + '
|
|
1816
|
+
const range = (r) => (r.from === r.to ? r.from + 'px' : r.from + '\u2013' + r.to + 'px');
|
|
1796
1817
|
const ranges = (rs) => rs.map(range).join(', ');
|
|
1797
1818
|
const plural = (n, w) => n + ' ' + w + (n === 1 ? '' : 's');
|
|
1798
1819
|
|
|
@@ -1818,10 +1839,10 @@ var JS = String.raw`
|
|
|
1818
1839
|
const started = new Date(D.meta.startedAt);
|
|
1819
1840
|
const scanned = D.routes.filter((r) => r.status === 'scanned');
|
|
1820
1841
|
$('meta').textContent = [
|
|
1821
|
-
D.meta.baseUrl, plural(scanned.length, 'page'), min + '
|
|
1842
|
+
D.meta.baseUrl, plural(scanned.length, 'page'), min + '\u2013' + max + 'px',
|
|
1822
1843
|
started.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }),
|
|
1823
1844
|
(D.meta.durationMs / 1000).toFixed(1) + 's',
|
|
1824
|
-
].filter(Boolean).join('
|
|
1845
|
+
].filter(Boolean).join(' \xB7 ');
|
|
1825
1846
|
$('version').textContent = D.meta.version;
|
|
1826
1847
|
$('copy-all').addEventListener('click', () => copy(D.prompt, 'Fix prompt copied. Paste it into your coding agent.'));
|
|
1827
1848
|
if (!s.issues) $('copy-all').hidden = true;
|
|
@@ -1891,11 +1912,9 @@ var JS = String.raw`
|
|
|
1891
1912
|
}
|
|
1892
1913
|
const skipped = D.routes.filter((r) => r.status === 'skipped' || r.status === 'redirect');
|
|
1893
1914
|
if (skipped.length) {
|
|
1894
|
-
const dynamic = skipped.find((r) => r.status === 'skipped');
|
|
1895
|
-
const example = dynamic && dynamic.route.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, 'a/b').replace(/\[\[?[^\]]+\]\]?/g, '1');
|
|
1896
1915
|
$('failed').append(el('div', { class: 'callout' },
|
|
1897
1916
|
el('h2', { text: plural(skipped.length, 'route') + ' left out' }),
|
|
1898
|
-
|
|
1917
|
+
D.leftOutHint ? el('p', {}, 'Give dynamic routes real values in your config (replace the 1s with real ones): ', el('code', { text: D.leftOutHint })) : false,
|
|
1899
1918
|
listing(skipped)));
|
|
1900
1919
|
}
|
|
1901
1920
|
const shaky = D.routes.filter((r) => r.warnings.length);
|
|
@@ -2217,9 +2236,6 @@ function globMatches(glob, path) {
|
|
|
2217
2236
|
}
|
|
2218
2237
|
return new RegExp(`^${source}/?$`).test(path);
|
|
2219
2238
|
}
|
|
2220
|
-
function examplePath(path) {
|
|
2221
|
-
return path.replace(/\[\[?\.\.\.[^\]]+\]\]?/g, "a/b").replace(/\[\[?[^\]]+\]\]?/g, "1");
|
|
2222
|
-
}
|
|
2223
2239
|
|
|
2224
2240
|
// ../routes/src/syntax.ts
|
|
2225
2241
|
function joinPaths(base, path) {
|
|
@@ -3055,26 +3071,44 @@ function detectProject(cwd) {
|
|
|
3055
3071
|
}
|
|
3056
3072
|
|
|
3057
3073
|
// ../routes/src/queue.ts
|
|
3074
|
+
function namesIn(label) {
|
|
3075
|
+
return [...label.matchAll(/\[\[?(?:\.\.\.)?([^\]]+)\]\]?/g)].map((m) => m[1]);
|
|
3076
|
+
}
|
|
3077
|
+
function isPlainParam(s) {
|
|
3078
|
+
return s.kind === "param" && !s.optional && namesIn(s.label).length === 1 && s.label.startsWith("[") && s.label.endsWith("]");
|
|
3079
|
+
}
|
|
3080
|
+
function prefixOf(segments, i) {
|
|
3081
|
+
return `/${segments.slice(0, i + 1).map((s) => s.kind === "static" ? s.value : s.label).join("/")}`;
|
|
3082
|
+
}
|
|
3058
3083
|
function trimSlash(path) {
|
|
3059
3084
|
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
3060
3085
|
}
|
|
3061
3086
|
var RouteQueue = class {
|
|
3062
|
-
constructor(baseUrl, samples, crawler, hashRouting = false) {
|
|
3087
|
+
constructor(baseUrl, samples, crawler, hashRouting = false, params = {}) {
|
|
3063
3088
|
this.baseUrl = baseUrl;
|
|
3064
3089
|
this.samples = samples;
|
|
3065
3090
|
this.crawler = crawler;
|
|
3066
3091
|
this.hashRouting = hashRouting;
|
|
3092
|
+
this.params = params;
|
|
3067
3093
|
this.basePath = baseUrl ? new URL(baseUrl).pathname.replace(/\/+$/, "") : "";
|
|
3068
3094
|
}
|
|
3069
3095
|
baseUrl;
|
|
3070
3096
|
samples;
|
|
3071
3097
|
crawler;
|
|
3072
3098
|
hashRouting;
|
|
3099
|
+
params;
|
|
3073
3100
|
jobs = [];
|
|
3074
3101
|
/** App paths already scanned or planned, so redirects to them aren't scanned twice. */
|
|
3075
3102
|
planned = /* @__PURE__ */ new Set();
|
|
3076
3103
|
/** Dynamic routes still waiting for a link that fits them. */
|
|
3077
3104
|
waiting = /* @__PURE__ */ new Map();
|
|
3105
|
+
/** For routes left waiting at the end: the parameters nothing gave a value. */
|
|
3106
|
+
missing = /* @__PURE__ */ new Map();
|
|
3107
|
+
/**
|
|
3108
|
+
* Values parameters took in real URLs, under the route they belong to: `/cohorts/[slug]` →
|
|
3109
|
+
* `css-101`. Routes below it reuse them, since a child of a real page is real too.
|
|
3110
|
+
*/
|
|
3111
|
+
seen = /* @__PURE__ */ new Map();
|
|
3078
3112
|
patterns = /* @__PURE__ */ new Map();
|
|
3079
3113
|
order = 0;
|
|
3080
3114
|
/** Where the app is mounted on its server, e.g. "/docs"; "" at the root. */
|
|
@@ -3128,17 +3162,84 @@ var RouteQueue = class {
|
|
|
3128
3162
|
return;
|
|
3129
3163
|
}
|
|
3130
3164
|
const sample = this.samples[pattern.path];
|
|
3131
|
-
if (sample)
|
|
3132
|
-
|
|
3165
|
+
if (sample) {
|
|
3166
|
+
const samplePath = /^https?:\/\//i.test(sample) ? this.appPath(new URL(sample).pathname) : sample;
|
|
3167
|
+
if (samplePath !== null) this.learn(pattern, samplePath);
|
|
3168
|
+
this.add({ url: this.urlFor(sample), route: pattern.path });
|
|
3169
|
+
} else this.waiting.set(pattern.path, pattern);
|
|
3170
|
+
}
|
|
3171
|
+
/** Remembers the values a real URL gives a route's parameters. */
|
|
3172
|
+
learn(pattern, path) {
|
|
3173
|
+
if (pattern.segments.some((s) => s.kind === "rest" || s.kind === "param" && s.optional))
|
|
3174
|
+
return;
|
|
3175
|
+
const parts = path.split(/[?#]/)[0].split("/").filter(Boolean);
|
|
3176
|
+
if (parts.length !== pattern.segments.length) return;
|
|
3177
|
+
pattern.segments.forEach((s, i) => {
|
|
3178
|
+
if (isPlainParam(s)) {
|
|
3179
|
+
const key = prefixOf(pattern.segments, i);
|
|
3180
|
+
if (!this.seen.has(key)) this.seen.set(key, parts[i]);
|
|
3181
|
+
}
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* A URL for a dynamic route from values already known: a parent route's real URL, then the
|
|
3186
|
+
* config's params by name. Null, with the names still needed, when something is missing.
|
|
3187
|
+
*/
|
|
3188
|
+
build(pattern) {
|
|
3189
|
+
const parts = [];
|
|
3190
|
+
const missing = [];
|
|
3191
|
+
pattern.segments.forEach((s, i) => {
|
|
3192
|
+
if (s.kind === "static") {
|
|
3193
|
+
parts.push(s.value);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
const inherited = isPlainParam(s) ? this.seen.get(prefixOf(pattern.segments, i)) : void 0;
|
|
3197
|
+
if (inherited !== void 0) {
|
|
3198
|
+
parts.push(inherited);
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
const names = namesIn(s.label);
|
|
3202
|
+
const absent = names.filter((n) => this.params[n] === void 0);
|
|
3203
|
+
if (absent.length) {
|
|
3204
|
+
if (!s.optional) missing.push(...absent);
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
let value = s.label;
|
|
3208
|
+
for (const n of names)
|
|
3209
|
+
value = value.replace(/\[\[?(?:\.\.\.)?[^\]]+\]\]?/, () => this.params[n]);
|
|
3210
|
+
parts.push(value.replace(/^\/+|\/+$/g, ""));
|
|
3211
|
+
});
|
|
3212
|
+
return missing.length ? { missing: [...new Set(missing)] } : { path: `/${parts.join("/")}` };
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* Gives the routes no link fitted a URL from known values, once the links have had their
|
|
3216
|
+
* chance. Returns how many were added; the rest stay waiting, with what they're missing.
|
|
3217
|
+
*/
|
|
3218
|
+
fillWaiting() {
|
|
3219
|
+
let added = 0;
|
|
3220
|
+
for (const [key, pattern] of this.waiting) {
|
|
3221
|
+
const built = this.build(pattern);
|
|
3222
|
+
if ("missing" in built) {
|
|
3223
|
+
this.missing.set(key, built.missing);
|
|
3224
|
+
continue;
|
|
3225
|
+
}
|
|
3226
|
+
this.waiting.delete(key);
|
|
3227
|
+
this.missing.delete(key);
|
|
3228
|
+
this.add({ url: this.urlFor(built.path), route: pattern.path });
|
|
3229
|
+
added++;
|
|
3230
|
+
}
|
|
3231
|
+
return added;
|
|
3133
3232
|
}
|
|
3134
3233
|
/** Links from a scanned page: samples for dynamic routes, and new pages when crawling. */
|
|
3135
3234
|
addLinks(found) {
|
|
3136
3235
|
if (!this.baseUrl) return;
|
|
3137
3236
|
const links = found.map((link) => this.linkPath(link)).filter((p) => p !== null && isCrawlable(p));
|
|
3138
|
-
if (this.
|
|
3237
|
+
if (this.patterns.size) {
|
|
3139
3238
|
for (const link of links) {
|
|
3140
3239
|
const route = routeFor(link, this.patterns.values());
|
|
3141
|
-
if (route
|
|
3240
|
+
if (!route?.dynamic) continue;
|
|
3241
|
+
this.learn(route, link);
|
|
3242
|
+
if (this.waiting.has(route.path)) {
|
|
3142
3243
|
this.waiting.delete(route.path);
|
|
3143
3244
|
this.add({ url: this.urlFor(link), route: route.path });
|
|
3144
3245
|
}
|
|
@@ -4193,6 +4294,7 @@ var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
|
4193
4294
|
"include",
|
|
4194
4295
|
"exclude",
|
|
4195
4296
|
"samples",
|
|
4297
|
+
"params",
|
|
4196
4298
|
"maxRoutes",
|
|
4197
4299
|
"server",
|
|
4198
4300
|
"widths",
|
|
@@ -4239,6 +4341,14 @@ function validateConfig(config) {
|
|
|
4239
4341
|
);
|
|
4240
4342
|
}
|
|
4241
4343
|
}
|
|
4344
|
+
if (config.params !== void 0) {
|
|
4345
|
+
const ok = config.params && typeof config.params === "object" && !Array.isArray(config.params) && Object.values(config.params).every((v) => typeof v === "string" && v !== "");
|
|
4346
|
+
if (!ok) {
|
|
4347
|
+
throw new Error(
|
|
4348
|
+
`params must give dynamic route parameters a real value by name, like { "slug": "css-101" }`
|
|
4349
|
+
);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4242
4352
|
if (config.states !== void 0 && typeof config.states !== "boolean" && !(Number.isInteger(config.states) && config.states >= 0)) {
|
|
4243
4353
|
throw new Error("states must be true, false, or the most menus and dialogs to open per page");
|
|
4244
4354
|
}
|
|
@@ -4707,7 +4817,9 @@ var SETUP = `breakhound fix needs a model to write the fixes. Use one of:
|
|
|
4707
4817
|
ANTHROPIC_API_KEY=... (your Anthropic key; uses ${DEFAULT_ANTHROPIC_MODEL} unless you pass --model)
|
|
4708
4818
|
OPENAI_API_KEY=... with --model <name> (OpenAI, or any compatible API via OPENAI_BASE_URL)
|
|
4709
4819
|
Ollama running locally, free: https://ollama.com, then for example: ollama pull qwen2.5-coder
|
|
4710
|
-
|
|
4820
|
+
Using a coding agent (Claude Code, Cursor, Antigravity...)? It doesn't need a model here: run
|
|
4821
|
+
npx @seybel/breakhound mcp install once, then ask your agent to fix the breakhound issues.
|
|
4822
|
+
Or paste npx @seybel/breakhound prompt --copy into any agent.`;
|
|
4711
4823
|
async function resolveModel(options = {}) {
|
|
4712
4824
|
const env = options.env ?? process.env;
|
|
4713
4825
|
const timeoutMs = options.timeoutMs ?? 3e5;
|
|
@@ -4883,13 +4995,12 @@ function printNotes(result, hasSavedLogin) {
|
|
|
4883
4995
|
` ${styleText4("yellow", "\u25CB")} ${plural22(login, "page needs", "pages need")} a login. ${how}`
|
|
4884
4996
|
);
|
|
4885
4997
|
}
|
|
4886
|
-
|
|
4998
|
+
const hint = leftOutHint(skipped);
|
|
4999
|
+
if (hint) {
|
|
4887
5000
|
console.log(
|
|
4888
|
-
` ${styleText4("dim", "\u2013")} ${plural22(skipped.length, "dynamic route", "dynamic routes")} had no link to follow. Give
|
|
5001
|
+
` ${styleText4("dim", "\u2013")} ${plural22(skipped.length, "dynamic route", "dynamic routes")} had no link to follow. Give them real values in your config (replace the 1s), e.g.`
|
|
4889
5002
|
);
|
|
4890
|
-
|
|
4891
|
-
if (first)
|
|
4892
|
-
console.log(styleText4("dim", ` samples: { '${first}': '${examplePath(first)}' }`));
|
|
5003
|
+
console.log(styleText4("dim", ` ${hint}`));
|
|
4893
5004
|
}
|
|
4894
5005
|
}
|
|
4895
5006
|
|
|
@@ -5101,6 +5212,9 @@ async function pool(items, size, work) {
|
|
|
5101
5212
|
};
|
|
5102
5213
|
await Promise.all(Array.from({ length: Math.min(size, items.length) }, worker));
|
|
5103
5214
|
}
|
|
5215
|
+
function leftOutReason(missing) {
|
|
5216
|
+
return missing.length ? `No page links to it, and nothing gives ${missing.map((n) => `[${n}]`).join(", ")} a value.` : "No page links to it, so there was no real URL to try.";
|
|
5217
|
+
}
|
|
5104
5218
|
async function drain(queue, size, work) {
|
|
5105
5219
|
const active = /* @__PURE__ */ new Set();
|
|
5106
5220
|
for (; ; ) {
|
|
@@ -5109,7 +5223,8 @@ async function drain(queue, size, work) {
|
|
|
5109
5223
|
const p = work(job).finally(() => active.delete(p));
|
|
5110
5224
|
active.add(p);
|
|
5111
5225
|
}
|
|
5112
|
-
if (!active.size) return;
|
|
5226
|
+
if (!active.size && !queue.fillWaiting()) return;
|
|
5227
|
+
if (!active.size) continue;
|
|
5113
5228
|
await Promise.race(active);
|
|
5114
5229
|
}
|
|
5115
5230
|
}
|
|
@@ -5157,7 +5272,8 @@ async function scan(req) {
|
|
|
5157
5272
|
app.baseUrl,
|
|
5158
5273
|
config.samples ?? {},
|
|
5159
5274
|
plan.crawl ? new Crawler(max2, (path) => isWanted(path, config)) : null,
|
|
5160
|
-
plan.hashRouting
|
|
5275
|
+
plan.hashRouting,
|
|
5276
|
+
config.params ?? {}
|
|
5161
5277
|
);
|
|
5162
5278
|
for (const url of plan.urls) queue.add({ url, route: routeOf(url, cwd) });
|
|
5163
5279
|
for (const p of plan.patterns) {
|
|
@@ -5305,7 +5421,7 @@ async function scan(req) {
|
|
|
5305
5421
|
url: "",
|
|
5306
5422
|
finalUrl: "",
|
|
5307
5423
|
status: "skipped",
|
|
5308
|
-
error:
|
|
5424
|
+
error: leftOutReason(queue.missing.get(pattern.path) ?? []),
|
|
5309
5425
|
pageErrors: [],
|
|
5310
5426
|
warnings: [],
|
|
5311
5427
|
breakpoints: { observed: [], declared: [], unexplained: [] },
|
|
@@ -5904,8 +6020,8 @@ var TEMPLATE = (baseUrl, typed) => `${typed ? "import { defineConfig } from '@se
|
|
|
5904
6020
|
// Every route is found automatically. To narrow it down:
|
|
5905
6021
|
// include: ['/blog/**'],
|
|
5906
6022
|
// exclude: ['/admin/**'],
|
|
5907
|
-
//
|
|
5908
|
-
//
|
|
6023
|
+
// Real values for dynamic route parameters no page links to, by name:
|
|
6024
|
+
// params: { slug: 'my-first-post', id: '42' },
|
|
5909
6025
|
|
|
5910
6026
|
// Viewport widths to sweep, in CSS pixels.
|
|
5911
6027
|
widths: { min: 320, max: 1920 },
|
|
@@ -6862,6 +6978,17 @@ function allClients(cwd, home = homedir()) {
|
|
|
6862
6978
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: VS Code variable.
|
|
6863
6979
|
cwd: "${workspaceFolder}"
|
|
6864
6980
|
}),
|
|
6981
|
+
// Antigravity's workspace config; its global one is ~/.gemini/config/mcp_config.json.
|
|
6982
|
+
jsonClient(
|
|
6983
|
+
"antigravity",
|
|
6984
|
+
"Antigravity",
|
|
6985
|
+
join17(cwd, ".agents", "mcp_config.json"),
|
|
6986
|
+
"mcpServers",
|
|
6987
|
+
{
|
|
6988
|
+
command,
|
|
6989
|
+
args
|
|
6990
|
+
}
|
|
6991
|
+
),
|
|
6865
6992
|
jsonClient(
|
|
6866
6993
|
"windsurf",
|
|
6867
6994
|
"Windsurf",
|
|
@@ -6877,6 +7004,7 @@ function detectClients(cwd, home = homedir()) {
|
|
|
6877
7004
|
claude: existsSync15(join17(cwd, ".mcp.json")) || existsSync15(join17(cwd, ".claude")) || existsSync15(join17(cwd, "CLAUDE.md")) || existsSync15(join17(home, ".claude")) || onPath("claude"),
|
|
6878
7005
|
cursor: existsSync15(join17(cwd, ".cursor")) || existsSync15(join17(home, ".cursor")),
|
|
6879
7006
|
vscode: existsSync15(join17(cwd, ".vscode")) || onPath("code"),
|
|
7007
|
+
antigravity: existsSync15(join17(cwd, ".agents", "mcp_config.json")) || existsSync15(join17(home, ".gemini", "config", "mcp_config.json")) || existsSync15(join17(home, ".gemini", "antigravity")) || onPath("antigravity"),
|
|
6880
7008
|
windsurf: existsSync15(join17(home, ".codeium", "windsurf")),
|
|
6881
7009
|
codex: existsSync15(join17(home, ".codex"))
|
|
6882
7010
|
};
|
|
@@ -6891,7 +7019,7 @@ async function installMcp(cwd, options = {}) {
|
|
|
6891
7019
|
const clients = options.clients?.length ? allClients(cwd, home).filter((c) => options.clients?.includes(c.id)) : detectClients(cwd, home);
|
|
6892
7020
|
if (!clients.length) {
|
|
6893
7021
|
console.log(
|
|
6894
|
-
"No coding agent found here. Pick one with --client claude, cursor, vscode, windsurf or codex, or add this to your agent's MCP settings:"
|
|
7022
|
+
"No coding agent found here. Pick one with --client claude, cursor, vscode, antigravity, windsurf or codex, or add this to your agent's MCP settings:"
|
|
6895
7023
|
);
|
|
6896
7024
|
console.log(JSON.stringify({ [SERVER_NAME]: serverCommand() }, null, 2));
|
|
6897
7025
|
return 1;
|
|
@@ -6974,7 +7102,7 @@ function readVersion() {
|
|
|
6974
7102
|
var VERSION2 = readVersion();
|
|
6975
7103
|
var HELP = `breakhound ${VERSION2}
|
|
6976
7104
|
Find where your layout breaks across every viewport width.
|
|
6977
|
-
Run it with npx @seybel/breakhound, or as breakhound
|
|
7105
|
+
Run it with npx @seybel/breakhound, or as breakhound after npm i -g @seybel/breakhound.
|
|
6978
7106
|
|
|
6979
7107
|
Usage
|
|
6980
7108
|
breakhound Find your app and scan every route
|
|
@@ -6992,7 +7120,8 @@ Fixing
|
|
|
6992
7120
|
breakhound undo [routes] Revert the fixes for these routes (--session for all); lists them with no routes
|
|
6993
7121
|
breakhound fix [routes] Fix the last scan's issues with your own model (Anthropic or OpenAI key, or
|
|
6994
7122
|
a local Ollama), verify each fix and commit it so it can be undone
|
|
6995
|
-
breakhound mcp install Add breakhound's MCP server to Claude Code, Cursor, VS Code,
|
|
7123
|
+
breakhound mcp install Add breakhound's MCP server to Claude Code, Cursor, VS Code, Antigravity,
|
|
7124
|
+
Windsurf or Codex
|
|
6996
7125
|
breakhound mcp Run the MCP server (your agent starts it)
|
|
6997
7126
|
|
|
6998
7127
|
Scan options
|
|
@@ -7030,7 +7159,7 @@ Fix options
|
|
|
7030
7159
|
--all Include info-level issues
|
|
7031
7160
|
|
|
7032
7161
|
MCP install options
|
|
7033
|
-
--client <name> claude, cursor, vscode, windsurf or codex (repeatable)
|
|
7162
|
+
--client <name> claude, cursor, vscode, antigravity, windsurf or codex (repeatable)
|
|
7034
7163
|
-y, --yes Don't ask before changing files
|
|
7035
7164
|
|
|
7036
7165
|
-h, --help Show this help
|
|
@@ -7120,7 +7249,7 @@ async function main(argv) {
|
|
|
7120
7249
|
if (command === "mcp") {
|
|
7121
7250
|
if (rest2[0] === "install") {
|
|
7122
7251
|
const clients = values.client ?? [];
|
|
7123
|
-
const known = ["claude", "cursor", "vscode", "windsurf", "codex"];
|
|
7252
|
+
const known = ["claude", "cursor", "vscode", "antigravity", "windsurf", "codex"];
|
|
7124
7253
|
const unknown = clients.filter((c) => !known.includes(c));
|
|
7125
7254
|
if (unknown.length)
|
|
7126
7255
|
throw new Error(`Unknown --client ${unknown.join(", ")}. Use one of: ${known.join(", ")}`);
|
package/dist/index.d.cts
CHANGED
|
@@ -172,6 +172,12 @@ export interface BreakhoundConfig {
|
|
|
172
172
|
exclude?: string[];
|
|
173
173
|
/** A real URL for each dynamic route, e.g. { '/products/[id]': '/products/42' }. */
|
|
174
174
|
samples?: Record<string, string>;
|
|
175
|
+
/**
|
|
176
|
+
* Values for dynamic route parameters by name, for routes no link on your pages fits,
|
|
177
|
+
* e.g. { slug: 'css-101', submissionId: '42' }. Every route using those names is filled in;
|
|
178
|
+
* a parent route's real URL fills its own parameters for the routes below it already.
|
|
179
|
+
*/
|
|
180
|
+
params?: Record<string, string>;
|
|
175
181
|
/** Most routes to scan in one run (default 100). */
|
|
176
182
|
maxRoutes?: number;
|
|
177
183
|
/** How to start the app when it isn't running, if not with its "dev" script. */
|
package/dist/index.d.ts
CHANGED
|
@@ -172,6 +172,12 @@ export interface BreakhoundConfig {
|
|
|
172
172
|
exclude?: string[];
|
|
173
173
|
/** A real URL for each dynamic route, e.g. { '/products/[id]': '/products/42' }. */
|
|
174
174
|
samples?: Record<string, string>;
|
|
175
|
+
/**
|
|
176
|
+
* Values for dynamic route parameters by name, for routes no link on your pages fits,
|
|
177
|
+
* e.g. { slug: 'css-101', submissionId: '42' }. Every route using those names is filled in;
|
|
178
|
+
* a parent route's real URL fills its own parameters for the routes below it already.
|
|
179
|
+
*/
|
|
180
|
+
params?: Record<string, string>;
|
|
175
181
|
/** Most routes to scan in one run (default 100). */
|
|
176
182
|
maxRoutes?: number;
|
|
177
183
|
/** How to start the app when it isn't running, if not with its "dev" script. */
|
package/dist/schema.json
CHANGED
|
@@ -38,6 +38,13 @@
|
|
|
38
38
|
},
|
|
39
39
|
"description": "A real URL for each dynamic route, e.g. { '/products/[id]': '/products/42' }."
|
|
40
40
|
},
|
|
41
|
+
"params": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"description": "Values for dynamic route parameters by name, for routes no link on your pages fits, e.g. { slug: 'css-101', submissionId: '42' }. Every route using those names is filled in; a parent route's real URL fills its own parameters for the routes below it already."
|
|
47
|
+
},
|
|
41
48
|
"maxRoutes": {
|
|
42
49
|
"type": "number",
|
|
43
50
|
"description": "Most routes to scan in one run (default 100)."
|