@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.
- package/README.md +43 -1
- package/config/client-meta.json +5 -6
- package/dist/catalogue-utils.js +9 -2
- package/dist/catalogue.d.ts +0 -1
- package/dist/catalogue.js +26 -8
- package/dist/commands/browse.js +1 -1
- package/dist/commands/capture.js +2 -2
- package/dist/commands/comments.js +3 -3
- package/dist/commands/create.js +108 -12
- package/dist/commands/createRemixContent.js +33 -3
- package/dist/commands/creations.js +2 -2
- package/dist/commands/credits.js +2 -2
- package/dist/commands/detail.js +1 -1
- package/dist/commands/dev.js +2 -2
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/generation.js +2 -2
- package/dist/commands/gettingStarted.js +1 -1
- package/dist/commands/init.js +31 -3
- package/dist/commands/login.d.ts +10 -10
- package/dist/commands/login.js +202 -79
- package/dist/commands/logout.js +1 -1
- package/dist/commands/notifications.js +3 -3
- package/dist/commands/versionsBrowse.js +1 -1
- package/dist/commands/whoami.js +9 -9
- package/dist/index.js +41 -25
- package/dist/messages.js +5 -5
- package/dist/taskSelection.js +2 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts +13 -2
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/client.js +21 -0
- package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +10 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/auth.js +94 -0
- package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +2 -2
- package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/payments.js +2 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts +16 -3
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +37 -4
- package/node_modules/@playdrop/config/client-meta.json +5 -6
- package/node_modules/@playdrop/config/dist/test/validateClientEnvironment.test.js +13 -0
- package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
- package/node_modules/@playdrop/types/dist/api.d.ts +204 -2
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/asset.d.ts +18 -50
- package/node_modules/@playdrop/types/dist/asset.d.ts.map +1 -1
- package/package.json +23 -2
package/README.md
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
# @playdrop/playdrop-cli
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
package/config/client-meta.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.3.
|
|
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.
|
|
29
|
+
"minimumVersion": "0.3.11",
|
|
30
30
|
"minimumBuild": 1
|
|
31
31
|
},
|
|
32
32
|
"admin": {
|
|
33
|
-
"minimumVersion": "0.3.
|
|
33
|
+
"minimumVersion": "0.3.11",
|
|
34
34
|
"minimumBuild": 1
|
|
35
35
|
},
|
|
36
36
|
"apple": {
|
|
37
|
-
"minimumVersion": "0.3.
|
|
37
|
+
"minimumVersion": "0.3.10",
|
|
38
38
|
"minimumBuild": 1
|
|
39
39
|
},
|
|
40
40
|
"cli": {
|
|
41
|
-
"minimumVersion": "0.3.
|
|
42
|
-
"minimumBuild": 1
|
|
41
|
+
"minimumVersion": "0.3.11"
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
}
|
package/dist/catalogue-utils.js
CHANGED
|
@@ -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 =
|
|
49
|
-
|
|
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
|
: [];
|
package/dist/catalogue.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
200
|
-
return { error: 'catalogue.json must
|
|
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
|
|
206
|
+
return { error: `catalogue.json forbids top-level "${key}"` };
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
|
-
const allowedKeys = new Set(['
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/dist/commands/browse.js
CHANGED
|
@@ -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
|
|
365
|
+
suggestions: ['Run "playdrop login" and retry.'],
|
|
366
366
|
};
|
|
367
367
|
}
|
|
368
368
|
return {
|
package/dist/commands/capture.js
CHANGED
|
@@ -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
|
|
330
|
-
'Use "playdrop
|
|
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
|
|
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
|
|
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
|
|
169
|
+
suggestions: ['Run "playdrop login" and retry.'],
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
if (apiError.status === 404) {
|
package/dist/commands/create.js
CHANGED
|
@@ -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 = {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
71
|
-
throw new Error(`catalogue.json at ${path} must
|
|
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
|
|
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
|
|
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
|
|
286
|
+
suggestions: ['Run "playdrop login" and retry.'],
|
|
287
287
|
}),
|
|
288
288
|
});
|
|
289
289
|
if (!handled) {
|
package/dist/commands/credits.js
CHANGED
|
@@ -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
|
|
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
|
|
83
|
+
suggestions: ['Run "playdrop login" and retry.'],
|
|
84
84
|
}),
|
|
85
85
|
});
|
|
86
86
|
if (!handled) {
|
package/dist/commands/detail.js
CHANGED
|
@@ -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
|
|
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 {
|
package/dist/commands/dev.js
CHANGED
|
@@ -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
|
|
124
|
-
'Use "playdrop
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7
|
+
console.log(' playdrop login');
|
|
8
8
|
console.log('');
|
|
9
9
|
console.log('2. Initialize a workspace');
|
|
10
10
|
console.log(' playdrop project init .');
|