blacksmith-cli 0.1.8 → 0.1.9
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/dist/index.js +1200 -812
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/frontend/src/api/hooks/.gitkeep +0 -0
package/dist/index.js
CHANGED
|
@@ -858,19 +858,23 @@ function banner() {
|
|
|
858
858
|
console.log(chalk.dim(" Welcome to Blacksmith \u2014 forge fullstack apps with one command."));
|
|
859
859
|
console.log();
|
|
860
860
|
}
|
|
861
|
-
function printNextSteps(projectName,
|
|
861
|
+
function printNextSteps(projectName, projectType = "fullstack", backendPort, frontendPort) {
|
|
862
862
|
log.blank();
|
|
863
863
|
log.success("Project created successfully!");
|
|
864
864
|
log.blank();
|
|
865
865
|
console.log(chalk.bold(" Next steps:"));
|
|
866
866
|
console.log();
|
|
867
867
|
console.log(` ${chalk.cyan("cd")} ${projectName}`);
|
|
868
|
-
console.log(` ${chalk.cyan("blacksmith dev")} ${chalk.dim("# Start development
|
|
868
|
+
console.log(` ${chalk.cyan("blacksmith dev")} ${chalk.dim("# Start development server" + (projectType === "fullstack" ? "s" : ""))}`);
|
|
869
869
|
console.log();
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
870
|
+
if (backendPort !== void 0) {
|
|
871
|
+
console.log(chalk.dim(` Django: http://localhost:${backendPort}`));
|
|
872
|
+
console.log(chalk.dim(` Swagger: http://localhost:${backendPort}/api/docs/`));
|
|
873
|
+
console.log(chalk.dim(` ReDoc: http://localhost:${backendPort}/api/redoc/`));
|
|
874
|
+
}
|
|
875
|
+
if (frontendPort !== void 0) {
|
|
876
|
+
console.log(chalk.dim(` React: http://localhost:${frontendPort}`));
|
|
877
|
+
}
|
|
874
878
|
log.blank();
|
|
875
879
|
}
|
|
876
880
|
|
|
@@ -1018,13 +1022,33 @@ function findProjectRoot(startDir) {
|
|
|
1018
1022
|
'Not inside a Blacksmith project. Run "blacksmith init <name>" to create one, or navigate to an existing Blacksmith project.'
|
|
1019
1023
|
);
|
|
1020
1024
|
}
|
|
1025
|
+
function getProjectType(projectRoot) {
|
|
1026
|
+
const config = loadConfig(projectRoot);
|
|
1027
|
+
return config.type || "fullstack";
|
|
1028
|
+
}
|
|
1029
|
+
function hasBackend(projectRoot) {
|
|
1030
|
+
const type = getProjectType(projectRoot);
|
|
1031
|
+
return type === "fullstack" || type === "backend";
|
|
1032
|
+
}
|
|
1033
|
+
function hasFrontend(projectRoot) {
|
|
1034
|
+
const type = getProjectType(projectRoot);
|
|
1035
|
+
return type === "fullstack" || type === "frontend";
|
|
1036
|
+
}
|
|
1021
1037
|
function getBackendDir(projectRoot) {
|
|
1022
1038
|
const root = projectRoot || findProjectRoot();
|
|
1023
|
-
|
|
1039
|
+
const type = getProjectType(root);
|
|
1040
|
+
if (type === "frontend") {
|
|
1041
|
+
throw new Error("This is a frontend-only project. There is no backend directory.");
|
|
1042
|
+
}
|
|
1043
|
+
return type === "backend" ? root : path3.join(root, "backend");
|
|
1024
1044
|
}
|
|
1025
1045
|
function getFrontendDir(projectRoot) {
|
|
1026
1046
|
const root = projectRoot || findProjectRoot();
|
|
1027
|
-
|
|
1047
|
+
const type = getProjectType(root);
|
|
1048
|
+
if (type === "backend") {
|
|
1049
|
+
throw new Error("This is a backend-only project. There is no frontend directory.");
|
|
1050
|
+
}
|
|
1051
|
+
return type === "frontend" ? root : path3.join(root, "frontend");
|
|
1028
1052
|
}
|
|
1029
1053
|
function loadConfig(projectRoot) {
|
|
1030
1054
|
const root = projectRoot || findProjectRoot();
|
|
@@ -1052,11 +1076,12 @@ var coreRulesSkill = {
|
|
|
1052
1076
|
- **Typography**: Use \`Heading\` and \`Text\` \u2014 NEVER raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, or \`<span>\` with text classes
|
|
1053
1077
|
- **Separators**: Use \`Divider\` \u2014 NEVER \`<hr>\`
|
|
1054
1078
|
- **Everything else**: \`Button\`, \`Card\`, \`Badge\`, \`Input\`, \`Table\`, \`Modal\`, \`Alert\`, \`Skeleton\`, \`Stat\`, etc.
|
|
1079
|
+
- **Exceptions**: Semantic HTML landmarks (\`<main>\`, \`<section>\`, \`<nav>\`, \`<header>\`, \`<footer>\`, \`<article>\`, \`<aside>\`) are acceptable for page structure. \`<form>\` is acceptable with React Hook Form. \`<Link>\` from react-router-dom for navigation
|
|
1055
1080
|
- See the \`chakra-ui-react\` skill for the full component list
|
|
1056
1081
|
|
|
1057
1082
|
### 2. Pages Are Thin Orchestrators
|
|
1058
|
-
-
|
|
1059
|
-
- Break every page into child components in a \`components/\` folder
|
|
1083
|
+
- Pages import components, call hooks, and compose JSX \u2014 they should not contain business logic or large JSX blocks
|
|
1084
|
+
- Break every page into child components in a \`components/\` folder. Aim for clarity, not a strict line count
|
|
1060
1085
|
- See the \`page-structure\` skill for the full pattern with examples
|
|
1061
1086
|
|
|
1062
1087
|
### 3. Components Render, Hooks Think
|
|
@@ -1071,14 +1096,34 @@ var coreRulesSkill = {
|
|
|
1071
1096
|
- When adding a new page, add its path to the enum before \`// blacksmith:path\`
|
|
1072
1097
|
- Use \`buildPath(Path.ResetPassword, { token })\` for dynamic segments
|
|
1073
1098
|
|
|
1074
|
-
### 5.
|
|
1099
|
+
### 5. API Hooks vs UI Hooks \u2014 Two Different Places
|
|
1100
|
+
- **API hooks** (data fetching) \u2192 \`src/api/hooks/<resource>/\` \u2014 queries, mutations, cache invalidation. Import as: \`import { usePosts } from '@/api/hooks/posts'\`
|
|
1101
|
+
- **UI hooks** (page logic) \u2192 \`pages/<page>/hooks/\` or \`features/<feature>/hooks/\` \u2014 form state, pagination, filtering, debouncing
|
|
1102
|
+
- Never put API data fetching in page-level hooks. Never put UI-only logic in \`src/api/hooks/\`
|
|
1103
|
+
- See the \`react-query\` skill for API hook conventions
|
|
1104
|
+
|
|
1105
|
+
### 6. Use Generated API Client Code
|
|
1106
|
+
- Always check \`src/api/generated/\` first before writing any API calls \u2014 use the generated types, query options, mutations, and query keys
|
|
1107
|
+
- Only write manual API client code when no generated code exists for the endpoint (e.g. the endpoint hasn't been synced yet)
|
|
1108
|
+
- **In fullstack projects:** after creating or modifying any backend endpoint (views, serializers, URLs), run \`blacksmith sync\` from the project root to regenerate the frontend API client before writing frontend code that consumes it
|
|
1109
|
+
|
|
1110
|
+
### 7. Prioritize Modularization and Code Reuse
|
|
1111
|
+
- **Always reuse before writing** \u2014 before creating a new function, search the codebase for an existing one that does the same thing or can be extended
|
|
1112
|
+
- **Extract reusable logic to utils** \u2014 if a function can be useful outside the file it lives in, move it to a \`utils/\` folder. This applies to both frontend and backend
|
|
1113
|
+
- Frontend: page/feature-scoped \u2192 \`<page>/utils/\`; app-wide \u2192 \`src/shared/utils/\`
|
|
1114
|
+
- Backend: app-scoped \u2192 \`apps/<app>/utils.py\` (or \`utils/\` package); project-wide \u2192 \`utils/\` at the backend root
|
|
1115
|
+
- **No inline helper functions** \u2014 standalone functions sitting in component files, view files, or serializer files should be extracted to utils
|
|
1116
|
+
- **No duplicated logic** \u2014 if the same logic appears in two places, extract it into a shared utility immediately
|
|
1117
|
+
- See the \`frontend-modularization\` and \`backend-modularization\` skills for full conventions
|
|
1118
|
+
|
|
1119
|
+
### 8. Follow the Page/Feature Folder Structure
|
|
1075
1120
|
\`\`\`
|
|
1076
1121
|
pages/<page>/
|
|
1077
1122
|
\u251C\u2500\u2500 <page>.tsx # Thin orchestrator (default export)
|
|
1078
1123
|
\u251C\u2500\u2500 routes.tsx # RouteObject[] using Path enum
|
|
1079
1124
|
\u251C\u2500\u2500 index.ts # Re-exports public API
|
|
1080
1125
|
\u251C\u2500\u2500 components/ # Child components
|
|
1081
|
-
\u2514\u2500\u2500 hooks/ # Page-local hooks (UI logic, not API hooks)
|
|
1126
|
+
\u2514\u2500\u2500 hooks/ # Page-local hooks (UI logic only, not API hooks)
|
|
1082
1127
|
\`\`\`
|
|
1083
1128
|
- See the \`page-structure\` skill for full conventions
|
|
1084
1129
|
`;
|
|
@@ -1094,49 +1139,80 @@ var projectOverviewSkill = {
|
|
|
1094
1139
|
render(ctx) {
|
|
1095
1140
|
return `# ${ctx.projectName}
|
|
1096
1141
|
|
|
1097
|
-
A
|
|
1142
|
+
A web application scaffolded by **Blacksmith CLI**. Check \`blacksmith.config.json\` at the project root for the project type (\`fullstack\`, \`backend\`, or \`frontend\`) and configuration.
|
|
1098
1143
|
|
|
1099
1144
|
## Project Structure
|
|
1100
1145
|
|
|
1146
|
+
The structure depends on the project type configured in \`blacksmith.config.json\`:
|
|
1147
|
+
|
|
1148
|
+
**Fullstack** (\`type: "fullstack"\`) \u2014 Django backend + React frontend in subdirectories:
|
|
1101
1149
|
\`\`\`
|
|
1102
1150
|
${ctx.projectName}/
|
|
1103
1151
|
\u251C\u2500\u2500 backend/ # Django project
|
|
1104
1152
|
\u2502 \u251C\u2500\u2500 apps/ # Django apps (one per resource)
|
|
1105
|
-
\u2502 \u2502 \u2514\u2500\u2500 users/ # Built-in user app
|
|
1106
1153
|
\u2502 \u251C\u2500\u2500 config/ # Django settings, urls, wsgi/asgi
|
|
1107
|
-
\u2502 \
|
|
1154
|
+
\u2502 \u251C\u2500\u2500 utils/ # Shared backend utilities
|
|
1108
1155
|
\u2502 \u251C\u2500\u2500 manage.py
|
|
1109
|
-
\u2502 \u251C\u2500\u2500 requirements.txt
|
|
1110
1156
|
\u2502 \u2514\u2500\u2500 venv/ # Python virtual environment
|
|
1111
1157
|
\u251C\u2500\u2500 frontend/ # React + Vite project
|
|
1112
1158
|
\u2502 \u251C\u2500\u2500 src/
|
|
1113
|
-
\u2502 \u2502 \u251C\u2500\u2500 api/ # API client
|
|
1159
|
+
\u2502 \u2502 \u251C\u2500\u2500 api/ # API client and hooks
|
|
1114
1160
|
\u2502 \u2502 \u251C\u2500\u2500 features/ # Feature modules (auth, etc.)
|
|
1115
1161
|
\u2502 \u2502 \u251C\u2500\u2500 pages/ # Top-level pages
|
|
1116
|
-
\u2502 \u2502 \u251C\u2500\u2500 router/ # React Router setup
|
|
1117
|
-
\u2502 \u2502 \
|
|
1118
|
-
\u2502 \
|
|
1119
|
-
\u2502 \u251C\u2500\u2500 package.json
|
|
1120
|
-
\u2502 \u2514\u2500\u2500 tailwind.config.js
|
|
1162
|
+
\u2502 \u2502 \u251C\u2500\u2500 router/ # React Router setup
|
|
1163
|
+
\u2502 \u2502 \u2514\u2500\u2500 shared/ # Shared components, hooks, utils
|
|
1164
|
+
\u2502 \u2514\u2500\u2500 package.json
|
|
1121
1165
|
\u251C\u2500\u2500 blacksmith.config.json
|
|
1122
|
-
\u2514\u2500\u2500 CLAUDE.md
|
|
1166
|
+
\u2514\u2500\u2500 CLAUDE.md
|
|
1167
|
+
\`\`\`
|
|
1168
|
+
|
|
1169
|
+
**Backend-only** (\`type: "backend"\`) \u2014 Django project at root:
|
|
1170
|
+
\`\`\`
|
|
1171
|
+
${ctx.projectName}/
|
|
1172
|
+
\u251C\u2500\u2500 apps/
|
|
1173
|
+
\u251C\u2500\u2500 config/
|
|
1174
|
+
\u251C\u2500\u2500 utils/
|
|
1175
|
+
\u251C\u2500\u2500 manage.py
|
|
1176
|
+
\u251C\u2500\u2500 venv/
|
|
1177
|
+
\u2514\u2500\u2500 blacksmith.config.json
|
|
1178
|
+
\`\`\`
|
|
1179
|
+
|
|
1180
|
+
**Frontend-only** (\`type: "frontend"\`) \u2014 React project at root:
|
|
1181
|
+
\`\`\`
|
|
1182
|
+
${ctx.projectName}/
|
|
1183
|
+
\u251C\u2500\u2500 src/
|
|
1184
|
+
\u2502 \u251C\u2500\u2500 api/
|
|
1185
|
+
\u2502 \u251C\u2500\u2500 pages/
|
|
1186
|
+
\u2502 \u251C\u2500\u2500 router/
|
|
1187
|
+
\u2502 \u2514\u2500\u2500 shared/
|
|
1188
|
+
\u251C\u2500\u2500 package.json
|
|
1189
|
+
\u2514\u2500\u2500 blacksmith.config.json
|
|
1123
1190
|
\`\`\`
|
|
1124
1191
|
|
|
1125
1192
|
## Commands
|
|
1126
1193
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1194
|
+
| Command | Fullstack | Backend | Frontend |
|
|
1195
|
+
|---|---|---|---|
|
|
1196
|
+
| \`blacksmith dev\` | Django + Vite + sync | Django only | Vite only |
|
|
1197
|
+
| \`blacksmith sync\` | Regenerate frontend types | N/A | N/A |
|
|
1198
|
+
| \`blacksmith make:resource <Name>\` | Both ends | Backend only | Frontend only |
|
|
1199
|
+
| \`blacksmith build\` | Both | collectstatic | Vite build |
|
|
1200
|
+
| \`blacksmith eject\` | Remove Blacksmith | Remove Blacksmith | Remove Blacksmith |
|
|
1132
1201
|
|
|
1133
1202
|
## Development Workflow
|
|
1134
1203
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1204
|
+
**Fullstack:**
|
|
1205
|
+
1. Define models, serializers, and viewsets in the backend
|
|
1206
|
+
2. Run \`blacksmith sync\` to generate TypeScript types and API client
|
|
1207
|
+
3. Build frontend features using the generated hooks and types
|
|
1208
|
+
|
|
1209
|
+
**Backend-only:**
|
|
1210
|
+
1. Define models, serializers, and viewsets
|
|
1211
|
+
2. Run migrations and test endpoints
|
|
1212
|
+
|
|
1213
|
+
**Frontend-only:**
|
|
1214
|
+
1. Build pages and components
|
|
1215
|
+
2. Create API hooks in \`src/api/hooks/\` for data fetching
|
|
1140
1216
|
`;
|
|
1141
1217
|
}
|
|
1142
1218
|
};
|
|
@@ -1179,6 +1255,11 @@ var djangoSkill = {
|
|
|
1179
1255
|
- Override \`get_queryset()\` to scope data to the current user when needed
|
|
1180
1256
|
- Override \`perform_create()\` to inject \`request.user\` or other context into the serializer save
|
|
1181
1257
|
|
|
1258
|
+
### Sync After Backend Changes (Fullstack Projects)
|
|
1259
|
+
> **RULE: After creating or modifying any endpoint (views, serializers, URLs, or model fields that affect the API), run \`blacksmith sync\` from the project root before writing frontend code that uses the endpoint.**
|
|
1260
|
+
>
|
|
1261
|
+
> This regenerates the TypeScript types, query options, mutations, and Zod schemas in \`frontend/src/api/generated/\`. Without syncing, the frontend will be working against stale or missing type definitions.
|
|
1262
|
+
|
|
1182
1263
|
### URLs
|
|
1183
1264
|
- Each app has its own \`urls.py\` with a \`DefaultRouter\`
|
|
1184
1265
|
- Register viewsets on the router: \`router.register('resources', ResourceViewSet)\`
|
|
@@ -2069,6 +2150,179 @@ def internal_health_check(self, request):
|
|
|
2069
2150
|
}
|
|
2070
2151
|
};
|
|
2071
2152
|
|
|
2153
|
+
// src/skills/backend-modularization.ts
|
|
2154
|
+
init_esm_shims();
|
|
2155
|
+
var backendModularizationSkill = {
|
|
2156
|
+
id: "backend-modularization",
|
|
2157
|
+
name: "Backend Modularization",
|
|
2158
|
+
description: "When and how to promote Django modules (models, serializers, views, tests, admin) from single files to packages with per-class files.",
|
|
2159
|
+
render(_ctx) {
|
|
2160
|
+
return `## Backend Modularization
|
|
2161
|
+
|
|
2162
|
+
> **RULE: When a Django module contains more than one class, promote it from a single file to a package (folder) with one class per file and re-export everything from \`__init__.py\`.**
|
|
2163
|
+
|
|
2164
|
+
This applies to \`models.py\`, \`serializers.py\`, \`views.py\`, \`admin.py\`, and \`tests.py\`.
|
|
2165
|
+
|
|
2166
|
+
### Single-Class Module \u2014 Keep as a File
|
|
2167
|
+
|
|
2168
|
+
When an app has only one model, one serializer, etc., a single file is fine:
|
|
2169
|
+
|
|
2170
|
+
\`\`\`
|
|
2171
|
+
apps/posts/
|
|
2172
|
+
\u251C\u2500\u2500 __init__.py
|
|
2173
|
+
\u251C\u2500\u2500 models.py # one model: Post
|
|
2174
|
+
\u251C\u2500\u2500 serializers.py # one serializer: PostSerializer
|
|
2175
|
+
\u251C\u2500\u2500 views.py # one viewset: PostViewSet
|
|
2176
|
+
\u251C\u2500\u2500 urls.py
|
|
2177
|
+
\u251C\u2500\u2500 admin.py
|
|
2178
|
+
\u2514\u2500\u2500 tests.py
|
|
2179
|
+
\`\`\`
|
|
2180
|
+
|
|
2181
|
+
### Multi-Class Module \u2014 Promote to a Package
|
|
2182
|
+
|
|
2183
|
+
When a second class is added, convert the file to a package:
|
|
2184
|
+
|
|
2185
|
+
\`\`\`
|
|
2186
|
+
apps/posts/
|
|
2187
|
+
\u251C\u2500\u2500 __init__.py
|
|
2188
|
+
\u251C\u2500\u2500 models/
|
|
2189
|
+
\u2502 \u251C\u2500\u2500 __init__.py # from .post import * / from .comment import *
|
|
2190
|
+
\u2502 \u251C\u2500\u2500 post.py # class Post(models.Model)
|
|
2191
|
+
\u2502 \u2514\u2500\u2500 comment.py # class Comment(models.Model)
|
|
2192
|
+
\u251C\u2500\u2500 serializers/
|
|
2193
|
+
\u2502 \u251C\u2500\u2500 __init__.py # from .post_serializer import * / from .comment_serializer import *
|
|
2194
|
+
\u2502 \u251C\u2500\u2500 post_serializer.py
|
|
2195
|
+
\u2502 \u2514\u2500\u2500 comment_serializer.py
|
|
2196
|
+
\u251C\u2500\u2500 views/
|
|
2197
|
+
\u2502 \u251C\u2500\u2500 __init__.py # from .post_views import * / from .comment_views import *
|
|
2198
|
+
\u2502 \u251C\u2500\u2500 post_views.py
|
|
2199
|
+
\u2502 \u2514\u2500\u2500 comment_views.py
|
|
2200
|
+
\u251C\u2500\u2500 admin/
|
|
2201
|
+
\u2502 \u251C\u2500\u2500 __init__.py # from .post_admin import * / from .comment_admin import *
|
|
2202
|
+
\u2502 \u251C\u2500\u2500 post_admin.py
|
|
2203
|
+
\u2502 \u2514\u2500\u2500 comment_admin.py
|
|
2204
|
+
\u251C\u2500\u2500 tests/
|
|
2205
|
+
\u2502 \u251C\u2500\u2500 __init__.py
|
|
2206
|
+
\u2502 \u251C\u2500\u2500 test_post.py
|
|
2207
|
+
\u2502 \u2514\u2500\u2500 test_comment.py
|
|
2208
|
+
\u251C\u2500\u2500 urls.py
|
|
2209
|
+
\u2514\u2500\u2500 ...
|
|
2210
|
+
\`\`\`
|
|
2211
|
+
|
|
2212
|
+
### The \`__init__.py\` Contract
|
|
2213
|
+
|
|
2214
|
+
Every package \`__init__.py\` re-exports all public names so that imports from outside the app remain unchanged:
|
|
2215
|
+
|
|
2216
|
+
\`\`\`python
|
|
2217
|
+
# models/__init__.py
|
|
2218
|
+
from .post import *
|
|
2219
|
+
from .comment import *
|
|
2220
|
+
\`\`\`
|
|
2221
|
+
|
|
2222
|
+
This means the rest of the codebase continues to use:
|
|
2223
|
+
\`\`\`python
|
|
2224
|
+
from apps.posts.models import Post, Comment
|
|
2225
|
+
from apps.posts.serializers import PostSerializer, CommentSerializer
|
|
2226
|
+
from apps.posts.views import PostViewSet, CommentViewSet
|
|
2227
|
+
\`\`\`
|
|
2228
|
+
|
|
2229
|
+
No import paths change when you promote a file to a package \u2014 that is the whole point.
|
|
2230
|
+
|
|
2231
|
+
### File Naming Conventions
|
|
2232
|
+
|
|
2233
|
+
| Module | Single file | Package file names |
|
|
2234
|
+
|---|---|---|
|
|
2235
|
+
| Models | \`models.py\` | \`models/<model_name>.py\` (e.g. \`post.py\`, \`comment.py\`) |
|
|
2236
|
+
| Serializers | \`serializers.py\` | \`serializers/<model_name>_serializer.py\` |
|
|
2237
|
+
| Views | \`views.py\` | \`views/<model_name>_views.py\` |
|
|
2238
|
+
| Admin | \`admin.py\` | \`admin/<model_name>_admin.py\` |
|
|
2239
|
+
| Tests | \`tests.py\` | \`tests/test_<model_name>.py\` |
|
|
2240
|
+
|
|
2241
|
+
### Using \`__all__\` in Each File
|
|
2242
|
+
|
|
2243
|
+
Define \`__all__\` in each file to make the \`from .module import *\` explicit:
|
|
2244
|
+
|
|
2245
|
+
\`\`\`python
|
|
2246
|
+
# models/post.py
|
|
2247
|
+
from django.db import models
|
|
2248
|
+
|
|
2249
|
+
__all__ = ['Post']
|
|
2250
|
+
|
|
2251
|
+
class Post(models.Model):
|
|
2252
|
+
title = models.CharField(max_length=255)
|
|
2253
|
+
content = models.TextField()
|
|
2254
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
2255
|
+
|
|
2256
|
+
def __str__(self):
|
|
2257
|
+
return self.title
|
|
2258
|
+
\`\`\`
|
|
2259
|
+
|
|
2260
|
+
\`\`\`python
|
|
2261
|
+
# serializers/post_serializer.py
|
|
2262
|
+
from rest_framework import serializers
|
|
2263
|
+
from apps.posts.models import Post
|
|
2264
|
+
|
|
2265
|
+
__all__ = ['PostSerializer']
|
|
2266
|
+
|
|
2267
|
+
class PostSerializer(serializers.ModelSerializer):
|
|
2268
|
+
class Meta:
|
|
2269
|
+
model = Post
|
|
2270
|
+
fields = '__all__'
|
|
2271
|
+
\`\`\`
|
|
2272
|
+
|
|
2273
|
+
### When to Promote
|
|
2274
|
+
|
|
2275
|
+
- **Do promote** when a module contains 2 or more classes (models, serializers, viewsets, admin classes)
|
|
2276
|
+
- **Don't promote** \`urls.py\` \u2014 URL configuration is typically one file regardless of size, since routers naturally aggregate
|
|
2277
|
+
- **Don't promote** a module just because it is long \u2014 if it has one class with many methods, keep it as a file; length alone is not a reason
|
|
2278
|
+
- **Don't promote preemptively** \u2014 start with a single file and convert when the second class arrives
|
|
2279
|
+
|
|
2280
|
+
### Migration Considerations
|
|
2281
|
+
|
|
2282
|
+
When promoting \`models.py\` to a \`models/\` package, Django migrations continue to work as long as the import path in \`__init__.py\` is correct. Existing migrations reference the app label, not the file path, so no migration changes are needed.
|
|
2283
|
+
|
|
2284
|
+
### Utility Functions
|
|
2285
|
+
|
|
2286
|
+
> **RULE: Reuse before writing. If a function can be used outside the file it lives in, it belongs in utils.**
|
|
2287
|
+
|
|
2288
|
+
- **App-scoped utilities** \u2192 \`apps/<app>/utils.py\` (or \`utils/\` package if multiple files)
|
|
2289
|
+
- **Project-wide utilities** \u2192 \`utils/\` at the backend root (e.g. \`utils/formatting.py\`, \`utils/permissions.py\`)
|
|
2290
|
+
|
|
2291
|
+
Before writing any helper function, search the codebase for an existing one. If the same logic exists in two places, extract it into a shared utility immediately.
|
|
2292
|
+
|
|
2293
|
+
\`\`\`python
|
|
2294
|
+
# BAD \u2014 helper function sitting in views.py
|
|
2295
|
+
def parse_date_range(request):
|
|
2296
|
+
...
|
|
2297
|
+
|
|
2298
|
+
class ReportViewSet(ModelViewSet):
|
|
2299
|
+
def get_queryset(self):
|
|
2300
|
+
start, end = parse_date_range(self.request)
|
|
2301
|
+
...
|
|
2302
|
+
|
|
2303
|
+
# GOOD \u2014 extracted to utils
|
|
2304
|
+
# apps/reports/utils.py
|
|
2305
|
+
def parse_date_range(request):
|
|
2306
|
+
...
|
|
2307
|
+
|
|
2308
|
+
# apps/reports/views.py
|
|
2309
|
+
from apps.reports.utils import parse_date_range
|
|
2310
|
+
\`\`\`
|
|
2311
|
+
|
|
2312
|
+
\`utils.py\` follows the same promotion rule \u2014 when it has multiple unrelated groups of functions, promote it to a \`utils/\` package with focused files (\`utils/dates.py\`, \`utils/formatting.py\`, etc.) and re-export from \`__init__.py\`.
|
|
2313
|
+
|
|
2314
|
+
### Summary
|
|
2315
|
+
|
|
2316
|
+
1. **One class per file** when a module is promoted to a package
|
|
2317
|
+
2. **\`__init__.py\` re-exports everything** \u2014 external imports never change
|
|
2318
|
+
3. **Use \`__all__\`** in each file to be explicit about exports
|
|
2319
|
+
4. **Promote at 2+ classes** \u2014 not preemptively, not based on line count alone
|
|
2320
|
+
5. **\`urls.py\` stays a file** \u2014 it does not get promoted
|
|
2321
|
+
6. **Extract reusable functions to utils** \u2014 search before writing, no duplicated logic
|
|
2322
|
+
`;
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2072
2326
|
// src/skills/react.ts
|
|
2073
2327
|
init_esm_shims();
|
|
2074
2328
|
var reactSkill = {
|
|
@@ -2298,9 +2552,14 @@ if (errorMessage) {
|
|
|
2298
2552
|
|
|
2299
2553
|
### Creating Resource Hook Files
|
|
2300
2554
|
|
|
2301
|
-
|
|
2555
|
+
> **RULE: All API hooks live in \`src/api/hooks/<resource>/\` \u2014 never in page-level \`hooks/\` folders.**
|
|
2556
|
+
> Page-level \`hooks/\` are for UI logic only (form state, filters, pagination). API data access is centralized in \`src/api/hooks/\`.
|
|
2557
|
+
|
|
2558
|
+
When building hooks for a resource, create a folder under \`src/api/hooks/\` with these files:
|
|
2302
2559
|
|
|
2303
|
-
**\`
|
|
2560
|
+
**\`src/api/hooks/<resource>/index.ts\`** \u2014 Re-exports all hooks from the folder.
|
|
2561
|
+
|
|
2562
|
+
**\`src/api/hooks/<resource>/use-<resources>.ts\`** \u2014 List query hook:
|
|
2304
2563
|
\`\`\`tsx
|
|
2305
2564
|
import { useApiQuery } from '@/shared/hooks/use-api-query'
|
|
2306
2565
|
import { postsListOptions } from '@/api/generated/@tanstack/react-query.gen'
|
|
@@ -2330,7 +2589,7 @@ export function usePosts(params: UsePostsParams = {}) {
|
|
|
2330
2589
|
}
|
|
2331
2590
|
\`\`\`
|
|
2332
2591
|
|
|
2333
|
-
**\`use-<resource>-mutations.ts\`** \u2014 Create/update/delete hooks:
|
|
2592
|
+
**\`src/api/hooks/<resource>/use-<resource>-mutations.ts\`** \u2014 Create/update/delete hooks:
|
|
2334
2593
|
\`\`\`tsx
|
|
2335
2594
|
import { useApiMutation } from '@/shared/hooks/use-api-mutation'
|
|
2336
2595
|
import {
|
|
@@ -2371,7 +2630,7 @@ export function useDeletePost() {
|
|
|
2371
2630
|
1. **Never use raw \`useQuery\` or \`useMutation\`** \u2014 always go through \`useApiQuery\` / \`useApiMutation\`
|
|
2372
2631
|
2. **Never manage API error state with \`useState\`** \u2014 error state is derived from TanStack Query's \`error\` field
|
|
2373
2632
|
3. **Always pass \`invalidateKeys\`** on mutations that modify data \u2014 ensures the UI stays in sync
|
|
2374
|
-
4. **
|
|
2633
|
+
4. **Always prefer generated API client code** \u2014 use the generated options, mutations, types, and query keys from \`@/api/generated/\`. Check \`@/api/generated/@tanstack/react-query.gen\` for available query options and mutations before writing anything manually. Only write a manual \`queryFn\` as a last resort when no generated code exists for the endpoint (e.g. the backend endpoint hasn't been synced yet)
|
|
2375
2634
|
5. **Use \`select\`** to reshape API responses at the hook level, not in components
|
|
2376
2635
|
6. **Use \`enabled\`** for conditional queries (e.g. waiting for an ID from URL params)
|
|
2377
2636
|
7. **Spread generated options first** (\`...postsListOptions()\`), then add overrides \u2014 this preserves the generated \`queryKey\` and \`queryFn\`
|
|
@@ -3157,62 +3416,108 @@ var chakraUiAuthSkill = {
|
|
|
3157
3416
|
return `## Custom Auth System \u2014 Authentication Context & Hooks
|
|
3158
3417
|
|
|
3159
3418
|
> **RULE: Use the local AuthProvider and useAuth hook for all auth functionality.**
|
|
3160
|
-
> Auth components
|
|
3419
|
+
> Auth components and pages are custom implementations in \`src/features/auth/\`.
|
|
3420
|
+
|
|
3421
|
+
### Imports
|
|
3161
3422
|
|
|
3162
3423
|
\`\`\`tsx
|
|
3163
|
-
import { AuthProvider } from '@/features/auth/context'
|
|
3164
3424
|
import { useAuth } from '@/features/auth/hooks/use-auth'
|
|
3165
|
-
import type { User
|
|
3425
|
+
import type { User } from '@/features/auth/types'
|
|
3166
3426
|
\`\`\`
|
|
3167
3427
|
|
|
3168
|
-
###
|
|
3428
|
+
### useAuth() Hook
|
|
3169
3429
|
|
|
3170
|
-
|
|
3171
|
-
- Place at the app root, inside \`ChakraProvider\`.
|
|
3172
|
-
- Props: \`config?: { adapter?: AuthAdapter }\`
|
|
3430
|
+
Returns auth state and actions. Use this everywhere \u2014 never manage auth state manually.
|
|
3173
3431
|
|
|
3174
|
-
|
|
3432
|
+
\`\`\`tsx
|
|
3433
|
+
const { user, isAuthenticated, loading, error, signInWithEmail, signOut } = useAuth()
|
|
3434
|
+
\`\`\`
|
|
3175
3435
|
|
|
3436
|
+
| Field | Type | Description |
|
|
3437
|
+
|-------|------|-------------|
|
|
3438
|
+
| \`user\` | \`User \\| null\` | Current authenticated user |
|
|
3439
|
+
| \`isAuthenticated\` | \`boolean\` | Whether the user is logged in |
|
|
3440
|
+
| \`loading\` | \`boolean\` | Auth state is being resolved |
|
|
3441
|
+
| \`error\` | \`string \\| null\` | Last auth error message |
|
|
3442
|
+
| \`signInWithEmail\` | \`(email, password) => Promise\` | Log in |
|
|
3443
|
+
| \`signUpWithEmail\` | \`(email, password, displayName?) => Promise\` | Register |
|
|
3444
|
+
| \`signOut\` | \`() => Promise\` | Log out |
|
|
3445
|
+
| \`sendPasswordResetEmail\` | \`(email) => Promise\` | Request password reset |
|
|
3446
|
+
| \`confirmPasswordReset\` | \`(code, newPassword) => Promise\` | Set new password |
|
|
3447
|
+
|
|
3448
|
+
### Usage Examples
|
|
3449
|
+
|
|
3450
|
+
**Conditionally render based on auth:**
|
|
3176
3451
|
\`\`\`tsx
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3452
|
+
import { useAuth } from '@/features/auth/hooks/use-auth'
|
|
3453
|
+
|
|
3454
|
+
function UserGreeting() {
|
|
3455
|
+
const { user, isAuthenticated } = useAuth()
|
|
3456
|
+
|
|
3457
|
+
if (!isAuthenticated) return <Text>Please log in.</Text>
|
|
3458
|
+
return <Text>Welcome, {user?.email}</Text>
|
|
3182
3459
|
}
|
|
3460
|
+
\`\`\`
|
|
3183
3461
|
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3462
|
+
**Protected action:**
|
|
3463
|
+
\`\`\`tsx
|
|
3464
|
+
function LogoutButton() {
|
|
3465
|
+
const { signOut } = useAuth()
|
|
3466
|
+
|
|
3467
|
+
return <Button onClick={signOut} variant="ghost">Sign out</Button>
|
|
3189
3468
|
}
|
|
3190
3469
|
\`\`\`
|
|
3191
3470
|
|
|
3192
|
-
|
|
3471
|
+
**Login form integration:**
|
|
3472
|
+
\`\`\`tsx
|
|
3473
|
+
import { useAuth } from '@/features/auth/hooks/use-auth'
|
|
3193
3474
|
|
|
3194
|
-
|
|
3195
|
-
|
|
3475
|
+
function useLoginForm() {
|
|
3476
|
+
const { signInWithEmail, error } = useAuth()
|
|
3477
|
+
const navigate = useNavigate()
|
|
3196
3478
|
|
|
3197
|
-
|
|
3479
|
+
const onSubmit = async (data: { email: string; password: string }) => {
|
|
3480
|
+
await signInWithEmail(data.email, data.password)
|
|
3481
|
+
navigate(Path.Dashboard)
|
|
3482
|
+
}
|
|
3198
3483
|
|
|
3199
|
-
|
|
3484
|
+
return { onSubmit, error }
|
|
3485
|
+
}
|
|
3486
|
+
\`\`\`
|
|
3200
3487
|
|
|
3201
|
-
|
|
3202
|
-
- \`RegisterPage\` \u2014 Registration form with email, password, and display name
|
|
3203
|
-
- \`ForgotPasswordPage\` \u2014 Password reset email request
|
|
3204
|
-
- \`ResetPasswordPage\` \u2014 Set new password form
|
|
3488
|
+
### Auth Provider Setup
|
|
3205
3489
|
|
|
3206
|
-
|
|
3490
|
+
\`AuthProvider\` wraps the app (inside \`ChakraProvider\`) and manages token storage and session lifecycle:
|
|
3207
3491
|
|
|
3208
|
-
|
|
3492
|
+
\`\`\`tsx
|
|
3493
|
+
// app.tsx
|
|
3494
|
+
import { AuthProvider } from '@/features/auth/context'
|
|
3209
3495
|
|
|
3210
|
-
|
|
3496
|
+
function App() {
|
|
3497
|
+
return (
|
|
3498
|
+
<ChakraProvider>
|
|
3499
|
+
<AuthProvider>
|
|
3500
|
+
<RouterProvider router={router} />
|
|
3501
|
+
</AuthProvider>
|
|
3502
|
+
</ChakraProvider>
|
|
3503
|
+
)
|
|
3504
|
+
}
|
|
3505
|
+
\`\`\`
|
|
3506
|
+
|
|
3507
|
+
### Auth Pages
|
|
3508
|
+
|
|
3509
|
+
Pre-built pages in \`src/features/auth/pages/\`:
|
|
3510
|
+
- \`LoginPage\` \u2014 email/password login with validation
|
|
3511
|
+
- \`RegisterPage\` \u2014 registration with email, password, display name
|
|
3512
|
+
- \`ForgotPasswordPage\` \u2014 password reset email request
|
|
3513
|
+
- \`ResetPasswordPage\` \u2014 set new password form
|
|
3514
|
+
|
|
3515
|
+
All use Chakra UI form components with React Hook Form + Zod validation.
|
|
3211
3516
|
|
|
3212
3517
|
### Rules
|
|
3213
|
-
- NEVER manage auth state manually
|
|
3214
|
-
-
|
|
3215
|
-
-
|
|
3518
|
+
- NEVER manage auth state manually \u2014 always use \`useAuth()\`
|
|
3519
|
+
- Use the \`Path\` enum for auth route paths (\`Path.Login\`, \`Path.Register\`, etc.)
|
|
3520
|
+
- Auth adapter (Django JWT) lives in \`src/features/auth/adapter.ts\`
|
|
3216
3521
|
`;
|
|
3217
3522
|
}
|
|
3218
3523
|
};
|
|
@@ -3222,20 +3527,16 @@ init_esm_shims();
|
|
|
3222
3527
|
var blacksmithHooksSkill = {
|
|
3223
3528
|
id: "blacksmith-hooks",
|
|
3224
3529
|
name: "Custom Hooks & Chakra UI Hooks",
|
|
3225
|
-
description: "Custom React hooks (
|
|
3530
|
+
description: "Custom React hooks (scaffolded implementations) and Chakra UI built-in hooks for state, UI, layout, and responsiveness.",
|
|
3226
3531
|
render(_ctx) {
|
|
3227
3532
|
return `## Custom Hooks & Chakra UI Hooks
|
|
3228
3533
|
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
> **RULE: Use Chakra UI hooks when available, and local custom hooks for additional utilities.**
|
|
3232
|
-
> Before creating a new hook, check if one already exists below.
|
|
3534
|
+
> **RULE: Use Chakra UI hooks when available, and project custom hooks for additional utilities.**
|
|
3535
|
+
> Before creating a new hook, check if one already exists in \`src/shared/hooks/\` or in Chakra UI.
|
|
3233
3536
|
|
|
3234
3537
|
### Chakra UI Built-in Hooks
|
|
3235
3538
|
|
|
3236
|
-
|
|
3237
|
-
import { useColorMode, useDisclosure, useBreakpointValue, useMediaQuery, useToast } from '@chakra-ui/react'
|
|
3238
|
-
\`\`\`
|
|
3539
|
+
These are always available from \`@chakra-ui/react\`:
|
|
3239
3540
|
|
|
3240
3541
|
| Hook | Description |
|
|
3241
3542
|
|------|-------------|
|
|
@@ -3248,42 +3549,20 @@ import { useColorMode, useDisclosure, useBreakpointValue, useMediaQuery, useToas
|
|
|
3248
3549
|
| \`useClipboard\` | Copy text to clipboard with status feedback |
|
|
3249
3550
|
| \`useBoolean\` | Boolean state with \`on\`, \`off\`, \`toggle\` actions |
|
|
3250
3551
|
| \`useOutsideClick\` | Detect clicks outside a ref element |
|
|
3251
|
-
| \`useControllable\` | Controlled/uncontrolled component pattern helper |
|
|
3252
3552
|
| \`useMergeRefs\` | Merge multiple refs into one |
|
|
3253
3553
|
| \`useTheme\` | Access the current Chakra UI theme object |
|
|
3254
3554
|
|
|
3255
|
-
###
|
|
3555
|
+
### Scaffolded Project Hooks
|
|
3256
3556
|
|
|
3257
|
-
These are
|
|
3557
|
+
These hooks are scaffolded by Blacksmith and live in \`src/shared/hooks/\`. Check the directory to see which ones are available in your project:
|
|
3258
3558
|
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3559
|
+
| Hook | File | Description |
|
|
3560
|
+
|------|------|-------------|
|
|
3561
|
+
| \`useDebounce\` | \`use-debounce.ts\` | Debounce a value with configurable delay |
|
|
3562
|
+
| \`useApiQuery\` | \`use-api-query.ts\` | Wrapper around \`useQuery\` with DRF error parsing and smart retry |
|
|
3563
|
+
| \`useApiMutation\` | \`use-api-mutation.ts\` | Wrapper around \`useMutation\` with DRF error parsing and cache invalidation |
|
|
3263
3564
|
|
|
3264
|
-
|
|
3265
|
-
|------|-------------|
|
|
3266
|
-
| \`useDebounce\` | Debounce a value with configurable delay |
|
|
3267
|
-
| \`useDebouncedCallback\` | Debounce a callback function |
|
|
3268
|
-
| \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
|
|
3269
|
-
| \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
|
|
3270
|
-
| \`usePrevious\` | Track the previous value of a variable |
|
|
3271
|
-
| \`useInterval\` | setInterval wrapper with pause support |
|
|
3272
|
-
| \`useTimeout\` | setTimeout wrapper with manual clear |
|
|
3273
|
-
| \`useEventListener\` | Attach event listeners to window or elements |
|
|
3274
|
-
| \`useElementSize\` | Track element width/height via ResizeObserver |
|
|
3275
|
-
| \`useHover\` | Track mouse hover state |
|
|
3276
|
-
| \`useKeyPress\` | Listen for a specific key press |
|
|
3277
|
-
| \`useScrollPosition\` | Track window scroll position |
|
|
3278
|
-
| \`useScrollLock\` | Lock/unlock body scroll |
|
|
3279
|
-
| \`useOnline\` | Track network connectivity |
|
|
3280
|
-
| \`useWindowSize\` | Track window dimensions |
|
|
3281
|
-
| \`usePageVisibility\` | Detect page visibility state |
|
|
3282
|
-
| \`useIsMounted\` | Check if component is currently mounted |
|
|
3283
|
-
| \`useIsFirstRender\` | Check if this is the first render |
|
|
3284
|
-
| \`useUpdateEffect\` | useEffect that skips the initial render |
|
|
3285
|
-
| \`useIntersectionObserver\` | Observe element intersection with viewport |
|
|
3286
|
-
| \`useInfiniteScroll\` | Infinite scroll with threshold detection |
|
|
3565
|
+
> **Note:** Additional hooks (e.g. \`useLocalStorage\`, \`usePrevious\`, \`useInterval\`) can be created as needed in \`src/shared/hooks/\`. Always check if one exists before writing a new one.
|
|
3287
3566
|
|
|
3288
3567
|
### Common Patterns
|
|
3289
3568
|
|
|
@@ -3311,58 +3590,28 @@ function MyComponent() {
|
|
|
3311
3590
|
}
|
|
3312
3591
|
\`\`\`
|
|
3313
3592
|
|
|
3314
|
-
**Debounced search
|
|
3593
|
+
**Debounced search:**
|
|
3315
3594
|
\`\`\`tsx
|
|
3316
3595
|
import { useDebounce } from '@/shared/hooks/use-debounce'
|
|
3317
3596
|
import { Input } from '@chakra-ui/react'
|
|
3318
3597
|
|
|
3319
|
-
function
|
|
3598
|
+
function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
|
|
3320
3599
|
const [query, setQuery] = useState('')
|
|
3321
3600
|
const debouncedQuery = useDebounce(query, 300)
|
|
3322
|
-
// Use debouncedQuery for API calls or filtering
|
|
3323
3601
|
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
3328
|
-
placeholder="Search..."
|
|
3329
|
-
/>
|
|
3330
|
-
)
|
|
3602
|
+
useEffect(() => { onSearch(debouncedQuery) }, [debouncedQuery, onSearch])
|
|
3603
|
+
|
|
3604
|
+
return <Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
|
|
3331
3605
|
}
|
|
3332
3606
|
\`\`\`
|
|
3333
3607
|
|
|
3334
|
-
**Responsive layout
|
|
3608
|
+
**Responsive layout:**
|
|
3335
3609
|
\`\`\`tsx
|
|
3336
3610
|
import { useBreakpointValue } from '@chakra-ui/react'
|
|
3337
3611
|
|
|
3338
3612
|
function Layout({ children }) {
|
|
3339
3613
|
const columns = useBreakpointValue({ base: 1, md: 2, lg: 3 })
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
|
|
3343
|
-
}
|
|
3344
|
-
\`\`\`
|
|
3345
|
-
|
|
3346
|
-
**Color mode toggle:**
|
|
3347
|
-
\`\`\`tsx
|
|
3348
|
-
import { useColorMode, useColorModeValue, IconButton } from '@chakra-ui/react'
|
|
3349
|
-
import { Sun, Moon } from 'lucide-react'
|
|
3350
|
-
|
|
3351
|
-
function ColorModeToggle() {
|
|
3352
|
-
const { colorMode, toggleColorMode } = useColorMode()
|
|
3353
|
-
const icon = colorMode === 'light' ? <Moon size={16} /> : <Sun size={16} />
|
|
3354
|
-
|
|
3355
|
-
return <IconButton aria-label="Toggle color mode" icon={icon} onClick={toggleColorMode} variant="ghost" />
|
|
3356
|
-
}
|
|
3357
|
-
\`\`\`
|
|
3358
|
-
|
|
3359
|
-
**Persisted state with local storage:**
|
|
3360
|
-
\`\`\`tsx
|
|
3361
|
-
import { useLocalStorage } from '@/shared/hooks/use-local-storage'
|
|
3362
|
-
|
|
3363
|
-
function Editor() {
|
|
3364
|
-
const [saved, setSaved] = useLocalStorage('draft', '')
|
|
3365
|
-
// saved persists across page refreshes
|
|
3614
|
+
return <SimpleGrid columns={columns} spacing={6}>{children}</SimpleGrid>
|
|
3366
3615
|
}
|
|
3367
3616
|
\`\`\`
|
|
3368
3617
|
`;
|
|
@@ -3385,12 +3634,12 @@ Blacksmith is the CLI that scaffolded and manages this project. It lives outside
|
|
|
3385
3634
|
| Command | Description |
|
|
3386
3635
|
|---|---|
|
|
3387
3636
|
| \`blacksmith init [name]\` | Create a new project (interactive prompts if no flags) |
|
|
3388
|
-
| \`blacksmith dev\` | Start
|
|
3389
|
-
| \`blacksmith sync\` | Regenerate frontend API client from Django OpenAPI schema |
|
|
3390
|
-
| \`blacksmith make:resource <Name>\` | Scaffold a
|
|
3391
|
-
| \`blacksmith build\` | Production build
|
|
3637
|
+
| \`blacksmith dev\` | Start development server(s) |
|
|
3638
|
+
| \`blacksmith sync\` | Regenerate frontend API client from Django OpenAPI schema (fullstack only) |
|
|
3639
|
+
| \`blacksmith make:resource <Name>\` | Scaffold a resource (backend, frontend, or both depending on project type) |
|
|
3640
|
+
| \`blacksmith build\` | Production build |
|
|
3392
3641
|
| \`blacksmith eject\` | Remove Blacksmith dependency, keep a clean project |
|
|
3393
|
-
| \`blacksmith setup:ai\` | Generate CLAUDE.md with AI development skills |
|
|
3642
|
+
| \`blacksmith setup:ai\` | Generate/regenerate CLAUDE.md with AI development skills |
|
|
3394
3643
|
| \`blacksmith skills\` | List all available AI development skills |
|
|
3395
3644
|
|
|
3396
3645
|
### Configuration
|
|
@@ -3401,47 +3650,46 @@ Project settings are stored in \`blacksmith.config.json\` at the project root:
|
|
|
3401
3650
|
{
|
|
3402
3651
|
"name": "my-app",
|
|
3403
3652
|
"version": "0.1.0",
|
|
3653
|
+
"type": "fullstack",
|
|
3404
3654
|
"backend": { "port": 8000 },
|
|
3405
3655
|
"frontend": { "port": 5173 }
|
|
3406
3656
|
}
|
|
3407
3657
|
\`\`\`
|
|
3408
3658
|
|
|
3409
|
-
-
|
|
3659
|
+
- **\`type\`** \u2014 \`"fullstack"\`, \`"backend"\`, or \`"frontend"\`. Determines which commands and steps are available
|
|
3660
|
+
- **\`backend\`** \u2014 present for fullstack and backend projects. Absent for frontend-only
|
|
3661
|
+
- **\`frontend\`** \u2014 present for fullstack and frontend projects. Absent for backend-only
|
|
3662
|
+
- **Ports** are read by \`blacksmith dev\` \u2014 change them here, not in code
|
|
3410
3663
|
- The CLI finds the project root by walking up directories looking for this file
|
|
3411
3664
|
|
|
3412
3665
|
### How \`blacksmith dev\` Works
|
|
3413
3666
|
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3667
|
+
Depends on project type:
|
|
3668
|
+
- **Fullstack**: Runs three concurrent processes \u2014 Django, Vite, and an OpenAPI file watcher (auto-syncs types on \`.py\` changes)
|
|
3669
|
+
- **Backend**: Runs Django only
|
|
3670
|
+
- **Frontend**: Runs Vite only
|
|
3418
3671
|
|
|
3419
|
-
All
|
|
3672
|
+
All processes are managed by \`concurrently\` and stop together on Ctrl+C.
|
|
3420
3673
|
|
|
3421
3674
|
### How \`blacksmith make:resource\` Works
|
|
3422
3675
|
|
|
3423
|
-
Given a PascalCase name (e.g. \`BlogPost\`), it
|
|
3676
|
+
Given a PascalCase name (e.g. \`BlogPost\`), it scaffolds based on project type:
|
|
3424
3677
|
|
|
3425
|
-
**Backend:**
|
|
3426
|
-
- \`
|
|
3427
|
-
- \`backend/apps/blog_posts/serializers.py\` \u2014 DRF ModelSerializer
|
|
3428
|
-
- \`backend/apps/blog_posts/views.py\` \u2014 DRF ModelViewSet with drf-spectacular schemas
|
|
3429
|
-
- \`backend/apps/blog_posts/urls.py\` \u2014 DefaultRouter registration
|
|
3430
|
-
- \`backend/apps/blog_posts/admin.py\` \u2014 Admin registration
|
|
3678
|
+
**Backend (fullstack and backend projects):**
|
|
3679
|
+
- \`apps/blog_posts/\` \u2014 model, serializer, viewset, urls, admin, tests
|
|
3431
3680
|
- Wires the app into \`INSTALLED_APPS\` and \`config/urls.py\`
|
|
3432
3681
|
- Runs \`makemigrations\` and \`migrate\`
|
|
3433
3682
|
|
|
3434
|
-
**Frontend:**
|
|
3435
|
-
- \`
|
|
3436
|
-
- \`
|
|
3437
|
-
- Registers route path in \`
|
|
3438
|
-
- Registers routes in \`frontend/src/router/routes.tsx\`
|
|
3683
|
+
**Frontend (fullstack and frontend projects):**
|
|
3684
|
+
- \`src/api/hooks/blog-posts/\` \u2014 query and mutation hooks
|
|
3685
|
+
- \`src/pages/blog-posts/\` \u2014 list and detail pages
|
|
3686
|
+
- Registers route path in \`src/router/paths.ts\` and routes in \`src/router/routes.tsx\`
|
|
3439
3687
|
|
|
3440
|
-
|
|
3688
|
+
**Fullstack only:** Also runs \`blacksmith sync\` to generate the TypeScript API client.
|
|
3441
3689
|
|
|
3442
|
-
### How \`blacksmith sync\` Works
|
|
3690
|
+
### How \`blacksmith sync\` Works (Fullstack Only)
|
|
3443
3691
|
|
|
3444
|
-
1.
|
|
3692
|
+
1. Generates the OpenAPI schema offline using \`manage.py spectacular\`
|
|
3445
3693
|
2. Runs \`openapi-ts\` to generate TypeScript types, Zod schemas, SDK functions, and TanStack Query hooks
|
|
3446
3694
|
3. Output goes to \`frontend/src/api/generated/\` \u2014 never edit these files manually
|
|
3447
3695
|
|
|
@@ -3454,11 +3702,12 @@ Then runs \`blacksmith sync\` to generate the TypeScript API client.
|
|
|
3454
3702
|
blacksmith init
|
|
3455
3703
|
|
|
3456
3704
|
# Skip prompts with flags
|
|
3457
|
-
blacksmith init my-app -b 9000 -f 3000 --ai
|
|
3705
|
+
blacksmith init my-app --type fullstack -b 9000 -f 3000 --ai
|
|
3458
3706
|
\`\`\`
|
|
3459
3707
|
|
|
3460
3708
|
| Flag | Description |
|
|
3461
3709
|
|---|---|
|
|
3710
|
+
| \`--type <type>\` | Project type: fullstack, backend, or frontend (default: fullstack) |
|
|
3462
3711
|
| \`-b, --backend-port <port>\` | Django port (default: 8000) |
|
|
3463
3712
|
| \`-f, --frontend-port <port>\` | Vite port (default: 5173) |
|
|
3464
3713
|
| \`--ai\` | Generate CLAUDE.md with project skills |
|
|
@@ -3472,252 +3721,91 @@ init_esm_shims();
|
|
|
3472
3721
|
var uiDesignSkill = {
|
|
3473
3722
|
id: "ui-design",
|
|
3474
3723
|
name: "UI/UX Design System",
|
|
3475
|
-
description: "
|
|
3724
|
+
description: "Design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the Chakra UI design language.",
|
|
3476
3725
|
render(_ctx) {
|
|
3477
3726
|
return `## UI/UX Design System \u2014 Modern Flat Design
|
|
3478
3727
|
|
|
3479
3728
|
> **Design philosophy: Clean, flat, content-first.**
|
|
3480
|
-
>
|
|
3729
|
+
> Follow a design language similar to Linear, Vercel, and Stripe \u2014 minimal chrome, generous whitespace, subtle depth, and purposeful motion.
|
|
3481
3730
|
|
|
3482
3731
|
### Core Principles
|
|
3483
3732
|
|
|
3484
|
-
1. **Flat over skeuomorphic** \u2014 No gradients on surfaces, no heavy drop shadows, no bevels.
|
|
3485
|
-
2. **Content over decoration** \u2014 UI
|
|
3486
|
-
3. **Whitespace is a feature** \u2014 Generous padding and margins create hierarchy
|
|
3487
|
-
4. **Consistency over creativity** \u2014 Every page should feel like part of the same app.
|
|
3488
|
-
5. **Progressive disclosure** \u2014 Show only what's needed
|
|
3733
|
+
1. **Flat over skeuomorphic** \u2014 No gradients on surfaces, no heavy drop shadows, no bevels. Solid colors, subtle borders, and \`shadow="sm"\` / \`shadow="md"\` only where elevation is meaningful (cards, dropdowns, modals)
|
|
3734
|
+
2. **Content over decoration** \u2014 UI presents content, not decoration. If a section looks empty, the content is the problem \u2014 not the lack of decorative elements
|
|
3735
|
+
3. **Whitespace is a feature** \u2014 Generous padding and margins create hierarchy. When in doubt, add more space
|
|
3736
|
+
4. **Consistency over creativity** \u2014 Every page should feel like part of the same app. Same spacing, same patterns, same interactions
|
|
3737
|
+
5. **Progressive disclosure** \u2014 Show only what's needed. Use tabs, dialogs, and drill-down navigation to manage complexity
|
|
3489
3738
|
|
|
3490
|
-
### Spacing
|
|
3739
|
+
### Spacing
|
|
3491
3740
|
|
|
3492
|
-
Use Chakra UI's spacing scale consistently
|
|
3741
|
+
Use Chakra UI's numeric spacing scale consistently (1 = 4px):
|
|
3493
3742
|
|
|
3494
|
-
| Scale |
|
|
3495
|
-
|
|
3496
|
-
| \`1\`\u2013\`2\` |
|
|
3497
|
-
| \`3\`\u2013\`4\` |
|
|
3498
|
-
| \`5\`\u2013\`6\` |
|
|
3499
|
-
| \`8\` |
|
|
3500
|
-
| \`10\`\u2013\`12\` |
|
|
3501
|
-
| \`16\`\u2013\`20\` | 64\u201380px | Page-level vertical padding (hero, landing sections) |
|
|
3743
|
+
| Scale | Use for |
|
|
3744
|
+
|-------|---------|
|
|
3745
|
+
| \`1\`\u2013\`2\` | Icon-to-text gaps, tight badge padding |
|
|
3746
|
+
| \`3\`\u2013\`4\` | Inner component padding, gap between related items |
|
|
3747
|
+
| \`5\`\u2013\`6\` | Card padding, section inner spacing |
|
|
3748
|
+
| \`8\` | Gap between sections within a page |
|
|
3749
|
+
| \`10\`\u2013\`12\` | Gap between major page sections |
|
|
3502
3750
|
|
|
3503
|
-
|
|
3504
|
-
-
|
|
3505
|
-
-
|
|
3506
|
-
- Page content padding: use \`Container\` which handles responsive horizontal padding
|
|
3507
|
-
- Card body padding: \`p={6}\` standard, \`p={4}\` for compact cards
|
|
3508
|
-
- Never mix spacing approaches in the same context \u2014 pick spacing OR margin, not both
|
|
3751
|
+
- Use \`VStack spacing={...}\` / \`HStack spacing={...}\` for sibling spacing \u2014 not margin on individual items
|
|
3752
|
+
- Page content: use \`Container\` for responsive horizontal padding
|
|
3753
|
+
- Card body: \`p={6}\` standard, \`p={4}\` for compact cards
|
|
3509
3754
|
|
|
3510
3755
|
### Typography
|
|
3511
3756
|
|
|
3512
|
-
Use \`Heading\` and \`Text\` components from \`@chakra-ui/react\`. Do NOT style raw HTML headings.
|
|
3513
|
-
|
|
3514
|
-
**Hierarchy:**
|
|
3515
3757
|
| Level | Component | Use for |
|
|
3516
3758
|
|-------|-----------|---------|
|
|
3517
|
-
| Page title | \`<Heading as="h1" size="2xl">\` | One per page
|
|
3518
|
-
| Section title | \`<Heading as="h2" size="xl">\` | Major sections
|
|
3759
|
+
| Page title | \`<Heading as="h1" size="2xl">\` | One per page |
|
|
3760
|
+
| Section title | \`<Heading as="h2" size="xl">\` | Major sections |
|
|
3519
3761
|
| Sub-section | \`<Heading as="h3" size="lg">\` | Groups within a section |
|
|
3520
3762
|
| Card title | \`<Heading as="h4" size="md">\` | Card headings |
|
|
3521
3763
|
| Body | \`<Text>\` | Paragraphs, descriptions |
|
|
3522
|
-
| Caption
|
|
3523
|
-
| Overline | \`<Text fontSize="xs" fontWeight="medium" textTransform="uppercase" letterSpacing="wide">\` | Category labels, section overlines |
|
|
3764
|
+
| Caption | \`<Text fontSize="sm" color="gray.500">\` | Metadata, timestamps |
|
|
3524
3765
|
|
|
3525
|
-
|
|
3526
|
-
-
|
|
3527
|
-
-
|
|
3528
|
-
- Body text: \`fontSize="sm"\` (14px) for dense UIs (tables, sidebars), \`fontSize="md"\` (16px) for reading content
|
|
3529
|
-
- Max reading width: \`maxW="prose"\` (~65ch) for long-form text. Never let paragraphs stretch full-width
|
|
3530
|
-
- Use \`color="gray.500"\` or theme-aware \`useColorModeValue('gray.600', 'gray.400')\` for secondary text
|
|
3531
|
-
- Font weight: \`fontWeight="medium"\` (500) for labels, \`fontWeight="semibold"\` (600) for headings, \`fontWeight="bold"\` (700) sparingly
|
|
3766
|
+
- One \`h1\` per page. Never skip heading levels
|
|
3767
|
+
- Max reading width: \`maxW="prose"\` for long-form text
|
|
3768
|
+
- Use \`useColorModeValue('gray.600', 'gray.400')\` for secondary text
|
|
3532
3769
|
|
|
3533
3770
|
### Color
|
|
3534
3771
|
|
|
3535
|
-
Use Chakra UI
|
|
3772
|
+
Use Chakra UI theme tokens \u2014 never hardcoded hex/rgb values:
|
|
3536
3773
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
| \`
|
|
3541
|
-
| \`
|
|
3542
|
-
| \`
|
|
3543
|
-
| \`green\` (colorScheme) | Success states |
|
|
3774
|
+
| colorScheme | Usage |
|
|
3775
|
+
|-------------|-------|
|
|
3776
|
+
| \`blue\` | Primary actions, active states |
|
|
3777
|
+
| \`gray\` | Secondary actions, subtle backgrounds |
|
|
3778
|
+
| \`red\` | Delete, error, danger |
|
|
3779
|
+
| \`green\` | Success states |
|
|
3544
3780
|
| \`yellow\` / \`orange\` | Warning states |
|
|
3545
3781
|
|
|
3546
|
-
|
|
3547
|
-
-
|
|
3548
|
-
- Status
|
|
3549
|
-
- Maximum 2\u20133 colors visible at any time (primary + foreground + muted). Colorful UIs feel noisy.
|
|
3550
|
-
- Every UI must render correctly in both light and dark mode. See the Dark Mode section below for the full rules.
|
|
3551
|
-
|
|
3552
|
-
### Layout Patterns
|
|
3553
|
-
|
|
3554
|
-
**Page layout:**
|
|
3555
|
-
\`\`\`tsx
|
|
3556
|
-
<Box as="main">
|
|
3557
|
-
<Container maxW="container.xl">
|
|
3558
|
-
<VStack spacing={8} align="stretch">
|
|
3559
|
-
{/* Page header */}
|
|
3560
|
-
<Flex align="center" justify="space-between">
|
|
3561
|
-
<VStack spacing={1} align="start">
|
|
3562
|
-
<Heading as="h1" size="2xl">Page Title</Heading>
|
|
3563
|
-
<Text color="gray.500">Brief description of this page</Text>
|
|
3564
|
-
</VStack>
|
|
3565
|
-
<Button colorScheme="blue">Primary Action</Button>
|
|
3566
|
-
</Flex>
|
|
3567
|
-
|
|
3568
|
-
{/* Page content sections */}
|
|
3569
|
-
<VStack spacing={6} align="stretch">
|
|
3570
|
-
{/* ... */}
|
|
3571
|
-
</VStack>
|
|
3572
|
-
</VStack>
|
|
3573
|
-
</Container>
|
|
3574
|
-
</Box>
|
|
3575
|
-
\`\`\`
|
|
3576
|
-
|
|
3577
|
-
**Card-based content:**
|
|
3578
|
-
\`\`\`tsx
|
|
3579
|
-
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
|
|
3580
|
-
{items.map((item) => (
|
|
3581
|
-
<Card key={item.id}>
|
|
3582
|
-
<CardHeader>
|
|
3583
|
-
<Heading size="md">{item.title}</Heading>
|
|
3584
|
-
<Text fontSize="sm" color="gray.500">{item.description}</Text>
|
|
3585
|
-
</CardHeader>
|
|
3586
|
-
<CardBody>
|
|
3587
|
-
{/* Content */}
|
|
3588
|
-
</CardBody>
|
|
3589
|
-
</Card>
|
|
3590
|
-
))}
|
|
3591
|
-
</SimpleGrid>
|
|
3592
|
-
\`\`\`
|
|
3593
|
-
|
|
3594
|
-
**Sidebar + main content:**
|
|
3595
|
-
\`\`\`tsx
|
|
3596
|
-
<Flex minH="100vh">
|
|
3597
|
-
<Box as="aside" w="250px">{/* Nav items */}</Box>
|
|
3598
|
-
<Box as="main" flex={1}>
|
|
3599
|
-
<Container maxW="container.xl">{/* Page content */}</Container>
|
|
3600
|
-
</Box>
|
|
3601
|
-
</Flex>
|
|
3602
|
-
\`\`\`
|
|
3603
|
-
|
|
3604
|
-
**Section with centered content (landing pages):**
|
|
3605
|
-
\`\`\`tsx
|
|
3606
|
-
<Box as="section" py={{ base: 16, sm: 20 }}>
|
|
3607
|
-
<Container maxW="container.xl">
|
|
3608
|
-
<VStack spacing={4} align="center" textAlign="center">
|
|
3609
|
-
<Heading as="h2" size="xl">Section Title</Heading>
|
|
3610
|
-
<Text color="gray.500" maxW="2xl">
|
|
3611
|
-
A concise description that explains the value proposition.
|
|
3612
|
-
</Text>
|
|
3613
|
-
</VStack>
|
|
3614
|
-
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} mt={12}>
|
|
3615
|
-
{/* Feature cards or content */}
|
|
3616
|
-
</SimpleGrid>
|
|
3617
|
-
</Container>
|
|
3618
|
-
</Box>
|
|
3619
|
-
\`\`\`
|
|
3620
|
-
|
|
3621
|
-
### Component Patterns
|
|
3622
|
-
|
|
3623
|
-
**Empty states:**
|
|
3624
|
-
\`\`\`tsx
|
|
3625
|
-
// GOOD \u2014 uses a well-structured empty state with Chakra components
|
|
3626
|
-
<VStack spacing={4} align="center" py={12} textAlign="center">
|
|
3627
|
-
<Icon as={Inbox} boxSize={12} color="gray.400" />
|
|
3628
|
-
<Heading size="md">No messages yet</Heading>
|
|
3629
|
-
<Text color="gray.500">Messages from your team will appear here.</Text>
|
|
3630
|
-
<Button colorScheme="blue">Send a message</Button>
|
|
3631
|
-
</VStack>
|
|
3632
|
-
|
|
3633
|
-
// BAD \u2014 hand-rolled empty state with raw HTML
|
|
3634
|
-
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
3635
|
-
<Inbox className="h-12 w-12 text-gray-400 mb-4" />
|
|
3636
|
-
<h3 className="text-lg font-medium">No messages yet</h3>
|
|
3637
|
-
<p className="text-gray-500 mt-1">Messages from your team will appear here.</p>
|
|
3638
|
-
</div>
|
|
3639
|
-
\`\`\`
|
|
3640
|
-
|
|
3641
|
-
**Stats/metrics:**
|
|
3642
|
-
\`\`\`tsx
|
|
3643
|
-
// GOOD \u2014 uses Chakra Stat component
|
|
3644
|
-
<SimpleGrid columns={{ base: 1, sm: 2, lg: 4 }} spacing={4}>
|
|
3645
|
-
<Stat>
|
|
3646
|
-
<StatLabel>Total Users</StatLabel>
|
|
3647
|
-
<StatNumber>2,847</StatNumber>
|
|
3648
|
-
<StatHelpText><StatArrow type="increase" />12%</StatHelpText>
|
|
3649
|
-
</Stat>
|
|
3650
|
-
<Stat>
|
|
3651
|
-
<StatLabel>Revenue</StatLabel>
|
|
3652
|
-
<StatNumber>$48,290</StatNumber>
|
|
3653
|
-
<StatHelpText><StatArrow type="increase" />8%</StatHelpText>
|
|
3654
|
-
</Stat>
|
|
3655
|
-
</SimpleGrid>
|
|
3656
|
-
\`\`\`
|
|
3657
|
-
|
|
3658
|
-
**Loading states:**
|
|
3659
|
-
\`\`\`tsx
|
|
3660
|
-
// GOOD \u2014 Skeleton matches the layout structure
|
|
3661
|
-
<VStack spacing={4} align="stretch">
|
|
3662
|
-
<Skeleton h="32px" w="200px" />
|
|
3663
|
-
<SkeletonText noOfLines={2} />
|
|
3664
|
-
<SimpleGrid columns={3} spacing={4}>
|
|
3665
|
-
{Array.from({ length: 3 }).map((_, i) => (
|
|
3666
|
-
<Skeleton key={i} h="128px" />
|
|
3667
|
-
))}
|
|
3668
|
-
</SimpleGrid>
|
|
3669
|
-
</VStack>
|
|
3670
|
-
|
|
3671
|
-
// BAD \u2014 generic spinner with no layout hint
|
|
3672
|
-
<div className="flex justify-center py-12">
|
|
3673
|
-
<div className="animate-spin h-8 w-8 border-2 border-blue-500 rounded-full" />
|
|
3674
|
-
</div>
|
|
3675
|
-
\`\`\`
|
|
3676
|
-
|
|
3677
|
-
### Dark Mode & Light Mode
|
|
3782
|
+
- Use \`useColorModeValue()\` for any color that must adapt between light and dark mode
|
|
3783
|
+
- Maximum 2\u20133 colors visible at once. Colorful UIs feel noisy
|
|
3784
|
+
- Status indicators: use \`Badge\` with \`colorScheme\` \u2014 don't hand-roll colored pills
|
|
3678
3785
|
|
|
3679
|
-
|
|
3786
|
+
### Dark Mode
|
|
3680
3787
|
|
|
3681
|
-
|
|
3788
|
+
> **Every screen and component MUST render correctly in both light and dark mode.**
|
|
3682
3789
|
|
|
3683
|
-
|
|
3684
|
-
-
|
|
3685
|
-
-
|
|
3686
|
-
- NEVER use \`bg="white"\` or \`bg="black"\`. Use \`bg={useColorModeValue('white', 'gray.800')}\` or Chakra semantic tokens.
|
|
3687
|
-
- All Chakra UI components automatically adapt to color mode \u2014 leverage this.
|
|
3790
|
+
- Use \`useColorModeValue()\` for any custom colors
|
|
3791
|
+
- NEVER hardcode \`bg="white"\` or \`bg="black"\`. Use \`bg={useColorModeValue('white', 'gray.800')}\`
|
|
3792
|
+
- All Chakra UI components automatically adapt \u2014 leverage this
|
|
3688
3793
|
|
|
3689
3794
|
### Interactions & Feedback
|
|
3690
3795
|
|
|
3691
|
-
- **
|
|
3692
|
-
- **
|
|
3693
|
-
- **
|
|
3694
|
-
- **
|
|
3695
|
-
- **Confirmation before destructive actions**: Always use \`AlertDialog\` for delete/remove actions. Never delete on single click
|
|
3796
|
+
- **Loading**: \`Skeleton\` for content areas, \`isLoading\` prop on buttons. Never leave the user without feedback
|
|
3797
|
+
- **Success/error**: \`useToast()\` for transient confirmations, \`Alert\` for persistent messages. Never \`window.alert()\`
|
|
3798
|
+
- **Destructive actions**: Always use \`AlertDialog\` for delete/remove. Never delete on single click
|
|
3799
|
+
- **Touch targets**: Minimum 44x44px for interactive elements on mobile
|
|
3696
3800
|
|
|
3697
3801
|
### Responsive Design
|
|
3698
3802
|
|
|
3699
|
-
- **Mobile-first**: Chakra
|
|
3700
|
-
- **
|
|
3701
|
-
- **
|
|
3702
|
-
-
|
|
3703
|
-
- **Touch targets**: Minimum 44x44px for interactive elements on mobile. Use \`Button size="lg"\` and adequate padding
|
|
3704
|
-
- **Stack direction**: Use \`Stack direction={{ base: 'column', md: 'row' }}\` for responsive stacking
|
|
3705
|
-
- **Container**: Always wrap page content in \`<Container>\` \u2014 it handles responsive horizontal padding
|
|
3803
|
+
- **Mobile-first**: Chakra responsive props use mobile-first breakpoints (\`base\`, \`sm\`, \`md\`, \`lg\`, \`xl\`)
|
|
3804
|
+
- **Responsive props**: \`columns={{ base: 1, md: 2, lg: 3 }}\`
|
|
3805
|
+
- **Stack direction**: \`Stack direction={{ base: 'column', md: 'row' }}\` for responsive stacking
|
|
3806
|
+
- Always wrap page content in \`<Container>\` for responsive horizontal padding
|
|
3706
3807
|
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
| Anti-pattern | What to do instead |
|
|
3710
|
-
|---|---|
|
|
3711
|
-
| Raw \`<div>\` with flex/grid classes | Use \`Flex\`, \`VStack\`, \`HStack\`, \`SimpleGrid\` |
|
|
3712
|
-
| Raw \`<h1>\`-\`<h6>\` tags | Use \`Heading\` with \`as\` and \`size\` props |
|
|
3713
|
-
| Raw \`<p>\` tags | Use \`Text\` |
|
|
3714
|
-
| Heavy box shadows | Use Chakra's built-in shadow prop: \`shadow="sm"\`, \`shadow="md"\` |
|
|
3715
|
-
| Gradient backgrounds on surfaces | Use solid backgrounds |
|
|
3716
|
-
| Custom scrollbar CSS hacks | Use Chakra's styling system |
|
|
3717
|
-
| Animated entrances (fade-in, slide-up) | Content should appear instantly. Only animate user-triggered changes |
|
|
3718
|
-
| Using \`<br />\` for spacing | Use \`VStack spacing={...}\` or Chakra spacing props |
|
|
3719
|
-
| Inline styles (\`style={{ ... }}\`) | Use Chakra style props (\`p\`, \`m\`, \`bg\`, \`color\`, etc.) |
|
|
3720
|
-
| Hardcoded color values | Use theme tokens and \`useColorModeValue()\` |
|
|
3808
|
+
For the full component reference, see the \`chakra-ui-react\` skill.
|
|
3721
3809
|
`;
|
|
3722
3810
|
}
|
|
3723
3811
|
};
|
|
@@ -4152,6 +4240,178 @@ class OrderService:
|
|
|
4152
4240
|
}
|
|
4153
4241
|
};
|
|
4154
4242
|
|
|
4243
|
+
// src/skills/frontend-modularization.ts
|
|
4244
|
+
init_esm_shims();
|
|
4245
|
+
var frontendModularizationSkill = {
|
|
4246
|
+
id: "frontend-modularization",
|
|
4247
|
+
name: "Frontend Modularization",
|
|
4248
|
+
description: "Component file structure, one-component-per-file rule, folder-based components, utility extraction, and barrel exports.",
|
|
4249
|
+
render(_ctx) {
|
|
4250
|
+
return `## Frontend Modularization
|
|
4251
|
+
|
|
4252
|
+
> **RULE: One component per file. Extract non-component functions to utils. Promote complex components to folders with barrel exports.**
|
|
4253
|
+
|
|
4254
|
+
### One Component Per File
|
|
4255
|
+
|
|
4256
|
+
Every \`.tsx\` file exports exactly one React component. No sibling components, no inline helpers that render JSX.
|
|
4257
|
+
|
|
4258
|
+
\`\`\`tsx
|
|
4259
|
+
// BAD \u2014 two components in one file
|
|
4260
|
+
export function UserCard({ user }: Props) { ... }
|
|
4261
|
+
export function UserCardSkeleton() { ... }
|
|
4262
|
+
|
|
4263
|
+
// GOOD \u2014 separate files
|
|
4264
|
+
// user-card.tsx
|
|
4265
|
+
export function UserCard({ user }: Props) { ... }
|
|
4266
|
+
|
|
4267
|
+
// user-card-skeleton.tsx
|
|
4268
|
+
export function UserCardSkeleton() { ... }
|
|
4269
|
+
\`\`\`
|
|
4270
|
+
|
|
4271
|
+
Small, tightly-coupled sub-components (e.g. a skeleton for a card) still get their own file \u2014 co-locate them in the same directory.
|
|
4272
|
+
|
|
4273
|
+
### Utility Functions Live in Utils
|
|
4274
|
+
|
|
4275
|
+
> **RULE: Reuse before writing. Before creating any helper function, search the codebase for an existing one. If a function can be used outside the file it lives in, it belongs in utils.**
|
|
4276
|
+
|
|
4277
|
+
Functions that are not components or hooks do not belong in component files. Extract them:
|
|
4278
|
+
|
|
4279
|
+
- **Page/feature-scoped utilities** \u2192 \`utils/\` folder next to the component
|
|
4280
|
+
- **App-wide utilities** \u2192 \`src/shared/utils/\`
|
|
4281
|
+
|
|
4282
|
+
If the same logic exists in two places, extract it into a shared utility immediately \u2014 never duplicate.
|
|
4283
|
+
|
|
4284
|
+
\`\`\`tsx
|
|
4285
|
+
// BAD \u2014 helper function sitting in the component file
|
|
4286
|
+
function formatCurrency(amount: number) {
|
|
4287
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
export function PriceTag({ amount }: { amount: number }) {
|
|
4291
|
+
return <Text>{formatCurrency(amount)}</Text>
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
// GOOD \u2014 utility extracted
|
|
4295
|
+
// utils/format.ts
|
|
4296
|
+
export function formatCurrency(amount: number) {
|
|
4297
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
// price-tag.tsx
|
|
4301
|
+
import { formatCurrency } from '../utils/format'
|
|
4302
|
+
|
|
4303
|
+
export function PriceTag({ amount }: { amount: number }) {
|
|
4304
|
+
return <Text>{formatCurrency(amount)}</Text>
|
|
4305
|
+
}
|
|
4306
|
+
\`\`\`
|
|
4307
|
+
|
|
4308
|
+
**Exception:** Tiny, one-line helpers used only in that file (e.g. a local type guard) can stay inline. If it grows beyond a few lines or is used elsewhere, extract it.
|
|
4309
|
+
|
|
4310
|
+
### Simple Components \u2014 Single File
|
|
4311
|
+
|
|
4312
|
+
A component that is self-contained and has no children components stays as a single file:
|
|
4313
|
+
|
|
4314
|
+
\`\`\`
|
|
4315
|
+
components/
|
|
4316
|
+
\u251C\u2500\u2500 user-avatar.tsx
|
|
4317
|
+
\u251C\u2500\u2500 status-badge.tsx
|
|
4318
|
+
\u2514\u2500\u2500 empty-state.tsx
|
|
4319
|
+
\`\`\`
|
|
4320
|
+
|
|
4321
|
+
### Complex Components \u2014 Folder with Barrel Export
|
|
4322
|
+
|
|
4323
|
+
When a component grows to have its own child components or hooks, promote it to a folder:
|
|
4324
|
+
|
|
4325
|
+
\`\`\`
|
|
4326
|
+
components/
|
|
4327
|
+
\u2514\u2500\u2500 data-table/
|
|
4328
|
+
\u251C\u2500\u2500 index.ts # Barrel \u2014 re-exports the main component
|
|
4329
|
+
\u251C\u2500\u2500 data-table.tsx # Main component (the orchestrator)
|
|
4330
|
+
\u251C\u2500\u2500 components/
|
|
4331
|
+
\u2502 \u251C\u2500\u2500 table-header.tsx # Child component
|
|
4332
|
+
\u2502 \u251C\u2500\u2500 table-row.tsx # Child component
|
|
4333
|
+
\u2502 \u251C\u2500\u2500 table-pagination.tsx # Child component
|
|
4334
|
+
\u2502 \u2514\u2500\u2500 table-empty-state.tsx # Child component
|
|
4335
|
+
\u251C\u2500\u2500 hooks/
|
|
4336
|
+
\u2502 \u251C\u2500\u2500 use-table-sorting.ts # Sorting logic
|
|
4337
|
+
\u2502 \u2514\u2500\u2500 use-table-filters.ts # Filter logic
|
|
4338
|
+
\u2514\u2500\u2500 utils/
|
|
4339
|
+
\u2514\u2500\u2500 column-helpers.ts # Non-component, non-hook helpers
|
|
4340
|
+
\`\`\`
|
|
4341
|
+
|
|
4342
|
+
**\`index.ts\`** \u2014 barrel export for the main component:
|
|
4343
|
+
\`\`\`ts
|
|
4344
|
+
export { DataTable } from './data-table'
|
|
4345
|
+
export type { DataTableProps } from './data-table'
|
|
4346
|
+
\`\`\`
|
|
4347
|
+
|
|
4348
|
+
Consumers import from the folder, not the internal file:
|
|
4349
|
+
\`\`\`tsx
|
|
4350
|
+
// GOOD
|
|
4351
|
+
import { DataTable } from '@/shared/components/data-table'
|
|
4352
|
+
|
|
4353
|
+
// BAD \u2014 reaching into internal structure
|
|
4354
|
+
import { DataTable } from '@/shared/components/data-table/data-table'
|
|
4355
|
+
\`\`\`
|
|
4356
|
+
|
|
4357
|
+
### When to Promote a File to a Folder
|
|
4358
|
+
|
|
4359
|
+
Promote a single-file component to a folder when any of these apply:
|
|
4360
|
+
|
|
4361
|
+
1. It has **child components** \u2014 sections of JSX that are large enough to extract
|
|
4362
|
+
2. It has **custom hooks** \u2014 local logic that warrants its own file
|
|
4363
|
+
3. It has **utility functions** \u2014 helpers that should live in \`utils/\`
|
|
4364
|
+
4. It exceeds **~150 lines** \u2014 a sign it needs decomposition
|
|
4365
|
+
|
|
4366
|
+
### Folder Structure Rules
|
|
4367
|
+
|
|
4368
|
+
| What | Where |
|
|
4369
|
+
|---|---|
|
|
4370
|
+
| Child components only used by the parent | \`<component>/components/\` |
|
|
4371
|
+
| UI logic hooks for the component | \`<component>/hooks/\` |
|
|
4372
|
+
| Non-component, non-hook helpers | \`<component>/utils/\` |
|
|
4373
|
+
| Main component | \`<component>/<component>.tsx\` |
|
|
4374
|
+
| Public API | \`<component>/index.ts\` (barrel) |
|
|
4375
|
+
|
|
4376
|
+
### Barrel Export Rules
|
|
4377
|
+
|
|
4378
|
+
- The \`index.ts\` only re-exports the main component and its public types
|
|
4379
|
+
- Child components in \`components/\` are private \u2014 they are **not** re-exported
|
|
4380
|
+
- If a child component needs to be used elsewhere, move it up to the parent \`components/\` directory or to \`shared/components/\`
|
|
4381
|
+
|
|
4382
|
+
\`\`\`ts
|
|
4383
|
+
// GOOD \u2014 index.ts re-exports only the public API
|
|
4384
|
+
export { DataTable } from './data-table'
|
|
4385
|
+
export type { DataTableProps, Column } from './data-table'
|
|
4386
|
+
|
|
4387
|
+
// BAD \u2014 leaking internal child components
|
|
4388
|
+
export { DataTable } from './data-table'
|
|
4389
|
+
export { TableHeader } from './components/table-header'
|
|
4390
|
+
export { TableRow } from './components/table-row'
|
|
4391
|
+
\`\`\`
|
|
4392
|
+
|
|
4393
|
+
### Where Components Live
|
|
4394
|
+
|
|
4395
|
+
| Scope | Location |
|
|
4396
|
+
|---|---|
|
|
4397
|
+
| Used across the entire app | \`src/shared/components/\` |
|
|
4398
|
+
| Used only within a feature | \`src/features/<feature>/components/\` |
|
|
4399
|
+
| Used only within a page | \`src/pages/<page>/components/\` |
|
|
4400
|
+
| Used only within a parent component | \`<parent>/components/\` |
|
|
4401
|
+
|
|
4402
|
+
### Summary
|
|
4403
|
+
|
|
4404
|
+
1. **One component per file** \u2014 always
|
|
4405
|
+
2. **Non-component functions go in \`utils/\`** \u2014 never loose functions in component files
|
|
4406
|
+
3. **Reuse before writing** \u2014 search the codebase for existing utils before creating new ones; no duplicated logic
|
|
4407
|
+
4. **Simple component = single file** \u2014 no folder overhead for leaf components
|
|
4408
|
+
5. **Complex component = folder** with \`components/\`, \`hooks/\`, \`utils/\` subdirectories and an \`index.ts\` barrel
|
|
4409
|
+
6. **Barrel exports are the public API** \u2014 consumers import from the folder, never reach into internals
|
|
4410
|
+
7. **Scope determines location** \u2014 shared, feature, page, or parent component level
|
|
4411
|
+
`;
|
|
4412
|
+
}
|
|
4413
|
+
};
|
|
4414
|
+
|
|
4155
4415
|
// src/skills/frontend-testing.ts
|
|
4156
4416
|
init_esm_shims();
|
|
4157
4417
|
var frontendTestingSkill = {
|
|
@@ -4224,12 +4484,47 @@ router/
|
|
|
4224
4484
|
- Use \`.spec.ts\` for pure logic tests (hooks, utilities, no JSX)
|
|
4225
4485
|
- Name matches the source file: \`customer-card.tsx\` \u2192 \`customer-card.spec.tsx\`
|
|
4226
4486
|
|
|
4227
|
-
###
|
|
4487
|
+
### Test Rendering Setup
|
|
4228
4488
|
|
|
4229
|
-
> **RULE: Never import \`render\` from \`@testing-library/react\` directly.
|
|
4489
|
+
> **RULE: Never import \`render\` from \`@testing-library/react\` directly. Create a \`renderWithProviders\` helper in \`src/__tests__/test-utils.tsx\` that wraps components with all app providers.**
|
|
4230
4490
|
|
|
4231
|
-
\`
|
|
4491
|
+
Create \`src/__tests__/test-utils.tsx\` if it doesn't exist:
|
|
4232
4492
|
|
|
4493
|
+
\`\`\`tsx
|
|
4494
|
+
// src/__tests__/test-utils.tsx
|
|
4495
|
+
import { render, type RenderOptions } from '@testing-library/react'
|
|
4496
|
+
import { ChakraProvider } from '@chakra-ui/react'
|
|
4497
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4498
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
4499
|
+
import userEvent from '@testing-library/user-event'
|
|
4500
|
+
|
|
4501
|
+
interface ProviderOptions extends Omit<RenderOptions, 'wrapper'> {
|
|
4502
|
+
routerEntries?: string[]
|
|
4503
|
+
queryClient?: QueryClient
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
export function renderWithProviders(ui: React.ReactElement, options: ProviderOptions = {}) {
|
|
4507
|
+
const { routerEntries = ['/'], queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }), ...renderOptions } = options
|
|
4508
|
+
const user = userEvent.setup()
|
|
4509
|
+
|
|
4510
|
+
const result = render(ui, {
|
|
4511
|
+
wrapper: ({ children }) => (
|
|
4512
|
+
<ChakraProvider>
|
|
4513
|
+
<QueryClientProvider client={queryClient}>
|
|
4514
|
+
<MemoryRouter initialEntries={routerEntries}>{children}</MemoryRouter>
|
|
4515
|
+
</QueryClientProvider>
|
|
4516
|
+
</ChakraProvider>
|
|
4517
|
+
),
|
|
4518
|
+
...renderOptions,
|
|
4519
|
+
})
|
|
4520
|
+
|
|
4521
|
+
return { ...result, user }
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
export { screen, waitFor, within } from '@testing-library/react'
|
|
4525
|
+
\`\`\`
|
|
4526
|
+
|
|
4527
|
+
**Usage:**
|
|
4233
4528
|
\`\`\`tsx
|
|
4234
4529
|
import { screen } from '@/__tests__/test-utils'
|
|
4235
4530
|
import { renderWithProviders } from '@/__tests__/test-utils'
|
|
@@ -4243,14 +4538,6 @@ describe('MyComponent', () => {
|
|
|
4243
4538
|
})
|
|
4244
4539
|
\`\`\`
|
|
4245
4540
|
|
|
4246
|
-
**Options:**
|
|
4247
|
-
\`\`\`tsx
|
|
4248
|
-
renderWithProviders(<MyComponent />, {
|
|
4249
|
-
routerEntries: ['/customers/1'], // Set initial route
|
|
4250
|
-
queryClient: customQueryClient, // Custom query client
|
|
4251
|
-
})
|
|
4252
|
-
\`\`\`
|
|
4253
|
-
|
|
4254
4541
|
**User interactions:**
|
|
4255
4542
|
\`\`\`tsx
|
|
4256
4543
|
const { user } = renderWithProviders(<MyComponent />)
|
|
@@ -4341,7 +4628,7 @@ vi.mock('react-router-dom', async () => {
|
|
|
4341
4628
|
|
|
4342
4629
|
### Test Structure
|
|
4343
4630
|
\`\`\`tsx
|
|
4344
|
-
import { screen
|
|
4631
|
+
import { screen } from '@/__tests__/test-utils'
|
|
4345
4632
|
import { renderWithProviders } from '@/__tests__/test-utils'
|
|
4346
4633
|
|
|
4347
4634
|
// Mocks at the top, before imports of modules that use them
|
|
@@ -4507,9 +4794,10 @@ var aiGuidelinesSkill = {
|
|
|
4507
4794
|
return `## AI Development Guidelines
|
|
4508
4795
|
|
|
4509
4796
|
### When Adding Features
|
|
4510
|
-
1. Use \`blacksmith make:resource <Name>\` for new CRUD resources \u2014 it scaffolds
|
|
4511
|
-
2. After any backend API change (new endpoint, changed schema, new field), run \`blacksmith sync\` to regenerate the frontend API client and types
|
|
4512
|
-
3. Never manually edit files in \`
|
|
4797
|
+
1. Use \`blacksmith make:resource <Name>\` for new CRUD resources \u2014 it scaffolds the appropriate files based on project type
|
|
4798
|
+
2. **Fullstack projects:** After any backend API change (new endpoint, changed schema, new field), run \`blacksmith sync\` from the project root to regenerate the frontend API client and types
|
|
4799
|
+
3. Never manually edit files in \`src/api/generated/\` \u2014 they are overwritten on every sync
|
|
4800
|
+
4. Before writing any new function, search the codebase for an existing one that does the same thing
|
|
4513
4801
|
|
|
4514
4802
|
### Code Style
|
|
4515
4803
|
- **Backend**: Follow PEP 8. Use Django and DRF conventions. Docstrings on models, serializers, and non-obvious view methods
|
|
@@ -4517,57 +4805,64 @@ var aiGuidelinesSkill = {
|
|
|
4517
4805
|
- Use existing patterns in the codebase as reference before inventing new ones
|
|
4518
4806
|
|
|
4519
4807
|
### Frontend Architecture (Mandatory)
|
|
4520
|
-
- **Use \`@chakra-ui/react\` for ALL UI** \u2014 \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\` for layout; \`Heading\`, \`Text\` for text; \`Card\`, \`Button\`, \`Badge\`, etc. for all elements. Never use raw HTML (\`<div>\`, \`<h1>\`, \`<p>\`, \`<button>\`) when a Chakra UI component exists
|
|
4521
|
-
- **Pages are thin orchestrators** \u2014 compose child components from \`components/\`, extract logic into \`hooks/\`.
|
|
4522
|
-
- **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings
|
|
4523
|
-
- **
|
|
4808
|
+
- **Use \`@chakra-ui/react\` for ALL UI** \u2014 \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\` for layout; \`Heading\`, \`Text\` for text; \`Card\`, \`Button\`, \`Badge\`, etc. for all elements. Never use raw HTML (\`<div>\`, \`<h1>\`, \`<p>\`, \`<button>\`) when a Chakra UI component exists. Semantic landmarks (\`<main>\`, \`<section>\`, \`<nav>\`, etc.) are the only exception
|
|
4809
|
+
- **Pages are thin orchestrators** \u2014 compose child components from \`components/\`, extract logic into \`hooks/\`. Aim for clarity over strict line counts
|
|
4810
|
+
- **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings
|
|
4811
|
+
- **One component per file** \u2014 promote complex components to folders with barrel exports (see \`frontend-modularization\` skill)
|
|
4524
4812
|
|
|
4525
4813
|
### Environment
|
|
4526
|
-
-
|
|
4527
|
-
- Frontend: \`http://localhost:5173\`
|
|
4528
|
-
- API docs: \`http://localhost:8000/api/docs/\` (Swagger UI) or \`/api/redoc/\` (ReDoc)
|
|
4529
|
-
- Python venv: \`backend/venv/\` \u2014 always use \`./venv/bin/python\` or \`./venv/bin/pip\`
|
|
4814
|
+
- Check \`blacksmith.config.json\` for configured ports and project type
|
|
4530
4815
|
- Start everything: \`blacksmith dev\`
|
|
4816
|
+
- API docs (fullstack/backend): \`http://localhost:<backend-port>/api/docs/\` (Swagger) or \`/api/redoc/\`
|
|
4817
|
+
- Python venv: \`venv/\` in the backend directory \u2014 always use \`./venv/bin/python\` or \`./venv/bin/pip\`
|
|
4531
4818
|
|
|
4532
4819
|
### Checklist Before Finishing a Task
|
|
4533
|
-
1. Backend tests pass
|
|
4534
|
-
2. Frontend tests pass
|
|
4535
|
-
3. Frontend builds
|
|
4536
|
-
4. API types are in sync: \`blacksmith sync\`
|
|
4820
|
+
1. Backend tests pass (if project has backend): \`./venv/bin/python manage.py test\` in the backend directory
|
|
4821
|
+
2. Frontend tests pass (if project has frontend): \`npm test\` in the frontend directory
|
|
4822
|
+
3. Frontend builds (if project has frontend): \`npm run build\` in the frontend directory
|
|
4823
|
+
4. API types are in sync (fullstack only): \`blacksmith sync\`
|
|
4537
4824
|
5. No lint errors in modified files
|
|
4538
4825
|
6. All UI uses \`@chakra-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
|
|
4539
4826
|
7. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
|
|
4540
4827
|
8. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
|
|
4541
4828
|
9. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
|
|
4542
|
-
10.
|
|
4543
|
-
11.
|
|
4829
|
+
10. No duplicated logic \u2014 reusable functions are extracted to \`utils/\`
|
|
4830
|
+
11. Tests are co-located \u2014 every new or modified page, component, or hook has a corresponding \`.spec.tsx\` / \`.spec.ts\` in a \`__tests__/\` folder next to the source file
|
|
4544
4831
|
`;
|
|
4545
4832
|
}
|
|
4546
4833
|
};
|
|
4547
4834
|
|
|
4548
4835
|
// src/commands/ai-setup.ts
|
|
4549
|
-
async function setupAiDev({ projectDir, projectName, includeChakraUiSkill }) {
|
|
4836
|
+
async function setupAiDev({ projectDir, projectName, includeChakraUiSkill, projectType = "fullstack" }) {
|
|
4550
4837
|
const aiSpinner = spinner("Setting up AI development environment...");
|
|
4838
|
+
const needsBackend = projectType === "fullstack" || projectType === "backend";
|
|
4839
|
+
const needsFrontend = projectType === "fullstack" || projectType === "frontend";
|
|
4551
4840
|
try {
|
|
4552
4841
|
const skills = [
|
|
4553
4842
|
coreRulesSkill,
|
|
4554
|
-
projectOverviewSkill
|
|
4555
|
-
djangoSkill,
|
|
4556
|
-
djangoRestAdvancedSkill,
|
|
4557
|
-
apiDocumentationSkill,
|
|
4558
|
-
reactSkill,
|
|
4559
|
-
reactQuerySkill,
|
|
4560
|
-
pageStructureSkill
|
|
4843
|
+
projectOverviewSkill
|
|
4561
4844
|
];
|
|
4562
|
-
if (
|
|
4563
|
-
skills.push(
|
|
4564
|
-
skills.push(
|
|
4565
|
-
skills.push(
|
|
4566
|
-
skills.push(
|
|
4567
|
-
|
|
4845
|
+
if (needsBackend) {
|
|
4846
|
+
skills.push(djangoSkill);
|
|
4847
|
+
skills.push(djangoRestAdvancedSkill);
|
|
4848
|
+
skills.push(apiDocumentationSkill);
|
|
4849
|
+
skills.push(backendModularizationSkill);
|
|
4850
|
+
}
|
|
4851
|
+
if (needsFrontend) {
|
|
4852
|
+
skills.push(reactSkill);
|
|
4853
|
+
skills.push(reactQuerySkill);
|
|
4854
|
+
skills.push(pageStructureSkill);
|
|
4855
|
+
skills.push(frontendModularizationSkill);
|
|
4856
|
+
if (includeChakraUiSkill) {
|
|
4857
|
+
skills.push(chakraUiReactSkill);
|
|
4858
|
+
skills.push(chakraUiFormsSkill);
|
|
4859
|
+
skills.push(chakraUiAuthSkill);
|
|
4860
|
+
skills.push(blacksmithHooksSkill);
|
|
4861
|
+
skills.push(uiDesignSkill);
|
|
4862
|
+
}
|
|
4863
|
+
skills.push(frontendTestingSkill);
|
|
4568
4864
|
}
|
|
4569
4865
|
skills.push(blacksmithCliSkill);
|
|
4570
|
-
skills.push(frontendTestingSkill);
|
|
4571
4866
|
skills.push(programmingParadigmsSkill);
|
|
4572
4867
|
skills.push(cleanCodeSkill);
|
|
4573
4868
|
skills.push(aiGuidelinesSkill);
|
|
@@ -4632,6 +4927,7 @@ function parsePort(value, label) {
|
|
|
4632
4927
|
return port;
|
|
4633
4928
|
}
|
|
4634
4929
|
var THEME_PRESETS = ["default", "blue", "green", "violet", "red", "neutral"];
|
|
4930
|
+
var PROJECT_TYPES = ["fullstack", "backend", "frontend"];
|
|
4635
4931
|
async function init(name, options) {
|
|
4636
4932
|
if (!name) {
|
|
4637
4933
|
name = await promptText("Project name");
|
|
@@ -4640,203 +4936,226 @@ async function init(name, options) {
|
|
|
4640
4936
|
process.exit(1);
|
|
4641
4937
|
}
|
|
4642
4938
|
}
|
|
4643
|
-
|
|
4939
|
+
let projectType;
|
|
4940
|
+
if (options.type && PROJECT_TYPES.includes(options.type)) {
|
|
4941
|
+
projectType = options.type;
|
|
4942
|
+
} else if (options.type) {
|
|
4943
|
+
log.error(`Invalid project type: "${options.type}". Must be one of: fullstack, backend, frontend`);
|
|
4944
|
+
process.exit(1);
|
|
4945
|
+
} else {
|
|
4946
|
+
projectType = await promptSelect("Project type", PROJECT_TYPES, "fullstack");
|
|
4947
|
+
}
|
|
4948
|
+
const needsBackend = projectType === "fullstack" || projectType === "backend";
|
|
4949
|
+
const needsFrontend = projectType === "fullstack" || projectType === "frontend";
|
|
4950
|
+
if (needsBackend && !options.backendPort) {
|
|
4644
4951
|
options.backendPort = await promptText("Backend port", "8000");
|
|
4645
4952
|
}
|
|
4646
|
-
if (!options.frontendPort) {
|
|
4953
|
+
if (needsFrontend && !options.frontendPort) {
|
|
4647
4954
|
options.frontendPort = await promptText("Frontend port", "5173");
|
|
4648
4955
|
}
|
|
4649
|
-
if (!options.themeColor) {
|
|
4956
|
+
if (needsFrontend && !options.themeColor) {
|
|
4650
4957
|
options.themeColor = await promptSelect("Theme preset", THEME_PRESETS, "default");
|
|
4651
4958
|
}
|
|
4652
4959
|
if (options.ai === void 0) {
|
|
4653
4960
|
options.ai = await promptYesNo("Set up AI coding support");
|
|
4654
4961
|
}
|
|
4655
|
-
const backendPort = parsePort(options.backendPort, "backend");
|
|
4656
|
-
const frontendPort = parsePort(options.frontendPort, "frontend");
|
|
4657
|
-
const themePreset = THEME_PRESETS.includes(options.themeColor) ? options.themeColor : "default";
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
});
|
|
4962
|
+
const backendPort = needsBackend ? parsePort(options.backendPort || "8000", "backend") : void 0;
|
|
4963
|
+
const frontendPort = needsFrontend ? parsePort(options.frontendPort || "5173", "frontend") : void 0;
|
|
4964
|
+
const themePreset = needsFrontend && options.themeColor && THEME_PRESETS.includes(options.themeColor) ? options.themeColor : "default";
|
|
4965
|
+
const configDisplay = { "Project": name, "Type": projectType };
|
|
4966
|
+
if (needsBackend) configDisplay["Backend"] = `Django on :${backendPort}`;
|
|
4967
|
+
if (needsFrontend) configDisplay["Frontend"] = `React on :${frontendPort}`;
|
|
4968
|
+
if (needsFrontend) configDisplay["Theme"] = themePreset;
|
|
4969
|
+
configDisplay["AI support"] = options.ai ? "Yes" : "No";
|
|
4970
|
+
printConfig(configDisplay);
|
|
4665
4971
|
const projectDir = path5.resolve(process.cwd(), name);
|
|
4666
|
-
const backendDir = path5.join(projectDir, "backend");
|
|
4667
|
-
const frontendDir = path5.join(projectDir, "frontend");
|
|
4972
|
+
const backendDir = needsBackend ? projectType === "backend" ? projectDir : path5.join(projectDir, "backend") : null;
|
|
4973
|
+
const frontendDir = needsFrontend ? projectType === "frontend" ? projectDir : path5.join(projectDir, "frontend") : null;
|
|
4668
4974
|
const templatesDir = getTemplatesDir();
|
|
4669
4975
|
if (fs4.existsSync(projectDir)) {
|
|
4670
4976
|
log.error(`Directory "${name}" already exists.`);
|
|
4671
4977
|
process.exit(1);
|
|
4672
4978
|
}
|
|
4673
4979
|
const checkSpinner = spinner("Checking prerequisites...");
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4980
|
+
if (needsBackend) {
|
|
4981
|
+
const hasPython = await commandExists("python3");
|
|
4982
|
+
if (!hasPython) {
|
|
4983
|
+
checkSpinner.fail("Python 3 is required but not found. Install it from https://python.org");
|
|
4984
|
+
process.exit(1);
|
|
4985
|
+
}
|
|
4680
4986
|
}
|
|
4681
|
-
if (
|
|
4682
|
-
|
|
4683
|
-
|
|
4987
|
+
if (needsFrontend) {
|
|
4988
|
+
const hasNode = await commandExists("node");
|
|
4989
|
+
const hasNpm = await commandExists("npm");
|
|
4990
|
+
if (!hasNode || !hasNpm) {
|
|
4991
|
+
checkSpinner.fail("Node.js and npm are required but not found. Install from https://nodejs.org");
|
|
4992
|
+
process.exit(1);
|
|
4993
|
+
}
|
|
4684
4994
|
}
|
|
4685
|
-
|
|
4995
|
+
const prereqs = [
|
|
4996
|
+
needsBackend ? "Python 3" : null,
|
|
4997
|
+
needsFrontend ? "Node.js, npm" : null
|
|
4998
|
+
].filter(Boolean).join(", ");
|
|
4999
|
+
checkSpinner.succeed(`Prerequisites OK (${prereqs})`);
|
|
4686
5000
|
const context = {
|
|
4687
5001
|
projectName: name,
|
|
4688
|
-
backendPort,
|
|
4689
|
-
frontendPort,
|
|
5002
|
+
backendPort: backendPort || 8e3,
|
|
5003
|
+
frontendPort: frontendPort || 5173,
|
|
4690
5004
|
themePreset
|
|
4691
5005
|
};
|
|
4692
5006
|
fs4.mkdirSync(projectDir, { recursive: true });
|
|
5007
|
+
const configObj = {
|
|
5008
|
+
name,
|
|
5009
|
+
version: "0.1.0",
|
|
5010
|
+
type: projectType
|
|
5011
|
+
};
|
|
5012
|
+
if (needsBackend) configObj.backend = { port: backendPort };
|
|
5013
|
+
if (needsFrontend) configObj.frontend = { port: frontendPort };
|
|
4693
5014
|
fs4.writeFileSync(
|
|
4694
5015
|
path5.join(projectDir, "blacksmith.config.json"),
|
|
4695
|
-
JSON.stringify(
|
|
4696
|
-
{
|
|
4697
|
-
name,
|
|
4698
|
-
version: "0.1.0",
|
|
4699
|
-
backend: { port: backendPort },
|
|
4700
|
-
frontend: { port: frontendPort }
|
|
4701
|
-
},
|
|
4702
|
-
null,
|
|
4703
|
-
2
|
|
4704
|
-
)
|
|
5016
|
+
JSON.stringify(configObj, null, 2)
|
|
4705
5017
|
);
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
try {
|
|
4757
|
-
renderDirectory(
|
|
4758
|
-
path5.join(templatesDir, "frontend"),
|
|
4759
|
-
frontendDir,
|
|
4760
|
-
context
|
|
4761
|
-
);
|
|
4762
|
-
frontendSpinner.succeed("React frontend generated");
|
|
4763
|
-
} catch (error) {
|
|
4764
|
-
frontendSpinner.fail("Failed to generate frontend");
|
|
4765
|
-
log.error(error.message);
|
|
4766
|
-
process.exit(1);
|
|
4767
|
-
}
|
|
4768
|
-
const npmSpinner = spinner("Installing Node.js dependencies...");
|
|
4769
|
-
try {
|
|
4770
|
-
await exec("npm", ["install"], { cwd: frontendDir, silent: true });
|
|
4771
|
-
npmSpinner.succeed("Node.js dependencies installed");
|
|
4772
|
-
} catch (error) {
|
|
4773
|
-
npmSpinner.fail("Failed to install Node.js dependencies");
|
|
4774
|
-
log.error(error.message);
|
|
4775
|
-
process.exit(1);
|
|
5018
|
+
if (backendDir) {
|
|
5019
|
+
const backendSpinner = spinner("Generating Django backend...");
|
|
5020
|
+
try {
|
|
5021
|
+
renderDirectory(
|
|
5022
|
+
path5.join(templatesDir, "backend"),
|
|
5023
|
+
backendDir,
|
|
5024
|
+
context
|
|
5025
|
+
);
|
|
5026
|
+
fs4.copyFileSync(
|
|
5027
|
+
path5.join(backendDir, ".env.example"),
|
|
5028
|
+
path5.join(backendDir, ".env")
|
|
5029
|
+
);
|
|
5030
|
+
backendSpinner.succeed("Django backend generated");
|
|
5031
|
+
} catch (error) {
|
|
5032
|
+
backendSpinner.fail("Failed to generate backend");
|
|
5033
|
+
log.error(error.message);
|
|
5034
|
+
process.exit(1);
|
|
5035
|
+
}
|
|
5036
|
+
const venvSpinner = spinner("Creating Python virtual environment...");
|
|
5037
|
+
try {
|
|
5038
|
+
await exec("python3", ["-m", "venv", "venv"], { cwd: backendDir, silent: true });
|
|
5039
|
+
venvSpinner.succeed("Virtual environment created");
|
|
5040
|
+
} catch (error) {
|
|
5041
|
+
venvSpinner.fail("Failed to create virtual environment");
|
|
5042
|
+
log.error(error.message);
|
|
5043
|
+
process.exit(1);
|
|
5044
|
+
}
|
|
5045
|
+
const pipSpinner = spinner("Installing Python dependencies...");
|
|
5046
|
+
try {
|
|
5047
|
+
await execPip(
|
|
5048
|
+
["install", "-r", "requirements.txt"],
|
|
5049
|
+
backendDir,
|
|
5050
|
+
true
|
|
5051
|
+
);
|
|
5052
|
+
pipSpinner.succeed("Python dependencies installed");
|
|
5053
|
+
} catch (error) {
|
|
5054
|
+
pipSpinner.fail("Failed to install Python dependencies");
|
|
5055
|
+
log.error(error.message);
|
|
5056
|
+
process.exit(1);
|
|
5057
|
+
}
|
|
5058
|
+
const migrateSpinner = spinner("Running initial migrations...");
|
|
5059
|
+
try {
|
|
5060
|
+
await execPython(["manage.py", "makemigrations", "users"], backendDir, true);
|
|
5061
|
+
await execPython(["manage.py", "migrate"], backendDir, true);
|
|
5062
|
+
migrateSpinner.succeed("Database migrated");
|
|
5063
|
+
} catch (error) {
|
|
5064
|
+
migrateSpinner.fail("Failed to run migrations");
|
|
5065
|
+
log.error(error.message);
|
|
5066
|
+
process.exit(1);
|
|
5067
|
+
}
|
|
4776
5068
|
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
const djangoProcess = spawn(
|
|
4780
|
-
"./venv/bin/python",
|
|
4781
|
-
["manage.py", "runserver", `0.0.0.0:${backendPort}`, "--noreload"],
|
|
4782
|
-
{
|
|
4783
|
-
cwd: backendDir,
|
|
4784
|
-
stdio: "ignore",
|
|
4785
|
-
detached: true
|
|
4786
|
-
}
|
|
4787
|
-
);
|
|
4788
|
-
djangoProcess.unref();
|
|
4789
|
-
await new Promise((resolve) => setTimeout(resolve, 4e3));
|
|
5069
|
+
if (frontendDir) {
|
|
5070
|
+
const frontendSpinner = spinner("Generating React frontend...");
|
|
4790
5071
|
try {
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
5072
|
+
renderDirectory(
|
|
5073
|
+
path5.join(templatesDir, "frontend"),
|
|
5074
|
+
frontendDir,
|
|
5075
|
+
context
|
|
5076
|
+
);
|
|
5077
|
+
frontendSpinner.succeed("React frontend generated");
|
|
5078
|
+
} catch (error) {
|
|
5079
|
+
frontendSpinner.fail("Failed to generate frontend");
|
|
5080
|
+
log.error(error.message);
|
|
5081
|
+
process.exit(1);
|
|
4795
5082
|
}
|
|
5083
|
+
const npmSpinner = spinner("Installing Node.js dependencies...");
|
|
5084
|
+
try {
|
|
5085
|
+
await exec("npm", ["install"], { cwd: frontendDir, silent: true });
|
|
5086
|
+
npmSpinner.succeed("Node.js dependencies installed");
|
|
5087
|
+
} catch (error) {
|
|
5088
|
+
npmSpinner.fail("Failed to install Node.js dependencies");
|
|
5089
|
+
log.error(error.message);
|
|
5090
|
+
process.exit(1);
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
if (backendDir && frontendDir) {
|
|
5094
|
+
const syncSpinner = spinner("Running initial OpenAPI sync...");
|
|
4796
5095
|
try {
|
|
4797
|
-
|
|
4798
|
-
|
|
5096
|
+
const djangoProcess = spawn(
|
|
5097
|
+
"./venv/bin/python",
|
|
5098
|
+
["manage.py", "runserver", `0.0.0.0:${backendPort}`, "--noreload"],
|
|
5099
|
+
{
|
|
5100
|
+
cwd: backendDir,
|
|
5101
|
+
stdio: "ignore",
|
|
5102
|
+
detached: true
|
|
5103
|
+
}
|
|
5104
|
+
);
|
|
5105
|
+
djangoProcess.unref();
|
|
5106
|
+
await new Promise((resolve) => setTimeout(resolve, 4e3));
|
|
5107
|
+
try {
|
|
5108
|
+
await exec(process.execPath, [path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
|
|
5109
|
+
syncSpinner.succeed("OpenAPI types synced");
|
|
5110
|
+
} catch {
|
|
5111
|
+
syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
|
|
5112
|
+
}
|
|
5113
|
+
try {
|
|
5114
|
+
if (djangoProcess.pid) {
|
|
5115
|
+
process.kill(-djangoProcess.pid);
|
|
5116
|
+
}
|
|
5117
|
+
} catch {
|
|
4799
5118
|
}
|
|
4800
5119
|
} catch {
|
|
5120
|
+
syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
|
|
4801
5121
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
5122
|
+
const generatedDir = path5.join(frontendDir, "src", "api", "generated");
|
|
5123
|
+
const stubFile = path5.join(generatedDir, "client.gen.ts");
|
|
5124
|
+
if (!fs4.existsSync(stubFile)) {
|
|
5125
|
+
if (!fs4.existsSync(generatedDir)) {
|
|
5126
|
+
fs4.mkdirSync(generatedDir, { recursive: true });
|
|
5127
|
+
}
|
|
5128
|
+
fs4.writeFileSync(
|
|
5129
|
+
stubFile,
|
|
5130
|
+
[
|
|
5131
|
+
"/**",
|
|
5132
|
+
" * Auto-generated API Client",
|
|
5133
|
+
" *",
|
|
5134
|
+
" * This is a stub file that allows the app to boot before",
|
|
5135
|
+
" * the first OpenAPI sync. Run `blacksmith sync` or `blacksmith dev`",
|
|
5136
|
+
" * to generate the real client from your Django API schema.",
|
|
5137
|
+
" *",
|
|
5138
|
+
" * Generated by Blacksmith. This file will be overwritten by openapi-ts.",
|
|
5139
|
+
" */",
|
|
5140
|
+
"",
|
|
5141
|
+
"import { createClient } from '@hey-api/client-fetch'",
|
|
5142
|
+
"",
|
|
5143
|
+
"export const client = createClient()",
|
|
5144
|
+
""
|
|
5145
|
+
].join("\n"),
|
|
5146
|
+
"utf-8"
|
|
5147
|
+
);
|
|
4810
5148
|
}
|
|
4811
|
-
fs4.writeFileSync(
|
|
4812
|
-
stubFile,
|
|
4813
|
-
[
|
|
4814
|
-
"/**",
|
|
4815
|
-
" * Auto-generated API Client",
|
|
4816
|
-
" *",
|
|
4817
|
-
" * This is a stub file that allows the app to boot before",
|
|
4818
|
-
" * the first OpenAPI sync. Run `blacksmith sync` or `blacksmith dev`",
|
|
4819
|
-
" * to generate the real client from your Django API schema.",
|
|
4820
|
-
" *",
|
|
4821
|
-
" * Generated by Blacksmith. This file will be overwritten by openapi-ts.",
|
|
4822
|
-
" */",
|
|
4823
|
-
"",
|
|
4824
|
-
"import { createClient } from '@hey-api/client-fetch'",
|
|
4825
|
-
"",
|
|
4826
|
-
"export const client = createClient()",
|
|
4827
|
-
""
|
|
4828
|
-
].join("\n"),
|
|
4829
|
-
"utf-8"
|
|
4830
|
-
);
|
|
4831
5149
|
}
|
|
4832
5150
|
if (options.ai) {
|
|
4833
5151
|
await setupAiDev({
|
|
4834
5152
|
projectDir,
|
|
4835
5153
|
projectName: name,
|
|
4836
|
-
includeChakraUiSkill: options.chakraUiSkill !== false
|
|
5154
|
+
includeChakraUiSkill: options.chakraUiSkill !== false,
|
|
5155
|
+
projectType
|
|
4837
5156
|
});
|
|
4838
5157
|
}
|
|
4839
|
-
printNextSteps(name, backendPort, frontendPort);
|
|
5158
|
+
printNextSteps(name, projectType, backendPort, frontendPort);
|
|
4840
5159
|
}
|
|
4841
5160
|
|
|
4842
5161
|
// src/commands/dev.ts
|
|
@@ -4871,82 +5190,96 @@ async function dev() {
|
|
|
4871
5190
|
process.exit(1);
|
|
4872
5191
|
}
|
|
4873
5192
|
const config = loadConfig(root);
|
|
4874
|
-
const
|
|
4875
|
-
const
|
|
5193
|
+
const projectHasBackend = hasBackend(root);
|
|
5194
|
+
const projectHasFrontend = hasFrontend(root);
|
|
4876
5195
|
let backendPort;
|
|
4877
5196
|
let frontendPort;
|
|
4878
5197
|
try {
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5198
|
+
if (projectHasBackend && config.backend) {
|
|
5199
|
+
backendPort = await findAvailablePort(config.backend.port);
|
|
5200
|
+
if (backendPort !== config.backend.port) {
|
|
5201
|
+
log.step(`Backend port ${config.backend.port} in use, using ${backendPort}`);
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
if (projectHasFrontend && config.frontend) {
|
|
5205
|
+
frontendPort = await findAvailablePort(config.frontend.port);
|
|
5206
|
+
if (frontendPort !== config.frontend.port) {
|
|
5207
|
+
log.step(`Frontend port ${config.frontend.port} in use, using ${frontendPort}`);
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
4884
5210
|
} catch (err) {
|
|
4885
5211
|
log.error(err.message);
|
|
4886
5212
|
process.exit(1);
|
|
4887
5213
|
}
|
|
4888
|
-
|
|
4889
|
-
|
|
5214
|
+
log.info("Starting development server" + (projectHasBackend && projectHasFrontend ? "s" : "") + "...");
|
|
5215
|
+
log.blank();
|
|
5216
|
+
if (projectHasBackend && backendPort) {
|
|
5217
|
+
log.step(`Django \u2192 http://localhost:${backendPort}`);
|
|
5218
|
+
log.step(`Swagger \u2192 http://localhost:${backendPort}/api/docs/`);
|
|
4890
5219
|
}
|
|
4891
|
-
if (
|
|
4892
|
-
log.step(`
|
|
5220
|
+
if (projectHasFrontend && frontendPort) {
|
|
5221
|
+
log.step(`Vite \u2192 http://localhost:${frontendPort}`);
|
|
5222
|
+
}
|
|
5223
|
+
if (projectHasBackend && projectHasFrontend) {
|
|
5224
|
+
log.step("OpenAPI sync \u2192 watching backend .py files");
|
|
4893
5225
|
}
|
|
4894
|
-
log.info("Starting development servers...");
|
|
4895
|
-
log.blank();
|
|
4896
|
-
log.step(`Django \u2192 http://localhost:${backendPort}`);
|
|
4897
|
-
log.step(`Vite \u2192 http://localhost:${frontendPort}`);
|
|
4898
|
-
log.step(`Swagger \u2192 http://localhost:${backendPort}/api/docs/`);
|
|
4899
|
-
log.step("OpenAPI sync \u2192 watching backend .py files");
|
|
4900
5226
|
log.blank();
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
},
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
{
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
}
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
5227
|
+
const processes = [];
|
|
5228
|
+
if (projectHasBackend && backendPort) {
|
|
5229
|
+
const backendDir = getBackendDir(root);
|
|
5230
|
+
processes.push({
|
|
5231
|
+
command: `./venv/bin/python manage.py runserver 0.0.0.0:${backendPort}`,
|
|
5232
|
+
name: "django",
|
|
5233
|
+
cwd: backendDir,
|
|
5234
|
+
prefixColor: "green"
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
if (projectHasFrontend && frontendPort) {
|
|
5238
|
+
const frontendDir = getFrontendDir(root);
|
|
5239
|
+
processes.push({
|
|
5240
|
+
command: "npm run dev",
|
|
5241
|
+
name: "vite",
|
|
5242
|
+
cwd: frontendDir,
|
|
5243
|
+
prefixColor: "blue"
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
5246
|
+
if (projectHasBackend && projectHasFrontend) {
|
|
5247
|
+
const backendDir = getBackendDir(root);
|
|
5248
|
+
const frontendDir = getFrontendDir(root);
|
|
5249
|
+
const syncCmd = `${process.execPath} ${path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
|
|
5250
|
+
const watcherCode = [
|
|
5251
|
+
`const{watch}=require("fs"),{exec}=require("child_process");`,
|
|
5252
|
+
`let t=null,s=false;`,
|
|
5253
|
+
`watch(${JSON.stringify(backendDir)},{recursive:true},(e,f)=>{`,
|
|
5254
|
+
`if(!f||!f.endsWith(".py"))return;`,
|
|
5255
|
+
`if(f.startsWith("venv/")||f.includes("__pycache__")||f.includes("/migrations/"))return;`,
|
|
5256
|
+
`if(t)clearTimeout(t);`,
|
|
5257
|
+
`t=setTimeout(()=>{`,
|
|
5258
|
+
`if(s)return;s=true;`,
|
|
5259
|
+
`console.log("Backend change detected \u2014 syncing OpenAPI types...");`,
|
|
5260
|
+
`exec(${JSON.stringify(syncCmd)},{cwd:${JSON.stringify(frontendDir)}},(err,o,se)=>{`,
|
|
5261
|
+
`s=false;`,
|
|
5262
|
+
`if(err)console.error("Sync failed:",se||err.message);`,
|
|
5263
|
+
`else console.log("OpenAPI types synced");`,
|
|
5264
|
+
`})`,
|
|
5265
|
+
`},2000)});`,
|
|
5266
|
+
`console.log("Watching for .py changes...");`
|
|
5267
|
+
].join("");
|
|
5268
|
+
processes.push({
|
|
5269
|
+
command: `node -e '${watcherCode}'`,
|
|
5270
|
+
name: "sync",
|
|
5271
|
+
cwd: frontendDir,
|
|
5272
|
+
prefixColor: "yellow"
|
|
5273
|
+
});
|
|
5274
|
+
}
|
|
5275
|
+
const { result } = concurrently(processes, {
|
|
5276
|
+
prefix: "name",
|
|
5277
|
+
killOthers: ["failure"],
|
|
5278
|
+
restartTries: 3
|
|
5279
|
+
});
|
|
4947
5280
|
const shutdown = () => {
|
|
4948
5281
|
log.blank();
|
|
4949
|
-
log.info("Development
|
|
5282
|
+
log.info("Development server" + (processes.length > 1 ? "s" : "") + " stopped.");
|
|
4950
5283
|
process.exit(0);
|
|
4951
5284
|
};
|
|
4952
5285
|
process.on("SIGINT", shutdown);
|
|
@@ -4969,6 +5302,12 @@ async function sync() {
|
|
|
4969
5302
|
log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
|
|
4970
5303
|
process.exit(1);
|
|
4971
5304
|
}
|
|
5305
|
+
const projectType = getProjectType(root);
|
|
5306
|
+
if (projectType !== "fullstack") {
|
|
5307
|
+
log.error('The "sync" command is only available for fullstack projects.');
|
|
5308
|
+
log.step("It generates frontend TypeScript types from the Django API schema.");
|
|
5309
|
+
process.exit(1);
|
|
5310
|
+
}
|
|
4972
5311
|
const backendDir = getBackendDir(root);
|
|
4973
5312
|
const frontendDir = getFrontendDir(root);
|
|
4974
5313
|
const s = spinner("Syncing OpenAPI schema to frontend...");
|
|
@@ -5042,150 +5381,168 @@ async function makeResource(name) {
|
|
|
5042
5381
|
process.exit(1);
|
|
5043
5382
|
}
|
|
5044
5383
|
const names = generateNames(name);
|
|
5045
|
-
const backendDir = getBackendDir(root);
|
|
5046
|
-
const frontendDir = getFrontendDir(root);
|
|
5047
5384
|
const templatesDir = getTemplatesDir();
|
|
5048
|
-
const
|
|
5049
|
-
|
|
5050
|
-
log.error(`Backend app "${names.snakes}" already exists.`);
|
|
5051
|
-
process.exit(1);
|
|
5052
|
-
}
|
|
5053
|
-
const frontendPageDir = path8.join(frontendDir, "src", "pages", names.kebabs);
|
|
5054
|
-
if (fs6.existsSync(frontendPageDir)) {
|
|
5055
|
-
log.error(`Frontend page "${names.kebabs}" already exists.`);
|
|
5056
|
-
process.exit(1);
|
|
5057
|
-
}
|
|
5385
|
+
const projectHasBackend = hasBackend(root);
|
|
5386
|
+
const projectHasFrontend = hasFrontend(root);
|
|
5058
5387
|
const context = { ...names, projectName: name };
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
backendSpinner.succeed(`Created backend/apps/${names.snakes}/`);
|
|
5067
|
-
} catch (error) {
|
|
5068
|
-
backendSpinner.fail("Failed to create backend app");
|
|
5069
|
-
log.error(error.message);
|
|
5070
|
-
process.exit(1);
|
|
5071
|
-
}
|
|
5072
|
-
const registerSpinner = spinner("Registering app in Django settings...");
|
|
5073
|
-
try {
|
|
5074
|
-
const settingsPath = path8.join(backendDir, "config", "settings", "base.py");
|
|
5075
|
-
appendAfterMarker(
|
|
5076
|
-
settingsPath,
|
|
5077
|
-
"# blacksmith:apps",
|
|
5078
|
-
` 'apps.${names.snakes}',`
|
|
5079
|
-
);
|
|
5080
|
-
registerSpinner.succeed("Registered in INSTALLED_APPS");
|
|
5081
|
-
} catch (error) {
|
|
5082
|
-
registerSpinner.fail("Failed to register app in settings");
|
|
5083
|
-
log.error(error.message);
|
|
5084
|
-
process.exit(1);
|
|
5085
|
-
}
|
|
5086
|
-
const urlSpinner = spinner("Registering API URLs...");
|
|
5087
|
-
try {
|
|
5088
|
-
const urlsPath = path8.join(backendDir, "config", "urls.py");
|
|
5089
|
-
insertBeforeMarker(
|
|
5090
|
-
urlsPath,
|
|
5091
|
-
"# blacksmith:urls",
|
|
5092
|
-
` path('api/${names.snakes}/', include('apps.${names.snakes}.urls')),`
|
|
5093
|
-
);
|
|
5094
|
-
urlSpinner.succeed("Registered API URLs");
|
|
5095
|
-
} catch (error) {
|
|
5096
|
-
urlSpinner.fail("Failed to register URLs");
|
|
5097
|
-
log.error(error.message);
|
|
5098
|
-
process.exit(1);
|
|
5388
|
+
if (projectHasBackend) {
|
|
5389
|
+
const backendDir = getBackendDir(root);
|
|
5390
|
+
const backendAppDir = path8.join(backendDir, "apps", names.snakes);
|
|
5391
|
+
if (fs6.existsSync(backendAppDir)) {
|
|
5392
|
+
log.error(`Backend app "${names.snakes}" already exists.`);
|
|
5393
|
+
process.exit(1);
|
|
5394
|
+
}
|
|
5099
5395
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
log.error(error.message);
|
|
5108
|
-
process.exit(1);
|
|
5396
|
+
if (projectHasFrontend) {
|
|
5397
|
+
const frontendDir = getFrontendDir(root);
|
|
5398
|
+
const frontendPageDir = path8.join(frontendDir, "src", "pages", names.kebabs);
|
|
5399
|
+
if (fs6.existsSync(frontendPageDir)) {
|
|
5400
|
+
log.error(`Frontend page "${names.kebabs}" already exists.`);
|
|
5401
|
+
process.exit(1);
|
|
5402
|
+
}
|
|
5109
5403
|
}
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
const
|
|
5113
|
-
|
|
5114
|
-
const configPath = path8.join(frontendDir, "openapi-ts.config.ts");
|
|
5115
|
-
const configBackup = fs6.readFileSync(configPath, "utf-8");
|
|
5116
|
-
const configWithFile = configBackup.replace(
|
|
5117
|
-
/path:\s*['"]http[^'"]+['"]/,
|
|
5118
|
-
`path: './_schema.yml'`
|
|
5119
|
-
);
|
|
5120
|
-
fs6.writeFileSync(configPath, configWithFile, "utf-8");
|
|
5404
|
+
if (projectHasBackend) {
|
|
5405
|
+
const backendDir = getBackendDir(root);
|
|
5406
|
+
const backendAppDir = path8.join(backendDir, "apps", names.snakes);
|
|
5407
|
+
const backendSpinner = spinner(`Creating backend app: apps/${names.snakes}/`);
|
|
5121
5408
|
try {
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5409
|
+
renderDirectory(
|
|
5410
|
+
path8.join(templatesDir, "resource", "backend"),
|
|
5411
|
+
backendAppDir,
|
|
5412
|
+
context
|
|
5413
|
+
);
|
|
5414
|
+
backendSpinner.succeed(`Created apps/${names.snakes}/`);
|
|
5415
|
+
} catch (error) {
|
|
5416
|
+
backendSpinner.fail("Failed to create backend app");
|
|
5417
|
+
log.error(error.message);
|
|
5418
|
+
process.exit(1);
|
|
5419
|
+
}
|
|
5420
|
+
const registerSpinner = spinner("Registering app in Django settings...");
|
|
5421
|
+
try {
|
|
5422
|
+
const settingsPath = path8.join(backendDir, "config", "settings", "base.py");
|
|
5423
|
+
appendAfterMarker(
|
|
5424
|
+
settingsPath,
|
|
5425
|
+
"# blacksmith:apps",
|
|
5426
|
+
` 'apps.${names.snakes}',`
|
|
5427
|
+
);
|
|
5428
|
+
registerSpinner.succeed("Registered in INSTALLED_APPS");
|
|
5429
|
+
} catch (error) {
|
|
5430
|
+
registerSpinner.fail("Failed to register app in settings");
|
|
5431
|
+
log.error(error.message);
|
|
5432
|
+
process.exit(1);
|
|
5433
|
+
}
|
|
5434
|
+
const urlSpinner = spinner("Registering API URLs...");
|
|
5435
|
+
try {
|
|
5436
|
+
const urlsPath = path8.join(backendDir, "config", "urls.py");
|
|
5437
|
+
insertBeforeMarker(
|
|
5438
|
+
urlsPath,
|
|
5439
|
+
"# blacksmith:urls",
|
|
5440
|
+
` path('api/${names.snakes}/', include('apps.${names.snakes}.urls')),`
|
|
5441
|
+
);
|
|
5442
|
+
urlSpinner.succeed("Registered API URLs");
|
|
5443
|
+
} catch (error) {
|
|
5444
|
+
urlSpinner.fail("Failed to register URLs");
|
|
5445
|
+
log.error(error.message);
|
|
5446
|
+
process.exit(1);
|
|
5447
|
+
}
|
|
5448
|
+
const migrateSpinner = spinner("Running migrations...");
|
|
5449
|
+
try {
|
|
5450
|
+
await execPython(["manage.py", "makemigrations", names.snakes], backendDir, true);
|
|
5451
|
+
await execPython(["manage.py", "migrate"], backendDir, true);
|
|
5452
|
+
migrateSpinner.succeed("Migrations complete");
|
|
5453
|
+
} catch (error) {
|
|
5454
|
+
migrateSpinner.fail("Migration failed");
|
|
5455
|
+
log.error(error.message);
|
|
5456
|
+
process.exit(1);
|
|
5129
5457
|
}
|
|
5130
|
-
syncSpinner.succeed("Frontend types and hooks regenerated");
|
|
5131
|
-
} catch {
|
|
5132
|
-
syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
|
|
5133
|
-
}
|
|
5134
|
-
const apiHooksDir = path8.join(frontendDir, "src", "api", "hooks", names.kebabs);
|
|
5135
|
-
const apiHooksSpinner = spinner(`Creating API hooks: api/hooks/${names.kebabs}/`);
|
|
5136
|
-
try {
|
|
5137
|
-
renderDirectory(
|
|
5138
|
-
path8.join(templatesDir, "resource", "api-hooks"),
|
|
5139
|
-
apiHooksDir,
|
|
5140
|
-
context
|
|
5141
|
-
);
|
|
5142
|
-
apiHooksSpinner.succeed(`Created frontend/src/api/hooks/${names.kebabs}/`);
|
|
5143
|
-
} catch (error) {
|
|
5144
|
-
apiHooksSpinner.fail("Failed to create API hooks");
|
|
5145
|
-
log.error(error.message);
|
|
5146
|
-
process.exit(1);
|
|
5147
|
-
}
|
|
5148
|
-
const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
|
|
5149
|
-
try {
|
|
5150
|
-
renderDirectory(
|
|
5151
|
-
path8.join(templatesDir, "resource", "pages"),
|
|
5152
|
-
frontendPageDir,
|
|
5153
|
-
context
|
|
5154
|
-
);
|
|
5155
|
-
frontendSpinner.succeed(`Created frontend/src/pages/${names.kebabs}/`);
|
|
5156
|
-
} catch (error) {
|
|
5157
|
-
frontendSpinner.fail("Failed to create frontend page");
|
|
5158
|
-
log.error(error.message);
|
|
5159
|
-
process.exit(1);
|
|
5160
5458
|
}
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
const
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5459
|
+
if (projectHasBackend && projectHasFrontend) {
|
|
5460
|
+
const backendDir = getBackendDir(root);
|
|
5461
|
+
const frontendDir = getFrontendDir(root);
|
|
5462
|
+
const syncSpinner = spinner("Syncing OpenAPI schema...");
|
|
5463
|
+
try {
|
|
5464
|
+
const schemaPath = path8.join(frontendDir, "_schema.yml");
|
|
5465
|
+
await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
|
|
5466
|
+
const configPath = path8.join(frontendDir, "openapi-ts.config.ts");
|
|
5467
|
+
const configBackup = fs6.readFileSync(configPath, "utf-8");
|
|
5468
|
+
const configWithFile = configBackup.replace(
|
|
5469
|
+
/path:\s*['"]http[^'"]+['"]/,
|
|
5470
|
+
`path: './_schema.yml'`
|
|
5471
|
+
);
|
|
5472
|
+
fs6.writeFileSync(configPath, configWithFile, "utf-8");
|
|
5473
|
+
try {
|
|
5474
|
+
await exec(process.execPath, [path8.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
|
|
5475
|
+
cwd: frontendDir,
|
|
5476
|
+
silent: true
|
|
5477
|
+
});
|
|
5478
|
+
} finally {
|
|
5479
|
+
fs6.writeFileSync(configPath, configBackup, "utf-8");
|
|
5480
|
+
if (fs6.existsSync(schemaPath)) fs6.unlinkSync(schemaPath);
|
|
5481
|
+
}
|
|
5482
|
+
syncSpinner.succeed("Frontend types and hooks regenerated");
|
|
5483
|
+
} catch {
|
|
5484
|
+
syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
|
|
5485
|
+
}
|
|
5172
5486
|
}
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
const
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5487
|
+
if (projectHasFrontend) {
|
|
5488
|
+
const frontendDir = getFrontendDir(root);
|
|
5489
|
+
const apiHooksDir = path8.join(frontendDir, "src", "api", "hooks", names.kebabs);
|
|
5490
|
+
const apiHooksSpinner = spinner(`Creating API hooks: api/hooks/${names.kebabs}/`);
|
|
5491
|
+
try {
|
|
5492
|
+
renderDirectory(
|
|
5493
|
+
path8.join(templatesDir, "resource", "api-hooks"),
|
|
5494
|
+
apiHooksDir,
|
|
5495
|
+
context
|
|
5496
|
+
);
|
|
5497
|
+
apiHooksSpinner.succeed(`Created src/api/hooks/${names.kebabs}/`);
|
|
5498
|
+
} catch (error) {
|
|
5499
|
+
apiHooksSpinner.fail("Failed to create API hooks");
|
|
5500
|
+
log.error(error.message);
|
|
5501
|
+
process.exit(1);
|
|
5502
|
+
}
|
|
5503
|
+
const frontendPageDir = path8.join(frontendDir, "src", "pages", names.kebabs);
|
|
5504
|
+
const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
|
|
5505
|
+
try {
|
|
5506
|
+
renderDirectory(
|
|
5507
|
+
path8.join(templatesDir, "resource", "pages"),
|
|
5508
|
+
frontendPageDir,
|
|
5509
|
+
context
|
|
5510
|
+
);
|
|
5511
|
+
frontendSpinner.succeed(`Created src/pages/${names.kebabs}/`);
|
|
5512
|
+
} catch (error) {
|
|
5513
|
+
frontendSpinner.fail("Failed to create frontend page");
|
|
5514
|
+
log.error(error.message);
|
|
5515
|
+
process.exit(1);
|
|
5516
|
+
}
|
|
5517
|
+
const pathSpinner = spinner("Registering route path...");
|
|
5518
|
+
try {
|
|
5519
|
+
const pathsFile = path8.join(frontendDir, "src", "router", "paths.ts");
|
|
5520
|
+
insertBeforeMarker(
|
|
5521
|
+
pathsFile,
|
|
5522
|
+
"// blacksmith:path",
|
|
5523
|
+
` ${names.Names} = '/${names.kebabs}',`
|
|
5524
|
+
);
|
|
5525
|
+
pathSpinner.succeed("Registered route path");
|
|
5526
|
+
} catch {
|
|
5527
|
+
pathSpinner.warn("Could not auto-register path. Add it manually to src/router/paths.ts");
|
|
5528
|
+
}
|
|
5529
|
+
const routeSpinner = spinner("Registering frontend routes...");
|
|
5530
|
+
try {
|
|
5531
|
+
const routesPath = path8.join(frontendDir, "src", "router", "routes.tsx");
|
|
5532
|
+
insertBeforeMarker(
|
|
5533
|
+
routesPath,
|
|
5534
|
+
"// blacksmith:import",
|
|
5535
|
+
`import { ${names.names}Routes } from '@/pages/${names.kebabs}'`
|
|
5536
|
+
);
|
|
5537
|
+
insertBeforeMarker(
|
|
5538
|
+
routesPath,
|
|
5539
|
+
"// blacksmith:routes",
|
|
5540
|
+
` ...${names.names}Routes,`
|
|
5541
|
+
);
|
|
5542
|
+
routeSpinner.succeed("Registered frontend routes");
|
|
5543
|
+
} catch {
|
|
5544
|
+
routeSpinner.warn("Could not auto-register routes. Add them manually to src/router/routes.tsx");
|
|
5545
|
+
}
|
|
5189
5546
|
}
|
|
5190
5547
|
log.blank();
|
|
5191
5548
|
log.success(`Resource "${names.Name}" created successfully!`);
|
|
@@ -5202,35 +5559,41 @@ async function build() {
|
|
|
5202
5559
|
log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
|
|
5203
5560
|
process.exit(1);
|
|
5204
5561
|
}
|
|
5205
|
-
const
|
|
5206
|
-
const
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5562
|
+
const projectHasBackend = hasBackend(root);
|
|
5563
|
+
const projectHasFrontend = hasFrontend(root);
|
|
5564
|
+
if (projectHasFrontend) {
|
|
5565
|
+
const frontendDir = getFrontendDir(root);
|
|
5566
|
+
const frontendSpinner = spinner("Building frontend...");
|
|
5567
|
+
try {
|
|
5568
|
+
await exec("npm", ["run", "build"], { cwd: frontendDir, silent: true });
|
|
5569
|
+
frontendSpinner.succeed("Frontend built \u2192 dist/");
|
|
5570
|
+
} catch (error) {
|
|
5571
|
+
frontendSpinner.fail("Frontend build failed");
|
|
5572
|
+
log.error(error.message || error);
|
|
5573
|
+
process.exit(1);
|
|
5574
|
+
}
|
|
5215
5575
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5576
|
+
if (projectHasBackend) {
|
|
5577
|
+
const backendDir = getBackendDir(root);
|
|
5578
|
+
const backendSpinner = spinner("Collecting static files...");
|
|
5579
|
+
try {
|
|
5580
|
+
await execPython(
|
|
5581
|
+
["manage.py", "collectstatic", "--noinput"],
|
|
5582
|
+
backendDir,
|
|
5583
|
+
true
|
|
5584
|
+
);
|
|
5585
|
+
backendSpinner.succeed("Static files collected");
|
|
5586
|
+
} catch (error) {
|
|
5587
|
+
backendSpinner.fail("Failed to collect static files");
|
|
5588
|
+
log.error(error.message || error);
|
|
5589
|
+
process.exit(1);
|
|
5590
|
+
}
|
|
5228
5591
|
}
|
|
5229
5592
|
log.blank();
|
|
5230
5593
|
log.success("Production build complete!");
|
|
5231
5594
|
log.blank();
|
|
5232
|
-
log.step("Frontend assets:
|
|
5233
|
-
log.step("Backend ready for deployment");
|
|
5595
|
+
if (projectHasFrontend) log.step("Frontend assets: dist/");
|
|
5596
|
+
if (projectHasBackend) log.step("Backend ready for deployment");
|
|
5234
5597
|
log.blank();
|
|
5235
5598
|
}
|
|
5236
5599
|
|
|
@@ -5246,20 +5609,34 @@ async function eject() {
|
|
|
5246
5609
|
log.error("Not inside a Blacksmith project.");
|
|
5247
5610
|
process.exit(1);
|
|
5248
5611
|
}
|
|
5612
|
+
const config = loadConfig(root);
|
|
5613
|
+
const projectType = config.type || "fullstack";
|
|
5249
5614
|
const configPath = path9.join(root, "blacksmith.config.json");
|
|
5250
5615
|
if (fs7.existsSync(configPath)) {
|
|
5251
5616
|
fs7.unlinkSync(configPath);
|
|
5252
5617
|
}
|
|
5253
5618
|
log.success("Blacksmith has been ejected.");
|
|
5254
5619
|
log.blank();
|
|
5255
|
-
|
|
5620
|
+
if (projectType === "fullstack") {
|
|
5621
|
+
log.step("Your project is now a standard Django + React project.");
|
|
5622
|
+
} else if (projectType === "backend") {
|
|
5623
|
+
log.step("Your project is now a standard Django project.");
|
|
5624
|
+
} else {
|
|
5625
|
+
log.step("Your project is now a standard React project.");
|
|
5626
|
+
}
|
|
5256
5627
|
log.step("All generated code remains in place and is fully owned by you.");
|
|
5257
5628
|
log.step("The blacksmith CLI commands will no longer work in this directory.");
|
|
5258
5629
|
log.blank();
|
|
5259
5630
|
log.info("To continue development without Blacksmith:");
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5631
|
+
if (projectType === "fullstack") {
|
|
5632
|
+
log.step("Backend: cd backend && ./venv/bin/python manage.py runserver");
|
|
5633
|
+
log.step("Frontend: cd frontend && npm run dev");
|
|
5634
|
+
log.step("Codegen: cd frontend && npx openapi-ts");
|
|
5635
|
+
} else if (projectType === "backend") {
|
|
5636
|
+
log.step("Start: ./venv/bin/python manage.py runserver");
|
|
5637
|
+
} else {
|
|
5638
|
+
log.step("Start: npm run dev");
|
|
5639
|
+
}
|
|
5263
5640
|
log.blank();
|
|
5264
5641
|
}
|
|
5265
5642
|
|
|
@@ -5271,7 +5648,9 @@ var allSkills = [
|
|
|
5271
5648
|
djangoSkill,
|
|
5272
5649
|
djangoRestAdvancedSkill,
|
|
5273
5650
|
apiDocumentationSkill,
|
|
5651
|
+
backendModularizationSkill,
|
|
5274
5652
|
reactSkill,
|
|
5653
|
+
frontendModularizationSkill,
|
|
5275
5654
|
chakraUiReactSkill,
|
|
5276
5655
|
chakraUiFormsSkill,
|
|
5277
5656
|
chakraUiAuthSkill,
|
|
@@ -5293,7 +5672,8 @@ async function setupSkills(options) {
|
|
|
5293
5672
|
await setupAiDev({
|
|
5294
5673
|
projectDir: root,
|
|
5295
5674
|
projectName: config.name,
|
|
5296
|
-
includeChakraUiSkill: options.chakraUiSkill !== false
|
|
5675
|
+
includeChakraUiSkill: options.chakraUiSkill !== false,
|
|
5676
|
+
projectType: getProjectType(root)
|
|
5297
5677
|
});
|
|
5298
5678
|
log.blank();
|
|
5299
5679
|
log.success("AI skills generated:");
|
|
@@ -5657,6 +6037,10 @@ async function backend(args) {
|
|
|
5657
6037
|
log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
|
|
5658
6038
|
process.exit(1);
|
|
5659
6039
|
}
|
|
6040
|
+
if (!hasBackend(root)) {
|
|
6041
|
+
log.error('This is a frontend-only project. The "backend" command is not available.');
|
|
6042
|
+
process.exit(1);
|
|
6043
|
+
}
|
|
5660
6044
|
if (args.length === 0) {
|
|
5661
6045
|
log.error("Please provide a Django management command.");
|
|
5662
6046
|
log.step("Usage: blacksmith backend <command> [args...]");
|
|
@@ -5681,6 +6065,10 @@ async function frontend(args) {
|
|
|
5681
6065
|
log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
|
|
5682
6066
|
process.exit(1);
|
|
5683
6067
|
}
|
|
6068
|
+
if (!hasFrontend(root)) {
|
|
6069
|
+
log.error('This is a backend-only project. The "frontend" command is not available.');
|
|
6070
|
+
process.exit(1);
|
|
6071
|
+
}
|
|
5684
6072
|
if (args.length === 0) {
|
|
5685
6073
|
log.error("Please provide an npm command.");
|
|
5686
6074
|
log.step("Usage: blacksmith frontend <command> [args...]");
|
|
@@ -5700,7 +6088,7 @@ var program = new Command();
|
|
|
5700
6088
|
program.name("blacksmith").description("Fullstack Django + React framework").version("0.1.0").hook("preAction", () => {
|
|
5701
6089
|
banner();
|
|
5702
6090
|
});
|
|
5703
|
-
program.command("init").argument("[name]", "Project name").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-chakra-ui-skill", "Disable Chakra UI skill when using --ai").option("-b, --backend-port <port>", "Django backend port (default: 8000)").option("-f, --frontend-port <port>", "Vite frontend port (default: 5173)").option("-t, --theme-color <color>", "Theme color (zinc, slate, blue, green, orange, red, violet)").description("Create a new Blacksmith project").action(init);
|
|
6091
|
+
program.command("init").argument("[name]", "Project name").option("--type <type>", "Project type: fullstack, backend, or frontend (default: fullstack)").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-chakra-ui-skill", "Disable Chakra UI skill when using --ai").option("-b, --backend-port <port>", "Django backend port (default: 8000)").option("-f, --frontend-port <port>", "Vite frontend port (default: 5173)").option("-t, --theme-color <color>", "Theme color (zinc, slate, blue, green, orange, red, violet)").description("Create a new Blacksmith project").action(init);
|
|
5704
6092
|
program.command("dev").description("Start development servers (Django + Vite + OpenAPI sync)").action(dev);
|
|
5705
6093
|
program.command("sync").description("Sync OpenAPI schema to frontend types, schemas, and hooks").action(sync);
|
|
5706
6094
|
program.command("make:resource").argument("<name>", "Resource name (PascalCase, e.g. BlogPost)").description("Create a new resource (model, serializer, viewset, hooks, pages)").action(makeResource);
|