@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.
- package/README.md +373 -0
- package/bin/mcp-server.js +22 -0
- package/package.json +42 -0
- package/src/index.js +754 -0
- package/src/tools/orchestrator.js +299 -0
- package/src/tools/unified-installer.js +886 -0
- package/src/utils/cli.js +380 -0
- package/src/utils/comment-detector.js +305 -0
- package/src/utils/config.js +149 -0
- package/src/utils/framework-detection.js +262 -0
- package/src/utils/header-positioning.js +146 -0
- package/src/utils/host-app-discovery.js +1000 -0
- package/src/utils/integration.js +803 -0
- package/src/utils/plan-formatter.js +1698 -0
- package/src/utils/screenshot.js +151 -0
- package/src/utils/use-client.js +366 -0
- package/src/utils/validation.js +556 -0
- package/src/utils/velt-docs-fetcher.js +288 -0
- package/src/utils/velt-docs-urls.js +140 -0
- package/src/utils/velt-mcp-client.js +202 -0
- package/src/utils/velt-mcp.js +718 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Velt Integration Script
|
|
3
|
+
*
|
|
4
|
+
* Post-CLI integration and validation tool for Velt Next.js projects.
|
|
5
|
+
* Assumes @velt-js/add-velt CLI has already been run.
|
|
6
|
+
*
|
|
7
|
+
* @module integration
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { applyHeaderPositioning, findVeltSidebarFiles } from './header-positioning.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} IntegrationConfig
|
|
16
|
+
* @property {string} apiKey - Required: Velt API key
|
|
17
|
+
* @property {string} [authToken] - Optional: Velt auth token for JWT
|
|
18
|
+
* @property {boolean} [enableReactFlowCursor] - Optional: Enable ReactFlow cursor integration
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} IntegrationPatterns
|
|
23
|
+
* @property {boolean} [hasReactFlow] - Automatically wire VeltCursor
|
|
24
|
+
* @property {boolean} [hasTiptap] - Add TODO comment for Tiptap integration
|
|
25
|
+
* @property {boolean} [hasCodeMirror] - Add TODO comment for CodeMirror integration
|
|
26
|
+
* @property {boolean} [hasAgGrid] - Add TODO comment for AG-Grid integration
|
|
27
|
+
* @property {boolean} [hasTanStack] - Add TODO comment for TanStack table integration
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} ComponentAdded
|
|
32
|
+
* @property {string} file - File path
|
|
33
|
+
* @property {string} name - Component name
|
|
34
|
+
* @property {string} [description] - Description of component
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} IntegrationPoint
|
|
39
|
+
* @property {string} file - File path
|
|
40
|
+
* @property {string} type - Type of integration point
|
|
41
|
+
* @property {string} description - Description of what was done
|
|
42
|
+
* @property {string} [placeholder] - The placeholder that was replaced
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} ValidationIssue
|
|
47
|
+
* @property {string} [file] - File path where issue occurred
|
|
48
|
+
* @property {string} type - Type of validation issue
|
|
49
|
+
* @property {string} message - Description of the issue
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {Object} IntegrationResult
|
|
54
|
+
* @property {boolean} success - True if no validation issues
|
|
55
|
+
* @property {Object} data - Integration data
|
|
56
|
+
* @property {string[]} data.filesModified - Paths of files that were modified
|
|
57
|
+
* @property {ComponentAdded[]} data.componentsAdded - Components that were added
|
|
58
|
+
* @property {IntegrationPoint[]} data.integrationPoints - Integration points
|
|
59
|
+
* @property {ValidationIssue[]} data.validationIssues - Validation issues found
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} AnalyzeOptions
|
|
64
|
+
* @property {string} projectPath - Project root path
|
|
65
|
+
* @property {IntegrationConfig} config - Integration configuration
|
|
66
|
+
* @property {IntegrationPatterns} [patterns] - Library detection patterns
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Helper Functions
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Detects project structure and returns path helper functions
|
|
75
|
+
*
|
|
76
|
+
* @param {string} projectPath - Project root path
|
|
77
|
+
* @returns {Object} Project structure information
|
|
78
|
+
*/
|
|
79
|
+
function detectProjectStructure(projectPath) {
|
|
80
|
+
const srcAppPath = path.join(projectPath, 'src', 'app');
|
|
81
|
+
const hasSrcApp = fs.existsSync(srcAppPath);
|
|
82
|
+
|
|
83
|
+
const appRoot = hasSrcApp
|
|
84
|
+
? path.join(projectPath, 'src', 'app')
|
|
85
|
+
: path.join(projectPath, 'app');
|
|
86
|
+
|
|
87
|
+
const componentsRoot = hasSrcApp
|
|
88
|
+
? path.join(projectPath, 'src', 'components')
|
|
89
|
+
: path.join(projectPath, 'components');
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
hasSrcApp,
|
|
93
|
+
appRoot,
|
|
94
|
+
componentsRoot,
|
|
95
|
+
appPath: (...segments) => path.join(appRoot, ...segments),
|
|
96
|
+
componentsPath: (...segments) => path.join(componentsRoot, ...segments),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Safely reads a file
|
|
102
|
+
*
|
|
103
|
+
* @param {string} filePath - File path
|
|
104
|
+
* @returns {string} File content
|
|
105
|
+
* @throws {Error} If file cannot be read
|
|
106
|
+
*/
|
|
107
|
+
function readFile(filePath) {
|
|
108
|
+
try {
|
|
109
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Writes file only if content has changed
|
|
117
|
+
*
|
|
118
|
+
* @param {string} filePath - File path
|
|
119
|
+
* @param {string} newContent - New content
|
|
120
|
+
* @param {string[]} filesModified - Array to track modified files
|
|
121
|
+
*/
|
|
122
|
+
function writeFileIfChanged(filePath, newContent, filesModified) {
|
|
123
|
+
try {
|
|
124
|
+
const currentContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
125
|
+
|
|
126
|
+
if (currentContent !== newContent) {
|
|
127
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
128
|
+
|
|
129
|
+
// Track as relative path
|
|
130
|
+
const relativePath = filePath.split('/src/').pop() || filePath.split('/app/').pop() || filePath;
|
|
131
|
+
if (!filesModified.includes(relativePath)) {
|
|
132
|
+
filesModified.push(relativePath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error(`Failed to write file ${filePath}: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Checks if CLI output exists
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} structure - Project structure from detectProjectStructure
|
|
144
|
+
* @param {ValidationIssue[]} validationIssues - Array to collect validation issues
|
|
145
|
+
* @returns {boolean} True if core CLI files exist
|
|
146
|
+
*/
|
|
147
|
+
function ensureCliOutputExists(structure, validationIssues) {
|
|
148
|
+
const { appPath, componentsPath } = structure;
|
|
149
|
+
|
|
150
|
+
const requiredFiles = [
|
|
151
|
+
{ path: appPath('page.tsx'), name: 'app/page.tsx' },
|
|
152
|
+
{ path: appPath('layout.tsx'), name: 'app/layout.tsx' },
|
|
153
|
+
{ path: appPath('userAuth', 'AppProviders.tsx'), name: 'app/userAuth/AppProviders.tsx' },
|
|
154
|
+
{ path: appPath('userAuth', 'SignIn.tsx'), name: 'app/userAuth/SignIn.tsx' },
|
|
155
|
+
{ path: appPath('document', 'page.tsx'), name: 'app/document/page.tsx' },
|
|
156
|
+
{ path: appPath('api', 'velt', 'token', 'route.ts'), name: 'app/api/velt/token/route.ts' },
|
|
157
|
+
{ path: componentsPath('velt', 'VeltCollaboration.tsx'), name: 'components/velt/VeltCollaboration.tsx' },
|
|
158
|
+
{ path: componentsPath('velt', 'ui-customization', 'styles.css'), name: 'components/velt/ui-customization/styles.css' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
let missingCount = 0;
|
|
162
|
+
|
|
163
|
+
for (const file of requiredFiles) {
|
|
164
|
+
if (!fs.existsSync(file.path)) {
|
|
165
|
+
validationIssues.push({
|
|
166
|
+
file: file.name,
|
|
167
|
+
type: 'missing-file',
|
|
168
|
+
message: `Expected file generated by @velt-js/add-velt CLI is missing: ${file.name}`,
|
|
169
|
+
});
|
|
170
|
+
missingCount++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If most files are missing, CLI likely wasn't run
|
|
175
|
+
if (missingCount >= requiredFiles.length / 2) {
|
|
176
|
+
validationIssues.push({
|
|
177
|
+
type: 'cli-not-run',
|
|
178
|
+
message: 'CLI output not found. Please run `npx @velt-js/add-velt` first before running integration.',
|
|
179
|
+
});
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Replaces API key placeholder in page.tsx
|
|
188
|
+
*
|
|
189
|
+
* @param {Object} structure - Project structure
|
|
190
|
+
* @param {string} apiKey - API key to insert
|
|
191
|
+
* @param {string[]} filesModified - Array to track modified files
|
|
192
|
+
* @param {IntegrationPoint[]} integrationPoints - Array to track integration points
|
|
193
|
+
* @param {ValidationIssue[]} validationIssues - Array to track validation issues
|
|
194
|
+
*/
|
|
195
|
+
function replaceApiKeyInPage(structure, apiKey, filesModified, integrationPoints, validationIssues) {
|
|
196
|
+
const { appPath } = structure;
|
|
197
|
+
const pagePath = appPath('page.tsx');
|
|
198
|
+
|
|
199
|
+
if (!fs.existsSync(pagePath)) {
|
|
200
|
+
validationIssues.push({
|
|
201
|
+
file: 'app/page.tsx',
|
|
202
|
+
type: 'missing-file',
|
|
203
|
+
message: 'Cannot replace API key: app/page.tsx does not exist',
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
let content = readFile(pagePath);
|
|
210
|
+
const originalContent = content;
|
|
211
|
+
|
|
212
|
+
// Replace API key placeholder
|
|
213
|
+
const placeholder = '"YOUR_VELT_API_KEY"';
|
|
214
|
+
if (content.includes(placeholder)) {
|
|
215
|
+
content = content.replace(new RegExp(placeholder, 'g'), `"${apiKey}"`);
|
|
216
|
+
|
|
217
|
+
writeFileIfChanged(pagePath, content, filesModified);
|
|
218
|
+
|
|
219
|
+
integrationPoints.push({
|
|
220
|
+
file: 'app/page.tsx',
|
|
221
|
+
type: 'apiKeyReplacement',
|
|
222
|
+
description: 'Replaced YOUR_VELT_API_KEY placeholder with provided Velt API key.',
|
|
223
|
+
placeholder: 'YOUR_VELT_API_KEY',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate that placeholder is gone
|
|
228
|
+
if (content.includes(placeholder)) {
|
|
229
|
+
validationIssues.push({
|
|
230
|
+
file: 'app/page.tsx',
|
|
231
|
+
type: 'placeholder-remaining',
|
|
232
|
+
message: 'YOUR_VELT_API_KEY placeholder still exists after replacement attempt',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
validationIssues.push({
|
|
237
|
+
file: 'app/page.tsx',
|
|
238
|
+
type: 'error',
|
|
239
|
+
message: `Error replacing API key: ${error.message}`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Replaces auth token placeholder in JWT route
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} structure - Project structure
|
|
248
|
+
* @param {string} apiKey - API key to insert
|
|
249
|
+
* @param {string} authToken - Auth token to insert
|
|
250
|
+
* @param {string[]} filesModified - Array to track modified files
|
|
251
|
+
* @param {IntegrationPoint[]} integrationPoints - Array to track integration points
|
|
252
|
+
* @param {ValidationIssue[]} validationIssues - Array to track validation issues
|
|
253
|
+
*/
|
|
254
|
+
function replaceAuthTokenInRoute(structure, apiKey, authToken, filesModified, integrationPoints, validationIssues) {
|
|
255
|
+
const { appPath } = structure;
|
|
256
|
+
const routePath = appPath('api', 'velt', 'token', 'route.ts');
|
|
257
|
+
|
|
258
|
+
if (!fs.existsSync(routePath)) {
|
|
259
|
+
validationIssues.push({
|
|
260
|
+
file: 'app/api/velt/token/route.ts',
|
|
261
|
+
type: 'missing-file',
|
|
262
|
+
message: 'Cannot replace auth token: app/api/velt/token/route.ts does not exist',
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
let content = readFile(routePath);
|
|
269
|
+
let modified = false;
|
|
270
|
+
|
|
271
|
+
// Replace API key if present
|
|
272
|
+
const apiKeyPlaceholder = '"YOUR_VELT_API_KEY"';
|
|
273
|
+
if (content.includes(apiKeyPlaceholder)) {
|
|
274
|
+
content = content.replace(new RegExp(apiKeyPlaceholder, 'g'), `"${apiKey}"`);
|
|
275
|
+
modified = true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Replace auth token if provided
|
|
279
|
+
const authTokenPlaceholder = '"YOUR_VELT_AUTH_TOKEN"';
|
|
280
|
+
if (authToken && content.includes(authTokenPlaceholder)) {
|
|
281
|
+
content = content.replace(new RegExp(authTokenPlaceholder, 'g'), `"${authToken}"`);
|
|
282
|
+
modified = true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (modified) {
|
|
286
|
+
writeFileIfChanged(routePath, content, filesModified);
|
|
287
|
+
|
|
288
|
+
integrationPoints.push({
|
|
289
|
+
file: 'app/api/velt/token/route.ts',
|
|
290
|
+
type: 'authTokenReplacement',
|
|
291
|
+
description: 'Replaced YOUR_VELT_API_KEY and YOUR_VELT_AUTH_TOKEN placeholders in Velt token route.',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Validate that placeholders are gone if they should be
|
|
296
|
+
if (content.includes(apiKeyPlaceholder)) {
|
|
297
|
+
validationIssues.push({
|
|
298
|
+
file: 'app/api/velt/token/route.ts',
|
|
299
|
+
type: 'placeholder-remaining',
|
|
300
|
+
message: 'YOUR_VELT_API_KEY placeholder still exists in token route',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (authToken && content.includes(authTokenPlaceholder)) {
|
|
305
|
+
validationIssues.push({
|
|
306
|
+
file: 'app/api/velt/token/route.ts',
|
|
307
|
+
type: 'placeholder-remaining',
|
|
308
|
+
message: 'YOUR_VELT_AUTH_TOKEN placeholder still exists in token route',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
validationIssues.push({
|
|
313
|
+
file: 'app/api/velt/token/route.ts',
|
|
314
|
+
type: 'error',
|
|
315
|
+
message: `Error replacing auth token: ${error.message}`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Wires ReactFlow cursor integration
|
|
322
|
+
*
|
|
323
|
+
* @param {Object} structure - Project structure
|
|
324
|
+
* @param {string[]} filesModified - Array to track modified files
|
|
325
|
+
* @param {ComponentAdded[]} componentsAdded - Array to track components added
|
|
326
|
+
* @param {IntegrationPoint[]} integrationPoints - Array to track integration points
|
|
327
|
+
* @param {ValidationIssue[]} validationIssues - Array to track validation issues
|
|
328
|
+
*/
|
|
329
|
+
function wireReactFlowCursor(structure, filesModified, componentsAdded, integrationPoints, validationIssues) {
|
|
330
|
+
const { componentsPath } = structure;
|
|
331
|
+
const collaborationPath = componentsPath('velt', 'VeltCollaboration.tsx');
|
|
332
|
+
|
|
333
|
+
if (!fs.existsSync(collaborationPath)) {
|
|
334
|
+
validationIssues.push({
|
|
335
|
+
file: 'components/velt/VeltCollaboration.tsx',
|
|
336
|
+
type: 'missing-file',
|
|
337
|
+
message: 'Cannot wire ReactFlow cursor: VeltCollaboration.tsx does not exist',
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
let content = readFile(collaborationPath);
|
|
344
|
+
|
|
345
|
+
// Check if VeltCursor already exists
|
|
346
|
+
if (content.includes('VeltCursor')) {
|
|
347
|
+
console.error(' ✓ VeltCursor already present');
|
|
348
|
+
return; // Already wired
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.error(' ✓ Adding VeltCursor component (ReactFlow integration)');
|
|
352
|
+
|
|
353
|
+
// Add import if not present
|
|
354
|
+
if (!content.includes("import { VeltCursor }")) {
|
|
355
|
+
const importStatement = 'import { VeltCursor } from "@veltdev/react";';
|
|
356
|
+
|
|
357
|
+
// Find last import and add after it
|
|
358
|
+
const importRegex = /^import\s+.*?from\s+['"].*?['"];?\s*$/gm;
|
|
359
|
+
const imports = content.match(importRegex) || [];
|
|
360
|
+
|
|
361
|
+
if (imports.length > 0) {
|
|
362
|
+
const lastImport = imports[imports.length - 1];
|
|
363
|
+
const lastImportIndex = content.lastIndexOf(lastImport);
|
|
364
|
+
const afterLastImport = lastImportIndex + lastImport.length;
|
|
365
|
+
content = content.slice(0, afterLastImport) + '\n' + importStatement + content.slice(afterLastImport);
|
|
366
|
+
} else {
|
|
367
|
+
// Add at top of file
|
|
368
|
+
content = importStatement + '\n' + content;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Find a good place to add VeltCursor - look for a return statement with a div/main
|
|
373
|
+
// Simple approach: add before the last closing tag in the return statement
|
|
374
|
+
const returnMatch = content.match(/return\s*\(([\s\S]*?)\);?\s*}[\s]*$/m);
|
|
375
|
+
|
|
376
|
+
if (returnMatch) {
|
|
377
|
+
const returnContent = returnMatch[1];
|
|
378
|
+
const lastClosingTag = returnContent.lastIndexOf('</');
|
|
379
|
+
|
|
380
|
+
if (lastClosingTag > 0) {
|
|
381
|
+
const beforeClosing = returnContent.slice(0, lastClosingTag);
|
|
382
|
+
const afterClosing = returnContent.slice(lastClosingTag);
|
|
383
|
+
const cursorComponent = ' <VeltCursor />\n';
|
|
384
|
+
const newReturnContent = beforeClosing + cursorComponent + afterClosing;
|
|
385
|
+
content = content.replace(returnMatch[0], `return (\n${newReturnContent}\n );\n}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
writeFileIfChanged(collaborationPath, content, filesModified);
|
|
390
|
+
|
|
391
|
+
componentsAdded.push({
|
|
392
|
+
file: 'components/velt/VeltCollaboration.tsx',
|
|
393
|
+
name: 'VeltCursor',
|
|
394
|
+
description: 'Automatically wired ReactFlow cursor into VeltCollaboration.',
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
integrationPoints.push({
|
|
398
|
+
file: 'components/velt/VeltCollaboration.tsx',
|
|
399
|
+
type: 'reactflowCursor',
|
|
400
|
+
description: 'Injected Velt cursor component into ReactFlow collaboration canvas because patterns.hasReactFlow is true.',
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
validationIssues.push({
|
|
404
|
+
file: 'components/velt/VeltCollaboration.tsx',
|
|
405
|
+
type: 'error',
|
|
406
|
+
message: `Error wiring ReactFlow cursor: ${error.message}`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Adds TODO comments for library integrations
|
|
413
|
+
*
|
|
414
|
+
* @param {Object} structure - Project structure
|
|
415
|
+
* @param {IntegrationPatterns} patterns - Library patterns
|
|
416
|
+
* @param {string[]} filesModified - Array to track modified files
|
|
417
|
+
* @param {IntegrationPoint[]} integrationPoints - Array to track integration points
|
|
418
|
+
* @param {ValidationIssue[]} validationIssues - Array to track validation issues
|
|
419
|
+
*/
|
|
420
|
+
function addLibraryTodoComments(structure, patterns, filesModified, integrationPoints, validationIssues) {
|
|
421
|
+
const { componentsPath } = structure;
|
|
422
|
+
const collaborationPath = componentsPath('velt', 'VeltCollaboration.tsx');
|
|
423
|
+
|
|
424
|
+
if (!fs.existsSync(collaborationPath)) {
|
|
425
|
+
return; // Already reported in ensureCliOutputExists
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
let content = readFile(collaborationPath);
|
|
430
|
+
let modified = false;
|
|
431
|
+
|
|
432
|
+
const todos = [];
|
|
433
|
+
|
|
434
|
+
if (patterns.hasTiptap && !content.includes('[Velt] TODO: Tiptap')) {
|
|
435
|
+
todos.push({
|
|
436
|
+
library: 'Tiptap',
|
|
437
|
+
comment: '// [Velt] TODO: Wire Tiptap comments/collaboration here. See docs at https://docs.velt.dev/text-editor/tiptap/setup',
|
|
438
|
+
pattern: 'hasTiptap',
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (patterns.hasCodeMirror && !content.includes('[Velt] TODO: CodeMirror')) {
|
|
443
|
+
todos.push({
|
|
444
|
+
library: 'CodeMirror',
|
|
445
|
+
comment: '// [Velt] TODO: Wire CodeMirror CRDT integration here. See docs at https://docs.velt.dev/async-collaboration/setup/codemirror',
|
|
446
|
+
pattern: 'hasCodeMirror',
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (patterns.hasAgGrid && !content.includes('[Velt] TODO: AG-Grid')) {
|
|
451
|
+
todos.push({
|
|
452
|
+
library: 'AG-Grid',
|
|
453
|
+
comment: '// [Velt] TODO: Wire AG-Grid table comments here. Add data-velt-target-comment-element-id to cells. See docs at https://docs.velt.dev/async-collaboration/comments/customize-behavior/target-element',
|
|
454
|
+
pattern: 'hasAgGrid',
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (patterns.hasTanStack && !content.includes('[Velt] TODO: TanStack')) {
|
|
459
|
+
todos.push({
|
|
460
|
+
library: 'TanStack',
|
|
461
|
+
comment: '// [Velt] TODO: Wire TanStack table comments here. Add data-velt-target-comment-element-id to cells. See docs at https://docs.velt.dev/async-collaboration/comments/customize-behavior/target-element',
|
|
462
|
+
pattern: 'hasTanStack',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (todos.length > 0) {
|
|
467
|
+
// Find a good place to add TODOs - after imports, before the component export
|
|
468
|
+
const exportMatch = content.match(/export\s+(default\s+)?function/);
|
|
469
|
+
|
|
470
|
+
if (exportMatch) {
|
|
471
|
+
const exportIndex = exportMatch.index;
|
|
472
|
+
const todosText = '\n' + todos.map(t => t.comment).join('\n') + '\n';
|
|
473
|
+
content = content.slice(0, exportIndex) + todosText + content.slice(exportIndex);
|
|
474
|
+
modified = true;
|
|
475
|
+
|
|
476
|
+
// Track each TODO
|
|
477
|
+
for (const todo of todos) {
|
|
478
|
+
integrationPoints.push({
|
|
479
|
+
file: 'components/velt/VeltCollaboration.tsx',
|
|
480
|
+
type: 'todoComment',
|
|
481
|
+
description: `Added TODO comment for ${todo.library} integration (patterns.${todo.pattern} detected).`,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (modified) {
|
|
488
|
+
writeFileIfChanged(collaborationPath, content, filesModified);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
validationIssues.push({
|
|
492
|
+
file: 'components/velt/VeltCollaboration.tsx',
|
|
493
|
+
type: 'error',
|
|
494
|
+
message: `Error adding library TODO comments: ${error.message}`,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Runs validation checks
|
|
501
|
+
*
|
|
502
|
+
* @param {Object} structure - Project structure
|
|
503
|
+
* @param {IntegrationConfig} config - Integration config
|
|
504
|
+
* @param {ValidationIssue[]} validationIssues - Array to collect validation issues
|
|
505
|
+
*/
|
|
506
|
+
function runValidationChecks(structure, config, validationIssues) {
|
|
507
|
+
const { appPath, componentsPath } = structure;
|
|
508
|
+
|
|
509
|
+
// Validate app/page.tsx
|
|
510
|
+
const pagePath = appPath('page.tsx');
|
|
511
|
+
if (fs.existsSync(pagePath)) {
|
|
512
|
+
try {
|
|
513
|
+
const pageContent = readFile(pagePath);
|
|
514
|
+
|
|
515
|
+
// Check for "use client" directive
|
|
516
|
+
if (!pageContent.includes('"use client"') && !pageContent.includes("'use client'")) {
|
|
517
|
+
validationIssues.push({
|
|
518
|
+
file: 'app/page.tsx',
|
|
519
|
+
type: 'validation-failed',
|
|
520
|
+
message: 'Missing "use client" directive at top of file',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Check for VeltProvider
|
|
525
|
+
if (!pageContent.includes('VeltProvider')) {
|
|
526
|
+
validationIssues.push({
|
|
527
|
+
file: 'app/page.tsx',
|
|
528
|
+
type: 'validation-failed',
|
|
529
|
+
message: 'VeltProvider not found in page.tsx. Expected CLI to generate this.',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Check that API key placeholder is gone
|
|
534
|
+
if (pageContent.includes('"YOUR_VELT_API_KEY"')) {
|
|
535
|
+
validationIssues.push({
|
|
536
|
+
file: 'app/page.tsx',
|
|
537
|
+
type: 'validation-failed',
|
|
538
|
+
message: 'YOUR_VELT_API_KEY placeholder still exists. API key replacement may have failed.',
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
validationIssues.push({
|
|
543
|
+
file: 'app/page.tsx',
|
|
544
|
+
type: 'error',
|
|
545
|
+
message: `Error validating page.tsx: ${error.message}`,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Validate app/layout.tsx
|
|
551
|
+
const layoutPath = appPath('layout.tsx');
|
|
552
|
+
if (fs.existsSync(layoutPath)) {
|
|
553
|
+
try {
|
|
554
|
+
const layoutContent = readFile(layoutPath);
|
|
555
|
+
|
|
556
|
+
// Check for AppProviders
|
|
557
|
+
if (!layoutContent.includes('AppProviders')) {
|
|
558
|
+
validationIssues.push({
|
|
559
|
+
file: 'app/layout.tsx',
|
|
560
|
+
type: 'validation-failed',
|
|
561
|
+
message: 'AppProviders not found in layout.tsx. Expected CLI to generate this.',
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Check that AppProviders wraps {children}
|
|
566
|
+
if (layoutContent.includes('AppProviders') && !layoutContent.includes('{children}')) {
|
|
567
|
+
validationIssues.push({
|
|
568
|
+
file: 'app/layout.tsx',
|
|
569
|
+
type: 'validation-failed',
|
|
570
|
+
message: 'AppProviders exists but {children} not found. Layout structure may be incorrect.',
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
validationIssues.push({
|
|
575
|
+
file: 'app/layout.tsx',
|
|
576
|
+
type: 'error',
|
|
577
|
+
message: `Error validating layout.tsx: ${error.message}`,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Validate JWT route if auth token was provided
|
|
583
|
+
const tokenRoutePath = appPath('api', 'velt', 'token', 'route.ts');
|
|
584
|
+
if (config.authToken && fs.existsSync(tokenRoutePath)) {
|
|
585
|
+
try {
|
|
586
|
+
const routeContent = readFile(tokenRoutePath);
|
|
587
|
+
|
|
588
|
+
// Check that auth token placeholder is gone
|
|
589
|
+
if (routeContent.includes('"YOUR_VELT_AUTH_TOKEN"')) {
|
|
590
|
+
validationIssues.push({
|
|
591
|
+
file: 'app/api/velt/token/route.ts',
|
|
592
|
+
type: 'validation-failed',
|
|
593
|
+
message: 'YOUR_VELT_AUTH_TOKEN placeholder still exists. Auth token replacement may have failed.',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
validationIssues.push({
|
|
598
|
+
file: 'app/api/velt/token/route.ts',
|
|
599
|
+
type: 'error',
|
|
600
|
+
message: `Error validating token route: ${error.message}`,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Validate core CLI-generated files exist (already done in ensureCliOutputExists, but double-check critical ones)
|
|
606
|
+
const criticalFiles = [
|
|
607
|
+
{ path: appPath('userAuth', 'AppProviders.tsx'), name: 'app/userAuth/AppProviders.tsx' },
|
|
608
|
+
{ path: appPath('document', 'page.tsx'), name: 'app/document/page.tsx' },
|
|
609
|
+
{ path: componentsPath('velt', 'VeltCollaboration.tsx'), name: 'components/velt/VeltCollaboration.tsx' },
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
for (const file of criticalFiles) {
|
|
613
|
+
if (!fs.existsSync(file.path)) {
|
|
614
|
+
// Only add if not already added
|
|
615
|
+
const alreadyReported = validationIssues.some(
|
|
616
|
+
issue => issue.file === file.name && issue.type === 'missing-file'
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
if (!alreadyReported) {
|
|
620
|
+
validationIssues.push({
|
|
621
|
+
file: file.name,
|
|
622
|
+
type: 'validation-failed',
|
|
623
|
+
message: `Critical CLI-generated file is missing: ${file.name}`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ============================================================================
|
|
631
|
+
// Main Integration Function
|
|
632
|
+
// ============================================================================
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Analyzes project and integrates Velt components
|
|
636
|
+
*
|
|
637
|
+
* @param {AnalyzeOptions} options - Integration options
|
|
638
|
+
* @returns {Promise<IntegrationResult>} Integration result
|
|
639
|
+
*/
|
|
640
|
+
export async function analyzeAndIntegrate(options) {
|
|
641
|
+
const { projectPath, config, patterns = {} } = options;
|
|
642
|
+
|
|
643
|
+
// Initialize result
|
|
644
|
+
const filesModified = [];
|
|
645
|
+
const componentsAdded = [];
|
|
646
|
+
const integrationPoints = [];
|
|
647
|
+
const validationIssues = [];
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
// Step 1: Verify this is a Next.js project
|
|
651
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
652
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
653
|
+
validationIssues.push({
|
|
654
|
+
type: 'not-nextjs',
|
|
655
|
+
message: 'package.json not found. This does not appear to be a valid Next.js project.',
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
success: false,
|
|
660
|
+
data: { filesModified, componentsAdded, integrationPoints, validationIssues },
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
666
|
+
const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next;
|
|
667
|
+
|
|
668
|
+
if (!hasNext) {
|
|
669
|
+
validationIssues.push({
|
|
670
|
+
type: 'not-nextjs',
|
|
671
|
+
message: 'Next.js dependency not found in package.json. This does not appear to be a Next.js project.',
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
data: { filesModified, componentsAdded, integrationPoints, validationIssues },
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
} catch (error) {
|
|
680
|
+
validationIssues.push({
|
|
681
|
+
type: 'error',
|
|
682
|
+
message: `Error reading package.json: ${error.message}`,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
success: false,
|
|
687
|
+
data: { filesModified, componentsAdded, integrationPoints, validationIssues },
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Step 2: Detect project structure
|
|
692
|
+
const structure = detectProjectStructure(projectPath);
|
|
693
|
+
|
|
694
|
+
// Step 3: Ensure CLI output exists
|
|
695
|
+
const cliOutputExists = ensureCliOutputExists(structure, validationIssues);
|
|
696
|
+
|
|
697
|
+
if (!cliOutputExists) {
|
|
698
|
+
// CLI likely not run - return early
|
|
699
|
+
return {
|
|
700
|
+
success: false,
|
|
701
|
+
data: { filesModified, componentsAdded, integrationPoints, validationIssues },
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Step 4: Replace API key in page.tsx
|
|
706
|
+
console.error(' 🔑 Replacing API key placeholders...');
|
|
707
|
+
replaceApiKeyInPage(structure, config.apiKey, filesModified, integrationPoints, validationIssues);
|
|
708
|
+
|
|
709
|
+
// Step 5: Replace auth token in JWT route if provided
|
|
710
|
+
if (config.authToken) {
|
|
711
|
+
console.error(' 🔐 Replacing auth token placeholders...');
|
|
712
|
+
replaceAuthTokenInRoute(structure, config.apiKey, config.authToken, filesModified, integrationPoints, validationIssues);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Step 6: Wire library-specific integrations
|
|
716
|
+
|
|
717
|
+
// ReactFlow cursor (automatic wiring)
|
|
718
|
+
if (patterns.hasReactFlow) {
|
|
719
|
+
console.error(' 🔌 Wiring ReactFlow cursor integration (from detected library)...');
|
|
720
|
+
wireReactFlowCursor(structure, filesModified, componentsAdded, integrationPoints, validationIssues);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Other libraries (TODO comments)
|
|
724
|
+
const librariesToWire = [];
|
|
725
|
+
if (patterns.hasTiptap) librariesToWire.push('Tiptap');
|
|
726
|
+
if (patterns.hasCodeMirror) librariesToWire.push('CodeMirror');
|
|
727
|
+
if (patterns.hasAgGrid) librariesToWire.push('AG-Grid');
|
|
728
|
+
if (patterns.hasTanStack) librariesToWire.push('TanStack');
|
|
729
|
+
|
|
730
|
+
if (librariesToWire.length > 0) {
|
|
731
|
+
console.error(` 📝 Adding TODO comments for: ${librariesToWire.join(', ')}`);
|
|
732
|
+
}
|
|
733
|
+
addLibraryTodoComments(structure, patterns, filesModified, integrationPoints, validationIssues);
|
|
734
|
+
|
|
735
|
+
// Step 7: Apply header positioning if specified
|
|
736
|
+
if (config.headerPosition) {
|
|
737
|
+
console.error(` 📍 Applying header positioning: ${config.headerPosition}...`);
|
|
738
|
+
const sidebarFiles = findVeltSidebarFiles(projectPath);
|
|
739
|
+
|
|
740
|
+
for (const filePath of sidebarFiles) {
|
|
741
|
+
applyHeaderPositioning(filePath, config.headerPosition, filesModified, integrationPoints);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Step 8: Handle comment-type specific integration
|
|
746
|
+
if (config.commentType && config.targetPlacement) {
|
|
747
|
+
console.error(` 💬 Applying ${config.commentType} comment integration...`);
|
|
748
|
+
|
|
749
|
+
if (config.commentType === 'popover' && config.targetPlacement.file) {
|
|
750
|
+
// Add popover-specific integration
|
|
751
|
+
integrationPoints.push({
|
|
752
|
+
file: config.targetPlacement.file,
|
|
753
|
+
type: 'popover-target',
|
|
754
|
+
description: `Recommended file for popover comments: ${config.targetPlacement.file}`,
|
|
755
|
+
implementationGuide: config.targetPlacement.implementationGuide,
|
|
756
|
+
});
|
|
757
|
+
} else if (config.commentType === 'freestyle' && config.targetPlacement.file) {
|
|
758
|
+
// Add freestyle-specific integration
|
|
759
|
+
integrationPoints.push({
|
|
760
|
+
file: config.targetPlacement.file,
|
|
761
|
+
type: 'freestyle-target',
|
|
762
|
+
description: `Recommended file for freestyle comments: ${config.targetPlacement.file}`,
|
|
763
|
+
implementationGuide: config.targetPlacement.implementationGuide,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Step 9: Run validation checks
|
|
769
|
+
runValidationChecks(structure, config, validationIssues);
|
|
770
|
+
|
|
771
|
+
// Step 10: Return result
|
|
772
|
+
const success = validationIssues.length === 0;
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
success,
|
|
776
|
+
data: {
|
|
777
|
+
filesModified,
|
|
778
|
+
componentsAdded,
|
|
779
|
+
integrationPoints,
|
|
780
|
+
validationIssues,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
} catch (error) {
|
|
784
|
+
// Unexpected error
|
|
785
|
+
validationIssues.push({
|
|
786
|
+
type: 'error',
|
|
787
|
+
message: `Unexpected error during integration: ${error.message}`,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
data: {
|
|
793
|
+
filesModified,
|
|
794
|
+
componentsAdded,
|
|
795
|
+
integrationPoints,
|
|
796
|
+
validationIssues,
|
|
797
|
+
},
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Export as both named and default for compatibility
|
|
803
|
+
export default { analyzeAndIntegrate };
|