@playdrop/playdrop-cli 0.5.1 → 0.5.3

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 (155) hide show
  1. package/config/client-meta.json +7 -7
  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 +132 -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/clientInfo.js +19 -3
  13. package/dist/commands/ads.d.ts +8 -0
  14. package/dist/commands/ads.js +124 -0
  15. package/dist/commands/boosts.d.ts +25 -0
  16. package/dist/commands/boosts.js +209 -0
  17. package/dist/commands/browse.d.ts +6 -1
  18. package/dist/commands/browse.js +365 -124
  19. package/dist/commands/captureListing.d.ts +53 -0
  20. package/dist/commands/captureListing.js +804 -0
  21. package/dist/commands/captureRemote.js +33 -0
  22. package/dist/commands/create.d.ts +1 -0
  23. package/dist/commands/create.js +183 -3
  24. package/dist/commands/credits.d.ts +6 -0
  25. package/dist/commands/credits.js +47 -1
  26. package/dist/commands/detail.js +38 -4
  27. package/dist/commands/devServer.js +10 -5
  28. package/dist/commands/generation.d.ts +2 -0
  29. package/dist/commands/generation.js +1 -0
  30. package/dist/commands/search.d.ts +5 -0
  31. package/dist/commands/search.js +139 -17
  32. package/dist/commands/tags.d.ts +7 -0
  33. package/dist/commands/tags.js +63 -0
  34. package/dist/commands/upload-content.d.ts +13 -3
  35. package/dist/commands/upload-content.js +86 -20
  36. package/dist/commands/upload.d.ts +2 -0
  37. package/dist/commands/upload.js +187 -11
  38. package/dist/commands/validate.js +163 -2
  39. package/dist/commands/versionsBrowse.js +128 -91
  40. package/dist/index.js +145 -3
  41. package/dist/refs.d.ts +2 -2
  42. package/dist/refs.js +13 -1
  43. package/dist/taskSelection.js +6 -3
  44. package/dist/taskUtils.d.ts +2 -2
  45. package/dist/taskUtils.js +1 -0
  46. package/dist/uploadLog.d.ts +1 -1
  47. package/dist/uploadLog.js +2 -2
  48. package/node_modules/@playdrop/ai-client/package.json +1 -1
  49. package/node_modules/@playdrop/api-client/dist/client.d.ts +131 -10
  50. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/client.js +6 -0
  52. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +9 -1
  53. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/admin.js +45 -0
  55. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +3 -0
  56. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  57. package/node_modules/@playdrop/api-client/dist/domains/apps.js +27 -0
  58. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts +2 -0
  59. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts.map +1 -1
  60. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.js +16 -0
  61. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +44 -2
  62. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  63. package/node_modules/@playdrop/api-client/dist/domains/assets.js +260 -3
  64. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +17 -1
  65. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  66. package/node_modules/@playdrop/api-client/dist/domains/payments.js +173 -0
  67. package/node_modules/@playdrop/api-client/dist/domains/search.d.ts.map +1 -1
  68. package/node_modules/@playdrop/api-client/dist/domains/search.js +39 -11
  69. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts +34 -0
  70. package/node_modules/@playdrop/api-client/dist/domains/tags.d.ts.map +1 -0
  71. package/node_modules/@playdrop/api-client/dist/domains/tags.js +111 -0
  72. package/node_modules/@playdrop/api-client/dist/index.d.ts +61 -1
  73. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  74. package/node_modules/@playdrop/api-client/dist/index.js +50 -0
  75. package/node_modules/@playdrop/api-client/package.json +1 -1
  76. package/node_modules/@playdrop/boxel-core/dist/test/entity-utils.test.d.ts +1 -0
  77. package/node_modules/@playdrop/boxel-core/dist/test/entity-utils.test.js +92 -0
  78. package/node_modules/@playdrop/boxel-core/dist/test/entity-utils.test.js.map +1 -0
  79. package/node_modules/@playdrop/boxel-core/dist/test/greedy-mesher.test.d.ts +1 -0
  80. package/node_modules/@playdrop/boxel-core/dist/test/greedy-mesher.test.js +48 -0
  81. package/node_modules/@playdrop/boxel-core/dist/test/greedy-mesher.test.js.map +1 -0
  82. package/node_modules/@playdrop/boxel-core/dist/test/humanoid/humanoid-builders.test.d.ts +1 -0
  83. package/node_modules/@playdrop/boxel-core/dist/test/humanoid/humanoid-builders.test.js +270 -0
  84. package/node_modules/@playdrop/boxel-core/dist/test/humanoid/humanoid-builders.test.js.map +1 -0
  85. package/node_modules/@playdrop/boxel-core/dist/test/index.test.d.ts +1 -0
  86. package/node_modules/@playdrop/boxel-core/dist/test/index.test.js +48 -0
  87. package/node_modules/@playdrop/boxel-core/dist/test/index.test.js.map +1 -0
  88. package/node_modules/@playdrop/boxel-core/dist/test/layer-mode.test.d.ts +1 -0
  89. package/node_modules/@playdrop/boxel-core/dist/test/layer-mode.test.js +67 -0
  90. package/node_modules/@playdrop/boxel-core/dist/test/layer-mode.test.js.map +1 -0
  91. package/node_modules/@playdrop/boxel-core/dist/test/materials.test.d.ts +1 -0
  92. package/node_modules/@playdrop/boxel-core/dist/test/materials.test.js +55 -0
  93. package/node_modules/@playdrop/boxel-core/dist/test/materials.test.js.map +1 -0
  94. package/node_modules/@playdrop/boxel-core/dist/test/palette-tools.test.d.ts +1 -0
  95. package/node_modules/@playdrop/boxel-core/dist/test/palette-tools.test.js +124 -0
  96. package/node_modules/@playdrop/boxel-core/dist/test/palette-tools.test.js.map +1 -0
  97. package/node_modules/@playdrop/boxel-core/dist/test/serialization.test.d.ts +1 -0
  98. package/node_modules/@playdrop/boxel-core/dist/test/serialization.test.js +35 -0
  99. package/node_modules/@playdrop/boxel-core/dist/test/serialization.test.js.map +1 -0
  100. package/node_modules/@playdrop/boxel-core/dist/test/textures.test.d.ts +1 -0
  101. package/node_modules/@playdrop/boxel-core/dist/test/textures.test.js +120 -0
  102. package/node_modules/@playdrop/boxel-core/dist/test/textures.test.js.map +1 -0
  103. package/node_modules/@playdrop/boxel-core/dist/test/types.test.d.ts +1 -0
  104. package/node_modules/@playdrop/boxel-core/dist/test/types.test.js +32 -0
  105. package/node_modules/@playdrop/boxel-core/dist/test/types.test.js.map +1 -0
  106. package/node_modules/@playdrop/boxel-core/dist/test/upscale.test.d.ts +1 -0
  107. package/node_modules/@playdrop/boxel-core/dist/test/upscale.test.js +100 -0
  108. package/node_modules/@playdrop/boxel-core/dist/test/upscale.test.js.map +1 -0
  109. package/node_modules/@playdrop/boxel-core/dist/test/validation.test.d.ts +1 -0
  110. package/node_modules/@playdrop/boxel-core/dist/test/validation.test.js +61 -0
  111. package/node_modules/@playdrop/boxel-core/dist/test/validation.test.js.map +1 -0
  112. package/node_modules/@playdrop/boxel-core/dist/test/voxels.test.d.ts +1 -0
  113. package/node_modules/@playdrop/boxel-core/dist/test/voxels.test.js +51 -0
  114. package/node_modules/@playdrop/boxel-core/dist/test/voxels.test.js.map +1 -0
  115. package/node_modules/@playdrop/boxel-core/package.json +1 -1
  116. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  117. package/node_modules/@playdrop/config/client-meta.json +7 -7
  118. package/node_modules/@playdrop/config/dist/src/constants.d.ts +11 -0
  119. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  120. package/node_modules/@playdrop/config/dist/src/constants.js +12 -1
  121. package/node_modules/@playdrop/config/dist/src/creator-docs.d.ts +24 -0
  122. package/node_modules/@playdrop/config/dist/src/creator-docs.d.ts.map +1 -0
  123. package/node_modules/@playdrop/config/dist/src/creator-docs.js +253 -0
  124. package/node_modules/@playdrop/config/dist/src/creator-faq.d.ts +17 -0
  125. package/node_modules/@playdrop/config/dist/src/creator-faq.d.ts.map +1 -0
  126. package/node_modules/@playdrop/config/dist/src/creator-faq.js +141 -0
  127. package/node_modules/@playdrop/config/dist/test/creator-docs.test.d.ts +2 -0
  128. package/node_modules/@playdrop/config/dist/test/creator-docs.test.d.ts.map +1 -0
  129. package/node_modules/@playdrop/config/dist/test/creator-docs.test.js +36 -0
  130. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  131. package/node_modules/@playdrop/config/package.json +1 -1
  132. package/node_modules/@playdrop/types/dist/api.d.ts +346 -6
  133. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  134. package/node_modules/@playdrop/types/dist/api.js +52 -1
  135. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +7 -1
  136. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  137. package/node_modules/@playdrop/types/dist/asset-spec-contract-meta-schema.json +86 -0
  138. package/node_modules/@playdrop/types/dist/asset-spec.d.ts +163 -0
  139. package/node_modules/@playdrop/types/dist/asset-spec.d.ts.map +1 -0
  140. package/node_modules/@playdrop/types/dist/asset-spec.js +101 -0
  141. package/node_modules/@playdrop/types/dist/asset.d.ts +23 -6
  142. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  143. package/node_modules/@playdrop/types/dist/asset.js +4 -1
  144. package/node_modules/@playdrop/types/dist/graph.d.ts +4 -2
  145. package/node_modules/@playdrop/types/dist/graph.d.ts.map +1 -1
  146. package/node_modules/@playdrop/types/dist/graph.js +9 -2
  147. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  148. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  149. package/node_modules/@playdrop/types/dist/index.js +1 -0
  150. package/node_modules/@playdrop/types/dist/version.d.ts +13 -0
  151. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  152. package/node_modules/@playdrop/types/dist/version.js +21 -0
  153. package/node_modules/@playdrop/types/package.json +6 -1
  154. package/node_modules/@playdrop/vox-three/package.json +1 -1
  155. package/package.json +3 -1
