@playdrop/playdrop-cli 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.0",
2
+ "version": "0.13.2",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.0",
4
+ "runtimeSdkVersion": "0.13.2",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -85,7 +85,7 @@ export declare function buildListingCaptureFrameUrl({ webBase, currentUsername,
85
85
  export declare function resolveListingCaptureSceneId(width: number, height: number): 'listing-landscape' | 'listing-portrait';
86
86
  export declare function parseCaptureListingOptions(targetArg: string | undefined, options?: CaptureListingOptions): ParsedCaptureListingOptions;
87
87
  export declare function assertSupportedListingEnvironment(platform?: NodeJS.Platform, macosVersion?: string): void;
88
- export declare function resolveListingRecorderPath(baseDir?: string): string;
88
+ export declare function resolveListingRecorderPath(workerHome?: string): string;
89
89
  export declare function buildSurfaceOutputPaths(outputDir: string, surface: AppSurface, useSurfacePrefix: boolean): {
90
90
  rawVideoPath: string;
91
91
  metadataPath: string;
@@ -59,6 +59,7 @@ const commandContext_1 = require("../commandContext");
59
59
  const http_1 = require("../http");
60
60
  const messages_1 = require("../messages");
61
61
  const playwright_1 = require("../playwright");
62
+ const recorderRelease_1 = require("../recorderRelease");
62
63
  const listingCaptureLease_1 = require("../listingCaptureLease");
63
64
  const dev_1 = require("./dev");
64
65
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
@@ -204,8 +205,8 @@ function commandExists(command) {
204
205
  async function ensureExecutableFile(filePath) {
205
206
  await (0, promises_1.access)(filePath);
206
207
  }
207
- function resolveListingRecorderPath(baseDir = __dirname) {
208
- return (0, node_path_1.resolve)(baseDir, '..', '..', '..', '..', 'clients', 'apple', 'playdrop-listing-recorder', '.build', 'release', 'playdrop-listing-recorder');
208
+ function resolveListingRecorderPath(workerHome) {
209
+ return (0, recorderRelease_1.resolveMacWorkerRecorderPath)(workerHome);
209
210
  }
210
211
  function buildTimestampFolder(date = new Date()) {
211
212
  const iso = date.toISOString();
@@ -755,7 +756,7 @@ async function ensureListingRecorderAvailable(recorderPath) {
755
756
  return true;
756
757
  }
757
758
  catch {
758
- (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Build it with: (cd clients/apple/playdrop-listing-recorder && swift build -c release)'], { command: 'project capture' });
759
+ (0, messages_1.printErrorWithHelp)(`Native recorder binary was not found at ${recorderPath}.`, ['Start the PlayDrop worker once on this Mac to install the recorder from the PlayDrop CDN.'], { command: 'project capture' });
759
760
  process.exitCode = 1;
760
761
  return false;
761
762
  }
@@ -85,6 +85,7 @@ export declare function buildClaudeExecArgs(input: {
85
85
  }): string[];
86
86
  export declare const PLAYWRIGHT_MCP_VERSION = "0.0.78";
87
87
  export type WorkerInstrumentSurface = 'DESKTOP' | 'MOBILE_LANDSCAPE' | 'MOBILE_PORTRAIT';
88
+ export declare function resolvePlaywrightMcpCliPath(): string;
88
89
  export declare function buildPlaywrightMcpCommand(surface?: WorkerInstrumentSurface | null): {
89
90
  command: string;
90
91
  args: string[];
@@ -18,6 +18,7 @@ exports.runLoggedProcess = runLoggedProcess;
18
18
  exports.buildCodexExecArgs = buildCodexExecArgs;
19
19
  exports.readCodexSandboxMode = readCodexSandboxMode;
20
20
  exports.buildClaudeExecArgs = buildClaudeExecArgs;
21
+ exports.resolvePlaywrightMcpCliPath = resolvePlaywrightMcpCliPath;
21
22
  exports.buildPlaywrightMcpCommand = buildPlaywrightMcpCommand;
22
23
  exports.buildPlaywrightMcpServerConfig = buildPlaywrightMcpServerConfig;
23
24
  exports.buildClaudePermissionSettings = buildClaudePermissionSettings;
@@ -27,6 +28,7 @@ exports.buildCleanRoomChildEnv = buildCleanRoomChildEnv;
27
28
  const node_child_process_1 = require("node:child_process");
28
29
  const node_fs_1 = require("node:fs");
29
30
  const promises_1 = require("node:fs/promises");
31
+ const node_module_1 = require("node:module");
30
32
  const node_os_1 = __importDefault(require("node:os"));
31
33
  const node_path_1 = __importDefault(require("node:path"));
32
34
  const surfaceProfiles_1 = require("../../apps/surfaceProfiles");
@@ -691,13 +693,32 @@ const CLAUDE_BASE_TOOL_NAMES = [
691
693
  // never through @latest drift (see the 2026-07-13 auto-update incident).
692
694
  exports.PLAYWRIGHT_MCP_VERSION = '0.0.78';
693
695
  const PLAYWRIGHT_MCP_ALLOWED_TOOL_RULE = 'mcp__playwright';
696
+ const requireFromWorkerRuntime = (0, node_module_1.createRequire)(__filename);
697
+ function resolvePlaywrightMcpCliPath() {
698
+ let packageJsonPath;
699
+ let packageJson;
700
+ try {
701
+ packageJsonPath = requireFromWorkerRuntime.resolve('@playwright/mcp/package.json');
702
+ packageJson = requireFromWorkerRuntime('@playwright/mcp/package.json');
703
+ }
704
+ catch {
705
+ throw new Error(`worker_preflight_playwright_mcp_missing:@playwright/mcp@${exports.PLAYWRIGHT_MCP_VERSION}`);
706
+ }
707
+ if (packageJson.version !== exports.PLAYWRIGHT_MCP_VERSION) {
708
+ throw new Error(`worker_preflight_playwright_mcp_version:expected=${exports.PLAYWRIGHT_MCP_VERSION}:actual=${packageJson.version ?? 'missing'}`);
709
+ }
710
+ const cliPath = node_path_1.default.join(node_path_1.default.dirname(packageJsonPath), 'cli.js');
711
+ if (!(0, node_fs_1.existsSync)(cliPath)) {
712
+ throw new Error(`worker_preflight_playwright_mcp_missing:${cliPath}`);
713
+ }
714
+ return cliPath;
715
+ }
694
716
  function buildPlaywrightMcpCommand(surface) {
695
717
  const surfaceArgs = surface ? (0, surfaceProfiles_1.buildPlaywrightMcpSurfaceArgs)(surface) : [];
696
718
  return {
697
- command: 'npx',
719
+ command: process.execPath,
698
720
  args: [
699
- '-y',
700
- `@playwright/mcp@${exports.PLAYWRIGHT_MCP_VERSION}`,
721
+ resolvePlaywrightMcpCliPath(),
701
722
  '--browser',
702
723
  'chrome',
703
724
  '--isolated',
@@ -304,6 +304,16 @@ export declare function parseCodexModelCatalog(output: string): string[];
304
304
  export declare function parseClaudeModelCatalog(output: string): string[];
305
305
  export declare function claudeModelProbeReportsSelection(output: string, expectedDisplayName: string): boolean;
306
306
  export declare function probeWorkerProviderIndependently<T>(probe: () => T, unavailable: T): T;
307
+ type MacChromeAutomationProbeRunner = (command: string, args: string[]) => {
308
+ status: number | null;
309
+ stdout: string;
310
+ stderr: string;
311
+ };
312
+ export declare function probeMacChromeAutomationPermission(options?: {
313
+ platform?: NodeJS.Platform;
314
+ runner?: MacChromeAutomationProbeRunner;
315
+ }): 'granted' | 'not_running' | 'denied' | 'not_applicable';
316
+ export declare function describeWorkerPreflightReason(reason: string): string;
307
317
  export declare function buildWorkerSetupActions(input: {
308
318
  degradedReasons: string[];
309
319
  playwright?: {
@@ -392,6 +402,17 @@ export declare function enforceInstrumentBrowserHygiene(assignment: WorkerTaskAs
392
402
  remainingTaskTabs: number;
393
403
  blankWindows: number;
394
404
  }>;
405
+ export declare function finalizeInstrumentBrowserHygiene(assignment: WorkerTaskAssignmentV2, options?: {
406
+ platform?: NodeJS.Platform;
407
+ runner?: InstrumentBrowserHygieneRunner;
408
+ }): Promise<{
409
+ ok: true;
410
+ remainingTaskTabs: number;
411
+ blankWindows: number;
412
+ } | {
413
+ ok: false;
414
+ error: string;
415
+ }>;
395
416
  export declare function assignmentRequiresRegisteredBundle(assignment: WorkerTaskAssignmentV2): boolean;
396
417
  export declare function startWorker(options?: WorkerStartOptions): Promise<void>;
397
418
  export declare function reportTask(options: TaskReportOptions): Promise<void>;
@@ -56,6 +56,8 @@ exports.parseCodexModelCatalog = parseCodexModelCatalog;
56
56
  exports.parseClaudeModelCatalog = parseClaudeModelCatalog;
57
57
  exports.claudeModelProbeReportsSelection = claudeModelProbeReportsSelection;
58
58
  exports.probeWorkerProviderIndependently = probeWorkerProviderIndependently;
59
+ exports.probeMacChromeAutomationPermission = probeMacChromeAutomationPermission;
60
+ exports.describeWorkerPreflightReason = describeWorkerPreflightReason;
59
61
  exports.buildWorkerSetupActions = buildWorkerSetupActions;
60
62
  exports.resolveWorkerStateDir = resolveWorkerStateDir;
61
63
  exports.resolveWorkerSupervisorLockFile = resolveWorkerSupervisorLockFile;
@@ -77,6 +79,7 @@ exports.showWorkerStatus = showWorkerStatus;
77
79
  exports.setupWorker = setupWorker;
78
80
  exports.instrumentTaskChromeTitleMatchers = instrumentTaskChromeTitleMatchers;
79
81
  exports.enforceInstrumentBrowserHygiene = enforceInstrumentBrowserHygiene;
82
+ exports.finalizeInstrumentBrowserHygiene = finalizeInstrumentBrowserHygiene;
80
83
  exports.assignmentRequiresRegisteredBundle = assignmentRequiresRegisteredBundle;
81
84
  exports.startWorker = startWorker;
82
85
  exports.reportTask = reportTask;
@@ -108,6 +111,7 @@ const commandContext_1 = require("../commandContext");
108
111
  const config_1 = require("../config");
109
112
  const messages_1 = require("../messages");
110
113
  const output_1 = require("../output");
114
+ const recorderRelease_1 = require("../recorderRelease");
111
115
  const shellProbe_1 = require("../shellProbe");
112
116
  const review_1 = require("./review");
113
117
  const upload_1 = require("./upload");
@@ -1548,14 +1552,6 @@ function readCodexAuthenticated(homeDir = node_os_1.default.homedir()) {
1548
1552
  return false;
1549
1553
  }
1550
1554
  }
1551
- function probeNpmVersion() {
1552
- const result = (0, shellProbe_1.runShell)('npm --version');
1553
- const version = result.output.trim().split('\n')[0]?.trim() ?? '';
1554
- if (result.exitCode !== 0 || !version) {
1555
- throw new Error(`worker_preflight_npm_missing: "npm --version" exited with code ${result.exitCode}.`);
1556
- }
1557
- return version;
1558
- }
1559
1555
  function probePlaywrightChromium() {
1560
1556
  let playwrightCore;
1561
1557
  let packageJson;
@@ -2833,6 +2829,146 @@ function errorMessage(error) {
2833
2829
  function errorCode(error) {
2834
2830
  return errorMessage(error).split(':')[0]?.trim() || 'unknown_error';
2835
2831
  }
2832
+ function workerCommandExists(command) {
2833
+ const probe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)(command));
2834
+ return probe.exitCode === 0 && Boolean(probe.output.trim());
2835
+ }
2836
+ function resolveInstalledChromeExecutable() {
2837
+ if (node_process_1.default.platform === 'darwin') {
2838
+ const executable = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
2839
+ return (0, node_fs_1.existsSync)(executable) ? executable : null;
2840
+ }
2841
+ if (node_process_1.default.platform === 'win32') {
2842
+ const roots = [node_process_1.default.env.PROGRAMFILES, node_process_1.default.env['PROGRAMFILES(X86)'], node_process_1.default.env.LOCALAPPDATA]
2843
+ .map((value) => value?.trim())
2844
+ .filter((value) => Boolean(value));
2845
+ for (const root of roots) {
2846
+ const executable = node_path_1.default.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe');
2847
+ if ((0, node_fs_1.existsSync)(executable)) {
2848
+ return executable;
2849
+ }
2850
+ }
2851
+ return null;
2852
+ }
2853
+ return workerCommandExists('google-chrome') ? 'google-chrome' : null;
2854
+ }
2855
+ const MAC_CHROME_AUTOMATION_PREFLIGHT_APPLESCRIPT = `
2856
+ if application "Google Chrome" is not running then
2857
+ return "not_running"
2858
+ end if
2859
+ tell application "Google Chrome"
2860
+ count of windows
2861
+ end tell
2862
+ return "granted"
2863
+ `;
2864
+ function probeMacChromeAutomationPermission(options = {}) {
2865
+ if ((options.platform ?? node_process_1.default.platform) !== 'darwin') {
2866
+ return 'not_applicable';
2867
+ }
2868
+ const runner = options.runner ?? ((command, args) => {
2869
+ const result = (0, node_child_process_1.spawnSync)(command, args, { encoding: 'utf8', timeout: 10000 });
2870
+ return {
2871
+ status: result.status,
2872
+ stdout: result.stdout ?? '',
2873
+ stderr: result.stderr ?? '',
2874
+ };
2875
+ });
2876
+ const result = runner('osascript', ['-e', MAC_CHROME_AUTOMATION_PREFLIGHT_APPLESCRIPT]);
2877
+ const output = result.stdout.trim();
2878
+ if (result.status === 0 && output === 'granted') {
2879
+ return 'granted';
2880
+ }
2881
+ if (result.status === 0 && output === 'not_running') {
2882
+ return 'not_running';
2883
+ }
2884
+ return 'denied';
2885
+ }
2886
+ function collectWorkerInfrastructureFailures() {
2887
+ const failures = [];
2888
+ if (!resolveInstalledChromeExecutable()) {
2889
+ failures.push('worker_preflight_google_chrome_missing');
2890
+ }
2891
+ try {
2892
+ (0, runtime_1.resolvePlaywrightMcpCliPath)();
2893
+ }
2894
+ catch (error) {
2895
+ failures.push(errorCode(error));
2896
+ }
2897
+ if (node_process_1.default.platform !== 'darwin') {
2898
+ return failures;
2899
+ }
2900
+ if (node_process_1.default.arch !== recorderRelease_1.MAC_WORKER_RECORDER_RELEASE.architecture) {
2901
+ failures.push('worker_preflight_recorder_architecture_unsupported');
2902
+ return failures;
2903
+ }
2904
+ if (!workerCommandExists('ffmpeg')) {
2905
+ failures.push('worker_preflight_ffmpeg_missing');
2906
+ }
2907
+ if (!workerCommandExists('ffprobe')) {
2908
+ failures.push('worker_preflight_ffprobe_missing');
2909
+ }
2910
+ const guiSession = (0, shellProbe_1.runShell)('/bin/launchctl print "gui/$(/usr/bin/id -u)"');
2911
+ if (guiSession.exitCode !== 0) {
2912
+ failures.push('worker_preflight_gui_session_missing');
2913
+ }
2914
+ if (probeMacChromeAutomationPermission() === 'denied') {
2915
+ failures.push('worker_preflight_chrome_automation_permission_missing');
2916
+ }
2917
+ const recorderPath = (0, recorderRelease_1.resolveMacWorkerRecorderPath)();
2918
+ if (!(0, node_fs_1.existsSync)(recorderPath)) {
2919
+ failures.push('worker_preflight_recorder_missing');
2920
+ return failures;
2921
+ }
2922
+ const recorderStat = (0, node_fs_1.statSync)(recorderPath);
2923
+ if (!recorderStat.isFile() || (recorderStat.mode & 0o111) === 0) {
2924
+ failures.push('worker_preflight_recorder_not_executable');
2925
+ return failures;
2926
+ }
2927
+ const installedSha256 = (0, recorderRelease_1.recorderSha256)((0, node_fs_1.readFileSync)(recorderPath));
2928
+ if (installedSha256 !== recorderRelease_1.MAC_WORKER_RECORDER_RELEASE.sha256) {
2929
+ failures.push('worker_preflight_recorder_hash_mismatch');
2930
+ return failures;
2931
+ }
2932
+ const permission = (0, node_child_process_1.spawnSync)(recorderPath, ['--check-permission'], {
2933
+ encoding: 'utf8',
2934
+ timeout: 10000,
2935
+ });
2936
+ if (permission.status !== 0) {
2937
+ failures.push('worker_preflight_screen_recording_permission_missing');
2938
+ }
2939
+ return failures;
2940
+ }
2941
+ function describeWorkerPreflightReason(reason) {
2942
+ if (reason.startsWith('worker_preflight_recorder_download_failed:')) {
2943
+ return `The PlayDrop macOS recorder could not be downloaded: ${reason.slice('worker_preflight_recorder_download_failed:'.length).trim()}`;
2944
+ }
2945
+ const descriptions = {
2946
+ worker_preflight_google_chrome_missing: 'Google Chrome is missing from its standard installation path.',
2947
+ worker_preflight_playwright_missing: 'The PlayDrop CLI cannot load Playwright.',
2948
+ worker_preflight_chromium_missing: 'The Chromium executable required by Playwright is missing.',
2949
+ worker_preflight_playwright_mcp_missing: 'The pinned Playwright MCP package is missing from the PlayDrop CLI installation.',
2950
+ worker_preflight_playwright_mcp_version: 'The installed Playwright MCP package does not match the PlayDrop CLI release.',
2951
+ worker_preflight_ffmpeg_missing: 'ffmpeg is missing from PATH.',
2952
+ worker_preflight_ffprobe_missing: 'ffprobe is missing from PATH.',
2953
+ worker_preflight_gui_session_missing: 'No active macOS GUI session is available.',
2954
+ worker_preflight_chrome_automation_permission_missing: 'Google Chrome Automation permission is missing. In System Settings > Privacy & Security > Automation, allow the PlayDrop worker process to control Google Chrome, then restart the worker.',
2955
+ worker_preflight_recorder_architecture_unsupported: 'The native recorder is currently available only for Apple silicon Macs.',
2956
+ worker_preflight_recorder_missing: 'The PlayDrop macOS recorder is missing.',
2957
+ worker_preflight_recorder_not_executable: 'The PlayDrop macOS recorder is not executable.',
2958
+ worker_preflight_recorder_hash_mismatch: 'The PlayDrop macOS recorder does not match this CLI release.',
2959
+ worker_preflight_screen_recording_permission_missing: 'Screen Recording permission is not granted to the PlayDrop recorder.',
2960
+ codex_not_authenticated: 'Codex is installed but not authenticated.',
2961
+ codex_models_unavailable: 'Codex is authenticated but reported no usable models.',
2962
+ claude_not_authenticated: 'Claude Code is installed but not authenticated.',
2963
+ claude_models_unavailable: 'Claude Code is authenticated but reported no usable models.',
2964
+ agent_cli_not_found: 'No supported authenticated agent CLI is installed.',
2965
+ };
2966
+ return descriptions[reason] ?? reason;
2967
+ }
2968
+ function formatWorkerPreflightFailure(reasons) {
2969
+ const uniqueReasons = Array.from(new Set(reasons));
2970
+ return new Error(`worker_preflight_failed:\n${uniqueReasons.map((reason) => `- ${describeWorkerPreflightReason(reason)}`).join('\n')}`);
2971
+ }
2836
2972
  function readWorkerTargetUpdateFailure() {
2837
2973
  const failureFile = node_process_1.default.env.PLAYDROP_WORKER_TARGET_UPDATE_FAILURE_FILE?.trim() ?? '';
2838
2974
  if (!failureFile || !(0, node_fs_1.existsSync)(failureFile)) {
@@ -2858,7 +2994,7 @@ function collectWorkerLocalStatus() {
2858
2994
  let claudeVersion = null;
2859
2995
  let claudeModels = [];
2860
2996
  let cursorVersion = null;
2861
- let npmVersion = null;
2997
+ const npmVersion = null;
2862
2998
  let playwright = null;
2863
2999
  let targetUpdateFailure = null;
2864
3000
  try {
@@ -2881,12 +3017,6 @@ function collectWorkerLocalStatus() {
2881
3017
  catch (error) {
2882
3018
  degradedReasons.push(errorCode(error));
2883
3019
  }
2884
- try {
2885
- npmVersion = probeNpmVersion();
2886
- }
2887
- catch (error) {
2888
- degradedReasons.push(errorCode(error));
2889
- }
2890
3020
  try {
2891
3021
  playwright = probePlaywrightChromium();
2892
3022
  if (!playwright.chromiumInstalled) {
@@ -2896,6 +3026,7 @@ function collectWorkerLocalStatus() {
2896
3026
  catch (error) {
2897
3027
  degradedReasons.push(errorCode(error));
2898
3028
  }
3029
+ degradedReasons.push(...collectWorkerInfrastructureFailures());
2899
3030
  try {
2900
3031
  targetUpdateFailure = readWorkerTargetUpdateFailure();
2901
3032
  }
@@ -2929,7 +3060,8 @@ function collectWorkerLocalStatus() {
2929
3060
  degradedReasons.push(errorCode(error));
2930
3061
  }
2931
3062
  const uniqueReasons = Array.from(new Set(degradedReasons));
2932
- const infrastructureReady = Boolean(npmVersion && playwright?.chromiumInstalled);
3063
+ const infrastructureFailures = degradedReasons.filter((reason) => reason.startsWith('worker_preflight_'));
3064
+ const infrastructureReady = Boolean(playwright?.chromiumInstalled) && infrastructureFailures.length === 0;
2933
3065
  const ready = Boolean(capabilities?.ready) && infrastructureReady;
2934
3066
  return {
2935
3067
  ready,
@@ -2949,6 +3081,11 @@ function collectWorkerLocalStatus() {
2949
3081
  function buildWorkerSetupActions(input) {
2950
3082
  const reasons = new Set(input.degradedReasons);
2951
3083
  const actions = [];
3084
+ const addManualAction = (reason, label) => {
3085
+ if (reasons.has(reason)) {
3086
+ actions.push({ reason, label, message: describeWorkerPreflightReason(reason) });
3087
+ }
3088
+ };
2952
3089
  if (reasons.has('worker_preflight_chromium_missing')) {
2953
3090
  const version = input.playwright?.version && input.playwright.version !== 'unknown'
2954
3091
  ? `@${input.playwright.version}`
@@ -2977,6 +3114,18 @@ function buildWorkerSetupActions(input) {
2977
3114
  message: 'Install and authenticate Codex or Claude Code, then rerun playdrop agents status --json.',
2978
3115
  });
2979
3116
  }
3117
+ addManualAction('worker_preflight_google_chrome_missing', 'Install Google Chrome');
3118
+ addManualAction('worker_preflight_playwright_mcp_missing', 'Repair the PlayDrop CLI installation');
3119
+ addManualAction('worker_preflight_playwright_mcp_version', 'Repair the PlayDrop CLI installation');
3120
+ addManualAction('worker_preflight_ffmpeg_missing', 'Install ffmpeg');
3121
+ addManualAction('worker_preflight_ffprobe_missing', 'Install ffprobe');
3122
+ addManualAction('worker_preflight_gui_session_missing', 'Sign in to the Mac desktop');
3123
+ addManualAction('worker_preflight_chrome_automation_permission_missing', 'Grant Google Chrome Automation permission');
3124
+ addManualAction('worker_preflight_recorder_architecture_unsupported', 'Use an Apple silicon Mac');
3125
+ addManualAction('worker_preflight_recorder_missing', 'Restart the worker to download the recorder');
3126
+ addManualAction('worker_preflight_recorder_not_executable', 'Restart the worker to repair the recorder');
3127
+ addManualAction('worker_preflight_recorder_hash_mismatch', 'Restart the worker to repair the recorder');
3128
+ addManualAction('worker_preflight_screen_recording_permission_missing', 'Grant Screen Recording permission');
2980
3129
  return actions;
2981
3130
  }
2982
3131
  function resolveWorkerStateDir() {
@@ -3595,13 +3744,30 @@ TARGET_CLI_VERSION="$(read_policy_value targetCliVersion)"
3595
3744
  MINIMUM_CLI_VERSION="$(read_policy_value minimumCliVersion)"
3596
3745
 
3597
3746
  if [ -n "$TARGET_CLI_VERSION" ]; then
3598
- if npm install -g --no-audit --no-fund "@playdrop/playdrop-cli@$TARGET_CLI_VERSION"; then
3599
- CURRENT_CLI_VERSION="$(current_cli_version)" || CURRENT_CLI_VERSION=""
3600
- if [ "$CURRENT_CLI_VERSION" != "$TARGET_CLI_VERSION" ]; then
3601
- record_target_failure "worker_update_cli_version_mismatch"
3747
+ CURRENT_CLI_VERSION="$(current_cli_version)" || CURRENT_CLI_VERSION=""
3748
+ if [ "$CURRENT_CLI_VERSION" != "$TARGET_CLI_VERSION" ]; then
3749
+ GLOBAL_NPM_ROOT="$(npm root -g)" || GLOBAL_NPM_ROOT=""
3750
+ BROKEN_FSEVENTS_PATH="$GLOBAL_NPM_ROOT/@playdrop/playdrop-cli/node_modules/fsevents"
3751
+ if [ -n "$GLOBAL_NPM_ROOT" ] && [ -f "$BROKEN_FSEVENTS_PATH/package.json" ]; then
3752
+ node - "$BROKEN_FSEVENTS_PATH" <<'NODE' || record_target_failure "worker_update_fsevents_cleanup_failed"
3753
+ const fs = require("fs");
3754
+ const path = process.argv[2];
3755
+ const packageJson = JSON.parse(fs.readFileSync(path + "/package.json", "utf8"));
3756
+ if (packageJson.version === "2.3.2") {
3757
+ fs.rmSync(path, { recursive: true, force: true });
3758
+ }
3759
+ NODE
3760
+ fi
3761
+ if [ "$TARGET_UPDATE_FAILED" -eq 0 ]; then
3762
+ if npm install -g --no-audit --no-fund "@playdrop/playdrop-cli@$TARGET_CLI_VERSION"; then
3763
+ CURRENT_CLI_VERSION="$(current_cli_version)" || CURRENT_CLI_VERSION=""
3764
+ if [ "$CURRENT_CLI_VERSION" != "$TARGET_CLI_VERSION" ]; then
3765
+ record_target_failure "worker_update_cli_version_mismatch"
3766
+ fi
3767
+ else
3768
+ record_target_failure "worker_update_cli_install_failed"
3769
+ fi
3602
3770
  fi
3603
- else
3604
- record_target_failure "worker_update_cli_install_failed"
3605
3771
  fi
3606
3772
  fi
3607
3773
 
@@ -3799,7 +3965,7 @@ async function setupWorker(options = {}) {
3799
3965
  const workerName = explicitWorkerName || `${username} - ${node_os_1.default.hostname()}`;
3800
3966
  const local = collectWorkerLocalStatus();
3801
3967
  const agentReady = local.agents.some((agent) => agent.ready === true);
3802
- const baseRuntimeReady = Boolean(agentReady && local.npmVersion && local.playwright?.chromiumInstalled);
3968
+ const baseRuntimeReady = Boolean(agentReady && local.playwright?.chromiumInstalled);
3803
3969
  if (!baseRuntimeReady) {
3804
3970
  throw new Error(`worker_setup_preflight_failed: ${local.degradedReasons.join(', ') || 'local worker runtime is not ready'}`);
3805
3971
  }
@@ -4037,6 +4203,15 @@ async function enforceInstrumentBrowserHygiene(assignment, options = {}) {
4037
4203
  instrumentBrowserHygieneTail = result.then(() => undefined, () => undefined);
4038
4204
  return result;
4039
4205
  }
4206
+ async function finalizeInstrumentBrowserHygiene(assignment, options = {}) {
4207
+ try {
4208
+ const result = await enforceInstrumentBrowserHygiene(assignment, options);
4209
+ return { ok: true, ...result };
4210
+ }
4211
+ catch (error) {
4212
+ return { ok: false, error: errorMessage(error) };
4213
+ }
4214
+ }
4040
4215
  function assignmentRequiresRegisteredBundle(assignment) {
4041
4216
  const playdropMetadata = assignment.task.metadata.playdrop;
4042
4217
  return Boolean(playdropMetadata
@@ -4082,15 +4257,35 @@ async function startWorker(options = {}) {
4082
4257
  const supervisorLock = acquireWorkerSupervisorLock({ supervisorType, workerName });
4083
4258
  try {
4084
4259
  await removeStaleAgentRunCredentials();
4085
- const npmVersion = probeNpmVersion();
4086
- const playwright = probePlaywrightChromium();
4260
+ const startupFailures = [];
4261
+ if (node_process_1.default.platform === 'darwin') {
4262
+ try {
4263
+ const recorder = await (0, recorderRelease_1.ensureMacWorkerRecorderInstalled)();
4264
+ if (recorder.downloaded) {
4265
+ console.log(`Installed PlayDrop macOS recorder ${recorderRelease_1.MAC_WORKER_RECORDER_RELEASE.version} at ${recorder.path}.`);
4266
+ }
4267
+ }
4268
+ catch (error) {
4269
+ startupFailures.push(`worker_preflight_recorder_download_failed:${errorMessage(error)}`);
4270
+ }
4271
+ }
4272
+ else if (node_process_1.default.platform === 'win32') {
4273
+ console.warn('Native listing recording is not available on Windows yet.');
4274
+ }
4275
+ const local = collectWorkerLocalStatus();
4276
+ startupFailures.push(...local.degradedReasons);
4277
+ if (startupFailures.length > 0 || !local.ready || !local.capabilities || !local.playwright?.chromiumInstalled) {
4278
+ throw formatWorkerPreflightFailure(startupFailures);
4279
+ }
4280
+ const npmVersion = local.npmVersion;
4281
+ const playwright = {
4282
+ version: local.playwright.version ?? 'unknown',
4283
+ chromiumInstalled: true,
4284
+ ...(local.playwright.executablePath ? { executablePath: local.playwright.executablePath } : {}),
4285
+ };
4087
4286
  const workerKey = (0, config_1.getOrCreateWorkerKey)();
4088
4287
  const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
4089
- let capabilities = probeReadyWorkerCapabilities({
4090
- npmVersion,
4091
- playwright,
4092
- maxParallelTasks,
4093
- });
4288
+ let capabilities = local.capabilities;
4094
4289
  let lastCapabilityRefreshAt = Date.now();
4095
4290
  writeWorkerRuntimeState({
4096
4291
  env,
@@ -4962,15 +5157,13 @@ async function startWorker(options = {}) {
4962
5157
  }
4963
5158
  if (agentStartedAt
4964
5159
  && assignment.agent.browser !== 'NONE') {
4965
- try {
4966
- const hygiene = await enforceInstrumentBrowserHygiene(assignment);
5160
+ const hygiene = await finalizeInstrumentBrowserHygiene(assignment);
5161
+ if (hygiene.ok) {
4967
5162
  console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
4968
5163
  }
4969
- catch (hygieneError) {
4970
- const error = hygieneError instanceof Error ? hygieneError : new Error(String(hygieneError));
4971
- console.error(`Agent task ${task.id} browser hygiene failed: ${error.message}`);
5164
+ else {
5165
+ console.error(`Agent task ${task.id} browser hygiene failed; worker continuing: ${hygiene.error}`);
4972
5166
  retainWorkspace = true;
4973
- recordFatalTaskError(error);
4974
5167
  }
4975
5168
  }
4976
5169
  activeTerminators.delete(task.id);
@@ -0,0 +1,18 @@
1
+ export declare const MAC_WORKER_RECORDER_RELEASE: {
2
+ readonly version: 1;
3
+ readonly architecture: "arm64";
4
+ readonly url: "https://assets.playdrop.ai/cli/recorder/1/darwin-arm64/playdrop-listing-recorder";
5
+ readonly sha256: "0a19f0613b8b69945b532d9bc75a52803cd84c5977c78da76a446ecd2d29f715";
6
+ };
7
+ export declare function resolveWorkerHomeDirectory(): string;
8
+ export declare function resolveMacWorkerRecorderPath(workerHome?: string): string;
9
+ export declare function recorderSha256(bytes: Uint8Array): string;
10
+ export declare function ensureMacWorkerRecorderInstalled(input?: {
11
+ workerHome?: string;
12
+ download?: (url: string) => Promise<Buffer>;
13
+ platform?: NodeJS.Platform;
14
+ architecture?: string;
15
+ }): Promise<{
16
+ path: string;
17
+ downloaded: boolean;
18
+ }>;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAC_WORKER_RECORDER_RELEASE = void 0;
7
+ exports.resolveWorkerHomeDirectory = resolveWorkerHomeDirectory;
8
+ exports.resolveMacWorkerRecorderPath = resolveMacWorkerRecorderPath;
9
+ exports.recorderSha256 = recorderSha256;
10
+ exports.ensureMacWorkerRecorderInstalled = ensureMacWorkerRecorderInstalled;
11
+ const node_crypto_1 = require("node:crypto");
12
+ const promises_1 = require("node:fs/promises");
13
+ const node_os_1 = __importDefault(require("node:os"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ exports.MAC_WORKER_RECORDER_RELEASE = {
16
+ version: 1,
17
+ architecture: 'arm64',
18
+ url: 'https://assets.playdrop.ai/cli/recorder/1/darwin-arm64/playdrop-listing-recorder',
19
+ sha256: '0a19f0613b8b69945b532d9bc75a52803cd84c5977c78da76a446ecd2d29f715',
20
+ };
21
+ const MAX_RECORDER_DOWNLOAD_BYTES = 10 * 1024 * 1024;
22
+ const RECORDER_DOWNLOAD_TIMEOUT_MS = 30000;
23
+ function resolveWorkerHomeDirectory() {
24
+ const configured = process.env.PLAYDROP_WORKER_HOME?.trim();
25
+ return configured ? node_path_1.default.resolve(configured) : node_path_1.default.join(node_os_1.default.homedir(), 'PlayDropWorker');
26
+ }
27
+ function resolveMacWorkerRecorderPath(workerHome = resolveWorkerHomeDirectory()) {
28
+ return node_path_1.default.join(node_path_1.default.resolve(workerHome), 'tools', 'playdrop-listing-recorder');
29
+ }
30
+ function recorderSha256(bytes) {
31
+ return (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
32
+ }
33
+ async function downloadRecorder(url) {
34
+ const controller = new AbortController();
35
+ const timeout = setTimeout(() => controller.abort(), RECORDER_DOWNLOAD_TIMEOUT_MS);
36
+ try {
37
+ const response = await fetch(url, { signal: controller.signal });
38
+ if (!response.ok) {
39
+ throw new Error(`worker_recorder_download_http_${response.status}`);
40
+ }
41
+ const bytes = Buffer.from(await response.arrayBuffer());
42
+ if (bytes.length === 0 || bytes.length > MAX_RECORDER_DOWNLOAD_BYTES) {
43
+ throw new Error(`worker_recorder_download_size_invalid:${bytes.length}`);
44
+ }
45
+ return bytes;
46
+ }
47
+ finally {
48
+ clearTimeout(timeout);
49
+ }
50
+ }
51
+ async function ensureMacWorkerRecorderInstalled(input = {}) {
52
+ const platform = input.platform ?? process.platform;
53
+ const architecture = input.architecture ?? process.arch;
54
+ if (platform !== 'darwin') {
55
+ throw new Error(`worker_recorder_platform_unsupported:${platform}`);
56
+ }
57
+ if (architecture !== exports.MAC_WORKER_RECORDER_RELEASE.architecture) {
58
+ throw new Error(`worker_recorder_architecture_unsupported:${architecture}`);
59
+ }
60
+ const recorderPath = resolveMacWorkerRecorderPath(input.workerHome);
61
+ let installedBytes = null;
62
+ try {
63
+ installedBytes = await (0, promises_1.readFile)(recorderPath);
64
+ }
65
+ catch (error) {
66
+ if (error.code !== 'ENOENT') {
67
+ throw error;
68
+ }
69
+ }
70
+ if (installedBytes && recorderSha256(installedBytes) === exports.MAC_WORKER_RECORDER_RELEASE.sha256) {
71
+ await (0, promises_1.chmod)(recorderPath, 0o755);
72
+ return { path: recorderPath, downloaded: false };
73
+ }
74
+ const bytes = await (input.download ?? downloadRecorder)(exports.MAC_WORKER_RECORDER_RELEASE.url);
75
+ const actualSha256 = recorderSha256(bytes);
76
+ if (actualSha256 !== exports.MAC_WORKER_RECORDER_RELEASE.sha256) {
77
+ throw new Error(`worker_recorder_hash_mismatch:expected=${exports.MAC_WORKER_RECORDER_RELEASE.sha256}:actual=${actualSha256}`);
78
+ }
79
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(recorderPath), { recursive: true });
80
+ const temporaryPath = `${recorderPath}.${process.pid}.${Date.now()}.tmp`;
81
+ try {
82
+ await (0, promises_1.writeFile)(temporaryPath, bytes, { mode: 0o755 });
83
+ await (0, promises_1.chmod)(temporaryPath, 0o755);
84
+ await (0, promises_1.rename)(temporaryPath, recorderPath);
85
+ }
86
+ finally {
87
+ await (0, promises_1.rm)(temporaryPath, { force: true });
88
+ }
89
+ return { path: recorderPath, downloaded: true };
90
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.0",
2
+ "version": "0.13.2",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.0",
4
+ "runtimeSdkVersion": "0.13.2",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playdrop/playdrop-cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
5
5
  "homepage": "https://www.playdrop.ai",
6
6
  "repository": {
@@ -78,6 +78,7 @@
78
78
  "@playdrop/config": "file:../config",
79
79
  "@playdrop/types": "file:../types",
80
80
  "@playdrop/vox-three": "file:../vox-three",
81
+ "@playwright/mcp": "0.0.78",
81
82
  "ajv": "^8.17.1",
82
83
  "commander": "^11.1.0",
83
84
  "entities": "^6.0.0",