diff --git a/lib/src/env/supporter_binaries.dart b/lib/src/env/supporter_binaries.dart index 7909ca70..cfd44cc1 100644 --- a/lib/src/env/supporter_binaries.dart +++ b/lib/src/env/supporter_binaries.dart @@ -91,3 +91,35 @@ class SupporterBinaries { } bool _fileExists(String p) => File(p).existsSync(); + +/// The user-scope SettingsStore key (app layer, `~/.clide`, per-machine) holding +/// the tool→absolute-path override map. +const supporterToolsKey = 'app.tools'; + +/// The external supporter binaries clide auto-detects (pql/git are bundled per +/// D-58/D-59 and excluded). +const knownSupporterTools = ['claude', 'd2']; + +/// Build a [SupporterBinaries] from persisted overrides, auto-detecting on first +/// run (D-104). [readOverrides] returns the stored map (or null if unset); +/// [writeOverrides] persists a freshly-detected one. Injected (not the +/// SettingsStore directly) so this stays Flutter-free and `dart test`-able; +/// [detect] is the prober (defaults to a real [SupporterBinaries]). +Future loadSupporterBinaries({ + required Map? Function() readOverrides, + required Future Function(Map) writeOverrides, + List tools = knownSupporterTools, + SupporterBinaries Function()? detect, +}) async { + final raw = readOverrides(); + if (raw != null) return SupporterBinaries(overrides: _asStringMap(raw)); + // First run: probe once, persist (pinned), and use the result. + final detected = (detect?.call() ?? SupporterBinaries()).detect(tools); + await writeOverrides(detected); + return SupporterBinaries(overrides: detected); +} + +Map _asStringMap(Map raw) => { + for (final e in raw.entries) + if (e.key is String && e.value is String) e.key as String: e.value as String, +}; diff --git a/test/src/env/supporter_binaries_test.dart b/test/src/env/supporter_binaries_test.dart index 9faece8d..b1b7275f 100644 --- a/test/src/env/supporter_binaries_test.dart +++ b/test/src/env/supporter_binaries_test.dart @@ -59,4 +59,39 @@ void main() { expect(r.detect(['d2'])['d2'], '/usr/bin/d2'); }); }); + + group('loadSupporterBinaries', () { + test('first run detects the tools and persists the result', () async { + Map? written; + final probe = SupporterBinaries(exists: {'/usr/bin/claude'}.contains, searchPath: () => '/usr/bin', home: '/h'); + await loadSupporterBinaries(readOverrides: () => null, writeOverrides: (m) async => written = m, tools: ['claude', 'd2'], detect: () => probe); + expect(written, {'claude': '/usr/bin/claude'}); + }); + + test('first run with nothing found still persists (marks first-run done)', () async { + Map? written; + await loadSupporterBinaries( + readOverrides: () => null, + writeOverrides: (m) async => written = m, + detect: () => SupporterBinaries(exists: (_) => false, searchPath: () => '/usr/bin'), + ); + expect(written, isEmpty); + }); + + test('a subsequent run uses stored overrides without detecting or writing', () async { + var wrote = false, detected = false; + final r = await loadSupporterBinaries( + readOverrides: () => {'d2': '/pin/d2'}, + writeOverrides: (m) async => wrote = true, + detect: () { + detected = true; + return SupporterBinaries(); + }, + ); + expect(wrote, isFalse); + expect(detected, isFalse); + // The stored override loaded — /pin/d2 isn't a real file, so it reads stale. + expect(r.isStalePin('d2'), isTrue); + }); + }); }