kalo-cli 0.1.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 (56) hide show
  1. package/README.md +47 -0
  2. package/bin/kalo.ts +17 -0
  3. package/generators/ai-enhancer/index.ts +281 -0
  4. package/generators/ai-enhancer/keywords.json +1158 -0
  5. package/generators/constants.ts +52 -0
  6. package/generators/django-app/index.ts +67 -0
  7. package/generators/django-app/templates/admin.py.hbs +6 -0
  8. package/generators/django-app/templates/apps.py.hbs +9 -0
  9. package/generators/django-app/templates/init.py.hbs +0 -0
  10. package/generators/django-app/templates/models_init.py.hbs +2 -0
  11. package/generators/django-app/templates/urls.py.hbs +8 -0
  12. package/generators/django-app/templates/views.py.hbs +5 -0
  13. package/generators/django-channel/index.ts +78 -0
  14. package/generators/django-channel/templates/consumer.py.hbs +47 -0
  15. package/generators/django-channel/templates/routing.py.hbs +8 -0
  16. package/generators/django-form/index.ts +62 -0
  17. package/generators/django-form/templates/form.py.hbs +12 -0
  18. package/generators/django-form/templates/forms_file.py.hbs +6 -0
  19. package/generators/django-form/templates/model_form.py.hbs +18 -0
  20. package/generators/django-view/index.ts +95 -0
  21. package/generators/django-view/templates/view_cbv.py.hbs +11 -0
  22. package/generators/django-view/templates/view_fbv.py.hbs +7 -0
  23. package/generators/django-view/templates/view_template.html.hbs +8 -0
  24. package/generators/docs/index.ts +36 -0
  25. package/generators/help/index.ts +84 -0
  26. package/generators/main/index.ts +429 -0
  27. package/generators/utils/ai/common.ts +141 -0
  28. package/generators/utils/ai/index.ts +2 -0
  29. package/generators/utils/analysis.ts +82 -0
  30. package/generators/utils/code-manipulation.ts +119 -0
  31. package/generators/utils/filesystem.ts +64 -0
  32. package/generators/utils/index.ts +47 -0
  33. package/generators/utils/plop-actions.ts +61 -0
  34. package/generators/utils/search.ts +24 -0
  35. package/generators/wagtail-admin/index.ts +122 -0
  36. package/generators/wagtail-admin/templates/admin_view.html.hbs +21 -0
  37. package/generators/wagtail-admin/templates/admin_view.py.hbs +15 -0
  38. package/generators/wagtail-admin/templates/component.html.hbs +6 -0
  39. package/generators/wagtail-admin/templates/component.py.hbs +11 -0
  40. package/generators/wagtail-admin/templates/wagtail_hooks.py.hbs +18 -0
  41. package/generators/wagtail-block/index.ts +55 -0
  42. package/generators/wagtail-block/templates/block_class.py.hbs +13 -0
  43. package/generators/wagtail-block/templates/block_template.html.hbs +5 -0
  44. package/generators/wagtail-page/actions/model.ts +18 -0
  45. package/generators/wagtail-page/actions/orderable.ts +21 -0
  46. package/generators/wagtail-page/actions/page.ts +40 -0
  47. package/generators/wagtail-page/actions/snippet.ts +19 -0
  48. package/generators/wagtail-page/index.ts +63 -0
  49. package/generators/wagtail-page/templates/django_model.py.hbs +18 -0
  50. package/generators/wagtail-page/templates/orderable_model.py.hbs +21 -0
  51. package/generators/wagtail-page/templates/page_pair_model.py.hbs +62 -0
  52. package/generators/wagtail-page/templates/page_template.html.hbs +14 -0
  53. package/generators/wagtail-page/templates/snippet_model.py.hbs +24 -0
  54. package/package.json +47 -0
  55. package/plopfile.ts +26 -0
  56. package/tsconfig.json +29 -0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Kalo - Générateur Django & Wagtail
