Run and test
Local CLI and operator UI
Install and use the offline-first Chalupa CLI without bypassing its Taskfile, confirmation, or secret boundaries.
Reviewed 2026-10-10
The Chalupa CLI is the operator entrypoint for your environments. It can
inspect a configuration, read bounded local state, and delegate approved
actions to the public Taskfile actions bundled with it. It does not implement a
second Pulumi lifecycle. With provider: chalupa, GPU commands instead call
the console queue using the local account session; no provider token is needed.
Everything an operator does to a specific environment is a chalupa command:
chalupa up, chalupa tunnel, chalupa seed, chalupa down, and the rest of
the allowlist. The CLI never replaces the Taskfile; it selects one of its
public actions and passes a canonical config path for BYOC.
Managed GPU uses the console API and a pinned local SSH record.
Opening the interface does not contact DigitalOcean, read tvault, select a Pulumi stack, start Docker, or open SSH. An external process runs only after you explicitly request an action or refresh local status.
Install
curl -fsSL https://chalupa.run/install.sh | bash
chalupa --version
chalupa -V and chalupa version print the same thing.
The installer puts checksum-verified Bun and go-task on your PATH and installs
or upgrades @thelacanians/chalupa-cli from the npm registry, with no sudo. If
Bun and go-task are already installed, bun add -g @thelacanians/chalupa-cli
installs or upgrades only the CLI. After that, operator commands work from your
own project directory. go-task is required for any action that reaches the
cloud, because the CLI delegates to it; chalupa doctor reports that
separately.
Use the project in your current directory
Interactive commands find chalupa.yml in the current directory or an ancestor,
stopping at the Git root. Explicit --config and the existing configuration
environment variables take precedence. No billable operation falls back to the
bundled demo. Scripts using --confirm still need --config on the same command
line so the target and authorization remain explicit.
chalupa up
chalupa exec -- bun run dev
chalupa down
up checks local port availability before provisioning, then opens temporary
tunnels to check the configured health endpoint. Without compute.health,
it reports tunnel availability and leaves application readiness unverified. GPU inference also checks installed model metadata; a real
turn is needed to observe loaded GPU placement. Readiness failures can leave
compute running.
exec runs the supplied command locally, in your working directory, using
the existing environment. It opens the Taskfile-owned tunnels and closes them
on exit. It never creates or destroys compute, never rewrites .env, and never
infers database passwords or URLs. Existing app connection settings should use
the local ports shown in the connection output. The child also receives
CHALUPA_ENVIRONMENT and CHALUPA_PORT_<remote-port> values.
For inference, chalupa launch (using inference.agents.default) configures the endpoint for your
agent's process. See Agent harnesses for supported tools
and chalupa exec -- my-agent for other commands. The command's stdout stays available for output
or JSON receipts; Chalupa's connection messages go to stderr. Ctrl-C cancels
the session, and a lost tunnel stops the local process. Compute remains running.
For a remote shell, use chalupa ssh. Image builds and live file synchronization
are not provided by exec; selected Compose services require pre-built images.
Check the workstation
Run the executable check before operating an environment:
chalupa doctor
chalupa doctor --json
doctor reports offlineReady separately from operatorReady. The offline
inspection path is ready when the CLI itself is running. Full operation also
requires Task, Pulumi, tvault, SSH, and nc to be discoverable on PATH.
doctor does not run those tools or query credentials, Docker, the
network, local stacks, or a cloud provider; the one local program it does run
is Python, to check validation readiness. Add --verbose to print a footnote
that says exactly what was and was not queried. A missing operator requirement
produces a non-zero exit while the report still identifies offline inspection
as available.
Inspect safely first
With no config, inspect uses the repository's examples/demo-harbor
fixture:
chalupa inspect
chalupa inspect --json
This is synthetic offline mode. It runs the same Compose inference as the Pulumi program and reports services, inferred roles, tunnel ports, named volumes, persistence, diagnostics, and the complete redacted remote Compose document. It does not run Task or any external process.
Redacted output is still operator information. Absolute paths, service names, topology, port numbers, images, and non-secret metadata can remain visible. Do not publish the output or treat it as a general data-loss-prevention scan.
Select a real configuration explicitly:
export DEMO_CONFIG="/absolute/path/to/project/chalupa.yml"
chalupa inspect --config "$DEMO_CONFIG"
chalupa tui --config "$DEMO_CONFIG"
Prefer --config in scripts and operator notes so the selected environment is
visible at the call site.
Commands find ./chalupa.yml, then CHALUPA_CONFIG, then parent configs.
--config PATH is an optional override. Legacy Taskfile config variables retain
precedence. Managed up and down ask yes/no; use --yes for scripts.
Help and exit codes
chalupa --help lists the commands in groups (Start, Work on a box, Sessions
and cost, CI and tests, Images and cache, Managed GPU and account). chalupa help <command> prints the flags of one command, for example chalupa help data
or chalupa help session extend, and chalupa help --all prints every form of
every command. chalupa <command> --help works too.
A usage error exits 64: an unknown command, an unknown flag, a missing
argument, or a missing config. The message names the problem and points back to
chalupa --help. A Taskfile action keeps the exit code of the action itself.
Managed environments are chosen with --name ENV; --env is an alias of it.
Command reference
| Command | Behavior |
|---|---|
chalupa |
Opens the operator UI when both input and output are interactive terminals. Prints help when invoked with no arguments outside a TTY. |
chalupa doctor --secrets [--config PATH] [--json] |
Names-only credential report: what the config needs, where each resolves from (TinyVault project and key, ambient, legacy file), locked or missing state and conflicts. See Choose the TinyVault source. |
chalupa doctor [--json] [--verbose] |
Reports separate offline and full-operator readiness without running the operator tools. --verbose (-v) adds a footnote stating what the check did and did not query. |
chalupa inspect [--config PATH] [--json] |
Builds a local inspection. Without a config, it uses the synthetic fixture. |
chalupa status [--config PATH] [--json] [--timeline] |
Reads local Pulumi status for BYOC; managed configs query the console and print the host and expiry. With --json, a running BYOC host that has a registered session also reports startedAt, expiresAt, extensionsLeft, elapsedMs and estimatedSpendUsd (omitted, never zero, when unknown; spend needs a known hourly rate). |
chalupa receipt [--last | --since 7d | --campaign NAME] [--json] [--redact] [--config PATH | --env NAME] |
Prints one line per session from the local timing ledger: alive, ready in (and the long pole) or why it never became ready, flows, wait vs work, idle, estimated cost and retained storage (data plus image cache). Offline; see Costs and idle observation. |
chalupa mark SPAN --start|--end|--point [--label KEY=VALUE] [--outcome ok|failed|skipped] (--config PATH | --env NAME) |
Appends one span boundary (or, with --point, one instant) to the session ledger so a harness can time its own phase: image-build per image, apps-build, apps-up, first-flow, flows. A local file append only. |
chalupa box timeline [--session ID] [--backfill] [--json] [--config PATH | --env NAME] |
One session stage by stage, offline: the ledger plus the box's own facts from the snapshot down took (container starts, the health gate, SSH throttling and refused forwards). --backfill keeps those facts for older closed sessions before snapshots rotate. See Costs and idle observation. |
chalupa tui [--config PATH] |
Opens the interactive operator UI. An explicit invocation fails outside a TTY. Home reports the supervised tunnel's health from the local state file (up and reconnect ages, or a caution when its supervisor is gone). |
chalupa setup [--inference] [--no-console] [--no-reaper-token] [--provider chalupa|digitalocean|hetzner] [--force] [--json] |
Managed GPU is the first/default choice: local key, account login, starter, balance and next command. --provider chalupa skips selection and implies inference. The current operator device flow cannot issue a managed account session; see Quickstart. BYOC adds provider credentials and an SSH allowlist. Inference offers to arm the console reaper using a separate DigitalOcean token with only droplet:read and droplet:delete; --provider hetzner offers the same arming once linked (no --inference needed for it) using a Read & Write token from a Hetzner project dedicated to Chalupa — Hetzner tokens are project-wide and cannot be scoped further. --no-reaper-token skips arming for either provider; --no-console skips linking too. JSON mode submits only when CHALUPA_SETUP_REAPER_TOKEN is set and an account is linked. |
chalupa exec [--config PATH] -- COMMAND [ARGS...] |
Runs a local command through managed tunnels; closes tunnels on exit and retains compute. |
chalupa up [--config PATH] [--wait] [--fresh | --reuse-only] [--keep-on-bootstrap-failure] |
BYOC: delegates to Task, asks for the launch <environment> confirmation, and after provisioning opens a temporary tunnel and waits for the forwarded ports and compute.health. --wait also waits for the compute.readiness probes through a supervised tunnel. With session.keepFor or session.reuse: auto a live, unchanged box is reused instead of launched; a changed config is refused unless --fresh. --reuse-only (needs --config) reuses a live, unchanged box or refuses with exit 1 and a reason code; it never launches, so it needs no --confirm and no terminal. Managed up shows price, estimate and balance, rejects insufficient credit before queueing and asks yes/no on a TTY; --yes, --json, --model, --tier and --region launch with optional model/tier/region overrides for this run; validates VRAM fit and saves choices in the managed record without editing YAML. Checks capacity before queueing; offers alternatives or a 60-second poll for up to 20 minutes. Non-TTY/JSON needs --yes or CHALUPA_YES=1; no capacity prints alternatives and exits 1. |
chalupa down [--yes] [--config PATH] |
Managed: asks yes/no (--yes for scripts), stops compute and retains the cache; --confirm "down <slug>" queues the teardown non-interactively and removes the local record after success. BYOC scripts use an exact --confirm "sink <environment>" phrase and an explicit config. |
chalupa launch [opencode|omp|crush|local-agent|sonar|vecai] [--config PATH] [--context auto|native|TOKENS] |
Uses inference.agents.default; context defaults to auto. Opens and closes its own tunnel. |
chalupa tunnel [--config PATH] |
Holds a standalone SSH tunnel until interrupted (Ctrl-C closes it). Managed configs use the pinned host key in the local record and forward Ollama to port 11435 by default. |
chalupa tunnel keep|stop|status|watch [--config PATH] [--json] |
keep opens a supervised tunnel in the background, stop closes it, status reports it and watch runs it in the foreground. --json works for keep, stop and status. |
chalupa tunnel ls [--json] |
Lists every environment on this machine with tunnel leases or a supervised tunnel: mode, supervisor state, pid, local-to-remote ports and the last reconnect. Reads local files only. |
chalupa urls [--config PATH | --name ENV] [--json] |
Where each forwarded service is reachable from this machine: http://127.0.0.1:<port> for web services, 127.0.0.1:<port> for tcp ones, plus the console link and the OpenAI base URL of a GPU environment. Offline; --json is the contract for agents (the MCP tool chalupa_urls returns the same document; in leased mode either may write the local lease file to reserve the ports). chalupa connect --format env and chalupa exec --format env export the same addresses as CHALUPA_<SERVICE>_URL (CHALUPA_<SERVICE>_ADDR, host:port, for tcp services), _HOST and _PORT, and CHALUPA_CONSOLE_URL. |
chalupa wait [--config PATH] [--probe NAME] [--timeout SECONDS] [--json | --events ndjson] |
Waits for every declared compute.readiness probe through the tunnel and prints when each became ready. Open a tunnel first with chalupa tunnel keep. |
chalupa session status [--config PATH] [--json] |
BYOC, read-only: one reconciled deadline (local registration against what the console was told, and the lifetime cap) with the extensions left. |
chalupa session extend|renew|release [--config PATH] |
Extend a live session, re-lease an expired one whose server still runs, or release a warm box for session.keepFor (--keep-for 45m, or --sink for the confirmed down). |
chalupa apps build|up [--config PATH] [--ssh USER@HOST] [--identity FILE] [--json] |
Print the app-image build script and the app compose up. They do not run it. |
chalupa apps swap SERVICE [--from REF|HEAD | --use-tag TAG] [--confirm "swap <environment>"] [--keep N] [--no-ready] [--dry-run] [--json] |
Replaces one running app service without rebuilding it: a derived image from the one on the box plus a clean export of the git ref, restarting only that service and keeping the previous tag. |
chalupa apps ls [--config PATH] [--json] |
Lists the swap tags kept on the box. |
chalupa data load <archive> --dataset DATASET [--config PATH] [--confirm "load <environment>"] |
Uploads a checksummed archive once to the running box's protected volume (sha256 verified on the box); restore it per case with a cleanupRecipes entry and chalupa reset --dataset DATASET. --name DATASET still works here as a deprecated spelling of --dataset. |
chalupa cache-up|cache-nuke [--config PATH] [--confirm PHRASE] |
Creates (cache <environment>) or deletes (drop cache <environment>) the disposable <env>-cache volume for Docker images and build cache. It is billed separately and never touches the protected data volume. |
chalupa explain [--json] |
Returns the whole agent-facing surface as data: every action with its risk tier and confirmation phrase. |
chalupa docs [TOPIC] [--json] |
Prints focused documentation offline. Topics: inference, overview, actions, non-interactive, config, safety. |
chalupa box refresh [--config PATH] |
Re-delivers what Chalupa runs on a live box (heartbeat agent, Monitor forwarder, crash-capture service) over SSH, verified by digest, without relaunching it. Not billable; no phrase, terminal or cloud.url needed. Same action as chalupa runtime deliver. |
chalupa preview [--config PATH] |
Shows the project configuration and estimate offline. |
chalupa ACTION [--config PATH] |
Delegates one allowlisted public action to Task from the Chalupa repository root. |
chalupa ci arm|follow|cancel|report|artifacts|secrets|collect [--config PATH] |
Delegates a namespaced ci: action. ci follow --json streams the run's events as NDJSON with no terminal; ci secrets reads NAME=value lines from stdin (never a terminal); ci collect brings a settled run's evidence home. |
chalupa ci status [--config PATH] [--json] |
Reads bounded CI run state from the environment, the ci: sibling of chalupa status. A run armed a moment ago whose engine has not yet taken its lock is reported with phase starting, not as the previous run. |
chalupa teardown register [--config PATH] |
Delegates the namespaced teardown:register action. |
chalupa account [--json] |
Reads a redacted hosted-account snapshot from local files only. |
chalupa session allow [--config PATH] |
Managed only: detect the current IPv4 /32 and update SSH access. Managed session extension is not available yet. |
chalupa balance [--top-up 10|25|50] [--json] |
Shows the Managed GPU prepaid balance and rates, or opens Stripe Checkout for a top-up; needs a linked account. |
chalupa audit [--json] [--provider digitalocean|hetzner] |
Read-only provider inventory: Chalupa droplets, volumes, snapshots, firewalls and keys with project membership (DigitalOcean), or chalupa-labeled servers and volumes (Hetzner), with estimated cost and leftovers. Never changes anything. |
chalupa gc [--json] [--provider digitalocean|hetzner] [--delete NAME --confirm "reclaim NAME"] |
Dry run of compute no local stack tracks (with $/h). --delete reclaims one leftover after the literal confirmation and verifies the provider no longer lists it, on DigitalOcean and Hetzner alike. Nothing is proposed while any local stack is unreadable, and a golden-image build (either provider) started less than 2 h ago is never proposed. Storage stays. |
chalupa snapshot build [--config PATH] [--with-verifiers [--toolchain onbox --cairntrace VERSION|/abs/path.tgz [--node-version V]] [--recipe-sha256 HEX]] [--confirm "bake <env>"] |
Bakes a golden image (DigitalOcean or Hetzner) on a temporary billable host and records it in ~/.chalupa/goldens.json. Prints the estimate (about 10 minutes of the configured size) and, with --with-verifiers, exactly which tool versions it would install plus a recipe sha256; it does nothing without --confirm. --toolchain onbox is the on-box CI toolchain: Node 22.22.0 (exact, checksum-pinned), cairntrace at an exact npm version or a local .tgz for pre-release testing, Playwright and playwright-core 1.61.1 (exact) with Chromium as the only browser (no agent-browser), mongosh, the temporal CLI, jq and rsync; the bake asserts each pinned version and records the observed ones in /etc/chalupa/toolchain.json and in the image stamp, and the pins in ~/.chalupa/goldens.toolchain.json (snapshot status shows them). --recipe-sha256 carries the digest the dry run printed: the baker refuses any other recipe, and a local .tgz bake requires it. Ctrl-C deletes the build host before the command exits; the setup run is capped at 90 minutes. The build host gets only the project's SSH key, is tagged chalupa, and its host key is pinned for the setup run. With no ssh.identityFile / CHALUPA_TASK_SSH_IDENTITY, the ssh-agent must hold a key matching one of the selected account keys (ssh-add -L); otherwise the bake is refused before the host is created. |
chalupa snapshot status [--config PATH] [--json] |
Lists the environment's golden images with age, the Chalupa and Monitor versions that baked them, and which one compute.image uses; flags stale or behind-the-pin images. Read-only. |
chalupa snapshot prune --keep N [--config PATH] [--confirm "prune goldens <env>"] |
Dry run by default. With the literal confirmation, deletes the environment's goldens beyond the newest N (at least 1), never the one compute.image uses and never another environment's images or the protected data volume's snapshots. |
chalupa storage sweep [--config PATH]... [--rate USD] [--apply | --confirm "retire storage <env>"] |
Warm-day storage. For an environment that declares persist.onDemand and/or cache.retireAfterIdleHours, prints what is idle on this machine's session ledger and what retiring it would free; read-only (no tvault, no provider call) without --apply or --confirm. Applying deletes the idle cache and snapshots the data volume, verifies the snapshot at the provider, then destroys the volume; a failed snapshot destroys nothing. It never retires while a compute box is recorded, never without a ledger, and the data step needs --rate (or SNAPSHOT_RATE_USD_PER_GIB_MONTH). The next chalupa up restores what it retired. See Warm-day storage. |
chalupa login [--url ORIGIN] [--config PATH] |
Legacy operator-only device authorization; an administrator must approve the terminal. |
chalupa open [console|billing] [--url ORIGIN] |
Opens a hosted view through a short-lived, one-use browser grant. |
chalupa open <service> [--config PATH | --name ENV] |
Opens a forwarded web service's local URL in the browser. Needs no grant and no terminal. The service name may come before or after the flags. Use --service NAME for a service called console or billing. |
chalupa logout |
Revokes hosted sessions and removes both local credential files. |
chalupa monitor VERB [ARGS...] [--config PATH] |
Read-only: runs one Monitor read verb (issues, snapshot, processes, diff, hot and the others listed under observability) on the box over SSH and prints Monitor's output and exit code verbatim. A mutating verb is refused locally with exit 64. After --, everything goes to Monitor (chalupa monitor -- issues --help). |
chalupa monitor serve (--config PATH | --name ENV) |
Read-only stdio door to the box's Monitor for Monitor Desktop and other monitor.app.v1 clients. See Performance telemetry. |
chalupa logs [SERVICE...] [--since T] [--until T] [--grep RE] [--level L] [--tail N] [--box] [--follow] [--json] |
Read-only: a running box's logs over a fixed SSH allowlist. See Logs, diagnose and evidence. |
chalupa diagnose [--since-up] [--recipe NAME] [--no-recipes] [--bundle DIR] [--json] |
Read-only: containers, memory, disk, cloud-init, failed units and your declared diagnostics: recipes. |
chalupa evidence (--last-failed | --run-dir DIR) [--live | --snapshot] [--json | --md] [--out FILE] |
Read-only: a failed test run joined with the box for that run's time window. |
chalupa mcp [--config PATH] |
Read-only MCP server over stdio. See Chalupa for agents. |
chalupa run ACTION [--config PATH] |
Compatibility form of the same actions, spelled with the exact Taskfile name. |
An action whose Taskfile name contains a colon is typed as two words, so
chalupa ci arm delegates to the ci:arm action and chalupa teardown register
delegates to the teardown:register action. Both spellings resolve to the same
allowlist entry, risk classification, confirmation phrase, and TTY rule;
chalupa run ci:arm remains accepted for scripts written against the earlier
surface.
The CLI passes only the canonical config path to Task. Other Taskfile inputs
are read from the environment, so a dedicated key is selected with
export SSH_IDENTITY=/absolute/path/to/private-key before the command.
Logs, diagnose and evidence
Three read-only verbs answer "what happened on the box?" without a hand-written
SSH session. They need a running BYOC environment (managed GPU hosts have no
Compose stack) and read it over SSH with the same identity and address as the
tunnel. SSH never prompts here, so the box's host key must already be in your
known_hosts (running chalupa tunnel once records it); otherwise the read
fails with Host key verification failed instead of hanging.
There is no passthrough: each verb builds a fixed command from a short
allowlist (journalctl -o json, docker compose logs, docker ps and an
inspect limited to lifecycle facts, free, df, cloud-init status,
failed units, Monitor's issues --json, and the recipes you declared). Nothing
that changes the box can be expressed, container environments are never read,
and lines are redacted best-effort before they reach your terminal or a file.
chalupa logs api worker --since 15m --level error
chalupa logs --box --since 1h --grep 'out of memory'
chalupa logs api --follow
chalupa logs box --since 20m
chalupa logs reads the box. It is not chalupa test logs, which prints
this machine's own transcripts of a chalupa test run (provisioning, tunnel,
results). T is 10m, 2h, now, an ISO 8601 instant or @epoch seconds;
a time without a zone is UTC. Services that log to journald are read with
journalctl; the rest with docker compose logs. --level is exact for the
former and a text heuristic for the latter. --box adds the whole box's
journal (warnings and above unless you pass --level). The service name
box is not a Compose service: it reads the box's own systemd units
(cloud-init's stages, chalupa-compose, chalupa-health, the image-cache mount
and reset, overlays and replica-set units) with journalctl -u, at every
severity unless you pass --level; it is the place to see why a boot was slow.
chalupa diagnose
chalupa diagnose --since-up --bundle ./diagnose-2026-10-01
diagnose is one bounded read: every container with markers for an OOM kill,
restarts and non-zero exits, memory and swap, disk usage, cloud-init status,
failed units, Monitor issues when observability.monitor is declared, and
every recipe in diagnostics:. --since-up
adds this boot's warnings and errors. --bundle DIR writes the report and the
latest log lines to a new directory (mode 0700, files 0600) that stays on your
machine.
chalupa evidence --last-failed --live
chalupa evidence --run-dir ~/.cairntrace/runs/2026-10-01T14-02-11-000Z_checkout_a1b2c3 --md --out evidence.md
evidence is the box side of a failed test run. It reads a cairn run
directory without changing it (only run.json and events.ndjson are opened,
so screenshots, network captures and traces never enter the brief), derives the
window the run was alive plus or minus 30 seconds, and joins it with the box:
containers with OOM and restart markers inside that window, error-first logs
from every service and the box, host facts and your diagnostics. Markdown is
the default, shaped like cairn's own agent context and kept under 16 KB;
--json is the full document. --last-failed looks in the artifact root named
by the cairn config in test.config, then ./runs beside chalupa.yml, then
~/.cairntrace/runs; --runs-root DIR overrides that.
By default evidence reads the box live when it answers and otherwise falls
back, with a note, to the snapshot that chalupa down takes just before the
sink. --live insists on a live read; --snapshot (or --no-live) stays
offline. The snapshot is a bounded read (30 seconds at most,
and it never delays the destroy) of containers, error-first logs and
diagnostics, saved under ~/.local/state/chalupa/evidence/<environment>/. That
is what lets you explain a failed round after the box is gone. The snapshot
records which session took it, and evidence only joins a snapshot to a run
from that same session, so a later box that reused the environment name is
never presented as the box of an earlier run. Evidence stays local by default;
nothing is uploaded.
Exit codes: 0 on success, 64 for a usage or config refusal, 69 when the box could not be reached.
Verbosity
Every action honours one verbosity control, spelled the same way on the CLI and on the bundled Taskfile:
| Spelling | Where |
|---|---|
-q, --quiet |
chalupa ACTION -q / chalupa run ACTION -q |
-v, --verbose |
chalupa ACTION -v / chalupa run ACTION -v |
CHALUPA_VERBOSITY=quiet|verbose |
environment, read when no flag is passed |
VERBOSITY=quiet|verbose |
the same control, for the bundled Taskfile run directly |
A flag always wins over the environment; omitting both leaves the readable
default. Verbose is exactly the historical firehose: Pulumi's own preview
table and per-resource progress on data-up/up/down, and ci follow's
raw chalupa-ci {...} NDJSON line per engine event. The default level
captures Pulumi's transcript and prints resource counts, duration, warnings,
and (for up/data-up) the ip/priceHourly/providerId or
volumeName/volumeId outputs, instead of the whole thing; it renders ci follow's journal as one line per step — verdict, duration, and a running
tally — instead of raw JSON. Quiet shortens a clean success further (a
single line for Pulumi; passing steps go unprinted on ci follow, though
the tally still counts them).
None of this changes what a failure shows: at every level, a failed
pulumi up/down prints the complete transcript it always printed, in
full, before the task exits non-zero. A diagnostic is never harder to reach
than it was before this control existed.
status reports what the local Pulumi backend knows about two exact stack
names. Compute is absent, empty, or present. Protected data is
not-configured, absent, present, or unknown; configured states include
the expected size from chalupa.yml, while a present state also includes the
bounded local volume name and ID. The expected size is not a live provider
capacity reading.
The read path does not select a stack, load a provider credential, or query
DigitalOcean. An unexpected data stack, invalid outputs, inconsistent config,
or failed read becomes unknown instead of being treated as absent. Local
state can still be stale; the provider console and invoice remain
authoritative.
Actions and confirmations
Only these public actions are accepted, whether they are typed directly or
through chalupa run. The Actions screen shows the same set plus the bounded
local-status and CI-status refreshes:
| Command | Taskfile action | Risk | Exact phrase |
|---|---|---|---|
chalupa preview |
preview |
offline | none |
chalupa validate |
validate |
offline | none |
chalupa data-up |
data-up |
billable persistent data | persist <environment> |
chalupa up |
up |
billable compute; with session.reuse: auto a matching released box is reused with no phrase and re-leased up to session.expiresAfterMinutes (tunnel.onReuse: none keeps the reuse from re-opening the supervised tunnel, tunnel.afterUp: none closes the readiness tunnel at the end of a cold up --wait); without a terminal the phrase is still required to reuse, unless you pass --reuse-only: it reuses a live unchanged box or refuses with a reason code, never launches, and needs no phrase or terminal |
launch <environment> |
chalupa agent-bootstrap |
agent-bootstrap |
changes the provisioned host | bootstrap <environment> |
chalupa runtime deliver (also chalupa box refresh) |
runtime:deliver |
changes Chalupa's own files on the host; not billable, idempotent | none |
chalupa tunnel |
tunnel |
read-only connection | none |
chalupa ssh |
ssh |
read-only interactive session | none |
chalupa seed |
seed |
changes application data | seed <environment> |
chalupa teardown register |
teardown:register |
changes the local expiry registry | register <environment> |
chalupa session extend --hours N |
session:extend |
billable: moves a live session's deadline | extend <environment> |
chalupa session renew |
session:renew |
billable: re-leases an expired session whose host is still running | renew <environment> |
chalupa session release |
session:release |
shortens the lease to the warm window, then stops the tunnel; never adds spend; refuses unless the local teardown schedule is healthy | none |
chalupa ci arm |
ci:arm |
billable suite time | test <environment> |
chalupa ci follow |
ci:follow |
read-only log stream (--json: NDJSON events of the run armed last, no terminal needed; exits 0 settled, 1 otherwise) |
none |
chalupa ci cancel |
ci:cancel |
stops the active run | cancel <environment> |
chalupa ci report |
ci:report |
publishes the settled run; idempotent | none |
chalupa ci artifacts |
ci:artifacts |
read-only artifact index | none |
chalupa ci secrets |
ci:secrets |
delivers the declared ci.secrets values (stdin) to the box; nothing created, no spend |
none |
chalupa ci collect |
ci:collect |
reads a settled run's evidence into ~/.local/state/chalupa/ci-runs/<runId>/ |
none |
chalupa down |
down |
destroys disposable compute | sink <environment> |
<environment> is the exact resolved environment name. Interactive up
and down use a yes/no prompt, defaulting to No; select Yes or press y to
continue. Escape, Ctrl-C, a closed terminal or No cancels. Other changing
actions retain their exact typed phrase. For non-interactive use, the table
lists the case-sensitive --confirm phrase; a mismatch refuses before Task.
Before each confirmation, Chalupa prints a target summary with the
redacted canonical config path, exact compute stack, region, and size.
data-up also shows the exact <environment>-data stack and configured
expected GiB. down shows the protected-data stack and states that it
survives.
For example:
chalupa up --config "$DEMO_CONFIG"
Every changing or long-running action requires an interactive terminal.
preview and validate are the only actions available in synthetic mode and
the only actions accepted outside a TTY. status --json remains available to
non-interactive consumers when they supply an explicit config.
chalupa validate runs the full gate (typecheck, the test suite, preview and
the CI plan). chalupa up and chalupa data-up run a lighter offline launch
gate first: the config's Compose preview and CI plan. They skip Chalupa's own
test suite. Set CHALUPA_STRICT_GATE=1 to make them run the full gate as well.
What --confirm actually is
chalupa ACTION --config PATH --confirm "<phrase>" supplies exact
environment-bound authorization as a literal command-line argument — so a script, cron job, or another
tool can run a billable or destructive action without a terminal attached.
Be honest about what this buys you: it is friction against a human running
the wrong command against the wrong environment, not authentication. The
phrase is not a secret — it is deterministically derived from the resolved
config's own name field ("launch " + config.name, "sink " + config.name,
and so on), so anyone who can read chalupa.yml can compute it, and
src/cli/chalupa.ts prints the exact expected phrase back on a mismatch
(The supplied --confirm phrase does not match; expected exactly "...") —
deliberately, because hiding it would only turn a typo into a support
question instead of a stopped command. What --confirm actually enforces is
that the target and the authorization are stated together, explicitly, in
the one invocation that uses them (--confirm requires --config on the
same command line; an inherited CONFIG does not count), so an automation's
own config carries a phrase written once on purpose rather than a credential
that could leak or be replayed against a different environment. See chalupa docs non-interactive for the complete rule set.
Sign in to the console
chalupa setup, chalupa login, chalupa open, and chalupa logout are the
commands in this flow that reach the network; every other command stays
offline until it delegates to Task. They link this workstation to the hosted
control plane rather than to any specific environment.
For an ordinary customer account, use setup. It signs in or creates the tenant account used by billing and organization views:
chalupa setup
chalupa account --json # offline, with session tokens redacted
chalupa open
chalupa open billing
chalupa logout
The web sign-in form takes your email. Add your password if you have one; without a password, it emails a single-use code and asks you to enter it on the same form.
chalupa login is not the customer sign-in command. It is the separate,
legacy operator-only device flow and requires an administrator to approve the
displayed code on the console's Terminals page:
chalupa login --config ./chalupa.yml # or --url https://chalupa.example
setup writes the verified tenant-account session to
~/.config/chalupa/account.json; this is the account used by chalupa account
and chalupa open. login is a separate, legacy operator-only device flow.
It writes a revocable terminal credential to session.json only after an
administrator approves the displayed code. A tenant session in account.json
never auto-approves that device flow or grants operator access. Both files have
owner-only (0600) permissions. When the tenant account session is still
valid, open exchanges it directly for a short-lived, one-use browser grant.
open billing requests the billing destination from the same allowlisted
endpoint. A stored credential is never
sent to an origin different from the one that issued it.
account and the TUI's Hosted screen merge those files into a local,
redacted status view without contacting Chalupa. The view can include
cached plan and managed-hour fields, always marked fresh or stale; the hosted
billing view is authoritative. Session tokens are deliberately absent from
both text and JSON output. logout attempts to revoke both sessions and
removes the local copies even when the control plane cannot be reached.
The stored credentials are scoped to hosted access, not to Chalupa's provider
access: neither is the DigitalOcean token, neither is printed, both expire,
and the terminal credential can be revoked early from Terminals in the
console. --url
selects the control-plane origin directly; without it, Chalupa falls back in
order to CHALUPA_CLOUD_URL, then the selected config's cloud.url, then the
origin from a valid setup account or a previous chalupa login on this
machine (even if that terminal credential has expired — the origin it named is
still meaningful), and finally to https://chalupa.run, the one production
control plane, so a bare chalupa login on a fresh machine has somewhere to
go. chalupa login prints
which of the last two it used, since both are a guess rather than something
you told it. Nothing about login changes what the Taskfile allowlist can
do — it only lets open skip a manual sign-in.
Provider-bound sinking
Both surfaces bind down to the provider ID of the compute they most
recently observed, and pass it to the Taskfile as a typed
EXPECTED_PROVIDER_ID argument. The provider-scoped destroy task compares
the current stack output with that value immediately before destruction and
refuses a mismatch. Where they differ is what happens without a fresh
binding.
The operator UI requires the operator to refresh status first: press r
immediately before selecting Sink compute. The UI accepts only a
present compute status observed within the last two minutes with a valid provider ID,
and shows that ID in the target summary. Without that fresh observation,
sinking stays disabled — the UI refuses rather than proceeding unbound.
Chalupa also revalidates the same two-minute observation immediately after
the confirmation; a prompt left open past the deadline is cancelled before
Task starts and requires another status review.
Direct chalupa down captures its own fresh status immediately before
running, with no separate refresh step required, and binds to the reported
provider ID exactly as the UI does. When that capture fails, or the reported
compute carries no provider ID, chalupa down proceeds anyway, bound only by
the configured local stack identity, and prints a warning
(Provider binding unavailable; down stays bound to the stack name only.)
before it runs. It still requires interactive yes/no confirmation or the exact
--confirm "sink <environment>" phrase and
uses the normal compute-only Taskfile path. Prefer the UI when you want a
missing binding to block the destroy instead of just being warned about it.
data-nuke is intentionally unavailable in the CLI and operator UI. Permanent
data deletion remains an independent, explicitly confirmed Taskfile operation
documented in Lifecycle. The one automated route
is chalupa storage sweep for an environment that opted in with
persist.onDemand, which destroys the data volume only after a snapshot is
verified at the provider. Never reproduce it with direct Pulumi commands.
Navigate the operator UI
The interface follows the same deep-green and teal visual language as
chalupa.run. It adapts to light and dark terminals, uses a bounded 256-color
fallback, and returns to terminal-default styling when theme or color support
cannot be established safely. Set NO_COLOR=1 to disable styling. Status,
risk, selection, and availability always retain text markers, so color is
never the only signal.
The full-screen interface requires a capable interactive terminal. When
TERM=dumb, use the linear chalupa inspect and chalupa status commands and
the direct actions instead; Chalupa will not force an alternate-screen UI onto
that terminal.
The landing screen is Home, and it shows a session rather than a parsed configuration. A session is four separate observations, in order:
| State | What proves it |
|---|---|
| Configured | A chalupa.yml was read offline. |
| Provisioned | The local Pulumi backend reports live compute. |
| Reachable | The forwarded Ollama answered on 127.0.0.1. |
| Model ready | The declared model is installed or loaded, with its GPU placement. |
Each state carries its own observation and its own age. A state this laptop has
not observed reads UNKNOWN; it is never inferred from a neighbouring one, and
an observation older than two minutes reads STALE rather than as fact. Home
also names exactly one next action — "Launch GPU to start a session", "Open
agent", "Launch an agent", "Sink to stop billing" — so the interface has an
answer to "what do I press now".
Which screens exist depends on what the configuration declares. An inference environment shows Home, Model, Actions, and Help. A Compose or CI environment shows Home, Preview, Status, CI, Actions, Hosted, and Help.
- Home — session state, cost, expiry, the endpoint an agent uses, and the primary actions.
- Model — the tunnelled endpoint: daemon version against the pin, installed or loaded state, digest, quantization, declared and loaded context, model ceiling, GPU placement, weight size, and the endpoint URLs an agent needs.
- Preview — the complete redacted remote Compose document.
- Status — independent compute and protected-data state from the local Pulumi backend.
- CI — the declared suite, bounded run state, connection state, and budget.
- Actions — the Taskfile allowlist, narrowed to the blocks this configuration declares, with risk classification.
- Hosted — the redacted local account/session cache and usage freshness; opening it never makes a network request.
- Help — safety boundaries, what each session state means, and the keyboard reference.
Home's primary actions are Launch GPU (up), Open agent, and Sink
(down), plus Install expiry and Register expiry when the config
declares a session: block. Open agent asks which agent to run and then runs exactly what chalupa launch runs: the
UI suspends, the managed tunnel opens, the agent takes the terminal, and the
agent's own settings are never modified. When the agent exits, the UI says what
is true: the GPU is still running and still billed.
Every screen of an inference session carries the same chrome line: elapsed
time, the hourly rate, the estimated cost so far, and how much of the declared
session budget is left. Elapsed time is measured from the first moment this
window observed the box — status:json carries no creation time — and it is
labelled as an observation, not as uptime. Whether the local teardown scheduler
actually holds this deployment is not something the UI reads, so it says
armed state unknown and keeps a banner up: "Expiry is not armed. This GPU
runs until you sink it."
Use these keys:
| Key | Effect |
|---|---|
1–8 or o, m, p, s, c, a, u, ? |
Open a visible screen directly. |
Left, Right, h, l, or Tab |
Move between screens; Shift-Tab moves backward. |
Up, Down, j, or k |
Move through actions or scroll the current screen. |
| Page Up or Page Down | Scroll by one visible page. |
| Home or End | Jump to the beginning or end of the current screen. |
| Enter | Run the selected enabled action. |
r |
Refresh what the current screen observes. |
x or Backspace |
Dismiss the current notice, or close the agent chooser. |
| Escape | Return to Home. |
q |
Exit while the operator UI owns the terminal. |
| Ctrl-C | Interrupt the active confirmation or foreground Task command. |
Long prose wraps at word boundaries, while long Compose values wrap by
terminal cell width so the complete redacted plan remains inspectable. A ↳
marker identifies a visual Compose continuation and is not part of the source
document. Scroll is bounded to real content. Narrow terminals use short tab
labels and a focused action detail view; Page Up and Page Down reveal any
remaining details. Terminals smaller than 40 columns by 12 rows show only a
resize instruction instead of hiding controls or presenting a partial action.
The UI starts from local inspection only, so it opens offline and instantly.
For a real configuration it then re-reads the session about every twenty
seconds on its own: the bounded status:json read, and — when compute is present and the
config declares inference: — one bounded loopback read of the forwarded
Ollama. Automatic reads are quiet: they never move the screen, never claim the
UI is busy, and a read that fails never replaces what is on screen, because a
failed read is not evidence that anything changed. r remains available and
refreshes immediately. In synthetic mode the UI displays SYNTHETIC OFFLINE DEMO, performs no automatic reads, disables status refresh, and disables every
action except Preview and Validate.
The Actions screen lists only what the configuration can actually run:
data-up without a persist: block, seed without a seed: command, ci:
actions without a ci: block, and expiry registration without a session:
block are hidden rather than shown as refusals. For everything still listed it
explains why an operation is disabled before anything is delegated.
Control-plane bootstrap and tunnels are available only when their corresponding
declarative config exists. If a loaded local status proves compute is absent, bootstrap, tunnel,
shell, seed, and expiry registration remain disabled; without a status observation,
the UI labels them config-eligible and leaves runtime verification to the
Taskfile. Provider actions require an explicit chalupa.yml; down
additionally requires a fresh provider-bound status as described above.
Compact mode keeps the selected action, risk, availability, and exact
confirmation phrase in the focused view.
For an explicit config, the Status screen presents compute and protected data
separately. An unknown data state remains visible with its bounded reason;
the UI never collapses it into absent. A fresh present compute status with a
provider ID also enables provider-bound sinking for two minutes.
For confirmations and foreground Task output, the UI temporarily returns
terminal ownership to the command. A tunnel keeps ownership until its Task or
SSH process exits. After a completed Task, press Enter to return to the UI.
Starting a status refresh clears the previous observation immediately, and a
failed refresh leaves it unknown. Running data-up, up, or down also
invalidates the displayed observation, even when the Task fails, so a stale
provider identity cannot keep a lifecycle action enabled. The status view
shows the safe-down freshness deadline and repaints when it expires.
Recover from unknown state
If data-up, up, or down fails or is interrupted, do not immediately
repeat it or assume that rollback succeeded. Resource state and billing may be
unknown.
- Run
chalupa status --config "$DEMO_CONFIG"to inspect bounded local compute and protected-data state. - Verify the exact resource in the DigitalOcean console and check billing.
- Re-run
chalupa inspect --config "$DEMO_CONFIG"before choosing the next lifecycle action.
An absent local stack is not proof that the provider has no resource. A
present protected-data state identifies bounded local outputs and the
configured expected size, not live provider capacity or billing. Do not use
data-nuke as recovery.
Taskfile and secret boundaries
The CLI passes a canonical config path to a fixed public Taskfile action. The Taskfile continues to own validation, the local Pulumi backend, tvault profiles, SSH, reporting, and separation of compute from persistent data. Do not call underscore-prefixed tasks or Pulumi lifecycle commands directly.
The CLI does not cache config history, persist secret values, or create a credential store. Tasks resolve their existing least-privilege tvault profiles only when the selected action needs them. The redacted preview and local Pulumi state can still contain private operational metadata; protect the workstation and state directory.
Continue with Quickstart, Lifecycle, Secrets with tvault, and Troubleshooting.
Remote inference
See Remote GPU inference for the experimental host-Ollama workflow.
chalupa exec --format env --config PATH prints endpoint exports without changing files.
See Agent harnesses for launch commands and configuration.
setup --inference links the hosted account and writes a GPU starter with console
heartbeats, then offers to arm the organization's console reaper so it can stop
reported GPU deployments even when the laptop sleeps. --no-reaper-token skips
arming. --no-console skips linking and omits cloud:, warning that expiry
depends on this machine staying awake and unlocked.
Context per inference session
Use chalupa launch opencode --context auto to negotiate context on
the existing remote GPU. --context 64k, 128k or native request a
specific size or the model ceiling. See Inference for
fit checks and limitations. The YAML and teardown identity are unchanged.
Managed telemetry bootstrap
chalupa up arms console telemetry after the managed host is ready. The success
line is Telemetry armed: heartbeats, GPU samples and inference runs will appear in the console.
Use chalupa session bootstrap [--config PATH] [--json] to retry delivery or
rotate the environment's ingest key. tunnel and launch retry automatically
when the managed record has bootstrapped: false (including older records without
that field). JSON reports bootstrap state and the public key ID, never key material.
See Managed GPU for signals and receipt limitations.