create-fs-cli 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/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +51 -0
- package/src/commands/create.js +251 -0
- package/src/generators/backend.js +84 -0
- package/src/generators/backendStatus.js +549 -0
- package/src/generators/frontend.js +99 -0
- package/src/index.js +34 -0
- package/src/utils/messages.js +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# FullStack CLI
|
|
2
|
+
|
|
3
|
+
A CLI tool to scaffold full-stack applications with interactive terminal UI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g fullstack-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Run the CLI
|
|
15
|
+
fullstack
|
|
16
|
+
|
|
17
|
+
# Or with a project name
|
|
18
|
+
fullstack create my-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- šØ **Interactive prompts** - Choose your stack step by step
|
|
24
|
+
- ā” **Multiple frontends** - Next.js, React + Vite, SvelteKit
|
|
25
|
+
- š§ **Multiple backends** - Express, Fastify, FastAPI, or Next.js API Routes
|
|
26
|
+
- šļø **Database ready** - PostgreSQL, MongoDB, MySQL, Supabase
|
|
27
|
+
- š **Backend status indicator** - Visual connection status in your frontend
|
|
28
|
+
|
|
29
|
+
## Stack Options
|
|
30
|
+
|
|
31
|
+
### Frontend
|
|
32
|
+
- **Next.js** - React framework with SSR/SSG
|
|
33
|
+
- **React + Vite** - Fast React development
|
|
34
|
+
- **SvelteKit** - Svelte framework
|
|
35
|
+
|
|
36
|
+
### Backend
|
|
37
|
+
- **Next.js API Routes** - Integrated with Next.js frontend
|
|
38
|
+
- **Express** - Minimalist Node.js framework
|
|
39
|
+
- **Fastify** - Fast Node.js framework
|
|
40
|
+
- **FastAPI** - Modern Python framework
|
|
41
|
+
|
|
42
|
+
### Database
|
|
43
|
+
- **PostgreSQL** - Relational database
|
|
44
|
+
- **MongoDB** - Document database
|
|
45
|
+
- **MySQL** - Relational database
|
|
46
|
+
- **Supabase** - PostgreSQL with extras
|
|
47
|
+
- **None** - No database setup
|
|
48
|
+
|
|
49
|
+
## Generated Structure
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
my-project/
|
|
53
|
+
āāā frontend/ # Your chosen frontend framework
|
|
54
|
+
ā āāā components/
|
|
55
|
+
ā āāā BackendStatus # Connection status indicator
|
|
56
|
+
āāā backend/ # Your chosen backend (if not Next.js API)
|
|
57
|
+
ā āāā config/
|
|
58
|
+
ā ā āāā db.js # Database configuration
|
|
59
|
+
ā āāā routes/
|
|
60
|
+
ā āāā .env # Environment variables
|
|
61
|
+
āāā .gitignore
|
|
62
|
+
āāā README.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-fs-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to scaffold full-stack applications with interactive prompts",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-fs-cli": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"link": "npm link"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"fullstack",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"generator",
|
|
19
|
+
"nextjs",
|
|
20
|
+
"react",
|
|
21
|
+
"vite",
|
|
22
|
+
"svelte",
|
|
23
|
+
"express",
|
|
24
|
+
"fastify",
|
|
25
|
+
"fastapi",
|
|
26
|
+
"boilerplate"
|
|
27
|
+
],
|
|
28
|
+
"author": "HemanthRaj0C",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/HemanthRaj0C/fullstack-cli.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/HemanthRaj0C/fullstack-cli/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/HemanthRaj0C/fullstack-cli#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"boxen": "^7.1.1",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"commander": "^11.1.0",
|
|
45
|
+
"execa": "^8.0.1",
|
|
46
|
+
"figlet": "^1.7.0",
|
|
47
|
+
"fs-extra": "^11.2.0",
|
|
48
|
+
"inquirer": "^9.2.12",
|
|
49
|
+
"ora": "^8.0.1"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { generateFrontend } from '../generators/frontend.js';
|
|
8
|
+
import { generateBackend } from '../generators/backend.js';
|
|
9
|
+
import { showSuccessMessage } from '../utils/messages.js';
|
|
10
|
+
|
|
11
|
+
export async function createProject(projectName) {
|
|
12
|
+
try {
|
|
13
|
+
// Build prompts - skip project name if provided via CLI
|
|
14
|
+
const prompts = [];
|
|
15
|
+
|
|
16
|
+
if (!projectName) {
|
|
17
|
+
prompts.push({
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'projectName',
|
|
20
|
+
message: 'Project name:',
|
|
21
|
+
default: 'my-fullstack-app',
|
|
22
|
+
validate: (input) => {
|
|
23
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
|
|
24
|
+
return 'Project name can only contain letters, numbers, dashes, and underscores (no spaces)';
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
prompts.push(
|
|
32
|
+
{
|
|
33
|
+
type: 'list',
|
|
34
|
+
name: 'frontend',
|
|
35
|
+
message: 'Choose frontend framework:',
|
|
36
|
+
choices: [
|
|
37
|
+
{ name: 'Next.js', value: 'nextjs' },
|
|
38
|
+
{ name: 'React + Vite', value: 'react-vite' },
|
|
39
|
+
{ name: 'Svelte', value: 'svelte' }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'list',
|
|
44
|
+
name: 'backend',
|
|
45
|
+
message: 'Choose backend framework:',
|
|
46
|
+
choices: [
|
|
47
|
+
{ name: 'Next.js API Routes (integrated)', value: 'nextjs-api' },
|
|
48
|
+
{ name: 'Express', value: 'express' },
|
|
49
|
+
{ name: 'Fastify', value: 'fastify' },
|
|
50
|
+
{ name: 'FastAPI (Python)', value: 'fastapi' }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'database',
|
|
56
|
+
message: 'Choose database:',
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: 'PostgreSQL', value: 'postgres' },
|
|
59
|
+
{ name: 'MongoDB', value: 'mongodb' },
|
|
60
|
+
{ name: 'MySQL', value: 'mysql' },
|
|
61
|
+
{ name: 'Supabase', value: 'supabase' },
|
|
62
|
+
{ name: 'None', value: 'none' }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Step 1: Gather all information
|
|
68
|
+
const answers = await inquirer.prompt(prompts);
|
|
69
|
+
|
|
70
|
+
// Use CLI argument if provided, otherwise use prompted value
|
|
71
|
+
if (projectName) {
|
|
72
|
+
answers.projectName = projectName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate backend/database combination
|
|
76
|
+
if (answers.backend === 'fastapi' && answers.database === 'mysql') {
|
|
77
|
+
console.log(chalk.yellow('\nā ļø FastAPI template does not support MySQL. Defaulting to PostgreSQL.\n'));
|
|
78
|
+
answers.database = 'postgres';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create project directory
|
|
82
|
+
const projectPath = path.join(process.cwd(), answers.projectName);
|
|
83
|
+
|
|
84
|
+
if (await fs.pathExists(projectPath)) {
|
|
85
|
+
const { overwrite } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'confirm',
|
|
88
|
+
name: 'overwrite',
|
|
89
|
+
message: `Directory ${answers.projectName} already exists. Overwrite?`,
|
|
90
|
+
default: false
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
if (!overwrite) {
|
|
95
|
+
console.log(chalk.red('Aborted.'));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
await fs.remove(projectPath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await fs.ensureDir(projectPath);
|
|
102
|
+
console.log(chalk.green(`\nš Created project directory: ${answers.projectName}\n`));
|
|
103
|
+
|
|
104
|
+
// Step 2: Generate frontend
|
|
105
|
+
await generateFrontend(answers, projectPath);
|
|
106
|
+
|
|
107
|
+
// Step 3: Generate backend (if not using Next.js API routes)
|
|
108
|
+
if (answers.backend !== 'nextjs-api') {
|
|
109
|
+
await generateBackend(answers, projectPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Step 4: Clean up and prepare project
|
|
113
|
+
await cleanupProject(projectPath);
|
|
114
|
+
|
|
115
|
+
// Step 5: Create root files
|
|
116
|
+
await createRootFiles(answers, projectPath);
|
|
117
|
+
|
|
118
|
+
// Step 6: Initialize fresh git repository
|
|
119
|
+
await initializeGit(projectPath);
|
|
120
|
+
|
|
121
|
+
// Step 7: Show success message
|
|
122
|
+
showSuccessMessage(answers);
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(chalk.red('\nā Error:'), error.message);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function createRootFiles(answers, projectPath) {
|
|
131
|
+
// Create root .gitignore
|
|
132
|
+
const gitignore = `# Dependencies
|
|
133
|
+
node_modules/
|
|
134
|
+
venv/
|
|
135
|
+
.venv/
|
|
136
|
+
|
|
137
|
+
# Environment
|
|
138
|
+
.env
|
|
139
|
+
.env.local
|
|
140
|
+
.env.*.local
|
|
141
|
+
|
|
142
|
+
# Build
|
|
143
|
+
.next/
|
|
144
|
+
dist/
|
|
145
|
+
build/
|
|
146
|
+
__pycache__/
|
|
147
|
+
|
|
148
|
+
# IDE
|
|
149
|
+
.vscode/
|
|
150
|
+
.idea/
|
|
151
|
+
*.swp
|
|
152
|
+
*.swo
|
|
153
|
+
|
|
154
|
+
# OS
|
|
155
|
+
.DS_Store
|
|
156
|
+
Thumbs.db
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
await fs.writeFile(path.join(projectPath, '.gitignore'), gitignore);
|
|
160
|
+
|
|
161
|
+
// Create root README
|
|
162
|
+
const readme = `# ${answers.projectName}
|
|
163
|
+
|
|
164
|
+
Full-stack application generated with FullStack CLI.
|
|
165
|
+
|
|
166
|
+
## Stack
|
|
167
|
+
- **Frontend**: ${answers.frontend === 'nextjs' ? 'Next.js' : answers.frontend === 'react-vite' ? 'React + Vite' : 'Svelte'}
|
|
168
|
+
- **Backend**: ${answers.backend === 'nextjs-api' ? 'Next.js API Routes' : answers.backend === 'express' ? 'Express' : answers.backend === 'fastify' ? 'Fastify' : 'FastAPI'}
|
|
169
|
+
- **Database**: ${answers.database === 'none' ? 'None' : answers.database.charAt(0).toUpperCase() + answers.database.slice(1)}
|
|
170
|
+
|
|
171
|
+
## Getting Started
|
|
172
|
+
|
|
173
|
+
### Install Dependencies
|
|
174
|
+
|
|
175
|
+
\`\`\`bash
|
|
176
|
+
# Frontend
|
|
177
|
+
cd frontend
|
|
178
|
+
npm install
|
|
179
|
+
|
|
180
|
+
${answers.backend !== 'nextjs-api' ? `# Backend
|
|
181
|
+
cd ../backend
|
|
182
|
+
${answers.backend === 'fastapi' ? 'pip install -r requirements.txt' : 'npm install'}` : ''}
|
|
183
|
+
\`\`\`
|
|
184
|
+
|
|
185
|
+
### Configure Environment
|
|
186
|
+
|
|
187
|
+
${answers.backend !== 'nextjs-api' ? `1. Edit \`backend/.env\` with your database credentials` : ''}
|
|
188
|
+
|
|
189
|
+
### Start Development
|
|
190
|
+
|
|
191
|
+
\`\`\`bash
|
|
192
|
+
${answers.backend !== 'nextjs-api' ? `# Terminal 1 - Backend
|
|
193
|
+
cd backend
|
|
194
|
+
${answers.backend === 'fastapi' ? 'uvicorn main:app --reload --port 5000' : 'npm run dev'}
|
|
195
|
+
|
|
196
|
+
# Terminal 2 - Frontend` : '# Frontend'}
|
|
197
|
+
cd frontend
|
|
198
|
+
npm run dev
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
### URLs
|
|
202
|
+
- Frontend: http://localhost:3000
|
|
203
|
+
${answers.backend !== 'nextjs-api' ? '- Backend: http://localhost:5000' : ''}
|
|
204
|
+
${answers.backend !== 'nextjs-api' ? '- Health Check: http://localhost:5000/api/health' : ''}
|
|
205
|
+
|
|
206
|
+
## Backend Status Indicator
|
|
207
|
+
|
|
208
|
+
The frontend includes a visual indicator in the top-right corner showing backend connection status:
|
|
209
|
+
- š **Checking** - Connecting to backend
|
|
210
|
+
- ā
**Connected** - Backend is running
|
|
211
|
+
- ā **Disconnected** - Backend is not reachable
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
await fs.writeFile(path.join(projectPath, 'README.md'), readme);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function cleanupProject(projectPath) {
|
|
218
|
+
const spinner = ora('Cleaning up...').start();
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Remove .git from frontend (created by create-next-app, create-vite, etc.)
|
|
222
|
+
const frontendGit = path.join(projectPath, 'frontend', '.git');
|
|
223
|
+
if (await fs.pathExists(frontendGit)) {
|
|
224
|
+
await fs.remove(frontendGit);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove .git from backend (already done in backend.js, but just in case)
|
|
228
|
+
const backendGit = path.join(projectPath, 'backend', '.git');
|
|
229
|
+
if (await fs.pathExists(backendGit)) {
|
|
230
|
+
await fs.remove(backendGit);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
spinner.succeed('Project cleaned up!');
|
|
234
|
+
} catch (error) {
|
|
235
|
+
spinner.warn('Cleanup warning: ' + error.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function initializeGit(projectPath) {
|
|
240
|
+
const spinner = ora('Initializing git repository...').start();
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await execa('git', ['init'], { cwd: projectPath });
|
|
244
|
+
await execa('git', ['add', '.'], { cwd: projectPath });
|
|
245
|
+
await execa('git', ['commit', '-m', 'Initial commit from FullStack CLI'], { cwd: projectPath });
|
|
246
|
+
|
|
247
|
+
spinner.succeed('Git repository initialized!');
|
|
248
|
+
} catch (error) {
|
|
249
|
+
spinner.warn('Git init skipped: ' + error.message);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
|
|
7
|
+
// GitHub template repositories
|
|
8
|
+
const TEMPLATES = {
|
|
9
|
+
express: 'https://github.com/HemanthRaj0C/express-template',
|
|
10
|
+
fastify: 'https://github.com/HemanthRaj0C/fastify-template',
|
|
11
|
+
fastapi: 'https://github.com/HemanthRaj0C/fastapi-template'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Database to branch mapping
|
|
15
|
+
const DATABASE_BRANCHES = {
|
|
16
|
+
postgres: 'postgres',
|
|
17
|
+
mongodb: 'mongodb',
|
|
18
|
+
mysql: 'mysql',
|
|
19
|
+
supabase: 'postgres', // Supabase uses PostgreSQL
|
|
20
|
+
none: 'main'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function generateBackend(answers, projectPath) {
|
|
24
|
+
const { backend, database } = answers;
|
|
25
|
+
|
|
26
|
+
const spinner = ora('Setting up backend...').start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const templateUrl = TEMPLATES[backend];
|
|
30
|
+
const branch = DATABASE_BRANCHES[database];
|
|
31
|
+
const backendPath = path.join(projectPath, 'backend');
|
|
32
|
+
const isPython = backend === 'fastapi';
|
|
33
|
+
|
|
34
|
+
// Clone the template
|
|
35
|
+
spinner.text = `Cloning ${backend} template (${branch} branch)...`;
|
|
36
|
+
|
|
37
|
+
await execa('git', [
|
|
38
|
+
'clone',
|
|
39
|
+
'-b', branch,
|
|
40
|
+
'--single-branch',
|
|
41
|
+
'--depth', '1',
|
|
42
|
+
templateUrl,
|
|
43
|
+
'backend'
|
|
44
|
+
], { cwd: projectPath });
|
|
45
|
+
|
|
46
|
+
// Remove .git directory to make it user's own project
|
|
47
|
+
await fs.remove(path.join(backendPath, '.git'));
|
|
48
|
+
|
|
49
|
+
// Copy .env.example to .env
|
|
50
|
+
const envExample = path.join(backendPath, '.env.example');
|
|
51
|
+
const envFile = path.join(backendPath, '.env');
|
|
52
|
+
|
|
53
|
+
if (await fs.pathExists(envExample)) {
|
|
54
|
+
await fs.copy(envExample, envFile);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If using Supabase, add a comment to .env
|
|
58
|
+
if (database === 'supabase' && await fs.pathExists(envFile)) {
|
|
59
|
+
let envContent = await fs.readFile(envFile, 'utf-8');
|
|
60
|
+
envContent = `# Using Supabase - Update DATABASE_URL with your Supabase connection string\n` + envContent;
|
|
61
|
+
await fs.writeFile(envFile, envContent);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Install dependencies
|
|
65
|
+
spinner.text = 'Installing backend dependencies...';
|
|
66
|
+
|
|
67
|
+
if (isPython) {
|
|
68
|
+
// For Python, just show message (user needs venv)
|
|
69
|
+
spinner.succeed(`Backend (${backend}) ready!`);
|
|
70
|
+
console.log(chalk.yellow(' ā ļø Run: cd backend && pip install -r requirements.txt\n'));
|
|
71
|
+
} else {
|
|
72
|
+
// For Node.js backends, auto-install
|
|
73
|
+
await execa('npm', ['install'], { cwd: backendPath });
|
|
74
|
+
spinner.succeed(`Backend (${backend}) ready! Dependencies installed.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(chalk.gray(` Template: ${templateUrl}`));
|
|
78
|
+
console.log(chalk.gray(` Branch: ${branch}\n`));
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
spinner.fail('Backend setup failed');
|
|
82
|
+
throw new Error(`Failed to setup backend: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
|
|
6
|
+
export async function injectBackendStatus(frontendPath, framework, isTypeScript, isIntegrated) {
|
|
7
|
+
const spinner = ora('Adding BackendStatus component...').start();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
switch (framework) {
|
|
11
|
+
case 'nextjs':
|
|
12
|
+
await injectNextJS(frontendPath, isTypeScript, isIntegrated);
|
|
13
|
+
break;
|
|
14
|
+
case 'react-vite':
|
|
15
|
+
await injectReactVite(frontendPath, isTypeScript);
|
|
16
|
+
break;
|
|
17
|
+
case 'svelte':
|
|
18
|
+
await injectSvelte(frontendPath, isTypeScript);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
spinner.succeed('BackendStatus component added!');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
spinner.warn(`Could not auto-inject BackendStatus: ${error.message}`);
|
|
24
|
+
console.log(chalk.yellow(' You can manually add the BackendStatus component later.\n'));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============== NEXT.JS ==============
|
|
29
|
+
async function injectNextJS(frontendPath, isTypeScript, isIntegrated) {
|
|
30
|
+
const ext = isTypeScript ? 'tsx' : 'jsx';
|
|
31
|
+
|
|
32
|
+
// Determine if using App Router or Pages Router
|
|
33
|
+
const appDir = path.join(frontendPath, 'app');
|
|
34
|
+
const srcAppDir = path.join(frontendPath, 'src', 'app');
|
|
35
|
+
|
|
36
|
+
let targetDir;
|
|
37
|
+
if (await fs.pathExists(appDir)) {
|
|
38
|
+
targetDir = appDir;
|
|
39
|
+
} else if (await fs.pathExists(srcAppDir)) {
|
|
40
|
+
targetDir = srcAppDir;
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error('Could not find app directory');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create components directory
|
|
46
|
+
const componentsDir = path.join(targetDir, 'components');
|
|
47
|
+
await fs.ensureDir(componentsDir);
|
|
48
|
+
|
|
49
|
+
// Backend URL - use relative path for integrated, absolute for separate
|
|
50
|
+
const backendUrl = isIntegrated ? '/api/health' : 'http://localhost:5000/api/health';
|
|
51
|
+
|
|
52
|
+
// Create BackendStatus component
|
|
53
|
+
const componentCode = getNextJSBackendStatusCode(isTypeScript, backendUrl);
|
|
54
|
+
await fs.writeFile(
|
|
55
|
+
path.join(componentsDir, `BackendStatus.${ext}`),
|
|
56
|
+
componentCode
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Try to modify page file
|
|
60
|
+
const pageFile = await findFile(targetDir, `page.${ext}`);
|
|
61
|
+
if (pageFile) {
|
|
62
|
+
await modifyNextJSPage(pageFile, isTypeScript);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getNextJSBackendStatusCode(isTypeScript, backendUrl) {
|
|
67
|
+
if (isTypeScript) {
|
|
68
|
+
return `'use client'
|
|
69
|
+
import { useEffect, useState } from 'react'
|
|
70
|
+
|
|
71
|
+
type Status = 'checking' | 'connected' | 'disconnected'
|
|
72
|
+
|
|
73
|
+
export default function BackendStatus() {
|
|
74
|
+
const [status, setStatus] = useState<Status>('checking')
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const checkBackend = async () => {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch('${backendUrl}')
|
|
80
|
+
setStatus(res.ok ? 'connected' : 'disconnected')
|
|
81
|
+
} catch {
|
|
82
|
+
setStatus('disconnected')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
checkBackend()
|
|
87
|
+
const interval = setInterval(checkBackend, 5000)
|
|
88
|
+
return () => clearInterval(interval)
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
const statusConfig = {
|
|
92
|
+
checking: { dot: '#fbbf24', text: 'Checking...' },
|
|
93
|
+
connected: { dot: '#22c55e', text: 'Connected' },
|
|
94
|
+
disconnected: { dot: '#ef4444', text: 'Disconnected' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { dot, text } = statusConfig[status]
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div style={{
|
|
101
|
+
position: 'fixed',
|
|
102
|
+
top: '1rem',
|
|
103
|
+
right: '1rem',
|
|
104
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
105
|
+
backdropFilter: 'blur(8px)',
|
|
106
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
107
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
108
|
+
padding: '0.5rem 0.875rem',
|
|
109
|
+
borderRadius: '9999px',
|
|
110
|
+
display: 'flex',
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
gap: '0.5rem',
|
|
113
|
+
zIndex: 50,
|
|
114
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
115
|
+
fontSize: '0.8125rem',
|
|
116
|
+
fontWeight: 500,
|
|
117
|
+
letterSpacing: '-0.01em',
|
|
118
|
+
transition: 'all 0.2s ease'
|
|
119
|
+
}}>
|
|
120
|
+
<span style={{
|
|
121
|
+
width: '8px',
|
|
122
|
+
height: '8px',
|
|
123
|
+
borderRadius: '50%',
|
|
124
|
+
backgroundColor: dot,
|
|
125
|
+
boxShadow: \`0 0 8px \${dot}\`
|
|
126
|
+
}} />
|
|
127
|
+
<span>Backend: {text}</span>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `'use client'
|
|
135
|
+
import { useEffect, useState } from 'react'
|
|
136
|
+
|
|
137
|
+
export default function BackendStatus() {
|
|
138
|
+
const [status, setStatus] = useState('checking')
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const checkBackend = async () => {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch('${backendUrl}')
|
|
144
|
+
setStatus(res.ok ? 'connected' : 'disconnected')
|
|
145
|
+
} catch {
|
|
146
|
+
setStatus('disconnected')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
checkBackend()
|
|
151
|
+
const interval = setInterval(checkBackend, 5000)
|
|
152
|
+
return () => clearInterval(interval)
|
|
153
|
+
}, [])
|
|
154
|
+
|
|
155
|
+
const statusConfig = {
|
|
156
|
+
checking: { dot: '#fbbf24', text: 'Checking...' },
|
|
157
|
+
connected: { dot: '#22c55e', text: 'Connected' },
|
|
158
|
+
disconnected: { dot: '#ef4444', text: 'Disconnected' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { dot, text } = statusConfig[status]
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div style={{
|
|
165
|
+
position: 'fixed',
|
|
166
|
+
top: '1rem',
|
|
167
|
+
right: '1rem',
|
|
168
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
169
|
+
backdropFilter: 'blur(8px)',
|
|
170
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
171
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
172
|
+
padding: '0.5rem 0.875rem',
|
|
173
|
+
borderRadius: '9999px',
|
|
174
|
+
display: 'flex',
|
|
175
|
+
alignItems: 'center',
|
|
176
|
+
gap: '0.5rem',
|
|
177
|
+
zIndex: 50,
|
|
178
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
179
|
+
fontSize: '0.8125rem',
|
|
180
|
+
fontWeight: 500,
|
|
181
|
+
letterSpacing: '-0.01em',
|
|
182
|
+
transition: 'all 0.2s ease'
|
|
183
|
+
}}>
|
|
184
|
+
<span style={{
|
|
185
|
+
width: '8px',
|
|
186
|
+
height: '8px',
|
|
187
|
+
borderRadius: '50%',
|
|
188
|
+
backgroundColor: dot,
|
|
189
|
+
boxShadow: \`0 0 8px \${dot}\`
|
|
190
|
+
}} />
|
|
191
|
+
<span>Backend: {text}</span>
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function modifyNextJSPage(pageFile, isTypeScript) {
|
|
199
|
+
let content = await fs.readFile(pageFile, 'utf-8');
|
|
200
|
+
|
|
201
|
+
// Skip if already has BackendStatus
|
|
202
|
+
if (content.includes('BackendStatus')) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add import at the top (after 'use client' if present, or at the very top)
|
|
207
|
+
const importStatement = `import BackendStatus from './components/BackendStatus'\n`;
|
|
208
|
+
|
|
209
|
+
if (content.includes("'use client'") || content.includes('"use client"')) {
|
|
210
|
+
content = content.replace(
|
|
211
|
+
/(['"]use client['"][\s\n]*)/,
|
|
212
|
+
`$1${importStatement}`
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
content = importStatement + content;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Try to add component after first opening tag in return
|
|
219
|
+
// Look for patterns like: return ( <main or return ( <div
|
|
220
|
+
const returnPattern = /(return\s*\(\s*<[a-zA-Z][^>]*>)/;
|
|
221
|
+
if (returnPattern.test(content)) {
|
|
222
|
+
content = content.replace(
|
|
223
|
+
returnPattern,
|
|
224
|
+
`$1\n <BackendStatus />`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await fs.writeFile(pageFile, content);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============== REACT + VITE ==============
|
|
232
|
+
async function injectReactVite(frontendPath, isTypeScript) {
|
|
233
|
+
const ext = isTypeScript ? 'tsx' : 'jsx';
|
|
234
|
+
|
|
235
|
+
// Create components directory
|
|
236
|
+
const componentsDir = path.join(frontendPath, 'src', 'components');
|
|
237
|
+
await fs.ensureDir(componentsDir);
|
|
238
|
+
|
|
239
|
+
// Create BackendStatus component
|
|
240
|
+
const componentCode = getReactBackendStatusCode(isTypeScript);
|
|
241
|
+
await fs.writeFile(
|
|
242
|
+
path.join(componentsDir, `BackendStatus.${ext}`),
|
|
243
|
+
componentCode
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Try to modify App file
|
|
247
|
+
const srcDir = path.join(frontendPath, 'src');
|
|
248
|
+
const appFile = await findFile(srcDir, `App.${ext}`);
|
|
249
|
+
if (appFile) {
|
|
250
|
+
await modifyReactApp(appFile);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getReactBackendStatusCode(isTypeScript) {
|
|
255
|
+
if (isTypeScript) {
|
|
256
|
+
return `import { useEffect, useState } from 'react'
|
|
257
|
+
|
|
258
|
+
type Status = 'checking' | 'connected' | 'disconnected'
|
|
259
|
+
|
|
260
|
+
export default function BackendStatus() {
|
|
261
|
+
const [status, setStatus] = useState<Status>('checking')
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const checkBackend = async () => {
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch('http://localhost:5000/api/health')
|
|
267
|
+
setStatus(res.ok ? 'connected' : 'disconnected')
|
|
268
|
+
} catch {
|
|
269
|
+
setStatus('disconnected')
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
checkBackend()
|
|
274
|
+
const interval = setInterval(checkBackend, 5000)
|
|
275
|
+
return () => clearInterval(interval)
|
|
276
|
+
}, [])
|
|
277
|
+
|
|
278
|
+
const statusConfig = {
|
|
279
|
+
checking: { dot: '#fbbf24', text: 'Checking...' },
|
|
280
|
+
connected: { dot: '#22c55e', text: 'Connected' },
|
|
281
|
+
disconnected: { dot: '#ef4444', text: 'Disconnected' }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { dot, text } = statusConfig[status]
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div style={{
|
|
288
|
+
position: 'fixed',
|
|
289
|
+
top: '1rem',
|
|
290
|
+
right: '1rem',
|
|
291
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
292
|
+
backdropFilter: 'blur(8px)',
|
|
293
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
294
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
295
|
+
padding: '0.5rem 0.875rem',
|
|
296
|
+
borderRadius: '9999px',
|
|
297
|
+
display: 'flex',
|
|
298
|
+
alignItems: 'center',
|
|
299
|
+
gap: '0.5rem',
|
|
300
|
+
zIndex: 50,
|
|
301
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
302
|
+
fontSize: '0.8125rem',
|
|
303
|
+
fontWeight: 500
|
|
304
|
+
}}>
|
|
305
|
+
<span style={{
|
|
306
|
+
width: '8px',
|
|
307
|
+
height: '8px',
|
|
308
|
+
borderRadius: '50%',
|
|
309
|
+
backgroundColor: dot,
|
|
310
|
+
boxShadow: \`0 0 8px \${dot}\`
|
|
311
|
+
}} />
|
|
312
|
+
<span>Backend: {text}</span>
|
|
313
|
+
</div>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return `import { useEffect, useState } from 'react'
|
|
320
|
+
|
|
321
|
+
export default function BackendStatus() {
|
|
322
|
+
const [status, setStatus] = useState('checking')
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
const checkBackend = async () => {
|
|
326
|
+
try {
|
|
327
|
+
const res = await fetch('http://localhost:5000/api/health')
|
|
328
|
+
setStatus(res.ok ? 'connected' : 'disconnected')
|
|
329
|
+
} catch {
|
|
330
|
+
setStatus('disconnected')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
checkBackend()
|
|
335
|
+
const interval = setInterval(checkBackend, 5000)
|
|
336
|
+
return () => clearInterval(interval)
|
|
337
|
+
}, [])
|
|
338
|
+
|
|
339
|
+
const statusConfig = {
|
|
340
|
+
checking: { dot: '#fbbf24', text: 'Checking...' },
|
|
341
|
+
connected: { dot: '#22c55e', text: 'Connected' },
|
|
342
|
+
disconnected: { dot: '#ef4444', text: 'Disconnected' }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const { dot, text } = statusConfig[status]
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div style={{
|
|
349
|
+
position: 'fixed',
|
|
350
|
+
top: '1rem',
|
|
351
|
+
right: '1rem',
|
|
352
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
353
|
+
backdropFilter: 'blur(8px)',
|
|
354
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
355
|
+
color: 'rgba(255, 255, 255, 0.9)',
|
|
356
|
+
padding: '0.5rem 0.875rem',
|
|
357
|
+
borderRadius: '9999px',
|
|
358
|
+
display: 'flex',
|
|
359
|
+
alignItems: 'center',
|
|
360
|
+
gap: '0.5rem',
|
|
361
|
+
zIndex: 50,
|
|
362
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
363
|
+
fontSize: '0.8125rem',
|
|
364
|
+
fontWeight: 500
|
|
365
|
+
}}>
|
|
366
|
+
<span style={{
|
|
367
|
+
width: '8px',
|
|
368
|
+
height: '8px',
|
|
369
|
+
borderRadius: '50%',
|
|
370
|
+
backgroundColor: dot,
|
|
371
|
+
boxShadow: \`0 0 8px \${dot}\`
|
|
372
|
+
}} />
|
|
373
|
+
<span>Backend: {text}</span>
|
|
374
|
+
</div>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function modifyReactApp(appFile) {
|
|
381
|
+
let content = await fs.readFile(appFile, 'utf-8');
|
|
382
|
+
|
|
383
|
+
if (content.includes('BackendStatus')) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Add import
|
|
388
|
+
const importStatement = `import BackendStatus from './components/BackendStatus'\n`;
|
|
389
|
+
|
|
390
|
+
// Find the last import and add after it
|
|
391
|
+
const importRegex = /^import .+ from .+$/gm;
|
|
392
|
+
let lastImportIndex = 0;
|
|
393
|
+
let match;
|
|
394
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
395
|
+
lastImportIndex = match.index + match[0].length;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (lastImportIndex > 0) {
|
|
399
|
+
content = content.slice(0, lastImportIndex) + '\n' + importStatement + content.slice(lastImportIndex);
|
|
400
|
+
} else {
|
|
401
|
+
content = importStatement + content;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Add component after first tag in return
|
|
405
|
+
const returnPattern = /(return\s*\(\s*<[a-zA-Z][^>]*>)/;
|
|
406
|
+
if (returnPattern.test(content)) {
|
|
407
|
+
content = content.replace(
|
|
408
|
+
returnPattern,
|
|
409
|
+
`$1\n <BackendStatus />`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
await fs.writeFile(appFile, content);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ============== SVELTE ==============
|
|
417
|
+
async function injectSvelte(frontendPath, isTypeScript) {
|
|
418
|
+
const ext = isTypeScript ? 'ts' : 'js';
|
|
419
|
+
|
|
420
|
+
// Create lib/components directory
|
|
421
|
+
const componentsDir = path.join(frontendPath, 'src', 'lib', 'components');
|
|
422
|
+
await fs.ensureDir(componentsDir);
|
|
423
|
+
|
|
424
|
+
// Create BackendStatus component
|
|
425
|
+
const componentCode = getSvelteBackendStatusCode();
|
|
426
|
+
await fs.writeFile(
|
|
427
|
+
path.join(componentsDir, 'BackendStatus.svelte'),
|
|
428
|
+
componentCode
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Try to modify +page.svelte
|
|
432
|
+
const routesDir = path.join(frontendPath, 'src', 'routes');
|
|
433
|
+
const pageFile = path.join(routesDir, '+page.svelte');
|
|
434
|
+
|
|
435
|
+
if (await fs.pathExists(pageFile)) {
|
|
436
|
+
await modifySveltePage(pageFile);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getSvelteBackendStatusCode() {
|
|
441
|
+
return `<script>
|
|
442
|
+
import { onMount, onDestroy } from 'svelte';
|
|
443
|
+
|
|
444
|
+
let status = 'checking';
|
|
445
|
+
let interval;
|
|
446
|
+
|
|
447
|
+
const statusConfig = {
|
|
448
|
+
checking: { text: 'Checking', color: '#eab308' },
|
|
449
|
+
connected: { text: 'Connected', color: '#22c55e' },
|
|
450
|
+
disconnected: { text: 'Disconnected', color: '#ef4444' }
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
async function checkBackend() {
|
|
454
|
+
try {
|
|
455
|
+
const res = await fetch('http://localhost:5000/api/health');
|
|
456
|
+
status = res.ok ? 'connected' : 'disconnected';
|
|
457
|
+
} catch {
|
|
458
|
+
status = 'disconnected';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
onMount(() => {
|
|
463
|
+
checkBackend();
|
|
464
|
+
interval = setInterval(checkBackend, 5000);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
onDestroy(() => {
|
|
468
|
+
if (interval) clearInterval(interval);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
$: config = statusConfig[status];
|
|
472
|
+
</script>
|
|
473
|
+
|
|
474
|
+
<div class="backend-status">
|
|
475
|
+
<span class="dot" style="background-color: {config.color}; box-shadow: 0 0 8px {config.color};"></span>
|
|
476
|
+
<span class="text">Backend: {config.text}</span>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<style>
|
|
480
|
+
.backend-status {
|
|
481
|
+
position: fixed;
|
|
482
|
+
top: 1rem;
|
|
483
|
+
right: 1rem;
|
|
484
|
+
background: rgba(0, 0, 0, 0.8);
|
|
485
|
+
backdrop-filter: blur(8px);
|
|
486
|
+
-webkit-backdrop-filter: blur(8px);
|
|
487
|
+
color: rgba(255, 255, 255, 0.9);
|
|
488
|
+
padding: 0.5rem 0.875rem;
|
|
489
|
+
border-radius: 9999px;
|
|
490
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
491
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3);
|
|
492
|
+
display: flex;
|
|
493
|
+
align-items: center;
|
|
494
|
+
gap: 0.5rem;
|
|
495
|
+
z-index: 50;
|
|
496
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
497
|
+
font-size: 0.75rem;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.dot {
|
|
501
|
+
width: 8px;
|
|
502
|
+
height: 8px;
|
|
503
|
+
border-radius: 50%;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.text {
|
|
507
|
+
font-weight: 500;
|
|
508
|
+
}
|
|
509
|
+
</style>
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function modifySveltePage(pageFile) {
|
|
514
|
+
let content = await fs.readFile(pageFile, 'utf-8');
|
|
515
|
+
|
|
516
|
+
if (content.includes('BackendStatus')) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check if there's a script tag
|
|
521
|
+
if (content.includes('<script')) {
|
|
522
|
+
// Add import inside existing script
|
|
523
|
+
content = content.replace(
|
|
524
|
+
/<script([^>]*)>/,
|
|
525
|
+
`<script$1>\n import BackendStatus from '$lib/components/BackendStatus.svelte';`
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
// Add script tag at the top
|
|
529
|
+
content = `<script>\n import BackendStatus from '$lib/components/BackendStatus.svelte';\n</script>\n\n` + content;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Add component at the end
|
|
533
|
+
content = content + '\n\n<BackendStatus />\n';
|
|
534
|
+
|
|
535
|
+
await fs.writeFile(pageFile, content);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ============== HELPERS ==============
|
|
539
|
+
async function findFile(dir, filename) {
|
|
540
|
+
try {
|
|
541
|
+
const files = await fs.readdir(dir);
|
|
542
|
+
if (files.includes(filename)) {
|
|
543
|
+
return path.join(dir, filename);
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import { injectBackendStatus } from './backendStatus.js';
|
|
7
|
+
|
|
8
|
+
export async function generateFrontend(answers, projectPath) {
|
|
9
|
+
const { frontend } = answers;
|
|
10
|
+
|
|
11
|
+
console.log(chalk.cyan('\nšØ Setting up frontend...\n'));
|
|
12
|
+
|
|
13
|
+
switch (frontend) {
|
|
14
|
+
case 'nextjs':
|
|
15
|
+
await generateNextJS(answers, projectPath);
|
|
16
|
+
break;
|
|
17
|
+
case 'react-vite':
|
|
18
|
+
await generateReactVite(answers, projectPath);
|
|
19
|
+
break;
|
|
20
|
+
case 'svelte':
|
|
21
|
+
await generateSvelte(answers, projectPath);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function generateNextJS(answers, projectPath) {
|
|
27
|
+
console.log(chalk.yellow('š¦ Running create-next-app (follow the prompts)...\n'));
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Run Next.js CLI interactively
|
|
31
|
+
await execa('npx', ['create-next-app@latest', 'frontend'], {
|
|
32
|
+
cwd: projectPath,
|
|
33
|
+
stdio: 'inherit'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log(chalk.green('\nā
Next.js project created!\n'));
|
|
37
|
+
|
|
38
|
+
// Detect if TypeScript was chosen
|
|
39
|
+
const frontendPath = path.join(projectPath, 'frontend');
|
|
40
|
+
const packageJson = await fs.readJSON(path.join(frontendPath, 'package.json'));
|
|
41
|
+
const isTypeScript = !!packageJson.devDependencies?.typescript;
|
|
42
|
+
|
|
43
|
+
// Inject BackendStatus component
|
|
44
|
+
await injectBackendStatus(frontendPath, 'nextjs', isTypeScript, answers.backend === 'nextjs-api');
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`Next.js setup failed: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function generateReactVite(answers, projectPath) {
|
|
52
|
+
console.log(chalk.yellow('š¦ Running create-vite (follow the prompts)...\n'));
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Run Vite CLI interactively
|
|
56
|
+
await execa('npm', ['create', 'vite@latest', 'frontend'], {
|
|
57
|
+
cwd: projectPath,
|
|
58
|
+
stdio: 'inherit'
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(chalk.green('\nā
React + Vite project created!\n'));
|
|
62
|
+
|
|
63
|
+
// Detect if TypeScript was chosen
|
|
64
|
+
const frontendPath = path.join(projectPath, 'frontend');
|
|
65
|
+
const files = await fs.readdir(frontendPath);
|
|
66
|
+
const isTypeScript = files.some(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
67
|
+
|
|
68
|
+
// Inject BackendStatus component
|
|
69
|
+
await injectBackendStatus(frontendPath, 'react-vite', isTypeScript, false);
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`Vite setup failed: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function generateSvelte(answers, projectPath) {
|
|
77
|
+
console.log(chalk.yellow('š¦ Running create-svelte (follow the prompts)...\n'));
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Run SvelteKit CLI interactively
|
|
81
|
+
await execa('npm', ['create', 'svelte@latest', 'frontend'], {
|
|
82
|
+
cwd: projectPath,
|
|
83
|
+
stdio: 'inherit'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(chalk.green('\nā
SvelteKit project created!\n'));
|
|
87
|
+
|
|
88
|
+
// Detect if TypeScript was chosen
|
|
89
|
+
const frontendPath = path.join(projectPath, 'frontend');
|
|
90
|
+
const files = await fs.readdir(frontendPath);
|
|
91
|
+
const isTypeScript = files.includes('tsconfig.json');
|
|
92
|
+
|
|
93
|
+
// Inject BackendStatus component
|
|
94
|
+
await injectBackendStatus(frontendPath, 'svelte', isTypeScript, false);
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(`SvelteKit setup failed: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import figlet from 'figlet';
|
|
6
|
+
import { createProject } from './commands/create.js';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
// Display banner
|
|
11
|
+
console.log(
|
|
12
|
+
chalk.cyan(
|
|
13
|
+
figlet.textSync('FSCLI', { font: 'ANSI Shadow' })
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
console.log(chalk.gray(' Full-Stack CLI - Scaffold applications with ease\n'));
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('create-fs-cli')
|
|
20
|
+
.description('CLI to scaffold full-stack applications')
|
|
21
|
+
.version('1.0.0');
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('create')
|
|
25
|
+
.description('Create a new full-stack project')
|
|
26
|
+
.argument('[project-name]', 'Name of the project')
|
|
27
|
+
.action(createProject);
|
|
28
|
+
|
|
29
|
+
// Default to create command if no command specified
|
|
30
|
+
if (process.argv.length === 2) {
|
|
31
|
+
createProject();
|
|
32
|
+
} else {
|
|
33
|
+
program.parse();
|
|
34
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import boxen from 'boxen';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export function showSuccessMessage(answers) {
|
|
5
|
+
const { projectName, frontend, backend, database } = answers;
|
|
6
|
+
|
|
7
|
+
const frontendName = frontend === 'nextjs' ? 'Next.js' :
|
|
8
|
+
frontend === 'react-vite' ? 'React + Vite' : 'SvelteKit';
|
|
9
|
+
|
|
10
|
+
const backendName = backend === 'nextjs-api' ? 'Next.js API Routes' :
|
|
11
|
+
backend === 'express' ? 'Express' :
|
|
12
|
+
backend === 'fastify' ? 'Fastify' : 'FastAPI';
|
|
13
|
+
|
|
14
|
+
const dbName = database === 'none' ? 'None' :
|
|
15
|
+
database.charAt(0).toUpperCase() + database.slice(1);
|
|
16
|
+
|
|
17
|
+
const hasBackend = backend !== 'nextjs-api';
|
|
18
|
+
const isPython = backend === 'fastapi';
|
|
19
|
+
|
|
20
|
+
let message = chalk.green.bold('ā
Project created successfully!') + '\n\n';
|
|
21
|
+
|
|
22
|
+
message += chalk.white('š¦ Stack:\n');
|
|
23
|
+
message += chalk.gray(` Frontend: ${frontendName}\n`);
|
|
24
|
+
message += chalk.gray(` Backend: ${backendName}\n`);
|
|
25
|
+
message += chalk.gray(` Database: ${dbName}\n\n`);
|
|
26
|
+
|
|
27
|
+
if (hasBackend && database !== 'none') {
|
|
28
|
+
message += chalk.yellow('āļø Configure database:\n');
|
|
29
|
+
message += chalk.white(` Edit ${chalk.cyan('backend/.env')} with your credentials\n\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
message += chalk.yellow('š¦ Install dependencies:\n');
|
|
33
|
+
message += chalk.white(` cd ${projectName}\n`);
|
|
34
|
+
message += chalk.white(` cd frontend && npm install\n`);
|
|
35
|
+
|
|
36
|
+
if (hasBackend) {
|
|
37
|
+
if (isPython) {
|
|
38
|
+
message += chalk.white(` cd ../backend && pip install -r requirements.txt\n\n`);
|
|
39
|
+
} else {
|
|
40
|
+
message += chalk.white(` cd ../backend && npm install\n\n`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
message += '\n';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
message += chalk.yellow('š Start development:\n');
|
|
47
|
+
|
|
48
|
+
if (hasBackend) {
|
|
49
|
+
message += chalk.gray(' # Terminal 1 - Backend:\n');
|
|
50
|
+
message += chalk.white(` cd backend && ${isPython ? 'uvicorn main:app --reload --port 5000' : 'npm run dev'}\n\n`);
|
|
51
|
+
message += chalk.gray(' # Terminal 2 - Frontend:\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
message += chalk.white(` cd frontend && npm run dev\n\n`);
|
|
55
|
+
|
|
56
|
+
message += chalk.cyan('š URLs:\n');
|
|
57
|
+
message += chalk.white(' Frontend: http://localhost:3000\n');
|
|
58
|
+
|
|
59
|
+
if (hasBackend) {
|
|
60
|
+
message += chalk.white(' Backend: http://localhost:5000\n');
|
|
61
|
+
message += chalk.white(' Health: http://localhost:5000/api/health\n\n');
|
|
62
|
+
} else {
|
|
63
|
+
message += '\n';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
message += chalk.magenta('šļø Backend Status Indicator:\n');
|
|
67
|
+
message += chalk.gray(' Look for the status badge in the top-right corner!');
|
|
68
|
+
|
|
69
|
+
console.log(
|
|
70
|
+
boxen(message, {
|
|
71
|
+
padding: 1,
|
|
72
|
+
margin: 1,
|
|
73
|
+
borderStyle: 'round',
|
|
74
|
+
borderColor: 'cyan'
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|