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,52 @@
|
|
|
1
|
+
export const WAGTAIL_PAGE_TYPES = [
|
|
2
|
+
{ name: 'Paire de Pages Standard (Index + Detail) : Pour une structure hiérarchique (ex: Liste de blog -> Article de blog).', value: 'page_pair' },
|
|
3
|
+
{ name: 'Modèle Orderable : Pour gérer des listes ordonnées d\'éléments dans une page (ex: Carrousel, Galerie).', value: 'orderable' },
|
|
4
|
+
{ name: 'Snippet Wagtail : Pour du contenu réutilisable hors arborescence (ex: Catégories, Auteurs, Publicités).', value: 'snippet' },
|
|
5
|
+
{ name: 'Modèle Django Standard : Une table de base de données classique sans fonctionnalités CMS Wagtail.', value: 'model' },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export const MAIN_COMPONENT_TYPES = [
|
|
9
|
+
{ name: 'Modifier une classe existante (AI Enhancer)', value: 'ai_enhancer' },
|
|
10
|
+
...WAGTAIL_PAGE_TYPES,
|
|
11
|
+
{ name: 'Block StreamField', value: 'block' },
|
|
12
|
+
{ name: 'Vue Django (FBV/CBV)', value: 'view' },
|
|
13
|
+
{ name: 'Formulaire Django', value: 'form' },
|
|
14
|
+
{ name: 'Consumer Django Channel', value: 'channel' },
|
|
15
|
+
{ name: 'Extension Admin Wagtail', value: 'admin_ext' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const VIEW_TYPES = [
|
|
19
|
+
{ name: 'Vue basée sur une fonction (FBV) : Idéale pour une logique sur-mesure simple sans l\'abstraction des classes.', value: 'fbv' },
|
|
20
|
+
{ name: 'TemplateView (CBV) : Affiche une page HTML statique ou avec des données contextuelles simples.', value: 'TemplateView' },
|
|
21
|
+
{ name: 'ListView (CBV) : Génère une page listant plusieurs objets d\'un modèle (ex: catalogue, blog).', value: 'ListView' },
|
|
22
|
+
{ name: 'DetailView (CBV) : Affiche les informations détaillées d\'un objet unique (ex: fiche produit, article).', value: 'DetailView' },
|
|
23
|
+
{ name: 'CreateView (CBV) : Génère un formulaire pour ajouter une nouvelle entrée dans la base de données.', value: 'CreateView' },
|
|
24
|
+
{ name: 'UpdateView (CBV) : Génère un formulaire pré-rempli pour modifier une entrée existante.', value: 'UpdateView' },
|
|
25
|
+
{ name: 'DeleteView (CBV) : Affiche une page de confirmation avant la suppression définitive d\'un objet.', value: 'DeleteView' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const FORM_TYPES = [
|
|
29
|
+
{ name: 'Formulaire Standard (forms.Form) : Pour traiter des données génériques (ex: recherche, contact).', value: 'form' },
|
|
30
|
+
{ name: 'Formulaire de Modèle (forms.ModelForm) : Crée automatiquement des champs basés sur un modèle existant.', value: 'model_form' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const ADMIN_EXT_TYPES = [
|
|
34
|
+
{ name: 'Vue d\'Administration (Page Personnalisée) : Ajoute une page complète dans le menu admin Wagtail.', value: 'view' },
|
|
35
|
+
{ name: 'Composant Template (Panneau UI) : Crée un élément d\'interface réutilisable (Panel) pour les formulaires d\'édition.', value: 'component' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const CHANNEL_TYPES = [
|
|
39
|
+
{ name: 'Consumer Websocket Asynchrone : Pour haute performance (chat, notifications) avec asyncio.', value: 'async' },
|
|
40
|
+
{ name: 'Consumer Websocket Synchrone : Pour logique bloquante standard ou code hérité.', value: 'sync' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const AI_PROVIDER_TYPES = ['Qwen', 'Gemini'];
|
|
44
|
+
|
|
45
|
+
export const AI_TEMP_TYPES = [
|
|
46
|
+
{ name: 'Strict & Précis (0.1)', value: 0.1 },
|
|
47
|
+
{ name: 'Équilibré (0.5)', value: 0.5 },
|
|
48
|
+
{ name: 'Créatif (0.9)', value: 0.9 }
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export const EXIT_VALUE = '__EXIT__';
|
|
52
|
+
export const BACK_VALUE = '__BACK__';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PlopGeneratorConfig } from 'plop';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generator configuration for creating a new Django Application.
|
|
5
|
+
* Sets up the standard directory structure, models, views, urls, and admin files.
|
|
6
|
+
*
|
|
7
|
+
* @type {PlopGeneratorConfig}
|
|
8
|
+
*/
|
|
9
|
+
export const djangoAppGenerator: PlopGeneratorConfig = {
|
|
10
|
+
description: 'Créer une nouvelle application Django avec la structure standard',
|
|
11
|
+
prompts: [
|
|
12
|
+
{
|
|
13
|
+
type: 'input',
|
|
14
|
+
name: 'name',
|
|
15
|
+
message: 'Quel est le nom de l\'application Django ? (Cela créera un dossier avec models, views, urls pour une fonctionnalité)',
|
|
16
|
+
validate: (value) => {
|
|
17
|
+
if (/.+/.test(value)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return 'Le nom est requis';
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
actions: [
|
|
25
|
+
{
|
|
26
|
+
type: 'add',
|
|
27
|
+
path: 'app/__init__.py',
|
|
28
|
+
templateFile: 'generators/django-app/templates/init.py.hbs',
|
|
29
|
+
skipIfExists: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'add',
|
|
33
|
+
path: 'app/{{name}}/__init__.py',
|
|
34
|
+
templateFile: 'generators/django-app/templates/init.py.hbs',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'add',
|
|
38
|
+
path: 'app/{{name}}/apps.py',
|
|
39
|
+
templateFile: 'generators/django-app/templates/apps.py.hbs',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'add',
|
|
43
|
+
path: 'app/{{name}}/models/__init__.py',
|
|
44
|
+
templateFile: 'generators/django-app/templates/models_init.py.hbs',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'add',
|
|
48
|
+
path: 'app/{{name}}/tests/__init__.py',
|
|
49
|
+
templateFile: 'generators/django-app/templates/init.py.hbs',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'add',
|
|
53
|
+
path: 'app/{{name}}/views.py',
|
|
54
|
+
templateFile: 'generators/django-app/templates/views.py.hbs',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'add',
|
|
58
|
+
path: 'app/{{name}}/urls.py',
|
|
59
|
+
templateFile: 'generators/django-app/templates/urls.py.hbs',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'add',
|
|
63
|
+
path: 'app/{{name}}/admin.py',
|
|
64
|
+
templateFile: 'generators/django-app/templates/admin.py.hbs',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class {{pascalCase name}}Config(AppConfig):
|
|
5
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
6
|
+
name = 'app.{{name}}'
|
|
7
|
+
|
|
8
|
+
# <!-- CLASS = {{pascalCase name}}Config START AI_GENERATED_CONFIG -->
|
|
9
|
+
# <!-- CLASS = {{pascalCase name}}Config END AI_GENERATED_CONFIG -->
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PlopGeneratorConfig } from 'plop';
|
|
2
|
+
import { ensureSuffix, createAppendActions } from '../utils/plop-actions';
|
|
3
|
+
import { CHANNEL_TYPES } from '../constants';
|
|
4
|
+
import { searchChoices } from '../utils/search';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generator configuration for creating a Django Channel Consumer and Routing.
|
|
8
|
+
* Supports Async and Sync Websocket Consumers.
|
|
9
|
+
* Automatically configures routing and adds tests.
|
|
10
|
+
*
|
|
11
|
+
* @type {PlopGeneratorConfig}
|
|
12
|
+
*/
|
|
13
|
+
export const djangoChannelGenerator: PlopGeneratorConfig = {
|
|
14
|
+
description: 'Créer un Consumer Django Channel et son routing',
|
|
15
|
+
prompts: [
|
|
16
|
+
{
|
|
17
|
+
type: 'autocomplete',
|
|
18
|
+
name: 'type',
|
|
19
|
+
message: 'Quel type de consumer voulez-vous créer ?',
|
|
20
|
+
source: (answers, input) => searchChoices(input, CHANNEL_TYPES),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'name',
|
|
25
|
+
message: 'Nom du Consumer (ex: ChatConsumer) :',
|
|
26
|
+
validate: (value) => {
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'input',
|
|
32
|
+
name: 'route',
|
|
33
|
+
message: 'Préfixe de la route URL (ex: chat -> ws/chat/room_name/) :',
|
|
34
|
+
default: 'chat',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'input',
|
|
38
|
+
name: 'app',
|
|
39
|
+
message: 'Nom de l\'application cible',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
actions: (data) => {
|
|
43
|
+
const actions = [];
|
|
44
|
+
const appPath = `app/${data.app}`;
|
|
45
|
+
const isAsync = data.type === 'async';
|
|
46
|
+
data.isAsync = isAsync;
|
|
47
|
+
|
|
48
|
+
// Ensure suffix
|
|
49
|
+
data.name = ensureSuffix(data.name, 'Consumer');
|
|
50
|
+
|
|
51
|
+
// Ensure consumers.py exists and Append Consumer
|
|
52
|
+
actions.push(...createAppendActions({
|
|
53
|
+
path: `${appPath}/consumers.py`,
|
|
54
|
+
templateFile: 'generators/django-channel/templates/consumer.py.hbs',
|
|
55
|
+
dumbTemplate: 'import json\nfrom channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer\n\n',
|
|
56
|
+
dumbData: {},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
// Ensure routing.py exists
|
|
60
|
+
actions.push({
|
|
61
|
+
type: 'add',
|
|
62
|
+
path: `${appPath}/routing.py`,
|
|
63
|
+
templateFile: 'generators/django-channel/templates/routing.py.hbs',
|
|
64
|
+
skipIfExists: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Append to websocket_urlpatterns in routing.py
|
|
68
|
+
// We look for the closing bracket of websocket_urlpatterns list
|
|
69
|
+
actions.push({
|
|
70
|
+
type: 'modify',
|
|
71
|
+
path: `${appPath}/routing.py`,
|
|
72
|
+
pattern: /(websocket_urlpatterns\s*=\s*\[)/,
|
|
73
|
+
template: '$1\n re_path(r\'ws/{{ route }}/(?P<room_name>\\w+)/$\', consumers.{{ name }}.as_asgi()),',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return actions;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from channels.generic.websocket import {{#if isAsync}}AsyncWebsocketConsumer{{else}}WebsocketConsumer{{/if}}
|
|
3
|
+
|
|
4
|
+
class {{ name }}({{#if isAsync}}AsyncWebsocketConsumer{{else}}WebsocketConsumer{{/if}}):
|
|
5
|
+
# <!-- CLASS = {{ name }} START AI_GENERATED_CONSUMER -->
|
|
6
|
+
# <!-- CLASS = {{ name }} END AI_GENERATED_CONSUMER -->
|
|
7
|
+
{{#if isAsync}}async {{/if}}def connect(self):
|
|
8
|
+
self.room_name = self.scope['url_route']['kwargs']['room_name']
|
|
9
|
+
self.room_group_name = 'chat_%s' % self.room_name
|
|
10
|
+
|
|
11
|
+
# Join room group
|
|
12
|
+
{{#if isAsync}}await {{/if}}self.channel_layer.group_add(
|
|
13
|
+
self.room_group_name,
|
|
14
|
+
self.channel_name
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
{{#if isAsync}}await {{/if}}self.accept()
|
|
18
|
+
|
|
19
|
+
{{#if isAsync}}async {{/if}}def disconnect(self, close_code):
|
|
20
|
+
# Leave room group
|
|
21
|
+
{{#if isAsync}}await {{/if}}self.channel_layer.group_discard(
|
|
22
|
+
self.room_group_name,
|
|
23
|
+
self.channel_name
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Receive message from WebSocket
|
|
27
|
+
{{#if isAsync}}async {{/if}}def receive(self, text_data):
|
|
28
|
+
text_data_json = json.loads(text_data)
|
|
29
|
+
message = text_data_json['message']
|
|
30
|
+
|
|
31
|
+
# Send message to room group
|
|
32
|
+
{{#if isAsync}}await {{/if}}self.channel_layer.group_send(
|
|
33
|
+
self.room_group_name,
|
|
34
|
+
{
|
|
35
|
+
'type': 'chat_message',
|
|
36
|
+
'message': message
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Receive message from room group
|
|
41
|
+
{{#if isAsync}}async {{/if}}def chat_message(self, event):
|
|
42
|
+
message = event['message']
|
|
43
|
+
|
|
44
|
+
# Send message to WebSocket
|
|
45
|
+
{{#if isAsync}}await {{/if}}self.send(text_data=json.dumps({
|
|
46
|
+
'message': message
|
|
47
|
+
}))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from django.urls import re_path
|
|
2
|
+
from . import consumers
|
|
3
|
+
|
|
4
|
+
websocket_urlpatterns = [
|
|
5
|
+
# <!-- CLASS = {{pascalCase name}}Routing START AI_GENERATED_ROUTING -->
|
|
6
|
+
# <!-- CLASS = {{pascalCase name}}Routing END AI_GENERATED_ROUTING -->
|
|
7
|
+
# re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
|
|
8
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PlopGeneratorConfig } from 'plop';
|
|
2
|
+
import { ensureSuffix, createAppendActions } from '../utils/plop-actions';
|
|
3
|
+
import { FORM_TYPES } from '../constants';
|
|
4
|
+
import { searchChoices } from '../utils/search';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generator configuration for creating a Django Form or ModelForm.
|
|
8
|
+
* Handles standard forms and model-bound forms.
|
|
9
|
+
*
|
|
10
|
+
* @type {PlopGeneratorConfig}
|
|
11
|
+
*/
|
|
12
|
+
export const djangoFormGenerator: PlopGeneratorConfig = {
|
|
13
|
+
description: 'Créer un Formulaire Django ou un ModelForm',
|
|
14
|
+
prompts: [
|
|
15
|
+
{
|
|
16
|
+
type: 'autocomplete',
|
|
17
|
+
name: 'type',
|
|
18
|
+
message: 'Quel type de formulaire voulez-vous créer ?',
|
|
19
|
+
source: (answers, input) => searchChoices(input, FORM_TYPES),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'input',
|
|
23
|
+
name: 'name',
|
|
24
|
+
message: 'Nom du Formulaire (ex: ContactForm, UserProfileForm) :',
|
|
25
|
+
validate: (value) => {
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'model',
|
|
32
|
+
message: 'Nom du Modèle associé (ex: UserProfile) :',
|
|
33
|
+
when: (answers) => answers.type === 'model_form',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'app',
|
|
38
|
+
message: 'Nom de l\'application cible',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
actions: (data) => {
|
|
42
|
+
const actions = [];
|
|
43
|
+
const appPath = `app/${data.app}`;
|
|
44
|
+
const isModelForm = data.type === 'model_form';
|
|
45
|
+
|
|
46
|
+
// Ensure suffix
|
|
47
|
+
data.name = ensureSuffix(data.name, 'Form');
|
|
48
|
+
|
|
49
|
+
const templateFile = isModelForm
|
|
50
|
+
? 'generators/django-form/templates/model_form.py.hbs'
|
|
51
|
+
: 'generators/django-form/templates/form.py.hbs';
|
|
52
|
+
|
|
53
|
+
actions.push(...createAppendActions({
|
|
54
|
+
path: `${appPath}/forms.py`,
|
|
55
|
+
templateFile,
|
|
56
|
+
dumbTemplateFile: 'generators/django-form/templates/forms_file.py.hbs',
|
|
57
|
+
dumbData: data,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return actions;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
class {{ name }}(forms.Form):
|
|
3
|
+
# TODO: Define form fields here
|
|
4
|
+
# name = forms.CharField(max_length=100)
|
|
5
|
+
# message = forms.CharField(widget=forms.Textarea)
|
|
6
|
+
|
|
7
|
+
# <!-- CLASS = {{ name }} START AI_GENERATED_FORM -->
|
|
8
|
+
# <!-- CLASS = {{ name }} END AI_GENERATED_FORM -->
|
|
9
|
+
def clean(self):
|
|
10
|
+
cleaned_data = super().clean()
|
|
11
|
+
# TODO: Add custom validation logic
|
|
12
|
+
return cleaned_data
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
{{#if model}}from .models import {{ model }}{{/if}}
|
|
3
|
+
|
|
4
|
+
class {{ name }}(forms.ModelForm):
|
|
5
|
+
class Meta:
|
|
6
|
+
|
|
7
|
+
{{#if model}} model = {{ model }}{{/if}}
|
|
8
|
+
{{#unless model}} # model = YourModel{{/unless}}
|
|
9
|
+
fields = '__all__' # TODO: Specify explicit fields ['field1', 'field2']
|
|
10
|
+
# widgets = {
|
|
11
|
+
# 'field_name': forms.TextInput(attrs={'class': 'form-control'}),
|
|
12
|
+
# }
|
|
13
|
+
# <!-- CLASS = {{ name }} START AI_GENERATED_MODEL_FORM -->
|
|
14
|
+
# <!-- CLASS = {{ name }} END AI_GENERATED_MODEL_FORM -->
|
|
15
|
+
def clean(self):
|
|
16
|
+
cleaned_data = super().clean()
|
|
17
|
+
# TODO: Add custom validation logic
|
|
18
|
+
return cleaned_data
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { PlopGeneratorConfig } from 'plop';
|
|
2
|
+
import { createAppendActions, ensureSuffix } from '../utils/plop-actions';
|
|
3
|
+
import { VIEW_TYPES } from '../constants';
|
|
4
|
+
import { searchChoices } from '../utils/search';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generator configuration for creating Django Views.
|
|
8
|
+
* Supports Function-Based Views (FBV) and Class-Based Views (CBV) including:
|
|
9
|
+
* - TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView.
|
|
10
|
+
* Handles automatic suffixing logic for naming conventions.
|
|
11
|
+
*
|
|
12
|
+
* @type {PlopGeneratorConfig}
|
|
13
|
+
*/
|
|
14
|
+
export const djangoViewGenerator: PlopGeneratorConfig = {
|
|
15
|
+
description: 'Créer une Vue Django (FBV ou CBV)',
|
|
16
|
+
prompts: [
|
|
17
|
+
{
|
|
18
|
+
type: 'autocomplete',
|
|
19
|
+
name: 'type',
|
|
20
|
+
message: 'Quel type de vue voulez-vous créer ?',
|
|
21
|
+
source: (answers, input) => searchChoices(input, VIEW_TYPES),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: 'input',
|
|
25
|
+
name: 'name',
|
|
26
|
+
message: 'Nom de la Vue (ex: UserProfile, ProductList) :',
|
|
27
|
+
validate: (value, answers) => {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'app',
|
|
34
|
+
message: 'Nom de l\'application cible',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
actions: (data) => {
|
|
38
|
+
const actions = [];
|
|
39
|
+
const appPath = `app/${data.app}`;
|
|
40
|
+
const isCBV = data.type !== 'fbv';
|
|
41
|
+
|
|
42
|
+
// Helper to ensure correct suffix for CBV
|
|
43
|
+
if (isCBV) {
|
|
44
|
+
let suffix = data.type; // e.g. ListView, DetailView
|
|
45
|
+
if (data.type === 'TemplateView') {
|
|
46
|
+
suffix = 'View';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!data.name.endsWith(suffix)) {
|
|
50
|
+
// Check if it ends with the "Type" part (e.g. "List" for "ListView")
|
|
51
|
+
const typePrefix = suffix.replace(/View$/, ''); // "List"
|
|
52
|
+
|
|
53
|
+
if (typePrefix && data.name.endsWith(typePrefix)) {
|
|
54
|
+
// e.g. ProductList + ListView -> ProductListView
|
|
55
|
+
data.name = `${data.name}View`;
|
|
56
|
+
} else if (data.name.endsWith('View')) {
|
|
57
|
+
// e.g. ProductView + ListView -> ProductListView
|
|
58
|
+
data.name = data.name.replace(/View$/, suffix);
|
|
59
|
+
} else {
|
|
60
|
+
// e.g. Product + ListView -> ProductListView
|
|
61
|
+
data.name = `${data.name}${suffix}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// FBV
|
|
66
|
+
data.name = ensureSuffix(data.name, '_view');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Helper for template name
|
|
70
|
+
data.templateName = data.name.replace(/View$/, '');
|
|
71
|
+
|
|
72
|
+
// Append to views.py
|
|
73
|
+
const templateFile = isCBV
|
|
74
|
+
? 'generators/django-view/templates/view_cbv.py.hbs'
|
|
75
|
+
: 'generators/django-view/templates/view_fbv.py.hbs';
|
|
76
|
+
|
|
77
|
+
actions.push(...createAppendActions({
|
|
78
|
+
path: `${appPath}/views.py`,
|
|
79
|
+
templateFile,
|
|
80
|
+
dumbTemplate: 'from django.shortcuts import render\n\n',
|
|
81
|
+
dumbData: {},
|
|
82
|
+
appendData: isCBV ? { viewType: data.type } : undefined
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
// Create Template
|
|
86
|
+
actions.push({
|
|
87
|
+
type: 'add',
|
|
88
|
+
path: `${appPath}/templates/${data.app}/{{snakeCase templateName}}.html`, // Clean filename
|
|
89
|
+
templateFile: 'generators/django-view/templates/view_template.html.hbs',
|
|
90
|
+
skipIfExists: true,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return actions;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
class {{pascalCase name}}({{viewType}}):
|
|
3
|
+
"""
|
|
4
|
+
Class-based view for {{name}}.
|
|
5
|
+
"""
|
|
6
|
+
template_name = '{{app}}/{{snakeCase name}}.html'
|
|
7
|
+
|
|
8
|
+
# <!-- CLASS = {{pascalCase name}} START AI_GENERATED_VIEW -->
|
|
9
|
+
# <!-- CLASS = {{pascalCase name}} END AI_GENERATED_VIEW -->
|
|
10
|
+
# context_object_name = '{{snakeCase name}}'
|
|
11
|
+
# model = MyModel
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
<!-- CLASS = {{pascalCase name}} START AI_GENERATED_VIEW_TEMPLATE -->
|
|
4
|
+
{% block content %}
|
|
5
|
+
<h1>{{titleCase name}}</h1>
|
|
6
|
+
<p>Welcome to the {{name}} view.</p>
|
|
7
|
+
{% endblock %}
|
|
8
|
+
<!-- CLASS = {{pascalCase name}} END AI_GENERATED_VIEW_TEMPLATE -->
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PlopGeneratorConfig } from 'plop';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
export const docsGenerator: PlopGeneratorConfig = {
|
|
5
|
+
description: 'Ouvrir le serveur de documentation',
|
|
6
|
+
prompts: [],
|
|
7
|
+
actions: [
|
|
8
|
+
function runDocs() {
|
|
9
|
+
console.log('🚀 Démarrage du serveur de documentation...');
|
|
10
|
+
console.log('🌐 Accédez à http://localhost:3333');
|
|
11
|
+
|
|
12
|
+
const isWindows = process.platform === 'win32';
|
|
13
|
+
const cmd = 'bun';
|
|
14
|
+
const args = ['run', '--port', '3333', 'docs-site/index.html'];
|
|
15
|
+
|
|
16
|
+
const child = spawn(cmd, args, {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
shell: isWindows
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Return a promise that resolves when the child process exits
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
child.on('close', (code) => {
|
|
24
|
+
if (code === 0) {
|
|
25
|
+
resolve('Serveur arrêté');
|
|
26
|
+
} else {
|
|
27
|
+
reject(`Serveur arrêté avec le code ${code}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
child.on('error', (err) => {
|
|
31
|
+
reject(err);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
import { PlopGeneratorConfig } from 'plop';
|
|
3
|
+
import keywordConfig from '../ai-enhancer/keywords.json';
|
|
4
|
+
import { EXIT_VALUE } from '../constants';
|
|
5
|
+
|
|
6
|
+
const getHelpItems = (input: string | undefined): any[] => {
|
|
7
|
+
const items: any[] = [];
|
|
8
|
+
|
|
9
|
+
if (input) {
|
|
10
|
+
const inputLower = input.toLowerCase();
|
|
11
|
+
let lastMatchIndex = -1;
|
|
12
|
+
let matchedInstruction = null;
|
|
13
|
+
|
|
14
|
+
const keywords = Array.isArray(keywordConfig) ? keywordConfig : (keywordConfig as any).keywords || [];
|
|
15
|
+
|
|
16
|
+
for (const entry of keywords) {
|
|
17
|
+
if (entry.terms && Array.isArray(entry.terms)) {
|
|
18
|
+
for (const term of entry.terms) {
|
|
19
|
+
const index = inputLower.lastIndexOf(term.toLowerCase());
|
|
20
|
+
if (index !== -1 && index >= lastMatchIndex) {
|
|
21
|
+
lastMatchIndex = index;
|
|
22
|
+
matchedInstruction = entry.instruction;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (matchedInstruction) {
|
|
29
|
+
// Split instruction into lines of max 80 chars for better visibility
|
|
30
|
+
const words = matchedInstruction.split(' ');
|
|
31
|
+
let currentLine = '💡 ';
|
|
32
|
+
|
|
33
|
+
for (const word of words) {
|
|
34
|
+
if ((currentLine + word).length > 80) {
|
|
35
|
+
items.push({ name: currentLine, value: 'INFO', disabled: true });
|
|
36
|
+
currentLine = ' ' + word + ' ';
|
|
37
|
+
} else {
|
|
38
|
+
currentLine += word + ' ';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (currentLine.trim()) {
|
|
42
|
+
items.push({ name: currentLine, value: 'INFO', disabled: true });
|
|
43
|
+
}
|
|
44
|
+
items.push({ name: '────────────────────────────────────────', value: 'SEP', disabled: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return items;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const helpGenerator: PlopGeneratorConfig = {
|
|
51
|
+
description: 'Search for AI instructions and keywords',
|
|
52
|
+
prompts: [
|
|
53
|
+
{
|
|
54
|
+
type: 'autocomplete',
|
|
55
|
+
name: 'query',
|
|
56
|
+
message: 'Type a keyword to see associated instructions (e.g. "model", "field"):',
|
|
57
|
+
suggestOnly: true,
|
|
58
|
+
source: async (answers, input) => {
|
|
59
|
+
const choices: any[] = [];
|
|
60
|
+
|
|
61
|
+
const helpItems = getHelpItems(input);
|
|
62
|
+
if (helpItems.length > 0) {
|
|
63
|
+
choices.push(...helpItems);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!input) {
|
|
67
|
+
choices.push({ name: 'Type to search...', value: 'INFO', disabled: true });
|
|
68
|
+
choices.push({ name: 'Exit', value: EXIT_VALUE });
|
|
69
|
+
return choices;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
choices.push({ name: 'Exit', value: EXIT_VALUE });
|
|
73
|
+
return choices;
|
|
74
|
+
},
|
|
75
|
+
validate: (value) => {
|
|
76
|
+
if (value === EXIT_VALUE || value?.toLowerCase() === 'exit') {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
actions: []
|
|
84
|
+
};
|