@playdrop/playdrop-cli 0.5.4 → 0.5.6

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 (99) hide show
  1. package/README.md +2 -1
  2. package/config/client-meta.json +4 -4
  3. package/dist/apps/upload.js +226 -80
  4. package/dist/assetSpecs.d.ts +1 -0
  5. package/dist/assetSpecs.js +22 -1
  6. package/dist/assets/model-artifacts.d.ts +2 -2
  7. package/dist/assets/model-artifacts.js +1 -1
  8. package/dist/captureRuntime.d.ts +1 -0
  9. package/dist/captureRuntime.js +3 -2
  10. package/dist/catalogue.d.ts +33 -8
  11. package/dist/catalogue.js +364 -46
  12. package/dist/commandContext.d.ts +5 -1
  13. package/dist/commandContext.js +90 -29
  14. package/dist/commands/browse.d.ts +3 -0
  15. package/dist/commands/browse.js +90 -17
  16. package/dist/commands/build.js +1 -1
  17. package/dist/commands/capture.d.ts +3 -0
  18. package/dist/commands/capture.js +121 -9
  19. package/dist/commands/captureListing.d.ts +2 -0
  20. package/dist/commands/captureListing.js +157 -61
  21. package/dist/commands/create.js +6 -28
  22. package/dist/commands/createRemixContent.js +4 -26
  23. package/dist/commands/creations.js +2 -3
  24. package/dist/commands/detail.js +24 -2
  25. package/dist/commands/dev.d.ts +8 -1
  26. package/dist/commands/dev.js +180 -2
  27. package/dist/commands/devRuntimeAssets.d.ts +34 -0
  28. package/dist/commands/devRuntimeAssets.js +308 -0
  29. package/dist/commands/devServer.d.ts +11 -0
  30. package/dist/commands/devServer.js +196 -13
  31. package/dist/commands/init.js +6 -24
  32. package/dist/commands/search.d.ts +4 -0
  33. package/dist/commands/search.js +68 -11
  34. package/dist/commands/upload-content.d.ts +3 -3
  35. package/dist/commands/upload-content.js +19 -38
  36. package/dist/commands/upload.js +67 -77
  37. package/dist/commands/validate.js +13 -20
  38. package/dist/commands/whoami.js +23 -26
  39. package/dist/devAuth.d.ts +16 -0
  40. package/dist/devAuth.js +60 -0
  41. package/dist/index.js +22 -4
  42. package/dist/taskSelection.js +4 -3
  43. package/dist/taskUtils.d.ts +2 -2
  44. package/dist/taskUtils.js +1 -1
  45. package/dist/uploadLog.d.ts +1 -1
  46. package/node_modules/@playdrop/ai-client/package.json +1 -1
  47. package/node_modules/@playdrop/api-client/dist/client.d.ts +44 -114
  48. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  49. package/node_modules/@playdrop/api-client/dist/client.js +22 -0
  50. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
  51. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  52. package/node_modules/@playdrop/api-client/dist/domains/admin.js +11 -0
  53. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +11 -19
  54. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  55. package/node_modules/@playdrop/api-client/dist/domains/apps.js +116 -106
  56. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +2 -1
  57. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  58. package/node_modules/@playdrop/api-client/dist/domains/assets.js +13 -0
  59. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +5 -5
  60. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  61. package/node_modules/@playdrop/api-client/dist/domains/payments.js +8 -8
  62. package/node_modules/@playdrop/api-client/dist/domains/search.d.ts.map +1 -1
  63. package/node_modules/@playdrop/api-client/dist/domains/search.js +24 -2
  64. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts +13 -1
  65. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts.map +1 -1
  66. package/node_modules/@playdrop/api-client/dist/domains/tags.js +52 -0
  67. package/node_modules/@playdrop/api-client/dist/index.d.ts +28 -29
  68. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  69. package/node_modules/@playdrop/api-client/dist/index.js +26 -8
  70. package/node_modules/@playdrop/api-client/package.json +1 -1
  71. package/node_modules/@playdrop/boxel-core/package.json +1 -1
  72. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  73. package/node_modules/@playdrop/config/client-meta.json +4 -4
  74. package/node_modules/@playdrop/config/package.json +1 -1
  75. package/node_modules/@playdrop/types/dist/api.d.ts +130 -3
  76. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  77. package/node_modules/@playdrop/types/dist/api.js +23 -0
  78. package/node_modules/@playdrop/types/dist/app-capability-filters.d.ts +24 -0
  79. package/node_modules/@playdrop/types/dist/app-capability-filters.d.ts.map +1 -0
  80. package/node_modules/@playdrop/types/dist/app-capability-filters.js +72 -0
  81. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +3 -2
  82. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  83. package/node_modules/@playdrop/types/dist/asset.d.ts +2 -3
  84. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  85. package/node_modules/@playdrop/types/dist/asset.js +1 -1
  86. package/node_modules/@playdrop/types/dist/index.d.ts +2 -0
  87. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  88. package/node_modules/@playdrop/types/dist/index.js +2 -0
  89. package/node_modules/@playdrop/types/dist/owned-assets.d.ts +21 -0
  90. package/node_modules/@playdrop/types/dist/owned-assets.d.ts.map +1 -0
  91. package/node_modules/@playdrop/types/dist/owned-assets.js +35 -0
  92. package/node_modules/@playdrop/types/dist/player-meta.d.ts +28 -0
  93. package/node_modules/@playdrop/types/dist/player-meta.d.ts.map +1 -1
  94. package/node_modules/@playdrop/types/dist/version.d.ts +111 -1
  95. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  96. package/node_modules/@playdrop/types/dist/version.js +4 -0
  97. package/node_modules/@playdrop/types/package.json +1 -1
  98. package/node_modules/@playdrop/vox-three/package.json +1 -1
  99. package/package.json +1 -1
