@playdrop/playdrop-cli 0.9.5 → 0.10.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 (69) hide show
  1. package/config/client-meta.json +1 -2
  2. package/dist/apiClient.d.ts +2 -0
  3. package/dist/apiClient.js +27 -2
  4. package/dist/appUrls.d.ts +1 -0
  5. package/dist/appUrls.js +9 -0
  6. package/dist/apps/build.js +39 -28
  7. package/dist/apps/index.d.ts +1 -0
  8. package/dist/apps/index.js +2 -0
  9. package/dist/apps/launchCheck.d.ts +2 -0
  10. package/dist/apps/launchCheck.js +31 -6
  11. package/dist/apps/registration.d.ts +1 -0
  12. package/dist/apps/registration.js +1 -0
  13. package/dist/apps/upload.d.ts +1 -0
  14. package/dist/apps/upload.js +4 -17
  15. package/dist/captureRuntime.d.ts +1 -0
  16. package/dist/captureRuntime.js +308 -0
  17. package/dist/catalogue.d.ts +4 -2
  18. package/dist/catalogue.js +50 -7
  19. package/dist/commandContext.js +42 -3
  20. package/dist/commands/capture.d.ts +1 -0
  21. package/dist/commands/capture.js +30 -13
  22. package/dist/commands/create.d.ts +0 -1
  23. package/dist/commands/create.js +2 -151
  24. package/dist/commands/creations.d.ts +0 -13
  25. package/dist/commands/creations.js +0 -141
  26. package/dist/commands/dev.d.ts +2 -1
  27. package/dist/commands/dev.js +23 -6
  28. package/dist/commands/devServer.js +3 -1
  29. package/dist/commands/generation.d.ts +1 -0
  30. package/dist/commands/generation.js +274 -0
  31. package/dist/commands/upload.d.ts +27 -1
  32. package/dist/commands/upload.js +962 -21
  33. package/dist/commands/validate.js +5 -0
  34. package/dist/commands/worker/runtime.d.ts +69 -0
  35. package/dist/commands/worker/runtime.js +414 -0
  36. package/dist/commands/worker.d.ts +144 -0
  37. package/dist/commands/worker.js +2219 -0
  38. package/dist/config.d.ts +2 -0
  39. package/dist/config.js +23 -0
  40. package/dist/index.js +71 -30
  41. package/dist/shellProbe.d.ts +1 -1
  42. package/dist/shellProbe.js +3 -3
  43. package/dist/workspaceAuth.d.ts +1 -0
  44. package/dist/workspaceAuth.js +8 -0
  45. package/node_modules/@playdrop/api-client/dist/client.d.ts +31 -14
  46. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  47. package/node_modules/@playdrop/api-client/dist/client.js +2 -2
  48. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +3 -1
  49. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  50. package/node_modules/@playdrop/api-client/dist/domains/admin.js +45 -0
  51. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +72 -0
  52. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -0
  53. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +442 -0
  54. package/node_modules/@playdrop/api-client/dist/index.d.ts +31 -14
  55. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  56. package/node_modules/@playdrop/api-client/dist/index.js +134 -38
  57. package/node_modules/@playdrop/config/client-meta.json +1 -2
  58. package/node_modules/@playdrop/types/dist/api.d.ts +501 -74
  59. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  60. package/node_modules/@playdrop/types/dist/api.js +90 -9
  61. package/node_modules/@playdrop/types/dist/app.d.ts +2 -0
  62. package/node_modules/@playdrop/types/dist/app.d.ts.map +1 -1
  63. package/node_modules/@playdrop/types/dist/app.js +3 -0
  64. package/node_modules/@playdrop/types/dist/version.d.ts +1 -0
  65. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  66. package/package.json +2 -1
  67. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts +0 -46
  68. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts.map +0 -1
  69. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.js +0 -177
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.MAX_CAPTURE_TIMEOUT_SECONDS = exports.CAPTURE_LOG_LEVEL_VALUES = void 0;
4
7
  exports.resolveCaptureLogLevel = resolveCaptureLogLevel;
@@ -6,6 +9,7 @@ exports.validateCaptureTimeout = validateCaptureTimeout;
6
9
  exports.runCapture = runCapture;
7
10
  const promises_1 = require("node:fs/promises");
8
11
  const node_path_1 = require("node:path");
12
+ const sharp_1 = __importDefault(require("sharp"));
9
13
  const playwright_1 = require("./playwright");
10
14
  const sessionCookie_1 = require("./sessionCookie");
11
15
  const FRAME_SELECTOR = 'iframe[title="Game"]';
@@ -18,6 +22,11 @@ const GOOGLE_TELEMETRY_HOSTS = [
18
22
  'doubleclick.net',
19
23
  ];
20
24
  const GOOGLE_COUNTRY_SECOND_LEVEL_DOMAINS = new Set(['ac', 'co', 'com', 'edu', 'gov', 'net', 'org']);
