dev

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: Apache-2.0 Imports: 39 Imported by: 0

README

dev

dev runs an app's whole development setup in one terminal: Postgres 18 and an S3-compatible server natively, with data kept between runs; its Go servers, rebuilt and restarted when their sources change; and processes beside them, such as Vite.

It belongs in the app's tools module, not the service's: built under the service module's godebug fips140=only, the S3 emulator's MD5 ETags would panic. So from the repository root it runs as go -C tools tool dev, not go tool dev, which only finds the root module's tools. Apps usually wrap it, as make dev or pnpm dev; every example below works the same way with go -C tools tool in front.

go -C tools get -tool github.com/parallelworks/foundation/dev/cmd/dev
go -C tools tool dev                # everything
go -C tools tool dev web            # only the web service
go -C tools tool dev restart api    # another terminal: rebuild and restart api
go -C tools tool dev logs api web   # another terminal: their output

Describe the app in dev.json at the repository root:

{
  "name": "shop",
  "postgres": { "parameters": { "max_connections": "300" } },
  "s3": {},
  "env": {
    "SHOP_DATABASE_URL": "{postgres}",
    "SHOP_TEST_DATABASE_URL": "{postgres_test}",
    "SHOP_S3_ENDPOINT": "{s3}",
    "SHOP_VITE_URL": "http://localhost:5173"
  },
  "services": [
    {
      "name": "server",
      "build": ["go", "build", "-o", "tmp/shop", "./cmd/shop"],
      "run": ["tmp/shop", "serve"],
      "exclude": ["tmp", "web", "tools"],
      "extensions": [".go", ".sql"],
      "url": "http://localhost:8080",
      "health": "http://localhost:8080/readyz"
    },
    { "name": "web", "run": ["pnpm", "--filter", "web", "dev"] }
  ]
}
Command
dev [service...] Run the stack and the services named, or every service not marked manual, until interrupted. In a terminal it shows the interactive view; piped, in CI or with --plain it prints each line prefixed with its source
dev status [--json] Show the running dev's services, their states and URLs; --json adds its ports, pid and version
dev start, stop, restart service Drive one service of the running dev; restart rebuilds a server
dev logs [service...] [-f] Print services' output from the latest run (all of them when none is named), and with -f keep following it
dev up [service...] Start dev in the background, and return once its services are up (or with why not); --for 2h stops it later
dev down Stop the dev running here
dev ps List every dev running on this machine: checkout, pid, ports
dev exec -- command Run a command with the running dev's environment: allocated ports, {postgres}, {s3}
dev stack Run Postgres on :5432 and S3 on 127.0.0.1:8333 until interrupted. User, password and database are name (required with Postgres); tests get name_test
dev wait [service...] Block until a stack started elsewhere accepts connections, or until the services named are up; fails as soon as one fails, exits or turns unhealthy
dev reset Delete the stack's data

Data and each service's latest log live in .devstack/ beside dev.json, or --dir. The command finds dev.json from the working directory up, which is how go -C tools tool dev finds it from tools/. A Postgres left running by a stack that was killed is stopped on the next start.

The interactive view

In a terminal, dev takes over the screen with its services, their states (building, starting, ready, running, unhealthy, failed, exited, stopped) and the stack's addresses:

Key
↑ ↓ Select a service
enter Its output; esc goes back, ↑ ↓ scroll, ctrl+u ctrl+d (or pgup pgdn) page, g jumps to the oldest line, G (or end) follows
a Every service's output, interleaved
r Restart the service, rebuilding a server
s Start or stop it, including a manual one
q Stop everything and quit

dev status, start, stop and restart reach a running dev through a Unix socket in a directory only you can enter ($XDG_RUNTIME_DIR/foundation-dev, or ~/.local/state/foundation-dev), so a second terminal or a tool can drive it while the view runs. Only one dev runs per checkout.

Services

A service with build is a server: built, run, and rebuilt and restarted when a file with one of its extensions (default .go) changes under its watch directories (default its dir). A build that fails stops it until the next change, so nothing runs stale code, and only one service builds at a time, so several servers do not exhaust a laptop's memory. A service without build runs once; if it exits, it is left stopped so its error is not buried under restarts.

A service with a health URL reads starting once its process is up, ready when the URL answers 2xx, and unhealthy if it stops answering, or never answers within a minute. Without one, a service reads running as soon as its process is up. dev wait api web waits for exactly that, for scripts, CI and tools that need the app answering before they go on.

A service's url is where to open it: the view shows it as a link the terminal opens on cmd-click, dev status prints it, and dev logs it when the service comes up. Without one, a service with a health URL links to that URL's origin, and any other takes the first local address it prints, as Vite and Storybook do when they start.

