feat(env): supporter-binary resolver — explicit override → PATH → well-known (T-495)
SupporterBinaries resolves claude/d2/etc. to absolute paths (D-104): an explicit override map first, then the login-shell/process PATH, then the well-known bin dirs incl. Homebrew-on-Linux — the dir the standard PATH expansion omits and a .bashrc-only brew shellenv misses (the D-104 gap). detect() probes those to seed the override map (pinned, not re-probed each launch); isStalePin flags a moved tool. Pure Dart, dart test (11 cases). Settings storage + first-run detect wire on next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
/// Resolves external supporter binaries (claude, d2, …) to absolute paths
|
||||
/// (T-495, D-104).
|
||||
///
|
||||
/// Order: an explicit user-scope **override** map is consulted FIRST; then the
|
||||
/// login-shell / process search PATH ([resolvedToolPath]); then the well-known
|
||||
/// user/local bin dirs — including **Homebrew-on-Linux** (`/home/linuxbrew/...`),
|
||||
/// which the standard PATH expansion omits and a login-shell probe misses when
|
||||
/// `brew shellenv` lives only in `~/.bashrc`. That omission is the gap D-104
|
||||
/// fixes.
|
||||
///
|
||||
/// [detect] probes those locations to seed the override map on first run (or a
|
||||
/// re-detect), so resolution is **pinned** thereafter — deterministic, not a
|
||||
/// per-launch heuristic. A pin that no longer points at a file is reported by
|
||||
/// [isStalePin] so the caller can warn + re-detect rather than silently fail.
|
||||
///
|
||||
/// Pure Dart (injected `exists` / `searchPath`), Flutter-free; runs under
|
||||
/// `dart test`.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'shell_env.dart' show resolvedToolPath;
|
||||
|
||||
/// Whether [absolutePath] points at an existing file (symlinks followed).
|
||||
typedef PathExists = bool Function(String absolutePath);
|
||||
|
||||
class SupporterBinaries {
|
||||
SupporterBinaries({Map<String, String> overrides = const {}, PathExists? exists, String Function()? searchPath, String? home, bool isWindows = false})
|
||||
: _overrides = overrides,
|
||||
_exists = exists ?? _fileExists,
|
||||
_searchPath = searchPath ?? resolvedToolPath,
|
||||
_home = home ?? Platform.environment['HOME'],
|
||||
_sep = isWindows ? ';' : ':';
|
||||
|
||||
final Map<String, String> _overrides;
|
||||
final PathExists _exists;
|
||||
final String Function() _searchPath;
|
||||
final String? _home;
|
||||
final String _sep;
|
||||
|
||||
/// Resolve [name] to an absolute path, or `null`. Order: explicit override (if
|
||||
/// it still exists) → search PATH → well-known dirs.
|
||||
String? resolve(String name) {
|
||||
final pinned = _overrides[name];
|
||||
if (pinned != null && pinned.isNotEmpty && _exists(pinned)) return pinned;
|
||||
for (final dir in _searchDirs()) {
|
||||
final p = '$dir/$name';
|
||||
if (_exists(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// True when [name]'s override is set but no longer points at a file — a stale
|
||||
/// pin (the tool moved on an upgrade). The caller warns and can re-detect.
|
||||
bool isStalePin(String name) {
|
||||
final p = _overrides[name];
|
||||
return p != null && p.isNotEmpty && !_exists(p);
|
||||
}
|
||||
|
||||
/// Probe for [names], returning name→absolute-path for those found — the seed
|
||||
/// for the override map on first run / re-detect.
|
||||
Map<String, String> detect(Iterable<String> names) {
|
||||
final dirs = _searchDirs().toList();
|
||||
final out = <String, String>{};
|
||||
for (final name in names) {
|
||||
for (final dir in dirs) {
|
||||
final p = '$dir/$name';
|
||||
if (_exists(p)) {
|
||||
out[name] = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Iterable<String> _searchDirs() sync* {
|
||||
final seen = <String>{};
|
||||
for (final dir in _searchPath().split(_sep)) {
|
||||
if (dir.isNotEmpty && seen.add(dir)) yield dir;
|
||||
}
|
||||
for (final dir in _wellKnownDirs()) {
|
||||
if (seen.add(dir)) yield dir;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _wellKnownDirs() {
|
||||
final h = _home ?? '';
|
||||
return [if (h.isNotEmpty) '$h/.local/bin', '/usr/local/bin', '/opt/homebrew/bin', '/opt/homebrew/sbin', '/home/linuxbrew/.linuxbrew/bin'];
|
||||
}
|
||||
}
|
||||
|
||||
bool _fileExists(String p) => File(p).existsSync();
|
||||
Reference in New Issue
Block a user