Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .claude/skills/archon/guides/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ If Bun was just installed in Prerequisites (macOS/Linux), use `~/.bun/bin/bun` i
3. Verify: `archon version`
4. Check Claude is installed: `which claude`, then `claude /login` if needed

> **Note — Claude Code binary path.** Archon does not bundle Claude Code. In compiled Archon binaries (quick install, Homebrew), the Claude Code SDK needs `CLAUDE_BIN_PATH` set to the absolute path of its `cli.js`. The `archon setup` wizard in Step 4 auto-detects this via `npm root -g` and writes it to `~/.archon/.env` — no manual action needed in the typical case. Source installs (`bun run`) don't need this; the SDK finds `cli.js` via `node_modules` automatically.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update setup note to reflect full resolver/detection behavior.

Line 122 still frames CLAUDE_BIN_PATH as cli.js-only and cites only npm root -g. The current flow supports native claude binary paths too and setup detection is broader.

Suggested wording update
-> **Note — Claude Code binary path.** Archon does not bundle Claude Code. In compiled Archon binaries (quick install, Homebrew), the Claude Code SDK needs `CLAUDE_BIN_PATH` set to the absolute path of its `cli.js`. The `archon setup` wizard in Step 4 auto-detects this via `npm root -g` and writes it to `~/.archon/.env` — no manual action needed in the typical case. Source installs (`bun run`) don't need this; the SDK finds `cli.js` via `node_modules` automatically.
+> **Note — Claude Code binary path.** Archon does not bundle Claude Code. In compiled Archon binaries (quick install, Homebrew), set `CLAUDE_BIN_PATH` to the Claude executable path (native `claude` binary or npm `cli.js`). The `archon setup` wizard in Step 4 auto-detects common locations and writes it to `~/.archon/.env` in typical setups. Source installs (`bun run`) usually don't need this because resolution can use `node_modules`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> **Note — Claude Code binary path.** Archon does not bundle Claude Code. In compiled Archon binaries (quick install, Homebrew), the Claude Code SDK needs `CLAUDE_BIN_PATH` set to the absolute path of its `cli.js`. The `archon setup` wizard in Step 4 auto-detects this via `npm root -g` and writes it to `~/.archon/.env` — no manual action needed in the typical case. Source installs (`bun run`) don't need this; the SDK finds `cli.js` via `node_modules` automatically.
> **Note — Claude Code binary path.** Archon does not bundle Claude Code. In compiled Archon binaries (quick install, Homebrew), set `CLAUDE_BIN_PATH` to the Claude executable path (native `claude` binary or npm `cli.js`). The `archon setup` wizard in Step 4 auto-detects common locations and writes it to `~/.archon/.env` in typical setups. Source installs (`bun run`) usually don't need this because resolution can use `node_modules`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/archon/guides/setup.md at line 122, Update the Note about
CLAUDE_BIN_PATH in the Archon setup guide: clarify that CLAUDE_BIN_PATH can
point to either the Claude Code SDK's cli.js or a native claude binary, and that
the archon setup wizard (Step 4 in `archon setup`) performs broader detection
(not only `npm root -g`) and resolves common global and system paths before
writing the resolved path to `~/.archon/.env`; retain the remark that source
installs (e.g., `bun run`) do not require setting CLAUDE_BIN_PATH because the
SDK finds `cli.js` via `node_modules`.


## Step 4: Configure Credentials

The CLI loads infrastructure config (database, tokens) from `~/.archon/.env` only. This prevents conflicts with project `.env` files that may contain different database URLs.
Expand Down
37 changes: 36 additions & 1 deletion .claude/skills/test-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,23 @@ git commit -q --allow-empty -m init

### Test 3 — SDK path works (assist workflow)

In the same `$TESTREPO`:
**Prerequisite.** Compiled binaries require Claude Code installed on the host and a configured binary path. Before running this test, ensure one of:

```bash
# Option A — env var (easy for ad-hoc testing)
# After the native installer (Anthropic's default):
export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"
# Or after npm global install:
export CLAUDE_BIN_PATH="$(npm root -g)/@anthropic-ai/claude-code/cli.js"

# Option B — config file (persistent)
# Add to ~/.archon/config.yaml:
# assistants:
# claude:
# claudeBinaryPath: /absolute/path/to/claude
```

