@playdrop/playdrop-cli 0.10.19 → 0.10.21

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,10 +1,14 @@
1
1
  {
2
- "version": "0.10.19",
2
+ "version": "0.10.21",
3
3
  "build": 1,
4
4
  "runtimeSdkVersion": "0.10.0",
5
5
  "clients": {
6
6
  "all": {
7
7
  "minimumVersion": "0.7.4"
8
+ },
9
+ "windows-playdrop": {
10
+ "minimumVersion": "0.9.6",
11
+ "minimumBuild": 1
8
12
  }
9
13
  }
10
14
  }
@@ -33,9 +33,10 @@ const projectBuildCache = new Map();
33
33
  const typescriptSourceCache = new Map();
34
34
  async function runNpmScript(projectDir, script) {
35
35
  await (0, promises_1.mkdir)(projectDir, { recursive: true });
36
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
36
+ const npmCommand = process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : 'npm';
37
+ const npmArgs = process.platform === 'win32' ? ['/d', '/s', '/c', `npm run ${script}`] : ['run', script];
37
38
  await new Promise((resolveRun, rejectRun) => {
38
- const child = (0, node_child_process_1.spawn)(npmCommand, ['run', script], {
39
+ const child = (0, node_child_process_1.spawn)(npmCommand, npmArgs, {
39
40
  cwd: projectDir,
40
41
  stdio: 'inherit',
41
42
  env: { ...process.env },
@@ -88,11 +88,16 @@ async function detectCli(command, runShell) {
88
88
  if (pathProbe.exitCode !== 0 || pathProbe.output.trim().length === 0) {
89
89
  return { installed: false };
90
90
  }
91
+ const path = pathProbe.output.split('\n')[0]?.trim();
91
92
  const versionProbe = await runShell(`${command} --version`);
93
+ const version = versionProbe.output.split('\n')[0]?.trim() || undefined;
94
+ if (versionProbe.exitCode !== 0 && !/\d+\.\d+\.\d+/.test(versionProbe.output)) {
95
+ return { installed: false, path };
96
+ }
92
97
  return {
93
98
  installed: true,
94
- path: pathProbe.output.split('\n')[0]?.trim(),
95
- version: versionProbe.exitCode === 0 ? versionProbe.output.split('\n')[0]?.trim() : undefined,
99
+ path,
100
+ version,
96
101
  };
97
102
  }
98
103
  function detectApp(appNames, homeDir, appExists) {
@@ -285,6 +290,9 @@ function printAgentsStatus(status) {
285
290
  }
286
291
  }
287
292
  function runCodexPluginAction(action, runCommand) {
293
+ if (!ensureAgentCliUsable('codex', runCommand)) {
294
+ return;
295
+ }
288
296
  const marketplaceCommand = action === 'install-plugin'
289
297
  ? ['plugin', 'marketplace', 'add', PLAYDROP_PLUGIN_MARKETPLACE]
290
298
  : ['plugin', 'marketplace', 'upgrade', 'playdrop'];
@@ -340,6 +348,9 @@ function runCodexPluginAction(action, runCommand) {
340
348
  console.log('Open Codex and run /plugins, then install or enable PlayDrop.');
341
349
  }
342
350
  function runClaudePluginAction(action, runCommand) {
351
+ if (!ensureAgentCliUsable('claude', runCommand)) {
352
+ return;
353
+ }
343
354
  if (action === 'install-plugin') {
344
355
  const marketplace = runCommand('claude', ['plugin', 'marketplace', 'add', PLAYDROP_PLUGIN_MARKETPLACE]);
345
356
  if (marketplace.status !== 0) {
@@ -379,6 +390,21 @@ function runClaudePluginAction(action, runCommand) {
379
390
  console.log('Updated the PlayDrop plugin for Claude.');
380
391
  console.log('Run /reload-plugins in Claude Code or restart Claude Code to apply it.');
381
392
  }
393
+ function ensureAgentCliUsable(command, runCommand) {
394
+ const result = runCommand(command, ['--version']);
395
+ const output = commandOutput(result).trim();
396
+ if (result.status === 0 || /\d+\.\d+\.\d+/.test(output)) {
397
+ return true;
398
+ }
399
+ const label = command === 'codex' ? 'Codex' : 'Claude';
400
+ console.error(`${label} CLI is missing or not runnable.`);
401
+ console.error(`${command} --version failed with code ${result.status ?? 'unknown'}.`);
402
+ if (output) {
403
+ console.error(`Output: ${output}`);
404
+ }
405
+ process.exitCode = 1;
406
+ return false;
407
+ }
382
408
  function readInstalledPlayDropPlugin(command, runCommand) {
383
409
  const list = runCommand(command, ['plugin', 'list', '--json']);
384
410
  if (list.status !== 0) {
@@ -477,9 +503,10 @@ async function runLoginShell(command) {
477
503
  }
478
504
  function runCommandSync(command, args) {
479
505
  const result = (0, node_child_process_1.spawnSync)(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
506
+ const spawnError = result.error instanceof Error ? result.error.message : '';
480
507
  return {
481
508
  status: result.status,
482
509
  stdout: result.stdout ?? '',
483
- stderr: result.stderr ?? '',
510
+ stderr: [result.stderr ?? '', spawnError].filter(Boolean).join('\n'),
484
511
  };
485
512
  }
@@ -17,6 +17,9 @@ const output_1 = require("../output");
17
17
  const versionCompare_1 = require("../versionCompare");
18
18
  const MAC_APP_PATH = '/Applications/PlayDrop.app';
19
19
  const MAC_CLIENT_LATEST_URL = 'https://www.playdrop.ai/api/mac-client/latest';
20
+ const WINDOWS_CLIENT_EXECUTABLE = 'PlayDrop.exe';
21
+ const WINDOWS_CLIENT_INSTALL_HINT = 'Install or update PlayDrop for Windows with the Windows installer or the in-app updater.';
22
+ const WINDOWS_CLIENT_PATH_ENV = 'PLAYDROP_WINDOWS_CLIENT_PATH';
20
23
  async function showClientStatus(options = {}) {
21
24
  const status = await getClientStatus(options);
22
25
  if (options.json) {
@@ -29,6 +32,9 @@ async function showClientStatus(options = {}) {
29
32
  }
30
33
  async function getClientStatus(options = {}) {
31
34
  const platform = options.platform ?? process.platform;
35
+ if (platform === 'win32') {
36
+ return getWindowsClientStatus(options);
37
+ }
32
38
  if (platform !== 'darwin') {
33
39
  return {
34
40
  platform,
@@ -65,6 +71,24 @@ async function updateClient(options = {}) {
65
71
  }
66
72
  async function launchClient(options = {}) {
67
73
  const platform = options.platform ?? process.platform;
74
+ if (platform === 'win32') {
75
+ const status = await getWindowsClientStatus(options);
76
+ if (!status.installed || !status.appPath) {
77
+ console.error('PlayDrop for Windows is not installed.');
78
+ console.error(WINDOWS_CLIENT_INSTALL_HINT);
79
+ process.exitCode = 1;
80
+ return;
81
+ }
82
+ const runCommand = options.runCommand ?? runCommandSync;
83
+ const result = runCommand(status.appPath, []);
84
+ if (result.status !== 0) {
85
+ console.error(`Failed to launch PlayDrop from ${status.appPath}.`);
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ console.log('Launched PlayDrop.');
90
+ return;
91
+ }
68
92
  if (platform !== 'darwin') {
69
93
  printUnsupportedPlatform(platform);
70
94
  return;
@@ -87,6 +111,12 @@ async function launchClient(options = {}) {
87
111
  }
88
112
  async function installOrUpdateClient(action, options) {
89
113
  const platform = options.platform ?? process.platform;
114
+ if (platform === 'win32') {
115
+ console.error(`PlayDrop for Windows ${action} is not available through the CLI yet.`);
116
+ console.error(WINDOWS_CLIENT_INSTALL_HINT);
117
+ process.exitCode = 1;
118
+ return;
119
+ }
90
120
  if (platform !== 'darwin') {
91
121
  printUnsupportedPlatform(platform);
92
122
  return;
@@ -129,7 +159,12 @@ function printClientStatus(status) {
129
159
  }
130
160
  if (!status.installed) {
131
161
  console.log('PlayDrop client: missing');
132
- console.log('Run: playdrop clients install');
162
+ if (status.nextAction) {
163
+ console.log(`Run: ${status.nextAction}`);
164
+ }
165
+ else if (status.reason) {
166
+ console.log(status.reason);
167
+ }
133
168
  return;
134
169
  }
135
170
  const version = status.version ? ` ${status.version}` : '';
@@ -141,6 +176,68 @@ function printClientStatus(status) {
141
176
  console.log('Run: playdrop clients update');
142
177
  }
143
178
  }
179
+ async function getWindowsClientStatus(options) {
180
+ const appPath = resolveWindowsClientPath(options.appPath);
181
+ if (!appPath) {
182
+ return {
183
+ platform: 'win32',
184
+ supported: true,
185
+ status: 'missing',
186
+ installed: false,
187
+ reason: WINDOWS_CLIENT_INSTALL_HINT,
188
+ };
189
+ }
190
+ const bundle = readWindowsClientInfo(appPath);
191
+ return {
192
+ platform: 'win32',
193
+ supported: true,
194
+ status: 'installed',
195
+ installed: true,
196
+ appPath,
197
+ version: bundle.version,
198
+ build: bundle.build,
199
+ updateAvailable: false,
200
+ };
201
+ }
202
+ function resolveWindowsClientPath(explicitPath) {
203
+ const candidates = windowsClientPathCandidates(explicitPath);
204
+ return candidates.find((candidate) => (0, node_fs_1.existsSync)(candidate));
205
+ }
206
+ function windowsClientPathCandidates(explicitPath) {
207
+ const candidates = [];
208
+ const add = (value) => {
209
+ if (value && value.trim().length > 0) {
210
+ candidates.push(value.trim());
211
+ }
212
+ };
213
+ add(explicitPath);
214
+ add(process.env[WINDOWS_CLIENT_PATH_ENV]);
215
+ const localAppData = process.env.LOCALAPPDATA;
216
+ if (localAppData) {
217
+ add((0, node_path_1.join)(localAppData, 'PlayDrop.Windows', 'current', WINDOWS_CLIENT_EXECUTABLE));
218
+ add((0, node_path_1.join)(localAppData, 'PlayDrop', 'current', WINDOWS_CLIENT_EXECUTABLE));
219
+ }
220
+ add(process.env.ProgramFiles ? (0, node_path_1.join)(process.env.ProgramFiles, 'PlayDrop', WINDOWS_CLIENT_EXECUTABLE) : undefined);
221
+ add(process.env['ProgramFiles(x86)'] ? (0, node_path_1.join)(process.env['ProgramFiles(x86)'], 'PlayDrop', WINDOWS_CLIENT_EXECUTABLE) : undefined);
222
+ return [...new Set(candidates)];
223
+ }
224
+ function readWindowsClientInfo(appPath) {
225
+ for (const versionPath of [
226
+ (0, node_path_1.join)((0, node_path_1.dirname)(appPath), 'sq.version'),
227
+ (0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.dirname)(appPath)), 'sq.version'),
228
+ ]) {
229
+ try {
230
+ const version = (0, node_fs_1.readFileSync)(versionPath, 'utf8').trim();
231
+ if (version.length > 0) {
232
+ return { version };
233
+ }
234
+ }
235
+ catch {
236
+ // Development and portable builds may not have a Velopack version file.
237
+ }
238
+ }
239
+ return {};
240
+ }
144
241
  async function fetchLatestMacClientManifest() {
145
242
  try {
146
243
  const response = await fetch(MAC_CLIENT_LATEST_URL, { headers: { Accept: 'application/json' } });
@@ -22,6 +22,14 @@ interface CliHealth {
22
22
  updateStatus: 'current' | 'update-available' | 'unknown';
23
23
  updateAvailable?: boolean;
24
24
  nextAction?: string;
25
+ command?: CliCommandHealth;
26
+ }
27
+ interface CliCommandHealth {
28
+ available: boolean;
29
+ path?: string;
30
+ npmGlobalBin?: string;
31
+ npmShimPath?: string;
32
+ issue?: 'not-on-path' | 'shim-missing';
25
33
  }
26
34
  export interface DoctorReport {
27
35
  cli: CliHealth;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.showDoctor = showDoctor;
4
4
  const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
5
6
  const clientInfo_1 = require("../clientInfo");
6
7
  const config_1 = require("../config");
7
8
  const output_1 = require("../output");
@@ -61,11 +62,13 @@ async function getAccountHealth() {
61
62
  }
62
63
  async function getCliHealth() {
63
64
  const currentVersion = (0, clientInfo_1.getCliVersion)();
65
+ const command = getCliCommandHealth();
64
66
  const result = (0, shellProbe_1.runShell)('npm view @playdrop/playdrop-cli version --json');
65
67
  if (result.exitCode !== 0 || !result.output.trim()) {
66
68
  return {
67
69
  version: (0, clientInfo_1.getCliVersionLabel)(),
68
70
  updateStatus: 'unknown',
71
+ command,
69
72
  };
70
73
  }
71
74
  const latestVersion = parseNpmVersion(result.output);
@@ -73,6 +76,7 @@ async function getCliHealth() {
73
76
  return {
74
77
  version: (0, clientInfo_1.getCliVersionLabel)(),
75
78
  updateStatus: 'unknown',
79
+ command,
76
80
  };
77
81
  }
78
82
  const updateAvailable = (0, versionCompare_1.firstVersionIsNewer)(latestVersion, currentVersion);
@@ -82,8 +86,34 @@ async function getCliHealth() {
82
86
  updateStatus: updateAvailable ? 'update-available' : 'current',
83
87
  updateAvailable,
84
88
  nextAction: updateAvailable ? 'playdrop update' : undefined,
89
+ command,
85
90
  };
86
91
  }
92
+ function getCliCommandHealth(platform = process.platform) {
93
+ const pathProbe = (0, shellProbe_1.runShell)((0, shellProbe_1.buildCommandPathProbe)('playdrop', platform), platform);
94
+ if (pathProbe.exitCode === 0 && pathProbe.output.trim()) {
95
+ return {
96
+ available: true,
97
+ path: pathProbe.output.split('\n')[0]?.trim(),
98
+ };
99
+ }
100
+ if (platform !== 'win32') {
101
+ return { available: false, issue: 'not-on-path' };
102
+ }
103
+ const npmGlobalBin = resolveNpmGlobalBin(platform);
104
+ const npmShimPath = npmGlobalBin ? (0, node_path_1.join)(npmGlobalBin, 'playdrop.cmd') : undefined;
105
+ const shimExists = npmShimPath ? (0, node_fs_1.existsSync)(npmShimPath) : false;
106
+ return {
107
+ available: false,
108
+ npmGlobalBin,
109
+ npmShimPath: shimExists ? npmShimPath : undefined,
110
+ issue: shimExists ? 'not-on-path' : 'shim-missing',
111
+ };
112
+ }
113
+ function resolveNpmGlobalBin(platform) {
114
+ const prefix = (0, shellProbe_1.runShell)('npm config get prefix', platform).output.split('\n')[0]?.trim();
115
+ return prefix && prefix.length > 0 ? prefix : undefined;
116
+ }
87
117
  function parseNpmVersion(output) {
88
118
  const trimmed = output.trim();
89
119
  try {
@@ -164,6 +194,7 @@ function printDoctorReport(report) {
164
194
  ? ', current'
165
195
  : ', update status unknown';
166
196
  console.log(`PlayDrop CLI: ${report.cli.version}${cliUpdate}`);
197
+ printCliCommandDoctorStatus(report.cli.command);
167
198
  console.log(`Account: ${report.account.loggedIn ? report.account.identity ?? 'logged in' : 'not logged in'}`);
168
199
  console.log(`Node: ${report.tools.node.installed ? report.tools.node.version ?? 'installed' : 'missing'}`);
169
200
  console.log(`npm: ${report.tools.npm.installed ? report.tools.npm.version ?? 'installed' : 'missing'}`);
@@ -192,6 +223,22 @@ function printDoctorReport(report) {
192
223
  }
193
224
  }
194
225
  }
226
+ function printCliCommandDoctorStatus(command) {
227
+ if (!command) {
228
+ return;
229
+ }
230
+ if (command.available) {
231
+ console.log(`CLI command: ${command.path ?? 'available'}`);
232
+ return;
233
+ }
234
+ console.log('CLI command: not on PATH');
235
+ if (command.npmShimPath) {
236
+ console.log(` Direct npm shim: ${command.npmShimPath}`);
237
+ }
238
+ if (command.npmGlobalBin) {
239
+ console.log(` Add ${command.npmGlobalBin} to PATH for new PowerShell sessions.`);
240
+ }
241
+ }
195
242
  function printNativeClientDoctorStatus(status) {
196
243
  if (!status.supported) {
197
244
  console.log(`Native client: unavailable on ${status.platform}`);
@@ -199,6 +246,9 @@ function printNativeClientDoctorStatus(status) {
199
246
  }
200
247
  if (!status.installed) {
201
248
  console.log('Native client: missing');
249
+ if (status.reason) {
250
+ console.log(` ${status.reason}`);
251
+ }
202
252
  return;
203
253
  }
204
254
  const statusLabel = status.updateAvailable ? 'update available' : 'up to date';
@@ -419,6 +419,9 @@ function resolveImageAttachment(value, optionName) {
419
419
  return undefined;
420
420
  }
421
421
  const trimmed = value.trim();
422
+ if ((0, node_path_1.isAbsolute)(trimmed)) {
423
+ return resolveLocalImageAttachment(optionName, trimmed);
424
+ }
422
425
  try {
423
426
  const parsed = new URL(trimmed);
424
427
  if (parsed.protocol !== 'https:') {
@@ -525,6 +525,12 @@ function normalizeClaudePermissionPath(value) {
525
525
  if (!trimmed) {
526
526
  return null;
527
527
  }
528
+ if (/^\/[^/]/.test(trimmed)) {
529
+ return node_path_1.default.posix.resolve(trimmed).replace(/\/+$/, '');
530
+ }
531
+ if (/^[A-Za-z]:[\\/]/.test(trimmed) || /^\\\\/.test(trimmed)) {
532
+ return node_path_1.default.win32.resolve(trimmed).replace(/\\/g, '/').replace(/\/+$/, '');
533
+ }
528
534
  return node_path_1.default.resolve(trimmed).replace(/\\/g, '/').replace(/\/+$/, '');
529
535
  }
530
536
  function buildClaudePermissionSettings(denyReadRoots = []) {
@@ -31,6 +31,7 @@ type WorkerLocalStatus = {
31
31
  playwright: {
32
32
  version?: string;
33
33
  chromiumInstalled?: boolean;
34
+ executablePath?: string;
34
35
  } | null;
35
36
  plugin: {
36
37
  ready: boolean;
@@ -40,6 +41,13 @@ type WorkerLocalStatus = {
40
41
  };
41
42
  agents: AgentWorkerCapabilities['agents'];
42
43
  capabilities: AgentWorkerCapabilities | null;
44
+ setupActions: WorkerSetupAction[];
45
+ };
46
+ type WorkerSetupAction = {
47
+ reason: string;
48
+ label: string;
49
+ command?: string;
50
+ message: string;
43
51
  };
44
52
  type WorkerStatusPayload = {
45
53
  schemaVersion: 1;
@@ -205,6 +213,14 @@ export declare function createLocalPlaydropShim(input: {
205
213
  attempt: number;
206
214
  devPort: number;
207
215
  }): Promise<string>;
216
+ export declare function buildWorkerSetupActions(input: {
217
+ degradedReasons: string[];
218
+ playwright?: {
219
+ version?: string;
220
+ chromiumInstalled?: boolean;
221
+ executablePath?: string;
222
+ } | null;
223
+ }): WorkerSetupAction[];
208
224
  type WorkerRuntimeState = {
209
225
  schemaVersion: 1;
210
226
  pid: number;
@@ -38,6 +38,7 @@ exports.assertWorkerProjectVersionBumped = assertWorkerProjectVersionBumped;
38
38
  exports.buildAgentFailureCode = buildAgentFailureCode;
39
39
  exports.describeAgentFailureForEvent = describeAgentFailureForEvent;
40
40
  exports.createLocalPlaydropShim = createLocalPlaydropShim;
41
+ exports.buildWorkerSetupActions = buildWorkerSetupActions;
41
42
  exports.readActivePersonalWorkerRuntimeState = readActivePersonalWorkerRuntimeState;
42
43
  exports.parseLaunchAgentProgramArguments = parseLaunchAgentProgramArguments;
43
44
  exports.assertLaunchAgentPlistCompatible = assertLaunchAgentPlistCompatible;
@@ -1181,9 +1182,9 @@ function probePlaywrightChromium() {
1181
1182
  const version = typeof packageJson.version === 'string' && packageJson.version.trim() ? packageJson.version.trim() : 'unknown';
1182
1183
  const chromiumInstalled = (0, node_fs_1.existsSync)(executablePath);
1183
1184
  if (!chromiumInstalled) {
1184
- throw new Error(`worker_preflight_chromium_missing: install Playwright Chromium before starting the worker (${executablePath}).`);
1185
+ return { version, chromiumInstalled: false, executablePath };
1185
1186
  }
1186
- return { version, chromiumInstalled };
1187
+ return { version, chromiumInstalled: true, executablePath };
1187
1188
  }
1188
1189
  function readWorkerModelList(agent, envName) {
1189
1190
  const raw = node_process_1.default.env[envName];
@@ -1260,7 +1261,10 @@ function buildWorkerCapabilities(input) {
1260
1261
  const cursorVersion = typeof input === 'string' ? null : input.cursorVersion?.trim() || null;
1261
1262
  const cursorAuthenticated = typeof input === 'string' ? false : input.cursorAuthenticated === true;
1262
1263
  const npmVersion = typeof input === 'string' ? null : input.npmVersion?.trim() || null;
1263
- const playwright = typeof input === 'string' ? null : input.playwright ?? null;
1264
+ const rawPlaywright = typeof input === 'string' ? null : input.playwright ?? null;
1265
+ const playwright = rawPlaywright
1266
+ ? { version: rawPlaywright.version, chromiumInstalled: rawPlaywright.chromiumInstalled }
1267
+ : null;
1264
1268
  const maxParallelTasks = typeof input === 'string'
1265
1269
  ? DEFAULT_WORKER_MAX_PARALLEL_TASKS
1266
1270
  : input.maxParallelTasks ?? DEFAULT_WORKER_MAX_PARALLEL_TASKS;
@@ -1954,6 +1958,9 @@ function probeCodexInstallation() {
1954
1958
  const versionResult = (0, shellProbe_1.runShell)('codex --version');
1955
1959
  const match = /(\d+\.\d+\.\d+\S*)/.exec(versionResult.output);
1956
1960
  if (!match?.[1]) {
1961
+ if (versionResult.exitCode !== 0) {
1962
+ return null;
1963
+ }
1957
1964
  throw new Error(`codex_version_check_failed: "codex --version" exited with code ${versionResult.exitCode} and reported no version. Output: ${versionResult.output.slice(0, 200)}`);
1958
1965
  }
1959
1966
  if (versionResult.exitCode !== 0) {
@@ -1970,7 +1977,13 @@ function probeClaudeInstallation() {
1970
1977
  }
1971
1978
  const versionResult = (0, shellProbe_1.runShell)('claude --version');
1972
1979
  const match = /(\d+\.\d+\.\d+\S*)/.exec(versionResult.output);
1973
- if (!match?.[1] || versionResult.exitCode !== 0) {
1980
+ if (!match?.[1]) {
1981
+ if (versionResult.exitCode !== 0) {
1982
+ return null;
1983
+ }
1984
+ throw new Error(`claude_version_check_failed: "claude --version" exited with code ${versionResult.exitCode} and reported no version. Output: ${versionResult.output.slice(0, 200)}`);
1985
+ }
1986
+ if (versionResult.exitCode !== 0) {
1974
1987
  throw new Error(`claude_version_check_failed: "claude --version" exited with code ${versionResult.exitCode} and reported no version. Output: ${versionResult.output.slice(0, 200)}`);
1975
1988
  }
1976
1989
  return { claudeVersion: match[1] };
@@ -2076,6 +2089,9 @@ function collectWorkerLocalStatus() {
2076
2089
  }
2077
2090
  try {
2078
2091
  playwright = probePlaywrightChromium();
2092
+ if (!playwright.chromiumInstalled) {
2093
+ degradedReasons.push('worker_preflight_chromium_missing');
2094
+ }
2079
2095
  }
2080
2096
  catch (error) {
2081
2097
  degradedReasons.push(errorCode(error));
@@ -2143,8 +2159,53 @@ function collectWorkerLocalStatus() {
2143
2159
  },
2144
2160
  agents: capabilities?.agents ?? [],
2145
2161
  capabilities,
2162
+ setupActions: buildWorkerSetupActions({
2163
+ degradedReasons: uniqueReasons,
2164
+ playwright,
2165
+ }),
2146
2166
  };
2147
2167
  }
2168
+ function buildWorkerSetupActions(input) {
2169
+ const reasons = new Set(input.degradedReasons);
2170
+ const actions = [];
2171
+ if (reasons.has('worker_preflight_chromium_missing')) {
2172
+ const version = input.playwright?.version && input.playwright.version !== 'unknown'
2173
+ ? `@${input.playwright.version}`
2174
+ : '';
2175
+ actions.push({
2176
+ reason: 'worker_preflight_chromium_missing',
2177
+ label: 'Install Playwright Chromium',
2178
+ command: `npx playwright${version} install chromium`,
2179
+ message: input.playwright?.executablePath
2180
+ ? `Chromium is required for PlayDrop launch checks. Expected browser path: ${input.playwright.executablePath}.`
2181
+ : 'Chromium is required for PlayDrop launch checks.',
2182
+ });
2183
+ }
2184
+ if (reasons.has('worker_preflight_playwright_missing')) {
2185
+ actions.push({
2186
+ reason: 'worker_preflight_playwright_missing',
2187
+ label: 'Repair the PlayDrop CLI install',
2188
+ command: 'npm install -g @playdrop/playdrop-cli@latest',
2189
+ message: 'The PlayDrop CLI package could not load playwright-core from its installed dependencies.',
2190
+ });
2191
+ }
2192
+ if (reasons.has('playdrop_plugin_staging_manifest_missing')) {
2193
+ actions.push({
2194
+ reason: 'playdrop_plugin_staging_manifest_missing',
2195
+ label: 'Install or update the PlayDrop agent plugin',
2196
+ command: 'playdrop agents status --json',
2197
+ message: 'Run the CLI-reported nextAction.command for a supported local agent so playdrop-worker-staging.json is available.',
2198
+ });
2199
+ }
2200
+ if (reasons.has('agent_cli_not_found')) {
2201
+ actions.push({
2202
+ reason: 'agent_cli_not_found',
2203
+ label: 'Install a supported agent CLI',
2204
+ message: 'Install and authenticate Codex or Claude Code, then rerun playdrop agents status --json.',
2205
+ });
2206
+ }
2207
+ return actions;
2208
+ }
2148
2209
  function getWorkerStateDir() {
2149
2210
  return node_path_1.default.join(node_os_1.default.homedir(), '.playdrop', 'worker');
2150
2211
  }
@@ -2782,6 +2843,13 @@ function printWorkerStatus(payload) {
2782
2843
  if (payload.local.degradedReasons.length > 0) {
2783
2844
  console.log(` Reasons: ${payload.local.degradedReasons.join(', ')}`);
2784
2845
  }
2846
+ if (payload.local.setupActions.length > 0) {
2847
+ console.log(' Setup actions:');
2848
+ for (const action of payload.local.setupActions) {
2849
+ console.log(` ${action.label}${action.command ? `: ${action.command}` : ''}`);
2850
+ console.log(` ${action.message}`);
2851
+ }
2852
+ }
2785
2853
  if (!payload.server.authenticated) {
2786
2854
  console.log('Server worker: not authenticated');
2787
2855
  }
@@ -307,7 +307,8 @@ function isMacProtectedWorkspaceRoot(rootPath, homeDir = node_os_1.default.homed
307
307
  return protectedRoots.some((protectedRoot) => absoluteRoot === protectedRoot || isPathInside(absoluteRoot, protectedRoot));
308
308
  }
309
309
  function isPathInside(candidate, parent) {
310
- return candidate.startsWith(`${parent}/`);
310
+ const relativePath = (0, node_path_1.relative)(parent, candidate);
311
+ return Boolean(relativePath) && !relativePath.startsWith('..') && !(0, node_path_1.isAbsolute)(relativePath);
311
312
  }
312
313
  function isAllowedProtectedPath(candidate, allowedProtectedRoots) {
313
314
  return allowedProtectedRoots.some((allowedRoot) => candidate === allowedRoot || isPathInside(candidate, allowedRoot));
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.10.19",
2
+ "version": "0.10.21",
3
3
  "build": 1,
4
4
  "runtimeSdkVersion": "0.10.0",
5
5
  "clients": {
6
6
  "all": {
7
7
  "minimumVersion": "0.7.4"
8
+ },
9
+ "windows-playdrop": {
10
+ "minimumVersion": "0.9.6",
11
+ "minimumBuild": 1
8
12
  }
9
13
  }
10
14
  }
@@ -80,6 +80,45 @@ function runtime(overrides = {}) {
80
80
  const result = (0, index_1.validateClientEnvironment)(info);
81
81
  strict_1.default.equal(result.ok, true);
82
82
  });
83
+ (0, node_test_1.default)('accepts windows-playdrop as a first-class Windows client identity', () => {
84
+ const meta = (0, index_1.loadClientMeta)();
85
+ const windowsRule = meta.clients['windows-playdrop'];
86
+ strict_1.default.ok(windowsRule, 'windows-playdrop metadata must be explicit');
87
+ const requiredVersion = windowsRule.minimumVersion ?? meta.version;
88
+ const requiredBuild = windowsRule.minimumBuild ?? meta.build;
89
+ const info = runtime({
90
+ client: 'windows-playdrop',
91
+ clientVersion: requiredVersion,
92
+ clientBuild: requiredBuild,
93
+ platform: 'windows',
94
+ platformVersion: '10.0.22631',
95
+ });
96
+ const result = (0, index_1.validateClientEnvironment)(info);
97
+ strict_1.default.equal(result.ok, true);
98
+ });
99
+ (0, node_test_1.default)('enforces the windows-playdrop alpha build floor', () => {
100
+ const meta = (0, index_1.loadClientMeta)();
101
+ const windowsRule = meta.clients['windows-playdrop'];
102
+ strict_1.default.ok(windowsRule?.minimumVersion, 'windows-playdrop requires an explicit minimum version');
103
+ const requiredBuild = windowsRule.minimumBuild;
104
+ if (typeof requiredBuild !== 'number') {
105
+ strict_1.default.fail('windows-playdrop requires an explicit minimum build');
106
+ }
107
+ const info = runtime({
108
+ client: 'windows-playdrop',
109
+ clientVersion: windowsRule.minimumVersion,
110
+ clientBuild: Math.max(requiredBuild - 1, 0),
111
+ platform: 'windows',
112
+ platformVersion: '10.0.22631',
113
+ });
114
+ const result = (0, index_1.validateClientEnvironment)(info);
115
+ strict_1.default.equal(result.ok, false);
116
+ if (!result.ok) {
117
+ strict_1.default.equal(result.code, 'unsupported_client_build');
118
+ strict_1.default.equal(result.requiredVersion, windowsRule.minimumVersion);
119
+ strict_1.default.equal(result.requiredBuild, requiredBuild);
120
+ }
121
+ });
83
122
  (0, node_test_1.default)('keeps accepting the legacy Apple client id during rollout', () => {
84
123
  const meta = (0, index_1.loadClientMeta)();
85
124
  const requiredVersion = meta.clients['all']?.minimumVersion ?? meta.version;