@playdrop/playdrop-cli 0.10.21 → 0.11.4

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.21",
2
+ "version": "0.11.4",
3
3
  "build": 1,
4
4
  "runtimeSdkVersion": "0.10.0",
5
5
  "clients": {
package/dist/appUrls.d.ts CHANGED
@@ -7,6 +7,9 @@ export type AppUrlInput = {
7
7
  export declare function normalizePlaydropWebBase(webBase?: string | null): string;
8
8
  export declare function getAppTypeSlug(type: AppType | string | null | undefined): string;
9
9
  export declare function buildPlatformPlayUrl(webBase: string | null | undefined, input: AppUrlInput): string;
10
+ export declare function buildPlatformPrivateDraftPlayUrl(webBase: string | null | undefined, input: AppUrlInput & {
11
+ version: string;
12
+ }): string;
10
13
  export declare function buildPlatformDevUrl(webBase: string | null | undefined, input: AppUrlInput & {
11
14
  devAuth?: 'prompt' | 'anonymous' | 'viewer' | 'player';
12
15
  player?: string | null;
package/dist/appUrls.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizePlaydropWebBase = normalizePlaydropWebBase;
4
4
  exports.getAppTypeSlug = getAppTypeSlug;
5
5
  exports.buildPlatformPlayUrl = buildPlatformPlayUrl;
6
+ exports.buildPlatformPrivateDraftPlayUrl = buildPlatformPrivateDraftPlayUrl;
6
7
  exports.buildPlatformDevUrl = buildPlatformDevUrl;
7
8
  exports.buildUploadLaunchCheckUrl = buildUploadLaunchCheckUrl;
8
9
  const DEFAULT_WEB_BASE = 'https://www.playdrop.ai';
@@ -33,6 +34,17 @@ function buildPlatformPlayUrl(webBase, input) {
33
34
  const appName = encodeURIComponent(input.appName);
34
35
  return `${base}/creators/${creator}/apps/${typeSlug}/${appName}/play`;
35
36
  }
37
+ function buildPlatformPrivateDraftPlayUrl(webBase, input) {
38
+ const base = normalizePlaydropWebBase(webBase);
39
+ const creator = encodeURIComponent(input.creatorUsername);
40
+ const typeSlug = getAppTypeSlug(input.appType);
41
+ const appName = encodeURIComponent(input.appName);
42
+ const version = encodeURIComponent(input.version.trim());
43
+ if (!version) {
44
+ throw new Error('invalid_private_draft_play_version');
45
+ }
46
+ return `${base}/_auth/creators/${creator}/apps/${typeSlug}/${appName}/play?v=${version}`;
47
+ }
36
48
  function buildPlatformDevUrl(webBase, input) {
37
49
  const base = normalizePlaydropWebBase(webBase);
38
50
  const creator = encodeURIComponent(input.creatorUsername);
@@ -8,7 +8,7 @@ export type HostedLaunchCheckResult = {
8
8
  checkedAt: string;
9
9
  artifactFingerprint: string | null;
10
10
  };
11
- export declare function formatHostedLaunchCheckFailure(taskName: string, result: HostedLaunchCheckResult, scope?: 'local' | 'staged'): string;
11
+ export declare function formatHostedLaunchCheckFailure(taskName: string, result: HostedLaunchCheckResult, scope?: 'local' | 'staged' | 'final'): string;
12
12
  export declare function runLocalHostedLaunchCheck(input: {
13
13
  client: ApiClient;
14
14
  apiBase: string;
@@ -30,3 +30,10 @@ export declare function runUploadedHostedLaunchCheck(input: {
30
30
  token?: string | null;
31
31
  currentUser?: UserResponse | null;
32
32
  }): Promise<HostedLaunchCheckResult>;
33
+ export declare function runPublishedHostedPlayCheck(input: {
34
+ targetUrl: string;
35
+ surfaceTargets?: unknown;
36
+ timeoutMs?: number;
37
+ token?: string | null;
38
+ currentUser?: UserResponse | null;
39
+ }): Promise<HostedLaunchCheckResult>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatHostedLaunchCheckFailure = formatHostedLaunchCheckFailure;
4
4
  exports.runLocalHostedLaunchCheck = runLocalHostedLaunchCheck;
5
5
  exports.runUploadedHostedLaunchCheck = runUploadedHostedLaunchCheck;
6
+ exports.runPublishedHostedPlayCheck = runPublishedHostedPlayCheck;
6
7
  const appUrls_1 = require("../appUrls");
7
8
  const captureRuntime_1 = require("../captureRuntime");
8
9
  const devShared_1 = require("../commands/devShared");
@@ -13,13 +14,67 @@ const devRuntimeAssets_1 = require("../commands/devRuntimeAssets");
13
14
  const registration_1 = require("./registration");
14
15
  const DEFAULT_HOSTED_LAUNCH_TIMEOUT_MS = 15000;
15
16
  const POST_READY_SETTLE_MS = 750;
16
- const CAPTURE_SURFACE = 'DESKTOP';
17
+ 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';
18
+ const LAUNCH_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
19
+ const LAUNCH_CHECK_SURFACE_CONTEXT_OPTIONS = {
20
+ DESKTOP: {
21
+ viewport: { width: 1280, height: 720 },
22
+ deviceScaleFactor: 1,
23
+ isMobile: false,
24
+ hasTouch: false,
25
+ },
26
+ MOBILE_LANDSCAPE: {
27
+ viewport: { width: 896, height: 414 },
28
+ deviceScaleFactor: 2,
29
+ isMobile: true,
30
+ hasTouch: true,
31
+ userAgent: MOBILE_USER_AGENT,
32
+ },
33
+ MOBILE_PORTRAIT: {
34
+ viewport: { width: 414, height: 896 },
35
+ deviceScaleFactor: 2,
36
+ isMobile: true,
37
+ hasTouch: true,
38
+ userAgent: MOBILE_USER_AGENT,
39
+ },
40
+ };
41
+ function normalizeLaunchCheckSurface(value) {
42
+ if (typeof value !== 'string') {
43
+ return null;
44
+ }
45
+ const normalized = value.trim().toUpperCase();
46
+ return LAUNCH_CHECK_SURFACE_ORDER.includes(normalized)
47
+ ? normalized
48
+ : null;
49
+ }
50
+ function normalizeLaunchCheckSurfaceTargets(value) {
51
+ if (!Array.isArray(value)) {
52
+ return [];
53
+ }
54
+ return Array.from(new Set(value.map(normalizeLaunchCheckSurface).filter((entry) => Boolean(entry))));
55
+ }
56
+ function resolveLaunchCheckSurface(surfaceTargets) {
57
+ const targets = normalizeLaunchCheckSurfaceTargets(surfaceTargets);
58
+ for (const surface of LAUNCH_CHECK_SURFACE_ORDER) {
59
+ if (targets.includes(surface)) {
60
+ return surface;
61
+ }
62
+ }
63
+ return 'DESKTOP';
64
+ }
65
+ function cloneLaunchCheckContextOptions(surface) {
66
+ const source = LAUNCH_CHECK_SURFACE_CONTEXT_OPTIONS[surface];
67
+ return {
68
+ ...source,
69
+ viewport: source.viewport ? { ...source.viewport } : source.viewport,
70
+ };
71
+ }
17
72
  function resolvePostAuthHostedLaunchState(controllerMode) {
18
73
  return controllerMode === 'REQUIRED' ? 'controller_required' : 'ready';
19
74
  }
20
75
  function resolveAnonymousHostedLaunchState(input) {
21
- const surfaceTargets = Array.isArray(input.surfaceTargets) ? input.surfaceTargets : [];
22
- if (!surfaceTargets.includes(CAPTURE_SURFACE)) {
76
+ const surfaceTargets = normalizeLaunchCheckSurfaceTargets(input.surfaceTargets);
77
+ if (!surfaceTargets.includes(input.captureSurface)) {
23
78
  return 'surface_unsupported';
24
79
  }
25
80
  if (input.authMode === 'REQUIRED') {
@@ -64,7 +119,11 @@ function formatCaptureErrors(errors) {
64
119
  return ` First error${normalized.length === 1 ? '' : 's'}: ${normalized.join(' | ')}`;
65
120
  }
66
121
  function formatHostedLaunchCheckFailure(taskName, result, scope = 'local') {
67
- const scopeLabel = scope === 'staged' ? 'staged upload preview' : 'local hosted preview';
122
+ const scopeLabel = scope === 'final'
123
+ ? 'final owner play route'
124
+ : scope === 'staged'
125
+ ? 'staged upload preview'
126
+ : 'local hosted preview';
68
127
  const errorCode = result.errorCode?.trim() || 'startup_failure';
69
128
  const message = normalizeLaunchCheckMessage(result.message);
70
129
  return `hosted_launch_check_failed: ${taskName} failed the ${scopeLabel} launch-check (${errorCode}). ${message}`;
@@ -102,13 +161,10 @@ function buildRegistrationRequiredLaunchCheckFailure(taskName) {
102
161
  return buildFailedLaunchCheckResult('app_registration_required_for_auth_validation', `Auth-required hosted app "${taskName}" must be registered on PlayDrop before viewer launch validation can run. Run "playdrop project create app ${taskName}" and try again.`, null);
103
162
  }
104
163
  function appendLaunchCheckParams(targetUrl, devAuth) {
105
- const withAuth = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(targetUrl, {
164
+ return (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(targetUrl, {
106
165
  devAuth,
107
166
  player: null,
108
167
  });
109
- const nextUrl = new URL(withAuth);
110
- nextUrl.searchParams.set('launchCheck', '1');
111
- return nextUrl.toString();
112
168
  }
113
169
  async function runHostedLaunchCheckWithCapture(options) {
114
170
  const expectedHostedLaunchState = options.expectedHostedLaunchState ?? 'ready';
@@ -125,6 +181,7 @@ async function runHostedLaunchCheckWithCapture(options) {
125
181
  token: options.token ?? null,
126
182
  user: options.user ?? null,
127
183
  savedSessionBootstrap: options.savedSessionBootstrap ?? false,
184
+ contextOptions: options.contextOptions,
128
185
  });
129
186
  if (result.errorCount > 0) {
130
187
  return buildFailedLaunchCheckResult('startup_failure', `Hosted app emitted ${result.errorCount} console, page, or network error(s) during startup.${formatCaptureErrors(result.errors)}`, options.artifactFingerprint ?? null);
@@ -210,7 +267,12 @@ async function runLocalHostedLaunchCheck(input) {
210
267
  ? null
211
268
  : input.devRouterPort;
212
269
  const localAppMetadata = mountedRuntime.runtimeAssetManifest.response.localAppMetadata;
213
- const anonymousExpectedState = resolveAnonymousHostedLaunchState(localAppMetadata);
270
+ const captureSurface = resolveLaunchCheckSurface(localAppMetadata.surfaceTargets);
271
+ const contextOptions = cloneLaunchCheckContextOptions(captureSurface);
272
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
273
+ ...localAppMetadata,
274
+ captureSurface,
275
+ });
214
276
  if (anonymousExpectedState === 'login_required') {
215
277
  const registeredApp = await (0, registration_1.fetchRegisteredAppShell)(input.client, input.creatorUsername, input.task.name);
216
278
  const canLaunchUnregisteredViewer = input.allowUnregisteredViewerLaunch === true
@@ -230,6 +292,7 @@ async function runLocalHostedLaunchCheck(input) {
230
292
  }),
231
293
  timeoutMs: input.timeoutMs,
232
294
  expectedHostedLaunchState: anonymousExpectedState,
295
+ contextOptions,
233
296
  });
234
297
  if (anonymousGateResult.status !== 'PASSED') {
235
298
  return anonymousGateResult;
@@ -248,6 +311,7 @@ async function runLocalHostedLaunchCheck(input) {
248
311
  token: input.token ?? null,
249
312
  user: input.currentUser ?? null,
250
313
  savedSessionBootstrap: true,
314
+ contextOptions,
251
315
  });
252
316
  }
253
317
  return await runHostedLaunchCheckWithCapture({
@@ -261,6 +325,7 @@ async function runLocalHostedLaunchCheck(input) {
261
325
  }),
262
326
  timeoutMs: input.timeoutMs,
263
327
  expectedHostedLaunchState: anonymousExpectedState,
328
+ contextOptions,
264
329
  });
265
330
  }
266
331
  finally {
@@ -271,7 +336,12 @@ async function runLocalHostedLaunchCheck(input) {
271
336
  }
272
337
  async function runUploadedHostedLaunchCheck(input) {
273
338
  const preview = await input.client.fetchAppUploadLaunchCheckPreview(input.creatorUsername, input.task.name, input.sessionId);
274
- const anonymousExpectedState = resolveAnonymousHostedLaunchState(preview.localAppMetadata);
339
+ const captureSurface = resolveLaunchCheckSurface(preview.localAppMetadata.surfaceTargets);
340
+ const contextOptions = cloneLaunchCheckContextOptions(captureSurface);
341
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
342
+ ...preview.localAppMetadata,
343
+ captureSurface,
344
+ });
275
345
  let result;
276
346
  if (anonymousExpectedState === 'login_required') {
277
347
  if (!preview.app?.id) {
@@ -283,6 +353,7 @@ async function runUploadedHostedLaunchCheck(input) {
283
353
  artifactFingerprint: preview.session.manifestHash,
284
354
  timeoutMs: input.timeoutMs,
285
355
  expectedHostedLaunchState: anonymousExpectedState,
356
+ contextOptions,
286
357
  });
287
358
  if (anonymousGateResult.status !== 'PASSED') {
288
359
  result = anonymousGateResult;
@@ -296,6 +367,7 @@ async function runUploadedHostedLaunchCheck(input) {
296
367
  token: input.token ?? null,
297
368
  user: input.currentUser ?? null,
298
369
  savedSessionBootstrap: true,
370
+ contextOptions,
299
371
  });
300
372
  }
301
373
  }
@@ -306,6 +378,7 @@ async function runUploadedHostedLaunchCheck(input) {
306
378
  artifactFingerprint: preview.session.manifestHash,
307
379
  timeoutMs: input.timeoutMs,
308
380
  expectedHostedLaunchState: anonymousExpectedState,
381
+ contextOptions,
309
382
  });
310
383
  }
311
384
  const recorded = await input.client.recordAppUploadLaunchCheck(input.creatorUsername, input.task.name, input.sessionId, {
@@ -326,3 +399,16 @@ async function runUploadedHostedLaunchCheck(input) {
326
399
  artifactFingerprint: recordedLaunchCheck.artifactFingerprint,
327
400
  };
328
401
  }
402
+ async function runPublishedHostedPlayCheck(input) {
403
+ const captureSurface = resolveLaunchCheckSurface(input.surfaceTargets);
404
+ return runHostedLaunchCheckWithCapture({
405
+ targetUrl: input.targetUrl,
406
+ artifactFingerprint: null,
407
+ timeoutMs: input.timeoutMs,
408
+ expectedHostedLaunchState: 'ready',
409
+ token: input.token ?? null,
410
+ user: input.currentUser ?? null,
411
+ savedSessionBootstrap: true,
412
+ contextOptions: cloneLaunchCheckContextOptions(captureSurface),
413
+ });
414
+ }
@@ -253,6 +253,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
253
253
  })),
254
254
  usesPacks: task.uses.packs,
255
255
  tags: task.tags,
256
+ design: task.design,
256
257
  clearTags: options?.clearTags,
257
258
  surfaceTargets: task.surfaceTargets,
258
259
  assetSpecSupport: (task.assetSpecSupport ?? []).map((row) => ({
@@ -409,6 +410,10 @@ async function uploadAppVersion(client, task, artifacts, options) {
409
410
  + `Delete one old version: playdrop creations apps versions delete ${task.name} <version>`);
410
411
  }
411
412
  if (unknownError.status === 409 && unknownError.code === 'version_exists') {
413
+ if (options?.agentTaskId !== undefined) {
414
+ throw new Error(`Version ${task.version} already exists for app "${task.name}".\n` +
415
+ 'This task has a fixed outputVersion; do not bump catalogue.json. The required task version has already been consumed by an upload attempt, so fail the task with the operational reason.');
416
+ }
412
417
  throw new Error(`Version ${task.version} already exists for app "${task.name}".\n` +
413
418
  `Hint: Update "version" in catalogue.json to a new version (e.g., bump to the next patch).`);
414
419
  }
@@ -48,7 +48,6 @@ const SDK_REFERENCE_PATTERNS = [
48
48
  const SDK_INIT_PATTERNS = [
49
49
  /\bwindow\s*\.\s*playdrop\s*\.\s*init\s*\(/g,
50
50
  /\bplaydrop\s*\.\s*init\s*\(/g,
51
- /\bsdk\s*\.\s*initialize\s*\(/g,
52
51
  ];
53
52
  const SDK_READY_PATTERNS = [
54
53
  /\bsdk\s*\??\.\s*host\s*\??\.\s*ready\s*\(/g,
@@ -16,6 +16,7 @@ const FRAME_SELECTOR = 'iframe[title="Game"]';
16
16
  exports.CAPTURE_LOG_LEVEL_VALUES = ['debug', 'info', 'warn', 'error'];
17
17
  exports.MAX_CAPTURE_TIMEOUT_SECONDS = 600;
18
18
  const GOOGLE_TELEMETRY_HOSTS = [
19
+ 'analytics.google.com',
19
20
  'googletagmanager.com',
20
21
  'google-analytics.com',
21
22
  'googleadservices.com',
@@ -97,7 +98,13 @@ function extractRefusedResourceUrls(text) {
97
98
  }
98
99
  function isKnownGoogleTelemetryConsoleIssue(text, locationUrl) {
99
100
  if (/Content Security Policy/i.test(text)) {
100
- return extractRefusedResourceUrls(text).some(isKnownGoogleTelemetryUrl);
101
+ const refusedUrls = extractRefusedResourceUrls(text);
102
+ if (refusedUrls.some(isKnownGoogleTelemetryUrl)) {
103
+ return true;
104
+ }
105
+ const messageUrls = extractHttpUrls(text);
106
+ return messageUrls.some(isKnownGoogleTelemetryUrl)
107
+ && !messageUrls.some(isLoopbackOrLocalUrl);
101
108
  }
102
109
  if (!/ERR_BLOCKED_BY_CLIENT/i.test(text)) {
103
110
  return false;
@@ -1,4 +1,4 @@
1
- import { type AppSurface, type AppType, type AppHostingMode, type AppAuthMode, type AppControllerMode, type AppVersionVisibility, type AppAchievementCatalogueDefinition, type AppLeaderboardCatalogueDefinition, type PlayerMetaDefinitionStatus, type CatalogueTagGroupDefinition, type AppMetadataAssetSpecSupport, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type ContentLicense } from '@playdrop/types';
1
+ import { type AppSurface, type AppType, type AppHostingMode, type AppAuthMode, type AppControllerMode, type AppVersionVisibility, type AppVersionDesign, type AppAchievementCatalogueDefinition, type AppLeaderboardCatalogueDefinition, type PlayerMetaDefinitionStatus, type CatalogueTagGroupDefinition, type AppMetadataAssetSpecSupport, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type ContentLicense } from '@playdrop/types';
2
2
  import { type AssetSpecSupportDeclaration } from './assetSpecs';
3
3
  export type CatalogueJson = {
4
4
  apps?: Array<Record<string, unknown>>;
@@ -41,6 +41,7 @@ export type AppCatalogueEntry = {
41
41
  remixSource?: string | null;
42
42
  remix?: string;
43
43
  surfaceTargets?: Record<string, unknown> | AppSurface[] | null;
44
+ design?: unknown;
44
45
  version?: string;
45
46
  releaseNotes?: string;
46
47
  visibility?: string;
@@ -177,6 +178,7 @@ export type AppTask = {
177
178
  emoji?: string | null;
178
179
  color?: string | null;
179
180
  surfaceTargets: AppSurface[];
181
+ design?: AppVersionDesign;
180
182
  missingMetadata: string[];
181
183
  ecsPath?: string;
182
184
  serverPath?: string;
package/dist/catalogue.js CHANGED
@@ -1300,6 +1300,17 @@ function buildAppTasks(rootDir, catalogues, options) {
1300
1300
  if (errors.length > errorCountBeforeDependencies) {
1301
1301
  continue;
1302
1302
  }
1303
+ let design;
1304
+ if (Object.prototype.hasOwnProperty.call(entry, 'design')) {
1305
+ const parsedDesign = (0, types_1.normalizeAppVersionDesign)(entry.design, {
1306
+ allowedPackRefs: usesPacks,
1307
+ });
1308
+ if (!parsedDesign.ok) {
1309
+ errors.push(`[${label}] App "${rawName}" ${parsedDesign.error}`);
1310
+ continue;
1311
+ }
1312
+ design = parsedDesign.value;
1313
+ }
1303
1314
  const errorCountBeforeTagMetadata = errors.length;
1304
1315
  const remix = normalizeCatalogueRemixRef(entry.remix, 'app', errors, `[${label}] App "${rawName}"`);
1305
1316
  const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] App "${rawName}"`);
@@ -1500,6 +1511,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1500
1511
  emoji: metadata.emoji === undefined ? undefined : metadata.emoji,
1501
1512
  color: metadata.color === undefined ? undefined : metadata.color,
1502
1513
  surfaceTargets: metadata.surfaceTargets,
1514
+ design,
1503
1515
  missingMetadata: metadata.missingFields,
1504
1516
  ecsPath,
1505
1517
  serverPath,
@@ -9,6 +9,7 @@ type CaptureRemoteOptions = {
9
9
  loginUrl?: string;
10
10
  actions?: string;
11
11
  viewport?: string;
12
+ surface?: string;
12
13
  fpsSampleMs?: string | number;
13
14
  };
14
15
  export declare function buildCaptureRemoteCommandArgs(url: string | undefined, options?: CaptureRemoteOptions): string[] | null;
@@ -10,6 +10,29 @@ const http_1 = require("../http");
10
10
  const messages_1 = require("../messages");
11
11
  const devShared_1 = require("./devShared");
12
12
  const captureRuntime_1 = require("../captureRuntime");
13
+ 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';
14
+ const SURFACE_CONTEXT_OPTIONS = {
15
+ desktop: {
16
+ viewport: { width: 1280, height: 720 },
17
+ deviceScaleFactor: 1,
18
+ isMobile: false,
19
+ hasTouch: false,
20
+ },
21
+ 'mobile-landscape': {
22
+ viewport: { width: 896, height: 414 },
23
+ deviceScaleFactor: 2,
24
+ isMobile: true,
25
+ hasTouch: true,
26
+ userAgent: MOBILE_USER_AGENT,
27
+ },
28
+ 'mobile-portrait': {
29
+ viewport: { width: 414, height: 896 },
30
+ deviceScaleFactor: 2,
31
+ isMobile: true,
32
+ hasTouch: true,
33
+ userAgent: MOBILE_USER_AGENT,
34
+ },
35
+ };
13
36
  function readUnsupportedPlaydropCaptureMessage(targetUrl) {
14
37
  let parsed;
15
38
  try {
@@ -69,6 +92,16 @@ function parseViewport(raw) {
69
92
  }
70
93
  return { width, height };
71
94
  }
95
+ function parseSurface(raw) {
96
+ const normalized = raw?.trim().toLowerCase();
97
+ if (!normalized) {
98
+ return null;
99
+ }
100
+ if (normalized === 'desktop' || normalized === 'mobile-landscape' || normalized === 'mobile-portrait') {
101
+ return normalized;
102
+ }
103
+ throw new Error('invalid_surface');
104
+ }
72
105
  function parseFpsSampleMs(raw) {
73
106
  if (raw === undefined) {
74
107
  return null;
@@ -172,6 +205,7 @@ function buildCaptureRemoteCommandArgs(url, options = {}) {
172
205
  pushOptionalArg(args, '--expected-url', options.expectedUrl);
173
206
  pushOptionalArg(args, '--actions', options.actions);
174
207
  pushOptionalArg(args, '--viewport', options.viewport);
208
+ pushOptionalArg(args, '--surface', options.surface);
175
209
  if (options.fpsSampleMs !== undefined) {
176
210
  args.push('--fps-sample-ms', String(parseFpsSampleMs(options.fpsSampleMs)));
177
211
  }
@@ -205,6 +239,7 @@ function parseOptions(url, options = {}) {
205
239
  logPath: options.log?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.log.trim()) : null,
206
240
  actionsPath: options.actions?.trim() ? (0, node_path_1.resolve)(process.cwd(), options.actions.trim()) : null,
207
241
  viewport: parseViewport(options.viewport),
242
+ surface: parseSurface(options.surface),
208
243
  fpsSampleMs: parseFpsSampleMs(options.fpsSampleMs),
209
244
  login: username
210
245
  ? {
@@ -247,6 +282,11 @@ async function captureRemote(url, options = {}) {
247
282
  process.exitCode = 1;
248
283
  return;
249
284
  }
285
+ if (error instanceof Error && error.message === 'invalid_surface') {
286
+ (0, messages_1.printErrorWithHelp)('The --surface value must be desktop, mobile-landscape, or mobile-portrait.', [], { command: 'project capture remote' });
287
+ process.exitCode = 1;
288
+ return;
289
+ }
250
290
  if (error instanceof Error && error.message === 'invalid_fps_sample_ms') {
251
291
  (0, messages_1.printErrorWithHelp)('The --fps-sample-ms value must be an integer between 100 and 60000 milliseconds.', [], { command: 'project capture remote' });
252
292
  process.exitCode = 1;
@@ -318,7 +358,11 @@ async function captureRemote(url, options = {}) {
318
358
  user: parsed.login ? undefined : currentUser,
319
359
  login: parsed.login,
320
360
  actions: actions ?? undefined,
321
- contextOptions: parsed.viewport ? { viewport: parsed.viewport } : undefined,
361
+ contextOptions: parsed.surface
362
+ ? { ...SURFACE_CONTEXT_OPTIONS[parsed.surface], ...(parsed.viewport ? { viewport: parsed.viewport } : {}) }
363
+ : parsed.viewport
364
+ ? { viewport: parsed.viewport }
365
+ : undefined,
322
366
  frameRateSampleMs: parsed.fpsSampleMs ?? undefined,
323
367
  });
324
368
  if (result.errorCount > 0) {