Skip to content
44 changes: 30 additions & 14 deletions cmd/creinit/creinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ import (
"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/templateconfig"
"github.com/smartcontractkit/cre-cli/internal/templaterepo"
"github.com/smartcontractkit/cre-cli/internal/tenantctx"
"github.com/smartcontractkit/cre-cli/internal/ui"
"github.com/smartcontractkit/cre-cli/internal/validation"
)

type Inputs struct {
ProjectName string `validate:"omitempty,project_name" cli:"project-name"`
TemplateName string `validate:"omitempty" cli:"template"`
WorkflowName string `validate:"omitempty,workflow_name" cli:"workflow-name"`
RpcURLs map[string]string // chain-name -> url, from --rpc-url flags
NonInteractive bool
ProjectRoot string // from -R / --project-root flag
ProjectName string `validate:"omitempty,project_name" cli:"project-name"`
TemplateName string `validate:"omitempty" cli:"template"`
WorkflowName string `validate:"omitempty,workflow_name" cli:"workflow-name"`
RpcURLs map[string]string // chain-name -> url, from --rpc-url flags
DeploymentRegistry string // from --deployment-registry flag
NonInteractive bool
ProjectRoot string // from -R / --project-root flag
}

func New(runtimeContext *runtime.Context) *cobra.Command {
Expand Down Expand Up @@ -75,6 +77,7 @@ 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().String("deployment-registry", "", "Registry ID to deploy workflows to (e.g. my-private-registry or onchain:ethereum-testnet-sepolia)")

// Deprecated: --template-id is kept for backwards compatibility, maps to hello-world-go
initCmd.Flags().Uint32("template-id", 0, "")
Expand Down Expand Up @@ -141,11 +144,12 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) {
}

return Inputs{
ProjectName: v.GetString("project-name"),
TemplateName: templateName,
WorkflowName: v.GetString("workflow-name"),
RpcURLs: rpcURLs,
NonInteractive: v.GetBool("non-interactive"),
ProjectName: v.GetString("project-name"),
TemplateName: templateName,
WorkflowName: v.GetString("workflow-name"),
RpcURLs: rpcURLs,
DeploymentRegistry: v.GetString("deployment-registry"),
NonInteractive: v.GetBool("non-interactive"),
}, nil
}

Expand Down Expand Up @@ -270,7 +274,18 @@ func (h *handler) Execute(inputs Inputs) error {
}

// Run the interactive wizard
result, err := RunWizard(inputs, isNewProject, startDir, workflowTemplates, selectedTemplate)
if inputs.NonInteractive && inputs.DeploymentRegistry == "" {
ui.Warning("No --deployment-registry specified, defaulting to on-chain registry. Add --deployment-registry to make this explicit.")
}

