@playdrop/playdrop-cli 0.5.5 → 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 (96) 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 +43 -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/apps.d.ts +11 -19
  51. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  52. package/node_modules/@playdrop/api-client/dist/domains/apps.js +116 -106
  53. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +2 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  55. package/node_modules/@playdrop/api-client/dist/domains/assets.js +13 -0
  56. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +5 -5
  57. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  58. package/node_modules/@playdrop/api-client/dist/domains/payments.js +8 -8
  59. package/node_modules/@playdrop/api-client/dist/domains/search.d.ts.map +1 -1
  60. package/node_modules/@playdrop/api-client/dist/domains/search.js +24 -2
  61. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts +13 -1
  62. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts.map +1 -1
  63. package/node_modules/@playdrop/api-client/dist/domains/tags.js +52 -0
  64. package/node_modules/@playdrop/api-client/dist/index.d.ts +25 -29
  65. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  66. package/node_modules/@playdrop/api-client/dist/index.js +23 -8
  67. package/node_modules/@playdrop/api-client/package.json +1 -1
  68. package/node_modules/@playdrop/boxel-core/package.json +1 -1
  69. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  70. package/node_modules/@playdrop/config/client-meta.json +4 -4
  71. package/node_modules/@playdrop/config/package.json +1 -1
  72. package/node_modules/@playdrop/types/dist/api.d.ts +124 -3
  73. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  74. package/node_modules/@playdrop/types/dist/api.js +23 -0
  75. package/node_modules/@playdrop/types/dist/app-capability-filters.d.ts +24 -0
  76. package/node_modules/@playdrop/types/dist/app-capability-filters.d.ts.map +1 -0
  77. package/node_modules/@playdrop/types/dist/app-capability-filters.js +72 -0
  78. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +3 -2
  79. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  80. package/node_modules/@playdrop/types/dist/asset.d.ts +2 -3
  81. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  82. package/node_modules/@playdrop/types/dist/asset.js +1 -1
  83. package/node_modules/@playdrop/types/dist/index.d.ts +2 -0
  84. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  85. package/node_modules/@playdrop/types/dist/index.js +2 -0
  86. package/node_modules/@playdrop/types/dist/owned-assets.d.ts +21 -0
  87. package/node_modules/@playdrop/types/dist/owned-assets.d.ts.map +1 -0
  88. package/node_modules/@playdrop/types/dist/owned-assets.js +35 -0
  89. package/node_modules/@playdrop/types/dist/player-meta.d.ts +28 -0
  90. package/node_modules/@playdrop/types/dist/player-meta.d.ts.map +1 -1
  91. package/node_modules/@playdrop/types/dist/version.d.ts +111 -1
  92. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  93. package/node_modules/@playdrop/types/dist/version.js +3 -0
  94. package/node_modules/@playdrop/types/package.json +1 -1
  95. package/node_modules/@playdrop/vox-three/package.json +1 -1
  96. package/package.json +1 -1
package/dist/catalogue.js CHANGED
@@ -22,7 +22,7 @@ const BOXEL_GENERATED_FILE_ROLES = new Set(['mesh', 'preview']);
22
22
  const BOXEL_REQUIRED_FILE_ROLES = ['primary', 'mesh', 'preview'];
23
23
  const VOX_GENERATED_FILE_ROLES = new Set(['boxel', 'mesh', 'preview']);
24
24
  const VOX_REQUIRED_FILE_ROLES = ['primary', 'boxel', 'mesh', 'preview'];
