@covro/covro 0.3.1-dev.2 → 0.3.1-dev.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/README.zh-CN.md +7 -3
- 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/http.js +33 -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/service.js +10 -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/update-worker.cjs +4 -0
- package/dist/updates/release-notes.js +73 -0
- package/dist/updates/release-notes.js.map +7 -0
- package/dist/web/assets/{abnfDiagram-WEYOIA3C-BFmMm05x.js → abnfDiagram-WEYOIA3C-DU421iUb.js} +1 -1
- package/dist/web/assets/{arc-lnrGH4K4.js → arc-CsuqxmT0.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-CDbpw3av.js → architecture-WOLXFQ4H-DsTUnfuI.js} +1 -1
- package/dist/web/assets/{architectureDiagram-ZHVYKT53-Br4uXjzV.js → architectureDiagram-ZHVYKT53-Dc3b_aSn.js} +1 -1
- package/dist/web/assets/{blockDiagram-RJAE7QA3-2cto1XrH.js → blockDiagram-RJAE7QA3-CJo6zohL.js} +1 -1
- package/dist/web/assets/{c4Diagram-XDPPPE54-pPOONSo8.js → c4Diagram-XDPPPE54-PmN0Qtd6.js} +1 -1
- package/dist/web/assets/channel-BHJ4nRbW.js +1 -0
- package/dist/web/assets/{chunk-2E7WHVMD-CqOBPcBj.js → chunk-2E7WHVMD-BzKtvYJa.js} +1 -1
- package/dist/web/assets/{chunk-4EMNKR2T-BvMm1kcN.js → chunk-4EMNKR2T-CcgitPaF.js} +1 -1
- package/dist/web/assets/{chunk-55XCU76K-CojzCeCA.js → chunk-55XCU76K-NmryZEsr.js} +1 -1
- package/dist/web/assets/{chunk-7S6XSTFO-wa76hIsr.js → chunk-7S6XSTFO-DT2qxT9H.js} +1 -1
- package/dist/web/assets/{chunk-AQDTSKZB-tD5bFGjM.js → chunk-AQDTSKZB-BFnqrVb4.js} +1 -1
- package/dist/web/assets/{chunk-DSNLSHJZ-ByVh88M0.js → chunk-DSNLSHJZ-BavzRNt-.js} +1 -1
- package/dist/web/assets/{chunk-HAGFOTED-BhOQL-iG.js → chunk-HAGFOTED-Cx7L4TRC.js} +1 -1
- package/dist/web/assets/{chunk-HNE4Q36P-BZiD7Dq0.js → chunk-HNE4Q36P-C38XMJJg.js} +1 -1
- package/dist/web/assets/{chunk-NQOKRVIF-DhSSkcDm.js → chunk-NQOKRVIF-Dpc22eOs.js} +1 -1
- package/dist/web/assets/{chunk-UHW7PFBP-BONg0JLM.js → chunk-UHW7PFBP-BcpaVf5S.js} +1 -1
- package/dist/web/assets/{chunk-VQ5W62QI-CehpDQpX.js → chunk-VQ5W62QI-Y3XkIrNL.js} +1 -1
- package/dist/web/assets/{chunk-WQQL4CDK-C4Cm0U7u.js → chunk-WQQL4CDK-CGnED4F9.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-Ddcg44F2.js → chunk-XXDRQBXY-BCTxe0a_.js} +1 -1
- package/dist/web/assets/{chunk-YZO67HFG-CJYc1t5Y.js → chunk-YZO67HFG-BQrkk4s_.js} +1 -1
- package/dist/web/assets/{chunk-ZAUX2LTX-BeF7PiBe.js → chunk-ZAUX2LTX-DpzBrbZ-.js} +1 -1
- package/dist/web/assets/{chunk-ZCHMQSOR-BzxdttZQ.js → chunk-ZCHMQSOR-BY7KKI-8.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-4MYWR3UA-XeRozDII.js → classDiagram-v2-4MYWR3UA-DqahpcIZ.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-zvgm5h82.js → cose-bilkent-JH36ORCC-4jWM2ykR.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-DffawypB.js → cynefin-EF2NZ3EQ-CrG0LFB9.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-WF24UDRP-e2kk_Xg4.js → cynefinDiagram-WF24UDRP-Cry3FTAy.js} +1 -1
- package/dist/web/assets/{dagre-NK4GIZVX-DPTt7HIR.js → dagre-NK4GIZVX-Cg4GppVh.js} +1 -1
- package/dist/web/assets/{diagram-CBK3MWTC-CKQPukdf.js → diagram-CBK3MWTC-BpQwz3HE.js} +1 -1
- package/dist/web/assets/{diagram-I5XIIV2D-CBI_lUfl.js → diagram-I5XIIV2D-B3o4jW3k.js} +1 -1
- package/dist/web/assets/{diagram-ODIL2UCW-DXuDuOob.js → diagram-ODIL2UCW-BU1V7yRC.js} +1 -1
- package/dist/web/assets/{diagram-PFBEJUXE-C9Y1lchY.js → diagram-PFBEJUXE-D5xrUiPd.js} +1 -1
- package/dist/web/assets/{diagram-RJA3I4VV-CbcN7pHo.js → diagram-RJA3I4VV-BQ6G7wDx.js} +1 -1
- package/dist/web/assets/{diagram-TYUNFSNR-D_MmDJKc.js → diagram-TYUNFSNR-CJ7rOc5D.js} +1 -1
- package/dist/web/assets/{dist-C-d_G2FS.js → dist-BID9-USD.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-V4P3TTPT-CIlDI7Ps.js → ebnfDiagram-V4P3TTPT-C2KvMr_G.js} +1 -1
- package/dist/web/assets/{elk-QONTBCTQ-BrywoKK7.js → elk-QONTBCTQ-CeySt_Wk.js} +1 -1
- package/dist/web/assets/{erDiagram-KYQIGQAA-CKg6BT7V.js → erDiagram-KYQIGQAA-BM3B_3tj.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-BkwvMA16.js → eventmodeling-K75KTNOO-BMAMYPh6.js} +1 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-By-VA-YW.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7GPNTJY-DR_AlXXn.js → ganttDiagram-R7GPNTJY-DB-g05eG.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-Cv1cefI5.js → gitGraph-VSP46ZUC-PkenObLV.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-D_prlvP9.js → gitGraphDiagram-PDYL7XHY-DWIs-lGp.js} +1 -1
- package/dist/web/assets/index-B1wlD3sg.css +1 -0
- package/dist/web/assets/index-j8-Ii3xv.js +147 -0
- package/dist/web/assets/{info-OHQRW6UA--i-b1e1W.js → info-OHQRW6UA-DK-Wm12q.js} +1 -1
- package/dist/web/assets/{infoDiagram-MVDRKAX5-D5xGvRSb.js → infoDiagram-MVDRKAX5-JOEaVccz.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-CdDzjaNo.js → ishikawaDiagram-HPEHV2GE-B-2x06G3.js} +1 -1
- package/dist/web/assets/{journeyDiagram-BOSHW7O7-DYXkvOgG.js → journeyDiagram-BOSHW7O7-CSkaKHoo.js} +1 -1
- package/dist/web/assets/{kanban-definition-FNSA4HC5-DMRrVeRI.js → kanban-definition-FNSA4HC5-BxhCZnNd.js} +1 -1
- package/dist/web/assets/{line-IKuSdaT6.js → line-BXURMkw2.js} +1 -1
- package/dist/web/assets/{linear-DG-Bn7df.js → linear-pVpEGZKc.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Cr4hqI31.js → mermaid-parser.core-BkW0a1PY.js} +3 -3
- package/dist/web/assets/{mermaid.core-Ch6ggfwZ.js → mermaid.core-CeVhftut.js} +4 -4
- package/dist/web/assets/{mindmap-definition-6OY57UG3-BLIFHye9.js → mindmap-definition-6OY57UG3-mQdsPF38.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-BaBw3Vck.js → packet-JDAUHWVQ-oobWVwGf.js} +1 -1
- package/dist/web/assets/{pegDiagram-ECIUJNBX-BgFAs43s.js → pegDiagram-ECIUJNBX-FSt7wyW3.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-Dr-clP68.js → pie-XZMESJXO-DUVTQw1D.js} +1 -1
- package/dist/web/assets/{pieDiagram-GNMPY74T-bQ1SwzEf.js → pieDiagram-GNMPY74T-etPUdDAu.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-D57K4MC3-suHq6vLw.js → quadrantDiagram-D57K4MC3-mfBTnvPL.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-Cd4Nn-aB.js → radar-ABXABTNO-D1Qyk6F_.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-BEUNjieA.js → railroad-I3PHUGI6-CwM9b5Py.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-DosoD9le.js → railroad-abnf-LNEFI6M7-cheN8Z4Q.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-CNykm4Du.js → railroad-ebnf-DWYD2UWJ-CEygxJQE.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-C97ZuCXG.js → railroad-peg-JR7BVNR7-CNOvVcos.js} +1 -1
- package/dist/web/assets/{railroadDiagram-Y72NRBL7-B1BnvTWP.js → railroadDiagram-Y72NRBL7-Cr86pwS6.js} +1 -1
- package/dist/web/assets/{requirementDiagram-EQYSW5RA-CrlURwLY.js → requirementDiagram-EQYSW5RA-CSxt1jYV.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-SY24JHO5-CEYWGGec.js → sankeyDiagram-SY24JHO5-E6hncSWD.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-BLEHNHQU-CbUEnXPs.js → sequenceDiagram-BLEHNHQU-BUTVi_WU.js} +1 -1
- package/dist/web/assets/{src-Dy1r7Mij.js → src-BUn_l5HW.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-DkxgX1jY.js → stateDiagram-v2-A6IESKWZ-CguJkUZp.js} +1 -1
- package/dist/web/assets/{swimlanes-B3ENRKFQ-B7RmFqNN.js → swimlanes-B3ENRKFQ-DXLF8UOQ.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BQoBHwji.js +8 -0
- package/dist/web/assets/{timeline-definition-JXYKGLGB-DdaRshUD.js → timeline-definition-JXYKGLGB-D-etuHSZ.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-UjtdDBri.js → treeView-D4JQ5SDB-CiMI5YgG.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-CfFPhlAU.js → treemap-SAJKECNS-BnncdTmf.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-WPOLBNNX-DpibMf1h.js → usecaseDiagram-WPOLBNNX-Crl4DQoV.js} +1 -1
- package/dist/web/assets/{vennDiagram-Q3Y3IDCW-DB_R8qT1.js → vennDiagram-Q3Y3IDCW-BxJZecDR.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-Bhatoa5s.js → wardley-7MLQ67FV-zPBbyPtv.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-IVAIDKWA-DBhy_JBR.js → wardleyDiagram-IVAIDKWA-CSyK6CR6.js} +1 -1
- package/dist/web/assets/{xychartDiagram-4URNHDU4-C2oV2jkE.js → xychartDiagram-4URNHDU4-eVobxotm.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/channel-D_YOL9S0.js +0 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-DTkRetsK.js +0 -1
- package/dist/web/assets/index-Br54jmas.js +0 -147
- package/dist/web/assets/index-Crq1cgK9.css +0 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-C0XU1o_x.js +0 -8
|
@@ -138,7 +138,7 @@ class ProjectClones {
|
|
|
138
138
|
))
|
|
139
139
|
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);
|
|
140
140
|
}
|
|
141
|
-
start(input) {
|
|
141
|
+
start(input, resolvePath = (path) => path) {
|
|
142
142
|
const parsed = cloneProjectSchema.parse(input);
|
|
143
143
|
validateRepository(parsed.repository);
|
|
144
144
|
const fingerprint = createHash("sha256").update(JSON.stringify(parsed)).digest("hex");
|
|
@@ -149,7 +149,7 @@ class ProjectClones {
|
|
|
149
149
|
return this.get(previous.view.id);
|
|
150
150
|
}
|
|
151
151
|
if (this.closing) throw new AppError("GATEWAY_STOPPING", "\u670D\u52A1\u6B63\u5728\u505C\u6B62\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", 503);
|
|
152
|
-
const path = targetPath(parsed.path);
|
|
152
|
+
const path = targetPath(resolvePath(parsed.path));
|
|
153
153
|
this.assertIdlePath(path);
|
|
154
154
|
if (this.registered(path))
|
|
155
155
|
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);
|
|
@@ -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
|
+
}
|
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,11 +133,14 @@ 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);
|
|
@@ -496,6 +500,9 @@ class GatewayService {
|
|
|
496
500
|
getThreadMarkdown(threadId, path) {
|
|
497
501
|
return this.readThreadFile(threadId, path, readLocalMarkdown);
|
|
498
502
|
}
|
|
503
|
+
getThreadText(threadId, path) {
|
|
504
|
+
return this.readThreadFile(threadId, path, readLocalText);
|
|
505
|
+
}
|
|
499
506
|
getThreadJson(threadId, path) {
|
|
500
507
|
return this.readThreadFile(threadId, path, readLocalJson);
|
|
501
508
|
}
|