@fringetools/cli 0.4.0 → 0.4.1

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
@@ -31,7 +31,8 @@ Prefer a permanent command? `npm install -D @fringetools/cli`, then `npx fringe
31
31
  | `fringe check --hook` | One-line `fringe/hook.v1` JSON for CI / pre-commit. Exits `1` on errors. Claude Code does **not** feed this back — use `--claude-hook` for agents. Wins over `--json`. |
32
32
  | `fringe check --update-baseline` | Accept today's findings into `fringe.baseline.json` (commit it). Exit `0`. |
33
33
  | `fringe check --record [--manifest <path>]` | Also append this run to `.fringe/ledger.jsonl`, optionally merging an agent flow manifest. |
34
- | `fringe init [path]` | Add the Claude Code hook and install the five skills. Safe to re-run; refuses to touch an invalid `settings.json`. |
34
+ | `fringe` (no arguments) | Checks the current folder, and if the post-edit check is not set up yet, says how to set it up. |
35
+ | `fringe init [path] [--global]` | Add the Claude Code hook and install the six skills. `--global` writes to `~/.claude` so every project is covered. Uses the project's own installed copy of Fringe when there is one (faster), otherwise `npx`. Safe to re-run; refuses to touch an invalid `settings.json`. |
35
36
  | `fringe open [path] [--port <n>]` | Serve the ledger as a plain HTML page on `127.0.0.1:4173` (loopback only). Ctrl+C to stop. |
36
37
  | `fringe --help` · `--version` | Usage / version. |
37
38
 
@@ -87,6 +88,21 @@ Most real problems live in states the first load never shows: an open menu, a mo
87
88
 
88
89
  For each state Fringe reloads the page, runs the steps, then audits the result (axe-core and the Tab walk). Steps are exactly one of `click`, `fill` (with `value`), `press`, `hover`, `waitFor`, `goto`; selectors are [Playwright selectors](https://playwright.dev/docs/other-locators) (CSS, `text=...`, `role=...`). Findings are labelled `[filters open]` and cluster separately from the same rule on the initial page. A step that fails (selector not found) is reported as `FR-402` ("state could not be reached") and nothing in that state is audited. Use throw-away test data in `fill` values and do not commit real credentials. `plan --scan` uses the same file.
89
90
 
91
+ ### Stress modes: how the page behaves under real-world conditions
92
+
93
+ ```sh
94
+ npx @fringetools/cli scan http://localhost:3000 --stress # all three
95
+ npx @fringetools/cli scan http://localhost:3000 --modes dark,reflow # a subset
96
+ ```
97
+
98
+ | Mode | What it does | Reports |
99
+ |---|---|---|
100
+ | `dark` | Re-audits with the browser asking for a dark color scheme | contrast and other axe findings that appear only in dark mode |
101
+ | `reduced` | Asks for reduced motion, then looks for animations still looping | `FR-403` (review) |
102
+ | `reflow` | Shrinks to 320px (a 400% zoom of a 1280px window) | `FR-404` (review, WCAG 1.4.10) if the page needs horizontal scrolling, plus new axe findings |
103
+
104
+ Each mode runs on a fresh page so one mode's emulation never leaks into the next, and only findings the plain page did not already have are kept. Findings are labelled `[dark mode]`, `[reduced motion]`, `[reflow]`. Modes apply to the initial page, not to named states.
105
+
90
106
  Limits, plainly: states are independent (each starts from a fresh load of the URL, so there are no multi-page flows that depend on earlier state beyond `goto`); it needs the page running; findings have no source line (they carry a CSS selector); automated checks only find part of the real problems. `fringe plan --scan <url>` merges it with the static findings into one fix order. It does not feed the baseline or `fringe-ignore` yet, and is not part of the edit hook (it needs a running app and takes seconds).
91
107
 
92
108
  ## Priority, clusters and fix plans
@@ -152,7 +168,7 @@ The hook also keeps `.fringe/last.json` (the previous run's errors) and tells th
152
168
 
153
169
  This was verified end to end with real agent runs: the agent receives the findings and fixes them (see [docs/architecture.md](../../docs/architecture.md)). Why `--claude-hook` and not `--hook`: Claude Code only passes a hook's output to the agent as `decision`/`reason` JSON on exit 0 (or stderr on exit 2); the CI contract `--hook` exits 1 and is ignored.
154
170
 
155
- **Skills** — `fringe init` installs five into `.claude/skills/` (sources in [skill/](skill/), one folder each); the Claude Code plugin ships the same folders, namespaced `/fringe:…`: `fringe` (below), `fringe-review` ("review this page": ranked, plain-English findings with fixes), `fringe-audit` (whole-repo plan: top rules and files, a fix order, offers `--update-baseline`), `fringe-debt` (lists `fringe-ignore` suppressions and the baseline) and `fringe-help` (quick reference). Invoke as `/fringe-review` etc. Reviews run `fringe check`, so Claude Code will ask you to approve that command once (or allow `Bash(fringe:*)` / `Bash(npx:*)`).
171
+ **Skills** — `fringe init` installs six into `.claude/skills/` (sources in [skill/](skill/), one folder each); the Claude Code plugin ships the same folders, namespaced `/fringetool:…`: `fringe` (below), `fringe-review` ("review this page": ranked, plain-English findings with fixes), `fringe-audit` (whole-repo plan: top rules and files, a fix order, offers `--update-baseline`), `fringe-debt` (lists `fringe-ignore` suppressions and the baseline), `fringe-setup` (guided onboarding: environment check, hook, baseline offer, critical paths) and `fringe-help` (quick reference). Invoke as `/fringe-review` etc. Reviews run `fringe check`, so Claude Code will ask you to approve that command once (or allow `Bash(fringe:*)` / `Bash(npx:*)`).
156
172
 
157
173
  **The main skill** (`.claude/skills/fringe/SKILL.md`) teaches the agent the ladder: *Flow* (cut what users don't need, with evidence in `.fringe/manifest.json`) → *Edges* (fix every hook finding, never silence a rule to get green) → *Polish* (tokens, scales, no AI look), then record the run. It is plain Markdown, so any agent platform that reads `AGENTS.md`/skill files can use it as-is.
158
174
 
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-JSWDIPHX.js";
4
+ } from "./chunk-7ZFEVMML.js";
5
5
 
6
6
  // src/bin.ts
7
7
  process.exitCode = await runCli(process.argv.slice(2));
@@ -4,7 +4,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync9, statSync } fr
4
4
  import { dirname as dirname2, join as join10, resolve } from "path";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = "0.4.0";
7
+ var VERSION = "0.4.1";
8
8
 
9
9
  // src/check.ts
10
10
  import { readFileSync as readFileSync5 } from "fs";
@@ -1683,7 +1683,47 @@ async function audit(page, axePath, url) {
1683
1683
  return [...axeToFindings(url, axeRes.violations), ...tabWalkFindings(url, focusables, visits)];
1684
1684
  }
1685
1685
  var inState = (name, url, f) => ({ ...f, message: `[${name}] ${f.message}`, loc: { ...f.loc, file: `${url} [${name}]` }, ...f.cluster ? { cluster: `${f.cluster} [${name}]` } : {} });
1686
- async function scan(url, cwd = process.cwd(), states = []) {
1686
+ var MODES = ["dark", "reduced", "reflow"];
1687
+ function parseModes(list) {
1688
+ const out = list.split(",").map((m) => m.trim()).filter(Boolean);
1689
+ const bad = out.find((m) => !MODES.includes(m));
1690
+ if (bad || out.length === 0) throw new Error(`unknown scan mode '${bad ?? ""}': use ${MODES.join(", ")}`);
1691
+ return out;
1692
+ }
1693
+ function onlyNew(base, extra) {
1694
+ const seen = new Set(base.map((f) => `${f.rule}|${f.message}`));
1695
+ return extra.filter((f) => !seen.has(`${f.rule}|${f.message}`));
1696
+ }
1697
+ var countRunningLoops = `[...document.getAnimations()].filter((a) => a.playState === 'running' && a.effect && a.effect.getComputedTiming().iterations === Infinity).map((a) => { const t = a.effect.target; return t ? t.tagName.toLowerCase() + (t.id ? '#' + t.id : '') + (typeof t.className === 'string' && t.className ? '.' + t.className.trim().split(/\\s+/)[0] : '') : 'unknown'; })`;
1698
+ async function stress(browser, axePath, url, mode, base) {
1699
+ const page = await browser.newPage();
1700
+ try {
1701
+ return await stressOn(page, axePath, url, mode, base);
1702
+ } finally {
1703
+ await page.close();
1704
+ }
1705
+ }
1706
+ async function stressOn(page, axePath, url, mode, base) {
1707
+ const at0 = { file: `${url} [${mode}]`, line: 1, col: 0 };
1708
+ await page.goto(url, { waitUntil: "load", timeout: 3e4 });
1709
+ if (mode === "dark") {
1710
+ await page.emulateMedia({ colorScheme: "dark" });
1711
+ await page.waitForTimeout(150);
1712
+ return onlyNew(base, await audit(page, axePath, url)).map((f) => inState("dark mode", url, f));
1713
+ }
1714
+ if (mode === "reduced") {
1715
+ await page.emulateMedia({ reducedMotion: "reduce" });
1716
+ await page.waitForTimeout(300);
1717
+ const loops = await page.evaluate(countRunningLoops);
1718
+ return loops.length === 0 ? [] : [{ rule: "FR-403", severity: "review", message: `[reduced motion] ${loops.length} animation(s) keep looping when the user asks for reduced motion: ${loops.slice(0, 3).join(", ")}`, hint: "honour prefers-reduced-motion: stop or replace infinite animations under @media (prefers-reduced-motion: reduce)", loc: at0 }];
1719
+ }
1720
+ await page.setViewportSize({ width: 320, height: 256 });
1721
+ await page.waitForTimeout(250);
1722
+ const wide = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
1723
+ const out = wide > 1 ? [{ rule: "FR-404", severity: "review", message: `[reflow] content is ${wide}px wider than a 320px screen, so it needs horizontal scrolling`, hint: "let content reflow to one column at 320px (WCAG 1.4.10); wide tables and code blocks may scroll inside their own container", loc: at0, wcag: "1.4.10" }] : [];
1724
+ return [...out, ...onlyNew(base, await audit(page, axePath, url)).map((f) => inState("reflow", url, f))];
1725
+ }
1726
+ async function scan(url, cwd = process.cwd(), states = [], modes = []) {
1687
1727
  if (!/^(https?|file):\/\//i.test(url)) throw new Error(`fringe scan takes an http(s):// or file:// URL, got '${url}'`);
