@playdrop/playdrop-cli 0.3.8-build.3 → 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.
Files changed (103) hide show
  1. package/README.md +43 -1
  2. package/config/client-meta.json +5 -6
  3. package/dist/apps/build.js +43 -38
  4. package/dist/catalogue-utils.js +30 -18
  5. package/dist/catalogue.d.ts +0 -3
  6. package/dist/catalogue.js +80 -49
  7. package/dist/clientInfo.js +16 -2
  8. package/dist/commands/browse.js +10 -1
  9. package/dist/commands/capture.js +3 -2
  10. package/dist/commands/create.js +153 -54
  11. package/dist/commands/createRemixContent.js +61 -46
  12. package/dist/commands/creations.js +10 -1
  13. package/dist/commands/devServer.d.ts +1 -1
  14. package/dist/commands/devShared.js +1 -1
  15. package/dist/commands/generation.js +91 -74
  16. package/dist/commands/gettingStarted.js +1 -1
  17. package/dist/commands/init.js +30 -2
  18. package/dist/commands/search.js +10 -1
  19. package/dist/commands/upload-content.d.ts +70 -0
  20. package/dist/commands/upload-content.js +627 -0
  21. package/dist/commands/upload-graph.d.ts +23 -0
  22. package/dist/commands/upload-graph.js +108 -0
  23. package/dist/commands/upload.js +264 -543
  24. package/dist/http.d.ts +1 -1
  25. package/dist/playwright.d.ts +12 -4
  26. package/dist/proxyFetch.js +3 -2
  27. package/dist/taskSelection.js +2 -2
  28. package/node_modules/@playdrop/ai-client/dist/index.d.ts.map +1 -1
  29. package/node_modules/@playdrop/ai-client/dist/index.js +74 -54
  30. package/node_modules/@playdrop/api-client/dist/client.d.ts +20 -12
  31. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  32. package/node_modules/@playdrop/api-client/dist/client.js +6 -8
  33. package/node_modules/@playdrop/api-client/dist/core/errors.js +11 -11
  34. package/node_modules/@playdrop/api-client/dist/core/request.d.ts +2 -0
  35. package/node_modules/@playdrop/api-client/dist/core/request.d.ts.map +1 -1
  36. package/node_modules/@playdrop/api-client/dist/core/request.js +10 -3
  37. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +12 -10
  38. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  39. package/node_modules/@playdrop/api-client/dist/domains/admin.js +33 -30
  40. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +1 -0
  41. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  42. package/node_modules/@playdrop/api-client/dist/domains/apps.js +127 -128
  43. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts +9 -5
  44. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.d.ts.map +1 -1
  45. package/node_modules/@playdrop/api-client/dist/domains/asset-packs.js +151 -88
  46. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +1 -0
  47. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  48. package/node_modules/@playdrop/api-client/dist/domains/assets.js +150 -115
  49. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  50. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/domains/auth.js +21 -0
  52. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +1 -0
  53. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/payments.js +10 -0
  55. package/node_modules/@playdrop/api-client/dist/index.d.ts +34 -31
  56. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  57. package/node_modules/@playdrop/api-client/dist/index.js +19 -9
  58. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js +2 -0
  59. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js.map +1 -1
  60. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js +1 -1
  61. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js.map +1 -1
  62. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js +1 -1
  63. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js.map +1 -1
  64. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/builder.js +95 -75
  65. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/builder.js.map +1 -1
  66. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/scanner.d.ts +2 -3
  67. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/scanner.js.map +1 -1
  68. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/textures/face-map.js +4 -4
  69. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r6/textures/face-map.js.map +1 -1
  70. package/node_modules/@playdrop/boxel-core/dist/src/palette_tools.d.ts +2 -2
  71. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/slice.d.ts +5 -5
  72. package/node_modules/@playdrop/boxel-core/dist/src/transforms/voxels/textured-to-voxel.d.ts +3 -3
  73. package/node_modules/@playdrop/boxel-core/dist/src/types.d.ts +25 -25
  74. package/node_modules/@playdrop/boxel-core/dist/src/validation.js +2 -1
  75. package/node_modules/@playdrop/boxel-core/dist/src/validation.js.map +1 -1
  76. package/node_modules/@playdrop/boxel-three/dist/src/exporters/glb.js +5 -0
  77. package/node_modules/@playdrop/config/client-meta.json +5 -6
  78. package/node_modules/@playdrop/config/dist/src/index.js +6 -6
  79. package/node_modules/@playdrop/config/dist/test/validateClientEnvironment.test.js +15 -2
  80. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  81. package/node_modules/@playdrop/types/dist/api.d.ts +54 -9
  82. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  83. package/node_modules/@playdrop/types/dist/api.js +15 -8
  84. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +105 -11
  85. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  86. package/node_modules/@playdrop/types/dist/asset-pack.js +2 -0
  87. package/node_modules/@playdrop/types/dist/asset.d.ts +18 -50
  88. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  89. package/node_modules/@playdrop/types/dist/ecs.d.ts.map +1 -1
  90. package/node_modules/@playdrop/types/dist/ecs.js +10 -6
  91. package/node_modules/@playdrop/types/dist/entity.d.ts +5 -10
  92. package/node_modules/@playdrop/types/dist/entity.d.ts.map +1 -1
  93. package/node_modules/@playdrop/types/dist/entity.js +40 -23
  94. package/node_modules/@playdrop/types/dist/graph.d.ts.map +1 -1
  95. package/node_modules/@playdrop/types/dist/graph.js +13 -5
  96. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  97. package/node_modules/@playdrop/types/dist/version.js +7 -3
  98. package/node_modules/@playdrop/vox-three/dist/src/vox.d.ts +1 -0
  99. package/node_modules/@playdrop/vox-three/dist/src/vox.js +15 -6
  100. package/node_modules/@playdrop/vox-three/dist/src/vox.js.map +1 -1
  101. package/node_modules/@playdrop/vox-three/dist/test/vox.test.js +16 -0
  102. package/node_modules/@playdrop/vox-three/dist/test/vox.test.js.map +1 -1
  103. 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.8",
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.8",
29
+ "minimumVersion": "0.3.10",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.3.8",
33
+ "minimumVersion": "0.3.10",
34
34
  "minimumBuild": 1
