distinguish a dev-tree clide from a packaged install (T-256)

CliInstaller.inspect() gains a devTree state: when `clide` on PATH resolves
to a dev-tree build artifact (native/<plat>/clide, the Makefile's
CLIDE_CLI_BIN output) it's classified separately from a packaged install
rather than silently treated as "installed". needsInstall stays false for a
dev build (it's intentional on a checkout, not a reinstall prompt), and the
launch-time check surfaces it as an info note. install() flags fromDevTree +
notes it in the result message when the copied source is a dev build.

Closes the last open child of the T-208 "give Claude hands" initiative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 21:53:01 +02:00
co-authored by Claude Opus 4.8
parent 9cb7da3f57
commit 51cfa15c78
7 changed files with 116 additions and 4 deletions
+33 -4
View File
@@ -23,7 +23,14 @@ enum CliInstallState {
/// client — running it launches a second app instead of querying.
staleGui,
/// `clide` resolves to something that is not the GUI — assumed good.
/// `clide` resolves to a dev-tree build artifact (`native/<plat>/clide`),
/// not a packaged production install (T-256). A working client — fine on a
/// dev checkout — but worth surfacing so it isn't mistaken for a real
/// install, and so a rebuild/reinstall isn't silently skipped.
devTree,
/// `clide` resolves to something that is not the GUI or a dev build —
/// assumed a good production install.
installed,
}
@@ -39,8 +46,10 @@ class CliInstallStatus {
/// Where [pathEntry] resolves to after following symlinks, if any.
final String? resolvedTarget;
/// True when the user should (re)install — missing or stale.
bool get needsInstall => state != CliInstallState.installed;
/// True when the user should (re)install — `clide` is absent or points at
/// the GUI. A dev-tree build is intentional on a checkout, so it doesn't
/// trigger an install prompt.
bool get needsInstall => state == CliInstallState.missing || state == CliInstallState.staleGui;
}
/// Result of [CliInstaller.install].
@@ -50,6 +59,7 @@ class CliInstallResult {
required this.message,
this.installedPath,
this.onPath = true,
this.fromDevTree = false,
});
final bool ok;
@@ -59,6 +69,10 @@ class CliInstallResult {
/// False when [installedPath]'s directory is not itself on PATH (the copy
/// succeeded but the user must add the dir to PATH to reach `clide`).
final bool onPath;
/// True when the source copied was a dev-tree build artifact rather than the
/// bundled production client (T-256) — a packaged install ships the latter.
final bool fromDevTree;
}
/// Copies the bundled C client onto PATH and reports what `clide` currently
@@ -105,6 +119,9 @@ class CliInstaller {
if (_isGui(resolved)) {
return CliInstallStatus(CliInstallState.staleGui, pathEntry: found, resolvedTarget: resolved);
}
if (isDevTreeClient(resolved)) {
return CliInstallStatus(CliInstallState.devTree, pathEntry: found, resolvedTarget: resolved);
}
return CliInstallStatus(CliInstallState.installed, pathEntry: found, resolvedTarget: resolved);
}
@@ -134,11 +151,14 @@ class CliInstaller {
return CliInstallResult(ok: false, message: 'Install failed: ${e.message}', installedPath: dest);
}
final onPath = _dirOnPath(installDir);
final fromDevTree = isDevTreeClient(_resolve(src));
final base = onPath ? 'Installed clide to $dest' : 'Installed clide to $dest — add $installDir to your PATH to use it.';
return CliInstallResult(
ok: true,
installedPath: dest,
onPath: onPath,
message: onPath ? 'Installed clide to $dest' : 'Installed clide to $dest — add $installDir to your PATH to use it.',
fromDevTree: fromDevTree,
message: fromDevTree ? '$base (from the dev-tree build; a packaged install ships the production client).' : base,
);
}
@@ -204,6 +224,15 @@ class CliInstaller {
}
}
final RegExp _devTreeClient = RegExp(r'(^|/)native/(linux|macos)-(x64|arm64)/clide$');
/// True when [path] is a dev-tree C-client build artifact —
/// `native/<platform>/clide`, the Makefile's `CLIDE_CLI_BIN` output. On a clide
/// checkout `make run` points `CLIDE_CLI_BIN` there and a dev may put it on
/// PATH; it's a working client but not a packaged production install, so it's
/// classified separately (T-256) rather than as a clean install.
bool isDevTreeClient(String path) => _devTreeClient.hasMatch(path);
/// Expand a `PATH` value. Mirrors `toolchain_paths.dart`: macOS GUI apps
/// launch with a sparse PATH that omits the usual user/homebrew bins, so on
/// macOS we prepend those (de-duplicated) before scanning. A top-level,