@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,125 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const SCAN_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.json', '.php', '.py', '.go', '.java'];
|
|
8
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', 'coverage', '.next', 'vendor'];
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async function getFiles(dir) {
|
|
12
|
+
const subdirs = await fs.readdir(dir);
|
|
13
|
+
const files = await Promise.all(subdirs.map(async (subdir) => {
|
|
14
|
+
const res = path.resolve(dir, subdir);
|
|
15
|
+
|
|
16
|
+
if (IGNORE_DIRS.includes(subdir)) return [];
|
|
17
|
+
|
|
18
|
+
const stat = await fs.stat(res);
|
|
19
|
+
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
return getFiles(res);
|
|
22
|
+
} else {
|
|
23
|
+
|
|
24
|
+
if (SCAN_EXTENSIONS.includes(path.extname(res))) {
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
return files.reduce((a, f) => a.concat(f), []);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractEnvVars(content) {
|
|
34
|
+
const vars = new Set();
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const regex = /\b(?:process\.env\.|import\.meta\.env\.|process\.env\[['"])([A-Z_][A-Z0-9_]*)\b/g;
|
|
38
|
+
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = regex.exec(content)) !== null) {
|
|
41
|
+
|
|
42
|
+
if (match[1] && match[1] !== 'NODE_ENV') {
|
|
43
|
+
vars.add(match[1]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Array.from(vars);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function envCommand() {
|
|
51
|
+
const spinner = ora(chalk.cyan('Scanning project for environment variables...')).start();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const rootDir = process.cwd();
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
const files = await getFiles(rootDir);
|
|
58
|
+
spinner.text = chalk.cyan(`Analyzing ${files.length} files...`);
|
|
59
|
+
|
|
60
|
+
const allEnvVars = new Set();
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
66
|
+
const vars = extractEnvVars(content);
|
|
67
|
+
vars.forEach(v => allEnvVars.add(v));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (allEnvVars.size === 0) {
|
|
74
|
+
spinner.warn(chalk.yellow('No environment variables found in code.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
const sortedVars = Array.from(allEnvVars).sort();
|
|
80
|
+
let envContent = `# Auto-generated by FlowDev\n# Environment Variables Example\n\n`;
|
|
81
|
+
|
|
82
|
+
sortedVars.forEach(variable => {
|
|
83
|
+
envContent += `${variable}=\n`;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const outputPath = path.join(rootDir, '.env.example');
|
|
87
|
+
|
|
88
|
+
if (await fs.pathExists(outputPath)) {
|
|
89
|
+
|
|
90
|
+
const oldContent = await fs.readFile(outputPath, 'utf-8');
|
|
91
|
+
const existingLines = oldContent.split('\n');
|
|
92
|
+
|
|
93
|
+
let newCount = 0;
|
|
94
|
+
let appendContent = "\n# New variables detected by FlowDev:\n";
|
|
95
|
+
|
|
96
|
+
sortedVars.forEach(variable => {
|
|
97
|
+
if (!oldContent.includes(variable + '=')) {
|
|
98
|
+
appendContent += `${variable}=\n`;
|
|
99
|
+
newCount++;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (newCount > 0) {
|
|
104
|
+
await fs.appendFile(outputPath, appendContent);
|
|
105
|
+
spinner.succeed(chalk.green(`Updated .env.example with ${newCount} new variables!`));
|
|
106
|
+
} else {
|
|
107
|
+
spinner.succeed(chalk.green('.env.example is already up to date.'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
} else {
|
|
111
|
+
|
|
112
|
+
await fs.writeFile(outputPath, envContent);
|
|
113
|
+
spinner.succeed(chalk.green(`Created .env.example with ${allEnvVars.size} variables!`));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
console.log(chalk.gray('\nDetected variables:'));
|
|
118
|
+
sortedVars.forEach(v => console.log(chalk.blue(`- ${v}`)));
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
spinner.stop();
|
|
123
|
+
logger.error(`Error generating .env file: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
|
|
6
|
+
export async function kubeCommand() {
|
|
7
|
+
const rootDir = process.cwd();
|
|
8
|
+
const spinner = ora(chalk.cyan('Checking project readiness...')).start();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const projectName = path.basename(rootDir).toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
12
|
+
let port = 3000;
|
|
13
|
+
let hasDockerfile = await fs.pathExists(path.join(rootDir, 'Dockerfile'));
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if (hasDockerfile) {
|
|
17
|
+
const dockerfile = await fs.readFile('Dockerfile', 'utf-8');
|
|
18
|
+
const exposeMatch = dockerfile.match(/EXPOSE\s+(\d+)/);
|
|
19
|
+
if (exposeMatch) port = exposeMatch[1];
|
|
20
|
+
spinner.text = chalk.blue('Docker configuration detected. Syncing ports...');
|
|
21
|
+
} else {
|
|
22
|
+
|
|
23
|
+
spinner.text = chalk.yellow('No Dockerfile found. Using smart defaults...');
|
|
24
|
+
if (await fs.pathExists('package.json')) {
|
|
25
|
+
const pkg = await fs.readJson('package.json');
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
spinner.text = chalk.magenta('Generating Kubernetes Manifests...');
|
|
31
|
+
|
|
32
|
+
const k8sContent = `
|
|
33
|
+
apiVersion: apps/v1
|
|
34
|
+
kind: Deployment
|
|
35
|
+
metadata:
|
|
36
|
+
name: ${projectName}
|
|
37
|
+
spec:
|
|
38
|
+
replicas: 2
|
|
39
|
+
selector:
|
|
40
|
+
matchLabels:
|
|
41
|
+
app: ${projectName}
|
|
42
|
+
template:
|
|
43
|
+
metadata:
|
|
44
|
+
labels:
|
|
45
|
+
app: ${projectName}
|
|
46
|
+
spec:
|
|
47
|
+
containers:
|
|
48
|
+
- name: ${projectName}
|
|
49
|
+
image: ${projectName}:latest
|
|
50
|
+
ports:
|
|
51
|
+
- containerPort: ${port}
|
|
52
|
+
---
|
|
53
|
+
apiVersion: v1
|
|
54
|
+
kind: Service
|
|
55
|
+
metadata:
|
|
56
|
+
name: ${projectName}-service
|
|
57
|
+
spec:
|
|
58
|
+
selector:
|
|
59
|
+
app: ${projectName}
|
|
60
|
+
ports:
|
|
61
|
+
- protocol: TCP
|
|
62
|
+
port: 80
|
|
63
|
+
targetPort: ${port}
|
|
64
|
+
type: LoadBalancer
|
|
65
|
+
`.trim();
|
|
66
|
+
|
|
67
|
+
await fs.writeFile(path.join(rootDir, 'k8s.yaml'), k8sContent);
|
|
68
|
+
|
|
69
|
+
spinner.succeed(chalk.green('Kubernetes manifests generated!'));
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if (!hasDockerfile) {
|
|
73
|
+
console.log(chalk.yellow(`\n Warning: No Dockerfile found in this directory.`));
|
|
74
|
+
console.log(`Kubernetes needs a Docker image to run your app.`);
|
|
75
|
+
console.log(`Recommended: Run ${chalk.bold('flowdev dockerize')} first.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(chalk.gray('\nConfiguration details:'));
|
|
79
|
+
console.log(`${chalk.blue('→ Deployment Name:')} ${projectName}`);
|
|
80
|
+
console.log(`${chalk.blue('→ Target Port:')} ${port}`);
|
|
81
|
+
console.log(`\nNext: ${chalk.yellow('kubectl apply -f k8s.yaml')}`);
|
|
82
|
+
|
|
83
|
+
} catch (error) {
|
|
84
|
+
spinner.fail(chalk.red('Kube generation failed.'));
|
|
85
|
+
console.error(error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.idea', '.vscode']);
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const BINARY_EXTS = new Set([
|
|
12
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg',
|
|
13
|
+
'.pdf', '.zip', '.tar', '.gz', '.7z', '.rar',
|
|
14
|
+
'.exe', '.bin', '.dll', '.so', '.dylib',
|
|
15
|
+
'.lock', '.pyc', '.mp3', '.mp4'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export async function findCommand(pattern, options) {
|
|
19
|
+
let targetExtensions = null;
|
|
20
|
+
if (options.ext) {
|
|
21
|
+
targetExtensions = new Set(
|
|
22
|
+
options.ext.split(',').map(e => {
|
|
23
|
+
let clean = e.trim().toLowerCase();
|
|
24
|
+
clean = clean.replace('*', '');
|
|
25
|
+
return clean.startsWith('.') ? clean : '.' + clean;
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const searchRegex = new RegExp(pattern, 'i');
|
|
31
|
+
const spinner = ora(`Search for "${chalk.cyan(pattern)}"...`).start();
|
|
32
|
+
|
|
33
|
+
let matchCount = 0;
|
|
34
|
+
let fileCount = 0;
|
|
35
|
+
const startDir = process.cwd();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await scanDirectory(startDir);
|
|
39
|
+
|
|
40
|
+
spinner.stop();
|
|
41
|
+
|
|
42
|
+
if (matchCount === 0) {
|
|
43
|
+
console.log(chalk.yellow(`No results for "${pattern}".`));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(chalk.green(`\nDone ! ${matchCount} occurrences found in ${fileCount} scanned files.`));
|
|
46
|
+
}
|
|
47
|
+
console.log('\n');
|
|
48
|
+
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spinner.fail('Error during search');
|
|
51
|
+
console.error(err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async function scanDirectory(dir) {
|
|
57
|
+
try {
|
|
58
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullPath = path.join(dir, entry.name);
|
|
62
|
+
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
if (!IGNORED_DIRS.has(entry.name)) {
|
|
65
|
+
await scanDirectory(fullPath);
|
|
66
|
+
}
|
|
67
|
+
} else if (entry.isFile()) {
|
|
68
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
69
|
+
|
|
70
|
+
let shouldScan = false;
|
|
71
|
+
|
|
72
|
+
if (targetExtensions) {
|
|
73
|
+
if (targetExtensions.has(ext)) shouldScan = true;
|
|
74
|
+
} else {
|
|
75
|
+
if (!BINARY_EXTS.has(ext)) shouldScan = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (shouldScan) {
|
|
79
|
+
fileCount++;
|
|
80
|
+
await searchInFile(fullPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function searchInFile(filePath) {
|
|
90
|
+
const fileStream = fs.createReadStream(filePath);
|
|
91
|
+
|
|
92
|
+
const rl = createInterface({
|
|
93
|
+
input: fileStream,
|
|
94
|
+
crlfDelay: Infinity
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let lineNum = 0;
|
|
98
|
+
let foundInFile = false;
|
|
99
|
+
|
|
100
|
+
for await (const line of rl) {
|
|
101
|
+
lineNum++;
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if (searchRegex.test(line)) {
|
|
105
|
+
matchCount++;
|
|
106
|
+
|
|
107
|
+
if (!foundInFile) {
|
|
108
|
+
const relativePath = path.relative(startDir, filePath);
|
|
109
|
+
console.log(chalk.bold.blue(`\n ${relativePath}`));
|
|
110
|
+
foundInFile = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const highlightedLine = line.replace(searchRegex, (match) => chalk.bgYellow.black(match));
|
|
114
|
+
console.log(` ${chalk.gray(lineNum.toString().padEnd(4))} │ ${highlightedLine.trim()}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
|
10
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
11
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const PYTHON_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
15
|
+
const GENERAL_REGEX = /^[a-zA-Z][a-zA-Z0-9-_]*$/;
|
|
16
|
+
|
|
17
|
+
export async function generateCommand() {
|
|
18
|
+
const answers = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'list',
|
|
21
|
+
name: 'type',
|
|
22
|
+
message: 'Choose your project template:',
|
|
23
|
+
choices: [
|
|
24
|
+
{ name: 'React + Tailwind (Vite)', value: 'react-tailwind' },
|
|
25
|
+
{ name: 'Vue + Tailwind (Vite)', value: 'vue-tailwind' },
|
|
26
|
+
{ name: 'Django (Project + App + Venv)', value: 'django' },
|
|
27
|
+
{ name: 'Angular (Workspace)', value: 'angular' },
|
|
28
|
+
{ name: 'Express API (Minimal)', value: 'express' }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'projectName',
|
|
34
|
+
message: 'Project name:',
|
|
35
|
+
default: 'my_flow_app',
|
|
36
|
+
validate: (input, currentAnswers) => {
|
|
37
|
+
const name = input.trim();
|
|
38
|
+
if (!name) return 'A name is required.';
|
|
39
|
+
|
|
40
|
+
if (/^\d/.test(name)) {
|
|
41
|
+
return ' Name cannot start with a number. Please use a letter.';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if (currentAnswers.type === 'django') {
|
|
46
|
+
if (!PYTHON_REGEX.test(name)) {
|
|
47
|
+
return ' Django projects cannot contain dashes (-) or spaces. Use underscores (_) instead.';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
else {
|
|
52
|
+
if (!GENERAL_REGEX.test(name)) {
|
|
53
|
+
return ' Invalid name. Use letters, numbers, dashes (-) or underscores (_).';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if (answers.type === 'django') {
|
|
64
|
+
const djangoSub = await inquirer.prompt([{
|
|
65
|
+
type: 'input',
|
|
66
|
+
name: 'appName',
|
|
67
|
+
message: 'Initial app name:',
|
|
68
|
+
default: 'core',
|
|
69
|
+
validate: (input) => {
|
|
70
|
+
const name = input.trim();
|
|
71
|
+
|
|
72
|
+
if (!PYTHON_REGEX.test(name)) {
|
|
73
|
+
return ' Invalid Python App name. Must start with a letter and contain no dashes.';
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}]);
|
|
78
|
+
answers.appName = djangoSub.appName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const projectDir = path.resolve(process.cwd(), answers.projectName);
|
|
82
|
+
|
|
83
|
+
if (await fs.pathExists(projectDir)) {
|
|
84
|
+
logger.error(`Error: Directory "${answers.projectName}" already exists.`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const spinner = ora(chalk.magenta('Generating your project...')).start();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
switch (answers.type) {
|
|
92
|
+
case 'react-tailwind': await setupVite(projectDir, answers.projectName, 'react', true, spinner); break;
|
|
93
|
+
case 'vue-tailwind': await setupVite(projectDir, answers.projectName, 'vue', true, spinner); break;
|
|
94
|
+
case 'django': await setupDjango(projectDir, answers, spinner); break;
|
|
95
|
+
case 'angular': await setupAngular(projectDir, answers.projectName, spinner); break;
|
|
96
|
+
case 'express': await setupExpress(projectDir, answers.projectName, spinner); break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await initGit(projectDir, spinner);
|
|
100
|
+
|
|
101
|
+
spinner.succeed(chalk.green(`Project "${answers.projectName}" is ready and configured!`));
|
|
102
|
+
showSuccessTips(answers);
|
|
103
|
+
|
|
104
|
+
} catch (error) {
|
|
105
|
+
spinner.fail(chalk.red('Generation failed.'));
|
|
106
|
+
console.error(chalk.red(`\nDetailed Error: ${error.message}`));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async function setupVite(dir, name, framework, withTailwind, spinner) {
|
|
113
|
+
spinner.text = `Scaffolding ${framework} with Vite...`;
|
|
114
|
+
execSync(`${npmCmd} create vite@latest "${name}" -- --template ${framework}`, { stdio: 'ignore' });
|
|
115
|
+
|
|
116
|
+
const originalDir = process.cwd();
|
|
117
|
+
process.chdir(dir);
|
|
118
|
+
|
|
119
|
+
spinner.text = 'Installing core dependencies...';
|
|
120
|
+
execSync(`${npmCmd} install`, { stdio: 'ignore' });
|
|
121
|
+
|
|
122
|
+
const folders = ['components', 'services', 'utils', 'hooks', 'assets'];
|
|
123
|
+
for (const f of folders) await fs.ensureDir(path.join(dir, 'src', f));
|
|
124
|
+
|
|
125
|
+
if (withTailwind) {
|
|
126
|
+
spinner.text = 'Installing & Configuring Tailwind CSS (Manual Setup)...';
|
|
127
|
+
execSync(`${npmCmd} install -D tailwindcss postcss autoprefixer`, { stdio: 'ignore' });
|
|
128
|
+
|
|
129
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
130
|
+
export default {
|
|
131
|
+
content: [
|
|
132
|
+
"./index.html",
|
|
133
|
+
"./src/**/*.{js,ts,jsx,tsx,vue}",
|
|
134
|
+
],
|
|
135
|
+
theme: {
|
|
136
|
+
extend: {},
|
|
137
|
+
},
|
|
138
|
+
plugins: [],
|
|
139
|
+
}`;
|
|
140
|
+
await fs.writeFile(path.join(dir, 'tailwind.config.js'), tailwindConfig);
|
|
141
|
+
|
|
142
|
+
const postcssConfig = `export default {
|
|
143
|
+
plugins: {
|
|
144
|
+
tailwindcss: {},
|
|
145
|
+
autoprefixer: {},
|
|
146
|
+
},
|
|
147
|
+
}`;
|
|
148
|
+
await fs.writeFile(path.join(dir, 'postcss.config.js'), postcssConfig);
|
|
149
|
+
|
|
150
|
+
const cssPath = path.join(dir, 'src', 'index.css');
|
|
151
|
+
const tailwindDirectives = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n`;
|
|
152
|
+
|
|
153
|
+
let currentCss = "";
|
|
154
|
+
if (await fs.pathExists(cssPath)) {
|
|
155
|
+
currentCss = await fs.readFile(cssPath, 'utf-8');
|
|
156
|
+
}
|
|
157
|
+
await fs.writeFile(cssPath, tailwindDirectives + currentCss);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.chdir(originalDir);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function setupDjango(dir, data, spinner) {
|
|
164
|
+
const { projectName, appName } = data;
|
|
165
|
+
const isWin = process.platform === 'win32';
|
|
166
|
+
|
|
167
|
+
await fs.ensureDir(dir);
|
|
168
|
+
const originalDir = process.cwd();
|
|
169
|
+
process.chdir(dir);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
spinner.text = 'Creating virtual environment...';
|
|
173
|
+
execSync(`${pythonCmd} -m venv venv`, { stdio: 'ignore' });
|
|
174
|
+
|
|
175
|
+
const venvPython = isWin
|
|
176
|
+
? path.join(dir, 'venv', 'Scripts', 'python.exe')
|
|
177
|
+
: path.join(dir, 'venv', 'bin', 'python');
|
|
178
|
+
|
|
179
|
+
spinner.text = 'Installing Django...';
|
|
180
|
+
execSync(`"${venvPython}" -m pip install django`, { stdio: 'ignore' });
|
|
181
|
+
|
|
182
|
+
spinner.text = 'Initializing Django project...';
|
|
183
|
+
|
|
184
|
+
execSync(`"${venvPython}" -m django startproject config .`, { stdio: 'ignore' });
|
|
185
|
+
|
|
186
|
+
spinner.text = `Creating app: ${appName}...`;
|
|
187
|
+
execSync(`"${venvPython}" manage.py startapp ${appName}`, { stdio: 'ignore' });
|
|
188
|
+
|
|
189
|
+
await fs.writeFile(path.join(dir, appName, 'urls.py'),
|
|
190
|
+
`from django.urls import path\nfrom . import views\n\nurlpatterns = [ path('', views.index, name='index'), ]`);
|
|
191
|
+
|
|
192
|
+
await fs.writeFile(path.join(dir, appName, 'views.py'),
|
|
193
|
+
`from django.http import HttpResponse\n\ndef index(request):\n return HttpResponse("<h1>${projectName} is live!</h1>")`);
|
|
194
|
+
|
|
195
|
+
} finally {
|
|
196
|
+
process.chdir(originalDir);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function initGit(dir, spinner) {
|
|
201
|
+
try {
|
|
202
|
+
spinner.text = 'Initializing Git...';
|
|
203
|
+
const ignorePath = path.join(dir, '.gitignore');
|
|
204
|
+
if (!(await fs.pathExists(ignorePath))) {
|
|
205
|
+
const defaultIgnore = 'node_modules\n.env\ndist\nbuild\n__pycache__\n*.log\nvenv\n.venv\n';
|
|
206
|
+
await fs.writeFile(ignorePath, defaultIgnore);
|
|
207
|
+
}
|
|
208
|
+
const originalDir = process.cwd();
|
|
209
|
+
process.chdir(dir);
|
|
210
|
+
execSync('git init', { stdio: 'ignore' });
|
|
211
|
+
execSync('git add .', { stdio: 'ignore' });
|
|
212
|
+
execSync('git commit -m "Initial commit by FlowDev 🚀"', { stdio: 'ignore' });
|
|
213
|
+
process.chdir(originalDir);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
spinner.warn(chalk.yellow('Git initialization skipped.'));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function setupAngular(dir, name, spinner) {
|
|
220
|
+
spinner.text = 'Generating Angular Workspace...';
|
|
221
|
+
|
|
222
|
+
execSync(`${npxCmd} --yes -p @angular/cli ng new "${name}" --defaults --skip-git`, { stdio: 'ignore' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function setupExpress(dir, name, spinner) {
|
|
226
|
+
spinner.text = 'Setting up Express...';
|
|
227
|
+
await fs.ensureDir(dir);
|
|
228
|
+
const pkg = { name, version: '1.0.0', scripts: { start: 'node src/index.js' }, dependencies: { express: '^4.18.2', cors: '^2.8.5' }};
|
|
229
|
+
await fs.writeJson(path.join(dir, 'package.json'), pkg, { spaces: 2 });
|
|
230
|
+
await fs.ensureDir(path.join(dir, 'src'));
|
|
231
|
+
await fs.writeFile(path.join(dir, 'src', 'index.js'), `const express = require('express');\nconst app = express();\napp.get('/', (req, res) => res.send('API OK'));\napp.listen(3000);`);
|
|
232
|
+
process.chdir(dir);
|
|
233
|
+
execSync(`${npmCmd} install`, { stdio: 'ignore' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function showSuccessTips(data) {
|
|
237
|
+
console.log(chalk.yellow('\n Tips :'));
|
|
238
|
+
console.log(`${chalk.white('*')} cd ${chalk.bold(data.projectName)}`);
|
|
239
|
+
if (data.type.includes('tailwind')) {
|
|
240
|
+
console.log(`${chalk.green('*')} Tailwind & PostCSS are manually configured.`);
|
|
241
|
+
console.log(`${chalk.white('*')} Run: ${chalk.bold('npm run dev')}`);
|
|
242
|
+
} else if (data.type === 'django') {
|
|
243
|
+
console.log(`${chalk.white('*')} Run: ${chalk.bold('python manage.py runserver')}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`${chalk.white('*')} Run: ${chalk.bold('npm start')}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ollama from 'ollama';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { ensureEngineReady } from '../../utils/engine-check.js';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
export async function readmeCommand() {
|
|
10
|
+
const spinner = ora(chalk.cyan('Reading project structure...')).start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const rootDir = process.cwd();
|
|
14
|
+
let projectInfo = "";
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if (await fs.pathExists(path.join(rootDir, 'package.json'))) {
|
|
18
|
+
const pkg = await fs.readJson(path.join(rootDir, 'package.json'));
|
|
19
|
+
projectInfo += `Type: Node.js/Web, Name: ${pkg.name}, Deps: ${Object.keys(pkg.dependencies || {}).join(', ')}`;
|
|
20
|
+
} else if (await fs.pathExists(path.join(rootDir, 'manage.py'))) {
|
|
21
|
+
projectInfo += `Type: Django/Python project`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await ensureEngineReady(spinner, 'llama3');
|
|
25
|
+
|
|
26
|
+
spinner.text = chalk.magenta('Drafting your documentation...');
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const prompt = `
|
|
30
|
+
You are a technical writer. Generate a professional README.md for a project with these details: ${projectInfo}.
|
|
31
|
+
The README should include:
|
|
32
|
+
- A catchy title with an emoji.
|
|
33
|
+
- A brief description.
|
|
34
|
+
- Installation steps.
|
|
35
|
+
- How to run the project.
|
|
36
|
+
- Tech stack used.
|
|
37
|
+
|
|
38
|
+
Return ONLY the markdown content.
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const response = await ollama.chat({
|
|
42
|
+
model: 'llama3.2',
|
|
43
|
+
messages: [{ role: 'user', content: prompt }],
|
|
44
|
+
});
|
|
45
|
+
const readmePath = path.join(rootDir, 'README.md');
|
|
46
|
+
await fs.writeFile(readmePath, response.message.content);
|
|
47
|
+
|
|
48
|
+
spinner.succeed(chalk.green('README.md generated successfully! '));
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
spinner.fail(chalk.red('Failed to generate README.'));
|
|
52
|
+
logger.error(error.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export async function updateCommand() {
|
|
10
|
+
const spinner = ora(chalk.cyan('Checking for update ...')).start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const packageJsonPath = path.resolve(__dirname, '../../../package.json');
|
|
14
|
+
const pkg = await fstat.readJson(packageJsonPath);
|
|
15
|
+
|
|
16
|
+
const currentVersion = pkg.version;
|
|
17
|
+
const packageName = pkg.name;
|
|
18
|
+
let latestVersion;
|
|
19
|
+
try {
|
|
20
|
+
latestVersion = execSync(`npm views ${packageName} version` , {encoding : 'utf-8'}).trim();
|
|
21
|
+
|
|
22
|
+
} catch(e) {
|
|
23
|
+
spinner.warn(chalk.yellow('Could not found registry'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (latestVersion == currentVersion) {
|
|
27
|
+
ora(chalk.green(`Flowdev is up to date ! You are already using the latest version ${currentVersion}`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
catch(error){
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { analyzeProject } from '../../services/analyzer.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function statsCommand() {
|
|
7
|
+
const spinner = ora('Analysis of the ongoing project...').start();
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const stats = await analyzeProject();
|
|
12
|
+
const duration = Date.now() - start;
|
|
13
|
+
|
|
14
|
+
spinner.succeed(chalk.green(`Analysis completed in ${duration}ms !`));
|
|
15
|
+
console.log('\n' + chalk.bold.underline('Analysis results :'));
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
console.table(stats.languages);
|
|
19
|
+
|
|
20
|
+
console.log(`\nFiles analyzed : ${chalk.cyan(stats.totalFiles)}`);
|
|
21
|
+
console.log(` Total rows : ${chalk.cyan(stats.totalLines)}`);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
spinner.fail(chalk.red('Analysis failed.'));
|
|
24
|
+
console.error(error.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|