@playdrop/playdrop-cli 0.17.5 → 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 (72) hide show
  1. package/README.md +12 -0
  2. package/config/client-meta.json +2 -2
  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/commandContext.js +34 -10
  22. package/dist/commands/build.js +3 -1
  23. package/dist/commands/create.js +35 -16
  24. package/dist/commands/dev.js +11 -0
  25. package/dist/commands/devGameServer.d.ts +8 -0
  26. package/dist/commands/devGameServer.js +12 -9
  27. package/dist/commands/devServer.d.ts +1 -0
  28. package/dist/commands/devServer.js +21 -3
  29. package/dist/commands/source.d.ts +11 -3
  30. package/dist/commands/source.js +82 -14
  31. package/dist/commands/taskCaptureSession.d.ts +1 -1
  32. package/dist/commands/taskCaptureSession.js +1 -1
  33. package/dist/commands/upload-content.js +13 -12
  34. package/dist/commands/upload.d.ts +2 -9
  35. package/dist/commands/upload.js +5 -21
  36. package/dist/commands/validate.js +1 -1
  37. package/dist/commands/worker/game-server-infra.d.ts +6 -0
  38. package/dist/commands/worker/game-server-infra.js +10 -0
  39. package/dist/commands/worker/game-source-git.d.ts +9 -1
  40. package/dist/commands/worker/game-source-git.js +65 -19
  41. package/dist/commands/worker/generation.d.ts +2 -0
  42. package/dist/commands/worker/generation.js +35 -5
  43. package/dist/commands/worker.d.ts +1 -1
  44. package/dist/commands/worker.js +57 -17
  45. package/dist/environment.d.ts +1 -0
  46. package/dist/environment.js +7 -0
  47. package/dist/index.js +10 -2
  48. package/dist/playwright.d.ts +6 -4
  49. package/dist/playwright.js +24 -9
  50. package/dist/proceduralValidation.d.ts +16 -0
  51. package/dist/proceduralValidation.js +227 -0
  52. package/dist/proceduralValidationBrowser.d.mts +3 -0
  53. package/dist/proceduralValidationBrowser.mjs +67 -0
  54. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  55. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -1
  56. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts +6 -1
  57. package/node_modules/@playdrop/api-client/dist/domains/chat.d.ts.map +1 -1
  58. package/node_modules/@playdrop/api-client/dist/domains/chat.js +13 -1
  59. package/node_modules/@playdrop/config/client-meta.json +2 -2
  60. package/node_modules/@playdrop/procedural/dist/index.d.ts +201 -0
  61. package/node_modules/@playdrop/procedural/dist/index.d.ts.map +1 -0
  62. package/node_modules/@playdrop/procedural/dist/index.js +763 -0
  63. package/node_modules/@playdrop/procedural/package.json +36 -0
  64. package/node_modules/@playdrop/types/dist/api.d.ts +37 -4
  65. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  66. package/node_modules/@playdrop/types/dist/chat.d.ts +7 -1
  67. package/node_modules/@playdrop/types/dist/chat.d.ts.map +1 -1
  68. package/node_modules/@playdrop/types/dist/social.d.ts +20 -0
  69. package/node_modules/@playdrop/types/dist/social.d.ts.map +1 -1
  70. package/node_modules/@playdrop/types/dist/version.d.ts +2 -0
  71. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  72. package/package.json +7 -4
@@ -3,25 +3,33 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseExactSourceRef = parseExactSourceRef;
4
4
  exports.prepareDestination = prepareDestination;
5
5
  exports.extractArchive = extractArchive;
6
+ exports.extractGitSource = extractGitSource;
6
7
  exports.showExactContentDetail = showExactContentDetail;
7
8
  exports.downloadContentSource = downloadContentSource;
8
9
  const node_fs_1 = require("node:fs");
10
+ const promises_1 = require("node:fs/promises");
11
+ const node_os_1 = require("node:os");
9
12
  const node_path_1 = require("node:path");
13
+ const game_source_git_1 = require("@playdrop/game-source-git");
14
+ const types_1 = require("@playdrop/types");
10
15
  const fflate_1 = require("fflate");
11
16
  const commandContext_1 = require("../commandContext");
12
17
  const errors_1 = require("../errors");
13
18
  const output_1 = require("../output");
