@world-engines/project-setup 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 ADDED
@@ -0,0 +1,46 @@
1
+ WorldEngine 官方作者工具许可证
2
+
3
+ 版权所有 (c) 2026 Nixdorfer。保留所有权利。
4
+
5
+ 本仓库的源代码、构建材料及附带资源(统称“本作品”)不是开源软件,
6
+ 不适用任何 OSI 认证的开源许可证。
7
+
8
+ 一、官方作者工具分发许可
9
+
10
+ 版权所有者可以通过其官方网站、npm registry、签名安装包或其他官方渠道,
11
+ 复制并分发由本作品构建的 WorldEngine 作者工具及其必要运行资源
12
+ (统称“官方作者工具”)。仅版权所有者或其书面指定的发布者享有此分发权。
13
+
14
+ 二、作者用户许可
15
+
16
+ 从官方渠道取得官方作者工具的作者用户,可以:
17
+
18
+ 1. 安装和运行官方作者工具;
19
+ 2. 使用官方作者工具创作、编辑、预览、导入、导出和提交其有权处理的内容;
20
+ 3. 为上述使用目的制作合理必要的本地备份副本。
21
+
22
+ 三、未授予的权利
23
+
24
+ 除第二条明确允许的行为外,本许可证不授予作者用户或其他第三方以下权利:
25
+
26
+ 1. 修改、改编、反编译、反汇编或制作官方作者工具的派生作品;
27
+ 2. 复制、镜像、转发、再上传、出售、出租、再分发或再许可官方作者工具;
28
+ 3. 使用本作品的源代码、构建材料或任何部分开发、提供或训练其他产品或服务;
29
+ 4. 删除或规避版权、许可、签名、访问控制或其他权利管理信息。
30
+
31
+ 法律强制允许且合同不得排除的权利不受上述限制影响。
32
+
33
+ 四、源代码查看
34
+
35
+ 第三方可以在已获合法访问权限的范围内查看本作品,用于审查、安全研究或学习参考;
36
+ 查看不授予运行、复制、修改、分发、再许可或用于其他产品与服务的权利。
37
+
38
+ 五、无担保与责任限制
39
+
40
+ 本作品与官方作者工具均按“现状”提供,不附带任何明示或暗示的担保。
41
+ 在适用法律允许的最大范围内,版权所有者不对因访问或使用本作品或官方作者工具
42
+ 造成的任何损失承担责任。
43
+
44
+ 六、终止
45
+
46
+ 违反本许可证将立即终止相应的访问与使用权;终止不影响版权所有者已经产生的权利和救济。
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ interface ReleaseStandaloneCmdReceiptV1 {
3
+ readonly schema_version: 1;
4
+ readonly release_identity: string;
5
+ readonly payload_root: string;
6
+ readonly command_file: string;
7
+ readonly command_sha256: string;
8
+ readonly node_version: string;
9
+ readonly payload_archive_sha256: string;
10
+ readonly package_source: string;
11
+ }
12
+ /**
13
+ * 将已经 fresh-pack 的 first-party tarball closure 变成可分发的 Windows 单 CMD。
14
+ * 它不执行 npm publish,也不运行 npm/npx;作者最终运行的是生成后的 CMD。
15
+ */
16
+ export declare function buildReleaseStandaloneCmd(argv: readonly string[]): Promise<ReleaseStandaloneCmdReceiptV1>;
17
+ export declare function runReleaseStandaloneCmdBuilder(argv: readonly string[]): Promise<number>;
18
+ export {};
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { access, rm } from "node:fs/promises";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import { buildSetupPayload, prepareInitializerBootstrap } from "./installer.js";
9
+ import { buildStandaloneSetupCmd } from "./standalone-cmd.js";
10
+ const execFileAsync = promisify(execFile);
11
+ function value(argv, name) {
12
+ const index = argv.indexOf(name);
13
+ const candidate = index < 0 ? undefined : argv[index + 1];
14
+ if (candidate === undefined || candidate.startsWith("--")) {
15
+ throw new Error(`E_ARGUMENT_INVALID: ${name} 需要绝对路径或 release identity`);
16
+ }
17
+ return candidate;
18
+ }
19
+ function absolute(value, name) {
20
+ const path = resolve(value);
21
+ if (path !== value && !/^[a-zA-Z]:[\\/]/.test(value)) {
22
+ throw new Error(`E_ARGUMENT_INVALID: ${name} 必须是绝对路径`);
23
+ }
24
+ return path;
25
+ }
26
+ async function existingDirectory(path, label) {
27
+ await access(path).catch(() => {
28
+ throw new Error(`E_INPUT_MISSING: ${label} 不存在: ${path}`);
29
+ });
30
+ }
31
+ /**
32
+ * 将已经 fresh-pack 的 first-party tarball closure 变成可分发的 Windows 单 CMD。
33
+ * 它不执行 npm publish,也不运行 npm/npx;作者最终运行的是生成后的 CMD。
34
+ */
35
+ export async function buildReleaseStandaloneCmd(argv) {
36
+ const allowed = new Set([
37
+ "--repository-root",
38
+ "--package-source",
39
+ "--runtime-directory",
40
+ "--payload-root",
41
+ "--output",
42
+ "--release-identity",
43
+ ]);
44
+ if (argv.length !== allowed.size * 2 || argv.some((item, index) => index % 2 === 0 && !allowed.has(item))) {
45
+ throw new Error("E_ARGUMENT_INVALID: 需要 repository-root、package-source、runtime-directory、payload-root、output、release-identity");
46
+ }
47
+ const repositoryRoot = absolute(value(argv, "--repository-root"), "--repository-root");
48
+ const packageSource = absolute(value(argv, "--package-source"), "--package-source");
49
+ const runtimeDirectory = absolute(value(argv, "--runtime-directory"), "--runtime-directory");
50
+ const payloadRoot = absolute(value(argv, "--payload-root"), "--payload-root");
51
+ const outputFile = absolute(value(argv, "--output"), "--output");
52
+ const releaseIdentity = value(argv, "--release-identity");
53
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(releaseIdentity)) {
54
+ throw new Error("E_ARGUMENT_INVALID: release identity 只能包含字母、数字、点、下划线和连字符");
55
+ }
56
+ await Promise.all([
57
+ existingDirectory(repositoryRoot, "repository root"),
58
+ existingDirectory(packageSource, "package source"),
59
+ existingDirectory(runtimeDirectory, "portable Node runtime"),
60
+ ]);
61
+ await access(dirname(outputFile)).catch(() => {
62
+ throw new Error(`E_OUTPUT_PARENT_MISSING: CMD 输出目录不存在: ${dirname(outputFile)}`);
63
+ });
64
+ const projectSetup = join(repositoryRoot, "tools", "worldengine-project-setup");
65
+ const createProject = join(repositoryRoot, "packages", "worldengine-create-project");
66
+ const projectFormat = join(repositoryRoot, "packages", "worldengine-project-format");
67
+ const agentKit = join(repositoryRoot, "packages", "worldengine-agent-kit");
68
+ await Promise.all([
69
+ existingDirectory(projectSetup, "project-setup package"),
70
+ existingDirectory(createProject, "create-project package"),
71
+ existingDirectory(projectFormat, "project-format package"),
72
+ existingDirectory(agentKit, "agent-kit package"),
73
+ ]);
74
+ const nodeExecutable = join(runtimeDirectory, "node.exe");
75
+ const { stdout } = await execFileAsync(nodeExecutable, ["--version"], { windowsHide: true });
76
+ const nodeVersion = stdout.trim();
77
+ if (!/^v\d+\.\d+\.\d+$/.test(nodeVersion)) {
78
+ throw new Error(`E_RUNTIME_INVALID: node.exe 未返回语义版本: ${nodeVersion}`);
79
+ }
80
+ const bootstrapDirectory = `${payloadRoot}.bootstrap`;
81
+ let bootstrapCreated = false;
82
+ try {
83
+ await prepareInitializerBootstrap({
84
+ outputDirectory: bootstrapDirectory,
85
+ projectSetupDirectory: projectSetup,
86
+ createProjectDirectory: createProject,
87
+ projectFormatDirectory: projectFormat,
88
+ agentKitDirectory: agentKit,
89
+ });
90
+ bootstrapCreated = true;
91
+ const payload = await buildSetupPayload({
92
+ outputRoot: payloadRoot,
93
+ runtimeDirectory,
94
+ bootstrapDirectory,
95
+ packageSource,
96
+ releaseIdentity,
97
+ nodeVersion,
98
+ });
99
+ const command = await buildStandaloneSetupCmd({ payloadRoot, outputFile });
100
+ const commandSha256 = createHash("sha256").update(await (await import("node:fs/promises")).readFile(outputFile)).digest("hex");
101
+ return {
102
+ schema_version: 1,
103
+ release_identity: releaseIdentity,
104
+ payload_root: payloadRoot,
105
+ command_file: outputFile,
106
+ command_sha256: commandSha256,
107
+ node_version: payload.node.version,
108
+ payload_archive_sha256: command.payload_archive_sha256,
109
+ package_source: packageSource,
110
+ };
111
+ }
112
+ finally {
113
+ if (bootstrapCreated)
114
+ await rm(bootstrapDirectory, { recursive: true, force: true });
115
+ }
116
+ }
117
+ export async function runReleaseStandaloneCmdBuilder(argv) {
118
+ try {
119
+ process.stdout.write(`${JSON.stringify(await buildReleaseStandaloneCmd(argv), null, 2)}\n`);
120
+ return 0;
121
+ }
122
+ catch (error) {
123
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
124
+ return 1;
125
+ }
126
+ }
127
+ const invokedPath = process.argv[1];
128
+ if (invokedPath !== undefined && resolve(invokedPath).toLocaleLowerCase("en-US") === resolve(fileURLToPath(import.meta.url)).toLocaleLowerCase("en-US")) {
129
+ void runReleaseStandaloneCmdBuilder(process.argv.slice(2)).then((exitCode) => {
130
+ process.exitCode = exitCode;
131
+ });
132
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runStandaloneCmdBuilder(argv: readonly string[]): Promise<number>;
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { buildStandaloneSetupCmd } from "./standalone-cmd.js";
5
+ function value(argv, name) {
6
+ const index = argv.indexOf(name);
7
+ const candidate = index < 0 ? undefined : argv[index + 1];
8
+ if (candidate === undefined || candidate.startsWith("--"))
9
+ throw new Error(`E_ARGUMENT_INVALID: ${name} 需要路径`);
10
+ return candidate;
11
+ }
12
+ export async function runStandaloneCmdBuilder(argv) {
13
+ try {
14
+ const allowed = new Set(["--payload-root", "--output"]);
15
+ for (let index = 0; index < argv.length; index += 2) {
16
+ const name = argv[index];
17
+ if (name === undefined || !allowed.has(name))
18
+ throw new Error(`E_ARGUMENT_INVALID: 未知参数 ${name ?? ""}`);
19
+ }
20
+ const receipt = await buildStandaloneSetupCmd({
21
+ payloadRoot: value(argv, "--payload-root"),
22
+ outputFile: value(argv, "--output"),
23
+ });
24
+ process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`);
25
+ return 0;
26
+ }
27
+ catch (error) {
28
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
29
+ return 1;
30
+ }
31
+ }
32
+ const invokedPath = process.argv[1];
33
+ if (invokedPath !== undefined && resolve(invokedPath).toLocaleLowerCase("en-US") === resolve(fileURLToPath(import.meta.url)).toLocaleLowerCase("en-US")) {
34
+ void runStandaloneCmdBuilder(process.argv.slice(2)).then((exitCode) => {
35
+ process.exitCode = exitCode;
36
+ });
37
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./installer.js";
2
+ export * from "./toolchain-manifest.js";
3
+ export * from "./standalone-cmd.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./installer.js";
2
+ export * from "./toolchain-manifest.js";
3
+ export * from "./standalone-cmd.js";
@@ -0,0 +1,97 @@
1
+ export declare const SETUP_PAYLOAD_SCHEMA_VERSION = 1;
2
+ export declare class ProjectSetupError extends Error {
3
+ readonly code: string;
4
+ constructor(code: string, message: string);
5
+ }
6
+ export interface SetupPayloadManifestV1 {
7
+ readonly schema_version: 1;
8
+ readonly release_identity: string;
9
+ readonly node: {
10
+ readonly path: string;
11
+ readonly sha256: string;
12
+ readonly runtime_sha256: string;
13
+ readonly version: string;
14
+ };
15
+ readonly create_project_tgz: {
16
+ readonly path: string;
17
+ readonly sha256: string;
18
+ };
19
+ readonly package_source: {
20
+ readonly path: string;
21
+ readonly sha256: string;
22
+ };
23
+ /** package source 的 first-party identity 与完整 tarball 数量。 */
24
+ readonly toolchain_manifest: {
25
+ readonly path: string;
26
+ readonly sha256: string;
27
+ };
28
+ readonly bootstrap: {
29
+ readonly path: string;
30
+ readonly sha256: string;
31
+ };
32
+ }
33
+ export interface SetupPreflightV1 {
34
+ readonly schema_version: 1;
35
+ readonly target: string;
36
+ readonly payload_identity: string;
37
+ readonly directory: {
38
+ readonly status: "ready";
39
+ readonly target_exists: boolean;
40
+ };
41
+ readonly network: {
42
+ readonly status: "ready" | "unavailable";
43
+ readonly required_for_dependency_install: boolean;
44
+ };
45
+ readonly disk: {
46
+ readonly status: "ready";
47
+ readonly required_bytes: number;
48
+ readonly available_bytes: number | null;
49
+ };
50
+ }
51
+ export interface SetupInstallationReceiptV1 {
52
+ readonly schema_version: 1;
53
+ readonly operation_id: string;
54
+ readonly status: "ready" | "recoverable_failure";
55
+ readonly payload_identity: string;
56
+ readonly target: string;
57
+ readonly progress: readonly string[];
58
+ readonly initializer_receipt?: unknown;
59
+ readonly recovery_path: string;
60
+ }
61
+ export declare function inspectSetupPayload(payloadRoot: string): Promise<SetupPayloadManifestV1>;
62
+ export declare function preflightSetup({ payloadRoot, target, requiredBytes, networkProbe }: {
63
+ payloadRoot: string;
64
+ target: string;
65
+ requiredBytes?: number;
66
+ networkProbe?: () => Promise<boolean>;
67
+ }): Promise<SetupPreflightV1>;
68
+ export declare function installProjectFromSetup({ payloadRoot, target, networkProbe }: {
69
+ payloadRoot: string;
70
+ target: string;
71
+ networkProbe?: () => Promise<boolean>;
72
+ }): Promise<SetupInstallationReceiptV1>;
73
+ export declare function recoverProjectSetup({ payloadRoot, target, networkProbe }: {
74
+ payloadRoot: string;
75
+ target: string;
76
+ networkProbe?: () => Promise<boolean>;
77
+ }): Promise<SetupInstallationReceiptV1>;
78
+ /**
79
+ * 将已经编译的唯一 initializer 及其运行时依赖放入 payload bootstrap。
80
+ * 这里不重写或复制 initializeLocalAuthorProject;run-initializer 只负责把
81
+ * 固定 Node/npm 与已验证 tarball source 传给该 public API。
82
+ */
83
+ export declare function prepareInitializerBootstrap({ outputDirectory, projectSetupDirectory, createProjectDirectory, projectFormatDirectory, agentKitDirectory }: {
84
+ outputDirectory: string;
85
+ projectSetupDirectory: string;
86
+ createProjectDirectory: string;
87
+ projectFormatDirectory: string;
88
+ agentKitDirectory: string;
89
+ }): Promise<string>;
90
+ export declare function buildSetupPayload({ outputRoot, runtimeDirectory, bootstrapDirectory, packageSource, releaseIdentity, nodeVersion }: {
91
+ outputRoot: string;
92
+ runtimeDirectory: string;
93
+ bootstrapDirectory: string;
94
+ packageSource: string;
95
+ releaseIdentity: string;
96
+ nodeVersion: string;
97
+ }): Promise<SetupPayloadManifestV1>;
@@ -0,0 +1,265 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
+ import { TOOLCHAIN_MANIFEST_FILE, readToolchainManifest } from "./toolchain-manifest.js";
6
+ export const SETUP_PAYLOAD_SCHEMA_VERSION = 1;
7
+ const SHA256 = /^[a-f0-9]{64}$/;
8
+ export class ProjectSetupError extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(`${code}: ${message}`);
12
+ this.code = code;
13
+ }
14
+ }
15
+ function fail(code, message) { throw new ProjectSetupError(code, message); }
16
+ function hash(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
17
+ function isWithin(path, root) { const result = relative(root, path); return result === "" || (!result.startsWith("..\\") && result !== ".." && !result.startsWith("../")); }
18
+ async function regularFile(path, label) {
19
+ const canonical = await realpath(path).catch(() => fail("E_PAYLOAD_INVALID", `${label} 不存在: ${path}`));
20
+ const info = await lstat(canonical);
21
+ if (!info.isFile() || info.isSymbolicLink())
22
+ fail("E_PAYLOAD_INVALID", `${label} 必须是常规文件`);
23
+ return canonical;
24
+ }
25
+ async function regularDirectory(path, label) {
26
+ const canonical = await realpath(path).catch(() => fail("E_PAYLOAD_INVALID", `${label} 不存在: ${path}`));
27
+ const info = await lstat(canonical);
28
+ if (!info.isDirectory() || info.isSymbolicLink())
29
+ fail("E_PAYLOAD_INVALID", `${label} 必须是非 reparse 目录`);
30
+ return canonical;
31
+ }
32
+ async function canonicalDirectoryDigest(directory) {
33
+ const root = await regularDirectory(directory, "目录");
34
+ const entries = [];
35
+ async function walk(current) {
36
+ for (const entry of (await readdir(current, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
37
+ const candidate = join(current, entry.name);
38
+ if (entry.isDirectory()) {
39
+ await walk(candidate);
40
+ continue;
41
+ }
42
+ if (!entry.isFile() || entry.isSymbolicLink())
43
+ fail("E_PAYLOAD_INVALID", `payload 含非普通文件: ${candidate}`);
44
+ const bytes = await readFile(candidate);
45
+ entries.push({ path: relative(root, candidate).replaceAll("\\", "/"), sha256: hash(bytes), size_bytes: bytes.length });
46
+ }
47
+ }
48
+ await walk(root);
49
+ return hash(Buffer.from(JSON.stringify(entries)));
50
+ }
51
+ // 与 @world-engines/project-format 的 computeCanonicalFileTreeDigest 保持逐字节一致;
52
+ // package source 是 create-project 的公开输入合同,不能另造一个近似 hash 算法。
53
+ async function canonicalPackageSourceDigest(directory) {
54
+ const root = await regularDirectory(directory, "package source");
55
+ const entries = [];
56
+ for (const entry of await readdir(root, { withFileTypes: true })) {
57
+ if (!entry.isFile() || entry.isSymbolicLink())
58
+ fail("E_PAYLOAD_INVALID", "package source 只能含普通 .tgz 文件与 ToolchainManifest");
59
+ if (entry.name === TOOLCHAIN_MANIFEST_FILE)
60
+ continue;
61
+ if (!entry.name.endsWith(".tgz"))
62
+ fail("E_PAYLOAD_INVALID", "package source 只能含普通 .tgz 文件与 ToolchainManifest");
63
+ entries.push({ path: entry.name, bytes: new Uint8Array(await readFile(join(root, entry.name))) });
64
+ }
65
+ entries.sort((left, right) => Buffer.compare(Buffer.from(left.path, "utf8"), Buffer.from(right.path, "utf8")));
66
+ const encoder = new TextEncoder();
67
+ const length = entries.reduce((total, entry) => total + 4 + encoder.encode(entry.path).byteLength + 8 + entry.bytes.byteLength, 0);
68
+ const canonical = new Uint8Array(length);
69
+ const view = new DataView(canonical.buffer);
70
+ let offset = 0;
71
+ for (const entry of entries) {
72
+ const pathBytes = encoder.encode(entry.path);
73
+ view.setUint32(offset, pathBytes.byteLength);
74
+ offset += 4;
75
+ canonical.set(pathBytes, offset);
76
+ offset += pathBytes.byteLength;
77
+ view.setBigUint64(offset, BigInt(entry.bytes.byteLength));
78
+ offset += 8;
79
+ canonical.set(entry.bytes, offset);
80
+ offset += entry.bytes.byteLength;
81
+ }
82
+ return hash(canonical);
83
+ }
84
+ async function readManifest(payloadRoot) {
85
+ const raw = await readFile(join(payloadRoot, "setup-payload-manifest.json"), "utf8").catch(() => fail("E_PAYLOAD_INVALID", "缺少 setup-payload-manifest.json"));
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(raw);
89
+ }
90
+ catch {
91
+ fail("E_PAYLOAD_INVALID", "payload manifest 不是 JSON");
92
+ }
93
+ const manifest = parsed;
94
+ if (manifest?.schema_version !== SETUP_PAYLOAD_SCHEMA_VERSION || typeof manifest.release_identity !== "string" || manifest.release_identity.length === 0)
95
+ fail("E_PAYLOAD_INVALID", "payload identity 无效");
96
+ return manifest;
97
+ }
98
+ export async function inspectSetupPayload(payloadRoot) {
99
+ const root = await regularDirectory(payloadRoot, "setup payload 根");
100
+ const manifest = await readManifest(root);
101
+ for (const [label, item] of Object.entries({ node: manifest.node, create_project_tgz: manifest.create_project_tgz, bootstrap: manifest.bootstrap })) {
102
+ if (!item || typeof item.path !== "string" || !SHA256.test(item.sha256))
103
+ fail("E_PAYLOAD_INVALID", `${label} manifest 项无效`);
104
+ const candidate = resolve(root, item.path);
105
+ if (!isWithin(candidate, root))
106
+ fail("E_PAYLOAD_INVALID", `${label} 路径逃离 payload`);
107
+ if (label === "bootstrap") {
108
+ if (await canonicalDirectoryDigest(candidate) !== item.sha256)
109
+ fail("E_PAYLOAD_INVALID", "bootstrap SHA-256 不匹配");
110
+ }
111
+ else if (hash(await readFile(await regularFile(candidate, label))) !== item.sha256)
112
+ fail("E_PAYLOAD_INVALID", `${label} SHA-256 不匹配`);
113
+ }
114
+ if (!SHA256.test(manifest.node.runtime_sha256) || await canonicalDirectoryDigest(resolve(root, "runtime")) !== manifest.node.runtime_sha256)
115
+ fail("E_PAYLOAD_INVALID", "固定 Node runtime SHA-256 不匹配");
116
+ await regularFile(resolve(root, "runtime", "node_modules", "npm", "bin", "npm-cli.js"), "固定 Node npm CLI");
117
+ if (!manifest.package_source || typeof manifest.package_source.path !== "string" || !SHA256.test(manifest.package_source.sha256))
118
+ fail("E_PAYLOAD_INVALID", "package_source manifest 项无效");
119
+ const source = resolve(root, manifest.package_source.path);
120
+ if (!isWithin(source, root) || await canonicalPackageSourceDigest(source) !== manifest.package_source.sha256)
121
+ fail("E_PAYLOAD_INVALID", "package_source SHA-256 不匹配");
122
+ if (!manifest.toolchain_manifest || typeof manifest.toolchain_manifest.path !== "string" || !SHA256.test(manifest.toolchain_manifest.sha256))
123
+ fail("E_PAYLOAD_INVALID", "toolchain_manifest manifest 项无效");
124
+ const toolchainPath = resolve(root, manifest.toolchain_manifest.path);
125
+ if (!isWithin(toolchainPath, source) || basename(toolchainPath) !== TOOLCHAIN_MANIFEST_FILE || hash(await readFile(await regularFile(toolchainPath, "toolchain_manifest"))) !== manifest.toolchain_manifest.sha256)
126
+ fail("E_PAYLOAD_INVALID", "toolchain_manifest SHA-256 不匹配");
127
+ try {
128
+ await readToolchainManifest(source);
129
+ }
130
+ catch (error) {
131
+ fail("E_PAYLOAD_INVALID", `toolchain_manifest 无效: ${error instanceof Error ? error.message : String(error)}`);
132
+ }
133
+ return manifest;
134
+ }
135
+ export async function preflightSetup({ payloadRoot, target, requiredBytes = 128 * 1024 * 1024, networkProbe }) {
136
+ const manifest = await inspectSetupPayload(payloadRoot);
137
+ const canonicalTarget = resolve(target);
138
+ let targetExists = false;
139
+ try {
140
+ const info = await stat(canonicalTarget);
141
+ if (!info.isDirectory())
142
+ fail("E_TARGET_INVALID", "目标必须是目录或不存在的目录");
143
+ if ((await readdir(canonicalTarget)).length !== 0)
144
+ fail("E_TARGET_NOT_EMPTY", "目标目录非空");
145
+ targetExists = true;
146
+ }
147
+ catch (error) {
148
+ if (error.code !== "ENOENT")
149
+ throw error;
150
+ }
151
+ const online = networkProbe === undefined ? true : await networkProbe().catch(() => false);
152
+ return { schema_version: 1, target: canonicalTarget, payload_identity: manifest.release_identity, directory: { status: "ready", target_exists: targetExists }, network: { status: online ? "ready" : "unavailable", required_for_dependency_install: true }, disk: { status: "ready", required_bytes: requiredBytes, available_bytes: null } };
153
+ }
154
+ async function writeRecovery(path, receipt) { await writeFile(path, `${JSON.stringify(receipt, null, 2)}\n`, "utf8"); }
155
+ async function runNode(node, args, cwd) {
156
+ const result = await new Promise((resolvePromise, reject) => {
157
+ const child = spawn(node, args, { cwd, windowsHide: true, shell: false, stdio: ["ignore", "pipe", "pipe"] });
158
+ let stdout = "";
159
+ let stderr = "";
160
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
161
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
162
+ child.once("error", reject);
163
+ child.once("close", (code) => code === 0 ? resolvePromise(stdout) : reject(new ProjectSetupError("E_INITIALIZER_FAILED", `${stderr.trim() || `initializer 退出码 ${code}`}`)));
164
+ });
165
+ // npm 会由唯一 initializer 继承 stdout 输出进度;最后一行才是它的 receipt。
166
+ const receiptLine = result.trim().split(/\r?\n/).filter(Boolean).at(-1);
167
+ try {
168
+ return JSON.parse(receiptLine ?? "");
169
+ }
170
+ catch {
171
+ fail("E_INITIALIZER_FAILED", "initializer 未返回 JSON receipt");
172
+ }
173
+ }
174
+ export async function installProjectFromSetup({ payloadRoot, target, networkProbe }) {
175
+ const preflight = await preflightSetup(networkProbe === undefined ? { payloadRoot, target } : { payloadRoot, target, networkProbe });
176
+ if (preflight.network.status !== "ready")
177
+ fail("E_NETWORK_UNAVAILABLE", "依赖安装需要网络;恢复时请重新运行 setup recover");
178
+ const root = await regularDirectory(payloadRoot, "setup payload 根");
179
+ const manifest = await inspectSetupPayload(root);
180
+ const recoveryPath = join(dirname(preflight.target), `.${resolve(preflight.target).split(/[\\/]/).pop()}.worldengine-setup-recovery.json`);
181
+ const progress = ["preflight:directory", "preflight:network", "preflight:disk", "initializer:starting"];
182
+ const receiptBase = { schema_version: 1, operation_id: `setup_${randomUUID().replaceAll("-", "")}`, payload_identity: manifest.release_identity, target: preflight.target, recovery_path: recoveryPath };
183
+ try {
184
+ const initializerReceipt = await runNode(resolve(root, manifest.node.path), [join(resolve(root, manifest.bootstrap.path), "run-initializer.mjs"), "--target", preflight.target, "--node", resolve(root, manifest.node.path), "--package-source", resolve(root, manifest.package_source.path), "--package-source-sha256", manifest.package_source.sha256], root);
185
+ const receipt = { ...receiptBase, status: "ready", progress: [...progress, "initializer:ready"], initializer_receipt: initializerReceipt };
186
+ await mkdir(join(preflight.target, ".worldengine", "operations"), { recursive: true });
187
+ await writeRecovery(join(preflight.target, ".worldengine", "operations", "windows-setup.json"), receipt);
188
+ return receipt;
189
+ }
190
+ catch (error) {
191
+ const receipt = { ...receiptBase, status: "recoverable_failure", progress: [...progress, "initializer:recoverable_failure"] };
192
+ await writeRecovery(recoveryPath, receipt);
193
+ throw error;
194
+ }
195
+ }
196
+ export async function recoverProjectSetup({ payloadRoot, target, networkProbe }) {
197
+ const receipt = await installProjectFromSetup(networkProbe === undefined ? { payloadRoot, target } : { payloadRoot, target, networkProbe });
198
+ await rm(receipt.recovery_path, { force: true });
199
+ return receipt;
200
+ }
201
+ /**
202
+ * 将已经编译的唯一 initializer 及其运行时依赖放入 payload bootstrap。
203
+ * 这里不重写或复制 initializeLocalAuthorProject;run-initializer 只负责把
204
+ * 固定 Node/npm 与已验证 tarball source 传给该 public API。
205
+ */
206
+ export async function prepareInitializerBootstrap({ outputDirectory, projectSetupDirectory, createProjectDirectory, projectFormatDirectory, agentKitDirectory }) {
207
+ const output = resolve(outputDirectory);
208
+ await mkdir(output, { recursive: true });
209
+ if ((await readdir(output)).length !== 0)
210
+ fail("E_OUTPUT_NOT_EMPTY", "bootstrap 输出目录必须为空");
211
+ const packages = [
212
+ ["project-setup", projectSetupDirectory],
213
+ ["create-project", createProjectDirectory],
214
+ ["project-format", projectFormatDirectory],
215
+ ["agent-kit", agentKitDirectory],
216
+ ];
217
+ for (const [name, directory] of packages) {
218
+ const source = await regularDirectory(directory, `bootstrap ${name}`);
219
+ await regularFile(join(source, "dist", "index.js"), `bootstrap ${name} dist`);
220
+ const destination = join(output, "node_modules", "@world-engines", name);
221
+ await mkdir(destination, { recursive: true });
222
+ await copyFile(join(source, "package.json"), join(destination, "package.json"));
223
+ await cp(join(source, "dist"), join(destination, "dist"), { recursive: true, force: false });
224
+ try {
225
+ await copyFile(join(source, "LICENSE"), join(destination, "LICENSE"));
226
+ }
227
+ catch (error) {
228
+ if (error.code !== "ENOENT")
229
+ throw error;
230
+ }
231
+ if (name === "agent-kit") {
232
+ await cp(join(source, "assets"), join(destination, "assets"), { recursive: true, force: false });
233
+ }
234
+ }
235
+ const runner = `import { initializeLocalAuthorProject } from "@world-engines/create-project";\nimport { dirname, join } from "node:path";\nconst read = (name) => { const index = process.argv.indexOf(name); const value = index < 0 ? undefined : process.argv[index + 1]; if (!value || value.startsWith("--")) throw new Error(name + " is required"); return value; };\nconst node = read("--node");\nconst receipt = await initializeLocalAuthorProject({ target: read("--target"), npm_executable: node, npm_arguments_prefix: [join(dirname(node), "node_modules", "npm", "bin", "npm-cli.js")], package_source: { tarball_directory: read("--package-source"), tarball_directory_sha256: read("--package-source-sha256") } });\nprocess.stdout.write(JSON.stringify(receipt));\n`;
236
+ await writeFile(join(output, "run-initializer.mjs"), runner, { flag: "wx" });
237
+ const setupRunner = `import { installProjectFromSetup } from "@world-engines/project-setup";\nconst read = (name) => { const index = process.argv.indexOf(name); const value = index < 0 ? undefined : process.argv[index + 1]; if (!value || value.startsWith("--")) throw new Error(name + " is required"); return value; };\nconst receipt = await installProjectFromSetup({ payloadRoot: read("--payload-root"), target: read("--target") });\nprocess.stdout.write(JSON.stringify(receipt));\n`;
238
+ await writeFile(join(output, "run-setup.mjs"), setupRunner, { flag: "wx" });
239
+ return output;
240
+ }
241
+ export async function buildSetupPayload({ outputRoot, runtimeDirectory, bootstrapDirectory, packageSource, releaseIdentity, nodeVersion }) {
242
+ const destination = resolve(outputRoot);
243
+ await mkdir(destination, { recursive: true });
244
+ if ((await readdir(destination)).length !== 0)
245
+ fail("E_OUTPUT_NOT_EMPTY", "setup payload 输出目录必须为空");
246
+ const runtime = await regularDirectory(runtimeDirectory, "固定 Node runtime");
247
+ await regularFile(join(runtime, "node.exe"), "固定 Node executable");
248
+ await regularFile(join(runtime, "node_modules", "npm", "bin", "npm-cli.js"), "固定 Node npm CLI");
249
+ const source = await regularDirectory(packageSource, "package source");
250
+ const toolchain = await readToolchainManifest(source).catch((error) => fail("E_PAYLOAD_INVALID", `toolchain_manifest 无效: ${error instanceof Error ? error.message : String(error)}`));
251
+ const bootstrapSource = await regularDirectory(bootstrapDirectory, "initializer bootstrap");
252
+ await regularFile(join(bootstrapSource, "run-initializer.mjs"), "initializer bootstrap entry");
253
+ await mkdir(join(destination, "runtime"));
254
+ await mkdir(join(destination, "packages"));
255
+ await cp(runtime, join(destination, "runtime"), { recursive: true, force: false });
256
+ await cp(source, join(destination, "packages"), { recursive: true, force: false });
257
+ await cp(bootstrapSource, join(destination, "bootstrap"), { recursive: true, force: false });
258
+ const createProject = toolchain.packages.find((item) => item.name === "@world-engines/create-project");
259
+ if (createProject === undefined)
260
+ fail("E_PAYLOAD_INVALID", "toolchain manifest 缺少 create-project.tgz");
261
+ const manifest = { schema_version: 1, release_identity: releaseIdentity, node: { path: "runtime/node.exe", sha256: hash(await readFile(join(destination, "runtime", "node.exe"))), runtime_sha256: await canonicalDirectoryDigest(join(destination, "runtime")), version: nodeVersion }, create_project_tgz: { path: `packages/${createProject.archive}`, sha256: createProject.sha256 }, package_source: { path: "packages", sha256: await canonicalPackageSourceDigest(join(destination, "packages")) }, toolchain_manifest: { path: `packages/${TOOLCHAIN_MANIFEST_FILE}`, sha256: hash(await readFile(join(destination, "packages", TOOLCHAIN_MANIFEST_FILE))) }, bootstrap: { path: "bootstrap", sha256: await canonicalDirectoryDigest(join(destination, "bootstrap")) } };
262
+ await writeFile(join(destination, "setup-payload-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
263
+ await inspectSetupPayload(destination);
264
+ return manifest;
265
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { initializeLocalAuthorProject } from "@world-engines/create-project";
3
+ import { dirname, join } from "node:path";
4
+ function option(name) {
5
+ const index = process.argv.indexOf(name);
6
+ const value = index < 0 ? undefined : process.argv[index + 1];
7
+ if (value === undefined || value.startsWith("--"))
8
+ throw new Error(`${name} is required`);
9
+ return value;
10
+ }
11
+ const target = option("--target");
12
+ const node = option("--node");
13
+ const source = option("--package-source");
14
+ const sourceHash = option("--package-source-sha256");
15
+ const npmCli = join(dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
16
+ initializeLocalAuthorProject({
17
+ target,
18
+ npm_executable: node,
19
+ npm_arguments_prefix: [npmCli],
20
+ package_source: { tarball_directory: source, tarball_directory_sha256: sourceHash },
21
+ }).then((receipt) => process.stdout.write(`${JSON.stringify(receipt)}\n`));
@@ -0,0 +1,16 @@
1
+ export interface WorldEngineNodePackage {
2
+ readonly name: string;
3
+ readonly directory: string;
4
+ /** 未指定时是原生 Node 入口;browser 必须通过隔离 Vite consumer 构建。 */
5
+ readonly runtime?: "browser";
6
+ /** 只约束 release build 顺序,不进入公开 runtime dependency。 */
7
+ readonly build_dependencies?: readonly string[];
8
+ }
9
+ export declare const WORLDENGINE_NODE_PACKAGE_CATALOG: readonly Readonly<{
10
+ name: string;
11
+ directory: string;
12
+ runtime?: "browser";
13
+ build_dependencies?: readonly string[];
14
+ }>[];
15
+ export declare const WORLDENGINE_NODE_PACKAGE_NAMES: readonly string[];
16
+ export type WorldEngineNodePackageName = string;