aws-runtime-bridge 1.6.1 → 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/dist/routes/pty.d.ts +2 -1
- package/dist/routes/pty.d.ts.map +1 -1
- package/dist/routes/pty.js +29 -4
- package/dist/routes/pty.test.js +9 -0
- package/dist/services/tool-installer.d.ts.map +1 -1
- package/dist/services/tool-installer.js +103 -59
- package/dist/services/tool-installer.test.js +25 -11
- package/package.json +1 -1
package/dist/routes/pty.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import type { IncomingMessage } from "node:http";
|
|
2
3
|
import type { Socket } from "node:net";
|
|
3
4
|
import type { Router } from "express";
|
|
@@ -38,7 +39,7 @@ export declare function resolvePtyIdleTtlMs(env?: NodeJS.ProcessEnv): number;
|
|
|
38
39
|
* 选择默认 shell。
|
|
39
40
|
* 主流程:Windows 优先 pwsh/powershell/cmd;Unix 优先 SHELL/bash/sh,返回可执行文件名与参数。
|
|
40
41
|
*/
|
|
41
|
-
export declare function resolveDefaultShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
|
|
42
|
+
export declare function resolveDefaultShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv, existsSync?: (path: fs.PathLike) => boolean): {
|
|
42
43
|
shell: string;
|
|
43
44
|
args: string[];
|
|
44
45
|
};
|
package/dist/routes/pty.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pty.d.ts","sourceRoot":"","sources":["../../src/routes/pty.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/pty.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Router as createRouter } from "express";
|
|
5
5
|
import * as pty from "node-pty";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
import WebSocket, { WebSocketServer } from "ws";
|
|
8
|
-
import {
|
|
8
|
+
import { isRuntimeTokenValid, validateToken } from "../middleware/auth.js";
|
|
9
9
|
import { createLogger } from "../utils/logger.js";
|
|
10
10
|
const log = createLogger("pty");
|
|
11
11
|
const DEFAULT_COLS = 80;
|
|
@@ -15,6 +15,15 @@ const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
|
15
15
|
const DEFAULT_EXITED_TTL_MS = 5 * 60 * 1000;
|
|
16
16
|
const DEFAULT_CONNECT_TOKEN_TTL_MS = 60 * 1000;
|
|
17
17
|
const DEFAULT_MAX_PTY_SESSIONS = 8;
|
|
18
|
+
const NON_INTERACTIVE_UNIX_SHELLS = new Set([
|
|
19
|
+
"false",
|
|
20
|
+
"halt",
|
|
21
|
+
"nologin",
|
|
22
|
+
"reboot",
|
|
23
|
+
"shutdown",
|
|
24
|
+
"sync",
|
|
25
|
+
"true",
|
|
26
|
+
]);
|
|
18
27
|
export const ptySessions = new Map();
|
|
19
28
|
const ptyConnectTokens = new Map();
|
|
20
29
|
/**
|
|
@@ -36,13 +45,29 @@ export function resolvePtyIdleTtlMs(env = process.env) {
|
|
|
36
45
|
* 选择默认 shell。
|
|
37
46
|
* 主流程:Windows 优先 pwsh/powershell/cmd;Unix 优先 SHELL/bash/sh,返回可执行文件名与参数。
|
|
38
47
|
*/
|
|
39
|
-
export function resolveDefaultShell(platform = process.platform, env = process.env) {
|
|
48
|
+
export function resolveDefaultShell(platform = process.platform, env = process.env, existsSync = fs.existsSync) {
|
|
40
49
|
if (platform === "win32") {
|
|
41
50
|
const comSpec = String(env.ComSpec || env.COMSPEC || "").trim();
|
|
42
51
|
return { shell: comSpec || "powershell.exe", args: [] };
|
|
43
52
|
}
|
|
44
53
|
const configuredShell = String(env.SHELL || "").trim();
|
|
45
|
-
|
|
54
|
+
const shell = [configuredShell, "/bin/bash", "/bin/sh"].find((candidate) => isUsableUnixShellCandidate(candidate, existsSync));
|
|
55
|
+
return { shell: shell || "/bin/sh", args: [] };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 判断 Unix shell 候选是否适合交互式 PTY。
|
|
59
|
+
* 具体逻辑:跳过 nologin/false 等会立即退出的系统 shell;绝对路径还必须真实存在。
|
|
60
|
+
*/
|
|
61
|
+
function isUsableUnixShellCandidate(candidate, existsSync) {
|
|
62
|
+
const shell = candidate.trim();
|
|
63
|
+
if (!shell) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const basename = path.basename(shell).toLowerCase();
|
|
67
|
+
if (NON_INTERACTIVE_UNIX_SHELLS.has(basename)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return !path.isAbsolute(shell) || existsSync(shell);
|
|
46
71
|
}
|
|
47
72
|
/**
|
|
48
73
|
* 校验并解析 shell 选择。
|
package/dist/routes/pty.test.js
CHANGED
|
@@ -20,6 +20,15 @@ afterEach(async () => {
|
|
|
20
20
|
ptyKill.mockClear();
|
|
21
21
|
});
|
|
22
22
|
describe('ordinary PTY persistence policy', () => {
|
|
23
|
+
it('falls back from non-interactive Linux login shells', async () => {
|
|
24
|
+
const { resolveDefaultShell } = await import('./pty.js');
|
|
25
|
+
expect(resolveDefaultShell('linux', { SHELL: '/usr/sbin/nologin' }, (candidate) => candidate === '/bin/bash')).toEqual({ shell: '/bin/bash', args: [] });
|
|
26
|
+
expect(resolveDefaultShell('linux', { SHELL: '/bin/false' }, (candidate) => candidate === '/bin/sh')).toEqual({ shell: '/bin/sh', args: [] });
|
|
27
|
+
});
|
|
28
|
+
it('falls back when Linux SHELL points to a missing absolute path', async () => {
|
|
29
|
+
const { resolveDefaultShell } = await import('./pty.js');
|
|
30
|
+
expect(resolveDefaultShell('linux', { SHELL: '/missing/shell' }, (candidate) => candidate === '/bin/bash')).toEqual({ shell: '/bin/bash', args: [] });
|
|
31
|
+
});
|
|
23
32
|
it('keeps persistent dashboard PTY sessions alive across browser disconnects', () => {
|
|
24
33
|
const source = readFileSync(resolve(currentDir, './pty.ts'), 'utf-8');
|
|
25
34
|
expect(source).toContain('persistent: boolean');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-installer.d.ts","sourceRoot":"","sources":["../../src/services/tool-installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-installer.d.ts","sourceRoot":"","sources":["../../src/services/tool-installer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;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,78 +1,69 @@
|
|
|
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";
|
|
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
|
-
packageName: "@anthropic-ai/claude-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
],
|
|
20
|
-
|
|
21
|
-
? [
|
|
22
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
23
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
24
|
-
]
|
|
25
|
-
: [
|
|
26
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
27
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
28
|
-
"rm -f ~/.local/bin/claude",
|
|
29
|
-
],
|
|
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
|
+
],
|
|
30
35
|
},
|
|
31
36
|
claudecode: {
|
|
32
37
|
key: "claudecode",
|
|
33
|
-
packageName: "@anthropic-ai/claude-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
],
|
|
42
|
-
|
|
43
|
-
? [
|
|
44
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
45
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
46
|
-
]
|
|
47
|
-
: [
|
|
48
|
-
"npm uninstall -g @anthropic-ai/claude-code",
|
|
49
|
-
"volta uninstall @anthropic-ai/claude-code",
|
|
50
|
-
"rm -f ~/.local/bin/claude",
|
|
51
|
-
],
|
|
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
|
+
],
|
|
52
48
|
},
|
|
53
49
|
opencode: {
|
|
54
50
|
key: "opencode",
|
|
55
|
-
packageName: "opencode-ai",
|
|
51
|
+
packageName: "@opencode-ai/sdk",
|
|
52
|
+
sdkPackageName: "@opencode-ai/sdk",
|
|
56
53
|
aliases: isWindows
|
|
57
54
|
? ["opencode.cmd", "opencode.exe", "opencode"]
|
|
58
55
|
: ["opencode"],
|
|
59
56
|
versionArgs: ["--version"],
|
|
60
|
-
installCommands:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"curl -fsSL https://opencode.ai/install | bash",
|
|
64
|
-
"npm install -g opencode-ai@latest",
|
|
65
|
-
],
|
|
57
|
+
installCommands: [
|
|
58
|
+
npmInstallIntoBridgeCommand(["@opencode-ai/sdk@latest", "opencode-ai@latest"]),
|
|
59
|
+
],
|
|
66
60
|
uninstallCommands: isWindows
|
|
67
61
|
? [
|
|
68
|
-
"opencode
|
|
69
|
-
"npm uninstall -g opencode-ai",
|
|
70
|
-
"volta uninstall opencode-ai",
|
|
62
|
+
npmUninstallFromBridgeCommand(["@opencode-ai/sdk", "opencode-ai"]),
|
|
71
63
|
]
|
|
72
64
|
: [
|
|
65
|
+
npmUninstallFromBridgeCommand(["@opencode-ai/sdk", "opencode-ai"]),
|
|
73
66
|
"opencode uninstall --force",
|
|
74
|
-
"npm uninstall -g opencode-ai",
|
|
75
|
-
"volta uninstall opencode-ai",
|
|
76
67
|
"rm -f ~/.opencode/bin/opencode",
|
|
77
68
|
],
|
|
78
69
|
extraSearchPaths: () => {
|
|
@@ -88,14 +79,12 @@ const TOOL_DEFINITIONS = {
|
|
|
88
79
|
},
|
|
89
80
|
codex: {
|
|
90
81
|
key: "codex",
|
|
91
|
-
packageName: "@openai/codex",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"volta uninstall @openai/codex",
|
|
98
|
-
],
|
|
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"])],
|
|
99
88
|
},
|
|
100
89
|
};
|
|
101
90
|
export const SUPPORTED_INSTALLABLE_TOOLS = Object.freeze(Object.keys(TOOL_DEFINITIONS));
|
|
@@ -271,6 +260,50 @@ async function resolveExecutableCandidate(definition) {
|
|
|
271
260
|
}
|
|
272
261
|
return { executable: null, version: null, error: "command not installed" };
|
|
273
262
|
}
|
|
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
|
+
}
|
|
274
307
|
async function runToolCommand(command) {
|
|
275
308
|
if (isWindows) {
|
|
276
309
|
await execFileAsync("cmd.exe", ["/d", "/s", "/c", command], {
|
|
@@ -298,6 +331,17 @@ export async function detectToolInstallStatus(tool) {
|
|
|
298
331
|
error: "unsupported tool",
|
|
299
332
|
};
|
|
300
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
|
+
}
|
|
301
345
|
const result = await resolveExecutableCandidate(definition);
|
|
302
346
|
return {
|
|
303
347
|
tool: normalizedTool,
|
|
@@ -76,30 +76,44 @@ describe('tool installer service', () => {
|
|
|
76
76
|
expect(typeof status.installed).toBe('boolean');
|
|
77
77
|
expect(status.error).not.toBe('unsupported tool');
|
|
78
78
|
});
|
|
79
|
+
it('detects OpenCode by SDK package instead of CLI availability', async () => {
|
|
80
|
+
const status = await detectToolInstallStatus('opencode');
|
|
81
|
+
expect(status.tool).toBe('opencode');
|
|
82
|
+
expect(status.executable === null || status.executable === '@opencode-ai/sdk').toBe(true);
|
|
83
|
+
if (!status.installed) {
|
|
84
|
+
expect(status.error).toContain('@opencode-ai/sdk');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it('detects Codex by SDK package instead of CLI availability', async () => {
|
|
88
|
+
const status = await detectToolInstallStatus('codex');
|
|
89
|
+
expect(status.tool).toBe('codex');
|
|
90
|
+
expect(status.executable === null || status.executable === '@openai/codex-sdk').toBe(true);
|
|
91
|
+
if (!status.installed) {
|
|
92
|
+
expect(status.error).toContain('@openai/codex-sdk');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
79
95
|
it('supports all panel tools as uninstallable tools', () => {
|
|
80
96
|
expect(SUPPORTED_UNINSTALLABLE_TOOLS).toEqual(expect.arrayContaining(['claude', 'opencode', 'codex']));
|
|
81
97
|
});
|
|
82
98
|
it('covers npm and native installer uninstall paths for OpenCode', () => {
|
|
83
99
|
const commands = getToolUninstallCommands('opencode');
|
|
84
|
-
expect(commands
|
|
85
|
-
expect(commands
|
|
100
|
+
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
101
|
+
expect(commands.some(command => command.includes('@opencode-ai/sdk'))).toBe(true);
|
|
102
|
+
expect(commands.some(command => command.includes('opencode-ai'))).toBe(true);
|
|
86
103
|
if (process.platform !== 'win32') {
|
|
87
104
|
expect(commands).toContain('opencode uninstall --force');
|
|
88
105
|
expect(commands).toContain('rm -f ~/.opencode/bin/opencode');
|
|
89
106
|
}
|
|
90
107
|
});
|
|
91
|
-
it('covers
|
|
108
|
+
it('covers bridge-local SDK uninstall path for Claude Code', () => {
|
|
92
109
|
const commands = getToolUninstallCommands('claude');
|
|
93
|
-
expect(commands
|
|
94
|
-
expect(commands
|
|
95
|
-
if (process.platform !== 'win32') {
|
|
96
|
-
expect(commands).toContain('rm -f ~/.local/bin/claude');
|
|
97
|
-
}
|
|
110
|
+
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
111
|
+
expect(commands.some(command => command.includes('@anthropic-ai/claude-agent-sdk'))).toBe(true);
|
|
98
112
|
});
|
|
99
|
-
it('covers
|
|
113
|
+
it('covers bridge-local SDK uninstall path for Codex', () => {
|
|
100
114
|
const commands = getToolUninstallCommands('codex');
|
|
101
|
-
expect(commands
|
|
102
|
-
expect(commands
|
|
115
|
+
expect(commands.some(command => command.includes('npm uninstall --prefix'))).toBe(true);
|
|
116
|
+
expect(commands.some(command => command.includes('@openai/codex-sdk'))).toBe(true);
|
|
103
117
|
});
|
|
104
118
|
it('recognizes Volta shim paths for package-manager-aware detection', () => {
|
|
105
119
|
expect(isVoltaShimPath('C:\\Users\\tester\\AppData\\Local\\Volta\\bin\\codex.cmd')).toBe(true);
|