@playdrop/playdrop-cli 0.12.27 → 0.12.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/config/client-meta.json +1 -1
  2. package/dist/catalogue.js +1 -3
  3. package/dist/commands/devRuntimeAssets.d.ts +1 -1
  4. package/dist/commands/devRuntimeAssets.js +28 -123
  5. package/dist/commands/init.d.ts +1 -1
  6. package/dist/commands/init.js +11 -6
  7. package/dist/commands/ownedRuntimeImageValidation.d.ts +7 -0
  8. package/dist/commands/ownedRuntimeImageValidation.js +76 -0
  9. package/dist/commands/upload.d.ts +11 -0
  10. package/dist/commands/upload.js +72 -447
  11. package/dist/commands/validate.js +15 -1
  12. package/dist/commands/worker.d.ts +16 -0
  13. package/dist/commands/worker.js +165 -67
  14. package/dist/listingPreflight.d.ts +10 -0
  15. package/dist/listingPreflight.js +34 -10
  16. package/node_modules/@playdrop/api-client/dist/client.d.ts +2 -1
  17. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  18. package/node_modules/@playdrop/api-client/dist/core/errors.d.ts.map +1 -1
  19. package/node_modules/@playdrop/api-client/dist/core/errors.js +3 -1
  20. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  21. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  23. package/node_modules/@playdrop/api-client/dist/index.d.ts +2 -1
  24. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/index.js +5 -0
  26. package/node_modules/@playdrop/config/client-meta.json +1 -1
  27. package/node_modules/@playdrop/types/dist/api.d.ts +13 -0
  28. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  29. package/node_modules/@playdrop/types/dist/version.d.ts +6 -12
  30. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  31. package/node_modules/@playdrop/types/dist/version.js +48 -88
  32. package/package.json +1 -1
@@ -1,40 +1,11 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveUploadOwner = resolveUploadOwner;
4
+ exports.resolveDefaultUploadCreator = resolveDefaultUploadCreator;
36
5
  exports.upload = upload;
6
+ exports.resolveWorkerPlaydropAssetRequirement = resolveWorkerPlaydropAssetRequirement;
37
7
  exports.assertTaskPlaytestEvidenceManifest = assertTaskPlaytestEvidenceManifest;
8
+ exports.assertWorkerAppLocalUploadPreflight = assertWorkerAppLocalUploadPreflight;
38
9
  exports.publishWorkerAppProject = publishWorkerAppProject;
39
10
  const node_crypto_1 = require("node:crypto");
40
11
  const node_fs_1 = require("node:fs");
@@ -53,6 +24,7 @@ const uploadLog_1 = require("../uploadLog");
53
24
  const upload_content_1 = require("./upload-content");
54
25
  const appUrls_1 = require("../appUrls");
55
26
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
27
+ const ownedRuntimeImageValidation_1 = require("./ownedRuntimeImageValidation");
56
28
  const listingPreflight_1 = require("../listingPreflight");
57
29
  const upload_graph_1 = require("./upload-graph");
58
30
  function normalizeSavedItemKinds(kinds) {
@@ -222,6 +194,19 @@ function normalizeCreatorUsername(username) {
222
194
  }
223
195
  return trimmed;
224
196
  }
197
+ function resolveUploadOwner(actorUsername, workspaceOwnerUsername) {
198
+ const workspaceOwner = workspaceOwnerUsername?.trim() ?? '';
199
+ if (workspaceOwner) {
200
+ return workspaceOwner;
201
+ }
202
+ const actor = actorUsername?.trim() ?? '';
203
+ return actor || 'unknown';
204
+ }
205
+ function resolveDefaultUploadCreator(owner, actorUsername) {
206
+ return normalizeCreatorUsername(owner ?? null)
207
+ ?? normalizeCreatorUsername(actorUsername ?? null)
208
+ ?? 'unknown';
209
+ }
225
210
  function isNotFoundApiError(error) {
226
211
  return error instanceof types_1.ApiError && error.status === 404;
227
212
  }
