Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ The version gate: every runtime-touching command first checks the runtime `mx.js

**Bin** (runtime-wide utility executables at `<runtime>/bin/`, meant for `PATH`) — `mx bin` (alias `mx bins`): `ls` (list bins with an mx-shipped vs user-added tag, flag non-executable ones, and note whether `bin/` is on `PATH`; porcelain returns `{dir, onPath, bins[]}`) · `path` (print the `bin/` dir for `export PATH="$(mx bin path):$PATH"`). mx ships `dcs`/`lcs` (delete/list Claude Code sessions by name) from `templates/bin/`; shipped bins are **re-stamped (overwritten) on every `init`/`sync`** like the runtime `CLAUDE.md` (so updates land), while **user-added bins are never touched**. To customize a shipped bin, copy it to a new name. Distinct from a work's `scripts/` (per-work). Bare `mx bin` defaults to `ls`.

**Divider** — `mx divider <text> [-o]`: fill a terminal with `<text>` as large block letters, a visual separator for macOS Mission Control Spaces (e.g. an `IN REVIEWS` window between work-window clusters). Bare = take over the current terminal and hold the banner (Ctrl-C/`q` to quit); `-o` = open a new fullscreen Terminal running it (macOS; `UNSUPPORTED` elsewhere). Auto-scales to the window via a zero-dep 5x7 block font (`renderBanner` in `@mx/core`); touches no runtime state.

## Hooks (what they are)

A **hook** is a user-owned script mx runs at a lifecycle moment. As of v3 they're **centralized** in a single `<runtime>/hooks/` directory — **one executable per event**, named exactly for the event (no extension): `pre-worktree-create`, `post-worktree-create`, `pre/post-worktree-remove`, `pre/post-work-archive`, `pre/post-work-unarchive`, `pre/post-repo-fetch`, `repo-health`, `work-health`, and `session-prompt`. This replaces v2's per-repo `hydrate.sh`/`health.sh` and per-work `hooks/`.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Domain logic lives in `packages/core` (`@mx/core`) as pure functions that return
| `mx work health` / `mx work -n <name> health` | local-only work-folder audit: stray files in the work root, worktree presence vs work.json, cross-work port collisions, archive invariants; augmented by the central `work-health` hook (`--all` includes archived) |
| `mx health [--all]` | whole-runtime overview: every repo's health + every active work's health in one view (`--all` includes archived works) |
| `mx mission-control [--port <n>] [-o]` (alias `mx mc`) | start a local, read-only **live web dashboard** — calm monochrome view of all repo/work health and a consolidated ports board (port → service → worktree → work → URL), streamed over SSE. `-o` opens the browser |
| `mx divider <text> [-o]` | fill a terminal with `<text>` as large block letters (a visual separator for macOS Spaces); bare takes over the current terminal and holds, `-o` opens a new fullscreen Terminal |
| `mx bin ls` / `mx bin path` (alias `mx bins`) | list the runtime's `bin/` utility executables (mx-shipped + your own); `path` prints the dir for `export PATH="$(mx bin path):$PATH"` |

Read commands take `--porcelain` for stable JSON; mutations echo the resulting object; errors are `{"error","code"}` with a non-zero exit. `-n <name>` can be omitted when your cwd implies it (inside a work folder/worktree, or a pristine clone).
Expand All @@ -124,6 +125,7 @@ See **[docs/release.md](docs/release.md)** for the full runbook, the `NPM_TOKEN`
- [x] Central hook hub (`<runtime>/hooks/`): worktree create/remove, work archive/unarchive, repo fetch, repo-health, work-health, session-prompt — any language, branch on `MX_*`
- [x] Health: `mx repo health`, `mx work health`, and `mx health` (whole-runtime overview)
- [x] `mx mission-control` — live web dashboard (zero-dep server + single-file React/Vite/Tailwind UI)
- [x] `mx divider <text> [-o]` — fill a terminal with large block text as a visual separator for macOS Spaces
- [x] Multiple worktrees of one repo per work (named worktrees; `wt/<name>`)
- [x] `mx work open` / `new -o` — fullscreen Terminal (macOS) that resumes-or-creates the per-work Claude session, with a `session-prompt` hook for the initial prompt
- [x] Env-based runtime discovery (`--runtime` / `$MX_RUNTIME` / default `~/mx`)
Expand Down
60 changes: 60 additions & 0 deletions apps/cli/src/divider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { renderBanner } from '@mx/core';

/** ANSI: clear the screen and scrollback, and move the cursor home. */
const CLEAR = '\x1b[2J\x1b[3J\x1b[H';
/** ANSI: hide the cursor. */
const HIDE_CURSOR = '\x1b[?25l';
/** ANSI: show the cursor. */
const SHOW_CURSOR = '\x1b[?25h';

/**
* Fill the current terminal with `text` as large block letters and **hold** it
* on screen until the user quits (Ctrl-C or `q`), so the terminal reads as a
* labelled divider. Re-renders on window resize (e.g. after the window is made
* fullscreen). Used by `mx divider`.
*
* When stdout is not a TTY (piped/redirected), it prints the banner once at a
* default size and returns, rather than holding.
*
* @param text - The label to display.
*/
export function runDivider(text: string): void {
const paint = (): void => {
const cols = process.stdout.columns || 80;
const rows = process.stdout.rows || 24;
process.stdout.write(CLEAR + renderBanner(text, cols, rows));
};

// Non-interactive (piped) stdout: emit once and return, no hold.
if (!process.stdout.isTTY) {
process.stdout.write(renderBanner(text, process.stdout.columns || 80, process.stdout.rows || 24) + '\n');
return;
}

process.stdout.write(HIDE_CURSOR);
paint();
process.stdout.on('resize', paint); // keep filling the window as it resizes

/** Restore the terminal and exit cleanly. */
const quit = (): void => {
process.stdout.write(CLEAR + SHOW_CURSOR);
process.exit(0);
};
process.on('SIGINT', quit);
process.on('SIGTERM', quit);

// Raw mode keeps typed keys from echoing over the banner; we watch for Ctrl-C
// (0x03) and `q`/`Q` to quit, and resume stdin so the process stays alive.
if (process.stdin.isTTY) {
try {
process.stdin.setRawMode(true);
} catch {
// Not all TTYs support raw mode; fall back to cooked mode + SIGINT.
}
}
process.stdin.resume();
process.stdin.on('data', (d: Buffer) => {
const b = d[0];
if (b === 0x03 || b === 0x71 || b === 0x51) quit(); // Ctrl-C, q, Q
});
}
3 changes: 3 additions & 0 deletions apps/cli/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ Bin (runtime-wide utility executables in <runtime>/bin, meant for PATH):
mx bin ls (alias mx bins) list bins (mx-shipped + your own); shows whether bin/ is on PATH
mx bin path print the bin/ dir: export PATH="$(mx bin path):$PATH"

Divider (a fullscreen label to separate your Spaces):
mx divider <text> [-o] fill the terminal with <text> as large block letters and hold it (Ctrl-C / q to quit); -o opens a new fullscreen Terminal (macOS)

The -n <name> selector may be omitted when your cwd implies it: inside a work folder or
worktree (works/<work>/...) the work is inferred; inside repos/<repo>/... the repo is inferred.

Expand Down
24 changes: 23 additions & 1 deletion apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { MxError } from '@mx/core';
import { parseArgs } from './args';
import { setPorcelain, emit, fail } from './output';
import { setPorcelain, emit, fail, check, bold, dim } from './output';
import { HELP } from './help';
import { runGlobal } from './commands/global';
import { dispatchRepo } from './commands/repo';
import { dispatchWork } from './commands/work';
import { dispatchBin } from './commands/bin';
import { dispatchHealth } from './commands/health';
import { dispatchMissionControl } from './commands/missionControl';
import { runDivider } from './divider';
import { openFullscreenTerminal, shq } from './open';

/**
* CLI version, surfaced by `mx version` / `mx --version`. Read from the
Expand Down Expand Up @@ -67,6 +69,26 @@ export function main(): void {
case 'mission-control':
case 'mc':
return dispatchMissionControl(positionals, flags);
case 'divider': {
// Fill a terminal with big block text as a visual separator for your
// Spaces. Bare: takes over the current terminal and holds. With -o:
// opens a new fullscreen Terminal (macOS) running this same command.
const text = positionals.slice(1).join(' ').trim();
if (!text) throw new MxError('usage: mx divider <text> [-o]', 'BAD_ARGS');
if (flags.open) {
// Re-invoke this exact CLI (node + entry script) in the new Terminal,
// without -o, so it clears + renders + holds there.
openFullscreenTerminal(
`${shq(process.execPath)} ${shq(process.argv[1])} divider ${shq(text)}`,
);
emit(
() => console.log(`${check()} opened divider ${bold(text)} ${dim('(new Terminal)')}`),
{ divider: text, opened: true },
);
return;
}
return runDivider(text);
}
default:
throw new MxError(`unknown command: ${positionals[0]}`, 'BAD_ARGS');
}
Expand Down
20 changes: 16 additions & 4 deletions apps/cli/src/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,27 @@ export interface OpenWorkOpts {
* @param opts - Optional launch command to run in the new Terminal (e.g. a `claude` invocation).
*/
export function openWorkLayout(workdir: string, opts: OpenWorkOpts = {}): void {
// The shell command Terminal runs: cd into the work folder, then optionally
// chain the caller's launch command (e.g. `claude --resume <id>`).
openFullscreenTerminal(`cd ${shq(workdir)}${opts.command ? ` && ${opts.command}` : ''}`);
}

/**
* macOS-only: open a **new fullscreen Terminal window** running `command`. The
* low-level primitive behind `mx work open` (which prefixes a `cd`) and
* `mx divider` (which runs the banner renderer). Throws `UNSUPPORTED` off macOS
* and `OSASCRIPT` if the AppleScript step fails.
*
* @param command - The shell command to run in the new Terminal window.
*/
export function openFullscreenTerminal(command: string): void {
if (process.platform !== 'darwin') {
throw new MxError('-o/--open is only supported on macOS', 'UNSUPPORTED');
}

// The shell command Terminal runs: cd into the work folder, then optionally
// chain the caller's launch command (e.g. `claude --resume <id>`).
const shellCmd = `cd ${shq(workdir)}${opts.command ? ` && ${opts.command}` : ''}`;
const shellCmd = command;

// Open a new Terminal window cd'd into the work folder, then fullscreen it.
// Open a new Terminal window running the command, then fullscreen it.
// When the frontmost Terminal window is already fullscreen, macOS's "prefer
// tabs in full screen" setting forces `do script` to open a TAB rather than a
// window. Detect that (window count didn't grow) and detach the tab into its
Expand Down
9 changes: 9 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,15 @@ Manage the runtime-wide `bin/` directory — utility executables shared across e

The directory is created by `mx init` and refreshed by `mx sync`. mx-shipped bins are **mx-owned: re-stamped (overwritten) on every sync**, like the runtime `CLAUDE.md`, so improvements land automatically; **your own bins are never touched**. To customize a shipped bin without losing it on the next sync, copy it to a new name. This is distinct from a work's `scripts/` folder (scoped to one work) — `bin/` is runtime-wide.

### `mx divider <text> [--open|-o]`

Fill a terminal with `<text>` rendered as **large block letters**, as a visual separator for your macOS Mission Control Spaces (e.g. an `IN REVIEWS` or `PR REVIEWS` window between clusters of work windows). The text auto-scales to fill the terminal and re-renders when the window resizes.

- **Bare** (`mx divider "IN REVIEWS"`) — takes over the **current** terminal: clears it, draws the banner, and **holds** it on screen (Ctrl-C or `q` to quit). Piped/non-TTY output just prints the banner once and returns.
- **`-o` / `--open` (macOS)** — opens a **new fullscreen Terminal** running the same banner, so you can drag it into place in the Spaces strip. Off macOS this is `UNSUPPORTED` (a warning).

This is a personal window-organization aid; it touches no runtime state. The renderer is a zero-dependency 5x7 block font (`renderBanner` in `@mx/core`), so it needs no `figlet`/`banner` binary.

## Loading the context-registry index

There is **no** `SessionStart` hook. mx through v2 stamped a per-work `.claude/settings.json` whose `SessionStart` hook printed `context/INDEX.json` into every session, but Claude Code caps hook output (~2KB) so a non-trivial index was silently truncated. v3 drops the hook; `mx work new` and `mx sync` no longer stamp it, and `mx migrate` removes a default-stamped one (a customized `settings.json` is kept with a warning).
Expand Down
7 changes: 7 additions & 0 deletions docs/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

What each release brought. Reverse-chronological. Dates reflect when the corresponding tag was pushed.

## 3.6.0 — 2026-07-08

**`mx divider <text> [-o]`** — fill a terminal with `<text>` as large block letters, a visual separator for macOS Mission Control Spaces (e.g. an `IN REVIEWS` / `PR REVIEWS` window between clusters of work windows). Bare, it takes over the current terminal, draws the banner, and holds it (Ctrl-C or `q` to quit); `-o` opens a new fullscreen Terminal running the same banner (macOS). The text auto-scales to fill the terminal and re-renders on resize.

- **Zero-dependency renderer:** a 5x7 block font in `@mx/core` (`renderBanner(text, cols, rows)`) — no `figlet`/`banner` binary needed. The CLI (`divider.ts`) owns the clear/hold/cursor/resize handling; `openFullscreenTerminal` (factored out of `openWorkLayout`) powers `-o`.
- Personal window-organization aid; touches no runtime state. Minor — runtime stays v3, no migration.

## 3.5.0 — 2026-07-06

**Runtime-wide `files/` store.** A new `<runtime>/files/` directory: a free-form place for operational **values** any work session can read — credentials, cluster names/URLs, usernames, API keys/tokens, endpoints, and other key-value meta a task needs to actually operate (a Playwright login, an authenticated API call, an ssh target). Created **empty** by `mx init` and backfilled by `mx sync`; mx only guarantees the directory exists and never reads, writes, or validates its contents — the layout is the agent's/user's to choose.
Expand Down
1 change: 1 addition & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Inside a work folder or worktree you can drop `-n` — mx infers the work/repo f
| `mx work health` / `mx work -n <name> health` | local-only work-folder audit (stray files, worktree presence, cross-work port collisions, archive invariants); augmented by the central `work-health` hook (`--all` includes archived) |
| `mx health [--all]` | whole-runtime overview: every repo's health + every active work's health |
| `mx bin ls` / `mx bin path` (alias `mx bins`) | list the runtime's `bin/` utility executables (mx-shipped + your own); `path` prints the dir for `export PATH="$(mx bin path):$PATH"` |
| `mx divider <text> [-o]` | fill a terminal with `<text>` as large block letters (a visual separator for macOS Spaces); `-o` opens a new fullscreen Terminal (macOS) |

## License

Expand Down
2 changes: 1 addition & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@roulabs/mx",
"version": "3.5.0",
"version": "3.6.0",
"description": "mx — run several features in parallel across shared repos using git worktrees",
"type": "module",
"bin": {
Expand Down
Loading
Loading