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.
- package/README.md +47 -0
- package/bin/kalo.ts +17 -0
- package/generators/ai-enhancer/index.ts +281 -0
- package/generators/ai-enhancer/keywords.json +1158 -0
- package/generators/constants.ts +52 -0
- package/generators/django-app/index.ts +67 -0
- package/generators/django-app/templates/admin.py.hbs +6 -0
- package/generators/django-app/templates/apps.py.hbs +9 -0
- package/generators/django-app/templates/init.py.hbs +0 -0
- package/generators/django-app/templates/models_init.py.hbs +2 -0
- package/generators/django-app/templates/urls.py.hbs +8 -0
- package/generators/django-app/templates/views.py.hbs +5 -0
- package/generators/django-channel/index.ts +78 -0
- package/generators/django-channel/templates/consumer.py.hbs +47 -0
- package/generators/django-channel/templates/routing.py.hbs +8 -0
- package/generators/django-form/index.ts +62 -0
- package/generators/django-form/templates/form.py.hbs +12 -0
- package/generators/django-form/templates/forms_file.py.hbs +6 -0
- package/generators/django-form/templates/model_form.py.hbs +18 -0
- package/generators/django-view/index.ts +95 -0
- package/generators/django-view/templates/view_cbv.py.hbs +11 -0
- package/generators/django-view/templates/view_fbv.py.hbs +7 -0
- package/generators/django-view/templates/view_template.html.hbs +8 -0
- package/generators/docs/index.ts +36 -0
- package/generators/help/index.ts +84 -0
- package/generators/main/index.ts +429 -0
- package/generators/utils/ai/common.ts +141 -0
- package/generators/utils/ai/index.ts +2 -0
- package/generators/utils/analysis.ts +82 -0
- package/generators/utils/code-manipulation.ts +119 -0
- package/generators/utils/filesystem.ts +64 -0
- package/generators/utils/index.ts +47 -0
- package/generators/utils/plop-actions.ts +61 -0
- package/generators/utils/search.ts +24 -0
- package/generators/wagtail-admin/index.ts +122 -0
- package/generators/wagtail-admin/templates/admin_view.html.hbs +21 -0
- package/generators/wagtail-admin/templates/admin_view.py.hbs +15 -0
- package/generators/wagtail-admin/templates/component.html.hbs +6 -0
- package/generators/wagtail-admin/templates/component.py.hbs +11 -0
- package/generators/wagtail-admin/templates/wagtail_hooks.py.hbs +18 -0
- package/generators/wagtail-block/index.ts +55 -0
- package/generators/wagtail-block/templates/block_class.py.hbs +13 -0
- package/generators/wagtail-block/templates/block_template.html.hbs +5 -0
- package/generators/wagtail-page/actions/model.ts +18 -0
- package/generators/wagtail-page/actions/orderable.ts +21 -0
- package/generators/wagtail-page/actions/page.ts +40 -0
- package/generators/wagtail-page/actions/snippet.ts +19 -0
- package/generators/wagtail-page/index.ts +63 -0
- package/generators/wagtail-page/templates/django_model.py.hbs +18 -0
- package/generators/wagtail-page/templates/orderable_model.py.hbs +21 -0
- package/generators/wagtail-page/templates/page_pair_model.py.hbs +62 -0
- package/generators/wagtail-page/templates/page_template.html.hbs +14 -0
- package/generators/wagtail-page/templates/snippet_model.py.hbs +24 -0
- package/package.json +47 -0
- package/plopfile.ts +26 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { NodePlopAPI, PlopGeneratorConfig } from 'plop';
|
|
2
|
+
import { djangoAppGenerator } from '../django-app';
|
|
3
|
+
import { wagtailPageGenerator } from '../wagtail-page';
|
|
4
|
+
import { wagtailBlockGenerator } from '../wagtail-block';
|
|
5
|
+
import { djangoViewGenerator } from '../django-view';
|
|
6
|
+
import { djangoFormGenerator } from '../django-form';
|
|
7
|
+
import { wagtailAdminGenerator } from '../wagtail-admin';
|
|
8
|
+
import { djangoChannelGenerator } from '../django-channel';
|
|
9
|
+
import { aiEnhancerGenerator } from '../ai-enhancer';
|
|
10
|
+
import keywordConfig from '../ai-enhancer/keywords.json';
|
|
11
|
+
import { getAppList, getAppFiles } from '../utils/filesystem';
|
|
12
|
+
import { analyzeAppFile, analyzeFile } from '../utils/analysis';
|
|
13
|
+
import { searchChoices } from '../utils/search';
|
|
14
|
+
import {
|
|
15
|
+
MAIN_COMPONENT_TYPES,
|
|
16
|
+
VIEW_TYPES,
|
|
17
|
+
FORM_TYPES,
|
|
18
|
+
ADMIN_EXT_TYPES,
|
|
19
|
+
CHANNEL_TYPES,
|
|
20
|
+
AI_PROVIDER_TYPES,
|
|
21
|
+
AI_TEMP_TYPES,
|
|
22
|
+
EXIT_VALUE
|
|
23
|
+
} from '../constants';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Main Generator Configuration (Interactive CLI).
|
|
27
|
+
* Acts as the entry point for the interactive wizard.
|
|
28
|
+
* Routes user choices to specific sub-generators (App, Page, Block, View, etc.).
|
|
29
|
+
* Handles creating new apps, adding components, and modifying existing code.
|
|
30
|
+
*
|
|
31
|
+
* @type {PlopGeneratorConfig}
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const getHelpItems = (input: string | undefined): any[] => {
|
|
35
|
+
const items: any[] = [];
|
|
36
|
+
|
|
37
|
+
if (input) {
|
|
38
|
+
const inputLower = input.toLowerCase();
|
|
39
|
+
let lastMatchIndex = -1;
|
|
40
|
+
let matchedInstruction = null;
|
|
41
|
+
|
|
42
|
+
const keywords = Array.isArray(keywordConfig) ? keywordConfig : (keywordConfig as any).keywords || [];
|
|
43
|
+
|
|
44
|
+
for (const entry of keywords) {
|
|
45
|
+
if (entry.terms && Array.isArray(entry.terms)) {
|
|
46
|
+
for (const term of entry.terms) {
|
|
47
|
+
const index = inputLower.lastIndexOf(term.toLowerCase());
|
|
48
|
+
if (index !== -1 && index >= lastMatchIndex) {
|
|
49
|
+
lastMatchIndex = index;
|
|
50
|
+
matchedInstruction = entry.instruction;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (matchedInstruction) {
|
|
57
|
+
// Split instruction into lines of max 80 chars for better visibility
|
|
58
|
+
const words = matchedInstruction.split(' ');
|
|
59
|
+
let currentLine = '💡 ';
|
|
60
|
+
|
|
61
|
+
for (const word of words) {
|
|
62
|
+
if ((currentLine + word).length > 80) {
|
|
63
|
+
items.push({ name: currentLine, value: 'INFO', disabled: true });
|
|
64
|
+
currentLine = ' ' + word + ' ';
|
|
65
|
+
} else {
|
|
66
|
+
currentLine += word + ' ';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (currentLine.trim()) {
|
|
70
|
+
items.push({ name: currentLine, value: 'INFO', disabled: true });
|
|
71
|
+
}
|
|
72
|
+
items.push({ name: '────────────────────────────────────────', value: 'SEP', disabled: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return items;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const helpGenerator: PlopGeneratorConfig = {
|
|
79
|
+
description: 'Search for AI instructions and keywords',
|
|
80
|
+
prompts: [
|
|
81
|
+
{
|
|
82
|
+
type: 'autocomplete',
|
|
83
|
+
name: 'query',
|
|
84
|
+
message: 'Type a keyword to see associated instructions (e.g. "model", "field"):',
|
|
85
|
+
suggestOnly: true,
|
|
86
|
+
source: async (answers, input) => {
|
|
87
|
+
const choices: any[] = [];
|
|
88
|
+
|
|
89
|
+
const helpItems = getHelpItems(input);
|
|
90
|
+
if (helpItems.length > 0) {
|
|
91
|
+
choices.push(...helpItems);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!input) {
|
|
95
|
+
choices.push({ name: 'Type to search...', value: 'INFO', disabled: true });
|
|
96
|
+
choices.push({ name: 'Exit', value: EXIT_VALUE });
|
|
97
|
+
return choices;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
choices.push({ name: 'Exit', value: EXIT_VALUE });
|
|
101
|
+
return choices;
|
|
102
|
+
},
|
|
103
|
+
validate: (value) => {
|
|
104
|
+
if (value === EXIT_VALUE || value?.toLowerCase() === 'exit') {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
actions: []
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const mainGenerator: PlopGeneratorConfig = {
|
|
115
|
+
description: 'CLI Interactif pour créer ou éditer des composants Django/Wagtail',
|
|
116
|
+
prompts: [
|
|
117
|
+
// Step 1: Mode Selection
|
|
118
|
+
{
|
|
119
|
+
type: 'list',
|
|
120
|
+
name: 'mode',
|
|
121
|
+
message: 'Que voulez-vous faire ?',
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: 'Créer une nouvelle application Django', value: 'create' },
|
|
124
|
+
{ name: 'Ajouter des fichiers/fonctionnalités à une application', value: 'add' },
|
|
125
|
+
{ name: 'Modifier un fichier d\'une application via IA', value: 'modify' },
|
|
126
|
+
{ name: 'Aide / Rechercher des instructions IA', value: 'help' },
|
|
127
|
+
{ name: 'Ouvrir la documentation', value: 'docs' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// Step 2a: Create - App Name
|
|
132
|
+
{
|
|
133
|
+
type: 'input',
|
|
134
|
+
name: 'appName',
|
|
135
|
+
message: 'Nom de la nouvelle application Django :',
|
|
136
|
+
when: (answers) => answers.mode === 'create',
|
|
137
|
+
validate: (value) => (/.+/.test(value) ? true : 'Le nom de l\'application est requis'),
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Step 2b: Add/Modify - Select App
|
|
141
|
+
{
|
|
142
|
+
type: 'autocomplete',
|
|
143
|
+
name: 'selectedApp',
|
|
144
|
+
message: 'Sélectionnez l\'application cible :',
|
|
145
|
+
when: (answers) => ['add', 'modify'].includes(answers.mode),
|
|
146
|
+
source: (answers, input) => {
|
|
147
|
+
const apps = getAppList();
|
|
148
|
+
if (apps.length === 0) {
|
|
149
|
+
return Promise.resolve([{ name: 'Aucune application trouvée', value: null }]);
|
|
150
|
+
}
|
|
151
|
+
const appChoices = apps.map(app => ({ name: app, value: app }));
|
|
152
|
+
return searchChoices(input, appChoices);
|
|
153
|
+
},
|
|
154
|
+
filter: (input) => {
|
|
155
|
+
if (!input) return null;
|
|
156
|
+
// Only analyze default models.py if we are NOT in modify mode (or as fallback)
|
|
157
|
+
// But for 'add' mode, we might want it. For 'modify', we will select file explicitly.
|
|
158
|
+
const info = analyzeAppFile(input);
|
|
159
|
+
return { name: input, info };
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Step 3: Add - Component Type
|
|
164
|
+
{
|
|
165
|
+
type: 'autocomplete',
|
|
166
|
+
name: 'componentType',
|
|
167
|
+
message: 'Que voulez-vous ajouter ?',
|
|
168
|
+
when: (answers) => answers.mode === 'add' && answers.selectedApp !== null,
|
|
169
|
+
source: (answers, input) => {
|
|
170
|
+
// Filter out 'ai_enhancer' as it is covered by 'modify' mode
|
|
171
|
+
const choices = MAIN_COMPONENT_TYPES.filter(c => c.value !== 'ai_enhancer');
|
|
172
|
+
// Add Help option
|
|
173
|
+
choices.push({ name: 'Aide / Rechercher des instructions IA', value: 'help' });
|
|
174
|
+
return searchChoices(input, choices);
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// --- SUB-TYPE PROMPTS (Only for 'add' mode) ---
|
|
179
|
+
|
|
180
|
+
// View Types
|
|
181
|
+
{
|
|
182
|
+
type: 'autocomplete',
|
|
183
|
+
name: 'viewType',
|
|
184
|
+
message: 'Quel type de vue voulez-vous créer ?',
|
|
185
|
+
when: (answers) => answers.mode === 'add' && answers.componentType === 'view',
|
|
186
|
+
source: (answers, input) => searchChoices(input, VIEW_TYPES),
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Form Types
|
|
190
|
+
{
|
|
191
|
+
type: 'autocomplete',
|
|
192
|
+
name: 'formType',
|
|
193
|
+
message: 'Quel type de formulaire voulez-vous créer ?',
|
|
194
|
+
when: (answers) => answers.mode === 'add' && answers.componentType === 'form',
|
|
195
|
+
source: (answers, input) => searchChoices(input, FORM_TYPES),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'input',
|
|
199
|
+
name: 'associatedModel',
|
|
200
|
+
message: 'Nom du Modèle associé (ex: UserProfile) :',
|
|
201
|
+
when: (answers) => answers.mode === 'add' && answers.componentType === 'form' && answers.formType === 'model_form',
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Admin Extension Types
|
|
205
|
+
{
|
|
206
|
+
type: 'autocomplete',
|
|
207
|
+
name: 'adminExtType',
|
|
208
|
+
message: 'Quel type d\'extension voulez-vous créer ?',
|
|
209
|
+
when: (answers) => answers.mode === 'add' && answers.componentType === 'admin_ext',
|
|
210
|
+
source: (answers, input) => searchChoices(input, ADMIN_EXT_TYPES),
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// Channel Types
|
|
214
|
+
{
|
|
215
|
+
type: 'autocomplete',
|
|
216
|
+
name: 'channelType',
|
|
217
|
+
message: 'Quel type de consumer ?',
|
|
218
|
+
when: (answers) => answers.mode === 'add' && answers.componentType === 'channel',
|
|
219
|
+
source: (answers, input) => searchChoices(input, CHANNEL_TYPES),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: 'input',
|
|
223
|
+
name: 'channelRoute',
|
|
224
|
+
message: 'Préfixe de la route URL (ex: chat) :',
|
|
225
|
+
when: (answers) => answers.mode === 'add' && answers.componentType === 'channel',
|
|
226
|
+
default: 'chat',
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// --- MODIFY (AI ENHANCER) PROMPTS ---
|
|
230
|
+
{
|
|
231
|
+
type: 'autocomplete',
|
|
232
|
+
name: 'selectedFile',
|
|
233
|
+
message: 'Sélectionnez le fichier à modifier :',
|
|
234
|
+
when: (answers) => answers.mode === 'modify' && answers.selectedApp !== null,
|
|
235
|
+
source: (answers, input) => {
|
|
236
|
+
const appName = answers.selectedApp.name || answers.selectedApp;
|
|
237
|
+
const files = getAppFiles(appName);
|
|
238
|
+
|
|
239
|
+
const choices = [];
|
|
240
|
+
if (files.length > 0) {
|
|
241
|
+
choices.push(...files.map(f => ({ name: f, value: f })));
|
|
242
|
+
} else {
|
|
243
|
+
choices.push({ name: 'Aucun fichier modifiable trouvé', value: null });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Add Help option
|
|
247
|
+
choices.push({ name: 'Aide / Rechercher des instructions IA', value: 'help' });
|
|
248
|
+
|
|
249
|
+
return searchChoices(input, choices);
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: 'autocomplete',
|
|
254
|
+
name: 'targetClass',
|
|
255
|
+
message: 'Sélectionnez la classe à modifier :',
|
|
256
|
+
when: (answers) => answers.mode === 'modify' && answers.selectedFile !== null && answers.selectedFile !== 'help',
|
|
257
|
+
source: (answers, input) => {
|
|
258
|
+
const appName = answers.selectedApp.name || answers.selectedApp;
|
|
259
|
+
const filePath = answers.selectedFile;
|
|
260
|
+
|
|
261
|
+
const info = analyzeFile(appName, filePath);
|
|
262
|
+
if (!info || !info.classes || info.classes.length === 0) {
|
|
263
|
+
return Promise.resolve([{ name: 'Aucune classe trouvée (ou fichier vide)', value: null }]);
|
|
264
|
+
}
|
|
265
|
+
const classChoices = info.classes.map((cls: string) => ({ name: cls, value: cls }));
|
|
266
|
+
return searchChoices(input, classChoices);
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: 'input',
|
|
271
|
+
name: 'instruction',
|
|
272
|
+
message: 'Quelle modification/ajout voulez-vous générer ?',
|
|
273
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
274
|
+
validate: (value) => value ? true : 'Instruction requise'
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: 'list',
|
|
278
|
+
name: 'aiProvider',
|
|
279
|
+
message: 'Sélectionnez le fournisseur IA :',
|
|
280
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
281
|
+
choices: AI_PROVIDER_TYPES,
|
|
282
|
+
default: 'Qwen'
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
type: 'list',
|
|
286
|
+
name: 'temperature',
|
|
287
|
+
message: 'Niveau de créativité :',
|
|
288
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
289
|
+
choices: AI_TEMP_TYPES,
|
|
290
|
+
default: 0.5
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: 'list',
|
|
294
|
+
name: 'verbosity',
|
|
295
|
+
message: 'Niveau de verbosité :',
|
|
296
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
297
|
+
choices: [
|
|
298
|
+
{ name: 'Minimal (Code seulement)', value: 'minimal' },
|
|
299
|
+
{ name: 'Standard (Code + commentaires basiques)', value: 'standard' },
|
|
300
|
+
{ name: 'Verbeux (Documentation détaillée)', value: 'verbose' }
|
|
301
|
+
],
|
|
302
|
+
default: 'standard'
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
type: 'input',
|
|
306
|
+
name: 'constraints',
|
|
307
|
+
message: 'Contraintes (ex: "pas de commentaires", "utiliser snake_case", "docstring en français") :',
|
|
308
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
309
|
+
filter: (value) => value ? value.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0) : []
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
// Step 4: Component Name (For 'add' mode)
|
|
313
|
+
{
|
|
314
|
+
type: 'input',
|
|
315
|
+
name: 'componentName',
|
|
316
|
+
message: (answers) => {
|
|
317
|
+
const type = answers.componentType || 'Composant';
|
|
318
|
+
return `Nom du ${type.charAt(0).toUpperCase() + type.slice(1)} :`;
|
|
319
|
+
},
|
|
320
|
+
when: (answers) => answers.mode === 'add' && answers.selectedApp !== null && answers.componentType !== 'help',
|
|
321
|
+
validate: (value) => (/.+/.test(value) ? true : 'Le nom est requis'),
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// --- HELP PROMPT (Reused from helpGenerator) ---
|
|
325
|
+
{
|
|
326
|
+
...helpGenerator.prompts[0],
|
|
327
|
+
name: 'helpQuery', // Rename to avoid conflict if needed, though 'query' is fine
|
|
328
|
+
when: (answers) =>
|
|
329
|
+
answers.mode === 'help' ||
|
|
330
|
+
answers.componentType === 'help' ||
|
|
331
|
+
answers.selectedFile === 'help'
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
actions: (data) => {
|
|
335
|
+
let generatorActions = [];
|
|
336
|
+
|
|
337
|
+
// Check for help mode first
|
|
338
|
+
if (data.mode === 'help' || data.componentType === 'help' || data.selectedFile === 'help') {
|
|
339
|
+
// Help actions (empty usually, as interaction is in prompt)
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (data.mode === 'docs') {
|
|
344
|
+
return docsGenerator.actions;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (data.mode === 'create') {
|
|
348
|
+
// --- 1. Create New App ---
|
|
349
|
+
data.name = data.appName;
|
|
350
|
+
const genActions = djangoAppGenerator.actions;
|
|
351
|
+
generatorActions = typeof genActions === 'function' ? genActions(data) : genActions;
|
|
352
|
+
|
|
353
|
+
} else if (data.mode === 'add' && data.selectedApp) {
|
|
354
|
+
// --- 2. Add Component to Existing App ---
|
|
355
|
+
data.app = data.selectedApp.name || data.selectedApp;
|
|
356
|
+
data.name = data.componentName;
|
|
357
|
+
|
|
358
|
+
let targetGenerator;
|
|
359
|
+
|
|
360
|
+
if (['page_pair', 'snippet', 'orderable', 'model'].includes(data.componentType)) {
|
|
361
|
+
targetGenerator = wagtailPageGenerator;
|
|
362
|
+
data.type = data.componentType;
|
|
363
|
+
} else if (data.componentType === 'block') {
|
|
364
|
+
targetGenerator = wagtailBlockGenerator;
|
|
365
|
+
} else if (data.componentType === 'view') {
|
|
366
|
+
targetGenerator = djangoViewGenerator;
|
|
367
|
+
data.type = data.viewType;
|
|
368
|
+
} else if (data.componentType === 'form') {
|
|
369
|
+
targetGenerator = djangoFormGenerator;
|
|
370
|
+
data.type = data.formType;
|
|
371
|
+
data.model = data.associatedModel;
|
|
372
|
+
} else if (data.componentType === 'admin_ext') {
|
|
373
|
+
targetGenerator = wagtailAdminGenerator;
|
|
374
|
+
data.type = data.adminExtType;
|
|
375
|
+
} else if (data.componentType === 'channel') {
|
|
376
|
+
targetGenerator = djangoChannelGenerator;
|
|
377
|
+
data.type = data.channelType;
|
|
378
|
+
data.route = data.channelRoute;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (targetGenerator) {
|
|
382
|
+
const genActions = targetGenerator.actions;
|
|
383
|
+
generatorActions = typeof genActions === 'function' ? genActions(data) : genActions;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
} else if (data.mode === 'modify' && data.selectedApp && data.selectedFile) {
|
|
387
|
+
// --- 3. Modify Existing App (AI) ---
|
|
388
|
+
data.app = data.selectedApp.name || data.selectedApp;
|
|
389
|
+
|
|
390
|
+
// Use the selected file path. analyzeFile returns relative path in 'fileName' prompt? No.
|
|
391
|
+
// getAppFiles returns relative path from app root.
|
|
392
|
+
// We need full path for the AI generator.
|
|
393
|
+
const appName = data.app;
|
|
394
|
+
const relativePath = data.selectedFile;
|
|
395
|
+
// We need to reconstruct absolute path or use analyzeFile to get it again.
|
|
396
|
+
// Or simply construct it here.
|
|
397
|
+
// We need 'filePath' for aiEnhancer.
|
|
398
|
+
// Let's use analyzeFile logic or just path.join.
|
|
399
|
+
// Ideally we should have passed the info object in the prompt but inquirer is tricky with dependent values.
|
|
400
|
+
|
|
401
|
+
// Re-analyze or just build path?
|
|
402
|
+
// Since we are in node environment here, we can use path module.
|
|
403
|
+
// However, Plop actions run in a context where we might not have 'path' easily unless we import it.
|
|
404
|
+
// We can use the analyzeFile helper again if needed, or just hardcode construction.
|
|
405
|
+
// Since we imported analyzeFile, we can use it.
|
|
406
|
+
const fileInfo = analyzeFile(appName, relativePath);
|
|
407
|
+
if (fileInfo) {
|
|
408
|
+
data.filePath = fileInfo.filePath;
|
|
409
|
+
} else {
|
|
410
|
+
// Fallback
|
|
411
|
+
data.filePath = `app/${appName}/${relativePath}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const targetGenerator = aiEnhancerGenerator;
|
|
415
|
+
const genActions = targetGenerator.actions;
|
|
416
|
+
generatorActions = typeof genActions === 'function' ? genActions(data) : genActions;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return generatorActions;
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
import { docsGenerator } from '../docs';
|
|
424
|
+
|
|
425
|
+
export default function (plop: NodePlopAPI) {
|
|
426
|
+
plop.setGenerator('generator', mainGenerator);
|
|
427
|
+
plop.setGenerator('help', helpGenerator);
|
|
428
|
+
plop.setGenerator('docs', docsGenerator);
|
|
429
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export interface GenerateOptions {
|
|
4
|
+
temperature?: number;
|
|
5
|
+
constraints?: string[];
|
|
6
|
+
plopContext?: Record<string, any>;
|
|
7
|
+
verbosity?: 'minimal' | 'standard' | 'verbose';
|
|
8
|
+
responseFormat?: 'text' | 'json';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const generateWithModel = async (
|
|
12
|
+
context: string,
|
|
13
|
+
instruction: string,
|
|
14
|
+
modelCommand: string,
|
|
15
|
+
modelDisplayName: string,
|
|
16
|
+
options: GenerateOptions = {}
|
|
17
|
+
) => {
|
|
18
|
+
const { temperature, constraints, plopContext, verbosity, responseFormat } = options;
|
|
19
|
+
|
|
20
|
+
// Determine verbosity rules
|
|
21
|
+
let verbosityRules: string[] = [];
|
|
22
|
+
if (verbosity === 'minimal') {
|
|
23
|
+
verbosityRules = [
|
|
24
|
+
"Output minimal code. No comments, no docstrings unless strictly required.",
|
|
25
|
+
"Do NOT repeat existing code or context.",
|
|
26
|
+
"Return ONLY the specific function/class/snippet requested."
|
|
27
|
+
];
|
|
28
|
+
} else if (verbosity === 'verbose') {
|
|
29
|
+
verbosityRules = [
|
|
30
|
+
"Include detailed docstrings and comments explaining complex logic.",
|
|
31
|
+
"Ensure code is fully documented."
|
|
32
|
+
];
|
|
33
|
+
} else {
|
|
34
|
+
// Standard
|
|
35
|
+
verbosityRules = [
|
|
36
|
+
"Include standard docstrings and necessary comments.",
|
|
37
|
+
"Do NOT repeat existing code unless necessary for context."
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isJsonFormat = responseFormat === 'json';
|
|
42
|
+
const outputRule = isJsonFormat
|
|
43
|
+
? "Output MUST be a valid JSON object. Do NOT wrap in markdown."
|
|
44
|
+
: "Output ONLY the raw Python code. Do NOT include markdown formatting (no ```python blocks).";
|
|
45
|
+
|
|
46
|
+
// Construct a structured JSON prompt
|
|
47
|
+
const promptObject = {
|
|
48
|
+
meta: {
|
|
49
|
+
task: "Code Generation",
|
|
50
|
+
language: "Python",
|
|
51
|
+
mode: "Strict",
|
|
52
|
+
description: "Generate precise, deterministic Python code based on the provided context and instruction.",
|
|
53
|
+
rules: [
|
|
54
|
+
outputRule,
|
|
55
|
+
"Do NOT include the markers in the output.",
|
|
56
|
+
"Be strictly deterministic. Avoid creativity unless explicitly requested.",
|
|
57
|
+
"Follow PEP 8 standards.",
|
|
58
|
+
...verbosityRules
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
input: {
|
|
62
|
+
instruction: instruction,
|
|
63
|
+
file_context: context,
|
|
64
|
+
generator_context: plopContext || {},
|
|
65
|
+
constraints: constraints || []
|
|
66
|
+
},
|
|
67
|
+
style: {
|
|
68
|
+
temperature: temperature ?? 0.1, // Default to strict (low temperature)
|
|
69
|
+
tone: "Technical, Precise, Concise"
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Override style description based on temperature if provided
|
|
74
|
+
if (temperature !== undefined) {
|
|
75
|
+
if (temperature < 0.3) promptObject.style.tone = "Strict, Deterministic, Precise";
|
|
76
|
+
else if (temperature > 0.7) promptObject.style.tone = "Creative, Exploratory";
|
|
77
|
+
else promptObject.style.tone = "Balanced, Standard";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const promptString = JSON.stringify(promptObject, null, 2);
|
|
81
|
+
|
|
82
|
+
// Wrap the JSON in a clear instruction for the model to parse it
|
|
83
|
+
const finalPrompt = `You are a strict code generator. Parse the following JSON input and execute the task described in 'input.instruction' using the context provided.
|
|
84
|
+
${isJsonFormat ? "Output MUST be a valid JSON string with no extra text or markdown." : "Output ONLY the generated code string, with no markdown."}
|
|
85
|
+
|
|
86
|
+
JSON_INPUT:
|
|
87
|
+
${promptString}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
console.log(`Executing: ${modelCommand} (with JSON structured input)`);
|
|
92
|
+
|
|
93
|
+
const child = spawn(modelCommand, [], {
|
|
94
|
+
shell: true,
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let stdout = '';
|
|
99
|
+
let stderr = '';
|
|
100
|
+
|
|
101
|
+
child.stdout.on('data', (data) => {
|
|
102
|
+
stdout += data.toString();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
child.stderr.on('data', (data) => {
|
|
106
|
+
stderr += data.toString();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
child.on('error', (error) => {
|
|
110
|
+
console.error(`Error spawning ${modelDisplayName}:`, error);
|
|
111
|
+
reject(error);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
child.on('close', (code) => {
|
|
115
|
+
if (code === 0) {
|
|
116
|
+
// Try to clean up any potential markdown if the model ignored instructions
|
|
117
|
+
let cleanOutput = stdout.trim();
|
|
118
|
+
if (cleanOutput.startsWith('```json')) {
|
|
119
|
+
cleanOutput = cleanOutput.replace(/^```json\s*/, '').replace(/\s*```$/, '');
|
|
120
|
+
} else if (cleanOutput.startsWith('```python')) {
|
|
121
|
+
cleanOutput = cleanOutput.replace(/^```python\s*/, '').replace(/\s*```$/, '');
|
|
122
|
+
} else if (cleanOutput.startsWith('```')) {
|
|
123
|
+
cleanOutput = cleanOutput.replace(/^```\s*/, '').replace(/\s*```$/, '');
|
|
124
|
+
}
|
|
125
|
+
resolve(cleanOutput);
|
|
126
|
+
} else {
|
|
127
|
+
console.error(`Error in ${modelDisplayName} generation (code ${code}):`, stderr);
|
|
128
|
+
reject(new Error(`Process exited with code ${code}: ${stderr}`));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Write prompt to stdin and close it to signal end of input
|
|
133
|
+
try {
|
|
134
|
+
child.stdin.write(finalPrompt);
|
|
135
|
+
child.stdin.end();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`Error writing to stdin of ${modelDisplayName}:`, error);
|
|
138
|
+
reject(error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface AppFileInfo {
|
|
5
|
+
fileName: string;
|
|
6
|
+
filePath: string;
|
|
7
|
+
classes: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Analyzes the selected application to find the main file (models.py) and extract class definitions.
|
|
12
|
+
* @param appName The name of the application (directory name)
|
|
13
|
+
* @returns AppFileInfo containing filename, absolute path, and list of class names, or null if not found.
|
|
14
|
+
*/
|
|
15
|
+
export const analyzeAppFile = (appName: string): AppFileInfo | null => {
|
|
16
|
+
const appDir = path.join(process.cwd(), 'app', appName);
|
|
17
|
+
|
|
18
|
+
// Priority 1: models.py (Standard Django)
|
|
19
|
+
let targetFile = 'models.py';
|
|
20
|
+
let targetPath = path.join(appDir, targetFile);
|
|
21
|
+
|
|
22
|
+
// Check if file exists
|
|
23
|
+
if (!fs.existsSync(targetPath)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
28
|
+
const classes = extractClassNames(content);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
fileName: targetFile,
|
|
32
|
+
filePath: targetPath,
|
|
33
|
+
classes: classes
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Analyzes a specific file within an application.
|
|
39
|
+
* @param appName The name of the application
|
|
40
|
+
* @param relativeFilePath The relative path of the file from the app root
|
|
41
|
+
* @returns AppFileInfo containing filename, absolute path, and list of class names, or null if not found.
|
|
42
|
+
*/
|
|
43
|
+
export const analyzeFile = (appName: string, relativeFilePath: string): AppFileInfo | null => {
|
|
44
|
+
const appDir = path.join(process.cwd(), 'app', appName);
|
|
45
|
+
const targetPath = path.join(appDir, relativeFilePath);
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(targetPath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
52
|
+
const classes = extractClassNames(content);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
fileName: path.basename(relativeFilePath),
|
|
56
|
+
filePath: targetPath,
|
|
57
|
+
classes: classes
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extracts class names from Python file content using Regex.
|
|
63
|
+
* Ignores comments and handles basic indentation (though top-level is preferred).
|
|
64
|
+
* @param content File content
|
|
65
|
+
*/
|
|
66
|
+
export const extractClassNames = (content: string): string[] => {
|
|
67
|
+
// Regex to match "class ClassName"
|
|
68
|
+
// ^\s*class\s+ -> start of line, optional whitespace, "class" keyword, whitespace
|
|
69
|
+
// (?<name>\w+) -> capture group for name
|
|
70
|
+
// .*?: -> match rest of line until colon (inheritance etc)
|
|
71
|
+
const classRegex = /^\s*class\s+(?<name>\w+)/gm;
|
|
72
|
+
const classes: string[] = [];
|
|
73
|
+
let match;
|
|
74
|
+
|
|
75
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
76
|
+
if (match.groups && match.groups.name && match.groups.name !== 'Meta') {
|
|
77
|
+
classes.push(match.groups.name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return classes;
|
|
82
|
+
};
|