next-watch
v0.7.1
Published
Terminal dashboard that watches a Next.js repo: pulls origin, restarts only the dev servers that changed, and keeps Claude Code and Codex sessions alive.
Maintainers
Readme
bun add -d next-watch
bunx next-watch --start devWith npm instead: npm install --save-dev next-watch, then npx next-watch --start dev.
The screen
One screen, redrawn in place. Two servers, the live agent sessions, the service status with the quota window each CLI is keeping open, and an event log of everything the watch has done:
╭─ next-watch 0.5.0 13:34:17 up 28s ───────────────────────────────────────────────────────────╮
│ REPO /tmp/nw-shot/site main 4bb9081 1 commit(s) behind origin/main │
│ nothing pulled during this watch · next git check 3573s │
│ SSH ssh-agent: 1 key(s) loaded │
│ │
│ SERVER STATE URL OWNER MODE UPTIME │
│ web up http://localhost:3400 pid 1133095 bun 27s │
│ admin up http://localhost:3401 pid 1133107 bun 27s │
│ │
│ Claude Max 20x │
│ 5h 0% ···················· resets 17:50 (in 4h15m) │
│ 7d 58% ███████████▋········ resets 07:00 (in 2d17h) │
│ Fable 7d 76% ███████████████▎···· resets 07:00 (in 2d17h) 13m ago │
│ Codex Plus │
│ 5h 0% ···················· resets 18:31 (in 4h57m) │
│ 7d 14% ██▊················· resets 00:20 (in 2d10h) 28s ago │
│ │
│ SESSION TREE AGENT STATUS MODEL CONTEXT IDLE VER │
│ site-5a │
│ site claude idle opus-5 38k 2m 2.1.274 │
│ site-4e │
│ site claude idle opus-5 38k 1h4m 2.1.274 │
│ not in tmux · (r)estart needs tmux │
│ │
│ SERVICE Claude ● All Systems Operational │
│ session: off │
│ OpenAI ● All Systems Operational │
│ session: auto · next refresh when the window closes (18:31) │
│ │
│ TOOL claude 2.1.274 · codex 0.154.0 │
├─ event log ────────────────────────────────────────────────────────────────────────────────────┤
│ 13:28:03 ⇣ pulled 2 commit(s) daf3f9f -> 4bb9081 │
│ 13:28:03 · 4bb9081 fix(web): the page greeting Dev 20s ago │
│ 13:28:03 · d8ce354 feat(web): a badge component Dev 20s ago │
│ 13:28:03 · 2 file(s) +2 -1 web/ 2 │
│ 13:28:03 · -> restart web │
│ 13:28:03 ○ web: stopping pid 955144, 955145 │
│ 13:28:04 ○ web stopped │
│ 13:28:04 ○ web: bun run dev │
│ 13:28:04 ○ web is listening on http://localhost:3400 │
│ 13:29:40 ✧ claude 2.1.274 is out (installed 2.1.273), updating ... │
│ 13:29:41 » update tool:claude ... │
│ 13:29:42 · Updating to 2.1.274... │
│ 13:29:43 ✧ claude 2.1.273 -> 2.1.274 │
│ 13:29:44 » update tool:claude done 4.1s │
│ 13:31:24 » restart server:admin ... │
│ 13:31:24 ○ admin: bun run build:admin ... │
│ 13:31:24 · $ node -e "console.log('compiled 14 routes')" │
│ 13:31:24 · compiled 14 routes │
│ 13:31:24 ○ admin: build:admin done │
│ 13:31:24 ○ admin: stopping pid 955153, 955154 │
│ 13:31:24 ○ admin stopped │
│ 13:31:24 ○ admin: bun run admin │
│ 13:31:25 ○ admin is listening on http://localhost:3401 │
│ 13:31:25 » restart server:admin done 0.3s │
│ 13:31:25 ✎ Codex quota session is closed (5h window reset 0min ago, 0% used), opening it with │
│ codex exec --skip-git-repo-check "hi" ... │
│ 13:31:29 ○ codex exec --skip-git-repo-check "hi" done 4.2s │
│ 13:32:01 » restart session:867126 ... │
│ 13:32:01 ○ SIGTERM to session 867126 in pane %35 │
│ 13:32:02 ◆ session ended: site-6a [site] │
│ 13:32:03 ○ typed "claude --resume 4d717cc5-942a-496a-a5e8-1e44c364bf66" into pane %35 │
│ 13:32:03 » restart session:867126 done 1.4s │
│ 13:32:05 ◆ session started: site-5a [site] │
│ 13:32:26 ▲ 1 commit(s) waiting on origin/main, not pulling │
│ 13:32:26 · db/data.txt - this machine owns the database │
│ 13:32:26 · look at them and pull by hand. │
├─ web access /tmp/nw-shot/site/log/web.txt ────────────────────────────────────────────────────┤
│ 13:32:09 GET 404 12ms /missing │
│ 13:32:09 GET 200 11ms / │
│ 13:32:09 GET 404 11ms /missing │
├─ admin access /tmp/nw-shot/site/log/admin.txt ────────────────────────────────────────────────┤
│ 12:23:14 GET 200 12ms / │
│ 12:23:14 GET 200 11ms / │
│ 12:23:15 GET 200 12ms / │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
⠼ Tab select · up/down scroll · (h)elp · (q)uitWhat it does
Built for the case where the machine that runs the code is not the machine the code is written on — a spare box, a second laptop, a machine on the desk that serves a site while the work happens elsewhere. Nobody is at that keyboard, so it has to be able to explain itself afterwards. Every capability below is one line; the rules behind them are in Reference.
Watch and ship
- Polls a remote branch on an interval and pulls what is there.
- Runs
<pm> installwhen the lockfile moved — the package manager read from the lockfile itself. - Restarts only the servers whose paths the incoming files actually touched.
- Runs
afterPullcommands once the servers are back, and refuses a pull that would overwrite apull.blockedpath. --once --dry-runprints what would be pulled and what would restart, and spawns nothing.
Servers
--start <script>makes an npm script a server; repeatable, and needs no config file.--build <script>runs before anything is stopped, and a failed build leaves the old server up.preStartprepares something first — an npm script, or a function.restartPaths/ignorePathsnarrow what a restart is for.detached: trueservers outlive the watch, and the next watch adopts the running process.- Servers that start and stop their own way are written out as an adapter instead.
- One log pane per server, newest at the bottom, request lines coloured by status.
Agents
- Lists the live Claude and Codex sessions on this machine: tree, model, context, idle, version.
- Restarts one — SIGTERM, then its resume command typed back into the same tmux pane — and kills
or stops anything in a
taskslist the config file supplies. - Restart is the one verb that needs tmux. A session started outside a pane is listed and can
be stopped, and its row says
not in tmux · (r)estart needs tmux. - A live
claudethe CLI wrote no session record for is listed too, with its unknown columns left empty and a note saying so. Linux only — the cwd comes from/proc.
Quota windows
- Shows how much of each five-hour window is spent, and when it resets.
- Opens a closed one (
claude -p "hi",codex exec "hi") in an empty sandbox directory — automatically, or only at times you list, decided per CLI. - One message per window, and another attempt only after one fails.
- The line under each service row says which mode is on and what it is waiting for.
Tools and services
- Shows the installed CLI versions against the newest published release.
- Installs a new Claude Code or Codex itself, one at a time, once per release —
tools: { autoUpdate: false }keeps the rows and the(u)pdateverb and installs nothing. - Shows the public status page of each service, openable in a browser from the screen.
- Shows whether ssh-agent holds a key, with an
(a)ddverb while it does not.
Never destructive on its own
- Blocked paths stop a pull rather than letting git overwrite a file a process holds open.
- A build runs before anything stops; a failed one changes nothing.
- Quit is refused once while something is running, so one Ctrl-C strands nothing.
- Stopping a server leaves the jobs it started in sessions of their own still running.
git fetchcannot raise an authentication prompt on a machine with nobody at the keyboard.- Nothing leaves the machine until a provider is switched on — the table below is all of it.
The screen
- One panel, redrawn in place, with the event log kept across restarts of the watch itself.
- Rows that overflow are wrapped, not cut.
- Off a terminal it is printed only on a change, so a piped watch does not fill a file.
- Tab moves a cursor, one letter runs that verb,
(f)ocusfills the frame,(h)elpprints the keys. - The Codex session rows need
/procand the restart verb needs tmux; without either they are absent.
What leaves the machine
Nothing at all until a provider is switched on — except on a run with no config file, where
every section but quotaSession is on and this is the traffic that follows. With all of them on,
and never more often than this:
| Where | What for | How often | Off with |
| ---------------------------------------------------------------- | --------------------------------- | ------------------------------------- | -------------------------------------- |
| api.anthropic.com/api/oauth/usage | the usage windows | ≤ 1/min | quota: false |
| codex app-server (a local process, not the network) | the same, for the other CLI | ≤ 1/min | quota: false |
| status.claude.com, status.openai.com | the public status summaries | ≤ 1/min | services: false |
| api.github.com/repos/<repo>/releases/latest | the newest published version | ≤ 1/10 min | tools: false |
| claude update, codex update (local processes) | installing a release that is out | once per release | tools: { autoUpdate: false } |
| claude -p "hi" (a local process, on the CLI's own credentials) | opening a closed five-hour window | once per closed window or listed time | quotaSession: false, claude: false |
| codex exec --skip-git-repo-check "hi" (the same, for Codex) | the same | the same | quotaSession: false, codex: false |
The usage windows are asked for less often than that in practice: not at all while a cache written by something else on this machine is still fresh.
The install is the one row where the watch itself opens no connection: it runs that CLI's own update command and reads its output. Where a CLI fetches its release from is that CLI's own business, and this package chooses nothing about it.
Every one of them has a timeout, none of them can throw, and a failure costs its own section and nothing else. The credentials the quota reads are the ones the CLI already wrote on this machine; the token goes to the endpoint that issued it and reaches no log, no event and no file.
How it works
origin/main ─▶ pull ─▶ install if the lockfile moved ─▶ restart ─▶ event logOnly the servers whose paths the incoming files touched are restarted, and a build that fails leaves the old one serving.
Zero config
Node 22 or later is all that is needed to run it — the package is plain ESM with one
dependency (yargs), and bun is the toolchain, not a requirement. In a Next.js project set up the
usual way, bunx next-watch --start dev is the whole setup: no config file, no adapter to write.
The first thing worth running is the one that changes nothing:
bunx next-watch --once --dry-run --start devIt prints what a pull would bring and what would restart, and spawns nothing at all.
--start <script> makes an npm script a server of this watch. It is started when the watch starts,
stopped when the watch stops, restarted when a pull brings in something it serves, and its output
goes to that server's log pane. The flag is repeatable — --start web --start admin.
For a server that has to be built first, --build <script> runs that script before anything is
stopped, and a build that fails leaves the running server alone and says so. That makes the
production shape:
bunx next-watch --start start --build buildWith no config file, the rest is derived: appName is the name in package.json (or the
directory's name), root is the working directory, branch is main, the remote is
origin/main, and a dependency change is package.json or whichever lockfile is there.
The package manager is the one the project already installs with, read from the lockfile:
bun.lock (or bun.lockb) → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn,
package-lock.json → npm, and npm where there is none. A checkout holding more than one lockfile
is read in that order, so a repository that moved to bun and left package-lock.json behind is a
bun repository. The install that follows a pull uses the same reading — <pm> install, with the
lockfile read at that moment, because the pull that swaps one lockfile for another is exactly the
pull that moves the dependencies. packageManager in package.json (corepack) is not consulted.
Config
You do not need a config file to start. You need one when:
- some incoming paths must never be pulled, because this machine is the one that writes them;
- a server needs a finer restart rule than "anything that is not a test file or a root-level document";
- there is more than one server, or one that has to prepare something before it starts, or one that should go on running after the watch is closed;
- there is a command to run after a pull that no server covers;
- you want to choose which of the sections above are drawn.
It is next-watch.config.mjs in the working directory unless --config says otherwise, and it is
a module rather than JSON because the interesting parts can be code — a predicate over paths, a
function that prepares something — though most servers need none of that.
A server is either described or written out. Describing one is naming its npm script and the
handful of decisions around it; { id: 'dev', script: 'dev' } is exactly what --start dev
builds, and a described server is started when the watch starts, because next-watch spawns that
child itself. Writing one out is supplying the four functions yourself, for a repository whose
starting and stopping are its own business — those are left alone at startup, since whether such a
server is already running is not something to decide on its behalf. The two forms may be mixed in
one servers array.
import { restartUnless } from 'next-watch/core';
// Your own functions, for the server that is written out below. next-watch
// never looks inside them — it only decides when to call which. Replace these.
const startAdmin = async emit => true;
const stopAdmin = async emit => true;
const buildAdmin = async () => true;
const isListening = async port => true;
export default {
// Names the cache and sandbox directories, and identifies this watcher to
// anything it talks to.
appName: 'my-site',
// root: import.meta.dirname, // a checkout other than the working directory
// branch: 'main', // the only branch it will merge into
// remote: 'origin/main', // watch a different remote branch
// logDir: 'log', // where the event log and pane positions live
// timezoneOffsetMinutes: 540, // pin the clocks; default: this machine's
pull: {
// ⚠️ What this checkout is the one that writes. Receiving such a file means
// something else wrote it, and letting git overwrite a file a running
// process holds open is not a conflict, it is destruction. Anything listed
// here stops the pull and asks for a person.
blocked: [
// { prefix: 'db/', reason: 'this machine owns the database' },
// { prefix: 'log/', reason: 'written by the servers running here' },
],
// Receiving one of these means the dependencies moved, so `<pm> install`
// runs before anything is restarted. Without it, a pull that changes
// package.json restarts servers onto old dependencies.
// dependencyPaths: ['package.json', 'bun.lock'],
},
servers: [
// Described: an npm script, and the decisions around running one.
{
id: 'web',
script: 'dev', // `<pm> run dev` — exactly what `--start dev` builds
// build: 'build', // runs first; if it fails, nothing stops
// preStart: 'prepare:web', // a script, or async emit => {}, first
// detached: true, // outlives the watch; the next one adopts it
// restartPaths: ['web/', 'lib/'], // restart only for these prefixes
// ignorePaths: ['docs/'], // or the opposite: anything except these
// label: 'the site', // the row's name; the id by default
},
// Written out: for a server whose starting and stopping are its own
// business. Mixing the two forms in one array is ordinary — describe what
// can be described, write out the rest.
{
id: 'admin',
// `restartUnless(prefixes)` restarts for anything **except** those
// prefixes (and test files, and root-level documents). `restartIfAny`
// is the opposite shape, for a dev server that reloads itself and needs
// a restart only for its own configuration.
restartOn: restartUnless(['docs/', 'web/']),
probe: async () => ({
server: 'admin',
state: (await isListening(3001)) ? 'up' : 'down',
url: 'http://localhost:3001',
mode: 'dev',
owner: 'me',
uptimeSeconds: null,
// The files this server's stdout collects in. The access pane reads
// their tail.
logFiles: ['log/admin.txt'],
}),
// ⚠️ Build before stopping anything. Stopping first and then finding the
// build broken leaves nothing running, which is worse than the old code
// still serving. Only the adapter knows what building means, so only the
// adapter can get this right — a described server gets the same order
// for free.
restart: async emit => {
emit('step', 'building ...');
if (!(await buildAdmin())) {
emit('error', 'build failed, leaving the old server up');
return false;
}
await stopAdmin(emit);
return startAdmin(emit);
},
stop: emit => stopAdmin(emit),
start: emit => startAdmin(emit),
},
],
// Work started outside the watcher, shown as its own section with a kill
// verb on each row.
// tasks: () => readMyTaskRegistry(),
// Commands to run after a pull that brought something in, once the servers
// are back. For what a checkout owns that no server adapter covers — a
// crontab to rewrite, a cache to warm.
// afterPull: [{ label: 'rewrite crontab', command: './scripts/cron.sh' }],
// Which of the machine-level sections are drawn. Omitted means none of them,
// so a config file that says nothing here gets a screen about the repository
// alone.
providers: {
// agentSessions: true, // live Claude / Codex sessions, restartable in tmux
// quota: true, // how much of each usage window is spent
// services: true, // the public status pages those CLIs depend on
// tools: true, // installed CLI versions — and it installs new ones
// sshAgent: true, // whether a key is loaded, and an (a)dd verb if not
//
// The two CLIs keep themselves up to date, which is what `tools: true`
// includes. To have the versions reported and nothing installed, say so
// once rather than copying the default list out:
// tools: { autoUpdate: false },
//
// The array forms, in place of `true`, for a machine that watches other
// pages or other CLIs. An entry's own `autoUpdate` decides for that one.
// services: [{ name: 'Anthropic', page: 'https://status.claude.com' }],
// tools: [
// { command: 'claude', repo: 'anthropics/claude-code', autoUpdate: false },
// ],
//
// ⚠️ The only switch that spends anything: it sends `claude -p "hi"` and
// `codex exec "hi"` to open a five-hour window that has closed, because a
// window left shut is a window off the day's total. It runs in an empty
// sandbox directory rather than in your repository, so the message costs
// one word instead of pulling a whole project's instructions into a
// context.
// quotaSession: true,
// Or only at these times, in this watch's own clock. Listing them turns
// the "whenever it is found closed" behaviour off.
// quotaSession: { at: ['06:00', '11:00', '16:00', '21:00'] },
// A key per CLI says something different about one of them. What it does
// not name keeps whatever the outer setting says.
// quotaSession: { claude: { at: ['09:00'] }, codex: false },
// quotaSession: { cwd: '/tmp/my-site-sandbox' }, // where they run
},
};Then run it:
bunx next-watchA config file and --start work together: the scripts named on the command line are added to
the file's own servers, and an id that is already taken is refused rather than silently doubled.
--build belongs to the servers named on the command line — a described entry carries its own.
providers in the file always wins over what a flag would have switched on.
Two things a repository often reaches for are already above and need nothing new: pull.blocked is
where paths that must never arrive go, and afterPull is where a command that has to run once a
pull has landed goes. --once --dry-run prints what a pull would bring, which servers would
restart, and — for a described server — the steps each restart would take, without running any of
them.
detached: true is for a server that should not go down when the dashboard does. It is spawned
into its own process group with its output going straight to its log file, and a note is left in
<logDir>/servers/<id>.pid. The next watch reads that note and adopts the process it names —
so reopening the dashboard shows the running server rather than failing to start a second one onto
a taken port. It is deliberately not stopped when the watch exits; stopping it is something a
person asks for, with (s)top on the screen.
The note is only trusted when the process it names is still the one it was written about: alive,
signalable by this user, running something whose command line still names the script, and — on
Linux, where /proc says so — started at the moment recorded. Pids get reused, and a note is a
hint about the world rather than a fact.
Keys and verbs
On a terminal the screen is operable, not only readable. Tab moves a cursor between the things on it, and a verb runs on whatever the cursor is on.
- Restart, stop or start a server — through that server's own adapter, which is the same path a pull takes, so the two cannot drift apart.
- Kill or stop a task — SIGKILL or SIGTERM to the pid the task list gave.
- Restart an agent session — SIGTERM, then its resume command is typed back into the same tmux pane, so the session comes back where it was.
- Open a service's status page in a browser.
- Update a tool — install the newest release of that CLI.
- Focus a log pane so it fills the frame, and again to go back.
- Run
ssh-add— offered only while the agent holds no key, and the terminal is handed over properly so the passphrase is typed tossh-addand not to the watcher. - Quit, which is refused once while something is running, so a single Ctrl-C does not strand a half-stopped server. Press it again to leave anyway.
The full key list, which (h)elp also prints:
Tab / Shift-Tab move the cursor · up/down scroll the log pane · Esc clear
press the letter in ( ) to run that verb on what the cursor is on
":" types a whole verb instead
server: (r)estart | (s)top | st(a)rt · task: (k)ill | (s)top
session: (r)estart | (s)top · service: (o)pen · tool: (u)pdate
session (r)estart = SIGTERM, then type its resume command back
into the same tmux pane; a session not in tmux says so on its row
log pane: (f)ocus to fill the frame, again to go back
ssh: (a)dd run ssh-add here (only shown while the agent has no key)
anywhere: (h)elp | (q)uitAgents in tmux
A session started inside a tmux pane is one next-watch can restart in place: it stops the session and types the resume command back into that same pane, so it comes back where it was. Started from a plain shell it cannot, and its row says so.
tmuxp is a convenient way to lay a team out:
mise use pipx:tmuxp # or: uv tool install tmuxp
tmuxp load .config/tmuxp.yaml# .config/tmuxp.yaml
session_name: agents
windows:
- window_name: workers
layout: even-horizontal
panes:
- start_directory: ./site
shell_command: claude
- start_directory: ./site
shell_command: codex
- start_directory: ./docs
shell_command: claudeEach pane is one agent in one working tree, and mixing the CLIs is ordinary — the panel lists both and tells them apart. Run next-watch in a window of its own beside them.
What only works through tmux
(r)estarton a session row. It is SIGTERM followed by typing the resume command into the session's own pane; with no pane there is nowhere to type. Without tmux the row is still listed, and(s)topstill works.- Nothing else. Every other section and verb is the same with or without tmux.
A session started by another Claude Code session inherits CLAUDE_CODE_CHILD_SESSION, and
the CLI then writes no ~/.claude/sessions/<pid>.json for it. Such a session is still listed —
it is live and spending the same quota as any other — but the row is built from ps alone, so
the model, the context, the idle time and the version are left empty rather than dashed: the
record that holds them was never written. Its status reads no record, and the note under it
gives how long the process has been running. It is never offered (r)estart, in a tmux pane or
out of one, because there is no session id to resume.
tmux list-panes -a gives each pane's id, its root process and what it is running now. The agent
is not that root process — the pane runs a shell and the agent is a descendant of it — so the
session's ancestors are walked and the innermost pane that matches wins. A session whose
ancestors reach no pane is outside tmux, and that is the same condition the restart verb uses, so
the note on the row and the verb list cannot disagree.
A pane is only offered a restart when it has returned to its shell: typing a command into a pane that is still running something would put the text into that program instead. A session with no readable resume command is not offered one either — stopping it would leave nothing behind but a prompt.
Reference
Which sections are drawn. The repository ones — the branch, the pulls, the servers, each
server's log pane and the event log — are always there. Tasks appears only when the config
file supplies a tasks list. The rest are about the machine rather than the repository: on a run
with no config file every one of them is drawn except the quota session, which waits for
--quota-session; with a config file, providers decides and nothing is drawn unless it says so.
The quota session, exactly. A window opens with its first message and closes five hours later, so time spent with it shut comes straight off the day's allowance.
- automatic (
quotaSession: true) — open one whenever it is found closed, which keeps one running around the clock. - on a schedule (
quotaSession: { at: ['09:00', '14:00'] }, or--quota-session-at) — open one only at those times, read in the watch's own clock (timezoneOffsetMinutes).atturns the automatic mode off: between the listed times a closed window stays closed. That is the point of it — a window opened at 04:00 is spent by the time the day starts.
Nothing is made up for a time that passed while the watch was not running, each listed time fires
at most once a day, and a time that comes round while a window is already open sends nothing and
says so. A key per CLI says something different about one of them
({ claude: { at: [...] }, codex: false }), and what it does not name keeps the outer setting. A
CLI that is not installed is skipped with one line in the log. Codex is sent
codex exec --skip-git-repo-check "hi", because the sandbox is deliberately not a git repository
and Codex refuses to run outside one without that flag.
One message per window, and retries only after a failure. A send that works is remembered
against the clock, so the window it opened is left alone for its whole five hours however long the
usage figures take to agree — they are cached, and a percentage that still rounds to zero is not
an empty window. A send that fails is tried again ten minutes later, three times, and then
nothing more until a window opens; the line under the service row says which
(failed 17:08 (exit 1) · retry 17:18). Either way that line names the mode and what it is
waiting for — session: manual · next refresh 14:00, or session: off.
What an automatic install writes. claude 2.1.280 is out (installed 2.1.273), updating ...,
then the installer's own output as it arrives, then claude 2.1.273 -> 2.1.280 once the new
version reads back.
next-watch [options]
watch a remote branch, pull it, and restart only what needs restarting
Options:
--version Show version number [boolean]
--config path to the config file
[string] [default: "next-watch.config.mjs"]
--interval how often to check git (seconds)[number] [default: 60]
--panel how often to redraw the status panel (seconds, 0 to
hide) [number] [default: 600]
--sample how often to poll local state (seconds)
[number] [default: 1]
--log lines in the event log pane (default: as many as the
terminal fits, 0 to hide) [number]
--access lines in each access log pane (default: auto, 0 to
hide) [number]
--start run this npm script as a server (repeatable; needs no
config file) [array] [default: []]
--build npm script that must pass before a --start server is
stopped for a restart [string]
--quota-session with no config file, allow the session that opens a
closed quota window [boolean] [default: false]
--quota-session-at open the quota window only at these times
(HH:MM,HH:MM); implies --quota-session
[array] [default: []]
--once check once and exit [boolean] [default: false]
--dry-run report what would be pulled without pulling
[boolean] [default: false]
--restart restart after pulling (--no-restart to pull only)
[boolean] [default: true]
-h, --help Show help [boolean]
-v, --verbose say more about what is happening [boolean]A server named by --start is started when the watch starts, which only the watching run
does: --once looks and leaves, so starting one there would be a side effect for no benefit. (A
plain --once that pulls something the server serves is the exception — the restart that follows
the pull starts it, and the process exiting a moment later stops it again.)
Note that which sections the panel draws is not a flag. Those are providers switches in the
config file, because they describe the machine rather than this particular run.
Each server gets a pane showing the tail of its log, newest at the bottom. The line it reads is the one a Next.js dev server writes:
GET /about 200 in 741msA server in production mode (next start) writes no request lines at all, and a pane that is
always empty is a pane nobody looks at. An application that wants its requests in the pane can
write that shape itself, and then one parser reads both — formatAccessLine produces exactly the
line above:
// instrumentation.ts
import { formatAccessLine } from 'next-watch/access-log';
export function register() {
// ...wherever your server records a finished request:
process.stdout.write(`${formatAccessLine({ method, target, status, ms })}\n`);
}Where nothing in a log parses as a request, the raw tail is shown instead: the startup and error lines are worth more there than an empty box.
| Import | What it is |
| ----------------------- | ------------------------------------------------------------------- |
| next-watch | startWatch, and the config types |
| next-watch/core | everything the dashboard decides and draws, with no I/O |
| next-watch/term | frames, tables and terminal widths |
| next-watch/access-log | reading and writing the access-log line |
| next-watch/process | ps snapshots, liveness, signals |
| next-watch/quota | the quota shapes and arithmetic. Pure — safe in a browser |
| next-watch/quota/io | reading that quota from the credentials, the endpoint and the cache |
Development
The toolchain is bun: bun install, then bun run lint, bun run typecheck,
bun test, bun run build. Using the package needs only node 22 or later — what is published
is plain ESM, and it runs under bun as well.
