@myvillage/cli 1.64.3 → 1.64.5
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 +93 -26
- 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 || []);
|
|
@@ -213,10 +262,22 @@ async function deployThreeJsGame(projectDir, pkg) {
|
|
|
213
262
|
|
|
214
263
|
const finalGameId = result.gameId || pkg.myvillage.gameId;
|
|
215
264
|
|
|
216
|
-
spinner.
|
|
265
|
+
spinner.succeed('Upload complete!');
|
|
266
|
+
|
|
267
|
+
saveVillages(projectDir, false, villageIds);
|
|
268
|
+
|
|
269
|
+
// Attach cover art before the final submit step. The deploy already moved the
|
|
270
|
+
// game to SUBMITTED; the platform lets the owner upload assets while a game is
|
|
271
|
+
// DRAFT/REJECTED/SUBMITTED (only APPROVED/PUBLISHED lock it), so this works
|
|
272
|
+
// for non-admin developers too — not just admins.
|
|
273
|
+
if (finalGameId) {
|
|
274
|
+
await submitCoverArt(finalGameId, projectDir, pkg.myvillage);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Safety net: promote to SUBMITTED if the deploy left the game in DRAFT.
|
|
278
|
+
// (A normal deploy already submits, so this is usually a no-op.)
|
|
217
279
|
const finalStatus = await autoSubmitForReview(finalGameId, result);
|
|
218
280
|
|
|
219
|
-
spinner.succeed('Deployment complete!');
|
|
220
281
|
console.log();
|
|
221
282
|
if (finalStatus === 'SUBMITTED') {
|
|
222
283
|
console.log(brand.green(` \u2713 Game "${metadata.title}" submitted for review`));
|
|
@@ -227,13 +288,6 @@ async function deployThreeJsGame(projectDir, pkg) {
|
|
|
227
288
|
if (result.slug) console.log(brand.teal(` Slug: ${result.slug}`));
|
|
228
289
|
if (result.gameId) console.log(brand.teal(` Game ID: ${result.gameId}`));
|
|
229
290
|
|
|
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
291
|
console.log();
|
|
238
292
|
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
239
293
|
console.log();
|
|
@@ -299,6 +353,9 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
299
353
|
|
|
300
354
|
console.log(brand.teal(` Deploying ${platformsToDeploy.length} platform(s): ${platformsToDeploy.join(', ')}`));
|
|
301
355
|
|
|
356
|
+
// If this game is already live, confirm + unpublish before we upload new builds.
|
|
357
|
+
if (!(await confirmRedeployIfPublished(mvConfig.gameId))) return;
|
|
358
|
+
|
|
302
359
|
// Village visibility is part of the submission — gather once for the whole
|
|
303
360
|
// multi-platform deploy and thread it into every platform's metadata.
|
|
304
361
|
const villageIds = await gatherVillages(mvConfig.villages || []);
|
|
@@ -308,23 +365,39 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
308
365
|
// Track final outcome across all platforms.
|
|
309
366
|
let finalGameId = mvConfig.gameId || null;
|
|
310
367
|
let finalSlug = null;
|
|
311
|
-
let finalStatus = null;
|
|
312
368
|
let finalTitle = null;
|
|
369
|
+
let deployResultForSubmit = null;
|
|
313
370
|
|
|
314
371
|
for (const platform of platformsToDeploy) {
|
|
315
372
|
const outcome = await deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverride: finalGameId, villageIds });
|
|
316
373
|
if (!outcome.ok) return; // bail on first platform failure; nothing else makes sense to ship
|
|
317
374
|
finalGameId = outcome.gameId || finalGameId;
|
|
318
375
|
finalSlug = outcome.slug || finalSlug;
|
|
319
|
-
finalStatus = outcome.status || finalStatus;
|
|
320
376
|
finalTitle = outcome.title || finalTitle;
|
|
377
|
+
deployResultForSubmit = outcome.deployResult || deployResultForSubmit;
|
|
321
378
|
// Reload mvConfig in case the first deploy stamped a new gameId into myvillage.json
|
|
322
379
|
if (outcome.gameId && !mvConfig.gameId) {
|
|
323
380
|
mvConfig.gameId = outcome.gameId;
|
|
324
381
|
}
|
|
325
382
|
}
|
|
326
383
|
|
|
327
|
-
|
|
384
|
+
saveVillages(projectDir, true, villageIds);
|
|
385
|
+
|
|
386
|
+
// Attach cover art before the final submit step. The deploy already moved the
|
|
387
|
+
// game to SUBMITTED; the platform lets the owner upload assets while a game is
|
|
388
|
+
// DRAFT/REJECTED/SUBMITTED (only APPROVED/PUBLISHED lock it). Submit is kept
|
|
389
|
+
// out of the per-platform loop so it runs once, after every platform is up.
|
|
390
|
+
if (finalGameId) {
|
|
391
|
+
await submitCoverArt(finalGameId, projectDir, mvConfig);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Safety net: promote to SUBMITTED if a platform deploy left it in DRAFT.
|
|
395
|
+
const finalStatus = await autoSubmitForReview(
|
|
396
|
+
finalGameId,
|
|
397
|
+
deployResultForSubmit || { status: 'DRAFT' },
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Single combined summary for the whole multi-platform deploy.
|
|
328
401
|
console.log();
|
|
329
402
|
console.log(brand.green(` \u2713 Game "${finalTitle}" deployed across ${platformsToDeploy.length} platform(s)`));
|
|
330
403
|
console.log(` Status: ${formatStatus(finalStatus)}`);
|
|
@@ -332,21 +405,15 @@ async function deployUnityGame(projectDir, mvConfig, options) {
|
|
|
332
405
|
if (finalSlug) console.log(brand.teal(` Slug: ${finalSlug}`));
|
|
333
406
|
if (finalGameId) console.log(brand.teal(` Game ID: ${finalGameId}`));
|
|
334
407
|
|
|
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
408
|
console.log();
|
|
343
409
|
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
344
410
|
console.log();
|
|
345
411
|
}
|
|
346
412
|
|
|
347
413
|
/**
|
|
348
|
-
* Deploy one specific platform. Returns { ok, gameId, slug,
|
|
349
|
-
* { ok: false } and writes a user-readable error to the console.
|
|
414
|
+
* Deploy one specific platform. Returns { ok, gameId, slug, deployResult, title }
|
|
415
|
+
* or { ok: false } and writes a user-readable error to the console. Submit-for-
|
|
416
|
+
* review is intentionally NOT done here — see deployUnityGame.
|
|
350
417
|
*/
|
|
351
418
|
async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverride, villageIds }) {
|
|
352
419
|
const buildDir = join(projectDir, 'builds', platform);
|
|
@@ -425,16 +492,16 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
|
|
|
425
492
|
|
|
426
493
|
const finalGameId = result.gameId || gameIdOverride || mvConfig.gameId;
|
|
427
494
|
|
|
428
|
-
spinner.text = `[${platform}] Submitting for review...`;
|
|
429
|
-
const finalStatus = await autoSubmitForReview(finalGameId, result);
|
|
430
|
-
|
|
431
495
|
spinner.succeed(`[${platform}] deployed (${gameType})`);
|
|
432
496
|
|
|
497
|
+
// Submit-for-review is deferred to deployUnityGame, AFTER cover art is
|
|
498
|
+
// uploaded — non-admin developers can't attach assets once a game is
|
|
499
|
+
// SUBMITTED, and re-submitting per platform would also be redundant.
|
|
433
500
|
return {
|
|
434
501
|
ok: true,
|
|
435
502
|
gameId: finalGameId,
|
|
436
503
|
slug: result.slug,
|
|
437
|
-
|
|
504
|
+
deployResult: result,
|
|
438
505
|
title: metadata.title,
|
|
439
506
|
};
|
|
440
507
|
} 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
|