@@ -512,7 +497,10 @@ function validateTagRefsForTarget(tagRegistry, target) {
512
497
  function collectTaskTagValidationTargets(task) {
513
498
  if (task.kind === 'app') {
514
499
  return [
515
- { tags: task.tags, targetKind: 'APP' },
500
+ {
501
+ tags: Array.from(new Set([...task.tags, ...Object.values(task.design ?? {})])),
502
+ targetKind: 'APP',
503
+ },
516
504
  ...task.ownedAssets.map((ownedAsset) => ({
517
505
  tags: ownedAsset.tags,
518
506
  targetKind: 'ASSET',
@@ -1059,7 +1047,7 @@ async function flushGraphState(client, graphState, results) {
1059
1047
  }
1060
1048
  async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, localTagGroups, currentUser, token, warnings, apiBase, webBase, options) {
1061
1049
  const portalBase = normalizePortalBase(webBase);
1062
- const defaultCreator = normalizeCreatorUsername(ownerUsername) ?? owner;
1050
+ const defaultCreator = resolveDefaultUploadCreator(owner, ownerUsername);
1063
1051
  const results = [];
1064
1052
  const preflight = await buildUploadPreflight(client, tasks, defaultCreator, currentUserRole, localTagGroups);
1065
1053
  const sortedTasks = preflight.nodes.map((node) => node.task);
@@ -1177,9 +1165,7 @@ async function upload(pathOrName, options) {
1177
1165
  }
1178
1166
  throw error;
1179
1167
  }
1180
- const owner = typeof userInfo.username === 'string' && userInfo.username.trim().length > 0
1181
- ? userInfo.username.trim()
1182
- : 'unknown';
1168
+ const owner = resolveUploadOwner(userInfo.username, ctx.workspaceAuth?.config.ownerUsername);
1183
1169
  const tagGroupLoad = (0, catalogue_1.resolveCatalogueTagGroups)(workspaceRoot);
1184
1170
  if (tagGroupLoad.errors.length > 0) {
1185
1171
  (0, taskSelection_1.reportTaskErrors)(tagGroupLoad.errors.map((message) => ({
@@ -1223,6 +1209,19 @@ async function upload(pathOrName, options) {
1223
1209
  (0, uploadLog_1.printTaskSummary)(results, warnings, { action: 'upload', environment: env });
1224
1210
  (0, uploadLog_1.printPublishedAppNextSteps)(results);
1225
1211
  }
1212
+ function resolveWorkerPlaydropAssetRequirement(value) {
1213
+ const text = typeof value === 'string' ? value.toLowerCase() : '';
1214
+ if (!text.trim() || !/\bplay\s*drop\b|\bplaydrop\b/.test(text)) {
1215
+ return null;
1216
+ }
1217
+ if (/\basset\s*pack(s)?\b|\bpack(s)?\b/.test(text)) {
1218
+ return 'PACK';
1219
+ }
1220
+ if (/\basset(s)?\b|\bsprite(s)?\b|\bmodel(s)?\b|\bart\b|\bcharacter(s)?\b|\bprop(s)?\b/.test(text)) {
1221
+ return 'ASSET_OR_PACK';
1222
+ }
1223
+ return null;
1224
+ }
1226
1225
  const WORKER_SUPERVISOR_SOURCE_EXCLUDES = [
1227
1226
  'PLAYDROP_TASK.md',
1228
1227
  '.playdrop/',
@@ -1234,8 +1233,6 @@ const WORKER_SUPERVISOR_BUNDLE_EXCLUDES = [
1234
1233
  'assets/marketing/',
1235
1234
  'listing/',
1236
1235
  ];
1237
- const OWNED_RUNTIME_IMAGE_CHECK_SIZE = 96;
1238
- const OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE = 512;
1239
1236
  function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
1240
1237
  if (!requirement) {
1241
1238
  return true;
@@ -1253,361 +1250,6 @@ function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
1253
1250
  });
1254
1251
  return hasFirstPartyPack || hasFirstPartyAsset || task.ownedAssets.length > 0;
1255
1252
  }
1256
- function isUnsafeDirectPlaydropAiImageAsset(detail, parsedRevision) {
1257
- const asset = detail?.asset;
1258
- if (!asset || asset.category !== 'IMAGE') {
1259
- return false;
1260
- }
1261
- const currentVersion = asset.currentVersion;
1262
- if (!currentVersion || Number(currentVersion.revision) !== parsedRevision) {
1263
- return false;
1264
- }
1265
- const sourceKind = typeof currentVersion.sourceKind === 'string' ? currentVersion.sourceKind.toUpperCase() : '';
1266
- if (sourceKind !== 'AI') {
1267
- return false;
1268
- }
1269
- const format = typeof currentVersion.format === 'string' ? currentVersion.format.toUpperCase() : '';
1270
- const files = Array.isArray(currentVersion.fileManifest?.files) ? currentVersion.fileManifest.files : [];
1271
- const primary = files.find((file) => file?.role === 'primary');
1272
- const primaryContentType = typeof primary?.contentType === 'string' ? primary.contentType.toLowerCase() : '';
1273
- return format === 'JPEG' || primaryContentType === 'image/jpeg' || primaryContentType === 'image/jpg';
1274
- }
1275
- async function assertNewGameDirectPlaydropAssetsAreRuntimeSafe(input) {
1276
- const unsafeRefs = [];
1277
- for (const dependency of input.task.uses.assets) {
1278
- const parsed = (0, types_1.parseContentVersionRef)(dependency.ref);
1279
- if (!parsed || parsed.kind !== 'asset' || parsed.creatorUsername.toLowerCase() !== 'playdrop') {
1280
- continue;
1281
- }
1282
- const detail = await input.client.fetchAssetBySlug(parsed.creatorUsername, parsed.name);
1283
- if (isUnsafeDirectPlaydropAiImageAsset(detail, parsed.revision)) {
1284
- unsafeRefs.push((0, types_1.formatContentVersionRef)(parsed));
1285
- }
1286
- }
1287
- if (unsafeRefs.length > 0) {
1288
- throw new Error(`agent_task_playdrop_direct_ai_image_asset_unsafe: direct PlayDrop AI JPEG image assets cannot be used as new-game runtime sprites because their backgrounds are baked into the image. Use a PlayDrop asset pack or create clean owned PNG assets through the PlayDrop make-assets workflow instead. Unsafe refs: ${unsafeRefs.join(', ')}`);
1289
- }
1290
- }
1291
- function isOwnedRuntimeImageAsset(task, ownedAsset) {
1292
- if (!ownedAsset.runtimeKey || ownedAsset.category !== 'IMAGE') {
1293
- return false;
1294
- }
1295
- const primaryPath = ownedAsset.filePaths?.primary;
1296
- if (!primaryPath) {
1297
- return false;
1298
- }
1299
- const relativePath = primaryPath.slice((0, node_path_1.resolve)(task.projectDir).length + 1).replace(/\\/g, '/');
1300
- return !relativePath.startsWith('assets/marketing/') && !relativePath.startsWith('listing/');
1301
- }
1302
- function isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset) {
1303
- const value = `${ownedAsset.name ?? ''} ${ownedAsset.runtimeKey ?? ''}`.toLowerCase();
1304
- return /\b(background|backdrop|scene|sky|floor|ground|wall|terrain|landscape|level|arena)\b/i.test(value);
1305
- }
1306
- function looksLikeBakedCheckerboardPreview(input) {
1307
- const { data, width, height, channels } = input;
1308
- if (width < 24 || height < 24 || channels < 4) {
1309
- return false;
1310
- }
1311
- const border = Math.max(8, Math.floor(Math.min(width, height) * 0.16));
1312
- let sampled = 0;
1313
- let transparent = 0;
1314
- let opaqueGray = 0;
1315
- let lowGray = 0;
1316
- let highGray = 0;
1317
- for (let y = 0; y < height; y += 1) {
1318
- for (let x = 0; x < width; x += 1) {
1319
- const inBorder = x < border || y < border || x >= width - border || y >= height - border;
1320
- if (!inBorder) {
1321
- continue;
1322
- }
1323
- sampled += 1;
1324
- const offset = (y * width + x) * channels;
1325
- const red = data[offset] ?? 0;
1326
- const green = data[offset + 1] ?? 0;
1327
- const blue = data[offset + 2] ?? 0;
1328
- const alpha = data[offset + 3] ?? 255;
1329
- if (alpha < 32) {
1330
- transparent += 1;
1331
- continue;
1332
- }
1333
- const brightness = (red + green + blue) / 3;
1334
- const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 10;
1335
- if (alpha > 240 && grayish && brightness >= 70 && brightness <= 210) {
1336
- opaqueGray += 1;
1337
- if (brightness < 125) {
1338
- lowGray += 1;
1339
- }
1340
- else if (brightness > 145) {
1341
- highGray += 1;
1342
- }
1343
- }
1344
- }
1345
- }
1346
- if (sampled === 0 || transparent / sampled > 0.18) {
1347
- return false;
1348
- }
1349
- return opaqueGray / sampled > 0.58
1350
- && lowGray / sampled > 0.08
1351
- && highGray / sampled > 0.18;
1352
- }
1353
- function looksLikeBakedSolidMattePreview(input) {
1354
- const { data, width, height, channels } = input;
1355
- if (width < 24 || height < 24 || channels < 4) {
1356
- return false;
1357
- }
1358
- const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
1359
- let sampled = 0;
1360
- let transparent = 0;
1361
- let matteLike = 0;
1362
- for (let y = 0; y < height; y += 1) {
1363
- for (let x = 0; x < width; x += 1) {
1364
- const inBorder = x < border || y < border || x >= width - border || y >= height - border;
1365
- if (!inBorder) {
1366
- continue;
1367
- }
1368
- sampled += 1;
1369
- const offset = (y * width + x) * channels;
1370
- const red = data[offset] ?? 0;
1371
- const green = data[offset + 1] ?? 0;
1372
- const blue = data[offset + 2] ?? 0;
1373
- const alpha = data[offset + 3] ?? 255;
1374
- if (alpha < 32) {
1375
- transparent += 1;
1376
- continue;
1377
- }
1378
- const dominant = Math.max(red, green, blue);
1379
- const weakest = Math.min(red, green, blue);
1380
- const chroma = dominant - weakest;
1381
- const brightGreenMatte = green >= 220 && green - Math.max(red, blue) >= 65 && Math.max(red, blue) >= 70;
1382
- const brightRedMatte = red >= 220 && red - Math.max(green, blue) >= 65;
1383
- const brightBlueMatte = blue >= 220 && blue - Math.max(red, green) >= 65;
1384
- const brightTwoChannelMatte = dominant >= 220
1385
- && chroma >= 65
1386
- && ((red >= 220 && green >= 220) || (red >= 220 && blue >= 220) || (green >= 220 && blue >= 220));
1387
- if (brightGreenMatte || brightRedMatte || brightBlueMatte || brightTwoChannelMatte) {
1388
- matteLike += 1;
1389
- }
1390
- }
1391
- }
1392
- if (sampled === 0 || transparent / sampled > 0.18) {
1393
- return false;
1394
- }
1395
- return matteLike / sampled > 0.58;
1396
- }
1397
- function looksLikeBakedNeutralSolidMattePreview(input) {
1398
- const { data, width, height, channels } = input;
1399
- if (width < 24 || height < 24 || channels < 4) {
1400
- return false;
1401
- }
1402
- const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
1403
- let sampled = 0;
1404
- let transparent = 0;
1405
- let neutralMatteLike = 0;
1406
- for (let y = 0; y < height; y += 1) {
1407
- for (let x = 0; x < width; x += 1) {
1408
- const inBorder = x < border || y < border || x >= width - border || y >= height - border;
1409
- if (!inBorder) {
1410
- continue;
1411
- }
1412
- sampled += 1;
1413
- const offset = (y * width + x) * channels;
1414
- const red = data[offset] ?? 0;
1415
- const green = data[offset + 1] ?? 0;
1416
- const blue = data[offset + 2] ?? 0;
1417
- const alpha = data[offset + 3] ?? 255;
1418
- if (alpha < 32) {
1419
- transparent += 1;
1420
- continue;
1421
- }
1422
- const brightness = (red + green + blue) / 3;
1423
- const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 12;
1424
- if (alpha > 240 && grayish && (brightness >= 235 || brightness <= 20)) {
1425
- neutralMatteLike += 1;
1426
- }
1427
- }
1428
- }
1429
- if (sampled === 0 || transparent / sampled > 0.18) {
1430
- return false;
1431
- }
1432
- return neutralMatteLike / sampled > 0.58;
1433
- }
1434
- function looksLikeResidualMatteGridArtifact(input) {
1435
- const { data, width, height, channels } = input;
1436
- if (width < 48 || height < 48 || channels < 4) {
1437
- return false;
1438
- }
1439
- const rowCounts = new Uint32Array(height);
1440
- const columnCounts = new Uint32Array(width);
1441
- let matteLike = 0;
1442
- for (let y = 0; y < height; y += 1) {
1443
- for (let x = 0; x < width; x += 1) {
1444
- const offset = (y * width + x) * channels;
1445
- const red = data[offset] ?? 0;
1446
- const green = data[offset + 1] ?? 0;
1447
- const blue = data[offset + 2] ?? 0;
1448
- const alpha = data[offset + 3] ?? 255;
1449
- if (alpha < 24 || alpha > 190) {
1450
- continue;
1451
- }
1452
- const greenMatte = green >= 35 && green - Math.max(red, blue) >= 25 && red <= 110 && blue <= 110;
1453
- const redMatte = red >= 35 && red - Math.max(green, blue) >= 25 && green <= 120 && blue <= 120;
1454
- const blueMatte = blue >= 35 && blue - Math.max(red, green) >= 25 && red <= 120 && green <= 120;
1455
- const purpleMatte = red >= 35 && blue >= 35 && Math.min(red, blue) - green >= 25 && green <= 120;
1456
- if (!greenMatte && !redMatte && !blueMatte && !purpleMatte) {
1457
- continue;
1458
- }
1459
- matteLike += 1;
1460
- rowCounts[y] += 1;
1461
- columnCounts[x] += 1;
1462
- }
1463
- }
1464
- const matteRatio = matteLike / (width * height);
1465
- if (matteRatio < 0.0015) {
1466
- return false;
1467
- }
1468
- let rowsWithLines = 0;
1469
- for (const count of rowCounts) {
1470
- if (count / width >= 0.12) {
1471
- rowsWithLines += 1;
1472
- }
1473
- }
1474
- let columnsWithLines = 0;
1475
- for (const count of columnCounts) {
1476
- if (count / height >= 0.12) {
1477
- columnsWithLines += 1;
1478
- }
1479
- }
1480
- return rowsWithLines + columnsWithLines >= 3
1481
- && (rowsWithLines >= 2 || columnsWithLines >= 2);
1482
- }
1483
- function looksLikeResidualTransparentGridLineArtifact(input) {
1484
- const { data, width, height, channels } = input;
1485
- if (width < 48 || height < 48 || channels < 4) {
1486
- return false;
1487
- }
1488
- const rowVisibleCounts = new Uint32Array(height);
1489
- const rowSemiCounts = new Uint32Array(height);
1490
- const columnVisibleCounts = new Uint32Array(width);
1491
- const columnSemiCounts = new Uint32Array(width);
1492
- for (let y = 0; y < height; y += 1) {
1493
- for (let x = 0; x < width; x += 1) {
1494
- const offset = (y * width + x) * channels;
1495
- const alpha = data[offset + 3] ?? 255;
1496
- if (alpha <= 0) {
1497
- continue;
1498
- }
1499
- rowVisibleCounts[y] += 1;
1500
- columnVisibleCounts[x] += 1;
1501
- if (alpha < 255) {
1502
- rowSemiCounts[y] += 1;
1503
- columnSemiCounts[x] += 1;
1504
- }
1505
- }
1506
- }
1507
- let rowsWithLines = 0;
1508
- for (let row = 0; row < height; row += 1) {
1509
- const visibleRatio = rowVisibleCounts[row] / width;
1510
- const semiRatio = rowSemiCounts[row] / width;
1511
- if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
1512
- rowsWithLines += 1;
1513
- }
1514
- }
1515
- let columnsWithLines = 0;
1516
- for (let column = 0; column < width; column += 1) {
1517
- const visibleRatio = columnVisibleCounts[column] / height;
1518
- const semiRatio = columnSemiCounts[column] / height;
1519
- if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
1520
- columnsWithLines += 1;
1521
- }
1522
- }
1523
- return rowsWithLines + columnsWithLines >= 3
1524
- && (rowsWithLines >= 2 || columnsWithLines >= 2);
1525
- }
1526
- async function imageHasBakedPreviewBackground(filePath, options = {}) {
1527
- let sharp;
1528
- try {
1529
- const sharpModule = await Promise.resolve().then(() => __importStar(require('sharp')));
1530
- sharp = sharpModule.default ?? sharpModule;
1531
- }
1532
- catch {
1533
- return false;
1534
- }
1535
- try {
1536
- const { data, info } = await sharp(filePath, { failOn: 'error' })
1537
- .rotate()
1538
- .resize(OWNED_RUNTIME_IMAGE_CHECK_SIZE, OWNED_RUNTIME_IMAGE_CHECK_SIZE, { fit: 'fill' })
1539
- .ensureAlpha()
1540
- .raw()
1541
- .toBuffer({ resolveWithObject: true });
1542
- const hasPreviewBackground = looksLikeBakedCheckerboardPreview({
1543
- data,
1544
- width: info.width,
1545
- height: info.height,
1546
- channels: info.channels,
1547
- }) || looksLikeBakedSolidMattePreview({
1548
- data,
1549
- width: info.width,
1550
- height: info.height,
1551
- channels: info.channels,
1552
- }) || (!options.allowNeutralSolidMatte && looksLikeBakedNeutralSolidMattePreview({
1553
- data,
1554
- width: info.width,
1555
- height: info.height,
1556
- channels: info.channels,
1557
- }));
1558
- if (hasPreviewBackground) {
1559
- return true;
1560
- }
1561
- }
1562
- catch {
1563
- return false;
1564
- }
1565
- try {
1566
- const { data, info } = await sharp(filePath, { failOn: 'error' })
1567
- .rotate()
1568
- .resize(OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, { fit: 'inside', withoutEnlargement: true })
1569
- .ensureAlpha()
1570
- .raw()
1571
- .toBuffer({ resolveWithObject: true });
1572
- if (looksLikeResidualMatteGridArtifact({
1573
- data,
1574
- width: info.width,
1575
- height: info.height,
1576
- channels: info.channels,
1577
- }) || looksLikeResidualTransparentGridLineArtifact({
1578
- data,
1579
- width: info.width,
1580
- height: info.height,
1581
- channels: info.channels,
1582
- })) {
1583
- return true;
1584
- }
1585
- }
1586
- catch {
1587
- return false;
1588
- }
1589
- return false;
1590
- }
1591
- async function assertNewGameOwnedRuntimeImageAssetsAreClean(task) {
1592
- const unsafeAssets = [];
1593
- for (const ownedAsset of task.ownedAssets) {
1594
- if (!isOwnedRuntimeImageAsset(task, ownedAsset)) {
1595
- continue;
1596
- }
1597
- const primaryPath = ownedAsset.filePaths?.primary;
1598
- if (!primaryPath) {
1599
- continue;
1600
- }
1601
- if (await imageHasBakedPreviewBackground(primaryPath, {
1602
- allowNeutralSolidMatte: isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset),
1603
- })) {
1604
- unsafeAssets.push(`${ownedAsset.name}:${ownedAsset.runtimeKey}`);
1605
- }
1606
- }
1607
- if (unsafeAssets.length > 0) {
1608
- throw new Error(`agent_task_owned_image_asset_checkerboard_background: owned runtime image assets must not contain baked transparency checkerboard, matte-color, or preview backgrounds. Recreate the asset through the PlayDrop make-assets workflow, use a cleaner PlayDrop asset, or use the image as non-runtime listing art only. Unsafe owned assets: ${unsafeAssets.join(', ')}`);
1609
- }
1610
- }
1611
1253
  const RUNTIME_SOURCE_EXTENSIONS = new Set([
1612
1254
  '.html',
1613
1255
  '.js',
@@ -1700,24 +1342,11 @@ function assertNoLaunchCheckConditionalSource(runtimeSource) {
1700
1342
  throw new Error(`agent_task_launch_check_conditional_disallowed: game source must not sniff validation or local route context. First match at ${environmentSniff.location}.`);
1701
1343
  }
1702
1344
  }
1703
- function hasProceduralAssetStrategy(task) {
1704
- const strategy = task.design?.assetStrategy;
1705
- return strategy?.value === 'procedural';
1706
- }
1707
1345
  function hasDeclaredRuntimeAssetDependency(task) {
1708
1346
  return task.uses.packs.length > 0
1709
1347
  || task.uses.assets.length > 0
1710
1348
  || task.ownedAssets.some((ownedAsset) => !isListingOwnedAsset(task, ownedAsset));
1711
1349
  }
1712
- function assertNewGameAssetStrategy(task) {
1713
- if (hasDeclaredRuntimeAssetDependency(task)) {
1714
- return;
1715
- }
1716
- if (hasProceduralAssetStrategy(task)) {
1717
- return;
1718
- }
1719
- throw new Error('agent_task_asset_strategy_missing: new game tasks must either declare true runtime assets/packs or set design.assetStrategy to { "value": "procedural", "note": "..." } for an honest procedural draft.');
1720
- }
1721
1350
  function isListingOwnedAsset(task, ownedAsset) {
1722
1351
  const primaryPath = ownedAsset.filePaths?.primary;
1723
1352
  if (!primaryPath) {
@@ -1966,7 +1595,6 @@ async function assertNewGameDeclaredAssetsAreUsedAtRuntime(input) {
1966
1595
  const runtimeSource = readRuntimeSourceText(input.task.projectDir);
1967
1596
  const source = runtimeSource.source;
1968
1597
  assertNoLaunchCheckConditionalSource(runtimeSource);
1969
- assertNewGameAssetStrategy(input.task);
1970
1598
  if (!hasDeclaredRuntimeAssetDependency(input.task)) {
1971
1599
  return;
1972
1600
  }
@@ -1984,8 +1612,7 @@ async function assertNewGameDeclaredAssetsAreUsedAtRuntime(input) {
1984
1612
  }
1985
1613
  const manifest = input.task.ownedAssets.length > 0
1986
1614
  ? await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
1987
- client: input.client,
1988
- apiBase: input.apiBase,
1615
+ apiBase: 'http://127.0.0.1',
1989
1616
  task: {
1990
1617
  ...input.task,
1991
1618
  uses: { assets: [], packs: [] },
@@ -2183,6 +1810,35 @@ function readRuntimeSourceLastModifiedAt(rootDir) {
2183
1810
  walk(root);
2184
1811
  return latest > 0 ? new Date(latest).toISOString() : null;
2185
1812
  }
1813
+ async function assertWorkerAppLocalUploadPreflight(input) {
1814
+ if (input.kind === 'GAME_UPDATE') {
1815
+ return assertTaskPlaytestEvidenceManifest(input.task);
1816
+ }
1817
+ (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(input.task);
1818
+ if (input.kind === 'NEW_GAME') {
1819
+ (0, listingPreflight_1.assertAgentNewGameArtDirectionPreflight)(input.task);
1820
+ }
1821
+ if (!appTaskSatisfiesPlaydropAssetRequirement(input.task, input.playdropAssetRequirement)) {
1822
+ 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.');
1823
+ }
1824
+ await (0, ownedRuntimeImageValidation_1.assertOwnedRuntimeImagesAreValid)(input.task);
1825
+ assertRequestedPlaydropAssetsAreUsedAtRuntime({
1826
+ task: input.task,
1827
+ requirement: input.playdropAssetRequirement,
1828
+ creatorRequest: input.creatorRequest,
1829
+ });
1830
+ await assertNewGameDeclaredAssetsAreUsedAtRuntime({
1831
+ creatorUsername: input.creatorUsername?.trim() ?? '',
1832
+ task: input.task,
1833
+ });
1834
+ (0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(input.task);
1835
+ (0, listingPreflight_1.assertAgentGameCaptureReportPreflight)(input.task, {
1836
+ requireCaptureReport: input.executionTarget === 'FIRST_PARTY',
1837
+ });
1838
+ return assertTaskPlaytestEvidenceManifest(input.task, {
1839
+ requireOutcomeProof: input.kind === 'NEW_GAME',
1840
+ });
1841
+ }
2186
1842
  // Task upload path used by "playdrop task upload": same internal pipeline as
2187
1843
  // "playdrop project publish", but always as one PRIVATE draft version for the
2188
1844
  // task's creator, with browser launch checks on before the version is created.
@@ -2229,38 +1885,14 @@ async function publishWorkerAppProject(input) {
2229
1885
  throw new Error(`agent_task_remix_ref_mismatch:${catalogueRemixRef}:${expectedRemixRef}`);
2230
1886
  }
2231
1887
  }
2232
- if (input.kind === 'GAME_UPDATE') {
2233
- assertTaskPlaytestEvidenceManifest(task);
2234
- }
2235
- if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2236
- (0, listingPreflight_1.assertAgentGameHeroListingPreflight)(task);
2237
- }
2238
- if (input.kind === 'NEW_GAME') {
2239
- (0, listingPreflight_1.assertAgentNewGameArtDirectionPreflight)(task);
2240
- }
2241
- if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
2242
- 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.');
2243
- }
2244
- if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2245
- await assertNewGameDirectPlaydropAssetsAreRuntimeSafe({
2246
- client: input.client,
2247
- task,
2248
- });
2249
- await assertNewGameOwnedRuntimeImageAssetsAreClean(task);
2250
- }
2251
- assertRequestedPlaydropAssetsAreUsedAtRuntime({
1888
+ const playtestProof = await assertWorkerAppLocalUploadPreflight({
2252
1889
  task,
2253
- requirement: input.playdropAssetRequirement,
1890
+ kind: input.kind,
1891
+ executionTarget: input.executionTarget,
1892
+ creatorUsername,
1893
+ playdropAssetRequirement: input.playdropAssetRequirement,
2254
1894
  creatorRequest: input.creatorRequest,
2255
1895
  });
2256
- if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2257
- await assertNewGameDeclaredAssetsAreUsedAtRuntime({
2258
- client: input.client,
2259
- apiBase: input.apiBase,
2260
- creatorUsername,
2261
- task,
2262
- });
2263
- }
2264
1896
  await assertDeclared3dPlaydropPacksContainRuntimeModels({
2265
1897
  client: input.client,
2266
1898
  task,
@@ -2284,13 +1916,6 @@ async function publishWorkerAppProject(input) {
2284
1916
  taskId: input.taskId,
2285
1917
  taskToken,
2286
1918
  };
2287
- let playtestProof = null;
2288
- if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
2289
- (0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(task);
2290
- playtestProof = assertTaskPlaytestEvidenceManifest(task, {
2291
- requireOutcomeProof: input.kind === 'NEW_GAME',
2292
- });
2293
- }
2294
1919
  const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
2295
1920
  creatorUsername,
2296
1921
  apiBase: input.apiBase,
@@ -11,6 +11,7 @@ const taskUtils_1 = require("../taskUtils");
11
11
  const uploadLog_1 = require("../uploadLog");
12
12
  const listingPreflight_1 = require("../listingPreflight");
13
13
  const upload_content_1 = require("./upload-content");
14
+ const upload_1 = require("./upload");
14
15
  function buildLocalAssetSpecLookups(tasks) {
15
16
  const exactByRef = new Map();
16
17
  const uniqueByNameVersion = new Map();
@@ -192,7 +193,20 @@ async function validate(pathOrName, options = {}) {
192
193
  const entityId = task.name;
193
194
  try {
194
195
  if (task.kind === 'app') {
195
- (0, listingPreflight_1.assertProjectListingPreflight)(task);
196
+ const workerContext = (0, listingPreflight_1.readWorkerUploadPreflightContext)(task.projectDir);
197
+ if (workerContext) {
198
+ await (0, upload_1.assertWorkerAppLocalUploadPreflight)({
199
+ task,
200
+ kind: workerContext.kind,
201
+ executionTarget: workerContext.target,
202
+ creatorUsername: workerContext.creatorUsername,
203
+ creatorRequest: workerContext.creatorRequest,
204
+ playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(workerContext.creatorRequest),
205
+ });
206
+ }
207
+ else {
208
+ (0, listingPreflight_1.assertProjectListingPreflight)(task);
209
+ }
196
210
  await (0, apps_1.validateAppTask)(task);
197
211
  (0, apps_1.collectAppValidationWarnings)(task, 'source').forEach((warning) => warnings.add(warning));
198
212
  }
@@ -310,6 +310,22 @@ export declare function acquireWorkerSupervisorLock(input: {
310
310
  file?: string;
311
311
  }): WorkerSupervisorLock;
312
312
  export declare function releaseWorkerSupervisorLock(lock: WorkerSupervisorLock): void;
313
+ export type TaskUploadLockOwner = {
314
+ schemaVersion: 1;
315
+ command: 'task upload';
316
+ pid: number;
317
+ processStartIdentity: string;
318
+ createdAt: string;
319
+ };
320
+ export type TaskUploadLock = {
321
+ file: string;
322
+ owner: TaskUploadLockOwner;
323
+ };
324
+ export declare function resolveTaskUploadLockFile(workspaceDir: string): string;
325
+ export declare function removeStaleTaskUploadLockIfUnchanged(file: string, observedHolder: TaskUploadLockOwner): boolean;
326
+ export declare function acquireTaskUploadLock(workspaceDir: string): TaskUploadLock;
327
+ export declare function releaseTaskUploadLock(lock: TaskUploadLock): void;
328
+ export declare function withTaskUploadLock<T>(workspaceDir: string, work: () => Promise<T>): Promise<T>;
313
329
  export declare function readActivePersonalWorkerRuntimeState(): WorkerRuntimeState | null;
314
330
  export declare function parseLaunchAgentProgramArguments(plist: string): string[];
315
331
  export declare function assertLaunchAgentPlistCompatible(input: {