Each service runs in its dir, relative to dev.json. Each command runs in its own process group, and stopping one ends everything it started, such as the Vite under pnpm. Unix only.

Tools and agents

A tool, or an agent working in its own worktree, can run the app without a terminal: dev up starts it in the background and returns once it is up (or with the services' errors), dev status --json gives its URLs and ports, dev exec -- make test runs tests against that checkout's stack, dev logs reads output, and dev down stops it. A detached dev also stops itself when its checkout is deleted, as a finished worktree is, and after --for.

Ports

ports names the ports the services listen on, with the one each prefers:

"ports": { "server": 8080, "web": 5173 },
"env": { "SHOP_HTTP_ADDR": ":{port.server}", "SHOP_VITE_URL": "http://localhost:{port.web}" },
"services": [
  { "name": "server", "url": "http://localhost:{port.server}", "health": "http://localhost:{port.server}/readyz", … },
  { "name": "web", "run": ["pnpm", "--filter", "web", "dev", "--port", "{port.web}", "--strictPort"] }
]

dev gives each its preferred port when nothing holds it, and the next free one otherwise, logging the move; Postgres and S3 do the same. {port.<name>} stands for the allocated port in env, run, build, before, url and health, and {postgres} and {s3} follow theirs, so a second checkout, or another app, runs beside the first without either changing its config.

Environment

Every command gets, from lowest to highest precedence: env in dev.json; the service's own env; envFile (default .env beside dev.json, skipped when missing); the service's envFile, relative to its dir; and the real environment, so SHOP_LOG_LEVEL=info make dev overrides everything. Values in env may name the stack: {postgres} and {postgres_test} are database URLs, and {s3} is the S3 endpoint. Keep .env for secrets and personal settings.

before commands run once, in order, before anything starts, such as generating files a server embeds. If one fails, dev stops with its error.

Your own command

An app with its own development tasks builds its command on the same base:

func main() {
	root := dev.NewRootCmd(dev.Config{Name: "shop", Postgres: &dev.Postgres{}, S3: &dev.S3{}})
	root.AddCommand(seedCmd)
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	if err := root.ExecuteContext(ctx); err != nil {
		os.Exit(1)
	}
}

Documentation

Overview

Package dev runs a web service's local development dependencies without Docker, and is the base for an app's own development command.

Index

Constants

View Source
const ConfigFile = "dev.json"

ConfigFile is the name the dev command looks for, from the working directory up, so that `go -C tools tool dev` still finds the repository's.

Variables

This section is empty.

Functions

func FindConfig

func FindConfig(dir string) (string, error)

FindConfig returns the path of the nearest dev.json in dir or its parents.

func LogPath added in v0.4.0

func LogPath(logs, service string) string

LogPath is where a service's output from the latest run is kept, given the directory of a stack's logs.

func NewRootCmd

func NewRootCmd(cfg Config) *cobra.Command

NewRootCmd returns the dev command with the standard subcommands. An app with its own development tasks adds them with AddCommand.

func ResetStack

func ResetStack(ctx context.Context, cfg Config, logger *slog.Logger) error

ResetStack deletes the stack's data. A server still running from the same directory is stopped first, since it would recreate files as they go.

func Up added in v0.2.0

func Up(ctx context.Context, cfg Config, logger *slog.Logger, out io.Writer, names ...string) error

Up runs the stack and the services until ctx ends: those named, or every service not marked Manual. Each service's output is prefixed with its name and also written to its log under cfg.Dir.

func WaitStack

func WaitStack(ctx context.Context, cfg Config, timeout time.Duration) error

WaitStack blocks until a stack started elsewhere accepts connections, so that a server started alongside it does not race it.

Types

type Config

type Config struct {
	// Name is the Postgres user, password and database, and required with
	// Postgres. Tests get Name_test.
	Name string `json:"name"`
	// Root is the directory commands run in and relative paths resolve
	// against. LoadConfig sets it to the config file's directory; otherwise
	// it defaults to the working directory.
	Root string `json:"-"`
	// Dir keeps Postgres data, S3 objects and service logs between runs;
	// defaults to .devstack.
	Dir string `json:"dir,omitempty"`
	// Env is set for every command. Values may name the stack:
	// {postgres} and {postgres_test} are database URLs, {s3} the S3 endpoint.
	Env map[string]string `json:"env,omitempty"`
	// EnvFile is a dotenv file for personal settings and secrets, over Env
	// and under the real environment. Defaults to .env; a missing file is
	// skipped.
	EnvFile string `json:"envFile,omitempty"`
	// Ports names the ports the app's services listen on, with the one each
	// prefers, such as {"server": 8080, "web": 5173}. dev gives each its
	// preferred port when free and the next free one otherwise, and
	// {port.<name>} stands for it in env, commands, url and health.
	Ports map[string]int `json:"ports,omitempty"`

	// Postgres runs when set.
	Postgres *Postgres `json:"postgres,omitempty"`
	// S3 runs when set.
	S3 *S3 `json:"s3,omitempty"`
	// Before lists commands run once, in order, before anything starts, such
	// as generating files a server embeds. A failure stops dev.
	Before [][]string `json:"before,omitempty"`
	// Services run until dev stops: servers rebuilt when their sources
	// change, and processes such as a Vite dev server.
	Services []Service `json:"services,omitempty"`
	// contains filtered or unexported fields
}

Config describes an app's local stack.

func LoadConfig

func LoadConfig(path string) (Config, error)

LoadConfig reads a dev.json, rooted at its directory.

func (Config) DatabaseURL

func (c Config) DatabaseURL() string

DatabaseURL is the URL of the app's database, or "" without Postgres.

func (Config) TestDatabaseURL

func (c Config) TestDatabaseURL() string

TestDatabaseURL is the URL of the database for tests, or "" without Postgres.

type ExitError added in v0.6.0

type ExitError struct{ Code int }

ExitError carries a command's exit status out of `dev exec`, so that dev exits with it.

func (ExitError) Error added in v0.6.0

func (e ExitError) Error() string

type Postgres

type Postgres struct {
	// Port defaults to 5432.
	Port int `json:"port,omitempty"`
	// Parameters are server settings, e.g. max_connections for test suites
	// that open pools in parallel.
	Parameters map[string]string `json:"parameters,omitempty"`
}

Postgres configures the local PostgreSQL server.

type S3

type S3 struct {
	// Addr defaults to 127.0.0.1:8333.
	Addr string `json:"addr,omitempty"`
}

S3 configures the local S3-compatible server.

type Service added in v0.4.0

type Service struct {
	// Name prefixes its output, names its log, and selects it on the
	// command line.
	Name string `json:"name"`
	// Dir is where its commands run, relative to the root.
	Dir string `json:"dir,omitempty"`
	// Build compiles a server, e.g. ["go", "build", "-o", "tmp/shop", "./cmd/shop"].
	Build []string `json:"build,omitempty"`
	// Run starts it, e.g. ["tmp/shop", "serve"] or ["pnpm", "--filter", "web", "dev"].
	Run []string `json:"run"`
	// Watch lists the directories, relative to Dir, whose changes rebuild a
	// server; defaults to Dir. Hidden directories and node_modules are skipped.
	Watch []string `json:"watch,omitempty"`
	// Exclude names further directories to skip wherever they appear, such
	// as the frontend or the build output.
	Exclude []string `json:"exclude,omitempty"`
	// Extensions are the file types that rebuild a server; defaults to .go.
	Extensions []string `json:"extensions,omitempty"`
	// Env is set for this service over the config's Env, with the same
	// references to the stack.
	Env map[string]string `json:"env,omitempty"`
	// EnvFile is a dotenv file for this service, relative to Dir, over the
	// config's EnvFile.
	EnvFile string `json:"envFile,omitempty"`
	// URL is where to open the service, such as http://localhost:8080. The
	// interactive view links to it, and dev logs it once the service is up.
	URL string `json:"url,omitempty"`
	// Health is a URL that answers 2xx once the service can do its job, such
	// as http://localhost:8080/readyz. With it, the service reads starting,
	// then ready, or unhealthy if it stops answering.
	Health string `json:"health,omitempty"`
	// Manual services start only when named: `dev web worker`.
	Manual bool `json:"manual,omitempty"`
}

Service is a long-running command. With Build it is a server: built, run, and rebuilt and restarted when its sources change; a failed build stops it until the next change. Without, it runs once and is left stopped if it exits, so a startup error is not buried under restarts.

type Stack

type Stack struct {
	// contains filtered or unexported fields
}

Stack is a running set of local dependencies.

func StartStack

func StartStack(ctx context.Context, cfg Config, logger *slog.Logger) (*Stack, error)

StartStack starts the configured dependencies and returns once they accept connections. The first run downloads the Postgres binaries.

func (*Stack) Stop

func (s *Stack) Stop(ctx context.Context) error

Stop shuts the dependencies down; Postgres flushes its data first.

Directories

Path Synopsis
cmd
dev command
Command dev runs an app's local development stack as described by the nearest dev.json.
Command dev runs an app's local development stack as described by the nearest dev.json.

Jump to

Keyboard shortcuts

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