dev: automated commit - 2025-05-31 17:04:45

This commit is contained in:
Mariano Z. 2025-05-31 17:04:49 -03:00
parent d08c9c562e
commit f037255a1a
Signed by: marianozunino
GPG key ID: 4C73BAD25156DACE
7 changed files with 783 additions and 377 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
logs/*
bin/

48
README.md Normal file
View file

@ -0,0 +1,48 @@
# Dev Tool
Simple development script runner that builds itself on first use.
## Quick Start
```bash
git clone https://git.mz.uy/marianozunino/dev
cd dev
./dev
```
## Usage
```bash
./dev run # Run all scripts
./dev run docker # Run scripts matching "docker"
./dev ls # List available scripts
./dev new backup # Create new script template
./dev push # Git push with security scan
```
## How It Works
The `./dev` script:
1. Detects your Linux architecture (amd64/arm64)
2. Builds `bin/dev-linux-{arch}` if needed
3. Runs your command
## Requirements
- Linux (any distro)
- Go installed: `sudo pacman -S go` (or your distro's equivalent)
## File Structure
```
project/
├── dev* # Launcher script
├── main.go # Source code
├── Makefile # Build helper
├── bin/ # Built binaries
├── runs/ # Your scripts
└── logs/ # Execution logs
```
That's it! Clone and run `./dev` to get started.

46
dev Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Simple dev tool launcher - builds if needed
set -euo pipefail
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[DEV]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# Simple platform detection
ARCH=$(uname -m)
case $ARCH in
x86_64) ARCH="amd64" ;;
arm64 | aarch64) ARCH="arm64" ;;
*)
error "Unsupported arch: $ARCH"
exit 1
;;
esac
BINARY="bin/dev-linux-$ARCH"
# Build if binary doesn't exist or source is newer
if [[ ! -f "$BINARY" ]] || [[ "main.go" -nt "$BINARY" ]]; then
if ! command -v go &>/dev/null; then
error "Go not installed. Install with: sudo pacman -S go"
exit 1
fi
log "Building dev tool..."
mkdir -p bin
if go build -ldflags="-s -w" -o "$BINARY" main.go; then
log "✅ Built: $BINARY"
else
error "Build failed"
exit 1
fi
fi
# Run the binary
exec "$BINARY" "$@"

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module marianozunino/dev
go 1.24.1

677
main.go Normal file
View file

@ -0,0 +1,677 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
)
// Colors for terminal output
const (
Red = "\033[0;31m"
Green = "\033[0;32m"
Blue = "\033[0;34m"
Yellow = "\033[0;33m"
NC = "\033[0m" // No Color
)
// Global configuration
type Config struct {
ScriptDir string
EnvFile string
RunsDir string
LogsDir string
DryRun bool
Verbose bool
Filters []string
Command string
}
// Script represents an executable script
type Script struct {
Path string
Name string
Description string
}
// ExecutionResult tracks script execution outcomes
type ExecutionResult struct {
Executed []string
Failed []string
}
// Logging functions
func log(msg string, args ...interface{}) {
fmt.Printf(Green+"[RUN]"+NC+" "+msg+"\n", args...)
}
func warn(msg string, args ...interface{}) {
fmt.Printf(Yellow+"[WARN]"+NC+" "+msg+"\n", args...)
}
func errorLog(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, Red+"[ERROR]"+NC+" "+msg+"\n", args...)
}
// Check if command exists in PATH
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// Check for required and optional dependencies
func checkDependencies() error {
requiredTools := []string{"git", "find", "grep"}
optionalTools := []string{"gitleaks"}
var missing []string
log("Checking dependencies...")
// Check required tools
for _, tool := range requiredTools {
if !commandExists(tool) {
missing = append(missing, tool)
}
}
// Check optional tools
for _, tool := range optionalTools {
if !commandExists(tool) {
warn("Optional tool missing: %s (recommended for security scanning)", tool)
} else {
log("✓ Found: %s", tool)
}
}
// Report missing required tools
if len(missing) > 0 {
errorLog("Missing required tools: %s", strings.Join(missing, ", "))
errorLog("Please install missing dependencies")
return fmt.Errorf("missing dependencies")
}
log("✓ All required dependencies found")
return nil
}
// Run security scan using gitleaks
func runSecurityScan() error {
log("Running security scan...")
if !commandExists("gitleaks") {
warn("GitLeaks not installed - skipping security scan")
warn("Install with: paru -S gitleaks")
fmt.Println()
fmt.Print("Continue without security scan? (y/N): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
errorLog("Push cancelled for security")
return fmt.Errorf("security scan cancelled")
}
return nil
}
log("Using GitLeaks for secret detection...")
cmd := exec.Command("gitleaks", "detect", "--verbose", "--exit-code", "1")
if err := cmd.Run(); err != nil {
errorLog("❌ Secrets detected! Review before pushing.")
return fmt.Errorf("secrets detected")
}
log("✅ No secrets detected")
return nil
}
// Check if we're in a git repository
func isGitRepo() bool {
cmd := exec.Command("git", "rev-parse", "--git-dir")
return cmd.Run() == nil
}
// Check for uncommitted changes
func hasUncommittedChanges() bool {
cmd := exec.Command("git", "diff-index", "--quiet", "HEAD", "--")
return cmd.Run() != nil
}
// Get current git branch
func getCurrentBranch() (string, error) {
cmd := exec.Command("git", "branch", "--show-current")
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
// Handle git push workflow
func handlePush() error {
log("Preparing to push repository...")
// Check if we're in a git repository
if !isGitRepo() {
errorLog("Not in a git repository")
return fmt.Errorf("not in git repo")
}
// Check for uncommitted changes
if hasUncommittedChanges() {
warn("You have uncommitted changes")
fmt.Println()
// Show git status
cmd := exec.Command("git", "status", "--short")
cmd.Stdout = os.Stdout
cmd.Run()
fmt.Println()
// Generate default commit message
defaultMsg := fmt.Sprintf("dev: automated commit - %s", time.Now().Format("2006-01-02 15:04:05"))
fmt.Print("Commit all changes? (Y/n): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "n" && answer != "no" {
fmt.Println()
fmt.Printf("Default: %s\n", defaultMsg)
fmt.Print("Custom commit message (or press Enter for default): ")
commitMsg, _ := reader.ReadString('\n')
commitMsg = strings.TrimSpace(commitMsg)
if commitMsg == "" {
commitMsg = defaultMsg
}
// Add and commit changes
if err := exec.Command("git", "add", ".").Run(); err != nil {
return fmt.Errorf("failed to add changes: %v", err)
}
if err := exec.Command("git", "commit", "-m", commitMsg).Run(); err != nil {
return fmt.Errorf("failed to commit changes: %v", err)
}
log("✓ Changes committed: %s", commitMsg)
}
}
// Run security scan
if err := runSecurityScan(); err != nil {
return err
}
// Get current branch
branch, err := getCurrentBranch()
if err != nil {
return fmt.Errorf("failed to get current branch: %v", err)
}
// Push to origin
log("Pushing branch '%s' to origin...", branch)
cmd := exec.Command("git", "push", "origin", branch)
if err := cmd.Run(); err != nil {
errorLog("❌ Push failed")
return fmt.Errorf("push failed")
}
log("✅ Successfully pushed to origin/%s", branch)
return nil
}
// Show help message
func showHelp(programName string) {
fmt.Printf(`%s - Development script runner
Usage: %s <command> [options] [arguments]
COMMANDS:
run [filters...] Run scripts matching filters (or all if no filters)
push,u,ush Commit, scan for secrets, and push to git origin
ls, list List all available scripts
new <name> Create a new script template
deps, check Check for required dependencies
help Show this help message
RUN OPTIONS:
--dry Show what would run without executing
--verbose Show detailed output during execution
EXAMPLES:
%s # Show this help
%s run # Run all scripts (with confirmation)
%s run docker # Run scripts containing 'docker'
%s run --dry api # Show what API scripts would run
%s ls # List all scripts
%s new backup # Create a new script called 'backup.sh'
%s push # Secure git push with secret scanning
SECURITY:
The push command automatically scans for secrets using GitLeaks
before pushing to prevent accidental credential exposure.
`, programName, programName, programName, programName, programName, programName, programName, programName, programName)
}
// Get script description from comment
func getDescription(scriptPath string) string {
file, err := os.Open(scriptPath)
if err != nil {
return "No description"
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "# NAME:") {
desc := strings.TrimSpace(strings.TrimPrefix(line, "# NAME:"))
if desc != "" {
return desc
}
}
}
return "No description"
}
// Check if script name matches any of the filters
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
}
// Find executable scripts in runs directory
func findScripts(runsDir string, filters []string) ([]Script, error) {
var scripts []Script
err := filepath.Walk(runsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check if file is executable
if info.Mode().IsRegular() && (info.Mode()&0o111) != 0 {
scriptName := filepath.Base(path)
if matchesFilters(scriptName, filters) {
scripts = append(scripts, Script{
Path: path,
Name: scriptName,
Description: getDescription(path),
})
}
}
return nil
})
if err != nil {
return nil, err
}
// Sort scripts by name
sort.Slice(scripts, func(i, j int) bool {
return scripts[i].Name < scripts[j].Name
})
return scripts, nil
}
// List all available scripts
func listScripts(runsDir string) error {
scripts, err := findScripts(runsDir, nil)
if err != nil {
return err
}
if len(scripts) == 0 {
warn("No executable scripts found in %s", 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
}
// Create a new script template
func createNewScript(runsDir, scriptName string) error {
if scriptName == "" {
return fmt.Errorf("script name required")
}
// Ensure runs directory exists
if err := os.MkdirAll(runsDir, 0o755); err != nil {
return err
}
// Add .sh extension if not provided
if !strings.HasSuffix(scriptName, ".sh") {
scriptName += ".sh"
}
scriptPath := filepath.Join(runsDir, scriptName)
// Check if script already exists
if _, err := os.Stat(scriptPath); err == nil {
return fmt.Errorf("script %s already exists", scriptName)
}
// Create script template
template := fmt.Sprintf(`#!/usr/bin/env bash
# NAME: %s script
set -euo pipefail
# Add your script logic here
echo "Running %s script..."
# Example:
# - Add your commands below
# - Use proper error handling
# - Add logging as needed
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
}
// Execute a single script
func executeScript(script Script, logsDir string, verbose bool) error {
logFile := filepath.Join(logsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log")
cmd := exec.Command(script.Path)
if verbose {
// Create log file
logFileHandle, err := os.Create(logFile)
if err != nil {
return err
}
defer logFileHandle.Close()
// Use MultiWriter to write to both stdout and log file
cmd.Stdout = io.MultiWriter(os.Stdout, logFileHandle)
cmd.Stderr = io.MultiWriter(os.Stderr, logFileHandle)
return cmd.Run()
} else {
// Only write to log file
logFileHandle, err := os.Create(logFile)
if err != nil {
return err
}
defer logFileHandle.Close()
cmd.Stdout = logFileHandle
cmd.Stderr = logFileHandle
return cmd.Run()
}
}
// Parse command line arguments
func parseArgs(args []string) (*Config, error) {
config := &Config{}
// Get current working directory (equivalent to bash SCRIPT_DIR)
scriptDir, err := os.Getwd()
if err != nil {
return nil, err
}
config.ScriptDir = scriptDir
config.EnvFile = filepath.Join(config.ScriptDir, ".env")
config.RunsDir = filepath.Join(config.ScriptDir, "runs")
config.LogsDir = filepath.Join(config.ScriptDir, "logs")
if len(args) == 0 {
return config, fmt.Errorf("help")
}
i := 0
// First argument should be the command
if i < len(args) {
arg := args[i]
switch arg {
case "--help", "help":
return config, fmt.Errorf("help")
case "run":
config.Command = "run"
i++
case "push", "u", "ush":
config.Command = "push"
i++
case "ls", "list":
config.Command = "list"
i++
case "new":
config.Command = "new"
i++
case "deps", "check":
config.Command = "deps"
i++
default:
return nil, fmt.Errorf("unknown command: %s", arg)
}
}
// Parse remaining arguments based on command
for i < len(args) {
arg := args[i]
switch arg {
case "--dry":
config.DryRun = true
case "--verbose":
config.Verbose = true
default:
if strings.HasPrefix(arg, "-") {
return nil, fmt.Errorf("unknown option: %s", arg)
}
config.Filters = append(config.Filters, arg)
}
i++
}
return config, nil
}
// Confirm execution of all scripts
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)
}
fmt.Println()
fmt.Print("Continue? (y/N): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
return answer == "y" || answer == "yes"
}
func main() {
const programName = "dev"
// Parse arguments
config, err := parseArgs(os.Args[1:])
if err != nil {
if err.Error() == "help" {
showHelp(programName)
os.Exit(0)
}
errorLog("Error: %v", err)
fmt.Println()
showHelp(programName)
os.Exit(1)
}
// Handle special commands
switch config.Command {
case "push":
if err := handlePush(); err != nil {
os.Exit(1)
}
os.Exit(0)
case "deps":
if err := checkDependencies(); err != nil {
os.Exit(1)
}
os.Exit(0)
case "list":
if err := listScripts(config.RunsDir); err != nil {
errorLog("Error listing scripts: %v", err)
os.Exit(1)
}
os.Exit(0)
case "new":
if len(config.Filters) == 0 {
errorLog("Script name required for 'new' command")
fmt.Printf("Usage: %s new <script-name>\n", programName)
os.Exit(1)
}
scriptName := config.Filters[0]
if err := createNewScript(config.RunsDir, scriptName); err != nil {
errorLog("Error creating script: %v", err)
os.Exit(1)
}
os.Exit(0)
case "run":
// Continue with script execution logic below
case "":
// No command provided, show help
showHelp(programName)
os.Exit(0)
default:
errorLog("Unknown command: %s", config.Command)
showHelp(programName)
os.Exit(1)
}
// Initialize runs directory if it doesn't exist (only for run command)
if _, err := os.Stat(config.RunsDir); os.IsNotExist(err) {
log("Creating runs directory at: %s", config.RunsDir)
if err := os.MkdirAll(config.RunsDir, 0o755); err != nil {
errorLog("Failed to create runs directory: %v", err)
os.Exit(1)
}
// Create example script
exampleScript := filepath.Join(config.RunsDir, "example.sh")
content := `#!/usr/bin/env bash
# NAME: Example script
echo "Hello from runs/example.sh!"
echo "Edit this script or add your own to runs/"
`
if err := os.WriteFile(exampleScript, []byte(content), 0o755); err != nil {
errorLog("Failed to create example script: %v", err)
os.Exit(1)
}
log("✅ Created runs/ directory with example script")
log("Use '%s ls' to list scripts or '%s new <name>' to create a new one", programName, programName)
os.Exit(0)
}
// Create logs directory
if err := os.MkdirAll(config.LogsDir, 0o755); err != nil {
errorLog("Failed to create logs directory: %v", err)
os.Exit(1)
}
// Find scripts to run
scripts, err := findScripts(config.RunsDir, config.Filters)
if err != nil {
errorLog("Error finding scripts: %v", err)
os.Exit(1)
}
if len(scripts) == 0 {
if len(config.Filters) > 0 {
warn("No scripts match the given filters: %s", strings.Join(config.Filters, ", "))
fmt.Printf("Use '%s ls' to see all available scripts\n", programName)
} else {
warn("No executable scripts found in %s", config.RunsDir)
fmt.Printf("Use '%s new <name>' to create a new script\n", programName)
}
os.Exit(0)
}
// Confirm execution if running all scripts without dry run
if len(config.Filters) == 0 && !config.DryRun {
if !confirmExecution(scripts) {
fmt.Println("Cancelled")
os.Exit(0)
}
}
// Execute scripts
result := ExecutionResult{}
for _, script := range scripts {
if config.DryRun {
fmt.Printf(Blue+"[DRY]"+NC+" Would run: %s - %s\n", script.Name, script.Description)
continue
}
fmt.Printf("\n"+Blue+"▶"+NC+" Running: %s\n", script.Name)
if script.Description != "No description" {
fmt.Printf(" %s\n", script.Description)
}
if err := executeScript(script, config.LogsDir, config.Verbose); err != nil {
errorLog("❌ %s failed (see %s)", script.Name,
filepath.Join(config.LogsDir, strings.TrimSuffix(script.Name, filepath.Ext(script.Name))+".log"))
result.Failed = append(result.Failed, script.Name)
} else {
log("✅ %s completed", script.Name)
result.Executed = append(result.Executed, script.Name)
}
}
// Print summary
if !config.DryRun {
fmt.Printf("\n" + Blue + "═══ SUMMARY ═══" + NC + "\n")
fmt.Printf("✅ Completed: %d\n", len(result.Executed))
if len(result.Failed) > 0 {
fmt.Printf("❌ Failed: %d\n", len(result.Failed))
fmt.Printf("\n" + Red + "Failed scripts:" + NC + "\n")
for _, failed := range result.Failed {
fmt.Printf(" • %s\n", failed)
}
os.Exit(1)
}
}
}

377
run
View file

@ -1,377 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# ═══════════════════════════════════════════════════════════
# 🔧 SETUP
# ═══════════════════════════════════════════════════════════
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
RUNS_DIR="$SCRIPT_DIR/runs"
LOGS_DIR="$SCRIPT_DIR/logs"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m'
# ═══════════════════════════════════════════════════════════
# 🛠️ HELPERS
# ═══════════════════════════════════════════════════════════
log() { echo -e "${GREEN}[RUN]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# ═══════════════════════════════════════════════════════════
# 🔍 DEPENDENCY VALIDATION
# ═══════════════════════════════════════════════════════════
check_dependencies() {
local missing=()
# Required tools
local required_tools=("git" "find" "grep")
# Optional but recommended tools
local optional_tools=("gitleaks")
log "Checking dependencies..."
# Check required tools
for tool in "${required_tools[@]}"; do
if ! command -v "$tool" &>/dev/null; then
missing+=("$tool")
fi
done
# Check optional tools
for tool in "${optional_tools[@]}"; do
if ! command -v "$tool" &>/dev/null; then
warn "Optional tool missing: $tool (recommended for security scanning)"
else
log "✓ Found: $tool"
fi
done
# Report missing required tools
if [[ ${#missing[@]} -gt 0 ]]; then
error "Missing required tools: ${missing[*]}"
error "Please install missing dependencies"
exit 1
fi
log "✓ All required dependencies found"
}
# ═══════════════════════════════════════════════════════════
# 🔒 SECURITY SCANNING
# ═══════════════════════════════════════════════════════════
run_security_scan() {
log "Running security scan..."
if command -v gitleaks &>/dev/null; then
log "Using GitLeaks for secret detection..."
if gitleaks detect --verbose --exit-code 1; then
log "✅ No secrets detected"
return 0
else
error "❌ Secrets detected! Review before pushing."
return 1
fi
else
warn "GitLeaks not installed - skipping security scan"
warn "Install with: paru -S gitleaks"
echo
read -p "Continue without security scan? (y/N): " -r answer
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
error "Push cancelled for security"
exit 1
fi
return 0
fi
}
# ═══════════════════════════════════════════════════════════
# 📤 PUSH COMMAND
# ═══════════════════════════════════════════════════════════
handle_push() {
log "Preparing to push repository..."
# Check if we're in a git repository
if ! git rev-parse --git-dir &>/dev/null; then
error "Not in a git repository"
exit 1
fi
# Check for uncommitted changes
if ! git diff-index --quiet HEAD --; then
warn "You have uncommitted changes"
echo
git status --short
echo
# Generate default commit message
local default_msg="dev: automated commit - $(date '+%Y-%m-%d %H:%M:%S')"
read -p "Commit all changes? (Y/n): " -r answer
if [[ ! "$answer" =~ ^[Nn]$ ]]; then
echo
echo "Default: $default_msg"
read -p "Custom commit message (or press Enter for default): " -r commit_msg
# Use default if empty
if [[ -z "$commit_msg" ]]; then
commit_msg="$default_msg"
fi
git add .
git commit -m "$commit_msg"
log "✓ Changes committed: $commit_msg"
fi
fi
# Run security scan
if ! run_security_scan; then
exit 1
fi
# Get current branch
local current_branch
current_branch=$(git branch --show-current)
# Push to origin
log "Pushing branch '$current_branch' to origin..."
if git push origin "$current_branch"; then
log "✅ Successfully pushed to origin/$current_branch"
else
error "❌ Push failed"
exit 1
fi
}
show_help() {
cat <<EOF
Usage: $0 [OPTIONS] [COMMAND|FILTERS...]
COMMANDS:
push Commit, scan for secrets, and push to git origin
deps, check Check for required dependencies
help Show this help message
SCRIPT EXECUTION:
[FILTERS...] Run scripts matching filters from runs/ directory
OPTIONS:
--dry Show what would run without executing
--verbose Show detailed output during execution
EXAMPLES:
$0 deps # Check dependencies
$0 push # Secure git push with secret scanning
$0 docker # Run scripts containing 'docker'
$0 --dry base # Show what base scripts would run
$0 --verbose all # Run all scripts with detailed output
SECURITY:
The push command automatically scans for secrets using GitLeaks
before pushing to prevent accidental credential exposure.
EOF
}
get_description() {
local script="$1"
grep '^# NAME:' "$script" 2>/dev/null | cut -d: -f2- | sed 's/^ *//' || echo "No description"
}
matches_filters() {
local script_name="$1"
shift
local filters=("$@")
[[ ${#filters[@]} -eq 0 ]] && return 0
for filter in "${filters[@]}"; do
if [[ "$script_name" == *"$filter"* ]] || [[ "${script_name,,}" == *"${filter,,}"* ]]; then
return 0
fi
done
return 1
}
# ═══════════════════════════════════════════════════════════
# 🚦 INITIALIZATION
# ═══════════════════════════════════════════════════════════
# Create runs directory if missing
if [[ ! -d "$RUNS_DIR" ]]; then
log "Creating runs directory at: $RUNS_DIR"
mkdir -p "$RUNS_DIR"
# Create example script
cat >"$RUNS_DIR/example.sh" <<'EOF'
#!/usr/bin/env bash
# NAME: Example script
echo "Hello from runs/example.sh!"
echo "Edit this script or add your own to $RUNS_DIR"
EOF
chmod +x "$RUNS_DIR/example.sh"
log "✅ Created runs/ directory with example script"
exit 0
fi
# Create logs directory
mkdir -p "$LOGS_DIR"
# Load environment
[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
# ═══════════════════════════════════════════════════════════
# 📝 PARSE ARGUMENTS
# ═══════════════════════════════════════════════════════════
DRY_RUN=0
VERBOSE=0
FILTERS=()
COMMAND=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry) DRY_RUN=1 ;;
--verbose) VERBOSE=1 ;;
--help | help)
show_help
exit 0
;;
push) COMMAND="push" ;;
deps | check) COMMAND="deps" ;;
-*)
error "Unknown option: $1"
show_help
exit 1
;;
*) FILTERS+=("$1") ;;
esac
shift
done
# ═══════════════════════════════════════════════════════════
# 🚦 HANDLE COMMANDS
# ═══════════════════════════════════════════════════════════
# Handle special commands first
case "$COMMAND" in
"push")
handle_push
exit 0
;;
"deps")
check_dependencies
exit 0
;;
"")
# No command, continue with script execution
;;
*)
error "Unknown command: $COMMAND"
show_help
exit 1
;;
esac
# ═══════════════════════════════════════════════════════════
# 🔍 FIND SCRIPTS TO RUN
# ═══════════════════════════════════════════════════════════
mapfile -t ALL_SCRIPTS < <(find "$RUNS_DIR" -type f -executable | sort)
if [[ ${#ALL_SCRIPTS[@]} -eq 0 ]]; then
warn "No executable scripts found in $RUNS_DIR"
exit 0
fi
SCRIPTS_TO_RUN=()
for script in "${ALL_SCRIPTS[@]}"; do
script_name="$(basename "$script")"
if matches_filters "$script_name" "${FILTERS[@]}"; then
SCRIPTS_TO_RUN+=("$script")
fi
done
if [[ ${#SCRIPTS_TO_RUN[@]} -eq 0 ]]; then
warn "No scripts match the given filters: ${FILTERS[*]}"
exit 0
fi
# ═══════════════════════════════════════════════════════════
# ⚠️ CONFIRMATION FOR RUNNING ALL SCRIPTS
# ═══════════════════════════════════════════════════════════
if [[ ${#FILTERS[@]} -eq 0 && $DRY_RUN -eq 0 ]]; then
echo -e "${RED}⚠️ About to run ALL scripts:${NC}"
for script in "${SCRIPTS_TO_RUN[@]}"; do
name="$(basename "$script")"
desc="$(get_description "$script")"
echo " • $name - $desc"
done
echo
read -p "Continue? (y/N): " -r answer
if [[ ! "$answer" =~ ^[Yy]$ ]]; then
echo "Cancelled"
exit 0
fi
fi
# ═══════════════════════════════════════════════════════════
# 🚀 EXECUTE SCRIPTS
# ═══════════════════════════════════════════════════════════
EXECUTED=()
FAILED=()
for script in "${SCRIPTS_TO_RUN[@]}"; do
name="$(basename "$script")"
desc="$(get_description "$script")"
log_file="$LOGS_DIR/${name%.*}.log"
if [[ $DRY_RUN -eq 1 ]]; then
echo -e "${BLUE}[DRY]${NC} Would run: $name - $desc"
continue
fi
echo -e "\n${BLUE}▶${NC} Running: $name"
[[ "$desc" != "No description" ]] && echo " $desc"
if [[ $VERBOSE -eq 1 ]]; then
if "$script" 2>&1 | tee "$log_file"; then
log "✅ $name completed"
EXECUTED+=("$name")
else
error "❌ $name failed (see $log_file)"
FAILED+=("$name")
fi
else
if "$script" &>"$log_file"; then
log "✅ $name completed"
EXECUTED+=("$name")
else
error "❌ $name failed (see $log_file)"
FAILED+=("$name")
fi
fi
done
# ═══════════════════════════════════════════════════════════
# 📊 SUMMARY
# ═══════════════════════════════════════════════════════════
if [[ $DRY_RUN -eq 0 ]]; then
echo -e "\n${BLUE}═══ SUMMARY ═══${NC}"
echo "✅ Completed: ${#EXECUTED[@]}"
[[ ${#FAILED[@]} -gt 0 ]] && echo "❌ Failed: ${#FAILED[@]}"
if [[ ${#FAILED[@]} -gt 0 ]]; then
echo -e "\n${RED}Failed scripts:${NC}"
for failed in "${FAILED[@]}"; do
echo " • $failed"
done
exit 1
fi
fi
exit 0

8
runs/test Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
# NAME: Enable and configure system services
set -euo pipefail
echo "Enabling and configuring system services..."
exit 0