ultra11y 2.27.0 → 2.28.0

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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { realpathSync as realpathSync4, writeFileSync as writeFileSync15, mkdirSync as mkdirSync12, existsSync as existsSync24, readFileSync as readFileSync20, appendFileSync } from "fs";
5
- import { join as join40, relative as relative3, sep as sep6, dirname as dirname11 } from "path";
4
+ import { realpathSync as realpathSync6, writeFileSync as writeFileSync16, mkdirSync as mkdirSync14, existsSync as existsSync30, readFileSync as readFileSync26, appendFileSync } from "fs";
5
+ import { join as join45, relative as relative4, sep as sep7, dirname as dirname13 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "2.27.0";
9
+ var VERSION = "2.28.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -33142,7 +33142,7 @@ function findWord(line, name2) {
33142
33142
  }
33143
33143
  }
33144
33144
  function renderScip(scan2, opts = {}) {
33145
- const projectRoot = opts.projectRoot ?? "file://" + scan2.root.replace(/\\/g, "/");
33145
+ const projectRoot2 = opts.projectRoot ?? "file://" + scan2.root.replace(/\\/g, "/");
33146
33146
  const toolVersion = opts.toolVersion ?? ENGINE_VERSION;
33147
33147
  const docs = scan2.files.filter((f) => f.kind === "code" && f.symbols.length > 0);
33148
33148
  const docDefs = /* @__PURE__ */ new Map();
@@ -33229,7 +33229,7 @@ function renderScip(scan2, opts = {}) {
33229
33229
  pushString(toolInfo, F_TOOL_VERSION, toolVersion);
33230
33230
  const metadata2 = new Bytes();
33231
33231
  pushMessage(metadata2, F_META_TOOL_INFO, toolInfo);
33232
- pushString(metadata2, F_META_PROJECT_ROOT, projectRoot);
33232
+ pushString(metadata2, F_META_PROJECT_ROOT, projectRoot2);
33233
33233
  pushVarintField(metadata2, F_META_TEXT_ENCODING, TEXT_ENCODING_UTF8);
33234
33234
  let total = 0;
33235
33235
  for (const d of documents) total += d.length;
@@ -47763,6 +47763,24 @@ function matchGitIntent(command) {
47763
47763
  for (const [intent, re] of INTENTS) if (re.test(command)) return intent;
47764
47764
  return null;
47765
47765
  }
47766
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "shell", "exec", "exec_command", "local_shell", "localshell", "run_command"]);
47767
+ function isShellTool(name2) {
47768
+ return !!name2 && SHELL_TOOLS.has(name2.toLowerCase());
47769
+ }
47770
+ var SHELL_BIN = /(?:^|\/)(?:ba|z|k|da)?sh$/;
47771
+ var DASH_C = /^-[a-z]*c$/;
47772
+ function commandOf(toolInput) {
47773
+ const raw = toolInput?.command;
47774
+ if (typeof raw === "string") return raw || null;
47775
+ if (!Array.isArray(raw)) return null;
47776
+ const parts2 = raw.filter((p) => typeof p === "string");
47777
+ if (parts2.length === 0) return null;
47778
+ if (SHELL_BIN.test(parts2[0])) {
47779
+ const i2 = parts2.findIndex((p, n) => n > 0 && DASH_C.test(p));
47780
+ if (i2 !== -1 && parts2[i2 + 1]) return parts2[i2 + 1];
47781
+ }
47782
+ return parts2.join(" ") || null;
47783
+ }
47766
47784
  function git2(cwd, args2) {
47767
47785
  try {
47768
47786
  return execFileSync2("git", args2, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -47849,8 +47867,8 @@ function decide(payload, deps = {}) {
47849
47867
  const env = deps.env ?? process.env;
47850
47868
  const audit2 = deps.audit ?? realAudit;
47851
47869
  const seen = deps.seen ?? firstSighting;
47852
- if (payload.tool_name !== "Bash") return null;
47853
- const command = payload.tool_input?.command;
47870
+ if (!isShellTool(payload.tool_name)) return null;
47871
+ const command = commandOf(payload.tool_input);
47854
47872
  if (!command) return null;
47855
47873
  if (env.SKIP_A11Y || env.ULTRA11Y_HOOK === "off") return null;
47856
47874
  const intent = matchGitIntent(command);
@@ -53046,9 +53064,9 @@ function fixSummary(r, lang = "fr", write = false) {
53046
53064
  }
53047
53065
 
53048
53066
  // src/init.ts
53049
- import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, chmodSync } from "fs";
53067
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, chmodSync, realpathSync as realpathSync2 } from "fs";
53050
53068
  import { execFileSync as execFileSync5 } from "child_process";
53051
- import { join as join34 } from "path";
53069
+ import { join as join34, relative as relative3, sep as sep4 } from "path";
53052
53070
  var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
53053
53071
  function repoRoot() {
53054
53072
  try {
@@ -53057,6 +53075,19 @@ function repoRoot() {
53057
53075
  return null;
53058
53076
  }
53059
53077
  }
53078
+ function resolveEnginePath(root) {
53079
+ const argv1 = process.argv[1] ?? "scripts/ultra11y.mjs";
53080
+ try {
53081
+ const abs = realpathSync2(argv1);
53082
+ return abs.startsWith(root + sep4) ? relative3(root, abs) : abs;
53083
+ } catch {
53084
+ return argv1;
53085
+ }
53086
+ }
53087
+ function engineInvocation(root) {
53088
+ const p = resolveEnginePath(root);
53089
+ return p.startsWith("/") || p.startsWith("..") ? "npx -y ultra11y" : `node ${p}`;
53090
+ }
53060
53091
  function stagedHookScript(enginePath, failOn) {
53061
53092
  const sev = EN_SEV[failOn];
53062
53093
  return `#!/bin/sh
@@ -53159,6 +53190,563 @@ function writeCi(root, enginePath, failOn) {
53159
53190
  return path;
53160
53191
  }
53161
53192
 
53193
+ // src/install/json-edit.ts
53194
+ import { copyFileSync, existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync17, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
53195
+ import { dirname as dirname10 } from "path";
53196
+ var SettingsParseError = class extends Error {
53197
+ constructor(path, cause) {
53198
+ super(`${path} is not valid JSON (${cause}) \u2014 fix or move it, then run install again. It has NOT been modified.`);
53199
+ this.path = path;
53200
+ this.name = "SettingsParseError";
53201
+ }
53202
+ path;
53203
+ };
53204
+ function stamp() {
53205
+ return (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "-");
53206
+ }
53207
+ function writeTextWithBackup(path, content, marker = "ultra11y") {
53208
+ if (existsSync21(path) && readFileSync17(path, "utf8") === content) return { path, changed: false };
53209
+ mkdirSync11(dirname10(path), { recursive: true });
53210
+ let backup;
53211
+ if (existsSync21(path)) {
53212
+ backup = `${path}.${marker}-backup-${stamp()}`;
53213
+ copyFileSync(path, backup);
53214
+ }
53215
+ const tmp = `${path}.${process.pid}.tmp`;
53216
+ try {
53217
+ writeFileSync14(tmp, content, { mode: 420 });
53218
+ renameSync2(tmp, path);
53219
+ } catch (e) {
53220
+ rmSync4(tmp, { force: true });
53221
+ throw e;
53222
+ }
53223
+ return { path, changed: true, backup };
53224
+ }
53225
+ function editJsonFile(path, mutate) {
53226
+ let root = {};
53227
+ if (existsSync21(path)) {
53228
+ const raw = readFileSync17(path, "utf8");
53229
+ if (raw.trim()) {
53230
+ let parsed;
53231
+ try {
53232
+ parsed = JSON.parse(raw);
53233
+ } catch (e) {
53234
+ throw new SettingsParseError(path, e.message);
53235
+ }
53236
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
53237
+ throw new SettingsParseError(path, "the top level is not an object");
53238
+ }
53239
+ root = parsed;
53240
+ }
53241
+ }
53242
+ const before = JSON.stringify(root);
53243
+ mutate(root);
53244
+ if (JSON.stringify(root) === before && existsSync21(path)) return { path, changed: false };
53245
+ return writeTextWithBackup(path, `${JSON.stringify(root, null, 2)}
53246
+ `);
53247
+ }
53248
+ function readJsonSafe(path) {
53249
+ try {
53250
+ const parsed = JSON.parse(readFileSync17(path, "utf8"));
53251
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
53252
+ } catch {
53253
+ return null;
53254
+ }
53255
+ }
53256
+
53257
+ // src/install/claude-code.ts
53258
+ var CLAUDE_MARKER = "hook --claude-code";
53259
+ var MATCHER = "Bash|bash|shell|local_shell|exec_command|run_command|exec";
53260
+ function withoutOurs(groups) {
53261
+ if (!Array.isArray(groups)) return [];
53262
+ return groups.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !String(h.command ?? "").includes(CLAUDE_MARKER)) })).filter((g) => (g.hooks ?? []).length > 0);
53263
+ }
53264
+ function installClaudeCode({ settingsPath, command }) {
53265
+ return editJsonFile(settingsPath, (root) => {
53266
+ const hooks = root.hooks ??= {};
53267
+ hooks.PreToolUse = [
53268
+ ...withoutOurs(hooks.PreToolUse),
53269
+ { matcher: MATCHER, hooks: [{ type: "command", command: `${command} ${CLAUDE_MARKER}`, timeout: 30 }] }
53270
+ ];
53271
+ });
53272
+ }
53273
+ function uninstallClaudeCode({ settingsPath }) {
53274
+ return editJsonFile(settingsPath, (root) => {
53275
+ const hooks = root.hooks;
53276
+ if (!hooks || typeof hooks !== "object") return;
53277
+ const kept = withoutOurs(hooks.PreToolUse);
53278
+ if (kept.length > 0) hooks.PreToolUse = kept;
53279
+ else delete hooks.PreToolUse;
53280
+ if (Object.keys(hooks).length === 0) delete root.hooks;
53281
+ });
53282
+ }
53283
+ function claudeCodeWired(settingsPath) {
53284
+ const root = readJsonSafe(settingsPath);
53285
+ const groups = root?.hooks?.PreToolUse;
53286
+ if (!Array.isArray(groups)) return 0;
53287
+ return groups.reduce((n, g) => n + (g.hooks ?? []).filter((h) => String(h.command ?? "").includes(CLAUDE_MARKER)).length, 0);
53288
+ }
53289
+
53290
+ // src/install/text-edit.ts
53291
+ import { existsSync as existsSync22, readFileSync as readFileSync18, rmSync as rmSync5 } from "fs";
53292
+ var BLOCK_BEGIN = "<!-- BEGIN ultra11y (managed by `ultra11y install --agents-md`; edit outside this block) -->";
53293
+ var BLOCK_END = "<!-- END ultra11y -->";
53294
+ var blockRe = () => new RegExp(`${escapeRe2(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe2(BLOCK_END)}\\n?`);
53295
+ function escapeRe2(s) {
53296
+ return s.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
53297
+ }
53298
+ function upsertManagedBlock(path, body2) {
53299
+ const block = `${BLOCK_BEGIN}
53300
+ ${body2.trim()}
53301
+ ${BLOCK_END}
53302
+ `;
53303
+ if (!existsSync22(path)) return writeTextWithBackup(path, block);
53304
+ const current = readFileSync18(path, "utf8");
53305
+ if (blockRe().test(current)) return writeTextWithBackup(path, current.replace(blockRe(), block));
53306
+ const sep8 = current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
53307
+ return writeTextWithBackup(path, `${current}${sep8}${block}`);
53308
+ }
53309
+ function removeManagedBlock(path) {
53310
+ if (!existsSync22(path)) return { path, changed: false };
53311
+ const current = readFileSync18(path, "utf8");
53312
+ if (!blockRe().test(current)) return { path, changed: false };
53313
+ const rest = current.replace(blockRe(), "");
53314
+ if (rest.trim() === "") {
53315
+ rmSync5(path, { force: true });
53316
+ return { path, changed: true };
53317
+ }
53318
+ return writeTextWithBackup(path, rest.replace(/\n+$/, "\n"));
53319
+ }
53320
+ function hasManagedBlock(path) {
53321
+ return existsSync22(path) && blockRe().test(readFileSync18(path, "utf8"));
53322
+ }
53323
+
53324
+ // src/install/agents-md.ts
53325
+ import { join as join35 } from "path";
53326
+ function agentsMdPath(root) {
53327
+ return join35(root, "AGENTS.md");
53328
+ }
53329
+ function agentsMdBlock(root) {
53330
+ const e = engineInvocation(root);
53331
+ return `## Accessibility (ultra11y)
53332
+
53333
+ This repository is checked against **WCAG 2.2 AA** with \`ultra11y\`: a zero-dependency
53334
+ static engine \u2014 no install, no API key, no network.
53335
+
53336
+ **Before you commit, push, or open a pull request**, whenever you changed HTML, CSS,
53337
+ JSX/TSX, Vue, Svelte or Astro:
53338
+
53339
+ \`\`\`sh
53340
+ ${e} audit --staged --graph # exactly what the commit would record
53341
+ ${e} audit --since origin/main --graph # the whole branch, for a pull request
53342
+ \`\`\`
53343
+
53344
+ \`--graph\` is not optional on JSX/TSX: it turns on the real cross-file analysis (an
53345
+ icon-only component used without a name, a label defined in another file) and it suppresses
53346
+ a whole class of single-file false positives.
53347
+
53348
+ Then **adjudicate**. Do not paste the output at the user.
53349
+
53350
+ 1. Read every finding at its \`file:line\` in the real code. Refute the ones the code
53351
+ disproves, and say what disproves them. Findings inside library sources or single-file
53352
+ components are **preliminary**: the audited artifact is the RENDERED HTML.
53353
+ 2. Rule on the judgment criteria yourself, from the code \u2014 is this \`alt\` relevant, is this
53354
+ link's purpose clear in context, is the reading order right.
53355
+ \`${e} verify --report <md> --in <audit.json> --manual\` emits the worklist and the
53356
+ per-criterion decision protocol.
53357
+ 3. Name what source cannot decide \u2014 computed contrast, visible focus, zoom and reflow,
53358
+ content on hover \u2014 as **residual risks**. Never call them conforming.
53359
+ 4. **Never invent a non-conformity.** \`${e} check --report <file>\` fails the report when a
53360
+ cited element does not resolve. Run it before claiming anything.
53361
+ 5. Apply the safe deterministic fixes with \`${e} fix --staged --write --safe\` and re-stage.
53362
+ What remains needs judgment and the user's agreement.
53363
+
53364
+ Severity vocabulary: \`blocking\` > \`major\` > \`minor\`. Block the work on \`blocking\`.
53365
+
53366
+ To make this automatic without relying on an agent, wire the repo itself \u2014 \`${e} init --hook\`
53367
+ (git pre-commit) and \`${e} init --ci\` (GitHub Actions) are harness-independent and are the
53368
+ real safety net here. Full audits, dated conformance reports, RGAA packs and remediation
53369
+ backlogs: \`${e} --help\`.`;
53370
+ }
53371
+ function installAgentsMd(root) {
53372
+ return upsertManagedBlock(agentsMdPath(root), agentsMdBlock(root));
53373
+ }
53374
+ function uninstallAgentsMd(root) {
53375
+ return removeManagedBlock(agentsMdPath(root));
53376
+ }
53377
+ function agentsMdWired(root) {
53378
+ return hasManagedBlock(agentsMdPath(root));
53379
+ }
53380
+
53381
+ // src/install/codex.ts
53382
+ import { existsSync as existsSync24, readFileSync as readFileSync20 } from "fs";
53383
+ import { join as join37 } from "path";
53384
+
53385
+ // src/install/paths.ts
53386
+ import { copyFileSync as copyFileSync2, cpSync, existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync10 } from "fs";
53387
+ import { homedir as homedir2 } from "os";
53388
+ import { dirname as dirname11, join as join36 } from "path";
53389
+ function codexHome() {
53390
+ return process.env.CODEX_HOME || join36(homedir2(), ".codex");
53391
+ }
53392
+ function opencodeConfigDir() {
53393
+ const xdg = process.env.XDG_CONFIG_HOME;
53394
+ return join36(xdg && xdg !== "" ? xdg : join36(homedir2(), ".config"), "opencode");
53395
+ }
53396
+ function claudeSettingsPath(project, cwd = process.cwd()) {
53397
+ return project ? join36(cwd, ".claude", "settings.json") : join36(homedir2(), ".claude", "settings.json");
53398
+ }
53399
+ function pinnedEnginePath() {
53400
+ return join36(homedir2(), ".ultra11y", "bin", "ultra11y.mjs");
53401
+ }
53402
+ function installedCliCommand() {
53403
+ const override = process.env.ULTRA11Y_BIN;
53404
+ if (override) return `node ${JSON.stringify(override)}`;
53405
+ const argv1 = process.argv[1];
53406
+ let source = null;
53407
+ try {
53408
+ if (argv1) source = realpathSync3(argv1);
53409
+ } catch {
53410
+ }
53411
+ const ephemeral = !source || /[/\\](?:_npx|\.npm[/\\]_npx|npm-cache|Temp|tmp)[/\\]/.test(source);
53412
+ if (source && !ephemeral) return `node ${JSON.stringify(source)}`;
53413
+ const pin = pinnedEnginePath();
53414
+ if (source) {
53415
+ mkdirSync12(join36(pin, ".."), { recursive: true });
53416
+ copyFileSync2(source, pin);
53417
+ }
53418
+ return `node ${JSON.stringify(pin)}`;
53419
+ }
53420
+ function packageRoot() {
53421
+ let dir;
53422
+ try {
53423
+ dir = dirname11(realpathSync3(process.argv[1] ?? ""));
53424
+ } catch {
53425
+ return null;
53426
+ }
53427
+ for (let i2 = 0; i2 < 6 && dir && dir !== "/"; i2++) {
53428
+ if (existsSync23(join36(dir, "skills", "review-a11y", "SKILL.md"))) return dir;
53429
+ dir = dirname11(dir);
53430
+ }
53431
+ return null;
53432
+ }
53433
+ function bundledSkillsDir() {
53434
+ const root = packageRoot();
53435
+ const p = root ? join36(root, "skills") : null;
53436
+ return p && existsSync23(p) ? p : null;
53437
+ }
53438
+ function bundledOpencodePlugin() {
53439
+ const root = packageRoot();
53440
+ const p = root ? join36(root, ".opencode", "plugins", "ultra11y.js") : null;
53441
+ return p && existsSync23(p) ? p : null;
53442
+ }
53443
+ function copySkillsInto(source, dest) {
53444
+ const out2 = [];
53445
+ for (const name2 of ["ultra11y", "review-a11y"]) {
53446
+ const from = join36(source, name2);
53447
+ if (!existsSync23(from)) continue;
53448
+ const to = join36(dest, name2);
53449
+ const same = ["SKILL.md", join36("scripts", "ultra11y.mjs")].every((f) => {
53450
+ const a = join36(from, f);
53451
+ const b = join36(to, f);
53452
+ try {
53453
+ return existsSync23(b) && statSync10(a).size === statSync10(b).size && readFileSync19(a).equals(readFileSync19(b));
53454
+ } catch {
53455
+ return false;
53456
+ }
53457
+ });
53458
+ if (same) {
53459
+ out2.push({ path: to, changed: false });
53460
+ continue;
53461
+ }
53462
+ cpSync(from, to, { recursive: true });
53463
+ out2.push({ path: to, changed: true });
53464
+ }
53465
+ return out2;
53466
+ }
53467
+
53468
+ // src/install/codex.ts
53469
+ var CODEX_MARKER = "hook --codex";
53470
+ var MATCHER2 = "Bash|bash|shell|local_shell|exec_command|run_command|exec";
53471
+ function withoutOurs2(groups) {
53472
+ if (!Array.isArray(groups)) return [];
53473
+ return groups.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !String(h.command ?? "").includes(CODEX_MARKER)) })).filter((g) => (g.hooks ?? []).length > 0);
53474
+ }
53475
+ function enableHooksFeature(content) {
53476
+ if (HOOKS_ON.test(featuresBody(content))) return { content, changed: false };
53477
+ const body2 = featuresBody(content);
53478
+ if (HOOKS_ANY.test(body2)) {
53479
+ return { content: content.replace(body2, body2.replace(HOOKS_ANY, "hooks = true")), changed: true };
53480
+ }
53481
+ if (FEATURES_HEADER.test(content)) {
53482
+ return { content: content.replace(FEATURES_HEADER, "$&\nhooks = true"), changed: true };
53483
+ }
53484
+ const prefix2 = content.trim() === "" ? "" : "\n";
53485
+ return { content: `[features]
53486
+ hooks = true
53487
+ ${prefix2}${content}`, changed: true };
53488
+ }
53489
+ var HOOKS_ON = /^[ \t]*hooks[ \t]*=[ \t]*true[ \t]*$/m;
53490
+ var HOOKS_ANY = /^[ \t]*hooks[ \t]*=[ \t]*(?:false|true)[ \t]*$/m;
53491
+ var FEATURES_HEADER = /^[ \t]*\[features\][ \t]*$/m;
53492
+ var ANY_TABLE_HEADER = /^[ \t]*\[/m;
53493
+ function featuresBody(content) {
53494
+ const m = FEATURES_HEADER.exec(content);
53495
+ if (!m) return "";
53496
+ const rest = content.slice(m.index + m[0].length);
53497
+ const next = ANY_TABLE_HEADER.exec(rest);
53498
+ return rest.slice(0, next ? next.index : void 0);
53499
+ }
53500
+ function installCodex({ codexDir, command, skillsSource }) {
53501
+ const reports = [];
53502
+ reports.push(
53503
+ editJsonFile(join37(codexDir, "hooks.json"), (root) => {
53504
+ const hooks = root.hooks ??= {};
53505
+ hooks.PreToolUse = [
53506
+ ...withoutOurs2(hooks.PreToolUse),
53507
+ { matcher: MATCHER2, hooks: [{ type: "command", command: `${command} ${CODEX_MARKER}`, timeout: 30, statusMessage: "ultra11y" }] }
53508
+ ];
53509
+ })
53510
+ );
53511
+ const configPath = join37(codexDir, "config.toml");
53512
+ const current = existsSync24(configPath) ? readFileSync20(configPath, "utf8") : "";
53513
+ const edit = enableHooksFeature(current);
53514
+ if (edit.changed) reports.push(writeTextWithBackup(configPath, edit.content));
53515
+ if (skillsSource) reports.push(...copySkillsInto(skillsSource, join37(codexDir, "skills")));
53516
+ return {
53517
+ reports,
53518
+ guidance: [
53519
+ // Deliberately NOT forging the trusted_hash. Reproducing Codex's canonicalisation
53520
+ // exactly is a bet that breaks silently, in the user's home directory, the next time
53521
+ // Codex changes it — and the failure mode of a wrong hash is a gate that never fires.
53522
+ "codex: Codex will ask you to trust this hook the first time it fires \u2014 accept it, or review it with /hooks.",
53523
+ "codex: restart the session \u2014 hooks load at startup.",
53524
+ "codex: `codex plugin marketplace add maxgfr/ultra11y` then `codex plugin add ultra11y@ultra11y` is the native route; it ships the skills and the hook together."
53525
+ ]
53526
+ };
53527
+ }
53528
+ function uninstallCodex({ codexDir }) {
53529
+ const reports = [];
53530
+ const hooksPath = join37(codexDir, "hooks.json");
53531
+ if (existsSync24(hooksPath)) {
53532
+ reports.push(
53533
+ editJsonFile(hooksPath, (root) => {
53534
+ const hooks = root.hooks;
53535
+ if (!hooks || typeof hooks !== "object") return;
53536
+ const kept = withoutOurs2(hooks.PreToolUse);
53537
+ if (kept.length > 0) hooks.PreToolUse = kept;
53538
+ else delete hooks.PreToolUse;
53539
+ if (Object.keys(hooks).length === 0) delete root.hooks;
53540
+ })
53541
+ );
53542
+ }
53543
+ return { reports, guidance: ["codex: `[features] hooks = true` was left enabled \u2014 other hooks may depend on it."] };
53544
+ }
53545
+ function codexWired(codexDir) {
53546
+ const root = readJsonSafe(join37(codexDir, "hooks.json"));
53547
+ const groups = root?.hooks?.PreToolUse;
53548
+ if (!Array.isArray(groups)) return 0;
53549
+ return groups.reduce((n, g) => n + (g.hooks ?? []).filter((h) => String(h.command ?? "").includes(CODEX_MARKER)).length, 0);
53550
+ }
53551
+ function codexHooksEnabled(codexDir) {
53552
+ const p = join37(codexDir, "config.toml");
53553
+ return existsSync24(p) && HOOKS_ON.test(featuresBody(readFileSync20(p, "utf8")));
53554
+ }
53555
+
53556
+ // src/install/opencode.ts
53557
+ import { existsSync as existsSync25, readFileSync as readFileSync21, rmSync as rmSync6 } from "fs";
53558
+ import { join as join38 } from "path";
53559
+ var OPENCODE_MARKER = "ULTRA11Y_OPENCODE_PLUGIN";
53560
+ function pluginTarget(configDir) {
53561
+ return join38(configDir, "plugin", "ultra11y.js");
53562
+ }
53563
+ function installOpencode({ configDir, pluginSource, skillsSource, enginePath }) {
53564
+ const target = pluginTarget(configDir);
53565
+ if (existsSync25(target) && !readFileSync21(target, "utf8").includes(OPENCODE_MARKER)) {
53566
+ throw new Error(`refusing to overwrite ${target}: it is not a file ultra11y wrote. Remove it manually if you want the plugin here.`);
53567
+ }
53568
+ const reports = [writeTextWithBackup(target, readFileSync21(pluginSource, "utf8"))];
53569
+ if (enginePath && existsSync25(enginePath)) {
53570
+ reports.push(writeTextWithBackup(join38(configDir, "plugin", "ultra11y.mjs"), readFileSync21(enginePath, "utf8")));
53571
+ }
53572
+ if (skillsSource) reports.push(...copySkillsInto(skillsSource, join38(configDir, "skills")));
53573
+ return {
53574
+ reports,
53575
+ guidance: [
53576
+ "opencode: restart the session \u2014 plugins load at startup.",
53577
+ 'opencode: the alternative is an npm pin \u2014 add "plugin": ["ultra11y@latest"] to ~/.config/opencode/opencode.json and drop this file.',
53578
+ "opencode: the gate surfaces as a blocked tool call whose error message carries the findings (OpenCode has no permission-decision channel)."
53579
+ ]
53580
+ };
53581
+ }
53582
+ function uninstallOpencode({ configDir }) {
53583
+ const reports = [];
53584
+ const target = pluginTarget(configDir);
53585
+ if (existsSync25(target) && readFileSync21(target, "utf8").includes(OPENCODE_MARKER)) {
53586
+ rmSync6(target, { force: true });
53587
+ rmSync6(join38(configDir, "plugin", "ultra11y.mjs"), { force: true });
53588
+ reports.push({ path: target, changed: true });
53589
+ }
53590
+ const configPath = join38(configDir, "opencode.json");
53591
+ if (existsSync25(configPath)) {
53592
+ reports.push(
53593
+ editJsonFile(configPath, (root) => {
53594
+ if (!Array.isArray(root.plugin)) return;
53595
+ const kept = root.plugin.filter((p) => !/^ultra11y(@|$)/.test(String(p)));
53596
+ if (kept.length > 0) root.plugin = kept;
53597
+ else delete root.plugin;
53598
+ })
53599
+ );
53600
+ }
53601
+ return { reports, guidance: [] };
53602
+ }
53603
+ function opencodeWired(configDir) {
53604
+ const target = pluginTarget(configDir);
53605
+ if (existsSync25(target) && readFileSync21(target, "utf8").includes(OPENCODE_MARKER)) return true;
53606
+ try {
53607
+ const cfg = JSON.parse(readFileSync21(join38(configDir, "opencode.json"), "utf8"));
53608
+ return Array.isArray(cfg.plugin) && cfg.plugin.some((p) => /^ultra11y(@|$)/.test(String(p)));
53609
+ } catch {
53610
+ return false;
53611
+ }
53612
+ }
53613
+
53614
+ // src/install/index.ts
53615
+ import { existsSync as existsSync26 } from "fs";
53616
+ import { join as join39 } from "path";
53617
+ var ALL_TARGETS = ["claude-code", "codex", "opencode"];
53618
+ function parseTargets(flags2) {
53619
+ const picked = /* @__PURE__ */ new Set();
53620
+ if (flags2.all === true) for (const t2 of ALL_TARGETS) picked.add(t2);
53621
+ if (flags2["claude-code"] === true) picked.add("claude-code");
53622
+ if (flags2.codex === true) picked.add("codex");
53623
+ if (flags2.opencode === true) picked.add("opencode");
53624
+ if (flags2["agents-md"] === true) picked.add("agents-md");
53625
+ return picked.size ? [...picked] : null;
53626
+ }
53627
+ function projectRoot(cwd) {
53628
+ return repoRoot() ?? cwd;
53629
+ }
53630
+ function installForTargets(opts) {
53631
+ const cwd = opts.cwd ?? process.cwd();
53632
+ const out2 = [];
53633
+ let command = null;
53634
+ const cmd = () => command ??= installedCliCommand();
53635
+ for (const target of opts.targets) {
53636
+ const r = { target, reports: [], guidance: [] };
53637
+ try {
53638
+ switch (target) {
53639
+ case "claude-code": {
53640
+ const settingsPath = claudeSettingsPath(opts.project === true, cwd);
53641
+ if (!opts.dryRun) r.reports.push(installClaudeCode({ settingsPath, command: cmd() }));
53642
+ else r.reports.push({ path: settingsPath, changed: false });
53643
+ r.guidance.push(
53644
+ "claude-code: the plugin route (`/plugin marketplace add maxgfr/ultra11y` then `/plugin install ultra11y@ultra11y`) ships the skills too \u2014 this settings hook only wires the gate.",
53645
+ "claude-code: restart the session \u2014 hooks load at startup."
53646
+ );
53647
+ break;
53648
+ }
53649
+ case "codex": {
53650
+ if (opts.project === true) r.guidance.push("codex: --project is not supported (Codex reads hooks from its home only) \u2014 wiring the user scope.");
53651
+ if (!opts.dryRun) {
53652
+ const out3 = installCodex({ codexDir: codexHome(), command: cmd(), skillsSource: opts.skills === false ? null : bundledSkillsDir() });
53653
+ r.reports.push(...out3.reports);
53654
+ r.guidance.push(...out3.guidance);
53655
+ } else r.reports.push({ path: join39(codexHome(), "hooks.json"), changed: false });
53656
+ break;
53657
+ }
53658
+ case "opencode": {
53659
+ const source = bundledOpencodePlugin();
53660
+ if (!source) {
53661
+ r.error = "opencode: this engine was not run from a package carrying .opencode/plugins/ultra11y.js \u2014 install ultra11y from npm and retry.";
53662
+ break;
53663
+ }
53664
+ const configDir = opts.project === true ? join39(cwd, ".opencode") : opencodeConfigDir();
53665
+ if (!opts.dryRun) {
53666
+ const engine = existsSync26(pinnedEnginePath()) ? pinnedEnginePath() : join39(packageRoot() ?? "", "scripts", "ultra11y.mjs");
53667
+ const out3 = installOpencode({
53668
+ configDir,
53669
+ pluginSource: source,
53670
+ skillsSource: opts.skills === false ? null : bundledSkillsDir(),
53671
+ enginePath: existsSync26(engine) ? engine : null
53672
+ });
53673
+ r.reports.push(...out3.reports);
53674
+ r.guidance.push(...out3.guidance);
53675
+ } else r.reports.push({ path: join39(configDir, "plugin", "ultra11y.js"), changed: false });
53676
+ break;
53677
+ }
53678
+ case "agents-md": {
53679
+ const root = projectRoot(cwd);
53680
+ if (!opts.dryRun) r.reports.push(installAgentsMd(root));
53681
+ else r.reports.push({ path: agentsMdPath(root), changed: false });
53682
+ r.guidance.push(
53683
+ "agents-md: this is a tracked file in your repository \u2014 review it and commit it.",
53684
+ "agents-md: it cannot make anything automatic (no hook API); `ultra11y init --hook` is what actually enforces the gate there."
53685
+ );
53686
+ break;
53687
+ }
53688
+ }
53689
+ } catch (e) {
53690
+ r.error = e instanceof SettingsParseError ? e.message : `${target}: ${e.message}`;
53691
+ }
53692
+ out2.push(r);
53693
+ }
53694
+ return out2;
53695
+ }
53696
+ function uninstallForTargets(opts) {
53697
+ const cwd = opts.cwd ?? process.cwd();
53698
+ const out2 = [];
53699
+ for (const target of opts.targets) {
53700
+ const r = { target, reports: [], guidance: [] };
53701
+ try {
53702
+ switch (target) {
53703
+ case "claude-code":
53704
+ r.reports.push(uninstallClaudeCode({ settingsPath: claudeSettingsPath(opts.project === true, cwd) }));
53705
+ break;
53706
+ case "codex": {
53707
+ const out3 = uninstallCodex({ codexDir: codexHome() });
53708
+ r.reports.push(...out3.reports);
53709
+ r.guidance.push(...out3.guidance);
53710
+ break;
53711
+ }
53712
+ case "opencode": {
53713
+ const out3 = uninstallOpencode({ configDir: opts.project === true ? join39(cwd, ".opencode") : opencodeConfigDir() });
53714
+ r.reports.push(...out3.reports);
53715
+ r.guidance.push(...out3.guidance);
53716
+ break;
53717
+ }
53718
+ case "agents-md":
53719
+ r.reports.push(uninstallAgentsMd(projectRoot(cwd)));
53720
+ break;
53721
+ }
53722
+ } catch (e) {
53723
+ r.error = e instanceof SettingsParseError ? e.message : `${target}: ${e.message}`;
53724
+ }
53725
+ out2.push(r);
53726
+ }
53727
+ return out2;
53728
+ }
53729
+ function statusReport(opts = {}) {
53730
+ const cwd = opts.cwd ?? process.cwd();
53731
+ const claudePath = claudeSettingsPath(opts.project === true, cwd);
53732
+ const root = projectRoot(cwd);
53733
+ const codexDir = codexHome();
53734
+ const ocDir = opts.project === true ? join39(cwd, ".opencode") : opencodeConfigDir();
53735
+ const codexOn = codexWired(codexDir) > 0;
53736
+ return [
53737
+ { target: "claude-code", wired: claudeCodeWired(claudePath) > 0, path: claudePath, note: "the plugin route wires this separately and does not show here" },
53738
+ {
53739
+ target: "codex",
53740
+ wired: codexOn,
53741
+ path: join39(codexDir, "hooks.json"),
53742
+ // A wired hook behind a disabled feature flag never fires and looks fine. Say so.
53743
+ note: codexOn && !codexHooksEnabled(codexDir) ? "WIRED BUT INERT: `[features] hooks = true` is not set in config.toml" : void 0
53744
+ },
53745
+ { target: "opencode", wired: opencodeWired(ocDir), path: join39(ocDir, "plugin", "ultra11y.js") },
53746
+ { target: "agents-md", wired: agentsMdWired(root), path: agentsMdPath(root) }
53747
+ ];
53748
+ }
53749
+
53162
53750
  // src/output.ts
53163
53751
  var STR = {
53164
53752
  fr: {
@@ -53456,7 +54044,7 @@ function stepSummary(result, opts = {}) {
53456
54044
 
53457
54045
  // src/dev.ts
53458
54046
  import { createServer } from "http";
53459
- import { join as join35 } from "path";
54047
+ import { join as join40 } from "path";
53460
54048
  var DEV_DEFAULT_PORT = 4111;
53461
54049
  function criterionLabel3(f, standard) {
53462
54050
  if (isCore(standard)) return `WCAG ${f.criteriaId}`;
@@ -53722,13 +54310,13 @@ function auditCollected(root, payload) {
53722
54310
  ...payload.axtree ? { axtree: payload.axtree } : {},
53723
54311
  ...payload.css ? { css: payload.css } : {}
53724
54312
  });
53725
- return { ok: true, result: runAudit({ inputs: [join35(dir, "dom.html")] }) };
54313
+ return { ok: true, result: runAudit({ inputs: [join40(dir, "dom.html")] }) };
53726
54314
  }
53727
54315
  function projectPages(root) {
53728
54316
  const snaps = readSnapshots(root);
53729
54317
  if (!snaps.length) return { result: null, pages: [] };
53730
54318
  const scope = pageScopesFrom(snaps);
53731
- const result = runAudit({ inputs: [join35(root, ".ultra11y/pages")] });
54319
+ const result = runAudit({ inputs: [join40(root, ".ultra11y/pages")] });
53732
54320
  result.scope.pages = scope;
53733
54321
  attributePages(result, scope);
53734
54322
  return { result, pages: derivePages(result, scope) };
@@ -54053,11 +54641,11 @@ function e2eSetupPlan(runners, paths, lang = "en") {
54053
54641
  }
54054
54642
 
54055
54643
  // src/orchestrate.ts
54056
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
54057
- import { join as join37, resolve as resolve7 } from "path";
54644
+ import { existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync23, rmSync as rmSync7, writeFileSync as writeFileSync15 } from "fs";
54645
+ import { join as join42, resolve as resolve7 } from "path";
54058
54646
 
54059
54647
  // src/orchestrate-templates.ts
54060
- import { join as join36 } from "path";
54648
+ import { join as join41 } from "path";
54061
54649
  var ONE_WRITER_FOOTER = `
54062
54650
  ## Return, don't write
54063
54651
 
@@ -54129,7 +54717,7 @@ var PHASE_SPECS = {
54129
54717
  title: "Adjudicate",
54130
54718
  schema: ADJUDICATE_SCHEMA,
54131
54719
  description: (n) => `Adjudicate the ${n} residual judgment criterion(ia) of an ultra11y audit (fan-out, fail-closed fold)`,
54132
- applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join36(run2, "audit-latest.json")} --out ${run2}`
54720
+ applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join41(run2, "audit-latest.json")} --out ${run2}`
54133
54721
  },
54134
54722
  "verify-report": {
54135
54723
  role: "refuter",
@@ -54151,7 +54739,7 @@ function toBatches(ids, batchSize) {
54151
54739
  }
54152
54740
  function phaseWorkflowScript(ph, runAbs, engineAbs, batchSize) {
54153
54741
  const spec = phaseSpec(ph.name);
54154
- const scriptPath = join36(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
54742
+ const scriptPath = join41(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
54155
54743
  const meta2 = { name: `ultra11y-${ph.name}`, description: spec.description(ph.items), phases: [{ title: spec.title }] };
54156
54744
  return [
54157
54745
  `export const meta = ${JSON.stringify(meta2)}`,
@@ -54195,7 +54783,7 @@ function agentContracts(runAbs, engineAbs) {
54195
54783
 
54196
54784
  You adjudicate the residual judgment criteria of an ultra11y audit \u2014 the ones the deterministic engine could not decide (alt-text relevance, link purpose in context, reading order\u2026). The ACTIVE STANDARD is recorded in the worklist's \`standard\` field: under a country standard (e.g. \`rgaa\`) the items are that standard's OWN criteria, each carrying its numbered tests \u2014 not WCAG success criteria.
54197
54785
 
54198
- Worklist: \`${join36(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
54786
+ Worklist: \`${join41(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
54199
54787
 
54200
54788
  For EACH of your criteria:
54201
54789
 
@@ -54213,7 +54801,7 @@ ${footer}`,
54213
54801
 
54214
54802
  You are an adversarial skeptic verifying the non-conformities of an ultra11y report. Your job is to try to REFUTE each claim: assume it is wrong until the source proves it.
54215
54803
 
54216
- Worklist: \`${join36(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
54804
+ Worklist: \`${join41(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
54217
54805
 
54218
54806
  For EACH of your entries:
54219
54807
 
@@ -54249,14 +54837,14 @@ ${status}
54249
54837
 
54250
54838
  ## The loop (play every role yourself, one item at a time)
54251
54839
 
54252
- 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join36(runAbs, "audit-latest.json")}\`.
54253
- 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join36(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join36(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join36(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join36(runAbs, "ADJUDICATE.todo.json")} --in ${join36(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54254
- 3. **Report**: \`${engine} report --in ${join36(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54255
- 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join36(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join36(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join36(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
54840
+ 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join41(runAbs, "audit-latest.json")}\`.
54841
+ 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join41(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join41(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join41(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join41(runAbs, "ADJUDICATE.todo.json")} --in ${join41(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54842
+ 3. **Report**: \`${engine} report --in ${join41(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54843
+ 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join41(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join41(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join41(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
54256
54844
  5. **Gate**: \`${engine} check --report <the report .md> --semantic\` must exit 0 before presenting anything.
54257
54845
  6. **Fix & re-audit**: \`${engine} fix <globs> --write --iterate\`, hand-apply the judgment fixes, then loop from step 1 until the gate stays green.
54258
54846
 
54259
- With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join36(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
54847
+ With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join41(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
54260
54848
  `;
54261
54849
  }
54262
54850
 
@@ -54266,12 +54854,12 @@ var SMALL_WORKLIST = 3;
54266
54854
  var BATCH_SIZE = 8;
54267
54855
  function listPhases(runDir, engineAbs) {
54268
54856
  const run2 = resolve7(runDir);
54269
- const adjPath = join37(run2, "ADJUDICATE.todo.json");
54857
+ const adjPath = join42(run2, "ADJUDICATE.todo.json");
54270
54858
  let adjIds = [];
54271
54859
  let adjReady = false;
54272
- if (existsSync21(adjPath)) {
54860
+ if (existsSync27(adjPath)) {
54273
54861
  try {
54274
- const f = JSON.parse(readFileSync17(adjPath, "utf8"));
54862
+ const f = JSON.parse(readFileSync23(adjPath, "utf8"));
54275
54863
  if (f && f.kind === "adjudication" && Array.isArray(f.items)) {
54276
54864
  adjReady = true;
54277
54865
  adjIds = f.items.map((i2) => i2.criteriaId);
@@ -54279,12 +54867,12 @@ function listPhases(runDir, engineAbs) {
54279
54867
  } catch {
54280
54868
  }
54281
54869
  }
54282
- const verPath = join37(run2, "VERIFY.todo.json");
54870
+ const verPath = join42(run2, "VERIFY.todo.json");
54283
54871
  let verIds = [];
54284
54872
  let verReady = false;
54285
- if (existsSync21(verPath)) {
54873
+ if (existsSync27(verPath)) {
54286
54874
  try {
54287
- const items = JSON.parse(readFileSync17(verPath, "utf8"));
54875
+ const items = JSON.parse(readFileSync23(verPath, "utf8"));
54288
54876
  if (Array.isArray(items)) {
54289
54877
  verReady = true;
54290
54878
  verIds = items.map((i2) => String(i2.n));
@@ -54299,7 +54887,7 @@ function listPhases(runDir, engineAbs) {
54299
54887
  worklist: adjPath,
54300
54888
  items: adjIds.length,
54301
54889
  ids: adjIds,
54302
- prerequisite: `node ${engineAbs} verify --manual --in ${join37(run2, "audit-latest.json")} --out ${run2}`
54890
+ prerequisite: `node ${engineAbs} verify --manual --in ${join42(run2, "audit-latest.json")} --out ${run2}`
54303
54891
  },
54304
54892
  {
54305
54893
  name: "verify-report",
@@ -54313,7 +54901,7 @@ function listPhases(runDir, engineAbs) {
54313
54901
  }
54314
54902
  function orchestrateRun(runDir, engineAbs, opts = {}) {
54315
54903
  const run2 = resolve7(runDir);
54316
- if (!existsSync21(run2)) {
54904
+ if (!existsSync27(run2)) {
54317
54905
  return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
54318
54906
  }
54319
54907
  const phases = listPhases(run2, engineAbs);
@@ -54340,23 +54928,23 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
54340
54928
  }
54341
54929
  selected = [ph];
54342
54930
  }
54343
- const orchDir = join37(run2, "orchestration");
54344
- const agentsDir = join37(orchDir, "agents");
54345
- mkdirSync11(join37(orchDir, "out"), { recursive: true });
54346
- mkdirSync11(agentsDir, { recursive: true });
54931
+ const orchDir = join42(run2, "orchestration");
54932
+ const agentsDir = join42(orchDir, "agents");
54933
+ mkdirSync13(join42(orchDir, "out"), { recursive: true });
54934
+ mkdirSync13(agentsDir, { recursive: true });
54347
54935
  const written = [];
54348
54936
  const notices = [];
54349
54937
  for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs))) {
54350
- const p = join37(agentsDir, `${name2}.md`);
54351
- writeFileSync14(p, content);
54938
+ const p = join42(agentsDir, `${name2}.md`);
54939
+ writeFileSync15(p, content);
54352
54940
  written.push(p);
54353
54941
  }
54354
54942
  const emitted = new Set(opts.eco ? [] : selected.filter((p) => p.items > 0).map((p) => p.name));
54355
54943
  for (const ph of phases) {
54356
54944
  if (emitted.has(ph.name)) continue;
54357
- const stale = join37(orchDir, `${ph.name}.workflow.mjs`);
54358
- if (existsSync21(stale)) {
54359
- rmSync4(stale, { force: true });
54945
+ const stale = join42(orchDir, `${ph.name}.workflow.mjs`);
54946
+ if (existsSync27(stale)) {
54947
+ rmSync7(stale, { force: true });
54360
54948
  notices.push(`phase "${ph.name}": stale workflow removed \u2014 its worklist ${ph.ready ? "is now empty" : "no longer exists"}.`);
54361
54949
  }
54362
54950
  }
@@ -54369,13 +54957,13 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
54369
54957
  if (ph.items <= SMALL_WORKLIST) {
54370
54958
  notices.push(`phase "${ph.name}": only ${ph.items} item(s) \u2014 the sequential --eco path is equivalent and cheaper.`);
54371
54959
  }
54372
- const p = join37(orchDir, `${ph.name}.workflow.mjs`);
54373
- writeFileSync14(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
54960
+ const p = join42(orchDir, `${ph.name}.workflow.mjs`);
54961
+ writeFileSync15(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
54374
54962
  written.push(p);
54375
54963
  }
54376
54964
  }
54377
- const rb = join37(orchDir, "RUNBOOK.md");
54378
- writeFileSync14(rb, runbookMd(phases, run2, engineAbs));
54965
+ const rb = join42(orchDir, "RUNBOOK.md");
54966
+ writeFileSync15(rb, runbookMd(phases, run2, engineAbs));
54379
54967
  written.push(rb);
54380
54968
  return { exitCode: 0, written, notices, errors: [], phases };
54381
54969
  }
@@ -54384,8 +54972,8 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
54384
54972
  import { createInterface as createInterface2 } from "readline";
54385
54973
 
54386
54974
  // src/mcp/handlers.ts
54387
- import { existsSync as existsSync22, readFileSync as readFileSync18, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
54388
- import { isAbsolute as isAbsolute3, join as join38, resolve as resolve8, sep as sep4 } from "path";
54975
+ import { existsSync as existsSync28, readFileSync as readFileSync24, realpathSync as realpathSync4, statSync as statSync11 } from "fs";
54976
+ import { isAbsolute as isAbsolute3, join as join43, resolve as resolve8, sep as sep5 } from "path";
54389
54977
 
54390
54978
  // src/project-lock.ts
54391
54979
  var chains = /* @__PURE__ */ new Map();
@@ -54433,8 +55021,8 @@ function requiredCwd(args2, defaults) {
54433
55021
  const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
54434
55022
  if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
54435
55023
  const abs = resolve8(cwd);
54436
- if (!existsSync22(abs)) throw new ToolError(`project root not found: ${abs}`);
54437
- if (!statSync10(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
55024
+ if (!existsSync28(abs)) throw new ToolError(`project root not found: ${abs}`);
55025
+ if (!statSync11(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
54438
55026
  return abs;
54439
55027
  }
54440
55028
  function standardOf(args2) {
@@ -54578,13 +55166,13 @@ function handlePackCheck(args2, cwd) {
54578
55166
  return { cwd, ...res };
54579
55167
  }
54580
55168
  function handleSampleCheck(cwd) {
54581
- const file = join38(cwd, ".ultra11yrc.json");
54582
- if (!existsSync22(file)) {
55169
+ const file = join43(cwd, ".ultra11yrc.json");
55170
+ if (!existsSync28(file)) {
54583
55171
  throw new ToolError(`no .ultra11yrc.json at ${cwd} \u2014 a page sample must be declared before it can be linted.`);
54584
55172
  }
54585
55173
  let raw;
54586
55174
  try {
54587
- raw = JSON.parse(readFileSync18(file, "utf8"));
55175
+ raw = JSON.parse(readFileSync24(file, "utf8"));
54588
55176
  } catch (e) {
54589
55177
  throw new ToolError(`.ultra11yrc.json is not valid JSON: ${e.message}`);
54590
55178
  }
@@ -54622,7 +55210,7 @@ function handleInit(args2, cwd) {
54622
55210
  const enginePath = resolve8("scripts/ultra11y.mjs");
54623
55211
  const written = [];
54624
55212
  if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
54625
- if (bool(args2.ci)) written.push(join38(cwd, ".github/workflows/a11y.yml"));
55213
+ if (bool(args2.ci)) written.push(join43(cwd, ".github/workflows/a11y.yml"));
54626
55214
  if (!written.length) {
54627
55215
  throw new ToolError("nothing to install \u2014 pass hook:true and/or ci:true.");
54628
55216
  }
@@ -54642,8 +55230,8 @@ function reportText(args2, tool) {
54642
55230
  if (inline) return inline;
54643
55231
  if (!file) throw new ToolError(`\`report_text\` is required \u2014 the report markdown for ultra11y_${tool} to work on.`);
54644
55232
  if (!isAbsolute3(file)) throw new ToolError("`report_file` must be an absolute path.");
54645
- if (!existsSync22(file)) throw new ToolError(`report file not found: ${file}`);
54646
- return readFileSync18(file, "utf8");
55233
+ if (!existsSync28(file)) throw new ToolError(`report file not found: ${file}`);
55234
+ return readFileSync24(file, "utf8");
54647
55235
  }
54648
55236
  function handleCriteria(args2) {
54649
55237
  const lang = langOf(args2);
@@ -54658,21 +55246,21 @@ function handleCriteria(args2) {
54658
55246
  function handleRead(args2, cwd) {
54659
55247
  const raw = str2(args2.path);
54660
55248
  if (!raw) throw new ToolError("`path` is required \u2014 relative to the project root, or an absolute path inside it.");
54661
- const target = isAbsolute3(raw) ? raw : join38(cwd, raw);
55249
+ const target = isAbsolute3(raw) ? raw : join43(cwd, raw);
54662
55250
  let real;
54663
55251
  try {
54664
- real = realpathSync2(target);
55252
+ real = realpathSync4(target);
54665
55253
  } catch {
54666
55254
  throw new ToolError(`no such file: ${raw}`);
54667
55255
  }
54668
- const root = realpathSync2(cwd);
54669
- if (real !== root && !real.startsWith(root + sep4)) {
55256
+ const root = realpathSync4(cwd);
55257
+ if (real !== root && !real.startsWith(root + sep5)) {
54670
55258
  throw new ToolError(`path is outside the project: ${raw}. Use your own file tool for anything else.`);
54671
55259
  }
54672
- const st = statSync10(real);
55260
+ const st = statSync11(real);
54673
55261
  if (!st.isFile()) throw new ToolError(`not a file: ${raw}`);
54674
55262
  if (st.size > MAX_READ_BYTES) throw new ToolError(`file is too large to read (${st.size} bytes): ${raw}`);
54675
- const lines = readFileSync18(real, "utf8").split("\n");
55263
+ const lines = readFileSync24(real, "utf8").split("\n");
54676
55264
  const total = lines.length;
54677
55265
  const start2 = Math.max(1, Math.floor(num2(args2.start_line) ?? 1));
54678
55266
  if (start2 > total) throw new ToolError(`start_line ${start2} is past the end of the file (${total} lines).`);
@@ -55167,25 +55755,25 @@ function str3(v) {
55167
55755
  var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
55168
55756
 
55169
55757
  // src/mcp/resources.ts
55170
- import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync11 } from "fs";
55171
- import { basename as basename3, dirname as dirname10, join as join39, resolve as resolve9, sep as sep5 } from "path";
55758
+ import { existsSync as existsSync29, readdirSync as readdirSync6, readFileSync as readFileSync25, realpathSync as realpathSync5, statSync as statSync12 } from "fs";
55759
+ import { basename as basename3, dirname as dirname12, join as join44, resolve as resolve9, sep as sep6 } from "path";
55172
55760
  import { fileURLToPath as fileURLToPath4 } from "url";
55173
55761
  var SKILL_NAME = "ultra11y";
55174
55762
  var URI_SCHEME = "skill://";
55175
55763
  function resolveSkillRoot(moduleDir) {
55176
- const here = moduleDir ?? dirname10(fileURLToPath4(import.meta.url));
55764
+ const here = moduleDir ?? dirname12(fileURLToPath4(import.meta.url));
55177
55765
  const candidates2 = [resolve9(here, ".."), resolve9(here, "..", "skills", SKILL_NAME), resolve9(here, "..", "..", "skills", SKILL_NAME)];
55178
- return candidates2.find((dir) => existsSync23(join39(dir, "SKILL.md")));
55766
+ return candidates2.find((dir) => existsSync29(join44(dir, "SKILL.md")));
55179
55767
  }
55180
55768
  function listResources(moduleDir) {
55181
55769
  const root = resolveSkillRoot(moduleDir);
55182
55770
  if (!root) return [];
55183
55771
  const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
55184
- const refDir = join39(root, "references");
55185
- if (!existsSync23(refDir)) return out2;
55772
+ const refDir = join44(root, "references");
55773
+ if (!existsSync29(refDir)) return out2;
55186
55774
  for (const file of readdirSync6(refDir).sort()) {
55187
55775
  if (!file.endsWith(".md")) continue;
55188
- out2.push(describe(root, join39("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
55776
+ out2.push(describe(root, join44("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
55189
55777
  }
55190
55778
  return out2;
55191
55779
  }
@@ -55198,36 +55786,36 @@ function readResource(uri, moduleDir) {
55198
55786
  const rel2 = uri.slice(URI_SCHEME.length);
55199
55787
  if (!rel2) throw new ResourceError("empty resource path");
55200
55788
  const target = resolve9(root, rel2);
55201
- const rootReal = realpathSync3(root);
55789
+ const rootReal = realpathSync5(root);
55202
55790
  let targetReal;
55203
55791
  try {
55204
- targetReal = realpathSync3(target);
55792
+ targetReal = realpathSync5(target);
55205
55793
  } catch {
55206
55794
  throw new ResourceError(`no such resource: ${uri}`);
55207
55795
  }
55208
- if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep5)) {
55796
+ if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep6)) {
55209
55797
  throw new ResourceError(`resource path escapes the skill root: ${uri}`);
55210
55798
  }
55211
- if (!statSync11(targetReal).isFile()) throw new ResourceError(`not a file: ${uri}`);
55212
- return { uri, mimeType: "text/markdown", text: readFileSync19(targetReal, "utf8") };
55799
+ if (!statSync12(targetReal).isFile()) throw new ResourceError(`not a file: ${uri}`);
55800
+ return { uri, mimeType: "text/markdown", text: readFileSync25(targetReal, "utf8") };
55213
55801
  }
55214
55802
  var ResourceError = class extends Error {
55215
55803
  };
55216
55804
  function describe(root, rel2, fallbackTitle) {
55217
55805
  const decl = {
55218
- uri: `${URI_SCHEME}${rel2.split(sep5).join("/")}`,
55219
- name: rel2.split(sep5).join("/"),
55806
+ uri: `${URI_SCHEME}${rel2.split(sep6).join("/")}`,
55807
+ name: rel2.split(sep6).join("/"),
55220
55808
  title: fallbackTitle,
55221
55809
  mimeType: "text/markdown"
55222
55810
  };
55223
- const summary = firstProse(join39(root, rel2));
55811
+ const summary = firstProse(join44(root, rel2));
55224
55812
  if (summary) decl.description = summary;
55225
55813
  return decl;
55226
55814
  }
55227
55815
  function firstProse(file) {
55228
55816
  let text;
55229
55817
  try {
55230
- text = readFileSync19(file, "utf8");
55818
+ text = readFileSync25(file, "utf8");
55231
55819
  } catch {
55232
55820
  return void 0;
55233
55821
  }
@@ -55660,7 +56248,10 @@ Usage:
55660
56248
  ultra11y snapshot list [--root <dir>] [--json]
55661
56249
  ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
55662
56250
  ultra11y mcp [--transport stdio|http] [--cwd <dir>] [--allow-write] [--port <n>] [--bind <addr>] [--allow-remote] [--allow-origin <o>] [--max-response-bytes <n>]
55663
- ultra11y hook --claude-code (internal: the plugin's PreToolUse hook; payload on stdin)
56251
+ ultra11y hook --claude-code|--codex|--opencode (internal: the PreToolUse hook; payload on stdin)
56252
+ ultra11y install --claude-code | --codex | --opencode | --agents-md | --all [--project] [--dry-run] [--no-skills]
56253
+ ultra11y uninstall --claude-code | --codex | --opencode | --agents-md | --all [--project]
56254
+ ultra11y status [--json] [--project] (doctor: which agents will run the review by themselves)
55664
56255
  ultra11y dev [--port <n>] [--root <dir>] [--standard <pack>] [--lang auto|en|fr] (dev side-car: live overlay + per-page dashboard)
55665
56256
  ultra11y dev --next [--port <n>] (write the Next overlay component, then wire one line into your layout)
55666
56257
 
@@ -55962,7 +56553,10 @@ var COMMANDS = [
55962
56553
  "init",
55963
56554
  "pack",
55964
56555
  "orchestrate",
55965
- "hook"
56556
+ "hook",
56557
+ "install",
56558
+ "uninstall",
56559
+ "status"
55966
56560
  ];
55967
56561
  function isCommand(s) {
55968
56562
  return !!s && COMMANDS.includes(s);
@@ -56058,7 +56652,16 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
56058
56652
  "help",
56059
56653
  "version",
56060
56654
  "allow-remote",
56061
- "allow-write"
56655
+ "allow-write",
56656
+ // Harness selectors, shared by `hook` and `install`/`uninstall`/`status`. Without these
56657
+ // the parser warned "unknown flag --claude-code" on stderr on EVERY guarded shell call.
56658
+ "claude-code",
56659
+ "codex",
56660
+ "opencode",
56661
+ "agents-md",
56662
+ "all",
56663
+ "project",
56664
+ "no-skills"
56062
56665
  ]);
56063
56666
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([...VALUE_FLAGS2, ...BOOLEAN_FLAGS]);
56064
56667
  var LIST_FLAGS = /* @__PURE__ */ new Set(["include", "exclude", "ext"]);
@@ -56183,9 +56786,9 @@ async function cmdAudit(p) {
56183
56786
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
56184
56787
  const capturesDir = capturesFlag ?? ".ultra11y/captures";
56185
56788
  const scopedToDiff = p.flags.changed === true || p.flags.staged === true || since !== void 0;
56186
- const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync24(capturesDir));
56789
+ const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync30(capturesDir));
56187
56790
  const useCaptures = capturesWanted && !scopedToDiff && !inputs.includes(capturesDir);
56188
- const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync24(PAGES_DIR);
56791
+ const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync30(PAGES_DIR);
56189
56792
  const usePages = pagesWanted && !scopedToDiff && !inputs.includes(PAGES_DIR);
56190
56793
  const auditInputs = [...inputs, ...useCaptures ? [capturesDir] : [], ...usePages ? [PAGES_DIR] : []];
56191
56794
  if (useCaptures)
@@ -56226,10 +56829,10 @@ async function cmdAudit(p) {
56226
56829
  if (typeof p.flags.out === "string") {
56227
56830
  const out2 = p.flags.out;
56228
56831
  const asFile = out2.toLowerCase().endsWith(".json");
56229
- const target = asFile ? out2 : join40(out2, "audit-latest.json");
56832
+ const target = asFile ? out2 : join45(out2, "audit-latest.json");
56230
56833
  try {
56231
- mkdirSync12(asFile ? dirname11(out2) : out2, { recursive: true });
56232
- writeFileSync15(target, JSON.stringify(result, null, 2) + "\n");
56834
+ mkdirSync14(asFile ? dirname13(out2) : out2, { recursive: true });
56835
+ writeFileSync16(target, JSON.stringify(result, null, 2) + "\n");
56233
56836
  console.error(lang === "fr" ? `\u2192 audit \xE9crit dans ${target}` : `\u2192 audit written to ${target}`);
56234
56837
  } catch {
56235
56838
  }
@@ -56248,7 +56851,7 @@ async function cmdAudit(p) {
56248
56851
  const baselineFlag = p.flags.baseline;
56249
56852
  if (typeof baselineFlag === "string" && baselineFlag) {
56250
56853
  let baseline = null;
56251
- if (existsSync24(baselineFlag)) {
56854
+ if (existsSync30(baselineFlag)) {
56252
56855
  try {
56253
56856
  const parsed = JSON.parse(readText(baselineFlag));
56254
56857
  if (isCurrentAudit(parsed)) baseline = parsed;
@@ -56302,11 +56905,11 @@ async function cmdDev(p) {
56302
56905
  return 2;
56303
56906
  }
56304
56907
  if (p.flags.next === true) {
56305
- const dir = join40(root, ".ultra11y", "next");
56908
+ const dir = join45(root, ".ultra11y", "next");
56306
56909
  const rel2 = ".ultra11y/next/overlay.jsx";
56307
56910
  try {
56308
- mkdirSync12(dir, { recursive: true });
56309
- writeFileSync15(join40(dir, "overlay.jsx"), nextOverlayComponent(port));
56911
+ mkdirSync14(dir, { recursive: true });
56912
+ writeFileSync16(join45(dir, "overlay.jsx"), nextOverlayComponent(port));
56310
56913
  } catch (e) {
56311
56914
  console.error(`ultra11y dev: could not write ${rel2}: ${e instanceof Error ? e.message : String(e)}`);
56312
56915
  return 1;
@@ -56394,7 +56997,7 @@ async function cmdSnapshot(p) {
56394
56997
  2
56395
56998
  )
56396
56999
  );
56397
- else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join40(root, PAGES_DIR)}.` : `No snapshot in ${join40(root, PAGES_DIR)}.`);
57000
+ else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join45(root, PAGES_DIR)}.` : `No snapshot in ${join45(root, PAGES_DIR)}.`);
56398
57001
  else for (const s of snaps) console.log(`${s.meta.id} ${s.meta.name} ${s.meta.url}${s.meta.auth ? " [auth]" : ""}`);
56399
57002
  return 0;
56400
57003
  }
@@ -56439,12 +57042,12 @@ async function cmdSnapshot(p) {
56439
57042
  }
56440
57043
  if (typeof payload.screenshot === "string" && payload.screenshot) {
56441
57044
  try {
56442
- writeFileSync15(join40(dir, "screen.png"), Buffer.from(payload.screenshot, "base64"));
57045
+ writeFileSync16(join45(dir, "screen.png"), Buffer.from(payload.screenshot, "base64"));
56443
57046
  } catch {
56444
57047
  console.error("ultra11y snapshot write: the screenshot could not be written \u2014 the pixel tier will be skipped for this page.");
56445
57048
  }
56446
57049
  }
56447
- const result = runAudit({ inputs: [join40(dir, "dom.html")], onWarn: (m) => console.error(m) });
57050
+ const result = runAudit({ inputs: [join45(dir, "dom.html")], onWarn: (m) => console.error(m) });
56448
57051
  const failOnRaw = p.flags["fail-on"];
56449
57052
  const failOnParsed = parseFailOn(failOnRaw);
56450
57053
  if (failOnRaw !== void 0 && failOnParsed === null) {
@@ -56461,12 +57064,7 @@ async function cmdSnapshot(p) {
56461
57064
  }
56462
57065
  function cmdInit(p) {
56463
57066
  const root = repoRoot() ?? process.cwd();
56464
- let engineRel = process.argv[1] ?? "scripts/ultra11y.mjs";
56465
- try {
56466
- const abs = realpathSync4(engineRel);
56467
- engineRel = abs.startsWith(root + sep6) ? relative3(root, abs) : abs;
56468
- } catch {
56469
- }
57067
+ const engineRel = resolveEnginePath(root);
56470
57068
  const failOnParsed = parseFailOn(p.flags["fail-on"]);
56471
57069
  if (p.flags["fail-on"] !== void 0 && failOnParsed === null) {
56472
57070
  console.error(`ultra11y init: --fail-on must be blocking|major|minor (got "${String(p.flags["fail-on"])}").`);
@@ -56481,9 +57079,9 @@ function cmdInit(p) {
56481
57079
  if (want.baseline) {
56482
57080
  const inputs = p.positionals.length ? p.positionals : ["."];
56483
57081
  const result = runAudit({ inputs, onWarn: (m) => console.error(m) });
56484
- mkdirSync12(join40(root, "audits"), { recursive: true });
56485
- const bp = join40(root, "audits", "baseline.json");
56486
- writeFileSync15(bp, JSON.stringify(result, null, 2) + "\n");
57082
+ mkdirSync14(join45(root, "audits"), { recursive: true });
57083
+ const bp = join45(root, "audits", "baseline.json");
57084
+ writeFileSync16(bp, JSON.stringify(result, null, 2) + "\n");
56487
57085
  wrote.push(bp);
56488
57086
  }
56489
57087
  if (want.hook) wrote.push(writeHook(root, engineRel, failOn, legacy ? "baseline" : "staged"));
@@ -56494,17 +57092,74 @@ function cmdInit(p) {
56494
57092
  return 0;
56495
57093
  }
56496
57094
  async function cmdHook(p) {
56497
- if (p.flags["claude-code"] !== true) {
56498
- console.error("ultra11y hook: expected `hook --claude-code` (the payload is read on stdin).");
57095
+ const opencode = p.flags.opencode === true;
57096
+ if (!opencode && p.flags["claude-code"] !== true && p.flags.codex !== true) {
57097
+ console.error("ultra11y hook: expected `hook --claude-code`, `--codex` or `--opencode` (the payload is read on stdin).");
56499
57098
  return 2;
56500
57099
  }
56501
57100
  try {
56502
57101
  const decision = decide(JSON.parse(await readStdin()));
56503
- if (decision) process.stdout.write(JSON.stringify(decision));
57102
+ if (!decision) return 0;
57103
+ const out2 = decision.hookSpecificOutput;
57104
+ process.stdout.write(opencode ? `${out2.permissionDecisionReason}
57105
+
57106
+ ${out2.additionalContext}` : JSON.stringify(decision));
56504
57107
  } catch {
56505
57108
  }
56506
57109
  return 0;
56507
57110
  }
57111
+ var TARGET_FLAGS = "--claude-code | --codex | --opencode | --agents-md | --all";
57112
+ function cmdInstall(p) {
57113
+ const targets = parseTargets(p.flags);
57114
+ if (!targets) {
57115
+ console.error(`ultra11y install: pick at least one target: ${TARGET_FLAGS}`);
57116
+ console.error(" --all covers the agent harnesses; --agents-md is separate because it writes a tracked file into your repository.");
57117
+ return 2;
57118
+ }
57119
+ const dryRun = p.flags["dry-run"] === true;
57120
+ if (dryRun && targets.includes("agents-md")) {
57121
+ console.log(agentsMdBlock(repoRoot() ?? process.cwd()));
57122
+ if (targets.length === 1) return 0;
57123
+ }
57124
+ const results = installForTargets({ targets, project: p.flags.project === true, dryRun, skills: p.flags["no-skills"] !== true });
57125
+ return reportInstall(results, dryRun ? "would wire" : "wired");
57126
+ }
57127
+ function cmdUninstall(p) {
57128
+ const targets = parseTargets(p.flags);
57129
+ if (!targets) {
57130
+ console.error(`ultra11y uninstall: pick at least one target: ${TARGET_FLAGS}`);
57131
+ return 2;
57132
+ }
57133
+ return reportInstall(uninstallForTargets({ targets, project: p.flags.project === true }), "removed");
57134
+ }
57135
+ function reportInstall(results, verb) {
57136
+ let failed2 = 0;
57137
+ for (const r of results) {
57138
+ if (r.error) {
57139
+ failed2++;
57140
+ console.error(`ultra11y: ${r.error}`);
57141
+ continue;
57142
+ }
57143
+ const changed = r.reports.filter((x) => x.changed);
57144
+ if (changed.length === 0) console.log(`ultra11y ${r.target}: already ${verb} (nothing to do)`);
57145
+ for (const x of changed) console.log(`ultra11y ${r.target}: ${verb} ${x.path}${x.backup ? ` (backed up to ${x.backup})` : ""}`);
57146
+ for (const g of r.guidance) console.log(` ${g}`);
57147
+ }
57148
+ return failed2 ? 1 : 0;
57149
+ }
57150
+ function cmdStatus(p) {
57151
+ const rows = statusReport({ project: p.flags.project === true });
57152
+ if (p.flags.json === true) {
57153
+ console.log(JSON.stringify({ version: VERSION, targets: rows }, null, 2));
57154
+ return 0;
57155
+ }
57156
+ console.log(`ultra11y ${VERSION}`);
57157
+ for (const r of rows) {
57158
+ console.log(` ${r.target.padEnd(12)} ${r.wired ? "wired " : "not wired"} ${r.path}`);
57159
+ if (r.note) console.log(` ${"".padEnd(12)} ${r.note}`);
57160
+ }
57161
+ return 0;
57162
+ }
56508
57163
  function cmdCriteria(p) {
56509
57164
  if (p.flags.generate === true) {
56510
57165
  process.stdout.write(renderCriteriaReference());
@@ -56632,8 +57287,8 @@ async function cmdPrd(p) {
56632
57287
  return gh2 && gh2.failed > 0 ? 1 : 0;
56633
57288
  }
56634
57289
  function depsAt(root) {
56635
- const pkgPath = join40(root, "package.json");
56636
- if (!existsSync24(pkgPath)) return {};
57290
+ const pkgPath = join45(root, "package.json");
57291
+ if (!existsSync30(pkgPath)) return {};
56637
57292
  try {
56638
57293
  const pkg = JSON.parse(readText(pkgPath));
56639
57294
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
@@ -56644,8 +57299,8 @@ function depsAt(root) {
56644
57299
  function engineRefFor(root) {
56645
57300
  let ref = process.argv[1] ?? "scripts/ultra11y.mjs";
56646
57301
  try {
56647
- const abs = realpathSync4(ref);
56648
- ref = abs.startsWith(root + sep6) ? relative3(root, abs) : abs;
57302
+ const abs = realpathSync6(ref);
57303
+ ref = abs.startsWith(root + sep7) ? relative4(root, abs) : abs;
56649
57304
  } catch {
56650
57305
  }
56651
57306
  return ref;
@@ -56659,24 +57314,24 @@ function cmdRender(p) {
56659
57314
  console.error(`ultra11y render: --runner must be playwright|cypress|auto (got "${forced}").`);
56660
57315
  return 2;
56661
57316
  }
56662
- const detected = detectE2eRunner(depsAt(root), (f) => existsSync24(join40(root, f)));
57317
+ const detected = detectE2eRunner(depsAt(root), (f) => existsSync30(join45(root, f)));
56663
57318
  const runners = forced && forced !== "auto" ? [forced] : detected;
56664
57319
  if (!runners.length) {
56665
57320
  console.error(e2eSetupPlan([], {}, lang));
56666
57321
  return 1;
56667
57322
  }
56668
57323
  const engineRef = engineRefFor(root);
56669
- const dir = join40(root, ".ultra11y", "e2e");
57324
+ const dir = join45(root, ".ultra11y", "e2e");
56670
57325
  const paths = {};
56671
57326
  try {
56672
- mkdirSync12(dir, { recursive: true });
57327
+ mkdirSync14(dir, { recursive: true });
56673
57328
  if (runners.includes("playwright")) {
56674
- writeFileSync15(join40(dir, "playwright.mjs"), playwrightFixture(engineRef));
57329
+ writeFileSync16(join45(dir, "playwright.mjs"), playwrightFixture(engineRef));
56675
57330
  paths.playwright = ".ultra11y/e2e/playwright.mjs";
56676
57331
  }
56677
57332
  if (runners.includes("cypress")) {
56678
- writeFileSync15(join40(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
56679
- writeFileSync15(join40(dir, "cypress-commands.mjs"), cypressCommands());
57333
+ writeFileSync16(join45(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
57334
+ writeFileSync16(join45(dir, "cypress-commands.mjs"), cypressCommands());
56680
57335
  paths.cypressPlugin = ".ultra11y/e2e/cypress-plugin.mjs";
56681
57336
  paths.cypressCommands = ".ultra11y/e2e/cypress-commands.mjs";
56682
57337
  }
@@ -56691,7 +57346,7 @@ function cmdRender(p) {
56691
57346
  if (p.flags.scaffold === true) {
56692
57347
  const out2 = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : "ultra11y-render.tsx";
56693
57348
  try {
56694
- writeFileSync15(out2, ssrHarness());
57349
+ writeFileSync16(out2, ssrHarness());
56695
57350
  } catch (e) {
56696
57351
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
56697
57352
  return 1;
@@ -56705,29 +57360,29 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56705
57360
  }
56706
57361
  if (p.flags.setup === true) {
56707
57362
  const rel2 = ".ultra11y/capture-setup.mjs";
56708
- const out2 = join40(root, rel2);
57363
+ const out2 = join45(root, rel2);
56709
57364
  try {
56710
- mkdirSync12(dirname11(out2), { recursive: true });
56711
- writeFileSync15(out2, captureSetup());
57365
+ mkdirSync14(dirname13(out2), { recursive: true });
57366
+ writeFileSync16(out2, captureSetup());
56712
57367
  } catch (e) {
56713
57368
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
56714
57369
  return 1;
56715
57370
  }
56716
57371
  let setupDeps = {};
56717
- const setupPkg = join40(root, "package.json");
56718
- if (existsSync24(setupPkg)) {
57372
+ const setupPkg = join45(root, "package.json");
57373
+ if (existsSync30(setupPkg)) {
56719
57374
  try {
56720
57375
  const pkg = JSON.parse(readText(setupPkg));
56721
57376
  setupDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
56722
57377
  } catch {
56723
57378
  }
56724
57379
  }
56725
- const tr = detectTestRunner(setupDeps, (f) => existsSync24(join40(root, f)));
57380
+ const tr = detectTestRunner(setupDeps, (f) => existsSync30(join45(root, f)));
56726
57381
  console.log(captureSetupPlan(tr, rel2, lang));
56727
57382
  const gaLine = ".ultra11y/captures/*.html text eol=lf linguist-generated=true";
56728
- const gaPath = join40(root, ".gitattributes");
57383
+ const gaPath = join45(root, ".gitattributes");
56729
57384
  try {
56730
- const existing = existsSync24(gaPath) ? readFileSync20(gaPath, "utf8") : "";
57385
+ const existing = existsSync30(gaPath) ? readFileSync26(gaPath, "utf8") : "";
56731
57386
  if (!existing.includes(".ultra11y/captures/")) {
56732
57387
  appendFileSync(gaPath, (existing && !existing.endsWith("\n") ? "\n" : "") + gaLine + "\n");
56733
57388
  console.log(lang === "fr" ? `.gitattributes : ajout\xE9 \xAB ${gaLine} \xBB` : `.gitattributes: added "${gaLine}"`);
@@ -56735,8 +57390,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56735
57390
  } catch {
56736
57391
  }
56737
57392
  try {
56738
- const giPath = join40(root, ".gitignore");
56739
- if (existsSync24(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync20(giPath, "utf8")))
57393
+ const giPath = join45(root, ".gitignore");
57394
+ if (existsSync30(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync26(giPath, "utf8")))
56740
57395
  console.error(
56741
57396
  lang === "fr" ? "\u26A0\uFE0F .ultra11y semble ignor\xE9 par .gitignore \u2014 les captures doivent \xEAtre committ\xE9es pour le gate (ajoutez \xAB !.ultra11y/captures/ \xBB)." : '\u26A0\uFE0F .ultra11y appears gitignored \u2014 captures must be committed for the gate (add "!.ultra11y/captures/").'
56742
57397
  );
@@ -56746,8 +57401,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56746
57401
  }
56747
57402
  if (p.flags.storybook === true || typeof p.flags.storybook === "string") {
56748
57403
  const sbDir = p.positionals[0] ?? "storybook-static";
56749
- const indexPath = existsSync24(join40(sbDir, "index.json")) ? join40(sbDir, "index.json") : join40(sbDir, "stories.json");
56750
- if (!existsSync24(indexPath)) {
57404
+ const indexPath = existsSync30(join45(sbDir, "index.json")) ? join45(sbDir, "index.json") : join45(sbDir, "stories.json");
57405
+ if (!existsSync30(indexPath)) {
56751
57406
  console.error(
56752
57407
  lang === "fr" ? `ultra11y render : aucun index Storybook (index.json/stories.json) dans ${sbDir}.` : `ultra11y render: no Storybook index (index.json/stories.json) in ${sbDir}.`
56753
57408
  );
@@ -56757,7 +57412,7 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56757
57412
  const provById = new Map(stories.map((s) => [s.id, storyProvenance(s)]));
56758
57413
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
56759
57414
  const htmlDir = capturesFlag ?? sbDir;
56760
- const htmlFiles = existsSync24(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
57415
+ const htmlFiles = existsSync30(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
56761
57416
  const outDir = ".ultra11y/captures";
56762
57417
  let attributed = 0;
56763
57418
  let skipped = 0;
@@ -56779,8 +57434,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56779
57434
  continue;
56780
57435
  }
56781
57436
  try {
56782
- mkdirSync12(outDir, { recursive: true });
56783
- writeFileSync15(join40(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
57437
+ mkdirSync14(outDir, { recursive: true });
57438
+ writeFileSync16(join45(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
56784
57439
  ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
56785
57440
  attributed++;
56786
57441
  } catch {
@@ -56802,11 +57457,11 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
56802
57457
  }
56803
57458
  if (p.flags.coverage === true) {
56804
57459
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
56805
- const capturesDir = capturesFlag ?? join40(root, ".ultra11y/captures");
57460
+ const capturesDir = capturesFlag ?? join45(root, ".ultra11y/captures");
56806
57461
  const graphExt = [...GRAPH_ONLY_EXT, ...asList(p.flags.ext) ?? []];
56807
57462
  const sourceFiles = discover([root], { include: asList(p.flags.include), exclude: asList(p.flags.exclude), ext: graphExt }).files;
56808
57463
  const graph = buildGraphStreaming(sourceFiles);
56809
- const capFiles = existsSync24(capturesDir) ? discover([capturesDir]).files : [];
57464
+ const capFiles = existsSync30(capturesDir) ? discover([capturesDir]).files : [];
56810
57465
  const entries = capFiles.map((f) => ({ file: toPosix(f), provenance: parseCaptureProvenance(readText(f)) }));
56811
57466
  const cov = computeCaptureCoverage(graph, entries);
56812
57467
  if (p.flags.json) console.log(JSON.stringify(cov, null, 2));
@@ -56814,15 +57469,15 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
56814
57469
  return 0;
56815
57470
  }
56816
57471
  let deps = {};
56817
- const pkgPath = join40(root, "package.json");
56818
- if (existsSync24(pkgPath)) {
57472
+ const pkgPath = join45(root, "package.json");
57473
+ if (existsSync30(pkgPath)) {
56819
57474
  try {
56820
57475
  const pkg = JSON.parse(readText(pkgPath));
56821
57476
  deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
56822
57477
  } catch {
56823
57478
  }
56824
57479
  }
56825
- const detection = detectFrameworks(deps, (f) => existsSync24(join40(root, f)));
57480
+ const detection = detectFrameworks(deps, (f) => existsSync30(join45(root, f)));
56826
57481
  if (p.flags.json) console.log(JSON.stringify(detection, null, 2));
56827
57482
  else console.log(renderPlan(detection, lang));
56828
57483
  return 0;
@@ -57021,9 +57676,9 @@ function applyAdjudicationFile(p, adj, lang) {
57021
57676
  return 1;
57022
57677
  }
57023
57678
  const out2 = typeof p.flags.out === "string" ? p.flags.out : ".";
57024
- mkdirSync12(out2, { recursive: true });
57025
- const auditPath = join40(out2, "audit-latest.json");
57026
- writeFileSync15(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
57679
+ mkdirSync14(out2, { recursive: true });
57680
+ const auditPath = join45(out2, "audit-latest.json");
57681
+ writeFileSync16(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
57027
57682
  if (p.flags.json) console.log(JSON.stringify({ ok: true, auditPath, applied: r.applied, stillManual: r.stillManual, grounding: r.grounding }, null, 2));
57028
57683
  else
57029
57684
  console.log(
@@ -57093,7 +57748,7 @@ async function cmdScan(p) {
57093
57748
  return 0;
57094
57749
  }
57095
57750
  for (const target of p.positionals.filter((a) => a !== "-")) {
57096
- if (/^https?:\/\//i.test(target) || existsSync24(target)) continue;
57751
+ if (/^https?:\/\//i.test(target) || existsSync30(target)) continue;
57097
57752
  console.error(
57098
57753
  lang === "fr" ? `ultra11y scan : fichier introuvable (file not found) : ${target}. Passez une URL http(s):// ou un fichier HTML existant.` : `ultra11y scan: File not found: ${target}. Pass an http(s):// URL or an existing HTML file.`
57099
57754
  );
@@ -57218,12 +57873,12 @@ async function cmdScan(p) {
57218
57873
  audit2 = parsed;
57219
57874
  }
57220
57875
  const merged = mergeDynamic(audit2, dynamic, lang);
57221
- mkdirSync12(out2, { recursive: true });
57222
- writeFileSync15(join40(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
57876
+ mkdirSync14(out2, { recursive: true });
57877
+ writeFileSync16(join45(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
57223
57878
  if (p.flags.json) console.log(JSON.stringify(merged, null, 2));
57224
57879
  else
57225
57880
  console.log(
57226
- lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join40(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join40(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
57881
+ lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join45(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join45(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
57227
57882
  );
57228
57883
  return 0;
57229
57884
  }
@@ -57401,9 +58056,9 @@ function cmdOrchestrate(p) {
57401
58056
  );
57402
58057
  return 2;
57403
58058
  }
57404
- const engineAbs = realpathSync4(fileURLToPath5(import.meta.url));
58059
+ const engineAbs = realpathSync6(fileURLToPath5(import.meta.url));
57405
58060
  if (p.flags.list === true) {
57406
- if (!existsSync24(runFlag)) {
58061
+ if (!existsSync30(runFlag)) {
57407
58062
  console.error(`ultra11y orchestrate: run dir not found: ${runFlag}.`);
57408
58063
  return 2;
57409
58064
  }
@@ -57430,7 +58085,7 @@ function cmdOrchestrate(p) {
57430
58085
  );
57431
58086
  } else {
57432
58087
  console.log(
57433
- lang === "fr" ? `Suivez ${join40(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join40(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
58088
+ lang === "fr" ? `Suivez ${join45(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join45(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
57434
58089
  );
57435
58090
  }
57436
58091
  if (p.flags.phase === void 0 && workflows.length === 0 && p.flags.eco !== true) {
@@ -57516,6 +58171,12 @@ async function main(argv) {
57516
58171
  return cmdMcp(p);
57517
58172
  case "hook":
57518
58173
  return cmdHook(p);
58174
+ case "install":
58175
+ return cmdInstall(p);
58176
+ case "uninstall":
58177
+ return cmdUninstall(p);
58178
+ case "status":
58179
+ return cmdStatus(p);
57519
58180
  default:
57520
58181
  console.error(`ultra11y: "${p.command}" is not implemented yet`);
57521
58182
  return 1;
@@ -57526,7 +58187,7 @@ function isInvokedDirectly() {
57526
58187
  if (argv1 === void 0) return false;
57527
58188
  const modulePath = fileURLToPath5(import.meta.url);
57528
58189
  try {
57529
- if (realpathSync4(argv1) === realpathSync4(modulePath)) return true;
58190
+ if (realpathSync6(argv1) === realpathSync6(modulePath)) return true;
57530
58191
  } catch {
57531
58192
  }
57532
58193
  return import.meta.url === pathToFileURL3(argv1).href;