go-run

module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 22, 2026 License: MIT

README

gorun

File watching run utilities for go and general use.

Tool Package Description
gorun pkg/gorun Drop-in go run replacement with auto-rebuild on file changes, can also have config file
execrun pkg/execrun Generic, language-agnostic file-watching command runner (YAML config)
runctl pkg/runctl Multi-target orchestrator — manage multiple gorun/execrun targets with HTTP API
runui pkg/runui runctl + embedded web dashboard for monitoring and control

All tools use content-based change detection (SHA-256 hashing) with polling and fsnotify. All rebuilding and watching is based on glob patterns.

Install

# gorun — Go auto-rebuild
go install github.com/gur-shatz/go-run@latest

# execrun — generic file watcher
go install github.com/gur-shatz/go-run/cmd/execrun@latest

# runctl — multi-target orchestrator (API only)
go install github.com/gur-shatz/go-run/cmd/runctl@latest

# runui — multi-target orchestrator with web dashboard
go install github.com/gur-shatz/go-run/cmd/runui@latest

Or from source:

make install

gorun

Use directly from the CLI or with a config file:

# CLI — no config needed
gorun ./cmd/server -port 8080

# Config — run with configuration from gorun.yaml
gorun

Both modes build to a temp binary, watch for changes (default: **/*.go, go.mod, go.sum), and auto-rebuild on change.

Config File

gorun.yaml (gorun init generates a starter):

watch:
  - "**/*.go"
  - "go.mod"
  - "go.sum"
  - "!vendor/**"

args: "./cmd/server -port 8080"

exec:
  - "go generate ./..."
Field Required Description
watch no Glob patterns for files to watch (defaults to **/*.go, go.mod, go.sum)
args yes Build flags + target + app args, parsed like go run arguments
exec no Commands to run before go build
Usage
gorun [flags] [--] [go-build-flags] <target> [app-args...]
gorun [-c <file>]                   Load config and run
gorun [-c <file>] init              Generate a default config
gorun [-c <file>] sum               Snapshot file hashes
Flags
Flag Default Description
-c, --config <file> gorun.yaml Config file path
--poll <duration> 500ms Poll interval for file changes
--debounce <duration> 300ms Nagle debounce window
--stdout <file> Redirect child stdout to file (append mode)
--stderr <file> Redirect child stderr to file (append mode)
-v, --verbose false Show config, patterns, and file counts

--stdout/--stderr only redirect the child process output; gorun's own messages still print to the terminal.

Examples
gorun ./cmd/server -port 8080          # CLI args, no config needed
gorun -- -race ./cmd/server -port 8080 # Pass build flags with --
gorun -c myapp.yaml                    # Use a specific config file
gorun                                  # Load gorun.yaml from current dir
Behavior
  • Build failure: previous process keeps running
  • Child exits on its own: gorun keeps watching, rebuilds on next change
  • Sum files (gorun.sum) are persisted and updated on each rebuild
Library Usage
import "github.com/gur-shatz/go-run/pkg/gorun"

cfg, _ := gorun.LoadConfig("gorun.yaml")
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
gorun.Run(ctx, *cfg, gorun.Options{Verbose: true})
Subprocess Control

Run gorun as a child process and receive structured events:

cmd := gorun.Command("./cmd/server", "-port", "8080")
cmd.OnEvent = func(event gorun.Event) {
    switch event.Type {
    case gorun.EventStarted:
        log.Printf("started pid=%d build=%dms", event.PID, event.BuildTimeMs)
    case gorun.EventRebuilt:
        log.Printf("rebuilt pid=%d", event.PID)
    case gorun.EventBuildFailed:
        log.Printf("build failed: %s", event.Error)
    }
}
cmd.Start()
defer cmd.Stop()
cmd.Wait()

execrun

Generic, language-agnostic file-watching command runner. Works with any language or toolchain — configured via a simple YAML file.

Reuses gorun's internals (watcher, hasher, glob, sumfile) with shell commands instead of go build.

Quick Start
# Generate a starter config
execrun init

# Edit execrun.yaml for your project, then run
execrun
Usage
execrun [flags] [command]
execrun init
execrun sum
Flags
Flag Default Description
-c, --config <path> execrun.yaml Path to config file
--poll <duration> 500ms Poll interval for file changes
--debounce <duration> 300ms Debounce window
--stdout <file> Redirect child stdout to file (append mode)
--stderr <file> Redirect child stderr to file (append mode)
-v false Verbose output
Commands
Command Description
execrun init Generate a starter execrun.yaml
execrun -c myapp.yaml init Generate myapp.yaml
execrun sum Snapshot watched file hashes to execrun.sum
Config File

execrun.yaml:

# File patterns to watch (gitignore-style globs)
watch:
  - "**/*.go"
  - "go.mod"
  - "go.sum"

# Build commands — preparation steps that run to completion.
build:
  - "go build -o ./bin/app ."

# Exec commands — the last command is the long-running managed process.
exec:
  - "./bin/app"

Commands are executed via sh -c, so pipes, redirects, and environment variables all work.

Field Required Description
watch yes Glob patterns for files to watch (gitignore-style, ! for exclusions)
build no Preparation commands that run to completion before starting the process
exec no Run commands — the last is the managed process. Empty = build-only

At least one of build or exec must be non-empty.

Examples

Python (no build steps):

watch:
  - "**/*.py"
  - "requirements.txt"
exec:
  - "python app.py"

Node.js with TypeScript:

watch:
  - "src/**/*.ts"
  - "package.json"
build:
  - "npm run build"
exec:
  - "node dist/index.js"

Rust:

watch:
  - "src/**/*.rs"
  - "Cargo.toml"
build:
  - "cargo build"
exec:
  - "./target/debug/myapp"

Build-only (no managed process):

watch:
  - "*.css"
build:
  - "mkdir -p dist && cp style.src.css dist/style.css"
  - "echo CSS build complete"

Multi-step with code generation:

watch:
  - "**/*.go"
  - "api/**/*.proto"
  - "!**/*.pb.go"
build:
  - "protoc --go_out=. api/*.proto"
  - "go generate ./..."
  - "go build -o ./bin/server ./cmd/server"
exec:
  - "./bin/server"
Restart Flow
File change detected
  → Run build steps sequentially (fail → keep old process)
  → Stop old process (SIGTERM → 5s timeout → SIGKILL)
  → Start last exec command as new process

If there are no build steps, the old process is stopped and restarted directly.

If the managed process exits on its own, execrun waits for the next file change to re-run the pipeline.

Library Usage
import "github.com/gur-shatz/go-run/pkg/execrun"

cfg, err := execrun.LoadConfig("execrun.yaml")
if err != nil {
    log.Fatal(err)
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

err = execrun.Run(ctx, *cfg, execrun.Options{
    Verbose: true,
})

runctl / runui

Multi-target orchestrator. Manage multiple gorun and execrun targets from a single runctl.yaml, with an HTTP API for status and control. runui adds an embedded web dashboard on top.

runctl                # API only
runui                 # API + web dashboard at http://localhost:9100
Flags
Flag Default Description
-c, --config runctl.yaml Config file path
-v false Verbose output
Config File
env:
  PORT: "8080"
  DB_HOST: "localhost"

api:
  port: 9100              # HTTP API port (default: 9100)

logs_dir: /tmp/runctl-logs # Optional: directory for build/run log files

targets:
  api:
    type: gorun            # "gorun" or "execrun" (default: execrun)
    config: services/api/gorun.yaml
    enabled: true          # Start on launch (default: true)
    links:
      - name: HTTP
        url: "http://localhost:$PORT"

  frontend:
    config: services/frontend/execrun.yaml
Field Required Description
env no Environment variables available to all targets
api.port no HTTP API port (default: 9100)
logs_dir no Directory for log files (<target>.build.log/.run.log)
targets yes Map of target name to target config
targets.*.type no gorun or execrun (default: execrun)
targets.*.config yes Path to the target's gorun/execrun YAML config
targets.*.enabled no Whether to start on launch (default: true)
targets.*.links no Named URLs shown in the dashboard

The config path is relative to the runctl.yaml directory. The target's working directory is derived from the config path's directory.

Web Dashboard (runui)

The web UI provides two tabs:

  • Build — target type, last build duration/timestamp, build count, errors, and a rebuild button
  • Run — target state, PID, uptime, restart count, custom links, and start/stop/restart buttons

Each target has a log viewer with virtual scrolling and a real-time tail mode.

HTTP API
GET  /api/health                    Health check
GET  /api/targets                   List all targets
GET  /api/targets/{name}            Get target status
POST /api/targets/{name}/build      Trigger rebuild + restart
POST /api/targets/{name}/start      Start target
POST /api/targets/{name}/stop       Stop target
POST /api/targets/{name}/restart    Stop + rebuild + restart
POST /api/targets/{name}/enable     Enable + start
POST /api/targets/{name}/disable    Disable + stop
GET  /api/targets/{name}/logs       Get logs (?stage=build|run&offset=N&limit=M)
Library Usage
import "github.com/gur-shatz/go-run/pkg/runctl"

cfg, err := runctl.LoadConfig("runctl.yaml")
if err != nil {
    log.Fatal(err)
}

ctl, err := runctl.New(*cfg, ".")
if err != nil {
    log.Fatal(err)
}

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

ctl.Run(ctx)

Watch Patterns

Both gorun and execrun use the same glob pattern syntax (doublestar):

Pattern Matches
**/*.go All .go files recursively
cmd/**/*.go .go files under cmd/
*.go .go files in root only
{src,internal}/**/*.go .go files under src/ or internal/
**/*.{go,mdx,yaml} Multiple extensions

Patterns starting with ! are exclusions:

Pattern Effect
!**/*.pb.go Exclude protobuf generated files
!vendor/** Exclude vendor directory

Excludes always win. All include patterns are expanded first, then all exclude patterns are removed. You cannot re-include a file that was excluded.

Sum File

The sum file (e.g., gorun.sum, execrun.sum) is a human-readable snapshot of watched files and their SHA-256 hashes:

cmd/server/main.go a1b2c3d
go.mod 9abcdef
internal/handler.go e4f5678

Sum files are derived from the config filename (x.yaml generates x.sum), persisted in the working directory, and updated on each rebuild.

Stdout Protocol

gorun emits structured protocol lines to stdout for consumption by parent services:

[gorun:<event>] <json>
Event Description
started Initial build and run complete
changed File changes detected, before rebuild
rebuilt Successful rebuild and restart
build_failed Build failed, old process kept running
stopping Shutting down

Use gorun.ScanOutput() or gorun.ParseProtocolLine() to parse these events programmatically.

Environment Variables

Env var expansion in YAML configs

All YAML configs (gorun.yaml, execrun.yaml, runctl.yaml) support $VAR and ${VAR} syntax. Variables are expanded from the OS environment before YAML parsing, so you can use them in any string field:

# execrun.yaml
exec:
  - "./bin/server --port $PORT"
Passing env vars to child processes

Child processes (build steps, exec commands, compiled binaries) inherit the full environment of the parent process. There is no filtering or isolation — whatever is in the parent's environment is available to every child.

This means you can pass env vars to targets using any standard method:

# Inline
PORT=8080 gorun ./cmd/server

# Export
export PORT=8080
gorun ./cmd/server

# .env via direnv, dotenv, etc.
runctl/runui env: block

runctl.yaml supports a top-level env: block for defining env vars shared across all targets. These are set into the process environment via os.Setenv before any target configs are loaded:

env:
  PORT: "8080"
  DB_HOST: "localhost"

targets:
  api:
    type: gorun
    config: api/gorun.yaml
    links:
      - name: HTTP
        url: "http://localhost:$PORT"

Two-pass expansion: The config is parsed twice. First, existing OS env vars are expanded and the env: block is extracted and applied via os.Setenv. Then the full config is re-expanded with the new vars in place. This means:

  • Values in env: can reference pre-existing OS vars (e.g., HOME: "$HOME/app")
  • Vars defined in env: are available everywhere else in runctl.yaml (link URLs, etc.)
  • Target configs (gorun.yaml, execrun.yaml) are loaded after env: is applied, so they see these vars too
  • Child processes inherit them automatically

Propagation flow:

OS environment (PATH, HOME, …)
  + runctl.yaml env: block (PORT=8080, DB_HOST=localhost)
    → set via os.Setenv into parent process
      → target configs expand $PORT when loaded
      → child processes inherit PORT=8080 automatically

Note: gorun.yaml and execrun.yaml do not have their own env: block. To define env vars centrally, use the runctl/runui env: block or set them in the shell before running.


Design

  • Polling + hashing over fsnotify: simpler, portable, no file descriptor limits on macOS, catches content-only changes
  • go build to temp binary: clean process lifecycle — can keep the old binary running on build failure
  • Nagle debounce: batches rapid IDE saves into a single rebuild without adding latency to single-file changes
  • Content-based detection: only rebuilds when file contents actually change, not on metadata updates

Directories

Path Synopsis
cmd
execrun command
gorun command
runctl command
runui command
internal
cli
log
pkg
gorun
subprocess.go provides a library for running gorun as a subprocess and receiving structured callbacks on rebuild events.
subprocess.go provides a library for running gorun as a subprocess and receiving structured callbacks on rebuild events.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL