Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
59 changes: 59 additions & 0 deletions src/oauth/kiro-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +172 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the exported resolver’s documented contract.

The JSDoc describes the resolver as “absolute” and “pure,” but the fallback intentionally returns bare kiro-cli/kiro-cli.exe, relative PATH entries can produce relative results, and the default existsSync performs filesystem I/O. Document the actual behavior or enforce the stronger contract.

Also applies to: 228-229

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/kiro-credentials.ts` around lines 172 - 181, The exported
resolver’s JSDoc overstates its guarantees. Update the documentation for the
resolver near resolveKiroCliExecutable to describe that it may return a bare or
relative executable name and performs filesystem checks via existsSync, or
change the implementation to enforce absolute, pure behavior; align the contract
with the existing PATH and platform-directory fallback behavior.

*/
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"));
Comment on lines +189 to +200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
printf '\n== file outline ==\n'
ast-grep outline src/oauth/kiro-credentials.ts --view expanded || true
printf '\n== relevant slice ==\n'
sed -n '160,250p' src/oauth/kiro-credentials.ts
printf '\n== search for spawn/exec/path checks ==\n'
rg -n "Bun\.spawn|spawn\(|existsSync|accessSync|statSync|isFile|mode &|chmod|executable" src/oauth -S

Repository: lidge-jun/opencodex

Length of output: 13864


🏁 Script executed:

set -e
sed -n '1,260p' src/oauth/kiro-credentials.ts

Repository: lidge-jun/opencodex

Length of output: 10569


🏁 Script executed:

set -e
printf '\n== references ==\n'
rg -n "resolveKiroCliExecutable|resolveKiroCliNativeSessionEntries|kiro-cli.exe|kiro-cli" src test tests . -g '!**/node_modules/**'

Repository: lidge-jun/opencodex

Length of output: 32085


🏁 Script executed:

set -e
sed -n '1,200p' tests/kiro-windows-cli-executable-path.test.ts

Repository: lidge-jun/opencodex

Length of output: 2934


Reject non-runnable PATH hits src/oauth/kiro-credentials.ts:189-200, 225-228existsSync() only checks presence, so a directory or non-executable file can win here and short-circuit the later install candidates and the bare-command fallback. That leaves Bun.spawn to fail on a path the resolver treated as valid. Use a regular-file + execute check, or inject a richer isRunnable predicate here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth/kiro-credentials.ts` around lines 189 - 200, Update the PATH
candidate validation in the Kiro credential resolver to use an injectable
isRunnable predicate, or equivalent regular-file-and-executable check, instead
of existsSync alone. Ensure directories and non-executable files are rejected
before selecting a candidate, preserving fallback to later install candidates
and the bare command; update the related validation around the resolver’s
candidate selection flow.


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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip unusable PATH entries before selecting the CLI

On macOS/Linux, if an earlier PATH directory contains a non-executable file or directory named kiro-cli while a later directory contains the real executable, this existsSync check selects the unusable entry and Bun.spawn fails with EACCES. The previous bare-command spawn allowed Bun's PATH search to skip that entry and launch the later executable. Validate that candidates are executable regular files (for example with stat and X_OK) or leave PATH resolution to Bun.spawn before trying the fixed install-directory fallbacks.

Useful? React with 👍 / 👎.

}
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.
Expand Down
15 changes: 13 additions & 2 deletions src/oauth/kiro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import {
persistKiroCliSessionRecovery,
readImportedKiroCredential,
readKiroCliSqliteCredential,
resolveKiroCliExecutable,
restoreKiroCliSession,
restoreStaleKiroCliSessionRecovery,
requireKiroRegion,
type ImportedKiroCredential,
type KiroCliSessionSnapshot,
type KiroImportDiagnostic,
} from "./kiro-credentials";
import { homedir } from "node:os";
import { getAccountSet, saveAccountCredential } from "./store";

const DEFAULT_REGION = "us-east-1";
Expand Down Expand Up @@ -72,9 +74,18 @@ const pendingKiroLoginTransactions = new WeakMap<OAuthCredentials, KiroCliSessio
/** Forced logins that started with no native CLI DB must logout on persistence failure. */
const pendingKiroEmptyPriorSessions = new WeakSet<OAuthCredentials>();


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",
Expand Down Expand Up @@ -128,7 +139,7 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
throwIfKiroLoginCancelled(signal);
let child: ReturnType<typeof Bun.spawn>;
try {
child = Bun.spawn(["kiro-cli", ...args], {
child = Bun.spawn([resolveRuntimeKiroCliExecutable(), ...args], {
stdin: "ignore",
stdout: "pipe",
stderr: "ignore",
Expand Down
75 changes: 75 additions & 0 deletions tests/kiro-windows-cli-executable-path.test.ts
Original file line number Diff line number Diff line change
@@ -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");
Comment on lines +12 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise production PATH parsing in at least one test.

All tests inject pathEntries, so they do not cover env.PATH/env.Path splitting, trimming, or platform-specific separators—the path used by resolveRuntimeKiroCliExecutable().

Suggested adjustment
       platform: "win32",
       home: WIN_HOME,
-      pathEntries: ["C:\\Tools", "C:\\Windows\\System32"],
       exists,

Add a separate case for the Windows Path casing if that compatibility path is intentional.

As per path instructions, source behavior changes should have focused regression coverage near the existing subsystem tests; the current suite bypasses the runtime PATH-parsing branch.

Also applies to: 26-37, 40-52, 55-63, 66-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/kiro-windows-cli-executable-path.test.ts` around lines 12 - 23, Add
regression coverage in the Windows executable-resolution tests that omits the
injected pathEntries and exercises env.PATH parsing, including Windows separator
handling and trimming. Add a separate case for the env.Path casing if
resolveRuntimeKiroCliExecutable intentionally supports that compatibility path,
while preserving the existing injected-entry tests.

Source: Path instructions

});

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");
});
});
Loading