Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b7d42b
Add `withSubcommandsAndArguments` to alfa-command
rcj-siteimprove Apr 28, 2026
299eee0
Use internalId cross-ref serialization in JSON formatter
rcj-siteimprove Apr 28, 2026
39c6d64
Add interview, query commands and pre-recorded answers support
rcj-siteimprove May 8, 2026
37b4d25
Merge branch 'main' into cli-record-questions-mode
rcj-siteimprove May 8, 2026
1a488e3
Rename interview command to review
rcj-siteimprove May 8, 2026
7eb8edd
Await ensureAlfaDir before writing files
rcj-siteimprove May 8, 2026
a4dd2f8
Move alfa agent skill into alfa-cli package
rcj-siteimprove May 8, 2026
c38f0b5
Add changeset for default format change earl -> json
rcj-siteimprove May 8, 2026
68b9b65
Document synchronous Future assumption in recording oracle
rcj-siteimprove May 8, 2026
c7ae4a9
Warn and skip out-of-range numeric index in answer pairs
rcj-siteimprove May 8, 2026
7352fe6
Comment out experimental rules in review commands
rcj-siteimprove May 8, 2026
967fb2c
Support arbitrary DOM nodes in findNodeByPath
rcj-siteimprove May 8, 2026
bd725e9
Use flat tree for node path output in query command
rcj-siteimprove May 8, 2026
b831e33
Extract API
github-actions[bot] May 8, 2026
dd8fb59
Replace alfa-dir.ts with paths.ts and remove --alfa-dir flag
rcj-siteimprove May 8, 2026
edfd958
Remove alfa review list and status subcommands
rcj-siteimprove May 8, 2026
7d60d9b
Replace alfa review subcommands with flat flag-based command
rcj-siteimprove May 8, 2026
e4169f8
Remove withSubcommandsAndArguments
rcj-siteimprove May 8, 2026
d613134
Reset command.ts
rcj-siteimprove May 8, 2026
fc487fc
Restrict answer value parsing to canonical values
rcj-siteimprove May 11, 2026
52291f9
Change --answer to non-repeatable space-separated flag
rcj-siteimprove May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/giant-dolls-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-cli": minor
---

**Changed:** The default output format for `alfa audit` is now `json` instead of `earl`. Pass `--format earl` explicitly to preserve the previous behaviour.
5 changes: 5 additions & 0 deletions .changeset/strict-pets-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-formatter-json": minor
---

**Changed:** The JSON-formatter now uses high verbosity when serializing the page and low when serializing the outcomes.
2 changes: 2 additions & 0 deletions docs/review/api/alfa-command.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export class Command<F extends Command.Flags = {}, A extends Command.Arguments =
static withArguments<F extends Command.Flags, A extends Command.Arguments>(name: string, version: string, description: string, flags: F, args: A, parent?: Option<Command>, run?: (command: Command<F, A, {}>) => Command.Runner<F, A>): Command<F, A, {}>;
// (undocumented)
static withSubcommands<F extends Command.Flags, S extends Command.Subcommands>(name: string, version: string, description: string, flags: F, subcommands: Mapper<Command, S>, parent?: Option<Command>, run?: (command: Command<F, {}, S>) => Command.Runner<F, {}>): Command<F, {}, S>;
// (undocumented)
static withSubcommandsAndArguments<F extends Command.Flags, A extends Command.Arguments, S extends Command.Subcommands>(name: string, version: string, description: string, flags: F, args: A, subcommands: Mapper<Command, S>, parent?: Option<Command>, run?: (command: Command<F, A, S>) => Command.Runner<F, A>): Command<F, A, S>;
}

// @public (undocumented)
Expand Down
17 changes: 17 additions & 0 deletions packages/alfa-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# @siteimprove/alfa-cli

The tool for all your accessibility needs on the command line.

## Install

```bash
npm install -g @siteimprove/alfa-cli
```

## Agent Skill

