Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 2 deletions docs/content/agent-notes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ Choose an agent or control how it runs:

```sh
npx diffsplain \
--agent claude \
--agent codex \
--model MODEL_NAME \
--reasoning low \
--batch-size 2
```

The default batch size is 12 files. Smaller batches add the first notes to
the open page sooner.
the open page sooner. All supported agents accept `--model`; only Codex and
OpenCode accept `--reasoning`.

## File limits and failed notes

Expand Down
38 changes: 37 additions & 1 deletion docs/content/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,25 @@ The normal check does not send prompts to a provider. `--deep` runs local
`--help` checks for installed providers after a warning; it does not send a
provider prompt.

## First run and shutdown

Run the setup check through `npx`, which installs the package if needed:

```sh
npx diffsplain doctor
```

You can instead run `npm install -g diffsplain` and then `diffsplain doctor`.
Use `--no-agent` for a plain local review when no coding agent is ready.

A normal review prints a local URL and tries to open it in your browser. If the
browser cannot open, open that URL yourself. The command keeps watching until
you press <kbd>Ctrl</kbd>+<kbd>C</kbd>. It then stops the watcher, local server,
and any active note process. Normal shutdown removes temporary page and agent
input files. Saved notes remain in the user cache. Fetched Git objects remain
in the installed package's `.cache/git` folder. See
[Agent notes](/agent-notes/) and [Diff data](/data/) for paths and cleanup.

## Run from source

The `diffsplain` package script accepts every public command and option:
Expand Down Expand Up @@ -134,12 +153,29 @@ npm run doctor
| `--jobs COUNT` | Set agent passes to run at once, from `1` to `8`. |
| `--force` | Regenerate all agent notes instead of using cached notes. |
| `--remote NAME\|URL` | Choose the Git remote. The default is `origin`. |
| `--summaries FILE` | Choose the saved agent-note file. |
| `--output FILE` | Choose the live snapshot file. |
| `--cache-dir PATH` | Choose the bare Git cache folder. |
| `--codex-bin PATH` | Choose the Codex executable. |
Comment thread
itsjling marked this conversation as resolved.
| `--port NUMBER` | Choose an exact local port. The default starts at `2299`. |
| `--host ADDRESS` | Choose the page bind address. The default is `localhost`. |
| `--no-browser` | Do not open the page in a browser. |
| `-h`, `--help` | Show command help. |
| `-v`, `--version` | Show the installed version. |

Remote repos need `--branch` or `--pr`. You cannot combine two target options.
When you omit `--port`, Diffsplain increments from `2299` until it finds a free
For an exact local range, `--base` and `--head` must appear together. A branch
can use `--base` without `--head`; it cannot use `--head`. `--worktree` cannot
use another target. `--no-agent` cannot use `--summaries`. Paths passed to
`--repo`, `--summaries`, `--output`, `--cache-dir`, and `--codex-bin` resolve
from the current directory. `CODEX_BIN`, `CLAUDE_BIN`, `COPILOT_BIN`,
`CURSOR_BIN`, and `OPENCODE_BIN` can select provider binaries. Supported agents
accept `--model`. Only Codex and OpenCode accept `--reasoning`; its levels are
`minimal`, `low`, `medium`, `high`, and `xhigh`.

`--batch-size` defaults to `12` and accepts `1` through `50`. `--jobs` defaults
to `3` and accepts `1` through `8`. `--port` accepts `0` through `65535`. When
you omit it, Diffsplain starts at `2299` and increments until it finds a free
port. When you omit `--agent`, Diffsplain tries Codex, Claude, Copilot, then
OpenCode. It stops with an error if none are installed. Cursor reviews stay
disabled because Cursor Agent has no supported read-only, no-network, no-tool
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"dev": "vite --port 2299",
"docs:dev": "cd docs && blume dev",
"docs:build": "cd docs && blume build",
"docs:check": "cd docs && blume check && blume validate",
"docs:check": "node --test tests/cli-docs.test.mjs && cd docs && blume check && blume validate",
"docs:preview": "cd docs && blume preview",
"diffsplain": "node scripts/present.mjs",
"doctor": "node scripts/present.mjs doctor",
Expand Down
146 changes: 99 additions & 47 deletions scripts/cli-args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,63 @@ import {
enabledCodingAgents,
} from './coding-agents.mjs';

