ctx/internal/plugins/csharp/helper/locate.go
Ricardo Carneiro 59cb2b5ddb feat(csharp): implement 'project' command via Roslyn helper
Replaces placeholder with full csharp@0.1.0 plugin. Adds helper/
package (locate, process, client, protocol) for JSON-RPC over stdio
to ctx-roslyn-helper. project.go finds .sln (fallback: single .csproj),
loads it, retrieves projectSummary, formats dense markdown with project
details, reference graph, and multi-targeting section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 19:31:00 -03:00

87 lines
2.4 KiB
Go

package helper
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
)
const helperBinary = "ctx-roslyn-helper"
// ErrHelperNotFound is returned when the Roslyn helper binary cannot be located.
var ErrHelperNotFound = errors.New("roslyn helper not found")
// LocateHelper searches for the ctx-roslyn-helper binary using four strategies:
// 1. CTX_ROSLYN_HELPER environment variable
// 2. Same directory as the running ctx binary
// 3. PATH
// 4. tools/roslyn-helper/publish/ relative to working directory
//
// Returns the absolute path or ErrHelperNotFound with a diagnostic message.
func LocateHelper() (string, error) {
name := helperBinary
if runtime.GOOS == "windows" {
name += ".exe"
}
tried := []string{}
// 1. Environment variable
if env := os.Getenv("CTX_ROSLYN_HELPER"); env != "" {
if fileExists(env) {
return env, nil
}
tried = append(tried, fmt.Sprintf("$CTX_ROSLYN_HELPER = %s (not found)", env))
} else {
tried = append(tried, "$CTX_ROSLYN_HELPER (not set)")
}
// 2. Same directory as ctx binary
if exePath, err := os.Executable(); err == nil {
exeDir := filepath.Dir(exePath)
candidate := filepath.Join(exeDir, name)
if fileExists(candidate) {
return candidate, nil
}
tried = append(tried, fmt.Sprintf("%s (not found)", exeDir))
}
// 3. PATH
if found, err := exec.LookPath(name); err == nil {
return found, nil
}
tried = append(tried, "PATH (not found)")
// 4. Dev fallback: tools/roslyn-helper/publish/ relative to working dir
if cwd, err := os.Getwd(); err == nil {
candidate := filepath.Join(cwd, "tools", "roslyn-helper", "publish", name)
if fileExists(candidate) {
return candidate, nil
}
tried = append(tried, fmt.Sprintf("tools/roslyn-helper/publish/ (not found)"))
}
return "", fmt.Errorf("%w\n\nLooked in:\n - %s\n - %s\n - %s\n - %s\n\nTo build the helper, run:\n cd tools\\roslyn-helper\n dotnet publish src/RoslynHelper -c Release -r win-x64 --self-contained false -o publish/",
ErrHelperNotFound,
safeIdx(tried, 0, "$CTX_ROSLYN_HELPER (not set)"),
safeIdx(tried, 1, "ctx.exe directory"),
safeIdx(tried, 2, "PATH"),
safeIdx(tried, 3, "tools/roslyn-helper/publish/"),
)
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func safeIdx(s []string, i int, fallback string) string {
if i < len(s) {
return s[i]
}
return fallback
}