@ai-devkit/agent-manager 0.18.0 → 0.19.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/dist/__tests__/terminal/TerminalFocusManager.test.js +73 -0
- package/dist/__tests__/terminal/TerminalFocusManager.test.js.map +1 -0
- package/dist/__tests__/utils/agents.test.js +17 -0
- package/dist/__tests__/utils/agents.test.js.map +1 -0
- package/dist/terminal/TerminalFocusManager.d.ts +1 -0
- package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +10 -9
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.d.ts.map +1 -1
- package/dist/utils/agents.js +29 -3
- package/dist/utils/agents.js.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/terminal/TerminalFocusManager.test.ts +92 -0
- package/src/__tests__/utils/agents.test.ts +17 -0
- package/src/terminal/TerminalFocusManager.ts +11 -3
- package/src/utils/agents.ts +22 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { TerminalFocusManager, TerminalType } from '../../terminal/TerminalFocusManager.js';
|
|
3
|
+
import { getProcessTty } from '../../utils/process.js';
|
|
4
|
+
vi.mock('child_process', ()=>({
|
|
5
|
+
execFile: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../../utils/process.js', async ()=>{
|
|
8
|
+
const actual = await vi.importActual('../../utils/process.js');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
getProcessTty: vi.fn()
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
const mockedExecFile = execFile;
|
|
15
|
+
const mockedGetProcessTty = getProcessTty;
|
|
16
|
+
function setExecFileHandler(handler) {
|
|
17
|
+
mockedExecFile.mockImplementation((cmd, args, cb)=>{
|
|
18
|
+
const result = handler(cmd, args);
|
|
19
|
+
if (result instanceof Error) cb(result);
|
|
20
|
+
else cb(null, {
|
|
21
|
+
stdout: result,
|
|
22
|
+
stderr: ''
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
describe('TerminalFocusManager', ()=>{
|
|
27
|
+
beforeEach(()=>{
|
|
28
|
+
mockedExecFile.mockReset();
|
|
29
|
+
mockedGetProcessTty.mockReset();
|
|
30
|
+
mockedGetProcessTty.mockReturnValue('ttys000');
|
|
31
|
+
});
|
|
32
|
+
it('finds iTerm2 when the process is listed by full app binary path', async ()=>{
|
|
33
|
+
setExecFileHandler((cmd, args)=>{
|
|
34
|
+
if (cmd === 'tmux') return new Error('tmux not running');
|
|
35
|
+
if (cmd === 'pgrep') return new Error('pgrep did not match GUI app');
|
|
36
|
+
if (cmd === 'ps' && args.join(' ') === '-Axo comm') {
|
|
37
|
+
return '/Applications/iTerm.app/Contents/MacOS/iTerm2\n';
|
|
38
|
+
}
|
|
39
|
+
if (cmd === 'osascript') return 'found\n';
|
|
40
|
+
return '';
|
|
41
|
+
});
|
|
42
|
+
const location = await new TerminalFocusManager().findTerminal(123);
|
|
43
|
+
expect(location).toEqual({
|
|
44
|
+
type: TerminalType.ITERM2,
|
|
45
|
+
identifier: '/dev/ttys000',
|
|
46
|
+
tty: '/dev/ttys000'
|
|
47
|
+
});
|
|
48
|
+
expect(mockedExecFile).not.toHaveBeenCalledWith('pgrep', expect.any(Array), expect.any(Function));
|
|
49
|
+
});
|
|
50
|
+
it('finds Terminal.app when the process is listed by app bundle path', async ()=>{
|
|
51
|
+
setExecFileHandler((cmd, args)=>{
|
|
52
|
+
if (cmd === 'tmux') return new Error('tmux not running');
|
|
53
|
+
if (cmd === 'pgrep') return new Error('pgrep did not match GUI app');
|
|
54
|
+
if (cmd === 'ps' && args.join(' ') === '-Axo comm') {
|
|
55
|
+
return '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal\n';
|
|
56
|
+
}
|
|
57
|
+
if (cmd === 'osascript') {
|
|
58
|
+
const script = args[1] ?? '';
|
|
59
|
+
return script.includes('tell application "Terminal"') ? 'found\n' : '';
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
62
|
+
});
|
|
63
|
+
const location = await new TerminalFocusManager().findTerminal(123);
|
|
64
|
+
expect(location).toEqual({
|
|
65
|
+
type: TerminalType.TERMINAL_APP,
|
|
66
|
+
identifier: '/dev/ttys000',
|
|
67
|
+
tty: '/dev/ttys000'
|
|
68
|
+
});
|
|
69
|
+
expect(mockedExecFile).not.toHaveBeenCalledWith('pgrep', expect.any(Array), expect.any(Function));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=TerminalFocusManager.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/terminal/TerminalFocusManager.test.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport type { MockedFunction } from 'vitest';\n\nimport { TerminalFocusManager, TerminalType } from '../../terminal/TerminalFocusManager.js';\nimport { getProcessTty } from '../../utils/process.js';\n\nvi.mock('child_process', () => ({\n execFile: vi.fn(),\n}));\n\nvi.mock('../../utils/process.js', async () => {\n const actual = await vi.importActual<typeof import('../../utils/process.js')>('../../utils/process.js');\n return {\n ...actual,\n getProcessTty: vi.fn(),\n };\n});\n\ntype ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;\nconst mockedExecFile = execFile as unknown as MockedFunction<\n (cmd: string, args: string[], cb: ExecFileCb) => void\n>;\nconst mockedGetProcessTty = getProcessTty as MockedFunction<typeof getProcessTty>;\n\nfunction setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {\n mockedExecFile.mockImplementation((cmd, args, cb) => {\n const result = handler(cmd, args);\n if (result instanceof Error) cb(result);\n else cb(null, { stdout: result, stderr: '' });\n });\n}\n\ndescribe('TerminalFocusManager', () => {\n beforeEach(() => {\n mockedExecFile.mockReset();\n mockedGetProcessTty.mockReset();\n mockedGetProcessTty.mockReturnValue('ttys000');\n });\n\n it('finds iTerm2 when the process is listed by full app binary path', async () => {\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return new Error('tmux not running');\n if (cmd === 'pgrep') return new Error('pgrep did not match GUI app');\n if (cmd === 'ps' && args.join(' ') === '-Axo comm') {\n return '/Applications/iTerm.app/Contents/MacOS/iTerm2\\n';\n }\n if (cmd === 'osascript') return 'found\\n';\n return '';\n });\n\n const location = await new TerminalFocusManager().findTerminal(123);\n\n expect(location).toEqual({\n type: TerminalType.ITERM2,\n identifier: '/dev/ttys000',\n tty: '/dev/ttys000',\n });\n expect(mockedExecFile).not.toHaveBeenCalledWith(\n 'pgrep',\n expect.any(Array),\n expect.any(Function),\n );\n });\n\n it('finds Terminal.app when the process is listed by app bundle path', async () => {\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return new Error('tmux not running');\n if (cmd === 'pgrep') return new Error('pgrep did not match GUI app');\n if (cmd === 'ps' && args.join(' ') === '-Axo comm') {\n return '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal\\n';\n }\n if (cmd === 'osascript') {\n const script = args[1] ?? '';\n return script.includes('tell application \"Terminal\"') ? 'found\\n' : '';\n }\n return '';\n });\n\n const location = await new TerminalFocusManager().findTerminal(123);\n\n expect(location).toEqual({\n type: TerminalType.TERMINAL_APP,\n identifier: '/dev/ttys000',\n tty: '/dev/ttys000',\n });\n expect(mockedExecFile).not.toHaveBeenCalledWith(\n 'pgrep',\n expect.any(Array),\n expect.any(Function),\n );\n });\n});\n"],"names":["execFile","TerminalFocusManager","TerminalType","getProcessTty","vi","mock","fn","actual","importActual","mockedExecFile","mockedGetProcessTty","setExecFileHandler","handler","mockImplementation","cmd","args","cb","result","Error","stdout","stderr","describe","beforeEach","mockReset","mockReturnValue","it","join","location","findTerminal","expect","toEqual","type","ITERM2","identifier","tty","not","toHaveBeenCalledWith","any","Array","Function","script","includes","TERMINAL_APP"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,gBAAgB;AAGzC,SAASC,oBAAoB,EAAEC,YAAY,QAAQ,yCAAyC;AAC5F,SAASC,aAAa,QAAQ,yBAAyB;AAEvDC,GAAGC,IAAI,CAAC,iBAAiB,IAAO,CAAA;QAC5BL,UAAUI,GAAGE,EAAE;IACnB,CAAA;AAEAF,GAAGC,IAAI,CAAC,0BAA0B;IAC9B,MAAME,SAAS,MAAMH,GAAGI,YAAY,CAA0C;IAC9E,OAAO;QACH,GAAGD,MAAM;QACTJ,eAAeC,GAAGE,EAAE;IACxB;AACJ;AAGA,MAAMG,iBAAiBT;AAGvB,MAAMU,sBAAsBP;AAE5B,SAASQ,mBAAmBC,OAAwD;IAChFH,eAAeI,kBAAkB,CAAC,CAACC,KAAKC,MAAMC;QAC1C,MAAMC,SAASL,QAAQE,KAAKC;QAC5B,IAAIE,kBAAkBC,OAAOF,GAAGC;aAC3BD,GAAG,MAAM;YAAEG,QAAQF;YAAQG,QAAQ;QAAG;IAC/C;AACJ;AAEAC,SAAS,wBAAwB;IAC7BC,WAAW;QACPb,eAAec,SAAS;QACxBb,oBAAoBa,SAAS;QAC7Bb,oBAAoBc,eAAe,CAAC;IACxC;IAEAC,GAAG,mEAAmE;QAClEd,mBAAmB,CAACG,KAAKC;YACrB,IAAID,QAAQ,QAAQ,OAAO,IAAII,MAAM;YACrC,IAAIJ,QAAQ,SAAS,OAAO,IAAII,MAAM;YACtC,IAAIJ,QAAQ,QAAQC,KAAKW,IAAI,CAAC,SAAS,aAAa;gBAChD,OAAO;YACX;YACA,IAAIZ,QAAQ,aAAa,OAAO;YAChC,OAAO;QACX;QAEA,MAAMa,WAAW,MAAM,IAAI1B,uBAAuB2B,YAAY,CAAC;QAE/DC,OAAOF,UAAUG,OAAO,CAAC;YACrBC,MAAM7B,aAAa8B,MAAM;YACzBC,YAAY;YACZC,KAAK;QACT;QACAL,OAAOpB,gBAAgB0B,GAAG,CAACC,oBAAoB,CAC3C,SACAP,OAAOQ,GAAG,CAACC,QACXT,OAAOQ,GAAG,CAACE;IAEnB;IAEAd,GAAG,oEAAoE;QACnEd,mBAAmB,CAACG,KAAKC;YACrB,IAAID,QAAQ,QAAQ,OAAO,IAAII,MAAM;YACrC,IAAIJ,QAAQ,SAAS,OAAO,IAAII,MAAM;YACtC,IAAIJ,QAAQ,QAAQC,KAAKW,IAAI,CAAC,SAAS,aAAa;gBAChD,OAAO;YACX;YACA,IAAIZ,QAAQ,aAAa;gBACrB,MAAM0B,SAASzB,IAAI,CAAC,EAAE,IAAI;gBAC1B,OAAOyB,OAAOC,QAAQ,CAAC,iCAAiC,YAAY;YACxE;YACA,OAAO;QACX;QAEA,MAAMd,WAAW,MAAM,IAAI1B,uBAAuB2B,YAAY,CAAC;QAE/DC,OAAOF,UAAUG,OAAO,CAAC;YACrBC,MAAM7B,aAAawC,YAAY;YAC/BT,YAAY;YACZC,KAAK;QACT;QACAL,OAAOpB,gBAAgB0B,GAAG,CAACC,oBAAoB,CAC3C,SACAP,OAAOQ,GAAG,CAACC,QACXT,OAAOQ,GAAG,CAACE;IAEnB;AACJ"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AGENTS } from '../../../src/utils/agents.js';
|
|
3
|
+
describe('AGENTS', ()=>{
|
|
4
|
+
it('includes Copilot as a startable agent', ()=>{
|
|
5
|
+
expect(AGENTS.copilot.command).toBe('copilot');
|
|
6
|
+
expect(AGENTS.copilot.matches('/opt/homebrew/Caskroom/copilot-cli/1.0.60/copilot')).toBe(true);
|
|
7
|
+
expect(AGENTS.copilot.matches('node /repo/feature-cli-copilot-cli/script.js')).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
it('includes Pi as a startable agent', ()=>{
|
|
10
|
+
expect(AGENTS.pi.command).toBe('pi');
|
|
11
|
+
expect(AGENTS.pi.matches('pi')).toBe(true);
|
|
12
|
+
expect(AGENTS.pi.matches('/usr/local/bin/pi --model x')).toBe(true);
|
|
13
|
+
expect(AGENTS.pi.matches('node /repo/feature-pi-adapter/script.js')).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=agents.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/utils/agents.test.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { AGENTS } from '../../../src/utils/agents.js';\n\ndescribe('AGENTS', () => {\n it('includes Copilot as a startable agent', () => {\n expect(AGENTS.copilot.command).toBe('copilot');\n expect(AGENTS.copilot.matches('/opt/homebrew/Caskroom/copilot-cli/1.0.60/copilot')).toBe(true);\n expect(AGENTS.copilot.matches('node /repo/feature-cli-copilot-cli/script.js')).toBe(false);\n });\n\n it('includes Pi as a startable agent', () => {\n expect(AGENTS.pi.command).toBe('pi');\n expect(AGENTS.pi.matches('pi')).toBe(true);\n expect(AGENTS.pi.matches('/usr/local/bin/pi --model x')).toBe(true);\n expect(AGENTS.pi.matches('node /repo/feature-pi-adapter/script.js')).toBe(false);\n });\n});\n"],"names":["describe","expect","it","AGENTS","copilot","command","toBe","matches","pi"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAC9C,SAASC,MAAM,QAAQ,+BAA+B;AAEtDH,SAAS,UAAU;IACfE,GAAG,yCAAyC;QACxCD,OAAOE,OAAOC,OAAO,CAACC,OAAO,EAAEC,IAAI,CAAC;QACpCL,OAAOE,OAAOC,OAAO,CAACG,OAAO,CAAC,sDAAsDD,IAAI,CAAC;QACzFL,OAAOE,OAAOC,OAAO,CAACG,OAAO,CAAC,iDAAiDD,IAAI,CAAC;IACxF;IAEAJ,GAAG,oCAAoC;QACnCD,OAAOE,OAAOK,EAAE,CAACH,OAAO,EAAEC,IAAI,CAAC;QAC/BL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,OAAOD,IAAI,CAAC;QACrCL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,gCAAgCD,IAAI,CAAC;QAC9DL,OAAOE,OAAOK,EAAE,CAACD,OAAO,CAAC,4CAA4CD,IAAI,CAAC;IAC9E;AACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TerminalFocusManager.d.ts","sourceRoot":"","sources":["../../src/terminal/TerminalFocusManager.ts"],"names":[],"mappings":"AAOA,oBAAY,YAAY;IACpB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,YAAY,iBAAiB;IAC7B,OAAO,YAAY;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,oBAAoB;IAC7B;;OAEG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IA8BjE;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;YAiBnD,YAAY;YAwBZ,iBAAiB;YAsCjB,qBAAqB;YAoCrB,aAAa;YASb,kBAAkB;YAqBlB,sBAAsB;CAmBvC"}
|
|
1
|
+
{"version":3,"file":"TerminalFocusManager.d.ts","sourceRoot":"","sources":["../../src/terminal/TerminalFocusManager.ts"],"names":[],"mappings":"AAOA,oBAAY,YAAY;IACpB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,YAAY,iBAAiB;IAC7B,OAAO,YAAY;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,oBAAoB;IAC7B;;OAEG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IA8BjE;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;YAiBnD,YAAY;YAwBZ,iBAAiB;YAsCjB,qBAAqB;YAoCrB,gBAAgB;YAQhB,aAAa;YASb,kBAAkB;YAqBlB,sBAAsB;CAmBvC"}
|
|
@@ -81,11 +81,8 @@ export class TerminalFocusManager {
|
|
|
81
81
|
}
|
|
82
82
|
async findITerm2Session(tty) {
|
|
83
83
|
try {
|
|
84
|
-
// Check if iTerm2 is running first to avoid launching it
|
|
85
|
-
await
|
|
86
|
-
'-x',
|
|
87
|
-
'iTerm2'
|
|
88
|
-
]);
|
|
84
|
+
// Check if iTerm2 is running first to avoid launching it.
|
|
85
|
+
if (!await this.isProcessRunning('iTerm2')) return null;
|
|
89
86
|
} catch {
|
|
90
87
|
return null;
|
|
91
88
|
}
|
|
@@ -123,10 +120,7 @@ export class TerminalFocusManager {
|
|
|
123
120
|
async findTerminalAppWindow(tty) {
|
|
124
121
|
try {
|
|
125
122
|
// Check if Terminal.app is running
|
|
126
|
-
await
|
|
127
|
-
'-x',
|
|
128
|
-
'Terminal'
|
|
129
|
-
]);
|
|
123
|
+
if (!await this.isProcessRunning('Terminal')) return null;
|
|
130
124
|
} catch {
|
|
131
125
|
return null;
|
|
132
126
|
}
|
|
@@ -159,6 +153,13 @@ export class TerminalFocusManager {
|
|
|
159
153
|
}
|
|
160
154
|
return null;
|
|
161
155
|
}
|
|
156
|
+
async isProcessRunning(name) {
|
|
157
|
+
const { stdout } = await execFileAsync('ps', [
|
|
158
|
+
'-Axo',
|
|
159
|
+
'comm'
|
|
160
|
+
]);
|
|
161
|
+
return stdout.split('\n').map((line)=>line.trim()).some((command)=>command === name || command.endsWith(`/${name}`));
|
|
162
|
+
}
|
|
162
163
|
async focusTmuxPane(identifier) {
|
|
163
164
|
try {
|
|
164
165
|
await execFileAsync('tmux', [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/terminal/TerminalFocusManager.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { getProcessTty } from '../utils/process.js';\nimport { escapeAppleScript } from '../utils/applescript.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport enum TerminalType {\n TMUX = 'tmux',\n ITERM2 = 'iterm2',\n TERMINAL_APP = 'terminal-app',\n UNKNOWN = 'unknown',\n}\n\nexport interface TerminalLocation {\n type: TerminalType;\n identifier: string; // e.g., \"session:window.pane\" for tmux, or TTY for others\n tty: string; // e.g., \"/dev/ttys030\"\n}\n\nexport class TerminalFocusManager {\n /**\n * Find the terminal location (emulator info) for a given process ID\n */\n async findTerminal(pid: number): Promise<TerminalLocation | null> {\n const ttyShort = getProcessTty(pid);\n\n // If no TTY or invalid, we can't find the terminal\n if (!ttyShort || ttyShort === '?') {\n return null;\n }\n\n const fullTty = `/dev/${ttyShort}`;\n\n // 1. Check tmux (most specific if running inside it)\n const tmuxLocation = await this.findTmuxPane(fullTty);\n if (tmuxLocation) return tmuxLocation;\n\n // 2. Check iTerm2\n const itermLocation = await this.findITerm2Session(fullTty);\n if (itermLocation) return itermLocation;\n\n // 3. Check Terminal.app\n const terminalAppLocation = await this.findTerminalAppWindow(fullTty);\n if (terminalAppLocation) return terminalAppLocation;\n\n // 4. Fallback: we know the TTY but not the emulator wrapper\n return {\n type: TerminalType.UNKNOWN,\n identifier: '',\n tty: fullTty\n };\n }\n\n /**\n * Focus the terminal identified by the location\n */\n async focusTerminal(location: TerminalLocation): Promise<boolean> {\n try {\n switch (location.type) {\n case TerminalType.TMUX:\n return await this.focusTmuxPane(location.identifier);\n case TerminalType.ITERM2:\n return await this.focusITerm2Session(location.tty);\n case TerminalType.TERMINAL_APP:\n return await this.focusTerminalAppWindow(location.tty);\n default:\n return false;\n }\n } catch {\n return false;\n }\n }\n\n private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {\n try {\n const { stdout } = await execFileAsync('tmux', [\n 'list-panes', '-a', '-F', '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'\n ]);\n\n const lines = stdout.trim().split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n const [paneTty, identifier] = line.split('|');\n if (paneTty === tty && identifier) {\n return {\n type: TerminalType.TMUX,\n identifier,\n tty\n };\n }\n }\n } catch {\n // tmux might not be installed or running\n }\n return null;\n }\n\n private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if iTerm2 is running first to avoid launching it\n await execFileAsync('pgrep', ['-x', 'iTerm2']);\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.ITERM2,\n identifier: tty,\n tty\n };\n }\n } catch {\n // iTerm2 script failed\n }\n return null;\n }\n\n private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if Terminal.app is running\n await execFileAsync('pgrep', ['-x', 'Terminal']);\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.TERMINAL_APP,\n identifier: tty,\n tty\n };\n }\n } catch {\n // Terminal.app script failed\n }\n return null;\n }\n\n private async focusTmuxPane(identifier: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['switch-client', '-t', identifier]);\n return true;\n } catch {\n return false;\n }\n }\n\n private async focusITerm2Session(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n select s\n return \"true\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n\n private async focusTerminalAppWindow(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n set index of w to 1\n set selected tab of w to t\n return \"true\"\n end if\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n}\n"],"names":["execFile","promisify","getProcessTty","escapeAppleScript","execFileAsync","TerminalType","TerminalFocusManager","findTerminal","pid","ttyShort","fullTty","tmuxLocation","findTmuxPane","itermLocation","findITerm2Session","terminalAppLocation","findTerminalAppWindow","type","identifier","tty","focusTerminal","location","focusTmuxPane","focusITerm2Session","focusTerminalAppWindow","stdout","lines","trim","split","line","paneTty","escapedTty","script"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/terminal/TerminalFocusManager.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { getProcessTty } from '../utils/process.js';\nimport { escapeAppleScript } from '../utils/applescript.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport enum TerminalType {\n TMUX = 'tmux',\n ITERM2 = 'iterm2',\n TERMINAL_APP = 'terminal-app',\n UNKNOWN = 'unknown',\n}\n\nexport interface TerminalLocation {\n type: TerminalType;\n identifier: string; // e.g., \"session:window.pane\" for tmux, or TTY for others\n tty: string; // e.g., \"/dev/ttys030\"\n}\n\nexport class TerminalFocusManager {\n /**\n * Find the terminal location (emulator info) for a given process ID\n */\n async findTerminal(pid: number): Promise<TerminalLocation | null> {\n const ttyShort = getProcessTty(pid);\n\n // If no TTY or invalid, we can't find the terminal\n if (!ttyShort || ttyShort === '?') {\n return null;\n }\n\n const fullTty = `/dev/${ttyShort}`;\n\n // 1. Check tmux (most specific if running inside it)\n const tmuxLocation = await this.findTmuxPane(fullTty);\n if (tmuxLocation) return tmuxLocation;\n\n // 2. Check iTerm2\n const itermLocation = await this.findITerm2Session(fullTty);\n if (itermLocation) return itermLocation;\n\n // 3. Check Terminal.app\n const terminalAppLocation = await this.findTerminalAppWindow(fullTty);\n if (terminalAppLocation) return terminalAppLocation;\n\n // 4. Fallback: we know the TTY but not the emulator wrapper\n return {\n type: TerminalType.UNKNOWN,\n identifier: '',\n tty: fullTty\n };\n }\n\n /**\n * Focus the terminal identified by the location\n */\n async focusTerminal(location: TerminalLocation): Promise<boolean> {\n try {\n switch (location.type) {\n case TerminalType.TMUX:\n return await this.focusTmuxPane(location.identifier);\n case TerminalType.ITERM2:\n return await this.focusITerm2Session(location.tty);\n case TerminalType.TERMINAL_APP:\n return await this.focusTerminalAppWindow(location.tty);\n default:\n return false;\n }\n } catch {\n return false;\n }\n }\n\n private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {\n try {\n const { stdout } = await execFileAsync('tmux', [\n 'list-panes', '-a', '-F', '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'\n ]);\n\n const lines = stdout.trim().split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n const [paneTty, identifier] = line.split('|');\n if (paneTty === tty && identifier) {\n return {\n type: TerminalType.TMUX,\n identifier,\n tty\n };\n }\n }\n } catch {\n // tmux might not be installed or running\n }\n return null;\n }\n\n private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if iTerm2 is running first to avoid launching it.\n if (!await this.isProcessRunning('iTerm2')) return null;\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.ITERM2,\n identifier: tty,\n tty\n };\n }\n } catch {\n // iTerm2 script failed\n }\n return null;\n }\n\n private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {\n try {\n // Check if Terminal.app is running\n if (!await this.isProcessRunning('Terminal')) return null;\n } catch {\n return null;\n }\n\n try {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n return \"found\"\n end if\n end repeat\n end repeat\n end tell\n `;\n\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n if (stdout.trim() === \"found\") {\n return {\n type: TerminalType.TERMINAL_APP,\n identifier: tty,\n tty\n };\n }\n } catch {\n // Terminal.app script failed\n }\n return null;\n }\n\n private async isProcessRunning(name: string): Promise<boolean> {\n const { stdout } = await execFileAsync('ps', ['-Axo', 'comm']);\n return stdout\n .split('\\n')\n .map((line) => line.trim())\n .some((command) => command === name || command.endsWith(`/${name}`));\n }\n\n private async focusTmuxPane(identifier: string): Promise<boolean> {\n try {\n await execFileAsync('tmux', ['switch-client', '-t', identifier]);\n return true;\n } catch {\n return false;\n }\n }\n\n private async focusITerm2Session(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"iTerm\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n repeat with s in sessions of t\n if tty of s is \"${escapedTty}\" then\n select s\n return \"true\"\n end if\n end repeat\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n\n private async focusTerminalAppWindow(tty: string): Promise<boolean> {\n const escapedTty = escapeAppleScript(tty);\n const script = `\n tell application \"Terminal\"\n activate\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is \"${escapedTty}\" then\n set index of w to 1\n set selected tab of w to t\n return \"true\"\n end if\n end repeat\n end repeat\n end tell\n `;\n const { stdout } = await execFileAsync('osascript', ['-e', script]);\n return stdout.trim() === \"true\";\n }\n}\n"],"names":["execFile","promisify","getProcessTty","escapeAppleScript","execFileAsync","TerminalType","TerminalFocusManager","findTerminal","pid","ttyShort","fullTty","tmuxLocation","findTmuxPane","itermLocation","findITerm2Session","terminalAppLocation","findTerminalAppWindow","type","identifier","tty","focusTerminal","location","focusTmuxPane","focusITerm2Session","focusTerminalAppWindow","stdout","lines","trim","split","line","paneTty","isProcessRunning","escapedTty","script","name","map","some","command","endsWith"],"mappings":"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"}
|
package/dist/utils/agents.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentType } from '../adapters/AgentAdapter.js';
|
|
2
|
-
export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'gemini_cli' | 'opencode'>;
|
|
2
|
+
export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;
|
|
3
3
|
export interface AgentConfig {
|
|
4
4
|
/** Shell command to launch the agent (sent to tmux via `send-keys`). */
|
|
5
5
|
command: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/utils/agents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,SAAS,EAAE,QAAQ,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/utils/agents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,SAAS,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;AAEvH,MAAM,WAAW,WAAW;IACxB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3C;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,kBAAkB,EAAE,WAAW,CAO1D,CAAC"}
|
package/dist/utils/agents.js
CHANGED
|
@@ -12,13 +12,23 @@ import path from 'path';
|
|
|
12
12
|
command: 'codex',
|
|
13
13
|
matches: matchArgv0('codex')
|
|
14
14
|
},
|
|
15
|
-
|
|
16
|
-
command: '
|
|
17
|
-
matches:
|
|
15
|
+
copilot: {
|
|
16
|
+
command: 'copilot',
|
|
17
|
+
matches: matchArgv0Name('copilot-cli')
|
|
18
18
|
},
|
|
19
19
|
gemini_cli: {
|
|
20
20
|
command: 'gemini',
|
|
21
21
|
matches: matchAnyToken('gemini')
|
|
22
|
+
},
|
|
23
|
+
opencode: {
|
|
24
|
+
command: 'opencode',
|
|
25
|
+
matches: matchArgv0('opencode')
|
|
26
|
+
},
|
|
27
|
+
pi: {
|
|
28
|
+
command: 'pi',
|
|
29
|
+
matches: matchAnyBasename([
|
|
30
|
+
'pi'
|
|
31
|
+
])
|
|
22
32
|
}
|
|
23
33
|
};
|
|
24
34
|
function matchArgv0(name) {
|
|
@@ -28,6 +38,13 @@ function matchArgv0(name) {
|
|
|
28
38
|
return token ? path.basename(token).toLowerCase() === lower : false;
|
|
29
39
|
};
|
|
30
40
|
}
|
|
41
|
+
function matchArgv0Name(name) {
|
|
42
|
+
const lower = name.toLowerCase();
|
|
43
|
+
return (psCommand)=>{
|
|
44
|
+
const token = psCommand.trim().split(/\s+/)[0];
|
|
45
|
+
return token ? token.toLowerCase().includes(lower) : false;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
31
48
|
function matchAnyToken(name) {
|
|
32
49
|
const lower = name.toLowerCase();
|
|
33
50
|
return (psCommand)=>{
|
|
@@ -37,5 +54,14 @@ function matchAnyToken(name) {
|
|
|
37
54
|
return false;
|
|
38
55
|
};
|
|
39
56
|
}
|
|
57
|
+
function matchAnyBasename(names) {
|
|
58
|
+
const lowers = new Set(names.map((name)=>name.toLowerCase()));
|
|
59
|
+
return (psCommand)=>{
|
|
60
|
+
for (const token of psCommand.trim().split(/\s+/)){
|
|
61
|
+
if (lowers.has(path.basename(token).toLowerCase())) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
40
66
|
|
|
41
67
|
//# sourceMappingURL=agents.js.map
|
package/dist/utils/agents.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/agents.ts"],"sourcesContent":["import path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\n\nexport type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'gemini_cli' | 'opencode'>;\n\nexport interface AgentConfig {\n /** Shell command to launch the agent (sent to tmux via `send-keys`). */\n command: string;\n /** Returns true if the given `ps` command line belongs to this agent. */\n matches: (psCommand: string) => boolean;\n}\n\n/**\n * Per-agent configuration: launch command plus a matcher that recognizes the\n * agent's process in `ps` output. Each matcher knows that agent's distribution\n * quirks (e.g. gemini ships as a Node script so its real binary is in argv[1..]).\n */\nexport const AGENTS: Record<StartableAgentType, AgentConfig> = {\n claude: { command: 'claude', matches: matchArgv0('claude') },\n codex: { command: 'codex', matches: matchArgv0('codex') },\n
|
|
1
|
+
{"version":3,"sources":["../../src/utils/agents.ts"],"sourcesContent":["import path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\n\nexport type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;\n\nexport interface AgentConfig {\n /** Shell command to launch the agent (sent to tmux via `send-keys`). */\n command: string;\n /** Returns true if the given `ps` command line belongs to this agent. */\n matches: (psCommand: string) => boolean;\n}\n\n/**\n * Per-agent configuration: launch command plus a matcher that recognizes the\n * agent's process in `ps` output. Each matcher knows that agent's distribution\n * quirks (e.g. gemini ships as a Node script so its real binary is in argv[1..]).\n */\nexport const AGENTS: Record<StartableAgentType, AgentConfig> = {\n claude: { command: 'claude', matches: matchArgv0('claude') },\n codex: { command: 'codex', matches: matchArgv0('codex') },\n copilot: { command: 'copilot', matches: matchArgv0Name('copilot-cli') },\n gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },\n opencode: { command: 'opencode', matches: matchArgv0('opencode') },\n pi: { command: 'pi', matches: matchAnyBasename(['pi']) },\n};\n\nfunction matchArgv0(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n const token = psCommand.trim().split(/\\s+/)[0];\n return token ? path.basename(token).toLowerCase() === lower : false;\n };\n}\n\nfunction matchArgv0Name(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n const token = psCommand.trim().split(/\\s+/)[0];\n return token ? token.toLowerCase().includes(lower) : false;\n };\n}\n\nfunction matchAnyToken(name: string): (psCommand: string) => boolean {\n const lower = name.toLowerCase();\n return (psCommand) => {\n for (const token of psCommand.trim().split(/\\s+/)) {\n if (path.basename(token).toLowerCase() === lower) return true;\n }\n return false;\n };\n}\n\nfunction matchAnyBasename(names: string[]): (psCommand: string) => boolean {\n const lowers = new Set(names.map((name) => name.toLowerCase()));\n return (psCommand) => {\n for (const token of psCommand.trim().split(/\\s+/)) {\n if (lowers.has(path.basename(token).toLowerCase())) return true;\n }\n return false;\n };\n}\n"],"names":["path","AGENTS","claude","command","matches","matchArgv0","codex","copilot","matchArgv0Name","gemini_cli","matchAnyToken","opencode","pi","matchAnyBasename","name","lower","toLowerCase","psCommand","token","trim","split","basename","includes","names","lowers","Set","map","has"],"mappings":"AAAA,OAAOA,UAAU,OAAO;AAYxB;;;;CAIC,GACD,OAAO,MAAMC,SAAkD;IAC3DC,QAAY;QAAEC,SAAS;QAAYC,SAASC,WAAW;IAAU;IACjEC,OAAY;QAAEH,SAAS;QAAYC,SAASC,WAAW;IAAS;IAChEE,SAAY;QAAEJ,SAAS;QAAYC,SAASI,eAAe;IAAe;IAC1EC,YAAY;QAAEN,SAAS;QAAYC,SAASM,cAAc;IAAU;IACpEC,UAAY;QAAER,SAAS;QAAYC,SAASC,WAAW;IAAY;IACnEO,IAAY;QAAET,SAAS;QAAYC,SAASS,iBAAiB;YAAC;SAAK;IAAE;AACzE,EAAE;AAEF,SAASR,WAAWS,IAAY;IAC5B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,MAAMC,QAAQD,UAAUE,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE;QAC9C,OAAOF,QAAQlB,KAAKqB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,QAAQ;IAClE;AACJ;AAEA,SAASP,eAAeM,IAAY;IAChC,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,MAAMC,QAAQD,UAAUE,IAAI,GAAGC,KAAK,CAAC,MAAM,CAAC,EAAE;QAC9C,OAAOF,QAAQA,MAAMF,WAAW,GAAGM,QAAQ,CAACP,SAAS;IACzD;AACJ;AAEA,SAASL,cAAcI,IAAY;IAC/B,MAAMC,QAAQD,KAAKE,WAAW;IAC9B,OAAO,CAACC;QACJ,KAAK,MAAMC,SAASD,UAAUE,IAAI,GAAGC,KAAK,CAAC,OAAQ;YAC/C,IAAIpB,KAAKqB,QAAQ,CAACH,OAAOF,WAAW,OAAOD,OAAO,OAAO;QAC7D;QACA,OAAO;IACX;AACJ;AAEA,SAASF,iBAAiBU,KAAe;IACrC,MAAMC,SAAS,IAAIC,IAAIF,MAAMG,GAAG,CAAC,CAACZ,OAASA,KAAKE,WAAW;IAC3D,OAAO,CAACC;QACJ,KAAK,MAAMC,SAASD,UAAUE,IAAI,GAAGC,KAAK,CAAC,OAAQ;YAC/C,IAAII,OAAOG,GAAG,CAAC3B,KAAKqB,QAAQ,CAACH,OAAOF,WAAW,KAAK,OAAO;QAC/D;QACA,OAAO;IACX;AACJ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-devkit/agent-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Standalone agent detection and management utilities for AI DevKit",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
],
|
|
30
30
|
"author": "",
|
|
31
31
|
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/codeaholicguy/ai-devkit.git",
|
|
35
|
+
"directory": "packages/agent-manager"
|
|
36
|
+
},
|
|
32
37
|
"dependencies": {
|
|
33
38
|
"better-sqlite3": "^12.6.2",
|
|
34
39
|
"uuid": "14.0.0"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import type { MockedFunction } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { TerminalFocusManager, TerminalType } from '../../terminal/TerminalFocusManager.js';
|
|
5
|
+
import { getProcessTty } from '../../utils/process.js';
|
|
6
|
+
|
|
7
|
+
vi.mock('child_process', () => ({
|
|
8
|
+
execFile: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('../../utils/process.js', async () => {
|
|
12
|
+
const actual = await vi.importActual<typeof import('../../utils/process.js')>('../../utils/process.js');
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
getProcessTty: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;
|
|
20
|
+
const mockedExecFile = execFile as unknown as MockedFunction<
|
|
21
|
+
(cmd: string, args: string[], cb: ExecFileCb) => void
|
|
22
|
+
>;
|
|
23
|
+
const mockedGetProcessTty = getProcessTty as MockedFunction<typeof getProcessTty>;
|
|
24
|
+
|
|
25
|
+
function setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {
|
|
26
|
+
mockedExecFile.mockImplementation((cmd, args, cb) => {
|
|
27
|
+
const result = handler(cmd, args);
|
|
28
|
+
if (result instanceof Error) cb(result);
|
|
29
|
+
else cb(null, { stdout: result, stderr: '' });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('TerminalFocusManager', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockedExecFile.mockReset();
|
|
36
|
+
mockedGetProcessTty.mockReset();
|
|
37
|
+
mockedGetProcessTty.mockReturnValue('ttys000');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('finds iTerm2 when the process is listed by full app binary path', async () => {
|
|
41
|
+
setExecFileHandler((cmd, args) => {
|
|
42
|
+
if (cmd === 'tmux') return new Error('tmux not running');
|
|
43
|
+
if (cmd === 'pgrep') return new Error('pgrep did not match GUI app');
|
|
44
|
+
if (cmd === 'ps' && args.join(' ') === '-Axo comm') {
|
|
45
|
+
return '/Applications/iTerm.app/Contents/MacOS/iTerm2\n';
|
|
46
|
+
}
|
|
47
|
+
if (cmd === 'osascript') return 'found\n';
|
|
48
|
+
return '';
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const location = await new TerminalFocusManager().findTerminal(123);
|
|
52
|
+
|
|
53
|
+
expect(location).toEqual({
|
|
54
|
+
type: TerminalType.ITERM2,
|
|
55
|
+
identifier: '/dev/ttys000',
|
|
56
|
+
tty: '/dev/ttys000',
|
|
57
|
+
});
|
|
58
|
+
expect(mockedExecFile).not.toHaveBeenCalledWith(
|
|
59
|
+
'pgrep',
|
|
60
|
+
expect.any(Array),
|
|
61
|
+
expect.any(Function),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('finds Terminal.app when the process is listed by app bundle path', async () => {
|
|
66
|
+
setExecFileHandler((cmd, args) => {
|
|
67
|
+
if (cmd === 'tmux') return new Error('tmux not running');
|
|
68
|
+
if (cmd === 'pgrep') return new Error('pgrep did not match GUI app');
|
|
69
|
+
if (cmd === 'ps' && args.join(' ') === '-Axo comm') {
|
|
70
|
+
return '/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal\n';
|
|
71
|
+
}
|
|
72
|
+
if (cmd === 'osascript') {
|
|
73
|
+
const script = args[1] ?? '';
|
|
74
|
+
return script.includes('tell application "Terminal"') ? 'found\n' : '';
|
|
75
|
+
}
|
|
76
|
+
return '';
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const location = await new TerminalFocusManager().findTerminal(123);
|
|
80
|
+
|
|
81
|
+
expect(location).toEqual({
|
|
82
|
+
type: TerminalType.TERMINAL_APP,
|
|
83
|
+
identifier: '/dev/ttys000',
|
|
84
|
+
tty: '/dev/ttys000',
|
|
85
|
+
});
|
|
86
|
+
expect(mockedExecFile).not.toHaveBeenCalledWith(
|
|
87
|
+
'pgrep',
|
|
88
|
+
expect.any(Array),
|
|
89
|
+
expect.any(Function),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AGENTS } from '../../../src/utils/agents.js';
|
|
3
|
+
|
|
4
|
+
describe('AGENTS', () => {
|
|
5
|
+
it('includes Copilot as a startable agent', () => {
|
|
6
|
+
expect(AGENTS.copilot.command).toBe('copilot');
|
|
7
|
+
expect(AGENTS.copilot.matches('/opt/homebrew/Caskroom/copilot-cli/1.0.60/copilot')).toBe(true);
|
|
8
|
+
expect(AGENTS.copilot.matches('node /repo/feature-cli-copilot-cli/script.js')).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('includes Pi as a startable agent', () => {
|
|
12
|
+
expect(AGENTS.pi.command).toBe('pi');
|
|
13
|
+
expect(AGENTS.pi.matches('pi')).toBe(true);
|
|
14
|
+
expect(AGENTS.pi.matches('/usr/local/bin/pi --model x')).toBe(true);
|
|
15
|
+
expect(AGENTS.pi.matches('node /repo/feature-pi-adapter/script.js')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -98,8 +98,8 @@ export class TerminalFocusManager {
|
|
|
98
98
|
|
|
99
99
|
private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {
|
|
100
100
|
try {
|
|
101
|
-
// Check if iTerm2 is running first to avoid launching it
|
|
102
|
-
await
|
|
101
|
+
// Check if iTerm2 is running first to avoid launching it.
|
|
102
|
+
if (!await this.isProcessRunning('iTerm2')) return null;
|
|
103
103
|
} catch {
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
@@ -137,7 +137,7 @@ export class TerminalFocusManager {
|
|
|
137
137
|
private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {
|
|
138
138
|
try {
|
|
139
139
|
// Check if Terminal.app is running
|
|
140
|
-
await
|
|
140
|
+
if (!await this.isProcessRunning('Terminal')) return null;
|
|
141
141
|
} catch {
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
@@ -170,6 +170,14 @@ export class TerminalFocusManager {
|
|
|
170
170
|
return null;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
private async isProcessRunning(name: string): Promise<boolean> {
|
|
174
|
+
const { stdout } = await execFileAsync('ps', ['-Axo', 'comm']);
|
|
175
|
+
return stdout
|
|
176
|
+
.split('\n')
|
|
177
|
+
.map((line) => line.trim())
|
|
178
|
+
.some((command) => command === name || command.endsWith(`/${name}`));
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
private async focusTmuxPane(identifier: string): Promise<boolean> {
|
|
174
182
|
try {
|
|
175
183
|
await execFileAsync('tmux', ['switch-client', '-t', identifier]);
|
package/src/utils/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import type { AgentType } from '../adapters/AgentAdapter.js';
|
|
3
3
|
|
|
4
|
-
export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'gemini_cli' | 'opencode'>;
|
|
4
|
+
export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;
|
|
5
5
|
|
|
6
6
|
export interface AgentConfig {
|
|
7
7
|
/** Shell command to launch the agent (sent to tmux via `send-keys`). */
|
|
@@ -18,8 +18,10 @@ export interface AgentConfig {
|
|
|
18
18
|
export const AGENTS: Record<StartableAgentType, AgentConfig> = {
|
|
19
19
|
claude: { command: 'claude', matches: matchArgv0('claude') },
|
|
20
20
|
codex: { command: 'codex', matches: matchArgv0('codex') },
|
|
21
|
-
|
|
21
|
+
copilot: { command: 'copilot', matches: matchArgv0Name('copilot-cli') },
|
|
22
22
|
gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },
|
|
23
|
+
opencode: { command: 'opencode', matches: matchArgv0('opencode') },
|
|
24
|
+
pi: { command: 'pi', matches: matchAnyBasename(['pi']) },
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
function matchArgv0(name: string): (psCommand: string) => boolean {
|
|
@@ -30,6 +32,14 @@ function matchArgv0(name: string): (psCommand: string) => boolean {
|
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
function matchArgv0Name(name: string): (psCommand: string) => boolean {
|
|
36
|
+
const lower = name.toLowerCase();
|
|
37
|
+
return (psCommand) => {
|
|
38
|
+
const token = psCommand.trim().split(/\s+/)[0];
|
|
39
|
+
return token ? token.toLowerCase().includes(lower) : false;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
function matchAnyToken(name: string): (psCommand: string) => boolean {
|
|
34
44
|
const lower = name.toLowerCase();
|
|
35
45
|
return (psCommand) => {
|
|
@@ -39,3 +49,13 @@ function matchAnyToken(name: string): (psCommand: string) => boolean {
|
|
|
39
49
|
return false;
|
|
40
50
|
};
|
|
41
51
|
}
|
|
52
|
+
|
|
53
|
+
function matchAnyBasename(names: string[]): (psCommand: string) => boolean {
|
|
54
|
+
const lowers = new Set(names.map((name) => name.toLowerCase()));
|
|
55
|
+
return (psCommand) => {
|
|
56
|
+
for (const token of psCommand.trim().split(/\s+/)) {
|
|
57
|
+
if (lowers.has(path.basename(token).toLowerCase())) return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
}
|