From e22ef9420989dca5b9e34c78494418e0e9c8221a Mon Sep 17 00:00:00 2001 From: aljjang95 Date: Fri, 31 Jul 2026 04:57:28 +0900 Subject: [PATCH] fix(kiro): resolve Windows kiro-cli executable without PATH After the Windows SQLite store path fix, forced/add-account login still spawned bare `kiro-cli` and failed when the installer binary was not on PATH. Resolve PATH first, then fall back to the LocalAppData and Program Files Kiro-Cli install layouts, with pure unit coverage. --- .../src/content/docs/guides/providers.md | 4 + src/oauth/kiro-credentials.ts | 59 +++++++++++++++ src/oauth/kiro.ts | 15 +++- .../kiro-windows-cli-executable-path.test.ts | 75 +++++++++++++++++++ 4 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests/kiro-windows-cli-executable-path.test.ts diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index e14f0d6e0..773a92447 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -165,6 +165,10 @@ read-only. Two environment variables make the source and token row selection exp - `KIROCLI_TOKEN_KEY` selects the exact `auth_kv` token key when a database contains multiple otherwise ambiguous token rows. A missing selection fails login instead of guessing. +On Windows, import looks for `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`. Forced/add-account login +also needs the local CLI binary: opencodex first uses `PATH`, then falls back to +`%LOCALAPPDATA%\Kiro-Cli\kiro-cli.exe` and `C:\Program Files\Kiro-Cli\kiro-cli.exe`. + After a successful import, opencodex persists the imported credential to `~/.opencodex/auth.json`. diff --git a/src/oauth/kiro-credentials.ts b/src/oauth/kiro-credentials.ts index 3ce3dc7cf..d572ab771 100644 --- a/src/oauth/kiro-credentials.ts +++ b/src/oauth/kiro-credentials.ts @@ -169,6 +169,65 @@ export function resolveKiroCliNativeSessionEntries( return [{ location: "kiro-cli-linux-data", path: posix.join(home, ".local", "share", "kiro-cli", "data.sqlite3") }]; } +/** + * Resolve the absolute kiro-cli executable for spawn/login helpers. + * + * Pure + parameterized like `resolveKiroCliNativeSessionEntries` so Windows install layouts can be + * covered from any host. PATH remains the first choice; only when bare `kiro-cli` is missing do we + * fall back to the platform-native install directories next to the session database. + * + * Windows: official MSI installs to `C:\Program Files\Kiro-Cli\kiro-cli.exe`, while some local + * installs keep the binary next to `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`. + * macOS/Linux: prefer PATH, then the usual user-local bin directories. + */ +export function resolveKiroCliExecutable( + inputs: KiroCliNativeInputs & { + pathEntries?: string[]; + exists?: (path: string) => boolean; + }, +): string { + const exists = inputs.exists ?? existsSync; + const pathEntries = inputs.pathEntries + ?? (inputs.env.PATH ?? inputs.env.Path ?? "").split(inputs.platform === "win32" ? ";" : ":") + .map(entry => entry.trim()) + .filter(Boolean); + + const pathCandidates = inputs.platform === "win32" + ? pathEntries.flatMap(entry => [ + win32.join(entry, "kiro-cli.exe"), + win32.join(entry, "kiro-cli"), + ]) + : pathEntries.map(entry => posix.join(entry, "kiro-cli")); + + const installCandidates: string[] = []; + if (inputs.platform === "win32") { + const localBase = inputs.env.LOCALAPPDATA?.trim() + || (inputs.env.USERPROFILE?.trim() ? win32.join(inputs.env.USERPROFILE.trim(), "AppData", "Local") : "") + || win32.join(inputs.home, "AppData", "Local"); + const programFiles = inputs.env["ProgramFiles"]?.trim() || "C:\\Program Files"; + installCandidates.push( + win32.join(localBase, "Kiro-Cli", "kiro-cli.exe"), + win32.join(programFiles, "Kiro-Cli", "kiro-cli.exe"), + ); + } else if (inputs.platform === "darwin") { + installCandidates.push( + posix.join(inputs.home, ".local", "bin", "kiro-cli"), + "/usr/local/bin/kiro-cli", + "/opt/homebrew/bin/kiro-cli", + ); + } else { + installCandidates.push( + posix.join(inputs.home, ".local", "bin", "kiro-cli"), + "/usr/local/bin/kiro-cli", + ); + } + + for (const candidate of [...pathCandidates, ...installCandidates]) { + if (exists(candidate)) return candidate; + } + return inputs.platform === "win32" ? "kiro-cli.exe" : "kiro-cli"; +} + function nativeKiroCliSessionEntries(): Array<{ location: KiroCliNativeLocation; path: string }> { // Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks // (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback. diff --git a/src/oauth/kiro.ts b/src/oauth/kiro.ts index d9a978913..f4df35282 100644 --- a/src/oauth/kiro.ts +++ b/src/oauth/kiro.ts @@ -18,6 +18,7 @@ import { persistKiroCliSessionRecovery, readImportedKiroCredential, readKiroCliSqliteCredential, + resolveKiroCliExecutable, restoreKiroCliSession, restoreStaleKiroCliSessionRecovery, requireKiroRegion, @@ -25,6 +26,7 @@ import { type KiroCliSessionSnapshot, type KiroImportDiagnostic, } from "./kiro-credentials"; +import { homedir } from "node:os"; import { getAccountSet, saveAccountCredential } from "./store"; const DEFAULT_REGION = "us-east-1"; @@ -72,9 +74,18 @@ const pendingKiroLoginTransactions = new WeakMap(); + +function resolveRuntimeKiroCliExecutable(): string { + return resolveKiroCliExecutable({ + env: process.env, + platform: process.platform, + home: process.platform === "win32" ? homedir() : (process.env.HOME || homedir()), + }); +} + function logoutKiroCliBestEffort(): void { try { - Bun.spawnSync(["kiro-cli", "logout"], { + Bun.spawnSync([resolveRuntimeKiroCliExecutable(), "logout"], { stdin: "ignore", stdout: "ignore", stderr: "ignore", @@ -128,7 +139,7 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi throwIfKiroLoginCancelled(signal); let child: ReturnType; try { - child = Bun.spawn(["kiro-cli", ...args], { + child = Bun.spawn([resolveRuntimeKiroCliExecutable(), ...args], { stdin: "ignore", stdout: "pipe", stderr: "ignore", diff --git a/tests/kiro-windows-cli-executable-path.test.ts b/tests/kiro-windows-cli-executable-path.test.ts new file mode 100644 index 000000000..505f51ecf --- /dev/null +++ b/tests/kiro-windows-cli-executable-path.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from "bun:test"; +import { resolveKiroCliExecutable } from "../src/oauth/kiro-credentials"; + +/** + * Forced/add-account Kiro login still shells out to the local CLI. After #710 fixed Windows + * SQLite discovery, Windows installs can import tokens while PATH still lacks `kiro-cli`. + * The pure executable resolver covers that layout without launching the real binary. + */ +describe("kiro-cli executable resolution", () => { + const WIN_HOME = "C:\\Users\\u"; + + test("prefers the first PATH hit before install-directory fallbacks", () => { + const exists = (path: string) => path === "C:\\Tools\\kiro-cli.exe"; + expect(resolveKiroCliExecutable({ + env: { + PATH: "C:\\Tools;C:\\Windows\\System32", + LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local", + }, + platform: "win32", + home: WIN_HOME, + pathEntries: ["C:\\Tools", "C:\\Windows\\System32"], + exists, + })).toBe("C:\\Tools\\kiro-cli.exe"); + }); + + test("win32 falls back to %LOCALAPPDATA%\\Kiro-Cli\\kiro-cli.exe", () => { + const exists = (path: string) => path === "C:\\Users\\u\\AppData\\Local\\Kiro-Cli\\kiro-cli.exe"; + expect(resolveKiroCliExecutable({ + env: { + PATH: "C:\\Windows\\System32", + LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local", + }, + platform: "win32", + home: WIN_HOME, + pathEntries: ["C:\\Windows\\System32"], + exists, + })).toBe("C:\\Users\\u\\AppData\\Local\\Kiro-Cli\\kiro-cli.exe"); + }); + + test("win32 falls back to Program Files\\Kiro-Cli when LOCALAPPDATA binary is absent", () => { + const exists = (path: string) => path === "C:\\Program Files\\Kiro-Cli\\kiro-cli.exe"; + expect(resolveKiroCliExecutable({ + env: { + PATH: "C:\\Windows\\System32", + LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local", + ProgramFiles: "C:\\Program Files", + }, + platform: "win32", + home: WIN_HOME, + pathEntries: ["C:\\Windows\\System32"], + exists, + })).toBe("C:\\Program Files\\Kiro-Cli\\kiro-cli.exe"); + }); + + test("linux keeps PATH-first resolution and falls back to ~/.local/bin", () => { + const exists = (path: string) => path === "/home/u/.local/bin/kiro-cli"; + expect(resolveKiroCliExecutable({ + env: { PATH: "/usr/bin" }, + platform: "linux", + home: "/home/u", + pathEntries: ["/usr/bin"], + exists, + })).toBe("/home/u/.local/bin/kiro-cli"); + }); + + test("returns the bare command when no candidate exists so spawn can report the original error", () => { + expect(resolveKiroCliExecutable({ + env: { PATH: "C:\\Windows\\System32" }, + platform: "win32", + home: WIN_HOME, + pathEntries: ["C:\\Windows\\System32"], + exists: () => false, + })).toBe("kiro-cli.exe"); + }); +});