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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// install command - install tmux-team for AI agents
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
import * as readline from 'node:readline';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import type { Context } from '../types.js';
|
|
11
|
+
import { ExitCodes } from '../exits.js';
|
|
12
|
+
import { colors } from '../ui.js';
|
|
13
|
+
import { cmdSetup } from './setup.js';
|
|
14
|
+
|
|
15
|
+
type AgentType = 'claude' | 'codex';
|
|
16
|
+
|
|
17
|
+
interface SkillConfig {
|
|
18
|
+
sourceFile: string;
|
|
19
|
+
userDir: string;
|
|
20
|
+
targetFile: string;
|
|
21
|
+
detected: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getCodexHome(): string {
|
|
25
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SKILL_CONFIGS: Record<AgentType, SkillConfig> = {
|
|
29
|
+
claude: {
|
|
30
|
+
sourceFile: 'skills/claude/team.md',
|
|
31
|
+
userDir: path.join(os.homedir(), '.claude', 'commands'),
|
|
32
|
+
targetFile: 'team.md',
|
|
33
|
+
detected: () => fs.existsSync(path.join(os.homedir(), '.claude')),
|
|
34
|
+
},
|
|
35
|
+
codex: {
|
|
36
|
+
sourceFile: 'skills/codex/SKILL.md',
|
|
37
|
+
userDir: path.join(getCodexHome(), 'skills', 'tmux-team'),
|
|
38
|
+
targetFile: 'SKILL.md',
|
|
39
|
+
detected: () => fs.existsSync(getCodexHome()),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const SUPPORTED_AGENTS = Object.keys(SKILL_CONFIGS) as AgentType[];
|
|
44
|
+
|
|
45
|
+
function findPackageRoot(): string {
|
|
46
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
47
|
+
let dir = path.dirname(currentFile);
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < 5; i++) {
|
|
50
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
51
|
+
if (fs.existsSync(pkgPath)) {
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
const parent = path.dirname(dir);
|
|
55
|
+
if (parent === dir) break;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
return path.resolve(path.dirname(currentFile), '..', '..');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function detectEnvironment(): AgentType[] {
|
|
62
|
+
const detected: AgentType[] = [];
|
|
63
|
+
for (const [agent, config] of Object.entries(SKILL_CONFIGS)) {
|
|
64
|
+
if (config.detected()) {
|
|
65
|
+
detected.push(agent as AgentType);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return detected;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function prompt(rl: readline.Interface, question: string): Promise<string> {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
rl.question(question, (answer) => {
|
|
74
|
+
resolve(answer.trim());
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function confirm(rl: readline.Interface, question: string): Promise<boolean> {
|
|
80
|
+
const answer = await prompt(rl, `${question} [Y/n]: `);
|
|
81
|
+
return answer.toLowerCase() !== 'n';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function installSkill(ctx: Context, agent: AgentType): boolean {
|
|
85
|
+
const config = SKILL_CONFIGS[agent];
|
|
86
|
+
const pkgRoot = findPackageRoot();
|
|
87
|
+
const sourcePath = path.join(pkgRoot, config.sourceFile);
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(sourcePath)) {
|
|
90
|
+
ctx.ui.error(`Skill file not found: ${sourcePath}`);
|
|
91
|
+
ctx.ui.info('Make sure tmux-team is properly installed.');
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const targetPath = path.join(config.userDir, config.targetFile);
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(targetPath) && !ctx.flags.force) {
|
|
98
|
+
ctx.ui.warn(`Skill already exists: ${targetPath}`);
|
|
99
|
+
ctx.ui.info('Use --force to overwrite.');
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(config.userDir)) {
|
|
104
|
+
fs.mkdirSync(config.userDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function cmdInstall(ctx: Context, agent?: string): Promise<void> {
|
|
112
|
+
const { ui, flags, exit } = ctx;
|
|
113
|
+
|
|
114
|
+
const rl = readline.createInterface({
|
|
115
|
+
input: process.stdin,
|
|
116
|
+
output: process.stdout,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
let selectedAgent: AgentType;
|
|
121
|
+
|
|
122
|
+
if (agent) {
|
|
123
|
+
// Direct agent specification
|
|
124
|
+
const agentLower = agent.toLowerCase() as AgentType;
|
|
125
|
+
if (!SUPPORTED_AGENTS.includes(agentLower)) {
|
|
126
|
+
ui.error(`Unknown agent: ${agent}`);
|
|
127
|
+
ui.info(`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`);
|
|
128
|
+
exit(ExitCodes.ERROR);
|
|
129
|
+
}
|
|
130
|
+
selectedAgent = agentLower;
|
|
131
|
+
} else {
|
|
132
|
+
// Auto-detect and prompt
|
|
133
|
+
const detected = detectEnvironment();
|
|
134
|
+
console.log();
|
|
135
|
+
|
|
136
|
+
if (detected.length === 0) {
|
|
137
|
+
ui.info('No AI agent environments detected.');
|
|
138
|
+
ui.info(`Supported agents: ${SUPPORTED_AGENTS.join(', ')}`);
|
|
139
|
+
console.log();
|
|
140
|
+
const choice = await prompt(rl, 'Which agent are you using? ');
|
|
141
|
+
const choiceLower = choice.toLowerCase() as AgentType;
|
|
142
|
+
if (!SUPPORTED_AGENTS.includes(choiceLower)) {
|
|
143
|
+
ui.error(`Unknown agent: ${choice}`);
|
|
144
|
+
exit(ExitCodes.ERROR);
|
|
145
|
+
}
|
|
146
|
+
selectedAgent = choiceLower;
|
|
147
|
+
} else if (detected.length === 1) {
|
|
148
|
+
ui.success(`Detected: ${detected[0]}`);
|
|
149
|
+
selectedAgent = detected[0];
|
|
150
|
+
} else {
|
|
151
|
+
ui.info(`Detected multiple environments: ${detected.join(', ')}`);
|
|
152
|
+
const choice = await prompt(rl, 'Which agent are you installing for? ');
|
|
153
|
+
const choiceLower = choice.toLowerCase() as AgentType;
|
|
154
|
+
if (!SUPPORTED_AGENTS.includes(choiceLower)) {
|
|
155
|
+
ui.error(`Unknown agent: ${choice}`);
|
|
156
|
+
exit(ExitCodes.ERROR);
|
|
157
|
+
}
|
|
158
|
+
selectedAgent = choiceLower;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Install the skill
|
|
163
|
+
console.log();
|
|
164
|
+
const success = installSkill(ctx, selectedAgent);
|
|
165
|
+
if (!success) {
|
|
166
|
+
exit(ExitCodes.ERROR);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const config = SKILL_CONFIGS[selectedAgent];
|
|
170
|
+
const targetPath = path.join(config.userDir, config.targetFile);
|
|
171
|
+
ui.success(`Installed ${selectedAgent} skill to ${targetPath}`);
|
|
172
|
+
|
|
173
|
+
// Agent-specific instructions
|
|
174
|
+
console.log();
|
|
175
|
+
if (selectedAgent === 'claude') {
|
|
176
|
+
console.log(colors.yellow('For full plugin features, run these in Claude Code:'));
|
|
177
|
+
console.log(` ${colors.cyan('/plugin marketplace add wkh237/tmux-team')}`);
|
|
178
|
+
console.log(` ${colors.cyan('/plugin install tmux-team')}`);
|
|
179
|
+
console.log();
|
|
180
|
+
} else if (selectedAgent === 'codex') {
|
|
181
|
+
console.log(colors.yellow('Enable skills in Codex:'));
|
|
182
|
+
console.log(` ${colors.cyan('codex --enable skills')}`);
|
|
183
|
+
console.log();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Offer to run setup
|
|
187
|
+
if (process.env.TMUX) {
|
|
188
|
+
const runSetup = await confirm(rl, 'Run setup wizard now?');
|
|
189
|
+
if (runSetup) {
|
|
190
|
+
rl.close();
|
|
191
|
+
await cmdSetup(ctx);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
ui.info('Run tmux-team setup inside tmux to configure your agents.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(colors.yellow('Next steps:'));
|
|
200
|
+
console.log(` 1. Start tmux and open panes for your AI agents`);
|
|
201
|
+
console.log(` 2. Run ${colors.cyan('tmux-team setup')} to configure agents`);
|
|
202
|
+
console.log(` 3. Use ${colors.cyan('tmux-team talk <agent> "message" --wait')}`);
|
|
203
|
+
console.log();
|
|
204
|
+
} finally {
|
|
205
|
+
rl.close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
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, tmux: Tmux, 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
|
+
return {
|
|
34
|
+
argv: [],
|
|
35
|
+
flags: { json: false, verbose: false, ...(flags ?? {}) } as Flags,
|
|
36
|
+
ui: createMockUI(),
|
|
37
|
+
config,
|
|
38
|
+
tmux,
|
|
39
|
+
paths,
|
|
40
|
+
exit: ((code: number) => {
|
|
41
|
+
const err = new Error(`exit(${code})`);
|
|
42
|
+
(err as Error & { exitCode: number }).exitCode = code;
|
|
43
|
+
throw err;
|
|
44
|
+
}) as any,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('cmdSetup', () => {
|
|
49
|
+
let testDir = '';
|
|
50
|
+
const originalTmux = process.env.TMUX;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmux-team-setup-'));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
process.env.TMUX = originalTmux;
|
|
58
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('errors when not in tmux', async () => {
|
|
63
|
+
vi.resetModules();
|
|
64
|
+
delete process.env.TMUX;
|
|
65
|
+
const { cmdSetup } = await import('./setup.js');
|
|
66
|
+
const ctx = createCtx(testDir, {
|
|
67
|
+
send: vi.fn(),
|
|
68
|
+
capture: vi.fn(),
|
|
69
|
+
listPanes: vi.fn(() => []),
|
|
70
|
+
getCurrentPaneId: vi.fn(() => null),
|
|
71
|
+
});
|
|
72
|
+
await expect(cmdSetup(ctx)).rejects.toThrow(`exit(${ExitCodes.ERROR})`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('creates tmux-team.json by configuring panes', async () => {
|
|
76
|
+
vi.resetModules();
|
|
77
|
+
process.env.TMUX = '1';
|
|
78
|
+
|
|
79
|
+
const answers = [
|
|
80
|
+
'', // accept default "codex" for pane %1
|
|
81
|
+
'reviewer', // remark
|
|
82
|
+
'1bad', // invalid name for pane %2 -> skipped
|
|
83
|
+
'', // remark (unused)
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
vi.doMock('readline', () => ({
|
|
87
|
+
default: {
|
|
88
|
+
createInterface: () => ({
|
|
89
|
+
question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
|
|
90
|
+
close: () => {},
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
createInterface: () => ({
|
|
94
|
+
question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
|
|
95
|
+
close: () => {},
|
|
96
|
+
}),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
const { cmdSetup } = await import('./setup.js');
|
|
100
|
+
const ctx = createCtx(testDir, {
|
|
101
|
+
send: vi.fn(),
|
|
102
|
+
capture: vi.fn(),
|
|
103
|
+
getCurrentPaneId: vi.fn(() => '%0'),
|
|
104
|
+
listPanes: vi.fn(() => [
|
|
105
|
+
{ id: '%0', command: 'zsh', suggestedName: null },
|
|
106
|
+
{ id: '%1', command: 'codex', suggestedName: 'codex' },
|
|
107
|
+
{ id: '%2', command: 'zsh', suggestedName: null },
|
|
108
|
+
]),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await cmdSetup(ctx);
|
|
112
|
+
const saved = JSON.parse(fs.readFileSync(ctx.paths.localConfig, 'utf-8'));
|
|
113
|
+
expect(saved.codex.pane).toBe('%1');
|
|
114
|
+
expect(saved.codex.remark).toBe('reviewer');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('errors when no panes found', async () => {
|
|
118
|
+
vi.resetModules();
|
|
119
|
+
process.env.TMUX = '1';
|
|
120
|
+
vi.doMock('readline', () => ({
|
|
121
|
+
default: {
|
|
122
|
+
createInterface: () => ({
|
|
123
|
+
question: (_q: string, cb: (a: string) => void) => cb(''),
|
|
124
|
+
close: () => {},
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
createInterface: () => ({
|
|
128
|
+
question: (_q: string, cb: (a: string) => void) => cb(''),
|
|
129
|
+
close: () => {},
|
|
130
|
+
}),
|
|
131
|
+
}));
|
|
132
|
+
const { cmdSetup } = await import('./setup.js');
|
|
133
|
+
const ctx = createCtx(testDir, {
|
|
134
|
+
send: vi.fn(),
|
|
135
|
+
capture: vi.fn(),
|
|
136
|
+
getCurrentPaneId: vi.fn(() => '%0'),
|
|
137
|
+
listPanes: vi.fn(() => []),
|
|
138
|
+
});
|
|
139
|
+
await expect(cmdSetup(ctx)).rejects.toThrow(`exit(${ExitCodes.ERROR})`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('exits success when user skips all panes', async () => {
|
|
143
|
+
vi.resetModules();
|
|
144
|
+
process.env.TMUX = '1';
|
|
145
|
+
|
|
146
|
+
const answers = [
|
|
147
|
+
'', // pane %1 has no suggested name -> press Enter to skip
|
|
148
|
+
];
|
|
149
|
+
vi.doMock('readline', () => ({
|
|
150
|
+
default: {
|
|
151
|
+
createInterface: () => ({
|
|
152
|
+
question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
|
|
153
|
+
close: () => {},
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
createInterface: () => ({
|
|
157
|
+
question: (_q: string, cb: (a: string) => void) => cb(answers.shift() ?? ''),
|
|
158
|
+
close: () => {},
|
|
159
|
+
}),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const { cmdSetup } = await import('./setup.js');
|
|
163
|
+
const ctx = createCtx(testDir, {
|
|
164
|
+
send: vi.fn(),
|
|
165
|
+
capture: vi.fn(),
|
|
166
|
+
getCurrentPaneId: vi.fn(() => '%0'),
|
|
167
|
+
listPanes: vi.fn(() => [
|
|
168
|
+
{ id: '%0', command: 'zsh', suggestedName: null },
|
|
169
|
+
{ id: '%1', command: 'zsh', suggestedName: null },
|
|
170
|
+
]),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(cmdSetup(ctx)).rejects.toThrow(`exit(${ExitCodes.SUCCESS})`);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// setup command - interactive wizard for configuring agents
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import type { Context, PaneEntry, PaneInfo, LocalConfigFile } from '../types.js';
|
|
8
|
+
import { ExitCodes } from '../exits.js';
|
|
9
|
+
import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
|
|
10
|
+
import { colors } from '../ui.js';
|
|
11
|
+
|
|
12
|
+
async function prompt(rl: readline.Interface, question: string): Promise<string> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
resolve(answer.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function promptWithDefault(
|
|
21
|
+
rl: readline.Interface,
|
|
22
|
+
question: string,
|
|
23
|
+
defaultValue: string
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const answer = await prompt(rl, `${question} [${defaultValue}]: `);
|
|
26
|
+
return answer || defaultValue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function confirm(rl: readline.Interface, question: string): Promise<boolean> {
|
|
30
|
+
const answer = await prompt(rl, `${question} [Y/n]: `);
|
|
31
|
+
return answer.toLowerCase() !== 'n';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function cmdSetup(ctx: Context): Promise<void> {
|
|
35
|
+
const { ui, tmux, paths, exit } = ctx;
|
|
36
|
+
|
|
37
|
+
// Check if in tmux
|
|
38
|
+
if (!process.env.TMUX) {
|
|
39
|
+
ui.error('Not running inside tmux. Please run this command from within a tmux session.');
|
|
40
|
+
exit(ExitCodes.ERROR);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
console.log();
|
|
50
|
+
ui.info('Detecting tmux panes...');
|
|
51
|
+
console.log();
|
|
52
|
+
|
|
53
|
+
const panes = tmux.listPanes();
|
|
54
|
+
const currentPaneId = tmux.getCurrentPaneId();
|
|
55
|
+
|
|
56
|
+
if (panes.length === 0) {
|
|
57
|
+
ui.error('No tmux panes found.');
|
|
58
|
+
exit(ExitCodes.ERROR);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Filter out current pane
|
|
62
|
+
const otherPanes = panes.filter((p) => p.id !== currentPaneId);
|
|
63
|
+
|
|
64
|
+
if (otherPanes.length === 0) {
|
|
65
|
+
ui.warn('No other panes found. Create more tmux panes with other agents first.');
|
|
66
|
+
ui.info('Hint: Use Ctrl+B % or Ctrl+B " to split panes, then start your AI agents.');
|
|
67
|
+
exit(ExitCodes.ERROR);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Show detected panes
|
|
71
|
+
console.log(colors.yellow('Found panes:'));
|
|
72
|
+
console.log(` ${colors.dim(currentPaneId || '?')} ${colors.dim('(current pane - skipped)')}`);
|
|
73
|
+
for (const pane of otherPanes) {
|
|
74
|
+
const detected = pane.suggestedName
|
|
75
|
+
? colors.green(pane.suggestedName)
|
|
76
|
+
: colors.dim('(unknown)');
|
|
77
|
+
console.log(` ${pane.id} running "${pane.command}" → detected: ${detected}`);
|
|
78
|
+
}
|
|
79
|
+
console.log();
|
|
80
|
+
|
|
81
|
+
// Load existing config
|
|
82
|
+
let localConfig: LocalConfigFile = {};
|
|
83
|
+
if (fs.existsSync(paths.localConfig)) {
|
|
84
|
+
localConfig = loadLocalConfigFile(paths);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const agents: Record<string, PaneEntry> = {};
|
|
88
|
+
let configuredCount = 0;
|
|
89
|
+
|
|
90
|
+
// Configure each pane
|
|
91
|
+
for (const pane of otherPanes) {
|
|
92
|
+
const detected = pane.suggestedName || '';
|
|
93
|
+
|
|
94
|
+
console.log(colors.cyan(`Configure pane ${pane.id}?`));
|
|
95
|
+
if (pane.suggestedName) {
|
|
96
|
+
console.log(` Detected: ${colors.green(pane.suggestedName)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ask for name
|
|
100
|
+
let name: string;
|
|
101
|
+
if (detected) {
|
|
102
|
+
name = await promptWithDefault(rl, ' Name', detected);
|
|
103
|
+
} else {
|
|
104
|
+
name = await prompt(rl, ' Name (or press Enter to skip): ');
|
|
105
|
+
if (!name) {
|
|
106
|
+
console.log(colors.dim(' Skipped'));
|
|
107
|
+
console.log();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate name
|
|
113
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
|
|
114
|
+
ui.warn(
|
|
115
|
+
' Invalid name. Use letters, numbers, underscores, hyphens. Starting with a letter.'
|
|
116
|
+
);
|
|
117
|
+
console.log(colors.dim(' Skipped'));
|
|
118
|
+
console.log();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Ask for optional remark
|
|
123
|
+
const remark = await prompt(rl, ' Remark (optional): ');
|
|
124
|
+
|
|
125
|
+
const entry: PaneEntry = { pane: pane.id };
|
|
126
|
+
if (remark) {
|
|
127
|
+
entry.remark = remark;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
agents[name] = entry;
|
|
131
|
+
configuredCount++;
|
|
132
|
+
console.log();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (configuredCount === 0) {
|
|
136
|
+
ui.warn('No agents configured.');
|
|
137
|
+
exit(ExitCodes.SUCCESS);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Merge with existing config (preserve $config and existing agents)
|
|
141
|
+
const newConfig: LocalConfigFile = { ...localConfig };
|
|
142
|
+
for (const [name, entry] of Object.entries(agents)) {
|
|
143
|
+
newConfig[name] = entry;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Save config
|
|
147
|
+
if (!fs.existsSync(paths.localConfig)) {
|
|
148
|
+
fs.writeFileSync(paths.localConfig, '{}\n');
|
|
149
|
+
}
|
|
150
|
+
saveLocalConfigFile(paths, newConfig);
|
|
151
|
+
|
|
152
|
+
ui.success(`Created ${paths.localConfig} with ${configuredCount} agent(s)`);
|
|
153
|
+
console.log();
|
|
154
|
+
|
|
155
|
+
// Show next steps
|
|
156
|
+
const firstAgent = Object.keys(agents)[0];
|
|
157
|
+
console.log(colors.yellow('Try it:'));
|
|
158
|
+
console.log(` ${colors.cyan(`tmux-team talk ${firstAgent} "hello" --wait`)}`);
|
|
159
|
+
console.log();
|
|
160
|
+
} finally {
|
|
161
|
+
rl.close();
|
|
162
|
+
}
|
|
163
|
+
}
|