code-coordinator 0.5.46__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. code_coordinator-0.5.46.dist-info/METADATA +625 -0
  2. code_coordinator-0.5.46.dist-info/RECORD +295 -0
  3. code_coordinator-0.5.46.dist-info/WHEEL +5 -0
  4. code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
  5. code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
  6. code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
  7. coord/__init__.py +176 -0
  8. coord/_board_mapping.py +229 -0
  9. coord/acceptance.py +468 -0
  10. coord/acceptance_drivers.py +632 -0
  11. coord/agent.py +7517 -0
  12. coord/agent_app.py +1555 -0
  13. coord/agent_update.py +417 -0
  14. coord/agents/opencode/.gitignore +13 -0
  15. coord/agents/opencode/agents/work.md +129 -0
  16. coord/agents/opencode/routing.jsonc +49 -0
  17. coord/audit.py +301 -0
  18. coord/auto_loop.py +1440 -0
  19. coord/board_bool_guard.py +72 -0
  20. coord/board_service.py +141 -0
  21. coord/board_wire.py +309 -0
  22. coord/brain.py +581 -0
  23. coord/branch_model.py +214 -0
  24. coord/cargo_cache.py +258 -0
  25. coord/ci_github.py +386 -0
  26. coord/ci_store.py +560 -0
  27. coord/claim.py +353 -0
  28. coord/cli.py +454 -0
  29. coord/client.py +610 -0
  30. coord/commands/__init__.py +1 -0
  31. coord/commands/_common.py +329 -0
  32. coord/commands/acceptance.py +916 -0
  33. coord/commands/agent_ops.py +1339 -0
  34. coord/commands/audit.py +131 -0
  35. coord/commands/chat.py +320 -0
  36. coord/commands/dispatch.py +1780 -0
  37. coord/commands/dispatch_workers.py +4894 -0
  38. coord/commands/drive.py +616 -0
  39. coord/commands/drive_queue.py +1203 -0
  40. coord/commands/gate_a.py +217 -0
  41. coord/commands/gates.py +89 -0
  42. coord/commands/issues.py +681 -0
  43. coord/commands/lifecycle.py +513 -0
  44. coord/commands/merge.py +1900 -0
  45. coord/commands/milestone.py +2081 -0
  46. coord/commands/plan_followup.py +1243 -0
  47. coord/commands/plans.py +156 -0
  48. coord/commands/release.py +2232 -0
  49. coord/commands/report.py +341 -0
  50. coord/commands/review.py +1523 -0
  51. coord/commands/scorecard.py +252 -0
  52. coord/commands/sessions.py +1930 -0
  53. coord/commands/setup.py +576 -0
  54. coord/commands/status.py +2089 -0
  55. coord/commands/terminal.py +385 -0
  56. coord/commands/test_gate.py +775 -0
  57. coord/commands/tui.py +288 -0
  58. coord/comments.py +718 -0
  59. coord/config.py +3032 -0
  60. coord/conflict_fix.py +633 -0
  61. coord/dao.py +483 -0
  62. coord/dashboard/__init__.py +0 -0
  63. coord/dashboard/fixture.py +376 -0
  64. coord/dashboard/index.html +658 -0
  65. coord/dashboard/server.py +1894 -0
  66. coord/dashboard/terminal.py +382 -0
  67. coord/dashboard/webapp/.gitignore +9 -0
  68. coord/dashboard/webapp/components.json +17 -0
  69. coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
  70. coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
  71. coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
  72. coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
  73. coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
  74. coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
  75. coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
  76. coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
  77. coord/dashboard/webapp/dist/icons/icon.svg +5 -0
  78. coord/dashboard/webapp/dist/index.html +38 -0
  79. coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
  80. coord/dashboard/webapp/dist/sw.js +1 -0
  81. coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
  82. coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
  83. coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
  84. coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
  85. coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
  86. coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
  87. coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
  88. coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
  89. coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
  90. coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
  91. coord/dashboard/webapp/eslint.config.js +20 -0
  92. coord/dashboard/webapp/index.html +37 -0
  93. coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
  94. coord/dashboard/webapp/package-lock.json +10584 -0
  95. coord/dashboard/webapp/package.json +63 -0
  96. coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
  97. coord/dashboard/webapp/playwright.config.ts +93 -0
  98. coord/dashboard/webapp/postcss.config.js +6 -0
  99. coord/dashboard/webapp/public/icons/icon-192.png +0 -0
  100. coord/dashboard/webapp/public/icons/icon-512.png +0 -0
  101. coord/dashboard/webapp/public/icons/icon.svg +5 -0
  102. coord/dashboard/webapp/src/App.tsx +140 -0
  103. coord/dashboard/webapp/src/api/client.ts +199 -0
  104. coord/dashboard/webapp/src/api/generated.ts +176 -0
  105. coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
  106. coord/dashboard/webapp/src/components/Detail.tsx +800 -0
  107. coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
  108. coord/dashboard/webapp/src/components/Home.tsx +435 -0
  109. coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
  110. coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
  111. coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
  112. coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
  113. coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
  114. coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
  115. coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
  116. coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
  117. coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
  118. coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
  119. coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
  120. coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
  121. coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
  122. coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
  123. coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
  124. coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
  125. coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
  126. coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
  127. coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
  128. coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
  129. coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
  130. coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
  131. coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
  132. coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
  133. coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
  134. coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
  135. coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
  136. coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
  137. coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
  138. coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
  139. coord/dashboard/webapp/src/index.css +210 -0
  140. coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
  141. coord/dashboard/webapp/src/lib/utils.ts +6 -0
  142. coord/dashboard/webapp/src/main.tsx +46 -0
  143. coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
  144. coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
  145. coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
  146. coord/dashboard/webapp/src/realtime/connection.ts +227 -0
  147. coord/dashboard/webapp/src/realtime/events.ts +100 -0
  148. coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
  149. coord/dashboard/webapp/src/routes/paths.ts +92 -0
  150. coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
  151. coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
  152. coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
  153. coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
  154. coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
  155. coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
  156. coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
  157. coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
  158. coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
  159. coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
  160. coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
  161. coord/dashboard/webapp/src/shell/railItems.ts +105 -0
  162. coord/dashboard/webapp/src/shell/shellState.ts +174 -0
  163. coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
  164. coord/dashboard/webapp/src/test-setup.ts +41 -0
  165. coord/dashboard/webapp/src/vite-env.d.ts +2 -0
  166. coord/dashboard/webapp/tailwind.config.js +140 -0
  167. coord/dashboard/webapp/tsconfig.json +25 -0
  168. coord/dashboard/webapp/tsconfig.node.json +11 -0
  169. coord/dashboard/webapp/vite.config.ts +71 -0
  170. coord/db.py +1076 -0
  171. coord/dead_end.py +332 -0
  172. coord/deploy/README.md +33 -0
  173. coord/deploy/coord-agent.service +89 -0
  174. coord/deploy/coord-db-backup.service +60 -0
  175. coord/deploy/coord-db-backup.sh +74 -0
  176. coord/deploy/coord-db-backup.timer +18 -0
  177. coord/deploy/coord-drive-queue.service +117 -0
  178. coord/deploy/coord-drive-queue.timer +39 -0
  179. coord/deploy/coord-notify.service +48 -0
  180. coord/deploy/coord-notify.timer +24 -0
  181. coord/deploy/coord-release-propagate.service +83 -0
  182. coord/deploy/coord-release-propagate.timer +38 -0
  183. coord/deploy/coord-release-window.service +119 -0
  184. coord/deploy/coord-release-window.timer +36 -0
  185. coord/deploy/coord-serve.service +82 -0
  186. coord/deploy/coord-web-dist-build.service +43 -0
  187. coord/deploy/coord-web-dist-build.timer +36 -0
  188. coord/deploy/coord-web.service +125 -0
  189. coord/deploy_manifest.py +80 -0
  190. coord/deploy_units.py +384 -0
  191. coord/deps.py +115 -0
  192. coord/diagnose.py +1623 -0
  193. coord/dispatch.py +1009 -0
  194. coord/dist_name.py +123 -0
  195. coord/drive.py +3101 -0
  196. coord/drive_queue.py +2298 -0
  197. coord/drive_state.py +870 -0
  198. coord/events.py +381 -0
  199. coord/failure_class.py +914 -0
  200. coord/filelock.py +168 -0
  201. coord/fleet_config_health.py +300 -0
  202. coord/freshness.py +206 -0
  203. coord/gate_a.py +469 -0
  204. coord/gate_b.py +411 -0
  205. coord/gate_snapshot.py +385 -0
  206. coord/gates.py +582 -0
  207. coord/github_ops.py +1954 -0
  208. coord/goal.py +125 -0
  209. coord/graph_health.py +348 -0
  210. coord/health/__init__.py +69 -0
  211. coord/health/aggregate.py +129 -0
  212. coord/health/checks/__init__.py +13 -0
  213. coord/health/checks/agent_install.py +280 -0
  214. coord/health/checks/cargo_targets.py +171 -0
  215. coord/health/checks/claude_binary.py +65 -0
  216. coord/health/checks/deploy_lane_facts.py +458 -0
  217. coord/health/checks/disk.py +99 -0
  218. coord/health/checks/fleet_board.py +89 -0
  219. coord/health/checks/fleet_deploy_lanes.py +469 -0
  220. coord/health/checks/fleet_phantom.py +69 -0
  221. coord/health/checks/fleet_unit_drift.py +151 -0
  222. coord/health/checks/graph.py +192 -0
  223. coord/health/checks/plan_usage.py +88 -0
  224. coord/health/checks/repo_state.py +161 -0
  225. coord/health/checks/spawned_coord.py +465 -0
  226. coord/health/checks/timer_active.py +254 -0
  227. coord/health/checks/toolchain.py +547 -0
  228. coord/health/checks/unit_drift.py +648 -0
  229. coord/health/checks/unit_enablement.py +171 -0
  230. coord/health/checks/worktrees.py +96 -0
  231. coord/health/cli.py +121 -0
  232. coord/health/context.py +106 -0
  233. coord/health/fleet_snapshot.py +477 -0
  234. coord/health/models.py +250 -0
  235. coord/health/pypi.py +231 -0
  236. coord/health/registry.py +240 -0
  237. coord/health/render.py +82 -0
  238. coord/health/units.py +60 -0
  239. coord/hooks.py +106 -0
  240. coord/housekeeping.py +204 -0
  241. coord/interactive.py +4286 -0
  242. coord/issue_store.py +1496 -0
  243. coord/liveness_auditor.py +293 -0
  244. coord/machine_pause.py +755 -0
  245. coord/merge_queue.py +4681 -0
  246. coord/milestone_chat.py +600 -0
  247. coord/milestone_dispatch.py +943 -0
  248. coord/milestone_gate.py +709 -0
  249. coord/milestone_order.py +840 -0
  250. coord/mock_author.py +334 -0
  251. coord/models.py +891 -0
  252. coord/network.py +269 -0
  253. coord/new_issue_chat.py +229 -0
  254. coord/notify.py +3226 -0
  255. coord/openapi.py +404 -0
  256. coord/overlap_fence.py +133 -0
  257. coord/parentage.py +200 -0
  258. coord/parentage_github.py +58 -0
  259. coord/pipeline.py +481 -0
  260. coord/plan_parser.py +266 -0
  261. coord/plans.py +543 -0
  262. coord/platform_paths.py +43 -0
  263. coord/pr_body_lint.py +67 -0
  264. coord/prereqs.py +533 -0
  265. coord/progress.py +425 -0
  266. coord/providers/__init__.py +683 -0
  267. coord/providers/base.py +218 -0
  268. coord/providers/claude.py +284 -0
  269. coord/providers/claude_pty.py +610 -0
  270. coord/providers/opencode.py +896 -0
  271. coord/reconcile.py +2233 -0
  272. coord/refine_chat.py +485 -0
  273. coord/release_cordon.py +525 -0
  274. coord/release_propagate.py +1176 -0
  275. coord/release_verify.py +777 -0
  276. coord/release_window.py +322 -0
  277. coord/reports.py +1643 -0
  278. coord/revalidate.py +1101 -0
  279. coord/review.py +3317 -0
  280. coord/scorecard.py +484 -0
  281. coord/serve_app.py +7192 -0
  282. coord/skills/update-issue/SKILL.md +93 -0
  283. coord/smoke.py +1030 -0
  284. coord/split_work.py +210 -0
  285. coord/stage_projection.py +650 -0
  286. coord/state.py +5720 -0
  287. coord/test_author.py +1064 -0
  288. coord/test_chat.py +352 -0
  289. coord/test_orchestrator.py +494 -0
  290. coord/test_report.py +178 -0
  291. coord/tui_release.py +271 -0
  292. coord/usage.py +753 -0
  293. coord/usage_limits.py +358 -0
  294. coord/usage_rollup.py +709 -0
  295. coord/worker_events.py +954 -0
