aws-runtime-bridge 1.5.0 → 1.6.2
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/README.md +1 -1
- package/dist/adapter/AdapterRegistry.d.ts +1 -1
- package/dist/adapter/AdapterRegistry.d.ts.map +1 -1
- package/dist/adapter/AdapterRegistry.js +0 -2
- package/dist/adapter/ClaudeSdkAdapter.d.ts +4 -0
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/ClaudeSdkAdapter.js +11 -2
- package/dist/adapter/CodexSdkAdapter.js +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +2 -2
- package/dist/adapter/types.d.ts +10 -0
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/index.js +14 -43
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +9 -1
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +21 -1
- package/dist/routes/file-browser.test.js +9 -0
- package/dist/routes/instance.d.ts +10 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +93 -2
- package/dist/routes/instance.test.js +50 -0
- package/dist/routes/pty.d.ts +107 -0
- package/dist/routes/pty.d.ts.map +1 -0
- package/dist/routes/pty.js +551 -0
- package/dist/routes/pty.test.d.ts +2 -0
- package/dist/routes/pty.test.d.ts.map +1 -0
- package/dist/routes/pty.test.js +82 -0
- package/dist/routes/sessions.d.ts +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +32 -213
- package/dist/routes/terminal.d.ts +32 -3
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +411 -243
- package/dist/routes/terminal.test.js +105 -29
- package/dist/services/agent-process-manager.d.ts +2 -2
- package/dist/services/agent-process-manager.d.ts.map +1 -1
- package/dist/services/agent-process-manager.js +3 -3
- package/dist/services/process-detector.d.ts +2 -4
- package/dist/services/process-detector.d.ts.map +1 -1
- package/dist/services/process-detector.js +9 -16
- package/dist/services/process-registry.d.ts +2 -2
- package/dist/services/process-registry.d.ts.map +1 -1
- package/dist/services/process-registry.js +1 -1
- package/dist/services/session-output.d.ts +15 -5
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +33 -3
- package/dist/services/session-output.test.js +43 -29
- package/dist/services/terminal-persistence.d.ts +9 -0
- package/dist/services/terminal-persistence.d.ts.map +1 -1
- package/dist/services/terminal-persistence.js +20 -0
- package/dist/services/tool-installer.d.ts +10 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +193 -28
- package/dist/services/tool-installer.test.js +46 -1
- package/dist/services/workspace-files.d.ts +14 -0
- package/dist/services/workspace-files.d.ts.map +1 -1
- package/dist/services/workspace-files.js +52 -0
- package/dist/services/workspace-files.test.js +85 -1
- package/dist/types.d.ts +8 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -80,6 +80,26 @@ export async function upsertPersistedSession(session) {
|
|
|
80
80
|
return runningSessions;
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 更新指定 Agent 的持久化自动命令配置。
|
|
85
|
+
*
|
|
86
|
+
* 主流程:加载 running 会话 -> 命中 agentId 时只替换自动命令字段,保留原 command/workspace/pid。
|
|
87
|
+
*/
|
|
88
|
+
export async function updatePersistedSessionAutoCommands(agentId, commands) {
|
|
89
|
+
let updated = false;
|
|
90
|
+
await updatePersistedSessions((sessions) => sessions.map((session) => {
|
|
91
|
+
if (session.agentId !== agentId) {
|
|
92
|
+
return session;
|
|
93
|
+
}
|
|
94
|
+
updated = true;
|
|
95
|
+
return {
|
|
96
|
+
...session,
|
|
97
|
+
idleInputAutoCommand: commands.idleInputAutoCommand,
|
|
98
|
+
nonInputAutoCommand: commands.nonInputAutoCommand,
|
|
99
|
+
};
|
|
100
|
+
}));
|
|
101
|
+
return updated;
|
|
102
|
+
}
|
|
83
103
|
/**
|
|
84
104
|
* 移除持久化会话(从完整文件中删除,包括 stopped 状态的)
|
|
85
105
|
*
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ToolInstallStatus } from "../types.js";
|
|
2
2
|
export declare const SUPPORTED_INSTALLABLE_TOOLS: readonly string[];
|
|
3
|
+
export declare const SUPPORTED_UNINSTALLABLE_TOOLS: readonly string[];
|
|
4
|
+
/**
|
|
5
|
+
* 返回工具对应的全局卸载命令副本,用于诊断与测试卸载覆盖范围。
|
|
6
|
+
*/
|
|
7
|
+
export declare function getToolUninstallCommands(tool: string): string[];
|
|
8
|
+
export declare function isVoltaShimPath(commandPath: string): boolean;
|
|
3
9
|
/**
|
|
4
10
|
* 检查单个工具的 CLI 可执行状态,供实例状态展示与初始化前判断使用。
|
|
5
11
|
*/
|
|
@@ -12,4 +18,8 @@ export declare function detectToolStatuses(tools: string[]): Promise<Record<stri
|
|
|
12
18
|
* 根据勾选工具自动安装缺失 CLI,安装后重新检测状态并返回。
|
|
13
19
|
*/
|
|
14
20
|
export declare function ensureToolsInstalled(tools: string[]): Promise<Record<string, ToolInstallStatus>>;
|
|
21
|
+
/**
|
|
22
|
+
* 按工具定义执行全局卸载命令,随后重新检测并返回最新安装状态。
|
|
23
|
+
*/
|
|
24
|
+
export declare function uninstallTools(tools: string[]): Promise<Record<string, ToolInstallStatus>>;
|
|
15
25
|
//# 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":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AA0GrD,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;AAgND;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAqC5B;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,44 +1,70 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
1
3
|
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
8
|
+
const bridgePackageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
7
9
|
const isWindows = process.platform === "win32";
|
|
10
|
+
function quoteCommandArg(value) {
|
|
11
|
+
if (isWindows) {
|
|
12
|
+
return `"${value.replaceAll('"', '\\"')}"`;
|
|
13
|
+
}
|
|
14
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
15
|
+
}
|
|
16
|
+
function npmInstallIntoBridgeCommand(packages) {
|
|
17
|
+
return `npm install --prefix ${quoteCommandArg(bridgePackageRoot)} ${packages.join(" ")}`;
|
|
18
|
+
}
|
|
19
|
+
function npmUninstallFromBridgeCommand(packages) {
|
|
20
|
+
return `npm uninstall --prefix ${quoteCommandArg(bridgePackageRoot)} ${packages.join(" ")}`;
|
|
21
|
+
}
|
|
8
22
|
const TOOL_DEFINITIONS = {
|
|
9
23
|
claude: {
|
|
10
24
|
key: "claude",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
packageName: "@anthropic-ai/claude-agent-sdk",
|
|
26
|
+
sdkPackageName: "@anthropic-ai/claude-agent-sdk",
|
|
27
|
+
aliases: [],
|
|
28
|
+
versionArgs: [],
|
|
29
|
+
installCommands: [
|
|
30
|
+
npmInstallIntoBridgeCommand(["@anthropic-ai/claude-agent-sdk@latest"]),
|
|
31
|
+
],
|
|
32
|
+
uninstallCommands: [
|
|
33
|
+
npmUninstallFromBridgeCommand(["@anthropic-ai/claude-agent-sdk"]),
|
|
34
|
+
],
|
|
19
35
|
},
|
|
20
36
|
claudecode: {
|
|
21
37
|
key: "claudecode",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
packageName: "@anthropic-ai/claude-agent-sdk",
|
|
39
|
+
sdkPackageName: "@anthropic-ai/claude-agent-sdk",
|
|
40
|
+
aliases: [],
|
|
41
|
+
versionArgs: [],
|
|
42
|
+
installCommands: [
|
|
43
|
+
npmInstallIntoBridgeCommand(["@anthropic-ai/claude-agent-sdk@latest"]),
|
|
44
|
+
],
|
|
45
|
+
uninstallCommands: [
|
|
46
|
+
npmUninstallFromBridgeCommand(["@anthropic-ai/claude-agent-sdk"]),
|
|
47
|
+
],
|
|
30
48
|
},
|
|
31
49
|
opencode: {
|
|
32
50
|
key: "opencode",
|
|
51
|
+
packageName: "@opencode-ai/sdk",
|
|
52
|
+
sdkPackageName: "@opencode-ai/sdk",
|
|
33
53
|
aliases: isWindows
|
|
34
54
|
? ["opencode.cmd", "opencode.exe", "opencode"]
|
|
35
55
|
: ["opencode"],
|
|
36
56
|
versionArgs: ["--version"],
|
|
37
|
-
installCommands:
|
|
38
|
-
|
|
57
|
+
installCommands: [
|
|
58
|
+
npmInstallIntoBridgeCommand(["@opencode-ai/sdk@latest", "opencode-ai@latest"]),
|
|
59
|
+
],
|
|
60
|
+
uninstallCommands: isWindows
|
|
61
|
+
? [
|
|
62
|
+
npmUninstallFromBridgeCommand(["@opencode-ai/sdk", "opencode-ai"]),
|
|
63
|
+
]
|
|
39
64
|
: [
|
|
40
|
-
"
|
|
41
|
-
"
|
|
65
|
+
npmUninstallFromBridgeCommand(["@opencode-ai/sdk", "opencode-ai"]),
|
|
66
|
+
"opencode uninstall --force",
|
|
67
|
+
"rm -f ~/.opencode/bin/opencode",
|
|
42
68
|
],
|
|
43
69
|
extraSearchPaths: () => {
|
|
44
70
|
const home = os.homedir();
|
|
@@ -53,12 +79,25 @@ const TOOL_DEFINITIONS = {
|
|
|
53
79
|
},
|
|
54
80
|
codex: {
|
|
55
81
|
key: "codex",
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
packageName: "@openai/codex-sdk",
|
|
83
|
+
sdkPackageName: "@openai/codex-sdk",
|
|
84
|
+
aliases: [],
|
|
85
|
+
versionArgs: [],
|
|
86
|
+
installCommands: [npmInstallIntoBridgeCommand(["@openai/codex-sdk@latest"])],
|
|
87
|
+
uninstallCommands: [npmUninstallFromBridgeCommand(["@openai/codex-sdk"])],
|
|
59
88
|
},
|
|
60
89
|
};
|
|
61
90
|
export const SUPPORTED_INSTALLABLE_TOOLS = Object.freeze(Object.keys(TOOL_DEFINITIONS));
|
|
91
|
+
export const SUPPORTED_UNINSTALLABLE_TOOLS = Object.freeze(Object.keys(TOOL_DEFINITIONS).filter((tool) => TOOL_DEFINITIONS[tool].uninstallCommands.length > 0));
|
|
92
|
+
/**
|
|
93
|
+
* 返回工具对应的全局卸载命令副本,用于诊断与测试卸载覆盖范围。
|
|
94
|
+
*/
|
|
95
|
+
export function getToolUninstallCommands(tool) {
|
|
96
|
+
const normalizedTool = String(tool || "")
|
|
97
|
+
.trim()
|
|
98
|
+
.toLowerCase();
|
|
99
|
+
return [...(TOOL_DEFINITIONS[normalizedTool]?.uninstallCommands || [])];
|
|
100
|
+
}
|
|
62
101
|
function parseVersion(output) {
|
|
63
102
|
const normalized = String(output || "").trim();
|
|
64
103
|
if (!normalized) {
|
|
@@ -103,6 +142,26 @@ function describeExecError(error) {
|
|
|
103
142
|
}
|
|
104
143
|
return message || "command execution failed";
|
|
105
144
|
}
|
|
145
|
+
export function isVoltaShimPath(commandPath) {
|
|
146
|
+
const normalizedPath = path
|
|
147
|
+
.normalize(commandPath)
|
|
148
|
+
.replaceAll("\\", "/")
|
|
149
|
+
.toLowerCase();
|
|
150
|
+
return /(?:^|\/)\.?volta\/bin\//.test(normalizedPath);
|
|
151
|
+
}
|
|
152
|
+
async function isVoltaPackageInstalled(packageName) {
|
|
153
|
+
try {
|
|
154
|
+
const { stdout, stderr } = await execFileAsync("volta", ["list", "--format", "plain"], {
|
|
155
|
+
timeout: 20_000,
|
|
156
|
+
});
|
|
157
|
+
const output = `${stdout || ""}\n${stderr || ""}`;
|
|
158
|
+
const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
return new RegExp(`^package\\s+${escapedPackageName}(?:@|\\s)`, "im").test(output);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
106
165
|
/**
|
|
107
166
|
* 在 Windows 上执行命令(处理 .cmd/.bat 文件)
|
|
108
167
|
* 在 Unix 系统上直接执行
|
|
@@ -151,7 +210,7 @@ async function resolveExecutableCandidate(definition) {
|
|
|
151
210
|
}
|
|
152
211
|
catch (error) {
|
|
153
212
|
executionFailureMessage = describeExecError(error);
|
|
154
|
-
if (executionFailureMessage !== "command not found in PATH") {
|
|
213
|
+
if (!isWindows && executionFailureMessage !== "command not found in PATH") {
|
|
155
214
|
return {
|
|
156
215
|
executable: alias,
|
|
157
216
|
version: null,
|
|
@@ -164,6 +223,11 @@ async function resolveExecutableCandidate(definition) {
|
|
|
164
223
|
try {
|
|
165
224
|
await access(absolutePath);
|
|
166
225
|
foundCommandPath = absolutePath;
|
|
226
|
+
if (isVoltaShimPath(absolutePath) &&
|
|
227
|
+
!(await isVoltaPackageInstalled(definition.packageName))) {
|
|
228
|
+
foundCommandPath = null;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
167
231
|
const { stdout, stderr } = await executeVersionCommand(absolutePath, definition.versionArgs);
|
|
168
232
|
const version = parseVersion(`${stdout || ""}\n${stderr || ""}`);
|
|
169
233
|
if (!version) {
|
|
@@ -196,7 +260,51 @@ async function resolveExecutableCandidate(definition) {
|
|
|
196
260
|
}
|
|
197
261
|
return { executable: null, version: null, error: "command not installed" };
|
|
198
262
|
}
|
|
199
|
-
async function
|
|
263
|
+
async function readPackageVersion(packageJsonPath) {
|
|
264
|
+
try {
|
|
265
|
+
const rawPackageJson = await readFile(packageJsonPath, "utf8");
|
|
266
|
+
const parsedPackageJson = JSON.parse(rawPackageJson);
|
|
267
|
+
return typeof parsedPackageJson.version === "string"
|
|
268
|
+
? parsedPackageJson.version
|
|
269
|
+
: null;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function readInstalledPackageVersion(packageName) {
|
|
276
|
+
const packageJsonPath = path.join(bridgePackageRoot, "node_modules", ...packageName.split("/"), "package.json");
|
|
277
|
+
return readPackageVersion(packageJsonPath);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 检测 SDK provider 包是否能被当前 bridge 进程解析。
|
|
281
|
+
* 主流程:使用与运行时代码相同的模块解析上下文,避免把全局 CLI 安装误报为 SDK 可用。
|
|
282
|
+
*/
|
|
283
|
+
async function resolveSdkPackageCandidate(definition) {
|
|
284
|
+
const sdkPackageName = definition.sdkPackageName;
|
|
285
|
+
if (!sdkPackageName) {
|
|
286
|
+
return resolveExecutableCandidate(definition);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
await import(sdkPackageName);
|
|
290
|
+
return {
|
|
291
|
+
executable: sdkPackageName,
|
|
292
|
+
version: await readInstalledPackageVersion(sdkPackageName),
|
|
293
|
+
error: null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const message = error instanceof Error ? error.message : String(error || "");
|
|
298
|
+
return {
|
|
299
|
+
executable: null,
|
|
300
|
+
version: null,
|
|
301
|
+
error: message.includes(sdkPackageName)
|
|
302
|
+
? `SDK package ${sdkPackageName} is not installed in aws-runtime-bridge`
|
|
303
|
+
: message || `SDK package ${sdkPackageName} is not installed in aws-runtime-bridge`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function runToolCommand(command) {
|
|
200
308
|
if (isWindows) {
|
|
201
309
|
await execFileAsync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
202
310
|
timeout: 10 * 60 * 1000,
|
|
@@ -223,6 +331,17 @@ export async function detectToolInstallStatus(tool) {
|
|
|
223
331
|
error: "unsupported tool",
|
|
224
332
|
};
|
|
225
333
|
}
|
|
334
|
+
const sdkResult = await resolveSdkPackageCandidate(definition);
|
|
335
|
+
if (definition.sdkPackageName) {
|
|
336
|
+
return {
|
|
337
|
+
tool: normalizedTool,
|
|
338
|
+
installed: Boolean(sdkResult.executable),
|
|
339
|
+
executable: sdkResult.executable,
|
|
340
|
+
version: sdkResult.version,
|
|
341
|
+
installing: false,
|
|
342
|
+
error: sdkResult.error,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
226
345
|
const result = await resolveExecutableCandidate(definition);
|
|
227
346
|
return {
|
|
228
347
|
tool: normalizedTool,
|
|
@@ -267,7 +386,7 @@ export async function ensureToolsInstalled(tools) {
|
|
|
267
386
|
let lastError = current.error;
|
|
268
387
|
for (const command of definition.installCommands) {
|
|
269
388
|
try {
|
|
270
|
-
await
|
|
389
|
+
await runToolCommand(command);
|
|
271
390
|
const detected = await detectToolInstallStatus(tool);
|
|
272
391
|
nextStatuses[tool] = detected;
|
|
273
392
|
if (detected.installed) {
|
|
@@ -295,3 +414,49 @@ export async function ensureToolsInstalled(tools) {
|
|
|
295
414
|
}
|
|
296
415
|
return nextStatuses;
|
|
297
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* 按工具定义执行全局卸载命令,随后重新检测并返回最新安装状态。
|
|
419
|
+
*/
|
|
420
|
+
export async function uninstallTools(tools) {
|
|
421
|
+
const normalizedTools = [
|
|
422
|
+
...new Set((Array.isArray(tools) ? tools : [])
|
|
423
|
+
.map((item) => String(item || "")
|
|
424
|
+
.trim()
|
|
425
|
+
.toLowerCase())
|
|
426
|
+
.filter(Boolean)),
|
|
427
|
+
];
|
|
428
|
+
const nextStatuses = {};
|
|
429
|
+
for (const tool of normalizedTools) {
|
|
430
|
+
const definition = TOOL_DEFINITIONS[tool];
|
|
431
|
+
if (!definition) {
|
|
432
|
+
nextStatuses[tool] = await detectToolInstallStatus(tool);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
let lastError = null;
|
|
436
|
+
for (const command of definition.uninstallCommands) {
|
|
437
|
+
try {
|
|
438
|
+
await runToolCommand(command);
|
|
439
|
+
lastError = null;
|
|
440
|
+
const detectedAfterCommand = await detectToolInstallStatus(tool);
|
|
441
|
+
if (!detectedAfterCommand.installed) {
|
|
442
|
+
nextStatuses[tool] = detectedAfterCommand;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
lastError =
|
|
448
|
+
error instanceof Error
|
|
449
|
+
? error.message
|
|
450
|
+
: String(error || "uninstall failed");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const detected = await detectToolInstallStatus(tool);
|
|
454
|
+
nextStatuses[tool] = {
|
|
455
|
+
...detected,
|
|
456
|
+
error: detected.installed
|
|
457
|
+
? lastError || detected.error || "uninstall completed but command is still available"
|
|
458
|
+
: detected.error,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return nextStatuses;
|
|
462
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { detectToolStatuses,
|
|
2
|
+
import { detectToolInstallStatus, SUPPORTED_INSTALLABLE_TOOLS, SUPPORTED_UNINSTALLABLE_TOOLS, detectToolStatuses, 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']);
|
|
@@ -76,6 +76,51 @@ 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
|
+
});
|
|
95
|
+
it('supports all panel tools as uninstallable tools', () => {
|
|
96
|
+
expect(SUPPORTED_UNINSTALLABLE_TOOLS).toEqual(expect.arrayContaining(['claude', 'opencode', 'codex']));
|
|
97
|
+
});
|
|
98
|
+
it('covers npm and native installer uninstall paths for OpenCode', () => {
|
|
99
|
+
const commands = getToolUninstallCommands('opencode');
|
|
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);
|
|
103
|
+
if (process.platform !== 'win32') {
|
|
104
|
+
expect(commands).toContain('opencode uninstall --force');
|
|
105
|
+
expect(commands).toContain('rm -f ~/.opencode/bin/opencode');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
it('covers bridge-local SDK uninstall path for Claude Code', () => {
|
|
109
|
+
const commands = getToolUninstallCommands('claude');
|
|
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);
|
|
112
|
+
});
|
|
113
|
+
it('covers bridge-local SDK uninstall path for Codex', () => {
|
|
114
|
+
const commands = getToolUninstallCommands('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);
|
|
117
|
+
});
|
|
118
|
+
it('recognizes Volta shim paths for package-manager-aware detection', () => {
|
|
119
|
+
expect(isVoltaShimPath('C:\\Users\\tester\\AppData\\Local\\Volta\\bin\\codex.cmd')).toBe(true);
|
|
120
|
+
expect(isVoltaShimPath('/home/tester/.volta/bin/codex')).toBe(true);
|
|
121
|
+
expect(isVoltaShimPath('/Users/tester/.volta/bin/claude')).toBe(true);
|
|
122
|
+
expect(isVoltaShimPath('/usr/local/bin/codex')).toBe(false);
|
|
123
|
+
});
|
|
79
124
|
it('detectToolStatuses returns status for all requested tools', async () => {
|
|
80
125
|
const tools = ['claude', 'opencode', 'unknown-tool'];
|
|
81
126
|
const statuses = await detectToolStatuses(tools);
|
|
@@ -67,6 +67,10 @@ interface RenameWorkspaceEntryParams extends WorkspacePathParams {
|
|
|
67
67
|
targetPath: string;
|
|
68
68
|
newName: string;
|
|
69
69
|
}
|
|
70
|
+
interface MoveWorkspaceEntryParams extends WorkspacePathParams {
|
|
71
|
+
targetPath: string;
|
|
72
|
+
destinationPath: string;
|
|
73
|
+
}
|
|
70
74
|
interface DeleteWorkspaceEntryParams extends WorkspacePathParams {
|
|
71
75
|
targetPath: string;
|
|
72
76
|
}
|
|
@@ -119,6 +123,16 @@ export declare function renameWorkspaceEntry(params: RenameWorkspaceEntryParams)
|
|
|
119
123
|
sourcePath: string;
|
|
120
124
|
targetPath: string;
|
|
121
125
|
}>;
|
|
126
|
+
/**
|
|
127
|
+
* 将工作区内文件或目录移动到另一个工作区目录,保持原文件名不变。
|
|
128
|
+
*/
|
|
129
|
+
export declare function moveWorkspaceEntry(params: MoveWorkspaceEntryParams): Promise<{
|
|
130
|
+
ok: true;
|
|
131
|
+
workspacePath: string;
|
|
132
|
+
sourcePath: string;
|
|
133
|
+
targetPath: string;
|
|
134
|
+
destinationPath: string;
|
|
135
|
+
}>;
|
|
122
136
|
/**
|
|
123
137
|
* 删除工作区内的文件或目录,并阻止误删工作区根目录。
|
|
124
138
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-files.d.ts","sourceRoot":"","sources":["../../src/services/workspace-files.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"workspace-files.d.ts","sourceRoot":"","sources":["../../src/services/workspace-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -487,6 +487,55 @@ export async function renameWorkspaceEntry(params) {
|
|
|
487
487
|
targetPath: toDisplayPath(renamedPath)
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* 将工作区内文件或目录移动到另一个工作区目录,保持原文件名不变。
|
|
492
|
+
*/
|
|
493
|
+
export async function moveWorkspaceEntry(params) {
|
|
494
|
+
const { workspaceRoot, resolvedTargetPath } = await resolveEntryTarget(params);
|
|
495
|
+
const destination = await resolveDirectoryTarget({
|
|
496
|
+
workspacePath: params.workspacePath,
|
|
497
|
+
targetPath: params.destinationPath,
|
|
498
|
+
});
|
|
499
|
+
const destinationStat = await fs.stat(destination.resolvedTargetPath);
|
|
500
|
+
if (!destinationStat.isDirectory()) {
|
|
501
|
+
throw new Error(`Destination is not a directory: ${destination.resolvedTargetPath}`);
|
|
502
|
+
}
|
|
503
|
+
const targetStat = await fs.stat(resolvedTargetPath);
|
|
504
|
+
if (targetStat.isDirectory()) {
|
|
505
|
+
const relativeDestination = path.relative(resolvedTargetPath, destination.resolvedTargetPath);
|
|
506
|
+
if (relativeDestination === '' || (!relativeDestination.startsWith('..') && !path.isAbsolute(relativeDestination))) {
|
|
507
|
+
throw new Error('Cannot move a directory into itself or its descendant');
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const movedPath = ensureInsideWorkspace(workspaceRoot, path.join(destination.resolvedTargetPath, path.basename(resolvedTargetPath)));
|
|
511
|
+
if (movedPath === resolvedTargetPath) {
|
|
512
|
+
return {
|
|
513
|
+
ok: true,
|
|
514
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
515
|
+
sourcePath: toDisplayPath(resolvedTargetPath),
|
|
516
|
+
targetPath: toDisplayPath(movedPath),
|
|
517
|
+
destinationPath: toDisplayPath(destination.resolvedTargetPath),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await fs.access(movedPath);
|
|
522
|
+
throw new Error(`Path already exists: ${movedPath}`);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
const errorCode = typeof error === 'object' && error !== null ? Reflect.get(error, 'code') : undefined;
|
|
526
|
+
if (errorCode !== 'ENOENT') {
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
await fs.rename(resolvedTargetPath, movedPath);
|
|
531
|
+
return {
|
|
532
|
+
ok: true,
|
|
533
|
+
workspacePath: toDisplayPath(workspaceRoot),
|
|
534
|
+
sourcePath: toDisplayPath(resolvedTargetPath),
|
|
535
|
+
targetPath: toDisplayPath(movedPath),
|
|
536
|
+
destinationPath: toDisplayPath(destination.resolvedTargetPath),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
490
539
|
/**
|
|
491
540
|
* 删除工作区内的文件或目录,并阻止误删工作区根目录。
|
|
492
541
|
*/
|
|
@@ -639,6 +688,9 @@ async function extractTarArchive(archivePath, outputRoot, gzip, meter) {
|
|
|
639
688
|
if (entryType === 'symboliclink' || entryType === 'link') {
|
|
640
689
|
throw new Error(`Archive links are not supported: ${entryPath}`);
|
|
641
690
|
}
|
|
691
|
+
if (entryType !== 'file' && entryType !== 'oldfile' && entryType !== 'contiguousfile' && entryType !== 'directory') {
|
|
692
|
+
throw new Error(`Unsupported archive entry type: ${entryPath}`);
|
|
693
|
+
}
|
|
642
694
|
if (typeof entry.size === 'number') {
|
|
643
695
|
trackArchiveBytes(meter, entry.size);
|
|
644
696
|
}
|
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import * as tar from 'tar';
|
|
5
5
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
6
|
-
import { createWorkspaceEntry, deleteWorkspaceEntry, extractWorkspaceArchive, listWorkspaceDirectory, previewWorkspaceDocument, readWorkspaceFile, renameWorkspaceEntry, resolveWorkspaceDownloadTarget, streamWorkspaceDirectoryZip, uploadWorkspaceFiles, writeWorkspaceFile, } from './workspace-files.js';
|
|
6
|
+
import { createWorkspaceEntry, deleteWorkspaceEntry, extractWorkspaceArchive, listWorkspaceDirectory, moveWorkspaceEntry, previewWorkspaceDocument, readWorkspaceFile, renameWorkspaceEntry, resolveWorkspaceDownloadTarget, streamWorkspaceDirectoryZip, uploadWorkspaceFiles, writeWorkspaceFile, } from './workspace-files.js';
|
|
7
7
|
const { ZipArchive } = await import('archiver');
|
|
8
8
|
function crc32(input) {
|
|
9
9
|
let crc = 0xffffffff;
|
|
@@ -70,6 +70,27 @@ async function writeStoredZipWithNameBytes(filePath, nameBytes, content) {
|
|
|
70
70
|
endRecord
|
|
71
71
|
]));
|
|
72
72
|
}
|
|
73
|
+
async function writeTarHeaderOnlyEntry(filePath, entryName, typeFlag) {
|
|
74
|
+
const header = Buffer.alloc(512, 0);
|
|
75
|
+
header.write(entryName, 0, Math.min(Buffer.byteLength(entryName), 100), 'utf-8');
|
|
76
|
+
header.write('0000644\0', 100, 'ascii');
|
|
77
|
+
header.write('0000000\0', 108, 'ascii');
|
|
78
|
+
header.write('0000000\0', 116, 'ascii');
|
|
79
|
+
header.write('00000000000\0', 124, 'ascii');
|
|
80
|
+
header.write('00000000000\0', 136, 'ascii');
|
|
81
|
+
header.fill(0x20, 148, 156);
|
|
82
|
+
header.write(typeFlag, 156, 'ascii');
|
|
83
|
+
header.write('ustar\0', 257, 'ascii');
|
|
84
|
+
header.write('00', 263, 'ascii');
|
|
85
|
+
let checksum = 0;
|
|
86
|
+
for (const byte of header) {
|
|
87
|
+
checksum += byte;
|
|
88
|
+
}
|
|
89
|
+
header.write(checksum.toString(8).padStart(6, '0'), 148, 'ascii');
|
|
90
|
+
header[154] = 0;
|
|
91
|
+
header[155] = 0x20;
|
|
92
|
+
await fs.writeFile(filePath, Buffer.concat([header, Buffer.alloc(1024, 0)]));
|
|
93
|
+
}
|
|
73
94
|
describe('workspace file service', () => {
|
|
74
95
|
const tempRoot = path.join(os.tmpdir(), `aws-workspace-files-${Date.now()}`);
|
|
75
96
|
const workspacePath = path.join(tempRoot, 'workspace');
|
|
@@ -201,6 +222,60 @@ describe('workspace file service', () => {
|
|
|
201
222
|
targetPath: workspacePath
|
|
202
223
|
})).rejects.toThrow(/Workspace root cannot be modified/);
|
|
203
224
|
});
|
|
225
|
+
it('moves files and rejects moving directories into themselves', async () => {
|
|
226
|
+
const moveSourceDir = path.join(workspacePath, 'move-source');
|
|
227
|
+
const moveDestinationDir = path.join(workspacePath, 'move-destination');
|
|
228
|
+
const fileToMove = path.join(moveSourceDir, 'move-me.txt');
|
|
229
|
+
await fs.mkdir(moveSourceDir, { recursive: true });
|
|
230
|
+
await fs.mkdir(moveDestinationDir, { recursive: true });
|
|
231
|
+
await fs.writeFile(fileToMove, 'move me', 'utf-8');
|
|
232
|
+
const result = await moveWorkspaceEntry({
|
|
233
|
+
workspacePath,
|
|
234
|
+
targetPath: fileToMove,
|
|
235
|
+
destinationPath: moveDestinationDir,
|
|
236
|
+
});
|
|
237
|
+
const movedPath = path.join(moveDestinationDir, 'move-me.txt');
|
|
238
|
+
expect(result.sourcePath).toBe(fileToMove.replace(/\\/g, '/'));
|
|
239
|
+
expect(result.targetPath).toBe((await fs.realpath(movedPath)).replace(/\\/g, '/'));
|
|
240
|
+
await expect(fs.access(fileToMove)).rejects.toThrow();
|
|
241
|
+
await expect(fs.readFile(movedPath, 'utf-8')).resolves.toBe('move me');
|
|
242
|
+
const childDestination = path.join(moveSourceDir, 'child');
|
|
243
|
+
await fs.mkdir(childDestination, { recursive: true });
|
|
244
|
+
await expect(moveWorkspaceEntry({
|
|
245
|
+
workspacePath,
|
|
246
|
+
targetPath: moveSourceDir,
|
|
247
|
+
destinationPath: childDestination,
|
|
248
|
+
})).rejects.toThrow(/itself or its descendant/);
|
|
249
|
+
});
|
|
250
|
+
it('rejects unsafe workspace move targets', async () => {
|
|
251
|
+
const moveSafetyDir = path.join(workspacePath, 'move-safety');
|
|
252
|
+
const moveSafetyFile = path.join(moveSafetyDir, 'safe.txt');
|
|
253
|
+
const moveSafetyDestination = path.join(workspacePath, 'move-safety-dest');
|
|
254
|
+
await fs.mkdir(moveSafetyDir, { recursive: true });
|
|
255
|
+
await fs.mkdir(moveSafetyDestination, { recursive: true });
|
|
256
|
+
await fs.writeFile(moveSafetyFile, 'safe', 'utf-8');
|
|
257
|
+
await fs.writeFile(path.join(moveSafetyDestination, 'safe.txt'), 'exists', 'utf-8');
|
|
258
|
+
await expect(moveWorkspaceEntry({
|
|
259
|
+
workspacePath,
|
|
260
|
+
targetPath: workspacePath,
|
|
261
|
+
destinationPath: moveSafetyDestination,
|
|
262
|
+
})).rejects.toThrow(/Workspace root cannot be modified/);
|
|
263
|
+
await expect(moveWorkspaceEntry({
|
|
264
|
+
workspacePath,
|
|
265
|
+
targetPath: path.join(tempRoot, 'outside.txt'),
|
|
266
|
+
destinationPath: moveSafetyDestination,
|
|
267
|
+
})).rejects.toThrow(/outside workspace/);
|
|
268
|
+
await expect(moveWorkspaceEntry({
|
|
269
|
+
workspacePath,
|
|
270
|
+
targetPath: moveSafetyFile,
|
|
271
|
+
destinationPath: tempRoot,
|
|
272
|
+
})).rejects.toThrow(/outside workspace/);
|
|
273
|
+
await expect(moveWorkspaceEntry({
|
|
274
|
+
workspacePath,
|
|
275
|
+
targetPath: moveSafetyFile,
|
|
276
|
+
destinationPath: moveSafetyDestination,
|
|
277
|
+
})).rejects.toThrow(/Path already exists/);
|
|
278
|
+
});
|
|
204
279
|
it('uploads files to a selected workspace directory', async () => {
|
|
205
280
|
const uploadSource = path.join(tempRoot, 'upload-source.txt');
|
|
206
281
|
const uploadTargetDir = path.join(workspacePath, 'uploads');
|
|
@@ -490,4 +565,13 @@ describe('workspace file service', () => {
|
|
|
490
565
|
outputPath: workspacePath
|
|
491
566
|
})).rejects.toThrow(/links are not supported/);
|
|
492
567
|
});
|
|
568
|
+
it('rejects tar archives with unsupported special entries', async () => {
|
|
569
|
+
const archivePath = path.join(workspacePath, 'special-entry.tar');
|
|
570
|
+
await writeTarHeaderOnlyEntry(archivePath, 'special-device', '3');
|
|
571
|
+
await expect(extractWorkspaceArchive({
|
|
572
|
+
workspacePath,
|
|
573
|
+
archivePath,
|
|
574
|
+
outputPath: workspacePath
|
|
575
|
+
})).rejects.toThrow(/Unsupported archive entry type/);
|
|
576
|
+
});
|
|
493
577
|
});
|