@playdrop/playdrop-cli 0.3.16 → 0.4.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 (58) hide show
  1. package/config/client-meta.json +7 -7
  2. package/dist/appUrls.d.ts +9 -0
  3. package/dist/appUrls.js +33 -0
  4. package/dist/browser.d.ts +5 -0
  5. package/dist/browser.js +45 -0
  6. package/dist/captureRuntime.d.ts +31 -0
  7. package/dist/captureRuntime.js +414 -0
  8. package/dist/commands/browse.js +1 -1
  9. package/dist/commands/capture.js +182 -479
  10. package/dist/commands/captureRemote.d.ts +2 -1
  11. package/dist/commands/captureRemote.js +144 -47
  12. package/dist/commands/comments.js +3 -3
  13. package/dist/commands/create.js +6 -6
  14. package/dist/commands/createRemixContent.js +1 -1
  15. package/dist/commands/creations.js +2 -2
  16. package/dist/commands/credits.js +2 -2
  17. package/dist/commands/detail.js +9 -4
  18. package/dist/commands/dev.js +2 -2
  19. package/dist/commands/feedback.js +26 -11
  20. package/dist/commands/generation.js +2 -2
  21. package/dist/commands/gettingStarted.js +1 -1
  22. package/dist/commands/init.js +1 -1
  23. package/dist/commands/login.d.ts +2 -4
  24. package/dist/commands/login.js +16 -51
  25. package/dist/commands/logout.js +1 -1
  26. package/dist/commands/notifications.js +3 -3
  27. package/dist/commands/play.d.ts +5 -0
  28. package/dist/commands/play.js +102 -0
  29. package/dist/commands/upload.js +10 -11
  30. package/dist/commands/versionsBrowse.js +11 -3
  31. package/dist/commands/whoami.js +3 -3
  32. package/dist/index.js +15 -3
  33. package/dist/messages.js +5 -5
  34. package/dist/playwright.d.ts +2 -1
  35. package/dist/playwright.js +18 -1
  36. package/dist/uploadLog.d.ts +2 -0
  37. package/dist/uploadLog.js +14 -0
  38. package/node_modules/@playdrop/ai-client/dist/index.d.ts.map +1 -1
  39. package/node_modules/@playdrop/ai-client/dist/index.js +136 -3
  40. package/node_modules/@playdrop/api-client/dist/client.d.ts +9 -1
  41. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  42. package/node_modules/@playdrop/api-client/dist/client.js +10 -1
  43. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  44. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  45. package/node_modules/@playdrop/api-client/dist/domains/auth.js +21 -0
  46. package/node_modules/@playdrop/api-client/dist/domains/free-credits.d.ts +27 -0
  47. package/node_modules/@playdrop/api-client/dist/domains/free-credits.d.ts.map +1 -0
  48. package/node_modules/@playdrop/api-client/dist/domains/free-credits.js +66 -0
  49. package/node_modules/@playdrop/api-client/dist/index.d.ts +10 -2
  50. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/index.js +31 -2
  52. package/node_modules/@playdrop/config/client-meta.json +7 -7
  53. package/node_modules/@playdrop/types/dist/api.d.ts +85 -1
  54. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  55. package/node_modules/@playdrop/types/dist/api.js +15 -0
  56. package/node_modules/@playdrop/types/dist/asset.d.ts +1 -0
  57. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  58. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.3.16",
3
- "build": 1,
2
+ "version": "0.4.1",
3
+ "build": 3,
4
4
  "platforms": {
5
5
  "ios": {
6
6
  "minimumVersion": "16.0"
@@ -26,19 +26,19 @@
26
26
  },
27
27
  "clients": {
28
28
  "web": {
29
- "minimumVersion": "0.3.16",
30
- "minimumBuild": 1
29
+ "minimumVersion": "0.4.1",
30
+ "minimumBuild": 3
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.3.16",
34
- "minimumBuild": 1
33
+ "minimumVersion": "0.4.1",
34
+ "minimumBuild": 3
35
35
  },
36
36
  "apple": {
37
37
  "minimumVersion": "0.3.10",
38
38
  "minimumBuild": 1
39
39
  },
40
40
  "cli": {
41
- "minimumVersion": "0.3.16"
41
+ "minimumVersion": "0.4.1"
42
42
  }
43
43
  }
44
44
  }
