go-run

module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 18, 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

All YAML configs support $VAR and ${VAR} syntax for environment variable expansion. Variables are expanded before YAML parsing.

runctl env: block

runctl.yaml supports a top-level env: block that defines environment variables available to all targets:

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

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

How it works:

  1. First pass: existing OS env vars are expanded, the env: block is extracted
  2. Each key-value pair is set via os.Setenv
  3. Second pass: the full config is re-expanded with the new env vars applied
  4. Target configs (gorun.yaml, execrun.yaml) also expand env vars when loaded

This means env vars from the env: block propagate into target configs, link URLs, and any other string field.


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