merge: sync fork with upstream/main (v1.1.31+)
This commit is contained in:
@@ -204,12 +204,38 @@ function buildWindowsShellCommandLine(command, args) {
|
||||
return [command, ...(args || [])].map(quoteWindowsShellArg).join(" ");
|
||||
}
|
||||
|
||||
function resolveWindowsShimToNativeExe(command, platform = process.platform) {
|
||||
if (platform !== "win32") return null;
|
||||
const normalized = String(command || "").trim();
|
||||
if (!normalized) return null;
|
||||
const ext = path.extname(normalized).toLowerCase();
|
||||
if (ext !== ".cmd" && ext !== ".bat") return null;
|
||||
if (!existsSync(normalized)) return null;
|
||||
try {
|
||||
const contents = readFileSync(normalized, "utf8");
|
||||
const shimDir = path.dirname(normalized);
|
||||
// Match patterns like: "%~dp0\..\node_modules\@anthropic-ai\claude-code\bin\claude.exe" %*
|
||||
// or: "%~dp0\..\@openai\codex\bin\codex.exe"
|
||||
const exeRefs = [...contents.matchAll(/"%~dp0\\([^"]+\.exe)"/gi)];
|
||||
for (const [, relativePath] of exeRefs) {
|
||||
const candidate = path.resolve(shimDir, relativePath.replace(/\\/g, "/"));
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function prepareCommandForSpawn(command, args) {
|
||||
const spawnArgs = Array.isArray(args) ? args : [];
|
||||
if (!shouldUseShellForCommand(command)) {
|
||||
return { command, args: spawnArgs, shell: false };
|
||||
}
|
||||
|
||||
const nativeExePath = resolveWindowsShimToNativeExe(command);
|
||||
if (nativeExePath) {
|
||||
return { command: nativeExePath, args: spawnArgs, shell: false };
|
||||
}
|
||||
|
||||
return {
|
||||
command: buildWindowsShellCommandLine(command, spawnArgs),
|
||||
args: [],
|
||||
@@ -231,6 +257,15 @@ function resolveClaudeCodeExecutableForSdk(claudeExecutablePath, platform = proc
|
||||
return packageCliPath;
|
||||
}
|
||||
|
||||
// Native binary check: Claude Code >= 2.1.169 ships as native exe with no cli.js
|
||||
const nativeExeCandidates = [
|
||||
path.join(baseDir, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"),
|
||||
path.join(baseDir, "..", "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"),
|
||||
];
|
||||
for (const exePath of nativeExeCandidates) {
|
||||
if (existsSync(exePath)) return exePath;
|
||||
}
|
||||
|
||||
const shimCandidates = [normalized];
|
||||
if (!ext) {
|
||||
shimCandidates.push(`${normalized}.cmd`, `${normalized}.bat`);
|
||||
@@ -264,6 +299,151 @@ function normalizeClaudeCodeExecutableEnvForSdk(env, platform = process.platform
|
||||
};
|
||||
}
|
||||
|
||||
const CODEX_WIN32_PLATFORM_PACKAGES = {
|
||||
x64: { triple: "x86_64-pc-windows-msvc", package: "@openai/codex-win32-x64" },
|
||||
arm64: { triple: "aarch64-pc-windows-msvc", package: "@openai/codex-win32-arm64" },
|
||||
};
|
||||
|
||||
function resolveCodexNativeExecutableWin32(moduleSearchDirs, arch = process.arch) {
|
||||
const archKey = arch === "arm64" ? "arm64" : "x64";
|
||||
const { triple, package: platformPackage } = CODEX_WIN32_PLATFORM_PACKAGES[archKey];
|
||||
|
||||
for (const dir of moduleSearchDirs) {
|
||||
if (!dir) continue;
|
||||
const candidates = [
|
||||
path.join(dir, "node_modules", platformPackage, "vendor", triple, "bin", "codex.exe"),
|
||||
path.join(dir, "node_modules", platformPackage, "vendor", triple, "codex", "codex.exe"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCodexNativeSearchDirsForShim(shimDir) {
|
||||
const dirs = [shimDir];
|
||||
const parentDir = path.dirname(shimDir);
|
||||
if (
|
||||
path.basename(shimDir).toLowerCase() === ".bin" &&
|
||||
path.basename(parentDir).toLowerCase() === "node_modules"
|
||||
) {
|
||||
dirs.push(path.dirname(parentDir));
|
||||
}
|
||||
dirs.push(path.join(shimDir, "node_modules", "@openai", "codex"));
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function getCodexNativePathDirsWin32(nativeExecutablePath) {
|
||||
const normalized = String(nativeExecutablePath || "").trim();
|
||||
if (!normalized || path.basename(normalized).toLowerCase() !== "codex.exe") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const executableDir = path.dirname(normalized);
|
||||
const packageRoot = path.dirname(executableDir);
|
||||
const dirs = [];
|
||||
if (path.basename(executableDir).toLowerCase() === "bin") {
|
||||
dirs.push(path.join(packageRoot, "codex-path"));
|
||||
} else if (path.basename(executableDir).toLowerCase() === "codex") {
|
||||
dirs.push(path.join(packageRoot, "path"));
|
||||
}
|
||||
return dirs.filter((dir) => existsSync(dir));
|
||||
}
|
||||
|
||||
function getPathEnvKey(env, platform = process.platform) {
|
||||
if (platform !== "win32") return "PATH";
|
||||
const keys = Object.keys(env || {}).filter((key) => key.toLowerCase() === "path");
|
||||
return keys.includes("Path") ? "Path" : keys.at(-1) || "PATH";
|
||||
}
|
||||
|
||||
function addCodexExecutableEnvForSdk(env, codexExecutablePath, platform = process.platform) {
|
||||
if (platform !== "win32" || !codexExecutablePath) return env;
|
||||
const pathDirs = getCodexNativePathDirsWin32(codexExecutablePath);
|
||||
if (pathDirs.length === 0) return env;
|
||||
|
||||
const nextEnv = { ...(env || {}) };
|
||||
const pathKey = getPathEnvKey(nextEnv, platform);
|
||||
for (const key of Object.keys(nextEnv)) {
|
||||
if (key.toLowerCase() === "path" && key !== pathKey) {
|
||||
delete nextEnv[key];
|
||||
}
|
||||
}
|
||||
const delimiter = platform === "win32" ? ";" : path.delimiter;
|
||||
const existingEntries = String(nextEnv[pathKey] || "")
|
||||
.split(delimiter)
|
||||
.filter((entry) => entry && !pathDirs.includes(entry));
|
||||
nextEnv[pathKey] = [...pathDirs, ...existingEntries].join(delimiter);
|
||||
return nextEnv;
|
||||
}
|
||||
|
||||
function resolveCodexExecutableForSdk(codexExecutablePath, platform = process.platform) {
|
||||
const normalized = String(codexExecutablePath || "").trim();
|
||||
if (!normalized) return null;
|
||||
if (platform !== "win32") return normalized;
|
||||
|
||||
const ext = path.extname(normalized).toLowerCase();
|
||||
if (ext === ".exe") return normalized;
|
||||
|
||||
const baseDir = path.dirname(normalized);
|
||||
const moduleSearchDirs = getCodexNativeSearchDirsForShim(baseDir);
|
||||
|
||||
if (ext === ".js" && /[\\/]codex\.js$/i.test(normalized)) {
|
||||
const codexPackageRoot = path.dirname(path.dirname(normalized));
|
||||
const globalPrefix = path.resolve(codexPackageRoot, "..", "..", "..");
|
||||
const nativeExe = resolveCodexNativeExecutableWin32([
|
||||
globalPrefix,
|
||||
codexPackageRoot,
|
||||
...moduleSearchDirs,
|
||||
]);
|
||||
if (nativeExe) return nativeExe;
|
||||
}
|
||||
|
||||
if (ext && ext !== ".cmd" && ext !== ".bat" && ext !== ".ps1") {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const nativeExe = resolveCodexNativeExecutableWin32(moduleSearchDirs);
|
||||
if (nativeExe) return nativeExe;
|
||||
|
||||
const shimCandidates = [normalized];
|
||||
if (!ext) {
|
||||
shimCandidates.push(`${normalized}.cmd`, `${normalized}.bat`);
|
||||
}
|
||||
|
||||
for (const shimPath of shimCandidates) {
|
||||
try {
|
||||
if (!existsSync(shimPath)) continue;
|
||||
const contents = readFileSync(shimPath, "utf8");
|
||||
if (!/@openai[\\/]codex[\\/]bin[\\/]codex\.js/i.test(contents)) {
|
||||
continue;
|
||||
}
|
||||
const resolved = resolveCodexNativeExecutableWin32(moduleSearchDirs);
|
||||
if (resolved) return resolved;
|
||||
} catch {
|
||||
// Fall back to the original executable path below.
|
||||
}
|
||||
}
|
||||
|
||||
return ext === ".cmd" || ext === ".bat" || ext === ".ps1" ? null : normalized;
|
||||
}
|
||||
|
||||
function resolveSdkBinPath(command, shellEnv, platform = process.platform) {
|
||||
const raw = resolveCliFromPath(command, shellEnv);
|
||||
if (!raw) return null;
|
||||
if (platform !== "win32") return raw;
|
||||
if (command === "codex") {
|
||||
return resolveCodexExecutableForSdk(raw, platform);
|
||||
}
|
||||
if (command === "claude") {
|
||||
return resolveClaudeCodeExecutableForSdk(raw, platform);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function resolveCliFromPath(command, shellEnv) {
|
||||
// Validate command: only allow valid binary names (alphanumeric, hyphens, underscores, dots)
|
||||
if (!command || !/^[a-zA-Z0-9._-]+$/.test(command)) {
|
||||
@@ -438,8 +618,12 @@ module.exports = {
|
||||
quoteWindowsShellArg,
|
||||
buildWindowsShellCommandLine,
|
||||
prepareCommandForSpawn,
|
||||
resolveWindowsShimToNativeExe,
|
||||
resolveClaudeCodeExecutableForSdk,
|
||||
normalizeClaudeCodeExecutableEnvForSdk,
|
||||
resolveCodexExecutableForSdk,
|
||||
addCodexExecutableEnvForSdk,
|
||||
resolveSdkBinPath,
|
||||
resolveCliFromPath,
|
||||
toUnpackedAsarPath,
|
||||
isPlausibleCliVersionOutput,
|
||||
|
||||
@@ -2,6 +2,7 @@ const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const {
|
||||
addCodexExecutableEnvForSdk,
|
||||
buildWindowsShellCommandLine,
|
||||
extractTrailingIdlePrompt,
|
||||
getFreshIdlePrompt,
|
||||
@@ -9,7 +10,9 @@ const {
|
||||
isPlausibleCliVersionOutput,
|
||||
looksLikeIdleAutoLogout,
|
||||
prepareCommandForSpawn,
|
||||
resolveWindowsShimToNativeExe,
|
||||
resolveClaudeCodeExecutableForSdk,
|
||||
resolveCodexExecutableForSdk,
|
||||
trackSessionIdlePrompt,
|
||||
} = require("./shellUtils.cjs");
|
||||
const fs = require("node:fs");
|
||||
@@ -149,6 +152,196 @@ test("resolveClaudeCodeExecutableForSdk keeps Windows cmd shim when Claude Code
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveClaudeCodeExecutableForSdk maps Windows npm cmd shim to native claude.exe when cli.js is absent", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-claude-native-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "claude.cmd");
|
||||
const nativeExe = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe");
|
||||
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
|
||||
fs.writeFileSync(nativeExe, "", "utf8");
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'@ECHO off\r\n"%~dp0\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.equal(resolveClaudeCodeExecutableForSdk(shimPath, "win32"), nativeExe);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveWindowsShimToNativeExe resolves npm .cmd shim to native exe", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-shim-native-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "claude.cmd");
|
||||
const nativeExe = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe");
|
||||
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
|
||||
fs.writeFileSync(nativeExe, "", "utf8");
|
||||
// Single backslashes in the .cmd content (%~dp0 expands to the shim dir)
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'@ECHO off\r\n"%~dp0\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const resolved = resolveWindowsShimToNativeExe(shimPath, "win32");
|
||||
assert.equal(resolved, nativeExe);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("prepareCommandForSpawn resolves Windows cmd shim to native exe with shell:false", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-spawn-native-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "claude.cmd");
|
||||
const nativeExe = path.join(tmp, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe");
|
||||
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
|
||||
fs.writeFileSync(nativeExe, "", "utf8");
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'@ECHO off\r\n"%~dp0\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = prepareCommandForSpawn(shimPath, ["--version"]);
|
||||
if (process.platform === "win32") {
|
||||
assert.deepEqual(result, {
|
||||
command: nativeExe,
|
||||
args: ["--version"],
|
||||
shell: false,
|
||||
});
|
||||
} else {
|
||||
// On non-Windows, resolveWindowsShimToNativeExe is skipped; verify win32 behavior explicitly.
|
||||
assert.equal(resolveWindowsShimToNativeExe(shimPath, "win32"), nativeExe);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function writeCodexWin32NativeLayout(globalPrefix, arch = process.arch === "arm64" ? "arm64" : "x64") {
|
||||
const triple = arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc";
|
||||
const platformPackage = arch === "arm64" ? "@openai/codex-win32-arm64" : "@openai/codex-win32-x64";
|
||||
const nativeExe = path.join(
|
||||
globalPrefix,
|
||||
"node_modules",
|
||||
platformPackage,
|
||||
"vendor",
|
||||
triple,
|
||||
"bin",
|
||||
"codex.exe",
|
||||
);
|
||||
fs.mkdirSync(path.dirname(nativeExe), { recursive: true });
|
||||
fs.writeFileSync(nativeExe, "", "utf8");
|
||||
return nativeExe;
|
||||
}
|
||||
|
||||
test("resolveCodexExecutableForSdk maps Windows npm cmd shim to native codex.exe", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-shim-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "codex.cmd");
|
||||
const nativeExe = writeCodexWin32NativeLayout(tmp);
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'@ECHO off\r\nnode "%~dp0\\node_modules\\@openai\\codex\\bin\\codex.js" %*\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveCodexExecutableForSdk maps Windows local npm bin shim to native codex.exe", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-local-shim-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "node_modules", ".bin", "codex.cmd");
|
||||
const nativeExe = writeCodexWin32NativeLayout(tmp);
|
||||
fs.mkdirSync(path.dirname(shimPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'@ECHO off\r\nnode "%~dp0\\..\\@openai\\codex\\bin\\codex.js" %*\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveCodexExecutableForSdk leaves non-Windows Codex paths unchanged", () => {
|
||||
assert.equal(
|
||||
resolveCodexExecutableForSdk("/usr/local/bin/codex", "darwin"),
|
||||
"/usr/local/bin/codex",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveCodexExecutableForSdk returns null for Windows cmd shim when native codex.exe is missing", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-missing-native-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "codex.cmd");
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'@ECHO off\r\nnode "%~dp0\\node_modules\\@openai\\codex\\bin\\codex.js" %*\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), null);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveCodexExecutableForSdk maps Windows PowerShell shim to native codex.exe", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-ps1-shim-"));
|
||||
try {
|
||||
const shimPath = path.join(tmp, "codex.ps1");
|
||||
const nativeExe = writeCodexWin32NativeLayout(tmp);
|
||||
fs.writeFileSync(
|
||||
shimPath,
|
||||
'& "$basedir/node_modules/@openai/codex/bin/codex.js" $args\r\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.equal(resolveCodexExecutableForSdk(shimPath, "win32"), nativeExe);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveCodexExecutableForSdk maps codex.js entry to native codex.exe", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-js-entry-"));
|
||||
try {
|
||||
const codexJs = path.join(tmp, "node_modules", "@openai", "codex", "bin", "codex.js");
|
||||
const nativeExe = writeCodexWin32NativeLayout(tmp);
|
||||
fs.mkdirSync(path.dirname(codexJs), { recursive: true });
|
||||
fs.writeFileSync(codexJs, "", "utf8");
|
||||
|
||||
assert.equal(resolveCodexExecutableForSdk(codexJs, "win32"), nativeExe);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("addCodexExecutableEnvForSdk prepends bundled Codex path dir on Windows", () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-codex-env-path-"));
|
||||
try {
|
||||
const nativeExe = writeCodexWin32NativeLayout(tmp);
|
||||
const pathDir = path.join(path.dirname(path.dirname(nativeExe)), "codex-path");
|
||||
fs.mkdirSync(pathDir, { recursive: true });
|
||||
|
||||
const env = addCodexExecutableEnvForSdk({ Path: "C:\\Windows\\System32" }, nativeExe, "win32");
|
||||
|
||||
assert.equal(env.Path, `${pathDir};C:\\Windows\\System32`);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("tracks PowerShell idle prompt after SSH output", () => {
|
||||
const session = {};
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ const {
|
||||
normalizeCliPathForPlatform,
|
||||
prepareCommandForSpawn,
|
||||
normalizeClaudeCodeExecutableEnvForSdk,
|
||||
addCodexExecutableEnvForSdk,
|
||||
resolveSdkBinPath,
|
||||
resolveCliFromPath,
|
||||
isPlausibleCliVersionOutput,
|
||||
getShellEnv,
|
||||
@@ -636,6 +638,8 @@ function createHandlerContext(ipcMain) {
|
||||
normalizeCliPathForPlatform,
|
||||
prepareCommandForSpawn,
|
||||
normalizeClaudeCodeExecutableEnvForSdk,
|
||||
addCodexExecutableEnvForSdk,
|
||||
resolveSdkBinPath,
|
||||
resolveCliFromPath,
|
||||
probeClaudeAuth,
|
||||
probeCopilotAuth,
|
||||
|
||||
@@ -101,16 +101,19 @@ function createAgentCliHelpers(ctx) {
|
||||
if (cached && now - cached.checkedAt < maxAgeMs) return cached;
|
||||
|
||||
const shellEnv = await getShellEnv();
|
||||
const codexPath = resolveCliFromPath("codex", shellEnv);
|
||||
if (!codexPath) {
|
||||
const rawCodexPath = resolveCliFromPath("codex", shellEnv);
|
||||
if (!rawCodexPath) {
|
||||
const result = { ok: false, checkedAt: now, error: "codex binary not found", code: "ENOENT" };
|
||||
setCodexValidationCache(result);
|
||||
return result;
|
||||
}
|
||||
const codexPath = resolveSdkBinPath("codex", shellEnv);
|
||||
try {
|
||||
// Minimal read-only probe turn through the SDK to confirm auth works.
|
||||
const { Codex } = await import("@openai/codex-sdk");
|
||||
const codex = new Codex({ codexPathOverride: codexPath, env: shellEnv });
|
||||
const codexOptions = { env: addCodexExecutableEnvForSdk(shellEnv, codexPath) };
|
||||
if (codexPath) codexOptions.codexPathOverride = codexPath;
|
||||
const codex = new Codex(codexOptions);
|
||||
const thread = codex.startThread({ skipGitRepoCheck: true });
|
||||
const { events } = await thread.runStreamed("ping", { sandbox: "read-only" });
|
||||
let failed = null;
|
||||
|
||||
@@ -173,7 +173,7 @@ function registerSdkStreamHandlers(ctx) {
|
||||
const claudeSettings = normalizedAgentEnv.NETCATTY_CLAUDE_SETTINGS;
|
||||
delete normalizedAgentEnv.NETCATTY_CLAUDE_SETTINGS;
|
||||
|
||||
const env = buildSdkAgentEnv({
|
||||
let env = buildSdkAgentEnv({
|
||||
shellEnv,
|
||||
requestedAgentEnv: normalizedAgentEnv,
|
||||
withCliDiscoveryEnv,
|
||||
|
||||
@@ -76,6 +76,7 @@ function createSessionOpsApi(ctx) {
|
||||
|
||||
async function getSessionPwd(event, payload) {
|
||||
const { sessionId } = payload;
|
||||
const allowHomeFallback = payload?.allowHomeFallback !== false;
|
||||
const session = sessions.get(sessionId);
|
||||
|
||||
if (!session || !session.conn) {
|
||||
@@ -98,12 +99,13 @@ function createSessionOpsApi(ctx) {
|
||||
// 2. Follows foreground child shells only, which covers bash->fish
|
||||
// without mistaking background shell scripts for the active shell.
|
||||
// 3. Reads /proc/<pid>/cwd via readlink.
|
||||
// 4. Falls back to the user's home directory if anything fails.
|
||||
// 4. Falls back to the user's home directory if the caller allows it.
|
||||
//
|
||||
// `exec` makes sh replace the user's login shell (fish/bash/...)
|
||||
// so sh keeps the same PID and $PPID = sshd. Starting another shell
|
||||
// without exec would make $PPID point at the intermediate shell instead.
|
||||
const posixScript = `SELF=$$
|
||||
ALLOW_FALLBACK=${allowHomeFallback ? "1" : "0"}
|
||||
# Find the user's interactive shell on this SSH connection.
|
||||
# Prefer the one attached to a controlling tty (the user's shell): probe exec
|
||||
# channels like this one have no tty ("?"), and ps output is unsorted, so
|
||||
@@ -193,11 +195,12 @@ function createSessionOpsApi(ctx) {
|
||||
# this unprivileged exec channel cannot read a su'd / sudo'd shell owned by
|
||||
# another user. Fall back to the same-uid login shell's cwd before giving up
|
||||
# to the home directory (#1065 review).
|
||||
if [ -z "$cwd" ] && [ "$pid" != "$login" ]; then
|
||||
if [ -z "$cwd" ] && [ "$pid" != "$login" ] && [ "$ALLOW_FALLBACK" = "1" ]; then
|
||||
cwd=$(readlink /proc/$login/cwd 2>/dev/null)
|
||||
fi
|
||||
[ -n "$cwd" ] && printf '%s\\n' "$cwd" && exit 0
|
||||
fi
|
||||
[ "$ALLOW_FALLBACK" = "1" ] || exit 1
|
||||
emit_home() {
|
||||
case "$1" in
|
||||
/*) printf '%s\\n' "$1"; exit 0 ;;
|
||||
|
||||
@@ -59,8 +59,11 @@ function createPreloadApi(ctx) {
|
||||
execCommand: async (options) => {
|
||||
return ipcRenderer.invoke("netcatty:ssh:exec", options);
|
||||
},
|
||||
getSessionPwd: async (sessionId) => {
|
||||
return ipcRenderer.invoke("netcatty:ssh:pwd", { sessionId });
|
||||
getSessionPwd: async (sessionId, options) => {
|
||||
return ipcRenderer.invoke("netcatty:ssh:pwd", {
|
||||
sessionId,
|
||||
allowHomeFallback: options?.allowHomeFallback,
|
||||
});
|
||||
},
|
||||
getSessionRemoteInfo: async (sessionId) => {
|
||||
return ipcRenderer.invoke("netcatty:ssh:remoteInfo", { sessionId });
|
||||
|
||||
Reference in New Issue
Block a user