Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions cmd/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"

"github.com/kernel/cli/pkg/interactive"
"github.com/kernel/cli/pkg/util"
"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
Expand All @@ -21,7 +22,8 @@ type APIKeysService interface {
}

type APIKeysCmd struct {
apiKeys APIKeysService
apiKeys APIKeysService
prompter interactive.Prompter
}

type APIKeysCreateInput struct {
Expand Down Expand Up @@ -181,9 +183,13 @@ func (c APIKeysCmd) Update(ctx context.Context, in APIKeysUpdateInput) error {

func (c APIKeysCmd) Delete(ctx context.Context, in APIKeysDeleteInput) error {
if !in.SkipConfirm {
msg := fmt.Sprintf("Are you sure you want to delete API key '%s'?", in.ID)
pterm.DefaultInteractiveConfirm.DefaultText = msg
ok, _ := pterm.DefaultInteractiveConfirm.Show()
ok, err := c.prompter.Confirm(
fmt.Sprintf("delete API key '%s'", in.ID),
fmt.Sprintf("Are you sure you want to delete API key '%s'?", in.ID),
)
if err != nil {
return err
}
if !ok {
pterm.Info.Println("Deletion cancelled")
return nil
Expand Down
11 changes: 8 additions & 3 deletions cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/kernel/cli/pkg/interactive"
"github.com/kernel/cli/pkg/util"
"github.com/kernel/kernel-go-sdk"
"github.com/pterm/pterm"
Expand Down Expand Up @@ -256,9 +257,13 @@ func runAppDelete(cmd *cobra.Command, args []string) error {
if version != "" {
scope = fmt.Sprintf("version '%s'", version)
}
msg := fmt.Sprintf("Delete all deployments for app '%s' (%s)? This cannot be undone.", appName, scope)
pterm.DefaultInteractiveConfirm.DefaultText = msg
ok, _ := pterm.DefaultInteractiveConfirm.Show()
ok, err := interactive.Confirm(
fmt.Sprintf("delete all deployments for app '%s' (%s)", appName, scope),
fmt.Sprintf("Delete all deployments for app '%s' (%s)? This cannot be undone.", appName, scope),
)
if err != nil {
return err
}
if !ok {
pterm.Info.Println("Deletion cancelled")
return nil
Expand Down
14 changes: 10 additions & 4 deletions cmd/auth_connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"
"time"

"github.com/kernel/cli/pkg/interactive"
"github.com/kernel/cli/pkg/util"
"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
Expand All @@ -29,7 +30,8 @@ type AuthConnectionService interface {

// AuthConnectionCmd handles auth connection operations independent of cobra.
type AuthConnectionCmd struct {
svc AuthConnectionService
svc AuthConnectionService
prompter interactive.Prompter
}

type AuthConnectionCreateInput struct {
Expand Down Expand Up @@ -491,9 +493,13 @@ func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput)

func (c AuthConnectionCmd) Delete(ctx context.Context, in AuthConnectionDeleteInput) error {
if !in.SkipConfirm {
msg := fmt.Sprintf("Are you sure you want to delete managed auth '%s'?", in.ID)
pterm.DefaultInteractiveConfirm.DefaultText = msg
ok, _ := pterm.DefaultInteractiveConfirm.Show()
ok, err := c.prompter.Confirm(
fmt.Sprintf("delete managed auth '%s'", in.ID),
fmt.Sprintf("Are you sure you want to delete managed auth '%s'?", in.ID),
)
if err != nil {
return err
}
if !ok {
pterm.Info.Println("Deletion cancelled")
return nil
Expand Down
36 changes: 24 additions & 12 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strings"
"time"

"github.com/kernel/cli/pkg/interactive"
"github.com/kernel/cli/pkg/table"
"github.com/kernel/cli/pkg/util"
"github.com/kernel/kernel-go-sdk"
Expand Down Expand Up @@ -2713,6 +2714,7 @@ func init() {
browsersCreateCmd.Flags().String("telemetry", "", "Configure telemetry (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)")
browsersCreateCmd.Flags().String("name", "", "Optional unique name for the browser session (used to find it later; can be changed with 'browsers update --name')")
browsersCreateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the session (repeatable; up to 50 pairs)")
browsersCreateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts")
browsersCreateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
browsersCreateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
browsersCreateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")
Expand Down Expand Up @@ -2803,6 +2805,7 @@ func poolLeaseAllowedFlags() map[string]bool {
"tag": true,
"telemetry": true,
"output": true,
"yes": true,
// Global persistent flags that don't configure browsers
"no-color": true,
"log-level": true,
Expand Down Expand Up @@ -2835,6 +2838,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
output, _ := cmd.Flags().GetString("output")
skipConfirm, _ := cmd.Flags().GetBool("yes")

if poolID != "" && poolName != "" {
pterm.Error.Println("must specify at most one of --pool-id or --pool-name")
Expand Down Expand Up @@ -2863,10 +2867,18 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
pterm.Info.Println("When using a pool, all browser configuration comes from the pool itself.")
pterm.Info.Println("The conflicting flags will be ignored.")

result, _ := pterm.DefaultInteractiveConfirm.Show("Continue with pool configuration?")
if !result {
pterm.Info.Println("Cancelled. Remove conflicting flags or omit the pool flag.")
return nil
if !skipConfirm {
result, err := interactive.Confirm(
fmt.Sprintf("ignore conflicting browser configuration flags (%s) and continue with %s", strings.Join(conflicts, ", "), flagLabel),
"Continue with pool configuration?",
)
if err != nil {
return err
}
if !result {
pterm.Info.Println("Cancelled. Remove conflicting flags or omit the pool flag.")
return nil
}
}
pterm.Success.Println("Proceeding with pool configuration...")
}
Expand Down Expand Up @@ -2911,17 +2923,17 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {

// Handle interactive viewport selection
if viewportInteractive {
if viewport != "" {
if viewport != "" && interactive.IsInteractive() {
pterm.Warning.Println("Both --viewport and --viewport-interactive specified; using interactive mode")
}
options := getAvailableViewports()
selectedViewport, err := pterm.DefaultInteractiveSelect.
WithOptions(options).
WithDefaultText("Select a viewport size:").
Show()
selectedViewport, err := interactive.Select(
"viewport selection",
"pass --viewport instead of --viewport-interactive (e.g. --viewport 1920x1080@25)",
"Select a viewport size:",
getAvailableViewports(),
)
if err != nil {
pterm.Error.Printf("Failed to select viewport: %v\n", err)
return nil
return err
}
viewport = selectedViewport
}
Expand Down
94 changes: 61 additions & 33 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import (
"strings"

"github.com/kernel/cli/pkg/create"
"github.com/kernel/cli/pkg/interactive"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

// CreateCmd is a cobra-independent command handler for create operations
type CreateCmd struct{}
type CreateCmd struct {
prompter interactive.Prompter
}

// Create executes the creating a new Kernel app logic
func (c CreateCmd) Create(ctx context.Context, ci create.CreateInput) error {
Expand All @@ -23,16 +26,20 @@ func (c CreateCmd) Create(ctx context.Context, ci create.CreateInput) error {
return fmt.Errorf("failed to resolve app path: %w", err)
}

// Check if directory already exists and prompt for overwrite
// Check if directory already exists and prompt for overwrite. This is a
// backstop for direct callers; runCreateApp resolves the overwrite
// confirmation before calling Create.
if _, err := os.Stat(appPath); err == nil {
overwrite, err := create.PromptForOverwrite(ci.Name)
if err != nil {
return fmt.Errorf("failed to prompt for overwrite: %w", err)
}

if !overwrite {
pterm.Warning.Println("Operation cancelled.")
return nil
if !ci.SkipConfirm {
overwrite, err := create.PromptOverwrite(c.prompter, ci.Name)
if err != nil {
return err
}

if !overwrite {
pterm.Warning.Println("Operation cancelled.")
return nil
}
}

// Remove existing directory
Expand Down Expand Up @@ -81,6 +88,7 @@ func init() {
createCmd.Flags().StringP("name", "n", "", "Name of the application")
createCmd.Flags().StringP("language", "l", "", fmt.Sprintf("Language of the application (%s)", strings.Join(supportedLanguageDisplay(), ", ")))
createCmd.Flags().StringP("template", "t", "", "Template to use for the application (see 'kernel create --help' for the full list)")
createCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts (overwrite an existing directory without asking)")
}

// supportedLanguageDisplay returns each supported language with its shorthand,
Expand All @@ -104,7 +112,9 @@ func buildCreateLongHelp() string {
var b strings.Builder
b.WriteString("Commands for creating new Kernel applications.\n\n")
b.WriteString("Pass --name, --language and --template to scaffold non-interactively;\n")
b.WriteString("any omitted flag falls back to an interactive prompt.\n\n")
b.WriteString("any omitted flag falls back to an interactive prompt. In a\n")
b.WriteString("non-interactive shell the command fails fast instead of prompting.\n")
b.WriteString("Pass --yes to overwrite an existing directory without confirmation.\n\n")

b.WriteString("Languages:\n")
for _, l := range create.SupportedLanguages {
Expand Down Expand Up @@ -140,29 +150,47 @@ func buildCreateLongHelp() string {
}

func runCreateApp(cmd *cobra.Command, args []string) error {
appName, _ := cmd.Flags().GetString("name")
language, _ := cmd.Flags().GetString("language")
template, _ := cmd.Flags().GetString("template")

appName, err := create.PromptForAppName(appName)
if err != nil {
return fmt.Errorf("failed to get app name: %w", err)
}
return createApp(cmd, interactive.NewPrompter())
}

language, err = create.PromptForLanguage(language)
if err != nil {
return fmt.Errorf("failed to get language: %w", err)
}
// createApp resolves the create inputs and scaffolds the app. The prompter
// carries the terminal capability, so tests can drive the non-interactive
// path deterministically without mutating any package state.
func createApp(cmd *cobra.Command, prompter interactive.Prompter) error {
raw := create.RawInput{}
raw.Name, _ = cmd.Flags().GetString("name")
raw.Language, _ = cmd.Flags().GetString("language")
raw.Template, _ = cmd.Flags().GetString("template")
raw.SkipOverwriteConfirm, _ = cmd.Flags().GetBool("yes")

c := CreateCmd{prompter: prompter}

// create.ResolveInput is the single resolver from raw flags to a
// normalized CreateInput for both modes. Interactively, each remaining
// problem carries its own resolution step: prompt for that field, apply
// the answer, re-resolve (so e.g. the template list reflects the chosen
// language). Non-interactively, every problem is reported at once in a
// single fail-fast error.
for {
in, problems := create.ResolveInput(raw)
if len(problems) == 0 {
return c.Create(cmd.Context(), in)
}
if !prompter.CanPrompt() {
return interactive.ErrInputsRequired(create.ProblemMessages(problems))
}

template, err = create.PromptForTemplate(template, language)
if err != nil {
return fmt.Errorf("failed to get template: %w", err)
p := problems[0]
if p.Invalid {
pterm.Warning.Println(p.Message)
}
cancelled, err := p.Resolve(prompter, &raw)
if err != nil {
return err
}
if cancelled {
pterm.Warning.Println("Operation cancelled.")
return nil
}
}

c := CreateCmd{}
return c.Create(cmd.Context(), create.CreateInput{
Name: appName,
Language: language,
Template: template,
Comment thread
cursor[bot] marked this conversation as resolved.
})
}
12 changes: 9 additions & 3 deletions cmd/credential_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/kernel/cli/pkg/interactive"
"github.com/kernel/cli/pkg/util"
"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
Expand All @@ -27,6 +28,7 @@ type CredentialProvidersService interface {
// CredentialProvidersCmd handles credential provider operations independent of cobra.
type CredentialProvidersCmd struct {
providers CredentialProvidersService
prompter interactive.Prompter
}

type CredentialProvidersListInput struct {
Expand Down Expand Up @@ -254,9 +256,13 @@ func (c CredentialProvidersCmd) Update(ctx context.Context, in CredentialProvide

func (c CredentialProvidersCmd) Delete(ctx context.Context, in CredentialProvidersDeleteInput) error {
if !in.SkipConfirm {
msg := fmt.Sprintf("Are you sure you want to delete credential provider '%s'?", in.ID)
pterm.DefaultInteractiveConfirm.DefaultText = msg
ok, _ := pterm.DefaultInteractiveConfirm.Show()
ok, err := c.prompter.Confirm(
fmt.Sprintf("delete credential provider '%s'", in.ID),
fmt.Sprintf("Are you sure you want to delete credential provider '%s'?", in.ID),
)
if err != nil {
return err
}
if !ok {
pterm.Info.Println("Deletion cancelled")
return nil
Expand Down
12 changes: 9 additions & 3 deletions cmd/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/kernel/cli/pkg/interactive"
"github.com/kernel/cli/pkg/util"
"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
Expand All @@ -26,6 +27,7 @@ type CredentialsService interface {
// CredentialsCmd handles credential operations independent of cobra.
type CredentialsCmd struct {
credentials CredentialsService
prompter interactive.Prompter
}

type CredentialsListInput struct {
Expand Down Expand Up @@ -281,9 +283,13 @@ func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) e

func (c CredentialsCmd) Delete(ctx context.Context, in CredentialsDeleteInput) error {
if !in.SkipConfirm {
msg := fmt.Sprintf("Are you sure you want to delete credential '%s'?", in.Identifier)
pterm.DefaultInteractiveConfirm.DefaultText = msg
ok, _ := pterm.DefaultInteractiveConfirm.Show()
ok, err := c.prompter.Confirm(
fmt.Sprintf("delete credential '%s'", in.Identifier),
fmt.Sprintf("Are you sure you want to delete credential '%s'?", in.Identifier),
)
if err != nil {
return err
}
if !ok {
pterm.Info.Println("Deletion cancelled")
return nil
Expand Down
Loading
Loading