@covro/covro 0.3.1-dev.1 → 0.3.1-dev.3

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 (130) hide show
  1. package/README.md +16 -10
  2. package/README.zh-CN.md +16 -10
  3. package/dist/drizzle/0006_project_order.sql +8 -0
  4. package/dist/drizzle/meta/0006_snapshot.json +488 -0
  5. package/dist/drizzle/meta/_journal.json +7 -0
  6. package/dist/server/config-store.js +10 -2
  7. package/dist/server/config-store.js.map +2 -2
  8. package/dist/server/config.js +4 -0
  9. package/dist/server/config.js.map +2 -2
  10. package/dist/server/db/schema.js +1 -0
  11. package/dist/server/db/schema.js.map +2 -2
  12. package/dist/server/http.js +43 -5
  13. package/dist/server/http.js.map +2 -2
  14. package/dist/server/local-files.js.map +1 -1
  15. package/dist/server/project-clones.js +2 -2
  16. package/dist/server/project-clones.js.map +2 -2
  17. package/dist/server/project-paths.js +39 -0
  18. package/dist/server/project-paths.js.map +7 -0
  19. package/dist/server/projects.js +48 -5
  20. package/dist/server/projects.js.map +2 -2
  21. package/dist/server/service.js +15 -3
  22. package/dist/server/service.js.map +2 -2
  23. package/dist/server/text-files.js +32 -0
  24. package/dist/server/text-files.js.map +7 -0
  25. package/dist/shared/file-preview.js +22 -0
  26. package/dist/shared/file-preview.js.map +7 -0
  27. package/dist/shared/project-path.js +8 -0
  28. package/dist/shared/project-path.js.map +7 -0
  29. package/dist/shared/release-notes.js +9 -0
  30. package/dist/shared/release-notes.js.map +7 -0
  31. package/dist/shared/updates.js.map +2 -2
  32. package/dist/shared/validation.js +2 -0
  33. package/dist/shared/validation.js.map +2 -2
  34. package/dist/update-worker.cjs +8 -3
  35. package/dist/updates/manager.js +7 -4
  36. package/dist/updates/manager.js.map +2 -2
  37. package/dist/updates/registry.js +21 -9
  38. package/dist/updates/registry.js.map +3 -3
  39. package/dist/updates/release-notes.js +73 -0
  40. package/dist/updates/release-notes.js.map +7 -0
  41. package/dist/updates/worker.js +3 -3
  42. package/dist/updates/worker.js.map +2 -2
  43. package/dist/web/assets/{abnfDiagram-WEYOIA3C-DLvlob5t.js → abnfDiagram-WEYOIA3C-CgSmgOr1.js} +1 -1
  44. package/dist/web/assets/{arc-DgauwgFe.js → arc-DHf4CdSk.js} +1 -1
  45. package/dist/web/assets/{architecture-WOLXFQ4H-DvjLv0QN.js → architecture-WOLXFQ4H-DMTD1pHe.js} +1 -1
  46. package/dist/web/assets/{architectureDiagram-ZHVYKT53-BAyZGlzu.js → architectureDiagram-ZHVYKT53-DTR8wiWo.js} +1 -1
  47. package/dist/web/assets/{blockDiagram-RJAE7QA3-BVfkmjlt.js → blockDiagram-RJAE7QA3-DcNiTPKZ.js} +1 -1
  48. package/dist/web/assets/{c4Diagram-XDPPPE54-rTsfyMoB.js → c4Diagram-XDPPPE54-BDxJVXjQ.js} +1 -1
  49. package/dist/web/assets/channel-B6d61z43.js +1 -0
  50. package/dist/web/assets/{chunk-2E7WHVMD-CQ_N_vLS.js → chunk-2E7WHVMD-D5oKHRGT.js} +1 -1
  51. package/dist/web/assets/{chunk-4EMNKR2T-wBy_gyo6.js → chunk-4EMNKR2T-CtrbVCM-.js} +1 -1
  52. package/dist/web/assets/{chunk-55XCU76K-DYiwIhmI.js → chunk-55XCU76K-Dhl7DEL7.js} +1 -1
  53. package/dist/web/assets/{chunk-7S6XSTFO-CRGtfVHg.js → chunk-7S6XSTFO-C8EwegP-.js} +1 -1
  54. package/dist/web/assets/{chunk-AQDTSKZB-1G2Za6bm.js → chunk-AQDTSKZB-lFiNKr7B.js} +1 -1
  55. package/dist/web/assets/{chunk-DSNLSHJZ-B1LLoYuG.js → chunk-DSNLSHJZ-B8GftONA.js} +1 -1
  56. package/dist/web/assets/{chunk-HAGFOTED-78h1RpHh.js → chunk-HAGFOTED-Cnc2mh7_.js} +1 -1
  57. package/dist/web/assets/{chunk-HNE4Q36P-DmR5B27Q.js → chunk-HNE4Q36P-BKl6ZgI0.js} +1 -1
  58. package/dist/web/assets/{chunk-NQOKRVIF-D7WSEPzg.js → chunk-NQOKRVIF-qKlxSw3D.js} +1 -1
  59. package/dist/web/assets/{chunk-UHW7PFBP-gl7a5V-l.js → chunk-UHW7PFBP-C_zh3mNe.js} +1 -1
  60. package/dist/web/assets/{chunk-VQ5W62QI-KaBXxBre.js → chunk-VQ5W62QI-BXoyXK0q.js} +1 -1
  61. package/dist/web/assets/{chunk-WQQL4CDK-DDczFoOr.js → chunk-WQQL4CDK-BBeqcU1J.js} +1 -1
  62. package/dist/web/assets/{chunk-XXDRQBXY-3ug4Qhxg.js → chunk-XXDRQBXY-DzssZtz_.js} +1 -1
  63. package/dist/web/assets/{chunk-YZO67HFG-BpWOuCXP.js → chunk-YZO67HFG-BbNSwLjS.js} +1 -1
  64. package/dist/web/assets/{chunk-ZAUX2LTX-1ojPb1if.js → chunk-ZAUX2LTX-CjURwZfv.js} +1 -1
  65. package/dist/web/assets/{chunk-ZCHMQSOR-D2kK2cPj.js → chunk-ZCHMQSOR-u39dymjE.js} +1 -1
  66. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-B7eMvxAO.js → classDiagram-v2-4MYWR3UA-BEXx5gKT.js} +1 -1
  67. package/dist/web/assets/{cose-bilkent-JH36ORCC-BtB8mbt8.js → cose-bilkent-JH36ORCC-BxDEucP_.js} +1 -1
  68. package/dist/web/assets/{cynefin-EF2NZ3EQ-CIctqxlE.js → cynefin-EF2NZ3EQ-BhkCsT6-.js} +1 -1
  69. package/dist/web/assets/{cynefinDiagram-WF24UDRP-qat2GD9P.js → cynefinDiagram-WF24UDRP-Wpu-yNke.js} +1 -1
  70. package/dist/web/assets/{dagre-NK4GIZVX-OJamzzID.js → dagre-NK4GIZVX-LNSSIN4l.js} +1 -1
  71. package/dist/web/assets/{diagram-CBK3MWTC-C7cX2tqO.js → diagram-CBK3MWTC-BVDr5Vx9.js} +1 -1
  72. package/dist/web/assets/{diagram-I5XIIV2D-DsMfKVN7.js → diagram-I5XIIV2D-MnLzR1Rv.js} +1 -1
  73. package/dist/web/assets/{diagram-ODIL2UCW-Cz66FxdJ.js → diagram-ODIL2UCW-CZ4igbo_.js} +1 -1
  74. package/dist/web/assets/{diagram-PFBEJUXE-CwhPuwKj.js → diagram-PFBEJUXE-4hCnyMdK.js} +1 -1
  75. package/dist/web/assets/{diagram-RJA3I4VV-C52IGiFJ.js → diagram-RJA3I4VV-DTo42rGA.js} +1 -1
  76. package/dist/web/assets/{diagram-TYUNFSNR-C2LZVLKY.js → diagram-TYUNFSNR-BC12iCUZ.js} +1 -1
  77. package/dist/web/assets/{dist-Bxu8YbZb.js → dist-Dg_ysSxT.js} +1 -1
  78. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-eQKKlMli.js → ebnfDiagram-V4P3TTPT-Du8LCQ31.js} +1 -1
  79. package/dist/web/assets/{elk-QONTBCTQ-C5kIOZTA.js → elk-QONTBCTQ-BzTAR-T1.js} +1 -1
  80. package/dist/web/assets/{erDiagram-KYQIGQAA-CMTowz5P.js → erDiagram-KYQIGQAA-SXYIjNOP.js} +1 -1
  81. package/dist/web/assets/{eventmodeling-K75KTNOO-C5v-UF8X.js → eventmodeling-K75KTNOO-Sbno3ZxQ.js} +1 -1
  82. package/dist/web/assets/flowDiagram-IUGLLWBU-Ae1H3vSR.js +1 -0
  83. package/dist/web/assets/{ganttDiagram-R7GPNTJY-B0viiC-b.js → ganttDiagram-R7GPNTJY-DBeX1qmu.js} +1 -1
  84. package/dist/web/assets/{gitGraph-VSP46ZUC-tEjFmPhN.js → gitGraph-VSP46ZUC-CKWihueS.js} +1 -1
  85. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-CsOwt7jz.js → gitGraphDiagram-PDYL7XHY-Cj1E3jqN.js} +1 -1
  86. package/dist/web/assets/index-CtTuHkNv.js +147 -0
  87. package/dist/web/assets/index-DwnjYADO.css +1 -0
  88. package/dist/web/assets/{info-OHQRW6UA-DnFGu4Ew.js → info-OHQRW6UA-D0w3nR0p.js} +1 -1
  89. package/dist/web/assets/{infoDiagram-MVDRKAX5-B6WiNaDF.js → infoDiagram-MVDRKAX5-F0jTCG-G.js} +1 -1
  90. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-Dn70fI1r.js → ishikawaDiagram-HPEHV2GE-CwQGP9AT.js} +1 -1
  91. package/dist/web/assets/{journeyDiagram-BOSHW7O7-BUjYzV2f.js → journeyDiagram-BOSHW7O7-CPY5GqD2.js} +1 -1
  92. package/dist/web/assets/{kanban-definition-FNSA4HC5-BDMfWbtB.js → kanban-definition-FNSA4HC5-BCL38eXh.js} +1 -1
  93. package/dist/web/assets/{line-DNSRZHBK.js → line-DoUzbVJS.js} +1 -1
  94. package/dist/web/assets/{linear-CrS49q6J.js → linear-LoqU-SEO.js} +1 -1
  95. package/dist/web/assets/{mermaid-parser.core-DXuyO8Px.js → mermaid-parser.core-CvO-MTG1.js} +3 -3
  96. package/dist/web/assets/{mermaid.core-DUTnbQkq.js → mermaid.core-DcCEGt0V.js} +4 -4
  97. package/dist/web/assets/{mindmap-definition-6OY57UG3-CqZTtItJ.js → mindmap-definition-6OY57UG3-yZvSibNz.js} +1 -1
  98. package/dist/web/assets/{packet-JDAUHWVQ-DXEZgY0a.js → packet-JDAUHWVQ-9ZiCegsV.js} +1 -1
  99. package/dist/web/assets/{pegDiagram-ECIUJNBX-D6CCPNem.js → pegDiagram-ECIUJNBX-BkGwBoEk.js} +1 -1
  100. package/dist/web/assets/{pie-XZMESJXO-AXRFjZ4G.js → pie-XZMESJXO-kAXIwFnU.js} +1 -1
  101. package/dist/web/assets/{pieDiagram-GNMPY74T-B9Ss8_5s.js → pieDiagram-GNMPY74T-jiwTgtdb.js} +1 -1
  102. package/dist/web/assets/{quadrantDiagram-D57K4MC3-JrlylCtz.js → quadrantDiagram-D57K4MC3-u9HdbUwr.js} +1 -1
  103. package/dist/web/assets/{radar-ABXABTNO-ChFm4hZg.js → radar-ABXABTNO-BQ911CvQ.js} +1 -1
  104. package/dist/web/assets/{railroad-I3PHUGI6-DaXXzqhZ.js → railroad-I3PHUGI6-Bvm7MWf9.js} +1 -1
  105. package/dist/web/assets/{railroad-abnf-LNEFI6M7-DGE67q11.js → railroad-abnf-LNEFI6M7-BfFmWnzu.js} +1 -1
  106. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-Cq9S3q72.js → railroad-ebnf-DWYD2UWJ-B8Ekz4aD.js} +1 -1
  107. package/dist/web/assets/{railroad-peg-JR7BVNR7-D2Gth3eY.js → railroad-peg-JR7BVNR7-B5ANjRbC.js} +1 -1
  108. package/dist/web/assets/{railroadDiagram-Y72NRBL7-BpkXX59H.js → railroadDiagram-Y72NRBL7-iu8Hmuma.js} +1 -1
  109. package/dist/web/assets/{requirementDiagram-EQYSW5RA-oNp3DMM4.js → requirementDiagram-EQYSW5RA-DgiEaaYZ.js} +1 -1
  110. package/dist/web/assets/{sankeyDiagram-SY24JHO5-bHfheSKO.js → sankeyDiagram-SY24JHO5-KzDvhd8J.js} +1 -1
  111. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-CEM2XfNF.js → sequenceDiagram-BLEHNHQU-DJMSHuO_.js} +1 -1
  112. package/dist/web/assets/{src-KjMctvzy.js → src-C5WkvxTS.js} +1 -1
  113. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-D_TaR_ix.js → stateDiagram-v2-A6IESKWZ-CyfHfUyU.js} +1 -1
  114. package/dist/web/assets/{swimlanes-B3ENRKFQ-BUYl2fsg.js → swimlanes-B3ENRKFQ-YPSzHf2U.js} +1 -1
  115. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-D2_NQS-o.js +8 -0
  116. package/dist/web/assets/{timeline-definition-JXYKGLGB-CpKsGN6d.js → timeline-definition-JXYKGLGB-BwvPIiSj.js} +1 -1
  117. package/dist/web/assets/{treeView-D4JQ5SDB-Dioaagqt.js → treeView-D4JQ5SDB-DsUhEw24.js} +1 -1
  118. package/dist/web/assets/{treemap-SAJKECNS-B8NLQ14o.js → treemap-SAJKECNS-D5NThB4V.js} +1 -1
  119. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-CfwdxcsG.js → usecaseDiagram-WPOLBNNX-fLWGvuS0.js} +1 -1
  120. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-DQoKGtXo.js → vennDiagram-Q3Y3IDCW-DFzlk_u2.js} +1 -1
  121. package/dist/web/assets/{wardley-7MLQ67FV-DQHLHmeZ.js → wardley-7MLQ67FV-0uG-noJX.js} +1 -1
  122. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-DV-lq7zb.js → wardleyDiagram-IVAIDKWA-DfEQPTLg.js} +1 -1
  123. package/dist/web/assets/{xychartDiagram-4URNHDU4-wV4ueoGC.js → xychartDiagram-4URNHDU4-BXVsWbL0.js} +1 -1
  124. package/dist/web/index.html +2 -2
  125. package/package.json +4 -4
  126. package/dist/web/assets/channel-GGfPocHk.js +0 -1
  127. package/dist/web/assets/flowDiagram-IUGLLWBU-C-jE8afx.js +0 -1
  128. package/dist/web/assets/index-COSho7m2.css +0 -1
  129. package/dist/web/assets/index-D7GZPtTj.js +0 -147
  130. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-DDLmyR4x.js +0 -8
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/server/project-clones.ts"],
4
- "sourcesContent": ["import { spawn, type ChildProcess } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { accessSync, constants, lstatSync, readdirSync, realpathSync } from 'node:fs';\nimport { basename, dirname, isAbsolute, join, resolve } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport type {\n CloneProjectInput,\n CreateProjectInput,\n Project,\n ProjectClone,\n} from '../shared/types.js';\nimport { cloneProjectSchema } from '../shared/validation.js';\nimport { containsPath } from './projects.js';\n\ninterface Job {\n view: ProjectClone;\n fingerprint: string;\n child?: ChildProcess;\n done: Promise<void>;\n}\nconst active = (job: Job) => ['cloning', 'cancelling'].includes(job.view.status);\nconst hasControl = (value: string) =>\n [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);\n\n/** Only remote Git transports; never pass a local path, option, or remote helper to Git. */\nexport function validateRepository(repository: string): void {\n let valid = false;\n if (!hasControl(repository) && !/[\\s\\\\]/.test(repository)) {\n if (/^(https|ssh):\\/\\//.test(repository)) {\n try {\n const url = new URL(repository);\n valid = Boolean(\n url.hostname &&\n !url.hostname.startsWith('-') &&\n url.pathname.length > 1 &&\n !url.password &&\n !url.search &&\n !url.hash &&\n (url.protocol === 'ssh:'\n ? /^(?:[a-zA-Z0-9_][\\w.-]*)?$/.test(url.username)\n : !url.username),\n );\n } catch {\n /* Report a fixed message, never the supplied URL. */\n }\n } else {\n valid = /^(?:[a-zA-Z0-9_][\\w.-]*@)?[a-zA-Z0-9][\\w.-]*:[^/:\\s][^\\s]*$/.test(repository);\n }\n }\n if (!valid)\n throw new AppError(\n 'INVALID_REPOSITORY',\n '\u8BF7\u8F93\u5165 HTTPS \u6216 SSH Git \u4ED3\u5E93\u5730\u5740\uFF1B\u8BF7\u4F7F\u7528\u4E3B\u673A\u5DF2\u6709\u7684\u8BA4\u8BC1\uFF0C\u4E0D\u8981\u5728\u5730\u5740\u4E2D\u586B\u5199\u5BC6\u7801\u3001\u4EE4\u724C\u6216\u67E5\u8BE2\u53C2\u6570',\n 400,\n );\n}\n\nfunction targetPath(path: string): string {\n if (!isAbsolute(path) || hasControl(path) || resolve(path) === dirname(resolve(path)))\n throw new AppError('INVALID_PROJECT_PATH', '\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u7EDD\u5BF9\u76EE\u6807\u76EE\u5F55', 400);\n try {\n const target = join(realpathSync(dirname(resolve(path))), basename(resolve(path)));\n accessSync(dirname(target), constants.W_OK | constants.X_OK);\n return target;\n } catch {\n throw new AppError(\n 'CLONE_PARENT_UNAVAILABLE',\n '\u76EE\u6807\u76EE\u5F55\u7684\u7236\u76EE\u5F55\u5FC5\u987B\u5DF2\u5B58\u5728\uFF0C\u4E14\u670D\u52A1\u9700\u8981\u5199\u5165\u548C\u8FDB\u5165\u6743\u9650',\n 409,\n );\n }\n}\n\nfunction checkEmpty(path: string): void {\n try {\n const stat = lstatSync(path);\n if (!stat.isDirectory() || stat.isSymbolicLink() || readdirSync(path).length)\n throw new AppError(\n 'CLONE_TARGET_EXISTS',\n '\u76EE\u6807\u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\uFF0C\u6216\u4E0D\u662F\u666E\u901A\u76EE\u5F55\uFF1B\u8BF7\u9009\u62E9\u65B0\u7684\u7A7A\u76EE\u5F55\uFF0C\u4E0D\u4F1A\u8986\u76D6\u5DF2\u6709\u6587\u4EF6',\n 409,\n );\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n if (error instanceof AppError) throw error;\n throw new AppError(\n 'CLONE_TARGET_UNAVAILABLE',\n '\u65E0\u6CD5\u68C0\u67E5\u76EE\u6807\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u5668\u8DEF\u5F84\u548C\u8BBF\u95EE\u6743\u9650',\n 409,\n );\n }\n}\n\nfunction failure(\n output: string,\n missingGit: boolean,\n): { code: NonNullable<ProjectClone['errorCode']>; message: string } {\n if (missingGit)\n return {\n code: 'CLONE_GIT_MISSING',\n message: '\u670D\u52A1\u5668\u672A\u5B89\u88C5 Git\uFF0C\u6216\u670D\u52A1\u7684 PATH \u4E2D\u627E\u4E0D\u5230 Git\u3002\u5B89\u88C5\u5E76\u914D\u7F6E\u540E\u91CD\u8BD5\u3002',\n };\n if (\n /authentication failed|could not read username|permission denied|publickey|terminal prompts disabled|credential|access denied|401|403/i.test(\n output,\n )\n )\n return {\n code: 'CLONE_AUTH_FAILED',\n message:\n 'Git \u8BA4\u8BC1\u5931\u8D25\u6216\u6CA1\u6709\u4ED3\u5E93\u8BBF\u95EE\u6743\u9650\u3002\u8BF7\u5728\u8FD0\u884C\u670D\u52A1\u7684\u4E3B\u673A\u4E0A\u914D\u7F6E Git \u51ED\u636E\u6216 SSH \u5BC6\u94A5\u540E\u91CD\u8BD5\u3002',\n };\n if (/host key verification failed/i.test(output))\n return {\n code: 'CLONE_HOST_KEY_FAILED',\n message: 'SSH \u4E3B\u673A\u9A8C\u8BC1\u5931\u8D25\u3002\u8BF7\u5728\u8FD0\u884C\u670D\u52A1\u7684\u4E3B\u673A\u4E0A\u786E\u8BA4\u5E76\u914D\u7F6E\u4ED3\u5E93\u4E3B\u673A\u7684 SSH \u4FE1\u4EFB\u540E\u91CD\u8BD5\u3002',\n };\n if (\n /repository .*not found|does not appear to be a git repository|not a repository|404/i.test(\n output,\n )\n )\n return {\n code: 'CLONE_REPOSITORY_UNAVAILABLE',\n message: '\u4ED3\u5E93\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE\u3002\u8BF7\u68C0\u67E5\u4ED3\u5E93\u5730\u5740\u548C\u4E3B\u673A\u4E0A\u7684 Git \u8BBF\u95EE\u6743\u9650\u3002',\n };\n if (\n /could not resolve|failed to connect|unable to access|connection|timed out|certificate/i.test(\n output,\n )\n )\n return {\n code: 'CLONE_NETWORK_FAILED',\n message: '\u65E0\u6CD5\u8FDE\u63A5 Git \u4ED3\u5E93\u3002\u8BF7\u68C0\u67E5\u670D\u52A1\u5668\u7F51\u7EDC\u3001\u4EE3\u7406\u548C\u8BC1\u4E66\u914D\u7F6E\u540E\u91CD\u8BD5\u3002',\n };\n if (/already exists and is not an empty directory/i.test(output))\n return {\n code: 'CLONE_TARGET_NOT_EMPTY',\n message: '\u76EE\u6807\u76EE\u5F55\u5DF2\u5305\u542B\u6587\u4EF6\uFF0CGit \u5DF2\u505C\u6B62\u514B\u9686\uFF1B\u4E0D\u4F1A\u8986\u76D6\u6216\u5220\u9664\u5DF2\u6709\u6587\u4EF6\u3002',\n };\n if (/no space left|disk quota/i.test(output))\n return {\n code: 'CLONE_DISK_FULL',\n message: '\u670D\u52A1\u5668\u78C1\u76D8\u7A7A\u95F4\u4E0D\u8DB3\uFF0C\u8BF7\u91CA\u653E\u7A7A\u95F4\u540E\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5\u3002',\n };\n return {\n code: 'CLONE_FAILED',\n message: 'Git \u514B\u9686\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u4ED3\u5E93\u5730\u5740\u3001\u4E3B\u673A\u4E0A\u7684 Git \u8BA4\u8BC1\u3001\u76EE\u5F55\u6743\u9650\u548C\u78C1\u76D8\u7A7A\u95F4\u540E\u91CD\u8BD5\u3002',\n };\n}\n\n/** Transient jobs never persist repository URLs, credentials, or raw Git output. */\nexport class ProjectClones {\n private readonly jobs = new Map<string, Job>();\n private closing = false;\n constructor(\n private readonly register: (input: CreateProjectInput) => Project,\n private readonly registered: (path: string) => boolean,\n ) {}\n\n get hasActive(): boolean {\n return [...this.jobs.values()].some(active);\n }\n\n assertIdlePath(path: string): void {\n if (!this.hasActive) return;\n let canonical = resolve(path);\n try {\n canonical = realpathSync(path);\n } catch {\n try {\n canonical = join(realpathSync(dirname(canonical)), basename(canonical));\n } catch {\n /* Normal registration validation reports unavailable paths. */\n }\n }\n if (\n [...this.jobs.values()].some(\n (job) =>\n active(job) &&\n (containsPath(job.view.path, canonical) || containsPath(canonical, job.view.path)),\n )\n )\n throw new AppError('CLONE_IN_PROGRESS', '\u8BE5\u76EE\u5F55\u6B63\u5728\u514B\u9686\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u6216\u53D6\u6D88\u540E\u518D\u64CD\u4F5C', 409);\n }\n\n start(input: CloneProjectInput): ProjectClone {\n const parsed = cloneProjectSchema.parse(input);\n validateRepository(parsed.repository);\n const fingerprint = createHash('sha256').update(JSON.stringify(parsed)).digest('hex');\n const previous = this.jobs.get(parsed.clientOperationId);\n if (previous) {\n if (previous.fingerprint !== fingerprint)\n throw new AppError('CLONE_ID_CONFLICT', '\u8BE5\u64CD\u4F5C ID \u5DF2\u7528\u4E8E\u5176\u4ED6\u514B\u9686\u8BF7\u6C42', 409);\n return this.get(previous.view.id);\n }\n if (this.closing) throw new AppError('GATEWAY_STOPPING', '\u670D\u52A1\u6B63\u5728\u505C\u6B62\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5', 503);\n const path = targetPath(parsed.path);\n this.assertIdlePath(path);\n if (this.registered(path))\n throw new AppError('PROJECT_EXISTS', '\u8BE5\u76EE\u5F55\u5DF2\u6DFB\u52A0\u4E3A\u9879\u76EE\uFF0C\u8BF7\u4F7F\u7528\u5DF2\u6709\u9879\u76EE\u6216\u9009\u62E9\u5176\u4ED6\u76EE\u5F55', 409);\n checkEmpty(path);\n if ([...this.jobs.values()].filter(active).length >= 4)\n throw new AppError('CLONE_LIMIT', '\u5DF2\u6709\u591A\u4E2A\u4ED3\u5E93\u6B63\u5728\u514B\u9686\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u540E\u91CD\u8BD5', 409);\n // Bound transient history without evicting a running process.\n if (this.jobs.size >= 100) {\n const oldest = [...this.jobs].find(([, job]) => !active(job));\n if (oldest) this.jobs.delete(oldest[0]);\n }\n const job: Job = {\n view: {\n id: parsed.clientOperationId,\n path,\n name: parsed.name,\n status: 'cloning',\n progress: '\u6B63\u5728\u8FDE\u63A5\u4ED3\u5E93\u2026',\n progressStage: 'connecting',\n },\n fingerprint,\n done: Promise.resolve(),\n };\n this.jobs.set(job.view.id, job);\n job.done = this.run(job, parsed.repository);\n return this.get(job.view.id);\n }\n\n get(id: string): ProjectClone {\n const job = this.jobs.get(id);\n if (!job)\n throw new AppError(\n 'CLONE_NOT_FOUND',\n '\u672A\u627E\u5230\u514B\u9686\u8BB0\u5F55\uFF1B\u670D\u52A1\u91CD\u542F\u540E\u8BF7\u68C0\u67E5\u76EE\u6807\u76EE\u5F55\uFF0C\u518D\u6DFB\u52A0\u5DF2\u6709\u76EE\u5F55\u6216\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5',\n 404,\n );\n return structuredClone(job.view);\n }\n\n cancel(id: string): ProjectClone {\n const job = this.jobs.get(id);\n this.get(id);\n if (job && active(job) && job.view.status !== 'cancelling') {\n job.view.status = 'cancelling';\n job.view.progress = '\u6B63\u5728\u53D6\u6D88\uFF0C\u7B49\u5F85 Git \u9000\u51FA\u2026';\n job.view.progressStage = 'cancelling';\n this.signal(job, 'SIGTERM');\n }\n return this.get(id);\n }\n\n retryRegistration(id: string): ProjectClone {\n const job = this.jobs.get(id);\n this.get(id);\n if (job?.view.status === 'registration-failed') this.finishRegistration(job);\n return this.get(id);\n }\n\n private finishRegistration(job: Job): void {\n try {\n job.view.project = this.register({ path: job.view.path, name: job.view.name });\n job.view.status = 'completed';\n job.view.progress = '\u514B\u9686\u5B8C\u6210\uFF0C\u9879\u76EE\u5DF2\u6DFB\u52A0';\n job.view.progressStage = 'completed';\n delete job.view.errorCode;\n delete job.view.error;\n } catch {\n job.view.status = 'registration-failed';\n job.view.progress = '\u514B\u9686\u5DF2\u5B8C\u6210\uFF0C\u9879\u76EE\u5C1A\u672A\u6DFB\u52A0';\n job.view.progressStage = 'registration_failed';\n job.view.errorCode = 'CLONE_REGISTRATION_FAILED';\n job.view.error =\n '\u9879\u76EE\u6CE8\u518C\u5931\u8D25\u3002\u5DF2\u514B\u9686\u7684\u6587\u4EF6\u4FDD\u7559\u5728\u76EE\u6807\u76EE\u5F55\uFF1B\u53EF\u91CD\u8BD5\u6DFB\u52A0\u9879\u76EE\uFF0C\u65E0\u9700\u518D\u6B21\u514B\u9686\uFF0C\u4E5F\u53EF\u901A\u8FC7\u201C\u5DF2\u6709\u76EE\u5F55\u201D\u6DFB\u52A0\u3002';\n }\n }\n\n private signal(job: Job, signal: NodeJS.Signals): void {\n if (!job.child?.pid) return;\n try {\n if (process.platform === 'win32') job.child.kill(signal);\n else process.kill(-job.child.pid, signal);\n } catch {\n /* Process may have already exited. */\n }\n }\n\n private async run(job: Job, repository: string): Promise<void> {\n let output = '';\n let pending = '';\n let missingGit = false;\n let timedOut = false;\n const sshCommand =\n process.env.GIT_SSH_COMMAND ??\n (process.env.GIT_SSH ? `'${process.env.GIT_SSH.replaceAll(\"'\", \"'\\\\''\")}'` : 'ssh');\n const child = spawn(\n 'git',\n [\n '-c',\n 'protocol.allow=never',\n '-c',\n 'protocol.https.allow=always',\n '-c',\n 'protocol.ssh.allow=always',\n 'clone',\n '--progress',\n '--',\n repository,\n job.view.path,\n ],\n {\n detached: process.platform !== 'win32',\n stdio: ['ignore', 'pipe', 'pipe'],\n env: {\n ...process.env,\n GIT_TERMINAL_PROMPT: '0',\n GCM_INTERACTIVE: 'never',\n GIT_SSH_COMMAND: `${sshCommand} -oBatchMode=yes -oStrictHostKeyChecking=yes`,\n LC_ALL: 'C',\n },\n },\n );\n job.child = child;\n const read = (chunk: Buffer) => {\n output = (output + chunk.toString()).slice(-16_384);\n pending = (pending + chunk.toString()).slice(-4096);\n const lines = pending.split(/[\\r\\n]/);\n pending = lines.pop() ?? '';\n for (const line of lines) {\n const match =\n /^(?:remote: )?(Receiving objects|Resolving deltas|Updating files|Checking out files|Counting objects|Compressing objects):\\s+(\\d{1,3})%/.exec(\n line,\n );\n if (!match || job.view.status !== 'cloning') continue;\n const labels: Record<string, [NonNullable<ProjectClone['progressStage']>, string]> = {\n 'Receiving objects': ['receiving', '\u6B63\u5728\u63A5\u6536\u5BF9\u8C61'],\n 'Resolving deltas': ['resolving', '\u6B63\u5728\u89E3\u6790\u5DEE\u5F02'],\n 'Updating files': ['checking_out', '\u6B63\u5728\u68C0\u51FA\u6587\u4EF6'],\n 'Checking out files': ['checking_out', '\u6B63\u5728\u68C0\u51FA\u6587\u4EF6'],\n 'Counting objects': ['counting', '\u6B63\u5728\u7EDF\u8BA1\u5BF9\u8C61'],\n 'Compressing objects': ['compressing', '\u6B63\u5728\u538B\u7F29\u5BF9\u8C61'],\n };\n [job.view.progressStage, job.view.progress] = labels[match[1]!]!;\n job.view.percent = Math.min(100, Number(match[2]));\n }\n };\n child.stdout?.on('data', read);\n child.stderr?.on('data', read);\n const timeout = setTimeout(() => {\n timedOut = true;\n this.cancel(job.view.id);\n }, 30 * 60_000);\n timeout.unref();\n const escalation = setInterval(() => {\n if (job.view.status === 'cancelling') this.signal(job, 'SIGKILL');\n }, 3000);\n escalation.unref();\n const code = await new Promise<number | null>((done) => {\n child.once('error', (error: NodeJS.ErrnoException) => {\n missingGit = error.code === 'ENOENT';\n });\n child.once('close', done);\n });\n clearTimeout(timeout);\n clearInterval(escalation);\n job.child = undefined;\n delete job.view.percent;\n if (job.view.status === 'cancelling') {\n job.view.status = timedOut ? 'failed' : 'cancelled';\n job.view.progress = timedOut ? '\u514B\u9686\u8D85\u65F6\uFF0C\u5DF2\u505C\u6B62' : '\u5DF2\u53D6\u6D88\u514B\u9686';\n job.view.progressStage = timedOut ? 'timed_out' : 'cancelled';\n job.view.errorCode = timedOut ? 'CLONE_TIMED_OUT' : 'CLONE_CANCELLED';\n job.view.error = `${timedOut ? '\u514B\u9686\u8D85\u8FC7 30 \u5206\u949F\uFF0C\u5DF2\u505C\u6B62\u3002' : ''}\u672A\u6DFB\u52A0\u9879\u76EE\uFF0C\u4E5F\u672A\u5220\u9664\u4EFB\u4F55\u6587\u4EF6\u3002\u76EE\u6807\u76EE\u5F55\u53EF\u80FD\u6B8B\u7559\u6587\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u540E\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5\u3002`;\n } else if (code !== 0) {\n job.view.status = 'failed';\n job.view.progress = '\u514B\u9686\u5931\u8D25';\n job.view.progressStage = 'failed';\n const problem = failure(output, missingGit);\n job.view.errorCode = problem.code;\n job.view.error = `${problem.message} \u672A\u6DFB\u52A0\u9879\u76EE\uFF1B\u76EE\u6807\u76EE\u5F55\u53EF\u80FD\u6B8B\u7559\u6587\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u540E\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5\u3002`;\n } else this.finishRegistration(job);\n }\n\n async close(): Promise<void> {\n this.closing = true;\n for (const job of this.jobs.values()) if (active(job)) this.cancel(job.view.id);\n await Promise.all([...this.jobs.values()].map((job) => job.done));\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { spawn, type ChildProcess } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { accessSync, constants, lstatSync, readdirSync, realpathSync } from 'node:fs';\nimport { basename, dirname, isAbsolute, join, resolve } from 'node:path';\nimport { AppError } from '../shared/errors.js';\nimport type {\n CloneProjectInput,\n CreateProjectInput,\n Project,\n ProjectClone,\n} from '../shared/types.js';\nimport { cloneProjectSchema } from '../shared/validation.js';\nimport { containsPath } from './projects.js';\n\ninterface Job {\n view: ProjectClone;\n fingerprint: string;\n child?: ChildProcess;\n done: Promise<void>;\n}\nconst active = (job: Job) => ['cloning', 'cancelling'].includes(job.view.status);\nconst hasControl = (value: string) =>\n [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);\n\n/** Only remote Git transports; never pass a local path, option, or remote helper to Git. */\nexport function validateRepository(repository: string): void {\n let valid = false;\n if (!hasControl(repository) && !/[\\s\\\\]/.test(repository)) {\n if (/^(https|ssh):\\/\\//.test(repository)) {\n try {\n const url = new URL(repository);\n valid = Boolean(\n url.hostname &&\n !url.hostname.startsWith('-') &&\n url.pathname.length > 1 &&\n !url.password &&\n !url.search &&\n !url.hash &&\n (url.protocol === 'ssh:'\n ? /^(?:[a-zA-Z0-9_][\\w.-]*)?$/.test(url.username)\n : !url.username),\n );\n } catch {\n /* Report a fixed message, never the supplied URL. */\n }\n } else {\n valid = /^(?:[a-zA-Z0-9_][\\w.-]*@)?[a-zA-Z0-9][\\w.-]*:[^/:\\s][^\\s]*$/.test(repository);\n }\n }\n if (!valid)\n throw new AppError(\n 'INVALID_REPOSITORY',\n '\u8BF7\u8F93\u5165 HTTPS \u6216 SSH Git \u4ED3\u5E93\u5730\u5740\uFF1B\u8BF7\u4F7F\u7528\u4E3B\u673A\u5DF2\u6709\u7684\u8BA4\u8BC1\uFF0C\u4E0D\u8981\u5728\u5730\u5740\u4E2D\u586B\u5199\u5BC6\u7801\u3001\u4EE4\u724C\u6216\u67E5\u8BE2\u53C2\u6570',\n 400,\n );\n}\n\nfunction targetPath(path: string): string {\n if (!isAbsolute(path) || hasControl(path) || resolve(path) === dirname(resolve(path)))\n throw new AppError('INVALID_PROJECT_PATH', '\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u7EDD\u5BF9\u76EE\u6807\u76EE\u5F55', 400);\n try {\n const target = join(realpathSync(dirname(resolve(path))), basename(resolve(path)));\n accessSync(dirname(target), constants.W_OK | constants.X_OK);\n return target;\n } catch {\n throw new AppError(\n 'CLONE_PARENT_UNAVAILABLE',\n '\u76EE\u6807\u76EE\u5F55\u7684\u7236\u76EE\u5F55\u5FC5\u987B\u5DF2\u5B58\u5728\uFF0C\u4E14\u670D\u52A1\u9700\u8981\u5199\u5165\u548C\u8FDB\u5165\u6743\u9650',\n 409,\n );\n }\n}\n\nfunction checkEmpty(path: string): void {\n try {\n const stat = lstatSync(path);\n if (!stat.isDirectory() || stat.isSymbolicLink() || readdirSync(path).length)\n throw new AppError(\n 'CLONE_TARGET_EXISTS',\n '\u76EE\u6807\u76EE\u5F55\u5DF2\u5B58\u5728\u4E14\u975E\u7A7A\uFF0C\u6216\u4E0D\u662F\u666E\u901A\u76EE\u5F55\uFF1B\u8BF7\u9009\u62E9\u65B0\u7684\u7A7A\u76EE\u5F55\uFF0C\u4E0D\u4F1A\u8986\u76D6\u5DF2\u6709\u6587\u4EF6',\n 409,\n );\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n if (error instanceof AppError) throw error;\n throw new AppError(\n 'CLONE_TARGET_UNAVAILABLE',\n '\u65E0\u6CD5\u68C0\u67E5\u76EE\u6807\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u5668\u8DEF\u5F84\u548C\u8BBF\u95EE\u6743\u9650',\n 409,\n );\n }\n}\n\nfunction failure(\n output: string,\n missingGit: boolean,\n): { code: NonNullable<ProjectClone['errorCode']>; message: string } {\n if (missingGit)\n return {\n code: 'CLONE_GIT_MISSING',\n message: '\u670D\u52A1\u5668\u672A\u5B89\u88C5 Git\uFF0C\u6216\u670D\u52A1\u7684 PATH \u4E2D\u627E\u4E0D\u5230 Git\u3002\u5B89\u88C5\u5E76\u914D\u7F6E\u540E\u91CD\u8BD5\u3002',\n };\n if (\n /authentication failed|could not read username|permission denied|publickey|terminal prompts disabled|credential|access denied|401|403/i.test(\n output,\n )\n )\n return {\n code: 'CLONE_AUTH_FAILED',\n message:\n 'Git \u8BA4\u8BC1\u5931\u8D25\u6216\u6CA1\u6709\u4ED3\u5E93\u8BBF\u95EE\u6743\u9650\u3002\u8BF7\u5728\u8FD0\u884C\u670D\u52A1\u7684\u4E3B\u673A\u4E0A\u914D\u7F6E Git \u51ED\u636E\u6216 SSH \u5BC6\u94A5\u540E\u91CD\u8BD5\u3002',\n };\n if (/host key verification failed/i.test(output))\n return {\n code: 'CLONE_HOST_KEY_FAILED',\n message: 'SSH \u4E3B\u673A\u9A8C\u8BC1\u5931\u8D25\u3002\u8BF7\u5728\u8FD0\u884C\u670D\u52A1\u7684\u4E3B\u673A\u4E0A\u786E\u8BA4\u5E76\u914D\u7F6E\u4ED3\u5E93\u4E3B\u673A\u7684 SSH \u4FE1\u4EFB\u540E\u91CD\u8BD5\u3002',\n };\n if (\n /repository .*not found|does not appear to be a git repository|not a repository|404/i.test(\n output,\n )\n )\n return {\n code: 'CLONE_REPOSITORY_UNAVAILABLE',\n message: '\u4ED3\u5E93\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE\u3002\u8BF7\u68C0\u67E5\u4ED3\u5E93\u5730\u5740\u548C\u4E3B\u673A\u4E0A\u7684 Git \u8BBF\u95EE\u6743\u9650\u3002',\n };\n if (\n /could not resolve|failed to connect|unable to access|connection|timed out|certificate/i.test(\n output,\n )\n )\n return {\n code: 'CLONE_NETWORK_FAILED',\n message: '\u65E0\u6CD5\u8FDE\u63A5 Git \u4ED3\u5E93\u3002\u8BF7\u68C0\u67E5\u670D\u52A1\u5668\u7F51\u7EDC\u3001\u4EE3\u7406\u548C\u8BC1\u4E66\u914D\u7F6E\u540E\u91CD\u8BD5\u3002',\n };\n if (/already exists and is not an empty directory/i.test(output))\n return {\n code: 'CLONE_TARGET_NOT_EMPTY',\n message: '\u76EE\u6807\u76EE\u5F55\u5DF2\u5305\u542B\u6587\u4EF6\uFF0CGit \u5DF2\u505C\u6B62\u514B\u9686\uFF1B\u4E0D\u4F1A\u8986\u76D6\u6216\u5220\u9664\u5DF2\u6709\u6587\u4EF6\u3002',\n };\n if (/no space left|disk quota/i.test(output))\n return {\n code: 'CLONE_DISK_FULL',\n message: '\u670D\u52A1\u5668\u78C1\u76D8\u7A7A\u95F4\u4E0D\u8DB3\uFF0C\u8BF7\u91CA\u653E\u7A7A\u95F4\u540E\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5\u3002',\n };\n return {\n code: 'CLONE_FAILED',\n message: 'Git \u514B\u9686\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u4ED3\u5E93\u5730\u5740\u3001\u4E3B\u673A\u4E0A\u7684 Git \u8BA4\u8BC1\u3001\u76EE\u5F55\u6743\u9650\u548C\u78C1\u76D8\u7A7A\u95F4\u540E\u91CD\u8BD5\u3002',\n };\n}\n\n/** Transient jobs never persist repository URLs, credentials, or raw Git output. */\nexport class ProjectClones {\n private readonly jobs = new Map<string, Job>();\n private closing = false;\n constructor(\n private readonly register: (input: CreateProjectInput) => Project,\n private readonly registered: (path: string) => boolean,\n ) {}\n\n get hasActive(): boolean {\n return [...this.jobs.values()].some(active);\n }\n\n assertIdlePath(path: string): void {\n if (!this.hasActive) return;\n let canonical = resolve(path);\n try {\n canonical = realpathSync(path);\n } catch {\n try {\n canonical = join(realpathSync(dirname(canonical)), basename(canonical));\n } catch {\n /* Normal registration validation reports unavailable paths. */\n }\n }\n if (\n [...this.jobs.values()].some(\n (job) =>\n active(job) &&\n (containsPath(job.view.path, canonical) || containsPath(canonical, job.view.path)),\n )\n )\n throw new AppError('CLONE_IN_PROGRESS', '\u8BE5\u76EE\u5F55\u6B63\u5728\u514B\u9686\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u6216\u53D6\u6D88\u540E\u518D\u64CD\u4F5C', 409);\n }\n\n start(input: CloneProjectInput, resolvePath = (path: string) => path): ProjectClone {\n const parsed = cloneProjectSchema.parse(input);\n validateRepository(parsed.repository);\n const fingerprint = createHash('sha256').update(JSON.stringify(parsed)).digest('hex');\n const previous = this.jobs.get(parsed.clientOperationId);\n if (previous) {\n if (previous.fingerprint !== fingerprint)\n throw new AppError('CLONE_ID_CONFLICT', '\u8BE5\u64CD\u4F5C ID \u5DF2\u7528\u4E8E\u5176\u4ED6\u514B\u9686\u8BF7\u6C42', 409);\n return this.get(previous.view.id);\n }\n if (this.closing) throw new AppError('GATEWAY_STOPPING', '\u670D\u52A1\u6B63\u5728\u505C\u6B62\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5', 503);\n const path = targetPath(resolvePath(parsed.path));\n this.assertIdlePath(path);\n if (this.registered(path))\n throw new AppError('PROJECT_EXISTS', '\u8BE5\u76EE\u5F55\u5DF2\u6DFB\u52A0\u4E3A\u9879\u76EE\uFF0C\u8BF7\u4F7F\u7528\u5DF2\u6709\u9879\u76EE\u6216\u9009\u62E9\u5176\u4ED6\u76EE\u5F55', 409);\n checkEmpty(path);\n if ([...this.jobs.values()].filter(active).length >= 4)\n throw new AppError('CLONE_LIMIT', '\u5DF2\u6709\u591A\u4E2A\u4ED3\u5E93\u6B63\u5728\u514B\u9686\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u540E\u91CD\u8BD5', 409);\n // Bound transient history without evicting a running process.\n if (this.jobs.size >= 100) {\n const oldest = [...this.jobs].find(([, job]) => !active(job));\n if (oldest) this.jobs.delete(oldest[0]);\n }\n const job: Job = {\n view: {\n id: parsed.clientOperationId,\n path,\n name: parsed.name,\n status: 'cloning',\n progress: '\u6B63\u5728\u8FDE\u63A5\u4ED3\u5E93\u2026',\n progressStage: 'connecting',\n },\n fingerprint,\n done: Promise.resolve(),\n };\n this.jobs.set(job.view.id, job);\n job.done = this.run(job, parsed.repository);\n return this.get(job.view.id);\n }\n\n get(id: string): ProjectClone {\n const job = this.jobs.get(id);\n if (!job)\n throw new AppError(\n 'CLONE_NOT_FOUND',\n '\u672A\u627E\u5230\u514B\u9686\u8BB0\u5F55\uFF1B\u670D\u52A1\u91CD\u542F\u540E\u8BF7\u68C0\u67E5\u76EE\u6807\u76EE\u5F55\uFF0C\u518D\u6DFB\u52A0\u5DF2\u6709\u76EE\u5F55\u6216\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5',\n 404,\n );\n return structuredClone(job.view);\n }\n\n cancel(id: string): ProjectClone {\n const job = this.jobs.get(id);\n this.get(id);\n if (job && active(job) && job.view.status !== 'cancelling') {\n job.view.status = 'cancelling';\n job.view.progress = '\u6B63\u5728\u53D6\u6D88\uFF0C\u7B49\u5F85 Git \u9000\u51FA\u2026';\n job.view.progressStage = 'cancelling';\n this.signal(job, 'SIGTERM');\n }\n return this.get(id);\n }\n\n retryRegistration(id: string): ProjectClone {\n const job = this.jobs.get(id);\n this.get(id);\n if (job?.view.status === 'registration-failed') this.finishRegistration(job);\n return this.get(id);\n }\n\n private finishRegistration(job: Job): void {\n try {\n job.view.project = this.register({ path: job.view.path, name: job.view.name });\n job.view.status = 'completed';\n job.view.progress = '\u514B\u9686\u5B8C\u6210\uFF0C\u9879\u76EE\u5DF2\u6DFB\u52A0';\n job.view.progressStage = 'completed';\n delete job.view.errorCode;\n delete job.view.error;\n } catch {\n job.view.status = 'registration-failed';\n job.view.progress = '\u514B\u9686\u5DF2\u5B8C\u6210\uFF0C\u9879\u76EE\u5C1A\u672A\u6DFB\u52A0';\n job.view.progressStage = 'registration_failed';\n job.view.errorCode = 'CLONE_REGISTRATION_FAILED';\n job.view.error =\n '\u9879\u76EE\u6CE8\u518C\u5931\u8D25\u3002\u5DF2\u514B\u9686\u7684\u6587\u4EF6\u4FDD\u7559\u5728\u76EE\u6807\u76EE\u5F55\uFF1B\u53EF\u91CD\u8BD5\u6DFB\u52A0\u9879\u76EE\uFF0C\u65E0\u9700\u518D\u6B21\u514B\u9686\uFF0C\u4E5F\u53EF\u901A\u8FC7\u201C\u5DF2\u6709\u76EE\u5F55\u201D\u6DFB\u52A0\u3002';\n }\n }\n\n private signal(job: Job, signal: NodeJS.Signals): void {\n if (!job.child?.pid) return;\n try {\n if (process.platform === 'win32') job.child.kill(signal);\n else process.kill(-job.child.pid, signal);\n } catch {\n /* Process may have already exited. */\n }\n }\n\n private async run(job: Job, repository: string): Promise<void> {\n let output = '';\n let pending = '';\n let missingGit = false;\n let timedOut = false;\n const sshCommand =\n process.env.GIT_SSH_COMMAND ??\n (process.env.GIT_SSH ? `'${process.env.GIT_SSH.replaceAll(\"'\", \"'\\\\''\")}'` : 'ssh');\n const child = spawn(\n 'git',\n [\n '-c',\n 'protocol.allow=never',\n '-c',\n 'protocol.https.allow=always',\n '-c',\n 'protocol.ssh.allow=always',\n 'clone',\n '--progress',\n '--',\n repository,\n job.view.path,\n ],\n {\n detached: process.platform !== 'win32',\n stdio: ['ignore', 'pipe', 'pipe'],\n env: {\n ...process.env,\n GIT_TERMINAL_PROMPT: '0',\n GCM_INTERACTIVE: 'never',\n GIT_SSH_COMMAND: `${sshCommand} -oBatchMode=yes -oStrictHostKeyChecking=yes`,\n LC_ALL: 'C',\n },\n },\n );\n job.child = child;\n const read = (chunk: Buffer) => {\n output = (output + chunk.toString()).slice(-16_384);\n pending = (pending + chunk.toString()).slice(-4096);\n const lines = pending.split(/[\\r\\n]/);\n pending = lines.pop() ?? '';\n for (const line of lines) {\n const match =\n /^(?:remote: )?(Receiving objects|Resolving deltas|Updating files|Checking out files|Counting objects|Compressing objects):\\s+(\\d{1,3})%/.exec(\n line,\n );\n if (!match || job.view.status !== 'cloning') continue;\n const labels: Record<string, [NonNullable<ProjectClone['progressStage']>, string]> = {\n 'Receiving objects': ['receiving', '\u6B63\u5728\u63A5\u6536\u5BF9\u8C61'],\n 'Resolving deltas': ['resolving', '\u6B63\u5728\u89E3\u6790\u5DEE\u5F02'],\n 'Updating files': ['checking_out', '\u6B63\u5728\u68C0\u51FA\u6587\u4EF6'],\n 'Checking out files': ['checking_out', '\u6B63\u5728\u68C0\u51FA\u6587\u4EF6'],\n 'Counting objects': ['counting', '\u6B63\u5728\u7EDF\u8BA1\u5BF9\u8C61'],\n 'Compressing objects': ['compressing', '\u6B63\u5728\u538B\u7F29\u5BF9\u8C61'],\n };\n [job.view.progressStage, job.view.progress] = labels[match[1]!]!;\n job.view.percent = Math.min(100, Number(match[2]));\n }\n };\n child.stdout?.on('data', read);\n child.stderr?.on('data', read);\n const timeout = setTimeout(() => {\n timedOut = true;\n this.cancel(job.view.id);\n }, 30 * 60_000);\n timeout.unref();\n const escalation = setInterval(() => {\n if (job.view.status === 'cancelling') this.signal(job, 'SIGKILL');\n }, 3000);\n escalation.unref();\n const code = await new Promise<number | null>((done) => {\n child.once('error', (error: NodeJS.ErrnoException) => {\n missingGit = error.code === 'ENOENT';\n });\n child.once('close', done);\n });\n clearTimeout(timeout);\n clearInterval(escalation);\n job.child = undefined;\n delete job.view.percent;\n if (job.view.status === 'cancelling') {\n job.view.status = timedOut ? 'failed' : 'cancelled';\n job.view.progress = timedOut ? '\u514B\u9686\u8D85\u65F6\uFF0C\u5DF2\u505C\u6B62' : '\u5DF2\u53D6\u6D88\u514B\u9686';\n job.view.progressStage = timedOut ? 'timed_out' : 'cancelled';\n job.view.errorCode = timedOut ? 'CLONE_TIMED_OUT' : 'CLONE_CANCELLED';\n job.view.error = `${timedOut ? '\u514B\u9686\u8D85\u8FC7 30 \u5206\u949F\uFF0C\u5DF2\u505C\u6B62\u3002' : ''}\u672A\u6DFB\u52A0\u9879\u76EE\uFF0C\u4E5F\u672A\u5220\u9664\u4EFB\u4F55\u6587\u4EF6\u3002\u76EE\u6807\u76EE\u5F55\u53EF\u80FD\u6B8B\u7559\u6587\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u540E\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5\u3002`;\n } else if (code !== 0) {\n job.view.status = 'failed';\n job.view.progress = '\u514B\u9686\u5931\u8D25';\n job.view.progressStage = 'failed';\n const problem = failure(output, missingGit);\n job.view.errorCode = problem.code;\n job.view.error = `${problem.message} \u672A\u6DFB\u52A0\u9879\u76EE\uFF1B\u76EE\u6807\u76EE\u5F55\u53EF\u80FD\u6B8B\u7559\u6587\u4EF6\uFF0C\u8BF7\u68C0\u67E5\u540E\u4F7F\u7528\u65B0\u7684\u7A7A\u76EE\u5F55\u91CD\u8BD5\u3002`;\n } else this.finishRegistration(job);\n }\n\n async close(): Promise<void> {\n this.closing = true;\n for (const job of this.jobs.values()) if (active(job)) this.cancel(job.view.id);\n await Promise.all([...this.jobs.values()].map((job) => job.done));\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["job"]
7
7
  }
