From 51cfa15c78ebdb99f35d77174f4778edc7c54809 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 6 Jun 2026 21:53:01 +0200 Subject: [PATCH] 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//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) --- .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 1 + CHANGELOG.md | 4 ++ lib/builtin/cli_install/src/extension.dart | 6 ++ lib/kernel/src/cli_install.dart | 37 +++++++++++-- .../cli_install_extension_test.dart | 16 ++++++ test/kernel/cli_install_test.dart | 55 +++++++++++++++++++ 7 files changed, 116 insertions(+), 4 deletions(-) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index c5de1be8..9765d09e 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -495,3 +495,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-222', 'status', 'backlog', 'done', NULL, '2026-06-06 19:24:44', '2026-06-06 19:24:44', '2026-06-06 19:24:44', NULL, '7ba18fee0a7abf0f43ee86b5f8601a2e', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-223', 'status', 'in_progress', 'done', NULL, '2026-06-06 19:24:44', '2026-06-06 19:24:44', '2026-06-06 19:24:44', NULL, 'c4edb71439a13551d1ec27b2e8bcf2d3', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-225', 'status', 'in_progress', 'done', NULL, '2026-06-06 19:37:47', '2026-06-06 19:37:47', '2026-06-06 19:37:47', NULL, '5622a2bf33c28bc00aab480172366698', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-256', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 19:45:03', '2026-06-06 19:45:03', '2026-06-06 19:45:03', NULL, 'f9997e23005cbb1966f5e132d20524a5', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index dc7d1319..1eca55dc 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -1051,3 +1051,4 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-225', 'task', 'T-208', 'MCP transport reachable by an external agent', 'Gap 6 from self-analysis.md. The MCP SSE server (T-130, D-68) is live but mcp__ide__* tools are not exposed to an external agent out of the box, and getDiagnostics/executeCode were noted as stubs. The CLI path is the priority (Epics A-C); MCP follows. Decide + wire the minimal reachable MCP surface, or explicitly defer with a note. Relates to open question Q-32 (minimum MCP tool surface). Refinement (2026-06-06): tool-surface question resolved. Q-32 closed by D-86 — expose the full mcp__clide__* namespace, but GENERATE tools/list from the co-registered command registry (D-74) that already feeds the CLI + palette, so there is no hand-maintained second surface; add a per-command MCP opt-out for poor-fit verbs (long-lived streams, UI-side-effecting). Transport stays SSE-only per D-73 (Q-33 re-confirmed, not reopened). Remaining stubs to make real: getDiagnostics + executeCode. Scope is now: registry->MCP tool-definition adapter (arg-schema -> JSON-Schema), served over the existing SSE transport.', 'done', 'low', NULL, NULL, 'D-68', '2026-06-02 18:13:52', '2026-06-06 19:37:47', NULL, '45f318499292f9987fe8c8d99d9948e8', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-256', 'task', 'T-208', 'Install affordance: distinguish a dev-tree clide from a production-installed build', 'Flagged during T-212 (2026-06-06). The Install-clide-command affordance copies whatever CliInstaller resolves as the bundled C client into ~/.local/bin, and ''make run'' points CLIDE_CLI_BIN at the dev-tree client (native//clide). On a normal user machine that is correct: the bundled client beside the GUI runner IS the production build. But on a clide DEV system the clide on PATH should point at the properly-installed production build, not a dev-tree binary or stale build artefact. CliInstaller.inspect only classifies missing / staleGui (points into the Flutter bundle) / installed, and treats any non-GUI binary as installed without distinguishing dev-tree vs a real install. Good enough for now per the user; the affordance works for end users and fixes the stale-GUI-symlink footgun. Decide desired dev-machine behaviour (e.g. detect dev tree via CLIDE_PROJECT / repo-relative path and warn instead of silently installing, or prefer an already-correct production build) and refine inspect() accordingly.', 'in_progress', 'low', NULL, NULL, NULL, '2026-06-06 10:07:35', '2026-06-06 19:45:03', NULL, 'edd0f58a489218a6de945f8c0e1f92b3', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dad8f37..dcb76854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Changed +- The `clide` CLI launch check now distinguishes a dev-tree build + (`native//clide`) from a packaged install — surfaced as an info note on + a checkout rather than treated as a clean install or prompting a reinstall. + (T-256) - Command palette (⌘⇧P) now fuzzy-matches command titles (subsequence, not just substring) and floats recently-used commands to the top. (T-23) - ⌘K now opens a **Settings** modal instead of a theme-only picker. Its first diff --git a/lib/builtin/cli_install/src/extension.dart b/lib/builtin/cli_install/src/extension.dart index bfbc8b39..ac5949d4 100644 --- a/lib/builtin/cli_install/src/extension.dart +++ b/lib/builtin/cli_install/src/extension.dart @@ -48,6 +48,12 @@ class CliInstallExtension extends ClideExtension { 'in PATH" to replace it.', title: 'clide CLI is stale', ); + case CliInstallState.devTree: + ctx.notify.info( + '`clide` on your PATH is the dev-tree build, not a packaged install ' + '— fine for development; rebuild it with `make clide-cli`.', + title: 'clide CLI: dev build', + ); case CliInstallState.installed: break; } diff --git a/lib/kernel/src/cli_install.dart b/lib/kernel/src/cli_install.dart index 5b46d76d..708fedef 100644 --- a/lib/kernel/src/cli_install.dart +++ b/lib/kernel/src/cli_install.dart @@ -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//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//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, diff --git a/test/builtin/cli_install/cli_install_extension_test.dart b/test/builtin/cli_install/cli_install_extension_test.dart index ea2e26ed..8d4bdff0 100644 --- a/test/builtin/cli_install/cli_install_extension_test.dart +++ b/test/builtin/cli_install/cli_install_extension_test.dart @@ -85,6 +85,22 @@ void main() { expect(notes.first.title, 'clide CLI is stale'); }); + test('notes a dev-tree build on activation (info, not a warning) (T-256)', () async { + final dev = touchExec('${tmp.path}/native/linux-x64/clide').path; + final binDir = Directory('${tmp.path}/bin')..createSync(); + Link('${binDir.path}/clide').createSync(dev); + await boot(CliInstaller( + resolvedExecutable: '${tmp.path}/gui/clide', + env: {'PATH': binDir.path}, + bundledClientCandidates: const [], + installDir: binDir.path, + )); + final notes = f.services.notify.active; + expect(notes, isNotEmpty); + expect(notes.first.level, NotificationLevel.info); + expect(notes.first.title, 'clide CLI: dev build'); + }); + test('does not warn when clide is already installed', () async { final binDir = Directory('${tmp.path}/bin')..createSync(); touchExec('${binDir.path}/clide'); diff --git a/test/kernel/cli_install_test.dart b/test/kernel/cli_install_test.dart index 455490f2..fb96869e 100644 --- a/test/kernel/cli_install_test.dart +++ b/test/kernel/cli_install_test.dart @@ -108,6 +108,34 @@ void main() { ); expect(i.inspect().state, CliInstallState.staleGui); }); + + test('devTree when clide resolves to a native//clide build (T-256)', () { + final dev = touchExec('${tmp.path}/native/linux-x64/clide').path; + final binDir = Directory('${tmp.path}/bin')..createSync(); + Link('${binDir.path}/clide').createSync(dev); + final i = installer( + resolvedExecutable: '${tmp.path}/gui/clide', + env: {'PATH': binDir.path}, + ); + final s = i.inspect(); + expect(s.state, CliInstallState.devTree); + // A dev build is intentional — it doesn't prompt a reinstall. + expect(s.needsInstall, isFalse); + }); + }); + + group('isDevTreeClient', () { + test('matches the native//clide build artifacts', () { + expect(isDevTreeClient('/repo/native/linux-x64/clide'), isTrue); + expect(isDevTreeClient('/repo/native/macos-arm64/clide'), isTrue); + expect(isDevTreeClient('native/linux-arm64/clide'), isTrue); + }); + + test('does not match installed or bundled paths', () { + expect(isDevTreeClient('/home/x/.local/bin/clide'), isFalse); + expect(isDevTreeClient('/usr/local/bin/clide'), isFalse); + expect(isDevTreeClient('/opt/clide/bundle/clide-cli'), isFalse); + }); }); group('defaults', () { @@ -191,6 +219,33 @@ void main() { } }); + test('flags fromDevTree when the source is a dev-tree build (T-256)', () { + final src = touchExec('${tmp.path}/native/linux-x64/clide'); + final binDir = '${tmp.path}/bin'; + final i = installer( + resolvedExecutable: '${tmp.path}/gui/clide', + env: {'PATH': binDir}, + candidates: [src.path], + installDir: binDir, + ); + final r = i.install(); + expect(r.ok, isTrue); + expect(r.fromDevTree, isTrue); + expect(r.message, contains('dev-tree build')); + }); + + test('a bundled (non-dev) source is not flagged fromDevTree', () { + final src = touchExec('${tmp.path}/bundle/clide-cli'); + final binDir = '${tmp.path}/bin'; + final i = installer( + resolvedExecutable: '${tmp.path}/gui/clide', + env: {'PATH': binDir}, + candidates: [src.path], + installDir: binDir, + ); + expect(i.install().fromDevTree, isFalse); + }); + test('overwrites a stale symlink rather than following it', () { final src = touchExec('${tmp.path}/bundle/clide-cli', contents: 'NEW'); final gui = touchExec('${tmp.path}/gui/clide', contents: 'GUI').path;