@playdrop/playdrop-cli 0.11.9 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/build.js +2 -3
  3. package/dist/apps/index.d.ts +4 -5
  4. package/dist/apps/index.js +14 -16
  5. package/dist/apps/loadCheck.d.ts +76 -0
  6. package/dist/apps/loadCheck.js +874 -0
  7. package/dist/apps/upload.d.ts +3 -2
  8. package/dist/apps/upload.js +110 -6
  9. package/dist/catalogue-utils.d.ts +1 -0
  10. package/dist/catalogue-utils.js +1 -0
  11. package/dist/catalogue.d.ts +2 -0
  12. package/dist/catalogue.js +17 -0
  13. package/dist/commands/build.js +4 -1
  14. package/dist/commands/captureListing.d.ts +30 -1
  15. package/dist/commands/captureListing.js +305 -120
  16. package/dist/commands/check.d.ts +8 -0
  17. package/dist/commands/check.js +188 -0
  18. package/dist/commands/review.d.ts +20 -0
  19. package/dist/commands/review.js +112 -0
  20. package/dist/commands/upload-content.d.ts +1 -1
  21. package/dist/commands/upload.d.ts +2 -1
  22. package/dist/commands/upload.js +9 -52
  23. package/dist/commands/validate.js +0 -155
  24. package/dist/commands/worker/runtime.d.ts +1 -0
  25. package/dist/commands/worker/runtime.js +43 -3
  26. package/dist/commands/worker.d.ts +1 -1
  27. package/dist/commands/worker.js +22 -44
  28. package/dist/index.js +33 -49
  29. package/dist/playwright.d.ts +5 -0
  30. package/dist/playwright.js +60 -1
  31. package/node_modules/@playdrop/api-client/dist/client.d.ts +4 -2
  32. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/client.js +0 -10
  34. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  35. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  36. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  37. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  38. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  39. package/node_modules/@playdrop/api-client/dist/domains/auth.js +22 -0
  40. package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -3
  41. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  42. package/node_modules/@playdrop/api-client/dist/index.js +10 -5
  43. package/node_modules/@playdrop/config/client-meta.json +2 -2
  44. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts +8 -0
  45. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts.map +1 -0
  46. package/node_modules/@playdrop/config/dist/src/client-meta.js +51 -0
  47. package/node_modules/@playdrop/config/dist/src/deployment.d.ts.map +1 -1
  48. package/node_modules/@playdrop/config/dist/src/deployment.js +2 -32
  49. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  50. package/node_modules/@playdrop/config/dist/src/index.js +2 -28
  51. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts +0 -1
  52. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts.map +1 -1
  53. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.js +21 -5
  54. package/node_modules/@playdrop/config/dist/test/runtime-service-origins.test.js +8 -0
  55. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  56. package/node_modules/@playdrop/types/dist/api.d.ts +19 -14
  57. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  58. package/node_modules/@playdrop/types/dist/api.js +5 -10
  59. package/node_modules/@playdrop/types/dist/version.d.ts +14 -0
  60. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/dist/apps/launchCheck.d.ts +0 -39
  63. package/dist/apps/launchCheck.js +0 -414
  64. package/dist/captureRuntime.d.ts +0 -73
  65. package/dist/captureRuntime.js +0 -1021
  66. package/dist/commands/capture.d.ts +0 -14
  67. package/dist/commands/capture.js +0 -548
  68. package/dist/commands/captureRemote.d.ts +0 -17
  69. package/dist/commands/captureRemote.js +0 -394
  70. package/node_modules/@playdrop/types/dist/entity.d.ts +0 -31
  71. package/node_modules/@playdrop/types/dist/entity.d.ts.map +0 -1
  72. package/node_modules/@playdrop/types/dist/entity.js +0 -67
@@ -2,8 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validate = validate;
4
4
  const types_1 = require("@playdrop/types");
5
- const node_fs_1 = require("node:fs");
6
- const node_path_1 = require("node:path");
7
5
  const apps_1 = require("../apps");
8
6
  const assetSpecs_1 = require("../assetSpecs");
9
7
  const commandContext_1 = require("../commandContext");
@@ -11,96 +9,7 @@ const externalAssetPackValidation_1 = require("../externalAssetPackValidation");
11
9
  const taskSelection_1 = require("../taskSelection");
12
10
  const taskUtils_1 = require("../taskUtils");
13
11
  const uploadLog_1 = require("../uploadLog");
14
- const dev_1 = require("./dev");
15
12
  const upload_content_1 = require("./upload-content");