Then in the same `$TESTREPO`:

```bash
"$BINARY" workflow run assist "say hello and nothing else" 2>&1 | tee /tmp/archon-test-assist.log
Expand All @@ -232,15 +248,34 @@ In the same `$TESTREPO`:

- Exit code 0
- The Claude subprocess spawns successfully (no `spawn EACCES`, `ENOENT`, or `process exited with code 1` in the early output)
- No `Claude Code CLI not found` error (that means the resolver rejected the configured path — verify the cli.js actually exists)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor text inconsistency with actual error message.

The pass criteria references "Claude Code CLI not found" but the actual error message from INSTALL_INSTRUCTIONS in binary-resolver.ts is "Claude Code not found" (without "CLI"). Consider aligning for consistency.

📝 Suggested fix
-- No `Claude Code CLI not found` error (that means the resolver rejected the configured path — verify the cli.js actually exists)
+- No `Claude Code not found` error (that means the resolver rejected the configured path — verify the cli.js actually exists)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- No `Claude Code CLI not found` error (that means the resolver rejected the configured path — verify the cli.js actually exists)
- No `Claude Code not found` error (that means the resolver rejected the configured path — verify the cli.js actually exists)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/test-release/SKILL.md at line 251, Update the pass criteria
text in SKILL.md to match the actual error string emitted by
INSTALL_INSTRUCTIONS in binary-resolver.ts: change "Claude Code CLI not found"
to "Claude Code not found" (or alternatively update INSTALL_INSTRUCTIONS to
include "CLI" if you prefer consistency the other way); locate the
INSTALL_INSTRUCTIONS symbol in binary-resolver.ts and the pass criteria sentence
in SKILL.md and make the text identical across both files.

- A response is produced (any response — even just "hello" — proves the SDK round-trip works)

**Common failures:**

- `Claude Code not found` → `CLAUDE_BIN_PATH` / `claudeBinaryPath` is unset or points at a non-existent file. Fix the path and re-run.
- `Module not found "/Users/runner/..."` → regression of #1210: the resolver was bypassed and the SDK's `import.meta.url` fallback leaked a build-host path. Investigate `packages/providers/src/claude/provider.ts` and the resolver.
- `Credit balance is too low` → auth is pointing at an exhausted API key (check `CLAUDE_USE_GLOBAL_AUTH` and `~/.archon/.env`)
- `unable to determine transport target for "pino-pretty"` → #960 regression, binary crashes on TTY
- `package.json not found (bad installation?)` → #961 regression, `isBinaryBuild` detection broken
- Process exits before producing output → generic spawn failure, capture stderr

### Test 3b — Resolver error path (run without `CLAUDE_BIN_PATH`)

Quickly verify the resolver fails loud when nothing is configured:

```bash
(unset CLAUDE_BIN_PATH; "$BINARY" workflow run assist "hello" 2>&1 | tee /tmp/archon-test-no-path.log)
```

**Pass criteria (when no `~/.archon/config.yaml` configures `claudeBinaryPath`):**

- Error message contains `Claude Code not found`
- Error message mentions both `CLAUDE_BIN_PATH` and `claudeBinaryPath` as remediation options
- No `Module not found` stack traces referencing the CI filesystem

If you *do* have `claudeBinaryPath` set globally, skip this test or temporarily rename `~/.archon/config.yaml`.

### Test 4 — Env-leak gate refuses a leaky .env (optional, for releases including #1036/#1038/#983)

Create a second throwaway repo with a fake sensitive key:
Expand Down
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ CLAUDE_USE_GLOBAL_AUTH=true
# CLAUDE_CODE_OAUTH_TOKEN=...
# CLAUDE_API_KEY=...

# Claude Code executable path (REQUIRED for compiled Archon binaries)
# Archon does not bundle Claude Code — install it separately and point us at it.
# Dev mode (`bun run`) auto-resolves via node_modules.
# Alternatively, set `assistants.claude.claudeBinaryPath` in ~/.archon/config.yaml.
#
# Install (Anthropic's recommended native installer):
# macOS/Linux: curl -fsSL https://claude.ai/install.sh | bash
# Windows: irm https://claude.ai/install.ps1 | iex
#
# Then:
# CLAUDE_BIN_PATH=$HOME/.local/bin/claude (native installer)
# CLAUDE_BIN_PATH=$(npm root -g)/@anthropic-ai/claude-code/cli.js (npm alternative)
# CLAUDE_BIN_PATH=

# Codex Authentication (get from ~/.codex/auth.json after running 'codex login')
# Required if using Codex as AI assistant
# On Linux/Mac: cat ~/.codex/auth.json
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,83 @@ jobs:
exit 1
fi

- name: Smoke-test Claude binary-path resolver (negative case)
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
run: |
# With no CLAUDE_BIN_PATH and no config, running a Claude workflow must
# fail with a clear, user-facing error — NOT with "Module not found
# /Users/runner/..." which would indicate the resolver was bypassed.
BIN="$PWD/dist/${{ matrix.binary }}"
TMP_REPO=$(mktemp -d)
cd "$TMP_REPO"
git init -q
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init

# Run without CLAUDE_BIN_PATH set. Expect a clean resolver error.
# Capture both stdout and stderr; we only care that the resolver message is present.
set +e
OUTPUT=$(env -u CLAUDE_BIN_PATH "$BIN" workflow run archon-assist "hello" 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"

if echo "$OUTPUT" | grep -qE 'Module not found.*Users/runner'; then
echo "::error::Resolver was bypassed — SDK hit the import.meta.url fallback (regression of #1210)"
exit 1
fi
if ! echo "$OUTPUT" | grep -q "Claude Code not found"; then
echo "::error::Expected 'Claude Code not found' error when CLAUDE_BIN_PATH is unset"
exit 1
fi
if ! echo "$OUTPUT" | grep -q "CLAUDE_BIN_PATH"; then
echo "::error::Error message does not reference CLAUDE_BIN_PATH remediation"
exit 1
fi
echo "::notice::Resolver error path works (exit code: $EXIT_CODE)"

- name: Smoke-test Claude subprocess spawn (positive case)
if: matrix.target == 'bun-linux-x64' && runner.os == 'Linux'
run: |
# Install Claude Code via the native installer (Anthropic's recommended
# default) and run a workflow with CLAUDE_BIN_PATH set. The subprocess
# must spawn cleanly. We do NOT require the query to succeed (no auth
# in CI — an auth error is fine and expected); we only fail if the SDK
# can't find the executable, which would indicate a resolver regression.
curl -fsSL https://claude.ai/install.sh | bash
CLI_PATH="$HOME/.local/bin/claude"
if [ ! -x "$CLI_PATH" ]; then
echo "::error::Claude Code binary not found after curl install at $CLI_PATH"
ls -la "$HOME/.local/bin/" || true
exit 1
fi
echo "Using CLAUDE_BIN_PATH=$CLI_PATH"

BIN="$PWD/dist/${{ matrix.binary }}"
TMP_REPO=$(mktemp -d)
cd "$TMP_REPO"
git init -q
git -c user.email=ci@example.com -c user.name=ci commit --allow-empty -q -m init

set +e
OUTPUT=$(CLAUDE_BIN_PATH="$CLI_PATH" "$BIN" workflow run archon-assist "hello" 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"

if echo "$OUTPUT" | grep -qE 'Module not found.*(cli\.js|Users/runner)'; then
echo "::error::Subprocess could not find the executable (resolver regression)"
exit 1
fi
if echo "$OUTPUT" | grep -q "Claude Code not found"; then
echo "::error::Resolver failed even though CLAUDE_BIN_PATH was set to an existing file"
exit 1
fi
# Any of these outcomes are acceptable — they prove the subprocess spawned:
# - auth error ("credit balance", "unauthorized", "authentication")
# - rate-limit / API error
# - successful query (if auth was injected via some other mechanism)
echo "::notice::Claude subprocess spawn path is healthy (exit code: $EXIT_CODE)"

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Claude Code binary resolution** (breaking for compiled binary users): Archon no longer embeds the Claude Code SDK into compiled binaries. In compiled builds, you must install Claude Code separately (`curl -fsSL https://claude.ai/install.sh | bash` on macOS/Linux, `irm https://claude.ai/install.ps1 | iex` on Windows, or `npm install -g @anthropic-ai/claude-code`) and point Archon at the executable via `CLAUDE_BIN_PATH` env var or `assistants.claude.claudeBinaryPath` in `.archon/config.yaml`. The Claude Agent SDK accepts either the native compiled binary (from the curl/PowerShell installer at `~/.local/bin/claude`) or a JS `cli.js` (from the npm install). Dev mode (`bun run`) is unaffected — the SDK resolves via `node_modules` as before. The Docker image ships Claude Code pre-installed with `CLAUDE_BIN_PATH` pre-set, so `docker run` still works out of the box. Resolves silent "Module not found /Users/runner/..." failures on macOS (#1210) and Windows (#1087).

