# Run Modes and Commands

Most of the time an Alepha app is a server. Sometimes the same codebase has to
be something else for thirty seconds: apply migrations, seed a database, print a
report, drain a queue. Two primitives cover that without a second entry point
and without a second copy of your wiring.

| Primitive  | Import           | Triggered by            | Good for                             |
| ---------- | ---------------- | ----------------------- | ------------------------------------ |
| `$mode`    | `alepha`         | An environment variable | One-shot tasks in the deployed image |
| `$command` | `alepha/command` | A CLI argument          | Tooling humans invoke by name        |

## `$mode`

`$mode` gates a whole bootstrap on an environment variable. When it matches, the
declaring class becomes `alepha.target` and the dependency graph is **pruned to
that class and its transitive dependencies**. The HTTP server, the job
scheduler, the cron ticker: none of them are constructed, because nothing in the
kept subgraph asks for them.

```typescript check
import { $inject, $mode } from "alepha";
import { DatabaseProvider } from "alepha/orm";

class DbMigrationMode {
  db = $inject(DatabaseProvider);

  mode = $mode({
    env: "MIGRATE",
    ready: async () => {
      await this.db.migrate();
    },
  });
}
```

```bash
MIGRATE=true node app.js
```

`MODE=MIGRATE node app.js` does the same thing. Two spellings because two
callers want different shapes: a Kubernetes init container sets one variable per
job, while a single-variable `MODE` suits a container image that takes one
argument.

That pruning is the point. A migration container that boots the HTTP server has
to bind a port it will never serve, and a seed job that starts the cron ticker
fires whatever was due. Running the task inside the real container, with the
real DI graph and the real configuration, and _nothing else_, is what makes this
different from a script that imports your services.

After `ready` resolves or throws, `alepha.stop()` runs, so connections close and
the process exits. Omit `ready` and the mode still prunes the graph but the
process stays alive, which is what a queue worker or a cron-only deployment
wants.

`$mode` returns a boolean, so a class can branch on whether it is the active
mode.

### `MIGRATE=false` does not activate it

The check is `isEnvEnabled`, not truthiness. `MIGRATE=false`, `MIGRATE=0` and
`MIGRATE=` all leave the mode off. This is worth stating because the naive
version of this check treats every non-empty string as true, and `MIGRATE=false`
then runs your migrations.

## `$command`

`$command` declares a CLI command: its name, its flags, its arguments, the
environment it requires, and its handler.

```typescript check
import { z } from "alepha";
import { $command } from "alepha/command";

class ReportCommands {
  report = $command({
    name: "report",
    description: "Render a usage report",
    args: z.text(),
    flags: z.object({
      format: z.enum(["json", "csv"]).default("json"),
      verbose: z.boolean().default(false),
    }),
    env: z.object({
      REPORT_TOKEN: z.text({ description: "API token for the report service" }),
    }),
    handler: async ({ args, flags, env, print }) => {
      print(`${args} as ${flags.format} with ${env.REPORT_TOKEN.length} chars`);
    },
  });
}
```

Register the class in your `alepha.config.ts` and it is reachable through the
`alepha` CLI:

```typescript
import { defineConfig } from "alepha/cli/config";

export default defineConfig({
  services: [ReportCommands],
});
```

```bash
alepha report monthly --format=csv --verbose
```

Everything on the declaration does double duty. `flags` and `args` are parsed
_and_ validated _and_ printed in `--help`, which `help` also answers as a word:
`alepha help report` prints exactly what `alepha report --help` prints, unless
the CLI registers a command named `help` of its own. A positional prints under
its schema's `title` (`args: z.text({ title: "month" })` reads `<month>`), and
a flag's help line is its description. `env` is validated before the handler
runs, so a missing token is a clear failure at second zero rather than a
`undefined` three API calls in.

### Flag syntax

`--name value` and `--name=value` are equivalent. A boolean flag needs no value:
`--compile` turns it on, and `--no-compile` or `--compile=false` turns it off. A
bare `--` ends flag parsing, so everything after it is an argument even when it
starts with a dash. An empty value is a value: `--summary ""` and `--summary=`
both pass `""`.

