@playdrop/playdrop-cli 0.5.2 → 0.5.4

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 (118) hide show
  1. package/config/client-meta.json +4 -4
  2. package/dist/apps/build.js +49 -6
  3. package/dist/apps/index.d.ts +2 -0
  4. package/dist/apps/index.js +2 -0
  5. package/dist/apps/upload.d.ts +2 -0
  6. package/dist/apps/upload.js +126 -28
  7. package/dist/assetSpecs.d.ts +16 -0
  8. package/dist/assetSpecs.js +263 -0
  9. package/dist/assets/model-artifacts.js +3 -0
  10. package/dist/catalogue.d.ts +57 -3
  11. package/dist/catalogue.js +342 -16
  12. package/dist/commandContext.d.ts +6 -2
  13. package/dist/commandContext.js +144 -20
  14. package/dist/commands/accounts.d.ts +2 -0
  15. package/dist/commands/accounts.js +48 -0
  16. package/dist/commands/ads.d.ts +8 -0
  17. package/dist/commands/ads.js +124 -0
  18. package/dist/commands/boosts.d.ts +25 -0
  19. package/dist/commands/boosts.js +209 -0
  20. package/dist/commands/browse.d.ts +6 -1
  21. package/dist/commands/browse.js +365 -124
  22. package/dist/commands/capture.js +30 -9
  23. package/dist/commands/captureListing.d.ts +53 -0
  24. package/dist/commands/captureListing.js +815 -0
  25. package/dist/commands/create.d.ts +1 -0
  26. package/dist/commands/create.js +183 -3
  27. package/dist/commands/credits.d.ts +6 -0
  28. package/dist/commands/credits.js +47 -1
  29. package/dist/commands/detail.js +38 -4
  30. package/dist/commands/dev.js +169 -192
  31. package/dist/commands/devServer.d.ts +26 -3
  32. package/dist/commands/devServer.js +415 -72
  33. package/dist/commands/login.js +10 -2
  34. package/dist/commands/logout.d.ts +6 -1
  35. package/dist/commands/logout.js +25 -3
  36. package/dist/commands/search.d.ts +5 -0
  37. package/dist/commands/search.js +139 -17
  38. package/dist/commands/tags.d.ts +7 -0
  39. package/dist/commands/tags.js +63 -0
  40. package/dist/commands/upload-content.d.ts +13 -3
  41. package/dist/commands/upload-content.js +86 -20
  42. package/dist/commands/upload.d.ts +2 -0
  43. package/dist/commands/upload.js +187 -11
  44. package/dist/commands/validate.js +163 -2
  45. package/dist/commands/versionsBrowse.js +128 -91
  46. package/dist/commands/whoami.js +10 -2
  47. package/dist/config.d.ts +37 -0
  48. package/dist/config.js +205 -3
  49. package/dist/index.js +177 -5
  50. package/dist/refs.d.ts +2 -2
  51. package/dist/refs.js +13 -1
  52. package/dist/taskSelection.js +6 -3
  53. package/dist/taskUtils.d.ts +2 -2
  54. package/dist/taskUtils.js +1 -0
  55. package/dist/uploadLog.d.ts +1 -1
  56. package/dist/uploadLog.js +2 -2
  57. package/dist/workspaceAuth.d.ts +14 -0
  58. package/dist/workspaceAuth.js +75 -0
  59. package/node_modules/@playdrop/ai-client/package.json +1 -1
  60. package/node_modules/@playdrop/api-client/dist/client.d.ts +139 -10
  61. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  62. package/node_modules/@playdrop/api-client/dist/client.js +6 -0
  63. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +9 -1
  64. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  65. package/node_modules/@playdrop/api-client/dist/domains/admin.js +45 -0
  66. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +7 -1
  67. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  68. package/node_modules/@playdrop/api-client/dist/domains/apps.js +58 -0
  69. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts +2 -0
  70. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts.map +1 -1
  71. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.js +16 -0
  72. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +44 -2
  73. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  74. package/node_modules/@playdrop/api-client/dist/domains/assets.js +260 -3
  75. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +22 -1
  76. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  77. package/node_modules/@playdrop/api-client/dist/domains/payments.js +228 -0
  78. package/node_modules/@playdrop/api-client/dist/domains/search.d.ts.map +1 -1
  79. package/node_modules/@playdrop/api-client/dist/domains/search.js +39 -11
  80. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts +34 -0
  81. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts.map +1 -0
  82. package/node_modules/@playdrop/api-client/dist/domains/tags.js +111 -0
  83. package/node_modules/@playdrop/api-client/dist/index.d.ts +69 -1
  84. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  85. package/node_modules/@playdrop/api-client/dist/index.js +74 -0
  86. package/node_modules/@playdrop/api-client/package.json +1 -1
  87. package/node_modules/@playdrop/boxel-core/package.json +1 -1
  88. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  89. package/node_modules/@playdrop/config/client-meta.json +4 -4
  90. package/node_modules/@playdrop/config/dist/src/constants.d.ts +11 -0
  91. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  92. package/node_modules/@playdrop/config/dist/src/constants.js +12 -1
  93. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  94. package/node_modules/@playdrop/config/package.json +1 -1
  95. package/node_modules/@playdrop/types/dist/api.d.ts +366 -6
  96. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  97. package/node_modules/@playdrop/types/dist/api.js +52 -1
  98. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +7 -1
  99. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  100. package/node_modules/@playdrop/types/dist/asset-spec-contract-meta-schema.json +86 -0
  101. package/node_modules/@playdrop/types/dist/asset-spec.d.ts +163 -0
  102. package/node_modules/@playdrop/types/dist/asset-spec.d.ts.map +1 -0
  103. package/node_modules/@playdrop/types/dist/asset-spec.js +101 -0
  104. package/node_modules/@playdrop/types/dist/asset.d.ts +23 -6
  105. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  106. package/node_modules/@playdrop/types/dist/asset.js +4 -1
  107. package/node_modules/@playdrop/types/dist/graph.d.ts +4 -2
  108. package/node_modules/@playdrop/types/dist/graph.d.ts.map +1 -1
  109. package/node_modules/@playdrop/types/dist/graph.js +9 -2
  110. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  111. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  112. package/node_modules/@playdrop/types/dist/index.js +1 -0
  113. package/node_modules/@playdrop/types/dist/version.d.ts +13 -0
  114. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  115. package/node_modules/@playdrop/types/dist/version.js +21 -0
  116. package/node_modules/@playdrop/types/package.json +6 -1
  117. package/node_modules/@playdrop/vox-three/package.json +1 -1
  118. package/package.json +3 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.1",
