@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 +18 -2
- package/dist/bin.js +1 -1
- package/dist/{chunk-JSWDIPHX.js → chunk-7ZFEVMML.js} +80 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skill/fringe/SKILL.md +1 -1
- package/skill/fringe-audit/SKILL.md +1 -1
- package/skill/fringe-debt/SKILL.md +1 -1
- package/skill/fringe-help/SKILL.md +2 -1
- package/skill/fringe-review/SKILL.md +1 -1
- package/skill/fringe-setup/SKILL.md +18 -0
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
|
|
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
|
|
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
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
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]
|
|
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
|
-
|
|
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
package/package.json
CHANGED
package/skill/fringe/SKILL.md
CHANGED
|
@@ -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.
|