@brimveyn/aimux 1.18.1 → 1.18.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.18.1",
3
+ "version": "1.18.3",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
package/src/cli/index.ts CHANGED
@@ -3,7 +3,7 @@ import type { CliContext } from './context'
3
3
 
4
4
  import { connectToDaemon, DaemonUnreachableError } from './client/bootstrap'
5
5
  import { resolveWorkspace } from './client/workspace-resolver'
6
- import { CliUsageError, parseArgs } from './flags'
6
+ import { type ArgSpec, CliUsageError, type FlagSpec, parseArgs, SHARED_FLAGS } from './flags'
7
7
  import {
8
8
  EXIT_DAEMON_UNREACHABLE,
9
9
  EXIT_OK,
@@ -12,24 +12,148 @@ import {
12
12
  writeError,
13
13
  writeJson,
14
14
  } from './output'
15
- import { COMMANDS, resolveCommand } from './registry'
15
+ import { type CliCommand, COMMANDS, resolveCommand } from './registry'
16
+
17
+ const EXIT_CODES_BLOCK = [
18
+ 'Exit codes:',
19
+ ' 0 success',
20
+ ' 2 usage error (bad flags, unknown command, missing argument)',
21
+ ' 3 runtime error (server replied with error, command failed)',
22
+ ' 4 daemon unreachable (socket missing and autostart failed)',
23
+ ' 124 timeout (tab wait, tab tail --timeout, workspace switch --wait)',
24
+ ].join('\n')
25
+
26
+ const OUTPUT_CONTRACT_BLOCK = [
27
+ 'Output contract:',
28
+ ' stdout one JSON object per command (NDJSON for `tab tail`, `tab wait`)',
29
+ ' stderr human-readable error lines, prefixed with `aimux:`',
30
+ ].join('\n')
31
+
32
+ function groupByCommand(commands: readonly CliCommand[]): Map<string, CliCommand[]> {
33
+ const byGroup = new Map<string, CliCommand[]>()
34
+ for (const command of commands) {
35
+ const existing = byGroup.get(command.group) ?? []
36
+ existing.push(command)
37
+ byGroup.set(command.group, existing)
38
+ }
39
+ return byGroup
40
+ }
41
+
42
+ function isSharedFlag(name: string): boolean {
43
+ return SHARED_FLAGS.some((f) => f.name === name)
44
+ }
45
+
46
+ function formatArgs(args: readonly ArgSpec[]): string {
47
+ if (args.length === 0) return ''
48
+ return args.map((a) => (a.required === true ? `<${a.name}>` : `[${a.name}]`)).join(' ')
49
+ }
50
+
51
+ function flagValueHint(flag: FlagSpec): string {
52
+ if (flag.kind === 'boolean') return ''
53
+ if (flag.kind === 'number') return ' <n>'
54
+ return ` <${flag.name}>`
55
+ }
56
+
57
+ function formatFlagLine(flag: FlagSpec): string {
58
+ const head = ` --${flag.name}${flagValueHint(flag)}`.padEnd(32)
59
+ return `${head}${flag.description ?? ''}`
60
+ }
16
61
 
17
62
  function printHelp(): void {
18
63
  process.stdout.write(
19
- 'aimux CLI control plane\n\nUsage:\n aimux <group> <verb> [flags] [args]\n\n'
64
+ [
65
+ 'aimux CLI control plane — drive workspaces, worktrees, and tabs from scripts.',
66
+ '',
67
+ 'Usage:',
68
+ ' aimux <group> <verb> [flags] [args]',
69
+ ' aimux <group> --help List verbs in a group',
70
+ ' aimux <group> <verb> --help Show flags/args for a verb',
71
+ '',
72
+ 'Groups:',
73
+ '',
74
+ ].join('\n')
20
75
  )
21
- const byGroup = new Map<string, typeof COMMANDS>()
22
- for (const command of COMMANDS) {
23
- const existing = byGroup.get(command.group) ?? []
24
- byGroup.set(command.group, [...existing, command] as typeof COMMANDS)
25
- }
76
+
77
+ const byGroup = groupByCommand(COMMANDS)
26
78
  for (const [group, commands] of byGroup) {
27
79
  process.stdout.write(` ${group}\n`)
28
80
  for (const command of commands) {
29
- process.stdout.write(` aimux ${group} ${command.verb.padEnd(10)} ${command.summary}\n`)
81
+ const args = formatArgs(command.args)
82
+ const signature = `aimux ${group} ${command.verb}${args === '' ? '' : ` ${args}`}`
83
+ process.stdout.write(` ${signature.padEnd(46)} ${command.summary}\n`)
30
84
  }
31
85
  process.stdout.write('\n')
32
86
  }
87
+
88
+ process.stdout.write(
89
+ [
90
+ 'Shared flags (accepted by every command):',
91
+ ...SHARED_FLAGS.map(formatFlagLine),
92
+ '',
93
+ OUTPUT_CONTRACT_BLOCK,
94
+ '',
95
+ EXIT_CODES_BLOCK,
96
+ '',
97
+ 'Env:',
98
+ ' AIMUX_PROFILE Runtime profile (state dir, socket paths); --profile overrides.',
99
+ '',
100
+ 'Agent recipes:',
101
+ ' # spawn Claude in a new tab, wait until it idles, snapshot the screen',
102
+ ' TAB=$(aimux tab create --assistant claude --title fixup | jq -r .tabId)',
103
+ ' aimux tab send "$TAB" "explain this repo" --enter',
104
+ ' aimux tab wait "$TAB" --status idle --timeout 60000',
105
+ ' aimux tab snapshot "$TAB" --tail 40 --format text',
106
+ '',
107
+ ' # stream renders as NDJSON (one event per line)',
108
+ ' aimux tab tail "$TAB" --rate-limit-ms 100 --follow-status',
109
+ '',
110
+ ].join('\n')
111
+ )
112
+ }
113
+
114
+ function printGroupHelp(group: string): void {
115
+ const commands = COMMANDS.filter((c) => c.group === group)
116
+ if (commands.length === 0) {
117
+ printHelp()
118
+ return
119
+ }
120
+ process.stdout.write(
121
+ [
122
+ `aimux ${group} — ${commands.length} verb${commands.length === 1 ? '' : 's'}`,
123
+ '',
124
+ 'Usage:',
125
+ ` aimux ${group} <verb> [flags] [args]`,
126
+ ` aimux ${group} <verb> --help Show flags/args for a verb`,
127
+ '',
128
+ 'Verbs:',
129
+ '',
130
+ ].join('\n')
131
+ )
132
+ for (const command of commands) {
133
+ const args = formatArgs(command.args)
134
+ const signature = `aimux ${group} ${command.verb}${args === '' ? '' : ` ${args}`}`
135
+ process.stdout.write(` ${signature.padEnd(46)} ${command.summary}\n`)
136
+ }
137
+ process.stdout.write('\n')
138
+ }
139
+
140
+ function printCommandHelp(command: CliCommand): void {
141
+ const args = formatArgs(command.args)
142
+ const signature = `aimux ${command.group} ${command.verb}${args === '' ? '' : ` ${args}`}`
143
+ process.stdout.write([signature, '', ` ${command.summary}`, '', 'Flags:', ''].join('\n'))
144
+ const own = command.flags.filter((f) => !isSharedFlag(f.name))
145
+ if (own.length === 0) {
146
+ process.stdout.write(' (this command takes only the shared flags)\n')
147
+ } else {
148
+ for (const flag of own) process.stdout.write(`${formatFlagLine(flag)}\n`)
149
+ }
150
+ process.stdout.write('\nShared flags:\n')
151
+ for (const flag of SHARED_FLAGS) process.stdout.write(`${formatFlagLine(flag)}\n`)
152
+ process.stdout.write(`\n${OUTPUT_CONTRACT_BLOCK}\n\n${EXIT_CODES_BLOCK}\n`)
153
+ }
154
+
155
+ function argvRequestsHelp(argv: readonly string[]): boolean {
156
+ return argv.includes('--help') || argv.includes('-h')
33
157
  }
34
158
 
35
159
  export async function runCli(argv: readonly string[]): Promise<number> {
@@ -40,6 +164,20 @@ export async function runCli(argv: readonly string[]): Promise<number> {
40
164
 
41
165
  const group = argv[0] ?? ''
42
166
  const verb = argv[1] ?? ''
167
+
168
+ // `aimux <group> --help` (or bare `aimux <group>`) — group-scoped help. Print
169
+ // the verb list without erroring on the missing verb, so agents can drill
170
+ // down step by step.
171
+ if (verb === '' || verb === '--help' || verb === '-h') {
172
+ if (COMMANDS.some((c) => c.group === group)) {
173
+ printGroupHelp(group)
174
+ return EXIT_OK
175
+ }
176
+ writeError(`unknown group: ${group}`)
177
+ printHelp()
178
+ return EXIT_USAGE
179
+ }
180
+
43
181
  const command = resolveCommand(group, verb)
44
182
  if (!command) {
45
183
  writeError(`unknown command: ${group} ${verb}`)
@@ -47,6 +185,13 @@ export async function runCli(argv: readonly string[]): Promise<number> {
47
185
  return EXIT_USAGE
48
186
  }
49
187
 
188
+ // `aimux <group> <verb> --help` — verb-scoped help. Handle BEFORE parseArgs
189
+ // so verbs that don't declare a `--help` flag don't reject it as unknown.
190
+ if (argvRequestsHelp(argv.slice(2))) {
191
+ printCommandHelp(command)
192
+ return EXIT_OK
193
+ }
194
+
50
195
  let parsed
51
196
  try {
52
197
  parsed = parseArgs(argv.slice(2), command.flags, command.args)
package/src/index.tsx CHANGED
@@ -60,9 +60,78 @@ if (command === 'terminal-manager') {
60
60
  await runTerminalManager()
61
61
  }
62
62
 
63
- if (command === '--help' || command === '-h') {
63
+ if (command === '--help' || command === '-h' || command === 'help') {
64
64
  process.stdout.write(
65
- 'aimux -- terminal multiplexer for AI CLIs\n\nUsage:\n aimux Start aimux\n aimux update Update to latest version\n aimux doctor Diagnose setup issues\n aimux restart-daemon Restart IPC daemon\n aimux restart-terminal-manager Restart terminal-manager (kills live workspaces)\n\n'
65
+ [
66
+ 'aimux — terminal multiplexer for AI CLIs',
67
+ '',
68
+ 'Two surfaces:',
69
+ ' • Interactive TUI (no args): drive assistants side-by-side in one window.',
70
+ ' • CLI control plane (`aimux <group> <verb>`): script-friendly, JSON output,',
71
+ ' designed to be driven by another agent or a shell pipeline.',
72
+ '',
73
+ 'Interactive',
74
+ ' aimux Start the TUI in the current profile',
75
+ '',
76
+ 'CLI control plane (JSON on stdout, human-readable errors on stderr)',
77
+ ' aimux tab list | create | send | focus | close | snapshot | tail | wait',
78
+ ' aimux workspace list | show | create | switch | close',
79
+ ' aimux worktree list | create | remove',
80
+ '',
81
+ ' aimux <group> --help List verbs in that group',
82
+ ' aimux <group> <verb> --help Show flags, args, and exit codes',
83
+ '',
84
+ 'Common CLI verbs at a glance',
85
+ ' aimux tab list Enumerate tabs (+ activeTabId)',
86
+ ' aimux tab create --assistant <id> [--title …] Spawn claude / codex / opencode / terminal / …',
87
+ ' aimux tab send <tabId> [text] [--enter|--keys|--stdin] Type, chord, or paste into a tab',
88
+ ' aimux tab focus <tabId> Bring a tab to the foreground',
89
+ ' aimux tab close <tabId> Terminate a tab',
90
+ ' aimux tab snapshot <tabId> [--tail N] [--format …] Capture the screen (json | text)',
91
+ ' aimux tab tail <tabId> [--follow-status] […] NDJSON stream of renders / status',
92
+ ' aimux tab wait <tabId> --status <idle|working|waiting-input> Block on an activity state',
93
+ ' aimux workspace show Dump the active workspace + worktrees',
94
+ ' aimux workspace create <name> [--project P] [--switch [--wait]]',
95
+ ' aimux workspace switch <ws> [--wait --timeout N] Move the running UI to another workspace',
96
+ ' aimux worktree create --name <n> [--branch B --base R]',
97
+ ' aimux worktree remove <id|path> [--force]',
98
+ '',
99
+ 'Shared flags (accepted by every CLI verb)',
100
+ ' --workspace <id|name> Target a specific workspace (default: active)',
101
+ ' --profile <name> Runtime profile (also settable via AIMUX_PROFILE)',
102
+ ' --json Reserved; JSON is always on',
103
+ '',
104
+ 'Maintenance',
105
+ ' aimux update Update aimux to the latest published version',
106
+ ' aimux doctor Diagnose setup (paths, sockets, PTY, integrations)',
107
+ ' aimux restart-daemon Restart the IPC daemon (keeps live workspaces)',
108
+ ' aimux restart-terminal-manager Restart terminal-manager (kills live workspaces)',
109
+ ' aimux --version Print the installed version',
110
+ '',
111
+ 'Exit codes',
112
+ ' 0 success',
113
+ ' 2 usage error (bad flags, unknown command, missing argument)',
114
+ ' 3 runtime error (server replied with error, command failed)',
115
+ ' 4 daemon unreachable (socket missing and autostart failed)',
116
+ ' 124 timeout (tab wait, tab tail --timeout, workspace switch --wait)',
117
+ '',
118
+ 'Env',
119
+ ' AIMUX_PROFILE Runtime profile (state dir, socket paths)',
120
+ '',
121
+ 'Recipes for agents',
122
+ ' # spawn Claude, wait for idle, dump the last 40 non-blank lines',
123
+ ' TAB=$(aimux tab create --assistant claude --title fixup | jq -r .tabId)',
124
+ ' aimux tab send "$TAB" "explain this repo" --enter',
125
+ ' aimux tab wait "$TAB" --status idle --timeout 60000',
126
+ ' aimux tab snapshot "$TAB" --tail 40 --format text',
127
+ '',
128
+ ' # send a control chord (Ctrl-C then Esc) using vim-style notation',
129
+ ' aimux tab send "$TAB" "<C-c><Esc>" --keys',
130
+ '',
131
+ ' # follow a tab as NDJSON, one render (or tabStatus) per line',
132
+ ' aimux tab tail "$TAB" --rate-limit-ms 100 --follow-status',
133
+ '',
134
+ ].join('\n')
66
135
  )
67
136
  process.exit(0)
68
137
  }
@@ -60,8 +60,15 @@ export async function assertSafeAimuxWorktreePath(path: string): Promise<void> {
60
60
  if (!isInsideAimuxWorktreeRoot(path)) {
61
61
  throw new Error(`refusing worktree path outside Aimux temp root: ${path}`)
62
62
  }
63
+ // Create the repo-scoped parent (<root>/r-<hash>) before resolving it: git
64
+ // worktree add does not create intermediate dirs, and realpath() would throw
65
+ // ENOENT on the first worktree for a repo. mkdir(recursive) leaves an
66
+ // existing symlink in place, so the realpath check below still catches an
67
+ // escape out of the temp root.
68
+ const parent = resolve(path, '..')
69
+ await mkdir(parent, { recursive: true })
63
70
  const realRoot = await realpath(root)
64
- const realParent = await realpath(resolve(path, '..'))
71
+ const realParent = await realpath(parent)
65
72
  if (`${realParent}/`.startsWith(`${realRoot}/`)) return
66
73
  throw new Error(`unsafe Aimux worktree parent: ${realParent}`)
67
74
  }