@playdrop/playdrop-cli 0.10.20 → 0.11.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.
@@ -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 = []) {
@@ -5,7 +5,7 @@ import { type LoggedProcessResult, type LoggedProcessTranscriptChunk } from './w
5
5
  export { DEFAULT_CODEX_TIMEOUT_MS, DEFAULT_WORKER_TOKEN_CAP, assertWorkerTokenUsageWithinCap, buildCodexExecArgs, buildClaudeExecArgs, buildClaudePermissionSettings, buildWorkerChildEnv, extractAgentTokenUsage, extractCodexRolloutTokenUsage, extractCodexThreadId, extractCodexTokensUsed, readEnvBoolean, readCodexSandboxMode, readCodexRolloutTokenUsageForThread, 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 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/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;
@@ -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;
@@ -124,12 +125,13 @@ const WORKER_CONTEXT_ALLOWED_COMMANDS = [
124
125
  ['versions', 'browse'],
125
126
  ['documentation', 'browse'],
126
127
  ['documentation', 'read'],
128
+ ['help'],
127
129
  ['ai', 'create'],
128
130
  ['ai', 'jobs', 'browse'],
129
131
  ['ai', 'jobs', 'detail'],
130
132
  ['ai', 'generations'],
131
133
  ];
132
- 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 catalogue/documentation lookup, and PlayDrop AI generation are permitted.';
134
+ 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.';
133
135
  function allocateWorkerDevPort(input) {
134
136
  const basePort = input.basePort ?? DEFAULT_WORKER_DEV_PORT_BASE;
135
137
  const maxParallelTasks = input.maxParallelTasks ?? DEFAULT_WORKER_MAX_PARALLEL_TASKS;
@@ -181,6 +183,28 @@ function normalizeRemixFlagValue(argv) {
181
183
  }
182
184
  return null;
183
185
  }
186
+ function normalizeTemplateKeyRef(value) {
187
+ const trimmed = value.trim();
188
+ const match = /^([^/]+)\/(?:template|app)\/([^/]+)$/.exec(trimmed);
189
+ if (!match) {
190
+ return trimmed || null;
191
+ }
192
+ return `${match[1]}/template/${match[2]}`;
193
+ }
194
+ function normalizeTemplateFlagValue(argv) {
195
+ for (let index = 0; index < argv.length; index += 1) {
196
+ const arg = argv[index];
197
+ if (arg === '--template') {
198
+ const value = argv[index + 1]?.trim() ?? '';
199
+ return value ? normalizeTemplateKeyRef(value) : null;
200
+ }
201
+ if (arg.startsWith('--template=')) {
202
+ const value = arg.slice('--template='.length).trim();
203
+ return value ? normalizeTemplateKeyRef(value) : null;
204
+ }
205
+ }
206
+ return null;
207
+ }
184
208
  function isAssignedAppScaffoldCommandAllowed(argv) {
185
209
  if (argv[0] !== 'project' || argv[1] !== 'create' || argv[2] !== 'app') {
186
210
  return false;
@@ -198,7 +222,11 @@ function isAssignedAppScaffoldCommandAllowed(argv) {
198
222
  || requestedAppName !== context.outputAppName) {
199
223
  return false;
200
224
  }
201
- return normalizeRemixFlagValue(argv) === null;
225
+ const templateKey = normalizeTemplateFlagValue(argv);
226
+ const allowedTemplateKeys = new Set((context.allowedTemplateKeys ?? []).map((key) => normalizeTemplateKeyRef(key)).filter(Boolean));
227
+ return normalizeRemixFlagValue(argv) === null
228
+ && templateKey !== null
229
+ && allowedTemplateKeys.has(templateKey);
202
230
  }
203
231
  return normalizeRemixFlagValue(argv) === context.remixSourceRef;
204
232
  }
@@ -301,6 +329,9 @@ function resolveWorkerClaimTaskAssignment(claim) {
301
329
  baseSource: inputs.baseSource ?? null,
302
330
  remixSource: inputs.remixSource ?? null,
303
331
  reviewTarget: inputs.reviewTarget ?? null,
332
+ allowedTemplateKeys: Array.isArray(inputs.allowedTemplateKeys)
333
+ ? Array.from(new Set(inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
334
+ : [],
304
335
  },
305
336
  output: {
306
337
  appId: typeof output.appId === 'number' && Number.isInteger(output.appId) && output.appId > 0 ? output.appId : null,
@@ -382,6 +413,9 @@ function buildWorkerTaskContext(claim, assignment) {
382
413
  }
383
414
  remixSourceRef = parsedRemixSourceRef;
384
415
  }
416
+ const allowedTemplateKeys = Array.isArray(assignment.inputs.allowedTemplateKeys)
417
+ ? Array.from(new Set(assignment.inputs.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
418
+ : [];
385
419
  const creatorRequest = assignment.inputs.request.prompt.trim();
386
420
  if (!creatorRequest) {
387
421
  throw new Error('agent_task_context_creator_request_missing');
@@ -397,6 +431,7 @@ function buildWorkerTaskContext(claim, assignment) {
397
431
  outputAppName,
398
432
  outputVersion,
399
433
  remixSourceRef,
434
+ allowedTemplateKeys,
400
435
  reviewAppVersionId,
401
436
  };
402
437
  }
@@ -454,6 +489,9 @@ function readTaskContextFile(startDir = node_process_1.default.cwd()) {
454
489
  const remixSourceRef = typeof parsed.remixSourceRef === 'string' && parsed.remixSourceRef.trim()
455
490
  ? parsed.remixSourceRef.trim()
456
491
  : null;
492
+ const allowedTemplateKeys = Array.isArray(parsed.allowedTemplateKeys)
493
+ ? Array.from(new Set(parsed.allowedTemplateKeys.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean)))
494
+ : [];
457
495
  let reviewAppVersionId = null;
458
496
  if (parsed.reviewAppVersionId !== null && parsed.reviewAppVersionId !== undefined) {
459
497
  const parsedReviewAppVersionId = Number(parsed.reviewAppVersionId);
@@ -487,6 +525,7 @@ function readTaskContextFile(startDir = node_process_1.default.cwd()) {
487
525
  outputAppName: typeof parsed.outputAppName === 'string' && parsed.outputAppName.trim() ? parsed.outputAppName.trim() : null,
488
526
  outputVersion,
489
527
  remixSourceRef,
528
+ allowedTemplateKeys,
490
529
  reviewAppVersionId,
491
530
  devPort,
492
531
  env,
@@ -676,6 +715,15 @@ async function readTaskNextStepsFileForCommand(input) {
676
715
  return NEXT_STEPS_READ_FAILED;
677
716
  }
678
717
  }
718
+ function isFinalOwnerRouteLaunchCheckFailure(error) {
719
+ const message = error instanceof Error ? error.message : String(error);
720
+ return message.startsWith('hosted_launch_check_failed:')
721
+ && message.includes('final owner play route');
722
+ }
723
+ function formatTerminalFinalOwnerRouteFailure(error) {
724
+ const message = error instanceof Error ? error.message : String(error);
725
+ 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}`;
726
+ }
679
727
  function assertTaskUploadResultMatchesContext(input) {
680
728
  if (input.uploadResult.taskId !== input.taskContext.taskId) {
681
729
  throw new Error('task_upload_result_task_mismatch');
@@ -1181,9 +1229,9 @@ function probePlaywrightChromium() {
1181
1229
  const version = typeof packageJson.version === 'string' && packageJson.version.trim() ? packageJson.version.trim() : 'unknown';
1182
1230
  const chromiumInstalled = (0, node_fs_1.existsSync)(executablePath);
1183
1231
  if (!chromiumInstalled) {
1184
- throw new Error(`worker_preflight_chromium_missing: install Playwright Chromium before starting the worker (${executablePath}).`);
1232
+ return { version, chromiumInstalled: false, executablePath };
1185
1233
  }
1186
- return { version, chromiumInstalled };
1234
+ return { version, chromiumInstalled: true, executablePath };
1187
1235
  }
1188
1236
  function readWorkerModelList(agent, envName) {
1189
1237
  const raw = node_process_1.default.env[envName];
@@ -1260,7 +1308,10 @@ function buildWorkerCapabilities(input) {
1260
1308
  const cursorVersion = typeof input === 'string' ? null : input.cursorVersion?.trim() || null;
1261
1309
  const cursorAuthenticated = typeof input === 'string' ? false : input.cursorAuthenticated === true;
1262
1310
  const npmVersion = typeof input === 'string' ? null : input.npmVersion?.trim() || null;
1263
- const playwright = typeof input === 'string' ? null : input.playwright ?? null;
1311
+ const rawPlaywright = typeof input === 'string' ? null : input.playwright ?? null;
1312
+ const playwright = rawPlaywright
1313
+ ? { version: rawPlaywright.version, chromiumInstalled: rawPlaywright.chromiumInstalled }
1314
+ : null;
1264
1315
  const maxParallelTasks = typeof input === 'string'
1265
1316
  ? DEFAULT_WORKER_MAX_PARALLEL_TASKS
1266
1317
  : input.maxParallelTasks ?? DEFAULT_WORKER_MAX_PARALLEL_TASKS;
@@ -1954,6 +2005,9 @@ function probeCodexInstallation() {
1954
2005
  const versionResult = (0, shellProbe_1.runShell)('codex --version');
1955
2006
  const match = /(\d+\.\d+\.\d+\S*)/.exec(versionResult.output);
1956
2007
  if (!match?.[1]) {
2008
+ if (versionResult.exitCode !== 0) {
2009
+ return null;
2010
+ }
1957
2011
  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
2012
  }
1959
2013
  if (versionResult.exitCode !== 0) {
@@ -1970,7 +2024,13 @@ function probeClaudeInstallation() {
1970
2024
  }
1971
2025
  const versionResult = (0, shellProbe_1.runShell)('claude --version');
1972
2026
  const match = /(\d+\.\d+\.\d+\S*)/.exec(versionResult.output);
1973
- if (!match?.[1] || versionResult.exitCode !== 0) {
2027
+ if (!match?.[1]) {
2028
+ if (versionResult.exitCode !== 0) {
2029
+ return null;
2030
+ }
2031
+ throw new Error(`claude_version_check_failed: "claude --version" exited with code ${versionResult.exitCode} and reported no version. Output: ${versionResult.output.slice(0, 200)}`);
2032
+ }
2033
+ if (versionResult.exitCode !== 0) {
1974
2034
  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
2035
  }
1976
2036
  return { claudeVersion: match[1] };
@@ -2076,6 +2136,9 @@ function collectWorkerLocalStatus() {
2076
2136
  }
2077
2137
  try {
2078
2138
  playwright = probePlaywrightChromium();
2139
+ if (!playwright.chromiumInstalled) {
2140
+ degradedReasons.push('worker_preflight_chromium_missing');
2141
+ }
2079
2142
  }
2080
2143
  catch (error) {
2081
2144
  degradedReasons.push(errorCode(error));
@@ -2143,8 +2206,53 @@ function collectWorkerLocalStatus() {
2143
2206
  },
2144
2207
  agents: capabilities?.agents ?? [],
2145
2208
  capabilities,
2209
+ setupActions: buildWorkerSetupActions({
2210
+ degradedReasons: uniqueReasons,
2211
+ playwright,
2212
+ }),
2146
2213
  };
2147
2214
  }
2215
+ function buildWorkerSetupActions(input) {
2216
+ const reasons = new Set(input.degradedReasons);
2217
+ const actions = [];
2218
+ if (reasons.has('worker_preflight_chromium_missing')) {
2219
+ const version = input.playwright?.version && input.playwright.version !== 'unknown'
2220
+ ? `@${input.playwright.version}`
2221
+ : '';
2222
+ actions.push({
2223
+ reason: 'worker_preflight_chromium_missing',
2224
+ label: 'Install Playwright Chromium',
2225
+ command: `npx playwright${version} install chromium`,
2226
+ message: input.playwright?.executablePath
2227
+ ? `Chromium is required for PlayDrop launch checks. Expected browser path: ${input.playwright.executablePath}.`
2228
+ : 'Chromium is required for PlayDrop launch checks.',
2229
+ });
2230
+ }
2231
+ if (reasons.has('worker_preflight_playwright_missing')) {
2232
+ actions.push({
2233
+ reason: 'worker_preflight_playwright_missing',
2234
+ label: 'Repair the PlayDrop CLI install',
2235
+ command: 'npm install -g @playdrop/playdrop-cli@latest',
2236
+ message: 'The PlayDrop CLI package could not load playwright-core from its installed dependencies.',
2237
+ });
2238
+ }
2239
+ if (reasons.has('playdrop_plugin_staging_manifest_missing')) {
2240
+ actions.push({
2241
+ reason: 'playdrop_plugin_staging_manifest_missing',
2242
+ label: 'Install or update the PlayDrop agent plugin',
2243
+ command: 'playdrop agents status --json',
2244
+ message: 'Run the CLI-reported nextAction.command for a supported local agent so playdrop-worker-staging.json is available.',
2245
+ });
2246
+ }
2247
+ if (reasons.has('agent_cli_not_found')) {
2248
+ actions.push({
2249
+ reason: 'agent_cli_not_found',
2250
+ label: 'Install a supported agent CLI',
2251
+ message: 'Install and authenticate Codex or Claude Code, then rerun playdrop agents status --json.',
2252
+ });
2253
+ }
2254
+ return actions;
2255
+ }
2148
2256
  function getWorkerStateDir() {
2149
2257
  return node_path_1.default.join(node_os_1.default.homedir(), '.playdrop', 'worker');
2150
2258
  }
@@ -2782,6 +2890,13 @@ function printWorkerStatus(payload) {
2782
2890
  if (payload.local.degradedReasons.length > 0) {
2783
2891
  console.log(` Reasons: ${payload.local.degradedReasons.join(', ')}`);
2784
2892
  }
2893
+ if (payload.local.setupActions.length > 0) {
2894
+ console.log(' Setup actions:');
2895
+ for (const action of payload.local.setupActions) {
2896
+ console.log(` ${action.label}${action.command ? `: ${action.command}` : ''}`);
2897
+ console.log(` ${action.message}`);
2898
+ }
2899
+ }
2785
2900
  if (!payload.server.authenticated) {
2786
2901
  console.log('Server worker: not authenticated');
2787
2902
  }
@@ -3771,21 +3886,40 @@ async function uploadTask(options = {}) {
3771
3886
  return;
3772
3887
  }
3773
3888
  const projectDir = discoverWorkerProjectRoot(node_process_1.default.cwd());
3774
- const published = await (0, upload_1.publishWorkerAppProject)({
3775
- client: ctx.client,
3776
- taskId: taskContext.taskId,
3777
- kind: taskContext.kind,
3778
- expectedAppName: taskContext.outputAppName ?? undefined,
3779
- remixSourceRef: taskContext.remixSourceRef ?? null,
3780
- playdropAssetRequirement: resolvePlaydropAssetRequirementFromTaskRequest(taskContext.creatorRequest),
3781
- creatorRequest: taskContext.creatorRequest,
3782
- projectDir,
3783
- creatorUsername: taskContext.creatorUsername,
3784
- apiBase: ctx.envConfig.apiBase,
3785
- webBase: ctx.envConfig.webBase ?? null,
3786
- token: ctx.token,
3787
- user: ctx.user,
3788
- });
3889
+ let published;
3890
+ try {
3891
+ published = await (0, upload_1.publishWorkerAppProject)({
3892
+ client: ctx.client,
3893
+ taskId: taskContext.taskId,
3894
+ kind: taskContext.kind,
3895
+ expectedAppName: taskContext.outputAppName ?? undefined,
3896
+ remixSourceRef: taskContext.remixSourceRef ?? null,
3897
+ playdropAssetRequirement: resolvePlaydropAssetRequirementFromTaskRequest(taskContext.creatorRequest),
3898
+ creatorRequest: taskContext.creatorRequest,
3899
+ projectDir,
3900
+ creatorUsername: taskContext.creatorUsername,
3901
+ apiBase: ctx.envConfig.apiBase,
3902
+ webBase: ctx.envConfig.webBase ?? null,
3903
+ token: ctx.token,
3904
+ user: ctx.user,
3905
+ });
3906
+ }
3907
+ catch (error) {
3908
+ if (isFinalOwnerRouteLaunchCheckFailure(error)) {
3909
+ const terminalError = formatTerminalFinalOwnerRouteFailure(error);
3910
+ await ctx.client.workerFailAgentTask(taskContext.taskId, {
3911
+ taskToken: taskContext.taskToken,
3912
+ error: terminalError,
3913
+ result: {
3914
+ failedBy: 'worker_upload_final_owner_route',
3915
+ finalOwnerRouteFailure: true,
3916
+ originalError: error instanceof Error ? error.message : String(error),
3917
+ },
3918
+ });
3919
+ throw new Error(terminalError);
3920
+ }
3921
+ throw error;
3922
+ }
3789
3923
  const result = {
3790
3924
  taskId: taskContext.taskId,
3791
3925
  appId: published.appId,
@@ -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.20",
2
+ "version": "0.11.0",
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;