@playdrop/playdrop-cli 0.6.4 → 0.6.6

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 (78) hide show
  1. package/config/client-meta.json +7 -7
  2. package/dist/appUrls.d.ts +8 -0
  3. package/dist/appUrls.js +27 -0
  4. package/dist/apps/index.d.ts +11 -0
  5. package/dist/apps/index.js +45 -1
  6. package/dist/apps/launchCheck.d.ts +30 -0
  7. package/dist/apps/launchCheck.js +272 -0
  8. package/dist/apps/registration.d.ts +12 -0
  9. package/dist/apps/registration.js +49 -0
  10. package/dist/apps/upload.d.ts +5 -0
  11. package/dist/apps/upload.js +17 -0
  12. package/dist/apps/validate.js +41 -3
  13. package/dist/captureRuntime.d.ts +13 -0
  14. package/dist/captureRuntime.js +99 -21
  15. package/dist/commands/capture.js +52 -39
  16. package/dist/commands/create.js +17 -3
  17. package/dist/commands/devServer.js +10 -2
  18. package/dist/commands/upload.js +16 -5
  19. package/dist/commands/validate.js +58 -1
  20. package/node_modules/@playdrop/api-client/dist/client.d.ts +3 -1
  21. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +3 -1
  23. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  24. package/node_modules/@playdrop/api-client/dist/domains/apps.js +21 -0
  25. package/node_modules/@playdrop/config/client-meta.json +7 -7
  26. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  27. package/node_modules/@playdrop/types/dist/version.d.ts +37 -1
  28. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  29. package/node_modules/@playdrop/types/dist/version.js +1 -0
  30. package/package.json +1 -1
  31. package/node_modules/@playdrop/boxel-core/dist/test/entity-utils.test.d.ts +0 -1
  32. package/node_modules/@playdrop/boxel-core/dist/test/entity-utils.test.js +0 -92
  33. package/node_modules/@playdrop/boxel-core/dist/test/entity-utils.test.js.map +0 -1
  34. package/node_modules/@playdrop/boxel-core/dist/test/greedy-mesher.test.d.ts +0 -1
  35. package/node_modules/@playdrop/boxel-core/dist/test/greedy-mesher.test.js +0 -48
  36. package/node_modules/@playdrop/boxel-core/dist/test/greedy-mesher.test.js.map +0 -1
  37. package/node_modules/@playdrop/boxel-core/dist/test/humanoid/humanoid-builders.test.d.ts +0 -1
  38. package/node_modules/@playdrop/boxel-core/dist/test/humanoid/humanoid-builders.test.js +0 -270
  39. package/node_modules/@playdrop/boxel-core/dist/test/humanoid/humanoid-builders.test.js.map +0 -1
  40. package/node_modules/@playdrop/boxel-core/dist/test/index.test.d.ts +0 -1
  41. package/node_modules/@playdrop/boxel-core/dist/test/index.test.js +0 -48
  42. package/node_modules/@playdrop/boxel-core/dist/test/index.test.js.map +0 -1
  43. package/node_modules/@playdrop/boxel-core/dist/test/layer-mode.test.d.ts +0 -1
  44. package/node_modules/@playdrop/boxel-core/dist/test/layer-mode.test.js +0 -67
  45. package/node_modules/@playdrop/boxel-core/dist/test/layer-mode.test.js.map +0 -1
  46. package/node_modules/@playdrop/boxel-core/dist/test/materials.test.d.ts +0 -1
  47. package/node_modules/@playdrop/boxel-core/dist/test/materials.test.js +0 -55
  48. package/node_modules/@playdrop/boxel-core/dist/test/materials.test.js.map +0 -1
  49. package/node_modules/@playdrop/boxel-core/dist/test/palette-tools.test.d.ts +0 -1
  50. package/node_modules/@playdrop/boxel-core/dist/test/palette-tools.test.js +0 -124
  51. package/node_modules/@playdrop/boxel-core/dist/test/palette-tools.test.js.map +0 -1
  52. package/node_modules/@playdrop/boxel-core/dist/test/serialization.test.d.ts +0 -1
  53. package/node_modules/@playdrop/boxel-core/dist/test/serialization.test.js +0 -35
  54. package/node_modules/@playdrop/boxel-core/dist/test/serialization.test.js.map +0 -1
  55. package/node_modules/@playdrop/boxel-core/dist/test/textures.test.d.ts +0 -1
  56. package/node_modules/@playdrop/boxel-core/dist/test/textures.test.js +0 -120
  57. package/node_modules/@playdrop/boxel-core/dist/test/textures.test.js.map +0 -1
  58. package/node_modules/@playdrop/boxel-core/dist/test/types.test.d.ts +0 -1
  59. package/node_modules/@playdrop/boxel-core/dist/test/types.test.js +0 -32
  60. package/node_modules/@playdrop/boxel-core/dist/test/types.test.js.map +0 -1
  61. package/node_modules/@playdrop/boxel-core/dist/test/upscale.test.d.ts +0 -1
  62. package/node_modules/@playdrop/boxel-core/dist/test/upscale.test.js +0 -100
  63. package/node_modules/@playdrop/boxel-core/dist/test/upscale.test.js.map +0 -1
  64. package/node_modules/@playdrop/boxel-core/dist/test/validation.test.d.ts +0 -1
  65. package/node_modules/@playdrop/boxel-core/dist/test/validation.test.js +0 -61
  66. package/node_modules/@playdrop/boxel-core/dist/test/validation.test.js.map +0 -1
  67. package/node_modules/@playdrop/boxel-core/dist/test/voxels.test.d.ts +0 -1
  68. package/node_modules/@playdrop/boxel-core/dist/test/voxels.test.js +0 -51
  69. package/node_modules/@playdrop/boxel-core/dist/test/voxels.test.js.map +0 -1
  70. package/node_modules/@playdrop/config/dist/src/creator-docs.d.ts +0 -24
  71. package/node_modules/@playdrop/config/dist/src/creator-docs.d.ts.map +0 -1
  72. package/node_modules/@playdrop/config/dist/src/creator-docs.js +0 -253
  73. package/node_modules/@playdrop/config/dist/src/creator-faq.d.ts +0 -17
  74. package/node_modules/@playdrop/config/dist/src/creator-faq.d.ts.map +0 -1
  75. package/node_modules/@playdrop/config/dist/src/creator-faq.js +0 -141
  76. package/node_modules/@playdrop/config/dist/test/creator-docs.test.d.ts +0 -2
  77. package/node_modules/@playdrop/config/dist/test/creator-docs.test.d.ts.map +0 -1
  78. package/node_modules/@playdrop/config/dist/test/creator-docs.test.js +0 -36