const valueOptions = new Set([
'--repo',
'--branch',
'--pr',
'--base',
'--head',
'--remote',
'--summaries',
'--output',
'--cache-dir',
'--codex-bin',
'--model',
'--reasoning',
'--batch-size',
'--jobs',
'--port',
'--host',
]);
const flagOptions = new Set([
'--help',
'--version',
'--agent',
'--no-agent',
'--force',
'--worktree',
'--no-browser',
]);
const pathOptions = new Set([
'--summaries',
'--output',
'--cache-dir',
'--codex-bin',
]);
function defineCliOptions(records) {
return Object.freeze(
Object.fromEntries(
Object.entries(records).map(([name, record]) => [
name,
Object.freeze(record),
]),
),
);
}

export const cliOptions = defineCliOptions({
'--repo': { kind: 'value' },
'--branch': { kind: 'value' },
'--pr': { kind: 'value' },
'--base': { kind: 'value' },
'--head': { kind: 'value' },
'--remote': { kind: 'value' },
'--summaries': { kind: 'value', path: true },
'--output': { kind: 'value', path: true },
'--cache-dir': { kind: 'value', path: true },
'--codex-bin': { kind: 'value', path: true },
'--model': { kind: 'value' },
'--reasoning': { kind: 'value' },
'--batch-size': { kind: 'value', default: 12, min: 1, max: 50 },
'--jobs': { kind: 'value', default: 3, min: 1, max: 8 },
'--port': { kind: 'value', default: 2299, min: 0, max: 65_535 },
'--host': { kind: 'value', default: 'localhost' },
'--help': { kind: 'flag' },
'--version': { kind: 'flag' },
'--agent': { kind: 'agent' },
'--no-agent': { kind: 'no-agent' },
'--force': { kind: 'flag' },
'--worktree': { kind: 'flag' },
'--no-browser': { kind: 'flag' },
});

const valueOptions = new Set(
Object.entries(cliOptions)
.filter(([, record]) => record.kind === 'value')
.map(([name]) => name),
);
const flagOptions = new Set(
Object.entries(cliOptions)
.filter(([, record]) => record.kind === 'flag')
.map(([name]) => name),
);
const pathOptions = new Set(
Object.entries(cliOptions)
.filter(([, record]) => record.path)
.map(([name]) => name),
);

const batchSizeOption = cliOptions['--batch-size'];
const jobsOption = cliOptions['--jobs'];
const portOption = cliOptions['--port'];
const hostOption = cliOptions['--host'];