var registries []*tenantctx.Registry
if h.runtimeContext.TenantContext != nil {
registries = h.runtimeContext.TenantContext.Registries
}
if len(registries) == 0 && !inputs.NonInteractive {
ui.Warning("No registries found for your organization — skipping registry selection. Run `cre registry list` to check.")
}
result, err := RunWizard(inputs, isNewProject, startDir, workflowTemplates, selectedTemplate, registries)
if err != nil {
// If stdin is not a terminal, the wizard will fail trying to open a TTY.
// Detect this via term.IsTerminal rather than matching third-party error strings.
Expand Down Expand Up @@ -301,6 +316,7 @@ func (h *handler) Execute(inputs Inputs) error {
// Extract values from wizard result
projName := result.ProjectName
workflowName := result.WorkflowName
deploymentRegistry := result.RegistryID

// Apply defaults
if projName == "" {
Expand Down Expand Up @@ -413,7 +429,7 @@ func (h *handler) Execute(inputs Inputs) error {
h.log.Debug().Msgf("Skipping workflow.yaml generation for %s (already exists from template)", wf.Dir)
continue
}
if _, err := settings.GenerateWorkflowSettingsFile(wfDir, wf.Dir, entryPoint); err != nil {
if _, err := settings.GenerateWorkflowSettingsFile(wfDir, wf.Dir, entryPoint, deploymentRegistry); err != nil {
return fmt.Errorf("failed to generate workflow settings for %s: %w", wf.Dir, err)
}
}
Expand All @@ -422,7 +438,7 @@ func (h *handler) Execute(inputs Inputs) error {
wfSettingsPath := filepath.Join(workflowDirectory, constants.DefaultWorkflowSettingsFileName)
if _, err := os.Stat(wfSettingsPath); err == nil {
h.log.Debug().Msgf("Skipping workflow.yaml generation (already exists from template)")
} else if _, err := settings.GenerateWorkflowSettingsFile(workflowDirectory, workflowName, entryPoint); err != nil {
} else if _, err := settings.GenerateWorkflowSettingsFile(workflowDirectory, workflowName, entryPoint, deploymentRegistry); err != nil {
return fmt.Errorf("failed to generate %s file: %w", constants.DefaultWorkflowSettingsFileName, err)
}
}
Expand Down
144 changes: 81 additions & 63 deletions cmd/creinit/creinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,10 +416,11 @@ func TestInitExecuteFlows(t *testing.T) {
defer restoreCwd()

inputs := Inputs{
ProjectName: tc.projectNameFlag,
TemplateName: tc.templateNameFlag,
WorkflowName: tc.workflowNameFlag,
RpcURLs: tc.rpcURLs,
ProjectName: tc.projectNameFlag,
TemplateName: tc.templateNameFlag,
WorkflowName: tc.workflowNameFlag,
RpcURLs: tc.rpcURLs,
DeploymentRegistry: "private",
}

ctx := sim.NewRuntimeContext()
Expand Down Expand Up @@ -451,10 +452,11 @@ func TestInsideExistingProjectAddsWorkflow(t *testing.T) {
_ = os.Remove(constants.DefaultEnvFileName)

inputs := Inputs{
ProjectName: "",
TemplateName: "test-go",
WorkflowName: "wf-inside-existing-project",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectName: "",
TemplateName: "test-go",
WorkflowName: "wf-inside-existing-project",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -483,9 +485,10 @@ func TestInitWithTypescriptTemplateSkipsGoScaffold(t *testing.T) {
defer restoreCwd()

inputs := Inputs{
ProjectName: "tsProj",
TemplateName: "test-ts",
WorkflowName: "ts-workflow-01",
ProjectName: "tsProj",
TemplateName: "test-ts",
WorkflowName: "ts-workflow-01",
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -514,14 +517,16 @@ func TestInitWithRpcUrlFlags(t *testing.T) {
require.NoError(t, err)
defer restoreCwd()

rpcURLs := map[string]string{
"ethereum-testnet-sepolia": "https://sepolia.example.com",
"ethereum-mainnet": "https://mainnet.example.com",
}
inputs := Inputs{
ProjectName: "rpcProj",
TemplateName: "test-multichain",
WorkflowName: "rpc-workflow",
RpcURLs: map[string]string{
"ethereum-testnet-sepolia": "https://sepolia.example.com",
"ethereum-mainnet": "https://mainnet.example.com",
},
ProjectName: "rpcProj",
TemplateName: "test-multichain",
WorkflowName: "rpc-workflow",
RpcURLs: rpcURLs,
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -554,9 +559,10 @@ func TestInitNoNetworksFallsBackToDefault(t *testing.T) {
// Built-in template has no project.yaml from scaffold,
// so the CLI generates one with default networks.
inputs := Inputs{
ProjectName: "defaultProj",
TemplateName: "hello-world-go",
WorkflowName: "default-wf",
ProjectName: "defaultProj",
TemplateName: "hello-world-go",
WorkflowName: "default-wf",
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand All @@ -583,9 +589,10 @@ func TestInitRemoteTemplateKeepsProjectYAML(t *testing.T) {
// Remote template (test-ts) has no Networks — mock creates project.yaml with default chain.
// CLI should preserve the template's project.yaml (no patching needed since no user RPCs).
inputs := Inputs{
ProjectName: "remoteProj",
TemplateName: "test-ts",
WorkflowName: "ts-wf",
ProjectName: "remoteProj",
TemplateName: "test-ts",
WorkflowName: "ts-wf",
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -625,6 +632,7 @@ func TestInitProjectDirTemplateRpcPatching(t *testing.T) {
"ethereum-testnet-sepolia": "https://sepolia.custom.com",
"ethereum-mainnet": "https://mainnet.custom.com",
},
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -655,9 +663,10 @@ func TestTemplateNotFound(t *testing.T) {
defer restoreCwd()

inputs := Inputs{
ProjectName: "proj",
TemplateName: "nonexistent-template",
WorkflowName: "wf",
ProjectName: "proj",
TemplateName: "nonexistent-template",
WorkflowName: "wf",
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand All @@ -679,10 +688,11 @@ func TestMultiWorkflowNoRename(t *testing.T) {

// Multi-workflow template: no --workflow-name needed, dirs stay as declared
inputs := Inputs{
ProjectName: "multiProj",
TemplateName: "bring-your-own-data-go",
WorkflowName: "",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectName: "multiProj",
TemplateName: "bring-your-own-data-go",
WorkflowName: "",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -713,10 +723,11 @@ func TestMultiWorkflowIgnoresWorkflowNameFlag(t *testing.T) {

// Multi-workflow with --workflow-name flag: flag should be ignored
inputs := Inputs{
ProjectName: "multiProj2",
TemplateName: "bring-your-own-data-go",
WorkflowName: "test-rename",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectName: "multiProj2",
TemplateName: "bring-your-own-data-go",
WorkflowName: "test-rename",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -748,9 +759,10 @@ func TestSingleWorkflowDefaultFromTemplate(t *testing.T) {
// Note: We must provide a workflow name to avoid the TTY prompt in tests.
// Instead, we verify the default logic by providing it explicitly.
inputs := Inputs{
ProjectName: "singleProj",
TemplateName: "kv-store-go",
WorkflowName: "my-workflow", // same as template's workflows[0].dir
ProjectName: "singleProj",
TemplateName: "kv-store-go",
WorkflowName: "my-workflow", // same as template's workflows[0].dir
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -797,9 +809,10 @@ func TestSingleWorkflowRenameWithFlag(t *testing.T) {

// Single workflow with --workflow-name: should rename to user's choice
inputs := Inputs{
ProjectName: "renameProj",
TemplateName: "kv-store-go",
WorkflowName: "counter",
ProjectName: "renameProj",
TemplateName: "kv-store-go",
WorkflowName: "counter",
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -827,9 +840,10 @@ func TestBuiltInTemplateBackwardsCompat(t *testing.T) {

// Built-in template has no Workflows field — should use existing heuristic
inputs := Inputs{
ProjectName: "builtinProj",
TemplateName: "hello-world-go",
WorkflowName: "hello-wf",
ProjectName: "builtinProj",
TemplateName: "hello-world-go",
WorkflowName: "hello-wf",
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand Down Expand Up @@ -906,11 +920,12 @@ func TestNonInteractiveMissingFlags(t *testing.T) {
defer restoreCwd()

inputs := Inputs{
ProjectName: "proj",
TemplateName: "test-multichain",
WorkflowName: "",
NonInteractive: true,
RpcURLs: map[string]string{},
ProjectName: "proj",
TemplateName: "test-multichain",
WorkflowName: "",
NonInteractive: true,
RpcURLs: map[string]string{},
DeploymentRegistry: "private",
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand All @@ -930,10 +945,11 @@ func TestNonInteractiveAllFlagsProvided(t *testing.T) {
defer restoreCwd()

inputs := Inputs{
ProjectName: "niProj",
TemplateName: "hello-world-go",
WorkflowName: "my-wf",
NonInteractive: true,
ProjectName: "niProj",
TemplateName: "hello-world-go",
WorkflowName: "my-wf",
DeploymentRegistry: "onchain:ethereum-testnet-sepolia",
NonInteractive: true,
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
Expand All @@ -958,11 +974,12 @@ func TestInitRespectsProjectRootFlag(t *testing.T) {
targetDir := t.TempDir()

inputs := Inputs{
ProjectName: "myproj",
TemplateName: "test-go",
WorkflowName: "mywf",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectRoot: targetDir,
ProjectName: "myproj",
TemplateName: "test-go",
WorkflowName: "mywf",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectRoot: targetDir,
DeploymentRegistry: "private",
}

ctx := sim.NewRuntimeContext()
Expand Down Expand Up @@ -1003,11 +1020,12 @@ func TestInitProjectRootFlagFindsExistingProject(t *testing.T) {
))

inputs := Inputs{
ProjectName: "",
TemplateName: "test-go",
WorkflowName: "new-workflow",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectRoot: existingProject,
ProjectName: "",
TemplateName: "test-go",
WorkflowName: "new-workflow",
RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"},
ProjectRoot: existingProject,
DeploymentRegistry: "private",
}

ctx := sim.NewRuntimeContext()
Expand Down
Loading
Loading