@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "1.64.3",
3
+ "version": "1.64.5",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.text = 'Submitting for review...';
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
- // Single combined summary + one image prompt for the whole multi-platform deploy.
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, status, title } or
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
- status: finalStatus,
504
+ deployResult: result,
438
505
  title: metadata.title,
439
506
  };
440
507
  } catch (err) {
@@ -693,22 +693,24 @@ export async function gameDraftCommand() {
693
693
  return;
694
694
  }
695
695
 
696
- if (game.status === 'PUBLISHED') {
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
- console.log(brand.teal(' This will withdraw the game from review so you can make changes.'));
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: true,
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 status (withdraw from review)')
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