diff --git a/components/osapi/README.md b/components/osapi/README.md index 87b3c91..6f957aa 100644 --- a/components/osapi/README.md +++ b/components/osapi/README.md @@ -49,6 +49,7 @@ A handler validates and delegates. It never touches the operating system. | [Building a domain](domains.md) | What a new endpoint touches, in what order, and what is forced by tooling | | [The embedded UI](ui.md) | The dashboard compiled into the binary, and what it does not verify | | [The Go SDK](sdk.md) | What a service owes, the five naming rules, and the seven methods that break them | +| [The CLI](cli.md) | A thin shell over the SDK: one command per endpoint, two inherited flags, and four shared renderers | | [Running commands](exec.md) | The five ways to run one, why ten minutes is a ceiling, and where a secret goes | | [Permissions](permissions.md) | The 37 permissions, the three roles, and why a direct permission overrides them | | [The audit trail](audit.md) | What is recorded, why redaction is a denylist, and what a read does not capture | diff --git a/components/osapi/cli.md b/components/osapi/cli.md new file mode 100644 index 0000000..b43addc --- /dev/null +++ b/components/osapi/cli.md @@ -0,0 +1,95 @@ +# The CLI + +`osapi client ...` is a thin shell over the SDK. It parses flags, calls one SDK +method, and renders the answer. No command talks to the controller directly, and +no command holds logic the SDK does not. + +That thinness is the point. A command that decided anything would be a second +place the decision lives, and the SDK is the one consumers other than the CLI +already use. + +## One command per endpoint + +A domain gets a parent command and one subcommand per endpoint, in files named +after the path they serve: `cmd/client_node_sysctl_get.go` serves +`GET /api/node/{hostname}/sysctl`. There were 156 such files in October 2026. + +The file layout is the routing table. A reader looking for what +`osapi client node sysctl get` does finds it by name rather than by searching. + +## Values arrive as flags, never as positional arguments + +An identifier is a named flag, marked required, not the first bare word after +the subcommand: + +``` +osapi client node sysctl get --key net.ipv4.ip_forward +``` + +Positional arguments read fine with one of them and stop reading at two, and a +flag can be made required by the parser rather than by a length check somebody +has to write. Required-ness is declared, so the error for omitting it is the +parser's and is the same everywhere. + +## Two flags every command inherits + +`--json`, `-j` is global, declared once on the root command. + +`--target`, `-T` is declared once on `client node` and defaults to `_all`. It +accepts a hostname, the reserved values `_any` and `_all`, or a label selector +such as `group:web.dev`. Nothing per-domain redeclares it, which is why its +meaning cannot drift between domains. + +## JSON returns before anything is formatted + +When `--json` is set, the command prints the response's raw JSON and returns. It +does not render a table and then serialize it. + +```go +if jsonOutput { + fmt.Println(string(resp.RawJSON())) + return +} +``` + +What a script sees is therefore what the API sent, not a reconstruction of it. A +field added to a response reaches `--json` consumers without any command +changing. + +## Rendering is four shared helpers, not per-command formatting + +They live in `internal/cli`: + +| Helper | For | +| ------------------- | ----------------------------------------------- | +| `PrintKV` | a single labelled value, such as the job ID | +| `PrintCompactTable` | rows, including the broadcast result table | +| `PrintErrors` | per-row errors beneath the rows | +| `PrintRawOutput` | output that is already text, such as a log tail | + +`BuildBroadcastTable` turns `[]ResultRow` into headers and rows, so a one-host +answer and a forty-host answer render through the same path. A domain that +formatted its own table would be the one that looks different. + +## Errors go through one handler + +`cli.HandleError` unwraps an `APIError`, logs the status code and message, and +exits non-zero. 120 of the commands call it, which is every command that can +fail. + +A command does not decide what an error means or print its own message. The +status code the API declared is the status code the user is told about. + +## Where this connects + +What the CLI calls, and the envelope every method returns, is [the SDK](sdk.md). + +What `_any`, `_all` and a label selector resolve to is +[the job system](job-system.md). + +What a contributor adds when a new domain needs commands, alongside the seven +other artifacts, is [building a domain](domains.md). + +______________________________________________________________________ + +Written from `cmd/client_*.go` and `internal/cli/`. diff --git a/components/osapi/domains.md b/components/osapi/domains.md index d32682e..3f3ab66 100644 --- a/components/osapi/domains.md +++ b/components/osapi/domains.md @@ -144,6 +144,27 @@ A new one has to be added in four places, and [permissions](permissions.md) says what each omission costs. **A permission that exists in the specification and in no role reaches nobody**, and nothing reports it. +## A domain is in every layer, or it is not done + +A domain is not one artifact. It is a provider, an agent processor and its +registration, a spec and the code generated from it, a handler and its route, an +SDK service, CLI commands, and the documentation pages and permission tables +that name it. + +Missing one of those is not a smaller domain. It is a domain that works until +somebody reaches it the way the missing layer would have been reached, and the +gap shows up as a bug rather than as an absence. + +The check is to pick a finished domain and search for its name across the +repository, then run the same search for the new one. The two lists should have +the same shape. A name that appears in eighty files and a name that appears in +sixty is the answer. + +```bash +grep -rl 'sysctl\|Sysctl' --include='*.go' --include='*.yaml' --include='*.md' . \ + | grep -vE '/gen/|/node_modules/|docs/docs/gen' +``` + ## Where this connects What the provider you are adding must implement, and the three boundary rules it