feat(env): supporter-binary resolver — explicit override → PATH → well-known (T-495)
SupporterBinaries resolves claude/d2/etc. to absolute paths (D-104): an explicit override map first, then the login-shell/process PATH, then the well-known bin dirs incl. Homebrew-on-Linux — the dir the standard PATH expansion omits and a .bashrc-only brew shellenv misses (the D-104 gap). detect() probes those to seed the override map (pinned, not re-probed each launch); isStalePin flags a moved tool. Pure Dart, dart test (11 cases). Settings storage + first-run detect wire on next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8827,3 +8827,28 @@ RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG rendere
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', NULL, '2026-06-28 20:25:50', '2026-06-28 20:25:50.683', '2026-06-28 20:25:50.683', NULL, 'e8481d8539f90b8d5fd2dddb1932e2a0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'description', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.
|
||||
|
||||
COMPLETE (2026-06-28): per-object overlay done — captions (data-label/data-description) + lightbox (data-lightbox tap-to-zoom), positioned via the shared svgViewportFit so they track the painted SVG. Full T-318 scope built + tested: document envelope (draw_doc), template dispatch (draw_dispatch), DrawingCard widget, the draw CLI command (draw_commands), the extension wire (DrawingMessage + subscriber + renderer + boot registration), and the per-object overlay. ~40 tests across dart + flutter test. All acceptance met (raw SVG draws; a template dispatches; objects render captions; unknown template/primitive → userError). Templates d2/icon/compare/image are separate children under T-317; T-494 (d2) is next, gated on T-495.', NULL, '2026-06-28 20:40:54', '2026-06-28 20:40:54.751', '2026-06-28 20:40:54.751', NULL, '3c1694be81a3d05e22c8c5a2221e99fd', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'status', 'in_progress', 'done', NULL, '2026-06-28 20:40:54', '2026-06-28 20:40:54.785', '2026-06-28 20:40:54.785', NULL, '389acc78ce89d7225e268e20f3822fee', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 21:00:18', '2026-06-28 21:00:18.364', '2026-06-28 21:00:18.364', NULL, '00fa58dcedc18f6ce827b5a0ef6ad928', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -10614,3 +10614,36 @@ RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG rendere
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:25:50.683', NULL, 'a5a8e71c0e31375e52004b37b181b484', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'story', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card core: canvas engine + JSON schema + template dispatch', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.
|
||||
|
||||
COMPLETE (2026-06-28): per-object overlay done — captions (data-label/data-description) + lightbox (data-lightbox tap-to-zoom), positioned via the shared svgViewportFit so they track the painted SVG. Full T-318 scope built + tested: document envelope (draw_doc), template dispatch (draw_dispatch), DrawingCard widget, the draw CLI command (draw_commands), the extension wire (DrawingMessage + subscriber + renderer + boot registration), and the per-object overlay. ~40 tests across dart + flutter test. All acceptance met (raw SVG draws; a template dispatches; objects render captions; unknown template/primitive → userError). Templates d2/icon/compare/image are separate children under T-317; T-494 (d2) is next, gated on T-495.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:40:54.751', NULL, 'aad56f10912aec6041ae4e4a749196cb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'story', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card core: canvas engine + JSON schema + template dispatch', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
|
||||
|
||||
SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set.
|
||||
|
||||
MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.
|
||||
|
||||
RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.
|
||||
|
||||
PROGRESS (2026-06-28): drawing-card core built bottom-up + tested (24 cases across dart/flutter test): DrawingCardDoc envelope + parser (lib/src/draw/draw_doc.dart); template dispatch (draw_dispatch.dart — resolveDrawingSvg + DrawingRegistry, primitive svg/svgPath now, handlers for d2/icon/compare/image plug in); DrawingCard widget (lib/widgets/src/draw/ — SvgView + themed caption, display-only); and the CLI command (draw_commands.dart — reads+parses the doc, lowers to SVG, publishes {svg,label,description} on the ''draw'' MessageBus channel). REMAINING: (1) the Claude-extension subscriber that consumes the ''draw'' channel → buildSvgDocument(svg) → injects a DrawingCard message into the primary session (mirror image.show''s consumer); (2) per-object data-* overlay (captions/lightbox anchored to SVG elements) — advanced follow-on.
|
||||
|
||||
EXTENSION WIRE DONE + PUSHED (2026-06-28): item (1) complete — DrawingMessage + the Claude-extension subscriber on the draw channel + the conversation renderer + boot registration; clide draw --file doc.json renders end-to-end (commit 2beb4b9, pushed in 75f378ff). REMAINING (this session): the per-object data-* overlay — data-label / data-description / data-lightbox anchored to individual SVG elements, the last piece of T-318 scope.
|
||||
|
||||
COMPLETE (2026-06-28): per-object overlay done — captions (data-label/data-description) + lightbox (data-lightbox tap-to-zoom), positioned via the shared svgViewportFit so they track the painted SVG. Full T-318 scope built + tested: document envelope (draw_doc), template dispatch (draw_dispatch), DrawingCard widget, the draw CLI command (draw_commands), the extension wire (DrawingMessage + subscriber + renderer + boot registration), and the per-object overlay. ~40 tests across dart + flutter test. All acceptance met (raw SVG draws; a template dispatches; objects render captions; unknown template/primitive → userError). Templates d2/icon/compare/image are separate children under T-317; T-494 (d2) is next, gated on T-495.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 20:40:54.785', NULL, '61c3c704be2b8282530d1459ec1655f6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGYPJ6FTEPP4JK3D7D01ZSMM', 'task', NULL, 'Supporter-binary path resolution: explicit user-scope overrides + first-run auto-detect', 'Implement D-104. clide resolves external supporter binaries (claude, d2, future supporter tools) via an explicit user-scope ''tools:'' map (name → absolute path), consulted FIRST.
|
||||
|
||||
Resolution order: (1) explicit settings path → use it; honest error + fallback-with-warning if missing/non-exec. (2) bundled/pinned (pql D-58, git D-59). (3) login-shell PATH (T-439). (4) bare PATH.
|
||||
|
||||
First run: auto-detect each tool ONCE — probe the login-shell PATH + well-known dirs (~/.local/bin, /usr/local/bin, /opt/homebrew, /home/linuxbrew/.linuxbrew/bin) — and WRITE the discovered absolute paths into the map (pinned, not re-probed each launch). Provide a re-detect action.
|
||||
|
||||
Storage: user-scope SettingsStore keyed by machine (D-93) — never committed. Surface in the Config tab (T-414) + flag unresolved tools in the Problems panel (echoes D-58''s soft floor). Generalizes D-58''s CLIDE_PQL_BIN override to all supporter tools. First consumer: the d2 template (T-494).', 'in_progress', 'medium', NULL, NULL, 'D-104', '2026-06-28 17:50:02.366', '2026-06-28 21:00:18.364', NULL, 'de1818996b3226c6df2516d1aff9cfad', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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);
|
||||
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
/// Resolves external supporter binaries (claude, d2, …) to absolute paths
|
||||
/// (T-495, D-104).
|
||||
///
|
||||
/// Order: an explicit user-scope **override** map is consulted FIRST; then the
|
||||
/// login-shell / process search PATH ([resolvedToolPath]); then the well-known
|
||||
/// user/local bin dirs — including **Homebrew-on-Linux** (`/home/linuxbrew/...`),
|
||||
/// which the standard PATH expansion omits and a login-shell probe misses when
|
||||
/// `brew shellenv` lives only in `~/.bashrc`. That omission is the gap D-104
|
||||
/// fixes.
|
||||
///
|
||||
/// [detect] probes those locations to seed the override map on first run (or a
|
||||
/// re-detect), so resolution is **pinned** thereafter — deterministic, not a
|
||||
/// per-launch heuristic. A pin that no longer points at a file is reported by
|
||||
/// [isStalePin] so the caller can warn + re-detect rather than silently fail.
|
||||
///
|
||||
/// Pure Dart (injected `exists` / `searchPath`), Flutter-free; runs under
|
||||
/// `dart test`.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'shell_env.dart' show resolvedToolPath;
|
||||
|
||||
/// Whether [absolutePath] points at an existing file (symlinks followed).
|
||||
typedef PathExists = bool Function(String absolutePath);
|
||||
|
||||
class SupporterBinaries {
|
||||
SupporterBinaries({Map<String, String> overrides = const {}, PathExists? exists, String Function()? searchPath, String? home, bool isWindows = false})
|
||||
: _overrides = overrides,
|
||||
_exists = exists ?? _fileExists,
|
||||
_searchPath = searchPath ?? resolvedToolPath,
|
||||
_home = home ?? Platform.environment['HOME'],
|
||||
_sep = isWindows ? ';' : ':';
|
||||
|
||||
final Map<String, String> _overrides;
|
||||
final PathExists _exists;
|
||||
final String Function() _searchPath;
|
||||
final String? _home;
|
||||
final String _sep;
|
||||
|
||||
/// Resolve [name] to an absolute path, or `null`. Order: explicit override (if
|
||||
/// it still exists) → search PATH → well-known dirs.
|
||||
String? resolve(String name) {
|
||||
final pinned = _overrides[name];
|
||||
if (pinned != null && pinned.isNotEmpty && _exists(pinned)) return pinned;
|
||||
for (final dir in _searchDirs()) {
|
||||
final p = '$dir/$name';
|
||||
if (_exists(p)) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// True when [name]'s override is set but no longer points at a file — a stale
|
||||
/// pin (the tool moved on an upgrade). The caller warns and can re-detect.
|
||||
bool isStalePin(String name) {
|
||||
final p = _overrides[name];
|
||||
return p != null && p.isNotEmpty && !_exists(p);
|
||||
}
|
||||
|
||||
/// Probe for [names], returning name→absolute-path for those found — the seed
|
||||
/// for the override map on first run / re-detect.
|
||||
Map<String, String> detect(Iterable<String> names) {
|
||||
final dirs = _searchDirs().toList();
|
||||
final out = <String, String>{};
|
||||
for (final name in names) {
|
||||
for (final dir in dirs) {
|
||||
final p = '$dir/$name';
|
||||
if (_exists(p)) {
|
||||
out[name] = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Iterable<String> _searchDirs() sync* {
|
||||
final seen = <String>{};
|
||||
for (final dir in _searchPath().split(_sep)) {
|
||||
if (dir.isNotEmpty && seen.add(dir)) yield dir;
|
||||
}
|
||||
for (final dir in _wellKnownDirs()) {
|
||||
if (seen.add(dir)) yield dir;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _wellKnownDirs() {
|
||||
final h = _home ?? '';
|
||||
return [if (h.isNotEmpty) '$h/.local/bin', '/usr/local/bin', '/opt/homebrew/bin', '/opt/homebrew/sbin', '/home/linuxbrew/.linuxbrew/bin'];
|
||||
}
|
||||
}
|
||||
|
||||
bool _fileExists(String p) => File(p).existsSync();
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'package:clide/src/env/supporter_binaries.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
SupporterBinaries make({Map<String, String> overrides = const {}, Set<String> files = const {}, String path = '/usr/bin:/bin', String home = '/home/u'}) =>
|
||||
SupporterBinaries(overrides: overrides, exists: files.contains, searchPath: () => path, home: home);
|
||||
|
||||
group('resolve', () {
|
||||
test('an explicit override wins', () {
|
||||
final r = make(overrides: {'d2': '/opt/d2'}, files: {'/opt/d2', '/usr/bin/d2'});
|
||||
expect(r.resolve('d2'), '/opt/d2');
|
||||
});
|
||||
|
||||
test('a stale override falls through to the PATH', () {
|
||||
final r = make(overrides: {'d2': '/gone/d2'}, files: {'/usr/bin/d2'});
|
||||
expect(r.resolve('d2'), '/usr/bin/d2');
|
||||
});
|
||||
|
||||
test('searches the PATH when there is no override', () {
|
||||
final r = make(files: {'/bin/d2'});
|
||||
expect(r.resolve('d2'), '/bin/d2');
|
||||
});
|
||||
|
||||
test('finds a tool in linuxbrew via the well-known dirs (the D-104 gap)', () {
|
||||
final r = make(files: {'/home/linuxbrew/.linuxbrew/bin/d2'});
|
||||
expect(r.resolve('d2'), '/home/linuxbrew/.linuxbrew/bin/d2');
|
||||
});
|
||||
|
||||
test('finds a tool in ~/.local/bin', () {
|
||||
final r = make(files: {'/home/u/.local/bin/pql'}, path: '/usr/bin');
|
||||
expect(r.resolve('pql'), '/home/u/.local/bin/pql');
|
||||
});
|
||||
|
||||
test('returns null when unresolved', () {
|
||||
expect(make().resolve('nope'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('isStalePin', () {
|
||||
test('true when the override points at a missing file', () {
|
||||
expect(make(overrides: {'d2': '/gone'}).isStalePin('d2'), isTrue);
|
||||
});
|
||||
test('false when the override exists', () {
|
||||
expect(make(overrides: {'d2': '/ok'}, files: {'/ok'}).isStalePin('d2'), isFalse);
|
||||
});
|
||||
test('false when there is no override', () {
|
||||
expect(make().isStalePin('d2'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('detect', () {
|
||||
test('finds tools across the PATH and well-known dirs', () {
|
||||
final r = make(files: {'/usr/bin/claude', '/home/linuxbrew/.linuxbrew/bin/d2'}, path: '/usr/bin');
|
||||
expect(r.detect(['claude', 'd2', 'missing']), {'claude': '/usr/bin/claude', 'd2': '/home/linuxbrew/.linuxbrew/bin/d2'});
|
||||
});
|
||||
|
||||
test('prefers the earliest matching directory', () {
|
||||
final r = make(files: {'/usr/bin/d2', '/home/linuxbrew/.linuxbrew/bin/d2'}, path: '/usr/bin');
|
||||
expect(r.detect(['d2'])['d2'], '/usr/bin/d2');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user