diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab2548..e0978bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index d8c419f..c7eb8a7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 5466d98..b86b9b4 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -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 diff --git a/internal/command/endpoints_test.go b/internal/command/endpoints_test.go index 1bc9f0b..a41cce3 100644 --- a/internal/command/endpoints_test.go +++ b/internal/command/endpoints_test.go @@ -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) + } + } +}