@world-engines/project-host 0.1.0-alpha.0

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 (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,656 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { randomUUID } from "node:crypto";
7
+ import { createHash } from "node:crypto";
8
+ import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
9
+ import { validateLocalAuthorProjectManifest } from "@world-engines/project-format";
10
+ import { assertSafeExistingProjectPath, ensureSafeProjectDirectory, safeProjectPath } from "./safe-project-path.js";
11
+ import { readFrozenReviewTargetBinding } from "./frozen-review-local-pipeline.js";
12
+ import { captureScenarioGitSource, applyScenarioGitSource } from "./project-git-scenario-source.js";
13
+ import { captureViewGitSource, applyViewGitSource } from "./project-git-view-source.js";
14
+ import { recoverGitSourceTree, replaceGitSourceTree } from "./project-git-capture-recovery.js";
15
+ import { acquireProjectWriterLease } from "./project-writer-lease.js";
16
+ const execFileAsync = promisify(execFile);
17
+ const PROJECT_GIT_CONTROL_DIRECTORY = ".worldengine/project-git";
18
+ export const PROJECT_GIT_MANIFEST_PATH = `${PROJECT_GIT_CONTROL_DIRECTORY}/manifest.json`;
19
+ const PROJECT_GIT_HTTP_MAX_BODY_BYTES = 96 * 1024 * 1024;
20
+ const STRICT_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
21
+ const GIT_OBJECT_ID = /^[a-f0-9]{40,64}$/;
22
+ export const PROJECT_GIT_REPOSITORY_KINDS = ["map", "database", "view"];
23
+ function fail(code, message) {
24
+ throw new AuthoringBridgeError(code, message);
25
+ }
26
+ function exactObject(value, keys, field) {
27
+ if (typeof value !== "object" || value === null || Array.isArray(value))
28
+ fail("E_SCHEMA_INVALID", `${field} 无效`);
29
+ const actual = Object.keys(value);
30
+ if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
31
+ fail("E_SCHEMA_INVALID", `${field} 字段无效`);
32
+ }
33
+ function strictSegment(value, field) {
34
+ if (typeof value !== "string" || !STRICT_SEGMENT.test(value))
35
+ fail("E_SCHEMA_INVALID", `${field} 无效`);
36
+ }
37
+ function expectedRemotePath(scenarioId, kind) {
38
+ return `/v1/project-git/${scenarioId}/${kind}.git`;
39
+ }
40
+ export function validateProjectGitManifest(value, expected) {
41
+ if (typeof value !== "object" || value === null || Array.isArray(value))
42
+ fail("E_SCHEMA_INVALID", "project git manifest response 无效");
43
+ const response = value;
44
+ if (!Number.isSafeInteger(response.revision) || response.revision < 0)
45
+ fail("E_SCHEMA_INVALID", "project git manifest revision 无效");
46
+ strictSegment(response.scenario_id, "scenario_id");
47
+ if (typeof response.manifest !== "object" || response.manifest === null || Array.isArray(response.manifest))
48
+ fail("E_SCHEMA_INVALID", "project git manifest body 无效");
49
+ const manifest = response.manifest;
50
+ exactObject(manifest, ["schema_version", "scenario_id", "metadata", "repositories"], "project git manifest body");
51
+ if (manifest.schema_version !== 1 || manifest.scenario_id !== response.scenario_id)
52
+ fail("E_SCHEMA_INVALID", "project git manifest identity 无效");
53
+ strictSegment(manifest.scenario_id, "manifest.scenario_id");
54
+ exactObject(manifest.metadata, ["title", "description"], "manifest.metadata");
55
+ if (typeof manifest.metadata.title !== "string" || typeof manifest.metadata.description !== "string")
56
+ fail("E_SCHEMA_INVALID", "manifest.metadata 无效");
57
+ exactObject(manifest.repositories, PROJECT_GIT_REPOSITORY_KINDS, "manifest.repositories");
58
+ for (const kind of PROJECT_GIT_REPOSITORY_KINDS) {
59
+ const repository = manifest.repositories[kind];
60
+ exactObject(repository, ["kind", "remote_path", "commit"], `manifest.repositories.${kind}`);
61
+ if (repository.kind !== kind || repository.remote_path !== expectedRemotePath(String(response.scenario_id), kind)
62
+ || (repository.commit !== null && (typeof repository.commit !== "string" || !/^[a-f0-9]{40}$/.test(repository.commit)))) {
63
+ fail("E_SCHEMA_INVALID", `manifest.repositories.${kind} 无效`);
64
+ }
65
+ }
66
+ const parsed = value;
67
+ if (expected !== undefined && parsed.scenario_id !== expected.scenario_id) {
68
+ fail("E_REMOTE_FORBIDDEN", "远端 manifest identity 与当前作者项目不匹配");
69
+ }
70
+ return parsed;
71
+ }
72
+ function validateProjectGitAccess(value, expected) {
73
+ if (typeof value !== "object" || value === null || Array.isArray(value))
74
+ fail("E_SCHEMA_INVALID", "project git collaboration response 无效");
75
+ const access = value.access;
76
+ if (typeof access !== "object" || access === null || Array.isArray(access))
77
+ fail("E_SCHEMA_INVALID", "project git access 无效");
78
+ const record = access;
79
+ if (record.scenario_id !== expected.scenario_id || typeof record.is_member !== "boolean" || typeof record.manifest_read !== "boolean" || typeof record.view_source_available !== "boolean")
80
+ fail("E_REMOTE_FORBIDDEN", "project git access identity 无效");
81
+ if (typeof record.permissions !== "object" || record.permissions === null || Array.isArray(record.permissions))
82
+ fail("E_SCHEMA_INVALID", "project git permissions 无效");
83
+ const permissions = record.permissions;
84
+ const fields = ["map_read", "map_write", "database_read", "database_write", "view_read", "view_write", "metadata_write"];
85
+ if (!fields.every((field) => typeof permissions[field] === "boolean"))
86
+ fail("E_SCHEMA_INVALID", "project git permissions 无效");
87
+ return record;
88
+ }
89
+ export function projectGitTransport(transport) {
90
+ return {
91
+ async request(request) {
92
+ const bodyBytes = request.body instanceof Uint8Array
93
+ ? request.body
94
+ : request.body === undefined
95
+ ? undefined
96
+ : Buffer.from(JSON.stringify(request.body), "utf8");
97
+ return await transport.request({
98
+ project_id: request.project_id,
99
+ method: request.method,
100
+ path: request.path,
101
+ ...(request.content_type === undefined ? {} : { content_type: request.content_type }),
102
+ ...(request.body === undefined ? {} : { body: request.body }),
103
+ ...(bodyBytes === undefined ? {} : {
104
+ body_digest: createHash("sha256").update(bodyBytes).digest("hex"),
105
+ body_size: bodyBytes.byteLength,
106
+ }),
107
+ });
108
+ },
109
+ };
110
+ }
111
+ async function readProjectIdentity(projectRoot) {
112
+ const path = await assertSafeExistingProjectPath(projectRoot, join(projectRoot, "worldengine.project.json"));
113
+ let manifest;
114
+ try {
115
+ manifest = validateLocalAuthorProjectManifest(JSON.parse(await readFile(path, "utf8")));
116
+ }
117
+ catch {
118
+ fail("E_SCHEMA_INVALID", "本地 author project manifest 无效");
119
+ }
120
+ strictSegment(manifest.project_id, "project_id");
121
+ const binding = await readFrozenReviewTargetBinding(projectRoot, manifest.project_id);
122
+ strictSegment(binding.scenario_id, "review-target.scenario_id");
123
+ return { project_id: manifest.project_id, scenario_id: binding.scenario_id };
124
+ }
125
+ function remoteManifestPath(scenarioId) {
126
+ return `/v1/creator/scenarios/${encodeURIComponent(scenarioId)}/project-manifest`;
127
+ }
128
+ function remoteAccessPath(scenarioId) {
129
+ return `/v1/creator/scenarios/${encodeURIComponent(scenarioId)}/collaboration`;
130
+ }
131
+ function remoteGitPath(scenarioId, kind, suffix) {
132
+ return `/v1/project-git/${encodeURIComponent(scenarioId)}/${kind}.git${suffix}`;
133
+ }
134
+ function bodyObject(response) {
135
+ if (response.status === 401)
136
+ fail("E_AUTH_REQUIRED", "project git 需要当前项目登录");
137
+ if (response.status === 403)
138
+ fail("E_REMOTE_FORBIDDEN", "project git ACL 拒绝当前操作");
139
+ if (response.status < 200 || response.status >= 300)
140
+ fail("E_OPERATION_UNCERTAIN", `project git 远端返回 ${response.status}`);
141
+ return response.body;
142
+ }
143
+ function assertKind(value) {
144
+ if (!PROJECT_GIT_REPOSITORY_KINDS.includes(value))
145
+ fail("E_SCHEMA_INVALID", "repository kind 仅允许 map/database/view");
146
+ return value;
147
+ }
148
+ function assertSourceFilePath(path) {
149
+ if (path.length === 0 || /[\\:\x00-\x1f<>"|?*]/.test(path)
150
+ || path.split("/").some((part) => !part || part === "." || part === ".." || [".git", ".worldengine"].includes(part.toLowerCase()) || /[. ]$/.test(part) || /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/i.test(part))) {
151
+ fail("E_PATH_OUTSIDE_PROJECT", "Git source 文件路径无效");
152
+ }
153
+ }
154
+ function checkGrant(access, kind, operation) {
155
+ if (!access.is_member || !access.manifest_read)
156
+ fail("E_REMOTE_FORBIDDEN", "当前身份不是项目成员或无 manifest 读取权限");
157
+ if (kind === "view" && !access.view_source_available)
158
+ fail("E_REMOTE_FORBIDDEN", "购买或外部 view 只提供运行包,禁止取得卖家源码");
159
+ const canRead = access.permissions[`${kind}_read`];
160
+ const canWrite = access.permissions[`${kind}_write`];
161
+ if ((operation === "read" && !canRead) || (operation === "write" && (!canRead || !canWrite))) {
162
+ fail("E_REMOTE_FORBIDDEN", `project git ${operation} ACL 拒绝当前操作`);
163
+ }
164
+ }
165
+ async function collectRequestBody(request) {
166
+ if (request.method === "GET" || request.method === "HEAD")
167
+ return undefined;
168
+ const length = Number(request.headers["content-length"] ?? 0);
169
+ if (!Number.isSafeInteger(length) || length < 0 || length > PROJECT_GIT_HTTP_MAX_BODY_BYTES)
170
+ fail("E_VALIDATION_FAILED", "git HTTP 请求过大");
171
+ const chunks = [];
172
+ let total = 0;
173
+ for await (const raw of request) {
174
+ const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
175
+ total += chunk.byteLength;
176
+ if (total > PROJECT_GIT_HTTP_MAX_BODY_BYTES)
177
+ fail("E_VALIDATION_FAILED", "git HTTP 请求过大");
178
+ chunks.push(chunk);
179
+ }
180
+ return Buffer.concat(chunks);
181
+ }
182
+ function relayRoute(url, relayPath) {
183
+ if (url === undefined || url.includes("#"))
184
+ fail("E_VALIDATION_FAILED", "git HTTP route 无效");
185
+ const parsed = new URL(url, "http://127.0.0.1");
186
+ if (parsed.pathname === `${relayPath}/info/refs`) {
187
+ const service = parsed.searchParams.get("service");
188
+ if (parsed.searchParams.size !== 1 || (service !== "git-upload-pack" && service !== "git-receive-pack"))
189
+ fail("E_VALIDATION_FAILED", "git smart HTTP service 无效");
190
+ return { suffix: `/info/refs?service=${service}`, contentType: undefined };
191
+ }
192
+ if (parsed.search !== "" || ![`${relayPath}/git-upload-pack`, `${relayPath}/git-receive-pack`].includes(parsed.pathname)) {
193
+ fail("E_VALIDATION_FAILED", "仅允许 git smart HTTP endpoint");
194
+ }
195
+ return { suffix: parsed.pathname.slice(relayPath.length), contentType: undefined };
196
+ }
197
+ function decodeGitEnvelope(value) {
198
+ exactObject(value, ["schema_version", "content_type", "body_base64", ...(typeof value?.cache_control === "string" ? ["cache_control"] : [])], "project git HTTP response");
199
+ if (value.schema_version !== 1 || typeof value.content_type !== "string" || !/^application\/x-git-(?:upload|receive)-pack-(?:advertisement|result)$/.test(value.content_type) || typeof value.body_base64 !== "string") {
200
+ fail("E_VALIDATION_FAILED", "project git HTTP response 无效");
201
+ }
202
+ if (value.cache_control !== undefined && (typeof value.cache_control !== "string" || value.cache_control.length > 256 || /[\r\n]/.test(value.cache_control))) {
203
+ fail("E_VALIDATION_FAILED", "project git HTTP cache_control 无效");
204
+ }
205
+ const bytes = Buffer.from(value.body_base64, "base64");
206
+ if (bytes.byteLength > PROJECT_GIT_HTTP_MAX_BODY_BYTES || bytes.toString("base64").replace(/=+$/, "") !== value.body_base64.replace(/=+$/, "")) {
207
+ fail("E_VALIDATION_FAILED", "project git HTTP body_base64 无效");
208
+ }
209
+ return { ...value, bytes };
210
+ }
211
+ async function sendRelayResponse(response, request, transportResponse) {
212
+ if (transportResponse.status < 200 || transportResponse.status >= 300) {
213
+ response.writeHead(transportResponse.status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
214
+ response.end("project git request rejected\n");
215
+ return;
216
+ }
217
+ const envelope = decodeGitEnvelope(transportResponse.body);
218
+ response.writeHead(transportResponse.status, {
219
+ "content-type": envelope.content_type,
220
+ "content-length": String(envelope.bytes.byteLength),
221
+ "cache-control": envelope.cache_control ?? "no-cache",
222
+ "x-content-type-options": "nosniff",
223
+ connection: "close",
224
+ });
225
+ if (request.method === "HEAD")
226
+ response.end();
227
+ else
228
+ response.end(envelope.bytes);
229
+ }
230
+ async function closeServer(server) {
231
+ await new Promise((resolvePromise, rejectPromise) => server.close((error) => error === undefined ? resolvePromise() : rejectPromise(error)));
232
+ }
233
+ async function startGitRelay(identity, kind, transport) {
234
+ // loopback relay 是一次 Git 操作的短期能力;随机路径避免同机其他进程仅凭端口复用 broker 出口。
235
+ const relayPath = `/project-git-${randomUUID()}/repository.git`;
236
+ const server = createServer(async (request, response) => {
237
+ try {
238
+ if (request.headers.authorization !== undefined || request.headers.cookie !== undefined)
239
+ fail("E_AUTH_REQUIRED", "loopback git 不接受客户端凭据");
240
+ if (request.method !== "GET" && request.method !== "POST" && request.method !== "HEAD")
241
+ fail("E_VALIDATION_FAILED", "git smart HTTP method 无效");
242
+ const route = relayRoute(request.url, relayPath);
243
+ const expectedContentType = route.suffix === "/git-upload-pack"
244
+ ? "application/x-git-upload-pack-request"
245
+ : route.suffix === "/git-receive-pack"
246
+ ? "application/x-git-receive-pack-request"
247
+ : undefined;
248
+ const receivedContentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
249
+ if (expectedContentType !== receivedContentType) {
250
+ if (expectedContentType !== undefined || receivedContentType !== undefined)
251
+ fail("E_VALIDATION_FAILED", "git smart HTTP content-type 无效");
252
+ }
253
+ const body = await collectRequestBody(request);
254
+ await sendRelayResponse(response, request, await transport.request({
255
+ project_id: identity.project_id,
256
+ method: request.method === "HEAD" ? "GET" : request.method,
257
+ path: remoteGitPath(identity.scenario_id, kind, route.suffix),
258
+ ...(expectedContentType === undefined ? {} : { content_type: expectedContentType }),
259
+ ...(body === undefined ? {} : { body }),
260
+ }));
261
+ }
262
+ catch {
263
+ if (!response.headersSent)
264
+ response.writeHead(502, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
265
+ response.end("project git relay failed\n");
266
+ }
267
+ });
268
+ await new Promise((resolvePromise, rejectPromise) => {
269
+ server.once("error", rejectPromise);
270
+ server.listen(0, "127.0.0.1", resolvePromise);
271
+ });
272
+ const address = server.address();
273
+ if (address === null || typeof address === "string") {
274
+ await closeServer(server);
275
+ fail("E_OPERATION_UNCERTAIN", "project git loopback relay 无法启动");
276
+ }
277
+ return Object.freeze({ url: `http://127.0.0.1:${address.port}${relayPath}`, close: () => closeServer(server) });
278
+ }
279
+ async function runGit(args, cwd) {
280
+ try {
281
+ const environment = { ...process.env };
282
+ for (const name of [
283
+ "WORLDENGINE_PROJECT_TRANSFER_PIPE",
284
+ "WORLDENGINE_PROJECT_TRANSFER_CHALLENGE_ID",
285
+ "WORLDENGINE_PROJECT_TRANSFER_CHALLENGE",
286
+ "WORLDENGINE_PROJECT_TRANSFER_EXPIRES_AT_MS",
287
+ ])
288
+ delete environment[name];
289
+ const { stdout } = await execFileAsync("git", [...args], {
290
+ ...(cwd === undefined ? {} : { cwd }),
291
+ windowsHide: true,
292
+ timeout: 5 * 60_000,
293
+ maxBuffer: 8 * 1024 * 1024,
294
+ env: {
295
+ ...environment,
296
+ GIT_TERMINAL_PROMPT: "0",
297
+ GCM_INTERACTIVE: "Never",
298
+ NO_PROXY: [process.env.NO_PROXY, "127.0.0.1", "localhost"].filter(Boolean).join(","),
299
+ },
300
+ });
301
+ return stdout.trim();
302
+ }
303
+ catch {
304
+ fail("E_OPERATION_UNCERTAIN", "系统 git 执行失败");
305
+ }
306
+ }
307
+ function gitBaseArgs(hooksDirectory) {
308
+ return ["-c", "credential.helper=", "-c", "http.extraHeader=", "-c", "http.proxy=", "-c", "http.followRedirects=false", "-c", `core.hooksPath=${hooksDirectory}`];
309
+ }
310
+ /** 在启动网络命令前解析 Git URL rewrite,拒绝任何配置把出口改到 relay 之外。 */
311
+ async function assertRelayTarget(url, cwd, push = false) {
312
+ const resolved = await runGit(["ls-remote", "--get-url", url], cwd);
313
+ if (resolved !== url) {
314
+ fail("E_REMOTE_FORBIDDEN", "Git URL rewrite 不能更改受控 relay 目标");
315
+ }
316
+ if (push) {
317
+ let rules;
318
+ try {
319
+ rules = (await execFileAsync("git", ["config", "--null", "--get-regexp", "^url\\..*\\.pushinsteadof$"], { cwd, windowsHide: true, timeout: 10_000, maxBuffer: 1024 * 1024 })).stdout;
320
+ }
321
+ catch (error) {
322
+ if (error.code === 1)
323
+ return;
324
+ fail("E_OPERATION_UNCERTAIN", "无法验证 Git push URL rewrite");
325
+ }
326
+ for (const rule of rules.split("\0")) {
327
+ const separator = rule.indexOf("\n");
328
+ if (separator >= 0 && url.startsWith(rule.slice(separator + 1))) {
329
+ fail("E_REMOTE_FORBIDDEN", "Git URL rewrite 不能更改受控 relay 目标");
330
+ }
331
+ }
332
+ }
333
+ }
334
+ function semanticRemote(identity, kind) {
335
+ return `worldengine://${identity.scenario_id}/${kind}`;
336
+ }
337
+ async function existingRepository(projectRoot, target) {
338
+ const canonical = await assertSafeExistingProjectPath(projectRoot, target);
339
+ const dotGit = await assertSafeExistingProjectPath(projectRoot, join(canonical, ".git"));
340
+ if (!(await lstat(dotGit)).isDirectory())
341
+ fail("E_VALIDATION_FAILED", "只允许独立仓库,拒绝父仓或 worktree .git");
342
+ const top = await runGit(["-C", canonical, "rev-parse", "--show-toplevel"]);
343
+ const canonicalTop = await realpath(top);
344
+ if (process.platform === "win32" ? canonicalTop.toLowerCase() !== canonical.toLowerCase() : canonicalTop !== canonical) {
345
+ fail("E_VALIDATION_FAILED", "仓库根与固定 project git 子目录不一致");
346
+ }
347
+ return canonical;
348
+ }
349
+ export class ProjectGitClient {
350
+ projectRoot;
351
+ identity;
352
+ transport;
353
+ constructor(projectRoot, identity, transport) {
354
+ this.projectRoot = projectRoot;
355
+ this.identity = identity;
356
+ this.transport = transport;
357
+ }
358
+ static async open(projectRoot, transport) {
359
+ if (transport === undefined)
360
+ fail("E_AUTH_REQUIRED", "project git 需要 ProjectAuthBroker");
361
+ const canonicalRoot = await realpath(resolve(projectRoot));
362
+ return new ProjectGitClient(canonicalRoot, await readProjectIdentity(canonicalRoot), transport);
363
+ }
364
+ async readManifest() {
365
+ const response = await this.transport.request({ project_id: this.identity.project_id, method: "GET", path: remoteManifestPath(this.identity.scenario_id) });
366
+ return validateProjectGitManifest(bodyObject(response), this.identity);
367
+ }
368
+ async readAccess() {
369
+ const response = await this.transport.request({ project_id: this.identity.project_id, method: "GET", path: remoteAccessPath(this.identity.scenario_id) });
370
+ return validateProjectGitAccess(bodyObject(response), this.identity);
371
+ }
372
+ captureSource(kind) {
373
+ return kind === "view" ? captureViewGitSource(this.projectRoot) : captureScenarioGitSource(this.projectRoot, kind);
374
+ }
375
+ /** Native 在命令后只读核对同一实际作者源,避免复制 map/database/view 摘要算法。 */
376
+ async inspect(kindValue) {
377
+ const kind = assertKind(kindValue);
378
+ checkGrant(await this.readAccess(), kind, "read");
379
+ return this.sourceResult("source_inspected", kind, await this.captureSource(kind));
380
+ }
381
+ async withSourceLease(kind, work) {
382
+ // 旁仓单独的 lease,不嵌套占用真实作者源 writer;Windows helper 随宿主管道
383
+ // 关闭按 file identity 清理,避免自制 PID/rename stale-lock 竞争误删后来者。
384
+ const scope = await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/source-leases/${kind}`);
385
+ const lock = await acquireProjectWriterLease(scope, { operation: "project_git_source", repository: kind });
386
+ try {
387
+ await recoverGitSourceTree(this.projectRoot, kind);
388
+ return await work();
389
+ }
390
+ finally {
391
+ await lock.release();
392
+ }
393
+ }
394
+ async assertCleanRepository(target) {
395
+ // 包括 ignored 文件,避免导出替换 source 目录时带走未知本地字节。
396
+ if (await runGit(["-C", target, "status", "--porcelain=v1", "--untracked-files=all", "--ignored"])) {
397
+ fail("E_VALIDATION_FAILED", "Git 仓库有未提交或忽略文件;请先保存、提交或自行处理,系统不会覆盖");
398
+ }
399
+ }
400
+ async saveSourcePreimage(kind, capture) {
401
+ const control = await ensureSafeProjectDirectory(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY);
402
+ const target = await safeProjectPath(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/${kind}-source-preimage.json`);
403
+ const temporary = join(control, `.source-preimage-${randomUUID()}.tmp`);
404
+ await writeFile(temporary, JSON.stringify({ schema_version: 1, project_id: this.identity.project_id, scenario_id: this.identity.scenario_id, repository: kind,
405
+ digest: capture.digest, project_revision: capture.project_revision, source_sha256: capture.source_sha256,
406
+ files: Object.fromEntries(Object.entries(capture.files).map(([path, bytes]) => [path, Buffer.from(bytes).toString("base64")])) }), { flag: "wx", mode: 0o600 });
407
+ await rename(temporary, target);
408
+ }
409
+ /** 导出真实作者源,不触碰 index;作者仍使用原始 Git add/commit/merge。 */
410
+ async capture(kindValue) {
411
+ const kind = assertKind(kindValue);
412
+ checkGrant(await this.readAccess(), kind, "write");
413
+ return this.withSourceLease(kind, async () => {
414
+ const repoRelative = `${PROJECT_GIT_CONTROL_DIRECTORY}/repositories/${kind}`;
415
+ await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/repositories`);
416
+ const repoPath = await safeProjectPath(this.projectRoot, repoRelative);
417
+ try {
418
+ await lstat(repoPath);
419
+ }
420
+ catch (error) {
421
+ if (error.code !== "ENOENT")
422
+ throw error;
423
+ await ensureSafeProjectDirectory(this.projectRoot, repoRelative);
424
+ const hooks = await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/hooks-disabled`);
425
+ await runGit([...gitBaseArgs(hooks), "init", "--initial-branch=main", "--", repoPath]);
426
+ await runGit(["-C", repoPath, "remote", "add", "origin", semanticRemote(this.identity, kind)]);
427
+ }
428
+ const target = await existingRepository(this.projectRoot, repoPath);
429
+ await this.assertCleanRepository(target);
430
+ const capture = await this.captureSource(kind);
431
+ const stageRelative = `${PROJECT_GIT_CONTROL_DIRECTORY}/capture-${randomUUID()}`;
432
+ const stage = await ensureSafeProjectDirectory(this.projectRoot, stageRelative);
433
+ for (const [path, bytes] of Object.entries(capture.files)) {
434
+ assertSourceFilePath(path);
435
+ await ensureSafeProjectDirectory(this.projectRoot, `${stageRelative}/${dirname(path) === "." ? "" : dirname(path)}`);
436
+ await writeFile(join(stage, path), bytes, { flag: "wx" });
437
+ }
438
+ const current = await this.captureSource(kind);
439
+ if (current.project_revision !== capture.project_revision || current.source_sha256 !== capture.source_sha256 || current.digest !== capture.digest)
440
+ fail("E_VALIDATION_FAILED", "导出期间作者源变化,请重试;暂存预影已保留");
441
+ await this.assertCleanRepository(target);
442
+ // Web 与 npm 共用仓根布局;持久 journal 保留崩溃后的精确恢复预影。
443
+ await replaceGitSourceTree(this.projectRoot, target, stage, kind);
444
+ await this.saveSourcePreimage(kind, capture);
445
+ return this.sourceResult("captured", kind, capture);
446
+ });
447
+ }
448
+ /** 在 fetch/merge 之前固定本地预影;后续本地编辑必须导致 apply 拒绝。 */
449
+ async prepareApply(kindValue) {
450
+ const kind = assertKind(kindValue);
451
+ checkGrant(await this.readAccess(), kind, "read");
452
+ return this.withSourceLease(kind, async () => {
453
+ const target = await existingRepository(this.projectRoot, join(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY, "repositories", kind));
454
+ await this.assertCleanRepository(target);
455
+ const capture = await this.captureSource(kind);
456
+ await this.saveSourcePreimage(kind, capture);
457
+ return this.sourceResult("apply_prepared", kind, capture);
458
+ });
459
+ }
460
+ async apply(kindValue) {
461
+ const kind = assertKind(kindValue);
462
+ checkGrant(await this.readAccess(), kind, "read");
463
+ return this.withSourceLease(kind, async () => {
464
+ const target = await existingRepository(this.projectRoot, join(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY, "repositories", kind));
465
+ await this.assertCleanRepository(target);
466
+ const path = await assertSafeExistingProjectPath(this.projectRoot, join(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY, `${kind}-source-preimage.json`));
467
+ const raw = await readFile(path);
468
+ if (raw.length > PROJECT_GIT_HTTP_MAX_BODY_BYTES)
469
+ fail("E_SCHEMA_INVALID", "Git source 预影过大");
470
+ const saved = JSON.parse(raw.toString("utf8"));
471
+ if (saved.schema_version !== 1 || saved.project_id !== this.identity.project_id || saved.scenario_id !== this.identity.scenario_id || saved.repository !== kind
472
+ || !Number.isSafeInteger(saved.project_revision) || saved.project_revision < 0 || !/^[a-f0-9]{64}$/.test(saved.digest) || !/^[a-f0-9]{64}$/.test(saved.source_sha256)
473
+ || typeof saved.files !== "object" || saved.files === null || Array.isArray(saved.files))
474
+ fail("E_SCHEMA_INVALID", "Git source 预影 identity 无效");
475
+ const expected = { ...saved, files: Object.fromEntries(Object.entries(saved.files).map(([name, value]) => {
476
+ assertSourceFilePath(name);
477
+ if (typeof value !== "string" || Buffer.from(value, "base64").toString("base64") !== value)
478
+ fail("E_SCHEMA_INVALID", "Git source 预影 bytes 无效");
479
+ return [name, new Uint8Array(Buffer.from(value, "base64"))];
480
+ })) };
481
+ const head = await runGit(["-C", target, "rev-parse", "HEAD"]);
482
+ if (!GIT_OBJECT_ID.test(head))
483
+ fail("E_VALIDATION_FAILED", "Git HEAD 无效");
484
+ const tree = await runGit(["-C", target, "ls-tree", "-rz", head]);
485
+ const files = Object.create(null);
486
+ const caseFoldedPaths = new Set();
487
+ let size = 0;
488
+ for (const entry of tree.split("\0").filter(Boolean)) {
489
+ const match = /^(100644|100755) blob ([a-f0-9]{40,64})\t(.+)$/s.exec(entry);
490
+ if (!match)
491
+ fail("E_VALIDATION_FAILED", "Git source 只允许普通 blob,禁止 symlink/submodule");
492
+ const name = match[3];
493
+ assertSourceFilePath(name);
494
+ const folded = name.toLowerCase();
495
+ if (caseFoldedPaths.has(folded))
496
+ fail("E_PATH_OUTSIDE_PROJECT", "Git source 包含大小写冲突路径");
497
+ caseFoldedPaths.add(folded);
498
+ const { stdout } = await execFileAsync("git", ["-C", target, "cat-file", "blob", match[2]], { encoding: "buffer", windowsHide: true, timeout: 10_000, maxBuffer: PROJECT_GIT_HTTP_MAX_BODY_BYTES });
499
+ size += stdout.length;
500
+ if (size > PROJECT_GIT_HTTP_MAX_BODY_BYTES)
501
+ fail("E_VALIDATION_FAILED", "Git source tree 过大");
502
+ files[name] = new Uint8Array(stdout);
503
+ }
504
+ if (Object.keys(files).length === 0)
505
+ fail("E_VALIDATION_FAILED", "Git 提交缺少作者源文件");
506
+ await this.assertCleanRepository(target);
507
+ if (await runGit(["-C", target, "rev-parse", "HEAD"]) !== head)
508
+ fail("E_VALIDATION_FAILED", "Git HEAD 在应用期间变化");
509
+ if (kind === "view")
510
+ await applyViewGitSource(this.projectRoot, files, expected);
511
+ else
512
+ await applyScenarioGitSource(this.projectRoot, kind, files, expected);
513
+ const current = await this.captureSource(kind);
514
+ await this.saveSourcePreimage(kind, current);
515
+ return this.sourceResult("applied", kind, current);
516
+ });
517
+ }
518
+ sourceResult(status, kind, capture) {
519
+ return { status, repository: kind, local_path: `${PROJECT_GIT_CONTROL_DIRECTORY}/repositories/${kind}`, project_revision: capture.project_revision, source_digest: capture.digest };
520
+ }
521
+ async pullManifest() {
522
+ const manifest = await this.readManifest();
523
+ const control = await ensureSafeProjectDirectory(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY);
524
+ const target = await safeProjectPath(this.projectRoot, PROJECT_GIT_MANIFEST_PATH);
525
+ const temporary = join(control, `.manifest-${randomUUID()}.tmp`);
526
+ try {
527
+ await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
528
+ await rename(temporary, target);
529
+ }
530
+ finally {
531
+ await rm(temporary, { force: true });
532
+ }
533
+ return manifest;
534
+ }
535
+ async pushManifest() {
536
+ const path = await assertSafeExistingProjectPath(this.projectRoot, join(this.projectRoot, PROJECT_GIT_MANIFEST_PATH));
537
+ if (!(await lstat(path)).isFile())
538
+ fail("E_SCHEMA_INVALID", "project git manifest cache 不是普通文件");
539
+ const bytes = await readFile(path);
540
+ if (bytes.byteLength > 256 * 1024)
541
+ fail("E_SCHEMA_INVALID", "project git manifest 过大");
542
+ let candidate;
543
+ try {
544
+ candidate = validateProjectGitManifest(JSON.parse(bytes.toString("utf8")), this.identity);
545
+ }
546
+ catch (error) {
547
+ if (error instanceof AuthoringBridgeError)
548
+ throw error;
549
+ fail("E_SCHEMA_INVALID", "project git manifest JSON 无效");
550
+ }
551
+ const [current, access] = await Promise.all([this.readManifest(), this.readAccess()]);
552
+ if (!access.permissions.metadata_write)
553
+ fail("E_REMOTE_FORBIDDEN", "当前 ACL 没有 manifest 写权限");
554
+ if (current.revision !== candidate.revision)
555
+ fail("E_VALIDATION_FAILED", "manifest revision 已变化,请先 pull");
556
+ const response = await this.transport.request({
557
+ project_id: this.identity.project_id,
558
+ method: "PUT",
559
+ path: remoteManifestPath(this.identity.scenario_id),
560
+ body: { expected_revision: current.revision, manifest: candidate.manifest },
561
+ });
562
+ const written = validateProjectGitManifest(bodyObject(response), this.identity);
563
+ if (written.revision <= current.revision)
564
+ fail("E_VALIDATION_FAILED", "manifest 写入未返回递增 revision");
565
+ return this.pullManifestFromValue(written);
566
+ }
567
+ async pullManifestFromValue(manifest) {
568
+ const control = await ensureSafeProjectDirectory(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY);
569
+ const target = await safeProjectPath(this.projectRoot, PROJECT_GIT_MANIFEST_PATH);
570
+ const temporary = join(control, `.manifest-${randomUUID()}.tmp`);
571
+ try {
572
+ await writeFile(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
573
+ await rename(temporary, target);
574
+ }
575
+ finally {
576
+ await rm(temporary, { force: true });
577
+ }
578
+ return manifest;
579
+ }
580
+ async clone(kindValue) {
581
+ const kind = assertKind(kindValue);
582
+ const [manifest, access] = await Promise.all([this.readManifest(), this.readAccess()]);
583
+ checkGrant(access, kind, "read");
584
+ if (manifest.manifest.repositories[kind].remote_path !== expectedRemotePath(this.identity.scenario_id, kind))
585
+ fail("E_REMOTE_FORBIDDEN", "manifest remote path 与当前仓不一致");
586
+ const repositoriesRoot = await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/repositories`);
587
+ const hooks = await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/hooks-disabled`);
588
+ const target = await safeProjectPath(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/repositories/${kind}`);
589
+ try {
590
+ await lstat(target);
591
+ fail("E_VALIDATION_FAILED", `${kind} 仓库已存在`);
592
+ }
593
+ catch (error) {
594
+ if (error.code !== "ENOENT")
595
+ throw error;
596
+ }
597
+ const rel = relative(this.projectRoot, target);
598
+ if (rel === "" || rel === ".git" || rel.startsWith(`..${sep}`))
599
+ fail("E_PATH_OUTSIDE_PROJECT", "project git clone target 无效");
600
+ const relay = await startGitRelay(this.identity, kind, this.transport);
601
+ try {
602
+ await assertRelayTarget(relay.url, repositoriesRoot);
603
+ await runGit([...gitBaseArgs(hooks), "clone", "--origin", "origin", "--", relay.url, target], repositoriesRoot);
604
+ await runGit([...gitBaseArgs(hooks), "-C", target, "remote", "set-url", "origin", semanticRemote(this.identity, kind)]);
605
+ }
606
+ finally {
607
+ await relay.close();
608
+ }
609
+ return this.result("cloned", kind, await existingRepository(this.projectRoot, target));
610
+ }
611
+ async fetch(kindValue) {
612
+ const kind = assertKind(kindValue);
613
+ const [manifest, access] = await Promise.all([this.readManifest(), this.readAccess()]);
614
+ checkGrant(access, kind, "read");
615
+ if (manifest.manifest.repositories[kind].remote_path !== expectedRemotePath(this.identity.scenario_id, kind))
616
+ fail("E_REMOTE_FORBIDDEN", "manifest remote path 与当前仓不一致");
617
+ const hooks = await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/hooks-disabled`);
618
+ const target = await existingRepository(this.projectRoot, join(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY, "repositories", kind));
619
+ const relay = await startGitRelay(this.identity, kind, this.transport);
620
+ try {
621
+ await assertRelayTarget(relay.url, target);
622
+ await runGit([...gitBaseArgs(hooks), "-C", target, "fetch", "--prune", "--", relay.url, "+refs/heads/*:refs/remotes/origin/*"]);
623
+ }
624
+ finally {
625
+ await relay.close();
626
+ }
627
+ return this.result("fetched", kind, target);
628
+ }
629
+ async push(kindValue) {
630
+ const kind = assertKind(kindValue);
631
+ const [manifest, access] = await Promise.all([this.readManifest(), this.readAccess()]);
632
+ checkGrant(access, kind, "write");
633
+ if (manifest.manifest.repositories[kind].remote_path !== expectedRemotePath(this.identity.scenario_id, kind))
634
+ fail("E_REMOTE_FORBIDDEN", "manifest remote path 与当前仓不一致");
635
+ const hooks = await ensureSafeProjectDirectory(this.projectRoot, `${PROJECT_GIT_CONTROL_DIRECTORY}/hooks-disabled`);
636
+ const target = await existingRepository(this.projectRoot, join(this.projectRoot, PROJECT_GIT_CONTROL_DIRECTORY, "repositories", kind));
637
+ const branch = await runGit(["-C", target, "symbolic-ref", "--short", "HEAD"]);
638
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/.test(branch) || branch.includes("..") || branch.endsWith(".") || branch.endsWith("/"))
639
+ fail("E_VALIDATION_FAILED", "当前 Git branch 无效或 detached");
640
+ const relay = await startGitRelay(this.identity, kind, this.transport);
641
+ try {
642
+ await assertRelayTarget(relay.url, target, true);
643
+ await runGit([...gitBaseArgs(hooks), "-c", "push.recurseSubmodules=no", "-C", target, "push", "--", relay.url, `HEAD:refs/heads/${branch}`]);
644
+ }
645
+ finally {
646
+ await relay.close();
647
+ }
648
+ return this.result("pushed", kind, target);
649
+ }
650
+ async result(status, kind, target) {
651
+ const head = await runGit(["-C", target, "rev-parse", "HEAD"]);
652
+ if (!GIT_OBJECT_ID.test(head))
653
+ fail("E_VALIDATION_FAILED", "Git HEAD identity 无效");
654
+ return { status, repository: kind, local_path: `${PROJECT_GIT_CONTROL_DIRECTORY}/repositories/${kind}`, head };
655
+ }
656
+ }