@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.
- package/config/client-meta.json +2 -2
- package/dist/apps/staticHtml.js +1 -0
- package/dist/apps/upload.js +2 -0
- package/dist/catalogue.d.ts +18 -5
- package/dist/catalogue.js +132 -158
- package/dist/commands/dev.js +9 -0
- package/dist/commands/devRuntimeAssets.d.ts +2 -1
- package/dist/commands/devRuntimeAssets.js +3 -0
- package/dist/commands/notes.d.ts +7 -0
- package/dist/commands/notes.js +107 -0
- package/dist/commands/tweaks.d.ts +6 -0
- package/dist/commands/tweaks.js +93 -0
- package/dist/commands/validate.js +14 -0
- package/dist/index.js +39 -0
- package/dist/listingPreflight.d.ts +4 -0
- package/dist/listingPreflight.js +17 -0
- package/dist/tweakStaleness.d.ts +7 -0
- package/dist/tweakStaleness.js +36 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +25 -4
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/client.js +40 -0
- package/node_modules/@playdrop/api-client/dist/core/request.d.ts +1 -0
- package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/core/request.js +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/admin.js +27 -0
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +18 -4
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +76 -6
- package/node_modules/@playdrop/api-client/dist/index.d.ts +35 -4
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +79 -4
- package/node_modules/@playdrop/config/client-meta.json +2 -2
- package/node_modules/@playdrop/config/dist/src/game-arena.d.ts +14 -0
- package/node_modules/@playdrop/config/dist/src/game-arena.d.ts.map +1 -0
- package/node_modules/@playdrop/config/dist/src/game-arena.js +30 -0
- package/node_modules/@playdrop/config/dist/src/index.d.ts +1 -0
- package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
- package/node_modules/@playdrop/config/dist/src/index.js +1 -0
- package/node_modules/@playdrop/config/dist/test/game-arena.test.d.ts +2 -0
- package/node_modules/@playdrop/config/dist/test/game-arena.test.d.ts.map +1 -0
- package/node_modules/@playdrop/config/dist/test/game-arena.test.js +17 -0
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/config/package.json +4 -0
- package/node_modules/@playdrop/types/dist/api.d.ts +104 -3
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/api.js +39 -2
- package/node_modules/@playdrop/types/dist/app-review.d.ts +11 -0
- package/node_modules/@playdrop/types/dist/app-review.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/app-review.js +56 -0
- package/node_modules/@playdrop/types/dist/index.d.ts +3 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +6 -0
- package/node_modules/@playdrop/types/dist/playtest-notes.d.ts +69 -0
- package/node_modules/@playdrop/types/dist/playtest-notes.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/playtest-notes.js +30 -0
- package/node_modules/@playdrop/types/dist/tweaks.d.ts +48 -0
- package/node_modules/@playdrop/types/dist/tweaks.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/tweaks.js +156 -0
- package/node_modules/@playdrop/types/dist/version.d.ts +37 -0
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.js +6 -0
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/staticHtml.js
CHANGED
package/dist/apps/upload.js
CHANGED
|
@@ -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,
|
package/dist/catalogue.d.ts
CHANGED
|
@@ -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?:
|
|
19
|
-
screenshotsLandscape?:
|
|
20
|
-
videosPortrait?:
|
|
21
|
-
videosLandscape?:
|
|
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
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
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;
|
package/dist/commands/dev.js
CHANGED
|
@@ -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,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 {};
|