tmux-team 1.1.0 → 2.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -22
- package/bin/tmux-team +31 -430
- package/package.json +28 -6
- package/src/cli.ts +222 -0
- package/src/commands/add.ts +38 -0
- package/src/commands/check.ts +34 -0
- package/src/commands/completion.ts +118 -0
- package/src/commands/config.ts +187 -0
- package/src/commands/help.ts +66 -0
- package/src/commands/init.ts +24 -0
- package/src/commands/list.ts +27 -0
- package/src/commands/preamble.ts +153 -0
- package/src/commands/remove.ts +25 -0
- package/src/commands/talk.test.ts +679 -0
- package/src/commands/talk.ts +274 -0
- package/src/commands/update.ts +47 -0
- package/src/config.test.ts +246 -0
- package/src/config.ts +223 -0
- package/src/context.ts +38 -0
- package/src/exits.ts +14 -0
- package/src/pm/commands.test.ts +1127 -0
- package/src/pm/commands.ts +723 -0
- package/src/pm/manager.test.ts +377 -0
- package/src/pm/manager.ts +146 -0
- package/src/pm/permissions.test.ts +332 -0
- package/src/pm/permissions.ts +278 -0
- package/src/pm/storage/adapter.ts +55 -0
- package/src/pm/storage/fs.test.ts +384 -0
- package/src/pm/storage/fs.ts +256 -0
- package/src/pm/storage/github.ts +763 -0
- package/src/pm/types.ts +85 -0
- package/src/state.test.ts +311 -0
- package/src/state.ts +83 -0
- package/src/tmux.test.ts +205 -0
- package/src/tmux.ts +27 -0
- package/src/types.ts +97 -0
- package/src/ui.ts +76 -0
- package/src/version.ts +21 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// tmux-team CLI entry point
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { createContext, ExitCodes } from './context.js';
|
|
7
|
+
import type { Flags } from './types.js';
|
|
8
|
+
|
|
9
|
+
// Commands
|
|
10
|
+
import { cmdHelp } from './commands/help.js';
|
|
11
|
+
import { cmdInit } from './commands/init.js';
|
|
12
|
+
import { cmdList } from './commands/list.js';
|
|
13
|
+
import { cmdAdd } from './commands/add.js';
|
|
14
|
+
import { cmdUpdate } from './commands/update.js';
|
|
15
|
+
import { cmdRemove } from './commands/remove.js';
|
|
16
|
+
import { cmdTalk } from './commands/talk.js';
|
|
17
|
+
import { cmdCheck } from './commands/check.js';
|
|
18
|
+
import { cmdCompletion } from './commands/completion.js';
|
|
19
|
+
import { cmdPm } from './pm/commands.js';
|
|
20
|
+
import { cmdConfig } from './commands/config.js';
|
|
21
|
+
import { cmdPreamble } from './commands/preamble.js';
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────
|
|
24
|
+
// Argument parsing
|
|
25
|
+
// ─────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv: string[]): { command: string; args: string[]; flags: Flags } {
|
|
28
|
+
const flags: Flags = {
|
|
29
|
+
json: false,
|
|
30
|
+
verbose: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const positional: string[] = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
|
|
36
|
+
while (i < argv.length) {
|
|
37
|
+
const arg = argv[i];
|
|
38
|
+
|
|
39
|
+
if (arg === '--json') {
|
|
40
|
+
flags.json = true;
|
|
41
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
42
|
+
flags.verbose = true;
|
|
43
|
+
} else if (arg === '--force' || arg === '-f') {
|
|
44
|
+
flags.force = true;
|
|
45
|
+
} else if (arg === '--config') {
|
|
46
|
+
flags.config = argv[++i];
|
|
47
|
+
} else if (arg === '--delay') {
|
|
48
|
+
flags.delay = parseTime(argv[++i]);
|
|
49
|
+
} else if (arg === '--wait') {
|
|
50
|
+
flags.wait = true;
|
|
51
|
+
} else if (arg === '--timeout') {
|
|
52
|
+
flags.timeout = parseTime(argv[++i]);
|
|
53
|
+
} else if (arg === '--no-preamble') {
|
|
54
|
+
flags.noPreamble = true;
|
|
55
|
+
} else if (arg.startsWith('--pane=')) {
|
|
56
|
+
// Handled in update command
|
|
57
|
+
positional.push(arg);
|
|
58
|
+
} else if (arg.startsWith('--remark=')) {
|
|
59
|
+
// Handled in update command
|
|
60
|
+
positional.push(arg);
|
|
61
|
+
} else if (arg.startsWith('-')) {
|
|
62
|
+
// Unknown flag, pass through
|
|
63
|
+
positional.push(arg);
|
|
64
|
+
} else {
|
|
65
|
+
positional.push(arg);
|
|
66
|
+
}
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const [command = 'help', ...args] = positional;
|
|
71
|
+
return { command, args, flags };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse time string to seconds.
|
|
76
|
+
* Default unit is seconds (no suffix needed).
|
|
77
|
+
*/
|
|
78
|
+
function parseTime(value: string): number {
|
|
79
|
+
if (!value) return 0;
|
|
80
|
+
|
|
81
|
+
const match = value.match(/^(\d+(?:\.\d+)?)(ms|s)?$/i);
|
|
82
|
+
if (!match) {
|
|
83
|
+
console.error(
|
|
84
|
+
`Invalid time format: ${value}. Use number (seconds) or number with ms/s suffix.`
|
|
85
|
+
);
|
|
86
|
+
process.exit(ExitCodes.ERROR);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const num = parseFloat(match[1]);
|
|
90
|
+
const unit = (match[2] || 's').toLowerCase();
|
|
91
|
+
|
|
92
|
+
if (unit === 'ms') {
|
|
93
|
+
return num / 1000;
|
|
94
|
+
}
|
|
95
|
+
return num; // seconds
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────
|
|
99
|
+
// Main
|
|
100
|
+
// ─────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function main(): void {
|
|
103
|
+
const argv = process.argv.slice(2);
|
|
104
|
+
const { command, args, flags } = parseArgs(argv);
|
|
105
|
+
|
|
106
|
+
// Help doesn't need context
|
|
107
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
108
|
+
cmdHelp();
|
|
109
|
+
process.exit(ExitCodes.SUCCESS);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (command === '--version' || command === '-V') {
|
|
113
|
+
import('./version.js').then((m) => console.log(m.VERSION));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Completion doesn't need context
|
|
118
|
+
if (command === 'completion') {
|
|
119
|
+
cmdCompletion(args[0]);
|
|
120
|
+
process.exit(ExitCodes.SUCCESS);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create context for all other commands
|
|
124
|
+
const ctx = createContext({ argv, flags });
|
|
125
|
+
|
|
126
|
+
const run = async (): Promise<void> => {
|
|
127
|
+
switch (command) {
|
|
128
|
+
case 'init':
|
|
129
|
+
cmdInit(ctx);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'list':
|
|
133
|
+
case 'ls':
|
|
134
|
+
cmdList(ctx);
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'add':
|
|
138
|
+
if (args.length < 2) {
|
|
139
|
+
ctx.ui.error('Usage: tmux-team add <name> <pane> [remark]');
|
|
140
|
+
ctx.exit(ExitCodes.ERROR);
|
|
141
|
+
}
|
|
142
|
+
cmdAdd(ctx, args[0], args[1], args[2]);
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'update':
|
|
146
|
+
if (args.length < 1) {
|
|
147
|
+
ctx.ui.error('Usage: tmux-team update <name> --pane <pane> | --remark <remark>');
|
|
148
|
+
ctx.exit(ExitCodes.ERROR);
|
|
149
|
+
}
|
|
150
|
+
{
|
|
151
|
+
const options: { pane?: string; remark?: string } = {};
|
|
152
|
+
for (let i = 1; i < args.length; i++) {
|
|
153
|
+
if (args[i] === '--pane' && args[i + 1]) {
|
|
154
|
+
options.pane = args[++i];
|
|
155
|
+
} else if (args[i] === '--remark' && args[i + 1]) {
|
|
156
|
+
options.remark = args[++i];
|
|
157
|
+
} else if (args[i].startsWith('--pane=')) {
|
|
158
|
+
options.pane = args[i].slice(7);
|
|
159
|
+
} else if (args[i].startsWith('--remark=')) {
|
|
160
|
+
options.remark = args[i].slice(9);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
cmdUpdate(ctx, args[0], options);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'remove':
|
|
168
|
+
case 'rm':
|
|
169
|
+
if (args.length < 1) {
|
|
170
|
+
ctx.ui.error('Usage: tmux-team remove <name>');
|
|
171
|
+
ctx.exit(ExitCodes.ERROR);
|
|
172
|
+
}
|
|
173
|
+
cmdRemove(ctx, args[0]);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'talk':
|
|
177
|
+
case 'send':
|
|
178
|
+
if (args.length < 2) {
|
|
179
|
+
ctx.ui.error('Usage: tmux-team talk <target> <message>');
|
|
180
|
+
ctx.exit(ExitCodes.ERROR);
|
|
181
|
+
}
|
|
182
|
+
await cmdTalk(ctx, args[0], args[1]);
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case 'check':
|
|
186
|
+
case 'read':
|
|
187
|
+
if (args.length < 1) {
|
|
188
|
+
ctx.ui.error('Usage: tmux-team check <target> [lines]');
|
|
189
|
+
ctx.exit(ExitCodes.ERROR);
|
|
190
|
+
}
|
|
191
|
+
cmdCheck(ctx, args[0], args[1] ? parseInt(args[1], 10) : undefined);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'pm':
|
|
195
|
+
await cmdPm(ctx, args);
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case 'config':
|
|
199
|
+
cmdConfig(ctx, args);
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'preamble':
|
|
203
|
+
cmdPreamble(ctx, args);
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
default:
|
|
207
|
+
ctx.ui.error(`Unknown command: ${command}. Run 'tmux-team help' for usage.`);
|
|
208
|
+
ctx.exit(ExitCodes.ERROR);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
run().catch((err) => {
|
|
213
|
+
if (!flags.json) {
|
|
214
|
+
console.error(err);
|
|
215
|
+
} else {
|
|
216
|
+
console.error(JSON.stringify({ error: String(err?.message ?? err) }));
|
|
217
|
+
}
|
|
218
|
+
process.exit(ExitCodes.ERROR);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// add command - register a new agent
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import type { Context } from '../types.js';
|
|
7
|
+
import { ExitCodes } from '../exits.js';
|
|
8
|
+
import { saveLocalConfig } from '../config.js';
|
|
9
|
+
|
|
10
|
+
export function cmdAdd(ctx: Context, name: string, pane: string, remark?: string): void {
|
|
11
|
+
const { ui, config, paths, flags, exit } = ctx;
|
|
12
|
+
|
|
13
|
+
// Create config file if it doesn't exist
|
|
14
|
+
if (!fs.existsSync(paths.localConfig)) {
|
|
15
|
+
fs.writeFileSync(paths.localConfig, '{}\n');
|
|
16
|
+
if (!flags.json) {
|
|
17
|
+
ui.info(`Created ${paths.localConfig}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (config.paneRegistry[name]) {
|
|
22
|
+
ui.error(`Agent '${name}' already exists. Use 'tmux-team update' to modify.`);
|
|
23
|
+
exit(ExitCodes.ERROR);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
config.paneRegistry[name] = { pane };
|
|
27
|
+
if (remark) {
|
|
28
|
+
config.paneRegistry[name].remark = remark;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
saveLocalConfig(paths, config.paneRegistry);
|
|
32
|
+
|
|
33
|
+
if (flags.json) {
|
|
34
|
+
ui.json({ added: name, pane, remark });
|
|
35
|
+
} else {
|
|
36
|
+
ui.success(`Added agent '${name}' at pane ${pane}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// check command - capture output from agent's pane
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import type { Context } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../exits.js';
|
|
7
|
+
import { colors } from '../ui.js';
|
|
8
|
+
|
|
9
|
+
export function cmdCheck(ctx: Context, target: string, lines?: number): void {
|
|
10
|
+
const { ui, config, tmux, flags, exit } = ctx;
|
|
11
|
+
|
|
12
|
+
if (!config.paneRegistry[target]) {
|
|
13
|
+
const available = Object.keys(config.paneRegistry).join(', ');
|
|
14
|
+
ui.error(`Agent '${target}' not found. Available: ${available || 'none'}`);
|
|
15
|
+
exit(ExitCodes.PANE_NOT_FOUND);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pane = config.paneRegistry[target].pane;
|
|
19
|
+
const captureLines = lines ?? config.defaults.captureLines;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const output = tmux.capture(pane, captureLines);
|
|
23
|
+
|
|
24
|
+
if (flags.json) {
|
|
25
|
+
ui.json({ target, pane, lines: captureLines, output });
|
|
26
|
+
} else {
|
|
27
|
+
console.log(colors.cyan(`─── Output from ${target} (${pane}) ───`));
|
|
28
|
+
console.log(output);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
ui.error(`Failed to capture pane ${pane}. Is tmux running?`);
|
|
32
|
+
exit(ExitCodes.ERROR);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// completion command - shell completion scripts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { colors } from '../ui.js';
|
|
6
|
+
|
|
7
|
+
const zshCompletion = `#compdef tmux-team
|
|
8
|
+
|
|
9
|
+
_tmux-team() {
|
|
10
|
+
local -a commands agents
|
|
11
|
+
|
|
12
|
+
commands=(
|
|
13
|
+
'talk:Send message to an agent'
|
|
14
|
+
'check:Capture output from agent pane'
|
|
15
|
+
'list:List all configured agents'
|
|
16
|
+
'add:Add a new agent'
|
|
17
|
+
'update:Update agent config'
|
|
18
|
+
'remove:Remove an agent'
|
|
19
|
+
'init:Create empty tmux-team.json'
|
|
20
|
+
'completion:Output shell completion script'
|
|
21
|
+
'help:Show help message'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_get_agents() {
|
|
25
|
+
if [[ -f ./tmux-team.json ]]; then
|
|
26
|
+
agents=(\${(f)"$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('./tmux-team.json'))).join('\\\\n'))" 2>/dev/null)"})
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (( CURRENT == 2 )); then
|
|
31
|
+
_describe -t commands 'tmux-team commands' commands
|
|
32
|
+
elif (( CURRENT == 3 )); then
|
|
33
|
+
case \${words[2]} in
|
|
34
|
+
talk|check|update|remove|rm)
|
|
35
|
+
_get_agents
|
|
36
|
+
if [[ -n "$agents" ]]; then
|
|
37
|
+
_describe -t agents 'agents' agents
|
|
38
|
+
fi
|
|
39
|
+
if [[ "\${words[2]}" == "talk" ]]; then
|
|
40
|
+
compadd "all"
|
|
41
|
+
fi
|
|
42
|
+
;;
|
|
43
|
+
completion)
|
|
44
|
+
compadd "zsh" "bash"
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
elif (( CURRENT == 4 )); then
|
|
48
|
+
case \${words[2]} in
|
|
49
|
+
update)
|
|
50
|
+
compadd -- "--pane" "--remark"
|
|
51
|
+
;;
|
|
52
|
+
talk)
|
|
53
|
+
compadd -- "--delay" "--wait" "--timeout"
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_tmux-team "$@"`;
|
|
60
|
+
|
|
61
|
+
const bashCompletion = `_tmux_team() {
|
|
62
|
+
local cur prev commands agents
|
|
63
|
+
COMPREPLY=()
|
|
64
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
65
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
66
|
+
|
|
67
|
+
commands="talk check list add update remove init completion help"
|
|
68
|
+
|
|
69
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
70
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
|
|
71
|
+
elif [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
72
|
+
case "\${prev}" in
|
|
73
|
+
talk|check|update|remove|rm)
|
|
74
|
+
if [[ -f ./tmux-team.json ]]; then
|
|
75
|
+
agents=$(node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('./tmux-team.json'))).join(' '))" 2>/dev/null)
|
|
76
|
+
fi
|
|
77
|
+
if [[ "\${prev}" == "talk" ]]; then
|
|
78
|
+
agents="\${agents} all"
|
|
79
|
+
fi
|
|
80
|
+
COMPREPLY=( $(compgen -W "\${agents}" -- \${cur}) )
|
|
81
|
+
;;
|
|
82
|
+
completion)
|
|
83
|
+
COMPREPLY=( $(compgen -W "zsh bash" -- \${cur}) )
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
86
|
+
elif [[ \${COMP_CWORD} -eq 3 ]]; then
|
|
87
|
+
case "\${COMP_WORDS[1]}" in
|
|
88
|
+
update)
|
|
89
|
+
COMPREPLY=( $(compgen -W "--pane --remark" -- \${cur}) )
|
|
90
|
+
;;
|
|
91
|
+
talk)
|
|
92
|
+
COMPREPLY=( $(compgen -W "--delay --wait --timeout" -- \${cur}) )
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
complete -F _tmux_team tmux-team`;
|
|
99
|
+
|
|
100
|
+
export function cmdCompletion(shell?: string): void {
|
|
101
|
+
if (shell === 'bash') {
|
|
102
|
+
console.log(bashCompletion);
|
|
103
|
+
} else if (shell === 'zsh') {
|
|
104
|
+
console.log(zshCompletion);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(`
|
|
107
|
+
${colors.cyan('Shell Completion Setup')}
|
|
108
|
+
|
|
109
|
+
${colors.yellow('Zsh')} (add to ~/.zshrc):
|
|
110
|
+
eval "$(tmux-team completion zsh)"
|
|
111
|
+
|
|
112
|
+
${colors.yellow('Bash')} (add to ~/.bashrc):
|
|
113
|
+
eval "$(tmux-team completion bash)"
|
|
114
|
+
|
|
115
|
+
Then restart your shell or run: source ~/.zshrc (or ~/.bashrc)
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// Config command - view and modify settings
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import type { Context } from '../types.js';
|
|
6
|
+
import { ExitCodes } from '../context.js';
|
|
7
|
+
import {
|
|
8
|
+
loadGlobalConfig,
|
|
9
|
+
saveGlobalConfig,
|
|
10
|
+
loadLocalConfigFile,
|
|
11
|
+
saveLocalConfigFile,
|
|
12
|
+
updateLocalSettings,
|
|
13
|
+
clearLocalSettings,
|
|
14
|
+
} from '../config.js';
|
|
15
|
+
|
|
16
|
+
type ConfigKey = 'mode' | 'preambleMode';
|
|
17
|
+
|
|
18
|
+
const VALID_KEYS: ConfigKey[] = ['mode', 'preambleMode'];
|
|
19
|
+
|
|
20
|
+
const VALID_VALUES: Record<ConfigKey, string[]> = {
|
|
21
|
+
mode: ['polling', 'wait'],
|
|
22
|
+
preambleMode: ['always', 'disabled'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isValidKey(key: string): key is ConfigKey {
|
|
26
|
+
return VALID_KEYS.includes(key as ConfigKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isValidValue(key: ConfigKey, value: string): boolean {
|
|
30
|
+
return VALID_VALUES[key].includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show resolved config with source indicators.
|
|
35
|
+
*/
|
|
36
|
+
function showConfig(ctx: Context): void {
|
|
37
|
+
const globalConfig = loadGlobalConfig(ctx.paths);
|
|
38
|
+
const localConfigFile = loadLocalConfigFile(ctx.paths);
|
|
39
|
+
const localSettings = localConfigFile.$config;
|
|
40
|
+
|
|
41
|
+
if (ctx.flags.json) {
|
|
42
|
+
ctx.ui.json({
|
|
43
|
+
resolved: {
|
|
44
|
+
mode: ctx.config.mode,
|
|
45
|
+
preambleMode: ctx.config.preambleMode,
|
|
46
|
+
defaults: ctx.config.defaults,
|
|
47
|
+
},
|
|
48
|
+
sources: {
|
|
49
|
+
mode: localSettings?.mode ? 'local' : globalConfig.mode ? 'global' : 'default',
|
|
50
|
+
preambleMode: localSettings?.preambleMode
|
|
51
|
+
? 'local'
|
|
52
|
+
: globalConfig.preambleMode
|
|
53
|
+
? 'global'
|
|
54
|
+
: 'default',
|
|
55
|
+
},
|
|
56
|
+
paths: {
|
|
57
|
+
global: ctx.paths.globalConfig,
|
|
58
|
+
local: ctx.paths.localConfig,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Determine sources
|
|
65
|
+
const modeSource = localSettings?.mode ? '(local)' : globalConfig.mode ? '(global)' : '(default)';
|
|
66
|
+
const preambleSource = localSettings?.preambleMode
|
|
67
|
+
? '(local)'
|
|
68
|
+
: globalConfig.preambleMode
|
|
69
|
+
? '(global)'
|
|
70
|
+
: '(default)';
|
|
71
|
+
|
|
72
|
+
ctx.ui.info('Current configuration:\n');
|
|
73
|
+
ctx.ui.table(
|
|
74
|
+
['Key', 'Value', 'Source'],
|
|
75
|
+
[
|
|
76
|
+
['mode', ctx.config.mode, modeSource],
|
|
77
|
+
['preambleMode', ctx.config.preambleMode, preambleSource],
|
|
78
|
+
['defaults.timeout', String(ctx.config.defaults.timeout), '(global)'],
|
|
79
|
+
['defaults.pollInterval', String(ctx.config.defaults.pollInterval), '(global)'],
|
|
80
|
+
['defaults.captureLines', String(ctx.config.defaults.captureLines), '(global)'],
|
|
81
|
+
]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
ctx.ui.info(`\nPaths:`);
|
|
85
|
+
ctx.ui.info(` Global: ${ctx.paths.globalConfig}`);
|
|
86
|
+
ctx.ui.info(` Local: ${ctx.paths.localConfig}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set a config value.
|
|
91
|
+
*/
|
|
92
|
+
function setConfig(ctx: Context, key: string, value: string, global: boolean): void {
|
|
93
|
+
if (!isValidKey(key)) {
|
|
94
|
+
ctx.ui.error(`Invalid key: ${key}. Valid keys: ${VALID_KEYS.join(', ')}`);
|
|
95
|
+
ctx.exit(ExitCodes.ERROR);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!isValidValue(key, value)) {
|
|
99
|
+
ctx.ui.error(
|
|
100
|
+
`Invalid value for ${key}: ${value}. Valid values: ${VALID_VALUES[key].join(', ')}`
|
|
101
|
+
);
|
|
102
|
+
ctx.exit(ExitCodes.ERROR);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (global) {
|
|
106
|
+
// Set in global config
|
|
107
|
+
const globalConfig = loadGlobalConfig(ctx.paths);
|
|
108
|
+
if (key === 'mode') {
|
|
109
|
+
globalConfig.mode = value as 'polling' | 'wait';
|
|
110
|
+
} else if (key === 'preambleMode') {
|
|
111
|
+
globalConfig.preambleMode = value as 'always' | 'disabled';
|
|
112
|
+
}
|
|
113
|
+
saveGlobalConfig(ctx.paths, globalConfig);
|
|
114
|
+
ctx.ui.success(`Set ${key}=${value} in global config`);
|
|
115
|
+
} else {
|
|
116
|
+
// Set in local config
|
|
117
|
+
if (key === 'mode') {
|
|
118
|
+
updateLocalSettings(ctx.paths, { mode: value as 'polling' | 'wait' });
|
|
119
|
+
} else if (key === 'preambleMode') {
|
|
120
|
+
updateLocalSettings(ctx.paths, { preambleMode: value as 'always' | 'disabled' });
|
|
121
|
+
}
|
|
122
|
+
ctx.ui.success(`Set ${key}=${value} in local config (repo override)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clear local config override.
|
|
128
|
+
*/
|
|
129
|
+
function clearConfig(ctx: Context, key?: string): void {
|
|
130
|
+
if (key) {
|
|
131
|
+
if (!isValidKey(key)) {
|
|
132
|
+
ctx.ui.error(`Invalid key: ${key}. Valid keys: ${VALID_KEYS.join(', ')}`);
|
|
133
|
+
ctx.exit(ExitCodes.ERROR);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Clear specific key from local settings
|
|
137
|
+
const localConfigFile = loadLocalConfigFile(ctx.paths);
|
|
138
|
+
if (localConfigFile.$config) {
|
|
139
|
+
delete localConfigFile.$config[key];
|
|
140
|
+
// Remove $config if empty
|
|
141
|
+
if (Object.keys(localConfigFile.$config).length === 0) {
|
|
142
|
+
delete localConfigFile.$config;
|
|
143
|
+
}
|
|
144
|
+
saveLocalConfigFile(ctx.paths, localConfigFile);
|
|
145
|
+
}
|
|
146
|
+
ctx.ui.success(`Cleared local override for ${key}`);
|
|
147
|
+
} else {
|
|
148
|
+
// Clear all local settings
|
|
149
|
+
clearLocalSettings(ctx.paths);
|
|
150
|
+
ctx.ui.success('Cleared all local config overrides');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Config command entry point.
|
|
156
|
+
*/
|
|
157
|
+
export function cmdConfig(ctx: Context, args: string[]): void {
|
|
158
|
+
const subcommand = args[0];
|
|
159
|
+
|
|
160
|
+
// Parse --global flag
|
|
161
|
+
const globalFlag = args.includes('--global') || args.includes('-g');
|
|
162
|
+
const filteredArgs = args.filter((a) => a !== '--global' && a !== '-g');
|
|
163
|
+
|
|
164
|
+
switch (subcommand) {
|
|
165
|
+
case undefined:
|
|
166
|
+
case 'show':
|
|
167
|
+
showConfig(ctx);
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'set':
|
|
171
|
+
if (filteredArgs.length < 3) {
|
|
172
|
+
ctx.ui.error('Usage: tmux-team config set <key> <value> [--global]');
|
|
173
|
+
ctx.exit(ExitCodes.ERROR);
|
|
174
|
+
}
|
|
175
|
+
setConfig(ctx, filteredArgs[1], filteredArgs[2], globalFlag);
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'clear':
|
|
179
|
+
clearConfig(ctx, filteredArgs[1]);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
ctx.ui.error(`Unknown config subcommand: ${subcommand}`);
|
|
184
|
+
ctx.ui.error('Usage: tmux-team config [show|set|clear]');
|
|
185
|
+
ctx.exit(ExitCodes.ERROR);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// help command - show usage information
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { colors } from '../ui.js';
|
|
6
|
+
import { VERSION } from '../version.js';
|
|
7
|
+
|
|
8
|
+
export function cmdHelp(): void {
|
|
9
|
+
console.log(`
|
|
10
|
+
${colors.cyan('tmux-team')} v${VERSION} - AI agent collaboration in tmux
|
|
11
|
+
|
|
12
|
+
${colors.yellow('USAGE')}
|
|
13
|
+
tmux-team <command> [arguments]
|
|
14
|
+
|
|
15
|
+
${colors.yellow('COMMANDS')}
|
|
16
|
+
${colors.green('talk')} <target> <message> Send message to an agent (or "all")
|
|
17
|
+
${colors.green('check')} <target> [lines] Capture output from agent's pane
|
|
18
|
+
${colors.green('list')} List all configured agents
|
|
19
|
+
${colors.green('add')} <name> <pane> [remark] Add a new agent
|
|
20
|
+
${colors.green('update')} <name> [options] Update an agent's config
|
|
21
|
+
${colors.green('remove')} <name> Remove an agent
|
|
22
|
+
${colors.green('init')} Create empty tmux-team.json
|
|
23
|
+
${colors.green('config')} [show|set|clear] View/modify settings
|
|
24
|
+
${colors.green('preamble')} [show|set|clear] Manage agent preambles
|
|
25
|
+
${colors.green('pm')} <subcommand> Project management (run 'pm help')
|
|
26
|
+
${colors.green('completion')} Output shell completion script
|
|
27
|
+
${colors.green('help')} Show this help message
|
|
28
|
+
|
|
29
|
+
${colors.yellow('OPTIONS')}
|
|
30
|
+
${colors.green('--json')} Output in JSON format
|
|
31
|
+
${colors.green('--verbose')} Show detailed output
|
|
32
|
+
${colors.green('--force')} Skip warnings
|
|
33
|
+
|
|
34
|
+
${colors.yellow('TALK OPTIONS')} ${colors.dim('(v2)')}
|
|
35
|
+
${colors.green('--delay')} <seconds> Wait before sending (default: seconds)
|
|
36
|
+
${colors.green('--wait')} Wait for agent response (nonce-based)
|
|
37
|
+
${colors.green('--timeout')} <seconds> Max wait time (default: 60)
|
|
38
|
+
${colors.green('--no-preamble')} Skip agent preamble for this message
|
|
39
|
+
|
|
40
|
+
${colors.yellow('EXAMPLES')}
|
|
41
|
+
tmux-team talk codex "Please review the PR"
|
|
42
|
+
tmux-team talk all "Sync meeting in 5 minutes"
|
|
43
|
+
tmux-team check gemini 50
|
|
44
|
+
tmux-team list --json
|
|
45
|
+
tmux-team add codex 10.1 "Code review specialist"
|
|
46
|
+
tmux-team update codex --pane 10.2
|
|
47
|
+
tmux-team remove codex
|
|
48
|
+
|
|
49
|
+
${colors.yellow('CONFIG')}
|
|
50
|
+
Local: ./tmux-team.json (pane registry + $config override)
|
|
51
|
+
Global: ~/.config/tmux-team/config.json (settings)
|
|
52
|
+
|
|
53
|
+
${colors.yellow('CONFIG EXAMPLES')}
|
|
54
|
+
tmux-team config Show current settings
|
|
55
|
+
tmux-team config set mode wait Set mode in local config (repo override)
|
|
56
|
+
tmux-team config set mode polling --global Set mode in global config
|
|
57
|
+
tmux-team config clear mode Clear local override for mode
|
|
58
|
+
tmux-team config clear Clear all local overrides
|
|
59
|
+
|
|
60
|
+
${colors.yellow('PREAMBLE EXAMPLES')}
|
|
61
|
+
tmux-team preamble Show all preambles
|
|
62
|
+
tmux-team preamble show codex Show preamble for codex
|
|
63
|
+
tmux-team preamble set codex "You are a code reviewer. Be concise."
|
|
64
|
+
tmux-team preamble clear codex Clear preamble for codex
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// init command - create tmux-team.json
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import type { Context } from '../types.js';
|
|
7
|
+
import { ExitCodes } from '../exits.js';
|
|
8
|
+
|
|
9
|
+
export function cmdInit(ctx: Context): void {
|
|
10
|
+
const { ui, paths, flags, exit } = ctx;
|
|
11
|
+
|
|
12
|
+
if (fs.existsSync(paths.localConfig)) {
|
|
13
|
+
ui.error(`${paths.localConfig} already exists. Remove it first if you want to reinitialize.`);
|
|
14
|
+
exit(ExitCodes.ERROR);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fs.writeFileSync(paths.localConfig, '{}\n');
|
|
18
|
+
|
|
19
|
+
if (flags.json) {
|
|
20
|
+
ui.json({ created: paths.localConfig });
|
|
21
|
+
} else {
|
|
22
|
+
ui.success(`Created ${paths.localConfig}`);
|
|
23
|
+
}
|
|
24
|
+
}
|