agent-gear 1.0.3 → 1.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/dist/acp/bridge.d.ts +1 -0
- package/dist/acp/bridge.d.ts.map +1 -1
- package/dist/acp/bridge.js +15 -3
- package/dist/acp/bridge.js.map +1 -1
- package/dist/acp/spawner.d.ts +2 -0
- package/dist/acp/spawner.d.ts.map +1 -1
- package/dist/acp/spawner.js +38 -4
- package/dist/acp/spawner.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/port-forward.d.ts +10 -0
- package/dist/port-forward.d.ts.map +1 -0
- package/dist/port-forward.js +68 -0
- package/dist/port-forward.js.map +1 -0
- package/dist/port-forward.test.d.ts +2 -0
- package/dist/port-forward.test.d.ts.map +1 -0
- package/dist/port-forward.test.js +163 -0
- package/dist/port-forward.test.js.map +1 -0
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +32 -3
- package/dist/socketio-client.js.map +1 -1
- package/package.json +5 -1
- package/src/acp/bridge.ts +16 -3
- package/src/acp/spawner.ts +38 -5
- package/src/port-forward.test.ts +202 -0
- package/src/port-forward.ts +88 -0
- package/src/socketio-client.ts +36 -3
- package/tests/acp-bridge.test.ts +61 -0
- package/tests/acp-spawner.test.ts +96 -1
- package/tests/socketio-client.test.ts +120 -2
|
@@ -2,14 +2,25 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import { Writable, Readable } from 'node:stream';
|
|
4
4
|
import type { ChildProcess } from 'child_process';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
vi.mock('os', () => ({
|
|
8
|
+
platform: vi.fn(() => 'linux'),
|
|
9
|
+
}));
|
|
5
10
|
|
|
6
11
|
vi.mock('child_process', () => ({
|
|
7
12
|
spawn: vi.fn(),
|
|
8
13
|
execFileSync: vi.fn(() => '/usr/bin/opencode'),
|
|
9
14
|
}));
|
|
10
15
|
|
|
16
|
+
vi.mock('fs', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import('fs')>();
|
|
18
|
+
return { ...actual, existsSync: vi.fn(() => false), mkdirSync: vi.fn() };
|
|
19
|
+
});
|
|
20
|
+
|
|
11
21
|
import { AcpSpawner } from '../../src/acp/spawner.js';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
22
|
+
import { spawn, execFileSync } from 'child_process';
|
|
23
|
+
import { existsSync } from 'fs';
|
|
13
24
|
|
|
14
25
|
function createMockChildProcess(): ChildProcess & EventEmitter {
|
|
15
26
|
const proc = new EventEmitter() as ChildProcess & EventEmitter;
|
|
@@ -146,4 +157,88 @@ describe('AcpSpawner', () => {
|
|
|
146
157
|
await vi.waitFor(() => expect(resolved).toBe(true));
|
|
147
158
|
});
|
|
148
159
|
});
|
|
160
|
+
|
|
161
|
+
describe('Windows platform', () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
vi.mocked(os.platform).mockReturnValue('win32');
|
|
164
|
+
vi.mocked(execFileSync).mockReset();
|
|
165
|
+
vi.mocked(existsSync).mockReset();
|
|
166
|
+
vi.mocked(spawn).mockClear();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
afterEach(() => {
|
|
170
|
+
vi.mocked(os.platform).mockReturnValue('linux');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('resolveCommand', () => {
|
|
174
|
+
it('should use `where` instead of `which`', () => {
|
|
175
|
+
vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
|
|
176
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot', args: ['--acp', '--stdio'] });
|
|
177
|
+
sp.start();
|
|
178
|
+
expect(execFileSync).toHaveBeenCalledWith('where', ['copilot'], { encoding: 'utf8', timeout: 1000 });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return full path from `where`', () => {
|
|
182
|
+
vi.mocked(execFileSync).mockReturnValue('C:\\Users\\ash\\AppData\\Roaming\\npm\\copilot.cmd\r\n');
|
|
183
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot', args: ['--acp', '--stdio'] });
|
|
184
|
+
sp.start();
|
|
185
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
186
|
+
'C:\\Users\\ash\\AppData\\Roaming\\npm\\copilot.cmd',
|
|
187
|
+
['--acp', '--stdio'],
|
|
188
|
+
expect.objectContaining({ shell: true }),
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should fall back to PATHEXT check when `where` fails', () => {
|
|
193
|
+
vi.mocked(execFileSync).mockImplementation(() => { throw new Error('not found'); });
|
|
194
|
+
vi.mocked(existsSync).mockImplementation((p: string) => p === 'copilot.cmd');
|
|
195
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
|
|
196
|
+
sp.start();
|
|
197
|
+
expect(spawn).toHaveBeenCalledWith('copilot.cmd', expect.any(Array), expect.objectContaining({ shell: true }));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return bare command as last resort', () => {
|
|
201
|
+
vi.mocked(execFileSync).mockImplementation(() => { throw new Error('not found'); });
|
|
202
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
203
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
|
|
204
|
+
sp.start();
|
|
205
|
+
expect(spawn).toHaveBeenCalledWith('copilot', expect.any(Array), expect.objectContaining({ shell: true }));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('start', () => {
|
|
210
|
+
it('should spawn with shell: true', () => {
|
|
211
|
+
vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
|
|
212
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
|
|
213
|
+
sp.start();
|
|
214
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
215
|
+
expect.any(String),
|
|
216
|
+
expect.any(Array),
|
|
217
|
+
expect.objectContaining({ shell: true }),
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('stop', () => {
|
|
223
|
+
it('should call kill() without signal on Windows', () => {
|
|
224
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
|
|
225
|
+
vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
|
|
226
|
+
sp.start();
|
|
227
|
+
sp.stop();
|
|
228
|
+
expect(mockProcess.kill).toHaveBeenCalledWith();
|
|
229
|
+
expect(mockProcess.kill).not.toHaveBeenCalledWith('SIGTERM');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should call kill() again after 5s on Windows', () => {
|
|
233
|
+
const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
|
|
234
|
+
vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
|
|
235
|
+
sp.start();
|
|
236
|
+
sp.stop();
|
|
237
|
+
expect(mockProcess.kill).toHaveBeenCalledTimes(1);
|
|
238
|
+
vi.advanceTimersByTime(5000);
|
|
239
|
+
expect(mockProcess.kill).toHaveBeenCalledTimes(2);
|
|
240
|
+
expect(mockProcess.kill).not.toHaveBeenCalledWith('SIGKILL');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
149
244
|
});
|
|
@@ -25,11 +25,25 @@ vi.mock('../src/config.js', () => ({
|
|
|
25
25
|
|
|
26
26
|
vi.mock('os', () => ({
|
|
27
27
|
hostname: vi.fn().mockReturnValue('test-machine'),
|
|
28
|
+
platform: vi.fn(() => 'linux'),
|
|
29
|
+
homedir: vi.fn(() => '/home/user'),
|
|
28
30
|
}));
|
|
29
31
|
|
|
32
|
+
vi.mock('fs/promises', () => ({
|
|
33
|
+
readdir: vi.fn(),
|
|
34
|
+
access: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('path', async (importOriginal) => {
|
|
38
|
+
const actual = await importOriginal<typeof import('path')>();
|
|
39
|
+
return actual;
|
|
40
|
+
});
|
|
41
|
+
|
|
30
42
|
import { connectToServer, unregister } from '../src/socketio-client.js';
|
|
31
43
|
import { io } from 'socket.io-client';
|
|
32
44
|
import { clearConfigKey } from '../src/config.js';
|
|
45
|
+
import * as os from 'os';
|
|
46
|
+
import { readdir, access } from 'fs/promises';
|
|
33
47
|
|
|
34
48
|
function createMockSocket() {
|
|
35
49
|
const handlers: Record<string, (...args: unknown[]) => void> = {};
|
|
@@ -87,7 +101,7 @@ describe('SocketIO Client', () => {
|
|
|
87
101
|
expect(mockSocket.emit).toHaveBeenCalledWith('register', {
|
|
88
102
|
key: 'zag_k_1_abc123',
|
|
89
103
|
hostname: 'my-machine',
|
|
90
|
-
version: '1.0.
|
|
104
|
+
version: '1.0.4',
|
|
91
105
|
});
|
|
92
106
|
});
|
|
93
107
|
|
|
@@ -102,7 +116,7 @@ describe('SocketIO Client', () => {
|
|
|
102
116
|
expect(mockSocket.emit).toHaveBeenCalledWith('register', {
|
|
103
117
|
key: 'zag_k_1_abc123',
|
|
104
118
|
hostname: 'test-machine',
|
|
105
|
-
version: '1.0.
|
|
119
|
+
version: '1.0.4',
|
|
106
120
|
});
|
|
107
121
|
});
|
|
108
122
|
|
|
@@ -193,4 +207,108 @@ describe('SocketIO Client', () => {
|
|
|
193
207
|
expect(mockSocket.emit).toHaveBeenCalledWith('unregister', { reason: 'user_detach' });
|
|
194
208
|
});
|
|
195
209
|
});
|
|
210
|
+
|
|
211
|
+
describe('fs:browse', () => {
|
|
212
|
+
it('should list drives on Windows when path is /', async () => {
|
|
213
|
+
vi.mocked(os.platform).mockReturnValue('win32');
|
|
214
|
+
vi.mocked(access).mockImplementation(async (p: string) => {
|
|
215
|
+
if (p === 'C:\\' || p === 'D:\\') return;
|
|
216
|
+
throw new Error('not found');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const mockSocket = createMockSocket();
|
|
220
|
+
vi.mocked(io).mockReturnValue(mockSocket as never);
|
|
221
|
+
|
|
222
|
+
connectToServer('http://localhost:4001', 'zag_k_1_abc123');
|
|
223
|
+
mockSocket._trigger('connect');
|
|
224
|
+
mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
|
|
225
|
+
|
|
226
|
+
mockSocket._trigger('fs:browse', { requestId: 'req-1', path: '/' });
|
|
227
|
+
await vi.waitFor(() => {
|
|
228
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
|
|
229
|
+
requestId: 'req-1',
|
|
230
|
+
entries: [
|
|
231
|
+
{ name: 'C:', path: 'C:\\', isDirectory: true },
|
|
232
|
+
{ name: 'D:', path: 'D:\\', isDirectory: true },
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should list drives on Windows when path is empty string', async () => {
|
|
239
|
+
vi.mocked(os.platform).mockReturnValue('win32');
|
|
240
|
+
vi.mocked(access).mockImplementation(async (p: string) => {
|
|
241
|
+
if (p === 'C:\\') return;
|
|
242
|
+
throw new Error('not found');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const mockSocket = createMockSocket();
|
|
246
|
+
vi.mocked(io).mockReturnValue(mockSocket as never);
|
|
247
|
+
|
|
248
|
+
connectToServer('http://localhost:4001', 'zag_k_1_abc123');
|
|
249
|
+
mockSocket._trigger('connect');
|
|
250
|
+
mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
|
|
251
|
+
|
|
252
|
+
mockSocket._trigger('fs:browse', { requestId: 'req-2', path: '' });
|
|
253
|
+
await vi.waitFor(() => {
|
|
254
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
|
|
255
|
+
requestId: 'req-2',
|
|
256
|
+
entries: [
|
|
257
|
+
{ name: 'C:', path: 'C:\\', isDirectory: true },
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should NOT list drives on Linux when path is /', async () => {
|
|
264
|
+
vi.mocked(os.platform).mockReturnValue('linux');
|
|
265
|
+
vi.mocked(readdir).mockResolvedValue([
|
|
266
|
+
{ name: 'home', isDirectory: () => true },
|
|
267
|
+
{ name: 'usr', isDirectory: () => true },
|
|
268
|
+
] as never);
|
|
269
|
+
|
|
270
|
+
const mockSocket = createMockSocket();
|
|
271
|
+
vi.mocked(io).mockReturnValue(mockSocket as never);
|
|
272
|
+
|
|
273
|
+
connectToServer('http://localhost:4001', 'zag_k_1_abc123');
|
|
274
|
+
mockSocket._trigger('connect');
|
|
275
|
+
mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
|
|
276
|
+
|
|
277
|
+
mockSocket._trigger('fs:browse', { requestId: 'req-3', path: '/' });
|
|
278
|
+
await vi.waitFor(() => {
|
|
279
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
|
|
280
|
+
requestId: 'req-3',
|
|
281
|
+
entries: [
|
|
282
|
+
{ name: 'home', path: '/home', isDirectory: true },
|
|
283
|
+
{ name: 'usr', path: '/usr', isDirectory: true },
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should skip unavailable drives on Windows', async () => {
|
|
290
|
+
vi.mocked(os.platform).mockReturnValue('win32');
|
|
291
|
+
vi.mocked(access).mockImplementation(async (p: string) => {
|
|
292
|
+
if (p === 'C:\\') return;
|
|
293
|
+
throw new Error('not found');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const mockSocket = createMockSocket();
|
|
297
|
+
vi.mocked(io).mockReturnValue(mockSocket as never);
|
|
298
|
+
|
|
299
|
+
connectToServer('http://localhost:4001', 'zag_k_1_abc123');
|
|
300
|
+
mockSocket._trigger('connect');
|
|
301
|
+
mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
|
|
302
|
+
|
|
303
|
+
mockSocket._trigger('fs:browse', { requestId: 'req-4', path: '/' });
|
|
304
|
+
await vi.waitFor(() => {
|
|
305
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
|
|
306
|
+
requestId: 'req-4',
|
|
307
|
+
entries: [
|
|
308
|
+
{ name: 'C:', path: 'C:\\', isDirectory: true },
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
196
314
|
});
|