diff --git a/main.go b/main.go index d98618a..6de3f87 100644 --- a/main.go +++ b/main.go @@ -30,9 +30,10 @@ type Config struct { } type Script struct { - Path string - Name string - Description string + Path string // Full filesystem path + Name string // Just filename (e.g. "install.sh") + RelPath string // Relative path from runs/ (e.g. "tools/install.sh") + Desc string // Description from script comment } var config Config @@ -225,21 +226,6 @@ func getDescription(scriptPath string) string { return "No description" } -func matchesFilters(scriptName string, filters []string) bool { - if len(filters) == 0 { - return true - } - - scriptNameLower := strings.ToLower(scriptName) - for _, filter := range filters { - filterLower := strings.ToLower(filter) - if strings.Contains(scriptName, filter) || strings.Contains(scriptNameLower, filterLower) { - return true - } - } - return false -} - func findScripts(filters []string) ([]Script, error) { var scripts []Script @@ -249,31 +235,72 @@ func findScripts(filters []string) ([]Script, error) { } if info.Mode().IsRegular() && (info.Mode()&0o111) != 0 { + // Get relative path from runs directory + relPath, err := filepath.Rel(config.RunsDir, path) + if err != nil { + return err + } + + // Skip hidden files and directories + if strings.HasPrefix(filepath.Base(path), ".") { + return nil + } + scriptName := filepath.Base(path) - if matchesFilters(scriptName, filters) { + + if len(filters) == 0 || matchesFilters(relPath, scriptName, filters) { scripts = append(scripts, Script{ - Path: path, - Name: scriptName, - Description: getDescription(path), + Path: path, + Name: scriptName, + RelPath: relPath, + Desc: getDescription(path), }) } } return nil }) - if err != nil { - return nil, err - } sort.Slice(scripts, func(i, j int) bool { - return scripts[i].Name < scripts[j].Name + return scripts[i].RelPath < scripts[j].RelPath }) - return scripts, nil + return scripts, err } -func executeScript(script Script, verbose bool) error { +func matchesFilters(relPath, scriptName string, filters []string) bool { + for _, filter := range filters { + // Normalize paths for comparison (remove .sh extension from filter if present) + normalizedFilter := strings.TrimSuffix(filter, ".sh") + normalizedRelPath := strings.TrimSuffix(relPath, ".sh") + normalizedScriptName := strings.TrimSuffix(scriptName, ".sh") + + // Check exact matches + if normalizedRelPath == normalizedFilter || normalizedScriptName == normalizedFilter { + return true + } + + // Check if filter matches the relative path or script name (case insensitive) + filterLower := strings.ToLower(normalizedFilter) + relPathLower := strings.ToLower(normalizedRelPath) + scriptNameLower := strings.ToLower(normalizedScriptName) + + if strings.Contains(relPathLower, filterLower) || strings.Contains(scriptNameLower, filterLower) { + return true + } + + // Check directory match (e.g., "tools" matches "tools/install.sh") + if strings.HasPrefix(relPathLower, filterLower+"/") { + return true + } + } + return false +} + +func executeScript(script Script, args []string, verbose bool) error { logFile := filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log") - cmd := exec.Command(script.Path) + + // Create command with script path and arguments + cmd := exec.Command(script.Path, args...) if verbose { logFileHandle, err := os.Create(logFile) @@ -298,19 +325,40 @@ func executeScript(script Script, verbose bool) error { } } -func confirmExecution(scripts []Script) bool { - fmt.Printf(Red + "⚠️ About to run ALL scripts:" + NC + "\n") - for _, script := range scripts { - fmt.Printf(" • %s - %s\n", script.Name, script.Description) +func createNewScript(scriptName string) error { + if err := os.MkdirAll(config.RunsDir, 0o755); err != nil { + return err } - fmt.Println() - fmt.Print("Continue? (y/N): ") - reader := bufio.NewReader(os.Stdin) - answer, _ := reader.ReadString('\n') - answer = strings.TrimSpace(strings.ToLower(answer)) + if !strings.HasSuffix(scriptName, ".sh") { + scriptName += ".sh" + } - return answer == "y" || answer == "yes" + scriptPath := filepath.Join(config.RunsDir, scriptName) + + if _, err := os.Stat(scriptPath); err == nil { + return fmt.Errorf("script %s already exists", scriptName) + } + + template := fmt.Sprintf(`#!/usr/bin/env bash +# NAME: %s script + +set -euo pipefail + +echo "Running %s script..." + +# Add your commands here + +echo "✅ %s completed successfully" +`, strings.TrimSuffix(scriptName, ".sh"), scriptName, strings.TrimSuffix(scriptName, ".sh")) + + err := os.WriteFile(scriptPath, []byte(template), 0o755) + if err != nil { + return err + } + + log("✅ Created script: %s", scriptPath) + return nil } func initConfig() { @@ -337,7 +385,6 @@ echo "Edit this script or add your own to runs/" } log("✅ Created runs/ directory with example script") - log("Use 'dev ls' to list scripts or 'dev new ' to create a new one") return nil } return nil @@ -346,12 +393,22 @@ echo "Edit this script or add your own to runs/" var rootCmd = &cobra.Command{ Use: "dev", Short: "Development script runner", - Long: "A simple tool to manage and run development scripts with security scanning", + Long: "A CLI tool for managing and running development scripts", + RunE: func(cmd *cobra.Command, args []string) error { + // Default behavior: list scripts if no subcommand + return listCmd.RunE(cmd, args) + }, } var runCmd = &cobra.Command{ - Use: "run [filters...]", + Use: "run [filters...] [-- script-args...]", Short: "Run scripts matching filters (or all if no filters)", + Long: `Run scripts matching filters. Use -- to pass arguments to scripts. + +Examples: + dev run tools/dev.sh # Run specific script + dev run tools/dev.sh -- arg1 arg2 # Run with arguments + dev run --verbose install -- --force # Run with flags`, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { scripts, err := findScripts(nil) if err != nil { @@ -360,14 +417,51 @@ var runCmd = &cobra.Command{ var completions []string for _, script := range scripts { - scriptName := strings.TrimSuffix(script.Name, ".sh") - if strings.HasPrefix(scriptName, toComplete) { - completions = append(completions, scriptName+"\t"+script.Description) + // Add both the script name and relative path as completion options + if strings.HasPrefix(script.Name, toComplete) || strings.HasPrefix(script.RelPath, toComplete) { + // Use relative path as the completion value with description + completions = append(completions, fmt.Sprintf("%s\t%s", script.RelPath, script.Desc)) } } return completions, cobra.ShellCompDirectiveNoFileComp }, RunE: func(cmd *cobra.Command, args []string) error { + // Parse arguments manually to handle -- separator + var filters []string + var scriptArgs []string + + // Find "run" command position and -- separator in raw args + rawArgs := os.Args[1:] // Skip program name + runIndex := -1 + dashDashIndex := -1 + + for i, arg := range rawArgs { + if arg == "run" { + runIndex = i + } + if arg == "--" && runIndex >= 0 { + dashDashIndex = i + break + } + } + + if dashDashIndex >= 0 { + // Extract everything between "run" and "--" as filters (skip flags) + for i := runIndex + 1; i < dashDashIndex; i++ { + arg := rawArgs[i] + // Skip flags + if !strings.HasPrefix(arg, "-") { + filters = append(filters, arg) + } + } + // Everything after "--" are script args + scriptArgs = rawArgs[dashDashIndex+1:] + } else { + // No --, use cobra's args as filters + filters = args + scriptArgs = []string{} + } + if err := ensureRunsDir(); err != nil { return err } @@ -376,68 +470,88 @@ var runCmd = &cobra.Command{ return err } - scripts, err := findScripts(args) + scripts, err := findScripts(filters) if err != nil { return err } if len(scripts) == 0 { - if len(args) > 0 { - warn("No scripts match the given filters: %s", strings.Join(args, ", ")) - fmt.Println("Use 'dev ls' to see all available scripts") + if len(filters) > 0 { + warn("No scripts match filters: %s", strings.Join(filters, ", ")) } else { - warn("No executable scripts found in %s", config.RunsDir) - fmt.Println("Use 'dev new ' to create a new script") + warn("No scripts found. Use 'dev new ' to create one") } return nil } - if len(args) == 0 && !config.DryRun { - if !confirmExecution(scripts) { - fmt.Println("Cancelled") - return nil - } - } - - var executed, failed []string - + // CLI execution for _, script := range scripts { if config.DryRun { - fmt.Printf(Blue+"[DRY]"+NC+" Would run: %s - %s\n", script.Name, script.Description) + argsStr := "" + if len(scriptArgs) > 0 { + argsStr = fmt.Sprintf(" with args: %s", strings.Join(scriptArgs, " ")) + } + fmt.Printf("[DRY] Would run: %s - %s%s\n", script.RelPath, script.Desc, argsStr) continue } - fmt.Printf("\n"+Blue+"▶"+NC+" Running: %s\n", script.Name) - if script.Description != "No description" { - fmt.Printf(" %s\n", script.Description) + argsDisplay := "" + if len(scriptArgs) > 0 { + argsDisplay = fmt.Sprintf(" with args: %s", strings.Join(scriptArgs, " ")) } + fmt.Printf("Running: %s%s\n", script.RelPath, argsDisplay) - if err := executeScript(script, config.Verbose); err != nil { - errorLog("❌ %s failed (see %s)", script.Name, - filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log")) - failed = append(failed, script.Name) - } else { - log("✅ %s completed", script.Name) - executed = append(executed, script.Name) - } - } - - if !config.DryRun { - fmt.Printf("\n" + Blue + "═══ SUMMARY ═══" + NC + "\n") - fmt.Printf("✅ Completed: %d\n", len(executed)) - if len(failed) > 0 { - fmt.Printf("❌ Failed: %d\n", len(failed)) - fmt.Printf("\n" + Red + "Failed scripts:" + NC + "\n") - for _, f := range failed { - fmt.Printf(" • %s\n", f) + if err := executeScript(script, scriptArgs, config.Verbose); err != nil { + errorLog("❌ %s failed", script.RelPath) + if !config.Verbose { + fmt.Printf(" Check log: %s\n", filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log")) } - return fmt.Errorf("some scripts failed") + } else { + log("✅ %s completed", script.RelPath) } } return nil }, } +var listCmd = &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List all available scripts", + RunE: func(cmd *cobra.Command, args []string) error { + if err := ensureRunsDir(); err != nil { + return err + } + + scripts, err := findScripts(nil) + if err != nil { + return err + } + + if len(scripts) == 0 { + warn("No scripts found in %s", config.RunsDir) + fmt.Println("Use 'dev new ' to create a new script") + return nil + } + + fmt.Printf("Available scripts in %s:\n\n", config.RunsDir) + for _, script := range scripts { + fmt.Printf(" %s%s%s - %s\n", Blue, script.RelPath, NC, script.Desc) + } + fmt.Printf("\nTotal: %d scripts\n", len(scripts)) + return nil + }, +} + +var newCmd = &cobra.Command{ + Use: "new ", + Short: "Create a new script template", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return createNewScript(args[0]) + }, +} + var pushCmd = &cobra.Command{ Use: "push", Aliases: []string{"u", "ush"}, @@ -447,73 +561,6 @@ var pushCmd = &cobra.Command{ }, } -var listCmd = &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List all available scripts", - RunE: func(cmd *cobra.Command, args []string) error { - scripts, err := findScripts(nil) - if err != nil { - return err - } - - if len(scripts) == 0 { - warn("No executable scripts found in %s", config.RunsDir) - return nil - } - - fmt.Printf(Blue + "Available scripts:" + NC + "\n") - for _, script := range scripts { - fmt.Printf(" %s%s%s - %s\n", Green, script.Name, NC, script.Description) - } - fmt.Printf("\nTotal: %d scripts\n", len(scripts)) - return nil - }, -} - -var newCmd = &cobra.Command{ - Use: "new ", - Short: "Create a new script template", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - scriptName := args[0] - - if err := os.MkdirAll(config.RunsDir, 0o755); err != nil { - return err - } - - if !strings.HasSuffix(scriptName, ".sh") { - scriptName += ".sh" - } - - scriptPath := filepath.Join(config.RunsDir, scriptName) - - if _, err := os.Stat(scriptPath); err == nil { - return fmt.Errorf("script %s already exists", scriptName) - } - - template := fmt.Sprintf(`#!/usr/bin/env bash -# NAME: %s script - -set -euo pipefail - -echo "Running %s script..." - -# Add your commands here - -echo "✅ %s completed successfully" -`, strings.TrimSuffix(scriptName, ".sh"), scriptName, strings.TrimSuffix(scriptName, ".sh")) - - if err := os.WriteFile(scriptPath, []byte(template), 0o755); err != nil { - return err - } - - log("✅ Created new script: %s", scriptPath) - log("Edit the script to add your commands") - return nil - }, -} - var completionCmd = &cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate completion script", @@ -537,19 +584,6 @@ Zsh: $ dev completion zsh > "${fpath[1]}/_dev" # You will need to start a new shell for this setup to take effect. - -Fish: - $ dev completion fish | source - - # To load completions for each session, execute once: - $ dev completion fish > ~/.config/fish/completions/dev.fish - -PowerShell: - PS> dev completion powershell | Out-String | Invoke-Expression - - # To load completions for every new session, run: - PS> dev completion powershell > dev.ps1 - # and source this file from your PowerShell profile. `, DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, @@ -569,22 +603,16 @@ PowerShell: }, } -var depsCmd = &cobra.Command{ - Use: "deps", - Aliases: []string{"check"}, - Short: "Check for required dependencies", - RunE: func(cmd *cobra.Command, args []string) error { - return checkDependencies() - }, -} - func main() { initConfig() runCmd.Flags().BoolVar(&config.DryRun, "dry", false, "Show what would run without executing") - runCmd.Flags().BoolVarP(&config.Verbose, "verbose", "v", false, "Show detailed output during execution") + runCmd.Flags().BoolVarP(&config.Verbose, "verbose", "v", false, "Show script output in terminal") - rootCmd.AddCommand(runCmd, pushCmd, listCmd, newCmd, depsCmd, completionCmd) + // This prevents Cobra from consuming -- and everything after it + runCmd.Flags().SetInterspersed(false) + + rootCmd.AddCommand(runCmd, listCmd, newCmd, pushCmd, completionCmd) if err := rootCmd.Execute(); err != nil { os.Exit(1) diff --git a/runs/tools/dev.sh b/runs/tools/dev.sh new file mode 100755 index 0000000..3954e4a --- /dev/null +++ b/runs/tools/dev.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# NAME: tools/dev script + +echo "Running tools/dev.sh script..." + +echo $1 $2 $3 + +# Add your commands here + +echo "✅ tools/dev completed successfully"