Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions internal/lister.go → cmd/ops/lister.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package internal
package main

import (
"fmt"
"io"
"strings"

"sean_seannery/opsfile/internal"
)

// FormatCommandList writes a formatted summary of the Opsfile's commands and
// formatCommandList writes a formatted summary of the Opsfile's commands and
// environments to w. Commands are printed in cmdOrder; environments in envOrder.
func FormatCommandList(w io.Writer, opsfilePath string, cmds map[string]OpsCommand, cmdOrder []string, envOrder []string) {
func formatCommandList(w io.Writer, opsfilePath string, cmds map[string]internal.OpsCommand, cmdOrder []string, envOrder []string) {
fmt.Fprintf(w, "Commands Found in [%s]:\n", opsfilePath)
fmt.Fprintln(w)

Expand Down
61 changes: 39 additions & 22 deletions internal/lister_test.go → cmd/ops/lister_test.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
package internal
package main

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"sean_seannery/opsfile/internal"
)

func TestFormatCommandList(t *testing.T) {
cases := []struct {
name string
path string
cmds map[string]OpsCommand
cmds map[string]internal.OpsCommand
cmdOrder []string
envOrder []string
want string
}{
{
name: "commands with descriptions",
path: "./Opsfile",
cmds: map[string]OpsCommand{
"tail-logs": {Name: "tail-logs", Description: "Tail CloudWatch logs", Environments: map[string][]string{"prod": {}}},
"show-profile": {Name: "show-profile", Description: "Using AWS profile", Environments: map[string][]string{"default": {}}},
"list-instance-ips": {Name: "list-instance-ips", Description: "List the private IPs of running instances", Environments: map[string][]string{"prod": {}}},
cmds: map[string]internal.OpsCommand{
"tail-logs": {Name: "tail-logs", Description: "Tail CloudWatch logs", Environments: map[string][]internal.ShellLine{"prod": {}}},
"show-profile": {Name: "show-profile", Description: "Using AWS profile", Environments: map[string][]internal.ShellLine{"default": {}}},
"list-instance-ips": {Name: "list-instance-ips", Description: "List the private IPs of running instances", Environments: map[string][]internal.ShellLine{"prod": {}}},
},
cmdOrder: []string{"show-profile", "tail-logs", "list-instance-ips"},
envOrder: []string{"default", "local", "preprod", "prod"},
Expand All @@ -39,9 +40,9 @@ func TestFormatCommandList(t *testing.T) {
{
name: "commands without descriptions",
path: "./Opsfile",
cmds: map[string]OpsCommand{
"deploy": {Name: "deploy", Environments: map[string][]string{"prod": {}}},
"restart": {Name: "restart", Environments: map[string][]string{"prod": {}}},
cmds: map[string]internal.OpsCommand{
"deploy": {Name: "deploy", Environments: map[string][]internal.ShellLine{"prod": {}}},
"restart": {Name: "restart", Environments: map[string][]internal.ShellLine{"prod": {}}},
},
cmdOrder: []string{"deploy", "restart"},
envOrder: []string{"prod"},
Expand All @@ -57,10 +58,10 @@ func TestFormatCommandList(t *testing.T) {
{
name: "mixed descriptions and no descriptions",
path: "examples/Opsfile",
cmds: map[string]OpsCommand{
"build": {Name: "build", Description: "Build the project", Environments: map[string][]string{"default": {}}},
"deploy": {Name: "deploy", Environments: map[string][]string{"prod": {}}},
"test": {Name: "test", Description: "Run tests", Environments: map[string][]string{"default": {}}},
cmds: map[string]internal.OpsCommand{
"build": {Name: "build", Description: "Build the project", Environments: map[string][]internal.ShellLine{"default": {}}},
"deploy": {Name: "deploy", Environments: map[string][]internal.ShellLine{"prod": {}}},
"test": {Name: "test", Description: "Run tests", Environments: map[string][]internal.ShellLine{"default": {}}},
},
cmdOrder: []string{"build", "deploy", "test"},
envOrder: []string{"default", "prod"},
Expand All @@ -77,8 +78,8 @@ func TestFormatCommandList(t *testing.T) {
{
name: "environment order preserved",
path: "./Opsfile",
cmds: map[string]OpsCommand{
"cmd": {Name: "cmd", Environments: map[string][]string{"zebra": {}, "alpha": {}}},
cmds: map[string]internal.OpsCommand{
"cmd": {Name: "cmd", Environments: map[string][]internal.ShellLine{"zebra": {}, "alpha": {}}},
},
cmdOrder: []string{"cmd"},
envOrder: []string{"zebra", "alpha"},
Expand All @@ -93,10 +94,10 @@ func TestFormatCommandList(t *testing.T) {
{
name: "column alignment with varying name lengths",
path: "./Opsfile",
cmds: map[string]OpsCommand{
"a": {Name: "a", Description: "short name", Environments: map[string][]string{"default": {}}},
"very-long-name": {Name: "very-long-name", Description: "long name", Environments: map[string][]string{"default": {}}},
"mid": {Name: "mid", Description: "medium", Environments: map[string][]string{"default": {}}},
cmds: map[string]internal.OpsCommand{
"a": {Name: "a", Description: "short name", Environments: map[string][]internal.ShellLine{"default": {}}},
"very-long-name": {Name: "very-long-name", Description: "long name", Environments: map[string][]internal.ShellLine{"default": {}}},
"mid": {Name: "mid", Description: "medium", Environments: map[string][]internal.ShellLine{"default": {}}},
},
cmdOrder: []string{"a", "very-long-name", "mid"},
envOrder: []string{"default"},
Expand All @@ -113,7 +114,7 @@ func TestFormatCommandList(t *testing.T) {
{
name: "single command",
path: "./Opsfile",
cmds: map[string]OpsCommand{"solo": {Name: "solo", Description: "Only command", Environments: map[string][]string{"prod": {}}}},
cmds: map[string]internal.OpsCommand{"solo": {Name: "solo", Description: "Only command", Environments: map[string][]internal.ShellLine{"prod": {}}}},
cmdOrder: []string{"solo"},
envOrder: []string{"prod"},
want: "Commands Found in [./Opsfile]:\n" +
Expand All @@ -127,9 +128,25 @@ func TestFormatCommandList(t *testing.T) {
{
name: "empty command map",
path: "./Opsfile",
cmds: map[string]OpsCommand{},
cmds: map[string]internal.OpsCommand{},
cmdOrder: []string{},
envOrder: []string{},
// Environments line renders as " \n" (two leading spaces, empty
// join result) — this is the intentional output for an empty env list.
want: "Commands Found in [./Opsfile]:\n" +
"\n" +
"Environments:\n" +
" \n" +
"\n" +
"Commands:\n",
},
{
name: "nil slices behave identically to empty slices",
path: "./Opsfile",
cmds: map[string]internal.OpsCommand{},
cmdOrder: nil,
envOrder: nil,
// Go: strings.Join(nil, sep) == "" and range over nil slice is a no-op.
want: "Commands Found in [./Opsfile]:\n" +
"\n" +
"Environments:\n" +
Expand All @@ -142,7 +159,7 @@ func TestFormatCommandList(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
FormatCommandList(&buf, tc.path, tc.cmds, tc.cmdOrder, tc.envOrder)
formatCommandList(&buf, tc.path, tc.cmds, tc.cmdOrder, tc.envOrder)
assert.Equal(t, tc.want, buf.String())
})
}
Expand Down
26 changes: 6 additions & 20 deletions cmd/ops/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ func main() {
// Best-effort: show available commands alongside help
if dir, dirErr := resolveOpsfileDir(flags.Directory); dirErr == nil {
opsfilePath := filepath.Join(dir, opsFileName)
if _, cmds, cmdOrder, envOrder, perr := internal.ParseOpsFile(opsfilePath); perr == nil {
if parsed, perr := internal.ParseOpsFile(opsfilePath); perr == nil {
fmt.Fprintln(os.Stderr)
internal.FormatCommandList(os.Stderr, opsfilePath, cmds, cmdOrder, envOrder)
formatCommandList(os.Stderr, opsfilePath, parsed.Commands, parsed.CommandOrder, parsed.EnvOrder)
}
}
os.Exit(0)
Expand All @@ -50,7 +50,7 @@ func main() {
}
}

vars, commands, cmdOrder, envOrder, err := internal.ParseOpsFile(filepath.Join(dir, opsFileName))
parsed, err := internal.ParseOpsFile(filepath.Join(dir, opsFileName))
if err != nil {
slog.Error("parsing Opsfile: " + err.Error())
os.Exit(1)
Expand All @@ -70,7 +70,7 @@ func main() {
displayPath = rel
}
}
internal.FormatCommandList(os.Stdout, displayPath, commands, cmdOrder, envOrder)
formatCommandList(os.Stdout, displayPath, parsed.Commands, parsed.CommandOrder, parsed.EnvOrder)
os.Exit(0)
}

Expand All @@ -80,27 +80,13 @@ func main() {
os.Exit(1)
}

resolved, err := internal.Resolve(args.OpsCommand, args.OpsEnv, commands, vars, envFileVars)
resolved, err := internal.Resolve(args.OpsCommand, args.OpsEnv, parsed.Commands, parsed.Variables, envFileVars, os.LookupEnv)
if err != nil {
slog.Error("resolving command: " + err.Error())
os.Exit(1)
}

if flags.DryRun {
if !flags.Silent {
for _, line := range resolved.Lines {
fmt.Println(line.Text)
}
}
return
}

shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}

if err := internal.Execute(resolved.Lines, shell, flags.Silent, os.Stderr); err != nil {
if err := internal.Execute(resolved.Lines, internal.DefaultShell(), flags.Silent, flags.DryRun, args.CommandArgs, os.Stderr); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode())
Expand Down
77 changes: 77 additions & 0 deletions examples/Opsfile_maclocal
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
APP_NAME=my-service
APP_PORT=8080
LOG_FILE=./logs/app.log

# PID_FILE is optional — set if your app writes a pidfile:
# PID_FILE=./tmp/app.pid

# CPU, memory, and disk usage snapshot
check-resources:
default:
@echo "=== CPU ===" && \
top -l 1 -n 0 | grep -E "^CPU" && \
@echo "=== Memory ===" && \
vm_stat | grep -E "Pages (free|active|wired)" && \
@echo "=== Disk ===" && \
df -h | grep -E "^/dev|Filesystem"

# Tail the local application log file
tail-logs:
default:
tail -f $(LOG_FILE)

# Show all processes for this service
ps-app:
default:
ps aux | grep -i $(APP_NAME) | grep -v grep

# List processes listening on all ports, or a specific port
list-ports:
default:
lsof -i -P -n | grep LISTEN
app:
lsof -i :$(APP_PORT) -P -n

# Kill whatever is holding the app port
kill-port:
default:
lsof -ti :$(APP_PORT) | xargs kill -9 && \
echo "Killed process on port $(APP_PORT)"

# Check if the local app health endpoint is responding
health-check:
default:
curl -sf http://localhost:$(APP_PORT)/health | python3 -m json.tool

# List Homebrew-managed background services and their status
brew-services:
default:
brew services list

# Flush macOS DNS cache
flush-dns:
default:
sudo dscacheutil -flushcache && \
sudo killall -HUP mDNSResponder && \
echo "DNS cache flushed"

# Basic network connectivity check
network-check:
default:
echo "=== Default Route ===" && \
netstat -rn | grep default && \
echo "=== DNS ===" && \
scutil --dns | grep nameserver | head -3 && \
echo "=== Internet ===" && \
ping -c 3 1.1.1.1

# Show open file descriptors and socket counts for the app process
open-files:
default:
lsof -c $(APP_NAME) | wc -l | xargs echo "open file descriptors:" && \
lsof -c $(APP_NAME) -i | wc -l | xargs echo "open sockets:"

# Open the macOS system log in Console.app
open-console:
default:
open -a Console
Loading
Loading