package/dist/catalogue.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateAppMetadata = validateAppMetadata;
4
+ exports.resolveCatalogueTagGroups = resolveCatalogueTagGroups;
4
5
  exports.resolveCatalogueEntries = resolveCatalogueEntries;
5
6
  exports.resolveWorkspaceApp = resolveWorkspaceApp;
6
7
  exports.formatMetadataWarnings = formatMetadataWarnings;
@@ -10,6 +11,7 @@ const node_fs_1 = require("node:fs");
10
11
  const node_path_1 = require("node:path");
11
12
  const types_1 = require("@playdrop/types");
12
13
  const catalogue_utils_1 = require("./catalogue-utils");
14
+ const assetSpecs_1 = require("./assetSpecs");
13
15
  const HEX_COLOR_REGEX = /^#?(?:[0-9a-fA-F]{6})$/;
14
16
  const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
15
17
  const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
@@ -20,6 +22,7 @@ const BOXEL_GENERATED_FILE_ROLES = new Set(['mesh', 'preview']);
20
22
  const BOXEL_REQUIRED_FILE_ROLES = ['primary', 'mesh', 'preview'];
21
23
  const VOX_GENERATED_FILE_ROLES = new Set(['boxel', 'mesh', 'preview']);
22
24
  const VOX_REQUIRED_FILE_ROLES = ['primary', 'boxel', 'mesh', 'preview'];
