magicpath-ai 1.2.0 → 1.3.0-beta.1
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/dist/cli.js +8 -212
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +237 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +76 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/clone.d.ts +2 -0
- package/dist/commands/clone.js +215 -0
- package/dist/commands/clone.js.map +1 -0
- package/dist/commands/integrate.d.ts +2 -0
- package/dist/commands/integrate.js +269 -0
- package/dist/commands/integrate.js.map +1 -0
- package/dist/util/api.d.ts +5 -1
- package/dist/util/api.js +16 -3
- package/dist/util/api.js.map +1 -1
- package/dist/util/auth.d.ts +11 -0
- package/dist/util/auth.js +38 -0
- package/dist/util/auth.js.map +1 -0
- package/dist/util/authError.d.ts +4 -0
- package/dist/util/authError.js +7 -0
- package/dist/util/authError.js.map +1 -0
- package/dist/util/component.d.ts +186 -0
- package/dist/util/component.js +343 -0
- package/dist/util/component.js.map +1 -0
- package/dist/util/dependencies.d.ts +11 -0
- package/dist/util/dependencies.js +71 -6
- package/dist/util/dependencies.js.map +1 -1
- package/dist/util/diff.d.ts +9 -0
- package/dist/util/diff.js +55 -0
- package/dist/util/diff.js.map +1 -0
- package/dist/util/error.d.ts +0 -1
- package/dist/util/error.js +0 -1
- package/dist/util/error.js.map +1 -1
- package/dist/util/ide.js +1 -1
- package/dist/util/integrate.d.ts +73 -0
- package/dist/util/integrate.js +660 -0
- package/dist/util/integrate.js.map +1 -0
- package/dist/util/ui.d.ts +11 -0
- package/dist/util/ui.js +30 -0
- package/dist/util/ui.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { createTwoFilesPatch } from 'diff';
|
|
5
|
+
import { makeIntegrateUrl } from './api.js';
|
|
6
|
+
import { getAuthHeaders } from './auth.js';
|
|
7
|
+
import { MagicPathError } from './error.js';
|
|
8
|
+
import { AuthRequiredError } from './authError.js';
|
|
9
|
+
export function detectFramework(cwd) {
|
|
10
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
11
|
+
if (!fs.existsSync(pkgPath))
|
|
12
|
+
return 'unknown';
|
|
13
|
+
try {
|
|
14
|
+
const pkg = fs.readJsonSync(pkgPath);
|
|
15
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
16
|
+
if (deps.next) {
|
|
17
|
+
if (fs.existsSync(path.join(cwd, 'app')) ||
|
|
18
|
+
fs.existsSync(path.join(cwd, 'src', 'app'))) {
|
|
19
|
+
return 'nextjs-app';
|
|
20
|
+
}
|
|
21
|
+
if (fs.existsSync(path.join(cwd, 'pages')) ||
|
|
22
|
+
fs.existsSync(path.join(cwd, 'src', 'pages'))) {
|
|
23
|
+
return 'nextjs-pages';
|
|
24
|
+
}
|
|
25
|
+
return 'nextjs-app'; // default for Next.js
|
|
26
|
+
}
|
|
27
|
+
if (deps.vite)
|
|
28
|
+
return 'vite';
|
|
29
|
+
// tslint:disable-next-line:no-string-literal
|
|
30
|
+
if (deps['react-scripts'])
|
|
31
|
+
return 'cra';
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
return 'unknown';
|
|
37
|
+
}
|
|
38
|
+
const LANGUAGE_CONFIGS = {
|
|
39
|
+
js: {
|
|
40
|
+
displayName: 'JavaScript/TypeScript',
|
|
41
|
+
extensions: ['.tsx', '.ts', '.jsx', '.js'],
|
|
42
|
+
excludedDirs: [
|
|
43
|
+
'node_modules',
|
|
44
|
+
'.git',
|
|
45
|
+
'dist',
|
|
46
|
+
'.next',
|
|
47
|
+
'build',
|
|
48
|
+
'out',
|
|
49
|
+
'.turbo',
|
|
50
|
+
'.vercel',
|
|
51
|
+
],
|
|
52
|
+
excludedPatterns: [
|
|
53
|
+
/\.test\.[jt]sx?$/,
|
|
54
|
+
/\.spec\.[jt]sx?$/,
|
|
55
|
+
/\.stories\.[jt]sx?$/,
|
|
56
|
+
/\.config\.[jt]sx?$/,
|
|
57
|
+
/\.d\.ts$/,
|
|
58
|
+
/\.env/,
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
swift: {
|
|
62
|
+
displayName: 'Swift/SwiftUI',
|
|
63
|
+
extensions: ['.swift'],
|
|
64
|
+
excludedDirs: [
|
|
65
|
+
'.git',
|
|
66
|
+
'.build',
|
|
67
|
+
'DerivedData',
|
|
68
|
+
'Pods',
|
|
69
|
+
'.swiftpm',
|
|
70
|
+
'build',
|
|
71
|
+
],
|
|
72
|
+
excludedPatterns: [
|
|
73
|
+
/Tests?\.swift$/,
|
|
74
|
+
/\.generated\.swift$/,
|
|
75
|
+
/Package\.swift$/,
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
kotlin: {
|
|
79
|
+
displayName: 'Kotlin/Jetpack Compose',
|
|
80
|
+
extensions: ['.kt', '.kts'],
|
|
81
|
+
excludedDirs: ['.git', '.gradle', 'build', '.idea', 'gradle'],
|
|
82
|
+
excludedPatterns: [/Test\.kt$/, /\.generated\.kt$/],
|
|
83
|
+
},
|
|
84
|
+
dart: {
|
|
85
|
+
displayName: 'Dart/Flutter',
|
|
86
|
+
extensions: ['.dart'],
|
|
87
|
+
excludedDirs: ['.git', '.dart_tool', 'build', '.idea', '.pub-cache'],
|
|
88
|
+
excludedPatterns: [/_test\.dart$/, /\.g\.dart$/, /\.freezed\.dart$/],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
export function detectLanguageFromExtension(filePath) {
|
|
92
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
93
|
+
if (['.tsx', '.ts', '.jsx', '.js'].includes(ext))
|
|
94
|
+
return 'js';
|
|
95
|
+
for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
|
|
96
|
+
if (config.extensions.includes(ext))
|
|
97
|
+
return lang;
|
|
98
|
+
}
|
|
99
|
+
return ext.replace(/^\./, '') || 'unknown';
|
|
100
|
+
}
|
|
101
|
+
export function detectProjectLanguage(cwd) {
|
|
102
|
+
const markers = [
|
|
103
|
+
['Package.swift', 'swift'],
|
|
104
|
+
['build.gradle', 'kotlin'],
|
|
105
|
+
['build.gradle.kts', 'kotlin'],
|
|
106
|
+
['pubspec.yaml', 'dart'],
|
|
107
|
+
['package.json', 'js'],
|
|
108
|
+
];
|
|
109
|
+
for (const [file, lang] of markers) {
|
|
110
|
+
if (fs.existsSync(path.join(cwd, file)))
|
|
111
|
+
return lang;
|
|
112
|
+
}
|
|
113
|
+
// Check for Xcode project directories
|
|
114
|
+
try {
|
|
115
|
+
const entries = fs.readdirSync(cwd);
|
|
116
|
+
if (entries.some((e) => e.endsWith('.xcodeproj') || e.endsWith('.xcworkspace'))) {
|
|
117
|
+
return 'swift';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// ignore unreadable directory
|
|
122
|
+
}
|
|
123
|
+
// No manifest found — infer language from file extensions in the directory
|
|
124
|
+
try {
|
|
125
|
+
const entries = fs.readdirSync(cwd);
|
|
126
|
+
for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
|
|
127
|
+
if (lang === 'js')
|
|
128
|
+
continue; // JS requires package.json
|
|
129
|
+
if (entries.some((e) => config.extensions.some((ext) => e.endsWith(ext)))) {
|
|
130
|
+
return lang;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore unreadable directory
|
|
136
|
+
}
|
|
137
|
+
return 'unknown';
|
|
138
|
+
}
|
|
139
|
+
export function getLanguageDisplayName(language) {
|
|
140
|
+
return LANGUAGE_CONFIGS[language]?.displayName ?? language;
|
|
141
|
+
}
|
|
142
|
+
const EXCLUDED_DIRS = LANGUAGE_CONFIGS.js.excludedDirs;
|
|
143
|
+
const EXCLUDED_PATTERNS = LANGUAGE_CONFIGS.js.excludedPatterns;
|
|
144
|
+
function walkSync(dir, base, maxDepth, depth = 0, opts) {
|
|
145
|
+
if (depth > maxDepth)
|
|
146
|
+
return [];
|
|
147
|
+
const results = [];
|
|
148
|
+
const effectiveExcludedDirs = opts?.excludedDirs ?? EXCLUDED_DIRS;
|
|
149
|
+
const effectiveExcludedPatterns = opts?.excludedPatterns ?? EXCLUDED_PATTERNS;
|
|
150
|
+
const extensionRegex = opts?.extensions
|
|
151
|
+
? new RegExp(`(${opts.extensions.map((e) => e.replace('.', '\\.')).join('|')})$`)
|
|
152
|
+
: /\.[jt]sx?$/;
|
|
153
|
+
let entries;
|
|
154
|
+
try {
|
|
155
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (effectiveExcludedDirs.some((d) => d.toLowerCase() === entry.name.toLowerCase()))
|
|
162
|
+
continue;
|
|
163
|
+
const fullPath = path.join(dir, entry.name);
|
|
164
|
+
const rel = path.relative(base, fullPath);
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
results.push(...walkSync(fullPath, base, maxDepth, depth + 1, opts));
|
|
167
|
+
}
|
|
168
|
+
else if (extensionRegex.test(entry.name)) {
|
|
169
|
+
if (!effectiveExcludedPatterns.some((p) => p.test(rel))) {
|
|
170
|
+
results.push(rel);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
function labelForFile(rel, framework) {
|
|
177
|
+
if (/app\/.*page\.[jt]sx?$/.test(rel))
|
|
178
|
+
return '(page)';
|
|
179
|
+
if (/app\/.*layout\.[jt]sx?$/.test(rel))
|
|
180
|
+
return '(layout)';
|
|
181
|
+
if (/pages\/.*\.[jt]sx?$/.test(rel))
|
|
182
|
+
return '(page)';
|
|
183
|
+
if (/App\.[jt]sx?$/.test(rel))
|
|
184
|
+
return '(app root)';
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
export function findIntegrationCandidates(cwd, framework) {
|
|
188
|
+
const candidates = [];
|
|
189
|
+
switch (framework) {
|
|
190
|
+
case 'nextjs-app': {
|
|
191
|
+
// Check both root app/ and src/app/
|
|
192
|
+
const appDir = fs.existsSync(path.join(cwd, 'src', 'app'))
|
|
193
|
+
? path.join(cwd, 'src', 'app')
|
|
194
|
+
: path.join(cwd, 'app');
|
|
195
|
+
if (fs.existsSync(appDir)) {
|
|
196
|
+
const pages = walkSync(appDir, cwd, 4).filter((f) => /page\.[jt]sx?$/.test(f));
|
|
197
|
+
candidates.push(...pages);
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 'nextjs-pages': {
|
|
202
|
+
const pagesDir = fs.existsSync(path.join(cwd, 'src', 'pages'))
|
|
203
|
+
? path.join(cwd, 'src', 'pages')
|
|
204
|
+
: path.join(cwd, 'pages');
|
|
205
|
+
if (fs.existsSync(pagesDir)) {
|
|
206
|
+
const pages = walkSync(pagesDir, cwd, 4).filter((f) => !/_(app|document)\.[jt]sx?$/.test(f));
|
|
207
|
+
candidates.push(...pages);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case 'vite':
|
|
212
|
+
case 'cra': {
|
|
213
|
+
const viteSrcDir = path.join(cwd, 'src');
|
|
214
|
+
const appFile = ['src/App.tsx', 'src/App.jsx'].find((f) => fs.existsSync(path.join(cwd, f)));
|
|
215
|
+
if (appFile)
|
|
216
|
+
candidates.push(appFile);
|
|
217
|
+
const pagesDir = path.join(viteSrcDir, 'pages');
|
|
218
|
+
if (fs.existsSync(pagesDir)) {
|
|
219
|
+
candidates.push(...walkSync(pagesDir, cwd, 4));
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
// Always include other .tsx/.jsx files from src/ (after framework-specific ones)
|
|
227
|
+
const srcDir = path.join(cwd, 'src');
|
|
228
|
+
if (fs.existsSync(srcDir)) {
|
|
229
|
+
const files = walkSync(srcDir, cwd, 4).filter((f) => /\.[jt]sx$/.test(f));
|
|
230
|
+
candidates.push(...files);
|
|
231
|
+
}
|
|
232
|
+
// Deduplicate
|
|
233
|
+
const unique = [...new Set(candidates)];
|
|
234
|
+
return unique.map((rel) => ({
|
|
235
|
+
relativePath: rel,
|
|
236
|
+
label: labelForFile(rel, framework),
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Find integration candidate files for any language.
|
|
241
|
+
* For JS/TS, delegates to the framework-aware findIntegrationCandidates.
|
|
242
|
+
* For other languages, scans using LANGUAGE_CONFIGS.
|
|
243
|
+
*/
|
|
244
|
+
export function findCandidates(cwd, language, framework) {
|
|
245
|
+
if (language === 'js') {
|
|
246
|
+
return findIntegrationCandidates(cwd, framework);
|
|
247
|
+
}
|
|
248
|
+
const config = LANGUAGE_CONFIGS[language];
|
|
249
|
+
const walkOpts = config
|
|
250
|
+
? {
|
|
251
|
+
extensions: config.extensions,
|
|
252
|
+
excludedDirs: config.excludedDirs,
|
|
253
|
+
excludedPatterns: config.excludedPatterns,
|
|
254
|
+
}
|
|
255
|
+
: undefined;
|
|
256
|
+
const candidates = walkSync(cwd, cwd, 10, 0, walkOpts);
|
|
257
|
+
const unique = [...new Set(candidates)];
|
|
258
|
+
return unique.map((rel) => ({
|
|
259
|
+
relativePath: rel,
|
|
260
|
+
label: '',
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
const MAX_TOTAL_SIZE = 100 * 1024; // 100KB
|
|
264
|
+
const RESOLVE_EXTENSIONS = [
|
|
265
|
+
'.tsx',
|
|
266
|
+
'.ts',
|
|
267
|
+
'.jsx',
|
|
268
|
+
'.js',
|
|
269
|
+
'/index.tsx',
|
|
270
|
+
'/index.ts',
|
|
271
|
+
];
|
|
272
|
+
/**
|
|
273
|
+
* Resolve an import specifier to an absolute file path.
|
|
274
|
+
* Handles relative paths (./foo, ../bar) and @/ alias (mapped to src/).
|
|
275
|
+
*/
|
|
276
|
+
function resolveImportPath(specifier, fromDir, cwd) {
|
|
277
|
+
let basePath;
|
|
278
|
+
if (specifier.startsWith('.')) {
|
|
279
|
+
basePath = path.resolve(fromDir, specifier);
|
|
280
|
+
}
|
|
281
|
+
else if (specifier.startsWith('@/')) {
|
|
282
|
+
basePath = path.resolve(cwd, 'src', specifier.slice(2));
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
return null; // node_modules / external
|
|
286
|
+
}
|
|
287
|
+
// Try with extensions
|
|
288
|
+
for (const ext of RESOLVE_EXTENSIONS) {
|
|
289
|
+
const candidate = basePath + ext;
|
|
290
|
+
if (fs.existsSync(candidate))
|
|
291
|
+
return candidate;
|
|
292
|
+
}
|
|
293
|
+
// Try exact path
|
|
294
|
+
if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
|
|
295
|
+
return basePath;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Find files under src/ that import the target file (reverse dependencies / "parents").
|
|
301
|
+
*/
|
|
302
|
+
export function findReverseDependencies(targetPath, cwd, sizeBudget) {
|
|
303
|
+
const targetAbs = path.resolve(cwd, targetPath);
|
|
304
|
+
const srcDir = path.join(cwd, 'src');
|
|
305
|
+
if (!fs.existsSync(srcDir))
|
|
306
|
+
return [];
|
|
307
|
+
const allFiles = walkSync(srcDir, cwd, 4);
|
|
308
|
+
const results = [];
|
|
309
|
+
const importRegex = /(?:import|from)\s+['"]([^'"]+)['"]/g;
|
|
310
|
+
for (const relFile of allFiles) {
|
|
311
|
+
if (results.length >= 5)
|
|
312
|
+
break;
|
|
313
|
+
const absFile = path.resolve(cwd, relFile);
|
|
314
|
+
if (absFile === targetAbs)
|
|
315
|
+
continue; // skip self
|
|
316
|
+
try {
|
|
317
|
+
const content = fs.readFileSync(absFile, 'utf8');
|
|
318
|
+
let imports = false;
|
|
319
|
+
importRegex.lastIndex = 0;
|
|
320
|
+
let match = importRegex.exec(content);
|
|
321
|
+
while (match !== null) {
|
|
322
|
+
const resolved = resolveImportPath(match[1], path.dirname(absFile), cwd);
|
|
323
|
+
if (resolved === targetAbs) {
|
|
324
|
+
imports = true;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
match = importRegex.exec(content);
|
|
328
|
+
}
|
|
329
|
+
if (imports) {
|
|
330
|
+
const size = Buffer.byteLength(content);
|
|
331
|
+
if (size > sizeBudget.remaining)
|
|
332
|
+
continue; // skip if too large, try next
|
|
333
|
+
sizeBudget.remaining -= size;
|
|
334
|
+
results.push({ path: relFile, content });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// skip unreadable files
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Read the target file, its imports (children), and files that import it (parents).
|
|
345
|
+
*/
|
|
346
|
+
/**
|
|
347
|
+
* Extract local imports from file content and resolve them to FileContent entries.
|
|
348
|
+
* Skips files already in the seenPaths set and respects the size budget.
|
|
349
|
+
*/
|
|
350
|
+
function extractImports(fileContent, fileAbsPath, cwd, seenPaths, sizeBudget, maxFiles) {
|
|
351
|
+
const results = [];
|
|
352
|
+
const importRegex = /(?:import|from)\s+['"]([.@][^'"]+)['"]/g;
|
|
353
|
+
const matches = [];
|
|
354
|
+
let m = importRegex.exec(fileContent);
|
|
355
|
+
while (m !== null) {
|
|
356
|
+
matches.push(m);
|
|
357
|
+
m = importRegex.exec(fileContent);
|
|
358
|
+
}
|
|
359
|
+
for (const match of matches) {
|
|
360
|
+
if (results.length >= maxFiles)
|
|
361
|
+
break;
|
|
362
|
+
const specifier = match[1];
|
|
363
|
+
const resolved = resolveImportPath(specifier, path.dirname(fileAbsPath), cwd);
|
|
364
|
+
if (!resolved)
|
|
365
|
+
continue;
|
|
366
|
+
if (resolved.includes('node_modules'))
|
|
367
|
+
continue;
|
|
368
|
+
const relPath = path.relative(cwd, resolved);
|
|
369
|
+
// Skip UI library primitives (e.g. components/ui/button) and static assets
|
|
370
|
+
if (/\/(ui|assets)\//.test(relPath))
|
|
371
|
+
continue;
|
|
372
|
+
if (seenPaths.has(relPath))
|
|
373
|
+
continue;
|
|
374
|
+
try {
|
|
375
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
376
|
+
const size = Buffer.byteLength(content);
|
|
377
|
+
if (size > sizeBudget.remaining)
|
|
378
|
+
continue;
|
|
379
|
+
sizeBudget.remaining -= size;
|
|
380
|
+
seenPaths.add(relPath);
|
|
381
|
+
results.push({ path: relPath, content });
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// skip unreadable files
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return results;
|
|
388
|
+
}
|
|
389
|
+
// ── Non-JS context gathering (type-name-based reverse deps) ──────────
|
|
390
|
+
/**
|
|
391
|
+
* Extract declared type names (struct, class, enum, protocol, etc.) from source code.
|
|
392
|
+
* Intentionally broad regex that works across Swift, Kotlin, Dart, and similar languages.
|
|
393
|
+
*/
|
|
394
|
+
function extractTypeNames(content) {
|
|
395
|
+
const typeRegex = /(?:struct|class|enum|protocol|extension|typealias)\s+([A-Z][a-zA-Z0-9]*)/g;
|
|
396
|
+
const names = new Set();
|
|
397
|
+
let match = typeRegex.exec(content);
|
|
398
|
+
while (match !== null) {
|
|
399
|
+
names.add(match[1]);
|
|
400
|
+
match = typeRegex.exec(content);
|
|
401
|
+
}
|
|
402
|
+
return [...names];
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Find files that reference any of the given type names (reverse dependencies by name).
|
|
406
|
+
* Used for non-JS languages where import resolution isn't feasible.
|
|
407
|
+
*/
|
|
408
|
+
function findReverseDependenciesByName(targetPath, typeNames, cwd, language, sizeBudget) {
|
|
409
|
+
const targetAbs = path.resolve(cwd, targetPath);
|
|
410
|
+
const config = LANGUAGE_CONFIGS[language];
|
|
411
|
+
const walkOpts = config
|
|
412
|
+
? {
|
|
413
|
+
extensions: config.extensions,
|
|
414
|
+
excludedDirs: config.excludedDirs,
|
|
415
|
+
excludedPatterns: config.excludedPatterns,
|
|
416
|
+
}
|
|
417
|
+
: undefined;
|
|
418
|
+
const allFiles = walkSync(cwd, cwd, 10, 0, walkOpts);
|
|
419
|
+
const results = [];
|
|
420
|
+
// Build a single regex that matches any of the type names as whole words
|
|
421
|
+
const namePattern = new RegExp(`\\b(${typeNames.join('|')})\\b`);
|
|
422
|
+
for (const relFile of allFiles) {
|
|
423
|
+
if (results.length >= 5)
|
|
424
|
+
break;
|
|
425
|
+
const absFile = path.resolve(cwd, relFile);
|
|
426
|
+
if (absFile === targetAbs)
|
|
427
|
+
continue;
|
|
428
|
+
try {
|
|
429
|
+
const content = fs.readFileSync(absFile, 'utf8');
|
|
430
|
+
if (namePattern.test(content)) {
|
|
431
|
+
const size = Buffer.byteLength(content);
|
|
432
|
+
if (size > sizeBudget.remaining)
|
|
433
|
+
continue;
|
|
434
|
+
sizeBudget.remaining -= size;
|
|
435
|
+
results.push({ path: relFile, content });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// skip unreadable files
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return results;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Read the target file, its imports (children), and files that import it (parents).
|
|
446
|
+
* Also follows imports from componentFiles so their dependencies are included.
|
|
447
|
+
*/
|
|
448
|
+
export function readTargetWithContext(targetPath, cwd, componentFiles, language) {
|
|
449
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
450
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
451
|
+
const targetFile = {
|
|
452
|
+
path: targetPath,
|
|
453
|
+
content,
|
|
454
|
+
};
|
|
455
|
+
const targetSize = Buffer.byteLength(content);
|
|
456
|
+
const sizeBudget = { remaining: MAX_TOTAL_SIZE - targetSize };
|
|
457
|
+
// For non-JS languages, use type-name-based reverse-dep scanning instead of JS import resolution
|
|
458
|
+
if (language && language !== 'js') {
|
|
459
|
+
const typeNames = extractTypeNames(content);
|
|
460
|
+
const nonJsReferencingFiles = typeNames.length > 0
|
|
461
|
+
? findReverseDependenciesByName(targetPath, typeNames, cwd, language, sizeBudget)
|
|
462
|
+
: [];
|
|
463
|
+
return {
|
|
464
|
+
targetFile,
|
|
465
|
+
importedFiles: [],
|
|
466
|
+
referencingFiles: nonJsReferencingFiles,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
// Track all paths we've already included to avoid duplicates
|
|
470
|
+
const seenPaths = new Set([targetPath]);
|
|
471
|
+
if (componentFiles) {
|
|
472
|
+
for (const cf of componentFiles) {
|
|
473
|
+
seenPaths.add(cf.path);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Extract imports from target file
|
|
477
|
+
const importedFiles = extractImports(content, absPath, cwd, seenPaths, sizeBudget, 5);
|
|
478
|
+
// Also extract imports from component files (e.g. FilesGrid imported by the component)
|
|
479
|
+
if (componentFiles) {
|
|
480
|
+
for (const cf of componentFiles) {
|
|
481
|
+
if (importedFiles.length >= 10)
|
|
482
|
+
break;
|
|
483
|
+
const cfAbsPath = path.resolve(cwd, cf.path);
|
|
484
|
+
const remaining = 10 - importedFiles.length;
|
|
485
|
+
const cfImports = extractImports(cf.content, cfAbsPath, cwd, seenPaths, sizeBudget, remaining);
|
|
486
|
+
importedFiles.push(...cfImports);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Find reverse dependencies (parents)
|
|
490
|
+
const referencingFiles = findReverseDependencies(targetPath, cwd, sizeBudget);
|
|
491
|
+
return { targetFile, importedFiles, referencingFiles };
|
|
492
|
+
}
|
|
493
|
+
// ── Diff preview ─────────────────────────────────────────────────────
|
|
494
|
+
export function generateDiff(originalPath, originalContent, newContent) {
|
|
495
|
+
return createTwoFilesPatch(originalPath, originalPath, originalContent, newContent, '', '', { context: 3 });
|
|
496
|
+
}
|
|
497
|
+
export function colorDiff(diff) {
|
|
498
|
+
return diff
|
|
499
|
+
.split('\n')
|
|
500
|
+
.map((line) => {
|
|
501
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
502
|
+
return `\x1b[32m${line}\x1b[0m`; // green
|
|
503
|
+
}
|
|
504
|
+
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
505
|
+
return `\x1b[31m${line}\x1b[0m`; // red
|
|
506
|
+
}
|
|
507
|
+
if (line.startsWith('@@')) {
|
|
508
|
+
return `\x1b[36m${line}\x1b[0m`; // cyan
|
|
509
|
+
}
|
|
510
|
+
return line;
|
|
511
|
+
})
|
|
512
|
+
.join('\n');
|
|
513
|
+
}
|
|
514
|
+
// ── Gather project files for retheme ─────────────────────────────────
|
|
515
|
+
const RETHEME_MAX_TOTAL_SIZE = 1024 * 1024; // 1MB
|
|
516
|
+
/**
|
|
517
|
+
* Quick heuristic to check if a file contains visual UI content.
|
|
518
|
+
*/
|
|
519
|
+
function hasVisualContent(content, language) {
|
|
520
|
+
switch (language) {
|
|
521
|
+
case 'js':
|
|
522
|
+
return /className[=:]|<[A-Z]|<div|<span|<section|<main|<header|<footer|<nav|<aside|tailwind|@apply|style[=:{]/i.test(content);
|
|
523
|
+
case 'swift':
|
|
524
|
+
return /some\s+View|VStack|HStack|ZStack|List\s*\{|NavigationStack|NavigationView|Form\s*\{|ScrollView|TabView|\.padding|\.font|\.foregroundColor|\.background|GeometryReader|@ViewBuilder/i.test(content);
|
|
525
|
+
case 'kotlin':
|
|
526
|
+
return /@Composable|Column\s*\(|Row\s*\(|Box\s*\(|LazyColumn|Scaffold|Modifier\./i.test(content);
|
|
527
|
+
case 'dart':
|
|
528
|
+
return /Widget\b|Scaffold|Column\(|Row\(|Container\(|Padding\(|ListView|GridView|AppBar/i.test(content);
|
|
529
|
+
default:
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Priority score for file ordering: pages/layouts/views first, then components, then other.
|
|
535
|
+
*/
|
|
536
|
+
function filePriority(rel) {
|
|
537
|
+
// JS/TS
|
|
538
|
+
if (/app\/.*layout\.[jt]sx?$/.test(rel))
|
|
539
|
+
return 0;
|
|
540
|
+
if (/app\/.*page\.[jt]sx?$/.test(rel))
|
|
541
|
+
return 1;
|
|
542
|
+
if (/pages\/.*\.[jt]sx?$/.test(rel))
|
|
543
|
+
return 2;
|
|
544
|
+
if (/App\.[jt]sx?$/.test(rel))
|
|
545
|
+
return 3;
|
|
546
|
+
// Swift — main app views and tabs
|
|
547
|
+
if (/App\/.*Tab\.swift$/.test(rel))
|
|
548
|
+
return 1;
|
|
549
|
+
if (/AppView\.swift$/.test(rel))
|
|
550
|
+
return 0;
|
|
551
|
+
if (/View\.swift$/.test(rel))
|
|
552
|
+
return 2;
|
|
553
|
+
// General
|
|
554
|
+
if (/component/i.test(rel))
|
|
555
|
+
return 4;
|
|
556
|
+
return 5;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Collect all UI source files from the project for retheme.
|
|
560
|
+
* For JS: filters to files containing visual content (JSX/className/Tailwind).
|
|
561
|
+
* Prioritizes pages/layouts first, then components, then other files.
|
|
562
|
+
* Respects a 1MB total size budget.
|
|
563
|
+
*/
|
|
564
|
+
export function gatherProjectFiles(cwd, language, framework) {
|
|
565
|
+
const config = LANGUAGE_CONFIGS[language] ?? LANGUAGE_CONFIGS.js;
|
|
566
|
+
const walkOpts = {
|
|
567
|
+
extensions: config.extensions,
|
|
568
|
+
excludedDirs: config.excludedDirs,
|
|
569
|
+
excludedPatterns: config.excludedPatterns,
|
|
570
|
+
};
|
|
571
|
+
const allRelPaths = walkSync(cwd, cwd, 6, 0, walkOpts);
|
|
572
|
+
// Sort by priority
|
|
573
|
+
allRelPaths.sort((a, b) => filePriority(a) - filePriority(b));
|
|
574
|
+
const files = [];
|
|
575
|
+
const skippedFiles = [];
|
|
576
|
+
let totalSize = 0;
|
|
577
|
+
let truncated = false;
|
|
578
|
+
for (const rel of allRelPaths) {
|
|
579
|
+
const absFile = path.resolve(cwd, rel);
|
|
580
|
+
try {
|
|
581
|
+
const content = fs.readFileSync(absFile, 'utf8');
|
|
582
|
+
const size = Buffer.byteLength(content);
|
|
583
|
+
// Skip non-visual files (hooks, utils, types, manifests, etc.)
|
|
584
|
+
if (!hasVisualContent(content, language)) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (totalSize + size > RETHEME_MAX_TOTAL_SIZE) {
|
|
588
|
+
truncated = true;
|
|
589
|
+
skippedFiles.push(rel);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
totalSize += size;
|
|
593
|
+
files.push({ path: rel, content });
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// skip unreadable files
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return { files, truncated, skippedFiles };
|
|
600
|
+
}
|
|
601
|
+
function handleIntegrateApiError(error, label) {
|
|
602
|
+
if (axios.isAxiosError(error)) {
|
|
603
|
+
if (error.response?.status === 401) {
|
|
604
|
+
throw new AuthRequiredError();
|
|
605
|
+
}
|
|
606
|
+
if (error.response?.status === 403) {
|
|
607
|
+
throw new MagicPathError('A Pro subscription is required for AI integration. Visit https://www.magicpath.ai/documentation/help/plans to see the plans available.');
|
|
608
|
+
}
|
|
609
|
+
if (error.response?.status === 404) {
|
|
610
|
+
throw new MagicPathError('Component not found. Please check the component name and try again.');
|
|
611
|
+
}
|
|
612
|
+
if (error.code === 'ECONNABORTED') {
|
|
613
|
+
throw new MagicPathError(`${label} request timed out. Please try again.`);
|
|
614
|
+
}
|
|
615
|
+
throw new MagicPathError(`${label} failed: ${error.message}`);
|
|
616
|
+
}
|
|
617
|
+
throw new MagicPathError(`${label} failed. Please check your internet connection and try again.`);
|
|
618
|
+
}
|
|
619
|
+
export async function requestIntegration(request) {
|
|
620
|
+
const url = makeIntegrateUrl();
|
|
621
|
+
let headers;
|
|
622
|
+
try {
|
|
623
|
+
headers = getAuthHeaders();
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
throw new AuthRequiredError();
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const response = await axios.post(url, request, {
|
|
630
|
+
headers,
|
|
631
|
+
timeout: 240000,
|
|
632
|
+
});
|
|
633
|
+
return response.data.data;
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
handleIntegrateApiError(error, 'Integration');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
export async function requestRetheme(request) {
|
|
640
|
+
const url = makeIntegrateUrl();
|
|
641
|
+
let headers;
|
|
642
|
+
try {
|
|
643
|
+
headers = getAuthHeaders();
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
throw new AuthRequiredError();
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
const response = await axios.post(url, request, {
|
|
650
|
+
headers,
|
|
651
|
+
timeout: 240000,
|
|
652
|
+
maxBodyLength: 5 * 1024 * 1024,
|
|
653
|
+
});
|
|
654
|
+
return response.data.data;
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
handleIntegrateApiError(error, 'Retheme');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=integrate.js.map
|