25
+ const CANVAS_VISUAL_SAMPLE_SIZE = 64;
26
+ const MIN_VISIBLE_CANVAS_EDGE_PX = 48;
27
+ const MIN_CANVAS_FOREGROUND_RATIO = 0.018;
28
+ const MIN_CANVAS_COLOR_RANGE = 34;
29
+ const CANVAS_BACKGROUND_DISTANCE_THRESHOLD = 28;
21
30
  function isHostOrSubdomain(hostname, domain) {
22
31
  return hostname === domain || hostname.endsWith(`.${domain}`);
23
32
  }
@@ -232,6 +241,298 @@ function shouldSettleHostedLaunchWaiter(state, expectedState) {
232
241
  }
233
242
  return state.state === expectedState;
234
243
  }
244
+ function shouldAssertHostedVisualReadiness(input) {
245
+ return Boolean(input.requireHostedLaunchReady) && (input.expectedHostedLaunchState ?? 'ready') === 'ready';
246
+ }
247
+ function summarizeRgbaPixels(pixels, pixelCount, channels) {
248
+ let bgR = 0;
249
+ let bgG = 0;
250
+ let bgB = 0;
251
+ const edge = Math.max(1, Math.round(Math.sqrt(pixelCount)));
252
+ const cornerIndexes = [
253
+ 0,
254
+ edge - 1,
255
+ edge * (edge - 1),
256
+ (edge * edge) - 1,
257
+ ].filter(index => index >= 0 && index < pixelCount);
258
+ for (const index of cornerIndexes) {
259
+ const offset = index * channels;
260
+ bgR += pixels[offset] ?? 0;
261
+ bgG += pixels[offset + 1] ?? 0;
262
+ bgB += pixels[offset + 2] ?? 0;
263
+ }
264
+ bgR /= cornerIndexes.length || 1;
265
+ bgG /= cornerIndexes.length || 1;
266
+ bgB /= cornerIndexes.length || 1;
267
+ let minLuma = 255;
268
+ let maxLuma = 0;
269
+ let foreground = 0;
270
+ let opaque = 0;
271
+ for (let index = 0; index < pixelCount; index += 1) {
272
+ const offset = index * channels;
273
+ const r = pixels[offset] ?? 0;
274
+ const g = pixels[offset + 1] ?? 0;
275
+ const b = pixels[offset + 2] ?? 0;
276
+ const alpha = channels >= 4 ? pixels[offset + 3] ?? 255 : 255;
277
+ if (alpha > 8) {
278
+ opaque += 1;
279
+ }
280
+ const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
281
+ minLuma = Math.min(minLuma, luma);
282
+ maxLuma = Math.max(maxLuma, luma);
283
+ const distance = Math.hypot(r - bgR, g - bgG, b - bgB);
284
+ if (distance >= CANVAS_BACKGROUND_DISTANCE_THRESHOLD && alpha > 8) {
285
+ foreground += 1;
286
+ }
287
+ }
288
+ const foregroundRatio = foreground / pixelCount;
289
+ const opaqueRatio = opaque / pixelCount;
290
+ const colorRange = maxLuma - minLuma;
291
+ return {
292
+ foregroundRatio,
293
+ colorRange,
294
+ opaqueRatio,
295
+ pass: opaqueRatio > 0.05
296
+ && (foregroundRatio >= MIN_CANVAS_FOREGROUND_RATIO || colorRange >= MIN_CANVAS_COLOR_RANGE),
297
+ };
298
+ }
299
+ async function summarizeVisibleCanvases(frame) {
300
+ return await frame.evaluate((config) => {
301
+ const canvases = Array.from(document.querySelectorAll('canvas'));
302
+ const visibleCanvases = canvases.filter((canvas) => {
303
+ const rect = canvas.getBoundingClientRect();
304
+ const style = window.getComputedStyle(canvas);
305
+ const opacity = Number.parseFloat(style.opacity || '1');
306
+ return rect.width >= config.minEdge
307
+ && rect.height >= config.minEdge
308
+ && style.display !== 'none'
309
+ && style.visibility !== 'hidden'
310
+ && opacity > 0.05;
311
+ });
312
+ const summaries = visibleCanvases.map((canvas) => {
313
+ const rect = canvas.getBoundingClientRect();
314
+ const sample = document.createElement('canvas');
315
+ sample.width = config.sampleSize;
316
+ sample.height = config.sampleSize;
317
+ const context = sample.getContext('2d', { willReadFrequently: true });
318
+ if (!context) {
319
+ return {
320
+ x: Math.round(rect.left),
321
+ y: Math.round(rect.top),
322
+ width: Math.round(rect.width),
323
+ height: Math.round(rect.height),
324
+ foregroundRatio: 0,
325
+ colorRange: 0,
326
+ opaqueRatio: 0,
327
+ readable: false,
328
+ pass: false,
329
+ error: 'canvas_2d_context_unavailable',
330
+ };
331
+ }
332
+ try {
333
+ context.drawImage(canvas, 0, 0, config.sampleSize, config.sampleSize);
334
+ const pixels = context.getImageData(0, 0, config.sampleSize, config.sampleSize).data;
335
+ const cornerIndexes = [
336
+ 0,
337
+ config.sampleSize - 1,
338
+ config.sampleSize * (config.sampleSize - 1),
339
+ (config.sampleSize * config.sampleSize) - 1,
340
+ ];
341
+ let bgR = 0;
342
+ let bgG = 0;
343
+ let bgB = 0;
344
+ for (const index of cornerIndexes) {
345
+ const offset = index * 4;
346
+ bgR += pixels[offset] ?? 0;
347
+ bgG += pixels[offset + 1] ?? 0;
348
+ bgB += pixels[offset + 2] ?? 0;
349
+ }
350
+ bgR /= cornerIndexes.length;
351
+ bgG /= cornerIndexes.length;
352
+ bgB /= cornerIndexes.length;
353
+ let minLuma = 255;
354
+ let maxLuma = 0;
355
+ let foreground = 0;
356
+ let opaque = 0;
357
+ const total = config.sampleSize * config.sampleSize;
358
+ for (let offset = 0; offset < pixels.length; offset += 4) {
359
+ const r = pixels[offset] ?? 0;
360
+ const g = pixels[offset + 1] ?? 0;
361
+ const b = pixels[offset + 2] ?? 0;
362
+ const alpha = pixels[offset + 3] ?? 0;
363
+ if (alpha > 8) {
364
+ opaque += 1;
365
+ }
366
+ const luma = (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
367
+ minLuma = Math.min(minLuma, luma);
368
+ maxLuma = Math.max(maxLuma, luma);
369
+ const distance = Math.hypot(r - bgR, g - bgG, b - bgB);
370
+ if (distance >= config.backgroundDistanceThreshold && alpha > 8) {
371
+ foreground += 1;
372
+ }
373
+ }
374
+ const foregroundRatio = foreground / total;
375
+ const opaqueRatio = opaque / total;
376
+ const colorRange = maxLuma - minLuma;
377
+ return {
378
+ x: Math.round(rect.left),
379
+ y: Math.round(rect.top),
380
+ width: Math.round(rect.width),
381
+ height: Math.round(rect.height),
382
+ foregroundRatio,
383
+ colorRange,
384
+ opaqueRatio,
385
+ readable: true,
386
+ pass: opaqueRatio > 0.05
387
+ && (foregroundRatio >= config.minForegroundRatio || colorRange >= config.minColorRange),
388
+ };
389
+ }
390
+ catch (error) {
391
+ return {
392
+ x: Math.round(rect.left),
393
+ y: Math.round(rect.top),
394
+ width: Math.round(rect.width),
395
+ height: Math.round(rect.height),
396
+ foregroundRatio: 0,
397
+ colorRange: 0,
398
+ opaqueRatio: 0,
399
+ readable: false,
400
+ pass: false,
401
+ error: error instanceof Error ? error.message : String(error),
402
+ };
403
+ }
404
+ });
405
+ return {
406
+ canvasCount: canvases.length,
407
+ visibleCanvasCount: visibleCanvases.length,
408
+ readableCanvasCount: summaries.filter((summary) => summary.readable).length,
409
+ passingCanvasCount: summaries.filter((summary) => summary.pass).length,
410
+ canvases: summaries,
411
+ };
412
+ }, {
413
+ sampleSize: CANVAS_VISUAL_SAMPLE_SIZE,
414
+ minEdge: MIN_VISIBLE_CANVAS_EDGE_PX,
415
+ minForegroundRatio: MIN_CANVAS_FOREGROUND_RATIO,
416
+ minColorRange: MIN_CANVAS_COLOR_RANGE,
417
+ backgroundDistanceThreshold: CANVAS_BACKGROUND_DISTANCE_THRESHOLD,
418
+ });
419
+ }
420
+ function formatCanvasVisualSummary(summary) {
421
+ const readable = summary.canvases.filter(canvas => canvas.readable);
422
+ const first = readable[0];
423
+ if (!first) {
424
+ return `${summary.visibleCanvasCount} visible canvas(es), none readable`;
425
+ }
426
+ return `${summary.visibleCanvasCount} visible canvas(es), foregroundRatio=${first.foregroundRatio.toFixed(3)}, colorRange=${first.colorRange.toFixed(1)}, opaqueRatio=${first.opaqueRatio.toFixed(3)}`;
427
+ }
428
+ async function getFramePageOffset(frame) {
429
+ try {
430
+ const element = await frame.frameElement();
431
+ try {
432
+ const box = await element.boundingBox();
433
+ return box ? { x: box.x, y: box.y } : null;
434
+ }
435
+ finally {
436
+ await element.dispose().catch(() => { });
437
+ }
438
+ }
439
+ catch {
440
+ return null;
441
+ }
442
+ }
443
+ function buildCanvasScreenshotClip(frameOffset, canvas, viewport) {
444
+ if (!Number.isFinite(canvas.x) || !Number.isFinite(canvas.y)) {
445
+ return null;
446
+ }
447
+ const x = Math.max(0, frameOffset.x + Number(canvas.x));
448
+ const y = Math.max(0, frameOffset.y + Number(canvas.y));
449
+ const viewportWidth = viewport?.width ?? Number.POSITIVE_INFINITY;
450
+ const viewportHeight = viewport?.height ?? Number.POSITIVE_INFINITY;
451
+ const width = Math.min(canvas.width, viewportWidth - x);
452
+ const height = Math.min(canvas.height, viewportHeight - y);
453
+ if (width < MIN_VISIBLE_CANVAS_EDGE_PX || height < MIN_VISIBLE_CANVAS_EDGE_PX) {
454
+ return null;
455
+ }
456
+ return { x, y, width, height };
457
+ }
458
+ async function summarizeScreenshotClip(page, clip) {
459
+ try {
460
+ const png = await page.screenshot({ type: 'png', clip });
461
+ const { data, info } = await (0, sharp_1.default)(png)
462
+ .ensureAlpha()
463
+ .resize(CANVAS_VISUAL_SAMPLE_SIZE, CANVAS_VISUAL_SAMPLE_SIZE, { fit: 'fill' })
464
+ .raw()
465
+ .toBuffer({ resolveWithObject: true });
466
+ const pixelSummary = summarizeRgbaPixels(data, info.width * info.height, info.channels);
467
+ return {
468
+ x: Math.round(clip.x),
469
+ y: Math.round(clip.y),
470
+ width: Math.round(clip.width),
471
+ height: Math.round(clip.height),
472
+ foregroundRatio: pixelSummary.foregroundRatio,
473
+ colorRange: pixelSummary.colorRange,
474
+ opaqueRatio: pixelSummary.opaqueRatio,
475
+ readable: true,
476
+ pass: pixelSummary.pass,
477
+ };
478
+ }
479
+ catch {
480
+ return null;
481
+ }
482
+ }
483
+ async function summarizeCanvasScreenshots(page, frames, summaries) {
484
+ const viewport = typeof page.viewportSize === 'function' ? page.viewportSize() : null;
485
+ const screenshotSummaries = [];
486
+ for (let index = 0; index < frames.length; index += 1) {
487
+ const frame = frames[index];
488
+ const summary = summaries[index];
489
+ if (!frame || !summary || summary.visibleCanvasCount === 0 || summary.passingCanvasCount > 0) {
490
+ continue;
491
+ }
492
+ const frameOffset = await getFramePageOffset(frame);
493
+ if (!frameOffset) {
494
+ continue;
495
+ }
496
+ for (const canvas of summary.canvases) {
497
+ const clip = buildCanvasScreenshotClip(frameOffset, canvas, viewport);
498
+ if (!clip) {
499
+ continue;
500
+ }
501
+ const screenshotSummary = await summarizeScreenshotClip(page, clip);
502
+ if (screenshotSummary) {
503
+ screenshotSummaries.push(screenshotSummary);
504
+ }
505
+ }
506
+ }
507
+ return screenshotSummaries;
508
+ }
509
+ async function assertHostedFrameVisualReadiness(page) {
510
+ const childFrames = page.frames().filter(frame => frame.parentFrame() !== null);
511
+ if (childFrames.length === 0) {
512
+ throw new Error('hosted_app_visual_blank: The hosted app reported ready but no game frame was available for visual validation.');
513
+ }
514
+ const summaries = await Promise.all(childFrames.map(frame => summarizeVisibleCanvases(frame)));
515
+ const framesWithCanvases = summaries.filter(summary => summary.visibleCanvasCount > 0);
516
+ if (framesWithCanvases.length === 0) {
517
+ return;
518
+ }
519
+ if (framesWithCanvases.some(summary => summary.passingCanvasCount > 0)) {
520
+ return;
521
+ }
522
+ const screenshotSummaries = await summarizeCanvasScreenshots(page, childFrames, summaries);
523
+ if (screenshotSummaries.some(summary => summary.pass)) {
524
+ return;
525
+ }
526
+ const readableFrames = framesWithCanvases.filter(summary => summary.readableCanvasCount > 0);
527
+ if (readableFrames.length === 0) {
528
+ return;
529
+ }
530
+ const screenshotDetail = screenshotSummaries.length > 0
531
+ ? `; screenshot foregroundRatio=${screenshotSummaries[0].foregroundRatio.toFixed(3)}, colorRange=${screenshotSummaries[0].colorRange.toFixed(1)}, opaqueRatio=${screenshotSummaries[0].opaqueRatio.toFixed(3)}`
532
+ : '';
533
+ const detail = `${readableFrames.map(formatCanvasVisualSummary).join('; ')}${screenshotDetail}`;
534
+ throw new Error(`hosted_app_visual_blank: The hosted app reported ready, but its visible canvas looked empty or background-only after startup. ${detail}. Make the playable objects visible in the initial ready state.`);
535
+ }
235
536
  async function writeLogFile(logPath, lines) {
236
537
  if (!logPath || logPath.trim().length === 0) {
237
538
  return;
@@ -623,6 +924,12 @@ async function runCapture(options) {
623
924
  finalUrl = await assertPageState();
624
925
  await page.waitForTimeout(options.settleAfterReadyMs ?? options.timeoutMs);
625
926
  finalUrl = await assertPageState();
927
+ if (shouldAssertHostedVisualReadiness({
928
+ requireHostedLaunchReady: options.requireHostedLaunchReady,
929
+ expectedHostedLaunchState,
930
+ })) {
931
+ await assertHostedFrameVisualReadiness(page);
932
+ }
626
933
  if (options.screenshotPath) {
627
934
  const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
628
935
  if (targetDir && targetDir !== '.' && targetDir !== '/') {
@@ -641,6 +948,7 @@ async function runCapture(options) {
641
948
  }
642
949
  return {
643
950
  errorCount: errors.length,
951
+ errors: errors.slice(0, 10),
644
952
  warningCount: warnings.length,
645
953
  warnings,
646
954
  finalUrl,
@@ -57,8 +57,8 @@ export type AppCatalogueEntry = {
57
57
  ownedAssets?: OwnedAssetCatalogueEntry[];
58
58
  assetSpecSupport?: AppMetadataAssetSpecSupport[];
59
59
  uses?: {
60
- assets?: string[];
61
- packs?: string[];
60
+ assets?: unknown[];
61
+ packs?: unknown[];
62
62
  };
63
63
  tags?: string[];
64
64
  relations?: Array<{
@@ -183,6 +183,8 @@ export type AppTask = {
183
183
  version?: string;
184
184
  releaseNotes?: string;
185
185
  versionVisibility?: AppVersionVisibility;
186
+ sourceArchiveExcludeRelativeFiles?: string[];
187
+ bundleArchiveExcludeRelativeFiles?: string[];
186
188
  license: ContentLicense;
187
189
  tags: string[];
188
190
  hostingMode?: AppHostingMode;
package/dist/catalogue.js CHANGED
@@ -270,6 +270,37 @@ function normalizeCatalogueAppDependencyAssets(value, errors, context) {
270
270
  }
271
271
  return rows;
272
272
  }
273
+ function normalizeCatalogueAppDependencyPacks(value, errors, context) {
274
+ if (value === undefined || value === null) {
275
+ return [];
276
+ }
277
+ if (!Array.isArray(value)) {
278
+ errors.push(`${context} uses.packs must be an array of canonical pack version ref strings.`);
279
+ return [];
280
+ }
281
+ const rows = [];
282
+ const seenRefs = new Set();
283
+ for (let index = 0; index < value.length; index += 1) {
284
+ const entry = value[index];
285
+ if (typeof entry !== 'string' || entry.trim().length === 0) {
286
+ errors.push(`${context} uses.packs[${index}] must be a non-empty pack version ref string. Use "pack:playdrop/name@version", not { ref, runtimeKey }.`);
287
+ continue;
288
+ }
289
+ const parsed = (0, types_1.parseContentVersionRef)(entry.trim());
290
+ if (!parsed || parsed.kind !== 'pack') {
291
+ errors.push(`${context} uses.packs[${index}] must be a canonical pack version ref.`);
292
+ continue;
293
+ }
294
+ const normalizedRef = (0, types_1.formatContentVersionRef)(parsed);
295
+ if (seenRefs.has(normalizedRef)) {
296
+ errors.push(`${context} uses.packs contains duplicate ref "${normalizedRef}".`);
297
+ continue;
298
+ }
299
+ seenRefs.add(normalizedRef);
300
+ rows.push(normalizedRef);
301
+ }
302
+ return rows;
303
+ }
273
304
  function rejectLegacyShopPriceCoinsField(value, errors, context) {
274
305
  if (Object.prototype.hasOwnProperty.call(value, 'shopPriceCoins')) {
275
306
  errors.push(`${context} uses legacy shopPriceCoins. Rename it to shopPriceCredits.`);
@@ -1130,8 +1161,13 @@ function buildAppTasks(rootDir, catalogues, options) {
1130
1161
  }
1131
1162
  }
1132
1163
  // Hero images
1133
- if (!listingValidationFailed && typeof rawListing.heroPortrait === 'string' && rawListing.heroPortrait.trim()) {
1134
- const heroPortraitPath = rawListing.heroPortrait.trim();
1164
+ const rawHeroPortrait = typeof rawListing.heroPortrait === 'string' && rawListing.heroPortrait.trim()
1165
+ ? rawListing.heroPortrait.trim()
1166
+ : typeof rawListing.heroPortraitPath === 'string' && rawListing.heroPortraitPath.trim()
1167
+ ? rawListing.heroPortraitPath.trim()
1168
+ : '';
1169
+ if (!listingValidationFailed && rawHeroPortrait) {
1170
+ const heroPortraitPath = rawHeroPortrait;
1135
1171
  const extensionError = validateListingAssetExtension(heroPortraitPath, 'listing.heroPortrait', 'image');
1136
1172
  if (extensionError) {
1137
1173
  errors.push(`[${label}] Skipping ${rawName}: ${extensionError}`);
@@ -1148,8 +1184,13 @@ function buildAppTasks(rootDir, catalogues, options) {
1148
1184
  }
1149
1185
  }
1150
1186
  }
1151
- if (!listingValidationFailed && typeof rawListing.heroLandscape === 'string' && rawListing.heroLandscape.trim()) {
1152
- const heroLandscapePath = rawListing.heroLandscape.trim();
1187
+ const rawHeroLandscape = typeof rawListing.heroLandscape === 'string' && rawListing.heroLandscape.trim()
1188
+ ? rawListing.heroLandscape.trim()
1189
+ : typeof rawListing.heroLandscapePath === 'string' && rawListing.heroLandscapePath.trim()
1190
+ ? rawListing.heroLandscapePath.trim()
1191
+ : '';
1192
+ if (!listingValidationFailed && rawHeroLandscape) {
1193
+ const heroLandscapePath = rawHeroLandscape;
1153
1194
  const extensionError = validateListingAssetExtension(heroLandscapePath, 'listing.heroLandscape', 'image');
1154
1195
  if (extensionError) {
1155
1196
  errors.push(`[${label}] Skipping ${rawName}: ${extensionError}`);
@@ -1253,10 +1294,12 @@ function buildAppTasks(rootDir, catalogues, options) {
1253
1294
  }
1254
1295
  listing = resolvedListing;
1255
1296
  }
1297
+ const errorCountBeforeDependencies = errors.length;
1256
1298
  const usesAssets = normalizeCatalogueAppDependencyAssets(entry.uses?.assets, errors, `[${label}] App "${rawName}"`);
1257
- const usesPacks = Array.isArray(entry.uses?.packs)
1258
- ? entry.uses.packs.filter((value) => typeof value === 'string' && value.trim().length > 0)
1259
- : [];
1299
+ const usesPacks = normalizeCatalogueAppDependencyPacks(entry.uses?.packs, errors, `[${label}] App "${rawName}"`);
1300
+ if (errors.length > errorCountBeforeDependencies) {
1301
+ continue;
1302
+ }
1260
1303
  const errorCountBeforeTagMetadata = errors.length;
1261
1304
  const remix = normalizeCatalogueRemixRef(entry.remix, 'app', errors, `[${label}] App "${rawName}"`);
1262
1305
  const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] App "${rawName}"`);
@@ -38,8 +38,10 @@ function resolveConfiguredEnvironment(envName, command, options) {
38
38
  }
39
39
  function buildContext(cfg, envConfig, account, workspaceAuth = null) {
40
40
  const token = account?.token ?? cfg.token ?? '';
41
- const client = (0, apiClient_1.createCliApiClient)({ baseUrl: envConfig.apiBase, token });
42
- const aiClient = (0, apiClient_1.createCliAiClient)({ baseUrl: envConfig.aiBase, token });
41
+ const onBehalfCreatorUsername = workspaceAuth?.config.ownerUsername ?? null;
42
+ const agentTaskToken = workspaceAuth?.config.taskToken ?? null;
43
+ const client = (0, apiClient_1.createCliApiClient)({ baseUrl: envConfig.apiBase, token, onBehalfCreatorUsername, agentTaskToken });
44
+ const aiClient = (0, apiClient_1.createCliAiClient)({ baseUrl: envConfig.aiBase, token, onBehalfCreatorUsername, agentTaskToken });
43
45
  return {
44
46
  client,
45
47
  aiClient,
@@ -124,8 +126,30 @@ function resolveWorkspaceSelectedAccount(cfg, currentAccount, workspaceAuth, req
124
126
  const matchingSessions = (0, config_1.listAccountSessionsForUsername)(workspaceAuth.config.ownerUsername, cfg);
125
127
  const preferredEnv = normalizeRequestedEnv(requestedEnv) ?? workspaceAuth.config.env;
126
128
  if (preferredEnv) {
129
+ const ownerAccount = (0, config_1.findAccountSession)(workspaceAuth.config.ownerUsername, preferredEnv, cfg);
130
+ if (ownerAccount) {
131
+ return {
132
+ account: ownerAccount,
133
+ matchingSessions,
134
+ };
135
+ }
136
+ if (workspaceAuth.config.taskToken && currentAccount) {
137
+ if (currentAccount.env === preferredEnv) {
138
+ return {
139
+ account: currentAccount,
140
+ matchingSessions,
141
+ };
142
+ }
143
+ const currentAccountForEnv = (0, config_1.findAccountSession)(currentAccount.username, preferredEnv, cfg);
144
+ if (currentAccountForEnv) {
145
+ return {
146
+ account: currentAccountForEnv,
147
+ matchingSessions,
148
+ };
149
+ }
150
+ }
127
151
  return {
128
- account: (0, config_1.findAccountSession)(workspaceAuth.config.ownerUsername, preferredEnv, cfg),
152
+ account: null,
129
153
  matchingSessions,
130
154
  };
131
155
  }
@@ -141,6 +165,21 @@ function resolveWorkspaceSelectedAccount(cfg, currentAccount, workspaceAuth, req
141
165
  matchingSessions,
142
166
  };
143
167
  }
168
+ if (currentAccount) {
169
+ if (!preferredEnv || currentAccount.env === preferredEnv) {
170
+ return {
171
+ account: currentAccount,
172
+ matchingSessions,
173
+ };
174
+ }
175
+ const currentAccountForEnv = (0, config_1.findAccountSession)(currentAccount.username, preferredEnv, cfg);
176
+ if (currentAccountForEnv) {
177
+ return {
178
+ account: currentAccountForEnv,
179
+ matchingSessions,
180
+ };
181
+ }
182
+ }
144
183
  return {
145
184
  account: null,
146
185
  matchingSessions,
@@ -3,6 +3,7 @@ type CaptureOptions = {
3
3
  appName?: string;
4
4
  logLevel?: string;
5
5
  screenshotPath?: string;
6
+ port?: string | number;
6
7
  surfaceTarget?: string;
7
8
  devAuth?: string;
8
9
  player?: string | number;
@@ -16,6 +16,7 @@ const captureRuntime_1 = require("../captureRuntime");
16
16
  const devAuth_1 = require("../devAuth");
17
17
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
18
18
  const dev_1 = require("./dev");
19
+ const dev_2 = require("./dev");
19
20
  const 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';
20
21
  const SURFACE_PRESETS = {
21
22
  desktop: {
@@ -120,6 +121,7 @@ function resolveCaptureDevOptions(options) {
120
121
  async function capture(targetArg, options = {}) {
121
122
  const timeoutSeconds = (0, captureRuntime_1.validateCaptureTimeout)(options.timeoutSeconds);
122
123
  const timeoutMs = Math.round(timeoutSeconds * 1000);
124
+ const devRouterPort = (0, dev_2.resolveDevRouterPort)(options.port ?? process.env.PLAYDROP_DEV_ROUTER_PORT);
123
125
  let minimumLogLevel;
124
126
  let loginOverride;
125
127
  let devOptions;
@@ -242,11 +244,13 @@ async function capture(targetArg, options = {}) {
242
244
  }
243
245
  const projectInfo = (0, devShared_1.findProjectInfo)(filePath);
244
246
  const devScriptAvailable = Boolean(projectInfo.projectDir && projectInfo.packageJson && typeof projectInfo.packageJson.scripts?.dev === 'string');
245
- await (0, commandContext_1.withEnvironment)('project capture', 'Capturing app logs', async ({ client, env, envConfig }) => {
247
+ await (0, commandContext_1.withEnvironment)('project capture', 'Capturing app logs', async ({ client, env, envConfig, token, workspaceAuth }) => {
246
248
  let currentUsername = '';
249
+ let currentUser = null;
247
250
  try {
248
- const currentUser = await (0, devShared_1.fetchDevUser)(client);
249
- currentUsername = currentUser.username.trim();
251
+ const fetchedCurrentUser = await (0, devShared_1.fetchDevUser)(client);
252
+ currentUser = fetchedCurrentUser;
253
+ currentUsername = fetchedCurrentUser.username.trim();
250
254
  }
251
255
  catch (error) {
252
256
  if (error instanceof http_1.CLIUnsupportedClientError) {
@@ -280,8 +284,14 @@ async function capture(targetArg, options = {}) {
280
284
  }
281
285
  throw error;
282
286
  }
283
- if ((devOptions.selection.devAuth === 'viewer' || devOptions.selection.devAuth === 'player') && !loginOverride) {
284
- (0, messages_1.printErrorWithHelp)(`Use --username and --password to establish a browser session before running --dev-auth ${devOptions.selection.devAuth}.`, [], { command: 'project capture' });
287
+ const taskScoped = Boolean(workspaceAuth?.config.taskToken);
288
+ if (devOptions.selection.devAuth === 'viewer' && !loginOverride && !taskScoped) {
289
+ (0, messages_1.printErrorWithHelp)('Use --username and --password to establish a browser session before running --dev-auth viewer.', [], { command: 'project capture' });
290
+ process.exitCode = 1;
291
+ return;
292
+ }
293
+ if (devOptions.selection.devAuth === 'player' && !loginOverride) {
294
+ (0, messages_1.printErrorWithHelp)('Use --username and --password to establish a browser session before running --dev-auth player.', [], { command: 'project capture' });
285
295
  process.exitCode = 1;
286
296
  return;
287
297
  }
@@ -291,7 +301,7 @@ async function capture(targetArg, options = {}) {
291
301
  creatorUsername: currentUsername,
292
302
  appType: appTypeSlug,
293
303
  appName,
294
- port: devServer_1.DEV_ROUTER_PORT,
304
+ port: devRouterPort,
295
305
  }, 750);
296
306
  const devServerStartedByCapture = !serverAlreadyRunning;
297
307
  let serverHandle = null;
@@ -328,7 +338,7 @@ async function capture(targetArg, options = {}) {
328
338
  creatorUsername: currentUsername,
329
339
  appType: appTypeSlug,
330
340
  appName,
331
- port: devServer_1.DEV_ROUTER_PORT,
341
+ port: devRouterPort,
332
342
  })).toString(),
333
343
  });
334
344
  }
@@ -344,14 +354,14 @@ async function capture(targetArg, options = {}) {
344
354
  creatorUsername: currentUsername,
345
355
  appType: appTypeSlug,
346
356
  appName,
347
- port: devServer_1.DEV_ROUTER_PORT,
357
+ port: devRouterPort,
348
358
  })}`);
349
359
  try {
350
360
  await (0, devServer_1.updateMountedDevRuntimeAssetManifest)({
351
361
  creatorUsername: currentUsername,
352
362
  appName,
353
363
  runtimeAssetManifest,
354
- port: devServer_1.DEV_ROUTER_PORT,
364
+ port: devRouterPort,
355
365
  });
356
366
  }
357
367
  catch (error) {
@@ -369,7 +379,7 @@ async function capture(targetArg, options = {}) {
369
379
  appType: appTypeSlug,
370
380
  creatorUsername: currentUsername,
371
381
  htmlPath: filePath,
372
- port: devServer_1.DEV_ROUTER_PORT,
382
+ port: devRouterPort,
373
383
  projectInfo,
374
384
  runtimeAssetManifest,
375
385
  });
@@ -389,7 +399,7 @@ async function capture(targetArg, options = {}) {
389
399
  ], { command: 'project capture' });
390
400
  }
391
401
  else {
392
- (0, messages_1.printErrorWithHelp)(error?.message || `Failed to start the shared dev router on port ${devServer_1.DEV_ROUTER_PORT}.`, [
402
+ (0, messages_1.printErrorWithHelp)(error?.message || `Failed to start the shared dev router on port ${devRouterPort}.`, [
393
403
  'Close the conflicting process or wait for the stale mount to exit.',
394
404
  'Ensure the HTML file exists and is readable.',
395
405
  ], { command: 'project capture' });
@@ -412,7 +422,11 @@ async function capture(targetArg, options = {}) {
412
422
  && (devOptions.resetMode === 'before' || devOptions.resetMode === 'before-and-after');
413
423
  const shouldResetAfter = devOptions.selection.devAuth === 'player'
414
424
  && (devOptions.resetMode === 'after' || devOptions.resetMode === 'before-and-after');
415
- const requiresRegisteredApp = devOptions.selection.devAuth !== 'anonymous' || shouldResetBefore || shouldResetAfter;
425
+ const savedSessionViewerCapture = devOptions.selection.devAuth === 'viewer' && !loginOverride && taskScoped;
426
+ const requiresRegisteredApp = devOptions.selection.devAuth === 'player'
427
+ || shouldResetBefore
428
+ || shouldResetAfter
429
+ || (devOptions.selection.devAuth === 'viewer' && !savedSessionViewerCapture);
416
430
  let registeredApp = null;
417
431
  if (requiresRegisteredApp) {
418
432
  try {
@@ -458,6 +472,7 @@ async function capture(targetArg, options = {}) {
458
472
  devAuth: devOptions.selection.devAuth,
459
473
  player: devOptions.selection.player ? String(devOptions.selection.player) : null,
460
474
  launchCheck: true,
475
+ localDevPort: devRouterPort === devServer_1.DEV_ROUTER_PORT ? null : devRouterPort,
461
476
  });
462
477
  const frameUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(captureBaseUrl, devOptions.selection);
463
478
  console.log(`[capture] Launching Playwright against ${frameUrl}`);
@@ -484,7 +499,9 @@ async function capture(targetArg, options = {}) {
484
499
  username: loginOverride.username,
485
500
  password: loginOverride.password,
486
501
  } : undefined,
487
- savedSessionBootstrap: false,
502
+ token: savedSessionViewerCapture ? token : undefined,
503
+ user: savedSessionViewerCapture ? currentUser : undefined,
504
+ savedSessionBootstrap: savedSessionViewerCapture,
488
505
  enableCaptureBridge: true,
489
506
  requireHostedLaunchReady: true,
490
507
  });
@@ -1,7 +1,6 @@
1
1
  type CreateOptions = {
2
2
  template?: string;
3
3
  remix?: string;
4
- gameIdea?: string;
5
4
  };
6
5
  export declare function createAssetSpecProject(name: string): Promise<void>;
7
6
  export declare function create(name: string, options?: CreateOptions): Promise<void>;