@playdrop/playdrop-cli 0.6.1 → 0.6.4

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 (50) hide show
  1. package/README.md +4 -1
  2. package/config/client-meta.json +12 -4
  3. package/dist/apps/index.d.ts +3 -2
  4. package/dist/apps/index.js +4 -2
  5. package/dist/apps/upload.js +3 -0
  6. package/dist/apps/validate.d.ts +2 -0
  7. package/dist/apps/validate.js +379 -0
  8. package/dist/catalogue.d.ts +10 -1
  9. package/dist/catalogue.js +44 -0
  10. package/dist/commands/create.js +5 -1
  11. package/dist/commands/detail.js +10 -3
  12. package/dist/commands/upload-content.js +4 -0
  13. package/dist/commands/upload.js +11 -10
  14. package/dist/commands/validate.js +1 -0
  15. package/dist/commands/versionsBrowse.js +8 -1
  16. package/dist/taskSelection.js +2 -0
  17. package/node_modules/@playdrop/api-client/dist/client.d.ts +4 -5
  18. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  19. package/node_modules/@playdrop/api-client/dist/client.js +12 -0
  20. package/node_modules/@playdrop/api-client/dist/core/errors.d.ts.map +1 -1
  21. package/node_modules/@playdrop/api-client/dist/core/errors.js +4 -1
  22. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  23. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  24. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  25. package/node_modules/@playdrop/api-client/dist/domains/assets.js +2 -0
  26. package/node_modules/@playdrop/api-client/dist/index.d.ts +4 -5
  27. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  28. package/node_modules/@playdrop/api-client/dist/index.js +3 -0
  29. package/node_modules/@playdrop/config/client-meta.json +12 -4
  30. package/node_modules/@playdrop/config/dist/test/validateClientEnvironment.test.js +29 -1
  31. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  32. package/node_modules/@playdrop/types/dist/api.d.ts +31 -1
  33. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  34. package/node_modules/@playdrop/types/dist/api.js +3 -1
  35. package/node_modules/@playdrop/types/dist/asset-pack.d.ts +6 -0
  36. package/node_modules/@playdrop/types/dist/asset-pack.d.ts.map +1 -1
  37. package/node_modules/@playdrop/types/dist/asset.d.ts +6 -0
  38. package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
  39. package/node_modules/@playdrop/types/dist/content-license.d.ts +13 -0
  40. package/node_modules/@playdrop/types/dist/content-license.d.ts.map +1 -0
  41. package/node_modules/@playdrop/types/dist/content-license.js +18 -0
  42. package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
  43. package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
  44. package/node_modules/@playdrop/types/dist/index.js +1 -0
  45. package/node_modules/@playdrop/types/dist/owned-assets.d.ts +2 -0
  46. package/node_modules/@playdrop/types/dist/owned-assets.d.ts.map +1 -1
  47. package/node_modules/@playdrop/types/dist/owned-assets.js +1 -0
  48. package/node_modules/@playdrop/types/dist/version.d.ts +8 -0
  49. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  50. package/package.json +1 -1
