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:
@@ -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;
|
||||
|
||||
@@ -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/<plat>/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);
|
||||
|
||||
@@ -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/<plat>/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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -108,6 +108,34 @@ void main() {
|
||||
);
|
||||
expect(i.inspect().state, CliInstallState.staleGui);
|
||||
});
|
||||
|
||||
test('devTree when clide resolves to a native/<plat>/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/<platform>/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;
|
||||
|
||||
Reference in New Issue
Block a user