@velt-js/mcp-installer 0.1.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,556 @@
1
+ /**
2
+ * Validation Utilities
3
+ *
4
+ * Validates Velt installation with basic and full integration checks.
5
+ * Includes CLI method verification and "use client" directive checks.
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { getCliResolutionInfo } from './cli.js';
11
+ import { isNextJsProject, detectProjectType, ProjectType } from './framework-detection.js';
12
+ import { validateUseClientDirectives, scanAndFixUseClient } from './use-client.js';
13
+
14
+ /**
15
+ * Validates that directory is a valid React project (Next.js or plain React)
16
+ *
17
+ * @param {string} projectPath - Path to project directory
18
+ * @param {Object} [options] - Validation options
19
+ * @param {boolean} [options.requireNextJs=false] - If true, only accepts Next.js projects
20
+ * @returns {Object} Validation result { valid: boolean, error?: string, projectType?: string }
21
+ */
22
+ export function validateProject(projectPath, options = {}) {
23
+ const { requireNextJs = false } = options;
24
+ const packageJsonPath = path.join(projectPath, 'package.json');
25
+
26
+ // Check package.json exists
27
+ if (!fs.existsSync(packageJsonPath)) {
28
+ return {
29
+ valid: false,
30
+ error: `No package.json found at ${projectPath}. Is this a Node.js project?`,
31
+ };
32
+ }
33
+
34
+ // Parse package.json
35
+ let packageJson;
36
+ try {
37
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
38
+ } catch (err) {
39
+ return {
40
+ valid: false,
41
+ error: `Failed to parse package.json: ${err.message}`,
42
+ };
43
+ }
44
+
45
+ const allDeps = {
46
+ ...packageJson.dependencies,
47
+ ...packageJson.devDependencies,
48
+ };
49
+
50
+ // Check for React (required for all)
51
+ const hasReact = !!allDeps.react;
52
+ if (!hasReact) {
53
+ return {
54
+ valid: false,
55
+ error: '"react" not found in package.json dependencies. Velt requires a React project.',
56
+ };
57
+ }
58
+
59
+ // Detect project type
60
+ const hasNext = !!allDeps.next;
61
+ const hasVite = !!allDeps.vite;
62
+ const hasReactScripts = !!allDeps['react-scripts'];
63
+
64
+ let projectType;
65
+ if (hasNext) {
66
+ projectType = 'nextjs';
67
+ } else if (hasVite) {
68
+ projectType = 'vite-react';
69
+ } else if (hasReactScripts) {
70
+ projectType = 'create-react-app';
71
+ } else {
72
+ projectType = 'react-unknown';
73
+ }
74
+
75
+ // If Next.js is required but not found
76
+ if (requireNextJs && !hasNext) {
77
+ return {
78
+ valid: false,
79
+ error: `"next" not found in package.json dependencies. This feature requires a Next.js project. Detected project type: ${projectType}`,
80
+ projectType,
81
+ };
82
+ }
83
+
84
+ return {
85
+ valid: true,
86
+ projectType,
87
+ isNextJs: hasNext,
88
+ framework: projectType,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Validates that directory is a valid Next.js project
94
+ * @deprecated Use validateProject() instead for broader framework support
95
+ *
96
+ * @param {string} projectPath - Path to project directory
97
+ * @returns {Object} Validation result { valid: boolean, error?: string }
98
+ */
99
+ export function validateNextJsProject(projectPath) {
100
+ // For backward compatibility, delegate to validateProject with requireNextJs=false
101
+ // This allows React projects to pass, but validates that it's at least a React project
102
+ const result = validateProject(projectPath, { requireNextJs: false });
103
+
104
+ // Add warning for non-Next.js projects
105
+ if (result.valid && !result.isNextJs) {
106
+ console.error(` ⚠️ Non-Next.js project detected: ${result.projectType}`);
107
+ console.error(` ℹ️ Some features (like "use client" directives) are Next.js-specific`);
108
+ }
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Validates CLI resolution and execution method
115
+ *
116
+ * @returns {Object} CLI validation result
117
+ */
118
+ export function validateCliResolution() {
119
+ const resolution = getCliResolutionInfo();
120
+
121
+ return {
122
+ name: 'CLI Resolution',
123
+ status: resolution.method === 'error' ? 'fail' : 'pass',
124
+ method: resolution.method,
125
+ message: resolution.method === 'error'
126
+ ? `CLI not found: ${resolution.error}`
127
+ : `Using npx @velt-js/add-velt`,
128
+ path: resolution.path,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Basic CLI installation validation (for SKIP/CLI-only path)
134
+ *
135
+ * Only checks CLI scaffolding was successful, NOT integration.
136
+ * Checks: CLI files exist, @veltdev/react in package.json, .env.local
137
+ * Does NOT check: VeltProvider placement, VeltComments integration
138
+ *
139
+ * @param {Object} params
140
+ * @param {string} params.projectPath - Project path
141
+ * @param {Object} [params.cliResult] - CLI execution result (optional, for method reporting)
142
+ * @returns {Promise<Object>} Validation result
143
+ */
144
+ export async function validateBasicCliInstall({ projectPath, cliResult = null }) {
145
+ const checks = [];
146
+ let passed = 0;
147
+
148
+ try {
149
+ // Check 0: CLI Resolution/Execution Method (new)
150
+ if (cliResult && cliResult.method) {
151
+ checks.push({
152
+ name: 'CLI Execution Method',
153
+ status: cliResult.method === 'error' ? 'fail' : 'pass',
154
+ message: cliResult.method === 'npx'
155
+ ? 'Used npx @velt-js/add-velt'
156
+ : `CLI method: ${cliResult.method}`,
157
+ });
158
+
159
+ if (cliResult.method !== 'error') passed++;
160
+ } else {
161
+ // Validate CLI resolution if no result provided
162
+ const cliCheck = validateCliResolution();
163
+ checks.push({
164
+ name: cliCheck.name,
165
+ status: cliCheck.status,
166
+ message: cliCheck.message,
167
+ });
168
+
169
+ if (cliCheck.status === 'pass') passed++;
170
+ }
171
+
172
+ // Check 1: @veltdev/react in package.json
173
+ const packageJsonPath = path.join(projectPath, 'package.json');
174
+ if (fs.existsSync(packageJsonPath)) {
175
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
176
+ const hasVeltPkg = !!(
177
+ packageJson.dependencies?.['@veltdev/react'] ||
178
+ packageJson.devDependencies?.['@veltdev/react']
179
+ );
180
+
181
+ checks.push({
182
+ name: '@veltdev/react package',
183
+ status: hasVeltPkg ? 'pass' : 'fail',
184
+ message: hasVeltPkg
185
+ ? 'Package found in package.json'
186
+ : 'Package not found - run: npm install @veltdev/react',
187
+ });
188
+
189
+ if (hasVeltPkg) passed++;
190
+ } else {
191
+ checks.push({
192
+ name: '@veltdev/react package',
193
+ status: 'fail',
194
+ message: 'package.json not found',
195
+ });
196
+ }
197
+
198
+ // Check 2-4: CLI scaffold files exist
199
+ // Check both standard and src/ paths
200
+ const scaffoldFiles = [
201
+ {
202
+ name: 'VeltInitializeUser.tsx',
203
+ paths: [
204
+ 'components/velt/VeltInitializeUser.tsx',
205
+ 'src/components/velt/VeltInitializeUser.tsx',
206
+ ],
207
+ },
208
+ {
209
+ name: 'VeltInitializeDocument.tsx',
210
+ paths: [
211
+ 'components/velt/VeltInitializeDocument.tsx',
212
+ 'src/components/velt/VeltInitializeDocument.tsx',
213
+ ],
214
+ },
215
+ {
216
+ name: 'VeltCollaboration.tsx',
217
+ paths: [
218
+ 'components/velt/VeltCollaboration.tsx',
219
+ 'src/components/velt/VeltCollaboration.tsx',
220
+ ],
221
+ },
222
+ ];
223
+
224
+ for (const file of scaffoldFiles) {
225
+ let exists = false;
226
+ let foundPath = '';
227
+
228
+ for (const filePath of file.paths) {
229
+ const fullPath = path.join(projectPath, filePath);
230
+ if (fs.existsSync(fullPath)) {
231
+ exists = true;
232
+ foundPath = filePath;
233
+ break;
234
+ }
235
+ }
236
+
237
+ checks.push({
238
+ name: `CLI file: ${file.name}`,
239
+ status: exists ? 'pass' : 'fail',
240
+ message: exists ? `Found at ${foundPath}` : 'File not created by CLI',
241
+ });
242
+
243
+ if (exists) passed++;
244
+ }
245
+
246
+ // Check 5: .env.local exists (or .env)
247
+ const envLocalPath = path.join(projectPath, '.env.local');
248
+ const envPath = path.join(projectPath, '.env');
249
+ const envExists = fs.existsSync(envLocalPath) || fs.existsSync(envPath);
250
+
251
+ checks.push({
252
+ name: 'Environment file',
253
+ status: envExists ? 'pass' : 'warning',
254
+ message: envExists
255
+ ? 'Environment file exists'
256
+ : 'No .env.local or .env found - you may need to create one with NEXT_PUBLIC_VELT_API_KEY',
257
+ });
258
+
259
+ if (envExists) passed++;
260
+
261
+ // Check 6: If .env.local exists, check for API key
262
+ if (fs.existsSync(envLocalPath)) {
263
+ const envContent = fs.readFileSync(envLocalPath, 'utf-8');
264
+ const hasApiKey = envContent.includes('NEXT_PUBLIC_VELT_API_KEY') ||
265
+ envContent.includes('VELT_API_KEY');
266
+
267
+ checks.push({
268
+ name: 'API key in environment',
269
+ status: hasApiKey ? 'pass' : 'warning',
270
+ message: hasApiKey
271
+ ? 'Velt API key found in environment file'
272
+ : 'No Velt API key found in .env.local - add NEXT_PUBLIC_VELT_API_KEY',
273
+ });
274
+
275
+ if (hasApiKey) passed++;
276
+ }
277
+
278
+ // Check 7: "use client" directives (Next.js only)
279
+ const projectType = detectProjectType(projectPath);
280
+ if (projectType.projectType === ProjectType.NEXTJS) {
281
+ const useClientValidation = validateUseClientDirectives(projectPath);
282
+
283
+ for (const check of useClientValidation.checks) {
284
+ checks.push(check);
285
+ if (check.status === 'pass') passed++;
286
+ }
287
+ }
288
+
289
+ return {
290
+ checks,
291
+ passed,
292
+ total: checks.length,
293
+ score: `${passed}/${checks.length}`,
294
+ status: passed === checks.length
295
+ ? 'excellent'
296
+ : passed >= checks.length * 0.6
297
+ ? 'good'
298
+ : 'needs_attention',
299
+ };
300
+ } catch (error) {
301
+ return {
302
+ checks,
303
+ passed,
304
+ total: checks.length,
305
+ score: `${passed}/${checks.length}`,
306
+ error: error.message,
307
+ };
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Full integration validation (for guided path after apply)
313
+ *
314
+ * Checks everything in basic validation PLUS integration:
315
+ * VeltProvider in layout, VeltComments in page, etc.
316
+ *
317
+ * @param {Object} params
318
+ * @param {string} params.projectPath - Project path
319
+ * @param {Object} [params.cliResult] - CLI execution result (optional)
320
+ * @returns {Promise<Object>} Validation result
321
+ */
322
+ export async function validateInstallation({ projectPath, cliResult = null }) {
323
+ const checks = [];
324
+ let passed = 0;
325
+ let total = 0;
326
+
327
+ try {
328
+ // Check 0: CLI Execution Method (new)
329
+ total++;
330
+ if (cliResult && cliResult.method) {
331
+ checks.push({
332
+ name: 'CLI Execution',
333
+ status: cliResult.success ? 'pass' : 'warning',
334
+ message: cliResult.success
335
+ ? `CLI succeeded via ${cliResult.method === 'npx' ? 'npx @velt-js/add-velt' : cliResult.method}`
336
+ : `CLI had issues (${cliResult.method}) but may have created files`,
337
+ });
338
+
339
+ if (cliResult.success || cliResult.method !== 'error') passed++;
340
+ } else {
341
+ const cliCheck = validateCliResolution();
342
+ checks.push({
343
+ name: 'CLI Resolution',
344
+ status: cliCheck.status,
345
+ message: cliCheck.message,
346
+ });
347
+
348
+ if (cliCheck.status === 'pass') passed++;
349
+ }
350
+
351
+ // Check 1: package.json has @veltdev/react
352
+ total++;
353
+ const packageJsonPath = path.join(projectPath, 'package.json');
354
+ if (fs.existsSync(packageJsonPath)) {
355
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
356
+ const hasVelt = !!(
357
+ packageJson.dependencies?.['@veltdev/react'] ||
358
+ packageJson.devDependencies?.['@veltdev/react']
359
+ );
360
+
361
+ checks.push({
362
+ name: 'Velt package installed',
363
+ status: hasVelt ? 'pass' : 'fail',
364
+ message: hasVelt
365
+ ? '@veltdev/react found in package.json'
366
+ : '@veltdev/react not found in package.json',
367
+ });
368
+
369
+ if (hasVelt) passed++;
370
+ } else {
371
+ checks.push({
372
+ name: 'Velt package installed',
373
+ status: 'fail',
374
+ message: 'package.json not found',
375
+ });
376
+ }
377
+
378
+ // Check 2: .env.local has API key
379
+ total++;
380
+ const envPath = path.join(projectPath, '.env.local');
381
+ if (fs.existsSync(envPath)) {
382
+ const envContent = fs.readFileSync(envPath, 'utf-8');
383
+ const hasApiKey = envContent.includes('NEXT_PUBLIC_VELT_API_KEY');
384
+
385
+ checks.push({
386
+ name: 'Environment configured',
387
+ status: hasApiKey ? 'pass' : 'fail',
388
+ message: hasApiKey
389
+ ? 'NEXT_PUBLIC_VELT_API_KEY found in .env.local'
390
+ : 'NEXT_PUBLIC_VELT_API_KEY not found in .env.local',
391
+ });
392
+
393
+ if (hasApiKey) passed++;
394
+ } else {
395
+ checks.push({
396
+ name: 'Environment configured',
397
+ status: 'fail',
398
+ message: '.env.local not found',
399
+ });
400
+ }
401
+
402
+ // Check 3: VeltProvider in layout
403
+ total++;
404
+ const layoutPaths = [
405
+ 'app/layout.tsx',
406
+ 'app/layout.js',
407
+ 'src/app/layout.tsx',
408
+ 'src/app/layout.js',
409
+ ];
410
+
411
+ let layoutFound = false;
412
+ let hasVeltProvider = false;
413
+
414
+ for (const layoutPath of layoutPaths) {
415
+ const fullPath = path.join(projectPath, layoutPath);
416
+ if (fs.existsSync(fullPath)) {
417
+ layoutFound = true;
418
+ const layoutContent = fs.readFileSync(fullPath, 'utf-8');
419
+ hasVeltProvider = layoutContent.includes('VeltProvider');
420
+ break;
421
+ }
422
+ }
423
+
424
+ checks.push({
425
+ name: 'VeltProvider configured',
426
+ status: hasVeltProvider ? 'pass' : 'fail',
427
+ message: layoutFound
428
+ ? hasVeltProvider
429
+ ? 'VeltProvider found in layout'
430
+ : 'VeltProvider not found in layout'
431
+ : 'Layout file not found',
432
+ });
433
+
434
+ if (hasVeltProvider) passed++;
435
+
436
+ // Check 4: VeltComments in page
437
+ total++;
438
+ const pagePaths = [
439
+ 'app/page.tsx',
440
+ 'app/page.js',
441
+ 'src/app/page.tsx',
442
+ 'src/app/page.js',
443
+ ];
444
+
445
+ let pageFound = false;
446
+ let hasVeltComments = false;
447
+
448
+ for (const pagePath of pagePaths) {
449
+ const fullPath = path.join(projectPath, pagePath);
450
+ if (fs.existsSync(fullPath)) {
451
+ pageFound = true;
452
+ const pageContent = fs.readFileSync(fullPath, 'utf-8');
453
+ hasVeltComments = pageContent.includes('VeltComments');
454
+ break;
455
+ }
456
+ }
457
+
458
+ checks.push({
459
+ name: 'VeltComments added',
460
+ status: hasVeltComments ? 'pass' : 'fail',
461
+ message: pageFound
462
+ ? hasVeltComments
463
+ ? 'VeltComments found in page'
464
+ : 'VeltComments not found in page'
465
+ : 'Page file not found',
466
+ });
467
+
468
+ if (hasVeltComments) passed++;
469
+
470
+ // Check 5: VeltCommentsSidebar in layout
471
+ total++;
472
+ let hasVeltSidebar = false;
473
+
474
+ if (layoutFound) {
475
+ for (const layoutPath of layoutPaths) {
476
+ const fullPath = path.join(projectPath, layoutPath);
477
+ if (fs.existsSync(fullPath)) {
478
+ const layoutContent = fs.readFileSync(fullPath, 'utf-8');
479
+ hasVeltSidebar = layoutContent.includes('VeltCommentsSidebar');
480
+ break;
481
+ }
482
+ }
483
+ }
484
+
485
+ checks.push({
486
+ name: 'VeltCommentsSidebar added',
487
+ status: hasVeltSidebar ? 'pass' : 'fail',
488
+ message: layoutFound
489
+ ? hasVeltSidebar
490
+ ? 'VeltCommentsSidebar found in layout'
491
+ : 'VeltCommentsSidebar not found in layout'
492
+ : 'Layout file not found',
493
+ });
494
+
495
+ if (hasVeltSidebar) passed++;
496
+
497
+ // Check 6: "use client" directives (Next.js only)
498
+ const projectType = detectProjectType(projectPath);
499
+ if (projectType.projectType === ProjectType.NEXTJS) {
500
+ const useClientValidation = validateUseClientDirectives(projectPath);
501
+
502
+ for (const check of useClientValidation.checks) {
503
+ total++;
504
+ checks.push(check);
505
+ if (check.status === 'pass') passed++;
506
+ }
507
+ }
508
+
509
+ return {
510
+ checks,
511
+ passed,
512
+ total,
513
+ score: `${passed}/${total}`,
514
+ status: passed === total ? 'excellent' : passed >= total * 0.8 ? 'good' : 'needs_improvement',
515
+ };
516
+ } catch (error) {
517
+ return {
518
+ checks,
519
+ passed,
520
+ total,
521
+ score: `${passed}/${total}`,
522
+ error: error.message,
523
+ };
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Applies "use client" fixes to files that need them (Next.js only)
529
+ *
530
+ * @param {string} projectPath - Project path
531
+ * @returns {Object} Fix results
532
+ */
533
+ export function applyUseClientFixes(projectPath) {
534
+ const projectType = detectProjectType(projectPath);
535
+
536
+ if (projectType.projectType !== ProjectType.NEXTJS) {
537
+ return {
538
+ skipped: true,
539
+ reason: `Project type is ${projectType.projectType}, not Next.js`,
540
+ };
541
+ }
542
+
543
+ return scanAndFixUseClient({
544
+ projectPath,
545
+ fix: true,
546
+ });
547
+ }
548
+
549
+ export default {
550
+ validateProject,
551
+ validateNextJsProject,
552
+ validateCliResolution,
553
+ validateBasicCliInstall,
554
+ validateInstallation,
555
+ applyUseClientFixes,
556
+ };