Download packages/kaos/src/environment.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 9.98 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/kaos/src/environment.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/kaos/src/environment.ts
-
curl -L -o environment.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/kaos/src/environment.ts
9.98 kB
| /** | |
| * Environment — cross-platform probe of OS / shell. | |
| * | |
| * Detection is a pure function of injected probes (`platform` / `arch` / | |
| * `release` / `env` / `isFile` / `execFileText`) so the same suite runs | |
| * identically on any host OS. `detectEnvironmentFromNode()` bundles the Node | |
| * defaults for production callers. | |
| * | |
| * On Windows the probe expects bash from Git for Windows or MSYS2. If it | |
| * cannot be located the function throws `KaosShellNotFoundError`; the SDK | |
| * layer can wrap that into a user-facing install hint. Set | |
| * `KIMI_SHELL_PATH` to override. | |
| */ | |
| import { execFile as nodeExecFile } from 'node:child_process'; | |
| import { constants as fsConstants } from 'node:fs'; | |
| import { access } from 'node:fs/promises'; | |
| import * as nodeOs from 'node:os'; | |
| import * as nodePath from 'node:path'; | |
| import { KaosShellNotFoundError } from './errors'; | |
| // `OsKind` carries 'macOS' / 'Linux' / 'Windows' for known platforms and | |
| // falls back to the raw `process.platform` string for unknown ones (e.g. | |
| // 'freebsd'). Typed as `string` so the union isn't inhabited-by-string. | |
| export type OsKind = string; | |
| export type ShellName = 'bash' | 'sh'; | |
| export interface Environment { | |
| readonly osKind: OsKind; | |
| readonly osArch: string; | |
| readonly osVersion: string; | |
| readonly shellName: ShellName; | |
| readonly shellPath: string; | |
| } | |
| export interface EnvironmentDeps { | |
| // Accepts the full Node `Platform` enum plus arbitrary strings for | |
| // forward-compatible OS kinds. | |
| readonly platform: string; | |
| readonly arch: string; | |
| readonly release: string; | |
| readonly env: Record<string, string | undefined>; | |
| readonly isFile: (path: string) => Promise<boolean>; | |
| readonly execFileText: ( | |
| file: string, | |
| args: readonly string[], | |
| timeoutMs: number, | |
| ) => Promise<string | undefined>; | |
| } | |
| const GIT_EXEC_PATH_TIMEOUT_MS = 5_000; | |
| const MINGW_PREFIX_SET: ReadonlySet<string> = new Set([ | |
| 'mingw32', | |
| 'mingw64', | |
| 'ucrt64', | |
| 'clang64', | |
| 'clangarm64', | |
| ]); | |
| function resolveOsKind(platform: string): OsKind { | |
| switch (platform) { | |
| case 'darwin': | |
| return 'macOS'; | |
| case 'linux': | |
| return 'Linux'; | |
| case 'win32': | |
| return 'Windows'; | |
| default: | |
| return platform; | |
| } | |
| } | |
| export async function detectEnvironment(deps: EnvironmentDeps): Promise<Environment> { | |
| const osKind = resolveOsKind(deps.platform); | |
| const osArch = deps.arch; | |
| const osVersion = deps.release; | |
| if (deps.platform === 'win32') { | |
| const shellPath = await locateWindowsGitBash(deps); | |
| return { osKind, osArch, osVersion, shellName: 'bash', shellPath }; | |
| } | |
| const candidates: readonly string[] = ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash']; | |
| let found: string | undefined; | |
| for (const p of candidates) { | |
| if (await deps.isFile(p)) { | |
| found = p; | |
| break; | |
| } | |
| } | |
| if (found !== undefined) { | |
| return { osKind, osArch, osVersion, shellName: 'bash', shellPath: found }; | |
| } | |
| return { osKind, osArch, osVersion, shellName: 'sh', shellPath: '/bin/sh' }; | |
| } | |
| async function locateWindowsGitBash(deps: EnvironmentDeps): Promise<string> { | |
| const checked: string[] = []; | |
| const override = deps.env['KIMI_SHELL_PATH']?.trim(); | |
| if (override !== undefined && override.length > 0) { | |
| checked.push(override); | |
| if (await deps.isFile(override)) { | |
| return override; | |
| } | |
| } | |
| const gitExecutables = await findExecutablesOnPath( | |
| 'git.exe', | |
| deps.env['PATH'], | |
| deps.platform, | |
| deps.isFile, | |
| ); | |
| for (const gitExe of gitExecutables) { | |
| const inferred = gitBashCandidatesFromGitExe(gitExe); | |
| if (inferred !== undefined) { | |
| for (const candidate of inferred) { | |
| checked.push(candidate); | |
| if (await deps.isFile(candidate)) { | |
| return candidate; | |
| } | |
| } | |
| } | |
| const gitExecPath = await readGitExecPath(deps, gitExe); | |
| if (gitExecPath === undefined) { | |
| continue; | |
| } | |
| for (const candidate of gitBashCandidatesFromGitExecPath(gitExecPath)) { | |
| checked.push(candidate); | |
| if (await deps.isFile(candidate)) { | |
| return candidate; | |
| } | |
| } | |
| } | |
| const candidates: string[] = [ | |
| 'C:\\Program Files\\Git\\bin\\bash.exe', | |
| 'C:\\Program Files\\Git\\usr\\bin\\bash.exe', | |
| 'C:\\Program Files (x86)\\Git\\bin\\bash.exe', | |
| 'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe', | |
| ]; | |
| const localAppData = deps.env['LOCALAPPDATA']?.trim(); | |
| if (localAppData !== undefined && localAppData.length > 0) { | |
| candidates.push(`${localAppData}\\Programs\\Git\\bin\\bash.exe`); | |
| candidates.push(`${localAppData}\\Programs\\Git\\usr\\bin\\bash.exe`); | |
| } | |
| for (const candidate of candidates) { | |
| checked.push(candidate); | |
| if (await deps.isFile(candidate)) { | |
| return candidate; | |
| } | |
| } | |
| throw new KaosShellNotFoundError( | |
| `Git Bash was not found on this Windows host. Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe. Checked: ${checked.join(', ')}.`, | |
| ); | |
| } | |
| async function readGitExecPath( | |
| deps: EnvironmentDeps, | |
| gitExe: string, | |
| ): Promise<string | undefined> { | |
| if (deps.platform === 'win32' && !isAbsoluteWindowsPath(gitExe)) return undefined; | |
| const stdout = await deps.execFileText(gitExe, ['--exec-path'], GIT_EXEC_PATH_TIMEOUT_MS); | |
| if (stdout === undefined) return undefined; | |
| for (const line of stdout.split(/\r?\n/)) { | |
| const execPath = line.trim(); | |
| if (execPath.length > 0) { | |
| return execPath; | |
| } | |
| } | |
| return undefined; | |
| } | |
| // Most Git for Windows installs put `git.exe` in `<root>\cmd\git.exe`, | |
| // with bash at `<root>\bin\bash.exe`. Portable installs sometimes put | |
| // both in `<root>\bin\`. Only infer from those anchored layouts; package | |
| // manager shims live elsewhere and must resolve through `git --exec-path`. | |
| function gitBashCandidatesFromGitExe(gitExe: string): readonly string[] | undefined { | |
| const normalizedGitExe = nodePath.win32.normalize(normalizeWindowsPath(gitExe)); | |
| const gitDir = nodePath.win32.dirname(normalizedGitExe); | |
| const gitDirName = nodePath.win32.basename(gitDir).toLowerCase(); | |
| if (gitDirName !== 'cmd' && gitDirName !== 'bin') { | |
| return undefined; | |
| } | |
| return gitBashCandidatesFromGitRoot(nodePath.win32.dirname(gitDir)); | |
| } | |
| function gitBashCandidatesFromGitExecPath(execPath: string): readonly string[] { | |
| const normalized = nodePath.win32.normalize(normalizeWindowsPath(execPath)); | |
| const parts = normalized.split('\\'); | |
| for (let i = parts.length - 1; i >= 0; i -= 1) { | |
| const segment = parts[i]?.toLowerCase(); | |
| if (segment !== undefined && MINGW_PREFIX_SET.has(segment)) { | |
| const root = parts.slice(0, i).join('\\'); | |
| if (root.length > 0) { | |
| return gitBashCandidatesFromGitRoot(root); | |
| } | |
| } | |
| } | |
| return gitBashCandidatesFromGitRoot(nodePath.win32.join(normalized, '..', '..')); | |
| } | |
| function gitBashCandidatesFromGitRoot(root: string): readonly string[] { | |
| return [ | |
| nodePath.win32.normalize(nodePath.win32.join(root, 'bin', 'bash.exe')), | |
| nodePath.win32.normalize(nodePath.win32.join(root, 'usr', 'bin', 'bash.exe')), | |
| ]; | |
| } | |
| function normalizeWindowsPath(path: string): string { | |
| return path.replaceAll('/', '\\'); | |
| } | |
| function isAbsoluteWindowsPath(path: string): boolean { | |
| return nodePath.win32.isAbsolute(normalizeWindowsPath(path)); | |
| } | |
| function dedupeWindowsPaths(paths: readonly string[]): readonly string[] { | |
| const deduped: string[] = []; | |
| const seen = new Set<string>(); | |
| for (const path of paths) { | |
| const key = normalizeWindowsPath(path).toLowerCase(); | |
| if (seen.has(key)) continue; | |
| seen.add(key); | |
| deduped.push(path); | |
| } | |
| return deduped; | |
| } | |
| /** | |
| * Production convenience — derive the deps bag from Node's ambient surface. | |
| * | |
| * The result is memoised: subsequent calls return the original promise. | |
| * `Environment` is immutable for the lifetime of the process (it derives | |
| * from `process.platform`, `process.arch`, `os.release()`, and one-time | |
| * shell-path discovery), so caching is sound. Tests that need to probe | |
| * with different inputs should call {@link detectEnvironment} directly | |
| * with an injected deps bag. | |
| */ | |
| let detectedEnvironment: Promise<Environment> | undefined; | |
| export function detectEnvironmentFromNode(): Promise<Environment> { | |
| if (detectedEnvironment !== undefined) return detectedEnvironment; | |
| const platform = process.platform; | |
| const env = process.env as Record<string, string | undefined>; | |
| const isFile = async (path: string): Promise<boolean> => { | |
| try { | |
| await access(path, fsConstants.F_OK); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| }; | |
| detectedEnvironment = detectEnvironment({ | |
| platform, | |
| arch: process.arch, | |
| release: nodeOs.release(), | |
| env, | |
| isFile, | |
| execFileText, | |
| }); | |
| return detectedEnvironment; | |
| } | |
| async function findExecutablesOnPath( | |
| name: string, | |
| pathEnv: string | undefined, | |
| platform: string, | |
| isFile: (p: string) => Promise<boolean>, | |
| ): Promise<readonly string[]> { | |
| if (pathEnv === undefined || pathEnv.length === 0) return []; | |
| const listSep = platform === 'win32' ? ';' : ':'; | |
| const dirSep = platform === 'win32' ? '\\' : '/'; | |
| const paths: string[] = []; | |
| for (const rawDir of pathEnv.split(listSep)) { | |
| const dir = rawDir.trim(); | |
| if (dir.length === 0) continue; | |
| if (platform === 'win32' && !isAbsoluteWindowsPath(dir)) continue; | |
| const candidate = dir.endsWith(dirSep) ? `${dir}${name}` : `${dir}${dirSep}${name}`; | |
| if (await isFile(candidate)) { | |
| paths.push(candidate); | |
| } | |
| } | |
| return platform === 'win32' ? dedupeWindowsPaths(paths) : paths; | |
| } | |
| export async function execFileText( | |
| file: string, | |
| args: readonly string[], | |
| timeoutMs: number, | |
| ): Promise<string | undefined> { | |
| return new Promise((resolve) => { | |
| nodeExecFile( | |
| file, | |
| [...args], | |
| { encoding: 'utf8', timeout: timeoutMs, windowsHide: true }, | |
| (error, stdout) => { | |
| if (error !== null) { | |
| resolve(undefined); | |
| return; | |
| } | |
| resolve(stdout); | |
| }, | |
| ); | |
| }); | |
| } | |