19
+ const game_source_git_2 = require("./worker/game-source-git");
14
20
  function parseExactSourceRef(raw, expectedKind) {
15
- const pattern = expectedKind === "asset"
16
- ? /^asset:([a-z0-9]+(?:-[a-z0-9]+)*)\/([a-z0-9]+(?:-[a-z0-9]+)*)@(r[1-9][0-9]*)$/
17
- : /^pack:([a-z0-9]+(?:-[a-z0-9]+)*)\/([a-z0-9]+(?:-[a-z0-9]+)*)@(\d+\.\d+\.\d+)$/;
18
- const match = pattern.exec(raw.trim());
19
- if (!match) {
21
+ const ref = (0, types_1.parseContentVersionRef)(raw);
22
+ if (!ref || ref.kind !== expectedKind) {
20
23
  throw new Error(expectedKind === "asset"
21
24
  ? "invalid_exact_asset_ref: expected asset:creator/name@rN"
22
- : "invalid_exact_pack_ref: expected pack:creator/name@X.Y.Z");
25
+ : `invalid_exact_${expectedKind}_ref: expected ${expectedKind}:creator/name@X.Y.Z`);
23
26
  }
24
- return { kind: expectedKind, creator: match[1], name: match[2], version: match[3] };
27
+ return {
28
+ kind: expectedKind,
29
+ creator: ref.creatorUsername,
30
+ name: ref.name,
31
+ version: ref.kind === "asset" ? `r${ref.revision}` : ref.version,
32
+ };
25
33
  }
26
34
  function prepareDestination(path) {
27
35
  const destination = (0, node_path_1.resolve)(path);
@@ -58,6 +66,36 @@ function extractArchive(bytes, destination) {
58
66
  }
59
67
  return fileCount;
60
68
  }
