@neuralnomads/codenomad-dev 0.20.0-dev-20260917-aa51cbb9 → 0.20.0-dev-20260918-02d4587c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/dist/api-types.js.map +1 -1
  2. package/dist/filesystem/__tests__/browser.test.js +10 -10
  3. package/dist/filesystem/__tests__/browser.test.js.map +1 -1
  4. package/dist/filesystem/__tests__/responsiveness.test.js +79 -0
  5. package/dist/filesystem/__tests__/responsiveness.test.js.map +1 -0
  6. package/dist/filesystem/__tests__/search-cache.test.js +48 -9
  7. package/dist/filesystem/__tests__/search-cache.test.js.map +1 -1
  8. package/dist/filesystem/__tests__/search.test.js +8 -8
  9. package/dist/filesystem/__tests__/search.test.js.map +1 -1
  10. package/dist/filesystem/__tests__/write-order.test.js +64 -0
  11. package/dist/filesystem/__tests__/write-order.test.js.map +1 -0
  12. package/dist/filesystem/browser.js +44 -46
  13. package/dist/filesystem/browser.js.map +1 -1
  14. package/dist/filesystem/file-access.js +19 -0
  15. package/dist/filesystem/file-access.js.map +1 -0
  16. package/dist/filesystem/search-cache.js +35 -9
  17. package/dist/filesystem/search-cache.js.map +1 -1
  18. package/dist/filesystem/search.js +9 -8
  19. package/dist/filesystem/search.js.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/opencode/compatibility/contract-fixture.js +13 -1
  23. package/dist/opencode/compatibility/contract-fixture.js.map +1 -1
  24. package/dist/opencode/compatibility/location.js +13 -0
  25. package/dist/opencode/compatibility/location.js.map +1 -1
  26. package/dist/opencode/compatibility/location.test.js +44 -0
  27. package/dist/opencode/compatibility/location.test.js.map +1 -1
  28. package/dist/opencode/compatibility/runtime.js.map +1 -1
  29. package/dist/opencode/compatibility/transport.js +3 -0
  30. package/dist/opencode/compatibility/transport.js.map +1 -1
  31. package/dist/opencode/compatibility/transport.test.js +40 -0
  32. package/dist/opencode/compatibility/transport.test.js.map +1 -1
  33. package/dist/permissions/opencode-yolo-metadata.js +3 -1
  34. package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
  35. package/dist/permissions/opencode-yolo-metadata.test.js +2 -2
  36. package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
  37. package/dist/server/routes/filesystem.js +3 -3
  38. package/dist/server/routes/filesystem.js.map +1 -1
  39. package/dist/server/routes/workspaces.js +13 -7
  40. package/dist/server/routes/workspaces.js.map +1 -1
  41. package/dist/server/routes/workspaces.test.js +31 -0
  42. package/dist/server/routes/workspaces.test.js.map +1 -1
  43. package/dist/server/routes/worktrees.js +233 -41
  44. package/dist/server/routes/worktrees.js.map +1 -1
  45. package/dist/server/routes/worktrees.test.js +113 -40
  46. package/dist/server/routes/worktrees.test.js.map +1 -1
  47. package/dist/workspaces/__tests__/git-worktrees.test.js +2 -2
  48. package/dist/workspaces/__tests__/git-worktrees.test.js.map +1 -1
  49. package/dist/workspaces/__tests__/native-worktree-fixture.js +22 -0
  50. package/dist/workspaces/__tests__/native-worktree-fixture.js.map +1 -0
  51. package/dist/workspaces/git-status.test.js +18 -1
  52. package/dist/workspaces/git-status.test.js.map +1 -1
  53. package/dist/workspaces/git-worktrees.js +37 -204
  54. package/dist/workspaces/git-worktrees.js.map +1 -1
  55. package/dist/workspaces/host-opencode-service.test.js +58 -1
  56. package/dist/workspaces/host-opencode-service.test.js.map +1 -1
  57. package/dist/workspaces/instance-events.js +6 -0
  58. package/dist/workspaces/instance-events.js.map +1 -1
  59. package/dist/workspaces/instance-events.test.js +36 -1
  60. package/dist/workspaces/instance-events.test.js.map +1 -1
  61. package/dist/workspaces/manager.js +117 -13
  62. package/dist/workspaces/manager.js.map +1 -1
  63. package/dist/workspaces/manager.test.js +25 -0
  64. package/dist/workspaces/manager.test.js.map +1 -1
  65. package/dist/workspaces/native-service-start.js +13 -0
  66. package/dist/workspaces/native-service-start.js.map +1 -0
  67. package/dist/workspaces/native-service-start.test.js +65 -0
  68. package/dist/workspaces/native-service-start.test.js.map +1 -0
  69. package/dist/workspaces/native-worktrees.js +140 -0
  70. package/dist/workspaces/native-worktrees.js.map +1 -0
  71. package/dist/workspaces/opencode-cli-service.js +10 -2
  72. package/dist/workspaces/opencode-cli-service.js.map +1 -1
  73. package/dist/workspaces/project-session-families.js +376 -0
  74. package/dist/workspaces/project-session-families.js.map +1 -0
  75. package/dist/workspaces/project-session-families.test.js +341 -0
  76. package/dist/workspaces/project-session-families.test.js.map +1 -0
  77. package/dist/workspaces/worktree-directory.js +26 -14
  78. package/dist/workspaces/worktree-directory.js.map +1 -1
  79. package/dist/workspaces/worktree-directory.test.js +35 -5
  80. package/dist/workspaces/worktree-directory.test.js.map +1 -1
  81. package/dist/workspaces/worktree-map.js +2 -5
  82. package/dist/workspaces/worktree-map.js.map +1 -1
  83. package/dist/workspaces/wsl-opencode-service.test.js +16 -2
  84. package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
  85. package/package.json +1 -1
  86. package/public/assets/App-DgGzQRKb.js +54 -0
  87. package/public/assets/App-DgGzQRKb.js.map +1 -0
  88. package/public/assets/{FilesTab-DY3Vr-3r.js → FilesTab-D15d-HIa.js} +3 -3
  89. package/public/assets/{FilesTab-DY3Vr-3r.js.map → FilesTab-D15d-HIa.js.map} +1 -1
  90. package/public/assets/{GitChangesTab-BD6CJngE.js → GitChangesTab-ILUOat5T.js} +3 -3
  91. package/public/assets/{GitChangesTab-BD6CJngE.js.map → GitChangesTab-ILUOat5T.js.map} +1 -1
  92. package/public/assets/{SplitFilePanel-BdY6d_6m.js → SplitFilePanel-sAJUYLjR.js} +2 -2
  93. package/public/assets/{SplitFilePanel-BdY6d_6m.js.map → SplitFilePanel-sAJUYLjR.js.map} +1 -1
  94. package/public/assets/{StatusTab-C-_2yaPa.js → StatusTab-Dd4UVFo5.js} +2 -2
  95. package/public/assets/{StatusTab-C-_2yaPa.js.map → StatusTab-Dd4UVFo5.js.map} +1 -1
  96. package/public/assets/{align-justify-DI8VTZ4G.js → align-justify-CMWGJza-.js} +2 -2
  97. package/public/assets/{align-justify-DI8VTZ4G.js.map → align-justify-CMWGJza-.js.map} +1 -1
  98. package/public/assets/{bundle-full-D_M65Eb_.js → bundle-full-CtBdJfBQ.js} +2 -2
  99. package/public/assets/{bundle-full-D_M65Eb_.js.map → bundle-full-CtBdJfBQ.js.map} +1 -1
  100. package/public/assets/{client-state-CWCzCOz6.js → client-state-CMTuRIal.js} +2 -2
  101. package/public/assets/{client-state-CWCzCOz6.js.map → client-state-CMTuRIal.js.map} +1 -1
  102. package/public/assets/{diff-viewer-CGPsYEr3.js → diff-viewer-BGGYs5qd.js} +2 -2
  103. package/public/assets/{diff-viewer-CGPsYEr3.js.map → diff-viewer-BGGYs5qd.js.map} +1 -1
  104. package/public/assets/{index-Ddjkirek.js → index-BYtkZkA5.js} +2 -2
  105. package/public/assets/{index-Ddjkirek.js.map → index-BYtkZkA5.js.map} +1 -1
  106. package/public/assets/index-C9E9E9Y8.js +2 -0
  107. package/public/assets/index-C9E9E9Y8.js.map +1 -0
  108. package/public/assets/index-CCzMZu6j.js +2 -0
  109. package/public/assets/index-CCzMZu6j.js.map +1 -0
  110. package/public/assets/index-CGQyGbYT.js +2 -0
  111. package/public/assets/index-CGQyGbYT.js.map +1 -0
  112. package/public/assets/{index-MeFffHqb.css → index-CJa0kr_w.css} +1 -1
  113. package/public/assets/index-CZ659znj.js +2 -0
  114. package/public/assets/index-CZ659znj.js.map +1 -0
  115. package/public/assets/index-Cm4KuFap.js +2 -0
  116. package/public/assets/index-Cm4KuFap.js.map +1 -0
  117. package/public/assets/index-D5O7kyo9.js +2 -0
  118. package/public/assets/index-D5O7kyo9.js.map +1 -0
  119. package/public/assets/index-DUcsFhNA.js +3 -0
  120. package/public/assets/index-DUcsFhNA.js.map +1 -0
  121. package/public/assets/index-Dd6l9E3_.js +2 -0
  122. package/public/assets/{index-BxRFHpJd.js.map → index-Dd6l9E3_.js.map} +1 -1
  123. package/public/assets/index-DgwWyAym.js +2 -0
  124. package/public/assets/index-DgwWyAym.js.map +1 -0
  125. package/public/assets/index-fPzgHLK2.js +2 -0
  126. package/public/assets/index-fPzgHLK2.js.map +1 -0
  127. package/public/assets/index-q67t_suc.js +2 -0
  128. package/public/assets/{index-Da9yOa6q.js.map → index-q67t_suc.js.map} +1 -1
  129. package/public/assets/{loading-Gqh6NPUr.js → loading-BpjxwRKQ.js} +2 -2
  130. package/public/assets/{loading-Gqh6NPUr.js.map → loading-BpjxwRKQ.js.map} +1 -1
  131. package/public/assets/main-2LIckgoD.js +42 -0
  132. package/public/assets/main-2LIckgoD.js.map +1 -0
  133. package/public/assets/{markdown-BWnjyvnX.js → markdown-CHmNKfcH.js} +4 -4
  134. package/public/assets/{markdown-BWnjyvnX.js.map → markdown-CHmNKfcH.js.map} +1 -1
  135. package/public/assets/monaco-viewer-Bv1s0h2m.js +27 -0
  136. package/public/assets/monaco-viewer-Bv1s0h2m.js.map +1 -0
  137. package/public/assets/{startup-state-settings-card-C4E2Itop.js → startup-state-settings-card-DXcinWzJ.js} +2 -2
  138. package/public/assets/{startup-state-settings-card-C4E2Itop.js.map → startup-state-settings-card-DXcinWzJ.js.map} +1 -1
  139. package/public/assets/{terminal-square-C3wutdV0.js → terminal-square-BbrDMwZF.js} +2 -2
  140. package/public/assets/{terminal-square-C3wutdV0.js.map → terminal-square-BbrDMwZF.js.map} +1 -1
  141. package/public/assets/tool-call-C_3EDRVw.js +60 -0
  142. package/public/assets/tool-call-C_3EDRVw.js.map +1 -0
  143. package/public/assets/unified-picker-C31HmBaM.js +2 -0
  144. package/public/assets/unified-picker-C31HmBaM.js.map +1 -0
  145. package/public/assets/{wrap-text-CkBxzt04.js → wrap-text-BU8bTcPx.js} +2 -2
  146. package/public/assets/{wrap-text-CkBxzt04.js.map → wrap-text-BU8bTcPx.js.map} +1 -1
  147. package/public/index.html +4 -4
  148. package/public/loading.html +4 -4
  149. package/public/sw.js +1 -1
  150. package/public/sw.js.map +1 -1
  151. package/public/assets/App-BROhwPo9.js +0 -54
  152. package/public/assets/App-BROhwPo9.js.map +0 -1
  153. package/public/assets/index-B8-lAqjr.js +0 -2
  154. package/public/assets/index-B8-lAqjr.js.map +0 -1
  155. package/public/assets/index-BeUewQym.js +0 -2
  156. package/public/assets/index-BeUewQym.js.map +0 -1
  157. package/public/assets/index-BxRFHpJd.js +0 -2
  158. package/public/assets/index-Bzv1kU-S.js +0 -2
  159. package/public/assets/index-Bzv1kU-S.js.map +0 -1
  160. package/public/assets/index-C9tI4CRG.js +0 -2
  161. package/public/assets/index-C9tI4CRG.js.map +0 -1
  162. package/public/assets/index-CGYHFZ6F.js +0 -2
  163. package/public/assets/index-CGYHFZ6F.js.map +0 -1
  164. package/public/assets/index-CMEAjBFp.js +0 -2
  165. package/public/assets/index-CMEAjBFp.js.map +0 -1
  166. package/public/assets/index-Cazypvyc.js +0 -2
  167. package/public/assets/index-Cazypvyc.js.map +0 -1
  168. package/public/assets/index-CwqzofWP.js +0 -2
  169. package/public/assets/index-CwqzofWP.js.map +0 -1
  170. package/public/assets/index-Da9yOa6q.js +0 -2
  171. package/public/assets/index-HHgvEOAH.js +0 -3
  172. package/public/assets/index-HHgvEOAH.js.map +0 -1
  173. package/public/assets/main-Cbx7KdTr.js +0 -38
  174. package/public/assets/main-Cbx7KdTr.js.map +0 -1
  175. package/public/assets/monaco-viewer-BEJIiZWL.js +0 -27
  176. package/public/assets/monaco-viewer-BEJIiZWL.js.map +0 -1
  177. package/public/assets/tool-call-DNsAQ_m4.js +0 -60
  178. package/public/assets/tool-call-DNsAQ_m4.js.map +0 -1
  179. package/public/assets/unified-picker-tB7y17Qz.js +0 -2
  180. package/public/assets/unified-picker-tB7y17Qz.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"api-types.js","sourceRoot":"","sources":["../src/api-types.ts"],"names":[],"mappings":"AAqQA,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AA8E/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,sBAA+B,CAAA","sourcesContent":["import type {\n AgentModelSelection,\n AgentModelSelections,\n ModelPreference,\n OpenCodeBinary,\n Preferences,\n RecentFolder,\n} from \"./config/schema\"\nimport type { OpenCodeEvent } from \"@opencode/client\"\n\n/**\n * Canonical HTTP/SSE contract for the CLI server.\n * These types are consumed by both the CLI implementation and any UI clients.\n */\n\nexport type WorkspaceStatus = \"starting\" | \"ready\" | \"stopped\" | \"error\"\n\nexport interface WorkspaceDescriptor {\n id: string\n /** Correlates creation events with the client request that initiated them. */\n requestId?: string\n /** Absolute path on the server host. */\n path: string\n name?: string\n status: WorkspaceStatus\n /** PID/port are populated when the workspace is running. */\n pid?: number\n port?: number\n /** Canonical proxy path the CLI exposes for this instance. */\n proxyPath: string\n /** Identifier of the binary resolved from config. */\n binaryId: string\n binaryLabel: string\n binaryVersion?: string\n createdAt: string\n updatedAt: string\n /** Present when `status` is \"error\". */\n error?: string\n}\n\nexport interface WorkspaceCreateRequest {\n path: string\n name?: string\n requestId?: string\n}\n\nexport interface WorkspaceCloneRequest {\n repositoryUrl: string\n destinationPath: string\n cleanup?: boolean\n}\n\nexport interface WorkspaceCloneResponse {\n path: string\n}\n\nexport type WorkspaceCreateResponse = WorkspaceDescriptor & {\n /** True when this request did not own creation of the returned workspace. */\n reused?: true\n}\nexport type WorkspaceListResponse = WorkspaceDescriptor[]\nexport type WorkspaceDetailResponse = WorkspaceDescriptor\n\nexport interface WorkspaceDeleteResponse {\n id: string\n status: WorkspaceStatus\n}\n\nexport interface ProviderUsageWindow {\n usedPercent: number | null\n remainingPercent: number | null\n windowSeconds: number | null\n resetAt: number | null\n valueLabel?: string\n}\n\nexport interface ProviderUsageResponse {\n requestedProviderId: string\n providerId: string | null\n providerName: string\n modelId?: string\n supported: boolean\n configured: boolean\n ok: boolean\n windows: Record<string, ProviderUsageWindow>\n fetchedAt: number\n}\n\nexport type WorktreeKind = \"root\" | \"worktree\"\n\nexport interface WorktreeDescriptor {\n /** Stable identifier used by CodeNomad + clients (\"root\" for the selected workspace folder). */\n slug: string\n /** Absolute directory path on the server host. */\n directory: string\n kind: WorktreeKind\n /** Optional VCS branch name when available. */\n branch?: string\n}\n\nexport interface WorktreeListResponse {\n worktrees: WorktreeDescriptor[]\n /** True when the workspace folder resolves to a Git repository. */\n isGitRepo?: boolean\n}\n\nexport interface WorktreeCreateRequest {\n slug: string\n /** Optional branch name (defaults to slug). */\n branch?: string\n}\n\nexport type GitChangeKind = \"added\" | \"modified\" | \"deleted\" | \"renamed\" | \"copied\" | \"untracked\" | \"unmerged\"\n\nexport interface WorktreeGitStatusEntry {\n path: string\n originalPath?: string | null\n stagedStatus: GitChangeKind | null\n stagedAdditions: number\n stagedDeletions: number\n unstagedStatus: GitChangeKind | null\n unstagedAdditions: number\n unstagedDeletions: number\n}\n\nexport type WorktreeGitStatusResponse = WorktreeGitStatusEntry[]\n\nexport type WorktreeGitDiffScope = \"staged\" | \"unstaged\"\n\nexport interface WorktreeGitPathsRequest {\n paths: string[]\n}\n\nexport interface WorktreeGitMutationResponse {\n ok: true\n}\n\nexport interface WorktreeGitCommitRequest {\n message: string\n}\n\nexport interface WorktreeGitCommitResponse {\n ok: true\n commitSha?: string\n}\n\nexport interface WorktreeGitDiffResponse {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n before: string\n after: string\n isBinary?: boolean\n}\n\nexport interface WorktreeGitDiffRequest {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface WorkspaceLogEntry {\n workspaceId: string\n timestamp: string\n level: LogLevel\n message: string\n}\n\nexport interface FileSystemEntry {\n name: string\n /**\n * Path identifier for the entry. Relative to the server root in restricted\n * single-root listings (\".\" represents the root itself); absolute in\n * unrestricted, drives, and multi-root top-level listings.\n */\n path: string\n /** Absolute path when available (unrestricted and multi-root listings). */\n absolutePath?: string\n type: \"file\" | \"directory\"\n size?: number\n /** ISO timestamp of last modification when available. */\n modifiedAt?: string\n}\n\nexport type FileSystemScope = \"restricted\" | \"unrestricted\"\nexport type FileSystemPathKind = \"relative\" | \"absolute\" | \"drives\"\n\nexport interface FileSystemListingMetadata {\n scope: FileSystemScope\n /**\n * Canonical identifier of the current view:\n * - \".\" for restricted single-root listings\n * - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root\n * - absolute path otherwise\n */\n currentPath: string\n /** Optional parent path if navigation upward is allowed. */\n parentPath?: string\n /** Absolute path representing the root or origin point for this listing. */\n rootPath: string\n /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */\n homePath: string\n /** Human-friendly label for the current path. */\n displayPath: string\n /** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */\n pathKind: FileSystemPathKind\n}\n\nexport interface FileSystemListResponse {\n entries: FileSystemEntry[]\n metadata: FileSystemListingMetadata\n}\n\nexport interface FileSystemCreateFolderRequest {\n /**\n * Path identifier for the currently browsed directory.\n * Matches the `path` parameter used for `/api/filesystem`.\n */\n parentPath?: string\n /** Single folder name (no separators). */\n name: string\n}\n\nexport interface FileSystemCreateFolderResponse {\n /**\n * Path identifier that can be passed back to `/api/filesystem` to browse the new folder.\n * Relative for restricted listings and absolute for unrestricted listings.\n */\n path: string\n /** Absolute folder path on the server host. */\n absolutePath: string\n}\n\nexport interface FileSystemFileContentResponse {\n path: string\n contents: string\n encoding: \"utf-8\" | \"base64\"\n}\n\nexport interface ConfigFileDescriptor {\n id: string\n label: string\n path: string\n language: string\n}\n\nexport type ConfigFileListResponse = ConfigFileDescriptor[]\n\nexport interface ConfigFileContentResponse {\n id: string\n path: string\n contents: string\n exists: boolean\n}\n\nexport interface ConfigFileContentRequest {\n contents: string\n}\n\nexport const WINDOWS_DRIVES_ROOT = \"__drives__\"\n\nexport interface WorkspaceFileResponse {\n workspaceId: string\n relativePath: string\n /** UTF-8 file contents; binary files should be base64 encoded by the caller. */\n contents: string\n encoding?: \"utf-8\" | \"base64\"\n}\n\nexport type WorkspaceFileSearchResponse = FileSystemEntry[]\n\nexport interface InstanceData {\n messageHistory: string[]\n agentModelSelections: AgentModelSelection\n}\n\nexport type InstanceStreamStatus = \"connecting\" | \"connected\" | \"error\" | \"disconnected\"\n\nexport type InstanceStreamEvent = OpenCodeEvent\n\nexport type SideCarKind = \"port\"\n\nexport type SideCarPrefixMode = \"strip\" | \"preserve\"\n\nexport type SideCarStatus = \"running\" | \"stopped\"\n\nexport interface SideCar {\n id: string\n kind: SideCarKind\n name: string\n port: number\n insecure: boolean\n prefixMode: SideCarPrefixMode\n status: SideCarStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface PreviewSession {\n token: string\n sessionId: string\n targetUrl: string\n proxyUrl: string\n createdAt: string\n}\n\nexport interface BinaryRecord {\n id: string\n path: string\n label: string\n version?: string\n\n /** Indicates that this binary will be picked when workspaces omit an explicit choice. */\n isDefault: boolean\n lastValidatedAt?: string\n validationError?: string\n}\n\nexport type SettingsOwner = string\nexport type SettingsBucket = Record<string, unknown>\nexport type SettingsDoc = Record<string, unknown>\n\nexport interface BinaryListResponse {\n binaries: BinaryRecord[]\n}\n\nexport interface BinaryCreateRequest {\n path: string\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryUpdateRequest {\n label?: string\n makeDefault?: boolean\n}\n\nexport const OPENCODE_V2_REQUIRED_ERROR_CODE = \"opencode_v2_required\" as const\n\nexport interface BinaryValidationResult {\n valid: boolean\n version?: string\n error?: string\n errorCode?: typeof OPENCODE_V2_REQUIRED_ERROR_CODE\n}\n\nexport interface OpenCodeUpdateStatus {\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean | null\n canUpgrade: boolean\n checkError?: \"update_check_failed\"\n}\n\nexport interface OpenCodeUpdateResponse {\n success: boolean\n version: string\n}\n\nexport interface SpeechSegment {\n startMs: number\n endMs: number\n text: string\n}\n\nexport interface SpeechCapabilitiesResponse {\n available: boolean\n configured: boolean\n provider: string\n supportsStt: boolean\n supportsTts: boolean\n supportsStreamingTts: boolean\n baseUrl?: string\n sttModel: string\n ttsModel: string\n ttsVoice: string\n ttsFormats: string[]\n streamingTtsFormats: string[]\n separateProviders?: boolean\n sttConfigured?: boolean\n ttsConfigured?: boolean\n sttBaseUrl?: string\n ttsBaseUrl?: string\n}\n\nexport interface SpeechTranscriptionResponse {\n text: string\n language?: string\n durationMs?: number\n segments?: SpeechSegment[]\n}\n\nexport interface SpeechSynthesisResponse {\n audioBase64: string\n mimeType: string\n}\n\nexport interface YoloStateResponse {\n enabled: boolean\n}\n\nexport interface RemoteServerProfile {\n id: string\n name: string\n baseUrl: string\n skipTlsVerify: boolean\n createdAt: string\n updatedAt: string\n lastConnectedAt?: string\n}\n\nexport interface RemoteServerProbeRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteServerProbeResponse {\n ok: boolean\n reachable: boolean\n normalizedUrl: string\n skipTlsVerify: boolean\n requiresAuth: boolean\n authenticated: boolean\n error?: string\n errorCode?: string\n}\n\nexport interface RemoteProxySessionCreateRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteProxySessionCreateResponse {\n sessionId: string\n windowUrl: string\n}\n\nexport type WorkspaceEventType =\n | \"workspace.created\"\n | \"workspace.started\"\n | \"workspace.error\"\n | \"workspace.stopped\"\n | \"workspace.log\"\n | \"sidecar.updated\"\n | \"sidecar.removed\"\n | \"storage.configChanged\"\n | \"storage.stateChanged\"\n | \"instance.dataChanged\"\n | \"instance.event\"\n | \"instance.eventStatus\"\n | \"yolo.stateChanged\"\n | \"yolo.autoAccepted\"\n\nexport type WorkspaceEventPayload =\n | { type: \"workspace.created\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.started\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.error\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.stopped\"; workspaceId: string; reason?: \"deleted\" | \"stopped\" }\n | { type: \"workspace.log\"; entry: WorkspaceLogEntry }\n | { type: \"sidecar.updated\"; sidecar: SideCar }\n | { type: \"sidecar.removed\"; sidecarId: string }\n | { type: \"storage.configChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"storage.stateChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"instance.dataChanged\"; instanceId: string; data: InstanceData }\n | { type: \"instance.event\"; instanceId: string; event: InstanceStreamEvent }\n | { type: \"instance.eventStatus\"; instanceId: string; status: InstanceStreamStatus; generation: number; reason?: string }\n | { type: \"yolo.stateChanged\"; instanceId: string; sessionId: string; enabled: boolean }\n | { type: \"yolo.autoAccepted\"; instanceId: string; sessionId: string; permissionId: string }\n\nexport interface NetworkAddress {\n ip: string\n family: \"ipv4\" | \"ipv6\"\n scope: \"external\" | \"internal\" | \"loopback\"\n /** Remote URL using the server's remote protocol/port for this IP. */\n remoteUrl: string\n}\n\nexport interface LatestReleaseInfo {\n version: string\n tag: string\n url: string\n channel: \"stable\" | \"dev\"\n publishedAt?: string\n notes?: string\n}\n\nexport interface UiMeta {\n version?: string\n source: \"bundled\" | \"downloaded\" | \"previous\" | \"override\" | \"dev-proxy\" | \"missing\"\n}\n\nexport interface SupportMeta {\n supported: boolean\n message?: string\n minServerVersion?: string\n latestServerVersion?: string\n latestServerUrl?: string\n}\n\nexport interface ServerMeta {\n /** URL desktop apps should use to connect (prefers loopback HTTP when enabled). */\n localUrl: string\n /** URL remote clients should use (prefers HTTPS when enabled). */\n remoteUrl?: string\n /** SSE endpoint advertised to clients (`/api/events` by default). */\n eventsUrl: string\n /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */\n host: string\n /** Listening mode derived from host binding. */\n listeningMode: \"local\" | \"all\"\n /** Actual local port in use after binding. */\n localPort: number\n /** Actual remote port in use after binding (when remoteUrl is set). */\n remotePort?: number\n /** Display label for the host (e.g., hostname or friendly name). */\n hostLabel: string\n /** Absolute path of the filesystem root exposed to clients. */\n workspaceRoot: string\n /** Reachable addresses for this server, external first. */\n addresses: NetworkAddress[]\n serverVersion?: string\n ui?: UiMeta\n support?: SupportMeta\n /** Optional update info (dev channel only). */\n update?: LatestReleaseInfo | null\n}\n\nexport type {\n Preferences,\n ModelPreference,\n AgentModelSelections,\n RecentFolder,\n OpenCodeBinary,\n}\nexport type { PruneRequest, PruneResult } from \"./opencode/session-pruning/contract\"\n"]}