Install the agent skill to let your AI agent run accessibility audits on your behalf:

```bash
npx skills add Siteimprove/alfa-integrations/packages/alfa-cli/skills/alfa
```
82 changes: 82 additions & 0 deletions packages/alfa-cli/skills/alfa/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: alfa
description: "Run accessibility audits using the Alfa CLI against local or remote web apps. Use when asked to audit accessibility, find WCAG failures, run alfa, check for a11y issues, or answer oracle interview questions."
---

## When to Use This Skill

- User asks to run an accessibility audit
- User wants to find WCAG failures on a page
- User mentions `alfa`, `a11y`, or `accessibility audit`
- User wants to understand or fix audit failures

## Prerequisites

- `alfa` CLI installed and on `$PATH`
- The target app is running and reachable (e.g. `http://localhost:5173/`)

## Workflow

### 1. Run the Review (Oracle Q&A)

Some checks cannot be automated and require human judgment. Run the review first to pre-answer these questions so the subsequent audit can resolve them automatically.

```bash
alfa review --start <url>
```

Alfa prints a list of questions. Each question has a **hash**, a **subject** (XPath to the node the question is about),
a **context** (XPath to a context node), a **type**, and a description. Answer them with:

```bash
alfa review --answer "hash1=value1 hash2=value2 ..."
```

**ALWAYS** ask the user for confirmation before answering questions, unless explicitly told to autonomously find answers.

**DO NOT** use browser, curl etc. unless explicitly allowed.

- `boolean`: `true` or `false`.
- `color[]`: comma-separated CSS hex colors (e.g. `#16213e`).
- `node`: XPath string, or `null` if no such element exists.
- Only basic XPath syntax supported: "/html[1]/body[1]/div[2]"

Answering may unlock follow-up questions — keep running `alfa review --answer` until no new questions appear.

When all questions are processed, **always reset the session**:

```bash
alfa review --reset
```

### 2. Run the Audit

```bash
alfa audit -o .alfa/<name-of-page>.json <url>
```

Give the output file a meaningful name such as "home.json" or "about.json".

### 3. Query the Results

**Inspect the first issue:**

```bash
alfa query --take 1 .alfa/home.json
```

**Inspect the second issue:**

```bash
alfa query --skip 1 --take 1 .alfa/home.json
```

**Look up a specific node by internalId:**

```bash
alfa query --node <internalId> .alfa/home.json
```

Use flag `--format path|html|json` (Default `path`) to view different representations of nodes.

Report the issues. **DO NOT** fix issues unless the user specifically asks you to.
8 changes: 6 additions & 2 deletions packages/alfa-cli/src/alfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Err } from "@siteimprove/alfa-result";
import * as pkg from "./pkg.js";

import audit from "./alfa/command/audit.js";
import review from "./alfa/command/review.js";
import query from "./alfa/command/query.js";
import scrape from "./alfa/command/scrape.js";

const {
Expand All @@ -31,9 +33,11 @@ const application = Command.withSubcommands(
},
(self) => ({
audit: audit(self),
review: review(self),
query: query(self),
scrape: scrape(self),
}),
None
None,
);

application
Expand All @@ -54,7 +58,7 @@ application
output = result.getErrUnsafe();
}

output = output.trimRight();
output = output.trimEnd();

