@playdrop/playdrop-cli 0.7.0 → 0.7.2

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 (72) hide show
  1. package/README.md +1 -0
  2. package/config/client-meta.json +9 -9
  3. package/dist/commands/devBrowser.d.ts +7 -0
  4. package/dist/commands/devBrowser.js +360 -0
  5. package/dist/commands/devServer.js +28 -1
  6. package/dist/commands/generation.d.ts +3 -0
  7. package/dist/commands/generation.js +84 -4
  8. package/dist/commands/versionsBrowse.js +63 -4
  9. package/dist/environment.d.ts +1 -1
  10. package/dist/environment.js +24 -1
  11. package/dist/index.js +32 -2
  12. package/dist/playwright.d.ts +7 -0
  13. package/dist/playwright.js +21 -2
  14. package/node_modules/@playdrop/ai-client/dist/index.d.ts +41 -5
  15. package/node_modules/@playdrop/ai-client/dist/index.d.ts.map +1 -1
  16. package/node_modules/@playdrop/ai-client/dist/index.js +11 -1
  17. package/node_modules/@playdrop/ai-client/package.json +1 -1
  18. package/node_modules/@playdrop/api-client/dist/client.d.ts +10 -2
  19. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  20. package/node_modules/@playdrop/api-client/dist/client.js +74 -0
  21. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  22. package/node_modules/@playdrop/api-client/dist/core/request.js +64 -12
  23. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +1 -2
  24. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/domains/admin.js +0 -11
  26. package/node_modules/@playdrop/api-client/dist/domains/comments.d.ts +4 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/comments.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/domains/comments.js +34 -0
  29. package/node_modules/@playdrop/api-client/dist/index.d.ts +11 -4
  30. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  31. package/node_modules/@playdrop/api-client/dist/index.js +27 -3
  32. package/node_modules/@playdrop/api-client/package.json +1 -1
  33. package/node_modules/@playdrop/boxel-core/package.json +3 -4
  34. package/node_modules/@playdrop/boxel-three/package.json +1 -1
  35. package/node_modules/@playdrop/config/client-meta.json +9 -9
  36. package/node_modules/@playdrop/config/dist/src/deployment.d.ts +27 -0
  37. package/node_modules/@playdrop/config/dist/src/deployment.d.ts.map +1 -0
  38. package/node_modules/@playdrop/config/dist/src/deployment.js +199 -0
  39. package/node_modules/@playdrop/config/dist/src/index.d.ts +2 -0
  40. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  41. package/node_modules/@playdrop/config/dist/src/index.js +2 -0
  42. package/node_modules/@playdrop/config/dist/src/public-deployment.d.ts +26 -0
  43. package/node_modules/@playdrop/config/dist/src/public-deployment.d.ts.map +1 -0
  44. package/node_modules/@playdrop/config/dist/src/public-deployment.js +148 -0
  45. package/node_modules/@playdrop/config/dist/src/server/fastify.d.ts +60 -1
  46. package/node_modules/@playdrop/config/dist/src/server/fastify.d.ts.map +1 -1
  47. package/node_modules/@playdrop/config/dist/src/server/fastify.js +119 -0
  48. package/node_modules/@playdrop/config/dist/src/server/logging.d.ts +3 -0
  49. package/node_modules/@playdrop/config/dist/src/server/logging.d.ts.map +1 -0
  50. package/node_modules/@playdrop/config/dist/src/server/logging.js +46 -0
  51. package/node_modules/@playdrop/config/dist/test/deployment.test.d.ts +2 -0
  52. package/node_modules/@playdrop/config/dist/test/deployment.test.d.ts.map +1 -0
  53. package/node_modules/@playdrop/config/dist/test/deployment.test.js +74 -0
  54. package/node_modules/@playdrop/config/dist/test/logging.test.d.ts +2 -0
  55. package/node_modules/@playdrop/config/dist/test/logging.test.d.ts.map +1 -0
  56. package/node_modules/@playdrop/config/dist/test/logging.test.js +17 -0
  57. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  58. package/node_modules/@playdrop/config/package.json +14 -2
  59. package/node_modules/@playdrop/types/dist/api.d.ts +27 -10
  60. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  61. package/node_modules/@playdrop/types/dist/api.js +11 -0
  62. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +1 -0
  63. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  64. package/node_modules/@playdrop/types/dist/asset.d.ts +36 -4
  65. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  66. package/node_modules/@playdrop/types/dist/asset.js +20 -1
  67. package/node_modules/@playdrop/types/dist/version.d.ts +3 -1
  68. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  69. package/node_modules/@playdrop/types/dist/version.js +2 -0
  70. package/node_modules/@playdrop/types/package.json +1 -1
  71. package/node_modules/@playdrop/vox-three/package.json +1 -1
  72. package/package.json +3 -3
