@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.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +19 -24
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +4 -4
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- 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({
|
|
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(
|
|
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({
|
|
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
|
});
|