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