@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: 'Portal \u2014 Customizable multi-page web app', value: 'portal' },
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 === 'portal') {
38
- await portalFlow();
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 portalFlow() {
49
- const answers = await inquirer.prompt([
43
+ async function applicationFlow() {
44
+ const basics = await inquirer.prompt([
50
45
  {
51
46
  type: 'input',
52
47
  name: 'name',
53
- message: 'Portal name:',
54
- default: 'My Portal',
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 portal built with React and Vite',
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
- // 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
- },
90
+ // Backend integration
91
+ const { integrations } = await inquirer.prompt([
164
92
  {
165
93
  type: 'checkbox',
166
- name: 'dataTypes',
167
- message: 'Select data types to support:',
94
+ name: 'integrations',
95
+ message: 'What MyVillageOS capabilities does your app need?',
168
96
  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 },
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 = answers.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
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 auth selected)
137
+ // OAuth registration (if OAuth or both)
138
+ const hasOAuth = authStrategy === 'oauth' || authStrategy === 'both';
194
139
  let oauthCredentials = null;
195
- if (answers.includeAuth) {
140
+ if (hasOAuth) {
196
141
  const oauthSpinner = villageSpinner('Registering OAuth application...').start();
197
142
  try {
198
143
  const result = await registerOAuthClient(
199
- answers.name,
200
- 'data-labeling',
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 authentication?',
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 data labeling project...').start();
168
+ const spinner = villageSpinner('Creating application...').start();
222
169
  try {
223
- createDataLabelingProject(targetDir, {
224
- name: answers.name,
225
- description: answers.description,
226
- dataTypes: answers.dataTypes,
227
- includeAuth: answers.includeAuth,
228
- oauthCredentials,
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(`Data labeling app "${answers.name}" created successfully!`);
200
+ spinner.succeed(`Application "${basics.name}" created successfully!`);
234
201
 
235
202
  console.log();
236
- console.log(brand.green(` \u2713 Data labeling app "${answers.name}" created successfully!`));
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
- 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')}`);
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
- console.log(brand.teal(` Data types: ${answers.dataTypes.join(', ')}`));
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 data labeling project.');
233
+ spinner.fail('Failed to create application.');
251
234
  console.error(chalk.red(` ${err.message}`));
252
235
  }
253
236
  }
package/src/index.js CHANGED
@@ -104,7 +104,7 @@ export function run() {
104
104
 
105
105
  program
106
106
  .command('create')
107
- .description('Create a new project (portal, data labeling app, or game)')
107
+ .description('Create a new application or game')
108
108
  .action(createCommand);
109
109
 
110
110
  program