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.
@@ -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"}
@@ -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 toolStatus = await detectToolStatuses(buildToolStatusDetectionTargets(state.enabledTools || []));
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,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
  };
@@ -1 +1 @@
1
- {"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"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"}
@@ -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 { validateToken, isRuntimeTokenValid } from "../middleware/auth.js";
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
- return { shell: configuredShell || "/bin/bash", args: [] };
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 选择。
@@ -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":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA8GrD,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;AAqJD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAyB5B;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,CA+C5C;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAiD5C"}
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-code",
12
- aliases: isWindows ? ["claude.cmd", "claude.exe", "claude"] : ["claude"],
13
- versionArgs: ["--version"],
14
- installCommands: isWindows
15
- ? ["npm install -g @anthropic-ai/claude-code@latest"]
16
- : [
17
- "curl -fsSL https://claude.ai/install.sh | bash",
18
- "npm install -g @anthropic-ai/claude-code@latest",
19
- ],
20
- uninstallCommands: isWindows
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-code",
34
- aliases: isWindows ? ["claude.cmd", "claude.exe", "claude"] : ["claude"],
35
- versionArgs: ["--version"],
36
- installCommands: isWindows
37
- ? ["npm install -g @anthropic-ai/claude-code@latest"]
38
- : [
39
- "curl -fsSL https://claude.ai/install.sh | bash",
40
- "npm install -g @anthropic-ai/claude-code@latest",
41
- ],
42
- uninstallCommands: isWindows
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: isWindows
61
- ? ["npm install -g opencode-ai@latest"]
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 uninstall --force",
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
- aliases: isWindows ? ["codex.cmd", "codex.exe", "codex"] : ["codex"],
93
- versionArgs: ["--version"],
94
- installCommands: ["npm install -g @openai/codex@latest"],
95
- uninstallCommands: [
96
- "npm uninstall -g @openai/codex",
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], { timeout: 10 * 60 * 1000 });
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
- await runToolCommand(command);
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).toContain('npm uninstall -g opencode-ai');
85
- expect(commands).toContain('volta uninstall opencode-ai');
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 npm and native installer uninstall paths for Claude Code on Unix', () => {
108
+ it('covers bridge-local SDK uninstall path for Claude Code', () => {
92
109
  const commands = getToolUninstallCommands('claude');
93
- expect(commands).toContain('npm uninstall -g @anthropic-ai/claude-code');
94
- expect(commands).toContain('volta uninstall @anthropic-ai/claude-code');
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 Volta package uninstall for Codex', () => {
113
+ it('covers bridge-local SDK uninstall path for Codex', () => {
100
114
  const commands = getToolUninstallCommands('codex');
101
- expect(commands).toContain('npm uninstall -g @openai/codex');
102
- expect(commands).toContain('volta uninstall @openai/codex');
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.6.1",
3
+ "version": "1.6.5",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",