@playdrop/playdrop-cli 0.15.6 → 0.15.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/index.d.ts +2 -3
  3. package/dist/apps/index.js +2 -2
  4. package/dist/apps/loadCheck.d.ts +4 -1
  5. package/dist/apps/loadCheck.js +64 -7
  6. package/dist/apps/staticHtml.js +1 -0
  7. package/dist/apps/upload.d.ts +38 -1
  8. package/dist/apps/upload.js +58 -44
  9. package/dist/catalogue.d.ts +3 -0
  10. package/dist/catalogue.js +26 -0
  11. package/dist/commands/create.js +45 -16
  12. package/dist/commands/devRuntimeAssets.d.ts +1 -0
  13. package/dist/commands/devRuntimeAssets.js +2 -0
  14. package/dist/commands/review.js +0 -3
  15. package/dist/commands/upload.d.ts +2 -3
  16. package/dist/commands/upload.js +23 -42
  17. package/dist/commands/validate.js +0 -4
  18. package/dist/commands/worker.d.ts +7 -1
  19. package/dist/commands/worker.js +65 -57
  20. package/dist/index.js +1 -1
  21. package/dist/listingPreflight.d.ts +1 -24
  22. package/dist/listingPreflight.js +3 -288
  23. package/node_modules/@playdrop/api-client/dist/client.d.ts +25 -2
  24. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/client.js +11 -0
  26. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +4 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/domains/admin.js +31 -2
  29. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +2 -1
  30. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +12 -0
  32. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +9 -1
  33. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  34. package/node_modules/@playdrop/api-client/dist/domains/apps.js +66 -0
  35. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts +1 -0
  36. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts.map +1 -1
  37. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.js +3 -0
  38. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +1 -0
  39. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  40. package/node_modules/@playdrop/api-client/dist/domains/assets.js +3 -0
  41. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +10 -2
  42. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  43. package/node_modules/@playdrop/api-client/dist/domains/payments.js +66 -1
  44. package/node_modules/@playdrop/api-client/dist/index.d.ts +27 -5
  45. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  46. package/node_modules/@playdrop/api-client/dist/index.js +73 -2
  47. package/node_modules/@playdrop/config/client-meta.json +2 -2
  48. package/node_modules/@playdrop/config/dist/src/creator-plans.d.ts +18 -0
  49. package/node_modules/@playdrop/config/dist/src/creator-plans.d.ts.map +1 -0
  50. package/node_modules/@playdrop/config/dist/src/creator-plans.js +117 -0
  51. package/node_modules/@playdrop/config/dist/src/game-arena.d.ts +1 -1
  52. package/node_modules/@playdrop/config/dist/src/game-arena.js +1 -1
  53. package/node_modules/@playdrop/config/dist/src/index.d.ts +1 -0
  54. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  55. package/node_modules/@playdrop/config/dist/src/index.js +1 -0
  56. package/node_modules/@playdrop/config/dist/test/creator-plans.test.d.ts +2 -0
  57. package/node_modules/@playdrop/config/dist/test/creator-plans.test.d.ts.map +1 -0
  58. package/node_modules/@playdrop/config/dist/test/creator-plans.test.js +28 -0
  59. package/node_modules/@playdrop/config/dist/test/game-arena.test.js +3 -3
  60. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  61. package/node_modules/@playdrop/types/dist/api.d.ts +153 -15
  62. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  63. package/node_modules/@playdrop/types/dist/api.js +27 -2
  64. package/node_modules/@playdrop/types/dist/app-review.d.ts +2 -0
  65. package/node_modules/@playdrop/types/dist/app-review.d.ts.map +1 -1
  66. package/node_modules/@playdrop/types/dist/app-review.js +31 -2
  67. package/node_modules/@playdrop/types/dist/index.d.ts +2 -0
  68. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  69. package/node_modules/@playdrop/types/dist/index.js +5 -1
  70. package/node_modules/@playdrop/types/dist/locale.d.ts +5 -0
  71. package/node_modules/@playdrop/types/dist/locale.d.ts.map +1 -0
  72. package/node_modules/@playdrop/types/dist/locale.js +47 -0
  73. package/node_modules/@playdrop/types/dist/release-smoke.d.ts +3 -0
  74. package/node_modules/@playdrop/types/dist/release-smoke.d.ts.map +1 -0
  75. package/node_modules/@playdrop/types/dist/release-smoke.js +35 -0
  76. package/node_modules/@playdrop/types/dist/version.d.ts +5 -0
  77. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  78. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.15.6",
2
+ "version": "0.15.11",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.15.6",
4
+ "runtimeSdkVersion": "0.15.11",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -4,7 +4,7 @@ import type { AppTask } from '../catalogue';
4
4
  import type { TaskCaptureSession } from '../appUrls';
5
5
  import { collectAppValidationWarnings, validateAppTask, runFormatScript } from './validate';
