@world-engines/create-project 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.
- package/LICENSE +46 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +77 -0
- package/dist/default-view/app-v3.d.ts +1 -0
- package/dist/default-view/app-v3.js +83 -0
- package/dist/default-view/app.d.ts +1 -0
- package/dist/default-view/app.js +122 -0
- package/dist/default-view/bridge.d.ts +1 -0
- package/dist/default-view/bridge.js +112 -0
- package/dist/default-view/config.d.ts +1 -0
- package/dist/default-view/config.js +8 -0
- package/dist/default-view/interaction-demo.d.ts +6 -0
- package/dist/default-view/interaction-demo.js +129 -0
- package/dist/default-view/panel.d.ts +1 -0
- package/dist/default-view/panel.js +311 -0
- package/dist/default-view/styles.d.ts +2 -0
- package/dist/default-view/styles.js +125 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/initialize.d.ts +52 -0
- package/dist/initialize.js +776 -0
- package/dist/npm-environment.d.ts +2 -0
- package/dist/npm-environment.js +11 -0
- package/dist/template.d.ts +8 -0
- package/dist/template.js +128 -0
- package/package.json +57 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { npmChildEnvironment } from "./npm-environment.js";
|
|
4
|
+
import { copyFile, cp, link, lstat, mkdir, readFile, readlink, readdir, realpath, rename, rm, stat, symlink, writeFile, } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
|
+
import { gunzipSync } from "node:zlib";
|
|
7
|
+
import { computeCanonicalProjectDigest, computeCanonicalFileTreeDigest, createEmptyScenarioPartWesp, sha256Hex, LOCAL_AUTHOR_GALLERY_PATHS, } from "@world-engines/project-format";
|
|
8
|
+
import { AGENT_KIT_ROOT_LINKS, materializeAgentKit } from "@world-engines/agent-kit";
|
|
9
|
+
import { ProjectInitializationError } from "./errors.js";
|
|
10
|
+
import { PROJECT_TEMPLATE_FILES } from "./template.js";
|
|
11
|
+
const TOOLCHAIN_VERSION = "0.1.0-alpha.0";
|
|
12
|
+
const EMPTY_DIGEST = "0".repeat(64);
|
|
13
|
+
const TOOLCHAIN_MANIFEST_FILE = "toolchain-manifest.json";
|
|
14
|
+
const PROJECT_DIRECTORIES = [
|
|
15
|
+
LOCAL_AUTHOR_GALLERY_PATHS.scenario,
|
|
16
|
+
LOCAL_AUTHOR_GALLERY_PATHS.view,
|
|
17
|
+
".agents",
|
|
18
|
+
".codex",
|
|
19
|
+
".claude",
|
|
20
|
+
".worldengine/lock",
|
|
21
|
+
".worldengine/auth",
|
|
22
|
+
".worldengine/webview",
|
|
23
|
+
".worldengine/projections/ladybug",
|
|
24
|
+
".worldengine/projections/trigger",
|
|
25
|
+
".worldengine/projections/spatial",
|
|
26
|
+
".worldengine/snapshots",
|
|
27
|
+
".worldengine/history",
|
|
28
|
+
".worldengine/staging",
|
|
29
|
+
".worldengine/operations",
|
|
30
|
+
".worldengine/cache",
|
|
31
|
+
".worldengine/logs",
|
|
32
|
+
".worldengine/tmp",
|
|
33
|
+
".worldengine/bin",
|
|
34
|
+
"docs/authoring",
|
|
35
|
+
];
|
|
36
|
+
function generatedId(prefix) {
|
|
37
|
+
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
38
|
+
}
|
|
39
|
+
function initializationIds(input) {
|
|
40
|
+
return input.ids ?? {
|
|
41
|
+
project_id: generatedId("project"),
|
|
42
|
+
local_scenario_id: generatedId("scenario"),
|
|
43
|
+
local_view_id: generatedId("view"),
|
|
44
|
+
snapshot_id: generatedId("snapshot"),
|
|
45
|
+
operation_id: generatedId("operation"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function inspectBootstrapFile(target, bootstrapFile) {
|
|
49
|
+
const path = resolve(bootstrapFile);
|
|
50
|
+
if (dirname(path).toLocaleLowerCase("en-US") !== target.toLocaleLowerCase("en-US")) {
|
|
51
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "启动 CMD 必须是初始化目标的直接子文件");
|
|
52
|
+
}
|
|
53
|
+
const info = await lstat(path).catch(() => {
|
|
54
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "声明的启动 CMD 不存在");
|
|
55
|
+
});
|
|
56
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
57
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "启动 CMD 必须是非链接普通文件");
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
path,
|
|
61
|
+
name: basename(path),
|
|
62
|
+
sha256: await sha256Hex(new Uint8Array(await readFile(path))),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function inspectTarget(target, bootstrapFile) {
|
|
66
|
+
try {
|
|
67
|
+
const targetStat = await stat(target);
|
|
68
|
+
if (!targetStat.isDirectory()) {
|
|
69
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "初始化目标必须是目录");
|
|
70
|
+
}
|
|
71
|
+
const entries = await readdir(target);
|
|
72
|
+
if (entries.length > 0) {
|
|
73
|
+
if (bootstrapFile !== undefined && entries.length === 1) {
|
|
74
|
+
const file = await inspectBootstrapFile(target, bootstrapFile);
|
|
75
|
+
if (entries[0]?.toLocaleLowerCase("en-US") === file.name.toLocaleLowerCase("en-US")) {
|
|
76
|
+
return { kind: "bootstrap", file };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw new ProjectInitializationError("E_TARGET_NOT_EMPTY", "初始化目标目录非空");
|
|
80
|
+
}
|
|
81
|
+
if (bootstrapFile !== undefined) {
|
|
82
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "声明的启动 CMD 不在初始化目标中");
|
|
83
|
+
}
|
|
84
|
+
return { kind: "empty" };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code === "ENOENT") {
|
|
88
|
+
if (bootstrapFile !== undefined) {
|
|
89
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "声明的启动 CMD 不存在");
|
|
90
|
+
}
|
|
91
|
+
return { kind: "missing" };
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function writeTemplate(root) {
|
|
97
|
+
for (const directory of PROJECT_DIRECTORIES) {
|
|
98
|
+
await mkdir(join(root, directory), { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
for (const file of PROJECT_TEMPLATE_FILES) {
|
|
101
|
+
const output = join(root, ...file.path.split("/"));
|
|
102
|
+
await mkdir(dirname(output), { recursive: true });
|
|
103
|
+
await writeFile(output, file.content, { encoding: "utf8", flag: "wx" });
|
|
104
|
+
}
|
|
105
|
+
const agentKit = await materializeAgentKit();
|
|
106
|
+
const manifestFile = agentKit.find(({ path }) => path === "manifest.json");
|
|
107
|
+
if (manifestFile === undefined) {
|
|
108
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", "AgentKit 缺少 manifest.json");
|
|
109
|
+
}
|
|
110
|
+
const manifest = JSON.parse(manifestFile.content);
|
|
111
|
+
const internalSkillNames = new Set(manifest.skills
|
|
112
|
+
.filter(({ source }) => source === "worldengine-chat-internal")
|
|
113
|
+
.map(({ name }) => name));
|
|
114
|
+
const authorManifest = {
|
|
115
|
+
...manifest,
|
|
116
|
+
skills: manifest.skills.filter(({ source }) => source !== "worldengine-chat-internal"),
|
|
117
|
+
};
|
|
118
|
+
for (const file of agentKit) {
|
|
119
|
+
if (file.path !== "manifest.json" &&
|
|
120
|
+
[...internalSkillNames].some((name) => file.path.startsWith(`skills-disabled/${name}/`)))
|
|
121
|
+
continue;
|
|
122
|
+
const output = join(root, "docs", "authoring", ...file.path.split("/"));
|
|
123
|
+
await mkdir(dirname(output), { recursive: true });
|
|
124
|
+
const content = file.path === "manifest.json"
|
|
125
|
+
? `${JSON.stringify(authorManifest, null, 2)}\n`
|
|
126
|
+
: file.content;
|
|
127
|
+
await writeFile(output, content, { encoding: "utf8", flag: "wx" });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function materializePortableRuntime(staging, sourceDirectory) {
|
|
131
|
+
const source = resolve(sourceDirectory);
|
|
132
|
+
const sourceInfo = await lstat(source).catch(() => {
|
|
133
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "便携 Node runtime 不存在");
|
|
134
|
+
});
|
|
135
|
+
if (!sourceInfo.isDirectory() || sourceInfo.isSymbolicLink()) {
|
|
136
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "便携 Node runtime 必须是非链接目录");
|
|
137
|
+
}
|
|
138
|
+
const node = join(source, "node.exe");
|
|
139
|
+
const npmCli = join(source, "node_modules", "npm", "bin", "npm-cli.js");
|
|
140
|
+
for (const [path, label] of [[node, "node.exe"], [npmCli, "npm CLI"]]) {
|
|
141
|
+
const info = await lstat(path).catch(() => {
|
|
142
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", `便携 Node runtime 缺少 ${label}`);
|
|
143
|
+
});
|
|
144
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
145
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", `便携 Node runtime ${label} 必须是普通文件`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const destination = join(staging, ".worldengine", "runtime", "node");
|
|
149
|
+
await mkdir(destination, { recursive: true });
|
|
150
|
+
await cp(source, destination, { recursive: true, force: false, verbatimSymlinks: true });
|
|
151
|
+
}
|
|
152
|
+
function tarOctal(bytes) {
|
|
153
|
+
const text = new TextDecoder().decode(bytes).replaceAll("\0", "").trim();
|
|
154
|
+
return text.length === 0 ? 0 : Number.parseInt(text, 8);
|
|
155
|
+
}
|
|
156
|
+
function tarString(bytes) {
|
|
157
|
+
return new TextDecoder().decode(bytes).replace(/\0.*$/s, "");
|
|
158
|
+
}
|
|
159
|
+
function readPackedPackageManifest(archive) {
|
|
160
|
+
const tar = gunzipSync(archive);
|
|
161
|
+
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
162
|
+
const header = tar.subarray(offset, offset + 512);
|
|
163
|
+
const name = tarString(header.subarray(0, 100));
|
|
164
|
+
const prefix = tarString(header.subarray(345, 500));
|
|
165
|
+
const path = prefix.length === 0 ? name : `${prefix}/${name}`;
|
|
166
|
+
const size = tarOctal(header.subarray(124, 136));
|
|
167
|
+
const contentStart = offset + 512;
|
|
168
|
+
if (path === "package/package.json") {
|
|
169
|
+
const parsed = JSON.parse(new TextDecoder().decode(tar.subarray(contentStart, contentStart + size)));
|
|
170
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
171
|
+
throw new Error("package.json 不是对象");
|
|
172
|
+
}
|
|
173
|
+
return parsed;
|
|
174
|
+
}
|
|
175
|
+
offset = contentStart + Math.ceil(size / 512) * 512;
|
|
176
|
+
}
|
|
177
|
+
throw new Error("tarball 缺少 package/package.json");
|
|
178
|
+
}
|
|
179
|
+
function packageDependencies(manifest) {
|
|
180
|
+
const dependencies = new Set();
|
|
181
|
+
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
182
|
+
const block = manifest[field];
|
|
183
|
+
if (block === undefined)
|
|
184
|
+
continue;
|
|
185
|
+
if (typeof block !== "object" || block === null || Array.isArray(block)) {
|
|
186
|
+
throw new Error(`${field} 不是对象`);
|
|
187
|
+
}
|
|
188
|
+
for (const name of Object.keys(block))
|
|
189
|
+
dependencies.add(name);
|
|
190
|
+
}
|
|
191
|
+
return [...dependencies].sort((left, right) => left.localeCompare(right, "en"));
|
|
192
|
+
}
|
|
193
|
+
function packageArchiveName(name, sha256) {
|
|
194
|
+
return `${name.replace(/^@/, "").replaceAll("/", "-")}-sha256-${sha256}.tgz`;
|
|
195
|
+
}
|
|
196
|
+
async function verifyPackageSource(source) {
|
|
197
|
+
const directory = resolve(source.tarball_directory);
|
|
198
|
+
if (!/^[a-f0-9]{64}$/.test(source.tarball_directory_sha256)) {
|
|
199
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", "tarball 目录 SHA-256 格式无效");
|
|
200
|
+
}
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `无法读取 tarball 目录:${message}`);
|
|
208
|
+
}
|
|
209
|
+
if (entries.some((entry) => !entry.isFile() || (extname(entry.name) !== ".tgz" && entry.name !== TOOLCHAIN_MANIFEST_FILE))) {
|
|
210
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", "tarball 目录只能包含 .tgz 常规文件与 ToolchainManifest");
|
|
211
|
+
}
|
|
212
|
+
const tarballs = entries.filter((entry) => extname(entry.name) === ".tgz");
|
|
213
|
+
const files = await Promise.all(tarballs.map(async (entry) => ({
|
|
214
|
+
path: entry.name,
|
|
215
|
+
bytes: new Uint8Array(await readFile(join(directory, entry.name))),
|
|
216
|
+
})));
|
|
217
|
+
const digest = await computeCanonicalFileTreeDigest(files);
|
|
218
|
+
if (digest !== source.tarball_directory_sha256) {
|
|
219
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", "tarball 目录 SHA-256 不匹配");
|
|
220
|
+
}
|
|
221
|
+
let manifest;
|
|
222
|
+
try {
|
|
223
|
+
manifest = JSON.parse(await readFile(join(directory, TOOLCHAIN_MANIFEST_FILE), "utf8"));
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `ToolchainManifest 无法读取:${message}`);
|
|
228
|
+
}
|
|
229
|
+
if (manifest.schema_version !== 1 ||
|
|
230
|
+
manifest.tarball_count !== tarballs.length ||
|
|
231
|
+
!Array.isArray(manifest.packages) ||
|
|
232
|
+
manifest.packages.length === 0) {
|
|
233
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", "ToolchainManifest schema 或 tarball_count 无效");
|
|
234
|
+
}
|
|
235
|
+
const tarballNames = new Set(tarballs.map(({ name }) => name));
|
|
236
|
+
const fileNameByPackage = new Map();
|
|
237
|
+
const versionByPackage = new Map();
|
|
238
|
+
const dependenciesByPackage = new Map();
|
|
239
|
+
for (const rawPackage of manifest.packages) {
|
|
240
|
+
const item = rawPackage;
|
|
241
|
+
if (typeof item.name !== "string" ||
|
|
242
|
+
!/^@(?:chat|world-engines|worldengine)\/[a-z0-9-]+$/.test(item.name) ||
|
|
243
|
+
typeof item.version !== "string" ||
|
|
244
|
+
item.version.length === 0 ||
|
|
245
|
+
typeof item.archive !== "string" ||
|
|
246
|
+
typeof item.sha256 !== "string" ||
|
|
247
|
+
!/^[a-f0-9]{64}$/.test(item.sha256) ||
|
|
248
|
+
item.archive !== packageArchiveName(item.name, item.sha256) ||
|
|
249
|
+
!tarballNames.has(item.archive) ||
|
|
250
|
+
fileNameByPackage.has(item.name)) {
|
|
251
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", "ToolchainManifest package identity 无效或重复");
|
|
252
|
+
}
|
|
253
|
+
const bytes = new Uint8Array(await readFile(join(directory, item.archive)));
|
|
254
|
+
if (await sha256Hex(bytes) !== item.sha256) {
|
|
255
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `${item.name} tarball 内容 SHA-256 不匹配`);
|
|
256
|
+
}
|
|
257
|
+
let packed;
|
|
258
|
+
try {
|
|
259
|
+
packed = readPackedPackageManifest(bytes);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `${item.name} tarball 无效:${message}`);
|
|
264
|
+
}
|
|
265
|
+
if (packed.name !== item.name || packed.version !== item.version || packed.private === true) {
|
|
266
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `${item.name} tarball package identity 不匹配`);
|
|
267
|
+
}
|
|
268
|
+
fileNameByPackage.set(item.name, item.archive);
|
|
269
|
+
versionByPackage.set(item.name, item.version);
|
|
270
|
+
try {
|
|
271
|
+
dependenciesByPackage.set(item.name, packageDependencies(packed));
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `${item.name} dependencies 无效:${message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const [name, dependencies] of dependenciesByPackage) {
|
|
279
|
+
for (const dependency of dependencies) {
|
|
280
|
+
if (/^@(?:chat|world-engines|worldengine)\//.test(dependency) && !fileNameByPackage.has(dependency)) {
|
|
281
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `ToolchainManifest 缺少 ${name} 的 first-party 依赖 ${dependency}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
fileNameByPackage,
|
|
287
|
+
versionByPackage,
|
|
288
|
+
dependenciesByPackage,
|
|
289
|
+
tarballFileNames: [...tarballNames].sort((left, right) => left.localeCompare(right, "en")),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function packageClosure(directDependencies, verified) {
|
|
293
|
+
const closure = new Set();
|
|
294
|
+
const pending = directDependencies.filter((name) => verified.fileNameByPackage.has(name));
|
|
295
|
+
while (pending.length > 0) {
|
|
296
|
+
const name = pending.pop();
|
|
297
|
+
if (name === undefined || closure.has(name))
|
|
298
|
+
continue;
|
|
299
|
+
closure.add(name);
|
|
300
|
+
for (const dependency of verified.dependenciesByPackage.get(name) ?? []) {
|
|
301
|
+
if (verified.fileNameByPackage.has(dependency) && !closure.has(dependency))
|
|
302
|
+
pending.push(dependency);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return [...closure].sort((left, right) => left.localeCompare(right, "en"));
|
|
306
|
+
}
|
|
307
|
+
async function materializePackageSource(root, source) {
|
|
308
|
+
const verified = await verifyPackageSource(source);
|
|
309
|
+
const outputDirectory = join(root, ".worldengine", "package-source");
|
|
310
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
311
|
+
for (const fileName of verified.tarballFileNames) {
|
|
312
|
+
await copyFile(join(resolve(source.tarball_directory), fileName), join(outputDirectory, fileName));
|
|
313
|
+
}
|
|
314
|
+
await copyFile(join(resolve(source.tarball_directory), TOOLCHAIN_MANIFEST_FILE), join(outputDirectory, TOOLCHAIN_MANIFEST_FILE));
|
|
315
|
+
const rootPackagePath = join(root, "package.json");
|
|
316
|
+
const rootPackage = JSON.parse(await readFile(rootPackagePath, "utf8"));
|
|
317
|
+
const rootDependencyBlocks = [rootPackage.dependencies, rootPackage.optionalDependencies ?? {}];
|
|
318
|
+
for (const block of rootDependencyBlocks) {
|
|
319
|
+
for (const name of Object.keys(block)) {
|
|
320
|
+
if (!/^@(?:chat|world-engines|worldengine)\//.test(name))
|
|
321
|
+
continue;
|
|
322
|
+
const fileName = verified.fileNameByPackage.get(name);
|
|
323
|
+
if (fileName === undefined) {
|
|
324
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `ToolchainManifest 缺少 root 依赖 ${name}`);
|
|
325
|
+
}
|
|
326
|
+
block[name] = `file:.worldengine/package-source/${fileName}`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
await writeFile(rootPackagePath, `${JSON.stringify(rootPackage, null, 2)}\n`, "utf8");
|
|
330
|
+
const viewPackagePath = join(root, "view", "package.json");
|
|
331
|
+
const viewPackage = JSON.parse(await readFile(viewPackagePath, "utf8"));
|
|
332
|
+
for (const name of Object.keys(viewPackage.dependencies)) {
|
|
333
|
+
if (!/^@(?:chat|world-engines|worldengine)\//.test(name))
|
|
334
|
+
continue;
|
|
335
|
+
const fileName = verified.fileNameByPackage.get(name);
|
|
336
|
+
if (fileName === undefined) {
|
|
337
|
+
throw new ProjectInitializationError("E_PACKAGE_SOURCE_INVALID", `ToolchainManifest 缺少 view 依赖 ${name}`);
|
|
338
|
+
}
|
|
339
|
+
viewPackage.dependencies[name] = `file:../.worldengine/package-source/${fileName}`;
|
|
340
|
+
}
|
|
341
|
+
await writeFile(viewPackagePath, `${JSON.stringify(viewPackage, null, 2)}\n`, "utf8");
|
|
342
|
+
return {
|
|
343
|
+
rootArchives: packageClosure(rootDependencyBlocks.flatMap((block) => Object.keys(block)), verified).map((name) => join(outputDirectory, verified.fileNameByPackage.get(name))),
|
|
344
|
+
viewArchives: packageClosure(Object.keys(viewPackage.dependencies), verified).map((name) => join(outputDirectory, verified.fileNameByPackage.get(name))),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async function assertLinkTargetMissing(output, displayPath) {
|
|
348
|
+
try {
|
|
349
|
+
await lstat(output);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (error.code === "ENOENT")
|
|
353
|
+
return;
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", `拒绝覆盖未知 AgentKit 入口:${displayPath}`);
|
|
357
|
+
}
|
|
358
|
+
async function createAgentSkillJunctions(root, previousRoot) {
|
|
359
|
+
for (const rootLink of AGENT_KIT_ROOT_LINKS.filter(({ kind }) => kind === "junction")) {
|
|
360
|
+
const output = join(root, ...rootLink.path.split("/"));
|
|
361
|
+
const target = join(root, ...rootLink.target.split("/"));
|
|
362
|
+
if (previousRoot === undefined) {
|
|
363
|
+
await assertLinkTargetMissing(output, rootLink.path);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const entry = await lstat(output);
|
|
367
|
+
const previousTarget = resolve(previousRoot, ...rootLink.target.split("/"));
|
|
368
|
+
const actualTarget = resolve(dirname(output), await readlink(output));
|
|
369
|
+
if (!entry.isSymbolicLink() || actualTarget !== previousTarget) {
|
|
370
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", `拒绝重定向未知 AgentKit 入口:${rootLink.path}`);
|
|
371
|
+
}
|
|
372
|
+
await rm(output, { recursive: true, force: false });
|
|
373
|
+
}
|
|
374
|
+
await symlink(target, output, "junction");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function verifyAgentKitLinks(root) {
|
|
378
|
+
for (const rootLink of AGENT_KIT_ROOT_LINKS.filter(({ kind }) => kind === "hardlink")) {
|
|
379
|
+
const output = join(root, ...rootLink.path.split("/"));
|
|
380
|
+
const target = join(root, ...rootLink.target.split("/"));
|
|
381
|
+
if ((await stat(output)).ino !== (await stat(target)).ino) {
|
|
382
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", `${rootLink.path} 未链接至 AgentKit 单真相源`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const rootLink of AGENT_KIT_ROOT_LINKS.filter(({ kind }) => kind === "junction")) {
|
|
386
|
+
const output = join(root, ...rootLink.path.split("/"));
|
|
387
|
+
const target = await realpath(join(root, ...rootLink.target.split("/")));
|
|
388
|
+
if (!(await lstat(output)).isSymbolicLink() || (await realpath(output)) !== target) {
|
|
389
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", `${rootLink.path} 未链接至 AgentKit 单真相源`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function createAgentKitLinks(root) {
|
|
394
|
+
for (const rootLink of AGENT_KIT_ROOT_LINKS.filter(({ kind }) => kind === "hardlink")) {
|
|
395
|
+
const output = join(root, ...rootLink.path.split("/"));
|
|
396
|
+
await mkdir(dirname(output), { recursive: true });
|
|
397
|
+
await assertLinkTargetMissing(output, rootLink.path);
|
|
398
|
+
await link(join(root, ...rootLink.target.split("/")), output);
|
|
399
|
+
}
|
|
400
|
+
// Windows junction 会把 target 固化为绝对路径。先指向 staging 通过验证,promote 后再重建到最终根目录。
|
|
401
|
+
await createAgentSkillJunctions(root);
|
|
402
|
+
await verifyAgentKitLinks(root);
|
|
403
|
+
}
|
|
404
|
+
async function runCommand(command, args, cwd, errorCode) {
|
|
405
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
406
|
+
// 始终通过 node.exe + npm-cli.js(或明确的 node 可执行文件)和参数数组调用,
|
|
407
|
+
// 不经 shell,项目路径与 tarball 名称不会被解释成 shell 语法。
|
|
408
|
+
const child = spawn(command, [...args], {
|
|
409
|
+
cwd, stdio: "inherit", windowsHide: true, shell: false,
|
|
410
|
+
env: npmChildEnvironment(command === "npm" ? process.execPath : command),
|
|
411
|
+
});
|
|
412
|
+
child.once("error", (error) => rejectPromise(new ProjectInitializationError(errorCode, error.message)));
|
|
413
|
+
child.once("exit", (code) => {
|
|
414
|
+
if (code === 0)
|
|
415
|
+
resolvePromise();
|
|
416
|
+
else
|
|
417
|
+
rejectPromise(new ProjectInitializationError(errorCode, `${command} ${args.join(" ")} 退出码 ${String(code)}`));
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async function resolveNpmInvocation(input) {
|
|
422
|
+
if (input.npm_executable !== undefined) {
|
|
423
|
+
if (input.npm_executable.toLowerCase().endsWith(".cmd")) {
|
|
424
|
+
const npmCli = join(dirname(resolve(input.npm_executable)), "node_modules", "npm", "bin", "npm-cli.js");
|
|
425
|
+
try {
|
|
426
|
+
await stat(npmCli);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", `无法从 npm.cmd 定位 npm CLI:${npmCli}`);
|
|
430
|
+
}
|
|
431
|
+
return { command: process.execPath, argumentsPrefix: [npmCli, ...(input.npm_arguments_prefix ?? [])] };
|
|
432
|
+
}
|
|
433
|
+
const executable = resolve(input.npm_executable);
|
|
434
|
+
if (!/^node(?:\.exe)?$/i.test(basename(executable))) {
|
|
435
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", "npm_executable 必须是 node.exe 或 npm.cmd,避免通过任意命令执行安装");
|
|
436
|
+
}
|
|
437
|
+
return { command: executable, argumentsPrefix: input.npm_arguments_prefix ?? [] };
|
|
438
|
+
}
|
|
439
|
+
if (process.platform === "win32") {
|
|
440
|
+
const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
441
|
+
try {
|
|
442
|
+
await stat(npmCli);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", `Node 安装不含可执行 npm CLI:${npmCli}`);
|
|
446
|
+
}
|
|
447
|
+
return { command: process.execPath, argumentsPrefix: [npmCli] };
|
|
448
|
+
}
|
|
449
|
+
return { command: "npm", argumentsPrefix: [] };
|
|
450
|
+
}
|
|
451
|
+
async function requireInstalledNodeModules(root) {
|
|
452
|
+
try {
|
|
453
|
+
if (!(await stat(join(root, "node_modules"))).isDirectory()) {
|
|
454
|
+
throw new Error("node_modules 不是目录");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
459
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", `依赖安装未生成 node_modules:${message}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function requireRegularInstalledFile(path, label) {
|
|
463
|
+
try {
|
|
464
|
+
const info = await lstat(path);
|
|
465
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
466
|
+
throw new Error("不是普通文件");
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
470
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", `${label} 无效:${message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function verifyInstalledProjectHostGui(root) {
|
|
474
|
+
const packageRoot = join(root, "node_modules", "@world-engines", "project-host");
|
|
475
|
+
const manifestPath = join(packageRoot, "package.json");
|
|
476
|
+
await requireRegularInstalledFile(manifestPath, "@world-engines/project-host manifest");
|
|
477
|
+
let manifest;
|
|
478
|
+
try {
|
|
479
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
483
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", `@world-engines/project-host manifest 无法读取:${message}`);
|
|
484
|
+
}
|
|
485
|
+
if (manifest.name !== "@world-engines/project-host" ||
|
|
486
|
+
typeof manifest.bin !== "object" || manifest.bin === null || Array.isArray(manifest.bin) ||
|
|
487
|
+
manifest.bin.worldengine !== "dist/cli.js") {
|
|
488
|
+
throw new ProjectInitializationError("E_DEPENDENCY_INSTALL_FAILED", "@world-engines/project-host manifest identity/bin 不匹配");
|
|
489
|
+
}
|
|
490
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "cli.js"), "@world-engines/project-host CLI");
|
|
491
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "index.html"), "@world-engines/project-host GUI HTML");
|
|
492
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "app.js"), "@world-engines/project-host GUI JavaScript");
|
|
493
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "local-preview-bridge.js"), "@world-engines/project-host GUI Preview Bridge");
|
|
494
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "styles.css"), "@world-engines/project-host GUI CSS");
|
|
495
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "WELogo-title.svg"), "@world-engines/project-host GUI Logo");
|
|
496
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "workspace-status.css"), "@world-engines/project-host Workspace Status CSS");
|
|
497
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "workspace-status.js"), "@world-engines/project-host Workspace Status JavaScript");
|
|
498
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "world-map-globe.js"), "@world-engines/project-host World Map Globe JavaScript");
|
|
499
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "world-map-worker.js"), "@world-engines/project-host World Map Worker JavaScript");
|
|
500
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "world-map.css"), "@world-engines/project-host World Map CSS");
|
|
501
|
+
await requireRegularInstalledFile(join(packageRoot, "assets", "gui", "world-map.js"), "@world-engines/project-host World Map JavaScript");
|
|
502
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "graph.js"), "@world-engines/project-host Graph GUI JavaScript");
|
|
503
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "graph.css"), "@world-engines/project-host Graph GUI CSS");
|
|
504
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "score.js"), "@world-engines/project-host Score GUI JavaScript");
|
|
505
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "score.css"), "@world-engines/project-host Score GUI CSS");
|
|
506
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "trigger.js"), "@world-engines/project-host Trigger GUI JavaScript");
|
|
507
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "trigger.css"), "@world-engines/project-host Trigger GUI CSS");
|
|
508
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "graph-layout-worker.js"), "@world-engines/project-host Graph Layout Worker");
|
|
509
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "graph-layout.wasm"), "@world-engines/project-host Graph Layout WASM");
|
|
510
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "plane-generator.js"), "@world-engines/project-host Plane Generator JavaScript");
|
|
511
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "plane-generator_bg.wasm"), "@world-engines/project-host Plane Generator WASM");
|
|
512
|
+
await requireRegularInstalledFile(join(packageRoot, "dist", "gui", "plane-generation.js"), "@world-engines/project-host Plane Generation JavaScript");
|
|
513
|
+
}
|
|
514
|
+
async function listViewSourceFiles(root, directory = "view") {
|
|
515
|
+
const absolute = join(root, ...directory.split("/"));
|
|
516
|
+
const entries = await readdir(absolute, { withFileTypes: true });
|
|
517
|
+
const files = [];
|
|
518
|
+
for (const entry of entries) {
|
|
519
|
+
if (["node_modules", "dist", ".vite"].includes(entry.name))
|
|
520
|
+
continue;
|
|
521
|
+
const child = `${directory}/${entry.name}`;
|
|
522
|
+
if (entry.isDirectory())
|
|
523
|
+
files.push(...(await listViewSourceFiles(root, child)));
|
|
524
|
+
else if (entry.isFile())
|
|
525
|
+
files.push(child);
|
|
526
|
+
}
|
|
527
|
+
return files.sort((left, right) => left.localeCompare(right, "en"));
|
|
528
|
+
}
|
|
529
|
+
async function computeViewTreeDigest(root) {
|
|
530
|
+
return computeCanonicalFileTreeDigest(await Promise.all((await listViewSourceFiles(root)).map(async (path) => ({
|
|
531
|
+
path: path.slice("view/".length),
|
|
532
|
+
bytes: new Uint8Array(await readFile(join(root, ...path.split("/")))),
|
|
533
|
+
}))));
|
|
534
|
+
}
|
|
535
|
+
async function digestFileOrEmpty(path) {
|
|
536
|
+
try {
|
|
537
|
+
return sha256Hex(new Uint8Array(await readFile(path)));
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
if (error.code === "ENOENT") {
|
|
541
|
+
return sha256Hex(new Uint8Array(0));
|
|
542
|
+
}
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function readSnapshotDigest(root) {
|
|
547
|
+
const manifest = JSON.parse(await readFile(join(root, "worldengine.project.json"), "utf8"));
|
|
548
|
+
const digest = manifest.snapshot?.canonical_digest;
|
|
549
|
+
if (typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest)) {
|
|
550
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", "snapshot 后 manifest digest 无效");
|
|
551
|
+
}
|
|
552
|
+
return digest;
|
|
553
|
+
}
|
|
554
|
+
async function buildManifest(root, ids, nowMs) {
|
|
555
|
+
const scenarioBytes = new Uint8Array(await readFile(join(root, "scenario", "source.wes")));
|
|
556
|
+
const manifest = {
|
|
557
|
+
schema_version: 2,
|
|
558
|
+
project_id: ids.project_id,
|
|
559
|
+
project_revision: 0,
|
|
560
|
+
created_at_ms: nowMs,
|
|
561
|
+
updated_at_ms: nowMs,
|
|
562
|
+
scenario: {
|
|
563
|
+
local_scenario_id: ids.local_scenario_id,
|
|
564
|
+
source_path: "scenario/source.wes",
|
|
565
|
+
source_sha256: await sha256Hex(scenarioBytes),
|
|
566
|
+
assets_digest: await sha256Hex(new Uint8Array(0)),
|
|
567
|
+
},
|
|
568
|
+
view: {
|
|
569
|
+
local_view_id: ids.local_view_id,
|
|
570
|
+
root_path: "view",
|
|
571
|
+
source_tree_sha256: await computeViewTreeDigest(root),
|
|
572
|
+
package_lock_sha256: await digestFileOrEmpty(join(root, "view", "package-lock.json")),
|
|
573
|
+
},
|
|
574
|
+
gallery: {
|
|
575
|
+
root_path: LOCAL_AUTHOR_GALLERY_PATHS.root,
|
|
576
|
+
scenario_root_path: LOCAL_AUTHOR_GALLERY_PATHS.scenario,
|
|
577
|
+
view_root_path: LOCAL_AUTHOR_GALLERY_PATHS.view,
|
|
578
|
+
content_tree_sha256: await computeCanonicalFileTreeDigest([]),
|
|
579
|
+
},
|
|
580
|
+
snapshot: {
|
|
581
|
+
snapshot_id: ids.snapshot_id,
|
|
582
|
+
canonical_digest: EMPTY_DIGEST,
|
|
583
|
+
previous_snapshot_id: null,
|
|
584
|
+
},
|
|
585
|
+
toolchain: {
|
|
586
|
+
template_version: TOOLCHAIN_VERSION,
|
|
587
|
+
project_host_version: TOOLCHAIN_VERSION,
|
|
588
|
+
authoring_bridge_version: TOOLCHAIN_VERSION,
|
|
589
|
+
desktop_package_version: TOOLCHAIN_VERSION,
|
|
590
|
+
agent_kit_version: TOOLCHAIN_VERSION,
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
manifest.snapshot.canonical_digest = await computeCanonicalProjectDigest(manifest);
|
|
594
|
+
return manifest;
|
|
595
|
+
}
|
|
596
|
+
async function assertBootstrapUnchanged(target, file) {
|
|
597
|
+
const entries = await readdir(target);
|
|
598
|
+
if (entries.length !== 1 || entries[0]?.toLocaleLowerCase("en-US") !== file.name.toLocaleLowerCase("en-US")) {
|
|
599
|
+
throw new ProjectInitializationError("E_TARGET_NOT_EMPTY", "验证期间目标目录发生变化");
|
|
600
|
+
}
|
|
601
|
+
const current = await inspectBootstrapFile(target, file.path);
|
|
602
|
+
if (current.sha256 !== file.sha256) {
|
|
603
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "验证期间启动 CMD 内容发生变化");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function promoteProjectRoot(staging, target, targetState, operationId) {
|
|
607
|
+
if (targetState.kind === "missing") {
|
|
608
|
+
await rename(staging, target);
|
|
609
|
+
return { kind: "renamed", backup: undefined };
|
|
610
|
+
}
|
|
611
|
+
if (targetState.kind === "bootstrap") {
|
|
612
|
+
await assertBootstrapUnchanged(target, targetState.file);
|
|
613
|
+
const movedEntries = [];
|
|
614
|
+
try {
|
|
615
|
+
for (const entry of await readdir(staging)) {
|
|
616
|
+
await rename(join(staging, entry), join(target, entry));
|
|
617
|
+
movedEntries.push(entry);
|
|
618
|
+
}
|
|
619
|
+
return { kind: "merged", movedEntries, staging };
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
for (const entry of movedEntries.reverse()) {
|
|
623
|
+
await rename(join(target, entry), join(staging, entry));
|
|
624
|
+
}
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const backup = join(dirname(target), `.${basename(target)}.worldengine-preimage-${operationId}`);
|
|
629
|
+
await rename(target, backup);
|
|
630
|
+
try {
|
|
631
|
+
await rename(staging, target);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
await rename(backup, target);
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
return { kind: "renamed", backup };
|
|
638
|
+
}
|
|
639
|
+
async function rollbackPromotion(target, promotion) {
|
|
640
|
+
if (promotion.kind === "merged") {
|
|
641
|
+
for (const entry of promotion.movedEntries) {
|
|
642
|
+
await rm(join(target, entry), { recursive: true, force: true });
|
|
643
|
+
}
|
|
644
|
+
await rm(promotion.staging, { recursive: true, force: true });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
await rm(target, { recursive: true, force: true });
|
|
648
|
+
if (promotion.backup !== undefined)
|
|
649
|
+
await rename(promotion.backup, target);
|
|
650
|
+
}
|
|
651
|
+
export async function initializeLocalAuthorProject(input) {
|
|
652
|
+
if (typeof input.target !== "string" || input.target.trim().length === 0) {
|
|
653
|
+
throw new ProjectInitializationError("E_TARGET_INVALID", "初始化目标不能为空");
|
|
654
|
+
}
|
|
655
|
+
const target = resolve(input.target);
|
|
656
|
+
const targetState = await inspectTarget(target, input.bootstrap_file);
|
|
657
|
+
const ids = initializationIds(input);
|
|
658
|
+
const nowMs = input.now_ms ?? Date.now();
|
|
659
|
+
const installDependencies = input.install_dependencies ?? true;
|
|
660
|
+
const verify = input.verify ?? installDependencies;
|
|
661
|
+
const npm = await resolveNpmInvocation(input);
|
|
662
|
+
const staging = join(dirname(target), `.${basename(target)}.worldengine-staging-${ids.operation_id}`);
|
|
663
|
+
let promotion;
|
|
664
|
+
let packageInstallationPlan;
|
|
665
|
+
await mkdir(staging, { recursive: false });
|
|
666
|
+
try {
|
|
667
|
+
await writeTemplate(staging);
|
|
668
|
+
if (input.portable_runtime_directory !== undefined) {
|
|
669
|
+
await materializePortableRuntime(staging, input.portable_runtime_directory);
|
|
670
|
+
}
|
|
671
|
+
if (input.fault_injection === "disk_write") {
|
|
672
|
+
const error = new Error("受控磁盘写入失败");
|
|
673
|
+
Object.assign(error, { code: "ENOSPC" });
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
if (input.package_source !== undefined) {
|
|
677
|
+
packageInstallationPlan = await materializePackageSource(staging, input.package_source);
|
|
678
|
+
}
|
|
679
|
+
await writeFile(join(staging, "scenario", "source.wes"), await createEmptyScenarioPartWesp(), {
|
|
680
|
+
flag: "wx",
|
|
681
|
+
});
|
|
682
|
+
await createAgentKitLinks(staging);
|
|
683
|
+
if (installDependencies) {
|
|
684
|
+
await runCommand(npm.command, [
|
|
685
|
+
...npm.argumentsPrefix,
|
|
686
|
+
"install",
|
|
687
|
+
"--workspaces=false",
|
|
688
|
+
...(packageInstallationPlan === undefined ? [] : ["--no-save", ...packageInstallationPlan.rootArchives]),
|
|
689
|
+
], staging, "E_DEPENDENCY_INSTALL_FAILED");
|
|
690
|
+
await requireInstalledNodeModules(staging);
|
|
691
|
+
await verifyInstalledProjectHostGui(staging);
|
|
692
|
+
if (packageInstallationPlan !== undefined) {
|
|
693
|
+
// --no-save 保留根 manifest 的四个公开入口;安装后的 lock-only 读取同一闭包,
|
|
694
|
+
// 将实际解析版本固定为可复现且可 npm audit 的 root package-lock。
|
|
695
|
+
await runCommand(npm.command, [
|
|
696
|
+
...npm.argumentsPrefix,
|
|
697
|
+
"install",
|
|
698
|
+
"--workspaces=false",
|
|
699
|
+
"--package-lock-only",
|
|
700
|
+
"--ignore-scripts",
|
|
701
|
+
], staging, "E_DEPENDENCY_INSTALL_FAILED");
|
|
702
|
+
await requireRegularInstalledFile(join(staging, "package-lock.json"), "root package-lock");
|
|
703
|
+
}
|
|
704
|
+
await runCommand(npm.command, [
|
|
705
|
+
...npm.argumentsPrefix,
|
|
706
|
+
"install",
|
|
707
|
+
"--workspaces=false",
|
|
708
|
+
"--prefix",
|
|
709
|
+
"view",
|
|
710
|
+
...(packageInstallationPlan === undefined ? [] : ["--no-save", ...packageInstallationPlan.viewArchives]),
|
|
711
|
+
], staging, "E_DEPENDENCY_INSTALL_FAILED");
|
|
712
|
+
await requireInstalledNodeModules(join(staging, "view"));
|
|
713
|
+
if (packageInstallationPlan !== undefined) {
|
|
714
|
+
await runCommand(npm.command, [
|
|
715
|
+
...npm.argumentsPrefix,
|
|
716
|
+
"install",
|
|
717
|
+
"--workspaces=false",
|
|
718
|
+
"--prefix",
|
|
719
|
+
"view",
|
|
720
|
+
"--package-lock-only",
|
|
721
|
+
"--ignore-scripts",
|
|
722
|
+
], staging, "E_DEPENDENCY_INSTALL_FAILED");
|
|
723
|
+
await requireRegularInstalledFile(join(staging, "view", "package-lock.json"), "view package-lock");
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const manifest = await buildManifest(staging, ids, nowMs);
|
|
727
|
+
await writeFile(join(staging, "worldengine.project.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
728
|
+
await writeFile(join(staging, ".worldengine", "runtime.json"), `${JSON.stringify({ schema_version: 1, project_id: ids.project_id, project_host_version: TOOLCHAIN_VERSION }, null, 2)}\n`, "utf8");
|
|
729
|
+
if (verify) {
|
|
730
|
+
await runCommand(npm.command, [...npm.argumentsPrefix, "run", "gui:check"], staging, "E_INITIALIZATION_VERIFY_FAILED");
|
|
731
|
+
await runCommand(npm.command, [...npm.argumentsPrefix, "run", "snapshot"], staging, "E_INITIALIZATION_VERIFY_FAILED");
|
|
732
|
+
await runCommand(npm.command, [...npm.argumentsPrefix, "run", "doctor"], staging, "E_INITIALIZATION_VERIFY_FAILED");
|
|
733
|
+
await runCommand(npm.command, [...npm.argumentsPrefix, "run", "typecheck"], staging, "E_INITIALIZATION_VERIFY_FAILED");
|
|
734
|
+
await runCommand(npm.command, [...npm.argumentsPrefix, "run", "build"], staging, "E_INITIALIZATION_VERIFY_FAILED");
|
|
735
|
+
}
|
|
736
|
+
// 所有安装和 doctor/typecheck/build 都已在 target 外完成后,才替换空目标。
|
|
737
|
+
promotion = await promoteProjectRoot(staging, target, targetState, ids.operation_id);
|
|
738
|
+
if (input.fault_injection === "agent_link_post_promote") {
|
|
739
|
+
throw new ProjectInitializationError("E_INITIALIZATION_VERIFY_FAILED", "受控 promote 后 skills 重建失败");
|
|
740
|
+
}
|
|
741
|
+
await createAgentSkillJunctions(target, staging);
|
|
742
|
+
await verifyAgentKitLinks(target);
|
|
743
|
+
const receipt = {
|
|
744
|
+
schema_version: 1,
|
|
745
|
+
operation_id: ids.operation_id,
|
|
746
|
+
project_id: ids.project_id,
|
|
747
|
+
status: "ready",
|
|
748
|
+
initialized_at_ms: nowMs,
|
|
749
|
+
root_dependencies_installed: installDependencies,
|
|
750
|
+
view_dependencies_installed: installDependencies,
|
|
751
|
+
dependency_installation: {
|
|
752
|
+
root: { status: installDependencies ? "completed" : "skipped", working_directory: "." },
|
|
753
|
+
view: { status: installDependencies ? "completed" : "skipped", working_directory: "view" },
|
|
754
|
+
},
|
|
755
|
+
verification_completed: verify,
|
|
756
|
+
snapshot_digest: verify ? await readSnapshotDigest(target) : manifest.snapshot.canonical_digest,
|
|
757
|
+
};
|
|
758
|
+
await writeFile(join(target, ".worldengine", "operations", "initialization.json"), `${JSON.stringify(receipt, null, 2)}\n`, "utf8");
|
|
759
|
+
if (promotion.kind === "renamed" && promotion.backup !== undefined) {
|
|
760
|
+
await rm(promotion.backup, { recursive: true, force: true });
|
|
761
|
+
}
|
|
762
|
+
else if (promotion.kind === "merged") {
|
|
763
|
+
await rm(promotion.staging, { recursive: true, force: true });
|
|
764
|
+
}
|
|
765
|
+
return receipt;
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
if (promotion !== undefined) {
|
|
769
|
+
await rollbackPromotion(target, promotion);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
await rm(staging, { recursive: true, force: true });
|
|
773
|
+
}
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|