@playdrop/playdrop-cli 0.13.16 → 0.13.17

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 (29) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/staticHtml.js +1 -0
  3. package/dist/commandContext.js +1 -1
  4. package/dist/commands/captureListing.d.ts +43 -0
  5. package/dist/commands/captureListing.js +184 -5
  6. package/dist/commands/upload.d.ts +12 -1
  7. package/dist/commands/upload.js +97 -88
  8. package/dist/commands/whoami.js +10 -1
  9. package/dist/commands/worker/runtime.js +2 -0
  10. package/dist/commands/worker.d.ts +28 -1
  11. package/dist/commands/worker.js +376 -28
  12. package/dist/index.js +17 -2
  13. package/dist/listingPreflight.d.ts +2 -1
  14. package/dist/listingPreflight.js +49 -18
  15. package/dist/workerAppProject.d.ts +2 -0
  16. package/dist/workerAppProject.js +33 -0
  17. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -2
  18. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  19. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +27 -1
  20. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  21. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +103 -0
  22. package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -2
  23. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  24. package/node_modules/@playdrop/api-client/dist/index.js +10 -0
  25. package/node_modules/@playdrop/config/client-meta.json +2 -2
  26. package/node_modules/@playdrop/types/dist/api.d.ts +150 -2
  27. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  28. package/node_modules/@playdrop/types/dist/api.js +3 -1
  29. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.16",
2
+ "version": "0.13.17",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.16",
4
+ "runtimeSdkVersion": "0.13.17",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -240,6 +240,7 @@ function buildStaticAppTask(input) {
240
240
  hasValidateScript: false,
241
241
  hasFormatScript: false,
242
242
  displayName: input.metadata.title,
243
+ ...(input.metadata.description ? { subtitle: input.metadata.description } : {}),
243
244
  ...(input.metadata.description ? { description: input.metadata.description } : {}),
244
245
  type: 'GAME',
245
246
  surfaceTargets: [...STATIC_GAME_SURFACES],
@@ -154,7 +154,7 @@ function resolveWorkspaceSelectedAccount(cfg, currentAccount, workspaceAuth, req
154
154
  matchingSessions,
155
155
  };
156
156
  }