25
- const MAX_CONTENT_TAGS_PER_ITEM = 8;
25
+ const MAX_CONTENT_TAGS_PER_ITEM = 20;
26
26
  const ASSET_SUBCATEGORY_SET_BY_CATEGORY = types_1.ASSET_SUBCATEGORY_DEFINITIONS.reduce((acc, definition) => {
27
27
  if (!acc[definition.category]) {
28
28
  acc[definition.category] = new Set();
@@ -62,6 +62,66 @@ function getGeneratedModel3DFileRoles(format) {
62
62
  required: BOXEL_REQUIRED_FILE_ROLES,
63
63
  };
64
64
  }
65
+ function extractGlbJsonChunk(buffer) {
66
+ if (buffer.length < 20) {
67
+ throw new Error('invalid_glb_payload');
68
+ }
69
+ if (buffer.toString('utf8', 0, 4) !== 'glTF') {
70
+ throw new Error('invalid_glb_payload');
71
+ }
72
+ const version = buffer.readUInt32LE(4);
73
+ const declaredLength = buffer.readUInt32LE(8);
74
+ if (version !== 2 || declaredLength !== buffer.length) {
75
+ throw new Error('invalid_glb_payload');
76
+ }
77
+ let offset = 12;
78
+ while (offset + 8 <= buffer.length) {
79
+ const chunkLength = buffer.readUInt32LE(offset);
80
+ const chunkType = buffer.readUInt32LE(offset + 4);
81
+ offset += 8;
82
+ if (offset + chunkLength > buffer.length) {
83
+ throw new Error('invalid_glb_payload');
84
+ }
85
+ const chunk = buffer.subarray(offset, offset + chunkLength);
86
+ offset += chunkLength;
87
+ if (chunkType !== 0x4E4F534A) {
88
+ continue;
89
+ }
90
+ const payload = chunk.toString('utf8').replace(/\0+$/, '').trimEnd();
91
+ try {
92
+ const parsed = JSON.parse(payload);
93
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
94
+ throw new Error('invalid_glb_payload');
95
+ }
96
+ return parsed;
97
+ }
98
+ catch {
99
+ throw new Error('invalid_glb_payload');
100
+ }
101
+ }
102
+ throw new Error('invalid_glb_payload');
103
+ }
104
+ function validateSelfContainedGlb(filePath) {
105
+ const gltf = extractGlbJsonChunk((0, node_fs_1.readFileSync)(filePath));
106
+ const buffers = Array.isArray(gltf.buffers) ? gltf.buffers : [];
107
+ for (const entry of buffers) {
108
+ const uri = entry && typeof entry === 'object' && typeof entry.uri === 'string'
109
+ ? entry.uri.trim()
110
+ : '';
111
+ if (uri && !uri.startsWith('data:')) {
112
+ throw new Error('glb_external_dependency_unsupported');
113
+ }
114
+ }
115
+ const images = Array.isArray(gltf.images) ? gltf.images : [];
116
+ for (const entry of images) {
117
+ const uri = entry && typeof entry === 'object' && typeof entry.uri === 'string'
118
+ ? entry.uri.trim()
119
+ : '';
120
+ if (uri && !uri.startsWith('data:')) {
121
+ throw new Error('glb_external_dependency_unsupported');
122
+ }
123
+ }
124
+ }
65
125
  /**
66
126
  * Validates a listing asset path:
67
127
  * - Must exist
@@ -161,6 +221,52 @@ function normalizeCatalogueRelations(value, errors, context) {
161
221
  }
162
222
  return rows.length > 0 ? rows : undefined;
163
223
  }
224
+ function normalizeCatalogueAppDependencyAssets(value, errors, context) {
225
+ if (!Array.isArray(value)) {
226
+ return [];
227
+ }
228
+ const rows = [];
229
+ const seenRefs = new Set();
230
+ const seenRuntimeKeys = new Set();
231
+ for (let index = 0; index < value.length; index += 1) {
232
+ const entry = value[index];
233
+ const ref = typeof entry === 'string'
234
+ ? entry.trim()
235
+ : entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.ref === 'string'
236
+ ? entry.ref.trim()
237
+ : '';
238
+ if (!ref) {
239
+ errors.push(`${context} uses.assets[${index}] must be a non-empty asset ref string or { ref, runtimeKey } object.`);
240
+ continue;
241
+ }
242
+ const parsed = (0, types_1.parseContentVersionRef)(ref);
243
+ if (!parsed || parsed.kind !== 'asset') {
244
+ errors.push(`${context} uses.assets[${index}] must be a canonical asset version ref.`);
245
+ continue;
246
+ }
247
+ const normalizedRef = (0, types_1.formatContentVersionRef)(parsed);
248
+ if (seenRefs.has(normalizedRef)) {
249
+ errors.push(`${context} uses.assets contains duplicate ref "${normalizedRef}".`);
250
+ continue;
251
+ }
252
+ const runtimeKey = entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.runtimeKey === 'string'
253
+ ? entry.runtimeKey.trim()
254
+ : '';
255
+ if (runtimeKey) {
256
+ if (seenRuntimeKeys.has(runtimeKey)) {
257
+ errors.push(`${context} uses.assets contains duplicate runtimeKey "${runtimeKey}".`);
258
+ continue;
259
+ }
260
+ seenRuntimeKeys.add(runtimeKey);
261
+ }
262
+ seenRefs.add(normalizedRef);
263
+ rows.push({
264
+ ref: normalizedRef,
265
+ ...(runtimeKey ? { runtimeKey } : {}),
266
+ });
267
+ }
268
+ return rows;
269
+ }
164
270
  function rejectLegacyShopPriceCoinsField(value, errors, context) {
165
271
  if (Object.prototype.hasOwnProperty.call(value, 'shopPriceCoins')) {
166
272
  errors.push(`${context} uses legacy shopPriceCoins. Rename it to shopPriceCredits.`);
@@ -450,6 +556,16 @@ function normalizeAssetCategoryValue(raw, errors, label) {
450
556
  return normalized;
451
557
  }
452
558
  function normalizeAssetSubcategoryValue(raw, category, errors, label) {
559
+ if (category === 'CUSTOM') {
560
+ if (raw === undefined || raw === null) {
561
+ return null;
562
+ }
563
+ if (typeof raw === 'string' && raw.trim().length === 0) {
564
+ return null;
565
+ }
566
+ errors.push(`${label} must not define subcategory when category is CUSTOM.`);
567
+ return null;
568
+ }
453
569
  if (typeof raw !== 'string' || raw.trim().length === 0) {
454
570
  errors.push(`${label} must define subcategory.`);
455
571
  return null;
@@ -742,12 +858,19 @@ function buildAppTasks(rootDir, catalogues, options) {
742
858
  errors.push(`[${label}] App entry is missing a name.`);
743
859
  continue;
744
860
  }
861
+ const ownedAssetNameMatch = filterName
862
+ ? Array.isArray(entry.ownedAssets)
863
+ && entry.ownedAssets.some((ownedAsset) => ownedAsset
864
+ && typeof ownedAsset === 'object'
865
+ && typeof ownedAsset.name === 'string'
866
+ && ownedAsset.name.trim() === filterName)
867
+ : true;
745
868
  const fileField = typeof entry.file === 'string' && entry.file.trim().length > 0
746
869
  ? entry.file.trim()
747
870
  : `${rawName}.html`;
748
871
  const absoluteFilePath = (0, node_path_1.resolve)(file.directory, fileField);
749
872
  const normalizedFile = normalizePathForComparison(absoluteFilePath);
750
- const matchName = filterName ? rawName === filterName : true;
873
+ const matchName = filterName ? rawName === filterName || ownedAssetNameMatch : true;
751
874
  const matchFile = filterFile ? normalizedFile === filterFile : true;
752
875
  if (!matchName || !matchFile) {
753
876
  continue;
@@ -1105,9 +1228,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1105
1228
  }
1106
1229
  listing = resolvedListing;
1107
1230
  }
1108
- const usesAssets = Array.isArray(entry.uses?.assets)
1109
- ? entry.uses.assets.filter((value) => typeof value === 'string' && value.trim().length > 0)
1110
- : [];
1231
+ const usesAssets = normalizeCatalogueAppDependencyAssets(entry.uses?.assets, errors, `[${label}] App "${rawName}"`);
1111
1232
  const usesPacks = Array.isArray(entry.uses?.packs)
1112
1233
  ? entry.uses.packs.filter((value) => typeof value === 'string' && value.trim().length > 0)
1113
1234
  : [];
@@ -1118,44 +1239,54 @@ function buildAppTasks(rootDir, catalogues, options) {
1118
1239
  if (errors.length > errorCountBeforeTagMetadata) {
1119
1240
  continue;
1120
1241
  }
1121
- const embeddedAssets = [];
1122
- const rawEmbeddedAssets = Array.isArray(entry.embeddedAssets)
1123
- ? entry.embeddedAssets
1242
+ if (Object.prototype.hasOwnProperty.call(entry, 'embeddedAssets')) {
1243
+ errors.push(`[${label}] App "${rawName}" uses legacy embeddedAssets. Rename it to ownedAssets.`);
1244
+ continue;
1245
+ }
1246
+ const ownedAssets = [];
1247
+ const rawOwnedAssets = Array.isArray(entry.ownedAssets)
1248
+ ? entry.ownedAssets
1124
1249
  : [];
1125
- for (let embeddedIndex = 0; embeddedIndex < rawEmbeddedAssets.length; embeddedIndex++) {
1126
- const embedded = rawEmbeddedAssets[embeddedIndex];
1127
- const embeddedName = typeof embedded?.name === 'string' ? embedded.name.trim() : '';
1128
- if (!embeddedName) {
1129
- errors.push(`[${label}] Skipping ${rawName}: embeddedAssets[${embeddedIndex}] is missing a name.`);
1250
+ for (let ownedIndex = 0; ownedIndex < rawOwnedAssets.length; ownedIndex++) {
1251
+ const owned = rawOwnedAssets[ownedIndex];
1252
+ const ownedName = typeof owned?.name === 'string' ? owned.name.trim() : '';
1253
+ if (!ownedName) {
1254
+ errors.push(`[${label}] Skipping ${rawName}: ownedAssets[${ownedIndex}] is missing a name.`);
1130
1255
  continue;
1131
1256
  }
1132
- const category = normalizeAssetCategoryValue(embedded?.category, errors, `[${label}] Skipping ${rawName}: embedded asset "${embeddedName}"`);
1257
+ const runtimeKey = typeof owned?.runtimeKey === 'string' && owned.runtimeKey.trim().length > 0
1258
+ ? owned.runtimeKey.trim()
1259
+ : undefined;
1260
+ const category = normalizeAssetCategoryValue(owned?.category, errors, `[${label}] Skipping ${rawName}: owned asset "${ownedName}"`);
1133
1261
  if (!category) {
1134
1262
  continue;
1135
1263
  }
1136
- const subcategory = normalizeAssetSubcategoryValue(embedded?.subcategory, category, errors, `[${label}] Skipping ${rawName}: embedded asset "${embeddedName}"`);
1137
- if (!subcategory) {
1264
+ const subcategory = normalizeAssetSubcategoryValue(owned?.subcategory, category, errors, `[${label}] Skipping ${rawName}: owned asset "${ownedName}"`);
1265
+ if (subcategory === null && category !== 'CUSTOM') {
1138
1266
  continue;
1139
1267
  }
1140
- const fileEntries = embedded?.files && typeof embedded.files === 'object' && !Array.isArray(embedded.files)
1141
- ? embedded.files
1268
+ const fileEntries = owned?.files && typeof owned.files === 'object' && !Array.isArray(owned.files)
1269
+ ? owned.files
1142
1270
  : null;
1143
- const embeddedFormat = typeof embedded?.format === 'string' && embedded.format.trim().length > 0
1144
- ? embedded.format.trim().toUpperCase()
1271
+ const assetSpec = (0, assetSpecs_1.normalizeAssetSpecVersionRef)(owned?.assetSpec, errors, `[${label}] App "${rawName}" owned asset "${ownedName}"`);
1272
+ const ownedFormat = typeof owned?.format === 'string' && owned.format.trim().length > 0
1273
+ ? owned.format.trim().toUpperCase()
1145
1274
  : undefined;
1146
- const generatedModel3DFormat = inferGeneratedModel3DFormat(category, subcategory, embeddedFormat, fileEntries);
1275
+ const generatedModel3DFormat = subcategory
1276
+ ? inferGeneratedModel3DFormat(category, subcategory, ownedFormat, fileEntries)
1277
+ : null;
1147
1278
  const generatedModel3DRoles = generatedModel3DFormat ? getGeneratedModel3DFileRoles(generatedModel3DFormat) : null;
1148
1279
  if (!fileEntries || Object.keys(fileEntries).length === 0) {
1149
- errors.push(`[${label}] Skipping ${rawName}: embedded asset "${embeddedName}" must define files.`);
1280
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" must define files.`);
1150
1281
  continue;
1151
1282
  }
1152
1283
  const files = {};
1153
1284
  const filePaths = {};
1154
- let embeddedHasError = false;
1285
+ let ownedAssetHasError = false;
1155
1286
  for (const [role, fileValue] of Object.entries(fileEntries)) {
1156
1287
  if (typeof fileValue !== 'string' || !fileValue.trim()) {
1157
- errors.push(`[${label}] Skipping ${rawName}: embedded asset "${embeddedName}" file role "${role}" must be a non-empty string path.`);
1158
- embeddedHasError = true;
1288
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" file role "${role}" must be a non-empty string path.`);
1289
+ ownedAssetHasError = true;
1159
1290
  continue;
1160
1291
  }
1161
1292
  const relativeEmbeddedPath = fileValue.trim();
@@ -1168,21 +1299,21 @@ function buildAppTasks(rootDir, catalogues, options) {
1168
1299
  filePaths[role] = absoluteEmbeddedPath;
1169
1300
  continue;
1170
1301
  }
1171
- errors.push(`[${label}] Skipping ${rawName}: embedded asset "${embeddedName}" file not found at ${absoluteEmbeddedPath}.`);
1172
- embeddedHasError = true;
1302
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" file not found at ${absoluteEmbeddedPath}.`);
1303
+ ownedAssetHasError = true;
1173
1304
  continue;
1174
1305
  }
1175
1306
  files[role] = relativeEmbeddedPath.replace(/\\/g, '/');
1176
1307
  filePaths[role] = absoluteEmbeddedPath;
1177
1308
  }
1178
- if (embeddedHasError) {
1309
+ if (ownedAssetHasError) {
1179
1310
  continue;
1180
1311
  }
1181
1312
  if (generatedModel3DRoles) {
1182
1313
  const normalizedRoles = new Set(Object.keys(files).map((role) => normalizeFileRole(role)));
1183
1314
  const missingRoles = generatedModel3DRoles.required.filter((role) => !normalizedRoles.has(role));
1184
1315
  if (missingRoles.length > 0) {
1185
- errors.push(`[${label}] Skipping ${rawName}: embedded asset "${embeddedName}" with subcategory=${subcategory} must define files.${missingRoles.join(' and files.')}.`);
1316
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" with subcategory=${subcategory} must define files.${missingRoles.join(' and files.')}.`);
1186
1317
  continue;
1187
1318
  }
1188
1319
  }
@@ -1190,35 +1321,86 @@ function buildAppTasks(rootDir, catalogues, options) {
1190
1321
  const hasAtlas = Object.prototype.hasOwnProperty.call(files, 'atlas');
1191
1322
  const hasManifest = Object.prototype.hasOwnProperty.call(files, 'manifest');
1192
1323
  if (!hasAtlas || !hasManifest) {
1193
- errors.push(`[${label}] Skipping ${rawName}: embedded asset "${embeddedName}" with category=SPRITESHEET must define files.atlas and files.manifest.`);
1324
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" with category=SPRITESHEET must define files.atlas and files.manifest.`);
1194
1325
  continue;
1195
1326
  }
1196
1327
  }
1197
1328
  if (category === 'AUDIO') {
1198
1329
  const hasPrimary = Object.prototype.hasOwnProperty.call(files, 'primary');
1199
1330
  if (!hasPrimary) {
1200
- errors.push(`[${label}] Skipping ${rawName}: embedded asset "${embeddedName}" with category=AUDIO must define files.primary.`);
1331
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" with category=AUDIO must define files.primary.`);
1332
+ continue;
1333
+ }
1334
+ }
1335
+ if (category === 'MODEL_3D' && ownedFormat === 'GLB') {
1336
+ const primaryPath = filePaths.primary;
1337
+ if (!primaryPath) {
1338
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" with format=GLB must define files.primary.`);
1339
+ continue;
1340
+ }
1341
+ try {
1342
+ validateSelfContainedGlb(primaryPath);
1343
+ }
1344
+ catch (error) {
1345
+ const message = typeof error?.message === 'string' ? error.message : 'invalid_glb_payload';
1346
+ if (message === 'glb_external_dependency_unsupported') {
1347
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" GLB must embed all buffers and textures instead of referencing external files.`);
1348
+ }
1349
+ else {
1350
+ errors.push(`[${label}] Skipping ${rawName}: owned asset "${ownedName}" primary GLB is invalid.`);
1351
+ }
1201
1352
  continue;
1202
1353
  }
1203
1354
  }
1204
- if (rejectLegacyShopPriceCoinsField(embedded, errors, `[${label}] Embedded asset "${embeddedName}"`)) {
1355
+ if (rejectLegacyShopPriceCoinsField(owned, errors, `[${label}] Owned asset "${ownedName}"`)) {
1205
1356
  continue;
1206
1357
  }
1207
- embeddedAssets.push({
1208
- kind: 'embedded-asset',
1358
+ ownedAssets.push({
1359
+ kind: 'owned-asset',
1209
1360
  appName: rawName,
1210
- name: embeddedName,
1361
+ name: ownedName,
1362
+ runtimeKey,
1211
1363
  cataloguePath: label,
1212
1364
  category,
1213
1365
  subcategory,
1214
- format: embeddedFormat,
1215
- visibility: normalizeAssetVisibilityValue(embedded?.visibility, errors, `${label} embedded asset "${embeddedName}"`),
1216
- shopListed: typeof embedded?.shopListed === 'boolean' ? embedded.shopListed : undefined,
1217
- shopPriceCredits: Number.isInteger(embedded?.shopPriceCredits) ? Number(embedded.shopPriceCredits) : undefined,
1366
+ assetSpec,
1367
+ format: ownedFormat,
1368
+ visibility: normalizeAssetVisibilityValue(owned?.visibility, errors, `${label} owned asset "${ownedName}"`),
1369
+ shopListed: typeof owned?.shopListed === 'boolean' ? owned.shopListed : undefined,
1370
+ shopPriceCredits: Number.isInteger(owned?.shopPriceCredits) ? Number(owned.shopPriceCredits) : undefined,
1218
1371
  files,
1219
1372
  filePaths,
1220
1373
  });
1221
1374
  }
1375
+ const runtimeValidationErrorCount = errors.length;
1376
+ const seenOwnedAssetNames = new Set();
1377
+ const seenOwnedRuntimeKeys = new Set();
1378
+ for (const ownedAsset of ownedAssets) {
1379
+ if (seenOwnedAssetNames.has(ownedAsset.name)) {
1380
+ errors.push(`[${label}] App "${rawName}" ownedAssets contains duplicate name "${ownedAsset.name}".`);
1381
+ continue;
1382
+ }
1383
+ seenOwnedAssetNames.add(ownedAsset.name);
1384
+ if (!ownedAsset.runtimeKey) {
1385
+ continue;
1386
+ }
1387
+ if (seenOwnedRuntimeKeys.has(ownedAsset.runtimeKey)) {
1388
+ errors.push(`[${label}] App "${rawName}" ownedAssets contains duplicate runtimeKey "${ownedAsset.runtimeKey}".`);
1389
+ continue;
1390
+ }
1391
+ seenOwnedRuntimeKeys.add(ownedAsset.runtimeKey);
1392
+ }
1393
+ for (const dependency of usesAssets) {
1394
+ if (!dependency.runtimeKey) {
1395
+ continue;
1396
+ }
1397
+ if (seenOwnedRuntimeKeys.has(dependency.runtimeKey)) {
1398
+ errors.push(`[${label}] App "${rawName}" reuses runtimeKey "${dependency.runtimeKey}" across ownedAssets and uses.assets.`);
1399
+ }
1400
+ }
1401
+ if (errors.length > runtimeValidationErrorCount) {
1402
+ continue;
1403
+ }
1222
1404
  tasks.push({
1223
1405
  kind: 'app',
1224
1406
  name: rawName,
@@ -1257,7 +1439,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1257
1439
  achievements,
1258
1440
  leaderboards,
1259
1441
  remix,
1260
- embeddedAssets,
1442
+ ownedAssets,
1261
1443
  assetSpecSupport,
1262
1444
  uses: {
1263
1445
  assets: usesAssets,
@@ -1390,11 +1572,25 @@ function buildAssetSpecTasks(catalogues) {
1390
1572
  }
1391
1573
  return { tasks, warnings, errors };
1392
1574
  }
1393
- function buildAssetTasks(catalogues, _assetSpecTasks) {
1575
+ function buildAssetTasks(catalogues, assetSpecTasks) {
1394
1576
  const tasks = [];
1395
1577
  const warnings = [];
1396
1578
  const errors = [];
1397
1579
  const seen = new Map();
1580
+ const exactAssetSpecTaskByRef = new Map();
1581
+ const uniqueAssetSpecTaskByNameVersion = new Map();
1582
+ for (const task of assetSpecTasks) {
1583
+ if (task.username) {
1584
+ exactAssetSpecTaskByRef.set(`asset-spec:${task.username}/${task.name}@${task.version}`, task);
1585
+ }
1586
+ const nameVersionKey = `${task.name}@${task.version}`;
1587
+ if (!uniqueAssetSpecTaskByNameVersion.has(nameVersionKey)) {
1588
+ uniqueAssetSpecTaskByNameVersion.set(nameVersionKey, task);
1589
+ }
1590
+ else {
1591
+ uniqueAssetSpecTaskByNameVersion.set(nameVersionKey, null);
1592
+ }
1593
+ }
1398
1594
  for (const file of catalogues) {
1399
1595
  const label = file.relativePath || 'catalogue.json';
1400
1596
  const rawAssets = Array.isArray(file.data.assets) ? file.data.assets : [];
@@ -1520,6 +1716,22 @@ function buildAssetTasks(catalogues, _assetSpecTasks) {
1520
1716
  if (rejectLegacyShopPriceCoinsField(entry, errors, `[${label}] Asset "${name}"`)) {
1521
1717
  continue;
1522
1718
  }
1719
+ let assetSpecContract;
1720
+ if (assetSpec) {
1721
+ const directAssetSpecTask = exactAssetSpecTaskByRef.get(assetSpec);
1722
+ if (directAssetSpecTask) {
1723
+ assetSpecContract = directAssetSpecTask.contract;
1724
+ }
1725
+ else {
1726
+ const parsedAssetSpec = (0, types_1.parseAssetSpecVersionRef)(assetSpec);
1727
+ if (parsedAssetSpec) {
1728
+ const fallbackAssetSpecTask = uniqueAssetSpecTaskByNameVersion.get(`${parsedAssetSpec.name}@${parsedAssetSpec.version}`);
1729
+ if (fallbackAssetSpecTask) {
1730
+ assetSpecContract = fallbackAssetSpecTask.contract;
1731
+ }
1732
+ }
1733
+ }
1734
+ }
1523
1735
  tasks.push({
1524
1736
  kind: 'asset',
1525
1737
  name,
@@ -1537,6 +1749,7 @@ function buildAssetTasks(catalogues, _assetSpecTasks) {
1537
1749
  shopPriceCredits: Number.isInteger(entry.shopPriceCredits) ? Number(entry.shopPriceCredits) : undefined,
1538
1750
  files,
1539
1751
  filePaths,
1752
+ assetSpecContract,
1540
1753
  relations,
1541
1754
  });
1542
1755
  }
@@ -1577,6 +1790,110 @@ function buildAssetPackTasks(catalogues) {
1577
1790
  const assets = Array.isArray(entry.assets)
1578
1791
  ? entry.assets.filter((value) => typeof value === 'string' && value.trim().length > 0).map((value) => value.trim())
1579
1792
  : [];
1793
+ const ownedAssets = [];
1794
+ const rawOwnedAssets = Array.isArray(entry.ownedAssets)
1795
+ ? entry.ownedAssets
1796
+ : [];
1797
+ for (let ownedIndex = 0; ownedIndex < rawOwnedAssets.length; ownedIndex++) {
1798
+ const owned = rawOwnedAssets[ownedIndex];
1799
+ const ownedName = typeof owned?.name === 'string' ? owned.name.trim() : '';
1800
+ if (!ownedName) {
1801
+ errors.push(`[${label}] Skipping asset pack "${name}": ownedAssets[${ownedIndex}] is missing a name.`);
1802
+ continue;
1803
+ }
1804
+ if (typeof owned?.runtimeKey === 'string' && owned.runtimeKey.trim().length > 0) {
1805
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" must not define runtimeKey.`);
1806
+ continue;
1807
+ }
1808
+ const category = normalizeAssetCategoryValue(owned?.category, errors, `[${label}] Skipping asset pack "${name}": owned asset "${ownedName}"`);
1809
+ if (!category) {
1810
+ continue;
1811
+ }
1812
+ const subcategory = normalizeAssetSubcategoryValue(owned?.subcategory, category, errors, `[${label}] Skipping asset pack "${name}": owned asset "${ownedName}"`);
1813
+ if (subcategory === null && category !== 'CUSTOM') {
1814
+ continue;
1815
+ }
1816
+ const fileEntries = owned?.files && typeof owned.files === 'object' && !Array.isArray(owned.files)
1817
+ ? owned.files
1818
+ : null;
1819
+ const assetSpec = (0, assetSpecs_1.normalizeAssetSpecVersionRef)(owned?.assetSpec, errors, `[${label}] Asset pack "${name}" owned asset "${ownedName}"`);
1820
+ const ownedFormat = typeof owned?.format === 'string' && owned.format.trim().length > 0
1821
+ ? owned.format.trim().toUpperCase()
1822
+ : undefined;
1823
+ const generatedModel3DFormat = subcategory
1824
+ ? inferGeneratedModel3DFormat(category, subcategory, ownedFormat, fileEntries)
1825
+ : null;
1826
+ const generatedModel3DRoles = generatedModel3DFormat ? getGeneratedModel3DFileRoles(generatedModel3DFormat) : null;
1827
+ if (!fileEntries || Object.keys(fileEntries).length === 0) {
1828
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" must define files.`);
1829
+ continue;
1830
+ }
1831
+ const files = {};
1832
+ const filePaths = {};
1833
+ let ownedAssetHasError = false;
1834
+ for (const [role, fileValue] of Object.entries(fileEntries)) {
1835
+ if (typeof fileValue !== 'string' || !fileValue.trim()) {
1836
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" file role "${role}" must be a non-empty string path.`);
1837
+ ownedAssetHasError = true;
1838
+ continue;
1839
+ }
1840
+ const relativeOwnedPath = fileValue.trim();
1841
+ const absoluteOwnedPath = (0, node_path_1.resolve)(file.directory, relativeOwnedPath);
1842
+ const normalizedRole = normalizeFileRole(role);
1843
+ const allowMissingGenerated = Boolean(generatedModel3DRoles?.generated.has(normalizedRole));
1844
+ if (!(0, node_fs_1.existsSync)(absoluteOwnedPath) || !(0, node_fs_1.statSync)(absoluteOwnedPath).isFile()) {
1845
+ if (allowMissingGenerated) {
1846
+ files[role] = relativeOwnedPath.replace(/\\/g, '/');
1847
+ filePaths[role] = absoluteOwnedPath;
1848
+ continue;
1849
+ }
1850
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" file not found at ${absoluteOwnedPath}.`);
1851
+ ownedAssetHasError = true;
1852
+ continue;
1853
+ }
1854
+ files[role] = relativeOwnedPath.replace(/\\/g, '/');
1855
+ filePaths[role] = absoluteOwnedPath;
1856
+ }
1857
+ if (ownedAssetHasError) {
1858
+ continue;
1859
+ }
1860
+ if (generatedModel3DRoles) {
1861
+ const normalizedRoles = new Set(Object.keys(files).map((role) => normalizeFileRole(role)));
1862
+ const missingRoles = generatedModel3DRoles.required.filter((role) => !normalizedRoles.has(role));
1863
+ if (missingRoles.length > 0) {
1864
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" with subcategory=${subcategory} must define files.${missingRoles.join(' and files.')}.`);
1865
+ continue;
1866
+ }
1867
+ }
1868
+ if (category === 'SPRITESHEET') {
1869
+ const hasAtlas = Object.prototype.hasOwnProperty.call(files, 'atlas');
1870
+ const hasManifest = Object.prototype.hasOwnProperty.call(files, 'manifest');
1871
+ if (!hasAtlas || !hasManifest) {
1872
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" with category=SPRITESHEET must define files.atlas and files.manifest.`);
1873
+ continue;
1874
+ }
1875
+ }
1876
+ if (category === 'AUDIO' && !Object.prototype.hasOwnProperty.call(files, 'primary')) {
1877
+ errors.push(`[${label}] Skipping asset pack "${name}": owned asset "${ownedName}" with category=AUDIO must define files.primary.`);
1878
+ continue;
1879
+ }
1880
+ if (rejectLegacyShopPriceCoinsField(owned, errors, `[${label}] Asset pack "${name}" owned asset "${ownedName}"`)) {
1881
+ continue;
1882
+ }
1883
+ ownedAssets.push({
1884
+ name: ownedName,
1885
+ cataloguePath: label,
1886
+ category,
1887
+ subcategory,
1888
+ assetSpec,
1889
+ format: ownedFormat,
1890
+ visibility: normalizeAssetVisibilityValue(owned?.visibility, errors, `${label} asset pack "${name}" owned asset "${ownedName}"`),
1891
+ shopListed: typeof owned?.shopListed === 'boolean' ? owned.shopListed : undefined,
1892
+ shopPriceCredits: Number.isInteger(owned?.shopPriceCredits) ? Number(owned.shopPriceCredits) : undefined,
1893
+ files,
1894
+ filePaths,
1895
+ });
1896
+ }
1580
1897
  let hostingMode;
1581
1898
  const rawHostingMode = typeof entry.hostingMode === 'string' ? entry.hostingMode.trim().toUpperCase() : '';
1582
1899
  if (rawHostingMode) {
@@ -1611,7 +1928,7 @@ function buildAssetPackTasks(catalogues) {
1611
1928
  if (externalUrl && hostingMode !== 'EXTERNAL') {
1612
1929
  hostingMode = 'EXTERNAL';
1613
1930
  }
1614
- if (assets.length === 0 && hostingMode !== 'EXTERNAL') {
1931
+ if (assets.length === 0 && ownedAssets.length === 0 && hostingMode !== 'EXTERNAL') {
1615
1932
  errors.push(`[${label}] Asset pack "${name}" must reference at least one asset.`);
1616
1933
  continue;
1617
1934
  }
@@ -1797,6 +2114,7 @@ function buildAssetPackTasks(catalogues) {
1797
2114
  username,
1798
2115
  remix,
1799
2116
  assets,
2117
+ ownedAssets,
1800
2118
  hostingMode,
1801
2119
  externalUrl,
1802
2120
  downloadUrl,
@@ -1917,7 +2235,7 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1917
2235
  apps: [],
1918
2236
  assetSpecs: [],
1919
2237
  assets: [],
1920
- embeddedAssets: [],
2238
+ ownedAssets: [],
1921
2239
  assetPacks: [],
1922
2240
  warnings: [],
1923
2241
  errors: discoveryErrors,
@@ -1930,7 +2248,7 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1930
2248
  apps: [],
1931
2249
  assetSpecs: [],
1932
2250
  assets: [],
1933
- embeddedAssets: [],
2251
+ ownedAssets: [],
1934
2252
  assetPacks: [],
1935
2253
  warnings: [],
1936
2254
  errors: [
@@ -1942,14 +2260,14 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1942
2260
  const { tasks: assetSpecTasks, warnings: assetSpecWarnings, errors: assetSpecErrors } = buildAssetSpecTasks(files);
1943
2261
  const { tasks: assetTasks, warnings: assetWarnings, errors: assetErrors } = buildAssetTasks(files, assetSpecTasks);
1944
2262
  const { tasks: assetPackTasks, warnings: assetPackWarnings, errors: assetPackErrors } = buildAssetPackTasks(files);
1945
- const embeddedAssetTasks = appTasks.flatMap((task) => task.embeddedAssets);
2263
+ const ownedAssetTasks = appTasks.flatMap((task) => task.ownedAssets);
1946
2264
  const warnings = [...appWarnings, ...assetSpecWarnings, ...assetWarnings, ...assetPackWarnings];
1947
2265
  const errors = [...appErrors, ...assetSpecErrors, ...assetErrors, ...assetPackErrors];
1948
2266
  return {
1949
2267
  apps: appTasks,
1950
2268
  assetSpecs: assetSpecTasks,
1951
2269
  assets: assetTasks,
1952
- embeddedAssets: embeddedAssetTasks,
2270
+ ownedAssets: ownedAssetTasks,
1953
2271
  assetPacks: assetPackTasks,
1954
2272
  warnings,
1955
2273
  errors,
@@ -2,6 +2,7 @@ import type { ApiClient } from '@playdrop/api-client';
2
2
  import type { AiClient } from '@playdrop/ai-client';
3
3
  import { type CliAccountSession, type CliConfig } from './config';
4
4
  import { type EnvironmentConfig } from './environment';
5
+ import { type ResolvedWorkspaceAuthConfig } from './workspaceAuth';
5
6
  export type EnvironmentContext = {
6
7
  client: ApiClient;
7
8
  aiClient: AiClient;
@@ -10,11 +11,14 @@ export type EnvironmentContext = {
10
11
  token: string;
11
12
  config: CliConfig;
12
13
  account: CliAccountSession | null;
14
+ workspaceAuth: ResolvedWorkspaceAuthConfig | null;
13
15
  };
14
16
  type EnvironmentCallback = (ctx: EnvironmentContext) => Promise<void> | void;
15
- type ResolveAuthenticatedEnvironmentOptions = {
17
+ export type ResolveAuthenticatedEnvironmentOptions = {
16
18
  workspacePath?: string;
17
19
  };
20
+ export declare function resolveAuthenticatedEnvironmentContext(command: string, actionLabel: string, options?: ResolveAuthenticatedEnvironmentOptions): Promise<EnvironmentContext | null>;
21
+ export declare function resolveOptionalEnvironmentContext(command: string, options?: ResolveAuthenticatedEnvironmentOptions): Promise<EnvironmentContext | null>;
18
22
  export declare function withEnvironment(command: string, actionLabel: string, callback: EnvironmentCallback, options?: ResolveAuthenticatedEnvironmentOptions): Promise<void>;
19
23
  export declare function withPublicEnvironment(command: string, callback: EnvironmentCallback): Promise<void>;
20
24
  export {};