6
6
  import { buildApp, type AppBuildArtifacts } from './build';
7
- import { uploadApp, type AppUploadResult } from './upload';
7
+ import { prepareAppUploadFiles, uploadApp, type AppUploadResult } from './upload';
8
8
  export type AppPipelineResult = {
9
9
  artifacts: AppBuildArtifacts | null;
10
10
  upload: AppUploadResult;
@@ -25,10 +25,9 @@ export type AppPipelineOptions = {
25
25
  ensureRegisteredAppShell?: boolean;
26
26
  agentTaskId?: number;
27
27
  playtestProof?: AgentTaskPlaytestProof;
28
- mediaCaptureRequired?: boolean;
29
28
  playtestSmokeCheckRequired?: boolean;
30
29
  captureSession?: TaskCaptureSession | null;
31
30
  artifacts?: AppBuildArtifacts;
32
31
  };
33
32
  export declare function runAppPipeline(client: ApiClient, task: AppTask, options?: AppPipelineOptions): Promise<AppPipelineResult>;
34
- export { collectAppValidationWarnings, validateAppTask, buildApp, uploadApp, runFormatScript };
33
+ export { collectAppValidationWarnings, validateAppTask, buildApp, prepareAppUploadFiles, uploadApp, runFormatScript };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runFormatScript = exports.uploadApp = exports.buildApp = exports.validateAppTask = exports.collectAppValidationWarnings = void 0;
3
+ exports.runFormatScript = exports.uploadApp = exports.prepareAppUploadFiles = exports.buildApp = exports.validateAppTask = exports.collectAppValidationWarnings = void 0;
4
4
  exports.runAppPipeline = runAppPipeline;
5
5
  const validate_1 = require("./validate");
6
6
  Object.defineProperty(exports, "collectAppValidationWarnings", { enumerable: true, get: function () { return validate_1.collectAppValidationWarnings; } });
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "runFormatScript", { enumerable: true, get: funct
9
9
  const build_1 = require("./build");
10
10
  Object.defineProperty(exports, "buildApp", { enumerable: true, get: function () { return build_1.buildApp; } });
11
11
  const upload_1 = require("./upload");
12
+ Object.defineProperty(exports, "prepareAppUploadFiles", { enumerable: true, get: function () { return upload_1.prepareAppUploadFiles; } });
12
13
  Object.defineProperty(exports, "uploadApp", { enumerable: true, get: function () { return upload_1.uploadApp; } });
13
14
  const registration_1 = require("./registration");
14
15
  const loadCheck_1 = require("./loadCheck");
@@ -66,7 +67,6 @@ async function runAppPipeline(client, task, options) {
66
67
  loadCheckTimeoutMs: options?.loadCheckTimeoutMs,
67
68
  agentTaskId: options?.agentTaskId,
68
69
  playtestProof: options?.playtestProof,
69
- mediaCaptureRequired: options?.mediaCaptureRequired,
70
70
  playtestSmokeCheckRequired: options?.playtestSmokeCheckRequired,
71
71
  captureSession: options?.captureSession,
72
72
  };
@@ -38,7 +38,8 @@ export declare function isSoftwareWebGlRenderer(renderer: string | null | undefi
38
38
  export declare function dispatchPlaytestTapeToFrame(page: Page, frame: Frame, surface: AppSurface, tape: AppPlaytestTape, hooks?: {
39
39
  onBeforeEvent?: (eventIndex: number, event: AppPlaytestTapeEvent) => void;
40
40
  }): Promise<void>;
41
- export declare function formatHostedLoadCheckFailure(taskName: string, result: HostedLoadCheckResult, scope?: 'local' | 'staged' | 'final'): string;
41
+ export declare function verifyHostedEditorPhaseRoundTrip(page: Pick<Page, 'evaluate'>, frame: Pick<Frame, 'evaluate' | 'waitForFunction'>, timeoutMs: number): Promise<void>;
42
+ export declare function formatHostedLoadCheckFailure(taskName: string, result: HostedLoadCheckResult, scope?: 'local' | 'staged' | 'editor' | 'final'): string;
42
43
  export declare function redactHostedLoadCheckSecrets(text: string, sourceUrl: string): string;
43
44
  export declare function runLocalHostedLoadCheck(input: {
44
45
  client: ApiClient;
@@ -71,6 +72,8 @@ export declare function runUploadedHostedLoadCheck(input: {
71
72
  gameFrameScreenshotPath?: string | null;
72
73
  playtestTape?: AppPlaytestTape;
73
74
  postReadyWaitMs?: number;
75
+ phase?: 'play' | 'editor';
76
+ verifyEditorPhaseRoundTrip?: boolean;
74
77
  }): Promise<HostedLoadCheckResult>;
75
78
  export declare function comparePlaytestFrames(firstPath: string, secondPath: string, thresholds?: {
76
79
  meanDeltaMin: number;
@@ -10,6 +10,7 @@ exports.resolveLoadCheckSurface = resolveLoadCheckSurface;
10
10
  exports.cloneLoadCheckContextOptions = cloneLoadCheckContextOptions;
11
11
  exports.isSoftwareWebGlRenderer = isSoftwareWebGlRenderer;
12
12
  exports.dispatchPlaytestTapeToFrame = dispatchPlaytestTapeToFrame;
13
+ exports.verifyHostedEditorPhaseRoundTrip = verifyHostedEditorPhaseRoundTrip;
13
14
  exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
14
15
  exports.redactHostedLoadCheckSecrets = redactHostedLoadCheckSecrets;
15
16
  exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
@@ -417,6 +418,48 @@ async function findGameFrame(page, timeoutMs) {
417
418
  }
418
419
  return frame;
419
420
  }
421
+ async function verifyHostedEditorPhaseRoundTrip(page, frame, timeoutMs) {
422
+ const armReadySignal = async () => {
423
+ await frame.evaluate(() => {
424
+ const runtimeWindow = window;
425
+ const state = runtimeWindow.__playdropEditorPhaseCheck ?? {
426
+ installed: false,
427
+ signals: [],
428
+ };
429
+ state.signals = [];
430
+ runtimeWindow.__playdropEditorPhaseCheck = state;
431
+ if (state.installed) {
432
+ return;
433
+ }
434
+ state.installed = true;
435
+ window.addEventListener('playdrop:loading-state', (event) => {
436
+ const detail = event.detail;
437
+ if (detail?.status === 'ready') {
438
+ state.signals.push({ editor: detail.editor === true });
439
+ }
440
+ });
441
+ });
442
+ };
443
+ const changePhaseAndWait = async (phase, editor) => {
444
+ await armReadySignal();
445
+ await page.evaluate((nextPhase) => {
446
+ window.postMessage({ type: 'playdrop-studio-runtime-state', active: true, phase: nextPhase }, window.location.origin);
447
+ }, phase);
448
+ await frame.waitForFunction((expectedEditor) => {
449
+ const runtimeWindow = window;
450
+ return runtimeWindow.__playdropEditorPhaseCheck?.signals?.some((signal) => signal.editor === expectedEditor) === true;
451
+ }, editor, { timeout: timeoutMs });
452
+ };
453
+ try {
454
+ await changePhaseAndWait('play', false);
455
+ await changePhaseAndWait('editor', true);
456
+ }
457
+ catch (error) {
458
+ const detail = error instanceof Error ? error.message : String(error);
459
+ throw new Error('project_check_editor_phase_switch_failed: The hosted bundle must switch Editor to Playtest and back '
460
+ + `inside the existing runtime frame. ${detail}`);
461
+ }
462
+ }
420
463
  async function readWebGlRenderer(frame) {
421
464
  const renderer = await frame.evaluate(() => {
422
465
  const canvas = document.createElement('canvas');
@@ -489,9 +532,11 @@ function normalizeLoadCheckFailure(error, artifactFingerprint) {
489
532
  function formatHostedLoadCheckFailure(taskName, result, scope = 'local') {
490
533
  const scopeLabel = scope === 'final'
491
534
  ? 'final owner play route'
492
- : scope === 'staged'
493
- ? 'staged upload preview'
494
- : 'local hosted preview';
535
+ : scope === 'editor'
536
+ ? 'staged Editor phase'
537
+ : scope === 'staged'
538
+ ? 'staged upload preview'
539
+ : 'local hosted preview';
495
540
  const errorCode = result.errorCode?.trim() || 'startup_failure';
496
541
  const message = normalizeLoadCheckMessage(result.message);
497
542
  return `hosted_load_check_failed: ${taskName} failed the ${scopeLabel} check (${errorCode}). ${message}`;
@@ -694,6 +739,9 @@ async function runHostedLoadCheck(options) {
694
739
  if (isSoftwareWebGlRenderer(webglRenderer)) {
695
740
  throw new Error(`project_check_webgl_renderer_software: WebGL renderer is "${webglRenderer}". Real hardware acceleration is required.`);
696
741
  }
742
+ if (options.verifyEditorPhaseRoundTrip) {
743
+ await verifyHostedEditorPhaseRoundTrip(page, frame, readinessTimeoutMs);
744
+ }
697
745
  await saveGameFrameScreenshot(page, options.readyGameFrameScreenshotPath);
698
746
  if (options.playtestTape) {
699
747
  if (!options.playtestSurface) {
@@ -917,6 +965,12 @@ function appendLoadCheckParams(targetUrl, devAuth) {
917
965
  }
918
966
  async function runUploadedHostedLoadCheck(input) {
919
967
  const preview = await input.client.fetchAppUploadLaunchCheckPreview(input.creatorUsername, input.task.name, input.sessionId);
968
+ const withRequestedPhase = (rawUrl) => {
969
+ const url = new URL(rawUrl);
970
+ if (input.phase === 'editor')
971
+ url.searchParams.set('hostPhase', 'editor');
972
+ return url.toString();
973
+ };
920
974
  const captureSurface = resolveLoadCheckSurface(preview.localAppMetadata.surfaceTargets, input.task.primarySurface);
921
975
  const contextOptions = cloneLoadCheckContextOptions(captureSurface);
922
976
  const anonymousExpectedState = resolveAnonymousHostedLaunchState({
@@ -925,7 +979,7 @@ async function runUploadedHostedLoadCheck(input) {
925
979
  });
926
980
  let result;
927
981
  if (input.captureSession) {
928
- const targetUrl = new URL(preview.launchUrl);
982
+ const targetUrl = new URL(withRequestedPhase(preview.launchUrl));
929
983
  (0, appUrls_1.appendTaskCaptureSession)(targetUrl, input.captureSession);
930
984
  result = await runHostedLoadCheck({
931
985
  targetUrl: targetUrl.toString(),
@@ -939,6 +993,7 @@ async function runUploadedHostedLoadCheck(input) {
939
993
  playtestTape: input.playtestTape,
940
994
  playtestSurface: captureSurface,
941
995
  postReadyWaitMs: input.postReadyWaitMs,
996
+ verifyEditorPhaseRoundTrip: input.verifyEditorPhaseRoundTrip,
942
997
  });
943
998
  }
944
999
  else if (anonymousExpectedState === 'login_required') {
@@ -947,7 +1002,7 @@ async function runUploadedHostedLoadCheck(input) {
947
1002
  }
948
1003
  else {
949
1004
  const anonymousGateResult = await runHostedLoadCheck({
950
- targetUrl: appendLoadCheckParams(preview.launchUrl, 'anonymous'),
1005
+ targetUrl: withRequestedPhase(appendLoadCheckParams(preview.launchUrl, 'anonymous')),
951
1006
  artifactFingerprint: preview.session.manifestHash,
952
1007
  timeoutMs: input.timeoutMs,
953
1008
  expectedHostedLaunchState: anonymousExpectedState,
@@ -958,7 +1013,7 @@ async function runUploadedHostedLoadCheck(input) {
958
1013
  }
959
1014
  else {
960
1015
  result = await runHostedLoadCheck({
961
- targetUrl: appendLoadCheckParams(preview.launchUrl, 'viewer'),
1016
+ targetUrl: withRequestedPhase(appendLoadCheckParams(preview.launchUrl, 'viewer')),
962
1017
  artifactFingerprint: preview.session.manifestHash,
963
1018
  timeoutMs: input.timeoutMs,
964
1019
  expectedHostedLaunchState: resolvePostAuthHostedLaunchState(preview.localAppMetadata.controllerMode),
@@ -972,13 +1027,14 @@ async function runUploadedHostedLoadCheck(input) {
972
1027
  playtestTape: input.playtestTape,
973
1028
  playtestSurface: captureSurface,
974
1029
  postReadyWaitMs: input.postReadyWaitMs,
1030
+ verifyEditorPhaseRoundTrip: input.verifyEditorPhaseRoundTrip,
975
1031
  });
976
1032
  }
977
1033
  }
978
1034
  }
979
1035
  else {
980
1036
  result = await runHostedLoadCheck({
981
- targetUrl: appendLoadCheckParams(preview.launchUrl, 'anonymous'),
1037
+ targetUrl: withRequestedPhase(appendLoadCheckParams(preview.launchUrl, 'anonymous')),
982
1038
  artifactFingerprint: preview.session.manifestHash,
983
1039
  timeoutMs: input.timeoutMs,
984
1040
  expectedHostedLaunchState: anonymousExpectedState,
@@ -989,6 +1045,7 @@ async function runUploadedHostedLoadCheck(input) {
989
1045
  playtestTape: input.playtestTape,
990
1046
  playtestSurface: captureSurface,
991
1047
  postReadyWaitMs: input.postReadyWaitMs,
1048
+ verifyEditorPhaseRoundTrip: input.verifyEditorPhaseRoundTrip,
992
1049
  });
993
1050
  }
994
1051
  const recorded = await input.client.recordAppUploadLaunchCheck(input.creatorUsername, input.task.name, input.sessionId, {
@@ -257,6 +257,7 @@ function buildStaticAppTask(input) {
257
257
  hostingMode: 'HOSTED',
258
258
  authMode: 'NONE',
259
259
  previewable: true,
260
+ editorSupported: false,
260
261
  ...(buildListing(input.metadata.imagePath, input.primarySurface)
261
262
  ? { listing: buildListing(input.metadata.imagePath, input.primarySurface) }
262
263
  : {}),
@@ -22,8 +22,45 @@ export type AppUploadOptions = {
22
22
  loadCheckTimeoutMs?: number;
23
23
  agentTaskId?: number;
24
24
  playtestProof?: AgentTaskPlaytestProof;
25
- mediaCaptureRequired?: boolean;
26
25
  playtestSmokeCheckRequired?: boolean;
27
26
  captureSession?: TaskCaptureSession | null;
28
27
  };
28
+ type PreparedSessionFile = {
29
+ fileKey: string;
30
+ file: File;
31
+ filename: string;
32
+ contentType: string;
33
+ sizeBytes: number;
34
+ sha256: string;
35
+ };
36
+ type PreparedOwnedAssetUpload = {
37
+ uploadKey: string;
38
+ runtimeKey?: string;
39
+ name: string;
40
+ displayName?: string;
41
+ description?: string;
42
+ category: string;
43
+ subcategory: string | null;
44
+ format: string;
45
+ visibility?: string;
46
+ license: string;
47
+ assetSpec?: string;
48
+ tags: string[];
49
+ shopListed?: boolean;
50
+ shopPriceCredits?: number;
51
+ files: Array<{
52
+ role: string;
53
+ file: File;
54
+ filename: string;
55
+ contentType: string;
56
+ sizeBytes: number;
57
+ sha256: string;
58
+ }>;
59
+ };
60
+ type PreparedAppUploadFiles = {
61
+ sessionFiles: PreparedSessionFile[];
62
+ ownedAssets: PreparedOwnedAssetUpload[];
63
+ };
64
+ export declare function prepareAppUploadFiles(task: AppTask, artifacts: AppBuildArtifacts | null): Promise<PreparedAppUploadFiles>;
29
65
  export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
66
+ export {};
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareAppUploadFiles = prepareAppUploadFiles;
3
4
  exports.uploadApp = uploadApp;
4
5
  const config_1 = require("@playdrop/config");
5
6
  const types_1 = require("@playdrop/types");
@@ -8,7 +9,6 @@ const node_fs_1 = require("node:fs");
8
9
  const node_path_1 = require("node:path");
9
10
  const http_1 = require("../http");
10
11
  const loadCheck_1 = require("./loadCheck");
11
- const listingPreflight_1 = require("../listingPreflight");
12
12
  const EXTENSION_TO_MIME = {
13
13
  '.png': 'image/png',
14
14
  '.jpg': 'image/jpeg',
@@ -149,37 +149,20 @@ function pushPreparedArtifact(task, currentTotal, file, maxBytes, code, label) {
149
149
  const bytes = ensurePreparedArtifactSize(file, maxBytes, code, label);
150
150
  return accumulateUploadBytes(task.name, currentTotal, bytes);
151
151
  }
152
- async function uploadApp(client, task, artifacts, options) {
153
- // Version is required - no legacy fallback
154
- if (!task.version) {
155
- throw new Error(`Missing "version" field in catalogue.json for app "${task.name}".\n` +
156
- `Add a version field like "version": "1.0.0" to upload your app.`);
157
- }
158
- return uploadAppVersion(client, task, artifacts, options);
159
- }
160
- /**
161
- * Upload an app version using the versioned API.
162
- */
163
- async function uploadAppVersion(client, task, artifacts, options) {
164
- const creatorUsername = typeof options?.creatorUsername === 'string' ? options.creatorUsername.trim() : '';
165
- if (!creatorUsername) {
166
- throw new Error(`Missing creator username for app "${task.name}" upload.`);
167
- }
152
+ async function prepareAppUploadFiles(task, artifacts) {
168
153
  let totalUploadBytes = 0;
169
- const achievements = task.achievements ?? [];
170
- const leaderboards = task.leaderboards ?? [];
171
154
  const listing = task.listing;
172
- const preparedSessionFiles = [];
173
- const preparedOwnedAssets = await Promise.all(task.ownedAssets.map((ownedAsset) => prepareOwnedAssetUpload(ownedAsset)));
174
- for (const ownedAsset of preparedOwnedAssets) {
155
+ const sessionFiles = [];
156
+ const ownedAssets = await Promise.all(task.ownedAssets.map((ownedAsset) => prepareOwnedAssetUpload(ownedAsset)));
157
+ for (const ownedAsset of ownedAssets) {
175
158
  for (const file of ownedAsset.files) {
176
159
  totalUploadBytes = accumulateUploadBytes(task.name, totalUploadBytes, file.sizeBytes);
177
160
  }
178
161
  }
179
- for (const definition of achievements) {
162
+ for (const definition of task.achievements ?? []) {
180
163
  const prepared = createFileFromPath(definition.iconPath, 'image/png', config_1.MAX_VERSION_ICON_BYTES, 'achievement_icon_too_large', 'invalid_achievement_icon_content_type', `Achievement icon "${definition.key}"`, isPngSignature);
181
164
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
182
- preparedSessionFiles.push(await buildPreparedSessionFile(`achievementIcon:${definition.key}`, prepared.file));
165
+ sessionFiles.push(await buildPreparedSessionFile(`achievementIcon:${definition.key}`, prepared.file));
183
166
  }
184
167
  if (task.hostingMode === 'EXTERNAL') {
185
168
  // no hosted files
@@ -187,48 +170,70 @@ async function uploadAppVersion(client, task, artifacts, options) {
187
170
  else if (artifacts) {
188
171
  totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.bundleFile, config_1.MAX_VERSION_BUNDLE_BYTES, 'bundle_too_large', 'Bundle');
189
172
  totalUploadBytes = pushPreparedArtifact(task, totalUploadBytes, artifacts.sourceFile, config_1.MAX_APP_SOURCE_BYTES, 'source_too_large', 'Source archive');
190
- preparedSessionFiles.push(await buildPreparedSessionFile('bundle', artifacts.bundleFile));
191
- preparedSessionFiles.push(await buildPreparedSessionFile('source', artifacts.sourceFile));
173
+ sessionFiles.push(await buildPreparedSessionFile('bundle', artifacts.bundleFile));
174
+ sessionFiles.push(await buildPreparedSessionFile('source', artifacts.sourceFile));
192
175
  }
193
176
  if (listing?.iconPath) {
194
177
  const prepared = createFileFromPath(listing.iconPath, 'image/png', config_1.MAX_VERSION_ICON_BYTES, 'icon_too_large', 'invalid_icon_content_type', 'Icon', isPngSignature);
195
178
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
196
- preparedSessionFiles.push(await buildPreparedSessionFile('icon', prepared.file));
179
+ sessionFiles.push(await buildPreparedSessionFile('icon', prepared.file));
197
180
  }
198
181
  if (listing?.heroPortraitPath) {
199
182
  const prepared = createFileFromPath(listing.heroPortraitPath, 'image/png', config_1.MAX_VERSION_HERO_BYTES, 'hero_portrait_too_large', 'invalid_hero_portrait_content_type', 'Hero portrait', isPngSignature);
200
183
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
201
- preparedSessionFiles.push(await buildPreparedSessionFile('heroPortrait', prepared.file));
184
+ sessionFiles.push(await buildPreparedSessionFile('heroPortrait', prepared.file));
202
185
  }
203
186
  if (listing?.heroLandscapePath) {
204
187
  const prepared = createFileFromPath(listing.heroLandscapePath, 'image/png', config_1.MAX_VERSION_HERO_BYTES, 'hero_landscape_too_large', 'invalid_hero_landscape_content_type', 'Hero landscape', isPngSignature);
205
188
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
206
- preparedSessionFiles.push(await buildPreparedSessionFile('heroLandscape', prepared.file));
189
+ sessionFiles.push(await buildPreparedSessionFile('heroLandscape', prepared.file));
207
190
  }
208
191
  for (let index = 0; index < (listing?.screenshotPortraitPaths ?? []).length; index += 1) {
209
192
  const filePath = listing.screenshotPortraitPaths[index];
210
193
  const prepared = createFileFromPath(filePath, 'image/png', config_1.MAX_VERSION_SCREENSHOT_BYTES, 'screenshot_portrait_too_large', 'invalid_screenshot_portrait_content_type', `Portrait screenshot ${index + 1}`, isPngSignature);
211
194
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
212
- preparedSessionFiles.push(await buildPreparedSessionFile(`screenshotsPortrait:${index}`, prepared.file));
195
+ sessionFiles.push(await buildPreparedSessionFile(`screenshotsPortrait:${index}`, prepared.file));
213
196
  }
214
197
  for (let index = 0; index < (listing?.screenshotLandscapePaths ?? []).length; index += 1) {
215
198
  const filePath = listing.screenshotLandscapePaths[index];
216
199
  const prepared = createFileFromPath(filePath, 'image/png', config_1.MAX_VERSION_SCREENSHOT_BYTES, 'screenshot_landscape_too_large', 'invalid_screenshot_landscape_content_type', `Landscape screenshot ${index + 1}`, isPngSignature);
217
200
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
218
- preparedSessionFiles.push(await buildPreparedSessionFile(`screenshotsLandscape:${index}`, prepared.file));
201
+ sessionFiles.push(await buildPreparedSessionFile(`screenshotsLandscape:${index}`, prepared.file));
219
202
  }
220
203
  for (let index = 0; index < (listing?.videoPortraitPaths ?? []).length; index += 1) {
221
204
  const filePath = listing.videoPortraitPaths[index];
222
205
  const prepared = createFileFromPath(filePath, 'video/mp4', config_1.MAX_VERSION_VIDEO_BYTES, 'video_portrait_too_large', 'invalid_video_portrait_content_type', `Portrait video ${index + 1}`, isMp4Signature);
223
206
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
224
- preparedSessionFiles.push(await buildPreparedSessionFile(`videosPortrait:${index}`, prepared.file));
207
+ sessionFiles.push(await buildPreparedSessionFile(`videosPortrait:${index}`, prepared.file));
225
208
  }
226
209
  for (let index = 0; index < (listing?.videoLandscapePaths ?? []).length; index += 1) {
227
210
  const filePath = listing.videoLandscapePaths[index];
228
211
  const prepared = createFileFromPath(filePath, 'video/mp4', config_1.MAX_VERSION_VIDEO_BYTES, 'video_landscape_too_large', 'invalid_video_landscape_content_type', `Landscape video ${index + 1}`, isMp4Signature);
229
212
  totalUploadBytes = pushPreparedFile(task.name, totalUploadBytes, prepared);
230
- preparedSessionFiles.push(await buildPreparedSessionFile(`videosLandscape:${index}`, prepared.file));
213
+ sessionFiles.push(await buildPreparedSessionFile(`videosLandscape:${index}`, prepared.file));
231
214
  }
215
+ return { sessionFiles, ownedAssets };
216
+ }
217
+ async function uploadApp(client, task, artifacts, options) {
218
+ // Version is required - no legacy fallback
219
+ if (!task.version) {
220
+ throw new Error(`Missing "version" field in catalogue.json for app "${task.name}".\n` +
221
+ `Add a version field like "version": "1.0.0" to upload your app.`);
222
+ }
223
+ return uploadAppVersion(client, task, artifacts, options);
224
+ }
225
+ /**
226
+ * Upload an app version using the versioned API.
227
+ */
228
+ async function uploadAppVersion(client, task, artifacts, options) {
229
+ const creatorUsername = typeof options?.creatorUsername === 'string' ? options.creatorUsername.trim() : '';
230
+ if (!creatorUsername) {
231
+ throw new Error(`Missing creator username for app "${task.name}" upload.`);
232
+ }
233
+ const achievements = task.achievements ?? [];
234
+ const leaderboards = task.leaderboards ?? [];
235
+ const listing = task.listing;
236
+ const { sessionFiles: preparedSessionFiles, ownedAssets: preparedOwnedAssets, } = await prepareAppUploadFiles(task, artifacts);
232
237
  const initializeRequest = {
233
238
  version: task.version,
234
239
  agentTaskId: options?.agentTaskId,
@@ -259,6 +264,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
259
264
  authMode: task.authMode,
260
265
  controllerMode: task.controllerMode,
261
266
  previewable: task.previewable,
267
+ editorSupported: task.editorSupported,
262
268
  skipReview: options?.skipReview,
263
269
  displayName: task.displayName,
264
270
  subtitle: task.subtitle,
@@ -346,15 +352,6 @@ async function uploadAppVersion(client, task, artifacts, options) {
346
352
  })),
347
353
  });
348
354
  }
349
- if (options?.mediaCaptureRequired === true
350
- && task.hostingMode !== 'EXTERNAL'
351
- && !task.externalUrl) {
352
- const mediaCaptureProof = (0, listingPreflight_1.readMediaCaptureProof)({
353
- task,
354
- preparedSessionFiles,
355
- });
356
- await client.recordAppUploadMediaCapture(creatorUsername, task.name, sessionId, mediaCaptureProof);
357
- }
358
355
  if (task.hostingMode !== 'EXTERNAL'
359
356
  && !task.externalUrl
360
357
  && options?.playtestSmokeCheckRequired) {
@@ -363,7 +360,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
363
360
  creatorUsername,
364
361
  task,
365
362
  sessionId,
366
- timeoutMs: options.loadCheckTimeoutMs,
363
+ timeoutMs: options?.loadCheckTimeoutMs,
367
364
  token: options?.token ?? null,
368
365
  currentUser: options?.user ?? null,
369
366
  captureSession: options?.captureSession,
@@ -377,7 +374,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
377
374
  creatorUsername,
378
375
  task,
379
376
  sessionId,
380
- timeoutMs: options.loadCheckTimeoutMs,
377
+ timeoutMs: options?.loadCheckTimeoutMs,
381
378
  token: options?.token ?? null,
382
379
  currentUser: options?.user ?? null,
383
380
  captureSession: options?.captureSession,
@@ -386,6 +383,23 @@ async function uploadAppVersion(client, task, artifacts, options) {
386
383
  throw new Error((0, loadCheck_1.formatHostedLoadCheckFailure)(task.name, loadCheck, 'staged'));
387
384
  }
388
385
  }
386
+ if (task.editorSupported && task.hostingMode !== 'EXTERNAL' && !task.externalUrl) {
387
+ const editorLoadCheck = await (0, loadCheck_1.runUploadedHostedLoadCheck)({
388
+ client,
389
+ creatorUsername,
390
+ task,
391
+ sessionId,
392
+ timeoutMs: options?.loadCheckTimeoutMs,
393
+ token: options?.token ?? null,
394
+ currentUser: options?.user ?? null,
395
+ captureSession: options?.captureSession,
396
+ phase: 'editor',
397
+ verifyEditorPhaseRoundTrip: true,
398
+ });
399
+ if (editorLoadCheck.status !== 'PASSED') {
400
+ throw new Error((0, loadCheck_1.formatHostedLoadCheckFailure)(task.name, editorLoadCheck, 'editor'));
401
+ }
402
+ }
389
403
  }
390
404
  const finalized = initialized.status === 'finalized'
391
405
  ? initialized.finalized
@@ -63,6 +63,7 @@ export type AppCatalogueEntry = {
63
63
  authMode?: string;
64
64
  controllerMode?: string;
65
65
  previewable?: boolean;
66
+ editorSupported?: boolean;
66
67
  externalUrl?: string;
67
68
  listing?: AppListingConfig;
68
69
  username?: string;
@@ -210,6 +211,7 @@ export type AppTask = {
210
211
  authMode?: AppAuthMode;
211
212
  controllerMode?: AppControllerMode;
212
213
  previewable?: boolean;
214
+ editorSupported?: boolean;
213
215
  externalUrl?: string;
214
216
  listing?: ResolvedListingAssets;
215
217
  username?: string;
@@ -368,6 +370,7 @@ export type ValidatedAppMetadata = {
368
370
  surfaceTargetMap: SurfaceTargetsMap;
369
371
  primarySurface?: AppSurface;
370
372
  playtestTapes?: AppPlaytestTapes;
373
+ editorSupported?: boolean;
371
374
  missingFields: string[];
372
375
  warnings: string[];
373
376
  errors: string[];
package/dist/catalogue.js CHANGED
@@ -948,6 +948,18 @@ function validateAppMetadata(entry) {
948
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
949
  }
950
950
  }
951
+ let editorSupported;
952
+ if (Object.prototype.hasOwnProperty.call(entry, 'editorSupported')) {
953
+ if (typeof entry.editorSupported !== 'boolean') {
954
+ errors.push('editorSupported must be true or false');
955
+ }
956
+ else {
957
+ editorSupported = entry.editorSupported;
958
+ if (editorSupported && entry.hostingMode === 'EXTERNAL') {
959
+ errors.push('editorSupported can only be true for HOSTED apps, not external apps');
960
+ }
961
+ }
962
+ }
951
963
  let surfaceTargetsList = [];
952
964
  let surfaceTargetsMap = { desktop: false, mobileLandscape: false, mobilePortrait: false };
953
965
  try {
@@ -992,6 +1004,7 @@ function validateAppMetadata(entry) {
992
1004
  surfaceTargetMap: surfaceTargetsMap,
993
1005
  primarySurface,
994
1006
  playtestTapes,
1007
+ editorSupported,
995
1008
  missingFields,
996
1009
  warnings,
997
1010
  errors,
@@ -1204,6 +1217,14 @@ function buildAppTasks(rootDir, catalogues, options) {
1204
1217
  }
1205
1218
  previewable = Boolean(entry.previewable);
1206
1219
  }
1220
+ let editorSupported;
1221
+ if (Object.prototype.hasOwnProperty.call(entry, 'editorSupported')) {
1222
+ if (typeof entry.editorSupported !== 'boolean') {
1223
+ errors.push(`[${label}] Skipping ${rawName}: editorSupported must be true or false.`);
1224
+ continue;
1225
+ }
1226
+ editorSupported = Boolean(entry.editorSupported);
1227
+ }
1207
1228
  let externalUrl;
1208
1229
  const rawExternalUrl = typeof entry.externalUrl === 'string' ? entry.externalUrl.trim() : '';
1209
1230
  if (rawExternalUrl) {
@@ -1226,6 +1247,10 @@ function buildAppTasks(rootDir, catalogues, options) {
1226
1247
  errors.push(`[${label}] Skipping ${rawName}: previewable can only be true for HOSTED apps.`);
1227
1248
  continue;
1228
1249
  }
1250
+ if ((hostingMode ?? 'HOSTED') === 'EXTERNAL' && editorSupported) {
1251
+ errors.push(`[${label}] Skipping ${rawName}: editorSupported can only be true for HOSTED apps.`);
1252
+ continue;
1253
+ }
1229
1254
  const defaultLicense = (hostingMode ?? 'HOSTED') === 'EXTERNAL' ? 'CLOSED' : types_1.DEFAULT_CONTENT_LICENSE;
1230
1255
  const license = normalizeContentLicenseValue(entry.license, errors, `${label} app "${rawName}"`, APP_ALLOWED_LICENSES, defaultLicense);
1231
1256
  if (!license) {
@@ -1600,6 +1625,7 @@ function buildAppTasks(rootDir, catalogues, options) {
1600
1625
  authMode,
1601
1626
  controllerMode,
1602
1627
  previewable,
1628
+ editorSupported,
1603
1629
  externalUrl,
1604
1630
  listing,
1605
1631
  // Admin-only field