@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 CHANGED
@@ -45,7 +45,13 @@ In your project folder:
45
45
  npx @seybel/breakhound
46
46
  ```
47
47
 
48
- That's it: no config. To keep it in your project instead, `npm i -D @seybel/breakhound`; the command it installs is `breakhound`.
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). If no page links to one, it's listed with a hint, and you can give it a URL:
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 each dynamic route |
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` once it's installed in your project (in npm scripts, for example).
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.some((r) => r.status === "skipped") ? 'Give dynamic routes a real URL with `samples` in your config, e.g. `{ "/products/[id]": "/products/42" }`.' : void 0
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"><span class="mark" aria-hidden="true"></span><span>breakhound</span></div>
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: 8px; font-weight: 650; letter-spacing: -.01em; }
1661
- .mark { width: 18px; height: 18px; border-radius: 5px; background: linear-gradient(135deg, var(--accent) 0 55%, var(--error) 55%); }
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 = String.raw`
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 + '–' + r.to + 'px');
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 + '–' + max + 'px',
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
- dynamic ? el('p', {}, 'Give dynamic routes a real URL in your config: ', el('code', { text: "samples: { '" + dynamic.route + "': '" + example + "' }" })) : false,
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) this.add({ url: this.urlFor(sample), route: pattern.path });
3132
- else this.waiting.set(pattern.path, pattern);
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.waiting.size) {
3237
+ if (this.patterns.size) {
3139
3238
  for (const link of links) {
3140
3239
  const route = routeFor(link, this.patterns.values());
3141
- if (route && this.waiting.has(route.path)) {
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
- Or skip it: npx @seybel/breakhound prompt --copy, and paste the prompt into your coding agent.`;
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
- if (skipped.length) {
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 each a real URL with samples in your config, e.g.`
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
- const first = skipped[0]?.route;
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: "No page links to it, so there was no real URL to try.",
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
- // A real URL for dynamic routes that no page links to:
5908
- // samples: { '/products/[id]': '/products/42' },
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 once it's installed in your project.
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, Windsurf or Codex
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)."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seybel/breakhound",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Find where your layout breaks across every viewport width, then hand the fix to your coding agent.",
5
5
  "keywords": [
6
6
  "responsive",