69
+ async function extractGitSource(bytes, destination, options) {
70
+ if (!/^[0-9a-f]{40}$/.test(options.commitSha))
71
+ throw new Error("app_source_commit_missing");
72
+ const temporary = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "playdrop-app-source-"));
73
+ const git = (0, game_source_git_1.createGitRunner)();
74
+ try {
75
+ const bundle = (0, node_path_1.join)(temporary, "source.bundle");
76
+ await (0, promises_1.writeFile)(bundle, bytes);
77
+ await git.run(["init", "--initial-branch=main", destination]);
78
+ await git.run(["fetch", "--update-head-ok", bundle, "+refs/*:refs/*"], destination);
79
+ const tag = `refs/tags/v${options.version}`;
80
+ const taggedCommit = (await git.run(["rev-parse", "--verify", `${tag}^{commit}`], destination).catch(() => {
81
+ throw new Error(`app_source_version_missing:${options.version}: Git source is missing ${tag}`);
82
+ })).toString("utf8").trim();
83
+ if (taggedCommit !== options.commitSha)
84
+ throw new Error(`app_source_version_commit_mismatch:${options.version}`);
85
+ await git.run(["checkout", "--detach", options.commitSha], destination);
86
+ const lock = await (0, game_source_git_2.materializeCurrentLock)({
87
+ projectDir: destination,
88
+ workerHomeDir: options.workerHomeDir,
89
+ readFiles: options.readFiles,
90
+ cloneFile: promises_1.copyFile,
91
+ });
92
+ const trackedFiles = (await git.run(["ls-files", "-z"], destination)).toString("utf8").split("\0").filter(Boolean);
93
+ return new Set([...trackedFiles, ...Object.keys(lock.files)]).size;
94
+ }
95
+ finally {
96
+ await (0, promises_1.rm)(temporary, { recursive: true, force: true });
97
+ }
98
+ }
61
99
  async function showExactContentDetail(kind, rawRef, options = {}) {
62
100
  const command = `${kind} detail`;
63
101
  await (0, commandContext_1.withPublicEnvironment)(command, async ({ client, envConfig }) => {
@@ -114,20 +152,48 @@ async function showExactContentDetail(kind, rawRef, options = {}) {
114
152
  }
115
153
  async function downloadContentSource(kind, rawRef, directory, options = {}) {
116
154
  const command = `${kind} source`;
117
- await (0, commandContext_1.withPublicEnvironment)(command, async ({ client }) => {
155
+ await (0, commandContext_1.withPublicEnvironment)(command, async ({ client, workspaceAuth }) => {
118
156
  try {
119
157
  const ref = parseExactSourceRef(rawRef, kind);
120
- const result = kind === "asset"
121
- ? await client.downloadAssetSource(ref.creator, ref.name, ref.version)
122
- : await client.downloadAssetPackSource(ref.creator, ref.name, ref.version);
123
- const destination = prepareDestination(directory ?? ref.name);
124
- const fileCount = extractArchive(new Uint8Array(await result.blob.arrayBuffer()), destination);
158
+ const result = kind === "app"
159
+ ? await client.downloadAppSource(ref.creator, ref.name, ref.version)
160
+ : kind === "asset"
161
+ ? await client.downloadAssetSource(ref.creator, ref.name, ref.version)
162
+ : await client.downloadAssetPackSource(ref.creator, ref.name, ref.version);
163
+ const bytes = new Uint8Array(await result.blob.arrayBuffer());
164
+ if (bytes.length !== result.metadata.sizeBytes) {
165
+ throw new Error(`source_download_incomplete:${rawRef}: expected ${result.metadata.sizeBytes} bytes, received ${bytes.length}`);
166
+ }
167
+ const metadata = result.metadata;
168
+ const isGit = "format" in metadata && metadata.format === "git-bundle";
169
+ const taskId = workspaceAuth?.config.taskId;
170
+ const workerHomeDir = process.env.PLAYDROP_WORKER_HOME?.trim();
171
+ if (isGit) {
172
+ if (process.env.PLAYDROP_WORKER_CONTEXT !== "1" || !taskId) {
173
+ throw new Error("app_source_task_required: Cloud Git material downloads require an active Cloud task");
174
+ }
175
+ if (!workerHomeDir || !(0, node_path_1.isAbsolute)(workerHomeDir)) {
176
+ throw new Error("app_source_worker_home_missing: the Cloud worker must provide its absolute PLAYDROP_WORKER_HOME cache directory");
177
+ }
178
+ }
179
+ const destination = directory === undefined && kind === "app"
180
+ ? await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "playdrop-app-source-"))
181
+ : prepareDestination(directory ?? ref.name);
182
+ const fileCount = isGit
183
+ ? await extractGitSource(bytes, destination, {
184
+ version: ref.version,
185
+ commitSha: "commitSha" in metadata && typeof metadata.commitSha === "string" ? metadata.commitSha : "",
186
+ workerHomeDir: workerHomeDir,
187
+ readFiles: (files) => client.workerReadGameSourceFiles(taskId, { files }),
188
+ })
189
+ : extractArchive(bytes, destination);
125
190
  const output = {
126
191
  ref: rawRef,
127
192
  destination,
128
193
  filename: result.metadata.filename,
129
194
  sizeBytes: result.metadata.sizeBytes,
130
195
  files: fileCount,
196
+ format: isGit ? "git-bundle" : "zip",
131
197
  };
132
198
  if (options.json)
133
199
  (0, output_1.printJson)(output);
@@ -135,7 +201,9 @@ async function downloadContentSource(kind, rawRef, directory, options = {}) {
135
201
  console.log(`Downloaded ${rawRef} source to ${destination} (${fileCount} files).`);
136
202
  }
137
203
  catch (error) {
138
- if (!(0, errors_1.handleCommandFailure)(error, command, "Source download"))
204
+ if (!(0, errors_1.handleCommandFailure)(error, command, "Source download", {
205
+ apiMessage: (apiError) => ({ problem: `Source download rejected for ${rawRef}: ${apiError.code ?? apiError.message} (HTTP ${apiError.status}).` }),
206
+ }))
139
207
  throw error;
140
208
  }
141
209
  }, { env: options.env });
@@ -1,4 +1,4 @@
1
1
  import type { AppResponse } from "@playdrop/types";
2
2
  import type { TaskCaptureSession } from "../appUrls";
3
3
  import type { ResolvedWorkspaceAuthConfig } from "../workspaceAuth";
4
- export declare function resolveTaskCaptureSession(workspaceAuth: ResolvedWorkspaceAuthConfig | null, app: AppResponse): TaskCaptureSession | null;
4
+ export declare function resolveTaskCaptureSession(workspaceAuth: ResolvedWorkspaceAuthConfig | null, app: AppResponse | null): TaskCaptureSession | null;
@@ -10,7 +10,7 @@ function resolveTaskCaptureSession(workspaceAuth, app) {
10
10
  if (!Number.isInteger(taskId) || Number(taskId) <= 0 || !taskToken) {
11
11
  throw new Error("worker_capture_session_incomplete");
12
12
  }
13
- if (!Number.isInteger(app.id) || Number(app.id) <= 0) {
13
+ if (!app || !Number.isInteger(app.id) || Number(app.id) <= 0) {
14
14
  throw new Error("registered_app_id_missing");
15
15
  }
16
16
  return {
@@ -13,6 +13,7 @@ const node_crypto_1 = require("node:crypto");
13
13
  const node_fs_1 = require("node:fs");
14
14
  const node_path_1 = require("node:path");
15
15
  const assetSpecs_1 = require("../assetSpecs");
16
+ const proceduralValidation_1 = require("../proceduralValidation");
16
17
  const EXTENSION_TO_FORMAT = {
17
18
  ".png": "PNG",
18
19
  ".jpg": "JPEG",
@@ -292,7 +293,8 @@ function validateAssetSubcategoryCompatibility(task, format) {
292
293
  }
293
294
  return subcategory;
294
295
  }
295
- async function prepareAssetTaskUploadData(task) {
296
+ async function prepareAssetTaskUploadData(task, client) {
297
+ await (0, proceduralValidation_1.assertProceduralAssetIsValid)(task, { client, primaryContentType: resolveMimeType(task.filePaths.primary ?? "") });
296
298
  if ("assetSpec" in task && typeof task.assetSpec === "string" && task.assetSpec.trim().length > 0) {
297
299
  const uploadFiles = Object.entries(task.filePaths)
298
300
  .sort(([leftRole], [rightRole]) => leftRole.localeCompare(rightRole))
@@ -331,7 +333,7 @@ async function prepareAssetTaskUploadData(task) {
331
333
  };
332
334
  }
333
335
  async function uploadAssetTask(client, task, sourceAppVersionId, creatorUsername, options) {
334
- const prepared = await prepareAssetTaskUploadData(task);
336
+ const prepared = await prepareAssetTaskUploadData(task, client);
335
337
  const visibility = task.visibility;
336
338
  const files = prepared.uploadFiles.map((entry) => ({
337
339
  file: entry.file,
@@ -485,20 +487,19 @@ function describeError(error) {
485
487
  }
486
488
  return String(error);
487
489
  }
488
- async function prepareLocalPackAssets(task, mutationCreatorUsername, localAssetTasks, uploadKeyByAssetKey) {
489
- const preparedLocalAssets = new Map();
490
- for (const localAssetTask of localAssetTasks) {
490
+ async function prepareLocalPackAssets(client, task, mutationCreatorUsername, localAssetTasks, uploadKeyByAssetKey) {
491
+ const preparedLocalAssets = await Promise.all(localAssetTasks.map(async (localAssetTask) => {
491
492
  const assetKey = buildAssetKey(mutationCreatorUsername, localAssetTask.name);
492
493
  const uploadKey = uploadKeyByAssetKey.get(assetKey);
493
494
  if (!uploadKey) {
494
495
  throw new Error(`Asset pack "${task.name}@${task.version}" is missing upload key for local asset "${localAssetTask.name}".`);
495
496
  }
496
- preparedLocalAssets.set(uploadKey, {
497
- task: localAssetTask,
498
- prepared: await prepareAssetTaskUploadData(localAssetTask),
499
- });
500
- }
501
- return preparedLocalAssets;
497
+ return [uploadKey, {
498
+ task: localAssetTask,
499
+ prepared: await prepareAssetTaskUploadData(localAssetTask, client),
500
+ }];
501
+ }));
502
+ return new Map(preparedLocalAssets);
502
503
  }
503
504
  function buildLocalAssetManifestEntries(preparedLocalAssets) {
504
505
  const localAssets = [];
@@ -656,7 +657,7 @@ function buildUploadedPackInfo(creatorUsername, task, versionNodeId, versionAsse
656
657
  }
657
658
  async function uploadAssetPackTask(client, task, creatorUsername, uploadedAssets, localAssetTasks, uploadKeyByAssetKey, targetCreatorUsername, options) {
658
659
  const mutationCreatorUsername = resolveMutationCreatorUsername(task, creatorUsername, targetCreatorUsername);
659
- const preparedLocalAssets = await prepareLocalPackAssets(task, mutationCreatorUsername, localAssetTasks, uploadKeyByAssetKey);
660
+ const preparedLocalAssets = await prepareLocalPackAssets(client, task, mutationCreatorUsername, localAssetTasks, uploadKeyByAssetKey);
660
661
  const ownedAssets = buildLocalAssetManifestEntries(preparedLocalAssets);
661
662
  const { existingAssets, members } = await buildPackMembersAndExistingAssets(client, task, mutationCreatorUsername, uploadedAssets, uploadKeyByAssetKey);
662
663
  const listingUploads = buildPackListingUploads(task);
@@ -1,5 +1,6 @@
1
1
  import type { ApiClient } from "@playdrop/api-client";
2
2
  import type { AgentExecutionTarget, AppSurface, FinalizeAppUploadRequest, UserResponse } from "@playdrop/types";
3
+ import type { GameServerDevServices } from "./devGameServer";
3
4
  import { type AppTask } from "../catalogue";
4
5
  export type UploadCommandOptions = {
5
6
  env?: string;
@@ -28,6 +29,7 @@ export type WorkerAppPublishInput = {
28
29
  token: string;
29
30
  user: UserResponse;
30
31
  devRouterPort?: number;
32
+ gameServerDev?: GameServerDevServices;
31
33
  deterministicE2EFixture?: boolean;
32
34
  prepareGitTransition?: (input: {
33
35
  appId: number;
@@ -35,15 +37,6 @@ export type WorkerAppPublishInput = {
35
37
  version: string;
36
38
  }) => Promise<NonNullable<FinalizeAppUploadRequest["gitTransition"]>>;
37
39
  };
38
- export declare function resolveWorkerHostedValidationPolicy(input: {
39
- kind: WorkerAppPublishInput["kind"];
40
- deterministicE2EFixture?: boolean;
41
- }): {
42
- runLocalLoadCheck: boolean;
43
- runStagedUploadLoadCheck: boolean;
44
- playtestSmokeCheckRequired: boolean;
45
- skipReview: boolean;
46
- };
47
40
  export type WorkerAppPublishResult = {
48
41
  appId: number;
49
42
  appVersionId: number;
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveUploadOwner = resolveUploadOwner;
4
4
  exports.resolveDefaultUploadCreator = resolveDefaultUploadCreator;
5
5
  exports.upload = upload;
6
- exports.resolveWorkerHostedValidationPolicy = resolveWorkerHostedValidationPolicy;
7
6
  exports.assertAgentPrimarySurfaceContract = assertAgentPrimarySurfaceContract;
8
7
  exports.assertAgentNewGamePlaytestContract = assertAgentNewGamePlaytestContract;
9
8
  exports.assertAgentNewGameListingIdentity = assertAgentNewGameListingIdentity;
@@ -1240,15 +1239,6 @@ async function upload(pathOrName, options) {
1240
1239
  (0, uploadLog_1.printTaskSummary)(results, warnings, { action: "upload", environment: env });
1241
1240
  (0, uploadLog_1.printPublishedAppNextSteps)(results);
1242
1241
  }
1243
- function resolveWorkerHostedValidationPolicy(input) {
1244
- const browserChecksRequired = input.deterministicE2EFixture !== true;
1245
- return {
1246
- runLocalLoadCheck: browserChecksRequired,
1247
- runStagedUploadLoadCheck: browserChecksRequired,
1248
- playtestSmokeCheckRequired: browserChecksRequired && input.kind === "NEW_GAME",
1249
- skipReview: !browserChecksRequired,
1250
- };
1251
- }
1252
1242
  function assertAgentPrimarySurfaceContract(task, kind) {
1253
1243
  if (!task.primarySurface) {
1254
1244
  throw new Error(`agent_task_primary_surface_required: ${kind} catalogue.json must declare primarySurface.`);
@@ -1365,9 +1355,9 @@ async function prepareWorkerAppProject(input) {
1365
1355
  throw new Error(`agent_task_remix_ref_mismatch:${catalogueRemixRef}:${expectedRemixRef}`);
1366
1356
  }
1367
1357
  }
1368
- await (0, apps_1.validateAppTask)(task);
1358
+ await (0, apps_1.validateAppTask)(task, { apiBase: input.apiBase });
1369
1359
  const isExternal = task.hostingMode === "EXTERNAL" || Boolean(task.externalUrl);
1370
- const artifacts = isExternal ? null : await (0, apps_1.buildApp)(task, { includeSourceArchive: false });
1360
+ const artifacts = isExternal ? null : await (0, apps_1.buildApp)(task, { includeSourceArchive: false, apiBase: input.apiBase });
1371
1361
  await assertWorkerAppLocalUploadPreflight({
1372
1362
  task,
1373
1363
  kind: input.kind,
@@ -1384,7 +1374,7 @@ async function prepareWorkerAppProject(input) {
1384
1374
  // succeed before the agent can complete the task.
1385
1375
  async function preflightWorkerAppProject(input) {
1386
1376
  const prepared = await prepareWorkerAppProject(input);
1387
- await (0, apps_1.prepareAppUploadFiles)(prepared.task, prepared.artifacts);
1377
+ await (0, apps_1.prepareAppUploadFiles)(prepared.task, prepared.artifacts, { apiBase: input.apiBase, client: input.client });
1388
1378
  return {
1389
1379
  appName: prepared.task.name,
1390
1380
  creatorUsername: prepared.creatorUsername,
@@ -1409,7 +1399,6 @@ async function publishWorkerAppProject(input) {
1409
1399
  displayName: fixtureDisplayName,
1410
1400
  });
1411
1401
  }
1412
- const validationPolicy = resolveWorkerHostedValidationPolicy(input);
1413
1402
  const { app } = await (0, registration_1.ensureRegisteredHostedAppShell)({
1414
1403
  client: input.client,
1415
1404
  creatorUsername,
@@ -1434,13 +1423,11 @@ async function publishWorkerAppProject(input) {
1434
1423
  webBase: normalizePortalBase(input.webBase ?? undefined),
1435
1424
  token: input.token,
1436
1425
  user: input.user,
1437
- runLocalLoadCheck: validationPolicy.runLocalLoadCheck,
1438
1426
  devRouterPort: input.devRouterPort,
1439
- runStagedUploadLoadCheck: validationPolicy.runStagedUploadLoadCheck,
1427
+ gameServerDev: input.gameServerDev,
1440
1428
  ensureRegisteredAppShell: true,
1441
1429
  agentTaskId: input.taskId,
1442
- playtestSmokeCheckRequired: validationPolicy.playtestSmokeCheckRequired,
1443
- skipReview: validationPolicy.skipReview,
1430
+ skipReview: input.deterministicE2EFixture === true,
1444
1431
  captureSession,
1445
1432
  prepareGitTransition: input.prepareGitTransition,
1446
1433
  ...(artifacts ? { artifacts } : {}),
@@ -1528,12 +1515,9 @@ async function publishStaticHtmlProject(input) {
1528
1515
  webBase: normalizePortalBase(input.webBase ?? undefined),
1529
1516
  token: input.token,
1530
1517
  user: input.user,
1531
- runLocalLoadCheck: true,
1532
1518
  devRouterPort: input.devRouterPort,
1533
- runStagedUploadLoadCheck: true,
1534
1519
  ensureRegisteredAppShell: true,
1535
1520
  agentTaskId: input.taskId,
1536
- playtestSmokeCheckRequired: false,
1537
1521
  skipReview: input.skipReview,
1538
1522
  captureSession,
1539
1523
  prepareGitTransition: input.prepareGitTransition,
@@ -215,7 +215,7 @@ async function validate(pathOrName, options = {}) {
215
215
  kind: workerContext.kind,
216
216
  });
217
217
  }
218
- await (0, apps_1.validateAppTask)(task);
218
+ await (0, apps_1.validateAppTask)(task, { environment: resolvedValidationContext?.envConfig });
219
219
  (0, apps_1.collectAppValidationWarnings)(task, "source").forEach((warning) => warnings.add(warning));
220
220
  }
221
221
  else if (task.kind === "asset-spec") {
@@ -78,3 +78,9 @@ export declare function provisionWorkerGameServerDevLease(input: {
78
78
  mongoAdminUrl: string;
79
79
  requireRedisAcl?: boolean;
80
80
  }): Promise<WorkerGameServerDevLease>;
81
+ export declare function provisionPublishingGameServerDevLease(input: {
82
+ sessionId: string;
83
+ environment: string;
84
+ redisAdminUrl: string;
85
+ mongoAdminUrl: string;
86
+ }): Promise<WorkerGameServerDevLease>;
@@ -13,6 +13,7 @@ exports.setupManagedWorkerGameServerInfrastructure = setupManagedWorkerGameServe
13
13
  exports.probeManagedWorkerGameServerInfrastructure = probeManagedWorkerGameServerInfrastructure;
14
14
  exports.cleanupStaleWorkerGameServerDevResources = cleanupStaleWorkerGameServerDevResources;
15
15
  exports.provisionWorkerGameServerDevLease = provisionWorkerGameServerDevLease;
16
+ exports.provisionPublishingGameServerDevLease = provisionPublishingGameServerDevLease;
16
17
  const node_child_process_1 = require("node:child_process");
17
18
  const node_crypto_1 = require("node:crypto");
18
19
  const node_fs_1 = require("node:fs");
@@ -507,6 +508,15 @@ async function cleanupStaleWorkerGameServerDevResources(input) {
507
508
  }
508
509
  async function provisionWorkerGameServerDevLease(input) {
509
510
  const scope = normalizeTaskScope(input.taskId, input.attempt);
511
+ return provisionScopedGameServerDevLease({ ...input, scope });
512
+ }
513
+ async function provisionPublishingGameServerDevLease(input) {
514
+ if (!/^[a-zA-Z0-9-]{1,100}$/u.test(input.sessionId))
515
+ throw new Error("publishing_session_id_invalid");
516
+ return provisionScopedGameServerDevLease({ ...input, scope: `upload-${input.sessionId}`, requireRedisAcl: true });
517
+ }
518
+ async function provisionScopedGameServerDevLease(input) {
519
+ const scope = input.scope;
510
520
  const environment = input.environment.trim().toLowerCase().replace(/[^a-z0-9_-]/gu, "-");
511
521
  if (!environment)
512
522
  throw new Error("worker_game_server_dev_environment_invalid");
@@ -1,5 +1,6 @@
1
1
  import type { ApiClient } from "@playdrop/api-client";
2
- import type { WorkerGameSourceAssignmentV1, WorkerGameTaskAssignment } from "@playdrop/types";
2
+ import { type GameSourceLockEntry, type GameSourceLockV1, type MaterializeLockOptions } from "@playdrop/game-source-git";
3
+ import type { WorkerGameSourceAssignmentV1, WorkerGameTaskAssignment, WorkerReadGameSourceFilesResponse } from "@playdrop/types";
3
4
  export declare function emitGameSourceWorkerEvent(event: string, fields: Record<string, string | number | boolean | null>, write?: (line: string) => void): void;
4
5
  export type GameSourceWorkspace = {
5
6
  assignment: WorkerGameSourceAssignmentV1;
@@ -50,6 +51,12 @@ export type FinalizedGameSource = ReconciledGameSource & {
50
51
  commitSha: string;
51
52
  }>;
52
53
  };
54
+ export declare function materializeCurrentLock(input: {
55
+ projectDir: string;
56
+ workerHomeDir: string;
57
+ readFiles: (files: GameSourceLockEntry[]) => Promise<WorkerReadGameSourceFilesResponse>;
58
+ cloneFile?: MaterializeLockOptions["cloneFile"];
59
+ }): Promise<GameSourceLockV1>;
53
60
  export declare function setupGameSourceWorkspace(input: {
54
61
  assignment: WorkerGameTaskAssignment;
55
62
  workspaceDir: string;
@@ -58,6 +65,7 @@ export declare function setupGameSourceWorkspace(input: {
58
65
  workerKey: string;
59
66
  leaseToken: string;
60
67
  }): Promise<GameSourceWorkspace>;
68
+ export declare function fingerprintGameSourcePublication(projectDir: string, includeServerOutput?: boolean): Promise<string>;
61
69
  export declare function checkpointFailedGameSourceWorkspace(input: {
62
70
  workspace: GameSourceWorkspace;
63
71
  taskId: number;
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.emitGameSourceWorkerEvent = emitGameSourceWorkerEvent;
7
+ exports.materializeCurrentLock = materializeCurrentLock;
7
8
  exports.setupGameSourceWorkspace = setupGameSourceWorkspace;
9
+ exports.fingerprintGameSourcePublication = fingerprintGameSourcePublication;
8
10
  exports.checkpointFailedGameSourceWorkspace = checkpointFailedGameSourceWorkspace;
9
11
  exports.reconcileGameSourceWorkspace = reconcileGameSourceWorkspace;
10
12
  exports.finalizeGameSourceRepository = finalizeGameSourceRepository;
@@ -222,36 +224,51 @@ async function reattachRetainedWorktree(input) {
222
224
  }
223
225
  }
224
226
  async function materializeCurrentLock(input) {
225
- const lock = (0, game_source_git_1.parseGameSourceLock)(await (0, promises_1.readFile)(node_path_1.default.join(input.projectDir, ".playdrop", "files.lock")));
227
+ const lockPath = node_path_1.default.join(input.projectDir, game_source_git_1.GAME_SOURCE_LOCK_PATH);
228
+ const lock = (0, game_source_git_1.parseGameSourceLock)(await (0, promises_1.readFile)(lockPath).catch((error) => {
229
+ if (error.code === "ENOENT")
230
+ throw new Error(`game_source_material_lock_missing:${lockPath}`);
231
+ throw error;
232
+ }));
226
233
  const requestedByKey = new Map();
227
234
  for (const entry of Object.values(lock.files)) {
228
235
  const existing = requestedByKey.get(entry.key);
229
236
  if (existing && (existing.sha256 !== entry.sha256 || existing.bytes !== entry.bytes)) {
230
237
  throw new Error("game_source_material_lock_conflict");
231
238
  }
232
- requestedByKey.set(entry.key, { sha256: entry.sha256, bytes: entry.bytes, key: entry.key });
239
+ requestedByKey.set(entry.key, entry);
233
240
  }
234
241
  const requested = [...requestedByKey.values()];
235
- const response = requested.length > 0
236
- ? await input.client.workerReadGameSourceFiles(input.taskId, {
237
- workerKey: input.workerKey,
238
- leaseToken: input.leaseToken,
239
- files: requested,
240
- })
241
- : { files: [] };
242
- const urlByKey = new Map(response.files.map((entry) => [entry.key, entry.downloadUrl]));
242
+ const urlByKey = new Map();
243
+ for (let offset = 0; offset < requested.length; offset += 100) {
244
+ const batch = requested.slice(offset, offset + 100);
245
+ const response = await input.readFiles(batch);
246
+ for (const expected of batch) {
247
+ const file = response.files.find((entry) => entry.key === expected.key);
248
+ if (!file || file.sha256 !== expected.sha256 || file.bytes !== expected.bytes || !file.downloadUrl) {
249
+ throw new Error(`game_source_material_url_missing:${expected.key}`);
250
+ }
251
+ urlByKey.set(expected.key, file.downloadUrl);
252
+ }
253
+ }
243
254
  await (0, game_source_git_1.materializeGameSourceLock)({
244
255
  rootDir: input.projectDir,
245
256
  blobCacheDir: blobCacheRoot(input.workerHomeDir),
246
257
  lock,
258
+ cloneFile: input.cloneFile,
247
259
  download: async (key) => {
248
- const entry = Object.values(lock.files).find((candidate) => candidate.key === key);
249
260
  const url = urlByKey.get(key);
250
- if (!entry || !url)
251
- throw new Error("game_source_material_url_missing");
252
- return await downloadBuffer(url, entry.bytes, entry.sha256);
261
+ if (!url)
262
+ throw new Error(`game_source_material_url_missing:${key}`);
263
+ try {
264
+ return await downloadBuffer(url);
265
+ }
266
+ catch (error) {
267
+ throw new Error(`game_source_material_download_failed:${key}: ${error instanceof Error ? error.message : String(error)}`);
268
+ }
253
269
  },
254
270
  });
271
+ return lock;
255
272
  }
256
273
  async function writeManagedGameSourceIgnore(projectDir) {
257
274
  const ignorePath = node_path_1.default.join(projectDir, ".gitignore");
@@ -329,7 +346,15 @@ async function setupGameSourceWorkspace(input) {
329
346
  });
330
347
  }
331
348
  if (taskBaseSha && !resumed) {
332
- await materializeCurrentLock({ ...input, projectDir, taskId: input.assignment.task.id });
349
+ await materializeCurrentLock({
350
+ projectDir,
351
+ workerHomeDir: input.workerHomeDir,
352
+ readFiles: (files) => input.client.workerReadGameSourceFiles(input.assignment.task.id, {
353
+ workerKey: input.workerKey,
354
+ leaseToken: input.leaseToken,
355
+ files,
356
+ }),
357
+ });
333
358
  }
334
359
  if (source.listingSync && !resumed) {
335
360
  const listingFiles = new Map();
@@ -411,7 +436,7 @@ async function setupGameSourceWorkspace(input) {
411
436
  pendingMaterials,
412
437
  };
413
438
  }
414
- async function walkFiles(root, current = root) {
439
+ async function walkFiles(root, current = root, includeBuildOutput = false) {
415
440
  const files = [];
416
441
  for (const entry of await (0, promises_1.readdir)(current, { withFileTypes: true })) {
417
442
  if (entry.name === ".git")
@@ -419,11 +444,15 @@ async function walkFiles(root, current = root) {
419
444
  const absolute = node_path_1.default.join(current, entry.name);
420
445
  const relative = normalizeRelativePath(root, absolute);
421
446
  if (entry.isDirectory()) {
422
- if (["node_modules", "dist", "build", "coverage", ".next", ".playdrop-task-events"].includes(entry.name))
447
+ if (["node_modules", "coverage", ".next", ".playdrop-task-events"].includes(entry.name))
448
+ continue;
449
+ if (!includeBuildOutput && ["dist", "build"].includes(entry.name))
450
+ continue;
451
+ if (relative === ".playdrop" && !includeBuildOutput)
423
452
  continue;
424
- if (relative === ".playdrop" || relative.startsWith(".playdrop/"))
453
+ if (relative.startsWith(".playdrop/") && relative !== ".playdrop/server")
425
454
  continue;
426
- files.push(...await walkFiles(root, absolute));
455
+ files.push(...await walkFiles(root, absolute, includeBuildOutput));
427
456
  }
428
457
  else if (entry.isFile()) {
429
458
  files.push(absolute);
@@ -431,6 +460,23 @@ async function walkFiles(root, current = root) {
431
460
  }
432
461
  return files;
433
462
  }
463
+ // Source reconciliation deliberately excludes generated builds. Publication
464
+ // freshness must cover those bytes too, while ignoring task feedback/logs.
465
+ async function fingerprintGameSourcePublication(projectDir, includeServerOutput = true) {
466
+ const files = [];
467
+ for (const absolute of await walkFiles(projectDir, projectDir, true)) {
468
+ const relative = normalizeRelativePath(projectDir, absolute);
469
+ const content = await (0, promises_1.readFile)(absolute);
470
+ const classification = (0, game_source_git_1.classifyGameSourceFile)(relative, content);
471
+ const buildOutput = relative.split("/").some((part) => ["dist", "build", "output"].includes(part));
472
+ const serverOutput = includeServerOutput && [".playdrop/server/bundle.mjs", ".playdrop/server/manifest.json"].includes(relative);
473
+ if (classification === "git" || classification === "material" || buildOutput || serverOutput || relative === "source.zip") {
474
+ files.push([relative, (0, game_source_git_1.sha256)(content)]);
475
+ }
476
+ }
477
+ files.sort(([left], [right]) => left.localeCompare(right));
478
+ return (0, game_source_git_1.sha256)(Buffer.from(JSON.stringify(files)));
479
+ }
434
480
  async function gitCommit(repoPath, message) {
435
481
  await git.run([
436
482
  "-c", "user.name=Playdrop Game Source",
@@ -59,7 +59,9 @@ export declare function buildSandboxedCodexArgs(input: {
59
59
  outputPath: string;
60
60
  prompt: string;
61
61
  outputSchemaPath?: string;
62
+ persistSession?: boolean;
62
63
  }): string[];
64
+ export declare function retainScreeningTranscript(codexHomeDir: string, taskId: number): Promise<void>;
63
65
  export declare function falInput(request: CreateAiGenerationTaskRequest, resolvedInputs: Map<string, string>): Record<string, unknown>;
64
66
  export declare function selectFalOutputs(kind: CreateAiGenerationTaskRequest["kind"], value: Record<string, unknown>): {
65
67
  primary: FalFile;