main.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. "time"
  12. "github.com/spf13/cobra"
  13. )
  14. const (
  15. Red = "\033[0;31m"
  16. Green = "\033[0;32m"
  17. Blue = "\033[0;34m"
  18. Yellow = "\033[0;33m"
  19. Purple = "\033[0;35m" // For elevated scripts
  20. NC = "\033[0m"
  21. )
  22. type Config struct {
  23. RunsDir string
  24. LogsDir string
  25. DryRun bool
  26. Verbose bool
  27. Interactive bool
  28. }
  29. type Script struct {
  30. Path string // Full filesystem path
  31. Name string // Just filename (e.g. "install.sh")
  32. RelPath string // Relative path from runs/ (e.g. "tools/install.sh")
  33. Desc string // Description from script comment
  34. RequiresSudo bool // Whether script needs elevated privileges
  35. RequiresInteractive bool // Whether script needs interactive input
  36. }
  37. var config Config
  38. func log(msg string, args ...any) {
  39. fmt.Printf(Green+"[RUN]"+NC+" "+msg+"\n", args...)
  40. }
  41. func warn(msg string, args ...any) {
  42. fmt.Printf(Yellow+"[WARN]"+NC+" "+msg+"\n", args...)
  43. }
  44. func errorLog(msg string, args ...any) {
  45. fmt.Fprintf(os.Stderr, Red+"[ERROR]"+NC+" "+msg+"\n", args...)
  46. }
  47. func sudoLog(msg string, args ...any) {
  48. fmt.Printf(Purple+"[SUDO]"+NC+" "+msg+"\n", args...)
  49. }
  50. func commandExists(cmd string) bool {
  51. _, err := exec.LookPath(cmd)
  52. return err == nil
  53. }
  54. func checkDependencies() error {
  55. requiredTools := []string{"git", "find", "grep"}
  56. optionalTools := []string{"gitleaks"}
  57. var missing []string
  58. log("Checking dependencies...")
  59. for _, tool := range requiredTools {
  60. if !commandExists(tool) {
  61. missing = append(missing, tool)
  62. }
  63. }
  64. for _, tool := range optionalTools {
  65. if !commandExists(tool) {
  66. warn("Optional tool missing: %s (recommended for security scanning)", tool)
  67. } else {
  68. log("✓ Found: %s", tool)
  69. }
  70. }
  71. if len(missing) > 0 {
  72. errorLog("Missing required tools: %s", strings.Join(missing, ", "))
  73. errorLog("Please install missing dependencies")
  74. return fmt.Errorf("missing dependencies")
  75. }
  76. log("✓ All required dependencies found")
  77. return nil
  78. }
  79. func runSecurityScan() error {
  80. log("Running security scan...")
  81. if !commandExists("gitleaks") {
  82. warn("GitLeaks not installed - skipping security scan")
  83. warn("Install with: paru -S gitleaks")
  84. fmt.Println()
  85. fmt.Print("Continue without security scan? (y/N): ")
  86. reader := bufio.NewReader(os.Stdin)
  87. answer, _ := reader.ReadString('\n')
  88. answer = strings.TrimSpace(strings.ToLower(answer))
  89. if answer != "y" && answer != "yes" {
  90. errorLog("Push cancelled for security")
  91. return fmt.Errorf("security scan cancelled")
  92. }
  93. return nil
  94. }
  95. log("Using GitLeaks for secret detection...")
  96. cmd := exec.Command("gitleaks", "detect", "--verbose", "--exit-code", "1")
  97. if err := cmd.Run(); err != nil {
  98. errorLog("❌ Secrets detected! Review before pushing.")
  99. return fmt.Errorf("secrets detected")
  100. }
  101. log("✅ No secrets detected")
  102. return nil
  103. }
  104. func isGitRepo() bool {
  105. cmd := exec.Command("git", "rev-parse", "--git-dir")
  106. return cmd.Run() == nil
  107. }
  108. func hasUncommittedChanges() bool {
  109. cmd := exec.Command("git", "diff-index", "--quiet", "HEAD", "--")
  110. return cmd.Run() != nil
  111. }
  112. func getCurrentBranch() (string, error) {
  113. cmd := exec.Command("git", "branch", "--show-current")
  114. output, err := cmd.Output()
  115. if err != nil {
  116. return "", err
  117. }
  118. return strings.TrimSpace(string(output)), nil
  119. }
  120. func handlePush() error {
  121. log("Preparing to push repository...")
  122. if !isGitRepo() {
  123. errorLog("Not in a git repository")
  124. return fmt.Errorf("not in git repo")
  125. }
  126. if hasUncommittedChanges() {
  127. warn("You have uncommitted changes")
  128. fmt.Println()
  129. cmd := exec.Command("git", "status", "--short")
  130. cmd.Stdout = os.Stdout
  131. cmd.Run()
  132. fmt.Println()
  133. defaultMsg := fmt.Sprintf("dev: automated commit - %s", time.Now().Format("2006-01-02 15:04:05"))
  134. fmt.Print("Commit all changes? (Y/n): ")
  135. reader := bufio.NewReader(os.Stdin)
  136. answer, _ := reader.ReadString('\n')
  137. answer = strings.TrimSpace(strings.ToLower(answer))
  138. if answer != "n" && answer != "no" {
  139. fmt.Println()
  140. fmt.Printf("Default: %s\n", defaultMsg)
  141. fmt.Print("Custom commit message (or press Enter for default): ")
  142. commitMsg, _ := reader.ReadString('\n')
  143. commitMsg = strings.TrimSpace(commitMsg)
  144. if commitMsg == "" {
  145. commitMsg = defaultMsg
  146. }
  147. if err := exec.Command("git", "add", ".").Run(); err != nil {
  148. return fmt.Errorf("failed to add changes: %v", err)
  149. }
  150. if err := exec.Command("git", "commit", "-m", commitMsg).Run(); err != nil {
  151. return fmt.Errorf("failed to commit changes: %v", err)
  152. }
  153. log("✓ Changes committed: %s", commitMsg)
  154. }
  155. }
  156. if err := runSecurityScan(); err != nil {
  157. return err
  158. }
  159. branch, err := getCurrentBranch()
  160. if err != nil {
  161. return fmt.Errorf("failed to get current branch: %v", err)
  162. }
  163. log("Pushing branch '%s' to origin...", branch)
  164. cmd := exec.Command("git", "push", "origin", branch)
  165. if err := cmd.Run(); err != nil {
  166. errorLog("❌ Push failed")
  167. return fmt.Errorf("push failed")
  168. }
  169. log("✅ Successfully pushed to origin/%s", branch)
  170. return nil
  171. }
  172. type ScriptMetadata struct {
  173. RequiresSudo bool
  174. RequiresInteractive bool
  175. }
  176. func getScriptMetadata(scriptPath string) (string, ScriptMetadata) {
  177. file, err := os.Open(scriptPath)
  178. if err != nil {
  179. return "No description", ScriptMetadata{}
  180. }
  181. defer file.Close()
  182. var desc string
  183. scriptMetadata := ScriptMetadata{}
  184. scanner := bufio.NewScanner(file)
  185. for scanner.Scan() {
  186. line := strings.TrimSpace(scanner.Text())
  187. if strings.HasPrefix(line, "# NAME:") {
  188. desc = strings.TrimSpace(strings.TrimPrefix(line, "# NAME:"))
  189. }
  190. if strings.HasPrefix(line, "# REQUIRES:") {
  191. scriptMetadata.RequiresSudo = strings.Contains(line, "sudo")
  192. scriptMetadata.RequiresInteractive = strings.Contains(line, "interactive")
  193. }
  194. }
  195. if desc == "" {
  196. desc = "No description"
  197. }
  198. return desc, scriptMetadata
  199. }
  200. func findScripts(filters []string) ([]Script, error) {
  201. var scripts []Script
  202. err := filepath.Walk(config.RunsDir, func(path string, info os.FileInfo, err error) error {
  203. if err != nil {
  204. return err
  205. }
  206. if info.Mode().IsRegular() && (info.Mode()&0o111) != 0 {
  207. // Get relative path from runs directory
  208. relPath, err := filepath.Rel(config.RunsDir, path)
  209. if err != nil {
  210. return err
  211. }
  212. // Skip hidden files and directories
  213. if strings.HasPrefix(filepath.Base(path), ".") {
  214. return nil
  215. }
  216. scriptName := filepath.Base(path)
  217. if len(filters) == 0 || matchesFilters(relPath, scriptName, filters) {
  218. desc, metaData := getScriptMetadata(path)
  219. scripts = append(scripts, Script{
  220. Path: path,
  221. Name: scriptName,
  222. RelPath: relPath,
  223. Desc: desc,
  224. RequiresSudo: metaData.RequiresSudo,
  225. RequiresInteractive: metaData.RequiresInteractive,
  226. })
  227. }
  228. }
  229. return nil
  230. })
  231. sort.Slice(scripts, func(i, j int) bool {
  232. return scripts[i].RelPath < scripts[j].RelPath
  233. })
  234. return scripts, err
  235. }
  236. func matchesFilters(relPath, scriptName string, filters []string) bool {
  237. for _, filter := range filters {
  238. // Normalize paths for comparison (remove .sh extension from filter if present)
  239. normalizedFilter := strings.TrimSuffix(filter, ".sh")
  240. normalizedRelPath := strings.TrimSuffix(relPath, ".sh")
  241. normalizedScriptName := strings.TrimSuffix(scriptName, ".sh")
  242. // Check exact matches
  243. if normalizedRelPath == normalizedFilter || normalizedScriptName == normalizedFilter {
  244. return true
  245. }
  246. // Check if filter matches the relative path or script name (case insensitive)
  247. filterLower := strings.ToLower(normalizedFilter)
  248. relPathLower := strings.ToLower(normalizedRelPath)
  249. scriptNameLower := strings.ToLower(normalizedScriptName)
  250. if strings.Contains(relPathLower, filterLower) || strings.Contains(scriptNameLower, filterLower) {
  251. return true
  252. }
  253. // Check directory match (e.g., "tools" matches "tools/install.sh")
  254. if strings.HasPrefix(relPathLower, filterLower+"/") {
  255. return true
  256. }
  257. }
  258. return false
  259. }
  260. func executeScript(script Script, args []string, verbose bool) error {
  261. logFile := filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log")
  262. var cmd *exec.Cmd
  263. if script.RequiresSudo {
  264. sudoLog("Script requires elevated privileges: %s", script.RelPath)
  265. if !commandExists("sudo") {
  266. errorLog("sudo command not found - cannot run elevated script")
  267. return fmt.Errorf("sudo not available")
  268. }
  269. // Use -S to read password from stdin, and preserve environment
  270. fullArgs := append([]string{"-S", "-E", script.Path}, args...)
  271. cmd = exec.Command("sudo", fullArgs...)
  272. sudoLog("Running with sudo: %s", strings.Join(append([]string{script.Path}, args...), " "))
  273. } else {
  274. cmd = exec.Command(script.Path, args...)
  275. }
  276. if config.Interactive || script.RequiresInteractive {
  277. cmd.Stdin = os.Stdin
  278. }
  279. logFileHandle, err := os.Create(logFile)
  280. if err != nil {
  281. return err
  282. }
  283. defer logFileHandle.Close()
  284. showOutput := verbose || config.Interactive || script.RequiresInteractive
  285. if showOutput {
  286. cmd.Stdout = io.MultiWriter(os.Stdout, logFileHandle)
  287. cmd.Stderr = io.MultiWriter(os.Stderr, logFileHandle)
  288. } else {
  289. cmd.Stdout = logFileHandle
  290. cmd.Stderr = logFileHandle
  291. }
  292. return cmd.Run()
  293. }
  294. func createNewScript(scriptName string) error {
  295. scriptPath := filepath.Join(config.RunsDir, scriptName)
  296. dirPath := filepath.Dir(scriptPath)
  297. if err := os.MkdirAll(dirPath, 0o755); err != nil {
  298. return err
  299. }
  300. if !strings.HasSuffix(scriptName, ".sh") {
  301. scriptName += ".sh"
  302. scriptPath = filepath.Join(config.RunsDir, scriptName)
  303. }
  304. if _, err := os.Stat(scriptPath); err == nil {
  305. return fmt.Errorf("script %s already exists", scriptName)
  306. }
  307. template := fmt.Sprintf(`#!/usr/bin/env bash
  308. # NAME: %s
  309. # REQUIRES: sudo interactive
  310. set -euo pipefail
  311. # Source common functions
  312. source "$(dirname "$0")/../common.sh" || {
  313. echo "[ERROR] Could not source common.sh" >&2
  314. exit 1
  315. }
  316. check_requirements() {
  317. # Check required commands
  318. local required_commands=()
  319. # Add your required commands here
  320. # required_commands=(git curl wget)
  321. for cmd in "${required_commands[@]}"; do
  322. if ! command_exists "$cmd"; then
  323. log_error "Required command not found: $cmd"
  324. exit 1
  325. fi
  326. done
  327. }
  328. main() {
  329. init_script
  330. check_requirements
  331. # Your main script logic goes here
  332. # Example operations:
  333. # if ! confirm "Proceed with operation?"; then
  334. # log_info "Operation cancelled"
  335. # finish_script 0
  336. # fi
  337. # Add your implementation here
  338. finish_script 0
  339. }
  340. main "$@"
  341. `, strings.TrimSuffix(filepath.Base(scriptName), ".sh"))
  342. err := os.WriteFile(scriptPath, []byte(template), 0o755)
  343. if err != nil {
  344. return err
  345. }
  346. log("✅ Created script: %s", scriptPath)
  347. return nil
  348. }
  349. func initConfig() {
  350. wd, _ := os.Getwd()
  351. config.RunsDir = filepath.Join(wd, "runs")
  352. config.LogsDir = filepath.Join(wd, "logs")
  353. }
  354. func ensureRunsDir() error {
  355. if _, err := os.Stat(config.RunsDir); os.IsNotExist(err) {
  356. log("Creating runs directory at: %s", config.RunsDir)
  357. if err := os.MkdirAll(config.RunsDir, 0o755); err != nil {
  358. return err
  359. }
  360. exampleScript := filepath.Join(config.RunsDir, "example.sh")
  361. content := `#!/usr/bin/env bash
  362. # NAME: Example script
  363. echo "Hello from runs/example.sh!"
  364. echo "Edit this script or add your own to runs/"
  365. `
  366. if err := os.WriteFile(exampleScript, []byte(content), 0o755); err != nil {
  367. return err
  368. }
  369. sudoExampleScript := filepath.Join(config.RunsDir, "system-info.sh")
  370. sudoContent := `#!/usr/bin/env bash
  371. # NAME: System information collector
  372. # REQUIRES: sudo
  373. set -euo pipefail
  374. echo "Collecting system information (requires elevated privileges)..."
  375. echo "=== Disk Usage ==="
  376. df -h
  377. echo "=== Memory Info ==="
  378. free -h
  379. echo "=== System Logs (last 10 lines) ==="
  380. tail -n 10 /var/log/syslog 2>/dev/null || tail -n 10 /var/log/messages 2>/dev/null || echo "No accessible system logs"
  381. echo "✅ System info collection completed"
  382. `
  383. if err := os.WriteFile(sudoExampleScript, []byte(sudoContent), 0o755); err != nil {
  384. return err
  385. }
  386. log("✅ Created runs/ directory with example scripts")
  387. return nil
  388. }
  389. return nil
  390. }
  391. var rootCmd = &cobra.Command{
  392. Use: "dev",
  393. Short: "Development script runner",
  394. Long: "A CLI tool for managing and running development scripts",
  395. RunE: func(cmd *cobra.Command, args []string) error {
  396. // Default behavior: list scripts if no subcommand
  397. return listCmd.RunE(cmd, args)
  398. },
  399. }
  400. var runCmd = &cobra.Command{
  401. Use: "run [filters...] [-- script-args...]",
  402. Short: "Run scripts matching filters (or all if no filters)",
  403. Long: `Run scripts matching filters. Use -- to pass arguments to scripts.
  404. Scripts marked with "# REQUIRES: sudo" will automatically run with elevated privileges.
  405. Examples:
  406. dev run tools/dev.sh # Run specific script
  407. dev run tools/dev.sh -- arg1 arg2 # Run with arguments
  408. dev run --verbose install -- --force # Run with flags
  409. dev run system-info # Runs with sudo if marked as required`,
  410. ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
  411. scripts, err := findScripts(nil)
  412. if err != nil {
  413. return nil, cobra.ShellCompDirectiveNoFileComp
  414. }
  415. var completions []string
  416. for _, script := range scripts {
  417. if strings.HasPrefix(script.Name, toComplete) || strings.HasPrefix(script.RelPath, toComplete) {
  418. desc := script.Desc
  419. if script.RequiresSudo {
  420. desc = desc + " [SUDO]"
  421. }
  422. completions = append(completions, fmt.Sprintf("%s\t%s", script.RelPath, desc))
  423. }
  424. }
  425. return completions, cobra.ShellCompDirectiveNoFileComp
  426. },
  427. RunE: func(cmd *cobra.Command, args []string) error {
  428. // Parse arguments manually to handle -- separator
  429. var filters []string
  430. var scriptArgs []string
  431. // Find "run" command position and -- separator in raw args
  432. rawArgs := os.Args[1:] // Skip program name
  433. runIndex := -1
  434. dashDashIndex := -1
  435. for i, arg := range rawArgs {
  436. if arg == "run" {
  437. runIndex = i
  438. }
  439. if arg == "--" && runIndex >= 0 {
  440. dashDashIndex = i
  441. break
  442. }
  443. }
  444. if dashDashIndex >= 0 {
  445. for i := runIndex + 1; i < dashDashIndex; i++ {
  446. arg := rawArgs[i]
  447. if !strings.HasPrefix(arg, "-") {
  448. filters = append(filters, arg)
  449. }
  450. }
  451. scriptArgs = rawArgs[dashDashIndex+1:]
  452. } else {
  453. filters = args
  454. scriptArgs = []string{}
  455. }
  456. if err := ensureRunsDir(); err != nil {
  457. return err
  458. }
  459. if err := os.MkdirAll(config.LogsDir, 0o755); err != nil {
  460. return err
  461. }
  462. scripts, err := findScripts(filters)
  463. if err != nil {
  464. return err
  465. }
  466. if len(scripts) == 0 {
  467. if len(filters) > 0 {
  468. warn("No scripts match filters: %s", strings.Join(filters, ", "))
  469. } else {
  470. warn("No scripts found. Use 'dev new <name>' to create one")
  471. }
  472. return nil
  473. }
  474. for _, script := range scripts {
  475. if config.DryRun {
  476. argsStr := ""
  477. if len(scriptArgs) > 0 {
  478. argsStr = fmt.Sprintf(" with args: %s", strings.Join(scriptArgs, " "))
  479. }
  480. sudoStr := ""
  481. if script.RequiresSudo {
  482. sudoStr = " [SUDO]"
  483. }
  484. fmt.Printf("[DRY] Would run: %s - %s%s%s\n", script.RelPath, script.Desc, sudoStr, argsStr)
  485. continue
  486. }
  487. argsDisplay := ""
  488. if len(scriptArgs) > 0 {
  489. argsDisplay = fmt.Sprintf(" with args: %s", strings.Join(scriptArgs, " "))
  490. }
  491. sudoDisplay := ""
  492. if script.RequiresSudo {
  493. sudoDisplay = " [ELEVATED]"
  494. }
  495. fmt.Printf("Running: %s%s%s\n", script.RelPath, sudoDisplay, argsDisplay)
  496. if err := executeScript(script, scriptArgs, config.Verbose); err != nil {
  497. errorLog("❌ %s failed", script.RelPath)
  498. if !config.Verbose {
  499. fmt.Printf(" Check log: %s\n", filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log"))
  500. }
  501. } else {
  502. if script.RequiresSudo {
  503. sudoLog("✅ %s completed (elevated)", script.RelPath)
  504. } else {
  505. log("✅ %s completed", script.RelPath)
  506. }
  507. }
  508. }
  509. return nil
  510. },
  511. }
  512. var listCmd = &cobra.Command{
  513. Use: "ls",
  514. Aliases: []string{"list"},
  515. Short: "List all available scripts",
  516. RunE: func(cmd *cobra.Command, args []string) error {
  517. if err := ensureRunsDir(); err != nil {
  518. return err
  519. }
  520. scripts, err := findScripts(nil)
  521. if err != nil {
  522. return err
  523. }
  524. if len(scripts) == 0 {
  525. warn("No scripts found in %s", config.RunsDir)
  526. fmt.Println("Use 'dev new <name>' to create a new script")
  527. return nil
  528. }
  529. fmt.Printf("Available scripts in %s:\n\n", config.RunsDir)
  530. for _, script := range scripts {
  531. if script.RequiresSudo {
  532. fmt.Printf(" %s%s%s - %s %s[SUDO]%s\n",
  533. Blue, script.RelPath, NC, script.Desc, Purple, NC)
  534. } else {
  535. fmt.Printf(" %s%s%s - %s\n",
  536. Blue, script.RelPath, NC, script.Desc)
  537. }
  538. }
  539. sudoCount := 0
  540. for _, script := range scripts {
  541. if script.RequiresSudo {
  542. sudoCount++
  543. }
  544. }
  545. fmt.Printf("\nTotal: %d scripts", len(scripts))
  546. if sudoCount > 0 {
  547. fmt.Printf(" (%d require elevated privileges)", sudoCount)
  548. }
  549. fmt.Println()
  550. return nil
  551. },
  552. }
  553. var newCmd = &cobra.Command{
  554. Use: "new <name>",
  555. Short: "Create a new script template",
  556. Args: cobra.ExactArgs(1),
  557. RunE: func(cmd *cobra.Command, args []string) error {
  558. return createNewScript(args[0])
  559. },
  560. }
  561. var pushCmd = &cobra.Command{
  562. Use: "push",
  563. Aliases: []string{"u", "ush"},
  564. Short: "Commit, scan for secrets, and push to git origin",
  565. RunE: func(cmd *cobra.Command, args []string) error {
  566. return handlePush()
  567. },
  568. }
  569. var depsCmd = &cobra.Command{
  570. Use: "deps",
  571. Short: "Install dependencies",
  572. RunE: func(cmd *cobra.Command, args []string) error {
  573. return checkDependencies()
  574. },
  575. }
  576. var completionCmd = &cobra.Command{
  577. Use: "completion [bash|zsh|fish|powershell]",
  578. Short: "Generate completion script",
  579. Long: `To load completions:
  580. Bash:
  581. $ source <(dev completion bash)
  582. # To load completions for each session, execute once:
  583. # Linux:
  584. $ dev completion bash > /etc/bash_completion.d/dev
  585. # macOS:
  586. $ dev completion bash > $(brew --prefix)/etc/bash_completion.d/dev
  587. Zsh:
  588. # If shell completion is not already enabled in your environment,
  589. # you will need to enable it. You can execute the following once:
  590. $ echo "autoload -U compinit; compinit" >> ~/.zshrc
  591. # To load completions for each session, execute once:
  592. $ dev completion zsh > "${fpath[1]}/_dev"
  593. # You will need to start a new shell for this setup to take effect.
  594. `,
  595. DisableFlagsInUseLine: true,
  596. ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
  597. Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
  598. RunE: func(cmd *cobra.Command, args []string) error {
  599. switch args[0] {
  600. case "bash":
  601. return rootCmd.GenBashCompletion(os.Stdout)
  602. case "zsh":
  603. return rootCmd.GenZshCompletion(os.Stdout)
  604. case "fish":
  605. return rootCmd.GenFishCompletion(os.Stdout, true)
  606. case "powershell":
  607. return rootCmd.GenPowerShellCompletionWithDesc(os.Stdout)
  608. }
  609. return nil
  610. },
  611. }
  612. func main() {
  613. initConfig()
  614. runCmd.Flags().BoolVar(&config.DryRun, "dry", false, "Show what would run without executing")
  615. runCmd.Flags().BoolVarP(&config.Verbose, "verbose", "v", false, "Show script output in terminal")
  616. runCmd.Flags().BoolVarP(&config.Interactive, "interactive", "i", false, "Run script interactively (show output and allow input)")
  617. // This prevents Cobra from consuming -- and everything after it
  618. runCmd.Flags().SetInterspersed(false)
  619. rootCmd.AddCommand(runCmd, listCmd, newCmd, pushCmd, depsCmd, completionCmd)
  620. if err := rootCmd.Execute(); err != nil {
  621. os.Exit(1)
  622. }
  623. }