package/README.md CHANGED
@@ -61,6 +61,7 @@ playdrop credits balance
61
61
  playdrop notifications browse
62
62
  playdrop creations browse
63
63
  playdrop ai create image "Pixel art hero portrait"
64
+ playdrop ai create image "Wide fantasy vista" --ratio 16:9 --size 4K
64
65
  playdrop project init .
65
66
  playdrop project create app my-app --template playdrop/template/html_template
66
67
  playdrop project dev my-app
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.7.0",
3
- "build": 1,
2
+ "version": "0.7.2",
3
+ "build": 12,
4
4
  "platforms": {
5
5
  "ios": {
6
6
  "minimumVersion": "16.0"
@@ -26,12 +26,12 @@
26
26
  },
27
27
  "clients": {
28
28
  "web": {
29
- "minimumVersion": "0.7.0",
30
- "minimumBuild": 1
29
+ "minimumVersion": "0.7.2",
30
+ "minimumBuild": 12
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.7.0",
34
- "minimumBuild": 1
33
+ "minimumVersion": "0.7.2",
34
+ "minimumBuild": 12
35
35
  },
36
36
  "apple": {
37
37
  "minimumVersion": "0.3.10",
@@ -46,11 +46,11 @@
46
46
  "minimumBuild": 1
47
47
  },
48
48
  "android-games": {
49
- "minimumVersion": "0.1.0",
50
- "minimumBuild": 1
49
+ "minimumVersion": "0.7.1",
50
+ "minimumBuild": 5
51
51
  },
52
52
  "cli": {
53
- "minimumVersion": "0.7.0"
53
+ "minimumVersion": "0.7.2"
54
54
  }
55
55
  }
56
56
  }
