Skip to content
55 changes: 36 additions & 19 deletions cmd/creinit/creinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,22 +196,30 @@ func (h *handler) Execute(inputs Inputs) error {
return fmt.Errorf("failed to fetch templates: %w", err)
}

// Filter to only workflow templates (category == "workflow")
var workflowTemplates []templaterepo.TemplateSummary
for _, t := range templates {
if t.Category == templaterepo.CategoryWorkflow {
workflowTemplates = append(workflowTemplates, t)
}
}

// Resolve template from flag if provided
var selectedTemplate *templaterepo.TemplateSummary
if inputs.TemplateName != "" {
for i := range templates {
if templates[i].Name == inputs.TemplateName {
selectedTemplate = &templates[i]
for i := range workflowTemplates {
if workflowTemplates[i].Name == inputs.TemplateName || workflowTemplates[i].ID == inputs.TemplateName {
selectedTemplate = &workflowTemplates[i]
break
}
}
if selectedTemplate == nil {
return fmt.Errorf("template %q not found", inputs.TemplateName)
return fmt.Errorf("template %q not found. Run 'cre templates list' to see all available templates", inputs.TemplateName)
}
}

// Run the interactive wizard
result, err := RunWizard(inputs, isNewProject, startDir, templates, selectedTemplate)
result, err := RunWizard(inputs, isNewProject, startDir, workflowTemplates, selectedTemplate)
if err != nil {
return fmt.Errorf("wizard error: %w", err)
}
Expand Down Expand Up @@ -287,22 +295,24 @@ func (h *handler) Execute(inputs Inputs) error {
return fmt.Errorf("failed to scaffold template: %w", err)
}

// Patch RPC URLs into project.yaml for all templates (including those with projectDir).
// Templates that ship their own project.yaml still need user-provided RPCs applied.
projectYAMLPath := filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)
if isNewProject && h.pathExists(projectYAMLPath) {
if err := settings.PatchProjectRPCs(projectYAMLPath, networkRPCs); err != nil {
return fmt.Errorf("failed to update RPC URLs in project.yaml: %w", err)
}
}

// Templates with projectDir provide their own project structure — skip config generation.
// Only built-in templates (no projectDir) need config files generated by the CLI.
if selectedTemplate.ProjectDir == "" {
// Handle project.yaml
projectYAMLPath := filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)
if isNewProject {
if h.pathExists(projectYAMLPath) {
if err := settings.PatchProjectRPCs(projectYAMLPath, networkRPCs); err != nil {
return fmt.Errorf("failed to update RPC URLs in project.yaml: %w", err)
}
} else {
networks := selectedTemplate.Networks
repl := settings.GetReplacementsWithNetworks(networks, networkRPCs)
if e := settings.FindOrCreateProjectSettings(projectRoot, repl); e != nil {
return e
}
// Generate project.yaml if the template didn't provide one
if isNewProject && !h.pathExists(projectYAMLPath) {
networks := selectedTemplate.Networks
repl := settings.GetReplacementsWithNetworks(networks, networkRPCs)
if e := settings.FindOrCreateProjectSettings(projectRoot, repl); e != nil {
return e
}
}

Expand Down Expand Up @@ -449,7 +459,14 @@ func (h *handler) printSuccessMessage(projectRoot string, tmpl *templaterepo.Tem
sb.WriteString(ui.RenderStep("2. Install Bun (if needed):") + "\n")
sb.WriteString(" " + ui.RenderDim("npm install -g bun") + "\n\n")
sb.WriteString(ui.RenderStep("3. Install dependencies:") + "\n")
sb.WriteString(" " + ui.RenderDim("bun install --cwd ./"+primaryWorkflow) + "\n\n")
if isMultiWorkflow {
for _, wf := range workflows {
sb.WriteString(" " + ui.RenderDim("bun install --cwd ./"+wf.Dir) + "\n")
}
} else {
sb.WriteString(" " + ui.RenderDim("bun install --cwd ./"+primaryWorkflow) + "\n")
}
sb.WriteString("\n")

if isMultiWorkflow {
sb.WriteString(ui.RenderStep("4. Run a workflow:") + "\n")
Expand Down
78 changes: 71 additions & 7 deletions cmd/creinit/creinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ var testGoTemplate = templaterepo.TemplateSummary{
Title: "Test Go Template",
Description: "A test Go template",
Language: "go",
Category: "test",
Category: "workflow",
Author: "Test",
License: "MIT",
Networks: []string{"ethereum-testnet-sepolia"},
Expand All @@ -146,7 +146,7 @@ var testTSTemplate = templaterepo.TemplateSummary{
Title: "Test TypeScript Template",
Description: "A test TypeScript template",
Language: "typescript",
Category: "test",
Category: "workflow",
Author: "Test",
License: "MIT",
Workflows: []templaterepo.WorkflowDirEntry{{Dir: "my-workflow"}},
Expand All @@ -166,7 +166,7 @@ var testStarterTemplate = templaterepo.TemplateSummary{
Title: "Starter Go Template",
Description: "A starter Go template",
Language: "go",
Category: "test",
Category: "workflow",
Author: "Test",
License: "MIT",
Workflows: []templaterepo.WorkflowDirEntry{{Dir: "my-workflow"}},
Expand All @@ -186,7 +186,7 @@ var testMultiNetworkTemplate = templaterepo.TemplateSummary{
Title: "Test Multi-Chain Template",
Description: "A template requiring multiple chains",
Language: "go",
Category: "test",
Category: "workflow",
Author: "Test",
License: "MIT",
Networks: []string{"ethereum-testnet-sepolia", "ethereum-mainnet"},
Expand All @@ -207,7 +207,7 @@ var testBuiltInGoTemplate = templaterepo.TemplateSummary{
Title: "Hello World (Go)",
Description: "A built-in Go template",
Language: "go",
Category: "getting-started",
Category: "workflow",
Author: "Test",
License: "MIT",
},
Expand All @@ -222,7 +222,7 @@ var testMultiWorkflowTemplate = templaterepo.TemplateSummary{
Title: "Bring Your Own Data (Go)",
Description: "Bring your own off-chain data on-chain with PoR and NAV publishing.",
Language: "go",
Category: "data-feeds",
Category: "workflow",
Author: "Test",
License: "MIT",
Networks: []string{"ethereum-testnet-sepolia"},
Expand All @@ -247,7 +247,7 @@ var testSingleWorkflowWithPostInit = templaterepo.TemplateSummary{
Title: "KV Store (Go)",
Description: "Read, increment, and write a counter in AWS S3.",
Language: "go",
Category: "off-chain-storage",
Category: "workflow",
Author: "Test",
License: "MIT",
Workflows: []templaterepo.WorkflowDirEntry{{Dir: "my-workflow"}},
Expand All @@ -261,6 +261,30 @@ var testSingleWorkflowWithPostInit = templaterepo.TemplateSummary{
},
}

var testProjectDirWithNetworks = templaterepo.TemplateSummary{
TemplateMetadata: templaterepo.TemplateMetadata{
Kind: "starter-template",
Name: "starter-with-projectdir",
Title: "Starter With ProjectDir",
Description: "A starter template that ships its own project structure",
Language: "typescript",
Category: "workflow",
Author: "Test",
License: "MIT",
ProjectDir: ".",
Networks: []string{"ethereum-testnet-sepolia", "ethereum-mainnet"},
Workflows: []templaterepo.WorkflowDirEntry{
{Dir: "my-workflow", Description: "Test workflow"},
},
},
Path: "starter-templates/test/starter-with-projectdir",
Source: templaterepo.RepoSource{
Owner: "test",
Repo: "templates",
Ref: "main",
},
}

func newMockRegistry() *mockRegistry {
return &mockRegistry{
templates: []templaterepo.TemplateSummary{
Expand All @@ -271,6 +295,7 @@ func newMockRegistry() *mockRegistry {
testBuiltInGoTemplate,
testMultiWorkflowTemplate,
testSingleWorkflowWithPostInit,
testProjectDirWithNetworks,
},
}
}
Expand Down Expand Up @@ -570,6 +595,45 @@ func TestInitRemoteTemplateKeepsProjectYAML(t *testing.T) {
require.Contains(t, string(envContent), "GITHUB_API_TOKEN=test-token")
}

func TestInitProjectDirTemplateRpcPatching(t *testing.T) {
sim := chainsim.NewSimulatedEnvironment(t)
defer sim.Close()

tempDir := t.TempDir()
restoreCwd, err := testutil.ChangeWorkingDirectory(tempDir)
require.NoError(t, err)
defer restoreCwd()

// Template with ProjectDir set AND Networks — the bug was that RPC URLs
// were silently dropped because the patching was inside the ProjectDir=="" block.
inputs := Inputs{
ProjectName: "projectDirProj",
TemplateName: "starter-with-projectdir",
WorkflowName: "my-workflow",
RpcURLs: map[string]string{
"ethereum-testnet-sepolia": "https://sepolia.custom.com",
"ethereum-mainnet": "https://mainnet.custom.com",
},
}

h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry())
require.NoError(t, h.ValidateInputs(inputs))
require.NoError(t, h.Execute(inputs))

projectRoot := filepath.Join(tempDir, "projectDirProj")
projectYAML, err := os.ReadFile(filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName))
require.NoError(t, err)
content := string(projectYAML)

// User-provided RPCs must be patched even though ProjectDir is set
require.Contains(t, content, "https://sepolia.custom.com",
"user RPC URL for sepolia should be patched into project.yaml for templates with ProjectDir")
require.Contains(t, content, "https://mainnet.custom.com",
"user RPC URL for mainnet should be patched into project.yaml for templates with ProjectDir")
require.NotContains(t, content, "https://default-rpc.example.com",
"mock default URLs should be replaced by user-provided URLs")
}

func TestTemplateNotFound(t *testing.T) {
sim := chainsim.NewSimulatedEnvironment(t)
defer sim.Close()
Expand Down
Loading
Loading