if (output.length > 0) {
stream.write(output + "\n");
Expand Down
12 changes: 6 additions & 6 deletions packages/alfa-cli/src/alfa/command/audit/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Flags = {
output: Flag.string(
"output",
`The path to write results to. If no path is provided, results are written
to stdout.`
to stdout.`,
)
.type("path")
.alias("o")
Expand All @@ -19,7 +19,7 @@ export const Flags = {
interviewer: Flag.string(
"interviewer",
`The interviewer to use for answering questions during the audit. If not
provided, questions will be left unanswered.`
provided, questions will be left unanswered.`,
)
.type("name or package")
.alias("i")
Expand All @@ -28,13 +28,13 @@ export const Flags = {
format: Flag.string("format", "The reporting format to use.")
.type("name or package")
.alias("f")
.default("earl"),
.default("json"),

outcomes: Flag.string(
"outcome",
`The type of outcome to include in the results. If not provided, all types
of outcomes are included. This flag can be repeated to include multiple
types of outcomes.`
types of outcomes.`,
)
.choices("passed", "failed", "inapplicable", "cantTell")
.repeatable()
Expand All @@ -43,15 +43,15 @@ export const Flags = {
cpuProfile: Flag.string(
"cpu-profile",
`The path to write a CPU profile of the audit to. If no path is provided,
no CPU profile is made.`
no CPU profile is made.`,
)
.type("path")
.optional(),

heapProfile: Flag.string(
"heap-profile",
`The path to write a heap profile of the audit to. If no path is provided,
no heap profile is made.`
no heap profile is made.`,
)
.type("path")
.optional(),
Expand Down
34 changes: 28 additions & 6 deletions packages/alfa-cli/src/alfa/command/audit/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import type { Command } from "@siteimprove/alfa-command";
import { Formatter } from "@siteimprove/alfa-formatter";
import { Interviewer } from "@siteimprove/alfa-interviewer";
import { Iterable } from "@siteimprove/alfa-iterable";
import { Option, None } from "@siteimprove/alfa-option";
import { None, Option } from "@siteimprove/alfa-option";
import type { Err } from "@siteimprove/alfa-result";
import { Result } from "@siteimprove/alfa-result";
import { Page } from "@siteimprove/alfa-web";

// TODO: replace with experimental rules once published
// import { experimentalRules, type Flattened } from "@siteimprove/alfa-rules";
// const { R98, R101 } = experimentalRules;
// const rules = [R98, R101] as Array<Flattened.Rule>;
import rules from "@siteimprove/alfa-rules";

import { Profiler } from "../../profiler.js";
Expand All @@ -21,6 +25,10 @@ import type { Flags } from "./flags.js";

import * as scrape from "../scrape/run.js";

import { ANSWERS_PATH } from "../common/paths.js";
import { createAnsweringOracle } from "../common/answering-oracle.js";
import { readAnswers } from "../common/question-store.js";

export const run: Command.Runner<typeof Flags, typeof Arguments> = async ({
flags,
args: { url: target },
Expand All @@ -34,7 +42,7 @@ export const run: Command.Runner<typeof Flags, typeof Arguments> = async ({
const interviewer = Option.from(
await flags.interviewer
.map((interviewer) => Interviewer.load<any, any, any, any>(interviewer))
.getOr(undefined)
.getOr(undefined),
);

if (interviewer.some((interviewer) => interviewer.isErr())) {
Expand Down Expand Up @@ -65,14 +73,28 @@ export const run: Command.Runner<typeof Flags, typeof Arguments> = async ({
json = result.get();
}

const page = Page.from(JSON.parse(json));
const pageResult = Page.from(JSON.parse(json));

if (pageResult.isErr()) {
return pageResult;
}
const page = pageResult.getUnsafe();

const oracle = interviewer
// The early .some ensured it is an Ok.
// If oracle is provided by --interviewer, use that, otherwise check if there are any pre-recorded answers.
let oracle = interviewer
.map((interviewer) => interviewer.getUnsafe()(page, rules))
.getOr(undefined);

const audit = Audit.of(page.getUnsafe(), rules, oracle);
if (oracle === undefined) {
const answersFilePath = ANSWERS_PATH;
if (fs.existsSync(answersFilePath)) {
process.stderr.write(`Using answers from ${answersFilePath}\n`);
const answers = readAnswers(answersFilePath);
oracle = createAnsweringOracle(answers, page.document);
}
}

const audit = Audit.of(page, rules, oracle);

for (const _ of flags.cpuProfile) {
await Profiler.CPU.start();
Expand Down
Loading
Loading