16
- function normalizePositivePort(value) {
17
- const parsed = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
18
- return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null;
19
- }
20
- function findWorkspaceMetadataFile(startPath) {
21
- let current = (0, node_path_1.resolve)(startPath);
22
- if ((0, node_fs_1.existsSync)(current) && (0, node_fs_1.statSync)(current).isFile()) {
23
- current = (0, node_path_1.dirname)(current);
24
- }
25
- while (true) {
26
- const workspacePinPath = (0, node_path_1.join)(current, '.playdrop.json');
27
- if ((0, node_fs_1.existsSync)(workspacePinPath) && (0, node_fs_1.statSync)(workspacePinPath).isFile()) {
28
- return workspacePinPath;
29
- }
30
- const taskContextPath = (0, node_path_1.join)(current, '.playdrop', 'task.json');
31
- if ((0, node_fs_1.existsSync)(taskContextPath) && (0, node_fs_1.statSync)(taskContextPath).isFile()) {
32
- return taskContextPath;
33
- }
34
- const parent = (0, node_path_1.dirname)(current);
35
- if (parent === current) {
36
- return null;
37
- }
38
- current = parent;
39
- }
40
- }
41
- function readTaskScopedDevRouterPort(workspacePath) {
42
- const metadataPath = findWorkspaceMetadataFile(workspacePath);
43
- if (!metadataPath) {
44
- throw new Error('project_validate_task_dev_port_missing');
45
- }
46
- let parsed;
47
- try {
48
- parsed = JSON.parse((0, node_fs_1.readFileSync)(metadataPath, 'utf8'));
49
- }
50
- catch {
51
- throw new Error(`project_validate_task_dev_port_invalid_json:${metadataPath}`);
52
- }
53
- const port = normalizePositivePort(parsed?.devPort);
54
- if (!port) {
55
- throw new Error(`project_validate_task_dev_port_missing:${metadataPath}`);
56
- }
57
- return { file: metadataPath, port };
58
- }
59
- function readWorkerTaskDevRouterPort() {
60
- if (process.env.PLAYDROP_WORKER_CONTEXT !== '1') {
61
- return null;
62
- }
63
- const workerPortRaw = process.env.PLAYDROP_WORKER_TASK_DEV_PORT;
64
- if (!workerPortRaw?.trim()) {
65
- throw new Error('project_validate_worker_dev_port_missing');
66
- }
67
- const workerPort = normalizePositivePort(workerPortRaw);
68
- if (!workerPort) {
69
- throw new Error(`project_validate_worker_dev_port_invalid:${workerPortRaw}`);
70
- }
71
- const devRouterPortRaw = process.env.PLAYDROP_DEV_ROUTER_PORT;
72
- if (devRouterPortRaw?.trim()) {
73
- const devRouterPort = normalizePositivePort(devRouterPortRaw);
74
- if (!devRouterPort) {
75
- throw new Error(`project_validate_env_dev_port_invalid:${devRouterPortRaw}`);
76
- }
77
- if (devRouterPort !== workerPort) {
78
- throw new Error(`project_validate_worker_dev_port_mismatch:env=${devRouterPort}:worker=${workerPort}`);
79
- }
80
- }
81
- return workerPort;
82
- }
83
- function resolveValidationDevRouterPort(workspacePath, taskScoped) {
84
- if (!taskScoped) {
85
- return (0, dev_1.resolveCliDevRouterPort)();
86
- }
87
- const workerPort = readWorkerTaskDevRouterPort();
88
- if (workerPort !== null) {
89
- return workerPort;
90
- }
91
- const taskPort = readTaskScopedDevRouterPort(workspacePath);
92
- const envPortRaw = process.env.PLAYDROP_DEV_ROUTER_PORT;
93
- if (envPortRaw !== undefined && envPortRaw.trim()) {
94
- const envPort = normalizePositivePort(envPortRaw);
95
- if (!envPort) {
96
- throw new Error(`project_validate_env_dev_port_invalid:${envPortRaw}`);
97
- }
98
- if (envPort !== taskPort.port) {
99
- throw new Error(`project_validate_worker_dev_port_mismatch:env=${envPort}:task=${taskPort.port}:${taskPort.file}`);
100
- }
101
- }
102
- return taskPort.port;
103
- }
104
13
  function buildLocalAssetSpecLookups(tasks) {
105
14
  const exactByRef = new Map();
106
15
  const uniqueByNameVersion = new Map();
@@ -172,42 +81,6 @@ async function loadAuthenticatedValidationContext(workspacePath, envOverride) {
172
81
  return null;
173
82
  }
174
83
  }
