import * as OS from "node:os";import { execFileSync } from "node:child_process"; const PATH_CAPTURE_START = "__T3CODE_PATH_START__";const PATH_CAPTURE_END = "__T3CODE_PATH_END__";const SHELL_ENV_NAME_PATTERN = /^[A-Z0-9_]+$/; type ExecFileSyncLike = ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number },) => string; function trimNonEmpty(value: string | null | undefined): string | undefined { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : undefined;} function readUserLoginShell(): string | undefined { try { return trimNonEmpty(OS.userInfo().shell); } catch { return undefined; }} export function listLoginShellCandidates( platform: NodeJS.Platform, shell: string | undefined, userShell = readUserLoginShell(),): ReadonlyArray<string> { const fallbackShell = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : undefined; const seen = new Set<string>(); const candidates: string[] = []; for (const candidate of [trimNonEmpty(shell), trimNonEmpty(userShell), fallbackShell]) { if (!candidate || seen.has(candidate)) { continue; } seen.add(candidate); candidates.push(candidate); } return candidates;} export function extractPathFromShellOutput(output: string): string | null { const startIndex = output.indexOf(PATH_CAPTURE_START); if (startIndex === -1) return null; const valueStartIndex = startIndex + PATH_CAPTURE_START.length; const endIndex = output.indexOf(PATH_CAPTURE_END, valueStartIndex); if (endIndex === -1) return null; const pathValue = output.slice(valueStartIndex, endIndex).trim(); return pathValue.length > 0 ? pathValue : null;} export function readPathFromLoginShell( shell: string, execFile: ExecFileSyncLike = execFileSync,): string | undefined { return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH;} export function readPathFromLaunchctl( execFile: ExecFileSyncLike = execFileSync,): string | undefined { try { return trimNonEmpty( execFile("/bin/launchctl", ["getenv", "PATH"], { encoding: "utf8", timeout: 2000, }), ); } catch { return undefined; }} export function mergePathEntries( preferredPath: string | undefined, inheritedPath: string | undefined, platform: NodeJS.Platform,): string | undefined { const delimiter = platform === "win32" ? ";" : ":"; const merged: string[] = []; const seen = new Set<string>(); for (const pathValue of [preferredPath, inheritedPath]) { if (!pathValue) continue; for (const entry of pathValue.split(delimiter)) { const trimmedEntry = entry.trim(); if (!trimmedEntry || seen.has(trimmedEntry)) { continue; } seen.add(trimmedEntry); merged.push(trimmedEntry); } } return merged.length > 0 ? merged.join(delimiter) : undefined;} function envCaptureStart(name: string): string { return `__T3CODE_ENV_${name}_START__`;} function envCaptureEnd(name: string): string { return `__T3CODE_ENV_${name}_END__`;} function buildEnvironmentCaptureCommand(names: ReadonlyArray<string>): string { return names .map((name) => { if (!SHELL_ENV_NAME_PATTERN.test(name)) { throw new Error(`Unsupported environment variable name: ${name}`); } return [ `printf '%s\\n' '${envCaptureStart(name)}'`, `printenv ${name} || true`, `printf '%s\\n' '${envCaptureEnd(name)}'`, ].join("; "); }) .join("; ");} function extractEnvironmentValue(output: string, name: string): string | undefined { const startMarker = envCaptureStart(name); const endMarker = envCaptureEnd(name); const startIndex = output.indexOf(startMarker); if (startIndex === -1) return undefined; const valueStartIndex = startIndex + startMarker.length; const endIndex = output.indexOf(endMarker, valueStartIndex); if (endIndex === -1) return undefined; let value = output.slice(valueStartIndex, endIndex); if (value.startsWith("\n")) { value = value.slice(1); } if (value.endsWith("\n")) { value = value.slice(0, -1); } return value.length > 0 ? value : undefined;} export type ShellEnvironmentReader = ( shell: string, names: ReadonlyArray<string>, execFile?: ExecFileSyncLike,) => Partial<Record<string, string>>; export const readEnvironmentFromLoginShell: ShellEnvironmentReader = ( shell, names, execFile = execFileSync,) => { if (names.length === 0) { return {}; } const output = execFile(shell, ["-ilc", buildEnvironmentCaptureCommand(names)], { encoding: "utf8", timeout: 5000, }); const environment: Partial<Record<string, string>> = {}; for (const name of names) { const value = extractEnvironmentValue(output, name); if (value !== undefined) { environment[name] = value; } } return environment;};
import * as OS from "node:os";
import { execFileSync } from "node:child_process";
const PATH_CAPTURE_START = "__T3CODE_PATH_START__";
const PATH_CAPTURE_END = "__T3CODE_PATH_END__";
const SHELL_ENV_NAME_PATTERN = /^[A-Z0-9_]+$/;
type ExecFileSyncLike = (
file: string,
args: ReadonlyArray<string>,
options: { encoding: "utf8"; timeout: number },
) => string;
function trimNonEmpty(value: string | null | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : undefined;
}
function readUserLoginShell(): string | undefined {
try {
return trimNonEmpty(OS.userInfo().shell);
} catch {
return undefined;
}
}
export function listLoginShellCandidates(
platform: NodeJS.Platform,
shell: string | undefined,
userShell = readUserLoginShell(),
): ReadonlyArray<string> {
const fallbackShell =
platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : undefined;
const seen = new Set<string>();
const candidates: string[] = [];
for (const candidate of [trimNonEmpty(shell), trimNonEmpty(userShell), fallbackShell]) {
if (!candidate || seen.has(candidate)) {
continue;
}
seen.add(candidate);
candidates.push(candidate);
}
return candidates;
}
export function extractPathFromShellOutput(output: string): string | null {
const startIndex = output.indexOf(PATH_CAPTURE_START);
if (startIndex === -1) return null;
const valueStartIndex = startIndex + PATH_CAPTURE_START.length;
const endIndex = output.indexOf(PATH_CAPTURE_END, valueStartIndex);
if (endIndex === -1) return null;
const pathValue = output.slice(valueStartIndex, endIndex).trim();
return pathValue.length > 0 ? pathValue : null;
}
export function readPathFromLoginShell(
shell: string,
execFile: ExecFileSyncLike = execFileSync,
): string | undefined {
return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH;
}
export function readPathFromLaunchctl(
execFile: ExecFileSyncLike = execFileSync,
): string | undefined {
try {
return trimNonEmpty(
execFile("/bin/launchctl", ["getenv", "PATH"], {
encoding: "utf8",
timeout: 2000,
}),
);
} catch {
return undefined;
}
}
export function mergePathEntries(
preferredPath: string | undefined,
inheritedPath: string | undefined,
platform: NodeJS.Platform,
): string | undefined {
const delimiter = platform === "win32" ? ";" : ":";
const merged: string[] = [];
const seen = new Set<string>();
for (const pathValue of [preferredPath, inheritedPath]) {
if (!pathValue) continue;
for (const entry of pathValue.split(delimiter)) {
const trimmedEntry = entry.trim();
if (!trimmedEntry || seen.has(trimmedEntry)) {
continue;
}
seen.add(trimmedEntry);
merged.push(trimmedEntry);
}
}
return merged.length > 0 ? merged.join(delimiter) : undefined;
}
function envCaptureStart(name: string): string {
return `__T3CODE_ENV_${name}_START__`;
}
function envCaptureEnd(name: string): string {
return `__T3CODE_ENV_${name}_END__`;
}
function buildEnvironmentCaptureCommand(names: ReadonlyArray<string>): string {
return names
.map((name) => {
if (!SHELL_ENV_NAME_PATTERN.test(name)) {
throw new Error(`Unsupported environment variable name: ${name}`);
}
return [
`printf '%s\\n' '${envCaptureStart(name)}'`,
`printenv ${name} || true`,
`printf '%s\\n' '${envCaptureEnd(name)}'`,
].join("; ");
})
.join("; ");
}
function extractEnvironmentValue(output: string, name: string): string | undefined {
const startMarker = envCaptureStart(name);
const endMarker = envCaptureEnd(name);
const startIndex = output.indexOf(startMarker);
if (startIndex === -1) return undefined;
const valueStartIndex = startIndex + startMarker.length;
const endIndex = output.indexOf(endMarker, valueStartIndex);
if (endIndex === -1) return undefined;
let value = output.slice(valueStartIndex, endIndex);
if (value.startsWith("\n")) {
value = value.slice(1);
}
if (value.endsWith("\n")) {
value = value.slice(0, -1);
}
return value.length > 0 ? value : undefined;
}
export type ShellEnvironmentReader = (
shell: string,
names: ReadonlyArray<string>,
execFile?: ExecFileSyncLike,
) => Partial<Record<string, string>>;
export const readEnvironmentFromLoginShell: ShellEnvironmentReader = (
shell,
names,
execFile = execFileSync,
) => {
if (names.length === 0) {
return {};
}
const output = execFile(shell, ["-ilc", buildEnvironmentCaptureCommand(names)], {
encoding: "utf8",
timeout: 5000,
});
const environment: Partial<Record<string, string>> = {};
for (const name of names) {
const value = extractEnvironmentValue(output, name);
if (value !== undefined) {
environment[name] = value;
}
}
return environment;
};