@libtmux/workspace-cli
v0.1.0-alpha.12
Published
A tmuxp-compatible workspace manager for Node and Bun.
Readme
Workspace CLI
Manage tmux workspaces from YAML and JSON with Node.js 22.12+ or Bun 1.3.14+.
tmux-workspace loads, captures, converts and imports tmuxp workspace files.
This implementation is in development.
Install
Run the command without installing it. With Node:
$ npx -y @libtmux/[email protected] --helpWith Bun. The command names node in its shebang, so bunx runs it on Node
when Node is installed; --bun runs it on Bun:
$ bunx --bun @libtmux/[email protected] --helpWith pnpm:
$ pnpm dlx @libtmux/[email protected] --helpWith Yarn. Yarn refuses a release younger than its npmMinimalAgeGate, one
day by default:
$ yarn dlx @libtmux/[email protected] --helpOr install it globally, which puts tmux-workspace on PATH. With npm:
$ npm install --global @libtmux/[email protected]With pnpm:
$ pnpm add --global @libtmux/[email protected]With Bun:
$ bun add --global @libtmux/[email protected]The rest of this page runs the installed tmux-workspace. A runner passes
the same arguments to the command when they follow the package name.
Load and capture
Save this document as workspace.yaml:
session_name: dev
windows:
- window_name: editor
layout: main-vertical
panes:
- vim
- topLoad on a separate tmux socket:
$ tmux-workspace load ./workspace.yaml \
-L workspace-cli-demo \
-d \
--jsonCapture its live topology:
$ tmux-workspace freeze dev \
-L workspace-cli-demo \
--jsonAll native input layouts are checked before scripts run or sessions change. Names accept unique abbreviations supported by the running daemon. Only version-sensitive names query it; a cold endpoint uses the selected client. Saved layouts require a valid checksum, a nonempty tree and enough pane cells. tmux still owns geometry correction and pruning, and may reject a layout when it is applied. The CLI limits custom layouts to 8192 characters and 256 nested groups; these are application limits, separate from tmux's parser.
One default differs from tmuxp on purpose: a window that names no layout
tiles its panes, where tmuxp stacks them by halving the last one. Explicit
layout and focus agree with tmuxp, and so does the pane left active when
nothing sets focus: the last pane created in the focused window.
@libtmux/workspace is a separate implementation, not this
command's engine: it converges a session that already exists, and its strict
schema rejects environment, before_script, window_index,
suppress_history, x- keys and the other fields below. A document written
here is not necessarily one that package accepts.
Loading reuses an existing session, and compares it against the document rather
than rebuilding it: a window the document names that the session does not hold
is reported, exit 1, and nothing is changed. Without -d, load attaches the
final workspace or switches the current tmux client. Attachment uses the
controlling terminal, including when standard streams are redirected; no
terminal means failure before creating sessions. Inside tmux, attached load
stays on the current server; use -d to load on another server. Explicit append
adds windows to the current pane's session. A load that cannot finish a session
it created removes it, so a rerun starts clean; an append keeps the windows it
added and names them.
When clients share a session, tmux selects its most recently active client before loading. The CLI captures that client and targets the switch explicitly.
Two loads racing on the same session name are not coordinated: tmux offers no
lock on a session that does not exist yet, so the loser's create can fail after
the winner's session exists, or both can observe no session and each start one.
Run concurrent loads against distinct session names.
load -2 forces 256-color terminal handling. Legacy -8 is rejected before
calling tmux because supported tmux versions do not implement that legacy flag.
Pane readiness waits apply before sending commands, whatever the pane's shell
is. Empty panes skip the wait, including with
workspace_builder_options.pane_readiness: always.
Explicit window_index values reserve their slots before implicit windows are
allocated from the effective base-index; null leaves the index unspecified.
Append also reserves indexes from later input files. Removing the temporary bootstrap window preserves requested
indexes and restores the prior local or inherited renumber-windows setting.
A failed restoration appears as renumber_restore_error in the load result.
Native load rejects unsupported fields before any input runs scripts or changes
tmux. The diagnostic includes the field path. For example, entter: false in a
command is an error; use enter: false to leave that command unsubmitted. A
key starting with x-, at any scope, is the exception: it is accepted,
ignored at load, and left alone by convert. The refusal for any other
unknown key names that prefix as the way to keep it.
| Scope | Accepted fields |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Workspace | session_name, windows, start_directory, environment, shell_command_before, suppress_history, options, global_options, before_script, workspace_builder_options, plugins, workspace_builder, workspace_builder_paths, description |
| Window | window_name, window_index, panes, start_directory, environment, shell_command_before, suppress_history, options, options_after, layout, window_shell, focus, description |
| Pane | shell_command, shell_command_before, start_directory, environment, suppress_history, shell, focus, enter, sleep_before, sleep_after, description |
| Command object | cmd, enter, sleep_before, sleep_after |
| workspace_builder_options | pane_readiness |
description is neutral metadata at workspace, window, and pane scope. Option
and environment maps retain their native names. Conversion preserves arbitrary
fields without applying the native execution contract. Documents delegated to
Python plugins or custom builders retain their extension fields; empty plugin
lists and blank builder names still use native validation.
Inspect a workspace through MCP
The separate MCP server inspects loaded workspaces on the
same tmux socket. After the detached load above, add it to your MCP client with
that socket and the inspect toolset:
{
"mcpServers": {
"tmux": {
"command": "npx",
"args": ["-y", "@libtmux/[email protected]"],
"env": { "LIBTMUX_SOCKET": "workspace-cli-demo", "LIBTMUX_TOOLSETS": "inspect" }
}
}
}The socket is selected when the MCP server starts.
For a workspace loaded with -S, set LIBTMUX_SOCKET_PATH to the same absolute
socket path instead of LIBTMUX_SOCKET. If the CLI uses TMUX_BIN, select that
executable with the MCP server's LIBTMUX_TMUX_BIN too.
Discover tools with tools/list, then call list_sessions and list_panes.
Call list_windows with session set to dev, the configuration's session
name. Pass a returned pane ID as paneId to capture_pane or wait_for_text;
bound output with maxLines and waits with timeoutMs. A pending text wait allows
other inspection requests on the same connection. Read tmux://capabilities
through resources/read to verify the selected socket and advertised tools.
See the capture guide for incremental
reads and cursors.
Closing the MCP subprocess's stdin cancels pending waits and joins its control connections. The loaded workspace remains available in tmux.
Imports
import tmuxinator and import teamocil translate source documents and validate
native configuration fields before previewing or saving. Unknown fields,
conflicting non-null aliases and malformed command shapes fail without
replacing a destination. Generic convert preserves arbitrary fields.
Tmuxinator window command arrays stay in one pane. Explicit panes lists create
separate panes. Project pre_window arrays form one ; -joined command; window
pre arrays form one &&-joined command before each explicit pane. Window
pre requires nonempty explicit panes. synchronize: true and before apply
before sequential pane creation and command delivery; after applies afterward.
Teamocil commands arrays form one ; -joined command. Legacy cmd and splits
are accepted. Window options apply before pane commands. The first requested
window and pane focus wins; absent focus selects the first item. Both formats
retain window order, pane order and layouts.
Imports save an absolute project root relative to the invocation directory, including when the source omits it. Window roots resolve relative to that root, so moving the saved file preserves working directories. Import requires neither tmux nor Python and does not check directory existence. Layout compatibility is checked against the selected tmux during load. Commands use native load's environment expansion from the invoking process before shell delivery.
Project lifecycle hooks, endpoint/runtime overrides, named pane titles,
Teamocil clear, filters and pane widths are refused. Use an explicit native
workspace or the source tool for those behaviors.
Tmuxinator expands ERB through Ruby before parsing; unexpanded <% markup in a
Tmuxinator key or value fails before output or overwrite. Teamocil evaluates no
templates, so <% in a Teamocil source is ordinary text and is preserved.
Output
Every command accepts --json and --ndjson before or after the command name.
NDJSON takes precedence when both are present. Machine load requires -d or
--append. Machine commands never prompt. Human output honors --color and
NO_COLOR; machine output contains encoded data without terminal styling.
convert, import commands, and machine freeze return a document when no
destination is given. --save-to writes a file; --force permits replacement.
The file format is separate from the JSON/NDJSON output mode.
freeze infers JSON from a .json destination and otherwise saves YAML;
-f overrides the inferred format. --quiet suppresses its human save message
and preserves machine results.
Capture accepts an exact session name or ID. Without one, it uses the authenticated
current pane's session or the sole session on the selected server. Several sessions
outside tmux require an explicit target.
Capture reads honor cancellation. Saves check for cancellation before publishing;
publication already in progress may still complete.
Human load shows progress on terminal stderr. --progress-format accepts
default, minimal, window, pane, verbose, or a template with tokens such
as {session}, {window}, and {session_pane_progress}. Pane counts advance
after configured commands are sent; they do not wait for those commands to exit.
--progress-lines controls the recent script-output panel: default 3, 0 forwards
each stream directly, and -1 uses the available terminal height. Retention is
bounded to 64 KiB of characters for completed lines and each partial stream.
TMUXP_PROGRESS_FORMAT and TMUXP_PROGRESS_LINES set defaults; explicit flags
take precedence. --no-progress, TMUXP_PROGRESS=0, a dumb terminal, or redirected
stderr disables the display. Frames update from build events without timing
sleeps, clip to terminal width, and clear before results or attachment.
--log-level filters diagnostics on stderr: debug, info, warning (default),
error, or critical. Machine diagnostics are NDJSON. load --log-file appends
the selected diagnostics as NDJSON, including script output at info level.
New log files use owner-only permissions. Nonregular destinations are rejected
before session creation; existing log contents are preserved.
If logging fails during execution, stderr reports log_error and the command
keeps its result and exit status. Cancellation closes the owned log handle.
Interrupting a blocked output write still joins owned children and exits 130.
An interrupted load allows up to 100ms to emit its final machine result before
exiting; an unread pipe cannot hold the process open. Cancelled output may be
incomplete.
Bootstrap and editor commands use quoted argv directly. Child output is UTF-8 with replacement for invalid bytes. Captured results retain at most 64 KiB of source bytes per stream and report truncation; NDJSON forwards output as it arrives. A nonzero editor status becomes the CLI exit status. After a captured child exits, its streams have 100ms to drain before owned descendants are stopped. Remaining capture streams close after group cleanup.
Python compatibility
Search uses native JavaScript regular expressions. -F treats patterns as
literal text; -w applies whole-word matching to the complete expression.
Python-specific regex syntax is outside the native search contract.
shell uses an interpreter with tmuxp 1.74.0 installed. Set
TMUX_WORKSPACE_PYTHON to choose its executable; otherwise it uses python3.
Code passed with -c runs in tmuxp's Python context. An interactive shell needs
a controlling terminal.
load uses the same optional interpreter when a workspace selects Python
plugins or a workspace_builder. Ordinary workspaces, empty plugin lists,
and blank builder names use the native builder without starting Python.
workspace_builder_paths adds existing import directories relative to the
workspace file. These directories contain executable extension code.
The adapter expands common configuration fields and supplies the tmuxp builder
protocol. Explicit custom builders may omit windows and consume their own
configuration. Append retains the authenticated current session across input
files. Python extension append rejects before_script; run that script
separately. Native append retains its bootstrap support.
Extension output uses the bounded child streams described above. Human progress
shows a workspace label and script output without native pane counters.
Extension results report effects_scope: "observed", effects_unknown: true,
and newly observed window and pane IDs. These observations include concurrent
changes and do not establish ownership; failures do not roll back opaque
extension effects. Cancellation joins the owned Python process group, then
allows up to one second to observe surviving topology. A changed daemon or
failed observation is reported without comparing IDs across daemon lifetimes.
Reference and completion
The command reference and command metadata come from the native parser. Regenerate them after changing command definitions:
$ bun run --cwd packages/workspace-cli docs:generateThe build checks that the reference and packaged completion scripts are current. The scripts complete commands, options, enum values, and file paths without starting Node, Bun, or tmux. Saved workspace names and live sessions are outside their completion scope.
Completion is tested on Linux with Bash 5.2, Zsh 5.9, and fish 4.8. Bash 3.2 is a syntax target; older shells and filenames containing newlines remain unverified.
With tmux-workspace on PATH, load the script for your shell. Bash:
$ source <(tmux-workspace completion bash)Zsh, with its completion system initialized:
$ autoload -Uz compinit && compinit && source <(tmux-workspace completion zsh)fish:
$ tmux-workspace completion fish | sourceFor automation, --json and --ndjson wrap the script in a structured result.
Build from source
Run from the repository root:
$ bun run --cwd packages/libtmux build$ bun run --cwd packages/workspace-cli build$ node packages/workspace-cli/dist/main.js --helpDevelopment status
Native discovery, conversion, imports, common searches, detached load, append, capture, editor invocation, diagnostics, and the Python shell have executable tests. The Linux installed-package check packs the CLI and core, installs both offline into an empty project, and exercises every command in JSON and NDJSON on Node 22 and Bun. It needs the optional Python runtime described above:
$ bun run --cwd packages/workspace-cli test:installPlugin and custom-builder execution has output, cancellation and borrowed-state tests using the optional interpreter. Confirmation and destination choices use explicit command-line flags. Native process and terminal evidence covers Linux; macOS coverage is a package canary. Benchmark results cover isolated Linux fixtures.
The core library keeps its zero-runtime-dependency contract. Commander, YAML,
and terminal text helpers are bundled into this CLI; libtmux remains a separate
package. Machine commands skip loading the progress renderer.
