tmux-team 4.0.0 → 4.2.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.
@@ -16,15 +16,15 @@ _tmux-team() {
16
16
  'add:Add a new agent'
17
17
  'update:Update agent config'
18
18
  'remove:Remove an agent'
19
- 'init:Create empty tmux-team.json'
19
+ 'migrate:Copy legacy tmux-team.json registry into tmux metadata'
20
+ 'team:Manage shared teams'
21
+ 'init:Create empty legacy tmux-team.json'
20
22
  'completion:Output shell completion script'
21
23
  'help:Show help message'
22
24
  )
23
25
 
24
26
  _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
27
+ agents=(\${(f)"$(tmux-team list --json 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{const j=JSON.parse(s); console.log(Object.keys(j.agents||{}).join("\\n"))}catch{}})')"})
28
28
  }
29
29
 
30
30
  if (( CURRENT == 2 )); then
@@ -64,16 +64,14 @@ const bashCompletion = `_tmux_team() {
64
64
  cur="\${COMP_WORDS[COMP_CWORD]}"
65
65
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
66
66
 
67
- commands="talk check list add update remove init completion help"
67
+ commands="talk check list add update remove migrate team init completion help"
68
68
 
69
69
  if [[ \${COMP_CWORD} -eq 1 ]]; then
70
70
  COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
71
71
  elif [[ \${COMP_CWORD} -eq 2 ]]; then
72
72
  case "\${prev}" in
73
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
74
+ agents=$(tmux-team list --json 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{const j=JSON.parse(s); console.log(Object.keys(j.agents||{}).join(" "))}catch{}})')
77
75
  if [[ "\${prev}" == "talk" ]]; then
78
76
  agents="\${agents} all"
79
77
  fi
@@ -51,6 +51,13 @@ function createCtx(
51
51
  capture: vi.fn(),
52
52
  listPanes: vi.fn(() => []),
53
53
  getCurrentPaneId: vi.fn(() => null),
54
+ resolvePaneTarget: vi.fn((target: string) => target),
55
+ getAgentRegistry: vi.fn(() => ({ paneRegistry: {}, agents: {} })),
56
+ setAgentRegistration: vi.fn(),
57
+ clearAgentRegistration: vi.fn(() => false),
58
+ listTeams: vi.fn(() => ({})),
59
+ listTeamPanes: vi.fn(() => []),
60
+ removeTeam: vi.fn(() => ({ removed: 0, agents: [] })),
54
61
  };
55
62
  return {
56
63
  argv: [],
@@ -167,10 +174,7 @@ describe('cmdConfig', () => {
167
174
 
168
175
  it('shows sources in table mode with local settings', () => {
169
176
  const ctx = createCtx(testDir);
170
- fs.writeFileSync(
171
- ctx.paths.localConfig,
172
- JSON.stringify({ $config: { mode: 'wait' } })
173
- );
177
+ fs.writeFileSync(ctx.paths.localConfig, JSON.stringify({ $config: { mode: 'wait' } }));
174
178
  cmdConfig(ctx, ['show']);
175
179
  expect(ctx.ui.table).toHaveBeenCalled();
176
180
  // The table call should include (local) source
@@ -181,10 +185,7 @@ describe('cmdConfig', () => {
181
185
  it('shows sources in table mode with global settings', () => {
182
186
  const ctx = createCtx(testDir);
183
187
  fs.mkdirSync(ctx.paths.globalDir, { recursive: true });
184
- fs.writeFileSync(
185
- ctx.paths.globalConfig,
186
- JSON.stringify({ mode: 'wait' })
187
- );
188
+ fs.writeFileSync(ctx.paths.globalConfig, JSON.stringify({ mode: 'wait' }));
188
189
  cmdConfig(ctx, ['show']);
189
190
  expect(ctx.ui.table).toHaveBeenCalled();
190
191
  const tableCall = (ctx.ui.table as ReturnType<typeof vi.fn>).mock.calls[0];
@@ -114,11 +114,7 @@ function showConfig(ctx: Context): void {
114
114
  ['mode', ctx.config.mode, modeSource],
115
115
  ['preambleMode', ctx.config.preambleMode, preambleSource],
116
116
  ['preambleEvery', String(ctx.config.defaults.preambleEvery), preambleEverySource],
117
- [
118
- 'pasteEnterDelayMs',
119
- String(ctx.config.defaults.pasteEnterDelayMs),
120
- pasteEnterDelaySource,
121
- ],
117
+ ['pasteEnterDelayMs', String(ctx.config.defaults.pasteEnterDelayMs), pasteEnterDelaySource],
122
118
  ['defaults.timeout', String(ctx.config.defaults.timeout), '(global)'],
123
119
  ['defaults.pollInterval', String(ctx.config.defaults.pollInterval), '(global)'],
124
120
  ['defaults.captureLines', String(ctx.config.defaults.captureLines), '(global)'],
@@ -47,11 +47,13 @@ ${colors.yellow('USAGE')}
47
47
  ${colors.yellow('COMMANDS')}
48
48
  ${colors.green('talk')} <target> <message> Send message to an agent (or "all")
49
49
  ${colors.green('check')} <target> [lines] Capture output from agent's pane
50
- ${colors.green('list')} List all configured agents
50
+ ${colors.green('list')} [team|pane] List workspace, team, or pane status
51
51
  ${colors.green('add')} <name> <pane> [remark] Add a new agent
52
52
  ${colors.green('this')} <name> [remark] Register current pane as an agent
53
53
  ${colors.green('update')} <name> [options] Update an agent's config
54
54
  ${colors.green('remove')} <name> Remove an agent
55
+ ${colors.green('migrate')} [--dry-run] Copy legacy JSON registry to tmux metadata
56
+ ${colors.green('team')} [ls|add|rm|panes] Manage shared teams
55
57
  ${colors.green('install')} [claude|codex] Install tmux-team for an AI agent
56
58
  ${colors.green('init')} Create empty tmux-team.json
57
59
  ${colors.green('config')} [show|set|clear] View/modify settings
@@ -86,12 +88,15 @@ ${colors.yellow('EXAMPLES')}${
86
88
  tmux-team check codex ${colors.dim('← read response later')}`