**An array flag repeats.** `--tag a --tag b` is `["a", "b"]`: each occurrence is
cast against the element schema, so `--count 1 --count 2` on a
`z.array(z.integer())` is `[1, 2]`. An occurrence written as a JSON array is
spread, so `--tag '["a","b"]'` keeps working and `--tag '["a","b"]' --tag c` is
`["a", "b", "c"]`. A scalar flag given twice keeps the last value. The help line
of an array flag says `(repeatable)`.

### Reading a value from a file or stdin

A shell cannot pass a Markdown body as a flag value: `--body "## H\nBody"` is the
two characters `\` and `n`, not a newline. A flag that declares `atFile: true`
reads it from a file instead:

```typescript check
import { z } from "alepha";
import { $command } from "alepha/command";

class NoteCommands {
  create = $command({
    name: "create",
    description: "Create a note",
    flags: z.object({
      title: z.text({ description: "The note's title" }),
      body: z.text({
        size: "rich",
        atFile: true,
        description: "The note, in Markdown",
      }),
      tag: z.array(z.text()).describe("A tag").optional(),
    }),
    handler: async ({ flags, print }) => {
      print(
        `${flags.title}: ${flags.body.length} chars, ${flags.tag?.length ?? 0} tags`,
      );
    },
  });
}
```

```bash
alepha create --title "Plan" --body @plan.md --tag cli --tag docs
git log -1 --format=%B | alepha create --title "Last commit" --body @-
```

| Value    | On a flag with `atFile: true`                                  |
| -------- | -------------------------------------------------------------- |
| `@path`  | The content of the file, resolved against the command's `root` |
| `@-`     | The whole of stdin                                             |
| `@@text` | The literal `@text`                                            |
| anything | Itself                                                         |

The file is read after the token is bound to its flag and before it is cast, so
a body that starts with `- ` is still a value, and a file holding JSON reaches a
string flag as that string. On an array flag, the content goes through the
repeat rule once. An empty file is accepted, since it was named on purpose.

`@-` is stricter, because the failure it guards against is silent. It may be
used once per invocation. It is refused when stdin is a terminal, since waiting
on a keyboard nobody is at is a hang. And it is refused when stdin is empty,
since an agent's shell tool with nothing piped in has an empty stdin, and
sending an empty body without a word is how that goes unnoticed: pass `""` to
mean empty.

**`atFile` is opt-in, never implied by a string.** `alepha test --project` takes
package names, and `--project '@alepha/ui*'` has to reach Vitest as typed. On a
flag without `atFile`, `@` is just a character. The help line of a flag that
declares it says `(takes @file, or @- for stdin; @@ for a literal @)`.

Stdin is read through `ConsoleInputProvider`; substitute `MemoryInputProvider`
in a spec, and `MemoryFileSystemProvider` for the files.

### What the handler gets

| Field   | Is                                                                |
| ------- | ----------------------------------------------------------------- |
| `flags` | Parsed and validated against `flags`                              |
| `args`  | Parsed and validated against `args`                               |
| `env`   | Validated against `env`, guaranteed present                       |
| `run`   | Runs a labelled step, or a shell command, with progress reporting |
| `ask`   | Interactive prompts                                               |
| `print` | Writes a line to stdout                                           |
| `fs`    | `node:fs/promises`                                                |
| `glob`  | `node:fs/promises`' `glob`                                        |
| `root`  | The directory the command is running in                           |
| `help`  | Prints this command's help                                        |

**`print` is not the logger, and the distinction matters.** Output is what a
command _produces_; the logger is what it _reports_. They go to different
streams: `print` to stdout, and in a CLI every log line to stderr (see
[Logging](/docs/guides-core-logging#which-stream-stdout-or-stderr)). Anything a caller
might pipe, parse or redirect goes through `print`. Sending it to the logger instead
is how `alepha --version` once answered `18:21:36 I Alepha v0.24.0`, in colour,
in a shape that changed with `LOG_FORMAT`: an environment variable the calling
script does not control. `print` strips colour when stdout is not a TTY, so a
coloured string is still safe to pipe.

### Failing, and exit codes

Throw a `CommandError` to fail a command. The CLI reports its message, and the
message of its innermost `cause` when that says something the message does not,
without a stack trace (`--verbose` keeps it), and exits non-zero. Any other
error is a crash and keeps its stack.

`exitCode` says why it failed, so a script can branch without parsing the
sentence:

```typescript check
import { z } from "alepha";
import { $command, CommandError } from "alepha/command";

