@covro/covro 0.2.1-dev.4 → 0.2.1-dev.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +6 -3
  2. package/README.zh-CN.md +5 -2
  3. package/dist/codex/auto-review.js +55 -0
  4. package/dist/codex/auto-review.js.map +2 -2
  5. package/dist/server/http.js +16 -0
  6. package/dist/server/http.js.map +2 -2
  7. package/dist/server/project-clones.js +332 -0
  8. package/dist/server/project-clones.js.map +7 -0
  9. package/dist/server/service.js +20 -0
  10. package/dist/server/service.js.map +2 -2
  11. package/dist/server/workspaces.js +6 -2
  12. package/dist/server/workspaces.js.map +2 -2
  13. package/dist/shared/validation.js +5 -0
  14. package/dist/shared/validation.js.map +2 -2
  15. package/dist/web/assets/{abnfDiagram-WEYOIA3C-DTTcFQGL.js → abnfDiagram-WEYOIA3C-BkC1EVBk.js} +1 -1
  16. package/dist/web/assets/{arc-K2pKIwfu.js → arc-CYGT9huc.js} +1 -1
  17. package/dist/web/assets/{architecture-WOLXFQ4H-DrxN_1CG.js → architecture-WOLXFQ4H-DCsF3gVg.js} +1 -1
  18. package/dist/web/assets/{architectureDiagram-ZHVYKT53-CyyCrVAb.js → architectureDiagram-ZHVYKT53-Cn6kovuz.js} +1 -1
  19. package/dist/web/assets/{blockDiagram-RJAE7QA3-C8vl8QXS.js → blockDiagram-RJAE7QA3-CNk2zXm9.js} +1 -1
  20. package/dist/web/assets/{c4Diagram-XDPPPE54-BtnIITFN.js → c4Diagram-XDPPPE54-CTuI_1XJ.js} +1 -1
  21. package/dist/web/assets/channel-BDxsxdm-.js +1 -0
  22. package/dist/web/assets/{chunk-2E7WHVMD-BssauSch.js → chunk-2E7WHVMD-TrYs0qV3.js} +1 -1
  23. package/dist/web/assets/{chunk-4EMNKR2T-BZOo1RWg.js → chunk-4EMNKR2T-QL4-PZ4r.js} +1 -1
  24. package/dist/web/assets/{chunk-55XCU76K-D8ZWazMQ.js → chunk-55XCU76K-yynn-EEr.js} +1 -1
  25. package/dist/web/assets/{chunk-7S6XSTFO-C8WqPzUl.js → chunk-7S6XSTFO-dzrEn-_W.js} +1 -1
  26. package/dist/web/assets/{chunk-AQDTSKZB-Coo6Axpg.js → chunk-AQDTSKZB-DaNZMEGO.js} +1 -1
  27. package/dist/web/assets/{chunk-DSNLSHJZ-5TwmEPWR.js → chunk-DSNLSHJZ-BnMtx8Ho.js} +1 -1
  28. package/dist/web/assets/{chunk-HAGFOTED-Dgtj-Fc_.js → chunk-HAGFOTED-fg-JyiA5.js} +1 -1
  29. package/dist/web/assets/{chunk-HNE4Q36P-COoS4UPL.js → chunk-HNE4Q36P-CCsfPbcn.js} +1 -1
  30. package/dist/web/assets/{chunk-NQOKRVIF-tPhNkBBj.js → chunk-NQOKRVIF-DLlOpcWZ.js} +1 -1
  31. package/dist/web/assets/{chunk-UHW7PFBP-BLk_BkCl.js → chunk-UHW7PFBP-B-sPKpeS.js} +1 -1
  32. package/dist/web/assets/{chunk-VQ5W62QI-HBeSVTU0.js → chunk-VQ5W62QI-BVqGMzey.js} +1 -1
  33. package/dist/web/assets/{chunk-WQQL4CDK-LmZUm8If.js → chunk-WQQL4CDK-DcwJXNuh.js} +1 -1
  34. package/dist/web/assets/{chunk-XXDRQBXY-49rKU8Nk.js → chunk-XXDRQBXY-DRwLkqyU.js} +1 -1
  35. package/dist/web/assets/{chunk-YZO67HFG-B_TgpaEG.js → chunk-YZO67HFG-BrrgBnrQ.js} +1 -1
  36. package/dist/web/assets/{chunk-ZAUX2LTX-f7-DSOrY.js → chunk-ZAUX2LTX-C7qvwkRr.js} +1 -1
  37. package/dist/web/assets/{chunk-ZCHMQSOR-QhJOh7x0.js → chunk-ZCHMQSOR-DdBd3Cvs.js} +1 -1
  38. package/dist/web/assets/{classDiagram-v2-4MYWR3UA-CwF2HuoZ.js → classDiagram-v2-4MYWR3UA-Dn2RZb3v.js} +1 -1
  39. package/dist/web/assets/{cose-bilkent-JH36ORCC-CLvKOlx5.js → cose-bilkent-JH36ORCC-BlXvtfgE.js} +1 -1
  40. package/dist/web/assets/{cynefin-EF2NZ3EQ-BNRVavZN.js → cynefin-EF2NZ3EQ-odDgTxc-.js} +1 -1
  41. package/dist/web/assets/{cynefinDiagram-WF24UDRP-DEd83kgI.js → cynefinDiagram-WF24UDRP-BySEoCW8.js} +1 -1
  42. package/dist/web/assets/{dagre-NK4GIZVX-C340UEI3.js → dagre-NK4GIZVX-ihxmhB4X.js} +1 -1
  43. package/dist/web/assets/{diagram-CBK3MWTC-CuN7ncUm.js → diagram-CBK3MWTC-Ckr5P3IM.js} +1 -1
  44. package/dist/web/assets/{diagram-I5XIIV2D-D9mGAfA7.js → diagram-I5XIIV2D-D5BZIval.js} +1 -1
  45. package/dist/web/assets/{diagram-ODIL2UCW-CXtBv4bI.js → diagram-ODIL2UCW-BC5jjqqY.js} +1 -1
  46. package/dist/web/assets/{diagram-PFBEJUXE-B8qP5nGT.js → diagram-PFBEJUXE-D_IdYKqC.js} +1 -1
  47. package/dist/web/assets/{diagram-RJA3I4VV-BVrzsw2b.js → diagram-RJA3I4VV-DlPNsPqP.js} +1 -1
  48. package/dist/web/assets/{diagram-TYUNFSNR-Djfine3D.js → diagram-TYUNFSNR-Ca2ANhGM.js} +1 -1
  49. package/dist/web/assets/{dist-D8kDnYqD.js → dist-BSWkYZRr.js} +1 -1
  50. package/dist/web/assets/{ebnfDiagram-V4P3TTPT-C69Oiekp.js → ebnfDiagram-V4P3TTPT-BrqvmeiB.js} +1 -1
  51. package/dist/web/assets/{elk-QONTBCTQ-Bd3BP1h1.js → elk-QONTBCTQ-BfXeOE2E.js} +1 -1
  52. package/dist/web/assets/{erDiagram-KYQIGQAA-Dxcl5jVl.js → erDiagram-KYQIGQAA-CMuYoCVJ.js} +1 -1
  53. package/dist/web/assets/{eventmodeling-K75KTNOO-Do8p535i.js → eventmodeling-K75KTNOO-9s4vQqv1.js} +1 -1
  54. package/dist/web/assets/flowDiagram-IUGLLWBU-BHwP7Ha7.js +1 -0
  55. package/dist/web/assets/{ganttDiagram-R7GPNTJY-MBGUZbVy.js → ganttDiagram-R7GPNTJY-B-Xmfmm2.js} +1 -1
  56. package/dist/web/assets/{gitGraph-VSP46ZUC-CHxRIbkg.js → gitGraph-VSP46ZUC-ZSbSQ7jf.js} +1 -1
  57. package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-DbW0GeW4.js → gitGraphDiagram-PDYL7XHY-BhxijLaZ.js} +1 -1
  58. package/dist/web/assets/index-BrvtlTwN.js +147 -0
  59. package/dist/web/assets/{index-DaMLk_sl.css → index-mkDvqMQ3.css} +1 -1
  60. package/dist/web/assets/{info-OHQRW6UA-K8BF1xZb.js → info-OHQRW6UA-CTSCkjt3.js} +1 -1
  61. package/dist/web/assets/{infoDiagram-MVDRKAX5-QaWV5-SY.js → infoDiagram-MVDRKAX5-D46MTesD.js} +1 -1
  62. package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-DpVz_sC0.js → ishikawaDiagram-HPEHV2GE-Bafu9NOk.js} +1 -1
  63. package/dist/web/assets/{journeyDiagram-BOSHW7O7-S9MzozV7.js → journeyDiagram-BOSHW7O7-CRsIi1Ry.js} +1 -1
  64. package/dist/web/assets/{kanban-definition-FNSA4HC5-BiU8Ibw-.js → kanban-definition-FNSA4HC5-DjIT_uLs.js} +1 -1
  65. package/dist/web/assets/{line-B1KaX5ms.js → line-B5MDdgVs.js} +1 -1
  66. package/dist/web/assets/{linear-Cg1dwAtU.js → linear-wm7JWxlT.js} +1 -1
  67. package/dist/web/assets/{mermaid-parser.core-hzYHYxfI.js → mermaid-parser.core-C7jTW4sZ.js} +3 -3
  68. package/dist/web/assets/{mermaid.core-N8xYYvXy.js → mermaid.core-DcWRGjP5.js} +4 -4
  69. package/dist/web/assets/{mindmap-definition-6OY57UG3-BmIQCrEd.js → mindmap-definition-6OY57UG3-qY6vV0UI.js} +1 -1
  70. package/dist/web/assets/{packet-JDAUHWVQ-DhOTX9De.js → packet-JDAUHWVQ-9B7vLYK0.js} +1 -1
  71. package/dist/web/assets/{pegDiagram-ECIUJNBX-CVfE6yUu.js → pegDiagram-ECIUJNBX-B7gLZ_Fs.js} +1 -1
  72. package/dist/web/assets/{pie-XZMESJXO-BayfKVL8.js → pie-XZMESJXO-MfazZSiE.js} +1 -1
  73. package/dist/web/assets/{pieDiagram-GNMPY74T-ClbMRve3.js → pieDiagram-GNMPY74T-1ahagzzC.js} +1 -1
  74. package/dist/web/assets/{quadrantDiagram-D57K4MC3-DFp-P_EZ.js → quadrantDiagram-D57K4MC3-C-GkwjOp.js} +1 -1
  75. package/dist/web/assets/{radar-ABXABTNO-Bt1EAsxC.js → radar-ABXABTNO-CgutvWTi.js} +1 -1
  76. package/dist/web/assets/{railroad-I3PHUGI6-CSos2Rm9.js → railroad-I3PHUGI6-Bl9LJINm.js} +1 -1
  77. package/dist/web/assets/{railroad-abnf-LNEFI6M7-CebY-3Kh.js → railroad-abnf-LNEFI6M7-CGRpAnuU.js} +1 -1
  78. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-DpA6-TBs.js → railroad-ebnf-DWYD2UWJ-D6A47xZS.js} +1 -1
  79. package/dist/web/assets/{railroad-peg-JR7BVNR7-Bg3QPvi5.js → railroad-peg-JR7BVNR7-DxGphrFO.js} +1 -1
  80. package/dist/web/assets/{railroadDiagram-Y72NRBL7-BM4RAgqS.js → railroadDiagram-Y72NRBL7-D8P17OV4.js} +1 -1
  81. package/dist/web/assets/{requirementDiagram-EQYSW5RA-CqP9bCS_.js → requirementDiagram-EQYSW5RA-c93_xAaQ.js} +1 -1
  82. package/dist/web/assets/{sankeyDiagram-SY24JHO5-BhS8f6-k.js → sankeyDiagram-SY24JHO5-FankqwcZ.js} +1 -1
  83. package/dist/web/assets/{sequenceDiagram-BLEHNHQU-p3C1DgeC.js → sequenceDiagram-BLEHNHQU-CWH5SeL1.js} +1 -1
  84. package/dist/web/assets/{src-9XJAFibc.js → src-BXQOmVZv.js} +1 -1
  85. package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-DVVy8chY.js → stateDiagram-v2-A6IESKWZ-DLfcw0e7.js} +1 -1
  86. package/dist/web/assets/{swimlanes-B3ENRKFQ-CtMPO6Gw.js → swimlanes-B3ENRKFQ-DZ-yBiap.js} +1 -1
  87. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BlmdUBbW.js +8 -0
  88. package/dist/web/assets/{timeline-definition-JXYKGLGB-DuTqpjLQ.js → timeline-definition-JXYKGLGB-B7XvxDHY.js} +1 -1
  89. package/dist/web/assets/{treeView-D4JQ5SDB-qpOZEphu.js → treeView-D4JQ5SDB-DrB6wXJ0.js} +1 -1
  90. package/dist/web/assets/{treemap-SAJKECNS-Dm6e3TpG.js → treemap-SAJKECNS-BynFjsvx.js} +1 -1
  91. package/dist/web/assets/{usecaseDiagram-WPOLBNNX-DoCSdm5y.js → usecaseDiagram-WPOLBNNX-BISzHOHN.js} +1 -1
  92. package/dist/web/assets/{vennDiagram-Q3Y3IDCW-qzd4yvh5.js → vennDiagram-Q3Y3IDCW-BZwjTIST.js} +1 -1
  93. package/dist/web/assets/{wardley-7MLQ67FV-CkcgIQgQ.js → wardley-7MLQ67FV-2bM0pMhO.js} +1 -1
  94. package/dist/web/assets/{wardleyDiagram-IVAIDKWA-CE6OR9hA.js → wardleyDiagram-IVAIDKWA-CflkU8us.js} +1 -1
  95. package/dist/web/assets/{xychartDiagram-4URNHDU4-OrFXHVc6.js → xychartDiagram-4URNHDU4-DRspOlD3.js} +1 -1
  96. package/dist/web/index.html +4 -4
  97. package/package.json +5 -3
  98. package/dist/web/assets/channel-CDExoPVe.js +0 -1
  99. package/dist/web/assets/flowDiagram-IUGLLWBU-X85uzgrG.js +0 -1
  100. package/dist/web/assets/index-Cz3AOJOw.js +0 -142
  101. package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BRXJWgnO.js +0 -8
