@playdrop/playdrop-cli 0.9.6 → 0.10.1

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 (73) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apiClient.d.ts +10 -0
  3. package/dist/apiClient.js +55 -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 +14 -0
  16. package/dist/captureRuntime.js +329 -0
  17. package/dist/catalogue.d.ts +4 -2
  18. package/dist/catalogue.js +50 -7
  19. package/dist/commandContext.js +61 -4
  20. package/dist/commands/capture.d.ts +1 -0
  21. package/dist/commands/capture.js +30 -13
  22. package/dist/commands/captureRemote.d.ts +2 -0
  23. package/dist/commands/captureRemote.js +90 -0
  24. package/dist/commands/create.d.ts +0 -1
  25. package/dist/commands/create.js +2 -151
  26. package/dist/commands/creations.d.ts +0 -13
  27. package/dist/commands/creations.js +0 -141
  28. package/dist/commands/dev.d.ts +2 -1
  29. package/dist/commands/dev.js +23 -6
  30. package/dist/commands/devServer.js +3 -1
  31. package/dist/commands/generation.d.ts +1 -0
  32. package/dist/commands/generation.js +274 -0
  33. package/dist/commands/review.d.ts +46 -0
  34. package/dist/commands/review.js +353 -0
  35. package/dist/commands/upload.d.ts +27 -1
  36. package/dist/commands/upload.js +962 -21
  37. package/dist/commands/validate.js +5 -0
  38. package/dist/commands/worker/runtime.d.ts +81 -0
  39. package/dist/commands/worker/runtime.js +458 -0
  40. package/dist/commands/worker.d.ts +158 -0
  41. package/dist/commands/worker.js +2626 -0
  42. package/dist/config.d.ts +2 -0
  43. package/dist/config.js +23 -0
  44. package/dist/index.js +116 -30
  45. package/dist/shellProbe.d.ts +1 -1
  46. package/dist/shellProbe.js +3 -3
  47. package/dist/workspaceAuth.d.ts +3 -0
  48. package/dist/workspaceAuth.js +14 -0
  49. package/node_modules/@playdrop/api-client/dist/client.d.ts +36 -15
  50. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/client.js +2 -2
  52. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +5 -2
  53. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/admin.js +51 -3
  55. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +75 -0
  56. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -0
  57. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +478 -0
  58. package/node_modules/@playdrop/api-client/dist/index.d.ts +36 -15
  59. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  60. package/node_modules/@playdrop/api-client/dist/index.js +153 -42
  61. package/node_modules/@playdrop/config/client-meta.json +2 -2
  62. package/node_modules/@playdrop/types/dist/api.d.ts +662 -75
  63. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  64. package/node_modules/@playdrop/types/dist/api.js +100 -9
  65. package/node_modules/@playdrop/types/dist/app.d.ts +2 -0
  66. package/node_modules/@playdrop/types/dist/app.d.ts.map +1 -1
  67. package/node_modules/@playdrop/types/dist/app.js +3 -0
  68. package/node_modules/@playdrop/types/dist/version.d.ts +1 -0
  69. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  70. package/package.json +2 -1
  71. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts +0 -46
  72. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts.map +0 -1
  73. 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,318 @@ 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 runCaptureActions(page, actions) {
510
+ if (!actions?.length) {
511
+ return;
512
+ }
513
+ for (const action of actions) {
514
+ if (action.type === 'click') {
515
+ await page.mouse.click(action.x, action.y, { button: action.button ?? 'left' });
516
+ continue;
517
+ }
518
+ if (action.type === 'press') {
519
+ await page.keyboard.press(action.key);
520
+ continue;
521
+ }
522
+ if (action.type === 'wait') {
523
+ await page.waitForTimeout(action.ms);
524
+ continue;
525
+ }
526
+ throw new Error('capture_action_type_unknown');
527
+ }
528
+ }
529
+ async function assertHostedFrameVisualReadiness(page) {
530
+ const childFrames = page.frames().filter(frame => frame.parentFrame() !== null);
531
+ if (childFrames.length === 0) {
532
+ throw new Error('hosted_app_visual_blank: The hosted app reported ready but no game frame was available for visual validation.');
533
+ }
534
+ const summaries = await Promise.all(childFrames.map(frame => summarizeVisibleCanvases(frame)));
535
+ const framesWithCanvases = summaries.filter(summary => summary.visibleCanvasCount > 0);
536
+ if (framesWithCanvases.length === 0) {
537
+ return;
538
+ }
539
+ if (framesWithCanvases.some(summary => summary.passingCanvasCount > 0)) {
540
+ return;
541
+ }
542
+ const screenshotSummaries = await summarizeCanvasScreenshots(page, childFrames, summaries);
543
+ if (screenshotSummaries.some(summary => summary.pass)) {
544
+ return;
545
+ }
546
+ const readableFrames = framesWithCanvases.filter(summary => summary.readableCanvasCount > 0);
547
+ if (readableFrames.length === 0) {
548
+ return;
549
+ }
550
+ const screenshotDetail = screenshotSummaries.length > 0
551
+ ? `; screenshot foregroundRatio=${screenshotSummaries[0].foregroundRatio.toFixed(3)}, colorRange=${screenshotSummaries[0].colorRange.toFixed(1)}, opaqueRatio=${screenshotSummaries[0].opaqueRatio.toFixed(3)}`
552
+ : '';
553
+ const detail = `${readableFrames.map(formatCanvasVisualSummary).join('; ')}${screenshotDetail}`;
554
+ 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.`);
555
+ }
235
556
  async function writeLogFile(logPath, lines) {
236
557
  if (!logPath || logPath.trim().length === 0) {
237
558
  return;
@@ -621,8 +942,15 @@ async function runCapture(options) {
621
942
  }
622
943
  }
623
944
  finalUrl = await assertPageState();
945
+ await runCaptureActions(page, options.actions);
624
946
  await page.waitForTimeout(options.settleAfterReadyMs ?? options.timeoutMs);
625
947
  finalUrl = await assertPageState();
948
+ if (shouldAssertHostedVisualReadiness({
949
+ requireHostedLaunchReady: options.requireHostedLaunchReady,
950
+ expectedHostedLaunchState,
951
+ })) {
952
+ await assertHostedFrameVisualReadiness(page);
953
+ }
626
954
  if (options.screenshotPath) {
627
955
  const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
628
956
  if (targetDir && targetDir !== '.' && targetDir !== '/') {
@@ -641,6 +969,7 @@ async function runCapture(options) {
641
969
  }
642
970
  return {
643
971
  errorCount: errors.length,
972
+ errors: errors.slice(0, 10),
644
973
  warningCount: warnings.length,
645
974
  warnings,
646
975
  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,26 @@ 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 agentTaskId = workspaceAuth?.config.taskId ?? null;
44
+ const agentTaskAttempt = workspaceAuth?.config.taskAttempt ?? null;
45
+ const client = (0, apiClient_1.createCliApiClient)({
46
+ baseUrl: envConfig.apiBase,
47
+ token,
48
+ onBehalfCreatorUsername,
49
+ agentTaskToken,
50
+ agentTaskId,
51
+ agentTaskAttempt,
52
+ });
53
+ const aiClient = (0, apiClient_1.createCliAiClient)({
54
+ baseUrl: envConfig.aiBase,
55
+ token,
56
+ onBehalfCreatorUsername,
57
+ agentTaskToken,
58
+ agentTaskId,
59
+ agentTaskAttempt,
60
+ });
43
61
  return {
44
62
  client,
45
63
  aiClient,
@@ -90,8 +108,10 @@ async function migrateLegacyConfigIfNeeded(cfg, envConfig, command) {
90
108
  async function loadWorkspaceAwareConfig(command, options) {
91
109
  let cfg = (0, config_1.loadConfig)();
92
110
  let workspaceAuth = null;
111
+ const workspacePath = options.workspacePath
112
+ ?? (process.env.PLAYDROP_WORKER_CONTEXT === '1' ? process.cwd() : undefined);
93
113
  try {
94
- workspaceAuth = options.workspacePath ? (0, workspaceAuth_1.findWorkspaceAuthConfig)(options.workspacePath) : null;
114
+ workspaceAuth = workspacePath ? (0, workspaceAuth_1.findWorkspaceAuthConfig)(workspacePath) : null;
95
115
  }
96
116
  catch (error) {
97
117
  if (error instanceof workspaceAuth_1.WorkspaceAuthConfigError) {
@@ -124,8 +144,30 @@ function resolveWorkspaceSelectedAccount(cfg, currentAccount, workspaceAuth, req
124
144
  const matchingSessions = (0, config_1.listAccountSessionsForUsername)(workspaceAuth.config.ownerUsername, cfg);
125
145
  const preferredEnv = normalizeRequestedEnv(requestedEnv) ?? workspaceAuth.config.env;
126
146
  if (preferredEnv) {
147
+ const ownerAccount = (0, config_1.findAccountSession)(workspaceAuth.config.ownerUsername, preferredEnv, cfg);
148
+ if (ownerAccount) {
149
+ return {
150
+ account: ownerAccount,
151
+ matchingSessions,
152
+ };
153
+ }
154
+ if (workspaceAuth.config.taskToken && currentAccount) {
155
+ if (currentAccount.env === preferredEnv) {
156
+ return {
157
+ account: currentAccount,
158
+ matchingSessions,
159
+ };
160
+ }
161
+ const currentAccountForEnv = (0, config_1.findAccountSession)(currentAccount.username, preferredEnv, cfg);
162
+ if (currentAccountForEnv) {
163
+ return {
164
+ account: currentAccountForEnv,
165
+ matchingSessions,
166
+ };
167
+ }
168
+ }
127
169
  return {
128
- account: (0, config_1.findAccountSession)(workspaceAuth.config.ownerUsername, preferredEnv, cfg),
170
+ account: null,
129
171
  matchingSessions,
130
172
  };
131
173
  }
@@ -141,6 +183,21 @@ function resolveWorkspaceSelectedAccount(cfg, currentAccount, workspaceAuth, req
141
183
  matchingSessions,
142
184
  };
143
185
  }
186
+ if (currentAccount) {
187
+ if (!preferredEnv || currentAccount.env === preferredEnv) {
188
+ return {
189
+ account: currentAccount,
190
+ matchingSessions,
191
+ };
192
+ }
193
+ const currentAccountForEnv = (0, config_1.findAccountSession)(currentAccount.username, preferredEnv, cfg);
194
+ if (currentAccountForEnv) {
195
+ return {
196
+ account: currentAccountForEnv,
197
+ matchingSessions,
198
+ };
199
+ }
200
+ }
144
201
  return {
145
202
  account: null,
146
203
  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;