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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ All notable changes to the FlatRun CLI are documented in this file.

- Commands read the agent's own description of its API where the agent serves one, so a mistyped field or query parameter fails before the request with the name it was probably meant to be, `COMMAND --help` lists the fields an endpoint takes and the permission it needs, and answers print as tables laid out from the types the agent returns. An agent that does not describe itself behaves as before.

- An answer nothing has described is still laid out: the field holding the rows becomes a table, an array of plain values prints one per line, and an empty one reads `None`. `--json` prints the raw answer, as before.

### Fixed

- `-url`, `-token` and other single-dash flags swallowed the following argument, because only the double-dash spelling was registered as taking a value.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ sends a number.

If the agent describes the answer, that decides the layout. Otherwise an array of objects prints as
a table, an array of scalars one per line, an empty one as `None`, and anything else as raw JSON.
`--json` overrides all of it.
Piping changes nothing, so `flatrun deployment list | grep running` works. `--json` is how you ask
for the raw answer.

### Driving it from a script or an agent

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ maps the JSON itself:
| Empty array | `None` |
| No array, or more than one | The raw JSON |

`--json` overrides all of it and prints the answer untouched.
This does not change with where the output goes: a pipe or a CI log gets what the terminal gets, so
`flatrun deployment list | grep running` keeps working. `--json` is how you ask for the raw answer.

## Listing what exists

Expand Down
50 changes: 50 additions & 0 deletions internal/command/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,53 @@ func TestUnknownResourceIsStillUnknown(t *testing.T) {
t.Fatalf("stderr = %s", stderr)
}
}

// Whatever a command printed before, it prints now: a pipe is not a reason to change the answer,
// and `deployment list | grep running` has to keep working.
func TestOutputDoesNotChangeWhenPiped(t *testing.T) {
server, _ := recordingServer(t, `{"containers":[{"id":"c-1","name":"web","state":"running"}]}`)
code, stdout, stderr := runCLI(t, server, "container", "list")
if code != 0 {
t.Fatalf("code=%d stderr=%s", code, stderr)
}
if !strings.Contains(stdout, "CONTAINER ID") {
t.Fatalf("expected the table, got:\n%s", stdout)
}
if !strings.Contains(stdout, "web") {
t.Fatalf("expected the row, got:\n%s", stdout)
}
}

func TestJSONIsHowYouAskForTheAnswer(t *testing.T) {
server, _ := recordingServer(t, `{"containers":[{"id":"c-1","name":"web","state":"running"}]}`)
code, stdout, stderr := runCLI(t, server, "container", "list", "--json")
if code != 0 {
t.Fatalf("code=%d stderr=%s", code, stderr)
}
var decoded map[string]any
if err := json.Unmarshal([]byte(stdout), &decoded); err != nil {
t.Fatalf("--json should print the raw answer, got:\n%s", stdout)
}
}

// The exact payload a new agent sends for a deployment list: the shared shape, the name it used
// to answer under, and the field it answered alongside. A CLI built before any of that has to
// keep reading it.
const newAgentDeploymentList = `{"deployments":[{"name":"trakli-staging","path":"/opt/flatrun/deployments/trakli-staging","status":"running","created_at":"2026-08-13T22:39:28Z","updated_at":"0001-01-01T00:00:00Z"}],"items":[{"name":"trakli-staging","path":"/opt/flatrun/deployments/trakli-staging","status":"running","created_at":"2026-08-13T22:39:28Z","updated_at":"0001-01-01T00:00:00Z"}],"path":"/opt/flatrun/deployments","total":1}`

func TestOldCommandsReadANewAgentsAnswer(t *testing.T) {
for _, command := range [][]string{{"deployment", "list"}, {"deployments", "list"}} {
server, _ := recordingServer(t, newAgentDeploymentList)
code, stdout, stderr := runCLI(t, server, command...)
if code != 0 {
t.Fatalf("%v: code=%d stderr=%s", command, code, stderr)
}
if !strings.Contains(stdout, "NAME") || !strings.Contains(stdout, "trakli-staging") {
t.Fatalf("%v: expected the table, got:\n%s", command, stdout)
}
// The rows must come from one of the two names, not from both.
if strings.Count(stdout, "trakli-staging") != 1 {
t.Fatalf("%v: the deployment was listed twice:\n%s", command, stdout)
}
}
}
Loading