@@ -0,0 +1,332 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { accessSync, constants, lstatSync, readdirSync, realpathSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { cloneProjectSchema } from "../shared/validation.js";
7
+ import { containsPath } from "./projects.js";
8
+ const active = (job) => ["cloning", "cancelling"].includes(job.view.status);
9
+ const hasControl = (value) => [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
10
+ function validateRepository(repository) {
11
+ let valid = false;
12
+ if (!hasControl(repository) && !/[\s\\]/.test(repository)) {
13
+ if (/^(https|ssh):\/\//.test(repository)) {
14
+ try {
15
+ const url = new URL(repository);
16
+ valid = Boolean(
17
+ url.hostname && !url.hostname.startsWith("-") && url.pathname.length > 1 && !url.password && !url.search && !url.hash && (url.protocol === "ssh:" ? /^(?:[a-zA-Z0-9_][\w.-]*)?$/.test(url.username) : !url.username)
18
+ );
19
+ } catch {
20
+ }
21
+ } else {
22
+ valid = /^(?:[a-zA-Z0-9_][\w.-]*@)?[a-zA-Z0-9][\w.-]*:[^/:\s][^\s]*$/.test(repository);
23
+ }
24
+ }
25
+ if (!valid)
26
+ throw new AppError(
27
+ "INVALID_REPOSITORY",
28
+ "\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",
29
+ 400
30
+ );
31
+ }
32
+ function targetPath(path) {
33
+ if (!isAbsolute(path) || hasControl(path) || resolve(path) === dirname(resolve(path)))
34
+ throw new AppError("INVALID_PROJECT_PATH", "\u8BF7\u586B\u5199\u670D\u52A1\u5668\u4E0A\u7684\u7EDD\u5BF9\u76EE\u6807\u76EE\u5F55", 400);
35
+ try {
36
+ const target = join(realpathSync(dirname(resolve(path))), basename(resolve(path)));
37
+ accessSync(dirname(target), constants.W_OK | constants.X_OK);
38
+ return target;
39
+ } catch {
40
+ throw new AppError(
41
+ "CLONE_PARENT_UNAVAILABLE",
42
+ "\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",
43
+ 409
44
+ );
45
+ }
46
+ }
47
+ function checkEmpty(path) {
48
+ try {
49
+ const stat = lstatSync(path);
50
+ if (!stat.isDirectory() || stat.isSymbolicLink() || readdirSync(path).length)
51
+ throw new AppError(
52
+ "CLONE_TARGET_EXISTS",
53
+ "\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",
54
+ 409
55
+ );
56
+ } catch (error) {
57
+ if (error.code === "ENOENT") return;
58
+ if (error instanceof AppError) throw error;
59
+ throw new AppError(
60
+ "CLONE_TARGET_UNAVAILABLE",
61
+ "\u65E0\u6CD5\u68C0\u67E5\u76EE\u6807\u76EE\u5F55\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u5668\u8DEF\u5F84\u548C\u8BBF\u95EE\u6743\u9650",
62
+ 409
63
+ );
64
+ }
65
+ }
66
+ function failure(output, missingGit) {
67
+ if (missingGit)
68
+ return {
69
+ code: "CLONE_GIT_MISSING",
70
+ 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"
71
+ };
72
+ if (/authentication failed|could not read username|permission denied|publickey|terminal prompts disabled|credential|access denied|401|403/i.test(
73
+ output
74
+ ))
75
+ return {
76
+ code: "CLONE_AUTH_FAILED",
77
+ message: "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"
78
+ };
79
+ if (/host key verification failed/i.test(output))
80
+ return {
81
+ code: "CLONE_HOST_KEY_FAILED",
82
+ 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"
83
+ };
84
+ if (/repository .*not found|does not appear to be a git repository|not a repository|404/i.test(
85
+ output
86
+ ))
87
+ return {
88
+ code: "CLONE_REPOSITORY_UNAVAILABLE",
89
+ 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"
90
+ };
91
+ if (/could not resolve|failed to connect|unable to access|connection|timed out|certificate/i.test(
92
+ output
93
+ ))
94
+ return {
95
+ code: "CLONE_NETWORK_FAILED",
96
+ 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"
97
+ };
98
+ if (/already exists and is not an empty directory/i.test(output))
99
+ return {
100
+ code: "CLONE_TARGET_NOT_EMPTY",
101
+ 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"
102
+ };
103
+ if (/no space left|disk quota/i.test(output))
104
+ return {
105
+ code: "CLONE_DISK_FULL",
106
+ 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"
107
+ };
108
+ return {
109
+ code: "CLONE_FAILED",
110
+ 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"
111
+ };
112
+ }
113
+ class ProjectClones {
114
+ constructor(register, registered) {
115
+ this.register = register;
116
+ this.registered = registered;
117
+ }
118
+ register;
119
+ registered;
120
+ jobs = /* @__PURE__ */ new Map();
121
+ closing = false;
122
+ get hasActive() {
123
+ return [...this.jobs.values()].some(active);
124
+ }
125
+ assertIdlePath(path) {
126
+ if (!this.hasActive) return;
127
+ let canonical = resolve(path);
128
+ try {
129
+ canonical = realpathSync(path);
130
+ } catch {
131
+ try {
132
+ canonical = join(realpathSync(dirname(canonical)), basename(canonical));
133
+ } catch {
134
+ }
135
+ }
136
+ if ([...this.jobs.values()].some(
137
+ (job) => active(job) && (containsPath(job.view.path, canonical) || containsPath(canonical, job.view.path))
138
+ ))
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
+ }
141
+ start(input) {
142
+ const parsed = cloneProjectSchema.parse(input);
143
+ validateRepository(parsed.repository);
144
+ const fingerprint = createHash("sha256").update(JSON.stringify(parsed)).digest("hex");
145
+ const previous = this.jobs.get(parsed.clientOperationId);
146
+ if (previous) {
147
+ if (previous.fingerprint !== fingerprint)
148
+ throw new AppError("CLONE_ID_CONFLICT", "\u8BE5\u64CD\u4F5C ID \u5DF2\u7528\u4E8E\u5176\u4ED6\u514B\u9686\u8BF7\u6C42", 409);
149
+ return this.get(previous.view.id);
150
+ }
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);
153
+ this.assertIdlePath(path);
154
+ if (this.registered(path))
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);
156
+ checkEmpty(path);
157
+ if ([...this.jobs.values()].filter(active).length >= 4)
158
+ 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);
159
+ if (this.jobs.size >= 100) {
160
+ const oldest = [...this.jobs].find(([, job2]) => !active(job2));
161
+ if (oldest) this.jobs.delete(oldest[0]);
162
+ }
163
+ const job = {
164
+ view: {
165
+ id: parsed.clientOperationId,
166
+ path,
167
+ name: parsed.name,
168
+ status: "cloning",
169
+ progress: "\u6B63\u5728\u8FDE\u63A5\u4ED3\u5E93\u2026",
170
+ progressStage: "connecting"
171
+ },
172
+ fingerprint,
173
+ done: Promise.resolve()
174
+ };
175
+ this.jobs.set(job.view.id, job);
176
+ job.done = this.run(job, parsed.repository);
177
+ return this.get(job.view.id);
178
+ }
179
+ get(id) {
180
+ const job = this.jobs.get(id);
181
+ if (!job)
182
+ throw new AppError(
183
+ "CLONE_NOT_FOUND",
184
+ "\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",
185
+ 404
186
+ );
187
+ return structuredClone(job.view);
188
+ }
189
+ cancel(id) {
190
+ const job = this.jobs.get(id);
191
+ this.get(id);
192
+ if (job && active(job) && job.view.status !== "cancelling") {
193
+ job.view.status = "cancelling";
194
+ job.view.progress = "\u6B63\u5728\u53D6\u6D88\uFF0C\u7B49\u5F85 Git \u9000\u51FA\u2026";
195
+ job.view.progressStage = "cancelling";
196
+ this.signal(job, "SIGTERM");
197
+ }
198
+ return this.get(id);
199
+ }
200
+ retryRegistration(id) {
201
+ const job = this.jobs.get(id);
202
+ this.get(id);
203
+ if (job?.view.status === "registration-failed") this.finishRegistration(job);
204
+ return this.get(id);
205
+ }
206
+ finishRegistration(job) {
207
+ try {
208
+ job.view.project = this.register({ path: job.view.path, name: job.view.name });
209
+ job.view.status = "completed";
210
+ job.view.progress = "\u514B\u9686\u5B8C\u6210\uFF0C\u9879\u76EE\u5DF2\u6DFB\u52A0";
211
+ job.view.progressStage = "completed";
212
+ delete job.view.errorCode;
213
+ delete job.view.error;
214
+ } catch {
215
+ job.view.status = "registration-failed";
216
+ job.view.progress = "\u514B\u9686\u5DF2\u5B8C\u6210\uFF0C\u9879\u76EE\u5C1A\u672A\u6DFB\u52A0";
217
+ job.view.progressStage = "registration_failed";
218
+ job.view.errorCode = "CLONE_REGISTRATION_FAILED";
219
+ job.view.error = "\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";
220
+ }
221
+ }
222
+ signal(job, signal) {
223
+ if (!job.child?.pid) return;
224
+ try {
225
+ if (process.platform === "win32") job.child.kill(signal);
226
+ else process.kill(-job.child.pid, signal);
227
+ } catch {
228
+ }
229
+ }
230
+ async run(job, repository) {
231
+ let output = "";
232
+ let pending = "";
233
+ let missingGit = false;
234
+ let timedOut = false;
235
+ const sshCommand = process.env.GIT_SSH_COMMAND ?? (process.env.GIT_SSH ? `'${process.env.GIT_SSH.replaceAll("'", "'\\''")}'` : "ssh");
236
+ const child = spawn(
237
+ "git",
238
+ [
239
+ "-c",
240
+ "protocol.allow=never",
241
+ "-c",
242
+ "protocol.https.allow=always",
243
+ "-c",
244
+ "protocol.ssh.allow=always",
245
+ "clone",
246
+ "--progress",
247
+ "--",
248
+ repository,
249
+ job.view.path
250
+ ],
251
+ {
252
+ detached: process.platform !== "win32",
253
+ stdio: ["ignore", "pipe", "pipe"],
254
+ env: {
255
+ ...process.env,
256
+ GIT_TERMINAL_PROMPT: "0",
257
+ GCM_INTERACTIVE: "never",
258
+ GIT_SSH_COMMAND: `${sshCommand} -oBatchMode=yes -oStrictHostKeyChecking=yes`,
259
+ LC_ALL: "C"
260
+ }
261
+ }
262
+ );
263
+ job.child = child;
264
+ const read = (chunk) => {
265
+ output = (output + chunk.toString()).slice(-16384);
266
+ pending = (pending + chunk.toString()).slice(-4096);
267
+ const lines = pending.split(/[\r\n]/);
268
+ pending = lines.pop() ?? "";
269
+ for (const line of lines) {
270
+ const match = /^(?:remote: )?(Receiving objects|Resolving deltas|Updating files|Checking out files|Counting objects|Compressing objects):\s+(\d{1,3})%/.exec(
271
+ line
272
+ );
273
+ if (!match || job.view.status !== "cloning") continue;
274
+ const labels = {
275
+ "Receiving objects": ["receiving", "\u6B63\u5728\u63A5\u6536\u5BF9\u8C61"],
276
+ "Resolving deltas": ["resolving", "\u6B63\u5728\u89E3\u6790\u5DEE\u5F02"],
277
+ "Updating files": ["checking_out", "\u6B63\u5728\u68C0\u51FA\u6587\u4EF6"],
278
+ "Checking out files": ["checking_out", "\u6B63\u5728\u68C0\u51FA\u6587\u4EF6"],
279
+ "Counting objects": ["counting", "\u6B63\u5728\u7EDF\u8BA1\u5BF9\u8C61"],
280
+ "Compressing objects": ["compressing", "\u6B63\u5728\u538B\u7F29\u5BF9\u8C61"]
281
+ };
282
+ [job.view.progressStage, job.view.progress] = labels[match[1]];
283
+ job.view.percent = Math.min(100, Number(match[2]));
284
+ }
285
+ };
286
+ child.stdout?.on("data", read);
287
+ child.stderr?.on("data", read);
288
+ const timeout = setTimeout(() => {
289
+ timedOut = true;
290
+ this.cancel(job.view.id);
291
+ }, 30 * 6e4);
292
+ timeout.unref();
293
+ const escalation = setInterval(() => {
294
+ if (job.view.status === "cancelling") this.signal(job, "SIGKILL");
295
+ }, 3e3);
296
+ escalation.unref();
297
+ const code = await new Promise((done) => {
298
+ child.once("error", (error) => {
299
+ missingGit = error.code === "ENOENT";
300
+ });
301
+ child.once("close", done);
302
+ });
303
+ clearTimeout(timeout);
304
+ clearInterval(escalation);
305
+ job.child = void 0;
306
+ delete job.view.percent;
307
+ if (job.view.status === "cancelling") {
308
+ job.view.status = timedOut ? "failed" : "cancelled";
309
+ job.view.progress = timedOut ? "\u514B\u9686\u8D85\u65F6\uFF0C\u5DF2\u505C\u6B62" : "\u5DF2\u53D6\u6D88\u514B\u9686";
310
+ job.view.progressStage = timedOut ? "timed_out" : "cancelled";
311
+ job.view.errorCode = timedOut ? "CLONE_TIMED_OUT" : "CLONE_CANCELLED";
312
+ 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`;
313
+ } else if (code !== 0) {
314
+ job.view.status = "failed";
315
+ job.view.progress = "\u514B\u9686\u5931\u8D25";
316
+ job.view.progressStage = "failed";
317
+ const problem = failure(output, missingGit);
318
+ job.view.errorCode = problem.code;
319
+ 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`;
320
+ } else this.finishRegistration(job);
321
+ }
322
+ async close() {
323
+ this.closing = true;
324
+ for (const job of this.jobs.values()) if (active(job)) this.cancel(job.view.id);
325
+ await Promise.all([...this.jobs.values()].map((job) => job.done));
326
+ }
327
+ }
328
+ export {
329
+ ProjectClones,
330
+ validateRepository
331
+ };
332
+ //# sourceMappingURL=project-clones.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": ["job"]
7
+ }
@@ -16,6 +16,7 @@ import {
16
16
  import { EventHub } from "./events.js";
17
17
  import { TurnUsageStore } from "./db/turn-usage.js";
18
18
  import { Projection, isActive, isTerminal } from "./projection.js";
19
+ import { ProjectClones } from "./project-clones.js";
19
20
  import { renderThreadExport } from "./thread-export.js";
20
21
  import { readLocalImage } from "./images.js";
21
22
  import { temporaryFileRoots } from "./local-files.js";
@@ -32,6 +33,15 @@ class GatewayService {
32
33
  this.projects,
33
34
  options.workspacesRoot
34
35
  );
36
+ this.projectClones = new ProjectClones(
37
+ (input) => {
38
+ this.workspaces.assertProjectDirectory(input.path);
39
+ const project = this.projects.add(input);
40
+ this.events.publishProjects(this.projects.list());
41
+ return project;
42
+ },
43
+ (path) => this.projects.list().some((project) => project.canonicalRoot === path)
44
+ );
35
45
  this.codex = options.codex;
36
46
  this.shell = new ShellService(this.codex);
37
47
  this.events = new EventHub(options.limits?.eventBytes);
@@ -41,6 +51,7 @@ class GatewayService {
41
51
  this.removeListener = this.codex.onEvent((event) => this.consume(event));
42
52
  }
43
53
  options;
54
+ projectClones;
44
55
  shell;
45
56
  events;
46
57
  projection;
@@ -96,6 +107,7 @@ class GatewayService {
96
107
  this.maintenance = value;
97
108
  }
98
109
  prepareUpdate() {
110
+ if (this.projectClones.hasActive) return { ready: false, reason: "\u7B49\u5F85\u9879\u76EE\u514B\u9686\u5B8C\u6210\u6216\u53D6\u6D88" };
99
111
  if (this.shell.active()) return { ready: false, reason: "\u7B49\u5F85 Shell \u547D\u4EE4\u7ED3\u675F" };
100
112
  if (this.inFlightOperations.size || this.resumes.size || this.pendingThreadCreations.size)
101
113
  return { ready: false, reason: "\u7B49\u5F85\u6B63\u5728\u63D0\u4EA4\u6216\u6062\u590D\u7684\u64CD\u4F5C\u5B8C\u6210" };
@@ -110,10 +122,17 @@ class GatewayService {
110
122
  }
111
123
  addProject(input) {
112
124
  this.workspaces.assertProjectDirectory(input.path);
125
+ this.projectClones.assertIdlePath(input.path);
113
126
  const project = this.projects.add(input);
114
127
  this.events.publishProjects(this.projects.list());
115
128
  return project;
116
129
  }
130
+ cloneProject(input) {
131
+ if (this.maintenance || !this.accepting)
132
+ throw new AppError("GATEWAY_STOPPING", "\u670D\u52A1\u6B63\u5728\u5347\u7EA7\u6216\u505C\u6B62\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", 503);
133
+ this.workspaces.assertProjectDirectory(input.path);
134
+ return this.projectClones.start(input);
135
+ }
117
136
  renameProject(projectId, input) {
118
137
  const project = this.projects.rename(projectId, input);
119
138
  this.events.publishProjects(this.projects.list());
@@ -855,6 +874,7 @@ class GatewayService {
855
874
  }
856
875
  async shutdown() {
857
876
  this.accepting = false;
877
+ await this.projectClones.close();
858
878
  this.store.recover();
859
879
  this.projection.runtimeReset();
860
880
  this.removeListener();