@@ -0,0 +1,7 @@
1
+ type DevBrowserOptions = {
2
+ app?: string;
3
+ devAuth?: string;
4
+ player?: string | number;
5
+ };
6
+ export declare function devBrowser(targetArg: string | undefined, options?: DevBrowserOptions): Promise<void>;
7
+ export {};
@@ -0,0 +1,360 @@
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.devBrowser = devBrowser;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = require("node:path");
10
+ const types_1 = require("@playdrop/types");
11
+ const commandContext_1 = require("../commandContext");
12
+ const http_1 = require("../http");
13
+ const messages_1 = require("../messages");
14
+ const catalogue_1 = require("../catalogue");
15
+ const catalogue_utils_1 = require("../catalogue-utils");
16
+ const devShared_1 = require("./devShared");
17
+ const appUrls_1 = require("../appUrls");
18
+ const devServer_1 = require("./devServer");
19
+ const devRuntimeAssets_1 = require("./devRuntimeAssets");
20
+ const devAuth_1 = require("../devAuth");
21
+ const dev_1 = require("./dev");
22
+ const playwright_1 = require("../playwright");
23
+ function resolveDevBrowserAuthSelection(options) {
24
+ const normalizedMode = options.devAuth?.trim().toLowerCase() || 'prompt';
25
+ if (options.player !== undefined && normalizedMode !== 'player') {
26
+ throw new Error('dev_auth_player_requires_player_mode');
27
+ }
28
+ return (0, devAuth_1.parseHostedDevAuthSelection)({
29
+ devAuth: options.devAuth,
30
+ player: options.player,
31
+ defaultMode: 'prompt',
32
+ allowPrompt: true,
33
+ });
34
+ }
35
+ function waitForContextClose(context) {
36
+ return new Promise((resolve) => {
37
+ context.once?.('close', () => {
38
+ resolve();
39
+ });
40
+ });
41
+ }
42
+ async function devBrowser(targetArg, options = {}) {
43
+ let resolvedTarget;
44
+ let devAuthSelection;
45
+ try {
46
+ resolvedTarget = (0, devShared_1.resolveDevTarget)(targetArg, options.app);
47
+ devAuthSelection = resolveDevBrowserAuthSelection(options);
48
+ }
49
+ catch (error) {
50
+ const message = error?.message || 'Unable to start the dev browser.';
51
+ if (message === 'dev_auth_player_requires_player_mode') {
52
+ (0, messages_1.printErrorWithHelp)('Use --player only with --dev-auth player.', [], { command: 'project dev-browser' });
53
+ process.exitCode = 1;
54
+ return;
55
+ }
56
+ if (message === 'dev_auth_player_slot_required') {
57
+ (0, messages_1.printErrorWithHelp)('Use --player 1, 2, 3, or 4 when --dev-auth player is selected.', [], { command: 'project dev-browser' });
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+ const code = error?.code;
62
+ let suggestions = [];
63
+ switch (code) {
64
+ case 'app_required':
65
+ suggestions = ['Use "--app <name>" to select which app to run.'];
66
+ break;
67
+ case 'app_not_found':
68
+ suggestions = ['Check the app name in catalogue.json or run "playdrop project create app" to register a new app.'];
69
+ break;
70
+ case 'catalogue_missing':
71
+ suggestions = ['Run "playdrop project dev-browser" from a workspace with catalogue.json or provide the path to an HTML file.'];
72
+ break;
73
+ case 'invalid_target':
74
+ suggestions = ['Provide a path to an HTML file or directory containing a catalogue.json.'];
75
+ break;
76
+ case 'empty_catalogue':
77
+ suggestions = ['Add apps to catalogue.json before running "playdrop project dev-browser".'];
78
+ break;
79
+ case 'invalid_catalogue':
80
+ suggestions = ['Fix the catalogue.json file and rerun "playdrop project dev-browser".'];
81
+ break;
82
+ default:
83
+ suggestions = ['Use --dev-auth prompt, anonymous, viewer, or player.', 'If you choose --dev-auth player, also pass --player 1, 2, 3, or 4.'];
84
+ break;
85
+ }
86
+ (0, messages_1.printErrorWithHelp)(message, suggestions, { command: 'project dev-browser' });
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ let appName = resolvedTarget.appName;
91
+ const filePath = resolvedTarget.htmlPath;
92
+ if (!(0, node_fs_1.existsSync)(filePath) || !(0, node_fs_1.statSync)(filePath).isFile()) {
93
+ (0, messages_1.printErrorWithHelp)(`App HTML was not found at ${filePath}.`, ['Ensure the catalogue entry points to a compiled HTML file or provide the correct file path.'], { command: 'project dev-browser' });
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+ let appTypeSlug = 'game';
98
+ try {
99
+ const match = (0, catalogue_utils_1.findAppDefinition)(filePath);
100
+ appName = match.name;
101
+ appTypeSlug = (0, appUrls_1.getAppTypeSlug)(match.type);
102
+ }
103
+ catch (error) {
104
+ if (resolvedTarget.cataloguePath) {
105
+ (0, messages_1.printErrorWithHelp)(error?.message || 'Catalogue lookup failed.', [
106
+ 'Ensure the HTML file is listed exactly once in catalogue.json with a valid type.',
107
+ 'Run "playdrop project create app <name>" to scaffold a new entry if needed.',
108
+ ], { command: 'project dev-browser' });
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ }
113
+ const workspacePath = resolvedTarget.cataloguePath ?? (0, node_path_1.dirname)(filePath);
114
+ await (0, commandContext_1.withEnvironment)('project dev-browser', 'Launching the Playdrop dev browser', async ({ client, env, envConfig, account }) => {
115
+ let currentUsername = account?.username?.trim() ?? '';
116
+ if (!currentUsername) {
117
+ try {
118
+ currentUsername = await (0, devShared_1.fetchDevUsername)(client);
119
+ }
120
+ catch (error) {
121
+ if (error instanceof http_1.CLIUnsupportedClientError) {
122
+ return;
123
+ }
124
+ if (error instanceof types_1.UnsupportedClientError) {
125
+ (0, http_1.handleUnsupportedError)(error, 'Authentication');
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ if (error instanceof types_1.ApiError) {
130
+ (0, messages_1.printErrorWithHelp)(`Could not fetch your account (status ${error.status}).`, [
131
+ 'Run "playdrop auth login" to refresh your session and ensure the API is reachable.',
132
+ 'Use "playdrop auth whoami" afterwards to confirm your status.',
133
+ ], { command: 'project dev-browser' });
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ if ((0, devShared_1.isNetworkError)(error)) {
138
+ (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to resolve your account.', 'project dev-browser');
139
+ process.exitCode = 1;
140
+ return;
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+ if (resolvedTarget.cataloguePath) {
146
+ try {
147
+ await (0, devShared_1.assertAppRegistered)(client, currentUsername, appName);
148
+ }
149
+ catch (error) {
150
+ if (error instanceof http_1.CLIUnsupportedClientError) {
151
+ return;
152
+ }
153
+ if (error instanceof types_1.UnsupportedClientError) {
154
+ (0, http_1.handleUnsupportedError)(error, 'Dev browser');
155
+ process.exitCode = 1;
156
+ return;
157
+ }
158
+ if (error instanceof types_1.ApiError) {
159
+ if (error.status === 404) {
160
+ (0, messages_1.printErrorWithHelp)(`App ${currentUsername}/${appName} is not registered on ${env}.`, [
161
+ `Run "playdrop project create app ${appName}" to register the app before opening the dev browser.`,
162
+ 'If you expected it to exist, ensure you are logged into the correct environment.',
163
+ ], { command: 'project dev-browser' });
164
+ }
165
+ else {
166
+ (0, messages_1.printErrorWithHelp)(`Failed to verify app registration (status ${error.status}).`, ['Retry in a moment.', 'If the issue persists, contact the Playdrop team.'], { command: 'project dev-browser' });
167
+ }
168
+ process.exitCode = 1;
169
+ return;
170
+ }
171
+ if ((0, devShared_1.isNetworkError)(error)) {
172
+ (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to verify the app registration.', 'project dev-browser');
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ throw error;
177
+ }
178
+ }
179
+ const projectInfo = (0, devShared_1.findProjectInfo)(filePath);
180
+ const devScriptAvailable = Boolean(projectInfo.projectDir
181
+ && projectInfo.packageJson
182
+ && typeof projectInfo.packageJson.scripts?.dev === 'string');
183
+ const taskLookup = (0, catalogue_1.findAppTaskByFile)(filePath);
184
+ if (taskLookup.errors.length > 0) {
185
+ (0, messages_1.printErrorWithHelp)(taskLookup.errors[0] || 'Failed to resolve the app task from catalogue.json.', taskLookup.errors.slice(1), { command: 'project dev-browser' });
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+ const localDevAppUrl = (0, devServer_1.buildLocalDevAppUrl)({
190
+ creatorUsername: currentUsername,
191
+ appType: appTypeSlug,
192
+ appName,
193
+ port: devServer_1.DEV_ROUTER_PORT,
194
+ });
195
+ let runtimeAssetManifest = (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)();
196
+ if (taskLookup.task) {
197
+ try {
198
+ runtimeAssetManifest = await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
199
+ client,
200
+ apiBase: envConfig.apiBase,
201
+ task: taskLookup.task,
202
+ creatorUsername: currentUsername,
203
+ appBaseUrl: new URL('.', localDevAppUrl).toString(),
204
+ });
205
+ }
206
+ catch (error) {
207
+ const formatted = (0, dev_1.formatDevRuntimeAssetManifestFailure)(error);
208
+ (0, messages_1.printErrorWithHelp)(formatted.message, formatted.suggestions, { command: 'project dev-browser' });
209
+ process.exitCode = 1;
210
+ return;
211
+ }
212
+ }
213
+ let handle;
214
+ try {
215
+ handle = await (0, devServer_1.startDevServer)({
216
+ appName,
217
+ appType: appTypeSlug,
218
+ creatorUsername: currentUsername,
219
+ htmlPath: filePath,
220
+ port: devServer_1.DEV_ROUTER_PORT,
221
+ projectInfo,
222
+ runtimeAssetManifest,
223
+ });
224
+ }
225
+ catch (error) {
226
+ const message = typeof error?.message === 'string' ? error.message : '';
227
+ const mountConflict = (0, devServer_1.parseMountConflictError)(message);
228
+ if (mountConflict) {
229
+ (0, messages_1.printErrorWithHelp)(`A different dev session already owns ${mountConflict.ref}.`, [
230
+ `Active owner pid: ${mountConflict.ownerPid}.`,
231
+ `Mounted repo root: ${mountConflict.repoRoot}.`,
232
+ `Mounted HTML path: ${mountConflict.htmlPath}.`,
233
+ ], { command: 'project dev-browser' });
234
+ }
235
+ else if (message.startsWith('dev_router_incompatible:')) {
236
+ (0, messages_1.printErrorWithHelp)(`An incompatible shared dev router is already running on port ${devServer_1.DEV_ROUTER_PORT}.`, [
237
+ 'Stop the existing dev router process, then retry "playdrop project dev-browser".',
238
+ 'If needed, rebuild the local CLI first with "npm run build --workspace @playdrop/playdrop-cli".',
239
+ ], { command: 'project dev-browser' });
240
+ }
241
+ else {
242
+ (0, messages_1.printErrorWithHelp)(error?.message || `Failed to start the shared dev router on port ${devServer_1.DEV_ROUTER_PORT}.`, [
243
+ 'Close the conflicting process or wait for the stale mount to exit.',
244
+ 'Ensure the app HTML file exists and is readable.',
245
+ ], { command: 'project dev-browser' });
246
+ }
247
+ process.exitCode = 1;
248
+ return;
249
+ }
250
+ const devUrl = (0, appUrls_1.buildPlatformDevUrl)(envConfig.webBase, {
251
+ creatorUsername: currentUsername,
252
+ appName,
253
+ appType: appTypeSlug,
254
+ devAuth: devAuthSelection.devAuth,
255
+ player: devAuthSelection.player ? String(devAuthSelection.player) : null,
256
+ });
257
+ let launchUrl = devUrl;
258
+ if (devAuthSelection.devAuth !== 'anonymous') {
259
+ try {
260
+ const target = new URL(devUrl);
261
+ const launch = await client.startCliWebLaunch({
262
+ redirectPath: `${target.pathname}${target.search}`,
263
+ });
264
+ launchUrl = launch.launchUrl;
265
+ }
266
+ catch (error) {
267
+ await handle.close().catch(() => { });
268
+ if (error instanceof http_1.CLIUnsupportedClientError) {
269
+ return;
270
+ }
271
+ if (error instanceof types_1.UnsupportedClientError) {
272
+ (0, http_1.handleUnsupportedError)(error, 'Dev browser launch');
273
+ process.exitCode = 1;
274
+ return;
275
+ }
276
+ if (error instanceof types_1.ApiError) {
277
+ (0, messages_1.printErrorWithHelp)(`Could not prepare the Playdrop browser session (status ${error.status}).`, ['Run "playdrop auth login" and retry the dev browser command.'], { command: 'project dev-browser' });
278
+ process.exitCode = 1;
279
+ return;
280
+ }
281
+ if ((0, devShared_1.isNetworkError)(error)) {
282
+ (0, messages_1.printNetworkIssue)('Could not reach the Playdrop API to prepare the browser session.', 'project dev-browser');
283
+ process.exitCode = 1;
284
+ return;
285
+ }
286
+ throw error;
287
+ }
288
+ }
289
+ const entryLabel = (0, node_path_1.relative)(process.cwd(), filePath) || filePath;
290
+ const runtimeRootLabel = (0, node_path_1.relative)(process.cwd(), (0, node_path_1.dirname)(filePath)) || (0, node_path_1.dirname)(filePath);
291
+ const profileDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)(node_os_1.default.tmpdir(), 'playdrop-dev-browser-'));
292
+ console.log(`App dev router mounted at ${handle.appUrl}`);
293
+ console.log(`Serving ${entryLabel}`);
294
+ console.log(`Static root ${runtimeRootLabel}`);
295
+ if (projectInfo.projectDir && !devScriptAvailable && projectInfo.packageJsonPath) {
296
+ const projectLabel = (0, devShared_1.formatProjectLabel)(projectInfo);
297
+ if (projectLabel) {
298
+ console.log(`package.json detected at ${projectLabel}, but no "dev" script was found. Run your build/watch scripts manually if needed.`);
299
+ }
300
+ }
301
+ console.log(`Dev URL: ${devUrl}`);
302
+ if (launchUrl !== devUrl) {
303
+ console.log(`Launch URL: ${launchUrl}`);
304
+ }
305
+ console.log('Opening an isolated Chromium dev window.');
306
+ console.log('Close the browser window or press Ctrl+C to stop.');
307
+ let context = null;
308
+ let cleanedUp = false;
309
+ const cleanup = async () => {
310
+ if (cleanedUp) {
311
+ return;
312
+ }
313
+ cleanedUp = true;
314
+ process.off('SIGINT', handleSignal);
315
+ process.off('SIGTERM', handleSignal);
316
+ if (context) {
317
+ try {
318
+ await context.close();
319
+ }
320
+ catch {
321
+ // Ignore browser shutdown noise during cleanup.
322
+ }
323
+ }
324
+ try {
325
+ await handle.close();
326
+ }
327
+ finally {
328
+ (0, node_fs_1.rmSync)(profileDir, { recursive: true, force: true });
329
+ }
330
+ };
331
+ const handleSignal = () => {
332
+ void cleanup();
333
+ };
334
+ process.on('SIGINT', handleSignal);
335
+ process.on('SIGTERM', handleSignal);
336
+ try {
337
+ context = await (0, playwright_1.launchPersistentChromiumContext)(profileDir, {
338
+ viewport: { width: 1440, height: 960 },
339
+ });
340
+ }
341
+ catch (error) {
342
+ await cleanup();
343
+ (0, messages_1.printErrorWithHelp)(error?.message || 'Failed to launch the Playdrop dev browser.', ['Install Playwright Chromium with "npx playwright install chromium" and retry.'], { command: 'project dev-browser' });
344
+ process.exitCode = 1;
345
+ return;
346
+ }
347
+ try {
348
+ const page = context.pages()[0] ?? await context.newPage();
349
+ await page.goto(launchUrl, { waitUntil: 'domcontentloaded' });
350
+ await waitForContextClose(context);
351
+ }
352
+ catch (error) {
353
+ await cleanup();
354
+ (0, messages_1.printErrorWithHelp)(error?.message || 'Failed to open the Playdrop dev browser window.', [], { command: 'project dev-browser' });
355
+ process.exitCode = 1;
356
+ return;
357
+ }
358
+ await cleanup();
359
+ }, { workspacePath });
360
+ }
@@ -22,6 +22,10 @@ exports.DEV_ROUTER_PORT = 8888;
22
22
  const DEV_ROUTER_HOST = '127.0.0.1';
23
23
  const CONTROL_PREFIX = '/_playdrop';
24
24
  const DEV_ROUTER_PROTOCOL_VERSION = 3;
25
+ const APPROVED_PLAYDROP_WEB_ORIGINS = new Set([
26
+ 'https://playdrop.ai',
27
+ 'https://www.playdrop.ai',
28
+ ]);
25
29
  const CONTENT_TYPE_BY_EXTENSION = {
26
30
  '.html': 'text/html; charset=utf-8',
27
31
  '.js': 'application/javascript; charset=utf-8',
@@ -42,6 +46,25 @@ const CONTENT_TYPE_BY_EXTENSION = {
42
46
  };
43
47
  const routerMountsById = new Map();
44
48
  const routerMountIdsByKey = new Map();
49
+ function resolveApprovedDevRouterOrigin(originHeader) {
50
+ const origin = originHeader?.trim();
51
+ if (!origin) {
52
+ return null;
53
+ }
54
+ let parsedOrigin;
55
+ try {
56
+ parsedOrigin = new URL(origin);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ if (APPROVED_PLAYDROP_WEB_ORIGINS.has(parsedOrigin.origin)) {
62
+ return parsedOrigin.origin;
63
+ }
64
+ const isLocalWebOrigin = (parsedOrigin.protocol === 'http:' || parsedOrigin.protocol === 'https:')
65
+ && (parsedOrigin.hostname === 'localhost' || parsedOrigin.hostname === '127.0.0.1');
66
+ return isLocalWebOrigin ? parsedOrigin.origin : null;
67
+ }
45
68
  function resolveContentType(filePath) {
46
69
  const extension = (0, node_path_1.extname)(filePath).toLowerCase();
47
70
  return CONTENT_TYPE_BY_EXTENSION[extension] || 'application/octet-stream';
@@ -426,7 +449,11 @@ function createDevRouterServer(initialPort = exports.DEV_ROUTER_PORT) {
426
449
  // eslint-disable-next-line complexity
427
450
  const handleRequest = async (req, res) => {
428
451
  const method = req.method || 'GET';
429
- res.setHeader('Access-Control-Allow-Origin', '*');
452
+ const allowedOrigin = resolveApprovedDevRouterOrigin(typeof req.headers.origin === 'string' ? req.headers.origin : undefined);
453
+ if (allowedOrigin) {
454
+ res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
455
+ }
456
+ res.setHeader('Vary', 'Origin');
430
457
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
431
458
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Playdrop-Client, X-Playdrop-Client-Version, X-Playdrop-Client-Build, X-Playdrop-Platform, X-Playdrop-Platform-Version');
432
459
  res.setHeader('Access-Control-Allow-Private-Network', 'true');
@@ -12,7 +12,10 @@ type AiGenerateOptions = {
12
12
  instrumental?: boolean;
13
13
  loop?: boolean;
14
14
  coverPrompt?: string;
15
+ voice?: string;
16
+ providerVoiceId?: string;
15
17
  ratio?: string;
18
+ size?: string;
16
19
  resolution?: string;
17
20
  sourceMode?: string;
18
21
  topology?: string;
@@ -13,15 +13,18 @@ const errors_1 = require("../errors");
13
13
  const messages_1 = require("../messages");
14
14
  const output_1 = require("../output");
15
15
  const IMAGE_ASPECT_RATIO_SET = new Set(['1:1', '3:4', '9:16', '4:3', '16:9']);
16
+ const IMAGE_SIZE_SET = new Set(['1K', '4K']);
16
17
  const VIDEO_ASPECT_RATIO_SET = new Set(['16:9', '9:16']);
17
18
  const DEFAULT_SUBCATEGORY_BY_MODALITY = {
18
19
  IMAGE: 'generic',
19
20
  MUSIC: 'music',
20
21
  SFX: 'sfx',
22
+ SPEECH: 'speech',
21
23
  VIDEO: 'generic',
22
24
  MODEL_3D: 'generic',
23
25
  };
24
26
  const DEFAULT_IMAGE_ASPECT_RATIO = '1:1';
27
+ const DEFAULT_IMAGE_SIZE = '1K';
25
28
  const DEFAULT_VIDEO_ASPECT_RATIO = '16:9';
26
29
  const DEFAULT_VIDEO_DURATION_SECONDS = 4;
27
30
  const VIDEO_FIRST_LAST_FRAME_DURATION_SECONDS = 8;
@@ -37,11 +40,14 @@ const LOCAL_IMAGE_MIME_BY_EXTENSION = {
37
40
  '.jpeg': 'image/jpeg',
38
41
  '.webp': 'image/webp',
39
42
  };
43
+ function isPlaydropSpeechVoiceId(value) {
44
+ return types_1.PLAYDROP_SPEECH_VOICE_IDS.includes(value);
45
+ }
40
46
  function handleGenerateBuildRequestError(message) {
41
47
  const errorByCode = {
42
48
  missing_type: {
43
49
  title: 'A generation type is required.',
44
- details: ['Use one of: image, music, sfx, video, model_3d.'],
50
+ details: ['Use one of: image, music, sfx, speech, video, model_3d.'],
45
51
  },
46
52
  missing_prompt: {
47
53
  title: 'A prompt is required.',
@@ -49,7 +55,7 @@ function handleGenerateBuildRequestError(message) {
49
55
  },
50
56
  invalid_type: {
51
57
  title: 'The generation type is invalid.',
52
- details: ['Use one of: image, music, sfx, video, model_3d.'],
58
+ details: ['Use one of: image, music, sfx, speech, video, model_3d.'],
53
59
  },
54
60
  invalid_duration: {
55
61
  title: 'The --duration value is invalid.',
@@ -63,6 +69,14 @@ function handleGenerateBuildRequestError(message) {
63
69
  title: 'The --ratio value is invalid.',
64
70
  details: ['Image: 1:1, 3:4, 9:16, 4:3, 16:9. Video: 16:9, 9:16.'],
65
71
  },
72
+ invalid_image_size: {
73
+ title: 'The --size value is invalid.',
74
+ details: ['Image size must be 1K or 4K.'],
75
+ },
76
+ image_size_not_supported_for_modality: {
77
+ title: 'The --size option is only supported for image generation.',
78
+ details: ['Use --size 1K or --size 4K with "playdrop ai create image ...".'],
79
+ },
66
80
  missing_model3d_images: {
67
81
  title: 'Model 3D IMAGE source mode requires at least one reference image.',
68
82
  details: ['Provide --image1 <url> or --image2 <url>, or switch to --source-mode TEXT.'],
@@ -75,6 +89,26 @@ function handleGenerateBuildRequestError(message) {
75
89
  title: 'The --subcategory value is invalid.',
76
90
  details: ['Use a lowercase slug such as generic, music, sfx, or avatar.'],
77
91
  },
92
+ speech_disallows_subcategory: {
93
+ title: 'Speech does not accept --subcategory.',
94
+ details: ['Speech jobs always create AUDIO assets with subcategory speech.'],
95
+ },
96
+ invalid_speech_voice_selection: {
97
+ title: 'Speech voice options are invalid.',
98
+ details: ['Use either --voice <alias> or --provider-voice-id <id>, but not both.'],
99
+ },
100
+ invalid_speech_voice: {
101
+ title: 'The --voice value is invalid.',
102
+ details: [`Use one of: ${types_1.PLAYDROP_SPEECH_VOICE_IDS.join(', ')}.`],
103
+ },
104
+ speech_voice_unavailable: {
105
+ title: 'The provider voice is unavailable.',
106
+ details: ['Use a different --provider-voice-id or a curated --voice alias.'],
107
+ },
108
+ speech_input_too_long: {
109
+ title: 'The speech input is too long.',
110
+ details: [`Use ${types_1.PLAYDROP_SPEECH_MAX_WORDS} words or fewer.`],
111
+ },
78
112
  };
79
113
  const entry = errorByCode[message];
80
114
  if (!entry) {
@@ -139,6 +173,8 @@ function normalizeModality(value) {
139
173
  return 'MUSIC';
140
174
  if (normalized === 'SFX')
141
175
  return 'SFX';
176
+ if (normalized === 'SPEECH')
177
+ return 'SPEECH';
142
178
  if (normalized === 'VIDEO')
143
179
  return 'VIDEO';
144
180
  if (normalized === 'MODEL3D' || normalized === 'MODEL_3D')
@@ -271,6 +307,7 @@ function parseListType(raw) {
271
307
  || normalized === 'image'
272
308
  || normalized === 'music'
273
309
  || normalized === 'sfx'
310
+ || normalized === 'speech'
274
311
  || normalized === 'video'
275
312
  || normalized === 'model_3d') {
276
313
  return normalized;
@@ -369,10 +406,17 @@ function buildGenerateRequest(typeInput, promptInput, options) {
369
406
  if (!modality) {
370
407
  throw new Error('invalid_type');
371
408
  }
372
- const prompt = typeof promptInput === 'string' ? promptInput.trim() : '';
409
+ const prompt = modality === 'SPEECH'
410
+ ? (0, types_1.normalizeSpeechText)(typeof promptInput === 'string' ? promptInput : '')
411
+ : typeof promptInput === 'string'
412
+ ? promptInput.trim()
413
+ : '';
373
414
  if (!prompt.length) {
374
415
  throw new Error('missing_prompt');
375
416
  }
417
+ if (modality === 'SPEECH' && (0, types_1.countSpeechWords)(prompt) > types_1.PLAYDROP_SPEECH_MAX_WORDS) {
418
+ throw new Error('speech_input_too_long');
419
+ }
376
420
  const visibility = normalizeVisibility(options.visibility);
377
421
  if (options.visibility && !visibility) {
378
422
  throw new Error('invalid_visibility');
@@ -386,7 +430,6 @@ function buildGenerateRequest(typeInput, promptInput, options) {
386
430
  const request = {
387
431
  modality,
388
432
  input: prompt,
389
- assetSubcategory: parseAssetSubcategory(options.subcategory) ?? DEFAULT_SUBCATEGORY_BY_MODALITY[modality],
390
433
  assetName: typeof options.assetName === 'string' && options.assetName.trim().length > 0 ? options.assetName.trim() : undefined,
391
434
  assetDisplayName: typeof options.assetDisplayName === 'string' && options.assetDisplayName.trim().length > 0
392
435
  ? options.assetDisplayName.trim()
@@ -397,6 +440,12 @@ function buildGenerateRequest(typeInput, promptInput, options) {
397
440
  assetVisibility: visibility ?? 'PUBLIC',
398
441
  attachAppVersionId: parseOptionalPositiveInt(options.attachAppVersionId, 'attach_app_version_id'),
399
442
  };
443
+ if (modality !== 'SPEECH') {
444
+ request.assetSubcategory = parseAssetSubcategory(options.subcategory) ?? DEFAULT_SUBCATEGORY_BY_MODALITY[modality];
445
+ }
446
+ else if (typeof options.subcategory === 'string' && options.subcategory.trim().length > 0) {
447
+ throw new Error('speech_disallows_subcategory');
448
+ }
400
449
  if (modality === 'IMAGE') {
401
450
  request.image1 = resolveImageAttachment(options.image1, 'image1');
402
451
  request.image2 = resolveImageAttachment(options.image2, 'image2');
@@ -410,6 +459,19 @@ function buildGenerateRequest(typeInput, promptInput, options) {
410
459
  else {
411
460
  request.aspectRatio = DEFAULT_IMAGE_ASPECT_RATIO;
412
461
  }
462
+ if (typeof options.size === 'string' && options.size.trim().length > 0) {
463
+ const imageSize = options.size.trim().toUpperCase();
464
+ if (!IMAGE_SIZE_SET.has(imageSize)) {
465
+ throw new Error('invalid_image_size');
466
+ }
467
+ request.imageSize = imageSize;
468
+ }
469
+ else {
470
+ request.imageSize = DEFAULT_IMAGE_SIZE;
471
+ }
472
+ }
473
+ else if (typeof options.size === 'string' && options.size.trim().length > 0) {
474
+ throw new Error('image_size_not_supported_for_modality');
413
475
  }
414
476
  if (modality === 'MUSIC') {
415
477
  request.durationMs = parseMusicDurationMs(duration);
@@ -421,6 +483,24 @@ function buildGenerateRequest(typeInput, promptInput, options) {
421
483
  request.loop = Boolean(options.loop);
422
484
  request.coverPrompt = typeof options.coverPrompt === 'string' && options.coverPrompt.trim().length > 0 ? options.coverPrompt.trim() : undefined;
423
485
  }
486
+ if (modality === 'SPEECH') {
487
+ const speechVoice = typeof options.voice === 'string' && options.voice.trim().length > 0 ? options.voice.trim() : undefined;
488
+ const speechProviderVoiceId = typeof options.providerVoiceId === 'string' && options.providerVoiceId.trim().length > 0
489
+ ? options.providerVoiceId.trim()
490
+ : undefined;
491
+ if (speechVoice && speechProviderVoiceId) {
492
+ throw new Error('invalid_speech_voice_selection');
493
+ }
494
+ let validatedSpeechVoice;
495
+ if (speechVoice) {
496
+ if (!isPlaydropSpeechVoiceId(speechVoice)) {
497
+ throw new Error('invalid_speech_voice');
498
+ }
499
+ validatedSpeechVoice = speechVoice;
500
+ }
501
+ request.speechVoice = validatedSpeechVoice;
502
+ request.speechProviderVoiceId = speechProviderVoiceId;
503
+ }
424
504
  if (modality === 'VIDEO') {
425
505
  request.image1 = resolveImageAttachment(options.image1, 'image1');
426
506
  request.image2 = resolveImageAttachment(options.image2, 'image2');