1
+ {"version":3,"file":"api-types.js","sourceRoot":"","sources":["../src/api-types.ts"],"names":[],"mappings":"AA8RA,MAAM,CAAC,MAAM,mBAAmB,GAAG,YAAY,CAAA;AA8E/C,MAAM,CAAC,MAAM,+BAA+B,GAAG,sBAA+B,CAAA","sourcesContent":["import type {\n AgentModelSelection,\n AgentModelSelections,\n ModelPreference,\n OpenCodeBinary,\n Preferences,\n RecentFolder,\n} from \"./config/schema\"\nimport type { OpenCodeEvent } from \"@opencode/client\"\n\n/**\n * Canonical HTTP/SSE contract for the CLI server.\n * These types are consumed by both the CLI implementation and any UI clients.\n */\n\nexport type WorkspaceStatus = \"starting\" | \"ready\" | \"stopped\" | \"error\"\n\nexport interface WorkspaceDescriptor {\n id: string\n /** Correlates creation events with the client request that initiated them. */\n requestId?: string\n /** Absolute path on the server host. */\n path: string\n name?: string\n status: WorkspaceStatus\n /** PID/port are populated when the workspace is running. */\n pid?: number\n port?: number\n /** Canonical proxy path the CLI exposes for this instance. */\n proxyPath: string\n /** Identifier of the binary resolved from config. */\n binaryId: string\n binaryLabel: string\n binaryVersion?: string\n createdAt: string\n updatedAt: string\n /** Present when `status` is \"error\". */\n error?: string\n}\n\nexport interface WorkspaceCreateRequest {\n path: string\n name?: string\n requestId?: string\n}\n\nexport interface WorkspaceCloneRequest {\n repositoryUrl: string\n destinationPath: string\n cleanup?: boolean\n}\n\nexport interface WorkspaceCloneResponse {\n path: string\n}\n\nexport type WorkspaceCreateResponse = WorkspaceDescriptor & {\n /** True when this request did not own creation of the returned workspace. */\n reused?: true\n}\nexport type WorkspaceListResponse = WorkspaceDescriptor[]\nexport type WorkspaceDetailResponse = WorkspaceDescriptor\n\nexport interface WorkspaceDeleteResponse {\n id: string\n status: WorkspaceStatus\n}\n\nexport interface ProviderUsageWindow {\n usedPercent: number | null\n remainingPercent: number | null\n windowSeconds: number | null\n resetAt: number | null\n valueLabel?: string\n}\n\nexport interface ProviderUsageResponse {\n requestedProviderId: string\n providerId: string | null\n providerName: string\n modelId?: string\n supported: boolean\n configured: boolean\n ok: boolean\n windows: Record<string, ProviderUsageWindow>\n fetchedAt: number\n}\n\nexport type WorktreeKind = \"root\" | \"worktree\"\n\nexport interface WorktreeDescriptor {\n /** Stable identifier used by CodeNomad + clients (\"root\" for the selected workspace folder). */\n slug: string\n /** Presentation only; the worktree identifier does not change with its branch. */\n label?: string\n /** Absolute directory path on the server host. */\n directory: string\n /** Equivalent path in the OpenCode service namespace (notably WSL). */\n serviceDirectory?: string\n /** Native checkout root (before mirroring a nested workspace folder). */\n serviceRoot?: string\n /** Exact path registered in Git's worktree inventory. */\n registeredDirectory?: string\n kind: WorktreeKind\n /** False for the opened folder and Git's main checkout. */\n removable?: boolean\n /** Optional VCS branch name when available. */\n branch?: string\n /** Commit recorded by the Git worktree inventory. */\n head?: string\n}\n\nexport interface WorktreeListResponse {\n worktrees: WorktreeDescriptor[]\n /** Default creation parent in the OpenCode service namespace. */\n defaultDirectory?: string\n /** True when the workspace folder resolves to a Git repository. */\n isGitRepo?: boolean\n}\n\nexport interface WorktreeCreateRequest {\n slug: string\n fromSlug?: string\n /** Optional branch name (defaults to slug). */\n branch?: string\n}\n\nexport interface WorktreeSessionMoveRequest {\n worktreeSlug: string\n}\n\nexport interface WorktreeSessionMoveResponse {\n rootSessionId: string\n sessionIds: string[]\n worktreeSlug: string\n}\n\nexport type GitChangeKind = \"added\" | \"modified\" | \"deleted\" | \"renamed\" | \"copied\" | \"untracked\" | \"unmerged\"\n\nexport interface WorktreeGitStatusEntry {\n path: string\n originalPath?: string | null\n stagedStatus: GitChangeKind | null\n stagedAdditions: number\n stagedDeletions: number\n unstagedStatus: GitChangeKind | null\n unstagedAdditions: number\n unstagedDeletions: number\n}\n\nexport type WorktreeGitStatusResponse = WorktreeGitStatusEntry[]\n\nexport type WorktreeGitDiffScope = \"staged\" | \"unstaged\"\n\nexport interface WorktreeGitPathsRequest {\n paths: string[]\n}\n\nexport interface WorktreeGitMutationResponse {\n ok: true\n}\n\nexport interface WorktreeGitCommitRequest {\n message: string\n}\n\nexport interface WorktreeGitCommitResponse {\n ok: true\n commitSha?: string\n}\n\nexport interface WorktreeGitDiffResponse {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n before: string\n after: string\n isBinary?: boolean\n}\n\nexport interface WorktreeGitDiffRequest {\n path: string\n originalPath?: string | null\n scope: WorktreeGitDiffScope\n}\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\"\n\nexport interface WorkspaceLogEntry {\n workspaceId: string\n timestamp: string\n level: LogLevel\n message: string\n}\n\nexport interface FileSystemEntry {\n name: string\n /**\n * Path identifier for the entry. Relative to the server root in restricted\n * single-root listings (\".\" represents the root itself); absolute in\n * unrestricted, drives, and multi-root top-level listings.\n */\n path: string\n /** Absolute path when available (unrestricted and multi-root listings). */\n absolutePath?: string\n type: \"file\" | \"directory\"\n size?: number\n /** ISO timestamp of last modification when available. */\n modifiedAt?: string\n}\n\nexport type FileSystemScope = \"restricted\" | \"unrestricted\"\nexport type FileSystemPathKind = \"relative\" | \"absolute\" | \"drives\"\n\nexport interface FileSystemListingMetadata {\n scope: FileSystemScope\n /**\n * Canonical identifier of the current view:\n * - \".\" for restricted single-root listings\n * - WINDOWS_DRIVES_ROOT for the Windows drives pseudo-root\n * - absolute path otherwise\n */\n currentPath: string\n /** Optional parent path if navigation upward is allowed. */\n parentPath?: string\n /** Absolute path representing the root or origin point for this listing. */\n rootPath: string\n /** Absolute home directory of the CLI host (useful defaults for unrestricted mode). */\n homePath: string\n /** Human-friendly label for the current path. */\n displayPath: string\n /** Indicates whether entry paths are relative, absolute, or represent the drive pseudo-view. */\n pathKind: FileSystemPathKind\n}\n\nexport interface FileSystemListResponse {\n entries: FileSystemEntry[]\n metadata: FileSystemListingMetadata\n}\n\nexport interface FileSystemCreateFolderRequest {\n /**\n * Path identifier for the currently browsed directory.\n * Matches the `path` parameter used for `/api/filesystem`.\n */\n parentPath?: string\n /** Single folder name (no separators). */\n name: string\n}\n\nexport interface FileSystemCreateFolderResponse {\n /**\n * Path identifier that can be passed back to `/api/filesystem` to browse the new folder.\n * Relative for restricted listings and absolute for unrestricted listings.\n */\n path: string\n /** Absolute folder path on the server host. */\n absolutePath: string\n}\n\nexport interface FileSystemFileContentResponse {\n path: string\n contents: string\n encoding: \"utf-8\" | \"base64\"\n}\n\nexport interface ConfigFileDescriptor {\n id: string\n label: string\n path: string\n language: string\n}\n\nexport type ConfigFileListResponse = ConfigFileDescriptor[]\n\nexport interface ConfigFileContentResponse {\n id: string\n path: string\n contents: string\n exists: boolean\n}\n\nexport interface ConfigFileContentRequest {\n contents: string\n}\n\nexport const WINDOWS_DRIVES_ROOT = \"__drives__\"\n\nexport interface WorkspaceFileResponse {\n workspaceId: string\n relativePath: string\n /** UTF-8 file contents; binary files should be base64 encoded by the caller. */\n contents: string\n encoding?: \"utf-8\" | \"base64\"\n}\n\nexport type WorkspaceFileSearchResponse = FileSystemEntry[]\n\nexport interface InstanceData {\n messageHistory: string[]\n agentModelSelections: AgentModelSelection\n}\n\nexport type InstanceStreamStatus = \"connecting\" | \"connected\" | \"error\" | \"disconnected\"\n\nexport type InstanceStreamEvent = OpenCodeEvent\n\nexport type SideCarKind = \"port\"\n\nexport type SideCarPrefixMode = \"strip\" | \"preserve\"\n\nexport type SideCarStatus = \"running\" | \"stopped\"\n\nexport interface SideCar {\n id: string\n kind: SideCarKind\n name: string\n port: number\n insecure: boolean\n prefixMode: SideCarPrefixMode\n status: SideCarStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface PreviewSession {\n token: string\n sessionId: string\n targetUrl: string\n proxyUrl: string\n createdAt: string\n}\n\nexport interface BinaryRecord {\n id: string\n path: string\n label: string\n version?: string\n\n /** Indicates that this binary will be picked when workspaces omit an explicit choice. */\n isDefault: boolean\n lastValidatedAt?: string\n validationError?: string\n}\n\nexport type SettingsOwner = string\nexport type SettingsBucket = Record<string, unknown>\nexport type SettingsDoc = Record<string, unknown>\n\nexport interface BinaryListResponse {\n binaries: BinaryRecord[]\n}\n\nexport interface BinaryCreateRequest {\n path: string\n label?: string\n makeDefault?: boolean\n}\n\nexport interface BinaryUpdateRequest {\n label?: string\n makeDefault?: boolean\n}\n\nexport const OPENCODE_V2_REQUIRED_ERROR_CODE = \"opencode_v2_required\" as const\n\nexport interface BinaryValidationResult {\n valid: boolean\n version?: string\n error?: string\n errorCode?: typeof OPENCODE_V2_REQUIRED_ERROR_CODE\n}\n\nexport interface OpenCodeUpdateStatus {\n currentVersion: string\n latestVersion: string | null\n updateAvailable: boolean | null\n canUpgrade: boolean\n checkError?: \"update_check_failed\"\n}\n\nexport interface OpenCodeUpdateResponse {\n success: boolean\n version: string\n}\n\nexport interface SpeechSegment {\n startMs: number\n endMs: number\n text: string\n}\n\nexport interface SpeechCapabilitiesResponse {\n available: boolean\n configured: boolean\n provider: string\n supportsStt: boolean\n supportsTts: boolean\n supportsStreamingTts: boolean\n baseUrl?: string\n sttModel: string\n ttsModel: string\n ttsVoice: string\n ttsFormats: string[]\n streamingTtsFormats: string[]\n separateProviders?: boolean\n sttConfigured?: boolean\n ttsConfigured?: boolean\n sttBaseUrl?: string\n ttsBaseUrl?: string\n}\n\nexport interface SpeechTranscriptionResponse {\n text: string\n language?: string\n durationMs?: number\n segments?: SpeechSegment[]\n}\n\nexport interface SpeechSynthesisResponse {\n audioBase64: string\n mimeType: string\n}\n\nexport interface YoloStateResponse {\n enabled: boolean\n}\n\nexport interface RemoteServerProfile {\n id: string\n name: string\n baseUrl: string\n skipTlsVerify: boolean\n createdAt: string\n updatedAt: string\n lastConnectedAt?: string\n}\n\nexport interface RemoteServerProbeRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteServerProbeResponse {\n ok: boolean\n reachable: boolean\n normalizedUrl: string\n skipTlsVerify: boolean\n requiresAuth: boolean\n authenticated: boolean\n error?: string\n errorCode?: string\n}\n\nexport interface RemoteProxySessionCreateRequest {\n baseUrl: string\n skipTlsVerify?: boolean\n}\n\nexport interface RemoteProxySessionCreateResponse {\n sessionId: string\n windowUrl: string\n}\n\nexport type WorkspaceEventType =\n | \"workspace.created\"\n | \"workspace.started\"\n | \"workspace.error\"\n | \"workspace.stopped\"\n | \"workspace.log\"\n | \"sidecar.updated\"\n | \"sidecar.removed\"\n | \"storage.configChanged\"\n | \"storage.stateChanged\"\n | \"instance.dataChanged\"\n | \"instance.event\"\n | \"instance.eventStatus\"\n | \"yolo.stateChanged\"\n | \"yolo.autoAccepted\"\n\nexport type WorkspaceEventPayload =\n | { type: \"workspace.created\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.started\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.error\"; workspace: WorkspaceDescriptor }\n | { type: \"workspace.stopped\"; workspaceId: string; reason?: \"deleted\" | \"stopped\" }\n | { type: \"workspace.log\"; entry: WorkspaceLogEntry }\n | { type: \"sidecar.updated\"; sidecar: SideCar }\n | { type: \"sidecar.removed\"; sidecarId: string }\n | { type: \"storage.configChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"storage.stateChanged\"; owner: SettingsOwner; value: SettingsBucket }\n | { type: \"instance.dataChanged\"; instanceId: string; data: InstanceData }\n | { type: \"instance.event\"; instanceId: string; event: InstanceStreamEvent }\n | { type: \"instance.eventStatus\"; instanceId: string; status: InstanceStreamStatus; generation: number; reason?: string }\n | { type: \"yolo.stateChanged\"; instanceId: string; sessionId: string; enabled: boolean }\n | { type: \"yolo.autoAccepted\"; instanceId: string; sessionId: string; permissionId: string }\n\nexport interface NetworkAddress {\n ip: string\n family: \"ipv4\" | \"ipv6\"\n scope: \"external\" | \"internal\" | \"loopback\"\n /** Remote URL using the server's remote protocol/port for this IP. */\n remoteUrl: string\n}\n\nexport interface LatestReleaseInfo {\n version: string\n tag: string\n url: string\n channel: \"stable\" | \"dev\"\n publishedAt?: string\n notes?: string\n}\n\nexport interface UiMeta {\n version?: string\n source: \"bundled\" | \"downloaded\" | \"previous\" | \"override\" | \"dev-proxy\" | \"missing\"\n}\n\nexport interface SupportMeta {\n supported: boolean\n message?: string\n minServerVersion?: string\n latestServerVersion?: string\n latestServerUrl?: string\n}\n\nexport interface ServerMeta {\n /** URL desktop apps should use to connect (prefers loopback HTTP when enabled). */\n localUrl: string\n /** URL remote clients should use (prefers HTTPS when enabled). */\n remoteUrl?: string\n /** SSE endpoint advertised to clients (`/api/events` by default). */\n eventsUrl: string\n /** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */\n host: string\n /** Listening mode derived from host binding. */\n listeningMode: \"local\" | \"all\"\n /** Actual local port in use after binding. */\n localPort: number\n /** Actual remote port in use after binding (when remoteUrl is set). */\n remotePort?: number\n /** Display label for the host (e.g., hostname or friendly name). */\n hostLabel: string\n /** Absolute path of the filesystem root exposed to clients. */\n workspaceRoot: string\n /** Reachable addresses for this server, external first. */\n addresses: NetworkAddress[]\n serverVersion?: string\n ui?: UiMeta\n support?: SupportMeta\n /** Optional update info (dev channel only). */\n update?: LatestReleaseInfo | null\n}\n\nexport type {\n Preferences,\n ModelPreference,\n AgentModelSelections,\n RecentFolder,\n OpenCodeBinary,\n}\nexport type { PruneRequest, PruneResult } from \"./opencode/session-pruning/contract\"\n"]}
@@ -12,11 +12,11 @@ describe("FileSystemBrowser", () => {
12
12
  fs.rmSync(directory, { recursive: true, force: true });
13
13
  }
14
14
  });
