tmux-team 3.0.1 → 3.1.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 +55 -259
- package/package.json +16 -14
- package/skills/README.md +9 -10
- package/src/cli.test.ts +163 -0
- package/src/cli.ts +20 -17
- package/src/commands/basic-commands.test.ts +252 -0
- package/src/commands/config-command.test.ts +116 -0
- package/src/commands/help.ts +4 -1
- package/src/commands/install.test.ts +205 -0
- package/src/commands/install.ts +207 -0
- package/src/commands/setup.test.ts +175 -0
- package/src/commands/setup.ts +163 -0
- package/src/commands/talk.test.ts +169 -101
- package/src/commands/talk.ts +186 -98
- package/src/context.test.ts +68 -0
- package/src/identity.test.ts +70 -0
- package/src/state.test.ts +14 -0
- package/src/tmux.test.ts +50 -0
- package/src/tmux.ts +66 -2
- package/src/types.ts +10 -1
- package/src/ui.test.ts +63 -0
- package/src/version.test.ts +31 -0
- package/src/commands/install-skill.ts +0 -148
package/src/cli.ts
CHANGED
|
@@ -19,7 +19,8 @@ import { cmdCheck } from './commands/check.js';
|
|
|
19
19
|
import { cmdCompletion } from './commands/completion.js';
|
|
20
20
|
import { cmdConfig } from './commands/config.js';
|
|
21
21
|
import { cmdPreamble } from './commands/preamble.js';
|
|
22
|
-
import {
|
|
22
|
+
import { cmdInstall } from './commands/install.js';
|
|
23
|
+
import { cmdSetup } from './commands/setup.js';
|
|
23
24
|
import { cmdLearn } from './commands/learn.js';
|
|
24
25
|
|
|
25
26
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -42,6 +43,8 @@ function parseArgs(argv: string[]): { command: string; args: string[]; flags: Fl
|
|
|
42
43
|
flags.json = true;
|
|
43
44
|
} else if (arg === '--verbose' || arg === '-v') {
|
|
44
45
|
flags.verbose = true;
|
|
46
|
+
} else if (arg === '--debug') {
|
|
47
|
+
flags.debug = true;
|
|
45
48
|
} else if (arg === '--force' || arg === '-f') {
|
|
46
49
|
flags.force = true;
|
|
47
50
|
} else if (arg === '--config') {
|
|
@@ -52,6 +55,8 @@ function parseArgs(argv: string[]): { command: string; args: string[]; flags: Fl
|
|
|
52
55
|
flags.wait = true;
|
|
53
56
|
} else if (arg === '--timeout') {
|
|
54
57
|
flags.timeout = parseTime(argv[++i]);
|
|
58
|
+
} else if (arg === '--lines') {
|
|
59
|
+
flags.lines = parseInt(argv[++i], 10) || 100;
|
|
55
60
|
} else if (arg === '--no-preamble') {
|
|
56
61
|
flags.noPreamble = true;
|
|
57
62
|
} else if (arg.startsWith('--pane=')) {
|
|
@@ -123,6 +128,7 @@ function main(): void {
|
|
|
123
128
|
cmdHelp({ showIntro });
|
|
124
129
|
}
|
|
125
130
|
process.exit(ExitCodes.SUCCESS);
|
|
131
|
+
return;
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
if (command === '--version' || command === '-V') {
|
|
@@ -134,11 +140,18 @@ function main(): void {
|
|
|
134
140
|
if (command === 'completion') {
|
|
135
141
|
cmdCompletion(args[0]);
|
|
136
142
|
process.exit(ExitCodes.SUCCESS);
|
|
143
|
+
return;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// Create context for all other commands
|
|
140
147
|
const ctx = createContext({ argv, flags });
|
|
141
148
|
|
|
149
|
+
// Warn if not in tmux for commands that require it
|
|
150
|
+
const TMUX_REQUIRED_COMMANDS = ['talk', 'send', 'check', 'read', 'setup'];
|
|
151
|
+
if (!process.env.TMUX && TMUX_REQUIRED_COMMANDS.includes(command)) {
|
|
152
|
+
ctx.ui.warn('Not running inside tmux. Some features may not work.');
|
|
153
|
+
}
|
|
154
|
+
|
|
142
155
|
const run = async (): Promise<void> => {
|
|
143
156
|
switch (command) {
|
|
144
157
|
case 'init':
|
|
@@ -215,22 +228,12 @@ function main(): void {
|
|
|
215
228
|
cmdPreamble(ctx, args);
|
|
216
229
|
break;
|
|
217
230
|
|
|
218
|
-
case 'install
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (arg === '--local') {
|
|
225
|
-
scope = 'local';
|
|
226
|
-
} else if (arg === '--user') {
|
|
227
|
-
scope = 'user';
|
|
228
|
-
} else {
|
|
229
|
-
filteredArgs.push(arg);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
cmdInstallSkill(ctx, filteredArgs[0], scope);
|
|
233
|
-
}
|
|
231
|
+
case 'install':
|
|
232
|
+
await cmdInstall(ctx, args[0]);
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
case 'setup':
|
|
236
|
+
await cmdSetup(ctx);
|
|
234
237
|
break;
|
|
235
238
|
|
|
236
239
|
case 'learn':
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import type { Context, Flags, Paths, ResolvedConfig, Tmux, UI } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../exits.js';
|
|
7
|
+
|
|
8
|
+
import { cmdInit } from './init.js';
|
|
9
|
+
import { cmdAdd } from './add.js';
|
|
10
|
+
import { cmdRemove } from './remove.js';
|
|
11
|
+
import { cmdUpdate } from './update.js';
|
|
12
|
+
import { cmdList } from './list.js';
|
|
13
|
+
import { cmdCheck } from './check.js';
|
|
14
|
+
import { cmdPreamble } from './preamble.js';
|
|
15
|
+
import { cmdConfig } from './config.js';
|
|
16
|
+
import { cmdCompletion } from './completion.js';
|
|
17
|
+
import { cmdHelp } from './help.js';
|
|
18
|
+
import { cmdLearn } from './learn.js';
|
|
19
|
+
|
|
20
|
+
function createMockUI(): UI & { jsonCalls: unknown[] } {
|
|
21
|
+
return {
|
|
22
|
+
jsonCalls: [],
|
|
23
|
+
info: vi.fn(),
|
|
24
|
+
success: vi.fn(),
|
|
25
|
+
warn: vi.fn(),
|
|
26
|
+
error: vi.fn(),
|
|
27
|
+
table: vi.fn(),
|
|
28
|
+
json(data: unknown) {
|
|
29
|
+
(this as any).jsonCalls.push(data);
|
|
30
|
+
},
|
|
31
|
+
} as any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createMockTmux(): Tmux {
|
|
35
|
+
return {
|
|
36
|
+
send: vi.fn(),
|
|
37
|
+
capture: vi.fn(() => 'captured'),
|
|
38
|
+
listPanes: vi.fn(() => []),
|
|
39
|
+
getCurrentPaneId: vi.fn(() => null),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createCtx(
|
|
44
|
+
testDir: string,
|
|
45
|
+
overrides?: Partial<{ flags: Partial<Flags>; config: Partial<ResolvedConfig> }>
|
|
46
|
+
): Context {
|
|
47
|
+
const paths: Paths = {
|
|
48
|
+
globalDir: testDir,
|
|
49
|
+
globalConfig: path.join(testDir, 'config.json'),
|
|
50
|
+
localConfig: path.join(testDir, 'tmux-team.json'),
|
|
51
|
+
stateFile: path.join(testDir, 'state.json'),
|
|
52
|
+
};
|
|
53
|
+
const baseConfig: ResolvedConfig = {
|
|
54
|
+
mode: 'polling',
|
|
55
|
+
preambleMode: 'always',
|
|
56
|
+
defaults: { timeout: 180, pollInterval: 1, captureLines: 100, preambleEvery: 3 },
|
|
57
|
+
agents: {},
|
|
58
|
+
paneRegistry: {},
|
|
59
|
+
...overrides?.config,
|
|
60
|
+
};
|
|
61
|
+
const flags: Flags = { json: false, verbose: false, ...(overrides?.flags ?? {}) } as Flags;
|
|
62
|
+
const ui = createMockUI();
|
|
63
|
+
const tmux = createMockTmux();
|
|
64
|
+
return {
|
|
65
|
+
argv: [],
|
|
66
|
+
flags,
|
|
67
|
+
ui,
|
|
68
|
+
config: baseConfig,
|
|
69
|
+
tmux,
|
|
70
|
+
paths,
|
|
71
|
+
exit: ((code: number) => {
|
|
72
|
+
const err = new Error(`exit(${code})`);
|
|
73
|
+
(err as Error & { exitCode: number }).exitCode = code;
|
|
74
|
+
throw err;
|
|
75
|
+
}) as any,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('basic commands', () => {
|
|
80
|
+
let testDir = '';
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-cmd-'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('cmdInit creates tmux-team.json', () => {
|
|
91
|
+
const ctx = createCtx(testDir);
|
|
92
|
+
cmdInit(ctx);
|
|
93
|
+
expect(fs.existsSync(ctx.paths.localConfig)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('cmdInit errors if tmux-team.json exists', () => {
|
|
97
|
+
const ctx = createCtx(testDir);
|
|
98
|
+
fs.writeFileSync(ctx.paths.localConfig, '{}\n');
|
|
99
|
+
expect(() => cmdInit(ctx)).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('cmdAdd creates config if missing and writes new agent', () => {
|
|
103
|
+
const ctx = createCtx(testDir);
|
|
104
|
+
cmdAdd(ctx, 'codex', '1.1', 'review');
|
|
105
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
106
|
+
expect(saved.codex.pane).toBe('1.1');
|
|
107
|
+
expect(saved.codex.remark).toBe('review');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('cmdAdd errors if agent exists', () => {
|
|
111
|
+
const ctx = createCtx(testDir, { config: { paneRegistry: { codex: { pane: '1.1' } } } });
|
|
112
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({ codex: { pane: '1.1' } }, null, 2));
|
|
113
|
+
expect(() => cmdAdd(ctx, 'codex', '1.1')).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('cmdRemove deletes agent', () => {
|
|
117
|
+
const ctx = createCtx(testDir, { config: { paneRegistry: { codex: { pane: '1.1' } } } });
|
|
118
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({ codex: { pane: '1.1' } }, null, 2));
|
|
119
|
+
cmdRemove(ctx, 'codex');
|
|
120
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
121
|
+
expect(saved.codex).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('cmdUpdate updates pane and remark; creates entry if missing', () => {
|
|
125
|
+
const ctx = createCtx(testDir, { config: { paneRegistry: { codex: { pane: '1.1' } } } });
|
|
126
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({}, null, 2));
|
|
127
|
+
cmdUpdate(ctx, 'codex', { pane: '2.2', remark: 'new' });
|
|
128
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
129
|
+
expect(saved.codex.pane).toBe('2.2');
|
|
130
|
+
expect(saved.codex.remark).toBe('new');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('cmdList outputs JSON when --json', () => {
|
|
134
|
+
const ctx = createCtx(testDir, {
|
|
135
|
+
flags: { json: true },
|
|
136
|
+
config: { paneRegistry: { claude: { pane: '1.0', remark: 'main' } } },
|
|
137
|
+
});
|
|
138
|
+
cmdList(ctx);
|
|
139
|
+
expect((ctx.ui as any).jsonCalls.length).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('cmdList prints hint when no agents', () => {
|
|
143
|
+
const ctx = createCtx(testDir);
|
|
144
|
+
cmdList(ctx);
|
|
145
|
+
expect(ctx.ui.info).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('cmdList prints table when agents exist', () => {
|
|
149
|
+
const ctx = createCtx(testDir, {
|
|
150
|
+
config: { paneRegistry: { claude: { pane: '1.0', remark: 'main' } } },
|
|
151
|
+
});
|
|
152
|
+
cmdList(ctx);
|
|
153
|
+
expect(ctx.ui.table).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('cmdCheck captures pane output', () => {
|
|
157
|
+
const ctx = createCtx(testDir, {
|
|
158
|
+
config: { paneRegistry: { claude: { pane: '1.0' } } },
|
|
159
|
+
});
|
|
160
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
161
|
+
cmdCheck(ctx, 'claude', 10);
|
|
162
|
+
expect(ctx.tmux.capture).toHaveBeenCalledWith('1.0', 10);
|
|
163
|
+
expect(logSpy).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('cmdCheck errors when agent missing', () => {
|
|
167
|
+
const ctx = createCtx(testDir);
|
|
168
|
+
expect(() => cmdCheck(ctx, 'nope')).toThrow(`exit(${ExitCodes.PANE_NOT_FOUND})`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('cmdCheck outputs JSON when --json', () => {
|
|
172
|
+
const ctx = createCtx(testDir, {
|
|
173
|
+
flags: { json: true },
|
|
174
|
+
config: { paneRegistry: { claude: { pane: '1.0' } } },
|
|
175
|
+
});
|
|
176
|
+
cmdCheck(ctx, 'claude', 5);
|
|
177
|
+
expect((ctx.ui as any).jsonCalls.length).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('cmdPreamble set/show/clear updates local config', () => {
|
|
181
|
+
const ctx = createCtx(testDir, {
|
|
182
|
+
config: { paneRegistry: { claude: { pane: '1.0' } }, agents: { claude: {} } },
|
|
183
|
+
});
|
|
184
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({ claude: { pane: '1.0' } }, null, 2));
|
|
185
|
+
|
|
186
|
+
cmdPreamble(ctx, ['set', 'claude', 'Be', 'concise']);
|
|
187
|
+
let saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
188
|
+
expect(saved.claude.preamble).toBe('Be concise');
|
|
189
|
+
|
|
190
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
191
|
+
// show will read from ctx.config.agents; update it to reflect loadConfig behavior
|
|
192
|
+
ctx.config.agents.claude = { preamble: 'Be concise' };
|
|
193
|
+
cmdPreamble(ctx, ['show', 'claude']);
|
|
194
|
+
expect(logSpy).toHaveBeenCalled();
|
|
195
|
+
|
|
196
|
+
cmdPreamble(ctx, ['clear', 'claude']);
|
|
197
|
+
saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
198
|
+
expect(saved.claude.preamble).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('cmdPreamble set errors when agent missing', () => {
|
|
202
|
+
const ctx = createCtx(testDir);
|
|
203
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({}, null, 2));
|
|
204
|
+
expect(() => cmdPreamble(ctx, ['set', 'nope', 'x'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('cmdPreamble clear returns not_set when missing', () => {
|
|
208
|
+
const ctx = createCtx(testDir, { flags: { json: true } });
|
|
209
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({ claude: { pane: '1.0' } }, null, 2));
|
|
210
|
+
cmdPreamble(ctx, ['clear', 'claude']);
|
|
211
|
+
const out = (ctx.ui as any).jsonCalls[0] as any;
|
|
212
|
+
expect(out.status).toBe('not_set');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('cmdConfig set/show/clear works for local settings', () => {
|
|
216
|
+
const ctx = createCtx(testDir);
|
|
217
|
+
fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({}, null, 2));
|
|
218
|
+
|
|
219
|
+
cmdConfig(ctx, ['set', 'mode', 'wait']);
|
|
220
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
221
|
+
expect(saved.$config.mode).toBe('wait');
|
|
222
|
+
|
|
223
|
+
cmdConfig(ctx, ['clear', 'mode']);
|
|
224
|
+
const saved2 = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
225
|
+
expect(saved2.$config?.mode).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('cmdConfig set supports --global', () => {
|
|
229
|
+
const ctx = createCtx(testDir);
|
|
230
|
+
cmdConfig(ctx, ['set', 'mode', 'wait', '--global']);
|
|
231
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.globalConfig, 'utf-8'));
|
|
232
|
+
expect(saved.mode).toBe('wait');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('cmdCompletion prints scripts', () => {
|
|
236
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
237
|
+
cmdCompletion('bash');
|
|
238
|
+
expect(logSpy.mock.calls.join('\n')).toContain('complete -F _tmux_team');
|
|
239
|
+
|
|
240
|
+
logSpy.mockClear();
|
|
241
|
+
cmdCompletion('zsh');
|
|
242
|
+
expect(logSpy.mock.calls.join('\n')).toContain('#compdef tmux-team');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('cmdHelp/cmdLearn print output', () => {
|
|
246
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
247
|
+
cmdHelp({ mode: 'polling', showIntro: true });
|
|
248
|
+
cmdHelp({ mode: 'wait', timeout: 10 });
|
|
249
|
+
cmdLearn();
|
|
250
|
+
expect(logSpy).toHaveBeenCalled();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import type { Context, Flags, Paths, ResolvedConfig, Tmux, UI } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../exits.js';
|
|
7
|
+
import { cmdConfig } from './config.js';
|
|
8
|
+
|
|
9
|
+
function createMockUI(): UI & { jsonCalls: unknown[] } {
|
|
10
|
+
return {
|
|
11
|
+
jsonCalls: [],
|
|
12
|
+
info: vi.fn(),
|
|
13
|
+
success: vi.fn(),
|
|
14
|
+
warn: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
table: vi.fn(),
|
|
17
|
+
json(data: unknown) {
|
|
18
|
+
(this as any).jsonCalls.push(data);
|
|
19
|
+
},
|
|
20
|
+
} as any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createCtx(
|
|
24
|
+
testDir: string,
|
|
25
|
+
flags?: Partial<Flags>,
|
|
26
|
+
configOverrides?: Partial<ResolvedConfig>
|
|
27
|
+
): Context {
|
|
28
|
+
const paths: Paths = {
|
|
29
|
+
globalDir: path.join(testDir, 'global'),
|
|
30
|
+
globalConfig: path.join(testDir, 'global', 'config.json'),
|
|
31
|
+
localConfig: path.join(testDir, 'tmux-team.json'),
|
|
32
|
+
stateFile: path.join(testDir, 'global', 'state.json'),
|
|
33
|
+
};
|
|
34
|
+
const config: ResolvedConfig = {
|
|
35
|
+
mode: 'polling',
|
|
36
|
+
preambleMode: 'always',
|
|
37
|
+
defaults: { timeout: 180, pollInterval: 1, captureLines: 100, preambleEvery: 3 },
|
|
38
|
+
agents: {},
|
|
39
|
+
paneRegistry: {},
|
|
40
|
+
...configOverrides,
|
|
41
|
+
};
|
|
42
|
+
const tmux: Tmux = {
|
|
43
|
+
send: vi.fn(),
|
|
44
|
+
capture: vi.fn(),
|
|
45
|
+
listPanes: vi.fn(() => []),
|
|
46
|
+
getCurrentPaneId: vi.fn(() => null),
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
argv: [],
|
|
50
|
+
flags: { json: false, verbose: false, ...(flags ?? {}) } as Flags,
|
|
51
|
+
ui: createMockUI(),
|
|
52
|
+
config,
|
|
53
|
+
tmux,
|
|
54
|
+
paths,
|
|
55
|
+
exit: ((code: number) => {
|
|
56
|
+
const err = new Error(`exit(${code})`);
|
|
57
|
+
(err as Error & { exitCode: number }).exitCode = code;
|
|
58
|
+
throw err;
|
|
59
|
+
}) as any,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('cmdConfig', () => {
|
|
64
|
+
let testDir = '';
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-configcmd-'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('shows config as JSON when --json', () => {
|
|
75
|
+
const ctx = createCtx(testDir, { json: true });
|
|
76
|
+
cmdConfig(ctx, ['show']);
|
|
77
|
+
expect((ctx.ui as any).jsonCalls.length).toBe(1);
|
|
78
|
+
const out = (ctx.ui as any).jsonCalls[0] as any;
|
|
79
|
+
expect(out.resolved).toBeTruthy();
|
|
80
|
+
expect(out.sources).toBeTruthy();
|
|
81
|
+
expect(out.paths).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('shows config as table in human mode', () => {
|
|
85
|
+
const ctx = createCtx(testDir);
|
|
86
|
+
cmdConfig(ctx, ['show']);
|
|
87
|
+
expect(ctx.ui.table).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects invalid keys and values', () => {
|
|
91
|
+
const ctx = createCtx(testDir);
|
|
92
|
+
expect(() => cmdConfig(ctx, ['set', 'nope', 'x'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
93
|
+
expect(() => cmdConfig(ctx, ['set', 'mode', 'nope'])).toThrow(`exit(${ExitCodes.ERROR})`);
|
|
94
|
+
expect(() => cmdConfig(ctx, ['set', 'preambleEvery', '-1'])).toThrow(
|
|
95
|
+
`exit(${ExitCodes.ERROR})`
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('sets and clears local settings', () => {
|
|
100
|
+
const ctx = createCtx(testDir);
|
|
101
|
+
cmdConfig(ctx, ['set', 'preambleMode', 'disabled']);
|
|
102
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
103
|
+
expect(saved.$config.preambleMode).toBe('disabled');
|
|
104
|
+
|
|
105
|
+
cmdConfig(ctx, ['clear']);
|
|
106
|
+
const saved2 = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
107
|
+
expect(saved2.$config).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('sets global settings with -g', () => {
|
|
111
|
+
const ctx = createCtx(testDir);
|
|
112
|
+
cmdConfig(ctx, ['set', 'preambleEvery', '5', '-g']);
|
|
113
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.globalConfig, 'utf-8'));
|
|
114
|
+
expect(saved.defaults.preambleEvery).toBe(5);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/src/commands/help.ts
CHANGED
|
@@ -51,10 +51,11 @@ ${colors.yellow('COMMANDS')}
|
|
|
51
51
|
${colors.green('add')} <name> <pane> [remark] Add a new agent
|
|
52
52
|
${colors.green('update')} <name> [options] Update an agent's config
|
|
53
53
|
${colors.green('remove')} <name> Remove an agent
|
|
54
|
+
${colors.green('install')} [claude|codex] Install tmux-team for an AI agent
|
|
55
|
+
${colors.green('setup')} Interactive wizard to configure agents
|
|
54
56
|
${colors.green('init')} Create empty tmux-team.json
|
|
55
57
|
${colors.green('config')} [show|set|clear] View/modify settings
|
|
56
58
|
${colors.green('preamble')} [show|set|clear] Manage agent preambles
|
|
57
|
-
${colors.green('install-skill')} <agent> Install skill for AI agent
|
|
58
59
|
${colors.green('completion')} Output shell completion script
|
|
59
60
|
${colors.green('learn')} Show educational guide
|
|
60
61
|
${colors.green('help')} Show this help message
|
|
@@ -68,7 +69,9 @@ ${colors.yellow('TALK OPTIONS')}
|
|
|
68
69
|
${colors.green('--delay')} <seconds> Wait before sending
|
|
69
70
|
${colors.green('--wait')} Force wait mode (block until response)
|
|
70
71
|
${colors.green('--timeout')} <seconds> Max wait time (current: ${timeout}s)
|
|
72
|
+
${colors.green('--lines')} <number> Lines to capture (default: 100)
|
|
71
73
|
${colors.green('--no-preamble')} Skip agent preamble for this message
|
|
74
|
+
${colors.green('--debug')} Show debug output
|
|
72
75
|
|
|
73
76
|
${colors.yellow('EXAMPLES')}${
|
|
74
77
|
isWaitMode
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import type { Context, Flags, Paths, ResolvedConfig, Tmux, UI } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../exits.js';
|
|
7
|
+
|
|
8
|
+
function createMockUI(): UI {
|
|
9
|
+
return {
|
|
10
|
+
info: vi.fn(),
|
|
11
|
+
success: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
error: vi.fn(),
|
|
14
|
+
table: vi.fn(),
|
|
15
|
+
json: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createCtx(testDir: string, overrides?: Partial<{ flags: Partial<Flags> }>): Context {
|
|
20
|
+
const paths: Paths = {
|
|
21
|
+
globalDir: testDir,
|
|
22
|
+
globalConfig: path.join(testDir, 'config.json'),
|
|
23
|
+
localConfig: path.join(testDir, 'tmux-team.json'),
|
|
24
|
+
stateFile: path.join(testDir, 'state.json'),
|
|
25
|
+
};
|
|
26
|
+
const config: ResolvedConfig = {
|
|
27
|
+
mode: 'polling',
|
|
28
|
+
preambleMode: 'always',
|
|
29
|
+
defaults: { timeout: 180, pollInterval: 1, captureLines: 100, preambleEvery: 3 },
|
|
30
|
+
agents: {},
|
|
31
|
+
paneRegistry: {},
|
|
32
|
+
};
|
|
33
|
+
const flags: Flags = { json: false, verbose: false, ...(overrides?.flags ?? {}) } as Flags;
|
|
34
|
+
const tmux: Tmux = {
|
|
35
|
+
send: vi.fn(),
|
|
36
|
+
capture: vi.fn(),
|
|
37
|
+
listPanes: vi.fn(() => []),
|
|
38
|
+
getCurrentPaneId: vi.fn(() => null),
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
argv: [],
|
|
42
|
+
flags,
|
|
43
|
+
ui: createMockUI(),
|
|
44
|
+
config,
|
|
45
|
+
tmux,
|
|
46
|
+
paths,
|
|
47
|
+
exit: ((code: number) => {
|
|
48
|
+
const err = new Error(`exit(${code})`);
|
|
49
|
+
(err as Error & { exitCode: number }).exitCode = code;
|
|
50
|
+
throw err;
|
|
51
|
+
}) as any,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('cmdInstall', () => {
|
|
56
|
+
let testDir = '';
|
|
57
|
+
let homeDir = '';
|
|
58
|
+
const originalHome = process.env.HOME;
|
|
59
|
+
const originalTmux = process.env.TMUX;
|
|
60
|
+
const originalCodexHome = process.env.CODEX_HOME;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-install-'));
|
|
64
|
+
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-home-'));
|
|
65
|
+
process.env.HOME = homeDir;
|
|
66
|
+
process.env.CODEX_HOME = path.join(homeDir, '.codex');
|
|
67
|
+
delete process.env.TMUX;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
process.env.HOME = originalHome;
|
|
72
|
+
process.env.TMUX = originalTmux;
|
|
73
|
+
process.env.CODEX_HOME = originalCodexHome;
|
|
74
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
75
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
76
|
+
vi.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('installs claude skill when agent is provided', async () => {
|
|
80
|
+
vi.resetModules();
|
|
81
|
+
vi.doMock('node:os', () => ({
|
|
82
|
+
default: { homedir: () => homeDir },
|
|
83
|
+
homedir: () => homeDir,
|
|
84
|
+
}));
|
|
85
|
+
vi.doMock('node:readline', () => ({
|
|
86
|
+
createInterface: () => ({
|
|
87
|
+
question: (_q: string, cb: (a: string) => void) => cb(''),
|
|
88
|
+
close: () => {},
|
|
89
|
+
}),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
const { cmdInstall } = await import('./install.js');
|
|
93
|
+
const ctx = createCtx(testDir, { flags: { force: true } });
|
|
94
|
+
await cmdInstall(ctx, 'claude');
|
|
95
|
+
|
|
96
|
+
const installed = path.join(homeDir, '.claude', 'commands', 'team.md');
|
|
97
|
+
expect(fs.existsSync(installed)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('errors on unknown agent', async () => {
|
|
101
|
+
vi.resetModules();
|
|
102
|
+
vi.doMock('node:os', () => ({
|
|
103
|
+
default: { homedir: () => homeDir },
|
|
104
|
+
homedir: () => homeDir,
|
|
105
|
+
}));
|
|
106
|
+
vi.doMock('node:readline', () => ({
|
|
107
|
+
createInterface: () => ({
|
|
108
|
+
question: (_q: string, cb: (a: string) => void) => cb(''),
|
|
109
|
+
close: () => {},
|
|
110
|
+
}),
|
|
111
|
+
}));
|
|
112
|
+
const { cmdInstall } = await import('./install.js');
|
|
113
|
+
const ctx = createCtx(testDir);
|
|
114
|
+
await expect(cmdInstall(ctx, 'nope')).rejects.toThrow(`exit(${ExitCodes.ERROR})`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('prompts when environment is not detected', async () => {
|
|
118
|
+
vi.resetModules();
|
|
119
|
+
const answers = ['codex'];
|
|
120
|
+
vi.doMock('node:os', () => ({
|
|
121
|
+
default: { homedir: () => homeDir },
|
|
122
|
+
homedir: () => homeDir,
|
|
123
|
+
}));
|
|
124
|
+
vi.doMock('node:readline', () => ({
|
|
125
|
+
createInterface: () => ({
|
|
126
|
+
question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
|
|
127
|
+
close: () => {},
|
|
128
|
+
}),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const { cmdInstall } = await import('./install.js');
|
|
132
|
+
const ctx = createCtx(testDir, { flags: { force: true } });
|
|
133
|
+
await cmdInstall(ctx);
|
|
134
|
+
|
|
135
|
+
const installed = path.join(homeDir, '.codex', 'skills', 'tmux-team', 'SKILL.md');
|
|
136
|
+
expect(fs.existsSync(installed)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('auto-selects detected environment when exactly one is found', async () => {
|
|
140
|
+
vi.resetModules();
|
|
141
|
+
fs.mkdirSync(path.join(homeDir, '.claude'), { recursive: true });
|
|
142
|
+
vi.doMock('node:os', () => ({
|
|
143
|
+
default: { homedir: () => homeDir },
|
|
144
|
+
homedir: () => homeDir,
|
|
145
|
+
}));
|
|
146
|
+
vi.doMock('node:readline', () => ({
|
|
147
|
+
createInterface: () => ({
|
|
148
|
+
question: (_q: string, cb: (a: string) => void) => cb(''),
|
|
149
|
+
close: () => {},
|
|
150
|
+
}),
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
const { cmdInstall } = await import('./install.js');
|
|
154
|
+
const ctx = createCtx(testDir, { flags: { force: true } });
|
|
155
|
+
await cmdInstall(ctx);
|
|
156
|
+
expect(fs.existsSync(path.join(homeDir, '.claude', 'commands', 'team.md'))).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('prompts when multiple environments are detected', async () => {
|
|
160
|
+
vi.resetModules();
|
|
161
|
+
fs.mkdirSync(path.join(homeDir, '.claude'), { recursive: true });
|
|
162
|
+
fs.mkdirSync(path.join(homeDir, '.codex'), { recursive: true });
|
|
163
|
+
|
|
164
|
+
vi.doMock('node:os', () => ({
|
|
165
|
+
default: { homedir: () => homeDir },
|
|
166
|
+
homedir: () => homeDir,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const answers = ['claude'];
|
|
170
|
+
vi.doMock('node:readline', () => ({
|
|
171
|
+
createInterface: () => ({
|
|
172
|
+
question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
|
|
173
|
+
close: () => {},
|
|
174
|
+
}),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const { cmdInstall } = await import('./install.js');
|
|
178
|
+
const ctx = createCtx(testDir, { flags: { force: true } });
|
|
179
|
+
await cmdInstall(ctx);
|
|
180
|
+
expect(fs.existsSync(path.join(homeDir, '.claude', 'commands', 'team.md'))).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('fails if skill exists and --force is not set', async () => {
|
|
184
|
+
vi.resetModules();
|
|
185
|
+
vi.doMock('node:os', () => ({
|
|
186
|
+
default: { homedir: () => homeDir },
|
|
187
|
+
homedir: () => homeDir,
|
|
188
|
+
}));
|
|
189
|
+
vi.doMock('node:readline', () => ({
|
|
190
|
+
createInterface: () => ({
|
|
191
|
+
question: (_q: string, cb: (a: string) => void) => cb(''),
|
|
192
|
+
close: () => {},
|
|
193
|
+
}),
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
const target = path.join(homeDir, '.claude', 'commands', 'team.md');
|
|
197
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
198
|
+
fs.writeFileSync(target, 'existing');
|
|
199
|
+
|
|
200
|
+
const { cmdInstall } = await import('./install.js');
|
|
201
|
+
const ctx = createCtx(testDir);
|
|
202
|
+
await expect(cmdInstall(ctx, 'claude')).rejects.toThrow(`exit(${ExitCodes.ERROR})`);
|
|
203
|
+
expect(ctx.ui.warn).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
});
|