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/src/tmux.ts CHANGED
@@ -1,9 +1,30 @@
1
1
  // ─────────────────────────────────────────────────────────────
2
- // Pure tmux wrapper - send-keys, capture-pane
2
+ // Pure tmux wrapper - send-keys, capture-pane, pane detection
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import { execSync } from 'child_process';
6
- import type { Tmux } from './types.js';
6
+ import type { Tmux, PaneInfo } from './types.js';
7
+
8
+ // Known agent patterns for auto-detection
9
+ const KNOWN_AGENTS: Record<string, string[]> = {
10
+ claude: ['claude', 'claude-code'],
11
+ codex: ['codex'],
12
+ gemini: ['gemini'],
13
+ aider: ['aider'],
14
+ cursor: ['cursor'],
15
+ };
16
+
17
+ function detectAgentName(command: string): string | null {
18
+ const lowerCommand = command.toLowerCase();
19
+ for (const [agentName, patterns] of Object.entries(KNOWN_AGENTS)) {
20
+ for (const pattern of patterns) {
21
+ if (lowerCommand.includes(pattern)) {
22
+ return agentName;
23
+ }
24
+ }
25
+ }
26
+ return null;
27
+ }
7
28
 
8
29
  export function createTmux(): Tmux {
9
30
  return {
@@ -23,5 +44,48 @@ export function createTmux(): Tmux {
23
44
  });
24
45
  return output;
25
46
  },
47
+
48
+ listPanes(): PaneInfo[] {
49
+ try {
50
+ // Get all panes with their IDs and current commands
51
+ const output = execSync('tmux list-panes -a -F "#{pane_id}\t#{pane_current_command}"', {
52
+ encoding: 'utf-8',
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ });
55
+
56
+ return output
57
+ .trim()
58
+ .split('\n')
59
+ .filter((line) => line.trim())
60
+ .map((line) => {
61
+ const [id, command] = line.split('\t');
62
+ return {
63
+ id: id || '',
64
+ command: command || '',
65
+ suggestedName: detectAgentName(command || ''),
66
+ };
67
+ });
68
+ } catch {
69
+ return [];
70
+ }
71
+ },
72
+
73
+ getCurrentPaneId(): string | null {
74
+ // First check environment variable
75
+ if (process.env.TMUX_PANE) {
76
+ return process.env.TMUX_PANE;
77
+ }
78
+
79
+ // Fall back to tmux command
80
+ try {
81
+ const output = execSync('tmux display-message -p "#{pane_id}"', {
82
+ encoding: 'utf-8',
83
+ stdio: ['pipe', 'pipe', 'pipe'],
84
+ });
85
+ return output.trim() || null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ },
26
90
  };
27
91
  }
