@playdrop/playdrop-cli 0.9.6 → 0.10.1

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 (73) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apiClient.d.ts +10 -0
  3. package/dist/apiClient.js +55 -2
  4. package/dist/appUrls.d.ts +1 -0
  5. package/dist/appUrls.js +9 -0
  6. package/dist/apps/build.js +39 -28
  7. package/dist/apps/index.d.ts +1 -0
  8. package/dist/apps/index.js +2 -0
  9. package/dist/apps/launchCheck.d.ts +2 -0
  10. package/dist/apps/launchCheck.js +31 -6
  11. package/dist/apps/registration.d.ts +1 -0
  12. package/dist/apps/registration.js +1 -0
  13. package/dist/apps/upload.d.ts +1 -0
  14. package/dist/apps/upload.js +4 -17
  15. package/dist/captureRuntime.d.ts +14 -0
  16. package/dist/captureRuntime.js +329 -0
  17. package/dist/catalogue.d.ts +4 -2
  18. package/dist/catalogue.js +50 -7
  19. package/dist/commandContext.js +61 -4
  20. package/dist/commands/capture.d.ts +1 -0
  21. package/dist/commands/capture.js +30 -13
  22. package/dist/commands/captureRemote.d.ts +2 -0
  23. package/dist/commands/captureRemote.js +90 -0
  24. package/dist/commands/create.d.ts +0 -1
  25. package/dist/commands/create.js +2 -151
  26. package/dist/commands/creations.d.ts +0 -13
  27. package/dist/commands/creations.js +0 -141
  28. package/dist/commands/dev.d.ts +2 -1
  29. package/dist/commands/dev.js +23 -6
  30. package/dist/commands/devServer.js +3 -1
  31. package/dist/commands/generation.d.ts +1 -0
  32. package/dist/commands/generation.js +274 -0
  33. package/dist/commands/review.d.ts +46 -0
  34. package/dist/commands/review.js +353 -0
  35. package/dist/commands/upload.d.ts +27 -1
  36. package/dist/commands/upload.js +962 -21
  37. package/dist/commands/validate.js +5 -0
  38. package/dist/commands/worker/runtime.d.ts +81 -0
  39. package/dist/commands/worker/runtime.js +458 -0
  40. package/dist/commands/worker.d.ts +158 -0
  41. package/dist/commands/worker.js +2626 -0
  42. package/dist/config.d.ts +2 -0
  43. package/dist/config.js +23 -0
  44. package/dist/index.js +116 -30
  45. package/dist/shellProbe.d.ts +1 -1
  46. package/dist/shellProbe.js +3 -3
  47. package/dist/workspaceAuth.d.ts +3 -0
  48. package/dist/workspaceAuth.js +14 -0
  49. package/node_modules/@playdrop/api-client/dist/client.d.ts +36 -15
  50. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/client.js +2 -2
  52. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +5 -2
  53. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/admin.js +51 -3
  55. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +75 -0
  56. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -0
  57. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +478 -0
  58. package/node_modules/@playdrop/api-client/dist/index.d.ts +36 -15
  59. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  60. package/node_modules/@playdrop/api-client/dist/index.js +153 -42
  61. package/node_modules/@playdrop/config/client-meta.json +2 -2
  62. package/node_modules/@playdrop/types/dist/api.d.ts +662 -75
  63. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  64. package/node_modules/@playdrop/types/dist/api.js +100 -9
  65. package/node_modules/@playdrop/types/dist/app.d.ts +2 -0
  66. package/node_modules/@playdrop/types/dist/app.d.ts.map +1 -1
  67. package/node_modules/@playdrop/types/dist/app.js +3 -0
  68. package/node_modules/@playdrop/types/dist/version.d.ts +1 -0
  69. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  70. package/package.json +2 -1
  71. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts +0 -46
  72. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.d.ts.map +0 -1
  73. package/node_modules/@playdrop/api-client/dist/domains/game-ideas.js +0 -177
@@ -1,9 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.browseCreations = browseCreations;
4
- exports.browseCreationIdeas = browseCreationIdeas;
5
- exports.startCreationIdea = startCreationIdea;
6
- exports.shipCreationIdea = shipCreationIdea;
7
4
  exports.updateCreationApp = updateCreationApp;
8
5
  exports.deleteCreationApp = deleteCreationApp;
9
6
  exports.setCurrentCreationAppVersion = setCurrentCreationAppVersion;