25
+ const MAX_CONTENT_TAGS_PER_ITEM = 8;
23
26
  const ASSET_SUBCATEGORY_SET_BY_CATEGORY = types_1.ASSET_SUBCATEGORY_DEFINITIONS.reduce((acc, definition) => {
24
27
  if (!acc[definition.category]) {
25
28
  acc[definition.category] = new Set();
@@ -165,6 +168,69 @@ function rejectLegacyShopPriceCoinsField(value, errors, context) {
165
168
  }
166
169
  return false;
167
170
  }
171
+ function normalizeCatalogueTags(value, errors, context) {
172
+ if (value === undefined || value === null) {
173
+ return [];
174
+ }
175
+ if (!Array.isArray(value)) {
176
+ errors.push(`${context} tags must be an array of canonical tag refs.`);
177
+ return [];
178
+ }
179
+ const normalizedRefs = [];
180
+ const seenRefs = new Set();
181
+ for (let index = 0; index < value.length; index += 1) {
182
+ const entry = value[index];
183
+ if (typeof entry !== 'string' || entry.trim().length === 0) {
184
+ errors.push(`${context} tags[${index}] must be a non-empty canonical tag ref string.`);
185
+ continue;
186
+ }
187
+ let normalizedRef = '';
188
+ try {
189
+ normalizedRef = (0, types_1.normalizeTagRef)(entry);
190
+ }
191
+ catch {
192
+ errors.push(`${context} tags[${index}] must use group-slug/tag-slug format.`);
193
+ continue;
194
+ }
195
+ if (seenRefs.has(normalizedRef)) {
196
+ errors.push(`${context} tags contains duplicate ref "${normalizedRef}".`);
197
+ continue;
198
+ }
199
+ seenRefs.add(normalizedRef);
200
+ normalizedRefs.push(normalizedRef);
201
+ }
202
+ if (normalizedRefs.length > MAX_CONTENT_TAGS_PER_ITEM) {
203
+ errors.push(`${context} tags supports at most ${MAX_CONTENT_TAGS_PER_ITEM} refs.`);
204
+ }
205
+ return normalizedRefs;
206
+ }
207
+ function normalizeCatalogueAllowedTargetKinds(value, errors, context) {
208
+ if (value === undefined || value === null) {
209
+ return undefined;
210
+ }
211
+ if (!Array.isArray(value)) {
212
+ errors.push(`${context} allowedTargetKinds must be an array of saved item kinds.`);
213
+ return undefined;
214
+ }
215
+ const normalizedKinds = [];
216
+ const seenKinds = new Set();
217
+ for (let index = 0; index < value.length; index += 1) {
218
+ const entry = value[index];
219
+ const normalizedKind = typeof entry === 'string' ? entry.trim().toUpperCase() : '';
220
+ if (!types_1.SAVED_ITEM_KIND_VALUES.includes(normalizedKind)) {
221
+ errors.push(`${context} allowedTargetKinds[${index}] must be one of ${types_1.SAVED_ITEM_KIND_VALUES.join(', ')}.`);
222
+ continue;
223
+ }
224
+ const savedItemKind = normalizedKind;
225
+ if (seenKinds.has(savedItemKind)) {
226
+ errors.push(`${context} allowedTargetKinds contains duplicate value "${savedItemKind}".`);
227
+ continue;
228
+ }
229
+ seenKinds.add(savedItemKind);
230
+ normalizedKinds.push(savedItemKind);
231
+ }
232
+ return normalizedKinds;
233
+ }
168
234
  function normalizeRelativePath(root, target) {
169
235
  const rel = (0, node_path_1.relative)(root, target).split(node_path_1.sep).join('/');
170
236
  return rel.replace(/^\/+/, '');
@@ -206,7 +272,7 @@ function readCatalogueFile(rootDir, filePath) {
206
272
  return { error: `catalogue.json forbids top-level "${key}"` };
207
273
  }
208
274
  }
209
- const allowedKeys = new Set(['apps', 'assets', 'assetPacks']);
275
+ const allowedKeys = new Set(['apps', 'tagGroups', 'assetSpecs', 'assets', 'assetPacks']);
210
276
  const unknownKeys = Object.keys(parsed).filter((key) => !allowedKeys.has(key));
211
277
  if (unknownKeys.length > 0) {
212
278
  return {
@@ -752,6 +818,7 @@ function buildAppTasks(rootDir, catalogues, options) {
752
818
  const errorCountBeforePlayerMeta = errors.length;
753
819
  const achievements = validateAchievementDefinitions(entry.achievements, file.directory, errors);
754
820
  const leaderboards = validateLeaderboardDefinitions(entry.leaderboards, errors);
821
+ const assetSpecSupport = (0, assetSpecs_1.normalizeAssetSpecSupportEntries)(entry.assetSpecSupport, errors, `[${label}] App "${rawName}"`);
755
822
  if (errors.length > errorCountBeforePlayerMeta) {
756
823
  continue;
757
824
  }
@@ -1044,8 +1111,13 @@ function buildAppTasks(rootDir, catalogues, options) {
1044
1111
  const usesPacks = Array.isArray(entry.uses?.packs)
1045
1112
  ? entry.uses.packs.filter((value) => typeof value === 'string' && value.trim().length > 0)
1046
1113
  : [];
1114
+ const errorCountBeforeTagMetadata = errors.length;
1047
1115
  const remix = normalizeCatalogueRemixRef(entry.remix, 'app', errors, `[${label}] App "${rawName}"`);
1116
+ const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] App "${rawName}"`);
1048
1117
  const graphRelations = normalizeCatalogueRelations(entry.relations, errors, `[${label}] App "${rawName}"`) ?? [];
1118
+ if (errors.length > errorCountBeforeTagMetadata) {
1119
+ continue;
1120
+ }
1049
1121
  const embeddedAssets = [];
1050
1122
  const rawEmbeddedAssets = Array.isArray(entry.embeddedAssets)
1051
1123
  ? entry.embeddedAssets
@@ -1173,6 +1245,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1173
1245
  version,
1174
1246
  releaseNotes,
1175
1247
  versionVisibility,
1248
+ tags,
1176
1249
  hostingMode,
1177
1250
  authMode,
1178
1251
  controllerMode,
@@ -1185,6 +1258,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1185
1258
  leaderboards,
1186
1259
  remix,
1187
1260
  embeddedAssets,
1261
+ assetSpecSupport,
1188
1262
  uses: {
1189
1263
  assets: usesAssets,
1190
1264
  packs: usesPacks,
@@ -1197,7 +1271,126 @@ function buildAppTasks(rootDir, catalogues, options) {
1197
1271
  }
1198
1272
  return { tasks, warnings, errors };
1199
1273
  }
1200
- function buildAssetTasks(catalogues) {
1274
+ function buildAssetSpecTasks(catalogues) {
1275
+ const tasks = [];
1276
+ const warnings = [];
1277
+ const errors = [];
1278
+ const seen = new Map();
1279
+ for (const file of catalogues) {
1280
+ const label = file.relativePath || 'catalogue.json';
1281
+ const rawAssetSpecs = Array.isArray(file.data.assetSpecs) ? file.data.assetSpecs : [];
1282
+ for (let index = 0; index < rawAssetSpecs.length; index += 1) {
1283
+ const entry = rawAssetSpecs[index];
1284
+ if (!entry || typeof entry !== 'object') {
1285
+ errors.push(`[${label}] Invalid assetSpecs[${index}] entry; expected object.`);
1286
+ continue;
1287
+ }
1288
+ const name = typeof entry.name === 'string' ? entry.name.trim() : '';
1289
+ if (!name) {
1290
+ errors.push(`[${label}] assetSpecs[${index}] is missing a name.`);
1291
+ continue;
1292
+ }
1293
+ if (!(0, assetSpecs_1.validateAssetSpecName)(name, errors, `[${label}] Asset spec "${name}"`)) {
1294
+ continue;
1295
+ }
1296
+ const version = typeof entry.version === 'string' ? entry.version.trim() : '';
1297
+ if (!SEMVER_REGEX.test(version)) {
1298
+ errors.push(`[${label}] Asset spec "${name}" must define a semver version.`);
1299
+ continue;
1300
+ }
1301
+ const dedupeKey = `${label}::${name}::${version}`;
1302
+ if (seen.has(dedupeKey)) {
1303
+ errors.push(`[${label}] Duplicate asset spec "${name}@${version}" in same catalogue.`);
1304
+ continue;
1305
+ }
1306
+ seen.set(dedupeKey, label);
1307
+ const displayName = typeof entry.displayName === 'string' ? entry.displayName.trim() : '';
1308
+ if (!displayName) {
1309
+ errors.push(`[${label}] Asset spec "${name}@${version}" must define displayName.`);
1310
+ continue;
1311
+ }
1312
+ const symbol = typeof entry.symbol === 'string' ? entry.symbol.trim() : '';
1313
+ if (!symbol) {
1314
+ errors.push(`[${label}] Asset spec "${name}@${version}" must define symbol.`);
1315
+ continue;
1316
+ }
1317
+ const symbolPath = (0, node_path_1.resolve)(file.directory, symbol);
1318
+ if (!(0, node_fs_1.existsSync)(symbolPath) || !(0, node_fs_1.statSync)(symbolPath).isFile()) {
1319
+ errors.push(`[${label}] Asset spec "${name}@${version}" symbol file not found at ${symbolPath}.`);
1320
+ continue;
1321
+ }
1322
+ const contractField = typeof entry.contract === 'string' ? entry.contract.trim() : '';
1323
+ if (!contractField) {
1324
+ errors.push(`[${label}] Asset spec "${name}@${version}" must define contract.`);
1325
+ continue;
1326
+ }
1327
+ const loadedContract = (0, assetSpecs_1.loadAndValidateAssetSpecContract)(file.directory, contractField, errors, `[${label}] Asset spec "${name}@${version}"`);
1328
+ if (!loadedContract) {
1329
+ continue;
1330
+ }
1331
+ let validationKind = loadedContract.validationKind;
1332
+ if (typeof entry.validationKind === 'string' && entry.validationKind.trim().length > 0) {
1333
+ const normalizedValidationKind = entry.validationKind.trim().toUpperCase();
1334
+ if (!types_1.ASSET_SPEC_VALIDATION_KIND_VALUES.includes(normalizedValidationKind)) {
1335
+ errors.push(`[${label}] Asset spec "${name}@${version}" validationKind must be one of ${types_1.ASSET_SPEC_VALIDATION_KIND_VALUES.join(', ')}.`);
1336
+ continue;
1337
+ }
1338
+ if (normalizedValidationKind !== loadedContract.validationKind) {
1339
+ errors.push(`[${label}] Asset spec "${name}@${version}" validationKind must match the contract content (${loadedContract.validationKind}).`);
1340
+ continue;
1341
+ }
1342
+ validationKind = normalizedValidationKind;
1343
+ }
1344
+ let status = 'ACTIVE';
1345
+ if (typeof entry.status === 'string' && entry.status.trim().length > 0) {
1346
+ const normalizedStatus = entry.status.trim().toUpperCase();
1347
+ if (!types_1.ASSET_SPEC_STATUS_VALUES.includes(normalizedStatus)) {
1348
+ errors.push(`[${label}] Asset spec "${name}@${version}" status must be one of ${types_1.ASSET_SPEC_STATUS_VALUES.join(', ')}.`);
1349
+ continue;
1350
+ }
1351
+ status = normalizedStatus;
1352
+ }
1353
+ const visibility = normalizeAssetVisibilityValue(entry.visibility, errors, `${label} asset spec "${name}@${version}"`);
1354
+ const successorVersion = typeof entry.successorVersion === 'string' && entry.successorVersion.trim().length > 0
1355
+ ? entry.successorVersion.trim()
1356
+ : undefined;
1357
+ if (successorVersion && !SEMVER_REGEX.test(successorVersion)) {
1358
+ errors.push(`[${label}] Asset spec "${name}@${version}" successorVersion must be a semver string.`);
1359
+ continue;
1360
+ }
1361
+ const documentationField = typeof entry.documentation === 'string' && entry.documentation.trim().length > 0
1362
+ ? entry.documentation.trim()
1363
+ : undefined;
1364
+ const documentationPath = documentationField ? (0, node_path_1.resolve)(file.directory, documentationField) : undefined;
1365
+ if (documentationPath && (!(0, node_fs_1.existsSync)(documentationPath) || !(0, node_fs_1.statSync)(documentationPath).isFile())) {
1366
+ errors.push(`[${label}] Asset spec "${name}@${version}" documentation file not found at ${documentationPath}.`);
1367
+ continue;
1368
+ }
1369
+ tasks.push({
1370
+ kind: 'asset-spec',
1371
+ name,
1372
+ version,
1373
+ displayName,
1374
+ description: typeof entry.description === 'string' ? entry.description.trim() || undefined : undefined,
1375
+ cataloguePath: label,
1376
+ catalogueAbsolutePath: file.absolutePath,
1377
+ catalogueDir: file.directory,
1378
+ username: typeof entry.username === 'string' && entry.username.trim().length > 0 ? entry.username.trim() : undefined,
1379
+ visibility,
1380
+ validationKind,
1381
+ status,
1382
+ releaseNotes: typeof entry.releaseNotes === 'string' ? entry.releaseNotes : undefined,
1383
+ successorVersion,
1384
+ symbolPath,
1385
+ contractPath: loadedContract.contractPath,
1386
+ documentationPath,
1387
+ contract: loadedContract.contract,
1388
+ });
1389
+ }
1390
+ }
1391
+ return { tasks, warnings, errors };
1392
+ }
1393
+ function buildAssetTasks(catalogues, _assetSpecTasks) {
1201
1394
  const tasks = [];
1202
1395
  const warnings = [];
1203
1396
  const errors = [];
@@ -1228,19 +1421,35 @@ function buildAssetTasks(catalogues) {
1228
1421
  continue;
1229
1422
  }
1230
1423
  seen.set(dedupeKey, label);
1231
- const category = normalizeAssetCategoryValue(entry.category, errors, `[${label}] Asset "${name}"`);
1232
- if (!category) {
1233
- continue;
1424
+ const assetSpec = (0, assetSpecs_1.normalizeAssetSpecVersionRef)(entry.assetSpec, errors, `[${label}] Asset "${name}"`);
1425
+ const isCustomAsset = typeof assetSpec === 'string';
1426
+ let category;
1427
+ let subcategory;
1428
+ let format;
1429
+ if (isCustomAsset) {
1430
+ if (entry.category !== undefined || entry.subcategory !== undefined || entry.format !== undefined) {
1431
+ errors.push(`[${label}] Asset "${name}" must not define category, subcategory, or format when assetSpec is set.`);
1432
+ continue;
1433
+ }
1434
+ subcategory = null;
1234
1435
  }
1235
- const subcategory = normalizeAssetSubcategoryValue(entry.subcategory, category, errors, `[${label}] Asset "${name}"`);
1236
- if (!subcategory) {
1237
- continue;
1436
+ else {
1437
+ category = normalizeAssetCategoryValue(entry.category, errors, `[${label}] Asset "${name}"`) ?? undefined;
1438
+ if (!category) {
1439
+ continue;
1440
+ }
1441
+ subcategory = normalizeAssetSubcategoryValue(entry.subcategory, category, errors, `[${label}] Asset "${name}"`);
1442
+ if (!subcategory) {
1443
+ continue;
1444
+ }
1445
+ format = typeof entry.format === 'string' && entry.format.trim().length > 0 ? entry.format.trim().toUpperCase() : undefined;
1238
1446
  }
1239
- const format = typeof entry.format === 'string' && entry.format.trim().length > 0 ? entry.format.trim().toUpperCase() : undefined;
1240
1447
  const fileEntries = entry.files && typeof entry.files === 'object' && !Array.isArray(entry.files)
1241
1448
  ? entry.files
1242
1449
  : null;
1243
- const generatedModel3DFormat = inferGeneratedModel3DFormat(category, subcategory, format, fileEntries);
1450
+ const generatedModel3DFormat = !isCustomAsset && category && subcategory
1451
+ ? inferGeneratedModel3DFormat(category, subcategory, format, fileEntries)
1452
+ : null;
1244
1453
  const generatedModel3DRoles = generatedModel3DFormat ? getGeneratedModel3DFileRoles(generatedModel3DFormat) : null;
1245
1454
  if (!fileEntries || Object.keys(fileEntries).length === 0) {
1246
1455
  errors.push(`[${label}] Asset "${name}" must define files.`);
@@ -1275,7 +1484,7 @@ function buildAssetTasks(catalogues) {
1275
1484
  if (hasError) {
1276
1485
  continue;
1277
1486
  }
1278
- if (generatedModel3DRoles) {
1487
+ if (!isCustomAsset && generatedModel3DRoles) {
1279
1488
  const normalizedRoles = new Set(Object.keys(files).map((role) => normalizeFileRole(role)));
1280
1489
  const missingRoles = generatedModel3DRoles.required.filter((role) => !normalizedRoles.has(role));
1281
1490
  if (missingRoles.length > 0) {
@@ -1283,7 +1492,7 @@ function buildAssetTasks(catalogues) {
1283
1492
  continue;
1284
1493
  }
1285
1494
  }
1286
- if (category === 'SPRITESHEET') {
1495
+ if (!isCustomAsset && category === 'SPRITESHEET') {
1287
1496
  const hasAtlas = Object.prototype.hasOwnProperty.call(files, 'atlas');
1288
1497
  const hasManifest = Object.prototype.hasOwnProperty.call(files, 'manifest');
1289
1498
  if (!hasAtlas || !hasManifest) {
@@ -1291,15 +1500,20 @@ function buildAssetTasks(catalogues) {
1291
1500
  continue;
1292
1501
  }
1293
1502
  }
1294
- if (category === 'AUDIO') {
1503
+ if (!isCustomAsset && category === 'AUDIO') {
1295
1504
  const hasPrimary = Object.prototype.hasOwnProperty.call(files, 'primary');
1296
1505
  if (!hasPrimary) {
1297
1506
  errors.push(`[${label}] Asset "${name}" with category=AUDIO must define files.primary.`);
1298
1507
  continue;
1299
1508
  }
1300
1509
  }
1510
+ const errorCountBeforeTagMetadata = errors.length;
1301
1511
  const remix = normalizeCatalogueRemixRef(entry.remix, 'asset', errors, `[${label}] Asset "${name}"`);
1512
+ const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] Asset "${name}"`);
1302
1513
  const relations = normalizeCatalogueRelations(entry.relations, errors, `[${label}] Asset "${name}"`);
1514
+ if (errors.length > errorCountBeforeTagMetadata) {
1515
+ continue;
1516
+ }
1303
1517
  const username = typeof entry.username === 'string' && entry.username.trim().length > 0
1304
1518
  ? entry.username.trim()
1305
1519
  : undefined;
@@ -1313,10 +1527,12 @@ function buildAssetTasks(catalogues) {
1313
1527
  cataloguePath: label,
1314
1528
  category,
1315
1529
  subcategory,
1530
+ assetSpec,
1316
1531
  username,
1317
1532
  remix,
1318
1533
  format,
1319
1534
  visibility: normalizeAssetVisibilityValue(entry.visibility, errors, `${label} asset "${name}"`),
1535
+ tags,
1320
1536
  shopListed: typeof entry.shopListed === 'boolean' ? entry.shopListed : undefined,
1321
1537
  shopPriceCredits: Number.isInteger(entry.shopPriceCredits) ? Number(entry.shopPriceCredits) : undefined,
1322
1538
  files,
@@ -1399,8 +1615,13 @@ function buildAssetPackTasks(catalogues) {
1399
1615
  errors.push(`[${label}] Asset pack "${name}" must reference at least one asset.`);
1400
1616
  continue;
1401
1617
  }
1618
+ const errorCountBeforeTagMetadata = errors.length;
1402
1619
  const remix = normalizeCatalogueRemixRef(entry.remix, 'pack', errors, `[${label}] Asset pack "${name}@${version}"`);
1620
+ const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] Asset pack "${name}@${version}"`);
1403
1621
  const relations = normalizeCatalogueRelations(entry.relations, errors, `[${label}] Asset pack "${name}@${version}"`);
1622
+ if (errors.length > errorCountBeforeTagMetadata) {
1623
+ continue;
1624
+ }
1404
1625
  const username = typeof entry.username === 'string' && entry.username.trim().length > 0
1405
1626
  ? entry.username.trim()
1406
1627
  : undefined;
@@ -1581,6 +1802,7 @@ function buildAssetPackTasks(catalogues) {
1581
1802
  downloadUrl,
1582
1803
  visibility,
1583
1804
  releaseNotes: typeof entry.releaseNotes === 'string' ? entry.releaseNotes : undefined,
1805
+ tags,
1584
1806
  listing,
1585
1807
  relations,
1586
1808
  });
@@ -1588,11 +1810,112 @@ function buildAssetPackTasks(catalogues) {
1588
1810
  }
1589
1811
  return { tasks, warnings, errors };
1590
1812
  }
1813
+ // eslint-disable-next-line complexity
1814
+ function resolveCatalogueTagGroups(rootDir) {
1815
+ const { files, errors: discoveryErrors } = walkCatalogueTree(rootDir);
1816
+ if (discoveryErrors.length > 0) {
1817
+ return {
1818
+ groups: [],
1819
+ errors: discoveryErrors,
1820
+ };
1821
+ }
1822
+ const rootCatalogue = files.find((file) => file.relativePath === 'catalogue.json');
1823
+ const hasNestedCatalogues = files.some((file) => file.relativePath !== 'catalogue.json');
1824
+ if (rootCatalogue && hasNestedCatalogues && !isCatalogueMarker(rootCatalogue)) {
1825
+ return {
1826
+ groups: [],
1827
+ errors: [
1828
+ '[catalogue.json] Workspace root catalogue.json must be {} when nested catalogue.json files exist below it.',
1829
+ ],
1830
+ };
1831
+ }
1832
+ const groups = [];
1833
+ const errors = [];
1834
+ const seenGroupSlugs = new Set();
1835
+ for (const file of files) {
1836
+ const label = file.relativePath || 'catalogue.json';
1837
+ const rawGroups = Array.isArray(file.data.tagGroups) ? file.data.tagGroups : [];
1838
+ for (let index = 0; index < rawGroups.length; index += 1) {
1839
+ const entry = rawGroups[index];
1840
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
1841
+ errors.push(`[${label}] tagGroups[${index}] must be an object.`);
1842
+ continue;
1843
+ }
1844
+ const slug = typeof entry.slug === 'string' ? entry.slug.trim().toLowerCase() : '';
1845
+ if (!slug || !(0, types_1.isValidTagSlug)(slug)) {
1846
+ errors.push(`[${label}] tagGroups[${index}] slug must use lowercase slug format.`);
1847
+ continue;
1848
+ }
1849
+ if (seenGroupSlugs.has(slug)) {
1850
+ errors.push(`[${label}] Duplicate tag group slug "${slug}".`);
1851
+ continue;
1852
+ }
1853
+ const displayName = typeof entry.displayName === 'string' ? entry.displayName.trim() : '';
1854
+ if (!displayName) {
1855
+ errors.push(`[${label}] tagGroups[${index}] displayName must be a non-empty string.`);
1856
+ continue;
1857
+ }
1858
+ const errorCountBeforeGroup = errors.length;
1859
+ const allowedTargetKinds = normalizeCatalogueAllowedTargetKinds(entry.allowedTargetKinds, errors, `[${label}] Tag group "${slug}"`);
1860
+ if (!Array.isArray(entry.tags)) {
1861
+ errors.push(`[${label}] Tag group "${slug}" must define a tags array.`);
1862
+ continue;
1863
+ }
1864
+ const tags = [];
1865
+ const seenTagSlugs = new Set();
1866
+ for (let tagIndex = 0; tagIndex < entry.tags.length; tagIndex += 1) {
1867
+ const rawTag = entry.tags[tagIndex];
1868
+ if (!rawTag || typeof rawTag !== 'object' || Array.isArray(rawTag)) {
1869
+ errors.push(`[${label}] Tag group "${slug}" tags[${tagIndex}] must be an object.`);
1870
+ continue;
1871
+ }
1872
+ const tagSlug = typeof rawTag.slug === 'string' ? rawTag.slug.trim().toLowerCase() : '';
1873
+ if (!tagSlug || !(0, types_1.isValidTagSlug)(tagSlug)) {
1874
+ errors.push(`[${label}] Tag group "${slug}" tags[${tagIndex}] slug must use lowercase slug format.`);
1875
+ continue;
1876
+ }
1877
+ if (seenTagSlugs.has(tagSlug)) {
1878
+ errors.push(`[${label}] Tag group "${slug}" contains duplicate tag slug "${tagSlug}".`);
1879
+ continue;
1880
+ }
1881
+ const tagDisplayName = typeof rawTag.displayName === 'string' ? rawTag.displayName.trim() : '';
1882
+ if (!tagDisplayName) {
1883
+ errors.push(`[${label}] Tag group "${slug}" tag "${tagSlug}" displayName must be a non-empty string.`);
1884
+ continue;
1885
+ }
1886
+ seenTagSlugs.add(tagSlug);
1887
+ tags.push({
1888
+ slug: tagSlug,
1889
+ displayName: tagDisplayName,
1890
+ });
1891
+ }
1892
+ if (tags.length === 0) {
1893
+ errors.push(`[${label}] Tag group "${slug}" must define at least one tag.`);
1894
+ continue;
1895
+ }
1896
+ if (errors.length > errorCountBeforeGroup) {
1897
+ continue;
1898
+ }
1899
+ seenGroupSlugs.add(slug);
1900
+ groups.push({
1901
+ slug,
1902
+ displayName,
1903
+ ...(allowedTargetKinds ? { allowedTargetKinds } : {}),
1904
+ tags,
1905
+ });
1906
+ }
1907
+ }
1908
+ return {
1909
+ groups,
1910
+ errors,
1911
+ };
1912
+ }
1591
1913
  function resolveCatalogueEntries(rootDir, options = {}) {
1592
1914
  const { files, errors: discoveryErrors } = walkCatalogueTree(rootDir);
1593
1915
  if (discoveryErrors.length > 0) {
1594
1916
  return {
1595
1917
  apps: [],
1918
+ assetSpecs: [],
1596
1919
  assets: [],
1597
1920
  embeddedAssets: [],
1598
1921
  assetPacks: [],
@@ -1605,6 +1928,7 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1605
1928
  if (rootCatalogue && hasNestedCatalogues && !isCatalogueMarker(rootCatalogue)) {
1606
1929
  return {
1607
1930
  apps: [],
1931
+ assetSpecs: [],
1608
1932
  assets: [],
1609
1933
  embeddedAssets: [],
1610
1934
  assetPacks: [],
@@ -1615,13 +1939,15 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1615
1939
  };
1616
1940
  }
1617
1941
  const { tasks: appTasks, warnings: appWarnings, errors: appErrors } = buildAppTasks(rootDir, files, options);
1618
- const { tasks: assetTasks, warnings: assetWarnings, errors: assetErrors } = buildAssetTasks(files);
1942
+ const { tasks: assetSpecTasks, warnings: assetSpecWarnings, errors: assetSpecErrors } = buildAssetSpecTasks(files);
1943
+ const { tasks: assetTasks, warnings: assetWarnings, errors: assetErrors } = buildAssetTasks(files, assetSpecTasks);
1619
1944
  const { tasks: assetPackTasks, warnings: assetPackWarnings, errors: assetPackErrors } = buildAssetPackTasks(files);
1620
1945
  const embeddedAssetTasks = appTasks.flatMap((task) => task.embeddedAssets);
1621
- const warnings = [...appWarnings, ...assetWarnings, ...assetPackWarnings];
1622
- const errors = [...appErrors, ...assetErrors, ...assetPackErrors];
1946
+ const warnings = [...appWarnings, ...assetSpecWarnings, ...assetWarnings, ...assetPackWarnings];
1947
+ const errors = [...appErrors, ...assetSpecErrors, ...assetErrors, ...assetPackErrors];
1623
1948
  return {
1624
1949
  apps: appTasks,
1950
+ assetSpecs: assetSpecTasks,
1625
1951
  assets: assetTasks,
1626
1952
  embeddedAssets: embeddedAssetTasks,
1627
1953
  assetPacks: assetPackTasks,
@@ -9,7 +9,23 @@ exports.getCliVersion = getCliVersion;
9
9
  exports.getCliBuild = getCliBuild;
10
10
  exports.getRuntimeInfo = getRuntimeInfo;
11
11
  const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const fs_1 = require("fs");
12
14
  const config_1 = require("@playdrop/config");
15
+ let cachedCliPackageVersion = null;
16
+ function readCliPackageVersion() {
17
+ if (cachedCliPackageVersion) {
18
+ return cachedCliPackageVersion;
19
+ }
20
+ const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
21
+ const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf8'));
22
+ const version = typeof packageJson.version === 'string' ? packageJson.version.trim() : '';
23
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
24
+ throw new Error(`invalid_cli_package_version:${version}`);
25
+ }
26
+ cachedCliPackageVersion = version;
27
+ return version;
28
+ }
13
29
  function mapPlatform(platform) {
14
30
  switch (platform) {
15
31
  case 'aix':
@@ -42,7 +58,7 @@ function detectPlatformVersion() {
42
58
  function buildRuntimeInfo() {
43
59
  return {
44
60
  client: 'cli',
45
- clientVersion: (0, config_1.getClientVersion)(),
61
+ clientVersion: readCliPackageVersion(),
46
62
  clientBuild: (0, config_1.getClientBuild)(),
47
63
  platform: mapPlatform(os_1.default.platform()),
48
64
  platformVersion: detectPlatformVersion()
@@ -52,10 +68,10 @@ function createClientHeaders() {
52
68
  return (0, config_1.createClientHeaderRecord)(buildRuntimeInfo());
53
69
  }
54
70
  function getCliVersionLabel() {
55
- return (0, config_1.getClientVersionLabel)();
71
+ return `${readCliPackageVersion()} (build ${(0, config_1.getClientBuild)()})`;
56
72
  }
57
73
  function getCliVersion() {
58
- return (0, config_1.getClientVersion)();
74
+ return readCliPackageVersion();
59
75
  }
60
76
  function getCliBuild() {
61
77
  return (0, config_1.getClientBuild)();
@@ -0,0 +1,8 @@
1
+ type AdsCommandOptions = {
2
+ app?: string;
3
+ days?: string | number;
4
+ json?: boolean;
5
+ };
6
+ export declare function showAdsEarnings(options?: AdsCommandOptions): Promise<void>;
7
+ export declare function showAdsReport(options?: AdsCommandOptions): Promise<void>;
8
+ export {};