A Herdr plugin. Drop a .herdr/dev.toml into a project, or
put project TOML files in the plugin config dir, press a key, and Herdr spawns
the tabs and panes for that project's dev stack —
tunnels, servers, simulators — then pulls the tunnel URL and rewrites the env
files that bake it in.
up— read project TOML config, create the tabs and panes it declares, and run each pane's command.sync— re-pull the tunnel URL (ngrok agent API, or a regex over a pane's output), rewrite the configured env vars, and restart the panes that bake them in. Run this whenever the tunnel rotates.down— close the tabs this project'supcreated.
Devup TOML is executable config: its cmd/command strings run in real shells
and its [[sync.apply]] blocks rewrite files. Treat a project's TOML like the
project's own code. Before running devup up in a repo you do not fully trust
(a clone, a PR branch, someone else's project), read its config first — pressing
the keybinding runs whatever commands it declares, as your user.
The plugin itself never runs anything not in that file, only writes the env keys
a [[sync.apply]] block names, and only closes the tabs its own up created.
Requires bun on PATH. ngrok features use the local ngrok agent API on :4040.
# local development
git clone <this repo> herdr-devup && cd herdr-devup
bun install
herdr plugin link "$PWD"
# or from GitHub once published
herdr plugin install <owner>/<repo>Verify:
herdr plugin action list --plugin alonz.devupThe plugin manifest cannot declare keys; add them to your Herdr config.toml:
[[keys.command]]
key = "prefix+u"
type = "plugin_action"
command = "alonz.devup.up"
description = "devup: start project layout"
[[keys.command]]
key = "prefix+y"
type = "plugin_action"
command = "alonz.devup.sync"
description = "devup: sync tunnel URL"
[[keys.command]]
key = "prefix+shift+u"
type = "plugin_action"
command = "alonz.devup.down"
description = "devup: close project layout"Then herdr server reload-config. Pressing the key in a workspace whose focused
pane sits inside a project with .herdr/dev.toml, or inside a working_dir/repo
matched by plugin config TOML, runs that project's layout.
Project-local config still works at .herdr/dev.toml. You can also put TOML
files in herdr plugin config-dir alonz.devup (or its projects/ subdir); those
match the focused pane by working_dir or by git repo name.
See examples/acme/.herdr/dev.toml for a full
sync example. Simple shape:
name = "Options Cafe"
description = "The main options.cafe monorepo"
working_dir = "~/Development/options-cafe/options.cafe" # ~ and $VARS expand
# repo = "options.cafe" # alternative matcher
[[tabs]]
name = "claude"
command = "claude --dangerously-skip-permissions --chrome"
[[tabs]]
name = "terminal" # no command: empty shell
[[tabs]]
name = "server"
[[tabs.panes]]
command = "php artisan serve"
[[tabs.panes]]
command = "npm run dev"
split = "down"Full shape with sync:
[project]
name = "myapp"
[[tabs]] # one tab
label = "dev" # `name` also works
[[tabs.panes]] # first pane = tab root
name = "web"
cmd = "bun run dev" # `command` also works
[[tabs.panes]] # splits from the previous pane
name = "api"
cwd = "services/api" # relative to project root / working_dir
cmd = "bun run dev"
split = "right" # right | down (default right)
ratio = 0.5 # optional split ratio
defer = true # don't run cmd at `up`; a sync starts it
[[sync]] # optional; repeatable
name = "tunnel-url"
source = "ngrok" # ngrok | pane
addr = 3000 # ngrok: prefer the tunnel for this local port
timeout_ms = 30000
restart = ["web", "api"] # panes to (re)start after env is written
[[sync.apply]]
file = ".env"
set = { PUBLIC_URL = "{url}" }source = "ngrok"— pollshttp://127.0.0.1:4040/api/tunnels, picks the https tunnel (preferringaddr). Value is available as{url}.source = "pane"— needspane = "<name>"andpattern = "<regex>"; scrapes that pane's recent output.{url}/{value}is capture group 1 (or the whole match);{1},{2}, … are individual groups;{match}is the whole match.
Each [[sync.apply]] sets KEY=value in file (created if missing): an existing
KEY= line (optionally export -prefixed) has its value replaced in place;
otherwise the line is appended. Placeholders in values are substituted from the
resolved sync value.
NEXT_PUBLIC_* and similar vars are baked at server startup, so the server must
start after the env is written. Panes in a sync's restart list are deferred
at up and started once the URL is known. restart also re-runs them on a later
sync (Ctrl-C, then the command again) when the tunnel rotates.
Per-project state (created tab and pane ids, pane commands) lives under
HERDR_PLUGIN_STATE_DIR, keyed by project path, so sync and down can find
the right panes. The plugin never edits files outside what [[sync.apply]]
declares.