carto-md 1.0.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/.cartoignore +16 -0
- package/package.json +35 -0
- package/src/agents/formatter.js +150 -0
- package/src/agents/merger.js +70 -0
- package/src/cli/index.js +43 -0
- package/src/cli/init.js +80 -0
- package/src/cli/sync.js +27 -0
- package/src/cli/watch.js +52 -0
- package/src/detector/files.js +92 -0
- package/src/detector/framework.js +117 -0
- package/src/extractors/dbtables.js +80 -0
- package/src/extractors/envvars.js +40 -0
- package/src/extractors/filemap.js +36 -0
- package/src/extractors/frontend.js +43 -0
- package/src/extractors/functions.js +69 -0
- package/src/extractors/languages/html.js +15 -0
- package/src/extractors/languages/javascript.js +376 -0
- package/src/extractors/languages/prisma.js +80 -0
- package/src/extractors/languages/python.js +26 -0
- package/src/extractors/languages/typescript.js +235 -0
- package/src/extractors/loader.js +67 -0
- package/src/extractors/models.js +40 -0
- package/src/extractors/routes.js +66 -0
- package/src/security/ignore.js +66 -0
- package/src/sync.js +180 -0
- package/src/watcher/watch.js +46 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma schema plugin — handles .prisma files.
|
|
3
|
+
* Pure regex — Prisma schema is simple enough.
|
|
4
|
+
*/
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'prisma',
|
|
7
|
+
extensions: ['.prisma'],
|
|
8
|
+
extract(content, filename) {
|
|
9
|
+
try {
|
|
10
|
+
const models = extractPrismaModels(content);
|
|
11
|
+
const dbTables = extractPrismaDBTables(content);
|
|
12
|
+
return {
|
|
13
|
+
routes: [],
|
|
14
|
+
models: models,
|
|
15
|
+
functions: [],
|
|
16
|
+
envVars: [],
|
|
17
|
+
dbTables: dbTables,
|
|
18
|
+
fetches: [],
|
|
19
|
+
storageKeys: [],
|
|
20
|
+
};
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn(`[CARTO] prisma plugin error on ${filename}: ${err.message}`);
|
|
23
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function extractPrismaModels(content) {
|
|
29
|
+
const models = [];
|
|
30
|
+
const modelPattern = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
|
|
31
|
+
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = modelPattern.exec(content)) !== null) {
|
|
34
|
+
const className = match[1];
|
|
35
|
+
const body = match[2];
|
|
36
|
+
const fields = [];
|
|
37
|
+
|
|
38
|
+
const lines = body.split('\n');
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
// Skip empty lines, comments, and @@directives
|
|
42
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
|
|
43
|
+
|
|
44
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+(\w+[\[\]?]*)/);
|
|
45
|
+
if (fieldMatch) {
|
|
46
|
+
fields.push({ name: fieldMatch[1], type: fieldMatch[2] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
models.push({ className, fields });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return models;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractPrismaDBTables(content) {
|
|
57
|
+
const tables = [];
|
|
58
|
+
const modelPattern = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
|
|
59
|
+
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = modelPattern.exec(content)) !== null) {
|
|
62
|
+
const modelName = match[1];
|
|
63
|
+
const body = match[2];
|
|
64
|
+
|
|
65
|
+
// Check for @@map('table_name')
|
|
66
|
+
const mapMatch = body.match(/@@map\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
67
|
+
const tableName = mapMatch ? mapMatch[1] : toSnakeCase(modelName);
|
|
68
|
+
|
|
69
|
+
tables.push({ tableName, modelName });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return tables;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toSnakeCase(str) {
|
|
76
|
+
return str
|
|
77
|
+
.replace(/([A-Z])/g, '_$1')
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/^_/, '');
|
|
80
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { extractRoutes } = require('../routes');
|
|
2
|
+
const { extractModels } = require('../models');
|
|
3
|
+
const { extractFunctions } = require('../functions');
|
|
4
|
+
const { extractEnvVars } = require('../envvars');
|
|
5
|
+
const { extractDBTables } = require('../dbtables');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
name: 'python',
|
|
9
|
+
extensions: ['.py'],
|
|
10
|
+
extract(content, filename) {
|
|
11
|
+
try {
|
|
12
|
+
return {
|
|
13
|
+
routes: extractRoutes(content),
|
|
14
|
+
models: extractModels(content),
|
|
15
|
+
functions: extractFunctions(content, filename),
|
|
16
|
+
envVars: extractEnvVars(content),
|
|
17
|
+
dbTables: extractDBTables(content),
|
|
18
|
+
fetches: [],
|
|
19
|
+
storageKeys: [],
|
|
20
|
+
};
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn(`[CARTO] python plugin error on ${filename}: ${err.message}`);
|
|
23
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const parser = require('@babel/parser');
|
|
2
|
+
const jsPlugin = require('./javascript');
|
|
3
|
+
|
|
4
|
+
const TS_PARSE_OPTIONS = {
|
|
5
|
+
sourceType: 'module',
|
|
6
|
+
allowImportExportEverywhere: true,
|
|
7
|
+
allowReturnOutsideFunction: true,
|
|
8
|
+
plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties', 'optionalChaining', 'nullishCoalescingOperator']
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
name: 'typescript',
|
|
13
|
+
extensions: ['.ts', '.tsx'],
|
|
14
|
+
extract(content, filename) {
|
|
15
|
+
let ast;
|
|
16
|
+
try {
|
|
17
|
+
ast = parser.parse(content, TS_PARSE_OPTIONS);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.warn(`[CARTO] TS parse failed on ${filename}: ${err.message} — skipping`);
|
|
20
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
routes: jsPlugin._extractExpressRoutes(ast, filename),
|
|
25
|
+
models: extractTSInterfaces(ast),
|
|
26
|
+
functions: extractTSFunctions(ast),
|
|
27
|
+
envVars: jsPlugin._extractProcessEnv(ast),
|
|
28
|
+
dbTables: [],
|
|
29
|
+
fetches: jsPlugin._extractJSFetches(ast),
|
|
30
|
+
storageKeys: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// AST traversal helper
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function walk(node, visitor) {
|
|
40
|
+
if (!node || typeof node !== 'object') return;
|
|
41
|
+
if (Array.isArray(node)) {
|
|
42
|
+
for (const child of node) walk(child, visitor);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (node.type) {
|
|
46
|
+
visitor(node);
|
|
47
|
+
}
|
|
48
|
+
for (const key of Object.keys(node)) {
|
|
49
|
+
if (key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments') continue;
|
|
50
|
+
const child = node[key];
|
|
51
|
+
if (child && typeof child === 'object') {
|
|
52
|
+
walk(child, visitor);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// TS function extraction (with return types)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function extractTSFunctions(ast) {
|
|
62
|
+
const functions = [];
|
|
63
|
+
|
|
64
|
+
if (!ast.program || !ast.program.body) return functions;
|
|
65
|
+
|
|
66
|
+
for (const node of ast.program.body) {
|
|
67
|
+
extractFuncFromNode(node, functions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return functions;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractFuncFromNode(node, functions) {
|
|
74
|
+
// FunctionDeclaration
|
|
75
|
+
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
76
|
+
const name = node.id.name;
|
|
77
|
+
if (shouldSkip(name, node.params)) return;
|
|
78
|
+
functions.push({
|
|
79
|
+
name,
|
|
80
|
+
params: extractTSParams(node.params),
|
|
81
|
+
returnType: extractReturnType(node)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// VariableDeclaration
|
|
86
|
+
if (node.type === 'VariableDeclaration') {
|
|
87
|
+
for (const decl of node.declarations) {
|
|
88
|
+
if (!decl.id || !decl.id.name || !decl.init) continue;
|
|
89
|
+
if (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression') {
|
|
90
|
+
const name = decl.id.name;
|
|
91
|
+
if (shouldSkip(name, decl.init.params)) continue;
|
|
92
|
+
functions.push({
|
|
93
|
+
name,
|
|
94
|
+
params: extractTSParams(decl.init.params),
|
|
95
|
+
returnType: extractReturnType(decl.init)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ExportDefaultDeclaration
|
|
102
|
+
if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
103
|
+
extractFuncFromNode(node.declaration, functions);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ExportNamedDeclaration
|
|
107
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
108
|
+
extractFuncFromNode(node.declaration, functions);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function shouldSkip(name, params) {
|
|
113
|
+
if (name.startsWith('_') && (!params || params.length === 0)) return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractTSParams(params) {
|
|
118
|
+
if (!params || params.length === 0) return '\u2014';
|
|
119
|
+
const names = [];
|
|
120
|
+
for (const p of params) {
|
|
121
|
+
if (p.type === 'Identifier') {
|
|
122
|
+
names.push(p.name);
|
|
123
|
+
} else if (p.type === 'AssignmentPattern' && p.left && p.left.type === 'Identifier') {
|
|
124
|
+
names.push(p.left.name);
|
|
125
|
+
} else if (p.type === 'RestElement' && p.argument && p.argument.type === 'Identifier') {
|
|
126
|
+
continue; // skip ...args
|
|
127
|
+
} else if (p.type === 'ObjectPattern') {
|
|
128
|
+
names.push('{...}');
|
|
129
|
+
} else if (p.type === 'ArrayPattern') {
|
|
130
|
+
names.push('[...]');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return names.length > 0 ? names.join(', ') : '\u2014';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractReturnType(funcNode) {
|
|
137
|
+
// Check for TSTypeAnnotation on the function's returnType
|
|
138
|
+
if (funcNode.returnType && funcNode.returnType.typeAnnotation) {
|
|
139
|
+
return typeToString(funcNode.returnType.typeAnnotation);
|
|
140
|
+
}
|
|
141
|
+
return '\u2014';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function typeToString(typeNode) {
|
|
145
|
+
if (!typeNode) return '\u2014';
|
|
146
|
+
|
|
147
|
+
switch (typeNode.type) {
|
|
148
|
+
case 'TSStringKeyword': return 'string';
|
|
149
|
+
case 'TSNumberKeyword': return 'number';
|
|
150
|
+
case 'TSBooleanKeyword': return 'boolean';
|
|
151
|
+
case 'TSVoidKeyword': return 'void';
|
|
152
|
+
case 'TSAnyKeyword': return 'any';
|
|
153
|
+
case 'TSNullKeyword': return 'null';
|
|
154
|
+
case 'TSUndefinedKeyword': return 'undefined';
|
|
155
|
+
case 'TSNeverKeyword': return 'never';
|
|
156
|
+
case 'TSObjectKeyword': return 'object';
|
|
157
|
+
case 'TSUnknownKeyword': return 'unknown';
|
|
158
|
+
case 'TSTypeReference':
|
|
159
|
+
if (typeNode.typeName && typeNode.typeName.name) {
|
|
160
|
+
const generics = typeNode.typeParameters
|
|
161
|
+
? `<${typeNode.typeParameters.params.map(typeToString).join(', ')}>`
|
|
162
|
+
: '';
|
|
163
|
+
return typeNode.typeName.name + generics;
|
|
164
|
+
}
|
|
165
|
+
return '\u2014';
|
|
166
|
+
case 'TSArrayType':
|
|
167
|
+
return typeToString(typeNode.elementType) + '[]';
|
|
168
|
+
case 'TSUnionType':
|
|
169
|
+
return typeNode.types.map(typeToString).join(' | ');
|
|
170
|
+
case 'TSIntersectionType':
|
|
171
|
+
return typeNode.types.map(typeToString).join(' & ');
|
|
172
|
+
case 'TSTypeLiteral':
|
|
173
|
+
return 'object';
|
|
174
|
+
case 'TSFunctionType':
|
|
175
|
+
return 'Function';
|
|
176
|
+
default:
|
|
177
|
+
return '\u2014';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// TS interface/type extraction → same shape as Pydantic models
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
function extractTSInterfaces(ast) {
|
|
186
|
+
const models = [];
|
|
187
|
+
|
|
188
|
+
if (!ast.program || !ast.program.body) return models;
|
|
189
|
+
|
|
190
|
+
for (const node of ast.program.body) {
|
|
191
|
+
let target = node;
|
|
192
|
+
// Unwrap exports
|
|
193
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
194
|
+
target = node.declaration;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TSInterfaceDeclaration
|
|
198
|
+
if (target.type === 'TSInterfaceDeclaration' && target.id) {
|
|
199
|
+
const className = target.id.name;
|
|
200
|
+
const fields = [];
|
|
201
|
+
if (target.body && target.body.body) {
|
|
202
|
+
for (const member of target.body.body) {
|
|
203
|
+
if (member.type === 'TSPropertySignature' && member.key) {
|
|
204
|
+
const name = member.key.name || member.key.value;
|
|
205
|
+
const type = member.typeAnnotation
|
|
206
|
+
? typeToString(member.typeAnnotation.typeAnnotation)
|
|
207
|
+
: '\u2014';
|
|
208
|
+
if (name) fields.push({ name, type });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
models.push({ className, fields });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// TSTypeAliasDeclaration with TSTypeLiteral
|
|
216
|
+
if (target.type === 'TSTypeAliasDeclaration' && target.id && target.typeAnnotation) {
|
|
217
|
+
if (target.typeAnnotation.type === 'TSTypeLiteral') {
|
|
218
|
+
const className = target.id.name;
|
|
219
|
+
const fields = [];
|
|
220
|
+
for (const member of target.typeAnnotation.members) {
|
|
221
|
+
if (member.type === 'TSPropertySignature' && member.key) {
|
|
222
|
+
const name = member.key.name || member.key.value;
|
|
223
|
+
const type = member.typeAnnotation
|
|
224
|
+
? typeToString(member.typeAnnotation.typeAnnotation)
|
|
225
|
+
: '\u2014';
|
|
226
|
+
if (name) fields.push({ name, type });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
models.push({ className, fields });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return models;
|
|
235
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* loadLanguagePlugins() → Array<plugin>
|
|
6
|
+
*
|
|
7
|
+
* Auto-discovers all .js files in src/extractors/languages/
|
|
8
|
+
* Validates each has: name (string), extensions (array), extract (function)
|
|
9
|
+
* Logs a warning and skips any plugin that fails validation or throws on require()
|
|
10
|
+
*/
|
|
11
|
+
function loadLanguagePlugins() {
|
|
12
|
+
const pluginsDir = path.join(__dirname, 'languages');
|
|
13
|
+
const plugins = [];
|
|
14
|
+
|
|
15
|
+
let files;
|
|
16
|
+
try {
|
|
17
|
+
files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.warn(`[CARTO] Warning: Could not read plugins directory: ${err.message}`);
|
|
20
|
+
return plugins;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const fullPath = path.join(pluginsDir, file);
|
|
25
|
+
try {
|
|
26
|
+
const plugin = require(fullPath);
|
|
27
|
+
|
|
28
|
+
// Validate plugin shape
|
|
29
|
+
if (typeof plugin.name !== 'string') {
|
|
30
|
+
console.warn(`[CARTO] Warning: Plugin ${file} missing 'name' (string) — skipping`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(plugin.extensions)) {
|
|
34
|
+
console.warn(`[CARTO] Warning: Plugin ${file} missing 'extensions' (array) — skipping`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (typeof plugin.extract !== 'function') {
|
|
38
|
+
console.warn(`[CARTO] Warning: Plugin ${file} missing 'extract' (function) — skipping`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
plugins.push(plugin);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn(`[CARTO] Warning: Failed to load plugin ${file}: ${err.message} — skipping`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return plugins;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* getPluginForFile(plugins, filePath) → plugin | null
|
|
53
|
+
*
|
|
54
|
+
* Returns the first plugin whose extensions array includes the file's extension.
|
|
55
|
+
* Returns null if no plugin handles this file type.
|
|
56
|
+
*/
|
|
57
|
+
function getPluginForFile(plugins, filePath) {
|
|
58
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
59
|
+
for (const plugin of plugins) {
|
|
60
|
+
if (plugin.extensions.includes(ext)) {
|
|
61
|
+
return plugin;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { loadLanguagePlugins, getPluginForFile };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts Pydantic model class definitions — class name + fields with types.
|
|
3
|
+
* Skips method definitions (def ...) to avoid false positives.
|
|
4
|
+
* Strips inline # comments from field types.
|
|
5
|
+
*/
|
|
6
|
+
function extractModels(content) {
|
|
7
|
+
const models = [];
|
|
8
|
+
const classPattern = /^class\s+(\w+)\s*\(.*BaseModel.*\)\s*:/gm;
|
|
9
|
+
const fieldPattern = /^\s{4}(\w+)\s*:\s*(.+?)(?:\s*=.*)?$/;
|
|
10
|
+
|
|
11
|
+
let classMatch;
|
|
12
|
+
while ((classMatch = classPattern.exec(content)) !== null) {
|
|
13
|
+
const className = classMatch[1];
|
|
14
|
+
const classStart = classMatch.index + classMatch[0].length;
|
|
15
|
+
const fields = [];
|
|
16
|
+
|
|
17
|
+
const remaining = content.substring(classStart);
|
|
18
|
+
const bodyLines = remaining.split('\n');
|
|
19
|
+
|
|
20
|
+
for (const line of bodyLines) {
|
|
21
|
+
// Stop at next top-level definition
|
|
22
|
+
if (/^class\s/.test(line) || (/^\S/.test(line) && line.trim() !== '')) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
// Skip method definitions — prevents false-positive on `def validate_...(self):`
|
|
26
|
+
if (/^\s{4}def\s/.test(line)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const fieldMatch = line.match(fieldPattern);
|
|
30
|
+
if (fieldMatch) {
|
|
31
|
+
fields.push({ name: fieldMatch[1], type: fieldMatch[2].replace(/#.*$/, '').trim() });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
models.push({ className, fields });
|
|
36
|
+
}
|
|
37
|
+
return models;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { extractModels };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Joins multiline decorator expressions into single lines.
|
|
5
|
+
* Scans for lines starting with @ and, if parentheses are unbalanced,
|
|
6
|
+
* appends subsequent lines until they balance.
|
|
7
|
+
*/
|
|
8
|
+
function collapseMultilineDecorators(content) {
|
|
9
|
+
const lines = content.split('\n');
|
|
10
|
+
const result = [];
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
if (/^\s*@/.test(lines[i])) {
|
|
14
|
+
let combined = lines[i];
|
|
15
|
+
let openParens = (combined.match(/\(/g) || []).length;
|
|
16
|
+
let closeParens = (combined.match(/\)/g) || []).length;
|
|
17
|
+
|
|
18
|
+
while (openParens > closeParens && i + 1 < lines.length) {
|
|
19
|
+
i++;
|
|
20
|
+
combined += ' ' + lines[i].trim();
|
|
21
|
+
openParens = (combined.match(/\(/g) || []).length;
|
|
22
|
+
closeParens = (combined.match(/\)/g) || []).length;
|
|
23
|
+
}
|
|
24
|
+
result.push(combined);
|
|
25
|
+
} else {
|
|
26
|
+
result.push(lines[i]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result.join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extracts HTTP route definitions from FastAPI Python files.
|
|
34
|
+
* Handles @app.get/post/put/delete/patch and @router.get/post/put/delete/patch,
|
|
35
|
+
* including multiline decorators.
|
|
36
|
+
*/
|
|
37
|
+
function extractRoutes(content) {
|
|
38
|
+
const routes = [];
|
|
39
|
+
const decoratorPattern = /@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']/gi;
|
|
40
|
+
const funcPattern = /(?:async\s+)?def\s+(\w+)/;
|
|
41
|
+
|
|
42
|
+
const collapsed = collapseMultilineDecorators(content);
|
|
43
|
+
const lines = collapsed.split('\n');
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const match = decoratorPattern.exec(lines[i]);
|
|
47
|
+
if (match) {
|
|
48
|
+
// Look ahead up to 5 lines for the function definition
|
|
49
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
50
|
+
const funcMatch = lines[j].match(funcPattern);
|
|
51
|
+
if (funcMatch) {
|
|
52
|
+
routes.push({
|
|
53
|
+
method: match[1].toUpperCase(),
|
|
54
|
+
path: match[2],
|
|
55
|
+
functionName: funcMatch[1]
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
decoratorPattern.lastIndex = 0;
|
|
62
|
+
}
|
|
63
|
+
return routes;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { extractRoutes, collapseMultilineDecorators };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
5
|
+
'.env',
|
|
6
|
+
'.env.*',
|
|
7
|
+
'*secret*',
|
|
8
|
+
'*SECRET*',
|
|
9
|
+
'*password*',
|
|
10
|
+
'*PASSWORD*',
|
|
11
|
+
'*credential*',
|
|
12
|
+
'*CREDENTIAL*',
|
|
13
|
+
'*private_key*',
|
|
14
|
+
'*PRIVATE_KEY*',
|
|
15
|
+
'*.pem',
|
|
16
|
+
'*.key'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* parseCartoIgnore(projectRoot) → isIgnored(filePath) → boolean
|
|
21
|
+
*
|
|
22
|
+
* Reads .cartoignore from the project root (if it exists) and merges with defaults.
|
|
23
|
+
* Returns a function that checks if a file path matches any ignore pattern.
|
|
24
|
+
*/
|
|
25
|
+
function parseCartoIgnore(projectRoot) {
|
|
26
|
+
let userPatterns = [];
|
|
27
|
+
|
|
28
|
+
const ignoreFile = path.join(projectRoot, '.cartoignore');
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(ignoreFile, 'utf-8');
|
|
31
|
+
userPatterns = content
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(line => line.trim())
|
|
34
|
+
.filter(line => line && !line.startsWith('#'));
|
|
35
|
+
} catch {
|
|
36
|
+
// No .cartoignore file — that's fine, use defaults only
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const allPatterns = [...DEFAULT_IGNORE_PATTERNS, ...userPatterns];
|
|
40
|
+
|
|
41
|
+
return function isIgnored(filePath) {
|
|
42
|
+
const basename = path.basename(filePath);
|
|
43
|
+
const relativePath = filePath; // can be absolute or relative
|
|
44
|
+
|
|
45
|
+
for (const pattern of allPatterns) {
|
|
46
|
+
if (matchPattern(basename, pattern) || matchPattern(relativePath, pattern)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Simple glob matching supporting * as wildcard.
|
|
56
|
+
*/
|
|
57
|
+
function matchPattern(str, pattern) {
|
|
58
|
+
// Escape regex special chars except *
|
|
59
|
+
const regexStr = pattern
|
|
60
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
61
|
+
.replace(/\*/g, '.*');
|
|
62
|
+
const regex = new RegExp(`^${regexStr}$`, 'i');
|
|
63
|
+
return regex.test(str);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { parseCartoIgnore, DEFAULT_IGNORE_PATTERNS };
|