kalo-cli 0.2.27 → 0.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 +80 -46
- package/bin/kalo.ts +3 -2
- package/package.json +2 -1
- package/plopfile.ts +182 -25
- package/dist/docs/app.js +0 -42644
- package/dist/docs/index.html +0 -32
- package/generators/constants.ts +0 -52
- package/generators/generator/django-app/index.ts +0 -72
- package/generators/generator/django-app/templates/admin.py.hbs +0 -6
- package/generators/generator/django-app/templates/apps.py.hbs +0 -9
- package/generators/generator/django-app/templates/init.py.hbs +0 -0
- package/generators/generator/django-app/templates/models_init.py.hbs +0 -2
- package/generators/generator/django-app/templates/urls.py.hbs +0 -8
- package/generators/generator/django-app/templates/views.py.hbs +0 -5
- package/generators/generator/django-channel/index.ts +0 -80
- package/generators/generator/django-channel/templates/consumer.py.hbs +0 -47
- package/generators/generator/django-channel/templates/routing.py.hbs +0 -8
- package/generators/generator/django-form/index.ts +0 -64
- package/generators/generator/django-form/templates/form.py.hbs +0 -15
- package/generators/generator/django-form/templates/forms_file.py.hbs +0 -6
- package/generators/generator/django-form/templates/model_form.py.hbs +0 -19
- package/generators/generator/django-view/index.ts +0 -96
- package/generators/generator/django-view/templates/view_cbv.py.hbs +0 -14
- package/generators/generator/django-view/templates/view_fbv.py.hbs +0 -10
- package/generators/generator/django-view/templates/view_template.html.hbs +0 -8
- package/generators/generator/main/index.ts +0 -70
- package/generators/generator/wagtail-admin/index.ts +0 -124
- package/generators/generator/wagtail-admin/templates/admin_view.html.hbs +0 -21
- package/generators/generator/wagtail-admin/templates/admin_view.py.hbs +0 -15
- package/generators/generator/wagtail-admin/templates/component.html.hbs +0 -6
- package/generators/generator/wagtail-admin/templates/component.py.hbs +0 -11
- package/generators/generator/wagtail-admin/templates/wagtail_hooks.py.hbs +0 -18
- package/generators/generator/wagtail-block/index.ts +0 -53
- package/generators/generator/wagtail-block/templates/block_class.py.hbs +0 -16
- package/generators/generator/wagtail-block/templates/block_template.html.hbs +0 -5
- package/generators/generator/wagtail-page/actions/model.ts +0 -20
- package/generators/generator/wagtail-page/actions/orderable.ts +0 -23
- package/generators/generator/wagtail-page/actions/page.ts +0 -42
- package/generators/generator/wagtail-page/actions/snippet.ts +0 -20
- package/generators/generator/wagtail-page/index.ts +0 -63
- package/generators/generator/wagtail-page/templates/django_model.py.hbs +0 -21
- package/generators/generator/wagtail-page/templates/orderable_model.py.hbs +0 -27
- package/generators/generator/wagtail-page/templates/page_pair_model.py.hbs +0 -69
- package/generators/generator/wagtail-page/templates/page_template.html.hbs +0 -14
- package/generators/generator/wagtail-page/templates/snippet_model.py.hbs +0 -29
- package/generators/ia/ai-enhancer/index.ts +0 -319
- package/generators/ia/docs/index.ts +0 -36
- package/generators/ia/docs/keywords.json +0 -1158
- package/generators/ia/help/index.ts +0 -85
- package/generators/main/index.ts +0 -422
- package/generators/utils/ai/common.ts +0 -141
- package/generators/utils/ai/index.ts +0 -2
- package/generators/utils/analysis.ts +0 -88
- package/generators/utils/code-manipulation.ts +0 -229
- package/generators/utils/config.ts +0 -43
- package/generators/utils/filesystem.ts +0 -131
- package/generators/utils/index.ts +0 -35
- package/generators/utils/plop-actions.ts +0 -104
- package/generators/utils/search.ts +0 -24
- package/tsconfig.json +0 -29
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { resolveAppPaths } from './filesystem.ts';
|
|
4
|
-
|
|
5
|
-
export interface AppFileInfo {
|
|
6
|
-
fileName: string;
|
|
7
|
-
filePath: string;
|
|
8
|
-
classes: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Analyzes the selected application to find the main file (models.py) and extract class definitions.
|
|
13
|
-
* @param appName The name of the application (directory name)
|
|
14
|
-
* @param config Configuration object
|
|
15
|
-
* @returns AppFileInfo containing filename, absolute path, and list of class names, or null if not found.
|
|
16
|
-
*/
|
|
17
|
-
export const analyzeAppFile = (appName: string, config?: any): AppFileInfo | null => {
|
|
18
|
-
const { appPath } = resolveAppPaths(config, appName);
|
|
19
|
-
|
|
20
|
-
// Priority 1: models.py (Standard Django)
|
|
21
|
-
let targetFile = 'models.py';
|
|
22
|
-
let targetPath = path.join(appPath!, targetFile);
|
|
23
|
-
|
|
24
|
-
// Check if file exists
|
|
25
|
-
if (!fs.existsSync(targetPath)) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
30
|
-
const classes = extractClassNames(content);
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
fileName: targetFile,
|
|
34
|
-
filePath: targetPath,
|
|
35
|
-
classes: classes
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Analyzes a specific file within an application.
|
|
41
|
-
* @param appName The name of the application
|
|
42
|
-
* @param relativeFilePath The relative path of the file from the app root
|
|
43
|
-
* @param config Configuration object
|
|
44
|
-
* @returns AppFileInfo containing filename, absolute path, and list of class names, or null if not found.
|
|
45
|
-
*/
|
|
46
|
-
export const analyzeFile = (appName: string, relativeFilePath: string, config?: any): AppFileInfo | null => {
|
|
47
|
-
const { appPath } = resolveAppPaths(config, appName);
|
|
48
|
-
const targetPath = path.join(appPath!, relativeFilePath);
|
|
49
|
-
|
|
50
|
-
if (!fs.existsSync(targetPath)) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
55
|
-
const classes = extractClassNames(content);
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
fileName: path.basename(relativeFilePath),
|
|
59
|
-
filePath: targetPath,
|
|
60
|
-
classes: classes
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Extracts class names from Python file content using Regex.
|
|
66
|
-
* Ignores comments and handles basic indentation (though top-level is preferred).
|
|
67
|
-
* @param content File content
|
|
68
|
-
*/
|
|
69
|
-
export const extractClassNames = (content: string): string[] => {
|
|
70
|
-
// Nettoyer les commentaires pour éviter les faux positifs
|
|
71
|
-
const cleanContent = content.replace(/#.*$/gm, '');
|
|
72
|
-
|
|
73
|
-
// Regex to match "class ClassName"
|
|
74
|
-
// ^\s*class\s+ -> start of line, optional whitespace, "class" keyword, whitespace
|
|
75
|
-
// (?<name>\w+) -> capture group for name
|
|
76
|
-
// .*?: -> match rest of line until colon (inheritance etc)
|
|
77
|
-
const classRegex = /^\s*class\s+(?<name>\w+)/gm;
|
|
78
|
-
const classes: string[] = [];
|
|
79
|
-
let match;
|
|
80
|
-
|
|
81
|
-
while ((match = classRegex.exec(cleanContent)) !== null) {
|
|
82
|
-
if (match.groups && match.groups.name && match.groups.name !== 'Meta') {
|
|
83
|
-
classes.push(match.groups.name);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return classes;
|
|
88
|
-
};
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export const getMarkerStart = (className: string, context: string = 'AI_GENERATED') => `# <!-- CLASS = ${className} START ${context} -->`;
|
|
3
|
-
export const getMarkerEnd = (className: string, context: string = 'AI_GENERATED') => `# <!-- CLASS = ${className} END ${context} -->`;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Injects markers into the file content to guide AI code generation.
|
|
7
|
-
* Tries to place markers inside the specified class, preferably before 'class Meta' or at the top of the class.
|
|
8
|
-
*/
|
|
9
|
-
export const injectMarkers = (fileContent: string, className: string, context?: string): string => {
|
|
10
|
-
const markerStart = getMarkerStart(className, context);
|
|
11
|
-
const markerEnd = getMarkerEnd(className, context);
|
|
12
|
-
|
|
13
|
-
// Check if markers exist
|
|
14
|
-
if (fileContent.includes(markerStart) || fileContent.includes(markerEnd)) {
|
|
15
|
-
return fileContent;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
console.log(`Markers for class ${className} not found, attempting to inject...`);
|
|
19
|
-
|
|
20
|
-
// More robust regex to find class definition - handles both spaces and tabs
|
|
21
|
-
const classRegex = new RegExp(`^(\\s*)class\\s+${className}\\s*(\\(|:|$)`, 'm');
|
|
22
|
-
const classMatch = fileContent.match(classRegex);
|
|
23
|
-
|
|
24
|
-
if (classMatch) {
|
|
25
|
-
const classIndentation = classMatch[1] || '';
|
|
26
|
-
// Determine indentation based on the class definition - could be spaces or tabs
|
|
27
|
-
const nextCharAfterColon = fileContent.substr(classMatch.index! + classMatch[0].length, 1);
|
|
28
|
-
let classBodyStartIndex = classMatch.index! + classMatch[0].length;
|
|
29
|
-
|
|
30
|
-
// Skip whitespace after the colon to find the actual class body start
|
|
31
|
-
if (nextCharAfterColon === ':') {
|
|
32
|
-
classBodyStartIndex++;
|
|
33
|
-
// Skip any whitespace/newlines until the first actual content
|
|
34
|
-
while (classBodyStartIndex < fileContent.length && /\s/.test(fileContent[classBodyStartIndex])) {
|
|
35
|
-
classBodyStartIndex++;
|
|
36
|
-
if (fileContent[classBodyStartIndex] === '\n') {
|
|
37
|
-
classBodyStartIndex++;
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Look for the first non-empty line after the class definition to determine indentation
|
|
44
|
-
let nextLineStart = classBodyStartIndex;
|
|
45
|
-
let nextLineEnd = fileContent.indexOf('\n', nextLineStart);
|
|
46
|
-
if (nextLineEnd === -1) nextLineEnd = fileContent.length;
|
|
47
|
-
|
|
48
|
-
let nextLine = fileContent.substring(nextLineStart, nextLineEnd);
|
|
49
|
-
let detectedIndentation = nextLine.match(/^\s*/)?.[0] || classIndentation + ' '; // Default to 4 spaces if none detected
|
|
50
|
-
|
|
51
|
-
// If the next line is empty or just whitespace, look for the first actual content line
|
|
52
|
-
if (nextLine.trim() === '') {
|
|
53
|
-
let searchIndex = nextLineEnd;
|
|
54
|
-
while (searchIndex < fileContent.length) {
|
|
55
|
-
const lineStart = searchIndex;
|
|
56
|
-
const lineEnd = fileContent.indexOf('\n', lineStart);
|
|
57
|
-
|
|
58
|
-
if (lineEnd === -1) break; // End of file
|
|
59
|
-
|
|
60
|
-
const line = fileContent.substring(lineStart, lineEnd);
|
|
61
|
-
const trimmedLine = line.trim();
|
|
62
|
-
|
|
63
|
-
if (trimmedLine !== '') {
|
|
64
|
-
// Found a non-empty line, determine its indentation
|
|
65
|
-
detectedIndentation = line.match(/^\s*/)?.[0] || classIndentation + ' ';
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
searchIndex = lineEnd + 1;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Search for 'class Meta' indented relative to our class
|
|
74
|
-
const escapedIndentation = detectedIndentation ? detectedIndentation.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
|
|
75
|
-
const metaRegex = new RegExp(`^${escapedIndentation}class\\s+Meta\\s*(:|\\()`, 'm');
|
|
76
|
-
const contentAfterClassStart = fileContent.substring(classBodyStartIndex);
|
|
77
|
-
const metaMatch = contentAfterClassStart.match(metaRegex);
|
|
78
|
-
|
|
79
|
-
if (metaMatch) {
|
|
80
|
-
// Inject before class Meta
|
|
81
|
-
const absoluteMetaIndex = classBodyStartIndex + metaMatch.index!;
|
|
82
|
-
const beforeMeta = fileContent.substring(0, absoluteMetaIndex);
|
|
83
|
-
const afterMeta = fileContent.substring(absoluteMetaIndex);
|
|
84
|
-
|
|
85
|
-
return `${beforeMeta}${detectedIndentation}${markerStart}\n${detectedIndentation}${markerEnd}\n\n${afterMeta}`;
|
|
86
|
-
} else {
|
|
87
|
-
// Inject at the top of the class body
|
|
88
|
-
const injection = `\n${detectedIndentation}${markerStart}\n${detectedIndentation}${markerEnd}\n`;
|
|
89
|
-
const beforeClassBody = fileContent.substring(0, classBodyStartIndex);
|
|
90
|
-
const afterClassBody = fileContent.substring(classBodyStartIndex);
|
|
91
|
-
|
|
92
|
-
return beforeClassBody + injection + afterClassBody;
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
console.warn(`Class "${className}" not found in file. Appending markers to end of file.`);
|
|
96
|
-
return fileContent + `\n\n${markerStart}\n${markerEnd}\n`;
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Applies the AI-generated JSON response to the content.
|
|
102
|
-
* Handles imports, replacements, and code injection between markers.
|
|
103
|
-
*/
|
|
104
|
-
export const applyGeneratedCode = (contentWithMarkers: string, generatedData: any, className: string, context?: string): string => {
|
|
105
|
-
const markerStart = getMarkerStart(className, context);
|
|
106
|
-
const markerEnd = getMarkerEnd(className, context);
|
|
107
|
-
|
|
108
|
-
let newContent = contentWithMarkers;
|
|
109
|
-
const { imports = [], replacements = [], code = '' } = generatedData;
|
|
110
|
-
|
|
111
|
-
// 1. Handle Imports
|
|
112
|
-
if (imports.length > 0) {
|
|
113
|
-
const newImports = imports.filter((imp: string) => !newContent.includes(imp.trim()));
|
|
114
|
-
if (newImports.length > 0) {
|
|
115
|
-
// Find the end of existing imports section to insert new imports
|
|
116
|
-
const importSectionEnd = findImportSectionEnd(newContent);
|
|
117
|
-
if (importSectionEnd !== -1) {
|
|
118
|
-
const beforeImports = newContent.substring(0, importSectionEnd);
|
|
119
|
-
const afterImports = newContent.substring(importSectionEnd);
|
|
120
|
-
newContent = beforeImports + newImports.join('\n') + '\n' + afterImports;
|
|
121
|
-
} else {
|
|
122
|
-
// If no import section found, prepend to file
|
|
123
|
-
newContent = newImports.join('\n') + '\n' + newContent;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 2. Handle Replacements - Use more robust replacement logic
|
|
129
|
-
if (replacements.length > 0) {
|
|
130
|
-
for (const rep of replacements) {
|
|
131
|
-
if (rep && rep.search && rep.replace !== undefined) {
|
|
132
|
-
// Use global replacement to handle multiple occurrences if needed
|
|
133
|
-
const searchPattern = new RegExp(escapeRegExp(rep.search), 'g');
|
|
134
|
-
|
|
135
|
-
if (searchPattern.test(newContent)) {
|
|
136
|
-
newContent = newContent.replace(searchPattern, rep.replace);
|
|
137
|
-
} else {
|
|
138
|
-
// If exact string not found, try fuzzy matching
|
|
139
|
-
const fuzzyMatch = findFuzzyMatch(newContent, rep.search);
|
|
140
|
-
if (fuzzyMatch) {
|
|
141
|
-
newContent = newContent.substring(0, fuzzyMatch.start) +
|
|
142
|
-
rep.replace +
|
|
143
|
-
newContent.substring(fuzzyMatch.end);
|
|
144
|
-
} else {
|
|
145
|
-
console.warn(`Replacement pattern not found: "${rep.search.substring(0, 40)}..."`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// 3. Handle Code Injection
|
|
153
|
-
let newCode = code;
|
|
154
|
-
if (Array.isArray(newCode)) newCode = newCode.join('\n');
|
|
155
|
-
|
|
156
|
-
if (typeof newCode === 'string' && newCode.trim().length > 0) {
|
|
157
|
-
const startIdx = newContent.indexOf(markerStart);
|
|
158
|
-
const endIdx = newContent.indexOf(markerEnd);
|
|
159
|
-
|
|
160
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
161
|
-
const prefix = newContent.substring(0, startIdx + markerStart.length);
|
|
162
|
-
const suffix = newContent.substring(endIdx);
|
|
163
|
-
|
|
164
|
-
// Detect indentation from the line before start marker
|
|
165
|
-
const lastNewLine = newContent.lastIndexOf('\n', startIdx);
|
|
166
|
-
const indentation = (lastNewLine !== -1)
|
|
167
|
-
? newContent.substring(lastNewLine + 1, startIdx)
|
|
168
|
-
: ' ';
|
|
169
|
-
|
|
170
|
-
// Indent new code to match marker indentation
|
|
171
|
-
const indentedCode = newCode.split('\n')
|
|
172
|
-
.map(line => line.trim() ? indentation + line : line)
|
|
173
|
-
.join('\n');
|
|
174
|
-
|
|
175
|
-
return `${prefix}\n${indentedCode}\n${suffix}`;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return newContent;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Helper function to find the end of import section
|
|
183
|
-
function findImportSectionEnd(content: string): number {
|
|
184
|
-
// Find the end of consecutive import statements
|
|
185
|
-
const lines = content.split('\n');
|
|
186
|
-
let lastImportLine = -1;
|
|
187
|
-
|
|
188
|
-
for (let i = 0; i < lines.length; i++) {
|
|
189
|
-
const line = lines[i].trim();
|
|
190
|
-
if (line.startsWith('import ') || line.startsWith('from ')) {
|
|
191
|
-
lastImportLine = i;
|
|
192
|
-
} else if (line !== '' && !line.startsWith('#')) {
|
|
193
|
-
// Stop at first non-import, non-empty, non-comment line
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (lastImportLine !== -1) {
|
|
199
|
-
// Return position after the last import line and any blank lines
|
|
200
|
-
for (let i = lastImportLine + 1; i < lines.length; i++) {
|
|
201
|
-
if (lines[i].trim() !== '') {
|
|
202
|
-
return content.indexOf(lines[i], content.split('\n').slice(0, i).join('\n').length + i);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// If import section goes to end of file, return length
|
|
206
|
-
return content.length;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return -1;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Helper function for fuzzy matching
|
|
213
|
-
function findFuzzyMatch(content: string, searchStr: string): { start: number; end: number } | null {
|
|
214
|
-
// Simple fuzzy matching - look for substring with some tolerance for whitespace differences
|
|
215
|
-
const normalizedSearch = searchStr.replace(/\s+/g, '\\s+');
|
|
216
|
-
const fuzzyRegex = new RegExp(normalizedSearch, 'g');
|
|
217
|
-
const match = fuzzyRegex.exec(content);
|
|
218
|
-
|
|
219
|
-
if (match) {
|
|
220
|
-
return { start: match.index, end: match.index + match[0].length };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Helper function to escape special regex characters
|
|
227
|
-
function escapeRegExp(string: string): string {
|
|
228
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
229
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { findProjectRoot } from './filesystem';
|
|
4
|
-
|
|
5
|
-
export interface KaloConfig {
|
|
6
|
-
aiProvider?: string;
|
|
7
|
-
temperature?: number;
|
|
8
|
-
verbosity?: 'minimal' | 'standard' | 'verbose';
|
|
9
|
-
appDir?: string;
|
|
10
|
-
[key: string]: any;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Charge la configuration à partir de kalo.config.json ou kalo.config.js
|
|
15
|
-
*/
|
|
16
|
-
export async function loadConfig(): Promise<KaloConfig> {
|
|
17
|
-
const rootDir = findProjectRoot();
|
|
18
|
-
const jsonPath = path.join(rootDir, 'kalo.config.json');
|
|
19
|
-
const jsPath = path.join(rootDir, 'kalo.config.js');
|
|
20
|
-
|
|
21
|
-
if (fs.existsSync(jsonPath)) {
|
|
22
|
-
try {
|
|
23
|
-
const content = fs.readFileSync(jsonPath, 'utf8');
|
|
24
|
-
return JSON.parse(content);
|
|
25
|
-
} catch (e) {
|
|
26
|
-
console.error(`Erreur lors de la lecture de ${jsonPath}:`, e);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (fs.existsSync(jsPath)) {
|
|
31
|
-
try {
|
|
32
|
-
// Utilisation de import dynamique pour le fichier .js
|
|
33
|
-
// Note: On utilise un timestamp pour éviter le cache de module si nécessaire en dev,
|
|
34
|
-
// mais ici un import simple devrait suffire.
|
|
35
|
-
const module = await import(jsPath);
|
|
36
|
-
return module.default || module;
|
|
37
|
-
} catch (e) {
|
|
38
|
-
console.error(`Erreur lors du chargement de ${jsPath}:`, e);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {};
|
|
43
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Recherche le répertoire racine du projet en remontant à partir d'un dossier donné.
|
|
6
|
-
* Cherche la présence d'un fichier package.json valide (avec name et version).
|
|
7
|
-
* Vérifie également les permissions d'écriture.
|
|
8
|
-
*
|
|
9
|
-
* @param startDir - Le dossier de départ pour la recherche.
|
|
10
|
-
* @returns Le chemin absolu vers la racine du projet.
|
|
11
|
-
*/
|
|
12
|
-
export const findProjectRoot = (startDir: string = process.cwd()): string => {
|
|
13
|
-
let currentDir = startDir;
|
|
14
|
-
|
|
15
|
-
while (currentDir !== path.parse(currentDir).root) {
|
|
16
|
-
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
17
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
18
|
-
try {
|
|
19
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
20
|
-
|
|
21
|
-
// Validation basique du package.json
|
|
22
|
-
if (!pkg.name || !pkg.version) {
|
|
23
|
-
// Package.json invalide, on continue
|
|
24
|
-
currentDir = path.dirname(currentDir);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Si on trouve un package.json qui n'est pas celui de kalo-cli, c'est probablement la racine du projet utilisateur
|
|
29
|
-
// Ou si on est dans un projet qui n'a pas encore de kalo-cli installé mais qui a un package.json
|
|
30
|
-
if (pkg.name !== 'kalo-cli' || currentDir.includes('node_modules')) {
|
|
31
|
-
// Si on est dans node_modules, on doit continuer à monter car on a trouvé le package.json de kalo-cli
|
|
32
|
-
if (currentDir.includes('node_modules')) {
|
|
33
|
-
currentDir = path.dirname(currentDir);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
return currentDir;
|
|
37
|
-
}
|
|
38
|
-
} catch (e) {
|
|
39
|
-
// Erreur de lecture, on continue
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
currentDir = path.dirname(currentDir);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Fallback: si on ne trouve rien, on retourne le dossier courant
|
|
46
|
-
return startDir;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Résout les chemins liés à l'application.
|
|
51
|
-
*
|
|
52
|
-
* @param config Configuration contenant potentiellement appDir
|
|
53
|
-
* @param appName Nom de l'application (optionnel)
|
|
54
|
-
* @returns Objet contenant les chemins résolus
|
|
55
|
-
*/
|
|
56
|
-
export const resolveAppPaths = (config?: any, appName?: string) => {
|
|
57
|
-
const rootDir = findProjectRoot();
|
|
58
|
-
const appDirName = config?.appDir || 'app';
|
|
59
|
-
const appDir = path.join(rootDir, appDirName);
|
|
60
|
-
const appPath = appName ? path.join(appDir, appName) : undefined;
|
|
61
|
-
|
|
62
|
-
return { rootDir, appDirName, appDir, appPath };
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Retrieves the list of existing Django applications in the project.
|
|
67
|
-
* Scans the 'app' directory in the project root.
|
|
68
|
-
*
|
|
69
|
-
* @returns {string[]} An array of application names (directory names).
|
|
70
|
-
* @throws {Error} If filesystem access fails (though basic read errors might propagate).
|
|
71
|
-
*/
|
|
72
|
-
export const getAppList = (config?: any): string[] => {
|
|
73
|
-
const rootDir = findProjectRoot();
|
|
74
|
-
const appDirName = config?.appDir || 'app';
|
|
75
|
-
const appDir = path.join(rootDir, appDirName);
|
|
76
|
-
if (!fs.existsSync(appDir)) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
return fs.readdirSync(appDir, { withFileTypes: true })
|
|
80
|
-
.filter(dirent => dirent.isDirectory())
|
|
81
|
-
.map(dirent => dirent.name);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Recursively lists files in an application directory with filtering.
|
|
86
|
-
* Excludes test files, __init__.py, and specific directories.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} appName - The name of the application.
|
|
89
|
-
* @param {any} config - Configuration object.
|
|
90
|
-
* @returns {string[]} List of relative file paths from the app root.
|
|
91
|
-
*/
|
|
92
|
-
export const getAppFiles = (appName: string, config?: any): string[] => {
|
|
93
|
-
const rootDir = findProjectRoot();
|
|
94
|
-
const appDirName = config?.appDir || 'app';
|
|
95
|
-
const appDir = path.join(rootDir, appDirName, appName);
|
|
96
|
-
if (!fs.existsSync(appDir)) {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const files: string[] = [];
|
|
101
|
-
|
|
102
|
-
const walk = (dir: string, relativePath: string) => {
|
|
103
|
-
const list = fs.readdirSync(dir, { withFileTypes: true });
|
|
104
|
-
|
|
105
|
-
for (const dirent of list) {
|
|
106
|
-
const currentRelPath = path.join(relativePath, dirent.name);
|
|
107
|
-
const currentAbsPath = path.join(dir, dirent.name);
|
|
108
|
-
|
|
109
|
-
if (dirent.isDirectory()) {
|
|
110
|
-
// Exclude test directories and cache
|
|
111
|
-
if (!['__tests__', 'test', 'tests', '__pycache__', 'migrations'].includes(dirent.name)) {
|
|
112
|
-
walk(currentAbsPath, currentRelPath);
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
// Exclude test files and __init__.py
|
|
116
|
-
if (
|
|
117
|
-
dirent.name !== '__init__.py' &&
|
|
118
|
-
!dirent.name.includes('__pycache__') &&
|
|
119
|
-
!dirent.name.includes('.test.') &&
|
|
120
|
-
!dirent.name.includes('.spec.') &&
|
|
121
|
-
dirent.name.endsWith('.py') // Assume we only care about Python files for now based on context
|
|
122
|
-
) {
|
|
123
|
-
files.push(currentRelPath);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
walk(appDir, '');
|
|
130
|
-
return files;
|
|
131
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for file path resolution and naming conventions.
|
|
3
|
-
* Pure functions only.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Formats a string into a URL-friendly slug.
|
|
8
|
-
* Converts to lowercase, replaces spaces with hyphens, and removes non-word characters.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} text - The input string to format.
|
|
11
|
-
* @returns {string} The formatted slug string.
|
|
12
|
-
*/
|
|
13
|
-
export const formatSlug = (text: string): string =>
|
|
14
|
-
text.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Formats a string into snake_case.
|
|
18
|
-
* Converts PascalCase or camelCase to snake_case.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} text - The input string to format.
|
|
21
|
-
* @returns {string} The string in snake_case.
|
|
22
|
-
*/
|
|
23
|
-
export const formatSnakeCase = (text: string): string =>
|
|
24
|
-
text.replace(/\.?([A-Z])/g, (x, y) => "_" + y.toLowerCase()).replace(/^_/, "");
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Formats a string into PascalCase.
|
|
28
|
-
* Capitalizes the first letter of each word and removes non-alphanumeric characters.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} text - The input string to format.
|
|
31
|
-
* @returns {string} The string in PascalCase.
|
|
32
|
-
*/
|
|
33
|
-
export const formatPascalCase = (text: string): string =>
|
|
34
|
-
text.replace(/(\w)(\w*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase()).replace(/\W/g, "");
|
|
35
|
-
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { ActionType } from 'plop';
|
|
2
|
-
import { extractClassNames } from './analysis.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Ensures a suffix is present on a name.
|
|
6
|
-
*
|
|
7
|
-
* @param {string} name - The name to check.
|
|
8
|
-
* @param {string} suffix - The suffix to ensure.
|
|
9
|
-
* @returns {string} The name with the suffix.
|
|
10
|
-
*/
|
|
11
|
-
export const ensureSuffix = (name: string, suffix: string): string => {
|
|
12
|
-
if (name.endsWith(suffix)) return name;
|
|
13
|
-
return `${name}${suffix}`;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
interface CreateAppendOptions {
|
|
17
|
-
path: string;
|
|
18
|
-
templateFile: string;
|
|
19
|
-
dumbData?: any;
|
|
20
|
-
appendData?: any;
|
|
21
|
-
dumbTemplateFile?: string;
|
|
22
|
-
dumbTemplate?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates a pair of actions: one to ensure the file exists (using a "dumb" template rendering or specific init template),
|
|
27
|
-
* and one to append the actual content.
|
|
28
|
-
*
|
|
29
|
-
* @param {CreateAppendOptions} options - Configuration options.
|
|
30
|
-
* @returns {ActionType[]} An array containing the add and append actions.
|
|
31
|
-
*/
|
|
32
|
-
export const createAppendActions = ({
|
|
33
|
-
path,
|
|
34
|
-
templateFile,
|
|
35
|
-
dumbData = { name: 'Dumb' },
|
|
36
|
-
appendData,
|
|
37
|
-
dumbTemplateFile,
|
|
38
|
-
dumbTemplate
|
|
39
|
-
}: CreateAppendOptions): ActionType[] => {
|
|
40
|
-
const addAction: any = {
|
|
41
|
-
type: 'add',
|
|
42
|
-
path,
|
|
43
|
-
skipIfExists: true,
|
|
44
|
-
data: dumbData,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (dumbTemplate) {
|
|
48
|
-
addAction.template = dumbTemplate;
|
|
49
|
-
} else {
|
|
50
|
-
addAction.templateFile = dumbTemplateFile || templateFile;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return [
|
|
54
|
-
addAction,
|
|
55
|
-
{
|
|
56
|
-
type: 'modify',
|
|
57
|
-
path,
|
|
58
|
-
transform: (content, data) => {
|
|
59
|
-
const className = data.name || data.baseName;
|
|
60
|
-
if (className) {
|
|
61
|
-
const existingClasses = extractClassNames(content);
|
|
62
|
-
// Check for exact class name or suffixed versions if relevant
|
|
63
|
-
if (existingClasses.includes(className)) {
|
|
64
|
-
console.log(`\n[SKIP] Class ${className} already exists in ${path}`);
|
|
65
|
-
return content;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Special case for page_pair which might have multiple classes
|
|
69
|
-
if (data.type === 'page_pair' && data.baseName) {
|
|
70
|
-
const indexClass = `${data.baseName}IndexPage`;
|
|
71
|
-
const detailClass = `${data.baseName}Page`;
|
|
72
|
-
if (existingClasses.includes(indexClass) || existingClasses.includes(detailClass)) {
|
|
73
|
-
console.log(`\n[SKIP] Wagtail Pages for ${data.baseName} already exist in ${path}`);
|
|
74
|
-
return content;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// If not skipping, we append the template
|
|
80
|
-
// Plop's modify transform doesn't automatically render templates,
|
|
81
|
-
// but since we are in a generator action, we can use the data.
|
|
82
|
-
// However, 'append' is better for just adding at the end.
|
|
83
|
-
// To keep it simple, we use a regex to append at the end of file.
|
|
84
|
-
// We use projectRoot as base for resolve if path is relative
|
|
85
|
-
const fs = require('fs');
|
|
86
|
-
const pathLib = require('path');
|
|
87
|
-
|
|
88
|
-
// We need to find the project root or use a reliable base.
|
|
89
|
-
// In plop, the configPath usually points to the plopfile.
|
|
90
|
-
const baseDir = data.plop.getPlopfilePath() ? pathLib.dirname(data.plop.getPlopfilePath()) : process.cwd();
|
|
91
|
-
|
|
92
|
-
const fullTemplatePath = pathLib.isAbsolute(templateFile)
|
|
93
|
-
? templateFile
|
|
94
|
-
: pathLib.resolve(baseDir, templateFile);
|
|
95
|
-
|
|
96
|
-
const template = fs.readFileSync(fullTemplatePath, 'utf8');
|
|
97
|
-
return content.trimEnd() + '\n\n' + data.plop.renderString(
|
|
98
|
-
template,
|
|
99
|
-
{ ...data, ...appendData }
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
];
|
|
104
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility for fuzzy searching/filtering choices in autocomplete prompts.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Filters a list of choices based on the input string.
|
|
7
|
-
* Uses simple case-insensitive substring matching.
|
|
8
|
-
*
|
|
9
|
-
* @param {string | null} input - The search input from the user.
|
|
10
|
-
* @param {Array<{ name: string; value: any }>} choices - The list of choices to filter.
|
|
11
|
-
* @returns {Promise<Array<{ name: string; value: any }>>} The filtered list of choices.
|
|
12
|
-
*/
|
|
13
|
-
export const searchChoices = (input: string | null, choices: Array<{ name: string; value: any }>) => {
|
|
14
|
-
if (!input) {
|
|
15
|
-
return Promise.resolve(choices);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const lowerInput = input.toLowerCase();
|
|
19
|
-
const filtered = choices.filter((choice) =>
|
|
20
|
-
choice.name.toLowerCase().includes(lowerInput)
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
return Promise.resolve(filtered);
|
|
24
|
-
};
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "Preserve",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
// Best practices
|
|
18
|
-
"strict": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
"noUncheckedIndexedAccess": true,
|
|
22
|
-
"noImplicitOverride": true,
|
|
23
|
-
|
|
24
|
-
// Some stricter flags (disabled by default)
|
|
25
|
-
"noUnusedLocals": false,
|
|
26
|
-
"noUnusedParameters": false,
|
|
27
|
-
"noPropertyAccessFromIndexSignature": false
|
|
28
|
-
}
|
|
29
|
-
}
|