@kabmo-labs/kabmo 0.7.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 +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -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/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -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/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- 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/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- 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/ProjectSettings.tsx +22 -8
- 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 +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- 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/index.tsx +5 -0
- 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.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- 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/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- 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/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- 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/components/workspace/worktree-modal/index.tsx +261 -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 +27 -28
- 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/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- 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/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- 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/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -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-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- 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/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- 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 +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- 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/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- 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/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- 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.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- 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/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -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-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- 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/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -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 +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- 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-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.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/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
|
@@ -1,208 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Which published native-shell artifact a browser or an installed shell should
|
|
3
8
|
* download, and whether the installed one is behind.
|
|
4
9
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* No shell is an npm package (see `.github/workflows/publish-{android,ios,macos,
|
|
11
|
+
* windows,linux}.yml`), so the npm probe that answers for the app and for `omp`
|
|
12
|
+
* cannot answer here. The only record of a published shell is a GitHub release
|
|
13
|
+
* asset.
|
|
14
|
+
*
|
|
15
|
+
* ## Two reads, because one of them has a quota
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* The REST API is the precise read and the FIRST one tried. It is also
|
|
18
|
+
* unauthenticated at 60 requests an hour **per source IP**, and a phone on mobile
|
|
19
|
+
* data shares that IP with an entire carrier NAT — so `shell-pages` is the second
|
|
20
|
+
* path, and it is why the button appears at all on a bad day. See that module's
|
|
21
|
+
* own comment for the measured failure.
|
|
15
22
|
*
|
|
16
|
-
*
|
|
23
|
+
* ## What travels back
|
|
17
24
|
*
|
|
18
|
-
* - `url` is
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* - `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* - `url` is what to download. For the mobile pair it is the artifact itself (an
|
|
26
|
+
* APK the shell installs, an IPA the release page explains). For the DESKTOP
|
|
27
|
+
* trio it is the artifact when the caller reported its ARCHITECTURE, and the
|
|
28
|
+
* release page otherwise: every desktop artifact is per-architecture, and a
|
|
29
|
+
* console served to another machine cannot guess. Naming the wrong arch's file
|
|
30
|
+
* would hand half the readers a build that will not launch.
|
|
31
|
+
* - `direct` says which of the two `url` is, so the client knows whether the
|
|
32
|
+
* offer can be installed from inside the shell.
|
|
33
|
+
* - `latest` is the version that asset ships, read from the asset NAME (see
|
|
34
|
+
* `shell-assets`). The release TAG cannot be used: the artifact is attached to
|
|
35
|
+
* the app's release, whose tag names the SERVER version.
|
|
27
36
|
*
|
|
28
|
-
* Failure is a normal answer here (`null`), never a throw: an offline box
|
|
29
|
-
*
|
|
30
|
-
*
|
|
37
|
+
* Failure is a normal answer here (`null`), never a throw: an offline box must
|
|
38
|
+
* not take the sidebar down, and a button that appears because a probe failed
|
|
39
|
+
* would offer a download that does not exist.
|
|
31
40
|
*/
|
|
32
41
|
|
|
33
42
|
import { REPO } from '@/server/lib/updates/release-url';
|
|
43
|
+
import {
|
|
44
|
+
cacheSlot,
|
|
45
|
+
CACHE_MS,
|
|
46
|
+
githubApiBase,
|
|
47
|
+
gitHubHeaders,
|
|
48
|
+
shellCacheKey,
|
|
49
|
+
} from '@/server/lib/updates/shell-cache';
|
|
50
|
+
import { SHELL_ASSETS, selfUpdateArtifact, versionFromAssetName } from '@/server/lib/updates/shell-assets';
|
|
51
|
+
import { findAssetInPages } from '@/server/lib/updates/shell-pages';
|
|
52
|
+
import type { ShellArch } from '@/shared/lib/updates/shell-arch';
|
|
34
53
|
import type { ShellKind } from '@/shared/lib/updates/shell-ua';
|
|
35
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The test seam for the process-wide answer cache, re-exported here because this
|
|
57
|
+
* module is what owns the answers. The cache itself lives next door (`shell-cache`)
|
|
58
|
+
* so its key — shell AND architecture — is stated in one place.
|
|
59
|
+
*/
|
|
60
|
+
export { resetShellReleaseCache } from '@/server/lib/updates/shell-cache';
|
|
61
|
+
|
|
36
62
|
/** One published shell artifact, as the console needs it. */
|
|
37
63
|
export interface ShellRelease {
|
|
38
|
-
/** Asset download URL, or
|
|
64
|
+
/** Asset download URL, or the release page for a per-arch desktop build. */
|
|
39
65
|
url: string | null;
|
|
40
|
-
/** Version the asset name declares (`
|
|
66
|
+
/** Version the asset name declares (`Kabmo-1.2.3-android.apk` → `1.2.3`). */
|
|
41
67
|
latest: string | null;
|
|
42
68
|
/** The release page, for a "what changed" link. */
|
|
43
69
|
releaseUrl: string | null;
|
|
70
|
+
/**
|
|
71
|
+
* True when `url` names the artifact itself rather than the release page.
|
|
72
|
+
*
|
|
73
|
+
* This is what tells the console whether the offer can be INSTALLED from
|
|
74
|
+
* inside the shell. It is true for the mobile pair (one file for every
|
|
75
|
+
* device) and for a desktop shell that reported its architecture — and false
|
|
76
|
+
* for a desktop probe without one, which still gets the page that lists every
|
|
77
|
+
* architecture.
|
|
78
|
+
*/
|
|
79
|
+
direct: boolean;
|
|
44
80
|
}
|
|
45
81
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
* carried BEFORE the rename.
|
|
49
|
-
*
|
|
50
|
-
* ONE scheme for every shell in the project — `<App>-<version>-<platform>.<ext>`:
|
|
51
|
-
* `Kabmo-0.5.0-android.apk`, `Kabmo-0.1.0-ios.ipa`, and the desktop trio's
|
|
52
|
-
* `Kabmo-0.1.2-mac-arm64.dmg` / `-win-x64-setup.exe` / `-linux-x86_64.tar.gz`.
|
|
53
|
-
* The mobile pair carries no architecture because neither artifact is per-arch:
|
|
54
|
-
* the APK is one universal build (the shell is pure Kotlin with no native
|
|
55
|
-
* libraries, so there is nothing to split), and the IPA contains only the device
|
|
56
|
-
* slice (`arm64-apple-ios`). A `-arm64` in those names would claim a distinction
|
|
57
|
-
* that does not exist and would make the parser look for a second artifact.
|
|
58
|
-
*
|
|
59
|
-
* **`legacyPrefix` is not dead code.** These names are PARSED to answer "is a
|
|
60
|
-
* newer shell published", and every release already on GitHub carries the old
|
|
61
|
-
* spelling. Dropping it would make the console answer "no update" for an
|
|
62
|
-
* installed shell the moment this ships — silently, because a name that does not
|
|
63
|
-
* match reads exactly like a release with no artifact. The probe tries the
|
|
64
|
-
* current prefix first, then the old one; new releases only ever write the
|
|
65
|
-
* current one.
|
|
66
|
-
*/
|
|
67
|
-
export const SHELL_ASSETS: Record<
|
|
68
|
-
ShellKind,
|
|
69
|
-
{ prefix: string; extension: string; legacyPrefix: string; legacyExtension: string }
|
|
70
|
-
> = {
|
|
71
|
-
android: {
|
|
72
|
-
prefix: 'Kabmo-',
|
|
73
|
-
extension: '-android.apk',
|
|
74
|
-
legacyPrefix: 'kabmo-android-v',
|
|
75
|
-
legacyExtension: '.apk',
|
|
76
|
-
},
|
|
77
|
-
ios: { prefix: 'Kabmo-', extension: '-ios.ipa', legacyPrefix: 'kabmo-ios-v', legacyExtension: '.ipa' },
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* `Kabmo-0.5.0-android.apk` → `0.5.0` (and the `.ipa` twin), plus the legacy
|
|
82
|
-
* `kabmo-android-v0.5.0.apk` spelling every published release still carries.
|
|
83
|
-
*
|
|
84
|
-
* The version is the MIDDLE of the name in the current scheme and the TAIL of the
|
|
85
|
-
* legacy one, so the two are parsed by different rules rather than by one that
|
|
86
|
-
* tries to cover both.
|
|
87
|
-
*/
|
|
88
|
-
export function versionFromAssetName(kind: ShellKind, name: string): string | null {
|
|
89
|
-
const { prefix, extension, legacyPrefix, legacyExtension } = SHELL_ASSETS[kind];
|
|
90
|
-
|
|
91
|
-
// Current: `<prefix><version><extension>`, where the prefix is the app's name
|
|
92
|
-
// and the extension names the platform.
|
|
93
|
-
if (name.startsWith(prefix) && name.endsWith(extension)) {
|
|
94
|
-
const middle = name.slice(prefix.length, -extension.length);
|
|
95
|
-
if (/^\d+\.\d+\.\d+$/.test(middle)) return middle;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Legacy: `kabmo-android-v<version>.apk`. Kept for the releases already
|
|
99
|
-
// published; no new release writes it.
|
|
100
|
-
if (name.startsWith(legacyPrefix) && name.endsWith(legacyExtension)) {
|
|
101
|
-
const middle = name.slice(legacyPrefix.length, -legacyExtension.length);
|
|
102
|
-
const version = middle.startsWith('v') ? middle.slice(1) : middle;
|
|
103
|
-
return /^\d+\.\d+\.\d+$/.test(version) ? version : null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
82
|
+
/** The answer a probe gives when it found nothing: a valid result, not an error. */
|
|
83
|
+
const EMPTY_RELEASE: ShellRelease = { url: null, latest: null, releaseUrl: null, direct: false };
|
|
108
84
|
|
|
109
85
|
/**
|
|
110
86
|
* The newest release that actually carries this shell's artifact.
|
|
111
87
|
*
|
|
112
88
|
* NOT `/releases/latest`, which answers the newest release of ANY kind. This
|
|
113
|
-
* repository publishes
|
|
114
|
-
* released FIRST (`packages → marketplace → android →
|
|
89
|
+
* repository publishes several artifacts from one push, and the packages are
|
|
90
|
+
* released FIRST (`packages → marketplace → android → … → app` in
|
|
115
91
|
* `release.yml`), so their `kabmo-sdk/v*` / `kabmo-ui/v*` releases are newer
|
|
116
|
-
* than the app release that carries the
|
|
92
|
+
* than the app release that carries the shells. `@semantic-release/github` runs
|
|
117
93
|
* in both pipelines, so those are real releases — measured against a stub that
|
|
118
|
-
* answered a package release: `/releases/latest` produced
|
|
119
|
-
*
|
|
120
|
-
*
|
|
94
|
+
* answered a package release: `/releases/latest` produced `{url: null, …}` and
|
|
95
|
+
* the sidebar's button disappeared even though the APK sat on the release
|
|
96
|
+
* directly below.
|
|
97
|
+
*
|
|
98
|
+
* `arch` is what a DESKTOP shell reports about its own machine, and it is the
|
|
99
|
+
* difference between "here is the page, pick your build" and "here is your
|
|
100
|
+
* installer". It is the THIRD parameter so every existing caller — including
|
|
101
|
+
* every test, which passes `fetchImpl` — keeps its shape.
|
|
121
102
|
*
|
|
122
|
-
*
|
|
103
|
+
* Cached for five minutes on `globalThis` (see `shell-cache`) for the same
|
|
123
104
|
* reason the database handle is anchored there: `bun --hot` re-evaluates this
|
|
124
|
-
* module while the cache it would otherwise hold lives on.
|
|
125
|
-
* unauthenticated reads an hour per address, and the sidebar asks on every
|
|
126
|
-
* mount.
|
|
105
|
+
* module while the cache it would otherwise hold lives on.
|
|
127
106
|
*/
|
|
128
107
|
export async function fetchShellRelease(
|
|
129
108
|
kind: ShellKind,
|
|
130
109
|
fetchImpl: typeof fetch = fetch,
|
|
110
|
+
arch: ShellArch | null = null,
|
|
131
111
|
): Promise<ShellRelease | null> {
|
|
132
|
-
const cache = cacheSlot(kind);
|
|
112
|
+
const cache = cacheSlot<ShellRelease>(shellCacheKey(kind, arch));
|
|
133
113
|
const now = Date.now();
|
|
134
114
|
if (cache.entry && now - cache.at < CACHE_MS) return cache.entry;
|
|
135
115
|
|
|
116
|
+
const headers = gitHubHeaders();
|
|
117
|
+
const resolved =
|
|
118
|
+
(await readFromApi(kind, arch, fetchImpl, headers)) ??
|
|
119
|
+
(await readFromPages(kind, arch, fetchImpl, headers));
|
|
120
|
+
if (!resolved) return cache.entry ?? null;
|
|
121
|
+
|
|
122
|
+
cache.entry = resolved;
|
|
123
|
+
cache.at = now;
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** How many releases to look back through for the artifact. */
|
|
128
|
+
const RELEASE_SCAN = 20;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The REST read, or null when it did not answer with a usable release list.
|
|
132
|
+
*
|
|
133
|
+
* A non-2xx is not an error to report — it is the quota, and the caller has a
|
|
134
|
+
* second path.
|
|
135
|
+
*/
|
|
136
|
+
async function readFromApi(
|
|
137
|
+
kind: ShellKind,
|
|
138
|
+
arch: ShellArch | null,
|
|
139
|
+
fetchImpl: typeof fetch,
|
|
140
|
+
headers: Record<string, string>,
|
|
141
|
+
): Promise<ShellRelease | null> {
|
|
136
142
|
try {
|
|
137
143
|
const response = await fetchImpl(`${githubApiBase()}/repos/${REPO}/releases?per_page=${RELEASE_SCAN}`, {
|
|
138
|
-
headers: {
|
|
139
|
-
accept: 'application/vnd.github+json',
|
|
140
|
-
// GitHub rejects a request with no User-Agent.
|
|
141
|
-
'user-agent': 'kabmo-console',
|
|
142
|
-
},
|
|
144
|
+
headers: { accept: 'application/vnd.github+json', ...headers },
|
|
143
145
|
});
|
|
144
|
-
if (!response.ok) return
|
|
146
|
+
if (!response.ok) return null;
|
|
145
147
|
|
|
146
148
|
const body = (await response.json()) as unknown;
|
|
147
149
|
// A body that is not the release list is a failed probe, not an empty
|
|
148
|
-
// repository: answering `{url: null,
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(body)) return cache.entry ?? null;
|
|
150
|
+
// repository: answering `{url: null, …}` here would be indistinguishable
|
|
151
|
+
// from "no release carries an artifact".
|
|
152
|
+
if (!Array.isArray(body)) return null;
|
|
152
153
|
|
|
153
154
|
// Drafts are not published and prereleases are not what a console offers an
|
|
154
|
-
// installed shell
|
|
155
|
+
// installed shell.
|
|
155
156
|
const published = body.filter(
|
|
156
157
|
(entry): entry is Record<string, unknown> =>
|
|
157
158
|
entry !== null && typeof entry === 'object' && entry.draft !== true && entry.prerelease !== true,
|
|
158
159
|
);
|
|
159
160
|
|
|
160
161
|
const release = published
|
|
161
|
-
.map((entry) => ({ entry, asset: shellAsset(kind, entry.assets) }))
|
|
162
|
+
.map((entry) => ({ entry, asset: shellAsset(kind, arch, entry.assets) }))
|
|
162
163
|
.find((candidate) => candidate.asset !== null);
|
|
163
164
|
|
|
164
|
-
if (!release?.asset)
|
|
165
|
-
const fallback: ShellRelease = { url: null, latest: null, releaseUrl: null };
|
|
166
|
-
cache.entry = fallback;
|
|
167
|
-
cache.at = now;
|
|
168
|
-
return fallback;
|
|
169
|
-
}
|
|
165
|
+
if (!release?.asset) return EMPTY_RELEASE;
|
|
170
166
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
return resolvedRelease(kind, arch, {
|
|
168
|
+
name: String(release.asset.name),
|
|
169
|
+
downloadUrl:
|
|
170
|
+
typeof release.asset.browser_download_url === 'string' ? release.asset.browser_download_url : null,
|
|
174
171
|
releaseUrl: typeof release.entry.html_url === 'string' ? release.entry.html_url : null,
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
cache.entry = resolved;
|
|
178
|
-
cache.at = now;
|
|
179
|
-
return resolved;
|
|
172
|
+
});
|
|
180
173
|
} catch {
|
|
181
|
-
|
|
182
|
-
// momentary outage must not remove a download button that was valid.
|
|
183
|
-
return cache.entry ?? null;
|
|
174
|
+
return null;
|
|
184
175
|
}
|
|
185
176
|
}
|
|
186
177
|
|
|
187
|
-
/**
|
|
188
|
-
|
|
178
|
+
/** The public-pages read, wrapped so a failure is the caller's `null`. */
|
|
179
|
+
async function readFromPages(
|
|
180
|
+
kind: ShellKind,
|
|
181
|
+
arch: ShellArch | null,
|
|
182
|
+
fetchImpl: typeof fetch,
|
|
183
|
+
headers: Record<string, string>,
|
|
184
|
+
): Promise<ShellRelease | null> {
|
|
185
|
+
try {
|
|
186
|
+
const found = await findAssetInPages(kind, fetchImpl, headers, arch);
|
|
187
|
+
if (found.kind === 'unreachable') return null;
|
|
188
|
+
if (found.kind === 'none') return EMPTY_RELEASE;
|
|
189
|
+
return resolvedRelease(kind, arch, { ...found.asset, releaseUrl: found.releaseUrl });
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
189
194
|
|
|
190
195
|
/**
|
|
191
196
|
* The asset of a release that names this shell's artifact, or null.
|
|
192
197
|
*
|
|
193
|
-
*
|
|
194
|
-
* release that carries both — which happens the moment a rename lands and
|
|
195
|
-
* `--clobber` has not yet replaced the old asset, or a hand-upload adds one
|
|
196
|
-
* beside the other — would otherwise resolve to whichever the asset list
|
|
197
|
-
* happened to order first. Measured: a release holding both
|
|
198
|
-
* `kabmo-android-v0.4.0.apk` and `Kabmo-0.6.0-android.apk` answered `0.4.0`,
|
|
199
|
-
* i.e. the console offered an update to a version it already had and hid the
|
|
200
|
-
* newer one.
|
|
198
|
+
* Two questions, in this order:
|
|
201
199
|
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
200
|
+
* 1. **Does an architecture match?** When the caller named one, the only
|
|
201
|
+
* acceptable asset is the file that machine installs (`selfUpdateArtifact`).
|
|
202
|
+
* Everything else in the release — the other architecture, the `.dmg`, a
|
|
203
|
+
* `.deb` — is skipped, and the next release is tried.
|
|
204
|
+
* 2. **Which spelling?** The CURRENT scheme wins over the legacy one, and that
|
|
205
|
+
* is not cosmetic. A release that carries both — which happens the moment a
|
|
206
|
+
* rename lands and `--clobber` has not yet replaced the old asset, or a
|
|
207
|
+
* hand-upload adds one beside the other — would otherwise resolve to
|
|
208
|
+
* whichever the asset list happened to order first. Measured: a release
|
|
209
|
+
* holding both `kabmo-android-v0.4.0.apk` and `Kabmo-0.6.0-android.apk`
|
|
210
|
+
* answered `0.4.0`, i.e. the console offered an update to a version it
|
|
211
|
+
* already had and hid the newer one.
|
|
204
212
|
*/
|
|
205
|
-
function shellAsset(
|
|
213
|
+
function shellAsset(
|
|
214
|
+
kind: ShellKind,
|
|
215
|
+
arch: ShellArch | null,
|
|
216
|
+
assets: unknown,
|
|
217
|
+
): Record<string, unknown> | null {
|
|
206
218
|
if (!Array.isArray(assets)) return null;
|
|
207
219
|
|
|
208
220
|
const named = assets.filter(
|
|
@@ -213,70 +225,38 @@ function shellAsset(kind: ShellKind, assets: unknown): Record<string, unknown> |
|
|
|
213
225
|
versionFromAssetName(kind, String((entry as Record<string, unknown>).name)) !== null,
|
|
214
226
|
);
|
|
215
227
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
named[0] ??
|
|
223
|
-
null
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const CACHE_MS = 5 * 60_000;
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Where the release read goes.
|
|
231
|
-
*
|
|
232
|
-
* Overridable for the same reason the npm probe honours `NPM_CONFIG_REGISTRY`: a
|
|
233
|
-
* test and a rehearsal need to exercise the real parsing against a stub instead
|
|
234
|
-
* of a live GitHub call, and hardcoding the host makes that impossible without
|
|
235
|
-
* intercepting `fetch` globally.
|
|
236
|
-
*/
|
|
237
|
-
function githubApiBase(): string {
|
|
238
|
-
const configured = Bun.env.KABMO_GITHUB_API ?? '';
|
|
239
|
-
return configured.trim().replace(/\/+$/, '') || 'https://api.github.com';
|
|
240
|
-
}
|
|
228
|
+
if (arch) {
|
|
229
|
+
const exact = named.find(
|
|
230
|
+
(asset) => selfUpdateArtifact(kind, arch, String(asset.name)) !== null,
|
|
231
|
+
);
|
|
232
|
+
if (exact) return exact;
|
|
233
|
+
}
|
|
241
234
|
|
|
242
|
-
|
|
243
|
-
entry: ShellRelease | null;
|
|
244
|
-
at: number;
|
|
235
|
+
return named.find((asset) => SHELL_ASSETS[kind].current.test(String(asset.name))) ?? named[0] ?? null;
|
|
245
236
|
}
|
|
246
237
|
|
|
247
|
-
type ShellCacheHost = typeof globalThis & {
|
|
248
|
-
__kabmoShellRelease?: Partial<Record<ShellKind, CacheSlot>>;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
238
|
/**
|
|
252
|
-
*
|
|
239
|
+
* Turn one resolved asset into the payload.
|
|
253
240
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* the
|
|
241
|
+
* `direct` is derived from the SAME decision that chose the asset: an arch was
|
|
242
|
+
* named and the asset is that arch's self-update file. Deriving it separately —
|
|
243
|
+
* from `directDownload`, or from the platform — is how the client ends up
|
|
244
|
+
* offering an install for a release page.
|
|
257
245
|
*/
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
246
|
+
function resolvedRelease(
|
|
247
|
+
kind: ShellKind,
|
|
248
|
+
arch: ShellArch | null,
|
|
249
|
+
asset: { name: string; downloadUrl: string | null; releaseUrl: string | null },
|
|
250
|
+
): ShellRelease {
|
|
251
|
+
const installable = arch !== null && selfUpdateArtifact(kind, arch, asset.name) !== null;
|
|
252
|
+
const direct = SHELL_ASSETS[kind].directDownload || installable;
|
|
264
253
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
*/
|
|
274
|
-
export function resetShellReleaseCache(kind?: ShellKind): void {
|
|
275
|
-
const host = globalThis as ShellCacheHost;
|
|
276
|
-
if (!kind) {
|
|
277
|
-
host.__kabmoShellRelease = {};
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
host.__kabmoShellRelease ??= {};
|
|
281
|
-
host.__kabmoShellRelease[kind] = { entry: null, at: 0 };
|
|
254
|
+
return {
|
|
255
|
+
// A per-arch desktop build cannot be named without an architecture — see
|
|
256
|
+
// the module comment — so the link is the page that lists every one.
|
|
257
|
+
url: direct ? asset.downloadUrl : asset.releaseUrl,
|
|
258
|
+
latest: versionFromAssetName(kind, asset.name),
|
|
259
|
+
releaseUrl: asset.releaseUrl,
|
|
260
|
+
direct: direct && asset.downloadUrl !== null,
|
|
261
|
+
};
|
|
282
262
|
}
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* The Files panel's "New file" branch.
|
|
8
8
|
*
|
|
9
|
-
* Two rules are load-bearing and neither is visible from the happy path
|
|
10
|
-
* name may
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Two rules are load-bearing and neither is visible from the happy path. The
|
|
10
|
+
* name may carry SEGMENTS (that is the nested-path grammar, and its refusals
|
|
11
|
+
* live in `action-nested.test.ts`), and an existing file is REFUSED rather
|
|
12
|
+
* than truncated — the second is the one a naive `Bun.write` gets wrong:
|
|
13
|
+
* `Bun.write` creates or truncates, so a name that collides with a real file
|
|
14
|
+
* would silently blank it.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
@@ -34,7 +34,10 @@ async function create(folderPath: string, name: string) {
|
|
|
34
34
|
form.append('path', folderPath);
|
|
35
35
|
form.append('name', name);
|
|
36
36
|
const res = await action({ request: new Request('http://localhost/api/fs/action', { method: 'POST', body: form }) } as never);
|
|
37
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
status: res.status,
|
|
39
|
+
body: (await res.json()) as { success?: boolean; kind?: string; path?: string; error?: string },
|
|
40
|
+
};
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
describe('fs create', () => {
|
|
@@ -43,7 +46,7 @@ describe('fs create', () => {
|
|
|
43
46
|
const { status, body } = await create('tmp-create-probe', 'notes.md');
|
|
44
47
|
|
|
45
48
|
expect(status).toBe(200);
|
|
46
|
-
expect(body).toEqual({ success: true, path: 'tmp-create-probe/notes.md' });
|
|
49
|
+
expect(body).toEqual({ success: true, kind: 'file', path: 'tmp-create-probe/notes.md' });
|
|
47
50
|
expect(fs.readFileSync(path.join(DIR, 'notes.md'), 'utf8')).toBe('');
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -67,12 +70,14 @@ describe('fs create', () => {
|
|
|
67
70
|
expect(fs.readFileSync(path.join(DIR, 'keep.txt'), 'utf8')).toBe('important');
|
|
68
71
|
});
|
|
69
72
|
|
|
70
|
-
test('
|
|
73
|
+
test('a path separator is a NESTED path, and `..` is still refused', async () => {
|
|
71
74
|
fs.mkdirSync(DIR, { recursive: true });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// A forward or back separator names a nested destination, which the dialog
|
|
76
|
+
// is for — the escape itself is the `..` segment, and that stays refused.
|
|
77
|
+
expect((await create('tmp-create-probe', 'sub/inside.txt')).status).toBe(200);
|
|
78
|
+
expect((await create('tmp-create-probe', 'sub\\also-inside.txt')).status).toBe(200);
|
|
79
|
+
expect((await create('tmp-create-probe', '../escape.txt')).status).toBe(400);
|
|
80
|
+
expect(fs.existsSync(path.join(DIR, 'sub/inside.txt'))).toBe(true);
|
|
76
81
|
expect(fs.existsSync(path.join(process.cwd(), 'escape.txt'))).toBe(false);
|
|
77
82
|
});
|
|
78
83
|
|