1dxway
v0.1.15
Published
One DX Way
Readme
1dx
One DX Way.
Install
Run the installer from any project (Node 18+):
npx 1dxwayRun the monitor after setup:
npx 1dxway startThe 1dx CLI itself runs on Node (it uses
node-ptyfor embedded service processes, which is currently broken on Bun). The services 1dx manages can still be written for and launched withbun— only the CLI host changed.
Service health checks
Each entry in services[].health of 1dx.json defines how 1dx decides whether a
service is up. The monitor probes services in parallel every ~1 s and runs the
result through a small state machine with hysteresis (a service must fail 3
consecutive probes to flip from RUNNING to STOPPED; a single success flips
it back to RUNNING).
Supported health check types:
tcp— open a TCP connection tohost:port(defaults to127.0.0.1). Use this for services whose port is reserved but where you don't need to speak the protocol on top.{ "type": "tcp", "host": "127.0.0.1", "port": 54322, "timeoutMs": 800 }http— issue aHEAD(orGET) request to a URL and require the status code to be inexpectedStatus(default: any 2xx/3xx/4xx that isn't 502/503/504). Use this for real liveness on HTTP services such as Vite, Kong, or edge functions.{ "type": "http", "url": "http://127.0.0.1:8080/", "method": "HEAD", "expectedStatus": [200, 301, 404], "bodyContains": "optional substring of GET response", "timeoutMs": 1500 }process— match a running process by image name and/or by a substring of its full command line. Command-line matching is the recommended option for scripts spawned by Bun/Node where the OS image name is justbun/node.{ "type": "process", "commandLineContains": "typesafe-i18n" }ambient— check an external dependency. The only supported check today isdocker, which callsdocker info(cached for ~5 s, invalidated on every observed up/down transition).{ "type": "ambient", "check": "docker" }all— composite. Runs all sub-checks in parallel. The service isRUNNINGonly if every sub-check is up; if some sub-checks are up and others are down, the service is reported asDEGRADED(yellow); if every sub-check is down, the service isSTOPPED.{ "type": "all", "checks": [ { "type": "tcp", "port": 54321 }, { "type": "tcp", "port": 54322 }, { "type": "http", "url": "http://127.0.0.1:54321/" } ] }
The legacy { "type": "port", "port": N } form is still accepted and is
auto-rewritten to { "type": "tcp", "host": "127.0.0.1", "port": N } at load
time, so existing configs keep working.
Dependencies between services
Add dependsOn: ["docker"] (or any other service id) to a service to make 1dx
skip probing it while its dependency is not up. The badge for a service whose
dependency is down/unknown renders as WAITING instead of flapping between
RUNNING and STOPPED.
{
"id": "backend",
"title": "Backend (Supabase)",
"dependsOn": ["docker"],
"health": { "type": "all", "checks": [/* ... */] }
}Status badges
● RUNNING— all health checks pass.◐ DEGRADED— some sub-checks of anallcomposite are passing, others aren't. Useful for backends like Supabase where Kong can be up while Postgres is still restarting.○ WAITING— service depends on another service that is not yet up.○ CHECKING— the first probe hasn't finished yet.○ STOPPED— service is down (after the hysteresis threshold). When the underlying PTY exits unexpectedly the state machine flips toSTOPPEDimmediately with the exit code in the reason field — no need to wait for the next probe.◐ STOPPING— 1dx is in the middle of stopping the service.
How services are run
Managed services run as node-pty children of the 1dx process — there are no
external terminal tabs anymore.
This means:
- 1dx owns each service's PID directly. Stopping a service is
pty.kill()on that PID; restarting iskill()+spawn(). No moretaskkill /Tby command-line substring, no more sibling services dying because two services share acommandLineContainsneedle. - Exit codes flow straight back into the state machine, so an unexpected
crash flips the row to
STOPPEDwith a reason likeprocess exited (code 1)instead of waiting for a probe to fail. - Output is buffered (~2000 lines per service, ANSI preserved) so you can open a log view on demand without losing scrollback.
- All children are killed on
SIGINT/SIGTERM/ process exit — even uncaught exceptions — so you don't leak ports between runs.
Viewing service logs (split panel)
The dashboard intentionally stays compact: status, badge, port/PID, and one
reason line per service. To see a service's live stdout, focus the service
(arrow keys) and press Enter. A vertical split opens with the dashboard on
the left and a stack of live log panes on the right:
project / 1dx 0.1.14 │ ▌ API Server up pid 12345 132 ln
──────────────────────────── │ GET / 200 12ms
● API Server up :4001 │ POST /api/foo 201 8ms
↳ r Restart s Stop │ [bun] hot reload triggered
● Worker up :4002 │ ──────────────────────────────────────
──────────────────────────── │ Worker up pid 12346 87 ln
│ processed job #432
Actions: │ processed job #433
d. Diagnostics │
n. New terminal │
q. Quit │
Open logs: api, worker │
Enter on a service: toggle · │
L: close all │Each pane shows the tail of the service's ring buffer (~2000 lines) with ANSI colour preserved, and stays populated even after a service exits so you can read why it died until you restart it.
Dashboard-mode controls
While focus is on the dashboard (the default):
Enteron a focused service: pin that service's log pane (or unpin if already pinned). When nothing is pinned, the focused service is shown as a preview pane — same layout, but the title is marked○ … previewinstead of●. Pin it withEnter(or just leave it as a preview that follows your dashboard cursor). The dashboard itself shows a small●next to every pinned service title.→orTab: focus the first open pane (enters Scroll mode).←orShift+Tab: focus the last open pane.L(capital): close every open pane and return to dashboard-only view.f: toggle fullscreen mode — hides the dashboard column so the log panes get the entire terminal width. Press it while a service row is focused (or any time inside Scroll mode); the first pane is auto-focused on entry so you immediately land somewhere usable, andfagain restores the split view. When the dashboard cursor is on the global Actions menu instead of a service row,fkeeps its configured meaning (e.g. the playground binds it to "Open in browser"), so you don't lose your global shortcut just because the log split is open.↑/↓: navigate services and actions in the dashboard.
Pane Scroll mode
Entered via → or Tab from the dashboard. The focused pane's header
turns cyan and gets a ▌ marker plus a SCROLL badge. While in Scroll
mode:
→/←(orTab/Shift+Tab): cycle focus to the next/previous pane. Stepping past the last pane returns to the dashboard.Esc: exit pane focus, return to the dashboard. If you're in fullscreen,Escalso drops fullscreen on the way out so you don't get stuck with an invisible dashboard.↑/↓: scroll one line up/down.PageUp/PageDown: scroll a viewport at a time.e(org/G): jump to the end (tail, newest content).s: jump to the start of the buffer (oldest line still resident). This shadows the per-services(stop) shortcut while you're in Scroll mode; stop is still reachable from the dashboard inline strip.b: toggle a bookmark at the current bottom-of-view line. The header shows a•Nindicator with the bookmark count, which turns into a yellow*Nwhenever the line at the bottom of the view is itself a bookmark.n/N: jump to the next / previous bookmark.nmoves toward the tail (newer content),Nmoves toward the top (older); both wrap around the ends of the list. Bookmarks are stored as absolute line indices, so they stay anchored to the same content as the ring buffer slides, and are cleared when the service is restarted.[/]: move the focused pane up/down in the stack.x: close just the focused pane.L: close every pane.f: toggle fullscreen (hide / show the dashboard column).i: enter Input mode on the focused pane.- Other letter shortcuts that match a per-service action (e.g.
rfor restart) still fire on the focused pane's service, so you can restart from inside a pane without losing focus.
Mouse wheel also works in any open log pane: scroll up over a pane
(no need to click first) to step into the buffer history, scroll down to
ease back toward the tail. Wheeling auto-focuses the pane you're pointing
at, so a follow-up b or e lands on the right one. Both wheel and
keyboard scroll are clamped against the wrap-aware top of the buffer, so
you can't overshoot. Mouse capture is disabled on exit; while 1dx is
running you can still copy text by holding Shift (Windows Terminal /
iTerm2 / GNOME / Konsole / vscode) or Alt (Alacritty / kitty), which
bypasses the capture and lets the terminal do its native selection.
A thin scrollbar runs down the right edge of every pane. The bright
thumb (█) shows the visible window's position inside the ring buffer;
it spans the whole track when nothing has scrolled off the top yet, and
shrinks/moves as you scroll up. Bookmarks appear on the same track as
yellow ◆ markers, so you can see at a glance how the open bookmarks
are distributed and use n / N to jump between them.
Pane Input mode
Entered via i from Scroll mode. The pane's header turns magenta and gets
a ▶ marker plus an INPUT badge. Every keystroke other than Esc is
forwarded into that service's PTY stdin:
- Printable characters,
Enter,Backspace,Delete,Tab, and arrow keys are translated to their standard terminal byte sequences. Ctrl+letteris translated to the corresponding control byte (e.g.Ctrl+C→0x03,Ctrl+D→0x04). This meansCtrl+Cinside Input mode signals the service, not 1dx.Escexits Input mode back to Scroll mode.
This is enough to drive things like Vite's r/o hotkeys, REPL prompts,
or interactive CLI installers that ask y/n questions.
Opening a real external terminal
The t global action ("Open new terminal") still spawns a real terminal tab
in your host terminal app (Windows Terminal, macOS Terminal.app, or the first
available Linux terminal). Use this as the escape hatch whenever you need a
free-form shell — it's intentionally not a PTY child of 1dx, so it survives
1dx exit and lets you type freely without modal mediation.
Debugging probes
Set 1DX_DEBUG=1 (or ONEDX_DEBUG=1) before launching the monitor to write
every probe outcome (timestamp, state, streak counters, latency, reason) to
.1dx/probe.log in your project. The bundled presets also include a
Toggle health diagnostics action (default shortcut d) that shows the last
probe result for each service inline in the TUI.
Per-service actions and focus navigation
In addition to the global actions list at the bottom of the monitor, each
managed service has its own inline action menu. To reach it, press Up from
the first global action item: focus moves to the last service in the list. Use
Up/Down to move between services, Esc to jump back to the actions list,
or Enter to open the focused service's full-screen log view.
While a service is focused 1dx shows a small radial menu directly under its row, e.g.:
Backend (Supabase) ● RUNNING :54321 PID 12345
↳ r Restart s Stop m Run migrations (Esc to leave)The defaults are inferred from the service's existing fields and current state:
rRestart — only shown while the service isupordegraded(nothing to restart otherwise). With node-pty the flow is justpty.kill()on the current handle followed bypty.spawn()for the start command — no shell wrappers, notaskkill, and unrelated services are untouched.sStop/Start — toggles based on current state. If the service isupordegraded,scallspty.kill(). If it isdown/unknown/dependencyDown,sspawns the service in a new PTY.
Customizing the built-in actions
Two optional fields on a service let you override how restart and stop
execute:
{
"id": "backend",
"stop": { "mode": "inline", "command": "bun", "args": ["run", "supabase:stop"] },
"restart": { "mode": "terminal", "shellCommand": "bun run supabase:stop && bun run supabase:start" }
}mode accepts "internal" (default — use the built-in stop/restart flow),
"inline" (run via the embedded CommandRunner UI), "external" (hand the
TTY over to the command), or "terminal" (spawn a new terminal tab and run
shellCommand there).
Adding extra inline actions per service
Add a service.actions[] array to attach any number of additional shortcuts
to a service. Each entry has the same shape as a global action:
{
"id": "api",
"actions": [
{
"id": "ping",
"label": "Ping",
"shortcut": "p",
"mode": "inline",
"command": "curl",
"args": ["-sS", "http://127.0.0.1:4001/"]
}
]
}If an entry uses one of the reserved shortcuts r or s, it replaces the
auto-injected default for that key on that service. Other shortcuts are added
on top of the defaults. Service-action shortcuts only fire when the
corresponding service is focused, so they don't collide with global action
shortcuts.
Publish
The repo includes a manual GitHub Actions workflow at .github/workflows/release.yml.
Trigger it with patch, minor, or major to:
- bump
package.json - typecheck and smoke-test the CLI
- publish to npm with provenance
- push the release commit and tag
- create a GitHub release