@@ -29,7 +26,6 @@ const messages_1 = require("../messages");
29
26
  const output_1 = require("../output");
30
27
  const refs_1 = require("../refs");
31
28
  const MERGED_PAGE_SIZE = 100;
32
- const IDEA_STATE_VALUES = new Set(['idea', 'started', 'shipped', 'all']);
33
29
  function parseBrowseKind(raw) {
34
30
  if (!raw || raw.trim().length === 0) {
35
31
  return 'all';
@@ -53,19 +49,6 @@ function parsePositiveInteger(raw, label, fallback) {
53
49
  }
54
50
  return parsed;
55
51
  }
56
- function parseIdeaState(raw) {
57
- if (!raw || raw.trim().length === 0) {
58
- return 'IDEA';
59
- }
60
- const normalized = raw.trim().toLowerCase();
61
- if (!IDEA_STATE_VALUES.has(normalized)) {
62
- return null;
63
- }
64
- if (normalized === 'all') {
65
- return 'ALL';
66
- }
67
- return normalized.toUpperCase();
68
- }
69
52
  async function resolveCreator(client, rawCreator, command) {
70
53
  let creator;
71
54
  try {
@@ -311,130 +294,6 @@ async function browseCreations(options = {}) {
311
294
  }
312
295
  });
313
296
  }
