@oscharko-dev/keiko-server 0.2.7 → 0.2.9

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 (302) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/chat-handlers.d.ts +18 -2
  3. package/dist/chat-handlers.d.ts.map +1 -1
  4. package/dist/chat-handlers.js +185 -3
  5. package/dist/command-runner-errors.d.ts +17 -0
  6. package/dist/command-runner-errors.d.ts.map +1 -0
  7. package/dist/command-runner-errors.js +37 -0
  8. package/dist/command-runner-evidence.d.ts +23 -0
  9. package/dist/command-runner-evidence.d.ts.map +1 -0
  10. package/dist/command-runner-evidence.js +69 -0
  11. package/dist/command-runner-routes.d.ts +7 -0
  12. package/dist/command-runner-routes.d.ts.map +1 -0
  13. package/dist/command-runner-routes.js +175 -0
  14. package/dist/command-runner.d.ts +29 -0
  15. package/dist/command-runner.d.ts.map +1 -0
  16. package/dist/command-runner.js +348 -0
  17. package/dist/conversation-prompt.d.ts +2 -2
  18. package/dist/conversation-prompt.d.ts.map +1 -1
  19. package/dist/conversation-prompt.js +17 -1
  20. package/dist/csp.d.ts.map +1 -1
  21. package/dist/csp.js +3 -0
  22. package/dist/deps.d.ts +28 -1
  23. package/dist/deps.d.ts.map +1 -1
  24. package/dist/deps.js +288 -13
  25. package/dist/discussion-prompt.d.ts +4 -0
  26. package/dist/discussion-prompt.d.ts.map +1 -0
  27. package/dist/discussion-prompt.js +19 -0
  28. package/dist/editor/agentActionAudit.d.ts +18 -0
  29. package/dist/editor/agentActionAudit.d.ts.map +1 -0
  30. package/dist/editor/agentActionAudit.js +80 -0
  31. package/dist/editor/agentRoutes.d.ts +1 -0
  32. package/dist/editor/agentRoutes.d.ts.map +1 -1
  33. package/dist/editor/agentRoutes.js +292 -55
  34. package/dist/editor/agentSessionRegistry.d.ts +35 -0
  35. package/dist/editor/agentSessionRegistry.d.ts.map +1 -0
  36. package/dist/editor/agentSessionRegistry.js +243 -0
  37. package/dist/editor/completionRoutes.d.ts.map +1 -1
  38. package/dist/editor/completionRoutes.js +5 -10
  39. package/dist/editor/languageRoutes.d.ts +12 -1
  40. package/dist/editor/languageRoutes.d.ts.map +1 -1
  41. package/dist/editor/languageRoutes.js +71 -8
  42. package/dist/editor/languageService.d.ts +3 -2
  43. package/dist/editor/languageService.d.ts.map +1 -1
  44. package/dist/editor/languageService.js +41 -3
  45. package/dist/editor/languageServiceHost.d.ts.map +1 -1
  46. package/dist/editor/languageServiceHost.js +2 -2
  47. package/dist/editor/lsp/hostLanguageOperation.d.ts +17 -0
  48. package/dist/editor/lsp/hostLanguageOperation.d.ts.map +1 -0
  49. package/dist/editor/lsp/hostLanguageOperation.js +436 -0
  50. package/dist/editor/lsp/hostLanguageProviders.d.ts +26 -0
  51. package/dist/editor/lsp/hostLanguageProviders.d.ts.map +1 -0
  52. package/dist/editor/lsp/hostLanguageProviders.js +161 -0
  53. package/dist/editor/lsp/lspFrameCodec.d.ts +13 -0
  54. package/dist/editor/lsp/lspFrameCodec.d.ts.map +1 -0
  55. package/dist/editor/lsp/lspFrameCodec.js +164 -0
  56. package/dist/editor/lsp/lspJsonRpcClient.d.ts +34 -0
  57. package/dist/editor/lsp/lspJsonRpcClient.d.ts.map +1 -0
  58. package/dist/editor/lsp/lspJsonRpcClient.js +173 -0
  59. package/dist/editor/lsp/lspLanguageProvider.d.ts +7 -0
  60. package/dist/editor/lsp/lspLanguageProvider.d.ts.map +1 -0
  61. package/dist/editor/lsp/lspLanguageProvider.js +29 -0
  62. package/dist/editor/lsp/lspLifecycleLedger.d.ts +5 -0
  63. package/dist/editor/lsp/lspLifecycleLedger.d.ts.map +1 -0
  64. package/dist/editor/lsp/lspLifecycleLedger.js +37 -0
  65. package/dist/editor/lsp/lspNodeAdapter.d.ts +31 -0
  66. package/dist/editor/lsp/lspNodeAdapter.d.ts.map +1 -0
  67. package/dist/editor/lsp/lspNodeAdapter.js +230 -0
  68. package/dist/editor/lsp/lspProcessManager.d.ts +24 -0
  69. package/dist/editor/lsp/lspProcessManager.d.ts.map +1 -0
  70. package/dist/editor/lsp/lspProcessManager.js +255 -0
  71. package/dist/editor/lsp/lspRestartThrottle.d.ts +6 -0
  72. package/dist/editor/lsp/lspRestartThrottle.d.ts.map +1 -0
  73. package/dist/editor/lsp/lspRestartThrottle.js +24 -0
  74. package/dist/editor/lsp/lspStatusRoute.d.ts +8 -0
  75. package/dist/editor/lsp/lspStatusRoute.d.ts.map +1 -0
  76. package/dist/editor/lsp/lspStatusRoute.js +22 -0
  77. package/dist/editor/lsp/lspTransport.d.ts +19 -0
  78. package/dist/editor/lsp/lspTransport.d.ts.map +1 -0
  79. package/dist/editor/lsp/lspTransport.js +55 -0
  80. package/dist/editor/lsp/testing/fakeLspProcess.d.ts +23 -0
  81. package/dist/editor/lsp/testing/fakeLspProcess.d.ts.map +1 -0
  82. package/dist/editor/lsp/testing/fakeLspProcess.js +132 -0
  83. package/dist/files.d.ts +63 -0
  84. package/dist/files.d.ts.map +1 -1
  85. package/dist/files.js +799 -1
  86. package/dist/gateway-readiness.d.ts +6 -0
  87. package/dist/gateway-readiness.d.ts.map +1 -0
  88. package/dist/gateway-readiness.js +624 -0
  89. package/dist/gateway-setup.d.ts +2 -0
  90. package/dist/gateway-setup.d.ts.map +1 -1
  91. package/dist/gateway-setup.js +275 -11
  92. package/dist/gitDelivery/actionSheetProjection.d.ts +30 -0
  93. package/dist/gitDelivery/actionSheetProjection.d.ts.map +1 -0
  94. package/dist/gitDelivery/actionSheetProjection.js +206 -0
  95. package/dist/gitDelivery/actionSheetRoutes.d.ts +29 -0
  96. package/dist/gitDelivery/actionSheetRoutes.d.ts.map +1 -0
  97. package/dist/gitDelivery/actionSheetRoutes.js +293 -0
  98. package/dist/gitDelivery/agentOperationsRoutes.d.ts +33 -0
  99. package/dist/gitDelivery/agentOperationsRoutes.d.ts.map +1 -0
  100. package/dist/gitDelivery/agentOperationsRoutes.js +405 -0
  101. package/dist/gitDelivery/commitRoutes.d.ts +23 -0
  102. package/dist/gitDelivery/commitRoutes.d.ts.map +1 -0
  103. package/dist/gitDelivery/commitRoutes.js +204 -0
  104. package/dist/gitDelivery/evidenceRoutes.d.ts +9 -0
  105. package/dist/gitDelivery/evidenceRoutes.d.ts.map +1 -0
  106. package/dist/gitDelivery/evidenceRoutes.js +101 -0
  107. package/dist/gitDelivery/execution.d.ts +38 -0
  108. package/dist/gitDelivery/execution.d.ts.map +1 -0
  109. package/dist/gitDelivery/execution.js +117 -0
  110. package/dist/gitDelivery/localMutationRoutes.d.ts +30 -0
  111. package/dist/gitDelivery/localMutationRoutes.d.ts.map +1 -0
  112. package/dist/gitDelivery/localMutationRoutes.js +165 -0
  113. package/dist/gitDelivery/mergeExecution.d.ts +63 -0
  114. package/dist/gitDelivery/mergeExecution.d.ts.map +1 -0
  115. package/dist/gitDelivery/mergeExecution.js +168 -0
  116. package/dist/gitDelivery/mergeRoutes.d.ts +12 -0
  117. package/dist/gitDelivery/mergeRoutes.d.ts.map +1 -0
  118. package/dist/gitDelivery/mergeRoutes.js +218 -0
  119. package/dist/gitDelivery/mutationEvidenceLedger.d.ts +23 -0
  120. package/dist/gitDelivery/mutationEvidenceLedger.d.ts.map +1 -0
  121. package/dist/gitDelivery/mutationEvidenceLedger.js +87 -0
  122. package/dist/gitDelivery/prExecution.d.ts +54 -0
  123. package/dist/gitDelivery/prExecution.d.ts.map +1 -0
  124. package/dist/gitDelivery/prExecution.js +192 -0
  125. package/dist/gitDelivery/prRoutes.d.ts +12 -0
  126. package/dist/gitDelivery/prRoutes.d.ts.map +1 -0
  127. package/dist/gitDelivery/prRoutes.js +256 -0
  128. package/dist/gitDelivery/pushExecution.d.ts +43 -0
  129. package/dist/gitDelivery/pushExecution.d.ts.map +1 -0
  130. package/dist/gitDelivery/pushExecution.js +124 -0
  131. package/dist/gitDelivery/pushRoutes.d.ts +12 -0
  132. package/dist/gitDelivery/pushRoutes.d.ts.map +1 -0
  133. package/dist/gitDelivery/pushRoutes.js +200 -0
  134. package/dist/gitDelivery/requestGuards.d.ts +15 -0
  135. package/dist/gitDelivery/requestGuards.d.ts.map +1 -0
  136. package/dist/gitDelivery/requestGuards.js +97 -0
  137. package/dist/gitDelivery/syncEvidence.d.ts +37 -0
  138. package/dist/gitDelivery/syncEvidence.d.ts.map +1 -0
  139. package/dist/gitDelivery/syncEvidence.js +85 -0
  140. package/dist/gitDelivery/syncExecution.d.ts +30 -0
  141. package/dist/gitDelivery/syncExecution.d.ts.map +1 -0
  142. package/dist/gitDelivery/syncExecution.js +266 -0
  143. package/dist/gitDelivery/syncRoutes.d.ts +13 -0
  144. package/dist/gitDelivery/syncRoutes.d.ts.map +1 -0
  145. package/dist/gitDelivery/syncRoutes.js +200 -0
  146. package/dist/gitPorcelainStatus.d.ts +15 -0
  147. package/dist/gitPorcelainStatus.d.ts.map +1 -0
  148. package/dist/gitPorcelainStatus.js +104 -0
  149. package/dist/gitRepositoryReads.d.ts +10 -0
  150. package/dist/gitRepositoryReads.d.ts.map +1 -0
  151. package/dist/gitRepositoryReads.js +314 -0
  152. package/dist/gitRepositoryRoutes.d.ts +7 -0
  153. package/dist/gitRepositoryRoutes.d.ts.map +1 -0
  154. package/dist/gitRepositoryRoutes.js +221 -0
  155. package/dist/gitRoutes.d.ts +66 -0
  156. package/dist/gitRoutes.d.ts.map +1 -0
  157. package/dist/gitRoutes.js +543 -0
  158. package/dist/governed-workflow.d.ts +2 -0
  159. package/dist/governed-workflow.d.ts.map +1 -1
  160. package/dist/governed-workflow.js +4 -0
  161. package/dist/grounded-qa-hybrid.d.ts.map +1 -1
  162. package/dist/grounded-qa-hybrid.js +2 -0
  163. package/dist/grounded-qa-multi-source.d.ts.map +1 -1
  164. package/dist/grounded-qa-multi-source.js +1 -0
  165. package/dist/grounded-qa.d.ts +11 -0
  166. package/dist/grounded-qa.d.ts.map +1 -1
  167. package/dist/grounded-qa.js +14 -4
  168. package/dist/headers.d.ts +4 -1
  169. package/dist/headers.d.ts.map +1 -1
  170. package/dist/headers.js +11 -4
  171. package/dist/index.d.ts +8 -1
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +9 -1
  174. package/dist/local-knowledge-grounded-qa.d.ts.map +1 -1
  175. package/dist/local-knowledge-grounded-qa.js +11 -2
  176. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts +1 -1
  177. package/dist/qualityIntelligence/figmaSnapshotRoutes.d.ts.map +1 -1
  178. package/dist/qualityIntelligence/figmaSnapshotRoutes.js +1 -1
  179. package/dist/read-handlers.d.ts +5 -0
  180. package/dist/read-handlers.d.ts.map +1 -1
  181. package/dist/read-handlers.js +57 -1
  182. package/dist/routes.d.ts.map +1 -1
  183. package/dist/routes.js +260 -12
  184. package/dist/run-engine.d.ts.map +1 -1
  185. package/dist/run-engine.js +3 -0
  186. package/dist/run-handlers.d.ts +0 -1
  187. package/dist/run-handlers.d.ts.map +1 -1
  188. package/dist/run-handlers.js +64 -211
  189. package/dist/run-request.d.ts +11 -0
  190. package/dist/run-request.d.ts.map +1 -1
  191. package/dist/run-request.js +158 -10
  192. package/dist/runtime/capabilityDetector.d.ts +38 -0
  193. package/dist/runtime/capabilityDetector.d.ts.map +1 -0
  194. package/dist/runtime/capabilityDetector.js +443 -0
  195. package/dist/runtime/capabilityRoutes.d.ts +9 -0
  196. package/dist/runtime/capabilityRoutes.d.ts.map +1 -0
  197. package/dist/runtime/capabilityRoutes.js +45 -0
  198. package/dist/runtime/containerEngineDetector.d.ts +17 -0
  199. package/dist/runtime/containerEngineDetector.d.ts.map +1 -0
  200. package/dist/runtime/containerEngineDetector.js +222 -0
  201. package/dist/runtime/containerRoutes.d.ts +8 -0
  202. package/dist/runtime/containerRoutes.d.ts.map +1 -0
  203. package/dist/runtime/containerRoutes.js +207 -0
  204. package/dist/runtime/containerRunner-errors.d.ts +18 -0
  205. package/dist/runtime/containerRunner-errors.d.ts.map +1 -0
  206. package/dist/runtime/containerRunner-errors.js +42 -0
  207. package/dist/runtime/containerRunner-evidence.d.ts +24 -0
  208. package/dist/runtime/containerRunner-evidence.d.ts.map +1 -0
  209. package/dist/runtime/containerRunner-evidence.js +74 -0
  210. package/dist/runtime/containerRunner.d.ts +37 -0
  211. package/dist/runtime/containerRunner.d.ts.map +1 -0
  212. package/dist/runtime/containerRunner.js +443 -0
  213. package/dist/server.d.ts.map +1 -1
  214. package/dist/server.js +24 -4
  215. package/dist/store/db.d.ts.map +1 -1
  216. package/dist/store/db.js +2 -1
  217. package/dist/store/index.d.ts +1 -1
  218. package/dist/store/index.d.ts.map +1 -1
  219. package/dist/store/messages.d.ts +2 -1
  220. package/dist/store/messages.d.ts.map +1 -1
  221. package/dist/store/messages.js +46 -4
  222. package/dist/store/schema.d.ts +1 -1
  223. package/dist/store/schema.d.ts.map +1 -1
  224. package/dist/store/schema.js +68 -1
  225. package/dist/store/types.d.ts +3 -2
  226. package/dist/store/types.d.ts.map +1 -1
  227. package/dist/task-workspace/active-store.d.ts +21 -0
  228. package/dist/task-workspace/active-store.d.ts.map +1 -0
  229. package/dist/task-workspace/active-store.js +55 -0
  230. package/dist/task-workspace/authorization.d.ts +7 -0
  231. package/dist/task-workspace/authorization.d.ts.map +1 -0
  232. package/dist/task-workspace/authorization.js +54 -0
  233. package/dist/task-workspace/binding.d.ts +3 -0
  234. package/dist/task-workspace/binding.d.ts.map +1 -0
  235. package/dist/task-workspace/binding.js +22 -0
  236. package/dist/task-workspace/cleanup.d.ts +4 -0
  237. package/dist/task-workspace/cleanup.d.ts.map +1 -0
  238. package/dist/task-workspace/cleanup.js +428 -0
  239. package/dist/task-workspace/errors.d.ts +14 -0
  240. package/dist/task-workspace/errors.d.ts.map +1 -0
  241. package/dist/task-workspace/errors.js +81 -0
  242. package/dist/task-workspace/evidence.d.ts +32 -0
  243. package/dist/task-workspace/evidence.d.ts.map +1 -0
  244. package/dist/task-workspace/evidence.js +52 -0
  245. package/dist/task-workspace/field-safety.d.ts +3 -0
  246. package/dist/task-workspace/field-safety.d.ts.map +1 -0
  247. package/dist/task-workspace/field-safety.js +42 -0
  248. package/dist/task-workspace/health.d.ts +4 -0
  249. package/dist/task-workspace/health.d.ts.map +1 -0
  250. package/dist/task-workspace/health.js +163 -0
  251. package/dist/task-workspace/lifecycle.d.ts +3 -0
  252. package/dist/task-workspace/lifecycle.d.ts.map +1 -0
  253. package/dist/task-workspace/lifecycle.js +248 -0
  254. package/dist/task-workspace/locks.d.ts +13 -0
  255. package/dist/task-workspace/locks.d.ts.map +1 -0
  256. package/dist/task-workspace/locks.js +44 -0
  257. package/dist/task-workspace/managed-root.d.ts +7 -0
  258. package/dist/task-workspace/managed-root.d.ts.map +1 -0
  259. package/dist/task-workspace/managed-root.js +98 -0
  260. package/dist/task-workspace/mutex.d.ts +8 -0
  261. package/dist/task-workspace/mutex.d.ts.map +1 -0
  262. package/dist/task-workspace/mutex.js +82 -0
  263. package/dist/task-workspace/naming.d.ts +15 -0
  264. package/dist/task-workspace/naming.d.ts.map +1 -0
  265. package/dist/task-workspace/naming.js +0 -0
  266. package/dist/task-workspace/provisioning.d.ts +3 -0
  267. package/dist/task-workspace/provisioning.d.ts.map +1 -0
  268. package/dist/task-workspace/provisioning.js +528 -0
  269. package/dist/task-workspace/reconciliation.d.ts +15 -0
  270. package/dist/task-workspace/reconciliation.d.ts.map +1 -0
  271. package/dist/task-workspace/reconciliation.js +274 -0
  272. package/dist/task-workspace/repair.d.ts +3 -0
  273. package/dist/task-workspace/repair.d.ts.map +1 -0
  274. package/dist/task-workspace/repair.js +286 -0
  275. package/dist/task-workspace/routes.d.ts +19 -0
  276. package/dist/task-workspace/routes.d.ts.map +1 -0
  277. package/dist/task-workspace/routes.js +481 -0
  278. package/dist/task-workspace/store.d.ts +12 -0
  279. package/dist/task-workspace/store.d.ts.map +1 -0
  280. package/dist/task-workspace/store.js +128 -0
  281. package/dist/task-workspace/types.d.ts +170 -0
  282. package/dist/task-workspace/types.d.ts.map +1 -0
  283. package/dist/task-workspace/types.js +5 -0
  284. package/dist/voice-action-governance.d.ts +23 -0
  285. package/dist/voice-action-governance.d.ts.map +1 -0
  286. package/dist/voice-action-governance.js +126 -0
  287. package/dist/voice-handlers.d.ts +6 -0
  288. package/dist/voice-handlers.d.ts.map +1 -0
  289. package/dist/voice-handlers.js +570 -0
  290. package/dist/voice-realtime-grounded-tool.d.ts +31 -0
  291. package/dist/voice-realtime-grounded-tool.d.ts.map +1 -0
  292. package/dist/voice-realtime-grounded-tool.js +322 -0
  293. package/dist/voice-realtime.d.ts +69 -0
  294. package/dist/voice-realtime.d.ts.map +1 -0
  295. package/dist/voice-realtime.js +787 -0
  296. package/dist/workspace-state-handlers.d.ts +5 -0
  297. package/dist/workspace-state-handlers.d.ts.map +1 -0
  298. package/dist/workspace-state-handlers.js +106 -0
  299. package/package.json +20 -19
  300. package/dist/grounded-handoff.d.ts +0 -4
  301. package/dist/grounded-handoff.d.ts.map +0 -1
  302. package/dist/grounded-handoff.js +0 -445
