From c2597e1f35288ca694a25999f0f199348c182a41 Mon Sep 17 00:00:00 2001
From: MagdielCAS <7864626+MagdielCAS@users.noreply.github.com>
Date: Wed, 1 Apr 2026 08:30:11 +0000
Subject: [PATCH 1/3] perf: move regexp.MustCompile to package level
Moves multiple instances of `regexp.MustCompile` from inside function bodies to package-level variables across the codebase.
Functions affected:
- `Execute` in `internal/cli/i18n/agents.go`
- `promptForAlias` in `internal/cli/ssh/add.go`
- `RemoveCodeBlock` in `pkg/utils/parsing.go`
This prevents the regular expressions from being recompiled every time the functions are called, improving performance and lowering CPU usage.
---
.jules/bolt.md | 3 +
internal/cli/crypto/salt_test.go | 12 +-
internal/cli/i18n/agents.go | 32 ++--
internal/cli/project/agents.go | 86 +++++----
internal/cli/project/check.go | 6 +-
internal/cli/project/command.go | 2 +-
internal/cli/project/command_test.go | 6 +-
internal/cli/project/exec.go | 264 ++++++++++++++-------------
internal/cli/project/executor.go | 34 ++--
internal/cli/project/init.go | 76 ++++----
internal/cli/project/models.go | 16 +-
internal/cli/project/redo.go | 12 +-
internal/cli/project/update.go | 110 +++++------
internal/cli/project/update_test.go | 6 +-
internal/cli/ssh/add.go | 4 +-
pkg/utils/parsing.go | 6 +-
16 files changed, 340 insertions(+), 335 deletions(-)
create mode 100644 .jules/bolt.md
diff --git a/.jules/bolt.md b/.jules/bolt.md
new file mode 100644
index 0000000..75203c1
--- /dev/null
+++ b/.jules/bolt.md
@@ -0,0 +1,3 @@
+## 2024-05-24 - [Avoid Regex Compilation in Functions]
+**Learning:** Re-compiling regular expressions using `regexp.MustCompile` inside frequently called functions degrades performance unnecessarily, as the compilation cost is paid on every function call.
+**Action:** Always move `regexp.MustCompile` out to package-level variables so that the regular expressions are compiled only once when the package is initialized.
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/i18n/agents.go b/internal/cli/i18n/agents.go
index 8bd0a4f..08461a3 100644
--- a/internal/cli/i18n/agents.go
+++ b/internal/cli/i18n/agents.go
@@ -26,6 +26,21 @@ type I18nKey struct {
// Agent Implementations
+// Regex patterns for different i18n usage, compiled once at package initialization
+// We use two capturing groups: one for single quotes, one for double quotes
+var i18nPatterns = []*regexp.Regexp{
+ // t('key') or t("key")
+ regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
+ // i18n.t('key') or i18n.t("key")
+ regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
+ // $t('key') or $t("key")
+ regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
+ //
+ regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`),
+ //
+ regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`),
+}
+
// KeyExtractor Agent
type KeyExtractor struct {
diff string
@@ -47,21 +62,6 @@ func (a *KeyExtractor) Execute(input map[string]string) (string, error) {
var keys []I18nKey
lines := strings.Split(a.diff, "\n")
- // Regex patterns for different i18n usage
- // We use two capturing groups: one for single quotes, one for double quotes
- patterns := []*regexp.Regexp{
- // t('key') or t("key")
- regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
- // i18n.t('key') or i18n.t("key")
- regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
- // $t('key') or $t("key")
- regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
- //
- regexp.MustCompile(`]+key=(?:'([^']+)'|"([^"]+)")`),
- //
- regexp.MustCompile(`]+keyName=(?:'([^']+)'|"([^"]+)")`),
- }
-
for _, line := range lines {
// We only care about added lines
if !strings.HasPrefix(line, "+") {
@@ -71,7 +71,7 @@ func (a *KeyExtractor) Execute(input map[string]string) (string, error) {
// Remove the "+" prefix
content := line[1:]
- for _, pattern := range patterns {
+ for _, pattern := range i18nPatterns {
matches := pattern.FindAllStringSubmatch(content, -1)
for _, match := range matches {
// match[0] is full match
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"}))
}
diff --git a/internal/cli/ssh/add.go b/internal/cli/ssh/add.go
index b309949..44da17e 100644
--- a/internal/cli/ssh/add.go
+++ b/internal/cli/ssh/add.go
@@ -14,6 +14,8 @@ import (
"github.com/spf13/viper"
)
+var aliasRegex = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
+
func addCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
@@ -79,8 +81,6 @@ func promptForAlias() (string, error) {
existing := viper.GetStringMap(ConfigSSHConnections)
- aliasRegex := regexp.MustCompile("^[a-zA-Z0-9_-]+$")
-
for {
alias, err = pterm.DefaultInteractiveTextInput.WithDefaultText("Connection Alias").Show()
if err != nil {
diff --git a/pkg/utils/parsing.go b/pkg/utils/parsing.go
index 5f18b7f..77089ab 100644
--- a/pkg/utils/parsing.go
+++ b/pkg/utils/parsing.go
@@ -2,12 +2,12 @@ package utils
import "regexp"
+var codeBlockRegex = regexp.MustCompile(`(\` + "`" + "`" + "`" + `[\w-]*)\n([\s\S]*)(\` + "`" + "`" + "`" + `)`)
+
// RemoveCodeBlock removes code block tags from a string.
// If no code block tags are found, it returns the original string.
func RemoveCodeBlock(input string) string {
- re := regexp.MustCompile(`(\` + "`" + "`" + "`" + `[\w-]*)\n([\s\S]*)(\` + "`" + "`" + "`" + `)`)
-
- matches := re.FindStringSubmatch(input)
+ matches := codeBlockRegex.FindStringSubmatch(input)
if len(matches) == 0 {
return input
}
From 180c661da4fc36fbfb95212f931e530ed0b88626 Mon Sep 17 00:00:00 2001
From: MagdielCAS <7864626+MagdielCAS@users.noreply.github.com>
Date: Wed, 1 Apr 2026 08:38:43 +0000
Subject: [PATCH 2/3] ci: fix github action push ref
Conditionally use github.head_ref when pushing from a pull_request event to avoid pushing to the read-only GITHUB_REF, which fixes the `deny updating a hidden ref` error.
---
.github/workflows/internal-ci.yml | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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
From 209c703cc4cf70a32379fdb2765ee102e3ae012b Mon Sep 17 00:00:00 2001
From: MagdielCAS
Date: Wed, 1 Apr 2026 08:39:38 +0000
Subject: [PATCH 3/3] docs: update docs with PTerm-CI
---
docs/docs.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs.md b/docs/docs.md
index 392b31f..2680bd0 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 01 April 2026**