package/README.md CHANGED
@@ -36,7 +36,8 @@ playdrop project publish .
36
36
  - CLI docs: [playdrop.ai/docs/cli](https://www.playdrop.ai/docs/cli)
37
37
  - Runtime docs: [playdrop.ai/docs/runtime](https://www.playdrop.ai/docs/runtime)
38
38
  - Templates and demos: [playdrop.ai/docs/examples#templates-and-demos](https://www.playdrop.ai/docs/examples#templates-and-demos)
39
- - Public Playdrop skill: [skills.sh/playdrop-ai/playdrop-skills/playdrop](https://skills.sh/playdrop-ai/playdrop-skills/playdrop)
39
+ - Canonical public plugin repo: [github.com/playdrop-ai/playdrop-plugin](https://github.com/playdrop-ai/playdrop-plugin)
40
+ - Legacy public skill surface: [skills.sh/playdrop-ai/playdrop-skills/playdrop](https://skills.sh/playdrop-ai/playdrop-skills/playdrop)
40
41
 
41
42
  ## Live Examples
42
43
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.4",
2
+ "version": "0.5.6",
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.4",
29
+ "minimumVersion": "0.5.6",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.5.4",
33
+ "minimumVersion": "0.5.6",
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.4"
41
+ "minimumVersion": "0.5.6"
42
42
  }
43
43
  }
44
44
  }
@@ -1,11 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.uploadApp = uploadApp;
4
+ const node_crypto_1 = require("node:crypto");
4
5
  const types_1 = require("@playdrop/types");
5
6
  const config_1 = require("@playdrop/config");
6
7
  const node_fs_1 = require("node:fs");
7
8
  const node_path_1 = require("node:path");
8
9
  const http_1 = require("../http");
10
+ const EXTENSION_TO_MIME = {
11
+ '.png': 'image/png',
12
+ '.jpg': 'image/jpeg',
13
+ '.jpeg': 'image/jpeg',
14
+ '.webp': 'image/webp',
15
+ '.gif': 'image/gif',
16
+ '.mp4': 'video/mp4',
17
+ '.webm': 'video/webm',
18
+ '.mp3': 'audio/mpeg',
19
+ '.wav': 'audio/wav',
20
+ '.ogg': 'audio/ogg',
21
+ '.json': 'application/json',
22
+ '.gltf': 'model/gltf+json',
23
+ '.glb': 'model/gltf-binary',
24
+ '.vox': 'application/octet-stream',
25
+ };
9
26
  function isPngSignature(content) {
10
27
  if (content.length < 8) {
11
28
  return false;
@@ -50,6 +67,59 @@ function createFileFromPath(filePath, mimeType, maxBytes, sizeCode, typeCode, la
50
67
  size: content.length,
51
68
  };
52
69
  }
70
+ async function bufferFromFile(file) {
71
+ return Buffer.from(await file.arrayBuffer());
72
+ }
73
+ function computeSha256Hex(buffer) {
74
+ return (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
75
+ }
76
+ function resolveMimeType(filename, fallback = 'application/octet-stream') {
77
+ const type = EXTENSION_TO_MIME[(0, node_path_1.extname)(filename).toLowerCase()];
78
+ return type ?? fallback;
79
+ }
80
+ async function buildPreparedSessionFile(fileKey, file) {
81
+ const buffer = await bufferFromFile(file);
82
+ return {
83
+ fileKey,
84
+ file,
85
+ filename: file.name,
86
+ contentType: file.type || resolveMimeType(file.name),
87
+ sizeBytes: buffer.length,
88
+ sha256: computeSha256Hex(buffer),
89
+ };
90
+ }
91
+ function createGenericFileFromPath(filePath) {
92
+ const content = (0, node_fs_1.readFileSync)(filePath);
93
+ const name = (0, node_path_1.basename)(filePath);
94
+ return new File([content], name, { type: resolveMimeType(name) });
95
+ }
96
+ async function prepareOwnedAssetUpload(task) {
97
+ const files = await Promise.all(Object.entries(task.filePaths).map(async ([role, filePath]) => {
98
+ const file = createGenericFileFromPath(filePath);
99
+ const buffer = await bufferFromFile(file);
100
+ return {
101
+ role,
102
+ file,
103
+ filename: file.name,
104
+ contentType: file.type || resolveMimeType(file.name),
105
+ sizeBytes: buffer.length,
106
+ sha256: computeSha256Hex(buffer),
107
+ };
108
+ }));
109
+ return {
110
+ uploadKey: task.name,
111
+ runtimeKey: task.runtimeKey,
112
+ name: task.name,
113
+ category: task.category,
114
+ subcategory: task.subcategory,
115
+ format: task.format || (0, node_path_1.extname)(files[0]?.filename || '').replace(/^\./, '').toUpperCase() || 'GLB',
116
+ visibility: task.visibility,
117
+ assetSpec: task.assetSpec,
118
+ shopListed: task.shopListed,
119
+ shopPriceCredits: task.shopPriceCredits,
120
+ files,
121
+ };
122
+ }
53
123
  // eslint-disable-next-line complexity -- Source archive failures need distinct guidance when publish-only media is already excluded.
54
124
  function ensurePreparedArtifactSize(file, maxBytes, code, label, task) {
55
125
  if (!file) {
@@ -109,21 +179,99 @@ async function uploadAppVersion(client, task, artifacts, options) {
109
179
  const achievements = task.achievements ?? [];
110
180
  const leaderboards = task.leaderboards ?? [];
111
181
  const listing = task.listing;
112
- const versionOptions = {
182
+ const preparedSessionFiles = [];
183
+ const preparedOwnedAssets = await Promise.all(task.ownedAssets.map((ownedAsset) => prepareOwnedAssetUpload(ownedAsset)));
184
+ for (const ownedAsset of preparedOwnedAssets) {
185
+ for (const file of ownedAsset.files) {
186
+ totalUploadBytes = accumulateUploadBytes(task.name, totalUploadBytes, file.sizeBytes);
187
+ }
188
+ }
189
+ for (const definition of achievements) {
190
+ 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);
191
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
192
+ preparedSessionFiles.push(await buildPreparedSessionFile(`achievementIcon:${definition.key}`, prepared.file));
193
+ }
194
+ if (task.hostingMode === 'EXTERNAL') {
195
+ // no hosted files
196
+ }
197
+ else if (artifacts) {
198
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.bundleFile, config_1.MAX_VERSION_BUNDLE_BYTES, 'bundle_too_large', 'Bundle');
199
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.sourceFile, config_1.MAX_APP_SOURCE_BYTES, 'source_too_large', 'Source archive');
200
+ preparedSessionFiles.push(await buildPreparedSessionFile('bundle', artifacts.bundleFile));
201
+ preparedSessionFiles.push(await buildPreparedSessionFile('source', artifacts.sourceFile));
202
+ if (!options?.skipEcs) {
203
+ if (artifacts.ecsFile) {
204
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.ecsFile, config_1.MAX_APP_ECS_BYTES, 'ecs_too_large', 'ECS config');
205
+ preparedSessionFiles.push(await buildPreparedSessionFile('ecs', artifacts.ecsFile));
206
+ }
207
+ if (artifacts.serverFile) {
208
+ totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.serverFile, config_1.MAX_APP_SERVER_BYTES, 'server_too_large', 'Server.js');
209
+ preparedSessionFiles.push(await buildPreparedSessionFile('server', artifacts.serverFile));
210
+ }
211
+ }
212
+ }
213
+ if (listing?.iconPath) {
214
+ const prepared = createFileFromPath(listing.iconPath, 'image/png', config_1.MAX_VERSION_ICON_BYTES, 'icon_too_large', 'invalid_icon_content_type', 'Icon', isPngSignature);
215
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
216
+ preparedSessionFiles.push(await buildPreparedSessionFile('icon', prepared.file));
217
+ }
218
+ if (listing?.heroPortraitPath) {
219
+ 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);
220
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
221
+ preparedSessionFiles.push(await buildPreparedSessionFile('heroPortrait', prepared.file));
222
+ }
223
+ if (listing?.heroLandscapePath) {
224
+ 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);
225
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
226
+ preparedSessionFiles.push(await buildPreparedSessionFile('heroLandscape', prepared.file));
227
+ }
228
+ for (let index = 0; index < (listing?.screenshotPortraitPaths ?? []).length; index += 1) {
229
+ const filePath = listing.screenshotPortraitPaths[index];
230
+ 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);
231
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
232
+ preparedSessionFiles.push(await buildPreparedSessionFile(`screenshotsPortrait:${index}`, prepared.file));
233
+ }
234
+ for (let index = 0; index < (listing?.screenshotLandscapePaths ?? []).length; index += 1) {
235
+ const filePath = listing.screenshotLandscapePaths[index];
236
+ 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);
237
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
238
+ preparedSessionFiles.push(await buildPreparedSessionFile(`screenshotsLandscape:${index}`, prepared.file));
239
+ }
240
+ for (let index = 0; index < (listing?.videoPortraitPaths ?? []).length; index += 1) {
241
+ const filePath = listing.videoPortraitPaths[index];
242
+ 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);
243
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
244
+ preparedSessionFiles.push(await buildPreparedSessionFile(`videosPortrait:${index}`, prepared.file));
245
+ }
246
+ for (let index = 0; index < (listing?.videoLandscapePaths ?? []).length; index += 1) {
247
+ const filePath = listing.videoLandscapePaths[index];
248
+ 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);
249
+ totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
250
+ preparedSessionFiles.push(await buildPreparedSessionFile(`videosLandscape:${index}`, prepared.file));
251
+ }
252
+ const initializeRequest = {
113
253
  version: task.version,
114
254
  releaseNotes: task.releaseNotes,
115
255
  visibility: task.versionVisibility,
116
256
  remixRef: task.remix,
257
+ usesAssets: task.uses.assets.map((entry) => ({
258
+ ref: entry.ref,
259
+ ...(entry.runtimeKey ? { runtimeKey: entry.runtimeKey } : {}),
260
+ })),
261
+ usesPacks: task.uses.packs,
117
262
  tags: task.tags,
118
263
  clearTags: options?.clearTags,
119
264
  surfaceTargets: task.surfaceTargets,
120
- assetSpecSupport: task.assetSpecSupport,
265
+ assetSpecSupport: (task.assetSpecSupport ?? []).map((row) => ({
266
+ assetSpecRef: row.assetSpec,
267
+ versionRange: row.versionRange,
268
+ capabilities: row.capabilities,
269
+ })),
121
270
  entryPoint: artifacts?.metadata?.entryPoint ?? undefined,
122
271
  authMode: task.authMode,
123
272
  controllerMode: task.controllerMode,
124
273
  previewable: task.previewable,
125
274
  skipReview: options?.skipReview,
126
- // App metadata (used when creating app if it doesn't exist)
127
275
  displayName: task.displayName,
128
276
  description: task.description,
129
277
  type: task.type,
@@ -148,93 +296,91 @@ async function uploadAppVersion(client, task, artifacts, options) {
148
296
  ...(definition.unitLabel ? { unitLabel: definition.unitLabel } : {}),
149
297
  status: definition.status,
150
298
  })),
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
- }),
299
+ externalUrl: task.hostingMode === 'EXTERNAL' ? task.externalUrl : undefined,
300
+ files: preparedSessionFiles.map((file) => ({
301
+ fileKey: file.fileKey,
302
+ filename: file.filename,
303
+ contentType: file.contentType,
304
+ sizeBytes: file.sizeBytes,
305
+ sha256: file.sha256,
306
+ })),
307
+ ownedAssets: preparedOwnedAssets.map((ownedAsset) => ({
308
+ uploadKey: ownedAsset.uploadKey,
309
+ runtimeKey: ownedAsset.runtimeKey,
310
+ name: ownedAsset.name,
311
+ category: ownedAsset.category,
312
+ subcategory: ownedAsset.subcategory,
313
+ format: ownedAsset.format,
314
+ assetSpec: ownedAsset.assetSpec,
315
+ visibility: ownedAsset.visibility,
316
+ shopListed: ownedAsset.shopListed,
317
+ shopPriceCredits: ownedAsset.shopPriceCredits,
318
+ files: ownedAsset.files.map((file) => ({
319
+ role: file.role,
320
+ filename: file.filename,
321
+ contentType: file.contentType,
322
+ sizeBytes: file.sizeBytes,
323
+ sha256: file.sha256,
324
+ })),
325
+ })),
159
326
  };