package/src/types.ts CHANGED
@@ -53,11 +53,13 @@ export interface ResolvedConfig {
53
53
  export interface Flags {
54
54
  json: boolean;
55
55
  verbose: boolean;
56
+ debug?: boolean;
56
57
  config?: string;
57
58
  force?: boolean;
58
59
  delay?: number; // seconds
59
60
  wait?: boolean;
60
61
  timeout?: number; // seconds
62
+ lines?: number; // lines to capture before end marker
61
63
  noPreamble?: boolean;
62
64
  }
63
65
 
@@ -77,15 +79,22 @@ export interface UI {
77
79
  json: (data: unknown) => void;
78
80
  }
79
81
 
82
+ export interface PaneInfo {
83
+ id: string; // e.g., "%1"
84
+ command: string; // e.g., "node", "python", "zsh"
85
+ suggestedName: string | null; // e.g., "codex" if detected from command
86
+ }
87
+
80
88
  export interface Tmux {
81
89
  send: (paneId: string, message: string) => void;
82
90
  capture: (paneId: string, lines: number) => string;
91
+ listPanes: () => PaneInfo[];
92
+ getCurrentPaneId: () => string | null;
83
93
  }
84
94
 
85
95
  export interface WaitResult {
86
96
  requestId: string;
87
97
  nonce: string;
88
- startMarker: string;
89
98
  endMarker: string;
90
99
  response: string;
91
100
  }
package/src/ui.test.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ describe('ui', () => {
4
+ beforeEach(() => {
5
+ vi.restoreAllMocks();
6
+ });
7
+
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ it('createUI(jsonMode=true) suppresses human output and emits JSON errors', async () => {
13
+ vi.resetModules();
14
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
15
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
16
+
17
+ const { createUI } = await import('./ui.js');
18
+ const ui = createUI(true);
19
+
20
+ ui.info('x');
21
+ ui.success('x');
22
+ ui.warn('x');
23
+ ui.table(['A'], [['B']]);
24
+ expect(logSpy).not.toHaveBeenCalled();
25
+
26
+ ui.error('boom');
27
+ expect(errSpy).toHaveBeenCalledWith(JSON.stringify({ error: 'boom' }));
28
+
29
+ ui.json({ ok: true });
30
+ expect(logSpy).toHaveBeenCalledWith(JSON.stringify({ ok: true }, null, 2));
31
+ });
32
+
33
+ it('createUI(jsonMode=false) prints messages and table', async () => {
34
+ vi.resetModules();
35
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
36
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
37
+
38
+ const { createUI } = await import('./ui.js');
39
+ const ui = createUI(false);
40
+
41
+ ui.info('hello');
42
+ ui.success('ok');
43
+ ui.warn('warn');
44
+ ui.error('err');
45
+
46
+ expect(logSpy).toHaveBeenCalled();
47
+ expect(errSpy).toHaveBeenCalled();
48
+
49
+ logSpy.mockClear();
50
+ ui.table(
51
+ ['Name', 'Pane'],
52
+ [
53
+ ['claude', '1.0'],
54
+ ['codex', '1.1'],
55
+ ]
56
+ );
57
+ const output = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
58
+ expect(output).toContain('Name');
59
+ expect(output).toContain('Pane');
60
+ expect(output).toContain('claude');
61
+ expect(output).toContain('1.0');
62
+ });
63
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ describe('version', () => {
4
+ beforeEach(() => {
5
+ vi.restoreAllMocks();
6
+ });
7
+
8
+ it('reads VERSION from package.json when possible', async () => {
9
+ vi.resetModules();
10
+ const { VERSION } = await import('./version.js');
11
+ expect(typeof VERSION).toBe('string');
12
+ expect(VERSION.length).toBeGreaterThan(0);
13
+ });
14
+
15
+ it('falls back to hardcoded version when package.json read fails', async () => {
16
+ vi.resetModules();
17
+ vi.doMock('fs', () => ({
18
+ default: {
19
+ readFileSync: () => {
20
+ throw new Error('read fail');
21
+ },
22
+ },
23
+ readFileSync: () => {
24
+ throw new Error('read fail');
25
+ },
26
+ }));
27
+
28
+ const { VERSION } = await import('./version.js');
29
+ expect(VERSION).toBe('3.0.1');
30
+ });
31
+ });
@@ -1,148 +0,0 @@
1
- // ─────────────────────────────────────────────────────────────
2
- // install-skill command - install tmux-team skills 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 { fileURLToPath } from 'node:url';
9
- import type { Context } from '../types.js';
10
- import { ExitCodes } from '../context.js';
11
-
12
- type AgentType = 'claude' | 'codex';
13
- type Scope = 'user' | 'local';
14
-
15
- interface SkillConfig {
16
- sourceFile: string;
17
- userDir: string;
18
- localDir: string;
19
- targetFile: string;
20
- }
21
-
22
- function getCodexHome(): string {
23
- return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
24
- }
25
-
26
- const SKILL_CONFIGS: Record<AgentType, SkillConfig> = {
27
- claude: {
28
- sourceFile: 'skills/claude/team.md',
29
- userDir: path.join(os.homedir(), '.claude', 'commands'),
30
- localDir: '.claude/commands',
31
- targetFile: 'team.md',
32
- },
33
- codex: {
34
- sourceFile: 'skills/codex/SKILL.md',
35
- userDir: path.join(getCodexHome(), 'skills', 'tmux-team'),
36
- localDir: '.codex/skills/tmux-team',
37
- targetFile: 'SKILL.md',
38
- },
39
- };
40
-
41
- const SUPPORTED_AGENTS = Object.keys(SKILL_CONFIGS) as AgentType[];
42
-
43
- function findPackageRoot(): string {
44
- // Get current file's directory (ES modules don't have __dirname)
45
- const currentFile = fileURLToPath(import.meta.url);
46
- let dir = path.dirname(currentFile);
47
-
48
- // Try to find the package root by looking for package.json
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
- // Fallback: assume we're in src/commands
59
- return path.resolve(path.dirname(currentFile), '..', '..');
60
- }
61
-
62
- function exitWithError(ctx: Context, error: string, hint?: string): never {
63
- if (ctx.flags.json) {
64
- ctx.ui.json({ success: false, error, hint });
65
- } else {
66
- ctx.ui.error(error);
67
- if (hint) ctx.ui.info(hint);
68
- }
69
- ctx.exit(ExitCodes.ERROR);
70
- }
71
-
72
- export function cmdInstallSkill(ctx: Context, agent?: string, scope: string = 'user'): void {
73
- // Validate agent
74
- if (!agent) {
75
- exitWithError(
76
- ctx,
77
- 'Usage: tmux-team install-skill <agent> [--local|--user]',
78
- `Supported agents: ${SUPPORTED_AGENTS.join(', ')}`
79
- );
80
- }
81
-
82
- const agentLower = agent.toLowerCase() as AgentType;
83
- if (!SUPPORTED_AGENTS.includes(agentLower)) {
84
- exitWithError(
85
- ctx,
86
- `Unknown agent: ${agent}`,
87
- `Supported agents: ${SUPPORTED_AGENTS.join(', ')}`
88
- );
89
- }
90
-
91
- // Validate scope
92
- const scopeLower = scope.toLowerCase() as Scope;
93
- if (scopeLower !== 'user' && scopeLower !== 'local') {
94
- exitWithError(ctx, `Invalid scope: ${scope}. Use 'user' or 'local'.`);
95
- }
96
-
97
- const config = SKILL_CONFIGS[agentLower];
98
- const pkgRoot = findPackageRoot();
99
- const sourcePath = path.join(pkgRoot, config.sourceFile);
100
-
101
- // Check source file exists
102
- if (!fs.existsSync(sourcePath)) {
103
- exitWithError(
104
- ctx,
105
- `Skill file not found: ${sourcePath}`,
106
- 'Make sure tmux-team is properly installed.'
107
- );
108
- }
109
-
110
- // Determine target directory
111
- const targetDir = scopeLower === 'user' ? config.userDir : path.resolve(config.localDir);
112
- const targetPath = path.join(targetDir, config.targetFile);
113
-
114
- // Check if already exists
115
- if (fs.existsSync(targetPath) && !ctx.flags.force) {
116
- exitWithError(ctx, `Skill already exists: ${targetPath}`, 'Use --force to overwrite.');
117
- }
118
-
119
- // Create directory if needed
120
- if (!fs.existsSync(targetDir)) {
121
- fs.mkdirSync(targetDir, { recursive: true });
122
- if (ctx.flags.verbose) {
123
- ctx.ui.info(`Created directory: ${targetDir}`);
124
- }
125
- }
126
-
127
- // Copy file
128
- fs.copyFileSync(sourcePath, targetPath);
129
-
130
- if (ctx.flags.json) {
131
- ctx.ui.json({
132
- success: true,
133
- agent: agentLower,
134
- scope: scopeLower,
135
- path: targetPath,
136
- });
137
- } else {
138
- ctx.ui.success(`Installed ${agentLower} skill to ${targetPath}`);
139
-
140
- // Show usage hint
141
- if (agentLower === 'claude') {
142
- ctx.ui.info('Usage: /team talk codex "message"');
143
- } else if (agentLower === 'codex') {
144
- ctx.ui.info('Enable skills: codex --enable skills');
145
- ctx.ui.info('Usage: $tmux-team or implicit invocation');
146
- }
147
- }
148
- }