@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.
- package/README.md +16 -10
- package/README.zh-CN.md +16 -10
- package/dist/drizzle/0006_project_order.sql +8 -0
- package/dist/drizzle/meta/0006_snapshot.json +488 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/server/config-store.js +10 -2
- package/dist/server/config-store.js.map +2 -2
- package/dist/server/config.js +4 -0
- package/dist/server/config.js.map +2 -2
- package/dist/server/db/schema.js +1 -0
- package/dist/server/db/schema.js.map +2 -2
- package/dist/server/http.js +43 -5
- package/dist/server/http.js.map +2 -2
- package/dist/server/local-files.js.map +1 -1
- package/dist/server/project-clones.js +2 -2
- package/dist/server/project-clones.js.map +2 -2
- package/dist/server/project-paths.js +39 -0
- package/dist/server/project-paths.js.map +7 -0
- package/dist/server/projects.js +48 -5
- package/dist/server/projects.js.map +2 -2
- package/dist/server/service.js +15 -3
- package/dist/server/service.js.map +2 -2
- package/dist/server/text-files.js +32 -0
- package/dist/server/text-files.js.map +7 -0
- package/dist/shared/file-preview.js +22 -0
- package/dist/shared/file-preview.js.map +7 -0
- package/dist/shared/project-path.js +8 -0
- package/dist/shared/project-path.js.map +7 -0
- package/dist/shared/release-notes.js +9 -0
- package/dist/shared/release-notes.js.map +7 -0
- package/dist/shared/updates.js.map +2 -2
- package/dist/shared/validation.js +2 -0
- package/dist/shared/validation.js.map +2 -2
- package/dist/update-worker.cjs +8 -3
- package/dist/updates/manager.js +7 -4
- package/dist/updates/manager.js.map +2 -2
- package/dist/updates/registry.js +21 -9
- package/dist/updates/registry.js.map +3 -3
- package/dist/updates/release-notes.js +73 -0
- package/dist/updates/release-notes.js.map +7 -0
- package/dist/updates/worker.js +3 -3
- package/dist/updates/worker.js.map +2 -2
- package/dist/web/assets/{abnfDiagram-WEYOIA3C-DLvlob5t.js → abnfDiagram-WEYOIA3C-CgSmgOr1.js} +1 -1
- package/dist/web/assets/{arc-DgauwgFe.js → arc-DHf4CdSk.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-DvjLv0QN.js → architecture-WOLXFQ4H-DMTD1pHe.js} +1 -1
- package/dist/web/assets/{architectureDiagram-ZHVYKT53-BAyZGlzu.js → architectureDiagram-ZHVYKT53-DTR8wiWo.js} +1 -1
- package/dist/web/assets/{blockDiagram-RJAE7QA3-BVfkmjlt.js → blockDiagram-RJAE7QA3-DcNiTPKZ.js} +1 -1
- package/dist/web/assets/{c4Diagram-XDPPPE54-rTsfyMoB.js → c4Diagram-XDPPPE54-BDxJVXjQ.js} +1 -1
- package/dist/web/assets/channel-B6d61z43.js +1 -0
- package/dist/web/assets/{chunk-2E7WHVMD-CQ_N_vLS.js → chunk-2E7WHVMD-D5oKHRGT.js} +1 -1
- package/dist/web/assets/{chunk-4EMNKR2T-wBy_gyo6.js → chunk-4EMNKR2T-CtrbVCM-.js} +1 -1
- package/dist/web/assets/{chunk-55XCU76K-DYiwIhmI.js → chunk-55XCU76K-Dhl7DEL7.js} +1 -1
- package/dist/web/assets/{chunk-7S6XSTFO-CRGtfVHg.js → chunk-7S6XSTFO-C8EwegP-.js} +1 -1
- package/dist/web/assets/{chunk-AQDTSKZB-1G2Za6bm.js → chunk-AQDTSKZB-lFiNKr7B.js} +1 -1
- package/dist/web/assets/{chunk-DSNLSHJZ-B1LLoYuG.js → chunk-DSNLSHJZ-B8GftONA.js} +1 -1
- package/dist/web/assets/{chunk-HAGFOTED-78h1RpHh.js → chunk-HAGFOTED-Cnc2mh7_.js} +1 -1
- package/dist/web/assets/{chunk-HNE4Q36P-DmR5B27Q.js → chunk-HNE4Q36P-BKl6ZgI0.js} +1 -1
- package/dist/web/assets/{chunk-NQOKRVIF-D7WSEPzg.js → chunk-NQOKRVIF-qKlxSw3D.js} +1 -1
- package/dist/web/assets/{chunk-UHW7PFBP-gl7a5V-l.js → chunk-UHW7PFBP-C_zh3mNe.js} +1 -1
- package/dist/web/assets/{chunk-VQ5W62QI-KaBXxBre.js → chunk-VQ5W62QI-BXoyXK0q.js} +1 -1
- package/dist/web/assets/{chunk-WQQL4CDK-DDczFoOr.js → chunk-WQQL4CDK-BBeqcU1J.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-3ug4Qhxg.js → chunk-XXDRQBXY-DzssZtz_.js} +1 -1
- package/dist/web/assets/{chunk-YZO67HFG-BpWOuCXP.js → chunk-YZO67HFG-BbNSwLjS.js} +1 -1
- package/dist/web/assets/{chunk-ZAUX2LTX-1ojPb1if.js → chunk-ZAUX2LTX-CjURwZfv.js} +1 -1
- package/dist/web/assets/{chunk-ZCHMQSOR-D2kK2cPj.js → chunk-ZCHMQSOR-u39dymjE.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-4MYWR3UA-B7eMvxAO.js → classDiagram-v2-4MYWR3UA-BEXx5gKT.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-BtB8mbt8.js → cose-bilkent-JH36ORCC-BxDEucP_.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-CIctqxlE.js → cynefin-EF2NZ3EQ-BhkCsT6-.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-WF24UDRP-qat2GD9P.js → cynefinDiagram-WF24UDRP-Wpu-yNke.js} +1 -1
- package/dist/web/assets/{dagre-NK4GIZVX-OJamzzID.js → dagre-NK4GIZVX-LNSSIN4l.js} +1 -1
- package/dist/web/assets/{diagram-CBK3MWTC-C7cX2tqO.js → diagram-CBK3MWTC-BVDr5Vx9.js} +1 -1
- package/dist/web/assets/{diagram-I5XIIV2D-DsMfKVN7.js → diagram-I5XIIV2D-MnLzR1Rv.js} +1 -1
- package/dist/web/assets/{diagram-ODIL2UCW-Cz66FxdJ.js → diagram-ODIL2UCW-CZ4igbo_.js} +1 -1
- package/dist/web/assets/{diagram-PFBEJUXE-CwhPuwKj.js → diagram-PFBEJUXE-4hCnyMdK.js} +1 -1
- package/dist/web/assets/{diagram-RJA3I4VV-C52IGiFJ.js → diagram-RJA3I4VV-DTo42rGA.js} +1 -1
- package/dist/web/assets/{diagram-TYUNFSNR-C2LZVLKY.js → diagram-TYUNFSNR-BC12iCUZ.js} +1 -1
- package/dist/web/assets/{dist-Bxu8YbZb.js → dist-Dg_ysSxT.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-V4P3TTPT-eQKKlMli.js → ebnfDiagram-V4P3TTPT-Du8LCQ31.js} +1 -1
- package/dist/web/assets/{elk-QONTBCTQ-C5kIOZTA.js → elk-QONTBCTQ-BzTAR-T1.js} +1 -1
- package/dist/web/assets/{erDiagram-KYQIGQAA-CMTowz5P.js → erDiagram-KYQIGQAA-SXYIjNOP.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-C5v-UF8X.js → eventmodeling-K75KTNOO-Sbno3ZxQ.js} +1 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-Ae1H3vSR.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7GPNTJY-B0viiC-b.js → ganttDiagram-R7GPNTJY-DBeX1qmu.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-tEjFmPhN.js → gitGraph-VSP46ZUC-CKWihueS.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-CsOwt7jz.js → gitGraphDiagram-PDYL7XHY-Cj1E3jqN.js} +1 -1
- package/dist/web/assets/index-CtTuHkNv.js +147 -0
- package/dist/web/assets/index-DwnjYADO.css +1 -0
- package/dist/web/assets/{info-OHQRW6UA-DnFGu4Ew.js → info-OHQRW6UA-D0w3nR0p.js} +1 -1
- package/dist/web/assets/{infoDiagram-MVDRKAX5-B6WiNaDF.js → infoDiagram-MVDRKAX5-F0jTCG-G.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-Dn70fI1r.js → ishikawaDiagram-HPEHV2GE-CwQGP9AT.js} +1 -1
- package/dist/web/assets/{journeyDiagram-BOSHW7O7-BUjYzV2f.js → journeyDiagram-BOSHW7O7-CPY5GqD2.js} +1 -1
- package/dist/web/assets/{kanban-definition-FNSA4HC5-BDMfWbtB.js → kanban-definition-FNSA4HC5-BCL38eXh.js} +1 -1
- package/dist/web/assets/{line-DNSRZHBK.js → line-DoUzbVJS.js} +1 -1
- package/dist/web/assets/{linear-CrS49q6J.js → linear-LoqU-SEO.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-DXuyO8Px.js → mermaid-parser.core-CvO-MTG1.js} +3 -3
- package/dist/web/assets/{mermaid.core-DUTnbQkq.js → mermaid.core-DcCEGt0V.js} +4 -4
- package/dist/web/assets/{mindmap-definition-6OY57UG3-CqZTtItJ.js → mindmap-definition-6OY57UG3-yZvSibNz.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-DXEZgY0a.js → packet-JDAUHWVQ-9ZiCegsV.js} +1 -1
- package/dist/web/assets/{pegDiagram-ECIUJNBX-D6CCPNem.js → pegDiagram-ECIUJNBX-BkGwBoEk.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-AXRFjZ4G.js → pie-XZMESJXO-kAXIwFnU.js} +1 -1
- package/dist/web/assets/{pieDiagram-GNMPY74T-B9Ss8_5s.js → pieDiagram-GNMPY74T-jiwTgtdb.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-D57K4MC3-JrlylCtz.js → quadrantDiagram-D57K4MC3-u9HdbUwr.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-ChFm4hZg.js → radar-ABXABTNO-BQ911CvQ.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-DaXXzqhZ.js → railroad-I3PHUGI6-Bvm7MWf9.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-DGE67q11.js → railroad-abnf-LNEFI6M7-BfFmWnzu.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-Cq9S3q72.js → railroad-ebnf-DWYD2UWJ-B8Ekz4aD.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-D2Gth3eY.js → railroad-peg-JR7BVNR7-B5ANjRbC.js} +1 -1
- package/dist/web/assets/{railroadDiagram-Y72NRBL7-BpkXX59H.js → railroadDiagram-Y72NRBL7-iu8Hmuma.js} +1 -1
- package/dist/web/assets/{requirementDiagram-EQYSW5RA-oNp3DMM4.js → requirementDiagram-EQYSW5RA-DgiEaaYZ.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-SY24JHO5-bHfheSKO.js → sankeyDiagram-SY24JHO5-KzDvhd8J.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-BLEHNHQU-CEM2XfNF.js → sequenceDiagram-BLEHNHQU-DJMSHuO_.js} +1 -1
- package/dist/web/assets/{src-KjMctvzy.js → src-C5WkvxTS.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-D_TaR_ix.js → stateDiagram-v2-A6IESKWZ-CyfHfUyU.js} +1 -1
- package/dist/web/assets/{swimlanes-B3ENRKFQ-BUYl2fsg.js → swimlanes-B3ENRKFQ-YPSzHf2U.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-D2_NQS-o.js +8 -0
- package/dist/web/assets/{timeline-definition-JXYKGLGB-CpKsGN6d.js → timeline-definition-JXYKGLGB-BwvPIiSj.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-Dioaagqt.js → treeView-D4JQ5SDB-DsUhEw24.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-B8NLQ14o.js → treemap-SAJKECNS-D5NThB4V.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-WPOLBNNX-CfwdxcsG.js → usecaseDiagram-WPOLBNNX-fLWGvuS0.js} +1 -1
- package/dist/web/assets/{vennDiagram-Q3Y3IDCW-DQoKGtXo.js → vennDiagram-Q3Y3IDCW-DFzlk_u2.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-DQHLHmeZ.js → wardley-7MLQ67FV-0uG-noJX.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-IVAIDKWA-DV-lq7zb.js → wardleyDiagram-IVAIDKWA-DfEQPTLg.js} +1 -1
- package/dist/web/assets/{xychartDiagram-4URNHDU4-wV4ueoGC.js → xychartDiagram-4URNHDU4-BXVsWbL0.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/web/assets/channel-GGfPocHk.js +0 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-C-jE8afx.js +0 -1
- package/dist/web/assets/index-COSho7m2.css +0 -1
- package/dist/web/assets/index-D7GZPtTj.js +0 -147
- 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": "AAAA,SAAS,aAAgC;AACzC,SAAS,kBAAkB;AAC3B,SAAS,YAAY,WAAW,WAAW,aAAa,oBAAoB;AAC5E,SAAS,UAAU,SAAS,YAAY,MAAM,eAAe;AAC7D,SAAS,gBAAgB;AAOzB,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAQ7B,MAAM,SAAS,CAAC,QAAa,CAAC,WAAW,YAAY,EAAE,SAAS,IAAI,KAAK,MAAM;AAC/E,MAAM,aAAa,CAAC,UAClB,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,cAAc,UAAU,WAAW,CAAC,IAAI,MAAM,UAAU,WAAW,CAAC,MAAM,GAAG;AAGzF,SAAS,mBAAmB,YAA0B;AAC3D,MAAI,QAAQ;AACZ,MAAI,CAAC,WAAW,UAAU,KAAK,CAAC,SAAS,KAAK,UAAU,GAAG;AACzD,QAAI,oBAAoB,KAAK,UAAU,GAAG;AACxC,UAAI;AACF,cAAM,MAAM,IAAI,IAAI,UAAU;AAC9B,gBAAQ;AAAA,UACN,IAAI,YACJ,CAAC,IAAI,SAAS,WAAW,GAAG,KAC5B,IAAI,SAAS,SAAS,KACtB,CAAC,IAAI,YACL,CAAC,IAAI,UACL,CAAC,IAAI,SACJ,IAAI,aAAa,SACd,6BAA6B,KAAK,IAAI,QAAQ,IAC9C,CAAC,IAAI;AAAA,QACX;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,OAAO;AACL,cAAQ,8DAA8D,KAAK,UAAU;AAAA,IACvF;AAAA,EACF;AACA,MAAI,CAAC;AACH,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACJ;AAEA,SAAS,WAAW,MAAsB;AACxC,MAAI,CAAC,WAAW,IAAI,KAAK,WAAW,IAAI,KAAK,QAAQ,IAAI,MAAM,QAAQ,QAAQ,IAAI,CAAC;AAClF,UAAM,IAAI,SAAS,wBAAwB,wFAAkB,GAAG;AAClE,MAAI;AACF,UAAM,SAAS,KAAK,aAAa,QAAQ,QAAQ,IAAI,CAAC,CAAC,GAAG,SAAS,QAAQ,IAAI,CAAC,CAAC;AACjF,eAAW,QAAQ,MAAM,GAAG,UAAU,OAAO,UAAU,IAAI;AAC3D,WAAO;AAAA,EACT,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,WAAW,MAAoB;AACtC,MAAI;AACF,UAAM,OAAO,UAAU,IAAI;AAC3B,QAAI,CAAC,KAAK,YAAY,KAAK,KAAK,eAAe,KAAK,YAAY,IAAI,EAAE;AACpE,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,EACJ,SAAS,OAAO;AACd,QAAK,MAAgC,SAAS,SAAU;AACxD,QAAI,iBAAiB,SAAU,OAAM;AACrC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,QACP,QACA,YACmE;AACnE,MAAI;AACF,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AACF,MACE,wIAAwI;AAAA,IACtI;AAAA,EACF;AAEA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SACE;AAAA,IACJ;AACF,MAAI,gCAAgC,KAAK,MAAM;AAC7C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AACF,MACE,sFAAsF;AAAA,IACpF;AAAA,EACF;AAEA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AACF,MACE,yFAAyF;AAAA,IACvF;AAAA,EACF;AAEA,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AACF,MAAI,gDAAgD,KAAK,MAAM;AAC7D,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AACF,MAAI,4BAA4B,KAAK,MAAM;AACzC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AACF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AACF;AAGO,MAAM,cAAc;AAAA,EAGzB,YACmB,UACA,YACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAJF,OAAO,oBAAI,IAAiB;AAAA,EACrC,UAAU;AAAA,EAMlB,IAAI,YAAqB;AACvB,WAAO,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,EAAE,KAAK,MAAM;AAAA,EAC5C;AAAA,EAEA,eAAe,MAAoB;AACjC,QAAI,CAAC,KAAK,UAAW;AACrB,QAAI,YAAY,QAAQ,IAAI;AAC5B,QAAI;AACF,kBAAY,aAAa,IAAI;AAAA,IAC/B,QAAQ;AACN,UAAI;AACF,oBAAY,KAAK,aAAa,QAAQ,SAAS,CAAC,GAAG,SAAS,SAAS,CAAC;AAAA,MACxE,QAAQ;AAAA,MAER;AAAA,IACF;AACA,QACE,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,EAAE;AAAA,MACtB,CAAC,QACC,OAAO,GAAG,MACT,aAAa,IAAI,KAAK,MAAM,SAAS,KAAK,aAAa,WAAW,IAAI,KAAK,IAAI;AAAA,IACpF;AAEA,YAAM,IAAI,SAAS,qBAAqB,4HAAwB,GAAG;AAAA,EACvE;AAAA,EAEA,MAAM,
|
|
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
|
+
}
|
package/dist/server/projects.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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).
|
|
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 = {
|
|
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": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,YAAY,WAAW,cAAc,gBAAgB;AAC9D,SAAS,UAAU,YAAY,UAAU,SAAS,WAAW;AAC7D,SAAS,
|
|
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
|
}
|
package/dist/server/service.js
CHANGED
|
@@ -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.
|
|
139
|
-
|
|
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
|
}
|