15
- it("starts unrestricted browsing from the configured root", () => {
15
+ it("starts unrestricted browsing from the configured root", async () => {
16
16
  const rootDir = createTempRoot();
17
17
  fs.mkdirSync(path.join(rootDir, "project"));
18
18
  const browser = new FileSystemBrowser({ rootDir, unrestricted: true });
19
- const listing = browser.browse();
19
+ const listing = await browser.browse();
20
20
  assert.equal(listing.metadata.scope, "unrestricted");
21
21
  assert.equal(listing.metadata.currentPath, rootDir);
22
22
  assert.equal(listing.metadata.rootPath, rootDir);
@@ -24,34 +24,34 @@ describe("FileSystemBrowser", () => {
24
24
  assert.equal(listing.metadata.pathKind, "absolute");
25
25
  assert.ok(listing.entries.some((entry) => entry.name === "project" && entry.absolutePath === path.join(rootDir, "project")));
26
26
  });
27
- it("treats dot as the configured unrestricted root", () => {
27
+ it("treats dot as the configured unrestricted root", async () => {
28
28
  const rootDir = createTempRoot();
29
29
  const browser = new FileSystemBrowser({ rootDir, unrestricted: true });
30
- const listing = browser.browse(".");
30
+ const listing = await browser.browse(".");
31
31
  assert.equal(listing.metadata.currentPath, rootDir);
32
32
  assert.equal(listing.metadata.rootPath, rootDir);
33
33
  });
34
- it("allows unrestricted browsing outside the configured root", () => {
34
+ it("allows unrestricted browsing outside the configured root", async () => {
35
35
  const rootDir = createTempRoot();
36
36
  const parentDir = path.dirname(rootDir);
37
37
  const browser = new FileSystemBrowser({ rootDir, unrestricted: true });
38
- const listing = browser.browse(parentDir);
38
+ const listing = await browser.browse(parentDir);
39
39
  assert.equal(listing.metadata.currentPath, parentDir);
40
40
  assert.equal(listing.metadata.rootPath, rootDir);
41
41
  assert.ok(listing.entries.some((entry) => entry.absolutePath === rootDir));
42
42
  });
43
- it("creates folders under the configured unrestricted root by default", () => {
43
+ it("creates folders under the configured unrestricted root by default", async () => {
44
44
  const rootDir = createTempRoot();
45
45
  const browser = new FileSystemBrowser({ rootDir, unrestricted: true });
46
- const created = browser.createFolder(undefined, "created-folder");
46
+ const created = await browser.createFolder(undefined, "created-folder");
47
47
  assert.equal(created.path, path.join(rootDir, "created-folder"));
48
48
  assert.equal(created.absolutePath, path.join(rootDir, "created-folder"));
49
49
  assert.equal(fs.statSync(created.absolutePath).isDirectory(), true);
50
50
  });
51
- it("reports the configured root for the Windows drives pseudo-root", () => {
51
+ it("reports the configured root for the Windows drives pseudo-root", async () => {
52
52
  const rootDir = createTempRoot();
53
53
  const browser = new FileSystemBrowser({ rootDir, unrestricted: true, platform: "win32" });
54
- const listing = browser.browse(WINDOWS_DRIVES_ROOT);
54
+ const listing = await browser.browse(WINDOWS_DRIVES_ROOT);
55
55
  assert.equal(listing.metadata.scope, "unrestricted");
56
56
  assert.equal(listing.metadata.currentPath, WINDOWS_DRIVES_ROOT);
57
57
  assert.equal(listing.metadata.rootPath, rootDir);
@@ -1 +1 @@
1
- {"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/browser.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { afterEach, describe, it } from \"node:test\"\nimport { FileSystemBrowser } from \"../browser\"\nimport { WINDOWS_DRIVES_ROOT } from \"../../api-types\"\n\nconst tempRoots: string[] = []\n\ndescribe(\"FileSystemBrowser\", () => {\n afterEach(() => {\n for (const directory of tempRoots.splice(0)) {\n fs.rmSync(directory, { recursive: true, force: true })\n }\n })\n\n it(\"starts unrestricted browsing from the configured root\", () => {\n const rootDir = createTempRoot()\n fs.mkdirSync(path.join(rootDir, \"project\"))\n\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n const listing = browser.browse()\n\n assert.equal(listing.metadata.scope, \"unrestricted\")\n assert.equal(listing.metadata.currentPath, rootDir)\n assert.equal(listing.metadata.rootPath, rootDir)\n assert.equal(listing.metadata.displayPath, rootDir)\n assert.equal(listing.metadata.pathKind, \"absolute\")\n assert.ok(listing.entries.some((entry) => entry.name === \"project\" && entry.absolutePath === path.join(rootDir, \"project\")))\n })\n\n it(\"treats dot as the configured unrestricted root\", () => {\n const rootDir = createTempRoot()\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n\n const listing = browser.browse(\".\")\n\n assert.equal(listing.metadata.currentPath, rootDir)\n assert.equal(listing.metadata.rootPath, rootDir)\n })\n\n it(\"allows unrestricted browsing outside the configured root\", () => {\n const rootDir = createTempRoot()\n const parentDir = path.dirname(rootDir)\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n\n const listing = browser.browse(parentDir)\n\n assert.equal(listing.metadata.currentPath, parentDir)\n assert.equal(listing.metadata.rootPath, rootDir)\n assert.ok(listing.entries.some((entry) => entry.absolutePath === rootDir))\n })\n\n it(\"creates folders under the configured unrestricted root by default\", () => {\n const rootDir = createTempRoot()\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n\n const created = browser.createFolder(undefined, \"created-folder\")\n\n assert.equal(created.path, path.join(rootDir, \"created-folder\"))\n assert.equal(created.absolutePath, path.join(rootDir, \"created-folder\"))\n assert.equal(fs.statSync(created.absolutePath).isDirectory(), true)\n })\n\n it(\"reports the configured root for the Windows drives pseudo-root\", () => {\n const rootDir = createTempRoot()\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true, platform: \"win32\" })\n\n const listing = browser.browse(WINDOWS_DRIVES_ROOT)\n\n assert.equal(listing.metadata.scope, \"unrestricted\")\n assert.equal(listing.metadata.currentPath, WINDOWS_DRIVES_ROOT)\n assert.equal(listing.metadata.rootPath, rootDir)\n assert.equal(listing.metadata.pathKind, \"drives\")\n })\n})\n\nfunction createTempRoot(): string {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-browser-\"))\n tempRoots.push(root)\n return fs.realpathSync(root)\n}\n"]}
1
+ {"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/browser.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { afterEach, describe, it } from \"node:test\"\nimport { FileSystemBrowser } from \"../browser\"\nimport { WINDOWS_DRIVES_ROOT } from \"../../api-types\"\n\nconst tempRoots: string[] = []\n\ndescribe(\"FileSystemBrowser\", () => {\n afterEach(() => {\n for (const directory of tempRoots.splice(0)) {\n fs.rmSync(directory, { recursive: true, force: true })\n }\n })\n\n it(\"starts unrestricted browsing from the configured root\", async () => {\n const rootDir = createTempRoot()\n fs.mkdirSync(path.join(rootDir, \"project\"))\n\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n const listing = await browser.browse()\n\n assert.equal(listing.metadata.scope, \"unrestricted\")\n assert.equal(listing.metadata.currentPath, rootDir)\n assert.equal(listing.metadata.rootPath, rootDir)\n assert.equal(listing.metadata.displayPath, rootDir)\n assert.equal(listing.metadata.pathKind, \"absolute\")\n assert.ok(listing.entries.some((entry) => entry.name === \"project\" && entry.absolutePath === path.join(rootDir, \"project\")))\n })\n\n it(\"treats dot as the configured unrestricted root\", async () => {\n const rootDir = createTempRoot()\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n\n const listing = await browser.browse(\".\")\n\n assert.equal(listing.metadata.currentPath, rootDir)\n assert.equal(listing.metadata.rootPath, rootDir)\n })\n\n it(\"allows unrestricted browsing outside the configured root\", async () => {\n const rootDir = createTempRoot()\n const parentDir = path.dirname(rootDir)\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n\n const listing = await browser.browse(parentDir)\n\n assert.equal(listing.metadata.currentPath, parentDir)\n assert.equal(listing.metadata.rootPath, rootDir)\n assert.ok(listing.entries.some((entry) => entry.absolutePath === rootDir))\n })\n\n it(\"creates folders under the configured unrestricted root by default\", async () => {\n const rootDir = createTempRoot()\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true })\n\n const created = await browser.createFolder(undefined, \"created-folder\")\n\n assert.equal(created.path, path.join(rootDir, \"created-folder\"))\n assert.equal(created.absolutePath, path.join(rootDir, \"created-folder\"))\n assert.equal(fs.statSync(created.absolutePath).isDirectory(), true)\n })\n\n it(\"reports the configured root for the Windows drives pseudo-root\", async () => {\n const rootDir = createTempRoot()\n const browser = new FileSystemBrowser({ rootDir, unrestricted: true, platform: \"win32\" })\n\n const listing = await browser.browse(WINDOWS_DRIVES_ROOT)\n\n assert.equal(listing.metadata.scope, \"unrestricted\")\n assert.equal(listing.metadata.currentPath, WINDOWS_DRIVES_ROOT)\n assert.equal(listing.metadata.rootPath, rootDir)\n assert.equal(listing.metadata.pathKind, \"drives\")\n })\n})\n\nfunction createTempRoot(): string {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-browser-\"))\n tempRoots.push(root)\n return fs.realpathSync(root)\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { once } from "node:events";
7
+ import { test } from "node:test";
8
+ import Fastify from "fastify";
9
+ import { FileSystemBrowser } from "../browser";
10
+ import { searchWorkspaceFiles } from "../search";
11
+ import { registerFilesystemRoutes } from "../../server/routes/filesystem";
12
+ test("slow directory I/O leaves HTTP and child exit processing responsive", { timeout: 10_000 }, async (t) => {
13
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-slow-io-"));
14
+ fs.writeFileSync(path.join(root, "needle.txt"), "fixture");
15
+ const originalStat = fs.promises.stat.bind(fs.promises);
16
+ let release;
17
+ let entered;
18
+ const stalled = new Promise((resolve) => { entered = resolve; });
19
+ const gate = new Promise((resolve) => { release = resolve; });
20
+ t.mock.method(fs.promises, "stat", async (...args) => {
21
+ entered();
22
+ await gate;
23
+ return originalStat(...args);
24
+ });
25
+ t.mock.method(fs, "statSync", () => { throw new Error("Synchronous filesystem access on HTTP thread"); });
26
+ const app = Fastify();
27
+ registerFilesystemRoutes(app, { fileSystemBrowser: new FileSystemBrowser({ rootDir: root }) });
28
+ app.get("/health", async () => ({ ok: true }));
29
+ await app.ready();
30
+ let finished = false;
31
+ const listing = app.inject("/api/filesystem").then((response) => { finished = true; return response; });
32
+ const search = searchWorkspaceFiles(root, "needle", { refresh: true });
33
+ try {
34
+ await stalled;
35
+ assert.equal((await app.inject("/health")).statusCode, 200);
36
+ const child = spawn(process.execPath, ["-e", "process.exit(0)"], { stdio: "ignore" });
37
+ assert.deepEqual(await once(child, "close"), [0, null]);
38
+ assert.equal(finished, false, "listing must still be waiting for disk I/O");
39
+ release();
40
+ const response = await listing;
41
+ assert.equal(response.statusCode, 200);
42
+ assert.equal(response.json().entries[0].name, "needle.txt");
43
+ assert.equal((await search)[0].name, "needle.txt");
44
+ }
45
+ finally {
46
+ release();
47
+ await Promise.allSettled([listing, search]);
48
+ await app.close();
49
+ t.mock.restoreAll();
50
+ fs.rmSync(root, { recursive: true, force: true });
51
+ }
52
+ });
53
+ test("filesystem routes preserve asynchronous errors and attachment size limits", async () => {
54
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-fs-errors-"));
55
+ const app = Fastify();
56
+ registerFilesystemRoutes(app, { fileSystemBrowser: new FileSystemBrowser({ rootDir: root }) });
57
+ try {
58
+ assert.equal((await app.inject("/api/filesystem?path=missing")).statusCode, 400);
59
+ assert.equal((await app.inject("/api/filesystem/files/content?path=missing")).statusCode, 400);
60
+ const payload = { name: "new-folder" };
61
+ assert.equal((await app.inject({ method: "POST", url: "/api/filesystem/folders", payload })).statusCode, 201);
62
+ assert.equal((await app.inject({ method: "POST", url: "/api/filesystem/folders", payload })).statusCode, 409);
63
+ const fd = fs.openSync(path.join(root, "large.sqlite"), "w");
64
+ try {
65
+ fs.ftruncateSync(fd, 6 * 1024 * 1024);
66
+ }
67
+ finally {
68
+ fs.closeSync(fd);
69
+ }
70
+ const response = await app.inject("/api/filesystem/files/content?path=large.sqlite");
71
+ assert.equal(response.statusCode, 400);
72
+ assert.match(response.body, /too large/);
73
+ }
74
+ finally {
75
+ await app.close();
76
+ fs.rmSync(root, { recursive: true, force: true });
77
+ }
78
+ });
79
+ //# sourceMappingURL=responsiveness.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responsiveness.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/responsiveness.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { spawn } from \"node:child_process\"\nimport { once } from \"node:events\"\nimport { test } from \"node:test\"\nimport Fastify from \"fastify\"\nimport { FileSystemBrowser } from \"../browser\"\nimport { searchWorkspaceFiles } from \"../search\"\nimport { registerFilesystemRoutes } from \"../../server/routes/filesystem\"\n\ntest(\"slow directory I/O leaves HTTP and child exit processing responsive\", { timeout: 10_000 }, async (t) => {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-slow-io-\"))\n fs.writeFileSync(path.join(root, \"needle.txt\"), \"fixture\")\n const originalStat = fs.promises.stat.bind(fs.promises)\n let release!: () => void\n let entered!: () => void\n const stalled = new Promise<void>((resolve) => { entered = resolve })\n const gate = new Promise<void>((resolve) => { release = resolve })\n t.mock.method(fs.promises, \"stat\", async (...args: Parameters<typeof fs.promises.stat>) => {\n entered()\n await gate\n return originalStat(...args)\n })\n t.mock.method(fs, \"statSync\", () => { throw new Error(\"Synchronous filesystem access on HTTP thread\") })\n const app = Fastify()\n registerFilesystemRoutes(app, { fileSystemBrowser: new FileSystemBrowser({ rootDir: root }) })\n app.get(\"/health\", async () => ({ ok: true }))\n await app.ready()\n let finished = false\n const listing = app.inject(\"/api/filesystem\").then((response) => { finished = true; return response })\n const search = searchWorkspaceFiles(root, \"needle\", { refresh: true })\n try {\n await stalled\n assert.equal((await app.inject(\"/health\")).statusCode, 200)\n const child = spawn(process.execPath, [\"-e\", \"process.exit(0)\"], { stdio: \"ignore\" })\n assert.deepEqual(await once(child, \"close\"), [0, null])\n assert.equal(finished, false, \"listing must still be waiting for disk I/O\")\n release()\n const response = await listing\n assert.equal(response.statusCode, 200)\n assert.equal(response.json().entries[0].name, \"needle.txt\")\n assert.equal((await search)[0].name, \"needle.txt\")\n } finally {\n release()\n await Promise.allSettled([listing, search])\n await app.close()\n t.mock.restoreAll()\n fs.rmSync(root, { recursive: true, force: true })\n }\n})\n\ntest(\"filesystem routes preserve asynchronous errors and attachment size limits\", async () => {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-fs-errors-\"))\n const app = Fastify()\n registerFilesystemRoutes(app, { fileSystemBrowser: new FileSystemBrowser({ rootDir: root }) })\n try {\n assert.equal((await app.inject(\"/api/filesystem?path=missing\")).statusCode, 400)\n assert.equal((await app.inject(\"/api/filesystem/files/content?path=missing\")).statusCode, 400)\n const payload = { name: \"new-folder\" }\n assert.equal((await app.inject({ method: \"POST\", url: \"/api/filesystem/folders\", payload })).statusCode, 201)\n assert.equal((await app.inject({ method: \"POST\", url: \"/api/filesystem/folders\", payload })).statusCode, 409)\n const fd = fs.openSync(path.join(root, \"large.sqlite\"), \"w\")\n try { fs.ftruncateSync(fd, 6 * 1024 * 1024) } finally { fs.closeSync(fd) }\n const response = await app.inject(\"/api/filesystem/files/content?path=large.sqlite\")\n assert.equal(response.statusCode, 400)\n assert.match(response.body, /too large/)\n } finally {\n await app.close()\n fs.rmSync(root, { recursive: true, force: true })\n }\n})\n"]}
@@ -1,14 +1,53 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { beforeEach, describe, it } from "node:test";
3
- import { clearWorkspaceSearchCache, getWorkspaceCandidates, refreshWorkspaceCandidates, WORKSPACE_CANDIDATE_CACHE_TTL_MS, } from "../search-cache";
3
+ import { searchWorkspaceFiles } from "../search";
4
+ import { clearWorkspaceSearchCache, getWorkspaceCandidates, refreshWorkspaceCandidates, WORKSPACE_CANDIDATE_CACHE_TTL_MS, WorkspaceSearchBusyError, } from "../search-cache";
4
5
  describe("workspace search cache", () => {
5
6
  beforeEach(() => {
6
7
  clearWorkspaceSearchCache();
7
8
  });
8
- it("expires cached candidates after the TTL", () => {
9
+ it("a rejected extra query does not invalidate the successful in-flight scans", async () => {
10
+ let release;
11
+ const gate = new Promise(resolve => { release = resolve; });
12
+ const first = refreshWorkspaceCandidates("/busy-root", "file\0first", () => gate);
13
+ const second = refreshWorkspaceCandidates("/another-root", "file\0second", () => gate);
14
+ try {
15
+ await assert.rejects(searchWorkspaceFiles("/busy-root", "third"), WorkspaceSearchBusyError);
16
+ }
17
+ finally {
18
+ release([createEntry("needle")]);
19
+ await Promise.all([first, second]);
20
+ }
21
+ assert.equal(getWorkspaceCandidates("/busy-root", "file\0first")?.[0].name, "needle");
22
+ });
23
+ it("coalesces scans, bounds parallel I/O and does not refill an invalidated cache", async () => {
24
+ let release;
25
+ const gate = new Promise((resolve) => { release = resolve; });
26
+ let calls = 0;
27
+ const builder = () => { calls += 1; return gate; };
28
+ const first = refreshWorkspaceCandidates("/workspace-one", "a", builder);
29
+ const duplicate = refreshWorkspaceCandidates("/workspace-one", "a", builder);
30
+ const second = refreshWorkspaceCandidates("/workspace-two", "b", builder);
31
+ try {
32
+ await assert.rejects(refreshWorkspaceCandidates("/workspace-three", "c", builder), WorkspaceSearchBusyError);
33
+ assert.equal(calls, 2);
34
+ clearWorkspaceSearchCache("/workspace-one");
35
+ }
36
+ finally {
37
+ release([createEntry("needle")]);
38
+ }
39
+ const results = await Promise.all([first, duplicate, second]);
40
+ results[0][0].name = "mutated";
41
+ assert.equal(results[1][0].name, "needle");
42
+ assert.equal(getWorkspaceCandidates("/workspace-one", "a"), undefined);
43
+ assert.equal(getWorkspaceCandidates("/workspace-two", "b")?.[0].name, "needle");
44
+ await assert.rejects(refreshWorkspaceCandidates("/workspace-three", "c", () => Promise.reject(new Error("disk"))), /disk/);
45
+ assert.equal((await refreshWorkspaceCandidates("/workspace-three", "c", () => [createEntry("recovered")]))[0].name, "recovered");
46
+ });
47
+ it("expires cached candidates after the TTL", async () => {
9
48
  const workspacePath = "/tmp/workspace";
10
49
  const startTime = 1_000;
11
- refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-a")], startTime);
50
+ await refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-a")], startTime);
12
51
  const beforeExpiry = getWorkspaceCandidates(workspacePath, "query-a", startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS - 1);
13
52
  assert.ok(beforeExpiry);
14
53
  assert.equal(beforeExpiry.length, 1);
@@ -16,23 +55,23 @@ describe("workspace search cache", () => {
16
55
  const afterExpiry = getWorkspaceCandidates(workspacePath, "query-a", startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS + 1);
17
56
  assert.equal(afterExpiry, undefined);
18
57
  });
19
- it("replaces cached entries when manually refreshed", () => {
58
+ it("replaces cached entries when manually refreshed", async () => {
20
59
  const workspacePath = "/tmp/workspace";
21
- refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-a")], 5_000);
60
+ await refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-a")], 5_000);
22
61
  const initial = getWorkspaceCandidates(workspacePath, "query-a", 5_001);
23
62
  assert.ok(initial);
24
63
  assert.equal(initial[0].name, "file-a");
25
- refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-b")], 6_000);
64
+ await refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-b")], 6_000);
26
65
  const refreshed = getWorkspaceCandidates(workspacePath, "query-a", 6_001);
27
66
  assert.ok(refreshed);
28
67
  assert.equal(refreshed[0].name, "file-b");
29
68
  });
30
- it("does not reuse candidates across query scopes", () => {
69
+ it("does not reuse candidates across query scopes", async () => {
31
70
  const workspacePath = "/tmp/workspace";
32
- refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-a")], 5_000);
71
+ await refreshWorkspaceCandidates(workspacePath, "query-a", () => [createEntry("file-a")], 5_000);
33
72
  assert.equal(getWorkspaceCandidates(workspacePath, "query-a", 5_001)?.[0].name, "file-a");
34
73
  assert.equal(getWorkspaceCandidates(workspacePath, "query-b", 5_001), undefined);
35
- refreshWorkspaceCandidates(workspacePath, "query-b", () => [createEntry("file-b")], 5_000);
74
+ await refreshWorkspaceCandidates(workspacePath, "query-b", () => [createEntry("file-b")], 5_000);
36
75
  assert.equal(getWorkspaceCandidates(workspacePath, "query-a", 5_001), undefined);
37
76
  assert.equal(getWorkspaceCandidates(workspacePath, "query-b", 5_001)?.[0].name, "file-b");
38
77
  clearWorkspaceSearchCache(workspacePath);
@@ -1 +1 @@
1
- {"version":3,"file":"search-cache.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/search-cache.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { beforeEach, describe, it } from \"node:test\"\nimport type { FileSystemEntry } from \"../../api-types\"\nimport {\n clearWorkspaceSearchCache,\n getWorkspaceCandidates,\n refreshWorkspaceCandidates,\n WORKSPACE_CANDIDATE_CACHE_TTL_MS,\n} from \"../search-cache\"\n\ndescribe(\"workspace search cache\", () => {\n beforeEach(() => {\n clearWorkspaceSearchCache()\n })\n\n it(\"expires cached candidates after the TTL\", () => {\n const workspacePath = \"/tmp/workspace\"\n const startTime = 1_000\n\n refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-a\")], startTime)\n\n const beforeExpiry = getWorkspaceCandidates(\n workspacePath,\n \"query-a\",\n startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS - 1,\n )\n assert.ok(beforeExpiry)\n assert.equal(beforeExpiry.length, 1)\n assert.equal(beforeExpiry[0].name, \"file-a\")\n\n const afterExpiry = getWorkspaceCandidates(\n workspacePath,\n \"query-a\",\n startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS + 1,\n )\n assert.equal(afterExpiry, undefined)\n })\n\n it(\"replaces cached entries when manually refreshed\", () => {\n const workspacePath = \"/tmp/workspace\"\n\n refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-a\")], 5_000)\n const initial = getWorkspaceCandidates(workspacePath, \"query-a\", 5_001)\n assert.ok(initial)\n assert.equal(initial[0].name, \"file-a\")\n\n refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-b\")], 6_000)\n const refreshed = getWorkspaceCandidates(workspacePath, \"query-a\", 6_001)\n assert.ok(refreshed)\n assert.equal(refreshed[0].name, \"file-b\")\n })\n\n it(\"does not reuse candidates across query scopes\", () => {\n const workspacePath = \"/tmp/workspace\"\n\n refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-a\")], 5_000)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-a\", 5_001)?.[0].name, \"file-a\")\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-b\", 5_001), undefined)\n\n refreshWorkspaceCandidates(workspacePath, \"query-b\", () => [createEntry(\"file-b\")], 5_000)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-a\", 5_001), undefined)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-b\", 5_001)?.[0].name, \"file-b\")\n\n clearWorkspaceSearchCache(workspacePath)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-a\", 5_001), undefined)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-b\", 5_001), undefined)\n })\n})\n\nfunction createEntry(name: string): FileSystemEntry {\n return {\n name,\n path: name,\n absolutePath: `/tmp/${name}`,\n type: \"file\",\n size: 1,\n modifiedAt: new Date().toISOString(),\n }\n}\n"]}
1
+ {"version":3,"file":"search-cache.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/search-cache.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EACL,yBAAyB,EACzB,sBAAsB,EACtB,0BAA0B,EAC1B,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,iBAAiB,CAAA;AAExB,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,UAAU,CAAC,GAAG,EAAE;QACd,yBAAyB,EAAE,CAAA;IAC7B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;QACzF,IAAI,OAA8C,CAAA;QAClD,MAAM,IAAI,GAAG,IAAI,OAAO,CAAoB,OAAO,CAAC,EAAE,GAAG,OAAO,GAAG,OAAO,CAAA,CAAC,CAAC,CAAC,CAAA;QAC7E,MAAM,KAAK,GAAG,0BAA0B,CAAC,YAAY,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;QACjF,MAAM,MAAM,GAAG,0BAA0B,CAAC,eAAe,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;QACtF,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,YAAY,EAAE,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAA;QAC7F,CAAC;gBAAS,CAAC;YAAC,OAAO,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAAC,CAAC;QAClF,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,YAAY,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACvF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,IAAI,OAA8C,CAAA;QAClD,MAAM,IAAI,GAAG,IAAI,OAAO,CAAoB,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,GAAG,OAAO,CAAA,CAAC,CAAC,CAAC,CAAA;QAC/E,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,MAAM,OAAO,GAAG,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAA,CAAC,CAAC,CAAA;QACjD,MAAM,KAAK,GAAG,0BAA0B,CAAC,gBAAgB,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;QACxE,MAAM,SAAS,GAAG,0BAA0B,CAAC,gBAAgB,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5E,MAAM,MAAM,GAAG,0BAA0B,CAAC,gBAAgB,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;QACzE,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,CAAC,0BAA0B,CAAC,kBAAkB,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAA;YAC5G,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;YACtB,yBAAyB,CAAC,gBAAgB,CAAC,CAAA;QAC7C,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;QAClC,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAA;QAC7D,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;QAC9B,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC1C,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,SAAS,CAAC,CAAA;QACtE,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAC/E,MAAM,MAAM,CAAC,OAAO,CAAC,0BAA0B,CAAC,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;QAC1H,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,0BAA0B,CAAC,kBAAkB,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;IAClI,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,aAAa,GAAG,gBAAgB,CAAA;QACtC,MAAM,SAAS,GAAG,KAAK,CAAA;QAEvB,MAAM,0BAA0B,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;QAEpG,MAAM,YAAY,GAAG,sBAAsB,CACzC,aAAa,EACb,SAAS,EACT,SAAS,GAAG,gCAAgC,GAAG,CAAC,CACjD,CAAA;QACD,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAA;QACvB,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;QACpC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAE5C,MAAM,WAAW,GAAG,sBAAsB,CACxC,aAAa,EACb,SAAS,EACT,SAAS,GAAG,gCAAgC,GAAG,CAAC,CACjD,CAAA;QACD,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,aAAa,GAAG,gBAAgB,CAAA;QAEtC,MAAM,0BAA0B,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QAChG,MAAM,OAAO,GAAG,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QACvE,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAA;QAClB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAEvC,MAAM,0BAA0B,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QAChG,MAAM,SAAS,GAAG,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QACzE,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAA;QACpB,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,aAAa,GAAG,gBAAgB,CAAA;QAEtC,MAAM,0BAA0B,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QAChG,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QACzF,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,SAAS,CAAC,CAAA;QAEhF,MAAM,0BAA0B,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QAChG,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,SAAS,CAAC,CAAA;QAChF,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAEzF,yBAAyB,CAAC,aAAa,CAAC,CAAA;QACxC,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,SAAS,CAAC,CAAA;QAChF,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,aAAa,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,SAAS,CAAC,CAAA;IAClF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,IAAI;QACV,YAAY,EAAE,QAAQ,IAAI,EAAE;QAC5B,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,CAAC;QACP,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAA;AACH,CAAC","sourcesContent":["import assert from \"node:assert/strict\"\nimport { beforeEach, describe, it } from \"node:test\"\nimport type { FileSystemEntry } from \"../../api-types\"\nimport { searchWorkspaceFiles } from \"../search\"\nimport {\n clearWorkspaceSearchCache,\n getWorkspaceCandidates,\n refreshWorkspaceCandidates,\n WORKSPACE_CANDIDATE_CACHE_TTL_MS,\n WorkspaceSearchBusyError,\n} from \"../search-cache\"\n\ndescribe(\"workspace search cache\", () => {\n beforeEach(() => {\n clearWorkspaceSearchCache()\n })\n\n it(\"a rejected extra query does not invalidate the successful in-flight scans\", async () => {\n let release!: (entries: FileSystemEntry[]) => void\n const gate = new Promise<FileSystemEntry[]>(resolve => { release = resolve })\n const first = refreshWorkspaceCandidates(\"/busy-root\", \"file\\0first\", () => gate)\n const second = refreshWorkspaceCandidates(\"/another-root\", \"file\\0second\", () => gate)\n try {\n await assert.rejects(searchWorkspaceFiles(\"/busy-root\", \"third\"), WorkspaceSearchBusyError)\n } finally { release([createEntry(\"needle\")]); await Promise.all([first, second]) }\n assert.equal(getWorkspaceCandidates(\"/busy-root\", \"file\\0first\")?.[0].name, \"needle\")\n })\n\n it(\"coalesces scans, bounds parallel I/O and does not refill an invalidated cache\", async () => {\n let release!: (entries: FileSystemEntry[]) => void\n const gate = new Promise<FileSystemEntry[]>((resolve) => { release = resolve })\n let calls = 0\n const builder = () => { calls += 1; return gate }\n const first = refreshWorkspaceCandidates(\"/workspace-one\", \"a\", builder)\n const duplicate = refreshWorkspaceCandidates(\"/workspace-one\", \"a\", builder)\n const second = refreshWorkspaceCandidates(\"/workspace-two\", \"b\", builder)\n try {\n await assert.rejects(refreshWorkspaceCandidates(\"/workspace-three\", \"c\", builder), WorkspaceSearchBusyError)\n assert.equal(calls, 2)\n clearWorkspaceSearchCache(\"/workspace-one\")\n } finally {\n release([createEntry(\"needle\")])\n }\n const results = await Promise.all([first, duplicate, second])\n results[0][0].name = \"mutated\"\n assert.equal(results[1][0].name, \"needle\")\n assert.equal(getWorkspaceCandidates(\"/workspace-one\", \"a\"), undefined)\n assert.equal(getWorkspaceCandidates(\"/workspace-two\", \"b\")?.[0].name, \"needle\")\n await assert.rejects(refreshWorkspaceCandidates(\"/workspace-three\", \"c\", () => Promise.reject(new Error(\"disk\"))), /disk/)\n assert.equal((await refreshWorkspaceCandidates(\"/workspace-three\", \"c\", () => [createEntry(\"recovered\")]))[0].name, \"recovered\")\n })\n\n it(\"expires cached candidates after the TTL\", async () => {\n const workspacePath = \"/tmp/workspace\"\n const startTime = 1_000\n\n await refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-a\")], startTime)\n\n const beforeExpiry = getWorkspaceCandidates(\n workspacePath,\n \"query-a\",\n startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS - 1,\n )\n assert.ok(beforeExpiry)\n assert.equal(beforeExpiry.length, 1)\n assert.equal(beforeExpiry[0].name, \"file-a\")\n\n const afterExpiry = getWorkspaceCandidates(\n workspacePath,\n \"query-a\",\n startTime + WORKSPACE_CANDIDATE_CACHE_TTL_MS + 1,\n )\n assert.equal(afterExpiry, undefined)\n })\n\n it(\"replaces cached entries when manually refreshed\", async () => {\n const workspacePath = \"/tmp/workspace\"\n\n await refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-a\")], 5_000)\n const initial = getWorkspaceCandidates(workspacePath, \"query-a\", 5_001)\n assert.ok(initial)\n assert.equal(initial[0].name, \"file-a\")\n\n await refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-b\")], 6_000)\n const refreshed = getWorkspaceCandidates(workspacePath, \"query-a\", 6_001)\n assert.ok(refreshed)\n assert.equal(refreshed[0].name, \"file-b\")\n })\n\n it(\"does not reuse candidates across query scopes\", async () => {\n const workspacePath = \"/tmp/workspace\"\n\n await refreshWorkspaceCandidates(workspacePath, \"query-a\", () => [createEntry(\"file-a\")], 5_000)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-a\", 5_001)?.[0].name, \"file-a\")\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-b\", 5_001), undefined)\n\n await refreshWorkspaceCandidates(workspacePath, \"query-b\", () => [createEntry(\"file-b\")], 5_000)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-a\", 5_001), undefined)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-b\", 5_001)?.[0].name, \"file-b\")\n\n clearWorkspaceSearchCache(workspacePath)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-a\", 5_001), undefined)\n assert.equal(getWorkspaceCandidates(workspacePath, \"query-b\", 5_001), undefined)\n })\n})\n\nfunction createEntry(name: string): FileSystemEntry {\n return {\n name,\n path: name,\n absolutePath: `/tmp/${name}`,\n type: \"file\",\n size: 1,\n modifiedAt: new Date().toISOString(),\n }\n}\n"]}
@@ -7,7 +7,7 @@ import { searchWorkspaceFiles } from "../search";
7
7
  import { getWorkspaceCandidates } from "../search-cache";
8
8
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-search-"));
9
9
  after(() => fs.rmSync(workspace, { recursive: true, force: true }));
10
- test("finds a matching file after more than 8000 non-matching entries", () => {
10
+ test("finds a matching file after more than 8000 non-matching entries", async () => {
11
11
  fs.mkdirSync(path.join(workspace, "a"));
12
12
  fs.mkdirSync(path.join(workspace, "b"));
13
13
  const [targetDirName, fillerDirName] = fs.readdirSync(workspace);
@@ -17,33 +17,33 @@ test("finds a matching file after more than 8000 non-matching entries", () => {
17
17
  fs.writeFileSync(path.join(fillerDir, `filler-${index}.txt`), "");
18
18
  }
19
19
  fs.writeFileSync(path.join(targetDir, "unique-search-target.txt"), "");
20
- const results = searchWorkspaceFiles(workspace, "unique-search-target", {
20
+ const results = await searchWorkspaceFiles(workspace, "unique-search-target", {
21
21
  type: "file",
22
22
  refresh: true,
23
23
  });
24
24
  assert.equal(results.some((entry) => entry.name === "unique-search-target.txt"), true);
25
- searchWorkspaceFiles(workspace, "filler", { type: "file", refresh: true });
25
+ await searchWorkspaceFiles(workspace, "filler", { type: "file", refresh: true });
26
26
  assert.equal(getWorkspaceCandidates(workspace, "file\0filler")?.length, 8_000);
27
27
  });
28
- test("does not revisit directory links", () => {
28
+ test("does not revisit directory links", async () => {
29
29
  const cyclicWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-search-cycle-"));
30
30
  try {
31
31
  fs.symlinkSync(cyclicWorkspace, path.join(cyclicWorkspace, "cycle"), process.platform === "win32" ? "junction" : "dir");
32
- assert.deepEqual(searchWorkspaceFiles(cyclicWorkspace, "not-present", { refresh: true }), []);
32
+ assert.deepEqual(await searchWorkspaceFiles(cyclicWorkspace, "not-present", { refresh: true }), []);
33
33
  }
34
34
  finally {
35
35
  fs.rmSync(cyclicWorkspace, { recursive: true, force: true });
36
36
  }
37
37
  });
38
- test("indexes both real and linked directory paths", () => {
38
+ test("indexes both real and linked directory paths", async () => {
39
39
  const linkedWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-search-link-"));
40
40
  try {
41
41
  const realDirectory = path.join(linkedWorkspace, "b");
42
42
  fs.mkdirSync(realDirectory);
43
43
  fs.writeFileSync(path.join(realDirectory, "needle.txt"), "");
44
44
  fs.symlinkSync(realDirectory, path.join(linkedWorkspace, "a"), process.platform === "win32" ? "junction" : "dir");
45
- const linkedResults = searchWorkspaceFiles(linkedWorkspace, "a/needle", { type: "file", refresh: true });
46
- const realResults = searchWorkspaceFiles(linkedWorkspace, "b/needle", { type: "file", refresh: true });
45
+ const linkedResults = await searchWorkspaceFiles(linkedWorkspace, "a/needle", { type: "file", refresh: true });
46
+ const realResults = await searchWorkspaceFiles(linkedWorkspace, "b/needle", { type: "file", refresh: true });
47
47
  assert.equal(linkedResults.some((entry) => entry.path === "a/needle.txt"), true);
48
48
  assert.equal(realResults.some((entry) => entry.path === "b/needle.txt"), true);
49
49
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/search.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { after, test } from \"node:test\"\nimport { searchWorkspaceFiles } from \"../search\"\nimport { getWorkspaceCandidates } from \"../search-cache\"\n\nconst workspace = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-search-\"))\n\nafter(() => fs.rmSync(workspace, { recursive: true, force: true }))\n\ntest(\"finds a matching file after more than 8000 non-matching entries\", () => {\n fs.mkdirSync(path.join(workspace, \"a\"))\n fs.mkdirSync(path.join(workspace, \"b\"))\n\n const [targetDirName, fillerDirName] = fs.readdirSync(workspace)\n const fillerDir = path.join(workspace, fillerDirName)\n const targetDir = path.join(workspace, targetDirName)\n\n for (let index = 0; index < 8_001; index += 1) {\n fs.writeFileSync(path.join(fillerDir, `filler-${index}.txt`), \"\")\n }\n fs.writeFileSync(path.join(targetDir, \"unique-search-target.txt\"), \"\")\n\n const results = searchWorkspaceFiles(workspace, \"unique-search-target\", {\n type: \"file\",\n refresh: true,\n })\n\n assert.equal(results.some((entry) => entry.name === \"unique-search-target.txt\"), true)\n\n searchWorkspaceFiles(workspace, \"filler\", { type: \"file\", refresh: true })\n assert.equal(getWorkspaceCandidates(workspace, \"file\\0filler\")?.length, 8_000)\n})\n\ntest(\"does not revisit directory links\", () => {\n const cyclicWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-search-cycle-\"))\n try {\n fs.symlinkSync(cyclicWorkspace, path.join(cyclicWorkspace, \"cycle\"), process.platform === \"win32\" ? \"junction\" : \"dir\")\n assert.deepEqual(searchWorkspaceFiles(cyclicWorkspace, \"not-present\", { refresh: true }), [])\n } finally {\n fs.rmSync(cyclicWorkspace, { recursive: true, force: true })\n }\n})\n\ntest(\"indexes both real and linked directory paths\", () => {\n const linkedWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-search-link-\"))\n try {\n const realDirectory = path.join(linkedWorkspace, \"b\")\n fs.mkdirSync(realDirectory)\n fs.writeFileSync(path.join(realDirectory, \"needle.txt\"), \"\")\n fs.symlinkSync(realDirectory, path.join(linkedWorkspace, \"a\"), process.platform === \"win32\" ? \"junction\" : \"dir\")\n\n const linkedResults = searchWorkspaceFiles(linkedWorkspace, \"a/needle\", { type: \"file\", refresh: true })\n const realResults = searchWorkspaceFiles(linkedWorkspace, \"b/needle\", { type: \"file\", refresh: true })\n\n assert.equal(linkedResults.some((entry) => entry.path === \"a/needle.txt\"), true)\n assert.equal(realResults.some((entry) => entry.path === \"b/needle.txt\"), true)\n } finally {\n fs.rmSync(linkedWorkspace, { recursive: true, force: true })\n }\n})\n"]}
1
+ {"version":3,"file":"search.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/search.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { after, test } from \"node:test\"\nimport { searchWorkspaceFiles } from \"../search\"\nimport { getWorkspaceCandidates } from \"../search-cache\"\n\nconst workspace = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-search-\"))\n\nafter(() => fs.rmSync(workspace, { recursive: true, force: true }))\n\ntest(\"finds a matching file after more than 8000 non-matching entries\", async () => {\n fs.mkdirSync(path.join(workspace, \"a\"))\n fs.mkdirSync(path.join(workspace, \"b\"))\n\n const [targetDirName, fillerDirName] = fs.readdirSync(workspace)\n const fillerDir = path.join(workspace, fillerDirName)\n const targetDir = path.join(workspace, targetDirName)\n\n for (let index = 0; index < 8_001; index += 1) {\n fs.writeFileSync(path.join(fillerDir, `filler-${index}.txt`), \"\")\n }\n fs.writeFileSync(path.join(targetDir, \"unique-search-target.txt\"), \"\")\n\n const results = await searchWorkspaceFiles(workspace, \"unique-search-target\", {\n type: \"file\",\n refresh: true,\n })\n\n assert.equal(results.some((entry) => entry.name === \"unique-search-target.txt\"), true)\n\n await searchWorkspaceFiles(workspace, \"filler\", { type: \"file\", refresh: true })\n assert.equal(getWorkspaceCandidates(workspace, \"file\\0filler\")?.length, 8_000)\n})\n\ntest(\"does not revisit directory links\", async () => {\n const cyclicWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-search-cycle-\"))\n try {\n fs.symlinkSync(cyclicWorkspace, path.join(cyclicWorkspace, \"cycle\"), process.platform === \"win32\" ? \"junction\" : \"dir\")\n assert.deepEqual(await searchWorkspaceFiles(cyclicWorkspace, \"not-present\", { refresh: true }), [])\n } finally {\n fs.rmSync(cyclicWorkspace, { recursive: true, force: true })\n }\n})\n\ntest(\"indexes both real and linked directory paths\", async () => {\n const linkedWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-search-link-\"))\n try {\n const realDirectory = path.join(linkedWorkspace, \"b\")\n fs.mkdirSync(realDirectory)\n fs.writeFileSync(path.join(realDirectory, \"needle.txt\"), \"\")\n fs.symlinkSync(realDirectory, path.join(linkedWorkspace, \"a\"), process.platform === \"win32\" ? \"junction\" : \"dir\")\n\n const linkedResults = await searchWorkspaceFiles(linkedWorkspace, \"a/needle\", { type: \"file\", refresh: true })\n const realResults = await searchWorkspaceFiles(linkedWorkspace, \"b/needle\", { type: \"file\", refresh: true })\n\n assert.equal(linkedResults.some((entry) => entry.path === \"a/needle.txt\"), true)\n assert.equal(realResults.some((entry) => entry.path === \"b/needle.txt\"), true)\n } finally {\n fs.rmSync(linkedWorkspace, { recursive: true, force: true })\n }\n})\n"]}
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { test } from "node:test";
6
+ import { FileSystemBrowser } from "../browser";
7
+ test("concurrent saves through distinct browsers cannot interleave file contents", async (t) => {
8
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-write-order-"));
9
+ const target = path.join(root, "file.txt");
10
+ const original = fs.promises.writeFile.bind(fs.promises);
11
+ let release;
12
+ let started;
13
+ const gate = new Promise(resolve => { release = resolve; });
14
+ const entered = new Promise(resolve => { started = resolve; });
15
+ t.mock.method(fs.promises, "writeFile", async (...args) => {
16
+ if (args[1] === "first-large-save") {
17
+ await original(target, "first-");
18
+ started();
19
+ await gate;
20
+ // Model the multiple underlying writes used by fs.writeFile for large data.
21
+ await fs.promises.appendFile(target, "large-save");
22
+ }
23
+ else
24
+ await original(...args);
25
+ });
26
+ const first = new FileSystemBrowser({ rootDir: root }).writeFile("file.txt", "first-large-save");
27
+ await entered;
28
+ let secondFinished = false;
29
+ const second = new FileSystemBrowser({ rootDir: root }).writeFile("file.txt", "last-save")
30
+ .then(() => { secondFinished = true; });
31
+ const read = new FileSystemBrowser({ rootDir: root }).readFile("file.txt");
32
+ try {
33
+ // Independent paths must remain writable while the first file is stalled.
34
+ await new FileSystemBrowser({ rootDir: root }).writeFile("other.txt", "independent");
35
+ await new Promise(resolve => setImmediate(resolve));
36
+ assert.equal(secondFinished, false);
37
+ }
38
+ finally {
39
+ release();
40
+ await Promise.all([first, second]);
41
+ t.mock.restoreAll();
42
+ }
43
+ try {
44
+ assert.equal(await read, "last-save");
45
+ assert.equal(await fs.promises.readFile(target, "utf8"), "last-save");
46
+ }
47
+ finally {
48
+ fs.rmSync(root, { recursive: true, force: true });
49
+ }
50
+ });
51
+ test("a failed save does not poison subsequent saves", async () => {
52
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-write-error-"));
53
+ const browser = new FileSystemBrowser({ rootDir: root });
54
+ try {
55
+ await assert.rejects(browser.writeFile("dir/file.txt", "first"), { code: "ENOENT" });
56
+ fs.mkdirSync(path.join(root, "dir"));
57
+ await browser.writeFile("dir/file.txt", "recovered");
58
+ assert.equal(await browser.readFile("dir/file.txt"), "recovered");
59
+ }
60
+ finally {
61
+ fs.rmSync(root, { recursive: true, force: true });
62
+ }
63
+ });
64
+ //# sourceMappingURL=write-order.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-order.test.js","sourceRoot":"","sources":["../../../src/filesystem/__tests__/write-order.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE9C,IAAI,CAAC,4EAA4E,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;IAC7F,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,wBAAwB,CAAC,CAAC,CAAA;IAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAC1C,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAA;IACxD,IAAI,OAAoB,CAAA;IACxB,IAAI,OAAoB,CAAA;IACxB,MAAM,IAAI,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,GAAG,OAAO,GAAG,OAAO,CAAA,CAAC,CAAC,CAAC,CAAA;IAChE,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,GAAG,OAAO,GAAG,OAAO,CAAA,CAAC,CAAC,CAAC,CAAA;IACnE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,IAA8C,EAAE,EAAE;QAClG,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,kBAAkB,EAAE,CAAC;YACnC,MAAM,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;YAChC,OAAO,EAAE,CAAA;YACT,MAAM,IAAI,CAAA;YACV,4EAA4E;YAC5E,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;QACpD,CAAC;;YAAM,MAAM,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAA;IAChC,CAAC,CAAC,CAAA;IACF,MAAM,KAAK,GAAG,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAA;IAChG,MAAM,OAAO,CAAA;IACb,IAAI,cAAc,GAAG,KAAK,CAAA;IAC1B,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,UAAU,EAAE,WAAW,CAAC;SACvF,IAAI,CAAC,GAAG,EAAE,GAAG,cAAc,GAAG,IAAI,CAAA,CAAC,CAAC,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;IAC1E,IAAI,CAAC;QACH,0EAA0E;QAC1E,MAAM,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA;QACpF,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAA;QACzD,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAA;IACrC,CAAC;YAAS,CAAC;QACT,OAAO,EAAE,CAAA;QACT,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAClC,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,CAAA;IACrB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,WAAW,CAAC,CAAA;QACrC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,CAAC,CAAA;IACvE,CAAC;YACO,CAAC;QAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAAC,CAAC;AAC/D,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;IAChE,MAAM,IAAI,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,wBAAwB,CAAC,CAAC,CAAA;IAC7E,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IACxD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,cAAc,EAAE,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QACpF,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;QACpC,MAAM,OAAO,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAA;QACpD,MAAM,CAAC,KAAK,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,WAAW,CAAC,CAAA;IACnE,CAAC;YAAS,CAAC;QAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IAAC,CAAC;AACjE,CAAC,CAAC,CAAA","sourcesContent":["import assert from \"node:assert/strict\"\nimport fs from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { test } from \"node:test\"\nimport { FileSystemBrowser } from \"../browser\"\n\ntest(\"concurrent saves through distinct browsers cannot interleave file contents\", async (t) => {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-write-order-\"))\n const target = path.join(root, \"file.txt\")\n const original = fs.promises.writeFile.bind(fs.promises)\n let release!: () => void\n let started!: () => void\n const gate = new Promise<void>(resolve => { release = resolve })\n const entered = new Promise<void>(resolve => { started = resolve })\n t.mock.method(fs.promises, \"writeFile\", async (...args: Parameters<typeof fs.promises.writeFile>) => {\n if (args[1] === \"first-large-save\") {\n await original(target, \"first-\")\n started()\n await gate\n // Model the multiple underlying writes used by fs.writeFile for large data.\n await fs.promises.appendFile(target, \"large-save\")\n } else await original(...args)\n })\n const first = new FileSystemBrowser({ rootDir: root }).writeFile(\"file.txt\", \"first-large-save\")\n await entered\n let secondFinished = false\n const second = new FileSystemBrowser({ rootDir: root }).writeFile(\"file.txt\", \"last-save\")\n .then(() => { secondFinished = true })\n const read = new FileSystemBrowser({ rootDir: root }).readFile(\"file.txt\")\n try {\n // Independent paths must remain writable while the first file is stalled.\n await new FileSystemBrowser({ rootDir: root }).writeFile(\"other.txt\", \"independent\")\n await new Promise<void>(resolve => setImmediate(resolve))\n assert.equal(secondFinished, false)\n } finally {\n release()\n await Promise.all([first, second])\n t.mock.restoreAll()\n }\n try {\n assert.equal(await read, \"last-save\")\n assert.equal(await fs.promises.readFile(target, \"utf8\"), \"last-save\")\n }\n finally { fs.rmSync(root, { recursive: true, force: true }) }\n})\n\ntest(\"a failed save does not poison subsequent saves\", async () => {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), \"codenomad-write-error-\"))\n const browser = new FileSystemBrowser({ rootDir: root })\n try {\n await assert.rejects(browser.writeFile(\"dir/file.txt\", \"first\"), { code: \"ENOENT\" })\n fs.mkdirSync(path.join(root, \"dir\"))\n await browser.writeFile(\"dir/file.txt\", \"recovered\")\n assert.equal(await browser.readFile(\"dir/file.txt\"), \"recovered\")\n } finally { fs.rmSync(root, { recursive: true, force: true }) }\n})\n"]}