Add support for JSON output from `workspace list` (#198)
diff --git a/workspace_list.go b/workspace_list.go
new file mode 100644
index 0000000..282b739
--- /dev/null
+++ b/workspace_list.go
@@ -0,0 +1,41 @@
+// Copyright IBM Corp. 2019, 2026
+// SPDX-License-Identifier: MPL-2.0
+
+package tfjson
+
+// WorkspaceListOutput represents JSON output from terraform workspace list
+// (available from 1.16 onwards)
+type WorkspaceListOutput struct {
+ FormatVersion string `json:"format_version"`
+
+ Workspaces []WorkspaceListEntry `json:"workspaces"`
+ Diagnostics []Diagnostic `json:"diagnostics"`
+}
+
+// WorkspaceListEntry represents a single workspace entry in the list of workspaces
+type WorkspaceListEntry struct {
+ Name string `json:"name"`
+ IsCurrent bool `json:"is_current"`
+}
+
+// CurrentWorkspace will return details about the workspace that's currently selected.
+// The result may be nil if:
+// 1) the command output is empty, i.e. no workspaces exist yet.
+// 2) or, if the output doesn't specify a selected workspace.
+// Number 2 may happen if the default workspace is selected but not created yet.
+//
+// This method doesn't return any errors when nil is returned, as there are valid conditions where
+// nil is returned. Calling code should look for diagnostics in the receiver WorkspaceListOutput to
+// see if an explicit error has occurred or not.
+func (wlo *WorkspaceListOutput) CurrentWorkspace() *WorkspaceListEntry {
+ if wlo == nil {
+ return nil
+ }
+
+ for _, ws := range wlo.Workspaces {
+ if ws.IsCurrent {
+ return &ws
+ }
+ }
+ return nil
+}
diff --git a/workspace_list_test.go b/workspace_list_test.go
new file mode 100644
index 0000000..d470cd9
--- /dev/null
+++ b/workspace_list_test.go
@@ -0,0 +1,199 @@
+// Copyright IBM Corp. 2019, 2026
+// SPDX-License-Identifier: MPL-2.0
+
+package tfjson
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestWorkspaceListOutput_basic(t *testing.T) {
+ output := `{
+ "workspaces": [
+ {
+ "name": "default",
+ "is_current": true
+ },
+ {
+ "name": "other",
+ "is_current": false
+ }
+ ],
+ "diagnostics": []
+}`
+ var parsed WorkspaceListOutput
+ if err := json.Unmarshal([]byte(output), &parsed); err != nil {
+ t.Fatal(err)
+ }
+
+ expected := &WorkspaceListOutput{
+ Workspaces: []WorkspaceListEntry{
+ {
+ Name: "default",
+ IsCurrent: true,
+ },
+ {
+ Name: "other",
+ IsCurrent: false,
+ },
+ },
+ Diagnostics: []Diagnostic{},
+ }
+ if diff := cmp.Diff(expected, &parsed); diff != "" {
+ t.Fatalf("output mismatch: %s", diff)
+ }
+}
+
+func TestWorkspaceListOutput_warning(t *testing.T) {
+ output := `{
+ "workspaces": [
+ {
+ "name": "default",
+ "is_current": true
+ },
+ {
+ "name": "other",
+ "is_current": false
+ }
+ ],
+ "diagnostics": [
+ {
+ "severity": "warning",
+ "summary": "Warning: Consider yourself warned"
+ }
+ ]
+}`
+ var parsed WorkspaceListOutput
+ if err := json.Unmarshal([]byte(output), &parsed); err != nil {
+ t.Fatal(err)
+ }
+
+ expected := &WorkspaceListOutput{
+ Workspaces: []WorkspaceListEntry{
+ {
+ Name: "default",
+ IsCurrent: true,
+ },
+ {
+ Name: "other",
+ IsCurrent: false,
+ },
+ },
+ Diagnostics: []Diagnostic{
+ {
+ Severity: "warning",
+ Summary: "Warning: Consider yourself warned",
+ },
+ },
+ }
+ if diff := cmp.Diff(expected, &parsed); diff != "" {
+ t.Fatalf("output mismatch: %s", diff)
+ }
+}
+
+func TestWorkspaceListOutput_error(t *testing.T) {
+ output := `{
+ "workspaces": [],
+ "diagnostics": [
+ {
+ "severity": "error",
+ "summary": "Error: Something went wrong"
+ }
+ ]
+}`
+ var parsed WorkspaceListOutput
+ if err := json.Unmarshal([]byte(output), &parsed); err != nil {
+ t.Fatal(err)
+ }
+
+ expected := &WorkspaceListOutput{
+ Workspaces: []WorkspaceListEntry{},
+ Diagnostics: []Diagnostic{
+ {
+ Severity: "error",
+ Summary: "Error: Something went wrong",
+ },
+ },
+ }
+ if diff := cmp.Diff(expected, &parsed); diff != "" {
+ t.Fatalf("output mismatch: %s", diff)
+ }
+}
+
+func TestWorkspaceListOutput_CurrentWorkspace(t *testing.T) {
+ t.Run("returns current workspace", func(t *testing.T) {
+ wlo := &WorkspaceListOutput{
+ Workspaces: []WorkspaceListEntry{
+ {
+ Name: "default",
+ IsCurrent: true,
+ },
+ {
+ Name: "other",
+ IsCurrent: false,
+ },
+ },
+ Diagnostics: []Diagnostic{},
+ }
+
+ current := wlo.CurrentWorkspace()
+ if current == nil {
+ t.Fatal("unexpected nil result")
+ }
+ if current.Name != "default" {
+ t.Fatalf("wanted %q, got: %q", "default", current.Name)
+ }
+ })
+ t.Run("returns nil when there isn't a current workspace", func(t *testing.T) {
+ // This scenario could happen if custom workspaces exist and the
+ // default workspace is selected, but there hasn't been an apply
+ // yet so no default workspace actually exists yet...
+
+ wlo := &WorkspaceListOutput{
+ Workspaces: []WorkspaceListEntry{
+ {
+ Name: "default",
+ IsCurrent: false,
+ },
+ {
+ Name: "other",
+ IsCurrent: false,
+ },
+ },
+ Diagnostics: []Diagnostic{},
+ }
+
+ current := wlo.CurrentWorkspace()
+ if current != nil {
+ t.Fatalf("expected nil result, got: %#v", current)
+ }
+ })
+
+ t.Run("returns nil when there are no workspaces", func(t *testing.T) {
+ // This scenario could happen if there are no custom workspaces yet
+ // and an apply hasn't yet happened in the default workspace.
+ // E.g. user creates an empty Terraform project and immediately performs
+ // `terraform workspace list -json`
+
+ wlo := &WorkspaceListOutput{
+ Workspaces: []WorkspaceListEntry{},
+ Diagnostics: []Diagnostic{
+ {
+ // Mimicking `warnNoEnvsExistDiag` from the Core repo:
+ // https://github.com/hashicorp/terraform/blob/527402d3fe2de2363c4587e7abd1a3b23669ca25/internal/command/workspace_command.go#L158
+ Severity: "warning",
+ Summary: "Terraform cannot find any existing workspaces.",
+ Detail: "The \"default\" workspace is selected in your working directory. You can create this workspace by running \"terraform init\", by using the \"terraform workspace new\" subcommand or by including the \"-or-create\" flag with the \"terraform workspace select\" subcommand.",
+ },
+ },
+ }
+
+ current := wlo.CurrentWorkspace()
+ if current != nil {
+ t.Fatalf("expected nil result, got: %#v", current)
+ }
+ })
+}