tmux-team 3.2.4 → 4.0.0-beta.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/README.md CHANGED
@@ -16,60 +16,86 @@ npm install -g tmux-team
16
16
 
17
17
  ```bash
18
18
  # 1. Install for your AI agent
19
- tmux-team install claude # or: tmux-team install codex
19
+ tmt install claude # or: tmt install codex
20
20
 
21
- # 2. Run the setup wizard (auto-detects panes)
22
- tmux-team setup
21
+ # 2. Go to working folder and initialize
22
+ tmt init
23
23
 
24
- # 3. Talk to agents
25
- tmux-team talk codex "Review this code" --wait
24
+ # 3. Register agents (run inside each agent's pane)
25
+ tmt this claude # registers current pane as "claude"
26
+ tmt this codex # registers current pane as "codex"
27
+
28
+ # 4. Talk to agents
29
+ tmt talk codex "Review this code" # waits for response by default
30
+
31
+ # 5. Update or remove an agent
32
+ tmt update codex --pane 2.3
33
+ tmt rm codex
26
34
  ```
27
35
 
28
- The `--wait` flag blocks until the agent responds, returning the response directly.
36
+ > **Tip:** Most AI agents support `!` to run bash commands. From inside Claude Code, Codex, or Gemini CLI, you can run `!tmt this myname` to quickly register that pane.
37
+
38
+ ## Cross-Folder Collaboration
39
+
40
+ Agents don't need to be in the same folder to collaborate. You can add an agent from one project to another:
41
+
42
+ ```bash
43
+ # In project-a folder, add an agent that's running in project-b
44
+ tmt add codex-reviewer 5.1 # Use the pane ID from the other project
45
+ ```
46
+
47
+ Find pane IDs with: `tmux display-message -p "#{pane_id}"`
29
48
 
30
49
  ## Commands
31
50
 
32
51
  | Command | Description |
33
52
  |---------|-------------|
34
53
  | `install [claude\|codex]` | Install tmux-team for an AI agent |
35
- | `setup` | Interactive wizard to configure agents |
36
- | `talk <agent> "msg" --wait` | Send message and wait for response |
37
- | `talk all "msg" --wait` | Broadcast to all agents |
38
- | `check <agent> [lines]` | Read agent's pane output (fallback if --wait times out) |
54
+ | `this <name> [remark]` | Register current pane as an agent |
55
+ | `talk <agent> "msg"` | Send message and wait for response |
56
+ | `talk all "msg"` | Broadcast to all agents |
57
+ | `check <agent> [lines]` | Read agent's pane output |
39
58
  | `list` | Show configured agents |
40
59
  | `learn` | Show educational guide |
41
60
 
42
- **Options for `talk --wait`:**
61
+ **Options for `talk`:**
43
62
  - `--timeout <seconds>` - Max wait time (default: 180s)
44
63
  - `--lines <number>` - Lines to capture from response (default: 100)
45
64
 
46
- Run `tmux-team help` for all commands and options.
65
+ Run `tmt help` for all commands and options.
47
66
 
48
- ## Managing Your Team
67
+ ## Message Delivery
49
68
 
50
- Configuration lives in `tmux-team.json` in your project root.
69
+ tmux-team uses tmux buffers + paste, then waits briefly before sending Enter. This avoids shell history expansion and handles paste-safety windows in CLIs like Gemini.
70
+
71
+ **Config:** `pasteEnterDelayMs` (default: 500)
51
72
 
52
- **Create** - Run the setup wizard to auto-detect agents:
53
73
  ```bash
54
- tmux-team setup
74
+ tmt config set pasteEnterDelayMs 500
55
75
  ```
56
76
 
57
- **Read** - List configured agents:
77
+ ## Managing Your Team
78
+
79
+ Configuration lives in `tmux-team.json` in your project root.
80
+
81
+ **List** - Show configured agents:
58
82
  ```bash
59
- tmux-team list
83
+ tmt ls
60
84
  ```
61
85
 
62
- **Update** - Edit `tmux-team.json` directly or re-run setup:
86
+ **Edit** - Modify `tmux-team.json` directly:
63
87
  ```json
64
88
  {
65
- "codex": { "pane": "%1", "remark": "Code reviewer" },
66
- "gemini": { "pane": "%2", "remark": "Documentation" }
89
+ "$config": { "pasteEnterDelayMs": 500 },
90
+ "codex": { "pane": "1.1", "remark": "Code reviewer" },
91
+ "gemini": { "pane": "1.2", "remark": "Documentation" }
67
92
  }
68
93
  ```