@@ -0,0 +1,39 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { AppError } from "../shared/errors.js";
4
+ function assertWorkDir(path) {
5
+ try {
6
+ if (!statSync(path).isDirectory()) throw new Error("not a directory");
7
+ accessSync(path, constants.R_OK | constants.X_OK);
8
+ } catch {
9
+ throw new AppError(
10
+ "WORK_DIR_UNAVAILABLE",
11
+ "workDir \u76EE\u5F55\u4E0D\u5B58\u5728\u3001\u4E0D\u662F\u76EE\u5F55\u6216\u4E0D\u53EF\u8BBF\u95EE\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u4E3B\u673A\u4E0A\u7684\u76EE\u5F55\u548C\u6743\u9650\uFF0C\u6216\u586B\u5199\u5176\u4ED6\u7EDD\u5BF9\u8DEF\u5F84\u3002",
12
+ 409
13
+ );
14
+ }
15
+ }
16
+ function resolveProjectPath(path, workDir) {
17
+ if (!path || path.includes("\0"))
18
+ throw new AppError("INVALID_PROJECT_PATH", "\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u9879\u76EE\u76EE\u5F55", 400);
19
+ if (isAbsolute(path)) return resolve(path);
20
+ if (!workDir)
21
+ throw new AppError("INVALID_PROJECT_PATH", "\u672A\u914D\u7F6E workDir\uFF0C\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84", 400);
22
+ assertWorkDir(workDir);
23
+ return resolve(workDir, path);
24
+ }
25
+ function projectPathPreview(path, workDir, directory) {
26
+ const input = path || (directory === void 0 ? workDir : workDir && directory);
27
+ if (!input) {
28
+ if (workDir) assertWorkDir(workDir);
29
+ return { workDir, path: "" };
30
+ }
31
+ if (!path && workDir) assertWorkDir(workDir);
32
+ return { workDir, path: resolveProjectPath(input, workDir) };
33
+ }
34
+ export {
35
+ assertWorkDir,
36
+ projectPathPreview,
37
+ resolveProjectPath
38
+ };
39
+ //# sourceMappingURL=project-paths.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/server/project-paths.ts"],
4
+ "sourcesContent": ["import { accessSync, constants, statSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\nimport { AppError } from '../shared/errors.js';\n\nexport function assertWorkDir(path: string): void {\n try {\n if (!statSync(path).isDirectory()) throw new Error('not a directory');\n accessSync(path, constants.R_OK | constants.X_OK);\n } catch {\n throw new AppError(\n 'WORK_DIR_UNAVAILABLE',\n 'workDir \u76EE\u5F55\u4E0D\u5B58\u5728\u3001\u4E0D\u662F\u76EE\u5F55\u6216\u4E0D\u53EF\u8BBF\u95EE\uFF1B\u8BF7\u68C0\u67E5\u670D\u52A1\u4E3B\u673A\u4E0A\u7684\u76EE\u5F55\u548C\u6743\u9650\uFF0C\u6216\u586B\u5199\u5176\u4ED6\u7EDD\u5BF9\u8DEF\u5F84\u3002',\n 409,\n );\n }\n}\n\n/** Resolve on the host so browser previews use the same path rules as registration. */\nexport function resolveProjectPath(path: string, workDir?: string): string {\n if (!path || path.includes('\\0'))\n throw new AppError('INVALID_PROJECT_PATH', '\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u9879\u76EE\u76EE\u5F55', 400);\n if (isAbsolute(path)) return resolve(path);\n if (!workDir)\n throw new AppError('INVALID_PROJECT_PATH', '\u672A\u914D\u7F6E workDir\uFF0C\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84', 400);\n assertWorkDir(workDir);\n return resolve(workDir, path);\n}\n\nexport function projectPathPreview(path: string, workDir?: string, directory?: string) {\n const input = path || (directory === undefined ? workDir : workDir && directory);\n if (!input) {\n if (workDir) assertWorkDir(workDir);\n return { workDir, path: '' };\n }\n if (!path && workDir) assertWorkDir(workDir);\n return { workDir, path: resolveProjectPath(input, workDir) };\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,WAAW,gBAAgB;AAChD,SAAS,YAAY,eAAe;AACpC,SAAS,gBAAgB;AAElB,SAAS,cAAc,MAAoB;AAChD,MAAI;AACF,QAAI,CAAC,SAAS,IAAI,EAAE,YAAY,EAAG,OAAM,IAAI,MAAM,iBAAiB;AACpE,eAAW,MAAM,UAAU,OAAO,UAAU,IAAI;AAAA,EAClD,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,mBAAmB,MAAc,SAA0B;AACzE,MAAI,CAAC,QAAQ,KAAK,SAAS,IAAI;AAC7B,UAAM,IAAI,SAAS,wBAAwB,4EAAgB,GAAG;AAChE,MAAI,WAAW,IAAI,EAAG,QAAO,QAAQ,IAAI;AACzC,MAAI,CAAC;AACH,UAAM,IAAI,SAAS,wBAAwB,wHAA8B,GAAG;AAC9E,gBAAc,OAAO;AACrB,SAAO,QAAQ,SAAS,IAAI;AAC9B;AAEO,SAAS,mBAAmB,MAAc,SAAkB,WAAoB;AACrF,QAAM,QAAQ,SAAS,cAAc,SAAY,UAAU,WAAW;AACtE,MAAI,CAAC,OAAO;AACV,QAAI,QAAS,eAAc,OAAO;AAClC,WAAO,EAAE,SAAS,MAAM,GAAG;AAAA,EAC7B;AACA,MAAI,CAAC,QAAQ,QAAS,eAAc,OAAO;AAC3C,SAAO,EAAE,SAAS,MAAM,mBAAmB,OAAO,OAAO,EAAE;AAC7D;",
6
+ "names": []
7
+ }
@@ -2,9 +2,13 @@ import { execFileSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
4
4
  import { basename, isAbsolute, relative, resolve, sep } from "node:path";
5
- import { asc } from "drizzle-orm";
5
+ import { asc, eq } from "drizzle-orm";
6
6
  import { AppError } from "../shared/errors.js";
7
- import { createProjectSchema, renameProjectSchema } from "../shared/validation.js";
7
+ import {
8
+ createProjectSchema,
9
+ renameProjectSchema,
10
+ reorderProjectsSchema
11
+ } from "../shared/validation.js";
8
12
  import { projectRegistrations } from "./db/schema.js";
9
13
  function containsPath(root, candidate) {
10
14
  const difference = relative(root, candidate);
@@ -25,7 +29,11 @@ function checkoutRoot(cwd) {
25
29
  class ProjectRegistry {
26
30
  constructor(configs, database) {
27
31
  this.database = database;
28
- for (const row of database?.select().from(projectRegistrations).orderBy(asc(projectRegistrations.createdAt), asc(projectRegistrations.id)).all() ?? [])
32
+ for (const row of database?.select().from(projectRegistrations).orderBy(
33
+ asc(projectRegistrations.sortOrder),
34
+ asc(projectRegistrations.createdAt),
35
+ asc(projectRegistrations.id)
36
+ ).all() ?? [])
29
37
  this.registered.set(row.id, row);
30
38
  const seeds = [];
31
39
  for (const config of configs) {
@@ -52,6 +60,7 @@ class ProjectRegistry {
52
60
  path,
53
61
  canonicalRoot,
54
62
  removedAt: null,
63
+ sortOrder: this.nextSortOrder(),
55
64
  createdAt: now,
56
65
  updatedAt: now
57
66
  };
@@ -68,6 +77,12 @@ class ProjectRegistry {
68
77
  Date.now()
69
78
  );
70
79
  }
80
+ nextSortOrder() {
81
+ return [...this.registered.values()].reduce(
82
+ (next, row) => Math.max(next, row.sortOrder + 1),
83
+ 0
84
+ );
85
+ }
71
86
  directory(path) {
72
87
  if (!isAbsolute(path) || path.includes("\0"))
73
88
  throw new AppError("INVALID_PROJECT_PATH", "\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84", 400);
@@ -130,7 +145,29 @@ class ProjectRegistry {
130
145
  }
131
146
  }
132
147
  list() {
133
- return [...this.registered.values()].filter((project) => project.removedAt === null).map((project) => this.inspect(project));
148
+ return [...this.registered.values()].filter((project) => project.removedAt === null).sort(
149
+ (a, b) => a.sortOrder - b.sortOrder || a.createdAt - b.createdAt || a.id.localeCompare(b.id)
150
+ ).map((project) => this.inspect(project));
151
+ }
152
+ reorder(input) {
153
+ const { projectIds } = reorderProjectsSchema.parse(input);
154
+ const active = [...this.registered.values()].filter((row) => row.removedAt === null);
155
+ if (projectIds.length !== active.length || projectIds.some(
156
+ (id) => !this.registered.has(id) || this.registered.get(id).removedAt !== null
157
+ ))
158
+ throw new AppError("PROJECT_ORDER_CONFLICT", "\u9879\u76EE\u5217\u8868\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u8C03\u6574\u987A\u5E8F", 409);
159
+ const now = Date.now();
160
+ const rows = projectIds.map((id, sortOrder) => ({
161
+ ...this.registration(id),
162
+ sortOrder,
163
+ updatedAt: now
164
+ }));
165
+ this.database?.transaction((tx) => {
166
+ for (const row of rows)
167
+ tx.update(projectRegistrations).set({ sortOrder: row.sortOrder, updatedAt: now }).where(eq(projectRegistrations.id, row.id)).run();
168
+ });
169
+ for (const row of rows) this.registered.set(row.id, row);
170
+ return this.list();
134
171
  }
135
172
  /** Also usable when a saved directory is missing, so it can still be managed. */
136
173
  describe(id) {
@@ -153,7 +190,12 @@ class ProjectRegistry {
153
190
  const removed = matching.find((row2) => row2.removedAt !== null);
154
191
  const now = Date.now();
155
192
  if (removed) {
156
- const restored = { ...removed, removedAt: null, updatedAt: now };
193
+ const restored = {
194
+ ...removed,
195
+ removedAt: null,
196
+ sortOrder: this.nextSortOrder(),
197
+ updatedAt: now
198
+ };
157
199
  const project = this.inspect(restored);
158
200
  if (!project.available)
159
201
  throw new AppError("PROJECT_UNAVAILABLE", project.error ?? "\u539F\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528", 409);
@@ -171,6 +213,7 @@ class ProjectRegistry {
171
213
  path: canonicalRoot,
172
214
  canonicalRoot,
173
215
  removedAt: null,
216
+ sortOrder: this.nextSortOrder(),
174
217
  createdAt: this.nextCreatedAt(),
175
218
  updatedAt: now
176
219
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/server/projects.ts"],
4
- "sourcesContent": ["import { execFileSync } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport { accessSync, constants, realpathSync, statSync } from 'node:fs';\nimport { basename, isAbsolute, relative, resolve, sep } from 'node:path';\nimport { asc } from 'drizzle-orm';\nimport { AppError } from '../shared/errors.js';\nimport type { CreateProjectInput, Project, RenameProjectInput } from '../shared/types.js';\nimport { createProjectSchema, renameProjectSchema } from '../shared/validation.js';\nimport type { AppDatabase } from './db/index.js';\nimport { projectRegistrations } from './db/schema.js';\n\nexport interface ProjectConfig {\n id: string;\n name: string;\n path: string;\n}\ntype Registration = typeof projectRegistrations.$inferSelect;\n\nexport function containsPath(root: string, candidate: string): boolean {\n const difference = relative(root, candidate);\n return (\n difference === '' ||\n (difference !== '..' && !difference.startsWith(`..${sep}`) && !isAbsolute(difference))\n );\n}\n\nfunction checkoutRoot(cwd: string): string | undefined {\n try {\n const output = execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {\n encoding: 'utf8',\n timeout: 3000,\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return output ? realpathSync(output) : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport class ProjectRegistry {\n private readonly registered = new Map<string, Registration>();\n\n constructor(\n configs: ProjectConfig[],\n private readonly database?: AppDatabase,\n ) {\n for (const row of database\n ?.select()\n .from(projectRegistrations)\n .orderBy(asc(projectRegistrations.createdAt), asc(projectRegistrations.id))\n .all() ?? [])\n this.registered.set(row.id, row);\n // JSON remains a source of new registrations, while persisted UI decisions win.\n // Keep legacy aliases and IDs intact; only new UI registrations reject aliases.\n const seeds: Registration[] = [];\n for (const config of configs) {\n const path = resolve(config.path);\n const existing = this.registered.get(config.id);\n if (existing) {\n if (existing.path !== path)\n throw new AppError(\n 'PROJECT_ID_CONFLICT',\n `\u9879\u76EE ${config.id} \u5DF2\u7ED1\u5B9A\u5176\u4ED6\u76EE\u5F55\uFF1B\u9879\u76EE ID \u548C\u76EE\u5F55\u4E0D\u53EF\u4FEE\u6539\uFF0C\u8BF7\u4F7F\u7528\u65B0 ID`,\n 409,\n { projectId: config.id },\n );\n continue;\n }\n let canonicalRoot: string | null = null;\n try {\n canonicalRoot = this.directory(path);\n } catch {\n // Existing configurations with temporarily missing directories still start.\n }\n const now = this.nextCreatedAt();\n const row = {\n ...config,\n path,\n canonicalRoot,\n removedAt: null,\n createdAt: now,\n updatedAt: now,\n };\n this.registered.set(config.id, row);\n seeds.push(row);\n }\n if (seeds.length) database?.insert(projectRegistrations).values(seeds).run();\n }\n\n private nextCreatedAt(): number {\n // Preserve seed order for legacy aliases even when imported within one millisecond.\n return [...this.registered.values()].reduce(\n (next, row) => Math.max(next, row.createdAt + 1),\n Date.now(),\n );\n }\n\n private directory(path: string): string {\n if (!isAbsolute(path) || path.includes('\\0'))\n throw new AppError('INVALID_PROJECT_PATH', '\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84', 400);\n try {\n const canonicalRoot = realpathSync(path);\n if (!statSync(canonicalRoot).isDirectory())\n throw new AppError('PROJECT_UNAVAILABLE', '\u9879\u76EE\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55', 409);\n accessSync(canonicalRoot, constants.R_OK | constants.X_OK);\n return canonicalRoot;\n } catch (error) {\n if (error instanceof AppError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n throw new AppError(\n 'PROJECT_UNAVAILABLE',\n code === 'ENOENT' || code === 'ENOTDIR'\n ? '\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1B\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u76EE\u5F55'\n : code === 'EACCES' || code === 'EPERM'\n ? '\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE\uFF0C\u9700\u8981\u8BFB\u53D6\u548C\u8FDB\u5165\u76EE\u5F55\u7684\u6743\u9650'\n : '\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u548C\u8BBF\u95EE\u6743\u9650',\n 409,\n );\n }\n }\n\n private save(row: Registration): void {\n this.database\n ?.insert(projectRegistrations)\n .values(row)\n .onConflictDoUpdate({ target: projectRegistrations.id, set: row })\n .run();\n this.registered.set(row.id, row);\n }\n\n private registration(id: string): Registration {\n const row = this.registered.get(id);\n if (!row || row.removedAt !== null)\n throw new AppError('PROJECT_NOT_FOUND', '\u9879\u76EE\u672A\u6CE8\u518C\u6216\u5DF2\u79FB\u9664', 404);\n return row;\n }\n\n private inspect(config: Registration): Project {\n const rootPath = config.path;\n try {\n const canonicalRoot = this.directory(rootPath);\n if (config.canonicalRoot !== null && canonicalRoot !== config.canonicalRoot)\n throw new AppError(\n 'PROJECT_UNAVAILABLE',\n '\u9879\u76EE\u76EE\u5F55\u7684\u771F\u5B9E\u8DEF\u5F84\u5DF2\u6539\u53D8\uFF0C\u4E0D\u80FD\u6CBF\u7528\u539F\u9879\u76EE\u8EAB\u4EFD',\n 409,\n );\n if (config.canonicalRoot === null)\n this.save({ ...config, canonicalRoot, updatedAt: Date.now() });\n return {\n id: config.id,\n name: config.name,\n rootPath,\n canonicalRoot,\n workspaceKey: checkoutRoot(canonicalRoot) ?? canonicalRoot,\n available: true,\n };\n } catch (error) {\n return {\n id: config.id,\n name: config.name,\n rootPath,\n canonicalRoot: config.canonicalRoot ?? rootPath,\n workspaceKey: config.canonicalRoot ?? rootPath,\n available: false,\n error: error instanceof Error ? error.message : '\u9879\u76EE\u4E0D\u53EF\u7528',\n };\n }\n }\n\n list(): Project[] {\n return [...this.registered.values()]\n .filter((project) => project.removedAt === null)\n .map((project) => this.inspect(project));\n }\n\n /** Also usable when a saved directory is missing, so it can still be managed. */\n describe(id: string): Project {\n return this.inspect(this.registration(id));\n }\n\n add(input: CreateProjectInput): Project {\n const parsed = createProjectSchema.parse(input);\n const canonicalRoot = this.directory(parsed.path);\n // Inspect lazily imported paths before checking, including legacy unavailable seeds.\n this.list();\n for (const row of this.registered.values()) {\n if (row.canonicalRoot !== null || row.removedAt === null) continue;\n try {\n this.save({ ...row, canonicalRoot: this.directory(row.path), updatedAt: Date.now() });\n } catch {\n // A tombstone still reserves its ID while its directory remains unavailable.\n }\n }\n const matching = [...this.registered.values()].filter(\n (row) => row.canonicalRoot === canonicalRoot,\n );\n const removed = matching.find((row) => row.removedAt !== null);\n const now = Date.now();\n if (removed) {\n // Restoring a grandfathered alias reuses an existing identity; it does not\n // create a duplicate registration even when another legacy alias is active.\n // A spelling/alias supplied during restoration does not change saved identity.\n const restored = { ...removed, removedAt: null, updatedAt: now };\n const project = this.inspect(restored);\n if (!project.available)\n throw new AppError('PROJECT_UNAVAILABLE', project.error ?? '\u539F\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528', 409);\n this.save({ ...restored, name: parsed.name ?? removed.name });\n return this.get(removed.id);\n }\n const existing = matching[0];\n if (existing)\n throw new AppError('PROJECT_EXISTS', '\u8BE5\u76EE\u5F55\u5DF2\u6DFB\u52A0\uFF0C\u8BF7\u9009\u62E9\u5DF2\u6709\u9879\u76EE', 409, {\n projectId: existing.id,\n });\n const row: Registration = {\n id: randomUUID(),\n name: parsed.name ?? (basename(canonicalRoot) || canonicalRoot),\n path: canonicalRoot,\n canonicalRoot,\n removedAt: null,\n createdAt: this.nextCreatedAt(),\n updatedAt: now,\n };\n this.save(row);\n return this.get(row.id);\n }\n\n rename(id: string, input: RenameProjectInput): Project {\n const { name } = renameProjectSchema.parse(input);\n this.save({ ...this.registration(id), name, updatedAt: Date.now() });\n return this.describe(id);\n }\n\n remove(id: string): void {\n const now = Date.now();\n this.save({ ...this.registration(id), removedAt: now, updatedAt: now });\n }\n\n get(id: string): Project {\n const project = this.describe(id);\n if (!project.available)\n throw new AppError('PROJECT_UNAVAILABLE', '\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528', 409, { projectId: id });\n return project;\n }\n\n assertOwnership(\n projectId: string,\n cwd: string,\n ): { project: Project; cwd: string; workspaceKey: string } {\n const project = this.get(projectId);\n let canonicalCwd: string;\n try {\n canonicalCwd = realpathSync(cwd);\n if (!statSync(canonicalCwd).isDirectory()) throw new Error('not a directory');\n } catch {\n throw new AppError('PROJECT_UNAVAILABLE', '\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u53EF\u7528', 409);\n }\n if (!containsPath(project.canonicalRoot, canonicalCwd))\n throw new AppError('PROJECT_OWNERSHIP', '\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u6240\u9009\u9879\u76EE\u76EE\u5F55', 403);\n return {\n project,\n cwd: canonicalCwd,\n workspaceKey: checkoutRoot(canonicalCwd) ?? project.canonicalRoot,\n };\n }\n\n findOwner(cwd: string): { project: Project; cwd: string; workspaceKey: string } {\n let canonicalCwd: string;\n try {\n canonicalCwd = realpathSync(cwd);\n } catch {\n throw new AppError('PROJECT_UNAVAILABLE', '\u4F1A\u8BDD\u76EE\u5F55\u4E0D\u53EF\u7528', 409);\n }\n // Tombstones reserve automatic ownership as well: a parent must not adopt\n // previously undiscovered history from a removed nested project.\n // An active grandfathered alias at the same root remains eligible; only a\n // deeper tombstone reserves history against a different, shallower directory.\n const project = [...this.registered.values()]\n .map((entry) => ({ project: this.inspect(entry), removed: entry.removedAt !== null }))\n .filter((entry) => containsPath(entry.project.canonicalRoot, canonicalCwd))\n .sort(\n (a, b) =>\n b.project.canonicalRoot.length - a.project.canonicalRoot.length ||\n Number(a.removed) - Number(b.removed),\n )[0]?.project;\n if (!project) throw new AppError('PROJECT_OWNERSHIP', '\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u5DF2\u6CE8\u518C\u9879\u76EE', 403);\n return this.assertOwnership(project.id, canonicalCwd);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { execFileSync } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport { accessSync, constants, realpathSync, statSync } from 'node:fs';\nimport { basename, isAbsolute, relative, resolve, sep } from 'node:path';\nimport { asc, eq } from 'drizzle-orm';\nimport { AppError } from '../shared/errors.js';\nimport type {\n CreateProjectInput,\n Project,\n RenameProjectInput,\n ReorderProjectsInput,\n} from '../shared/types.js';\nimport {\n createProjectSchema,\n renameProjectSchema,\n reorderProjectsSchema,\n} from '../shared/validation.js';\nimport type { AppDatabase } from './db/index.js';\nimport { projectRegistrations } from './db/schema.js';\n\nexport interface ProjectConfig {\n id: string;\n name: string;\n path: string;\n}\ntype Registration = typeof projectRegistrations.$inferSelect;\n\nexport function containsPath(root: string, candidate: string): boolean {\n const difference = relative(root, candidate);\n return (\n difference === '' ||\n (difference !== '..' && !difference.startsWith(`..${sep}`) && !isAbsolute(difference))\n );\n}\n\nfunction checkoutRoot(cwd: string): string | undefined {\n try {\n const output = execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {\n encoding: 'utf8',\n timeout: 3000,\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n return output ? realpathSync(output) : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport class ProjectRegistry {\n private readonly registered = new Map<string, Registration>();\n\n constructor(\n configs: ProjectConfig[],\n private readonly database?: AppDatabase,\n ) {\n for (const row of database\n ?.select()\n .from(projectRegistrations)\n .orderBy(\n asc(projectRegistrations.sortOrder),\n asc(projectRegistrations.createdAt),\n asc(projectRegistrations.id),\n )\n .all() ?? [])\n this.registered.set(row.id, row);\n // JSON remains a source of new registrations, while persisted UI decisions win.\n // Keep legacy aliases and IDs intact; only new UI registrations reject aliases.\n const seeds: Registration[] = [];\n for (const config of configs) {\n const path = resolve(config.path);\n const existing = this.registered.get(config.id);\n if (existing) {\n if (existing.path !== path)\n throw new AppError(\n 'PROJECT_ID_CONFLICT',\n `\u9879\u76EE ${config.id} \u5DF2\u7ED1\u5B9A\u5176\u4ED6\u76EE\u5F55\uFF1B\u9879\u76EE ID \u548C\u76EE\u5F55\u4E0D\u53EF\u4FEE\u6539\uFF0C\u8BF7\u4F7F\u7528\u65B0 ID`,\n 409,\n { projectId: config.id },\n );\n continue;\n }\n let canonicalRoot: string | null = null;\n try {\n canonicalRoot = this.directory(path);\n } catch {\n // Existing configurations with temporarily missing directories still start.\n }\n const now = this.nextCreatedAt();\n const row = {\n ...config,\n path,\n canonicalRoot,\n removedAt: null,\n sortOrder: this.nextSortOrder(),\n createdAt: now,\n updatedAt: now,\n };\n this.registered.set(config.id, row);\n seeds.push(row);\n }\n if (seeds.length) database?.insert(projectRegistrations).values(seeds).run();\n }\n\n private nextCreatedAt(): number {\n // Preserve seed order for legacy aliases even when imported within one millisecond.\n return [...this.registered.values()].reduce(\n (next, row) => Math.max(next, row.createdAt + 1),\n Date.now(),\n );\n }\n\n private nextSortOrder(): number {\n return [...this.registered.values()].reduce(\n (next, row) => Math.max(next, row.sortOrder + 1),\n 0,\n );\n }\n\n private directory(path: string): string {\n if (!isAbsolute(path) || path.includes('\\0'))\n throw new AppError('INVALID_PROJECT_PATH', '\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u7EDD\u5BF9\u76EE\u5F55\u8DEF\u5F84', 400);\n try {\n const canonicalRoot = realpathSync(path);\n if (!statSync(canonicalRoot).isDirectory())\n throw new AppError('PROJECT_UNAVAILABLE', '\u9879\u76EE\u8DEF\u5F84\u4E0D\u662F\u76EE\u5F55', 409);\n accessSync(canonicalRoot, constants.R_OK | constants.X_OK);\n return canonicalRoot;\n } catch (error) {\n if (error instanceof AppError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n throw new AppError(\n 'PROJECT_UNAVAILABLE',\n code === 'ENOENT' || code === 'ENOTDIR'\n ? '\u9879\u76EE\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1B\u8BF7\u8F93\u5165\u8FD0\u884C Codex \u7684\u7535\u8111\u4E0A\u7684\u76EE\u5F55'\n : code === 'EACCES' || code === 'EPERM'\n ? '\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u8BBF\u95EE\uFF0C\u9700\u8981\u8BFB\u53D6\u548C\u8FDB\u5165\u76EE\u5F55\u7684\u6743\u9650'\n : '\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u548C\u8BBF\u95EE\u6743\u9650',\n 409,\n );\n }\n }\n\n private save(row: Registration): void {\n this.database\n ?.insert(projectRegistrations)\n .values(row)\n .onConflictDoUpdate({ target: projectRegistrations.id, set: row })\n .run();\n this.registered.set(row.id, row);\n }\n\n private registration(id: string): Registration {\n const row = this.registered.get(id);\n if (!row || row.removedAt !== null)\n throw new AppError('PROJECT_NOT_FOUND', '\u9879\u76EE\u672A\u6CE8\u518C\u6216\u5DF2\u79FB\u9664', 404);\n return row;\n }\n\n private inspect(config: Registration): Project {\n const rootPath = config.path;\n try {\n const canonicalRoot = this.directory(rootPath);\n if (config.canonicalRoot !== null && canonicalRoot !== config.canonicalRoot)\n throw new AppError(\n 'PROJECT_UNAVAILABLE',\n '\u9879\u76EE\u76EE\u5F55\u7684\u771F\u5B9E\u8DEF\u5F84\u5DF2\u6539\u53D8\uFF0C\u4E0D\u80FD\u6CBF\u7528\u539F\u9879\u76EE\u8EAB\u4EFD',\n 409,\n );\n if (config.canonicalRoot === null)\n this.save({ ...config, canonicalRoot, updatedAt: Date.now() });\n return {\n id: config.id,\n name: config.name,\n rootPath,\n canonicalRoot,\n workspaceKey: checkoutRoot(canonicalRoot) ?? canonicalRoot,\n available: true,\n };\n } catch (error) {\n return {\n id: config.id,\n name: config.name,\n rootPath,\n canonicalRoot: config.canonicalRoot ?? rootPath,\n workspaceKey: config.canonicalRoot ?? rootPath,\n available: false,\n error: error instanceof Error ? error.message : '\u9879\u76EE\u4E0D\u53EF\u7528',\n };\n }\n }\n\n list(): Project[] {\n return [...this.registered.values()]\n .filter((project) => project.removedAt === null)\n .sort(\n (a, b) =>\n a.sortOrder - b.sortOrder || a.createdAt - b.createdAt || a.id.localeCompare(b.id),\n )\n .map((project) => this.inspect(project));\n }\n\n reorder(input: ReorderProjectsInput): Project[] {\n const { projectIds } = reorderProjectsSchema.parse(input);\n const active = [...this.registered.values()].filter((row) => row.removedAt === null);\n if (\n projectIds.length !== active.length ||\n projectIds.some(\n (id) => !this.registered.has(id) || this.registered.get(id)!.removedAt !== null,\n )\n )\n throw new AppError('PROJECT_ORDER_CONFLICT', '\u9879\u76EE\u5217\u8868\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u8C03\u6574\u987A\u5E8F', 409);\n const now = Date.now();\n const rows = projectIds.map((id, sortOrder) => ({\n ...this.registration(id),\n sortOrder,\n updatedAt: now,\n }));\n // Commit the complete order atomically, then update memory only after success.\n this.database?.transaction((tx) => {\n for (const row of rows)\n tx.update(projectRegistrations)\n .set({ sortOrder: row.sortOrder, updatedAt: now })\n .where(eq(projectRegistrations.id, row.id))\n .run();\n });\n for (const row of rows) this.registered.set(row.id, row);\n return this.list();\n }\n\n /** Also usable when a saved directory is missing, so it can still be managed. */\n describe(id: string): Project {\n return this.inspect(this.registration(id));\n }\n\n add(input: CreateProjectInput): Project {\n const parsed = createProjectSchema.parse(input);\n const canonicalRoot = this.directory(parsed.path);\n // Inspect lazily imported paths before checking, including legacy unavailable seeds.\n this.list();\n for (const row of this.registered.values()) {\n if (row.canonicalRoot !== null || row.removedAt === null) continue;\n try {\n this.save({ ...row, canonicalRoot: this.directory(row.path), updatedAt: Date.now() });\n } catch {\n // A tombstone still reserves its ID while its directory remains unavailable.\n }\n }\n const matching = [...this.registered.values()].filter(\n (row) => row.canonicalRoot === canonicalRoot,\n );\n const removed = matching.find((row) => row.removedAt !== null);\n const now = Date.now();\n if (removed) {\n // Restoring a grandfathered alias reuses an existing identity; it does not\n // create a duplicate registration even when another legacy alias is active.\n // A spelling/alias supplied during restoration does not change saved identity.\n const restored = {\n ...removed,\n removedAt: null,\n sortOrder: this.nextSortOrder(),\n updatedAt: now,\n };\n const project = this.inspect(restored);\n if (!project.available)\n throw new AppError('PROJECT_UNAVAILABLE', project.error ?? '\u539F\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528', 409);\n this.save({ ...restored, name: parsed.name ?? removed.name });\n return this.get(removed.id);\n }\n const existing = matching[0];\n if (existing)\n throw new AppError('PROJECT_EXISTS', '\u8BE5\u76EE\u5F55\u5DF2\u6DFB\u52A0\uFF0C\u8BF7\u9009\u62E9\u5DF2\u6709\u9879\u76EE', 409, {\n projectId: existing.id,\n });\n const row: Registration = {\n id: randomUUID(),\n name: parsed.name ?? (basename(canonicalRoot) || canonicalRoot),\n path: canonicalRoot,\n canonicalRoot,\n removedAt: null,\n sortOrder: this.nextSortOrder(),\n createdAt: this.nextCreatedAt(),\n updatedAt: now,\n };\n this.save(row);\n return this.get(row.id);\n }\n\n rename(id: string, input: RenameProjectInput): Project {\n const { name } = renameProjectSchema.parse(input);\n this.save({ ...this.registration(id), name, updatedAt: Date.now() });\n return this.describe(id);\n }\n\n remove(id: string): void {\n const now = Date.now();\n this.save({ ...this.registration(id), removedAt: now, updatedAt: now });\n }\n\n get(id: string): Project {\n const project = this.describe(id);\n if (!project.available)\n throw new AppError('PROJECT_UNAVAILABLE', '\u9879\u76EE\u76EE\u5F55\u4E0D\u53EF\u7528', 409, { projectId: id });\n return project;\n }\n\n assertOwnership(\n projectId: string,\n cwd: string,\n ): { project: Project; cwd: string; workspaceKey: string } {\n const project = this.get(projectId);\n let canonicalCwd: string;\n try {\n canonicalCwd = realpathSync(cwd);\n if (!statSync(canonicalCwd).isDirectory()) throw new Error('not a directory');\n } catch {\n throw new AppError('PROJECT_UNAVAILABLE', '\u4F1A\u8BDD\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u53EF\u7528', 409);\n }\n if (!containsPath(project.canonicalRoot, canonicalCwd))\n throw new AppError('PROJECT_OWNERSHIP', '\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u6240\u9009\u9879\u76EE\u76EE\u5F55', 403);\n return {\n project,\n cwd: canonicalCwd,\n workspaceKey: checkoutRoot(canonicalCwd) ?? project.canonicalRoot,\n };\n }\n\n findOwner(cwd: string): { project: Project; cwd: string; workspaceKey: string } {\n let canonicalCwd: string;\n try {\n canonicalCwd = realpathSync(cwd);\n } catch {\n throw new AppError('PROJECT_UNAVAILABLE', '\u4F1A\u8BDD\u76EE\u5F55\u4E0D\u53EF\u7528', 409);\n }\n // Tombstones reserve automatic ownership as well: a parent must not adopt\n // previously undiscovered history from a removed nested project.\n // An active grandfathered alias at the same root remains eligible; only a\n // deeper tombstone reserves history against a different, shallower directory.\n const project = [...this.registered.values()]\n .map((entry) => ({ project: this.inspect(entry), removed: entry.removedAt !== null }))\n .filter((entry) => containsPath(entry.project.canonicalRoot, canonicalCwd))\n .sort(\n (a, b) =>\n b.project.canonicalRoot.length - a.project.canonicalRoot.length ||\n Number(a.removed) - Number(b.removed),\n )[0]?.project;\n if (!project) throw new AppError('PROJECT_OWNERSHIP', '\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u5DF2\u6CE8\u518C\u9879\u76EE', 403);\n return this.assertOwnership(project.id, canonicalCwd);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["row"]
7
7
  }
@@ -21,6 +21,7 @@ import { renderThreadExport } from "./thread-export.js";
21
21
  import { readLocalImage } from "./images.js";
22
22
  import { temporaryFileRoots } from "./local-files.js";
23
23
  import { readLocalMarkdown } from "./markdown.js";
24
+ import { readLocalText } from "./text-files.js";
24
25
  import { readLocalJson } from "./json-files.js";
25
26
  class GatewayService {
26
27
  constructor(options) {
@@ -132,17 +133,25 @@ class GatewayService {
132
133
  this.events.publishProjects(this.projects.list());
133
134
  return project;
134
135
  }
135
- cloneProject(input) {
136
+ cloneProject(input, resolvePath = (path) => path) {
136
137
  if (this.maintenance || !this.accepting)
137
138
  throw new AppError("GATEWAY_STOPPING", "\u670D\u52A1\u6B63\u5728\u5347\u7EA7\u6216\u505C\u6B62\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", 503);
138
- this.workspaces.assertProjectDirectory(input.path);
139
- return this.projectClones.start(input);
139
+ return this.projectClones.start(input, (path) => {
140
+ const resolved = resolvePath(path);
141
+ this.workspaces.assertProjectDirectory(resolved);
142
+ return resolved;
143
+ });
140
144
  }
141
145
  renameProject(projectId, input) {
142
146
  const project = this.projects.rename(projectId, input);
143
147
  this.events.publishProjects(this.projects.list());
144
148
  return project;
145
149
  }
150
+ reorderProjects(input) {
151
+ const projects = this.projects.reorder(input);
152
+ this.events.publishProjects(projects);
153
+ return { data: projects };
154
+ }
146
155
  removeProject(projectId) {
147
156
  const project = this.projects.describe(projectId);
148
157
  const pending = this.store.pendingProjectOperations(projectId)[0];
@@ -491,6 +500,9 @@ class GatewayService {
491
500
  getThreadMarkdown(threadId, path) {
492
501
  return this.readThreadFile(threadId, path, readLocalMarkdown);
493
502
  }
503
+ getThreadText(threadId, path) {
504
+ return this.readThreadFile(threadId, path, readLocalText);
505
+ }
494
506
  getThreadJson(threadId, path) {
495
507
  return this.readThreadFile(threadId, path, readLocalJson);
496
508
  }