@@ -50,6 +50,9 @@ const SDK_INIT_PATTERNS = [
50
50
  /\bplaydrop\s*\.\s*init\s*\(/g,
51
51
  /\bsdk\s*\.\s*initialize\s*\(/g,
52
52
  ];
53
+ const SDK_READY_PATTERNS = [
54
+ /\bsdk\s*\??\.\s*host\s*\??\.\s*ready\s*\(/g,
55
+ ];
53
56
  const LOCAL_HTML_SCRIPT_PATTERN = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
54
57
  const LOCAL_IMPORT_PATTERNS = [
55
58
  /(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?["']([^"']+)["']/g,
@@ -124,6 +127,33 @@ function scanForLegacySdkSymbols(task) {
124
127
  function stripSpecifierDecorators(specifier) {
125
128
  return specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
126
129
  }
130
+ function escapeRegexLiteral(value) {
131
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
132
+ }
133
+ function detectSdkInitInSource(source) {
134
+ if (SDK_INIT_PATTERNS.some((pattern) => {
135
+ pattern.lastIndex = 0;
136
+ return pattern.test(source);
137
+ })) {
138
+ return true;
139
+ }
140
+ const playdropAliases = new Set();
141
+ const aliasAssignmentPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*[^;\n]*\bplaydrop\b[^;\n]*/g;
142
+ let aliasMatch;
143
+ while ((aliasMatch = aliasAssignmentPattern.exec(source)) !== null) {
144
+ const alias = aliasMatch[1]?.trim();
145
+ if (alias) {
146
+ playdropAliases.add(alias);
147
+ }
148
+ }
149
+ for (const alias of playdropAliases) {
150
+ const aliasInitPattern = new RegExp(`\\b${escapeRegexLiteral(alias)}\\s*(?:\\.\\s*|\\?\\.\\s*)init\\s*\\(`, 'g');
151
+ if (aliasInitPattern.test(source)) {
152
+ return true;
153
+ }
154
+ }
155
+ return false;
156
+ }
127
157
  function safeReadFile(filePath) {
128
158
  try {
129
159
  return (0, node_fs_1.readFileSync)(filePath, 'utf8');
@@ -378,9 +408,10 @@ function collectSdkDetectionFiles(task, mode) {
378
408
  function detectSdkUsage(task, mode) {
379
409
  let hasSdkReference = false;
380
410
  let hasSdkInit = false;
411
+ let hasSdkReady = false;
381
412
  const files = collectSdkDetectionFiles(task, mode);
382
413
  for (const filePath of files) {
383
- if (hasSdkReference && hasSdkInit) {
414
+ if (hasSdkReference && hasSdkInit && hasSdkReady) {
384
415
  break;
385
416
  }
386
417
  const source = safeReadFile(filePath);
@@ -394,13 +425,16 @@ function detectSdkUsage(task, mode) {
394
425
  });
395
426
  }
396
427
  if (!hasSdkInit) {
397
- hasSdkInit = SDK_INIT_PATTERNS.some((pattern) => {
428
+ hasSdkInit = detectSdkInitInSource(source);
429
+ }
430
+ if (!hasSdkReady) {
431
+ hasSdkReady = SDK_READY_PATTERNS.some((pattern) => {
398
432
  pattern.lastIndex = 0;
399
433
  return pattern.test(source);
400
434
  });
401
435
  }
402
436
  }
403
- return { hasSdkReference, hasSdkInit };
437
+ return { hasSdkReference, hasSdkInit, hasSdkReady };
404
438
  }
405
439
  function collectOutdatedSdkVersionWarnings(task) {
406
440
  if (!task.packageJsonPath) {
@@ -451,6 +485,7 @@ function collectAppValidationWarnings(task, mode = 'source') {
451
485
  : primaryDetection;
452
486
  const hasSdkReference = primaryDetection.hasSdkReference || sourceDetection.hasSdkReference;
453
487
  const hasSdkInit = primaryDetection.hasSdkInit || sourceDetection.hasSdkInit;
488
+ const hasSdkReady = primaryDetection.hasSdkReady || sourceDetection.hasSdkReady;
454
489
  const warnings = [];
455
490
  if (!hasSdkReference) {
456
491
  warnings.push(`[apps][validate] Could not detect the Playdrop SDK loader or @playdrop/sdk import for ${task.name}. We could not find https://assets.playdrop.ai/sdk/playdrop.js or @playdrop/sdk in the app files, so it might not work once uploaded.`);
@@ -458,6 +493,9 @@ function collectAppValidationWarnings(task, mode = 'source') {
458
493
  if (!hasSdkInit) {
459
494
  warnings.push(`[apps][validate] Could not detect Playdrop SDK initialization for ${task.name}. We could not find playdrop.init() in the app files, so it might not work once uploaded.`);
460
495
  }
496
+ if (!hasSdkReady) {
497
+ warnings.push(`[apps][validate] Could not detect Playdrop host readiness for ${task.name}. We could not find sdk.host.ready() in the app files, so the app will fail to start correctly inside Playdrop.`);
498
+ }
461
499
  return [...warnings, ...collectOutdatedSdkVersionWarnings(task)];
462
500
  }
463
501
  async function runFormatScript(task) {
@@ -12,6 +12,7 @@ export type RunCaptureOptions = {
12
12
  targetUrl: string;
13
13
  expectedUrl?: string | null;
14
14
  timeoutMs: number;
15
+ settleAfterReadyMs?: number;
15
16
  minimumLogLevel: CaptureLogLevel;
16
17
  screenshotPath?: string | null;
17
18
  logPath?: string | null;
@@ -21,10 +22,22 @@ export type RunCaptureOptions = {
21
22
  login?: LoginOptions | null;
22
23
  savedSessionBootstrap?: boolean;
23
24
  enableCaptureBridge?: boolean;
25
+ requireHostedLaunchReady?: boolean;
26
+ expectedHostedLaunchState?: 'ready' | 'login_required';
27
+ };
28
+ export type CaptureHostedLaunchState = {
29
+ state: 'ready';
30
+ } | {
31
+ state: 'login_required';
32
+ } | {
33
+ state: 'error';
34
+ errorCode: string | null;
35
+ message: string | null;
24
36
  };
25
37
  export type CaptureRunResult = {
26
38
  errorCount: number;
27
39
  finalUrl: string;
40
+ hostedLaunchState: CaptureHostedLaunchState | null;
28
41
  };
29
42
  export declare function resolveCaptureLogLevel(value: string | undefined): CaptureLogLevel;
30
43
  export declare function validateCaptureTimeout(value: number | undefined): number;
@@ -9,7 +9,6 @@ const node_path_1 = require("node:path");
9
9
  const playwright_1 = require("./playwright");
10
10
  const sessionCookie_1 = require("./sessionCookie");
11
11
  const FRAME_SELECTOR = 'iframe[title="Game"]';
12
- const LOGIN_BUTTON_NAME = 'Sign in to play';
13
12
  exports.CAPTURE_LOG_LEVEL_VALUES = ['debug', 'info', 'warn', 'error'];
14
13
  exports.MAX_CAPTURE_TIMEOUT_SECONDS = 600;
15
14
  function formatConsoleValue(value) {
@@ -80,6 +79,44 @@ function normalizeComparableUrl(rawUrl) {
80
79
  const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
81
80
  return `${parsed.origin}${pathname}`;
82
81
  }
82
+ function normalizeHostedLaunchStatePayload(payload) {
83
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
84
+ return null;
85
+ }
86
+ const state = typeof payload.state === 'string'
87
+ ? payload.state.trim().toLowerCase()
88
+ : '';
89
+ if (state === 'ready') {
90
+ return { state: 'ready' };
91
+ }
92
+ if (state === 'login_required') {
93
+ return { state: 'login_required' };
94
+ }
95
+ if (state !== 'error') {
96
+ return null;
97
+ }
98
+ const errorCode = typeof payload.errorCode === 'string'
99
+ ? payload.errorCode.trim() || null
100
+ : null;
101
+ const message = typeof payload.message === 'string'
102
+ ? payload.message.trim() || null
103
+ : null;
104
+ return {
105
+ state: 'error',
106
+ errorCode,
107
+ message,
108
+ };
109
+ }
110
+ function formatHostedLaunchError(state) {
111
+ const parts = ['Hosted app failed to launch.'];
112
+ if (state.errorCode) {
113
+ parts.push(`errorCode=${state.errorCode}.`);
114
+ }
115
+ if (state.message) {
116
+ parts.push(state.message);
117
+ }
118
+ return parts.join(' ');
119
+ }
83
120
  async function writeLogFile(logPath, lines) {
84
121
  if (!logPath || logPath.trim().length === 0) {
85
122
  return;
@@ -113,11 +150,15 @@ async function runCapture(options) {
113
150
  const errors = [];
114
151
  const outputLines = [];
115
152
  const expectedUrl = options.expectedUrl ? normalizeComparableUrl(options.expectedUrl) : null;
153
+ const expectedHostedLaunchState = options.requireHostedLaunchReady
154
+ ? options.expectedHostedLaunchState ?? 'ready'
155
+ : null;
116
156
  const hasExplicitLogin = Boolean(options.login?.username);
117
157
  const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && !hasExplicitLogin;
118
158
  const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
119
159
  const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
120
160
  let finalUrl = options.targetUrl;
161
+ let hostedLaunchState = null;
121
162
  const record = (level, message) => {
122
163
  const line = `[capture][${level}] ${message}`;
123
164
  outputLines.push(line);
@@ -142,6 +183,21 @@ async function runCapture(options) {
142
183
  try {
143
184
  await (0, playwright_1.withChromiumPage)(async ({ context, page }) => {
144
185
  const targetOrigin = new URL(options.targetUrl).origin;
186
+ let hostedLaunchWaiterSettled = false;
187
+ let resolveHostedLaunchWaiter = null;
188
+ const hostedLaunchWaiter = options.requireHostedLaunchReady
189
+ ? new Promise((resolve) => {
190
+ resolveHostedLaunchWaiter = resolve;
191
+ })
192
+ : null;
193
+ const settleHostedLaunchWaiter = (state) => {
194
+ hostedLaunchState = state;
195
+ if (!hostedLaunchWaiter || hostedLaunchWaiterSettled) {
196
+ return;
197
+ }
198
+ hostedLaunchWaiterSettled = true;
199
+ resolveHostedLaunchWaiter?.(state);
200
+ };
145
201
  if (bootstrapToken) {
146
202
  await context.addCookies((0, sessionCookie_1.buildCaptureAccessTokenCookies)(options.targetUrl, bootstrapToken));
147
203
  }
@@ -173,6 +229,12 @@ async function runCapture(options) {
173
229
  }
174
230
  if (options.enableCaptureBridge) {
175
231
  await page.exposeBinding('__playdropCaptureLog', async (_source, type, payload) => {
232
+ if (type === 'hosted-launch-state') {
233
+ const normalizedHostedState = normalizeHostedLaunchStatePayload(payload);
234
+ if (normalizedHostedState) {
235
+ settleHostedLaunchWaiter(normalizedHostedState);
236
+ }
237
+ }
176
238
  const normalized = typeof type === 'string' ? type.toLowerCase() : 'info';
177
239
  const level = normalized === 'error' || normalized === 'fatal'
178
240
  ? 'error'
@@ -216,7 +278,11 @@ async function runCapture(options) {
216
278
  });
217
279
  const bridgeWindow = window;
218
280
  bridgeWindow.playdrop = bridgeWindow.playdrop || {};
281
+ bridgeWindow.__playdropCaptureHostedLaunchState = null;
219
282
  const bridge = (type, payload) => {
283
+ if (type === 'hosted-launch-state') {
284
+ bridgeWindow.__playdropCaptureHostedLaunchState = payload;
285
+ }
220
286
  try {
221
287
  const binding = bridgeWindow.__playdropCaptureLog;
222
288
  if (typeof binding === 'function') {
@@ -342,22 +408,20 @@ async function runCapture(options) {
342
408
  recordError(`[navigation] ${response.status()} ${response.statusText()} ${options.targetUrl}`);
343
409
  }
344
410
  const readinessTimeoutMs = Math.min(Math.max(options.timeoutMs, 1000), 30000);
345
- const readinessHandle = await page.waitForFunction(({ frameSelector, loginButtonName }) => {
411
+ const readinessHandle = await page.waitForFunction(({ frameSelector, waitForHostedLaunchState }) => {
346
412
  const frame = document.querySelector(frameSelector);
347
413
  if (frame) {
348
414
  return 'frame';
349
415
  }
350
- const buttons = Array.from(document.querySelectorAll('button'));
351
- const loginButton = buttons.find((button) => button.textContent?.trim() === loginButtonName);
352
- if (loginButton) {
353
- return 'login';
416
+ if (waitForHostedLaunchState) {
417
+ const state = window.__playdropCaptureHostedLaunchState;
418
+ if (state && typeof state.state === 'string' && state.state.trim().length > 0) {
419
+ return `hosted:${state.state.trim().toLowerCase()}`;
420
+ }
354
421
  }
355
422
  return null;
356
- }, { frameSelector: FRAME_SELECTOR, loginButtonName: LOGIN_BUTTON_NAME }, { timeout: readinessTimeoutMs });
357
- const readiness = await readinessHandle.jsonValue();
358
- if (readiness !== 'frame') {
359
- throw new Error('The Playdrop app frame did not load. The page is still showing the login wall.');
360
- }
423
+ }, { frameSelector: FRAME_SELECTOR, waitForHostedLaunchState: Boolean(expectedHostedLaunchState) }, { timeout: readinessTimeoutMs });
424
+ await readinessHandle.jsonValue();
361
425
  const assertPageState = async () => {
362
426
  const currentUrl = page.url();
363
427
  if (!currentUrl) {
@@ -371,22 +435,35 @@ async function runCapture(options) {
371
435
  if (finalPathname.startsWith('/login')) {
372
436
  throw new Error(`Capture landed on the login page (${currentUrl}).`);
373
437
  }
374
- const loginVisible = await page
375
- .getByRole('button', { name: LOGIN_BUTTON_NAME })
376
- .first()
377
- .isVisible()
378
- .catch(() => false);
379
- if (loginVisible) {
380
- throw new Error('The Playdrop login wall is still visible.');
381
- }
382
438
  const frameCount = await page.locator(FRAME_SELECTOR).count();
383
- if (frameCount === 0) {
439
+ if ((expectedHostedLaunchState ?? 'ready') === 'ready' && frameCount === 0) {
384
440
  throw new Error('The Playdrop app frame never appeared.');
385
441
  }
442
+ if (expectedHostedLaunchState === 'login_required' && frameCount > 0) {
443
+ throw new Error('The Playdrop auth gate booted the app frame unexpectedly.');
444
+ }
386
445
  return currentUrl;
387
446
  };
447
+ if (hostedLaunchWaiter) {
448
+ const launchState = await Promise.race([
449
+ hostedLaunchWaiter,
450
+ page.waitForTimeout(options.timeoutMs).then(() => {
451
+ throw new Error(expectedHostedLaunchState === 'login_required'
452
+ ? 'Hosted app did not stop at the PlayDrop sign-in gate before the launch-check timeout.'
453
+ : 'Hosted app did not reach the ready state before the launch-check timeout.');
454
+ }),
455
+ ]);
456
+ if (launchState.state === 'error') {
457
+ throw new Error(formatHostedLaunchError(launchState));
458
+ }
459
+ if (expectedHostedLaunchState && launchState.state !== expectedHostedLaunchState) {
460
+ throw new Error(expectedHostedLaunchState === 'login_required'
461
+ ? 'Hosted app did not stop at the PlayDrop sign-in gate.'
462
+ : `Hosted app reported ${launchState.state} instead of ready.`);
463
+ }
464
+ }
388
465
  finalUrl = await assertPageState();
389
- await page.waitForTimeout(options.timeoutMs);
466
+ await page.waitForTimeout(options.settleAfterReadyMs ?? options.timeoutMs);
390
467
  finalUrl = await assertPageState();
391
468
  if (options.screenshotPath) {
392
469
  const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
@@ -404,5 +481,6 @@ async function runCapture(options) {
404
481
  return {
405
482
  errorCount: errors.length,
406
483
  finalUrl,
484
+ hostedLaunchState,
407
485
  };
408
486
  }
@@ -283,42 +283,6 @@ async function capture(targetArg, options = {}) {
283
283
  process.exitCode = 1;
284
284
  return;
285
285
  }
286
- let registeredApp = null;
287
- try {
288
- registeredApp = await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
289
- }
290
- catch (error) {
291
- if (error instanceof http_1.CLIUnsupportedClientError) {
292
- return;
293
- }
294
- if (error instanceof types_1.UnsupportedClientError) {
295
- (0, http_1.handleUnsupportedError)(error, 'Capture');
296
- process.exitCode = 1;
297
- return;
298
- }
299
- if (error instanceof types_1.ApiError) {
300
- if (error.status === 404) {
301
- (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [
302
- `Run "playdrop project create app ${appName}" to register the app before running capture.`,
303
- 'If you expected it to exist, ensure you are logged into the correct environment.',
304
- ], { command: 'project capture' });
305
- }
306
- else {
307
- (0, messages_1.printErrorWithHelp)(`Failed to verify app registration (status ${error.status}).`, [
308
- 'Retry in a moment.',
309
- 'If the issue persists, contact the Playdrop team.',
310
- ], { command: 'project capture' });
311
- }
312
- process.exitCode = 1;
313
- return;
314
- }
315
- if ((0, devShared_1.isNetworkError)(error)) {
316
- (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to verify the app registration.', 'project capture');
317
- process.exitCode = 1;
318
- return;
319
- }
320
- throw error;
321
- }
322
286
  const entryLabel = (0, node_path_1.relative)(process.cwd(), filePath) || filePath;
323
287
  console.log(`[capture] Preparing ${entryLabel} for ${env} (${appTypeSlug}).`);
324
288
  const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
@@ -442,14 +406,61 @@ async function capture(targetArg, options = {}) {
442
406
  await new Promise(resolve => setTimeout(resolve, 1000));
443
407
  }
444
408
  const webBase = envConfig.webBase ?? 'https://www.playdrop.ai';
445
- const frameUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(`${webBase}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/dev`, devOptions.selection);
446
- console.log(`[capture] Launching Playwright against ${frameUrl}`);
447
409
  const shouldResetBefore = devOptions.selection.devAuth === 'player'
448
410
  && (devOptions.resetMode === 'before' || devOptions.resetMode === 'before-and-after');
449
411
  const shouldResetAfter = devOptions.selection.devAuth === 'player'
450
412
  && (devOptions.resetMode === 'after' || devOptions.resetMode === 'before-and-after');
413
+ const requiresRegisteredApp = devOptions.selection.devAuth !== 'anonymous' || shouldResetBefore || shouldResetAfter;
414
+ let registeredApp = null;
415
+ if (requiresRegisteredApp) {
416
+ try {
417
+ registeredApp = await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
418
+ }
419
+ catch (error) {
420
+ if (error instanceof http_1.CLIUnsupportedClientError) {
421
+ return;
422
+ }
423
+ if (error instanceof types_1.UnsupportedClientError) {
424
+ (0, http_1.handleUnsupportedError)(error, 'Capture');
425
+ process.exitCode = 1;
426
+ return;
427
+ }
428
+ if (error instanceof types_1.ApiError) {
429
+ if (error.status === 404) {
430
+ (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [
431
+ `Run "playdrop project create app ${appName}" to register the app before using viewer or player capture.`,
432
+ 'Use anonymous capture for local launch validation before upload.',
433
+ ], { command: 'project capture' });
434
+ }
435
+ else {
436
+ (0, messages_1.printErrorWithHelp)(`Failed to verify app registration (status ${error.status}).`, [
437
+ 'Retry in a moment.',
438
+ 'If the issue persists, contact the Playdrop team.',
439
+ ], { command: 'project capture' });
440
+ }
441
+ process.exitCode = 1;
442
+ return;
443
+ }
444
+ if ((0, devShared_1.isNetworkError)(error)) {
445
+ (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to verify the app registration.', 'project capture');
446
+ process.exitCode = 1;
447
+ return;
448
+ }
449
+ throw error;
450
+ }
451
+ }
452
+ const captureBaseUrl = (0, appUrls_1.buildPlatformDevUrl)(webBase, {
453
+ creatorUsername: currentUsername,
454
+ appName,
455
+ appType: appTypeSlug,
456
+ devAuth: devOptions.selection.devAuth,
457
+ player: devOptions.selection.player ? String(devOptions.selection.player) : null,
458
+ launchCheck: devOptions.selection.devAuth === 'anonymous',
459
+ });
460
+ const frameUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(captureBaseUrl, devOptions.selection);
461
+ console.log(`[capture] Launching Playwright against ${frameUrl}`);
451
462
  const selectedPlayerSlot = devOptions.selection.player;
452
- const registeredAppId = typeof registeredApp.id === 'number' ? registeredApp.id : null;
463
+ const registeredAppId = typeof registeredApp?.id === 'number' ? registeredApp.id : null;
453
464
  if ((shouldResetBefore || shouldResetAfter) && registeredAppId === null) {
454
465
  (0, messages_1.printErrorWithHelp)('The registered app is missing a numeric id, so test-player reset is unavailable.', [], { command: 'project capture' });
455
466
  process.exitCode = 1;
@@ -463,6 +474,7 @@ async function capture(targetArg, options = {}) {
463
474
  targetUrl: frameUrl,
464
475
  expectedUrl: frameUrl,
465
476
  timeoutMs,
477
+ settleAfterReadyMs: timeoutMs,
466
478
  minimumLogLevel,
467
479
  screenshotPath,
468
480
  contextOptions: surfaceContextOptions,
@@ -472,6 +484,7 @@ async function capture(targetArg, options = {}) {
472
484
  } : undefined,
473
485
  savedSessionBootstrap: false,
474
486
  enableCaptureBridge: true,
487
+ requireHostedLaunchReady: true,
475
488
  });
476
489
  if (result.errorCount > 0) {
477
490
  console.error(`[capture] Completed with ${result.errorCount} error(s) after ${timeoutSeconds} seconds.`);
@@ -19,11 +19,25 @@ const init_1 = require("./init");
19
19
  const app_1 = require("@playdrop/types/app");
20
20
  const catalogue_utils_1 = require("../catalogue-utils");
21
21
  const catalogue_1 = require("../catalogue");
22
+ const clientInfo_1 = require("../clientInfo");
22
23
  const CATALOGUE_FILENAME = 'catalogue.json';
23
24
  const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
24
25
  const ALLOWED_CATALOGUE_TOP_LEVEL_KEYS = new Set(['apps', 'assetSpecs', 'assets', 'assetPacks']);
25
- async function downloadArchive(url) {
26
- const response = await fetch(url);
26
+ function buildArchiveDownloadHeaders(url, apiBase, token) {
27
+ const targetUrl = new URL(url, apiBase);
28
+ const apiOrigin = new URL(apiBase).origin;
29
+ if (targetUrl.origin !== apiOrigin) {
30
+ return undefined;
31
+ }
32
+ const headers = new Headers((0, clientInfo_1.createClientHeaders)());
33
+ if (token.trim().length > 0) {
34
+ headers.set('authorization', `Bearer ${token}`);
35
+ }
36
+ return headers;
37
+ }
38
+ async function downloadArchive(url, apiBase, token) {
39
+ const headers = buildArchiveDownloadHeaders(url, apiBase, token);
40
+ const response = await fetch(url, headers ? { headers } : undefined);
27
41
  if (!response.ok) {
28
42
  throw new Error(`Failed to download project archive (${response.status})`);
29
43
  }
@@ -1179,7 +1193,7 @@ async function create(name, options = {}) {
1179
1193
  try {
1180
1194
  const archiveBuffer = remixSourceTarget
1181
1195
  ? new Uint8Array(await (await client.downloadAppSource(remixSourceTarget.creator, remixSourceTarget.name, remixSourceTarget.version)).blob.arrayBuffer())
1182
- : await downloadArchive(resolveArchiveDownloadUrl(scaffold.archiveUrl, ctx.envConfig.apiBase));
1196
+ : await downloadArchive(resolveArchiveDownloadUrl(scaffold.archiveUrl, ctx.envConfig.apiBase), ctx.envConfig.apiBase, ctx.token);
1183
1197
  const extraction = extractProjectArchive(archiveBuffer, projectDir);
1184
1198
  const extractedCatalogue = (0, node_path_1.resolve)(projectDir, CATALOGUE_FILENAME);
1185
1199
  projectCataloguePath = (0, node_fs_1.existsSync)(extractedCatalogue) ? extractedCatalogue : null;
@@ -396,15 +396,23 @@ async function startDevServer(options) {
396
396
  };
397
397
  }
398
398
  async function isDevServerAvailable(input, timeoutMs = 1000) {
399
+ const port = input.port ?? exports.DEV_ROUTER_PORT;
400
+ const health = await fetchRouterHealth(port, Math.min(timeoutMs, 400));
401
+ if (!health || !isCompatibleRouterHealth(health)) {
402
+ return false;
403
+ }
399
404
  const controller = new AbortController();
400
405
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
401
406
  timeout.unref?.();
402
407
  try {
403
- await fetch(buildLocalDevAppUrl(input), {
408
+ const response = await fetch(buildLocalDevAppUrl({
409
+ ...input,
410
+ port,
411
+ }), {
404
412
  method: 'GET',
405
413
  signal: controller.signal,
406
414
  });
407
- return true;
415
+ return response.ok;
408
416
  }
409
417
  catch {
410
418
  return false;
@@ -150,18 +150,19 @@ async function fetchCurrentUserInfo(client, apiBase) {
150
150
  return {
151
151
  username: data?.user?.username ?? null,
152
152
  role: data?.user?.role ?? null,
153
+ user: data?.user ?? null,
153
154
  };
154
155
  }
155
156
  catch (error) {
156
157
  if (error instanceof types_1.UnsupportedClientError) {
157
158
  (0, http_1.handleUnsupportedError)(error, 'Upload');
158
- return { username: null, role: null };
159
+ return { username: null, role: null, user: null };
159
160
  }
160
161
  if (error instanceof http_1.CLIUnsupportedClientError) {
161
162
  throw error;
162
163
  }
163
164
  if (error instanceof types_1.ApiError) {
164
- return { username: null, role: null };
165
+ return { username: null, role: null, user: null };
165
166
  }
166
167
  if (isConnectionRefusedError(error)) {
167
168
  throw new Error(buildApiUnavailableMessage(apiBase));
@@ -282,6 +283,13 @@ async function uploadAppTask(state, task, taskCreator, options) {
282
283
  skipReview: options?.skipReview,
283
284
  clearTags: options?.clearTags,
284
285
  creatorUsername: taskCreator,
286
+ apiBase: state.apiBase,
287
+ webBase: state.portalBase,
288
+ token: state.token,
289
+ user: state.currentUser,
290
+ runLocalLaunchCheck: true,
291
+ runStagedUploadLaunchCheck: true,
292
+ ensureRegisteredAppShell: true,
285
293
  });
286
294
  if (!upload.versionCreated || !upload.version) {
287
295
  throw new Error(`App "${task.name}" upload did not return a created version.`);
@@ -476,7 +484,7 @@ async function flushGraphState(client, graphState, results) {
476
484
  process.exitCode = process.exitCode || 1;
477
485
  }
478
486
  }
479
- async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, warnings, webBase, options) {
487
+ async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, currentUser, token, warnings, apiBase, webBase, options) {
480
488
  const portalBase = normalizePortalBase(webBase);
481
489
  const defaultCreator = normalizeCreatorUsername(ownerUsername) ?? owner;
482
490
  const results = [];
@@ -494,6 +502,9 @@ async function processUploadTasks(client, tasks, owner, ownerUsername, currentUs
494
502
  sortedTasks,
495
503
  defaultCreator,
496
504
  currentUserRole,
505
+ currentUser,
506
+ token,
507
+ apiBase,
497
508
  portalBase,
498
509
  uploadedAppsByName: new Map(),
499
510
  uploadedAssetsByKey: new Map(),
@@ -539,7 +550,7 @@ async function upload(pathOrName, options) {
539
550
  return;
540
551
  }
541
552
  const { client, env, envConfig } = ctx;
542
- let userInfo = { username: null, role: null };
553
+ let userInfo = { username: null, role: null, user: null };
543
554
  try {
544
555
  userInfo = await fetchCurrentUserInfo(client, envConfig.apiBase);
545
556
  }
@@ -589,7 +600,7 @@ async function upload(pathOrName, options) {
589
600
  taxonomyEntries.forEach((entry) => pushLoggedEntry(results, entry));
590
601
  }
591
602
  if (tasks.length > 0) {
592
- const uploadEntries = await processUploadTasks(client, tasks, owner, userInfo.username, userInfo.role, warnings, envConfig.webBase, options);
603
+ const uploadEntries = await processUploadTasks(client, tasks, owner, userInfo.username, userInfo.role, userInfo.user, ctx.token, warnings, envConfig.apiBase, envConfig.webBase, options);
593
604
  results.push(...uploadEntries);
594
605
  }
595
606
  (0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'upload', environment: env });
@@ -79,6 +79,37 @@ async function loadAuthenticatedValidationContext(workspacePath) {
79
79
  return null;
80
80
  }
81
81
  }
82
+ async function loadHostedLaunchValidationContext(workspacePath) {
83
+ const ctx = await (0, commandContext_1.resolveAuthenticatedEnvironmentContext)('project validate', 'run hosted app launch validation', { workspacePath });
84
+ if (!ctx) {
85
+ return null;
86
+ }
87
+ const username = typeof ctx.account?.username === 'string' ? ctx.account.username.trim() : '';
88
+ if (!username) {
89
+ process.exitCode = 1;
90
+ return null;
91
+ }
92
+ let currentUserRole = null;
93
+ let currentUser = null;
94
+ try {
95
+ const me = await ctx.client.me();
96
+ currentUserRole = me.user.role ?? null;
97
+ currentUser = me.user ?? null;
98
+ }
99
+ catch {
100
+ currentUserRole = null;
101
+ currentUser = null;
102
+ }
103
+ return {
104
+ client: ctx.client,
105
+ currentUserRole,
106
+ currentUser,
107
+ username,
108
+ apiBase: ctx.envConfig.apiBase,
109
+ webBase: ctx.envConfig.webBase ?? null,
110
+ token: ctx.token,
111
+ };
112
+ }
82
113
  function resolveValidationWorkspacePath(pathOrName, resolution) {
83
114
  return resolution === 'name' ? process.cwd() : pathOrName;
84
115
  }
@@ -147,6 +178,7 @@ async function collectLiveTagClearWarnings(tasks, workspacePath) {
147
178
  }
148
179
  async function validate(pathOrName) {
149
180
  const selection = (0, taskSelection_1.selectTasks)(pathOrName);
181
+ const workspacePath = resolveValidationWorkspacePath(pathOrName, selection.resolution);
150
182
  if (selection.errors.length > 0) {
151
183
  (0, taskSelection_1.reportTaskErrors)(selection.errors, 'project validate');
152
184
  process.exitCode = 1;
@@ -172,12 +204,38 @@ async function validate(pathOrName) {
172
204
  const assetSpecTasks = tasks.filter((task) => task.kind === 'asset-spec');
173
205
  const assetSpecLookups = buildLocalAssetSpecLookups(assetSpecTasks);
174
206
  const sortedTasks = (0, taskUtils_1.sortTasks)(tasks);
207
+ const hostedLaunchValidationRequired = sortedTasks.some((task) => task.kind === 'app' && task.hostingMode !== 'EXTERNAL' && !task.externalUrl);
208
+ const hostedLaunchValidationContext = hostedLaunchValidationRequired
209
+ ? await loadHostedLaunchValidationContext(workspacePath)
210
+ : null;
211
+ if (hostedLaunchValidationRequired && !hostedLaunchValidationContext) {
212
+ return;
213
+ }
175
214
  for (const task of sortedTasks) {
176
215
  const entityId = task.name;
177
216
  try {
178
217
  if (task.kind === 'app') {
179
218
  await (0, apps_1.validateAppTask)(task);
180
219
  (0, apps_1.collectAppValidationWarnings)(task, 'source').forEach((warning) => warnings.add(warning));
220
+ if (task.hostingMode !== 'EXTERNAL' && !task.externalUrl) {
221
+ const launchContext = hostedLaunchValidationContext;
222
+ const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, launchContext.username, launchContext.currentUserRole);
223
+ if (creatorResult.creatorTargetError) {
224
+ throw new Error(creatorResult.creatorTargetError);
225
+ }
226
+ const launchCheck = await (0, apps_1.runLocalHostedLaunchCheck)({
227
+ client: launchContext.client,
228
+ apiBase: launchContext.apiBase,
229
+ webBase: launchContext.webBase,
230
+ creatorUsername: creatorResult.taskCreator,
231
+ task,
232
+ token: launchContext.token,
233
+ currentUser: launchContext.currentUser,
234
+ });
235
+ if (launchCheck.status !== 'PASSED') {
236
+ throw new Error((0, apps_1.formatHostedLaunchCheckFailure)(task.name, launchCheck, 'local'));
237
+ }
238
+ }
181
239
  }
182
240
  else if (task.kind === 'asset-spec') {
183
241
  // Parsing already validates the contract, symbol, and documentation files.
@@ -215,7 +273,6 @@ async function validate(pathOrName) {
215
273
  console.log((0, uploadLog_1.formatTaskLogLine)(entry));
216
274
  }
217
275
  }
218
- const workspacePath = resolveValidationWorkspacePath(pathOrName, selection.resolution);
219
276
  const liveTagWarnings = await collectLiveTagClearWarnings(sortedTasks, workspacePath);
220
277
  liveTagWarnings.forEach((warning) => warnings.add(warning));
221
278
  (0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'validate' });