From 4ac52664eac54998cd80be25a45c166d963449f9 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 3 Jun 2026 11:01:13 +0200 Subject: [PATCH] ship the clide CLI on PATH (Epic A) `make build`/`make install` now compile the C `clide` client by default (clide-cli is a prereq of both), and `make install` places that binary at $(INSTALL_DIR)/clide on Linux and macOS instead of symlinking the Flutter GUI runner. The GUI still launches via its desktop entry / clide.app. macOS install now delivers the CLI too; macOS uninstall removes it. Adds a regression test asserting clideVersion (build_info.g.dart) stays equal to pubspec.yaml version, so an agent keying off `clide version` is never misled. Closes T-210, T-211, T-213 (under T-209 / T-208 "Give Claude hands"). Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 6 ++++ .pql/changelog/tickets/2026-06.sql | 3 ++ CHANGELOG.md | 4 +++ Makefile | 16 +++++++---- test/cli/clide_version_test.dart | 35 +++++++++++++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 test/cli/clide_version_test.dart diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 657df591..d5fa4b1e 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -47,3 +47,9 @@ 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-206', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 19:16:58', '2026-06-01 19:16:58', '2026-06-01 19:16:58', NULL, '4b1e494c63361eb99d59e0491ff841c3', 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-206', 'status', 'in_progress', 'done', NULL, '2026-06-01 19:27:18', '2026-06-01 19:27:18', '2026-06-01 19:27:18', NULL, '9ec3404aa10ff5c655124c4e5b857a31', 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-65', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 19:29:15', '2026-06-01 19:29:15', '2026-06-01 19:29:15', NULL, '021a754b25453a1548ee9278f2719072', 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-211', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 08:57:22', '2026-06-03 08:57:22', '2026-06-03 08:57:22', NULL, '5ffa9c6d4624f86acf4b959aa43fde6f', 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-210', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 08:57:22', '2026-06-03 08:57:22', '2026-06-03 08:57:22', NULL, 'b8cd1a9e012f2fef2e1fed265b647d36', 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-213', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 08:57:22', '2026-06-03 08:57:22', '2026-06-03 08:57:22', NULL, 'fc0e30131455fa1db38505ca0aa222e4', 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-213', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:00:37', '2026-06-03 09:00:37', '2026-06-03 09:00:37', NULL, '7df53985fc88d02f9087ccfcf78770ae', 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-210', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:00:37', '2026-06-03 09:00:37', '2026-06-03 09:00:37', NULL, '84dd4f5fa45cb78da9a0b11c73fb149b', 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-211', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:00:37', '2026-06-03 09:00:37', '2026-06-03 09:00:37', NULL, 'aa86077555916ee351369e23a6ca155b', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index b07cc8ea..cc060feb 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -79,3 +79,6 @@ 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-210', 'task', 'T-209', 'Build the C clide client by default', 'Make the `clide-cli` target (Makefile:284) a dependency of `build` (Makefile:169) and `install` (Makefile:195) so the C client is always produced. Today it builds only via the explicit, non-default `make clide-cli`. Acceptance: a plain `make build` / `make install` produces the C client binary for the current OS.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, 'e2bead8a722cf0375adf79d22050b30e', 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-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).', 'backlog', 'low', NULL, NULL, 'D-68', '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, 'ebc32a0ac23296877246ac17f50d15b5', 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-215', 'task', 'T-214', 'Export CLIDE_SOCK/CLIDE_WORKSPACE + ensure clide on the child PATH', 'When clide spawns/hosts an agent (the stream-json session per D-77, and any launched agent), inject CLIDE_SOCK (the per-workspace deterministic socket, D-70) and CLIDE_WORKSPACE into the child env, and guarantee `clide` is on the child PATH. Acceptance: a hosted agent''s shell has both vars and `clide ping` works with zero manual socket discovery. Blocked by Epic A delivery.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, 'fed9697da84ca9013659b9a882475c3a', 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-213', 'task', 'T-209', 'Regression test: clideVersion matches pubspec version', 'Gap 6 version-drift guard. clideVersion in lib/src/build_info.g.dart (generated by gen-build-info) currently matches pubspec.yaml `version:` (both 2.1.0). Add a test asserting they stay equal so an agent keying off `clide version` is never misled.', 'done', 'low', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-03 09:00:37', NULL, '1cc6925922c30c6f7e1dc9ee356de8e3', 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-211', 'task', 'T-209', 'install delivers the C client to PATH on macOS and Linux', 'Fix `make install` so $(INSTALL_DIR)/clide (default ~/.local/bin/clide) IS the compiled C client, not the Flutter GUI binary. Today Makefile:200 symlinks the GUI bundle runner on both platforms. Acceptance: after `make install`, `which clide` resolves and `clide ping` returns {"pong":true} against a running instance on macOS and Linux. Blocked by ''Build the C client by default''.', 'done', 'high', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-03 09:00:37', NULL, '2e1f0a48393ff4f04079124fd2a1f579', 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-210', 'task', 'T-209', 'Build the C clide client by default', 'Make the `clide-cli` target (Makefile:284) a dependency of `build` (Makefile:169) and `install` (Makefile:195) so the C client is always produced. Today it builds only via the explicit, non-default `make clide-cli`. Acceptance: a plain `make build` / `make install` produces the C client binary for the current OS.', 'done', 'high', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-03 09:00:37', NULL, '738656b25288166130ca516a88b94784', 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 eebcb89b..9e195d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- The `clide` CLI now ships on PATH: `make build` and `make install` compile + the C client by default, and `make install` places it at `~/.local/bin/clide` + on Linux and macOS (the GUI launches via its desktop entry). Previously no + build produced the client and `install` symlinked the GUI runner. (T-209) - Vim keymap preset: a modal editor (normal/insert/visual) with hjkl/w/b/e motions, dd/dw/x/D/yy/p/cc/cw edits, counts (`5j`), visual-range d/y/c, and a status-bar mode indicator. Switch presets from the palette (`Keymap: Vim` diff --git a/Makefile b/Makefile index 4986ca56..6ccbb280 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ ui-smoke: ## Build + serve + run Playwright smoke + stop. @sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts' .PHONY: build -build: gen-build-info ## flutter build for the current OS. +build: gen-build-info clide-cli ## flutter build for the current OS (incl. the C CLI client). flutter build $(FLUTTER_OS) .PHONY: build-linux @@ -192,12 +192,12 @@ endif ICON_SIZES := 16 32 48 128 192 256 512 .PHONY: install -install: build ## Build + install clide to ~/.local (INSTALL_PREFIX, INSTALL_DIR). +install: build clide-cli ## Build + install clide to ~/.local (INSTALL_PREFIX, INSTALL_DIR). ifeq ($(FLUTTER_OS),linux) @mkdir -p $(INSTALL_PREFIX) $(INSTALL_DIR) rm -rf $(INSTALL_PREFIX)/clide cp -a $(BUNDLE_DIR) $(INSTALL_PREFIX)/clide - ln -sf $(INSTALL_PREFIX)/clide/clide $(INSTALL_DIR)/clide + install -m 755 $(CLIDE_CLI_BIN) $(INSTALL_DIR)/clide @for size in $(ICON_SIZES); do \ dir=$(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps; \ mkdir -p $$dir; \ @@ -208,14 +208,17 @@ ifeq ($(FLUTTER_OS),linux) > $(HOME)/.local/share/applications/net.schweitz.clide.desktop @gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true @update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true - @echo "installed: $(INSTALL_DIR)/clide -> $(INSTALL_PREFIX)/clide/clide" + @echo "cli: $(INSTALL_DIR)/clide (C client)" + @echo "gui: $(INSTALL_PREFIX)/clide/clide" @echo "desktop: ~/.local/share/applications/net.schweitz.clide.desktop" @echo "version: $(VERSION)" else ifeq ($(FLUTTER_OS),macos) - @mkdir -p $(HOME)/Applications + @mkdir -p $(HOME)/Applications $(INSTALL_DIR) rm -rf $(HOME)/Applications/clide.app cp -a $(BUNDLE_DIR) $(HOME)/Applications/clide.app - @echo "installed: ~/Applications/clide.app" + install -m 755 $(CLIDE_CLI_BIN) $(INSTALL_DIR)/clide + @echo "gui: ~/Applications/clide.app" + @echo "cli: $(INSTALL_DIR)/clide (C client)" @echo "version: $(VERSION)" else @echo "install not yet supported on $(FLUTTER_OS)" @@ -236,6 +239,7 @@ ifeq ($(FLUTTER_OS),linux) @update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true @echo "uninstalled" else ifeq ($(FLUTTER_OS),macos) + rm -f $(INSTALL_DIR)/clide rm -rf $(HOME)/Applications/clide.app @echo "uninstalled" endif diff --git a/test/cli/clide_version_test.dart b/test/cli/clide_version_test.dart new file mode 100644 index 00000000..89df2a05 --- /dev/null +++ b/test/cli/clide_version_test.dart @@ -0,0 +1,35 @@ +/// Regression guard for the `clide version` contract (T-213). +/// +/// `clideVersion` (lib/src/build_info.g.dart, baked by gen-build-info) +/// is what the dispatcher returns for `ping`/`version` and what the C +/// client surfaces. An agent keys off `clide version`; if it ever drifts +/// from pubspec.yaml `version:` — the single source of truth — that agent +/// is misled. This test fails the moment the two diverge. +library; + +import 'dart:io'; + +import 'package:clide/clide.dart' show clideVersion; +import 'package:test/test.dart'; + +/// First `version:` entry in pubspec.yaml — mirrors the Makefile awk +/// (`/^version:/ {gsub(/[" ]/,"",$2); print $2}`). +String _pubspecVersion() { + final lines = File('pubspec.yaml').readAsLinesSync(); + for (final line in lines) { + if (line.startsWith('version:')) { + return line.substring('version:'.length).replaceAll('"', '').trim(); + } + } + fail('no `version:` line in pubspec.yaml'); +} + +void main() { + test('clideVersion matches pubspec.yaml version (T-213)', () { + expect( + clideVersion, + _pubspecVersion(), + reason: 'build_info.g.dart drifted from pubspec.yaml — run `make gen-build-info`', + ); + }); +}