Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e5579c3
Sync openapi.yaml with preview branch endpoints
nathanjcochran Oct 16, 2026
f7a762c
Add experimental tiger branch commands
nathanjcochran Oct 16, 2026
2e6ef7d
Add experimental branch MCP tools
nathanjcochran Oct 16, 2026
1735b2f
Keep branches out of the default service
nathanjcochran Oct 19, 2026
d8c8c53
Move to service-nested branch endpoints and the resolve endpoint
nathanjcochran Oct 19, 2026
306096c
Keep service commands on the service list's ref filter
nathanjcochran Oct 19, 2026
545d8c3
Stop saying a branch starts with its parent's password
nathanjcochran Oct 19, 2026
b56a2f9
Add --iops to branch create and resize
nathanjcochran Oct 19, 2026
9ab58f7
Don't guess a branch's port
nathanjcochran Oct 19, 2026
f40bd0e
Don't guess service ports
nathanjcochran Oct 19, 2026
aeb420c
Move the shared branch create command into branch_create.go
nathanjcochran Oct 19, 2026
424b939
Note the IOPS-only branch resize workaround as a TODO
nathanjcochran Oct 19, 2026
6e31a18
Stop dressing branches up as services
nathanjcochran Oct 19, 2026
6900ece
Sync openapi.yaml and send IOPS-only branch resizes alone
nathanjcochran Oct 20, 2026
85dd2c7
Complete branch refs in the branch commands
nathanjcochran Oct 20, 2026
7e12872
Keep the service and branch ref helpers side by side
nathanjcochran Oct 20, 2026
b5c0a5f
Show a branch's parent name and a service's branch count
nathanjcochran Oct 20, 2026
cae0fd2
Say what unblocks moving service lookups to the resolve endpoint
nathanjcochran Oct 20, 2026
e490fb9
Stop printing a Console URL for branches
nathanjcochran Oct 20, 2026
705959f
Keep structured output structured when a list is empty
nathanjcochran Oct 20, 2026
c93c3ab
List the branches deleted along with a service
nathanjcochran Oct 21, 2026
2af0dd1
Merge remote-tracking branch 'origin/main' into nathan/branches
nathanjcochran Oct 21, 2026
e008b4e
Return branch IOPS in resources, as for services
nathanjcochran Oct 21, 2026
30ef207
Accept only a 204 from the password update endpoints
nathanjcochran Oct 21, 2026
8357fec
Pin generated passwords in tests and cover generation failures
nathanjcochran Oct 21, 2026
bec7b64
Pass a replica its primary's credentials, not a whole target
nathanjcochran Oct 21, 2026
de3ec7c
Offer the db psql password reset for branches
nathanjcochran Oct 21, 2026
39fee03
Name branches in db help text and readiness errors
nathanjcochran Oct 21, 2026
d7cc309
Drop BranchCPUMemoryConfigs in favor of FluidCPUMemoryConfigs
nathanjcochran Oct 21, 2026
e42320d
Fold Resolve into its private helper
nathanjcochran Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ Further conventions:

### Service Refs

A command that identifies a service takes a **ref** — its ID, a read replica set ID, or its name — and hands it to the `ref` filter on the API's service list through the helpers in `service_ref_helper.go`, which also return the whole service. Never try to tell an ID from a name in the CLI.
A command that identifies a service takes a **ref** — its ID, a read replica set ID, or its name — and resolves it through the API, which also returns the whole resource. Never try to tell an ID from a name in the CLI. `tiger service` commands resolve through the `ref` filter on the API's service list (`resolveService`). `tiger branch` and `tiger db` commands use the API's resolve endpoint, whose `type` parameter decides what a ref can name: only branches for `tiger branch` (`common.ResolveBranch`, called by `resolveBranch`), and anything they can connect to — a service, a read replica set, or a branch — for `tiger db` (`common.ResolveConnectionTarget`, called by `resolveConnectionTarget` in `db.go`). The service and branch ref helpers sit side by side in `ref_helper.go`.

- A command that changes the service it resolves uses `resolveServiceForWrite`, which carries the read-only gate; everything else uses `resolveService`.
- Help text shows `[name-or-id]` and says the service "can be given by ID or name". Shell completion offers names, falling back to IDs when what is typed matches an ID instead.
- A name is accepted only as an argument. A configured default must be an ID, and one that resolves by name is refused. The `--service-id` flag completion only offers IDs.
- Destructive commands accept a name, but their confirmation prompt takes only the ID.
- MCP tools stay IDs-only — an intentional divergence, documented at `setServiceIDSchemaProperties`.
- MCP tools stay IDs-only — an intentional divergence, documented at `setServiceIDSchemaProperties`. A tool that goes through the resolve endpoint (the branch tools, `db_query`, `db_schema`) refuses a match by name.

