@myvillage/cli 1.10.0 → 1.10.1
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/package.json +1 -1
- package/src/commands/deploy.js +126 -3
- package/src/commands/game.js +81 -0
- package/src/index.js +6 -0
- package/src/utils/templates.js +4 -2
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
-
import { join, resolve, relative } from 'path';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, resolve, relative, extname } from 'path';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
6
8
|
import { villageSpinner, brand } from '../utils/brand.js';
|
|
7
9
|
import { isAuthenticated } from '../utils/auth.js';
|
|
8
|
-
import { deployGame } from '../utils/api.js';
|
|
10
|
+
import { deployGame, getGameUploadUrl, confirmGameUpload } from '../utils/api.js';
|
|
9
11
|
|
|
10
12
|
function readPackageJson(dir) {
|
|
11
13
|
const pkgPath = join(dir, 'package.json');
|
|
@@ -156,6 +158,13 @@ async function deployThreeJsGame(projectDir, pkg) {
|
|
|
156
158
|
console.log(` Status: ${formatStatus(result.status)}`);
|
|
157
159
|
if (result.slug) console.log(brand.teal(` Slug: ${result.slug}`));
|
|
158
160
|
if (result.gameId) console.log(brand.teal(` Game ID: ${result.gameId}`));
|
|
161
|
+
|
|
162
|
+
// Prompt for thumbnail/banner images
|
|
163
|
+
const finalGameId = result.gameId || pkg.myvillage.gameId;
|
|
164
|
+
if (finalGameId) {
|
|
165
|
+
await promptForImages(finalGameId);
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
console.log();
|
|
160
169
|
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
161
170
|
console.log();
|
|
@@ -258,6 +267,13 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
258
267
|
console.log(` Platform: ${platform}`);
|
|
259
268
|
if (result.slug) console.log(brand.teal(` Slug: ${result.slug}`));
|
|
260
269
|
if (result.gameId) console.log(brand.teal(` Game ID: ${result.gameId}`));
|
|
270
|
+
|
|
271
|
+
// Prompt for thumbnail/banner images
|
|
272
|
+
const finalGameId = result.gameId || mvConfig.gameId;
|
|
273
|
+
if (finalGameId) {
|
|
274
|
+
await promptForImages(finalGameId);
|
|
275
|
+
}
|
|
276
|
+
|
|
261
277
|
console.log();
|
|
262
278
|
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
263
279
|
console.log();
|
|
@@ -274,6 +290,113 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
274
290
|
}
|
|
275
291
|
}
|
|
276
292
|
|
|
293
|
+
// ── Image upload helpers ──
|
|
294
|
+
|
|
295
|
+
const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
296
|
+
const IMAGE_CONTENT_TYPES = {
|
|
297
|
+
'.png': 'image/png',
|
|
298
|
+
'.jpg': 'image/jpeg',
|
|
299
|
+
'.jpeg': 'image/jpeg',
|
|
300
|
+
'.webp': 'image/webp',
|
|
301
|
+
'.gif': 'image/gif',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
async function uploadImageAsset(gameId, filePath, assetType) {
|
|
305
|
+
const resolved = resolve(filePath);
|
|
306
|
+
if (!existsSync(resolved)) {
|
|
307
|
+
console.log(chalk.red(` ✗ File not found: ${resolved}`));
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const ext = extname(resolved).toLowerCase();
|
|
312
|
+
if (!IMAGE_EXTS.includes(ext)) {
|
|
313
|
+
console.log(chalk.red(` ✗ Unsupported format "${ext}". Use: ${IMAGE_EXTS.join(', ')}`));
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const contentType = IMAGE_CONTENT_TYPES[ext];
|
|
318
|
+
const fileSize = statSync(resolved).size;
|
|
319
|
+
const fileName = `${assetType.toLowerCase()}${ext}`;
|
|
320
|
+
const label = assetType.toLowerCase();
|
|
321
|
+
|
|
322
|
+
const spinner = villageSpinner(`Uploading ${label}...`).start();
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const urlResult = await getGameUploadUrl(gameId, {
|
|
326
|
+
fileName,
|
|
327
|
+
assetType,
|
|
328
|
+
contentType,
|
|
329
|
+
fileSize,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const fileBuffer = readFileSync(resolved);
|
|
333
|
+
await axios.put(urlResult.uploadUrl, fileBuffer, {
|
|
334
|
+
headers: { 'Content-Type': contentType },
|
|
335
|
+
maxBodyLength: Infinity,
|
|
336
|
+
maxContentLength: Infinity,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await confirmGameUpload(gameId, {
|
|
340
|
+
s3Key: urlResult.s3Key,
|
|
341
|
+
fileName,
|
|
342
|
+
assetType,
|
|
343
|
+
contentType,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
spinner.succeed(`${label.charAt(0).toUpperCase() + label.slice(1)} uploaded!`);
|
|
347
|
+
return true;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const message = err.response?.data?.error || err.message;
|
|
350
|
+
spinner.fail(`${label} upload failed: ${message}`);
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function promptForImages(gameId) {
|
|
356
|
+
console.log();
|
|
357
|
+
const { addThumbnail } = await inquirer.prompt([{
|
|
358
|
+
type: 'confirm',
|
|
359
|
+
name: 'addThumbnail',
|
|
360
|
+
message: 'Upload a thumbnail image? (shown on game cards)',
|
|
361
|
+
default: false,
|
|
362
|
+
}]);
|
|
363
|
+
|
|
364
|
+
if (addThumbnail) {
|
|
365
|
+
const { thumbnailPath } = await inquirer.prompt([{
|
|
366
|
+
type: 'input',
|
|
367
|
+
name: 'thumbnailPath',
|
|
368
|
+
message: 'Path to thumbnail image:',
|
|
369
|
+
validate: (input) => {
|
|
370
|
+
if (!input.trim()) return 'Path is required.';
|
|
371
|
+
if (!existsSync(resolve(input.trim()))) return 'File not found.';
|
|
372
|
+
return true;
|
|
373
|
+
},
|
|
374
|
+
}]);
|
|
375
|
+
await uploadImageAsset(gameId, thumbnailPath.trim(), 'THUMBNAIL');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const { addBanner } = await inquirer.prompt([{
|
|
379
|
+
type: 'confirm',
|
|
380
|
+
name: 'addBanner',
|
|
381
|
+
message: 'Upload a banner image? (shown on featured displays)',
|
|
382
|
+
default: false,
|
|
383
|
+
}]);
|
|
384
|
+
|
|
385
|
+
if (addBanner) {
|
|
386
|
+
const { bannerPath } = await inquirer.prompt([{
|
|
387
|
+
type: 'input',
|
|
388
|
+
name: 'bannerPath',
|
|
389
|
+
message: 'Path to banner image:',
|
|
390
|
+
validate: (input) => {
|
|
391
|
+
if (!input.trim()) return 'Path is required.';
|
|
392
|
+
if (!existsSync(resolve(input.trim()))) return 'File not found.';
|
|
393
|
+
return true;
|
|
394
|
+
},
|
|
395
|
+
}]);
|
|
396
|
+
await uploadImageAsset(gameId, bannerPath.trim(), 'BANNER');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
277
400
|
function formatStatus(status) {
|
|
278
401
|
switch (status) {
|
|
279
402
|
case 'SUBMITTED':
|
package/src/commands/game.js
CHANGED
|
@@ -511,6 +511,87 @@ export async function gameUploadThumbnailCommand(filePath) {
|
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
513
|
|
|
514
|
+
// ── game upload-banner ──────────────────────────────
|
|
515
|
+
|
|
516
|
+
export async function gameUploadBannerCommand(filePath) {
|
|
517
|
+
if (!isAuthenticated()) {
|
|
518
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const gameId = getGameId();
|
|
523
|
+
if (!gameId) return;
|
|
524
|
+
|
|
525
|
+
const resolvedPath = resolve(filePath);
|
|
526
|
+
|
|
527
|
+
if (!existsSync(resolvedPath)) {
|
|
528
|
+
console.log(chalk.red(` ✗ File not found: ${resolvedPath}`));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const ext = extname(resolvedPath).toLowerCase();
|
|
533
|
+
const allowedExts = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
534
|
+
if (!allowedExts.includes(ext)) {
|
|
535
|
+
console.log(chalk.red(` ✗ Unsupported image format "${ext}". Use: ${allowedExts.join(', ')}`));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const contentTypes = {
|
|
540
|
+
'.png': 'image/png',
|
|
541
|
+
'.jpg': 'image/jpeg',
|
|
542
|
+
'.jpeg': 'image/jpeg',
|
|
543
|
+
'.webp': 'image/webp',
|
|
544
|
+
'.gif': 'image/gif',
|
|
545
|
+
};
|
|
546
|
+
const contentType = contentTypes[ext];
|
|
547
|
+
const fileSize = statSync(resolvedPath).size;
|
|
548
|
+
const fileName = `banner${ext}`;
|
|
549
|
+
|
|
550
|
+
const spinner = villageSpinner(`Uploading banner (${formatBytes(fileSize)})...`).start();
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
// 1. Get presigned URL
|
|
554
|
+
const urlResult = await getGameUploadUrl(gameId, {
|
|
555
|
+
fileName,
|
|
556
|
+
assetType: 'BANNER',
|
|
557
|
+
contentType,
|
|
558
|
+
fileSize,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// 2. Upload to S3
|
|
562
|
+
spinner.text = chalk.yellow('Uploading to storage...');
|
|
563
|
+
const fileBuffer = readFileSync(resolvedPath);
|
|
564
|
+
|
|
565
|
+
await axios.put(urlResult.uploadUrl, fileBuffer, {
|
|
566
|
+
headers: { 'Content-Type': contentType },
|
|
567
|
+
maxBodyLength: Infinity,
|
|
568
|
+
maxContentLength: Infinity,
|
|
569
|
+
onUploadProgress: (progressEvent) => {
|
|
570
|
+
if (progressEvent.total) {
|
|
571
|
+
const pct = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
|
572
|
+
spinner.text = chalk.yellow(`Uploading banner — ${pct}%`);
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// 3. Confirm
|
|
578
|
+
spinner.text = chalk.yellow('Confirming upload...');
|
|
579
|
+
await confirmGameUpload(gameId, {
|
|
580
|
+
s3Key: urlResult.s3Key,
|
|
581
|
+
fileName,
|
|
582
|
+
assetType: 'BANNER',
|
|
583
|
+
contentType,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
spinner.succeed('Banner uploaded!');
|
|
587
|
+
console.log(brand.teal(` S3 key: ${urlResult.s3Key}`));
|
|
588
|
+
console.log();
|
|
589
|
+
} catch (err) {
|
|
590
|
+
const message = err.response?.data?.error || err.message;
|
|
591
|
+
spinner.fail(`Banner upload failed: ${message}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
514
595
|
// ── game submit ─────────────────────────────────────
|
|
515
596
|
|
|
516
597
|
export async function gameSubmitCommand() {
|
package/src/index.js
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
import {
|
|
57
57
|
gameUpdateCommand,
|
|
58
58
|
gameUploadThumbnailCommand,
|
|
59
|
+
gameUploadBannerCommand,
|
|
59
60
|
gameSubmitCommand,
|
|
60
61
|
gameMissionsInitCommand,
|
|
61
62
|
gameMissionsSyncCommand,
|
|
@@ -144,6 +145,11 @@ export function run() {
|
|
|
144
145
|
.description('Upload or replace game thumbnail image')
|
|
145
146
|
.action(gameUploadThumbnailCommand);
|
|
146
147
|
|
|
148
|
+
gameCmd
|
|
149
|
+
.command('upload-banner <file>')
|
|
150
|
+
.description('Upload or replace game banner image')
|
|
151
|
+
.action(gameUploadBannerCommand);
|
|
152
|
+
|
|
147
153
|
gameCmd
|
|
148
154
|
.command('submit')
|
|
149
155
|
.description('Submit a DRAFT game for admin review')
|
package/src/utils/templates.js
CHANGED
|
@@ -36,7 +36,7 @@ export function createGameProject(targetDir, options) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Write common files
|
|
39
|
-
writeFileSync(join(targetDir, 'package.json'), generatePackageJson(name, description, slug, type));
|
|
39
|
+
writeFileSync(join(targetDir, 'package.json'), generatePackageJson(name, description, slug, type, ageGroup));
|
|
40
40
|
writeFileSync(join(targetDir, 'vite.config.js'), generateViteConfig());
|
|
41
41
|
writeFileSync(join(targetDir, '.gitignore'), generateGitignore());
|
|
42
42
|
writeFileSync(join(targetDir, 'README.md'), generateReadme(name, description, type, ageGroup));
|
|
@@ -670,7 +670,7 @@ Published games appear automatically in the Izi Graham mobile app or M-UNI web p
|
|
|
670
670
|
|
|
671
671
|
// ── Three.js Templates ─────────────────────────────────────────────
|
|
672
672
|
|
|
673
|
-
function generatePackageJson(name, description, slug, type) {
|
|
673
|
+
function generatePackageJson(name, description, slug, type, ageGroup) {
|
|
674
674
|
return JSON.stringify({
|
|
675
675
|
name: slug,
|
|
676
676
|
version: '1.0.0',
|
|
@@ -689,8 +689,10 @@ function generatePackageJson(name, description, slug, type) {
|
|
|
689
689
|
vite: '^5.0.0',
|
|
690
690
|
},
|
|
691
691
|
myvillage: {
|
|
692
|
+
name: name,
|
|
692
693
|
gameId: null,
|
|
693
694
|
gameType: type,
|
|
695
|
+
targetAge: ageGroup,
|
|
694
696
|
lastDeployed: null,
|
|
695
697
|
},
|
|
696
698
|
}, null, 2) + '\n';
|