aws-runtime-bridge 1.6.1 → 1.6.5
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/routes/instance.d.ts +1 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +97 -1
- package/dist/routes/instance.test.js +34 -0
- package/dist/routes/pty.d.ts +2 -1
- package/dist/routes/pty.d.ts.map +1 -1
- package/dist/routes/pty.js +29 -4
- package/dist/routes/pty.test.js +9 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +163 -63
- package/dist/services/tool-installer.test.js +25 -11
- package/package.json +1 -1
|
@@ -47,5 +47,6 @@ export declare function buildToolStatusDetectionTargets(enabledTools: string[]):
|
|
|
47
47
|
* 汇总卸载后仍可执行的工具,用于避免把部分卸载误报为成功。
|
|
48
48
|
*/
|
|
49
49
|
export declare function buildUninstallFailureMessage(requestedTools: string[], toolStatus: Record<string, ToolInstallStatus>): string;
|
|
50
|
+
export declare function summarizeToolStatus(toolStatus: Record<string, ToolInstallStatus>): Record<string, Pick<ToolInstallStatus, "installed" | "version" | "executable" | "error">>;
|
|
50
51
|
export {};
|
|
51
52
|
//# sourceMappingURL=instance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iCAAiC;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,8BAA8B,CAAC;IACtC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B;AAED;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,MAAM,EAAE,GACnB,iCAAiC,CAUnC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQhF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAaR"}
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/routes/instance.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAYrD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/D,IAAI,CAEN;AAED,eAAO,MAAM,cAAc,4CAAW,CAAC;AAyBvC,wBAAgB,4BAA4B,CAAC,gBAAgB,EAAE,MAAM,GAAG;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,OAAO,CAAC;QACZ,aAAa,CAAC,EAAE,SAAS,CAAC;QAC1B,oBAAoB,EAAE,OAAO,CAAC;QAC9B,qBAAqB,EAAE,OAAO,CAAC;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAqCA;AAED,MAAM,WAAW,4BAA4B;IAC3C,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,iCAAiC;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,8BAA8B,CAAC;IACtC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,aAAa,EAAE,SAAS,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,KAAK,EACZ,0BAA0B,EAAE,MAAM,GACjC,4BAA4B,CAiB9B;AAED;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,UAAU,EAAE,MAAM,EAAE,GACnB,iCAAiC,CAUnC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQhF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,MAAM,EAAE,EACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAaR;AAED,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAC5C,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,OAAO,CAAC,CAAC,CAY3F"}
|
package/dist/routes/instance.js
CHANGED
|
@@ -132,6 +132,17 @@ export function buildUninstallFailureMessage(requestedTools, toolStatus) {
|
|
|
132
132
|
})
|
|
133
133
|
.join("; ");
|
|
134
134
|
}
|
|
135
|
+
export function summarizeToolStatus(toolStatus) {
|
|
136
|
+
return Object.fromEntries(Object.entries(toolStatus || {}).map(([tool, status]) => [
|
|
137
|
+
tool,
|
|
138
|
+
{
|
|
139
|
+
installed: Boolean(status?.installed),
|
|
140
|
+
version: status?.version || null,
|
|
141
|
+
executable: status?.executable || null,
|
|
142
|
+
error: status?.error || null,
|
|
143
|
+
},
|
|
144
|
+
]));
|
|
145
|
+
}
|
|
135
146
|
instanceRouter.get("/healthz", (_req, res) => {
|
|
136
147
|
res.json({
|
|
137
148
|
ok: true,
|
|
@@ -180,6 +191,13 @@ instanceRouter.post("/init-instance", validateToken, async (req, res) => {
|
|
|
180
191
|
return;
|
|
181
192
|
}
|
|
182
193
|
try {
|
|
194
|
+
log.info("[init-instance] Request received", {
|
|
195
|
+
agentId: String(agentId),
|
|
196
|
+
workspacePath: String(workspacePath),
|
|
197
|
+
skillEnabled,
|
|
198
|
+
mcpEnabled,
|
|
199
|
+
ccSwitchEnabledTools,
|
|
200
|
+
});
|
|
183
201
|
const result = await initInstance(agentId, workspacePath, {
|
|
184
202
|
skillEnabled,
|
|
185
203
|
mcpEnabled,
|
|
@@ -188,10 +206,22 @@ instanceRouter.post("/init-instance", validateToken, async (req, res) => {
|
|
|
188
206
|
skillPackages,
|
|
189
207
|
mcpServers,
|
|
190
208
|
});
|
|
209
|
+
log.info("[init-instance] Initialization completed", {
|
|
210
|
+
agentId: String(agentId),
|
|
211
|
+
ok: result.ok,
|
|
212
|
+
logCount: result.logs?.length || 0,
|
|
213
|
+
enabledTools: result.enabledTools,
|
|
214
|
+
});
|
|
191
215
|
res.json(result);
|
|
192
216
|
}
|
|
193
217
|
catch (error) {
|
|
194
218
|
const err = error;
|
|
219
|
+
log.error("[init-instance] Initialization failed", {
|
|
220
|
+
agentId: String(agentId),
|
|
221
|
+
workspacePath: String(workspacePath),
|
|
222
|
+
error: err?.message || String(error),
|
|
223
|
+
stack: err?.stack || null,
|
|
224
|
+
});
|
|
195
225
|
res.status(400).json({
|
|
196
226
|
error: err?.message || "initialize instance failed",
|
|
197
227
|
});
|
|
@@ -204,11 +234,23 @@ instanceRouter.post("/cc-switch/state", validateToken, async (req, res) => {
|
|
|
204
234
|
return;
|
|
205
235
|
}
|
|
206
236
|
try {
|
|
237
|
+
log.info("[cc-switch/state] Request received", { agentId: String(agentId) });
|
|
207
238
|
const sdk = await loadCcSwitchSdk(agentId);
|
|
208
239
|
const imported = await discoverCcSwitchConfiguredItems(sdk);
|
|
209
240
|
const synced = await syncLegacyStateFromSdk(agentId, sdk);
|
|
210
241
|
const state = synced.state;
|
|
211
|
-
const
|
|
242
|
+
const detectionTargets = buildToolStatusDetectionTargets(state.enabledTools || []);
|
|
243
|
+
log.info("[cc-switch/state] Detecting SDK statuses", {
|
|
244
|
+
agentId: String(agentId),
|
|
245
|
+
detectionTargets,
|
|
246
|
+
});
|
|
247
|
+
const toolStatus = await detectToolStatuses(detectionTargets);
|
|
248
|
+
log.info("[cc-switch/state] SDK status detection completed", {
|
|
249
|
+
agentId: String(agentId),
|
|
250
|
+
toolStatus: summarizeToolStatus(toolStatus),
|
|
251
|
+
importedMcpCount: imported.mcpServers,
|
|
252
|
+
importedSkillCount: imported.skills,
|
|
253
|
+
});
|
|
212
254
|
res.json({
|
|
213
255
|
ok: true,
|
|
214
256
|
agentId: String(agentId),
|
|
@@ -221,6 +263,11 @@ instanceRouter.post("/cc-switch/state", validateToken, async (req, res) => {
|
|
|
221
263
|
}
|
|
222
264
|
catch (error) {
|
|
223
265
|
const err = error;
|
|
266
|
+
log.error("[cc-switch/state] Failed to load state", {
|
|
267
|
+
agentId: String(agentId),
|
|
268
|
+
error: err?.message || String(error),
|
|
269
|
+
stack: err?.stack || null,
|
|
270
|
+
});
|
|
224
271
|
res.status(400).json({ error: err?.message || "load state failed" });
|
|
225
272
|
}
|
|
226
273
|
});
|
|
@@ -237,9 +284,23 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
|
|
|
237
284
|
.toLowerCase())
|
|
238
285
|
.filter(Boolean)
|
|
239
286
|
: [];
|
|
287
|
+
log.info("[cc-switch/install-tools] Request received", {
|
|
288
|
+
agentId: String(agentId),
|
|
289
|
+
requestedTools,
|
|
290
|
+
});
|
|
240
291
|
const supportedInstallableTools = new Set(SUPPORTED_INSTALLABLE_TOOLS);
|
|
241
292
|
const installableTools = requestedTools.filter((tool) => supportedInstallableTools.has(tool));
|
|
293
|
+
log.info("[cc-switch/install-tools] Request normalized", {
|
|
294
|
+
agentId: String(agentId),
|
|
295
|
+
requestedTools,
|
|
296
|
+
installableTools,
|
|
297
|
+
supportedInstallableTools: SUPPORTED_INSTALLABLE_TOOLS,
|
|
298
|
+
});
|
|
242
299
|
if (installableTools.length === 0) {
|
|
300
|
+
log.warn("[cc-switch/install-tools] No supported installable tools requested", {
|
|
301
|
+
agentId: String(agentId),
|
|
302
|
+
requestedTools,
|
|
303
|
+
});
|
|
243
304
|
res
|
|
244
305
|
.status(400)
|
|
245
306
|
.json({
|
|
@@ -250,8 +311,22 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
|
|
|
250
311
|
try {
|
|
251
312
|
const state = await loadInstanceState(agentId);
|
|
252
313
|
const enabledTools = Array.from(new Set([...(state.enabledTools || []), ...installableTools]));
|
|
314
|
+
log.info("[cc-switch/install-tools] Starting installer", {
|
|
315
|
+
agentId: String(agentId),
|
|
316
|
+
installableTools,
|
|
317
|
+
previousEnabledTools: state.enabledTools || [],
|
|
318
|
+
nextEnabledTools: enabledTools,
|
|
319
|
+
});
|
|
253
320
|
const toolStatus = await ensureToolsInstalled(installableTools);
|
|
321
|
+
log.info("[cc-switch/install-tools] Installer completed", {
|
|
322
|
+
agentId: String(agentId),
|
|
323
|
+
toolStatus: summarizeToolStatus(toolStatus),
|
|
324
|
+
});
|
|
254
325
|
const refreshedToolStatus = await detectToolStatuses(enabledTools);
|
|
326
|
+
log.info("[cc-switch/install-tools] Refreshed enabled SDK statuses", {
|
|
327
|
+
agentId: String(agentId),
|
|
328
|
+
refreshedToolStatus: summarizeToolStatus(refreshedToolStatus),
|
|
329
|
+
});
|
|
255
330
|
const nextToolStatus = {
|
|
256
331
|
...refreshedToolStatus,
|
|
257
332
|
...toolStatus,
|
|
@@ -261,6 +336,20 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
|
|
|
261
336
|
enabledTools,
|
|
262
337
|
toolStatus: nextToolStatus,
|
|
263
338
|
});
|
|
339
|
+
const failedTools = Object.entries(nextToolStatus)
|
|
340
|
+
.filter(([, status]) => !status.installed)
|
|
341
|
+
.map(([tool, status]) => ({ tool, error: status.error || null }));
|
|
342
|
+
if (failedTools.length > 0) {
|
|
343
|
+
log.warn("[cc-switch/install-tools] Some SDKs remain unavailable", {
|
|
344
|
+
agentId: String(agentId),
|
|
345
|
+
failedTools,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
log.info("[cc-switch/install-tools] State saved", {
|
|
349
|
+
agentId: String(agentId),
|
|
350
|
+
enabledTools: savedState.enabledTools,
|
|
351
|
+
toolStatus: summarizeToolStatus(nextToolStatus),
|
|
352
|
+
});
|
|
264
353
|
res.json({
|
|
265
354
|
ok: true,
|
|
266
355
|
agentId: String(agentId),
|
|
@@ -270,6 +359,13 @@ instanceRouter.post("/cc-switch/install-tools", validateToken, async (req, res)
|
|
|
270
359
|
}
|
|
271
360
|
catch (error) {
|
|
272
361
|
const err = error;
|
|
362
|
+
log.error("[cc-switch/install-tools] Install route failed", {
|
|
363
|
+
agentId: String(agentId),
|
|
364
|
+
requestedTools,
|
|
365
|
+
installableTools,
|
|
366
|
+
error: err?.message || String(error),
|
|
367
|
+
stack: err?.stack || null,
|
|
368
|
+
});
|
|
273
369
|
res.status(400).json({ error: err?.message || "install tools failed" });
|
|
274
370
|
}
|
|
275
371
|
});
|
|
@@ -104,6 +104,40 @@ describe('instance route validation', () => {
|
|
|
104
104
|
},
|
|
105
105
|
})).toContain('opencode: uninstall completed but command is still available');
|
|
106
106
|
});
|
|
107
|
+
it('summarizes tool status for route diagnostics', async () => {
|
|
108
|
+
const { summarizeToolStatus } = await import('./instance.js');
|
|
109
|
+
expect(summarizeToolStatus({
|
|
110
|
+
opencode: {
|
|
111
|
+
tool: 'opencode',
|
|
112
|
+
installed: false,
|
|
113
|
+
executable: null,
|
|
114
|
+
version: null,
|
|
115
|
+
installing: false,
|
|
116
|
+
error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
|
|
117
|
+
},
|
|
118
|
+
claude: {
|
|
119
|
+
tool: 'claude',
|
|
120
|
+
installed: true,
|
|
121
|
+
executable: '@anthropic-ai/claude-agent-sdk',
|
|
122
|
+
version: '0.2.87',
|
|
123
|
+
installing: false,
|
|
124
|
+
error: null,
|
|
125
|
+
},
|
|
126
|
+
})).toEqual({
|
|
127
|
+
opencode: {
|
|
128
|
+
installed: false,
|
|
129
|
+
executable: null,
|
|
130
|
+
version: null,
|
|
131
|
+
error: 'SDK package @opencode-ai/sdk is not installed in aws-runtime-bridge',
|
|
132
|
+
},
|
|
133
|
+
claude: {
|
|
134
|
+
installed: true,
|
|
135
|
+
executable: '@anthropic-ai/claude-agent-sdk',
|
|
136
|
+
version: '0.2.87',
|
|
137
|
+
error: null,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
});
|
|
107
141
|
it('does not report uninstall failure when requested tools are no longer installed', async () => {
|
|
108
142
|
const { buildUninstallFailureMessage } = await import('./instance.js');
|
|
109
143
|
expect(buildUninstallFailureMessage(['claude'], {
|
package/dist/routes/pty.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import type { IncomingMessage } from "node:http";
|
|
2
3
|
import type { Socket } from "node:net";
|
|
3
4
|
import type { Router } from "express";
|
|
@@ -38,7 +39,7 @@ export declare function resolvePtyIdleTtlMs(env?: NodeJS.ProcessEnv): number;
|
|
|
38
39
|
* 选择默认 shell。
|
|
39
40
|
* 主流程:Windows 优先 pwsh/powershell/cmd;Unix 优先 SHELL/bash/sh,返回可执行文件名与参数。
|
|
40
41
|
*/
|
|
41
|
-
export declare function resolveDefaultShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
|
|
42
|
+
export declare function resolveDefaultShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, existsSync?: (path: fs.PathLike) => boolean): {
|
|
42
43
|
shell: string;
|
|
43
44
|
args: string[];
|
|
44
45
|
};
|
package/dist/routes/pty.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAEhC,OAAO,SAA8B,MAAM,IAAI,CAAC;AAwBhD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,eAAgB,SAAQ,iBAAiB;IACjD,UAAU,EAAE,GAAG,CAAC,IAAI,CAAC;IACrB,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACjC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,eAAO,MAAM,WAAW,8BAAqC,CAAC;AAG9D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAUhF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,UAAU,GAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,KAAK,OAAuB,GACzD;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAWnC;AAqBD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,OAAO,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAuBnC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGjF;AAUD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,OAAO,EACtB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAoBR;AA2ED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,iBAAiB,CAuEpB;AAgCD,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAUlF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAUlF;AAgCD,wBAAgB,eAAe,IAAI,iBAAiB,EAAE,CAErD;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAG9E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAQzG;AA0BD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,SAAW,GAAG,OAAO,CAkBtE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,SAAa,GAAG,IAAI,CAI7D;AAED,eAAO,MAAM,SAAS,EAAE,MAAuB,CAAC;AA2HhD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAAE,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,OAAO,CAAA;CAAE,GAAG,IAAI,CA8BxK"}
|
package/dist/routes/pty.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Router as createRouter } from "express";
|
|
5
5
|
import * as pty from "node-pty";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
import WebSocket, { WebSocketServer } from "ws";
|
|
8
|
-
import {
|
|
8
|
+
import { isRuntimeTokenValid, validateToken } from "../middleware/auth.js";
|
|
9
9
|
import { createLogger } from "../utils/logger.js";
|
|
10
10
|
const log = createLogger("pty");
|
|
11
11
|
const DEFAULT_COLS = 80;
|
|
@@ -15,6 +15,15 @@ const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
|
15
15
|
const DEFAULT_EXITED_TTL_MS = 5 * 60 * 1000;
|
|
16
16
|
const DEFAULT_CONNECT_TOKEN_TTL_MS = 60 * 1000;
|
|
17
17
|
const DEFAULT_MAX_PTY_SESSIONS = 8;
|
|
18
|
+
const NON_INTERACTIVE_UNIX_SHELLS = new Set([
|
|
19
|
+
"false",
|
|
20
|
+
"halt",
|
|
21
|
+
"nologin",
|
|
22
|
+
"reboot",
|
|
23
|
+
"shutdown",
|
|
24
|
+
"sync",
|
|
25
|
+
"true",
|
|
26
|
+
]);
|
|
18
27
|
export const ptySessions = new Map();
|
|
19
28
|
const ptyConnectTokens = new Map();
|
|
20
29
|
/**
|
|
@@ -36,13 +45,29 @@ export function resolvePtyIdleTtlMs(env = process.env) {
|
|
|
36
45
|
* 选择默认 shell。
|
|
37
46
|
* 主流程:Windows 优先 pwsh/powershell/cmd;Unix 优先 SHELL/bash/sh,返回可执行文件名与参数。
|
|
38
47
|
*/
|
|
39
|
-
export function resolveDefaultShell(platform = process.platform, env = process.env) {
|
|
48
|
+
export function resolveDefaultShell(platform = process.platform, env = process.env, existsSync = fs.existsSync) {
|
|
40
49
|
if (platform === "win32") {
|
|
41
50
|
const comSpec = String(env.ComSpec || env.COMSPEC || "").trim();
|
|
42
51
|
return { shell: comSpec || "powershell.exe", args: [] };
|
|
43
52
|
}
|
|
44
53
|
const configuredShell = String(env.SHELL || "").trim();
|
|
45
|
-
|
|
54
|
+
const shell = [configuredShell, "/bin/bash", "/bin/sh"].find((candidate) => isUsableUnixShellCandidate(candidate, existsSync));
|
|
55
|
+
return { shell: shell || "/bin/sh", args: [] };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 判断 Unix shell 候选是否适合交互式 PTY。
|
|
59
|
+
* 具体逻辑:跳过 nologin/false 等会立即退出的系统 shell;绝对路径还必须真实存在。
|
|
60
|
+
*/
|
|
61
|
+
function isUsableUnixShellCandidate(candidate, existsSync) {
|
|
62
|
+
const shell = candidate.trim();
|
|
63
|
+
if (!shell) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const basename = path.basename(shell).toLowerCase();
|
|
67
|
+
if (NON_INTERACTIVE_UNIX_SHELLS.has(basename)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return !path.isAbsolute(shell) || existsSync(shell);
|
|
46
71
|
}
|
|
47
72
|
/**
|
|
48
73
|
* 校验并解析 shell 选择。
|
package/dist/routes/pty.test.js
CHANGED
|
@@ -20,6 +20,15 @@ afterEach(async () => {
|
|
|
20
20
|
ptyKill.mockClear();
|
|
21
21
|
});
|
|
22
22
|
describe('ordinary PTY persistence policy', () => {
|
|
23
|
+
it('falls back from non-interactive Linux login shells', async () => {
|
|
24
|
+
const { resolveDefaultShell } = await import('./pty.js');
|
|
25
|
+
expect(resolveDefaultShell('linux', { SHELL: '/usr/sbin/nologin' }, (candidate) => candidate === '/bin/bash')).toEqual({ shell: '/bin/bash', args: [] });
|
|
26
|
+
expect(resolveDefaultShell('linux', { SHELL: '/bin/false' }, (candidate) => candidate === '/bin/sh')).toEqual({ shell: '/bin/sh', args: [] });
|
|
27
|
+
});
|
|
28
|
+
it('falls back when Linux SHELL points to a missing absolute path', async () => {
|
|
29
|
+
const { resolveDefaultShell } = await import('./pty.js');
|
|
30
|
+
expect(resolveDefaultShell('linux', { SHELL: '/missing/shell' }, (candidate) => candidate === '/bin/bash')).toEqual({ shell: '/bin/bash', args: [] });
|
|
31
|
+
});
|
|
23
32
|
it('keeps persistent dashboard PTY sessions alive across browser disconnects', () => {
|
|
24
33
|
const source = readFileSync(resolve(currentDir, './pty.ts'), 'utf-8');
|
|
25
34
|
expect(source).toContain('persistent: boolean');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-installer.d.ts","sourceRoot":"","sources":["../../src/services/tool-installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-installer.d.ts","sourceRoot":"","sources":["../../src/services/tool-installer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAiHrD,eAAO,MAAM,2BAA2B,mBAEvC,CAAC;AAEF,eAAO,MAAM,6BAA6B,mBAIzC,CAAC;AAEF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAK/D;AAoDD,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAM5D;AAkOD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAsC5B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAkB5C;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAqF5C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAiD5C"}
|
|
@@ -1,78 +1,71 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { createLogger } from "../utils/logger.js";
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
9
|
+
const log = createLogger("tool-installer");
|
|
10
|
+
const bridgePackageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
7
11
|
const isWindows = process.platform === "win32";
|
|
12
|
+
function quoteCommandArg(value) {
|
|
13
|
+
if (isWindows) {
|
|
14
|
+
return `"${value.replaceAll('"', '\\"')}"`;
|
|
15
|
+
}
|
|
16
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
17
|
+
}
|
|
18
|
+
function npmInstallIntoBridgeCommand(packages) {
|
|
19
|
+
return `npm install --prefix ${quoteCommandArg(bridgePackageRoot)} ${packages.join(" ")}`;
|
|
20
|
+
}
|
|
21
|
+
function npmUninstallFromBridgeCommand(packages) {
|
|
22
|
+
return `npm uninstall --prefix ${quoteCommandArg(bridgePackageRoot)} ${packages.join(" ")}`;
|
|
23
|
+
}
|
|
8
24
|
const TOOL_DEFINITIONS = {
|
|
9
25
|
claude: {
|
|
10
26
|
key: "claude",
|
|
11
|
-
packageName: "@anthropic-ai/claude-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
],
|
|
20
|
-
|
|
21
|
-
? [
|
|
22
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
23
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
24
|
-
]
|
|
25
|
-
: [
|
|
26
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
27
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
28
|
-
"rm -f ~/.local/bin/claude",
|
|
29
|
-
],
|
|
27
|
+
packageName: "@anthropic-ai/claude-agent-sdk",
|
|
28
|
+
sdkPackageName: "@anthropic-ai/claude-agent-sdk",
|
|
29
|
+
aliases: [],
|
|
30
|
+
versionArgs: [],
|
|
31
|
+
installCommands: [
|
|
32
|
+
npmInstallIntoBridgeCommand(["@anthropic-ai/claude-agent-sdk@latest"]),
|
|
33
|
+
],
|
|
34
|
+
uninstallCommands: [
|
|
35
|
+
npmUninstallFromBridgeCommand(["@anthropic-ai/claude-agent-sdk"]),
|
|
36
|
+
],
|
|
30
37
|
},
|
|
31
38
|
claudecode: {
|
|
32
39
|
key: "claudecode",
|
|
33
|
-
packageName: "@anthropic-ai/claude-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
],
|
|
42
|
-
|
|
43
|
-
? [
|
|
44
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
45
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
46
|
-
]
|
|
47
|
-
: [
|
|
48
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
49
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
50
|
-
"rm -f ~/.local/bin/claude",
|
|
51
|
-
],
|
|
40
|
+
packageName: "@anthropic-ai/claude-agent-sdk",
|
|
41
|
+
sdkPackageName: "@anthropic-ai/claude-agent-sdk",
|
|
42
|
+
aliases: [],
|
|
43
|
+
versionArgs: [],
|
|
44
|
+
installCommands: [
|
|
45
|
+
npmInstallIntoBridgeCommand(["@anthropic-ai/claude-agent-sdk@latest"]),
|
|
46
|
+
],
|
|
47
|
+
uninstallCommands: [
|
|
48
|
+
npmUninstallFromBridgeCommand(["@anthropic-ai/claude-agent-sdk"]),
|
|
49
|
+
],
|
|
52
50
|
},
|
|
53
51
|
opencode: {
|
|
54
52
|
key: "opencode",
|
|
55
|
-
packageName: "opencode-ai",
|
|
53
|
+
packageName: "@opencode-ai/sdk",
|
|
54
|
+
sdkPackageName: "@opencode-ai/sdk",
|
|
56
55
|
aliases: isWindows
|
|
57
56
|
? ["opencode.cmd", "opencode.exe", "opencode"]
|
|
58
57
|
: ["opencode"],
|
|
59
58
|
versionArgs: ["--version"],
|
|
60
|
-
installCommands:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"curl -fsSL https://opencode.ai/install | bash",
|
|
64
|
-
"npm install -g opencode-ai@latest",
|
|
65
|
-
],
|
|
59
|
+
installCommands: [
|
|
60
|
+
npmInstallIntoBridgeCommand(["@opencode-ai/sdk@latest", "opencode-ai@latest"]),
|
|
61
|
+
],
|
|
66
62
|
uninstallCommands: isWindows
|
|
67
63
|
? [
|
|
68
|
-
"opencode
|
|
69
|
-
"npm uninstall -g opencode-ai",
|
|
70
|
-
"volta uninstall opencode-ai",
|
|
64
|
+
npmUninstallFromBridgeCommand(["@opencode-ai/sdk", "opencode-ai"]),
|
|
71
65
|
]
|
|
72
66
|
: [
|
|
67
|
+
npmUninstallFromBridgeCommand(["@opencode-ai/sdk", "opencode-ai"]),
|
|
73
68
|
"opencode uninstall --force",
|
|
74
|
-
"npm uninstall -g opencode-ai",
|
|
75
|
-
"volta uninstall opencode-ai",
|
|
76
69
|
"rm -f ~/.opencode/bin/opencode",
|
|
77
70
|
],
|
|
78
71
|
extraSearchPaths: () => {
|
|
@@ -88,14 +81,12 @@ const TOOL_DEFINITIONS = {
|
|
|
88
81
|
},
|
|
89
82
|
codex: {
|
|
90
83
|
key: "codex",
|
|
91
|
-
packageName: "@openai/codex",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"volta uninstall @openai/codex",
|
|
98
|
-
],
|
|
84
|
+
packageName: "@openai/codex-sdk",
|
|
85
|
+
sdkPackageName: "@openai/codex-sdk",
|
|
86
|
+
aliases: [],
|
|
87
|
+
versionArgs: [],
|
|
88
|
+
installCommands: [npmInstallIntoBridgeCommand(["@openai/codex-sdk@latest"])],
|
|
89
|
+
uninstallCommands: [npmUninstallFromBridgeCommand(["@openai/codex-sdk"])],
|
|
99
90
|
},
|
|
100
91
|
};
|
|
101
92
|
export const SUPPORTED_INSTALLABLE_TOOLS = Object.freeze(Object.keys(TOOL_DEFINITIONS));
|
|
@@ -271,14 +262,74 @@ async function resolveExecutableCandidate(definition) {
|
|
|
271
262
|
}
|
|
272
263
|
return { executable: null, version: null, error: "command not installed" };
|
|
273
264
|
}
|
|
265
|
+
async function readPackageVersion(packageJsonPath) {
|
|
266
|
+
try {
|
|
267
|
+
const rawPackageJson = await readFile(packageJsonPath, "utf8");
|
|
268
|
+
const parsedPackageJson = JSON.parse(rawPackageJson);
|
|
269
|
+
return typeof parsedPackageJson.version === "string"
|
|
270
|
+
? parsedPackageJson.version
|
|
271
|
+
: null;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function readInstalledPackageVersion(packageName) {
|
|
278
|
+
const packageJsonPath = path.join(bridgePackageRoot, "node_modules", ...packageName.split("/"), "package.json");
|
|
279
|
+
return readPackageVersion(packageJsonPath);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 检测 SDK provider 包是否能被当前 bridge 进程解析。
|
|
283
|
+
* 主流程:使用与运行时代码相同的模块解析上下文,避免把全局 CLI 安装误报为 SDK 可用。
|
|
284
|
+
*/
|
|
285
|
+
async function resolveSdkPackageCandidate(definition) {
|
|
286
|
+
const sdkPackageName = definition.sdkPackageName;
|
|
287
|
+
if (!sdkPackageName) {
|
|
288
|
+
return resolveExecutableCandidate(definition);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await import(sdkPackageName);
|
|
292
|
+
return {
|
|
293
|
+
executable: sdkPackageName,
|
|
294
|
+
version: await readInstalledPackageVersion(sdkPackageName),
|
|
295
|
+
error: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error || "");
|
|
300
|
+
return {
|
|
301
|
+
executable: null,
|
|
302
|
+
version: null,
|
|
303
|
+
error: message.includes(sdkPackageName)
|
|
304
|
+
? `SDK package ${sdkPackageName} is not installed in aws-runtime-bridge`
|
|
305
|
+
: message || `SDK package ${sdkPackageName} is not installed in aws-runtime-bridge`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function describeCommandFailure(error) {
|
|
310
|
+
if (!error || typeof error !== "object") {
|
|
311
|
+
return { message: String(error || "command failed") };
|
|
312
|
+
}
|
|
313
|
+
const failure = error;
|
|
314
|
+
return {
|
|
315
|
+
message: typeof failure.message === "string" ? failure.message : String(error),
|
|
316
|
+
code: failure.code ?? null,
|
|
317
|
+
signal: failure.signal ?? null,
|
|
318
|
+
stdout: typeof failure.stdout === "string" ? failure.stdout : "",
|
|
319
|
+
stderr: typeof failure.stderr === "string" ? failure.stderr : "",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
274
322
|
async function runToolCommand(command) {
|
|
275
323
|
if (isWindows) {
|
|
276
|
-
await execFileAsync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
324
|
+
const result = await execFileAsync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
277
325
|
timeout: 10 * 60 * 1000,
|
|
278
326
|
});
|
|
279
|
-
return;
|
|
327
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
280
328
|
}
|
|
281
|
-
await execFileAsync("/bin/sh", ["-lc", command], {
|
|
329
|
+
const result = await execFileAsync("/bin/sh", ["-lc", command], {
|
|
330
|
+
timeout: 10 * 60 * 1000,
|
|
331
|
+
});
|
|
332
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
282
333
|
}
|
|
283
334
|
/**
|
|
284
335
|
* 检查单个工具的 CLI 可执行状态,供实例状态展示与初始化前判断使用。
|
|
@@ -289,6 +340,7 @@ export async function detectToolInstallStatus(tool) {
|
|
|
289
340
|
.toLowerCase();
|
|
290
341
|
const definition = TOOL_DEFINITIONS[normalizedTool];
|
|
291
342
|
if (!definition) {
|
|
343
|
+
log.warn("Unsupported tool status requested", { tool: normalizedTool });
|
|
292
344
|
return {
|
|
293
345
|
tool: normalizedTool,
|
|
294
346
|
installed: false,
|
|
@@ -298,6 +350,17 @@ export async function detectToolInstallStatus(tool) {
|
|
|
298
350
|
error: "unsupported tool",
|
|
299
351
|
};
|
|
300
352
|
}
|
|
353
|
+
const sdkResult = await resolveSdkPackageCandidate(definition);
|
|
354
|
+
if (definition.sdkPackageName) {
|
|
355
|
+
return {
|
|
356
|
+
tool: normalizedTool,
|
|
357
|
+
installed: Boolean(sdkResult.executable),
|
|
358
|
+
executable: sdkResult.executable,
|
|
359
|
+
version: sdkResult.version,
|
|
360
|
+
installing: false,
|
|
361
|
+
error: sdkResult.error,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
301
364
|
const result = await resolveExecutableCandidate(definition);
|
|
302
365
|
return {
|
|
303
366
|
tool: normalizedTool,
|
|
@@ -326,25 +389,51 @@ export async function detectToolStatuses(tools) {
|
|
|
326
389
|
* 根据勾选工具自动安装缺失 CLI,安装后重新检测状态并返回。
|
|
327
390
|
*/
|
|
328
391
|
export async function ensureToolsInstalled(tools) {
|
|
392
|
+
log.info("Install request received", {
|
|
393
|
+
requestedTools: tools,
|
|
394
|
+
bridgePackageRoot,
|
|
395
|
+
platform: process.platform,
|
|
396
|
+
node: process.version,
|
|
397
|
+
});
|
|
329
398
|
const initialStatuses = await detectToolStatuses(tools);
|
|
399
|
+
log.info("Initial install status detected", { statuses: initialStatuses });
|
|
330
400
|
const nextStatuses = {
|
|
331
401
|
...initialStatuses,
|
|
332
402
|
};
|
|
333
403
|
for (const tool of Object.keys(initialStatuses)) {
|
|
334
404
|
const current = initialStatuses[tool];
|
|
335
405
|
if (current.installed) {
|
|
406
|
+
log.info("Skipping install because tool SDK is already available", {
|
|
407
|
+
tool,
|
|
408
|
+
version: current.version,
|
|
409
|
+
executable: current.executable,
|
|
410
|
+
});
|
|
336
411
|
continue;
|
|
337
412
|
}
|
|
338
413
|
const definition = TOOL_DEFINITIONS[tool];
|
|
339
414
|
if (!definition) {
|
|
415
|
+
log.warn("Skipping install because tool is unsupported", { tool, current });
|
|
340
416
|
continue;
|
|
341
417
|
}
|
|
342
418
|
let lastError = current.error;
|
|
343
419
|
for (const command of definition.installCommands) {
|
|
344
420
|
try {
|
|
345
|
-
|
|
421
|
+
log.info("Running install command", {
|
|
422
|
+
tool,
|
|
423
|
+
command,
|
|
424
|
+
sdkPackageName: definition.sdkPackageName || null,
|
|
425
|
+
packageName: definition.packageName,
|
|
426
|
+
});
|
|
427
|
+
const commandResult = await runToolCommand(command);
|
|
428
|
+
log.info("Install command completed", {
|
|
429
|
+
tool,
|
|
430
|
+
command,
|
|
431
|
+
stdout: commandResult.stdout,
|
|
432
|
+
stderr: commandResult.stderr,
|
|
433
|
+
});
|
|
346
434
|
const detected = await detectToolInstallStatus(tool);
|
|
347
435
|
nextStatuses[tool] = detected;
|
|
436
|
+
log.info("Post-install status detected", { tool, status: detected });
|
|
348
437
|
if (detected.installed) {
|
|
349
438
|
lastError = null;
|
|
350
439
|
break;
|
|
@@ -352,6 +441,11 @@ export async function ensureToolsInstalled(tools) {
|
|
|
352
441
|
lastError = detected.error;
|
|
353
442
|
}
|
|
354
443
|
catch (error) {
|
|
444
|
+
log.error("Install command failed", {
|
|
445
|
+
tool,
|
|
446
|
+
command,
|
|
447
|
+
failure: describeCommandFailure(error),
|
|
448
|
+
});
|
|
355
449
|
lastError =
|
|
356
450
|
error instanceof Error
|
|
357
451
|
? error.message
|
|
@@ -366,8 +460,14 @@ export async function ensureToolsInstalled(tools) {
|
|
|
366
460
|
installing: false,
|
|
367
461
|
error: lastError || "install failed",
|
|
368
462
|
};
|
|
463
|
+
log.warn("Tool SDK remains unavailable after install attempts", {
|
|
464
|
+
tool,
|
|
465
|
+
status: nextStatuses[tool],
|
|
466
|
+
attemptedCommands: definition.installCommands,
|
|
467
|
+
});
|
|
369
468
|
}
|
|
370
469
|
}
|
|
470
|
+
log.info("Install request completed", { statuses: nextStatuses });
|
|
371
471
|
return nextStatuses;
|
|
372
472
|
}
|
|
373
473
|
/**
|
|
@@ -76,30 +76,44 @@ describe('tool installer service', () => {
|
|
|
76
76
|
expect(typeof status.installed).toBe('boolean');
|
|
77
77
|
expect(status.error).not.toBe('unsupported tool');
|
|
78
78
|
});
|
|
79
|
+
it('detects OpenCode by SDK package instead of CLI availability', async () => {
|
|
80
|
+
const status = await detectToolInstallStatus('opencode');
|
|
81
|
+
expect(status.tool).toBe('opencode');
|
|
82
|
+
expect(status.executable === null || status.executable === '@opencode-ai/sdk').toBe(true);
|
|
83
|
+
if (!status.installed) {
|
|
84
|
+
expect(status.error).toContain('@opencode-ai/sdk');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it('detects Codex by SDK package instead of CLI availability', async () => {
|
|
88
|
+
const status = await detectToolInstallStatus('codex');
|
|
89
|
+
expect(status.tool).toBe('codex');
|
|
90
|
+
expect(status.executable === null || status.executable === '@openai/codex-sdk').toBe(true);
|
|
91
|
+
if (!status.installed) {
|
|
92
|
+
expect(status.error).toContain('@openai/codex-sdk');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
79
95
|
it('supports all panel tools as uninstallable tools', () => {
|
|
80
96
|
expect(SUPPORTED_UNINSTALLABLE_TOOLS).toEqual(expect.arrayContaining(['claude', 'opencode', 'codex']));
|
|
81
97
|
});
|
|
82
98
|
it('covers npm and native installer uninstall paths for OpenCode', () => {
|
|
83
99
|
const commands = getToolUninstallCommands('opencode');
|
|
84
|
-
expect(commands
|
|
85
|
-
expect(commands
|
|
100
|
+
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
101
|
+
expect(commands.some(command => command.includes('@opencode-ai/sdk'))).toBe(true);
|
|
102
|
+
expect(commands.some(command => command.includes('opencode-ai'))).toBe(true);
|
|
86
103
|
if (process.platform !== 'win32') {
|
|
87
104
|
expect(commands).toContain('opencode uninstall --force');
|
|
88
105
|
expect(commands).toContain('rm -f ~/.opencode/bin/opencode');
|
|
89
106
|
}
|
|
90
107
|
});
|
|
91
|
-
it('covers
|
|
108
|
+
it('covers bridge-local SDK uninstall path for Claude Code', () => {
|
|
92
109
|
const commands = getToolUninstallCommands('claude');
|
|
93
|
-
expect(commands
|
|
94
|
-
expect(commands
|
|
95
|
-
if (process.platform !== 'win32') {
|
|
96
|
-
expect(commands).toContain('rm -f ~/.local/bin/claude');
|
|
97
|
-
}
|
|
110
|
+
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
111
|
+
expect(commands.some(command => command.includes('@anthropic-ai/claude-agent-sdk'))).toBe(true);
|
|
98
112
|
});
|
|
99
|
-
it('covers
|
|
113
|
+
it('covers bridge-local SDK uninstall path for Codex', () => {
|
|
100
114
|
const commands = getToolUninstallCommands('codex');
|
|
101
|
-
expect(commands
|
|
102
|
-
expect(commands
|
|
115
|
+
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
116
|
+
expect(commands.some(command => command.includes('@openai/codex-sdk'))).toBe(true);
|
|
103
117
|
});
|
|
104
118
|
it('recognizes Volta shim paths for package-manager-aware detection', () => {
|
|
105
119
|
expect(isVoltaShimPath('C:\\Users\\tester\\AppData\\Local\\Volta\\bin\\codex.cmd')).toBe(true);
|