# The `rask` CLI

`Rask.Cli` is a [.NET tool](https://learn.microsoft.com/dotnet/core/tools/global-tools) that gives
Rask a short, task-focused command line on top of the .NET SDK. It generates, or shells out to `dotnet`,
for almost everything it does — its one package reference is SQLite, which `rask db backup` needs to take
a consistent copy of a live database — and it never gets in the way of the tools you already use.

> **In a hurry?** The [cheat sheet](https://rask.sh/docs/guides/cheatsheet/) lists every command on one page, and the
> [recipes](https://rask.sh/docs/guides/recipes/) answer "how do I do X?" with the command and the wiring line.

## Install

```bash
curl -sSL https://rask.sh/rask.sh | sh
```

That puts a `rask` command on your `PATH`, along with the .NET SDK and the dependencies the CLI
shells out to. Re-run it to upgrade. On a machine that already has the .NET 10 SDK,
`dotnet tool install -g Rask.Cli` installs just the tool, and `dotnet tool update -g Rask.Cli`
upgrades it. Options, install locations and uninstall: [Installing Rask](https://rask.sh/docs/guides/installation/).

> `rask` is a thin, Rask-aware layer over the .NET SDK: it owns scaffolding end to end (`rask new`,
> and shells out to `dotnet` for the rest — `rask dev` wraps `dotnet watch`, `rask db`
> wraps `dotnet ef`.

## Getting help

`rask <command> --help` (or `-h`) prints that command's full reference — its arguments, an aligned
table of every option with a one-line description, and copy-pasteable examples:

```bash
rask                       # on a terminal: the new-project wizard. Piped: the command list.
rask --help                # the command list, always
rask new --help            # arguments, options, and examples for `new`
rask deploy --help
```

Help (and other output) is colorized when `rask` is writing to a terminal, and falls back to plain
text when the output is piped or when the [`NO_COLOR`](https://no-color.org) environment variable is
set — so `rask info | cat` and CI logs stay clean. Long descriptions wrap to the terminal's width;
piped output is never reflowed, so a line you grep for stays on one line.

## Short names mean one thing

A short flag means the same option on every command that has it, so muscle memory carries between
them:

| short | option |
| --- | --- |
| `-h` | `--help` (reserved CLI-wide; no command may claim it) |
| `-p` | `--project` |
| `-o` | `--output` |
| `-n` | `--name` (`rask new`) |
| `-t` | `--template` |
| `-f` | `--follow` |
| `-y` | `--yes` |

A few options have no short name on purpose, because the letter belongs to something else: `rask dev
--open` (`-o` is `--output`).

`--force` means *overwrite files* (`rask new`). Skipping a destructive confirmation is
`--yes` (`rask db drop`, `rask db restore`) — a different word, because it is a different power.

A test enforces all of this, so a new option cannot quietly reuse a letter.

## `--dry-run` and `--json`

**`--dry-run` lists what would happen and changes nothing**, in the same shape everywhere: one
`[dry-run] would …` line per action. It is on `new`, `dev`, `db` and `deploy`.

```bash
rask db drop --dry-run        # the exact `dotnet ef` command, without the database going anywhere
rask dev --dry-run            # the `dotnet watch` command line and the environment it sets
rask new Shop --dry-run                                        # the files it would write
```

`new` and `dev` print the same plan as a document with `--dry-run --json`:

```bash
rask new Shop --dry-run --json | jq -r '.files[]'      # template, name, directory, files
rask dev --dry-run --json | jq '.environment'          # command, arguments, workingDirectory, environment
```

There `--json` belongs to the dry run: without `--dry-run` it is a usage error (exit `2`), because neither
command has a document to print once it actually scaffolds or starts the app.

A dry run never prompts — it does nothing, so there is nothing to consent to.

**`--json` prints a document and nothing else**, so it pipes into `jq` without filtering banners out:

```bash
rask info --json
rask deploy status --json
rask db list --json
```

Errors still go to stderr and the exit code still distinguishes `2` (you typed something wrong) from
`1` (what you asked for failed), so a script never has to parse prose to find out what happened. Fields
that have no value are **absent** rather than carrying a human placeholder — `rask info --json` on a
machine with no SDK simply has no `dotnetSdk` key, where the human report prints `not found`.

## `rask new` — scaffold a project

```bash
rask                                 # the wizard, from a blank slate
rask new                             # the same wizard
rask new MyApp                       # everything: a server app with the whole stack wired
rask new MyApp -t wasm-hosted        # a WebAssembly app in Client/, served by an ASP.NET host
rask new MyApp -t react              # a React + TypeScript app in client/, served by an ASP.NET host
rask new MyApp -t vue                # the same with Vue — or preact, angular, solid, svelte, lit
rask new Blog --no-push --no-ops     # everything except those two
rask new Tiny --no-data --no-docker  # a lean project, one --no- at a time
rask new Shop --no-tests             # no Shop.Tests project beside the app
rask new Spa --template wasm         # an installable browser-WASM PWA
rask new Shop --islands react        # a server app with a React island beside the C# pages
```

**Batteries are included.** `rask new MyApp` gives you everything the template carries as standard — a
SQLite database, CQRS, background jobs, transactional email, a cache, a transactional outbox, file
storage for uploads, scheduled backups, a durable log store, the operator dashboard, an installable PWA with Web Push, a Dockerfile,
and the localization machinery. Not a sample page to delete: the wiring, ready for your first feature.

**One thing is left to you**, because it changes what the app *is* rather than what it can do:
**where the UI runs** — that is the **template**, not a flag. `server` renders pages live; `wasm-hosted`
writes them into `Client/` as a WebAssembly app an ASP.NET host serves
([serving a WebAssembly app](https://rask.sh/docs/guides/deployment/#serving-a-webassembly-app)); `wasm` is the same browser app with no backend at
all; `react`, `preact`, `vue`, `angular`, `solid`, `svelte` and `lit` each put that framework's TypeScript
app in `client/` on the same kind of host, with a typed client generated from your C# messages
([single-page app front ends](https://rask.sh/docs/guides/spa/#scaffolding-one)). It used to be a `--wasm` flag on `server`, which asked the same question twice — once as a project
type and again as a yes/no afterwards.

```csharp
var app = RaskApp.Create(args);

// Every other battery is on, and every setting lives in appsettings.json under "Rask" (docs/configuration.md).
app.Configure(c =>
{
    c.Jobs.Off();
    c.Push.Off();
});

app.Run<App>();
```

Turning a battery back on is deleting its line.

Run `rask` (or `rask new`) with no project name and — on a terminal — it walks you through a short
wizard: the project name, an arrow-key **project type** picker, **styling**, whether to add **auth**,
and a **battery checklist that arrives fully ticked** — space unticks anything you don't want. Pressing
enter through it gives you the same project a bare `rask new` does. It then scaffolds exactly as if
you'd passed the flags.

The wizard **fills gaps rather than re-asking**: anything already on the command line is kept and its
question skipped, so `rask new --template wasm --no-pwa` asks only for the name, and a `--no-` flag
already typed skips the checklist entirely. Piped or in a script (no terminal), a missing name is a
plain error instead, and bare `rask` prints the command list — so automation stays predictable.

Every project also gets a `.gitignore`, an `.editorconfig`, a `.slnx` solution and a `global.json`, and
is initialized as a git repository with one commit — `--no-git` skips that, and it is skipped
automatically inside an existing repository.

A `server` or `wasm` app also gets a test project, `<Name>.Tests/`, listed in the `.slnx` beside the app,
with one passing test — so `dotnet test` is green before you have written anything. `--no-tests` leaves
it out.

**`global.json` decides which SDK compiles the app**, and it names the .NET 10 band:

```json
{
  "sdk": {
    "version": "10.0.0",
    "rollForward": "latestFeature"
  }
}
```

Without it the SDK picks the newest one installed. On a machine that also carries the next major in
preview, that means a `net10.0` app is compiled by an `11.0.x` release candidate — it builds, tells you
so once per build (`NETSDK1057`), and quietly hands two people on the same repository different
compilers. `latestFeature` takes the newest SDK **within** the band and never crosses a major, so you
get `10.0.400` if you have it and the next patch when you install one.

The version is the band floor, `10.0.0`, rather than a real SDK release, and that matters while a band
is still in preview: a release candidate sorts *below* the release it is a candidate for, and
roll-forward only ever goes up — so pinning the release would resolve nothing at all on a machine
holding the candidate, and `allowPrerelease` does not rescue it. The floor is satisfied by every SDK in the band.

The trade is that a machine with **only** a newer major fails outright instead of building on it. Run
[`rask doctor`](#rask-doctor--check-before-you-hit-it) if a scaffold will not restore: it reads this
file and names it, rather than reporting a machine with no .NET on it — which is what a pin nothing
satisfies looks like from the outside, since `dotnet --version` is the thing that fails. Delete the
file if you would rather track whatever SDK is newest.

**Styling is not a choice: every project is Tailwind and the [UI kit](https://rask.sh/docs/guides/ui-kit/).** The compiler ships
inside the host package, so the build compiles `Styles/app.css` into `wwwroot/css/app.css` by scanning
the project's own source (see [Tailwind](https://rask.sh/docs/guides/tailwind/)), and the classes the kit's components write
arrive with `Rask.Ui` — so a C# project gets the whole component vocabulary with **no npm and no
`node_modules`**. There is no
config file and no property that turns it off. `--bootstrap` and `--tailwind` are gone, and both are
*refused* rather than ignored, because a flag the CLI accepts and then disregards is the most expensive
kind to discover.

**Every C# template draws the same starter page**, with the [UI kit](https://rask.sh/docs/guides/ui-kit/)'s Flux components and
plain Tailwind around them — a `Ui.Header` with the `Ui.Brand`, a `Ui.Heading`, a `Ui.Card` holding the
next steps and a primary `Ui.Button`, and a footer — so a project looks the same whether it runs on the
server or in WebAssembly. The
seven front-end templates draw one starter too, in plain Tailwind utilities: a front end brings its own
component library.
The sign-in pages `rask new` writes into `Features/Auth/` are drawn with the kit too — a `Ui.Card` of
`Ui.Input`s, a `Ui.Checkbox`, a primary `Ui.Button` and a `Ui.Callout` for what went wrong — and an app
with a database links sign-in from the starter's header.

The CLI writes the project's files itself, pins the `Rask.*` package references, and runs `dotnet
restore` so the output builds immediately.

React, Preact, Vue, Angular, Solid, Svelte and Lit come two ways: as a **template**, where the whole client
is that framework, or as [islands](https://rask.sh/docs/guides/islands/) inside a Rask page, scaffolded with `--islands` (below).
The meta-framework templates (Next, Nuxt, SvelteKit and the rest) were removed.

A new project has **wiring, not sample code** — there is still nothing to delete before you start — and
everything it scaffolds follows the vertical-slice layout the guides build on: feature code under
`Features/<Name>/`, cross-cutting code under `Features/Shared/`.

```
MyApp/
  MyApp.csproj
  Program.cs                      RaskApp.Create(args).Run<App>() — every battery on
  Dockerfile  .dockerignore       a production image
  appsettings.json                every Rask setting, under "Rask"; logging levels
  appsettings.Production.json     overrides applied when deployed
  Features/
    Shared/App.cs                 the root component every page renders through
    Shared/User.cs                the account type
    Shared/ErrorPage.cs           what a visitor sees when something outside a component throws
    Home/HomePage.cs              a [Route("/")] welcome page that teaches the CLI
    Auth/                         sign-in, registration, sign-out, password reset — your pages to restyle
  Migrations/                     the first migration, which the app applies when it starts
  Resources/Strings.en.json       the text of the UI, compiled into typed members
  wwwroot/                        manifest.webmanifest, icon.svg, offline.html
  Properties/launchSettings.json
  MyApp.slnx                      the app and its tests, for `dotnet test` and your IDE
  MyApp.Tests/
    MyApp.Tests.csproj            references the app, Rask.Testing and xUnit v3
    Features/Home/HomePageTests.cs  renders the home page and checks its greeting
```

The shell lives in `Features/Shared/`; the welcome page is its own `Features/Home/` slice. The account
flows come from [the auth battery](https://rask.sh/docs/guides/authentication/); the pages in `Features/Auth/` that drive them —
routed at `/login`, `/register`, `/logout` and so on — are yours. There is no database context to write:
`RaskAppDbContext` maps every aggregate you declare and every battery's tables. Add
pages and components to taste — the [tutorial](https://rask.sh/docs/guides/00-overview/) shows the shapes.

### It runs before you touch it

`rask new` doesn't stop at writing files. After the restore it **creates the first migration** for
you, and a `RaskApp` [applies its pending migrations when it starts](https://rask.sh/docs/guides/data/#migrations-apply-themselves-on-start),
so:

```bash
rask new Shop && cd Shop && rask dev
```

serves the app — there is no `rask db update` step. The migration is not a convenience: the
database-backed batteries keep their state in tables that only a migration creates, and the app can only
apply one that exists.

It reuses `rask db` to do it, so your project ends up in exactly the state `rask db add Init` leaves it
in — including installing the EF Core tools on first use. `--no-restore` skips the migration along with
the restore, and if it can't complete, `rask new` says so and prints the command to run rather than
failing: the files on disk are correct either way.

| Option | Meaning |
|--------|---------|
| `<name>` (or `--name`) | The project name. Required. |
| `--template`, `-t` | `server` (default), `wasm`, `wasm-hosted`, or a front end: `react`, `preact`, `vue`, `angular`, `solid`, `svelte`, `lit`. |
| `--no-pwa` | Leave out the web app manifest, service worker, icon and the wiring to serve them. Takes Web Push with it. |
| `--no-cqrs` | Leave out [`Rask.Cqrs`](https://rask.sh/docs/guides/cqrs/), the mediator. Your pages read through the [model surface](https://rask.sh/docs/guides/data/) without it, but they write through it: a save is a command whose handler loads the entity and calls `SaveChangesAsync`. The scaffold's plumbing needs it too — background jobs run through their command handlers, the outbox and `Rask.Data`'s domain events are published through it, and a `wasm-hosted` or front-end template's client messages arrive through it (so they refuse the flag). So it still takes the database with it, and every battery that maps onto a `DbContext` (below). It also takes [`Rask.Query`](https://rask.sh/docs/guides/query/), which rides along with the dispatcher: a dispatcher without a cache refetches on every render, so the cache is not a separate decision and has no flag of its own. |
| `--no-data` | Leave out the SQLite database (`c.Data.Off()`): no `RaskAppDbContext`, no WAL + `busy_timeout` connection, and no **continuous backup** ([Litestream](https://rask.sh/docs/guides/sqlite/#continuous-backup-with-litestream) — otherwise inert until you set `Rask:Litestream:ReplicaUrl`, so turning it on is one env var at deploy time: `rask deploy --env "Rask:Litestream:ReplicaUrl=s3://bucket/app"`). Takes every battery that maps onto a `DbContext` with it. |
| `--no-jobs` | Leave out durable background jobs (`c.Jobs.Off()`). |
| `--no-mail` | Leave out transactional email, delivered off the request thread; the dev default writes `.eml` files to `./mail-pickup` instead of needing SMTP. |
| `--no-cache` | Leave out the database-backed cache — the standard `IDistributedCache` plus a typed `ICache`. |
| `--no-storage` | Leave out [file storage](https://rask.sh/docs/guides/file-storage/) for uploads — a `StoredFile` row per file on the database, the bytes on disk (or in S3 or Azure, by configuration), and the routes that serve public and temporary links. |
| `--no-push` | Leave out server-sent [Web Push](https://rask.sh/docs/guides/webpush/) (`c.Push.Off()`): the subscriber table, `Push.Send(…)`, and the `/_rask/push/key`, `/_rask/push/subscribe` and `/_rask/push/unsubscribe` endpoints. The PWA stays. |
| `--no-snapshots` | Leave out scheduled point-in-time SQLite backups via the Online Backup API — a second line of defence alongside the continuous backup the database already wires. |
| `--no-logs` | Leave out the [durable log store](https://rask.sh/docs/guides/logging/) in a SQLite file of its own, which keeps the application log across a restart — buffered off the request thread, with retention by age and row count. The **only** battery unaffected by `--no-data`: it takes a connection string rather than a `DbContext`, so it needs no migration and works on an app with no database. |
| `--no-ops` | Leave out the [operator dashboard](https://rask.sh/docs/guides/dashboard/) at `/_rask` over every battery's table — queue depth, dead letters and the error behind each, the log, the live SQLite pragmas. It is gated on the `admin` role — the one the first account to register holds — because it shows job payloads, stored email bodies and log lines. |
| `--no-docker` | Leave out the production `Dockerfile` and `.dockerignore`. |
| `--no-tests` | Leave out the `<Name>.Tests` project — on `server` and `wasm`, the templates that scaffold one. It references the app and [`Rask.Testing`](https://rask.sh/docs/guides/testing/) and carries one passing test, `Home_page_greets_the_visitor`, so `dotnet test` works before you have written anything. |
| `--output`, `-o` | Target directory (defaults to a folder named after the project). |
| `--dry-run` | Print the files that would be created and write nothing (skips the restore, the build and the migration). |
| `--json` | With `--dry-run`: print that plan as JSON — `template`, `name`, `directory`, `files` — and nothing else. |
| `--force` | Scaffold into a directory that already contains files, overwriting on collision. Without it, any existing file the template would overwrite stops the command. |
| `--no-git` | Don't initialize a git repository (one is created with an initial commit by default). |
| `--no-restore` | Skip `dotnet restore` (for offline use), and with it the build and the first migration — there is nothing to build against. Without it, a restore failure is reported as a failure: the files are written, but the project won't build until it succeeds. |

The batteries wire a feature up; they don't scaffold sample pages for you to delete.

**The positive flags are gone.** `--data`, `--jobs`, `--ops`, `--all-batteries` and the rest turned
something on that is now already on, so they'd be flags the CLI accepts and disregards — the most
expensive kind to discover. They're rejected instead, with the answer:

```console
$ rask new Shop --data
--data is on by default now, so there is nothing to turn on. Pass --no-data to leave it out.

$ rask new Shop --all-batteries
--all-batteries is gone: every battery is on by default now. Pass --no-<battery> to leave one out, e.g. --no-push.
```

Every server app also gets `Features/Shared/ErrorPage.cs` and `app.UseExceptionHandler("/error")` outside
Development. `ErrorBoundary` already catches anything thrown *inside* a component tree; this covers
everything outside it, which would otherwise be a bare 500 with an empty body. The page renders through
your app shell and shows a **correlation id and nothing else** — the exception goes to `ILogger`, where you
match it by that id. Locally the handler stays off, because the developer exception page is strictly more
useful than a page designed to reveal nothing.

### Which template supports which flag

A template gets every battery in its column, and nothing outside it. Nobody maintains a per-template
default list: the default set *is* the column.

| Battery | `server` | `wasm` | `wasm-hosted` | front ends⁴ |
| --- | :-: | :-: | :-: | :-: |
| database, CQRS | ✅ | — | ✅ | ✅ |
| jobs, mail, cache, storage, outbox, snapshots, logs | ✅ | — | ✅ | ✅ |
| ops *(the operator dashboard)* | ✅ | — | ✅¹ | ✅¹ |
| PWA | ✅ | ✅ | ✅ | ✅³ |
| Web Push | ✅ | — | ✅ | ✅³ |
| Docker | ✅ | ✅ | ✅ | ✅ |
| localization *(in `appsettings.json`, not a flag)* | ✅ | —² | —² | — |
| `--islands <runtime>…` *(opt-in)* | ✅ | ✅ | ✅ | — |

¹ The operator dashboard is Rask components, so it needs a host built on `Rask.Server` to mount it.
`wasm-hosted` and the front ends are `RaskApp.Create(args).Serve()`, which serves the browser app *and*
server-renders the dashboard at `/_rask`.

³ On a front end both are the client's own files: `client/public/manifest.webmanifest` and `rask-sw.js`, and
`client/src/push.ts`, which subscribes through the browser's `PushManager` and the host's `/_rask/push`
endpoints.

⁴ `react`, `preact`, `vue`, `angular`, `solid`, `svelte` and `lit` — one column, because the host is the
same. Only the client differs.

### `--islands` — a React, Vue, Svelte, Solid, Lit, Angular, Preact or Blazor component

```bash
rask new Shop --islands react                  # one runtime
rask new Shop --islands react angular blazor   # several at once
rask new Shop --template wasm --islands vue    # in the browser-WASM host too
```

Each runtime named gets a paired `.cs` and front-end file in a folder of its own, its npm dependencies
merged into one root `package.json`, the tsconfig mapping that makes `@rask/<Name>.props` resolve, and
the package reference it needs. The home page renders each one — `ReactCounter.Caption("React island")`
in `Features/Home/HomePage.cs` — so `rask dev` shows it working before you have written anything. The
base class **is** the declaration; see [Islands](https://rask.sh/docs/guides/islands/).

One combination is refused by name, because the build would refuse it later: **`react` and `preact`
together.** `@vitejs/plugin-react` resolves Babel 8 while `@preact/preset-vite` pins a `@babel/core` 7
peer, so npm will not install both. A Preact component can also be rendered by `ReactComponent` if the
app aliases `react` to `preact/compat`.

`blazor` is the one kind with no npm side at all: the Razor SDK compiles the `.razor` and Rask renders
it server-side into the first response, so a Blazor-only island project gets no `package.json` and
never probes for Node.

² Languages are configured, never chosen on the command line — there is no `--culture` and no
`--no-localization`. Every C# template starts with English in `Rask:Cultures` — `appsettings.json` on
`server` and `wasm-hosted` (the host's file, which the browser half's build reads too), `wwwroot/appsettings.json`
on `wasm` — and an English `Resources/Strings.en.json` (`Client/Resources/` on `wasm-hosted`).

In the browser a language other than English is not free: culture data is roughly **a megabyte of extra
download** — on the WASM showcase a published trimmed bundle goes from 3.28 MB to 4.33 MB brotli (+32%). So
the build ships it only when the list names one, and adding `"hu"` is the whole edit.
See [localization](https://rask.sh/docs/guides/localization/#wasm-and-icu).

The wizard only offers what the chosen template supports, so an interactive run cannot assemble a
combination that is then rejected. On the command line, turning off something a template never had is a
usage error that names both halves:

```console
$ rask new X --template wasm --no-data
Template 'wasm' has nothing to change for: --no-data. It supports: docker, pwa.
```

The database-backed batteries need an ASP.NET host to put a database in, which `server`, `wasm-hosted` and the
front-end templates are — a pure browser-WASM app has none.

Turning one off takes its dependents with it, so you never end up with a battery whose database isn't there:

```bash
rask new Shop --no-data     # …and no jobs, mail, cache, storage, outbox, snapshots or dashboard
rask new Shop --no-cqrs     # …and no database either — writes, jobs, the outbox and domain events run through the mediator
rask new Shop --no-pwa      # …and no Web Push, which subscribes through the service worker
rask new Shop --no-logs     # …and nothing else: the log store owns a database of its own
```

On `server`, the order they are wired in — load-bearing rather than stylistic, like the Litestream restore
before anything opens the database, or authentication before `MapRask` — lives in `RaskApp`, not in your
`Program.cs`, and is pinned by tests rather than left to chance.

Turning off a battery a template doesn't have (for example `--no-cqrs` on `wasm`) fails fast with the
list of what that template *does* support, rather than passing an unknown option through.

## Writing code — by hand, from the guides

`rask` scaffolds a **project**; it does not scaffold code inside one. There is no `rask generate`.

Pages, components, CRUD slices, background jobs, emails and cached accessors are all ordinary C# you
write yourself, and every one of them is documented as code you can copy:

| What you want | Where the code is |
| --- | --- |
| A routed page, a reusable component | [Composition](https://rask.sh/docs/guides/composition/), [Routing](https://rask.sh/docs/guides/routing/) |
| A CRUD slice — entity, commands, queries, pages | [Tutorial ch.2](https://rask.sh/docs/guides/02-first-feature/), [Rask.Data](https://rask.sh/docs/guides/data/), [CQRS](https://rask.sh/docs/guides/cqrs/) |
| A background job | [Tutorial ch.4](https://rask.sh/docs/guides/04-background-jobs/), [Jobs](https://rask.sh/docs/guides/jobs/) |
| A transactional email | [Tutorial ch.5](https://rask.sh/docs/guides/05-email/), [Mail](https://rask.sh/docs/guides/mail/) |
| A cached read | [Tutorial ch.6](https://rask.sh/docs/guides/06-cache/), [Cache](https://rask.sh/docs/guides/cache/) |
| Domain events through the outbox | [Tutorial ch.7](https://rask.sh/docs/guides/07-outbox-events/), [Outbox](https://rask.sh/docs/guides/outbox/) |

The [tutorial](https://rask.sh/docs/guides/00-overview/) builds all of it in order, one chapter per pillar, so a snippet
that needs its surroundings has them by the time you reach it.

> **Why no generator?** Scaffolded code is read far more often than it is written, and a generator's
> output has to be understood line by line the first time you meet it anyway. Teaching the same code in
> the guides means there is one version of it — the one you can read, adapt, and keep — instead of a
> generated one plus a document describing it, drifting apart.

### When you get it wrong

A rejected command line always names what was wrong, what is allowed, and what to run next — and,
where there's an obvious candidate, what you probably meant:

```console
$ rask deplyo
Unknown command 'deplyo'. Did you mean 'deploy'?

$ rask new Shop --template srever
Option '--template' does not accept 'srever'. Did you mean 'server'? Choose one of: server, wasm, wasm-hosted, react, preact, vue, angular, solid, svelte, lit.
Usage: rask new <name> [options]
Run 'rask new --help' for details.

$ rask db
Specify a 'rask db' action: add, remove, list, update, drop, backup, restore.
```

`-h` is `--help` for every command, so no option has `-h` as a short name (`rask deploy --host`
has no short form). Anything after `--` is your app's, so `rask dev -- --help` passes it through.

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | The command ran and what you asked for failed — a build error, an unreachable host, a refused deploy. |
| `2` | The command line was wrong — an unknown command, option, action, or value; a missing value; options that contradict each other. |
| `130` | Interrupted with Ctrl+C. |

The `1` / `2` split is what lets a script tell a broken invocation from a broken deploy. The line
between them is where the bad input came from: anything decidable from the arguments alone is `2`,
while a value that could have come from `.rask/deploy.json` — or from the state of the disk, the
network, or the host — is `1`.

## `rask dev` — run with hot reload

```bash
rask dev                             # find the project, run it, open https://<name>.test
rask dev --no-open                   # …without the browser
rask dev --open                      # open a browser even on a plain localhost run
rask dev --project src/MyApp/MyApp.csproj
rask dev --urls http://localhost:5005
rask dev -- --my-app-flag            # everything after -- goes to the app
```

`rask dev` runs `dotnet watch run`, so editing a component's `Render()` (or a scoped `.css` / `.ts`) and
saving re-renders the open page live — see [what hot-reloads](#what-hot-reloads) below.

It finds the project for you: in a **client-plus-host** solution it picks the `.Server` host (the client is
built into it).

A host that references `Rask.Spa.Hosting` and has an **npm front end** — a `client` folder, or the one
`RaskSpaClientDir` names — gets two processes: `dotnet watch` for the host, and the front end's own dev
server (`npm run dev`, or `start` where that is what its `package.json` has). The browser opens on the dev
server, since that is what serves the app and what its hot reload reaches; the production bundle is not built
during the session. See [single-page app front ends](https://rask.sh/docs/guides/spa/#development).

It also sets up the environment the loop needs: `ASPNETCORE_ENVIRONMENT=Development` when you have not
set an environment yourself, and `HotReloadAutoRestart` so an edit hot reload *can't* apply restarts the
app instead of stopping at an interactive prompt. Pass `--no-restart` to be asked instead.

### `https://<name>.test`

An app called `AppName` is served on **`https://appname.test`** — a real name, real HTTPS, no port.
Nothing to install and nothing to configure: the name comes from the project, and `rask dev` sets the
machine up the first time you run it. macOS, Windows and Linux.

**`rask dev` opens it for you.** Not behind `--open`: that flag is opt-in on a plain localhost run
because the URL there is a guess out of a launch profile, while the `.test` name is one the command just
configured the machine for and issued a certificate covering. `--no-open` still stops it. The one case
it stands down is a `launchSettings.json` whose profile sets `"launchBrowser": true` — `dotnet watch`
honours that itself and no environment variable suppresses it, so opening as well would give you two
tabs every run. The scaffolded templates set it to `false` for exactly this reason; an older project
that still has it `true` opens the profile's `https://localhost:PORT` instead, and `rask dev` says so.

The first run asks for permission once, showing exactly what it will change:

```text
To serve this app on https://appname.test, Rask needs permission once to:
  • trust 'Rask Local Development CA' as a local certificate authority (System keychain)
  • add '127.0.0.1 appname.test' to /etc/hosts
  • redirect port 443 to this app (pf anchor 'com.apple/rask')

Set that up now? [Y/n]
```

Say no and it serves on localhost as it always did. **Every later run is silent** — the plan is
recomputed from the machine each time and comes back empty, so the password belongs to first-time setup
rather than to starting an app. The certificate authority is trusted **once, ever**: every other project
you run afterwards gets its own `.test` name with no prompt at all.

**The authority can sign for `.test` and loopback, and nothing else.** A root in the system trust store is
believed by every browser on the machine, and its key sits under `~/.rask`. So the certificate carries a
critical name constraint — `.test`, `localhost`, `127.0.0.1`, `::1` — and cannot mint a subordinate
authority: a copied key yields certificates for your own dev names and for no real site. An authority
created before this was added is replaced the next time `rask dev` runs: one more prompt, naming the
replacement, after which the old root is taken back out of the keychain (macOS) or certificate store
(Windows); on Linux trusting the new one overwrites it.

`.test` is not a stylistic choice. [RFC 6761 §6.2](https://www.rfc-editor.org/rfc/rfc6761#section-6.2)
reserves it for exactly this and guarantees it is never delegated in the real DNS root, so a dev name
can never collide with a site you actually need to reach. The two obvious alternatives are traps:
`.local` belongs to multicast DNS and on macOS is answered by `mDNSResponder` rather than `/etc/hosts`,
and `.dev` is a real gTLD on the HSTS preload list — which is what forced Valet off it when Chrome began
force-upgrading every `.dev` to HTTPS.

It is **HTTPS only**. No plaintext listener is bound, so the live WebSocket is `wss://`, a `Secure`
cookie behaves in development the way it will in production, and there is nothing on the machine serving
the app unencrypted. `dotnet dev-certs https` cannot provide this — it has no hostname option of any
kind and only ever mints `CN=localhost` — so Rask issues the certificate itself from its own local
authority, using .NET's X.509 stack rather than an installed `openssl` or `mkcert`.

The certificate covers the name, `*.appname.test` beneath it, and **the loopback names too** —
`localhost`, `127.0.0.1` and `::1`. Kestrel binds loopback, so those reach the very same app, and a
certificate that covered only the name turned every other way in — an older scaffold's launch profile, a
bookmark, an IDE's run button, `--no-host` — into a name-mismatch interstitial on a page that *is* your
app. A certificate issued before this covered the name alone, so `rask dev` re-mints it on the next run.

Everything it stores lives in `~/.rask/certs`, with private keys readable only by you.

#### What each platform actually does

The three steps are the same everywhere; what carries them out is not. The port is the one that
surprises people — **only macOS needs a redirect at all**, because it alone reserves ports below 1024
from an ordinary process.

| | macOS | Windows | Linux |
| --- | --- | --- | --- |
| **Trust** | System keychain, via `security add-trusted-cert` | `CurrentUser\Root`, through .NET's own API — no admin, just Windows' consent dialog | The distribution's CA anchors (`update-ca-certificates`, `update-ca-trust`, or `trust extract-compat`), plus NSS for Chrome and Firefox |
| **Hosts file** | `/etc/hosts` via `sudo install` | `System32\drivers\etc\hosts` via a UAC-elevated copy | `/etc/hosts` via `sudo install` |
| **Port 443** | Kestrel on 5001, pf anchor redirects 443 → 5001 | **Nothing.** Kestrel binds 443 directly | One sysctl (`net.ipv4.ip_unprivileged_port_start`), then Kestrel binds 443 |
| **Elevation** | `sudo`, primed once up front | UAC, on the hosts step only | `sudo`, primed once up front |

On **Linux**, port 443 is opened with a sysctl rather than a firewall rule on purpose: `nftables` and
`iptables` are actively managed by docker, ufw and firewalld, and injecting a redirect into them is far
likelier to collide with something you depend on. The trade-off is that the sysctl is machine-wide —
any unprivileged process may then bind 443 and up — which is why the prompt names it explicitly. It
resets on reboot.

Linux also needs `certutil` (from `libnss3-tools`) for **Chrome and Firefox**, which consult NSS
databases rather than the system anchors. Without it the system trust still works — curl, `wget` and
.NET are fine — and `rask dev` tells you what to install.

> **Firefox on macOS and Windows.** It keeps its own certificate store and never reads the operating
> system's, so it will warn on `appname.test` there until you add the CA to it by hand (Settings →
> Privacy & Security → View Certificates → Authorities → Import `~/.rask/certs/rask-local-ca.pem`).
> On Linux `certutil` handles it for you.

**To undo it:** delete the `# >>> rask dev >>>` block from your hosts file, and remove **Rask Local
Development CA** from Keychain Access (macOS), `certmgr.msc` → Trusted Root Certification Authorities
(Windows), or the anchor directory plus `certutil -D` (Linux). The macOS pf anchor and the Linux sysctl
both reset on reboot; Windows leaves nothing behind but the two above.

**When it stays on localhost.** It never fails a dev loop over a URL — any step that does not work falls
back to `http://localhost:5000` with a note. It is also skipped by design when:

| Situation | Why |
| --- | --- |
| `--no-host`, or `RASK_DEV_NO_HOST` is set | You asked for localhost. |
| `--urls` was passed | You named the addresses to listen on; serving somewhere else would be the opposite of helpful. |
| Not macOS, Windows or Linux | There is no implementation of the three steps for that platform. |
| No terminal (CI, a piped run) | The prompt would have nobody to answer it. |
| The project has [**islands**](https://rask.sh/docs/guides/islands/) | Islands load from a second dev server over HTTP, which an HTTPS page is not allowed to do at all (mixed content). |

The last one needs its bundler taught to serve TLS before this can cover them; until then it keeps the
localhost URL that does work.

> **One app at a time gets port 443.** Only one process can hold it, and on macOS the pf redirect points
> at whichever `rask dev` is currently running (pf matches on address and port — it cannot see a
> hostname, let alone a TLS SNI). Two dev servers at once is the case this cannot serve on any platform.

| Flag | What it does |
| --- | --- |
| `--project`, `-p` | Project to run. Accepts a `.csproj` or a directory. |
| `--urls` | URLs to listen on (sets `ASPNETCORE_URLS`). |
| `--launch-profile` | launchSettings profile to use. |
| `--open` | Open a browser once the app answers. Implied when the app is on its `.test` name. Skipped if the launch profile already opens one. |
| `--no-open` | Never open a browser. |
| `--no-hot-reload` | Keep watching, but restart on change instead of applying live. |
| `--no-restart` | Ask before restarting on an edit hot reload can't apply. |
| `--once` | Run once without watching (a plain `dotnet run`). |
| `--no-banner` | Suppress the startup banner. |
| `--no-host` | Serve on localhost instead of this project's `https://<name>.test` address. |
| `--dry-run` | Print the `dotnet` command that would run, where, and the environment it sets — and start nothing. |
| `--json` | With `--dry-run`: print that plan as JSON — `command`, `arguments`, `workingDirectory`, `environment` — and nothing else. |

> **Changed in this release.** `--no-hot-reload` used to mean "a plain `dotnet run`" — it stopped watching
> altogether, and cleared `DOTNET_WATCH`, which is what the framework keys its own dev-time behaviour off.
> It now means what it says: keep watching, restart instead of applying edits live. Use **`--once`** for
> the old behaviour.

### What hot-reloads

C# Hot Reload applies new IL to the running process; Rask then refreshes what the generators registered
at startup and repaints every open session. Some edits the runtime cannot apply at all — those are *rude
edits*, and `rask dev` restarts the app for you and the browser reloads itself.

| Edit | What happens |
| --- | --- |
| A component's `Render()`, or anything it calls | ✅ Applied live; the page repaints in place. |
| A scoped `.css` / `.ts` sibling | ✅ Applied live; the bundle URL changes and the `<link>` is swapped. |
| Deleting a scoped `.css` | ✅ The rules disappear from the page. |
| A `[Route]` template | ✅ The route table is rebuilt. |
| A CQRS command/query/event handler body | ✅ The next dispatch runs the new code. |
| A job type's or durable handler's body | ✅ Applied live. |
| **Adding or removing a type** — a new component, page, handler, job | ⚠️ Rude edit → the app restarts, and the browser reloads itself. |
| **Changing a signature** — a new required prop, a changed method signature | ⚠️ Rude edit → restart. |
| Renaming a job type | ✅ Applied. The old name stops resolving too. |
| An island's `.tsx` / `.vue` / `.svelte` | ✅ Hot-replaced by its own framework — see below. |

**Islands hot-reload too, on a second dev server.** When the project has islands, `rask dev` starts
Vite for them on `http://localhost:5174` and skips only the production island bundle. Everything
else still runs, including the prop type-check.

How much survives a save is the framework's call rather than Rask's: React, Preact, Solid, Vue and
Svelte keep component state through their own refresh integrations, while Lit and Angular have none
and fall back to a page reload. Even those skip the C# rebuild, which is the slow half. See
[islands](https://rask.sh/docs/guides/islands/#hot-reload).

One thing it does not cover:

- **A rude edit is not announced.** `dotnet watch` restarts the process, so nothing in Rask observes the
  edit; what you see is the app coming back and the page reloading.

**The database is not a source edit.** `dotnet watch` reacts to any file that appears in the project
folder, not only to what the project compiles, and SQLite writes `app.db-wal` and `app.db-shm` beside the
database every time the app starts. Rask adds SQLite's `-wal`, `-shm` and `-journal` files (for `.db`,
`.sqlite` and `.sqlite3`) to `DefaultItemExcludes`, the list the watcher skips, so the app's own writes
never set off another project load. The database file itself stays an ordinary project item.

**WASM is covered** — a client-plus-host app hot-reloads under `rask dev` like a Server one. To make that
possible the host serves the client's *build* output for the session rather than its published bundle:
the published bundle is trimmed, and trimming disables the runtime's metadata-update support outright,
so no applied edit could ever reach the page. It also drops the nested `dotnet publish` from the inner
loop, which is most of the wait. `--no-hot-reload` and `--once` keep the published bundle.

In Development you get a small "Hot reload applied" pill in the corner when an edit lands, so a save that
changed nothing visible is distinguishable from one that didn't apply. It is never present in production,
and it looks and behaves the same on both transports — Server and WASM share one implementation.

### When the build fails

A save that doesn't compile takes the app down — and until now the browser reported that as a *network*
problem: "Reconnecting…", then "Still trying to reconnect…" and a **Retry now** button that could never
succeed. It is a compile problem, and it now says so:

```
┌───────────────────────────────────────────────────────────┐
│ Build failed                              Stack   Dismiss │
├───────────────────────────────────────────────────────────┤
│ 2 build errors                                            │
│ Features/Products/ProductPage.cs(31,13): error CS0103:    │
│ The name 'titel' does not exist in the current context    │
└───────────────────────────────────────────────────────────┘
```

Fix the file and it disappears on its own — the reconnect keeps running underneath the panel, so the app
comes back the moment it compiles. No reload, no clicking anything.

**How it reaches the browser.** Nothing in the app can report this, because the app is what died. So
`rask dev` reads `dotnet watch`'s output as it passes it through to your terminal, and serves what it
learned from a small read-only endpoint on `127.0.0.1` that it owns for as long as the session lasts. Its
URL is stamped onto every page the app serves (`data-rask-dev-status` on `<body>`), which is what lets the
browser still ask after the server that sent it has gone. Development only: production HTML never carries
the attribute, so there is nothing to poll and nothing to leak. If the endpoint can't be bound, `rask dev`
runs exactly as before — the browser just falls back to the reconnect overlay.

### When your code throws

An unhandled exception from an event handler shows the same style of panel —
**over** the running app, which stays mounted, scrolled where it was, with your form input intact. That is
the state that produced the bug, so it is the state worth keeping. Dismiss the panel and keep clicking; it
counts repeats, so a handler throwing on every click is visible as such.

A fault during *render*, or in a *lifecycle hook*, replaces the page, in development as in production:
re-rendering the subtree that just threw would only throw again, and a component whose load threw would sit on
its placeholder for good. In development that page names the exception, with its stack and source. In
production every such fault gets the styled error page (a first response
answers `500`), and nothing of the exception reaches the browser — no type, no message, no stack: the page
carries a reference, and the Error log line carries the same one beside the exception.

> **If nothing ever applies, suspect the path.** `dotnet watch` produces an empty hot-reload delta —
> silently, reporting success at every step — when the project path traverses a symlink. `rask dev`
> resolves the path for you, so this only bites if you drive `dotnet watch` yourself; run it against the
> resolved path (on macOS, `/private/var/…` rather than `/var/…`) and edits apply again.

### Debugging in VS Code

Every template ships a `.vscode/` folder. For one with an ASP.NET host, **F5** in VS Code runs the app under the C#
debugger (the `wasm` template debugs in the browser — [below](#code-that-runs-in-the-browser)): breakpoints hit from the first line of `Program.cs`, and a handler or async lifecycle hook that
throws stops the debugger at the fault instead of only showing the panel above. It needs the
[C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension, which the
folder recommends.

**Edits need a restart under the debugger** (Ctrl+Shift+F5). The runtime refuses to apply a hot-reload update
to a process a debugger is attached to, so `dotnet watch` and a debugger cannot share one — and C# Dev Kit's
own debug hot reload reports itself unavailable for this launch, so a saved edit does not reach the running
app. Use F5 to find a bug and `rask dev` to work on the fix: that is the loop that applies edits live.

What F5 does:

| Step | What happens |
| --- | --- |
| Build | `dotnet build --property:RaskDevSession=true` — the same dev-session switch `rask dev` passes, so islands come from their dev server instead of a production bundle. |
| Launch | The C# debugger runs `bin/Debug/net10.0/<App>.dll` with the launch profile's settings and Just My Code on. |
| Dev servers | The app starts what `rask dev` would have started beside it: the islands' Vite on 5174. |
| Address | `https://<name>.test` when an earlier `rask dev` already set that name up on this machine (with `:5001` on macOS, where the pf redirect cannot be checked without root); the launch profile's localhost otherwise. F5 never prompts and never changes the machine. |
| Browser | The app prints `Rask dev: open <url>` once it is listening, and VS Code opens it. |

None of that runs under `rask dev`, which starts its dev servers itself, outside Development, or for a build
that was not a dev session — a plain `dotnet run` behaves exactly as it always has. Stopping the debugger
kills the app outright, so a dev server it started can be left holding its port; the next F5 ends that
leftover before starting its own, and only if it is still the same process it recorded.

**Where the debugger stops on a throw.** With Just My Code on (the default, and set in `launch.json`) and
**User-Unhandled Exceptions** ticked in the Breakpoints view, it stops once, on the line that threw. A handler's
exception leaves your code for Rask's, which the debugger reports as user-unhandled by itself. An async
lifecycle hook's exception reaches Rask through its faulted task instead, where no debugger would notice it, so
Rask asks the debugger to stop there (`Debugger.BreakForUserUnhandledException`); the stack it shows still names
your throw line.

**Stack frames open the file.** In development, every frame of the panel's stack and every compiler error that
names a file on your machine is a `vscode://` link to that line.

**Editor settings.** The folder's `settings.json` is committed too, so everyone who clones the app gets it:

| Setting | What you get |
| --- | --- |
| File nesting | A component's paired files (`Counter.css`, `Counter.ts`, a `Counter.tsx` island, a package island's `Counter.props.json`) fold under `Counter.cs` in the explorer. Every template. |
| Tailwind completion | Class completion and hover inside `Div.Class("…")`, from the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension the folder then also recommends. Only the templates that compile Tailwind (`server`, `wasm`, `wasm-hosted`). |

Your own preferences belong in your VS Code *User* settings, which a workspace `settings.json` does not replace.

**Recommended extensions.** `extensions.json` is written from what the scaffold holds, and VS Code offers to
install the list when the folder opens:

| Extension | Recommended to |
| --- | --- |
| [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit), [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig), [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) | Every template. |
| [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) | `server`, `wasm`, `wasm-hosted` — the templates that compile Tailwind. |
| [Vite+](https://marketplace.visualstudio.com/items?itemName=VoidZero.vite-plus-extension-pack) | Every Vite front end (`react`, `preact`, `vue`, `solid`, `svelte`, `lit`), and an app with islands that use npm (every `--islands` runtime but `blazor`): Oxlint and Oxfmt from its `vite.config`. |
| [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) | `angular`, whose client keeps angular-eslint. |
| [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) | An app with a `.vue` island (`--islands vue`). |
| [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) | An app with a `.svelte` island (`--islands svelte`). |

**An existing project** gets the same setup by copying `.vscode/` from a fresh `rask new` app of the same
template and replacing the project name in `launch.json` and `tasks.json`. Add these lines to `.gitignore`, so
the four files are committed and the rest of `.vscode/` stays yours:

```gitignore
/.vscode/*
!/.vscode/launch.json
!/.vscode/tasks.json
!/.vscode/extensions.json
!/.vscode/settings.json
```

#### Code that runs in the browser

C# compiled to WebAssembly and a component's scoped `.ts` run in the browser, where the C# debugger cannot
reach. F5 debugs them there instead: VS Code's built-in JavaScript debugger launches Chrome with an `inspectUri`
through the **WebAssembly debug proxy** at `/_framework/debug`, which turns WebAssembly frames back into C#.
Breakpoints then hit in `.cs` and `.ts` files alike.

| Template | What F5 does |
| --- | --- |
| `wasm` | Starts the dev server in the background (`dotnet run --urls http://localhost:5210`, whose SDK dev server maps the proxy), then opens the app in a debugged browser. |
| `wasm-hosted` | Runs the host under the C# debugger as above, and once it prints `Rask dev: open`, starts a second session: the browser at the launch profile's `https://localhost:5001`, attached to the proxy `MapRaskSpa` maps in Development. Both halves stop at breakpoints. |

- **The proxy comes from the WebAssembly SDK** (`BrowserDebugHost.dll`, beside its dev server). The client's build
  records where it is, next to its build manifest, and `MapRaskSpa` starts it on the first debugger that asks. It
  listens on 127.0.0.1 and refuses a `?browser=` that is not a DevTools socket on this machine.
- **Scoped `.ts` is debuggable as `.ts`.** A Debug build emits source maps for it, and the scoped-script bundle is
  served with an index map beside it, so a breakpoint in `Counter.ts` binds where the browser runs it. A Release
  build emits none.
- **Chrome by default.** Change `"type": "chrome"` to `"msedge"` in `launch.json` to debug in Edge.
- **Code before the debugger attaches is not stopped in** — the first lines of `Program.Main`. Reload the page once
  the session is up to stop there.

## `rask db` — migrations, and getting the database in and out

```bash
rask db add InitialCreate            # create a migration for the current model
rask db list                         # list migrations and which are applied
rask db update                       # apply pending migrations now (the app also does it when it starts)
rask db update 20240101_Init         # migrate up/down to a specific migration
rask db remove                       # undo the last (unapplied) migration
rask db drop --yes                 # drop the database (a dev reset)
rask db backup                       # a consistent copy of the local database
rask db backup --remote -o backups/  # ...of the deployed one, pulled down
rask db restore backups/app-20260805-081500.db --remote
```

A friendly wrapper over `dotnet ef` for the everyday migration lifecycle, meant to pair with what
your feature code needs. It finds the project for you (the single `.csproj` at or above the
current directory, or — at a wasm-hosted solution's root — its `.Server` project, as `rask dev` does;
override with `--project`), and if the EF Core tools aren't installed it installs
`dotnet-ef` globally the first time you run it.

| Action | Wraps | Notes |
| --- | --- | --- |
| `add <Name>` | `dotnet ef migrations add` | `--output <dir>` sets the migrations folder |
| `remove` | `dotnet ef migrations remove` | undo the last migration |
| `list` | `dotnet ef migrations list` | show migrations and applied state |
| `update [<target>]` | `dotnet ef database update` | apply pending, or migrate to a named point. A `RaskApp` applies pending migrations itself on start, so this is for rolling back, for a hand-wired host, and for an app that [turned that off](https://rask.sh/docs/guides/data/#migrations-apply-themselves-on-start) |
| `drop` | `dotnet ef database drop` | drops the database; prompts unless `--yes` |
| `backup` | — | a consistent copy; `--output/-o` a file or directory, `--remote` for the deployed one |
| `restore <file>` | — | replaces the database with a copy; prompts unless `--yes` |

Shared options: `--project/-p` (the project owning the `DbContext`), `--startup-project/-s` (the app
that configures it; defaults to `--project`), and `--context/-c` (when the app has more than one
`DbContext`). Anything after `--` is forwarded to `dotnet ef` verbatim (e.g. `rask db update -- --verbose`).

The EF Core tools need the startup project to reference `Microsoft.EntityFrameworkCore.Design`, and
`rask db` adds it for you (via `dotnet add package`) if it's missing. A project from `rask new` already
has it, because the first migration `rask new` runs goes through this same code path. `backup` and
`restore` need none of that: they copy a database rather than migrate one, so they never install
`dotnet-ef`.

### Backup and restore

```bash
rask db backup                                  # ./<app>-20260805-081500.db
rask db backup --output backups/                # into a directory, same generated name
rask db backup --output nightly.db              # a name you choose
rask db backup --remote                         # the deployed database, pulled down
rask db restore nightly.db                      # replace the local database
rask db restore nightly.db --remote --yes     # ...and the deployed one, unattended
```

**A file copy of a live SQLite database is not a backup.** With WAL on — and every Rask app has it, it is
one of the [production pragmas](https://rask.sh/docs/guides/sqlite/) — committed transactions live in the `-wal` sidecar until a
checkpoint, so the `.db` file on its own is torn or stale. Both paths go through SQLite instead: locally
via the Online Backup API, remotely via `VACUUM INTO`. Either way what lands is a single self-contained
file with the WAL already folded in, taken while the app keeps serving. Locally, the database is the file
named by `Rask:ConnectionStrings:App` in the project's `appsettings.json` (comments and trailing commas are
fine — it reads the file as JSONC).

The remote path needs **nothing installed on the host**. It runs the copy inside a throwaway container
mounted on the app's data volume — the same shape the deploy's readiness probe uses — and brings the
result down over the existing `docker -H ssh://…` connection. The host does need to be able to pull
`alpine`, which it already does for every deploy. Host and app name come from `.rask/deploy.json`, so a
repeat backup is a bare `rask db backup --remote`; override with `--host` and `--app`.

**Uploaded files go with the database.** When the app keeps [stored files](https://rask.sh/docs/guides/file-storage/) on disk, `backup`
archives them beside the copy — `nightly.db` comes with `nightly.files.tgz` — and `restore` puts both back when it
finds the archive next to the file you name. The archive is taken after the database copy, so every row in it has
its bytes; locally the files are the disk root the app would use (`Rask:Storage:Disk:Root`, else `storage/`), and
remotely they are `/data/files` on the volume. A restore unpacks the archive before it touches the database, so a
corrupt archive stops it with nothing replaced. With no archive beside it, the files are left as they are, and
`restore` names any rows that now point at bytes that aren't there.

**Restore replaces a database, so it behaves like `rask db drop`**: it asks first, takes `--yes` to skip
the prompt, and refuses outright when there's no terminal to ask on rather than guessing. A remote restore
also **stops the app first and starts it again afterwards** — within the app's `"stopTimeout"`, finishing the
jobs it is running and saying what it waits for, as a deploy does — because replacing the file under a live writer
leaves the running process holding the database it thinks it has, and its next checkpoint writes that
belief back over the restored one. If it can't stop the app, it refuses. The stale `-wal`/`-shm` sidecars
go with the old file for the same reason: left behind, SQLite replays them over the restored database.

> Backups are a copy at a moment; [Litestream](https://rask.sh/docs/guides/sqlite/#continuous-backup-with-litestream) is continuous
> replication for when the box dies. They answer different questions — "let me look at what production
> has" and "the server is gone" — and an app that matters wants both.

## `rask deploy` — ship to a single host over SSH

```bash
rask deploy --host root@box --domain app.example.com      # bare VPS → live HTTPS site (sets the box up first)
rask deploy --host deploy@box --port 8080                 # no domain: publish a port, bring your own TLS
rask deploy                                               # redeploy: host/domain remembered
rask deploy --github-actions                              # write a workflow that deploys on push to main
rask deploy --dry-run --host deploy@box --domain app.example.com   # print the docker commands, run nothing
```

One command builds your app's Docker image **on the box** and runs it. Every deploy step is
`docker -H ssh://<host> …`, so there's no registry, no local Docker daemon, and no image tarball to
copy — the build context ships to the host's daemon over SSH and builds there. It deploys the
`Dockerfile` that `rask new` scaffolds (point at another with `--dockerfile`).

**Handed a box that isn't ready, it sets it up** — installs Docker, creates a non-root `deploy` login
with your keys, configures a firewall (one that covers Docker's published ports, which ufw does not
reach on its own), and hardens SSH — after showing you the list and asking once. So
a fresh VPS goes live without you opening an SSH session. It's idempotent (a ready box is left alone,
with no prompt), and nothing that could lock you out happens until a fresh connection has proved the new
login works — with a rollback timer on the box as the backstop. See
[deployment.md](https://rask.sh/docs/guides/deployment/#the-first-deploy-to-a-bare-box) for the full story.

**With `--domain`** Rask runs a shared [Caddy](https://caddyserver.com) reverse proxy on the box that
fetches an automatic Let's Encrypt certificate, so you get a live HTTPS site with nothing else to
configure. Deploys are **zero-downtime**: the new container starts alongside the old one (blue-green),
is waited on until its container is running **and answers an HTTP health check** (`GET /health` by
default — the endpoint `rask new` scaffolds), then Caddy is reloaded to point at it and the route is
checked through the proxy before the old one is removed. If the new container fails to start, fails its
health probe, or isn't what the proxy serves after the reload, the previous version keeps serving. Probe a different path with `--health-path`, or skip the probe with `--no-health-check`.
HTTP requests are zero-downtime; **live sessions re-establish**, because a session lives in the container
being replaced and cannot hand over. The retiring container announces its shutdown first, so open pages
show "Updating…" and reload onto the new one at their previous scroll position, with whatever the user
had typed put back — see
[the shutdown ladder](https://rask.sh/docs/guides/deployment/#the-shutdown-ladder).

**Running jobs finish on the container that started them.** Once traffic has moved, the old container claims no
new job but runs the ones it has to the end; the deploy waits for it — up to `"stopTimeout"` in
`.rask/deploy.json` (`"30m"` by default, written there on the first deploy) — and prints what it is still
finishing (`app-blue is finishing 1 job: AtPump (3m12s) — waited 3m20s of 30m`). A container still busy when the
budget runs out is killed, and the deploy says so in red. A second deploy of the same app waits for it. A bare
`--port` deploy has no proxy to switch, so it keeps a 20s stop and warns that running jobs need `--domain`.

**Multiple apps share one box.** Each app has its own file in the proxy (`/data/rask/sites/<app>.caddy`),
and a deploy writes only its own, under a box-wide deploy lock — deploying a second app (a different
`--domain`), even in the same minute, leaves the first untouched. See
[the shared proxy](https://rask.sh/docs/guides/deployment/#the-shared-proxy-one-file-per-app). **One app can answer on several domains**: repeat `--domain` (or give
`--domain example.com,www.example.com`) and each gets its own certificate, all routed to the same
container. The first is the primary — the address emailed links use — and every one of them is
remembered (as `"domains": [...]` in `.rask/deploy.json`; a single one stays `"domain"`). A domain another
app on the box already serves is refused before anything starts. Without `--domain`, the app is published on `--port` (default `8080`) and
you put your own TLS/reverse proxy in front (there's no zero-downtime swap on a single published port).
That downtime is inherent to publishing one port; *staying* down is not. If the new container fails to
start or fails its health check, port mode brings `:previous` back automatically — the last image that
passed the same gate — and still exits non-zero, so a bad image costs you a blip rather than an outage.
Use `rask deploy rollback` to undo a deploy that *did* come up healthy.

**Your database survives redeploys.** Each deploy runs a fresh container, so `rask deploy` mounts a
per-app named volume and points the app at it (`Rask:ConnectionStrings:App` → `Data Source=/data/app.db`) — the
SQLite database persists across container replacements. The old container keeps serving for a moment after
the proxy switches (so a request already in flight to it isn't cut), then is stopped gracefully (SIGTERM →
its Litestream flush + WAL checkpoint) before removal. The `rask new` Dockerfile prepares a
writable `/data`; a custom Dockerfile needs `RUN mkdir -p /data && chown $APP_UID:$APP_UID /data`. Add
[`Rask.SQLite.Litestream`](https://rask.sh/docs/guides/sqlite/#continuous-backup-with-litestream) to also stream it off the box.

| Option | Purpose |
| --- | --- |
| `--host <user@host>` | SSH target. Required on the first deploy, then remembered in `.rask/deploy.json`. |
| `--domain <host>` | Front the app with auto-HTTPS Caddy. Repeat it, or comma-separate the hosts, to serve one app on several domains (`--domain example.com --domain www.example.com`); the first is the primary. Omit to publish `--port` directly. |
| `--redirect <host>` | A name the proxy redirects (`308`, path and query kept) to the primary domain — `'*.example.com'` for every subdomain. Repeatable; remembered as `"redirects"`. Needs `--domain`. |
| `--dns <provider>` | Prove certificates over DNS (DNS-01), which a `*.` host needs: `azure`. Builds `rask-caddy:dns-azure` on the box and replaces the shared proxy with it once (a few seconds of downtime for every site); later deploys keep it. Not remembered — the proxy carries it. |
| `--dns-env-file <path>` | The provider's credentials as `KEY=VALUE` lines (for `azure`: `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP_NAME`), needed when the proxy is given the provider. Never stored or printed. |
| `--port <n>` | Host port when there's no domain (default `8080`). |
| `--container-port <n>` | The port your app listens on **inside** the container — what the proxy is pointed at and what the readiness probe hits (default `8080`, which every `rask new` Dockerfile uses). Only needed for a hand-written Dockerfile that exposes something else. Remembered in `.rask/deploy.json`, and recorded on the container so a host running apps on different ports keeps each one's routing correct. |
| `--app <name>` | Image/container name (default: the project name). The same flag names the app for `rask db --remote`. |
| `--project <path>` · `--dockerfile <path>` | The build context / Dockerfile, if not the current project. |
| `--env KEY=VALUE` · `--env-file <path>` | Runtime environment for the app container (repeat `--env`). |
| `--health-path <path>` | The path the readiness probe hits before switching traffic (default `/health`). Remembered in `.rask/deploy.json`. |
| `--no-health-check` | Gate only on the container running (skip the HTTP probe) — for apps without a health endpoint. Remembered. |
| `--github-actions` | Write `.github/workflows/deploy.yml` (deploy on push to main) and print the secrets to add. Touches no host. |
| `--dry-run` | Print the exact docker commands without running them. |

### After it's live — `status`, `logs`, `rollback`

```bash
rask deploy status            # what's running on the box (every app, not just this one)
rask deploy logs             # the live container's last 100 lines
rask deploy logs --follow    # ...and stream new ones
rask deploy rollback         # put the previous image back, health-gated
```

These read the same `rask.*` container labels a deploy writes, so they describe the box **as it actually
is** rather than as `.rask/deploy.json` remembers it. They need a host (from the config or `--host`) and
nothing else — no Dockerfile, no build.

`status` lists every Rask-managed app sharing the box, with its URL or published port, its blue/green
colour, and how long it has been up — and tells you whether a rollback is currently possible.

`rollback` exists for the failure the blue-green swap can't catch. That swap protects you from a release
that *fails* — one that won't start, or won't answer its health check. It can do nothing about a release
that starts, answers, and is simply **wrong**. Each deploy therefore moves the image it replaces to
`<app>:previous` before building, and `rask deploy rollback` starts that image back up through the same
gates a deploy uses (running → healthy → under the deploy lock: reload the proxy → check through it → retire the old container). It then swaps the
two tags, so running it again undoes the rollback rather than repeating it.

| Option | Applies to | Purpose |
| --- | --- | --- |
| `--tail <n\|all>` | `logs` | Lines to show (default `100`). |
| `--follow` | `logs` | Stream new lines until interrupted. |

Options that describe *what to deploy* (`--domain`, `--container-port`, `--dockerfile`, `--dry-run`, …)
are rejected on these verbs rather than silently ignored — they operate on what is already deployed.

Host setup options — these only matter the first time you deploy to a box:

| Option | Purpose |
| --- | --- |
| `--setup-host` | Prepare the host without asking. Needed when there's no terminal to confirm on. |
| `--no-setup-host` | Never change the host; fail with instructions instead. What the generated CI workflow uses. |
| `--deploy-user <name>` | The non-root login to create and deploy as when given a root host (default: `deploy`). |
| `--no-deploy-user` | Keep deploying as the `--host` login instead of creating a non-root one. |
| `--no-firewall` | Don't configure `ufw` on the host, and don't put Docker's published ports behind it. |
| `--no-harden-ssh` | Don't disable SSH password login and root login on the host. |

**Prerequisites.** The [Docker CLI](https://docs.docker.com/get-docker/) installed locally (it's the
client for every remote `docker` call, even though nothing builds on your machine), and key-based SSH
to the host so `ssh user@box` works non-interactively. **The host needs nothing else** — Docker and the
rest are installed for you on the first deploy. Point your domain's DNS `A`/`AAAA` record at the host
before the first `--domain` deploy so the certificate can be issued. `.rask/deploy.json` remembers the
host/domain/port for repeat deploys but **never stores secrets** — pass those via `--env`/`--env-file`
each time.

**Deploying from CI.** `rask deploy --github-actions` writes a workflow that runs this same command on
every push to `main`, and prints the two `gh secret set` lines it needs (an SSH key and the host's
fingerprint). Everything else comes from the committed `.rask/deploy.json`. It deploys with
`--no-setup-host`: prepare the box once from your own machine, so CI never reconfigures a host.

## `rask doctor` — check before you hit it

```bash
rask doctor          # what's here, what's missing, and what only some commands need
rask doctor --json   # the same verdict, for CI
```

Every probe it runs already existed, each reachable only from the command that needed it — so the way
to find out whether your machine could run something was to run it and see where it stopped, halfway
through, having already done some of the work.

```
  ok    rask                0.20.1
  ok    dotnet sdk          10.0.302
  ok    dotnet-ef           installed
  warn  wasm-tools          not installed
                            Every browser-WASM build needs it — the wasm and wasm-hosted
                            template, and `dotnet publish` of either. Fix: dotnet workload
                            install wasm-tools
  warn  node                v24.14.0 (below the 24 LTS line)
                            Islands build on it, but their toolchains raise their own
                            floors over time.
  ok    npm                 11.19.0
  ok    git                 git version 2.50.1
  ok    ssh                 OpenSSH_9.8p1, LibreSSL 3.3.6
  warn  docker              not found
                            Only `rask deploy` needs it — https://docs.docker.com/get-docker/
  ok    project             /src/Shop
  ok    database            SQLite
  fail  .rask/deploy.json   isn't valid JSON: 'o' is an invalid start of a property name…
                            Until it parses, its remembered settings are silently ignored.
```

**All seven things the CLI shells out to, not three.** It used to probe `dotnet`, `dotnet-ef` and
Docker; the `wasm-tools` workload, Node, npm, `git` and `ssh` were each discovered by failure instead
([#883](https://github.com/pal-tamas/rask/issues/883)). The workload was the worst of them: nothing
checked for it anywhere, and a missing one surfaces as `NETSDK1147`, which reads like a broken machine
rather than a missing install.

**Two of them compare a version, rather than echoing one.** A .NET 9 box used to show a green
`dotnet sdk` row and then fail at the first build, because the row printed whatever string the tool
returned. Node is measured against the current Active LTS line.

**Warnings aren't failures.** Docker missing is fatal to `rask deploy` and irrelevant to everyone else,
so only a genuinely broken thing sets the exit code (`1`); a machine that can start every command exits
`0`. Every row added above is a warning for the same reason — `dotnet` is the one dependency fatal to
everything, because every command shells out to it.

**It is read-only.** It reports; it never installs or fixes. A doctor that quietly installed the tooling
it found missing would be doing the thing you ran it to avoid.

One thing it exists to catch: a corrupt `.rask/deploy.json` used to be swallowed — the loader falls
back to defaults, so a typo'd file looked exactly like no file, and the remembered host vanished with
nothing said. It now says so in passing, and `doctor` reports it as a failure.

## `rask info` — environment report

```bash
rask info
```

```text
  Rask CLI         0.17.0
  .NET SDK         10.0.201
  OS               macOS 26.5.1
```

A quick check when diagnosing a machine: the tool version, the .NET SDK version, and the OS.
`rask --version` prints just the tool version.

## `rask completion` — shell completion

```bash
rask completion bash >> ~/.bashrc
rask completion zsh  > "${fpath[1]}/_rask"
rask completion fish > ~/.config/fish/completions/rask.fish
```

Prints a completion script for `bash`, `zsh`, or `fish`. It's generated from the live command list and
each command's option schema, so it always matches the installed CLI — completing command names and
their `--options`. Re-run it after upgrading `rask` to pick up new commands and flags.

## Roadmap

The CLI is the front door to the whole Rask stack — from `rask new` through `rask db` to `rask deploy`,
the whole lifecycle lives here. See the [development workflow](https://rask.sh/docs/guides/development-workflow/) for how the
framework is built.