175
- async function loadHostedLaunchValidationContext(workspacePath, envOverride) {
176
- const ctx = await (0, commandContext_1.resolveAuthenticatedEnvironmentContext)('project validate', 'run hosted app launch validation', {
177
- env: envOverride,
178
- workspacePath,
179
- });
180
- if (!ctx) {
181
- return null;
182
- }
183
- const username = typeof ctx.account?.username === 'string' ? ctx.account.username.trim() : '';
184
- if (!username) {
185
- process.exitCode = 1;
186
- return null;
187
- }
188
- let currentUserRole = null;
189
- let currentUser = null;
190
- try {
191
- const me = await ctx.client.me();
192
- currentUserRole = me.user.role ?? null;
193
- currentUser = me.user ?? null;
194
- }
195
- catch {
196
- currentUserRole = null;
197
- currentUser = null;
198
- }
199
- return {
200
- client: ctx.client,
201
- currentUserRole,
202
- currentUser,
203
- username,
204
- apiBase: ctx.envConfig.apiBase,
205
- webBase: ctx.envConfig.webBase ?? null,
206
- token: ctx.token,
207
- devRouterPort: resolveValidationDevRouterPort(workspacePath, Boolean(ctx.workspaceAuth?.config.taskToken)),
208
- taskScoped: Boolean(ctx.workspaceAuth?.config.taskToken),
209
- };
210
- }
211
84
  function printResolvedValidationAccount(ctx) {
212
85
  const username = typeof ctx.account?.username === 'string' && ctx.account.username.trim().length > 0
213
86
  ? ctx.account.username.trim()
@@ -314,40 +187,12 @@ async function validate(pathOrName, options = {}) {
314
187
  const assetSpecTasks = tasks.filter((task) => task.kind === 'asset-spec');
315
188
  const assetSpecLookups = buildLocalAssetSpecLookups(assetSpecTasks);
316
189
  const sortedTasks = (0, taskUtils_1.sortTasks)(tasks);
317
- const hostedLaunchValidationRequired = sortedTasks.some((task) => task.kind === 'app' && task.hostingMode !== 'EXTERNAL' && !task.externalUrl);
318
- const hostedLaunchValidationContext = hostedLaunchValidationRequired
319
- ? await loadHostedLaunchValidationContext(workspacePath, options.env)
320
- : null;
321
- if (hostedLaunchValidationRequired && !hostedLaunchValidationContext) {
322
- return;
323
- }
324
190
  for (const task of sortedTasks) {
325
191
  const entityId = task.name;
326
192
  try {
327
193
  if (task.kind === 'app') {
328
194
  await (0, apps_1.validateAppTask)(task);
329
195
  (0, apps_1.collectAppValidationWarnings)(task, 'source').forEach((warning) => warnings.add(warning));
330
- if (task.hostingMode !== 'EXTERNAL' && !task.externalUrl) {
331
- const launchContext = hostedLaunchValidationContext;
332
- const creatorResult = (0, upload_content_1.getTaskCreatorResult)(task, launchContext.username, launchContext.currentUserRole);
333
- if (creatorResult.creatorTargetError) {
334
- throw new Error(creatorResult.creatorTargetError);
335
- }
336
- const launchCheck = await (0, apps_1.runLocalHostedLaunchCheck)({
337
- client: launchContext.client,
338
- apiBase: launchContext.apiBase,
339
- webBase: launchContext.webBase,
340
- creatorUsername: creatorResult.taskCreator,
341
- task,
342
- token: launchContext.token,
343
- currentUser: launchContext.currentUser,
344
- devRouterPort: launchContext.devRouterPort,
345
- allowUnregisteredViewerLaunch: launchContext.taskScoped,
346
- });
347
- if (launchCheck.status !== 'PASSED') {
348
- throw new Error((0, apps_1.formatHostedLaunchCheckFailure)(task.name, launchCheck, 'local'));
349
- }
350
- }
351
196
  }
352
197
  else if (task.kind === 'asset-spec') {
353
198
  // Parsing already validates the contract, symbol, and documentation files.
@@ -66,6 +66,7 @@ export declare function readCodexSandboxMode(env?: NodeJS.ProcessEnv): CodexSand
66
66
  export declare function buildClaudeExecArgs(input: {
67
67
  model: string;
68
68
  effort?: string | null;
69
+ chrome?: boolean;
69
70
  workspaceDir?: string | null;
70
71
  denyReadRoots?: string[] | null;
71
72
  pluginDir?: string | null;
@@ -461,7 +461,13 @@ function buildClaudeExecArgs(input) {
461
461
  throw new Error('agent_task_assignment_model_missing');
462
462
  }
463
463
  const permissionSettings = buildClaudePermissionSettings(input.denyReadRoots ?? []);
464
- const tools = 'Bash,Edit,Write,Read,Glob,Grep';
464
+ const permissionMode = input.chrome ? 'bypassPermissions' : 'acceptEdits';
465
+ const tools = input.chrome
466
+ ? [...CLAUDE_BASE_TOOL_NAMES, ...CLAUDE_CHROME_TOOL_NAMES].join(',')
467
+ : CLAUDE_BASE_TOOL_NAMES.join(',');
468
+ const allowedTools = input.chrome
469
+ ? [...CLAUDE_ALLOWED_PERMISSION_RULES, ...CLAUDE_CHROME_TOOL_NAMES]
470
+ : CLAUDE_ALLOWED_PERMISSION_RULES;
465
471
  const args = [
466
472
  '-p',
467
473
  '--model',
@@ -472,16 +478,19 @@ function buildClaudeExecArgs(input) {
472
478
  '--include-partial-messages',
473
479
  '--no-session-persistence',
474
480
  '--permission-mode',
475
- 'acceptEdits',
481
+ permissionMode,
476
482
  '--tools',
477
483
  tools,
478
484
  '--allowedTools',
479
- ...CLAUDE_ALLOWED_PERMISSION_RULES,
485
+ ...allowedTools,
480
486
  '--disallowedTools',
481
487
  ...buildClaudeDeniedPermissionRules(input.denyReadRoots ?? []),
482
488
  '--settings',
483
489
  JSON.stringify(permissionSettings),
484
490
  ];
491
+ if (input.chrome) {
492
+ args.unshift('--chrome');
493
+ }
485
494
  const pluginDir = typeof input.pluginDir === 'string' ? input.pluginDir.trim() : '';
486
495
  if (pluginDir) {
487
496
  args.push('--plugin-dir', pluginDir);
@@ -492,6 +501,37 @@ function buildClaudeExecArgs(input) {
492
501
  }
493
502
  return args;
494
503
  }
504
+ const CLAUDE_BASE_TOOL_NAMES = [
505
+ 'Bash',
506
+ 'Edit',
507
+ 'Write',
508
+ 'Read',
509
+ 'Glob',
510
+ 'Grep',
511
+ ];
512
+ const CLAUDE_CHROME_TOOL_NAMES = [
513
+ 'mcp__claude-in-chrome__browser_batch',
514
+ 'mcp__claude-in-chrome__computer',
515
+ 'mcp__claude-in-chrome__file_upload',
516
+ 'mcp__claude-in-chrome__find',
517
+ 'mcp__claude-in-chrome__form_input',
518
+ 'mcp__claude-in-chrome__get_page_text',
519
+ 'mcp__claude-in-chrome__javascript_tool',
520
+ 'mcp__claude-in-chrome__list_connected_browsers',
521
+ 'mcp__claude-in-chrome__navigate',
522
+ 'mcp__claude-in-chrome__read_console_messages',
523
+ 'mcp__claude-in-chrome__read_network_requests',
524
+ 'mcp__claude-in-chrome__read_page',
525
+ 'mcp__claude-in-chrome__resize_window',
526
+ 'mcp__claude-in-chrome__select_browser',
527
+ 'mcp__claude-in-chrome__shortcuts_execute',
528
+ 'mcp__claude-in-chrome__shortcuts_list',
529
+ 'mcp__claude-in-chrome__switch_browser',
530
+ 'mcp__claude-in-chrome__tabs_close_mcp',
531
+ 'mcp__claude-in-chrome__tabs_context_mcp',
532
+ 'mcp__claude-in-chrome__tabs_create_mcp',
533
+ 'mcp__claude-in-chrome__upload_image',
534
+ ];
495
535
  const CLAUDE_ALLOWED_PERMISSION_RULES = [
496
536
  'Read',
497
537
  'Edit',
@@ -5,7 +5,7 @@ import { type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './w
5
5
  export { assertWorkerTokenUsageWithinCap, buildClaudeExecArgs, buildClaudePermissionSettings, buildCodexExecArgs, buildWorkerChildEnv, DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, extractAgentTokenUsage, extractCodexRolloutTokenUsage, extractCodexThreadId, extractCodexTokensUsed, readCodexRolloutTokenUsageForThread, readCodexSandboxMode, readEnvBoolean, runLoggedProcess } from './worker/runtime';
6
6
  export type { AgentTokenUsage, LoggedProcessResult, LoggedProcessTranscriptChunk } from './worker/runtime';
7
7
  export declare const WORKER_SESSION_EXPIRED_MESSAGE = "worker session expired: run \"playdrop auth login\" and start the worker again";
8
- export declare const WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = "worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
8
+ export declare const WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = "worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/check/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.";
9
9
  type WorkerStartOptions = {
10
10
  env?: string;
11
11
  once?: boolean;
@@ -120,11 +120,14 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
120
120
  ['task', 'submit-eval'],
121
121
  ['task', 'fail'],
122
122
  ['review', 'validate-result'],
123
+ ['review', 'list-windows'],
124
+ ['review', 'screenshot'],
123
125
  ['review', 'compose-evidence'],
124
126
  ['review', 'rating-card'],
125
127
  ['project', 'validate'],
126
128
  ['project', 'build'],
127
129
  ['project', 'dev'],
130
+ ['project', 'check'],
128
131
  ['project', 'capture'],
129
132
  ['browse'],
130
133
  ['search'],
@@ -138,7 +141,7 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
138
141
  ['ai', 'jobs', 'detail'],
139
142
  ['ai', 'generations'],
140
143
  ];
141
- exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = 'worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.';
144
+ exports.WORKER_CONTEXT_COMMAND_NOT_ALLOWED_MESSAGE = 'worker_context_command_not_allowed: inside a PlayDrop worker task only task progress, task claim-slug, task upload/done/fail, assigned app scaffolding, project validation/build/dev/check/capture, read-only help/catalogue/documentation lookup, and PlayDrop AI generation are permitted.';
142
145
  function allocateWorkerDevPort(input) {
143
146
  const basePort = input.basePort ?? DEFAULT_WORKER_DEV_PORT_BASE;
144
147
  const maxParallelTasks = input.maxParallelTasks ?? DEFAULT_WORKER_MAX_PARALLEL_TASKS;
@@ -727,15 +730,6 @@ async function readTaskNextStepsFileForCommand(input) {
727
730
  return NEXT_STEPS_READ_FAILED;
728
731
  }
729
732
  }
730
- function isFinalOwnerRouteLaunchCheckFailure(error) {
731
- const message = error instanceof Error ? error.message : String(error);
732
- return message.startsWith('hosted_launch_check_failed:')
733
- && message.includes('final owner play route');
734
- }
735
- function formatTerminalFinalOwnerRouteFailure(error) {
736
- const message = error instanceof Error ? error.message : String(error);
737
- return `agent_task_final_owner_route_failed: final owner play route validation failed after the task version was created. Task uploads have a fixed outputVersion and cannot be retried by bumping catalogue.json. ${message}`;
738
- }
739
733
  function assertTaskUploadResultMatchesContext(input) {
740
734
  if (input.uploadResult.taskId !== input.taskContext.taskId) {
741
735
  throw new Error('task_upload_result_task_mismatch');
@@ -1851,6 +1845,7 @@ async function runClaude(input) {
1851
1845
  workspaceDir: input.workspaceDir,
1852
1846
  denyReadRoots,
1853
1847
  pluginDir: input.playdropPluginRoot,
1848
+ chrome: input.enableChrome,
1854
1849
  }),
1855
1850
  cwd: input.workspaceDir,
1856
1851
  env: (0, runtime_1.buildWorkerChildEnv)({
@@ -3529,6 +3524,7 @@ async function startWorker(options = {}) {
3529
3524
  binDir,
3530
3525
  eventDir,
3531
3526
  prompt,
3527
+ enableChrome: task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL',
3532
3528
  envName: env,
3533
3529
  taskId: task.id,
3534
3530
  attempt: taskContext.attempt,
@@ -3966,40 +3962,22 @@ async function uploadTask(options = {}) {
3966
3962
  return;
3967
3963
  }
3968
3964
  const projectDir = discoverWorkerProjectRoot(node_process_1.default.cwd());
3969
- let published;
3970
- try {
3971
- published = await (0, upload_1.publishWorkerAppProject)({
3972
- client: ctx.client,
3973
- taskId: taskContext.taskId,
3974
- kind: taskContext.kind,
3975
- expectedAppName: taskContext.outputAppName ?? undefined,
3976
- remixSourceRef: taskContext.remixSourceRef ?? null,
3977
- playdropAssetRequirement: resolvePlaydropAssetRequirementFromTaskRequest(taskContext.creatorRequest),
3978
- creatorRequest: taskContext.creatorRequest,
3979
- projectDir,
3980
- creatorUsername: taskContext.creatorUsername,
3981
- apiBase: ctx.envConfig.apiBase,
3982
- webBase: ctx.envConfig.webBase ?? null,
3983
- token: ctx.token,
3984
- user: ctx.user,
3985
- });
3986
- }
3987
- catch (error) {
3988
- if (isFinalOwnerRouteLaunchCheckFailure(error)) {
3989
- const terminalError = formatTerminalFinalOwnerRouteFailure(error);
3990
- await ctx.client.workerFailAgentTask(taskContext.taskId, {
3991
- taskToken: taskContext.taskToken,
3992
- error: terminalError,
3993
- result: {
3994
- failedBy: 'worker_upload_final_owner_route',
3995
- finalOwnerRouteFailure: true,
3996
- originalError: error instanceof Error ? error.message : String(error),
3997
- },
3998
- });
3999
- throw new Error(terminalError);
4000
- }
4001
- throw error;
4002
- }
3965
+ const published = await (0, upload_1.publishWorkerAppProject)({
3966
+ client: ctx.client,
3967
+ taskId: taskContext.taskId,
3968
+ kind: taskContext.kind,
3969
+ executionTarget: taskContext.target,
3970
+ expectedAppName: taskContext.outputAppName ?? undefined,
3971
+ remixSourceRef: taskContext.remixSourceRef ?? null,
3972
+ playdropAssetRequirement: resolvePlaydropAssetRequirementFromTaskRequest(taskContext.creatorRequest),
3973
+ creatorRequest: taskContext.creatorRequest,
3974
+ projectDir,
3975
+ creatorUsername: taskContext.creatorUsername,
3976
+ apiBase: ctx.envConfig.apiBase,
3977
+ webBase: ctx.envConfig.webBase ?? null,
3978
+ token: ctx.token,
3979
+ user: ctx.user,
3980
+ });
4003
3981
  const result = {
4004
3982
  taskId: taskContext.taskId,
4005
3983
  appId: published.appId,
package/dist/index.js CHANGED
@@ -30,10 +30,9 @@ const create_1 = require("./commands/create");
30
30
  const createRemixContent_1 = require("./commands/createRemixContent");
31
31
  const dev_1 = require("./commands/dev");
32
32
  const devBrowser_1 = require("./commands/devBrowser");
33
- const capture_1 = require("./commands/capture");
33
+ const check_1 = require("./commands/check");
34
34
  const devServer_1 = require("./commands/devServer");
35
35
  const captureListing_1 = require("./commands/captureListing");
36
- const captureRemote_1 = require("./commands/captureRemote");
37
36
  const marketing_1 = require("./commands/marketing");
38
37
  const review_1 = require("./commands/review");
39
38
  const validate_1 = require("./commands/validate");
@@ -290,6 +289,23 @@ task
290
289
  await (0, worker_1.failTask)(opts);
291
290
  });
292
291
  const review = program.command('review').description('Review helper commands for PlayDrop worker tasks');
292
+ review
293
+ .command('list-windows')
294
+ .description('List native capture windows for review evidence')
295
+ .requiredOption('--pid <pid>', 'Browser process id')
296
+ .action(async (opts) => {
297
+ await (0, review_1.listReviewCaptureWindows)(opts);
298
+ });
299
+ review
300
+ .command('screenshot')
301
+ .description('Capture a native screenshot for review evidence')
302
+ .requiredOption('--pid <pid>', 'Browser process id')
303
+ .option('--window-id <id>', 'Native capture window id')
304
+ .requiredOption('--out <path>', 'Output PNG path')
305
+ .option('--metadata <path>', 'Output metadata JSON path')
306
+ .action(async (opts) => {
307
+ await (0, review_1.captureReviewScreenshot)(opts);
308
+ });
293
309
  review
294
310
  .command('validate-result')
295
311
  .description('Validate a canonical game review result')
@@ -986,37 +1002,24 @@ project
986
1002
  player: opts.player,
987
1003
  });
988
1004
  });
989
- const projectCapture = project.command('capture').description('Run a Playwright capture');
990
- projectCapture
991
- .argument('[target]')
1005
+ project
1006
+ .command('check [target]')
1007
+ .description('Run the deterministic hosted game load-check in installed Chrome')
992
1008
  .option('--app <name>', 'App name when the target is a workspace')
993
- .option('--timeout <seconds>', 'Capture timeout in seconds')
994
- .option('--log-level <level>', 'debug, info, warn, or error')
1009
+ .option('--timeout <seconds>', 'Load-check timeout in seconds')
995
1010
  .option('--screenshot <path>', 'Screenshot output path')
996
- .option('--surface <surface>', 'desktop, mobile-landscape, or mobile-portrait')
997
- .option('--dev-auth <mode>', 'anonymous, viewer, or player')
998
- .option('--player <slot>', '1, 2, 3, or 4 when --dev-auth player is selected')
999
- .option('--reset-dev-player <mode>', 'before, after, before-and-after, or never')
1000
- .option('--username <username>', 'Override saved login username for this run')
1001
- .option('--password <password>', 'Override saved login password for this run')
1002
- .action(async (target, opts) => {
1003
- const timeoutSeconds = opts.timeout !== undefined ? Number.parseFloat(String(opts.timeout)) : undefined;
1004
- await (0, capture_1.capture)(target, {
1005
- timeoutSeconds,
1006
- appName: opts.app,
1007
- logLevel: opts.logLevel,
1008
- screenshotPath: opts.screenshot,
1009
- surfaceTarget: opts.surface,
1010
- devAuth: opts.devAuth,
1011
- player: opts.player,
1012
- resetDevPlayer: opts.resetDevPlayer,
1013
- username: opts.username,
1014
- password: opts.password,
1011
+ .option('--actions <path>', 'JSON action script to dispatch after the game frame is ready')
1012
+ .action(async (target, opts = {}) => {
1013
+ await (0, check_1.check)(target, {
1014
+ app: opts.app,
1015
+ timeout: opts.timeout,
1016
+ screenshot: opts.screenshot,
1017
+ actions: opts.actions,
1015
1018
  });
1016
1019
  });
1020
+ const projectCapture = project.command('capture').description('Record game media with the native macOS recorder');
1017
1021
  projectCapture
1018
- .command('listing [target]', { hidden: true })
1019
- .description('Record private listing video for a hosted app')
1022
+ .argument('[target]')
1020
1023
  .option('--app <name>', 'App name when the target is a workspace')
1021
1024
  .option('--duration <seconds>', 'Recording duration in seconds')
1022
1025
  .option('--width <pixels>', 'Final gameplay width in pixels')
@@ -1026,6 +1029,7 @@ projectCapture
1026
1029
  .option('--audio', 'Capture gameplay audio')
1027
1030
  .option('--output-dir <path>', 'Output directory for the capture artifacts')
1028
1031
  .option('--keep-raw', 'Keep the raw intermediate recording')
1032
+ .option('--content-hash <sha256>', 'Uploaded build content hash to bind this capture to')
1029
1033
  .action(async (target, opts) => {
1030
1034
  await (0, captureListing_1.captureListing)(target, {
1031
1035
  app: opts.app,
@@ -1037,27 +1041,7 @@ projectCapture
1037
1041
  audio: opts.audio,
1038
1042
  outputDir: opts.outputDir,
1039
1043
  keepRaw: opts.keepRaw,
1040
- });
1041
- });
1042
- projectCapture
1043
- .command('remote <url>')
1044
- .description('Run a Playwright capture against a published URL')
1045
- .option('--timeout <seconds>', 'Readiness timeout in seconds')
1046
- .option('--settle <seconds>', 'Post-ready settle delay before screenshot in seconds')
1047
- .option('--screenshot <path>', 'Screenshot output path')
1048
- .option('--log <path>', 'Log output path')
1049
- .option('--expected-url <url>', 'Expected final URL after redirects')
1050
- .option('--actions <path>', 'JSON action file with click, press, and wait steps before screenshot')
1051
- .option('--viewport <size>', 'Viewport size as WIDTHxHEIGHT')
1052
- .option('--surface <surface>', 'Browser surface preset: desktop, mobile-landscape, or mobile-portrait')
1053
- .option('--fps-sample-ms <milliseconds>', 'Sample frame rate for this many milliseconds after capture')
1054
- .option('--username <username>', 'Login username')
1055
- .option('--password <password>', 'Login password')
1056
- .option('--login-url <url>', 'Explicit login URL')
1057
- .action(async (url, _opts, command) => {
1058
- await (0, captureRemote_1.captureRemote)(url, {
1059
- ...(command.parent?.opts() ?? {}),
1060
- ...command.opts(),
1044
+ contentHash: opts.contentHash,
1061
1045
  });
1062
1046
  });
1063
1047
  const projectMarketing = project.command('marketing').description('Prepare local marketing captures and assets');
@@ -1,6 +1,7 @@
1
1
  import type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright-core';
2
2
  export declare const HEADLESS_CHROMIUM_ARGS: readonly ["--no-sandbox", "--disable-web-security", "--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"];
3
3
  export declare const INTERACTIVE_CHROMIUM_ARGS: readonly ["--no-sandbox", "--disable-web-security", "--ignore-gpu-blocklist"];
4
+ export declare const INSTALLED_CHROME_ARGS: readonly ["--no-sandbox", "--disable-web-security", "--ignore-gpu-blocklist"];
4
5
  export declare const CHROMIUM_ARGS: readonly ["--no-sandbox", "--disable-web-security", "--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"];
5
6
  type PlaywrightModule = {
6
7
  chromium: {
@@ -8,11 +9,13 @@ type PlaywrightModule = {
8
9
  headless?: boolean;
9
10
  args?: readonly string[];
10
11
  chromiumSandbox?: boolean;
12
+ channel?: string;
11
13
  }): Promise<Browser>;
12
14
  launchPersistentContext?(userDataDir: string, options: BrowserContextOptions & {
13
15
  headless?: boolean;
14
16
  args?: readonly string[];
15
17
  chromiumSandbox?: boolean;
18
+ channel?: string;
16
19
  }): Promise<BrowserContext>;
17
20
  };
18
21
  };
@@ -21,11 +24,13 @@ type PlaydropBrowserContextOptions = BrowserContextOptions & {
21
24
  };
22
25
  export declare function setPlaywrightLoader(loader: (() => Promise<PlaywrightModule>) | null): void;
23
26
  export declare function createPlaywrightLaunchError(tool: string, error: unknown): Error;
27
+ export declare function createInstalledChromeLaunchError(tool: string, error: unknown): Error;
24
28
  type PageCallback<T> = (context: {
25
29
  browser: Browser;
26
30
  context: BrowserContext;
27
31
  page: Page;
28
32
  }) => Promise<T>;
29
33
  export declare function withChromiumPage<T>(callback: PageCallback<T>, options?: PlaydropBrowserContextOptions): Promise<T>;
34
+ export declare function withInstalledChromePage<T>(callback: PageCallback<T>, options?: PlaydropBrowserContextOptions): Promise<T>;
30
35
  export declare function launchPersistentChromiumContext(userDataDir: string, options?: PlaydropBrowserContextOptions): Promise<BrowserContext>;
31
36
  export {};
@@ -33,13 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CHROMIUM_ARGS = exports.INTERACTIVE_CHROMIUM_ARGS = exports.HEADLESS_CHROMIUM_ARGS = void 0;
36
+ exports.CHROMIUM_ARGS = exports.INSTALLED_CHROME_ARGS = exports.INTERACTIVE_CHROMIUM_ARGS = exports.HEADLESS_CHROMIUM_ARGS = void 0;
37
37
  exports.setPlaywrightLoader = setPlaywrightLoader;
38
38
  exports.createPlaywrightLaunchError = createPlaywrightLaunchError;
39
+ exports.createInstalledChromeLaunchError = createInstalledChromeLaunchError;
39
40
  exports.withChromiumPage = withChromiumPage;
41
+ exports.withInstalledChromePage = withInstalledChromePage;
40
42
  exports.launchPersistentChromiumContext = launchPersistentChromiumContext;
41
43
  const node_url_1 = require("node:url");
42
44
  const TOOL_NAME = 'playdrop';
45
+ const INSTALLED_CHROME_CHANNEL = 'chrome';
43
46
  const COMMON_CHROMIUM_ARGS = [
44
47
  '--no-sandbox',
45
48
  '--disable-web-security',
@@ -57,6 +60,8 @@ exports.INTERACTIVE_CHROMIUM_ARGS = [
57
60
  ...COMMON_CHROMIUM_ARGS,
58
61
  '--ignore-gpu-blocklist',
59
62
  ];
63
+ // Deterministic project checks must exercise the installed Chrome GPU path.
64
+ exports.INSTALLED_CHROME_ARGS = exports.INTERACTIVE_CHROMIUM_ARGS;
60
65
  // Backward-compatible alias for older imports.
61
66
  exports.CHROMIUM_ARGS = exports.HEADLESS_CHROMIUM_ARGS;
62
67
  let customLoader = null;
@@ -101,6 +106,19 @@ function createPlaywrightLaunchError(tool, error) {
101
106
  }
102
107
  return error instanceof Error ? error : new Error(message);
103
108
  }
109
+ function createInstalledChromeLaunchError(tool, error) {
110
+ const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : String(error);
111
+ return new Error([
112
+ `[${tool}] Failed to launch installed Google Chrome.`,
113
+ '',
114
+ 'Required setup:',
115
+ ' 1. Install Google Chrome for this macOS user.',
116
+ ' 2. Run from a logged-in GUI session with WindowServer access.',
117
+ ' 3. If invoked over SSH, keep keychain unlock and the command in the same SSH session.',
118
+ '',
119
+ `Original error: ${message}`,
120
+ ].join('\n'));
121
+ }
104
122
  async function grantAutomationLocalNetworkAccess(context, origin) {
105
123
  if (!origin?.trim()) {
106
124
  return;
@@ -153,6 +171,47 @@ async function withChromiumPage(callback, options = {}) {
153
171
  }
154
172
  }
155
173
  }
174
+ async function withInstalledChromePage(callback, options = {}) {
175
+ const { chromium } = await loadPlaywright();
176
+ let browser;
177
+ try {
178
+ browser = await chromium.launch({
179
+ channel: INSTALLED_CHROME_CHANNEL,
180
+ headless: false,
181
+ args: [...exports.INSTALLED_CHROME_ARGS],
182
+ chromiumSandbox: false,
183
+ });
184
+ }
185
+ catch (error) {
186
+ throw createInstalledChromeLaunchError(TOOL_NAME, error);
187
+ }
188
+ let context = null;
189
+ try {
190
+ const { automationOrigin, ...contextOptions } = options;
191
+ context = await browser.newContext(contextOptions);
192
+ await grantAutomationLocalNetworkAccess(context, automationOrigin);
193
+ const page = await context.newPage();
194
+ return await callback({ browser, context, page });
195
+ }
196
+ finally {
197
+ if (context) {
198
+ try {
199
+ await context.close();
200
+ }
201
+ catch (closeError) {
202
+ const message = closeError instanceof Error ? closeError.message : String(closeError);
203
+ console.warn(`[${TOOL_NAME}] Failed to close Playwright context: ${message}`);
204
+ }
205
+ }
206
+ try {
207
+ await browser.close();
208
+ }
209
+ catch (closeError) {
210
+ const message = closeError instanceof Error ? closeError.message : String(closeError);
211
+ console.warn(`[${TOOL_NAME}] Failed to close Playwright browser: ${message}`);
212
+ }
213
+ }
214
+ }
156
215
  async function launchPersistentChromiumContext(userDataDir, options = {}) {
157
216
  const { chromium } = await loadPlaywright();
158
217
  if (typeof chromium.launchPersistentContext !== 'function') {