ai-cli-mcp 2.12.0 → 2.14.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/.github/workflows/publish.yml +25 -0
- package/CHANGELOG.md +20 -0
- package/README.ja.md +20 -5
- package/README.md +20 -6
- package/dist/__tests__/app-cli.test.js +34 -2
- package/dist/__tests__/cli-bin-smoke.test.js +4 -0
- package/dist/__tests__/cli-builder.test.js +37 -0
- package/dist/__tests__/cli-process-service.test.js +180 -5
- package/dist/__tests__/cli-utils.test.js +31 -0
- package/dist/__tests__/mcp-contract.test.js +287 -9
- package/dist/__tests__/parsers.test.js +37 -1
- package/dist/__tests__/process-management.test.js +2 -1
- package/dist/app/cli.js +8 -6
- package/dist/app/mcp.js +16 -8
- package/dist/cli-builder.js +14 -0
- package/dist/cli-parse.js +8 -5
- package/dist/cli-process-service.js +13 -23
- package/dist/cli-utils.js +17 -0
- package/dist/cli.js +4 -3
- package/dist/model-catalog.js +4 -1
- package/dist/parsers.js +55 -0
- package/dist/process-result.js +51 -0
- package/dist/process-service.js +11 -22
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/server.json +1 -1
- package/src/__tests__/app-cli.test.ts +43 -1
- package/src/__tests__/cli-bin-smoke.test.ts +4 -0
- package/src/__tests__/cli-builder.test.ts +47 -0
- package/src/__tests__/cli-process-service.test.ts +200 -5
- package/src/__tests__/cli-utils.test.ts +34 -0
- package/src/__tests__/mcp-contract.test.ts +325 -9
- package/src/__tests__/parsers.test.ts +44 -1
- package/src/__tests__/process-management.test.ts +2 -1
- package/src/app/cli.ts +9 -7
- package/src/app/mcp.ts +17 -8
- package/src/cli-builder.ts +18 -3
- package/src/cli-parse.ts +8 -5
- package/src/cli-process-service.ts +12 -23
- package/src/cli-utils.ts +21 -1
- package/src/cli.ts +4 -3
- package/src/model-catalog.ts +5 -1
- package/src/parsers.ts +61 -0
- package/src/process-result.ts +79 -0
- package/src/process-service.ts +11 -24
- package/src/server.ts +1 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CLI_HELP_TEXT,
|
|
4
4
|
DOCTOR_HELP_TEXT,
|
|
5
5
|
MODELS_HELP_TEXT,
|
|
6
|
+
RESULT_HELP_TEXT,
|
|
6
7
|
RUN_HELP_TEXT,
|
|
7
8
|
WAIT_HELP_TEXT,
|
|
8
9
|
runCli,
|
|
@@ -142,10 +143,28 @@ describe('ai-cli app', () => {
|
|
|
142
143
|
);
|
|
143
144
|
|
|
144
145
|
expect(exitCode).toBe(0);
|
|
145
|
-
expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5);
|
|
146
|
+
expect(waitForProcesses).toHaveBeenCalledWith([123, 456], 5, false);
|
|
146
147
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"status": "completed"'));
|
|
147
148
|
});
|
|
148
149
|
|
|
150
|
+
it('passes verbose through to wait', async () => {
|
|
151
|
+
const stdout = vi.fn();
|
|
152
|
+
const stderr = vi.fn();
|
|
153
|
+
const waitForProcesses = vi.fn().mockResolvedValue([{ pid: 123, status: 'completed' }]);
|
|
154
|
+
|
|
155
|
+
const exitCode = await runCli(
|
|
156
|
+
['wait', '123', '--verbose'],
|
|
157
|
+
{
|
|
158
|
+
stdout,
|
|
159
|
+
stderr,
|
|
160
|
+
waitForProcesses,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(exitCode).toBe(0);
|
|
165
|
+
expect(waitForProcesses).toHaveBeenCalledWith([123], undefined, true);
|
|
166
|
+
});
|
|
167
|
+
|
|
149
168
|
it('rejects invalid wait timeout values', async () => {
|
|
150
169
|
const stdout = vi.fn();
|
|
151
170
|
const stderr = vi.fn();
|
|
@@ -222,6 +241,7 @@ describe('ai-cli app', () => {
|
|
|
222
241
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"aliases"'));
|
|
223
242
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"claude-ultra"'));
|
|
224
243
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"gpt-5.4"'));
|
|
244
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('"forge"'));
|
|
225
245
|
expect(stderr).not.toHaveBeenCalled();
|
|
226
246
|
});
|
|
227
247
|
|
|
@@ -247,6 +267,12 @@ describe('ai-cli app', () => {
|
|
|
247
267
|
available: true,
|
|
248
268
|
lookup: 'path',
|
|
249
269
|
},
|
|
270
|
+
forge: {
|
|
271
|
+
configuredCommand: 'forge',
|
|
272
|
+
resolvedPath: '/tmp/bin/forge',
|
|
273
|
+
available: true,
|
|
274
|
+
lookup: 'path',
|
|
275
|
+
},
|
|
250
276
|
});
|
|
251
277
|
|
|
252
278
|
const exitCode = await runCli(['doctor'], { stdout, stderr, getDoctorStatus });
|
|
@@ -280,6 +306,20 @@ describe('ai-cli app', () => {
|
|
|
280
306
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('claude-ultra'));
|
|
281
307
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gpt-5.2-codex'));
|
|
282
308
|
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('gemini-2.5-pro'));
|
|
309
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('forge'));
|
|
310
|
+
expect(stderr).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('prints detailed help for result --help', async () => {
|
|
314
|
+
const stdout = vi.fn();
|
|
315
|
+
const stderr = vi.fn();
|
|
316
|
+
|
|
317
|
+
const exitCode = await runCli(['result', '--help'], { stdout, stderr });
|
|
318
|
+
|
|
319
|
+
expect(exitCode).toBe(0);
|
|
320
|
+
expect(stdout).toHaveBeenCalledWith(RESULT_HELP_TEXT);
|
|
321
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('compact result shape'));
|
|
322
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
283
323
|
expect(stderr).not.toHaveBeenCalled();
|
|
284
324
|
});
|
|
285
325
|
|
|
@@ -291,6 +331,8 @@ describe('ai-cli app', () => {
|
|
|
291
331
|
|
|
292
332
|
expect(exitCode).toBe(0);
|
|
293
333
|
expect(stdout).toHaveBeenCalledWith(WAIT_HELP_TEXT);
|
|
334
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('compact shape'));
|
|
335
|
+
expect(stdout).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
|
|
294
336
|
expect(stderr).not.toHaveBeenCalled();
|
|
295
337
|
});
|
|
296
338
|
|
|
@@ -30,6 +30,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
30
30
|
writeExecutable(fakeBinDir, 'claude');
|
|
31
31
|
writeExecutable(fakeBinDir, 'codex');
|
|
32
32
|
writeExecutable(fakeBinDir, 'gemini');
|
|
33
|
+
writeExecutable(fakeBinDir, 'forge');
|
|
33
34
|
|
|
34
35
|
const output = execFileSync(
|
|
35
36
|
'node',
|
|
@@ -43,6 +44,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
43
44
|
CLAUDE_CLI_NAME: 'claude',
|
|
44
45
|
CODEX_CLI_NAME: 'codex',
|
|
45
46
|
GEMINI_CLI_NAME: 'gemini',
|
|
47
|
+
FORGE_CLI_NAME: 'forge',
|
|
46
48
|
},
|
|
47
49
|
}
|
|
48
50
|
);
|
|
@@ -50,6 +52,7 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
50
52
|
expect(output).toContain('"claude"');
|
|
51
53
|
expect(output).toContain('"codex"');
|
|
52
54
|
expect(output).toContain('"gemini"');
|
|
55
|
+
expect(output).toContain('"forge"');
|
|
53
56
|
expect(output).toContain('"available": true');
|
|
54
57
|
});
|
|
55
58
|
|
|
@@ -67,5 +70,6 @@ describe('ai-cli entrypoint smoke', () => {
|
|
|
67
70
|
expect(output).toContain('Usage: ai-cli run --cwd <path> [options]');
|
|
68
71
|
expect(output).toContain('--model <model>');
|
|
69
72
|
expect(output).toContain('claude-ultra');
|
|
73
|
+
expect(output).toContain('forge');
|
|
70
74
|
});
|
|
71
75
|
});
|
|
@@ -22,6 +22,7 @@ const DEFAULT_CLI_PATHS = {
|
|
|
22
22
|
claude: '/usr/bin/claude',
|
|
23
23
|
codex: '/usr/bin/codex',
|
|
24
24
|
gemini: '/usr/bin/gemini',
|
|
25
|
+
forge: '/usr/bin/forge',
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
describe('cli-builder', () => {
|
|
@@ -97,6 +98,12 @@ describe('cli-builder', () => {
|
|
|
97
98
|
'reasoning_effort is only supported for Claude and Codex models.'
|
|
98
99
|
);
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
it('should reject reasoning_effort for forge explicitly', () => {
|
|
103
|
+
expect(() => getReasoningEffort('forge', 'high')).toThrow(
|
|
104
|
+
'reasoning_effort is not supported for forge.'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
100
107
|
});
|
|
101
108
|
|
|
102
109
|
describe('buildCliCommand', () => {
|
|
@@ -401,5 +408,45 @@ describe('cli-builder', () => {
|
|
|
401
408
|
expect(cmd.resolvedModel).toBe('gemini-3.1-pro-preview');
|
|
402
409
|
});
|
|
403
410
|
});
|
|
411
|
+
|
|
412
|
+
describe('forge agent', () => {
|
|
413
|
+
it('should build forge command without model flags', () => {
|
|
414
|
+
const cmd = buildCliCommand({
|
|
415
|
+
prompt: 'test',
|
|
416
|
+
workFolder: '/tmp',
|
|
417
|
+
model: 'forge',
|
|
418
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(cmd.agent).toBe('forge');
|
|
422
|
+
expect(cmd.cliPath).toBe('/usr/bin/forge');
|
|
423
|
+
expect(cmd.resolvedModel).toBe('forge');
|
|
424
|
+
expect(cmd.args).toEqual(['-C', '/tmp', '-p', 'test']);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should map session_id to --conversation-id for forge', () => {
|
|
428
|
+
const cmd = buildCliCommand({
|
|
429
|
+
prompt: 'test',
|
|
430
|
+
workFolder: '/tmp',
|
|
431
|
+
model: 'forge',
|
|
432
|
+
session_id: 'forge-conv-123',
|
|
433
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(cmd.args).toEqual(['-C', '/tmp', '--conversation-id', 'forge-conv-123', '-p', 'test']);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should reject reasoning_effort for forge in command building', () => {
|
|
440
|
+
expect(() =>
|
|
441
|
+
buildCliCommand({
|
|
442
|
+
prompt: 'test',
|
|
443
|
+
workFolder: '/tmp',
|
|
444
|
+
model: 'forge',
|
|
445
|
+
reasoning_effort: 'high',
|
|
446
|
+
cliPaths: DEFAULT_CLI_PATHS,
|
|
447
|
+
})
|
|
448
|
+
).toThrow('reasoning_effort is not supported for forge.');
|
|
449
|
+
});
|
|
450
|
+
});
|
|
404
451
|
});
|
|
405
452
|
});
|
|
@@ -65,6 +65,7 @@ describe('CliProcessService', () => {
|
|
|
65
65
|
claude: scriptPath,
|
|
66
66
|
codex: scriptPath,
|
|
67
67
|
gemini: scriptPath,
|
|
68
|
+
forge: scriptPath,
|
|
68
69
|
},
|
|
69
70
|
});
|
|
70
71
|
|
|
@@ -83,8 +84,18 @@ describe('CliProcessService', () => {
|
|
|
83
84
|
|
|
84
85
|
const waitResult = await service.waitForProcesses([runResult.pid], 5);
|
|
85
86
|
expect(waitResult).toHaveLength(1);
|
|
86
|
-
expect(waitResult[0]
|
|
87
|
-
|
|
87
|
+
expect(waitResult[0]).toMatchObject({
|
|
88
|
+
pid: runResult.pid,
|
|
89
|
+
agent: 'claude',
|
|
90
|
+
status: 'completed',
|
|
91
|
+
exitCode: null,
|
|
92
|
+
model: 'sonnet',
|
|
93
|
+
stdout: expect.any(String),
|
|
94
|
+
stderr: expect.any(String),
|
|
95
|
+
});
|
|
96
|
+
expect(waitResult[0]).not.toHaveProperty('startTime');
|
|
97
|
+
expect(waitResult[0]).not.toHaveProperty('workFolder');
|
|
98
|
+
expect(waitResult[0]).not.toHaveProperty('prompt');
|
|
88
99
|
|
|
89
100
|
const listed = await service.listProcesses();
|
|
90
101
|
expect(listed).toContainEqual({
|
|
@@ -94,12 +105,141 @@ describe('CliProcessService', () => {
|
|
|
94
105
|
});
|
|
95
106
|
|
|
96
107
|
const result = await service.getProcessResult(runResult.pid, false);
|
|
97
|
-
expect(result
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
expect(result).toMatchObject({
|
|
109
|
+
pid: runResult.pid,
|
|
110
|
+
agent: 'claude',
|
|
111
|
+
status: 'completed',
|
|
112
|
+
exitCode: null,
|
|
113
|
+
model: 'sonnet',
|
|
114
|
+
stdout: expect.stringContaining('Command executed successfully'),
|
|
115
|
+
stderr: expect.any(String),
|
|
116
|
+
});
|
|
117
|
+
expect(result).not.toHaveProperty('startTime');
|
|
118
|
+
expect(result).not.toHaveProperty('workFolder');
|
|
119
|
+
expect(result).not.toHaveProperty('prompt');
|
|
100
120
|
expect(readFileSync(join(processDir, 'meta.json'), 'utf-8')).toContain('"status": "completed"');
|
|
101
121
|
});
|
|
102
122
|
|
|
123
|
+
it('returns compact results by default and full results when verbose is true', async () => {
|
|
124
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
125
|
+
tempDirs.push(root);
|
|
126
|
+
const scriptPath = join(root, 'mock-claude-json');
|
|
127
|
+
writeFileSync(
|
|
128
|
+
scriptPath,
|
|
129
|
+
`#!/bin/bash
|
|
130
|
+
printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"tool-1","name":"Read","input":{"file_path":"/tmp/demo.txt"}}]}}'
|
|
131
|
+
printf '%s\n' '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tool-1","content":[{"type":"text","text":"demo output"}]}]}}'
|
|
132
|
+
printf '%s\n' '{"type":"result","result":"Completed cli-process-service test"}'
|
|
133
|
+
printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
|
|
134
|
+
`
|
|
135
|
+
);
|
|
136
|
+
chmodSync(scriptPath, 0o755);
|
|
137
|
+
const stateDir = join(root, 'state');
|
|
138
|
+
const workFolder = join(root, 'work');
|
|
139
|
+
mkdirSync(workFolder, { recursive: true });
|
|
140
|
+
|
|
141
|
+
const service = new CliProcessService({
|
|
142
|
+
stateDir,
|
|
143
|
+
cliPaths: {
|
|
144
|
+
claude: scriptPath,
|
|
145
|
+
codex: scriptPath,
|
|
146
|
+
gemini: scriptPath,
|
|
147
|
+
forge: scriptPath,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const runResult = await service.startProcess({
|
|
152
|
+
prompt: 'hello structured output',
|
|
153
|
+
cwd: workFolder,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const compactWait = await service.waitForProcesses([runResult.pid], 5);
|
|
157
|
+
expect(compactWait).toHaveLength(1);
|
|
158
|
+
expect(compactWait[0]).toMatchObject({
|
|
159
|
+
pid: runResult.pid,
|
|
160
|
+
agent: 'claude',
|
|
161
|
+
status: 'completed',
|
|
162
|
+
exitCode: null,
|
|
163
|
+
model: null,
|
|
164
|
+
session_id: 'session-cli-1',
|
|
165
|
+
agentOutput: {
|
|
166
|
+
message: 'Completed cli-process-service test',
|
|
167
|
+
session_id: 'session-cli-1',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(compactWait[0]).not.toHaveProperty('startTime');
|
|
171
|
+
expect(compactWait[0]).not.toHaveProperty('workFolder');
|
|
172
|
+
expect(compactWait[0]).not.toHaveProperty('prompt');
|
|
173
|
+
expect(compactWait[0].agentOutput).not.toHaveProperty('tools');
|
|
174
|
+
|
|
175
|
+
const compactResult = await service.getProcessResult(runResult.pid, false);
|
|
176
|
+
expect(compactResult).toMatchObject({
|
|
177
|
+
pid: runResult.pid,
|
|
178
|
+
agent: 'claude',
|
|
179
|
+
status: 'completed',
|
|
180
|
+
exitCode: null,
|
|
181
|
+
model: null,
|
|
182
|
+
session_id: 'session-cli-1',
|
|
183
|
+
agentOutput: {
|
|
184
|
+
message: 'Completed cli-process-service test',
|
|
185
|
+
session_id: 'session-cli-1',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
expect(compactResult).not.toHaveProperty('startTime');
|
|
189
|
+
expect(compactResult).not.toHaveProperty('workFolder');
|
|
190
|
+
expect(compactResult).not.toHaveProperty('prompt');
|
|
191
|
+
expect(compactResult.agentOutput).not.toHaveProperty('tools');
|
|
192
|
+
|
|
193
|
+
const verboseWait = await service.waitForProcesses([runResult.pid], 5, true);
|
|
194
|
+
expect(verboseWait).toHaveLength(1);
|
|
195
|
+
expect(verboseWait[0]).toMatchObject({
|
|
196
|
+
pid: runResult.pid,
|
|
197
|
+
agent: 'claude',
|
|
198
|
+
status: 'completed',
|
|
199
|
+
exitCode: null,
|
|
200
|
+
model: null,
|
|
201
|
+
startTime: expect.any(String),
|
|
202
|
+
workFolder,
|
|
203
|
+
prompt: 'hello structured output',
|
|
204
|
+
session_id: 'session-cli-1',
|
|
205
|
+
agentOutput: {
|
|
206
|
+
message: 'Completed cli-process-service test',
|
|
207
|
+
session_id: 'session-cli-1',
|
|
208
|
+
tools: [
|
|
209
|
+
{
|
|
210
|
+
tool: 'Read',
|
|
211
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
212
|
+
output: 'demo output',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const verboseResult = await service.getProcessResult(runResult.pid, true);
|
|
219
|
+
expect(verboseResult).toMatchObject({
|
|
220
|
+
pid: runResult.pid,
|
|
221
|
+
agent: 'claude',
|
|
222
|
+
status: 'completed',
|
|
223
|
+
exitCode: null,
|
|
224
|
+
model: null,
|
|
225
|
+
startTime: expect.any(String),
|
|
226
|
+
workFolder,
|
|
227
|
+
prompt: 'hello structured output',
|
|
228
|
+
session_id: 'session-cli-1',
|
|
229
|
+
agentOutput: {
|
|
230
|
+
message: 'Completed cli-process-service test',
|
|
231
|
+
session_id: 'session-cli-1',
|
|
232
|
+
tools: [
|
|
233
|
+
{
|
|
234
|
+
tool: 'Read',
|
|
235
|
+
input: { file_path: '/tmp/demo.txt' },
|
|
236
|
+
output: 'demo output',
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
103
243
|
it('can terminate a tracked process', async () => {
|
|
104
244
|
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
105
245
|
tempDirs.push(root);
|
|
@@ -114,6 +254,7 @@ describe('CliProcessService', () => {
|
|
|
114
254
|
claude: scriptPath,
|
|
115
255
|
codex: scriptPath,
|
|
116
256
|
gemini: scriptPath,
|
|
257
|
+
forge: scriptPath,
|
|
117
258
|
},
|
|
118
259
|
});
|
|
119
260
|
|
|
@@ -152,6 +293,7 @@ describe('CliProcessService', () => {
|
|
|
152
293
|
claude: '/bin/sh',
|
|
153
294
|
codex: '/bin/sh',
|
|
154
295
|
gemini: '/bin/sh',
|
|
296
|
+
forge: '/bin/sh',
|
|
155
297
|
},
|
|
156
298
|
});
|
|
157
299
|
|
|
@@ -254,6 +396,7 @@ describe('CliProcessService', () => {
|
|
|
254
396
|
claude: '/bin/sh',
|
|
255
397
|
codex: '/bin/sh',
|
|
256
398
|
gemini: '/bin/sh',
|
|
399
|
+
forge: '/bin/sh',
|
|
257
400
|
},
|
|
258
401
|
});
|
|
259
402
|
|
|
@@ -275,4 +418,56 @@ describe('CliProcessService', () => {
|
|
|
275
418
|
expect(existsSync(failedDir)).toBe(false);
|
|
276
419
|
killSpy.mockRestore();
|
|
277
420
|
});
|
|
421
|
+
|
|
422
|
+
it('parses forge output from detached process logs', async () => {
|
|
423
|
+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
|
|
424
|
+
tempDirs.push(root);
|
|
425
|
+
const stateDir = join(root, 'state');
|
|
426
|
+
const workFolder = join(root, 'forge-project');
|
|
427
|
+
mkdirSync(workFolder, { recursive: true });
|
|
428
|
+
const pid = 54321;
|
|
429
|
+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
|
|
430
|
+
mkdirSync(processDir, { recursive: true });
|
|
431
|
+
|
|
432
|
+
writeFileSync(
|
|
433
|
+
join(processDir, 'stdout.log'),
|
|
434
|
+
`● [21:09:01] Initialize forge-conv-1
|
|
435
|
+
Forge assistant reply
|
|
436
|
+
● [21:09:08] Finished forge-conv-1
|
|
437
|
+
`
|
|
438
|
+
);
|
|
439
|
+
writeFileSync(join(processDir, 'stderr.log'), '');
|
|
440
|
+
writeFileSync(
|
|
441
|
+
join(processDir, 'meta.json'),
|
|
442
|
+
JSON.stringify({
|
|
443
|
+
pid,
|
|
444
|
+
prompt: 'hello forge',
|
|
445
|
+
workFolder,
|
|
446
|
+
model: 'forge',
|
|
447
|
+
toolType: 'forge',
|
|
448
|
+
startTime: new Date().toISOString(),
|
|
449
|
+
stdoutPath: join(processDir, 'stdout.log'),
|
|
450
|
+
stderrPath: join(processDir, 'stderr.log'),
|
|
451
|
+
status: 'completed',
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const service = new CliProcessService({
|
|
456
|
+
stateDir,
|
|
457
|
+
cliPaths: {
|
|
458
|
+
claude: '/bin/sh',
|
|
459
|
+
codex: '/bin/sh',
|
|
460
|
+
gemini: '/bin/sh',
|
|
461
|
+
forge: '/bin/sh',
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const result = await service.getProcessResult(pid, false);
|
|
466
|
+
expect(result.agent).toBe('forge');
|
|
467
|
+
expect(result.session_id).toBe('forge-conv-1');
|
|
468
|
+
expect(result.agentOutput).toEqual({
|
|
469
|
+
message: 'Forge assistant reply',
|
|
470
|
+
session_id: 'forge-conv-1',
|
|
471
|
+
});
|
|
472
|
+
});
|
|
278
473
|
});
|
|
@@ -19,6 +19,7 @@ describe('cli-utils doctor status', () => {
|
|
|
19
19
|
delete process.env.CLAUDE_CLI_NAME;
|
|
20
20
|
delete process.env.CODEX_CLI_NAME;
|
|
21
21
|
delete process.env.GEMINI_CLI_NAME;
|
|
22
|
+
delete process.env.FORGE_CLI_NAME;
|
|
22
23
|
process.env.PATH = '/mock/bin:/usr/bin';
|
|
23
24
|
});
|
|
24
25
|
|
|
@@ -44,6 +45,12 @@ describe('cli-utils doctor status', () => {
|
|
|
44
45
|
available: true,
|
|
45
46
|
lookup: 'path',
|
|
46
47
|
});
|
|
48
|
+
expect(status.forge).toEqual({
|
|
49
|
+
configuredCommand: 'forge',
|
|
50
|
+
resolvedPath: null,
|
|
51
|
+
available: false,
|
|
52
|
+
lookup: 'path',
|
|
53
|
+
});
|
|
47
54
|
});
|
|
48
55
|
|
|
49
56
|
it('does not mark non-executable PATH entries as available', async () => {
|
|
@@ -60,6 +67,12 @@ describe('cli-utils doctor status', () => {
|
|
|
60
67
|
available: false,
|
|
61
68
|
lookup: 'path',
|
|
62
69
|
});
|
|
70
|
+
expect(status.forge).toEqual({
|
|
71
|
+
configuredCommand: 'forge',
|
|
72
|
+
resolvedPath: null,
|
|
73
|
+
available: false,
|
|
74
|
+
lookup: 'path',
|
|
75
|
+
});
|
|
63
76
|
});
|
|
64
77
|
|
|
65
78
|
it('reports invalid relative env paths as doctor errors', async () => {
|
|
@@ -129,4 +142,25 @@ describe('cli-utils doctor status', () => {
|
|
|
129
142
|
lookup: 'env',
|
|
130
143
|
});
|
|
131
144
|
});
|
|
145
|
+
|
|
146
|
+
it('supports forge lookup via FORGE_CLI_NAME', async () => {
|
|
147
|
+
process.env.FORGE_CLI_NAME = 'forge-custom';
|
|
148
|
+
mockAccessSync.mockImplementation((filePath) => {
|
|
149
|
+
if (filePath === '/mock/bin/forge-custom') {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
throw new Error('not executable');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { getCliDoctorStatus, findForgeCli } = await import('../cli-utils.js');
|
|
156
|
+
const status = getCliDoctorStatus();
|
|
157
|
+
|
|
158
|
+
expect(status.forge).toEqual({
|
|
159
|
+
configuredCommand: 'forge-custom',
|
|
160
|
+
resolvedPath: '/mock/bin/forge-custom',
|
|
161
|
+
available: true,
|
|
162
|
+
lookup: 'env',
|
|
163
|
+
});
|
|
164
|
+
expect(findForgeCli()).toBe('forge-custom');
|
|
165
|
+
});
|
|
132
166
|
});
|