@kabmo-labs/kabmo 0.8.0 → 0.9.0

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 (254) hide show
  1. package/README.md +7 -0
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/index.js +314 -314
  4. package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
  5. package/dist/client/static/js/index-k315p1e8.css +1 -0
  6. package/package.json +9 -1
  7. package/src/client/components/common/code-surface/documents.test.ts +120 -0
  8. package/src/client/components/common/code-surface/documents.ts +201 -0
  9. package/src/client/components/common/code-surface/extensions.test.ts +110 -0
  10. package/src/client/components/common/code-surface/extensions.ts +162 -0
  11. package/src/client/components/common/code-surface/handle.ts +102 -0
  12. package/src/client/components/common/code-surface/index.tsx +253 -314
  13. package/src/client/components/common/code-surface/keymap.test.ts +133 -0
  14. package/src/client/components/common/code-surface/keymap.ts +267 -0
  15. package/src/client/components/common/code-surface/marks.ts +74 -0
  16. package/src/client/components/common/code-surface/shiki.ts +234 -0
  17. package/src/client/components/common/code-surface/sync.test.ts +185 -0
  18. package/src/client/components/common/code-surface/sync.ts +58 -0
  19. package/src/client/components/common/code-surface/theme.test.ts +202 -0
  20. package/src/client/components/common/code-surface/theme.ts +111 -0
  21. package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
  22. package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
  23. package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
  24. package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
  25. package/src/client/components/common/shell-update-button/index.tsx +125 -0
  26. package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
  27. package/src/client/components/common/toolbar-row/index.tsx +189 -0
  28. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
  29. package/src/client/components/layout/desktop-layout/index.tsx +47 -67
  30. package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
  31. package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
  32. package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
  33. package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
  34. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
  35. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
  36. package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
  37. package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
  38. package/src/client/components/settings/Modal.tsx +8 -0
  39. package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
  40. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
  41. package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
  42. package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
  43. package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
  44. package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
  45. package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
  46. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
  47. package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
  48. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
  49. package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
  50. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
  51. package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
  52. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
  53. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
  54. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
  55. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
  56. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
  57. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
  58. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
  59. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
  60. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
  63. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
  64. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
  65. package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
  66. package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
  67. package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
  68. package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
  69. package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
  70. package/src/client/components/workspace/diff-panel/index.tsx +76 -145
  71. package/src/client/components/workspace/diff-panel/merge.ts +315 -0
  72. package/src/client/components/workspace/editor/Body.tsx +4 -7
  73. package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
  74. package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
  75. package/src/client/components/workspace/editor/FilePane.tsx +4 -7
  76. package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
  77. package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
  78. package/src/client/components/workspace/editor/index.tsx +7 -9
  79. package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
  80. package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
  81. package/src/client/components/workspace/search-panel/Body.tsx +1 -1
  82. package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
  83. package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
  84. package/src/client/hooks/chat/omp/reconnect.ts +187 -0
  85. package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
  86. package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
  87. package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
  88. package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
  89. package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
  90. package/src/client/hooks/chat/omp/session-list.tsx +69 -9
  91. package/src/client/hooks/chat/timeline/generating.ts +19 -2
  92. package/src/client/hooks/chat/timeline/index.ts +19 -24
  93. package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
  94. package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
  95. package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
  96. package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
  97. package/src/client/hooks/chat/timeline/session-load.ts +69 -37
  98. package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
  99. package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
  100. package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
  101. package/src/client/hooks/code/deferred-highlight.ts +129 -0
  102. package/src/client/hooks/editor/find-state.ts +1 -1
  103. package/src/client/hooks/editor/theme.test.ts +206 -0
  104. package/src/client/hooks/editor/theme.ts +82 -0
  105. package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
  106. package/src/client/hooks/editor/use-editor-drop.ts +67 -14
  107. package/src/client/hooks/editor/use-editor-find.ts +7 -7
  108. package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
  109. package/src/client/hooks/editor/use-outline-view.ts +94 -7
  110. package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
  111. package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
  112. package/src/client/hooks/editor/use-search-reveal.ts +1 -1
  113. package/src/client/hooks/ui/settings-modal.ts +56 -0
  114. package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
  115. package/src/client/hooks/ui/updates/shell-download.ts +55 -14
  116. package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
  117. package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
  118. package/src/client/hooks/ui/updates/shell-update.ts +119 -0
  119. package/src/client/hooks/workspace/diff-file.ts +29 -10
  120. package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
  121. package/src/client/hooks/workspace/editor-view.ts +61 -0
  122. package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
  123. package/src/client/hooks/workspace/file-tabs.ts +24 -11
  124. package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
  125. package/src/client/hooks/workspace/worktree/index.ts +20 -5
  126. package/src/client/lib/desktop/bridge.ts +45 -0
  127. package/src/client/lib/search-reveal.ts +1 -1
  128. package/src/client/main.tsx +5 -1
  129. package/src/client/tailwind.css +37 -8
  130. package/src/server/lib/auth/guard.test.ts +28 -0
  131. package/src/server/lib/auth/guard.ts +10 -0
  132. package/src/server/lib/fs/worktree/create.ts +2 -3
  133. package/src/server/lib/fs/worktree/list.ts +13 -0
  134. package/src/server/lib/fs/worktree/remove.ts +3 -3
  135. package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
  136. package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
  137. package/src/server/lib/lifecycle/port-guard.ts +39 -7
  138. package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
  139. package/src/server/lib/lifecycle/probe.test.ts +77 -164
  140. package/src/server/lib/lifecycle/probe.ts +121 -13
  141. package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
  142. package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
  143. package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
  144. package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
  145. package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
  146. package/src/server/lib/omp/session/telemetry.test.ts +33 -0
  147. package/src/server/lib/realtime/signals.server.ts +10 -0
  148. package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
  149. package/src/server/lib/realtime/topics.server.ts +16 -0
  150. package/src/server/lib/updates/shell-assets.ts +94 -3
  151. package/src/server/lib/updates/shell-cache.ts +105 -0
  152. package/src/server/lib/updates/shell-pages.test.ts +33 -2
  153. package/src/server/lib/updates/shell-pages.ts +17 -5
  154. package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
  155. package/src/server/lib/updates/shell-release.test.ts +10 -27
  156. package/src/server/lib/updates/shell-release.ts +107 -111
  157. package/src/server/routes/fs/git-merge.test.ts +11 -1
  158. package/src/server/routes/fs/read.test.ts +24 -7
  159. package/src/server/routes/fs/scope.test.ts +15 -10
  160. package/src/server/routes/fs/scope.ts +3 -14
  161. package/src/server/routes/updates/android.ts +6 -1
  162. package/src/server/routes/updates/desktop.ts +29 -7
  163. package/src/server/routes/updates/ios.ts +5 -1
  164. package/src/server/routes/worktrees/list.ts +11 -1
  165. package/src/server/routes/worktrees/routes.test.ts +17 -0
  166. package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
  167. package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
  168. package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
  169. package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
  170. package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
  171. package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
  172. package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
  173. package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
  174. package/src/shared/lib/code/editor/find-scan.ts +177 -0
  175. package/src/shared/lib/code/editor/keymap.ts +3 -3
  176. package/src/shared/lib/code/editor/lines.test.ts +7 -55
  177. package/src/shared/lib/code/editor/lines.ts +8 -48
  178. package/src/shared/lib/code/editor/marks.test.ts +4 -5
  179. package/src/shared/lib/code/editor/marks.ts +13 -25
  180. package/src/shared/lib/code/editor/theme-loader.ts +97 -0
  181. package/src/shared/lib/code/editor/themes.generated.ts +156 -0
  182. package/src/shared/lib/code/editor/themes.test.ts +165 -0
  183. package/src/shared/lib/code/editor/themes.ts +165 -0
  184. package/src/shared/lib/code/highlight-memo.test.ts +103 -0
  185. package/src/shared/lib/code/highlight-memo.ts +149 -0
  186. package/src/shared/lib/code/highlighter-warmup.ts +112 -0
  187. package/src/shared/lib/code/highlighter.ts +3 -1
  188. package/src/shared/lib/code/idle.ts +37 -0
  189. package/src/shared/lib/code/outline/rows.test.ts +96 -0
  190. package/src/shared/lib/code/outline/rows.ts +94 -0
  191. package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
  192. package/src/shared/lib/code/syntax-highlight.ts +2 -3
  193. package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
  194. package/src/shared/lib/fs/file-kind.ts +47 -13
  195. package/src/shared/lib/fs/file-url.test.ts +52 -0
  196. package/src/shared/lib/fs/file-url.ts +37 -0
  197. package/src/shared/lib/fs/patch-sides.test.ts +114 -0
  198. package/src/shared/lib/fs/patch-sides.ts +139 -0
  199. package/src/shared/lib/realtime/protocol.ts +16 -0
  200. package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
  201. package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
  202. package/src/shared/lib/updates/shell-arch.ts +54 -0
  203. package/src/shared/lib/updates/shell-ua.ts +4 -4
  204. package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
  205. package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
  206. package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
  207. package/src/shared/lib/workspace/panel-widths.ts +25 -5
  208. package/src/shared/lib/workspace/session-state/cache.ts +159 -0
  209. package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
  210. package/src/shared/lib/workspace/session-state/merge.ts +100 -0
  211. package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
  212. package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
  213. package/src/shared/lib/workspace/session-state/store.ts +92 -130
  214. package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
  215. package/src/shared/types/realtime.ts +29 -0
  216. package/src/shared/types/settings/state.ts +10 -0
  217. package/src/test-support/loopback.ts +183 -0
  218. package/dist/client/static/js/index-gtbpewr7.css +0 -1
  219. package/src/client/components/common/code-editor/handler.test.ts +0 -274
  220. package/src/client/components/common/code-editor/handler.ts +0 -175
  221. package/src/client/components/common/code-editor/index.tsx +0 -314
  222. package/src/client/components/common/code-surface/measure.ts +0 -148
  223. package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
  224. package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
  225. package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
  226. package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
  227. package/src/client/hooks/editor/scroll-container.test.ts +0 -102
  228. package/src/client/hooks/editor/scroll-container.ts +0 -26
  229. package/src/client/hooks/editor/use-code-window.ts +0 -100
  230. package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
  231. package/src/client/hooks/editor/use-editor-history.ts +0 -134
  232. package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
  233. package/src/client/hooks/editor/use-occurrences.ts +0 -98
  234. package/src/shared/lib/code/editor/commands.test.ts +0 -155
  235. package/src/shared/lib/code/editor/commands.ts +0 -260
  236. package/src/shared/lib/code/editor/edits.test.ts +0 -100
  237. package/src/shared/lib/code/editor/edits.ts +0 -122
  238. package/src/shared/lib/code/editor/history.test.ts +0 -118
  239. package/src/shared/lib/code/editor/history.ts +0 -101
  240. package/src/shared/lib/code/editor/layers.test.ts +0 -77
  241. package/src/shared/lib/code/editor/layers.ts +0 -67
  242. package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
  243. package/src/shared/lib/code/editor/line-edits.ts +0 -159
  244. package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
  245. package/src/shared/lib/code/editor/occurrences.ts +0 -111
  246. package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
  247. package/src/shared/lib/code/editor/reset-css.ts +0 -32
  248. package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
  249. package/src/shared/lib/code/editor/text-commands.ts +0 -154
  250. package/src/shared/lib/code/lazy-window.test.ts +0 -161
  251. package/src/shared/lib/code/lazy-window.ts +0 -156
  252. package/src/shared/lib/code/windowed-highlight.ts +0 -114
  253. package/src/shared/lib/fs/diff-parser.test.ts +0 -116
  254. package/src/shared/lib/fs/diff-parser.ts +0 -213
