@myvillage/cli 2.1.0 → 2.3.0

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": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,76 @@
1
+ import chalk from 'chalk';
2
+ import { isAuthenticated } from '../utils/auth.js';
3
+ import { getMyStoryBooks } from '../utils/api.js';
4
+
5
+ /**
6
+ * books — the author's own shelf, every status.
7
+ *
8
+ * Exists so an author can see where their submissions stand without being sent
9
+ * to the portal. The portal's story-books screen is a reviewer's surface: it
10
+ * carries other people's books, the approve/reject controls, and the raw state
11
+ * names. None of that belongs in front of the person waiting on a decision.
12
+ *
13
+ * The `--json` shape is what Studio's Story Books screen renders.
14
+ */
15
+
16
+ function fail(json, message) {
17
+ if (json) {
18
+ console.log(JSON.stringify({ success: false, error: message }));
19
+ } else {
20
+ console.log(chalk.red(` ✗ ${message}`));
21
+ }
22
+ process.exitCode = 1;
23
+ }
24
+
25
+ /** Reviewer state name → what it means to the person who wrote the book. */
26
+ const STATE_LABEL = {
27
+ DRAFT: 'Draft',
28
+ PROCESSING: 'Processing',
29
+ SUBMITTED: 'Waiting for review',
30
+ UNDER_REVIEW: 'Being reviewed',
31
+ APPROVED: 'Approved',
32
+ REJECTED: 'Needs changes',
33
+ PUBLISHED: 'Live',
34
+ ARCHIVED: 'Removed',
35
+ };
36
+
37
+ export async function booksListCommand(options = {}) {
38
+ const json = !!options.json;
39
+
40
+ if (!isAuthenticated()) {
41
+ return fail(json, "Authentication required. Run 'myvillage login' first.");
42
+ }
43
+
44
+ let books;
45
+ try {
46
+ const result = await getMyStoryBooks();
47
+ books = result.storyBooks || [];
48
+ } catch (err) {
49
+ return fail(json, err.response?.data?.error || err.message);
50
+ }
51
+
52
+ if (json) {
53
+ console.log(JSON.stringify({ success: true, storyBooks: books }));
54
+ return;
55
+ }
56
+
57
+ if (books.length === 0) {
58
+ console.log('');
59
+ console.log(` No books yet. ${chalk.cyan('myvillage publish-book <pdf>')} publishes one.`);
60
+ console.log('');
61
+ return;
62
+ }
63
+
64
+ console.log('');
65
+ for (const b of books) {
66
+ const label = STATE_LABEL[b.status] || b.status;
67
+ console.log(` ${chalk.bold(b.title)} ${chalk.dim(`(${b.slug})`)}`);
68
+ console.log(
69
+ ` ${chalk.dim('Status:')} ${label} ${chalk.dim('Pages:')} ${b.pageCount ?? 0}`,
70
+ );
71
+ if (b.status === 'REJECTED' && b.rejectionReason) {
72
+ console.log(` ${chalk.red(b.rejectionReason)}`);
73
+ }
74
+ console.log('');
75
+ }
76
+ }
@@ -463,6 +463,13 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
463
463
  buildDir,
464
464
  sdkVersion: mvConfig.sdkVersion,
465
465
  platform,
466
+ onStatus: (text) => {
467
+ preflightSpinner.text = `[${platform}] ${text}`;
468
+ // Studio streams our stdout into its publish log and isn't a TTY, so
469
+ // the spinner's text changes never reach the developer there. Print a
470
+ // plain line too, otherwise the one-time UnityPy install looks like a hang.
471
+ if (!process.stderr.isTTY) console.log(brand.teal(` [${platform}] ${text}`));
472
+ },
466
473
  });
