@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.
@@ -0,0 +1,126 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { isAuthenticated } from '../utils/auth.js';
6
+ import { getMyGames, getGameStats } from '../utils/api.js';
7
+
8
+ function readPackageJson(dir) {
9
+ const pkgPath = join(dir, 'package.json');
10
+ if (!existsSync(pkgPath)) return null;
11
+
12
+ try {
13
+ return JSON.parse(readFileSync(pkgPath, 'utf-8'));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function formatDate(dateStr) {
20
+ if (!dateStr) return 'N/A';
21
+ return new Date(dateStr).toLocaleDateString('en-US', {
22
+ year: 'numeric',
23
+ month: 'short',
24
+ day: 'numeric',
25
+ hour: '2-digit',
26
+ minute: '2-digit',
27
+ });
28
+ }
29
+
30
+ export async function statusCommand() {
31
+ // Check authentication
32
+ if (!isAuthenticated()) {
33
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
34
+ return;
35
+ }
36
+
37
+ const projectDir = resolve(process.cwd());
38
+ const pkg = readPackageJson(projectDir);
39
+
40
+ // If in a game project directory with a gameId, show that game's status
41
+ if (pkg?.myvillage?.gameId) {
42
+ await showGameStatus(pkg.myvillage.gameId);
43
+ return;
44
+ }
45
+
46
+ // Otherwise, list all deployed games
47
+ await showAllGames();
48
+ }
49
+
50
+ async function showGameStatus(gameId) {
51
+ const spinner = ora('Fetching game status...').start();
52
+
53
+ try {
54
+ const stats = await getGameStats(gameId);
55
+
56
+ spinner.stop();
57
+
58
+ console.log();
59
+ console.log(chalk.bold(` ${stats.name || gameId}`));
60
+ console.log();
61
+ console.log(` Status: ${formatStatus(stats.status)}`);
62
+ console.log(` Play Count: ${chalk.cyan(stats.playCount ?? 0)}`);
63
+ console.log(` MVT Earned: ${chalk.yellow(stats.mvtEarned ?? 0)} tokens`);
64
+ console.log(` Last Updated: ${chalk.dim(formatDate(stats.lastUpdated))}`);
65
+
66
+ if (stats.url) {
67
+ console.log(` URL: ${chalk.cyan(stats.url)}`);
68
+ }
69
+ console.log();
70
+ } catch (err) {
71
+ const message = err.response?.data?.message || err.message;
72
+ spinner.fail(`Failed to fetch game status: ${message}`);
73
+ }
74
+ }
75
+
76
+ async function showAllGames() {
77
+ const spinner = ora('Fetching your games...').start();
78
+
79
+ try {
80
+ const data = await getMyGames();
81
+ const games = data.games || data || [];
82
+
83
+ spinner.stop();
84
+
85
+ if (!games.length) {
86
+ console.log();
87
+ console.log(chalk.dim(' No deployed games found.'));
88
+ console.log(chalk.dim(' Create a game with "myvillage create-game" and deploy it!'));
89
+ console.log();
90
+ return;
91
+ }
92
+
93
+ console.log();
94
+ console.log(chalk.bold(` Your Games (${games.length})`));
95
+ console.log();
96
+
97
+ for (const game of games) {
98
+ console.log(` ${chalk.yellow(game.name || game.gameId)}`);
99
+ console.log(` Status: ${formatStatus(game.status)} | Plays: ${chalk.cyan(game.playCount ?? 0)} | MVT: ${chalk.yellow(game.mvtEarned ?? 0)}`);
100
+
101
+ if (game.url) {
102
+ console.log(` ${chalk.dim(game.url)}`);
103
+ }
104
+
105
+ console.log();
106
+ }
107
+ } catch (err) {
108
+ const message = err.response?.data?.message || err.message;
109
+ spinner.fail(`Failed to fetch games: ${message}`);
110
+ }
111
+ }
112
+
113
+ function formatStatus(status) {
114
+ switch (status) {
115
+ case 'live':
116
+ return chalk.green('Live');
117
+ case 'building':
118
+ return chalk.yellow('Building');
119
+ case 'failed':
120
+ return chalk.red('Failed');
121
+ case 'pending':
122
+ return chalk.dim('Pending');
123
+ default:
124
+ return chalk.dim(status || 'Unknown');
125
+ }
126
+ }
package/src/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import { Command } from 'commander';
2
+ import { createRequire } from 'module';
3
+ import updateNotifier from 'update-notifier';
4
+ import { loginCommand } from './commands/login.js';
5
+ import { logoutCommand } from './commands/logout.js';
6
+ import { createGameCommand } from './commands/create-game.js';
7
+ import { deployCommand } from './commands/deploy.js';
8
+ import { statusCommand } from './commands/status.js';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('../package.json');
12
+
13
+ export function run() {
14
+ // Check for CLI updates
15
+ updateNotifier({ pkg }).notify();
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name('myvillage')
21
+ .description('MyVillageOS CLI for student game developers')
22
+ .version(pkg.version);
23
+
24
+ program
25
+ .command('login')
26
+ .description('Authenticate with MyVillageOS')
27
+ .action(loginCommand);
28
+
29
+ program
30
+ .command('logout')
31
+ .description('Clear stored credentials')
32
+ .action(logoutCommand);
33
+
34
+ program
35
+ .command('create-game')
36
+ .description('Create a new game project with interactive wizard')
37
+ .action(createGameCommand);
38
+
39
+ program
40
+ .command('deploy')
41
+ .description('Deploy your game to MyVillageOS')
42
+ .action(deployCommand);
43
+
44
+ program
45
+ .command('status')
46
+ .description('Check deployment status and game analytics')
47
+ .action(statusCommand);
48
+
49
+ program.parse();
50
+ }
@@ -0,0 +1,121 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+ import { loadCredentials, saveCredentials, getAccessToken } from './auth.js';
4
+
5
+ const USER_AGENT = 'MyVillageOS-CLI/1.0.0';
6
+
7
+ function createClient() {
8
+ const config = getConfig();
9
+
10
+ const client = axios.create({
11
+ baseURL: config.apiBaseUrl,
12
+ headers: {
13
+ 'User-Agent': USER_AGENT,
14
+ },
15
+ });
16
+
17
+ // Add auth token to requests
18
+ client.interceptors.request.use((reqConfig) => {
19
+ const token = getAccessToken();
20
+ if (token) {
21
+ reqConfig.headers.Authorization = `Bearer ${token}`;
22
+ }
23
+ return reqConfig;
24
+ });
25
+
26
+ // Handle 401 with token refresh
27
+ client.interceptors.response.use(
28
+ (response) => response,
29
+ async (error) => {
30
+ const originalRequest = error.config;
31
+
32
+ if (error.response?.status === 401 && !originalRequest._retry) {
33
+ originalRequest._retry = true;
34
+
35
+ const refreshed = await refreshAccessToken();
36
+ if (refreshed) {
37
+ originalRequest.headers.Authorization = `Bearer ${getAccessToken()}`;
38
+ return client(originalRequest);
39
+ }
40
+ }
41
+
42
+ return Promise.reject(error);
43
+ }
44
+ );
45
+
46
+ return client;
47
+ }
48
+
49
+ async function refreshAccessToken() {
50
+ const config = getConfig();
51
+ const creds = loadCredentials();
52
+
53
+ if (!creds?.refresh_token) {
54
+ return false;
55
+ }
56
+
57
+ try {
58
+ const response = await axios.post(
59
+ `${config.oauthBaseUrl}/token`,
60
+ new URLSearchParams({
61
+ grant_type: 'refresh_token',
62
+ refresh_token: creds.refresh_token,
63
+ client_id: config.clientId,
64
+ }),
65
+ {
66
+ headers: {
67
+ 'Content-Type': 'application/x-www-form-urlencoded',
68
+ 'User-Agent': USER_AGENT,
69
+ },
70
+ }
71
+ );
72
+
73
+ const { access_token, refresh_token, expires_in } = response.data;
74
+
75
+ saveCredentials({
76
+ ...creds,
77
+ access_token,
78
+ refresh_token: refresh_token || creds.refresh_token,
79
+ expires_at: Date.now() + expires_in * 1000,
80
+ });
81
+
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ export function getApiClient() {
89
+ return createClient();
90
+ }
91
+
92
+ export async function getUserInfo(accessToken) {
93
+ const config = getConfig();
94
+
95
+ const response = await axios.get(`${config.oauthBaseUrl}/userinfo`, {
96
+ headers: {
97
+ Authorization: `Bearer ${accessToken}`,
98
+ 'User-Agent': USER_AGENT,
99
+ },
100
+ });
101
+
102
+ return response.data;
103
+ }
104
+
105
+ export async function deployGame(gameData) {
106
+ const client = getApiClient();
107
+ const response = await client.post('/games/deploy', gameData);
108
+ return response.data;
109
+ }
110
+
111
+ export async function getMyGames() {
112
+ const client = getApiClient();
113
+ const response = await client.get('/games/my-games');
114
+ return response.data;
115
+ }
116
+
117
+ export async function getGameStats(gameId) {
118
+ const client = getApiClient();
119
+ const response = await client.get(`/games/${gameId}/stats`);
120
+ return response.data;
121
+ }
@@ -0,0 +1,104 @@
1
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, chmodSync } from 'fs';
2
+ import { homedir, hostname, userInfo } from 'os';
3
+ import { join } from 'path';
4
+ import { createCipheriv, createDecipheriv, scryptSync, randomBytes } from 'crypto';
5
+ import { getConfigDir } from './config.js';
6
+
7
+ const CREDENTIALS_FILE = join(homedir(), '.myvillage', 'credentials.json');
8
+ const ALGORITHM = 'aes-256-gcm';
9
+
10
+ // Derive an encryption key from machine-specific data
11
+ function getEncryptionKey() {
12
+ const machineId = `${hostname()}-${userInfo().username}-myvillage-cli`;
13
+ return scryptSync(machineId, 'myvillageos-salt', 32);
14
+ }
15
+
16
+ function encrypt(text) {
17
+ const key = getEncryptionKey();
18
+ const iv = randomBytes(16);
19
+ const cipher = createCipheriv(ALGORITHM, key, iv);
20
+
21
+ let encrypted = cipher.update(text, 'utf8', 'hex');
22
+ encrypted += cipher.final('hex');
23
+
24
+ const authTag = cipher.getAuthTag().toString('hex');
25
+
26
+ return JSON.stringify({
27
+ iv: iv.toString('hex'),
28
+ encrypted,
29
+ authTag,
30
+ });
31
+ }
32
+
33
+ function decrypt(payload) {
34
+ const key = getEncryptionKey();
35
+ const { iv, encrypted, authTag } = JSON.parse(payload);
36
+
37
+ const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(iv, 'hex'));
38
+ decipher.setAuthTag(Buffer.from(authTag, 'hex'));
39
+
40
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
41
+ decrypted += decipher.final('utf8');
42
+
43
+ return decrypted;
44
+ }
45
+
46
+ export function saveCredentials(credentials) {
47
+ getConfigDir(); // Ensure directory exists
48
+ const encrypted = encrypt(JSON.stringify(credentials));
49
+ writeFileSync(CREDENTIALS_FILE, encrypted, { mode: 0o600 });
50
+
51
+ // Ensure restrictive permissions
52
+ try {
53
+ chmodSync(CREDENTIALS_FILE, 0o600);
54
+ } catch {
55
+ // Permissions may not be supported on all platforms
56
+ }
57
+ }
58
+
59
+ export function loadCredentials() {
60
+ if (!existsSync(CREDENTIALS_FILE)) {
61
+ return null;
62
+ }
63
+
64
+ try {
65
+ const data = readFileSync(CREDENTIALS_FILE, 'utf-8');
66
+ const decrypted = decrypt(data);
67
+ return JSON.parse(decrypted);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export function clearCredentials() {
74
+ if (existsSync(CREDENTIALS_FILE)) {
75
+ unlinkSync(CREDENTIALS_FILE);
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ export function isAuthenticated() {
82
+ const creds = loadCredentials();
83
+ if (!creds || !creds.access_token) {
84
+ return false;
85
+ }
86
+
87
+ // Check if token is expired
88
+ if (creds.expires_at && Date.now() >= creds.expires_at) {
89
+ // Token expired, but we might have a refresh token
90
+ return !!creds.refresh_token;
91
+ }
92
+
93
+ return true;
94
+ }
95
+
96
+ export function getAccessToken() {
97
+ const creds = loadCredentials();
98
+ return creds?.access_token || null;
99
+ }
100
+
101
+ export function getRefreshToken() {
102
+ const creds = loadCredentials();
103
+ return creds?.refresh_token || null;
104
+ }
@@ -0,0 +1,48 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.myvillage');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULT_CONFIG = {
9
+ apiBaseUrl: 'https://portal.myvillageproject.ai/api/v1',
10
+ oauthBaseUrl: 'https://portal.myvillageproject.ai/api/oauth',
11
+ clientId: 'myvillage-cli',
12
+ callbackPort: 3737,
13
+ };
14
+
15
+ function ensureConfigDir() {
16
+ if (!existsSync(CONFIG_DIR)) {
17
+ mkdirSync(CONFIG_DIR, { recursive: true });
18
+ }
19
+ }
20
+
21
+ export function getConfig() {
22
+ ensureConfigDir();
23
+
24
+ if (!existsSync(CONFIG_FILE)) {
25
+ writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
26
+ return { ...DEFAULT_CONFIG };
27
+ }
28
+
29
+ try {
30
+ const data = readFileSync(CONFIG_FILE, 'utf-8');
31
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
32
+ } catch {
33
+ return { ...DEFAULT_CONFIG };
34
+ }
35
+ }
36
+
37
+ export function setConfig(updates) {
38
+ ensureConfigDir();
39
+ const current = getConfig();
40
+ const merged = { ...current, ...updates };
41
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
42
+ return merged;
43
+ }
44
+
45
+ export function getConfigDir() {
46
+ ensureConfigDir();
47
+ return CONFIG_DIR;
48
+ }