# Command line and Pasty CLI

`pasty README.md` imports a local Markdown file into Pasty and opens it in your browser; `cat notes.md | pasty` reads stdin. `pasty new` opens a new blank document without an import. `--no-open` prints the URL without a browser, which is the form scripts and agents want.

```bash
pasty README.md              # create a Pasty document and open it in your browser
pasty push README.md         # send your local edits up to that document
pasty pull README.md         # bring your browser edits back into the file
pasty delete README.md       # delete the document (asks first; the file stays)
cat notes.md | pasty         # stdin works too
pasty new                    # open a new blank document
some-agent | pasty --no-open # scripts/agents: print the URL, skip the browser
```

## Install

```bash
uv tool install https://pasty.sh/cli/pasty_cli-0.2.0-py3-none-any.whl
```

That puts `pasty` on your PATH. `pipx install https://pasty.sh/cli/pasty_cli-0.2.0-py3-none-any.whl` works too. Either way it needs Python 3.10 or newer, and nothing else: the CLI has no dependencies.

To upgrade, run the install command from this page again, with `--force` if the version in the URL hasn't changed. `pasty --version` says which one you have.

## Usage

```
pasty [FILE] [--title TITLE] [--no-open]
pasty push [--force] FILE
pasty pull [--force] FILE
pasty delete [--yes] TARGET
pasty new | login | logout
```

- `FILE` omitted or `-` reads stdin.
- `--title` sets the document title. Otherwise the file name is used, then the first level-one heading, and an untitled document shows as "Untitled document".
- `pasty push FILE` sends a file's current contents up to the document it was opened as.
- `pasty pull FILE` replaces a file with the current contents of the document it was opened as.
- `pasty delete TARGET` deletes a document. `TARGET` is a document URL, a document id, or a linked file. It asks first; `--yes` (or `-y`) skips the question and is required when stdin is not a terminal.
- `pasty new` opens a blank document without creating or importing anything on the server; `pasty new --no-open` only prints its URL.
- `--no-open` skips the browser and just prints the URL, for imports and `new`.
- A file literally named `new`, `push`, `pull`, `delete`, `login`, or `logout` imports by spelling its path, for example `pasty ./new`.

### The output contract

Agents and scripts can rely on this:

- **stdout carries exactly one line: the primary result.** That is the document URL for an import, for `new` and for `push`; the file's canonical absolute path for `pull`; and the document id for `delete`. Everything else (status, errors) goes to stderr.
- Exit codes: `0` success, including when the browser could not be opened (the URL is still printed); `1` any failure; `2` usage errors.
- Errors are one clean line, with no tracebacks or HTML. `PASTY_DEBUG=1` re-raises for a traceback.

## Authentication

No account needed: an anonymous import prints a link that opens the document in the editor, and creating an account there claims it. To import straight into your account:

1. Generate a token at [pasty.sh/settings/cli](https://pasty.sh/settings/cli).
2. Run `pasty login` and paste it (input is hidden).

`pasty logout` removes the saved token. `PASTY_API_TOKEN` overrides it; prefer `pasty login` for interactive use, because an inline environment variable lands in your shell history. Config lives at `~/.config/pasty/config.json` (`$XDG_CONFIG_HOME` honoured; `%APPDATA%\pasty\config.json` on Windows), written with mode 0600.

## Pulling edits back

`pasty plan.md` sends the file up; `pasty pull plan.md` brings the browser's version back down, replacing the file in place.

An authenticated file import records which document the file became, in `~/.config/pasty/links.json`, along with a hash of the bytes it uploaded. That hash is what makes pull safe: **if the file changed locally since it was opened in Pasty, pull refuses**, because it has no way to merge and overwriting would silently lose the local work. `--force` replaces the file anyway. A file that has been deleted counts as changed; `--force` recreates it.

Pull writes through a temporary file in the same directory and renames it over the original, so an interrupted pull leaves the original untouched. A symlink keeps its link and its target is what changes. The file's permissions are preserved.

What is not linked, and so cannot be pulled:

- stdin imports, because there is no file to write back to;
- anonymous imports, because the document is a 7-day stash rather than yours to read back (the CLI says so, and suggests `pasty login`);
- a file that has since been moved or renamed, because its old path is what was recorded. Import it again to relink.

## Pushing edits up

`pasty push plan.md` is pull's mirror image: it sends the file's current contents up to the document it was opened as. The title is left alone — and not merely because the CLI does not send one: the push endpoint itself writes Markdown and nothing else, so a document is named in the browser and a file name can never become a rename.

```bash
pasty plan.md            # opens /d/<id>
# edit plan.md locally, or let an agent rewrite it
pasty push plan.md       # the browser now shows what the file holds
```

Push is as careful as pull, in the other direction. Every link records the revision the file was last synced at, and the push names it: **if the document changed in Pasty since then, push refuses**, rather than writing over whatever was typed in the browser. The message says the two ways out — `pasty pull FILE` to take the Pasty version, or `pasty push --force FILE` to overwrite it. A forced push is not destructive either: the server archives the version it displaced, exactly as `Keep mine` does in the browser.

A link recorded before `push` existed has no revision to build on, so push asks for `--force` rather than guessing. Pulling once records one.

Pushing into a document somebody is editing live is fine: their tabs notice within a poll and reload into the pushed text. Nothing is merged — push replaces the document's Markdown with the file's.

The same things that cannot be pulled cannot be pushed, and for the same reasons: stdin imports, anonymous imports, and files that have since moved.

## Deleting

`pasty delete` removes a document for good. Its share and edit links stop working for everyone holding them, and the local file is left exactly where it is.

```bash
pasty delete plan.md                      # a linked file
pasty delete https://pasty.sh/d/<id>      # the URL the CLI printed
pasty delete <id>                         # or just the id
```

Deletion cannot be undone, so it asks — but only when there is somebody there to ask:

| Where it runs              | What happens                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------------------------------- |
| A terminal                 | `Delete … ? This cannot be undone. [y/N]` on stderr. Anything but `y` or `yes` leaves the document alone. |
| `--yes` / `-y`             | Straight through, terminal or not.                                                                        |
| A pipe, a script, an agent | **Refused**, with a message naming `--yes`.                                                               |

The last row is the one worth explaining. Assuming yes would make every un-flagged automated call a silent permanent deletion; assuming no would make it a mystery. Refusing is the only answer that says what to do next, and it means `--yes` is an explicit statement of destructive intent rather than noise you paste out of habit. The question goes to stderr, so the output contract is unchanged.

The browser asks too, in its own way — `⌘K` → `Delete document` → confirm.

A delete prints the document id, and forgets every `links.json` entry that pointed at it, so nothing is left pointing at a document that no longer exists. Deleting something already gone — or something that was never in this account — is an error, not a silent success. A cancelled delete exits non-zero and prints nothing on stdout: the document is still there, and a caller reading the exit code must not conclude otherwise.

## Self-hosting

`PASTY_BASE_URL` points the CLI at another Pasty server (the default is `https://pasty.sh`). When set during `pasty login`, it is saved alongside the token, because a token is only meaningful against the server that minted it.

```bash
PASTY_BASE_URL=http://127.0.0.1:8000 pasty README.md --no-open
```