314
- async function browseCreationIdeas(options = {}) {
315
- const state = parseIdeaState(options.state);
316
- if (!state) {
317
- (0, messages_1.printErrorWithHelp)('The --state value is invalid.', ['Use one of: idea, started, shipped, all.'], { command: 'creations ideas browse' });
318
- process.exitCode = 1;
319
- return;
320
- }
321
- const limit = parsePositiveInteger(options.limit, 'limit', 20);
322
- if (limit === null) {
323
- (0, messages_1.printErrorWithHelp)('The --limit value must be a positive integer.', ['Example: --limit 20'], { command: 'creations ideas browse' });
324
- process.exitCode = 1;
325
- return;
326
- }
327
- const offset = parsePositiveInteger(options.offset, 'offset', 0);
328
- if (offset === null) {
329
- (0, messages_1.printErrorWithHelp)('The --offset value must be zero or a positive integer.', ['Example: --offset 20'], { command: 'creations ideas browse' });
330
- process.exitCode = 1;
331
- return;
332
- }
333
- await (0, commandContext_1.withEnvironment)('creations ideas browse', 'Browsing your game ideas', async ({ client }) => {
334
- try {
335
- const response = await client.listGameIdeas({ state, limit, offset });
336
- if (options.json) {
337
- (0, output_1.printJson)(response);
338
- return;
339
- }
340
- if (response.gameIdeas.length === 0) {
341
- console.log('No game ideas found.');
342
- console.log('Next: create one at https://playdrop.ai/create.');
343
- return;
344
- }
345
- console.log('Game ideas:\n');
346
- response.gameIdeas.forEach((idea, index) => {
347
- const tags = idea.tagRefs.length > 0 ? ` | ${idea.tagRefs.join(', ')}` : '';
348
- const app = idea.createdAppRef ? ` | app ${idea.createdAppRef}` : '';
349
- console.log(`${index + 1}. ${idea.slug} | ${idea.displayName} | ${idea.state.toLowerCase()}${tags}${app}`);
350
- });
351
- if (response.pagination.hasMore) {
352
- console.log(`\nNext page: playdrop creations ideas browse --state ${String(options.state ?? 'idea')} --offset ${response.pagination.offset + response.pagination.limit}`);
353
- }
354
- }
355
- catch (error) {
356
- const handled = (0, errors_1.handleCommandFailure)(error, 'creations ideas browse', 'Game idea lookup', {
357
- apiMessage: (apiError) => ({
358
- problem: `Game idea lookup failed with status ${apiError.status}.`,
359
- suggestions: ['Run "playdrop auth login" and retry.'],
360
- }),
361
- });
362
- if (!handled) {
363
- throw error;
364
- }
365
- }
366
- });
367
- }
368
- async function startCreationIdea(idOrSlug, options = {}) {
369
- const ref = typeof idOrSlug === 'string' ? idOrSlug.trim() : '';
370
- if (!ref) {
371
- (0, messages_1.printErrorWithHelp)('Provide a game idea id or slug.', ['Example: playdrop creations ideas start foxfire-kitchen'], { command: 'creations ideas start' });
372
- process.exitCode = 1;
373
- return;
374
- }
375
- await (0, commandContext_1.withEnvironment)('creations ideas start', 'Marking game idea started', async ({ client }) => {
376
- try {
377
- const response = await client.startGameIdea(ref);
378
- if (options.json) {
379
- (0, output_1.printJson)(response);
380
- return;
381
- }
382
- (0, output_1.printSuccess)(`Marked ${response.gameIdea.slug} as started.`, [
383
- `Next: build and publish the app, then run "playdrop creations ideas ship ${response.gameIdea.slug} --app <creator>/<name>".`,
384
- ]);
385
- }
386
- catch (error) {
387
- const handled = (0, errors_1.handleCommandFailure)(error, 'creations ideas start', 'Game idea update', {
388
- apiMessage: (apiError) => ({
389
- problem: `Game idea update failed with status ${apiError.status}.`,
390
- suggestions: ['Confirm the id or slug, then retry.'],
391
- }),
392
- });
393
- if (!handled) {
394
- throw error;
395
- }
396
- }
397
- });
398
- }
399
- async function shipCreationIdea(idOrSlug, options = {}) {
400
- const ref = typeof idOrSlug === 'string' ? idOrSlug.trim() : '';
401
- const appRef = typeof options.app === 'string' ? options.app.trim() : '';
402
- if (!ref) {
403
- (0, messages_1.printErrorWithHelp)('Provide a game idea id or slug.', ['Example: playdrop creations ideas ship foxfire-kitchen --app olivier/foxfire-kitchen'], { command: 'creations ideas ship' });
404
- process.exitCode = 1;
405
- return;
406
- }
407
- if (!appRef) {
408
- (0, messages_1.printErrorWithHelp)('Provide --app <creator>/<name>.', ['Example: --app olivier/foxfire-kitchen'], { command: 'creations ideas ship' });
409
- process.exitCode = 1;
410
- return;
411
- }
412
- await (0, commandContext_1.withEnvironment)('creations ideas ship', 'Marking game idea shipped', async ({ client }) => {
413
- try {
414
- const response = await client.shipGameIdea(ref, { app: appRef });
415
- if (options.json) {
416
- (0, output_1.printJson)(response);
417
- return;
418
- }
419
- (0, output_1.printSuccess)(`Marked ${response.gameIdea.slug} as shipped.`, [
420
- response.gameIdea.createdAppRef
421
- ? `Linked app: ${response.gameIdea.createdAppRef}.`
422
- : `Linked app: ${appRef}.`,
423
- ]);
424
- }
425
- catch (error) {
426
- const handled = (0, errors_1.handleCommandFailure)(error, 'creations ideas ship', 'Game idea update', {
427
- apiMessage: (apiError) => ({
428
- problem: `Game idea update failed with status ${apiError.status}.`,
429
- suggestions: ['Confirm the id or slug and --app value, then retry.'],
430
- }),
431
- });
432
- if (!handled) {
433
- throw error;
434
- }
435
- }
436
- });
437
- }
438
297
  async function updateCreationApp(nameArg, options = {}) {
439
298
  const name = parseManagedName(nameArg, 'creations apps update');
440
299
  if (!name) {
@@ -2,7 +2,8 @@ export declare function formatDevRuntimeAssetManifestFailure(error: unknown): {
2
2
  message: string;
3
3
  suggestions: string[];
4
4
  };
5
- export declare function dev(targetArg: string | undefined, _port?: number, appOption?: string, devOptions?: {
5
+ export declare function dev(targetArg: string | undefined, port?: number | string, appOption?: string, devOptions?: {
6
6
  devAuth?: string;
7
7
  player?: string | number;
8
8
  }): Promise<void>;
9
+ export declare function resolveDevRouterPort(value: number | string | null | undefined): number;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatDevRuntimeAssetManifestFailure = formatDevRuntimeAssetManifestFailure;
4
4
  exports.dev = dev;
5
+ exports.resolveDevRouterPort = resolveDevRouterPort;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_path_1 = require("node:path");
7
8
  const types_1 = require("@playdrop/types");
@@ -133,7 +134,8 @@ function formatDevRuntimeAssetManifestFailure(error) {
133
134
  ],
134
135
  };
135
136
  }
136
- async function dev(targetArg, _port, appOption, devOptions = {}) {
137
+ async function dev(targetArg, port, appOption, devOptions = {}) {
138
+ const devRouterPort = resolveDevRouterPort(port ?? process.env.PLAYDROP_DEV_ROUTER_PORT);
137
139
  let resolvedTarget;
138
140
  try {
139
141
  resolvedTarget = (0, devShared_1.resolveDevTarget)(targetArg, appOption);
@@ -298,7 +300,7 @@ async function dev(targetArg, _port, appOption, devOptions = {}) {
298
300
  creatorUsername: currentUsername,
299
301
  appType: appTypeSlug,
300
302
  appName,
301
- port: devServer_1.DEV_ROUTER_PORT,
303
+ port: devRouterPort,
302
304
  });
303
305
  let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
304
306
  if (taskLookup.task) {
@@ -367,7 +369,7 @@ async function dev(targetArg, _port, appOption, devOptions = {}) {
367
369
  appType: appTypeSlug,
368
370
  creatorUsername: currentUsername,
369
371
  htmlPath: filePath,
370
- port: devServer_1.DEV_ROUTER_PORT,
372
+ port: devRouterPort,
371
373
  projectInfo,
372
374
  runtimeAssetManifest,
373
375
  });
@@ -383,13 +385,13 @@ async function dev(targetArg, _port, appOption, devOptions = {}) {
383
385
  ], { command: 'project dev' });
384
386
  }
385
387
  else if (message.startsWith('dev_router_incompatible:')) {
386
- (0, messages_1.printErrorWithHelp)(`An incompatible shared dev router is already running on port ${devServer_1.DEV_ROUTER_PORT}.`, [
388
+ (0, messages_1.printErrorWithHelp)(`An incompatible shared dev router is already running on port ${devRouterPort}.`, [
387
389
  'Stop the existing dev router process, then retry "playdrop project dev".',
388
390
  `If needed, rebuild the local CLI first with "npm run build --workspace @playdrop/playdrop-cli" from /Users/oliviermichon/Documents/playdrop.`,
389
391
  ], { command: 'project dev' });
390
392
  }
391
393
  else {
392
- (0, messages_1.printErrorWithHelp)(error?.message || `Failed to start the shared dev router on port ${devServer_1.DEV_ROUTER_PORT}.`, [
394
+ (0, messages_1.printErrorWithHelp)(error?.message || `Failed to start the shared dev router on port ${devRouterPort}.`, [
393
395
  'Close the conflicting process or wait for the stale mount to exit.',
394
396
  'Ensure the app HTML file exists and is readable.',
395
397
  ], { command: 'project dev' });
@@ -425,7 +427,12 @@ async function dev(targetArg, _port, appOption, devOptions = {}) {
425
427
  }
426
428
  }
427
429
  if (webUrl) {
428
- const iframeUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(`${webUrl}/creators/${encodeURIComponent(currentUsername)}/apps/${appTypeSlug}/${encodeURIComponent(appName)}/dev`, devAuthSelection);
430
+ const iframeUrl = (0, devAuth_1.applyHostedDevAuthSelectionToUrl)((0, appUrls_1.buildPlatformDevUrl)(webUrl, {
431
+ creatorUsername: currentUsername,
432
+ appType: appTypeSlug,
433
+ appName,
434
+ localDevPort: devRouterPort === devServer_1.DEV_ROUTER_PORT ? null : devRouterPort,
435
+ }), devAuthSelection);
429
436
  console.log('\nTest your app at:');
430
437
  console.log(` ${iframeUrl}`);
431
438
  console.log('\nMake something fun then share it with');
@@ -436,3 +443,13 @@ async function dev(targetArg, _port, appOption, devOptions = {}) {
436
443
  }
437
444
  }, { workspacePath });
438
445
  }
446
+ function resolveDevRouterPort(value) {
447
+ if (value === null || value === undefined || String(value).trim() === '') {
448
+ return devServer_1.DEV_ROUTER_PORT;
449
+ }
450
+ const parsed = typeof value === 'number' ? value : Number.parseInt(String(value), 10);
451
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
452
+ throw new Error('invalid_dev_router_port');
453
+ }
454
+ return parsed;
455
+ }
@@ -413,11 +413,13 @@ async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
413
413
  throw new Error(`dev_router_incompatible:port_${port}`);
414
414
  }
415
415
  const cliEntrypoint = getCliEntrypointPath();
416
+ const childEnv = { ...process.env };
417
+ delete childEnv.PLAYDROP_WORKER_CONTEXT;
416
418
  const child = (0, node_child_process_1.spawn)(process.execPath, [cliEntrypoint, 'project', '_dev-router', 'serve'], {
417
419
  detached: true,
418
420
  stdio: 'ignore',
419
421
  env: {
420
- ...process.env,
422
+ ...childEnv,
421
423
  PLAYDROP_DEV_ROUTER_PORT: String(port),
422
424
  },
423
425
  });
@@ -24,6 +24,7 @@ type AiGenerateOptions = {
24
24
  texture?: boolean;
25
25
  timeoutSeconds?: string | number;
26
26
  pollIntervalMs?: string | number;
27
+ output?: string;
27
28
  json?: boolean;
28
29
  };
29
30
  type AiListOptions = {
@@ -1,10 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.resolveImageAttachment = resolveImageAttachment;
4
37
  exports.generate = generate;
5
38
  exports.browseGenerationJobs = browseGenerationJobs;
6
39
  exports.showGenerationJob = showGenerationJob;
7
40
  exports.generations = generations;
41
+ const config_1 = require("@playdrop/config");
8
42
  const types_1 = require("@playdrop/types");
9
43
  const node_fs_1 = require("node:fs");
10
44
  const node_path_1 = require("node:path");
@@ -40,6 +74,8 @@ const LOCAL_IMAGE_MIME_BY_EXTENSION = {
40
74
  '.jpeg': 'image/jpeg',
41
75
  '.webp': 'image/webp',
42
76
  };
77
+ const OUTPUT_IMAGE_MIME_BY_EXTENSION = LOCAL_IMAGE_MIME_BY_EXTENSION;
78
+ const GENERATED_PNG_OUTPUT_MAX_BYTES = config_1.MAX_VERSION_HERO_BYTES;
43
79
  function isPlaydropSpeechVoiceId(value) {
44
80
  return types_1.PLAYDROP_SPEECH_VOICE_IDS.includes(value);
45
81
  }
@@ -596,6 +632,211 @@ function printGenerateResult(modality, payload) {
596
632
  console.log('Generated Asset: none');
597
633
  }
598
634
  }
635
+ let pngOutputCrcTable = null;
636
+ function getPngOutputCrcTable() {
637
+ if (pngOutputCrcTable) {
638
+ return pngOutputCrcTable;
639
+ }
640
+ const table = new Uint32Array(256);
641
+ for (let n = 0; n < 256; n++) {
642
+ let c = n;
643
+ for (let k = 0; k < 8; k++) {
644
+ c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
645
+ }
646
+ table[n] = c >>> 0;
647
+ }
648
+ pngOutputCrcTable = table;
649
+ return table;
650
+ }
651
+ function pngOutputCrc32(buffer) {
652
+ const table = getPngOutputCrcTable();
653
+ let crc = 0xffffffff;
654
+ for (let index = 0; index < buffer.length; index++) {
655
+ const byte = buffer[index];
656
+ crc = table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
657
+ }
658
+ return (crc ^ 0xffffffff) >>> 0;
659
+ }
660
+ function createPngOutputTextChunk(keyword, value) {
661
+ const keywordText = keyword.trim().slice(0, 79);
662
+ if (!/^[\x20-\x7e]+$/.test(keywordText) || keywordText.includes('\0')) {
663
+ throw new Error('ai_generation_image_output_metadata_failed');
664
+ }
665
+ const textValue = value.replace(/[^\x20-\xff]/g, ' ').slice(0, 4096);
666
+ const data = Buffer.concat([
667
+ Buffer.from(keywordText, 'latin1'),
668
+ Buffer.from([0]),
669
+ Buffer.from(textValue, 'latin1'),
670
+ ]);
671
+ const type = Buffer.from('tEXt', 'ascii');
672
+ const length = Buffer.alloc(4);
673
+ length.writeUInt32BE(data.length, 0);
674
+ const crc = Buffer.alloc(4);
675
+ crc.writeUInt32BE(pngOutputCrc32(Buffer.concat([type, data])), 0);
676
+ return Buffer.concat([length, type, data, crc]);
677
+ }
678
+ function extractGeneratedImageUrl(payload) {
679
+ if (typeof payload?.imageUrl === 'string' && payload.imageUrl.trim()) {
680
+ return payload.imageUrl.trim();
681
+ }
682
+ if (typeof payload?.outputImage === 'string' && payload.outputImage.trim()) {
683
+ return payload.outputImage.trim();
684
+ }
685
+ return null;
686
+ }
687
+ function embedGeneratedImageOutputMetadata(buffer, inputPrompt, payload) {
688
+ if (!isPngSignature(buffer)) {
689
+ return buffer;
690
+ }
691
+ const generationId = typeof payload?.billing?.generationId === 'string'
692
+ ? payload.billing.generationId
693
+ : typeof payload?.generationId === 'string'
694
+ ? payload.generationId
695
+ : '';
696
+ const chunks = [
697
+ createPngOutputTextChunk('playdrop:cliOutputTool', 'playdrop ai create image --output'),
698
+ createPngOutputTextChunk('playdrop:cliOutputPrompt', inputPrompt),
699
+ ...(generationId ? [createPngOutputTextChunk('playdrop:generationId', generationId)] : []),
700
+ ];
701
+ const signature = buffer.subarray(0, 8);
702
+ const parts = [signature];
703
+ let offset = 8;
704
+ let inserted = false;
705
+ while (offset + 12 <= buffer.length) {
706
+ const length = buffer.readUInt32BE(offset);
707
+ const chunkEnd = offset + 12 + length;
708
+ if (chunkEnd > buffer.length) {
709
+ throw new Error('ai_generation_image_output_metadata_failed');
710
+ }
711
+ const chunkType = buffer.toString('ascii', offset + 4, offset + 8);
712
+ if (chunkType === 'IEND' && !inserted) {
713
+ parts.push(...chunks);
714
+ inserted = true;
715
+ }
716
+ parts.push(buffer.subarray(offset, chunkEnd));
717
+ offset = chunkEnd;
718
+ if (chunkType === 'IEND') {
719
+ break;
720
+ }
721
+ }
722
+ if (!inserted) {
723
+ throw new Error('ai_generation_image_output_metadata_failed');
724
+ }
725
+ return Buffer.concat(parts);
726
+ }
727
+ async function writeGeneratedImageOutput(payload, outputPath, inputPrompt) {
728
+ const imageUrl = extractGeneratedImageUrl(payload);
729
+ if (!imageUrl) {
730
+ throw new Error('ai_generation_image_output_missing');
731
+ }
732
+ const response = await fetch(imageUrl);
733
+ if (!response.ok) {
734
+ throw new Error(`ai_generation_image_output_fetch_failed:${response.status}`);
735
+ }
736
+ const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
737
+ if (!contentType.startsWith('image/')) {
738
+ throw new Error(`ai_generation_image_output_invalid_content_type:${contentType || 'unknown'}`);
739
+ }
740
+ const buffer = Buffer.from(await response.arrayBuffer());
741
+ if (buffer.length === 0) {
742
+ throw new Error('ai_generation_image_output_empty');
743
+ }
744
+ const normalizedBuffer = await normalizeGeneratedImageOutputBuffer(buffer, outputPath);
745
+ const outputBuffer = embedGeneratedImageOutputMetadata(normalizedBuffer, inputPrompt, payload);
746
+ const resolvedPath = (0, node_path_1.resolve)(process.cwd(), outputPath);
747
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(resolvedPath), { recursive: true });
748
+ (0, node_fs_1.writeFileSync)(resolvedPath, outputBuffer);
749
+ console.log(`Saved Image: ${resolvedPath}`);
750
+ }
751
+ async function normalizeGeneratedImageOutputBuffer(buffer, outputPath) {
752
+ const extension = (0, node_path_1.extname)(outputPath).toLowerCase();
753
+ const targetMimeType = OUTPUT_IMAGE_MIME_BY_EXTENSION[extension];
754
+ if (!targetMimeType) {
755
+ return buffer;
756
+ }
757
+ const detectedMimeType = detectLocalImageMimeType(buffer);
758
+ if (!detectedMimeType) {
759
+ throw new Error('ai_generation_image_output_unsupported_type');
760
+ }
761
+ if (detectedMimeType === targetMimeType) {
762
+ if (targetMimeType === 'image/png' && buffer.length > GENERATED_PNG_OUTPUT_MAX_BYTES) {
763
+ return optimizeGeneratedPngOutputBuffer(buffer);
764
+ }
765
+ return buffer;
766
+ }
767
+ let sharp;
768
+ try {
769
+ const sharpModule = await Promise.resolve().then(() => __importStar(require('sharp')));
770
+ sharp = sharpModule.default ?? sharpModule;
771
+ }
772
+ catch {
773
+ throw new Error('ai_generation_image_output_conversion_unavailable');
774
+ }
775
+ try {
776
+ if (targetMimeType === 'image/png') {
777
+ return await optimizeGeneratedPngOutputBuffer(buffer, sharp);
778
+ }
779
+ const pipeline = sharp(buffer, { failOn: 'error' });
780
+ if (targetMimeType === 'image/jpeg') {
781
+ return await pipeline.jpeg({ quality: 92 }).toBuffer();
782
+ }
783
+ if (targetMimeType === 'image/webp') {
784
+ return await pipeline.webp({ quality: 92 }).toBuffer();
785
+ }
786
+ }
787
+ catch {
788
+ throw new Error('ai_generation_image_output_conversion_failed');
789
+ }
790
+ return buffer;
791
+ }
792
+ async function optimizeGeneratedPngOutputBuffer(buffer, loadedSharp) {
793
+ let sharp = loadedSharp;
794
+ if (!sharp) {
795
+ try {
796
+ const sharpModule = await Promise.resolve().then(() => __importStar(require('sharp')));
797
+ sharp = sharpModule.default ?? sharpModule;
798
+ }
799
+ catch {
800
+ throw new Error('ai_generation_image_output_conversion_unavailable');
801
+ }
802
+ }
803
+ const metadata = await sharp(buffer, { failOn: 'error' }).metadata();
804
+ const sourceWidth = typeof metadata.width === 'number' && metadata.width > 0 ? metadata.width : null;
805
+ const sourceHeight = typeof metadata.height === 'number' && metadata.height > 0 ? metadata.height : null;
806
+ const attempts = [
807
+ { scale: 1, palette: false, quality: 100 },
808
+ { scale: 1, palette: true, quality: 92 },
809
+ { scale: 0.9, palette: true, quality: 88 },
810
+ { scale: 0.8, palette: true, quality: 84 },
811
+ { scale: 0.7, palette: true, quality: 80 },
812
+ { scale: 0.6, palette: true, quality: 76 },
813
+ { scale: 0.5, palette: true, quality: 72 },
814
+ ];
815
+ let smallest = null;
816
+ for (const attempt of attempts) {
817
+ let pipeline = sharp(buffer, { failOn: 'error' }).rotate();
818
+ if (attempt.scale < 1 && sourceWidth && sourceHeight) {
819
+ pipeline = pipeline.resize({
820
+ width: Math.max(1, Math.round(sourceWidth * attempt.scale)),
821
+ height: Math.max(1, Math.round(sourceHeight * attempt.scale)),
822
+ fit: 'inside',
823
+ withoutEnlargement: true,
824
+ });
825
+ }
826
+ const output = await pipeline.png({
827
+ compressionLevel: 9,
828
+ adaptiveFiltering: true,
829
+ ...(attempt.palette ? { palette: true, quality: attempt.quality } : {}),
830
+ }).toBuffer();
831
+ if (!smallest || output.length < smallest.length) {
832
+ smallest = output;
833
+ }
834
+ if (output.length <= GENERATED_PNG_OUTPUT_MAX_BYTES) {
835
+ return output;
836
+ }
837
+ }
838
+ throw new Error(`ai_generation_image_output_too_large:${smallest?.length ?? buffer.length}:${GENERATED_PNG_OUTPUT_MAX_BYTES}`);
839
+ }
599
840
  function formatJobStatus(job) {
600
841
  const progress = typeof job.progressPercent === 'number'
601
842
  ? ` progress=${Math.max(0, Math.min(100, Math.round(job.progressPercent)))}%`
@@ -624,6 +865,16 @@ async function generate(typeInput, promptInput, options = {}) {
624
865
  process.exitCode = 1;
625
866
  return;
626
867
  }
868
+ const outputPath = typeof options.output === 'string' && options.output.trim().length > 0
869
+ ? options.output.trim()
870
+ : null;
871
+ if (outputPath && parsed.modality !== 'IMAGE') {
872
+ (0, messages_1.printErrorWithHelp)('The --output option is only supported for image generation.', ['Use "playdrop ai create image ... --output <file>".'], {
873
+ command: 'ai create',
874
+ });
875
+ process.exitCode = 1;
876
+ return;
877
+ }
627
878
  await (0, commandContext_1.withEnvironment)('ai create', 'Generating AI assets', async ({ aiClient }) => {
628
879
  try {
629
880
  const created = await aiClient.createGenerationJob(parsed.request);
@@ -656,6 +907,9 @@ async function generate(typeInput, promptInput, options = {}) {
656
907
  throw new Error(`${errorCode}: ${errorMessage}`);
657
908
  }
658
909
  const payload = (job.resultData && typeof job.resultData === 'object') ? job.resultData : {};
910
+ if (outputPath) {
911
+ await writeGeneratedImageOutput(payload, outputPath, parsed.request.input);
912
+ }
659
913
  if (options.json) {
660
914
  (0, output_1.printJson)({ job, result: payload });
661
915
  return;
@@ -711,6 +965,26 @@ function handleAiFailure(error, command, context) {
711
965
  process.exitCode = 1;
712
966
  return true;
713
967
  }
968
+ if (message === 'ai_generation_image_output_unsupported_type') {
969
+ (0, messages_1.printErrorWithHelp)('AI create returned an image format that Playdrop CLI could not identify.', ['Retry the command. If the problem persists, contact Playdrop support.'], { command });
970
+ process.exitCode = 1;
971
+ return true;
972
+ }
973
+ if (message === 'ai_generation_image_output_conversion_unavailable') {
974
+ (0, messages_1.printErrorWithHelp)('AI create could not convert the generated image to the requested output file type.', ['Install the Playdrop CLI dependencies again, then retry.'], { command });
975
+ process.exitCode = 1;
976
+ return true;
977
+ }
978
+ if (message === 'ai_generation_image_output_conversion_failed') {
979
+ (0, messages_1.printErrorWithHelp)('AI create failed while converting the generated image to the requested output file type.', ['Retry with a matching output extension such as .jpg, or retry the generation.'], { command });
980
+ process.exitCode = 1;
981
+ return true;
982
+ }
983
+ if (message === 'ai_generation_image_output_metadata_failed') {
984
+ (0, messages_1.printErrorWithHelp)('AI create failed while writing PlayDrop metadata to the generated image.', ['Retry the generation. The output image must keep prompt metadata for worker validation.'], { command });
985
+ process.exitCode = 1;
986
+ return true;
987
+ }
714
988
  if (message.startsWith('ai_generation_job_timeout:')) {
715
989
  const detail = message.split(':').slice(1).join(':').trim();
716
990
  (0, messages_1.printErrorWithHelp)(detail || 'AI create timed out while waiting for the job to finish.', ['Retry the command with a higher --timeout value if the job normally takes longer.'], { command });
@@ -0,0 +1,46 @@
1
+ export declare const REVIEW_CRITERIA: readonly ["Gameplay / Core Loop", "Depth / Replayability", "Controls / Input", "UX / Usability", "First Time User Experience", "Visuals / Art Direction", "Audio / Feedback", "Store Listing & Metadata Accuracy", "Safety / Age Rating / Compliance", "Performance / Stability"];
2
+ export declare const REQUIRED_REVIEW_EVIDENCE_FILES: string[];
3
+ export type ValidateGameReviewResultInput = {
4
+ creatorFeedback?: string;
5
+ evidenceDir: string;
6
+ reviewMessage: string;
7
+ reviewState: string;
8
+ };
9
+ export type ReviewValidationResult = {
10
+ average?: number;
11
+ evidenceDir?: string;
12
+ outcome?: string;
13
+ primarySurface?: string;
14
+ reason?: string;
15
+ scoreCount?: number;
16
+ skipped: boolean;
17
+ };
18
+ export type ReviewValidateResultOptions = {
19
+ creatorFeedbackFile?: string;
20
+ evidenceDir: string;
21
+ messageFile: string;
22
+ state: string;
23
+ };
24
+ export type ReviewComposeEvidenceOptions = {
25
+ core?: string;
26
+ win?: string;
27
+ loss?: string;
28
+ out: string;
29
+ };
30
+ export type ReviewRatingCardOptions = {
31
+ reviewMessageFile: string;
32
+ out: string;
33
+ punchline?: string;
34
+ title?: string;
35
+ };
36
+ export declare function validateGameReviewResult(input: ValidateGameReviewResultInput): Promise<ReviewValidationResult>;
37
+ export declare function validateReviewResultCommand(options: ReviewValidateResultOptions): Promise<void>;
38
+ export declare function composeReviewEvidence(options: ReviewComposeEvidenceOptions): Promise<{
39
+ outputPath: string;
40
+ imageCount: number;
41
+ }>;
42
+ export declare function createReviewRatingCard(options: ReviewRatingCardOptions): Promise<{
43
+ outputPath: string;
44
+ width: number;
45
+ height: number;
46
+ }>;