@myvillage/cli 1.2.2 → 1.5.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/README.md +14 -199
- package/package.json +6 -2
- package/src/agent-runtime/context.js +99 -0
- package/src/agent-runtime/daemon-entry.js +66 -0
- package/src/agent-runtime/daemon.js +65 -0
- package/src/agent-runtime/loop.js +281 -0
- package/src/agent-runtime/mcp-client.js +400 -0
- package/src/agent-runtime/scheduler.js +53 -0
- package/src/commands/agent-local.js +607 -0
- package/src/commands/agent.js +540 -0
- package/src/commands/bizreqs.js +965 -0
- package/src/commands/comment.js +29 -3
- package/src/commands/community.js +13 -12
- package/src/commands/create-app.js +253 -0
- package/src/commands/create-game.js +9 -8
- package/src/commands/deploy.js +101 -23
- package/src/commands/feed.js +4 -3
- package/src/commands/login.js +7 -6
- package/src/commands/logout.js +3 -2
- package/src/commands/post.js +67 -11
- package/src/commands/profile.js +4 -3
- package/src/commands/search.js +3 -2
- package/src/commands/status.js +64 -28
- package/src/commands/vote.js +46 -18
- package/src/index.js +179 -1
- package/src/utils/agent-scaffolder.js +165 -0
- package/src/utils/api.js +175 -11
- package/src/utils/app-templates.js +2983 -0
- package/src/utils/brand.js +107 -0
- package/src/utils/config.js +16 -1
- package/src/utils/formatters.js +404 -11
- package/src/utils/local-agent.js +168 -0
package/src/commands/comment.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
3
4
|
import inquirer from 'inquirer';
|
|
4
5
|
import { isAuthenticated } from '../utils/auth.js';
|
|
5
|
-
import { createComment } from '../utils/api.js';
|
|
6
|
+
import { createComment, listMyAgents } from '../utils/api.js';
|
|
6
7
|
import { relativeTime } from '../utils/formatters.js';
|
|
7
8
|
|
|
8
9
|
export async function commentCommand(postId, options) {
|
|
@@ -11,6 +12,28 @@ export async function commentCommand(postId, options) {
|
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// Resolve agent identity
|
|
16
|
+
let agentProfileId = null;
|
|
17
|
+
|
|
18
|
+
if (options.as) {
|
|
19
|
+
const agentsSpinner = villageSpinner('Resolving agent...').start();
|
|
20
|
+
try {
|
|
21
|
+
const agentsResult = await listMyAgents();
|
|
22
|
+
const agents = agentsResult.data || agentsResult;
|
|
23
|
+
const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
|
|
24
|
+
agentsSpinner.stop();
|
|
25
|
+
|
|
26
|
+
if (!agent) {
|
|
27
|
+
console.log(chalk.red(` ✗ Agent @${options.as} not found. Run 'myvillage agent' to see your agents.`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
agentProfileId = agent.id;
|
|
31
|
+
console.log(brand.teal(` Commenting as agent @${agent.handle}\n`));
|
|
32
|
+
} catch {
|
|
33
|
+
agentsSpinner.stop();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
let body = options.body;
|
|
15
38
|
|
|
16
39
|
if (!body) {
|
|
@@ -33,20 +56,23 @@ export async function commentCommand(postId, options) {
|
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
|
|
36
|
-
const spinner =
|
|
59
|
+
const spinner = villageSpinner('Posting comment...').start();
|
|
37
60
|
|
|
38
61
|
try {
|
|
39
62
|
const data = { body: body.trim() };
|
|
40
63
|
if (options.replyTo) {
|
|
41
64
|
data.parentCommentId = options.replyTo;
|
|
42
65
|
}
|
|
66
|
+
if (agentProfileId) {
|
|
67
|
+
data.agentProfileId = agentProfileId;
|
|
68
|
+
}
|
|
43
69
|
|
|
44
70
|
const result = await createComment(postId, data);
|
|
45
71
|
spinner.succeed('Comment posted!');
|
|
46
72
|
|
|
47
73
|
const comment = result.data || result;
|
|
48
74
|
console.log('');
|
|
49
|
-
console.log(` ${
|
|
75
|
+
console.log(` ${brand.teal('You')} ${brand.teal('·')} ${brand.teal(relativeTime(comment.createdAt || new Date().toISOString()))} ${chalk.green('▲ 0')}`);
|
|
50
76
|
const bodyLines = comment.body?.split('\n') || body.trim().split('\n');
|
|
51
77
|
for (const line of bodyLines) {
|
|
52
78
|
console.log(` ${line}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
3
4
|
import inquirer from 'inquirer';
|
|
4
5
|
import { isAuthenticated } from '../utils/auth.js';
|
|
5
6
|
import {
|
|
@@ -36,7 +37,7 @@ export async function communityListCommand(options) {
|
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const spinner =
|
|
40
|
+
const spinner = villageSpinner('Loading communities...').start();
|
|
40
41
|
|
|
41
42
|
try {
|
|
42
43
|
const params = {
|
|
@@ -56,7 +57,7 @@ export async function communityListCommand(options) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
if (!Array.isArray(communities) || communities.length === 0) {
|
|
59
|
-
console.log(
|
|
60
|
+
console.log(brand.teal('\n No communities found.\n'));
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -75,7 +76,7 @@ export async function communityViewCommand(slug) {
|
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
const spinner =
|
|
79
|
+
const spinner = villageSpinner(`Loading community ${slug}...`).start();
|
|
79
80
|
|
|
80
81
|
try {
|
|
81
82
|
const result = await getCommunity(slug);
|
|
@@ -130,11 +131,11 @@ export async function communityCreateCommand() {
|
|
|
130
131
|
]);
|
|
131
132
|
|
|
132
133
|
if (!answers.confirm) {
|
|
133
|
-
console.log(
|
|
134
|
+
console.log(brand.teal(' Cancelled.\n'));
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
const spinner =
|
|
138
|
+
const spinner = villageSpinner('Creating community...').start();
|
|
138
139
|
|
|
139
140
|
const data = {
|
|
140
141
|
name: answers.name.trim(),
|
|
@@ -147,8 +148,8 @@ export async function communityCreateCommand() {
|
|
|
147
148
|
spinner.succeed('Community created!');
|
|
148
149
|
|
|
149
150
|
const community = result.data || result;
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(
|
|
151
|
+
console.log(brand.green(` ✓ r/${community.slug} is now live!`));
|
|
152
|
+
console.log(brand.teal(` ID: ${community.id}\n`));
|
|
152
153
|
} catch (err) {
|
|
153
154
|
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
154
155
|
console.log(chalk.red(` ✗ Failed to create community: ${message}\n`));
|
|
@@ -161,7 +162,7 @@ export async function communityJoinCommand(slug) {
|
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
const spinner =
|
|
165
|
+
const spinner = villageSpinner(`Joining r/${slug}...`).start();
|
|
165
166
|
|
|
166
167
|
try {
|
|
167
168
|
await apiJoinCommunity(slug);
|
|
@@ -178,7 +179,7 @@ export async function communityLeaveCommand(slug) {
|
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
const spinner =
|
|
182
|
+
const spinner = villageSpinner(`Leaving r/${slug}...`).start();
|
|
182
183
|
|
|
183
184
|
try {
|
|
184
185
|
await apiLeaveCommunity(slug);
|
|
@@ -195,7 +196,7 @@ export async function communityMembersCommand(slug, options) {
|
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
const spinner =
|
|
199
|
+
const spinner = villageSpinner(`Loading members of r/${slug}...`).start();
|
|
199
200
|
|
|
200
201
|
try {
|
|
201
202
|
const params = { pageSize: parseInt(options.limit) || 20 };
|
|
@@ -205,7 +206,7 @@ export async function communityMembersCommand(slug, options) {
|
|
|
205
206
|
const members = result.data || result;
|
|
206
207
|
|
|
207
208
|
if (!Array.isArray(members) || members.length === 0) {
|
|
208
|
-
console.log(
|
|
209
|
+
console.log(brand.teal('\n No members found.\n'));
|
|
209
210
|
return;
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -214,7 +215,7 @@ export async function communityMembersCommand(slug, options) {
|
|
|
214
215
|
const name = member.villager
|
|
215
216
|
? [member.villager.firstName, member.villager.lastName].filter(Boolean).join(' ')
|
|
216
217
|
: member.agentProfile
|
|
217
|
-
? `${member.agentProfile.handle} ${
|
|
218
|
+
? `${member.agentProfile.handle} ${brand.teal('(agent)')}`
|
|
218
219
|
: 'unknown';
|
|
219
220
|
const role = member.role !== 'MEMBER' ? chalk.yellow(` [${member.role}]`) : '';
|
|
220
221
|
console.log(` ${chalk.white(`@${name}`)}${role}`);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
8
|
+
import { createGameCommand } from './create-game.js';
|
|
9
|
+
import { createPortalProject, createDataLabelingProject } from '../utils/app-templates.js';
|
|
10
|
+
import { registerOAuthClient } from '../utils/api.js';
|
|
11
|
+
|
|
12
|
+
export async function createCommand() {
|
|
13
|
+
// Check authentication
|
|
14
|
+
if (!isAuthenticated()) {
|
|
15
|
+
console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { projectType } = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'list',
|
|
22
|
+
name: 'projectType',
|
|
23
|
+
message: 'What would you like to build?',
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: 'Portal \u2014 Customizable multi-page web app', value: 'portal' },
|
|
26
|
+
{ name: 'Data Labeling App \u2014 Multi-modal annotation workspace', value: 'data-labeling' },
|
|
27
|
+
{ name: 'Game \u2014 Three.js educational game', value: 'game' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
if (projectType === 'game') {
|
|
33
|
+
await createGameCommand();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (projectType === 'portal') {
|
|
38
|
+
await portalFlow();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (projectType === 'data-labeling') {
|
|
43
|
+
await dataLabelingFlow();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function portalFlow() {
|
|
49
|
+
const answers = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'input',
|
|
52
|
+
name: 'name',
|
|
53
|
+
message: 'Portal name:',
|
|
54
|
+
default: 'My Portal',
|
|
55
|
+
validate: input => input.trim() ? true : 'Name is required.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'input',
|
|
59
|
+
name: 'description',
|
|
60
|
+
message: 'Description:',
|
|
61
|
+
default: 'A portal built with React and Vite',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'checkbox',
|
|
65
|
+
name: 'features',
|
|
66
|
+
message: 'Select features:',
|
|
67
|
+
choices: [
|
|
68
|
+
{ name: 'Authentication (MyVillageOS OAuth) \u2014 recommended', value: 'auth', checked: true },
|
|
69
|
+
{ name: 'Dashboard with widgets', value: 'dashboard', checked: true },
|
|
70
|
+
{ name: 'Settings page', value: 'settings', checked: true },
|
|
71
|
+
{ name: 'User management', value: 'users', checked: false },
|
|
72
|
+
{ name: 'Notifications', value: 'notifications', checked: false },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// Create project directory
|
|
78
|
+
const slug = answers.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
79
|
+
const targetDir = resolve(process.cwd(), slug);
|
|
80
|
+
|
|
81
|
+
if (existsSync(targetDir)) {
|
|
82
|
+
console.log(chalk.red(`\n \u2717 Directory "${slug}" already exists. Choose a different name or remove the existing directory.`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// OAuth registration (if auth selected)
|
|
87
|
+
let oauthCredentials = null;
|
|
88
|
+
if (answers.features.includes('auth')) {
|
|
89
|
+
const oauthSpinner = villageSpinner('Registering OAuth application...').start();
|
|
90
|
+
try {
|
|
91
|
+
const result = await registerOAuthClient(
|
|
92
|
+
answers.name,
|
|
93
|
+
'portal',
|
|
94
|
+
['http://localhost:5173/callback']
|
|
95
|
+
);
|
|
96
|
+
oauthCredentials = { clientId: result.clientId, clientSecret: result.clientSecret };
|
|
97
|
+
oauthSpinner.succeed('OAuth application registered!');
|
|
98
|
+
console.log(brand.teal(` Client ID: ${result.clientId}`));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
oauthSpinner.fail('Failed to register OAuth application.');
|
|
101
|
+
console.log(chalk.red(` ${err.response?.data?.error || err.message}`));
|
|
102
|
+
const { continueWithoutAuth } = await inquirer.prompt([{
|
|
103
|
+
type: 'confirm',
|
|
104
|
+
name: 'continueWithoutAuth',
|
|
105
|
+
message: 'Continue without authentication?',
|
|
106
|
+
default: true,
|
|
107
|
+
}]);
|
|
108
|
+
if (!continueWithoutAuth) return;
|
|
109
|
+
answers.features = answers.features.filter(f => f !== 'auth');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Scaffold project
|
|
114
|
+
const spinner = villageSpinner('Creating portal project...').start();
|
|
115
|
+
try {
|
|
116
|
+
createPortalProject(targetDir, {
|
|
117
|
+
name: answers.name,
|
|
118
|
+
description: answers.description,
|
|
119
|
+
features: answers.features,
|
|
120
|
+
oauthCredentials,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
spinner.text = 'Installing dependencies...';
|
|
124
|
+
execSync('npm install', { cwd: targetDir, stdio: 'pipe' });
|
|
125
|
+
spinner.succeed(`Portal "${answers.name}" created successfully!`);
|
|
126
|
+
|
|
127
|
+
console.log();
|
|
128
|
+
console.log(brand.green(` \u2713 Portal "${answers.name}" created successfully!`));
|
|
129
|
+
console.log();
|
|
130
|
+
console.log(brand.teal(' Next steps:'));
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(` ${brand.gold('cd')} ${slug}`);
|
|
133
|
+
console.log(` ${brand.gold('npm run dev')} ${brand.teal('- Start development server')}`);
|
|
134
|
+
console.log(` ${brand.gold('npm run build')} ${brand.teal('- Build for production')}`);
|
|
135
|
+
console.log();
|
|
136
|
+
if (answers.features.length > 0) {
|
|
137
|
+
console.log(brand.teal(` Features: ${answers.features.join(', ')}`));
|
|
138
|
+
}
|
|
139
|
+
if (oauthCredentials) {
|
|
140
|
+
console.log(brand.teal(` OAuth Client ID: ${oauthCredentials.clientId}`));
|
|
141
|
+
}
|
|
142
|
+
console.log();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
spinner.fail('Failed to create portal project.');
|
|
145
|
+
console.error(chalk.red(` ${err.message}`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function dataLabelingFlow() {
|
|
150
|
+
const answers = await inquirer.prompt([
|
|
151
|
+
{
|
|
152
|
+
type: 'input',
|
|
153
|
+
name: 'name',
|
|
154
|
+
message: 'App name:',
|
|
155
|
+
default: 'My Data Labeler',
|
|
156
|
+
validate: input => input.trim() ? true : 'Name is required.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: 'input',
|
|
160
|
+
name: 'description',
|
|
161
|
+
message: 'Description:',
|
|
162
|
+
default: 'A data labeling workspace built with React and Vite',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'checkbox',
|
|
166
|
+
name: 'dataTypes',
|
|
167
|
+
message: 'Select data types to support:',
|
|
168
|
+
choices: [
|
|
169
|
+
{ name: 'Image \u2014 bounding boxes, polygons, classification', value: 'image', checked: true },
|
|
170
|
+
{ name: 'Text \u2014 NER, classification', value: 'text', checked: true },
|
|
171
|
+
{ name: 'Audio \u2014 segment labeling, transcription', value: 'audio', checked: false },
|
|
172
|
+
{ name: 'Video \u2014 frame-by-frame annotation', value: 'video', checked: false },
|
|
173
|
+
],
|
|
174
|
+
validate: input => input.length > 0 ? true : 'Select at least one data type.',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
type: 'confirm',
|
|
178
|
+
name: 'includeAuth',
|
|
179
|
+
message: 'Include MyVillageOS OAuth authentication? (recommended)',
|
|
180
|
+
default: true,
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Create project directory
|
|
185
|
+
const slug = answers.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
186
|
+
const targetDir = resolve(process.cwd(), slug);
|
|
187
|
+
|
|
188
|
+
if (existsSync(targetDir)) {
|
|
189
|
+
console.log(chalk.red(`\n \u2717 Directory "${slug}" already exists. Choose a different name or remove the existing directory.`));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// OAuth registration (if auth selected)
|
|
194
|
+
let oauthCredentials = null;
|
|
195
|
+
if (answers.includeAuth) {
|
|
196
|
+
const oauthSpinner = villageSpinner('Registering OAuth application...').start();
|
|
197
|
+
try {
|
|
198
|
+
const result = await registerOAuthClient(
|
|
199
|
+
answers.name,
|
|
200
|
+
'data-labeling',
|
|
201
|
+
['http://localhost:5173/callback']
|
|
202
|
+
);
|
|
203
|
+
oauthCredentials = { clientId: result.clientId, clientSecret: result.clientSecret };
|
|
204
|
+
oauthSpinner.succeed('OAuth application registered!');
|
|
205
|
+
console.log(brand.teal(` Client ID: ${result.clientId}`));
|
|
206
|
+
} catch (err) {
|
|
207
|
+
oauthSpinner.fail('Failed to register OAuth application.');
|
|
208
|
+
console.log(chalk.red(` ${err.response?.data?.error || err.message}`));
|
|
209
|
+
const { continueWithoutAuth } = await inquirer.prompt([{
|
|
210
|
+
type: 'confirm',
|
|
211
|
+
name: 'continueWithoutAuth',
|
|
212
|
+
message: 'Continue without authentication?',
|
|
213
|
+
default: true,
|
|
214
|
+
}]);
|
|
215
|
+
if (!continueWithoutAuth) return;
|
|
216
|
+
answers.includeAuth = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Scaffold project
|
|
221
|
+
const spinner = villageSpinner('Creating data labeling project...').start();
|
|
222
|
+
try {
|
|
223
|
+
createDataLabelingProject(targetDir, {
|
|
224
|
+
name: answers.name,
|
|
225
|
+
description: answers.description,
|
|
226
|
+
dataTypes: answers.dataTypes,
|
|
227
|
+
includeAuth: answers.includeAuth,
|
|
228
|
+
oauthCredentials,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
spinner.text = 'Installing dependencies...';
|
|
232
|
+
execSync('npm install', { cwd: targetDir, stdio: 'pipe' });
|
|
233
|
+
spinner.succeed(`Data labeling app "${answers.name}" created successfully!`);
|
|
234
|
+
|
|
235
|
+
console.log();
|
|
236
|
+
console.log(brand.green(` \u2713 Data labeling app "${answers.name}" created successfully!`));
|
|
237
|
+
console.log();
|
|
238
|
+
console.log(brand.teal(' Next steps:'));
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(` ${brand.gold('cd')} ${slug}`);
|
|
241
|
+
console.log(` ${brand.gold('npm run dev')} ${brand.teal('- Start development server')}`);
|
|
242
|
+
console.log(` ${brand.gold('npm run build')} ${brand.teal('- Build for production')}`);
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(brand.teal(` Data types: ${answers.dataTypes.join(', ')}`));
|
|
245
|
+
if (oauthCredentials) {
|
|
246
|
+
console.log(brand.teal(` OAuth Client ID: ${oauthCredentials.clientId}`));
|
|
247
|
+
}
|
|
248
|
+
console.log();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
spinner.fail('Failed to create data labeling project.');
|
|
251
|
+
console.error(chalk.red(` ${err.message}`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'path';
|
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
6
7
|
import inquirer from 'inquirer';
|
|
7
8
|
import { isAuthenticated } from '../utils/auth.js';
|
|
8
9
|
import { createGameProject } from '../utils/templates.js';
|
|
@@ -65,7 +66,7 @@ export async function createGameCommand() {
|
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const spinner =
|
|
69
|
+
const spinner = villageSpinner('Creating game project...').start();
|
|
69
70
|
|
|
70
71
|
try {
|
|
71
72
|
// Generate project from template
|
|
@@ -88,16 +89,16 @@ export async function createGameCommand() {
|
|
|
88
89
|
|
|
89
90
|
// Display next steps
|
|
90
91
|
console.log();
|
|
91
|
-
console.log(
|
|
92
|
+
console.log(brand.green(` \u2713 Game "${answers.name}" created successfully!`));
|
|
92
93
|
console.log();
|
|
93
|
-
console.log(
|
|
94
|
+
console.log(brand.teal(' Next steps:'));
|
|
94
95
|
console.log();
|
|
95
|
-
console.log(` ${
|
|
96
|
-
console.log(` ${
|
|
97
|
-
console.log(` ${
|
|
98
|
-
console.log(` ${
|
|
96
|
+
console.log(` ${brand.gold('cd')} ${slug}`);
|
|
97
|
+
console.log(` ${brand.gold('npm run dev')} ${brand.teal('- Start development server')}`);
|
|
98
|
+
console.log(` ${brand.gold('npm run build')} ${brand.teal('- Build for production')}`);
|
|
99
|
+
console.log(` ${brand.gold('myvillage deploy')} ${brand.teal('- Deploy to MyVillageOS')}`);
|
|
99
100
|
console.log();
|
|
100
|
-
console.log(
|
|
101
|
+
console.log(brand.teal(` Game type: ${answers.type} | Age group: ${answers.ageGroup}`));
|
|
101
102
|
console.log();
|
|
102
103
|
} catch (err) {
|
|
103
104
|
spinner.fail('Failed to create game project.');
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync,
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { join, resolve, relative } from 'path';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
6
7
|
import { isAuthenticated } from '../utils/auth.js';
|
|
7
8
|
import { deployGame } from '../utils/api.js';
|
|
8
9
|
|
|
@@ -17,9 +18,16 @@ function readPackageJson(dir) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
21
|
+
function saveGameIdToPackageJson(dir, gameId, slug) {
|
|
22
|
+
const pkgPath = join(dir, 'package.json');
|
|
23
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
24
|
+
pkg.myvillage = { ...pkg.myvillage, gameId, slug };
|
|
25
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Recursively collect file paths from a directory
|
|
29
|
+
function collectFilePaths(dir) {
|
|
30
|
+
const paths = [];
|
|
23
31
|
|
|
24
32
|
function walk(currentDir) {
|
|
25
33
|
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -28,15 +36,13 @@ function collectBuildFiles(dir, baseDir) {
|
|
|
28
36
|
if (entry.isDirectory()) {
|
|
29
37
|
walk(fullPath);
|
|
30
38
|
} else {
|
|
31
|
-
|
|
32
|
-
const content = readFileSync(fullPath);
|
|
33
|
-
files[relPath] = content.toString('base64');
|
|
39
|
+
paths.push(fullPath);
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
walk(dir);
|
|
39
|
-
return
|
|
45
|
+
return paths;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export async function deployCommand() {
|
|
@@ -52,11 +58,11 @@ export async function deployCommand() {
|
|
|
52
58
|
// Verify this is a valid MyVillageOS game project
|
|
53
59
|
if (!pkg || !pkg.myvillage) {
|
|
54
60
|
console.log(chalk.red(' \u2717 Not a valid game project directory.'));
|
|
55
|
-
console.log(
|
|
61
|
+
console.log(brand.teal(' Make sure you are inside a game directory created with "myvillage create-game".'));
|
|
56
62
|
return;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
const spinner =
|
|
65
|
+
const spinner = villageSpinner('Building game...').start();
|
|
60
66
|
|
|
61
67
|
try {
|
|
62
68
|
// Run production build
|
|
@@ -75,32 +81,61 @@ export async function deployCommand() {
|
|
|
75
81
|
spinner.text = 'Packaging build artifacts...';
|
|
76
82
|
|
|
77
83
|
// Collect build files
|
|
78
|
-
const
|
|
84
|
+
const filePaths = collectFilePaths(distDir);
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
if (filePaths.length === 0) {
|
|
87
|
+
spinner.fail('No files found in dist/ directory.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
81
90
|
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
|
|
91
|
+
// Build multipart FormData
|
|
92
|
+
const formData = new FormData();
|
|
93
|
+
|
|
94
|
+
// Add metadata
|
|
95
|
+
const metadata = {
|
|
96
|
+
title: pkg.myvillage.name || pkg.name,
|
|
85
97
|
description: pkg.description || '',
|
|
86
|
-
category: pkg.myvillage.gameType || '
|
|
98
|
+
category: (pkg.myvillage.gameType || 'OTHER').toUpperCase(),
|
|
99
|
+
gameType: 'THREE_JS',
|
|
87
100
|
targetAge: pkg.myvillage.targetAge || 'all',
|
|
88
|
-
|
|
89
|
-
}
|
|
101
|
+
entryFile: 'index.html',
|
|
102
|
+
};
|
|
103
|
+
formData.append('metadata', JSON.stringify(metadata));
|
|
104
|
+
|
|
105
|
+
// Add each file with relative path as the filename
|
|
106
|
+
for (const fullPath of filePaths) {
|
|
107
|
+
const relPath = relative(distDir, fullPath);
|
|
108
|
+
const fileBuffer = readFileSync(fullPath);
|
|
109
|
+
const blob = new Blob([fileBuffer], { type: getContentType(relPath) });
|
|
110
|
+
formData.append('files', blob, relPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
spinner.text = 'Deploying to MyVillageOS...';
|
|
114
|
+
|
|
115
|
+
// Use existing gameId if this project was deployed before, otherwise "new"
|
|
116
|
+
const gameIdOrNew = pkg.myvillage.gameId || 'new';
|
|
117
|
+
const result = await deployGame(gameIdOrNew, formData);
|
|
118
|
+
|
|
119
|
+
// Save gameId to package.json for subsequent deploys
|
|
120
|
+
if (result.gameId && !pkg.myvillage.gameId) {
|
|
121
|
+
saveGameIdToPackageJson(projectDir, result.gameId, result.slug);
|
|
122
|
+
}
|
|
90
123
|
|
|
91
124
|
spinner.succeed('Deployment complete!');
|
|
92
125
|
console.log();
|
|
93
|
-
console.log(
|
|
126
|
+
console.log(brand.green(` \u2713 Game "${metadata.title}" submitted for review`));
|
|
127
|
+
console.log(` Status: ${formatStatus(result.status)}`);
|
|
94
128
|
|
|
95
|
-
if (result.
|
|
96
|
-
console.log(
|
|
129
|
+
if (result.slug) {
|
|
130
|
+
console.log(brand.teal(` Slug: ${result.slug}`));
|
|
97
131
|
}
|
|
98
|
-
|
|
99
132
|
if (result.gameId) {
|
|
100
|
-
console.log(
|
|
133
|
+
console.log(brand.teal(` Game ID: ${result.gameId}`));
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
console.log();
|
|
137
|
+
console.log(brand.teal(' Run "myvillage status" to check the review status.'));
|
|
138
|
+
console.log();
|
|
104
139
|
} catch (err) {
|
|
105
140
|
if (err.response) {
|
|
106
141
|
// API error
|
|
@@ -118,3 +153,46 @@ export async function deployCommand() {
|
|
|
118
153
|
}
|
|
119
154
|
}
|
|
120
155
|
}
|
|
156
|
+
|
|
157
|
+
function formatStatus(status) {
|
|
158
|
+
switch (status) {
|
|
159
|
+
case 'SUBMITTED':
|
|
160
|
+
return chalk.yellow('Submitted for review');
|
|
161
|
+
case 'DRAFT':
|
|
162
|
+
return brand.teal('Draft');
|
|
163
|
+
case 'APPROVED':
|
|
164
|
+
return chalk.green('Approved');
|
|
165
|
+
case 'PUBLISHED':
|
|
166
|
+
return chalk.green('Published');
|
|
167
|
+
default:
|
|
168
|
+
return brand.teal(status || 'Unknown');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getContentType(filePath) {
|
|
173
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
174
|
+
const types = {
|
|
175
|
+
html: 'text/html',
|
|
176
|
+
js: 'application/javascript',
|
|
177
|
+
css: 'text/css',
|
|
178
|
+
json: 'application/json',
|
|
179
|
+
png: 'image/png',
|
|
180
|
+
jpg: 'image/jpeg',
|
|
181
|
+
jpeg: 'image/jpeg',
|
|
182
|
+
gif: 'image/gif',
|
|
183
|
+
svg: 'image/svg+xml',
|
|
184
|
+
webp: 'image/webp',
|
|
185
|
+
glb: 'model/gltf-binary',
|
|
186
|
+
gltf: 'model/gltf+json',
|
|
187
|
+
obj: 'text/plain',
|
|
188
|
+
mtl: 'text/plain',
|
|
189
|
+
mp3: 'audio/mpeg',
|
|
190
|
+
ogg: 'audio/ogg',
|
|
191
|
+
wav: 'audio/wav',
|
|
192
|
+
woff: 'font/woff',
|
|
193
|
+
woff2: 'font/woff2',
|
|
194
|
+
ttf: 'font/ttf',
|
|
195
|
+
wasm: 'application/wasm',
|
|
196
|
+
};
|
|
197
|
+
return types[ext] || 'application/octet-stream';
|
|
198
|
+
}
|
package/src/commands/feed.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
3
4
|
import { isAuthenticated } from '../utils/auth.js';
|
|
4
5
|
import { getFeed, getTrendingFeed, getLatestFeed, listPosts } from '../utils/api.js';
|
|
5
6
|
import { formatPostList, formatPagination } from '../utils/formatters.js';
|
|
@@ -10,7 +11,7 @@ export async function feedCommand(options) {
|
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const spinner =
|
|
14
|
+
const spinner = villageSpinner('Loading feed...').start();
|
|
14
15
|
|
|
15
16
|
try {
|
|
16
17
|
const params = {
|
|
@@ -43,8 +44,8 @@ export async function feedCommand(options) {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (!Array.isArray(posts) || posts.length === 0) {
|
|
46
|
-
console.log(
|
|
47
|
-
console.log(
|
|
47
|
+
console.log(brand.teal('\n No posts found. Join some communities to see content!'));
|
|
48
|
+
console.log(brand.teal(' Run "myvillage community list" to discover communities.\n'));
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|