@playdrop/playdrop-cli 0.17.4 → 0.17.14

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 (81) hide show
  1. package/README.md +12 -0
  2. package/config/client-meta.json +4 -4
  3. package/dist/apps/build.d.ts +2 -1
  4. package/dist/apps/build.js +6 -2
  5. package/dist/apps/index.d.ts +2 -3
  6. package/dist/apps/index.js +17 -49
  7. package/dist/apps/loadCheck.d.ts +3 -1
  8. package/dist/apps/loadCheck.js +123 -64
  9. package/dist/apps/publishingRuntime.d.ts +18 -0
  10. package/dist/apps/publishingRuntime.js +111 -0
  11. package/dist/apps/runtimeSyntax.d.ts +7 -0
  12. package/dist/apps/runtimeSyntax.js +154 -0
  13. package/dist/apps/serverBuild.d.ts +3 -3
  14. package/dist/apps/serverBuild.js +8 -43
  15. package/dist/apps/serverSdk.d.ts +23 -0
  16. package/dist/apps/serverSdk.js +157 -0
  17. package/dist/apps/upload.d.ts +9 -3
  18. package/dist/apps/upload.js +71 -25
  19. package/dist/apps/validate.d.ts +2 -1
  20. package/dist/apps/validate.js +3 -1
  21. package/dist/assetSpecs.js +2 -0
  22. package/dist/commandContext.js +34 -10
  23. package/dist/commands/build.js +3 -1
  24. package/dist/commands/create.js +35 -16
  25. package/dist/commands/dev.js +11 -0
  26. package/dist/commands/devGameServer.d.ts +8 -0
  27. package/dist/commands/devGameServer.js +12 -9
  28. package/dist/commands/devServer.d.ts +1 -0
  29. package/dist/commands/devServer.js +21 -3
  30. package/dist/commands/source.d.ts +11 -3
  31. package/dist/commands/source.js +82 -14
  32. package/dist/commands/taskCaptureSession.d.ts +1 -1
  33. package/dist/commands/taskCaptureSession.js +1 -1
  34. package/dist/commands/upload-content.js +15 -12
  35. package/dist/commands/upload.d.ts +2 -9
  36. package/dist/commands/upload.js +5 -21
  37. package/dist/commands/validate.js +1 -1
  38. package/dist/commands/worker/classification.d.ts +1 -0
  39. package/dist/commands/worker/classification.js +57 -0
  40. package/dist/commands/worker/game-server-infra.d.ts +6 -0
  41. package/dist/commands/worker/game-server-infra.js +10 -0
  42. package/dist/commands/worker/game-source-git.d.ts +9 -1
  43. package/dist/commands/worker/game-source-git.js +65 -19
  44. package/dist/commands/worker/generation.d.ts +4 -0
  45. package/dist/commands/worker/generation.js +70 -7
  46. package/dist/commands/worker.d.ts +1 -1
  47. package/dist/commands/worker.js +57 -17
  48. package/dist/environment.d.ts +1 -0
  49. package/dist/environment.js +7 -0
  50. package/dist/index.js +10 -2
  51. package/dist/playwright.d.ts +6 -4
  52. package/dist/playwright.js +24 -9
  53. package/dist/proceduralValidation.d.ts +16 -0
  54. package/dist/proceduralValidation.js +227 -0
  55. package/dist/proceduralValidationBrowser.d.mts +3 -0
  56. package/dist/proceduralValidationBrowser.mjs +67 -0
  57. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  58. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -1
  59. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts +6 -1
  60. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts.map +1 -1
  61. package/node_modules/@playdrop/api-client/dist/domains/chat.js +13 -1
  62. package/node_modules/@playdrop/config/client-meta.json +4 -4
  63. package/node_modules/@playdrop/procedural/dist/index.d.ts +201 -0
  64. package/node_modules/@playdrop/procedural/dist/index.d.ts.map +1 -0
  65. package/node_modules/@playdrop/procedural/dist/index.js +763 -0
  66. package/node_modules/@playdrop/procedural/package.json +36 -0
  67. package/node_modules/@playdrop/types/dist/agent-task-classification.d.ts +68 -0
  68. package/node_modules/@playdrop/types/dist/agent-task-classification.d.ts.map +1 -0
  69. package/node_modules/@playdrop/types/dist/agent-task-classification.js +96 -0
  70. package/node_modules/@playdrop/types/dist/api.d.ts +39 -4
  71. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  72. package/node_modules/@playdrop/types/dist/chat.d.ts +7 -1
  73. package/node_modules/@playdrop/types/dist/chat.d.ts.map +1 -1
  74. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  75. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  76. package/node_modules/@playdrop/types/dist/index.js +2 -0
  77. package/node_modules/@playdrop/types/dist/social.d.ts +20 -0
  78. package/node_modules/@playdrop/types/dist/social.d.ts.map +1 -1
  79. package/node_modules/@playdrop/types/dist/version.d.ts +2 -0
  80. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  81. package/package.json +7 -4
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startPublishingRuntime = startPublishingRuntime;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const appUrls_1 = require("../appUrls");
8
+ const devGameServer_1 = require("../commands/devGameServer");
9
+ const devServer_1 = require("../commands/devServer");
10
+ const game_server_infra_1 = require("../commands/worker/game-server-infra");
11
+ async function startPublishingRuntime(input) {
12
+ if (!Number.isSafeInteger(input.appId) || input.appId <= 0) {
13
+ throw new Error("publishing_game_server_app_missing: Publishing requires the registered game's exact app ID.");
14
+ }
15
+ if (!input.task.server || !input.task.version || !input.task.filePath || !input.apiBase?.trim()) {
16
+ throw new Error("publishing_game_server_context_missing: Game server validation requires the built game, exact version, and API address.");
17
+ }
18
+ const credential = await input.client.requestDevGameServerCredential(input.appId, { version: input.task.version });
19
+ let ownedServices = null;
20
+ let router = null;
21
+ let gameServer = null;
22
+ let mount = null;
23
+ let frozenDir = null;
24
+ let closed = false;
25
+ const close = async () => {
26
+ if (closed)
27
+ return;
28
+ closed = true;
29
+ const stopped = await Promise.allSettled([
30
+ mount ? mount.close() : (0, devServer_1.terminateChildProcessTree)(gameServer?.process ?? null),
31
+ ]);
32
+ // The mount unregisters over HTTP, so its router must still be running.
33
+ const routerStopped = await Promise.allSettled([(0, devServer_1.terminateChildProcessTree)(router)]);
34
+ // A Cloud task owns its lease beyond this publishing attempt. A direct
35
+ // publication owns only the temporary lease created here.
36
+ const cleaned = await Promise.allSettled([
37
+ ownedServices?.cleanup(),
38
+ frozenDir ? (0, promises_1.rm)(frozenDir, { recursive: true, force: true }) : undefined,
39
+ ]);
40
+ const failures = [...stopped, ...routerStopped, ...cleaned].filter((result) => result.status === "rejected");
41
+ if (failures.length) {
42
+ throw Object.assign(new Error("publishing_game_server_cleanup_failed: Could not clean up this publishing job's temporary resources."), {
43
+ errors: failures.map((result) => result.reason),
44
+ });
45
+ }
46
+ };
47
+ try {
48
+ let services = input.gameServerDev;
49
+ if (!services) {
50
+ const redisAdminUrl = process.env.PLAYDROP_WORKER_GAME_SERVER_REDIS_ADMIN_URL?.trim();
51
+ const mongoAdminUrl = process.env.PLAYDROP_DEV_MONGO_URL?.trim();
52
+ if (!redisAdminUrl || !mongoAdminUrl) {
53
+ throw new Error("publishing_game_server_services_missing: Start the local PlayDrop platform and provide its Redis administration and MongoDB settings. Cloud must pass this task's database settings to publishing.");
54
+ }
55
+ ownedServices = await (0, game_server_infra_1.provisionPublishingGameServerDevLease)({
56
+ sessionId: input.sessionId, environment: credential.environment, redisAdminUrl, mongoAdminUrl,
57
+ });
58
+ services = ownedServices;
59
+ }
60
+ if (!services.redisUrl.startsWith("redis://") || !services.mongoUrl.startsWith("mongodb://") || !services.redisNamespace) {
61
+ throw new Error("publishing_game_server_services_invalid: This publishing job has incomplete database or server settings.");
62
+ }
63
+ // Keep the task's database and ACL, but never match into a room belonging
64
+ // to its still-running development server or an earlier publishing check.
65
+ const publishingServices = {
66
+ ...services, redisNamespace: `${services.redisNamespace}upload-${(0, node_crypto_1.randomUUID)()}:`,
67
+ };
68
+ const [bundle, manifest] = await Promise.all([
69
+ (0, promises_1.readFile)(input.gameServerBuild.bundlePath), (0, promises_1.readFile)(input.gameServerBuild.manifestPath),
70
+ ]);
71
+ if ((0, node_crypto_1.createHash)("sha256").update(bundle).digest("hex") !== input.gameServerBuild.manifest.sha256 ||
72
+ JSON.stringify(JSON.parse(manifest.toString("utf8"))) !== JSON.stringify(input.gameServerBuild.manifest)) {
73
+ throw new Error("publishing_game_server_changed: The game server changed after upload preparation. Finish editing before publishing again.");
74
+ }
75
+ await (0, promises_1.mkdir)((0, node_path_1.join)(input.task.projectDir, ".playdrop"), { recursive: true });
76
+ frozenDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)(input.task.projectDir, ".playdrop", "publishing-"));
77
+ const frozenBuild = {
78
+ ...input.gameServerBuild,
79
+ bundlePath: (0, node_path_1.join)(frozenDir, "bundle.mjs"), manifestPath: (0, node_path_1.join)(frozenDir, "manifest.json"),
80
+ };
81
+ await Promise.all([(0, promises_1.writeFile)(frozenBuild.bundlePath, bundle), (0, promises_1.writeFile)(frozenBuild.manifestPath, manifest)]);
82
+ const localDevPort = await (0, devGameServer_1.reserveLocalGameServerPort)();
83
+ router = await (0, devServer_1.startOwnedDevRouter)(localDevPort);
84
+ const appType = (0, appUrls_1.getAppTypeSlug)(input.task.type ?? "GAME");
85
+ const appPath = `/apps/dev/${encodeURIComponent(input.creatorUsername)}/${appType}/${encodeURIComponent(input.task.name)}`;
86
+ gameServer = await (0, devGameServer_1.startDevGameServer)({
87
+ task: input.task, credential, platformApiUrl: input.apiBase,
88
+ publicAddress: `127.0.0.1:${localDevPort}${appPath}/_playdrop/rooms`,
89
+ gameServerDev: publishingServices, gameServerBuild: frozenBuild,
90
+ });
91
+ if (!gameServer)
92
+ throw new Error("publishing_game_server_missing");
93
+ mount = await (0, devServer_1.startDevServer)({
94
+ creatorUsername: input.creatorUsername, appName: input.task.name, appType,
95
+ htmlPath: input.task.filePath, port: localDevPort,
96
+ projectInfo: { projectDir: input.task.projectDir, packageJsonPath: input.task.packageJsonPath ?? null },
97
+ roomsPort: gameServer.port, roomsProcess: gameServer.process,
98
+ });
99
+ console.log(`[upload] Game server ready for ${input.creatorUsername}/${input.task.name}, publishing check ${input.sessionId}.`);
100
+ return { localDevPort, close };
101
+ }
102
+ catch (error) {
103
+ try {
104
+ await close();
105
+ }
106
+ catch (cleanupError) {
107
+ console.error("[upload] Publishing runtime cleanup also failed:", cleanupError);
108
+ }
109
+ throw error;
110
+ }
111
+ }
@@ -0,0 +1,7 @@
1
+ type RuntimeFile = {
2
+ absolutePath: string;
3
+ relativePath: string;
4
+ };
5
+ /** Parse the exact runtime files after packaging, without evaluating game code. */
6
+ export declare function validateRuntimeSyntax(files: RuntimeFile[]): Promise<void>;
7
+ export {};
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateRuntimeSyntax = validateRuntimeSyntax;
4
+ const esbuild_1 = require("esbuild");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_vm_1 = require("node:vm");
7
+ const parse5_1 = require("parse5");
8
+ const JAVASCRIPT_TYPES = new Set([
9
+ "application/ecmascript",
10
+ "application/javascript",
11
+ "application/x-ecmascript",
12
+ "application/x-javascript",
13
+ "text/ecmascript",
14
+ "text/javascript",
15
+ "text/javascript1.0",
16
+ "text/javascript1.1",
17
+ "text/javascript1.2",
18
+ "text/javascript1.3",
19
+ "text/javascript1.4",
20
+ "text/javascript1.5",
21
+ "text/jscript",
22
+ "text/livescript",
23
+ "text/x-ecmascript",
24
+ "text/x-javascript",
25
+ ]);
26
+ const LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/;
27
+ function syntaxError(source, reason, line, column) {
28
+ const fileLine = source.lineOffset + line;
29
+ const fileColumn = column === null ? null : column + (line === 1 ? source.columnOffset : 0);
30
+ const lineText = source.fileContent.split(LINE_BREAK)[fileLine - 1] ?? "";
31
+ const excerptStart = Math.max(0, (fileColumn ?? 0) - 70);
32
+ const excerptEnd = Math.min(lineText.length, excerptStart + 180);
33
+ const prefix = excerptStart > 0 ? "..." : "";
34
+ const excerpt = `${prefix}${lineText.slice(excerptStart, excerptEnd)}${excerptEnd < lineText.length ? "..." : ""}`;
35
+ const caret = fileColumn === null ? "" : `${" ".repeat(prefix.length + Math.max(0, fileColumn - excerptStart))}^`;
36
+ const location = `${source.file.absolutePath}:${fileLine}${fileColumn === null ? "" : `:${fileColumn + 1}`}`;
37
+ return new Error([
38
+ `app_bundle_syntax_invalid: ${location}: ${reason}`,
39
+ excerpt,
40
+ caret,
41
+ "The packaged JavaScript is invalid. Fix the final build output before upload; a successful compiler/build command is not sufficient.",
42
+ ]
43
+ .filter(Boolean)
44
+ .join("\n"));
45
+ }
46
+ async function validateScript(source) {
47
+ let classicError = null;
48
+ if (source.mode === "classic") {
49
+ try {
50
+ // Constructing Script only parses code, never executes it. Its native
51
+ // browser-style error avoids mislabeling corrupt HTML as unsupported JSX.
52
+ new node_vm_1.Script(source.code, { filename: source.file.relativePath });
53
+ return;
54
+ }
55
+ catch (error) {
56
+ if (!(error instanceof SyntaxError))
57
+ throw error;
58
+ classicError = error;
59
+ }
60
+ }
61
+ try {
62
+ // esbuild uses .mjs to enforce module grammar, including strict mode and the
63
+ // rejection of legacy HTML comments. Transform output is discarded, never run.
64
+ await (0, esbuild_1.transform)(source.code, {
65
+ loader: "js",
66
+ sourcefile: `${source.file.relativePath}.${source.mode === "module" ? "mjs" : "js"}`,
67
+ target: "esnext",
68
+ logLevel: "silent",
69
+ });
70
+ }
71
+ catch (error) {
72
+ const diagnostic = error.errors?.[0];
73
+ if (!diagnostic)
74
+ throw error;
75
+ const location = diagnostic.location;
76
+ const column = location
77
+ ? Buffer.from(location.lineText).subarray(0, location.column).toString("utf8").length
78
+ : null;
79
+ throw syntaxError(source, classicError?.message ?? diagnostic.text, location?.line ?? 1, column);
80
+ }
81
+ if (classicError) {
82
+ // esbuild accepts module/CommonJS constructs forbidden in classic scripts.
83
+ // A successful transform cannot override the native classic parse failure.
84
+ const stackLines = classicError.stack?.split("\n") ?? [];
85
+ const line = Number(stackLines[0]?.match(/:(\d+)$/)?.[1] ?? 1);
86
+ const caret = stackLines[2]?.indexOf("^") ?? -1;
87
+ throw syntaxError(source, classicError.message, line, caret < 0 ? null : caret);
88
+ }
89
+ }
90
+ function scriptsIn(node) {
91
+ const scripts = [];
92
+ if ("tagName" in node && node.tagName === "script")
93
+ scripts.push(node);
94
+ if ("childNodes" in node) {
95
+ for (const child of node.childNodes)
96
+ scripts.push(...scriptsIn(child));
97
+ }
98
+ return scripts;
99
+ }
100
+ /** Parse the exact runtime files after packaging, without evaluating game code. */
101
+ async function validateRuntimeSyntax(files) {
102
+ const filesByPath = new Map(files.map((file) => [file.relativePath, file]));
103
+ const validatedFiles = new Set();
104
+ const validateFile = async (file, mode) => {
105
+ const key = `${mode}:${file.relativePath}`;
106
+ if (validatedFiles.has(key))
107
+ return;
108
+ const code = (0, node_fs_1.readFileSync)(file.absolutePath, "utf8");
109
+ await validateScript({ file, code, fileContent: code, mode, lineOffset: 0, columnOffset: 0 });
110
+ validatedFiles.add(key);
111
+ };
112
+ for (const file of files) {
113
+ if (!/\.html?$/i.test(file.relativePath))
114
+ continue;
115
+ const html = (0, node_fs_1.readFileSync)(file.absolutePath, "utf8");
116
+ const document = (0, parse5_1.parse)(html, { sourceCodeLocationInfo: true });
117
+ for (const script of scriptsIn(document)) {
118
+ const attrs = new Map(script.attrs.map((attr) => [attr.name, attr.value]));
119
+ const type = attrs.get("type")?.trim().toLowerCase() ?? "";
120
+ if (type && type !== "module" && !JAVASCRIPT_TYPES.has(type))
121
+ continue;
122
+ const mode = type === "module" ? "module" : "classic";
123
+ if (attrs.has("src")) {
124
+ const url = new URL(attrs.get("src"), `https://playdrop-bundle.invalid/${file.relativePath}`);
125
+ if (url.origin !== "https://playdrop-bundle.invalid")
126
+ continue;
127
+ const referenced = filesByPath.get(decodeURIComponent(url.pathname.slice(1)));
128
+ if (referenced)
129
+ await validateFile(referenced, mode);
130
+ continue;
131
+ }
132
+ const location = script.sourceCodeLocation;
133
+ if (!location?.startTag)
134
+ continue;
135
+ await validateScript({
136
+ file,
137
+ code: html.slice(location.startTag.endOffset, location.endTag?.startOffset ?? location.endOffset),
138
+ fileContent: html,
139
+ mode,
140
+ lineOffset: location.startTag.endLine - 1,
141
+ columnOffset: location.startTag.endCol - 1,
142
+ });
143
+ }
144
+ }
145
+ for (const file of files) {
146
+ if (!/\.(?:js|mjs|cjs)$/i.test(file.relativePath))
147
+ continue;
148
+ if (validatedFiles.has(`classic:${file.relativePath}`) || validatedFiles.has(`module:${file.relativePath}`))
149
+ continue;
150
+ // Unreferenced .js files may be imported modules or classic worker scripts.
151
+ // esbuild detects their syntax without inventing a script loading mode.
152
+ await validateFile(file, /\.mjs$/i.test(file.relativePath) ? "module" : "auto");
153
+ }
154
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AppTask } from "../catalogue";
2
- export declare const GAME_SERVER_RUNTIME_VERSION: "colyseus-0.17-v1";
2
+ import { GAME_SERVER_RUNTIME_VERSION, type ServerSdkOptions } from "./serverSdk";
3
+ export { GAME_SERVER_RUNTIME_VERSION, verifyGameServerDependencies } from "./serverSdk";
3
4
  export declare const GAME_SERVER_BUNDLE_MAX_BYTES: number;