package/dist/files.js CHANGED
@@ -2,13 +2,17 @@
2
2
  // preview or editor content; every request is contained inside a selected root after
3
3
  // realpath resolution.
4
4
  import { createHash } from "node:crypto";
5
- import { lstat, opendir, open, readFile, realpath, stat, writeFile } from "node:fs/promises";
5
+ import { cp, lstat, mkdir, opendir, open, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
6
6
  import { basename, dirname, extname, isAbsolute, join, parse as parsePath, posix as pathPosix, relative, resolve, } from "node:path";
7
7
  import { redact } from "@oscharko-dev/keiko-security";
8
8
  import { EDITOR_SESSION_SCHEMA_VERSION, parseEditorDocumentVersion, } from "@oscharko-dev/keiko-contracts";
9
9
  import { DENIED_MESSAGE, pathIsDenied } from "./files-deny.js";
10
10
  import { errorBody } from "./routes.js";
11
11
  const MAX_DIRECTORY_ENTRIES = 1_000;
12
+ const DEFAULT_FILE_SEARCH_LIMIT = 24;
13
+ const MAX_FILE_SEARCH_LIMIT = 50;
14
+ const MAX_FILE_SEARCH_QUERY_CHARS = 120;
15
+ const MAX_FILE_SEARCH_SCAN = 20_000;
12
16
  const MAX_TEXT_PREVIEW_BYTES = 1_000_000;
13
17
  const MAX_IMAGE_PREVIEW_BYTES = 3_000_000;
14
18
  const STABLE_CONTENT_READ_ATTEMPTS = 3;
@@ -352,6 +356,427 @@ export async function readFilesTree(store, rootInput, pathInput, redactor = stat
352
356
  truncated: listed.truncated,
353
357
  };
354
358
  }
