mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Add "coder" CLI (#221)
* feat: Add "coder" CLI * Add CLI test for login * Add "bin/coder" target to Makefile * Update promptui to fix race * Fix error scope * Don't run CLI tests on Windows * Fix requested changes
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package clitest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coder/coder/cli"
|
||||
"github.com/coder/coder/cli/config"
|
||||
)
|
||||
|
||||
func New(t *testing.T, args ...string) (*cobra.Command, config.Root) {
|
||||
cmd := cli.Root()
|
||||
dir := t.TempDir()
|
||||
root := config.Root(dir)
|
||||
cmd.SetArgs(append([]string{"--global-config", dir}, args...))
|
||||
return cmd, root
|
||||
}
|
||||
|
||||
func StdoutLogs(t *testing.T) io.Writer {
|
||||
reader, writer := io.Pipe()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
t.Cleanup(func() {
|
||||
_ = reader.Close()
|
||||
_ = writer.Close()
|
||||
})
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
if scanner.Err() != nil {
|
||||
return
|
||||
}
|
||||
t.Log(scanner.Text())
|
||||
}
|
||||
}()
|
||||
return writer
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Root represents the configuration directory.
|
||||
type Root string
|
||||
|
||||
func (r Root) Session() File {
|
||||
return File(filepath.Join(string(r), "session"))
|
||||
}
|
||||
|
||||
func (r Root) URL() File {
|
||||
return File(filepath.Join(string(r), "url"))
|
||||
}
|
||||
|
||||
func (r Root) Organization() File {
|
||||
return File(filepath.Join(string(r), "organization"))
|
||||
}
|
||||
|
||||
// File provides convenience methods for interacting with *os.File.
|
||||
type File string
|
||||
|
||||
// Delete deletes the file.
|
||||
func (f File) Delete() error {
|
||||
return os.Remove(string(f))
|
||||
}
|
||||
|
||||
// Write writes the string to the file.
|
||||
func (f File) Write(s string) error {
|
||||
return write(string(f), 0600, []byte(s))
|
||||
}
|
||||
|
||||
// Read reads the file to a string.
|
||||
func (f File) Read() (string, error) {
|
||||
byt, err := read(string(f))
|
||||
return string(byt), err
|
||||
}
|
||||
|
||||
// open opens a file in the configuration directory,
|
||||
// creating all intermediate directories.
|
||||
func open(path string, flag int, mode os.FileMode) (*os.File, error) {
|
||||
err := os.MkdirAll(filepath.Dir(path), 0750)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.OpenFile(path, flag, mode)
|
||||
}
|
||||
|
||||
func write(path string, mode os.FileMode, dat []byte) error {
|
||||
fi, err := open(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fi.Close()
|
||||
_, err = fi.Write(dat)
|
||||
return err
|
||||
}
|
||||
|
||||
func read(path string) ([]byte, error) {
|
||||
fi, err := open(path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fi.Close()
|
||||
return ioutil.ReadAll(fi)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/config"
|
||||
)
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Write", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := config.Root(t.TempDir()).Session().Write("test")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
root := config.Root(t.TempDir())
|
||||
err := root.Session().Write("test")
|
||||
require.NoError(t, err)
|
||||
data, err := root.Session().Read()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", data)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
root := config.Root(t.TempDir())
|
||||
err := root.Session().Write("test")
|
||||
require.NoError(t, err)
|
||||
err = root.Session().Delete()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func login() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "login <url>",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
rawURL := args[0]
|
||||
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(rawURL, "localhost") {
|
||||
scheme = "http"
|
||||
}
|
||||
rawURL = fmt.Sprintf("%s://%s", scheme, rawURL)
|
||||
}
|
||||
serverURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse raw url %q: %w", rawURL, err)
|
||||
}
|
||||
// Default to HTTPs. Enables simple URLs like: master.cdr.dev
|
||||
if serverURL.Scheme == "" {
|
||||
serverURL.Scheme = "https"
|
||||
}
|
||||
|
||||
client := codersdk.New(serverURL)
|
||||
hasInitialUser, err := client.HasInitialUser(cmd.Context())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("has initial user: %w", err)
|
||||
}
|
||||
if !hasInitialUser {
|
||||
if !isTTY(cmd.InOrStdin()) {
|
||||
return xerrors.New("the initial user cannot be created in non-interactive mode. use the API")
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Your Coder deployment hasn't been set up!\n", color.HiBlackString(">"))
|
||||
|
||||
_, err := runPrompt(cmd, &promptui.Prompt{
|
||||
Label: "Would you like to create the first user?",
|
||||
IsConfirm: true,
|
||||
Default: "y",
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create user prompt: %w", err)
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get current user: %w", err)
|
||||
}
|
||||
username, err := runPrompt(cmd, &promptui.Prompt{
|
||||
Label: "What username would you like?",
|
||||
Default: currentUser.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("pick username prompt: %w", err)
|
||||
}
|
||||
|
||||
organization, err := runPrompt(cmd, &promptui.Prompt{
|
||||
Label: "What is the name of your organization?",
|
||||
Default: "acme-corp",
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("pick organization prompt: %w", err)
|
||||
}
|
||||
|
||||
email, err := runPrompt(cmd, &promptui.Prompt{
|
||||
Label: "What's your email?",
|
||||
Validate: func(s string) error {
|
||||
err := validator.New().Var(s, "email")
|
||||
if err != nil {
|
||||
return xerrors.New("That's not a valid email address!")
|
||||
}
|
||||
return err
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("specify email prompt: %w", err)
|
||||
}
|
||||
|
||||
password, err := runPrompt(cmd, &promptui.Prompt{
|
||||
Label: "Enter a password:",
|
||||
Mask: '*',
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("specify password prompt: %w", err)
|
||||
}
|
||||
|
||||
_, err = client.CreateInitialUser(cmd.Context(), coderd.CreateInitialUserRequest{
|
||||
Email: email,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Organization: organization,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create initial user: %w", err)
|
||||
}
|
||||
resp, err := client.LoginWithPassword(cmd.Context(), coderd.LoginWithPasswordRequest{
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("login with password: %w", err)
|
||||
}
|
||||
config := createConfig(cmd)
|
||||
err = config.Session().Write(resp.SessionToken)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("write session token: %w", err)
|
||||
}
|
||||
err = config.URL().Write(serverURL.String())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("write server url: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s Welcome to Coder, %s! You're authenticated.\n", color.HiBlackString(">"), color.HiCyanString(username))
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//go:build !windows
|
||||
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Netflix/go-expect"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("InitialUserNoTTY", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t)
|
||||
root, _ := clitest.New(t, "login", client.URL.String())
|
||||
err := root.Execute()
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("InitialUserTTY", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
console, err := expect.NewConsole(expect.WithStdout(clitest.StdoutLogs(t)))
|
||||
require.NoError(t, err)
|
||||
client := coderdtest.New(t)
|
||||
root, _ := clitest.New(t, "login", client.URL.String())
|
||||
root.SetIn(console.Tty())
|
||||
root.SetOut(console.Tty())
|
||||
go func() {
|
||||
err := root.Execute()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
matches := []string{
|
||||
"first user?", "y",
|
||||
"username", "testuser",
|
||||
"organization", "testorg",
|
||||
"email", "user@coder.com",
|
||||
"password", "password",
|
||||
}
|
||||
for i := 0; i < len(matches); i += 2 {
|
||||
match := matches[i]
|
||||
value := matches[i+1]
|
||||
_, err = console.ExpectString(match)
|
||||
require.NoError(t, err)
|
||||
_, err = console.SendLine(value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err = console.ExpectString("Welcome to Coder")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/fatih/color"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/database"
|
||||
)
|
||||
|
||||
func projectCreate() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a project from the current directory",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := createClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
organization, err := currentOrganization(cmd, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = runPrompt(cmd, &promptui.Prompt{
|
||||
Default: "y",
|
||||
IsConfirm: true,
|
||||
Label: fmt.Sprintf("Set up %s in your organization?", color.New(color.FgHiCyan).Sprintf("%q", workingDir)),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name, err := runPrompt(cmd, &promptui.Prompt{
|
||||
Default: filepath.Base(workingDir),
|
||||
Label: "What's your project's name?",
|
||||
Validate: func(s string) error {
|
||||
_, err = client.Project(cmd.Context(), organization.Name, s)
|
||||
if err == nil {
|
||||
return xerrors.New("A project already exists with that name!")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spin := spinner.New(spinner.CharSets[0], 50*time.Millisecond)
|
||||
spin.Suffix = " Uploading current directory..."
|
||||
spin.Start()
|
||||
defer spin.Stop()
|
||||
|
||||
bytes, err := tarDirectory(workingDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.UploadFile(cmd.Context(), codersdk.ContentTypeTar, bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job, err := client.CreateProjectVersionImportProvisionerJob(cmd.Context(), organization.Name, coderd.CreateProjectImportJobRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: resp.Hash,
|
||||
Provisioner: database.ProvisionerTypeTerraform,
|
||||
// SkipResources on first import to detect variables defined by the project.
|
||||
SkipResources: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spin.Stop()
|
||||
|
||||
logs, err := client.FollowProvisionerJobLogsAfter(context.Background(), organization.Name, job.ID, time.Time{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
log, ok := <-logs
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s %s\n", color.HiGreenString("[parse]"), log.Output)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Create project %q!\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func tarDirectory(directory string) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&buffer)
|
||||
err := filepath.Walk(directory, func(file string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := tar.FileInfoHeader(fileInfo, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(directory, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = rel
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(tarWriter, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return data.Close()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tarWriter.Flush()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func projectPlan() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "plan <directory>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Plan a project update from the current directory",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func projects() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "projects",
|
||||
Long: "Testing something",
|
||||
Example: `
|
||||
- Create a project for developers to create workspaces
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder projects create") + `
|
||||
|
||||
- Make changes to your project, and plan the changes
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder projects plan <name>") + `
|
||||
|
||||
- Update the project. Your developers can update their workspaces
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder projects update <name>"),
|
||||
}
|
||||
cmd.AddCommand(projectCreate())
|
||||
cmd.AddCommand(projectPlan())
|
||||
cmd.AddCommand(projectUpdate())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func projectUpdate() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "update <name>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Update a project from the current directory",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/kirsle/configdir"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/cli/config"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
varGlobalConfig = "global-config"
|
||||
)
|
||||
|
||||
func Root() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "coder",
|
||||
Long: ` ▄█▀ ▀█▄
|
||||
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
|
||||
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
|
||||
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
|
||||
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
|
||||
` + color.New(color.Underline).Sprint("Self-hosted developer workspaces on your infra") + `
|
||||
|
||||
`,
|
||||
Example: `
|
||||
- Create a project for developers to create workspaces
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder projects create <directory>") + `
|
||||
|
||||
- Create a workspace for a specific project
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder workspaces create <project>") + `
|
||||
|
||||
- Maintain consistency by updating a workspace
|
||||
|
||||
` + color.New(color.FgHiMagenta).Sprint("$ coder workspaces update <workspace>"),
|
||||
}
|
||||
// Customizes the color of headings to make subcommands
|
||||
// more visually appealing.
|
||||
header := color.New(color.FgHiBlack)
|
||||
cmd.SetUsageTemplate(strings.NewReplacer(
|
||||
`Usage:`, header.Sprint("Usage:"),
|
||||
`Examples:`, header.Sprint("Examples:"),
|
||||
`Available Commands:`, header.Sprint("Commands:"),
|
||||
`Global Flags:`, header.Sprint("Global Flags:"),
|
||||
`Flags:`, header.Sprint("Flags:"),
|
||||
`Additional help topics:`, header.Sprint("Additional help:"),
|
||||
).Replace(cmd.UsageTemplate()))
|
||||
|
||||
cmd.AddCommand(login())
|
||||
cmd.AddCommand(projects())
|
||||
cmd.AddCommand(workspaces())
|
||||
cmd.AddCommand(users())
|
||||
|
||||
cmd.PersistentFlags().String(varGlobalConfig, configdir.LocalConfig("coder"), "Path to the global `coder` config directory")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
|
||||
root := createConfig(cmd)
|
||||
rawURL, err := root.URL().Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := root.Session().Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := codersdk.New(serverURL)
|
||||
return client, client.SetSessionToken(token)
|
||||
}
|
||||
|
||||
func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (coderd.Organization, error) {
|
||||
orgs, err := client.UserOrganizations(cmd.Context(), "me")
|
||||
if err != nil {
|
||||
return coderd.Organization{}, nil
|
||||
}
|
||||
// For now, we won't use the config to set this.
|
||||
// Eventually, we will support changing using "coder switch <org>"
|
||||
return orgs[0], nil
|
||||
}
|
||||
|
||||
func createConfig(cmd *cobra.Command) config.Root {
|
||||
globalRoot, err := cmd.Flags().GetString(varGlobalConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return config.Root(globalRoot)
|
||||
}
|
||||
|
||||
// isTTY returns whether the passed reader is a TTY or not.
|
||||
// This accepts a reader to work with Cobra's "InOrStdin"
|
||||
// function for simple testing.
|
||||
func isTTY(reader io.Reader) bool {
|
||||
file, ok := reader.(*os.File)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return isatty.IsTerminal(file.Fd())
|
||||
}
|
||||
|
||||
func runPrompt(cmd *cobra.Command, prompt *promptui.Prompt) (string, error) {
|
||||
var ok bool
|
||||
prompt.Stdin, ok = cmd.InOrStdin().(io.ReadCloser)
|
||||
if !ok {
|
||||
return "", xerrors.New("stdin must be a readcloser")
|
||||
}
|
||||
prompt.Stdout, ok = cmd.OutOrStdout().(io.WriteCloser)
|
||||
if !ok {
|
||||
return "", xerrors.New("stdout must be a readcloser")
|
||||
}
|
||||
|
||||
// The prompt library displays defaults in a jarring way for the user
|
||||
// by attempting to autocomplete it. This sets no default enabling us
|
||||
// to customize the display.
|
||||
defaultValue := prompt.Default
|
||||
if !prompt.IsConfirm {
|
||||
prompt.Default = ""
|
||||
}
|
||||
|
||||
// Rewrite the confirm template to remove bold, and fit to the Coder style.
|
||||
confirmEnd := fmt.Sprintf("[y/%s] ", color.New(color.Bold).Sprint("N"))
|
||||
if prompt.Default == "y" {
|
||||
confirmEnd = fmt.Sprintf("[%s/n] ", color.New(color.Bold).Sprint("Y"))
|
||||
}
|
||||
confirm := color.HiBlackString("?") + ` {{ . }} ` + confirmEnd
|
||||
|
||||
// Customize to remove bold.
|
||||
valid := color.HiBlackString("?") + " {{ . }} "
|
||||
if defaultValue != "" {
|
||||
valid += fmt.Sprintf("(%s) ", defaultValue)
|
||||
}
|
||||
|
||||
success := valid
|
||||
invalid := valid
|
||||
if prompt.IsConfirm {
|
||||
success = confirm
|
||||
invalid = confirm
|
||||
}
|
||||
|
||||
prompt.Templates = &promptui.PromptTemplates{
|
||||
Confirm: confirm,
|
||||
Success: success,
|
||||
Invalid: invalid,
|
||||
Valid: valid,
|
||||
}
|
||||
value, err := prompt.Run()
|
||||
if value == "" && !prompt.IsConfirm {
|
||||
value = defaultValue
|
||||
}
|
||||
|
||||
return value, err
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package cli
|
||||
@@ -0,0 +1,10 @@
|
||||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func users() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "users",
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cli
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func workspaces() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "workspaces",
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
Reference in New Issue
Block a user