@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
package/src/utils/cli.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Velt CLI Execution Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles running the Velt CLI via npx @velt-js/add-velt.
|
|
5
|
+
* This module spawns npx to execute the published npm package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps MCP feature selections to CLI flags
|
|
12
|
+
*
|
|
13
|
+
* CLI Flags supported by @velt-js/add-velt:
|
|
14
|
+
* --presence Add presence (VeltPresence - shows online users)
|
|
15
|
+
* --cursors Add cursors (VeltCursor - shows live cursor positions)
|
|
16
|
+
* --comments Add comments (VeltComments, VeltCommentsSidebar)
|
|
17
|
+
* --notifications Add notifications (VeltNotificationsTool)
|
|
18
|
+
* --reactflow-crdt Add ReactFlow CRDT
|
|
19
|
+
* --tiptap-crdt Add Tiptap CRDT
|
|
20
|
+
* --codemirror-crdt Add CodeMirror CRDT
|
|
21
|
+
* --all Enable presence + cursors + comments + notifications + CRDT (REQUIRES a CRDT flag!)
|
|
22
|
+
* --force, -f Force overwrite existing files
|
|
23
|
+
* --legacy-peer-deps Use legacy peer deps (npm only)
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} params
|
|
26
|
+
* @param {string[]} [params.features=[]] - Features to install: 'presence', 'cursors', 'comments', 'notifications', 'crdt'
|
|
27
|
+
* @param {string} [params.crdtType=null] - CRDT type: 'tiptap', 'codemirror', 'reactflow'
|
|
28
|
+
* @param {boolean} [params.force=false] - Force overwrite files
|
|
29
|
+
* @param {boolean} [params.legacyPeerDeps=false] - Use legacy peer deps
|
|
30
|
+
* @returns {string[]} Array of CLI flags
|
|
31
|
+
*/
|
|
32
|
+
export function mapFeaturesToCliFlags({
|
|
33
|
+
features = [],
|
|
34
|
+
crdtType = null,
|
|
35
|
+
force = false,
|
|
36
|
+
legacyPeerDeps = false,
|
|
37
|
+
}) {
|
|
38
|
+
const flags = [];
|
|
39
|
+
|
|
40
|
+
// Normalize features to lowercase
|
|
41
|
+
const normalizedFeatures = features.map(f => f.toLowerCase());
|
|
42
|
+
|
|
43
|
+
const hasPresence = normalizedFeatures.includes('presence');
|
|
44
|
+
const hasCursors = normalizedFeatures.includes('cursors');
|
|
45
|
+
const hasComments = normalizedFeatures.includes('comments');
|
|
46
|
+
const hasNotifications = normalizedFeatures.includes('notifications');
|
|
47
|
+
const hasCrdt = normalizedFeatures.includes('crdt') && crdtType;
|
|
48
|
+
|
|
49
|
+
// Validate CRDT type if provided
|
|
50
|
+
// Recognized types (accepted by MCP): includes blocknote which is handled via docs, not CLI flag
|
|
51
|
+
const recognizedCrdtTypes = ['tiptap', 'codemirror', 'reactflow', 'blocknote'];
|
|
52
|
+
// CLI-supported types (have a corresponding --X-crdt flag)
|
|
53
|
+
const cliFlagCrdtTypes = ['tiptap', 'codemirror', 'reactflow'];
|
|
54
|
+
const isCrdtRecognized = hasCrdt && recognizedCrdtTypes.includes(crdtType.toLowerCase());
|
|
55
|
+
const isCrdtCliSupported = hasCrdt && cliFlagCrdtTypes.includes(crdtType.toLowerCase());
|
|
56
|
+
if (hasCrdt && !isCrdtRecognized) {
|
|
57
|
+
console.error(` ⚠️ Unknown CRDT type "${crdtType}", skipping CRDT flag`);
|
|
58
|
+
} else if (hasCrdt && !isCrdtCliSupported) {
|
|
59
|
+
console.error(` ℹ️ CRDT type "${crdtType}" is handled via docs/plan, no CLI flag generated`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Determine flag strategy
|
|
63
|
+
// --all requires a CLI-supported CRDT flag, so only use it when we have all features with a supported CRDT type
|
|
64
|
+
const useAllFlag = hasPresence && hasCursors && hasComments && hasNotifications && isCrdtCliSupported;
|
|
65
|
+
|
|
66
|
+
if (useAllFlag) {
|
|
67
|
+
// Use --all with CRDT type
|
|
68
|
+
flags.push('--all');
|
|
69
|
+
flags.push(`--${crdtType.toLowerCase()}-crdt`);
|
|
70
|
+
} else {
|
|
71
|
+
// Build individual flags
|
|
72
|
+
if (hasPresence) {
|
|
73
|
+
flags.push('--presence');
|
|
74
|
+
}
|
|
75
|
+
if (hasCursors) {
|
|
76
|
+
flags.push('--cursors');
|
|
77
|
+
}
|
|
78
|
+
if (hasComments) {
|
|
79
|
+
flags.push('--comments');
|
|
80
|
+
}
|
|
81
|
+
if (hasNotifications) {
|
|
82
|
+
flags.push('--notifications');
|
|
83
|
+
}
|
|
84
|
+
if (isCrdtCliSupported) {
|
|
85
|
+
flags.push(`--${crdtType.toLowerCase()}-crdt`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add installation flags
|
|
90
|
+
if (force) {
|
|
91
|
+
flags.push('--force');
|
|
92
|
+
}
|
|
93
|
+
if (legacyPeerDeps) {
|
|
94
|
+
flags.push('--legacy-peer-deps');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return flags;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Runs the Velt CLI via npx @velt-js/add-velt
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} params
|
|
104
|
+
* @param {string} params.installDir - Directory to install Velt in
|
|
105
|
+
* @param {string} params.apiKey - Velt API key
|
|
106
|
+
* @param {string} [params.authToken] - Optional auth token
|
|
107
|
+
* @param {string[]} [params.features=[]] - Features to install (for guided mode)
|
|
108
|
+
* @param {string} [params.crdtType=null] - CRDT editor type if 'crdt' feature selected
|
|
109
|
+
* @param {boolean} [params.force=false] - Force overwrite existing files
|
|
110
|
+
* @param {boolean} [params.legacyPeerDeps=false] - Use legacy peer deps (npm only)
|
|
111
|
+
* @returns {Promise<Object>} CLI execution result
|
|
112
|
+
*/
|
|
113
|
+
export async function runVeltCli({
|
|
114
|
+
installDir,
|
|
115
|
+
apiKey,
|
|
116
|
+
authToken = null,
|
|
117
|
+
features = [],
|
|
118
|
+
crdtType = null,
|
|
119
|
+
force = false,
|
|
120
|
+
legacyPeerDeps = false,
|
|
121
|
+
}) {
|
|
122
|
+
try {
|
|
123
|
+
// Log what we're about to do
|
|
124
|
+
console.error('\n📦 Velt CLI Execution');
|
|
125
|
+
console.error(` Directory: ${installDir}`);
|
|
126
|
+
console.error(` API Key: ${apiKey ? `${apiKey.substring(0, 8)}...` : '(not provided)'}`);
|
|
127
|
+
|
|
128
|
+
// Map features to CLI flags
|
|
129
|
+
const flags = mapFeaturesToCliFlags({
|
|
130
|
+
features,
|
|
131
|
+
crdtType,
|
|
132
|
+
force,
|
|
133
|
+
legacyPeerDeps,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.error(` 🏷️ Features: ${features.length > 0 ? features.join(', ') : '(core only)'}`);
|
|
137
|
+
console.error(` 🚩 Flags: ${flags.length > 0 ? flags.join(' ') : '(none)'}`);
|
|
138
|
+
|
|
139
|
+
// Build environment with API credentials
|
|
140
|
+
const env = {
|
|
141
|
+
...process.env,
|
|
142
|
+
VELT_API_KEY: apiKey,
|
|
143
|
+
NEXT_PUBLIC_VELT_API_KEY: apiKey,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (authToken) {
|
|
147
|
+
env.VELT_AUTH_TOKEN = authToken;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Build npx command
|
|
151
|
+
const npxArgs = ['@velt-js/add-velt', ...flags];
|
|
152
|
+
const fullCommand = `npx ${npxArgs.join(' ')}`;
|
|
153
|
+
|
|
154
|
+
console.error('\n ═══════════════════════════════════════════════════════════');
|
|
155
|
+
console.error(` 📋 EXACT CLI COMMAND: ${fullCommand}`);
|
|
156
|
+
console.error(' ═══════════════════════════════════════════════════════════\n');
|
|
157
|
+
|
|
158
|
+
// Execute via npx
|
|
159
|
+
const result = await new Promise((resolve) => {
|
|
160
|
+
let stdout = '';
|
|
161
|
+
let stderr = '';
|
|
162
|
+
const timeout = 120000;
|
|
163
|
+
|
|
164
|
+
const proc = spawn('npx', npxArgs, {
|
|
165
|
+
cwd: installDir,
|
|
166
|
+
env,
|
|
167
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
168
|
+
shell: process.platform === 'win32',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const timeoutId = setTimeout(() => {
|
|
172
|
+
proc.kill('SIGTERM');
|
|
173
|
+
resolve({
|
|
174
|
+
success: false,
|
|
175
|
+
error: `CLI execution timed out after ${timeout}ms`,
|
|
176
|
+
method: 'npx',
|
|
177
|
+
command: fullCommand,
|
|
178
|
+
exitCode: -1,
|
|
179
|
+
stdout,
|
|
180
|
+
stderr,
|
|
181
|
+
});
|
|
182
|
+
}, timeout);
|
|
183
|
+
|
|
184
|
+
proc.stdout?.on('data', (data) => {
|
|
185
|
+
stdout += data.toString();
|
|
186
|
+
process.stderr.write(data);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
proc.stderr?.on('data', (data) => {
|
|
190
|
+
stderr += data.toString();
|
|
191
|
+
process.stderr.write(data);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
proc.on('error', (err) => {
|
|
195
|
+
clearTimeout(timeoutId);
|
|
196
|
+
resolve({
|
|
197
|
+
success: false,
|
|
198
|
+
error: err.message,
|
|
199
|
+
method: 'npx',
|
|
200
|
+
command: fullCommand,
|
|
201
|
+
exitCode: 1,
|
|
202
|
+
stdout,
|
|
203
|
+
stderr,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
proc.on('close', (code) => {
|
|
208
|
+
clearTimeout(timeoutId);
|
|
209
|
+
resolve({
|
|
210
|
+
success: code === 0,
|
|
211
|
+
exitCode: code,
|
|
212
|
+
method: 'npx',
|
|
213
|
+
command: fullCommand,
|
|
214
|
+
stdout,
|
|
215
|
+
stderr,
|
|
216
|
+
output: stdout + stderr,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Log result summary
|
|
222
|
+
if (result.success) {
|
|
223
|
+
console.error(`\n ✅ CLI completed successfully (method: ${result.method})`);
|
|
224
|
+
} else {
|
|
225
|
+
console.error(`\n ⚠️ CLI exited with code ${result.exitCode} (method: ${result.method})`);
|
|
226
|
+
if (result.error) {
|
|
227
|
+
console.error(` Error: ${result.error}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
success: result.success,
|
|
233
|
+
exitCode: result.exitCode,
|
|
234
|
+
output: result.output,
|
|
235
|
+
command: result.command,
|
|
236
|
+
method: result.method,
|
|
237
|
+
error: result.error,
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(` ❌ CLI execution error: ${error.message}`);
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: error.message,
|
|
244
|
+
exitCode: 1,
|
|
245
|
+
method: 'error',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Runs CLI with feature-specific flags (for guided mode)
|
|
252
|
+
*
|
|
253
|
+
* This is the recommended function for the guided installation path
|
|
254
|
+
* where users have selected specific features.
|
|
255
|
+
*
|
|
256
|
+
* @param {Object} params
|
|
257
|
+
* @param {string} params.projectPath - Target project directory
|
|
258
|
+
* @param {string} params.apiKey - Velt API key
|
|
259
|
+
* @param {string} [params.authToken] - Auth token
|
|
260
|
+
* @param {string[]} params.features - Features: 'comments', 'notifications', 'crdt', 'presence', 'cursors', 'recorder'
|
|
261
|
+
* @param {string} [params.commentType] - Comment type (not used by CLI, for plan only)
|
|
262
|
+
* @param {string} [params.crdtEditorType] - CRDT editor: 'tiptap', 'codemirror', 'reactflow'
|
|
263
|
+
* @param {boolean} [params.force=false] - Force overwrite
|
|
264
|
+
* @returns {Promise<Object>} CLI execution result
|
|
265
|
+
*/
|
|
266
|
+
export async function runVeltCliWithFeatures({
|
|
267
|
+
projectPath,
|
|
268
|
+
apiKey,
|
|
269
|
+
authToken = null,
|
|
270
|
+
features = [],
|
|
271
|
+
commentType = null,
|
|
272
|
+
crdtEditorType = null,
|
|
273
|
+
force = false,
|
|
274
|
+
}) {
|
|
275
|
+
console.error('\n🎯 Running Velt CLI with feature flags');
|
|
276
|
+
|
|
277
|
+
// Map MCP features to CLI-compatible features
|
|
278
|
+
// The CLI supports: comments, notifications, presence, cursors, and CRDT types
|
|
279
|
+
// Only 'recorder' is handled by the MCP guided plan (not supported by CLI)
|
|
280
|
+
const cliFeatures = [];
|
|
281
|
+
|
|
282
|
+
if (features.includes('comments')) {
|
|
283
|
+
cliFeatures.push('comments');
|
|
284
|
+
}
|
|
285
|
+
if (features.includes('notifications')) {
|
|
286
|
+
cliFeatures.push('notifications');
|
|
287
|
+
}
|
|
288
|
+
if (features.includes('presence')) {
|
|
289
|
+
cliFeatures.push('presence');
|
|
290
|
+
}
|
|
291
|
+
if (features.includes('cursors')) {
|
|
292
|
+
cliFeatures.push('cursors');
|
|
293
|
+
}
|
|
294
|
+
if (features.includes('crdt')) {
|
|
295
|
+
if (crdtEditorType) {
|
|
296
|
+
cliFeatures.push('crdt');
|
|
297
|
+
} else {
|
|
298
|
+
console.error(' ⚠️ CRDT feature selected but no crdtEditorType provided — CRDT will be skipped');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.error(` Requested features: ${features.join(', ')}`);
|
|
303
|
+
console.error(` CLI features to forward: ${cliFeatures.length > 0 ? cliFeatures.join(', ') : '(core only)'}`);
|
|
304
|
+
|
|
305
|
+
if (crdtEditorType) {
|
|
306
|
+
console.error(` CRDT Editor: ${crdtEditorType}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return runVeltCli({
|
|
310
|
+
installDir: projectPath,
|
|
311
|
+
apiKey,
|
|
312
|
+
authToken,
|
|
313
|
+
features: cliFeatures,
|
|
314
|
+
crdtType: crdtEditorType,
|
|
315
|
+
force,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Runs CLI in core-only mode (for SKIP/CLI-only path)
|
|
321
|
+
*
|
|
322
|
+
* This runs the CLI without any feature flags, generating only
|
|
323
|
+
* the core scaffold files (VeltInitializeDocument, VeltInitializeUser, VeltCollaboration).
|
|
324
|
+
*
|
|
325
|
+
* @param {Object} params
|
|
326
|
+
* @param {string} params.projectPath - Target project directory
|
|
327
|
+
* @param {string} params.apiKey - Velt API key
|
|
328
|
+
* @param {string} [params.authToken] - Auth token
|
|
329
|
+
* @param {boolean} [params.force=false] - Force overwrite
|
|
330
|
+
* @returns {Promise<Object>} CLI execution result
|
|
331
|
+
*/
|
|
332
|
+
export async function runVeltCliCoreOnly({
|
|
333
|
+
projectPath,
|
|
334
|
+
apiKey,
|
|
335
|
+
authToken = null,
|
|
336
|
+
force = false,
|
|
337
|
+
}) {
|
|
338
|
+
console.error('\n📦 Running Velt CLI (core only, no feature flags)');
|
|
339
|
+
|
|
340
|
+
return runVeltCli({
|
|
341
|
+
installDir: projectPath,
|
|
342
|
+
apiKey,
|
|
343
|
+
authToken,
|
|
344
|
+
features: [], // No features = core scaffold only
|
|
345
|
+
crdtType: null,
|
|
346
|
+
force,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Gets CLI resolution info (for diagnostics/validation)
|
|
352
|
+
*
|
|
353
|
+
* @returns {Object} CLI resolution result
|
|
354
|
+
*/
|
|
355
|
+
export function getCliResolutionInfo() {
|
|
356
|
+
return {
|
|
357
|
+
method: 'npx',
|
|
358
|
+
command: 'npx @velt-js/add-velt',
|
|
359
|
+
path: null,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gets the CLI flags that would be used for given features
|
|
365
|
+
*
|
|
366
|
+
* @param {Object} params - Same as mapFeaturesToCliFlags
|
|
367
|
+
* @returns {string[]} Array of CLI flags
|
|
368
|
+
*/
|
|
369
|
+
export function previewCliFlags(params) {
|
|
370
|
+
return mapFeaturesToCliFlags(params);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export default {
|
|
374
|
+
runVeltCli,
|
|
375
|
+
runVeltCliWithFeatures,
|
|
376
|
+
runVeltCliCoreOnly,
|
|
377
|
+
getCliResolutionInfo,
|
|
378
|
+
previewCliFlags,
|
|
379
|
+
mapFeaturesToCliFlags,
|
|
380
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment Placement Detector
|
|
3
|
+
*
|
|
4
|
+
* Analyzes project structure to determine the best files and locations
|
|
5
|
+
* for placing Velt comments (Freestyle or Popover).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} CommentPlacement
|
|
13
|
+
* @property {string} file - File path where comment should be added
|
|
14
|
+
* @property {string} componentName - Name of the component/file
|
|
15
|
+
* @property {string} reason - Why this location was chosen
|
|
16
|
+
* @property {string} implementationGuide - How to implement the comment type
|
|
17
|
+
* @property {number} confidence - Confidence score (0-100)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detects where to place comments based on user's description and comment type
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} options - Detection options
|
|
24
|
+
* @param {string} options.projectPath - Path to the Next.js project
|
|
25
|
+
* @param {string} options.commentType - 'freestyle' or 'popover'
|
|
26
|
+
* @param {string} [options.targetDescription] - User's description of where to place comments (e.g., "header", "sidebar")
|
|
27
|
+
* @param {string} [options.targetComponent] - Specific component name if known
|
|
28
|
+
* @returns {Promise<Object>} Detection results
|
|
29
|
+
*/
|
|
30
|
+
export async function detectCommentPlacement(options) {
|
|
31
|
+
const {
|
|
32
|
+
projectPath,
|
|
33
|
+
commentType,
|
|
34
|
+
targetDescription = '',
|
|
35
|
+
targetComponent = '',
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
console.error(`🔍 Analyzing project for ${commentType} comment placement...`);
|
|
40
|
+
if (targetDescription) {
|
|
41
|
+
console.error(` 🎯 Target: ${targetDescription}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 1: Detect project structure
|
|
45
|
+
const structure = detectProjectStructure(projectPath);
|
|
46
|
+
|
|
47
|
+
// Step 2: Find candidate files
|
|
48
|
+
const candidates = await findCandidateFiles({
|
|
49
|
+
structure,
|
|
50
|
+
targetDescription,
|
|
51
|
+
targetComponent,
|
|
52
|
+
commentType,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Step 3: Rank candidates by confidence
|
|
56
|
+
const rankedCandidates = rankCandidates(candidates, {
|
|
57
|
+
commentType,
|
|
58
|
+
targetDescription,
|
|
59
|
+
targetComponent,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.error(` ✅ Found ${rankedCandidates.length} potential placement(s)`);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
data: {
|
|
67
|
+
commentType,
|
|
68
|
+
targetDescription,
|
|
69
|
+
placements: rankedCandidates,
|
|
70
|
+
recommendedPlacement: rankedCandidates[0] || null,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(` ❌ Error detecting comment placement: ${error.message}`);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: error.message,
|
|
79
|
+
stack: error.stack,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detects project structure
|
|
86
|
+
*/
|
|
87
|
+
function detectProjectStructure(projectPath) {
|
|
88
|
+
const srcAppPath = path.join(projectPath, 'src', 'app');
|
|
89
|
+
const hasSrcApp = fs.existsSync(srcAppPath);
|
|
90
|
+
|
|
91
|
+
const appRoot = hasSrcApp
|
|
92
|
+
? path.join(projectPath, 'src', 'app')
|
|
93
|
+
: path.join(projectPath, 'app');
|
|
94
|
+
|
|
95
|
+
const componentsRoot = hasSrcApp
|
|
96
|
+
? path.join(projectPath, 'src', 'components')
|
|
97
|
+
: path.join(projectPath, 'components');
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
hasSrcApp,
|
|
101
|
+
appRoot,
|
|
102
|
+
componentsRoot,
|
|
103
|
+
projectPath,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Finds candidate files for comment placement
|
|
109
|
+
*/
|
|
110
|
+
async function findCandidateFiles(options) {
|
|
111
|
+
const { structure, targetDescription, targetComponent, commentType } = options;
|
|
112
|
+
const candidates = [];
|
|
113
|
+
|
|
114
|
+
// Helper to search directory recursively
|
|
115
|
+
function searchDirectory(dir, relativePath = '') {
|
|
116
|
+
if (!fs.existsSync(dir)) return;
|
|
117
|
+
|
|
118
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
119
|
+
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const fullPath = path.join(dir, entry.name);
|
|
122
|
+
const relPath = path.join(relativePath, entry.name);
|
|
123
|
+
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
// Skip node_modules and .next
|
|
126
|
+
if (entry.name === 'node_modules' || entry.name === '.next') continue;
|
|
127
|
+
searchDirectory(fullPath, relPath);
|
|
128
|
+
} else if (entry.isFile() && /\.(tsx|jsx)$/.test(entry.name)) {
|
|
129
|
+
// Analyze this file
|
|
130
|
+
const analysis = analyzeFile(fullPath, relPath, targetDescription, targetComponent, commentType);
|
|
131
|
+
if (analysis) {
|
|
132
|
+
candidates.push(analysis);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Search app directory
|
|
139
|
+
searchDirectory(structure.appRoot, 'app');
|
|
140
|
+
|
|
141
|
+
// Search components directory
|
|
142
|
+
if (fs.existsSync(structure.componentsRoot)) {
|
|
143
|
+
searchDirectory(structure.componentsRoot, 'components');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return candidates;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Analyzes a file to determine if it's a good candidate
|
|
151
|
+
*/
|
|
152
|
+
function analyzeFile(filePath, relativePath, targetDescription, targetComponent, commentType) {
|
|
153
|
+
try {
|
|
154
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
155
|
+
const fileName = path.basename(filePath);
|
|
156
|
+
const componentName = fileName.replace(/\.(tsx|jsx)$/, '');
|
|
157
|
+
|
|
158
|
+
// Calculate match score
|
|
159
|
+
let matchScore = 0;
|
|
160
|
+
let matchReasons = [];
|
|
161
|
+
|
|
162
|
+
// Check if filename matches target description
|
|
163
|
+
const normalizedTarget = targetDescription.toLowerCase();
|
|
164
|
+
const normalizedFileName = fileName.toLowerCase();
|
|
165
|
+
const normalizedRelPath = relativePath.toLowerCase();
|
|
166
|
+
|
|
167
|
+
// Keyword matching
|
|
168
|
+
const keywords = {
|
|
169
|
+
header: ['header', 'navbar', 'nav', 'topbar', 'menu'],
|
|
170
|
+
sidebar: ['sidebar', 'drawer', 'aside', 'panel'],
|
|
171
|
+
footer: ['footer', 'bottom'],
|
|
172
|
+
main: ['main', 'content', 'body', 'page'],
|
|
173
|
+
card: ['card', 'item', 'post', 'article'],
|
|
174
|
+
list: ['list', 'grid', 'table'],
|
|
175
|
+
form: ['form', 'input', 'field'],
|
|
176
|
+
button: ['button', 'btn', 'action'],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Match against keywords
|
|
180
|
+
for (const [category, words] of Object.entries(keywords)) {
|
|
181
|
+
if (normalizedTarget.includes(category)) {
|
|
182
|
+
for (const word of words) {
|
|
183
|
+
if (normalizedFileName.includes(word) || normalizedRelPath.includes(word)) {
|
|
184
|
+
matchScore += 30;
|
|
185
|
+
matchReasons.push(`Matches "${word}" in filename/path`);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if specific component name matches
|
|
193
|
+
if (targetComponent && normalizedFileName.includes(targetComponent.toLowerCase())) {
|
|
194
|
+
matchScore += 50;
|
|
195
|
+
matchReasons.push(`Matches specified component name "${targetComponent}"`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check for React component patterns
|
|
199
|
+
if (content.includes('export default') || content.includes('export function') || content.includes('export const')) {
|
|
200
|
+
matchScore += 10;
|
|
201
|
+
matchReasons.push('Contains React component export');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Freestyle comments work best on page-level components
|
|
205
|
+
if (commentType === 'freestyle') {
|
|
206
|
+
if (fileName === 'page.tsx' || fileName === 'page.jsx') {
|
|
207
|
+
matchScore += 20;
|
|
208
|
+
matchReasons.push('Page component is ideal for freestyle comments');
|
|
209
|
+
}
|
|
210
|
+
if (content.includes('VeltComments') || content.includes('VeltProvider')) {
|
|
211
|
+
matchScore += 15;
|
|
212
|
+
matchReasons.push('Already has Velt integration');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Popover comments work best on interactive components
|
|
217
|
+
if (commentType === 'popover') {
|
|
218
|
+
if (content.includes('onClick') || content.includes('button') || content.includes('Button')) {
|
|
219
|
+
matchScore += 15;
|
|
220
|
+
matchReasons.push('Has interactive elements (good for popover)');
|
|
221
|
+
}
|
|
222
|
+
if (content.includes('card') || content.includes('Card') || content.includes('item')) {
|
|
223
|
+
matchScore += 15;
|
|
224
|
+
matchReasons.push('Has card/item components (good for popover)');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// If no matches, skip this file
|
|
229
|
+
if (matchScore === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Build implementation guide
|
|
234
|
+
const implementationGuide = buildImplementationGuide(commentType, componentName, content);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
file: relativePath,
|
|
238
|
+
fullPath: filePath,
|
|
239
|
+
componentName,
|
|
240
|
+
reason: matchReasons.join('; '),
|
|
241
|
+
implementationGuide,
|
|
242
|
+
confidence: Math.min(matchScore, 100),
|
|
243
|
+
commentType,
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Builds implementation guide for the comment type
|
|
252
|
+
*/
|
|
253
|
+
function buildImplementationGuide(commentType, componentName, fileContent) {
|
|
254
|
+
if (commentType === 'freestyle') {
|
|
255
|
+
return `
|
|
256
|
+
To add Freestyle Comments to ${componentName}:
|
|
257
|
+
|
|
258
|
+
1. Import: import { VeltComments } from '@veltdev/react';
|
|
259
|
+
2. Add component: <VeltComments />
|
|
260
|
+
3. Freestyle comments allow users to click anywhere on the page to add comments
|
|
261
|
+
4. No additional props required for basic setup
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
<div>
|
|
265
|
+
<VeltComments />
|
|
266
|
+
{/* Your existing content */}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
Docs: https://docs.velt.dev/async-collaboration/comments/setup/freestyle
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (commentType === 'popover') {
|
|
274
|
+
return `
|
|
275
|
+
To add Popover Comments to ${componentName}:
|
|
276
|
+
|
|
277
|
+
1. Import: import { VeltCommentTool } from '@veltdev/react';
|
|
278
|
+
2. Add data attribute to target elements: data-velt-comment-target="{uniqueId}"
|
|
279
|
+
3. Add the comment tool component: <VeltCommentTool commentTargetId="{uniqueId}" />
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
<div data-velt-comment-target="component-${componentName.toLowerCase()}">
|
|
283
|
+
<VeltCommentTool commentTargetId="component-${componentName.toLowerCase()}" />
|
|
284
|
+
{/* Your existing content */}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
Docs: https://docs.velt.dev/async-collaboration/comments/setup/popover
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return `See Velt documentation for ${commentType} comments implementation.`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Ranks candidates by confidence score
|
|
296
|
+
*/
|
|
297
|
+
function rankCandidates(candidates, options) {
|
|
298
|
+
return candidates
|
|
299
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
300
|
+
.slice(0, 5); // Return top 5 candidates
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default {
|
|
304
|
+
detectCommentPlacement,
|
|
305
|
+
};
|