|
1 | 1 | import { randomUUID } from "node:crypto"; |
2 | | -import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; |
| 2 | +import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"; |
3 | 3 | import { homedir } from "node:os"; |
4 | 4 | import { isAbsolute, join, posix, win32 } from "node:path"; |
5 | 5 | import { Database } from "bun:sqlite"; |
@@ -169,6 +169,77 @@ export function resolveKiroCliNativeSessionEntries( |
169 | 169 | return [{ location: "kiro-cli-linux-data", path: posix.join(home, ".local", "share", "kiro-cli", "data.sqlite3") }]; |
170 | 170 | } |
171 | 171 |
|
| 172 | +/** |
| 173 | + * Resolve the absolute kiro-cli executable for spawn/login helpers. |
| 174 | + * |
| 175 | + * Pure + parameterized like `resolveKiroCliNativeSessionEntries` so Windows install layouts can be |
| 176 | + * covered from any host. PATH remains the first choice; only when bare `kiro-cli` is missing do we |
| 177 | + * fall back to the platform-native install directories next to the session database. |
| 178 | + * |
| 179 | + * Windows: official MSI installs to `C:\Program Files\Kiro-Cli\kiro-cli.exe`, while some local |
| 180 | + * installs keep the binary next to `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`. |
| 181 | + * macOS/Linux: prefer PATH, then the usual user-local bin directories. |
| 182 | + */ |
| 183 | +export function resolveKiroCliExecutable( |
| 184 | + inputs: KiroCliNativeInputs & { |
| 185 | + pathEntries?: string[]; |
| 186 | + exists?: (path: string) => boolean; |
| 187 | + isFile?: (path: string) => boolean; |
| 188 | + }, |
| 189 | +): string { |
| 190 | + const exists = inputs.exists ?? existsSync; |
| 191 | + // A directory named `kiro-cli` on PATH satisfies existsSync and would then be handed to |
| 192 | + // spawn(), which fails with EACCES at login instead of falling through to the next candidate. |
| 193 | + // When a caller injects `exists` it owns the whole filesystem view, so the real stat would |
| 194 | + // reject every synthetic path; such callers inject `isFile` too when they care about it. |
| 195 | + const isFile = inputs.isFile ?? (inputs.exists ? () => true : ((path: string) => { |
| 196 | + try { |
| 197 | + return statSync(path).isFile(); |
| 198 | + } catch { |
| 199 | + return false; |
| 200 | + } |
| 201 | + })); |
| 202 | + const pathEntries = inputs.pathEntries |
| 203 | + ?? (inputs.env.PATH ?? inputs.env.Path ?? "").split(inputs.platform === "win32" ? ";" : ":") |
| 204 | + .map(entry => entry.trim()) |
| 205 | + .filter(Boolean); |
| 206 | + |
| 207 | + const pathCandidates = inputs.platform === "win32" |
| 208 | + ? pathEntries.flatMap(entry => [ |
| 209 | + win32.join(entry, "kiro-cli.exe"), |
| 210 | + win32.join(entry, "kiro-cli"), |
| 211 | + ]) |
| 212 | + : pathEntries.map(entry => posix.join(entry, "kiro-cli")); |
| 213 | + |
| 214 | + const installCandidates: string[] = []; |
| 215 | + if (inputs.platform === "win32") { |
| 216 | + const localBase = inputs.env.LOCALAPPDATA?.trim() |
| 217 | + || (inputs.env.USERPROFILE?.trim() ? win32.join(inputs.env.USERPROFILE.trim(), "AppData", "Local") : "") |
| 218 | + || win32.join(inputs.home, "AppData", "Local"); |
| 219 | + const programFiles = inputs.env["ProgramFiles"]?.trim() || "C:\\Program Files"; |
| 220 | + installCandidates.push( |
| 221 | + win32.join(localBase, "Kiro-Cli", "kiro-cli.exe"), |
| 222 | + win32.join(programFiles, "Kiro-Cli", "kiro-cli.exe"), |
| 223 | + ); |
| 224 | + } else if (inputs.platform === "darwin") { |
| 225 | + installCandidates.push( |
| 226 | + posix.join(inputs.home, ".local", "bin", "kiro-cli"), |
| 227 | + "/usr/local/bin/kiro-cli", |
| 228 | + "/opt/homebrew/bin/kiro-cli", |
| 229 | + ); |
| 230 | + } else { |
| 231 | + installCandidates.push( |
| 232 | + posix.join(inputs.home, ".local", "bin", "kiro-cli"), |
| 233 | + "/usr/local/bin/kiro-cli", |
| 234 | + ); |
| 235 | + } |
| 236 | + |
| 237 | + for (const candidate of [...pathCandidates, ...installCandidates]) { |
| 238 | + if (exists(candidate) && isFile(candidate)) return candidate; |
| 239 | + } |
| 240 | + return inputs.platform === "win32" ? "kiro-cli.exe" : "kiro-cli"; |
| 241 | +} |
| 242 | + |
172 | 243 | function nativeKiroCliSessionEntries(): Array<{ location: KiroCliNativeLocation; path: string }> { |
173 | 244 | // Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks |
174 | 245 | // (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback. |
|
0 commit comments