359
+ function parseSearchLimit(rawLimit) {
360
+ if (rawLimit === null || rawLimit.trim().length === 0)
361
+ return DEFAULT_FILE_SEARCH_LIMIT;
362
+ const parsed = Number(rawLimit);
363
+ if (!Number.isInteger(parsed) || parsed < 1) {
364
+ throw new FilesError(400, "BAD_LIMIT", "The search limit must be a positive integer.");
365
+ }
366
+ return Math.min(parsed, MAX_FILE_SEARCH_LIMIT);
367
+ }
368
+ function normalizeSearchQuery(queryInput) {
369
+ const query = (queryInput ?? "").trim().replace(/\s+/gu, " ");
370
+ if (query.includes("\0")) {
371
+ throw new FilesError(400, "BAD_QUERY", "The search query contains an invalid character.");
372
+ }
373
+ if (query.length > MAX_FILE_SEARCH_QUERY_CHARS) {
374
+ throw new FilesError(400, "BAD_QUERY", `The search query must be at most ${String(MAX_FILE_SEARCH_QUERY_CHARS)} characters.`);
375
+ }
376
+ return query;
377
+ }
378
+ function searchTokens(query) {
379
+ return query
380
+ .toLocaleLowerCase()
381
+ .split(/\s+/u)
382
+ .map((token) => token.trim())
383
+ .filter((token) => token.length > 0);
384
+ }
385
+ function matchesSearch(relativePath, tokens) {
386
+ const lowerPath = relativePath.toLocaleLowerCase();
387
+ return tokens.every((token) => lowerPath.includes(token));
388
+ }
389
+ function fileSearchScore(relativePath, query) {
390
+ const lowerPath = relativePath.toLocaleLowerCase();
391
+ const lowerName = basename(relativePath).toLocaleLowerCase();
392
+ const lowerQuery = query.toLocaleLowerCase();
393
+ if (lowerName === lowerQuery)
394
+ return 0;
395
+ if (lowerName.startsWith(lowerQuery))
396
+ return 100 + relativePath.length;
397
+ if (lowerPath === lowerQuery)
398
+ return 200 + relativePath.length;
399
+ if (lowerPath.startsWith(lowerQuery))
400
+ return 300 + relativePath.length;
401
+ const nameIndex = lowerName.indexOf(lowerQuery);
402
+ if (nameIndex >= 0)
403
+ return 400 + nameIndex + relativePath.length;
404
+ const pathIndex = lowerPath.indexOf(lowerQuery);
405
+ if (pathIndex >= 0)
406
+ return 600 + pathIndex + relativePath.length;
407
+ return 1_000 + relativePath.length;
408
+ }
409
+ const GENERATED_FILE_SEARCH_SEGMENTS = new Set([
410
+ ".next",
411
+ ".turbo",
412
+ "build",
413
+ "coverage",
414
+ "dist",
415
+ "out",
416
+ "storybook-static",
417
+ "storybookstatic",
418
+ "target",
419
+ ]);
420
+ const SOURCE_FILE_SEARCH_SEGMENTS = new Set([
421
+ "__tests__",
422
+ "app",
423
+ "components",
424
+ "lib",
425
+ "packages",
426
+ "scripts",
427
+ "src",
428
+ "test",
429
+ "tests",
430
+ ]);
431
+ const SOURCE_FILE_SEARCH_EXTENSIONS = new Set([
432
+ "astro",
433
+ "c",
434
+ "cc",
435
+ "cjs",
436
+ "cpp",
437
+ "cs",
438
+ "css",
439
+ "go",
440
+ "h",
441
+ "hpp",
442
+ "html",
443
+ "java",
444
+ "js",
445
+ "jsx",
446
+ "kt",
447
+ "kts",
448
+ "mjs",
449
+ "mts",
450
+ "php",
451
+ "py",
452
+ "rb",
453
+ "rs",
454
+ "scala",
455
+ "scss",
456
+ "sh",
457
+ "svelte",
458
+ "swift",
459
+ "ts",
460
+ "tsx",
461
+ "vue",
462
+ ]);
463
+ const TEST_FILE_SEARCH_SEGMENTS = new Set(["__tests__", "__test__", "spec", "test", "tests"]);
464
+ const DOCS_FILE_SEARCH_SEGMENTS = new Set(["doc", "docs", "documentation"]);
465
+ const DOCS_FILE_SEARCH_EXTENSIONS = new Set(["adoc", "md", "mdx", "rst", "txt"]);
466
+ const CONFIG_FILE_SEARCH_NAMES = new Set([
467
+ ".babelrc",
468
+ ".editorconfig",
469
+ ".env.example",
470
+ ".eslintrc",
471
+ ".gitattributes",
472
+ ".gitignore",
473
+ ".npmrc",
474
+ ".prettierrc",
475
+ "dockerfile",
476
+ "package.json",
477
+ "tsconfig.json",
478
+ "vite.config.ts",
479
+ "vitest.config.ts",
480
+ ]);
481
+ const CONFIG_FILE_SEARCH_EXTENSIONS = new Set([
482
+ "config",
483
+ "conf",
484
+ "ini",
485
+ "json",
486
+ "jsonc",
487
+ "lock",
488
+ "toml",
489
+ "yaml",
490
+ "yml",
491
+ ]);
492
+ const ASSET_FILE_SEARCH_EXTENSIONS = new Set([
493
+ "avif",
494
+ "gif",
495
+ "ico",
496
+ "jpeg",
497
+ "jpg",
498
+ "map",
499
+ "png",
500
+ "svg",
501
+ "webp",
502
+ "woff",
503
+ "woff2",
504
+ ]);
505
+ function fileSearchPathParts(relativePath) {
506
+ const normalized = relativePath.replaceAll("\\", "/");
507
+ const lowerSegments = normalized
508
+ .toLocaleLowerCase()
509
+ .split("/")
510
+ .filter((segment) => segment.length > 0);
511
+ const lowerName = basename(normalized).toLocaleLowerCase();
512
+ const extension = extensionOf(lowerName)?.toLocaleLowerCase() ?? "";
513
+ return { lowerSegments, lowerName, extension };
514
+ }
515
+ function fileSearchQualityScore(relativePath) {
516
+ const normalized = relativePath.replaceAll("\\", "/");
517
+ const lowerSegments = normalized
518
+ .toLocaleLowerCase()
519
+ .split("/")
520
+ .filter((segment) => segment.length > 0);
521
+ const extension = extensionOf(basename(normalized))?.toLocaleLowerCase() ?? "";
522
+ let score = 0;
523
+ if (lowerSegments.some((segment) => GENERATED_FILE_SEARCH_SEGMENTS.has(segment))) {
524
+ score += 20_000;
525
+ }
526
+ if (lowerSegments.includes("assets") && /\b[a-f0-9]{7,}\b/u.test(basename(normalized))) {
527
+ score += 2_000;
528
+ }
529
+ if (lowerSegments.some((segment) => SOURCE_FILE_SEARCH_SEGMENTS.has(segment))) {
530
+ score -= 250;
531
+ }
532
+ if (SOURCE_FILE_SEARCH_EXTENSIONS.has(extension)) {
533
+ score -= 100;
534
+ }
535
+ if (lowerSegments.includes("src")) {
536
+ score -= 200;
537
+ }
538
+ return score;
539
+ }
540
+ function fileSearchPathHasSegment(parts, segments) {
541
+ return parts.lowerSegments.some((segment) => segments.has(segment));
542
+ }
543
+ function fileSearchPathIsGenerated(parts) {
544
+ return fileSearchPathHasSegment(parts, GENERATED_FILE_SEARCH_SEGMENTS);
545
+ }
546
+ function fileSearchPathIsAsset(parts) {
547
+ return (parts.lowerSegments.includes("assets") || ASSET_FILE_SEARCH_EXTENSIONS.has(parts.extension));
548
+ }
549
+ function fileSearchPathIsTest(parts) {
550
+ return (fileSearchPathHasSegment(parts, TEST_FILE_SEARCH_SEGMENTS) ||
551
+ /\.(?:spec|test)\.[^.]+$/u.test(parts.lowerName));
552
+ }
553
+ function fileSearchPathIsDocs(parts) {
554
+ return (fileSearchPathHasSegment(parts, DOCS_FILE_SEARCH_SEGMENTS) ||
555
+ DOCS_FILE_SEARCH_EXTENSIONS.has(parts.extension));
556
+ }
557
+ function fileSearchPathIsConfig(parts) {
558
+ return (CONFIG_FILE_SEARCH_NAMES.has(parts.lowerName) ||
559
+ CONFIG_FILE_SEARCH_EXTENSIONS.has(parts.extension));
560
+ }
561
+ function fileSearchPathIsSource(parts) {
562
+ return (fileSearchPathHasSegment(parts, SOURCE_FILE_SEARCH_SEGMENTS) ||
563
+ SOURCE_FILE_SEARCH_EXTENSIONS.has(parts.extension));
564
+ }
565
+ const FILE_SEARCH_ROLE_MATCHERS = [
566
+ ["generated", fileSearchPathIsGenerated],
567
+ ["asset", fileSearchPathIsAsset],
568
+ ["test", fileSearchPathIsTest],
569
+ ["docs", fileSearchPathIsDocs],
570
+ ["config", fileSearchPathIsConfig],
571
+ ["source", fileSearchPathIsSource],
572
+ ];
573
+ function fileSearchRole(relativePath) {
574
+ const parts = fileSearchPathParts(relativePath);
575
+ return FILE_SEARCH_ROLE_MATCHERS.find(([_role, matches]) => matches(parts))?.[0] ?? "other";
576
+ }
577
+ function fileSearchMatchQuality(relativePath, query) {
578
+ const lowerPath = relativePath.toLocaleLowerCase();
579
+ const lowerName = basename(relativePath).toLocaleLowerCase();
580
+ const lowerQuery = query.toLocaleLowerCase();
581
+ const nameStem = lowerName.replace(/\.[^.]+$/u, "");
582
+ if (lowerName === lowerQuery || lowerPath === lowerQuery || nameStem === lowerQuery) {
583
+ return "exact";
584
+ }
585
+ if (lowerName.startsWith(lowerQuery) || lowerName.includes(lowerQuery)) {
586
+ return "strong";
587
+ }
588
+ if (lowerPath.startsWith(lowerQuery) || lowerPath.includes(lowerQuery)) {
589
+ return "path";
590
+ }
591
+ return "weak";
592
+ }
593
+ function directoryOf(relativePath) {
594
+ const dir = pathPosix.dirname(relativePath);
595
+ return dir === "." ? "" : dir;
596
+ }
597
+ function entryVisibleToFileSearch(relativePath, entry, redactor) {
598
+ return (metadataIsSafe(relativePath, redactor) && !pathIsDenied(relativePath) && !entry.isSymbolicLink());
599
+ }
600
+ async function hasGitMarker(directory) {
601
+ try {
602
+ await lstat(join(directory, ".git"));
603
+ return true;
604
+ }
605
+ catch {
606
+ return false;
607
+ }
608
+ }
609
+ async function nearestGitRoot(startDirectory, cache) {
610
+ let current = resolve(startDirectory);
611
+ const visited = [];
612
+ for (;;) {
613
+ const cached = cache.get(current);
614
+ if (cached !== undefined) {
615
+ for (const directory of visited)
616
+ cache.set(directory, cached);
617
+ return cached;
618
+ }
619
+ visited.push(current);
620
+ if (await hasGitMarker(current)) {
621
+ for (const directory of visited)
622
+ cache.set(directory, current);
623
+ return current;
624
+ }
625
+ const parent = dirname(current);
626
+ if (parent === current) {
627
+ for (const directory of visited)
628
+ cache.set(directory, null);
629
+ return null;
630
+ }
631
+ current = parent;
632
+ }
633
+ }
634
+ function canExposeFileSearchGitRoot(selectedRoot, gitRoot, redactor) {
635
+ return (gitRoot !== selectedRoot.realRoot &&
636
+ isContained(selectedRoot.realRoot, gitRoot) &&
637
+ !pathIsDenied(gitRoot) &&
638
+ metadataIsSafe(gitRoot, redactor));
639
+ }
640
+ function canExposeFileSearchRelativePath(relativePath, redactor) {
641
+ return (relativePath.length > 0 &&
642
+ !relativePath.startsWith("../") &&
643
+ !pathPosix.isAbsolute(relativePath) &&
644
+ !pathIsDenied(relativePath) &&
645
+ metadataIsSafe(relativePath, redactor));
646
+ }
647
+ async function resolveFileSearchResultPath(args) {
648
+ const fallback = {
649
+ root: args.root.root,
650
+ relativePath: args.relativePath,
651
+ rootKind: "selected-root",
652
+ };
653
+ const gitRoot = await nearestGitRoot(dirname(args.nativePath), args.state.gitRootCache);
654
+ if (gitRoot === null || !canExposeFileSearchGitRoot(args.root, gitRoot, args.redactor)) {
655
+ return fallback;
656
+ }
657
+ const rebasedPath = rootRelativePosixPath(gitRoot, args.nativePath);
658
+ if (!canExposeFileSearchRelativePath(rebasedPath, args.redactor))
659
+ return fallback;
660
+ return { root: gitRoot, relativePath: rebasedPath, rootKind: "nested-git-root" };
661
+ }
662
+ async function addFileSearchCandidate(args) {
663
+ if (!matchesSearch(args.relativePath, args.tokens))
664
+ return;
665
+ let info;
666
+ try {
667
+ info = await lstat(args.nativePath);
668
+ }
669
+ catch {
670
+ return;
671
+ }
672
+ const resolvedPath = await resolveFileSearchResultPath({
673
+ root: args.root,
674
+ relativePath: args.relativePath,
675
+ nativePath: args.nativePath,
676
+ redactor: args.redactor,
677
+ state: args.state,
678
+ });
679
+ args.state.candidates.push({
680
+ score: fileSearchScore(resolvedPath.relativePath, args.query) +
681
+ fileSearchQualityScore(resolvedPath.relativePath),
682
+ result: {
683
+ root: resolvedPath.root,
684
+ path: resolvedPath.relativePath,
685
+ name: args.entryName,
686
+ directory: directoryOf(resolvedPath.relativePath),
687
+ extension: extensionOf(args.entryName),
688
+ sizeBytes: info.size,
689
+ modifiedAt: info.mtimeMs,
690
+ fileRole: fileSearchRole(resolvedPath.relativePath),
691
+ matchQuality: fileSearchMatchQuality(resolvedPath.relativePath, args.query),
692
+ rootKind: resolvedPath.rootKind,
693
+ },
694
+ });
695
+ }
696
+ async function collectFileSearchEntry(args) {
697
+ const relativePath = childRelative(args.current.relativePath, args.entry.name);
698
+ if (!entryVisibleToFileSearch(relativePath, args.entry, args.redactor))
699
+ return;
700
+ const nativePath = join(args.current.path, args.entry.name);
701
+ if (args.entry.isDirectory()) {
702
+ args.state.stack.push({ path: nativePath, relativePath });
703
+ return;
704
+ }
705
+ if (!args.entry.isFile())
706
+ return;
707
+ args.state.scannedFileCount += 1;
708
+ if (args.state.scannedFileCount > MAX_FILE_SEARCH_SCAN) {
709
+ args.state.scanTruncated = true;
710
+ return;
711
+ }
712
+ await addFileSearchCandidate({
713
+ root: args.root,
714
+ query: args.query,
715
+ relativePath,
716
+ nativePath,
717
+ entryName: args.entry.name,
718
+ tokens: args.tokens,
719
+ redactor: args.redactor,
720
+ state: args.state,
721
+ });
722
+ }
723
+ async function collectFileSearchDirectory(args) {
724
+ let dir;
725
+ try {
726
+ dir = await opendir(args.current.path);
727
+ }
728
+ catch {
729
+ return;
730
+ }
731
+ try {
732
+ for await (const entry of dir) {
733
+ await collectFileSearchEntry({ ...args, entry });
734
+ if (args.state.scanTruncated)
735
+ break;
736
+ }
737
+ }
738
+ finally {
739
+ await dir.close().catch(() => undefined);
740
+ }
741
+ }
742
+ async function collectFileSearchResults(args) {
743
+ const tokens = searchTokens(args.query);
744
+ if (tokens.length === 0) {
745
+ return { results: [], truncated: false, scannedFileCount: 0 };
746
+ }
747
+ const state = {
748
+ candidates: [],
749
+ stack: [{ path: args.root.realRoot, relativePath: "" }],
750
+ gitRootCache: new Map(),
751
+ scannedFileCount: 0,
752
+ scanTruncated: false,
753
+ };
754
+ while (state.stack.length > 0) {
755
+ const current = state.stack.pop();
756
+ if (current === undefined)
757
+ break;
758
+ await collectFileSearchDirectory({ ...args, current, tokens, state });
759
+ if (state.scanTruncated)
760
+ break;
761
+ }
762
+ state.candidates.sort((a, b) => a.score - b.score || a.result.path.localeCompare(b.result.path));
763
+ return {
764
+ results: state.candidates.slice(0, args.limit).map((candidate) => candidate.result),
765
+ truncated: state.scanTruncated || state.candidates.length > args.limit,
766
+ scannedFileCount: Math.min(state.scannedFileCount, MAX_FILE_SEARCH_SCAN),
767
+ };
768
+ }
769
+ export async function searchFiles(store, rootInput, queryInput, limitInput, redactor = staticFilesMetadataRedactor) {
770
+ const root = await resolveRoot(store, rootInput, redactor);
771
+ const query = normalizeSearchQuery(queryInput);
772
+ const limit = Math.min(Math.max(limitInput ?? DEFAULT_FILE_SEARCH_LIMIT, 1), MAX_FILE_SEARCH_LIMIT);
773
+ const collected = await collectFileSearchResults({ root, query, limit, redactor });
774
+ return {
775
+ root: root.root,
776
+ query,
777
+ ...collected,
778
+ };
779
+ }
355
780
  const IMAGE_MIME = {
356
781
  png: "image/png",
357
782
  jpg: "image/jpeg",
@@ -647,6 +1072,250 @@ export async function writeFilesContent(args) {
647
1072
  baseVersion: args.baseVersion,
648
1073
  });
