fundaia 0.5.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
fundaia-0.5.0 is not a library.

fundaia

The command line for the Fundaia 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

cargo install fundaia

Or take a prebuilt binary from the releases:

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

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 project new Fisiovista          # a project, which everything else hangs from
fundaia project rename fisiovista "FisioVista" --description "…"
fundaia project move fisiovista equipo  # hand it to a shared space
fundaia project archive fisiovista --confirm fisiovista

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 set fisiovista api JWT_SECRET … --sealed   # nunca legible, ni una vez
fundaia vars seal fisiovista api JWT_SECRET --confirm JWT_SECRET
fundaia vars unset fisiovista api JWT_SECRET
fundaia vars import fisiovista api .env --secret   # the whole file, in one request

fundaia volumes list fisiovista postgres
fundaia volumes add fisiovista postgres /var/lib/postgresql/data
fundaia volumes remove vol_abc123

fundaia recipe show fisiovista api                 # the Containerfile it builds with
fundaia recipe show fisiovista api --plain > Containerfile
fundaia recipe edit fisiovista api --file Containerfile

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
fundaia disconnect fisiovista api Postgres  # and undo it

Everything the web interface can do, this can do, with three exceptions that only make sense with a mouse: moving a node on the canvas, the live event stream, and the metric graphs — metrics prints the numbers instead.

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.

Sealed variables

A secret can be read back by whoever can reach the instance — that is what --reveal is. Sealing takes that away, permanently:

fundaia vars seal fisiovista api STRIPE_SECRET --confirm STRIPE_SECRET

The container keeps being handed the value. Nothing else can have it: not this tool, not the web interface, not the API. The row cannot be overwritten either, so the key cannot be quietly pointed at a different credential while everything downstream keeps reading the same name. Deleting it is the only operation left.

The key has to be typed back because it cannot be undone. --sealed on set does the same in one step, and is the only way a value is never readable at any point — sealing afterwards leaves it readable for as long as it takes to run the second command.

What it does not defend against is somebody who can deploy code into that project: a container can print its own environment, and a sibling service can still reference the variable. It defends against the platform's own surfaces — a screenshot, a shared terminal, a stolen session — and against changing a credential by accident.

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:

$ 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:

$ 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:

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

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:

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:

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

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.