@playdrop/playdrop-cli 0.6.5 → 0.6.7
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.
- package/config/client-meta.json +4 -4
- package/dist/appUrls.d.ts +8 -0
- package/dist/appUrls.js +27 -0
- package/dist/apps/index.d.ts +11 -0
- package/dist/apps/index.js +45 -1
- package/dist/apps/launchCheck.d.ts +30 -0
- package/dist/apps/launchCheck.js +292 -0
- package/dist/apps/registration.d.ts +12 -0
- package/dist/apps/registration.js +49 -0
- package/dist/apps/upload.d.ts +5 -0
- package/dist/apps/upload.js +17 -0
- package/dist/apps/validate.js +41 -3
- package/dist/captureRuntime.d.ts +18 -0
- package/dist/captureRuntime.js +137 -21
- package/dist/clientInfo.js +33 -4
- package/dist/commands/capture.js +52 -39
- package/dist/commands/devServer.js +10 -2
- package/dist/commands/upload.js +16 -5
- package/dist/commands/validate.js +58 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +21 -0
- package/node_modules/@playdrop/config/client-meta.json +4 -4
- package/node_modules/@playdrop/types/dist/version.d.ts +37 -1
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.js +1 -0
- package/package.json +1 -1
package/dist/apps/validate.js
CHANGED
|
@@ -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 =
|
|
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) {
|
package/dist/captureRuntime.d.ts
CHANGED
|
@@ -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,27 @@ export type RunCaptureOptions = {
|
|
|
21
22
|
login?: LoginOptions | null;
|
|
22
23
|
savedSessionBootstrap?: boolean;
|
|
23
24
|
enableCaptureBridge?: boolean;
|
|
25
|
+
requireHostedLaunchReady?: boolean;
|
|
26
|
+
expectedHostedLaunchState?: HostedLaunchExpectedState;
|
|
27
|
+
};
|
|
28
|
+
export type HostedLaunchExpectedState = 'ready' | 'login_required' | 'controller_required' | 'surface_unsupported';
|
|
29
|
+
export type CaptureHostedLaunchState = {
|
|
30
|
+
state: 'ready';
|
|
31
|
+
} | {
|
|
32
|
+
state: 'login_required';
|
|
33
|
+
} | {
|
|
34
|
+
state: 'controller_required';
|
|
35
|
+
} | {
|
|
36
|
+
state: 'surface_unsupported';
|
|
37
|
+
} | {
|
|
38
|
+
state: 'error';
|
|
39
|
+
errorCode: string | null;
|
|
40
|
+
message: string | null;
|
|
24
41
|
};
|
|
25
42
|
export type CaptureRunResult = {
|
|
26
43
|
errorCount: number;
|
|
27
44
|
finalUrl: string;
|
|
45
|
+
hostedLaunchState: CaptureHostedLaunchState | null;
|
|
28
46
|
};
|
|
29
47
|
export declare function resolveCaptureLogLevel(value: string | undefined): CaptureLogLevel;
|
|
30
48
|
export declare function validateCaptureTimeout(value: number | undefined): number;
|
package/dist/captureRuntime.js
CHANGED
|
@@ -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,68 @@ 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 === 'controller_required') {
|
|
96
|
+
return { state: 'controller_required' };
|
|
97
|
+
}
|
|
98
|
+
if (state === 'surface_unsupported') {
|
|
99
|
+
return { state: 'surface_unsupported' };
|
|
100
|
+
}
|
|
101
|
+
if (state !== 'error') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const errorCode = typeof payload.errorCode === 'string'
|
|
105
|
+
? payload.errorCode.trim() || null
|
|
106
|
+
: null;
|
|
107
|
+
const message = typeof payload.message === 'string'
|
|
108
|
+
? payload.message.trim() || null
|
|
109
|
+
: null;
|
|
110
|
+
return {
|
|
111
|
+
state: 'error',
|
|
112
|
+
errorCode,
|
|
113
|
+
message,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function formatHostedLaunchError(state) {
|
|
117
|
+
const parts = ['Hosted app failed to launch.'];
|
|
118
|
+
if (state.errorCode) {
|
|
119
|
+
parts.push(`errorCode=${state.errorCode}.`);
|
|
120
|
+
}
|
|
121
|
+
if (state.message) {
|
|
122
|
+
parts.push(state.message);
|
|
123
|
+
}
|
|
124
|
+
return parts.join(' ');
|
|
125
|
+
}
|
|
126
|
+
function formatHostedLaunchStateMismatch(actualState, expectedState) {
|
|
127
|
+
if (expectedState === "login_required") {
|
|
128
|
+
return `Hosted app reported ${actualState} instead of login_required.`;
|
|
129
|
+
}
|
|
130
|
+
if (expectedState === "controller_required") {
|
|
131
|
+
return `Hosted app reported ${actualState} instead of controller_required.`;
|
|
132
|
+
}
|
|
133
|
+
if (expectedState === "surface_unsupported") {
|
|
134
|
+
return `Hosted app reported ${actualState} instead of surface_unsupported.`;
|
|
135
|
+
}
|
|
136
|
+
return `Hosted app reported ${actualState} instead of ready.`;
|
|
137
|
+
}
|
|
138
|
+
function shouldSettleHostedLaunchWaiter(state, expectedState) {
|
|
139
|
+
if (state.state === "error" || !expectedState) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return state.state === expectedState;
|
|
143
|
+
}
|
|
83
144
|
async function writeLogFile(logPath, lines) {
|
|
84
145
|
if (!logPath || logPath.trim().length === 0) {
|
|
85
146
|
return;
|
|
@@ -113,11 +174,15 @@ async function runCapture(options) {
|
|
|
113
174
|
const errors = [];
|
|
114
175
|
const outputLines = [];
|
|
115
176
|
const expectedUrl = options.expectedUrl ? normalizeComparableUrl(options.expectedUrl) : null;
|
|
177
|
+
const expectedHostedLaunchState = options.requireHostedLaunchReady
|
|
178
|
+
? options.expectedHostedLaunchState ?? 'ready'
|
|
179
|
+
: null;
|
|
116
180
|
const hasExplicitLogin = Boolean(options.login?.username);
|
|
117
181
|
const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && !hasExplicitLogin;
|
|
118
182
|
const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
|
|
119
183
|
const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
|
|
120
184
|
let finalUrl = options.targetUrl;
|
|
185
|
+
let hostedLaunchState = null;
|
|
121
186
|
const record = (level, message) => {
|
|
122
187
|
const line = `[capture][${level}] ${message}`;
|
|
123
188
|
outputLines.push(line);
|
|
@@ -142,6 +207,24 @@ async function runCapture(options) {
|
|
|
142
207
|
try {
|
|
143
208
|
await (0, playwright_1.withChromiumPage)(async ({ context, page }) => {
|
|
144
209
|
const targetOrigin = new URL(options.targetUrl).origin;
|
|
210
|
+
let hostedLaunchWaiterSettled = false;
|
|
211
|
+
let resolveHostedLaunchWaiter = null;
|
|
212
|
+
const hostedLaunchWaiter = options.requireHostedLaunchReady
|
|
213
|
+
? new Promise((resolve) => {
|
|
214
|
+
resolveHostedLaunchWaiter = resolve;
|
|
215
|
+
})
|
|
216
|
+
: null;
|
|
217
|
+
const settleHostedLaunchWaiter = (state) => {
|
|
218
|
+
hostedLaunchState = state;
|
|
219
|
+
if (!hostedLaunchWaiter || hostedLaunchWaiterSettled) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!shouldSettleHostedLaunchWaiter(state, expectedHostedLaunchState)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
hostedLaunchWaiterSettled = true;
|
|
226
|
+
resolveHostedLaunchWaiter?.(state);
|
|
227
|
+
};
|
|
145
228
|
if (bootstrapToken) {
|
|
146
229
|
await context.addCookies((0, sessionCookie_1.buildCaptureAccessTokenCookies)(options.targetUrl, bootstrapToken));
|
|
147
230
|
}
|
|
@@ -173,6 +256,12 @@ async function runCapture(options) {
|
|
|
173
256
|
}
|
|
174
257
|
if (options.enableCaptureBridge) {
|
|
175
258
|
await page.exposeBinding('__playdropCaptureLog', async (_source, type, payload) => {
|
|
259
|
+
if (type === 'hosted-launch-state') {
|
|
260
|
+
const normalizedHostedState = normalizeHostedLaunchStatePayload(payload);
|
|
261
|
+
if (normalizedHostedState) {
|
|
262
|
+
settleHostedLaunchWaiter(normalizedHostedState);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
176
265
|
const normalized = typeof type === 'string' ? type.toLowerCase() : 'info';
|
|
177
266
|
const level = normalized === 'error' || normalized === 'fatal'
|
|
178
267
|
? 'error'
|
|
@@ -216,7 +305,11 @@ async function runCapture(options) {
|
|
|
216
305
|
});
|
|
217
306
|
const bridgeWindow = window;
|
|
218
307
|
bridgeWindow.playdrop = bridgeWindow.playdrop || {};
|
|
308
|
+
bridgeWindow.__playdropCaptureHostedLaunchState = null;
|
|
219
309
|
const bridge = (type, payload) => {
|
|
310
|
+
if (type === 'hosted-launch-state') {
|
|
311
|
+
bridgeWindow.__playdropCaptureHostedLaunchState = payload;
|
|
312
|
+
}
|
|
220
313
|
try {
|
|
221
314
|
const binding = bridgeWindow.__playdropCaptureLog;
|
|
222
315
|
if (typeof binding === 'function') {
|
|
@@ -342,22 +435,20 @@ async function runCapture(options) {
|
|
|
342
435
|
recordError(`[navigation] ${response.status()} ${response.statusText()} ${options.targetUrl}`);
|
|
343
436
|
}
|
|
344
437
|
const readinessTimeoutMs = Math.min(Math.max(options.timeoutMs, 1000), 30000);
|
|
345
|
-
const readinessHandle = await page.waitForFunction(({ frameSelector,
|
|
438
|
+
const readinessHandle = await page.waitForFunction(({ frameSelector, waitForHostedLaunchState }) => {
|
|
346
439
|
const frame = document.querySelector(frameSelector);
|
|
347
440
|
if (frame) {
|
|
348
441
|
return 'frame';
|
|
349
442
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
443
|
+
if (waitForHostedLaunchState) {
|
|
444
|
+
const state = window.__playdropCaptureHostedLaunchState;
|
|
445
|
+
if (state && typeof state.state === 'string' && state.state.trim().length > 0) {
|
|
446
|
+
return `hosted:${state.state.trim().toLowerCase()}`;
|
|
447
|
+
}
|
|
354
448
|
}
|
|
355
449
|
return null;
|
|
356
|
-
}, { frameSelector: FRAME_SELECTOR,
|
|
357
|
-
|
|
358
|
-
if (readiness !== 'frame') {
|
|
359
|
-
throw new Error('The Playdrop app frame did not load. The page is still showing the login wall.');
|
|
360
|
-
}
|
|
450
|
+
}, { frameSelector: FRAME_SELECTOR, waitForHostedLaunchState: Boolean(expectedHostedLaunchState) }, { timeout: readinessTimeoutMs });
|
|
451
|
+
await readinessHandle.jsonValue();
|
|
361
452
|
const assertPageState = async () => {
|
|
362
453
|
const currentUrl = page.url();
|
|
363
454
|
if (!currentUrl) {
|
|
@@ -371,22 +462,46 @@ async function runCapture(options) {
|
|
|
371
462
|
if (finalPathname.startsWith('/login')) {
|
|
372
463
|
throw new Error(`Capture landed on the login page (${currentUrl}).`);
|
|
373
464
|
}
|
|
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
465
|
const frameCount = await page.locator(FRAME_SELECTOR).count();
|
|
383
|
-
if (frameCount === 0) {
|
|
466
|
+
if ((expectedHostedLaunchState ?? 'ready') === 'ready' && frameCount === 0) {
|
|
384
467
|
throw new Error('The Playdrop app frame never appeared.');
|
|
385
468
|
}
|
|
469
|
+
if (expectedHostedLaunchState === 'login_required' && frameCount > 0) {
|
|
470
|
+
throw new Error('The Playdrop auth gate booted the app frame unexpectedly.');
|
|
471
|
+
}
|
|
472
|
+
if (expectedHostedLaunchState === 'controller_required' && frameCount > 0) {
|
|
473
|
+
throw new Error('The Playdrop controller gate booted the app frame unexpectedly.');
|
|
474
|
+
}
|
|
475
|
+
if (expectedHostedLaunchState === 'surface_unsupported' && frameCount > 0) {
|
|
476
|
+
throw new Error('The Playdrop surface gate booted the app frame unexpectedly.');
|
|
477
|
+
}
|
|
386
478
|
return currentUrl;
|
|
387
479
|
};
|
|
480
|
+
if (hostedLaunchWaiter) {
|
|
481
|
+
const launchState = await Promise.race([
|
|
482
|
+
hostedLaunchWaiter,
|
|
483
|
+
page.waitForTimeout(options.timeoutMs).then(() => {
|
|
484
|
+
if (hostedLaunchState && expectedHostedLaunchState && hostedLaunchState.state !== expectedHostedLaunchState) {
|
|
485
|
+
throw new Error(formatHostedLaunchStateMismatch(hostedLaunchState.state, expectedHostedLaunchState));
|
|
486
|
+
}
|
|
487
|
+
throw new Error(expectedHostedLaunchState === 'login_required'
|
|
488
|
+
? 'Hosted app did not stop at the PlayDrop sign-in gate before the launch-check timeout.'
|
|
489
|
+
: expectedHostedLaunchState === 'controller_required'
|
|
490
|
+
? 'Hosted app did not stop at the PlayDrop controller gate before the launch-check timeout.'
|
|
491
|
+
: expectedHostedLaunchState === 'surface_unsupported'
|
|
492
|
+
? 'Hosted app did not stop at the PlayDrop unsupported-surface gate before the launch-check timeout.'
|
|
493
|
+
: 'Hosted app did not reach the ready state before the launch-check timeout.');
|
|
494
|
+
}),
|
|
495
|
+
]);
|
|
496
|
+
if (launchState.state === 'error') {
|
|
497
|
+
throw new Error(formatHostedLaunchError(launchState));
|
|
498
|
+
}
|
|
499
|
+
if (expectedHostedLaunchState && launchState.state !== expectedHostedLaunchState) {
|
|
500
|
+
throw new Error(formatHostedLaunchStateMismatch(launchState.state, expectedHostedLaunchState));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
388
503
|
finalUrl = await assertPageState();
|
|
389
|
-
await page.waitForTimeout(options.timeoutMs);
|
|
504
|
+
await page.waitForTimeout(options.settleAfterReadyMs ?? options.timeoutMs);
|
|
390
505
|
finalUrl = await assertPageState();
|
|
391
506
|
if (options.screenshotPath) {
|
|
392
507
|
const targetDir = (0, node_path_1.dirname)(options.screenshotPath);
|
|
@@ -404,5 +519,6 @@ async function runCapture(options) {
|
|
|
404
519
|
return {
|
|
405
520
|
errorCount: errors.length,
|
|
406
521
|
finalUrl,
|
|
522
|
+
hostedLaunchState,
|
|
407
523
|
};
|
|
408
524
|
}
|
package/dist/clientInfo.js
CHANGED
|
@@ -13,15 +13,44 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
13
|
const fs_1 = require("fs");
|
|
14
14
|
const config_1 = require("@playdrop/config");
|
|
15
15
|
let cachedCliPackageVersion = null;
|
|
16
|
+
function assertValidCliVersion(version) {
|
|
17
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
18
|
+
throw new Error(`invalid_cli_package_version:${version}`);
|
|
19
|
+
}
|
|
20
|
+
return version;
|
|
21
|
+
}
|
|
22
|
+
function readVersionFromJson(pathname, field) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(pathname, 'utf8'));
|
|
25
|
+
const value = typeof parsed[field] === 'string' ? parsed[field].trim() : '';
|
|
26
|
+
if (!value) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return assertValidCliVersion(value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
16
35
|
function readCliPackageVersion() {
|
|
17
36
|
if (cachedCliPackageVersion) {
|
|
18
37
|
return cachedCliPackageVersion;
|
|
19
38
|
}
|
|
39
|
+
const candidateMetaPaths = [
|
|
40
|
+
path_1.default.resolve(__dirname, '..', 'config', 'client-meta.json'),
|
|
41
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'config', 'client-meta.json'),
|
|
42
|
+
];
|
|
43
|
+
for (const candidatePath of candidateMetaPaths) {
|
|
44
|
+
const clientMetaVersion = readVersionFromJson(candidatePath, 'version');
|
|
45
|
+
if (clientMetaVersion) {
|
|
46
|
+
cachedCliPackageVersion = clientMetaVersion;
|
|
47
|
+
return clientMetaVersion;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
20
50
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error(`invalid_cli_package_version:${version}`);
|
|
51
|
+
const version = readVersionFromJson(packageJsonPath, 'version');
|
|
52
|
+
if (!version) {
|
|
53
|
+
throw new Error(`invalid_cli_package_version:${packageJsonPath}`);
|
|
25
54
|
}
|
|
26
55
|
cachedCliPackageVersion = version;
|
|
27
56
|
return version;
|
package/dist/commands/capture.js
CHANGED
|
@@ -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
|
|
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.`);
|
|
@@ -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(
|
|
408
|
+
const response = await fetch(buildLocalDevAppUrl({
|
|
409
|
+
...input,
|
|
410
|
+
port,
|
|
411
|
+
}), {
|
|
404
412
|
method: 'GET',
|
|
405
413
|
signal: controller.signal,
|
|
406
414
|
});
|
|
407
|
-
return
|
|
415
|
+
return response.ok;
|
|
408
416
|
}
|
|
409
417
|
catch {
|
|
410
418
|
return false;
|
package/dist/commands/upload.js
CHANGED
|
@@ -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 });
|