@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
@@ -17,8 +17,9 @@ export type AppUploadOptions = {
17
17
  creatorUsername?: string;
18
18
  token?: string | null;
19
19
  user?: UserResponse | null;
20
- runStagedUploadLaunchCheck?: boolean;
21
- launchCheckTimeoutMs?: number;
20
+ runStagedUploadLoadCheck?: boolean;
21
+ loadCheckTimeoutMs?: number;
22
22
  agentTaskId?: number;
23
+ mediaCaptureRequired?: boolean;
23
24
  };
24
25
  export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
@@ -7,7 +7,7 @@ const node_crypto_1 = require("node:crypto");
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
9
  const http_1 = require("../http");
10
- const launchCheck_1 = require("./launchCheck");
10
+ const loadCheck_1 = require("./loadCheck");
11
11
  const EXTENSION_TO_MIME = {
12
12
  '.png': 'image/png',
13
13
  '.jpg': 'image/jpeg',
@@ -74,6 +74,97 @@ async function bufferFromFile(file) {
74
74
  function computeSha256Hex(buffer) {
75
75
  return (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
76
76
  }
77
+ function isSha256Hex(value) {
78
+ return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
79
+ }
80
+ function isCaptureMediaFileKey(fileKey) {
81
+ return fileKey.startsWith('screenshotsPortrait:')
82
+ || fileKey.startsWith('screenshotsLandscape:')
83
+ || fileKey.startsWith('videosPortrait:')
84
+ || fileKey.startsWith('videosLandscape:');
85
+ }
86
+ function readMediaCaptureProof(input) {
87
+ const reportPath = input.task.listing?.captureReportPath;
88
+ if (!reportPath) {
89
+ throw new Error(`agent_task_media_capture_report_missing: ${input.task.name} must include listing.captureReport from "playdrop project capture".`);
90
+ }
91
+ let report;
92
+ try {
93
+ report = JSON.parse((0, node_fs_1.readFileSync)(reportPath, 'utf8'));
94
+ }
95
+ catch (error) {
96
+ const reason = error instanceof Error ? error.message : String(error);
97
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} must be valid JSON. ${reason}`);
98
+ }
99
+ const binding = report?.binding;
100
+ if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
101
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} is missing binding.`);
102
+ }
103
+ const contentHash = typeof binding.contentHash === 'string' ? binding.contentHash.trim().toLowerCase() : '';
104
+ if (!isSha256Hex(contentHash)) {
105
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.contentHash must be a SHA-256 hex string.`);
106
+ }
107
+ if (contentHash !== input.artifacts.bundleHash) {
108
+ throw new Error(`agent_task_media_capture_build_mismatch: capture report contentHash does not match the bundle being uploaded for ${input.task.name}. Rerun "playdrop project capture" after the final build changes.`);
109
+ }
110
+ const rawArtifactHashes = binding.artifactHashes;
111
+ if (!rawArtifactHashes || typeof rawArtifactHashes !== 'object' || Array.isArray(rawArtifactHashes)) {
112
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must be an object.`);
113
+ }
114
+ const captureArtifactHashEntries = Object.values(rawArtifactHashes)
115
+ .map((value) => typeof value === 'string' ? value.trim().toLowerCase() : '')
116
+ .filter((value) => isSha256Hex(value));
117
+ const captureArtifactHashes = new Set(captureArtifactHashEntries);
118
+ if (captureArtifactHashEntries.length === 0) {
119
+ throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes is empty.`);
120
+ }
121
+ const captureArtifactHashCounts = new Map();
122
+ for (const hash of captureArtifactHashEntries) {
123
+ captureArtifactHashCounts.set(hash, (captureArtifactHashCounts.get(hash) ?? 0) + 1);
124
+ }
125
+ const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureMediaFileKey(file.fileKey));
126
+ if (mediaFiles.length === 0) {
127
+ throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder stills and video in listing.screenshots* and listing.videos*.`);
128
+ }
129
+ const uploadedMediaHashCounts = new Map();
130
+ for (const file of mediaFiles) {
131
+ uploadedMediaHashCounts.set(file.sha256, (uploadedMediaHashCounts.get(file.sha256) ?? 0) + 1);
132
+ }
133
+ const missingUploadedCaptureArtifacts = [...captureArtifactHashCounts.entries()]
134
+ .filter(([hash, expectedCount]) => (uploadedMediaHashCounts.get(hash) ?? 0) < expectedCount);
135
+ if (missingUploadedCaptureArtifacts.length > 0) {
136
+ throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder poster and video from listing.captureReport in catalogue.json.`);
137
+ }
138
+ let hasStill = false;
139
+ let hasVideo = false;
140
+ const artifactHashes = {};
141
+ for (const file of mediaFiles) {
142
+ if (!captureArtifactHashes.has(file.sha256)) {
143
+ throw new Error(`agent_task_media_capture_artifact_mismatch: ${file.fileKey} was not produced by listing.captureReport for ${input.task.name}.`);
144
+ }
145
+ hasStill = hasStill || file.fileKey.startsWith('screenshots');
146
+ hasVideo = hasVideo || file.fileKey.startsWith('videos');
147
+ artifactHashes[file.fileKey] = file.sha256;
148
+ }
149
+ if (!hasStill || !hasVideo) {
150
+ throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder still and one recorder video.`);
151
+ }
152
+ const capturedSurfaces = Array.isArray(binding.capturedSurfaces)
153
+ ? Array.from(new Set(binding.capturedSurfaces
154
+ .map((surface) => typeof surface === 'string' ? surface.trim().toUpperCase() : '')
155
+ .filter((surface) => input.task.surfaceTargets.includes(surface))))
156
+ : [];
157
+ const capturedSurfaceSet = new Set(capturedSurfaces);
158
+ const missingSurfaces = input.task.surfaceTargets.filter((surface) => !capturedSurfaceSet.has(surface));
159
+ if (missingSurfaces.length > 0) {
160
+ throw new Error(`agent_task_media_capture_surface_missing: listing.captureReport did not capture ${missingSurfaces.join(', ')} for ${input.task.name}.`);
161
+ }
162
+ return {
163
+ contentHash,
164
+ artifactHashes,
165
+ capturedSurfaces,
166
+ };
167
+ }
77
168
  function resolveMimeType(filename, fallback = 'application/octet-stream') {
78
169
  const type = EXTENSION_TO_MIME[(0, node_path_1.extname)(filename).toLowerCase()];
79
170
  return type ?? fallback;
@@ -339,20 +430,33 @@ async function uploadAppVersion(client, task, artifacts, options) {
339
430
  })),
