# Make control-plane create verbs human-by-default

`ab6c6e5`→[main](/content/gh/entireio/cli/commits/main/index.html)

peyton-alt·2w ago·8 files·+130 added/-33 removed

org/project/repo create and grant add were the only CLI verbs whose
default output was raw JSON. They now print '✓ Created …' confirmations
like the rest of the CLI; the full wire JSON stays available via --json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

## Sessions

990b88bcc14aView transcript

## Changes

8

- cmd/entire/cli

- Mcorecmd.go+15/-9

- Mcorecmd_list_test.go+9/-1

- Acorecmd_mutation_test.go+51

- Mgrant.go+25/-13

- Morg.go+7/-2

- Mproject.go+7/-3

- Mrepo.go+12/-4

- Mrepo_mirror.go+4/-1

```  
353 unmodified lines

```

// runCoreJSON runs fn against an authenticated control-plane client and
// prints its result as indented JSON. It owns the preamble every
// control-plane command shares: silence usage so input errors don't spam
// the usage block, build the client, and map an API error to a
// problem-detail SilentError. Commands supply only the call + the value to
// render.
func runCoreJSON(cmd *cobra.Command, fn func(ctx context.Context, c *coreapi.Client) (any, error)) error {
// runCoreMutation runs fn against the control plane and renders its outcome
// the way the rest of the CLI renders mutations: the ✓ human confirmation on
// stdout by default, or the wire object as JSON when --json was passed. fn
// returns both so the human line can name the created resource while --json
// preserves the full wire model (additive-only: synthesized fields like the
// repo remote URL are merged in, nothing is ever omitted). It owns the same
// preamble as the other runCore variants: silence usage, build the client,
// map API errors to problem-detail messages.
func runCoreMutation(cmd *cobra.Command, fn func(ctx context.Context, c *coreapi.Client) (message string, wire any, err error)) error {
	return runCore(cmd, func(ctx context.Context, c *coreapi.Client) error {
		out, err := fn(ctx, c)
		message, wire, err := fn(ctx, c)
		if err != nil {
			return err
		}
		return printJSON(cmd.OutOrStdout(), out)
		if jsonRequested(cmd) {
			return printJSON(cmd.OutOrStdout(), wire)
		}
		fmt.Fprintln(cmd.OutOrStdout(), message)
		return nil
	})
}
```

Mcmd/entire/cli/corecmd.go+15/-9

```
10 unmodified lines

```

// listCmd builds a minimal command wired to renderCoreList via runCoreList so
// runListRender builds a minimal command wired to renderCoreList via runCoreList so
// the rendering path (table / empty state / --json) is exercised without a
// server: fn returns items directly.
func runListRender(t *testing.T, jsonFlag bool, items []coreapi.Org) (stdout, stderr string) {
	// 38 unmodified lines

out, _ := runListRender(t, true, nil)
	require.JSONEq(t, "[]", out, "empty --json list must be [], not null")
}

// Not parallel: swaps the package-level activeCoreClient seam.
func TestRunCoreList_RendersRows(t *testing.T) {
	out, errOut := runListRender(t, false, []coreapi.Org{{ID: testDeleteULID, Name: "acme", Region: "us"}})
	require.Contains(t, out, "NAME")
	require.Contains(t, out, "acme")
	require.Empty(t, errOut)
}
```

Mcmd/entire/cli/corecmd_list_test.go+9/-1

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

package cli

import (
	"net/http"
	"net/http/httptest"
	"testing"

"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

"github.com/entireio/cli/internal/coreapi"
)

// newCreateOrgServer answers POST /api/v1/orgs with a created org. The 201
// status is load-bearing: the generated decodeCreateOrgResponse only accepts
// http.StatusCreated — a default 200 makes CreateOrg return an error.
func newCreateOrgServer(t *testing.T) *httptest.Server {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		assert.Equal(t, http.MethodPost, r.Method)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		if err := writeJSON(w, &coreapi.Org{ID: testDeleteULID, Name: "acme", Region: "us"}); err != nil {
			t.Errorf("encode org: %v", err)
		}
	}))
	t.Cleanup(srv.Close)
	return srv
}

// Not parallel: runDeleteCmd swaps the package-level activeCoreClient seam.
func TestOrgCreate_HumanByDefault(t *testing.T) {
	srv := newCreateOrgServer(t)
	out, errOut, err := runDeleteCmd(t, newOrgCreateCmd, srv.URL, "acme")
	require.NoError(t, err)
	require.Contains(t, out, "✓ Created org acme ("+testDeleteULID+")")
	require.NotContains(t, out, "{" , "default output must not be JSON")
	require.Empty(t, errOut)
}

// Not parallel: runDeleteCmd swaps the package-level activeCoreClient seam.
func TestOrgCreate_JSONOnRequest(t *testing.T) {
	srv := newCreateOrgServer(t)
	// org create's --json is persistent on the group root, so drive the
	// full group command with "create" as a subcommand arg.
	out, _, err := runDeleteCmd(t, newOrgCmd, srv.URL, "create", "acme", "--json")
	require.NoError(t, err)
	require.Contains(t, out, `"name": "acme"`)
	require.Contains(t, out, `"id": "+testDeleteULID+"`)
	require.NotContains(t, out, "✓ Created")
}
```

Acmd/entire/cli/corecmd_mutation_test.go+51/-13

```
116 unmodified lines

```

Example: "  entire grant org add acme github:alice --role admin",
	Args:    cobra.ExactArgs(2),
	RunE: func(cmd *cobra.Command, args []string) error {
		return runCoreJSON(cmd, func(ctx context.Context, c *coreapi.Client) (any, error) {
			return runCoreMutation(cmd, func(ctx context.Context, c *coreapi.Client) (string, any, error) {
				orgID, err := resolveOrgRef(ctx, c, args[0])
				if err != nil {
					return nil, err
				}
				provider, providerUserID, err := resolveGranteeProvider(ctx, c, args[1])
				if err != nil {
					return nil, err
				}
				body := &coreapi.AddOrgMemberInputBody{
					Provider:       provider,
				}

}
```

Mcmd/entire/cli/grant.go+25/-13

```
1 unmodified line

```

import (
	"context"
	"fmt"

"github.com/spf13/cobra"

"github.com/entireio/cli/internal/coreapi"
)

// Short: "Create an organization",
// Args:  cobra.ExactArgs(1),
// RunE: func(cmd *cobra.Command, args []string) error {
// return runCoreJSON(cmd, func(ctx context.Context, c *coreapi.Client) (any, error) {
// return runCoreMutation(cmd, func(ctx context.Context, c *coreapi.Client) (string, any, error) {
// }
```

Mcmd/entire/cli/org.go+7/-2

```
54 unmodified lines

```

if err != nil {
		return err
	}
	return runCoreJSON(cmd, func(ctx context.Context, c *coreapi.Client) (any, error) {
		return runCoreMutation(cmd, func(ctx context.Context, c *coreapi.Client) (string, any, error) {
			// Orgs are addressed by name, accounts by github:handle; both
			// also accept a raw ULID.
			var ownerRef string
```

Mcmd/entire/cli/project.go+7/-3

```
120 unmodified lines

```

// Short: "Create a repository in a project",
// Args:  cobra.ExactArgs(1),
// RunE: func(cmd *cobra.Command, args []string) error {
// return runCoreJSON(cmd, func(ctx context.Context, c *coreapi.Client) (any, error) {
// return runCoreMutation(cmd, func(ctx context.Context, c *coreapi.Client) (string, any, error) {
// }
```

Mcmd/entire/cli/repo.go+12/-4

```
290 unmodified lines

```

```
