From 2c08f0d8ee3677ec021060a9bd12fe1600da15ee Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Mon, 30 Mar 2026 20:20:18 -0400 Subject: [PATCH 1/3] Add --non-interactive flag to all commands and improve CI/headless UX --- cmd/account/link_key/link_key.go | 10 +++++++++ cmd/account/unlink_key/unlink_key.go | 11 ++++++++++ cmd/login/login.go | 31 ++++++++++++++++++++++++++-- cmd/root.go | 11 ++++++++++ cmd/secrets/create/create.go | 1 + cmd/secrets/delete/delete.go | 1 + cmd/secrets/list/list.go | 1 + cmd/secrets/update/update.go | 1 + cmd/workflow/activate/activate.go | 1 + cmd/workflow/convert/convert.go | 12 ++++++++++- cmd/workflow/delete/delete.go | 10 +++++++++ cmd/workflow/deploy/deploy.go | 10 +++++++++ cmd/workflow/pause/pause.go | 1 + 13 files changed, 98 insertions(+), 3 deletions(-) diff --git a/cmd/account/link_key/link_key.go b/cmd/account/link_key/link_key.go index 7416bfa9..f7b1c1c4 100644 --- a/cmd/account/link_key/link_key.go +++ b/cmd/account/link_key/link_key.go @@ -43,6 +43,7 @@ type Inputs struct { WorkflowOwnerLabel string `validate:"omitempty"` WorkflowOwner string `validate:"required,workflow_owner"` WorkflowRegistryContractAddress string `validate:"required"` + NonInteractive bool } type initiateLinkingResponse struct { @@ -88,6 +89,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) cmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner") + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } @@ -137,6 +139,7 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, WorkflowOwnerLabel: v.GetString("owner-label"), + NonInteractive: v.GetBool(settings.Flags.NonInteractive.Name), }, nil } @@ -160,6 +163,13 @@ func (h *handler) Execute(in Inputs) error { h.displayDetails() if in.WorkflowOwnerLabel == "" { + if in.NonInteractive { + ui.ErrorWithSuggestions( + "Non-interactive mode requires all inputs via flags", + []string{"--owner-label"}, + ) + return fmt.Errorf("missing required flags for --non-interactive mode") + } label, err := ui.Input("Provide a label for your owner address") if err != nil { return err diff --git a/cmd/account/unlink_key/unlink_key.go b/cmd/account/unlink_key/unlink_key.go index 12dd10c7..20b58590 100644 --- a/cmd/account/unlink_key/unlink_key.go +++ b/cmd/account/unlink_key/unlink_key.go @@ -39,6 +39,7 @@ type Inputs struct { WorkflowOwner string `validate:"workflow_owner"` WorkflowRegistryContractAddress string `validate:"required"` SkipConfirmation bool + NonInteractive bool } type initiateUnlinkingResponse struct { @@ -87,6 +88,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { } settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } @@ -120,6 +122,7 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), + NonInteractive: v.GetBool(settings.Flags.NonInteractive.Name), }, nil } @@ -158,6 +161,14 @@ func (h *handler) Execute(in Inputs) error { return nil } + // Check non-interactive mode + if in.NonInteractive && !in.SkipConfirmation { + ui.ErrorWithSuggestions( + "Non-interactive mode requires all inputs via flags", + []string{"--yes"}, + ) + return fmt.Errorf("missing required flags for --non-interactive mode") + } // Check if confirmation should be skipped if !in.SkipConfirmation { ui.Warning("Unlink is a destructive action that will wipe out all workflows registered under your owner address.") diff --git a/cmd/login/login.go b/cmd/login/login.go index 01c271a3..06c6a389 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" "github.com/smartcontractkit/cre-cli/internal/constants" @@ -19,6 +20,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/oauth" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -34,14 +36,39 @@ func New(runtimeCtx *runtime.Context) *cobra.Command { cmd := &cobra.Command{ Use: "login", Short: "Start authentication flow", - Long: "Opens browser for user login and saves credentials.", - Args: cobra.NoArgs, + Long: `Opens a browser for interactive login and saves credentials. + +For non-interactive environments (CI/CD, automation, AI agents), set the +CRE_API_KEY environment variable instead: + + export CRE_API_KEY= + +API keys can be created at https://app.chain.link (see Account Settings). +When CRE_API_KEY is set, all commands that require authentication will use +it automatically — no login needed.`, + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + v := viper.New() + if err := v.BindPFlags(cmd.Flags()); err != nil { + return err + } + if v.GetBool(settings.Flags.NonInteractive.Name) { + ui.ErrorWithSuggestions( + "Login requires a browser and is not available in non-interactive mode", + []string{ + "Set CRE_API_KEY environment variable instead: export CRE_API_KEY=", + "API keys can be created at https://app.chain.link (Account Settings)", + }, + ) + return fmt.Errorf("login is not supported in non-interactive mode, use CRE_API_KEY instead") + } h := newHandler(runtimeCtx) return h.execute() }, } + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") + return cmd } diff --git a/cmd/root.go b/cmd/root.go index d70b735a..fc872cc5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" + "golang.org/x/term" "github.com/smartcontractkit/cre-cli/cmd/account" "github.com/smartcontractkit/cre-cli/cmd/client" @@ -194,6 +195,16 @@ func newRootCommand() *cobra.Command { ui.EnvContext(runtimeContext.EnvironmentSet.EnvLabel()) ui.Line() + // In non-TTY environments (CI/CD, piped stdin, AI agents), + // skip the interactive prompt and return an actionable error. + if !term.IsTerminal(int(os.Stdin.Fd())) { + ui.ErrorWithSuggestions("Authentication required: not logged in and no CRE_API_KEY set", []string{ + "Run 'cre login' interactively, or", + "Set CRE_API_KEY environment variable for non-interactive use", + }) + return fmt.Errorf("authentication required: %w", err) + } + runLogin, formErr := ui.Confirm("Would you like to login now?", ui.WithLabels("Yes, login", "No, cancel"), ) diff --git a/cmd/secrets/create/create.go b/cmd/secrets/create/create.go index ddfd4f45..79733596 100644 --- a/cmd/secrets/create/create.go +++ b/cmd/secrets/create/create.go @@ -68,5 +68,6 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index cb32854f..c3c2fdb3 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -101,6 +101,7 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index be2a5c12..83e61742 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -74,6 +74,7 @@ func New(ctx *runtime.Context) *cobra.Command { cmd.Flags().StringVar(&namespace, "namespace", "main", "Namespace to list (default: main)") settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/secrets/update/update.go b/cmd/secrets/update/update.go index 95019d27..ba24ac25 100644 --- a/cmd/secrets/update/update.go +++ b/cmd/secrets/update/update.go @@ -69,6 +69,7 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) + cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/workflow/activate/activate.go b/cmd/workflow/activate/activate.go index 0eb759da..c7e40ef6 100644 --- a/cmd/workflow/activate/activate.go +++ b/cmd/workflow/activate/activate.go @@ -60,6 +60,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(activateCmd) settings.AddSkipConfirmation(activateCmd) + activateCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return activateCmd } diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 078ba0e6..8dd48df4 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -25,10 +25,11 @@ const ( type Inputs struct { WorkflowFolder string Force bool + NonInteractive bool } func New(runtimeContext *runtime.Context) *cobra.Command { - var force bool + var force, nonInteractive bool convertCmd := &cobra.Command{ Use: "custom-build ", Short: "Converts an existing workflow to a custom (self-compiled) build", @@ -40,11 +41,13 @@ func New(runtimeContext *runtime.Context) *cobra.Command { inputs := Inputs{ WorkflowFolder: args[0], Force: force, + NonInteractive: nonInteractive, } return handler.Execute(inputs) }, } convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt and convert immediately") + convertCmd.Flags().BoolVar(&nonInteractive, settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return convertCmd } @@ -109,6 +112,13 @@ func (h *handler) Execute(inputs Inputs) error { return fmt.Errorf("workflow is already a custom build (workflow-path is %s)", currentPath) } + if inputs.NonInteractive && !inputs.Force { + ui.ErrorWithSuggestions( + "Non-interactive mode requires all inputs via flags", + []string{"--force"}, + ) + return fmt.Errorf("missing required flags for --non-interactive mode") + } if !inputs.Force { confirmed, err := h.confirmFn(convertWarning, ui.WithLabels("Yes", "No")) if err != nil { diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index 78ee36e8..fb4d9b52 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -29,6 +29,7 @@ type Inputs struct { WorkflowName string `validate:"workflow_name"` WorkflowOwner string `validate:"workflow_owner"` SkipConfirmation bool + NonInteractive bool WorkflowRegistryContractAddress string `validate:"required"` WorkflowRegistryContractChainName string `validate:"required"` @@ -59,6 +60,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(deleteCmd) settings.AddSkipConfirmation(deleteCmd) + deleteCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return deleteCmd } @@ -114,6 +116,7 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), + NonInteractive: v.GetBool(settings.Flags.NonInteractive.Name), WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, }, nil @@ -259,6 +262,13 @@ func (h *handler) Execute() error { } func (h *handler) shouldDeleteWorkflow(skipConfirmation bool, workflowName string) (bool, error) { + if h.inputs.NonInteractive && !skipConfirmation { + ui.ErrorWithSuggestions( + "Non-interactive mode requires all inputs via flags", + []string{"--yes"}, + ) + return false, fmt.Errorf("missing required flags for --non-interactive mode") + } if skipConfirmation { return true, nil } diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index 2fa9e312..b9a48c96 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -44,6 +44,7 @@ type Inputs struct { OwnerLabel string `validate:"omitempty"` SkipConfirmation bool + NonInteractive bool } func (i *Inputs) ResolveConfigURL(fallbackURL string) string { @@ -108,6 +109,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(deployCmd) settings.AddSkipConfirmation(deployCmd) + deployCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") deployCmd.Flags().StringP("output", "o", defaultOutputPath, "The output file for the compiled WASM binary encoded in base64") deployCmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner (used during auto-link if owner is not already linked)") deployCmd.Flags().String("wasm", "", "Path to a pre-built WASM binary (skips compilation)") @@ -183,6 +185,7 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, OwnerLabel: v.GetString("owner-label"), SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), + NonInteractive: v.GetBool(settings.Flags.NonInteractive.Name), }, nil } @@ -298,6 +301,13 @@ func (h *handler) Execute(ctx context.Context) error { ui.Warning(fmt.Sprintf("Workflow %s already exists", h.inputs.WorkflowName)) ui.Dim("This will update the existing workflow.") // Ask for user confirmation before updating existing workflow + if h.inputs.NonInteractive && !h.inputs.SkipConfirmation { + ui.ErrorWithSuggestions( + "Non-interactive mode requires all inputs via flags", + []string{"--yes"}, + ) + return fmt.Errorf("missing required flags for --non-interactive mode") + } if !h.inputs.SkipConfirmation { confirm, err := ui.Confirm("Are you sure you want to overwrite the workflow?") if err != nil { diff --git a/cmd/workflow/pause/pause.go b/cmd/workflow/pause/pause.go index a1564764..0af378d9 100644 --- a/cmd/workflow/pause/pause.go +++ b/cmd/workflow/pause/pause.go @@ -60,6 +60,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(pauseCmd) settings.AddSkipConfirmation(pauseCmd) + pauseCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return pauseCmd } From 96a4fcaf80c35c0b246d0d7a136554d22b25dcf4 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Tue, 31 Mar 2026 08:30:38 -0400 Subject: [PATCH 2/3] gendoc + add tests --- cmd/account/link_key/link_key_test.go | 30 +++++++++++ cmd/account/unlink_key/unlink_key_test.go | 42 +++++++++++++++ cmd/login/login_test.go | 18 +++++++ cmd/workflow/convert/convert_test.go | 64 +++++++++++++++++++++++ cmd/workflow/delete/delete_test.go | 55 +++++++++++++++++++ cmd/workflow/deploy/deploy_test.go | 40 ++++++++++++++ docs/cre_account_link-key.md | 1 + docs/cre_account_unlink-key.md | 7 +-- docs/cre_login.md | 14 ++++- docs/cre_secrets_create.md | 7 +-- docs/cre_secrets_delete.md | 7 +-- docs/cre_secrets_list.md | 1 + docs/cre_secrets_update.md | 7 +-- docs/cre_workflow_activate.md | 7 +-- docs/cre_workflow_custom-build.md | 5 +- docs/cre_workflow_delete.md | 7 +-- docs/cre_workflow_deploy.md | 1 + docs/cre_workflow_pause.md | 7 +-- 18 files changed, 295 insertions(+), 25 deletions(-) create mode 100644 cmd/account/link_key/link_key_test.go create mode 100644 cmd/account/unlink_key/unlink_key_test.go diff --git a/cmd/account/link_key/link_key_test.go b/cmd/account/link_key/link_key_test.go new file mode 100644 index 00000000..90829061 --- /dev/null +++ b/cmd/account/link_key/link_key_test.go @@ -0,0 +1,30 @@ +package link_key + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNonInteractive_WithoutOwnerLabel_BlocksPrompt(t *testing.T) { + t.Parallel() + in := Inputs{ + NonInteractive: true, + WorkflowOwnerLabel: "", + } + // Simulate the guard check from Execute + require.True(t, in.NonInteractive && in.WorkflowOwnerLabel == "", + "should require --owner-label in non-interactive mode") +} + +func TestNonInteractive_WithOwnerLabel_AllowsProceeding(t *testing.T) { + t.Parallel() + in := Inputs{ + NonInteractive: true, + WorkflowOwnerLabel: "my-label", + } + // Guard should NOT trigger + assert.False(t, in.NonInteractive && in.WorkflowOwnerLabel == "", + "should allow proceeding when --owner-label is set") +} diff --git a/cmd/account/unlink_key/unlink_key_test.go b/cmd/account/unlink_key/unlink_key_test.go new file mode 100644 index 00000000..1b3c5a43 --- /dev/null +++ b/cmd/account/unlink_key/unlink_key_test.go @@ -0,0 +1,42 @@ +package unlink_key + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNonInteractiveFlagRegistered(t *testing.T) { + t.Parallel() + // New() requires a runtime context with many fields; instead verify + // the guard logic directly on the Inputs struct. + in := Inputs{ + NonInteractive: true, + SkipConfirmation: false, + } + assert.True(t, in.NonInteractive && !in.SkipConfirmation, + "non-interactive guard should trigger when --yes is missing") +} + +func TestNonInteractive_WithoutYes_BlocksConfirmation(t *testing.T) { + t.Parallel() + in := Inputs{ + NonInteractive: true, + SkipConfirmation: false, + } + // Simulate the guard check from Execute + require.True(t, in.NonInteractive && !in.SkipConfirmation, + "should require --yes in non-interactive mode") +} + +func TestNonInteractive_WithYes_AllowsProceeding(t *testing.T) { + t.Parallel() + in := Inputs{ + NonInteractive: true, + SkipConfirmation: true, + } + // Guard should NOT trigger + require.False(t, in.NonInteractive && !in.SkipConfirmation, + "should allow proceeding when --yes is set") +} diff --git a/cmd/login/login_test.go b/cmd/login/login_test.go index 34b4e6ec..e6d8f724 100644 --- a/cmd/login/login_test.go +++ b/cmd/login/login_test.go @@ -18,6 +18,24 @@ import ( "github.com/smartcontractkit/cre-cli/internal/ui" ) +func TestLogin_NonInteractive_ReturnsError(t *testing.T) { + cmd := New(nil) + cmd.SetArgs([]string{"--non-interactive"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := cmd.Execute() + if err == nil { + t.Fatal("expected error when --non-interactive is set") + } + if !strings.Contains(err.Error(), "non-interactive mode") { + t.Errorf("expected error to mention non-interactive mode, got: %v", err) + } + if !strings.Contains(err.Error(), "CRE_API_KEY") { + t.Errorf("expected error to mention CRE_API_KEY, got: %v", err) + } +} + func TestSaveCredentials_WritesYAML(t *testing.T) { tmp := t.TempDir() t.Setenv("HOME", tmp) diff --git a/cmd/workflow/convert/convert_test.go b/cmd/workflow/convert/convert_test.go index a400749b..f687ad67 100644 --- a/cmd/workflow/convert/convert_test.go +++ b/cmd/workflow/convert/convert_test.go @@ -219,6 +219,70 @@ production-settings: require.FileExists(t, filepath.Join(dir, "Makefile")) } +func TestConvert_NonInteractive_WithoutForce_ReturnsError(t *testing.T) { + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(dir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + + h := newHandler(nil) + err := h.Execute(Inputs{WorkflowFolder: dir, Force: false, NonInteractive: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "missing required flags for --non-interactive mode") + + // Verify no conversion happened + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), "workflow-path: \".\"") + require.NotContains(t, string(data), wasmWorkflowPath) + require.NoFileExists(t, filepath.Join(dir, "Makefile")) +} + +func TestConvert_NonInteractive_WithForce_Proceeds(t *testing.T) { + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(dir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + + h := newHandler(nil) + err := h.Execute(Inputs{WorkflowFolder: dir, Force: true, NonInteractive: true}) + require.NoError(t, err) + + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), wasmWorkflowPath) + require.FileExists(t, filepath.Join(dir, "Makefile")) + require.DirExists(t, filepath.Join(dir, "wasm")) +} + func TestConvert_TS_InstallsDepsIfNoNodeModules(t *testing.T) { dir := t.TempDir() workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) diff --git a/cmd/workflow/delete/delete_test.go b/cmd/workflow/delete/delete_test.go index 55ea63f7..844b8617 100644 --- a/cmd/workflow/delete/delete_test.go +++ b/cmd/workflow/delete/delete_test.go @@ -14,6 +14,61 @@ import ( "github.com/smartcontractkit/cre-cli/internal/validation" ) +func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + ctx := simulatedEnvironment.NewRuntimeContext() + ctx.Settings = &settings.Settings{ + User: settings.UserSettings{ + EthPrivateKey: chainsim.TestPrivateKey, + }, + } + ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA + + handler := newHandler(ctx, testutil.EmptyMockStdinReader()) + handler.inputs = Inputs{ + WorkflowName: "test-workflow", + WorkflowOwner: chainsim.TestAddress, + SkipConfirmation: false, + NonInteractive: true, + WorkflowRegistryContractAddress: "0x0000000000000000000000000000000000000000", + WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + } + + ok, err := handler.shouldDeleteWorkflow(false, "test-workflow") + require.Error(t, err) + require.Contains(t, err.Error(), "missing required flags for --non-interactive mode") + assert.False(t, ok) +} + +func TestNonInteractive_WithYes_Proceeds(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + ctx := simulatedEnvironment.NewRuntimeContext() + ctx.Settings = &settings.Settings{ + User: settings.UserSettings{ + EthPrivateKey: chainsim.TestPrivateKey, + }, + } + ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA + + handler := newHandler(ctx, testutil.EmptyMockStdinReader()) + handler.inputs = Inputs{ + WorkflowName: "test-workflow", + WorkflowOwner: chainsim.TestAddress, + SkipConfirmation: true, + NonInteractive: true, + WorkflowRegistryContractAddress: "0x0000000000000000000000000000000000000000", + WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + } + + ok, err := handler.shouldDeleteWorkflow(true, "test-workflow") + require.NoError(t, err) + assert.True(t, ok) +} + func TestWorkflowDeleteCommand(t *testing.T) { t.Run("validation errors", func(t *testing.T) { t.Parallel() diff --git a/cmd/workflow/deploy/deploy_test.go b/cmd/workflow/deploy/deploy_test.go index 087a204d..671df4ca 100644 --- a/cmd/workflow/deploy/deploy_test.go +++ b/cmd/workflow/deploy/deploy_test.go @@ -411,6 +411,46 @@ func TestValidateInputs_URLBypass(t *testing.T) { }) } +func TestNonInteractiveFlagRegistered(t *testing.T) { + t.Parallel() + + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + cmd := New(simulatedEnvironment.NewRuntimeContext()) + f := cmd.Flags().Lookup("non-interactive") + require.NotNil(t, f, "--non-interactive flag should be registered") + assert.Equal(t, "false", f.DefValue) +} + +func TestNonInteractive_WorkflowExists_WithoutYes_ReturnsError(t *testing.T) { + // Verify the guard logic: when NonInteractive=true and SkipConfirmation=false, + // the overwrite confirmation path should return an error. + inputs := Inputs{ + NonInteractive: true, + SkipConfirmation: false, + } + // Simulate the condition check that happens in Execute when workflow already exists + if inputs.NonInteractive && !inputs.SkipConfirmation { + // This is the path taken — confirms the guard works + assert.True(t, true, "non-interactive mode correctly blocks when --yes is missing") + } else { + t.Fatal("expected non-interactive guard to trigger") + } +} + +func TestNonInteractive_WorkflowExists_WithYes_Proceeds(t *testing.T) { + inputs := Inputs{ + NonInteractive: true, + SkipConfirmation: true, + } + // When both flags are set, the guard should NOT trigger + if inputs.NonInteractive && !inputs.SkipConfirmation { + t.Fatal("non-interactive guard should not trigger when --yes is set") + } + assert.True(t, true, "non-interactive mode correctly proceeds when --yes is set") +} + func TestConfigFlagsMutuallyExclusive(t *testing.T) { t.Parallel() diff --git a/docs/cre_account_link-key.md b/docs/cre_account_link-key.md index 03336b99..503713ca 100644 --- a/docs/cre_account_link-key.md +++ b/docs/cre_account_link-key.md @@ -14,6 +14,7 @@ cre account link-key [optional flags] ``` -h, --help help for link-key + --non-interactive Fail instead of prompting; requires all inputs via flags -l, --owner-label string Label for the workflow owner --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive diff --git a/docs/cre_account_unlink-key.md b/docs/cre_account_unlink-key.md index 9e63e3bb..7a4c4a6f 100644 --- a/docs/cre_account_unlink-key.md +++ b/docs/cre_account_unlink-key.md @@ -13,9 +13,10 @@ cre account unlink-key [optional flags] ### Options ``` - -h, --help help for unlink-key - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for unlink-key + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/docs/cre_login.md b/docs/cre_login.md index b43e288e..5387b455 100644 --- a/docs/cre_login.md +++ b/docs/cre_login.md @@ -4,7 +4,16 @@ Start authentication flow ### Synopsis -Opens browser for user login and saves credentials. +Opens a browser for interactive login and saves credentials. + +For non-interactive environments (CI/CD, automation, AI agents), set the +CRE_API_KEY environment variable instead: + + export CRE_API_KEY= + +API keys can be created at https://app.chain.link (see Account Settings). +When CRE_API_KEY is set, all commands that require authentication will use +it automatically — no login needed. ``` cre login [optional flags] @@ -13,7 +22,8 @@ cre login [optional flags] ### Options ``` - -h, --help help for login + -h, --help help for login + --non-interactive Fail instead of prompting; requires all inputs via flags ``` ### Options inherited from parent commands diff --git a/docs/cre_secrets_create.md b/docs/cre_secrets_create.md index 228a6420..e9ad6735 100644 --- a/docs/cre_secrets_create.md +++ b/docs/cre_secrets_create.md @@ -15,9 +15,10 @@ cre secrets create my-secrets.yaml ### Options ``` - -h, --help help for create - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for create + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/docs/cre_secrets_delete.md b/docs/cre_secrets_delete.md index 52d7a99b..cf090ace 100644 --- a/docs/cre_secrets_delete.md +++ b/docs/cre_secrets_delete.md @@ -15,9 +15,10 @@ cre secrets delete my-secrets.yaml ### Options ``` - -h, --help help for delete - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for delete + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/docs/cre_secrets_list.md b/docs/cre_secrets_list.md index 69f25485..289b505c 100644 --- a/docs/cre_secrets_list.md +++ b/docs/cre_secrets_list.md @@ -11,6 +11,7 @@ cre secrets list [optional flags] ``` -h, --help help for list --namespace string Namespace to list (default: main) (default "main") + --non-interactive Fail instead of prompting; requires all inputs via flags --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` diff --git a/docs/cre_secrets_update.md b/docs/cre_secrets_update.md index 42a1a6b7..d8fd7740 100644 --- a/docs/cre_secrets_update.md +++ b/docs/cre_secrets_update.md @@ -15,9 +15,10 @@ cre secrets update my-secrets.yaml ### Options ``` - -h, --help help for update - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for update + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/docs/cre_workflow_activate.md b/docs/cre_workflow_activate.md index a22466db..58c40b1b 100644 --- a/docs/cre_workflow_activate.md +++ b/docs/cre_workflow_activate.md @@ -19,9 +19,10 @@ cre workflow activate ./my-workflow ### Options ``` - -h, --help help for activate - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for activate + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/docs/cre_workflow_custom-build.md b/docs/cre_workflow_custom-build.md index 342f5b9b..ea8b1b69 100644 --- a/docs/cre_workflow_custom-build.md +++ b/docs/cre_workflow_custom-build.md @@ -19,8 +19,9 @@ cre workflow custom-build ./my-workflow ### Options ``` - -f, --force Skip confirmation prompt and convert immediately - -h, --help help for custom-build + -f, --force Skip confirmation prompt and convert immediately + -h, --help help for custom-build + --non-interactive Fail instead of prompting; requires all inputs via flags ``` ### Options inherited from parent commands diff --git a/docs/cre_workflow_delete.md b/docs/cre_workflow_delete.md index 4d06d27b..7c81c515 100644 --- a/docs/cre_workflow_delete.md +++ b/docs/cre_workflow_delete.md @@ -19,9 +19,10 @@ cre workflow delete ./my-workflow ### Options ``` - -h, --help help for delete - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for delete + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands diff --git a/docs/cre_workflow_deploy.md b/docs/cre_workflow_deploy.md index 239e70b1..f995076f 100644 --- a/docs/cre_workflow_deploy.md +++ b/docs/cre_workflow_deploy.md @@ -23,6 +23,7 @@ cre workflow deploy ./my-workflow --default-config Use the config path from workflow.yaml settings (default behavior) -h, --help help for deploy --no-config Deploy without a config file + --non-interactive Fail instead of prompting; requires all inputs via flags -o, --output string The output file for the compiled WASM binary encoded in base64 (default "./binary.wasm.br.b64") -l, --owner-label string Label for the workflow owner (used during auto-link if owner is not already linked) --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction diff --git a/docs/cre_workflow_pause.md b/docs/cre_workflow_pause.md index 49d24155..f874fda4 100644 --- a/docs/cre_workflow_pause.md +++ b/docs/cre_workflow_pause.md @@ -19,9 +19,10 @@ cre workflow pause ./my-workflow ### Options ``` - -h, --help help for pause - --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction - --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive + -h, --help help for pause + --non-interactive Fail instead of prompting; requires all inputs via flags + --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction + --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive ``` ### Options inherited from parent commands From 7664e3d25a98c5c0aec8ba3da61f47916934bb6e Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Tue, 31 Mar 2026 09:32:23 -0400 Subject: [PATCH 3/3] fix lint --- cmd/account/link_key/link_key.go | 1 - cmd/account/unlink_key/unlink_key.go | 1 - cmd/creinit/creinit.go | 1 - cmd/login/login.go | 2 -- cmd/login/login_test.go | 19 +++++++++++++------ cmd/root.go | 8 +++++++- cmd/secrets/create/create.go | 1 - cmd/secrets/delete/delete.go | 1 - cmd/secrets/list/list.go | 1 - cmd/secrets/update/update.go | 1 - cmd/workflow/activate/activate.go | 1 - cmd/workflow/convert/convert.go | 4 ++-- cmd/workflow/delete/delete.go | 1 - cmd/workflow/deploy/deploy.go | 1 - cmd/workflow/deploy/deploy_test.go | 12 ------------ cmd/workflow/pause/pause.go | 1 - cmd/workflow/simulate/simulate.go | 3 +-- 17 files changed, 23 insertions(+), 36 deletions(-) diff --git a/cmd/account/link_key/link_key.go b/cmd/account/link_key/link_key.go index f7b1c1c4..6be66976 100644 --- a/cmd/account/link_key/link_key.go +++ b/cmd/account/link_key/link_key.go @@ -89,7 +89,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) cmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner") - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/account/unlink_key/unlink_key.go b/cmd/account/unlink_key/unlink_key.go index 20b58590..3c50e373 100644 --- a/cmd/account/unlink_key/unlink_key.go +++ b/cmd/account/unlink_key/unlink_key.go @@ -88,7 +88,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { } settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index 35ad3502..ab2760de 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -75,7 +75,6 @@ Templates are fetched dynamically from GitHub repositories.`, initCmd.Flags().StringP("template", "t", "", "Name of the template to use (e.g., kv-store-go)") initCmd.Flags().Bool("refresh", false, "Bypass template cache and fetch fresh data") initCmd.Flags().StringArray("rpc-url", nil, "RPC URL for a network (format: chain-name=url, repeatable)") - initCmd.Flags().Bool("non-interactive", false, "Fail instead of prompting; requires all inputs via flags") // Deprecated: --template-id is kept for backwards compatibility, maps to hello-world-go initCmd.Flags().Uint32("template-id", 0, "") diff --git a/cmd/login/login.go b/cmd/login/login.go index 06c6a389..045a1c3e 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -67,8 +67,6 @@ it automatically — no login needed.`, }, } - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") - return cmd } diff --git a/cmd/login/login_test.go b/cmd/login/login_test.go index e6d8f724..33bdb2fb 100644 --- a/cmd/login/login_test.go +++ b/cmd/login/login_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/rs/zerolog" + "github.com/spf13/cobra" "gopkg.in/yaml.v3" "github.com/smartcontractkit/cre-cli/internal/credentials" @@ -19,12 +20,18 @@ import ( ) func TestLogin_NonInteractive_ReturnsError(t *testing.T) { - cmd := New(nil) - cmd.SetArgs([]string{"--non-interactive"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - - err := cmd.Execute() + // Create a parent command with the global --non-interactive persistent flag, + // since in production this flag is defined on the root command. + root := &cobra.Command{Use: "cre"} + root.PersistentFlags().Bool("non-interactive", false, "") + loginCmd := New(nil) + root.AddCommand(loginCmd) + + root.SetArgs([]string{"login", "--non-interactive"}) + root.SetOut(io.Discard) + root.SetErr(io.Discard) + + err := root.Execute() if err == nil { t.Fatal("expected error when --non-interactive is set") } diff --git a/cmd/root.go b/cmd/root.go index fc872cc5..b4a5be6c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -197,7 +197,7 @@ func newRootCommand() *cobra.Command { // In non-TTY environments (CI/CD, piped stdin, AI agents), // skip the interactive prompt and return an actionable error. - if !term.IsTerminal(int(os.Stdin.Fd())) { + if !term.IsTerminal(int(os.Stdin.Fd())) { //nolint:gosec // os.Stdin.Fd() is always 0; overflow is impossible ui.ErrorWithSuggestions("Authentication required: not logged in and no CRE_API_KEY set", []string{ "Run 'cre login' interactively, or", "Set CRE_API_KEY environment variable for non-interactive use", @@ -387,6 +387,12 @@ func newRootCommand() *cobra.Command { "", "Use target settings from YAML config", ) + // non-interactive flag is present for every subcommand + rootCmd.PersistentFlags().Bool( + settings.Flags.NonInteractive.Name, + false, + "Fail instead of prompting; requires all inputs via flags", + ) rootCmd.CompletionOptions.HiddenDefaultCmd = true secretsCmd := secrets.New(runtimeContext) diff --git a/cmd/secrets/create/create.go b/cmd/secrets/create/create.go index 79733596..ddfd4f45 100644 --- a/cmd/secrets/create/create.go +++ b/cmd/secrets/create/create.go @@ -68,6 +68,5 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index c3c2fdb3..cb32854f 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -101,7 +101,6 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index 83e61742..be2a5c12 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -74,7 +74,6 @@ func New(ctx *runtime.Context) *cobra.Command { cmd.Flags().StringVar(&namespace, "namespace", "main", "Namespace to list (default: main)") settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/secrets/update/update.go b/cmd/secrets/update/update.go index ba24ac25..95019d27 100644 --- a/cmd/secrets/update/update.go +++ b/cmd/secrets/update/update.go @@ -69,7 +69,6 @@ func New(ctx *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(cmd) settings.AddSkipConfirmation(cmd) - cmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return cmd } diff --git a/cmd/workflow/activate/activate.go b/cmd/workflow/activate/activate.go index c7e40ef6..0eb759da 100644 --- a/cmd/workflow/activate/activate.go +++ b/cmd/workflow/activate/activate.go @@ -60,7 +60,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(activateCmd) settings.AddSkipConfirmation(activateCmd) - activateCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return activateCmd } diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 8dd48df4..c01818a8 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -29,7 +29,7 @@ type Inputs struct { } func New(runtimeContext *runtime.Context) *cobra.Command { - var force, nonInteractive bool + var force bool convertCmd := &cobra.Command{ Use: "custom-build ", Short: "Converts an existing workflow to a custom (self-compiled) build", @@ -37,6 +37,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Args: cobra.ExactArgs(1), Example: `cre workflow custom-build ./my-workflow`, RunE: func(cmd *cobra.Command, args []string) error { + nonInteractive, _ := cmd.Flags().GetBool(settings.Flags.NonInteractive.Name) handler := newHandler(runtimeContext) inputs := Inputs{ WorkflowFolder: args[0], @@ -47,7 +48,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { }, } convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt and convert immediately") - convertCmd.Flags().BoolVar(&nonInteractive, settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return convertCmd } diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index fb4d9b52..236a8d4f 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -60,7 +60,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(deleteCmd) settings.AddSkipConfirmation(deleteCmd) - deleteCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return deleteCmd } diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index b9a48c96..895cd0bd 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -109,7 +109,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(deployCmd) settings.AddSkipConfirmation(deployCmd) - deployCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") deployCmd.Flags().StringP("output", "o", defaultOutputPath, "The output file for the compiled WASM binary encoded in base64") deployCmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner (used during auto-link if owner is not already linked)") deployCmd.Flags().String("wasm", "", "Path to a pre-built WASM binary (skips compilation)") diff --git a/cmd/workflow/deploy/deploy_test.go b/cmd/workflow/deploy/deploy_test.go index 671df4ca..961f3eb8 100644 --- a/cmd/workflow/deploy/deploy_test.go +++ b/cmd/workflow/deploy/deploy_test.go @@ -411,18 +411,6 @@ func TestValidateInputs_URLBypass(t *testing.T) { }) } -func TestNonInteractiveFlagRegistered(t *testing.T) { - t.Parallel() - - simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) - defer simulatedEnvironment.Close() - - cmd := New(simulatedEnvironment.NewRuntimeContext()) - f := cmd.Flags().Lookup("non-interactive") - require.NotNil(t, f, "--non-interactive flag should be registered") - assert.Equal(t, "false", f.DefValue) -} - func TestNonInteractive_WorkflowExists_WithoutYes_ReturnsError(t *testing.T) { // Verify the guard logic: when NonInteractive=true and SkipConfirmation=false, // the overwrite confirmation path should return an error. diff --git a/cmd/workflow/pause/pause.go b/cmd/workflow/pause/pause.go index 0af378d9..a1564764 100644 --- a/cmd/workflow/pause/pause.go +++ b/cmd/workflow/pause/pause.go @@ -60,7 +60,6 @@ func New(runtimeContext *runtime.Context) *cobra.Command { settings.AddTxnTypeFlags(pauseCmd) settings.AddSkipConfirmation(pauseCmd) - pauseCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Fail instead of prompting; requires all inputs via flags") return pauseCmd } diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 771eef49..ffa5af34 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -96,8 +96,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.Flags().Bool("no-config", false, "Simulate without a config file") simulateCmd.Flags().Bool("default-config", false, "Use the config path from workflow.yaml settings (default behavior)") simulateCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config") - // Non-interactive flags - simulateCmd.Flags().Bool(settings.Flags.NonInteractive.Name, false, "Run without prompts; requires --trigger-index and inputs for the selected trigger type") + // Non-interactive trigger selection flags simulateCmd.Flags().Int("trigger-index", -1, "Index of the trigger to run (0-based)") simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix)") simulateCmd.Flags().String("evm-tx-hash", "", "EVM trigger transaction hash (0x...)")