@playdrop/playdrop-cli 0.3.9 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.10",
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.10",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.3.9",
33
+ "minimumVersion": "0.3.10",
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.10"
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);
@@ -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);
@@ -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;
@@ -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 });
@@ -16,7 +16,9 @@ const http_1 = require("../http");
16
16
  const environment_1 = require("../environment");
17
17
  const messages_1 = require("../messages");
18
18
  const clientInfo_1 = require("../clientInfo");
19
- const DEFAULT_CATALOGUE = `${JSON.stringify({ apps: [] }, null, 2)}\n`;
19
+ const DEFAULT_CATALOGUE = '{}\n';
20
+ const LEGACY_CATALOGUE_VERSION_KEY = ['schema', 'Version'].join('');
21
+ const ALLOWED_CATALOGUE_TOP_LEVEL_KEYS = new Set(['apps', 'assets', 'assetPacks']);
20
22
  function ensureTrailingNewline(content) {
21
23
  return content.endsWith('\n') ? content : `${content}\n`;
22
24
  }
@@ -100,7 +102,33 @@ function normalizeBootstrapPayload(payload) {
100
102
  const catalogueRaw = typeof payload.catalogue === 'string' && payload.catalogue.trim().length > 0
101
103
  ? payload.catalogue
102
104
  : DEFAULT_CATALOGUE;
103
- const catalogue = ensureTrailingNewline(catalogueRaw.trim() ? catalogueRaw : DEFAULT_CATALOGUE);
105
+ const catalogueCandidate = ensureTrailingNewline(catalogueRaw.trim() ? catalogueRaw : DEFAULT_CATALOGUE);
106
+ let parsedCatalogue;
107
+ try {
108
+ parsedCatalogue = JSON.parse(catalogueCandidate);
109
+ }
110
+ catch {
111
+ (0, messages_1.printErrorWithHelp)('Bootstrap payload returned an invalid catalogue.json.', ['Contact the platform team or try again shortly.'], { command: 'project init' });
112
+ process.exitCode = 1;
113
+ return null;
114
+ }
115
+ if (!parsedCatalogue || typeof parsedCatalogue !== 'object' || Array.isArray(parsedCatalogue)) {
116
+ (0, messages_1.printErrorWithHelp)('Bootstrap payload returned an invalid catalogue.json.', ['Contact the platform team or try again shortly.'], { command: 'project init' });
117
+ process.exitCode = 1;
118
+ return null;
119
+ }
120
+ if (Object.prototype.hasOwnProperty.call(parsedCatalogue, LEGACY_CATALOGUE_VERSION_KEY)) {
121
+ (0, messages_1.printErrorWithHelp)('Bootstrap payload returned a legacy catalogue.json with a top-level version field.', ['Contact the platform team or try again shortly.'], { command: 'project init' });
122
+ process.exitCode = 1;
123
+ return null;
124
+ }
125
+ const unsupportedKey = Object.keys(parsedCatalogue).find((key) => !ALLOWED_CATALOGUE_TOP_LEVEL_KEYS.has(key));
126
+ if (unsupportedKey) {
127
+ (0, messages_1.printErrorWithHelp)(`Bootstrap payload returned catalogue.json with unsupported top-level key "${unsupportedKey}".`, ['Contact the platform team or try again shortly.'], { command: 'project init' });
128
+ process.exitCode = 1;
129
+ return null;
130
+ }
131
+ const catalogue = catalogueCandidate;
104
132
  return { readmeUri, agentsUri, catalogue };
105
133
  }
106
134
  async function downloadBootstrapAsset(webBase, uri, label) {
@@ -66,8 +66,8 @@ function selectTasks(pathOrName) {
66
66
  if (taskList.length === 0) {
67
67
  errors.push({
68
68
  type: 'no-tasks',
69
- message: `No catalogue.json files were found under ${absolute}.`,
70
- help: ['Add catalogue.json files to the project folders you want to process, then rerun the command.'],
69
+ message: `No publishable entries were found under ${absolute}.`,
70
+ help: ['Add child catalogue.json files or add apps, assets, or assetPacks to the root catalogue.json, then rerun the command.'],
71
71
  directory: absolute,
72
72
  });
73
73
  return { tasks: [], warnings, errors, resolution: 'directory' };
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.9",
2
+ "version": "0.3.10",
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.10",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.3.9",
33
+ "minimumVersion": "0.3.10",
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.10"
43
42
  }
44
43
  }
45
44
  }
@@ -53,6 +53,19 @@ function runtime(overrides = {}) {
53
53
  strict_1.default.equal(result.code, 'unsupported_platform_version');
54
54
  }
55
55
  });
56
+ (0, node_test_1.default)('does not require a build floor for newer cli versions once the version is supported', () => {
57
+ const meta = (0, index_1.loadClientMeta)();
58
+ const requiredVersion = meta.clients['cli']?.minimumVersion ?? meta.version;
59
+ const info = runtime({
60
+ client: 'cli',
61
+ clientVersion: requiredVersion,
62
+ clientBuild: 0,
63
+ platform: 'macos',
64
+ platformVersion: '14.0',
65
+ });
66
+ const result = (0, index_1.validateClientEnvironment)(info);
67
+ strict_1.default.equal(result.ok, true);
68
+ });
56
69
  (0, node_test_1.default)('returns failure when metadata is incomplete', () => {
57
70
  const info = runtime({ clientVersion: '', platformVersion: '' });
58
71
  const result = (0, index_1.validateClientEnvironment)(info);