@playdrop/playdrop-cli 0.14.5 → 0.14.7

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 (64) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/staticHtml.js +1 -0
  3. package/dist/apps/upload.js +2 -0
  4. package/dist/catalogue.d.ts +18 -5
  5. package/dist/catalogue.js +132 -158
  6. package/dist/commands/dev.js +9 -0
  7. package/dist/commands/devRuntimeAssets.d.ts +2 -1
  8. package/dist/commands/devRuntimeAssets.js +3 -0
  9. package/dist/commands/notes.d.ts +7 -0
  10. package/dist/commands/notes.js +107 -0
  11. package/dist/commands/tweaks.d.ts +6 -0
  12. package/dist/commands/tweaks.js +93 -0
  13. package/dist/commands/validate.js +14 -0
  14. package/dist/index.js +39 -0
  15. package/dist/listingPreflight.d.ts +4 -0
  16. package/dist/listingPreflight.js +17 -0
  17. package/dist/tweakStaleness.d.ts +7 -0
  18. package/dist/tweakStaleness.js +36 -0
  19. package/node_modules/@playdrop/api-client/dist/client.d.ts +25 -4
  20. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  21. package/node_modules/@playdrop/api-client/dist/client.js +40 -0
  22. package/node_modules/@playdrop/api-client/dist/core/request.d.ts +1 -0
  23. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  24. package/node_modules/@playdrop/api-client/dist/core/request.js +2 -1
  25. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
  26. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/admin.js +27 -0
  28. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +18 -4
  29. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  30. package/node_modules/@playdrop/api-client/dist/domains/apps.js +76 -6
  31. package/node_modules/@playdrop/api-client/dist/index.d.ts +35 -4
  32. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/index.js +79 -4
  34. package/node_modules/@playdrop/config/client-meta.json +2 -2
  35. package/node_modules/@playdrop/config/dist/src/game-arena.d.ts +14 -0
  36. package/node_modules/@playdrop/config/dist/src/game-arena.d.ts.map +1 -0
  37. package/node_modules/@playdrop/config/dist/src/game-arena.js +30 -0
  38. package/node_modules/@playdrop/config/dist/src/index.d.ts +1 -0
  39. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  40. package/node_modules/@playdrop/config/dist/src/index.js +1 -0
  41. package/node_modules/@playdrop/config/dist/test/game-arena.test.d.ts +2 -0
  42. package/node_modules/@playdrop/config/dist/test/game-arena.test.d.ts.map +1 -0
  43. package/node_modules/@playdrop/config/dist/test/game-arena.test.js +17 -0
  44. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  45. package/node_modules/@playdrop/config/package.json +4 -0
  46. package/node_modules/@playdrop/types/dist/api.d.ts +104 -3
  47. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  48. package/node_modules/@playdrop/types/dist/api.js +39 -2
  49. package/node_modules/@playdrop/types/dist/app-review.d.ts +11 -0
  50. package/node_modules/@playdrop/types/dist/app-review.d.ts.map +1 -0
  51. package/node_modules/@playdrop/types/dist/app-review.js +56 -0
  52. package/node_modules/@playdrop/types/dist/index.d.ts +3 -0
  53. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  54. package/node_modules/@playdrop/types/dist/index.js +6 -0
  55. package/node_modules/@playdrop/types/dist/playtest-notes.d.ts +69 -0
  56. package/node_modules/@playdrop/types/dist/playtest-notes.d.ts.map +1 -0
  57. package/node_modules/@playdrop/types/dist/playtest-notes.js +30 -0
  58. package/node_modules/@playdrop/types/dist/tweaks.d.ts +48 -0
  59. package/node_modules/@playdrop/types/dist/tweaks.d.ts.map +1 -0
  60. package/node_modules/@playdrop/types/dist/tweaks.js +156 -0
  61. package/node_modules/@playdrop/types/dist/version.d.ts +37 -0
  62. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  63. package/node_modules/@playdrop/types/dist/version.js +6 -0
  64. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.14.5",
2
+ "version": "0.14.7",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.14.5",
4
+ "runtimeSdkVersion": "0.14.7",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -223,6 +223,7 @@ function buildListing(imagePath, primarySurface) {
223
223
  screenshotLandscapePaths: [],
224
224
  videoPortraitPaths: [],
225
225
  videoLandscapePaths: [],
226
+ mediaMetadata: [],
226
227
  };
227
228
  }
228
229
  function buildStaticAppTask(input) {
@@ -245,6 +245,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
245
245
  usesPacks: task.uses.packs,
246
246
  tags: task.tags,
247
247
  design: task.design,
248
+ tweaks: task.tweaks,
248
249
  clearTags: options?.clearTags,
249
250
  surfaceTargets: task.surfaceTargets,
250
251
  primarySurface: task.primarySurface,
@@ -285,6 +286,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
285
286
  status: definition.status,
286
287
  })),
