fix: add support for capturing login shell environment snapshot (#455)
- Capture and cache environment variables from the user's login shell on Unix and Windows. - Support multiple shell candidates including zsh, bash, sh, PowerShell, and cmd. - Provide helper to merge PATH values from preferred and fallback sources.
This commit is contained in:
committed by
GitHub
parent
073f44de2a
commit
1d6895a9e7
+163
-33
@@ -2969,6 +2969,166 @@ async function ensureLocalOpenCodeServerPassword({ rotateManaged = false } = {})
|
||||
return generatedPassword;
|
||||
}
|
||||
|
||||
let cachedLoginShellEnvSnapshot = undefined;
|
||||
|
||||
function parseNullSeparatedEnvSnapshot(raw) {
|
||||
if (typeof raw !== 'string' || raw.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
const entries = raw.split('\0');
|
||||
for (const entry of entries) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
const idx = entry.indexOf('=');
|
||||
if (idx <= 0) {
|
||||
continue;
|
||||
}
|
||||
const key = entry.slice(0, idx);
|
||||
const value = entry.slice(idx + 1);
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
function getLoginShellEnvSnapshot() {
|
||||
if (cachedLoginShellEnvSnapshot !== undefined) {
|
||||
return cachedLoginShellEnvSnapshot;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const windowsSnapshot = getWindowsShellEnvSnapshot();
|
||||
cachedLoginShellEnvSnapshot = windowsSnapshot;
|
||||
return windowsSnapshot;
|
||||
}
|
||||
|
||||
const shellCandidates = [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh'].filter(Boolean);
|
||||
|
||||
for (const shellPath of shellCandidates) {
|
||||
if (!isExecutable(shellPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = spawnSync(shellPath, ['-lic', 'env -0'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseNullSeparatedEnvSnapshot(result.stdout || '');
|
||||
if (parsed) {
|
||||
cachedLoginShellEnvSnapshot = parsed;
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
cachedLoginShellEnvSnapshot = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function getWindowsShellEnvSnapshot() {
|
||||
const parseResult = (stdout) => parseNullSeparatedEnvSnapshot(typeof stdout === 'string' ? stdout : '');
|
||||
|
||||
const psScript =
|
||||
"Get-ChildItem Env: | ForEach-Object { [Console]::Out.Write($_.Name); [Console]::Out.Write('='); [Console]::Out.Write($_.Value); [Console]::Out.Write([char]0) }";
|
||||
|
||||
const powershellCandidates = [
|
||||
'pwsh.exe',
|
||||
'powershell.exe',
|
||||
path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
|
||||
];
|
||||
|
||||
for (const shellPath of powershellCandidates) {
|
||||
try {
|
||||
const result = spawnSync(shellPath, ['-NoLogo', '-Command', psScript], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseResult(result.stdout);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const comspec = process.env.ComSpec || 'cmd.exe';
|
||||
try {
|
||||
const result = spawnSync(comspec, ['/d', '/s', '/c', 'set'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
if (result.status === 0 && typeof result.stdout === 'string' && result.stdout.length > 0) {
|
||||
return parseNullSeparatedEnvSnapshot(result.stdout.replace(/\r?\n/g, '\0'));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function mergePathValues(preferred, fallback) {
|
||||
const merged = new Set();
|
||||
|
||||
const addSegments = (value) => {
|
||||
if (typeof value !== 'string' || !value) {
|
||||
return;
|
||||
}
|
||||
for (const segment of value.split(path.delimiter)) {
|
||||
if (segment) {
|
||||
merged.add(segment);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addSegments(preferred);
|
||||
addSegments(fallback);
|
||||
|
||||
return Array.from(merged).join(path.delimiter);
|
||||
}
|
||||
|
||||
function applyLoginShellEnvSnapshot() {
|
||||
const snapshot = getLoginShellEnvSnapshot();
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const skipKeys = new Set(['PWD', 'OLDPWD', 'SHLVL', '_']);
|
||||
|
||||
for (const [key, value] of Object.entries(snapshot)) {
|
||||
if (skipKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const existing = process.env[key];
|
||||
if (typeof existing === 'string' && existing.length > 0) {
|
||||
continue;
|
||||
}
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
process.env.PATH = mergePathValues(snapshot.PATH || '', process.env.PATH || '');
|
||||
}
|
||||
|
||||
applyLoginShellEnvSnapshot();
|
||||
|
||||
const ENV_CONFIGURED_API_PREFIX = normalizeApiPrefix(
|
||||
process.env.OPENCODE_API_PREFIX || process.env.OPENCHAMBER_API_PREFIX || ''
|
||||
);
|
||||
@@ -3604,42 +3764,12 @@ async function waitForOpenCodePort(timeoutMs = 15000) {
|
||||
throw new Error('Timed out waiting for OpenCode port');
|
||||
}
|
||||
|
||||
let cachedLoginShellPath = undefined;
|
||||
|
||||
function getLoginShellPath() {
|
||||
if (cachedLoginShellPath !== undefined) {
|
||||
return cachedLoginShellPath;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
cachedLoginShellPath = null;
|
||||
const snapshot = getLoginShellEnvSnapshot();
|
||||
if (!snapshot || typeof snapshot.PATH !== 'string' || snapshot.PATH.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shell = process.env.SHELL || '/bin/zsh';
|
||||
const shellName = path.basename(shell);
|
||||
|
||||
// Nushell requires different flag syntax and PATH access
|
||||
const isNushell = shellName === 'nu' || shellName === 'nushell';
|
||||
const args = isNushell
|
||||
? ['-l', '-i', '-c', '$env.PATH | str join (char esep)']
|
||||
: ['-lic', 'echo -n "$PATH"'];
|
||||
|
||||
try {
|
||||
const result = spawnSync(shell, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
if (result.status === 0 && typeof result.stdout === 'string') {
|
||||
const value = result.stdout.trim();
|
||||
if (value) {
|
||||
cachedLoginShellPath = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
cachedLoginShellPath = null;
|
||||
return null;
|
||||
return snapshot.PATH;
|
||||
}
|
||||
|
||||
function buildAugmentedPath() {
|
||||
|
||||
Reference in New Issue
Block a user