340
431
  });
341
432
  }
433
+ if (options?.mediaCaptureRequired === true
434
+ && task.hostingMode !== 'EXTERNAL'
435
+ && !task.externalUrl) {
436
+ if (!artifacts) {
437
+ throw new Error(`agent_task_media_capture_build_missing: ${task.name} has no hosted build artifacts to bind capture media to.`);
438
+ }
439
+ const mediaCaptureProof = readMediaCaptureProof({
440
+ task,
441
+ artifacts,
442
+ preparedSessionFiles,
443
+ });
444
+ await client.recordAppUploadMediaCapture(creatorUsername, task.name, sessionId, mediaCaptureProof);
445
+ }
342
446
  if (task.hostingMode !== 'EXTERNAL'
343
447
  && !task.externalUrl
344
- && options?.runStagedUploadLaunchCheck) {
345
- const launchCheck = await (0, launchCheck_1.runUploadedHostedLaunchCheck)({
448
+ && options?.runStagedUploadLoadCheck) {
449
+ const loadCheck = await (0, loadCheck_1.runUploadedHostedLoadCheck)({
346
450
  client,
347
451
  creatorUsername,
348
452
  task,
349
453
  sessionId,
350
- timeoutMs: options.launchCheckTimeoutMs,
454
+ timeoutMs: options.loadCheckTimeoutMs,
351
455
  token: options?.token ?? null,
352
456
  currentUser: options?.user ?? null,
353
457
  });
354
- if (launchCheck.status !== 'PASSED') {
355
- throw new Error((0, launchCheck_1.formatHostedLaunchCheckFailure)(task.name, launchCheck, 'staged'));
458
+ if (loadCheck.status !== 'PASSED') {
459
+ throw new Error((0, loadCheck_1.formatHostedLoadCheckFailure)(task.name, loadCheck, 'staged'));
356
460
  }
357
461
  }
358
462
  }
@@ -12,6 +12,7 @@ export declare function parseSurfaceTargets(input: unknown): ParsedSurfaceTarget
12
12
  export type AppCatalogueInfo = {
13
13
  name: string;
14
14
  type: string;
15
+ previewable: boolean;
15
16
  surfaceTargets: ParsedSurfaceTargets;
16
17
  cataloguePath: string;
17
18
  };
@@ -108,6 +108,7 @@ function findAppCatalogueInfo(htmlPath) {
108
108
  return {
109
109
  name,
110
110
  type: parsedType,
111
+ previewable: match.previewable === true,
111
112
  surfaceTargets: cloneSurfaceTargets(surfaceTargets),
112
113
  cataloguePath,
113
114
  };
@@ -19,6 +19,7 @@ export type AppListingConfig = {
19
19
  screenshotsLandscape?: string[];
20
20
  videosPortrait?: string[];
21
21
  videosLandscape?: string[];
22
+ captureReport?: string;
22
23
  };
23
24
  export type ResolvedAppAchievementDefinition = Omit<AppAchievementCatalogueDefinition, 'status'> & {
24
25
  iconPath: string;
@@ -152,6 +153,7 @@ export type ResolvedListingAssets = {
152
153
  screenshotLandscapePaths: string[];
153
154
  videoPortraitPaths: string[];
154
155
  videoLandscapePaths: string[];
156
+ captureReportPath?: string;
155
157
  };
156
158
  export type AppDependencyAssetRef = {
157
159
  ref: string;
package/dist/catalogue.js CHANGED
@@ -1249,6 +1249,23 @@ function buildAppTasks(rootDir, catalogues, options) {
1249
1249
  }
1250
1250
  }
1251
1251
  }
1252
+ if (!listingValidationFailed && typeof rawListing.captureReport === 'string' && rawListing.captureReport.trim()) {
1253
+ const captureReportPath = rawListing.captureReport.trim();
1254
+ if ((0, node_path_1.extname)(captureReportPath).toLowerCase() !== '.json') {
1255
+ errors.push(`[${label}] Skipping ${rawName}: listing.captureReport must point to a JSON file.`);
1256
+ listingValidationFailed = true;
1257
+ }
1258
+ else {
1259
+ const validationError = validateListingAssetPath(file.directory, captureReportPath, 'listing.captureReport');
1260
+ if (validationError) {
1261
+ errors.push(`[${label}] Skipping ${rawName}: ${validationError}`);
1262
+ listingValidationFailed = true;
1263
+ }
1264
+ else {
1265
+ resolvedListing.captureReportPath = resolveListingAssetPath(file.directory, captureReportPath);
1266
+ }
1267
+ }
1268
+ }
1252
1269
  if (listingValidationFailed) {
1253
1270
  continue;
1254
1271
  }
@@ -23,9 +23,11 @@ async function build(pathOrName) {
23
23
  for (const task of (0, taskUtils_1.sortTasks)(tasks)) {
24
24
  const entityId = task.name;
25
25
  try {
26
+ let detail;
26
27
  if (task.kind === 'app') {
27
28
  await (0, apps_1.validateAppTask)(task);
28
- await (0, apps_1.buildApp)(task);
29
+ const artifacts = await (0, apps_1.buildApp)(task);
30
+ detail = `bundleHash=${artifacts.bundleHash}`;
29
31
  }
30
32
  else if (task.kind === 'asset' || task.kind === 'owned-asset') {
31
33
  const generated = await (0, model_artifacts_1.prepareModel3DAssetArtifacts)(task);
@@ -42,6 +44,7 @@ async function build(pathOrName) {
42
44
  entityType: task.kind,
43
45
  entityId,
44
46
  catalogue: task.cataloguePath,
47
+ detail,
45
48
  };
46
49
  results.push(entry);
47
50
  console.log((0, uploadLog_1.formatTaskLogLine)(entry));
@@ -1,3 +1,5 @@
1
+ import { type AppSurface } from '@playdrop/types';
2
+ import type { BrowserContextOptions } from 'playwright-core';
1
3
  type CaptureListingOptions = {
2
4
  app?: string;
3
5
  duration?: string | number;
@@ -8,6 +10,7 @@ type CaptureListingOptions = {
8
10
  audio?: boolean;
9
11
  outputDir?: string;
10
12
  keepRaw?: boolean;
13
+ contentHash?: string;
11
14
  };
12
15
  type ParsedCaptureListingOptions = {
13
16
  targetArg?: string;
@@ -20,6 +23,8 @@ type ParsedCaptureListingOptions = {
20
23
  audio: boolean;
21
24
  outputDir: string | null;
22
25
  keepRaw: boolean;
26
+ contentHash: string | null;
27
+ explicitDimensions: boolean;
23
28
  };
24
29
  type CaptureRect = {
25
30
  x: number;
@@ -45,11 +50,35 @@ type CropRect = {
45
50
  width: number;
46
51
  height: number;
47
52
  };
48
- export declare function resolveListingCaptureRouteSegment(previewable: boolean): 'dev' | 'dev-preview';
53
+ export declare function resolveListingCaptureRouteSegment(previewable: boolean): 'dev-preview';
54
+ export declare function resolveCaptureListingDevRouterPort(env?: NodeJS.ProcessEnv): number;
55
+ export declare function resolveListingCaptureLocalDevPort(devRouterPort: number): number | null;
56
+ export declare function buildListingCaptureFrameUrl({ webBase, currentUsername, appTypeSlug, appName, routeSegment, localDevPort, }: {
57
+ webBase: string;
58
+ currentUsername: string;
59
+ appTypeSlug: string;
60
+ appName: string;
61
+ routeSegment: 'dev-preview';
62
+ localDevPort: number | null;
63
+ }): string;
49
64
  export declare function resolveListingCaptureSceneId(width: number, height: number): 'listing-landscape' | 'listing-portrait';
50
65
  export declare function parseCaptureListingOptions(targetArg: string | undefined, options?: CaptureListingOptions): ParsedCaptureListingOptions;
51
66
  export declare function assertSupportedListingEnvironment(platform?: NodeJS.Platform, macosVersion?: string): void;
52
67
  export declare function resolveListingRecorderPath(baseDir?: string): string;
68
+ export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppSurface, useSurfacePrefix: boolean): {
69
+ rawVideoPath: string;
70
+ metadataPath: string;
71
+ posterPath: string;
72
+ finalVideoPath: string;
73
+ };
74
+ export declare function resolveCaptureDimensions(surface: AppSurface, parsedOptions: ParsedCaptureListingOptions): {
75
+ width: number;
76
+ height: number;
77
+ };
78
+ export declare function resolveListingCaptureBrowserContextOptions(surface: AppSurface, dimensions: {
79
+ width: number;
80
+ height: number;
81
+ }): BrowserContextOptions;
53
82
  export declare function computeRecordedCrop(measurement: HostedGameMeasurement, rawWidth: number, rawHeight: number): CropRect;
54
83
  export declare function captureListing(targetArg: string | undefined, options?: CaptureListingOptions): Promise<void>;
55
84
  export {};