1688
1728
  const req = createRequire(join8(cwd, "noop.js"));
1689
1729
  let playwright, axePath;
@@ -1707,6 +1747,7 @@ async function scan(url, cwd = process.cwd(), states = []) {
1707
1747
  const page = await browser.newPage();
1708
1748
  await page.goto(url, { waitUntil: "load", timeout: 3e4 });
1709
1749
  const all = await audit(page, axePath, url);
1750
+ const plain = [...all];
1710
1751
  for (const st of states) {
1711
1752
  await page.goto(url, { waitUntil: "load", timeout: 3e4 });
1712
1753
  let step = 0;
@@ -1718,10 +1759,11 @@ async function scan(url, cwd = process.cwd(), states = []) {
1718
1759
  }
1719
1760
  all.push(...(await audit(page, axePath, url)).map((f) => inState(st.name, url, f)));
1720
1761
  }
1762
+ for (const m of modes) all.push(...await stress(browser, axePath, url, m, plain));
1721
1763
  const { findings, clusters } = rankScan(url, all);
1722
1764
  const counts = { error: 0, review: 0, signal: 0 };
1723
1765
  for (const f of findings) counts[f.severity]++;
1724
- return { stack: { jsx: false, tailwind: false, vue: false, svelte: false, html: true }, files: 1 + states.length, skipped: [], findings, clusters, counts, score: score(counts, { error: 10, review: 3, signal: 1 }, 1) };
1766
+ return { stack: { jsx: false, tailwind: false, vue: false, svelte: false, html: true }, files: 1 + states.length + modes.length, skipped: [], findings, clusters, counts, score: score(counts, { error: 10, review: 3, signal: 1 }, 1) };
1725
1767
  } finally {
1726
1768
  await browser.close();
1727
1769
  }
