kalo-cli 0.1.0 → 0.1.2
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 +21 -0
- package/bin/kalo.ts +38 -2
- package/generators/ai-enhancer/index.ts +3 -3
- package/generators/django-app/index.ts +46 -43
- package/generators/django-channel/index.ts +3 -2
- package/generators/django-form/index.ts +4 -3
- package/generators/django-view/index.ts +3 -2
- package/generators/main/index.ts +31 -39
- package/generators/utils/analysis.ts +15 -5
- package/generators/utils/config.ts +43 -0
- package/generators/utils/filesystem.ts +50 -6
- package/generators/utils/plop-actions.ts +48 -5
- package/generators/wagtail-admin/index.ts +8 -7
- package/generators/wagtail-block/index.ts +4 -7
- package/generators/wagtail-page/actions/model.ts +2 -1
- package/generators/wagtail-page/actions/orderable.ts +2 -1
- package/generators/wagtail-page/actions/page.ts +2 -1
- package/generators/wagtail-page/actions/snippet.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,27 @@ bun add -g kalo-cli
|
|
|
33
33
|
kalo
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
### Configuration
|
|
37
|
+
|
|
38
|
+
Vous pouvez personnaliser le comportement de Kalo en créant un fichier `kalo.config.json` ou `kalo.config.js` à la racine de votre projet.
|
|
39
|
+
|
|
40
|
+
Exemple `kalo.config.json` :
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"appDir": "apps",
|
|
45
|
+
"aiProvider": "Qwen",
|
|
46
|
+
"temperature": 0.5,
|
|
47
|
+
"verbosity": "standard"
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Options disponibles :
|
|
52
|
+
- `appDir` : Le dossier où se trouvent vos applications Django (par défaut: `app`).
|
|
53
|
+
- `aiProvider` : Le fournisseur d'IA par défaut (`Qwen`, `Ollama`, `Mistral`, `OpenAI`).
|
|
54
|
+
- `temperature` : La créativité de l'IA (entre 0 et 1).
|
|
55
|
+
- `verbosity` : Le niveau de détail des commentaires générés (`minimal`, `standard`, `verbose`).
|
|
56
|
+
|
|
36
57
|
### Si installé dans un projet
|
|
37
58
|
|
|
38
59
|
Utilisez `bun run` ou `bunx` :
|
package/bin/kalo.ts
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import minimist from "minimist";
|
|
4
5
|
import { Plop, run } from "plop";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { loadConfig } from "../generators/utils/config";
|
|
6
8
|
|
|
7
9
|
const args = process.argv.slice(2);
|
|
8
10
|
const argv = minimist(args);
|
|
9
11
|
|
|
10
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Recherche le répertoire racine du projet en remontant à partir d'un dossier donné.
|
|
16
|
+
*/
|
|
17
|
+
function findProjectRoot(startDir: string): string {
|
|
18
|
+
let currentDir = startDir;
|
|
19
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
20
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
21
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
24
|
+
if (pkg.name !== 'kalo-cli' || currentDir.includes('node_modules')) {
|
|
25
|
+
if (!currentDir.includes('node_modules')) {
|
|
26
|
+
return currentDir;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
return currentDir;
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
}
|
|
33
|
+
currentDir = path.dirname(currentDir);
|
|
34
|
+
}
|
|
35
|
+
return startDir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
39
|
+
const config = await loadConfig();
|
|
40
|
+
|
|
12
41
|
Plop.prepare({
|
|
13
|
-
cwd: argv.cwd,
|
|
42
|
+
cwd: argv.cwd || projectRoot,
|
|
14
43
|
configPath: path.join(__dirname, '../plopfile.ts'),
|
|
15
44
|
preload: argv.require || [],
|
|
16
45
|
completion: argv.completion
|
|
17
|
-
}, env => Plop.execute(env, (env) =>
|
|
46
|
+
}, env => Plop.execute(env, (env) => {
|
|
47
|
+
const options = {
|
|
48
|
+
...env,
|
|
49
|
+
dest: argv.dest || env.dest,
|
|
50
|
+
config: config // Passer la config au contexte Plop
|
|
51
|
+
};
|
|
52
|
+
return run(options, undefined, true);
|
|
53
|
+
}));
|
|
@@ -144,7 +144,7 @@ export const aiEnhancerGenerator: PlopGeneratorConfig = {
|
|
|
144
144
|
name: 'aiProvider',
|
|
145
145
|
message: 'Select AI Provider:',
|
|
146
146
|
default: 'Qwen',
|
|
147
|
-
when: (answers) => !isBack(answers),
|
|
147
|
+
when: (answers: any) => !isBack(answers) && !answers.config?.aiProvider,
|
|
148
148
|
source: async (answers, input) => {
|
|
149
149
|
const choices = AI_PROVIDER_TYPES.map(p => ({ name: p, value: p }));
|
|
150
150
|
const results = await searchChoices(input, choices);
|
|
@@ -158,7 +158,7 @@ export const aiEnhancerGenerator: PlopGeneratorConfig = {
|
|
|
158
158
|
name: 'temperature',
|
|
159
159
|
message: 'Select Creativity Level:',
|
|
160
160
|
default: 0.5,
|
|
161
|
-
when: (answers) => !isBack(answers),
|
|
161
|
+
when: (answers: any) => !isBack(answers) && answers.config?.temperature === undefined,
|
|
162
162
|
source: async (answers, input) => {
|
|
163
163
|
const results = await searchChoices(input, AI_TEMP_TYPES);
|
|
164
164
|
results.push({ name: 'Exit', value: EXIT_VALUE });
|
|
@@ -171,7 +171,7 @@ export const aiEnhancerGenerator: PlopGeneratorConfig = {
|
|
|
171
171
|
name: 'verbosity',
|
|
172
172
|
message: 'Select Verbosity Level:',
|
|
173
173
|
default: 'standard',
|
|
174
|
-
when: (answers) => !isBack(answers),
|
|
174
|
+
when: (answers: any) => !isBack(answers) && !answers.config?.verbosity,
|
|
175
175
|
source: async (answers, input) => {
|
|
176
176
|
const choices = [
|
|
177
177
|
{ name: 'Minimal (Code only)', value: 'minimal' },
|
|
@@ -21,47 +21,50 @@ export const djangoAppGenerator: PlopGeneratorConfig = {
|
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
],
|
|
24
|
-
actions:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
24
|
+
actions: (data: any) => {
|
|
25
|
+
const appDir = data.config?.appDir || 'app';
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
type: 'add',
|
|
29
|
+
path: `${appDir}/__init__.py`,
|
|
30
|
+
templateFile: 'generators/django-app/templates/init.py.hbs',
|
|
31
|
+
skipIfExists: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'add',
|
|
35
|
+
path: `${appDir}/{{name}}/__init__.py`,
|
|
36
|
+
templateFile: 'generators/django-app/templates/init.py.hbs',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'add',
|
|
40
|
+
path: `${appDir}/{{name}}/apps.py`,
|
|
41
|
+
templateFile: 'generators/django-app/templates/apps.py.hbs',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'add',
|
|
45
|
+
path: `${appDir}/{{name}}/models/__init__.py`,
|
|
46
|
+
templateFile: 'generators/django-app/templates/models_init.py.hbs',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'add',
|
|
50
|
+
path: `${appDir}/{{name}}/tests/__init__.py`,
|
|
51
|
+
templateFile: 'generators/django-app/templates/init.py.hbs',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'add',
|
|
55
|
+
path: `${appDir}/{{name}}/views.py`,
|
|
56
|
+
templateFile: 'generators/django-app/templates/views.py.hbs',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'add',
|
|
60
|
+
path: `${appDir}/{{name}}/urls.py`,
|
|
61
|
+
templateFile: 'generators/django-app/templates/urls.py.hbs',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'add',
|
|
65
|
+
path: `${appDir}/{{name}}/admin.py`,
|
|
66
|
+
templateFile: 'generators/django-app/templates/admin.py.hbs',
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
},
|
|
67
70
|
};
|
|
@@ -39,9 +39,10 @@ export const djangoChannelGenerator: PlopGeneratorConfig = {
|
|
|
39
39
|
message: 'Nom de l\'application cible',
|
|
40
40
|
},
|
|
41
41
|
],
|
|
42
|
-
actions: (data) => {
|
|
42
|
+
actions: (data: any) => {
|
|
43
43
|
const actions = [];
|
|
44
|
-
const
|
|
44
|
+
const appDir = data.config?.appDir || 'app';
|
|
45
|
+
const appPath = `${appDir}/${data.app}`;
|
|
45
46
|
const isAsync = data.type === 'async';
|
|
46
47
|
data.isAsync = isAsync;
|
|
47
48
|
|
|
@@ -38,15 +38,16 @@ export const djangoFormGenerator: PlopGeneratorConfig = {
|
|
|
38
38
|
message: 'Nom de l\'application cible',
|
|
39
39
|
},
|
|
40
40
|
],
|
|
41
|
-
actions: (data) => {
|
|
41
|
+
actions: (data: any) => {
|
|
42
42
|
const actions = [];
|
|
43
|
-
const
|
|
43
|
+
const appDir = data.config?.appDir || 'app';
|
|
44
|
+
const appPath = `${appDir}/${data.app}`;
|
|
44
45
|
const isModelForm = data.type === 'model_form';
|
|
45
46
|
|
|
46
47
|
// Ensure suffix
|
|
47
48
|
data.name = ensureSuffix(data.name, 'Form');
|
|
48
49
|
|
|
49
|
-
const templateFile = isModelForm
|
|
50
|
+
const templateFile = isModelForm
|
|
50
51
|
? 'generators/django-form/templates/model_form.py.hbs'
|
|
51
52
|
: 'generators/django-form/templates/form.py.hbs';
|
|
52
53
|
|
|
@@ -34,9 +34,10 @@ export const djangoViewGenerator: PlopGeneratorConfig = {
|
|
|
34
34
|
message: 'Nom de l\'application cible',
|
|
35
35
|
},
|
|
36
36
|
],
|
|
37
|
-
actions: (data) => {
|
|
37
|
+
actions: (data: any) => {
|
|
38
38
|
const actions = [];
|
|
39
|
-
const
|
|
39
|
+
const appDir = data.config?.appDir || 'app';
|
|
40
|
+
const appPath = `${appDir}/${data.app}`;
|
|
40
41
|
const isCBV = data.type !== 'fbv';
|
|
41
42
|
|
|
42
43
|
// Helper to ensure correct suffix for CBV
|
package/generators/main/index.ts
CHANGED
|
@@ -11,15 +11,15 @@ import keywordConfig from '../ai-enhancer/keywords.json';
|
|
|
11
11
|
import { getAppList, getAppFiles } from '../utils/filesystem';
|
|
12
12
|
import { analyzeAppFile, analyzeFile } from '../utils/analysis';
|
|
13
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,
|
|
14
|
+
import {
|
|
15
|
+
MAIN_COMPONENT_TYPES,
|
|
16
|
+
VIEW_TYPES,
|
|
17
|
+
FORM_TYPES,
|
|
18
|
+
ADMIN_EXT_TYPES,
|
|
19
|
+
CHANNEL_TYPES,
|
|
20
|
+
AI_PROVIDER_TYPES,
|
|
21
21
|
AI_TEMP_TYPES,
|
|
22
|
-
EXIT_VALUE
|
|
22
|
+
EXIT_VALUE
|
|
23
23
|
} from '../constants';
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -143,19 +143,19 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
143
143
|
name: 'selectedApp',
|
|
144
144
|
message: 'Sélectionnez l\'application cible :',
|
|
145
145
|
when: (answers) => ['add', 'modify'].includes(answers.mode),
|
|
146
|
-
source: (answers, input) => {
|
|
147
|
-
const apps = getAppList();
|
|
146
|
+
source: (answers: any, input) => {
|
|
147
|
+
const apps = getAppList(answers.config);
|
|
148
148
|
if (apps.length === 0) {
|
|
149
149
|
return Promise.resolve([{ name: 'Aucune application trouvée', value: null }]);
|
|
150
150
|
}
|
|
151
151
|
const appChoices = apps.map(app => ({ name: app, value: app }));
|
|
152
152
|
return searchChoices(input, appChoices);
|
|
153
153
|
},
|
|
154
|
-
filter: (input) => {
|
|
154
|
+
filter: (input: string, answers: any) => {
|
|
155
155
|
if (!input) return null;
|
|
156
156
|
// Only analyze default models.py if we are NOT in modify mode (or as fallback)
|
|
157
157
|
// But for 'add' mode, we might want it. For 'modify', we will select file explicitly.
|
|
158
|
-
const info = analyzeAppFile(input);
|
|
158
|
+
const info = analyzeAppFile(input, answers?.config);
|
|
159
159
|
return { name: input, info };
|
|
160
160
|
}
|
|
161
161
|
},
|
|
@@ -232,9 +232,9 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
232
232
|
name: 'selectedFile',
|
|
233
233
|
message: 'Sélectionnez le fichier à modifier :',
|
|
234
234
|
when: (answers) => answers.mode === 'modify' && answers.selectedApp !== null,
|
|
235
|
-
source: (answers, input) => {
|
|
235
|
+
source: (answers: any, input) => {
|
|
236
236
|
const appName = answers.selectedApp.name || answers.selectedApp;
|
|
237
|
-
const files = getAppFiles(appName);
|
|
237
|
+
const files = getAppFiles(appName, answers.config);
|
|
238
238
|
|
|
239
239
|
const choices = [];
|
|
240
240
|
if (files.length > 0) {
|
|
@@ -254,11 +254,11 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
254
254
|
name: 'targetClass',
|
|
255
255
|
message: 'Sélectionnez la classe à modifier :',
|
|
256
256
|
when: (answers) => answers.mode === 'modify' && answers.selectedFile !== null && answers.selectedFile !== 'help',
|
|
257
|
-
source: (answers, input) => {
|
|
257
|
+
source: (answers: any, input) => {
|
|
258
258
|
const appName = answers.selectedApp.name || answers.selectedApp;
|
|
259
259
|
const filePath = answers.selectedFile;
|
|
260
260
|
|
|
261
|
-
const info = analyzeFile(appName, filePath);
|
|
261
|
+
const info = analyzeFile(appName, filePath, answers.config);
|
|
262
262
|
if (!info || !info.classes || info.classes.length === 0) {
|
|
263
263
|
return Promise.resolve([{ name: 'Aucune classe trouvée (ou fichier vide)', value: null }]);
|
|
264
264
|
}
|
|
@@ -277,29 +277,29 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
277
277
|
type: 'list',
|
|
278
278
|
name: 'aiProvider',
|
|
279
279
|
message: 'Sélectionnez le fournisseur IA :',
|
|
280
|
-
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
280
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help' && !answers.config?.aiProvider,
|
|
281
281
|
choices: AI_PROVIDER_TYPES,
|
|
282
|
-
default: 'Qwen'
|
|
282
|
+
default: (answers: any) => answers.config?.aiProvider || 'Qwen'
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
285
|
type: 'list',
|
|
286
286
|
name: 'temperature',
|
|
287
287
|
message: 'Niveau de créativité :',
|
|
288
|
-
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
288
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help' && answers.config?.temperature === undefined,
|
|
289
289
|
choices: AI_TEMP_TYPES,
|
|
290
|
-
default: 0.5
|
|
290
|
+
default: (answers: any) => answers.config?.temperature !== undefined ? answers.config.temperature : 0.5
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
293
|
type: 'list',
|
|
294
294
|
name: 'verbosity',
|
|
295
295
|
message: 'Niveau de verbosité :',
|
|
296
|
-
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help',
|
|
296
|
+
when: (answers) => answers.mode === 'modify' && answers.targetClass !== null && answers.selectedFile !== 'help' && !answers.config?.verbosity,
|
|
297
297
|
choices: [
|
|
298
298
|
{ name: 'Minimal (Code seulement)', value: 'minimal' },
|
|
299
299
|
{ name: 'Standard (Code + commentaires basiques)', value: 'standard' },
|
|
300
300
|
{ name: 'Verbeux (Documentation détaillée)', value: 'verbose' }
|
|
301
301
|
],
|
|
302
|
-
default: 'standard'
|
|
302
|
+
default: (answers: any) => answers.config?.verbosity || 'standard'
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
type: 'input',
|
|
@@ -325,9 +325,9 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
325
325
|
{
|
|
326
326
|
...helpGenerator.prompts[0],
|
|
327
327
|
name: 'helpQuery', // Rename to avoid conflict if needed, though 'query' is fine
|
|
328
|
-
when: (answers) =>
|
|
329
|
-
answers.mode === 'help' ||
|
|
330
|
-
answers.componentType === 'help' ||
|
|
328
|
+
when: (answers) =>
|
|
329
|
+
answers.mode === 'help' ||
|
|
330
|
+
answers.componentType === 'help' ||
|
|
331
331
|
answers.selectedFile === 'help'
|
|
332
332
|
}
|
|
333
333
|
],
|
|
@@ -337,7 +337,7 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
337
337
|
// Check for help mode first
|
|
338
338
|
if (data.mode === 'help' || data.componentType === 'help' || data.selectedFile === 'help') {
|
|
339
339
|
// Help actions (empty usually, as interaction is in prompt)
|
|
340
|
-
return [];
|
|
340
|
+
return [];
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
if (data.mode === 'docs') {
|
|
@@ -359,7 +359,7 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
359
359
|
|
|
360
360
|
if (['page_pair', 'snippet', 'orderable', 'model'].includes(data.componentType)) {
|
|
361
361
|
targetGenerator = wagtailPageGenerator;
|
|
362
|
-
data.type = data.componentType;
|
|
362
|
+
data.type = data.componentType;
|
|
363
363
|
} else if (data.componentType === 'block') {
|
|
364
364
|
targetGenerator = wagtailBlockGenerator;
|
|
365
365
|
} else if (data.componentType === 'view') {
|
|
@@ -392,23 +392,15 @@ const mainGenerator: PlopGeneratorConfig = {
|
|
|
392
392
|
// We need full path for the AI generator.
|
|
393
393
|
const appName = data.app;
|
|
394
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
395
|
// 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
396
|
|
|
401
|
-
|
|
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);
|
|
397
|
+
const fileInfo = analyzeFile(appName, relativePath, data.config);
|
|
407
398
|
if (fileInfo) {
|
|
408
399
|
data.filePath = fileInfo.filePath;
|
|
409
400
|
} else {
|
|
410
401
|
// Fallback
|
|
411
|
-
data.
|
|
402
|
+
const appDirName = data.config?.appDir || 'app';
|
|
403
|
+
data.filePath = `${appDirName}/${appName}/${relativePath}`;
|
|
412
404
|
}
|
|
413
405
|
|
|
414
406
|
const targetGenerator = aiEnhancerGenerator;
|
|
@@ -426,4 +418,4 @@ export default function (plop: NodePlopAPI) {
|
|
|
426
418
|
plop.setGenerator('generator', mainGenerator);
|
|
427
419
|
plop.setGenerator('help', helpGenerator);
|
|
428
420
|
plop.setGenerator('docs', docsGenerator);
|
|
429
|
-
}
|
|
421
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { findProjectRoot } from './filesystem';
|
|
3
4
|
|
|
4
5
|
export interface AppFileInfo {
|
|
5
6
|
fileName: string;
|
|
@@ -10,10 +11,13 @@ export interface AppFileInfo {
|
|
|
10
11
|
/**
|
|
11
12
|
* Analyzes the selected application to find the main file (models.py) and extract class definitions.
|
|
12
13
|
* @param appName The name of the application (directory name)
|
|
14
|
+
* @param config Configuration object
|
|
13
15
|
* @returns AppFileInfo containing filename, absolute path, and list of class names, or null if not found.
|
|
14
16
|
*/
|
|
15
|
-
export const analyzeAppFile = (appName: string): AppFileInfo | null => {
|
|
16
|
-
const
|
|
17
|
+
export const analyzeAppFile = (appName: string, config?: any): AppFileInfo | null => {
|
|
18
|
+
const rootDir = findProjectRoot();
|
|
19
|
+
const appDirName = config?.appDir || 'app';
|
|
20
|
+
const appDir = path.join(rootDir, appDirName, appName);
|
|
17
21
|
|
|
18
22
|
// Priority 1: models.py (Standard Django)
|
|
19
23
|
let targetFile = 'models.py';
|
|
@@ -38,10 +42,13 @@ export const analyzeAppFile = (appName: string): AppFileInfo | null => {
|
|
|
38
42
|
* Analyzes a specific file within an application.
|
|
39
43
|
* @param appName The name of the application
|
|
40
44
|
* @param relativeFilePath The relative path of the file from the app root
|
|
45
|
+
* @param config Configuration object
|
|
41
46
|
* @returns AppFileInfo containing filename, absolute path, and list of class names, or null if not found.
|
|
42
47
|
*/
|
|
43
|
-
export const analyzeFile = (appName: string, relativeFilePath: string): AppFileInfo | null => {
|
|
44
|
-
const
|
|
48
|
+
export const analyzeFile = (appName: string, relativeFilePath: string, config?: any): AppFileInfo | null => {
|
|
49
|
+
const rootDir = findProjectRoot();
|
|
50
|
+
const appDirName = config?.appDir || 'app';
|
|
51
|
+
const appDir = path.join(rootDir, appDirName, appName);
|
|
45
52
|
const targetPath = path.join(appDir, relativeFilePath);
|
|
46
53
|
|
|
47
54
|
if (!fs.existsSync(targetPath)) {
|
|
@@ -64,6 +71,9 @@ export const analyzeFile = (appName: string, relativeFilePath: string): AppFileI
|
|
|
64
71
|
* @param content File content
|
|
65
72
|
*/
|
|
66
73
|
export const extractClassNames = (content: string): string[] => {
|
|
74
|
+
// Nettoyer les commentaires pour éviter les faux positifs
|
|
75
|
+
const cleanContent = content.replace(/#.*$/gm, '');
|
|
76
|
+
|
|
67
77
|
// Regex to match "class ClassName"
|
|
68
78
|
// ^\s*class\s+ -> start of line, optional whitespace, "class" keyword, whitespace
|
|
69
79
|
// (?<name>\w+) -> capture group for name
|
|
@@ -72,7 +82,7 @@ export const extractClassNames = (content: string): string[] => {
|
|
|
72
82
|
const classes: string[] = [];
|
|
73
83
|
let match;
|
|
74
84
|
|
|
75
|
-
while ((match = classRegex.exec(
|
|
85
|
+
while ((match = classRegex.exec(cleanContent)) !== null) {
|
|
76
86
|
if (match.groups && match.groups.name && match.groups.name !== 'Meta') {
|
|
77
87
|
classes.push(match.groups.name);
|
|
78
88
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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,15 +1,55 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
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 qui n'est pas celui du package kalo-cli lui-même.
|
|
7
|
+
*
|
|
8
|
+
* @param startDir - Le dossier de départ pour la recherche.
|
|
9
|
+
* @returns Le chemin absolu vers la racine du projet.
|
|
10
|
+
*/
|
|
11
|
+
export const findProjectRoot = (startDir: string = process.cwd()): string => {
|
|
12
|
+
let currentDir = startDir;
|
|
13
|
+
|
|
14
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
15
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
16
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
19
|
+
// Si on trouve un package.json qui n'est pas celui de kalo-cli, c'est probablement la racine du projet utilisateur
|
|
20
|
+
// Ou si on est dans un projet qui n'a pas encore de kalo-cli installé mais qui a un package.json
|
|
21
|
+
if (pkg.name !== 'kalo-cli' || currentDir.includes('node_modules')) {
|
|
22
|
+
// Si on est dans node_modules, on doit continuer à monter car on a trouvé le package.json de kalo-cli
|
|
23
|
+
if (currentDir.includes('node_modules')) {
|
|
24
|
+
// On continue de monter
|
|
25
|
+
} else {
|
|
26
|
+
return currentDir;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// C'est le package.json de kalo-cli en mode développement local
|
|
30
|
+
return currentDir;
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// En cas d'erreur de lecture, on continue de monter
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
currentDir = path.dirname(currentDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return startDir; // Fallback sur le dossier de départ
|
|
40
|
+
};
|
|
41
|
+
|
|
4
42
|
/**
|
|
5
43
|
* Retrieves the list of existing Django applications in the project.
|
|
6
|
-
* Scans the 'app' directory in the
|
|
44
|
+
* Scans the 'app' directory in the project root.
|
|
7
45
|
*
|
|
8
46
|
* @returns {string[]} An array of application names (directory names).
|
|
9
47
|
* @throws {Error} If filesystem access fails (though basic read errors might propagate).
|
|
10
48
|
*/
|
|
11
|
-
export const getAppList = (): string[] => {
|
|
12
|
-
const
|
|
49
|
+
export const getAppList = (config?: any): string[] => {
|
|
50
|
+
const rootDir = findProjectRoot();
|
|
51
|
+
const appDirName = config?.appDir || 'app';
|
|
52
|
+
const appDir = path.join(rootDir, appDirName);
|
|
13
53
|
if (!fs.existsSync(appDir)) {
|
|
14
54
|
return [];
|
|
15
55
|
}
|
|
@@ -21,12 +61,15 @@ export const getAppList = (): string[] => {
|
|
|
21
61
|
/**
|
|
22
62
|
* Recursively lists files in an application directory with filtering.
|
|
23
63
|
* Excludes test files, __init__.py, and specific directories.
|
|
24
|
-
*
|
|
64
|
+
*
|
|
25
65
|
* @param {string} appName - The name of the application.
|
|
66
|
+
* @param {any} config - Configuration object.
|
|
26
67
|
* @returns {string[]} List of relative file paths from the app root.
|
|
27
68
|
*/
|
|
28
|
-
export const getAppFiles = (appName: string): string[] => {
|
|
29
|
-
const
|
|
69
|
+
export const getAppFiles = (appName: string, config?: any): string[] => {
|
|
70
|
+
const rootDir = findProjectRoot();
|
|
71
|
+
const appDirName = config?.appDir || 'app';
|
|
72
|
+
const appDir = path.join(rootDir, appDirName, appName);
|
|
30
73
|
if (!fs.existsSync(appDir)) {
|
|
31
74
|
return [];
|
|
32
75
|
}
|
|
@@ -49,6 +92,7 @@ export const getAppFiles = (appName: string): string[] => {
|
|
|
49
92
|
// Exclude test files and __init__.py
|
|
50
93
|
if (
|
|
51
94
|
dirent.name !== '__init__.py' &&
|
|
95
|
+
!dirent.name.includes('__pycache__') &&
|
|
52
96
|
!dirent.name.includes('.test.') &&
|
|
53
97
|
!dirent.name.includes('.spec.') &&
|
|
54
98
|
dirent.name.endsWith('.py') // Assume we only care about Python files for now based on context
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ActionType } from 'plop';
|
|
2
|
+
import { extractClassNames } from './analysis';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Ensures a suffix is present on a name.
|
|
5
|
-
*
|
|
6
|
+
*
|
|
6
7
|
* @param {string} name - The name to check.
|
|
7
8
|
* @param {string} suffix - The suffix to ensure.
|
|
8
9
|
* @returns {string} The name with the suffix.
|
|
@@ -24,7 +25,7 @@ interface CreateAppendOptions {
|
|
|
24
25
|
/**
|
|
25
26
|
* Creates a pair of actions: one to ensure the file exists (using a "dumb" template rendering or specific init template),
|
|
26
27
|
* and one to append the actual content.
|
|
27
|
-
*
|
|
28
|
+
*
|
|
28
29
|
* @param {CreateAppendOptions} options - Configuration options.
|
|
29
30
|
* @returns {ActionType[]} An array containing the add and append actions.
|
|
30
31
|
*/
|
|
@@ -52,10 +53,52 @@ export const createAppendActions = ({
|
|
|
52
53
|
return [
|
|
53
54
|
addAction,
|
|
54
55
|
{
|
|
55
|
-
type: '
|
|
56
|
+
type: 'modify',
|
|
56
57
|
path,
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
59
102
|
}
|
|
60
103
|
];
|
|
61
104
|
};
|
|
@@ -33,9 +33,10 @@ export const wagtailAdminGenerator: PlopGeneratorConfig = {
|
|
|
33
33
|
message: 'Nom de l\'application cible',
|
|
34
34
|
},
|
|
35
35
|
],
|
|
36
|
-
actions: (data) => {
|
|
36
|
+
actions: (data: any) => {
|
|
37
37
|
const actions = [];
|
|
38
|
-
const
|
|
38
|
+
const appDir = data.config?.appDir || 'app';
|
|
39
|
+
const appPath = `${appDir}/${data.app}`;
|
|
39
40
|
const isView = data.type === 'view';
|
|
40
41
|
|
|
41
42
|
// Ensure suffixes
|
|
@@ -68,14 +69,14 @@ export const wagtailAdminGenerator: PlopGeneratorConfig = {
|
|
|
68
69
|
skipIfExists: true,
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
// If file existed, we might need to append.
|
|
72
|
-
// For simplicity, we just append the registration code.
|
|
72
|
+
// If file existed, we might need to append.
|
|
73
|
+
// For simplicity, we just append the registration code.
|
|
73
74
|
// Ideally we would check for imports, but appending is safer than overwriting.
|
|
74
|
-
// We use a different template for appending to avoid duplicate imports if possible,
|
|
75
|
-
// but for now re-using the file with imports is "okay" in Python (just messy)
|
|
75
|
+
// We use a different template for appending to avoid duplicate imports if possible,
|
|
76
|
+
// but for now re-using the file with imports is "okay" in Python (just messy)
|
|
76
77
|
// or we can rely on user to clean up.
|
|
77
78
|
// BETTER STRATEGY: Create a separate partial for appending without top-level imports if feasible.
|
|
78
|
-
// But for this MVP, let's just append the same content.
|
|
79
|
+
// But for this MVP, let's just append the same content.
|
|
79
80
|
// The user can deduplicate imports.
|
|
80
81
|
|
|
81
82
|
actions.push({
|
|
@@ -27,26 +27,23 @@ export const wagtailBlockGenerator: PlopGeneratorConfig = {
|
|
|
27
27
|
message: 'Nom de l\'application cible',
|
|
28
28
|
},
|
|
29
29
|
],
|
|
30
|
-
actions: (data) => {
|
|
30
|
+
actions: (data: any) => {
|
|
31
31
|
const actions = [];
|
|
32
|
-
const
|
|
32
|
+
const appDir = data.config?.appDir || 'app';
|
|
33
33
|
|
|
34
34
|
// Ensure suffix
|
|
35
35
|
data.name = ensureSuffix(data.name, 'Block');
|
|
36
36
|
|
|
37
|
-
// This variable blocksFile is not really used by Plop actions directly but was here for calculation
|
|
38
|
-
// const blocksFile = path.join('app', appPath, 'blocks.py');
|
|
39
|
-
|
|
40
37
|
// 1. Ensure blocks.py exists and Append the block
|
|
41
38
|
actions.push(...createAppendActions({
|
|
42
|
-
path:
|
|
39
|
+
path: `${appDir}/${data.app}/blocks.py`,
|
|
43
40
|
templateFile: 'generators/wagtail-block/templates/block_class.py.hbs'
|
|
44
41
|
}));
|
|
45
42
|
|
|
46
43
|
// 3. Create the template
|
|
47
44
|
actions.push({
|
|
48
45
|
type: 'add',
|
|
49
|
-
path:
|
|
46
|
+
path: `${appDir}/{{app}}/templates/{{app}}/blocks/{{snakeCase name}}.html`,
|
|
50
47
|
templateFile: 'generators/wagtail-block/templates/block_template.html.hbs',
|
|
51
48
|
});
|
|
52
49
|
|
|
@@ -9,7 +9,8 @@ import { createAppendActions } from '../../utils/plop-actions';
|
|
|
9
9
|
* @returns {ActionType[]} An array of Plop actions to create/update model files.
|
|
10
10
|
*/
|
|
11
11
|
export const getModelActions = (data: any): ActionType[] => {
|
|
12
|
-
const
|
|
12
|
+
const appDir = data.config?.appDir || 'app';
|
|
13
|
+
const appPath = `${appDir}/${data.app}`;
|
|
13
14
|
|
|
14
15
|
return createAppendActions({
|
|
15
16
|
path: `${appPath}/models/models.py`,
|
|
@@ -10,7 +10,8 @@ import { ensureSuffix, createAppendActions } from '../../utils/plop-actions';
|
|
|
10
10
|
* @returns {ActionType[]} An array of Plop actions to create/update orderable files.
|
|
11
11
|
*/
|
|
12
12
|
export const getOrderableActions = (data: any): ActionType[] => {
|
|
13
|
-
const
|
|
13
|
+
const appDir = data.config?.appDir || 'app';
|
|
14
|
+
const appPath = `${appDir}/${data.app}`;
|
|
14
15
|
|
|
15
16
|
data.name = ensureSuffix(data.name, 'Orderable');
|
|
16
17
|
|
|
@@ -11,7 +11,8 @@ import { createAppendActions } from '../../utils/plop-actions';
|
|
|
11
11
|
*/
|
|
12
12
|
export const getPageActions = (data: any): ActionType[] => {
|
|
13
13
|
const actions: ActionType[] = [];
|
|
14
|
-
const
|
|
14
|
+
const appDir = data.config?.appDir || 'app';
|
|
15
|
+
const appPath = `${appDir}/${data.app}`;
|
|
15
16
|
|
|
16
17
|
// Clean base name: Remove 'Page' if present to avoid BlogPagePage
|
|
17
18
|
const baseName = data.name.replace(/Page$/, '');
|
|
@@ -8,7 +8,8 @@ import { createAppendActions, ensureSuffix } from '../../utils/plop-actions';
|
|
|
8
8
|
* @returns {ActionType[]} An array of Plop actions to create snippet files and tests.
|
|
9
9
|
*/
|
|
10
10
|
export const getSnippetActions = (data: any): ActionType[] => {
|
|
11
|
-
const
|
|
11
|
+
const appDir = data.config?.appDir || 'app';
|
|
12
|
+
const appPath = `${appDir}/${data.app}`;
|
|
12
13
|
|
|
13
14
|
data.name = ensureSuffix(data.name, 'Snippet');
|
|
14
15
|
|