@myvillage/cli 1.6.2 → 1.7.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 +1 -1
- package/src/commands/create-app.js +120 -137
- package/src/index.js +1 -1
- package/src/utils/agentic-templates.js +1654 -0
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import inquirer from 'inquirer';
|
|
|
7
7
|
import { isAuthenticated } from '../utils/auth.js';
|
|
8
8
|
import { createGameCommand } from './create-game.js';
|
|
9
9
|
import { createPortalProject, createDataLabelingProject } from '../utils/app-templates.js';
|
|
10
|
+
import { createAgenticAppProject } from '../utils/agentic-templates.js';
|
|
10
11
|
import { registerOAuthClient } from '../utils/api.js';
|
|
11
12
|
|
|
12
13
|
export async function createCommand() {
|
|
@@ -22,8 +23,7 @@ export async function createCommand() {
|
|
|
22
23
|
name: 'projectType',
|
|
23
24
|
message: 'What would you like to build?',
|
|
24
25
|
choices: [
|
|
25
|
-
{ name: '
|
|
26
|
-
{ name: 'Data Labeling App \u2014 Multi-modal annotation workspace', value: 'data-labeling' },
|
|
26
|
+
{ name: 'Application \u2014 Web app with optional AI agent, MCP, and API access', value: 'application' },
|
|
27
27
|
{ name: 'Game \u2014 Three.js educational game', value: 'game' },
|
|
28
28
|
],
|
|
29
29
|
},
|
|
@@ -34,38 +34,51 @@ export async function createCommand() {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
if (projectType === '
|
|
38
|
-
await
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (projectType === 'data-labeling') {
|
|
43
|
-
await dataLabelingFlow();
|
|
37
|
+
if (projectType === 'application') {
|
|
38
|
+
await applicationFlow();
|
|
44
39
|
return;
|
|
45
40
|
}
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
async function
|
|
49
|
-
const
|
|
43
|
+
async function applicationFlow() {
|
|
44
|
+
const basics = await inquirer.prompt([
|
|
50
45
|
{
|
|
51
46
|
type: 'input',
|
|
52
47
|
name: 'name',
|
|
53
|
-
message: '
|
|
54
|
-
default: 'My
|
|
48
|
+
message: 'Application name:',
|
|
49
|
+
default: 'My App',
|
|
55
50
|
validate: input => input.trim() ? true : 'Name is required.',
|
|
56
51
|
},
|
|
57
52
|
{
|
|
58
53
|
type: 'input',
|
|
59
54
|
name: 'description',
|
|
60
55
|
message: 'Description:',
|
|
61
|
-
default: 'A
|
|
56
|
+
default: 'A web application built with MyVillageOS',
|
|
62
57
|
},
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// Auth strategy
|
|
61
|
+
const { authStrategy } = await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'list',
|
|
64
|
+
name: 'authStrategy',
|
|
65
|
+
message: 'How should users authenticate?',
|
|
66
|
+
choices: [
|
|
67
|
+
{ name: 'MyVillageOS OAuth \u2014 "Login with MyVillageOS" (recommended)', value: 'oauth' },
|
|
68
|
+
{ name: 'Email / Password \u2014 Local credentials', value: 'email-password' },
|
|
69
|
+
{ name: 'Both \u2014 OAuth + local fallback', value: 'both' },
|
|
70
|
+
{ name: 'No login needed \u2014 Public / anonymous', value: 'none' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// UI features
|
|
76
|
+
const { features } = await inquirer.prompt([
|
|
63
77
|
{
|
|
64
78
|
type: 'checkbox',
|
|
65
79
|
name: 'features',
|
|
66
|
-
message: 'Select features:',
|
|
80
|
+
message: 'Select UI features:',
|
|
67
81
|
choices: [
|
|
68
|
-
{ name: 'Authentication (MyVillageOS OAuth) \u2014 recommended', value: 'auth', checked: true },
|
|
69
82
|
{ name: 'Dashboard with widgets', value: 'dashboard', checked: true },
|
|
70
83
|
{ name: 'Settings page', value: 'settings', checked: true },
|
|
71
84
|
{ name: 'User management', value: 'users', checked: false },
|
|
@@ -74,115 +87,46 @@ async function portalFlow() {
|
|
|
74
87
|
},
|
|
75
88
|
]);
|
|
76
89
|
|
|
77
|
-
//
|
|
78
|
-
const
|
|
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
|
-
},
|
|
90
|
+
// Backend integration
|
|
91
|
+
const { integrations } = await inquirer.prompt([
|
|
164
92
|
{
|
|
165
93
|
type: 'checkbox',
|
|
166
|
-
name: '
|
|
167
|
-
message: '
|
|
94
|
+
name: 'integrations',
|
|
95
|
+
message: 'What MyVillageOS capabilities does your app need?',
|
|
168
96
|
choices: [
|
|
169
|
-
{ name: '
|
|
170
|
-
{ name: '
|
|
171
|
-
{ name: 'Audio \u2014 segment labeling, transcription', value: 'audio', checked: false },
|
|
172
|
-
{ name: 'Video \u2014 frame-by-frame annotation', value: 'video', checked: false },
|
|
97
|
+
{ name: 'MCP Server \u2014 AI agent access to villages, posts, events, etc.', value: 'mcp', checked: false },
|
|
98
|
+
{ name: 'REST API \u2014 Direct HTTP calls to MyVillageOS endpoints', value: 'rest-api', checked: false },
|
|
173
99
|
],
|
|
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
100
|
},
|
|
182
101
|
]);
|
|
183
102
|
|
|
103
|
+
const includeMcp = integrations.includes('mcp');
|
|
104
|
+
const includeRestApi = integrations.includes('rest-api');
|
|
105
|
+
|
|
106
|
+
// MCP tool group selection
|
|
107
|
+
let mcpToolGroups = [];
|
|
108
|
+
if (includeMcp) {
|
|
109
|
+
const { toolGroups } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: 'checkbox',
|
|
112
|
+
name: 'toolGroups',
|
|
113
|
+
message: 'Which MCP tool groups should your app use?',
|
|
114
|
+
choices: [
|
|
115
|
+
{ name: 'Social \u2014 Posts, comments, votes, feed', value: 'social', checked: true },
|
|
116
|
+
{ name: 'Communities \u2014 Create, join, events', value: 'communities', checked: true },
|
|
117
|
+
{ name: 'Villages \u2014 Manage villages, members, leaders', value: 'villages', checked: true },
|
|
118
|
+
{ name: 'Moments & Pulse \u2014 Check-ins, engagement tracking', value: 'moments', checked: false },
|
|
119
|
+
{ name: 'Villager Search \u2014 Lookup by name, phone, platform', value: 'villagers', checked: false },
|
|
120
|
+
{ name: 'Meetings \u2014 Recall.ai bot, attendance tracking', value: 'meetings', checked: false },
|
|
121
|
+
],
|
|
122
|
+
validate: input => input.length > 0 ? true : 'Select at least one tool group.',
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
mcpToolGroups = toolGroups;
|
|
126
|
+
}
|
|
127
|
+
|
|
184
128
|
// Create project directory
|
|
185
|
-
const slug =
|
|
129
|
+
const slug = basics.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
186
130
|
const targetDir = resolve(process.cwd(), slug);
|
|
187
131
|
|
|
188
132
|
if (existsSync(targetDir)) {
|
|
@@ -190,14 +134,15 @@ async function dataLabelingFlow() {
|
|
|
190
134
|
return;
|
|
191
135
|
}
|
|
192
136
|
|
|
193
|
-
// OAuth registration (if
|
|
137
|
+
// OAuth registration (if OAuth or both)
|
|
138
|
+
const hasOAuth = authStrategy === 'oauth' || authStrategy === 'both';
|
|
194
139
|
let oauthCredentials = null;
|
|
195
|
-
if (
|
|
140
|
+
if (hasOAuth) {
|
|
196
141
|
const oauthSpinner = villageSpinner('Registering OAuth application...').start();
|
|
197
142
|
try {
|
|
198
143
|
const result = await registerOAuthClient(
|
|
199
|
-
|
|
200
|
-
'
|
|
144
|
+
basics.name,
|
|
145
|
+
'agentic-app',
|
|
201
146
|
['http://localhost:5173/callback']
|
|
202
147
|
);
|
|
203
148
|
oauthCredentials = { clientId: result.clientId, clientSecret: result.clientSecret };
|
|
@@ -209,45 +154,83 @@ async function dataLabelingFlow() {
|
|
|
209
154
|
const { continueWithoutAuth } = await inquirer.prompt([{
|
|
210
155
|
type: 'confirm',
|
|
211
156
|
name: 'continueWithoutAuth',
|
|
212
|
-
message: 'Continue without
|
|
157
|
+
message: 'Continue without OAuth?',
|
|
213
158
|
default: true,
|
|
214
159
|
}]);
|
|
215
160
|
if (!continueWithoutAuth) return;
|
|
216
|
-
answers.includeAuth = false;
|
|
217
161
|
}
|
|
218
162
|
}
|
|
219
163
|
|
|
164
|
+
// Determine if we need the agentic backend layer or just the frontend portal
|
|
165
|
+
const needsAgenticBackend = includeMcp || includeRestApi;
|
|
166
|
+
|
|
220
167
|
// Scaffold project
|
|
221
|
-
const spinner = villageSpinner('Creating
|
|
168
|
+
const spinner = villageSpinner('Creating application...').start();
|
|
222
169
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
170
|
+
if (needsAgenticBackend) {
|
|
171
|
+
// Full-stack: React frontend + Node.js backend with MCP/API integration
|
|
172
|
+
createAgenticAppProject(targetDir, {
|
|
173
|
+
name: basics.name,
|
|
174
|
+
description: basics.description,
|
|
175
|
+
authStrategy: oauthCredentials ? authStrategy : (authStrategy === 'oauth' ? 'none' : authStrategy),
|
|
176
|
+
features,
|
|
177
|
+
includeMcp,
|
|
178
|
+
includeRestApi,
|
|
179
|
+
mcpToolGroups,
|
|
180
|
+
oauthCredentials,
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
// Frontend-only: React + Vite portal (existing template)
|
|
184
|
+
// Map authStrategy to the features array the portal template expects
|
|
185
|
+
const portalFeatures = [...features];
|
|
186
|
+
if (hasOAuth && oauthCredentials) {
|
|
187
|
+
portalFeatures.push('auth');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
createPortalProject(targetDir, {
|
|
191
|
+
name: basics.name,
|
|
192
|
+
description: basics.description,
|
|
193
|
+
features: portalFeatures,
|
|
194
|
+
oauthCredentials,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
230
197
|
|
|
231
198
|
spinner.text = 'Installing dependencies...';
|
|
232
199
|
execSync('npm install', { cwd: targetDir, stdio: 'pipe' });
|
|
233
|
-
spinner.succeed(`
|
|
200
|
+
spinner.succeed(`Application "${basics.name}" created successfully!`);
|
|
234
201
|
|
|
235
202
|
console.log();
|
|
236
|
-
console.log(brand.green(` \u2713
|
|
203
|
+
console.log(brand.green(` \u2713 Application "${basics.name}" created successfully!`));
|
|
237
204
|
console.log();
|
|
238
205
|
console.log(brand.teal(' Next steps:'));
|
|
239
206
|
console.log();
|
|
240
207
|
console.log(` ${brand.gold('cd')} ${slug}`);
|
|
241
|
-
|
|
242
|
-
|
|
208
|
+
if (needsAgenticBackend) {
|
|
209
|
+
console.log(` ${brand.gold('npm run dev')} ${brand.teal('- Start development server (auto-restart)')}`);
|
|
210
|
+
console.log(` ${brand.gold('npm start')} ${brand.teal('- Start production server')}`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(` ${brand.gold('npm run dev')} ${brand.teal('- Start development server')}`);
|
|
213
|
+
console.log(` ${brand.gold('npm run build')} ${brand.teal('- Build for production')}`);
|
|
214
|
+
}
|
|
243
215
|
console.log();
|
|
244
|
-
|
|
216
|
+
|
|
217
|
+
// Summary
|
|
218
|
+
const summaryParts = [];
|
|
219
|
+
summaryParts.push(`Auth: ${authStrategy}`);
|
|
220
|
+
if (features.length > 0) summaryParts.push(`UI: ${features.join(', ')}`);
|
|
221
|
+
if (includeMcp) summaryParts.push(`MCP tools: ${mcpToolGroups.join(', ')}`);
|
|
222
|
+
if (includeRestApi) summaryParts.push('REST API proxy');
|
|
223
|
+
console.log(brand.teal(` ${summaryParts.join(' | ')}`));
|
|
224
|
+
|
|
245
225
|
if (oauthCredentials) {
|
|
246
226
|
console.log(brand.teal(` OAuth Client ID: ${oauthCredentials.clientId}`));
|
|
247
227
|
}
|
|
228
|
+
if (includeMcp) {
|
|
229
|
+
console.log(brand.teal(' Set ANTHROPIC_API_KEY in .env to enable the AI agent'));
|
|
230
|
+
}
|
|
248
231
|
console.log();
|
|
249
232
|
} catch (err) {
|
|
250
|
-
spinner.fail('Failed to create
|
|
233
|
+
spinner.fail('Failed to create application.');
|
|
251
234
|
console.error(chalk.red(` ${err.message}`));
|
|
252
235
|
}
|
|
253
236
|
}
|
package/src/index.js
CHANGED