package/README.md CHANGED
@@ -4,6 +4,8 @@ Official Playdrop CLI for the live [Playdrop](https://www.playdrop.ai/) platform
4
4
 
5
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
6
 
7
+ For AI coding workflows, pair the CLI with the public Playdrop plugin. The plugin is the preferred public setup and contains many specialist Playdrop skills for Codex, Claude Code, and Cursor.
8
+
7
9
  ## Install
8
10
 
9
11
  ```bash
@@ -33,11 +35,12 @@ playdrop project publish .
33
35
 
34
36
  - Website: [playdrop.ai](https://www.playdrop.ai/)
35
37
  - Getting started: [playdrop.ai/getting-started](https://www.playdrop.ai/getting-started)
38
+ - Plugin docs: [playdrop.ai/docs/plugin](https://www.playdrop.ai/docs/plugin)
36
39
  - CLI docs: [playdrop.ai/docs/cli](https://www.playdrop.ai/docs/cli)
37
40
  - Runtime docs: [playdrop.ai/docs/runtime](https://www.playdrop.ai/docs/runtime)
38
41
  - Templates and demos: [playdrop.ai/docs/examples#templates-and-demos](https://www.playdrop.ai/docs/examples#templates-and-demos)
39
42
  - Canonical public plugin repo: [github.com/playdrop-ai/playdrop-plugin](https://github.com/playdrop-ai/playdrop-plugin)
40
- - Legacy public skill surface: [skills.sh/playdrop-ai/playdrop-skills/playdrop](https://skills.sh/playdrop-ai/playdrop-skills/playdrop)
43
+ - Legacy public skill compatibility: [skills.sh/playdrop-ai/playdrop-skills/playdrop](https://skills.sh/playdrop-ai/playdrop-skills/playdrop)
41
44
 
42
45
  ## Live Examples
43
46
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.1",
2
+ "version": "0.6.4",
3
3
  "build": 1,
4
4
  "platforms": {
5
5
  "ios": {
@@ -26,19 +26,27 @@
26
26
  },
27
27
  "clients": {
28
28
  "web": {
29
- "minimumVersion": "0.6.1",
29
+ "minimumVersion": "0.6.4",
30
30
  "minimumBuild": 1
31
31
  },
32
32
  "admin": {
33
- "minimumVersion": "0.6.1",
33
+ "minimumVersion": "0.6.4",
34
34
  "minimumBuild": 1
35
35
  },
36
36
  "apple": {
37
37
  "minimumVersion": "0.3.10",
38
38
  "minimumBuild": 1
39
39
  },
40
+ "apple-playdrop": {
41
+ "minimumVersion": "0.3.10",
42
+ "minimumBuild": 1
43
+ },
44
+ "apple-games": {
45
+ "minimumVersion": "0.3.10",
46
+ "minimumBuild": 1
47
+ },
40
48
  "cli": {
41
- "minimumVersion": "0.6.1"
49
+ "minimumVersion": "0.6.4"
42
50
  }
43
51
  }
44
52
  }
@@ -1,11 +1,12 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
2
  import type { AppTask } from '../catalogue';
3
- import { validateAppTask, runFormatScript } from './validate';
3
+ import { collectAppValidationWarnings, validateAppTask, runFormatScript } from './validate';
4
4
  import { buildApp, type AppBuildArtifacts } from './build';
5
5
  import { uploadApp, type AppUploadResult } from './upload';
6
6
  export type AppPipelineResult = {
7
7
  artifacts: AppBuildArtifacts | null;
8
8
  upload: AppUploadResult;
9
+ warnings: string[];
9
10
  };
10
11
  export type AppPipelineOptions = {
11
12
  skipEcs?: boolean;
@@ -14,4 +15,4 @@ export type AppPipelineOptions = {
14
15
  creatorUsername?: string;
15
16
  };
16
17
  export declare function runAppPipeline(client: ApiClient, task: AppTask, options?: AppPipelineOptions): Promise<AppPipelineResult>;
17
- export { validateAppTask, buildApp, uploadApp, runFormatScript };
18
+ export { collectAppValidationWarnings, validateAppTask, buildApp, uploadApp, runFormatScript };
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runFormatScript = exports.uploadApp = exports.buildApp = exports.validateAppTask = void 0;
3
+ exports.runFormatScript = exports.uploadApp = exports.buildApp = exports.validateAppTask = exports.collectAppValidationWarnings = void 0;
4
4
  exports.runAppPipeline = runAppPipeline;
5
5
  const validate_1 = require("./validate");
6
+ Object.defineProperty(exports, "collectAppValidationWarnings", { enumerable: true, get: function () { return validate_1.collectAppValidationWarnings; } });
6
7
  Object.defineProperty(exports, "validateAppTask", { enumerable: true, get: function () { return validate_1.validateAppTask; } });
7
8
  Object.defineProperty(exports, "runFormatScript", { enumerable: true, get: function () { return validate_1.runFormatScript; } });
8
9
  const build_1 = require("./build");
@@ -14,6 +15,7 @@ async function runAppPipeline(client, task, options) {
14
15
  // External apps don't need to be built - they're hosted elsewhere
15
16
  const isExternal = task.hostingMode === 'EXTERNAL' || !!task.externalUrl;
16
17
  const artifacts = isExternal ? null : await (0, build_1.buildApp)(task);
18
+ const warnings = (0, validate_1.collectAppValidationWarnings)(task, isExternal ? 'source' : 'bundle');
17
19
  const uploadOptions = {
18
20
  skipEcs: options?.skipEcs,
19
21
  skipReview: options?.skipReview,
@@ -21,5 +23,5 @@ async function runAppPipeline(client, task, options) {
21
23
  creatorUsername: options?.creatorUsername,
22
24
  };
23
25
  const upload = await (0, upload_1.uploadApp)(client, task, artifacts, uploadOptions);
24
- return { artifacts, upload };
26
+ return { artifacts, upload, warnings };
25
27
  }
@@ -114,6 +114,7 @@ async function prepareOwnedAssetUpload(task) {
114
114
  subcategory: task.subcategory,
115
115
  format: task.format || (0, node_path_1.extname)(files[0]?.filename || '').replace(/^\./, '').toUpperCase() || 'GLB',
116
116
  visibility: task.visibility,
117
+ license: task.license,
117
118
  assetSpec: task.assetSpec,
118
119
  shopListed: task.shopListed,
119
120
  shopPriceCredits: task.shopPriceCredits,
@@ -253,6 +254,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
253
254
  version: task.version,
254
255
  releaseNotes: task.releaseNotes,
255
256
  visibility: task.versionVisibility,
257
+ license: task.license,
256
258
  remixRef: task.remix,
257
259
  usesAssets: task.uses.assets.map((entry) => ({
258
260
  ref: entry.ref,
@@ -313,6 +315,7 @@ async function uploadAppVersion(client, task, artifacts, options) {
313
315
  format: ownedAsset.format,
314
316
  assetSpec: ownedAsset.assetSpec,
315
317
  visibility: ownedAsset.visibility,
318
+ license: ownedAsset.license,
316
319
  shopListed: ownedAsset.shopListed,
317
320
  shopPriceCredits: ownedAsset.shopPriceCredits,
318
321
  files: ownedAsset.files.map((file) => ({
@@ -1,3 +1,5 @@
1
1
  import type { AppTask } from '../catalogue';
2
+ export type AppValidationWarningMode = 'source' | 'bundle';
3
+ export declare function collectAppValidationWarnings(task: AppTask, mode?: AppValidationWarningMode): string[];
2
4
  export declare function runFormatScript(task: AppTask): Promise<boolean>;
3
5
  export declare function validateAppTask(task: AppTask): Promise<void>;
@@ -1,14 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectAppValidationWarnings = collectAppValidationWarnings;
3
4
  exports.runFormatScript = runFormatScript;
4
5
  exports.validateAppTask = validateAppTask;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_path_1 = require("node:path");
8
+ const semver_1 = require("semver");
9
+ const clientInfo_1 = require("../clientInfo");
7
10
  const build_1 = require("./build");
8
11
  const projectValidateCache = new Map();
9
12
  const projectFormatCache = new Map();
10
13
  const LEGACY_SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.html']);
11
14
  const LEGACY_SCAN_IGNORED_DIRS = new Set(['node_modules', '.git', 'dist-test', 'coverage']);
15
+ const SDK_SCAN_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte', '.astro', '.html']);
16
+ const SDK_SCAN_IGNORED_DIRS = new Set([
17
+ 'node_modules',
18
+ '.git',
19
+ 'dist',
20
+ 'build',
21
+ 'out',
22
+ '.next',
23
+ '.turbo',
24
+ '.cache',
25
+ '.svelte-kit',
26
+ 'coverage',
27
+ 'tmp',
28
+ 'logs',
29
+ 'test',
30
+ 'tests',
31
+ '__tests__',
32
+ ]);
33
+ const SDK_ENTRY_HTML_FILENAMES = ['index.html', 'main.html', 'template.html'];
34
+ const SDK_ENTRY_SOURCE_BASENAMES = ['main', 'index', 'app', 'bootstrap', 'entry', 'client'];
35
+ const SDK_ENTRY_ROOT_DIRS = ['src', 'app', 'pages'];
36
+ const SDK_SPECIAL_ENTRY_PATHS = ['app/page', 'app/layout', 'pages/index', 'pages/_app'];
12
37
  const LEGACY_SDK_SYMBOL_PATTERNS = [
13
38
  { symbol: 'selectedAvatarKey', pattern: /\bselectedAvatarKey\b/g },
14
39
  { symbol: 'sdk.entities', pattern: /\bsdk\s*\.\s*entities\b/g },
@@ -16,6 +41,21 @@ const LEGACY_SDK_SYMBOL_PATTERNS = [
16
41
  { symbol: 'sdk.assets.block', pattern: /\bsdk\s*\.\s*assets\s*\.\s*block\b/g },
17
42
  { symbol: 'loadEntity(', pattern: /\.\s*loadEntity\s*\(/g },
18
43
  ];
44
+ const SDK_REFERENCE_PATTERNS = [
45
+ /https:\/\/assets\.playdrop\.ai\/sdk\/playdrop\.js/g,
46
+ /@playdrop\/sdk(?:\/browser)?/g,
47
+ ];
48
+ const SDK_INIT_PATTERNS = [
49
+ /\bwindow\s*\.\s*playdrop\s*\.\s*init\s*\(/g,
50
+ /\bplaydrop\s*\.\s*init\s*\(/g,
51
+ /\bsdk\s*\.\s*initialize\s*\(/g,
52
+ ];
53
+ const LOCAL_HTML_SCRIPT_PATTERN = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
54
+ const LOCAL_IMPORT_PATTERNS = [
55
+ /(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?["']([^"']+)["']/g,
56
+ /import\s*\(\s*["']([^"']+)["']\s*\)/g,
57
+ ];
58
+ const LOCAL_SCRIPT_COMMAND_PATTERN = /(?:^|(?:&&|\|\||;)\s*)(?:node(?:\s+--[^\s"'`;|&]+)*|tsx|ts-node(?:-esm)?|bun|deno\s+run)\s+(["']?)([^"'`\s;&|]+)\1/g;
19
59
  function ensureWithinProject(task) {
20
60
  const relativePath = (0, node_path_1.relative)(task.projectDir, task.filePath);
21
61
  const segments = relativePath.split(/[/\\]+/);
@@ -81,6 +121,345 @@ function scanForLegacySdkSymbols(task) {
81
121
  throw new Error(`[apps][validate] legacy SDK symbols detected for ${task.name}: ${details}${suffix}. Remove entity-era APIs before publishing.`);
82
122
  }
83
123
  }
124
+ function stripSpecifierDecorators(specifier) {
125
+ return specifier.split(/[?#]/, 1)[0]?.trim() ?? '';
126
+ }
127
+ function safeReadFile(filePath) {
128
+ try {
129
+ return (0, node_fs_1.readFileSync)(filePath, 'utf8');
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ function isIgnoredSdkScanDirectory(name) {
136
+ return SDK_SCAN_IGNORED_DIRS.has(name);
137
+ }
138
+ function resolveSdkModulePath(basePath) {
139
+ const tryFile = (candidate) => {
140
+ if (!(0, node_fs_1.existsSync)(candidate)) {
141
+ return null;
142
+ }
143
+ try {
144
+ return (0, node_fs_1.statSync)(candidate).isFile() ? candidate : null;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ };
150
+ const direct = tryFile(basePath);
151
+ if (direct) {
152
+ return direct;
153
+ }
154
+ if (!(0, node_path_1.extname)(basePath)) {
155
+ for (const extension of SDK_SCAN_EXTENSIONS) {
156
+ const withExtension = tryFile(`${basePath}${extension}`);
157
+ if (withExtension) {
158
+ return withExtension;
159
+ }
160
+ }
161
+ }
162
+ if ((0, node_fs_1.existsSync)(basePath)) {
163
+ try {
164
+ if ((0, node_fs_1.statSync)(basePath).isDirectory()) {
165
+ for (const extension of SDK_SCAN_EXTENSIONS) {
166
+ const indexCandidate = tryFile((0, node_path_1.join)(basePath, `index${extension}`));
167
+ if (indexCandidate) {
168
+ return indexCandidate;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+ function resolveLocalSpecifier(task, sourceFilePath, specifier) {
180
+ const normalized = stripSpecifierDecorators(specifier);
181
+ if (!normalized) {
182
+ return null;
183
+ }
184
+ if (normalized.startsWith('/')) {
185
+ return resolveSdkModulePath((0, node_path_1.resolve)(task.projectDir, `.${normalized}`));
186
+ }
187
+ if (!normalized.startsWith('.')) {
188
+ return null;
189
+ }
190
+ return resolveSdkModulePath((0, node_path_1.resolve)((0, node_path_1.dirname)(sourceFilePath), normalized));
191
+ }
192
+ function extractLocalHtmlScriptFiles(task, filePath, source) {
193
+ const matches = [];
194
+ LOCAL_HTML_SCRIPT_PATTERN.lastIndex = 0;
195
+ let match = null;
196
+ while ((match = LOCAL_HTML_SCRIPT_PATTERN.exec(source)) !== null) {
197
+ const specifier = match[1];
198
+ if (!specifier) {
199
+ continue;
200
+ }
201
+ const resolved = resolveLocalSpecifier(task, filePath, specifier);
202
+ if (resolved) {
203
+ matches.push(resolved);
204
+ }
205
+ }
206
+ return matches;
207
+ }
208
+ function extractLocalImportedFiles(task, filePath, source) {
209
+ const matches = [];
210
+ for (const pattern of LOCAL_IMPORT_PATTERNS) {
211
+ pattern.lastIndex = 0;
212
+ let match = null;
213
+ while ((match = pattern.exec(source)) !== null) {
214
+ const specifier = match[1];
215
+ if (!specifier) {
216
+ continue;
217
+ }
218
+ const resolved = resolveLocalSpecifier(task, filePath, specifier);
219
+ if (resolved) {
220
+ matches.push(resolved);
221
+ }
222
+ }
223
+ }
224
+ return matches;
225
+ }
226
+ function addFileIfPresent(target, filePath) {
227
+ if (!(0, node_fs_1.existsSync)(filePath)) {
228
+ return;
229
+ }
230
+ try {
231
+ if ((0, node_fs_1.statSync)(filePath).isFile()) {
232
+ target.add(filePath);
233
+ }
234
+ }
235
+ catch {
236
+ // Ignore unreadable candidates and continue scanning the rest.
237
+ }
238
+ }
239
+ function extractLocalScriptCommandFiles(task) {
240
+ if (!task.packageJsonPath) {
241
+ return [];
242
+ }
243
+ const rawPackageJson = safeReadFile(task.packageJsonPath);
244
+ if (!rawPackageJson) {
245
+ return [];
246
+ }
247
+ let packageJson;
248
+ try {
249
+ packageJson = JSON.parse(rawPackageJson);
250
+ }
251
+ catch {
252
+ return [];
253
+ }
254
+ const scripts = packageJson?.scripts;
255
+ if (!scripts || typeof scripts !== 'object') {
256
+ return [];
257
+ }
258
+ const matches = new Set();
259
+ Object.values(scripts).forEach((scriptValue) => {
260
+ if (typeof scriptValue !== 'string') {
261
+ return;
262
+ }
263
+ LOCAL_SCRIPT_COMMAND_PATTERN.lastIndex = 0;
264
+ let match = null;
265
+ while ((match = LOCAL_SCRIPT_COMMAND_PATTERN.exec(scriptValue)) !== null) {
266
+ const specifier = stripSpecifierDecorators(match[2] ?? '');
267
+ if (!specifier || specifier.startsWith('-')) {
268
+ continue;
269
+ }
270
+ const resolvedPath = specifier.startsWith('/')
271
+ ? resolveSdkModulePath((0, node_path_1.resolve)(specifier))
272
+ : resolveSdkModulePath((0, node_path_1.resolve)(task.projectDir, specifier));
273
+ if (resolvedPath) {
274
+ matches.add(resolvedPath);
275
+ }
276
+ }
277
+ });
278
+ return [...matches];
279
+ }
280
+ function collectSourceSeedFiles(task) {
281
+ const seeds = new Set();
282
+ const entryRelativePath = (0, node_path_1.relative)(task.projectDir, task.filePath);
283
+ const entrySegments = entryRelativePath.split(/[/\\]+/);
284
+ const entryCrossesIgnoredSourceDirectory = entrySegments.some((segment) => isIgnoredSdkScanDirectory(segment));
285
+ if (entryRelativePath && !entryRelativePath.startsWith('..') && !entryCrossesIgnoredSourceDirectory) {
286
+ addFileIfPresent(seeds, task.filePath);
287
+ }
288
+ SDK_ENTRY_HTML_FILENAMES.forEach((filename) => {
289
+ addFileIfPresent(seeds, (0, node_path_1.join)(task.projectDir, filename));
290
+ addFileIfPresent(seeds, (0, node_path_1.join)(task.projectDir, 'public', filename));
291
+ });
292
+ const sourceExtensions = [...SDK_SCAN_EXTENSIONS].filter((extension) => extension !== '.html');
293
+ const addEntryBasenames = (directory) => {
294
+ SDK_ENTRY_SOURCE_BASENAMES.forEach((basename) => {
295
+ sourceExtensions.forEach((extension) => {
296
+ addFileIfPresent(seeds, (0, node_path_1.join)(directory, `${basename}${extension}`));
297
+ });
298
+ });
299
+ };
300
+ addEntryBasenames(task.projectDir);
301
+ SDK_ENTRY_ROOT_DIRS.forEach((directory) => {
302
+ addEntryBasenames((0, node_path_1.join)(task.projectDir, directory));
303
+ });
304
+ SDK_SPECIAL_ENTRY_PATHS.forEach((relativePath) => {
305
+ sourceExtensions.forEach((extension) => {
306
+ addFileIfPresent(seeds, (0, node_path_1.join)(task.projectDir, `${relativePath}${extension}`));
307
+ });
308
+ });
309
+ extractLocalScriptCommandFiles(task).forEach((filePath) => {
310
+ const relativePath = (0, node_path_1.relative)(task.projectDir, filePath);
311
+ const segments = relativePath.split(/[/\\]+/);
312
+ const crossesIgnoredSourceDirectory = segments.some((segment) => isIgnoredSdkScanDirectory(segment));
313
+ if (!relativePath || relativePath.startsWith('..') || crossesIgnoredSourceDirectory) {
314
+ return;
315
+ }
316
+ seeds.add(filePath);
317
+ });
318
+ return [...seeds];
319
+ }
320
+ function collectBundleSeedFiles(task) {
321
+ const seeds = new Set();
322
+ addFileIfPresent(seeds, task.filePath);
323
+ return [...seeds];
324
+ }
325
+ function collectSdkDetectionFiles(task, mode) {
326
+ const queue = mode === 'bundle' ? collectBundleSeedFiles(task) : collectSourceSeedFiles(task);
327
+ const queued = new Set(queue);
328
+ const visited = new Set();
329
+ if (task.packageJsonPath) {
330
+ visited.add(task.packageJsonPath);
331
+ }
332
+ while (queue.length > 0) {
333
+ const filePath = queue.shift();
334
+ if (!filePath || visited.has(filePath)) {
335
+ continue;
336
+ }
337
+ if (!(0, node_fs_1.existsSync)(filePath)) {
338
+ continue;
339
+ }
340
+ let isFile = false;
341
+ try {
342
+ isFile = (0, node_fs_1.statSync)(filePath).isFile();
343
+ }
344
+ catch {
345
+ isFile = false;
346
+ }
347
+ if (!isFile) {
348
+ continue;
349
+ }
350
+ const extension = (0, node_path_1.extname)(filePath).toLowerCase();
351
+ if (extension && !SDK_SCAN_EXTENSIONS.has(extension)) {
352
+ continue;
353
+ }
354
+ visited.add(filePath);
355
+ const source = safeReadFile(filePath);
356
+ if (!source) {
357
+ continue;
358
+ }
359
+ const localReferences = extension === '.html'
360
+ ? extractLocalHtmlScriptFiles(task, filePath, source)
361
+ : extractLocalImportedFiles(task, filePath, source);
362
+ localReferences.forEach((resolvedPath) => {
363
+ const relativePath = (0, node_path_1.relative)(task.projectDir, resolvedPath);
364
+ const segments = relativePath.split(/[/\\]+/);
365
+ const crossesIgnoredSourceDirectory = mode === 'source'
366
+ && segments.some((segment) => isIgnoredSdkScanDirectory(segment));
367
+ if (!relativePath || relativePath.startsWith('..') || crossesIgnoredSourceDirectory) {
368
+ return;
369
+ }
370
+ if (!queued.has(resolvedPath) && !visited.has(resolvedPath)) {
371
+ queue.push(resolvedPath);
372
+ queued.add(resolvedPath);
373
+ }
374
+ });
375
+ }
376
+ return [...visited];
377
+ }
378
+ function detectSdkUsage(task, mode) {
379
+ let hasSdkReference = false;
380
+ let hasSdkInit = false;
381
+ const files = collectSdkDetectionFiles(task, mode);
382
+ for (const filePath of files) {
383
+ if (hasSdkReference && hasSdkInit) {
384
+ break;
385
+ }
386
+ const source = safeReadFile(filePath);
387
+ if (!source) {
388
+ continue;
389
+ }
390
+ if (!hasSdkReference) {
391
+ hasSdkReference = SDK_REFERENCE_PATTERNS.some((pattern) => {
392
+ pattern.lastIndex = 0;
393
+ return pattern.test(source);
394
+ });
395
+ }
396
+ if (!hasSdkInit) {
397
+ hasSdkInit = SDK_INIT_PATTERNS.some((pattern) => {
398
+ pattern.lastIndex = 0;
399
+ return pattern.test(source);
400
+ });
401
+ }
402
+ }
403
+ return { hasSdkReference, hasSdkInit };
404
+ }
405
+ function collectOutdatedSdkVersionWarnings(task) {
406
+ if (!task.packageJsonPath) {
407
+ return [];
408
+ }
409
+ const rawPackageJson = safeReadFile(task.packageJsonPath);
410
+ if (!rawPackageJson) {
411
+ return [];
412
+ }
413
+ let packageJson;
414
+ try {
415
+ packageJson = JSON.parse(rawPackageJson);
416
+ }
417
+ catch {
418
+ return [];
419
+ }
420
+ const cliVersion = (0, clientInfo_1.getCliVersion)();
421
+ if (!(0, semver_1.valid)(cliVersion)) {
422
+ return [];
423
+ }
424
+ const sdkTypesDependency = typeof packageJson?.devDependencies?.['playdrop-sdk-types'] === 'string'
425
+ ? packageJson.devDependencies['playdrop-sdk-types']
426
+ : typeof packageJson?.dependencies?.['playdrop-sdk-types'] === 'string'
427
+ ? packageJson.dependencies['playdrop-sdk-types']
428
+ : '';
429
+ const sdkTypesVersion = typeof packageJson?.playdrop?.sdkTypesVersion === 'string'
430
+ ? packageJson.playdrop.sdkTypesVersion.trim()
431
+ : '';
432
+ const usesVendoredSdkTypes = sdkTypesDependency.includes('playdrop-sdk-types')
433
+ && (sdkTypesDependency.startsWith('file:') || sdkTypesDependency.includes('.tgz'));
434
+ if (!usesVendoredSdkTypes
435
+ || !(0, semver_1.valid)(sdkTypesVersion)
436
+ || !(0, semver_1.lt)(sdkTypesVersion, cliVersion)) {
437
+ return [];
438
+ }
439
+ return [
440
+ `[apps][validate] ${task.name} vendors playdrop-sdk-types.tgz at ${sdkTypesVersion}, which is older than this CLI version ${cliVersion}. Refresh vendor/playdrop-sdk-types.tgz and package.json playdrop.sdkTypesVersion before upload.`,
441
+ ];
442
+ }
443
+ function collectAppValidationWarnings(task, mode = 'source') {
444
+ const isExternal = task.hostingMode === 'EXTERNAL' || !!task.externalUrl;
445
+ if (isExternal) {
446
+ return [];
447
+ }
448
+ const primaryDetection = detectSdkUsage(task, mode);
449
+ const sourceDetection = mode === 'bundle'
450
+ ? detectSdkUsage(task, 'source')
451
+ : primaryDetection;
452
+ const hasSdkReference = primaryDetection.hasSdkReference || sourceDetection.hasSdkReference;
453
+ const hasSdkInit = primaryDetection.hasSdkInit || sourceDetection.hasSdkInit;
454
+ const warnings = [];
455
+ if (!hasSdkReference) {
456
+ warnings.push(`[apps][validate] Could not detect the Playdrop SDK loader or @playdrop/sdk import for ${task.name}. We could not find https://assets.playdrop.ai/sdk/playdrop.js or @playdrop/sdk in the app files, so it might not work once uploaded.`);
457
+ }
458
+ if (!hasSdkInit) {
459
+ warnings.push(`[apps][validate] Could not detect Playdrop SDK initialization for ${task.name}. We could not find playdrop.init() in the app files, so it might not work once uploaded.`);
460
+ }
461
+ return [...warnings, ...collectOutdatedSdkVersionWarnings(task)];
462
+ }
84
463
  async function runFormatScript(task) {
85
464
  if (!task.packageJsonPath || !task.hasFormatScript) {
86
465
  return false;
@@ -1,4 +1,4 @@
1
- import { type AppSurface, type AppType, type AppHostingMode, type AppAuthMode, type AppControllerMode, type AppVersionVisibility, type AppAchievementCatalogueDefinition, type AppLeaderboardCatalogueDefinition, type PlayerMetaDefinitionStatus, type CatalogueTagGroupDefinition, type AppMetadataAssetSpecSupport, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind } from '@playdrop/types';
1
+ import { type AppSurface, type AppType, type AppHostingMode, type AppAuthMode, type AppControllerMode, type AppVersionVisibility, type AppAchievementCatalogueDefinition, type AppLeaderboardCatalogueDefinition, type PlayerMetaDefinitionStatus, type CatalogueTagGroupDefinition, type AppMetadataAssetSpecSupport, type AssetSpecContract, type AssetSpecStatus, type AssetSpecValidationKind, type ContentLicense } from '@playdrop/types';
2
2
  import { type AssetSpecSupportDeclaration } from './assetSpecs';
3
3
  export type CatalogueJson = {
4
4
  apps?: Array<Record<string, unknown>>;
@@ -44,6 +44,7 @@ export type AppCatalogueEntry = {
44
44
  version?: string;
45
45
  releaseNotes?: string;
46
46
  visibility?: string;
47
+ license?: string;
47
48
  hostingMode?: string;
48
49
  authMode?: string;
49
50
  controllerMode?: string;
@@ -76,6 +77,7 @@ export type AssetCatalogueEntry = {
76
77
  username?: string;
77
78
  files?: Record<string, string>;
78
79
  visibility?: string;
80
+ license?: string;
79
81
  tags?: string[];
80
82
  shopListed?: boolean;
81
83
  shopPriceCredits?: number;
@@ -107,6 +109,7 @@ export type OwnedAssetCatalogueEntry = {
107
109
  format?: string;
108
110
  files?: Record<string, string>;
109
111
  visibility?: string;
112
+ license?: string;
110
113
  shopListed?: boolean;
111
114
  shopPriceCredits?: number;
112
115
  };
@@ -121,6 +124,7 @@ export type AssetPackCatalogueEntry = {
121
124
  externalUrl?: string;
122
125
  downloadUrl?: string;
123
126
  visibility?: string;
127
+ license?: string;
124
128
  releaseNotes?: string;
125
129
  listing?: AppListingConfig;
126
130
  tags?: string[];
@@ -176,6 +180,7 @@ export type AppTask = {
176
180
  version?: string;
177
181
  releaseNotes?: string;
178
182
  versionVisibility?: AppVersionVisibility;
183
+ license: ContentLicense;
179
184
  tags: string[];
180
185
  hostingMode?: AppHostingMode;
181
186
  authMode?: AppAuthMode;
@@ -212,6 +217,7 @@ export type AssetTask = {
212
217
  remix?: string;
213
218
  format?: string;
214
219
  visibility?: string;
220
+ license: ContentLicense;
215
221
  tags: string[];
216
222
  shopListed?: boolean;
217
223
  shopPriceCredits?: number;
@@ -255,6 +261,7 @@ export type OwnedAssetTask = {
255
261
  assetSpecContract?: AssetSpecContract;
256
262
  format?: string;
257
263
  visibility?: string;
264
+ license: ContentLicense;
258
265
  shopListed?: boolean;
259
266
  shopPriceCredits?: number;
260
267
  files: Record<string, string>;
@@ -269,6 +276,7 @@ export type PackOwnedAssetTask = {
269
276
  assetSpecContract?: AssetSpecContract;
270
277
  format?: string;
271
278
  visibility?: string;
279
+ license: ContentLicense;
272
280
  shopListed?: boolean;
273
281
  shopPriceCredits?: number;
274
282
  files: Record<string, string>;
@@ -289,6 +297,7 @@ export type AssetPackTask = {
289
297
  externalUrl?: string;
290
298
  downloadUrl?: string;
291
299
  visibility?: string;
300
+ license: ContentLicense;
292
301
  releaseNotes?: string;
293
302
  listing?: ResolvedListingAssets;
294
303
  relations?: Array<{