### Added

- **`CLAUDE_BIN_PATH` environment variable** — highest-precedence override for the Claude Code SDK `cli.js` path (#1176)
- **`assistants.claude.claudeBinaryPath` config option** — durable config-file alternative to the env var (#1176)
- **Release-workflow Claude subprocess smoke test** — the release CI now installs Claude Code on the Linux runner and exercises the resolver + subprocess spawn, catching binary-resolution regressions before they ship

### Removed

- **`@anthropic-ai/claude-agent-sdk/embed` import** — the Bun `with { type: 'file' }` asset-embedding path and its `$bunfs` extraction logic. The embed was a bundler-dependent optimization that failed silently when Bun couldn't produce a usable virtual FS path (#1210, #1087); it is replaced by explicit binary-path resolution.

### Fixed

- **Cross-clone worktree isolation**: prevent workflows in one local clone from silently adopting worktrees or DB state owned by another local clone of the same remote. Two clones sharing a remote previously resolved to the same `codebase_id`, causing the isolation resolver's DB-driven paths (`findReusable`, `findLinkedIssueEnv`, `tryBranchAdoption`) to return the other clone's environment. All adoption paths now verify the worktree's `.git` pointer matches the requesting clone and throw a classified error on mismatch. `archon-implement` prompt was also tightened to stop AI agents from adopting unrelated branches they see via `git branch`. Thanks to @halindrome for the three-issue root-cause mapping. (#1193, #1188, #1183, #1198, #1206)
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,11 @@ assistants:
settingSources: # Controls which CLAUDE.md files Claude SDK loads
- project # Default: only project-level CLAUDE.md
- user # Optional: also load ~/.claude/CLAUDE.md
claudeBinaryPath: /absolute/path/to/claude # Optional: Claude Code executable.
# Native binary (curl installer at
# ~/.local/bin/claude) or npm cli.js.
# Required in compiled binaries if
# CLAUDE_BIN_PATH env var is not set.
codex:
model: gpt-5.3-codex
modelReasoningEffort: medium # 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'
Expand Down
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm \
# Point agent-browser to system Chromium (avoids ~400MB Chrome for Testing download)
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium

# Pre-configure the Claude Code SDK cli.js path for any consumer that runs
# a compiled Archon binary inside (or extending) this image. In source mode
# (the default `bun run start` ENTRYPOINT), BUNDLED_IS_BINARY is false and
# this variable is ignored — the SDK resolves cli.js via node_modules. Kept
# here so extenders don't need to rediscover the path.
# Path matches the hoisted layout produced by `bun install --linker=hoisted`.
ENV CLAUDE_BIN_PATH=/app/node_modules/@anthropic-ai/claude-agent-sdk/cli.js
Comment on lines +111 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check which Claude package is actually installed and contains cli.js
echo "=== Checking package.json dependencies ==="
rg -n "claude" package.json packages/providers/package.json

echo ""
echo "=== Looking for cli.js references in codebase ==="
rg -n "claude.*cli\.js" --type ts

Repository: coleam00/Archon

Length of output: 2302


🏁 Script executed:

# Let's read the binary-resolver.ts file to understand the context
head -60 packages/providers/src/claude/binary-resolver.ts

Repository: coleam00/Archon

Length of output: 2643


🏁 Script executed:

# Also check the Dockerfile context around the line in question
head -120 Dockerfile | tail -20

Repository: coleam00/Archon

Length of output: 1048


🏁 Script executed:

# Search for any package.json or documentation that mentions both package names
rg -n "claude-code|claude-agent-sdk" --type json --type md | head -30

Repository: coleam00/Archon

Length of output: 4716


Fix Dockerfile: CLAUDE_BIN_PATH must point to @anthropic-ai/claude-code, not @anthropic-ai/claude-agent-sdk.

The Dockerfile sets ENV CLAUDE_BIN_PATH=/app/node_modules/@anthropic-ai/claude-agent-sdk/cli.js, but this is incorrect. Per the CHANGELOG and source code, @anthropic-ai/claude-agent-sdk is the SDK package that Archon uses; it internally spawns a separate Claude Code executable. The cli.js file belongs to @anthropic-ai/claude-code, not the SDK.

All documentation, install instructions in binary-resolver.ts, and the CHANGELOG consistently reference @anthropic-ai/claude-code/cli.js as the correct path. Update line 117 to:

ENV CLAUDE_BIN_PATH=/app/node_modules/@anthropic-ai/claude-code/cli.js

Note: @anthropic-ai/claude-code is not currently listed in the Dockerfile's or root package.json dependencies, but it will be installed as a peer dependency or must be installed separately in compiled binary deployments (as documented in binary-resolver.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 111 - 117, The Dockerfile sets ENV CLAUDE_BIN_PATH
to the wrong package path; change the environment variable CLAUDE_BIN_PATH so it
points to the Claude Code package's cli.js (i.e.,
/app/node_modules/@anthropic-ai/claude-code/cli.js) instead of
/app/node_modules/@anthropic-ai/claude-agent-sdk/cli.js, ensuring the Docker
image and any compiled Archon binary will resolve the correct executable
referenced by binary-resolver.ts and the CHANGELOG.


# Create non-root user for running Claude Code
# Claude Code refuses to run with --dangerously-skip-permissions as root for security
RUN useradd -m -u 1001 -s /bin/bash appuser \
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,22 @@ irm https://archon.diy/install.ps1 | iex
brew install coleam00/archon/archon
```

> **Compiled binaries need a `CLAUDE_BIN_PATH`.** The quick-install binaries
> don't bundle Claude Code. Install it separately, then point Archon at it:
>
> ```bash
> # macOS / Linux / WSL
> curl -fsSL https://claude.ai/install.sh | bash
> export CLAUDE_BIN_PATH="$HOME/.local/bin/claude"
>
> # Windows (PowerShell)
> irm https://claude.ai/install.ps1 | iex
> $env:CLAUDE_BIN_PATH = "$env:USERPROFILE\.local\bin\claude.exe"
> ```
>
> Or set `assistants.claude.claudeBinaryPath` in `~/.archon/config.yaml`.
> The Docker image ships Claude Code pre-installed. See [AI Assistants → Binary path configuration](https://archon.diy/docs/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only) for details.

### Start Using Archon

Once you've completed either setup path, go to your project and start working:
Expand Down
116 changes: 116 additions & 0 deletions packages/cli/src/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
generateWebhookSecret,
spawnTerminalWithSetup,
copyArchonSkill,
detectClaudeExecutablePath,
} from './setup';
import * as setupModule from './setup';

// Test directory for file operations
const TEST_DIR = join(tmpdir(), 'archon-setup-test-' + Date.now());
Expand Down Expand Up @@ -176,6 +178,41 @@ CODEX_ACCOUNT_ID=account1
expect(content).toContain('CLAUDE_API_KEY=sk-test-key');
});

it('emits CLAUDE_BIN_PATH when claudeBinaryPath is configured', () => {
const content = generateEnvContent({
database: { type: 'sqlite' },
ai: {
claude: true,
claudeAuthType: 'global',
claudeBinaryPath: '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
codex: false,
defaultAssistant: 'claude',
},
platforms: { github: false, telegram: false, slack: false, discord: false },
botDisplayName: 'Archon',
});

expect(content).toContain(
'CLAUDE_BIN_PATH=/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js'
);
});

it('omits CLAUDE_BIN_PATH when not configured', () => {
const content = generateEnvContent({
database: { type: 'sqlite' },
ai: {
claude: true,
claudeAuthType: 'global',
codex: false,
defaultAssistant: 'claude',
},
platforms: { github: false, telegram: false, slack: false, discord: false },
botDisplayName: 'Archon',
});

expect(content).not.toContain('CLAUDE_BIN_PATH=');
});

it('should include platform configurations', () => {
const content = generateEnvContent({
database: { type: 'sqlite' },
Expand Down Expand Up @@ -418,3 +455,82 @@ CODEX_ACCOUNT_ID=account1
});
});
});

describe('detectClaudeExecutablePath probe order', () => {
// Use spies on the exported probe wrappers so each tier can be controlled
// independently without touching the real filesystem or shell.
let fileExistsSpy: ReturnType<typeof spyOn>;
let npmRootSpy: ReturnType<typeof spyOn>;
let whichSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
fileExistsSpy = spyOn(setupModule, 'probeFileExists').mockReturnValue(false);
npmRootSpy = spyOn(setupModule, 'probeNpmRoot').mockReturnValue(null);
whichSpy = spyOn(setupModule, 'probeWhichClaude').mockReturnValue(null);
});

afterEach(() => {
fileExistsSpy.mockRestore();
npmRootSpy.mockRestore();
whichSpy.mockRestore();
});

it('returns the native installer path when present (tier 1 wins)', () => {
// Native path exists; subsequent probes must not be called.
fileExistsSpy.mockImplementation(
(p: string) => p.includes('.local/bin/claude') || p.includes('.local\\bin\\claude')
);
const result = detectClaudeExecutablePath();
expect(result).toBeTruthy();
expect(result).toMatch(/\.local[\\/]bin[\\/]claude/);
// Tier 2 / 3 must not have been consulted.
expect(npmRootSpy).not.toHaveBeenCalled();
expect(whichSpy).not.toHaveBeenCalled();
});

it('falls through to npm cli.js when native is missing (tier 2 wins)', () => {
// Use path.join so the expected result matches whatever separator the
// production code produces on the current platform (backslash on Windows,
// forward slash elsewhere).
const npmRoot = join('fake', 'npm', 'root');
const expectedCliJs = join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js');
npmRootSpy.mockReturnValue(npmRoot);
fileExistsSpy.mockImplementation((p: string) => p === expectedCliJs);
const result = detectClaudeExecutablePath();
expect(result).toBe(expectedCliJs);
// Tier 3 must not have been consulted.
expect(whichSpy).not.toHaveBeenCalled();
});

it('falls through to which/where when native and npm probes both miss (tier 3 wins)', () => {
npmRootSpy.mockReturnValue('/fake/npm/root');
// Native miss, npm cli.js miss, but `which claude` returns a path that exists.
whichSpy.mockReturnValue('/opt/homebrew/bin/claude');
fileExistsSpy.mockImplementation((p: string) => p === '/opt/homebrew/bin/claude');
const result = detectClaudeExecutablePath();
expect(result).toBe('/opt/homebrew/bin/claude');
});

it('returns null when every probe misses', () => {
// All defaults already return false/null; nothing to override.
expect(detectClaudeExecutablePath()).toBeNull();
});

it('does not return a which-resolved path that fails the existsSync check', () => {
// `which` returns a path string but the file is not actually present
// (stale PATH entry, dangling symlink, etc.) — must not be returned.
npmRootSpy.mockReturnValue('/fake/npm/root');
whichSpy.mockReturnValue('/stale/path/claude');
fileExistsSpy.mockReturnValue(false);
expect(detectClaudeExecutablePath()).toBeNull();
});

it('skips npm tier when probeNpmRoot returns null (e.g. npm not installed)', () => {
// npm probe fails; tier 3 must still run.
whichSpy.mockReturnValue('/usr/local/bin/claude');
fileExistsSpy.mockImplementation((p: string) => p === '/usr/local/bin/claude');
const result = detectClaudeExecutablePath();
expect(result).toBe('/usr/local/bin/claude');
expect(npmRootSpy).toHaveBeenCalled();
});
});
Loading
Loading