Status: accepted.
Restish can already be embedded in a branded Go binary. An embedder can set the
command name, install default config, register auth handlers, loaders,
formatters, content types, encodings, and link parsers, then call Run.
That is enough to build a custom binary, but not enough to make a polished
single-API CLI. A binary such as acme should feel like the Acme API CLI, not
like generic Restish with a different executable name:
acme list-users
acme get-user 123
acme auth header
acme cache clear
The current stock shape keeps generated API operations under the configured API name:
acme api list-users
or, if the API is also named acme:
acme acme list-users
That is a poor first impression for a shipped product CLI. At the same time, removing every Restish support command is too strict: auth inspection, cache cleanup, config display, shell completion, version output, and diagnostics are useful for scripting, CI, support tickets, and user recovery.
- Let an embedder promote one configured API's generated operations to the CLI root.
- Keep the promoted API's generated commands present in help and dispatch on first run by loading the configured spec source before commands are needed.
- Keep a small support surface that uses app vocabulary rather than leaking the Restish implementation name.
- Preserve scriptability for auth headers, cache cleanup, config inspection, diagnostics, completion, and version output.
- Keep the public API small enough to maintain permanently.
- Do not expose the Cobra root command as the primary embedding API.
- Do not add arbitrary custom Go command registration in the first pass.
- Do not add generated-operation invocation by operation ID from Go in the first pass.
- Do not add a public embedded OpenAPI bytes helper in the first pass.
- Do not add an immutable "never update OpenAPI from the network" embedded-spec mode in the first pass.
- Do not add arbitrary per-command-family visibility subsets until a real embedder needs them.
- Do not bundle out-of-process plugin executables into custom binaries.
The stock root command is built by the central CLI runtime. It owns global
flags, grouped help, completions, builtin command families, generated API
commands, plugin command discovery, request execution, auth, cache paths, and
diagnostics.
Generated OpenAPI commands are currently added as API command groups. The runtime loads generated metadata from cache when possible, rebuilds operation metadata from stale raw spec cache when needed, and can refresh stale remote metadata on generated command use. Local spec files are authoritative and invalidate stale metadata when changed.
Stock startup and help intentionally do not trigger remote spec discovery. They
load cached generated metadata, rebuild from raw cache, or reload local spec
files, but a missing remote cache leaves the generated command group absent
until the user runs api sync or invokes a generated-looking command under an
API short name. That is the right default for the generic restish binary, but
it is not enough for a branded promoted-root CLI: acme --help and
acme list-users cannot depend on the user knowing to run a Restish-flavored
sync command first.
The existing discovery layer already supports explicit spec_url, local
spec_files, and normal OpenAPI discovery from the API base URL. It writes the
raw spec cache and extracted operation metadata in the same cache entry. The
custom CLI work should reuse that machinery rather than add a new first-run
spec source before it is proven necessary.
The embedding API must not make embedders depend on internal packages or the
exact Cobra tree. Anything exported from the root restish package is a
long-term public promise.
Names are placeholders until the implementation design is accepted. The desired developer experience is:
package main
import (
"fmt"
"os"
restish "github.com/rest-sh/restish/v2"
)
func main() {
cli := restish.New()
cli.SetCommandName("acme")
cli.SetCommandDescription("Acme API CLI", "")
cli.SetDefaultConfig(&restish.Config{APIs: map[string]*restish.APIConfig{
"api": {
BaseURL: "https://api.acme.com",
SpecURL: "https://api.acme.com/openapi.yaml",
},
}})
cli.SetCommandSurface(restish.CommandSurface{
PromotedAPI: "api",
})
if err := cli.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}The command surface options are intentionally a small public struct:
type CommandSurface struct {
PromotedAPI string
SupportCommandNamespace string
HideSupportCommands bool
}The design intent is:
- The zero value preserves the stock Restish surface.
PromotedAPIpromotes generated operations from the configured API to the root command.- The promoted API is the primary API. There is no separate primary/default API concept in the first pass.
- Support commands stay at root by default when an API is promoted.
SupportCommandNamespacemoves support commands under that command name, for exampleacme cli cache clear.HideSupportCommandsremoves support commands from the custom CLI surface.SupportCommandNamespaceandHideSupportCommandsare mutually exclusive.- Support-command layout fields without
PromotedAPIare invalid in the first version. - Examples should configure
SpecURLfor predictable first-run behavior. A promoted API may also use localSpecFilesor the normal discovery flow fromBaseURL, but a custom CLI that wants reliable help should provide an explicit spec URL when possible. - There is no separate embedded spec API in the first version.
The default single-API surface is:
acme <generated-operation> [...]
acme auth
acme cache
acme config
acme doctor
acme completion
acme version
The default single-API surface hides stock Restish control-plane commands that would distract from a branded API CLI:
api
plugin
get|head|options|post|put|patch|delete
shell
links
cert
Embedders may choose a fuller surface, hide support commands, or move the support commands under an embedder-owned namespace:
acme cli cache clear
acme cli auth header
Examples should use cli as the generic namespace because it describes support
commands without exposing Restish as an implementation detail. Embedders may
choose names such as admin, support, or system.
Support commands at root must use the branded command name and product language in help text. They should not say "Restish" unless the text is explicitly about diagnostics for maintainers.
Root auth exists for scripting and troubleshooting auth for the promoted API.
It is a thin wrapper over existing api auth internals with the promoted API
name injected. It must not duplicate token resolution logic.
Supported root sugar:
acme auth get [credential-id]
acme auth get [credential-id] --operation <operation> [--print-header]
acme auth header [credential-id] [--operation <operation>]
acme auth inspect [--operation <operation>] [--credential <id>] [--redact]
acme auth logout [--all-profiles] [--auth-profile <name>]
auth header is the preferred scripting primitive. It prints exactly:
Name: value
and exits non-zero for query auth, cookie auth, missing auth, or multi-header
auth. Restish should not add auth token as the first root sugar because many
auth schemes are not bearer tokens.
Root auth add and auth remove are deferred. If a custom CLI needs
user-managed credential bindings before root sugar is expanded, the fuller
advanced auth tree can live under the support namespace.
Root cache reuses existing cache info and cache clear behavior. Do not
change cache clear semantics silently: without an argument, it clears all HTTP
cache entries.
In the default single-API surface, generic HTTP and direct URL commands are
hidden, so clearing all HTTP cache entries is acceptable. If a fuller surface
keeps generic HTTP commands, cache clear <api> and cache clear --direct
retain their existing meaning.
Root config reuses path, show, set, edit, and theme commands where
retained. Help text must be app-shaped, for example "Manage local Acme CLI
configuration" rather than "Manage local Restish configuration."
config show -o json remains redacted and scriptable.
Root doctor diagnoses runtime paths, config, cache, auth cache permissions,
and the promoted API's generated operation status.
doctor api with no API argument diagnoses the promoted API. Full
doctor api <name> and doctor plugin <name> belong in fuller surfaces or the
support namespace.
TTY diagnostics should use app vocabulary. JSON diagnostics may include enough Restish-specific detail for maintainers and support workflows.
completion and version remain available at root by default.
--version must keep working even when the version command is hidden or moved
under a namespace.
A custom single-API CLI should never show an empty operation list merely because the user has not run a sync command.
For a promoted API, Restish should build the command tree from the best available source:
- fresh generated operation metadata
- fresh raw spec cache that can rebuild operation metadata
- the configured spec source, fetched or reloaded through the existing discovery path and cached with extracted operations
- stale generated operation metadata or stale raw spec cache as last-known-good when refresh fails
- a clear diagnostic if no source can produce generated operations
The fetch-on-demand behavior is specific to promoted custom CLIs. The stock
restish command should keep its current no-network startup and generic help
behavior.
Top-level help, generated operation help, and generated command execution for a promoted API may trigger spec discovery when no fresh command metadata is available. If stale metadata exists, Restish should attempt a bounded refresh and fall back to the stale last-known-good tree when refresh fails. The initial timeout should reuse the generated metadata refresh timeout, currently three seconds, so promoted custom CLIs do not introduce a second freshness policy.
Completion should use the same metadata policy where practical, but shell completion must remain responsive. If completion cannot refresh within the bounded timeout, it should fall back to cached or stale command names rather than hanging the shell.
Unknown-command handling for promoted roots must account for newly added remote operations. If a token looks like a generated operation and the promoted API has a refreshable spec source, Restish should try a bounded sync before returning an unknown-command error.
doctor should report whether generated operations are fresh, stale, refreshed
on demand, refresh-failed with stale fallback, or unavailable.
Generated operations, app support commands, and the optional support namespace must not silently shadow one another.
If a promoted operation collides with a root support command such as auth,
cache, or doctor, startup should fail with an actionable error that suggests
moving support commands under a namespace or hiding them.
If the chosen support namespace collides with a promoted operation, startup should fail with the same kind of actionable error.
The stock Restish surface remains unchanged unless the embedder opts into a custom command surface.
The implementation should reuse the stock Restish command construction path instead of maintaining a second root builder for custom CLIs:
- Build the normal Restish root command first.
- Load generated commands through the existing generated-command path.
- For promoted APIs, ensure generated metadata is available before root help, generated operation help, completion, or command dispatch needs the promoted command tree.
- Reuse existing spec discovery and cache writes for configured
SpecURL,SpecFiles, and normal base-URL discovery. - Transform the command tree for the selected surface instead of maintaining a separate root builder.
- Skip adding the promoted API's normal wrapper/short-name command at root.
- Move, hide, or retain builtin support commands according to
SupportCommandNamespaceandHideSupportCommands. - Add only the minimal app-shaped auth sugar described above.
- Reuse cache, config, and doctor behavior where possible, adjusting help text and primary-API defaults rather than duplicating implementation.
This gives the ergonomic root promotion but removes useful operational tools such as auth inspection, cache cleanup, config display, completion, version, and diagnostics. It also pushes embedders toward rebuilding those tools in custom code.
This preserves current architecture but leaves custom CLIs with awkward command
shapes such as acme api list-users or acme acme list-users.
This gives embedders maximum flexibility, but it turns the internal command tree into public API and makes help, completion, grouping, generated commands, and compatibility harder to preserve.
Custom Go commands are useful, but they need their own design for flags, args, I/O, global flags, completion, command grouping, and collision behavior. They are future work, not required to make single-API promotion useful.
A public helper for go:embed OpenAPI bytes is useful for offline help,
pre-auth environments, and traditional CLIs that want command changes to follow
binary releases. It also adds public API, cache identity, freshness,
user-override, and diagnostics questions. Since a custom CLI that can reach the
API should usually be able to fetch that API's OpenAPI document, the first pass
should make configured spec fetch and cache behavior reliable before adding an
embedded-spec helper.
A "never update OpenAPI from the network" mode is useful for traditional release-driven CLIs, but it depends on the embedded-spec helper above and needs explicit decisions about user config overrides, sync behavior, and diagnostics.
The default restish command surface remains unchanged.
All new behavior is opt-in through the embedding API. A custom CLI that promotes an API opts into bounded spec fetches before help, completion, or generated command dispatch when cached metadata is missing or stale. Once exported, embedding types and methods must be treated as public and maintained additively.
The public API must not expose internal packages, cache paths, Cobra command objects, or implementation-specific command grouping as stable contracts.
OpenAPI documents may contain server URLs, examples, schemas, and operation metadata. They should not contain secrets. Diagnostics should avoid printing credential material or unredacted auth configuration.
Promoted-root help and completion can make network requests when metadata is missing or stale. That behavior must stay bounded and must use the existing discovery transport, auth, TLS, redaction, and cross-origin protections.
auth header prints credential material by design and must write only the
single header line to stdout on success. Errors and diagnostics go to stderr via
normal CLI error handling.
Config and doctor output must preserve existing redaction behavior.
Add focused tests for:
- promoted operation execution at root
- promoted operation help at root on first run from configured
SpecURL - promoted operation help when only stale cached metadata is available and refresh fails
- remote
spec_urlrefresh populating raw spec and generated operation caches - local
SpecFilesreload for promoted root help and execution - stale metadata fallback when refresh fails
- unknown promoted operation triggering bounded refresh before error
- collision with root support commands
- collision with support namespace
- support commands retained at root
- support commands moved under a namespace
- support commands hidden
auth get,auth header,auth inspect, andauth logoutfor the promoted APIauth headerfailures for query, cookie, missing, and multi-header auth- cache clear semantics unchanged
- branded config and doctor help text
- completion and
--versionbehavior - compile-time embedding example using the root
restishpackage
When implemented, update:
- root package Go docs for embedding
- a custom CLI embedding guide on the docs site
- command reference/help generated regions if public command shape changes
- this design record with the accepted public API names
- How should JSON doctor output represent generated operation metadata source, freshness, and the last refresh error?
- Should the first official custom CLI example live under
examples/, the docs site, or both?