@@ -1751,6 +1793,18 @@ import { dirname, join as join9 } from "path";
1751
1793
  import { fileURLToPath } from "url";
1752
1794
  var HOOK = "npx -y @fringetools/cli check --claude-hook";
1753
1795
  var SKILL_DIR = fileURLToPath(new URL("../skill/", import.meta.url));
1796
+ function hookCommand(root) {
1797
+ return existsSync2(join9(root, "node_modules", "@fringetools", "cli", "package.json")) ? "npx --no-install fringe check --claude-hook" : HOOK;
1798
+ }
1799
+ function isWired(root, home) {
1800
+ for (const dir of [root, home]) {
1801
+ try {
1802
+ if (/claude-hook|fringe(tool)?@/.test(readFileSync8(join9(dir, ".claude", "settings.json"), "utf8"))) return true;
1803
+ } catch {
1804
+ }
1805
+ }
1806
+ return false;
1807
+ }
1754
1808
  function initProject(root) {
1755
1809
  const done = [];
1756
1810
  const settingsPath = join9(root, ".claude", "settings.json");
@@ -1765,7 +1819,7 @@ function initProject(root) {
1765
1819
  const post = (settings.hooks ??= {}).PostToolUse ??= [];
1766
1820
  if (/(fringe|@fringetools\/cli) check/.test(JSON.stringify(post))) done.push("hook already present");
1767
1821
  else {
1768
- post.push({ matcher: "Edit|Write", hooks: [{ type: "command", command: HOOK }] });
1822
+ post.push({ matcher: "Edit|Write", hooks: [{ type: "command", command: hookCommand(root) }] });
1769
1823
  mkdirSync3(dirname(settingsPath), { recursive: true });
1770
1824
  writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1771
1825
  done.push(`hook added to ${settingsPath}`);
@@ -1782,6 +1836,9 @@ function initProject(root) {
1782
1836
  return done;
1783
1837
  }
1784
1838
 
1839
+ // src/index.ts
1840
+ import { homedir } from "os";
1841
+
1785
1842
  // src/patterns.ts
1786
1843
  var PATTERNS = {
1787
1844
  form: {
@@ -1958,15 +2015,19 @@ function loadStates(cwd, rest) {
1958
2015
  }
1959
2016
  return parseStates(raw);
1960
2017
  }
2018
+ function loadModes(rest) {
2019
+ const i = rest.indexOf("--modes");
2020
+ return i !== -1 ? parseModes(rest[i + 1] ?? "") : rest.includes("--stress") ? [...MODES] : [];
2021
+ }
1961
2022
  var weakEvidence = (e) => typeof e !== "string" || e.trim().length < 12 || /^(n\/?a|none|tbd|obvious|unnecessary|clutter|unused)\.?$/i.test(e.trim());
1962
2023
  var HELP = `fringe check [--json] [--hook] [--record] [--manifest <path>] [path]
1963
2024
  fringe check --claude-hook [path] for Claude Code hooks (feeds errors back to the agent)
1964
2025
  fringe check --update-baseline [path] accept today's findings; only new ones gate
1965
- fringe scan <url> [--states f] [--json] runtime check in a real browser (axe-core + keyboard walk); needs playwright-core and axe-core
2026
+ fringe scan <url> [--states f] [--stress | --modes dark,reduced,reflow] [--json] runtime check in a real browser (axe-core + keyboard walk); needs playwright-core and axe-core
1966
2027
  fringe plan [dir|file] [--scan url] [--json] [--limit n] ordered fix batches with recipes; --scan adds the live page
1967
2028
  fringe explain pattern [form] best-practice checklist for a component type
1968
2029
  fringe explain [FR-203] why a rule matters and how to fix it well
1969
- fringe init [path] add the Claude Code hook + skill
2030
+ fringe init [path] [--global] add the Claude Code hook + skills (--global: every project)
1970
2031
  fringe open [path] [--port <n>] serve the .fringe ledger on 127.0.0.1
1971
2032
  fringe --help | --version`;
1972
2033
  var KNOWN_FLAGS = /* @__PURE__ */ new Set(["--json", "--hook", "--claude-hook", "--record", "--manifest", "--update-baseline"]);
@@ -1989,6 +2050,11 @@ function parseArgs(rest, warn) {
1989
2050
  }
1990
2051
  async function runCli(argv, opts = {}) {
1991
2052
  const [cmd, ...rest] = argv;
2053
+ if (cmd === void 0) {
2054
+ const code = await runCli(["check"], opts);
2055
+ if (!isWired(opts.cwd ?? process.cwd(), opts.home ?? homedir())) log(opts)("\nfringe: not checking after your agent's edits yet. Run `npx @fringetools/cli init` (this project) or `init --global` (every project).");
2056
+ return code;
2057
+ }
1992
2058
  if (cmd === "--version" || cmd === "-v") {
1993
2059
  log(opts)(VERSION);
1994
2060
  return 0;
@@ -2039,13 +2105,13 @@ async function runCli(argv, opts = {}) {
2039
2105
  }
2040
2106
  if (cmd === "scan") {
2041
2107
  try {
2042
- const url = rest.find((a, i) => !a.startsWith("--") && rest[i - 1] !== "--states");
2108
+ const url = rest.find((a, i) => !a.startsWith("--") && rest[i - 1] !== "--states" && rest[i - 1] !== "--modes");
2043
2109
  if (!url) {
2044
2110
  err(opts)("fringe: scan needs a URL, e.g. fringe scan http://localhost:3000\n");
2045
2111
  return 2;
2046
2112
  }
2047
2113
  const cwd = opts.cwd ?? process.cwd();
2048
- const result = await scan(url, cwd, loadStates(cwd, rest));
2114
+ const result = await scan(url, cwd, loadStates(cwd, rest), loadModes(rest));
2049
2115
  log(opts)(rest.includes("--json") ? toJson(result) : toPretty(result));
2050
2116
  return result.counts.error > 0 ? 1 : 0;
2051
2117
  } catch (e) {
@@ -2064,14 +2130,14 @@ async function runCli(argv, opts = {}) {
2064
2130
  }
2065
2131
  const li = rest.indexOf("--limit");
2066
2132
  const limit = li !== -1 ? Number(rest[li + 1]) || void 0 : void 0;
2067
- const paths = parseArgs(rest.filter((a, i) => a !== "--limit" && rest[i - 1] !== "--limit" && a !== "--json" && a !== "--scan" && rest[i - 1] !== "--scan" && a !== "--states" && rest[i - 1] !== "--states"), err(opts));
2133
+ const paths = parseArgs(rest.filter((a, i) => a !== "--limit" && rest[i - 1] !== "--limit" && a !== "--json" && a !== "--scan" && rest[i - 1] !== "--scan" && a !== "--states" && rest[i - 1] !== "--states" && a !== "--stress" && a !== "--modes" && rest[i - 1] !== "--modes"), err(opts));
2068
2134
  const arg = resolve(opts.cwd ?? process.cwd(), paths[0] ?? ".");
2069
2135
  const { root, only } = resolveTarget(arg);
2070
2136
  const cfg = loadConfig(root);
2071
2137
  let result = await runCheck(root, cfg, true, only);
2072
2138
  if (scanUrl) {
2073
2139
  const cwd = opts.cwd ?? process.cwd();
2074
- result = mergeScan(result, await scan(scanUrl, cwd, loadStates(cwd, rest)), cfg.weights);
2140
+ result = mergeScan(result, await scan(scanUrl, cwd, loadStates(cwd, rest), loadModes(rest)), cfg.weights);
2075
2141
  }
2076
2142
  log(opts)(rest.includes("--json") ? toPlanJson(result, limit) : toPlanMarkdown(result, paths[0] ?? ".", limit));
2077
2143
  return 0;
@@ -2103,7 +2169,10 @@ async function runCli(argv, opts = {}) {
2103
2169
  }
2104
2170
  if (cmd === "init") {
2105
2171
  try {
2106
- for (const line of initProject(resolve(opts.cwd ?? process.cwd(), rest[0] ?? "."))) log(opts)(`fringe: ${line}`);
2172
+ const global = rest.includes("--global") || rest.includes("-g");
2173
+ const target = global ? opts.home ?? homedir() : resolve(opts.cwd ?? process.cwd(), rest.find((a) => !a.startsWith("-")) ?? ".");
2174
+ for (const line of initProject(target)) log(opts)(`fringe: ${line}`);
2175
+ if (global) log(opts)("fringe: set up for your user, so every project is covered (run without --global to set up one project only)");
2107
2176
  return 0;
2108
2177
  } catch (e) {
2109
2178
  err(opts)(`fringe: ${e.message}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  interface CliOpts {
2
2
  log?: (s: string) => void;
3
3
  cwd?: string;
4
- err?: (s: string) => void;
4
+ err?: (s: string) => void; /** Home directory for `init --global` (tests override it). */
5
+ home?: string;
5
6
  }
6
7
  declare function runCli(argv: string[], opts?: CliOpts): Promise<number>;
7
8
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runCli
3
- } from "./chunk-JSWDIPHX.js";
3
+ } from "./chunk-7ZFEVMML.js";
4
4
  export {
5
5
  runCli
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fringetools/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,7 +5,7 @@ description: Use when writing or changing frontend code (JSX/TSX, HTML, CSS, Vue
5
5
 
6
6
  # Fringe
7
7
 
8
- Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli` in its place (e.g. `npx -y @fringetools/cli check --json .`).
8
+ Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli@0.4.0` in its place (e.g. `npx -y @fringetools/cli@0.4.0 check --json .`).
9
9
 
10
10
  Ponytail trims your code. Fringe handles your edges. Work the ladder in order.
11
11
 
@@ -5,7 +5,7 @@ description: Use when the user wants a whole-repo UX/accessibility audit, a plan
5
5
 
6
6
  # Fringe audit
7
7
 
8
- Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli` in its place (e.g. `npx -y @fringetools/cli check --json .`).
8
+ Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli@0.4.0` in its place (e.g. `npx -y @fringetools/cli@0.4.0 check --json .`).
9
9
 
10
10
  1. Run `fringe plan --json --limit 5` (or `fringe plan` for Markdown). It already orders the work: P0 errors in shared components or critical paths, P1 other errors with mechanical fixes first, P2 review, P3 polish, each cluster with its recipe. Use that order; do not re-derive it. Add `"critical": ["src/checkout/**"]` to `fringe.config.json` if the user names the pages that matter most. Do not print every finding.
11
11
  2. Summarize as a plan:
@@ -7,7 +7,7 @@ description: Use when the user asks what Fringe findings have been silenced or d
7
7
 
8
8
  Fringe has two ways to defer a finding: an inline `fringe-ignore` comment and the `fringe.baseline.json` file. This lists both so a deferral can't quietly become permanent.
9
9
 
10
- Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli` in its place.
10
+ Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli@0.4.0` in its place.
11
11
 
12
12
  1. **Inline suppressions.** `git grep --no-index --exclude-standard -nE 'fringe-ignore +FR-[0-9]+'`. One row per hit: `<file>:<line>, <rule> — <why, if the comment or nearby code says>`. Skip hits that are only prose mentioning the convention.
13
13
  2. **Baseline.** If `fringe.baseline.json` exists, count entries per rule (each entry is `rule|file|message`) and list the files with the most. If it is missing, say there is no baseline.
@@ -15,9 +15,10 @@ Fringe checks agent-written frontends for accessibility, state, dark-pattern and
15
15
  | `/fringe-review` | Review a page or component: ranked findings with plain-English fixes |
16
16
  | `/fringe-audit` | Audit the whole repo: top rules and files, a fix order, offer a baseline |
17
17
  | `/fringe-debt` | List silenced findings (`fringe-ignore`, baseline) |
18
+ | `/fringe-setup` | Guided onboarding: wire the hook, offer a baseline, set critical paths |
18
19
  | `/fringe-help` | This reference |
19
20
 
20
- **Commands** (`fringe`, or `npx -y @fringetools/cli`)
21
+ **Commands** (`fringe`, or `npx -y @fringetools/cli@0.4.0`)
21
22
 
22
23
  | Command | What it does |
23
24
  |---|---|
@@ -5,7 +5,7 @@ description: Use when the user asks to review, check or critique a UI, page or c
5
5
 
6
6
  # Fringe review
7
7
 
8
- Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli` in its place (e.g. `npx -y @fringetools/cli check --json .`).
8
+ Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli@0.4.0` in its place (e.g. `npx -y @fringetools/cli@0.4.0 check --json .`).
9
9
 
10
10
  1. Run `fringe check --json <file or folder>` from the project root: the file or page the user named, else `.`. Each finding already carries `priority` and `fixability`; report highest priority first. For the fix text use `fringe explain <rule>` (why, recipe, what to avoid) rather than improvising.
11
11
  2. Report in plain English, no jargon left unexplained:
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: fringe-setup
3
+ description: Use when the user wants to set up or get started with Fringe, or asks why it is not checking their edits ("set up fringe", "get started", "install fringe", /fringe-setup). Guided onboarding: checks the environment, wires the hook, offers a baseline, and records the critical pages.
4
+ ---
5
+
6
+ # Fringe setup
7
+
8
+ Command: use `fringe` if it is on the PATH, otherwise `npx -y @fringetools/cli` in its place. Ask before each step that changes a file, and say what it will change.
9
+
10
+ 1. **Environment.** Run `node -v` (needs 18 or newer). If it is missing or older, stop and tell the user to install Node first.
11
+ 2. **Where things stand.** Run `fringe check .` and report the counts. If it says it is not checking after edits, the hook is not wired.
12
+ 3. **Wire the hook (skip if the Fringe plugin is already installed, or the check above reported it is set up).** Ask: this project only, or every project? Then run `fringe init` or `fringe init --global`. Both add the post-edit hook and the skills without touching other settings.
13
+ 4. **Existing problems.** If there are more than a handful of errors, explain that `fringe check --update-baseline` records today's findings so only new ones block, and offer it. Do not run it without a yes. Remind the user to commit `fringe.baseline.json`.
14
+ 5. **What matters most.** Ask which folders are most important to users (checkout, sign-up, login). Add them to `fringe.config.json` as `"critical": ["src/checkout/**", ...]`, merging into the file if it exists, never overwriting other keys.
15
+ 6. **Optional.** Mention `fringe scan <url>` (needs `npm i -D playwright-core axe-core` and Chrome) for real contrast and keyboard checks; install nothing unless asked.
16
+ 7. **Finish** with one short summary of what was changed and the commands to remember: `/fringe-review`, `/fringe-audit`, `fringe plan`, `fringe explain FR-203`.
17
+
18
+ If anything fails, report the exact message; do not retry blindly or edit `.claude/settings.json` by hand.