scripts.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. )
  12. func getScriptMetadata(scriptPath string) (string, bool) {
  13. file, err := os.Open(scriptPath)
  14. if err != nil {
  15. return "No description", false
  16. }
  17. defer file.Close()
  18. var desc string
  19. var requiresSudo bool
  20. scanner := bufio.NewScanner(file)
  21. for scanner.Scan() {
  22. line := strings.TrimSpace(scanner.Text())
  23. if strings.HasPrefix(line, "# NAME:") {
  24. desc = strings.TrimSpace(strings.TrimPrefix(line, "# NAME:"))
  25. }
  26. if strings.HasPrefix(line, "# REQUIRES: sudo") ||
  27. strings.HasPrefix(line, "# REQUIRES:sudo") ||
  28. strings.HasPrefix(line, "# ELEVATED: true") ||
  29. strings.HasPrefix(line, "# ELEVATED:true") ||
  30. strings.HasPrefix(line, "# SUDO: true") ||
  31. strings.HasPrefix(line, "# SUDO:true") {
  32. requiresSudo = true
  33. }
  34. }
  35. if desc == "" {
  36. desc = "No description"
  37. }
  38. return desc, requiresSudo
  39. }
  40. func findScripts(filters []string) ([]Script, error) {
  41. var scripts []Script
  42. err := filepath.Walk(config.RunsDir, func(path string, info os.FileInfo, err error) error {
  43. if err != nil {
  44. return err
  45. }
  46. if info.Mode().IsRegular() && (info.Mode()&0o111) != 0 {
  47. // Get relative path from runs directory
  48. relPath, err := filepath.Rel(config.RunsDir, path)
  49. if err != nil {
  50. return err
  51. }
  52. // Skip hidden files and directories
  53. if strings.HasPrefix(filepath.Base(path), ".") {
  54. return nil
  55. }
  56. scriptName := filepath.Base(path)
  57. if len(filters) == 0 || matchesFilters(relPath, scriptName, filters) {
  58. desc, requiresSudo := getScriptMetadata(path)
  59. scripts = append(scripts, Script{
  60. Path: path,
  61. Name: scriptName,
  62. RelPath: relPath,
  63. Desc: desc,
  64. RequiresSudo: requiresSudo,
  65. })
  66. }
  67. }
  68. return nil
  69. })
  70. sort.Slice(scripts, func(i, j int) bool {
  71. return scripts[i].RelPath < scripts[j].RelPath
  72. })
  73. return scripts, err
  74. }
  75. func matchesFilters(relPath, scriptName string, filters []string) bool {
  76. for _, filter := range filters {
  77. // Normalize paths for comparison (remove .sh extension from filter if present)
  78. normalizedFilter := strings.TrimSuffix(filter, ".sh")
  79. normalizedRelPath := strings.TrimSuffix(relPath, ".sh")
  80. normalizedScriptName := strings.TrimSuffix(scriptName, ".sh")
  81. // Check exact matches
  82. if normalizedRelPath == normalizedFilter || normalizedScriptName == normalizedFilter {
  83. return true
  84. }
  85. // Check if filter matches the relative path or script name (case insensitive)
  86. filterLower := strings.ToLower(normalizedFilter)
  87. relPathLower := strings.ToLower(normalizedRelPath)
  88. scriptNameLower := strings.ToLower(normalizedScriptName)
  89. if strings.Contains(relPathLower, filterLower) || strings.Contains(scriptNameLower, filterLower) {
  90. return true
  91. }
  92. // Check directory match (e.g., "tools" matches "tools/install.sh")
  93. if strings.HasPrefix(relPathLower, filterLower+"/") {
  94. return true
  95. }
  96. }
  97. return false
  98. }
  99. func executeScript(script Script, args []string, verbose bool) error {
  100. logFile := filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log")
  101. var cmd *exec.Cmd
  102. if script.RequiresSudo {
  103. sudoLog("Script requires elevated privileges: %s", script.RelPath)
  104. if !commandExists("sudo") {
  105. errorLog("sudo command not found - cannot run elevated script")
  106. return fmt.Errorf("sudo not available")
  107. }
  108. fullArgs := append([]string{script.Path}, args...)
  109. cmd = exec.Command("sudo", fullArgs...)
  110. sudoLog("Running with sudo: %s", strings.Join(append([]string{script.Path}, args...), " "))
  111. } else {
  112. cmd = exec.Command(script.Path, args...)
  113. }
  114. if config.Interactive {
  115. cmd.Stdin = os.Stdin
  116. }
  117. logFileHandle, err := os.Create(logFile)
  118. if err != nil {
  119. return err
  120. }
  121. defer logFileHandle.Close()
  122. showOutput := verbose || config.Interactive
  123. if showOutput {
  124. cmd.Stdout = io.MultiWriter(os.Stdout, logFileHandle)
  125. cmd.Stderr = io.MultiWriter(os.Stderr, logFileHandle)
  126. } else {
  127. cmd.Stdout = logFileHandle
  128. cmd.Stderr = logFileHandle
  129. }
  130. return cmd.Run()
  131. }
  132. func createNewScript(scriptName string) error {
  133. scriptPath := filepath.Join(config.RunsDir, scriptName)
  134. dirPath := filepath.Dir(scriptPath)
  135. if err := os.MkdirAll(dirPath, 0o755); err != nil {
  136. return err
  137. }
  138. if !strings.HasSuffix(scriptName, ".sh") {
  139. scriptName += ".sh"
  140. }
  141. if _, err := os.Stat(scriptPath); err == nil {
  142. return fmt.Errorf("script %s already exists", scriptName)
  143. }
  144. template := fmt.Sprintf(`#!/usr/bin/env bash
  145. # NAME: %s script
  146. # REQUIRES: sudo
  147. set -euo pipefail
  148. echo "Running %s script..."
  149. echo "✅ %s completed successfully"
  150. `, strings.TrimSuffix(scriptName, ".sh"), scriptName, strings.TrimSuffix(scriptName, ".sh"))
  151. err := os.WriteFile(scriptPath, []byte(template), 0o755)
  152. if err != nil {
  153. return err
  154. }
  155. log("✅ Created script: %s", scriptPath)
  156. return nil
  157. }
  158. func ensureRunsDir() error {
  159. if _, err := os.Stat(config.RunsDir); os.IsNotExist(err) {
  160. log("Creating runs directory at: %s", config.RunsDir)
  161. if err := os.MkdirAll(config.RunsDir, 0o755); err != nil {
  162. return err
  163. }
  164. exampleScript := filepath.Join(config.RunsDir, "example.sh")
  165. content := `#!/usr/bin/env bash
  166. # NAME: Example script
  167. echo "Hello from runs/example.sh!"
  168. echo "Edit this script or add your own to runs/"
  169. `
  170. if err := os.WriteFile(exampleScript, []byte(content), 0o755); err != nil {
  171. return err
  172. }
  173. sudoExampleScript := filepath.Join(config.RunsDir, "system-info.sh")
  174. sudoContent := `#!/usr/bin/env bash
  175. # NAME: System information collector
  176. # REQUIRES: sudo
  177. set -euo pipefail
  178. echo "Collecting system information (requires elevated privileges)..."
  179. echo "=== Disk Usage ==="
  180. df -h
  181. echo "=== Memory Info ==="
  182. free -h
  183. echo "=== System Logs (last 10 lines) ==="
  184. tail -n 10 /var/log/syslog 2>/dev/null || tail -n 10 /var/log/messages 2>/dev/null || echo "No accessible system logs"
  185. echo "✅ System info collection completed"
  186. `
  187. if err := os.WriteFile(sudoExampleScript, []byte(sudoContent), 0o755); err != nil {
  188. return err
  189. }
  190. log("✅ Created runs/ directory with example scripts")
  191. return nil
  192. }
  193. return nil
  194. }