2
+
3
+ CLI pour générer du code structuré et uniforme pour Django et Wagtail.
4
+
5
+ ## Prérequis
6
+
7
+ Ce projet est conçu pour fonctionner avec **Bun**. Vous devez avoir [Bun](https://bun.sh) installé sur votre machine.
8
+
9
+ ```bash
10
+ # Installation de Bun (Mac, Linux, WSL)
11
+ curl -fsSL https://bun.sh/install | bash
12
+
13
+ # Installation de Bun (Windows)
14
+ powershell -c "irm bun.sh/install.ps1 | iex"
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ### Via Bun (Recommandé)
20
+
21
+ ```bash
22
+ bun add -d kalo-cli
23
+ # ou globalement
24
+ bun add -g kalo-cli
25
+ ```
26
+
27
+
28
+ ## Utilisation
29
+
30
+ ### Si installé globalement
31
+
32
+ ```bash
33
+ kalo
34
+ ```
35
+
36
+ ### Si installé dans un projet
37
+
38
+ Utilisez `bun run` ou `bunx` :
39
+
40
+ ```bash
41
+ bun run kalo
42
+ # ou
43
+ bunx kalo
44
+ ```
45
+
46
+ Si vous utilisez `npx`, assurez-vous que l'environnement peut exécuter le script (nécessite un environnement compatible avec le shebang `#!/usr/bin/env bun`, généralement Mac/Linux/WSL). Sur Windows standard, préférez `bunx`.
47
+
package/bin/kalo.ts ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bun
2
+ import path from "node:path";
3
+ import minimist from "minimist";
4
+ import { Plop, run } from "plop";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const args = process.argv.slice(2);
8
+ const argv = minimist(args);
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ Plop.prepare({
13
+ cwd: argv.cwd,
14
+ configPath: path.join(__dirname, '../plopfile.ts'),
15
+ preload: argv.require || [],
16
+ completion: argv.completion
17
+ }, env => Plop.execute(env, (env) => run(env, undefined, true)));
@@ -0,0 +1,281 @@
1
+
2
+ import { PlopGeneratorConfig } from 'plop';
3
+ import { generateWithModel } from '../utils/ai/common';
4
+ import { injectMarkers, applyGeneratedCode } from '../utils/code-manipulation';
5
+ import { Glob } from "bun";
6
+ import { searchChoices } from '../utils/search';
7
+ import { AI_PROVIDER_TYPES, AI_TEMP_TYPES, BACK_VALUE, EXIT_VALUE } from '../constants';
8
+ import keywordConfig from './keywords.json';
9
+
10
+ const getHelpItems = (input: string | undefined): any[] => {
11
+ const items: any[] = [];
12
+
13
+ if (input) {
14
+ const inputLower = input.toLowerCase();
15
+ let lastMatchIndex = -1;
16
+ let matchedInstruction = null;
17
+
18
+ const keywords = Array.isArray(keywordConfig) ? keywordConfig : (keywordConfig as any).keywords || [];
19
+
20
+ for (const entry of keywords) {
21
+ if (entry.terms && Array.isArray(entry.terms)) {
22
+ for (const term of entry.terms) {
23
+ const index = inputLower.lastIndexOf(term.toLowerCase());
24
+ if (index !== -1 && index >= lastMatchIndex) {
25
+ lastMatchIndex = index;
26
+ matchedInstruction = entry.instruction;
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ if (matchedInstruction) {
33
+ // Split instruction into lines of max 80 chars for better visibility
34
+ const words = matchedInstruction.split(' ');
35
+ let currentLine = '💡 ';
36
+
37
+ for (const word of words) {
38
+ if ((currentLine + word).length > 80) {
39
+ items.push({ name: currentLine, value: 'INFO', disabled: true });
40
+ currentLine = ' ' + word + ' ';
41
+ } else {
42
+ currentLine += word + ' ';
43
+ }
44
+ }
45
+ if (currentLine.trim()) {
46
+ items.push({ name: currentLine, value: 'INFO', disabled: true });
47
+ }
48
+ items.push({ name: '────────────────────────────────────────', value: 'SEP', disabled: true });
49
+ }
50
+ }
51
+ return items;
52
+ };
53
+
54
+ export const helpGenerator: PlopGeneratorConfig = {
55
+ description: 'Search for AI instructions and keywords',
56
+ prompts: [
57
+ {
58
+ type: 'autocomplete',
59
+ name: 'query',
60
+ message: 'Type a keyword to see associated instructions (e.g. "model", "field"):',
61
+ suggestOnly: true,
62
+ source: async (answers, input) => {
63
+ const choices: any[] = [];
64
+
65
+ const helpItems = getHelpItems(input);
66
+ if (helpItems.length > 0) {
67
+ choices.push(...helpItems);
68
+ }
69
+
70
+ if (!input) {
71
+ choices.push({ name: 'Type to search...', value: 'INFO', disabled: true });
72
+ choices.push({ name: 'Exit', value: EXIT_VALUE });
73
+ return choices;
74
+ }
75
+
76
+ choices.push({ name: 'Exit', value: EXIT_VALUE });
77
+ return choices;
78
+ },
79
+ validate: (value) => {
80
+ if (value === EXIT_VALUE || value?.toLowerCase() === 'exit') {
81
+ return true;
82
+ }
83
+ return true;
84
+ }
85
+ }
86
+ ],
87
+ actions: []
88
+ };
89
+
90
+ const handleInput = (value: any) => {
91
+ if (value === EXIT_VALUE || (typeof value === 'string' && value.toLowerCase() === 'exit')) {
92
+ return EXIT_VALUE;
93
+ }
94
+ return value;
95
+ };
96
+
97
+ const isBack = (answers: any) => {
98
+ return Object.values(answers).includes(BACK_VALUE) || Object.values(answers).includes(EXIT_VALUE);
99
+ };
100
+
101
+ export const aiEnhancerGenerator: PlopGeneratorConfig = {
102
+ description: 'Enhance existing models with AI generated code',
103
+ prompts: [
104
+ {
105
+ type: 'autocomplete',
106
+ name: 'filePath',
107
+ message: 'Select the model file to enhance:',
108
+ source: async (answers, input) => {
109
+ const glob = new Glob("**/*.py");
110
+ const files: string[] = [];
111
+ // Scans the current directory
112
+ for await (const file of glob.scan(".")) {
113
+ // Filter for likely python model files or snippets
114
+ if (file.endsWith('.py') && (file.includes("models") || file.includes("snippet"))) {
115
+ files.push(file);
116
+ }
117
+ }
118
+ const choices = files.map(f => ({ name: f, value: f }));
119
+ const results = await searchChoices(input, choices);
120
+ results.push({ name: 'Exit', value: EXIT_VALUE });
121
+ return results;
122
+ },
123
+ filter: handleInput
124
+ },
125
+ {
126
+ type: 'autocomplete',
127
+ name: 'instruction',
128
+ message: 'What modification/addition do you want to generate?',
129
+ when: (answers) => !isBack(answers),
130
+ suggestOnly: true,
131
+ source: async (answers, input) => {
132
+ // We provide a "Exit" option even for input by using autocomplete with suggestOnly
133
+ if (!input) return [{ name: 'Exit', value: EXIT_VALUE }];
134
+ return [{ name: 'Exit', value: EXIT_VALUE }];
135
+ },
136
+ validate: (value) => {
137
+ if (value === EXIT_VALUE || value?.toLowerCase() === 'exit') return true;
138
+ return value ? true : 'Instruction is required';
139
+ },
140
+ filter: handleInput
141
+ },
142
+ {
143
+ type: 'autocomplete',
144
+ name: 'aiProvider',
145
+ message: 'Select AI Provider:',
146
+ default: 'Qwen',
147
+ when: (answers) => !isBack(answers),
148
+ source: async (answers, input) => {
149
+ const choices = AI_PROVIDER_TYPES.map(p => ({ name: p, value: p }));
150
+ const results = await searchChoices(input, choices);
151
+ results.push({ name: 'Exit', value: EXIT_VALUE });
152
+ return results;
153
+ },
154
+ filter: handleInput
155
+ },
156
+ {
157
+ type: 'autocomplete',
158
+ name: 'temperature',
159
+ message: 'Select Creativity Level:',
160
+ default: 0.5,
161
+ when: (answers) => !isBack(answers),
162
+ source: async (answers, input) => {
163
+ const results = await searchChoices(input, AI_TEMP_TYPES);
164
+ results.push({ name: 'Exit', value: EXIT_VALUE });
165
+ return results;
166
+ },
167
+ filter: handleInput
168
+ },
169
+ {
170
+ type: 'autocomplete',
171
+ name: 'verbosity',
172
+ message: 'Select Verbosity Level:',
173
+ default: 'standard',
174
+ when: (answers) => !isBack(answers),
175
+ source: async (answers, input) => {
176
+ const choices = [
177
+ { name: 'Minimal (Code only)', value: 'minimal' },
178
+ { name: 'Standard (Code + basic comments)', value: 'standard' },
179
+ { name: 'Verbose (Detailed docs)', value: 'verbose' }
180
+ ];
181
+ const results = await searchChoices(input, choices);
182
+ results.push({ name: 'Exit', value: EXIT_VALUE });
183
+ return results;
184
+ },
185
+ filter: handleInput
186
+ },
187
+ {
188
+ type: 'input',
189
+ name: 'constraints',
190
+ message: 'Terms to listen to / Constraints (comma separated, optional):',
191
+ when: (answers) => !isBack(answers),
192
+ filter: (value) => {
193
+ if (value === EXIT_VALUE || (typeof value === 'string' && value.toLowerCase() === 'exit')) {
194
+ return EXIT_VALUE;
195
+ }
196
+ return value.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
197
+ }
198
+ }
199
+ ],
200
+ actions: (data) => {
201
+ // Handle "Back" action (Abort generator)
202
+ if (Object.values(data).includes(BACK_VALUE) || Object.values(data).includes(EXIT_VALUE)) {
203
+ return [];
204
+ }
205
+
206
+ return [
207
+ {
208
+ type: 'modify',
209
+ path: '{{filePath}}',
210
+ transform: async (fileContent, data) => {
211
+ const className = data.targetClass || 'AI_GENERATED';
212
+
213
+ // 1. Inject Markers if missing
214
+ const contentWithMarkers = injectMarkers(fileContent, className);
215
+
216
+ // Keyword analysis for enhanced context
217
+ const keywordInstructions: string[] = [];
218
+ const instructionLower = data.instruction.toLowerCase();
219
+
220
+ const keywords = Array.isArray(keywordConfig) ? keywordConfig : (keywordConfig as any).keywords || [];
221
+ for (const keyword of keywords) {
222
+ if (keyword.term && instructionLower.includes(keyword.term)) {
223
+ keywordInstructions.push(keyword.instruction);
224
+ } else if (keyword.terms) {
225
+ if (keyword.terms.some((t: string) => instructionLower.includes(t))) {
226
+ keywordInstructions.push(keyword.instruction);
227
+ }
228
+ }
229
+ }
230
+
231
+ // Build options object
232
+ const options = {
233
+ temperature: data.temperature,
234
+ constraints: [
235
+ ...(data.constraints || []),
236
+ `Target Class: ${className}`,
237
+ "Output Format: JSON with keys: 'imports' (string[]), 'replacements' ({search, replace}[]), 'code' (string).",
238
+ "Rules:",
239
+ "1. 'imports': New import statements.",
240
+ "2. 'replacements': exact 'search' string to find and 'replace' string to overwrite. Use for modifying existing lists/dicts.",
241
+ "3. 'code': New methods/logic to be injected into the class.",
242
+ ...keywordInstructions
243
+ ],
244
+ verbosity: data.verbosity,
245
+ plopContext: data,
246
+ responseFormat: 'json'
247
+ };
248
+
249
+ let generatedResponseString = '';
250
+
251
+ try {
252
+ const providerName = data.aiProvider || 'Qwen';
253
+ const modelCommand = providerName.toLowerCase();
254
+
255
+ generatedResponseString = await generateWithModel(
256
+ contentWithMarkers, // context
257
+ data.instruction, // instruction
258
+ modelCommand,
259
+ providerName,
260
+ options as any
261
+ );
262
+ } catch (error) {
263
+ console.error('AI Generation Failed:', error);
264
+ return fileContent;
265
+ }
266
+
267
+ let generatedData;
268
+ try {
269
+ generatedData = JSON.parse(generatedResponseString);
270
+ } catch (e) {
271
+ console.error('Failed to parse AI response as JSON.');
272
+ return contentWithMarkers;
273
+ }
274
+
275
+ // 2. Apply Changes
276
+ return applyGeneratedCode(contentWithMarkers, generatedData, className);
277
+ }
278
+ }
279
+ ]
280
+ }
281
+ };