35
35
  },
36
36
  "apple": {
37
- "minimumVersion": "0.3.8",
37
+ "minimumVersion": "0.3.10",
38
38
  "minimumBuild": 1
39
39
  },
40
40
  "cli": {
41
- "minimumVersion": "0.3.8",
42
- "minimumBuild": 1
41
+ "minimumVersion": "0.3.10"
43
42
  }
44
43
  }
45
44
  }
@@ -182,6 +182,47 @@ function isPathWithinRoot(rootDir, targetPath) {
182
182
  const rootPrefix = normalizedRoot.endsWith(node_path_1.sep) ? normalizedRoot : `${normalizedRoot}${node_path_1.sep}`;
183
183
  return normalizedTarget.startsWith(rootPrefix);
184
184
  }
185
+ function readProjectDirEntries(current) {
186
+ try {
187
+ return (0, node_fs_1.readdirSync)(current, { withFileTypes: true });
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ }
193
+ function appendIncludedProjectFile(rootDir, includePath, fileByRelativePath, results, enforceReservedBundleNames) {
194
+ const normalizedIncludePath = (0, node_path_1.normalize)(includePath).split(node_path_1.sep).join('/');
195
+ if (!normalizedIncludePath || normalizedIncludePath === '.') {
196
+ return;
197
+ }
198
+ if (normalizedIncludePath.startsWith('..')) {
199
+ throw new Error(`[apps][build] Invalid include file path "${includePath}".`);
200
+ }
201
+ if (fileByRelativePath.has(normalizedIncludePath)) {
202
+ return;
203
+ }
204
+ const absolutePath = (0, node_path_1.resolve)(rootDir, normalizedIncludePath);
205
+ if (!isPathWithinRoot(rootDir, absolutePath)) {
206
+ throw new Error(`[apps][build] Included file "${includePath}" escapes the project root.`);
207
+ }
208
+ if (!(0, node_fs_1.existsSync)(absolutePath)) {
209
+ return;
210
+ }
211
+ const stats = (0, node_fs_1.statSync)(absolutePath);
212
+ if (!stats.isFile()) {
213
+ return;
214
+ }
215
+ if (enforceReservedBundleNames && isReservedBundlePath(normalizedIncludePath)) {
216
+ throw new Error(`[apps][build] Reserved bundle filename "${normalizedIncludePath}" is not allowed. Use dedicated source/ecs/server upload fields.`);
217
+ }
218
+ const record = {
219
+ absolutePath,
220
+ relativePath: normalizedIncludePath,
221
+ mtime: stats.mtime,
222
+ };
223
+ results.push(record);
224
+ fileByRelativePath.set(record.relativePath, record);
225
+ }
185
226
  function collectProjectFiles(rootDir, rules, options) {
186
227
  const includeRelativeFiles = options?.includeRelativeFiles ?? [];
187
228
  const enforceReservedBundleNames = options?.enforceReservedBundleNames ?? false;
@@ -194,13 +235,7 @@ function collectProjectFiles(rootDir, rules, options) {
194
235
  if (visited.has(current))
195
236
  continue;
196
237
  visited.add(current);
197
- let dirEntries = [];
198
- try {
199
- dirEntries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true });
200
- }
201
- catch {
202
- continue;
203
- }
238
+ const dirEntries = readProjectDirEntries(current);
204
239
  for (const entry of dirEntries) {
205
240
  const absolutePath = (0, node_path_1.join)(current, entry.name);
206
241
  const relativePath = normalizeRelativePath(rootDir, absolutePath);
@@ -238,37 +273,7 @@ function collectProjectFiles(rootDir, rules, options) {
238
273
  }
239
274
  }
240
275
  for (const includePath of includeRelativeFiles) {
241
- const normalizedIncludePath = (0, node_path_1.normalize)(includePath).split(node_path_1.sep).join('/');
242
- if (!normalizedIncludePath || normalizedIncludePath === '.') {
243
- continue;
244
- }
245
- if (normalizedIncludePath.startsWith('..')) {
246
- throw new Error(`[apps][build] Invalid include file path "${includePath}".`);
247
- }
248
- if (fileByRelativePath.has(normalizedIncludePath)) {
249
- continue;
250
- }
251
- const absolutePath = (0, node_path_1.resolve)(rootDir, normalizedIncludePath);
252
- if (!isPathWithinRoot(rootDir, absolutePath)) {
253
- throw new Error(`[apps][build] Included file "${includePath}" escapes the project root.`);
254
- }
255
- if (!(0, node_fs_1.existsSync)(absolutePath)) {
256
- continue;
257
- }
258
- const stats = (0, node_fs_1.statSync)(absolutePath);
259
- if (!stats.isFile()) {
260
- continue;
261
- }
262
- if (enforceReservedBundleNames && isReservedBundlePath(normalizedIncludePath)) {
263
- throw new Error(`[apps][build] Reserved bundle filename "${normalizedIncludePath}" is not allowed. Use dedicated source/ecs/server upload fields.`);
264
- }
265
- const record = {
266
- absolutePath,
267
- relativePath: normalizedIncludePath,
268
- mtime: stats.mtime,
269
- };
270
- results.push(record);
271
- fileByRelativePath.set(record.relativePath, record);
276
+ appendIncludedProjectFile(rootDir, includePath, fileByRelativePath, results, enforceReservedBundleNames);
272
277
  }
273
278
  results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
274
279
  return results;
@@ -42,6 +42,34 @@ 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('');
46
+ function parseCatalogueEntries(cataloguePath) {
47
+ try {
48
+ const raw = (0, node_fs_1.readFileSync)(cataloguePath, 'utf8');
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;
57
+ return Array.isArray(entries)
58
+ ? entries.filter((entry) => Boolean(entry && typeof entry === 'object'))
59
+ : [];
60
+ }
61
+ catch {
62
+ throw new Error(`Failed to parse ${cataloguePath}. Fix the JSON and retry.`);
63
+ }
64
+ }
65
+ function findMatchingCatalogueEntries(apps, relativePath, fileName) {
66
+ return apps.filter((entry) => {
67
+ const entryName = typeof entry.name === 'string' ? entry.name.trim() : '';
68
+ const entryFile = typeof entry.file === 'string' ? entry.file.trim() : `${entryName}.html`;
69
+ const normalized = entryFile.replace(/\\/g, '/');
70
+ return normalized === relativePath || normalized === fileName;
71
+ });
72
+ }
45
73
  function cloneSurfaceTargets(targets) {
46
74
  return {
47
75
  list: [...targets.list],
@@ -54,25 +82,9 @@ function findAppCatalogueInfo(htmlPath) {
54
82
  for (let dir = (0, node_path_1.dirname)(absolute);; dir = (0, node_path_1.dirname)(dir)) {
55
83
  const cataloguePath = (0, node_path_1.join)(dir, 'catalogue.json');
56
84
  if ((0, node_fs_1.existsSync)(cataloguePath) && (0, node_fs_1.statSync)(cataloguePath).isFile()) {
57
- let entries;
58
- try {
59
- const raw = (0, node_fs_1.readFileSync)(cataloguePath, 'utf8');
60
- const data = raw.trim() ? JSON.parse(raw) : {};
61
- entries = Array.isArray(data) ? data : data?.apps;
62
- }
63
- catch {
64
- throw new Error(`Failed to parse ${cataloguePath}. Fix the JSON and retry.`);
65
- }
66
- const apps = Array.isArray(entries) ? entries : [];
67
85
  const rel = (0, node_path_1.relative)(dir, absolute).replace(/\\/g, '/');
68
- const matches = apps.filter((entry) => {
69
- if (!entry || typeof entry !== 'object')
70
- return false;
71
- const entryName = typeof entry.name === 'string' ? entry.name.trim() : '';
72
- const entryFile = typeof entry.file === 'string' ? entry.file.trim() : `${entryName}.html`;
73
- const normalized = entryFile.replace(/\\/g, '/');
74
- return normalized === rel || normalized === fileName;
75
- });
86
+ const apps = parseCatalogueEntries(cataloguePath);
87
+ const matches = findMatchingCatalogueEntries(apps, rel, fileName);
76
88
  if (matches.length > 1) {
77
89
  throw new Error(`Multiple entries in ${cataloguePath} reference ${absolute}.`);
78
90
  }
@@ -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>>;
@@ -85,7 +84,6 @@ export type AssetPackCatalogueEntry = {
85
84
  hostingMode?: string;
86
85
  externalUrl?: string;
87
86
  downloadUrl?: string;
88
- previewApp?: string;
89
87
  visibility?: string;
90
88
  releaseNotes?: string;
91
89
  listing?: AppListingConfig;
@@ -200,7 +198,6 @@ export type AssetPackTask = {
200
198
  hostingMode?: AppHostingMode;
201
199
  externalUrl?: string;
202
200
  downloadUrl?: string;
203
- previewApp?: string;
204
201
  visibility?: string;
205
202
  releaseNotes?: string;
206
203
  listing?: ResolvedListingAssets;
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');
@@ -396,6 +400,48 @@ function normalizeAssetSubcategoryValue(raw, category, errors, label) {
396
400
  }
397
401
  return normalized;
398
402
  }
403
+ function validateOptionalEmoji(value, hasEmojiField, errors) {
404
+ const emoji = value;
405
+ if (!hasEmojiField) {
406
+ return emoji;
407
+ }
408
+ if (emoji !== null && typeof emoji === 'string') {
409
+ const trimmed = emoji.trim();
410
+ if (!trimmed) {
411
+ return null;
412
+ }
413
+ if (Array.from(trimmed).length > 4) {
414
+ errors.push('emoji must be a single character');
415
+ return emoji;
416
+ }
417
+ return trimmed;
418
+ }
419
+ if (emoji !== null) {
420
+ errors.push('emoji must be a string or null');
421
+ }
422
+ return emoji;
423
+ }
424
+ function validateOptionalColor(value, hasColorField, errors) {
425
+ const color = value;
426
+ if (!hasColorField) {
427
+ return color;
428
+ }
429
+ if (color !== null && typeof color === 'string') {
430
+ const trimmed = color.trim();
431
+ if (!trimmed) {
432
+ return null;
433
+ }
434
+ if (!HEX_COLOR_REGEX.test(trimmed)) {
435
+ errors.push('color must be a hex RGB value like #FFAA00');
436
+ return color;
437
+ }
438
+ return trimmed.startsWith('#') ? trimmed.toUpperCase() : `#${trimmed.toUpperCase()}`;
439
+ }
440
+ if (color !== null) {
441
+ errors.push('color must be a string or null');
442
+ }
443
+ return color;
444
+ }
399
445
  function validateAppMetadata(entry) {
400
446
  const warnings = [];
401
447
  const errors = [];
@@ -403,42 +449,8 @@ function validateAppMetadata(entry) {
403
449
  if (missingFields.length > 0) {
404
450
  warnings.push(formatMissingMetadata(missingFields));
405
451
  }
406
- let emoji = entry.emoji;
407
- if (Object.prototype.hasOwnProperty.call(entry, 'emoji')) {
408
- if (emoji !== null && typeof emoji === 'string') {
409
- const trimmed = emoji.trim();
410
- if (!trimmed) {
411
- emoji = null;
412
- }
413
- else if (Array.from(trimmed).length > 4) {
414
- errors.push('emoji must be a single character');
415
- }
416
- else {
417
- emoji = trimmed;
418
- }
419
- }
420
- else if (emoji !== null) {
421
- errors.push('emoji must be a string or null');
422
- }
423
- }
424
- let color = entry.color;
425
- if (Object.prototype.hasOwnProperty.call(entry, 'color')) {
426
- if (color !== null && typeof color === 'string') {
427
- const trimmed = color.trim();
428
- if (!trimmed) {
429
- color = null;
430
- }
431
- else if (!HEX_COLOR_REGEX.test(trimmed)) {
432
- errors.push('color must be a hex RGB value like #FFAA00');
433
- }
434
- else {
435
- color = trimmed.startsWith('#') ? trimmed.toUpperCase() : `#${trimmed.toUpperCase()}`;
436
- }
437
- }
438
- else if (color !== null) {
439
- errors.push('color must be a string or null');
440
- }
441
- }
452
+ const emoji = validateOptionalEmoji(entry.emoji, Object.prototype.hasOwnProperty.call(entry, 'emoji'), errors);
453
+ const color = validateOptionalColor(entry.color, Object.prototype.hasOwnProperty.call(entry, 'color'), errors);
442
454
  let type;
443
455
  if (typeof entry.type === 'string' && entry.type.trim()) {
444
456
  const parsedType = (0, types_1.parseAppType)(entry.type.trim());
@@ -625,18 +637,17 @@ function buildAppTasks(rootDir, catalogues, options) {
625
637
  }
626
638
  serverPath = absoluteServerPath;
627
639
  }
628
- // Parse versioning fields (required in schema v2)
629
- let version;
640
+ // Parse versioning fields.
630
641
  const rawVersion = typeof entry.version === 'string' ? entry.version.trim() : '';
631
642
  if (!rawVersion) {
632
- 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").`);
633
644
  continue;
634
645
  }
635
646
  if (!SEMVER_REGEX.test(rawVersion)) {
636
647
  errors.push(`[${label}] Skipping ${rawName}: version must be in Major.Minor.Patch format (e.g., "1.0.0").`);
637
648
  continue;
638
649
  }
639
- version = rawVersion;
650
+ const version = rawVersion;
640
651
  const releaseNotes = typeof entry.releaseNotes === 'string' ? entry.releaseNotes : undefined;
641
652
  let versionVisibility;
642
653
  const rawVisibility = typeof entry.visibility === 'string' ? entry.visibility.trim().toUpperCase() : '';
@@ -936,7 +947,7 @@ function buildAppTasks(rootDir, catalogues, options) {
936
947
  category,
937
948
  subcategory,
938
949
  format: embeddedFormat,
939
- visibility: normalizeAssetVisibilityValue(embedded?.visibility, errors, `${label} embedded asset \"${embeddedName}\"`),
950
+ visibility: normalizeAssetVisibilityValue(embedded?.visibility, errors, `${label} embedded asset "${embeddedName}"`),
940
951
  shopListed: typeof embedded?.shopListed === 'boolean' ? embedded.shopListed : undefined,
941
952
  shopPriceCredits: Number.isInteger(embedded?.shopPriceCredits) ? Number(embedded.shopPriceCredits) : undefined,
942
953
  files,
@@ -1107,7 +1118,7 @@ function buildAssetTasks(catalogues) {
1107
1118
  username,
1108
1119
  remix,
1109
1120
  format,
1110
- visibility: normalizeAssetVisibilityValue(entry.visibility, errors, `${label} asset \"${name}\"`),
1121
+ visibility: normalizeAssetVisibilityValue(entry.visibility, errors, `${label} asset "${name}"`),
1111
1122
  shopListed: typeof entry.shopListed === 'boolean' ? entry.shopListed : undefined,
1112
1123
  shopPriceCredits: Number.isInteger(entry.shopPriceCredits) ? Number(entry.shopPriceCredits) : undefined,
1113
1124
  files,
@@ -1351,6 +1362,13 @@ function buildAssetPackTasks(catalogues) {
1351
1362
  }
1352
1363
  listing = resolvedListing;
1353
1364
  }
1365
+ const previewApp = typeof entry.previewApp === 'string'
1366
+ ? String(entry.previewApp).trim()
1367
+ : '';
1368
+ if (previewApp.length > 0) {
1369
+ errors.push(`[${label}] Skipping asset pack "${name}": previewApp is no longer supported during publish. Upload the pack first, then set previewAppVersionId explicitly with creations asset-packs update.`);
1370
+ continue;
1371
+ }
1354
1372
  tasks.push({
1355
1373
  kind: 'asset-pack',
1356
1374
  name,
@@ -1363,7 +1381,6 @@ function buildAssetPackTasks(catalogues) {
1363
1381
  hostingMode,
1364
1382
  externalUrl,
1365
1383
  downloadUrl,
1366
- previewApp: typeof entry.previewApp === 'string' && entry.previewApp.trim().length > 0 ? entry.previewApp.trim() : undefined,
1367
1384
  visibility,
1368
1385
  releaseNotes: typeof entry.releaseNotes === 'string' ? entry.releaseNotes : undefined,
1369
1386
  listing,
@@ -1385,6 +1402,20 @@ function resolveCatalogueEntries(rootDir, options = {}) {
1385
1402
  errors: discoveryErrors,
1386
1403
  };
1387
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
+ }
1388
1419
  const { tasks: appTasks, warnings: appWarnings, errors: appErrors } = buildAppTasks(rootDir, files, options);
1389
1420
  const { tasks: assetTasks, warnings: assetWarnings, errors: assetErrors } = buildAssetTasks(files);
1390
1421
  const { tasks: assetPackTasks, warnings: assetPackWarnings, errors: assetPackErrors } = buildAssetPackTasks(files);
@@ -12,14 +12,28 @@ const os_1 = __importDefault(require("os"));
12
12
  const config_1 = require("@playdrop/config");
13
13
  function mapPlatform(platform) {
14
14
  switch (platform) {
15
+ case 'aix':
16
+ return 'aix';
17
+ case 'android':
18
+ return 'android';
19
+ case 'cygwin':
20
+ return 'cygwin';
15
21
  case 'darwin':
16
22
  return 'macos';
23
+ case 'freebsd':
24
+ return 'freebsd';
25
+ case 'haiku':
26
+ return 'haiku';
17
27
  case 'win32':
18
28
  return 'windows';
19
29
  case 'linux':
20
30
  return 'linux';
21
- default:
22
- return platform || 'unknown';
31
+ case 'netbsd':
32
+ return 'netbsd';
33
+ case 'openbsd':
34
+ return 'openbsd';
35
+ case 'sunos':
36
+ return 'sunos';
23
37
  }
24
38
  }
25
39
  function detectPlatformVersion() {
@@ -91,7 +91,16 @@ function formatCountValue(value) {
91
91
  }
92
92
  function buildCountsSuffix(item) {
93
93
  const parts = [];
94
- if ((item.kind === 'app' || item.kind === 'asset') && typeof item.item.playCount === 'number') {
94
+ if (item.kind === 'app' && typeof item.item.playCount === 'number') {
95
+ if (typeof item.item.viewCount === 'number') {
96
+ parts.push(`view ${formatCountValue(item.item.viewCount)}`);
97
+ }
98
+ parts.push(`launch ${formatCountValue(item.item.playCount)}`);
99
+ }
100
+ if (item.kind === 'asset' && typeof item.item.playCount === 'number') {
101
+ if (typeof item.item.viewCount === 'number') {
102
+ parts.push(`view ${formatCountValue(item.item.viewCount)}`);
103
+ }
95
104
  parts.push(`play ${formatCountValue(item.item.playCount)}`);
96
105
  }
97
106
  if (typeof item.item.likeCount === 'number') {
@@ -35,6 +35,8 @@ function formatAppTypeSlug(type) {
35
35
  return 'template';
36
36
  case 'DEMO':
37
37
  return 'demo';
38
+ case undefined:
39
+ return 'game';
38
40
  default:
39
41
  return 'game';
40
42
  }
@@ -414,8 +416,7 @@ async function capture(targetArg, options = {}) {
414
416
  projectInfo,
415
417
  });
416
418
  signalHandler = () => {
417
- cleanup()
418
- .finally(() => process.exit(130));
419
+ void cleanup().finally(() => process.exit(130));
419
420
  };
420
421
  process.on('SIGINT', signalHandler);
421
422
  process.on('SIGTERM', signalHandler);