649
1074
  }
1075
+ // errno → client-safe (status, code, message), one row per outcome. fs.cp reports collisions and
1076
+ // shape mismatches with bespoke ERR_FS_CP_* codes, so each row groups every code that should map to
1077
+ // the same response. A flat table keeps mapNodeFsError a single lookup (no branch-per-code) and well
1078
+ // under the complexity budget. None of these messages echoes a path or the raw OS string.
1079
+ const FS_ERRNO_TABLE = [
1080
+ // `fs.cp` reports a no-overwrite collision with its own code, not the bare EEXIST.
1081
+ [
1082
+ ["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"],
1083
+ 409,
1084
+ "ALREADY_EXISTS",
1085
+ "An entry with that name already exists.",
1086
+ ],
1087
+ [["ENOENT"], 404, "NOT_FOUND", "The requested path was not found."],
1088
+ // `fs.cp` shape-mismatch / invalid-argument codes (e.g. copying a dir over a file) → bad request.
1089
+ [
1090
+ [
1091
+ "ERR_FS_CP_DIR_TO_NON_DIR",
1092
+ "ERR_FS_CP_NON_DIR_TO_DIR",
1093
+ "ERR_FS_CP_EINVAL",
1094
+ "ERR_FS_CP_FIFO_PIPE_OR_SOCKET",
1095
+ ],
1096
+ 400,
1097
+ "BAD_PATH",
1098
+ "This entry cannot be copied here.",
1099
+ ],
1100
+ [["EACCES", "EPERM"], 403, "DENIED", DENIED_MESSAGE],
1101
+ [["EXDEV"], 400, "CROSS_DEVICE", "This move crosses filesystems and is not supported here."],
1102
+ [["ENOTDIR"], 400, "NOT_DIRECTORY", "Part of the path is not a folder."],
1103
+ [["EISDIR"], 400, "IS_DIRECTORY", "The target is a folder."],
1104
+ ];
1105
+ const FS_ERRNO_LOOKUP = new Map(FS_ERRNO_TABLE.flatMap(([codes, status, code, message]) => codes.map((errno) => [errno, [status, code, message]])));
1106
+ // Translate a Node fs errno into a FilesError without ever echoing the absolute path or the raw OS
1107
+ // message back to the client — mirroring the non-probeable DENIED_MESSAGE discipline. A FilesError is
1108
+ // passed through unchanged; an unrecognised error becomes a generic 500 so an internal detail (e.g.
1109
+ // a path embedded in the OS message) cannot leak through the response body.
1110
+ function mapNodeFsError(error) {
1111
+ if (error instanceof FilesError)
1112
+ return error;
1113
+ const code = typeof error === "object" && error !== null ? error.code : undefined;
1114
+ const mapped = code !== undefined ? FS_ERRNO_LOOKUP.get(code) : undefined;
1115
+ if (mapped !== undefined) {
1116
+ return new FilesError(mapped[0], mapped[1], mapped[2]);
1117
+ }
1118
+ return new FilesError(500, "IO_ERROR", "The file operation could not be completed.");
1119
+ }
1120
+ // Resolve a path that should NOT exist yet (a create destination, or a rename target). The PARENT
1121
+ // directory must already exist; it is resolved through symlinks and re-checked for containment and
1122
+ // deny so a link in the parent chain cannot redirect the new entry outside the root. The final name
1123
+ // is validated as a single, safe path segment.
1124
+ // The final segment of a new entry must be a single, safe name (the parent path was already split
1125
+ // off and validated). normalizeRelativePath has already rejected NUL/absolute/`..`-escape inputs.
1126
+ function assertSafeLeafName(name) {
1127
+ if (name.length === 0 || name === "." || name === "..") {
1128
+ throw new FilesError(400, "BAD_PATH", "The new entry name is not valid.");
1129
+ }
1130
+ }
1131
+ // Resolve the directory a new entry will be created in: it must already exist, be a directory,
1132
+ // resolve (through symlinks) to a path inside the root, and not be deny-listed.
1133
+ async function resolveContainedParentDir(realRoot, parentRelative) {
1134
+ const parentNative = nativePath(realRoot, parentRelative === "." ? "" : parentRelative);
1135
+ let realParent;
1136
+ try {
1137
+ realParent = await realpath(parentNative);
1138
+ }
1139
+ catch {
1140
+ throw new FilesError(404, "PARENT_NOT_FOUND", "The destination folder does not exist.");
1141
+ }
1142
+ if (!(await stat(realParent)).isDirectory()) {
1143
+ throw new FilesError(400, "NOT_DIRECTORY", "The destination is not a folder.");
1144
+ }
1145
+ if (!isContained(realRoot, realParent)) {
1146
+ throw new FilesError(403, "PATH_ESCAPE", "The destination is outside the selected root.");
1147
+ }
1148
+ const parentRelReal = rootRelativePosixPath(realRoot, realParent);
1149
+ if (parentRelReal.length > 0 && pathIsDenied(parentRelReal)) {
1150
+ throw new FilesError(403, "DENIED", DENIED_MESSAGE);
1151
+ }
1152
+ return realParent;
1153
+ }
1154
+ async function resolveCreationTarget(store, rootInput, pathInput, redactor) {
1155
+ const root = await resolveRoot(store, rootInput, redactor);
1156
+ const relativePath = normalizeRelativePath(pathInput);
1157
+ if (relativePath.length === 0) {
1158
+ throw new FilesError(400, "BAD_PATH", "A new entry needs a name inside the selected root.");
1159
+ }
1160
+ assertMetadataSafe(relativePath, redactor);
1161
+ // Deny check runs before any existence probe so a denied path is never distinguishable by status.
1162
+ if (pathIsDenied(relativePath)) {
1163
+ throw new FilesError(403, "DENIED", DENIED_MESSAGE);
1164
+ }
1165
+ const name = pathPosix.basename(relativePath);
1166
+ assertSafeLeafName(name);
1167
+ const realParent = await resolveContainedParentDir(root.realRoot, pathPosix.dirname(relativePath));
1168
+ const targetNative = join(realParent, name);
1169
+ if (!isContained(root.realRoot, targetNative)) {
1170
+ throw new FilesError(403, "PATH_ESCAPE", "The destination is outside the selected root.");
1171
+ }
1172
+ const targetRel = rootRelativePosixPath(root.realRoot, targetNative);
1173
+ assertMetadataSafe(targetRel, redactor);
1174
+ if (pathIsDenied(targetRel)) {
1175
+ throw new FilesError(403, "DENIED", DENIED_MESSAGE);
1176
+ }
1177
+ return { root: root.root, realRoot: root.realRoot, relativePath: targetRel, path: targetNative };
1178
+ }
1179
+ export async function createFilesEntry(args) {
1180
+ if (args.kind !== "file" && args.kind !== "directory") {
1181
+ throw new FilesError(400, "BAD_REQUEST", "A new entry must be a file or a directory.");
1182
+ }
1183
+ const target = await resolveCreationTarget(args.store, args.rootInput, args.pathInput, args.redactor ?? staticFilesMetadataRedactor);
1184
+ try {
1185
+ if (args.kind === "directory") {
1186
+ // Non-recursive: the parent was already verified, and EEXIST surfaces as a clean 409.
1187
+ await mkdir(target.path);
1188
+ }
1189
+ else {
1190
+ // `wx` = O_CREAT | O_EXCL: atomically refuse to overwrite an existing entry AND refuse to
1191
+ // follow a final symlink, closing the create-time TOCTOU/symlink window.
1192
+ await writeFile(target.path, "", { flag: "wx" });
1193
+ }
1194
+ }
1195
+ catch (error) {
1196
+ throw mapNodeFsError(error);
1197
+ }
1198
+ return { root: target.root, path: target.relativePath, kind: args.kind };
1199
+ }
1200
+ // No-clobber guard for a rename. Refuses any pre-existing destination (lstat so a symlink there is
1201
+ // detected too), with ONE exception: a pure case-only rename on a case-insensitive filesystem
1202
+ // (macOS/Windows), where `lstat(newPath)` resolves to the SAME inode as the source — legitimate
1203
+ // because it only changes the on-disk case. The exception is gated by realpath identity, never a raw
1204
+ // string compare. rename() has no atomic no-overwrite flag in Node; containment + deny bound the
1205
+ // residual TOCTOU window.
1206
+ async function assertRenameDestinationFree(targetPath, sourcePath) {
1207
+ try {
1208
+ await lstat(targetPath);
1209
+ }
1210
+ catch {
1211
+ return; // destination does not exist — free to use
1212
+ }
1213
+ let resolved;
1214
+ try {
1215
+ resolved = await realpath(targetPath);
1216
+ }
1217
+ catch {
1218
+ resolved = null;
1219
+ }
1220
+ if (resolved !== sourcePath) {
1221
+ throw new FilesError(409, "ALREADY_EXISTS", "An entry with that name already exists.");
1222
+ }
1223
+ }
1224
+ export async function renameFilesEntry(args) {
1225
+ const redactor = args.redactor ?? staticFilesMetadataRedactor;
1226
+ const source = await resolveInsideRoot(args.store, args.rootInput, args.pathInput, redactor);
1227
+ if (source.relativePath.length === 0) {
1228
+ throw new FilesError(400, "BAD_PATH", "The root folder cannot be renamed.");
1229
+ }
1230
+ if (source.symlink) {
1231
+ throw new FilesError(400, "UNSUPPORTED", "Symbolic links cannot be renamed here.");
1232
+ }
1233
+ if (args.baseVersion !== undefined && source.stats.isFile()) {
1234
+ await assertSessionNotStale(source, args.baseVersion);
1235
+ }
1236
+ const kind = source.stats.isDirectory() ? "directory" : "file";
1237
+ const target = await resolveCreationTarget(args.store, args.rootInput, args.newPathInput, redactor);
1238
+ if (target.relativePath === source.relativePath) {
1239
+ throw new FilesError(409, "ALREADY_EXISTS", "The new name matches the current name.");
1240
+ }
1241
+ // A folder cannot be moved into itself or one of its own descendants (it would orphan the subtree).
1242
+ if (target.relativePath.startsWith(`${source.relativePath}/`)) {
1243
+ throw new FilesError(400, "BAD_PATH", "A folder cannot be moved into itself.");
1244
+ }
1245
+ await assertRenameDestinationFree(target.path, source.path);
1246
+ try {
1247
+ await rename(source.path, target.path);
1248
+ }
1249
+ catch (error) {
1250
+ throw mapNodeFsError(error);
1251
+ }
1252
+ return {
1253
+ root: target.root,
1254
+ path: target.relativePath,
1255
+ previousPath: source.relativePath,
1256
+ kind,
1257
+ };
1258
+ }
1259
+ export async function deleteFilesEntry(args) {
1260
+ const target = await resolveInsideRoot(args.store, args.rootInput, args.pathInput, args.redactor ?? staticFilesMetadataRedactor);
1261
+ if (target.relativePath.length === 0) {
1262
+ throw new FilesError(400, "BAD_PATH", "The root folder cannot be deleted.");
1263
+ }
1264
+ if (target.symlink) {
1265
+ throw new FilesError(400, "UNSUPPORTED", "Symbolic links cannot be deleted here.");
1266
+ }
1267
+ if (args.baseVersion !== undefined && target.stats.isFile()) {
1268
+ await assertSessionNotStale(target, args.baseVersion);
1269
+ }
1270
+ const kind = target.stats.isDirectory() ? "directory" : "file";
1271
+ // `recursive` removes a non-empty folder, matching editor expectations. Containment + the deny-list
1272
+ // bound the blast radius to inside the selected root and away from .git/node_modules/secrets, and
1273
+ // symlinks are rejected above so `rm` never recurses THROUGH a link out of the root.
1274
+ try {
1275
+ await rm(target.path, { recursive: kind === "directory", force: false });
1276
+ }
1277
+ catch (error) {
1278
+ throw mapNodeFsError(error);
1279
+ }
1280
+ return { root: target.root, path: target.relativePath, kind };
1281
+ }
1282
+ export async function copyFilesEntry(args) {
1283
+ const redactor = args.redactor ?? staticFilesMetadataRedactor;
1284
+ // Source must exist, be contained, and not be denied or a symlink (we never dereference one).
1285
+ const source = await resolveInsideRoot(args.store, args.rootInput, args.sourcePathInput, redactor);
1286
+ if (source.relativePath.length === 0) {
1287
+ throw new FilesError(400, "BAD_PATH", "The root folder cannot be copied.");
1288
+ }
1289
+ if (source.symlink) {
1290
+ throw new FilesError(400, "UNSUPPORTED", "Symbolic links cannot be copied here.");
1291
+ }
1292
+ const kind = source.stats.isDirectory() ? "directory" : "file";
1293
+ // Destination resolves like a create target: parent must exist + be contained + non-denied.
1294
+ const target = await resolveCreationTarget(args.store, args.rootInput, args.destPathInput, redactor);
1295
+ if (target.relativePath === source.relativePath ||
1296
+ target.relativePath.startsWith(`${source.relativePath}/`)) {
1297
+ throw new FilesError(400, "BAD_PATH", "A folder cannot be copied into itself.");
1298
+ }
1299
+ try {
1300
+ // `force:false` + `errorOnExist` refuse to overwrite; `dereference:false` copies symlinks as
1301
+ // links (never follows one out of the root). Contents stay inside the root throughout.
1302
+ await cp(source.path, target.path, {
1303
+ recursive: kind === "directory",
1304
+ force: false,
1305
+ errorOnExist: true,
1306
+ dereference: false,
1307
+ });
1308
+ }
1309
+ catch (error) {
1310
+ throw mapNodeFsError(error);
1311
+ }
1312
+ return {
1313
+ root: target.root,
1314
+ path: target.relativePath,
1315
+ previousPath: source.relativePath,
1316
+ kind,
1317
+ };
1318
+ }
650
1319
  export async function readFilesPreview(store, rootInput, pathInput, redactor = staticFilesMetadataRedactor) {
651
1320
  const target = await resolveInsideRoot(store, rootInput, pathInput, redactor);
652
1321
  if (!target.stats.isFile()) {
@@ -677,6 +1346,12 @@ export async function handleFilesTree(ctx, deps) {
677
1346
  body: await readFilesTree(deps.store, ctx.url.searchParams.get("root"), ctx.url.searchParams.get("path"), deps.redactor),
678
1347
  }));
