fundaia 0.4.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
# fundaia

The command line for the [Fundaia](https://infrastructure.fundaia.com) deployment platform:
projects, services, variables, deployments, logs and metrics, from a terminal.

It talks to any instance of the platform, not just one — `--url` and profiles are first-class,
so the same binary reaches production, staging and a laptop.

## Install

```bash
cargo install fundaia
```

Or take a prebuilt binary from the [releases](https://github.com/alex28042/fundaia-apps/releases):

```bash
curl -fsSL https://raw.githubusercontent.com/alex28042/fundaia-apps/main/cli/install.sh | sh
```

The script downloads the binary for your platform, verifies its checksum and puts it in
`~/.local/bin`. It never uses `sudo`; if that directory is not on your `PATH` it says so rather
than editing your shell profile behind your back.

## Sign in

```bash
fundaia login --url https://infrastructure.fundaia.com
```

A terminal cannot present a passkey, so the credential is minted where one works. The command
prints a short code, opens the browser at the approval page, and waits. You compare the code,
press **Autorizar**, and the terminal picks up a token of its own.

It works over SSH too: nothing depends on the browser being on the same machine, and
`--no-browser` prints the address instead of opening it.

The token is stored in `~/.config/fundaia/config.toml`, owner-readable only. Revoke it from
**Línea de comandos** in the web interface — `logout` only forgets it locally, and says so.

## What it does

```
fundaia status                          # every project, in one screen
fundaia services fisiovista             # what is in one project, and what depends on what
fundaia service fisiovista api          # one service in detail

fundaia deploy fisiovista api           # deploy, and watch the pipeline
fundaia deploy fisiovista api --restart # reuse the image, restart the container
fundaia rollback fisiovista api 41      # put deployment #41 back
fundaia stop fisiovista api             # sleep now; the next request wakes it

fundaia logs fisiovista api -f          # follow the live output, in colour
fundaia logs fisiovista api -n 500 --stream build
fundaia history fisiovista api          # the deployments, newest first
fundaia metrics fisiovista api          # CPU and memory, now and over an hour

fundaia vars list fisiovista api
fundaia vars list fisiovista api --reveal
fundaia vars set fisiovista api JWT_SECRET … --secret
fundaia vars unset fisiovista api JWT_SECRET

fundaia domains list fisiovista api
fundaia domains add fisiovista api app.midominio.com
fundaia expose fisiovista postgres --off   # off the internet; siblings still reach it
fundaia expose fisiovista api              # publish it again

fundaia templates                       # the components on offer
fundaia new fisiovista Postgres --template postgres-16
fundaia new fisiovista Claude --template claude-code
fundaia new fisiovista api --repo owner/api --connect-to Postgres,Storage
fundaia connect fisiovista api Postgres # wire an existing pair together
```

Projects and services are named however you can see them — the id, the slug or the display name
all work. An ambiguous name is an error rather than a guess.

### Private services

Every service is always reachable inside its project by name — that part is not optional and never
was. What is optional is the hostname on top of it:

```
fundaia expose fisiovista postgres --off
```

The database keeps running, `api` keeps reaching it at `postgres:5432`, and nothing outside the
machine can. Its domains are removed rather than kept "for later": a hostname that still resolves is
the opposite of what was just asked for. Publishing it again gives it a hostname on the next
deployment.

### Deploying, watched

`deploy` is the reason this exists rather than a `curl` alias. It starts the deployment and stays,
redrawing the six stages in place while the build scrolls underneath:

```
  → fisiovista api despliegue #42

  ✔ En cola 120ms
  ✔ Descargando el código 1.4s
  ▸ Construyendo la imagen …
  ○ Arrancando el contenedor
  ○ Comprobando que responde
  ○ En marcha
```

The stages come from the server, which is why they are the same six the web interface draws. Where
it stopped has one answer, whichever window you are looking at.

It exits non-zero when the deployment fails, so `deploy && smoke-test` behaves.

### Wiring services together

`--connect-to` and `connect` write **references**, never copied values:

```bash
$ fundaia connect fisiovista api Postgres
  ✔ api → Postgres
  DATABASE_URL apunta a Postgres
    + DATABASE_URL
    + PGHOST
```

`DATABASE_URL` becomes `${{ Postgres.DATABASE_URL }}`, resolved at deploy time. Rotating the
database password changes one value and every consumer follows — and the reference is what draws
the arrow on the canvas, so the picture cannot drift from what the containers are handed.

What each kind of provider hands over is decided by the server, not here: connecting to a bucket
gives the `S3_*` set, connecting to another application gives its internal address and port.

### Components

`templates` lists what this instance offers **your account**, and `new --template <key>` adds one:

```bash
$ fundaia templates
  postgres-16  PostgreSQL 16              docker.io/library/postgres:16-alpine
  redis-7      Redis 7                    docker.io/library/redis:7-alpine
  bucket       Bucket · almacenamiento S3 docker.io/minio/minio:latest
  claude-code  Claude Code · Sonnet 5     localhost/claude-code-service:latest

$ fundaia new fisiovista Claude --template claude-code
$ fundaia connect fisiovista api Claude
  ✔ api → Claude
  CLAUDE_CODE_URL y su token apuntan a Claude
    + CLAUDE_CODE_URL
    + CLAUDE_CODE_TOKEN
    + CLAUDE_CODE_MODEL
```

`claude-code` is an agent a sibling service can ask questions of, and **only the owner of an
instance may add one** — it spends money and runs what it is told. An instance answers a stranger
asking for it exactly as it answers one asking for a component that does not exist, on purpose: what
it offers somebody else is not a stranger's business. So the tool says both things that answer can
mean rather than printing a 404:

```
✖ el componente `claude-code` no está disponible: o no existe en esta instancia, o solo su
  dueño puede añadirlo — `fundaia templates` lista los que esta cuenta sí puede crear
```

## In CI

Every option reads an environment variable, so nothing has to be written to disk:

```yaml
env:
  FUNDAIA_URL: https://infrastructure.fundaia.com
  FUNDAIA_TOKEN: ${{ secrets.FUNDAIA_TOKEN }}
run: fundaia deploy fisiovista api
```

Colour turns itself off when the output is not a terminal; `--no-color` forces it.

## Profiles

```bash
fundaia login --url https://staging.example.com --as-profile staging
fundaia --profile staging status
```

`--url` and `--token` on their own override whatever is saved, for one invocation, without
disturbing it.

## Updates

The tool updates itself. When crates.io has a newer release it says so **on stderr**, never on
stdout — `deploy` is meant to be chained and its output is read by scripts — and then downloads that
release's binary for your platform, verifies its `sha256` and moves it over the one you are running.
The command you typed finishes first; the next one is the new version.

```
  ! Hay una versión nueva de fundaia: 0.4.0 → 0.4.1
    descargando…
  ✔ Actualizado. La siguiente orden ya usa 0.4.1.
```

It never gets in the way of what you asked for. The look at the registry runs alongside the command
rather than before it, the answer is cached for a day next to the profiles
(`~/.config/fundaia/update-check.toml`), and anything that fails — no network, a captive
portal, crates.io down, a binary somebody else owns — prints a line and leaves what is installed
alone. Nothing here can make a command that worked exit non-zero.

**It only replaces itself in front of a person.** With stderr on a terminal this is your laptop and
the next command should be the new version; through a pipe it is a script or a CI job, where a binary
that changes underneath a running pipeline is a bug nobody will find. There it prints the notice and
stops.

Two environment variables, for the two different objections:

```bash
FUNDAIA_NO_SELF_UPDATE=1   # keep the notice, never install — your package manager owns this binary
FUNDAIA_NO_UPDATE_CHECK=1  # no notice, no registry, nothing
```

To do it now rather than on the next command:

```bash
fundaia upgrade          # download and install the newest release
fundaia upgrade --check  # only say whether there is one
```

`upgrade` ignores the cache, talks on stdout and fails loudly — somebody who asked to be updated and
was not is owed the reason.

The checksum is not optional, unlike in `install.sh`. That script runs because you typed it and can
read what it says; this runs by itself, over the binary that holds your deploy token, so a release
with no `.sha256` beside it fails the update instead of being installed on trust.

## Building from source

```bash
cd cli
cargo build --release
cargo test
cargo clippy --all-targets
```

## Releasing

Nobody edits the version in `Cargo.toml`. A push to `main` that changed anything under `cli/` runs
`.github/workflows/cli.yml`, which reads the conventional-commit type of the commits that touched
this directory — `feat` a minor, a `!` or a `BREAKING CHANGE` footer a major, anything else a
patch — raises the manifest and the lockfile, commits, tags `cli-v<version>`, builds the four
binaries and offers the crate to crates.io. A `feat` anywhere else in the repository reaches none of
it.

The release does not wait for that tag to start a second workflow, because it cannot: the commit and
the tag are pushed with `github.token`, and a push made with that token triggers nothing. `release`
and `publish` read the version off the job that decided it, in the same run. The workflow says why
at length, next to the job.

A version is never published twice. The number is derived by raising the one on `main`, and the
release commit moves that starting point on; a tag that already exists stops the run before it
commits; the publish step asks crates.io whether the version is there and skips it if it is; and
`cargo publish` refuses a duplicate on its own. If a target fails to build after the tag is already
pushed, `gh workflow run cli.yml --ref cli-v0.4.0` runs the release again from the tag, and the
publish step skips whatever already landed.

**crates.io needs `CARGO_REGISTRY_TOKEN`, and this repository has it.** The token is scoped to
`publish-new` and `publish-update` on the `fundaia` crate and to nothing else — it cannot yank
a version, cannot change who owns the crate, and cannot touch any other crate on the account. It
does not expire, because a credential that lapses on a date nobody wrote down turns into a red
release three months after the person who minted it stopped thinking about it.

Should it ever be missing, the publish step says it is skipping and ends green: the tag, the four
binaries and their checksums are attached regardless, so `install.sh` keeps working and only
`cargo install fundaia` does not. To mint a replacement, go to **Account settings → API
tokens** on crates.io and add it here under **Settings → Secrets and variables → Actions**.

## Licence

MIT.