@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.
- package/README.md +6 -3
- package/README.zh-CN.md +5 -2
- package/dist/codex/auto-review.js +55 -0
- package/dist/codex/auto-review.js.map +2 -2
- package/dist/server/http.js +16 -0
- package/dist/server/http.js.map +2 -2
- package/dist/server/project-clones.js +332 -0
- package/dist/server/project-clones.js.map +7 -0
- package/dist/server/service.js +20 -0
- package/dist/server/service.js.map +2 -2
- package/dist/server/workspaces.js +6 -2
- package/dist/server/workspaces.js.map +2 -2
- package/dist/shared/validation.js +5 -0
- package/dist/shared/validation.js.map +2 -2
- package/dist/web/assets/{abnfDiagram-WEYOIA3C-DTTcFQGL.js → abnfDiagram-WEYOIA3C-BkC1EVBk.js} +1 -1
- package/dist/web/assets/{arc-K2pKIwfu.js → arc-CYGT9huc.js} +1 -1
- package/dist/web/assets/{architecture-WOLXFQ4H-DrxN_1CG.js → architecture-WOLXFQ4H-DCsF3gVg.js} +1 -1
- package/dist/web/assets/{architectureDiagram-ZHVYKT53-CyyCrVAb.js → architectureDiagram-ZHVYKT53-Cn6kovuz.js} +1 -1
- package/dist/web/assets/{blockDiagram-RJAE7QA3-C8vl8QXS.js → blockDiagram-RJAE7QA3-CNk2zXm9.js} +1 -1
- package/dist/web/assets/{c4Diagram-XDPPPE54-BtnIITFN.js → c4Diagram-XDPPPE54-CTuI_1XJ.js} +1 -1
- package/dist/web/assets/channel-BDxsxdm-.js +1 -0
- package/dist/web/assets/{chunk-2E7WHVMD-BssauSch.js → chunk-2E7WHVMD-TrYs0qV3.js} +1 -1
- package/dist/web/assets/{chunk-4EMNKR2T-BZOo1RWg.js → chunk-4EMNKR2T-QL4-PZ4r.js} +1 -1
- package/dist/web/assets/{chunk-55XCU76K-D8ZWazMQ.js → chunk-55XCU76K-yynn-EEr.js} +1 -1
- package/dist/web/assets/{chunk-7S6XSTFO-C8WqPzUl.js → chunk-7S6XSTFO-dzrEn-_W.js} +1 -1
- package/dist/web/assets/{chunk-AQDTSKZB-Coo6Axpg.js → chunk-AQDTSKZB-DaNZMEGO.js} +1 -1
- package/dist/web/assets/{chunk-DSNLSHJZ-5TwmEPWR.js → chunk-DSNLSHJZ-BnMtx8Ho.js} +1 -1
- package/dist/web/assets/{chunk-HAGFOTED-Dgtj-Fc_.js → chunk-HAGFOTED-fg-JyiA5.js} +1 -1
- package/dist/web/assets/{chunk-HNE4Q36P-COoS4UPL.js → chunk-HNE4Q36P-CCsfPbcn.js} +1 -1
- package/dist/web/assets/{chunk-NQOKRVIF-tPhNkBBj.js → chunk-NQOKRVIF-DLlOpcWZ.js} +1 -1
- package/dist/web/assets/{chunk-UHW7PFBP-BLk_BkCl.js → chunk-UHW7PFBP-B-sPKpeS.js} +1 -1
- package/dist/web/assets/{chunk-VQ5W62QI-HBeSVTU0.js → chunk-VQ5W62QI-BVqGMzey.js} +1 -1
- package/dist/web/assets/{chunk-WQQL4CDK-LmZUm8If.js → chunk-WQQL4CDK-DcwJXNuh.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-49rKU8Nk.js → chunk-XXDRQBXY-DRwLkqyU.js} +1 -1
- package/dist/web/assets/{chunk-YZO67HFG-B_TgpaEG.js → chunk-YZO67HFG-BrrgBnrQ.js} +1 -1
- package/dist/web/assets/{chunk-ZAUX2LTX-f7-DSOrY.js → chunk-ZAUX2LTX-C7qvwkRr.js} +1 -1
- package/dist/web/assets/{chunk-ZCHMQSOR-QhJOh7x0.js → chunk-ZCHMQSOR-DdBd3Cvs.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-4MYWR3UA-CwF2HuoZ.js → classDiagram-v2-4MYWR3UA-Dn2RZb3v.js} +1 -1
- package/dist/web/assets/{cose-bilkent-JH36ORCC-CLvKOlx5.js → cose-bilkent-JH36ORCC-BlXvtfgE.js} +1 -1
- package/dist/web/assets/{cynefin-EF2NZ3EQ-BNRVavZN.js → cynefin-EF2NZ3EQ-odDgTxc-.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-WF24UDRP-DEd83kgI.js → cynefinDiagram-WF24UDRP-BySEoCW8.js} +1 -1
- package/dist/web/assets/{dagre-NK4GIZVX-C340UEI3.js → dagre-NK4GIZVX-ihxmhB4X.js} +1 -1
- package/dist/web/assets/{diagram-CBK3MWTC-CuN7ncUm.js → diagram-CBK3MWTC-Ckr5P3IM.js} +1 -1
- package/dist/web/assets/{diagram-I5XIIV2D-D9mGAfA7.js → diagram-I5XIIV2D-D5BZIval.js} +1 -1
- package/dist/web/assets/{diagram-ODIL2UCW-CXtBv4bI.js → diagram-ODIL2UCW-BC5jjqqY.js} +1 -1
- package/dist/web/assets/{diagram-PFBEJUXE-B8qP5nGT.js → diagram-PFBEJUXE-D_IdYKqC.js} +1 -1
- package/dist/web/assets/{diagram-RJA3I4VV-BVrzsw2b.js → diagram-RJA3I4VV-DlPNsPqP.js} +1 -1
- package/dist/web/assets/{diagram-TYUNFSNR-Djfine3D.js → diagram-TYUNFSNR-Ca2ANhGM.js} +1 -1
- package/dist/web/assets/{dist-D8kDnYqD.js → dist-BSWkYZRr.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-V4P3TTPT-C69Oiekp.js → ebnfDiagram-V4P3TTPT-BrqvmeiB.js} +1 -1
- package/dist/web/assets/{elk-QONTBCTQ-Bd3BP1h1.js → elk-QONTBCTQ-BfXeOE2E.js} +1 -1
- package/dist/web/assets/{erDiagram-KYQIGQAA-Dxcl5jVl.js → erDiagram-KYQIGQAA-CMuYoCVJ.js} +1 -1
- package/dist/web/assets/{eventmodeling-K75KTNOO-Do8p535i.js → eventmodeling-K75KTNOO-9s4vQqv1.js} +1 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-BHwP7Ha7.js +1 -0
- package/dist/web/assets/{ganttDiagram-R7GPNTJY-MBGUZbVy.js → ganttDiagram-R7GPNTJY-B-Xmfmm2.js} +1 -1
- package/dist/web/assets/{gitGraph-VSP46ZUC-CHxRIbkg.js → gitGraph-VSP46ZUC-ZSbSQ7jf.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-PDYL7XHY-DbW0GeW4.js → gitGraphDiagram-PDYL7XHY-BhxijLaZ.js} +1 -1
- package/dist/web/assets/index-BrvtlTwN.js +147 -0
- package/dist/web/assets/{index-DaMLk_sl.css → index-mkDvqMQ3.css} +1 -1
- package/dist/web/assets/{info-OHQRW6UA-K8BF1xZb.js → info-OHQRW6UA-CTSCkjt3.js} +1 -1
- package/dist/web/assets/{infoDiagram-MVDRKAX5-QaWV5-SY.js → infoDiagram-MVDRKAX5-D46MTesD.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-HPEHV2GE-DpVz_sC0.js → ishikawaDiagram-HPEHV2GE-Bafu9NOk.js} +1 -1
- package/dist/web/assets/{journeyDiagram-BOSHW7O7-S9MzozV7.js → journeyDiagram-BOSHW7O7-CRsIi1Ry.js} +1 -1
- package/dist/web/assets/{kanban-definition-FNSA4HC5-BiU8Ibw-.js → kanban-definition-FNSA4HC5-DjIT_uLs.js} +1 -1
- package/dist/web/assets/{line-B1KaX5ms.js → line-B5MDdgVs.js} +1 -1
- package/dist/web/assets/{linear-Cg1dwAtU.js → linear-wm7JWxlT.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-hzYHYxfI.js → mermaid-parser.core-C7jTW4sZ.js} +3 -3
- package/dist/web/assets/{mermaid.core-N8xYYvXy.js → mermaid.core-DcWRGjP5.js} +4 -4
- package/dist/web/assets/{mindmap-definition-6OY57UG3-BmIQCrEd.js → mindmap-definition-6OY57UG3-qY6vV0UI.js} +1 -1
- package/dist/web/assets/{packet-JDAUHWVQ-DhOTX9De.js → packet-JDAUHWVQ-9B7vLYK0.js} +1 -1
- package/dist/web/assets/{pegDiagram-ECIUJNBX-CVfE6yUu.js → pegDiagram-ECIUJNBX-B7gLZ_Fs.js} +1 -1
- package/dist/web/assets/{pie-XZMESJXO-BayfKVL8.js → pie-XZMESJXO-MfazZSiE.js} +1 -1
- package/dist/web/assets/{pieDiagram-GNMPY74T-ClbMRve3.js → pieDiagram-GNMPY74T-1ahagzzC.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-D57K4MC3-DFp-P_EZ.js → quadrantDiagram-D57K4MC3-C-GkwjOp.js} +1 -1
- package/dist/web/assets/{radar-ABXABTNO-Bt1EAsxC.js → radar-ABXABTNO-CgutvWTi.js} +1 -1
- package/dist/web/assets/{railroad-I3PHUGI6-CSos2Rm9.js → railroad-I3PHUGI6-Bl9LJINm.js} +1 -1
- package/dist/web/assets/{railroad-abnf-LNEFI6M7-CebY-3Kh.js → railroad-abnf-LNEFI6M7-CGRpAnuU.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-DpA6-TBs.js → railroad-ebnf-DWYD2UWJ-D6A47xZS.js} +1 -1
- package/dist/web/assets/{railroad-peg-JR7BVNR7-Bg3QPvi5.js → railroad-peg-JR7BVNR7-DxGphrFO.js} +1 -1
- package/dist/web/assets/{railroadDiagram-Y72NRBL7-BM4RAgqS.js → railroadDiagram-Y72NRBL7-D8P17OV4.js} +1 -1
- package/dist/web/assets/{requirementDiagram-EQYSW5RA-CqP9bCS_.js → requirementDiagram-EQYSW5RA-c93_xAaQ.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-SY24JHO5-BhS8f6-k.js → sankeyDiagram-SY24JHO5-FankqwcZ.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-BLEHNHQU-p3C1DgeC.js → sequenceDiagram-BLEHNHQU-CWH5SeL1.js} +1 -1
- package/dist/web/assets/{src-9XJAFibc.js → src-BXQOmVZv.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-A6IESKWZ-DVVy8chY.js → stateDiagram-v2-A6IESKWZ-DLfcw0e7.js} +1 -1
- package/dist/web/assets/{swimlanes-B3ENRKFQ-CtMPO6Gw.js → swimlanes-B3ENRKFQ-DZ-yBiap.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-AYQRV4AK-BlmdUBbW.js +8 -0
- package/dist/web/assets/{timeline-definition-JXYKGLGB-DuTqpjLQ.js → timeline-definition-JXYKGLGB-B7XvxDHY.js} +1 -1
- package/dist/web/assets/{treeView-D4JQ5SDB-qpOZEphu.js → treeView-D4JQ5SDB-DrB6wXJ0.js} +1 -1
- package/dist/web/assets/{treemap-SAJKECNS-Dm6e3TpG.js → treemap-SAJKECNS-BynFjsvx.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-WPOLBNNX-DoCSdm5y.js → usecaseDiagram-WPOLBNNX-BISzHOHN.js} +1 -1
- package/dist/web/assets/{vennDiagram-Q3Y3IDCW-qzd4yvh5.js → vennDiagram-Q3Y3IDCW-BZwjTIST.js} +1 -1
- package/dist/web/assets/{wardley-7MLQ67FV-CkcgIQgQ.js → wardley-7MLQ67FV-2bM0pMhO.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-IVAIDKWA-CE6OR9hA.js → wardleyDiagram-IVAIDKWA-CflkU8us.js} +1 -1
- package/dist/web/assets/{xychartDiagram-4URNHDU4-OrFXHVc6.js → xychartDiagram-4URNHDU4-DRspOlD3.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +5 -3
- package/dist/web/assets/channel-CDExoPVe.js +0 -1
- package/dist/web/assets/flowDiagram-IUGLLWBU-X85uzgrG.js +0 -1
- package/dist/web/assets/index-Cz3AOJOw.js +0 -142
- 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
|
+
}
|
package/dist/server/service.js
CHANGED
|
@@ -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();
|