@playdrop/playdrop-cli 0.3.9 → 0.3.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 (47) hide show
  1. package/README.md +43 -1
  2. package/config/client-meta.json +5 -6
  3. package/dist/catalogue-utils.js +9 -2
  4. package/dist/catalogue.d.ts +0 -1
  5. package/dist/catalogue.js +26 -8
  6. package/dist/commands/browse.js +1 -1
  7. package/dist/commands/capture.js +2 -2
  8. package/dist/commands/comments.js +3 -3
  9. package/dist/commands/create.js +108 -12
  10. package/dist/commands/createRemixContent.js +33 -3
  11. package/dist/commands/creations.js +2 -2
  12. package/dist/commands/credits.js +2 -2
  13. package/dist/commands/detail.js +1 -1
  14. package/dist/commands/dev.js +2 -2
  15. package/dist/commands/feedback.js +1 -1
  16. package/dist/commands/generation.js +2 -2
  17. package/dist/commands/gettingStarted.js +1 -1
  18. package/dist/commands/init.js +31 -3
  19. package/dist/commands/login.d.ts +10 -10
  20. package/dist/commands/login.js +202 -79
  21. package/dist/commands/logout.js +1 -1
  22. package/dist/commands/notifications.js +3 -3
  23. package/dist/commands/versionsBrowse.js +1 -1
  24. package/dist/commands/whoami.js +9 -9
  25. package/dist/index.js +41 -25
  26. package/dist/messages.js +5 -5
  27. package/dist/taskSelection.js +2 -2
  28. package/node_modules/@playdrop/api-client/dist/client.d.ts +13 -2
  29. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  30. package/node_modules/@playdrop/api-client/dist/client.js +21 -0
  31. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +10 -1
  32. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  33. package/node_modules/@playdrop/api-client/dist/domains/auth.js +94 -0
  34. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +2 -2
  35. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  36. package/node_modules/@playdrop/api-client/dist/domains/payments.js +2 -1
  37. package/node_modules/@playdrop/api-client/dist/index.d.ts +16 -3
  38. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  39. package/node_modules/@playdrop/api-client/dist/index.js +37 -4
  40. package/node_modules/@playdrop/config/client-meta.json +5 -6
  41. package/node_modules/@playdrop/config/dist/test/validateClientEnvironment.test.js +13 -0
  42. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  43. package/node_modules/@playdrop/types/dist/api.d.ts +204 -2
  44. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  45. package/node_modules/@playdrop/types/dist/asset.d.ts +18 -50
  46. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  47. package/package.json +23 -2
package/README.md CHANGED
@@ -1,6 +1,48 @@
1
1
  # @playdrop/playdrop-cli
2
2
 
