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.
Files changed (60) hide show
  1. package/README.md +80 -46
  2. package/bin/kalo.ts +3 -2
  3. package/package.json +2 -1
  4. package/plopfile.ts +182 -25
  5. package/dist/docs/app.js +0 -42644
  6. package/dist/docs/index.html +0 -32
  7. package/generators/constants.ts +0 -52
  8. package/generators/generator/django-app/index.ts +0 -72
  9. package/generators/generator/django-app/templates/admin.py.hbs +0 -6
  10. package/generators/generator/django-app/templates/apps.py.hbs +0 -9
  11. package/generators/generator/django-app/templates/init.py.hbs +0 -0
  12. package/generators/generator/django-app/templates/models_init.py.hbs +0 -2
  13. package/generators/generator/django-app/templates/urls.py.hbs +0 -8
  14. package/generators/generator/django-app/templates/views.py.hbs +0 -5
  15. package/generators/generator/django-channel/index.ts +0 -80
  16. package/generators/generator/django-channel/templates/consumer.py.hbs +0 -47
  17. package/generators/generator/django-channel/templates/routing.py.hbs +0 -8
  18. package/generators/generator/django-form/index.ts +0 -64
  19. package/generators/generator/django-form/templates/form.py.hbs +0 -15
  20. package/generators/generator/django-form/templates/forms_file.py.hbs +0 -6
  21. package/generators/generator/django-form/templates/model_form.py.hbs +0 -19
  22. package/generators/generator/django-view/index.ts +0 -96
  23. package/generators/generator/django-view/templates/view_cbv.py.hbs +0 -14
  24. package/generators/generator/django-view/templates/view_fbv.py.hbs +0 -10
  25. package/generators/generator/django-view/templates/view_template.html.hbs +0 -8
  26. package/generators/generator/main/index.ts +0 -70
  27. package/generators/generator/wagtail-admin/index.ts +0 -124
  28. package/generators/generator/wagtail-admin/templates/admin_view.html.hbs +0 -21
  29. package/generators/generator/wagtail-admin/templates/admin_view.py.hbs +0 -15
  30. package/generators/generator/wagtail-admin/templates/component.html.hbs +0 -6
  31. package/generators/generator/wagtail-admin/templates/component.py.hbs +0 -11
  32. package/generators/generator/wagtail-admin/templates/wagtail_hooks.py.hbs +0 -18
  33. package/generators/generator/wagtail-block/index.ts +0 -53
  34. package/generators/generator/wagtail-block/templates/block_class.py.hbs +0 -16
  35. package/generators/generator/wagtail-block/templates/block_template.html.hbs +0 -5
  36. package/generators/generator/wagtail-page/actions/model.ts +0 -20
  37. package/generators/generator/wagtail-page/actions/orderable.ts +0 -23
  38. package/generators/generator/wagtail-page/actions/page.ts +0 -42
  39. package/generators/generator/wagtail-page/actions/snippet.ts +0 -20
  40. package/generators/generator/wagtail-page/index.ts +0 -63
  41. package/generators/generator/wagtail-page/templates/django_model.py.hbs +0 -21
  42. package/generators/generator/wagtail-page/templates/orderable_model.py.hbs +0 -27
  43. package/generators/generator/wagtail-page/templates/page_pair_model.py.hbs +0 -69
  44. package/generators/generator/wagtail-page/templates/page_template.html.hbs +0 -14
  45. package/generators/generator/wagtail-page/templates/snippet_model.py.hbs +0 -29
  46. package/generators/ia/ai-enhancer/index.ts +0 -319
  47. package/generators/ia/docs/index.ts +0 -36
  48. package/generators/ia/docs/keywords.json +0 -1158
  49. package/generators/ia/help/index.ts +0 -85
  50. package/generators/main/index.ts +0 -422
  51. package/generators/utils/ai/common.ts +0 -141
  52. package/generators/utils/ai/index.ts +0 -2
  53. package/generators/utils/analysis.ts +0 -88
  54. package/generators/utils/code-manipulation.ts +0 -229
  55. package/generators/utils/config.ts +0 -43
  56. package/generators/utils/filesystem.ts +0 -131
  57. package/generators/utils/index.ts +0 -35
  58. package/generators/utils/plop-actions.ts +0 -104
  59. package/generators/utils/search.ts +0 -24
  60. 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
- }