aws-runtime-bridge 1.6.2 → 1.6.6
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/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +5 -6
- 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/services/tool-installer.d.ts +19 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +127 -19
- package/dist/services/tool-installer.test.js +20 -1
- package/dist/utils/sdk-package-loader.d.ts +12 -0
- package/dist/utils/sdk-package-loader.d.ts.map +1 -0
- package/dist/utils/sdk-package-loader.js +78 -0
- package/dist/utils/sdk-package-loader.test.d.ts +2 -0
- package/dist/utils/sdk-package-loader.test.d.ts.map +1 -0
- package/dist/utils/sdk-package-loader.test.js +66 -0
- package/package.json +2 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpencodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/OpencodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"OpencodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/OpencodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,OAAO,KAAK,EAEV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EAEnB,aAAa,EACd,MAAM,YAAY,CAAC;AA2GpB,qBAAa,kBAAmB,SAAQ,YAAa,YAAW,mBAAmB;IACjF,QAAQ,CAAC,UAAU,cAAc;IACjC,QAAQ,CAAC,WAAW,cAAc;IAElC,OAAO,CAAC,QAAQ,CAA2C;IAC3D,OAAO,CAAC,SAAS,CAAC,CAAoB;IAEhC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IA+I5E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwC9D,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBnE,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7F,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAUnG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BxD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,EAAE;IAIzD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI9D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOpD,OAAO,IAAI,IAAI;IAaf;;;OAGG;YACW,OAAO;YA2BP,aAAa;IAmB3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAahC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAY7B,OAAO,CAAC,YAAY;YAYN,UAAU;IAuBxB,OAAO,CAAC,mBAAmB;IA6H3B,OAAO,CAAC,gBAAgB;IA6GxB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,2BAA2B;CAOpC"}
|
|
@@ -17,6 +17,7 @@ import * as net from 'node:net';
|
|
|
17
17
|
import * as os from 'node:os';
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import { v4 as uuidv4 } from 'uuid';
|
|
20
|
+
import { BRIDGE_PACKAGE_ROOT, importBridgeSdkPackage } from '../utils/sdk-package-loader.js';
|
|
20
21
|
import { getToolActionInfo } from './types.js';
|
|
21
22
|
// ============ 可执行文件查找 ============
|
|
22
23
|
/**
|
|
@@ -359,7 +360,7 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
359
360
|
async loadSdk() {
|
|
360
361
|
if (!this.sdkModule) {
|
|
361
362
|
try {
|
|
362
|
-
this.sdkModule = await
|
|
363
|
+
this.sdkModule = await importBridgeSdkPackage('@opencode-ai/sdk');
|
|
363
364
|
}
|
|
364
365
|
catch (error) {
|
|
365
366
|
if (isMissingPackageError(error, '@opencode-ai/sdk')) {
|
|
@@ -367,13 +368,11 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
367
368
|
'OpenCode SDK provider is not installed.',
|
|
368
369
|
'',
|
|
369
370
|
'aws-runtime-bridge keeps Codex/OpenCode provider SDKs optional to reduce default install size.',
|
|
370
|
-
'To use SDK mode with command "opencode", install:',
|
|
371
|
+
'To use SDK mode with command "opencode", install the SDK into the aws-runtime-bridge package root:',
|
|
371
372
|
'',
|
|
372
|
-
|
|
373
|
+
` npm install --prefix "${BRIDGE_PACKAGE_ROOT}" @opencode-ai/sdk@latest opencode-ai@latest`,
|
|
373
374
|
'',
|
|
374
|
-
'If aws-runtime-bridge is installed locally,
|
|
375
|
-
'',
|
|
376
|
-
' npm install @opencode-ai/sdk',
|
|
375
|
+
'If aws-runtime-bridge is installed locally, run the same command with that local package path.',
|
|
377
376
|
'',
|
|
378
377
|
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
379
378
|
].join('\n'));
|
|
@@ -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'], {
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import type { ToolInstallStatus } from "../types.js";
|
|
2
|
+
type ToolCommand = {
|
|
3
|
+
kind: "execFile";
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
display: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "shell";
|
|
9
|
+
display: string;
|
|
10
|
+
};
|
|
2
11
|
export declare const SUPPORTED_INSTALLABLE_TOOLS: readonly string[];
|
|
3
12
|
export declare const SUPPORTED_UNINSTALLABLE_TOOLS: readonly string[];
|
|
4
13
|
/**
|
|
5
14
|
* 返回工具对应的全局卸载命令副本,用于诊断与测试卸载覆盖范围。
|
|
6
15
|
*/
|
|
7
16
|
export declare function getToolUninstallCommands(tool: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* 返回工具对应的安装/卸载执行参数副本,用于验证 Windows 下不会把路径引号传给 npm。
|
|
19
|
+
*/
|
|
20
|
+
export declare function getToolCommandSpecs(tool: string, phase: "install" | "uninstall"): Array<{
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
display: string;
|
|
24
|
+
kind: ToolCommand["kind"];
|
|
25
|
+
}>;
|
|
8
26
|
export declare function isVoltaShimPath(commandPath: string): boolean;
|
|
9
27
|
/**
|
|
10
28
|
* 检查单个工具的 CLI 可执行状态,供实例状态展示与初始化前判断使用。
|
|
@@ -22,4 +40,5 @@ export declare function ensureToolsInstalled(tools: string[]): Promise<Record<st
|
|
|
22
40
|
* 按工具定义执行全局卸载命令,随后重新检测并返回最新安装状态。
|
|
23
41
|
*/
|
|
24
42
|
export declare function uninstallTools(tools: string[]): Promise<Record<string, ToolInstallStatus>>;
|
|
43
|
+
export {};
|
|
25
44
|
//# sourceMappingURL=tool-installer.d.ts.map
|
|
@@ -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":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAwBrD,KAAK,WAAW,GACZ;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAiHN,eAAO,MAAM,2BAA2B,mBAEvC,CAAC;AAEF,eAAO,MAAM,6BAA6B,mBAIzC,CAAC;AAEF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAO/D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,GAAG,WAAW,GAC7B,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAaxF;AAoDD,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAM5D;AAgPD;;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,CAsF5C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAiD5C"}
|
|
@@ -2,10 +2,12 @@ import { execFile } from "node:child_process";
|
|
|
2
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";
|
|
6
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { createLogger } from "../utils/logger.js";
|
|
7
|
+
import { BRIDGE_PACKAGE_ROOT, importBridgeSdkPackage } from "../utils/sdk-package-loader.js";
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
|
-
const
|
|
9
|
+
const log = createLogger("tool-installer");
|
|
10
|
+
const bridgePackageRoot = BRIDGE_PACKAGE_ROOT;
|
|
9
11
|
const isWindows = process.platform === "win32";
|
|
10
12
|
function quoteCommandArg(value) {
|
|
11
13
|
if (isWindows) {
|
|
@@ -19,6 +21,27 @@ function npmInstallIntoBridgeCommand(packages) {
|
|
|
19
21
|
function npmUninstallFromBridgeCommand(packages) {
|
|
20
22
|
return `npm uninstall --prefix ${quoteCommandArg(bridgePackageRoot)} ${packages.join(" ")}`;
|
|
21
23
|
}
|
|
24
|
+
function npmExecutableName() {
|
|
25
|
+
return isWindows ? "npm.cmd" : "npm";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 构建 bridge 本地 npm 包安装/卸载命令。
|
|
29
|
+
* 主流程:展示层保留可读命令;执行层使用参数数组,避免 Windows shell 引号被 npm 当作路径内容。
|
|
30
|
+
*/
|
|
31
|
+
function npmBridgeCommand(action, packages) {
|
|
32
|
+
const display = action === "install"
|
|
33
|
+
? npmInstallIntoBridgeCommand(packages)
|
|
34
|
+
: npmUninstallFromBridgeCommand(packages);
|
|
35
|
+
return {
|
|
36
|
+
kind: "execFile",
|
|
37
|
+
command: npmExecutableName(),
|
|
38
|
+
args: [action, "--prefix", bridgePackageRoot, ...packages],
|
|
39
|
+
display,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function shellToolCommand(command) {
|
|
43
|
+
return { kind: "shell", display: command };
|
|
44
|
+
}
|
|
22
45
|
const TOOL_DEFINITIONS = {
|
|
23
46
|
claude: {
|
|
24
47
|
key: "claude",
|
|
@@ -27,10 +50,10 @@ const TOOL_DEFINITIONS = {
|
|
|
27
50
|
aliases: [],
|
|
28
51
|
versionArgs: [],
|
|
29
52
|
installCommands: [
|
|
30
|
-
|
|
53
|
+
npmBridgeCommand("install", ["@anthropic-ai/claude-agent-sdk@latest"]),
|
|
31
54
|
],
|
|
32
55
|
uninstallCommands: [
|
|
33
|
-
|
|
56
|
+
npmBridgeCommand("uninstall", ["@anthropic-ai/claude-agent-sdk"]),
|
|
34
57
|
],
|
|
35
58
|
},
|
|
36
59
|
claudecode: {
|
|
@@ -40,10 +63,10 @@ const TOOL_DEFINITIONS = {
|
|
|
40
63
|
aliases: [],
|
|
41
64
|
versionArgs: [],
|
|
42
65
|
installCommands: [
|
|
43
|
-
|
|
66
|
+
npmBridgeCommand("install", ["@anthropic-ai/claude-agent-sdk@latest"]),
|
|
44
67
|
],
|
|
45
68
|
uninstallCommands: [
|
|
46
|
-
|
|
69
|
+
npmBridgeCommand("uninstall", ["@anthropic-ai/claude-agent-sdk"]),
|
|
47
70
|
],
|
|
48
71
|
},
|
|
49
72
|
opencode: {
|
|
@@ -55,16 +78,16 @@ const TOOL_DEFINITIONS = {
|
|
|
55
78
|
: ["opencode"],
|
|
56
79
|
versionArgs: ["--version"],
|
|
57
80
|
installCommands: [
|
|
58
|
-
|
|
81
|
+
npmBridgeCommand("install", ["@opencode-ai/sdk@latest", "opencode-ai@latest"]),
|
|
59
82
|
],
|
|
60
83
|
uninstallCommands: isWindows
|
|
61
84
|
? [
|
|
62
|
-
|
|
85
|
+
npmBridgeCommand("uninstall", ["@opencode-ai/sdk", "opencode-ai"]),
|
|
63
86
|
]
|
|
64
87
|
: [
|
|
65
|
-
|
|
66
|
-
"opencode uninstall --force",
|
|
67
|
-
"rm -f ~/.opencode/bin/opencode",
|
|
88
|
+
npmBridgeCommand("uninstall", ["@opencode-ai/sdk", "opencode-ai"]),
|
|
89
|
+
shellToolCommand("opencode uninstall --force"),
|
|
90
|
+
shellToolCommand("rm -f ~/.opencode/bin/opencode"),
|
|
68
91
|
],
|
|
69
92
|
extraSearchPaths: () => {
|
|
70
93
|
const home = os.homedir();
|
|
@@ -83,8 +106,8 @@ const TOOL_DEFINITIONS = {
|
|
|
83
106
|
sdkPackageName: "@openai/codex-sdk",
|
|
84
107
|
aliases: [],
|
|
85
108
|
versionArgs: [],
|
|
86
|
-
installCommands: [
|
|
87
|
-
uninstallCommands: [
|
|
109
|
+
installCommands: [npmBridgeCommand("install", ["@openai/codex-sdk@latest"])],
|
|
110
|
+
uninstallCommands: [npmBridgeCommand("uninstall", ["@openai/codex-sdk"])],
|
|
88
111
|
},
|
|
89
112
|
};
|
|
90
113
|
export const SUPPORTED_INSTALLABLE_TOOLS = Object.freeze(Object.keys(TOOL_DEFINITIONS));
|
|
@@ -96,7 +119,24 @@ export function getToolUninstallCommands(tool) {
|
|
|
96
119
|
const normalizedTool = String(tool || "")
|
|
97
120
|
.trim()
|
|
98
121
|
.toLowerCase();
|
|
99
|
-
return
|
|
122
|
+
return (TOOL_DEFINITIONS[normalizedTool]?.uninstallCommands || []).map((command) => command.display);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 返回工具对应的安装/卸载执行参数副本,用于验证 Windows 下不会把路径引号传给 npm。
|
|
126
|
+
*/
|
|
127
|
+
export function getToolCommandSpecs(tool, phase) {
|
|
128
|
+
const normalizedTool = String(tool || "")
|
|
129
|
+
.trim()
|
|
130
|
+
.toLowerCase();
|
|
131
|
+
const commands = phase === "install"
|
|
132
|
+
? TOOL_DEFINITIONS[normalizedTool]?.installCommands
|
|
133
|
+
: TOOL_DEFINITIONS[normalizedTool]?.uninstallCommands;
|
|
134
|
+
return (commands || []).map((command) => ({
|
|
135
|
+
command: command.kind === "execFile" ? command.command : command.display,
|
|
136
|
+
args: command.kind === "execFile" ? [...command.args] : [],
|
|
137
|
+
display: command.display,
|
|
138
|
+
kind: command.kind,
|
|
139
|
+
}));
|
|
100
140
|
}
|
|
101
141
|
function parseVersion(output) {
|
|
102
142
|
const normalized = String(output || "").trim();
|
|
@@ -286,7 +326,7 @@ async function resolveSdkPackageCandidate(definition) {
|
|
|
286
326
|
return resolveExecutableCandidate(definition);
|
|
287
327
|
}
|
|
288
328
|
try {
|
|
289
|
-
await
|
|
329
|
+
await importBridgeSdkPackage(sdkPackageName);
|
|
290
330
|
return {
|
|
291
331
|
executable: sdkPackageName,
|
|
292
332
|
version: await readInstalledPackageVersion(sdkPackageName),
|
|
@@ -304,14 +344,43 @@ async function resolveSdkPackageCandidate(definition) {
|
|
|
304
344
|
};
|
|
305
345
|
}
|
|
306
346
|
}
|
|
347
|
+
function describeCommandFailure(error) {
|
|
348
|
+
if (!error || typeof error !== "object") {
|
|
349
|
+
return { message: String(error || "command failed") };
|
|
350
|
+
}
|
|
351
|
+
const failure = error;
|
|
352
|
+
return {
|
|
353
|
+
message: typeof failure.message === "string" ? failure.message : String(error),
|
|
354
|
+
code: failure.code ?? null,
|
|
355
|
+
signal: failure.signal ?? null,
|
|
356
|
+
stdout: typeof failure.stdout === "string" ? failure.stdout : "",
|
|
357
|
+
stderr: typeof failure.stderr === "string" ? failure.stderr : "",
|
|
358
|
+
};
|
|
359
|
+
}
|
|
307
360
|
async function runToolCommand(command) {
|
|
361
|
+
if (command.kind === "execFile") {
|
|
362
|
+
const needsWindowsCmdShim = isWindows && /\.(?:cmd|bat)$/i.test(command.command);
|
|
363
|
+
const result = needsWindowsCmdShim
|
|
364
|
+
? await execFileAsync("cmd.exe", ["/d", "/s", "/c", command.command, ...command.args], {
|
|
365
|
+
timeout: 10 * 60 * 1000,
|
|
366
|
+
windowsVerbatimArguments: false,
|
|
367
|
+
})
|
|
368
|
+
: await execFileAsync(command.command, command.args, {
|
|
369
|
+
timeout: 10 * 60 * 1000,
|
|
370
|
+
windowsVerbatimArguments: false,
|
|
371
|
+
});
|
|
372
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
373
|
+
}
|
|
308
374
|
if (isWindows) {
|
|
309
|
-
await execFileAsync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
375
|
+
const result = await execFileAsync("cmd.exe", ["/d", "/s", "/c", command.display], {
|
|
310
376
|
timeout: 10 * 60 * 1000,
|
|
311
377
|
});
|
|
312
|
-
return;
|
|
378
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
313
379
|
}
|
|
314
|
-
await execFileAsync("/bin/sh", ["-lc", command], {
|
|
380
|
+
const result = await execFileAsync("/bin/sh", ["-lc", command.display], {
|
|
381
|
+
timeout: 10 * 60 * 1000,
|
|
382
|
+
});
|
|
383
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
315
384
|
}
|
|
316
385
|
/**
|
|
317
386
|
* 检查单个工具的 CLI 可执行状态,供实例状态展示与初始化前判断使用。
|
|
@@ -322,6 +391,7 @@ export async function detectToolInstallStatus(tool) {
|
|
|
322
391
|
.toLowerCase();
|
|
323
392
|
const definition = TOOL_DEFINITIONS[normalizedTool];
|
|
324
393
|
if (!definition) {
|
|
394
|
+
log.warn("Unsupported tool status requested", { tool: normalizedTool });
|
|
325
395
|
return {
|
|
326
396
|
tool: normalizedTool,
|
|
327
397
|
installed: false,
|
|
@@ -370,25 +440,51 @@ export async function detectToolStatuses(tools) {
|
|
|
370
440
|
* 根据勾选工具自动安装缺失 CLI,安装后重新检测状态并返回。
|
|
371
441
|
*/
|
|
372
442
|
export async function ensureToolsInstalled(tools) {
|
|
443
|
+
log.info("Install request received", {
|
|
444
|
+
requestedTools: tools,
|
|
445
|
+
bridgePackageRoot,
|
|
446
|
+
platform: process.platform,
|
|
447
|
+
node: process.version,
|
|
448
|
+
});
|
|
373
449
|
const initialStatuses = await detectToolStatuses(tools);
|
|
450
|
+
log.info("Initial install status detected", { statuses: initialStatuses });
|
|
374
451
|
const nextStatuses = {
|
|
375
452
|
...initialStatuses,
|
|
376
453
|
};
|
|
377
454
|
for (const tool of Object.keys(initialStatuses)) {
|
|
378
455
|
const current = initialStatuses[tool];
|
|
379
456
|
if (current.installed) {
|
|
457
|
+
log.info("Skipping install because tool SDK is already available", {
|
|
458
|
+
tool,
|
|
459
|
+
version: current.version,
|
|
460
|
+
executable: current.executable,
|
|
461
|
+
});
|
|
380
462
|
continue;
|
|
381
463
|
}
|
|
382
464
|
const definition = TOOL_DEFINITIONS[tool];
|
|
383
465
|
if (!definition) {
|
|
466
|
+
log.warn("Skipping install because tool is unsupported", { tool, current });
|
|
384
467
|
continue;
|
|
385
468
|
}
|
|
386
469
|
let lastError = current.error;
|
|
387
470
|
for (const command of definition.installCommands) {
|
|
388
471
|
try {
|
|
389
|
-
|
|
472
|
+
log.info("Running install command", {
|
|
473
|
+
tool,
|
|
474
|
+
command: command.display,
|
|
475
|
+
sdkPackageName: definition.sdkPackageName || null,
|
|
476
|
+
packageName: definition.packageName,
|
|
477
|
+
});
|
|
478
|
+
const commandResult = await runToolCommand(command);
|
|
479
|
+
log.info("Install command completed", {
|
|
480
|
+
tool,
|
|
481
|
+
command: command.display,
|
|
482
|
+
stdout: commandResult.stdout,
|
|
483
|
+
stderr: commandResult.stderr,
|
|
484
|
+
});
|
|
390
485
|
const detected = await detectToolInstallStatus(tool);
|
|
391
486
|
nextStatuses[tool] = detected;
|
|
487
|
+
log.info("Post-install status detected", { tool, status: detected });
|
|
392
488
|
if (detected.installed) {
|
|
393
489
|
lastError = null;
|
|
394
490
|
break;
|
|
@@ -396,6 +492,11 @@ export async function ensureToolsInstalled(tools) {
|
|
|
396
492
|
lastError = detected.error;
|
|
397
493
|
}
|
|
398
494
|
catch (error) {
|
|
495
|
+
log.error("Install command failed", {
|
|
496
|
+
tool,
|
|
497
|
+
command: command.display,
|
|
498
|
+
failure: describeCommandFailure(error),
|
|
499
|
+
});
|
|
399
500
|
lastError =
|
|
400
501
|
error instanceof Error
|
|
401
502
|
? error.message
|
|
@@ -410,8 +511,15 @@ export async function ensureToolsInstalled(tools) {
|
|
|
410
511
|
installing: false,
|
|
411
512
|
error: lastError || "install failed",
|
|
412
513
|
};
|
|
514
|
+
log.warn("Tool SDK remains unavailable after install attempts", {
|
|
515
|
+
tool,
|
|
516
|
+
status: nextStatuses[tool],
|
|
517
|
+
attemptedCommands: definition.installCommands,
|
|
518
|
+
attemptedCommandDisplays: definition.installCommands.map((command) => command.display),
|
|
519
|
+
});
|
|
413
520
|
}
|
|
414
521
|
}
|
|
522
|
+
log.info("Install request completed", { statuses: nextStatuses });
|
|
415
523
|
return nextStatuses;
|
|
416
524
|
}
|
|
417
525
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { SUPPORTED_INSTALLABLE_TOOLS, SUPPORTED_UNINSTALLABLE_TOOLS, detectToolInstallStatus, detectToolStatuses, getToolCommandSpecs, getToolUninstallCommands, isVoltaShimPath } from './tool-installer.js';
|
|
3
3
|
describe('tool installer service', () => {
|
|
4
4
|
it('returns structured status for supported tools', async () => {
|
|
5
5
|
const statuses = await detectToolStatuses(['claude', 'opencode', 'codex']);
|
|
@@ -115,6 +115,25 @@ describe('tool installer service', () => {
|
|
|
115
115
|
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
116
116
|
expect(commands.some(command => command.includes('@openai/codex-sdk'))).toBe(true);
|
|
117
117
|
});
|
|
118
|
+
it('executes npm-backed install and uninstall commands with prefix as an unquoted argument', () => {
|
|
119
|
+
const expectedPackagesByTool = {
|
|
120
|
+
claude: '@anthropic-ai/claude-agent-sdk',
|
|
121
|
+
claudecode: '@anthropic-ai/claude-agent-sdk',
|
|
122
|
+
opencode: '@opencode-ai/sdk',
|
|
123
|
+
codex: '@openai/codex-sdk'
|
|
124
|
+
};
|
|
125
|
+
for (const [tool, packageName] of Object.entries(expectedPackagesByTool)) {
|
|
126
|
+
for (const phase of ['install', 'uninstall']) {
|
|
127
|
+
const [command] = getToolCommandSpecs(tool, phase);
|
|
128
|
+
expect(command.kind).toBe('execFile');
|
|
129
|
+
expect(command.command).toMatch(/^npm(?:\.cmd)?$/);
|
|
130
|
+
expect(command.args.slice(0, 3)).toEqual([phase, '--prefix', expect.any(String)]);
|
|
131
|
+
expect(command.args[2]).not.toMatch(/^"|"$/);
|
|
132
|
+
expect(command.args.some(arg => arg.startsWith(packageName))).toBe(true);
|
|
133
|
+
expect(command.display).toContain(`npm ${phase} --prefix`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
118
137
|
it('recognizes Volta shim paths for package-manager-aware detection', () => {
|
|
119
138
|
expect(isVoltaShimPath('C:\\Users\\tester\\AppData\\Local\\Volta\\bin\\codex.cmd')).toBe(true);
|
|
120
139
|
expect(isVoltaShimPath('/home/tester/.volta/bin/codex')).toBe(true);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const BRIDGE_PACKAGE_ROOT: string;
|
|
2
|
+
/**
|
|
3
|
+
* 主干流程:从 bridge 自身安装根下的 node_modules 读取包元数据,
|
|
4
|
+
* 按 package.json 的 exports/module/main 定位真实 ESM 入口,避免猜测根目录 index.js。
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveBridgePackageEntryPath(packageName: string, packageRoot?: string): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* 具体实现逻辑:优先从 bridge-local 安装目录显式导入真实入口;
|
|
9
|
+
* 若没有本地 package.json,则退回 Node 的包名解析以兼容开发环境。
|
|
10
|
+
*/
|
|
11
|
+
export declare function importBridgeSdkPackage<TModule>(packageName: string): Promise<TModule>;
|
|
12
|
+
//# sourceMappingURL=sdk-package-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-package-loader.d.ts","sourceRoot":"","sources":["../../src/utils/sdk-package-loader.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,mBAAmB,QAI/B,CAAC;AA4DF;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,WAAW,EAAE,MAAM,EACnB,WAAW,SAAsB,GAChC,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAClD,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,CAAC,CAUlB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
export const BRIDGE_PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function getPackageDirectory(packageName, packageRoot) {
|
|
9
|
+
return path.join(packageRoot, 'node_modules', ...packageName.split('/'));
|
|
10
|
+
}
|
|
11
|
+
function resolveConditionalExport(value) {
|
|
12
|
+
if (typeof value === 'string') {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
for (const condition of ['import', 'module', 'default', 'node']) {
|
|
19
|
+
const resolved = resolveConditionalExport(value[condition]);
|
|
20
|
+
if (resolved) {
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function resolvePackageRootExport(exportsField) {
|
|
27
|
+
if (typeof exportsField === 'string') {
|
|
28
|
+
return exportsField;
|
|
29
|
+
}
|
|
30
|
+
if (!isRecord(exportsField)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const rootExport = resolveConditionalExport(exportsField['.']);
|
|
34
|
+
return rootExport ?? resolveConditionalExport(exportsField);
|
|
35
|
+
}
|
|
36
|
+
function resolveEntryFromPackageJson(packageJson) {
|
|
37
|
+
const exportedEntry = resolvePackageRootExport(packageJson.exports);
|
|
38
|
+
if (exportedEntry) {
|
|
39
|
+
return exportedEntry;
|
|
40
|
+
}
|
|
41
|
+
if (typeof packageJson.module === 'string') {
|
|
42
|
+
return packageJson.module;
|
|
43
|
+
}
|
|
44
|
+
if (typeof packageJson.main === 'string') {
|
|
45
|
+
return packageJson.main;
|
|
46
|
+
}
|
|
47
|
+
return 'index.js';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 主干流程:从 bridge 自身安装根下的 node_modules 读取包元数据,
|
|
51
|
+
* 按 package.json 的 exports/module/main 定位真实 ESM 入口,避免猜测根目录 index.js。
|
|
52
|
+
*/
|
|
53
|
+
export async function resolveBridgePackageEntryPath(packageName, packageRoot = BRIDGE_PACKAGE_ROOT) {
|
|
54
|
+
const packageDirectory = getPackageDirectory(packageName, packageRoot);
|
|
55
|
+
const packageJsonPath = path.join(packageDirectory, 'package.json');
|
|
56
|
+
const rawPackageJson = await readFile(packageJsonPath, 'utf8');
|
|
57
|
+
const packageJson = JSON.parse(rawPackageJson);
|
|
58
|
+
const entry = resolveEntryFromPackageJson(packageJson);
|
|
59
|
+
const entryPath = path.resolve(packageDirectory, entry);
|
|
60
|
+
await access(entryPath);
|
|
61
|
+
return entryPath;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 具体实现逻辑:优先从 bridge-local 安装目录显式导入真实入口;
|
|
65
|
+
* 若没有本地 package.json,则退回 Node 的包名解析以兼容开发环境。
|
|
66
|
+
*/
|
|
67
|
+
export async function importBridgeSdkPackage(packageName) {
|
|
68
|
+
try {
|
|
69
|
+
const entryPath = await resolveBridgePackageEntryPath(packageName);
|
|
70
|
+
return (await import(pathToFileURL(entryPath).href));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
74
|
+
return (await import(packageName));
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-package-loader.test.d.ts","sourceRoot":"","sources":["../../src/utils/sdk-package-loader.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { resolveBridgePackageEntryPath } from './sdk-package-loader.js';
|
|
6
|
+
const tempRoots = [];
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
9
|
+
});
|
|
10
|
+
async function createTempPackageRoot() {
|
|
11
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'aws-sdk-package-loader-'));
|
|
12
|
+
tempRoots.push(root);
|
|
13
|
+
return root;
|
|
14
|
+
}
|
|
15
|
+
describe('sdk package loader', () => {
|
|
16
|
+
it('resolves package exports instead of assuming a root index.js file', async () => {
|
|
17
|
+
const packageRoot = await createTempPackageRoot();
|
|
18
|
+
const packageDirectory = path.join(packageRoot, 'node_modules', '@opencode-ai', 'sdk');
|
|
19
|
+
await mkdir(path.join(packageDirectory, 'dist'), { recursive: true });
|
|
20
|
+
await writeFile(path.join(packageDirectory, 'package.json'), JSON.stringify({
|
|
21
|
+
name: '@opencode-ai/sdk',
|
|
22
|
+
type: 'module',
|
|
23
|
+
exports: {
|
|
24
|
+
'.': {
|
|
25
|
+
import: './dist/index.js',
|
|
26
|
+
types: './dist/index.d.ts',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
await writeFile(path.join(packageDirectory, 'dist', 'index.js'), 'export const ok = true;');
|
|
31
|
+
await expect(resolveBridgePackageEntryPath('@opencode-ai/sdk', packageRoot)).resolves.toBe(path.join(packageDirectory, 'dist', 'index.js'));
|
|
32
|
+
});
|
|
33
|
+
it('resolves default conditional exports', async () => {
|
|
34
|
+
const packageRoot = await createTempPackageRoot();
|
|
35
|
+
const packageDirectory = path.join(packageRoot, 'node_modules', 'default-only-sdk');
|
|
36
|
+
await mkdir(path.join(packageDirectory, 'dist'), { recursive: true });
|
|
37
|
+
await writeFile(path.join(packageDirectory, 'package.json'), JSON.stringify({ exports: { '.': { default: './dist/default.js' } } }));
|
|
38
|
+
await writeFile(path.join(packageDirectory, 'dist', 'default.js'), 'export const ok = true;');
|
|
39
|
+
await expect(resolveBridgePackageEntryPath('default-only-sdk', packageRoot)).resolves.toBe(path.join(packageDirectory, 'dist', 'default.js'));
|
|
40
|
+
});
|
|
41
|
+
it('falls back to module when exports is absent', async () => {
|
|
42
|
+
const packageRoot = await createTempPackageRoot();
|
|
43
|
+
const packageDirectory = path.join(packageRoot, 'node_modules', 'module-sdk');
|
|
44
|
+
await mkdir(path.join(packageDirectory, 'esm'), { recursive: true });
|
|
45
|
+
await writeFile(path.join(packageDirectory, 'package.json'), JSON.stringify({ module: './esm/index.js' }));
|
|
46
|
+
await writeFile(path.join(packageDirectory, 'esm', 'index.js'), 'export const ok = true;');
|
|
47
|
+
await expect(resolveBridgePackageEntryPath('module-sdk', packageRoot)).resolves.toBe(path.join(packageDirectory, 'esm', 'index.js'));
|
|
48
|
+
});
|
|
49
|
+
it('falls back to main when exports and module are absent', async () => {
|
|
50
|
+
const packageRoot = await createTempPackageRoot();
|
|
51
|
+
const packageDirectory = path.join(packageRoot, 'node_modules', 'main-sdk');
|
|
52
|
+
await mkdir(path.join(packageDirectory, 'lib'), { recursive: true });
|
|
53
|
+
await writeFile(path.join(packageDirectory, 'package.json'), JSON.stringify({ main: './lib/main.js' }));
|
|
54
|
+
await writeFile(path.join(packageDirectory, 'lib', 'main.js'), 'export const ok = true;');
|
|
55
|
+
await expect(resolveBridgePackageEntryPath('main-sdk', packageRoot)).resolves.toBe(path.join(packageDirectory, 'lib', 'main.js'));
|
|
56
|
+
});
|
|
57
|
+
it('rejects when the resolved package entry is missing', async () => {
|
|
58
|
+
const packageRoot = await createTempPackageRoot();
|
|
59
|
+
const packageDirectory = path.join(packageRoot, 'node_modules', 'broken-sdk');
|
|
60
|
+
await mkdir(packageDirectory, { recursive: true });
|
|
61
|
+
await writeFile(path.join(packageDirectory, 'package.json'), JSON.stringify({ exports: './dist/missing.js' }));
|
|
62
|
+
await expect(resolveBridgePackageEntryPath('broken-sdk', packageRoot)).rejects.toMatchObject({
|
|
63
|
+
code: 'ENOENT',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-runtime-bridge",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
|
50
50
|
"@cc-switch/sdk": "file:package/cc-switch-sdk",
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
52
|
+
"@openai/codex-sdk": "^0.130.0",
|
|
52
53
|
"archiver": "^8.0.0",
|
|
53
54
|
"axios": "^1.7.9",
|
|
54
55
|
"cors": "^2.8.5",
|
|
@@ -64,7 +65,6 @@
|
|
|
64
65
|
"zod": "^4.1.12"
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
|
-
"@openai/codex-sdk": "^0.125.0",
|
|
68
68
|
"@opencode-ai/sdk": "^1.3.13"
|
|
69
69
|
},
|
|
70
70
|
"peerDependenciesMeta": {
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@eslint/js": "^9.0.0",
|
|
80
|
-
"@openai/codex-sdk": "^0.125.0",
|
|
81
80
|
"@opencode-ai/sdk": "^1.3.13",
|
|
82
81
|
"@types/archiver": "^7.0.0",
|
|
83
82
|
"@types/cors": "^2.8.19",
|