3
- Command-line interface for creators and AI agents using Playdrop.
3
+ Official Playdrop CLI for the live [Playdrop](https://www.playdrop.ai/) platform.
4
+
5
+ Use it to browse live examples, create or remix projects, publish browser games and creator apps, and work with AI-generated game assets on Playdrop.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @playdrop/playdrop-cli
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ playdrop auth login
17
+ playdrop auth whoami
18
+ playdrop project init .
19
+ playdrop project create app my-first-game --template playdrop/template/typescript_template
20
+ playdrop project dev my-first-game
21
+ playdrop project publish .
22
+ ```
23
+
24
+ ## What You Can Do
25
+
26
+ - Browse the live Playdrop catalogue of apps, assets, and packs
27
+ - Inspect real references before building or remixing
28
+ - Create, remix, validate, and publish Playdrop projects
29
+ - Manage comments, versions, credits, notifications, and creations
30
+ - Read the public Playdrop docs directly from the CLI
31
+
32
+ ## Links
33
+
34
+ - Website: [playdrop.ai](https://www.playdrop.ai/)
35
+ - Getting started: [playdrop.ai/docs/getting-started](https://www.playdrop.ai/docs/getting-started)
36
+ - CLI docs: [playdrop.ai/docs/cli](https://www.playdrop.ai/docs/cli)
37
+ - SDK docs: [playdrop.ai/docs/sdk](https://www.playdrop.ai/docs/sdk)
38
+ - Templates and demos: [playdrop.ai/docs/samples/templates-and-demos](https://www.playdrop.ai/docs/samples/templates-and-demos)
39
+ - Public Playdrop skill: [skills.sh/playdrop-ai/playdrop-skills/playdrop](https://skills.sh/playdrop-ai/playdrop-skills/playdrop)
40
+
41
+ ## Live Examples
42
+
43
+ - [Starter Kit Racing](https://www.playdrop.ai/creators/playdrop/apps/game/starter-kit-racing)
44
+ - [Hangingout](https://www.playdrop.ai/creators/playdrop/apps/game/hangingout)
45
+ - [Top Creators](https://www.playdrop.ai/top-creators)
4
46
 
5
47
  ## Main Commands
6
48
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.9",
2
+ "version": "0.3.11",
3
3
  "build": 1,
4
4
  "platforms": {
5
5
  "ios": {
@@ -26,20 +26,19 @@
26
26
  },
27
27
  "clients": {
28
28
  "web": {
29
- "minimumVersion": "0.3.9",
29
+ "minimumVersion": "0.3.11",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.3.9",
33
+ "minimumVersion": "0.3.11",
34
34
  "minimumBuild": 1
35
35
  },
36
36
  "apple": {
37
- "minimumVersion": "0.3.9",
37
+ "minimumVersion": "0.3.10",
38
38
  "minimumBuild": 1
39
39
  },
40
40
  "cli": {
41
- "minimumVersion": "0.3.9",
42
- "minimumBuild": 1
41
+ "minimumVersion": "0.3.11"
43
42
  }
44
43
  }
45
44
  }
@@ -42,11 +42,18 @@ function parseSurfaceTargets(input) {
42
42
  return { map, list };
43
43
  }
44
44
  const DEFAULT_SURFACE_TARGETS_OBJECT = { desktop: true, mobileLandscape: false, mobilePortrait: false };
45
+ const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
45
46
  function parseCatalogueEntries(cataloguePath) {
46
47
  try {
47
48
  const raw = (0, node_fs_1.readFileSync)(cataloguePath, 'utf8');
48
- const data = raw.trim() ? JSON.parse(raw) : {};
49
- const entries = Array.isArray(data) ? data : data?.apps;
49
+ const data = JSON.parse(raw);
50
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
51
+ throw new Error('catalogue.json must be an object');
52
+ }
53
+ if (Object.prototype.hasOwnProperty.call(data, LEGACY_CATALOGUE_VERSION_KEY)) {
54
+ throw new Error('legacy top-level version field is not supported');
55
+ }
56
+ const entries = data?.apps;
50
57
  return Array.isArray(entries)
51
58
  ? entries.filter((entry) => Boolean(entry && typeof entry === 'object'))
52
59
  : [];
@@ -1,6 +1,5 @@
1
1
  import { type AppSurface, type AppType, type AppHostingMode, type AppVersionVisibility } from '@playdrop/types';
2
2
  export type CatalogueJson = {
3
- schemaVersion?: number;
4
3
  apps?: Array<Record<string, unknown>>;
5
4
  assets?: Array<Record<string, unknown>>;
6
5
  assetPacks?: Array<Record<string, unknown>>;
package/dist/catalogue.js CHANGED
@@ -12,6 +12,7 @@ const types_1 = require("@playdrop/types");
12
12
  const catalogue_utils_1 = require("./catalogue-utils");
13
13
  const HEX_COLOR_REGEX = /^#?(?:[0-9a-fA-F]{6})$/;
14
14
  const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
15
+ const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
15
16
  const ASSET_CATEGORY_SET = new Set(types_1.ASSET_CATEGORY_VALUES);
16
17
  const LISTING_IMAGE_EXTENSIONS = new Set(['.png']);
17
18
  const LISTING_VIDEO_EXTENSIONS = new Set(['.mp4']);
@@ -192,24 +193,24 @@ function shouldSkipDirectory(name) {
192
193
  function readCatalogueFile(rootDir, filePath) {
193
194
  try {
194
195
  const raw = (0, node_fs_1.readFileSync)(filePath, 'utf8');
195
- const parsed = raw.trim() ? JSON.parse(raw) : {};
196
+ const parsed = JSON.parse(raw);
196
197
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
197
198
  return { error: 'catalogue.json must contain a JSON object' };
198
199
  }
199
- if (parsed.schemaVersion !== 2) {
200
- return { error: 'catalogue.json must set "schemaVersion": 2' };
200
+ if (Object.prototype.hasOwnProperty.call(parsed, LEGACY_CATALOGUE_VERSION_KEY)) {
201
+ return { error: 'catalogue.json must not define the legacy top-level version field. Remove it and retry.' };
201
202
  }
202
203
  const forbiddenKeys = ['avatars', 'humanoids', 'creatures', 'blocks', 'games'];
203
204
  for (const key of forbiddenKeys) {
204
205
  if (Object.prototype.hasOwnProperty.call(parsed, key)) {
205
- return { error: `catalogue.json schema v2 forbids top-level "${key}"` };
206
+ return { error: `catalogue.json forbids top-level "${key}"` };
206
207
  }
207
208
  }
208
- const allowedKeys = new Set(['schemaVersion', 'apps', 'assets', 'assetPacks']);
209
+ const allowedKeys = new Set(['apps', 'assets', 'assetPacks']);
209
210
  const unknownKeys = Object.keys(parsed).filter((key) => !allowedKeys.has(key));
210
211
  if (unknownKeys.length > 0) {
211
212
  return {
212
- error: `catalogue.json schema v2 contains unsupported top-level key "${unknownKeys[0]}"`,
213
+ error: `catalogue.json contains unsupported top-level key "${unknownKeys[0]}"`,
213
214
  };
214
215
  }
215
216
  const directory = (0, node_path_1.dirname)(filePath);
@@ -283,6 +284,9 @@ function walkCatalogueTree(rootDir) {
283
284
  files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
284
285
  return { files, errors };
285
286
  }
287
+ function isCatalogueMarker(file) {
288
+ return Object.keys(file.data).length === 0;
289
+ }
286
290
  const packageJsonCache = new Map();
287
291
  function resolvePackageJson(projectDir) {
288
292
  const candidate = (0, node_path_1.join)(projectDir, 'package.json');
@@ -633,10 +637,10 @@ function buildAppTasks(rootDir, catalogues, options) {
633
637
  }
634
638
  serverPath = absoluteServerPath;
635
639
  }
636
- // Parse versioning fields (required in schema v2)
640
+ // Parse versioning fields.
637
641
  const rawVersion = typeof entry.version === 'string' ? entry.version.trim() : '';
638
642
  if (!rawVersion) {
639
- errors.push(`[${label}] Skipping ${rawName}: app version is required in schema v2 (e.g., "1.0.0").`);
643
+ errors.push(`[${label}] Skipping ${rawName}: app version is required (e.g., "1.0.0").`);
640
644
  continue;
641
645
  }
642
646
  if (!SEMVER_REGEX.test(rawVersion)) {
@@ -1398,6 +1402,20 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1398
1402
  errors: discoveryErrors,
1399
1403
  };
1400
1404
  }
1405
+ const rootCatalogue = files.find((file) => file.relativePath === 'catalogue.json');
1406
+ const hasNestedCatalogues = files.some((file) => file.relativePath !== 'catalogue.json');
1407
+ if (rootCatalogue && hasNestedCatalogues && !isCatalogueMarker(rootCatalogue)) {
1408
+ return {
1409
+ apps: [],
1410
+ assets: [],
1411
+ embeddedAssets: [],
1412
+ assetPacks: [],
1413
+ warnings: [],
1414
+ errors: [
1415
+ '[catalogue.json] Workspace root catalogue.json must be {} when nested catalogue.json files exist below it.',
1416
+ ],
1417
+ };
1418
+ }
1401
1419
  const { tasks: appTasks, warnings: appWarnings, errors: appErrors } = buildAppTasks(rootDir, files, options);
1402
1420
  const { tasks: assetTasks, warnings: assetWarnings, errors: assetErrors } = buildAssetTasks(files);
1403
1421
  const { tasks: assetPackTasks, warnings: assetPackWarnings, errors: assetPackErrors } = buildAssetPackTasks(files);
@@ -362,7 +362,7 @@ async function browse(options = {}) {
362
362
  if (apiError.status === 401 || apiError.status === 403) {
363
363
  return {
364
364
  problem: 'Browsing this content requires you to be logged in.',
365
- suggestions: ['Run "playdrop auth login" and retry.'],
365
+ suggestions: ['Run "playdrop login" and retry.'],
366
366
  };
367
367
  }
368
368
  return {
@@ -326,8 +326,8 @@ async function capture(targetArg, options = {}) {
326
326
  }
327
327
  if (error instanceof types_1.ApiError) {
328
328
  (0, messages_1.printErrorWithHelp)(`Could not fetch your account (status ${error.status}).`, [
329
- 'Run "playdrop auth login" to refresh your session and ensure the API is reachable.',
330
- 'Use "playdrop auth whoami" afterwards to confirm your status.'
329
+ 'Run "playdrop login" to refresh your session and ensure the API is reachable.',
330
+ 'Use "playdrop whoami" afterwards to confirm your status.',
331
331
  ], { command: 'project capture' });
332
332
  process.exitCode = 1;
333
333
  return;
@@ -72,7 +72,7 @@ async function browseComments(rawRef, options = {}) {
72
72
  if (apiError.status === 401 || apiError.status === 403) {
73
73
  return {
74
74
  problem: `You do not have access to comments for "${ref.ref}".`,
75
- suggestions: ['Run "playdrop auth login" if this is private content you own.'],
75
+ suggestions: ['Run "playdrop login" if this is private content you own.'],
76
76
  };
77
77
  }
78
78
  return {
@@ -125,7 +125,7 @@ async function addComment(rawRef, options = {}) {
125
125
  if (apiError.status === 401 || apiError.status === 403) {
126
126
  return {
127
127
  problem: 'Adding a comment requires you to be logged in.',
128
- suggestions: ['Run "playdrop auth login" and retry.'],
128
+ suggestions: ['Run "playdrop login" and retry.'],
129
129
  };
130
130
  }
131
131
  if (apiError.status === 404) {
@@ -166,7 +166,7 @@ async function deleteComment(commentId, options = {}) {
166
166
  if (apiError.status === 401 || apiError.status === 403) {
167
167
  return {
168
168
  problem: 'Deleting a comment requires a valid login with permission to manage it.',
169
- suggestions: ['Run "playdrop auth login" and retry.'],
169
+ suggestions: ['Run "playdrop login" and retry.'],
170
170
  };
171
171
  }
172
172
  if (apiError.status === 404) {
@@ -21,6 +21,8 @@ const app_1 = require("@playdrop/types/app");
21
21
  const catalogue_utils_1 = require("../catalogue-utils");
22
22
  const catalogue_1 = require("../catalogue");
23
23
  const CATALOGUE_FILENAME = 'catalogue.json';
24
+ const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
25
+ const ALLOWED_CATALOGUE_TOP_LEVEL_KEYS = new Set(['apps', 'assets', 'assetPacks']);
24
26
  async function downloadArchive(url) {
25
27
  const response = await fetch(url);
26
28
  if (!response.ok) {
@@ -147,6 +149,14 @@ function runNpmInstall(projectDir) {
147
149
  });
148
150
  return result.status === 0;
149
151
  }
152
+ function findUnsupportedCatalogueTopLevelKey(parsed) {
153
+ for (const key of Object.keys(parsed)) {
154
+ if (!ALLOWED_CATALOGUE_TOP_LEVEL_KEYS.has(key) && key !== 'games') {
155
+ return key;
156
+ }
157
+ }
158
+ return null;
159
+ }
150
160
  function extractEntryPointFromMetadata(metadata) {
151
161
  if (!metadata) {
152
162
  return null;
@@ -265,9 +275,6 @@ function buildCatalogueEntry(name, metadata, sourceInfo, relativeFilePath) {
265
275
  function readCatalogue(path) {
266
276
  try {
267
277
  const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
268
- if (!raw.trim()) {
269
- return { schemaVersion: 2, apps: [] };
270
- }
271
278
  const parsed = JSON.parse(raw);
272
279
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
273
280
  return null;
@@ -278,14 +285,39 @@ function readCatalogue(path) {
278
285
  return null;
279
286
  }
280
287
  }
288
+ function hasNestedCatalogueFiles(rootDir) {
289
+ const rootCataloguePath = (0, node_path_1.join)(rootDir, CATALOGUE_FILENAME);
290
+ const stack = [rootDir];
291
+ while (stack.length > 0) {
292
+ const current = stack.pop();
293
+ const entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true });
294
+ for (const entry of entries) {
295
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
296
+ continue;
297
+ }
298
+ const absolutePath = (0, node_path_1.join)(current, entry.name);
299
+ if (entry.isDirectory()) {
300
+ stack.push(absolutePath);
301
+ continue;
302
+ }
303
+ if (entry.isFile() && entry.name === CATALOGUE_FILENAME && absolutePath !== rootCataloguePath) {
304
+ return true;
305
+ }
306
+ }
307
+ }
308
+ return false;
309
+ }
281
310
  function ensureCatalogueEntry(name, metadata, cataloguePath, sourceInfo, htmlFilePath) {
282
311
  const path = (0, node_path_1.resolve)(cataloguePath);
283
312
  const catalogueDir = (0, node_path_1.dirname)(path);
313
+ if (hasNestedCatalogueFiles(catalogueDir)) {
314
+ return { path, createdFile: false, addedEntry: false, error: 'workspace_root_marker_required' };
315
+ }
284
316
  const relativeFilePath = (0, node_path_1.relative)(catalogueDir, (0, node_path_1.resolve)(htmlFilePath)) || `${name}.html`;
285
317
  const normalizedFilePath = relativeFilePath.replace(/\\/g, '/');
286
318
  const entry = buildCatalogueEntry(name, metadata, sourceInfo, normalizedFilePath);
287
319
  if (!(0, node_fs_1.existsSync)(path)) {
288
- const catalogue = { schemaVersion: 2, apps: [entry] };
320
+ const catalogue = { apps: [entry] };
289
321
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(catalogue, null, 2)}\n`);
290
322
  return { path, createdFile: true, addedEntry: true };
