@playdrop/playdrop-cli 0.11.9 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/build.js +2 -3
  3. package/dist/apps/index.d.ts +4 -5
  4. package/dist/apps/index.js +14 -16
  5. package/dist/apps/loadCheck.d.ts +76 -0
  6. package/dist/apps/loadCheck.js +874 -0
  7. package/dist/apps/upload.d.ts +3 -2
  8. package/dist/apps/upload.js +110 -6
  9. package/dist/catalogue-utils.d.ts +1 -0
  10. package/dist/catalogue-utils.js +1 -0
  11. package/dist/catalogue.d.ts +2 -0
  12. package/dist/catalogue.js +17 -0
  13. package/dist/commands/build.js +4 -1
  14. package/dist/commands/captureListing.d.ts +30 -1
  15. package/dist/commands/captureListing.js +305 -120
  16. package/dist/commands/check.d.ts +8 -0
  17. package/dist/commands/check.js +188 -0
  18. package/dist/commands/review.d.ts +20 -0
  19. package/dist/commands/review.js +112 -0
  20. package/dist/commands/upload-content.d.ts +1 -1
  21. package/dist/commands/upload.d.ts +2 -1
  22. package/dist/commands/upload.js +9 -52
  23. package/dist/commands/validate.js +0 -155
  24. package/dist/commands/worker/runtime.d.ts +1 -0
  25. package/dist/commands/worker/runtime.js +43 -3
  26. package/dist/commands/worker.d.ts +1 -1
  27. package/dist/commands/worker.js +22 -44
  28. package/dist/index.js +33 -49
  29. package/dist/playwright.d.ts +5 -0
  30. package/dist/playwright.js +60 -1
  31. package/node_modules/@playdrop/api-client/dist/client.d.ts +4 -2
  32. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/client.js +0 -10
  34. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  35. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  36. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  37. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  38. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  39. package/node_modules/@playdrop/api-client/dist/domains/auth.js +22 -0
  40. package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -3
  41. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  42. package/node_modules/@playdrop/api-client/dist/index.js +10 -5
  43. package/node_modules/@playdrop/config/client-meta.json +2 -2
  44. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts +8 -0
  45. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts.map +1 -0
  46. package/node_modules/@playdrop/config/dist/src/client-meta.js +51 -0
  47. package/node_modules/@playdrop/config/dist/src/deployment.d.ts.map +1 -1
  48. package/node_modules/@playdrop/config/dist/src/deployment.js +2 -32
  49. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  50. package/node_modules/@playdrop/config/dist/src/index.js +2 -28
  51. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts +0 -1
  52. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts.map +1 -1
  53. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.js +21 -5
  54. package/node_modules/@playdrop/config/dist/test/runtime-service-origins.test.js +8 -0
  55. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  56. package/node_modules/@playdrop/types/dist/api.d.ts +19 -14
  57. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  58. package/node_modules/@playdrop/types/dist/api.js +5 -10
  59. package/node_modules/@playdrop/types/dist/version.d.ts +14 -0
  60. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/dist/apps/launchCheck.d.ts +0 -39
  63. package/dist/apps/launchCheck.js +0 -414
  64. package/dist/captureRuntime.d.ts +0 -73
  65. package/dist/captureRuntime.js +0 -1021
  66. package/dist/commands/capture.d.ts +0 -14
  67. package/dist/commands/capture.js +0 -548
  68. package/dist/commands/captureRemote.d.ts +0 -17
  69. package/dist/commands/captureRemote.js +0 -394
  70. package/node_modules/@playdrop/types/dist/entity.d.ts +0 -31
  71. package/node_modules/@playdrop/types/dist/entity.d.ts.map +0 -1
  72. package/node_modules/@playdrop/types/dist/entity.js +0 -67
@@ -34,22 +34,32 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.resolveListingCaptureRouteSegment = resolveListingCaptureRouteSegment;
37
+ exports.resolveCaptureListingDevRouterPort = resolveCaptureListingDevRouterPort;
38
+ exports.resolveListingCaptureLocalDevPort = resolveListingCaptureLocalDevPort;
39
+ exports.buildListingCaptureFrameUrl = buildListingCaptureFrameUrl;
37
40
  exports.resolveListingCaptureSceneId = resolveListingCaptureSceneId;
38
41
  exports.parseCaptureListingOptions = parseCaptureListingOptions;
39
42
  exports.assertSupportedListingEnvironment = assertSupportedListingEnvironment;
40
43
  exports.resolveListingRecorderPath = resolveListingRecorderPath;
44
+ exports.buildSurfaceOutputPaths = buildSurfaceOutputPaths;
45
+ exports.resolveCaptureDimensions = resolveCaptureDimensions;
46
+ exports.resolveListingCaptureBrowserContextOptions = resolveListingCaptureBrowserContextOptions;
41
47
  exports.computeRecordedCrop = computeRecordedCrop;
42
48
  exports.captureListing = captureListing;
43
49
  const types_1 = require("@playdrop/types");
44
50
  const node_child_process_1 = require("node:child_process");
51
+ const node_crypto_1 = require("node:crypto");
45
52
  const promises_1 = require("node:fs/promises");
46
53
  const node_path_1 = require("node:path");
47
54
  const appUrls_1 = require("../appUrls");