160
- // For EXTERNAL hosting mode, use externalUrl instead of bundle
161
- if (task.hostingMode === 'EXTERNAL') {
162
- versionOptions.externalUrl = task.externalUrl;
163
- }
164
- else if (artifacts) {
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');
168
- versionOptions.bundle = artifacts.bundleFile;
169
- versionOptions.source = artifacts.sourceFile;
170
- // Include ecs/server files unless skipEcs is set
171
- if (!options?.skipEcs) {
172
- if (artifacts.ecsFile) {
173
- totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.ecsFile, config_1.MAX_APP_ECS_BYTES, 'ecs_too_large', 'ECS config');
174
- versionOptions.ecs = artifacts.ecsFile;
327
+ let sessionId = null;
328
+ try {
329
+ const initialized = await client.initializeAppUpload(creatorUsername, task.name, initializeRequest);
330
+ sessionId = initialized.session.id;
331
+ if (initialized.status !== 'finalized') {
332
+ const uploadedFileKeys = new Set(initialized.session.uploadedFileKeys);
333
+ const uploadedOwnedAssetKeys = new Set(initialized.session.uploadedOwnedAssetKeys);
334
+ for (const file of preparedSessionFiles) {
335
+ if (uploadedFileKeys.has(file.fileKey)) {
336
+ continue;
337
+ }
338
+ await client.uploadAppSessionFile(creatorUsername, task.name, sessionId, file.fileKey, {
339
+ file: file.file,
340
+ filename: file.filename,
341
+ });
175
342
  }
176
- if (artifacts.serverFile) {
177
- totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.serverFile, config_1.MAX_APP_SERVER_BYTES, 'server_too_large', 'Server.js');
178
- versionOptions.server = artifacts.serverFile;
343
+ for (const ownedAsset of preparedOwnedAssets) {
344
+ if (uploadedOwnedAssetKeys.has(ownedAsset.uploadKey)) {
345
+ continue;
346
+ }
347
+ await client.uploadAppSessionOwnedAsset(creatorUsername, task.name, sessionId, ownedAsset.uploadKey, {
348
+ files: ownedAsset.files.map((file) => ({
349
+ fieldName: file.role,
350
+ file: file.file,
351
+ filename: file.filename,
352
+ })),
353
+ });
179
354
  }
180
355
  }
181
- }
182
- // Add listing assets if provided
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;
188
- }
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;
193
- }
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;
198
- }
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
- });
356
+ const finalized = initialized.status === 'finalized'
357
+ ? initialized.finalized
358
+ : await client.finalizeAppUpload(creatorUsername, task.name, sessionId);
359
+ if (!finalized) {
360
+ throw new Error(`Upload failed for ${task.name}: missing finalized app upload payload`);
205
361
  }
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
- });
212
- }
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
- });
219
- }
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
- });
226
- }
227
- }
228
- try {
229
- const response = await client.uploadAppVersion(creatorUsername, task.name, versionOptions);
362
+ const version = initialized.status === 'finalized'
363
+ ? finalized?.version
364
+ : finalized.version;
365
+ const versionNodeId = initialized.status === 'finalized'
366
+ ? finalized?.versionNodeId
367
+ : finalized.versionNodeId;
230
368
  return {
231
369
  versionCreated: true,
232
- version: response?.version?.version,
233
- versionId: typeof response?.version?.id === 'number' ? response.version.id : undefined,
234
- versionNodeId: typeof response?.versionNodeId === 'string' ? response.versionNodeId : undefined,
370
+ version: version?.version,
371
+ versionId: typeof version?.id === 'number' ? version.id : undefined,
372
+ versionNodeId: typeof versionNodeId === 'string' ? versionNodeId : undefined,
235
373
  };
236
374
  }
