numux 2.10.4 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/numux.js CHANGED
@@ -31,13 +31,517 @@ var __toESM = (mod, isNodeMode, target) => {
31
31
  return to;
32
32
  };
33
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
38
+ var __export = (target, all) => {
39
+ for (var name in all)
40
+ __defProp(target, name, {
41
+ get: all[name],
42
+ enumerable: true,
43
+ configurable: true,
44
+ set: __exportSetter.bind(all, name)
45
+ });
46
+ };
47
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
34
48
  var __require = import.meta.require;
35
49
 
50
+ // src/generated/help-topics.ts
51
+ var TOPIC_ALIASES, HELP_TOPICS;
52
+ var init_help_topics = __esm(() => {
53
+ TOPIC_ALIASES = {
54
+ keys: "keybindings",
55
+ icons: "tab-icons",
56
+ deps: "dependency-orchestration",
57
+ "env-interpolation": "environment-variable-interpolation"
58
+ };
59
+ HELP_TOPICS = {
60
+ install: { title: "Install", body: `Requires [Bun](https://bun.sh) >= 1.0.
61
+
62
+ \`\`\`sh
63
+ bun install -g numux
64
+ \`\`\`` },
65
+ "quick-start": { title: "Quick start", body: `\`\`\`sh
66
+ numux init
67
+ \`\`\`
68
+
69
+ This creates a starter \`numux.config.ts\` with commented-out examples. Edit it, then run \`numux\`.` },
70
+ "config-file": { title: "Config file", body: `Create \`numux.config.ts\` (or \`.js\`):
71
+
72
+ \`\`\`ts
73
+ import { defineConfig } from 'numux'
74
+
75
+ export default defineConfig({
76
+ processes: {
77
+ db: {
78
+ command: 'docker compose up postgres',
79
+ readyPattern: 'ready to accept connections',
80
+ },
81
+ migrate: {
82
+ command: 'bun run migrate',
83
+ dependsOn: ['db'],
84
+ },
85
+ api: {
86
+ command: 'bun run dev:api',
87
+ dependsOn: ['migrate'],
88
+ readyPattern: 'listening on port 3000',
89
+ },
90
+ // String shorthand for simple processes
91
+ web: 'bun run dev:web',
92
+ // Interactive process \u2014 keyboard input is forwarded
93
+ confirm: {
94
+ command: 'sh -c "printf \\'Deploy to staging? [y/n] \\' && read answer && echo $answer"',
95
+ interactive: true,
96
+ },
97
+ },
98
+ })
99
+ \`\`\`
100
+
101
+ The \`defineConfig()\` helper is optional \u2014 it provides type checking for your config.
102
+
103
+ Processes can be a string (shorthand for \`{ command: "..." }\`), \`true\` or \`{}\` (auto-resolves to a matching \`package.json\` script), or a full config object.
104
+
105
+ Then run:
106
+
107
+ \`\`\`sh
108
+ numux
109
+ \`\`\`` },
110
+ subcommands: { title: "Subcommands", body: `<!-- generated:subcommands -->
111
+ \`\`\`sh
112
+ numux init # Create a starter config file
113
+ numux validate # Validate config and show process graph
114
+ numux exec <name> [--] <cmd> # Run a command in a process's environment
115
+ numux logs [name] # Open the log directory or a specific process log
116
+ numux completions <shell> # Generate shell completions (bash, zsh, fish)
117
+ numux help [topic] # Show help for a topic
118
+ \`\`\`
119
+ <!-- /generated:subcommands -->
120
+
121
+ \`validate\` respects \`--only\`/\`--exclude\` filters and shows processes grouped by dependency tiers.
122
+
123
+ \`exec\` runs a one-off command using a process's configured \`cwd\`, \`env\`, and \`envFile\` \u2014 useful for migrations, scripts, or any command that needs the same environment:
124
+
125
+ \`\`\`sh
126
+ numux exec api -- npx prisma migrate
127
+ numux exec web npm run build
128
+ \`\`\`
129
+
130
+ Set up completions for your shell:
131
+
132
+ \`\`\`sh
133
+ # Bash (add to ~/.bashrc)
134
+ eval "$(numux completions bash)"
135
+
136
+ # Zsh (add to ~/.zshrc)
137
+ eval "$(numux completions zsh)"
138
+
139
+ # Fish
140
+ numux completions fish | source
141
+ # Or save permanently:
142
+ numux completions fish > ~/.config/fish/completions/numux.fish
143
+ \`\`\`` },
144
+ workspaces: { title: "Workspaces", body: `Run a \`package.json\` script across all workspaces in a monorepo:
145
+
146
+ \`\`\`sh
147
+ numux -w dev
148
+ \`\`\`
149
+
150
+ Reads the \`workspaces\` field from your root \`package.json\`, finds which workspaces have the given script, and spawns \`<pm> run <script>\` in each. The package manager is auto-detected from \`packageManager\` field or lockfiles.
151
+
152
+ Composes with other flags:
153
+
154
+ \`\`\`sh
155
+ numux -w dev -n redis="redis-server" --colors
156
+ \`\`\`` },
157
+ "ad-hoc-commands": { title: "Ad-hoc commands", body: `\`\`\`sh
158
+ # Unnamed (name derived from command)
159
+ numux "bun dev:api" "bun dev:web"
160
+
161
+ # Named process
162
+ numux -n api="bun dev:api" -n web="bun dev:web"
163
+ \`\`\`` },
164
+ "script-patterns": { title: "Script patterns", body: `Run package.json scripts by name \u2014 any colon-containing name is automatically recognized as a script reference:
165
+
166
+ \`\`\`sh
167
+ numux 'lint:eslint --fix' # runs: yarn run lint:eslint --fix
168
+ numux 'dev:*' # all scripts matching dev:*
169
+ numux 'npm:*:dev' # explicit npm: prefix (same behavior)
170
+ \`\`\`
171
+
172
+ <!-- generated:script-pattern-rules -->` },
173
+ "script-pattern-rules": { title: "Script pattern rules", body: `**Recognition:** A process name is treated as a script reference when it:
174
+ - starts with \`npm:\` (e.g. \`npm:dev:*\`)
175
+ - contains glob metacharacters (\`*\`, \`?\`, \`[\`)
176
+ - contains a colon AND has no explicit \`command\` (e.g. \`lint:eslint: {}\`)
177
+
178
+ **Glob matching:** Patterns are matched against \`package.json\` scripts using
179
+ \`Bun.Glob\`. The \`*\` wildcard does NOT match across \`:\` separators \u2014 \`dev:*\`
180
+ matches \`dev:web\` but not \`dev:web:hmr\`. Use \`dev:*:*\` for two levels deep.
181
+
182
+ **Leaf-only (\`^\`):** Append \`^\` to skip scripts that are group runners \u2014
183
+ scripts that have sub-scripts beneath them. E.g. if \`format:check\` has
184
+ \`format:check:store\` and \`format:check:odoo\` below it, \`format:*^\` excludes
185
+ \`format:check\` but keeps the leaf scripts.
186
+
187
+ **Extra args:** Anything after the first space in the pattern is forwarded
188
+ as extra arguments to each matched command: \`lint:* --fix\` \u2192 \`bun run lint:js -- --fix\`.
189
+
190
+ **Template inheritance:** Config properties on a pattern entry (color, env,
191
+ dependsOn, etc.) are inherited by all expanded processes. Color arrays are
192
+ distributed round-robin across matches.
193
+
194
+ **Display names:** The glob's literal prefix and suffix are stripped from
195
+ matched script names: \`dev:*\` + \`dev:web\` \u2192 display name \`web\`.` },
196
+ "auto-resolution": { title: "Auto-resolution", body: `When a process has no \`command\` and its name matches a \`package.json\` script,
197
+ the command is auto-resolved to \`<pm> run <name>\`. This works for:
198
+ - \`true\` or \`{}\` shorthand: \`lint: true\` \u2192 \`bun run lint\`
199
+ - Objects without \`command\`: \`typecheck: { dependsOn: ['db'] }\` \u2192 \`bun run typecheck\`` },
200
+ "npm-prefix": { title: "npm: prefix", body: `Commands starting with \`npm:\` are rewritten to use the detected package
201
+ manager: \`npm:dev\` \u2192 \`bun run dev\` (if bun is detected).
202
+ <!-- /generated:script-pattern-rules -->
203
+
204
+ \`\`\`sh
205
+ numux 'lint:* --fix' # \u2192 bun run lint:js --fix, bun run lint:ts --fix
206
+ \`\`\`
207
+
208
+ In a config file, use the pattern as the process name:
209
+
210
+ \`\`\`ts
211
+ export default defineConfig({
212
+ processes: {
213
+ 'dev:*': { color: ['green', 'cyan'] },
214
+ 'lint:* --fix': {},
215
+ },
216
+ })
217
+ \`\`\`
218
+
219
+ Auto-resolution example:
220
+
221
+ \`\`\`ts
222
+ export default defineConfig({
223
+ processes: {
224
+ lint: true, // \u2192 bun run lint
225
+ typecheck: { dependsOn: ['db'] }, // \u2192 bun run typecheck (with dependency)
226
+ db: 'docker compose up postgres', // explicit command, not resolved
227
+ },
228
+ })
229
+ \`\`\`` },
230
+ options: { title: "Options", body: `<!-- generated:options -->
231
+ | Flag | Description |
232
+ |------|-------------|
233
+ | \`-s,\` \`--sort\` \`<config|alphabetical|topological>\` | Tab display order |
234
+ | \`-w,\` \`--workspace\` \`<script>\` | Run a package.json script across all workspaces |
235
+ | \`-n,\` \`--name\` \`<name=command>\` | Add a named process |
236
+ | \`-c,\` \`--color\` \`<colors>\` | Comma-separated colors (hex or names: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple) |
237
+ | \`--colors\` | Auto-assign colors to processes based on their name |
238
+ | \`-e,\` \`--env-file\` \`<path|false>\` | Env file path, or "false" to disable env file loading |
239
+ | \`--config\` \`<path>\` | Config file path (default: auto-detect) |
240
+ | \`-p,\` \`--prefix\` | Prefixed output mode (no TUI, for CI/scripts) |
241
+ | \`--only\` \`<a,b,...>\` | Only run these processes (+ their dependencies) |
242
+ | \`--exclude\` \`<a,b,...>\` | Exclude these processes |
243
+ | \`--kill-others\` | Kill all processes when any exits (regardless of exit code) |
244
+ | \`--kill-others-on-fail\` | Kill all processes when any exits with non-zero code |
245
+ | \`--max-restarts\` \`<n>\` | Max auto-restarts for crashed processes |
246
+ | \`--no-watch\` | Disable file watching even if config has watch patterns |
247
+ | \`-t,\` \`--timestamps\` \`[<format>]\` | Add timestamps to output (default HH:mm:ss, or pass a format string) |
248
+ | \`--log-dir\` \`<path>\` | Write per-process logs to directory |
249
+ | \`--debug\` | Enable debug logging to .numux/debug.log |
250
+ | \`-h,\` \`--help\` | Show this help |
251
+ | \`-v,\` \`--version\` | Show version |
252
+ <!-- /generated:options -->` },
253
+ "prefix-mode": { title: "Prefix mode", body: `Use \`--prefix\` (\`-p\`) for CI or headless environments. Output is printed with colored \`[name]\` prefixes instead of the TUI:
254
+
255
+ \`\`\`sh
256
+ numux --prefix
257
+ \`\`\`
258
+
259
+ Auto-exits when all processes finish. Exit code 1 if any process failed.` },
260
+ logging: { title: "Logging", body: `numux writes per-process log files (ANSI-stripped) when \`--log-dir\` is set or \`logDir\` is configured. Each session creates a timestamped subdirectory with a \`latest\` symlink pointing to the most recent run.
261
+
262
+ <!-- generated:logging-usage -->
263
+ \`\`\`sh
264
+ numux logs # Print log directory path
265
+ numux logs api # Pipe the api process log to stdout
266
+ numux logs api | grep "ERROR" # Search process logs
267
+ numux logs api | tail -f # Follow process log output
268
+ \`\`\`
269
+ <!-- /generated:logging-usage -->` },
270
+ "global-options": { title: "Global options", body: `Top-level options apply to all processes (process-level settings override):
271
+
272
+ <!-- generated:config-global -->
273
+ | Field | Type | Description |
274
+ |-------|------|-------------|
275
+ | \`cwd\` | \`string\` | Global working directory, inherited by all processes |
276
+ | \`env\` | \`Record<string, string>\` | Global env vars, merged into each process (process-level overrides) |
277
+ | \`envFile\` | \`string \\| string[] \\| false\` | Global .env file(s), inherited by processes without their own envFile; \`false\` disables |
278
+ | \`showCommand\` | \`boolean\` | Global showCommand flag, inherited by all processes |
279
+ | \`maxRestarts\` | \`number\` | Global restart limit, inherited by all processes (only restarts on non-zero exit) |
280
+ | \`readyTimeout\` | \`number\` | Global ready timeout (ms), inherited by all processes |
281
+ | \`stopSignal\` | \`'SIGTERM' \\| 'SIGINT' \\| 'SIGHUP'\` | Global stop signal, inherited by all processes |
282
+ | \`errorMatcher\` | \`boolean \\| string\` | Global error matcher, inherited by all processes. \`true\` = detect ANSI red output, string = regex |
283
+ | \`watch\` | \`string \\| string[]\` | Global watch patterns, inherited by processes without their own watch |
284
+ | \`sort\` | \`'config' \\| 'alphabetical' \\| 'topological'\` | Tab display order. \`'config'\` preserves definition order (package.json script order for wildcards), \`'alphabetical'\` sorts by process name, \`'topological'\` sorts by dependency tiers. |
285
+ | \`prefix\` | \`boolean\` | Use prefixed output mode instead of TUI (for CI/scripts) |
286
+ | \`timestamps\` | \`boolean \\| string\` | Add timestamps to output lines. \`true\` uses default \`HH:mm:ss\` format, or pass a format string (e.g. \`"HH:mm:ss.SSS"\`) |
287
+ | \`killOthers\` | \`boolean\` | Kill all processes when any one exits (regardless of exit code) |
288
+ | \`killOthersOnFail\` | \`boolean\` | Kill all processes when any one exits with a non-zero exit code |
289
+ | \`noWatch\` | \`boolean\` | Disable file watching even if processes have watch patterns |
290
+ | \`logDir\` | \`string\` | Directory to write per-process log files |
291
+ <!-- /generated:config-global -->
292
+
293
+ \`\`\`ts
294
+ export default defineConfig({
295
+ cwd: './packages/backend',
296
+ env: { NODE_ENV: 'development' },
297
+ envFile: '.env',
298
+ processes: {
299
+ api: { command: 'node server.js' }, // inherits cwd, env, envFile
300
+ web: { command: 'vite', cwd: './packages/web' }, // overrides cwd
301
+ },
302
+ })
303
+ \`\`\`` },
304
+ "process-options": { title: "Process options", body: `Each process accepts:
305
+
306
+ <!-- generated:config-process -->
307
+ | Field | Type | Default | Description |
308
+ |-------|------|---------|-------------|
309
+ | \`command\` | \`string\` | *required* | Shell command to run. Supports \`$dep.group\` references from dependency capture groups |
310
+ | \`cwd\` | \`string\` | \u2014 | Working directory for the process |
311
+ | \`env\` | \`Record<string, string>\` | \u2014 | Extra environment variables. Values support \`$dep.group\` references from dependency capture groups. |
312
+ | \`envFile\` | \`string \\| string[] \\| false\` | \u2014 | .env file path(s) to load, or \`false\` to disable |
313
+ | \`dependsOn\` | \`string \\| string[]\` | \u2014 | Processes that must be ready before this one starts |
314
+ | \`readyPattern\` | \`string \\| RegExp\` | \u2014 | Regex matched against stdout to signal readiness. Use \`RegExp\` to capture groups for \`$dep.group\` expansion |
315
+ | \`maxRestarts\` | \`number\` | \`0\` | Limit auto-restart attempts (only restarts on non-zero exit) |
316
+ | \`readyTimeout\` | \`number\` | \u2014 | Milliseconds to wait for readyPattern before failing |
317
+ | \`delay\` | \`number\` | \u2014 | Milliseconds to wait before starting the process |
318
+ | \`condition\` | \`string\` | \u2014 | Env var name (prefix with \`!\` to negate); process skipped if condition is falsy |
319
+ | \`platform\` | \`string \\| string[]\` | \u2014 | OS(es) this process runs on (e.g. \`'darwin'\`, \`'linux'\`). Non-matching processes are removed, their dependents still start |
320
+ | \`stopSignal\` | \`'SIGTERM' \\| 'SIGINT' \\| 'SIGHUP'\` | \`'SIGTERM'\` | Signal for graceful stop |
321
+ | \`color\` | \`string \\| string[]\` | \u2014 | Hex color (e.g. \`"#ff6600"\`) or color name. Array for round-robin in script patterns |
322
+ | \`watch\` | \`string \\| string[]\` | \u2014 | Glob patterns \u2014 restart process when matching files change |
323
+ | \`interactive\` | \`boolean\` | \`false\` | When true, keyboard input is forwarded to the process |
324
+ | \`optional\` | \`boolean\` | \u2014 | Process is visible but not started automatically. Use Alt+S to start manually |
325
+ | \`errorMatcher\` | \`boolean \\| string\` | \u2014 | \`true\` = detect ANSI red output, string = regex pattern |
326
+ | \`workspaces\` | \`boolean \\| string \\| string[]\` | \u2014 | Run command in monorepo workspaces. \`true\` = all workspaces, string = specific workspace by name/path, string[] = multiple workspaces |
327
+ | \`showCommand\` | \`boolean\` | \`true\` | Print the command being run as the first line of output |
328
+ <!-- /generated:config-process -->` },
329
+ "workspace-expansion": { title: "Workspace expansion", body: `Use \`workspaces\` on a process to expand it into per-workspace processes. Reads the \`workspaces\` field from your root \`package.json\`.
330
+
331
+ \`\`\`ts
332
+ export default defineConfig({
333
+ processes: {
334
+ // All workspaces \u2014 filters by script availability for PM run commands
335
+ lint: { command: 'npm run lint', workspaces: true },
336
+
337
+ // Specific workspace by package name
338
+ validate: { command: 'npm run validate', workspaces: '@repo/image-worker' },
339
+
340
+ // Multiple specific workspaces
341
+ dev: { command: 'npm run dev', workspaces: ['@repo/api', '@repo/web'] },
342
+ },
343
+ })
344
+ \`\`\`
345
+
346
+ Each entry expands into \`{name}:{wsName}\` processes (e.g. \`lint:api\`, \`lint:web\`) with \`cwd\` set to the workspace directory. All other config (env, dependsOn, color, etc.) is inherited from the template.
347
+
348
+ When \`workspaces: true\` is used with a PM run command (\`npm run lint\`), only workspaces that have the matching script are included. Raw commands (\`eslint .\`) run in all workspaces.
349
+
350
+ String values resolve by package name first (with or without scope), then fall back to relative path. Cannot be combined with \`cwd\`.` },
351
+ "file-watching": { title: "File watching", body: `Use \`watch\` to automatically restart a process when source files change:
352
+
353
+ \`\`\`ts
354
+ export default defineConfig({
355
+ processes: {
356
+ api: {
357
+ command: 'node server.js',
358
+ watch: 'src/**/*.ts',
359
+ },
360
+ styles: {
361
+ command: 'sass --watch src:dist',
362
+ watch: ['src/**/*.scss', 'src/**/*.css'],
363
+ },
364
+ },
365
+ })
366
+ \`\`\`
367
+
368
+ Patterns are matched relative to the process's \`cwd\` (or the project root). Changes in \`node_modules\` and \`.git\` are always ignored. Rapid file changes are debounced (300ms) to avoid restart storms.
369
+
370
+ A watched process is only restarted if it's currently running, ready, or failed \u2014 manually stopped processes are not affected.` },
371
+ "environment-variable-interpolation": { title: "Environment variable interpolation", body: `Config values support \`\${VAR}\` syntax for environment variable substitution:
372
+
373
+ \`\`\`ts
374
+ export default defineConfig({
375
+ processes: {
376
+ api: {
377
+ command: 'node server.js --port \${PORT:-3000}',
378
+ env: {
379
+ DATABASE_URL: '\${DATABASE_URL:?DATABASE_URL must be set}',
380
+ },
381
+ },
382
+ },
383
+ })
384
+ \`\`\`
385
+
386
+ | Syntax | Behavior |
387
+ |--------|----------|
388
+ | \`\${VAR}\` | Value of \`VAR\`, or empty string if unset |
389
+ | \`\${VAR:-default}\` | Value of \`VAR\`, or \`default\` if unset |
390
+ | \`\${VAR:?error}\` | Value of \`VAR\`, or error with message if unset |
391
+
392
+ Interpolation applies to all string values in the config (command, cwd, env, envFile, readyPattern, etc.).` },
393
+ "conditional-processes": { title: "Conditional processes", body: `Use \`condition\` to run a process only when an environment variable is set:
394
+
395
+ \`\`\`ts
396
+ export default defineConfig({
397
+ processes: {
398
+ seed: {
399
+ command: 'bun run seed',
400
+ condition: 'SEED_DB', // only runs when SEED_DB is set and truthy
401
+ },
402
+ storybook: {
403
+ command: 'bun run storybook',
404
+ condition: '!CI', // skipped in CI environments
405
+ },
406
+ },
407
+ })
408
+ \`\`\`
409
+
410
+ Falsy values: unset, empty string, \`"0"\`, \`"false"\`, \`"no"\`, \`"off"\` (case-insensitive). If a conditional process is skipped, its dependents are also skipped.` },
411
+ "optional-processes": { title: "Optional processes", body: `Use \`optional\` for tools you want visible in tabs but not auto-started (e.g. Prisma Studio, debug servers):
412
+
413
+ \`\`\`ts
414
+ export default defineConfig({
415
+ processes: {
416
+ app: { command: 'bun run dev' },
417
+ studio: {
418
+ command: 'bunx prisma studio',
419
+ optional: true, // shows as stopped tab, start with Alt+S
420
+ },
421
+ },
422
+ })
423
+ \`\`\`
424
+
425
+ Unlike \`condition\`, optional processes don't cascade \u2014 their dependents still start normally.` },
426
+ "dependency-orchestration": { title: "Dependency orchestration", body: `Each process starts as soon as its declared \`dependsOn\` dependencies are ready \u2014 it does not wait for unrelated processes. If a process fails, its dependents are skipped.
427
+
428
+ A process becomes **ready** when:
429
+ - **Has \`readyPattern\`** \u2014 the pattern matches in stdout (long-running server)
430
+ - **No \`readyPattern\`** \u2014 exits with code 0 (one-shot task)
431
+
432
+ Processes that crash (non-zero exit) can be auto-restarted by setting \`maxRestarts\` (default: \`0\`). Restarts use exponential backoff (1s\u201330s), which resets after 10s of uptime.` },
433
+ "dependency-output-capture": { title: "Dependency output capture", body: `When \`readyPattern\` is a \`RegExp\` (not a string), capture groups are extracted on match and expanded into dependent process \`command\` and \`env\` values using \`$process.group\` syntax:
434
+
435
+ \`\`\`ts
436
+ export default defineConfig({
437
+ processes: {
438
+ db: {
439
+ command: 'docker compose up postgres',
440
+ readyPattern: /ready to accept connections on port (?<port>\\d+)/,
441
+ },
442
+ api: {
443
+ command: 'node server.js --db-port $db.port',
444
+ dependsOn: ['db'],
445
+ env: { DB_PORT: '$db.port' },
446
+ },
447
+ },
448
+ })
449
+ \`\`\`
450
+
451
+ Both named (\`$db.port\`) and positional (\`$db.1\`) references work. Named groups also populate positional slots, so \`$db.port\` and \`$db.1\` both resolve to the same value above.
452
+
453
+ Unmatched references are left as-is (the shell will expand \`$db\` as empty + \`.port\` literal, making the issue visible). String \`readyPattern\` values work as before \u2014 readiness detection only, no capture extraction.` },
454
+ keybindings: { title: "Keybindings", body: `Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default \u2014 keyboard input is not forwarded to processes. Set \`interactive: true\` on processes that need stdin (REPLs, shells, etc.).
455
+
456
+ <!-- generated:keybindings -->
457
+ | Key | Action |
458
+ |-----|--------|
459
+ | \`\u2190\`/\`\u2192\` or \`1\`-\`9\` | Tabs |
460
+ | \`G/Shift+G\` | Top/bottom |
461
+ | \`R\` | Restart |
462
+ | \`S\` | Stop/start |
463
+ | \`F\` | Search |
464
+ | \`Y\` | Copy all |
465
+ | \`L\` | Clear |
466
+ | \`T\` | Timestamps |
467
+ | \`O\` | Open logs |
468
+ | \`Ctrl+Click\` | Open link |
469
+ | \`Ctrl+C\` | Quit |
470
+ <!-- /generated:keybindings -->
471
+
472
+ Search mode (after pressing \`F\`):
473
+
474
+ | Key | Action |
475
+ |-----|--------|
476
+ | \`Tab\` | Toggle between single-pane and all-process search |
477
+ | \`Enter\`/\`Shift+Enter\` | Next/previous match |
478
+ | \`Esc\` | Exit search |
479
+ | \`PageUp\`/\`PageDown\` | Scroll by page |` },
480
+ "tab-icons": { title: "Tab icons", body: `<!-- generated:tab-icons -->
481
+ | Icon | Status |
482
+ |------|--------|
483
+ | \u25CB | Pending |
484
+ | \u25D0 | Starting |
485
+ | \u25C9 | Running |
486
+ | \u25CF | Ready |
487
+ | \u25D1 | Stopping |
488
+ | \u25A0 | Stopped |
489
+ | \u2713 | Finished |
490
+ | \u2716 | Failed |
491
+ | \u2298 | Skipped |
492
+ <!-- /generated:tab-icons -->` },
493
+ "ghostty-opentui": { title: "ghostty-opentui", body: `Despite the name, [\`ghostty-opentui\`](https://github.com/remorses/ghostty-opentui) is **not** a compatibility layer for the [Ghostty](https://ghostty.org) terminal. It uses Ghostty's Zig-based VT parser as the ANSI terminal emulation engine for OpenTUI's terminal renderable. It works in any terminal emulator (iTerm, Kitty, Alacritty, WezTerm, etc.) and adds ~8MB to install size due to native binaries.` },
494
+ license: { title: "License", body: `MIT` }
495
+ };
496
+ });
497
+
498
+ // src/help.ts
499
+ var exports_help = {};
500
+ __export(exports_help, {
501
+ showHelp: () => showHelp
502
+ });
503
+ function stripMarkdown(md) {
504
+ return md.replace(/<!--[\s\S]*?-->/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\n{3,}/g, `
505
+
506
+ `).trim();
507
+ }
508
+ function showHelp(topic) {
509
+ if (!topic) {
510
+ const entries = Object.entries(HELP_TOPICS).map(([slug, t]) => ` ${slug.padEnd(40)}${t.title}`).join(`
511
+ `);
512
+ const aliases = Object.entries(TOPIC_ALIASES).map(([alias, target]) => ` ${alias} \u2192 ${target}`).join(`
513
+ `);
514
+ return `Available help topics:
515
+
516
+ ${entries}
517
+
518
+ Aliases:
519
+ ${aliases}
520
+
521
+ Usage: numux help <topic>`;
522
+ }
523
+ const resolved = TOPIC_ALIASES[topic] ?? topic;
524
+ const entry = HELP_TOPICS[resolved];
525
+ if (!entry) {
526
+ const available = Object.keys(HELP_TOPICS).join(", ");
527
+ return `Unknown topic: "${topic}"
528
+
529
+ Available topics: ${available}`;
530
+ }
531
+ return `${entry.title}
532
+ ${"=".repeat(entry.title.length)}
533
+
534
+ ${stripMarkdown(entry.body)}`;
535
+ }
536
+ var init_help = __esm(() => {
537
+ init_help_topics();
538
+ });
539
+
36
540
  // package.json
