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 +54 -28
- package/package.json +15 -16
- package/skills/README.md +1 -1
- package/skills/claude/team.md +2 -2
- package/skills/codex/SKILL.md +2 -2
- package/src/cli.test.ts +427 -1
- package/src/cli.ts +10 -6
- package/src/commands/basic-commands.test.ts +133 -1
- package/src/commands/config-command.test.ts +87 -1
- package/src/commands/config.ts +36 -2
- package/src/commands/help.ts +2 -2
- package/src/commands/install.test.ts +1 -1
- package/src/commands/install.ts +2 -14
- package/src/commands/learn.ts +3 -3
- package/src/commands/preamble.test.ts +378 -0
- package/src/commands/talk.test.ts +162 -114
- package/src/commands/talk.ts +51 -38
- package/src/commands/this.ts +20 -0
- package/src/config.test.ts +2 -1
- package/src/config.ts +5 -1
- package/src/context.test.ts +1 -1
- package/src/tmux.test.ts +60 -30
- package/src/tmux.ts +43 -8
- package/src/types.ts +3 -1
- package/src/ui.test.ts +46 -0
- package/src/commands/setup.test.ts +0 -175
- package/src/commands/setup.ts +0 -163
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
|
-
|
|
19
|
+
tmt install claude # or: tmt install codex
|
|
20
20
|
|
|
21
|
-
# 2.
|
|
22
|
-
|
|
21
|
+
# 2. Go to working folder and initialize
|
|
22
|
+
tmt init
|
|
23
23
|
|
|
24
|
-
# 3.
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
36
|
-
| `talk <agent> "msg"
|
|
37
|
-
| `talk all "msg"
|
|
38
|
-
| `check <agent> [lines]` | Read agent's pane output
|
|
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
|
|
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 `
|
|
65
|
+
Run `tmt help` for all commands and options.
|
|
47
66
|
|
|
48
|
-
##
|
|
67
|
+
## Message Delivery
|
|
49
68
|
|
|
50
|
-
|
|
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
|
-
|
|
74
|
+
tmt config set pasteEnterDelayMs 500
|
|
55
75
|
```
|
|
56
76
|
|
|
57
|
-
|
|
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
|
-
|
|
83
|
+
tmt ls
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
**
|
|
86
|
+
**Edit** - Modify `tmux-team.json` directly:
|
|
63
87
|
```json
|
|
64
88
|
{
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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
|
-
**
|
|
71
|
-
|
|
72
|
-
|
|
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"
|
|
92
|
-
/team talk all "I'm starting the database migration"
|
|
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
|
-
|
|
101
|
-
|
|
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
|
+
"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
|
|
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
|
|
package/skills/claude/team.md
CHANGED
|
@@ -39,8 +39,8 @@ tmux-team list
|
|
|
39
39
|
|
|
40
40
|
## Notes
|
|
41
41
|
|
|
42
|
-
- `talk`
|
|
43
|
-
-
|
|
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/skills/codex/SKILL.md
CHANGED
|
@@ -39,8 +39,8 @@ tmux-team list
|
|
|
39
39
|
|
|
40
40
|
## Notes
|
|
41
41
|
|
|
42
|
-
- `talk`
|
|
43
|
-
-
|
|
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
|
});
|