export const helpText = `Usage: diffsplain [REPO] [options]

Expand All @@ -63,13 +87,17 @@ Options:
--no-agent Do not write agent notes
--model NAME Model for agent notes
--reasoning LEVEL Agent reasoning effort when supported
--batch-size COUNT Maximum files per agent pass (default: 12)
--jobs COUNT Agent passes to run at once (default: 3)
--batch-size COUNT Maximum files per agent pass (default: ${batchSizeOption.default})
--jobs COUNT Agent passes to run at once (default: ${jobsOption.default})
--force Regenerate all agent notes
--remote NAME|URL Git remote (default: origin)
--port NUMBER Local page port (default: 2299)
--host ADDRESS Page bind address (default: localhost)
--port NUMBER Local page port (default: ${portOption.default})
--host ADDRESS Page bind address (default: ${hostOption.default})
--no-browser Do not open the page in a browser
--summaries FILE Saved agent-note file
--output FILE Live snapshot file
--cache-dir PATH Bare Git cache folder
--codex-bin PATH Codex executable
-h, --help Show this help
-v, --version Show the installed version

Expand Down Expand Up @@ -316,8 +344,6 @@ export function parseCliArgs(
'--codex-bin',
'--model',
'--reasoning',
'--batch-size',
'--jobs',
]) {
const value = options.get(name);
if (value) {
Expand All @@ -327,6 +353,14 @@ export function parseCliArgs(
);
}
}
if (!noAgent) {
for (const name of ['--batch-size', '--jobs']) {
agentArgs.push(
name,
options.get(name) || String(cliOptions[name].default),
);
}
}

const reasoning = options.get('--reasoning');
if (
Expand All @@ -343,21 +377,39 @@ export function parseCliArgs(
const batchSize = options.get('--batch-size');
if (
batchSize &&
(!/^[1-9]\d*$/.test(batchSize) || Number(batchSize) > 50)
(
!/^[1-9]\d*$/.test(batchSize) ||
Number(batchSize) < batchSizeOption.min ||
Number(batchSize) > batchSizeOption.max
)
) {
fail('--batch-size must be a number from 1 to 50');
fail(
`--batch-size must be a number from ${batchSizeOption.min} to ${batchSizeOption.max}`,
);
}
const jobs = options.get('--jobs');
if (
jobs &&
(!/^[1-9]\d*$/.test(jobs) || Number(jobs) > 8)
(
!/^[1-9]\d*$/.test(jobs) ||
Number(jobs) < jobsOption.min ||
Number(jobs) > jobsOption.max
)
) {
fail('--jobs must be a number from 1 to 8');
fail(
`--jobs must be a number from ${jobsOption.min} to ${jobsOption.max}`,
);
}

const portValue = options.get('--port') || '2299';
if (!/^\d+$/.test(portValue) || Number(portValue) > 65_535) {
fail('--port must be a number from 0 to 65535');
const portValue = options.get('--port') || String(portOption.default);
if (
!/^\d+$/.test(portValue) ||
Number(portValue) < portOption.min ||
Number(portValue) > portOption.max
) {
fail(
`--port must be a number from ${portOption.min} to ${portOption.max}`,
);
}

return {
Expand All @@ -372,7 +424,7 @@ export function parseCliArgs(
agentArgs,
port: Number(portValue),
portWasPassed: options.has('--port'),
host: options.get('--host') || 'localhost',
host: options.get('--host') || hostOption.default,
browserEnabled: !options.has('--no-browser'),
forceSummaryRegeneration: options.has('--force'),
};
Expand Down
21 changes: 18 additions & 3 deletions tests/cli-args.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ test('leaves agent selection open when no agent is passed', () => {
assert.equal(parsed.host, 'localhost');
assert.equal(parsed.browserEnabled, true);
assert.deepEqual(parsed.feedArgs, ['--repo', cwd, '--checkout']);
assert.deepEqual(parsed.agentArgs, ['--repo', cwd, '--checkout']);
assert.deepEqual(parsed.agentArgs, [
'--repo',
cwd,
'--checkout',
'--batch-size',
'12',
'--jobs',
'3',
]);
});

test('accepts headless browser and explicit bind options', () => {
Expand Down Expand Up @@ -141,11 +149,15 @@ test('keeps split and equals-style values consistent', () => {
'--cache-dir',
resolve(cwd, 'state/git'),
]);
assert.deepEqual(split.agentArgs.slice(-4), [
assert.deepEqual(split.agentArgs.slice(-8), [
'--codex-bin',
resolve(cwd, 'bin/codex'),
'--model',
'review-model',
'--batch-size',
'12',
'--jobs',
'3',
]);
});

Expand Down Expand Up @@ -250,6 +262,7 @@ test('rejects a missing value for every value option', () => {
'--batch-size',
'--jobs',
'--port',
'--host',
'--agent',
]) {
assert.throws(
Expand Down Expand Up @@ -277,6 +290,8 @@ test('rejects duplicate options and aliases', () => {
['--no-agent', '--no-agent'],
['--worktree', '--worktree'],
['--force', '--force'],
['--no-browser', '--no-browser'],
['--host', 'localhost', '--host', '0.0.0.0'],
['-h', '--help'],
['-v', '--version'],
]) {
Expand Down Expand Up @@ -350,7 +365,7 @@ test('forces note regeneration only in the agent process', () => {

assert.equal(parsed.forceSummaryRegeneration, true);
assert.doesNotMatch(parsed.feedArgs.join(' '), /--force/);
assert.equal(parsed.agentArgs.at(-1), '--force');
assert.ok(parsed.agentArgs.includes('--force'));
});

test('rejects invalid reasoning and batch settings', () => {
Expand Down
Loading
Loading