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 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
+ }