4
5
  export type GameServerManifest = {
5
6
  runtimeVersion: typeof GAME_SERVER_RUNTIME_VERSION;
@@ -12,6 +13,5 @@ export type GameServerBuild = {
12
13
  manifestPath: string;
13
14
  manifest: GameServerManifest;
14
15
  };
15
- export declare function verifyGameServerDependencies(task: AppTask): void;
16
- export declare function buildGameServer(task: AppTask): Promise<GameServerBuild | null>;
16
+ export declare function buildGameServer(task: AppTask, options?: ServerSdkOptions): Promise<GameServerBuild | null>;
17
17
  export declare function formatServerBuildPath(task: AppTask, absolutePath: string): string;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GAME_SERVER_BUNDLE_MAX_BYTES = exports.GAME_SERVER_RUNTIME_VERSION = void 0;
4
- exports.verifyGameServerDependencies = verifyGameServerDependencies;
3
+ exports.GAME_SERVER_BUNDLE_MAX_BYTES = exports.verifyGameServerDependencies = exports.GAME_SERVER_RUNTIME_VERSION = void 0;
5
4
  exports.buildGameServer = buildGameServer;
6
5
  exports.formatServerBuildPath = formatServerBuildPath;
7
6
  const node_crypto_1 = require("node:crypto");
@@ -9,16 +8,11 @@ const node_fs_1 = require("node:fs");
9
8
  const node_path_1 = require("node:path");
10
9
  const node_url_1 = require("node:url");
11
10
  const esbuild_1 = require("esbuild");
12
- exports.GAME_SERVER_RUNTIME_VERSION = "colyseus-0.17-v1";
11
+ const serverSdk_1 = require("./serverSdk");
12
+ var serverSdk_2 = require("./serverSdk");
13
+ Object.defineProperty(exports, "GAME_SERVER_RUNTIME_VERSION", { enumerable: true, get: function () { return serverSdk_2.GAME_SERVER_RUNTIME_VERSION; } });
14
+ Object.defineProperty(exports, "verifyGameServerDependencies", { enumerable: true, get: function () { return serverSdk_2.verifyGameServerDependencies; } });
13
15
  exports.GAME_SERVER_BUNDLE_MAX_BYTES = 2 * 1024 * 1024;
14
- const REQUIRED_PACKAGE_VERSIONS = Object.freeze({
15
- colyseus: "0.17.10",
16
- "@colyseus/core": "0.17.50",
17
- "@colyseus/schema": "4.0.30",
18
- mongodb: "7.5.0",
19
- "@playdrop/server": "0.1.0",
20
- typescript: "5.9.3",
21
- });
22
16
  const CREATOR_IMPORTS = new Set([
23
17
  "colyseus",
24
18
  "@colyseus/core",
@@ -26,35 +20,6 @@ const CREATOR_IMPORTS = new Set([
26
20
  "mongodb",
27
21
  "@playdrop/server",
28
22
  ]);
29
- function resolveInstalledPackageVersion(projectPackageJsonPath, packageName) {
30
- let current = (0, node_path_1.dirname)(projectPackageJsonPath);
31
- while (true) {
32
- const packageJsonPath = (0, node_path_1.join)(current, "node_modules", ...packageName.split("/"), "package.json");
33
- if ((0, node_fs_1.existsSync)(packageJsonPath)) {
34
- const manifest = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, "utf8"));
35
- if (manifest.name === packageName && typeof manifest.version === "string") {
36
- return manifest.version;
37
- }
38
- }
39
- const parent = (0, node_path_1.dirname)(current);
40
- if (parent === current)
41
- break;
42
- current = parent;
43
- }
44
- throw new Error(`server_dependency_missing:${packageName}`);
45
- }
46
- function verifyGameServerDependencies(task) {
47
- if (!task.server)
48
- return;
49
- if (!task.packageJsonPath)
50
- throw new Error("server_package_json_required");
51
- for (const [packageName, expected] of Object.entries(REQUIRED_PACKAGE_VERSIONS)) {
52
- const actual = resolveInstalledPackageVersion(task.packageJsonPath, packageName);
53
- if (actual !== expected) {
54
- throw new Error(`server_dependency_version_mismatch:${packageName}:expected=${expected}:actual=${actual}`);
55
- }
56
- }
57
- }
58
23
  function allowedImportsPlugin(projectDir) {
59
24
  const normalizedProjectDir = `${(0, node_fs_1.realpathSync)(projectDir)}${node_path_1.sep}`;
60
25
  return {
@@ -111,10 +76,10 @@ async function validateRoomExports(task, bundlePath) {
111
76
  throw new Error(`server_room_invalid:${roomName}`);
112
77
  }
113
78
  }
114
- async function buildGameServer(task) {
79
+ async function buildGameServer(task, options = {}) {
115
80
  if (!task.server)
116
81
  return null;
117
- verifyGameServerDependencies(task);
82
+ await (0, serverSdk_1.ensureGameServerSdk)(task, options);
118
83
  const outputDir = (0, node_path_1.join)(task.projectDir, ".playdrop", "server");
119
84
  const bundlePath = (0, node_path_1.join)(outputDir, "bundle.mjs");
120
85
  const manifestPath = (0, node_path_1.join)(outputDir, "manifest.json");
@@ -142,7 +107,7 @@ async function buildGameServer(task) {
142
107
  await validateRoomExports(task, bundlePath);
143
108
  const sha256 = (0, node_crypto_1.createHash)("sha256").update((0, node_fs_1.readFileSync)(bundlePath)).digest("hex");
144
109
  const manifest = {
145
- runtimeVersion: exports.GAME_SERVER_RUNTIME_VERSION,
110
+ runtimeVersion: serverSdk_1.GAME_SERVER_RUNTIME_VERSION,
146
111
  rooms: [...task.server.rooms],
147
112
  size,
148
113
  sha256,
@@ -0,0 +1,23 @@
1
+ import type { AppTask } from "../catalogue";
2
+ import { type EnvironmentConfig } from "../environment";
3
+ export declare const GAME_SERVER_RUNTIME_VERSION: "colyseus-0.17-v1";
4
+ export type ServerSdkRelease = {
5
+ version: string;
6
+ build: number;
7
+ runtimeVersion: typeof GAME_SERVER_RUNTIME_VERSION;
8
+ package: string;
9
+ sha256: string;
10
+ dependencies: Record<string, string>;
11
+ };
12
+ export type ServerSdkOptions = {
13
+ apiBase?: string;
14
+ environment?: EnvironmentConfig;
15
+ };
16
+ export declare function resolveServerSdkEnvironment(task: AppTask, apiBase?: string): EnvironmentConfig;
17
+ export declare function parseServerSdkRelease(value: unknown, expected: {
18
+ version: string;
19
+ build: number;
20
+ }): ServerSdkRelease;
21
+ export declare function resolveActiveServerSdk(environment: EnvironmentConfig): Promise<ServerSdkRelease>;
22
+ export declare function verifyGameServerDependencies(task: AppTask, release: ServerSdkRelease): void;
23
+ export declare function ensureGameServerSdk(task: AppTask, options?: ServerSdkOptions): Promise<ServerSdkRelease | null>;
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GAME_SERVER_RUNTIME_VERSION = void 0;
4
+ exports.resolveServerSdkEnvironment = resolveServerSdkEnvironment;
5
+ exports.parseServerSdkRelease = parseServerSdkRelease;
6
+ exports.resolveActiveServerSdk = resolveActiveServerSdk;
7
+ exports.verifyGameServerDependencies = verifyGameServerDependencies;
8
+ exports.ensureGameServerSdk = ensureGameServerSdk;
9
+ const node_child_process_1 = require("node:child_process");
10
+ const node_crypto_1 = require("node:crypto");
11
+ const node_fs_1 = require("node:fs");
12
+ const node_path_1 = require("node:path");
13
+ const config_1 = require("../config");
14
+ const environment_1 = require("../environment");
15
+ const workspaceAuth_1 = require("../workspaceAuth");
16
+ const npmProcess_1 = require("../commands/npmProcess");
17
+ exports.GAME_SERVER_RUNTIME_VERSION = "colyseus-0.17-v1";
18
+ const PACKAGE_NAMES = ["@colyseus/core", "@colyseus/schema", "@playdrop/server", "colyseus", "mongodb", "typescript"];
19
+ const VERSION = /^\d+\.\d+\.\d+$/u;
20
+ const MAX_ARCHIVE_BYTES = 20 * 1024 * 1024;
21
+ function resolveServerSdkEnvironment(task, apiBase) {
22
+ const workspace = (0, workspaceAuth_1.findWorkerTaskWorkspaceAuthConfig)(task.projectDir) ?? (0, workspaceAuth_1.findWorkspaceAuthConfig)(task.projectDir);
23
+ const preferred = (0, environment_1.resolveEnvironmentConfig)(workspace?.config.env ?? (0, config_1.loadConfig)().env ?? "prod");
24
+ if (!preferred)
25
+ throw new Error("server_sdk_environment_invalid");
26
+ if (!apiBase || preferred.apiBase === apiBase.replace(/\/$/u, ""))
27
+ return preferred;
28
+ for (const name of ["dev", "local", "prod", "preview", "blue", "green"]) {
29
+ const candidate = (0, environment_1.resolveEnvironmentConfig)(name);
30
+ if (candidate.apiBase === apiBase.replace(/\/$/u, ""))
31
+ return candidate;
32
+ }
33
+ throw new Error(`server_sdk_environment_unknown:${apiBase}`);
34
+ }
35
+ async function fetchJson(url) {
36
+ const response = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(15000) });
37
+ if (!response.ok)
38
+ throw new Error(`server_sdk_unavailable:${response.status}:${url}`);
39
+ return response.json();
40
+ }
41
+ function parseServerSdkRelease(value, expected) {
42
+ const metadata = value;
43
+ if (!metadata || metadata.version !== expected.version || metadata.build !== expected.build ||
44
+ metadata.runtimeVersion !== exports.GAME_SERVER_RUNTIME_VERSION ||
45
+ metadata.package !== `playdrop-server-${expected.version}.tgz` ||
46
+ typeof metadata.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(metadata.sha256) ||
47
+ !metadata.dependencies || Object.keys(metadata.dependencies).sort().join(",") !== PACKAGE_NAMES.join(",") ||
48
+ Object.values(metadata.dependencies).some((version) => typeof version !== "string" || !VERSION.test(version)) ||
49
+ metadata.dependencies["@playdrop/server"] !== expected.version) {
50
+ throw new Error(`server_sdk_release_mismatch:${expected.version}:${expected.build}`);
51
+ }
52
+ return metadata;
53
+ }
54
+ async function resolveActiveServerSdk(environment) {
55
+ if (!environment.roomsBase || !environment.cdnBase) {
56
+ throw new Error(`server_sdk_environment_unavailable:${environment.name}:game-server endpoint is not configured`);
57
+ }
58
+ // The deployed rooms runtime is authoritative. Client latest.json can be ahead
59
+ // or behind it while a release is being promoted.
60
+ const status = await fetchJson(`${environment.roomsBase}/status`);
61
+ if (typeof status?.status !== "string" || status.runtimeVersion !== exports.GAME_SERVER_RUNTIME_VERSION ||
62
+ typeof status.sdk?.version !== "string" || !VERSION.test(status.sdk.version) ||
63
+ !Number.isInteger(status.sdk.build) || status.sdk.build < 1) {
64
+ throw new Error(`server_sdk_runtime_metadata_missing:${environment.roomsBase}`);
65
+ }
66
+ if (status.status !== "ok") {
67
+ throw new Error(`server_sdk_runtime_unhealthy:${environment.roomsBase}:status=${status.status}` +
68
+ `,redis=${status.redis?.status ?? "unknown"},mongodb=${status.mongodb?.status ?? "unknown"}` +
69
+ `,unhealthyAppVersions=${status.colyseus?.unhealthyAppVersions ?? "unknown"}`);
70
+ }
71
+ if (environment.name === "prod" && (status.slot !== status.activeSlot || status.acceptingNewRooms !== true)) {
72
+ throw new Error("server_sdk_runtime_not_active");
73
+ }
74
+ const metadata = await fetchJson(`${environment.cdnBase}/sdk/playdrop-server-${status.sdk.version}.json`);
75
+ return parseServerSdkRelease(metadata, status.sdk);
76
+ }
77
+ function verifyGameServerDependencies(task, release) {
78
+ if (!task.packageJsonPath)
79
+ throw new Error("server_package_json_required");
80
+ for (const [name, expected] of Object.entries(release.dependencies)) {
81
+ const path = (0, node_path_1.join)(task.projectDir, "node_modules", ...name.split("/"), "package.json");
82
+ if (!(0, node_fs_1.existsSync)(path))
83
+ throw new Error(`server_dependency_missing:${name}`);
84
+ const installed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
85
+ if (installed.name !== name || installed.version !== expected) {
86
+ throw new Error(`server_dependency_version_mismatch:${name}:expected=${expected}:actual=${installed.version}`);
87
+ }
88
+ }
89
+ }
90
+ async function installServerSdk(task, release, cdnBase) {
91
+ if (!task.packageJsonPath)
92
+ throw new Error("server_package_json_required: add package.json beside catalogue.json");
93
+ const archiveRelativePath = `vendor/playdrop/${release.package}`;
94
+ const archivePath = (0, node_path_1.join)(task.projectDir, archiveRelativePath);
95
+ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(task.packageJsonPath, "utf8"));
96
+ const sdkDependency = packageJson.dependencies?.["@playdrop/server"] ?? packageJson.devDependencies?.["@playdrop/server"];
97
+ const archiveMatches = (0, node_fs_1.existsSync)(archivePath) &&
98
+ (0, node_crypto_1.createHash)("sha256").update((0, node_fs_1.readFileSync)(archivePath)).digest("hex") === release.sha256;
99
+ if (archiveMatches && sdkDependency === `file:${archiveRelativePath}` && (0, node_fs_1.existsSync)((0, node_path_1.join)(task.projectDir, "package-lock.json"))) {
100
+ try {
101
+ verifyGameServerDependencies(task, release);
102
+ return;
103
+ }
104
+ catch {
105
+ // The selected SDK is unchanged, but npm ci or a fresh checkout can leave
106
+ // dependencies missing. Install that same selected release below.
107
+ }
108
+ }
109
+ if (!archiveMatches) {
110
+ const url = `${cdnBase}/sdk/${release.package}`;
111
+ const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
112
+ if (!response.ok)
113
+ throw new Error(`server_sdk_unavailable:${response.status}:${url}`);
114
+ const archive = Buffer.from(await response.arrayBuffer());
115
+ if (archive.length > MAX_ARCHIVE_BYTES)
116
+ throw new Error("server_sdk_archive_too_large");
117
+ if ((0, node_crypto_1.createHash)("sha256").update(archive).digest("hex") !== release.sha256) {
118
+ throw new Error(`server_sdk_integrity_mismatch:${release.package}`);
119
+ }
120
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(task.projectDir, "vendor", "playdrop"), { recursive: true });
121
+ (0, node_fs_1.writeFileSync)(archivePath, archive);
122
+ }
123
+ const dependencies = Object.entries(release.dependencies)
124
+ .filter(([name]) => name !== "@playdrop/server")
125
+ .map(([name, version]) => `${name}@${version}`);
126
+ const invocation = (0, npmProcess_1.resolveNpmInvocation)([
127
+ "install", "--ignore-scripts", "--no-audit", "--no-fund", "--save-exact", `./${archiveRelativePath}`, ...dependencies,
128
+ ]);
129
+ console.log(`[server-sdk] Installing platform SDK ${release.version} (build ${release.build})`);
130
+ await new Promise((resolveInstall, rejectInstall) => {
131
+ const child = (0, node_child_process_1.spawn)(invocation.command, invocation.args, { cwd: task.projectDir, stdio: "inherit" });
132
+ child.once("error", rejectInstall);
133
+ child.once("close", (code) => code === 0 ? resolveInstall() :
134
+ rejectInstall(new Error(`server_sdk_install_failed:${code}: ${release.version}`)));
135
+ });
136
+ verifyGameServerDependencies(task, release);
137
+ }
138
+ // One selection per command and environment. Rebuilding a running dev game must
139
+ // not silently change its SDK after a platform release.
140
+ const preparations = new Map();
141
+ async function ensureGameServerSdk(task, options = {}) {
142
+ if (!task.server)
143
+ return null;
144
+ const environment = options.environment ?? resolveServerSdkEnvironment(task, options.apiBase);
145
+ const key = JSON.stringify([(0, node_path_1.resolve)(task.projectDir), environment.roomsBase, environment.cdnBase]);
146
+ let preparation = preparations.get(key);
147
+ if (!preparation) {
148
+ preparation = (async () => {
149
+ const release = await resolveActiveServerSdk(environment);
150
+ await installServerSdk(task, release, environment.cdnBase);
151
+ return release;
152
+ })();
153
+ preparations.set(key, preparation);
154
+ preparation.catch(() => preparations.delete(key));
155
+ }
156
+ return preparation;
157
+ }
@@ -3,6 +3,8 @@ import type { AgentTaskPlaytestProof, FinalizeAppUploadRequest, UserResponse } f
3
3
  import type { AppTask } from "../catalogue";
4
4
  import type { TaskCaptureSession } from "../appUrls";
5
5
  import type { AppBuildArtifacts } from "./build";
6
+ import { type GameServerBuild } from "./serverBuild";
7
+ import type { GameServerDevServices } from "../commands/devGameServer";
6
8
  export type AppUploadResult = {
7
9
  fileUnchanged?: boolean;
8
10
  metadataUnchanged?: boolean;
@@ -13,16 +15,16 @@ export type AppUploadResult = {
13
15
  versionNodeId?: string;
14
16
  };
15
17
  export type AppUploadOptions = {
18
+ apiBase?: string;
16
19
  skipReview?: boolean;
17
20
  clearTags?: boolean;
18
21
  creatorUsername?: string;
19
22
  token?: string | null;
20
23
  user?: UserResponse | null;
21
- runStagedUploadLoadCheck?: boolean;
24
+ gameServerDev?: GameServerDevServices;
22
25
  loadCheckTimeoutMs?: number;
23
26
  agentTaskId?: number;
24
27
  playtestProof?: AgentTaskPlaytestProof;
25
- playtestSmokeCheckRequired?: boolean;
26
28
  captureSession?: TaskCaptureSession | null;
27
29
  prepareGitTransition?: (input: {
28
30
  appId: number;
@@ -70,7 +72,11 @@ type PreparedOwnedAssetUpload = {
70
72
  type PreparedAppUploadFiles = {
71
73
  sessionFiles: PreparedSessionFile[];
72
74
  ownedAssets: PreparedOwnedAssetUpload[];
75
+ gameServerBuild: GameServerBuild | null;
73
76
  };
74
- export declare function prepareAppUploadFiles(task: AppTask, artifacts: AppBuildArtifacts | null): Promise<PreparedAppUploadFiles>;
77
+ export declare function prepareAppUploadFiles(task: AppTask, artifacts: AppBuildArtifacts | null, options?: {
78
+ apiBase?: string;
79
+ client?: ApiClient;
80
+ }): Promise<PreparedAppUploadFiles>;
75
81
  export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
76
82
  export {};