679
1348
  }
1349
+ export async function handleFilesSearch(ctx, deps) {
1350
+ return runFilesHandler(async () => ({
1351
+ status: 200,
1352
+ body: await searchFiles(deps.store, ctx.url.searchParams.get("root"), ctx.url.searchParams.get("q") ?? ctx.url.searchParams.get("query"), parseSearchLimit(ctx.url.searchParams.get("limit")), deps.redactor),
1353
+ }));
1354
+ }
680
1355
  export async function handleFilesPreview(ctx, deps) {
681
1356
  return runFilesHandler(async () => ({
682
1357
  status: 200,
@@ -731,3 +1406,126 @@ export async function handleFilesContent(ctx, deps) {
731
1406
  ? readFilesContentRoute(ctx, deps)
732
1407
  : writeFilesContentRoute(ctx, deps));
733
1408
  }
1409
+ // Bounded body for a mutation request: a path plus a few short fields, never file content.
1410
+ const MAX_FILES_MUTATION_BODY_BYTES = 16_384;
1411
+ export async function handleFilesCreate(ctx, deps) {
1412
+ return runFilesHandler(async () => {
1413
+ const body = await readJsonObject(ctx.req, MAX_FILES_MUTATION_BODY_BYTES);
1414
+ if (isRouteResult(body))
1415
+ return body;
1416
+ const rootInput = typeof body.root === "string" ? body.root : null;
1417
+ const pathInput = typeof body.path === "string" ? body.path : null;
1418
+ const kind = body.kind === "directory" ? "directory" : body.kind === "file" ? "file" : null;
1419
+ if (rootInput === null || pathInput === null || kind === null) {
1420
+ return {
1421
+ status: 400,
1422
+ body: errorBody("BAD_REQUEST", "root, path, and kind ('file' or 'directory') are required to create an entry."),
1423
+ };
1424
+ }
1425
+ return {
1426
+ status: 201,
1427
+ body: await createFilesEntry({
1428
+ store: deps.store,
1429
+ rootInput,
1430
+ pathInput,
1431
+ kind,
1432
+ redactor: deps.redactor,
1433
+ }),
1434
+ };
1435
+ });
1436
+ }
1437
+ // Parse an optional `baseVersion` from a mutation body: undefined when absent, the parsed version when
1438
+ // valid, or a 400 RouteResult when present-but-malformed (Issue 2.6).
1439
+ function parseOptionalBaseVersion(body) {
1440
+ if (body.baseVersion === undefined)
1441
+ return { version: undefined };
1442
+ const parsed = parseEditorDocumentVersion(body.baseVersion);
1443
+ if (!parsed.ok) {
1444
+ return { status: 400, body: errorBody("BAD_REQUEST", "baseVersion is not a valid version.") };
1445
+ }
1446
+ return { version: parsed.value };
1447
+ }
1448
+ export async function handleFilesRename(ctx, deps) {
1449
+ return runFilesHandler(async () => {
1450
+ const body = await readJsonObject(ctx.req, MAX_FILES_MUTATION_BODY_BYTES);
1451
+ if (isRouteResult(body))
1452
+ return body;
1453
+ const rootInput = typeof body.root === "string" ? body.root : null;
1454
+ const pathInput = typeof body.path === "string" ? body.path : null;
1455
+ const newPathInput = typeof body.newPath === "string" ? body.newPath : null;
1456
+ if (rootInput === null || pathInput === null || newPathInput === null) {
1457
+ return {
1458
+ status: 400,
1459
+ body: errorBody("BAD_REQUEST", "root, path, and newPath are required to rename an entry."),
1460
+ };
1461
+ }
1462
+ const baseVersion = parseOptionalBaseVersion(body);
1463
+ if (isRouteResult(baseVersion))
1464
+ return baseVersion;
1465
+ return {
1466
+ status: 200,
1467
+ body: await renameFilesEntry({
1468
+ store: deps.store,
1469
+ rootInput,
1470
+ pathInput,
1471
+ newPathInput,
1472
+ baseVersion: baseVersion.version,
1473
+ redactor: deps.redactor,
1474
+ }),
1475
+ };
1476
+ });
1477
+ }
1478
+ export async function handleFilesDelete(ctx, deps) {
1479
+ return runFilesHandler(async () => {
1480
+ const body = await readJsonObject(ctx.req, MAX_FILES_MUTATION_BODY_BYTES);
1481
+ if (isRouteResult(body))
1482
+ return body;
1483
+ const rootInput = typeof body.root === "string" ? body.root : null;
1484
+ const pathInput = typeof body.path === "string" ? body.path : null;
1485
+ if (rootInput === null || pathInput === null) {
1486
+ return {
1487
+ status: 400,
1488
+ body: errorBody("BAD_REQUEST", "root and path are required to delete an entry."),
1489
+ };
1490
+ }
1491
+ const baseVersion = parseOptionalBaseVersion(body);
1492
+ if (isRouteResult(baseVersion))
1493
+ return baseVersion;
1494
+ return {
1495
+ status: 200,
1496
+ body: await deleteFilesEntry({
1497
+ store: deps.store,
1498
+ rootInput,
1499
+ pathInput,
1500
+ baseVersion: baseVersion.version,
1501
+ redactor: deps.redactor,
1502
+ }),
1503
+ };
1504
+ });
1505
+ }
1506
+ export async function handleFilesCopy(ctx, deps) {
1507
+ return runFilesHandler(async () => {
1508
+ const body = await readJsonObject(ctx.req, MAX_FILES_MUTATION_BODY_BYTES);
1509
+ if (isRouteResult(body))
1510
+ return body;
1511
+ const rootInput = typeof body.root === "string" ? body.root : null;
1512
+ const sourcePathInput = typeof body.sourcePath === "string" ? body.sourcePath : null;
1513
+ const destPathInput = typeof body.destPath === "string" ? body.destPath : null;
1514
+ if (rootInput === null || sourcePathInput === null || destPathInput === null) {
1515
+ return {
1516
+ status: 400,
1517
+ body: errorBody("BAD_REQUEST", "root, sourcePath, and destPath are required to copy an entry."),
1518
+ };
1519
+ }
1520
+ return {
1521
+ status: 201,
1522
+ body: await copyFilesEntry({
1523
+ store: deps.store,
1524
+ rootInput,
1525
+ sourcePathInput,
1526
+ destPathInput,
1527
+ redactor: deps.redactor,
1528
+ }),
1529
+ };
1530
+ });
1531
+ }