From 85ff4b7fde7093b34e3eb00c99c4115e19f32f92 Mon Sep 17 00:00:00 2001 From: Kunal Kushwaha Date: Mon, 22 Jun 2026 00:21:02 +0900 Subject: [PATCH] feat(init): implement interactive wizard for `agk init -i` The `-i/--interactive` flag was parsed but never did anything. This wires it to a guided setup flow that prompts for project name, template, LLM provider, and description, then scaffolds the chosen project. - stdin/stdout-driven prompter (decoupled for testing) with number-or-name choice selection and safe defaults (EOF falls back to defaults, no hang). - `agk init -i` may be run with no project-name arg; the wizard asks for it. - Unit tests cover the wizard flow, name prompting, EOF defaults, and invalid-then-valid choice handling. Co-Authored-By: Claude Opus 4.8 --- README.md | 5 ++- cmd/init.go | 24 +++++++++-- cmd/init_wizard.go | 91 +++++++++++++++++++++++++++++++++++++++++ cmd/init_wizard_test.go | 63 ++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 cmd/init_wizard.go create mode 100644 cmd/init_wizard_test.go diff --git a/README.md b/README.md index 375d8a7..c84b438 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ go build -o agk main.go # Initialize a new project with the quickstart template ./agk init my-agent --template quickstart --llm openai +# ...or let the interactive wizard guide you through template + provider +./agk init -i + # Navigate to the project cd my-agent @@ -214,6 +217,7 @@ agk trace mermaid > trace_flow.md - **Smart Scaffolding** (Quickstart, Workflow bases) - **Eval Framework** (Semantic matching, LLM-as-judge, professional reports) - **Trace System** (Interactive TUI, Mermaid export, detailed spans) +- **Interactive Init Wizard** (`agk init -i` — guided template & provider setup) - **Streaming Support** (Native across all templates) ### In Progress @@ -224,7 +228,6 @@ agk trace mermaid > trace_flow.md - **Template Distribution** (`pack`, `push`) - **Cloud Deployment Engine** (`agk deploy`) - **Workflow Visualization** (Interactive graph editor) -- **Interactive Init Wizard** (`agk init -i`) - **MCP Server Management** - **RAG & Knowledge Base Management** diff --git a/cmd/init.go b/cmd/init.go index 5fcbfcc..82d2285 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -70,9 +70,10 @@ Examples: # List available templates agk init --list`, Args: func(cmd *cobra.Command, args []string) error { - // Allow zero args only when listing templates - if initListTemplates { - return nil + // Allow zero args when listing templates or in interactive mode + // (the wizard prompts for the project name). + if initListTemplates || initInteractive { + return cobra.MaximumNArgs(1)(cmd, args) } return cobra.ExactArgs(1)(cmd, args) }, @@ -94,7 +95,22 @@ func runInitCommand(cmd *cobra.Command, args []string) error { return nil } - projectName := args[0] + projectName := "" + if len(args) > 0 { + projectName = args[0] + } + + // Interactive mode: walk the user through setup and fill in any unset options. + if initInteractive { + res := runInitWizard(os.Stdin, os.Stdout, projectName) + projectName = res.ProjectName + initTemplate = res.Template + initLLMProvider = res.LLMProvider + if res.Description != "" { + initDescription = res.Description + } + } + span.SetAttributes( attribute.String("project_name", projectName), attribute.String("template", initTemplate), diff --git a/cmd/init_wizard.go b/cmd/init_wizard.go new file mode 100644 index 0000000..eb5beb0 --- /dev/null +++ b/cmd/init_wizard.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" +) + +// InitWizardResult holds the answers collected by the interactive init wizard. +type InitWizardResult struct { + ProjectName string + Template string + LLMProvider string + Description string +} + +// prompter reads typed answers from a reader and writes prompts to a writer. +// It is decoupled from os.Stdin/os.Stdout so the wizard can be unit-tested. +type prompter struct { + reader *bufio.Reader + out io.Writer +} + +func newPrompter(in io.Reader, out io.Writer) *prompter { + return &prompter{reader: bufio.NewReader(in), out: out} +} + +// ask prompts for a free-text value, returning def when the user enters nothing +// (or input ends). +func (p *prompter) ask(label, def string) string { + if def != "" { + fmt.Fprintf(p.out, "%s [%s]: ", label, def) + } else { + fmt.Fprintf(p.out, "%s: ", label) + } + line, _ := p.reader.ReadString('\n') + line = strings.TrimSpace(line) + if line == "" { + return def + } + return line +} + +// choose prompts the user to pick one of options, by number or name. def must be a +// member of options and is returned on empty input or EOF, so this never loops forever. +func (p *prompter) choose(label string, options []string, def string) string { + fmt.Fprintln(p.out, label) + for i, o := range options { + marker := " " + if o == def { + marker = "*" + } + fmt.Fprintf(p.out, " %s %d) %s\n", marker, i+1, o) + } + + for { + ans := p.ask("Choose (number or name)", def) + if n, err := strconv.Atoi(ans); err == nil && n >= 1 && n <= len(options) { + return options[n-1] + } + for _, o := range options { + if strings.EqualFold(o, ans) { + return o + } + } + fmt.Fprintf(p.out, " invalid choice: %q\n", ans) + // On EOF, ask() returns def (a valid option), so the loop terminates. + } +} + +// runInitWizard walks the user through project setup, pre-filling the project name +// when one was already provided on the command line. +func runInitWizard(in io.Reader, out io.Writer, name string) InitWizardResult { + fmt.Fprintln(out, "🧙 AGK interactive project setup") + fmt.Fprintln(out) + + p := newPrompter(in, out) + + res := InitWizardResult{ProjectName: name} + if res.ProjectName == "" { + res.ProjectName = p.ask("Project name", "my-agent") + } + res.Template = p.choose("Template:", []string{"quickstart", "workflow"}, "quickstart") + res.LLMProvider = p.choose("LLM provider:", []string{"openai", "anthropic", "ollama"}, "openai") + res.Description = p.ask("Description (optional)", "") + + fmt.Fprintln(out) + return res +} diff --git a/cmd/init_wizard_test.go b/cmd/init_wizard_test.go new file mode 100644 index 0000000..e0a3997 --- /dev/null +++ b/cmd/init_wizard_test.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" +) + +func TestRunInitWizard(t *testing.T) { + // name pre-filled; choose workflow (by number), ollama (by name), with a description. + in := strings.NewReader("2\nollama\nA test agent\n") + var out bytes.Buffer + + res := runInitWizard(in, &out, "my-proj") + + if res.ProjectName != "my-proj" { + t.Errorf("ProjectName = %q, want my-proj", res.ProjectName) + } + if res.Template != "workflow" { + t.Errorf("Template = %q, want workflow", res.Template) + } + if res.LLMProvider != "ollama" { + t.Errorf("LLMProvider = %q, want ollama", res.LLMProvider) + } + if res.Description != "A test agent" { + t.Errorf("Description = %q, want 'A test agent'", res.Description) + } +} + +func TestRunInitWizardPromptsForName(t *testing.T) { + // No name pre-filled: first answer is the project name; then accept defaults. + in := strings.NewReader("cool-bot\n\n\n\n") + var out bytes.Buffer + + res := runInitWizard(in, &out, "") + + if res.ProjectName != "cool-bot" { + t.Errorf("ProjectName = %q, want cool-bot", res.ProjectName) + } + // Empty answers should fall back to the defaults. + if res.Template != "quickstart" { + t.Errorf("Template = %q, want quickstart (default)", res.Template) + } + if res.LLMProvider != "openai" { + t.Errorf("LLMProvider = %q, want openai (default)", res.LLMProvider) + } +} + +func TestRunInitWizardEOFUsesDefaults(t *testing.T) { + // Empty input (immediate EOF): name default + all defaults, no infinite loop. + res := runInitWizard(strings.NewReader(""), new(bytes.Buffer), "") + if res.ProjectName != "my-agent" || res.Template != "quickstart" || res.LLMProvider != "openai" { + t.Errorf("unexpected defaults: %+v", res) + } +} + +func TestPrompterChooseInvalidThenValid(t *testing.T) { + p := newPrompter(strings.NewReader("nope\nworkflow\n"), new(bytes.Buffer)) + got := p.choose("Template:", []string{"quickstart", "workflow"}, "quickstart") + if got != "workflow" { + t.Errorf("choose = %q, want workflow", got) + } +}