@@ -0,0 +1,9 @@
1
+ import type { AppType } from '@playdrop/types';
2
+ export type AppUrlInput = {
3
+ creatorUsername: string;
4
+ appName: string;
5
+ appType?: AppType | string | null;
6
+ };
7
+ export declare function normalizePlaydropWebBase(webBase?: string | null): string;
8
+ export declare function getAppTypeSlug(type: AppType | string | null | undefined): string;
9
+ export declare function buildPlatformPlayUrl(webBase: string | null | undefined, input: AppUrlInput): string;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePlaydropWebBase = normalizePlaydropWebBase;
4
+ exports.getAppTypeSlug = getAppTypeSlug;
5
+ exports.buildPlatformPlayUrl = buildPlatformPlayUrl;
6
+ const DEFAULT_WEB_BASE = 'https://www.playdrop.ai';
7
+ function normalizePlaydropWebBase(webBase) {
8
+ const base = typeof webBase === 'string' && webBase.trim().length > 0 ? webBase.trim() : DEFAULT_WEB_BASE;
9
+ return base.replace(/\/$/, '');
10
+ }
11
+ function getAppTypeSlug(type) {
12
+ if (typeof type !== 'string') {
13
+ return 'game';
14
+ }
15
+ switch (type) {
16
+ case 'DEMO':
17
+ return 'demo';
18
+ case 'TOOL':
19
+ return 'tool';
20
+ case 'TEMPLATE':
21
+ return 'template';
22
+ case 'GAME':
23
+ default:
24
+ return 'game';
25
+ }
26
+ }
27
+ function buildPlatformPlayUrl(webBase, input) {
28
+ const base = normalizePlaydropWebBase(webBase);
29
+ const creator = encodeURIComponent(input.creatorUsername);
30
+ const typeSlug = getAppTypeSlug(input.appType);
31
+ const appName = encodeURIComponent(input.appName);
32
+ return `${base}/creators/${creator}/apps/${typeSlug}/${appName}/play`;
33
+ }
@@ -0,0 +1,5 @@
1
+ type OpenBrowserFn = (url: string) => Promise<boolean>;
2
+ export declare function openBrowserUrl(url: string): Promise<boolean>;
3
+ export declare function setOpenBrowserForTests(fn: OpenBrowserFn): void;
4
+ export declare function resetOpenBrowserForTests(): void;
5
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openBrowserUrl = openBrowserUrl;
4
+ exports.setOpenBrowserForTests = setOpenBrowserForTests;
5
+ exports.resetOpenBrowserForTests = resetOpenBrowserForTests;
6
+ const node_child_process_1 = require("node:child_process");
7
+ function getBrowserCommand(url) {
8
+ if (process.platform === 'darwin') {
9
+ return { command: 'open', args: [url] };
10
+ }
11
+ if (process.platform === 'win32') {
12
+ return { command: 'cmd', args: ['/c', 'start', '', url] };
13
+ }
14
+ if (process.platform === 'linux') {
15
+ return { command: 'xdg-open', args: [url] };
16
+ }
17
+ return null;
18
+ }
19
+ async function defaultOpenBrowser(url) {
20
+ const command = getBrowserCommand(url);
21
+ if (!command) {
22
+ return false;
23
+ }
24
+ return await new Promise((resolve) => {
25
+ const child = (0, node_child_process_1.spawn)(command.command, command.args, {
26
+ detached: true,
27
+ stdio: 'ignore',
28
+ });
29
+ child.once('error', () => resolve(false));
30
+ child.once('spawn', () => {
31
+ child.unref();
32
+ resolve(true);
33
+ });
34
+ });
35
+ }
36
+ let openBrowser = defaultOpenBrowser;
37
+ async function openBrowserUrl(url) {
38
+ return await openBrowser(url);
39
+ }
40
+ function setOpenBrowserForTests(fn) {
41
+ openBrowser = fn;
42
+ }
43
+ function resetOpenBrowserForTests() {
44
+ openBrowser = defaultOpenBrowser;
45
+ }
@@ -0,0 +1,31 @@
1
+ import type { UserResponse } from '@playdrop/types';
2
+ import type { BrowserContextOptions } from 'playwright-core';
3
+ export type CaptureLogLevel = 'debug' | 'info' | 'warn' | 'error';
4
+ export declare const CAPTURE_LOG_LEVEL_VALUES: CaptureLogLevel[];
5
+ export declare const MAX_CAPTURE_TIMEOUT_SECONDS = 600;
6
+ type LoginOptions = {
7
+ username: string;
8
+ password: string;
9
+ loginUrl?: string | null;
10
+ };
11
+ export type RunCaptureOptions = {
12
+ targetUrl: string;
13
+ expectedUrl?: string | null;
14
+ timeoutMs: number;
15
+ minimumLogLevel: CaptureLogLevel;
16
+ screenshotPath?: string | null;
17
+ logPath?: string | null;
18
+ contextOptions?: BrowserContextOptions;
19
+ token?: string | null;
20
+ user?: UserResponse | null;
21
+ login?: LoginOptions | null;
22
+ enableCaptureBridge?: boolean;
23
+ };
24
+ export type CaptureRunResult = {
25
+ errorCount: number;
26
+ finalUrl: string;
27
+ };
28
+ export declare function resolveCaptureLogLevel(value: string | undefined): CaptureLogLevel;
29
+ export declare function validateCaptureTimeout(value: number | undefined): number;
30
+ export declare function runCapture(options: RunCaptureOptions): Promise<CaptureRunResult>;
31
+ export {};
@@ -0,0 +1,414 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_CAPTURE_TIMEOUT_SECONDS = exports.CAPTURE_LOG_LEVEL_VALUES = void 0;
4
+ exports.resolveCaptureLogLevel = resolveCaptureLogLevel;
5
+ exports.validateCaptureTimeout = validateCaptureTimeout;
6
+ exports.runCapture = runCapture;
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = require("node:path");
9
+ const playwright_1 = require("./playwright");
10
+ const ACCESS_TOKEN_COOKIE_NAME = 'playdrop_access_token';
11
+ const FRAME_SELECTOR = 'iframe[title="Game"]';
12
+ const LOGIN_BUTTON_NAME = 'Sign in to play';
13
+ exports.CAPTURE_LOG_LEVEL_VALUES = ['debug', 'info', 'warn', 'error'];
14
+ exports.MAX_CAPTURE_TIMEOUT_SECONDS = 600;
15
+ function formatConsoleValue(value) {
16
+ if (typeof value === 'string')
17
+ return value;
18
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
19
+ return String(value);
20
+ }
21
+ if (typeof value === 'undefined') {
22
+ return 'undefined';
23
+ }
24
+ if (typeof value === 'function') {
25
+ return '[function]';
26
+ }
27
+ if (typeof value === 'object') {
28
+ try {
29
+ return JSON.stringify(value);
30
+ }
31
+ catch {
32
+ return '[object]';
33
+ }
34
+ }
35
+ return String(value);
36
+ }
37
+ function serializePayload(payload) {
38
+ if (payload === undefined) {
39
+ return '';
40
+ }
41
+ if (typeof payload === 'string') {
42
+ return payload;
43
+ }
44
+ try {
45
+ return JSON.stringify(payload);
46
+ }
47
+ catch {
48
+ return String(payload);
49
+ }
50
+ }
51
+ function severityOrder(level) {
52
+ switch (level) {
53
+ case 'debug':
54
+ return 0;
55
+ case 'info':
56
+ return 1;
57
+ case 'warn':
58
+ return 2;
59
+ case 'error':
60
+ return 3;
61
+ default:
62
+ return 1;
63
+ }
64
+ }
65
+ function mapConsoleTypeToLevel(type) {
66
+ const normalized = type.toLowerCase();
67
+ if (normalized === 'debug')
68
+ return 'debug';
69
+ if (normalized === 'warning' || normalized === 'warn')
70
+ return 'warn';
71
+ if (normalized === 'error' || normalized === 'assert' || normalized === 'trace')
72
+ return 'error';
73
+ return 'info';
74
+ }
75
+ function shouldEmit(level, threshold) {
76
+ return severityOrder(level) >= severityOrder(threshold);
77
+ }
78
+ function normalizeComparableUrl(rawUrl) {
79
+ const parsed = new URL(rawUrl);
80
+ const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
81
+ return `${parsed.origin}${pathname}`;
82
+ }
83
+ async function writeLogFile(logPath, lines) {
84
+ if (!logPath || logPath.trim().length === 0) {
85
+ return;
86
+ }
87
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(logPath), { recursive: true });
88
+ await (0, promises_1.writeFile)(logPath, `${lines.join('\n')}\n`, 'utf8');
89
+ console.log(`[capture] Saved logs to ${(0, node_path_1.relative)(process.cwd(), logPath) || logPath}`);
90
+ }
91
+ function resolveCaptureLogLevel(value) {
92
+ if (!value)
93
+ return 'info';
94
+ const normalized = value.trim().toLowerCase();
95
+ if (exports.CAPTURE_LOG_LEVEL_VALUES.includes(normalized)) {
96
+ return normalized;
97
+ }
98
+ throw new Error(`Unsupported log level "${value}". Choose one of: ${exports.CAPTURE_LOG_LEVEL_VALUES.join(', ')}`);
99
+ }
100
+ function validateCaptureTimeout(value) {
101
+ if (value === undefined || Number.isNaN(value)) {
102
+ return 5;
103
+ }
104
+ if (!Number.isFinite(value) || value <= 0) {
105
+ throw new Error('Timeout must be a positive number of seconds.');
106
+ }
107
+ if (value > exports.MAX_CAPTURE_TIMEOUT_SECONDS) {
108
+ throw new Error(`Timeout cannot exceed ${exports.MAX_CAPTURE_TIMEOUT_SECONDS} seconds (10 minutes).`);
109
+ }
110
+ return value;
111
+ }
112
+ async function runCapture(options) {
113
+ const errors = [];
114
+ const outputLines = [];
115
+ const expectedUrl = options.expectedUrl ? normalizeComparableUrl(options.expectedUrl) : null;
116
+ const hasExplicitLogin = Boolean(options.login?.username);
117
+ const bootstrapToken = hasExplicitLogin ? null : options.token?.trim() || null;
118
+ const bootstrapUser = hasExplicitLogin ? null : options.user ?? null;
119
+ let finalUrl = options.targetUrl;
120
+ const record = (level, message) => {
121
+ const line = `[capture][${level}] ${message}`;
122
+ outputLines.push(line);
123
+ if (level === 'error') {
124
+ console.error(line);
125
+ return;
126
+ }
127
+ if (level === 'warn') {
128
+ console.warn(line);
129
+ return;
130
+ }
131
+ if (level === 'debug') {
132
+ console.debug(line);
133
+ return;
134
+ }
135
+ console.log(line);
136
+ };
137
+ const recordError = (message) => {
138
+ errors.push(message);
139
+ record('error', message);
140
+ };
141
+ try {
142
+ await (0, playwright_1.withChromiumPage)(async ({ context, page }) => {
143
+ const targetOrigin = new URL(options.targetUrl).origin;
144
+ if (bootstrapToken) {
145
+ await context.addCookies([
146
+ {
147
+ name: ACCESS_TOKEN_COOKIE_NAME,
148
+ value: bootstrapToken,
149
+ url: targetOrigin,
150
+ path: '/',
151
+ },
152
+ ]);
153
+ }
154
+ if (bootstrapToken || bootstrapUser) {
155
+ await page.addInitScript(({ token, user }) => {
156
+ try {
157
+ if (token) {
158
+ window.localStorage.setItem('playdrop.accessToken', token);
159
+ }
160
+ }
161
+ catch {
162
+ // ignore storage errors
163
+ }
164
+ try {
165
+ if (user) {
166
+ window.localStorage.setItem('playdrop.user', JSON.stringify(user));
167
+ }
168
+ }
169
+ catch {
170
+ // ignore storage errors
171
+ }
172
+ try {
173
+ window.sessionStorage.removeItem('playdrop.logoutReason');
174
+ }
175
+ catch {
176
+ // ignore storage errors
177
+ }
178
+ }, { token: bootstrapToken, user: bootstrapUser });
179
+ }
180
+ if (options.enableCaptureBridge) {
181
+ await page.exposeBinding('__playdropCaptureLog', async (_source, type, payload) => {
182
+ const normalized = typeof type === 'string' ? type.toLowerCase() : 'info';
183
+ const level = normalized === 'error' || normalized === 'fatal'
184
+ ? 'error'
185
+ : normalized === 'warn' || normalized === 'warning'
186
+ ? 'warn'
187
+ : normalized === 'debug'
188
+ ? 'debug'
189
+ : 'info';
190
+ const serialized = serializePayload(payload);
191
+ const line = ['[capture][custom]', normalized, serialized].filter(Boolean).join(' ');
192
+ if (level === 'error') {
193
+ errors.push(line);
194
+ if (shouldEmit(level, options.minimumLogLevel)) {
195
+ console.error(line);
196
+ }
197
+ outputLines.push(line);
198
+ return;
199
+ }
200
+ outputLines.push(line);
201
+ if (!shouldEmit(level, options.minimumLogLevel)) {
202
+ return;
203
+ }
204
+ if (level === 'warn') {
205
+ console.warn(line);
206
+ }
207
+ else if (level === 'debug') {
208
+ console.debug(line);
209
+ }
210
+ else {
211
+ console.log(line);
212
+ }
213
+ });
214
+ await page.addInitScript(() => {
215
+ window.addEventListener('unhandledrejection', event => {
216
+ try {
217
+ console.error('[capture][unhandledrejection]', event.reason);
218
+ }
219
+ catch {
220
+ console.error('[capture][unhandledrejection]');
221
+ }
222
+ });
223
+ const bridgeWindow = window;
224
+ bridgeWindow.playdrop = bridgeWindow.playdrop || {};
225
+ const bridge = (type, payload) => {
226
+ try {
227
+ const binding = bridgeWindow.__playdropCaptureLog;
228
+ if (typeof binding === 'function') {
229
+ binding(type, payload);
230
+ }
231
+ }
232
+ catch (error) {
233
+ console.error('[capture][bridge-error]', error);
234
+ }
235
+ };
236
+ Object.defineProperty(bridgeWindow.playdrop, 'capture', {
237
+ value: bridge,
238
+ configurable: true,
239
+ writable: true,
240
+ });
241
+ });
242
+ }
243
+ const handleConsoleMessage = async (message) => {
244
+ const type = message.type();
245
+ const level = mapConsoleTypeToLevel(type);
246
+ const args = await Promise.all(message.args().map(arg => arg.jsonValue().catch(() => arg.toString())));
247
+ const rendered = args.length > 0 ? args.map(formatConsoleValue).join(' ') : message.text();
248
+ const line = `[capture][console:${type}] ${rendered}`;
249
+ outputLines.push(line);
250
+ let effectiveLevel = level;
251
+ if (level === 'warn' && line.includes('GL Driver Message')) {
252
+ effectiveLevel = 'debug';
253
+ }
254
+ if (type === 'error' || type === 'assert' || type === 'trace') {
255
+ errors.push(line);
256
+ if (shouldEmit(effectiveLevel, options.minimumLogLevel)) {
257
+ console.error(line);
258
+ }
259
+ return;
260
+ }
261
+ if (!shouldEmit(effectiveLevel, options.minimumLogLevel)) {
262
+ return;
263
+ }
264
+ if (effectiveLevel === 'warn') {
265
+ console.warn(line);
266
+ }
267
+ else if (effectiveLevel === 'debug') {
268
+ console.debug(line);
269
+ }
270
+ else if (effectiveLevel === 'info') {
271
+ console.info(line);
272
+ }
273
+ else {
274
+ console.log(line);
275
+ }
276
+ };
277
+ page.on('console', message => {
278
+ void handleConsoleMessage(message);
279
+ });
280
+ page.on('pageerror', error => {
281
+ const text = error?.message ?? String(error);
282
+ errors.push(text);
283
+ outputLines.push(`[capture][pageerror] ${text}`);
284
+ if (shouldEmit('error', options.minimumLogLevel)) {
285
+ console.error(`[capture][pageerror] ${text}`);
286
+ }
287
+ });
288
+ page.on('requestfailed', request => {
289
+ const failure = request.failure();
290
+ const errorText = failure?.errorText ?? '';
291
+ const text = `${request.method()} ${request.url()} - ${errorText || 'failed'}`;
292
+ if (/ERR_ABORTED/i.test(errorText) || /ERR_HTTP2_PROTOCOL_ERROR/i.test(errorText)) {
293
+ const line = `[capture][requestcancelled] ${text}`;
294
+ outputLines.push(line);
295
+ if (shouldEmit('debug', options.minimumLogLevel)) {
296
+ console.debug(line);
297
+ }
298
+ return;
299
+ }
300
+ errors.push(text);
301
+ outputLines.push(`[capture][requestfailed] ${text}`);
302
+ if (shouldEmit('warn', options.minimumLogLevel)) {
303
+ console.error(`[capture][requestfailed] ${text}`);
304
+ }
305
+ });
306
+ page.on('response', response => {
307
+ const status = response.status();
308
+ if (status < 400) {
309
+ return;
310
+ }
311
+ const text = `${status} ${response.statusText()} ${response.url()}`;
312
+ if (status === 404 && /\\?playdrop_channel=/.test(response.url())) {
313
+ const line = `[capture][response-reload] ${text}`;
314
+ outputLines.push(line);
315
+ if (shouldEmit('debug', options.minimumLogLevel)) {
316
+ console.debug(line);
317
+ }
318
+ return;
319
+ }
320
+ errors.push(text);
321
+ outputLines.push(`[capture][response] ${text}`);
322
+ if (shouldEmit('warn', options.minimumLogLevel)) {
323
+ console.error(`[capture][response] ${text}`);
324
+ }
325
+ });
326
+ if (options.login?.username) {
327
+ const loginUrl = options.login.loginUrl?.trim() || `${targetOrigin}/login`;
328
+ record('info', `Opening login page ${loginUrl}`);
329
+ await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
330
+ await page.waitForLoadState('networkidle', { timeout: 30000 });
331
+ await page.fill('input[name="username"]', options.login.username);
332
+ await page.fill('input[name="password"]', options.login.password);
333
+ await page.getByRole('button', { name: 'Login' }).click();
334
+ await page.waitForURL((url) => {
335
+ try {
336
+ return !new URL(url.toString()).pathname.startsWith('/login');
337
+ }
338
+ catch {
339
+ return false;
340
+ }
341
+ }, { timeout: 30000 });
342
+ await page.waitForLoadState('networkidle', { timeout: 30000 });
343
+ record('info', 'Login succeeded');
344
+ }
345
+ record('info', `Opening ${options.targetUrl}`);
346
+ const response = await page.goto(options.targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
347
+ if (response && !response.ok()) {
348
+ recordError(`[navigation] ${response.status()} ${response.statusText()} ${options.targetUrl}`);
349
+ }
350
+ const readinessTimeoutMs = Math.min(Math.max(options.timeoutMs, 1000), 30000);
351
+ const readinessHandle = await page.waitForFunction(({ frameSelector, loginButtonName }) => {
352
+ const frame = document.querySelector(frameSelector);
353
+ if (frame) {
354
+ return 'frame';
355
+ }
356
+ const buttons = Array.from(document.querySelectorAll('button'));
357
+ const loginButton = buttons.find((button) => button.textContent?.trim() === loginButtonName);
358
+ if (loginButton) {
359
+ return 'login';
360
+ }
361
+ return null;
362
+ }, { frameSelector: FRAME_SELECTOR, loginButtonName: LOGIN_BUTTON_NAME }, { timeout: readinessTimeoutMs });
363
+ const readiness = await readinessHandle.jsonValue();
364
+ if (readiness !== 'frame') {
365
+ throw new Error('The Playdrop app frame did not load. The page is still showing the login wall.');
366
+ }
367
+ const assertPageState = async () => {
368
+ const currentUrl = page.url();
369
+ if (!currentUrl) {
370
+ throw new Error('Capture page did not finish loading a URL.');
371
+ }
372
+ const finalComparableUrl = normalizeComparableUrl(currentUrl);
373
+ if (expectedUrl && finalComparableUrl !== expectedUrl) {
374
+ throw new Error(`Capture landed on ${finalComparableUrl} instead of ${expectedUrl}.`);
375
+ }
376
+ const finalPathname = new URL(currentUrl).pathname;
377
+ if (finalPathname.startsWith('/login')) {
378
+ throw new Error(`Capture landed on the login page (${currentUrl}).`);
379
+ }
380
+ const loginVisible = await page
381
+ .getByRole('button', { name: LOGIN_BUTTON_NAME })
382
+ .first()
383
+ .isVisible()
384
+ .catch(() => false);
385
+ if (loginVisible) {
386
+ throw new Error('The Playdrop login wall is still visible.');
387
+ }
388
+ const frameCount = await page.locator(FRAME_SELECTOR).count();
389
+ if (frameCount === 0) {
390
+ throw new Error('The Playdrop app frame never appeared.');
391
+ }
392
+ return currentUrl;
393
+ };
394
+ finalUrl = await assertPageState();
395
+ await page.waitForTimeout(options.timeoutMs);
396
+ finalUrl = await assertPageState();
397
+ if (options.screenshotPath) {
398
+ const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
399
+ if (targetDir && targetDir !== '.' && targetDir !== '/') {
400
+ await (0, promises_1.mkdir)(targetDir, { recursive: true });
401
+ }
402
+ await page.screenshot({ path: options.screenshotPath, fullPage: true });
403
+ console.log(`[capture] Saved screenshot to ${(0, node_path_1.relative)(process.cwd(), options.screenshotPath) || options.screenshotPath}`);
404
+ }
405
+ }, options.contextOptions ?? {});
406
+ }
407
+ finally {
408
+ await writeLogFile(options.logPath, outputLines);
409
+ }
410
+ return {
411
+ errorCount: errors.length,
412
+ finalUrl,
413
+ };
414
+ }
@@ -365,7 +365,7 @@ async function browse(options = {}) {
365
365
  if (apiError.status === 401 || apiError.status === 403) {
366
366
  return {
367
367
  problem: 'Browsing this content requires you to be logged in.',
368
- suggestions: ['Run "playdrop login" and retry.'],
368
+ suggestions: ['Run "playdrop auth login" and retry.'],
369
369
  };
370
370
  }
371
371
  return {