291
323
  }
@@ -293,9 +325,13 @@ function ensureCatalogueEntry(name, metadata, cataloguePath, sourceInfo, htmlFil
293
325
  if (!parsed) {
294
326
  return { path, createdFile: false, addedEntry: false, error: 'invalid_json' };
295
327
  }
296
- if (parsed.schemaVersion !== 2) {
328
+ if (Object.prototype.hasOwnProperty.call(parsed, LEGACY_CATALOGUE_VERSION_KEY)) {
297
329
  return { path, createdFile: false, addedEntry: false, error: 'invalid_schema_version' };
298
330
  }
331
+ const unsupportedKey = findUnsupportedCatalogueTopLevelKey(parsed);
332
+ if (unsupportedKey) {
333
+ return { path, createdFile: false, addedEntry: false, error: 'unsupported_top_level_key', unsupportedKey };
334
+ }
299
335
  if (!Array.isArray(parsed.apps) && Array.isArray(parsed.games)) {
300
336
  return { path, createdFile: false, addedEntry: false, error: 'legacy_games_property' };
301
337
  }
@@ -325,7 +361,17 @@ function logCatalogueOutcome(result, name) {
325
361
  return false;
326
362
  }
327
363
  if (result.error === 'invalid_schema_version') {
328
- (0, messages_1.printErrorWithHelp)(`${label} must set "schemaVersion": 2.`, ['Update the file to schema v2 and rerun `playdrop project create app`.'], { command: 'project create app' });
364
+ (0, messages_1.printErrorWithHelp)(`${label} must not define the legacy top-level version field.`, ['Remove the legacy top-level version field from the file and rerun `playdrop project create app`.'], { command: 'project create app' });
365
+ process.exitCode = process.exitCode || 1;
366
+ return false;
367
+ }
368
+ if (result.error === 'unsupported_top_level_key') {
369
+ (0, messages_1.printErrorWithHelp)(`${label} contains unsupported top-level key "${result.unsupportedKey || 'unknown'}".`, ['Keep catalogue.json limited to apps, assets, and assetPacks, then rerun `playdrop project create app`.'], { command: 'project create app' });
370
+ process.exitCode = process.exitCode || 1;
371
+ return false;
372
+ }
373
+ if (result.error === 'workspace_root_marker_required') {
374
+ (0, messages_1.printErrorWithHelp)(`${label} must stay {} because nested catalogue.json files already exist below this directory.`, ['Create the app inside its own project folder instead of writing entries into the workspace root catalogue.json.'], { command: 'project create app' });
329
375
  process.exitCode = process.exitCode || 1;
330
376
  return false;
331
377
  }
@@ -344,13 +390,20 @@ function logCatalogueOutcome(result, name) {
344
390
  }
345
391
  function updateExtractedProjectCatalogue(name, metadata, projectCataloguePath, sourceInfo, htmlFilePath) {
346
392
  const path = (0, node_path_1.resolve)(projectCataloguePath);
393
+ if (hasNestedCatalogueFiles((0, node_path_1.dirname)(path))) {
394
+ return { path, updated: false, error: 'workspace_root_marker_required' };
395
+ }
347
396
  const parsed = readCatalogue(path);
348
397
  if (!parsed) {
349
398
  return { path, updated: false, error: 'invalid_json' };
350
399
  }
351
- if (parsed.schemaVersion !== 2) {
400
+ if (Object.prototype.hasOwnProperty.call(parsed, LEGACY_CATALOGUE_VERSION_KEY)) {
352
401
  return { path, updated: false, error: 'invalid_schema_version' };
353
402
  }
403
+ const unsupportedKey = findUnsupportedCatalogueTopLevelKey(parsed);
404
+ if (unsupportedKey) {
405
+ return { path, updated: false, error: 'unsupported_top_level_key', unsupportedKey };
406
+ }
354
407
  if (!Array.isArray(parsed.apps) && Array.isArray(parsed.games)) {
355
408
  return { path, updated: false, error: 'legacy_games_property' };
356
409
  }
@@ -436,7 +489,17 @@ function logProjectCatalogueOutcome(result, name) {
436
489
  return false;
437
490
  }
438
491
  if (result.error === 'invalid_schema_version') {
439
- (0, messages_1.printErrorWithHelp)(`${label} must set "schemaVersion": 2.`, ['Update the file to schema v2 and rerun `playdrop project create app`.'], { command: 'project create app' });
492
+ (0, messages_1.printErrorWithHelp)(`${label} must not define the legacy top-level version field.`, ['Remove the legacy top-level version field from the file and rerun `playdrop project create app`.'], { command: 'project create app' });
493
+ process.exitCode = process.exitCode || 1;
494
+ return false;
495
+ }
496
+ if (result.error === 'unsupported_top_level_key') {
497
+ (0, messages_1.printErrorWithHelp)(`${label} contains unsupported top-level key "${result.unsupportedKey || 'unknown'}".`, ['Keep catalogue.json limited to apps, assets, and assetPacks, then rerun `playdrop project create app`.'], { command: 'project create app' });
498
+ process.exitCode = process.exitCode || 1;
499
+ return false;
500
+ }
501
+ if (result.error === 'workspace_root_marker_required') {
502
+ (0, messages_1.printErrorWithHelp)(`${label} must stay {} because nested catalogue.json files already exist below this directory.`, ['Create the app inside its own project folder instead of writing entries into the workspace root catalogue.json.'], { command: 'project create app' });
440
503
  process.exitCode = process.exitCode || 1;
441
504
  return false;
442
505
  }
@@ -640,9 +703,10 @@ function parseRemixScaffoldResponse(response, version) {
640
703
  if (!externalListingUrl) {
641
704
  throw new Error('Remix response missing externalListingUrl');
642
705
  }
706
+ const externalMetadata = parseExternalRemixMetadata(response.metadata);
643
707
  return {
644
708
  html: null,
645
- metadata,
709
+ metadata: externalMetadata,
646
710
  externalListingUrl,
647
711
  entryPoint,
648
712
  sourceVersion: null,
@@ -650,6 +714,38 @@ function parseRemixScaffoldResponse(response, version) {
650
714
  }
651
715
  throw new Error(`Unsupported remix mode: ${remixMode || 'missing'}`);
652
716
  }
717
+ function parseExternalRemixMetadata(value) {
718
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
719
+ throw new Error('Remix response missing external metadata');
720
+ }
721
+ const record = value;
722
+ const name = typeof record.name === 'string' ? record.name.trim() : '';
723
+ const displayName = typeof record.displayName === 'string' ? record.displayName.trim() : '';
724
+ const description = typeof record.description === 'string' ? record.description.trim() : '';
725
+ const creatorUsername = typeof record.creatorUsername === 'string' ? record.creatorUsername.trim() : '';
726
+ const type = (0, app_1.parseAppType)(record.type) ?? '';
727
+ const emoji = record.emoji;
728
+ const color = record.color;
729
+ if (!name || !displayName || !description || !creatorUsername || !type) {
730
+ throw new Error('Remix response missing required external metadata fields');
731
+ }
732
+ if (emoji !== null && typeof emoji !== 'string') {
733
+ throw new Error('Remix response has invalid external metadata emoji');
734
+ }
735
+ if (color !== null && typeof color !== 'string') {
736
+ throw new Error('Remix response has invalid external metadata color');
737
+ }
738
+ return {
739
+ name,
740
+ displayName,
741
+ description,
742
+ creatorUsername,
743
+ type,
744
+ emoji,
745
+ color,
746
+ surfaceTargets: (0, catalogue_utils_1.parseSurfaceTargets)(record.surfaceTargets).list,
747
+ };
748
+ }
653
749
  async function fetchRemixScaffold(client, creator, app, version) {
654
750
  try {
655
751
  const response = await client.fetchRemixScaffold(creator, app, version);
@@ -781,7 +877,7 @@ async function create(name, options = {}) {
781
877
  const choices = (0, environment_1.formatEnvironmentList)();
782
878
  (0, messages_1.printErrorWithHelp)(`Environment "${envName}" from your Playdrop config is not supported.`, [
783
879
  `Available environments: ${choices}.`,
784
- 'Run "playdrop auth login --env <env>" to save a supported environment before creating apps.'
880
+ 'Run "playdrop login --env <env>" to save a supported environment before creating apps.'
785
881
  ], { command: 'project create app', includeGeneralHelp: false });
786
882
  process.exitCode = 1;
787
883
  return;
@@ -1131,7 +1227,7 @@ async function create(name, options = {}) {
1131
1227
  return;
1132
1228
  }
1133
1229
  if (error instanceof types_1.ApiError) {
1134
- (0, messages_1.printErrorWithHelp)(`Failed to resolve your creator account (status ${error.status}).`, ['Run "playdrop auth login" to refresh your session, then try again.'], { command: 'project create app' });
1230
+ (0, messages_1.printErrorWithHelp)(`Failed to resolve your creator account (status ${error.status}).`, ['Run "playdrop login" to refresh your session, then try again.'], { command: 'project create app' });
1135
1231
  process.exitCode = 1;
1136
1232
  return;
1137
1233
  }
@@ -1141,7 +1237,7 @@ async function create(name, options = {}) {
1141
1237
  return;
1142
1238
  }
1143
1239
  if (error instanceof Error && error.message === 'missing_creator_username') {
1144
- (0, messages_1.printErrorWithHelp)('The API did not return a creator username.', ['Run "playdrop auth login" again, then retry.'], { command: 'project create app' });
1240
+ (0, messages_1.printErrorWithHelp)('The API did not return a creator username.', ['Run "playdrop login" again, then retry.'], { command: 'project create app' });
1145
1241
  process.exitCode = 1;
1146
1242
  return;
1147
1243
  }
@@ -13,6 +13,8 @@ const http_1 = require("../http");
13
13
  const messages_1 = require("../messages");
14
14
  const init_1 = require("./init");
15
15
  const CATALOGUE_FILENAME = 'catalogue.json';
16
+ const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
17
+ const ALLOWED_CATALOGUE_TOP_LEVEL_KEYS = new Set(['apps', 'assets', 'assetPacks']);
16
18
  const MIME_TYPE_TO_EXTENSION = {
17
19
  'image/png': '.png',
18
20
  'image/jpeg': '.jpg',
@@ -67,14 +69,40 @@ function readWorkspaceCatalogue(path) {
67
69
  throw new Error(`Invalid catalogue.json at ${path}.`);
68
70
  }
69
71
  const catalogue = parsed;
70
- if (catalogue.schemaVersion !== 2) {
71
- throw new Error(`catalogue.json at ${path} must set "schemaVersion": 2.`);
72
+ if (Object.prototype.hasOwnProperty.call(catalogue, LEGACY_CATALOGUE_VERSION_KEY)) {
73
+ throw new Error(`catalogue.json at ${path} must not define the legacy top-level version field.`);
74
+ }
75
+ const unsupportedKey = Object.keys(catalogue).find((key) => !ALLOWED_CATALOGUE_TOP_LEVEL_KEYS.has(key));
76
+ if (unsupportedKey) {
77
+ throw new Error(`catalogue.json at ${path} contains unsupported top-level key "${unsupportedKey}".`);
72
78
  }
73
79
  return catalogue;
74
80
  }
75
81
  function writeWorkspaceCatalogue(path, catalogue) {
76
82
  (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(catalogue, null, 2)}\n`);
77
83
  }
84
+ function assertWorkspaceRootCanOwnEntries(cataloguePath) {
85
+ const rootDir = (0, node_path_1.resolve)(cataloguePath, '..');
86
+ const rootCataloguePath = (0, node_path_1.join)(rootDir, CATALOGUE_FILENAME);
87
+ const stack = [rootDir];
88
+ while (stack.length > 0) {
89
+ const current = stack.pop();
90
+ const entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
93
+ continue;
94
+ }
95
+ const absolutePath = (0, node_path_1.join)(current, entry.name);
96
+ if (entry.isDirectory()) {
97
+ stack.push(absolutePath);
98
+ continue;
99
+ }
100
+ if (entry.isFile() && entry.name === CATALOGUE_FILENAME && absolutePath !== rootCataloguePath) {
101
+ throw new Error(`catalogue.json at ${cataloguePath} must stay {} because nested catalogue.json files already exist below this directory.`);
102
+ }
103
+ }
104
+ }
105
+ }
78
106
  async function createAuthenticatedClient(commandLabel) {
79
107
  const cfg = (0, config_1.loadConfig)();
80
108
  const envName = cfg.env;
@@ -87,7 +115,7 @@ async function createAuthenticatedClient(commandLabel) {
87
115
  if (!envConfig) {
88
116
  (0, messages_1.printErrorWithHelp)(`Environment "${envName}" from your Playdrop config is not supported.`, [
89
117
  `Available environments: ${(0, environment_1.formatEnvironmentList)()}.`,
90
- 'Run "playdrop auth login --env <env>" to save a supported environment before retrying.',
118
+ 'Run "playdrop login --env <env>" to save a supported environment before retrying.',
91
119
  ], { command: commandLabel, includeGeneralHelp: false });
92
120
  process.exitCode = 1;
93
121
  throw new Error('unsupported_env');
@@ -178,6 +206,7 @@ async function createAssetRemix(name, remixRef) {
178
206
  const client = await createAuthenticatedClient('project create asset');
179
207
  const cataloguePath = await ensureWorkspaceCataloguePath();
180
208
  const catalogue = readWorkspaceCatalogue(cataloguePath);
209
+ assertWorkspaceRootCanOwnEntries(cataloguePath);
181
210
  ensureUniqueEntryName(catalogue, 'asset', name);
182
211
  const detail = await client.fetchAssetBySlug(parsedRef.creatorUsername, parsedRef.name);
183
212
  const versions = await client.listAssetVersions(parsedRef.creatorUsername, parsedRef.name, { limit: 200, offset: 0 });
@@ -223,6 +252,7 @@ async function createPackRemix(name, remixRef) {
223
252
  const client = await createAuthenticatedClient('project create pack');
224
253
  const cataloguePath = await ensureWorkspaceCataloguePath();
225
254
  const catalogue = readWorkspaceCatalogue(cataloguePath);
255
+ assertWorkspaceRootCanOwnEntries(cataloguePath);
226
256
  ensureUniqueEntryName(catalogue, 'pack', name);
227
257
  const detail = await client.fetchAssetPackBySlug(parsedRef.creatorUsername, parsedRef.name);
228
258
  const versions = await client.listAssetPackVersions(parsedRef.creatorUsername, parsedRef.name, { limit: 200, offset: 0 });
@@ -65,7 +65,7 @@ async function resolveCreator(client, rawCreator, command) {
65
65
  const response = await client.me();
66
66
  const username = response.user?.username?.trim();
67
67
  if (!username) {
68
- (0, messages_1.printErrorWithHelp)('Could not resolve your current creator account.', ['Run "playdrop auth whoami" to confirm your session, then retry.'], { command });
68
+ (0, messages_1.printErrorWithHelp)('Could not resolve your current creator account.', ['Run "playdrop whoami" to confirm your session, then retry.'], { command });
69
69
  process.exitCode = 1;
70
70
  return null;
71
71
  }
@@ -283,7 +283,7 @@ async function browseCreations(options = {}) {
283
283
  const handled = (0, errors_1.handleCommandFailure)(error, 'creations browse', 'Creation lookup', {
284
284
  apiMessage: (apiError) => ({
285
285
  problem: `Creation lookup failed with status ${apiError.status}.`,
286
- suggestions: ['Run "playdrop auth login" and retry.'],
286
+ suggestions: ['Run "playdrop login" and retry.'],
287
287
  }),
288
288
  });
289
289
  if (!handled) {
@@ -34,7 +34,7 @@ async function showCreditBalance(options = {}) {
34
34
  const handled = (0, errors_1.handleCommandFailure)(error, 'credits balance', 'Credit balance lookup', {
35
35
  apiMessage: (apiError) => ({
36
36
  problem: `Credit balance lookup failed with status ${apiError.status}.`,
37
- suggestions: ['Run "playdrop auth login" and retry.'],
37
+ suggestions: ['Run "playdrop login" and retry.'],
38
38
  }),
39
39
  });
40
40
  if (!handled) {
@@ -80,7 +80,7 @@ async function browseCreditTransactions(options = {}) {
80
80
  const handled = (0, errors_1.handleCommandFailure)(error, 'credits transactions', 'Credit transaction lookup', {
81
81
  apiMessage: (apiError) => ({
82
82
  problem: `Credit transaction lookup failed with status ${apiError.status}.`,
83
- suggestions: ['Run "playdrop auth login" and retry.'],
83
+ suggestions: ['Run "playdrop login" and retry.'],
84
84
  }),
85
85
  });
86
86
  if (!handled) {
@@ -161,7 +161,7 @@ async function detail(rawRef, options = {}) {
161
161
  if (apiError.status === 401 || apiError.status === 403) {
162
162
  return {
163
163
  problem: `You do not have access to "${ref.ref}".`,
164
- suggestions: ['Run "playdrop auth login" if this is private content you own.', 'Use "playdrop help detail" for the ref format.'],
164
+ suggestions: ['Run "playdrop login" if this is private content you own.', 'Use "playdrop help detail" for the ref format.'],
165
165
  };
166
166
  }
167
167
  return {
@@ -120,8 +120,8 @@ async function dev(targetArg, _port, appOption) {
120
120
  }
121
121
  if (error instanceof types_1.ApiError) {
122
122
  (0, messages_1.printErrorWithHelp)(`Could not fetch your account (status ${error.status}).`, [
123
- 'Run "playdrop auth login" to refresh your session and ensure the API is reachable.',
124
- 'Use "playdrop auth whoami" afterwards to confirm your status.'
123
+ 'Run "playdrop login" to refresh your session and ensure the API is reachable.',
124
+ 'Use "playdrop whoami" afterwards to confirm your status.',
125
125
  ], { command: 'project dev' });
126
126
  process.exitCode = 1;
127
127
  return;
@@ -96,7 +96,7 @@ async function sendFeedback(options = {}) {
96
96
  const handled = (0, errors_1.handleCommandFailure)(error, 'feedback send', 'Feedback send', {
97
97
  apiMessage: (apiError) => ({
98
98
  problem: `Feedback send failed with status ${apiError.status}.`,
99
- suggestions: ['Check the provided fields and retry.', 'Run "playdrop auth login" if your session may be stale.'],
99
+ suggestions: ['Check the provided fields and retry.', 'Run "playdrop login" if your session may be stale.'],
100
100
  }),
101
101
  });
102
102
  if (!handled) {
@@ -601,7 +601,7 @@ function handleAiFailure(error, command, context) {
601
601
  if (apiError.status === 401 || apiError.status === 403) {
602
602
  return {
603
603
  problem: `${context} requires you to be logged in.`,
604
- suggestions: ['Run "playdrop auth login" and retry.'],
604
+ suggestions: ['Run "playdrop login" and retry.'],
605
605
  };
606
606
  }
607
607
  return {
@@ -616,7 +616,7 @@ function handleAiFailure(error, command, context) {
616
616
  const status = typeof error?.status === 'number' ? Number(error.status) : null;
617
617
  if (status !== null) {
618
618
  if (status === 401 || status === 403) {
619
- (0, messages_1.printErrorWithHelp)(`${context} requires you to be logged in.`, ['Run "playdrop auth login" and retry.'], { command });
619
+ (0, messages_1.printErrorWithHelp)(`${context} requires you to be logged in.`, ['Run "playdrop login" and retry.'], { command });
620
620
  }
621
621
  else {
622
622
  (0, messages_1.printErrorWithHelp)(`${context} failed with status ${status}.`, ['Retry in a moment.'], { command });
@@ -4,7 +4,7 @@ exports.printGettingStarted = printGettingStarted;
4
4
  function printGettingStarted() {
5
5
  console.log('Start here:\n');
6
6
  console.log('1. Log in');
7
- console.log(' playdrop auth login');
7
+ console.log(' playdrop login');
8
8
  console.log('');
9
9
  console.log('2. Initialize a workspace');
10
10
  console.log(' playdrop project init .');