@playdrop/playdrop-cli 0.12.11 → 0.12.13
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/create.d.ts +5 -0
- package/dist/commands/create.js +8 -2
- package/dist/commands/devServer.d.ts +1 -0
- package/dist/commands/devServer.js +45 -12
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +51 -3
- package/dist/commands/npmProcess.d.ts +4 -0
- package/dist/commands/npmProcess.js +20 -0
- package/dist/commands/upload.js +4 -118
- package/dist/commands/validate.js +2 -0
- package/dist/commands/worker.d.ts +2 -0
- package/dist/commands/worker.js +60 -17
- 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
|
});
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare function resolveNpmInstallInvocation(platform?: NodeJS.Platform, nodeExecutable?: string): {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
};
|
|
5
|
+
export declare function runNpmInstall(projectDir: string): boolean;
|
|
1
6
|
type CreateOptions = {
|
|
2
7
|
template?: string;
|
|
3
8
|
remix?: string;
|
package/dist/commands/create.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveNpmInstallInvocation = resolveNpmInstallInvocation;
|
|
7
|
+
exports.runNpmInstall = runNpmInstall;
|
|
6
8
|
exports.createAssetSpecProject = createAssetSpecProject;
|
|
7
9
|
exports.create = create;
|
|
8
10
|
const types_1 = require("@playdrop/types");
|
|
@@ -22,6 +24,7 @@ const messages_1 = require("../messages");
|
|
|
22
24
|
const loadCheck_1 = require("../apps/loadCheck");
|
|
23
25
|
const devShared_1 = require("./devShared");
|
|
24
26
|
const init_1 = require("./init");
|
|
27
|
+
const npmProcess_1 = require("./npmProcess");
|
|
25
28
|
const taskCaptureSession_1 = require("./taskCaptureSession");
|
|
26
29
|
const CATALOGUE_FILENAME = 'catalogue.json';
|
|
27
30
|
const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
|
|
@@ -186,9 +189,12 @@ async function promptInstallDependencies(projectDir) {
|
|
|
186
189
|
rl.close();
|
|
187
190
|
return /^y(es)?$/i.test(answer.trim());
|
|
188
191
|
}
|
|
192
|
+
function resolveNpmInstallInvocation(platform = process.platform, nodeExecutable = process.execPath) {
|
|
193
|
+
return (0, npmProcess_1.resolveNpmInvocation)(['install'], platform, nodeExecutable);
|
|
194
|
+
}
|
|
189
195
|
function runNpmInstall(projectDir) {
|
|
190
|
-
const
|
|
191
|
-
const result = (0, node_child_process_1.spawnSync)(
|
|
196
|
+
const invocation = resolveNpmInstallInvocation();
|
|
197
|
+
const result = (0, node_child_process_1.spawnSync)(invocation.command, invocation.args, {
|
|
192
198
|
cwd: projectDir,
|
|
193
199
|
stdio: 'inherit',
|
|
194
200
|
});
|
|
@@ -34,6 +34,7 @@ export declare function buildLocalDevAppUrl(input: {
|
|
|
34
34
|
}): string;
|
|
35
35
|
export declare function terminateChildProcessTree(child: ChildProcess | null, timeoutMs?: number): Promise<void>;
|
|
36
36
|
export declare function ensureDevRouterRunning(port?: number): Promise<void>;
|
|
37
|
+
export declare function resolveDevRouterWorkingDirectory(cliEntrypoint: string): string;
|
|
37
38
|
export declare function updateMountedDevRuntimeAssetManifest(input: {
|
|
38
39
|
creatorUsername: string;
|
|
39
40
|
appName: string;
|
|
@@ -8,6 +8,7 @@ exports.parseMountConflictError = parseMountConflictError;
|
|
|
8
8
|
exports.buildLocalDevAppUrl = buildLocalDevAppUrl;
|
|
9
9
|
exports.terminateChildProcessTree = terminateChildProcessTree;
|
|
10
10
|
exports.ensureDevRouterRunning = ensureDevRouterRunning;
|
|
11
|
+
exports.resolveDevRouterWorkingDirectory = resolveDevRouterWorkingDirectory;
|
|
11
12
|
exports.updateMountedDevRuntimeAssetManifest = updateMountedDevRuntimeAssetManifest;
|
|
12
13
|
exports.startDevServer = startDevServer;
|
|
13
14
|
exports.isDevServerAvailable = isDevServerAvailable;
|
|
@@ -19,6 +20,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
19
20
|
const node_fs_1 = require("node:fs");
|
|
20
21
|
const node_http_1 = __importDefault(require("node:http"));
|
|
21
22
|
const node_path_1 = require("node:path");
|
|
23
|
+
const npmProcess_1 = require("./npmProcess");
|
|
22
24
|
exports.DEV_ROUTER_PORT = 8888;
|
|
23
25
|
const DEV_ROUTER_HOST = '127.0.0.1';
|
|
24
26
|
const CONTROL_PREFIX = '/_playdrop';
|
|
@@ -248,8 +250,8 @@ function spawnDevScript(projectInfo) {
|
|
|
248
250
|
if (!projectInfo.projectDir || !projectInfo.packageJson || typeof projectInfo.packageJson.scripts?.dev !== 'string') {
|
|
249
251
|
return null;
|
|
250
252
|
}
|
|
251
|
-
const
|
|
252
|
-
const child = (0, node_child_process_1.spawn)(
|
|
253
|
+
const npm = (0, npmProcess_1.resolveNpmInvocation)(['run', 'dev']);
|
|
254
|
+
const child = (0, node_child_process_1.spawn)(npm.command, npm.args, {
|
|
253
255
|
cwd: projectInfo.projectDir,
|
|
254
256
|
stdio: 'inherit',
|
|
255
257
|
env: { ...process.env },
|
|
@@ -341,25 +343,52 @@ async function waitForProcessGroupExit(pid, timeoutMs) {
|
|
|
341
343
|
}
|
|
342
344
|
return !isProcessGroupAlive(pid);
|
|
343
345
|
}
|
|
346
|
+
async function terminateWindowsProcessTree(pid, timeoutMs) {
|
|
347
|
+
const windowsRoot = process.env.SystemRoot?.trim() || process.env.WINDIR?.trim() || 'C:\\Windows';
|
|
348
|
+
const taskkillPath = (0, node_path_1.join)(windowsRoot, 'System32', 'taskkill.exe');
|
|
349
|
+
await new Promise((resolvePromise) => {
|
|
350
|
+
let settled = false;
|
|
351
|
+
const taskkill = (0, node_child_process_1.spawn)(taskkillPath, ['/PID', String(pid), '/T', '/F'], {
|
|
352
|
+
stdio: 'ignore',
|
|
353
|
+
windowsHide: true,
|
|
354
|
+
});
|
|
355
|
+
const settle = () => {
|
|
356
|
+
if (settled) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
settled = true;
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
resolvePromise();
|
|
362
|
+
};
|
|
363
|
+
const timeout = setTimeout(() => {
|
|
364
|
+
taskkill.kill();
|
|
365
|
+
settle();
|
|
366
|
+
}, timeoutMs);
|
|
367
|
+
timeout.unref?.();
|
|
368
|
+
taskkill.once('error', settle);
|
|
369
|
+
taskkill.once('close', settle);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
344
372
|
async function terminateChildProcessTree(child, timeoutMs = 2000) {
|
|
345
373
|
const pid = child?.pid;
|
|
346
374
|
if (!child || !pid || !isPidAlive(pid)) {
|
|
347
375
|
return;
|
|
348
376
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
377
|
+
if (process.platform === 'win32') {
|
|
378
|
+
await terminateWindowsProcessTree(pid, timeoutMs);
|
|
379
|
+
if (isPidAlive(pid)) {
|
|
380
|
+
signalProcess(pid, 'SIGKILL', false);
|
|
381
|
+
await waitForChildExit(child, timeoutMs);
|
|
382
|
+
}
|
|
355
383
|
return;
|
|
356
384
|
}
|
|
357
|
-
signalProcess(pid, '
|
|
358
|
-
|
|
359
|
-
|
|
385
|
+
signalProcess(pid, 'SIGTERM', true);
|
|
386
|
+
const exited = await waitForProcessGroupExit(pid, timeoutMs);
|
|
387
|
+
if (exited) {
|
|
360
388
|
return;
|
|
361
389
|
}
|
|
362
|
-
|
|
390
|
+
signalProcess(pid, 'SIGKILL', true);
|
|
391
|
+
await waitForProcessGroupExit(pid, timeoutMs);
|
|
363
392
|
}
|
|
364
393
|
async function fetchRouterJson(path, init = {}, port = exports.DEV_ROUTER_PORT) {
|
|
365
394
|
const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${path}`, init);
|
|
@@ -416,6 +445,7 @@ async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
|
|
|
416
445
|
const childEnv = { ...process.env };
|
|
417
446
|
delete childEnv.PLAYDROP_WORKER_CONTEXT;
|
|
418
447
|
const child = (0, node_child_process_1.spawn)(process.execPath, [cliEntrypoint, 'project', '_dev-router', 'serve'], {
|
|
448
|
+
cwd: resolveDevRouterWorkingDirectory(cliEntrypoint),
|
|
419
449
|
detached: true,
|
|
420
450
|
stdio: 'ignore',
|
|
421
451
|
env: {
|
|
@@ -434,6 +464,9 @@ async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
|
|
|
434
464
|
}
|
|
435
465
|
throw new Error('dev_router_start_failed');
|
|
436
466
|
}
|
|
467
|
+
function resolveDevRouterWorkingDirectory(cliEntrypoint) {
|
|
468
|
+
return (0, node_path_1.dirname)((0, node_path_1.resolve)(cliEntrypoint));
|
|
469
|
+
}
|
|
437
470
|
async function registerDevMount(input, port = exports.DEV_ROUTER_PORT) {
|
|
438
471
|
const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/mounts`, {
|
|
439
472
|
method: 'POST',
|
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) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveNpmInvocation = resolveNpmInvocation;
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
9
|
+
function resolveNpmInvocation(args, platform = node_process_1.default.platform, nodeExecutable = node_process_1.default.execPath) {
|
|
10
|
+
if (platform === 'win32') {
|
|
11
|
+
return {
|
|
12
|
+
command: nodeExecutable,
|
|
13
|
+
args: [
|
|
14
|
+
node_path_1.win32.join(node_path_1.win32.dirname(nodeExecutable), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
15
|
+
...args,
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { command: 'npm', args };
|
|
20
|
+
}
|
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
|
}
|
|
@@ -266,6 +266,7 @@ export declare function createLocalPlaydropShim(input: {
|
|
|
266
266
|
export declare function parseCodexModelCatalog(output: string): string[];
|
|
267
267
|
export declare function parseClaudeModelCatalog(output: string): string[];
|
|
268
268
|
export declare function claudeModelProbeReportsSelection(output: string, expectedDisplayName: string): boolean;
|
|
269
|
+
export declare function probeWorkerProviderIndependently<T>(probe: () => T, unavailable: T): T;
|
|
269
270
|
export declare function buildWorkerSetupActions(input: {
|
|
270
271
|
degradedReasons: string[];
|
|
271
272
|
playwright?: {
|
|
@@ -301,6 +302,7 @@ export declare function resolveWorkerSupervisorLockFile(input?: {
|
|
|
301
302
|
workerHomeDirectory?: string;
|
|
302
303
|
}): string;
|
|
303
304
|
export declare function parseLinuxProcessStartIdentity(stat: string): string | null;
|
|
305
|
+
export declare function parseWindowsProcessStartIdentity(output: string): string | null;
|
|
304
306
|
export declare function removeStaleWorkerSupervisorLockIfUnchanged(file: string, observedHolder: WorkerSupervisorLockOwner): boolean;
|
|
305
307
|
export declare function acquireWorkerSupervisorLock(input: {
|
|
306
308
|
supervisorType: WorkerSupervisorType;
|