diff --git a/CLAUDE.md b/CLAUDE.md index 8a9f5c3..5c9635c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,8 @@ src/ - `T` toggles an `HH:mm:ss.SSS` timestamp gutter in TUI mode; also enabled via `timestamps: true` config or `--timestamps` flag. Accepts a format string (e.g. `timestamps: "HH:mm:ss"`) with tokens: `YYYY`, `MM`, `DD`, `HH`, `hh`, `mm`, `ss`, `SSS`, `A` - Compact keybinding hints in the status bar; `H` or `?` opens a full help overlay. Config lives in `src/ui/keybindings.ts` - `autowrap: false` config opts into disabling the host terminal's autowrap (DECAWM, `\x1b[?7l`) at startup, restored (`?7h`) on shutdown (`App.disableAutowrap()`). Default is `true` (untouched). Works around an OpenTUI renderer bug: its incremental diff positions every run with an absolute cursor move and never relies on autowrap, but it also never disables it and skips its right-edge cursor re-home to avoid tripping a wrap — so a run that fills the last column wraps into column 1 of the next row, smearing right-pane output into the tab sidebar. Because the diff records the *intended* cell (`syncCell`), the stray cells never repaint until a full repaint (resize). Pane content still wraps inside its own VT grid — only the host emit-cursor wrap is turned off. Off by default because it changes a global terminal mode; the proper fix is upstream ([anomalyco/opentui#1187](https://github.com/anomalyco/opentui/issues/1187)) +- `--serial` (or `serial: true`) runs one process at a time in display order (`sort`). Each waits for the previous to become ready — for one-shot commands, to exit. `dependsOn` still holds, so the order is reordered when needed to keep dependencies first (`ProcessManager.serialOrder()`). A failed process does not skip the rest of the chain, unlike a failed dependency +- Script-pattern extra args (`'npm:lint:* --fix'`): only npm gets a `--` separator. yarn and pnpm forward the args as-is and would pass a literal `--` to the script, which breaks flag parsing in Go/cobra CLIs like `supabase`; bun accepts either form - Set `interactive: true` on processes that need stdin (REPLs, shells) - Non-interactive panes hide the terminal cursor (shown during input mode) - Set `errorMatcher: true` to detect ANSI red output, or a regex string to match custom patterns — shows a red indicator on the tab while the process keeps running diff --git a/README.md b/README.md index d48609e..a018a8f 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,9 @@ scripts that have sub-scripts beneath them. E.g. if `format:check` has `format:check` but keeps the leaf scripts. **Extra args:** Anything after the first space in the pattern is forwarded -as extra arguments to each matched command: `lint:* --fix` → `bun run lint:js -- --fix`. +as extra arguments to each matched command: `lint:* --fix` → `bun run lint:js --fix`. +npm is the only package manager that needs a `--` separator, so it gets one: +`npm run lint:js -- --fix`. **Template inheritance:** Config properties on a pattern entry (color, env, dependsOn, etc.) are inherited by all expanded processes. Color arrays are @@ -220,6 +222,7 @@ export default defineConfig({ | `-e,` `--env-file` `` | Env file path, or "false" to disable env file loading | | `--config` `` | Config file path (default: auto-detect) | | `-p,` `--prefix` | Prefixed output mode (no TUI, for CI/scripts) | +| `--serial` | Run one process at a time, in display order | | `-o,` `--only` `` | Only run these processes (+ their dependencies) | | `-x,` `--exclude` `` | Exclude these processes | | `--kill-others` | Kill all processes when any exits (regardless of exit code) | @@ -280,6 +283,7 @@ Top-level options apply to all processes (process-level settings override): | `watch` | `string \| string[]` | Global watch patterns, inherited by processes without their own watch | | `sort` | `'config' \| 'alphabetical' \| 'topological' \| 'status'` | Tab display order. `'config'` preserves definition order (package.json script order for wildcards), `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers, `'status'` uses config order but moves finished/stopped/failed/skipped tabs to the bottom. | | `prefix` | `boolean` | Use prefixed output mode instead of TUI (for CI/scripts) | +| `serial` | `boolean` | Run one process at a time, in display order (see `sort`). Each process waits for the previous one to become ready — for one-shot commands, to exit. Dependencies still hold: a process never starts before what it depends on. | | `timestamps` | `boolean \| string` | Add timestamps to output lines. `true` uses default `HH:mm:ss.SSS` format, or pass a format string (e.g. `"HH:mm:ss"`) | | `killOthers` | `boolean` | Kill all processes when any one exits (regardless of exit code) | | `killOthersOnFail` | `boolean` | Kill all processes when any one exits with a non-zero exit code | diff --git a/src/cli-flags.ts b/src/cli-flags.ts index a5de00d..3717849 100644 --- a/src/cli-flags.ts +++ b/src/cli-flags.ts @@ -135,6 +135,12 @@ export const FLAGS: FlagDef[] = [ key: 'prefix', description: 'Prefixed output mode (no TUI, for CI/scripts)' }, + { + type: 'boolean', + long: '--serial', + key: 'serial', + description: 'Run one process at a time, in display order' + }, { type: 'value', long: '--only', diff --git a/src/cli.ts b/src/cli.ts index 0ac69e1..ca40421 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,6 +16,7 @@ export interface ParsedArgs { logsProcess?: string completions?: string prefix: boolean + serial: boolean killOthers: boolean killOthersOnFail: boolean timestamps: boolean | string @@ -58,6 +59,7 @@ export function parseArgs(argv: string[]): ParsedArgs { exec: false, logs: false, prefix: false, + serial: false, killOthers: false, killOthersOnFail: false, timestamps: false, diff --git a/src/config/expand-scripts.test.ts b/src/config/expand-scripts.test.ts index 9e9bda0..b24cbb7 100644 --- a/src/config/expand-scripts.test.ts +++ b/src/config/expand-scripts.test.ts @@ -406,6 +406,34 @@ describe('expandScriptPatterns', () => { expect(proc(result, 'ts').command).toBe('npm run lint:ts -- --fix') }) + test('yarn gets no -- separator (it would reach the script as a literal arg)', () => { + const dir = setupDir('args-yarn', { + 'package.json': pkgJson({ 'codegen:ts': 'gen-types', 'codegen:go': 'gen-go' }), + 'yarn.lock': '' + }) + const result = expandScriptPatterns({ processes: { 'npm:codegen:* --project-id abc': {} } }, dir) + expect(proc(result, 'ts').command).toBe('yarn run codegen:ts --project-id abc') + expect(proc(result, 'go').command).toBe('yarn run codegen:go --project-id abc') + }) + + test('pnpm gets no -- separator', () => { + const dir = setupDir('args-pnpm', { + 'package.json': pkgJson({ lint: 'eslint' }), + 'pnpm-lock.yaml': '' + }) + const result = expandScriptPatterns({ processes: { 'npm:lint --fix': {} } }, dir) + expect(proc(result, 'lint').command).toBe('pnpm run lint --fix') + }) + + test('bun gets no -- separator', () => { + const dir = setupDir('args-bun', { + 'package.json': pkgJson({ lint: 'eslint' }), + 'bun.lock': '' + }) + const result = expandScriptPatterns({ processes: { 'npm:lint --fix': {} } }, dir) + expect(proc(result, 'lint').command).toBe('bun run lint --fix') + }) + test('multiple extra args forwarded', () => { const dir = setupDir('args-multi', { 'package.json': pkgJson({ 'lint:js': 'eslint' }) diff --git a/src/config/expand-scripts.ts b/src/config/expand-scripts.ts index 47cb580..f966bfb 100644 --- a/src/config/expand-scripts.ts +++ b/src/config/expand-scripts.ts @@ -75,11 +75,16 @@ function splitPatternArgs(raw: string): { glob: string; extraArgs: string } { return { glob: raw.slice(0, i), extraArgs: raw.slice(i) } } -/** Convert a script name (with optional extra args) into a ` run