@myvillage/cli 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MyVillage Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # MyVillageOS CLI
2
+
3
+ A command-line interface for MyVillage Project student developers to create, manage, and deploy educational games built with Three.js for the M-UNI platform.
4
+
5
+ ## Installation
6
+
7
+ Requires Node.js 18 or higher.
8
+
9
+ ### From npm (once published)
10
+
11
+ ```bash
12
+ npm install -g @myvillage/cli
13
+ ```
14
+
15
+ > **Note:** The package is not yet published to npm. Use the development install below.
16
+
17
+ ### Development install (from source)
18
+
19
+ ```bash
20
+ # Clone the repository
21
+ git clone https://github.com/MyVillage-Project-Technologies/MyVillageOS-CLI.git
22
+ cd MyVillageOS-CLI
23
+
24
+ # Install dependencies
25
+ npm install
26
+
27
+ # Link the CLI globally so "myvillage" command works anywhere
28
+ npm link
29
+ ```
30
+
31
+ After linking, the `myvillage` command is available system-wide. To unlink later, run `npm unlink -g @myvillage/cli`.
32
+
33
+ You can also run commands directly without linking:
34
+
35
+ ```bash
36
+ node bin/myvillage.js --help
37
+ node bin/myvillage.js create-game
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ # 1. Log in with your MyVillageOS account
44
+ myvillage login
45
+
46
+ # 2. Create a new game project
47
+ myvillage create-game
48
+
49
+ # 3. Develop your game
50
+ cd my-game
51
+ npm run dev
52
+
53
+ # 4. Deploy to MyVillageOS
54
+ myvillage deploy
55
+ ```
56
+
57
+ ## Commands
58
+
59
+ ### `myvillage login`
60
+
61
+ Authenticate with your MyVillageOS account via OAuth. Opens your browser for secure login, then stores credentials locally.
62
+
63
+ ```bash
64
+ myvillage login
65
+ ```
66
+
67
+ ### `myvillage logout`
68
+
69
+ Clear stored credentials.
70
+
71
+ ```bash
72
+ myvillage logout
73
+ ```
74
+
75
+ ### `myvillage create-game`
76
+
77
+ Interactive wizard to scaffold a new game project. Choose from four templates:
78
+
79
+ | Template | Description |
80
+ |----------|-------------|
81
+ | **Quiz Game** | Test knowledge with interactive questions, scoring, and celebrations |
82
+ | **Exploration Game** | Navigate with WASD, collect items, minimap, and inventory |
83
+ | **Narrative Game** | Story-driven with dialogue, branching choices, and scene transitions |
84
+ | **Custom** | Base Three.js template to build anything from scratch |
85
+
86
+ All templates include:
87
+ - Responsive Three.js canvas (desktop + mobile)
88
+ - Camera and lighting setup
89
+ - Input handling (keyboard, mouse, touch)
90
+ - MyVillage brand styling
91
+ - UI overlay system
92
+ - Audio manager
93
+ - Vite dev server with hot reload
94
+
95
+ ```bash
96
+ myvillage create-game
97
+ ```
98
+
99
+ ### `myvillage deploy`
100
+
101
+ Build and deploy your game to the MyVillageOS platform. Run this from inside a game project directory.
102
+
103
+ ```bash
104
+ cd my-game
105
+ myvillage deploy
106
+ ```
107
+
108
+ You earn MVT tokens for each successful deployment.
109
+
110
+ ### `myvillage status`
111
+
112
+ Check deployment status and analytics. If run inside a game project directory, shows status for that game. Otherwise, lists all your deployed games.
113
+
114
+ ```bash
115
+ myvillage status
116
+ ```
117
+
118
+ ## Game Project Structure
119
+
120
+ Games created with the CLI follow this structure:
121
+
122
+ ```
123
+ my-game/
124
+ ├── public/
125
+ │ ├── index.html # HTML entry point
126
+ │ └── assets/ # Static assets
127
+ │ ├── models/ # 3D models
128
+ │ ├── textures/ # Textures and images
129
+ │ └── audio/ # Sound effects and music
130
+ ├── src/
131
+ │ ├── main.js # Game entry point
132
+ │ ├── scenes/
133
+ │ │ └── MainScene.js # Three.js scene setup
134
+ │ ├── components/
135
+ │ │ ├── UI.js # UI overlay (HUD, menus)
136
+ │ │ └── GameLogic.js # Game-specific logic
137
+ │ └── utils/
138
+ │ ├── InputManager.js # Keyboard/mouse/touch input
139
+ │ └── AudioManager.js # Sound effects and music
140
+ ├── package.json
141
+ ├── vite.config.js
142
+ └── README.md
143
+ ```
144
+
145
+ ## Development Workflow
146
+
147
+ ```bash
148
+ # Start the dev server with hot reload
149
+ npm run dev
150
+
151
+ # Build for production
152
+ npm run build
153
+
154
+ # Preview the production build
155
+ npm run preview
156
+ ```
157
+
158
+ ## Configuration
159
+
160
+ CLI configuration is stored in `~/.myvillage/config.json`:
161
+
162
+ ```json
163
+ {
164
+ "apiBaseUrl": "https://portal.myvillageproject.ai/api/v1",
165
+ "oauthBaseUrl": "https://portal.myvillageproject.ai/api/oauth",
166
+ "clientId": "myvillage-cli",
167
+ "callbackPort": 3737
168
+ }
169
+ ```
170
+
171
+ Credentials are stored encrypted in `~/.myvillage/credentials.json` with restrictive file permissions.
172
+
173
+ ## Troubleshooting
174
+
175
+ **"Authentication required" error:**
176
+ Run `myvillage login` to authenticate before using other commands.
177
+
178
+ **"Not a valid game project directory" error:**
179
+ Make sure you're inside a game directory created with `myvillage create-game`. The `package.json` must contain a `myvillage` configuration block.
180
+
181
+ **Browser doesn't open during login:**
182
+ Copy the URL printed in the terminal and open it manually in your browser.
183
+
184
+ **Port 3737 in use during login:**
185
+ Close other applications using port 3737, or update `callbackPort` in `~/.myvillage/config.json`.
186
+
187
+ ## Support
188
+
189
+ - **Technical Support**: dev@myvillageproject.ai
190
+ - **General Inquiries**: info@myvillageproject.ai
191
+ - **MyVillageOS Portal**: https://portal.myvillageproject.ai
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/index.js';
4
+
5
+ run();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@myvillage/cli",
3
+ "version": "1.0.0",
4
+ "description": "MyVillageOS CLI for student game developers",
5
+ "type": "module",
6
+ "bin": {
7
+ "myvillage": "./bin/myvillage.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/"
15
+ ],
16
+ "keywords": [
17
+ "myvillage",
18
+ "myvillageos",
19
+ "cli",
20
+ "game",
21
+ "threejs",
22
+ "education",
23
+ "m-uni"
24
+ ],
25
+ "author": "MyVillage Project",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "commander": "^11.1.0",
29
+ "inquirer": "^9.2.12",
30
+ "axios": "^1.6.2",
31
+ "chalk": "^5.3.0",
32
+ "ora": "^8.0.1",
33
+ "open": "^10.0.3",
34
+ "conf": "^12.0.0",
35
+ "update-notifier": "^7.0.0"
36
+ }
37
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import inquirer from 'inquirer';
7
+ import { isAuthenticated } from '../utils/auth.js';
8
+ import { createGameProject } from '../utils/templates.js';
9
+
10
+ export async function createGameCommand() {
11
+ // Check authentication
12
+ if (!isAuthenticated()) {
13
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
14
+ return;
15
+ }
16
+
17
+ // Interactive prompts
18
+ const answers = await inquirer.prompt([
19
+ {
20
+ type: 'list',
21
+ name: 'type',
22
+ message: 'What type of game would you like to create?',
23
+ choices: [
24
+ { name: 'Quiz Game - Test knowledge with interactive questions', value: 'quiz' },
25
+ { name: 'Exploration Game - Navigate and discover', value: 'exploration' },
26
+ { name: 'Narrative Game - Story-driven experience', value: 'narrative' },
27
+ { name: 'Custom - Start with base Three.js template', value: 'custom' },
28
+ ],
29
+ },
30
+ {
31
+ type: 'input',
32
+ name: 'name',
33
+ message: 'Game name:',
34
+ default: 'My Game',
35
+ validate: (input) => {
36
+ if (!input.trim()) return 'Game name is required.';
37
+ return true;
38
+ },
39
+ },
40
+ {
41
+ type: 'input',
42
+ name: 'description',
43
+ message: 'Game description:',
44
+ default: 'An educational game built with Three.js',
45
+ },
46
+ {
47
+ type: 'list',
48
+ name: 'ageGroup',
49
+ message: 'Target age group:',
50
+ choices: [
51
+ { name: '5-8 years', value: '5-8' },
52
+ { name: '9-12 years', value: '9-12' },
53
+ { name: '13-15 years', value: '13-15' },
54
+ { name: '16-18 years', value: '16-18' },
55
+ ],
56
+ },
57
+ ]);
58
+
59
+ // Create project directory
60
+ const slug = answers.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
61
+ const targetDir = resolve(process.cwd(), slug);
62
+
63
+ if (existsSync(targetDir)) {
64
+ console.log(chalk.red(`\n \u2717 Directory "${slug}" already exists. Choose a different name or remove the existing directory.`));
65
+ return;
66
+ }
67
+
68
+ const spinner = ora('Creating game project...').start();
69
+
70
+ try {
71
+ // Generate project from template
72
+ createGameProject(targetDir, {
73
+ name: answers.name,
74
+ description: answers.description,
75
+ type: answers.type,
76
+ ageGroup: answers.ageGroup,
77
+ });
78
+
79
+ spinner.text = 'Installing dependencies...';
80
+
81
+ // Run npm install in the new project directory
82
+ execSync('npm install', {
83
+ cwd: targetDir,
84
+ stdio: 'pipe',
85
+ });
86
+
87
+ spinner.succeed(`Game "${answers.name}" created successfully!`);
88
+
89
+ // Display next steps
90
+ console.log();
91
+ console.log(chalk.green(` \u2713 Game "${answers.name}" created successfully!`));
92
+ console.log();
93
+ console.log(chalk.dim(' Next steps:'));
94
+ console.log();
95
+ console.log(` ${chalk.cyan('cd')} ${slug}`);
96
+ console.log(` ${chalk.cyan('npm run dev')} ${chalk.dim('- Start development server')}`);
97
+ console.log(` ${chalk.cyan('npm run build')} ${chalk.dim('- Build for production')}`);
98
+ console.log(` ${chalk.cyan('myvillage deploy')} ${chalk.dim('- Deploy to MyVillageOS')}`);
99
+ console.log();
100
+ console.log(chalk.dim(` Game type: ${answers.type} | Age group: ${answers.ageGroup}`));
101
+ console.log();
102
+ } catch (err) {
103
+ spinner.fail('Failed to create game project.');
104
+ console.error(chalk.red(` ${err.message}`));
105
+ }
106
+ }
@@ -0,0 +1,120 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, resolve, relative } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { isAuthenticated } from '../utils/auth.js';
7
+ import { deployGame } from '../utils/api.js';
8
+
9
+ function readPackageJson(dir) {
10
+ const pkgPath = join(dir, 'package.json');
11
+ if (!existsSync(pkgPath)) return null;
12
+
13
+ try {
14
+ return JSON.parse(readFileSync(pkgPath, 'utf-8'));
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ // Recursively collect files from a directory, returning { relativePath: base64Content }
21
+ function collectBuildFiles(dir, baseDir) {
22
+ const files = {};
23
+
24
+ function walk(currentDir) {
25
+ const entries = readdirSync(currentDir, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ const fullPath = join(currentDir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ walk(fullPath);
30
+ } else {
31
+ const relPath = relative(baseDir, fullPath);
32
+ const content = readFileSync(fullPath);
33
+ files[relPath] = content.toString('base64');
34
+ }
35
+ }
36
+ }
37
+
38
+ walk(dir);
39
+ return files;
40
+ }
41
+
42
+ export async function deployCommand() {
43
+ // Check authentication
44
+ if (!isAuthenticated()) {
45
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
46
+ return;
47
+ }
48
+
49
+ const projectDir = resolve(process.cwd());
50
+ const pkg = readPackageJson(projectDir);
51
+
52
+ // Verify this is a valid MyVillageOS game project
53
+ if (!pkg || !pkg.myvillage) {
54
+ console.log(chalk.red(' \u2717 Not a valid game project directory.'));
55
+ console.log(chalk.dim(' Make sure you are inside a game directory created with "myvillage create-game".'));
56
+ return;
57
+ }
58
+
59
+ const spinner = ora('Building game...').start();
60
+
61
+ try {
62
+ // Run production build
63
+ execSync('npm run build', {
64
+ cwd: projectDir,
65
+ stdio: 'pipe',
66
+ });
67
+
68
+ // Check that build output exists
69
+ const distDir = join(projectDir, 'dist');
70
+ if (!existsSync(distDir)) {
71
+ spinner.fail('Build completed but no dist/ directory found.');
72
+ return;
73
+ }
74
+
75
+ spinner.text = 'Packaging build artifacts...';
76
+
77
+ // Collect build files
78
+ const files = collectBuildFiles(distDir, distDir);
79
+
80
+ spinner.text = 'Deploying to MyVillageOS...';
81
+
82
+ // Upload to MyVillageOS API
83
+ const result = await deployGame({
84
+ name: pkg.name,
85
+ description: pkg.description || '',
86
+ category: pkg.myvillage.gameType || 'custom',
87
+ targetAge: pkg.myvillage.targetAge || 'all',
88
+ files,
89
+ });
90
+
91
+ spinner.succeed('Deployment complete!');
92
+ console.log();
93
+ console.log(chalk.green(` \u2713 Deployed to ${result.url}`));
94
+
95
+ if (result.mvtAwarded) {
96
+ console.log(chalk.green(` \u2713 You earned ${result.mvtAwarded} MVT tokens!`));
97
+ }
98
+
99
+ if (result.gameId) {
100
+ console.log(chalk.dim(` Game ID: ${result.gameId}`));
101
+ }
102
+
103
+ console.log();
104
+ } catch (err) {
105
+ if (err.response) {
106
+ // API error
107
+ const message = err.response.data?.error_description
108
+ || err.response.data?.error
109
+ || err.response.data?.message
110
+ || 'Unknown server error';
111
+ spinner.fail(`Deployment failed: ${message}`);
112
+ } else if (err.stderr) {
113
+ // Build error
114
+ spinner.fail('Build failed.');
115
+ console.error(chalk.red(` ${err.stderr.toString().trim()}`));
116
+ } else {
117
+ spinner.fail(`Deployment failed: ${err.message}`);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,200 @@
1
+ import { createServer } from 'http';
2
+ import { randomBytes, createHash } from 'crypto';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import open from 'open';
6
+ import axios from 'axios';
7
+ import { getConfig } from '../utils/config.js';
8
+ import { saveCredentials, loadCredentials } from '../utils/auth.js';
9
+ import { getUserInfo } from '../utils/api.js';
10
+
11
+ // Generate PKCE code verifier (random 64-char hex string)
12
+ function generateCodeVerifier() {
13
+ return randomBytes(32).toString('hex');
14
+ }
15
+
16
+ // Generate S256 code challenge from verifier
17
+ function generateCodeChallenge(verifier) {
18
+ return createHash('sha256')
19
+ .update(verifier)
20
+ .digest('base64url');
21
+ }
22
+
23
+ export async function loginCommand() {
24
+ const config = getConfig();
25
+
26
+ // Check if already logged in
27
+ const existing = loadCredentials();
28
+ if (existing?.access_token) {
29
+ console.log(chalk.yellow('You are already logged in.'));
30
+ console.log(chalk.dim('Run "myvillage logout" first to log in with a different account.'));
31
+ return;
32
+ }
33
+
34
+ const spinner = ora('Preparing authentication...').start();
35
+
36
+ // Generate PKCE values and state token
37
+ const state = randomBytes(16).toString('hex');
38
+ const codeVerifier = generateCodeVerifier();
39
+ const codeChallenge = generateCodeChallenge(codeVerifier);
40
+
41
+ // Build authorization URL
42
+ const params = new URLSearchParams({
43
+ client_id: config.clientId,
44
+ redirect_uri: `http://localhost:${config.callbackPort}/callback`,
45
+ response_type: 'code',
46
+ scope: 'openid profile email villager offline_access',
47
+ state,
48
+ code_challenge: codeChallenge,
49
+ code_challenge_method: 'S256',
50
+ });
51
+
52
+ const authUrl = `${config.oauthBaseUrl}/authorize?${params.toString()}`;
53
+
54
+ // Create a promise that resolves when we receive the OAuth callback
55
+ const authResult = await new Promise((resolve, reject) => {
56
+ const server = createServer(async (req, res) => {
57
+ const url = new URL(req.url, `http://localhost:${config.callbackPort}`);
58
+
59
+ if (url.pathname !== '/callback') {
60
+ res.writeHead(404);
61
+ res.end('Not found');
62
+ return;
63
+ }
64
+
65
+ const code = url.searchParams.get('code');
66
+ const returnedState = url.searchParams.get('state');
67
+ const error = url.searchParams.get('error');
68
+ const errorDescription = url.searchParams.get('error_description');
69
+
70
+ // Send response to the browser
71
+ res.writeHead(200, { 'Content-Type': 'text/html' });
72
+ if (error) {
73
+ res.end(`
74
+ <html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
75
+ <div style="text-align: center;">
76
+ <h1 style="color: #FF6B6B;">Authentication Failed</h1>
77
+ <p>${errorDescription || error}</p>
78
+ <p>You can close this window.</p>
79
+ </div>
80
+ </body></html>
81
+ `);
82
+ } else {
83
+ res.end(`
84
+ <html><body style="font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; background: #302017; color: #E4DCCB;">
85
+ <div style="text-align: center;">
86
+ <h1 style="color: #FFD700;">Success!</h1>
87
+ <p>You are now logged in to MyVillageOS CLI.</p>
88
+ <p>You can close this window and return to your terminal.</p>
89
+ </div>
90
+ </body></html>
91
+ `);
92
+ }
93
+
94
+ // Close the server
95
+ server.close();
96
+
97
+ if (error) {
98
+ reject(new Error(errorDescription || error));
99
+ return;
100
+ }
101
+
102
+ // Validate state parameter
103
+ if (returnedState !== state) {
104
+ reject(new Error('State mismatch - possible CSRF attack. Please try again.'));
105
+ return;
106
+ }
107
+
108
+ resolve(code);
109
+ });
110
+
111
+ server.listen(config.callbackPort, () => {
112
+ spinner.text = 'Opening browser for authentication...';
113
+
114
+ // Open browser to authorization URL
115
+ open(authUrl).catch(() => {
116
+ spinner.stop();
117
+ console.log(chalk.yellow('\nCould not open browser automatically.'));
118
+ console.log(chalk.dim('Please open this URL in your browser:'));
119
+ console.log(chalk.cyan(authUrl));
120
+ });
121
+
122
+ spinner.text = 'Waiting for authentication in browser...';
123
+ });
124
+
125
+ server.on('error', (err) => {
126
+ if (err.code === 'EADDRINUSE') {
127
+ reject(new Error(`Port ${config.callbackPort} is already in use. Close other applications and try again.`));
128
+ } else {
129
+ reject(err);
130
+ }
131
+ });
132
+
133
+ // Timeout after 5 minutes
134
+ setTimeout(() => {
135
+ server.close();
136
+ reject(new Error('Authentication timed out. Please try again.'));
137
+ }, 5 * 60 * 1000);
138
+ }).catch((err) => {
139
+ spinner.fail(err.message);
140
+ return null;
141
+ });
142
+
143
+ if (!authResult) return;
144
+
145
+ // Exchange authorization code for tokens
146
+ spinner.text = 'Exchanging authorization code for tokens...';
147
+
148
+ try {
149
+ const tokenResponse = await axios.post(
150
+ `${config.oauthBaseUrl}/token`,
151
+ new URLSearchParams({
152
+ grant_type: 'authorization_code',
153
+ code: authResult,
154
+ redirect_uri: `http://localhost:${config.callbackPort}/callback`,
155
+ client_id: config.clientId,
156
+ code_verifier: codeVerifier,
157
+ }),
158
+ {
159
+ headers: {
160
+ 'Content-Type': 'application/x-www-form-urlencoded',
161
+ 'User-Agent': 'MyVillageOS-CLI/1.0.0',
162
+ },
163
+ }
164
+ );
165
+
166
+ const { access_token, refresh_token, expires_in } = tokenResponse.data;
167
+
168
+ // Fetch user info
169
+ spinner.text = 'Fetching user info...';
170
+ const userInfo = await getUserInfo(access_token);
171
+
172
+ // Store credentials securely
173
+ saveCredentials({
174
+ access_token,
175
+ refresh_token,
176
+ expires_at: Date.now() + expires_in * 1000,
177
+ user_id: userInfo.sub,
178
+ user_email: userInfo.email,
179
+ user_name: userInfo.name,
180
+ });
181
+
182
+ spinner.succeed('Authentication complete!');
183
+ console.log();
184
+ console.log(chalk.green(` \u2713 Successfully logged in as ${userInfo.email}`));
185
+
186
+ if (userInfo.villager) {
187
+ const v = userInfo.villager;
188
+ if (v.mvtBalance !== undefined) {
189
+ console.log(chalk.dim(` MVT Balance: ${v.mvtBalance} tokens`));
190
+ }
191
+ }
192
+
193
+ console.log();
194
+ } catch (err) {
195
+ const message = err.response?.data?.error_description
196
+ || err.response?.data?.error
197
+ || err.message;
198
+ spinner.fail(`Authentication failed: ${message}`);
199
+ }
200
+ }
@@ -0,0 +1,12 @@
1
+ import chalk from 'chalk';
2
+ import { clearCredentials } from '../utils/auth.js';
3
+
4
+ export async function logoutCommand() {
5
+ const removed = clearCredentials();
6
+
7
+ if (removed) {
8
+ console.log(chalk.green(' \u2713 Successfully logged out.'));
9
+ } else {
10
+ console.log(chalk.dim(' No stored credentials found. You are not logged in.'));
11
+ }
12
+ }