287
288
  externalUrl: task.hostingMode === 'EXTERNAL' ? task.externalUrl : undefined,
289
+ listingMedia: listing?.mediaMetadata,
288
290
  files: preparedSessionFiles.map((file) => ({
289
291
  fileKey: file.fileKey,
290
292
  filename: file.filename,
@@ -1,4 +1,4 @@
1
- import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppMetadataAssetSpecSupport, type AppPlaytestTapes, type AppSurface, type AppType, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from '@playdrop/types';
1
+ import { type AppAchievementCatalogueDefinition, type AppAuthMode, type AppControllerMode, type AppHostingMode, type AppLeaderboardCatalogueDefinition, type AppListingMediaMetadata, type AppMetadataAssetSpecSupport, type AppPlaytestTapes, type AppSurface, type AppType, type AppTweaksDeclaration, type AppVersionDesign, type AppVersionVisibility, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type CatalogueTagGroupDefinition, type ContentLicense, type PlayerMetaDefinitionStatus } from '@playdrop/types';
2
2
  import { type AssetSpecSupportDeclaration } from './assetSpecs';
3
3
  export type CatalogueJson = {
4
4
  apps?: Array<Record<string, unknown>>;
@@ -7,6 +7,8 @@ export type CatalogueJson = {
7
7
  assets?: Array<Record<string, unknown>>;
8
8
  assetPacks?: Array<Record<string, unknown>>;
9
9
  } & Record<string, unknown>;
10
+ export declare const APP_DESCRIPTION_RECOMMENDED_MIN_CHARACTERS = 120;
11
+ export declare const APP_DESCRIPTION_RECOMMENDED_MIN_WORDS = 20;
10
12
  /**
11
13
  * Listing assets configuration for an app version.
12
14
  * All paths are relative to the catalogue.json directory.
@@ -15,12 +17,20 @@ export type AppListingConfig = {
15
17
  icon?: string;
16
18
  heroPortrait?: string;
17
19
  heroLandscape?: string;
18
- screenshotsPortrait?: string[];
19
- screenshotsLandscape?: string[];
20
- videosPortrait?: string[];
21
- videosLandscape?: string[];
20
+ screenshotsPortrait?: AppListingMediaConfig[];
21
+ screenshotsLandscape?: AppListingMediaConfig[];
22
+ videosPortrait?: AppListingMediaConfig[];
23
+ videosLandscape?: AppListingMediaConfig[];
22
24
  captureReport?: string;
23
25
  };
26
+ export type AppListingMediaConfig = string | {
27
+ path: string;
28
+ slug?: string;
29
+ title?: string;
30
+ alt?: string;
31
+ caption?: string;
32
+ description?: string;
33
+ };
24
34
  export type ResolvedAppAchievementDefinition = Omit<AppAchievementCatalogueDefinition, 'status'> & {
25
35
  iconPath: string;
26
36
  status: PlayerMetaDefinitionStatus;
@@ -44,6 +54,7 @@ export type AppCatalogueEntry = {
44
54
  primarySurface?: string | null;
45
55
  playtestTapes?: unknown;
46
56
  design?: unknown;
57
+ tweaks?: unknown;
47
58
  version?: string;
48
59
  releaseNotes?: string;
49
60
  visibility?: string;
@@ -156,6 +167,7 @@ export type ResolvedListingAssets = {
156
167
  screenshotLandscapePaths: string[];
157
168
  videoPortraitPaths: string[];
158
169
  videoLandscapePaths: string[];
170
+ mediaMetadata: AppListingMediaMetadata[];
159
171
  captureReportPath?: string;
160
172
  };
161
173
  export type AppDependencyAssetRef = {
@@ -185,6 +197,7 @@ export type AppTask = {
185
197
  primarySurface?: AppSurface;
186
198
  playtestTapes?: AppPlaytestTapes;
187
199
  design?: AppVersionDesign;
200
+ tweaks?: AppTweaksDeclaration;
188
201
  missingMetadata: string[];
189
202
  version?: string;
190
203
  releaseNotes?: string;
package/dist/catalogue.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.APP_DESCRIPTION_RECOMMENDED_MIN_WORDS = exports.APP_DESCRIPTION_RECOMMENDED_MIN_CHARACTERS = void 0;
3
4
  exports.validateAppMetadata = validateAppMetadata;
4
5
  exports.resolveCatalogueTagGroups = resolveCatalogueTagGroups;
5
6
  exports.resolveCatalogueEntries = resolveCatalogueEntries;
@@ -18,6 +19,8 @@ const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
18
19
  const ASSET_CATEGORY_SET = new Set(types_1.ASSET_CATEGORY_VALUES);
19
20
  const LISTING_IMAGE_EXTENSIONS = new Set(['.png']);
20
21
  const LISTING_VIDEO_EXTENSIONS = new Set(['.mp4']);
22
+ exports.APP_DESCRIPTION_RECOMMENDED_MIN_CHARACTERS = 120;
23
+ exports.APP_DESCRIPTION_RECOMMENDED_MIN_WORDS = 20;
21
24
  const BOXEL_GENERATED_FILE_ROLES = new Set(['mesh', 'preview']);
22
25
  const BOXEL_REQUIRED_FILE_ROLES = ['primary', 'mesh', 'preview'];
23
26
  const VOX_GENERATED_FILE_ROLES = new Set(['boxel', 'mesh', 'preview']);
@@ -177,6 +180,74 @@ function validateListingAssetExtension(relativePath, fieldName, kind) {
177
180
  function resolveListingAssetPath(baseDir, relativePath) {
178
181
  return (0, node_path_1.resolve)(baseDir, relativePath);
179
182
  }
183
+ const LISTING_MEDIA_SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
184
+ const LISTING_MEDIA_TEXT_LIMITS = {
185
+ title: 120,
186
+ alt: 200,
187
+ caption: 300,
188
+ description: 300,
189
+ };
190
+ function resolveListingMediaEntries(input) {
191
+ if (input.value === undefined) {
192
+ return { paths: [], metadata: [] };
193
+ }
194
+ if (!Array.isArray(input.value)) {
195
+ return { paths: [], metadata: [], error: `${input.fieldName} must be an array` };
196
+ }
197
+ const paths = [];
198
+ const metadata = [];
199
+ for (let index = 0; index < input.value.length; index += 1) {
200
+ const value = input.value[index];
201
+ const fieldName = `${input.fieldName}[${index}]`;
202
+ const objectValue = value && typeof value === 'object' && !Array.isArray(value)
203
+ ? value
204
+ : null;
205
+ const relativePath = typeof value === 'string'
206
+ ? value.trim()
207
+ : typeof objectValue?.path === 'string'
208
+ ? objectValue.path.trim()
209
+ : '';
210
+ if (!relativePath) {
211
+ return { paths, metadata, error: `${fieldName} must be a path string or an object with a non-empty path` };
212
+ }
213
+ const extensionError = validateListingAssetExtension(relativePath, fieldName, input.kind);
214
+ if (extensionError) {
215
+ return { paths, metadata, error: extensionError };
216
+ }
217
+ const pathError = validateListingAssetPath(input.baseDir, relativePath, fieldName);
218
+ if (pathError) {
219
+ return { paths, metadata, error: pathError };
220
+ }
221
+ const item = {
222
+ fileKey: `${input.fileKeyPrefix}:${index}`,
223
+ };
224
+ if (objectValue) {
225
+ for (const key of ['title', 'alt', 'caption', 'description']) {
226
+ const raw = objectValue[key];
227
+ if (raw === undefined) {
228
+ continue;
229
+ }
230
+ if (typeof raw !== 'string' || raw.trim().length === 0) {
231
+ return { paths, metadata, error: `${fieldName}.${key} must be a non-empty string when provided` };
232
+ }
233
+ const normalized = raw.trim();
234
+ if (normalized.length > LISTING_MEDIA_TEXT_LIMITS[key]) {
235
+ return { paths, metadata, error: `${fieldName}.${key} must be ${LISTING_MEDIA_TEXT_LIMITS[key]} characters or fewer` };
236
+ }
237
+ item[key] = normalized;
238
+ }
239
+ if (objectValue.slug !== undefined) {
240
+ if (typeof objectValue.slug !== 'string' || !LISTING_MEDIA_SLUG_REGEX.test(objectValue.slug.trim())) {
241
+ return { paths, metadata, error: `${fieldName}.slug must use lowercase words separated by hyphens` };
242
+ }
243
+ item.slug = objectValue.slug.trim();
244
+ }
245
+ }
246
+ paths.push(resolveListingAssetPath(input.baseDir, relativePath));
247
+ metadata.push(item);
248
+ }
249
+ return { paths, metadata };
250
+ }
180
251
  function normalizeCatalogueRemixRef(value, expectedKind, errors, context) {
181
252
  if (value === undefined || value === null) {
182
253
  return undefined;
@@ -869,6 +940,13 @@ function validateAppMetadata(entry) {
869
940
  let description;
870
941
  if (typeof entry.description === 'string') {
871
942
  description = entry.description;
943
+ const normalizedDescription = description.trim();
944
+ const wordCount = normalizedDescription ? normalizedDescription.split(/\s+/).length : 0;
945
+ if (normalizedDescription
946
+ && (normalizedDescription.length < exports.APP_DESCRIPTION_RECOMMENDED_MIN_CHARACTERS
947
+ || wordCount < exports.APP_DESCRIPTION_RECOMMENDED_MIN_WORDS)) {
948
+ warnings.push(`description is ${normalizedDescription.length} characters and ${wordCount} words. Aim for at least ${exports.APP_DESCRIPTION_RECOMMENDED_MIN_CHARACTERS} characters and ${exports.APP_DESCRIPTION_RECOMMENDED_MIN_WORDS} words describing what the player does, the objective, and the game's distinctive hook.`);
949
+ }
872
950
  }
873
951
  let surfaceTargetsList = [];
874
952
  let surfaceTargetsMap = { desktop: false, mobileLandscape: false, mobilePortrait: false };
@@ -1169,6 +1247,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1169
1247
  screenshotLandscapePaths: [],
1170
1248
  videoPortraitPaths: [],
1171
1249
  videoLandscapePaths: [],
1250
+ mediaMetadata: [],
1172
1251
  };
1173
1252
  // Icon
1174
1253
  if (typeof rawListing.icon === 'string' && rawListing.icon.trim()) {
@@ -1236,87 +1315,28 @@ function buildAppTasks(rootDir, catalogues, options) {
1236
1315
  }
1237
1316
  }
1238
1317
  }
1239
- // Screenshots
1240
- if (!listingValidationFailed && Array.isArray(rawListing.screenshotsPortrait)) {
1241
- for (const p of rawListing.screenshotsPortrait) {
1242
- if (typeof p === 'string' && p.trim()) {
1243
- const screenshotPath = p.trim();
1244
- const extensionError = validateListingAssetExtension(screenshotPath, 'listing.screenshotsPortrait item', 'image');
1245
- if (extensionError) {
1246
- errors.push(`[${label}] Skipping ${rawName}: ${extensionError}`);
1247
- listingValidationFailed = true;
1248
- break;
1249
- }
1250
- const validationError = validateListingAssetPath(file.directory, screenshotPath, 'listing.screenshotsPortrait item');
1251
- if (validationError) {
1252
- errors.push(`[${label}] Skipping ${rawName}: ${validationError}`);
1253
- listingValidationFailed = true;
1254
- break;
1255
- }
1256
- resolvedListing.screenshotPortraitPaths.push(resolveListingAssetPath(file.directory, screenshotPath));
1257
- }
1258
- }
1259
- }
1260
- if (!listingValidationFailed && Array.isArray(rawListing.screenshotsLandscape)) {
1261
- for (const p of rawListing.screenshotsLandscape) {
1262
- if (typeof p === 'string' && p.trim()) {
1263
- const screenshotPath = p.trim();
1264
- const extensionError = validateListingAssetExtension(screenshotPath, 'listing.screenshotsLandscape item', 'image');
1265
- if (extensionError) {
1266
- errors.push(`[${label}] Skipping ${rawName}: ${extensionError}`);
1267
- listingValidationFailed = true;
1268
- break;
1269
- }
1270
- const validationError = validateListingAssetPath(file.directory, screenshotPath, 'listing.screenshotsLandscape item');
1271
- if (validationError) {
1272
- errors.push(`[${label}] Skipping ${rawName}: ${validationError}`);
1273
- listingValidationFailed = true;
1274
- break;
1275
- }
1276
- resolvedListing.screenshotLandscapePaths.push(resolveListingAssetPath(file.directory, screenshotPath));
1277
- }
1278
- }
1279
- }
1280
- // Videos
1281
- if (!listingValidationFailed && Array.isArray(rawListing.videosPortrait)) {
1282
- for (const p of rawListing.videosPortrait) {
1283
- if (typeof p === 'string' && p.trim()) {
1284
- const videoPath = p.trim();
1285
- const extensionError = validateListingAssetExtension(videoPath, 'listing.videosPortrait item', 'video');
1286
- if (extensionError) {
1287
- errors.push(`[${label}] Skipping ${rawName}: ${extensionError}`);
1288
- listingValidationFailed = true;
1289
- break;
1290
- }
1291
- const validationError = validateListingAssetPath(file.directory, videoPath, 'listing.videosPortrait item');
1292
- if (validationError) {
1293
- errors.push(`[${label}] Skipping ${rawName}: ${validationError}`);
1294
- listingValidationFailed = true;
1295
- break;
1296
- }
1297
- resolvedListing.videoPortraitPaths.push(resolveListingAssetPath(file.directory, videoPath));
1298
- }
1299
- }
1300
- }
1301
- if (!listingValidationFailed && Array.isArray(rawListing.videosLandscape)) {
1302
- for (const p of rawListing.videosLandscape) {
1303
- if (typeof p === 'string' && p.trim()) {
1304
- const videoPath = p.trim();
1305
- const extensionError = validateListingAssetExtension(videoPath, 'listing.videosLandscape item', 'video');
1306
- if (extensionError) {
1307
- errors.push(`[${label}] Skipping ${rawName}: ${extensionError}`);
1308
- listingValidationFailed = true;
1309
- break;
1310
- }
1311
- const validationError = validateListingAssetPath(file.directory, videoPath, 'listing.videosLandscape item');
1312
- if (validationError) {
1313
- errors.push(`[${label}] Skipping ${rawName}: ${validationError}`);
1314
- listingValidationFailed = true;
1315
- break;
1316
- }
1317
- resolvedListing.videoLandscapePaths.push(resolveListingAssetPath(file.directory, videoPath));
1318
- }
1318
+ for (const mediaField of [
1319
+ { value: rawListing.screenshotsPortrait, fieldName: 'listing.screenshotsPortrait', kind: 'image', fileKeyPrefix: 'screenshotsPortrait', target: resolvedListing.screenshotPortraitPaths },
1320
+ { value: rawListing.screenshotsLandscape, fieldName: 'listing.screenshotsLandscape', kind: 'image', fileKeyPrefix: 'screenshotsLandscape', target: resolvedListing.screenshotLandscapePaths },
1321
+ { value: rawListing.videosPortrait, fieldName: 'listing.videosPortrait', kind: 'video', fileKeyPrefix: 'videosPortrait', target: resolvedListing.videoPortraitPaths },
1322
+ { value: rawListing.videosLandscape, fieldName: 'listing.videosLandscape', kind: 'video', fileKeyPrefix: 'videosLandscape', target: resolvedListing.videoLandscapePaths },
1323
+ ]) {
1324
+ if (listingValidationFailed)
1325
+ break;
1326
+ const resolvedMedia = resolveListingMediaEntries({
1327
+ value: mediaField.value,
1328
+ baseDir: file.directory,
1329
+ fieldName: mediaField.fieldName,
1330
+ kind: mediaField.kind,
1331
+ fileKeyPrefix: mediaField.fileKeyPrefix,
1332
+ });
1333
+ if (resolvedMedia.error) {
1334
+ errors.push(`[${label}] Skipping ${rawName}: ${resolvedMedia.error}`);
1335
+ listingValidationFailed = true;
1336
+ break;
1319
1337
  }
1338
+ mediaField.target.push(...resolvedMedia.paths);
1339
+ resolvedListing.mediaMetadata.push(...resolvedMedia.metadata);
1320
1340
  }
1321
1341
  if (!listingValidationFailed && typeof rawListing.captureReport === 'string' && rawListing.captureReport.trim()) {
1322
1342
  const captureReportPath = rawListing.captureReport.trim();
@@ -1355,6 +1375,15 @@ function buildAppTasks(rootDir, catalogues, options) {
1355
1375
  }
1356
1376
  design = parsedDesign.value;
1357
1377
  }
1378
+ let tweaks;
1379
+ if (Object.prototype.hasOwnProperty.call(entry, 'tweaks')) {
1380
+ const parsedTweaks = (0, types_1.normalizeAppTweaks)(entry.tweaks);
1381
+ if (!parsedTweaks.ok) {
1382
+ errors.push(`[${label}] App "${rawName}" ${parsedTweaks.error}`);
1383
+ continue;
1384
+ }
1385
+ tweaks = parsedTweaks.value;
1386
+ }
1358
1387
  const errorCountBeforeTagMetadata = errors.length;
1359
1388
  const remix = normalizeCatalogueRemixRef(entry.remix, 'app', errors, `[${label}] App "${rawName}"`);
1360
1389
  const tags = normalizeCatalogueTags(entry.tags, errors, `[${label}] App "${rawName}"`);
@@ -1559,6 +1588,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1559
1588
  primarySurface: metadata.primarySurface,
1560
1589
  playtestTapes: metadata.playtestTapes,
1561
1590
  design,
1591
+ tweaks,
1562
1592
  missingMetadata: metadata.missingFields,
1563
1593
  // Versioning fields
1564
1594
  version,
@@ -2117,6 +2147,7 @@ function buildAssetPackTasks(catalogues) {
2117
2147
  screenshotLandscapePaths: [],
2118
2148
  videoPortraitPaths: [],
2119
2149
  videoLandscapePaths: [],
2150
+ mediaMetadata: [],
2120
2151
  };
2121
2152
  if (typeof rawListing.icon === 'string' && rawListing.icon.trim()) {
2122
2153
  const iconPath = rawListing.icon.trim();
@@ -2172,85 +2203,28 @@ function buildAssetPackTasks(catalogues) {
2172
2203
  }
2173
2204
  }
2174
2205
  }
2175
- if (!listingValidationFailed && Array.isArray(rawListing.screenshotsPortrait)) {
2176
- for (const p of rawListing.screenshotsPortrait) {
2177
- if (typeof p === 'string' && p.trim()) {
2178
- const screenshotPath = p.trim();
2179
- const extensionError = validateListingAssetExtension(screenshotPath, 'assetPacks.listing.screenshotsPortrait item', 'image');
2180
- if (extensionError) {
2181
- errors.push(`[${label}] Skipping asset pack "${name}": ${extensionError}`);
2182
- listingValidationFailed = true;
2183
- break;
2184
- }
2185
- const validationError = validateListingAssetPath(file.directory, screenshotPath, 'assetPacks.listing.screenshotsPortrait item');
2186
- if (validationError) {
2187
- errors.push(`[${label}] Skipping asset pack "${name}": ${validationError}`);
2188
- listingValidationFailed = true;
2189
- break;
2190
- }
2191
- resolvedListing.screenshotPortraitPaths.push(resolveListingAssetPath(file.directory, screenshotPath));
2192
- }
2193
- }
2194
- }
2195
- if (!listingValidationFailed && Array.isArray(rawListing.screenshotsLandscape)) {
2196
- for (const p of rawListing.screenshotsLandscape) {
2197
- if (typeof p === 'string' && p.trim()) {
2198
- const screenshotPath = p.trim();
2199
- const extensionError = validateListingAssetExtension(screenshotPath, 'assetPacks.listing.screenshotsLandscape item', 'image');
2200
- if (extensionError) {
2201
- errors.push(`[${label}] Skipping asset pack "${name}": ${extensionError}`);
2202
- listingValidationFailed = true;
2203
- break;
2204
- }
2205
- const validationError = validateListingAssetPath(file.directory, screenshotPath, 'assetPacks.listing.screenshotsLandscape item');
2206
- if (validationError) {
2207
- errors.push(`[${label}] Skipping asset pack "${name}": ${validationError}`);
2208
- listingValidationFailed = true;
2209
- break;
2210
- }
2211
- resolvedListing.screenshotLandscapePaths.push(resolveListingAssetPath(file.directory, screenshotPath));
2212
- }
2213
- }
2214
- }
2215
- if (!listingValidationFailed && Array.isArray(rawListing.videosPortrait)) {
2216
- for (const p of rawListing.videosPortrait) {
2217
- if (typeof p === 'string' && p.trim()) {
2218
- const videoPath = p.trim();
2219
- const extensionError = validateListingAssetExtension(videoPath, 'assetPacks.listing.videosPortrait item', 'video');
2220
- if (extensionError) {
2221
- errors.push(`[${label}] Skipping asset pack "${name}": ${extensionError}`);
2222
- listingValidationFailed = true;
2223
- break;
2224
- }
2225
- const validationError = validateListingAssetPath(file.directory, videoPath, 'assetPacks.listing.videosPortrait item');
2226
- if (validationError) {
2227
- errors.push(`[${label}] Skipping asset pack "${name}": ${validationError}`);
2228
- listingValidationFailed = true;
2229
- break;
2230
- }
2231
- resolvedListing.videoPortraitPaths.push(resolveListingAssetPath(file.directory, videoPath));
2232
- }
2233
- }
2234
- }
2235
- if (!listingValidationFailed && Array.isArray(rawListing.videosLandscape)) {
2236
- for (const p of rawListing.videosLandscape) {
2237
- if (typeof p === 'string' && p.trim()) {
2238
- const videoPath = p.trim();
2239
- const extensionError = validateListingAssetExtension(videoPath, 'assetPacks.listing.videosLandscape item', 'video');
2240
- if (extensionError) {
2241
- errors.push(`[${label}] Skipping asset pack "${name}": ${extensionError}`);
2242
- listingValidationFailed = true;
2243
- break;
2244
- }
2245
- const validationError = validateListingAssetPath(file.directory, videoPath, 'assetPacks.listing.videosLandscape item');
2246
- if (validationError) {
2247
- errors.push(`[${label}] Skipping asset pack "${name}": ${validationError}`);
2248
- listingValidationFailed = true;
2249
- break;
2250
- }
2251
- resolvedListing.videoLandscapePaths.push(resolveListingAssetPath(file.directory, videoPath));
2252
- }
2206
+ for (const mediaField of [
2207
+ { value: rawListing.screenshotsPortrait, fieldName: 'assetPacks.listing.screenshotsPortrait', kind: 'image', fileKeyPrefix: 'screenshotsPortrait', target: resolvedListing.screenshotPortraitPaths },
2208
+ { value: rawListing.screenshotsLandscape, fieldName: 'assetPacks.listing.screenshotsLandscape', kind: 'image', fileKeyPrefix: 'screenshotsLandscape', target: resolvedListing.screenshotLandscapePaths },
2209
+ { value: rawListing.videosPortrait, fieldName: 'assetPacks.listing.videosPortrait', kind: 'video', fileKeyPrefix: 'videosPortrait', target: resolvedListing.videoPortraitPaths },
2210
+ { value: rawListing.videosLandscape, fieldName: 'assetPacks.listing.videosLandscape', kind: 'video', fileKeyPrefix: 'videosLandscape', target: resolvedListing.videoLandscapePaths },
2211
+ ]) {
2212
+ if (listingValidationFailed)
2213
+ break;
2214
+ const resolvedMedia = resolveListingMediaEntries({
2215
+ value: mediaField.value,
2216
+ baseDir: file.directory,
2217
+ fieldName: mediaField.fieldName,
2218
+ kind: mediaField.kind,
2219
+ fileKeyPrefix: mediaField.fileKeyPrefix,
2220
+ });
2221
+ if (resolvedMedia.error) {
2222
+ errors.push(`[${label}] Skipping asset pack "${name}": ${resolvedMedia.error}`);
2223
+ listingValidationFailed = true;
2224
+ break;
2253
2225
  }
2226
+ mediaField.target.push(...resolvedMedia.paths);
2227
+ resolvedListing.mediaMetadata.push(...resolvedMedia.metadata);
2254
2228
  }
2255
2229
  if (listingValidationFailed) {
2256
2230
  continue;
@@ -16,6 +16,7 @@ const http_1 = require("../http");
16
16
  const messages_1 = require("../messages");
17
17
  const devRuntimeAssets_1 = require("./devRuntimeAssets");
18
18
  const devServer_1 = require("./devServer");
19
+ const tweakStaleness_1 = require("../tweakStaleness");
19
20
  const devShared_1 = require("./devShared");
20
21
  function formatDevRuntimeAssetManifestFailure(error) {
21
22
  const rawMessage = error instanceof Error ? error.message : String(error ?? 'unknown_error');
@@ -301,6 +302,14 @@ async function dev(targetArg, appOption, devOptions = {}) {
301
302
  });
302
303
  let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
303
304
  if (taskLookup.task) {
305
+ try {
306
+ await (0, tweakStaleness_1.fetchAndAssertTweakState)(client, currentUsername, taskLookup.task);
307
+ }
308
+ catch (error) {
309
+ (0, messages_1.printErrorWithHelp)(error instanceof Error ? error.message : 'Could not verify the current tweak state.', [], { command: 'project dev' });
310
+ process.exitCode = 1;
311
+ return;
312
+ }
304
313
  try {
305
314
  runtimeAssetManifest = await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
306
315
  client,
@@ -1,5 +1,5 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
- import { type AppAuthMode, type AppControllerMode, type AppMetadataAssetSpecSupport, type AppRuntimeAssetsResponse, type AppSurface, type AppType } from '@playdrop/types';
2
+ import { type AppAuthMode, type AppControllerMode, type AppMetadataAssetSpecSupport, type AppRuntimeAssetsResponse, type AppSurface, type AppType, type TweakValues } from '@playdrop/types';
3
3
  import type { AppTask } from '../catalogue';
4
4
  export type LocalOwnedRuntimeAssetFileRecord = {
5
5
  absolutePath: string;
@@ -14,6 +14,7 @@ export type LocalDevAppMetadata = {
14
14
  previewable: boolean;
15
15
  assetSpecSupport: AppMetadataAssetSpecSupport[];
16
16
  version: string | null;
17
+ tweakValues?: TweakValues;
17
18
  };
18
19
  export type DevRuntimeAssetsResponse = AppRuntimeAssetsResponse & {
19
20
  localAppMetadata: LocalDevAppMetadata;
@@ -124,6 +124,9 @@ function buildLocalAppMetadata(task) {
124
124
  capabilities: Array.from(entry.capabilities),
125
125
  })),
126
126
  version: typeof task.version === 'string' && task.version.trim().length > 0 ? task.version.trim() : null,
127
+ ...(task.tweaks && task.tweaks.removed !== true
128
+ ? { tweakValues: { ...task.tweaks.defaults } }
129
+ : {}),
127
130
  };
128
131
  }
129
132
  function assertCustomAssetVersionSupported(task, assetRef, version) {
@@ -0,0 +1,7 @@
1
+ export declare function browseNotes(rawRef: string | undefined, options?: {
2
+ json?: boolean;
3
+ output?: string;
4
+ }): Promise<void>;
5
+ export declare function clearNotes(rawRef: string | undefined, rawIds: string[], options?: {
6
+ json?: boolean;
7
+ }): Promise<void>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.browseNotes = browseNotes;
4
+ exports.clearNotes = clearNotes;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const types_1 = require("@playdrop/types");
8
+ const commandContext_1 = require("../commandContext");
9
+ const errors_1 = require("../errors");
10
+ const messages_1 = require("../messages");
11
+ const output_1 = require("../output");
12
+ function notesApiMessage(error) {
13
+ if (error.code === 'playtest_notes_changed') {
14
+ const missingIds = Array.isArray(error.details?.['missingIds']) ? error.details['missingIds'] : [];
15
+ return {
16
+ problem: `The note inbox changed before it could be cleared${missingIds.length > 0 ? `: ${missingIds.join(', ')}` : '.'}`,
17
+ suggestions: ['Run notes browse again, then clear the current IDs.'],
18
+ };
19
+ }
20
+ return { problem: `PlayDrop rejected the notes request: ${error.code ?? error.message} (HTTP ${error.status}).` };
21
+ }
22
+ function parseAppVersionRef(raw, command) {
23
+ const ref = (0, types_1.parseContentVersionRef)(raw ?? '');
24
+ if (!ref || ref.kind !== 'app') {
25
+ (0, messages_1.printErrorWithHelp)('An exact canonical app-version ref is required.', ['Use app:creator/name@x.y.z.', `Example: playdrop ${command} app:olivier/platformer@1.4.0`], { command });
26
+ process.exitCode = 1;
27
+ return null;
28
+ }
29
+ return ref;
30
+ }
31
+ async function browseNotes(rawRef, options = {}) {
32
+ const ref = parseAppVersionRef(rawRef, 'notes browse');
33
+ if (!ref)
34
+ return;
35
+ await (0, commandContext_1.withEnvironment)('notes browse', 'Reading playtest notes', async ({ client }) => {
36
+ try {
37
+ const response = await client.listPlaytestNotes(ref.creatorUsername, ref.name, ref.version);
38
+ let output = response;
39
+ if (options.output) {
40
+ const outputDirectory = (0, node_path_1.resolve)(options.output);
41
+ (0, node_fs_1.mkdirSync)(outputDirectory, { recursive: true });
42
+ const notes = [];
43
+ for (const note of response.notes) {
44
+ if (!note.attachment) {
45
+ notes.push(note);
46
+ continue;
47
+ }
48
+ const localPath = (0, node_path_1.resolve)(outputDirectory, `${note.id}-${note.attachment.fileName}`);
49
+ const blob = await client.downloadPlaytestNoteAttachment(ref.creatorUsername, ref.name, ref.version, note.id);
50
+ (0, node_fs_1.writeFileSync)(localPath, Buffer.from(await blob.arrayBuffer()));
51
+ notes.push({ ...note, localPath });
52
+ }
53
+ output = { notes };
54
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(outputDirectory, 'notes.json'), `${JSON.stringify(output, null, 2)}\n`, 'utf8');
55
+ }
56
+ if (options.json) {
57
+ (0, output_1.printJson)(output);
58
+ return;
59
+ }
60
+ if (options.output) {
61
+ (0, output_1.printSuccess)(`Materialized ${output.notes.length} playtest notes in ${(0, node_path_1.resolve)(options.output)}.`);
62
+ return;
63
+ }
64
+ if (output.notes.length === 0) {
65
+ console.log('No pending playtest notes.');
66
+ return;
67
+ }
68
+ for (const note of output.notes) {
69
+ const summary = note.kind === 'TEXT'
70
+ ? note.text
71
+ : note.kind === 'ASSET'
72
+ ? `${note.assetRef}: ${note.comment}`
73
+ : note.attachment?.fileName;
74
+ console.log(`${note.id}\t${note.kind}\tv${note.authoredVersion}\t${summary ?? ''}`);
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (!(0, errors_1.handleCommandFailure)(error, 'notes browse', 'Read playtest notes', { apiMessage: notesApiMessage }))
79
+ throw error;
80
+ }
81
+ });
82
+ }
83
+ async function clearNotes(rawRef, rawIds, options = {}) {
84
+ const ref = parseAppVersionRef(rawRef, 'notes clear');
85
+ if (!ref)
86
+ return;
87
+ const ids = rawIds.map(Number);
88
+ if (ids.length === 0 || ids.some((id) => !Number.isInteger(id) || id <= 0) || new Set(ids).size !== ids.length) {
89
+ (0, messages_1.printErrorWithHelp)('One or more explicit note IDs are required.', ['Example: playdrop notes clear app:olivier/platformer@1.4.0 42 43'], { command: 'notes clear' });
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ await (0, commandContext_1.withEnvironment)('notes clear', 'Clearing playtest notes', async ({ client }) => {
94
+ try {
95
+ const result = await client.clearPlaytestNotes(ref.creatorUsername, ref.name, ref.version, ids);
96
+ if (options.json) {
97
+ (0, output_1.printJson)(result);
98
+ return;
99
+ }
100
+ (0, output_1.printSuccess)(`Cleared ${result.cleared.length} playtest notes. ${result.count} pending.`);
101
+ }
102
+ catch (error) {
103
+ if (!(0, errors_1.handleCommandFailure)(error, 'notes clear', 'Clear playtest notes', { apiMessage: notesApiMessage }))
104
+ throw error;
105
+ }
106
+ });
107
+ }
@@ -0,0 +1,6 @@
1
+ type TweaksOptions = {
2
+ json?: boolean;
3
+ };
4
+ export declare function getTweaks(rawRef: string | undefined, options?: TweaksOptions): Promise<void>;
5
+ export declare function replaceTweaks(rawRef: string | undefined, file: string | undefined, options?: TweaksOptions): Promise<void>;
6
+ export {};