## Output

Expand All @@ -154,12 +154,13 @@ A command that identifies a service takes a **ref** — its ID, a read replica s
- **stdout** gets the command's primary output: the data payload (table, JSON, YAML, env vars), and in the plain-text path the result text itself.
- **stderr** gets everything else: errors and warnings; interactive UI (confirmation prompts, password prompts, spinners); and progress/status messages that accompany structured output, so a piped stdout stays clean. `tiger service create` is the model — every status line is `cmd.PrintErrf`, and only the final service payload goes to stdout.

Helper functions inside `internal/cmd` take the `*cobra.Command` and print through it. A helper whose whole job is rendering into a writer (the `output*Table` functions) takes an `io.Writer` instead, since `tablewriter` needs one anyway. Code in other packages (`internal/common`, `internal/version`) must not import cobra — it takes an `io.Writer` (e.g. `common.WaitForServiceArgs.Output`) and the caller in `internal/cmd` passes `cmd.ErrOrStderr()` or `cmd.OutOrStdout()`.
Helper functions inside `internal/cmd` take the `*cobra.Command` and print through it. A helper whose whole job is rendering into a writer (the `output*Table` functions) takes an `io.Writer` instead, since `tablewriter` needs one anyway — as its first parameter, matching `util.SerializeToJSON` and `util.SerializeToYAML`. Code in other packages (`internal/common`, `internal/version`) must not import cobra — it takes an `io.Writer` (e.g. `common.WaitForServiceArgs.Output`) and the caller in `internal/cmd` passes `cmd.ErrOrStderr()` or `cmd.OutOrStdout()`.

### Formatting

- Commands with structured output take `-o`/`--output` with `json`, `yaml`, and `table` (the default); some commands support extras (`env`, `bare`). Register the flag with the validating flag types in `flag_helper.go` (`new(outputFlag)` and friends, with no bound variable) and read the value from `cfg.Output` — `output` is a config value.
- Serialize with `util.SerializeToJSON` and `util.SerializeToYAML`. **Don't add `yaml:` struct tags to output types** — `SerializeToYAML` encodes to JSON first and converts, so only `json:` tags matter and the two formats stay consistent (including for generated types that only carry `json:` tags).
- **Structured output is unconditional.** When a command succeeds with a structured format (`json`, `yaml`, `env`), stdout carries a payload in that format and nothing else, whatever the result, so it can always be piped into `jq` and the like. An empty list is `[]`, never a message and never empty output. A message for an empty or fallback result ("No services found.") belongs only in the output function's table case, printed to stderr before the table function would run. `tiger service list` is the model.
- Colored output uses lipgloss styles, restricted to the eight base ANSI colors (`lipgloss.Blue`, `lipgloss.Green`, …) rather than their bright variants or 256-color and true-color values, and without bold; de-emphasize text with `Faint` rather than a grey. That way output follows the user's terminal theme (some themes render bright colors and bold as greys). Render styles unconditionally: the output writers strip colors when the stream isn't a terminal, and the lifecycle disables color on them when `cfg.Color` is off, so commands need no per-command wiring. The `tiger skills install` picker is the model.

### Tone
Expand Down Expand Up @@ -195,7 +196,7 @@ Interactive menus and spinners use BubbleTea v2 (`charm.land/bubbletea/v2`). Alw
- Cleanup that can fail (closing a reader, removing a temporary directory) is called explicitly on the success path, with its error returned, and also deferred as best-effort cleanup for the error paths, where a more important error is already being returned. This relies on the cleanup being safe to call twice, so use it only for idempotent operations. `writeSkill` in `skills_install.go` is the model.
- Return errors from `RunE`; never call `os.Exit` in a command. For specific exit codes, wrap with `common.ExitWithCode`, and map API errors to exit codes with `common.ExitWithErrorFromStatusCode`. The exit codes are defined in `internal/common/errors.go`, and `main.go` honors a `common.ExitCodeError` anywhere in the error chain.
- After checking an API response's status code, nil-check the parsed body (`resp.JSON200`, etc.) before using it — a nil body indicates a bug and should return `errors.New("empty response from API")`.
- Operations that connect to a service should call `common.CheckServiceReady` first and translate `common.ErrPaused`/`common.ErrNotReady` into actionable messages.
- Operations that connect to a database should check `ConnectionTarget.CheckReady` first and translate `common.ErrPaused`/`common.ErrNotReady` into actionable messages.