37
541
  var require_package = __commonJS((exports, module) => {
38
542
  module.exports = {
39
543
  name: "numux",
40
- version: "2.10.4",
544
+ version: "2.12.0",
41
545
  description: "Terminal multiplexer with dependency orchestration",
42
546
  type: "module",
43
547
  license: "MIT",
@@ -60,6 +564,7 @@ var require_package = __commonJS((exports, module) => {
60
564
  bin: {
61
565
  numux: "dist/bin.js"
62
566
  },
567
+ man: "dist/man/numux.1",
63
568
  exports: {
64
569
  ".": {
65
570
  types: "./dist/config.d.ts",
@@ -67,8 +572,9 @@ var require_package = __commonJS((exports, module) => {
67
572
  }
68
573
  },
69
574
  scripts: {
575
+ docs: "bun scripts/generate-docs.ts",
70
576
  build: "bun run build.ts",
71
- prepublishOnly: "bun run build",
577
+ prepublishOnly: "bun run docs && bun run build",
72
578
  dev: "cd example && bun run dev --debug",
73
579
  test: "bun test",
74
580
  typecheck: "bunx tsc --noEmit",
@@ -87,7 +593,8 @@ var require_package = __commonJS((exports, module) => {
87
593
  "@biomejs/biome": "^2.4.4",
88
594
  "@commitlint/cli": "^20.4.2",
89
595
  "@commitlint/config-conventional": "^20.4.2",
90
- "@types/bun": "^1.3.9"
596
+ "@types/bun": "^1.3.9",
597
+ "marked-man": "^2.1.0"
91
598
  },
92
599
  patchedDependencies: {
93
600
  "ghostty-opentui@1.4.7": "patches/ghostty-opentui@1.4.7.patch"
@@ -99,6 +606,122 @@ var require_package = __commonJS((exports, module) => {
99
606
  import { existsSync as existsSync6, writeFileSync } from "fs";
100
607
  import { resolve as resolve8 } from "path";
101
608
 
609
+ // src/config/loader.ts
610
+ import { existsSync as existsSync2, readFileSync } from "fs";
611
+ import { resolve as resolve2 } from "path";
612
+
613
+ // src/utils/logger.ts
614
+ import { appendFileSync, existsSync, mkdirSync } from "fs";
615
+ import { resolve } from "path";
616
+ var enabled = false;
617
+ var logFile = "";
618
+ var debugCallback = null;
619
+ function enableDebugLog(dir) {
620
+ const logDir = dir ?? resolve(process.cwd(), ".numux");
621
+ logFile = resolve(logDir, "debug.log");
622
+ if (!existsSync(logDir)) {
623
+ mkdirSync(logDir, { recursive: true });
624
+ }
625
+ enabled = true;
626
+ }
627
+ function log(...args) {
628
+ if (!enabled)
629
+ return;
630
+ try {
631
+ const timestamp = new Date().toISOString();
632
+ const formatted = args.length > 0 ? `${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}` : "";
633
+ const line = `[${timestamp}] ${formatted}`;
634
+ appendFileSync(logFile, `${line}
635
+ `);
636
+ debugCallback?.(line);
637
+ } catch {
638
+ enabled = false;
639
+ }
640
+ }
641
+
642
+ // src/config/interpolate.ts
643
+ var VAR_RE = /\$\{([^}:]+)(?::([-?])([^}]*))?\}/g;
644
+ function interpolateConfig(config) {
645
+ return interpolateValue(config);
646
+ }
647
+ function interpolateValue(value) {
648
+ if (typeof value === "string") {
649
+ return interpolateString(value);
650
+ }
651
+ if (Array.isArray(value)) {
652
+ return value.map(interpolateValue);
653
+ }
654
+ if (value instanceof RegExp) {
655
+ return value;
656
+ }
657
+ if (value && typeof value === "object") {
658
+ const result = {};
659
+ for (const [k, v] of Object.entries(value)) {
660
+ result[k] = interpolateValue(v);
661
+ }
662
+ return result;
663
+ }
664
+ return value;
665
+ }
666
+ function interpolateString(str) {
667
+ return str.replace(VAR_RE, (_match, name, operator, operand) => {
668
+ const value = process.env[name];
669
+ if (value !== undefined && value !== "") {
670
+ return value;
671
+ }
672
+ if (operator === "-") {
673
+ return operand ?? "";
674
+ }
675
+ if (operator === "?") {
676
+ throw new Error(operand || `Required variable ${name} is not set`);
677
+ }
678
+ return "";
679
+ });
680
+ }
681
+
682
+ // src/config/loader.ts
683
+ var CONFIG_FILES = ["numux.config.ts", "numux.config.js"];
684
+ async function loadConfig(configPath, cwd) {
685
+ if (configPath) {
686
+ return loadExplicitConfig(configPath);
687
+ }
688
+ return autoDetectConfig(cwd ?? process.cwd());
689
+ }
690
+ async function loadFile(path) {
691
+ try {
692
+ const mod = await import(path);
693
+ return interpolateConfig(mod.default ?? mod);
694
+ } catch (err) {
695
+ throw new Error(`Failed to load ${path}: ${err instanceof Error ? err.message : err}`, { cause: err });
696
+ }
697
+ }
698
+ async function loadExplicitConfig(configPath) {
699
+ const path = resolve2(configPath);
700
+ if (!existsSync2(path)) {
701
+ throw new Error(`Config file not found: ${path}`);
702
+ }
703
+ log(`Loading explicit config: ${path}`);
704
+ return loadFile(path);
705
+ }
706
+ async function autoDetectConfig(cwd) {
707
+ for (const file of CONFIG_FILES) {
708
+ const path = resolve2(cwd, file);
709
+ if (existsSync2(path)) {
710
+ log(`Found config file: ${path}`);
711
+ return loadFile(path);
712
+ }
713
+ }
714
+ const pkgPath = resolve2(cwd, "package.json");
715
+ if (existsSync2(pkgPath)) {
716
+ const pkgJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
717
+ if (pkgJson.numux && typeof pkgJson.numux === "object") {
718
+ log(`Found numux config in package.json`);
719
+ return interpolateConfig(pkgJson.numux);
720
+ }
721
+ }
722
+ throw new Error(`No numux config found. Create one of: ${CONFIG_FILES.join(", ")}, or add a "numux" key to package.json`);
723
+ }
724
+
102
725
  // src/cli-flags.ts
103
726
  var commaSplit = (raw) => raw.split(",").map((s) => s.trim()).filter(Boolean);
104
727
  var FLAGS = [
@@ -305,6 +928,14 @@ var SUBCOMMANDS = [
305
928
  name: "logs",
306
929
  description: "Open the log directory or a specific process log",
307
930
  usage: "logs [name]",
931
+ examples: [
932
+ ["numux logs", "Print log directory path"],
933
+ ["numux logs api", "Pipe the api process log to stdout"],
934
+ ['numux logs api | grep "ERROR"', "Search process logs"],
935
+ ["numux logs api | tail -f", "Follow process log output"]
936
+ ],
937
+ completionArgs: "dynamic",
938
+ completionScript: "numux logs 2>/dev/null",
308
939
  parse: (args, i, result) => {
309
940
  result.logs = true;
310
941
  const next = args[i + 1];
@@ -319,6 +950,7 @@ var SUBCOMMANDS = [
319
950
  name: "completions",
320
951
  description: "Generate shell completions (bash, zsh, fish)",
321
952
  usage: "completions <shell>",
953
+ completionArgs: ["bash", "zsh", "fish"],
322
954
  parse: (args, i, result) => {
323
955
  const next = args[++i];
324
956
  if (next === undefined)
@@ -326,6 +958,21 @@ var SUBCOMMANDS = [
326
958
  result.completions = next;
327
959
  return i;
328
960
  }
961
+ },
962
+ {
963
+ name: "help",
964
+ description: "Show help for a topic",
965
+ usage: "help [topic]",
966
+ completionArgs: "dynamic",
967
+ parse: (args, i, result) => {
968
+ result.help = true;
969
+ const next = args[i + 1];
970
+ if (next !== undefined && !next.startsWith("-")) {
971
+ result.helpTopic = next;
972
+ i++;
973
+ }
974
+ return i;
975
+ }
329
976
  }
330
977
  ];
331
978
  function generateHelp() {
@@ -355,7 +1002,7 @@ function generateHelp() {
355
1002
  const left = ` ${parts.join(" ")}`;
356
1003
  lines.push(`${left.padEnd(29)}${f.description}`);
357
1004
  }
358
- lines.push("", "Config files (auto-detected):", " numux.config.ts, numux.config.js");
1005
+ lines.push("", "Config files (auto-detected):", ` ${CONFIG_FILES.join(", ")}, or "numux" key in package.json`);
359
1006
  return lines.join(`
360
1007
  `);
361
1008
  }
@@ -514,7 +1161,16 @@ function filterConfig(config, only, exclude) {
514
1161
  }
515
1162
 
516
1163
  // src/completions.ts
1164
+ init_help_topics();
517
1165
  var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
1166
+ var HELP_TOPIC_NAMES = [...Object.keys(HELP_TOPICS), ...Object.keys(TOPIC_ALIASES)];
1167
+ function resolveArgs(sub) {
1168
+ if (!sub.completionArgs)
1169
+ return;
1170
+ if (sub.completionArgs === "dynamic" && sub.name === "help")
1171
+ return HELP_TOPIC_NAMES;
1172
+ return sub.completionArgs;
1173
+ }
518
1174
  function generateCompletions(shell) {
519
1175
  switch (shell) {
520
1176
  case "bash":
@@ -552,9 +1208,30 @@ function bashCompletions() {
552
1208
  return ;;`);
553
1209
  }
554
1210
  }
555
- caseEntries.push(` completions)
556
- COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
1211
+ for (const sub of SUBCOMMANDS) {
1212
+ const args = resolveArgs(sub);
1213
+ if (!args)
1214
+ continue;
1215
+ if (args === "dynamic") {
1216
+ const script = sub.completionScript;
1217
+ if (!script)
1218
+ continue;
1219
+ const dir = `$(${script})`;
1220
+ caseEntries.push(` ${sub.name})
1221
+ local logdir
1222
+ logdir="${dir}"
1223
+ if [ -n "$logdir" ] && [ -d "$logdir" ]; then
1224
+ local names
1225
+ names="$(ls "$logdir"/*.log 2>/dev/null | xargs -I{} basename {} .log)"
1226
+ COMPREPLY=( $(compgen -W "$names" -- "$cur") )
1227
+ fi
557
1228
  return ;;`);
1229
+ } else {
1230
+ caseEntries.push(` ${sub.name})
1231
+ COMPREPLY=( $(compgen -W "${args.join(" ")}" -- "$cur") )
1232
+ return ;;`);
1233
+ }
1234
+ }
558
1235
  const allFlags = FLAGS.flatMap((f) => f.short ? [f.short, f.long] : [f.long]);
559
1236
  const subcmds = SUBCOMMANDS.map((s) => s.name);
560
1237
  return `# numux bash completions
@@ -614,6 +1291,31 @@ function zshCompletions() {
614
1291
  }
615
1292
  const argsBlock = argLines.map((l) => `${l} \\`).join(`
616
1293
  `);
1294
+ const subcmdCases = [];
1295
+ for (const sub of SUBCOMMANDS) {
1296
+ const args = resolveArgs(sub);
1297
+ if (!args)
1298
+ continue;
1299
+ if (args === "dynamic") {
1300
+ const script = sub.completionScript;
1301
+ if (!script)
1302
+ continue;
1303
+ subcmdCases.push(` ${sub.name})
1304
+ local logdir names
1305
+ logdir="\\$(${script})"
1306
+ if [[ -n "\\$logdir" ]] && [[ -d "\\$logdir" ]]; then
1307
+ names=( \\$(ls "\\$logdir"/*.log 2>/dev/null | xargs -I{} basename {} .log) )
1308
+ _describe 'process' names
1309
+ fi
1310
+ ;;`);
1311
+ } else {
1312
+ subcmdCases.push(` ${sub.name})
1313
+ local -a args
1314
+ args=(${args.map((a) => `'${a}'`).join(" ")})
1315
+ _describe '${sub.name}' args
1316
+ ;;`);
1317
+ }
1318
+ }
617
1319
  return `#compdef numux
618
1320
  # numux zsh completions
619
1321
  # Add to ~/.zshrc: eval "$(numux completions zsh)"
@@ -634,6 +1336,11 @@ ${argsBlock}
634
1336
  _describe 'subcommand' subcmds
635
1337
  ;;
636
1338
  esac
1339
+
1340
+ case "\${words[2]}" in
1341
+ ${subcmdCases.join(`
1342
+ `)}
1343
+ esac
637
1344
  }
638
1345
  _numux`;
639
1346
  }
@@ -649,7 +1356,22 @@ function fishCompletions() {
649
1356
  for (const s of SUBCOMMANDS) {
650
1357
  lines.push(`complete -c numux -n __fish_use_subcommand -a ${s.name} -d '${sq(s.description)}'`);
651
1358
  }
652
- lines.push("", "# Completions subcommand", "complete -c numux -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'", "", "# Options");
1359
+ for (const sub of SUBCOMMANDS) {
1360
+ const args = resolveArgs(sub);
1361
+ if (!args)
1362
+ continue;
1363
+ lines.push("");
1364
+ lines.push(`# ${sub.name} subcommand`);
1365
+ if (args === "dynamic") {
1366
+ const script = sub.completionScript;
1367
+ if (!script)
1368
+ continue;
1369
+ lines.push(`complete -c numux -n '__fish_seen_subcommand_from ${sub.name}' -a '(set -l d (${script}); and ls $d/*.log 2>/dev/null | xargs -I{} basename {} .log)'`);
1370
+ } else {
1371
+ lines.push(`complete -c numux -n '__fish_seen_subcommand_from ${sub.name}' -a '${args.join(" ")}'`);
1372
+ }
1373
+ }
1374
+ lines.push("", "# Options");
653
1375
  for (const f of FLAGS) {
654
1376
  const parts = ["complete -c numux"];
655
1377
  if (f.short)
@@ -672,8 +1394,8 @@ function fishCompletions() {
672
1394
  }
673
1395
 
674
1396
  // src/config/expand-scripts.ts
675
- import { existsSync, readFileSync } from "fs";
676
- import { resolve } from "path";
1397
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1398
+ import { resolve as resolve3 } from "path";
677
1399
  var LOCKFILE_PM = [
678
1400
  ["bun.lockb", "bun"],
679
1401
  ["bun.lock", "bun"],
@@ -689,7 +1411,7 @@ function detectPackageManager(pkgJson, cwd) {
689
1411
  return name;
690
1412
  }
691
1413
  for (const [file, pm] of LOCKFILE_PM) {
692
- if (existsSync(resolve(cwd, file)))
1414
+ if (existsSync3(resolve3(cwd, file)))
693
1415
  return pm;
694
1416
  }
695
1417
  return "npm";
@@ -757,11 +1479,11 @@ function expandScriptPatterns(config, cwd) {
757
1479
  if (!(hasScriptRef || hasNpmCommand || hasCommandlessEntry))
758
1480
  return config;
759
1481
  const dir = config.cwd ?? cwd ?? process.cwd();
760
- const pkgPath = resolve(dir, "package.json");
761
- if (!existsSync(pkgPath) && hasScriptRef) {
1482
+ const pkgPath = resolve3(dir, "package.json");
1483
+ if (!existsSync3(pkgPath) && hasScriptRef) {
762
1484
  throw new Error(`Wildcard patterns require a package.json (looked in ${dir})`);
763
1485
  }
764
- const pkgJson = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf-8")) : {};
1486
+ const pkgJson = existsSync3(pkgPath) ? JSON.parse(readFileSync2(pkgPath, "utf-8")) : {};
765
1487
  const scripts = pkgJson.scripts;
766
1488
  const scriptNames = scripts && typeof scripts === "object" ? Object.keys(scripts) : [];
767
1489
  const pm = detectPackageManager(pkgJson, dir);
@@ -820,122 +1542,6 @@ function expandScriptPatterns(config, cwd) {
820
1542
  return { ...config, processes: expanded };
821
1543
  }
822
1544
 
823
- // src/config/loader.ts
824
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
825
- import { resolve as resolve3 } from "path";
826
-
827
- // src/utils/logger.ts
828
- import { appendFileSync, existsSync as existsSync2, mkdirSync } from "fs";
829
- import { resolve as resolve2 } from "path";
830
- var enabled = false;
831
- var logFile = "";
832
- var debugCallback = null;
833
- function enableDebugLog(dir) {
834
- const logDir = dir ?? resolve2(process.cwd(), ".numux");
835
- logFile = resolve2(logDir, "debug.log");
836
- if (!existsSync2(logDir)) {
837
- mkdirSync(logDir, { recursive: true });
838
- }
839
- enabled = true;
840
- }
841
- function log(...args) {
842
- if (!enabled)
843
- return;
844
- try {
845
- const timestamp = new Date().toISOString();
846
- const formatted = args.length > 0 ? `${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}` : "";
847
- const line = `[${timestamp}] ${formatted}`;
848
- appendFileSync(logFile, `${line}
849
- `);
850
- debugCallback?.(line);
851
- } catch {
852
- enabled = false;
853
- }
854
- }
855
-
856
- // src/config/interpolate.ts
857
- var VAR_RE = /\$\{([^}:]+)(?::([-?])([^}]*))?\}/g;
858
- function interpolateConfig(config) {
859
- return interpolateValue(config);
860
- }
861
- function interpolateValue(value) {
862
- if (typeof value === "string") {
863
- return interpolateString(value);
864
- }
865
- if (Array.isArray(value)) {
866
- return value.map(interpolateValue);
867
- }
868
- if (value instanceof RegExp) {
869
- return value;
870
- }
871
- if (value && typeof value === "object") {
872
- const result = {};
873
- for (const [k, v] of Object.entries(value)) {
874
- result[k] = interpolateValue(v);
875
- }
876
- return result;
877
- }
878
- return value;
879
- }
880
- function interpolateString(str) {
881
- return str.replace(VAR_RE, (_match, name, operator, operand) => {
882
- const value = process.env[name];
883
- if (value !== undefined && value !== "") {
884
- return value;
885
- }
886
- if (operator === "-") {
887
- return operand ?? "";
888
- }
889
- if (operator === "?") {
890
- throw new Error(operand || `Required variable ${name} is not set`);
891
- }
892
- return "";
893
- });
894
- }
895
-
896
- // src/config/loader.ts
897
- var CONFIG_FILES = ["numux.config.ts", "numux.config.js"];
898
- async function loadConfig(configPath, cwd) {
899
- if (configPath) {
900
- return loadExplicitConfig(configPath);
901
- }
902
- return autoDetectConfig(cwd ?? process.cwd());
903
- }
904
- async function loadFile(path) {
905
- try {
906
- const mod = await import(path);
907
- return interpolateConfig(mod.default ?? mod);
908
- } catch (err) {
909
- throw new Error(`Failed to load ${path}: ${err instanceof Error ? err.message : err}`, { cause: err });
910
- }
911
- }
912
- async function loadExplicitConfig(configPath) {
913
- const path = resolve3(configPath);
914
- if (!existsSync3(path)) {
915
- throw new Error(`Config file not found: ${path}`);
916
- }
917
- log(`Loading explicit config: ${path}`);
918
- return loadFile(path);
919
- }
920
- async function autoDetectConfig(cwd) {
921
- for (const file of CONFIG_FILES) {
922
- const path = resolve3(cwd, file);
923
- if (existsSync3(path)) {
924
- log(`Found config file: ${path}`);
925
- return loadFile(path);
926
- }
927
- }
928
- const pkgPath = resolve3(cwd, "package.json");
929
- if (existsSync3(pkgPath)) {
930
- const pkgJson = JSON.parse(readFileSync2(pkgPath, "utf-8"));
931
- if (pkgJson.numux && typeof pkgJson.numux === "object") {
932
- log(`Found numux config in package.json`);
933
- return interpolateConfig(pkgJson.numux);
934
- }
935
- }
936
- throw new Error(`No numux config found. Create one of: ${CONFIG_FILES.join(", ")}, or add a "numux" key to package.json`);
937
- }
938
-
939
1545
  // src/config/platform.ts
940
1546
  function filterByPlatform(config, currentPlatform = process.platform) {
941
1547
  const excluded = new Set;
@@ -3724,7 +4330,7 @@ class App {
3724
4330
  }
3725
4331
  if (name === SHORTCUTS.clear.key) {
3726
4332
  this.panes.get(this.activePane)?.clear();
3727
- this.logWriter.truncate(this.activePane);
4333
+ this.logWriter.markCopyStart(this.activePane);
3728
4334
  return;
3729
4335
  }
3730
4336
  if (name === SHORTCUTS.timestamps.key) {
@@ -3855,10 +4461,7 @@ class App {
3855
4461
  copyAllText() {
3856
4462
  if (!this.activePane)
3857
4463
  return;
3858
- const pane = this.panes.get(this.activePane);
3859
- if (!pane)
3860
- return;
3861
- const text = pane.getText();
4464
+ const text = this.logWriter.readLog(this.activePane);
3862
4465
  if (!text) {
3863
4466
  this.statusBar.showTemporaryMessage("No output to copy");
3864
4467
  return;
@@ -4113,13 +4716,14 @@ ${DIM}Done in ${elapsed}${RESET}
4113
4716
  }
4114
4717
 
4115
4718
  // src/utils/log-writer.ts
4116
- import { closeSync, mkdirSync as mkdirSync2, openSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
4719
+ import { closeSync, mkdirSync as mkdirSync2, openSync, readSync, rmSync, symlinkSync, unlinkSync, writeSync } from "fs";
4117
4720
  import { tmpdir } from "os";
4118
4721
  import { basename as basename2, join } from "path";
4119
4722
  class LogWriter {
4120
4723
  dir;
4121
4724
  isTemp;
4122
4725
  files = new Map;
4726
+ copyOffsets = new Map;
4123
4727
  decoder = new TextDecoder;
4124
4728
  encoder = new TextEncoder;
4125
4729
  constructor(dir, isTemp = false) {
@@ -4175,6 +4779,35 @@ class LogWriter {
4175
4779
  `);
4176
4780
  }
4177
4781
  };
4782
+ markCopyStart(name) {
4783
+ const path = this.getLogPath(name);
4784
+ if (!path)
4785
+ return;
4786
+ try {
4787
+ this.copyOffsets.set(name, Bun.file(path).size);
4788
+ } catch {}
4789
+ }
4790
+ readLog(name) {
4791
+ const path = this.getLogPath(name);
4792
+ if (!path)
4793
+ return;
4794
+ try {
4795
+ const size = Bun.file(path).size;
4796
+ const offset = this.copyOffsets.get(name) ?? 0;
4797
+ if (size <= offset)
4798
+ return;
4799
+ const fd = openSync(path, "r");
4800
+ try {
4801
+ const buf = Buffer.alloc(size - offset);
4802
+ readSync(fd, buf, 0, buf.length, offset);
4803
+ return buf.toString("utf-8");
4804
+ } finally {
4805
+ closeSync(fd);
4806
+ }
4807
+ } catch {
4808
+ return;
4809
+ }
4810
+ }
4178
4811
  getLogPath(name) {
4179
4812
  if (this.files.has(name)) {
4180
4813
  return join(this.dir, `${name}.log`);
@@ -4278,17 +4911,6 @@ class LogWriter {
4278
4911
  return [];
4279
4912
  }
4280
4913
  }
4281
- truncate(name) {
4282
- const fd = this.files.get(name);
4283
- if (fd === undefined)
4284
- return;
4285
- try {
4286
- closeSync(fd);
4287
- const path = join(this.dir, `${name}.log`);
4288
- const newFd = openSync(path, "w");
4289
- this.files.set(name, newFd);
4290
- } catch {}
4291
- }
4292
4914
  close() {
4293
4915
  for (const fd of this.files.values()) {
4294
4916
  closeSync(fd);
@@ -4392,7 +5014,12 @@ export default defineConfig({
4392
5014
  async function main() {
4393
5015
  const parsed = parseArgs(process.argv);
4394
5016
  if (parsed.help) {
4395
- console.info(HELP);
5017
+ if (parsed.helpTopic) {
5018
+ const { showHelp: showHelp2 } = await Promise.resolve().then(() => (init_help(), exports_help));
5019
+ console.info(showHelp2(parsed.helpTopic));
5020
+ } else {
5021
+ console.info(HELP);
5022
+ }
4396
5023
  process.exit(0);
4397
5024
  }
4398
5025
  if (parsed.version) {