237
375
  catch (unknownError) {
376
+ if (sessionId) {
377
+ try {
378
+ await client.abortAppUpload(creatorUsername, task.name, sessionId);
379
+ }
380
+ catch {
381
+ // keep original error
382
+ }
383
+ }
238
384
  if (unknownError instanceof types_1.UnsupportedClientError) {
239
385
  (0, http_1.handleUnsupportedError)(unknownError, 'Upload app version');
240
386
  }
@@ -1,4 +1,5 @@
1
1
  import { type AssetSpecCapability, type AssetSpecContract, type AssetSpecValidationKind } from '@playdrop/types';
2
+ export declare function resolveCustomAssetRoleContentType(filePath: string, roleContract?: Pick<AssetSpecContract['roles'][number], 'contentTypes'> | null): string;
2
3
  export type AssetSpecSupportDeclaration = {
3
4
  assetSpec: string;
4
5
  versionRange: string;
@@ -3,6 +3,7 @@ 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.resolveCustomAssetRoleContentType = resolveCustomAssetRoleContentType;
6
7
  exports.normalizeAssetSpecSupportEntries = normalizeAssetSpecSupportEntries;
7
8
  exports.loadAndValidateAssetSpecContract = loadAndValidateAssetSpecContract;
8
9
  exports.validateCustomAssetFilesAgainstContract = validateCustomAssetFilesAgainstContract;
@@ -54,6 +55,26 @@ function createContractValidator() {
54
55
  function resolveMimeTypeFromPath(filePath) {
55
56
  return LOCAL_EXTENSION_TO_MIME[(0, node_path_1.extname)(filePath).toLowerCase()] ?? 'application/octet-stream';
56
57
  }
58
+ function resolveCustomAssetRoleContentType(filePath, roleContract) {
59
+ const resolvedContentType = resolveMimeTypeFromPath(filePath);
60
+ if (!roleContract) {
61
+ return resolvedContentType;
62
+ }
63
+ const normalizedResolvedContentType = resolvedContentType.toLowerCase();
64
+ for (const contentType of roleContract.contentTypes) {
65
+ const normalizedContentType = contentType.trim().toLowerCase();
66
+ if (normalizedContentType === normalizedResolvedContentType) {
67
+ return contentType.trim();
68
+ }
69
+ }
70
+ if ((0, node_path_1.extname)(filePath).toLowerCase() === '.json') {
71
+ const jsonLikeContentType = roleContract.contentTypes.find((contentType) => (0, types_1.isJsonLikeAssetSpecContentType)(contentType));
72
+ if (jsonLikeContentType) {
73
+ return jsonLikeContentType.trim();
74
+ }
75
+ }
76
+ return resolvedContentType;
77
+ }
57
78
  // eslint-disable-next-line complexity
58
79
  function normalizeAssetSpecSupportEntries(raw, errors, context) {
59
80
  if (raw === undefined || raw === null) {
@@ -211,7 +232,7 @@ function validateCustomAssetFilesAgainstContract(contract, filePaths, errors, co
211
232
  errors.push(`${context} role "${rawRole}" must use one of ${allowedExtensions.join(', ')}.`);
212
233
  }
213
234
  }
214
- const resolvedContentType = resolveMimeTypeFromPath(filePath);
235
+ const resolvedContentType = resolveCustomAssetRoleContentType(filePath, roleContract);
215
236
  const normalizedContentTypes = roleContract.contentTypes.map((value) => value.trim().toLowerCase());
216
237
  if (!normalizedContentTypes.includes(resolvedContentType.toLowerCase())) {
217
238
  errors.push(`${context} role "${rawRole}" must use one of ${normalizedContentTypes.join(', ')}.`);
@@ -1,5 +1,5 @@
1
- import type { AssetTask, EmbeddedAssetTask } from '../catalogue';
2
- type ModelAssetTask = AssetTask | EmbeddedAssetTask;
1
+ import type { AssetTask, OwnedAssetTask, PackOwnedAssetTask } from '../catalogue';
2
+ type ModelAssetTask = AssetTask | OwnedAssetTask | PackOwnedAssetTask;
3
3
  export declare function isGeneratedModel3DAssetTask(task: ModelAssetTask, format?: string): boolean;
4
4
  export declare function assertGeneratedModel3DAssetRoles(task: ModelAssetTask, format?: string): void;
5
5
  export declare function prepareModel3DAssetArtifacts(task: ModelAssetTask, options?: {
@@ -100,7 +100,7 @@ function normalizeRole(role) {
100
100
  return role.trim().toLowerCase();
101
101
  }
102
102
  function resolveTaskLabel(task) {
103
- if (task.kind === 'embedded-asset') {
103
+ if ('kind' in task && task.kind === 'owned-asset') {
104
104
  return `${task.appName}:${task.name}`;
105
105
  }
106
106
  return task.name;
@@ -19,6 +19,7 @@ export type RunCaptureOptions = {
19
19
  token?: string | null;
20
20
  user?: UserResponse | null;
21
21
  login?: LoginOptions | null;
22
+ savedSessionBootstrap?: boolean;
22
23
  enableCaptureBridge?: boolean;
23
24
  };
24
25
  export type CaptureRunResult = {
@@ -114,8 +114,9 @@ async function runCapture(options) {
114
114
  const outputLines = [];
115
115
  const expectedUrl = options.expectedUrl ? normalizeComparableUrl(options.expectedUrl) : null;
116
116
  const hasExplicitLogin = Boolean(options.login?.username);
117
- const bootstrapToken = hasExplicitLogin ? null : options.token?.trim() || null;
118
- const bootstrapUser = hasExplicitLogin ? null : options.user ?? null;
117
+ const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && !hasExplicitLogin;
118
+ const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
119
+ const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
119
120
  let finalUrl = options.targetUrl;
120
121
  const record = (level, message) => {
121
122
  const line = `[capture][${level}] ${message}`;
@@ -53,7 +53,7 @@ export type AppCatalogueEntry = {
53
53
  username?: string;
54
54
  achievements?: AppAchievementCatalogueDefinition[];
55
55
  leaderboards?: AppLeaderboardCatalogueDefinition[];
56
- embeddedAssets?: EmbeddedAssetCatalogueEntry[];
56
+ ownedAssets?: OwnedAssetCatalogueEntry[];
57
57
  assetSpecSupport?: AppMetadataAssetSpecSupport[];
58
58
  uses?: {
59
59
  assets?: string[];
@@ -99,8 +99,9 @@ export type AssetSpecCatalogueEntry = {
99
99
  successorVersion?: string;
100
100
  username?: string;
101
101
  };
102
- export type EmbeddedAssetCatalogueEntry = {
102
+ export type OwnedAssetCatalogueEntry = {
103
103
  name: string;
104
+ runtimeKey?: string;
104
105
  category?: string;
105
106
  subcategory?: string;
106
107
  format?: string;
@@ -115,6 +116,7 @@ export type AssetPackCatalogueEntry = {
115
116
  remix?: string;
116
117
  username?: string;
117
118
  assets?: string[];
119
+ ownedAssets?: OwnedAssetCatalogueEntry[];
118
120
  hostingMode?: string;
119
121
  externalUrl?: string;
120
122
  downloadUrl?: string;
@@ -145,6 +147,10 @@ export type ResolvedListingAssets = {
145
147
  videoPortraitPaths: string[];
146
148
  videoLandscapePaths: string[];
147
149
  };
150
+ export type AppDependencyAssetRef = {
151
+ ref: string;
152
+ runtimeKey?: string;
153
+ };
148
154
  export type AppTask = {
149
155
  kind: 'app';
150
156
  name: string;
@@ -181,10 +187,10 @@ export type AppTask = {
181
187
  achievements: ResolvedAppAchievementDefinition[];
182
188
  leaderboards: ResolvedAppLeaderboardDefinition[];
183
189
  remix?: string;
184
- embeddedAssets: EmbeddedAssetTask[];
190
+ ownedAssets: OwnedAssetTask[];
185
191
  assetSpecSupport: AssetSpecSupportDeclaration[];
186
192
  uses: {
187
- assets: string[];
193
+ assets: AppDependencyAssetRef[];
188
194
  packs: string[];
189
195
  };
190
196
  graph: {
@@ -211,6 +217,7 @@ export type AssetTask = {
211
217
  shopPriceCredits?: number;
212
218
  files: Record<string, string>;
213
219
  filePaths: Record<string, string>;
220
+ assetSpecContract?: AssetSpecContract;
214
221
  relations?: Array<{
215
222
  type: string;
216
223
  to: string;
@@ -236,13 +243,30 @@ export type AssetSpecTask = {
236
243
  documentationPath?: string;
237
244
  contract: AssetSpecContract;
238
245
  };
239
- export type EmbeddedAssetTask = {
240
- kind: 'embedded-asset';
246
+ export type OwnedAssetTask = {
247
+ kind: 'owned-asset';
241
248
  appName: string;
242
249
  name: string;
250
+ runtimeKey?: string;
243
251
  cataloguePath: string;
244
252
  category: string;
245
- subcategory: string;
253
+ subcategory: string | null;
254
+ assetSpec?: string;
255
+ assetSpecContract?: AssetSpecContract;
256
+ format?: string;
257
+ visibility?: string;
258
+ shopListed?: boolean;
259
+ shopPriceCredits?: number;
260
+ files: Record<string, string>;
261
+ filePaths: Record<string, string>;
262
+ };
263
+ export type PackOwnedAssetTask = {
264
+ name: string;
265
+ cataloguePath: string;
266
+ category: string;
267
+ subcategory: string | null;
268
+ assetSpec?: string;
269
+ assetSpecContract?: AssetSpecContract;
246
270
  format?: string;
247
271
  visibility?: string;
248
272
  shopListed?: boolean;
@@ -259,6 +283,7 @@ export type AssetPackTask = {
259
283
  username?: string;
260
284
  remix?: string;
261
285
  assets: string[];
286
+ ownedAssets: PackOwnedAssetTask[];
262
287
  tags: string[];
263
288
  hostingMode?: AppHostingMode;
264
289
  externalUrl?: string;
@@ -279,7 +304,7 @@ export type CatalogueLoadResult = {
279
304
  apps: AppTask[];
280
305
  assetSpecs: AssetSpecTask[];
281
306
  assets: AssetTask[];
282
- embeddedAssets: EmbeddedAssetTask[];
307
+ ownedAssets: OwnedAssetTask[];
283
308
  assetPacks: AssetPackTask[];
284
309
  warnings: string[];
285
310
  errors: string[];