@playdrop/playdrop-cli 0.12.11 → 0.12.13

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,6 +1,6 @@
1
1
  {
2
2
  "version": "0.12.8",
3
- "build": 1,
3
+ "build": 5,
4
4
  "runtimeSdkVersion": "0.12.5",
5
5
  "runtimeSdkBuild": 2,
6
6
  "clients": {
@@ -8,6 +8,7 @@ const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = require("node:path");
9
9
  const http_1 = require("../http");
10
10
  const loadCheck_1 = require("./loadCheck");
11
+ const listingPreflight_1 = require("../listingPreflight");
11
12
  const EXTENSION_TO_MIME = {
12
13
  '.png': 'image/png',
13
14
  '.jpg': 'image/jpeg',
@@ -74,89 +75,6 @@ async function bufferFromFile(file) {
74
75
  function computeSha256Hex(buffer) {
75
76
  return (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
76
77
  }
77
- function isSha256Hex(value) {
78
- return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
79
- }
80
- function isCaptureMediaFileKey(fileKey) {
81
- return fileKey.startsWith('screenshotsPortrait:')
82
- || fileKey.startsWith('screenshotsLandscape:')
83
- || fileKey.startsWith('videosPortrait:')
84
- || fileKey.startsWith('videosLandscape:');
85
- }
86
- function readMediaCaptureProof(input) {
87
- const reportPath = input.task.listing?.captureReportPath;
88
- if (!reportPath) {
89
- throw new Error(`agent_task_media_capture_report_missing: ${input.task.name} must include listing.captureReport from "playdrop project capture".`);
90
- }
91
- let report;
92
- try {
93
- report = JSON.parse((0, node_fs_1.readFileSync)(reportPath, 'utf8'));
94
- }
95
- catch (error) {
96
- const reason = error instanceof Error ? error.message : String(error);
97
- throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} must be valid JSON. ${reason}`);
98
- }
99
- const binding = report?.binding;
100
- if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
101
- throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} is missing binding.`);
102
- }
103
- const rawArtifactHashes = binding.artifactHashes;
104
- if (!rawArtifactHashes || typeof rawArtifactHashes !== 'object' || Array.isArray(rawArtifactHashes)) {
105
- throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must be an object.`);
106
- }
107
- const captureArtifactHashEntries = Object.values(rawArtifactHashes)
108
- .map((value) => typeof value === 'string' ? value.trim().toLowerCase() : '')
109
- .filter((value) => isSha256Hex(value));
110
- const captureArtifactHashes = new Set(captureArtifactHashEntries);
111
- if (captureArtifactHashEntries.length === 0) {
112
- throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes is empty.`);
113
- }
114
- const captureArtifactHashCounts = new Map();
115
- for (const hash of captureArtifactHashEntries) {
116
- captureArtifactHashCounts.set(hash, (captureArtifactHashCounts.get(hash) ?? 0) + 1);
117
- }
118
- const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureMediaFileKey(file.fileKey));
119
- if (mediaFiles.length === 0) {
120
- throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder stills and video in listing.screenshots* and listing.videos*.`);
121
- }
122
- const uploadedMediaHashCounts = new Map();
123
- for (const file of mediaFiles) {
124
- uploadedMediaHashCounts.set(file.sha256, (uploadedMediaHashCounts.get(file.sha256) ?? 0) + 1);
125
- }
126
- const missingUploadedCaptureArtifacts = [...captureArtifactHashCounts.entries()]
127
- .filter(([hash, expectedCount]) => (uploadedMediaHashCounts.get(hash) ?? 0) < expectedCount);
128
- if (missingUploadedCaptureArtifacts.length > 0) {
129
- throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder poster and video from listing.captureReport in catalogue.json.`);
130
- }
131
- let hasStill = false;
132
- let hasVideo = false;
133
- const artifactHashes = {};
134
- for (const file of mediaFiles) {
135
- if (!captureArtifactHashes.has(file.sha256)) {
136
- throw new Error(`agent_task_media_capture_artifact_mismatch: ${file.fileKey} was not produced by listing.captureReport for ${input.task.name}.`);
137
- }
138
- hasStill = hasStill || file.fileKey.startsWith('screenshots');
139
- hasVideo = hasVideo || file.fileKey.startsWith('videos');
140
- artifactHashes[file.fileKey] = file.sha256;
141
- }
142
- if (!hasStill || !hasVideo) {
143
- throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder still and one recorder video.`);
144
- }
145
- const capturedSurfaces = Array.isArray(binding.capturedSurfaces)
146
- ? Array.from(new Set(binding.capturedSurfaces
147
- .map((surface) => typeof surface === 'string' ? surface.trim().toUpperCase() : '')
148
- .filter((surface) => input.task.surfaceTargets.includes(surface))))
149
- : [];
150
- const capturedSurfaceSet = new Set(capturedSurfaces);
151
- const missingSurfaces = input.task.surfaceTargets.filter((surface) => !capturedSurfaceSet.has(surface));
152
- if (missingSurfaces.length > 0) {
153
- throw new Error(`agent_task_media_capture_surface_missing: listing.captureReport did not capture ${missingSurfaces.join(', ')} for ${input.task.name}.`);
154
- }
155
- return {
156
- artifactHashes,
157
- capturedSurfaces,
158
- };
159
- }
160
78
  function resolveMimeType(filename, fallback = 'application/octet-stream') {
161
79
  const type = EXTENSION_TO_MIME[(0, node_path_1.extname)(filename).toLowerCase()];
162
80
  return type ?? fallback;
@@ -425,7 +343,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
425
343
  if (options?.mediaCaptureRequired === true
426
344
  && task.hostingMode !== 'EXTERNAL'
427
345
  && !task.externalUrl) {
428
- const mediaCaptureProof = readMediaCaptureProof({
346
+ const mediaCaptureProof = (0, listingPreflight_1.readMediaCaptureProof)({
429
347
  task,
430
348
  preparedSessionFiles,
431
349
  });
@@ -1,3 +1,8 @@
1
+ export declare function resolveNpmInstallInvocation(platform?: NodeJS.Platform, nodeExecutable?: string): {
2
+ command: string;
3
+ args: string[];
4
+ };
5
+ export declare function runNpmInstall(projectDir: string): boolean;
1
6
  type CreateOptions = {
2
7
  template?: string;
3
8
  remix?: string;
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveNpmInstallInvocation = resolveNpmInstallInvocation;
7
+ exports.runNpmInstall = runNpmInstall;
6
8
  exports.createAssetSpecProject = createAssetSpecProject;
7
9
  exports.create = create;
8
10
  const types_1 = require("@playdrop/types");
@@ -22,6 +24,7 @@ const messages_1 = require("../messages");
22
24
  const loadCheck_1 = require("../apps/loadCheck");
23
25
  const devShared_1 = require("./devShared");
24
26
  const init_1 = require("./init");
27
+ const npmProcess_1 = require("./npmProcess");
25
28
  const taskCaptureSession_1 = require("./taskCaptureSession");
26
29
  const CATALOGUE_FILENAME = 'catalogue.json';
27
30
  const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
@@ -186,9 +189,12 @@ async function promptInstallDependencies(projectDir) {
186
189
  rl.close();
187
190
  return /^y(es)?$/i.test(answer.trim());
188
191
  }
192
+ function resolveNpmInstallInvocation(platform = process.platform, nodeExecutable = process.execPath) {
193
+ return (0, npmProcess_1.resolveNpmInvocation)(['install'], platform, nodeExecutable);
194
+ }
189
195
  function runNpmInstall(projectDir) {
190
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
191
- const result = (0, node_child_process_1.spawnSync)(npmCommand, ['install'], {
196
+ const invocation = resolveNpmInstallInvocation();
197
+ const result = (0, node_child_process_1.spawnSync)(invocation.command, invocation.args, {
192
198
  cwd: projectDir,
193
199
  stdio: 'inherit',
194
200
  });
@@ -34,6 +34,7 @@ export declare function buildLocalDevAppUrl(input: {
34
34
  }): string;
35
35
  export declare function terminateChildProcessTree(child: ChildProcess | null, timeoutMs?: number): Promise<void>;
36
36
  export declare function ensureDevRouterRunning(port?: number): Promise<void>;
37
+ export declare function resolveDevRouterWorkingDirectory(cliEntrypoint: string): string;
37
38
  export declare function updateMountedDevRuntimeAssetManifest(input: {
38
39
  creatorUsername: string;
39
40
  appName: string;
@@ -8,6 +8,7 @@ exports.parseMountConflictError = parseMountConflictError;
8
8
  exports.buildLocalDevAppUrl = buildLocalDevAppUrl;
9
9
  exports.terminateChildProcessTree = terminateChildProcessTree;
10
10
  exports.ensureDevRouterRunning = ensureDevRouterRunning;
11
+ exports.resolveDevRouterWorkingDirectory = resolveDevRouterWorkingDirectory;
11
12
  exports.updateMountedDevRuntimeAssetManifest = updateMountedDevRuntimeAssetManifest;
12
13
  exports.startDevServer = startDevServer;
13
14
  exports.isDevServerAvailable = isDevServerAvailable;
@@ -19,6 +20,7 @@ const node_crypto_1 = require("node:crypto");
19
20
  const node_fs_1 = require("node:fs");
20
21
  const node_http_1 = __importDefault(require("node:http"));
21
22
  const node_path_1 = require("node:path");
23
+ const npmProcess_1 = require("./npmProcess");
22
24
  exports.DEV_ROUTER_PORT = 8888;
23
25
  const DEV_ROUTER_HOST = '127.0.0.1';
24
26
  const CONTROL_PREFIX = '/_playdrop';
@@ -248,8 +250,8 @@ function spawnDevScript(projectInfo) {
248
250
  if (!projectInfo.projectDir || !projectInfo.packageJson || typeof projectInfo.packageJson.scripts?.dev !== 'string') {
249
251
  return null;
250
252
  }
251
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
252
- const child = (0, node_child_process_1.spawn)(npmCommand, ['run', 'dev'], {
253
+ const npm = (0, npmProcess_1.resolveNpmInvocation)(['run', 'dev']);
254
+ const child = (0, node_child_process_1.spawn)(npm.command, npm.args, {
253
255
  cwd: projectInfo.projectDir,
254
256
  stdio: 'inherit',
255
257
  env: { ...process.env },
@@ -341,25 +343,52 @@ async function waitForProcessGroupExit(pid, timeoutMs) {
341
343
  }
342
344
  return !isProcessGroupAlive(pid);
343
345
  }
346
+ async function terminateWindowsProcessTree(pid, timeoutMs) {
347
+ const windowsRoot = process.env.SystemRoot?.trim() || process.env.WINDIR?.trim() || 'C:\\Windows';
348
+ const taskkillPath = (0, node_path_1.join)(windowsRoot, 'System32', 'taskkill.exe');
349
+ await new Promise((resolvePromise) => {
350
+ let settled = false;
351
+ const taskkill = (0, node_child_process_1.spawn)(taskkillPath, ['/PID', String(pid), '/T', '/F'], {
352
+ stdio: 'ignore',
353
+ windowsHide: true,
354
+ });
355
+ const settle = () => {
356
+ if (settled) {
357
+ return;
358
+ }
359
+ settled = true;
360
+ clearTimeout(timeout);
361
+ resolvePromise();
362
+ };
363
+ const timeout = setTimeout(() => {
364
+ taskkill.kill();
365
+ settle();
366
+ }, timeoutMs);
367
+ timeout.unref?.();
368
+ taskkill.once('error', settle);
369
+ taskkill.once('close', settle);
370
+ });
371
+ }
344
372
  async function terminateChildProcessTree(child, timeoutMs = 2000) {
345
373
  const pid = child?.pid;
346
374
  if (!child || !pid || !isPidAlive(pid)) {
347
375
  return;
348
376
  }
349
- const useGroup = process.platform !== 'win32';
350
- signalProcess(pid, 'SIGTERM', useGroup);
351
- const exited = useGroup
352
- ? await waitForProcessGroupExit(pid, timeoutMs)
353
- : await waitForChildExit(child, timeoutMs);
354
- if (exited) {
377
+ if (process.platform === 'win32') {
378
+ await terminateWindowsProcessTree(pid, timeoutMs);
379
+ if (isPidAlive(pid)) {
380
+ signalProcess(pid, 'SIGKILL', false);
381
+ await waitForChildExit(child, timeoutMs);
382
+ }
355
383
  return;
356
384
  }
357
- signalProcess(pid, 'SIGKILL', useGroup);
358
- if (useGroup) {
359
- await waitForProcessGroupExit(pid, timeoutMs);
385
+ signalProcess(pid, 'SIGTERM', true);
386
+ const exited = await waitForProcessGroupExit(pid, timeoutMs);
387
+ if (exited) {
360
388
  return;
361
389
  }
362
- await waitForChildExit(child, timeoutMs);
390
+ signalProcess(pid, 'SIGKILL', true);
391
+ await waitForProcessGroupExit(pid, timeoutMs);
363
392
  }
364
393
  async function fetchRouterJson(path, init = {}, port = exports.DEV_ROUTER_PORT) {
365
394
  const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${path}`, init);
@@ -416,6 +445,7 @@ async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
416
445
  const childEnv = { ...process.env };
417
446
  delete childEnv.PLAYDROP_WORKER_CONTEXT;
418
447
  const child = (0, node_child_process_1.spawn)(process.execPath, [cliEntrypoint, 'project', '_dev-router', 'serve'], {
448
+ cwd: resolveDevRouterWorkingDirectory(cliEntrypoint),
419
449
  detached: true,
420
450
  stdio: 'ignore',
421
451
  env: {
@@ -434,6 +464,9 @@ async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
434
464
  }
435
465
  throw new Error('dev_router_start_failed');
436
466
  }
467
+ function resolveDevRouterWorkingDirectory(cliEntrypoint) {
468
+ return (0, node_path_1.dirname)((0, node_path_1.resolve)(cliEntrypoint));
469
+ }
437
470
  async function registerDevMount(input, port = exports.DEV_ROUTER_PORT) {
438
471
  const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/mounts`, {
439
472
  method: 'POST',
@@ -7,4 +7,5 @@ type LoginOptions = {
7
7
  handoffToken?: string;
8
8
  json?: boolean;
9
9
  };
10
+ export declare function buildCliLoginTokenName(machineHostname?: string): string;
10
11
  export declare function login(env: string, options?: LoginOptions): Promise<void>;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setOpenBrowserForTests = exports.resetOpenBrowserForTests = void 0;
4
+ exports.buildCliLoginTokenName = buildCliLoginTokenName;
4
5
  exports.login = login;
5
6
  const types_1 = require("@playdrop/types");
7
+ const node_os_1 = require("node:os");
6
8
  const config_1 = require("../config");
7
9
  const apiClient_1 = require("../apiClient");
8
10
  const http_1 = require("../http");
@@ -22,6 +24,13 @@ function extractAccessToken(data) {
22
24
  function isLoginResponse(data) {
23
25
  return typeof data === 'object' && data !== null && 'user' in data;
24
26
  }
27
+ function buildCliLoginTokenName(machineHostname = (0, node_os_1.hostname)()) {
28
+ const normalizedHostname = machineHostname.trim();
29
+ if (!normalizedHostname) {
30
+ throw new Error('cli_login_hostname_missing');
31
+ }
32
+ return `CLI on ${normalizedHostname}`;
33
+ }
25
34
  function storeLogin(env, data, options = {}) {
26
35
  const token = extractAccessToken(data);
27
36
  if (!token) {
@@ -53,11 +62,50 @@ function storeLogin(env, data, options = {}) {
53
62
  console.log(`Logged in as ${username} on ${env}.`);
54
63
  console.log('Next: run "playdrop auth whoami" to confirm your session.');
55
64
  }
65
+ async function storeDeviceLogin(env, baseUrl, initialLogin, options = {}) {
66
+ const sessionToken = extractAccessToken(initialLogin);
67
+ const username = typeof initialLogin.user?.username === 'string'
68
+ ? initialLogin.user.username.trim()
69
+ : '';
70
+ if (!sessionToken) {
71
+ throw new Error('Login response is missing the session token required to create this computer token.');
72
+ }
73
+ if (!username) {
74
+ throw new Error('Login response is missing the username required to save this computer token.');
75
+ }
76
+ try {
77
+ const sessionClient = (0, apiClient_1.createCliApiClient)({ baseUrl, token: sessionToken });
78
+ const deviceToken = await sessionClient.createCliApiToken({
79
+ name: buildCliLoginTokenName(),
80
+ });
81
+ const deviceLogin = await (0, apiClient_1.createCliApiClient)({ baseUrl }).loginWithApiKey({
82
+ apiKey: deviceToken.apiKey,
83
+ });
84
+ storeLogin(env, deviceLogin, options);
85
+ }
86
+ catch (unknownError) {
87
+ if (unknownError instanceof types_1.UnsupportedClientError) {
88
+ (0, http_1.handleUnsupportedError)(unknownError, 'Login');
89
+ return;
90
+ }
91
+ if (unknownError instanceof types_1.ApiError) {
92
+ const tokenLimitReached = unknownError.code === 'cli_api_token_limit_reached';
93
+ (0, messages_1.printErrorWithHelp)(tokenLimitReached
94
+ ? 'Login succeeded, but this account already has the maximum of 25 CLI tokens.'
95
+ : `Login succeeded, but this computer's CLI token could not be created (status ${unknownError.status}).`, tokenLimitReached
96
+ ? ['Delete an unused CLI token in account settings, then rerun "playdrop auth login".']
97
+ : ['Rerun "playdrop auth login" to try again.'], { command: 'login' });
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ throw unknownError;
102
+ }
103
+ }
56
104
  async function loginWithUsernamePassword(env, baseUrl, username, password, options = {}) {
57
105
  const client = (0, apiClient_1.createCliApiClient)({ baseUrl });
58
106
  try {
59
107
  const data = await client.login({ username, password });
60
- storeLogin(env, data, options);
108
+ await storeDeviceLogin(env, baseUrl, data, options);
61
109
  }
62
110
  catch (unknownError) {
63
111
  if (unknownError instanceof types_1.UnsupportedClientError) {
@@ -101,7 +149,7 @@ async function loginWithHandoff(env, baseUrl, handoffToken, options = {}) {
101
149
  const client = (0, apiClient_1.createCliApiClient)({ baseUrl });
102
150
  try {
103
151
  const data = await client.exchangeAppleOAuthHandoff({ token: handoffToken });
104
- storeLogin(env, data, options);
152
+ await storeDeviceLogin(env, baseUrl, data, options);
105
153
  }
106
154
  catch (unknownError) {
107
155
  if (unknownError instanceof types_1.UnsupportedClientError) {
@@ -163,7 +211,7 @@ async function loginInBrowser(env, baseUrl, options = {}) {
163
211
  await sleep(intervalMs);
164
212
  continue;
165
213
  }
166
- storeLogin(env, result, options);
214
+ await storeDeviceLogin(env, baseUrl, result, options);
167
215
  return;
168
216
  }
169
217
  catch (unknownError) {
@@ -0,0 +1,4 @@
1
+ export declare function resolveNpmInvocation(args: string[], platform?: NodeJS.Platform, nodeExecutable?: string): {
2
+ command: string;
3
+ args: string[];
4
+ };
@@ -0,0 +1,20 @@
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.resolveNpmInvocation = resolveNpmInvocation;
7
+ const node_path_1 = require("node:path");
8
+ const node_process_1 = __importDefault(require("node:process"));
9
+ function resolveNpmInvocation(args, platform = node_process_1.default.platform, nodeExecutable = node_process_1.default.execPath) {
10
+ if (platform === 'win32') {
11
+ return {
12
+ command: nodeExecutable,
13
+ args: [
14
+ node_path_1.win32.join(node_path_1.win32.dirname(nodeExecutable), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
15
+ ...args,
16
+ ],
17
+ };
18
+ }
19
+ return { command: 'npm', args };
20
+ }
@@ -51,6 +51,7 @@ const uploadLog_1 = require("../uploadLog");
51
51
  const upload_content_1 = require("./upload-content");
52
52
  const appUrls_1 = require("../appUrls");
53
53
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
54
+ const listingPreflight_1 = require("../listingPreflight");
54
55
  const upload_graph_1 = require("./upload-graph");
55
56
  function normalizeSavedItemKinds(kinds) {
56
57
  return [...new Set(Array.isArray(kinds) ? kinds : [])].sort();
@@ -847,6 +848,7 @@ function appendTaskRelations(graphState, fromNodeId, relations, contextLabel) {
847
848
  });
848
849
  }
849
850
  async function uploadAppTask(state, task, taskCreator, options) {
851
+ (0, listingPreflight_1.assertProjectListingPreflight)(task);
850
852
  const { upload, warnings } = await (0, apps_1.runAppPipeline)(state.client, task, {
851
853
  skipReview: options?.skipReview,
852
854
  clearTags: options?.clearTags,
@@ -1230,10 +1232,6 @@ const WORKER_SUPERVISOR_BUNDLE_EXCLUDES = [
1230
1232
  'assets/marketing/',
1231
1233
  'listing/',
1232
1234
  ];
1233
- const MIN_HERO_PORTRAIT_WIDTH = 512;
1234
- const MIN_HERO_PORTRAIT_HEIGHT = 768;
1235
- const MIN_HERO_LANDSCAPE_WIDTH = 768;
1236
- const MIN_HERO_LANDSCAPE_HEIGHT = 432;
1237
1235
  const OWNED_RUNTIME_IMAGE_CHECK_SIZE = 96;
1238
1236
  const OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE = 512;
1239
1237
  function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
@@ -2024,118 +2022,6 @@ async function assertNewGameDeclaredAssetsAreUsedAtRuntime(input) {
2024
2022
  throw new Error('agent_task_game_asset_fallback_disallowed: new game tasks must fail clearly when declared gameplay assets cannot load instead of rendering placeholder or fallback visuals.');
2025
2023
  }
2026
2024
  }
2027
- function readPngDimensions(filePath) {
2028
- const buffer = (0, node_fs_1.readFileSync)(filePath);
2029
- if (buffer.length < 24) {
2030
- throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} is too small to be a valid PNG.`);
2031
- }
2032
- const isPng = (buffer[0] === 0x89
2033
- && buffer[1] === 0x50
2034
- && buffer[2] === 0x4e
2035
- && buffer[3] === 0x47
2036
- && buffer[4] === 0x0d
2037
- && buffer[5] === 0x0a
2038
- && buffer[6] === 0x1a
2039
- && buffer[7] === 0x0a);
2040
- if (!isPng || buffer.toString('ascii', 12, 16) !== 'IHDR') {
2041
- throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} must be a PNG with an IHDR header.`);
2042
- }
2043
- const width = buffer.readUInt32BE(16);
2044
- const height = buffer.readUInt32BE(20);
2045
- if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
2046
- throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} has invalid PNG dimensions.`);
2047
- }
2048
- return { width, height };
2049
- }
2050
- function readAgentTaskCatalogueApp(task) {
2051
- let data;
2052
- try {
2053
- data = JSON.parse((0, node_fs_1.readFileSync)(task.catalogueAbsolutePath, 'utf8'));
2054
- }
2055
- catch (error) {
2056
- const reason = error instanceof Error ? error.message : String(error);
2057
- throw new Error(`agent_task_catalogue_invalid_json: ${task.cataloguePath} must be valid JSON. ${reason}`);
2058
- }
2059
- const apps = Array.isArray(data?.apps) ? data.apps : [];
2060
- const app = apps.find((entry) => entry && typeof entry === 'object' && entry.name === task.name);
2061
- if (!app || typeof app !== 'object') {
2062
- throw new Error(`agent_task_catalogue_app_missing: ${task.cataloguePath} must contain apps[] entry "${task.name}".`);
2063
- }
2064
- return app;
2065
- }
2066
- function assertNewGameHeroListingKeys(task) {
2067
- const app = readAgentTaskCatalogueApp(task);
2068
- const listing = app.listing;
2069
- if (!listing || typeof listing !== 'object' || Array.isArray(listing)) {
2070
- return;
2071
- }
2072
- const hasLegacyPortrait = typeof listing.heroPortraitPath === 'string' && listing.heroPortraitPath.trim().length > 0;
2073
- const hasLegacyLandscape = typeof listing.heroLandscapePath === 'string' && listing.heroLandscapePath.trim().length > 0;
2074
- if (hasLegacyPortrait || hasLegacyLandscape) {
2075
- throw new Error('agent_task_listing_hero_art_legacy_keys: new game tasks must reference hero art through listing.heroPortrait and listing.heroLandscape, not listing.heroPortraitPath or listing.heroLandscapePath.');
2076
- }
2077
- }
2078
- function assertNewGameHeroListingPath(task, filePath, field) {
2079
- const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), (0, node_path_1.resolve)(filePath)).replace(/\\/g, '/');
2080
- if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
2081
- throw new Error(`agent_task_listing_hero_art_path_invalid: ${field} must point to a PNG inside the project under assets/marketing/playdrop/.`);
2082
- }
2083
- if (!relativePath.startsWith('assets/marketing/playdrop/') || (0, node_path_1.extname)(relativePath).toLowerCase() !== '.png') {
2084
- throw new Error(`agent_task_listing_hero_art_path_invalid: ${field} must point to a PNG under assets/marketing/playdrop/.`);
2085
- }
2086
- }
2087
- function assertNewGameHeroListingArt(task) {
2088
- const heroPortraitPath = task.listing?.heroPortraitPath;
2089
- const heroLandscapePath = task.listing?.heroLandscapePath;
2090
- if (!heroPortraitPath || !heroLandscapePath) {
2091
- throw new Error('agent_task_listing_hero_art_missing: new game tasks must include listing.heroPortrait and listing.heroLandscape PNG assets.');
2092
- }
2093
- assertNewGameHeroListingKeys(task);
2094
- assertNewGameHeroListingPath(task, heroPortraitPath, 'listing.heroPortrait');
2095
- assertNewGameHeroListingPath(task, heroLandscapePath, 'listing.heroLandscape');
2096
- const portrait = readPngDimensions(heroPortraitPath);
2097
- if (portrait.width < MIN_HERO_PORTRAIT_WIDTH || portrait.height < MIN_HERO_PORTRAIT_HEIGHT || portrait.height <= portrait.width) {
2098
- throw new Error(`agent_task_listing_hero_art_invalid: listing.heroPortrait must be a portrait PNG at least ${MIN_HERO_PORTRAIT_WIDTH}x${MIN_HERO_PORTRAIT_HEIGHT}.`);
2099
- }
2100
- const landscape = readPngDimensions(heroLandscapePath);
2101
- if (landscape.width < MIN_HERO_LANDSCAPE_WIDTH || landscape.height < MIN_HERO_LANDSCAPE_HEIGHT || landscape.width <= landscape.height) {
2102
- throw new Error(`agent_task_listing_hero_art_invalid: listing.heroLandscape must be a landscape PNG at least ${MIN_HERO_LANDSCAPE_WIDTH}x${MIN_HERO_LANDSCAPE_HEIGHT}.`);
2103
- }
2104
- }
2105
- function assertListingScreenshotPath(task, filePath, field) {
2106
- const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), (0, node_path_1.resolve)(filePath)).replace(/\\/g, '/');
2107
- const requiredPrefix = field === 'listing.screenshotsPortrait'
2108
- ? 'assets/marketing/playdrop/screenshots/portrait/'
2109
- : 'assets/marketing/playdrop/screenshots/landscape/';
2110
- if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
2111
- throw new Error(`agent_task_listing_screenshots_path_invalid: ${field} entries must point to PNG files inside the project under ${requiredPrefix}.`);
2112
- }
2113
- if (!relativePath.startsWith(requiredPrefix) || (0, node_path_1.extname)(relativePath).toLowerCase() !== '.png') {
2114
- throw new Error(`agent_task_listing_screenshots_path_invalid: ${field} entries must point to PNG files under ${requiredPrefix}.`);
2115
- }
2116
- }
2117
- function assertNewGameListingScreenshots(task) {
2118
- const listing = task.listing;
2119
- const portraitPaths = listing?.screenshotPortraitPaths ?? [];
2120
- const landscapePaths = listing?.screenshotLandscapePaths ?? [];
2121
- if (portraitPaths.length === 0 && landscapePaths.length === 0) {
2122
- throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG captures.');
2123
- }
2124
- if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
2125
- throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG capture.');
2126
- }
2127
- if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
2128
- throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG capture.');
2129
- }
2130
- for (const filePath of portraitPaths) {
2131
- assertListingScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
2132
- readPngDimensions(filePath);
2133
- }
2134
- for (const filePath of landscapePaths) {
2135
- assertListingScreenshotPath(task, filePath, 'listing.screenshotsLandscape');
2136
- readPngDimensions(filePath);
2137
- }
2138
- }
2139
2025
  function readPlaytestEvidenceManifest(task) {
2140
2026
  const manifestPath = (0, node_path_1.join)(task.projectDir, 'playtest-evidence.json');
2141
2027
  if (!(0, node_fs_1.existsSync)(manifestPath)) {
@@ -2297,7 +2183,7 @@ async function publishWorkerAppProject(input) {
2297
2183
  assertTaskPlaytestEvidenceManifest(task);
2298
2184
  }
2299
2185
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2300
- assertNewGameHeroListingArt(task);
2186
+ (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(task);
2301
2187
  }
2302
2188
  if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
2303
2189
  throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
@@ -2346,7 +2232,7 @@ async function publishWorkerAppProject(input) {
2346
2232
  taskToken,
2347
2233
  };
2348
2234
  if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2349
- assertNewGameListingScreenshots(task);
2235
+ (0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(task);
2350
2236
  assertTaskPlaytestEvidenceManifest(task);
2351
2237
  }
2352
2238
  const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
@@ -9,6 +9,7 @@ const externalAssetPackValidation_1 = require("../externalAssetPackValidation");
9
9
  const taskSelection_1 = require("../taskSelection");
10
10
  const taskUtils_1 = require("../taskUtils");
11
11
  const uploadLog_1 = require("../uploadLog");
12
+ const listingPreflight_1 = require("../listingPreflight");
12
13
  const upload_content_1 = require("./upload-content");
13
14
  function buildLocalAssetSpecLookups(tasks) {
14
15
  const exactByRef = new Map();
@@ -191,6 +192,7 @@ async function validate(pathOrName, options = {}) {
191
192
  const entityId = task.name;
192
193
  try {
193
194
  if (task.kind === 'app') {
195
+ (0, listingPreflight_1.assertProjectListingPreflight)(task);
194
196
  await (0, apps_1.validateAppTask)(task);
195
197
  (0, apps_1.collectAppValidationWarnings)(task, 'source').forEach((warning) => warnings.add(warning));
196
198
  }
@@ -266,6 +266,7 @@ export declare function createLocalPlaydropShim(input: {
266
266
  export declare function parseCodexModelCatalog(output: string): string[];
267
267
  export declare function parseClaudeModelCatalog(output: string): string[];
268
268
  export declare function claudeModelProbeReportsSelection(output: string, expectedDisplayName: string): boolean;
269
+ export declare function probeWorkerProviderIndependently<T>(probe: () => T, unavailable: T): T;
269
270
  export declare function buildWorkerSetupActions(input: {
270
271
  degradedReasons: string[];
271
272
  playwright?: {
@@ -301,6 +302,7 @@ export declare function resolveWorkerSupervisorLockFile(input?: {
301
302
  workerHomeDirectory?: string;
302
303
  }): string;
303
304
  export declare function parseLinuxProcessStartIdentity(stat: string): string | null;
305
+ export declare function parseWindowsProcessStartIdentity(output: string): string | null;
304
306
  export declare function removeStaleWorkerSupervisorLockIfUnchanged(file: string, observedHolder: WorkerSupervisorLockOwner): boolean;
305
307
  export declare function acquireWorkerSupervisorLock(input: {
306
308
  supervisorType: WorkerSupervisorType;