Skip to content

Commit 082c021

Browse files
Claude: resolve CLI executable across npm and native installers
Probe PATH for claude.cmd / claude.exe / claude rather than hardcoding claude.cmd, so the native installer's claude.exe also works. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 14a27c1 commit 082c021

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

src/PostSharp.Engineering.BuildTools/Resources/RunClaude.ps1

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,20 @@ if ($Prompt)
210210
# Stream JSON output for human-readable real-time monitoring
211211
$processArgs = "-p --output-format stream-json --verbose --model $Model --dangerously-skip-permissions $mcpConfigArg"
212212

213+
# Resolve the Claude CLI launcher: npm ships claude.cmd on Windows, the native installer ships
214+
# claude.exe, and on Linux/macOS the binary is plain "claude". Get-Command honors PATHEXT so
215+
# asking for "claude" without an extension finds whichever variant is on PATH.
216+
$claudeCommand = Get-Command claude.cmd -ErrorAction SilentlyContinue
217+
if (-not $claudeCommand) { $claudeCommand = Get-Command claude.exe -ErrorAction SilentlyContinue }
218+
if (-not $claudeCommand) { $claudeCommand = Get-Command claude -ErrorAction SilentlyContinue }
219+
if (-not $claudeCommand) {
220+
Write-Error "Claude CLI not found on PATH. Install it via 'npm i -g @anthropic-ai/claude-code' or the native installer."
221+
exit 1
222+
}
223+
Write-Host "Using Claude executable: $($claudeCommand.Source)" -ForegroundColor Cyan
224+
213225
$psi = [System.Diagnostics.ProcessStartInfo]::new()
214-
$psi.FileName = "claude.cmd"
226+
$psi.FileName = $claudeCommand.Source
215227
$psi.Arguments = $processArgs
216228
$psi.RedirectStandardInput = $true
217229
$psi.RedirectStandardOutput = $true

src/PostSharp.Engineering.BuildTools/Utilities/ClaudeCodeHelper.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Collections.Immutable;
6+
using System.IO;
67
using System.Linq;
78
using System.Reflection;
9+
using System.Runtime.InteropServices;
810
using System.Text;
911
using System.Text.Json;
1012
using System.Text.RegularExpressions;
@@ -139,10 +141,23 @@ void HandleOutput( string line )
139141
}
140142
}
141143

144+
// Resolve the Claude CLI executable on PATH. The npm install ships claude.cmd on Windows;
145+
// the native installer ships claude.exe; on Unix it is plain "claude".
146+
if ( !TryResolveClaudeExecutable( out var claudeExecutable ) )
147+
{
148+
console.WriteError(
149+
"Claude CLI not found on PATH. Install it via 'npm i -g @anthropic-ai/claude-code' or the native installer from https://claude.com/claude-code." );
150+
prBodyText = "";
151+
152+
return false;
153+
}
154+
155+
console.WriteMessage( $"Using Claude executable: {claudeExecutable}" );
156+
142157
// Invoke claude with custom output handler
143158
var success = ToolInvocationHelper.InvokeTool(
144159
console,
145-
"claude.cmd",
160+
claudeExecutable,
146161
claudeArgs,
147162
workingDirectory,
148163
out var exitCode,
@@ -511,4 +526,48 @@ This conclusion will be extracted and placed at the TOP of the PR description.
511526
Begin now.
512527
""";
513528
}
529+
530+
/// <summary>
531+
/// Probes PATH for a Claude CLI launcher. The npm package installs <c>claude.cmd</c> on Windows,
532+
/// the native installer ships <c>claude.exe</c>, and on Unix the binary is named <c>claude</c>.
533+
/// Returns the resolved absolute path so <see cref="System.Diagnostics.Process"/> doesn't have to
534+
/// rely on PATHEXT (which is not consulted when <c>UseShellExecute</c> is false).
535+
/// </summary>
536+
private static bool TryResolveClaudeExecutable( out string path )
537+
{
538+
var candidates = RuntimeInformation.IsOSPlatform( OSPlatform.Windows )
539+
? new[] { "claude.cmd", "claude.exe", "claude.bat", "claude" }
540+
: new[] { "claude" };
541+
542+
var pathEnv = Environment.GetEnvironmentVariable( "PATH" ) ?? "";
543+
544+
foreach ( var dir in pathEnv.Split( Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries ) )
545+
{
546+
foreach ( var candidate in candidates )
547+
{
548+
string fullPath;
549+
550+
try
551+
{
552+
fullPath = Path.Combine( dir.Trim().Trim( '"' ), candidate );
553+
}
554+
catch ( ArgumentException )
555+
{
556+
// Skip malformed PATH entries.
557+
continue;
558+
}
559+
560+
if ( File.Exists( fullPath ) )
561+
{
562+
path = fullPath;
563+
564+
return true;
565+
}
566+
}
567+
}
568+
569+
path = "";
570+
571+
return false;
572+
}
514573
}

0 commit comments

Comments
 (0)