# @pixelhop/dit

The `dit` CLI for [Did It Though?](https://diditthough.app) — visual proof for agent-built
software. An agent uploads the screenshots and screen recordings it produced, gets back
PR-ready Markdown, and later reads the humans' annotations as structured JSON.

Node 22+. No runtime dependencies.

```bash
npm install -g @pixelhop/dit
dit login
dit project create --name "Marketing site"
dit upload --project marketing-site --review pr-1234 --file shot.png
```

`dit login` on its own prints a link. Open it, approve, and the CLI has its own key —
nothing to copy, and no account needed beforehand. `--url` defaults to
`https://diditthough.app` and the token is remembered, so neither is repeated.

## Signing in

Sign in once; after that every command is just the command.

```bash
dit login
```

With no arguments this runs a browser approval. The CLI prints a link and a short code and
waits; a person opens the link, signs in or signs up, and clicks approve. The CLI is then
issued its own agent token scoped to that person's workspace. Nothing is ever copied
between windows, and the person's session is discarded immediately — it is a courier for
the approval, not the credential the agent keeps.

You can still pass a token directly, which is what CI does:

```bash
dit login --token dit_…
```

Either way the token is checked against the API before being stored — a token saved
without checking turns one clear failure here into a puzzling one on the next upload — and
written to `~/.config/dit/config.json` with `0600` permissions. `$XDG_CONFIG_HOME` is
respected, and `DIT_CONFIG` overrides the path outright. `dit logout` removes it.

### Where each value comes from

`--url` defaults to the hosted service, so you only pass it to point at a local
`wrangler dev`. Tokens are saved per URL, so signing in locally cannot quietly overwrite
the production one.

| Value | Resolution order                                                          |
| ----- | ------------------------------------------------------------------------- |
| URL   | `--url`, then `DIT_URL`, then `https://diditthough.app`                   |
| Token | `--token`, then `DIT_TOKEN`, then whatever `dit login` saved for that URL |

The saved token comes last on purpose: a one-off `--token`, or `DIT_TOKEN` in CI, wins
without anyone having to sign out first. Set `DIT_DEBUG=1` to print stack traces instead
of one-line errors.

## Commands

### `dit login` and `dit logout`

```bash
dit login                                            # browser approval
dit login --token dit_…                              # a token you already have
dit login --token dit_… --url http://localhost:3000  # a local server
dit logout
```

`login` also accepts the token from `DIT_TOKEN`, which keeps it out of your shell history.

### Uploading with no account at all

If nothing is signed in, `dit upload` does not fail. It creates a temporary workspace that
nobody owns yet, uploads into that, and prints a claim link alongside the usual Markdown:

```
This workspace has not been claimed. Everything uploaded here is deleted in 5 days unless
somebody claims it.
Claim it: https://diditthough.app/claim/…
```

Pass that link on. Whoever follows it and signs in becomes the owner, and the workspace
moves to the Free plan with everything in it intact — including evidence that has already
stopped displaying, because the visible window is derived from the current plan rather than
frozen at upload time. Unclaimed workspaces are small on purpose: one project, three
reviews, media visible for five days and recoverable for seven more.

Deployments can switch this off, in which case `dit upload` asks you to run `dit login`
instead.

### `dit project create`

A project is where reviews live, one per repository or site. An agent can make its own,
provided its token is workspace-scoped rather than pinned to a single project — a token
tied to one project cannot create another, because widening your own reach is not
something a credential should be able to do.

```bash
dit project create --name "Marketing site"
```

It prints the slug to pass to `--project` afterwards.

### `dit upload`

Uploads one or more files to a review, creating the review if it does not exist yet.

```bash
dit upload --project marketing-site --review pr-1234 \
  --title "Fix the mobile action bar" \
  --commit "$(git rev-parse HEAD)" --branch "$(git branch --show-current)" \
  --file shots/action-bar-mobile.png --file shots/action-bar-desktop.png
```

`--review` takes either an existing `rev_…` id or your own reference — a PR number, a run
id, anything stable. A reference that has been seen before reuses that review, so a job
that runs twice on the same PR adds screens to the existing review. To update an existing
screen, use `dit revision --artifact <id>` instead. `--project`
takes the project slug or its `prj_…` id.

Per-capture context, all optional: `--route /checkout`, `--viewport 390x844@3`,
`--scenario "logged out"`, `--title`, `--description`, and `--group`. Give each screen a
clear title and describe what the reviewer should check. These flags apply to every file
in a command; use separate commands with the same `--round` key when titles, descriptions
or viewports differ. Pass `--json` for the machine-readable form; the
default prints the review URL and the Markdown block to paste into a PR.

Images are sniffed by magic bytes (PNG, JPEG, WebP, GIF) and their dimensions read from
the file, so a mislabelled extension does not matter. For MP4 and WebM the CLI shells out
to `ffprobe` for duration and dimensions, and to `ffmpeg` for a poster frame — both are
optional. Without them the upload still succeeds, with a warning, minus the poster.

### Design alternatives

Use the same `--group` name within a review for alternatives to the same design. Upload
options separately so each has its own title, description, artifact ID and revision history:

```bash
dit upload --project marketing-site --review pr-1234 --file checkout-a.png \
  --title "Checkout — compact" --description "Shorter form with an inline order summary" \
  --group "Checkout layout" --round checkout-options --summary "Two checkout layouts to compare"
dit upload --project marketing-site --review pr-1234 --file checkout-b.png \
  --title "Checkout — split" --description "Form and order summary side by side" \
  --group "Checkout layout" --round checkout-options
```

Use `--json` to get artifact IDs and keep them for later `revision` commands. A variation group
connects alternatives; a round groups a batch of changes. The human chooses the preferred
option.

### Review rounds

Each upload or revision command creates one round; an upload includes all of its files. Use the same
`--round` key on upload and revision commands to group one batch of changes:

```sh
dit revision --artifact art_123 --file overview.png --round polish-2 --summary "Tightened spacing and simplified the mobile header"
dit upload --project my-app --review rev_123 --file mobile.png --round polish-2
```

Use a new key for the next batch. Historical round keys cannot be extended after a
new round starts. The first declaration supplies the round's optional summary.
Review rounds track evidence; screen titles, choices and feedback retain their
current state. Older uploads are shown as an Earlier uploads baseline.
Rounds let the reviewer step through the evidence timeline. Their Updated filter and
Mark as seen state are personal: agents must not clear them or verify the review.
After a feedback pass, reply, mark the relevant threads addressed, and return the review URL.

### `dit feedback`

```bash
dit feedback --review pr-1234                    # open threads, human-readable
dit feedback --review pr-1234 --status all --json
```

Threads come back with normalised 0–1 geometry and `timestampMs` for video notes, so an
agent can map a comment to the place it points at without knowing the render size.
`--status` is `open` (default), `addressed`, `resolved` or `all`.

### `dit reply` and `dit address`

```bash
dit reply   --thread thr_… --message "Fixed the padding, re-shot below."
dit address --thread thr_… --message "Done in 4a1c9f2."
```

`reply` adds a comment. `address` marks the thread as handled and leaves the resolve
decision to the human who raised it.

### `dit revision`

Uploads a new take of one artifact, keeping it linked to the original and to any notes on
it.

```bash
dit revision --artifact art_… --file shots/action-bar-mobile.png
```

### `dit markdown`

Reprints the PR Markdown for a whole review — every artifact, current revision, as image
links that GitHub will render inline.

```bash
dit markdown --review pr-1234 >> pr-body.md
```

## Exit codes

`0` success, `2` usage error (a bad or missing flag), `1` everything else, including API
errors. Errors go to stderr as a single line.

## Development

From the repository root:

```bash
pnpm --filter @pixelhop/dit build      # tsc → dist/
pnpm --filter @pixelhop/dit test       # vitest
pnpm --filter @pixelhop/dit typecheck
```

Releases are cut by tagging `cli-v<version>`, which runs `.github/workflows/publish-cli.yml`.
See that file for the publish path and the repository README for the service itself.