Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

All notable changes to the FlatRun CLI are documented in this file.

## [0.4.0] - 2026-08-23

### Added

- Per-operation help with request fields, types, accepted values, permissions, query parameters, and plan support
- Request body skeleton generation for operations described by the connected agent
- Plan mode for every operation that declares support in the agent API
- Agent and CLI version reporting when a server profile is saved
- Profile commands with explicit add, select, list, current, and remove operations
- Login through username/password or API key without placing credentials in shell history
- Terminal-aware colors, responsive tables, semantic statuses, and shell completion

### Fixed

- Image updates change the referenced environment variable instead of replacing a compose expression
- Generated API flags remain available for commands that also have a task-specific CLI form

## [0.3.1] - 2026-08-21

### Added
Expand Down
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,34 @@ export FLATRUN_TOKEN=fr_xxx
Or configure a local profile:

```bash
flatrun configure set --url https://panel.example.com --token fr_xxx
flatrun configure list
flatrun profile add production --url https://panel.example.com --token fr_xxx
flatrun profile use production
flatrun profile list
```

Log in through the agent without placing a password in shell history:

```bash
printf '%s\n' "$FLATRUN_PASSWORD" |
flatrun auth login --profile production --username operator --password-stdin
```

`configure` remains available as a compatibility alias for profile management.
Config is stored at `~/.flatrun/config.json` by default. Use `FLATRUN_CONFIG` to override the path.

## Terminal output

Human-readable output uses terminal-aware colors and tables. Colors are disabled automatically when output is piped or redirected. Set `NO_COLOR=1` to disable them explicitly. `--json` always returns unstyled machine-readable output.

Generate completion for a supported shell:

```bash
flatrun completion bash
flatrun completion zsh
flatrun completion fish
flatrun completion powershell
```

## Commands

Deploy an existing app from CI by pulling images and then applying a runtime operation:
Expand Down Expand Up @@ -174,4 +196,21 @@ Check API connectivity:
flatrun health
```

Learn any operation from the connected agent, then print a request body to fill in:

```bash
flatrun deployments create --help
flatrun deployments create --generate-cli-skeleton
```

Operations that declare plan support accept `--plan`. The command returns the changes without
applying them:

```bash
flatrun deployments delete staging --plan \
-q delete_ssl=true \
-q delete_database=true \
-q delete_vhost=true
```

See [docs](docs/README.md) for guides and command reference.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.1
0.4.0
29 changes: 27 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
module github.com/flatrun/cli

go 1.21
go 1.25.0

require gopkg.in/yaml.v3 v3.0.1
require (
charm.land/lipgloss/v2 v2.0.5
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
49 changes: 49 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
47 changes: 44 additions & 3 deletions internal/command/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"time"

"github.com/flatrun/cli/internal/flatrun"
"github.com/flatrun/cli/internal/presentation"
"github.com/flatrun/cli/internal/spec"
)

Expand Down Expand Up @@ -159,19 +160,23 @@ func runEndpoint(family string, args []string, stdout, stderr io.Writer) int {
fields := fieldValues{}
query := queryValues{}
dataArg := ""
generateSkeleton := false
planOnly := false
var api *spec.Spec
var operation spec.Operation
described := false

cmd := clientCommand{
name: family + " " + e.op,
usage: "Usage: " + e.command() + " [-f name=value] [--data JSON] [-q name=value]",
usage: "Usage: " + e.command() + " [-f name=value] [--data JSON] [-q name=value] [--plan] [--generate-cli-skeleton]",
positionals: len(e.args),
valueFlags: []string{"data", "f", "q"},
flags: func(fs *flag.FlagSet) {
fs.StringVar(&dataArg, "data", "", "JSON request body, or @file to read one")
fs.Var(fields, "f", "Request body field as name=value, repeatable")
fs.Var(query, "q", "Query parameter as name=value, repeatable")
fs.BoolVar(&planOnly, "plan", false, "Preview the operation without applying it")
fs.BoolVar(&generateSkeleton, "generate-cli-skeleton", false, "Print a request body template")
},
run: func(ctx context.Context, client *flatrun.Client, positional []string) ([]byte, error) {
path, err := e.resolvePath(positional)
Expand All @@ -191,6 +196,18 @@ func runEndpoint(family string, args []string, stdout, stderr io.Writer) int {
}
}
}
if generateSkeleton {
if !described {
return nil, fmt.Errorf("this agent does not describe the request body")
}
return json.MarshalIndent(api.Skeleton(operation), "", " ")
}
if planOnly {
if !described || !operation.PlanSupported {
return nil, fmt.Errorf("this operation does not support plan mode")
}
url.Values(query).Set("plan", "true")
}

if len(query) > 0 {
path += "?" + url.Values(query).Encode()
Expand All @@ -207,6 +224,10 @@ func runEndpoint(family string, args []string, stdout, stderr io.Writer) int {
return client.Do(ctx, e.method, path, payload)
},
render: func(w io.Writer, data []byte) error {
if generateSkeleton {
printResponse(w, true, data, "")
return nil
}
if renderAnswer(w, api, operation, data) {
return nil
}
Expand All @@ -217,6 +238,15 @@ func runEndpoint(family string, args []string, stdout, stderr io.Writer) int {
return runClientCommand(cmd, args[1:], stdout, stderr)
}

func usesGenericEndpointFlags(args []string) bool {
for _, arg := range args {
if arg == "--data" || strings.HasPrefix(arg, "--data=") || arg == "-f" || strings.HasPrefix(arg, "-f=") || arg == "-q" || strings.HasPrefix(arg, "-q=") || arg == "--plan" || arg == "--generate-cli-skeleton" {
return true
}
}
return false
}

// runAliasedEndpoint reaches a plural family's endpoint from its singular name, so the two are
// not different surfaces.
func runAliasedEndpoint(plural, singular string, args []string, stdout, stderr io.Writer) int {
Expand Down Expand Up @@ -339,17 +369,28 @@ func listEndpoints(stdout, stderr io.Writer, family string, asJSON bool) int {
return 0
}

view := presentation.New(stdout)
current := ""
rows := [][]string{}
flush := func() {
if current == "" {
return
}
_, _ = fmt.Fprintln(stdout, view.Heading(current))
view.Table(stdout, []string{"OPERATION", "METHOD", "PATH"}, rows)
}
for _, e := range list {
if e.family != current {
flush()
if current != "" {
_, _ = fmt.Fprintln(stdout)
}
current = e.family
_, _ = fmt.Fprintln(stdout, e.family)
rows = nil
}
_, _ = fmt.Fprintf(stdout, " %-38s %s %s\n", strings.TrimSpace(e.op+" "+argNames(e)+" "+e.flags), e.method, e.path)
rows = append(rows, []string{strings.TrimSpace(e.op + " " + argNames(e) + " " + e.flags), e.method, e.path})
}
flush()
_, _ = fmt.Fprintln(stdout)
_, _ = fmt.Fprintln(stdout, "Send a body with -f name=value (repeatable) or --data JSON.")
return 0
Expand Down
Loading
Loading