bharatcode 0.1.0__py3-none-any.whl
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.
- bharatcode/__init__.py +13 -0
- bharatcode/agent.py +2088 -0
- bharatcode/commands.py +1072 -0
- bharatcode/config.py +93 -0
- bharatcode/coordinator.py +670 -0
- bharatcode/cost.py +77 -0
- bharatcode/diff.py +113 -0
- bharatcode/hooks.py +75 -0
- bharatcode/index.py +155 -0
- bharatcode/main.py +846 -0
- bharatcode/memory.py +286 -0
- bharatcode/permissions.py +99 -0
- bharatcode/project.py +179 -0
- bharatcode/session_storage.py +108 -0
- bharatcode/skills.py +1746 -0
- bharatcode/subagent.py +363 -0
- bharatcode/tools.py +1021 -0
- bharatcode/ui.py +72 -0
- bharatcode-0.1.0.dist-info/METADATA +150 -0
- bharatcode-0.1.0.dist-info/RECORD +23 -0
- bharatcode-0.1.0.dist-info/WHEEL +5 -0
- bharatcode-0.1.0.dist-info/entry_points.txt +3 -0
- bharatcode-0.1.0.dist-info/top_level.txt +1 -0
bharatcode/skills.py
ADDED
|
@@ -0,0 +1,1746 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skills system — interactive Q&A → fully tailored agent prompt per tech stack.
|
|
3
|
+
newsite and newapp ask for frontend + backend tech separately and produce
|
|
4
|
+
framework-specific instructions with a frontend/ and backend/ folder structure.
|
|
5
|
+
Custom skills from ~/.bharatcode/skills/*.md are also supported as-is.
|
|
6
|
+
"""
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
SKILLS_DIR = Path.home() / ".bharatcode" / "skills"
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
# ── Tech stack option labels ───────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
FE_OPTIONS = [
|
|
16
|
+
"React + Vite",
|
|
17
|
+
"Vue 3 + Vite",
|
|
18
|
+
"Next.js 14",
|
|
19
|
+
"Angular 17",
|
|
20
|
+
"Svelte + Vite",
|
|
21
|
+
"Vanilla HTML / CSS / JS",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
BE_OPTIONS = [
|
|
25
|
+
"Flask (Python)",
|
|
26
|
+
"Django + DRF (Python)",
|
|
27
|
+
"Node.js + Express",
|
|
28
|
+
"FastAPI (Python)",
|
|
29
|
+
"Go + Gin",
|
|
30
|
+
"None (static / frontend only)",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
BE_OPTIONS_APP = [ # newapp — backend is required
|
|
34
|
+
"Flask (Python)",
|
|
35
|
+
"Django + DRF (Python)",
|
|
36
|
+
"Node.js + Express",
|
|
37
|
+
"FastAPI (Python)",
|
|
38
|
+
"Go + Gin",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# ── Per-skill questions ────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
SKILL_QUESTIONS: dict[str, list[dict]] = {
|
|
44
|
+
"newsite": [
|
|
45
|
+
{"key": "name", "label": "Site name", "required": True},
|
|
46
|
+
{"key": "type", "label": "What kind of site",
|
|
47
|
+
"choices": ["portfolio", "landing page", "SaaS marketing", "e-commerce",
|
|
48
|
+
"blog", "agency", "product showcase", "community", "other"],
|
|
49
|
+
"required": True},
|
|
50
|
+
{"key": "desc", "label": "Describe it — what it does, who it's for",
|
|
51
|
+
"hint": "e.g. dark developer portfolio with projects, blog, and contact form"},
|
|
52
|
+
{"key": "frontend", "label": "Frontend technology",
|
|
53
|
+
"choices": FE_OPTIONS, "required": True},
|
|
54
|
+
{"key": "backend", "label": "Backend (choose None for static / no API needed)",
|
|
55
|
+
"choices": BE_OPTIONS, "required": True},
|
|
56
|
+
{"key": "sections", "label": "Key sections",
|
|
57
|
+
"hint": "e.g. hero, about, projects, skills, pricing, blog, testimonials, contact"},
|
|
58
|
+
{"key": "features", "label": "Special features",
|
|
59
|
+
"hint": "e.g. dark mode toggle, contact form with backend, blog CMS, animations"},
|
|
60
|
+
{"key": "theme", "label": "Visual theme",
|
|
61
|
+
"choices": ["dark", "light", "minimal", "bold / colorful", "corporate", "playful"]},
|
|
62
|
+
],
|
|
63
|
+
|
|
64
|
+
"newapp": [
|
|
65
|
+
{"key": "name", "label": "App name", "required": True},
|
|
66
|
+
{"key": "desc", "label": "What does this app do?", "required": True},
|
|
67
|
+
{"key": "frontend", "label": "Frontend technology",
|
|
68
|
+
"choices": FE_OPTIONS, "required": True},
|
|
69
|
+
{"key": "backend", "label": "Backend technology",
|
|
70
|
+
"choices": BE_OPTIONS_APP, "required": True},
|
|
71
|
+
{"key": "database", "label": "Database",
|
|
72
|
+
"choices": ["PostgreSQL", "MySQL", "MongoDB", "SQLite", "Redis + PostgreSQL", "none"]},
|
|
73
|
+
{"key": "auth", "label": "Authentication",
|
|
74
|
+
"choices": ["JWT (email / password)", "JWT + Google OAuth", "Session-based", "no auth"]},
|
|
75
|
+
{"key": "features", "label": "Core features (comma-separated)",
|
|
76
|
+
"hint": "e.g. user profiles, dashboard, file uploads, real-time notifications, admin panel"},
|
|
77
|
+
{"key": "extras", "label": "Extra integrations (optional)",
|
|
78
|
+
"hint": "e.g. Stripe, SendGrid, WebSockets, S3, Redis cache, cron jobs"},
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
"docker": [
|
|
82
|
+
{"key": "database", "label": "Database to include in compose",
|
|
83
|
+
"choices": ["PostgreSQL", "MySQL", "MongoDB", "Redis", "none"]},
|
|
84
|
+
{"key": "extras", "label": "Extra services",
|
|
85
|
+
"choices": ["Redis + Celery", "Nginx reverse proxy", "both", "none"]},
|
|
86
|
+
{"key": "multistage", "label": "Multi-stage build (smaller production image)?",
|
|
87
|
+
"choices": ["yes", "no"]},
|
|
88
|
+
],
|
|
89
|
+
|
|
90
|
+
"ci-github": [
|
|
91
|
+
{"key": "test_fw", "label": "Test framework",
|
|
92
|
+
"hint": "e.g. pytest, Jest, JUnit, go test — or 'none'"},
|
|
93
|
+
{"key": "deploy", "label": "Deployment target",
|
|
94
|
+
"choices": ["AWS EC2", "AWS ECS / ECR", "GCP Cloud Run", "Azure App Service",
|
|
95
|
+
"VPS (SSH deploy)", "Heroku", "none"]},
|
|
96
|
+
{"key": "auto_deploy","label": "Auto-deploy on merge to main?",
|
|
97
|
+
"choices": ["yes", "no"]},
|
|
98
|
+
],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── Interactive Q&A ───────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def _ask_choice(label: str, choices: list[str]) -> str | None:
|
|
105
|
+
try:
|
|
106
|
+
import questionary
|
|
107
|
+
from questionary import Style
|
|
108
|
+
result = questionary.select(
|
|
109
|
+
f" {label}:",
|
|
110
|
+
choices=choices + ["↩ Skip"],
|
|
111
|
+
style=Style([
|
|
112
|
+
("highlighted", "fg:cyan bold"),
|
|
113
|
+
("pointer", "fg:cyan bold"),
|
|
114
|
+
("selected", "fg:green"),
|
|
115
|
+
("question", "fg:yellow bold"),
|
|
116
|
+
]),
|
|
117
|
+
instruction=" (↑↓ move Enter select)",
|
|
118
|
+
).ask()
|
|
119
|
+
return None if result == "↩ Skip" else result
|
|
120
|
+
except (ImportError, Exception):
|
|
121
|
+
console.print(f"\n [yellow]{label}:[/yellow]")
|
|
122
|
+
for i, c in enumerate(choices, 1):
|
|
123
|
+
console.print(f" [green]{i}[/green] {c}")
|
|
124
|
+
try:
|
|
125
|
+
raw = input(" Pick number (Enter to skip): ").strip()
|
|
126
|
+
except (EOFError, KeyboardInterrupt):
|
|
127
|
+
return None
|
|
128
|
+
if raw.isdigit():
|
|
129
|
+
idx = int(raw) - 1
|
|
130
|
+
if 0 <= idx < len(choices):
|
|
131
|
+
return choices[idx]
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def ask_skill_questions(name: str, prefilled: dict | None = None) -> dict | None:
|
|
136
|
+
"""
|
|
137
|
+
Run the interactive Q&A for a skill.
|
|
138
|
+
prefilled: keys already known — those questions are skipped.
|
|
139
|
+
Returns answers dict, or None if the user cancelled.
|
|
140
|
+
"""
|
|
141
|
+
questions = SKILL_QUESTIONS.get(name)
|
|
142
|
+
if not questions:
|
|
143
|
+
return {}
|
|
144
|
+
|
|
145
|
+
pre = prefilled or {}
|
|
146
|
+
answers = dict(pre)
|
|
147
|
+
|
|
148
|
+
console.print(f"\n[bold cyan] {name} setup[/bold cyan]\n")
|
|
149
|
+
|
|
150
|
+
for q in questions:
|
|
151
|
+
key = q["key"]
|
|
152
|
+
label = q["label"]
|
|
153
|
+
hint = q.get("hint", "")
|
|
154
|
+
choices = q.get("choices")
|
|
155
|
+
required = q.get("required", False)
|
|
156
|
+
|
|
157
|
+
if key in pre and pre[key]:
|
|
158
|
+
console.print(f" [dim]{label}:[/dim] [cyan]{pre[key]}[/cyan]")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
while True:
|
|
162
|
+
if choices:
|
|
163
|
+
result = _ask_choice(label, choices)
|
|
164
|
+
if result is None and required:
|
|
165
|
+
console.print(" [red]Required — please pick one.[/red]")
|
|
166
|
+
continue
|
|
167
|
+
if result:
|
|
168
|
+
answers[key] = result
|
|
169
|
+
break
|
|
170
|
+
else:
|
|
171
|
+
if hint:
|
|
172
|
+
console.print(f" [dim]{hint}[/dim]")
|
|
173
|
+
try:
|
|
174
|
+
val = input(f" {label}: ").strip()
|
|
175
|
+
except (EOFError, KeyboardInterrupt):
|
|
176
|
+
console.print("\n [dim]Cancelled.[/dim]")
|
|
177
|
+
return None
|
|
178
|
+
if not val and required:
|
|
179
|
+
console.print(" [red]Required.[/red]")
|
|
180
|
+
continue
|
|
181
|
+
if val:
|
|
182
|
+
answers[key] = val
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
console.print()
|
|
186
|
+
return answers
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ── Per-framework detail blocks ───────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
_FE_PORTS = {
|
|
192
|
+
"React + Vite": 5173,
|
|
193
|
+
"Vue 3 + Vite": 5173,
|
|
194
|
+
"Svelte + Vite": 5173,
|
|
195
|
+
"Next.js 14": 3000,
|
|
196
|
+
"Angular 17": 4200,
|
|
197
|
+
"Vanilla HTML / CSS / JS": 3000,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_BE_PORTS = {
|
|
201
|
+
"Flask (Python)": 5000,
|
|
202
|
+
"Django + DRF (Python)": 8000,
|
|
203
|
+
"Node.js + Express": 5000,
|
|
204
|
+
"FastAPI (Python)": 8000,
|
|
205
|
+
"Go + Gin": 8080,
|
|
206
|
+
"None (static / frontend only)": None,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _fe_detail(tech: str) -> str:
|
|
211
|
+
"""Detailed file structure + coding rules for the chosen frontend tech."""
|
|
212
|
+
|
|
213
|
+
if tech == "React + Vite":
|
|
214
|
+
return """
|
|
215
|
+
FRONTEND: React + Vite
|
|
216
|
+
══════════════════════
|
|
217
|
+
Folder structure (frontend/):
|
|
218
|
+
src/
|
|
219
|
+
components/
|
|
220
|
+
ui/ ← Button.jsx, Input.jsx, Modal.jsx, Card.jsx, Spinner.jsx
|
|
221
|
+
(pure presentational, no API calls, fully typed props)
|
|
222
|
+
layout/ ← Navbar.jsx, Footer.jsx, Sidebar.jsx, PageWrapper.jsx
|
|
223
|
+
[feature]/ ← feature-specific components (one sub-folder per domain)
|
|
224
|
+
pages/ ← one file per route: HomePage.jsx, DashboardPage.jsx, LoginPage.jsx
|
|
225
|
+
hooks/ ← useAuth.js, useFetch.js, useForm.js, useDebounce.js
|
|
226
|
+
services/
|
|
227
|
+
api.js ← axios instance + request/response interceptors (see below)
|
|
228
|
+
auth.service.js ← login(creds), register(data), logout(), refreshToken()
|
|
229
|
+
[domain].service.js ← one file per API domain
|
|
230
|
+
store/ ← Zustand store (preferred) or Context + useReducer
|
|
231
|
+
authStore.js ← user, token, setUser, clearAuth
|
|
232
|
+
utils/
|
|
233
|
+
formatters.js ← formatDate(), formatCurrency(), truncate()
|
|
234
|
+
validators.js ← isEmail(), isStrongPassword(), required()
|
|
235
|
+
constants.js ← ROUTES, USER_ROLES, API_PATHS
|
|
236
|
+
config/
|
|
237
|
+
api.js ← export const API_BASE = import.meta.env.VITE_API_URL
|
|
238
|
+
styles/
|
|
239
|
+
index.css ← global reset, CSS custom properties, font-face
|
|
240
|
+
App.jsx ← <BrowserRouter> + <Routes> setup ONLY
|
|
241
|
+
main.jsx ← ReactDOM.createRoot + StrictMode
|
|
242
|
+
|
|
243
|
+
services/api.js (complete — copy this pattern exactly):
|
|
244
|
+
import axios from 'axios';
|
|
245
|
+
const api = axios.create({
|
|
246
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
248
|
+
});
|
|
249
|
+
api.interceptors.request.use(config => {
|
|
250
|
+
const token = localStorage.getItem('token');
|
|
251
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
252
|
+
return config;
|
|
253
|
+
});
|
|
254
|
+
api.interceptors.response.use(
|
|
255
|
+
response => response,
|
|
256
|
+
async error => {
|
|
257
|
+
if (error.response?.status === 401) {
|
|
258
|
+
localStorage.removeItem('token');
|
|
259
|
+
window.location.href = '/login';
|
|
260
|
+
}
|
|
261
|
+
return Promise.reject(error);
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
export default api;
|
|
265
|
+
|
|
266
|
+
Domain service pattern (services/auth.service.js):
|
|
267
|
+
import api from './api.js';
|
|
268
|
+
export const authService = {
|
|
269
|
+
login: (data) => api.post('/api/auth/login', data).then(r => r.data),
|
|
270
|
+
register: (data) => api.post('/api/auth/register', data).then(r => r.data),
|
|
271
|
+
logout: () => api.post('/api/auth/logout').then(r => r.data),
|
|
272
|
+
me: () => api.get('/api/auth/me').then(r => r.data),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
vite.config.js (dev proxy — eliminates CORS in development):
|
|
276
|
+
import { defineConfig } from 'vite';
|
|
277
|
+
import react from '@vitejs/plugin-react';
|
|
278
|
+
export default defineConfig({
|
|
279
|
+
plugins: [react()],
|
|
280
|
+
server: {
|
|
281
|
+
port: 5173,
|
|
282
|
+
proxy: { '/api': { target: 'http://localhost:BACKEND_PORT', changeOrigin: true } }
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
.env.example:
|
|
287
|
+
VITE_API_URL=http://localhost:BACKEND_PORT
|
|
288
|
+
|
|
289
|
+
package.json essentials:
|
|
290
|
+
react@18, react-dom@18, react-router-dom@6, axios, zustand
|
|
291
|
+
devDeps: vite, @vitejs/plugin-react
|
|
292
|
+
|
|
293
|
+
Coding rules:
|
|
294
|
+
- ALL API calls go through services/*.service.js — never axios/fetch directly in components
|
|
295
|
+
- All pages in pages/, reusable UI in components/ui/, layout in components/layout/
|
|
296
|
+
- React Router v6: useNavigate(), useParams(), <Outlet /> pattern
|
|
297
|
+
- No class components — functional components + hooks only
|
|
298
|
+
- Zustand for global state (auth, user, theme) — useState/useReducer for local state
|
|
299
|
+
- Never hardcode backend URLs in component files
|
|
300
|
+
- Error boundaries at the page level
|
|
301
|
+
- PropTypes or TypeScript interface for every component's props"""
|
|
302
|
+
|
|
303
|
+
if tech == "Vue 3 + Vite":
|
|
304
|
+
return """
|
|
305
|
+
FRONTEND: Vue 3 + Vite
|
|
306
|
+
══════════════════════
|
|
307
|
+
Folder structure (frontend/):
|
|
308
|
+
src/
|
|
309
|
+
components/
|
|
310
|
+
base/ ← BaseButton.vue, BaseInput.vue, BaseModal.vue, BaseCard.vue
|
|
311
|
+
layout/ ← AppHeader.vue, AppFooter.vue, AppSidebar.vue
|
|
312
|
+
[feature]/ ← feature-specific components
|
|
313
|
+
views/ ← one .vue file per route: HomeView.vue, DashboardView.vue, LoginView.vue
|
|
314
|
+
router/
|
|
315
|
+
index.js ← createRouter, createWebHistory, route guards
|
|
316
|
+
stores/ ← Pinia (one store per domain)
|
|
317
|
+
auth.store.js ← useAuthStore: state, login(), logout(), fetchMe()
|
|
318
|
+
[domain].store.js
|
|
319
|
+
services/
|
|
320
|
+
api.js ← axios instance + interceptors
|
|
321
|
+
[domain].service.js
|
|
322
|
+
composables/ ← useAuth.js, useForm.js, usePagination.js, useNotify.js
|
|
323
|
+
utils/
|
|
324
|
+
formatters.js
|
|
325
|
+
validators.js
|
|
326
|
+
config/
|
|
327
|
+
api.js ← export const API_BASE = import.meta.env.VITE_API_URL
|
|
328
|
+
assets/
|
|
329
|
+
styles/
|
|
330
|
+
main.css
|
|
331
|
+
variables.css
|
|
332
|
+
App.vue
|
|
333
|
+
main.js ← createApp + use(router) + use(pinia) + mount
|
|
334
|
+
|
|
335
|
+
services/api.js (same axios pattern as React — copy):
|
|
336
|
+
import axios from 'axios';
|
|
337
|
+
const api = axios.create({ baseURL: import.meta.env.VITE_API_URL });
|
|
338
|
+
api.interceptors.request.use(config => {
|
|
339
|
+
const token = localStorage.getItem('token');
|
|
340
|
+
if (token) config.headers.Authorization = `Bearer ${token}`;
|
|
341
|
+
return config;
|
|
342
|
+
});
|
|
343
|
+
api.interceptors.response.use(r => r, err => {
|
|
344
|
+
if (err.response?.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; }
|
|
345
|
+
return Promise.reject(err);
|
|
346
|
+
});
|
|
347
|
+
export default api;
|
|
348
|
+
|
|
349
|
+
Pinia store pattern (stores/auth.store.js):
|
|
350
|
+
import { defineStore } from 'pinia';
|
|
351
|
+
import { authService } from '../services/auth.service.js';
|
|
352
|
+
export const useAuthStore = defineStore('auth', {
|
|
353
|
+
state: () => ({ user: null, token: localStorage.getItem('token') || null }),
|
|
354
|
+
getters: { isLoggedIn: s => !!s.token },
|
|
355
|
+
actions: {
|
|
356
|
+
async login(credentials) {
|
|
357
|
+
const { token, user } = await authService.login(credentials);
|
|
358
|
+
this.token = token; this.user = user; localStorage.setItem('token', token);
|
|
359
|
+
},
|
|
360
|
+
logout() { this.token = null; this.user = null; localStorage.removeItem('token'); },
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
vite.config.js:
|
|
365
|
+
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:BACKEND_PORT', changeOrigin: true } } }
|
|
366
|
+
|
|
367
|
+
.env.example: VITE_API_URL=http://localhost:BACKEND_PORT
|
|
368
|
+
|
|
369
|
+
Coding rules:
|
|
370
|
+
- Composition API ONLY inside <script setup> — NEVER Options API
|
|
371
|
+
- Pinia for all shared state — no Vuex, no prop drilling past 2 levels
|
|
372
|
+
- All API calls in services/*.service.js — composables call services, components call composables
|
|
373
|
+
- defineModel() macro (Vue 3.4+) for two-way binding in child components
|
|
374
|
+
- Route guards in router/index.js using beforeEach for auth protection
|
|
375
|
+
- Named routes always (router.push({ name: 'dashboard' }) not string paths)"""
|
|
376
|
+
|
|
377
|
+
if tech == "Next.js 14":
|
|
378
|
+
return """
|
|
379
|
+
FRONTEND: Next.js 14 (App Router)
|
|
380
|
+
══════════════════════════════════
|
|
381
|
+
Folder structure (frontend/):
|
|
382
|
+
app/
|
|
383
|
+
layout.tsx ← root layout: <html>, <body>, global providers, fonts
|
|
384
|
+
page.tsx ← home route (server component)
|
|
385
|
+
globals.css
|
|
386
|
+
(public)/ ← route group: public pages (no auth needed)
|
|
387
|
+
about/page.tsx
|
|
388
|
+
[page]/page.tsx
|
|
389
|
+
(auth)/ ← route group: login, signup
|
|
390
|
+
login/page.tsx
|
|
391
|
+
signup/page.tsx
|
|
392
|
+
(dashboard)/ ← route group: protected pages
|
|
393
|
+
layout.tsx ← auth guard: redirect if no session
|
|
394
|
+
page.tsx
|
|
395
|
+
[feature]/page.tsx
|
|
396
|
+
api/ ← API routes (ONLY for webhooks / third-party callbacks)
|
|
397
|
+
health/route.ts
|
|
398
|
+
components/
|
|
399
|
+
ui/ ← Button.tsx, Input.tsx, Card.tsx, Modal.tsx ("use client")
|
|
400
|
+
layout/ ← Header.tsx, Footer.tsx, Sidebar.tsx
|
|
401
|
+
[feature]/
|
|
402
|
+
lib/
|
|
403
|
+
api.ts ← typed fetch wrapper (see below)
|
|
404
|
+
auth.ts ← getSession(), requireAuth()
|
|
405
|
+
db.ts ← Prisma client (if DB in Next.js)
|
|
406
|
+
utils.ts ← cn(), formatDate(), formatCurrency()
|
|
407
|
+
types/
|
|
408
|
+
index.ts ← all shared TypeScript interfaces/types
|
|
409
|
+
hooks/ ← "use client" hooks: useAuth.ts, useForm.ts, useLocalStorage.ts
|
|
410
|
+
config/
|
|
411
|
+
site.ts ← SITE_NAME, SITE_URL, nav links
|
|
412
|
+
middleware.ts ← protects /dashboard/** routes, redirects to /login
|
|
413
|
+
next.config.js
|
|
414
|
+
.env.example
|
|
415
|
+
tsconfig.json
|
|
416
|
+
|
|
417
|
+
lib/api.ts (typed fetch wrapper):
|
|
418
|
+
const BASE = process.env.NEXT_PUBLIC_API_URL ?? '';
|
|
419
|
+
function getToken() { return typeof window !== 'undefined' ? localStorage.getItem('token') : null; }
|
|
420
|
+
export async function apiFetch<T>(path: string, opts: RequestInit = {}): Promise<T> {
|
|
421
|
+
const token = getToken();
|
|
422
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
423
|
+
...opts,
|
|
424
|
+
headers: {
|
|
425
|
+
'Content-Type': 'application/json',
|
|
426
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
427
|
+
...opts.headers,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
if (!res.ok) {
|
|
431
|
+
const msg = await res.text();
|
|
432
|
+
throw new Error(msg || `HTTP ${res.status}`);
|
|
433
|
+
}
|
|
434
|
+
return res.json() as Promise<T>;
|
|
435
|
+
}
|
|
436
|
+
export const api = {
|
|
437
|
+
get: <T>(path: string) => apiFetch<T>(path, { method: 'GET' }),
|
|
438
|
+
post: <T>(path: string, body: unknown) => apiFetch<T>(path, { method: 'POST', body: JSON.stringify(body) }),
|
|
439
|
+
put: <T>(path: string, body: unknown) => apiFetch<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
|
|
440
|
+
delete: <T>(path: string) => apiFetch<T>(path, { method: 'DELETE' }),
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
next.config.js (API proxy to backend):
|
|
444
|
+
/** @type {import('next').NextConfig} */
|
|
445
|
+
module.exports = {
|
|
446
|
+
async rewrites() {
|
|
447
|
+
return [{ source: '/api/:path*', destination: `${process.env.BACKEND_URL}/api/:path*` }];
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
.env.example:
|
|
452
|
+
NEXT_PUBLIC_API_URL=http://localhost:BACKEND_PORT
|
|
453
|
+
BACKEND_URL=http://localhost:BACKEND_PORT
|
|
454
|
+
|
|
455
|
+
Coding rules:
|
|
456
|
+
- Server components by default — add "use client" ONLY for: hooks, event listeners, browser APIs
|
|
457
|
+
- Server Actions for all form mutations — not /api routes for internal data changes
|
|
458
|
+
- /api routes ONLY for external webhooks (Stripe, GitHub, etc.)
|
|
459
|
+
- TypeScript everywhere — no any, no @ts-ignore
|
|
460
|
+
- NEXT_PUBLIC_ prefix for env vars needed in client components
|
|
461
|
+
- next/image for ALL images, next/font for ALL fonts
|
|
462
|
+
- Route groups (parentheses) to share layouts without adding URL segments
|
|
463
|
+
- NEVER use Pages Router patterns (getServerSideProps, getStaticProps, pages/ directory)
|
|
464
|
+
- middleware.ts: use next/server NextResponse.redirect for auth protection"""
|
|
465
|
+
|
|
466
|
+
if tech == "Angular 17":
|
|
467
|
+
return """
|
|
468
|
+
FRONTEND: Angular 17 (Standalone)
|
|
469
|
+
══════════════════════════════════
|
|
470
|
+
Folder structure (frontend/):
|
|
471
|
+
src/app/
|
|
472
|
+
core/
|
|
473
|
+
guards/
|
|
474
|
+
auth.guard.ts ← CanActivateFn — redirects to /login if no token
|
|
475
|
+
interceptors/
|
|
476
|
+
auth.interceptor.ts ← HttpInterceptorFn — injects Bearer token into every request
|
|
477
|
+
error.interceptor.ts ← handles 401 (redirect), 500 (show toast)
|
|
478
|
+
services/
|
|
479
|
+
auth.service.ts ← login(), logout(), isLoggedIn(), currentUser signal
|
|
480
|
+
api.service.ts ← HttpClient wrapper (typed GET/POST/PUT/DELETE)
|
|
481
|
+
shared/
|
|
482
|
+
components/ ← ButtonComponent, InputComponent, ModalComponent, CardComponent
|
|
483
|
+
pipes/ ← DateFormatPipe, CurrencyInrPipe
|
|
484
|
+
directives/ ← ClickOutsideDirective, AutofocusDirective
|
|
485
|
+
features/
|
|
486
|
+
[feature]/
|
|
487
|
+
components/
|
|
488
|
+
[feature].component.ts ← standalone, imports: CommonModule, RouterModule, ...
|
|
489
|
+
services/
|
|
490
|
+
[feature].service.ts ← injects ApiService
|
|
491
|
+
[feature].routes.ts ← Routes array, lazy loaded
|
|
492
|
+
app.component.ts ← standalone root component
|
|
493
|
+
app.config.ts ← provideRouter, provideHttpClient, withInterceptors
|
|
494
|
+
app.routes.ts ← top-level routes with loadChildren lazy loading
|
|
495
|
+
environments/
|
|
496
|
+
environment.ts ← { production: false, apiUrl: 'http://localhost:BACKEND_PORT' }
|
|
497
|
+
environment.prod.ts ← { production: true, apiUrl: 'https://api.yourdomain.com' }
|
|
498
|
+
proxy.conf.json ← "/api": { "target": "http://localhost:BACKEND_PORT", "changeOrigin": true }
|
|
499
|
+
|
|
500
|
+
core/services/api.service.ts:
|
|
501
|
+
@Injectable({ providedIn: 'root' })
|
|
502
|
+
export class ApiService {
|
|
503
|
+
private http = inject(HttpClient);
|
|
504
|
+
private baseUrl = environment.apiUrl;
|
|
505
|
+
get<T>(path: string) { return this.http.get<T>(`${this.baseUrl}${path}`); }
|
|
506
|
+
post<T>(path: string, body: unknown) { return this.http.post<T>(`${this.baseUrl}${path}`, body); }
|
|
507
|
+
put<T>(path: string, body: unknown) { return this.http.put<T>(`${this.baseUrl}${path}`, body); }
|
|
508
|
+
delete<T>(path: string) { return this.http.delete<T>(`${this.baseUrl}${path}`); }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
core/interceptors/auth.interceptor.ts:
|
|
512
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
513
|
+
const token = localStorage.getItem('token');
|
|
514
|
+
if (token) {
|
|
515
|
+
req = req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) });
|
|
516
|
+
}
|
|
517
|
+
return next(req).pipe(
|
|
518
|
+
catchError(err => { if (err.status === 401) { inject(Router).navigate(['/login']); } throw err; })
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
app.config.ts:
|
|
523
|
+
export const appConfig: ApplicationConfig = {
|
|
524
|
+
providers: [
|
|
525
|
+
provideRouter(appRoutes),
|
|
526
|
+
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
|
|
527
|
+
provideAnimations(),
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
angular.json (add proxyConfig):
|
|
532
|
+
"serve": { "options": { "proxyConfig": "proxy.conf.json" } }
|
|
533
|
+
|
|
534
|
+
Coding rules:
|
|
535
|
+
- Standalone components EVERYWHERE — never NgModules
|
|
536
|
+
- Angular Signals: signal(), computed(), effect() for reactive state
|
|
537
|
+
- inject() function in constructor body or at field declaration — never constructor injection style
|
|
538
|
+
- All API calls in *.service.ts — components call services, never HttpClient directly
|
|
539
|
+
- Lazy-load every feature route: loadChildren: () => import('./features/x/x.routes')
|
|
540
|
+
- environment.ts for all config — never hardcode URLs
|
|
541
|
+
- OnPush change detection strategy on all components"""
|
|
542
|
+
|
|
543
|
+
if tech == "Svelte + Vite":
|
|
544
|
+
return """
|
|
545
|
+
FRONTEND: Svelte + Vite
|
|
546
|
+
═══════════════════════
|
|
547
|
+
Folder structure (frontend/):
|
|
548
|
+
src/
|
|
549
|
+
components/
|
|
550
|
+
ui/ ← Button.svelte, Input.svelte, Modal.svelte, Card.svelte
|
|
551
|
+
layout/ ← Navbar.svelte, Footer.svelte
|
|
552
|
+
[feature]/
|
|
553
|
+
pages/ ← one .svelte per route (use svelte-routing or SvelteKit)
|
|
554
|
+
stores/
|
|
555
|
+
auth.js ← writable store: { user, token }
|
|
556
|
+
[domain].js
|
|
557
|
+
services/
|
|
558
|
+
api.js ← fetch wrapper with auth header injection
|
|
559
|
+
[domain].service.js
|
|
560
|
+
utils/
|
|
561
|
+
formatters.js
|
|
562
|
+
validators.js
|
|
563
|
+
config/
|
|
564
|
+
api.js ← export const API_BASE = import.meta.env.VITE_API_URL
|
|
565
|
+
styles/
|
|
566
|
+
global.css
|
|
567
|
+
variables.css
|
|
568
|
+
App.svelte
|
|
569
|
+
main.js
|
|
570
|
+
vite.config.js
|
|
571
|
+
.env.example
|
|
572
|
+
|
|
573
|
+
stores/auth.js:
|
|
574
|
+
import { writable } from 'svelte/store';
|
|
575
|
+
function createAuthStore() {
|
|
576
|
+
const { subscribe, set, update } = writable({
|
|
577
|
+
user: null,
|
|
578
|
+
token: localStorage.getItem('token') || null,
|
|
579
|
+
});
|
|
580
|
+
return {
|
|
581
|
+
subscribe,
|
|
582
|
+
login: (user, token) => {
|
|
583
|
+
localStorage.setItem('token', token);
|
|
584
|
+
set({ user, token });
|
|
585
|
+
},
|
|
586
|
+
logout: () => {
|
|
587
|
+
localStorage.removeItem('token');
|
|
588
|
+
set({ user: null, token: null });
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
export const authStore = createAuthStore();
|
|
593
|
+
|
|
594
|
+
services/api.js:
|
|
595
|
+
import { get as getStore } from 'svelte/store';
|
|
596
|
+
import { authStore } from '../stores/auth.js';
|
|
597
|
+
const BASE = import.meta.env.VITE_API_URL;
|
|
598
|
+
async function request(method, path, body) {
|
|
599
|
+
const { token } = getStore(authStore);
|
|
600
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
601
|
+
method,
|
|
602
|
+
headers: {
|
|
603
|
+
'Content-Type': 'application/json',
|
|
604
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
605
|
+
},
|
|
606
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
607
|
+
});
|
|
608
|
+
if (!res.ok) {
|
|
609
|
+
const msg = await res.text();
|
|
610
|
+
if (res.status === 401) { authStore.logout(); window.location.href = '/login'; }
|
|
611
|
+
throw new Error(msg || `HTTP ${res.status}`);
|
|
612
|
+
}
|
|
613
|
+
return res.json();
|
|
614
|
+
}
|
|
615
|
+
export const api = {
|
|
616
|
+
get: (path) => request('GET', path),
|
|
617
|
+
post: (path, body) => request('POST', path, body),
|
|
618
|
+
put: (path, body) => request('PUT', path, body),
|
|
619
|
+
delete: (path) => request('DELETE', path),
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
vite.config.js:
|
|
623
|
+
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:BACKEND_PORT', changeOrigin: true } } }
|
|
624
|
+
|
|
625
|
+
Coding rules:
|
|
626
|
+
- Svelte stores for ALL shared state — components use $storeName reactive syntax
|
|
627
|
+
- All API calls through services/api.js — never fetch() directly in .svelte files
|
|
628
|
+
- $: reactive declarations for derived/computed values
|
|
629
|
+
- onMount for lifecycle effects (like useEffect in React)
|
|
630
|
+
- Dispatch custom events to parent instead of prop callbacks
|
|
631
|
+
- Slots for component composition"""
|
|
632
|
+
|
|
633
|
+
# Vanilla HTML / CSS / JS
|
|
634
|
+
return """
|
|
635
|
+
FRONTEND: Vanilla HTML / CSS / JS
|
|
636
|
+
══════════════════════════════════
|
|
637
|
+
Folder structure (frontend/):
|
|
638
|
+
index.html ← main entry point (type="module" on script tags)
|
|
639
|
+
[page].html ← one file per page
|
|
640
|
+
css/
|
|
641
|
+
variables.css ← ALL custom properties: colors, spacing, fonts, shadows, radii
|
|
642
|
+
reset.css ← modern CSS reset (box-sizing, margin 0, line-height)
|
|
643
|
+
typography.css ← @font-face / Google Fonts, heading scale, body text
|
|
644
|
+
layout.css ← .container, grid wrappers, section padding, flex rows
|
|
645
|
+
navbar.css ← nav links, hamburger, mobile overlay, scroll-shrink
|
|
646
|
+
components.css ← .btn, .card, .badge, .form-group, .input — every reusable piece
|
|
647
|
+
[section].css ← hero.css, about.css, projects.css, services.css, etc.
|
|
648
|
+
animations.css ← @keyframes, .reveal, .fade-in, hover transitions
|
|
649
|
+
responsive.css ← ALL media queries, every breakpoint
|
|
650
|
+
main.css ← @import in correct order (variables first, reset second)
|
|
651
|
+
js/
|
|
652
|
+
config.js ← export const CONFIG = { API_BASE: 'http://localhost:BACKEND_PORT', ... }
|
|
653
|
+
utils.js ← $() querySelector, $$() querySelectorAll, debounce, throttle, formatDate
|
|
654
|
+
api.js ← fetch wrapper that reads CONFIG.API_BASE (see below)
|
|
655
|
+
auth.js ← getToken(), setToken(), clearToken(), isLoggedIn()
|
|
656
|
+
navbar.js ← mobile toggle, scroll-hide/show, active link highlighting
|
|
657
|
+
animations.js ← IntersectionObserver scroll-reveal, counter animation, parallax
|
|
658
|
+
theme.js ← dark/light toggle, localStorage persistence
|
|
659
|
+
forms.js ← field validation, error display, submit handler
|
|
660
|
+
[feature].js ← one file per distinct feature
|
|
661
|
+
main.js ← DOMContentLoaded init — imports and calls init functions only
|
|
662
|
+
assets/
|
|
663
|
+
images/
|
|
664
|
+
icons/
|
|
665
|
+
fonts/
|
|
666
|
+
|
|
667
|
+
js/api.js (complete — copy exactly):
|
|
668
|
+
import { CONFIG } from './config.js';
|
|
669
|
+
import { getToken, clearToken } from './auth.js';
|
|
670
|
+
async function request(method, path, body) {
|
|
671
|
+
const token = getToken();
|
|
672
|
+
const res = await fetch(`${CONFIG.API_BASE}${path}`, {
|
|
673
|
+
method,
|
|
674
|
+
headers: {
|
|
675
|
+
'Content-Type': 'application/json',
|
|
676
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
677
|
+
},
|
|
678
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
679
|
+
});
|
|
680
|
+
if (res.status === 401) { clearToken(); window.location.href = '/login.html'; }
|
|
681
|
+
if (!res.ok) { const msg = await res.text(); throw new Error(msg || `HTTP ${res.status}`); }
|
|
682
|
+
if (res.status === 204) return null;
|
|
683
|
+
return res.json();
|
|
684
|
+
}
|
|
685
|
+
export const api = {
|
|
686
|
+
get: (path) => request('GET', path),
|
|
687
|
+
post: (path, body) => request('POST', path, body),
|
|
688
|
+
put: (path, body) => request('PUT', path, body),
|
|
689
|
+
delete: (path) => request('DELETE', path),
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
Coding rules:
|
|
693
|
+
- ES6 modules (type="module") everywhere — no inline <script> blocks
|
|
694
|
+
- ALL fetch calls go through api.js — never raw fetch() in feature files
|
|
695
|
+
- CSS: use real values from the chosen palette — real pixel values, real colors, real spacing
|
|
696
|
+
- HTML: real, meaningful content — never lorem ipsum in any visible text
|
|
697
|
+
- JS: real event listeners, real DOM queries, real working logic
|
|
698
|
+
- Every section gets its own .css file imported in main.css
|
|
699
|
+
- config.js is the single source of truth for API URL and constants"""
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _be_detail(tech: str) -> str:
|
|
703
|
+
"""Detailed file structure + coding rules for the chosen backend tech."""
|
|
704
|
+
|
|
705
|
+
if tech == "Flask (Python)":
|
|
706
|
+
return """
|
|
707
|
+
BACKEND: Flask (Python)
|
|
708
|
+
═══════════════════════
|
|
709
|
+
Folder structure (backend/):
|
|
710
|
+
app/
|
|
711
|
+
__init__.py ← create_app(config_name='development') factory function
|
|
712
|
+
config.py ← Config, DevelopmentConfig, ProductionConfig classes
|
|
713
|
+
extensions.py ← db = SQLAlchemy(); jwt = JWTManager(); cors = CORS()
|
|
714
|
+
models/
|
|
715
|
+
__init__.py
|
|
716
|
+
user.py ← class User(db.Model): id, email, password_hash, role, created_at, is_active
|
|
717
|
+
[resource].py ← one model file per domain entity
|
|
718
|
+
routes/
|
|
719
|
+
__init__.py ← def register_routes(app): app.register_blueprint(auth_bp); ...
|
|
720
|
+
auth.py ← auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')
|
|
721
|
+
[resource].py ← [resource]_bp = Blueprint(...)
|
|
722
|
+
services/
|
|
723
|
+
auth_service.py ← register_user(), authenticate_user(), create_tokens()
|
|
724
|
+
[resource]_service.py
|
|
725
|
+
utils/
|
|
726
|
+
decorators.py ← @admin_required, @validate_json(schema)
|
|
727
|
+
validators.py ← validate_email(), validate_password_strength()
|
|
728
|
+
responses.py ← success_response(data, code=200), error_response(msg, code=400)
|
|
729
|
+
migrations/ ← flask db init, flask db migrate, flask db upgrade
|
|
730
|
+
tests/
|
|
731
|
+
test_auth.py
|
|
732
|
+
test_[resource].py
|
|
733
|
+
run.py ← if __name__ == '__main__': create_app('development').run(port=5000, debug=True)
|
|
734
|
+
requirements.txt
|
|
735
|
+
.env.example
|
|
736
|
+
Makefile
|
|
737
|
+
|
|
738
|
+
app/__init__.py (application factory — copy this pattern):
|
|
739
|
+
from flask import Flask
|
|
740
|
+
from .extensions import db, jwt, cors
|
|
741
|
+
from .config import config
|
|
742
|
+
def create_app(config_name='development'):
|
|
743
|
+
app = Flask(__name__)
|
|
744
|
+
app.config.from_object(config[config_name])
|
|
745
|
+
db.init_app(app)
|
|
746
|
+
jwt.init_app(app)
|
|
747
|
+
cors.init_app(app, resources={r'/api/*': {'origins': app.config['CORS_ORIGINS']}})
|
|
748
|
+
with app.app_context():
|
|
749
|
+
db.create_all()
|
|
750
|
+
from .routes import register_routes
|
|
751
|
+
register_routes(app)
|
|
752
|
+
return app
|
|
753
|
+
|
|
754
|
+
app/config.py:
|
|
755
|
+
import os
|
|
756
|
+
class Config:
|
|
757
|
+
SECRET_KEY = os.environ['SECRET_KEY']
|
|
758
|
+
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
|
|
759
|
+
JWT_SECRET_KEY = os.environ['JWT_SECRET_KEY']
|
|
760
|
+
JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=15)
|
|
761
|
+
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7)
|
|
762
|
+
CORS_ORIGINS = os.environ.get('CORS_ORIGINS', 'http://localhost:5173').split(',')
|
|
763
|
+
class DevelopmentConfig(Config):
|
|
764
|
+
DEBUG = True
|
|
765
|
+
SQLALCHEMY_ECHO = True
|
|
766
|
+
config = {'development': DevelopmentConfig, 'production': ProductionConfig}
|
|
767
|
+
|
|
768
|
+
Route pattern (every endpoint returns JSON):
|
|
769
|
+
@auth_bp.route('/register', methods=['POST'])
|
|
770
|
+
def register():
|
|
771
|
+
data = request.get_json()
|
|
772
|
+
if not data: return error_response('No data provided', 400)
|
|
773
|
+
result, error = auth_service.register_user(data)
|
|
774
|
+
if error: return error_response(error, 400)
|
|
775
|
+
return success_response(result, 201)
|
|
776
|
+
|
|
777
|
+
@auth_bp.route('/login', methods=['POST'])
|
|
778
|
+
def login():
|
|
779
|
+
data = request.get_json()
|
|
780
|
+
tokens, error = auth_service.authenticate_user(data.get('email'), data.get('password'))
|
|
781
|
+
if error: return error_response(error, 401)
|
|
782
|
+
return success_response(tokens)
|
|
783
|
+
|
|
784
|
+
@auth_bp.route('/me', methods=['GET'])
|
|
785
|
+
@jwt_required()
|
|
786
|
+
def me():
|
|
787
|
+
user_id = get_jwt_identity()
|
|
788
|
+
user = User.query.get_or_404(user_id)
|
|
789
|
+
return success_response(user.to_dict())
|
|
790
|
+
|
|
791
|
+
Consistent response envelope (utils/responses.py):
|
|
792
|
+
from flask import jsonify
|
|
793
|
+
def success_response(data, code=200): return jsonify({'data': data, 'error': None}), code
|
|
794
|
+
def error_response(msg, code=400): return jsonify({'data': None, 'error': msg}), code
|
|
795
|
+
|
|
796
|
+
GET /api/health endpoint (always include):
|
|
797
|
+
@app.route('/api/health')
|
|
798
|
+
def health(): return jsonify({'status': 'ok', 'timestamp': datetime.utcnow().isoformat()})
|
|
799
|
+
|
|
800
|
+
requirements.txt:
|
|
801
|
+
flask>=3.0, flask-sqlalchemy, flask-jwt-extended, flask-cors,
|
|
802
|
+
flask-migrate, psycopg2-binary, python-dotenv, bcrypt, email-validator
|
|
803
|
+
|
|
804
|
+
Coding rules:
|
|
805
|
+
- NEVER use app = Flask(__name__) at module level — always application factory
|
|
806
|
+
- One Blueprint per route group, all registered in routes/__init__.py
|
|
807
|
+
- SQLAlchemy ORM only — never raw SQL strings
|
|
808
|
+
- JWT via Flask-JWT-Extended: @jwt_required() on protected routes, get_jwt_identity()
|
|
809
|
+
- All config from os.environ — never hardcode secrets
|
|
810
|
+
- Passwords: bcrypt.generate_password_hash(pw, rounds=12) — never MD5/SHA
|
|
811
|
+
- Every model has a .to_dict() method for JSON serialization"""
|
|
812
|
+
|
|
813
|
+
if tech == "Django + DRF (Python)":
|
|
814
|
+
return """
|
|
815
|
+
BACKEND: Django + REST Framework (Python)
|
|
816
|
+
══════════════════════════════════════════
|
|
817
|
+
Folder structure (backend/):
|
|
818
|
+
config/
|
|
819
|
+
__init__.py
|
|
820
|
+
settings/
|
|
821
|
+
__init__.py ← from .development import * (or set via DJANGO_SETTINGS_MODULE)
|
|
822
|
+
base.py ← installed apps, middleware, DRF config, JWT config
|
|
823
|
+
development.py ← DEBUG=True, local DB, CORS allow all
|
|
824
|
+
production.py ← DEBUG=False, production DB, ALLOWED_HOSTS, SECURE_* headers
|
|
825
|
+
urls.py ← urlpatterns: path('api/', include('apps.users.urls')), ...
|
|
826
|
+
wsgi.py
|
|
827
|
+
asgi.py
|
|
828
|
+
apps/
|
|
829
|
+
users/
|
|
830
|
+
models.py ← class User(AbstractUser): bio, avatar, ... (ALWAYS custom)
|
|
831
|
+
serializers.py ← UserSerializer, UserCreateSerializer, LoginSerializer
|
|
832
|
+
views.py ← UserViewSet, LoginView, RegisterView
|
|
833
|
+
urls.py ← router = DefaultRouter(); router.register('users', UserViewSet)
|
|
834
|
+
permissions.py ← IsOwnerOrReadOnly, IsAdmin
|
|
835
|
+
tests.py
|
|
836
|
+
[app]/ ← one Django app per domain (posts, products, orders, etc.)
|
|
837
|
+
requirements.txt
|
|
838
|
+
manage.py
|
|
839
|
+
.env.example
|
|
840
|
+
Makefile
|
|
841
|
+
|
|
842
|
+
config/settings/base.py essentials:
|
|
843
|
+
AUTH_USER_MODEL = 'users.User' ← MUST be set before first migration
|
|
844
|
+
INSTALLED_APPS = [..., 'rest_framework', 'corsheaders', 'apps.users', ...]
|
|
845
|
+
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...]
|
|
846
|
+
REST_FRAMEWORK = {
|
|
847
|
+
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework_simplejwt.authentication.JWTAuthentication'],
|
|
848
|
+
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
|
|
849
|
+
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
|
850
|
+
'PAGE_SIZE': 20,
|
|
851
|
+
}
|
|
852
|
+
SIMPLE_JWT = {
|
|
853
|
+
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
|
854
|
+
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
|
855
|
+
}
|
|
856
|
+
CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS', default=['http://localhost:5173'])
|
|
857
|
+
|
|
858
|
+
ViewSet pattern (apps/[app]/views.py):
|
|
859
|
+
class ArticleViewSet(ModelViewSet):
|
|
860
|
+
serializer_class = ArticleSerializer
|
|
861
|
+
permission_classes = [IsAuthenticated]
|
|
862
|
+
def get_queryset(self):
|
|
863
|
+
return Article.objects.filter(author=self.request.user).select_related('author')
|
|
864
|
+
def perform_create(self, serializer):
|
|
865
|
+
serializer.save(author=self.request.user)
|
|
866
|
+
|
|
867
|
+
Serializer pattern:
|
|
868
|
+
class ArticleSerializer(ModelSerializer):
|
|
869
|
+
author = UserSerializer(read_only=True)
|
|
870
|
+
class Meta:
|
|
871
|
+
model = Article
|
|
872
|
+
fields = ['id', 'title', 'content', 'author', 'created_at']
|
|
873
|
+
read_only_fields = ['id', 'author', 'created_at']
|
|
874
|
+
|
|
875
|
+
URL pattern (apps/[app]/urls.py):
|
|
876
|
+
router = DefaultRouter()
|
|
877
|
+
router.register(r'articles', ArticleViewSet, basename='article')
|
|
878
|
+
urlpatterns = router.urls
|
|
879
|
+
|
|
880
|
+
Auth endpoints (add to config/urls.py):
|
|
881
|
+
path('api/auth/token/', TokenObtainPairView.as_view()),
|
|
882
|
+
path('api/auth/token/refresh/', TokenRefreshView.as_view()),
|
|
883
|
+
path('api/auth/register/', RegisterView.as_view()),
|
|
884
|
+
|
|
885
|
+
GET /api/health/:
|
|
886
|
+
path('api/health/', lambda req: JsonResponse({'status': 'ok'})),
|
|
887
|
+
|
|
888
|
+
requirements.txt:
|
|
889
|
+
django>=5.0, djangorestframework, djangorestframework-simplejwt,
|
|
890
|
+
django-cors-headers, django-environ, psycopg2-binary, pillow
|
|
891
|
+
|
|
892
|
+
Coding rules:
|
|
893
|
+
- Custom User model from day 1 (AbstractUser) — impossible to change after first migration
|
|
894
|
+
- DRF ViewSets + DefaultRouter for standard CRUD — class-based views always
|
|
895
|
+
- JWT via simplejwt — never session auth for API endpoints
|
|
896
|
+
- All settings from environment variables via django-environ
|
|
897
|
+
- API versioned at /api/v1/ using namespace in urls.py
|
|
898
|
+
- select_related / prefetch_related in every queryset — never N+1
|
|
899
|
+
- Override get_queryset() to filter by request.user — never trust URL params for ownership"""
|
|
900
|
+
|
|
901
|
+
if tech == "Node.js + Express":
|
|
902
|
+
return """
|
|
903
|
+
BACKEND: Node.js + Express
|
|
904
|
+
══════════════════════════
|
|
905
|
+
Folder structure (backend/):
|
|
906
|
+
src/
|
|
907
|
+
config/
|
|
908
|
+
db.js ← DB connection (mongoose.connect or Prisma client init)
|
|
909
|
+
env.js ← zod or joi schema validating all required env vars on startup
|
|
910
|
+
middleware/
|
|
911
|
+
auth.middleware.js ← verifyToken(req, res, next): checks Authorization header, attaches req.user
|
|
912
|
+
error.middleware.js ← (err, req, res, next): global error handler — LAST app.use()
|
|
913
|
+
validate.middleware.js ← validate(schema)(req, res, next): validates req.body with zod/joi
|
|
914
|
+
rateLimiter.js ← express-rate-limit configuration
|
|
915
|
+
routes/
|
|
916
|
+
index.js ← mount all routers: router.use('/auth', authRoutes); router.use('/...', ...)
|
|
917
|
+
auth.routes.js ← router.post('/register', validate(registerSchema), authController.register)
|
|
918
|
+
[resource].routes.js ← router.use(authMiddleware) for protected routes
|
|
919
|
+
controllers/
|
|
920
|
+
auth.controller.js
|
|
921
|
+
← export const register = async (req, res, next) => { try { ... } catch (e) { next(e) } }
|
|
922
|
+
← export const login = async (req, res, next) => { try { ... } catch (e) { next(e) } }
|
|
923
|
+
[resource].controller.js
|
|
924
|
+
models/
|
|
925
|
+
User.js ← Mongoose Schema / Prisma model definition
|
|
926
|
+
[Resource].js
|
|
927
|
+
services/
|
|
928
|
+
auth.service.js ← registerUser(data), loginUser(email, pw), refreshTokens(token)
|
|
929
|
+
[resource].service.js ← ALL business logic lives here, not in controllers
|
|
930
|
+
utils/
|
|
931
|
+
jwt.js ← signAccess(payload), signRefresh(payload), verifyToken(token)
|
|
932
|
+
hash.js ← hashPassword(pw), comparePassword(pw, hash)
|
|
933
|
+
apiResponse.js ← success(res, data, code=200), error(res, msg, code=400)
|
|
934
|
+
AppError.js ← class AppError extends Error { constructor(message, statusCode) }
|
|
935
|
+
app.js ← express setup, middleware, routes mounting
|
|
936
|
+
server.js ← app.listen(PORT) ONLY
|
|
937
|
+
package.json
|
|
938
|
+
.env.example
|
|
939
|
+
|
|
940
|
+
app.js (complete — copy this structure):
|
|
941
|
+
import express from 'express';
|
|
942
|
+
import cors from 'cors';
|
|
943
|
+
import { routes } from './routes/index.js';
|
|
944
|
+
import { errorMiddleware } from './middleware/error.middleware.js';
|
|
945
|
+
const app = express();
|
|
946
|
+
app.use(cors({ origin: process.env.CORS_ORIGIN, credentials: true }));
|
|
947
|
+
app.use(express.json({ limit: '10mb' }));
|
|
948
|
+
app.use(express.urlencoded({ extended: true }));
|
|
949
|
+
app.get('/api/health', (_, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
950
|
+
app.use('/api', routes);
|
|
951
|
+
app.use(errorMiddleware); // MUST be the last middleware
|
|
952
|
+
export default app;
|
|
953
|
+
|
|
954
|
+
Controller pattern (copy exactly):
|
|
955
|
+
export const register = async (req, res, next) => {
|
|
956
|
+
try {
|
|
957
|
+
const user = await authService.registerUser(req.body);
|
|
958
|
+
return success(res, user, 201);
|
|
959
|
+
} catch (err) { next(err); }
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
Global error middleware (copy exactly):
|
|
963
|
+
export const errorMiddleware = (err, req, res, next) => {
|
|
964
|
+
const status = err.statusCode || 500;
|
|
965
|
+
const message = err.message || 'Internal Server Error';
|
|
966
|
+
res.status(status).json({ data: null, error: message });
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
JWT pattern (utils/jwt.js):
|
|
970
|
+
import jwt from 'jsonwebtoken';
|
|
971
|
+
export const signAccess = (payload) => jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
|
|
972
|
+
export const signRefresh = (payload) => jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
|
|
973
|
+
export const verifyToken = (token, secret) => jwt.verify(token, secret);
|
|
974
|
+
|
|
975
|
+
package.json dependencies:
|
|
976
|
+
express, jsonwebtoken, bcryptjs, cors, dotenv, mongoose OR @prisma/client,
|
|
977
|
+
zod, express-rate-limit, morgan
|
|
978
|
+
devDeps: nodemon, jest / vitest
|
|
979
|
+
|
|
980
|
+
Coding rules:
|
|
981
|
+
- NEVER put business logic in controllers — controllers only call services and return responses
|
|
982
|
+
- errorMiddleware is the LAST app.use() in app.js — nothing after it
|
|
983
|
+
- async/await everywhere — zero callbacks
|
|
984
|
+
- Validate request BEFORE controller via validate middleware
|
|
985
|
+
- Refresh tokens stored in DB as SHA-256 hash — never the raw token
|
|
986
|
+
- CORS_ORIGIN from env — never '*' in production
|
|
987
|
+
- AppError class for all operational errors (wrong password, not found, etc.)
|
|
988
|
+
- server.js only has: import app; app.listen(PORT, cb) — nothing else"""
|
|
989
|
+
|
|
990
|
+
if tech == "FastAPI (Python)":
|
|
991
|
+
return """
|
|
992
|
+
BACKEND: FastAPI (Python)
|
|
993
|
+
═════════════════════════
|
|
994
|
+
Folder structure (backend/):
|
|
995
|
+
app/
|
|
996
|
+
main.py ← FastAPI() instance, lifespan, middleware, include_router
|
|
997
|
+
config.py ← class Settings(BaseSettings): reads from .env automatically
|
|
998
|
+
database.py ← engine, SessionLocal, Base, get_db Depends
|
|
999
|
+
models/
|
|
1000
|
+
user.py ← class User(Base): __tablename__, columns, relationships
|
|
1001
|
+
[resource].py
|
|
1002
|
+
schemas/
|
|
1003
|
+
user.py ← UserCreate(BaseModel), UserUpdate, UserResponse, Token
|
|
1004
|
+
[resource].py ← [Resource]Create, [Resource]Update, [Resource]Response
|
|
1005
|
+
routers/
|
|
1006
|
+
auth.py ← router = APIRouter(prefix='/api/auth', tags=['auth'])
|
|
1007
|
+
[resource].py ← router = APIRouter(prefix='/api/[resource]', tags=['[resource]'])
|
|
1008
|
+
dependencies/
|
|
1009
|
+
auth.py ← get_current_user(token: str = Depends(oauth2_scheme), db = Depends(get_db))
|
|
1010
|
+
services/
|
|
1011
|
+
auth_service.py ← async register_user(db, user_in), authenticate_user(db, email, pw)
|
|
1012
|
+
[resource]_service.py
|
|
1013
|
+
utils/
|
|
1014
|
+
security.py ← hash_password(pw), verify_password(pw, hash), create_access_token(data)
|
|
1015
|
+
alembic/
|
|
1016
|
+
versions/
|
|
1017
|
+
env.py
|
|
1018
|
+
requirements.txt
|
|
1019
|
+
.env.example
|
|
1020
|
+
alembic.ini
|
|
1021
|
+
|
|
1022
|
+
app/main.py (complete):
|
|
1023
|
+
from fastapi import FastAPI
|
|
1024
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
1025
|
+
from contextlib import asynccontextmanager
|
|
1026
|
+
from .config import settings
|
|
1027
|
+
from .database import engine, Base
|
|
1028
|
+
from .routers import auth, [resource]
|
|
1029
|
+
@asynccontextmanager
|
|
1030
|
+
async def lifespan(app: FastAPI):
|
|
1031
|
+
Base.metadata.create_all(bind=engine) # replace with alembic in prod
|
|
1032
|
+
yield
|
|
1033
|
+
app = FastAPI(title=settings.APP_NAME, version='1.0.0', lifespan=lifespan)
|
|
1034
|
+
app.add_middleware(CORSMiddleware,
|
|
1035
|
+
allow_origins=settings.CORS_ORIGINS, allow_credentials=True,
|
|
1036
|
+
allow_methods=['*'], allow_headers=['*'])
|
|
1037
|
+
app.include_router(auth.router)
|
|
1038
|
+
app.include_router([resource].router, dependencies=[Depends(get_current_user)])
|
|
1039
|
+
@app.get('/api/health')
|
|
1040
|
+
async def health(): return {'status': 'ok'}
|
|
1041
|
+
|
|
1042
|
+
app/config.py:
|
|
1043
|
+
from pydantic_settings import BaseSettings
|
|
1044
|
+
from typing import list
|
|
1045
|
+
class Settings(BaseSettings):
|
|
1046
|
+
APP_NAME: str = 'My App'
|
|
1047
|
+
DATABASE_URL: str
|
|
1048
|
+
SECRET_KEY: str
|
|
1049
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
|
|
1050
|
+
CORS_ORIGINS: list[str] = ['http://localhost:5173']
|
|
1051
|
+
class Config: env_file = '.env'
|
|
1052
|
+
settings = Settings()
|
|
1053
|
+
|
|
1054
|
+
Router + schema pattern (copy exactly):
|
|
1055
|
+
@router.post('/register', response_model=UserResponse, status_code=201)
|
|
1056
|
+
async def register(user_in: UserCreate, db: Session = Depends(get_db)):
|
|
1057
|
+
existing = db.query(User).filter(User.email == user_in.email).first()
|
|
1058
|
+
if existing: raise HTTPException(status_code=400, detail='Email already registered')
|
|
1059
|
+
return await auth_service.register_user(db, user_in)
|
|
1060
|
+
|
|
1061
|
+
@router.post('/login', response_model=Token)
|
|
1062
|
+
async def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
|
1063
|
+
user = await auth_service.authenticate_user(db, form.username, form.password)
|
|
1064
|
+
if not user: raise HTTPException(status_code=401, detail='Invalid credentials')
|
|
1065
|
+
token = create_access_token({'sub': str(user.id)})
|
|
1066
|
+
return {'access_token': token, 'token_type': 'bearer'}
|
|
1067
|
+
|
|
1068
|
+
dependencies/auth.py:
|
|
1069
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/api/auth/login')
|
|
1070
|
+
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
|
1071
|
+
try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
|
|
1072
|
+
except JWTError: raise HTTPException(401, 'Could not validate credentials')
|
|
1073
|
+
user = db.query(User).filter(User.id == payload.get('sub')).first()
|
|
1074
|
+
if not user: raise HTTPException(401, 'User not found')
|
|
1075
|
+
return user
|
|
1076
|
+
|
|
1077
|
+
requirements.txt:
|
|
1078
|
+
fastapi>=0.110, uvicorn[standard], sqlalchemy>=2.0, alembic,
|
|
1079
|
+
pydantic-settings, python-jose[cryptography], passlib[bcrypt], psycopg2-binary, python-dotenv
|
|
1080
|
+
|
|
1081
|
+
Coding rules:
|
|
1082
|
+
- Type EVERYTHING — Pydantic models for all request/response bodies, never dict
|
|
1083
|
+
- Separate ORM models (models/) from Pydantic schemas (schemas/) — never reuse between layers
|
|
1084
|
+
- Depends() for every injected dependency: DB session, current user, settings
|
|
1085
|
+
- pydantic-settings reads .env automatically via env_file = '.env' in Config
|
|
1086
|
+
- HTTPException with status_code + detail for all error responses
|
|
1087
|
+
- Alembic for migrations in production — db.create_all() only in development
|
|
1088
|
+
- Auto-generated /docs (Swagger UI) — document it in README, it's a feature"""
|
|
1089
|
+
|
|
1090
|
+
# Go + Gin
|
|
1091
|
+
return """
|
|
1092
|
+
BACKEND: Go + Gin
|
|
1093
|
+
═════════════════
|
|
1094
|
+
Folder structure (backend/):
|
|
1095
|
+
cmd/server/
|
|
1096
|
+
main.go ← entry point: load config, init DB, wire dependencies, start server
|
|
1097
|
+
internal/
|
|
1098
|
+
config/
|
|
1099
|
+
config.go ← type Config struct; func Load() *Config — reads env via viper/godotenv
|
|
1100
|
+
db/
|
|
1101
|
+
db.go ← func Connect(cfg *Config) *gorm.DB — opens DB, auto-migrate
|
|
1102
|
+
middleware/
|
|
1103
|
+
auth.go ← JWT verification middleware: reads Authorization header, sets userID in context
|
|
1104
|
+
cors.go ← gin-contrib/cors setup with allowed origins from config
|
|
1105
|
+
logger.go ← request/response logging middleware
|
|
1106
|
+
handlers/
|
|
1107
|
+
auth.go ← Register, Login, RefreshToken, Me — each returns gin.HandlerFunc
|
|
1108
|
+
[resource].go ← List, Get, Create, Update, Delete
|
|
1109
|
+
models/
|
|
1110
|
+
user.go ← type User struct { gorm.Model; Email string `gorm:"uniqueIndex"; ... }
|
|
1111
|
+
[resource].go
|
|
1112
|
+
repository/
|
|
1113
|
+
interfaces.go ← type UserRepository interface { FindByEmail, Create, FindByID, ... }
|
|
1114
|
+
user_repo.go ← type userRepo struct { db *gorm.DB }; implements UserRepository
|
|
1115
|
+
[resource]_repo.go
|
|
1116
|
+
services/
|
|
1117
|
+
auth_service.go ← type AuthService interface; type authService struct { repo, cfg }
|
|
1118
|
+
[resource]_service.go
|
|
1119
|
+
router/
|
|
1120
|
+
router.go ← func NewRouter(deps *Deps) *gin.Engine — mounts all routes
|
|
1121
|
+
pkg/
|
|
1122
|
+
jwt/
|
|
1123
|
+
jwt.go ← GenerateAccessToken, GenerateRefreshToken, ValidateToken
|
|
1124
|
+
hash/
|
|
1125
|
+
hash.go ← HashPassword, CheckPasswordHash (bcrypt)
|
|
1126
|
+
response/
|
|
1127
|
+
response.go ← Success(c, data, code), Error(c, msg, code), Paginated(c, data, total)
|
|
1128
|
+
go.mod
|
|
1129
|
+
.env.example
|
|
1130
|
+
Makefile
|
|
1131
|
+
|
|
1132
|
+
cmd/server/main.go:
|
|
1133
|
+
func main() {
|
|
1134
|
+
cfg := config.Load()
|
|
1135
|
+
db := db.Connect(cfg)
|
|
1136
|
+
userRepo := repository.NewUserRepo(db)
|
|
1137
|
+
authService := services.NewAuthService(userRepo, cfg)
|
|
1138
|
+
r := router.NewRouter(&router.Deps{ AuthService: authService, Config: cfg })
|
|
1139
|
+
log.Printf("Server starting on :%s", cfg.Port)
|
|
1140
|
+
r.Run(":" + cfg.Port)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
router/router.go:
|
|
1144
|
+
func NewRouter(deps *Deps) *gin.Engine {
|
|
1145
|
+
r := gin.New()
|
|
1146
|
+
r.Use(gin.Recovery(), middleware.Logger(), middleware.CORS(deps.Config))
|
|
1147
|
+
api := r.Group("/api")
|
|
1148
|
+
api.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) })
|
|
1149
|
+
auth := api.Group("/auth")
|
|
1150
|
+
{
|
|
1151
|
+
auth.POST("/register", handlers.Register(deps.AuthService))
|
|
1152
|
+
auth.POST("/login", handlers.Login(deps.AuthService))
|
|
1153
|
+
auth.POST("/refresh", handlers.RefreshToken(deps.AuthService))
|
|
1154
|
+
}
|
|
1155
|
+
protected := api.Group("/")
|
|
1156
|
+
protected.Use(middleware.AuthRequired(deps.Config.JWTSecret))
|
|
1157
|
+
{
|
|
1158
|
+
protected.GET("/auth/me", handlers.Me(deps.AuthService))
|
|
1159
|
+
[resource] := protected.Group("/[resource]")
|
|
1160
|
+
[resource].GET("", handlers.List[Resource](deps.[Resource]Service))
|
|
1161
|
+
[resource].POST("", handlers.Create[Resource](deps.[Resource]Service))
|
|
1162
|
+
[resource].GET("/:id", handlers.Get[Resource](deps.[Resource]Service))
|
|
1163
|
+
[resource].PUT("/:id", handlers.Update[Resource](deps.[Resource]Service))
|
|
1164
|
+
}
|
|
1165
|
+
return r
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
Handler pattern (copy exactly):
|
|
1169
|
+
func Register(svc services.AuthService) gin.HandlerFunc {
|
|
1170
|
+
return func(c *gin.Context) {
|
|
1171
|
+
var req RegisterRequest
|
|
1172
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
1173
|
+
response.Error(c, err.Error(), http.StatusBadRequest); return
|
|
1174
|
+
}
|
|
1175
|
+
user, err := svc.Register(c.Request.Context(), req)
|
|
1176
|
+
if err != nil {
|
|
1177
|
+
response.Error(c, err.Error(), http.StatusBadRequest); return
|
|
1178
|
+
}
|
|
1179
|
+
response.Success(c, user, http.StatusCreated)
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
go.mod dependencies:
|
|
1184
|
+
github.com/gin-gonic/gin, gorm.io/gorm, gorm.io/driver/postgres,
|
|
1185
|
+
github.com/golang-jwt/jwt/v5, golang.org/x/crypto, github.com/spf13/viper,
|
|
1186
|
+
github.com/gin-contrib/cors
|
|
1187
|
+
|
|
1188
|
+
Coding rules:
|
|
1189
|
+
- Dependency injection via constructor functions (NewAuthService, NewUserRepo) — zero global state
|
|
1190
|
+
- Repository interface defined alongside service, implemented in repository/ — swap DB without changing service
|
|
1191
|
+
- Error wrapping: return fmt.Errorf("authService.Register: %w", err)
|
|
1192
|
+
- Handlers are closures that take a service — never directly access DB in handlers
|
|
1193
|
+
- All config from env vars via viper — never hardcode ports, secrets, DB strings
|
|
1194
|
+
- Table-driven tests for services and handlers"""
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _integration(fe: str, be: str) -> str:
|
|
1198
|
+
"""Frontend ↔ Backend connectivity rules for any tech combo."""
|
|
1199
|
+
if be == "None (static / frontend only)":
|
|
1200
|
+
return """
|
|
1201
|
+
INTEGRATION: Static / Frontend Only
|
|
1202
|
+
═════════════════════════════════════
|
|
1203
|
+
No backend folder is needed for this project.
|
|
1204
|
+
- If a contact form is needed, use a third-party service: Formspree or EmailJS.
|
|
1205
|
+
- If data display is needed, use a public API or mock JSON files in assets/.
|
|
1206
|
+
- Document in README: "This is a static frontend — no server required."
|
|
1207
|
+
- Deploy to: Vercel, Netlify, or GitHub Pages."""
|
|
1208
|
+
|
|
1209
|
+
fe_port = _FE_PORTS.get(fe, 5173)
|
|
1210
|
+
be_port = _BE_PORTS.get(be, 5000)
|
|
1211
|
+
|
|
1212
|
+
# Proxy config per frontend
|
|
1213
|
+
if fe in ("React + Vite", "Vue 3 + Vite", "Svelte + Vite"):
|
|
1214
|
+
proxy_block = f"""vite.config.js dev proxy:
|
|
1215
|
+
server: {{
|
|
1216
|
+
port: {fe_port},
|
|
1217
|
+
proxy: {{ '/api': {{ target: 'http://localhost:{be_port}', changeOrigin: true }} }}
|
|
1218
|
+
}}"""
|
|
1219
|
+
elif fe == "Next.js 14":
|
|
1220
|
+
proxy_block = f"""next.config.js rewrites (proxy in development):
|
|
1221
|
+
async rewrites() {{
|
|
1222
|
+
return [{{ source: '/api/:path*', destination: 'http://localhost:{be_port}/api/:path*' }}];
|
|
1223
|
+
}}"""
|
|
1224
|
+
elif fe == "Angular 17":
|
|
1225
|
+
proxy_block = f"""proxy.conf.json:
|
|
1226
|
+
{{ "/api": {{ "target": "http://localhost:{be_port}", "secure": false, "changeOrigin": true }} }}
|
|
1227
|
+
angular.json → architect.serve.options: {{ "proxyConfig": "proxy.conf.json" }}"""
|
|
1228
|
+
else: # Vanilla
|
|
1229
|
+
proxy_block = f"""Vanilla JS: No dev proxy. Use the full backend URL directly.
|
|
1230
|
+
In js/config.js: export const CONFIG = {{ API_BASE: 'http://localhost:{be_port}' }};
|
|
1231
|
+
In production, update CONFIG.API_BASE to your deployed backend URL."""
|
|
1232
|
+
|
|
1233
|
+
# CORS config per backend
|
|
1234
|
+
if be == "Flask (Python)":
|
|
1235
|
+
cors_block = f"CORS(app, resources={{r'/api/*': {{origins: [\"http://localhost:{fe_port}\"]}}}})"
|
|
1236
|
+
elif be == "Django + DRF (Python)":
|
|
1237
|
+
cors_block = f"CORS_ALLOWED_ORIGINS = ['http://localhost:{fe_port}']"
|
|
1238
|
+
elif be == "Node.js + Express":
|
|
1239
|
+
cors_block = f"cors({{ origin: process.env.CORS_ORIGIN }}) # CORS_ORIGIN=http://localhost:{fe_port}"
|
|
1240
|
+
elif be == "FastAPI (Python)":
|
|
1241
|
+
cors_block = f"CORSMiddleware(allow_origins=['http://localhost:{fe_port}'])"
|
|
1242
|
+
else: # Go + Gin
|
|
1243
|
+
cors_block = f"cors.Config{{AllowOrigins: []string{{\"http://localhost:{fe_port}\"}}}})"
|
|
1244
|
+
|
|
1245
|
+
return f"""
|
|
1246
|
+
INTEGRATION: {fe} ↔ {be}
|
|
1247
|
+
{'═' * (len(fe) + len(be) + 4)}
|
|
1248
|
+
Ports:
|
|
1249
|
+
Frontend: http://localhost:{fe_port}
|
|
1250
|
+
Backend: http://localhost:{be_port}
|
|
1251
|
+
|
|
1252
|
+
Step 1 — Backend CORS (must allow the frontend origin):
|
|
1253
|
+
{cors_block}
|
|
1254
|
+
In production: set CORS origin from environment variable — never hardcode localhost.
|
|
1255
|
+
|
|
1256
|
+
Step 2 — Dev proxy (eliminates CORS errors during development):
|
|
1257
|
+
{proxy_block}
|
|
1258
|
+
|
|
1259
|
+
Step 3 — API health check (always implement this first):
|
|
1260
|
+
Backend: GET /api/health → 200 {{"status": "ok", "timestamp": "..."}}
|
|
1261
|
+
Frontend: On app init, call GET /api/health to verify connectivity.
|
|
1262
|
+
|
|
1263
|
+
Step 4 — Consistent API response envelope (both sides must agree on this shape):
|
|
1264
|
+
Success: {{ "data": <payload>, "error": null }}
|
|
1265
|
+
Failure: {{ "data": null, "error": "<human readable message>" }}
|
|
1266
|
+
HTTP status codes must match: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Server Error.
|
|
1267
|
+
|
|
1268
|
+
Step 5 — Authentication flow (JWT):
|
|
1269
|
+
1. POST /api/auth/login → returns {{ access_token, refresh_token, user }}
|
|
1270
|
+
2. Frontend stores access_token in localStorage; stores refresh_token in httpOnly cookie or localStorage.
|
|
1271
|
+
3. Every subsequent request: Authorization: Bearer <access_token>
|
|
1272
|
+
4. On 401: try POST /api/auth/refresh → new access_token; on failure, redirect to login.
|
|
1273
|
+
5. Frontend api.js / api.service.js / ApiService handles this automatically in interceptors.
|
|
1274
|
+
|
|
1275
|
+
Step 6 — Environment variables (never hardcode URLs):
|
|
1276
|
+
frontend/.env: VITE_API_URL=http://localhost:{be_port} (or NEXT_PUBLIC_API_URL for Next.js)
|
|
1277
|
+
backend/.env: PORT={be_port}, DATABASE_URL=..., SECRET_KEY=..., CORS_ORIGINS=http://localhost:{fe_port}
|
|
1278
|
+
Both: provide .env.example with every variable documented.
|
|
1279
|
+
|
|
1280
|
+
Step 7 — Folder structure at project root:
|
|
1281
|
+
project-root/
|
|
1282
|
+
frontend/ ← {fe} code
|
|
1283
|
+
backend/ ← {be} code
|
|
1284
|
+
README.md ← complete setup instructions (both sides)
|
|
1285
|
+
|
|
1286
|
+
README.md must include:
|
|
1287
|
+
## Prerequisites
|
|
1288
|
+
[list all required tools with version numbers]
|
|
1289
|
+
|
|
1290
|
+
## Setup & Run
|
|
1291
|
+
|
|
1292
|
+
### Backend
|
|
1293
|
+
cd backend
|
|
1294
|
+
[install command] # pip install -r requirements.txt OR npm install OR go mod download
|
|
1295
|
+
[setup command] # flask db upgrade OR python manage.py migrate OR npx prisma migrate dev
|
|
1296
|
+
[run command] # flask run OR uvicorn app.main:app --reload OR npm run dev OR go run ./cmd/server
|
|
1297
|
+
|
|
1298
|
+
### Frontend
|
|
1299
|
+
cd frontend
|
|
1300
|
+
[install command] # npm install
|
|
1301
|
+
[run command] # npm run dev
|
|
1302
|
+
|
|
1303
|
+
Both must run simultaneously. Open http://localhost:{fe_port} in your browser."""
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
# ── Prompt builders ───────────────────────────────────────────────────────────
|
|
1307
|
+
|
|
1308
|
+
def _build_newsite(answers: dict) -> str:
|
|
1309
|
+
name = answers.get("name", "Website")
|
|
1310
|
+
site_type = answers.get("type", "website")
|
|
1311
|
+
desc = answers.get("desc", "")
|
|
1312
|
+
fe = answers.get("frontend", "React + Vite")
|
|
1313
|
+
be = answers.get("backend", "None (static / frontend only)")
|
|
1314
|
+
sections = answers.get("sections", "")
|
|
1315
|
+
features = answers.get("features", "")
|
|
1316
|
+
theme = answers.get("theme", "")
|
|
1317
|
+
|
|
1318
|
+
req_lines = [f'Build a {site_type} called "{name}".']
|
|
1319
|
+
if desc: req_lines.append(f"Description: {desc}")
|
|
1320
|
+
if theme: req_lines.append(f"Visual theme: {theme}")
|
|
1321
|
+
if sections: req_lines.append(f"Sections: {sections}")
|
|
1322
|
+
if features: req_lines.append(f"Features: {features}")
|
|
1323
|
+
requirements = "\n".join(req_lines)
|
|
1324
|
+
|
|
1325
|
+
be_str = be.replace("None (static / frontend only)", "None")
|
|
1326
|
+
|
|
1327
|
+
return f"""## PROJECT: {name}
|
|
1328
|
+
## TYPE: {site_type}
|
|
1329
|
+
{requirements}
|
|
1330
|
+
|
|
1331
|
+
## TECH STACK
|
|
1332
|
+
Frontend: {fe}
|
|
1333
|
+
Backend: {be_str}
|
|
1334
|
+
|
|
1335
|
+
## STEP 1 — THINK BEFORE WRITING ANY CODE
|
|
1336
|
+
Understand what this site is for, who uses it, and what impression it should make.
|
|
1337
|
+
Design a color palette, typography, and layout that genuinely fit the project.
|
|
1338
|
+
Plan every section, every page, every API endpoint before writing a single file.
|
|
1339
|
+
|
|
1340
|
+
## STEP 2 — FOLDER STRUCTURE (non-negotiable)
|
|
1341
|
+
All code goes in TWO top-level folders:
|
|
1342
|
+
frontend/ ← all frontend code
|
|
1343
|
+
backend/ ← all backend code (skip if backend is None)
|
|
1344
|
+
|
|
1345
|
+
{_fe_detail(fe)}
|
|
1346
|
+
|
|
1347
|
+
{_be_detail(be) if "None" not in be else ""}
|
|
1348
|
+
|
|
1349
|
+
{_integration(fe, be)}
|
|
1350
|
+
|
|
1351
|
+
## STEP 3 — DESIGN SYSTEM (use these — do NOT use grey defaults)
|
|
1352
|
+
|
|
1353
|
+
Pick one palette and use it consistently everywhere:
|
|
1354
|
+
|
|
1355
|
+
Option A — Dark (recommended for portfolios, SaaS, agency):
|
|
1356
|
+
:root {{
|
|
1357
|
+
--bg: #0f172a; --surface: #1e293b; --border: #334155;
|
|
1358
|
+
--primary: #6366f1; --primary-hover: #4f46e5; --accent: #06b6d4;
|
|
1359
|
+
--text: #f1f5f9; --text-muted: #94a3b8;
|
|
1360
|
+
--radius: 10px; --shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
1361
|
+
}}
|
|
1362
|
+
|
|
1363
|
+
Option B — Light (recommended for e-commerce, blog, corporate):
|
|
1364
|
+
:root {{
|
|
1365
|
+
--bg: #f8fafc; --surface: #ffffff; --border: #e2e8f0;
|
|
1366
|
+
--primary: #6366f1; --primary-hover: #4f46e5; --accent: #0ea5e9;
|
|
1367
|
+
--text: #0f172a; --text-muted: #64748b;
|
|
1368
|
+
--radius: 10px; --shadow: 0 2px 16px rgba(0,0,0,0.08);
|
|
1369
|
+
}}
|
|
1370
|
+
|
|
1371
|
+
Option C — Bold (recommended for landing pages, product showcase):
|
|
1372
|
+
:root {{
|
|
1373
|
+
--bg: #09090b; --surface: #18181b; --border: #27272a;
|
|
1374
|
+
--primary: #f97316; --primary-hover: #ea580c; --accent: #facc15;
|
|
1375
|
+
--text: #fafafa; --text-muted: #a1a1aa;
|
|
1376
|
+
--radius: 8px; --shadow: 0 4px 32px rgba(0,0,0,0.4);
|
|
1377
|
+
}}
|
|
1378
|
+
|
|
1379
|
+
Required component styles in every project:
|
|
1380
|
+
.btn-primary {{ background: var(--primary); color: #fff; padding: 12px 24px; border-radius: var(--radius); font-weight: 600; border: none; cursor: pointer; }}
|
|
1381
|
+
.btn-primary:hover {{ background: var(--primary-hover); transform: translateY(-1px); }}
|
|
1382
|
+
.card {{ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; }}
|
|
1383
|
+
.section {{ padding: 80px 0; }}
|
|
1384
|
+
.container {{ max-width: 1200px; margin: 0 auto; padding: 0 24px; }}
|
|
1385
|
+
|
|
1386
|
+
Design rules (non-negotiable):
|
|
1387
|
+
- HTML: REAL content — real headings, real body text, real copy. Zero lorem ipsum anywhere.
|
|
1388
|
+
- CSS: real pixel/rem values from the palette above. Every element styled. Not a wireframe.
|
|
1389
|
+
- JS: real working event listeners, real DOM manipulation, real logic.
|
|
1390
|
+
- Mobile-first: works at 375px. Hamburger menu for mobile nav.
|
|
1391
|
+
- Every section polished and complete — hero, cards, forms, footer.
|
|
1392
|
+
|
|
1393
|
+
## STEP 4 — WRITE EVERY FILE using <<<FILE:absolute/path>>> marker
|
|
1394
|
+
Write every planned file completely. Do not skip any file.
|
|
1395
|
+
Write CSS files before JS files. Write every section's CSS.
|
|
1396
|
+
After all files, list what was created.
|
|
1397
|
+
End with: what you built, design decisions, and exact commands to run it."""
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
def _build_newapp(answers: dict) -> str:
|
|
1401
|
+
name = answers.get("name", "App")
|
|
1402
|
+
desc = answers.get("desc", "")
|
|
1403
|
+
fe = answers.get("frontend", "React + Vite")
|
|
1404
|
+
be = answers.get("backend", "Flask (Python)")
|
|
1405
|
+
database = answers.get("database", "PostgreSQL")
|
|
1406
|
+
auth = answers.get("auth", "JWT (email / password)")
|
|
1407
|
+
features = answers.get("features", "")
|
|
1408
|
+
extras = answers.get("extras", "")
|
|
1409
|
+
|
|
1410
|
+
lines = [f'Build an app called "{name}".']
|
|
1411
|
+
if desc: lines.append(f"What it does: {desc}")
|
|
1412
|
+
if features: lines.append(f"Core features: {features}")
|
|
1413
|
+
if extras: lines.append(f"Extra integrations: {extras}")
|
|
1414
|
+
if database: lines.append(f"Database: {database}")
|
|
1415
|
+
if auth: lines.append(f"Authentication: {auth}")
|
|
1416
|
+
requirements = "\n".join(lines)
|
|
1417
|
+
|
|
1418
|
+
return f"""## PROJECT: {name}
|
|
1419
|
+
{requirements}
|
|
1420
|
+
|
|
1421
|
+
## TECH STACK
|
|
1422
|
+
Frontend: {fe}
|
|
1423
|
+
Backend: {be}
|
|
1424
|
+
|
|
1425
|
+
## STEP 1 — ANALYZE AND DESIGN BEFORE ANY CODE
|
|
1426
|
+
- What problem does this app solve? Who are the users?
|
|
1427
|
+
- What data entities exist? List every model and its fields.
|
|
1428
|
+
- What user roles exist? What can each role do?
|
|
1429
|
+
- What are ALL the pages / routes / screens?
|
|
1430
|
+
- What API endpoints does the backend need?
|
|
1431
|
+
Draw the full architecture in your mind before writing file 1.
|
|
1432
|
+
|
|
1433
|
+
## STEP 1.5 — WRITE API_CONTRACT.md FIRST (the very first file, before ANY code)
|
|
1434
|
+
Write <<<FILE:API_CONTRACT.md>>> in the project root containing:
|
|
1435
|
+
- Every endpoint: METHOD /api/path → request JSON (exact keys + types) →
|
|
1436
|
+
response JSON (exact keys + types) → status codes (200/201/400/401/404)
|
|
1437
|
+
- Ports: backend {_BE_PORTS.get(be, 5000)}, frontend {_FE_PORTS.get(fe, 5173)}
|
|
1438
|
+
- Env var names BOTH sides use (VITE_API_URL, DATABASE_URL, JWT_SECRET, ...)
|
|
1439
|
+
- Auth: header format (Authorization: Bearer <token>), token lifetimes
|
|
1440
|
+
|
|
1441
|
+
This contract is LAW for the rest of the build:
|
|
1442
|
+
- Backend routes implement it EXACTLY — same paths, same JSON keys
|
|
1443
|
+
- Frontend services/api.js calls it EXACTLY — never invent an endpoint not in it
|
|
1444
|
+
- If the design must change mid-build: edit API_CONTRACT.md FIRST, then update
|
|
1445
|
+
BOTH sides to match. The contract and the code must never disagree.
|
|
1446
|
+
Frontend↔backend disconnection is the #1 way full-stack builds fail — the
|
|
1447
|
+
contract is what prevents it. Never skip this step.
|
|
1448
|
+
|
|
1449
|
+
## STEP 2 — FOLDER STRUCTURE (non-negotiable)
|
|
1450
|
+
All code in two top-level folders:
|
|
1451
|
+
frontend/ ← {fe}
|
|
1452
|
+
backend/ ← {be}
|
|
1453
|
+
|
|
1454
|
+
{_fe_detail(fe)}
|
|
1455
|
+
|
|
1456
|
+
{_be_detail(be)}
|
|
1457
|
+
|
|
1458
|
+
{_integration(fe, be)}
|
|
1459
|
+
|
|
1460
|
+
## STEP 3 — DATA MODELS & AUTH
|
|
1461
|
+
Database: {database}
|
|
1462
|
+
Auth: {auth}
|
|
1463
|
+
|
|
1464
|
+
Every model must have:
|
|
1465
|
+
- Proper field types, constraints, indexes
|
|
1466
|
+
- Created_at / updated_at timestamps
|
|
1467
|
+
- Relationships defined correctly (FK, many-to-many)
|
|
1468
|
+
|
|
1469
|
+
Auth implementation:
|
|
1470
|
+
- Passwords: bcrypt (cost 12) — NEVER MD5, SHA1, or plain text
|
|
1471
|
+
- JWT: access token (15 min) + refresh token (7 days)
|
|
1472
|
+
- Refresh tokens stored as SHA-256 hash in DB — never the raw token
|
|
1473
|
+
- Protected routes: require valid access token in Authorization: Bearer header
|
|
1474
|
+
|
|
1475
|
+
## STEP 4 — DESIGN SYSTEM (use this exact palette — do NOT use grey defaults)
|
|
1476
|
+
|
|
1477
|
+
Dark theme CSS custom properties (put in index.css or variables.css):
|
|
1478
|
+
:root {{
|
|
1479
|
+
--bg: #0f172a; /* deep navy page background */
|
|
1480
|
+
--surface: #1e293b; /* card / panel background */
|
|
1481
|
+
--surface-2: #263348; /* elevated surface, hover */
|
|
1482
|
+
--border: #334155; /* subtle dividers */
|
|
1483
|
+
--primary: #6366f1; /* indigo — buttons, links */
|
|
1484
|
+
--primary-hover:#4f46e5; /* darker on hover */
|
|
1485
|
+
--accent: #06b6d4; /* cyan — highlights, badges */
|
|
1486
|
+
--success: #10b981; /* green — success states */
|
|
1487
|
+
--error: #ef4444; /* red — errors, destructive */
|
|
1488
|
+
--warning: #f59e0b; /* amber — warnings */
|
|
1489
|
+
--text: #f1f5f9; /* primary text */
|
|
1490
|
+
--text-muted: #94a3b8; /* secondary text, labels */
|
|
1491
|
+
--radius: 10px; /* border radius */
|
|
1492
|
+
--shadow: 0 4px 24px rgba(0,0,0,0.3);
|
|
1493
|
+
}}
|
|
1494
|
+
|
|
1495
|
+
Component patterns (apply consistently):
|
|
1496
|
+
Buttons: background: var(--primary); border-radius: var(--radius); padding: 10px 20px; font-weight: 600;
|
|
1497
|
+
Cards: background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px;
|
|
1498
|
+
Inputs: background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; color: var(--text);
|
|
1499
|
+
Navbar: background: var(--surface); border-bottom: 1px solid var(--border); height: 60px;
|
|
1500
|
+
Sidebar: background: var(--surface); border-right: 1px solid var(--border); width: 240px;
|
|
1501
|
+
Tables: border-collapse: collapse; th background: var(--surface-2); td padding: 12px 16px; border-bottom: 1px solid var(--border);
|
|
1502
|
+
|
|
1503
|
+
Mobile-first: every layout must work at 375px width. Use CSS Grid and Flexbox.
|
|
1504
|
+
Responsive breakpoints: 640px (sm), 768px (md), 1024px (lg).
|
|
1505
|
+
Typography: system-ui font stack. Headings 700 weight. Body 400. Labels 500.
|
|
1506
|
+
|
|
1507
|
+
## STEP 5 — WRITE EVERY FILE using <<<FILE:absolute/path>>> marker
|
|
1508
|
+
|
|
1509
|
+
BUILD IN THIS ORDER (strictly — do not skip ahead):
|
|
1510
|
+
|
|
1511
|
+
Phase 0 — Contract:
|
|
1512
|
+
0. API_CONTRACT.md (from STEP 1.5 — the very first file)
|
|
1513
|
+
|
|
1514
|
+
Phase 1 — Backend:
|
|
1515
|
+
1. backend/.env.example
|
|
1516
|
+
2. backend/ config + models + database setup
|
|
1517
|
+
3. backend/ auth routes (register, login, /api/health)
|
|
1518
|
+
4. backend/ all other routes and services
|
|
1519
|
+
5. backend/ requirements.txt / package.json / go.mod
|
|
1520
|
+
|
|
1521
|
+
Phase 2 — Frontend:
|
|
1522
|
+
6. frontend/.env.example and vite.config.js (with proxy to backend)
|
|
1523
|
+
7. frontend/ services/api.js (axios with interceptors, uses VITE_API_URL)
|
|
1524
|
+
8. frontend/ auth pages (Login, Register)
|
|
1525
|
+
9. frontend/ all other pages and components
|
|
1526
|
+
10. frontend/ package.json
|
|
1527
|
+
|
|
1528
|
+
Phase 3 — Documentation:
|
|
1529
|
+
11. README.md with exact setup + run commands for BOTH sides
|
|
1530
|
+
|
|
1531
|
+
Writing rules:
|
|
1532
|
+
- Use <<<FILE:absolute/path>>> for EVERY file — no exceptions
|
|
1533
|
+
- Every file is complete — zero TODOs, zero stubs, zero "add your logic here"
|
|
1534
|
+
- Never write the same file twice — write it right the first time
|
|
1535
|
+
|
|
1536
|
+
## STEP 6 — RUNTIME VERIFICATION (actually RUN it — never just claim it works)
|
|
1537
|
+
Run these for real with bash. Fix every error you hit before moving on:
|
|
1538
|
+
|
|
1539
|
+
1. bash("cd " + backend folder + " && pip install -r requirements.txt", timeout=300)
|
|
1540
|
+
— a package-name error here means requirements.txt is wrong: fix it now
|
|
1541
|
+
2. bash("cd " + backend folder + " && python app.py", run_in_background=true)
|
|
1542
|
+
→ returns process_id (e.g. 'proc-1')
|
|
1543
|
+
3. process_output(process_id="proc-1", wait_seconds=4)
|
|
1544
|
+
— any traceback in the boot log = fix the bug, restart, recheck
|
|
1545
|
+
4. web_fetch("http://localhost:{_BE_PORTS.get(be, 5000)}/api/health")
|
|
1546
|
+
— must return the ok status. If connection refused: wrong port or app crashed.
|
|
1547
|
+
5. bash("cd " + frontend folder + " && npm install", timeout=600)
|
|
1548
|
+
6. bash("cd " + frontend folder + " && npm run build", timeout=300)
|
|
1549
|
+
— the production build catches missing imports, bad paths, and broken JSX
|
|
1550
|
+
without needing a browser. Fix EVERY build error.
|
|
1551
|
+
7. process_kill(process_id="proc-1") — always stop the backend when done.
|
|
1552
|
+
|
|
1553
|
+
Only after all 7 pass, confirm the static checklist below:
|
|
1554
|
+
|
|
1555
|
+
[ ] API_CONTRACT.md exists and every frontend API call matches a backend route
|
|
1556
|
+
in it — same path, same method, same JSON keys (grep the frontend for
|
|
1557
|
+
api. / fetch( / axios and cross-check each one)
|
|
1558
|
+
[ ] GET /api/health returns 200 {{"status": "ok"}}
|
|
1559
|
+
[ ] Backend CORS configured for http://localhost:{_FE_PORTS.get(fe, 5173)}
|
|
1560
|
+
[ ] vite.config.js proxy: /api → http://localhost:{_BE_PORTS.get(be, 5000)}
|
|
1561
|
+
[ ] frontend/.env: VITE_API_URL=http://localhost:{_BE_PORTS.get(be, 5000)}
|
|
1562
|
+
[ ] services/api.js uses import.meta.env.VITE_API_URL — no hardcoded URLs in code
|
|
1563
|
+
[ ] All imports in every file match the actual file paths created
|
|
1564
|
+
[ ] requirements.txt / package.json has every package the code imports
|
|
1565
|
+
[ ] No file contains TODO, placeholder, stub, or "implement this"
|
|
1566
|
+
[ ] README has working copy-paste commands for backend AND frontend setup
|
|
1567
|
+
|
|
1568
|
+
If any item is false, fix it before finishing."""
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def _build_docker(answers: dict) -> str:
|
|
1572
|
+
database = answers.get("database", "none")
|
|
1573
|
+
extras = answers.get("extras", "none")
|
|
1574
|
+
multistage = answers.get("multistage", "yes")
|
|
1575
|
+
|
|
1576
|
+
services = []
|
|
1577
|
+
if database != "none": services.append(database)
|
|
1578
|
+
if "Redis" in (extras or ""): services.append("Redis")
|
|
1579
|
+
if "Nginx" in (extras or ""): services.append("Nginx")
|
|
1580
|
+
if "Celery" in (extras or ""): services.append("Celery worker")
|
|
1581
|
+
|
|
1582
|
+
return f"""## DOCKER REQUIREMENTS
|
|
1583
|
+
Database: {database}
|
|
1584
|
+
Extra services: {extras}
|
|
1585
|
+
Multi-stage build: {multistage}
|
|
1586
|
+
Services in compose: app{', ' + ', '.join(services) if services else ''}
|
|
1587
|
+
|
|
1588
|
+
## WHAT TO BUILD
|
|
1589
|
+
|
|
1590
|
+
1. Read the project fully — understand language, framework, start command, and port.
|
|
1591
|
+
|
|
1592
|
+
2. Dockerfile:
|
|
1593
|
+
{" Multi-stage: builder stage (install + compile) → runtime stage (copy artifacts only)." if multistage == "yes" else " Single-stage Dockerfile."}
|
|
1594
|
+
- Slim base image: python:3.11-slim, node:18-alpine, golang:1.21-alpine, etc.
|
|
1595
|
+
- Non-root user: RUN useradd -r appuser; USER appuser
|
|
1596
|
+
- Layer caching: COPY dependency files first, install, then COPY source code
|
|
1597
|
+
- EXPOSE correct port
|
|
1598
|
+
- HEALTHCHECK: CMD curl -f http://localhost:PORT/health || exit 1
|
|
1599
|
+
- CMD: exec form only: ["gunicorn", ...] not shell form
|
|
1600
|
+
|
|
1601
|
+
3. .dockerignore: __pycache__, node_modules, .env, .git, *.pyc, dist, build, coverage, *.log
|
|
1602
|
+
|
|
1603
|
+
4. docker-compose.yml:
|
|
1604
|
+
version: '3.9'
|
|
1605
|
+
- app: build: ., env_file: .env, depends_on with condition: service_healthy, restart: unless-stopped
|
|
1606
|
+
{f" - {database.lower()}: official image, named volume, healthcheck" if database != "none" else ""}
|
|
1607
|
+
{" - redis: redis:7-alpine, named volume" if "Redis" in (extras or "") else ""}
|
|
1608
|
+
{" - celery: same image as app, command: celery -A app.celery worker -l info" if "Celery" in (extras or "") else ""}
|
|
1609
|
+
{" - nginx: nginx:alpine, volumes for nginx.conf + static, ports: 80:80 443:443" if "Nginx" in (extras or "") else ""}
|
|
1610
|
+
|
|
1611
|
+
5. .env.example: every variable with example values
|
|
1612
|
+
6. Makefile: build, up, down, logs, shell, test
|
|
1613
|
+
7. README: docker-compose up --build and verify app starts
|
|
1614
|
+
|
|
1615
|
+
Write every file completely."""
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
def _build_ci(answers: dict) -> str:
|
|
1619
|
+
test_fw = answers.get("test_fw", "")
|
|
1620
|
+
deploy = answers.get("deploy", "none")
|
|
1621
|
+
auto_deploy = answers.get("auto_deploy", "no")
|
|
1622
|
+
|
|
1623
|
+
def deploy_step(d):
|
|
1624
|
+
m = {"EC2": "SSH pull + restart", "ECS": "ECR push → ECS deploy",
|
|
1625
|
+
"Cloud Run": "GCR push → gcloud run deploy",
|
|
1626
|
+
"Azure": "az webapp container set", "VPS": "SSH docker-compose up -d",
|
|
1627
|
+
"Heroku": "heroku container:push + release"}
|
|
1628
|
+
for k, v in m.items():
|
|
1629
|
+
if k in d: return v
|
|
1630
|
+
return "deploy to target"
|
|
1631
|
+
|
|
1632
|
+
def ci_secrets(d):
|
|
1633
|
+
m = {"EC2": "EC2_HOST, EC2_USER, EC2_SSH_KEY",
|
|
1634
|
+
"ECS": "AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, ECR_REPOSITORY, ECS_CLUSTER",
|
|
1635
|
+
"Cloud Run": "GCP_PROJECT_ID, GCP_SA_KEY",
|
|
1636
|
+
"Heroku": "HEROKU_API_KEY, HEROKU_APP_NAME",
|
|
1637
|
+
"VPS": "VPS_HOST, VPS_USER, VPS_SSH_KEY"}
|
|
1638
|
+
for k, v in m.items():
|
|
1639
|
+
if k in d: return f" - {v}"
|
|
1640
|
+
return " - No deployment secrets needed"
|
|
1641
|
+
|
|
1642
|
+
return f"""## CI/CD REQUIREMENTS
|
|
1643
|
+
Test framework: {test_fw or "detect from project"}
|
|
1644
|
+
Deployment: {deploy}
|
|
1645
|
+
Auto-deploy on main: {auto_deploy}
|
|
1646
|
+
|
|
1647
|
+
## WHAT TO BUILD
|
|
1648
|
+
|
|
1649
|
+
1. Read project to detect: language, test command, package manager, Dockerfile presence.
|
|
1650
|
+
|
|
1651
|
+
2. .github/workflows/ci.yml:
|
|
1652
|
+
Triggers: push/PR to main and develop
|
|
1653
|
+
Jobs (sequential, each depends_on previous):
|
|
1654
|
+
|
|
1655
|
+
a. lint — checkout → setup → cache deps → install → lint
|
|
1656
|
+
Python: ruff / flake8. Node: eslint. Go: golangci-lint.
|
|
1657
|
+
|
|
1658
|
+
b. test — checkout → setup → cache → install → run tests with coverage
|
|
1659
|
+
{test_fw or "Detect and run test command"}
|
|
1660
|
+
Upload coverage artifact.
|
|
1661
|
+
|
|
1662
|
+
c. build — push to main only
|
|
1663
|
+
Build Docker image, tag :sha-XXXX and :latest, push to registry.
|
|
1664
|
+
|
|
1665
|
+
{" d. deploy — push to main only" if auto_deploy == "yes" and "none" not in deploy else ""}
|
|
1666
|
+
{" " + deploy_step(deploy) if auto_deploy == "yes" and "none" not in deploy else ""}
|
|
1667
|
+
|
|
1668
|
+
3. Caching: hashFiles on requirements.txt / package-lock.json / go.sum
|
|
1669
|
+
4. Secrets needed:
|
|
1670
|
+
{ci_secrets(deploy)}
|
|
1671
|
+
5. Add workflow_dispatch trigger for manual runs.
|
|
1672
|
+
6. README: branch protection — require CI to pass before merge.
|
|
1673
|
+
|
|
1674
|
+
Write every file completely."""
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
_PROMPT_BUILDERS = {
|
|
1678
|
+
"newsite": _build_newsite,
|
|
1679
|
+
"newapp": _build_newapp,
|
|
1680
|
+
"docker": _build_docker,
|
|
1681
|
+
"ci-github": _build_ci,
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
def build_skill_prompt(name: str, answers: dict) -> str | None:
|
|
1686
|
+
builder = _PROMPT_BUILDERS.get(name)
|
|
1687
|
+
if builder:
|
|
1688
|
+
return builder(answers)
|
|
1689
|
+
return get_skill_raw(name)
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
# ── Custom file skills (backward compat) ──────────────────────────────────────
|
|
1693
|
+
|
|
1694
|
+
BUILTIN_SKILLS = {k: f"[interactive — run via /skill {k}]" for k in _PROMPT_BUILDERS}
|
|
1695
|
+
|
|
1696
|
+
|
|
1697
|
+
def get_skill_raw(name: str) -> str | None:
|
|
1698
|
+
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1699
|
+
f = SKILLS_DIR / f"{name}.md"
|
|
1700
|
+
return f.read_text(encoding="utf-8") if f.exists() else None
|
|
1701
|
+
|
|
1702
|
+
|
|
1703
|
+
def get_skill(name: str) -> str | None:
|
|
1704
|
+
if name in _PROMPT_BUILDERS:
|
|
1705
|
+
return f"[Use ask_skill_questions('{name}') + build_skill_prompt() for this skill]"
|
|
1706
|
+
return get_skill_raw(name)
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
def load_skills() -> dict[str, str]:
|
|
1710
|
+
skills = dict(BUILTIN_SKILLS)
|
|
1711
|
+
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1712
|
+
for f in sorted(SKILLS_DIR.glob("*.md")):
|
|
1713
|
+
name = f.stem.lower().replace(" ", "-")
|
|
1714
|
+
if name not in skills:
|
|
1715
|
+
skills[name] = f.read_text(encoding="utf-8")
|
|
1716
|
+
return skills
|
|
1717
|
+
|
|
1718
|
+
|
|
1719
|
+
def show_skills(console) -> None:
|
|
1720
|
+
builtin = list(_PROMPT_BUILDERS.keys())
|
|
1721
|
+
custom = []
|
|
1722
|
+
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1723
|
+
for f in sorted(SKILLS_DIR.glob("*.md")):
|
|
1724
|
+
name = f.stem.lower().replace(" ", "-")
|
|
1725
|
+
if name not in builtin:
|
|
1726
|
+
custom.append((name, f.read_text(encoding="utf-8")))
|
|
1727
|
+
|
|
1728
|
+
descriptions = {
|
|
1729
|
+
"newsite": "Full-stack site — choose frontend + backend tech, frontend/ + backend/ folders",
|
|
1730
|
+
"newapp": "Full-stack app — choose frontend + backend tech, detailed per-framework rules",
|
|
1731
|
+
"docker": "Dockerize — multi-stage build, compose, healthcheck, .dockerignore",
|
|
1732
|
+
"ci-github": "GitHub Actions — lint → test → build → deploy, secrets, caching",
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
console.print("\n[bold]Built-in Skills[/bold] [dim](interactive Q&A)[/dim]")
|
|
1736
|
+
for name in builtin:
|
|
1737
|
+
console.print(f" [green]{name:<18}[/green] [dim]{descriptions.get(name, '')}[/dim]")
|
|
1738
|
+
|
|
1739
|
+
if custom:
|
|
1740
|
+
console.print("\n[bold]Custom Skills[/bold] [dim](~/.bharatcode/skills/)[/dim]")
|
|
1741
|
+
for name, content in custom:
|
|
1742
|
+
preview = content.split("\n")[0][:60]
|
|
1743
|
+
console.print(f" [cyan]{name:<18}[/cyan] [dim]{preview}[/dim]")
|
|
1744
|
+
|
|
1745
|
+
console.print()
|
|
1746
|
+
console.print("[dim]Usage: /skill <name> Custom: ~/.bharatcode/skills/<name>.md[/dim]\n")
|