coord/commands/tui.py ADDED
@@ -0,0 +1,288 @@
1
+ """`coord tui` — self-update the coord-tui binary from this coordinator's
2
+ own GitHub Release channel (#1240, PKG-4).
3
+
4
+ PKG-3 (#1239) put ``coord-tui-<target>`` binaries on the GitHub Release; the
5
+ gap this closes is that a user still had to find the right asset, download
6
+ it, `chmod +x` it, and place it by hand. `coord tui update` does all of
7
+ that in one command, and `coord tui status` (also what a bare `coord tui`
8
+ runs) is the "lightweight version-skew notice" the issue asks for — a
9
+ local, on-demand check, not something bolted onto `main()`'s callback and
10
+ paid by every `coord` invocation.
11
+
12
+ The actual resolve/download/install mechanics live in
13
+ :mod:`coord.tui_release`, framework-agnostic so they're unit-testable
14
+ without going through Click at all; this module is just the CLI wiring.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import click
23
+
24
+ from coord import __version__
25
+ from coord.tui_release import (
26
+ DEFAULT_API_BASE,
27
+ DEFAULT_INSTALL_PATH,
28
+ DEFAULT_REPO,
29
+ DEV_BUILD_SENTINEL_VERSION,
30
+ ReleaseAssetNotFoundError,
31
+ UnsupportedPlatformError,
32
+ detect_target,
33
+ download_asset,
34
+ fetch_release_assets,
35
+ find_asset,
36
+ find_checksum_asset,
37
+ install_atomically,
38
+ read_installed_version,
39
+ sha256_file,
40
+ )
41
+
42
+
43
+ def _dest_path(dest: str | None) -> Path:
44
+ return Path(dest).expanduser() if dest else Path(DEFAULT_INSTALL_PATH).expanduser()
45
+
46
+
47
+ @click.group(
48
+ "tui",
49
+ invoke_without_command=True,
50
+ help=(
51
+ "Manage the coord-tui binary. Without a subcommand, runs `coord tui "
52
+ "status` (installed coord-tui version vs. this coordinator's own "
53
+ f"version, {DEFAULT_INSTALL_PATH} by default)."
54
+ ),
55
+ )
56
+ @click.pass_context
57
+ def tui_group(ctx: click.Context) -> None:
58
+ if ctx.invoked_subcommand is None:
59
+ ctx.invoke(tui_status)
60
+
61
+
62
+ @tui_group.command(
63
+ "status",
64
+ help=(
65
+ "Print the installed coord-tui version next to this coordinator's "
66
+ "own version, and hint at `coord tui update` on skew. Purely local "
67
+ "-- reads `<dest> --version`, no network call -- so it's cheap "
68
+ "enough to run on demand without adding it to every `coord` "
69
+ "invocation's hot path."
70
+ ),
71
+ )
72
+ @click.option(
73
+ "--dest",
74
+ default=None,
75
+ help=f"Path to the coord-tui binary to check. Default: {DEFAULT_INSTALL_PATH}",
76
+ )
77
+ def tui_status(dest: str | None) -> None:
78
+ _print_skew_notice(_dest_path(dest))
79
+
80
+
81
+ def _print_skew_notice(binary_path: Path) -> bool:
82
+ """Print installed-vs-coordinator version lines. Returns True on skew
83
+ (including "not installed at all"), False when they match."""
84
+ if not binary_path.exists():
85
+ click.echo(f"coord-tui: not installed at {binary_path}")
86
+ click.echo(f"coord: {__version__}")
87
+ click.echo("Run `coord tui update` to install it.")
88
+ return True
89
+
90
+ installed = read_installed_version(binary_path)
91
+ if installed is None:
92
+ click.echo(f"coord-tui: {binary_path} did not report a parseable --version")
93
+ click.echo(f"coord: {__version__}")
94
+ click.echo("Run `coord tui update` to reinstall it.")
95
+ return True
96
+
97
+ click.echo(f"coord-tui: {installed} ({binary_path})")
98
+ click.echo(f"coord: {__version__}")
99
+ if installed != __version__:
100
+ click.echo(
101
+ "⚠ version skew — run `coord tui update` to match this "
102
+ "coordinator's version."
103
+ )
104
+ return True
105
+ click.echo("✓ up to date")
106
+ return False
107
+
108
+
109
+ @tui_group.command(
110
+ "update",
111
+ help=(
112
+ "Download the coord-tui binary matching this coordinator's own "
113
+ "version (coord --version) from its GitHub Release, and install "
114
+ "it.\n\n"
115
+ "Platform detection: maps this host's `platform.system()`/"
116
+ "`platform.machine()` to one of release-tui.yml's build-matrix "
117
+ "targets (x86_64-linux, x86_64-macos, aarch64-macos, "
118
+ "x86_64-windows) and downloads the matching `coord-tui-<target>` "
119
+ "asset from the GitHub Release tagged v<version>.\n\n"
120
+ f"Install path: {DEFAULT_INSTALL_PATH} by default, override with "
121
+ "--dest. The download always lands in a temp file next to the "
122
+ "destination first -- chmod +x'd and checksum-verified (when the "
123
+ "release publishes one) before an atomic rename into place -- so a "
124
+ "running coord-tui, or an interrupted download, never observes a "
125
+ "partial binary.\n\n"
126
+ "Dev-checkout guard: tui/Cargo.toml's committed [package] version "
127
+ f"is the {DEV_BUILD_SENTINEL_VERSION!r} placeholder that only "
128
+ "release-tui.yml's CI build stamps a real version over (never "
129
+ "committed back) -- so a plain local `cargo build` always reports "
130
+ f"exactly {DEV_BUILD_SENTINEL_VERSION!r}. If the binary already at "
131
+ "--dest reports that sentinel, this refuses to overwrite it "
132
+ "(assuming a developer is iterating on a local build) unless "
133
+ "--force is given."
134
+ ),
135
+ )
136
+ @click.option(
137
+ "--version",
138
+ "version_override",
139
+ default=None,
140
+ help="Install this exact version instead of this coordinator's own version (coord --version).",
141
+ )
142
+ @click.option(
143
+ "--dest",
144
+ default=None,
145
+ help=f"Where to install the binary. Default: {DEFAULT_INSTALL_PATH}",
146
+ )
147
+ @click.option(
148
+ "--repo",
149
+ default=DEFAULT_REPO,
150
+ show_default=True,
151
+ help="GitHub owner/repo the release lives on.",
152
+ )
153
+ @click.option(
154
+ "--api-base",
155
+ default=DEFAULT_API_BASE,
156
+ show_default=True,
157
+ help="GitHub API base URL -- override to point at a stub endpoint (tests only).",
158
+ )
159
+ @click.option(
160
+ "--timeout",
161
+ default=30.0,
162
+ show_default=True,
163
+ type=float,
164
+ help="Per-request network timeout (seconds), for both the release lookup and the download.",
165
+ )
166
+ @click.option(
167
+ "--force",
168
+ is_flag=True,
169
+ help=(
170
+ "Overwrite an installed dev build (see the dev-checkout guard "
171
+ "above), and reinstall even when the destination already reports "
172
+ "the target version."
173
+ ),
174
+ )
175
+ def tui_update(
176
+ version_override: str | None,
177
+ dest: str | None,
178
+ repo: str,
179
+ api_base: str,
180
+ timeout: float,
181
+ force: bool, # noqa: FBT001
182
+ ) -> None:
183
+ target_version = version_override or __version__
184
+ dest_path = _dest_path(dest)
185
+
186
+ try:
187
+ target = detect_target()
188
+ except UnsupportedPlatformError as exc:
189
+ click.echo(f"error: {exc}", err=True)
190
+ sys.exit(1)
191
+
192
+ if dest_path.exists() and not force:
193
+ installed = read_installed_version(dest_path)
194
+ if installed == DEV_BUILD_SENTINEL_VERSION:
195
+ click.echo(
196
+ f"refusing to overwrite {dest_path}: it reports version "
197
+ f"{DEV_BUILD_SENTINEL_VERSION!r}, the sentinel a locally "
198
+ "`cargo build`'d coord-tui always carries (tui/Cargo.toml's "
199
+ "committed version is only stamped for real in CI release "
200
+ "builds). This looks like a dev build someone is iterating "
201
+ "on -- pass --force to overwrite it anyway, or keep "
202
+ "building it yourself: cd tui && cargo build && cp "
203
+ f"target/debug/coord-tui {dest_path}",
204
+ err=True,
205
+ )
206
+ sys.exit(3)
207
+ if installed == target_version:
208
+ click.echo(
209
+ f"coord-tui is already v{target_version} at {dest_path} -- "
210
+ "nothing to do (--force to reinstall)."
211
+ )
212
+ return
213
+
214
+ click.echo(f"Detected platform target: {target}")
215
+ click.echo(f"Resolving coord-tui v{target_version} from {repo}...")
216
+ try:
217
+ assets = fetch_release_assets(
218
+ target_version, repo=repo, api_base=api_base, timeout=timeout
219
+ )
220
+ asset = find_asset(assets, target)
221
+ except ReleaseAssetNotFoundError as exc:
222
+ click.echo(f"error: {exc}", err=True)
223
+ sys.exit(1)
224
+ except Exception as exc: # noqa: BLE001 -- surface any network/HTTP failure plainly
225
+ click.echo(
226
+ f"error: could not resolve release v{target_version} from {repo}: {exc}",
227
+ err=True,
228
+ )
229
+ sys.exit(1)
230
+
231
+ checksum_asset = find_checksum_asset(assets, asset)
232
+
233
+ click.echo(f"Downloading {asset.name}...")
234
+ try:
235
+ tmp_path = download_asset(asset.download_url, dest_path.parent, timeout=timeout)
236
+ except Exception as exc: # noqa: BLE001
237
+ click.echo(f"error: download failed: {exc}", err=True)
238
+ sys.exit(1)
239
+
240
+ if checksum_asset is not None:
241
+ click.echo(f"Verifying checksum against {checksum_asset.name}...")
242
+ try:
243
+ checksum_tmp = download_asset(
244
+ checksum_asset.download_url, dest_path.parent, timeout=timeout
245
+ )
246
+ except Exception as exc: # noqa: BLE001
247
+ tmp_path.unlink(missing_ok=True)
248
+ click.echo(
249
+ f"error: could not download checksum {checksum_asset.name}: {exc}",
250
+ err=True,
251
+ )
252
+ sys.exit(1)
253
+ try:
254
+ expected = checksum_tmp.read_text().split()[0].strip()
255
+ finally:
256
+ checksum_tmp.unlink(missing_ok=True)
257
+ actual = sha256_file(tmp_path)
258
+ if actual != expected:
259
+ tmp_path.unlink(missing_ok=True)
260
+ click.echo(
261
+ f"error: checksum mismatch for {asset.name}: expected "
262
+ f"{expected}, got {actual}",
263
+ err=True,
264
+ )
265
+ sys.exit(1)
266
+ click.echo(f"Checksum OK ({actual}).")
267
+ else:
268
+ click.echo(
269
+ "(no published checksum asset for this binary -- PKG-3 does "
270
+ "not currently publish one, so skipping verification)"
271
+ )
272
+
273
+ try:
274
+ install_atomically(tmp_path, dest_path)
275
+ except Exception as exc: # noqa: BLE001
276
+ tmp_path.unlink(missing_ok=True)
277
+ click.echo(f"error: install failed: {exc}", err=True)
278
+ sys.exit(1)
279
+
280
+ installed_now = read_installed_version(dest_path)
281
+ click.echo(f"Installed {dest_path} -- coord-tui reports {installed_now or '?'}.")
282
+ if installed_now != target_version:
283
+ click.echo(
284
+ f"⚠ installed binary reports {installed_now!r}, expected "
285
+ f"{target_version!r} -- the release asset may be mislabeled.",
286
+ err=True,
287
+ )
288
+ sys.exit(1)