@myvillage/cli 1.64.3 → 1.64.6
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 +101 -28
- package/src/commands/game.js +9 -7
- package/src/index.js +1 -1
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -7,7 +7,7 @@ import axios from 'axios';
|
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
8
|
import { villageSpinner, brand } from '../utils/brand.js';
|
|
9
9
|
import { isAuthenticated } from '../utils/auth.js';
|
|
10
|
-
import { deployGame, getGameUploadUrl, confirmGameUpload, submitGameForReview } from '../utils/api.js';
|
|
10
|
+
import { deployGame, getGameUploadUrl, confirmGameUpload, submitGameForReview, getGameDetail, updateGameMetadata } from '../utils/api.js';
|
|
11
11
|
import { preflightBundles } from '../utils/preflight.js';
|
|
12
12
|
import { selectGameVillages } from '../utils/village-resolver.js';
|
|
13
13
|
|
|
@@ -77,6 +77,52 @@ async function gatherVillages(existingIds = []) {
|
|
|
77
77
|
return villageIds;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// If the target game is currently PUBLISHED, a redeploy takes it offline and
|
|
81
|
+
// sends the new build back through admin review. Confirm with the developer and
|
|
82
|
+
// move it to DRAFT first. Returns true to proceed with the deploy, false to abort.
|
|
83
|
+
async function confirmRedeployIfPublished(gameId) {
|
|
84
|
+
if (!gameId) return true; // brand-new game — nothing live to disturb
|
|
85
|
+
|
|
86
|
+
let game;
|
|
87
|
+
try {
|
|
88
|
+
const result = await getGameDetail(gameId);
|
|
89
|
+
game = result.game;
|
|
90
|
+
} catch {
|
|
91
|
+
return true; // can't determine status — let the deploy proceed and surface real errors
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!game || game.status !== 'PUBLISHED') return true;
|
|
95
|
+
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(chalk.yellow(` ! "${game.title}" is currently PUBLISHED (live).`));
|
|
98
|
+
console.log(brand.teal(' Redeploying takes it offline and sends the new build back through'));
|
|
99
|
+
console.log(brand.teal(' admin review before it can go live again.'));
|
|
100
|
+
console.log();
|
|
101
|
+
|
|
102
|
+
const { confirm } = await inquirer.prompt([{
|
|
103
|
+
type: 'confirm',
|
|
104
|
+
name: 'confirm',
|
|
105
|
+
message: 'Take the live game offline and redeploy?',
|
|
106
|
+
default: false,
|
|
107
|
+
}]);
|
|
108
|
+
|
|
109
|
+
if (!confirm) {
|
|
110
|
+
console.log(brand.teal('\n Deploy cancelled — game is still live.\n'));
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const spinner = villageSpinner('Taking game offline (returning to draft)...').start();
|
|
115
|
+
try {
|
|
116
|
+
await updateGameMetadata(gameId, { status: 'DRAFT' });
|
|
117
|
+
spinner.succeed('Game returned to draft.');
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const message = err.response?.data?.error || err.message;
|
|
121
|
+
spinner.fail(`Could not unpublish: ${message}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
80
126
|
// Recursively collect file paths from a directory
|
|
81
127
|
function collectFilePaths(dir) {
|
|
82
128
|
const paths = [];
|
|
@@ -151,6 +197,9 @@ async function autoSubmitForReview(gameId, deployResult) {
|
|
|
151
197
|
// ── Three.js deploy (existing flow) ──
|
|
152
198
|
|
|
153
199
|
async function deployThreeJsGame(projectDir, pkg) {
|
|
200
|
+
// If this game is already live, confirm + unpublish before we rebuild/upload.
|
|
201
|
+
if (!(await confirmRedeployIfPublished(pkg.myvillage.gameId))) return;
|
|
202
|
+
|
|
154
203
|
// Village visibility is part of the submission — gather it up front (before
|
|
155
204
|
// the build) so it travels in the deploy metadata.
|
|
156
205
|
const villageIds = await gatherVillages(pkg.myvillage.villages || []);
|
|
@@ -196,7 +245,10 @@ async function deployThreeJsGame(projectDir, pkg) {
|
|
|
196
245
|
formData.append('metadata', JSON.stringify(metadata));
|
|
197
246
|
|
|
198
247
|
for (const fullPath of filePaths) {
|
|
199
|
-
|
|
248
|
+
// Force POSIX separators: path.relative() returns backslashes on Windows,
|
|
249
|
+
// which would land in the S3 key (e.g. assets\foo.js) and 404 when the
|
|
250
|
+
// browser requests assets/foo.js — a blank game. (Server normalizes too.)
|
|
251
|
+
const relPath = relative(distDir, fullPath).replace(/\\/g, '/');
|
|
200
252
|
const fileBuffer = readFileSync(fullPath);
|
|
201
253
|
const blob = new Blob([fileBuffer], { type: getContentType(relPath) });
|
|
202
254
|
formData.append('files', blob, relPath);
|
|
@@ -213,10 +265,22 @@ async function deployThreeJsGame(projectDir, pkg) {
|
|
|
213
265
|
|
|
214
266
|
const finalGameId = result.gameId || pkg.myvillage.gameId;
|
|
215
267
|
|
|
216
|
-
spinner.
|
|
268
|
+
spinner.succeed('Upload complete!');
|
|
269
|
+
|
|
270
|
+
saveVillages(projectDir, false, villageIds);
|
|
271
|
+
|
|
272
|
+
// Attach cover art before the final submit step. The deploy already moved the
|
|
273
|
+
// game to SUBMITTED; the platform lets the owner upload assets while a game is
|
|
274
|
+
// DRAFT/REJECTED/SUBMITTED (only APPROVED/PUBLISHED lock it), so this works
|
|
275
|
+
// for non-admin developers too — not just admins.
|
|
276
|
+
if (finalGameId) {
|
|
277
|
+
await submitCoverArt(finalGameId, projectDir, pkg.myvillage);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Safety net: promote to SUBMITTED if the deploy left the game in DRAFT.
|
|
281
|
+
// (A normal deploy already submits, so this is usually a no-op.)
|
|
217
282
|
const finalStatus = await autoSubmitForReview(finalGameId, result);
|
|
218
283
|
|
|
219
|
-
spinner.succeed('Deployment complete!');
|
|
220
284
|
console.log();
|
|
221
285
|
if (finalStatus === 'SUBMITTED') {
|
|
222
286
|
console.log(brand.green(` \u2713 Game "${metadata.title}" submitted for review`));
|
|
@@ -227,13 +291,6 @@ async function deployThreeJsGame(projectDir, pkg) {
|
|
|
227
291
|
if (result.slug) console.log(brand.teal(` Slug: ${result.slug}`));
|
|
228
292
|
if (result.gameId) console.log(brand.teal(` Game ID: ${result.gameId}`));
|
|
229
293
|
|
|
230
|
-
saveVillages(projectDir, false, villageIds);
|
|
231
|
-
|
|
232
|
-
// Cover art is part of the submission: ship declared assets, require a thumbnail.
|
|
233
|
-
if (finalGameId) {
|
|
234
|
-
await submitCoverArt(finalGameId, projectDir, pkg.myvillage);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
294
|
console.log();
|
|
238
295
|
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
239
296
|
console.log();
|
|
@@ -299,6 +356,9 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
299
356
|
|
|
300
357
|
console.log(brand.teal(` Deploying ${platformsToDeploy.length} platform(s): ${platformsToDeploy.join(', ')}`));
|
|
301
358
|
|
|
359
|
+
// If this game is already live, confirm + unpublish before we upload new builds.
|
|
360
|
+
if (!(await confirmRedeployIfPublished(mvConfig.gameId))) return;
|
|
361
|
+
|
|
302
362
|
// Village visibility is part of the submission — gather once for the whole
|
|
303
363
|
// multi-platform deploy and thread it into every platform's metadata.
|
|
304
364
|
const villageIds = await gatherVillages(mvConfig.villages || []);
|
|
@@ -308,23 +368,39 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
308
368
|
// Track final outcome across all platforms.
|
|
309
369
|
let finalGameId = mvConfig.gameId || null;
|
|
310
370
|
let finalSlug = null;
|
|
311
|
-
let finalStatus = null;
|
|
312
371
|
let finalTitle = null;
|
|
372
|
+
let deployResultForSubmit = null;
|
|
313
373
|
|
|
314
374
|
for (const platform of platformsToDeploy) {
|
|
315
375
|
const outcome = await deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverride: finalGameId, villageIds });
|
|
316
376
|
if (!outcome.ok) return; // bail on first platform failure; nothing else makes sense to ship
|
|
317
377
|
finalGameId = outcome.gameId || finalGameId;
|
|
318
378
|
finalSlug = outcome.slug || finalSlug;
|
|
319
|
-
finalStatus = outcome.status || finalStatus;
|
|
320
379
|
finalTitle = outcome.title || finalTitle;
|
|
380
|
+
deployResultForSubmit = outcome.deployResult || deployResultForSubmit;
|
|
321
381
|
// Reload mvConfig in case the first deploy stamped a new gameId into myvillage.json
|
|
322
382
|
if (outcome.gameId && !mvConfig.gameId) {
|
|
323
383
|
mvConfig.gameId = outcome.gameId;
|
|
324
384
|
}
|
|
325
385
|
}
|
|
326
386
|
|
|
327
|
-
|
|
387
|
+
saveVillages(projectDir, true, villageIds);
|
|
388
|
+
|
|
389
|
+
// Attach cover art before the final submit step. The deploy already moved the
|
|
390
|
+
// game to SUBMITTED; the platform lets the owner upload assets while a game is
|
|
391
|
+
// DRAFT/REJECTED/SUBMITTED (only APPROVED/PUBLISHED lock it). Submit is kept
|
|
392
|
+
// out of the per-platform loop so it runs once, after every platform is up.
|
|
393
|
+
if (finalGameId) {
|
|
394
|
+
await submitCoverArt(finalGameId, projectDir, mvConfig);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Safety net: promote to SUBMITTED if a platform deploy left it in DRAFT.
|
|
398
|
+
const finalStatus = await autoSubmitForReview(
|
|
399
|
+
finalGameId,
|
|
400
|
+
deployResultForSubmit || { status: 'DRAFT' },
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// Single combined summary for the whole multi-platform deploy.
|
|
328
404
|
console.log();
|
|
329
405
|
console.log(brand.green(` \u2713 Game "${finalTitle}" deployed across ${platformsToDeploy.length} platform(s)`));
|
|
330
406
|
console.log(` Status: ${formatStatus(finalStatus)}`);
|
|
@@ -332,21 +408,15 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
332
408
|
if (finalSlug) console.log(brand.teal(` Slug: ${finalSlug}`));
|
|
333
409
|
if (finalGameId) console.log(brand.teal(` Game ID: ${finalGameId}`));
|
|
334
410
|
|
|
335
|
-
saveVillages(projectDir, true, villageIds);
|
|
336
|
-
|
|
337
|
-
// Cover art is part of the submission: ship declared assets, require a thumbnail.
|
|
338
|
-
if (finalGameId) {
|
|
339
|
-
await submitCoverArt(finalGameId, projectDir, mvConfig);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
411
|
console.log();
|
|
343
412
|
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
344
413
|
console.log();
|
|
345
414
|
}
|
|
346
415
|
|
|
347
416
|
/**
|
|
348
|
-
* Deploy one specific platform. Returns { ok, gameId, slug,
|
|
349
|
-
* { ok: false } and writes a user-readable error to the console.
|
|
417
|
+
* Deploy one specific platform. Returns { ok, gameId, slug, deployResult, title }
|
|
418
|
+
* or { ok: false } and writes a user-readable error to the console. Submit-for-
|
|
419
|
+
* review is intentionally NOT done here — see deployUnityGame.
|
|
350
420
|
*/
|
|
351
421
|
async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverride, villageIds }) {
|
|
352
422
|
const buildDir = join(projectDir, 'builds', platform);
|
|
@@ -408,7 +478,10 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
|
|
|
408
478
|
formData.append('metadata', JSON.stringify(metadata));
|
|
409
479
|
|
|
410
480
|
for (const fullPath of filePaths) {
|
|
411
|
-
|
|
481
|
+
// Force POSIX separators: path.relative() returns backslashes on Windows,
|
|
482
|
+
// which would land in the S3 key (e.g. assets\foo.js) and 404 when the
|
|
483
|
+
// browser requests assets/foo.js — a blank game. (Server normalizes too.)
|
|
484
|
+
const relPath = relative(buildDir, fullPath).replace(/\\/g, '/');
|
|
412
485
|
const fileBuffer = readFileSync(fullPath);
|
|
413
486
|
const blob = new Blob([fileBuffer], { type: getContentType(relPath) });
|
|
414
487
|
formData.append('files', blob, relPath);
|
|
@@ -425,16 +498,16 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
|
|
|
425
498
|
|
|
426
499
|
const finalGameId = result.gameId || gameIdOverride || mvConfig.gameId;
|
|
427
500
|
|
|
428
|
-
spinner.text = `[${platform}] Submitting for review...`;
|
|
429
|
-
const finalStatus = await autoSubmitForReview(finalGameId, result);
|
|
430
|
-
|
|
431
501
|
spinner.succeed(`[${platform}] deployed (${gameType})`);
|
|
432
502
|
|
|
503
|
+
// Submit-for-review is deferred to deployUnityGame, AFTER cover art is
|
|
504
|
+
// uploaded — non-admin developers can't attach assets once a game is
|
|
505
|
+
// SUBMITTED, and re-submitting per platform would also be redundant.
|
|
433
506
|
return {
|
|
434
507
|
ok: true,
|
|
435
508
|
gameId: finalGameId,
|
|
436
509
|
slug: result.slug,
|
|
437
|
-
|
|
510
|
+
deployResult: result,
|
|
438
511
|
title: metadata.title,
|
|
439
512
|
};
|
|
440
513
|
} catch (err) {
|
package/src/commands/game.js
CHANGED
|
@@ -693,22 +693,24 @@ export async function gameDraftCommand() {
|
|
|
693
693
|
return;
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
-
|
|
697
|
-
console.log(chalk.red(`\n ✗ "${game.title}" is PUBLISHED and cannot be returned to draft.\n`));
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
696
|
+
const isPublished = game.status === 'PUBLISHED';
|
|
700
697
|
|
|
701
698
|
console.log();
|
|
702
699
|
console.log(brand.gold(` Return "${game.title}" to DRAFT?`));
|
|
703
700
|
console.log(brand.teal(` Current status: ${game.status}`));
|
|
704
|
-
|
|
701
|
+
if (isPublished) {
|
|
702
|
+
console.log(chalk.yellow(' ! This game is LIVE. Unpublishing takes it offline immediately, and'));
|
|
703
|
+
console.log(chalk.yellow(' it must be re-reviewed by an admin before it can be published again.'));
|
|
704
|
+
} else {
|
|
705
|
+
console.log(brand.teal(' This will withdraw the game from review so you can make changes.'));
|
|
706
|
+
}
|
|
705
707
|
console.log();
|
|
706
708
|
|
|
707
709
|
const { confirm } = await inquirer.prompt([{
|
|
708
710
|
type: 'confirm',
|
|
709
711
|
name: 'confirm',
|
|
710
|
-
message: 'Return to draft?',
|
|
711
|
-
default:
|
|
712
|
+
message: isPublished ? 'Take the live game offline?' : 'Return to draft?',
|
|
713
|
+
default: !isPublished,
|
|
712
714
|
}]);
|
|
713
715
|
|
|
714
716
|
if (!confirm) {
|
package/src/index.js
CHANGED
|
@@ -225,7 +225,7 @@ export function run() {
|
|
|
225
225
|
|
|
226
226
|
gameCmd
|
|
227
227
|
.command('draft')
|
|
228
|
-
.description('Return a game to DRAFT
|
|
228
|
+
.description('Return a game to DRAFT (withdraw from review, or unpublish a live game)')
|
|
229
229
|
.action(gameDraftCommand);
|
|
230
230
|
|
|
231
231
|
// Game missions subcommands
|