2
+ "version": "0.5.4",
3
3
  "build": 1,
4
4
  "platforms": {
5
5
  "ios": {
@@ -26,11 +26,11 @@
26
26
  },
27
27
  "clients": {
28
28
  "web": {
29
- "minimumVersion": "0.5.1",
29
+ "minimumVersion": "0.5.4",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.5.1",
33
+ "minimumVersion": "0.5.4",
34
34
  "minimumBuild": 1
35
35
  },
36
36
  "apple": {
@@ -38,7 +38,7 @@
38
38
  "minimumBuild": 1
39
39
  },
40
40
  "cli": {
41
- "minimumVersion": "0.5.1"
41
+ "minimumVersion": "0.5.4"
42
42
  }
43
43
  }
44
44
  }
@@ -190,7 +190,7 @@ function readProjectDirEntries(current) {
190
190
  return [];
191
191
  }
192
192
  }
193
- function appendIncludedProjectFile(rootDir, includePath, fileByRelativePath, results, enforceReservedBundleNames) {
193
+ function appendIncludedProjectFile(rootDir, includePath, excludedRelativeFiles, fileByRelativePath, results, enforceReservedBundleNames) {
194
194
  const normalizedIncludePath = (0, node_path_1.normalize)(includePath).split(node_path_1.sep).join('/');
195
195
  if (!normalizedIncludePath || normalizedIncludePath === '.') {
196
196
  return;
@@ -198,6 +198,9 @@ function appendIncludedProjectFile(rootDir, includePath, fileByRelativePath, res
198
198
  if (normalizedIncludePath.startsWith('..')) {
199
199
  throw new Error(`[apps][build] Invalid include file path "${includePath}".`);
200
200
  }
201
+ if (excludedRelativeFiles.has(normalizedIncludePath)) {
202
+ return;
203
+ }
201
204
  if (fileByRelativePath.has(normalizedIncludePath)) {
202
205
  return;
203
206
  }
@@ -223,8 +226,12 @@ function appendIncludedProjectFile(rootDir, includePath, fileByRelativePath, res
223
226
  results.push(record);
224
227
  fileByRelativePath.set(record.relativePath, record);
225
228
  }
229
+ // eslint-disable-next-line complexity -- Source archive collection needs one pass to enforce ignore/include/exclude rules deterministically.
226
230
  function collectProjectFiles(rootDir, rules, options) {
227
231
  const includeRelativeFiles = options?.includeRelativeFiles ?? [];
232
+ const excludeRelativeFiles = new Set(Array.from(options?.excludeRelativeFiles ?? [])
233
+ .map((filePath) => (0, node_path_1.normalize)(filePath).split(node_path_1.sep).join('/'))
234
+ .filter((filePath) => filePath.length > 0 && filePath !== '.'));
228
235
  const enforceReservedBundleNames = options?.enforceReservedBundleNames ?? false;
229
236
  const results = [];
230
237
  const fileByRelativePath = new Map();
@@ -263,6 +270,8 @@ function collectProjectFiles(rootDir, rules, options) {
263
270
  }
264
271
  if (!stats.isFile())
265
272
  continue;
273
+ if (excludeRelativeFiles.has(relativePath))
274
+ continue;
266
275
  const record = {
267
276
  absolutePath,
268
277
  relativePath: relativePath.split('/').join('/'),
@@ -273,7 +282,7 @@ function collectProjectFiles(rootDir, rules, options) {
273
282
  }
274
283
  }
275
284
  for (const includePath of includeRelativeFiles) {
276
- appendIncludedProjectFile(rootDir, includePath, fileByRelativePath, results, enforceReservedBundleNames);
285
+ appendIncludedProjectFile(rootDir, includePath, excludeRelativeFiles, fileByRelativePath, results, enforceReservedBundleNames);
277
286
  }
278
287
  results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
279
288
  return results;
@@ -288,6 +297,32 @@ function isReservedBundlePath(relativePath) {
288
297
  function resolveRuntimeRoot(task) {
289
298
  return (0, node_path_1.resolve)((0, node_path_1.dirname)(task.filePath));
290
299
  }
300
+ function collectSeparatelyUploadedSourceFiles(task) {
301
+ const excludedFiles = new Set();
302
+ const addFile = (filePath) => {
303
+ if (!filePath) {
304
+ return;
305
+ }
306
+ const absolutePath = (0, node_path_1.resolve)(filePath);
307
+ if (!isPathWithinRoot(task.projectDir, absolutePath)) {
308
+ return;
309
+ }
310
+ const relativePath = normalizeRelativePath(task.projectDir, absolutePath);
311
+ if (!relativePath || relativePath.startsWith('..')) {
312
+ return;
313
+ }
314
+ excludedFiles.add(relativePath);
315
+ };
316
+ addFile(task.listing?.iconPath);
317
+ addFile(task.listing?.heroPortraitPath);
318
+ addFile(task.listing?.heroLandscapePath);
319
+ task.listing?.screenshotPortraitPaths?.forEach(addFile);
320
+ task.listing?.screenshotLandscapePaths?.forEach(addFile);
321
+ task.listing?.videoPortraitPaths?.forEach(addFile);
322
+ task.listing?.videoLandscapePaths?.forEach(addFile);
323
+ task.achievements?.forEach((definition) => addFile(definition.iconPath));
324
+ return Array.from(excludedFiles).sort((left, right) => left.localeCompare(right));
325
+ }
291
326
  function resolveBundleEntryPoint(task, runtimeRoot) {
292
327
  const entryPoint = normalizeRelativePath(runtimeRoot, task.filePath);
293
328
  if (!entryPoint || entryPoint.startsWith('..')) {
@@ -451,23 +486,31 @@ function createZipArchive(entries) {
451
486
  function createSourceArchive(task) {
452
487
  const rules = buildIgnoreRules(task.projectDir);
453
488
  const includeFiles = ['README.md', 'AGENTS.md'];
489
+ const excludedFiles = collectSeparatelyUploadedSourceFiles(task);
454
490
  if (task.catalogueAbsolutePath && (0, node_fs_1.existsSync)(task.catalogueAbsolutePath)) {
455
491
  const relativeCataloguePath = normalizeRelativePath(task.projectDir, task.catalogueAbsolutePath) || 'catalogue.json';
456
492
  includeFiles.push(relativeCataloguePath);
457
493
  }
458
494
  if (task.isTypescriptProject) {
459
- const cached = typescriptSourceCache.get(task.projectDir);
495
+ const cacheKey = [task.projectDir, ...excludedFiles].join('\u0000');
496
+ const cached = typescriptSourceCache.get(cacheKey);
460
497
  if (cached) {
461
498
  return cached;
462
499
  }
463
- const entries = collectProjectFiles(task.projectDir, rules, { includeRelativeFiles: includeFiles });
500
+ const entries = collectProjectFiles(task.projectDir, rules, {
501
+ includeRelativeFiles: includeFiles,
502
+ excludeRelativeFiles: excludedFiles,
503
+ });
464
504
  const buffer = createZipArchive(entries);
465
505
  const hash = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
466
506
  const record = { buffer, hash };
467
- typescriptSourceCache.set(task.projectDir, record);
507
+ typescriptSourceCache.set(cacheKey, record);
468
508
  return record;
469
509
  }
470
- const entries = collectProjectFiles(task.projectDir, rules, { includeRelativeFiles: includeFiles });
510
+ const entries = collectProjectFiles(task.projectDir, rules, {
511
+ includeRelativeFiles: includeFiles,
512
+ excludeRelativeFiles: excludedFiles,
513
+ });
471
514
  const buffer = createZipArchive(entries);
472
515
  const hash = (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
473
516
  return { buffer, hash };
@@ -9,6 +9,8 @@ export type AppPipelineResult = {
9
9
  };
10
10
  export type AppPipelineOptions = {
11
11
  skipEcs?: boolean;
12
+ skipReview?: boolean;
13
+ clearTags?: boolean;
12
14
  creatorUsername?: string;
13
15
  };
14
16
  export declare function runAppPipeline(client: ApiClient, task: AppTask, options?: AppPipelineOptions): Promise<AppPipelineResult>;
@@ -16,6 +16,8 @@ async function runAppPipeline(client, task, options) {
16
16
  const artifacts = isExternal ? null : await (0, build_1.buildApp)(task);
17
17
  const uploadOptions = {
18
18
  skipEcs: options?.skipEcs,
19
+ skipReview: options?.skipReview,
20
+ clearTags: options?.clearTags,
19
21
  creatorUsername: options?.creatorUsername,
20
22
  };
21
23
  const upload = await (0, upload_1.uploadApp)(client, task, artifacts, uploadOptions);
@@ -14,6 +14,8 @@ export type AppUploadResult = {
14
14
  };
15
15
  export type AppUploadOptions = {
16
16
  skipEcs?: boolean;
17
+ skipReview?: boolean;
18
+ clearTags?: boolean;
17
19
  creatorUsername?: string;
18
20
  };
19
21
  export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.uploadApp = uploadApp;
4
4
  const types_1 = require("@playdrop/types");
5
+ const config_1 = require("@playdrop/config");
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_path_1 = require("node:path");
7
8
  const http_1 = require("../http");
@@ -24,16 +25,68 @@ function isMp4Signature(content) {
24
25
  }
25
26
  return content.toString('ascii', 4, 8) === 'ftyp';
26
27
  }
27
- /**
28
- * Create a File object from a file path with specified MIME type.
29
- */
30
- function createFileFromPath(filePath, mimeType, validator) {
28
+ function createUploadValidationError(code, message) {
29
+ const error = new Error(`${code}: ${message}`);
30
+ error.code = code;
31
+ return error;
32
+ }
33
+ function formatLimitBytes(maxBytes) {
34
+ if (maxBytes >= 1024 * 1024) {
35
+ return `${maxBytes / 1024 / 1024}MB`;
36
+ }
37
+ return `${maxBytes / 1024}KB`;
38
+ }
39
+ function createFileFromPath(filePath, mimeType, maxBytes, sizeCode, typeCode, label, validator) {
31
40
  const content = (0, node_fs_1.readFileSync)(filePath);
41
+ if (content.length > maxBytes) {
42
+ throw createUploadValidationError(sizeCode, `${label} must be under ${formatLimitBytes(maxBytes)}. Received ${content.length} bytes from ${filePath}.`);
43
+ }
32
44
  if (validator && !validator(content)) {
33
- throw new Error(`Invalid listing file signature for ${filePath}. Expected ${mimeType}.`);
45
+ throw createUploadValidationError(typeCode, `${label} must match ${mimeType}. Invalid file signature at ${filePath}.`);
34
46
  }
35
47
  const name = (0, node_path_1.basename)(filePath);
36
- return new File([content], name, { type: mimeType });
48
+ return {
49
+ file: new File([content], name, { type: mimeType }),
50
+ size: content.length,
51
+ };
52
+ }
53
+ // eslint-disable-next-line complexity -- Source archive failures need distinct guidance when publish-only media is already excluded.
54
+ function ensurePreparedArtifactSize(file, maxBytes, code, label, task) {
55
+ if (!file) {
56
+ return 0;
57
+ }
58
+ if (file.size > maxBytes) {
59
+ if (code === 'source_too_large' && task) {
60
+ const hasSeparatePublishMedia = Boolean(task.listing?.iconPath
61
+ || task.listing?.heroPortraitPath
62
+ || task.listing?.heroLandscapePath
63
+ || task.listing?.screenshotPortraitPaths?.length
64
+ || task.listing?.screenshotLandscapePaths?.length
65
+ || task.listing?.videoPortraitPaths?.length
66
+ || task.listing?.videoLandscapePaths?.length
67
+ || task.achievements?.length);
68
+ const suffix = hasSeparatePublishMedia
69
+ ? ' Listing and achievement media are uploaded separately and excluded from source.zip. The remaining source archive is still too large.'
70
+ : '';
71
+ throw createUploadValidationError(code, `${label} must be under ${formatLimitBytes(maxBytes)}. Received ${file.size} bytes.${suffix}`);
72
+ }
73
+ throw createUploadValidationError(code, `${label} must be under ${formatLimitBytes(maxBytes)}. Received ${file.size} bytes.`);
74
+ }
75
+ return file.size;
76
+ }
77
+ function accumulateUploadBytes(taskName, currentTotal, nextBytes) {
78
+ const nextTotal = currentTotal + nextBytes;
79
+ if (nextTotal > config_1.MAX_VERSION_TOTAL_BYTES) {
80
+ throw createUploadValidationError('upload_too_large', `Total upload for "${taskName}" exceeds ${formatLimitBytes(config_1.MAX_VERSION_TOTAL_BYTES)}. Received ${nextTotal} bytes.`);
81
+ }
82
+ return nextTotal;
83
+ }
84
+ function pushPreparedFile(taskName, currentTotal, prepared) {
85
+ return accumulateUploadBytes(taskName, currentTotal, prepared.size);
86
+ }
87
+ function pushPreparedArtifact(task, currentTotal, file, maxBytes, code, label) {
88
+ const bytes = ensurePreparedArtifactSize(file, maxBytes, code, label, task);
89
+ return accumulateUploadBytes(task.name, currentTotal, bytes);
37
90
  }
38
91
  async function uploadApp(client, task, artifacts, options) {
39
92
  // Version is required - no legacy fallback
@@ -46,28 +99,37 @@ async function uploadApp(client, task, artifacts, options) {
46
99
  /**
47
100
  * Upload an app version using the versioned API.
48
101
  */
102
+ // eslint-disable-next-line complexity -- Upload assembly mirrors the versioned multipart contract in one place.
49
103
  async function uploadAppVersion(client, task, artifacts, options) {
50
104
  const creatorUsername = typeof options?.creatorUsername === 'string' ? options.creatorUsername.trim() : '';
51
105
  if (!creatorUsername) {
52
106
  throw new Error(`Missing creator username for app "${task.name}" upload.`);
53
107
  }
108
+ let totalUploadBytes = 0;
109
+ const achievements = task.achievements ?? [];
110
+ const leaderboards = task.leaderboards ?? [];
111
+ const listing = task.listing;
54
112
  const versionOptions = {
55
113
  version: task.version,
56
114
  releaseNotes: task.releaseNotes,
57
115
  visibility: task.versionVisibility,
58
116
  remixRef: task.remix,
117
+ tags: task.tags,
118
+ clearTags: options?.clearTags,
59
119
  surfaceTargets: task.surfaceTargets,
120
+ assetSpecSupport: task.assetSpecSupport,
60
121
  entryPoint: artifacts?.metadata?.entryPoint ?? undefined,
61
122
  authMode: task.authMode,
62
123
  controllerMode: task.controllerMode,
63
124
  previewable: task.previewable,
125
+ skipReview: options?.skipReview,
64
126
  // App metadata (used when creating app if it doesn't exist)
65
127
  displayName: task.displayName,
66
128
  description: task.description,
67
129
  type: task.type,
68
130
  emoji: task.emoji ?? undefined,
69
131
  color: task.color ?? undefined,
70
- achievements: task.achievements.map((definition) => ({
132
+ achievements: achievements.map((definition) => ({
71
133
  key: definition.key,
72
134
  displayName: definition.displayName,
73
135
  description: definition.description,
@@ -77,7 +139,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
77
139
  icon: definition.icon,
78
140
  status: definition.status,
79
141
  })),
80
- leaderboards: task.leaderboards.map((definition) => ({
142
+ leaderboards: leaderboards.map((definition) => ({
81
143
  key: definition.key,
82
144
  displayName: definition.displayName,
83
145
  description: definition.description,
@@ -86,10 +148,14 @@ async function uploadAppVersion(client, task, artifacts, options) {
86
148
  ...(definition.unitLabel ? { unitLabel: definition.unitLabel } : {}),
87
149
  status: definition.status,
88
150
  })),
89
- achievementIconFiles: task.achievements.map((definition) => ({
90
- key: definition.key,
91
- file: createFileFromPath(definition.iconPath, 'image/png', isPngSignature),
92
- })),
151
+ achievementIconFiles: achievements.map((definition) => {
152
+ const prepared = createFileFromPath(definition.iconPath, 'image/png', config_1.MAX_VERSION_ICON_BYTES, 'achievement_icon_too_large', 'invalid_achievement_icon_content_type', `Achievement icon "${definition.key}"`, isPngSignature);
153
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
154
+ return {
155
+ key: definition.key,
156
+ file: prepared.file,
157
+ };
158
+ }),
93
159
  };
94
160
  // For EXTERNAL hosting mode, use externalUrl instead of bundle
95
161
  if (task.hostingMode === 'EXTERNAL') {
@@ -97,40 +163,66 @@ async function uploadAppVersion(client, task, artifacts, options) {
97
163
  }
98
164
  else if (artifacts) {
99
165
  // HOSTED mode - include bundle and optional files
166
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.bundleFile, config_1.MAX_VERSION_BUNDLE_BYTES, 'bundle_too_large', 'Bundle');
167
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.sourceFile, config_1.MAX_APP_SOURCE_BYTES, 'source_too_large', 'Source archive');
100
168
  versionOptions.bundle = artifacts.bundleFile;
101
169
  versionOptions.source = artifacts.sourceFile;
102
170
  // Include ecs/server files unless skipEcs is set
103
171
  if (!options?.skipEcs) {
104
172
  if (artifacts.ecsFile) {
173
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.ecsFile, config_1.MAX_APP_ECS_BYTES, 'ecs_too_large', 'ECS config');
105
174
  versionOptions.ecs = artifacts.ecsFile;
106
175
  }
107
176
  if (artifacts.serverFile) {
177
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.serverFile, config_1.MAX_APP_SERVER_BYTES, 'server_too_large', 'Server.js');
108
178
  versionOptions.server = artifacts.serverFile;
109
179
  }
110
180
  }
111
181
  }
112
182
  // Add listing assets if provided
113
- if (task.listing) {
114
- if (task.listing.iconPath) {
115
- versionOptions.icon = createFileFromPath(task.listing.iconPath, 'image/png', isPngSignature);
183
+ if (listing) {
184
+ if (listing.iconPath) {
185
+ const prepared = createFileFromPath(listing.iconPath, 'image/png', config_1.MAX_VERSION_ICON_BYTES, 'icon_too_large', 'invalid_icon_content_type', 'Icon', isPngSignature);
186
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
187
+ versionOptions.icon = prepared.file;
116
188
  }
117
- if (task.listing.heroPortraitPath) {
118
- versionOptions.heroPortrait = createFileFromPath(task.listing.heroPortraitPath, 'image/png', isPngSignature);
189
+ if (listing.heroPortraitPath) {
190
+ const prepared = createFileFromPath(listing.heroPortraitPath, 'image/png', config_1.MAX_VERSION_HERO_BYTES, 'hero_portrait_too_large', 'invalid_hero_portrait_content_type', 'Hero portrait', isPngSignature);
191
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
192
+ versionOptions.heroPortrait = prepared.file;
119
193
  }
120
- if (task.listing.heroLandscapePath) {
121
- versionOptions.heroLandscape = createFileFromPath(task.listing.heroLandscapePath, 'image/png', isPngSignature);
194
+ if (listing.heroLandscapePath) {
195
+ const prepared = createFileFromPath(listing.heroLandscapePath, 'image/png', config_1.MAX_VERSION_HERO_BYTES, 'hero_landscape_too_large', 'invalid_hero_landscape_content_type', 'Hero landscape', isPngSignature);
196
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
197
+ versionOptions.heroLandscape = prepared.file;
122
198
  }
123
- if (task.listing.screenshotPortraitPaths.length > 0) {
124
- versionOptions.screenshotsPortrait = task.listing.screenshotPortraitPaths.map((p) => createFileFromPath(p, 'image/png', isPngSignature));
199
+ if ((listing.screenshotPortraitPaths ?? []).length > 0) {
200
+ versionOptions.screenshotsPortrait = listing.screenshotPortraitPaths.map((filePath, index) => {
201
+ const prepared = createFileFromPath(filePath, 'image/png', config_1.MAX_VERSION_SCREENSHOT_BYTES, 'screenshot_portrait_too_large', 'invalid_screenshot_portrait_content_type', `Portrait screenshot ${index + 1}`, isPngSignature);
202
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
203
+ return prepared.file;
204
+ });
125
205
  }
126
- if (task.listing.screenshotLandscapePaths.length > 0) {
127
- versionOptions.screenshotsLandscape = task.listing.screenshotLandscapePaths.map((p) => createFileFromPath(p, 'image/png', isPngSignature));
206
+ if ((listing.screenshotLandscapePaths ?? []).length > 0) {
207
+ versionOptions.screenshotsLandscape = listing.screenshotLandscapePaths.map((filePath, index) => {
208
+ const prepared = createFileFromPath(filePath, 'image/png', config_1.MAX_VERSION_SCREENSHOT_BYTES, 'screenshot_landscape_too_large', 'invalid_screenshot_landscape_content_type', `Landscape screenshot ${index + 1}`, isPngSignature);
209
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
210
+ return prepared.file;
211
+ });
128
212
  }
129
- if (task.listing.videoPortraitPaths.length > 0) {
130
- versionOptions.videosPortrait = task.listing.videoPortraitPaths.map((p) => createFileFromPath(p, 'video/mp4', isMp4Signature));
213
+ if ((listing.videoPortraitPaths ?? []).length > 0) {
214
+ versionOptions.videosPortrait = listing.videoPortraitPaths.map((filePath, index) => {
215
+ const prepared = createFileFromPath(filePath, 'video/mp4', config_1.MAX_VERSION_VIDEO_BYTES, 'video_portrait_too_large', 'invalid_video_portrait_content_type', `Portrait video ${index + 1}`, isMp4Signature);
216
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
217
+ return prepared.file;
218
+ });
131
219
  }
132
- if (task.listing.videoLandscapePaths.length > 0) {
133
- versionOptions.videosLandscape = task.listing.videoLandscapePaths.map((p) => createFileFromPath(p, 'video/mp4', isMp4Signature));
220
+ if ((listing.videoLandscapePaths ?? []).length > 0) {
221
+ versionOptions.videosLandscape = listing.videoLandscapePaths.map((filePath, index) => {
222
+ const prepared = createFileFromPath(filePath, 'video/mp4', config_1.MAX_VERSION_VIDEO_BYTES, 'video_landscape_too_large', 'invalid_video_landscape_content_type', `Landscape video ${index + 1}`, isMp4Signature);
223
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
224
+ return prepared.file;
225
+ });
134
226
  }
135
227
  }
136
228
  try {
@@ -164,7 +256,13 @@ async function uploadAppVersion(client, task, artifacts, options) {
164
256
  }
165
257
  const statusText = `status ${unknownError.status}`;
166
258
  const message = detail ? `${statusText} - ${detail}` : statusText;
167
- throw new Error(`Upload failed for ${task.name}: ${message}`);
259
+ const errorCode = typeof unknownError.code === 'string' && unknownError.code.trim().length > 0
260
+ ? unknownError.code.trim()
261
+ : '';
262
+ const annotatedMessage = detail
263
+ ? `${errorCode ? `${errorCode}: ` : ''}${detail}`
264
+ : `${statusText}${errorCode ? ` (${errorCode})` : ''}`;
265
+ throw new Error(`Upload failed for ${task.name}: ${annotatedMessage}`);
168
266
  }
169
267
  throw unknownError;
170
268
  }
@@ -0,0 +1,16 @@
1
+ import { type AssetSpecCapability, type AssetSpecContract, type AssetSpecValidationKind } from '@playdrop/types';
2
+ export type AssetSpecSupportDeclaration = {
3
+ assetSpec: string;
4
+ versionRange: string;
5
+ capabilities: AssetSpecCapability[];
6
+ };
7
+ export type LoadedAssetSpecContract = {
8
+ contract: AssetSpecContract;
9
+ validationKind: AssetSpecValidationKind;
10
+ contractPath: string;
11
+ };
12
+ export declare function normalizeAssetSpecSupportEntries(raw: unknown, errors: string[], context: string): AssetSpecSupportDeclaration[];
13
+ export declare function loadAndValidateAssetSpecContract(catalogueDir: string, relativeContractPath: string, errors: string[], context: string): LoadedAssetSpecContract | null;
14
+ export declare function validateCustomAssetFilesAgainstContract(contract: AssetSpecContract, filePaths: Record<string, string>, errors: string[], context: string): void;
15
+ export declare function normalizeAssetSpecVersionRef(raw: unknown, errors: string[], context: string): string | undefined;
16
+ export declare function validateAssetSpecName(name: string, errors: string[], context: string): boolean;