55
+ const catalogue_1 = require("../catalogue");
48
56
  const catalogue_utils_1 = require("../catalogue-utils");
49
57
  const commandContext_1 = require("../commandContext");
50
58
  const http_1 = require("../http");
51
59
  const messages_1 = require("../messages");
52
60
  const playwright_1 = require("../playwright");
61
+ const dev_1 = require("./dev");
62
+ const devRuntimeAssets_1 = require("./devRuntimeAssets");
53
63
  const devServer_1 = require("./devServer");
54
64
  const devShared_1 = require("./devShared");
55
65
  const CAPTURE_FRAME_SELECTOR = 'iframe[title="Game"]';
@@ -63,10 +73,43 @@ const MIN_CAPTURE_SECONDS = 1;
63
73
  const MIN_DIMENSION = 16;
64
74
  const MAX_DIMENSION = 8192;
65
75
  const MAX_FPS = 120;
66
- const SUPPORTED_MACOS_PREFIX = '26.4';
76
+ const MIN_SUPPORTED_MACOS_MAJOR = 26;
77
+ const MIN_SUPPORTED_MACOS_MINOR = 4;
67
78
  const LISTING_CAPTURE_HOOK_TIMEOUT_MS = 60000;
79
+ const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
80
+ const LISTING_CAPTURE_MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
81
+ const SURFACE_CAPTURE_DIMENSIONS = {
82
+ DESKTOP: { width: 1280, height: 720 },
83
+ MOBILE_LANDSCAPE: { width: 844, height: 390 },
84
+ MOBILE_PORTRAIT: { width: 390, height: 844 },
85
+ };
86
+ const SURFACE_OUTPUT_SLUG = {
87
+ DESKTOP: 'desktop',
88
+ MOBILE_LANDSCAPE: 'mobile-landscape',
89
+ MOBILE_PORTRAIT: 'mobile-portrait',
90
+ };
68
91
  function resolveListingCaptureRouteSegment(previewable) {
69
- return previewable ? 'dev-preview' : 'dev';
92
+ if (!previewable) {
93
+ throw new Error('listing_preview_support_missing');
94
+ }
95
+ return 'dev-preview';
96
+ }
97
+ function resolveCaptureListingDevRouterPort(env = process.env) {
98
+ return (0, dev_1.resolveCliDevRouterPort)(env);
99
+ }
100
+ function resolveListingCaptureLocalDevPort(devRouterPort) {
101
+ return devRouterPort === devServer_1.DEV_ROUTER_PORT ? null : devRouterPort;
102
+ }
103
+ function buildListingCaptureFrameUrl({ webBase, currentUsername, appTypeSlug, appName, routeSegment, localDevPort, }) {
104
+ const frameUrlObject = new URL(`${(0, appUrls_1.normalizePlaydropWebBase)(webBase)}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/${routeSegment}`);
105
+ frameUrlObject.searchParams.set('devAuth', 'anonymous');
106
+ frameUrlObject.searchParams.set('launchCheck', '1');
107
+ frameUrlObject.searchParams.set('captureShell', 'listing');
108
+ frameUrlObject.searchParams.set('mobileFrameHost', 'admin');
109
+ if (localDevPort !== null) {
110
+ frameUrlObject.searchParams.set('localDevPort', String(localDevPort));
111
+ }
112
+ return frameUrlObject.toString();
70
113
  }
71
114
  function resolveListingCaptureSceneId(width, height) {
72
115
  return height > width ? 'listing-portrait' : 'listing-landscape';
@@ -92,6 +135,7 @@ function parseCaptureListingOptions(targetArg, options = {}) {
92
135
  minimum: MIN_CAPTURE_SECONDS,
93
136
  maximum: MAX_CAPTURE_SECONDS,
94
137
  });
138
+ const explicitDimensions = options.width !== undefined || options.height !== undefined;
95
139
  const width = parsePositiveNumber(options.width, DEFAULT_WIDTH, 'width', {
96
140
  minimum: MIN_DIMENSION,
97
141
  maximum: MAX_DIMENSION,
@@ -112,6 +156,12 @@ function parseCaptureListingOptions(targetArg, options = {}) {
112
156
  maximum: durationSeconds,
113
157
  });
114
158
  const outputDir = options.outputDir?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.outputDir.trim()) : null;
159
+ const contentHash = typeof options.contentHash === 'string' && options.contentHash.trim().length > 0
160
+ ? options.contentHash.trim().toLowerCase()
161
+ : null;
162
+ if (contentHash && !SHA256_HEX_REGEX.test(contentHash)) {
163
+ throw new Error('content_hash_invalid');
164
+ }
115
165
  return {
116
166
  targetArg,
117
167
  appName: options.app?.trim() || undefined,
@@ -123,6 +173,8 @@ function parseCaptureListingOptions(targetArg, options = {}) {
123
173
  audio: Boolean(options.audio),
124
174
  outputDir,
125
175
  keepRaw: Boolean(options.keepRaw),
176
+ contentHash,
177
+ explicitDimensions,
126
178
  };
127
179
  }
