fundaia 0.9.1

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# 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 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 backups list fisiovista postgres
fundaia backups new fisiovista postgres          # una copia ahora, y espera a que esté
fundaia backups schedule fisiovista postgres daily,monthly
fundaia backups restore bkp_abc --confirm bkp_abc
fundaia backups keep bkp_abc                     # que la retención no se la lleve

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.

### Backups

Every service with a volume is copied **daily by default** — including the ones that existed before
this arrived. A backup nobody turned on is the only kind that is there when it is needed.

```
fundaia backups schedule fisiovista postgres daily,monthly
```

Three schedules, any combination of them, each with its own retention: daily kept for six days,
weekly for a month, monthly for three. Passing none turns automatic copies off and leaves the
existing ones alone.

What is copied is the **volume**, not a `pg_dump`. That is the same operation for Postgres, Redis
and a bucket, and it does not need the platform to know what is inside one. The service keeps
running while the copy is taken.

Restoring overwrites the volume, so it asks for the id back:

```
fundaia backups restore bkp_abc --confirm bkp_abc
```

The server takes a copy of the current state **first**, then stops the service, restores, and starts
it again. That safety copy is not optional: the restore that goes wrong is the one where somebody
picked the wrong row, and without it the state they replaced is gone.

`backups keep` pins a copy so retention will not delete it — what you reach for when you know a bad
migration is coming.

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

```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
```

## Scripting and AI agents

`--json` answers every question in the server's own DTOs, compact, one value per invocation —
which is what a program (or an AI agent driving this tool) wants to read instead of a table laid
out for eyes:

```bash
fundaia status --json | jq '.projects[].name'
fundaia services fisiovista --json | jq '.services[] | select(.state != "active")'
fundaia history fisiovista api --json | jq '.[0].status'
fundaia deploy fisiovista api --json | jq '.deployment.status'   # waits, then one object
```

The streams speak JSON too, one object per line, so `jq` and a read-loop both work without
buffering:

```bash
fundaia logs fisiovista api -f --json      # every log line as {"sequence":…,"message":…}
fundaia watch --json                       # every platform event as it happens
```

Four properties make this reliable to build on:

- **stdout carries the answer and nothing else.** Notices, progress and update chatter go to
  stderr or nowhere; `deploy --json` prints exactly one object when the pipeline ends.
- **A failure is one JSON object on stderr**`{"error":{"code","message","status","hint"}}`  with the server's own error code, so a program branches on `unauthorized` or `conflict` instead
  of parsing a sentence written in Spanish. The exit code still says whether it worked.
- **Colour is off**, whatever the terminal says: no escape ever lands inside a value.
- **`FUNDAIA_JSON=1` says the same thing from the environment**, so an agent is configured once
  and then types plain commands.

Write commands (`vars set`, `new`, `connect`…) confirm with their exit code; the sentence they
print is for people and needs no parsing.

Shell completions come from the same argument definitions the parser uses:

```bash
fundaia completions bash > ~/.local/share/bash-completion/completions/fundaia
fundaia completions zsh  > "${fpath[1]}/_fundaia"
fundaia completions fish > ~/.config/fish/completions/fundaia.fish
```

## 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 replaces itself whenever it is used, except inside CI.** The swap is a `rename` and the
running command keeps its old inode, so upgrading under a pipe is as safe as upgrading under a
person — an AI agent that drives this tool all day stays current the same way a laptop does, and
the command that triggered the update finishes on the version it started with. The one place a
binary must not move is a CI job, where a tool that changes between two steps of a pipeline is a
bug nobody will find: with `CI` in the environment — every hosted runner sets it — 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
```

The performance budgets — startup time of the real binary, table rendering, SSE parsing — are
tests too, `#[ignore]`d because they only mean something on the release profile:

```bash
FUNDAIA_PERF_BUDGET_MS=150 cargo test --release -- --include-ignored
```

The pipeline runs them on every change under `cli/`, next to an 8 MiB ceiling on the binary
itself, so a dependency that drags startup down or doubles the download fails the build instead of
being discovered by somebody's stopwatch.

## 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.