157
- if (workspaceAuth.config.taskToken && currentAccount) {
157
+ if (currentAccount) {
158
158
  if (currentAccount.env === preferredEnv) {
159
159
  return {
160
160
  account: currentAccount,
@@ -22,6 +22,7 @@ type ParsedCaptureListingOptions = {
22
22
  posterAtSeconds: number;
23
23
  audio: boolean;
24
24
  outputDir: string | null;
25
+ outputDirInput: string | null;
25
26
  keepRaw: boolean;
26
27
  explicitDimensions: boolean;
27
28
  };
@@ -70,6 +71,43 @@ type ListingRecorderMetadata = {
70
71
  recordedDurationSeconds: number;
71
72
  recordedFileSizeBytes: number;
72
73
  };
74
+ type ListingCaptureSurfaceReport = {
75
+ surface: AppSurface;
76
+ targetUrl: string;
77
+ command: {
78
+ durationSeconds: number;
79
+ width: number;
80
+ height: number;
81
+ fps: number;
82
+ posterAtSeconds: number;
83
+ audio: boolean;
84
+ keepRaw: boolean;
85
+ };
86
+ measurement: HostedGameMeasurement;
87
+ recorder: ListingRecorderMetadata;
88
+ crop: CropRect;
89
+ rawVideo: {
90
+ path: string | null;
91
+ width: number;
92
+ height: number;
93
+ durationSeconds: number;
94
+ audioTrackCount: number;
95
+ };
96
+ finalVideo: {
97
+ path: string;
98
+ width: number;
99
+ height: number;
100
+ durationSeconds: number;
101
+ audioTrackCount: number;
102
+ fps: number | null;
103
+ };
104
+ posterPath: string;
105
+ warnings: string[];
106
+ };
107
+ export type ExportedListingAudio = {
108
+ mimeType: string;
109
+ base64: string;
110
+ };
73
111
  export declare function resolveListingCaptureRouteSegment(previewable: boolean): 'dev-preview';
74
112
  export declare function resolveCaptureListingDevRouterPort(env?: NodeJS.ProcessEnv): number;
75
113
  export declare function resolveListingCaptureLocalDevPort(devRouterPort: number): number | null;
@@ -84,6 +122,7 @@ export declare function buildListingCaptureFrameUrl({ webBase, currentUsername,
84
122
  }): string;
85
123
  export declare function resolveListingCaptureSceneId(width: number, height: number): 'listing-landscape' | 'listing-portrait';
86
124
  export declare function parseCaptureListingOptions(targetArg: string | undefined, options?: CaptureListingOptions): ParsedCaptureListingOptions;
125
+ export declare function resolveCaptureListingExplicitOutputDir(outputDirInput: string | null, cwdResolvedOutputDir: string | null, projectDir: string): string | null;
87
126
  export declare function assertSupportedListingEnvironment(platform?: NodeJS.Platform, macosVersion?: string): void;
88
127
  export declare function resolveListingRecorderPath(workerHome?: string): string;
89
128
  export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppSurface, useSurfacePrefix: boolean): {
@@ -92,6 +131,7 @@ export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppS
92
131
  posterPath: string;
93
132
  finalVideoPath: string;
94
133
  };
134
+ export declare function resolveWorkerCaptureOutputDir(projectDir: string, isWorkerGameTask: boolean): string | null;
95
135
  export declare function resolveCaptureDimensions(surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): {
96
136
  width: number;
97
137
  height: number;
@@ -100,7 +140,10 @@ export declare function resolveListingCaptureBrowserContextOptions(surface: AppS
100
140
  width: number;
101
141
  height: number;
102
142
  }): BrowserContextOptions;
143
+ export declare function assertExportedListingAudio(value: unknown): ExportedListingAudio;
103
144
  export declare function computeRecordedCrop(measurement: HostedGameMeasurement, recorder: ListingRecorderMetadata, rawWidth: number, rawHeight: number): CropRect;
104
145
  export declare function runListingRecorder(recorderPath: string, pid: number, durationSeconds: number, rawOutputPath: string, metadataPath: string, audio: boolean, deadlineAt: number): Promise<ListingRecorderMetadata>;
146
+ export declare function captureCommandMatchesExceptPoster(capture: ListingCaptureSurfaceReport, surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): boolean;
147
+ export declare function resolveCurrentCaptureReuseAction(captures: ListingCaptureSurfaceReport[], surfaces: AppSurface[], parsedOptions: ParsedCaptureListingOptions): 'reuse' | 'refresh_poster';
105
148
  export declare function captureListing(targetArg: string | undefined, options?: CaptureListingOptions): Promise<void>;
106
149
  export {};
@@ -39,13 +39,18 @@ exports.resolveListingCaptureLocalDevPort = resolveListingCaptureLocalDevPort;
39
39
  exports.buildListingCaptureFrameUrl = buildListingCaptureFrameUrl;
40
40
  exports.resolveListingCaptureSceneId = resolveListingCaptureSceneId;
41
41
  exports.parseCaptureListingOptions = parseCaptureListingOptions;
42
+ exports.resolveCaptureListingExplicitOutputDir = resolveCaptureListingExplicitOutputDir;
42
43
  exports.assertSupportedListingEnvironment = assertSupportedListingEnvironment;
43
44
  exports.resolveListingRecorderPath = resolveListingRecorderPath;
44
45
  exports.buildSurfaceOutputPaths = buildSurfaceOutputPaths;
46
+ exports.resolveWorkerCaptureOutputDir = resolveWorkerCaptureOutputDir;
45
47
  exports.resolveCaptureDimensions = resolveCaptureDimensions;
46
48
  exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowserContextOptions;
49
+ exports.assertExportedListingAudio = assertExportedListingAudio;
47
50
  exports.computeRecordedCrop = computeRecordedCrop;
48
51
  exports.runListingRecorder = runListingRecorder;
52
+ exports.captureCommandMatchesExceptPoster = captureCommandMatchesExceptPoster;
53
+ exports.resolveCurrentCaptureReuseAction = resolveCurrentCaptureReuseAction;
49
54
  exports.captureListing = captureListing;
50
55
  const types_1 = require("@playdrop/types");
51
56
  const node_child_process_1 = require("node:child_process");
@@ -53,10 +58,12 @@ const node_crypto_1 = require("node:crypto");
53
58
  const promises_1 = require("node:fs/promises");
54
59
  const node_path_1 = require("node:path");
55
60
  const appUrls_1 = require("../appUrls");
61
+ const build_1 = require("../apps/build");
56
62
  const catalogue_1 = require("../catalogue");
57
63
  const catalogue_utils_1 = require("../catalogue-utils");
58
64
  const commandContext_1 = require("../commandContext");
59
65
  const http_1 = require("../http");
66
+ const listingPreflight_1 = require("../listingPreflight");
60
67
  const messages_1 = require("../messages");
61
68
  const playwright_1 = require("../playwright");
62
69
  const recorderRelease_1 = require("../recorderRelease");
@@ -66,6 +73,7 @@ const devRuntimeAssets_1 = require("./devRuntimeAssets");
66
73
  const devServer_1 = require("./devServer");
67
74
  const devShared_1 = require("./devShared");
68
75
  const taskCaptureSession_1 = require("./taskCaptureSession");
76
+ const workerAppProject_1 = require("../workerAppProject");
69
77
  const CAPTURE_FRAME_SELECTOR = 'iframe[title="Game"]';
70
78
  const DEFAULT_DURATION_SECONDS = 8;
71
79
  const DEFAULT_WIDTH = 1280;
@@ -158,7 +166,8 @@ function parseCaptureListingOptions(targetArg, options = {}) {
158
166
  minimum: 0,
159
167
  maximum: durationSeconds,
160
168
  });
161
- const outputDir = options.outputDir?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.outputDir.trim()) : null;
169
+ const outputDirInput = options.outputDir?.trim() || null;
170
+ const outputDir = outputDirInput ? (0, node_path_1.resolve)(process.cwd(), outputDirInput) : null;
162
171
  return {
163
172
  targetArg,
164
173
  appName: options.app?.trim() || undefined,
@@ -169,10 +178,18 @@ function parseCaptureListingOptions(targetArg, options = {}) {
169
178
  posterAtSeconds,
170
179
  audio: Boolean(options.audio),
171
180
  outputDir,
181
+ outputDirInput,
172
182
  keepRaw: Boolean(options.keepRaw),
173
183
  explicitDimensions,
174
184
  };
175
185
  }
186
+ function resolveCaptureListingExplicitOutputDir(outputDirInput, cwdResolvedOutputDir, projectDir) {
187
+ if (!outputDirInput || !cwdResolvedOutputDir)
188
+ return null;
189
+ return (0, node_path_1.isAbsolute)(outputDirInput)
190
+ ? cwdResolvedOutputDir
191
+ : (0, node_path_1.resolve)(projectDir, outputDirInput);
192
+ }
176
193
  function readMacOsVersion() {
177
194
  const result = (0, node_child_process_1.spawnSync)('sw_vers', ['-productVersion'], {
178
195
  encoding: 'utf8',
@@ -231,6 +248,11 @@ function buildSurfaceOutputPaths(outputDir, surface, useSurfacePrefix) {
231
248
  finalVideoPath: (0, node_path_1.join)(outputDir, `${prefix}listing.mp4`),
232
249
  };
233
250
  }
251
+ function resolveWorkerCaptureOutputDir(projectDir, isWorkerGameTask) {
252
+ return isWorkerGameTask
253
+ ? (0, node_path_1.join)(projectDir, 'assets', 'marketing', 'playdrop', 'capture')
254
+ : null;
255
+ }
234
256
  function resolveCaptureDimensions(surface, parsedOptions) {
235
257
  if (parsedOptions.explicitDimensions) {
236
258
  return {
@@ -432,6 +454,21 @@ async function startHostedListingAudioCapture(page) {
432
454
  await hook.startAudioCapture();
433
455
  });
434
456
  }
457
+ function assertExportedListingAudio(value) {
458
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
459
+ throw new Error('listing_audio_capture_export_missing');
460
+ }
461
+ const mimeType = typeof value.mimeType === 'string'
462
+ ? value.mimeType.trim()
463
+ : '';
464
+ const base64 = typeof value.base64 === 'string'
465
+ ? value.base64.trim()
466
+ : '';
467
+ if (!mimeType || !base64) {
468
+ throw new Error('listing_audio_capture_export_missing');
469
+ }
470
+ return { mimeType, base64 };
471
+ }
435
472
  function resolveExportedAudioFileName(mimeType) {
436
473
  if (mimeType.includes('webm')) {
437
474
  return 'listing-audio.webm';
@@ -443,18 +480,23 @@ function resolveExportedAudioFileName(mimeType) {
443
480
  }
444
481
  async function stopHostedListingAudioCapture(page, outputDir) {
445
482
  const frame = await waitForHostedGameFrame(page, 20000);
446
- const exportedAudio = await frame.evaluate(async () => {
483
+ const exportedAudio = assertExportedListingAudio(await frame.evaluate(async () => {
447
484
  const captureWindow = window;
448
485
  const hook = captureWindow.__listingCapture;
449
486
  if (!hook || typeof hook.stopAudioCapture !== 'function') {
450
487
  throw new Error('listing_audio_capture_hook_missing');
451
488
  }
452
489
  return hook.stopAudioCapture();
453
- });
490
+ }));
454
491
  const filePath = (0, node_path_1.join)(outputDir, resolveExportedAudioFileName(exportedAudio.mimeType));
455
492
  await (0, promises_1.writeFile)(filePath, Buffer.from(exportedAudio.base64, 'base64'));
456
493
  return filePath;
457
494
  }
495
+ async function assertHostedListingAudioCaptureContract(page, outputDir) {
496
+ await startHostedListingAudioCapture(page);
497
+ await page.waitForTimeout(300);
498
+ await stopHostedListingAudioCapture(page, outputDir);
499
+ }
458
500
  async function fitWindowToRequestedGameplay(page, requestedWidth, requestedHeight) {
459
501
  const cdpSession = await page.context().newCDPSession(page);
460
502
  let measurement = await waitForHostedGameMeasurement(page, 15000);
@@ -666,6 +708,24 @@ function formatCommandError(error) {
666
708
  suggestions: ['Set previewable: true in catalogue.json, implement the preview scene and __listingCapture hooks, then rerun "playdrop project capture".'],
667
709
  };
668
710
  }
711
+ if (error.message === 'listing_audio_capture_export_missing') {
712
+ return {
713
+ message: 'The game preview audio hook did not export recorded audio.',
714
+ suggestions: ['Make window.__listingCapture.stopAudioCapture() resolve { mimeType, base64 }; toggling game audio on or off is not an export.'],
715
+ };
716
+ }
717
+ if (error.message === 'listing_capture_runtime_already_recorded') {
718
+ return {
719
+ message: 'This exact game runtime already has a canonical recording with different capture settings.',
720
+ suggestions: ['Use the existing video, or change and revalidate the preview implementation before recording the changed runtime once.'],
721
+ };
722
+ }
723
+ if (error.message === 'listing_capture_current_report_incomplete') {
724
+ return {
725
+ message: 'The canonical capture report is incomplete for this runtime.',
726
+ suggestions: ['Fix or remove the incomplete canonical capture directory, then record the runtime once.'],
727
+ };
728
+ }
669
729
  if (error.message.startsWith('listing_recorder_failed:')) {
670
730
  return {
671
731
  message: error.message.slice('listing_recorder_failed:'.length),
@@ -710,6 +770,77 @@ async function encodeListingVideo(rawVideoPath, finalVideoPath, posterPath, crop
710
770
  posterPath,
711
771
  ], 'ffmpeg_failed', deadlineAt);
712
772
  }
773
+ function captureCommandMatchesExceptPoster(capture, surface, parsedOptions) {
774
+ const dimensions = resolveCaptureDimensions(surface, parsedOptions);
775
+ return capture.command.durationSeconds === parsedOptions.durationSeconds
776
+ && capture.command.width === dimensions.width
777
+ && capture.command.height === dimensions.height
778
+ && capture.command.fps === parsedOptions.fps
779
+ && capture.command.audio === parsedOptions.audio
780
+ && capture.command.keepRaw === parsedOptions.keepRaw;
781
+ }
782
+ function resolveCurrentCaptureReuseAction(captures, surfaces, parsedOptions) {
783
+ if (captures.length !== surfaces.length) {
784
+ throw new Error('listing_capture_current_report_incomplete');
785
+ }
786
+ const orderedCaptures = surfaces.map((surface) => captures.find((candidate) => candidate.surface === surface));
787
+ if (orderedCaptures.some((capture) => !capture)) {
788
+ throw new Error('listing_capture_current_report_incomplete');
789
+ }
790
+ if (orderedCaptures.some((capture, index) => !captureCommandMatchesExceptPoster(capture, surfaces[index], parsedOptions))) {
791
+ throw new Error('listing_capture_runtime_already_recorded');
792
+ }
793
+ return orderedCaptures.some((capture) => capture.command.posterAtSeconds !== parsedOptions.posterAtSeconds)
794
+ ? 'refresh_poster'
795
+ : 'reuse';
796
+ }
797
+ async function reuseCurrentListingCapture(input) {
798
+ let report;
799
+ try {
800
+ report = JSON.parse(await (0, promises_1.readFile)(input.outputPaths.reportPath, 'utf8'));
801
+ }
802
+ catch (error) {
803
+ if (error?.code === 'ENOENT') {
804
+ return false;
805
+ }
806
+ throw new Error(`listing_capture_report_invalid:${input.outputPaths.reportPath}`);
807
+ }
808
+ if (report.appName !== input.appName) {
809
+ throw new Error(`listing_capture_report_app_mismatch:${report.appName}:${input.appName}`);
810
+ }
811
+ if (!input.runtimeBundleHash || report.binding?.runtimeBundleHash !== input.runtimeBundleHash) {
812
+ return false;
813
+ }
814
+ const action = resolveCurrentCaptureReuseAction(report.captures, input.surfaces, input.parsedOptions);
815
+ const captures = input.surfaces.map((surface) => report.captures.find((candidate) => candidate.surface === surface));
816
+ for (const capture of captures) {
817
+ await (0, promises_1.access)(capture.finalVideo.path);
818
+ }
819
+ if (action === 'reuse') {
820
+ console.log('[listing] The current runtime already has a matching canonical capture; reusing it.');
821
+ console.log(`[listing] Existing report: ${(0, node_path_1.relative)(process.cwd(), input.outputPaths.reportPath) || input.outputPaths.reportPath}`);
822
+ return true;
823
+ }
824
+ for (const capture of captures) {
825
+ runTool('ffmpeg', [
826
+ '-y',
827
+ '-ss',
828
+ String(input.parsedOptions.posterAtSeconds),
829
+ '-i',
830
+ capture.finalVideo.path,
831
+ '-frames:v',
832
+ '1',
833
+ capture.posterPath,
834
+ ], 'ffmpeg_failed', input.deadlineAt);
835
+ capture.command.posterAtSeconds = input.parsedOptions.posterAtSeconds;
836
+ const artifactKey = (0, node_path_1.relative)(input.outputPaths.outputDir, capture.posterPath) || 'poster.png';
837
+ report.binding.artifactHashes[artifactKey] = await computeFileSha256(capture.posterPath);
838
+ }
839
+ await (0, promises_1.writeFile)(input.outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
840
+ console.log(`[listing] Refreshed the canonical poster at ${input.parsedOptions.posterAtSeconds}s without relaunching the game.`);
841
+ console.log(`[listing] Updated report: ${(0, node_path_1.relative)(process.cwd(), input.outputPaths.reportPath) || input.outputPaths.reportPath}`);
842
+ return true;
843
+ }
713
844
  async function muxListingAudio(finalVideoPath, audioPath, deadlineAt) {
714
845
  const muxedVideoPath = `${finalVideoPath}.muxed.mp4`;
715
846
  runTool('ffmpeg', [
@@ -824,8 +955,20 @@ async function captureListing(targetArg, options = {}) {
824
955
  process.exitCode = 1;
825
956
  return;
826
957
  }
827
- const outputPaths = await ensureOutputPaths(appName, parsedOptions.outputDir);
828
958
  const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
959
+ const projectDir = projectInfo.projectDir ?? (0, node_path_1.dirname)(resolvedTarget.htmlPath);
960
+ let isWorkerGameTask = false;
961
+ try {
962
+ isWorkerGameTask = Boolean((0, listingPreflight_1.readWorkerUploadPreflightContext)(projectDir));
963
+ }
964
+ catch (error) {
965
+ const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
966
+ (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
967
+ process.exitCode = 1;
968
+ return;
969
+ }
970
+ const explicitOutputDir = resolveCaptureListingExplicitOutputDir(parsedOptions.outputDirInput, parsedOptions.outputDir, projectDir);
971
+ const outputPaths = await ensureOutputPaths(appName, explicitOutputDir ?? resolveWorkerCaptureOutputDir(projectDir, isWorkerGameTask));
829
972
  const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
830
973
  await (0, commandContext_1.withEnvironment)('project capture', 'Capturing game media', async ({ client, env, envConfig, account, workspaceAuth }) => {
831
974
  let currentUsername = '';
@@ -902,6 +1045,40 @@ async function captureListing(targetArg, options = {}) {
902
1045
  process.exitCode = 1;
903
1046
  return;
904
1047
  }
1048
+ let runtimeBundleHash = null;
1049
+ if (taskLookup.task) {
1050
+ try {
1051
+ const captureTask = (0, listingPreflight_1.readWorkerUploadPreflightContext)(taskLookup.task.projectDir)
1052
+ ? (0, workerAppProject_1.prepareWorkerAppTask)(taskLookup.task)
1053
+ : taskLookup.task;
1054
+ runtimeBundleHash = (await (0, build_1.buildApp)(captureTask)).bundleHash;
1055
+ }
1056
+ catch (error) {
1057
+ (0, messages_1.printErrorWithHelp)(error instanceof Error ? error.message : String(error), ['Fix the game build before recording final gameplay.'], { command: 'project capture' });
1058
+ process.exitCode = 1;
1059
+ return;
1060
+ }
1061
+ }
1062
+ const captureDeadlineAt = Date.now() + listingCaptureLease_1.LISTING_CAPTURE_MAX_DURATION_MS;
1063
+ try {
1064
+ const reused = await reuseCurrentListingCapture({
1065
+ appName,
1066
+ outputPaths,
1067
+ parsedOptions,
1068
+ runtimeBundleHash,
1069
+ surfaces: resolvedTarget.surfaceTargets.list,
1070
+ deadlineAt: captureDeadlineAt,
1071
+ });
1072
+ if (reused) {
1073
+ return;
1074
+ }
1075
+ }
1076
+ catch (error) {
1077
+ const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
1078
+ (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
1079
+ process.exitCode = 1;
1080
+ return;
1081
+ }
905
1082
  const localDevAppUrl = (0, devServer_1.buildLocalDevAppUrl)({
906
1083
  creatorUsername: currentUsername,
907
1084
  appType: appTypeSlug,
@@ -926,7 +1103,6 @@ async function captureListing(targetArg, options = {}) {
926
1103
  return;
927
1104
  }
928
1105
  }
929
- const captureDeadlineAt = Date.now() + listingCaptureLease_1.LISTING_CAPTURE_MAX_DURATION_MS;
930
1106
  let serverAlreadyRunning = false;
931
1107
  let devServerStartedByCapture = false;
932
1108
  let serverHandle = null;
@@ -1020,6 +1196,8 @@ async function captureListing(targetArg, options = {}) {
1020
1196
  await browserHandle.page.waitForTimeout(1000);
1021
1197
  }
1022
1198
  if (shouldExportPreviewAudio) {
1199
+ console.log('[listing] Checking the in-app preview audio export contract.');
1200
+ await assertHostedListingAudioCaptureContract(browserHandle.page, outputPaths.outputDir);
1023
1201
  console.log('[listing] Starting in-app preview audio capture.');
1024
1202
  await startHostedListingAudioCapture(browserHandle.page);
1025
1203
  }
@@ -1126,6 +1304,7 @@ async function captureListing(targetArg, options = {}) {
1126
1304
  binding: {
1127
1305
  artifactHashes,
1128
1306
  capturedSurfaces,
1307
+ runtimeBundleHash,
1129
1308
  },
1130
1309
  };
1131
1310
  await (0, promises_1.writeFile)(outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
@@ -1,5 +1,5 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
- import type { AgentExecutionTarget, UserResponse } from '@playdrop/types';
2
+ import type { AgentExecutionTarget, AppSurface, UserResponse } from '@playdrop/types';
3
3
  import { type AppTask } from '../catalogue';
4
4
  export type UploadCommandOptions = {
5
5
  env?: string;
@@ -17,6 +17,9 @@ export type WorkerAppPublishInput = {
17
17
  kind: 'NEW_GAME' | 'REMIX_GAME' | 'GAME_UPDATE';
18
18
  executionTarget?: AgentExecutionTarget;
19
19
  expectedAppName?: string | null;
20
+ expectedDisplayName?: string | null;
21
+ expectedSubtitle?: string | null;
22
+ expectedPrimarySurface?: AppSurface | null;
20
23
  remixSourceRef?: string | null;
21
24
  playdropAssetRequirement?: WorkerPlaydropAssetRequirement | null;
22
25
  creatorRequest?: string | null;
@@ -69,6 +72,14 @@ export declare function assertWorkerAppLocalUploadPreflight(input: {
69
72
  creatorUsername?: string | null;
70
73
  playdropAssetRequirement?: WorkerPlaydropAssetRequirement | null;
71
74
  creatorRequest?: string | null;
75
+ stage?: 'PRE_CAPTURE' | 'COMPLETE';
76
+ expectedRuntimeBundleHash?: string;
72
77
  }): Promise<void>;
78
+ export type WorkerAppPreflightResult = {
79
+ appName: string;
80
+ creatorUsername: string;
81
+ warnings: string[];
82
+ };
83
+ export declare function preflightWorkerAppProject(input: WorkerAppPublishInput): Promise<WorkerAppPreflightResult>;
73
84
  export declare function publishWorkerAppProject(input: WorkerAppPublishInput): Promise<WorkerAppPublishResult>;
74
85
  export declare function publishStaticHtmlProject(input: StaticHtmlPublishInput): Promise<StaticHtmlPublishResult>;