467
474
  if (preflightResult.skipped) {
468
475
  if (preflightResult.fatal) {
@@ -471,7 +478,18 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
471
478
  }
472
479
  preflightSpinner.warn(`[${platform}] Preflight skipped: ${preflightResult.reason}`);
473
480
  } else if (!preflightResult.ok) {
474
- preflightSpinner.fail(`[${platform}] Preflight found ${preflightResult.violations.length} violation(s).`);
481
+ // "preflight_error" / "preflight_crash" mean the scanner itself failed
482
+ // (couldn't parse the bundle, script crashed). That's on us, not on the
483
+ // developer's scripts — so don't tell them to rewrite their game.
484
+ const toolingFailures = preflightResult.violations.filter(
485
+ (v) => v.kind === 'preflight_error' || v.kind === 'preflight_crash',
486
+ );
487
+ const codeViolations = preflightResult.violations.filter((v) => !toolingFailures.includes(v));
488
+ if (codeViolations.length > 0) {
489
+ preflightSpinner.fail(`[${platform}] Preflight found ${codeViolations.length} violation(s).`);
490
+ } else {
491
+ preflightSpinner.fail(`[${platform}] Preflight could not scan the bundle.`);
492
+ }
475
493
  console.log();
476
494
  for (const v of preflightResult.violations) {
477
495
  console.log(chalk.red(` \u2717 ${v.kind}`));
@@ -481,7 +499,12 @@ async function deployUnityPlatform(projectDir, mvConfig, platform, { gameIdOverr
481
499
  if (v.reason) console.log(` ${v.reason}`);
482
500
  console.log();
483
501
  }
484
- console.log(brand.teal(' Fix these scripts to extend MissionBase / use SDK templates, rebuild, and retry.'));
502
+ if (codeViolations.length > 0) {
503
+ console.log(brand.teal(' Fix these scripts to extend MissionBase / use SDK templates, rebuild, and retry.'));
504
+ } else {
505
+ console.log(brand.teal(' This is a problem with the bundle scanner, not with your game.'));
506
+ console.log(brand.teal(' Rebuild the bundle in Unity and publish again. If it keeps happening, send the message above to the MyVillage team.'));
507
+ }
485
508
  console.log();
486
509
  return { ok: false };
487
510
  } else {
package/src/index.js CHANGED
@@ -19,6 +19,7 @@ import { createGameCommand } from './commands/create-game.js';
19
19
  import { createCommand } from './commands/create-app.js';
20
20
  import { deployCommand } from './commands/deploy.js';
21
21
  import { publishBookCommand } from './commands/publish-book.js';
22
+ import { booksListCommand } from './commands/books.js';
22
23
  import { publishEpisodeCommand } from './commands/publish-episode.js';
23
24
  import {
24
25
  podcastCreateCommand,
@@ -251,6 +252,12 @@ export function run() {
251
252
  .option('--json', 'Output the result as JSON (implies non-interactive)')
252
253
  .action((pdf, options) => publishBookCommand(pdf, options));
253
254
 
255
+ program
256
+ .command('books')
257
+ .description('List your story books and where each one stands')
258
+ .option('--json', 'Output the result as JSON')
259
+ .action((options) => booksListCommand(options));
260
+
254
261
  program
255
262
  .command('publish-episode <audio>')
256
263
  .description('Publish an audio file as a podcast episode')
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
- import { existsSync, mkdtempSync, readdirSync, statSync, writeFileSync } from 'fs';
3
- import { tmpdir } from 'os';
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, statSync, writeFileSync } from 'fs';
3
+ import { homedir, tmpdir } from 'os';
4
4
  import { dirname, join, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { getGameKitAllowlist } from './api.js';
@@ -14,6 +14,91 @@ const PREFLIGHT_SCRIPT = resolve(__dirname, '../../tools/preflight/preflight.py'
14
14
 
15
15
  const PYTHON_CANDIDATES = ['python3', 'python'];
16
16
 
17
+ // The bundle scanner needs UnityPy. Rather than asking every game developer to
18
+ // pip-install it by hand (the #1 reason deploys failed with "preflight_error"),
19
+ // we install it once into a private venv under ~/.myvillage and reuse it.
20
+ const UNITYPY_SPEC = 'UnityPy==1.25.0';
21
+ const PREFLIGHT_VENV_DIR = join(homedir(), '.myvillage', 'tools', 'preflight-venv');
22
+
23
+ function venvPython(dir) {
24
+ return process.platform === 'win32' ? join(dir, 'Scripts', 'python.exe') : join(dir, 'bin', 'python');
25
+ }
26
+
27
+ /** Run a command, capture output, never throw. */
28
+ function runCapture(cmd, args) {
29
+ return new Promise((resolveRun) => {
30
+ let stdout = '';
31
+ let stderr = '';
32
+ let proc;
33
+ try {
34
+ proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
35
+ } catch (err) {
36
+ resolveRun({ code: -1, stdout, stderr: err.message });
37
+ return;
38
+ }
39
+ proc.stdout.on('data', (b) => { stdout += b.toString(); });
40
+ proc.stderr.on('data', (b) => { stderr += b.toString(); });
41
+ proc.on('error', (err) => resolveRun({ code: -1, stdout, stderr: err.message }));
42
+ proc.on('exit', (code) => resolveRun({ code, stdout, stderr }));
43
+ });
44
+ }
45
+
46
+ async function hasUnityPy(python) {
47
+ const r = await runCapture(python, ['-c', 'import UnityPy']);
48
+ return r.code === 0;
49
+ }
50
+
51
+ function lastLine(text) {
52
+ const lines = (text || '').trim().split(/\r?\n/).filter(Boolean);
53
+ return lines.length ? lines[lines.length - 1] : '';
54
+ }
55
+
56
+ /**
57
+ * Find a Python that can import UnityPy, installing it if needed.
58
+ *
59
+ * Order: our managed venv → a system Python that already has UnityPy →
60
+ * create the managed venv with the system Python and pip-install UnityPy.
61
+ * `onStatus(text)` is called with progress copy for the caller's spinner.
62
+ *
63
+ * Returns { python } on success, or { python: null, reason } when there's no
64
+ * usable interpreter — the caller decides whether that skips or blocks.
65
+ */
66
+ export async function ensurePreflightPython({ onStatus } = {}) {
67
+ const managed = venvPython(PREFLIGHT_VENV_DIR);
68
+ if (existsSync(managed) && await hasUnityPy(managed)) return { python: managed };
69
+
70
+ const system = await findPython();
71
+ if (!system) {
72
+ return {
73
+ python: null,
74
+ reason: 'Python 3 was not found on your PATH. Install Python 3.9+ from python.org (on Windows, tick "Add to PATH"), then publish again.',
75
+ };
76
+ }
77
+ if (await hasUnityPy(system)) return { python: system };
78
+
79
+ const manualHint = `${system} -m pip install "${UNITYPY_SPEC}"`;
80
+
81
+ onStatus?.('First publish from this computer: setting up the bundle scanner (one-time, about a minute)...');
82
+ mkdirSync(PREFLIGHT_VENV_DIR, { recursive: true });
83
+ const venv = await runCapture(system, ['-m', 'venv', PREFLIGHT_VENV_DIR]);
84
+ if (venv.code !== 0 || !existsSync(managed)) {
85
+ return {
86
+ python: null,
87
+ reason: `Could not create a Python environment at ${PREFLIGHT_VENV_DIR} (${lastLine(venv.stderr) || `exit ${venv.code}`}). Repair your Python install or run: ${manualHint}`,
88
+ };
89
+ }
90
+
91
+ onStatus?.('Installing the bundle scanner (UnityPy)...');
92
+ const pip = await runCapture(managed, ['-m', 'pip', 'install', '--disable-pip-version-check', '--quiet', UNITYPY_SPEC]);
93
+ if (pip.code !== 0 || !(await hasUnityPy(managed))) {
94
+ return {
95
+ python: null,
96
+ reason: `Could not install ${UNITYPY_SPEC} (${lastLine(pip.stderr) || `exit ${pip.code}`}). Check your network connection and retry, or run: ${manualHint}`,
97
+ };
98
+ }
99
+ return { python: managed };
100
+ }
101
+
17
102
  /**
18
103
  * Find the first usable Python interpreter on PATH.
19
104
  * Returns the command name (e.g. "python3") or null if none works.
@@ -94,11 +179,11 @@ async function runPreflight(python, bundlePath, allowlistPath) {
94
179
  * Run preflight against all AssetBundles in a build directory.
95
180
  *
96
181
  * Returns:
97
- * { skipped: true, reason } — bundles couldn't be checked (no SDK, no Python, etc.)
182
+ * { skipped: true, reason } — bundles couldn't be checked (no SDK, no usable Python, etc.)
98
183
  * { ok: true, bundlesChecked } — all bundles passed
99
184
  * { ok: false, violations, bundle } — at least one violation found
100
185
  */
101
- export async function preflightBundles({ buildDir, sdkVersion, platform }) {
186
+ export async function preflightBundles({ buildDir, sdkVersion, platform, onStatus }) {
102
187
  if (!sdkVersion) {
103
188
  return { skipped: true, reason: 'No sdkVersion in myvillage.json — preflight only runs for SDK-based games.' };
104
189
  }
@@ -109,12 +194,13 @@ export async function preflightBundles({ buildDir, sdkVersion, platform }) {
109
194
  return { skipped: true, reason: `preflight.py not found at ${PREFLIGHT_SCRIPT} — CLI install may be incomplete.` };
110
195
  }
111
196
 
112
- const python = await findPython();
197
+ // No usable Python (or UnityPy couldn't be installed) is a *tooling* gap on
198
+ // the developer's machine, never a problem with their game. Skip with a clear
199
+ // warning instead of blocking — the same outcome for "no Python at all" and
200
+ // "Python but no UnityPy", which used to diverge (one skipped, one failed).
201
+ const { python, reason: pythonReason } = await ensurePreflightPython({ onStatus });
113
202
  if (!python) {
114
- return {
115
- skipped: true,
116
- reason: 'Python 3 not found on PATH. Install Python 3.9+ and then run: pip install UnityPy==1.25.0',
117
- };
203
+ return { skipped: true, reason: `${pythonReason} (Bundle scan skipped; the platform re-checks on upload.)` };
118
204
  }
119
205
 
120
206
  let allowlistResponse;
@@ -12,7 +12,7 @@ const GAME_ART_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets
12
12
 
13
13
  // GameKit SDK version that scaffolded Unity projects pin to.
14
14
  // Bump alongside CLI minor releases as the SDK contract evolves.
15
- const GAMEKIT_SDK_VERSION = '1.1.0-alpha.4';
15
+ const GAMEKIT_SDK_VERSION = '1.1.0-alpha.5';
16
16
  const GAMEKIT_SDK_URL =
17
17
  `https://github.com/MyVillage-Project-Technologies/myvillage-gamekit.git#v${GAMEKIT_SDK_VERSION}`;
18
18
 
@@ -1302,7 +1302,7 @@ export async function createUnityGameProject(targetDir, options) {
1302
1302
  // Unity project files. When the SDK is embedded it must NOT also be listed as
1303
1303
  // a manifest dependency (Unity would try to re-resolve it); when it isn't, the
1304
1304
  // git-URL entry is the fallback resolution path.
1305
- writeFileSync(join(targetDir, 'Packages/manifest.json'), generateUnityPackagesManifest(sdkEmbedded));
1305
+ writeFileSync(join(targetDir, 'Packages/manifest.json'), generateUnityPackagesManifest(sdkEmbedded, unityVersion));
1306
1306
  writeFileSync(join(targetDir, 'ProjectSettings/ProjectVersion.txt'), generateUnityProjectVersion(unityVersion));
1307
1307
 
1308
1308
  // Starter mission. New devs subclass MissionBase here.
@@ -1367,7 +1367,59 @@ public static class MyVillageOrientation
1367
1367
 
1368
1368
  // ── Unity (GameKit SDK) ─────────────────────────────────────────────
1369
1369
 
1370
- function generateUnityPackagesManifest(sdkEmbedded = true) {
1370
+ // Unity's built-in engine modules. A project created through Unity Hub lists
1371
+ // every one of these in Packages/manifest.json; a manifest that omits them
1372
+ // DISABLES them. An earlier version of this scaffold wrote only the registry
1373
+ // packages, so fresh projects had no Particle System, Terrain, Animation,
1374
+ // Tilemap, Physics2D, Video, ... and developers had to hand-restore them.
1375
+ const UNITY_BUILTIN_MODULES = [
1376
+ 'accessibility',
1377
+ 'ai',
1378
+ 'androidjni',
1379
+ 'animation',
1380
+ 'assetbundle',
1381
+ 'audio',
1382
+ 'cloth',
1383
+ 'director',
1384
+ 'imageconversion',
1385
+ 'imgui',
1386
+ 'jsonserialize',
1387
+ 'particlesystem',
1388
+ 'physics',
1389
+ 'physics2d',
1390
+ 'screencapture',
1391
+ 'terrain',
1392
+ 'terrainphysics',
1393
+ 'tilemap',
1394
+ 'ui',
1395
+ 'uielements',
1396
+ 'umbra',
1397
+ 'unityanalytics',
1398
+ 'unitywebrequest',
1399
+ 'unitywebrequestassetbundle',
1400
+ 'unitywebrequestaudio',
1401
+ 'unitywebrequesttexture',
1402
+ 'unitywebrequestwww',
1403
+ 'vehicles',
1404
+ 'video',
1405
+ 'vr',
1406
+ 'wind',
1407
+ 'xr',
1408
+ ];
1409
+
1410
+ // Visual Scripting version pinned in the scaffold manifest. Keep in sync with
1411
+ // the `com.unity.visualscripting` dependency in the GameKit's package.json —
1412
+ // GameKit's MyVillage.GameKit.VisualScripting assembly references it, and an
1413
+ // embedded package's own dependency isn't enough on its own for Unity to keep
1414
+ // it resolved, so the manifest lists it explicitly.
1415
+ const UNITY_VISUAL_SCRIPTING_VERSION = '1.9.4';
1416
+
1417
+ function unityMajor(unityVersion) {
1418
+ const m = /^(\d+)\./.exec(unityVersion || '');
1419
+ return m ? Number(m[1]) : 6000;
1420
+ }
1421
+
1422
+ function generateUnityPackagesManifest(sdkEmbedded = true, unityVersion = UNITY_EDITOR_VERSION_FALLBACK) {
1371
1423
  const dependencies = {};
1372
1424
  // Only declare the SDK as a git dependency when it could NOT be embedded
1373
1425
  // locally. When embedded under Packages/com.myvillage.gamekit/, Unity
@@ -1378,11 +1430,25 @@ function generateUnityPackagesManifest(sdkEmbedded = true) {
1378
1430
  Object.assign(dependencies, {
1379
1431
  'com.unity.feature.development': '1.0.2',
1380
1432
  'com.unity.render-pipelines.universal': '17.2.0',
1381
- 'com.unity.textmeshpro': '3.0.6',
1382
1433
  'com.unity.inputsystem': '1.14.2',
1434
+ // GameKit ships Visual Scripting units (CompleteMission, ReadConfig, ...).
1435
+ // Without this package the SDK's VisualScripting assembly can't compile.
1436
+ 'com.unity.visualscripting': UNITY_VISUAL_SCRIPTING_VERSION,
1383
1437
  'com.unity.ide.rider': '3.0.38',
1384
1438
  'com.unity.ide.visualstudio': '2.0.22',
1385
1439
  });
1440
+ // Unity 6 folded TextMesh Pro into com.unity.ugui 2.x; declaring the old
1441
+ // standalone com.unity.textmeshpro there triggers a deprecation conflict.
1442
+ // Older editors still need the standalone package.
1443
+ if (unityMajor(unityVersion) >= 6000) {
1444
+ dependencies['com.unity.ugui'] = '2.0.0';
1445
+ } else {
1446
+ dependencies['com.unity.ugui'] = '1.0.0';
1447
+ dependencies['com.unity.textmeshpro'] = '3.0.6';
1448
+ }
1449
+ for (const mod of UNITY_BUILTIN_MODULES) {
1450
+ dependencies[`com.unity.modules.${mod}`] = '1.0.0';
1451
+ }
1386
1452
  return JSON.stringify({ dependencies }, null, 2) + '\n';
1387
1453
  }
1388
1454
 
@@ -16,7 +16,7 @@ Requires **Python 3.9+** and **UnityPy 1.25.0** (specifically — newer versions
16
16
  pip install -r requirements.txt
17
17
  ```
18
18
 
19
- The CLI checks for `python3` and `UnityPy` automatically and prints install hints if either is missing.
19
+ The CLI finds `python3`/`python` on PATH and, if UnityPy isn't importable, installs it once into a private venv at `~/.myvillage/tools/preflight-venv/` (see `ensurePreflightPython` in `src/utils/preflight.js`). If Python is missing or the install fails, preflight is skipped with a warning and the upload proceeds; the ed-platform re-checks on upload.
20
20
 
21
21
  ## Run directly (for debugging)
22
22