69
94
 
70
- **Delete** - Remove an agent entry from `tmux-team.json` or delete the file entirely.
71
-
72
- Find pane IDs: `tmux display-message -p "#{pane_id}"`
95
+ **Remove** - Delete an agent:
96
+ ```bash
97
+ tmt rm codex
98
+ ```
73
99
 
74
100
  ## Claude Code Plugin
75
101
 
@@ -88,8 +114,8 @@ Run this once when starting a session. Claude will understand how to coordinate
88
114
 
89
115
  **`/team`** - Talk to other agents
90
116
  ```
91
- /team talk codex "Review my authentication changes" --wait
92
- /team talk all "I'm starting the database migration" --wait
117
+ /team talk codex "Review my authentication changes"
118
+ /team talk all "I'm starting the database migration"
93
119
  /team list
94
120
  ```
95
121
  Use this to delegate tasks, ask for reviews, or broadcast updates.
@@ -97,8 +123,8 @@ Use this to delegate tasks, ask for reviews, or broadcast updates.
97
123
  ## Learn More
98
124
 
99
125
  ```bash
100
- tmux-team learn # Comprehensive guide
101
- tmux-team help # All commands and options
126
+ tmt learn # Comprehensive guide
127
+ tmt help # All commands and options
102
128
  ```
103
129
 
104
130
  ## License
package/package.json CHANGED
@@ -1,25 +1,12 @@
1
1
  {
2
2
  "name": "tmux-team",
3
- "version": "3.2.4",
3
+ "version": "4.0.0-beta.0",
4
4
  "description": "CLI tool for AI agent collaboration in tmux - manage cross-pane communication",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tmux-team": "./bin/tmux-team",
8
8
  "tmt": "./bin/tmux-team"
9
9
  },
10
- "scripts": {
11
- "dev": "tsx src/cli.ts",
12
- "tmt": "./bin/tmux-team",
13
- "test": "pnpm test:run",
14
- "test:watch": "vitest",
15
- "test:run": "vitest run --coverage && node scripts/check-coverage.mjs --threshold 95",
16
- "lint": "oxlint src/",
17
- "lint:fix": "oxlint src/ --fix",
18
- "format": "prettier --write src/",
19
- "format:check": "prettier --check src/",
20
- "type:check": "tsc --noEmit",
21
- "check": "pnpm type:check && pnpm lint && pnpm format:check"
22
- },
23
10
  "keywords": [
24
11
  "tmux",
25
12
  "cli",
@@ -37,7 +24,6 @@
37
24
  "engines": {
38
25
  "node": ">=18"
39
26
  },
40
- "packageManager": "pnpm@9.15.4",
41
27
  "os": [
42
28
  "darwin",
43
29
  "linux"
@@ -57,5 +43,18 @@
57
43
  "prettier": "^3.7.4",
58
44
  "typescript": "^5.3.0",
59
45
  "vitest": "^1.2.0"
46
+ },
47
+ "scripts": {
48
+ "dev": "tsx src/cli.ts",
49
+ "tmt": "./bin/tmux-team",
50
+ "test": "pnpm test:run",
51
+ "test:watch": "vitest",
52
+ "test:run": "vitest run --coverage && node scripts/check-coverage.mjs --threshold 90 --branches 85",
53
+ "lint": "oxlint src/",
54
+ "lint:fix": "oxlint src/ --fix",
55
+ "format": "prettier --write src/",
56
+ "format:check": "prettier --check src/",
57
+ "type:check": "tsc --noEmit",
58
+ "check": "pnpm type:check && pnpm lint && pnpm format:check"
60
59
  }
61
- }
60
+ }
package/skills/README.md CHANGED
@@ -29,7 +29,7 @@ tmux-team install claude
29
29
  tmux-team install codex
