diff --git a/.github/workflows/internal-ci.yml b/.github/workflows/internal-ci.yml index 400d247..12b32db 100644 --- a/.github/workflows/internal-ci.yml +++ b/.github/workflows/internal-ci.yml @@ -39,7 +39,11 @@ jobs: if [[ -n $(git status -s) ]]; then git add . git commit -m "docs: update docs with PTerm-CI" - git push origin HEAD:${GITHUB_REF} + if [ "${{ github.event_name }}" == "pull_request" ]; then + git push origin HEAD:${{ github.head_ref }} + else + git push origin HEAD:${GITHUB_REF} + fi else echo "No changes to commit" fi diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/docs.md b/docs/docs.md index 392b31f..b8e38df 100755 --- a/docs/docs.md +++ b/docs/docs.md @@ -1043,4 +1043,4 @@ Run 'magi version --help' for more information on a specific command. --- -> **Documentation automatically generated with [PTerm](https://github.com/pterm/cli-template) on 06 February 2026** +> **Documentation automatically generated with [PTerm](https://github.com/pterm/cli-template) on 18 March 2026** diff --git a/internal/cli/crypto/salt_test.go b/internal/cli/crypto/salt_test.go index f301f16..789d718 100644 --- a/internal/cli/crypto/salt_test.go +++ b/internal/cli/crypto/salt_test.go @@ -52,13 +52,13 @@ func TestRunGenerateSalt(t *testing.T) { // Read captured output var buf bytes.Buffer -_, _ = buf.ReadFrom(r) -output := buf.String() + _, _ = buf.ReadFrom(r) + output := buf.String() -if tt.expectSkip { -assert.Empty(t, strings.TrimSpace(output)) -return -} + if tt.expectSkip { + assert.Empty(t, strings.TrimSpace(output)) + return + } // Verify lines := strings.Split(strings.TrimSpace(output), "\n") diff --git a/internal/cli/docker/compose/command.go b/internal/cli/docker/compose/command.go index 4ab3be1..21b8859 100644 --- a/internal/cli/docker/compose/command.go +++ b/internal/cli/docker/compose/command.go @@ -108,11 +108,15 @@ func runCompose(ctx context.Context, autoAccept bool) { func findDockerfiles() []string { var dockerfiles []string - filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + // ⚡ Bolt: Replaced filepath.Walk with filepath.WalkDir. + // filepath.WalkDir is more efficient as it avoids calling os.Lstat on every file + // or directory it visits, which significantly speeds up directory traversal, + // especially for large project structures. + filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error { if err != nil { return nil } - if !info.IsDir() && strings.HasSuffix(info.Name(), "Dockerfile") { + if !d.IsDir() && strings.HasSuffix(d.Name(), "Dockerfile") { // Use relative path relPath, _ := filepath.Rel(".", path) dockerfiles = append(dockerfiles, relPath) diff --git a/internal/cli/project/agents.go b/internal/cli/project/agents.go index 60617ed..9e9bb7a 100644 --- a/internal/cli/project/agents.go +++ b/internal/cli/project/agents.go @@ -104,7 +104,6 @@ Return the result in strictly valid JSON format matching this schema: return &result, nil } - // ValidatorAgent validates and corrects the AnalysisResult. type ValidatorAgent struct { runtime *shared.RuntimeContext @@ -116,30 +115,30 @@ func NewValidatorAgent(runtime *shared.RuntimeContext) *ValidatorAgent { // Validate checks the analysis result for common issues and attempts to fix them via LLM. func (v *ValidatorAgent) Validate(result *AnalysisResult) (*AnalysisResult, error) { - // 1. Check for invalid steps programmaticall first to save tokens - needsFix := false - var issues []string - - for _, action := range result.Actions { - for i, step := range action.Steps { - if step.Tool == "run_command" { - if _, ok := step.Parameters["command"]; !ok { - needsFix = true - issues = append(issues, fmt.Sprintf("Action '%s' Step %d ('%s'): Missing 'command' parameter in run_command.", action.Name, i+1, step.Instruction)) - } - } - } - } - - if !needsFix { - return result, nil - } + // 1. Check for invalid steps programmaticall first to save tokens + needsFix := false + var issues []string + + for _, action := range result.Actions { + for i, step := range action.Steps { + if step.Tool == "run_command" { + if _, ok := step.Parameters["command"]; !ok { + needsFix = true + issues = append(issues, fmt.Sprintf("Action '%s' Step %d ('%s'): Missing 'command' parameter in run_command.", action.Name, i+1, step.Instruction)) + } + } + } + } - // 2. Fix via LLM - resultJSON, _ := json.Marshal(result) - issuesStr := strings.Join(issues, "\n") - - systemPrompt := `You are a Strict Configuration Validator. + if !needsFix { + return result, nil + } + + // 2. Fix via LLM + resultJSON, _ := json.Marshal(result) + issuesStr := strings.Join(issues, "\n") + + systemPrompt := `You are a Strict Configuration Validator. Your task is to FIX the provided Project Analysis JSON based on the reported validity issues. Verify that all "run_command" steps have a "command" parameter with the actual executable shell command. If the "instruction" contains the command, move it to "parameters.command" and keep "instruction" as a description. @@ -149,7 +148,7 @@ Reported Issues: Return the CORRECTED JSON strictly matching the input schema.` - userPrompt := string(resultJSON) + userPrompt := string(resultJSON) service, err := llm.NewServiceBuilder(v.runtime).UseHeavyModel().Build() if err != nil { @@ -238,7 +237,7 @@ Schema: var plan FileGenerationPlan if err := json.Unmarshal([]byte(resp), &plan); err != nil { - // Fallback: try to find JSON block if strict JSON failed + // Fallback: try to find JSON block if strict JSON failed return nil, fmt.Errorf("failed to parse planning response: %w (%s)", err, resp) } @@ -275,11 +274,11 @@ If it is a go file, include the package declaration.`, architecture, projectType return nil, err } - // Strip markdown code blocks if present - content := strings.TrimSpace(resp) - content = strings.TrimPrefix(content, "```go") - content = strings.TrimPrefix(content, "```") - content = strings.TrimSuffix(content, "```") + // Strip markdown code blocks if present + content := strings.TrimSpace(resp) + content = strings.TrimPrefix(content, "```go") + content = strings.TrimPrefix(content, "```") + content = strings.TrimSuffix(content, "```") return &FileContent{ Path: file.Path, @@ -342,17 +341,17 @@ func NewReviewerAgent(runtime *shared.RuntimeContext) *ReviewerAgent { // ReviewCompliance checks if the project structure matches the rules. func (r *ReviewerAgent) ReviewCompliance(rootPath string, rulesContent string) (string, error) { - // 1. Get File Tree - // We reuse a similar file tree function or extract it to a helper. - // Since getFileTree is a method of ArchitectureAgent, let's copy or refactor. - // For simplicity I'll duplicate the walker logic here or make it a private function in agents.go - // assuming I can access it if I make it a function not method, or just duplicate. - // Let's refactor getFileTree to be a standalone function "getFileTree" in this package. - - fileTree, err := getFileTree(rootPath) - if err != nil { - return "", err - } + // 1. Get File Tree + // We reuse a similar file tree function or extract it to a helper. + // Since getFileTree is a method of ArchitectureAgent, let's copy or refactor. + // For simplicity I'll duplicate the walker logic here or make it a private function in agents.go + // assuming I can access it if I make it a function not method, or just duplicate. + // Let's refactor getFileTree to be a standalone function "getFileTree" in this package. + + fileTree, err := getFileTree(rootPath) + if err != nil { + return "", err + } // 2. Build Prompt systemPrompt := `You are an expert Software Architect. @@ -389,7 +388,7 @@ Format the output as a bulleted list of issues.` return resp, nil } -// Helper function to be shared. +// Helper function to be shared. // I need to change ArchitectureAgent.getFileTree to use this or be this. func getFileTree(root string) (string, error) { var sb strings.Builder @@ -417,4 +416,3 @@ func getFileTree(root string) (string, error) { }) return sb.String(), err } - diff --git a/internal/cli/project/check.go b/internal/cli/project/check.go index 09b8b82..e5a3789 100644 --- a/internal/cli/project/check.go +++ b/internal/cli/project/check.go @@ -15,13 +15,13 @@ func NewCheckCmd() *cobra.Command { return &cobra.Command{ Use: "check", Short: "Check compliance with project rules", - Long: `Verifies if the current project structure complies with the rules defined in AGENTS.md.`, - RunE: func(cmd *cobra.Command, args []string) error { + Long: `Verifies if the current project structure complies with the rules defined in AGENTS.md.`, + RunE: func(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get cwd: %w", err) } - + // 1. Find Rules file configPath := filepath.Join(cwd, ".magi.yaml") var rulesFile string = "AGENTS.md" // Default diff --git a/internal/cli/project/command.go b/internal/cli/project/command.go index c0c9e7d..c6d182e 100644 --- a/internal/cli/project/command.go +++ b/internal/cli/project/command.go @@ -36,7 +36,7 @@ Examples: cmd.AddCommand(NewCheckCmd()) cmd.AddCommand(NewUpdateCmd()) cmd.AddCommand(NewRedoCmd()) - cmd.AddCommand(NewListCmd()) + cmd.AddCommand(NewListCmd()) return cmd } diff --git a/internal/cli/project/command_test.go b/internal/cli/project/command_test.go index a1a554c..db4e4bc 100644 --- a/internal/cli/project/command_test.go +++ b/internal/cli/project/command_test.go @@ -17,14 +17,14 @@ func TestNewProjectCmd(t *testing.T) { // Check for expected subcommands expectedParams := []string{"init", "exec", "check", "update", "redo", "list"} - foundCount := 0 + foundCount := 0 for _, sub := range cmd.Commands() { for _, expected := range expectedParams { if sub.Name() == expected { foundCount++ - break + break } } } - assert.Equal(t, len(expectedParams), foundCount, "Not all expected subcommands found") + assert.Equal(t, len(expectedParams), foundCount, "Not all expected subcommands found") } diff --git a/internal/cli/project/exec.go b/internal/cli/project/exec.go index 4563809..9741934 100644 --- a/internal/cli/project/exec.go +++ b/internal/cli/project/exec.go @@ -19,138 +19,142 @@ func NewExecCmd() *cobra.Command { Long: `Executes a project action (e.g., create a slice, add a feature) defined in .magi.yaml.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get cwd: %w", err) - } - - // 1. Load Actions Logic - configPath := filepath.Join(cwd, ".magi.yaml") - if _, err := os.Stat(configPath); os.IsNotExist(err) { - pterm.Warning.Println(".magi.yaml not found. Run 'magi project init' first.") - return nil - } - - data, err := os.ReadFile(configPath) - if err != nil { - return fmt.Errorf("failed to read config: %w", err) - } - var config MagiConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("failed to parse config: %w", err) - } - - if len(config.Actions) == 0 { - pterm.Warning.Println("No actions defined in .magi.yaml. Run 'magi project init' to detect actions.") - return nil - } - - // 2. Select Action - var actionName string - if len(args) > 0 { - actionName = args[0] - } else { - var options []string - for _, a := range config.Actions { - options = append(options, a.Name) - } - actionName, _ = pterm.DefaultInteractiveSelect.WithOptions(options).Show("Select action to perform") - } - - var selectedAction *Action - for _, a := range config.Actions { - if a.Name == actionName { - selectedAction = &a - break - } - } - if selectedAction == nil { - return fmt.Errorf("action '%s' not found", actionName) - } - - // 3. Collect Parameters - params := make(map[string]string) - for _, p := range selectedAction.Parameters { - val, _ := pterm.DefaultInteractiveTextInput.Show(fmt.Sprintf("%s (%s)", p.Name, p.Description)) - params[p.Name] = val - } - - // 4. Execution Logic - runtime, err := shared.BuildRuntimeContext() - if err != nil { - return err - } - agent := NewGeneratorAgent(runtime) - - architecture := config.Architecture - if architecture == "" { architecture = "Go Project" } - projectType := config.ProjectType - if projectType == "" { projectType = "Standard" } - - // If action has defined steps, execute them - if len(selectedAction.Steps) > 0 { - executor := NewExecutor(runtime, cwd, architecture, projectType, *selectedAction, params) - if err := executor.ExecuteSteps(selectedAction.Steps); err != nil { - return err - } - pterm.Success.Println("Action completed successfully!") - return nil - } - - // Fallback to legacy Plan/Geneate - pterm.Info.Println("No steps defined. specific steps. Fallback to auto-planning...") - - // ... Legacy Logic ... - agent = NewGeneratorAgent(runtime) - plan, err := agent.PlanGeneration(cwd, architecture, projectType, *selectedAction, params) - if err != nil { - return fmt.Errorf("planning failed: %w", err) - } - - // 5. Confirm Plan - pterm.DefaultSection.Println("Generation Plan") - for _, f := range plan.Files { - pterm.Println(pterm.Green(" + ") + f.Path + pterm.Gray(" ("+f.Description+")")) - } - - confirm, _ := pterm.DefaultInteractiveConfirm.Show("Proceed with generation?") - if !confirm { - pterm.Info.Println("Aborted.") - return nil - } - - // 6. Generate and Write - progressBar, _ := pterm.DefaultProgressbar.WithTotal(len(plan.Files)).Start() - for _, f := range plan.Files { - progressBar.UpdateTitle("Generating " + f.Path) - content, err := agent.GenerateContent(cwd, architecture, projectType, *selectedAction, params, f) - if err != nil { - pterm.Error.Printf("Failed to generate %s: %v\n", f.Path, err) - continue - } - - fullPath := filepath.Join(cwd, f.Path) - if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { - pterm.Error.Printf("Failed to create dir for %s: %v\n", f.Path, err) - continue - } - - // Check overwrite - if _, err := os.Stat(fullPath); err == nil { - // In non-interactive mode or simply proceed for now as user confirmed plan. - // Ideally we verify individually but bulk confirm is standard for "create". - } - - if err := os.WriteFile(fullPath, []byte(content.Content), 0644); err != nil { - pterm.Error.Printf("Failed to write %s: %v\n", f.Path, err) - } - progressBar.Increment() - } - progressBar.Stop() - pterm.Success.Println("Generation complete!") + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get cwd: %w", err) + } + + // 1. Load Actions Logic + configPath := filepath.Join(cwd, ".magi.yaml") + if _, err := os.Stat(configPath); os.IsNotExist(err) { + pterm.Warning.Println(".magi.yaml not found. Run 'magi project init' first.") + return nil + } + + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read config: %w", err) + } + var config MagiConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + if len(config.Actions) == 0 { + pterm.Warning.Println("No actions defined in .magi.yaml. Run 'magi project init' to detect actions.") + return nil + } + + // 2. Select Action + var actionName string + if len(args) > 0 { + actionName = args[0] + } else { + var options []string + for _, a := range config.Actions { + options = append(options, a.Name) + } + actionName, _ = pterm.DefaultInteractiveSelect.WithOptions(options).Show("Select action to perform") + } + + var selectedAction *Action + for _, a := range config.Actions { + if a.Name == actionName { + selectedAction = &a + break + } + } + if selectedAction == nil { + return fmt.Errorf("action '%s' not found", actionName) + } + + // 3. Collect Parameters + params := make(map[string]string) + for _, p := range selectedAction.Parameters { + val, _ := pterm.DefaultInteractiveTextInput.Show(fmt.Sprintf("%s (%s)", p.Name, p.Description)) + params[p.Name] = val + } + + // 4. Execution Logic + runtime, err := shared.BuildRuntimeContext() + if err != nil { + return err + } + agent := NewGeneratorAgent(runtime) + + architecture := config.Architecture + if architecture == "" { + architecture = "Go Project" + } + projectType := config.ProjectType + if projectType == "" { + projectType = "Standard" + } + + // If action has defined steps, execute them + if len(selectedAction.Steps) > 0 { + executor := NewExecutor(runtime, cwd, architecture, projectType, *selectedAction, params) + if err := executor.ExecuteSteps(selectedAction.Steps); err != nil { + return err + } + pterm.Success.Println("Action completed successfully!") + return nil + } + + // Fallback to legacy Plan/Geneate + pterm.Info.Println("No steps defined. specific steps. Fallback to auto-planning...") + + // ... Legacy Logic ... + agent = NewGeneratorAgent(runtime) + plan, err := agent.PlanGeneration(cwd, architecture, projectType, *selectedAction, params) + if err != nil { + return fmt.Errorf("planning failed: %w", err) + } + + // 5. Confirm Plan + pterm.DefaultSection.Println("Generation Plan") + for _, f := range plan.Files { + pterm.Println(pterm.Green(" + ") + f.Path + pterm.Gray(" ("+f.Description+")")) + } + + confirm, _ := pterm.DefaultInteractiveConfirm.Show("Proceed with generation?") + if !confirm { + pterm.Info.Println("Aborted.") + return nil + } + + // 6. Generate and Write + progressBar, _ := pterm.DefaultProgressbar.WithTotal(len(plan.Files)).Start() + for _, f := range plan.Files { + progressBar.UpdateTitle("Generating " + f.Path) + content, err := agent.GenerateContent(cwd, architecture, projectType, *selectedAction, params, f) + if err != nil { + pterm.Error.Printf("Failed to generate %s: %v\n", f.Path, err) + continue + } + + fullPath := filepath.Join(cwd, f.Path) + if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { + pterm.Error.Printf("Failed to create dir for %s: %v\n", f.Path, err) + continue + } + + // Check overwrite + if _, err := os.Stat(fullPath); err == nil { + // In non-interactive mode or simply proceed for now as user confirmed plan. + // Ideally we verify individually but bulk confirm is standard for "create". + } + + if err := os.WriteFile(fullPath, []byte(content.Content), 0644); err != nil { + pterm.Error.Printf("Failed to write %s: %v\n", f.Path, err) + } + progressBar.Increment() + } + progressBar.Stop() + pterm.Success.Println("Generation complete!") return nil }, } - return cmd + return cmd } diff --git a/internal/cli/project/executor.go b/internal/cli/project/executor.go index a1b7949..de4c0f8 100644 --- a/internal/cli/project/executor.go +++ b/internal/cli/project/executor.go @@ -13,25 +13,25 @@ import ( // Executor handles the execution of action steps. type Executor struct { - Agent *GeneratorAgent - Runtime *shared.RuntimeContext - Cwd string - Architecture string - ProjectType string - CurrentAction Action - CurrentParams map[string]string + Agent *GeneratorAgent + Runtime *shared.RuntimeContext + Cwd string + Architecture string + ProjectType string + CurrentAction Action + CurrentParams map[string]string } // NewExecutor creates a new Executor. func NewExecutor(runtime *shared.RuntimeContext, cwd, arch, pType string, action Action, params map[string]string) *Executor { return &Executor{ - Agent: NewGeneratorAgent(runtime), - Runtime: runtime, - Cwd: cwd, - Architecture: arch, - ProjectType: pType, - CurrentAction: action, - CurrentParams: params, + Agent: NewGeneratorAgent(runtime), + Runtime: runtime, + Cwd: cwd, + Architecture: arch, + ProjectType: pType, + CurrentAction: action, + CurrentParams: params, } } @@ -98,7 +98,7 @@ func (e *Executor) handleCreateFile(step ActionStep) error { // handleEditFile handles editing existing files. func (e *Executor) handleEditFile(step ActionStep) error { targetFile := e.resolveVariable(step.Parameters["target"]) - + // If target is missing, try to resolve it via LLM or interactive prompt if targetFile == "" { // Use instruction to hint at the file. For now, interactive fallback. @@ -142,7 +142,7 @@ func (e *Executor) handleRunCommand(step ActionStep) error { } pterm.Info.Printf("Command: %s\n", cmdStr) - + if confirm, _ := pterm.DefaultInteractiveConfirm.WithDefaultValue(false).Show("Run this command?"); !confirm { pterm.Info.Println("Skipped command execution.") return nil @@ -153,7 +153,7 @@ func (e *Executor) handleRunCommand(step ActionStep) error { if len(parts) == 0 { return nil } - + cmd := exec.Command(parts[0], parts[1:]...) cmd.Dir = e.Cwd cmd.Stdout = os.Stdout diff --git a/internal/cli/project/init.go b/internal/cli/project/init.go index eda7513..77ec782 100644 --- a/internal/cli/project/init.go +++ b/internal/cli/project/init.go @@ -18,7 +18,7 @@ func NewInitCmd() *cobra.Command { Long: `Analyzes the current project structure and creates/updates the .magi.yaml configuration and AGENTS.md rules file. Uses AI to detect architecture and suggest actions.`, RunE: func(cmd *cobra.Command, args []string) error { - forceRules, _ := cmd.Flags().GetBool("force-rules") + forceRules, _ := cmd.Flags().GetBool("force-rules") // 1. Safety Confirm confirm, _ := pterm.DefaultInteractiveConfirm.Show("This will analyze your project using LLM (consuming tokens) and may create/overwrite .magi.yaml. Proceed?") @@ -30,8 +30,8 @@ and AGENTS.md rules file. Uses AI to detect architecture and suggest actions.`, return RunAnalysisAndConfig(true, forceRules) }, } - cmd.Flags().Bool("force-rules", false, "Force creation/overwrite of AGENTS.md rules file") - return cmd + cmd.Flags().Bool("force-rules", false, "Force creation/overwrite of AGENTS.md rules file") + return cmd } // RunAnalysisAndConfig shared logic for init and redo. @@ -58,17 +58,17 @@ func RunAnalysisAndConfig(createRules bool, forceRules bool) error { } spinner.Success("Analysis complete!") - // 2.5 Validation - validAgent := NewValidatorAgent(runtime) - spinnerVal, _ := pterm.DefaultSpinner.Start("Validating analysis results...") - analysis, err = validAgent.Validate(analysis) - if err != nil { - spinnerVal.Warning("Validation incomplete: " + err.Error()) - // Continue with original analysis instead of failing hard? - // Or fail? Let's log warning and proceed with potentially flawed analysis or original. - } else { - spinnerVal.Success("Validation complete!") - } + // 2.5 Validation + validAgent := NewValidatorAgent(runtime) + spinnerVal, _ := pterm.DefaultSpinner.Start("Validating analysis results...") + analysis, err = validAgent.Validate(analysis) + if err != nil { + spinnerVal.Warning("Validation incomplete: " + err.Error()) + // Continue with original analysis instead of failing hard? + // Or fail? Let's log warning and proceed with potentially flawed analysis or original. + } else { + spinnerVal.Success("Validation complete!") + } // 3. Log Results pterm.DefaultSection.Println("Project Analysis Result") @@ -122,33 +122,33 @@ func RunAnalysisAndConfig(createRules bool, forceRules bool) error { // 5. Create AGENTS.md if missing if createRules || forceRules { rulesPath := filepath.Join(cwd, config.RulesPath) - - // Check existence - rulesExist := false + + // Check existence + rulesExist := false if _, err := os.Stat(rulesPath); err == nil { - rulesExist = true - } - - shouldCreate := false - if forceRules { - shouldCreate = true - if rulesExist { - pterm.Info.Println("Forcing recreation of AGENTS.md...") - } - } else if !rulesExist { - createConfirm, _ := pterm.DefaultInteractiveConfirm.Show("Create default AGENTS.md?") - if createConfirm { - shouldCreate = true - } - } + rulesExist = true + } + + shouldCreate := false + if forceRules { + shouldCreate = true + if rulesExist { + pterm.Info.Println("Forcing recreation of AGENTS.md...") + } + } else if !rulesExist { + createConfirm, _ := pterm.DefaultInteractiveConfirm.Show("Create default AGENTS.md?") + if createConfirm { + shouldCreate = true + } + } if shouldCreate { - defaultRules := fmt.Sprintf("# %s Agent Rules\n\n## Architecture: %s\n## Type: %s\n\nAdd your project-specific rules here.", filepath.Base(cwd), analysis.Architecture, analysis.ProjectType) - if err := os.WriteFile(rulesPath, []byte(defaultRules), 0644); err != nil { - pterm.Error.Println("Failed to create AGENTS.md: " + err.Error()) - } else { - pterm.Success.Println("Created/Updated " + config.RulesPath) - } + defaultRules := fmt.Sprintf("# %s Agent Rules\n\n## Architecture: %s\n## Type: %s\n\nAdd your project-specific rules here.", filepath.Base(cwd), analysis.Architecture, analysis.ProjectType) + if err := os.WriteFile(rulesPath, []byte(defaultRules), 0644); err != nil { + pterm.Error.Println("Failed to create AGENTS.md: " + err.Error()) + } else { + pterm.Success.Println("Created/Updated " + config.RulesPath) + } } } diff --git a/internal/cli/project/models.go b/internal/cli/project/models.go index 87fe722..64e47bc 100644 --- a/internal/cli/project/models.go +++ b/internal/cli/project/models.go @@ -4,9 +4,9 @@ package project type MagiConfig struct { Actions []Action `mapstructure:"actions" yaml:"actions"` RulesPath string `mapstructure:"rules_path" yaml:"rules_path"` - Architecture string `mapstructure:"architecture" yaml:"architecture"` - ProjectType string `mapstructure:"project_type" yaml:"project_type"` - Remaining map[string]interface{} `mapstructure:",remain" yaml:",inline"` + Architecture string `mapstructure:"architecture" yaml:"architecture"` + ProjectType string `mapstructure:"project_type" yaml:"project_type"` + Remaining map[string]interface{} `mapstructure:",remain" yaml:",inline"` } // Action defines a project-specific action (e.g., creating a slice, service, etc.). @@ -14,7 +14,7 @@ type Action struct { Name string `mapstructure:"name" yaml:"name"` Description string `mapstructure:"description" yaml:"description"` Parameters []ActionParameter `mapstructure:"parameters" yaml:"parameters"` - Steps []ActionStep `mapstructure:"steps" yaml:"steps"` + Steps []ActionStep `mapstructure:"steps" yaml:"steps"` } // ActionParameter defines a parameter for an action. @@ -22,12 +22,12 @@ type ActionParameter struct { Name string `mapstructure:"name" yaml:"name"` Description string `mapstructure:"description" yaml:"description"` Type string `mapstructure:"type" yaml:"type"` // string, boolean, etc. - Required bool `mapstructure:"required" yaml:"required"` + Required bool `mapstructure:"required" yaml:"required"` } // ActionStep defines a specific step in an action workflow. type ActionStep struct { - Tool string `mapstructure:"tool" yaml:"tool"` // create_file, edit_file, etc. - Instruction string `mapstructure:"instruction" yaml:"instruction"` // What to do - Parameters map[string]string `mapstructure:"parameters" yaml:"parameters"` // Additional static params if needed + Tool string `mapstructure:"tool" yaml:"tool"` // create_file, edit_file, etc. + Instruction string `mapstructure:"instruction" yaml:"instruction"` // What to do + Parameters map[string]string `mapstructure:"parameters" yaml:"parameters"` // Additional static params if needed } diff --git a/internal/cli/project/redo.go b/internal/cli/project/redo.go index 1dab6b0..bc2639a 100644 --- a/internal/cli/project/redo.go +++ b/internal/cli/project/redo.go @@ -11,20 +11,20 @@ func NewRedoCmd() *cobra.Command { Short: "Re-analyze project structure", Long: `Re-runs the project analysis to identify new structures or actions. Updates .magi.yaml with findings.`, - RunE: func(cmd *cobra.Command, args []string) error { - forceRules, _ := cmd.Flags().GetBool("force-rules") + RunE: func(cmd *cobra.Command, args []string) error { + forceRules, _ := cmd.Flags().GetBool("force-rules") - // Safety Confirm + // Safety Confirm confirm, _ := pterm.DefaultInteractiveConfirm.Show("This will re-analyze your project using LLM and update .magi.yaml. Proceed?") if !confirm { pterm.Info.Println("Aborted by user.") return nil } - // Reuse init logic + // Reuse init logic return RunAnalysisAndConfig(false, forceRules) }, } - cmd.Flags().Bool("force-rules", false, "Force creation/overwrite of AGENTS.md rules file") - return cmd + cmd.Flags().Bool("force-rules", false, "Force creation/overwrite of AGENTS.md rules file") + return cmd } diff --git a/internal/cli/project/update.go b/internal/cli/project/update.go index c2b0e01..0d40226 100644 --- a/internal/cli/project/update.go +++ b/internal/cli/project/update.go @@ -24,79 +24,79 @@ Requires the file path as an argument.`, return fmt.Errorf("failed to get cwd: %w", err) } - // 1. Get Target File - var targetFile string - if len(args) > 0 { - targetFile = args[0] - } else { - targetFile, _ = pterm.DefaultInteractiveTextInput.Show("Enter file path to update") - } + // 1. Get Target File + var targetFile string + if len(args) > 0 { + targetFile = args[0] + } else { + targetFile, _ = pterm.DefaultInteractiveTextInput.Show("Enter file path to update") + } - if targetFile == "" { - return fmt.Errorf("file path is required") - } + if targetFile == "" { + return fmt.Errorf("file path is required") + } - fullPath := filepath.Join(cwd, targetFile) - content, err := os.ReadFile(fullPath) - if err != nil { - return fmt.Errorf("failed to read file '%s': %w", targetFile, err) - } + fullPath := filepath.Join(cwd, targetFile) + content, err := os.ReadFile(fullPath) + if err != nil { + return fmt.Errorf("failed to read file '%s': %w", targetFile, err) + } - // 2. Get Instruction - instruction, _ := pterm.DefaultInteractiveTextInput.Show("What changes should be made?") - if instruction == "" { - pterm.Warning.Println("No instruction provided.") - return nil - } + // 2. Get Instruction + instruction, _ := pterm.DefaultInteractiveTextInput.Show("What changes should be made?") + if instruction == "" { + pterm.Warning.Println("No instruction provided.") + return nil + } - // 3. Load Config for Context - configPath := filepath.Join(cwd, ".magi.yaml") - architecture := "Go Project" - projectType := "Standard" + // 3. Load Config for Context + configPath := filepath.Join(cwd, ".magi.yaml") + architecture := "Go Project" + projectType := "Standard" if _, err := os.Stat(configPath); err == nil { data, err := os.ReadFile(configPath) if err == nil { var config MagiConfig if err := yaml.Unmarshal(data, &config); err == nil { - if config.Architecture != "" { - architecture = config.Architecture - } - if config.ProjectType != "" { - projectType = config.ProjectType - } + if config.Architecture != "" { + architecture = config.Architecture + } + if config.ProjectType != "" { + projectType = config.ProjectType + } } } } - // 4. Run Update - pterm.Info.Println("Generating updates...") - runtime, err := shared.BuildRuntimeContext() + // 4. Run Update + pterm.Info.Println("Generating updates...") + runtime, err := shared.BuildRuntimeContext() if err != nil { return err } - agent := NewGeneratorAgent(runtime) - updatedFile, err := agent.UpdateContent(targetFile, string(content), instruction, architecture, projectType) - if err != nil { - return fmt.Errorf("update failed: %w", err) - } + agent := NewGeneratorAgent(runtime) + updatedFile, err := agent.UpdateContent(targetFile, string(content), instruction, architecture, projectType) + if err != nil { + return fmt.Errorf("update failed: %w", err) + } + + // 5. Confirm and Write + pterm.Println() + pterm.DefaultSection.Println("Proposed Changes") + // TODO: In the future, show a helper diff here? For now, we rely on user trust/git. + pterm.Info.Printf("File: %s\n", updatedFile.Path) + pterm.Info.Println("Content Length:", len(updatedFile.Content)) - // 5. Confirm and Write - pterm.Println() - pterm.DefaultSection.Println("Proposed Changes") - // TODO: In the future, show a helper diff here? For now, we rely on user trust/git. - pterm.Info.Printf("File: %s\n", updatedFile.Path) - pterm.Info.Println("Content Length:", len(updatedFile.Content)) - - confirm, _ := pterm.DefaultInteractiveConfirm.Show("Apply changes?") - if confirm { - if err := os.WriteFile(fullPath, []byte(updatedFile.Content), 0644); err != nil { - return fmt.Errorf("failed to write file: %w", err) - } - pterm.Success.Println("File updated successfully.") - } else { - pterm.Warning.Println("Changes discarded.") - } + confirm, _ := pterm.DefaultInteractiveConfirm.Show("Apply changes?") + if confirm { + if err := os.WriteFile(fullPath, []byte(updatedFile.Content), 0644); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + pterm.Success.Println("File updated successfully.") + } else { + pterm.Warning.Println("Changes discarded.") + } return nil }, diff --git a/internal/cli/project/update_test.go b/internal/cli/project/update_test.go index bab013f..c633a80 100644 --- a/internal/cli/project/update_test.go +++ b/internal/cli/project/update_test.go @@ -14,7 +14,7 @@ func TestNewUpdateCmd(t *testing.T) { assert.NotEmpty(t, cmd.Short) assert.NotEmpty(t, cmd.Long) assert.NotNil(t, cmd.RunE) - // Check args validation - assert.Error(t, cmd.Args(&cobra.Command{}, []string{"a", "b"})) // Max 1 arg - assert.NoError(t, cmd.Args(&cobra.Command{}, []string{"a"})) + // Check args validation + assert.Error(t, cmd.Args(&cobra.Command{}, []string{"a", "b"})) // Max 1 arg + assert.NoError(t, cmd.Args(&cobra.Command{}, []string{"a"})) }