@@ -89,6 +89,7 @@ describe('the fallback when the API is unavailable', () => {
89
89
  url: 'https://github.com/kabmo-labs/kabmo/releases/download/v0.7.0/Kabmo-0.6.0-android.apk',
90
90
  latest: '0.6.0',
91
91
  releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.7.0',
92
+ direct: true,
92
93
  });
93
94
  expect(calls.some((url) => url.includes('/repos/'))).toBe(true);
94
95
  expect(calls.some((url) => url.endsWith('/releases.atom'))).toBe(true);
@@ -106,13 +107,38 @@ describe('the fallback when the API is unavailable', () => {
106
107
  expect((await fetchShellRelease('android', impl))?.latest).toBe('0.6.0');
107
108
  });
108
109
 
109
- test('answers the release page for a desktop shell', async () => {
110
+ test('answers the release page for a desktop shell with no arch', async () => {
110
111
  const { impl } = routedFetch({ atom: ATOM, assets: { 'v0.7.0': ASSETS_V070 } });
111
112
 
112
113
  expect(await fetchShellRelease('macos', impl)).toEqual({
113
114
  url: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.7.0',
114
115
  latest: '0.2.0',
115
116
  releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.7.0',
117
+ direct: false,
118
+ });
119
+ });
120
+
121
+ test('answers that arch’s own artifact when the fragment lists it', async () => {
122
+ // The pages fallback carries the same arch rule as the API read: a desktop
123
+ // shell that named its machine gets that machine's installer, and a build it
124
+ // does not publish is never substituted.
125
+ const { impl } = routedFetch({ atom: ATOM, assets: { 'v0.7.0': ASSETS_V070 } });
126
+
127
+ expect(await fetchShellRelease('macos', impl, 'arm64')).toEqual({
128
+ url: 'https://github.com/kabmo-labs/kabmo/releases/download/v0.7.0/Kabmo-0.2.0-mac-arm64.zip',
129
+ latest: '0.2.0',
130
+ releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.7.0',
131
+ direct: true,
132
+ });
133
+
134
+ // x64 is not in the fragment, so the page is the answer rather than the
135
+ // arm64 file.
136
+ const again = routedFetch({ atom: ATOM, assets: { 'v0.7.0': ASSETS_V070 } });
137
+ expect(await fetchShellRelease('macos', again.impl, 'x64')).toEqual({
138
+ url: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.7.0',
139
+ latest: '0.2.0',
140
+ releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v0.7.0',
141
+ direct: false,
116
142
  });
117
143
  });
118
144
 
@@ -121,7 +147,12 @@ describe('the fallback when the API is unavailable', () => {
121
147
  // distinguishable: the first is cacheable and the second keeps the last good
122
148
  // answer.
123
149
  const { impl } = routedFetch({ atom: ATOM, assets: { 'v0.7.0': '<div></div>' } });
124
- expect(await fetchShellRelease('android', impl)).toEqual({ url: null, latest: null, releaseUrl: null });
150
+ expect(await fetchShellRelease('android', impl)).toEqual({
151
+ url: null,
152
+ latest: null,
153
+ releaseUrl: null,
154
+ direct: false,
155
+ });
125
156
  });
126
157
 
127
158
  test('an unreachable feed answers null rather than an empty result', async () => {
@@ -21,7 +21,8 @@
21
21
  */
22
22
 
23
23
  import { REPO } from '@/server/lib/updates/release-url';
24
- import { SHELL_ASSETS, versionFromAssetName } from '@/server/lib/updates/shell-assets';
24
+ import { SHELL_ASSETS, selfUpdateArtifact, versionFromAssetName } from '@/server/lib/updates/shell-assets';
25
+ import type { ShellArch } from '@/shared/lib/updates/shell-arch';
25
26
  import type { ShellKind } from '@/shared/lib/updates/shell-ua';
26
27
 
27
28
  /** How many tags to walk before giving up. */
@@ -64,6 +65,7 @@ export async function findAssetInPages(
64
65
  kind: ShellKind,
65
66
  fetchImpl: typeof fetch,
66
67
  headers: Record<string, string>,
68
+ arch: ShellArch | null = null,
67
69
  ): Promise<PagesResult> {
68
70
  const feed = await fetchImpl(`https://github.com/${REPO}/releases.atom`, {
69
71
  headers: { accept: 'application/atom+xml, text/xml', ...headers },
@@ -80,7 +82,7 @@ export async function findAssetInPages(
80
82
  );
81
83
  if (!page.ok) continue;
82
84
 
83
- const asset = shellAssetFromPage(kind, await page.text());
85
+ const asset = shellAssetFromPage(kind, await page.text(), arch);
84
86
  if (!asset) continue;
85
87
 
86
88
  return { kind: 'found', asset, releaseUrl: tag.url };
@@ -121,19 +123,29 @@ export function releaseTags(xml: string): ReleaseTag[] {
121
123
  * `shell-release`'s API path documents: a release carrying both spellings must
122
124
  * not resolve to the older version.
123
125
  */
124
- export function shellAssetFromPage(kind: ShellKind, html: string): PageAsset | null {
126
+ export function shellAssetFromPage(kind: ShellKind, html: string, arch: ShellArch | null = null): PageAsset | null {
125
127
  const found: PageAsset[] = [];
126
- const pattern = /href="(\/[^"]+\/releases\/download\/[^"]+)"/g;
127
128
 
128
- for (const match of html.matchAll(pattern)) {
129
+ for (const match of html.matchAll(ASSET_HREF)) {
129
130
  const path = decodeEntities(match[1] ?? '');
130
131
  const name = decodeURIComponent(path.slice(path.lastIndexOf('/') + 1));
131
132
  if (versionFromAssetName(kind, name) !== null) found.push({ name, downloadUrl: `https://github.com${path}` });
132
133
  }
133
134
 
135
+ // The architecture decides FIRST, for the reason the API path documents: a
136
+ // desktop shell that named its own machine must not be handed the other
137
+ // architecture's installer just because the fragment lists it first.
138
+ if (arch) {
139
+ const exact = found.find((asset) => selfUpdateArtifact(kind, arch, asset.name) !== null);
140
+ if (exact) return exact;
141
+ }
142
+
134
143
  return found.find((asset) => SHELL_ASSETS[kind].current.test(asset.name)) ?? found[0] ?? null;
135
144
  }
136
145
 
146
+ /** One asset link in a release fragment. Stated once: the loop and the regex must agree. */
147
+ const ASSET_HREF = /href="(\/[^"]+\/releases\/download\/[^"]+)"/g;
148
+
137
149
  /** The entities that appear in a feed title or a link. */
138
150
  function decodeEntities(text: string): string {
139
151
  return text
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The DESKTOP half of the shell-release read: which artifact a per-architecture
8
+ * shell installs, and when the answer is only the release page.
9
+ *
10
+ * Split from `shell-release.test.ts` because that file is at the repo's 350-line
11
+ * ceiling, and the two questions are separable: the mobile pair resolves ONE
12
+ * artifact for every device, while the desktop trio's answer depends on the
13
+ * architecture the shell reported.
14
+ */
15
+
16
+ import { beforeEach, describe, expect, test } from 'bun:test';
17
+
18
+ import { fetchShellRelease, resetShellReleaseCache } from '@/server/lib/updates/shell-release';
19
+
20
+ /** A fetch double that answers one body. */
21
+ function stubFetch(body: unknown) {
22
+ const calls: string[] = [];
23
+ const impl = (async (input: string | URL | Request) => {
24
+ calls.push(String(input));
25
+ return {
26
+ ok: true,
27
+ json: async () => body,
28
+ text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
29
+ } as Response;
30
+ }) as unknown as typeof fetch;
31
+ return { impl, calls };
32
+ }
33
+
34
+ describe('fetchShellRelease — desktop architectures', () => {
35
+ // The cache is process-wide (see `resetShellReleaseCache`), so every case
36
+ // starts from a cold one or it reads the previous case's answer.
37
+ beforeEach(() => resetShellReleaseCache());
38
+
39
+ /**
40
+ * A desktop probe WITHOUT an architecture answers the RELEASE PAGE.
41
+ *
42
+ * Every desktop artifact is per-architecture, and a console served to another
43
+ * machine cannot know which one the window asking runs on. Naming one arch's
44
+ * file would hand half the readers a build that will not launch, so the link is
45
+ * the page that lists all of them and `direct` says it is not installable.
46
+ */
47
+ test('a desktop probe without an arch answers the release page', async () => {
48
+ const { impl } = stubFetch([
49
+ {
50
+ html_url: 'https://x/app',
51
+ assets: [
52
+ { name: 'Kabmo-0.2.0-mac-arm64.zip', browser_download_url: 'https://x/mac-arm.zip' },
53
+ { name: 'Kabmo-0.2.0-mac-x64.dmg', browser_download_url: 'https://x/mac-x64.dmg' },
54
+ ],
55
+ },
56
+ ]);
57
+
58
+ expect(await fetchShellRelease('macos', impl)).toEqual({
59
+ url: 'https://x/app',
60
+ latest: '0.2.0',
61
+ releaseUrl: 'https://x/app',
62
+ direct: false,
63
+ });
64
+ });
65
+
66
+ /**
67
+ * WITH an architecture it answers that machine's own installer.
68
+ *
69
+ * This is what makes the desktop self-update possible at all: the shell
70
+ * reports its arch over the bridge, and the console names ONE file instead of
71
+ * a page a human has to read.
72
+ */
73
+ test('a desktop probe with an arch answers that arch’s installer', async () => {
74
+ const { impl } = stubFetch([
75
+ {
76
+ html_url: 'https://x/app',
77
+ assets: [
78
+ { name: 'Kabmo-0.2.0-mac-x64.zip', browser_download_url: 'https://x/mac-x64.zip' },
79
+ { name: 'Kabmo-0.2.0-mac-arm64.zip', browser_download_url: 'https://x/mac-arm.zip' },
80
+ ],
81
+ },
82
+ ]);
83
+
84
+ expect(await fetchShellRelease('macos', impl, 'arm64')).toEqual({
85
+ url: 'https://x/mac-arm.zip',
86
+ latest: '0.2.0',
87
+ releaseUrl: 'https://x/app',
88
+ direct: true,
89
+ });
90
+ });
91
+
92
+ test('an arch the release does not publish keeps the page, never the sibling', async () => {
93
+ // A build that only shipped x64 must not be installed on an arm64 machine:
94
+ // the page is a correct answer, the other architecture's file is not.
95
+ const { impl } = stubFetch([
96
+ {
97
+ html_url: 'https://x/app',
98
+ assets: [
99
+ { name: 'Kabmo-0.2.0-mac-x64.zip', browser_download_url: 'https://x/mac-x64.zip' },
100
+ ],
101
+ },
102
+ ]);
103
+
104
+ expect(await fetchShellRelease('macos', impl, 'arm64')).toEqual({
105
+ url: 'https://x/app',
106
+ latest: '0.2.0',
107
+ releaseUrl: 'https://x/app',
108
+ direct: false,
109
+ });
110
+ });
111
+
112
+ test('the self-update file is the ONE file that shell can install', async () => {
113
+ // macOS installs from the `.zip` (ditto extracts it); the `.dmg` beside it is
114
+ // for a human. Windows installs from the Inno `.exe`; Linux from the
115
+ // AppImage, which is the only one of its four formats a shell can replace.
116
+ const mac = stubFetch([
117
+ {
118
+ html_url: 'https://x/app',
119
+ assets: [
120
+ { name: 'Kabmo-0.2.0-mac-arm64.dmg', browser_download_url: 'https://x/mac-arm.dmg' },
121
+ { name: 'Kabmo-0.2.0-mac-arm64.zip', browser_download_url: 'https://x/mac-arm.zip' },
122
+ ],
123
+ },
124
+ ]);
125
+ expect((await fetchShellRelease('macos', mac.impl, 'arm64'))?.url).toBe('https://x/mac-arm.zip');
126
+
127
+ const win = stubFetch([
128
+ {
129
+ html_url: 'https://x/app',
130
+ assets: [
131
+ { name: 'Kabmo-0.2.0-win-x64-setup.exe', browser_download_url: 'https://x/win-x64.exe' },
132
+ ],
133
+ },
134
+ ]);
135
+ expect((await fetchShellRelease('windows', win.impl, 'x64'))?.url).toBe('https://x/win-x64.exe');
136
+
137
+ const linux = stubFetch([
138
+ {
139
+ html_url: 'https://x/app',
140
+ assets: [
141
+ { name: 'Kabmo-0.2.0-linux-x86_64.tar.gz', browser_download_url: 'https://x/linux.tgz' },
142
+ { name: 'Kabmo-0.2.0-linux-x86_64.AppImage', browser_download_url: 'https://x/linux.AppImage' },
143
+ ],
144
+ },
145
+ ]);
146
+ expect((await fetchShellRelease('linux', linux.impl, 'x64'))?.url).toBe('https://x/linux.AppImage');
147
+ });
148
+
149
+ test('the arch is part of the cache key', async () => {
150
+ // One console is routinely shared by a whole team, and two machines on it
151
+ // differ only by architecture. A shared slot would answer the second one with
152
+ // the first one's installer.
153
+ const release = {
154
+ html_url: 'https://x/app',
155
+ assets: [
156
+ { name: 'Kabmo-0.3.0-mac-arm64.zip', browser_download_url: 'https://x/arm.zip' },
157
+ { name: 'Kabmo-0.3.0-mac-x64.zip', browser_download_url: 'https://x/x64.zip' },
158
+ ],
159
+ };
160
+ expect((await fetchShellRelease('macos', stubFetch([release]).impl, 'arm64'))?.url).toBe('https://x/arm.zip');
161
+
162
+ const { impl, calls } = stubFetch([release]);
163
+ expect((await fetchShellRelease('macos', impl, 'x64'))?.url).toBe('https://x/x64.zip');
164
+ expect(calls.length, 'a different arch is a different question').toBeGreaterThan(0);
165
+ });
166
+ });
@@ -84,6 +84,7 @@ describe('fetchShellRelease', () => {
84
84
  url: 'https://x/Kabmo-1.2.3-android.apk',
85
85
  latest: '1.2.3',
86
86
  releaseUrl: 'https://github.com/kabmo-labs/kabmo/releases/tag/v9.9.9',
87
+ direct: true,
87
88
  });
88
89
  });
89
90
 
@@ -102,6 +103,7 @@ describe('fetchShellRelease', () => {
102
103
  url: 'https://x/i.ipa',
103
104
  latest: '0.4.0',
104
105
  releaseUrl: 'https://x/app',
106
+ direct: true,
105
107
  });
106
108
  });
107
109
 
@@ -130,6 +132,7 @@ describe('fetchShellRelease', () => {
130
132
  url: 'https://x/new.apk',
131
133
  latest: '0.6.0',
132
134
  releaseUrl: 'https://x/r',
135
+ direct: true,
133
136
  });
134
137
  });
135
138
 
@@ -183,6 +186,7 @@ describe('fetchShellRelease', () => {
183
186
  url: 'https://x/a.apk',
184
187
  latest: '0.3.0',
185
188
  releaseUrl: 'https://x/app',
189
+ direct: true,
186
190
  });
187
191
  });
188
192
 
@@ -212,7 +216,12 @@ describe('fetchShellRelease', () => {
212
216
  { html_url: 'https://x', assets: [{ name: 'notes.md' }] },
213
217
  { html_url: 'https://y', assets: [] },
214
218
  ]);
215
- expect(await fetchShellRelease('android', impl)).toEqual({ url: null, latest: null, releaseUrl: null });
219
+ expect(await fetchShellRelease('android', impl)).toEqual({
220
+ url: null,
221
+ latest: null,
222
+ releaseUrl: null,
223
+ direct: false,
224
+ });
216
225
  });
217
226
 
218
227
  test('answers null when the probe fails', async () => {
@@ -231,30 +240,4 @@ describe('fetchShellRelease', () => {
231
240
  const { impl } = routedFetch({ api: { assets: 'nope' } });
232
241
  expect(await fetchShellRelease('android', impl)).toBeNull();
233
242
  });
234
-
235
- /**
236
- * The desktop trio reads the same release list and answers the RELEASE PAGE.
237
- *
238
- * Every desktop artifact is per-architecture, and this server cannot know which
239
- * architecture the window asking is running on — it may be a remote console on
240
- * another machine. Naming one arch's file would hand half the readers a build
241
- * that will not launch, so the link is the page that lists all of them.
242
- */
243
- test('a desktop probe answers the release page, never one architecture', async () => {
244
- const { impl } = stubFetch([
245
- {
246
- html_url: 'https://x/app',
247
- assets: [
248
- { name: 'Kabmo-0.2.0-mac-arm64.zip', browser_download_url: 'https://x/mac-arm.zip' },
249
- { name: 'Kabmo-0.2.0-mac-x64.dmg', browser_download_url: 'https://x/mac-x64.dmg' },
250
- ],
251
- },
252
- ]);
253
-
254
- expect(await fetchShellRelease('macos', impl)).toEqual({
255
- url: 'https://x/app',
256
- latest: '0.2.0',
257
- releaseUrl: 'https://x/app',
258
- });
259
- });
260
243
  });