30
30
  ```
31
31
 
32
- After installation, run `tmux-team setup` to configure your agents interactively.
32
+ After installation, run `tmux-team add <name> <pane>` to register your agents, or use `tmux-team this <name>` inside each agent's tmux pane.
33
33
 
34
34
  ## Claude Code
35
35
 
@@ -39,8 +39,8 @@ tmux-team list
39
39
 
40
40
  ## Notes
41
41
 
42
- - `talk` automatically sends Enter key after the message
43
- - `talk` automatically filters exclamation marks for Gemini (TTY issue)
42
+ - `talk` sends via tmux buffer paste, then waits briefly before Enter
43
+ - Control the delay with `pasteEnterDelayMs` in config (default: 500)
44
44
  - Use `--delay` instead of sleep (safer for tool whitelists)
45
45
  - Use `--wait` for synchronous request-response patterns
46
46
  - Run `tmux-team help` for full CLI documentation
@@ -39,8 +39,8 @@ tmux-team list
39
39
 
40
40
  ## Notes
41
41
 
42
- - `talk` automatically sends Enter key after the message
43
- - `talk` automatically filters exclamation marks for Gemini (TTY issue)
42
+ - `talk` sends via tmux buffer paste, then waits briefly before Enter
43
+ - Control the delay with `pasteEnterDelayMs` in config (default: 500)
44
44
  - Use `--delay` instead of sleep (safer for tool whitelists)
45
45
  - Use `--wait` for synchronous request-response patterns
46
46
  - Run `tmux-team help` for full CLI documentation
package/src/cli.test.ts CHANGED
@@ -16,7 +16,7 @@ function makeStubContext(): Context {
16
16
  config: {
17
17
  mode: 'polling',
18
18
  preambleMode: 'always',
19
- defaults: { timeout: 180, pollInterval: 1, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3 },
19
+ defaults: { timeout: 180, pollInterval: 1, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 },
20
20
  agents: {},
21
21
  paneRegistry: {},
22
22
  },
@@ -160,4 +160,430 @@ describe('cli', () => {
160
160
  expect(errSpy).toHaveBeenCalledWith(JSON.stringify({ error: 'boom' }));
161
161
  expect(exitSpy).toHaveBeenCalledWith(1);
162
162
  });
163
+
164
+ it('routes install command', async () => {
165
+ vi.resetModules();
166
+ process.argv = ['node', 'cli', 'install', 'claude'];
167
+
168
+ const ctx = makeStubContext();
169
+ const installSpy = vi.fn();
170
+ vi.doMock('./context.js', () => ({
171
+ createContext: () => ctx,
172
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
173
+ }));
174
+ vi.doMock('./commands/install.js', () => ({ cmdInstall: installSpy }));
175
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
176
+
177
+ await import('./cli.js');
178
+ // allow async to resolve
179
+ await new Promise((r) => setTimeout(r, 0));
180
+
181
+ expect(installSpy).toHaveBeenCalledWith(ctx, 'claude');
182
+ expect(exitSpy).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it('routes preamble command', async () => {
186
+ vi.resetModules();
187
+ process.argv = ['node', 'cli', 'preamble', 'show'];
188
+
189
+ const ctx = makeStubContext();
190
+ const preambleSpy = vi.fn();
191
+ vi.doMock('./context.js', () => ({
192
+ createContext: () => ctx,
193
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
194
+ }));
195
+ vi.doMock('./commands/preamble.js', () => ({ cmdPreamble: preambleSpy }));
196
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
197
+
198
+ await import('./cli.js');
199
+ await new Promise((r) => setTimeout(r, 0));
200
+
201
+ expect(preambleSpy).toHaveBeenCalledWith(ctx, ['show']);
202
+ expect(exitSpy).not.toHaveBeenCalled();
203
+ });
204
+
205
+ it('routes this command', async () => {
206
+ vi.resetModules();
207
+ process.argv = ['node', 'cli', 'this', 'myagent', 'remark'];
208
+
209
+ const ctx = makeStubContext();
210
+ const thisSpy = vi.fn();
211
+ vi.doMock('./context.js', () => ({
212
+ createContext: () => ctx,
213
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
214
+ }));
215
+ vi.doMock('./commands/this.js', () => ({ cmdThis: thisSpy }));
216
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
217
+
218
+ await import('./cli.js');
219
+ await new Promise((r) => setTimeout(r, 0));
220
+
221
+ expect(thisSpy).toHaveBeenCalledWith(ctx, 'myagent', 'remark');
222
+ expect(exitSpy).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it('errors when this command is missing name', async () => {
226
+ vi.resetModules();
227
+ process.argv = ['node', 'cli', 'this'];
228
+
229
+ const ctx = makeStubContext();
230
+ const exitSpy = vi.fn();
231
+ ctx.exit = exitSpy as any;
232
+ vi.doMock('./context.js', () => ({
233
+ createContext: () => ctx,
234
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
235
+ }));
236
+ vi.doMock('./commands/this.js', () => ({ cmdThis: vi.fn() }));
237
+
238
+ await import('./cli.js');
239
+ await new Promise((r) => setTimeout(r, 0));
240
+
241
+ expect(ctx.ui.error).toHaveBeenCalledWith('Usage: tmux-team this <name> [remark]');
242
+ expect(exitSpy).toHaveBeenCalledWith(1);
243
+ });
244
+
245
+ it('routes update command with --pane and --remark flags', async () => {
246
+ vi.resetModules();
247
+ process.argv = ['node', 'cli', 'update', 'codex', '--pane', '2.0', '--remark', 'updated'];
248
+
249
+ const ctx = makeStubContext();
250
+ const updateSpy = vi.fn();
251
+ vi.doMock('./context.js', () => ({
252
+ createContext: () => ctx,
253
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
254
+ }));
255
+ vi.doMock('./commands/update.js', () => ({ cmdUpdate: updateSpy }));
256
+
257
+ await import('./cli.js');
258
+ await new Promise((r) => setTimeout(r, 0));
259
+
260
+ expect(updateSpy).toHaveBeenCalledWith(ctx, 'codex', { pane: '2.0', remark: 'updated' });
261
+ });
262
+
263
+ it('routes init command', async () => {
264
+ vi.resetModules();
265
+ process.argv = ['node', 'cli', 'init'];
266
+
267
+ const ctx = makeStubContext();
268
+ const initSpy = vi.fn();
269
+ vi.doMock('./context.js', () => ({
270
+ createContext: () => ctx,
271
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
272
+ }));
273
+ vi.doMock('./commands/init.js', () => ({ cmdInit: initSpy }));
274
+
275
+ await import('./cli.js');
276
+ await new Promise((r) => setTimeout(r, 0));
277
+
278
+ expect(initSpy).toHaveBeenCalledWith(ctx);
279
+ });
280
+
281
+ it('routes list command', async () => {
282
+ vi.resetModules();
283
+ process.argv = ['node', 'cli', 'list'];
284
+
285
+ const ctx = makeStubContext();
286
+ const listSpy = vi.fn();
287
+ vi.doMock('./context.js', () => ({
288
+ createContext: () => ctx,
289
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
290
+ }));
291
+ vi.doMock('./commands/list.js', () => ({ cmdList: listSpy }));
292
+
293
+ await import('./cli.js');
294
+ await new Promise((r) => setTimeout(r, 0));
295
+
296
+ expect(listSpy).toHaveBeenCalledWith(ctx);
297
+ });
298
+
299
+ it('routes ls alias to list command', async () => {
300
+ vi.resetModules();
301
+ process.argv = ['node', 'cli', 'ls'];
302
+
303
+ const ctx = makeStubContext();
304
+ const listSpy = vi.fn();
305
+ vi.doMock('./context.js', () => ({
306
+ createContext: () => ctx,
307
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
308
+ }));
309
+ vi.doMock('./commands/list.js', () => ({ cmdList: listSpy }));
310
+
311
+ await import('./cli.js');
312
+ await new Promise((r) => setTimeout(r, 0));
313
+
314
+ expect(listSpy).toHaveBeenCalledWith(ctx);
315
+ });
316
+
317
+ it('routes add command', async () => {
318
+ vi.resetModules();
319
+ process.argv = ['node', 'cli', 'add', 'myagent', '1.0', 'remark'];
320
+
321
+ const ctx = makeStubContext();
322
+ const addSpy = vi.fn();
323
+ vi.doMock('./context.js', () => ({
324
+ createContext: () => ctx,
325
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
326
+ }));
327
+ vi.doMock('./commands/add.js', () => ({ cmdAdd: addSpy }));
328
+
329
+ await import('./cli.js');
330
+ await new Promise((r) => setTimeout(r, 0));
331
+
332
+ expect(addSpy).toHaveBeenCalledWith(ctx, 'myagent', '1.0', 'remark');
333
+ });
334
+
335
+ it('routes config command', async () => {
336
+ vi.resetModules();
337
+ process.argv = ['node', 'cli', 'config', 'get', 'mode'];
338
+
339
+ const ctx = makeStubContext();
340
+ const configSpy = vi.fn();
341
+ vi.doMock('./context.js', () => ({
342
+ createContext: () => ctx,
343
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
344
+ }));
345
+ vi.doMock('./commands/config.js', () => ({ cmdConfig: configSpy }));
346
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
347
+
348
+ await import('./cli.js');
349
+ await new Promise((r) => setTimeout(r, 0));
350
+
351
+ expect(configSpy).toHaveBeenCalledWith(ctx, ['get', 'mode']);
352
+ expect(exitSpy).not.toHaveBeenCalled();
353
+ });
354
+
355
+ it('parses --timeout flag with seconds', async () => {
356
+ vi.resetModules();
357
+ process.argv = ['node', 'cli', 'talk', 'claude', 'hi', '--timeout', '30'];
358
+
359
+ const ctx = makeStubContext();
360
+ const talkSpy = vi.fn();
361
+ vi.doMock('./context.js', () => ({
362
+ createContext: (opts: any) => {
363
+ ctx.flags = opts.flags;
364
+ return ctx;
365
+ },
366
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
367
+ }));
368
+ vi.doMock('./commands/talk.js', () => ({ cmdTalk: talkSpy }));
369
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
370
+
371
+ await import('./cli.js');
372
+ await new Promise((r) => setTimeout(r, 0));
373
+
374
+ expect(ctx.flags.timeout).toBe(30);
375
+ });
376
+
377
+ it('parses --timeout flag with ms suffix', async () => {
378
+ vi.resetModules();
379
+ process.argv = ['node', 'cli', 'talk', 'claude', 'hi', '--timeout', '500ms'];
380
+
381
+ const ctx = makeStubContext();
382
+ const talkSpy = vi.fn();
383
+ vi.doMock('./context.js', () => ({
384
+ createContext: (opts: any) => {
385
+ ctx.flags = opts.flags;
386
+ return ctx;
387
+ },
388
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
389
+ }));
390
+ vi.doMock('./commands/talk.js', () => ({ cmdTalk: talkSpy }));
391
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
392
+
393
+ await import('./cli.js');
394
+ await new Promise((r) => setTimeout(r, 0));
395
+
396
+ expect(ctx.flags.timeout).toBe(0.5);
397
+ });
398
+
399
+ it('parses --lines flag', async () => {
400
+ vi.resetModules();
401
+ process.argv = ['node', 'cli', 'talk', 'claude', 'hi', '--wait', '--lines', '50'];
402
+
403
+ const ctx = makeStubContext();
404
+ const talkSpy = vi.fn();
405
+ vi.doMock('./context.js', () => ({
406
+ createContext: (opts: any) => {
407
+ ctx.flags = opts.flags;
408
+ return ctx;
409
+ },
410
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
411
+ }));
412
+ vi.doMock('./commands/talk.js', () => ({ cmdTalk: talkSpy }));
413
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
414
+
415
+ await import('./cli.js');
416
+ await new Promise((r) => setTimeout(r, 0));
417
+
418
+ expect(ctx.flags.lines).toBe(50);
419
+ });
420
+
421
+ it('parses --no-preamble flag', async () => {
422
+ vi.resetModules();
423
+ process.argv = ['node', 'cli', 'talk', 'claude', 'hi', '--no-preamble'];
424
+
425
+ const ctx = makeStubContext();
426
+ const talkSpy = vi.fn();
427
+ vi.doMock('./context.js', () => ({
428
+ createContext: (opts: any) => {
429
+ ctx.flags = opts.flags;
430
+ return ctx;
431
+ },
432
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
433
+ }));
434
+ vi.doMock('./commands/talk.js', () => ({ cmdTalk: talkSpy }));
435
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
436
+
437
+ await import('./cli.js');
438
+ await new Promise((r) => setTimeout(r, 0));
439
+
440
+ expect(ctx.flags.noPreamble).toBe(true);
441
+ });
442
+
443
+ it('routes check command with lines argument', async () => {
444
+ vi.resetModules();
445
+ process.argv = ['node', 'cli', 'check', 'claude', '50'];
446
+
447
+ const ctx = makeStubContext();
448
+ const checkSpy = vi.fn();
449
+ vi.doMock('./context.js', () => ({
450
+ createContext: () => ctx,
451
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
452
+ }));
453
+ vi.doMock('./commands/check.js', () => ({ cmdCheck: checkSpy }));
454
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
455
+
456
+ await import('./cli.js');
457
+ await new Promise((r) => setTimeout(r, 0));
458
+
459
+ expect(checkSpy).toHaveBeenCalledWith(ctx, 'claude', 50);
460
+ });
461
+
462
+ it('routes update command with --pane= syntax', async () => {
463
+ vi.resetModules();
464
+ process.argv = ['node', 'cli', 'update', 'claude', '--pane=2.0'];
465
+
466
+ const ctx = makeStubContext();
467
+ const updateSpy = vi.fn();
468
+ vi.doMock('./context.js', () => ({
469
+ createContext: () => ctx,
470
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
471
+ }));
472
+ vi.doMock('./commands/update.js', () => ({ cmdUpdate: updateSpy }));
473
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
474
+
475
+ await import('./cli.js');
476
+ await new Promise((r) => setTimeout(r, 0));
477
+
478
+ expect(updateSpy).toHaveBeenCalledWith(ctx, 'claude', { pane: '2.0' });
479
+ });
480
+
481
+ it('routes update command with --remark= syntax', async () => {
482
+ vi.resetModules();
483
+ process.argv = ['node', 'cli', 'update', 'claude', '--remark=new remark'];
484
+
485
+ const ctx = makeStubContext();
486
+ const updateSpy = vi.fn();
487
+ vi.doMock('./context.js', () => ({
488
+ createContext: () => ctx,
489
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
490
+ }));
491
+ vi.doMock('./commands/update.js', () => ({ cmdUpdate: updateSpy }));
492
+ vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
493
+
494
+ await import('./cli.js');
495
+ await new Promise((r) => setTimeout(r, 0));
496
+
497
+ expect(updateSpy).toHaveBeenCalledWith(ctx, 'claude', { remark: 'new remark' });
498
+ });
499
+
500
+ it('errors on talk with missing arguments', async () => {
501
+ vi.resetModules();
502
+ process.argv = ['node', 'cli', 'talk', 'claude']; // missing message
503
+
504
+ const ctx = makeStubContext();
505
+ vi.doMock('./context.js', () => ({
506
+ createContext: () => ctx,
507
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
508
+ }));
509
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
510
+
511
+ await import('./cli.js');
512
+ await new Promise((r) => setTimeout(r, 0));
513
+
514
+ expect(ctx.ui.error).toHaveBeenCalled();
515
+ expect(exitSpy).toHaveBeenCalledWith(1);
516
+ });
517
+
518
+ it('errors on add with missing arguments', async () => {
519
+ vi.resetModules();
520
+ process.argv = ['node', 'cli', 'add', 'claude']; // missing pane
521
+
522
+ const ctx = makeStubContext();
523
+ vi.doMock('./context.js', () => ({
524
+ createContext: () => ctx,
525
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
526
+ }));
527
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
528
+
529
+ await import('./cli.js');
530
+ await new Promise((r) => setTimeout(r, 0));
531
+
532
+ expect(ctx.ui.error).toHaveBeenCalled();
533
+ expect(exitSpy).toHaveBeenCalledWith(1);
534
+ });
535
+
536
+ it('errors on update with missing arguments', async () => {
537
+ vi.resetModules();
538
+ process.argv = ['node', 'cli', 'update']; // missing name
539
+
540
+ const ctx = makeStubContext();
541
+ vi.doMock('./context.js', () => ({
542
+ createContext: () => ctx,
543
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
544
+ }));
545
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
546
+
547
+ await import('./cli.js');
548
+ await new Promise((r) => setTimeout(r, 0));
549
+
550
+ expect(ctx.ui.error).toHaveBeenCalled();
551
+ expect(exitSpy).toHaveBeenCalledWith(1);
552
+ });
553
+
554
+ it('errors on remove with missing arguments', async () => {
555
+ vi.resetModules();
556
+ process.argv = ['node', 'cli', 'remove']; // missing name
557
+
558
+ const ctx = makeStubContext();
559
+ vi.doMock('./context.js', () => ({
560
+ createContext: () => ctx,
561
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
562
+ }));
563
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
564
+
565
+ await import('./cli.js');
566
+ await new Promise((r) => setTimeout(r, 0));
567
+
568
+ expect(ctx.ui.error).toHaveBeenCalled();
569
+ expect(exitSpy).toHaveBeenCalledWith(1);
570
+ });
571
+
572
+ it('errors on check with missing arguments', async () => {
573
+ vi.resetModules();
574
+ process.argv = ['node', 'cli', 'check']; // missing target
575
+
576
+ const ctx = makeStubContext();
577
+ vi.doMock('./context.js', () => ({
578
+ createContext: () => ctx,
579
+ ExitCodes: { SUCCESS: 0, ERROR: 1 },
580
+ }));
581
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
582
+
583
+ await import('./cli.js');
584
+ await new Promise((r) => setTimeout(r, 0));
585
+
586
+ expect(ctx.ui.error).toHaveBeenCalled();
587
+ expect(exitSpy).toHaveBeenCalledWith(1);
588
+ });
163
589
  });