class SyncCommands {
  sync = $command({
    name: "sync",
    env: z.object({ SYNC_TOKEN: z.text().optional() }),
    handler: async ({ env }) => {
      if (!env.SYNC_TOKEN) {
        throw new CommandError("Not authenticated. Set SYNC_TOKEN.", {
          exitCode: 3,
        });
      }
    },
  });
}
```

| Code | Meaning                                                       |
| ---- | ------------------------------------------------------------- |
| 0    | Success                                                       |
| 1    | Generic failure: the default when a `CommandError` names none |
| 2    | Reserved: `alepha i18n check` found drift                     |
| 3    | Not authenticated, or the credential was not accepted         |
| 4    | Forbidden                                                     |

A mistyped command, flag or argument is a `UsageError`, a `CommandError` of its
own: the reason, then the help, then exit 1.

### Subcommands

`children` turns a command into a parent:

```typescript check
import { $command } from "alepha/command";

class PublishCommands {
  vercel = $command({
    description: "Deploy to Vercel",
    handler: async ({ print }) => print("vercel"),
  });

  cloudflare = $command({
    description: "Deploy to Cloudflare",
    handler: async ({ print }) => print("cloudflare"),
  });

  publish = $command({
    description: "Publish the application",
    children: [this.vercel, this.cloudflare],
    handler: async ({ help }) => help(),
  });
}
```

`alepha publish vercel` runs the child; `alepha publish` runs the parent
handler, which here prints the help rather than guessing.

### Hooks

`pre` and `post` attach a command to another one by name. They are hidden from
help, cannot be invoked directly, and receive the same parsed flags and
arguments as their target.

```typescript check
import { $command } from "alepha/command";

class BuildCommands {
  build = $command({
    name: "build",
    handler: async ({ print }) => print("building"),
  });

  prebuild = $command({
    pre: "build",
    handler: async ({ run }) => {
      await run("cleaning dist", async () => {});
    },
  });
}
```

### `exclusive`

`exclusive: true` gives the command a machine-wide slot. A second process
running the same command **queues** rather than failing, and reports who is
holding the slot while it waits. The slot covers the pre-hooks, the handler and
the post-hooks as one unit.

The key is derived from the package name at the command's root plus the command
name, so several git worktrees of one project share a slot while unrelated
projects never block each other. Pass a string to set the key yourself, which is
also how two different commands come to share one.

It is one machine, not a cluster. `ALEPHA_NO_EXCLUSIVE=1` bypasses it.

#### A key that depends on the flags

Pass a function instead, and it is called once the flags, args and env have been
parsed. Returning `undefined` takes no slot at all:

```ts
verify = $command({
  flags: z.object({ fast: z.boolean().optional() }),
  exclusive: ({ flags }) => (flags.fast ? undefined : true),
  handler: async ({ run }) => {
    await run("yarn test");
  },
});
```

That is how one command offers both a serialised lane and a concurrent one
without splitting into two commands. It sees `flags`, `args`, `env` and `root`,
and nothing else: two processes have to compute the same key from the same
invocation, so the decision must be a pure function of the parsed input.

#### Claims are reentrant

A command already inside a key may acquire it again. The inner claim takes no
ticket, so it does not queue behind a slot this process already owns, and only
the outermost release frees it.

#### What frees a stuck slot

A ticket is swept when the process that wrote it is gone, which is read from the
pid rather than from the age of its heartbeat. A holder starved of CPU keeps its
slot however long it stalls for. That is the whole point: on a saturated machine
the holder is exactly the process most likely to miss a heartbeat, and sweeping
one meant two processes ended up inside the critical section at once.

`SIGINT` and `SIGTERM` hand the slot over immediately. `SIGKILL` cannot, so the
next arrival sweeps it.

### `mode`

`mode: true` adds a `--mode, -m` flag that loads environment files the way Vite
does: `.env` and `.env.local` always, plus `.env.<mode>` and `.env.<mode>.local`
when a mode is given. Pass a string instead of `true` to set a default, so a
publish command can load production files without anyone typing
`--mode production`.

## See also

- [Migrations](/docs/guides-persistence-migrations) for the migration commands
  that ship with the CLI
- [The Alepha Instance](/docs/guides-core-alepha-instance) for the lifecycle
  `$mode` is pruning
