@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,149 @@
1
+ /**
2
+ * Configuration Collection Utilities
3
+ *
4
+ * Handles collecting user configuration from .env file
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ /**
11
+ * Reads environment variables from .env.local or .env file
12
+ *
13
+ * @param {string} projectPath - Project root path
14
+ * @returns {Object} Environment variables object
15
+ */
16
+ function readEnvFile(projectPath) {
17
+ // Ensure we're using absolute path - never search outside the specified directory
18
+ const absolutePath = path.resolve(projectPath);
19
+
20
+ // Process .env first, then .env.local so local overrides take precedence
21
+ // (later values overwrite earlier ones in the envVars object)
22
+ const envFiles = [
23
+ path.join(absolutePath, '.env'),
24
+ path.join(absolutePath, '.env.local'),
25
+ ];
26
+
27
+ const envVars = {};
28
+
29
+ console.error(` 📂 Reading .env files from: ${absolutePath}`);
30
+
31
+ for (const envFile of envFiles) {
32
+ if (fs.existsSync(envFile)) {
33
+ try {
34
+ console.error(` ✓ Found: ${path.basename(envFile)}`);
35
+ const content = fs.readFileSync(envFile, 'utf-8');
36
+ const lines = content.split('\n');
37
+
38
+ for (const line of lines) {
39
+ // Skip comments and empty lines
40
+ const trimmed = line.trim();
41
+ if (!trimmed || trimmed.startsWith('#')) {
42
+ continue;
43
+ }
44
+
45
+ // Parse KEY=VALUE format
46
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
47
+ if (match) {
48
+ const key = match[1].trim();
49
+ let value = match[2].trim();
50
+
51
+ // Remove quotes if present
52
+ if ((value.startsWith('"') && value.endsWith('"')) ||
53
+ (value.startsWith("'") && value.endsWith("'"))) {
54
+ value = value.slice(1, -1);
55
+ }
56
+
57
+ envVars[key] = value;
58
+ // Log found keys (but not values for security)
59
+ if (key.includes('VELT') || key.includes('API')) {
60
+ console.error(` ✓ Found ${key}`);
61
+ }
62
+ }
63
+ }
64
+ } catch (error) {
65
+ // Continue to next file if one fails
66
+ console.error(` ⚠️ Could not read ${path.basename(envFile)}: ${error.message}`);
67
+ }
68
+ } else {
69
+ console.error(` ⚠️ Not found: ${path.basename(envFile)}`);
70
+ }
71
+ }
72
+
73
+ return envVars;
74
+ }
75
+
76
+ /**
77
+ * Collects installation configuration from .env file or provided parameters
78
+ *
79
+ * @param {Object} params
80
+ * @param {string} params.projectPath - Project path
81
+ * @param {string} [params.apiKey] - API key provided directly (optional, will read from .env if not provided)
82
+ * @param {string} [params.authToken] - Auth token provided directly (optional, will read from .env if not provided)
83
+ * @returns {Promise<Object>} Configuration result
84
+ */
85
+ export async function collectConfiguration({ projectPath, apiKey: providedApiKey, authToken: providedAuthToken }) {
86
+ try {
87
+ // Resolve absolute path to ensure we're reading from the correct directory
88
+ const absoluteProjectPath = path.resolve(projectPath);
89
+
90
+ // If API key provided directly, use it; otherwise read from .env file
91
+ let apiKey = providedApiKey;
92
+ let authToken = providedAuthToken;
93
+
94
+ if (!apiKey) {
95
+ // Read from .env file ONLY in the specified project directory
96
+ console.error(` 📖 API key not provided, reading from .env files...`);
97
+ const envVars = readEnvFile(absoluteProjectPath);
98
+ apiKey = envVars.VELT_API_KEY || envVars.NEXT_PUBLIC_VELT_API_KEY;
99
+ if (!authToken) {
100
+ authToken = envVars.VELT_AUTH_TOKEN;
101
+ }
102
+ } else {
103
+ console.error(` ✓ Using API key provided as parameter`);
104
+ if (!authToken) {
105
+ // Still try to read auth token from .env if not provided
106
+ const envVars = readEnvFile(absoluteProjectPath);
107
+ authToken = envVars.VELT_AUTH_TOKEN;
108
+ }
109
+ }
110
+
111
+ // API key is required - must be in .env file
112
+ if (!apiKey) {
113
+ const envLocalPath = path.join(absoluteProjectPath, '.env.local');
114
+ const envPath = path.join(absoluteProjectPath, '.env');
115
+ const envLocalExists = fs.existsSync(envLocalPath);
116
+ const envExists = fs.existsSync(envPath);
117
+
118
+ let errorMessage = `API key not found in ${absoluteProjectPath}\n\n`;
119
+ errorMessage += `Checked files:\n`;
120
+ errorMessage += ` - ${envLocalPath} ${envLocalExists ? '(exists but no VELT_API_KEY found)' : '(not found)'}\n`;
121
+ errorMessage += ` - ${envPath} ${envExists ? '(exists but no VELT_API_KEY found)' : '(not found)'}\n\n`;
122
+ errorMessage += `Please add VELT_API_KEY or NEXT_PUBLIC_VELT_API_KEY to ${envLocalExists ? '.env.local' : '.env'} file in ${absoluteProjectPath}\n\n`;
123
+ errorMessage += `Example:\nVELT_API_KEY=your_api_key_here\n\n`;
124
+ errorMessage += `Get your API key from: https://console.velt.dev`;
125
+
126
+ return {
127
+ success: false,
128
+ error: errorMessage,
129
+ };
130
+ }
131
+
132
+ return {
133
+ success: true,
134
+ data: {
135
+ installDir: absoluteProjectPath,
136
+ apiKey: apiKey,
137
+ authToken: authToken || null,
138
+ source: 'env-file',
139
+ },
140
+ };
141
+ } catch (error) {
142
+ return {
143
+ success: false,
144
+ error: error.message,
145
+ };
146
+ }
147
+ }
148
+
149
+
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Framework Detection Utility
3
+ *
4
+ * Detects whether the target project is Next.js, React (CRA/Vite), or unknown.
5
+ * This information is used for:
6
+ * - Determining if "use client" directives are needed (Next.js only)
7
+ * - Adjusting installation patterns based on framework
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ /**
14
+ * Project type enum
15
+ */
16
+ export const ProjectType = {
17
+ NEXTJS: 'nextjs',
18
+ REACT: 'react',
19
+ UNKNOWN: 'unknown',
20
+ };
21
+
22
+ /**
23
+ * Detects the project type by analyzing package.json and file structure
24
+ *
25
+ * @param {string} projectPath - Path to the project directory
26
+ * @returns {Object} Detection result { projectType, signals, confidence }
27
+ */
28
+ export function detectProjectType(projectPath) {
29
+ const signals = [];
30
+ let nextjsScore = 0;
31
+ let reactScore = 0;
32
+
33
+ // === Check package.json dependencies ===
34
+ const packageJsonPath = path.join(projectPath, 'package.json');
35
+
36
+ if (fs.existsSync(packageJsonPath)) {
37
+ try {
38
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
39
+ const allDeps = {
40
+ ...packageJson.dependencies,
41
+ ...packageJson.devDependencies,
42
+ };
43
+
44
+ // Next.js indicators
45
+ if (allDeps.next) {
46
+ signals.push({ type: 'dependency', name: 'next', framework: 'nextjs' });
47
+ nextjsScore += 3; // Strong signal
48
+ }
49
+
50
+ // React (non-Next) indicators
51
+ if (allDeps['react-scripts']) {
52
+ signals.push({ type: 'dependency', name: 'react-scripts', framework: 'react' });
53
+ reactScore += 3; // CRA
54
+ }
55
+
56
+ if (allDeps.vite && allDeps.react) {
57
+ signals.push({ type: 'dependency', name: 'vite+react', framework: 'react' });
58
+ reactScore += 3; // Vite React
59
+ }
60
+
61
+ // Generic React (both Next.js and React have this)
62
+ if (allDeps.react) {
63
+ signals.push({ type: 'dependency', name: 'react', framework: 'both' });
64
+ }
65
+
66
+ } catch (err) {
67
+ signals.push({ type: 'error', message: `Failed to parse package.json: ${err.message}` });
68
+ }
69
+ }
70
+
71
+ // === Check file structure ===
72
+
73
+ // Next.js specific files/folders
74
+ const nextjsIndicators = [
75
+ { path: 'next.config.js', weight: 3 },
76
+ { path: 'next.config.ts', weight: 3 },
77
+ { path: 'next.config.mjs', weight: 3 },
78
+ { path: 'app', weight: 2, isDir: true }, // App Router
79
+ { path: 'pages', weight: 2, isDir: true }, // Pages Router
80
+ { path: 'app/layout.tsx', weight: 2 },
81
+ { path: 'app/layout.js', weight: 2 },
82
+ { path: 'app/page.tsx', weight: 1 },
83
+ { path: 'app/page.js', weight: 1 },
84
+ { path: 'pages/_app.tsx', weight: 2 },
85
+ { path: 'pages/_app.js', weight: 2 },
86
+ { path: 'src/app/layout.tsx', weight: 2 },
87
+ { path: 'src/app/layout.js', weight: 2 },
88
+ ];
89
+
90
+ for (const indicator of nextjsIndicators) {
91
+ const fullPath = path.join(projectPath, indicator.path);
92
+ const exists = indicator.isDir
93
+ ? fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()
94
+ : fs.existsSync(fullPath);
95
+
96
+ if (exists) {
97
+ signals.push({ type: 'file', path: indicator.path, framework: 'nextjs' });
98
+ nextjsScore += indicator.weight;
99
+ }
100
+ }
101
+
102
+ // React (non-Next) specific files
103
+ const reactIndicators = [
104
+ { path: 'src/main.tsx', weight: 2 }, // Vite React
105
+ { path: 'src/main.jsx', weight: 2 },
106
+ { path: 'src/index.tsx', weight: 2 }, // CRA
107
+ { path: 'src/index.jsx', weight: 2 },
108
+ { path: 'vite.config.ts', weight: 2 },
109
+ { path: 'vite.config.js', weight: 2 },
110
+ { path: 'public/index.html', weight: 1 }, // CRA/Vite
111
+ { path: 'index.html', weight: 1 }, // Vite root
112
+ ];
113
+
114
+ for (const indicator of reactIndicators) {
115
+ const fullPath = path.join(projectPath, indicator.path);
116
+ if (fs.existsSync(fullPath)) {
117
+ signals.push({ type: 'file', path: indicator.path, framework: 'react' });
118
+ reactScore += indicator.weight;
119
+ }
120
+ }
121
+
122
+ // === Determine project type ===
123
+ let projectType;
124
+ let confidence;
125
+
126
+ if (nextjsScore > reactScore && nextjsScore >= 3) {
127
+ projectType = ProjectType.NEXTJS;
128
+ confidence = nextjsScore >= 6 ? 'high' : 'medium';
129
+ } else if (reactScore > nextjsScore && reactScore >= 3) {
130
+ projectType = ProjectType.REACT;
131
+ confidence = reactScore >= 6 ? 'high' : 'medium';
132
+ } else if (nextjsScore > 0 || reactScore > 0) {
133
+ // Some signals but not conclusive
134
+ projectType = nextjsScore >= reactScore ? ProjectType.NEXTJS : ProjectType.REACT;
135
+ confidence = 'low';
136
+ } else {
137
+ projectType = ProjectType.UNKNOWN;
138
+ confidence = 'none';
139
+ }
140
+
141
+ return {
142
+ projectType,
143
+ signals,
144
+ confidence,
145
+ scores: {
146
+ nextjs: nextjsScore,
147
+ react: reactScore,
148
+ },
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Checks if project is Next.js
154
+ *
155
+ * @param {string} projectPath - Path to project
156
+ * @returns {boolean} True if Next.js project
157
+ */
158
+ export function isNextJsProject(projectPath) {
159
+ const detection = detectProjectType(projectPath);
160
+ return detection.projectType === ProjectType.NEXTJS;
161
+ }
162
+
163
+ /**
164
+ * Detects which router type Next.js project uses
165
+ *
166
+ * @param {string} projectPath - Path to project
167
+ * @returns {Object} Router detection { routerType: 'app'|'pages'|'both'|'unknown', paths: {} }
168
+ */
169
+ export function detectNextJsRouter(projectPath) {
170
+ const result = {
171
+ routerType: 'unknown',
172
+ paths: {
173
+ appDir: null,
174
+ pagesDir: null,
175
+ layoutFile: null,
176
+ pageFile: null,
177
+ },
178
+ };
179
+
180
+ // Check for app directory (App Router)
181
+ const appDirPaths = ['app', 'src/app'];
182
+ for (const dir of appDirPaths) {
183
+ const fullPath = path.join(projectPath, dir);
184
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
185
+ result.paths.appDir = dir;
186
+
187
+ // Find layout file
188
+ for (const ext of ['tsx', 'jsx', 'ts', 'js']) {
189
+ const layoutPath = path.join(fullPath, `layout.${ext}`);
190
+ if (fs.existsSync(layoutPath)) {
191
+ result.paths.layoutFile = path.join(dir, `layout.${ext}`);
192
+ break;
193
+ }
194
+ }
195
+
196
+ // Find page file
197
+ for (const ext of ['tsx', 'jsx', 'ts', 'js']) {
198
+ const pagePath = path.join(fullPath, `page.${ext}`);
199
+ if (fs.existsSync(pagePath)) {
200
+ result.paths.pageFile = path.join(dir, `page.${ext}`);
201
+ break;
202
+ }
203
+ }
204
+ break;
205
+ }
206
+ }
207
+
208
+ // Check for pages directory (Pages Router)
209
+ const pagesDirPaths = ['pages', 'src/pages'];
210
+ for (const dir of pagesDirPaths) {
211
+ const fullPath = path.join(projectPath, dir);
212
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
213
+ result.paths.pagesDir = dir;
214
+ break;
215
+ }
216
+ }
217
+
218
+ // Determine router type
219
+ if (result.paths.appDir && result.paths.pagesDir) {
220
+ result.routerType = 'both';
221
+ } else if (result.paths.appDir) {
222
+ result.routerType = 'app';
223
+ } else if (result.paths.pagesDir) {
224
+ result.routerType = 'pages';
225
+ }
226
+
227
+ return result;
228
+ }
229
+
230
+ /**
231
+ * Gets framework-specific information for installation
232
+ *
233
+ * @param {string} projectPath - Path to project
234
+ * @returns {Object} Framework info for installation
235
+ */
236
+ export function getFrameworkInfo(projectPath) {
237
+ const detection = detectProjectType(projectPath);
238
+ const info = {
239
+ projectType: detection.projectType,
240
+ confidence: detection.confidence,
241
+ needsUseClient: false,
242
+ routerType: null,
243
+ paths: {},
244
+ };
245
+
246
+ if (detection.projectType === ProjectType.NEXTJS) {
247
+ info.needsUseClient = true;
248
+ const routerInfo = detectNextJsRouter(projectPath);
249
+ info.routerType = routerInfo.routerType;
250
+ info.paths = routerInfo.paths;
251
+ }
252
+
253
+ return info;
254
+ }
255
+
256
+ export default {
257
+ ProjectType,
258
+ detectProjectType,
259
+ isNextJsProject,
260
+ detectNextJsRouter,
261
+ getFrameworkInfo,
262
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Header Positioning Utility
3
+ *
4
+ * Handles VeltCommentsSidebar positioning based on user preference.
5
+ */
6
+
7
+ import fs from 'fs';
8
+
9
+ /**
10
+ * Position mapping
11
+ */
12
+ const POSITION_STYLES = {
13
+ 'top-left': {
14
+ position: 'fixed',
15
+ top: '20px',
16
+ left: '20px',
17
+ right: 'auto',
18
+ bottom: 'auto',
19
+ },
20
+ 'top-right': {
21
+ position: 'fixed',
22
+ top: '20px',
23
+ right: '20px',
24
+ left: 'auto',
25
+ bottom: 'auto',
26
+ },
27
+ 'bottom-left': {
28
+ position: 'fixed',
29
+ bottom: '20px',
30
+ left: '20px',
31
+ right: 'auto',
32
+ top: 'auto',
33
+ },
34
+ 'bottom-right': {
35
+ position: 'fixed',
36
+ bottom: '20px',
37
+ right: '20px',
38
+ left: 'auto',
39
+ top: 'auto',
40
+ },
41
+ };
42
+
43
+ /**
44
+ * Applies header positioning to VeltCommentsSidebar
45
+ *
46
+ * @param {string} filePath - Path to the file containing VeltCommentsSidebar
47
+ * @param {string} position - Position (top-left, top-right, bottom-left, bottom-right)
48
+ * @param {string[]} filesModified - Array to track modified files
49
+ * @param {Object[]} integrationPoints - Array to track integration points
50
+ * @returns {boolean} Success status
51
+ */
52
+ export function applyHeaderPositioning(filePath, position, filesModified, integrationPoints) {
53
+ if (!fs.existsSync(filePath)) {
54
+ return false;
55
+ }
56
+
57
+ try {
58
+ let content = fs.readFileSync(filePath, 'utf-8');
59
+
60
+ // Check if VeltCommentsSidebar exists
61
+ if (!content.includes('VeltCommentsSidebar')) {
62
+ console.error(` ⚠️ VeltCommentsSidebar not found in ${filePath}`);
63
+ return false;
64
+ }
65
+
66
+ const positionStyle = POSITION_STYLES[position] || POSITION_STYLES['top-right'];
67
+
68
+ // Build inline style string
69
+ const styleString = `style={{ position: '${positionStyle.position}', top: '${positionStyle.top}', right: '${positionStyle.right}', bottom: '${positionStyle.bottom}', left: '${positionStyle.left}', zIndex: 9999 }}`;
70
+
71
+ // Match all VeltCommentsSidebar tags: self-closing (with or without props) and opening tags.
72
+ // Captures existing attributes in group 1 and self-closing slash in group 2.
73
+ const tagPattern = /<VeltCommentsSidebar(\s[^>]*?)?\s*(\/?)>/g;
74
+ content = content.replace(tagPattern, (match, existingAttrs, selfClose) => {
75
+ // Strip any existing style prop from the captured attributes to avoid
76
+ // the duplicate-prop override problem (last prop wins in JSX).
77
+ let attrs = (existingAttrs || '').replace(/\s*style=\{\{[^}]*\}\}/g, '');
78
+ const closing = selfClose ? ' /' : '';
79
+ return `<VeltCommentsSidebar ${styleString}${attrs}${closing}>`;
80
+ });
81
+
82
+ // Write back
83
+ fs.writeFileSync(filePath, content, 'utf-8');
84
+
85
+ const relativePath = filePath.split('/').slice(-3).join('/');
86
+ if (!filesModified.includes(relativePath)) {
87
+ filesModified.push(relativePath);
88
+ }
89
+
90
+ integrationPoints.push({
91
+ file: relativePath,
92
+ type: 'header-positioning',
93
+ description: `Applied ${position} positioning to VeltCommentsSidebar`,
94
+ position,
95
+ });
96
+
97
+ console.error(` 📍 Applied ${position} positioning to VeltCommentsSidebar`);
98
+ return true;
99
+ } catch (error) {
100
+ console.error(` ❌ Error applying header positioning: ${error.message}`);
101
+ return false;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Finds files containing VeltCommentsSidebar
107
+ *
108
+ * @param {string} projectPath - Project root path
109
+ * @returns {string[]} Array of file paths
110
+ */
111
+ export function findVeltSidebarFiles(projectPath) {
112
+ const results = [];
113
+
114
+ function searchDirectory(dir) {
115
+ if (!fs.existsSync(dir)) return;
116
+
117
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
118
+
119
+ for (const entry of entries) {
120
+ const fullPath = `${dir}/${entry.name}`;
121
+
122
+ if (entry.isDirectory()) {
123
+ if (entry.name === 'node_modules' || entry.name === '.next') continue;
124
+ searchDirectory(fullPath);
125
+ } else if (entry.isFile() && /\.(tsx|jsx)$/.test(entry.name)) {
126
+ try {
127
+ const content = fs.readFileSync(fullPath, 'utf-8');
128
+ if (content.includes('VeltCommentsSidebar')) {
129
+ results.push(fullPath);
130
+ }
131
+ } catch (err) {
132
+ // Skip files we can't read
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ searchDirectory(projectPath);
139
+ return results;
140
+ }
141
+
142
+ export default {
143
+ applyHeaderPositioning,
144
+ findVeltSidebarFiles,
145
+ POSITION_STYLES,
146
+ };