feat: add coder exp sync list command (#26443)

Add a new subcommand to list all registered sync units and their current
statuses. This provides a quick overview of the dependency coordination
state in a workspace without needing to query each unit individually.

The command supports both table (default) and JSON output formats.

```
$ coder exp sync list
UNIT    STATUS     READY
unit-a  started    true
unit-b  completed  true
unit-c  pending    false

$ coder exp sync list --output json
[
  {
    "unit_name": "my-unit",
    "status": "started",
    "is_ready": true
  }
]
```

When no units are registered, the command prints `No units registered`.

<details><summary>Changes across layers</summary>

- `agent/unit`: add `Manager.ListUnits()` method
- `agent/agentsocket/proto`: add `SyncList` RPC, bump API to v1.2
- `agent/agentsocket`: add service and client implementations
- `cli`: add `sync_list.go` command, register in `sync.go`
- Tests: three golden-file test cases (empty list, multiple units, JSON)

</details>

> Generated by Coder Agents on behalf of @SasSwart

---------

Co-authored-by: Cian Johnston <cian@coder.com>
This commit is contained in:
Sas Swart
2026-06-17 13:01:57 +02:00
committed by GitHub
co-authored by Cian Johnston
parent 74a7ad0929
commit 7d95153bf4
18 changed files with 553 additions and 39 deletions
+4
View File
@@ -98,6 +98,10 @@ func TestCommandHelp(t *testing.T) {
Name: "coder exp sync status --help",
Cmd: []string{"exp", "sync", "status", "--help"},
},
clitest.CommandHelpCase{
Name: "coder exp sync list --help",
Cmd: []string{"exp", "sync", "list", "--help"},
},
))
}
+1
View File
@@ -20,6 +20,7 @@ func (r *RootCmd) syncCommand() *serpent.Command {
r.syncWant(&socketPath),
r.syncComplete(&socketPath),
r.syncStatus(&socketPath),
r.syncList(&socketPath),
},
Options: serpent.OptionSet{
{
+67
View File
@@ -0,0 +1,67 @@
package cli
import (
"fmt"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/agent/agentsocket"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/serpent"
)
func (*RootCmd) syncList(socketPath *string) *serpent.Command {
formatter := cliui.NewOutputFormatter(
cliui.TableFormat(
[]agentsocket.SyncListItem{},
[]string{
"unit",
"status",
"ready",
},
),
cliui.JSONFormat(),
)
cmd := &serpent.Command{
Use: "list",
Short: "List all registered units and their statuses",
Long: "List all units currently registered with the workspace agent. Shows each unit's name, status, and whether it is ready to start.",
Handler: func(i *serpent.Invocation) error {
ctx := i.Context()
opts := []agentsocket.Option{}
if *socketPath != "" {
opts = append(opts, agentsocket.WithPath(*socketPath))
}
client, err := agentsocket.NewClient(ctx, opts...)
if err != nil {
return xerrors.Errorf("connect to agent socket: %w", err)
}
defer client.Close()
items, err := client.SyncList(ctx)
if err != nil {
return xerrors.Errorf("list units failed: %w", err)
}
if len(items) == 0 && formatter.FormatID() == "table" {
cliui.Info(i.Stdout, "No units registered")
return nil
}
out, err := formatter.Format(ctx, items)
if err != nil {
return xerrors.Errorf("format output: %w", err)
}
_, _ = fmt.Fprintln(i.Stdout, out)
return nil
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
+1 -1
View File
@@ -36,7 +36,7 @@ func (*RootCmd) syncStatus(socketPath *string) *serpent.Command {
cmd := &serpent.Command{
Use: "status <unit>",
Short: "Show unit status and dependency state",
Long: "Show the current status of a unit, whether it is ready to start, and lists its dependencies. Shows which dependencies are satisfied and which are still pending. Supports multiple output formats.",
Long: "Show the current status of a unit, whether it is ready to start, and lists its dependencies. Shows which dependencies are satisfied and which are still pending.",
Handler: func(i *serpent.Invocation) error {
ctx := i.Context()
+77
View File
@@ -409,4 +409,81 @@ func TestSyncCommands_Golden(t *testing.T) {
clitest.TestGoldenFile(t, "TestSyncCommands_Golden/status_json_format", outBuf.Bytes(), nil)
})
t.Run("list_no_units", func(t *testing.T) {
t.Parallel()
path, cleanup := setupSocketServer(t)
defer cleanup()
ctx := testutil.Context(t, testutil.WaitShort)
var outBuf bytes.Buffer
inv, _ := clitest.New(t, "exp", "sync", "list", "--socket-path", path)
inv.Stdout = &outBuf
inv.Stderr = &outBuf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, "TestSyncCommands_Golden/list_no_units", outBuf.Bytes(), nil)
})
t.Run("list_with_units", func(t *testing.T) {
t.Parallel()
path, cleanup := setupSocketServer(t)
defer cleanup()
ctx := testutil.Context(t, testutil.WaitShort)
// Register some units in various states.
client, err := agentsocket.NewClient(ctx, agentsocket.WithPath(path))
require.NoError(t, err)
// unit-a: started
err = client.SyncStart(ctx, "unit-a")
require.NoError(t, err)
// unit-b: completed
err = client.SyncStart(ctx, "unit-b")
require.NoError(t, err)
err = client.SyncComplete(ctx, "unit-b")
require.NoError(t, err)
// unit-c: pending (has unsatisfied dependency on unit-a completing)
err = client.SyncWant(ctx, "unit-c", "unit-a")
require.NoError(t, err)
client.Close()
var outBuf bytes.Buffer
inv, _ := clitest.New(t, "exp", "sync", "list", "--socket-path", path)
inv.Stdout = &outBuf
inv.Stderr = &outBuf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, "TestSyncCommands_Golden/list_with_units", outBuf.Bytes(), nil)
})
t.Run("list_json_format", func(t *testing.T) {
t.Parallel()
path, cleanup := setupSocketServer(t)
defer cleanup()
ctx := testutil.Context(t, testutil.WaitShort)
// Register a unit.
client, err := agentsocket.NewClient(ctx, agentsocket.WithPath(path))
require.NoError(t, err)
err = client.SyncStart(ctx, "my-unit")
require.NoError(t, err)
client.Close()
var outBuf bytes.Buffer
inv, _ := clitest.New(t, "exp", "sync", "list", "--output", "json", "--socket-path", path)
inv.Stdout = &outBuf
inv.Stderr = &outBuf
err = inv.WithContext(ctx).Run()
require.NoError(t, err)
clitest.TestGoldenFile(t, "TestSyncCommands_Golden/list_json_format", outBuf.Bytes(), nil)
})
}
@@ -0,0 +1,7 @@
[
{
"unit_name": "my-unit",
"status": "started",
"is_ready": true
}
]
@@ -0,0 +1 @@
No units registered
@@ -0,0 +1,4 @@
UNIT STATUS READY
unit-a started true
unit-b completed true
unit-c pending false
+1
View File
@@ -13,6 +13,7 @@ USAGE:
SUBCOMMANDS:
complete Mark a unit as complete
list List all registered units and their statuses
ping Test agent socket connectivity and health
start Wait until all unit dependencies are satisfied
status Show unit status and dependency state
+19
View File
@@ -0,0 +1,19 @@
coder v0.0.0-devel
USAGE:
coder exp sync list [flags]
List all registered units and their statuses
List all units currently registered with the workspace agent. Shows each
unit's name, status, and whether it is ready to start.
OPTIONS:
-c, --column [unit|status|ready] (default: unit,status,ready)
Columns to display in table output.
-o, --output table|json (default: table)
Output format.
———
Run `coder --help` for a list of global options.
+1 -1
View File
@@ -7,7 +7,7 @@ USAGE:
Show the current status of a unit, whether it is ready to start, and lists its
dependencies. Shows which dependencies are satisfied and which are still
pending. Supports multiple output formats.
pending.
OPTIONS:
-c, --column [depends on|required status|current status|satisfied] (default: depends on,required status,current status,satisfied)