128
180
  function readMacOsVersion() {
@@ -138,7 +190,13 @@ function assertSupportedListingEnvironment(platform = process.platform, macosVer
138
190
  if (platform !== 'darwin') {
139
191
  throw new Error('unsupported_platform');
140
192
  }
141
- if (!macosVersion.startsWith(SUPPORTED_MACOS_PREFIX)) {
193
+ const [majorRaw, minorRaw] = macosVersion.split('.');
194
+ const major = Number.parseInt(majorRaw ?? '', 10);
195
+ const minor = Number.parseInt(minorRaw ?? '', 10);
196
+ if (!Number.isInteger(major)
197
+ || !Number.isInteger(minor)
198
+ || major !== MIN_SUPPORTED_MACOS_MAJOR
199
+ || minor < MIN_SUPPORTED_MACOS_MINOR) {
142
200
  throw new Error(`unsupported_macos_version:${macosVersion}`);
143
201
  }
144
202
  }
@@ -165,11 +223,40 @@ async function ensureOutputPaths(appName, explicitOutputDir) {
165
223
  await (0, promises_1.mkdir)(outputDir, { recursive: true });
166
224
  return {
167
225
  outputDir,
168
- rawVideoPath: (0, node_path_1.join)(outputDir, 'listing-raw.mp4'),
169
- metadataPath: (0, node_path_1.join)(outputDir, 'listing-raw.json'),
170
226
  reportPath: (0, node_path_1.join)(outputDir, 'capture-report.json'),
171
- posterPath: (0, node_path_1.join)(outputDir, 'poster.png'),
172
- finalVideoPath: (0, node_path_1.join)(outputDir, 'listing.mp4'),
227
+ };
228
+ }
229
+ function buildSurfaceOutputPaths(outputDir, surface, useSurfacePrefix) {
230
+ const prefix = useSurfacePrefix ? `${SURFACE_OUTPUT_SLUG[surface]}-` : '';
231
+ return {
232
+ rawVideoPath: (0, node_path_1.join)(outputDir, `${prefix}listing-raw.mp4`),
233
+ metadataPath: (0, node_path_1.join)(outputDir, `${prefix}listing-raw.json`),
234
+ posterPath: (0, node_path_1.join)(outputDir, `${prefix}poster.png`),
235
+ finalVideoPath: (0, node_path_1.join)(outputDir, `${prefix}listing.mp4`),
236
+ };
237
+ }
238
+ function resolveCaptureDimensions(surface, parsedOptions) {
239
+ if (parsedOptions.explicitDimensions) {
240
+ return {
241
+ width: parsedOptions.width,
242
+ height: parsedOptions.height,
243
+ };
244
+ }
245
+ return SURFACE_CAPTURE_DIMENSIONS[surface];
246
+ }
247
+ function resolveListingCaptureBrowserContextOptions(surface, dimensions) {
248
+ if (surface === 'DESKTOP') {
249
+ return { viewport: null };
250
+ }
251
+ return {
252
+ viewport: {
253
+ width: dimensions.width,
254
+ height: dimensions.height,
255
+ },
256
+ deviceScaleFactor: 2,
257
+ isMobile: true,
258
+ hasTouch: true,
259
+ userAgent: LISTING_CAPTURE_MOBILE_USER_AGENT,
173
260
  };
174
261
  }
175
262
  async function grantListingBrowserLocalNetworkAccess(context, origin) {
@@ -179,7 +266,7 @@ async function grantListingBrowserLocalNetworkAccess(context, origin) {
179
266
  }
180
267
  await grantPermissions.call(context, ['local-network-access'], { origin });
181
268
  }
182
- async function launchListingBrowser(initialViewport, automationOrigin) {
269
+ async function launchListingBrowser(initialViewport, automationOrigin, contextOptions) {
183
270
  const playwright = await Promise.resolve().then(() => __importStar(require('playwright-core')));
184
271
  const args = [
185
272
  '--no-sandbox',
@@ -206,7 +293,7 @@ async function launchListingBrowser(initialViewport, automationOrigin) {
206
293
  throw new Error('browser_process_missing');
207
294
  }
208
295
  const browser = await playwright.chromium.connect(server.wsEndpoint());
209
- const context = await browser.newContext({ viewport: null });
296
+ const context = await browser.newContext(contextOptions);
210
297
  await grantListingBrowserLocalNetworkAccess(context, automationOrigin);
211
298
  const page = await context.newPage();
212
299
  return {
@@ -470,6 +557,9 @@ async function probeMediaFile(filePath) {
470
557
  ], 'ffprobe_failed');
471
558
  return JSON.parse(stdout);
472
559
  }
560
+ async function computeFileSha256(filePath) {
561
+ return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(filePath)).digest('hex');
562
+ }
473
563
  function parseFrameRate(raw) {
474
564
  if (!raw) {
475
565
  return null;
@@ -532,14 +622,14 @@ async function runListingRecorder(recorderPath, pid, durationSeconds, rawOutputP
532
622
  function formatCommandError(error) {
533
623
  if (error.message === 'unsupported_platform') {
534
624
  return {
535
- message: 'Listing capture only runs on macOS.',
625
+ message: 'Game media capture only runs on macOS.',
536
626
  suggestions: [],
537
627
  };
538
628
  }
539
629
  if (error.message.startsWith('unsupported_macos_version:')) {
540
630
  const version = error.message.split(':')[1] || 'unknown';
541
631
  return {
542
- message: `Listing capture only supports macOS ${SUPPORTED_MACOS_PREFIX} on this machine. Current version: ${version}.`,
632
+ message: `Game media capture requires macOS ${MIN_SUPPORTED_MACOS_MAJOR}.${MIN_SUPPORTED_MACOS_MINOR} or newer on this machine. Current version: ${version}.`,
543
633
  suggestions: [],
544
634
  };
545
635
  }
@@ -549,6 +639,18 @@ function formatCommandError(error) {
549
639
  suggestions: [],
550
640
  };
551
641
  }
642
+ if (error.message === 'content_hash_invalid') {
643
+ return {
644
+ message: 'The --content-hash value must be a 64-character SHA-256 hex string.',
645
+ suggestions: [],
646
+ };
647
+ }
648
+ if (error.message === 'listing_preview_support_missing') {
649
+ return {
650
+ message: 'This game does not declare preview support, so listing capture cannot run.',
651
+ suggestions: ['Set previewable: true in catalogue.json, implement the preview scene and __listingCapture hooks, then rerun "playdrop project capture".'],
652
+ };
653
+ }
552
654
  if (error.message.startsWith('listing_recorder_failed:')) {
553
655
  return {
554
656
  message: error.message.slice('listing_recorder_failed:'.length),
@@ -628,7 +730,7 @@ function ensureCaptureListingToolAvailable(command) {
628
730
  return true;
629
731
  }
630
732
  (0, messages_1.printErrorWithHelp)(`${command} is required for listing capture.`, [`Install ${command} and make sure it is available on PATH.`], {
631
- command: 'project capture listing',
733
+ command: 'project capture',
632
734
  });
633
735
  process.exitCode = 1;
634
736
  return false;
@@ -639,7 +741,7 @@ async function ensureListingRecorderAvailable(recorderPath) {
639
741
  return true;
640
742
  }
641
743
  catch {
642
- (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture listing' });
744
+ (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture' });
643
745
  process.exitCode = 1;
644
746
  return false;
645
747
  }
@@ -656,20 +758,22 @@ function resolveCaptureListingTarget(parsedOptions) {
656
758
  : code === 'catalogue_missing'
657
759
  ? ['Run the command inside a Playdrop workspace or pass the direct HTML file path.']
658
760
  : ['Provide a valid app target or HTML file path.'];
659
- (0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project capture listing' });
761
+ (0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project capture' });
660
762
  process.exitCode = 1;
661
763
  return null;
662
764
  }
663
765
  }
664
766
  async function captureListing(targetArg, options = {}) {
665
767
  let parsedOptions;
768
+ let devRouterPort;
666
769
  try {
667
770
  parsedOptions = parseCaptureListingOptions(targetArg, options);
771
+ devRouterPort = resolveCaptureListingDevRouterPort();
668
772
  assertSupportedListingEnvironment();
669
773
  }
670
774
  catch (error) {
671
775
  const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
672
- (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture listing' });
776
+ (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
673
777
  process.exitCode = 1;
674
778
  return;
675
779
  }
@@ -686,20 +790,30 @@ async function captureListing(targetArg, options = {}) {
686
790
  }
687
791
  let appName = resolvedTarget.appName;
688
792
  let appTypeSlug = (0, appUrls_1.getAppTypeSlug)(null);
793
+ let declaresPreviewSupport = false;
689
794
  try {
690
- const match = (0, catalogue_utils_1.findAppDefinition)(resolvedTarget.htmlPath);
795
+ const match = (0, catalogue_utils_1.findAppCatalogueInfo)(resolvedTarget.htmlPath);
691
796
  appName = match.name;
692
797
  appTypeSlug = (0, appUrls_1.getAppTypeSlug)(match.type);
798
+ declaresPreviewSupport = match.previewable;
693
799
  }
694
800
  catch {
695
- // keep defaults for direct-file targets outside a registered catalogue
801
+ declaresPreviewSupport = false;
802
+ }
803
+ try {
804
+ resolveListingCaptureRouteSegment(declaresPreviewSupport);
805
+ }
806
+ catch (error) {
807
+ const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
808
+ (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
809
+ process.exitCode = 1;
810
+ return;
696
811
  }
697
812
  const outputPaths = await ensureOutputPaths(appName, parsedOptions.outputDir);
698
813
  const projectInfo = (0, devShared_1.findProjectInfo)(resolvedTarget.htmlPath);
699
814
  const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
700
- await (0, commandContext_1.withEnvironment)('project capture listing', 'Capturing listing media', async ({ client, env, envConfig }) => {
815
+ await (0, commandContext_1.withEnvironment)('project capture', 'Capturing game media', async ({ client, env, envConfig }) => {
701
816
  let currentUsername = '';
702
- let registeredApp = null;
703
817
  try {
704
818
  const currentUser = await (0, devShared_1.fetchDevUser)(client);
705
819
  currentUsername = currentUser.username.trim();
@@ -717,36 +831,36 @@ async function captureListing(targetArg, options = {}) {
717
831
  (0, messages_1.printErrorWithHelp)(`Could not fetch your account (status ${error.status}).`, [
718
832
  'Run "playdrop auth login" to refresh your session.',
719
833
  'Use "playdrop auth whoami" afterwards to verify the current account.',
720
- ], { command: 'project capture listing' });
834
+ ], { command: 'project capture' });
721
835
  process.exitCode = 1;
722
836
  return;
723
837
  }
724
838
  if ((0, devShared_1.isNetworkError)(error)) {
725
- (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your account.', 'project capture listing');
839
+ (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your account.', 'project capture');
726
840
  process.exitCode = 1;
727
841
  return;
728
842
  }
729
843
  throw error;
730
844
  }
731
845
  try {
732
- registeredApp = await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
846
+ await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
733
847
  }
734
848
  catch (error) {
735
849
  if (error instanceof http_1.CLIUnsupportedClientError) {
736
850
  return;
737
851
  }
738
852
  if (error instanceof types_1.UnsupportedClientError) {
739
- (0, http_1.handleUnsupportedError)(error, 'Listing capture');
853
+ (0, http_1.handleUnsupportedError)(error, 'Game media capture');
740
854
  process.exitCode = 1;
741
855
  return;
742
856
  }
743
857
  if (error instanceof types_1.ApiError && error.status === 404) {
744
- (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [`Run "playdrop project create app ${appName}" before using listing capture.`], { command: 'project capture listing' });
858
+ (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [`Run "playdrop project create app ${appName}" before using listing capture.`], { command: 'project capture' });
745
859
  process.exitCode = 1;
746
860
  return;
747
861
  }
748
862
  if ((0, devShared_1.isNetworkError)(error)) {
749
- (0, messages_1.printNetworkIssue)('Could not verify the app registration.', 'project capture listing');
863
+ (0, messages_1.printNetworkIssue)('Could not verify the app registration.', 'project capture');
750
864
  process.exitCode = 1;
751
865
  return;
752
866
  }
@@ -754,11 +868,41 @@ async function captureListing(targetArg, options = {}) {
754
868
  }
755
869
  const entryLabel = (0, node_path_1.relative)(process.cwd(), resolvedTarget.htmlPath) || resolvedTarget.htmlPath;
756
870
  console.log(`[listing] Preparing ${entryLabel} for ${env} (${appTypeSlug}).`);
871
+ const taskLookup = (0, catalogue_1.findAppTaskByFile)(resolvedTarget.htmlPath);
872
+ if (taskLookup.errors.length > 0) {
873
+ (0, messages_1.printErrorWithHelp)(taskLookup.errors[0] || 'Failed to resolve the app task from catalogue.json.', taskLookup.errors.slice(1), { command: 'project capture' });
874
+ process.exitCode = 1;
875
+ return;
876
+ }
877
+ const localDevAppUrl = (0, devServer_1.buildLocalDevAppUrl)({
878
+ creatorUsername: currentUsername,
879
+ appType: appTypeSlug,
880
+ appName,
881
+ port: devRouterPort,
882
+ });
883
+ let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
884
+ if (taskLookup.task) {
885
+ try {
886
+ runtimeAssetManifest = await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
887
+ client,
888
+ apiBase: envConfig.apiBase,
889
+ task: taskLookup.task,
890
+ creatorUsername: currentUsername,
891
+ appBaseUrl: new URL('.', localDevAppUrl).toString(),
892
+ });
893
+ }
894
+ catch (error) {
895
+ const formatted = (0, dev_1.formatDevRuntimeAssetManifestFailure)(error);
896
+ (0, messages_1.printErrorWithHelp)(formatted.message, formatted.suggestions, { command: 'project capture' });
897
+ process.exitCode = 1;
898
+ return;
899
+ }
900
+ }
757
901
  const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
758
902
  creatorUsername: currentUsername,
759
903
  appType: appTypeSlug,
760
904
  appName,
761
- port: devServer_1.DEV_ROUTER_PORT,
905
+ port: devRouterPort,
762
906
  }, 750);
763
907
  const devServerStartedByCapture = !serverAlreadyRunning;
764
908
  let serverHandle = null;
@@ -778,12 +922,13 @@ async function captureListing(targetArg, options = {}) {
778
922
  };
779
923
  try {
780
924
  if (serverAlreadyRunning) {
781
- console.log(`[listing] Reusing dev server at ${(0, devServer_1.buildLocalDevAppUrl)({
925
+ await (0, devServer_1.updateMountedDevRuntimeAssetManifest)({
782
926
  creatorUsername: currentUsername,
783
- appType: appTypeSlug,
784
927
  appName,
785
- port: devServer_1.DEV_ROUTER_PORT,
786
- })}`);
928
+ runtimeAssetManifest,
929
+ port: devRouterPort,
930
+ });
931
+ console.log(`[listing] Reusing dev server at ${localDevAppUrl}`);
787
932
  }
788
933
  else {
789
934
  serverHandle = await (0, devServer_1.startDevServer)({
@@ -791,8 +936,9 @@ async function captureListing(targetArg, options = {}) {
791
936
  appType: appTypeSlug,
792
937
  creatorUsername: currentUsername,
793
938
  htmlPath: resolvedTarget.htmlPath,
794
- port: devServer_1.DEV_ROUTER_PORT,
939
+ port: devRouterPort,
795
940
  projectInfo,
941
+ runtimeAssetManifest,
796
942
  });
797
943
  }
798
944
  if (!serverAlreadyRunning && projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
@@ -805,114 +951,153 @@ async function captureListing(targetArg, options = {}) {
805
951
  await new Promise(resolve => setTimeout(resolve, 1000));
806
952
  }
807
953
  const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
808
- const routeSegment = resolveListingCaptureRouteSegment(Boolean(registeredApp?.previewable));
809
- const frameUrlObject = new URL(`${(0, appUrls_1.normalizePlaydropWebBase)(webBase)}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/${routeSegment}`);
810
- frameUrlObject.searchParams.set('launchCheck', '1');
811
- const frameUrl = frameUrlObject.toString();
812
- const captureSceneId = resolveListingCaptureSceneId(parsedOptions.width, parsedOptions.height);
954
+ const routeSegment = resolveListingCaptureRouteSegment(declaresPreviewSupport);
955
+ const localDevPort = resolveListingCaptureLocalDevPort(devRouterPort);
956
+ const frameUrl = buildListingCaptureFrameUrl({
957
+ webBase,
958
+ currentUsername,
959
+ appTypeSlug,
960
+ appName,
961
+ routeSegment,
962
+ localDevPort,
963
+ });
964
+ const frameUrlObject = new URL(frameUrl);
965
+ console.log(`[listing] Opening ${frameUrl}`);
813
966
  const shouldExportPreviewAudio = parsedOptions.audio && routeSegment === 'dev-preview';
814
- browserHandle = await launchListingBrowser({
815
- width: parsedOptions.width,
816
- height: parsedOptions.height,
817
- }, frameUrlObject.origin);
818
- await browserHandle.page.goto(frameUrl, { waitUntil: 'domcontentloaded' });
819
- await browserHandle.page.waitForTimeout(1000);
820
- if (routeSegment === 'dev-preview') {
821
- console.log(`[listing] Preparing preview scene ${captureSceneId}.`);
822
- await prepareHostedListingScene(browserHandle.page, captureSceneId);
823
- await browserHandle.page.waitForTimeout(1000);
824
- }
825
- if (shouldExportPreviewAudio) {
826
- console.log('[listing] Starting in-app preview audio capture.');
827
- await startHostedListingAudioCapture(browserHandle.page);
828
- }
829
- const measurement = await fitWindowToRequestedGameplay(browserHandle.page, parsedOptions.width, parsedOptions.height);
830
- console.log(`[listing] Gameplay frame ${Math.round(measurement.iframeRect.width)}x${Math.round(measurement.iframeRect.height)} in window ${Math.round(measurement.outerWidth)}x${Math.round(measurement.outerHeight)}.`);
831
- await browserHandle.page.waitForTimeout(750);
832
- const recorderMetadata = await runListingRecorder(recorderPath, browserHandle.processId, parsedOptions.durationSeconds, outputPaths.rawVideoPath, outputPaths.metadataPath, shouldExportPreviewAudio ? false : parsedOptions.audio);
833
- const exportedAudioPath = shouldExportPreviewAudio
834
- ? await stopHostedListingAudioCapture(browserHandle.page, outputPaths.outputDir)
835
- : null;
836
- const rawProbe = await probeMediaFile(outputPaths.rawVideoPath);
837
- const rawVideoStream = extractVideoStream(rawProbe);
838
- const rawDurationSeconds = readDurationSeconds(rawProbe);
839
- const rawAudioTrackCount = countAudioStreams(rawProbe);
840
- const crop = computeRecordedCrop(measurement, rawVideoStream.width, rawVideoStream.height);
841
- console.log(`[listing] Cropping raw capture to gameplay at ${crop.width}x${crop.height}+${crop.x}+${crop.y}.`);
842
- await encodeListingVideo(outputPaths.rawVideoPath, outputPaths.finalVideoPath, outputPaths.posterPath, crop, parsedOptions.width, parsedOptions.height, parsedOptions.fps, parsedOptions.posterAtSeconds);
843
- if (exportedAudioPath) {
844
- await muxListingAudio(outputPaths.finalVideoPath, exportedAudioPath);
845
- }
846
- const finalProbe = await probeMediaFile(outputPaths.finalVideoPath);
847
- const finalVideoStream = extractVideoStream(finalProbe);
848
- const finalDurationSeconds = readDurationSeconds(finalProbe);
849
- const finalAudioTrackCount = countAudioStreams(finalProbe);
850
- const finalFps = parseFrameRate(finalVideoStream.avg_frame_rate);
851
- ensureTolerance(finalVideoStream.width, parsedOptions.width, 0, 'final_width_mismatch');
852
- ensureTolerance(finalVideoStream.height, parsedOptions.height, 0, 'final_height_mismatch');
853
- ensureTolerance(finalDurationSeconds, parsedOptions.durationSeconds, 1.5, 'final_duration_mismatch');
854
- if (parsedOptions.audio && finalAudioTrackCount === 0) {
855
- throw new Error('audio_track_missing');
967
+ const surfaces = resolvedTarget.surfaceTargets.list;
968
+ const useSurfacePrefix = surfaces.length > 1;
969
+ const captures = [];
970
+ const artifactHashes = {};
971
+ const capturedSurfaces = [];
972
+ const allWarnings = [];
973
+ for (const surface of surfaces) {
974
+ const dimensions = resolveCaptureDimensions(surface, parsedOptions);
975
+ const captureSceneId = resolveListingCaptureSceneId(dimensions.width, dimensions.height);
976
+ const surfaceOutputPaths = buildSurfaceOutputPaths(outputPaths.outputDir, surface, useSurfacePrefix);
977
+ console.log(`[listing] Capturing ${surface} at ${dimensions.width}x${dimensions.height}.`);
978
+ const contextOptions = resolveListingCaptureBrowserContextOptions(surface, dimensions);
979
+ browserHandle = await launchListingBrowser(dimensions, frameUrlObject.origin, contextOptions);
980
+ try {
981
+ await browserHandle.page.goto(frameUrl, { waitUntil: 'domcontentloaded' });
982
+ await browserHandle.page.waitForTimeout(1000);
983
+ if (routeSegment === 'dev-preview') {
984
+ console.log(`[listing] Preparing preview scene ${captureSceneId}.`);
985
+ await prepareHostedListingScene(browserHandle.page, captureSceneId);
986
+ await browserHandle.page.waitForTimeout(1000);
987
+ }
988
+ if (shouldExportPreviewAudio) {
989
+ console.log('[listing] Starting in-app preview audio capture.');
990
+ await startHostedListingAudioCapture(browserHandle.page);
991
+ }
992
+ const measurement = await fitWindowToRequestedGameplay(browserHandle.page, dimensions.width, dimensions.height);
993
+ console.log(`[listing] Gameplay frame ${Math.round(measurement.iframeRect.width)}x${Math.round(measurement.iframeRect.height)} in window ${Math.round(measurement.outerWidth)}x${Math.round(measurement.outerHeight)}.`);
994
+ await browserHandle.page.waitForTimeout(750);
995
+ const recorderMetadata = await runListingRecorder(recorderPath, browserHandle.processId, parsedOptions.durationSeconds, surfaceOutputPaths.rawVideoPath, surfaceOutputPaths.metadataPath, shouldExportPreviewAudio ? false : parsedOptions.audio);
996
+ const exportedAudioPath = shouldExportPreviewAudio
997
+ ? await stopHostedListingAudioCapture(browserHandle.page, outputPaths.outputDir)
998
+ : null;
999
+ const rawProbe = await probeMediaFile(surfaceOutputPaths.rawVideoPath);
1000
+ const rawVideoStream = extractVideoStream(rawProbe);
1001
+ const rawDurationSeconds = readDurationSeconds(rawProbe);
1002
+ const rawAudioTrackCount = countAudioStreams(rawProbe);
1003
+ const crop = computeRecordedCrop(measurement, rawVideoStream.width, rawVideoStream.height);
1004
+ console.log(`[listing] Cropping raw capture to gameplay at ${crop.width}x${crop.height}+${crop.x}+${crop.y}.`);
1005
+ await encodeListingVideo(surfaceOutputPaths.rawVideoPath, surfaceOutputPaths.finalVideoPath, surfaceOutputPaths.posterPath, crop, dimensions.width, dimensions.height, parsedOptions.fps, parsedOptions.posterAtSeconds);
1006
+ if (exportedAudioPath) {
1007
+ await muxListingAudio(surfaceOutputPaths.finalVideoPath, exportedAudioPath);
1008
+ }
1009
+ const finalProbe = await probeMediaFile(surfaceOutputPaths.finalVideoPath);
1010
+ const finalVideoStream = extractVideoStream(finalProbe);
1011
+ const finalDurationSeconds = readDurationSeconds(finalProbe);
1012
+ const finalAudioTrackCount = countAudioStreams(finalProbe);
1013
+ const finalFps = parseFrameRate(finalVideoStream.avg_frame_rate);
1014
+ ensureTolerance(finalVideoStream.width, dimensions.width, 0, 'final_width_mismatch');
1015
+ ensureTolerance(finalVideoStream.height, dimensions.height, 0, 'final_height_mismatch');
1016
+ ensureTolerance(finalDurationSeconds, parsedOptions.durationSeconds, 1.5, 'final_duration_mismatch');
1017
+ if (parsedOptions.audio && finalAudioTrackCount === 0) {
1018
+ throw new Error('audio_track_missing');
1019
+ }
1020
+ const warnings = createWarnings(finalProbe, parsedOptions.audio);
1021
+ for (const warning of warnings) {
1022
+ allWarnings.push(`${surface}: ${warning}`);
1023
+ }
1024
+ artifactHashes[(0, node_path_1.relative)(outputPaths.outputDir, surfaceOutputPaths.finalVideoPath) || 'listing.mp4'] = await computeFileSha256(surfaceOutputPaths.finalVideoPath);
1025
+ artifactHashes[(0, node_path_1.relative)(outputPaths.outputDir, surfaceOutputPaths.posterPath) || 'poster.png'] = await computeFileSha256(surfaceOutputPaths.posterPath);
1026
+ capturedSurfaces.push(surface);
1027
+ captures.push({
1028
+ surface,
1029
+ targetUrl: frameUrl,
1030
+ command: {
1031
+ durationSeconds: parsedOptions.durationSeconds,
1032
+ width: dimensions.width,
1033
+ height: dimensions.height,
1034
+ fps: parsedOptions.fps,
1035
+ posterAtSeconds: parsedOptions.posterAtSeconds,
1036
+ audio: parsedOptions.audio,
1037
+ keepRaw: parsedOptions.keepRaw,
1038
+ },
1039
+ measurement,
1040
+ recorder: recorderMetadata,
1041
+ crop,
1042
+ rawVideo: {
1043
+ path: parsedOptions.keepRaw ? surfaceOutputPaths.rawVideoPath : null,
1044
+ width: rawVideoStream.width,
1045
+ height: rawVideoStream.height,
1046
+ durationSeconds: rawDurationSeconds,
1047
+ audioTrackCount: rawAudioTrackCount,
1048
+ },
1049
+ finalVideo: {
1050
+ path: surfaceOutputPaths.finalVideoPath,
1051
+ width: finalVideoStream.width,
1052
+ height: finalVideoStream.height,
1053
+ durationSeconds: finalDurationSeconds,
1054
+ audioTrackCount: finalAudioTrackCount,
1055
+ fps: finalFps,
1056
+ },
1057
+ posterPath: surfaceOutputPaths.posterPath,
1058
+ warnings,
1059
+ });
1060
+ if (!parsedOptions.keepRaw) {
1061
+ await (0, promises_1.rm)(surfaceOutputPaths.rawVideoPath, { force: true });
1062
+ }
1063
+ console.log(`[listing] Saved ${surface} video to ${(0, node_path_1.relative)(process.cwd(), surfaceOutputPaths.finalVideoPath) || surfaceOutputPaths.finalVideoPath}`);
1064
+ console.log(`[listing] Saved ${surface} poster to ${(0, node_path_1.relative)(process.cwd(), surfaceOutputPaths.posterPath) || surfaceOutputPaths.posterPath}`);
1065
+ if (warnings.length > 0) {
1066
+ for (const warning of warnings) {
1067
+ console.warn(`[listing][warn][${surface}] ${warning}`);
1068
+ }
1069
+ }
1070
+ }
1071
+ finally {
1072
+ await browserHandle.close();
1073
+ browserHandle = null;
1074
+ }
856
1075
  }
857
- const warnings = createWarnings(finalProbe, parsedOptions.audio);
858
1076
  const report = {
859
1077
  appName,
860
- targetUrl: frameUrl,
861
1078
  outputDir: outputPaths.outputDir,
862
- command: {
863
- durationSeconds: parsedOptions.durationSeconds,
864
- width: parsedOptions.width,
865
- height: parsedOptions.height,
866
- fps: parsedOptions.fps,
867
- posterAtSeconds: parsedOptions.posterAtSeconds,
868
- audio: parsedOptions.audio,
869
- keepRaw: parsedOptions.keepRaw,
870
- },
871
- measurement,
872
- recorder: recorderMetadata,
873
- crop,
874
- rawVideo: {
875
- path: parsedOptions.keepRaw ? outputPaths.rawVideoPath : null,
876
- width: rawVideoStream.width,
877
- height: rawVideoStream.height,
878
- durationSeconds: rawDurationSeconds,
879
- audioTrackCount: rawAudioTrackCount,
880
- },
881
- finalVideo: {
882
- path: outputPaths.finalVideoPath,
883
- width: finalVideoStream.width,
884
- height: finalVideoStream.height,
885
- durationSeconds: finalDurationSeconds,
886
- audioTrackCount: finalAudioTrackCount,
887
- fps: finalFps,
1079
+ captures,
1080
+ warnings: allWarnings,
1081
+ binding: {
1082
+ contentHash: parsedOptions.contentHash,
1083
+ artifactHashes,
1084
+ capturedSurfaces,
888
1085
  },
889
- posterPath: outputPaths.posterPath,
890
- warnings,
891
1086
  };
892
1087
  await (0, promises_1.writeFile)(outputPaths.reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
893
- if (!parsedOptions.keepRaw) {
894
- await (0, promises_1.rm)(outputPaths.rawVideoPath, { force: true });
895
- }
896
- console.log(`[listing] Saved video to ${(0, node_path_1.relative)(process.cwd(), outputPaths.finalVideoPath) || outputPaths.finalVideoPath}`);
897
- console.log(`[listing] Saved poster to ${(0, node_path_1.relative)(process.cwd(), outputPaths.posterPath) || outputPaths.posterPath}`);
898
1088
  console.log(`[listing] Saved report to ${(0, node_path_1.relative)(process.cwd(), outputPaths.reportPath) || outputPaths.reportPath}`);
899
- if (warnings.length > 0) {
900
- for (const warning of warnings) {
901
- console.warn(`[listing][warn] ${warning}`);
902
- }
903
- }
904
1089
  }
905
1090
  catch (error) {
906
1091
  if (error instanceof http_1.CLIUnsupportedClientError) {
907
1092
  return;
908
1093
  }
909
1094
  if (error instanceof types_1.UnsupportedClientError) {
910
- (0, http_1.handleUnsupportedError)(error, 'Listing capture');
1095
+ (0, http_1.handleUnsupportedError)(error, 'Game media capture');
911
1096
  process.exitCode = 1;
912
1097
  return;
913
1098
  }
914
1099
  const detail = formatCommandError(error instanceof Error ? error : new Error(String(error)));
915
- (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture listing' });
1100
+ (0, messages_1.printErrorWithHelp)(detail.message, detail.suggestions, { command: 'project capture' });
916
1101
  process.exitCode = 1;
917
1102
  }
918
1103
  finally {