@flowdevcli/flowdev 1.0.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/.dockerignore +7 -0
- package/.env.example +5 -0
- package/.eslintrc.json +0 -0
- package/Dockerfile +14 -0
- package/bin/flowdev.js +32 -0
- package/docker-compose.yml +9 -0
- package/k8s.yaml +32 -0
- package/package.json +51 -0
- package/src/commands/ai/ask.js +178 -0
- package/src/commands/ai/audit.js +78 -0
- package/src/commands/ai/explain.js +66 -0
- package/src/commands/ai/test.js +73 -0
- package/src/commands/devops/ci.js +0 -0
- package/src/commands/devops/dockerize.js +90 -0
- package/src/commands/devops/env.js +125 -0
- package/src/commands/devops/kube.js +87 -0
- package/src/commands/scaffold/find.js +118 -0
- package/src/commands/scaffold/generate.js +247 -0
- package/src/commands/scaffold/readme.js +54 -0
- package/src/commands/system/update.js +35 -0
- package/src/commands/utils/stats.js +27 -0
- package/src/commands/utils/tree.js +10 -0
- package/src/core/cli.js +113 -0
- package/src/services/analyzer.js +77 -0
- package/src/services/file-system.js +31 -0
- package/src/templates/docker/templates.js +40 -0
- package/src/utils/ascii.js +49 -0
- package/src/utils/engine-check.js +71 -0
- package/src/utils/logger.js +23 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { generateTree } from '../../services/file-system.js';
|
|
3
|
+
|
|
4
|
+
export async function treeCommand() {
|
|
5
|
+
console.log(chalk.bold.cyan('\nProject\' tree :'));
|
|
6
|
+
console.log(chalk.cyan('.\n'));
|
|
7
|
+
|
|
8
|
+
const tree = await generateTree(process.cwd());
|
|
9
|
+
console.log(tree);
|
|
10
|
+
}
|
package/src/core/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { statsCommand } from '../commands/utils/stats.js';
|
|
5
|
+
import { treeCommand } from '../commands/utils/tree.js';
|
|
6
|
+
import { dockerizeCommand } from '../commands/devops/dockerize.js';
|
|
7
|
+
import {findCommand} from '../commands/scaffold/find.js'
|
|
8
|
+
import { askCommand } from '../commands/ai/ask.js';
|
|
9
|
+
import { explainCommand } from '../commands/ai/explain.js';
|
|
10
|
+
import { envCommand } from '../commands/devops/env.js';
|
|
11
|
+
import { kubeCommand } from '../commands/devops/kube.js';
|
|
12
|
+
import { generateCommand } from '../commands/scaffold/generate.js';
|
|
13
|
+
import { auditCommand } from '../commands/ai/audit.js';
|
|
14
|
+
import { testCommand } from '../commands/ai/test.js';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const pkg = require('../../package.json');
|
|
19
|
+
|
|
20
|
+
export function setupCLI() {
|
|
21
|
+
program
|
|
22
|
+
.name('flowdev')
|
|
23
|
+
.description('Intelligent CLI to automate your workflow')
|
|
24
|
+
.version(pkg.version, '-v, --version', 'output the current version');
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('version')
|
|
29
|
+
.description('Display the current version of FlowDev')
|
|
30
|
+
.action(() => {
|
|
31
|
+
console.log(`\n ${chalk.bold('FLOWDEV')} ${chalk.cyan('v' + pkg.version)}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('tree')
|
|
36
|
+
.description('Displays the project tree')
|
|
37
|
+
.action(treeCommand);
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
program.on('command:*', () => {
|
|
41
|
+
console.error('Invalid command. Type "flowdev --help" to see the list.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('dockerize')
|
|
47
|
+
.description('Automatically generates Dockerfile and docker-compose.yml')
|
|
48
|
+
.action(dockerizeCommand);
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.command('stats')
|
|
52
|
+
.description('Analyzes the current project and displays code statistics')
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await statsCommand();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command('find <pattern>')
|
|
60
|
+
.description('Search for a text in the project')
|
|
61
|
+
.option('-e, --ext <extensions>', ' Filter by extensions(ex: js,md,json)')
|
|
62
|
+
.action((pattern, options) => findCommand(pattern, options));
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command('ask <question...>')
|
|
66
|
+
.description('Ask a question to the local AI ')
|
|
67
|
+
.action((args) => {
|
|
68
|
+
const question = args.join('');
|
|
69
|
+
askCommand(question)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command('explain <file>')
|
|
74
|
+
.description('Analyzes and explains the contents of a source file')
|
|
75
|
+
.action(async (file) => {
|
|
76
|
+
await explainCommand(file);
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('env')
|
|
81
|
+
.description('Scans project code and generates a .env.example file')
|
|
82
|
+
.action(async () => {
|
|
83
|
+
await envCommand();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('kube')
|
|
88
|
+
.description('Generate Kubernetes deployment and service manifests')
|
|
89
|
+
.action(async () => {
|
|
90
|
+
await kubeCommand();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('generate')
|
|
95
|
+
.description('Generates a complete project (React, Django, Vue, etc...) with Git and dependencies')
|
|
96
|
+
.action(generateCommand);
|
|
97
|
+
|
|
98
|
+
program
|
|
99
|
+
.command('audit')
|
|
100
|
+
.description('Audit your code for bugs, security, and performance with AI')
|
|
101
|
+
.action(async () => {
|
|
102
|
+
await auditCommand();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command('test <file>')
|
|
107
|
+
.description('Automatically generates unit tests for a specific file')
|
|
108
|
+
.action(async (file) => {
|
|
109
|
+
await testCommand(file);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return program;
|
|
113
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'target']);
|
|
6
|
+
const SUPPORTED_EXTENSIONS = {
|
|
7
|
+
'.js': 'JavaScript',
|
|
8
|
+
'.ts': 'TypeScript',
|
|
9
|
+
'.py': 'Python',
|
|
10
|
+
'.go': 'Go',
|
|
11
|
+
'.java': 'Java',
|
|
12
|
+
'.rb': 'Ruby',
|
|
13
|
+
'.php': 'PHP',
|
|
14
|
+
'.html': 'HTML',
|
|
15
|
+
'.css': 'CSS',
|
|
16
|
+
'.json': 'JSON',
|
|
17
|
+
'.yml': 'YAML',
|
|
18
|
+
'.yaml': 'YAML'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const countLinesInFile = (filePath) => {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
let count = 0;
|
|
24
|
+
createReadStream(filePath)
|
|
25
|
+
.on('data', (chunk) => {
|
|
26
|
+
for (let i = 0; i < chunk.length; ++i) {
|
|
27
|
+
if (chunk[i] === 10) count++;
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.on('end', () => resolve(count))
|
|
31
|
+
.on('error', () => resolve(0));
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function scanDirectory(dirPath, stats) {
|
|
36
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
37
|
+
|
|
38
|
+
const tasks = entries.map(async (entry) => {
|
|
39
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
40
|
+
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
43
|
+
await scanDirectory(fullPath, stats);
|
|
44
|
+
}
|
|
45
|
+
} else if (entry.isFile()) {
|
|
46
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
47
|
+
if (SUPPORTED_EXTENSIONS[ext]) {
|
|
48
|
+
const lang = SUPPORTED_EXTENSIONS[ext];
|
|
49
|
+
const lines = await countLinesInFile(fullPath);
|
|
50
|
+
stats.totalFiles++;
|
|
51
|
+
stats.totalLines += lines;
|
|
52
|
+
if (!stats.languages[lang]) {
|
|
53
|
+
stats.languages[lang] = { files: 0, lines: 0 };
|
|
54
|
+
}
|
|
55
|
+
stats.languages[lang].files++;
|
|
56
|
+
stats.languages[lang].lines += lines;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
await Promise.all(tasks);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function analyzeProject(rootPath = process.cwd()) {
|
|
64
|
+
const stats = {
|
|
65
|
+
totalFiles: 0,
|
|
66
|
+
totalLines: 0,
|
|
67
|
+
languages: {},
|
|
68
|
+
timestamp: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await scanDirectory(rootPath, stats);
|
|
73
|
+
return stats;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`Erreur lors de l'analyse du projet : ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
const IGNORED = new Set(['node_modules', '.git', 'dist']);
|
|
6
|
+
|
|
7
|
+
export async function generateTree(dir, prefix = '') {
|
|
8
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
9
|
+
let output = '';
|
|
10
|
+
|
|
11
|
+
entries.sort((a, b) => b.isDirectory() - a.isDirectory());
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < entries.length; i++) {
|
|
14
|
+
const entry = entries[i];
|
|
15
|
+
if (IGNORED.has(entry.name)) continue;
|
|
16
|
+
|
|
17
|
+
const isLast = i === entries.length - 1;
|
|
18
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
19
|
+
const name = entry.isDirectory()
|
|
20
|
+
? chalk.blue.bold(entry.name + '/')
|
|
21
|
+
: chalk.white(entry.name);
|
|
22
|
+
|
|
23
|
+
output += `${prefix}${connector}${name}\n`;
|
|
24
|
+
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
27
|
+
output += await generateTree(path.join(dir, entry.name), newPrefix);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return output;
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
export const nodeDockerfile = (version, port, command) => `
|
|
3
|
+
# Stage 1: Build
|
|
4
|
+
FROM node:${version}-alpine AS builder
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm ci
|
|
8
|
+
COPY . .
|
|
9
|
+
|
|
10
|
+
# Stage 2: Production
|
|
11
|
+
FROM node:${version}-alpine
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY --from=builder /app ./
|
|
14
|
+
EXPOSE ${port}
|
|
15
|
+
# Formats the command string into a JSON array for CMD
|
|
16
|
+
CMD ["${command.split(' ').join('", "')}"]
|
|
17
|
+
`.trim();
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export const pythonDockerfile = (version, port, startFile) => `
|
|
21
|
+
FROM python:${version}-slim
|
|
22
|
+
WORKDIR /app
|
|
23
|
+
COPY requirements.txt .
|
|
24
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
25
|
+
COPY . .
|
|
26
|
+
EXPOSE ${port}
|
|
27
|
+
CMD ["python", "${startFile}"]
|
|
28
|
+
`.trim();
|
|
29
|
+
|
|
30
|
+
export const dockerCompose = (projectName, port) => `
|
|
31
|
+
version: '3.8'
|
|
32
|
+
services:
|
|
33
|
+
${projectName}:
|
|
34
|
+
build: .
|
|
35
|
+
ports:
|
|
36
|
+
- "${port}:${port}"
|
|
37
|
+
environment:
|
|
38
|
+
- NODE_ENV=production
|
|
39
|
+
restart: always
|
|
40
|
+
`.trim();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export async function showLogo() {
|
|
6
|
+
try {
|
|
7
|
+
const text = 'FLOWDEV';
|
|
8
|
+
const subTitle = "System intelligence & Workflow automation";
|
|
9
|
+
|
|
10
|
+
const data = figlet.textSync(text, {
|
|
11
|
+
font: 'ANSI Shadow',
|
|
12
|
+
horizontalLayout: 'full',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const colors = [
|
|
16
|
+
{ color: '#fe0000', pos: 0 },
|
|
17
|
+
{ color: '#f02e20', pos: 0.2 },
|
|
18
|
+
{ color: '#f35322', pos: 0.6 },
|
|
19
|
+
{ color: '#ff6a00', pos: 1 }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const flowGradient = gradient(colors);
|
|
23
|
+
|
|
24
|
+
const lines = data.split('\n').filter(line => line.trim() !== "");
|
|
25
|
+
const maxLength = Math.max(...lines.map(l => l.length));
|
|
26
|
+
|
|
27
|
+
const padding = " ";
|
|
28
|
+
|
|
29
|
+
const b = (char) => chalk.whiteBright.bold(char);
|
|
30
|
+
|
|
31
|
+
console.log("");
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
console.log(`${padding}${b('┏━')} ${" ".repeat(maxLength - 2)} ${b('━┓')}`);
|
|
35
|
+
|
|
36
|
+
lines.forEach(line => {
|
|
37
|
+
console.log(`${padding} ${flowGradient(line)}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(`${padding} ${chalk.whiteBright.bold(subTitle)}`);
|
|
41
|
+
|
|
42
|
+
console.log(`${padding}${b('┗━')} ${" ".repeat(maxLength - 2)} ${b('━┛')}`);
|
|
43
|
+
|
|
44
|
+
console.log("");
|
|
45
|
+
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.log(chalk.red.bold('\n FLOWDEV'));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import ollama from 'ollama';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { exec, spawn } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
|
|
11
|
+
async function waitForOllamaServer({ retries = 15, delayMs = 1000 } = {}) {
|
|
12
|
+
for (let i = 0; i < retries; i++) {
|
|
13
|
+
try {
|
|
14
|
+
await ollama.list();
|
|
15
|
+
return true;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
await sleep(delayMs);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function installOllamaEngine(spinner) {
|
|
24
|
+
const isWindows = process.platform === 'win32';
|
|
25
|
+
const installCmd = isWindows
|
|
26
|
+
? 'winget install Ollama.Ollama --silent --accept-source-agreements'
|
|
27
|
+
: 'curl -fsSL https://ollama.com/install.sh | sh';
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
spinner.text = chalk.yellow("Installing FlowDev's engine package to run the command... (This may take a while)");
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await execAsync(installCmd, { maxBuffer: 10 * 1024 * 1024 });
|
|
34
|
+
spinner.succeed(chalk.green('FlowDev Engine installed successfully!'));
|
|
35
|
+
await sleep(1500);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
spinner.fail(chalk.red('Automatic installation failed.'));
|
|
38
|
+
logger.error(`Please install it manually: ${isWindows ? 'winget install Ollama.Ollama' : 'curl -fsSL https://ollama.com/install.sh | sh'}`);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function ensureEngineReady(spinner, modelName = 'llama3') {
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await execAsync('ollama --version');
|
|
47
|
+
} catch (e) {
|
|
48
|
+
await installOllamaEngine(spinner);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!(await waitForOllamaServer({ retries: 3, delayMs: 1000 }))) {
|
|
52
|
+
spinner.text = chalk.blue('Starting engine in background...');
|
|
53
|
+
const child = spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' });
|
|
54
|
+
child.unref();
|
|
55
|
+
|
|
56
|
+
const ready = await waitForOllamaServer({ retries: 30, delayMs: 1000 });
|
|
57
|
+
if (!ready) throw new Error('Engine server failed to start.');
|
|
58
|
+
}
|
|
59
|
+
let hasModel = false;
|
|
60
|
+
try {
|
|
61
|
+
const models = await ollama.list();
|
|
62
|
+
hasModel = Array.isArray(models?.models) && models.models.some((m) => m.name?.startsWith(modelName));
|
|
63
|
+
} catch (err) { }
|
|
64
|
+
|
|
65
|
+
if (!hasModel) {
|
|
66
|
+
spinner.text = chalk.magenta(`First launch: Downloading AI neurons (${modelName})...`);
|
|
67
|
+
await ollama.pull({ model: modelName });
|
|
68
|
+
spinner.succeed(chalk.green('AI brain loaded!'));
|
|
69
|
+
spinner.start();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg) => {
|
|
5
|
+
console.log(`${chalk.blue.bold(' INFO')} ${msg}`);
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
success: (msg) => {
|
|
9
|
+
console.log(`${chalk.green.bold(' SUCCESS')} ${msg}`);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
warn: (msg) => {
|
|
13
|
+
console.log(`${chalk.yellow.bold(' WARN')} ${msg}`);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
error: (msg) => {
|
|
17
|
+
console.log(`${chalk.red.bold(' ERROR')} ${msg}`);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
log: (msg) => {
|
|
21
|
+
console.log(msg);
|
|
22
|
+
}
|
|
23
|
+
};
|