@playdrop/playdrop-cli 0.12.12 → 0.12.14
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 +1 -1
- package/dist/apps/upload.js +2 -84
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +51 -3
- package/dist/commands/upload.js +4 -118
- package/dist/commands/validate.js +2 -0
- package/dist/commands/worker/runtime.js +15 -1
- package/dist/listingPreflight.d.ts +16 -0
- package/dist/listingPreflight.js +259 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +5 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.js +41 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +5 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +20 -0
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +19 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/package.json +1 -1
package/config/client-meta.json
CHANGED
package/dist/apps/upload.js
CHANGED
|
@@ -8,6 +8,7 @@ const node_fs_1 = require("node:fs");
|
|
|
8
8
|
const node_path_1 = require("node:path");
|
|
9
9
|
const http_1 = require("../http");
|
|
10
10
|
const loadCheck_1 = require("./loadCheck");
|
|
11
|
+
const listingPreflight_1 = require("../listingPreflight");
|
|
11
12
|
const EXTENSION_TO_MIME = {
|
|
12
13
|
'.png': 'image/png',
|
|
13
14
|
'.jpg': 'image/jpeg',
|
|
@@ -74,89 +75,6 @@ async function bufferFromFile(file) {
|
|
|
74
75
|
function computeSha256Hex(buffer) {
|
|
75
76
|
return (0, node_crypto_1.createHash)('sha256').update(buffer).digest('hex');
|
|
76
77
|
}
|
|
77
|
-
function isSha256Hex(value) {
|
|
78
|
-
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
|
|
79
|
-
}
|
|
80
|
-
function isCaptureMediaFileKey(fileKey) {
|
|
81
|
-
return fileKey.startsWith('screenshotsPortrait:')
|
|
82
|
-
|| fileKey.startsWith('screenshotsLandscape:')
|
|
83
|
-
|| fileKey.startsWith('videosPortrait:')
|
|
84
|
-
|| fileKey.startsWith('videosLandscape:');
|
|
85
|
-
}
|
|
86
|
-
function readMediaCaptureProof(input) {
|
|
87
|
-
const reportPath = input.task.listing?.captureReportPath;
|
|
88
|
-
if (!reportPath) {
|
|
89
|
-
throw new Error(`agent_task_media_capture_report_missing: ${input.task.name} must include listing.captureReport from "playdrop project capture".`);
|
|
90
|
-
}
|
|
91
|
-
let report;
|
|
92
|
-
try {
|
|
93
|
-
report = JSON.parse((0, node_fs_1.readFileSync)(reportPath, 'utf8'));
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
97
|
-
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} must be valid JSON. ${reason}`);
|
|
98
|
-
}
|
|
99
|
-
const binding = report?.binding;
|
|
100
|
-
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
101
|
-
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} is missing binding.`);
|
|
102
|
-
}
|
|
103
|
-
const rawArtifactHashes = binding.artifactHashes;
|
|
104
|
-
if (!rawArtifactHashes || typeof rawArtifactHashes !== 'object' || Array.isArray(rawArtifactHashes)) {
|
|
105
|
-
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes must be an object.`);
|
|
106
|
-
}
|
|
107
|
-
const captureArtifactHashEntries = Object.values(rawArtifactHashes)
|
|
108
|
-
.map((value) => typeof value === 'string' ? value.trim().toLowerCase() : '')
|
|
109
|
-
.filter((value) => isSha256Hex(value));
|
|
110
|
-
const captureArtifactHashes = new Set(captureArtifactHashEntries);
|
|
111
|
-
if (captureArtifactHashEntries.length === 0) {
|
|
112
|
-
throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} binding.artifactHashes is empty.`);
|
|
113
|
-
}
|
|
114
|
-
const captureArtifactHashCounts = new Map();
|
|
115
|
-
for (const hash of captureArtifactHashEntries) {
|
|
116
|
-
captureArtifactHashCounts.set(hash, (captureArtifactHashCounts.get(hash) ?? 0) + 1);
|
|
117
|
-
}
|
|
118
|
-
const mediaFiles = input.preparedSessionFiles.filter((file) => isCaptureMediaFileKey(file.fileKey));
|
|
119
|
-
if (mediaFiles.length === 0) {
|
|
120
|
-
throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload recorder stills and video in listing.screenshots* and listing.videos*.`);
|
|
121
|
-
}
|
|
122
|
-
const uploadedMediaHashCounts = new Map();
|
|
123
|
-
for (const file of mediaFiles) {
|
|
124
|
-
uploadedMediaHashCounts.set(file.sha256, (uploadedMediaHashCounts.get(file.sha256) ?? 0) + 1);
|
|
125
|
-
}
|
|
126
|
-
const missingUploadedCaptureArtifacts = [...captureArtifactHashCounts.entries()]
|
|
127
|
-
.filter(([hash, expectedCount]) => (uploadedMediaHashCounts.get(hash) ?? 0) < expectedCount);
|
|
128
|
-
if (missingUploadedCaptureArtifacts.length > 0) {
|
|
129
|
-
throw new Error(`agent_task_media_capture_artifact_missing_from_listing: ${input.task.name} must list every recorder poster and video from listing.captureReport in catalogue.json.`);
|
|
130
|
-
}
|
|
131
|
-
let hasStill = false;
|
|
132
|
-
let hasVideo = false;
|
|
133
|
-
const artifactHashes = {};
|
|
134
|
-
for (const file of mediaFiles) {
|
|
135
|
-
if (!captureArtifactHashes.has(file.sha256)) {
|
|
136
|
-
throw new Error(`agent_task_media_capture_artifact_mismatch: ${file.fileKey} was not produced by listing.captureReport for ${input.task.name}.`);
|
|
137
|
-
}
|
|
138
|
-
hasStill = hasStill || file.fileKey.startsWith('screenshots');
|
|
139
|
-
hasVideo = hasVideo || file.fileKey.startsWith('videos');
|
|
140
|
-
artifactHashes[file.fileKey] = file.sha256;
|
|
141
|
-
}
|
|
142
|
-
if (!hasStill || !hasVideo) {
|
|
143
|
-
throw new Error(`agent_task_media_capture_artifacts_missing: ${input.task.name} must upload at least one recorder still and one recorder video.`);
|
|
144
|
-
}
|
|
145
|
-
const capturedSurfaces = Array.isArray(binding.capturedSurfaces)
|
|
146
|
-
? Array.from(new Set(binding.capturedSurfaces
|
|
147
|
-
.map((surface) => typeof surface === 'string' ? surface.trim().toUpperCase() : '')
|
|
148
|
-
.filter((surface) => input.task.surfaceTargets.includes(surface))))
|
|
149
|
-
: [];
|
|
150
|
-
const capturedSurfaceSet = new Set(capturedSurfaces);
|
|
151
|
-
const missingSurfaces = input.task.surfaceTargets.filter((surface) => !capturedSurfaceSet.has(surface));
|
|
152
|
-
if (missingSurfaces.length > 0) {
|
|
153
|
-
throw new Error(`agent_task_media_capture_surface_missing: listing.captureReport did not capture ${missingSurfaces.join(', ')} for ${input.task.name}.`);
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
artifactHashes,
|
|
157
|
-
capturedSurfaces,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
78
|
function resolveMimeType(filename, fallback = 'application/octet-stream') {
|
|
161
79
|
const type = EXTENSION_TO_MIME[(0, node_path_1.extname)(filename).toLowerCase()];
|
|
162
80
|
return type ?? fallback;
|
|
@@ -425,7 +343,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
|
|
|
425
343
|
if (options?.mediaCaptureRequired === true
|
|
426
344
|
&& task.hostingMode !== 'EXTERNAL'
|
|
427
345
|
&& !task.externalUrl) {
|
|
428
|
-
const mediaCaptureProof = readMediaCaptureProof({
|
|
346
|
+
const mediaCaptureProof = (0, listingPreflight_1.readMediaCaptureProof)({
|
|
429
347
|
task,
|
|
430
348
|
preparedSessionFiles,
|
|
431
349
|
});
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.setOpenBrowserForTests = exports.resetOpenBrowserForTests = void 0;
|
|
4
|
+
exports.buildCliLoginTokenName = buildCliLoginTokenName;
|
|
4
5
|
exports.login = login;
|
|
5
6
|
const types_1 = require("@playdrop/types");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
6
8
|
const config_1 = require("../config");
|
|
7
9
|
const apiClient_1 = require("../apiClient");
|
|
8
10
|
const http_1 = require("../http");
|
|
@@ -22,6 +24,13 @@ function extractAccessToken(data) {
|
|
|
22
24
|
function isLoginResponse(data) {
|
|
23
25
|
return typeof data === 'object' && data !== null && 'user' in data;
|
|
24
26
|
}
|
|
27
|
+
function buildCliLoginTokenName(machineHostname = (0, node_os_1.hostname)()) {
|
|
28
|
+
const normalizedHostname = machineHostname.trim();
|
|
29
|
+
if (!normalizedHostname) {
|
|
30
|
+
throw new Error('cli_login_hostname_missing');
|
|
31
|
+
}
|
|
32
|
+
return `CLI on ${normalizedHostname}`;
|
|
33
|
+
}
|
|
25
34
|
function storeLogin(env, data, options = {}) {
|
|
26
35
|
const token = extractAccessToken(data);
|
|
27
36
|
if (!token) {
|
|
@@ -53,11 +62,50 @@ function storeLogin(env, data, options = {}) {
|
|
|
53
62
|
console.log(`Logged in as ${username} on ${env}.`);
|
|
54
63
|
console.log('Next: run "playdrop auth whoami" to confirm your session.');
|
|
55
64
|
}
|
|
65
|
+
async function storeDeviceLogin(env, baseUrl, initialLogin, options = {}) {
|
|
66
|
+
const sessionToken = extractAccessToken(initialLogin);
|
|
67
|
+
const username = typeof initialLogin.user?.username === 'string'
|
|
68
|
+
? initialLogin.user.username.trim()
|
|
69
|
+
: '';
|
|
70
|
+
if (!sessionToken) {
|
|
71
|
+
throw new Error('Login response is missing the session token required to create this computer token.');
|
|
72
|
+
}
|
|
73
|
+
if (!username) {
|
|
74
|
+
throw new Error('Login response is missing the username required to save this computer token.');
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const sessionClient = (0, apiClient_1.createCliApiClient)({ baseUrl, token: sessionToken });
|
|
78
|
+
const deviceToken = await sessionClient.createCliApiToken({
|
|
79
|
+
name: buildCliLoginTokenName(),
|
|
80
|
+
});
|
|
81
|
+
const deviceLogin = await (0, apiClient_1.createCliApiClient)({ baseUrl }).loginWithApiKey({
|
|
82
|
+
apiKey: deviceToken.apiKey,
|
|
83
|
+
});
|
|
84
|
+
storeLogin(env, deviceLogin, options);
|
|
85
|
+
}
|
|
86
|
+
catch (unknownError) {
|
|
87
|
+
if (unknownError instanceof types_1.UnsupportedClientError) {
|
|
88
|
+
(0, http_1.handleUnsupportedError)(unknownError, 'Login');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (unknownError instanceof types_1.ApiError) {
|
|
92
|
+
const tokenLimitReached = unknownError.code === 'cli_api_token_limit_reached';
|
|
93
|
+
(0, messages_1.printErrorWithHelp)(tokenLimitReached
|
|
94
|
+
? 'Login succeeded, but this account already has the maximum of 25 CLI tokens.'
|
|
95
|
+
: `Login succeeded, but this computer's CLI token could not be created (status ${unknownError.status}).`, tokenLimitReached
|
|
96
|
+
? ['Delete an unused CLI token in account settings, then rerun "playdrop auth login".']
|
|
97
|
+
: ['Rerun "playdrop auth login" to try again.'], { command: 'login' });
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
throw unknownError;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
56
104
|
async function loginWithUsernamePassword(env, baseUrl, username, password, options = {}) {
|
|
57
105
|
const client = (0, apiClient_1.createCliApiClient)({ baseUrl });
|
|
58
106
|
try {
|
|
59
107
|
const data = await client.login({ username, password });
|
|
60
|
-
|
|
108
|
+
await storeDeviceLogin(env, baseUrl, data, options);
|
|
61
109
|
}
|
|
62
110
|
catch (unknownError) {
|
|
63
111
|
if (unknownError instanceof types_1.UnsupportedClientError) {
|
|
@@ -101,7 +149,7 @@ async function loginWithHandoff(env, baseUrl, handoffToken, options = {}) {
|
|
|
101
149
|
const client = (0, apiClient_1.createCliApiClient)({ baseUrl });
|
|
102
150
|
try {
|
|
103
151
|
const data = await client.exchangeAppleOAuthHandoff({ token: handoffToken });
|
|
104
|
-
|
|
152
|
+
await storeDeviceLogin(env, baseUrl, data, options);
|
|
105
153
|
}
|
|
106
154
|
catch (unknownError) {
|
|
107
155
|
if (unknownError instanceof types_1.UnsupportedClientError) {
|
|
@@ -163,7 +211,7 @@ async function loginInBrowser(env, baseUrl, options = {}) {
|
|
|
163
211
|
await sleep(intervalMs);
|
|
164
212
|
continue;
|
|
165
213
|
}
|
|
166
|
-
|
|
214
|
+
await storeDeviceLogin(env, baseUrl, result, options);
|
|
167
215
|
return;
|
|
168
216
|
}
|
|
169
217
|
catch (unknownError) {
|
package/dist/commands/upload.js
CHANGED
|
@@ -51,6 +51,7 @@ const uploadLog_1 = require("../uploadLog");
|
|
|
51
51
|
const upload_content_1 = require("./upload-content");
|
|
52
52
|
const appUrls_1 = require("../appUrls");
|
|
53
53
|
const devRuntimeAssets_1 = require("./devRuntimeAssets");
|
|
54
|
+
const listingPreflight_1 = require("../listingPreflight");
|
|
54
55
|
const upload_graph_1 = require("./upload-graph");
|
|
55
56
|
function normalizeSavedItemKinds(kinds) {
|
|
56
57
|
return [...new Set(Array.isArray(kinds) ? kinds : [])].sort();
|
|
@@ -847,6 +848,7 @@ function appendTaskRelations(graphState, fromNodeId, relations, contextLabel) {
|
|
|
847
848
|
});
|
|
848
849
|
}
|
|
849
850
|
async function uploadAppTask(state, task, taskCreator, options) {
|
|
851
|
+
(0, listingPreflight_1.assertProjectListingPreflight)(task);
|
|
850
852
|
const { upload, warnings } = await (0, apps_1.runAppPipeline)(state.client, task, {
|
|
851
853
|
skipReview: options?.skipReview,
|
|
852
854
|
clearTags: options?.clearTags,
|
|
@@ -1230,10 +1232,6 @@ const WORKER_SUPERVISOR_BUNDLE_EXCLUDES = [
|
|
|
1230
1232
|
'assets/marketing/',
|
|
1231
1233
|
'listing/',
|
|
1232
1234
|
];
|
|
1233
|
-
const MIN_HERO_PORTRAIT_WIDTH = 512;
|
|
1234
|
-
const MIN_HERO_PORTRAIT_HEIGHT = 768;
|
|
1235
|
-
const MIN_HERO_LANDSCAPE_WIDTH = 768;
|
|
1236
|
-
const MIN_HERO_LANDSCAPE_HEIGHT = 432;
|
|
1237
1235
|
const OWNED_RUNTIME_IMAGE_CHECK_SIZE = 96;
|
|
1238
1236
|
const OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE = 512;
|
|
1239
1237
|
function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
|
|
@@ -2024,118 +2022,6 @@ async function assertNewGameDeclaredAssetsAreUsedAtRuntime(input) {
|
|
|
2024
2022
|
throw new Error('agent_task_game_asset_fallback_disallowed: new game tasks must fail clearly when declared gameplay assets cannot load instead of rendering placeholder or fallback visuals.');
|
|
2025
2023
|
}
|
|
2026
2024
|
}
|
|
2027
|
-
function readPngDimensions(filePath) {
|
|
2028
|
-
const buffer = (0, node_fs_1.readFileSync)(filePath);
|
|
2029
|
-
if (buffer.length < 24) {
|
|
2030
|
-
throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} is too small to be a valid PNG.`);
|
|
2031
|
-
}
|
|
2032
|
-
const isPng = (buffer[0] === 0x89
|
|
2033
|
-
&& buffer[1] === 0x50
|
|
2034
|
-
&& buffer[2] === 0x4e
|
|
2035
|
-
&& buffer[3] === 0x47
|
|
2036
|
-
&& buffer[4] === 0x0d
|
|
2037
|
-
&& buffer[5] === 0x0a
|
|
2038
|
-
&& buffer[6] === 0x1a
|
|
2039
|
-
&& buffer[7] === 0x0a);
|
|
2040
|
-
if (!isPng || buffer.toString('ascii', 12, 16) !== 'IHDR') {
|
|
2041
|
-
throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} must be a PNG with an IHDR header.`);
|
|
2042
|
-
}
|
|
2043
|
-
const width = buffer.readUInt32BE(16);
|
|
2044
|
-
const height = buffer.readUInt32BE(20);
|
|
2045
|
-
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
2046
|
-
throw new Error(`agent_task_listing_hero_art_invalid: ${filePath} has invalid PNG dimensions.`);
|
|
2047
|
-
}
|
|
2048
|
-
return { width, height };
|
|
2049
|
-
}
|
|
2050
|
-
function readAgentTaskCatalogueApp(task) {
|
|
2051
|
-
let data;
|
|
2052
|
-
try {
|
|
2053
|
-
data = JSON.parse((0, node_fs_1.readFileSync)(task.catalogueAbsolutePath, 'utf8'));
|
|
2054
|
-
}
|
|
2055
|
-
catch (error) {
|
|
2056
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
2057
|
-
throw new Error(`agent_task_catalogue_invalid_json: ${task.cataloguePath} must be valid JSON. ${reason}`);
|
|
2058
|
-
}
|
|
2059
|
-
const apps = Array.isArray(data?.apps) ? data.apps : [];
|
|
2060
|
-
const app = apps.find((entry) => entry && typeof entry === 'object' && entry.name === task.name);
|
|
2061
|
-
if (!app || typeof app !== 'object') {
|
|
2062
|
-
throw new Error(`agent_task_catalogue_app_missing: ${task.cataloguePath} must contain apps[] entry "${task.name}".`);
|
|
2063
|
-
}
|
|
2064
|
-
return app;
|
|
2065
|
-
}
|
|
2066
|
-
function assertNewGameHeroListingKeys(task) {
|
|
2067
|
-
const app = readAgentTaskCatalogueApp(task);
|
|
2068
|
-
const listing = app.listing;
|
|
2069
|
-
if (!listing || typeof listing !== 'object' || Array.isArray(listing)) {
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
const hasLegacyPortrait = typeof listing.heroPortraitPath === 'string' && listing.heroPortraitPath.trim().length > 0;
|
|
2073
|
-
const hasLegacyLandscape = typeof listing.heroLandscapePath === 'string' && listing.heroLandscapePath.trim().length > 0;
|
|
2074
|
-
if (hasLegacyPortrait || hasLegacyLandscape) {
|
|
2075
|
-
throw new Error('agent_task_listing_hero_art_legacy_keys: new game tasks must reference hero art through listing.heroPortrait and listing.heroLandscape, not listing.heroPortraitPath or listing.heroLandscapePath.');
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
function assertNewGameHeroListingPath(task, filePath, field) {
|
|
2079
|
-
const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), (0, node_path_1.resolve)(filePath)).replace(/\\/g, '/');
|
|
2080
|
-
if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
|
|
2081
|
-
throw new Error(`agent_task_listing_hero_art_path_invalid: ${field} must point to a PNG inside the project under assets/marketing/playdrop/.`);
|
|
2082
|
-
}
|
|
2083
|
-
if (!relativePath.startsWith('assets/marketing/playdrop/') || (0, node_path_1.extname)(relativePath).toLowerCase() !== '.png') {
|
|
2084
|
-
throw new Error(`agent_task_listing_hero_art_path_invalid: ${field} must point to a PNG under assets/marketing/playdrop/.`);
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
function assertNewGameHeroListingArt(task) {
|
|
2088
|
-
const heroPortraitPath = task.listing?.heroPortraitPath;
|
|
2089
|
-
const heroLandscapePath = task.listing?.heroLandscapePath;
|
|
2090
|
-
if (!heroPortraitPath || !heroLandscapePath) {
|
|
2091
|
-
throw new Error('agent_task_listing_hero_art_missing: new game tasks must include listing.heroPortrait and listing.heroLandscape PNG assets.');
|
|
2092
|
-
}
|
|
2093
|
-
assertNewGameHeroListingKeys(task);
|
|
2094
|
-
assertNewGameHeroListingPath(task, heroPortraitPath, 'listing.heroPortrait');
|
|
2095
|
-
assertNewGameHeroListingPath(task, heroLandscapePath, 'listing.heroLandscape');
|
|
2096
|
-
const portrait = readPngDimensions(heroPortraitPath);
|
|
2097
|
-
if (portrait.width < MIN_HERO_PORTRAIT_WIDTH || portrait.height < MIN_HERO_PORTRAIT_HEIGHT || portrait.height <= portrait.width) {
|
|
2098
|
-
throw new Error(`agent_task_listing_hero_art_invalid: listing.heroPortrait must be a portrait PNG at least ${MIN_HERO_PORTRAIT_WIDTH}x${MIN_HERO_PORTRAIT_HEIGHT}.`);
|
|
2099
|
-
}
|
|
2100
|
-
const landscape = readPngDimensions(heroLandscapePath);
|
|
2101
|
-
if (landscape.width < MIN_HERO_LANDSCAPE_WIDTH || landscape.height < MIN_HERO_LANDSCAPE_HEIGHT || landscape.width <= landscape.height) {
|
|
2102
|
-
throw new Error(`agent_task_listing_hero_art_invalid: listing.heroLandscape must be a landscape PNG at least ${MIN_HERO_LANDSCAPE_WIDTH}x${MIN_HERO_LANDSCAPE_HEIGHT}.`);
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
function assertListingScreenshotPath(task, filePath, field) {
|
|
2106
|
-
const relativePath = (0, node_path_1.relative)((0, node_path_1.resolve)(task.projectDir), (0, node_path_1.resolve)(filePath)).replace(/\\/g, '/');
|
|
2107
|
-
const requiredPrefix = field === 'listing.screenshotsPortrait'
|
|
2108
|
-
? 'assets/marketing/playdrop/screenshots/portrait/'
|
|
2109
|
-
: 'assets/marketing/playdrop/screenshots/landscape/';
|
|
2110
|
-
if (relativePath === '' || relativePath === '..' || relativePath.startsWith('../') || relativePath.startsWith('/')) {
|
|
2111
|
-
throw new Error(`agent_task_listing_screenshots_path_invalid: ${field} entries must point to PNG files inside the project under ${requiredPrefix}.`);
|
|
2112
|
-
}
|
|
2113
|
-
if (!relativePath.startsWith(requiredPrefix) || (0, node_path_1.extname)(relativePath).toLowerCase() !== '.png') {
|
|
2114
|
-
throw new Error(`agent_task_listing_screenshots_path_invalid: ${field} entries must point to PNG files under ${requiredPrefix}.`);
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
function assertNewGameListingScreenshots(task) {
|
|
2118
|
-
const listing = task.listing;
|
|
2119
|
-
const portraitPaths = listing?.screenshotPortraitPaths ?? [];
|
|
2120
|
-
const landscapePaths = listing?.screenshotLandscapePaths ?? [];
|
|
2121
|
-
if (portraitPaths.length === 0 && landscapePaths.length === 0) {
|
|
2122
|
-
throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG captures.');
|
|
2123
|
-
}
|
|
2124
|
-
if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
|
|
2125
|
-
throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG capture.');
|
|
2126
|
-
}
|
|
2127
|
-
if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
|
|
2128
|
-
throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG capture.');
|
|
2129
|
-
}
|
|
2130
|
-
for (const filePath of portraitPaths) {
|
|
2131
|
-
assertListingScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
|
|
2132
|
-
readPngDimensions(filePath);
|
|
2133
|
-
}
|
|
2134
|
-
for (const filePath of landscapePaths) {
|
|
2135
|
-
assertListingScreenshotPath(task, filePath, 'listing.screenshotsLandscape');
|
|
2136
|
-
readPngDimensions(filePath);
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
2025
|
function readPlaytestEvidenceManifest(task) {
|
|
2140
2026
|
const manifestPath = (0, node_path_1.join)(task.projectDir, 'playtest-evidence.json');
|
|
2141
2027
|
if (!(0, node_fs_1.existsSync)(manifestPath)) {
|
|
@@ -2297,7 +2183,7 @@ async function publishWorkerAppProject(input) {
|
|
|
2297
2183
|
assertTaskPlaytestEvidenceManifest(task);
|
|
2298
2184
|
}
|
|
2299
2185
|
if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
|
|
2300
|
-
|
|
2186
|
+
(0, listingPreflight_1.assertAgentGameHeroListingPreflight)(task);
|
|
2301
2187
|
}
|
|
2302
2188
|
if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
|
|
2303
2189
|
throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
|
|
@@ -2346,7 +2232,7 @@ async function publishWorkerAppProject(input) {
|
|
|
2346
2232
|
taskToken,
|
|
2347
2233
|
};
|
|
2348
2234
|
if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
|
|
2349
|
-
|
|
2235
|
+
(0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(task);
|
|
2350
2236
|
assertTaskPlaytestEvidenceManifest(task);
|
|
2351
2237
|
}
|
|
2352
2238
|
const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
|
|
@@ -9,6 +9,7 @@ const externalAssetPackValidation_1 = require("../externalAssetPackValidation");
|
|
|
9
9
|
const taskSelection_1 = require("../taskSelection");
|
|
10
10
|
const taskUtils_1 = require("../taskUtils");
|
|
11
11
|
const uploadLog_1 = require("../uploadLog");
|
|
12
|
+
const listingPreflight_1 = require("../listingPreflight");
|
|
12
13
|
const upload_content_1 = require("./upload-content");
|
|
13
14
|
function buildLocalAssetSpecLookups(tasks) {
|
|
14
15
|
const exactByRef = new Map();
|
|
@@ -191,6 +192,7 @@ async function validate(pathOrName, options = {}) {
|
|
|
191
192
|
const entityId = task.name;
|
|
192
193
|
try {
|
|
193
194
|
if (task.kind === 'app') {
|
|
195
|
+
(0, listingPreflight_1.assertProjectListingPreflight)(task);
|
|
194
196
|
await (0, apps_1.validateAppTask)(task);
|
|
195
197
|
(0, apps_1.collectAppValidationWarnings)(task, 'source').forEach((warning) => warnings.add(warning));
|
|
196
198
|
}
|
|
@@ -347,7 +347,14 @@ async function runLoggedProcess(input) {
|
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
349
|
const chunks = pendingTranscriptChunks.splice(0, pendingTranscriptChunks.length);
|
|
350
|
-
|
|
350
|
+
try {
|
|
351
|
+
await input.onTranscriptChunks(chunks);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
// Transcript upload is telemetry, never task-critical: requeue in order and retry with the next flush.
|
|
355
|
+
pendingTranscriptChunks.unshift(...chunks);
|
|
356
|
+
console.error(`warning: transcript chunk upload failed, will retry with next flush: ${error instanceof Error ? error.message : String(error)}`);
|
|
357
|
+
}
|
|
351
358
|
};
|
|
352
359
|
const queueTranscriptFlush = () => {
|
|
353
360
|
transcriptFlushPromise = transcriptFlushPromise.then(flushTranscriptChunks, flushTranscriptChunks);
|
|
@@ -416,6 +423,13 @@ async function runLoggedProcess(input) {
|
|
|
416
423
|
}
|
|
417
424
|
void (async () => {
|
|
418
425
|
await queueTranscriptFlush();
|
|
426
|
+
if (pendingTranscriptChunks.length > 0) {
|
|
427
|
+
// Last flush failed: retry once, then drop with a warning. Missing transcript chunks never fail the task.
|
|
428
|
+
await flushTranscriptChunks();
|
|
429
|
+
}
|
|
430
|
+
if (pendingTranscriptChunks.length > 0) {
|
|
431
|
+
console.error(`warning: dropped ${pendingTranscriptChunks.length} transcript chunk(s) after final retry; transcript is telemetry, continuing`);
|
|
432
|
+
}
|
|
419
433
|
if (transcriptFlushError) {
|
|
420
434
|
throwLoggedProcessError(transcriptFlushError);
|
|
421
435
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RecordAppUploadMediaCaptureRequest } from '@playdrop/types';
|
|
2
|
+
import type { AppTask } from './catalogue';
|
|
3
|
+
export type ListingMediaFile = {
|
|
4
|
+
fileKey: string;
|
|
5
|
+
sha256: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function readMediaCaptureProof(input: {
|
|
8
|
+
task: AppTask;
|
|
9
|
+
preparedSessionFiles: ListingMediaFile[];
|
|
10
|
+
}): RecordAppUploadMediaCaptureRequest;
|
|
11
|
+
export declare function assertAgentGameHeroListingPreflight(task: AppTask): void;
|
|
12
|
+
export declare function assertAgentGameScreenshotListingPreflight(task: AppTask): void;
|
|
13
|
+
export declare function assertAgentGameListingPreflight(task: AppTask, options: {
|
|
14
|
+
requireCaptureReport: boolean;
|
|
15
|
+
}): void;
|
|
16
|
+
export declare function assertProjectListingPreflight(task: AppTask): void;
|