## HTTP Requests

Expand Down Expand Up @@ -260,12 +261,12 @@ The CLI can be used to get around the MCP server's safeguards: `tiger config set

## Read-Only Mode

`cfg.ReadOnly` is a `config.ReadOnlyMode` — `all`, `prod`, or `off` (`prod` protects only services tagged `PROD`). `config.Load` normalizes every value through `parseReadOnlyMode`, which also accepts the legacy boolean spellings, so nothing downstream sees an unnormalized value. Every write/destructive surface on both planes gates through one of the two checks in `internal/common/read_only.go` — on the CLI side that's the `RunE` of `service create`, `fork`, `start`, `stop`, `rename`, `resize`, `update-password`, `delete`, `backup region add`, `backup region remove`, and `db create role`; on the MCP side, the handlers of the write tools listed in `readOnlyGatedTools`:
`cfg.ReadOnly` is a `config.ReadOnlyMode` — `all`, `prod`, or `off` (`prod` protects only services tagged `PROD`). `config.Load` normalizes every value through `parseReadOnlyMode`, which also accepts the legacy boolean spellings, so nothing downstream sees an unnormalized value. Every write/destructive surface on both planes gates through one of the two checks in `internal/common/read_only.go` — on the CLI side that's the `RunE` of `service create`, `fork`, `start`, `stop`, `rename`, `resize`, `update-password`, `delete`, `backup region add`, `backup region remove`, `db create role`, and every `branch` command that writes (`create` — also reachable as `service branch` — `delete`, `start`, `stop`, `resize`, `rename`, `update-password`); on the MCP side, the handlers of the write tools listed in `readOnlyGatedTools`:

- `common.CheckReadOnly(cfg, tag)` — for a caller that has the target's environment tag: read off a service it already holds via `common.ServiceEnvironmentTag(service)` (what `resolveServiceForWrite` does on the CLI side), or the tag about to be requested (`--environment` on `service create`/`fork`, and the `environment` parameter of the matching MCP tools, both defaulting to `DEV`).
- `common.CheckReadOnlyByServiceID(ctx, cfg, client, projectID, serviceID)` — the same verdict from an ID, fetching the service to read its tag. The fetch happens only under `prod` (`all` refuses and `off` allows without one), and a failed fetch is a refusal. Only the MCP write tools that don't already hold the service need it.

Gotchas when adding a gated surface: a replica set is judged on its *own* tag, never its primary's; `prod` refuses *creating* a `PROD` service too (otherwise it would create services it then can't stop or delete), so `create`/`fork` gate on the tag they're about to request; the MCP server skips registering write tools only under `all` — under `prod` they stay registered and refuse per call, with a `prod` variant of the server instructions explaining that; and where the verdict is wanted as a boolean, write `CheckReadOnly(…) != nil` rather than a wrapper, so grepping `CheckReadOnly` finds every gate. `CheckReadOnly` requires a tag so it can't silently ignore `prod`; the one shortcut allowed is refusing the blanket case before a fetch the command was making anyway (`if cfg.ReadOnly.BlocksAll() { return common.ErrReadOnly }`), then still gating once the service arrives — on the CLI side `resolveServiceForWrite` does both, so a command gets that order by calling it.
Gotchas when adding a gated surface: a replica set is judged on its *own* tag, never its primary's; a branch has no tag and is always judged as DEV (`common.BranchEnvironmentTag`), never by its parent's, so `prod` leaves branches of a PROD service writable; `prod` refuses *creating* a `PROD` service too (otherwise it would create services it then can't stop or delete), so `create`/`fork` gate on the tag they're about to request; the MCP server skips registering write tools only under `all` — under `prod` they stay registered and refuse per call, with a `prod` variant of the server instructions explaining that; and where the verdict is wanted as a boolean, write `CheckReadOnly(…) != nil` rather than a wrapper, so grepping `CheckReadOnly` finds every gate. `CheckReadOnly` requires a tag so it can't silently ignore `prod`; the one shortcut allowed is refusing the blanket case before a fetch the command was making anyway (`if cfg.ReadOnly.BlocksAll() { return common.ErrReadOnly }`), then still gating once the service arrives — on the CLI side `resolveServiceForWrite` does both, so a command gets that order by calling it.

## CLI/MCP Synchronization

Expand Down
Loading
Loading