87
89
  }
88
90
  tmux-team list --json
91
+ tmux-team list acme-app
92
+ tmux-team list main.1.0
89
93
  tmux-team add codex 10.1 "Code review specialist"
90
94
 
91
95
  ${colors.yellow('CONFIG')}
92
- Local: ./tmux-team.json (pane registry + $config override)
96
+ Runtime: tmux pane metadata (agent registry)
97
+ Local: ./tmux-team.json (legacy registry + $config override)
93
98
  Global: ~/.config/tmux-team/config.json (settings)
94
- Teams: ~/.config/tmux-team/teams/<name>.json (shared teams)
99
+ Teams: tmux pane metadata; team panes shows cwd + workspace/team scopes
95
100
 
96
101
  ${colors.yellow('CHANGE MODE')}
97
102
  tmux-team config set mode wait ${colors.dim('Enable wait mode (local)')}
@@ -26,7 +26,14 @@ function createCtx(testDir: string, overrides?: Partial<{ flags: Partial<Flags>
26
26
  const config: ResolvedConfig = {
27
27
  mode: 'polling',
28
28
  preambleMode: 'always',
29
- defaults: { timeout: 180, pollInterval: 1, captureLines: 100, maxCaptureLines: 2000, preambleEvery: 3, pasteEnterDelayMs: 500 },
29
+ defaults: {
30
+ timeout: 180,
31
+ pollInterval: 1,
32
+ captureLines: 100,
33
+ maxCaptureLines: 2000,
34
+ preambleEvery: 3,
35
+ pasteEnterDelayMs: 500,
36
+ },
30
37
  agents: {},
31
38
  paneRegistry: {},
32
39
  };
@@ -36,6 +43,13 @@ function createCtx(testDir: string, overrides?: Partial<{ flags: Partial<Flags>
36
43
  capture: vi.fn(),
37
44
  listPanes: vi.fn(() => []),
38
45
  getCurrentPaneId: vi.fn(() => null),
46
+ resolvePaneTarget: vi.fn((target: string) => target),
47
+ getAgentRegistry: vi.fn(() => ({ paneRegistry: {}, agents: {} })),
48
+ setAgentRegistration: vi.fn(),
49
+ clearAgentRegistration: vi.fn(() => false),
50
+ listTeams: vi.fn(() => ({})),
51
+ listTeamPanes: vi.fn(() => []),
52
+ removeTeam: vi.fn(() => ({ removed: 0, agents: [] })),
39
53
  };
40
54
  return {
41
55
  argv: [],
@@ -3,9 +3,22 @@
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import type { Context } from '../types.js';
6
+ import { listPaneStatus, listTeamMembers } from './team.js';
6
7
 
7
- export function cmdList(ctx: Context): void {
8
+ export function cmdList(ctx: Context, target?: string): void {
8
9
  const { ui, config, flags } = ctx;
10
+
11
+ if (target) {
12
+ const teams = ctx.tmux.listTeams();
13
+ if (teams[target]) {
14
+ listTeamMembers(ctx, [target]);
15
+ return;
16
+ }
17
+
18
+ listPaneStatus(ctx, target);
19
+ return;
20
+ }
21
+
9
22
  const agents = Object.entries(config.paneRegistry);
10
23
 
11
24
  if (flags.json) {
@@ -15,7 +28,9 @@ export function cmdList(ctx: Context): void {
15
28
 
16
29
  if (agents.length === 0) {
17
30
  if (flags.team) {
18
- ui.info(`No agents in team "${flags.team}". Use 'tmt this <name> --team ${flags.team}' to add one.`);
31
+ ui.info(
32
+ `No agents in team "${flags.team}". Use 'tmt this <name> --team ${flags.team}' to add one.`
33
+ );
19
34
  } else {
20
35
  ui.info("No agents configured. Use 'tmux-team add <name> <pane>' to add one.");
21
36
  }
@@ -26,6 +41,10 @@ export function cmdList(ctx: Context): void {
26
41
  if (flags.team) {
27
42
  console.log(`Team: ${flags.team}`);
28
43
  console.log();
44
+ } else if (config.registrySource === 'legacy') {
45
+ ui.warn(
46
+ 'Using legacy tmux-team.json registry. Run `tmt migrate` to store registrations in tmux.'
47
+ );
29
48
  }
30
49
  ui.table(
31
50
  ['NAME', 'PANE', 'REMARK'],
@@ -0,0 +1,84 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // migrate command - copy legacy JSON registry into tmux metadata
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import type { Context, PaneEntry } from '../types.js';
6
+ import { ExitCodes } from '../exits.js';
7
+ import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
+ import { getRegistryScope, registrationFromEntry, scopeLabel } from '../registry.js';
9
+
10
+ interface MigrationItem {
11
+ agent: string;
12
+ fromPane: string;
13
+ pane: string;
14
+ remark?: string;
15
+ status: 'ready' | 'migrated';
16
+ }
17
+
18
+ export function cmdMigrate(ctx: Context, args: string[]): void {
19
+ const dryRun = args.includes('--dry-run');
20
+ const cleanup = args.includes('--cleanup');
21
+ const { ui, paths, flags, tmux, exit } = ctx;
22
+ const localConfig = loadLocalConfigFile(paths);
23
+ const scope = getRegistryScope(ctx);
24
+ const items: MigrationItem[] = [];
25
+
26
+ for (const [agentName, rawEntry] of Object.entries(localConfig)) {
27
+ if (agentName === '$config') continue;
28
+ const entry = rawEntry as PaneEntry | undefined;
29
+ if (!entry?.pane) continue;
30
+
31
+ const pane = tmux.resolvePaneTarget(entry.pane);
32
+ if (!pane) {
33
+ ui.error(`Pane '${entry.pane}' for agent '${agentName}' not found. Is tmux running?`);
34
+ exit(ExitCodes.PANE_NOT_FOUND);
35
+ }
36
+ const paneId = pane as string;
37
+
38
+ items.push({
39
+ agent: agentName,
40
+ fromPane: entry.pane,
41
+ pane: paneId,
42
+ ...(entry.remark !== undefined && { remark: entry.remark }),
43
+ status: dryRun ? 'ready' : 'migrated',
44
+ });
45
+
46
+ if (!dryRun) {
47
+ tmux.setAgentRegistration(paneId, scope, registrationFromEntry(agentName, entry));
48
+ }
49
+ }
50
+
51
+ if (!dryRun && cleanup && items.length > 0) {
52
+ for (const item of items) {
53
+ delete localConfig[item.agent];
54
+ }
55
+ saveLocalConfigFile(paths, localConfig);
56
+ }
57
+
58
+ if (flags.json) {
59
+ ui.json({
60
+ dryRun,
61
+ cleanup,
62
+ scope,
63
+ migrated: dryRun ? 0 : items.length,
64
+ items,
65
+ });
66
+ return;
67
+ }
68
+
69
+ if (items.length === 0) {
70
+ ui.info(`No legacy agents found in ${paths.localConfig}`);
71
+ return;
72
+ }
73
+
74
+ const action = dryRun ? 'Would migrate' : 'Migrated';
75
+ ui.success(`${action} ${items.length} agent(s) to ${scopeLabel(scope)}`);
76
+ ui.table(
77
+ ['AGENT', 'FROM', 'PANE', 'REMARK'],
78
+ items.map((item) => [item.agent, item.fromPane, item.pane, item.remark ?? '-'])
79
+ );
80
+
81
+ if (!dryRun && !cleanup) {
82
+ ui.info('Legacy JSON was left in place. Use --cleanup to remove migrated agent entries.');
83
+ }
84
+ }
@@ -14,7 +14,12 @@ import { cmdPreamble } from './preamble.js';
14
14
  // Test utilities
15
15
  // ─────────────────────────────────────────────────────────────
16
16
 
17
- function createMockUI(): UI & { errors: string[]; warnings: string[]; infos: string[]; jsonOutput: unknown[] } {
17
+ function createMockUI(): UI & {
18
+ errors: string[];
19
+ warnings: string[];
20
+ infos: string[];
21
+ jsonOutput: unknown[];
22
+ } {
18
23
  const mock = {
19
24
  errors: [] as string[],
20
25
  warnings: [] as string[],
@@ -48,7 +53,8 @@ function createDefaultConfig(): ResolvedConfig {
48
53
  pollInterval: 1,
49
54
  captureLines: 100,
50
55
  maxCaptureLines: 2000,
51
- preambleEvery: 3, pasteEnterDelayMs: 500,
56
+ preambleEvery: 3,
57
+ pasteEnterDelayMs: 500,
52
58
  },
53
59
  agents: {},
54
60
  paneRegistry: {
@@ -64,6 +70,13 @@ function createMockTmux(): Tmux {
64
70
  capture: vi.fn(() => ''),
65
71
  listPanes: vi.fn(() => []),
66
72
  getCurrentPaneId: vi.fn(() => null),
73
+ resolvePaneTarget: vi.fn((target: string) => target),
74
+ getAgentRegistry: vi.fn(() => ({ paneRegistry: {}, agents: {} })),
75
+ setAgentRegistration: vi.fn(),
76
+ clearAgentRegistration: vi.fn(() => false),
77
+ listTeams: vi.fn(() => ({})),
78
+ listTeamPanes: vi.fn(() => []),
79
+ removeTeam: vi.fn(() => ({ removed: 0, agents: [] })),
67
80
  };
68
81
  }
69
82
 
@@ -5,6 +5,7 @@
5
5
  import type { Context } from '../types.js';
6
6
  import { ExitCodes } from '../context.js';
7
7
  import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
+ import { getRegistryScope, registrationFromEntry } from '../registry.js';
8
9
 
9
10
  /**
10
11
  * Show preamble(s) for agent(s).
@@ -60,7 +61,7 @@ function showPreamble(ctx: Context, agentName?: string): void {
60
61
  * Set preamble for an agent (in local config).
61
62
  */
62
63
  function setPreamble(ctx: Context, agentName: string, preamble: string): void {
63
- const { ui, paths, flags, config } = ctx;
64
+ const { ui, paths, flags, config, tmux } = ctx;
64
65
 
65
66
  // Check if agent exists in pane registry
66
67
  if (!config.paneRegistry[agentName]) {
@@ -69,15 +70,19 @@ function setPreamble(ctx: Context, agentName: string, preamble: string): void {
69
70
  ctx.exit(ExitCodes.ERROR);
70
71
  }
71
72
 
72
- const localConfig = loadLocalConfigFile(paths);
73
-
74
- // Update preamble in local config
75
- const agentEntry = localConfig[agentName] as { pane: string; preamble?: string } | undefined;
76
- if (agentEntry) {
77
- agentEntry.preamble = preamble;
73
+ const pane = tmux.resolvePaneTarget(config.paneRegistry[agentName].pane);
74
+ if (!pane) {
75
+ ui.error(`Pane '${config.paneRegistry[agentName].pane}' not found. Is tmux running?`);
76
+ ctx.exit(ExitCodes.PANE_NOT_FOUND);
78
77
  }
79
78
 
80
- saveLocalConfigFile(paths, localConfig);
79
+ const nextEntry = { ...config.paneRegistry[agentName], pane, preamble };
80
+ tmux.setAgentRegistration(
81
+ pane,
82
+ getRegistryScope(ctx),
83
+ registrationFromEntry(agentName, nextEntry)
84
+ );
85
+ updateLegacyPreambleIfPresent(paths, agentName, preamble);
81
86
 
82
87
  if (flags.json) {
83
88
  ui.json({ agent: agentName, preamble, status: 'set' });
@@ -90,14 +95,28 @@ function setPreamble(ctx: Context, agentName: string, preamble: string): void {
90
95
  * Clear preamble for an agent (in local config).
91
96
  */
92
97
  function clearPreamble(ctx: Context, agentName: string): void {
93
- const { ui, paths, flags } = ctx;
94
-
95
- const localConfig = loadLocalConfigFile(paths);
96
- const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
97
-
98
- if (agentEntry?.preamble) {
99
- delete agentEntry.preamble;
100
- saveLocalConfigFile(paths, localConfig);
98
+ const { ui, paths, flags, config, tmux } = ctx;
99
+
100
+ const entry = config.paneRegistry[agentName];
101
+ const hasPreamble =
102
+ entry?.preamble !== undefined ||
103
+ config.agents[agentName]?.preamble !== undefined ||
104
+ legacyHasPreamble(paths, agentName);
105
+
106
+ if (entry && hasPreamble) {
107
+ const pane = tmux.resolvePaneTarget(entry.pane);
108
+ if (!pane) {
109
+ ui.error(`Pane '${entry.pane}' not found. Is tmux running?`);
110
+ ctx.exit(ExitCodes.PANE_NOT_FOUND);
111
+ }
112
+ const nextEntry = { ...entry, pane };
113
+ delete nextEntry.preamble;
114
+ tmux.setAgentRegistration(
115
+ pane,
116
+ getRegistryScope(ctx),
117
+ registrationFromEntry(agentName, nextEntry)
118
+ );
119
+ clearLegacyPreambleIfPresent(paths, agentName);
101
120
 
102
121
  if (flags.json) {
103
122
  ui.json({ agent: agentName, status: 'cleared' });
@@ -113,6 +132,32 @@ function clearPreamble(ctx: Context, agentName: string): void {
113
132
  }
114
133
  }
115
134
 
135
+ function updateLegacyPreambleIfPresent(
136
+ paths: Context['paths'],
137
+ agentName: string,
138
+ preamble: string
139
+ ): void {
140
+ const localConfig = loadLocalConfigFile(paths);
141
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
142
+ if (!agentEntry) return;
143
+ agentEntry.preamble = preamble;
144
+ saveLocalConfigFile(paths, localConfig);
145
+ }
146
+
147
+ function clearLegacyPreambleIfPresent(paths: Context['paths'], agentName: string): void {
148
+ const localConfig = loadLocalConfigFile(paths);
149
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
150
+ if (!agentEntry || !Object.prototype.hasOwnProperty.call(agentEntry, 'preamble')) return;
151
+ delete agentEntry.preamble;
152
+ saveLocalConfigFile(paths, localConfig);
153
+ }
154
+
155
+ function legacyHasPreamble(paths: Context['paths'], agentName: string): boolean {
156
+ const localConfig = loadLocalConfigFile(paths);
157
+ const agentEntry = localConfig[agentName] as { pane?: string; preamble?: string } | undefined;
158
+ return Boolean(agentEntry && Object.prototype.hasOwnProperty.call(agentEntry, 'preamble'));
159
+ }
160
+
116
161
  /**
117
162
  * Preamble command entry point.
118
163
  */
@@ -5,22 +5,26 @@
5
5
  import type { Context } from '../types.js';
6
6
  import { ExitCodes } from '../exits.js';
7
7
  import { loadLocalConfigFile, saveLocalConfigFile } from '../config.js';
8
+ import { getRegistryScope } from '../registry.js';
8
9
 
9
10
  export function cmdRemove(ctx: Context, name: string): void {
10
- const { ui, config, paths, flags, exit } = ctx;
11
+ const { ui, config, paths, flags, tmux, exit } = ctx;
11
12
 
12
13
  if (!config.paneRegistry[name]) {
13
14
  ui.error(`Agent '${name}' not found.`);
14
15
  exit(ExitCodes.PANE_NOT_FOUND);
15
16
  }
16
17
 
17
- // Load existing config to preserve other agents' fields (preamble, deny, etc.)
18
- const localConfig = loadLocalConfigFile(paths);
19
- delete localConfig[name];
20
- saveLocalConfigFile(paths, localConfig);
18
+ const removedFromTmux = tmux.clearAgentRegistration(name, getRegistryScope(ctx));
19
+ if (!removedFromTmux) {
20
+ // Legacy fallback: remove from tmux-team.json when this scope still uses it.
21
+ const localConfig = loadLocalConfigFile(paths);
22
+ delete localConfig[name];
23
+ saveLocalConfigFile(paths, localConfig);
24
+ }
21
25
 
22
26
  if (flags.json) {
23
- ui.json({ removed: name });
27
+ ui.json({ removed: name, source: removedFromTmux ? 'tmux' : 'legacy' });
24
28
  } else {
25
29
  ui.success(`Removed agent '${name}'`);
26
30
  }