@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The local-edit gate. The sync REPLACES files, so a plugin whose tree no longer
|
|
8
|
+
* matches what the install wrote is refused — and the baseline has to be stable
|
|
9
|
+
* across the install's own build, or every install would read as edited the first
|
|
10
|
+
* time it built and the sync would never run again.
|
|
11
|
+
*
|
|
12
|
+
* What the baseline must NOT do is outrank the trees in front of it: see
|
|
13
|
+
* `baselineDescribesInstall`. A baseline that contradicts them describes a copy
|
|
14
|
+
* this install never had, and obeying it is what made a bundled update
|
|
15
|
+
* unrecoverable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { syncBundledPlugins } from '@/server/lib/panels/sync.server';
|
|
23
|
+
import { hashTrackedFiles } from '@/server/lib/panels/local-edits.server';
|
|
24
|
+
import { readBlockedSyncs, writeBaseline } from '@/server/lib/panels/baseline.server';
|
|
25
|
+
import {
|
|
26
|
+
FIXTURE_MANIFEST as MANIFEST,
|
|
27
|
+
installBundled,
|
|
28
|
+
setupSyncFixture,
|
|
29
|
+
teardownSyncFixture,
|
|
30
|
+
writePlugin,
|
|
31
|
+
type SyncFixture,
|
|
32
|
+
} from '@/server/lib/panels/sync.fixture.test-util';
|
|
33
|
+
|
|
34
|
+
let fixture: SyncFixture;
|
|
35
|
+
let root = '';
|
|
36
|
+
let store = '';
|
|
37
|
+
|
|
38
|
+
beforeEach(async () => {
|
|
39
|
+
fixture = await setupSyncFixture();
|
|
40
|
+
root = fixture.root;
|
|
41
|
+
store = fixture.store;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
await teardownSyncFixture(fixture);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('local edit protection', () => {
|
|
49
|
+
test('an edited file blocks the sync and is named', async () => {
|
|
50
|
+
await installBundled(fixture, '1.0.0', { 'src/note.txt': 'original' });
|
|
51
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'mine');
|
|
52
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
53
|
+
|
|
54
|
+
const result = await syncBundledPlugins();
|
|
55
|
+
expect(result.updated).toEqual([]);
|
|
56
|
+
expect(result.blocked).toHaveLength(1);
|
|
57
|
+
expect(result.blocked[0].changed).toEqual(['src/note.txt']);
|
|
58
|
+
// The edit survives — that is the whole point.
|
|
59
|
+
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'utf8')).toBe('mine');
|
|
60
|
+
// And the refusal is recorded for the pane.
|
|
61
|
+
expect((await readBlockedSyncs())['demo'].changed).toEqual(['src/note.txt']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('an added file blocks the sync', async () => {
|
|
65
|
+
await installBundled(fixture, '1.0.0');
|
|
66
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'src-extra.js'), 'export default {};');
|
|
67
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
68
|
+
|
|
69
|
+
const result = await syncBundledPlugins();
|
|
70
|
+
expect(result.blocked[0].changed).toContain('src-extra.js');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a deleted file blocks the sync', async () => {
|
|
74
|
+
await installBundled(fixture, '1.0.0', { 'src/note.txt': 'original' });
|
|
75
|
+
fs.rmSync(join(root, 'plugins', 'demo', 'src', 'note.txt'));
|
|
76
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
77
|
+
|
|
78
|
+
expect((await syncBundledPlugins()).blocked[0].changed).toEqual(['src/note.txt']);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('an unchanged tree syncs, and the new baseline allows the NEXT sync', async () => {
|
|
82
|
+
await installBundled(fixture, '1.0.0');
|
|
83
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
84
|
+
expect((await syncBundledPlugins()).updated).toHaveLength(1);
|
|
85
|
+
|
|
86
|
+
// The baseline was re-taken after the build, so the tree the sync just wrote
|
|
87
|
+
// is not read as an edit — without this the pass would block itself forever.
|
|
88
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '3.0.0' });
|
|
89
|
+
const second = await syncBundledPlugins();
|
|
90
|
+
expect(second.updated).toHaveLength(1);
|
|
91
|
+
expect(second.blocked).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('build output and lockfiles are not edits', async () => {
|
|
95
|
+
// A plugin WITH source: its `dist` is this build's output, so its hashes
|
|
96
|
+
// change on every build and tracking it would block the sync forever.
|
|
97
|
+
await installBundled(fixture, '1.0.0', { 'src/app.tsx': 'export default {};' });
|
|
98
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'bun.lock'), '{}');
|
|
99
|
+
fs.mkdirSync(join(root, 'plugins', 'demo', 'node_modules', 'dep'), { recursive: true });
|
|
100
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'node_modules', 'dep', 'package.json'), '{"name":"dep"}');
|
|
101
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'dist', 'app.js'), 'export default { built: true };');
|
|
102
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' }, { 'src/app.tsx': 'export default {};' });
|
|
103
|
+
|
|
104
|
+
const result = await syncBundledPlugins();
|
|
105
|
+
expect(result.blocked).toEqual([]);
|
|
106
|
+
expect(result.updated).toHaveLength(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('an edited bundle of a plugin with no SOURCE blocks the sync', async () => {
|
|
110
|
+
// The shape this gate exists for: a plugin with no `src/` DISTRIBUTES its
|
|
111
|
+
// bundle, so `dist` is content the user can edit rather than build output.
|
|
112
|
+
// Excluding it let the sync silently overwrite the edit — measured against a
|
|
113
|
+
// live server before this case existed.
|
|
114
|
+
writePlugin(join(store, 'plugins', 'demo'), MANIFEST);
|
|
115
|
+
writePlugin(join(root, 'plugins', 'demo'), MANIFEST);
|
|
116
|
+
await writeBaseline('demo', await hashTrackedFiles(join(root, 'plugins', 'demo')));
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'dist', 'app.js'), 'export default { MINE: true };');
|
|
119
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
120
|
+
|
|
121
|
+
const result = await syncBundledPlugins();
|
|
122
|
+
expect(result.blocked).toHaveLength(1);
|
|
123
|
+
expect(result.blocked[0].changed).toEqual(['dist/app.js']);
|
|
124
|
+
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'dist', 'app.js'), 'utf8')).toContain('MINE');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('a plugin with no baseline runs, so an upgrade cannot freeze it', async () => {
|
|
128
|
+
// An install written before this check existed. Refusing would leave it on
|
|
129
|
+
// the old version forever with no way to say why.
|
|
130
|
+
writePlugin(join(root, 'plugins', 'demo'), MANIFEST);
|
|
131
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
132
|
+
|
|
133
|
+
expect((await syncBundledPlugins()).updated).toHaveLength(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('reverting the edit lets the next pass sync, with no user action', async () => {
|
|
137
|
+
await installBundled(fixture, '1.0.0', { 'src/note.txt': 'original' });
|
|
138
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'mine');
|
|
139
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
140
|
+
expect((await syncBundledPlugins()).blocked).toHaveLength(1);
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'original');
|
|
143
|
+
const result = await syncBundledPlugins();
|
|
144
|
+
expect(result.updated).toHaveLength(1);
|
|
145
|
+
expect(result.blocked).toEqual([]);
|
|
146
|
+
// The refusal record is cleared, or the pane would keep reporting a fault
|
|
147
|
+
// the user has already resolved.
|
|
148
|
+
expect(await readBlockedSyncs()).toEqual({});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -8,65 +8,48 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Three properties decide whether this pass is safe to run on every boot, and
|
|
10
10
|
* each of them is a failure that is invisible until it has already overwritten
|
|
11
|
-
* something: the
|
|
12
|
-
*
|
|
13
|
-
* replaced by it), and the preservation of
|
|
14
|
-
* no build output, so wiping those would
|
|
15
|
-
* rebuild).
|
|
11
|
+
* something: the provenance gate (a hand-copied directory must not be replaced
|
|
12
|
+
* just because a store entry shares its id), the git exclusion (a clone that
|
|
13
|
+
* shares an id must not be replaced by it), and the preservation of
|
|
14
|
+
* `node_modules`/`dist` (the store ships no build output, so wiping those would
|
|
15
|
+
* leave the plugin unservable until a rebuild).
|
|
16
|
+
*
|
|
17
|
+
* The local-edit gate has its own suite (`sync.local-edits.test.ts`).
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
19
21
|
import fs from 'node:fs';
|
|
20
|
-
import { tmpdir } from 'node:os';
|
|
21
22
|
import { join } from 'node:path';
|
|
22
23
|
|
|
23
24
|
import { forceSyncBundledPlugin, syncBundledPlugins } from '@/server/lib/panels/sync.server';
|
|
24
25
|
import { installPanelPluginFromGit } from '@/server/lib/panels/install.server';
|
|
25
|
-
import { hashTrackedFiles } from '@/server/lib/panels/local-edits.server';
|
|
26
|
-
import { readBlockedSyncs, writeBaseline } from '@/server/lib/panels/baseline.server';
|
|
27
|
-
import {
|
|
28
|
-
|
|
26
|
+
import { changedPaths, hashTrackedFiles } from '@/server/lib/panels/local-edits.server';
|
|
27
|
+
import { readBaseline, readBlockedSyncs, writeBaseline } from '@/server/lib/panels/baseline.server';
|
|
28
|
+
import {
|
|
29
|
+
FIXTURE_MANIFEST as MANIFEST,
|
|
30
|
+
installBundled,
|
|
31
|
+
setupSyncFixture,
|
|
32
|
+
teardownSyncFixture,
|
|
33
|
+
writePlugin,
|
|
34
|
+
type SyncFixture,
|
|
35
|
+
} from '@/server/lib/panels/sync.fixture.test-util';
|
|
29
36
|
|
|
30
37
|
const hasGit = Boolean(Bun.which('git'));
|
|
31
38
|
|
|
39
|
+
let fixture: SyncFixture;
|
|
32
40
|
let root = '';
|
|
33
41
|
let store = '';
|
|
34
42
|
let work = '';
|
|
35
43
|
|
|
36
|
-
const MANIFEST = { id: 'demo', name: 'Demo', version: '1.0.0', app: 'dist/app.js' };
|
|
37
|
-
|
|
38
|
-
/** A plugin directory: manifest, bundle, and optionally a marker file. */
|
|
39
|
-
function writePlugin(dir: string, manifest: unknown, files: Record<string, string> = {}): void {
|
|
40
|
-
fs.mkdirSync(join(dir, 'dist'), { recursive: true });
|
|
41
|
-
fs.writeFileSync(join(dir, 'kabmo.json'), JSON.stringify(manifest));
|
|
42
|
-
fs.writeFileSync(join(dir, 'dist', 'app.js'), 'export default {};');
|
|
43
|
-
for (const [name, content] of Object.entries(files)) {
|
|
44
|
-
const target = join(dir, name);
|
|
45
|
-
fs.mkdirSync(join(target, '..'), { recursive: true });
|
|
46
|
-
fs.writeFileSync(target, content);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
beforeEach(async () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
process.env.KABMO_BUNDLED_MARKETPLACE_DIR = store;
|
|
56
|
-
// The baseline and refusal records live in `app_settings`, so this suite needs
|
|
57
|
-
// an isolated database: a shared handle would read the developer's real one.
|
|
58
|
-
await isolateDb();
|
|
59
|
-
invalidatePanelScan();
|
|
45
|
+
fixture = await setupSyncFixture();
|
|
46
|
+
root = fixture.root;
|
|
47
|
+
store = fixture.store;
|
|
48
|
+
work = fixture.work;
|
|
60
49
|
});
|
|
61
50
|
|
|
62
51
|
afterEach(async () => {
|
|
63
|
-
|
|
64
|
-
delete process.env.KABMO_BUNDLED_MARKETPLACE_DIR;
|
|
65
|
-
await releaseDb();
|
|
66
|
-
invalidatePanelScan();
|
|
67
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
68
|
-
fs.rmSync(store, { recursive: true, force: true });
|
|
69
|
-
fs.rmSync(work, { recursive: true, force: true });
|
|
52
|
+
await teardownSyncFixture(fixture);
|
|
70
53
|
});
|
|
71
54
|
|
|
72
55
|
describe('syncBundledPlugins', () => {
|
|
@@ -95,7 +78,8 @@ describe('syncBundledPlugins', () => {
|
|
|
95
78
|
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'marker.txt'), 'utf8')).toBe('installed');
|
|
96
79
|
});
|
|
97
80
|
|
|
98
|
-
test('leaves an equal version alone', async () => {
|
|
81
|
+
test('leaves an equal version alone when Kabmo cannot prove it wrote the install', async () => {
|
|
82
|
+
// No baseline, so provenance is unknown and the version gate governs.
|
|
99
83
|
writePlugin(join(root, 'plugins', 'demo'), MANIFEST, { 'src/marker.txt': 'installed' });
|
|
100
84
|
writePlugin(join(store, 'plugins', 'demo'), MANIFEST, { 'src/marker.txt': 'store' });
|
|
101
85
|
|
|
@@ -167,142 +151,136 @@ describe('syncBundledPlugins', () => {
|
|
|
167
151
|
});
|
|
168
152
|
|
|
169
153
|
/**
|
|
170
|
-
* The
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
154
|
+
* The reported bug: a store change that did NOT move the manifest version was
|
|
155
|
+
* never applied, so the only way to get it was to remove the plugin and install
|
|
156
|
+
* it again.
|
|
157
|
+
*
|
|
158
|
+
* Measured on a real install: the product rename rewrote every bundled file AND
|
|
159
|
+
* reset the versions to `0.0.1`, so the version gate saw "not newer" forever
|
|
160
|
+
* while the recorded baseline still described the pre-rename files — which made
|
|
161
|
+
* the local-edit gate report the store's own updates as the user's edits.
|
|
174
162
|
*/
|
|
175
|
-
describe('
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version }, files);
|
|
184
|
-
writePlugin(join(root, 'plugins', 'demo'), { ...MANIFEST, version }, files);
|
|
185
|
-
await writeBaseline('demo', await hashTrackedFiles(join(root, 'plugins', 'demo')));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
test('an edited file blocks the sync and is named', async () => {
|
|
189
|
-
await installBundled('1.0.0', { 'src/note.txt': 'original' });
|
|
190
|
-
fs.writeFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'mine');
|
|
191
|
-
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
163
|
+
describe('store content that did not bump its version', () => {
|
|
164
|
+
test('a fix shipped without a version bump is applied', async () => {
|
|
165
|
+
await installBundled(fixture, '1.0.0', { 'src/app.tsx': 'export default { old: true };' });
|
|
166
|
+
// The store changes, the version does not — a shipped fix whose bump was
|
|
167
|
+
// missed, which is exactly what left users stuck.
|
|
168
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '1.0.0' }, {
|
|
169
|
+
'src/app.tsx': 'export default { fixed: true };',
|
|
170
|
+
});
|
|
192
171
|
|
|
193
172
|
const result = await syncBundledPlugins();
|
|
194
|
-
expect(result.
|
|
195
|
-
expect(result.
|
|
196
|
-
expect(result.
|
|
197
|
-
|
|
198
|
-
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'utf8')).toBe('mine');
|
|
199
|
-
// And the refusal is recorded for the pane.
|
|
200
|
-
expect((await readBlockedSyncs())['demo'].changed).toEqual(['src/note.txt']);
|
|
173
|
+
expect(result.blocked).toEqual([]);
|
|
174
|
+
expect(result.updated).toHaveLength(1);
|
|
175
|
+
expect(result.updated[0]).toMatchObject({ pluginId: 'demo', contentOnly: true });
|
|
176
|
+
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'app.tsx'), 'utf8')).toContain('fixed');
|
|
201
177
|
});
|
|
202
178
|
|
|
203
|
-
test('
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
writePlugin(join(store, 'plugins', 'demo'),
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
expect(result.blocked[0].changed).toContain('src-extra.js');
|
|
210
|
-
});
|
|
179
|
+
test('a stale baseline does not read as the user editing every file', async () => {
|
|
180
|
+
// The install and the store already agree, but the recorded baseline
|
|
181
|
+
// describes a copy that no longer exists — the state the rename produced.
|
|
182
|
+
writePlugin(join(store, 'plugins', 'demo'), MANIFEST, { 'src/app.tsx': 'export default { current: true };' });
|
|
183
|
+
writePlugin(join(root, 'plugins', 'demo'), MANIFEST, { 'src/app.tsx': 'export default { current: true };' });
|
|
184
|
+
await writeBaseline('demo', { 'src/app.tsx': 'a-hash-of-a-file-that-is-gone', 'package.json': 'also-gone' });
|
|
211
185
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
fs.rmSync(join(root, 'plugins', 'demo', 'src', 'note.txt'));
|
|
215
|
-
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
186
|
+
// Now the store moves on again. The stale baseline must not block it.
|
|
187
|
+
writePlugin(join(store, 'plugins', 'demo'), MANIFEST, { 'src/app.tsx': 'export default { newer: true };' });
|
|
216
188
|
|
|
217
|
-
|
|
189
|
+
const result = await syncBundledPlugins();
|
|
190
|
+
expect(result.blocked).toEqual([]);
|
|
191
|
+
expect(result.updated).toHaveLength(1);
|
|
192
|
+
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'app.tsx'), 'utf8')).toContain('newer');
|
|
218
193
|
});
|
|
219
194
|
|
|
220
|
-
test('
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
195
|
+
test('a stale baseline is healed even when the store has nothing new', async () => {
|
|
196
|
+
// The exact state the rename left on a real install: the store and the
|
|
197
|
+
// install already agree, and the baseline describes neither. Nothing is
|
|
198
|
+
// applied, so this pass must still re-record the baseline — otherwise the
|
|
199
|
+
// NEXT release's fix reads as a user edit on every path and the plugin is
|
|
200
|
+
// stuck until it is reinstalled.
|
|
201
|
+
writePlugin(join(store, 'plugins', 'demo'), MANIFEST, { 'src/app.tsx': 'export default { current: true };' });
|
|
202
|
+
writePlugin(join(root, 'plugins', 'demo'), MANIFEST, { 'src/app.tsx': 'export default { current: true };' });
|
|
203
|
+
await writeBaseline('demo', { 'src/app.tsx': 'a-hash-of-a-file-that-is-gone' });
|
|
224
204
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
205
|
+
expect((await syncBundledPlugins()).updated).toEqual([]);
|
|
206
|
+
const healed = await readBaseline('demo');
|
|
207
|
+
expect(healed).not.toBeNull();
|
|
208
|
+
expect(changedPaths(healed ?? {}, await hashTrackedFiles(join(root, 'plugins', 'demo')))).toEqual([]);
|
|
209
|
+
|
|
210
|
+
// And the next release's fix then applies with no further action.
|
|
211
|
+
writePlugin(join(store, 'plugins', 'demo'), MANIFEST, { 'src/app.tsx': 'export default { newer: true };' });
|
|
212
|
+
const next = await syncBundledPlugins();
|
|
213
|
+
expect(next.blocked).toEqual([]);
|
|
214
|
+
expect(next.updated).toHaveLength(1);
|
|
231
215
|
});
|
|
232
216
|
|
|
233
|
-
test('
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
fs.
|
|
239
|
-
|
|
240
|
-
fs.writeFileSync(join(root, 'plugins', 'demo', 'dist', 'app.js'), 'export default { built: true };');
|
|
241
|
-
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' }, { 'src/app.tsx': 'export default {};' });
|
|
217
|
+
test('a real edit still blocks, even with a trusted baseline', async () => {
|
|
218
|
+
// The healing above must not become "overwrite whatever the user wrote":
|
|
219
|
+
// an install that differs from BOTH the baseline and the store is the
|
|
220
|
+
// user's own work.
|
|
221
|
+
await installBundled(fixture, '1.0.0', { 'src/note.txt': 'original' });
|
|
222
|
+
fs.writeFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'mine');
|
|
223
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '1.0.0' }, { 'src/note.txt': 'original' });
|
|
242
224
|
|
|
243
225
|
const result = await syncBundledPlugins();
|
|
244
|
-
expect(result.
|
|
245
|
-
expect(result.
|
|
226
|
+
expect(result.updated).toEqual([]);
|
|
227
|
+
expect(result.blocked[0].changed).toEqual(['src/note.txt']);
|
|
228
|
+
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'utf8')).toBe('mine');
|
|
246
229
|
});
|
|
247
230
|
|
|
248
|
-
test('
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
writePlugin(join(store, 'plugins', 'demo'), MANIFEST);
|
|
254
|
-
writePlugin(join(root, 'plugins', 'demo'), MANIFEST);
|
|
255
|
-
await writeBaseline('demo', await hashTrackedFiles(join(root, 'plugins', 'demo')));
|
|
256
|
-
|
|
257
|
-
fs.writeFileSync(join(root, 'plugins', 'demo', 'dist', 'app.js'), 'export default { MINE: true };');
|
|
258
|
-
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
231
|
+
test('a hand-copied directory still keeps the version gate', async () => {
|
|
232
|
+
// No baseline means unknown provenance: an equal-version store entry must
|
|
233
|
+
// not replace a directory Kabmo cannot prove it wrote.
|
|
234
|
+
writePlugin(join(root, 'plugins', 'demo'), MANIFEST, { 'src/note.txt': 'mine' });
|
|
235
|
+
writePlugin(join(store, 'plugins', 'demo'), MANIFEST, { 'src/note.txt': 'store' });
|
|
259
236
|
|
|
260
|
-
|
|
261
|
-
expect(
|
|
262
|
-
expect(result.blocked[0].changed).toEqual(['dist/app.js']);
|
|
263
|
-
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'dist', 'app.js'), 'utf8')).toContain('MINE');
|
|
237
|
+
expect((await syncBundledPlugins()).updated).toEqual([]);
|
|
238
|
+
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'utf8')).toBe('mine');
|
|
264
239
|
});
|
|
265
240
|
|
|
266
|
-
test('a
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
241
|
+
test('a second pass is a no-op once the copy has landed', async () => {
|
|
242
|
+
await installBundled(fixture, '1.0.0', { 'src/app.tsx': 'export default { old: true };' });
|
|
243
|
+
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '1.0.0' }, {
|
|
244
|
+
'src/app.tsx': 'export default { fixed: true };',
|
|
245
|
+
});
|
|
272
246
|
expect((await syncBundledPlugins()).updated).toHaveLength(1);
|
|
273
|
-
});
|
|
274
247
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
248
|
+
// The baseline was re-taken after the build, so the pass that just ran must
|
|
249
|
+
// not be read as a difference on the next boot — a sync that re-applies
|
|
250
|
+
// forever would rebuild the plugin on every start.
|
|
251
|
+
expect((await syncBundledPlugins()).updated).toEqual([]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
280
254
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
255
|
+
/**
|
|
256
|
+
* The override: the user has seen which files differ and asked for the store's
|
|
257
|
+
* copy anyway.
|
|
258
|
+
*/
|
|
259
|
+
describe('forceSyncBundledPlugin', () => {
|
|
260
|
+
test('refuses a plugin that is not a bundled install', async () => {
|
|
261
|
+
writePlugin(join(root, 'plugins', 'demo'), MANIFEST);
|
|
262
|
+
const result = await forceSyncBundledPlugin('demo');
|
|
263
|
+
expect(result.ok).toBe(false);
|
|
264
|
+
expect(result.error).toContain('No bundled plugin');
|
|
288
265
|
});
|
|
289
266
|
|
|
290
|
-
test('the
|
|
291
|
-
|
|
267
|
+
test('applies the store copy even when its version is not newer', async () => {
|
|
268
|
+
// The case the override most often exists for: a stale baseline refusing an
|
|
269
|
+
// update whose version did not move. Clearing the baseline and re-running
|
|
270
|
+
// the pass made this a no-op, because a cleared baseline reads as unknown
|
|
271
|
+
// provenance and the version gate then declines.
|
|
272
|
+
await installBundled(fixture, '1.0.0', { 'src/note.txt': 'original' });
|
|
292
273
|
fs.writeFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'mine');
|
|
293
|
-
writePlugin(join(store, 'plugins', 'demo'), { ...MANIFEST, version: '2.0.0' });
|
|
294
274
|
expect((await syncBundledPlugins()).blocked).toHaveLength(1);
|
|
295
275
|
|
|
296
276
|
const forced = await forceSyncBundledPlugin('demo');
|
|
297
277
|
expect(forced.ok).toBe(true);
|
|
298
278
|
expect(fs.readFileSync(join(root, 'plugins', 'demo', 'src', 'note.txt'), 'utf8')).toBe('original');
|
|
299
279
|
expect(await readBlockedSyncs()).toEqual({});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
expect(result.ok).toBe(false);
|
|
306
|
-
expect(result.error).toContain('No bundled plugin');
|
|
280
|
+
// The baseline now describes what the override wrote, so the next pass is
|
|
281
|
+
// not read as a fresh edit.
|
|
282
|
+
const after = await hashTrackedFiles(join(root, 'plugins', 'demo'));
|
|
283
|
+
expect((await syncBundledPlugins()).blocked).toEqual([]);
|
|
284
|
+
expect(after['src/note.txt']).toBeDefined();
|
|
307
285
|
});
|
|
308
286
|
});
|