blacksmith-cli 0.1.7 → 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 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, backendPort = 8e3, frontendPort = 5173) {
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 servers")}`);
868
+ console.log(` ${chalk.cyan("blacksmith dev")} ${chalk.dim("# Start development server" + (projectType === "fullstack" ? "s" : ""))}`);
869
869
  console.log();
870
- console.log(chalk.dim(` Django: http://localhost:${backendPort}`));
871
- console.log(chalk.dim(` React: http://localhost:${frontendPort}`));
872
- console.log(chalk.dim(` Swagger: http://localhost:${backendPort}/api/docs/`));
873
- console.log(chalk.dim(` ReDoc: http://localhost:${backendPort}/api/redoc/`));
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
- return path3.join(root, "backend");
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
- return path3.join(root, "frontend");
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
- - A page file should be ~20-30 lines: import components, call hooks, compose JSX
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. Follow the Page/Feature Folder Structure
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 fullstack web application built with **Django** (backend) and **React** (frontend), scaffolded by **Blacksmith CLI**.
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 \u2502 \u2514\u2500\u2500 settings/ # Split settings (base, development, production)
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 (auto-generated from OpenAPI)
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 with guards
1117
- \u2502 \u2502 \u251C\u2500\u2500 shared/ # Shared components and hooks
1118
- \u2502 \u2502 \u2514\u2500\u2500 styles/ # Global styles (Tailwind)
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 # This file
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
- - \`blacksmith dev\` \u2014 Start Django + Vite + OpenAPI sync in parallel
1128
- - \`blacksmith sync\` \u2014 Regenerate frontend API types from Django OpenAPI schema
1129
- - \`blacksmith make:resource <Name>\` \u2014 Scaffold a full resource (model, serializer, viewset, hooks, pages)
1130
- - \`blacksmith build\` \u2014 Production build (frontend + collectstatic)
1131
- - \`blacksmith eject\` \u2014 Remove Blacksmith, keep a clean Django + React project
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
- 1. Define models in \`backend/apps/<app>/models.py\`
1136
- 2. Create serializers in \`backend/apps/<app>/serializers.py\`
1137
- 3. Add viewsets in \`backend/apps/<app>/views.py\` and register URLs in \`backend/apps/<app>/urls.py\`
1138
- 4. Run \`blacksmith sync\` to generate TypeScript types and API client
1139
- 5. Build frontend features using generated hooks in \`frontend/src/features/\`
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
- When building hooks for a resource, create two files:
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
- **\`use-<resources>.ts\`** \u2014 List query hook:
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. **Use generated options/mutations** from \`@/api/generated/@tanstack/react-query.gen\` \u2014 never write \`queryFn\` manually
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 (LoginForm, RegisterForm, etc.) are custom implementations in \`frontend/src/features/auth/\`.
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, AuthState } from '@/features/auth/types'
3425
+ import type { User } from '@/features/auth/types'
3166
3426
  \`\`\`
3167
3427
 
3168
- ### Context & Provider
3428
+ ### useAuth() Hook
3169
3429
 
3170
- - \`AuthProvider\` \u2014 Context provider wrapping the app. Manages auth state, token storage, and session lifecycle.
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
- ### Types
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
- interface User {
3178
- id: string
3179
- email: string
3180
- displayName?: string
3181
- avatar?: string
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
- interface AuthState {
3185
- user: User | null
3186
- loading: boolean
3187
- error: string | null
3188
- isAuthenticated: boolean
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
- ### Hooks
3471
+ **Login form integration:**
3472
+ \`\`\`tsx
3473
+ import { useAuth } from '@/features/auth/hooks/use-auth'
3193
3474
 
3194
- - \`useAuth()\` \u2014 Hook for auth state and actions
3195
- - Returns: \`user\`, \`loading\`, \`error\`, \`isAuthenticated\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)\`
3475
+ function useLoginForm() {
3476
+ const { signInWithEmail, error } = useAuth()
3477
+ const navigate = useNavigate()
3196
3478
 
3197
- ### Auth Pages
3479
+ const onSubmit = async (data: { email: string; password: string }) => {
3480
+ await signInWithEmail(data.email, data.password)
3481
+ navigate(Path.Dashboard)
3482
+ }
3198
3483
 
3199
- Auth pages are custom implementations in \`frontend/src/features/auth/pages/\`:
3484
+ return { onSubmit, error }
3485
+ }
3486
+ \`\`\`
3200
3487
 
3201
- - \`LoginPage\` \u2014 Login form with email/password, validation, and navigation links
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
- Each auth page uses Chakra UI form components (\`FormControl\`, \`FormLabel\`, \`Input\`, \`Button\`, etc.) with React Hook Form + Zod validation.
3490
+ \`AuthProvider\` wraps the app (inside \`ChakraProvider\`) and manages token storage and session lifecycle:
3207
3491
 
3208
- ### Adapter
3492
+ \`\`\`tsx
3493
+ // app.tsx
3494
+ import { AuthProvider } from '@/features/auth/context'
3209
3495
 
3210
- - \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter in \`frontend/src/features/auth/adapter.ts\`)
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. Use \`useAuth()\` hook.
3214
- - Auth pages live in \`frontend/src/features/auth/pages/\` and use Chakra UI components.
3215
- - Use the \`Path\` enum for auth route paths (\`Path.Login\`, \`Path.Register\`, etc.).
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 (local implementations) and Chakra UI built-in hooks for state, UI, layout, and responsiveness.",
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
- A combination of local custom hooks and Chakra UI built-in hooks for common UI patterns.
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
- \`\`\`tsx
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
- ### Custom Local Hooks
3555
+ ### Scaffolded Project Hooks
3256
3556
 
3257
- These are implemented locally in the project (e.g., in \`frontend/src/shared/hooks/\`):
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
- \`\`\`tsx
3260
- import { useDebounce } from '@/shared/hooks/use-debounce'
3261
- import { useLocalStorage } from '@/shared/hooks/use-local-storage'
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
- | Hook | Description |
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 (custom hook):**
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 SearchPage({ items }) {
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
- return (
3325
- <Input
3326
- value={query}
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 with Chakra hook:**
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
- const isMobile = useBreakpointValue({ base: true, md: false })
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 Django + Vite + OpenAPI watcher in parallel |
3389
- | \`blacksmith sync\` | Regenerate frontend API client from Django OpenAPI schema |
3390
- | \`blacksmith make:resource <Name>\` | Scaffold a full CRUD resource across backend and frontend |
3391
- | \`blacksmith build\` | Production build (Vite build + Django collectstatic) |
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
- - **Ports** are read by \`blacksmith dev\` and \`blacksmith sync\` \u2014 change them here, not in code
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
- Runs three concurrent processes:
3415
- 1. **Django** \u2014 \`./venv/bin/python manage.py runserver 0.0.0.0:<backend-port>\`
3416
- 2. **Vite** \u2014 \`npm run dev\` in the frontend directory
3417
- 3. **OpenAPI watcher** \u2014 watches \`.py\` files in backend, runs \`npx openapi-ts\` on changes (2s debounce)
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 three are managed by \`concurrently\` and stop together on Ctrl+C.
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 generates:
3676
+ Given a PascalCase name (e.g. \`BlogPost\`), it scaffolds based on project type:
3424
3677
 
3425
- **Backend:**
3426
- - \`backend/apps/blog_posts/models.py\` \u2014 Django model with timestamps
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
- - \`frontend/src/features/blog-posts/\` \u2014 Feature module with hooks and components
3436
- - \`frontend/src/pages/blog-posts/\` \u2014 List and detail pages
3437
- - Registers route path in \`frontend/src/router/paths.ts\` (\`Path\` enum)
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
- Then runs \`blacksmith sync\` to generate the TypeScript API client.
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. Fetches the OpenAPI schema from \`http://localhost:<backend-port>/api/schema/\`
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: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the Chakra UI design language.",
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
- > Chakra UI follows a clean design language similar to Anthropic, Apple, Linear, Vercel, and OpenAI \u2014 minimal chrome, generous whitespace, subtle depth, and purposeful motion. Every UI you build must conform to this standard.
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. Use solid colors, subtle borders, and minimal \`shadow-sm\` / \`shadow-md\` only where elevation is meaningful (cards, dropdowns, modals).
3485
- 2. **Content over decoration** \u2014 UI exists to present content, not to look busy. Remove any element that doesn't serve the user. If a section looks empty, the content is the problem \u2014 not the lack of decorative elements.
3486
- 3. **Whitespace is a feature** \u2014 Generous padding and margins create hierarchy and breathing room. Cramped UIs feel cheap. When in doubt, add more space.
3487
- 4. **Consistency over creativity** \u2014 Every page should feel like part of the same app. Use the same spacing scale, the same component patterns, the same interaction behaviors everywhere.
3488
- 5. **Progressive disclosure** \u2014 Show only what's needed at each level. Use expandable sections, tabs, dialogs, and drill-down navigation to manage complexity. Don't overwhelm with everything at once.
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 System
3739
+ ### Spacing
3491
3740
 
3492
- Use Chakra UI's spacing scale consistently. Chakra uses a numeric spacing scale (1 = 4px, 2 = 8px, etc.).
3741
+ Use Chakra UI's numeric spacing scale consistently (1 = 4px):
3493
3742
 
3494
- | Scale | Value | Use for |
3495
- |-------|-------|---------|
3496
- | \`1\`\u2013\`2\` | 4\u20138px | Inline gaps, icon-to-text spacing, tight badge padding |
3497
- | \`3\`\u2013\`4\` | 12\u201316px | Inner component padding, gap between related items |
3498
- | \`5\`\u2013\`6\` | 20\u201324px | Card padding, section inner spacing |
3499
- | \`8\` | 32px | Gap between sections within a page |
3500
- | \`10\`\u2013\`12\` | 40\u201348px | Gap between major page sections |
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
- **Rules:**
3504
- - Use \`spacing\` (via \`VStack\`, \`HStack\`, \`SimpleGrid\`) for spacing between siblings \u2014 not margin on individual items
3505
- - Use \`VStack spacing={...}\` for vertical rhythm within a section
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. The main heading. |
3518
- | Section title | \`<Heading as="h2" size="xl">\` | Major sections within a page |
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/label | \`<Text fontSize="sm" color="gray.500">\` | Secondary info, metadata, timestamps |
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
- **Rules:**
3526
- - One \`h1\` per page \u2014 it's the page title
3527
- - Headings should never skip levels (h1 -> h3 without h2)
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's theme-aware color tokens, never hardcoded colors.
3772
+ Use Chakra UI theme tokens \u2014 never hardcoded hex/rgb values:
3536
3773
 
3537
- **Semantic palette (via colorScheme and theme tokens):**
3538
- | Token | Usage |
3539
- |-------|-------|
3540
- | \`blue\` (colorScheme) | Primary actions (buttons, links, active states) |
3541
- | \`gray\` (colorScheme) | Secondary actions, subtle backgrounds |
3542
- | \`red\` (colorScheme) | Delete, error, danger states |
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
- **Rules:**
3547
- - Use \`useColorModeValue()\` for any colors that need to adapt between light and dark mode
3548
- - Status colors: use \`Badge\` with \`colorScheme\` (\`green\`, \`red\`, \`blue\`, \`gray\`) \u2014 don't hand-roll colored pills.
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
- > **CRITICAL: Every screen, component, and custom style MUST look correct in both light and dark mode. No exceptions.**
3786
+ ### Dark Mode
3680
3787
 
3681
- Chakra UI supports color mode via \`useColorMode()\` and \`useColorModeValue()\`. All built-in components automatically adapt.
3788
+ > **Every screen and component MUST render correctly in both light and dark mode.**
3682
3789
 
3683
- **Rules:**
3684
- - Use \`useColorModeValue()\` for any custom colors that need to differ between modes
3685
- - NEVER hardcode colors that only work in one mode. Use theme tokens or \`useColorModeValue()\`.
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
- - **Hover states**: Subtle background change \u2014 Chakra components handle this automatically
3692
- - **Focus**: Chakra components include accessible focus rings by default
3693
- - **Loading feedback**: Show \`Spinner\` on buttons via \`isLoading\` prop. Use \`Skeleton\` for content areas. Never leave the user without feedback during loading
3694
- - **Success/error feedback**: Use \`useToast()\` for transient confirmations. Use \`Alert\` for persistent messages. Never use \`window.alert()\`
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's responsive props use mobile-first breakpoints
3700
- - **Breakpoints**: \`sm\` (480px), \`md\` (768px), \`lg\` (992px), \`xl\` (1280px), \`2xl\` (1536px)
3701
- - **Responsive props**: \`columns={{ base: 1, md: 2, lg: 3 }}\` \u2014 single column on mobile, expand on larger screens
3702
- - **Hide/show**: Use Chakra's \`Show\` and \`Hide\` components or \`display={{ base: 'none', md: 'block' }}\`
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
- ### Anti-Patterns \u2014 NEVER Do These
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
- ### Always Use \`renderWithProviders\`
4487
+ ### Test Rendering Setup
4228
4488
 
4229
- > **RULE: Never import \`render\` from \`@testing-library/react\` directly. Always use \`renderWithProviders\` from \`@/__tests__/test-utils\`.**
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
- \`renderWithProviders\` wraps components with all app providers (ChakraProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
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, waitFor } from '@/__tests__/test-utils'
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 model, serializer, viewset, URLs, hooks, components, and pages across both backend and frontend
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 \`frontend/src/api/generated/\` \u2014 they are overwritten on every sync
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/\`. A page file should be ~20-30 lines, not a monolith
4522
- - **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings like \`'/login'\` or \`'/dashboard'\`
4523
- - **Add new paths to the enum** \u2014 when creating a new page, add its path to the \`Path\` enum before the \`// blacksmith:path\` marker
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
- - Backend: \`http://localhost:8000\`
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: \`cd backend && ./venv/bin/python manage.py test\`
4534
- 2. Frontend tests pass: \`cd frontend && npm test\`
4535
- 3. Frontend builds: \`cd frontend && npm run build\`
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. New routes have a corresponding \`Path\` enum entry
4543
- 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 (see the \`frontend-testing\` skill)
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 (includeChakraUiSkill) {
4563
- skills.push(chakraUiReactSkill);
4564
- skills.push(chakraUiFormsSkill);
4565
- skills.push(chakraUiAuthSkill);
4566
- skills.push(blacksmithHooksSkill);
4567
- skills.push(uiDesignSkill);
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
- if (!options.backendPort) {
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
- printConfig({
4659
- "Project": name,
4660
- "Backend": `Django on :${backendPort}`,
4661
- "Frontend": `React on :${frontendPort}`,
4662
- "Theme": themePreset,
4663
- "AI support": options.ai ? "Yes" : "No"
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
- const hasPython = await commandExists("python3");
4675
- const hasNode = await commandExists("node");
4676
- const hasNpm = await commandExists("npm");
4677
- if (!hasPython) {
4678
- checkSpinner.fail("Python 3 is required but not found. Install it from https://python.org");
4679
- process.exit(1);
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 (!hasNode || !hasNpm) {
4682
- checkSpinner.fail("Node.js and npm are required but not found. Install from https://nodejs.org");
4683
- process.exit(1);
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
- checkSpinner.succeed("Prerequisites OK (Python 3, Node.js, npm)");
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
- const backendSpinner = spinner("Generating Django backend...");
4707
- try {
4708
- renderDirectory(
4709
- path5.join(templatesDir, "backend"),
4710
- backendDir,
4711
- context
4712
- );
4713
- fs4.copyFileSync(
4714
- path5.join(backendDir, ".env.example"),
4715
- path5.join(backendDir, ".env")
4716
- );
4717
- backendSpinner.succeed("Django backend generated");
4718
- } catch (error) {
4719
- backendSpinner.fail("Failed to generate backend");
4720
- log.error(error.message);
4721
- process.exit(1);
4722
- }
4723
- const venvSpinner = spinner("Creating Python virtual environment...");
4724
- try {
4725
- await exec("python3", ["-m", "venv", "venv"], { cwd: backendDir, silent: true });
4726
- venvSpinner.succeed("Virtual environment created");
4727
- } catch (error) {
4728
- venvSpinner.fail("Failed to create virtual environment");
4729
- log.error(error.message);
4730
- process.exit(1);
4731
- }
4732
- const pipSpinner = spinner("Installing Python dependencies...");
4733
- try {
4734
- await execPip(
4735
- ["install", "-r", "requirements.txt"],
4736
- backendDir,
4737
- true
4738
- );
4739
- pipSpinner.succeed("Python dependencies installed");
4740
- } catch (error) {
4741
- pipSpinner.fail("Failed to install Python dependencies");
4742
- log.error(error.message);
4743
- process.exit(1);
4744
- }
4745
- const migrateSpinner = spinner("Running initial migrations...");
4746
- try {
4747
- await execPython(["manage.py", "makemigrations", "users"], backendDir, true);
4748
- await execPython(["manage.py", "migrate"], backendDir, true);
4749
- migrateSpinner.succeed("Database migrated");
4750
- } catch (error) {
4751
- migrateSpinner.fail("Failed to run migrations");
4752
- log.error(error.message);
4753
- process.exit(1);
4754
- }
4755
- const frontendSpinner = spinner("Generating React frontend...");
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
- const syncSpinner = spinner("Running initial OpenAPI sync...");
4778
- try {
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
- await exec(process.execPath, [path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
4792
- syncSpinner.succeed("OpenAPI types synced");
4793
- } catch {
4794
- syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
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
- if (djangoProcess.pid) {
4798
- process.kill(-djangoProcess.pid);
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
- } catch {
4803
- syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
4804
- }
4805
- const generatedDir = path5.join(frontendDir, "src", "api", "generated");
4806
- const stubFile = path5.join(generatedDir, "client.gen.ts");
4807
- if (!fs4.existsSync(stubFile)) {
4808
- if (!fs4.existsSync(generatedDir)) {
4809
- fs4.mkdirSync(generatedDir, { recursive: true });
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 backendDir = getBackendDir(root);
4875
- const frontendDir = getFrontendDir(root);
5193
+ const projectHasBackend = hasBackend(root);
5194
+ const projectHasFrontend = hasFrontend(root);
4876
5195
  let backendPort;
4877
5196
  let frontendPort;
4878
5197
  try {
4879
- ;
4880
- [backendPort, frontendPort] = await Promise.all([
4881
- findAvailablePort(config.backend.port),
4882
- findAvailablePort(config.frontend.port)
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
- if (backendPort !== config.backend.port) {
4889
- log.step(`Backend port ${config.backend.port} in use, using ${backendPort}`);
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 (frontendPort !== config.frontend.port) {
4892
- log.step(`Frontend port ${config.frontend.port} in use, using ${frontendPort}`);
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 syncCmd = `${process.execPath} ${path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
4902
- const watcherCode = [
4903
- `const{watch}=require("fs"),{exec}=require("child_process");`,
4904
- `let t=null,s=false;`,
4905
- `watch(${JSON.stringify(backendDir)},{recursive:true},(e,f)=>{`,
4906
- `if(!f||!f.endsWith(".py"))return;`,
4907
- `if(f.startsWith("venv/")||f.includes("__pycache__")||f.includes("/migrations/"))return;`,
4908
- `if(t)clearTimeout(t);`,
4909
- `t=setTimeout(()=>{`,
4910
- `if(s)return;s=true;`,
4911
- `console.log("Backend change detected \u2014 syncing OpenAPI types...");`,
4912
- `exec(${JSON.stringify(syncCmd)},{cwd:${JSON.stringify(frontendDir)}},(err,o,se)=>{`,
4913
- `s=false;`,
4914
- `if(err)console.error("Sync failed:",se||err.message);`,
4915
- `else console.log("OpenAPI types synced");`,
4916
- `})`,
4917
- `},2000)});`,
4918
- `console.log("Watching for .py changes...");`
4919
- ].join("");
4920
- const { result } = concurrently(
4921
- [
4922
- {
4923
- command: `./venv/bin/python manage.py runserver 0.0.0.0:${backendPort}`,
4924
- name: "django",
4925
- cwd: backendDir,
4926
- prefixColor: "green"
4927
- },
4928
- {
4929
- command: "npm run dev",
4930
- name: "vite",
4931
- cwd: frontendDir,
4932
- prefixColor: "blue"
4933
- },
4934
- {
4935
- command: `node -e '${watcherCode}'`,
4936
- name: "sync",
4937
- cwd: frontendDir,
4938
- prefixColor: "yellow"
4939
- }
4940
- ],
4941
- {
4942
- prefix: "name",
4943
- killOthers: ["failure"],
4944
- restartTries: 3
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 servers stopped.");
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 backendAppDir = path8.join(backendDir, "apps", names.snakes);
5049
- if (fs6.existsSync(backendAppDir)) {
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
- const backendSpinner = spinner(`Creating backend app: apps/${names.snakes}/`);
5060
- try {
5061
- renderDirectory(
5062
- path8.join(templatesDir, "resource", "backend"),
5063
- backendAppDir,
5064
- context
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
- const migrateSpinner = spinner("Running migrations...");
5101
- try {
5102
- await execPython(["manage.py", "makemigrations", names.snakes], backendDir, true);
5103
- await execPython(["manage.py", "migrate"], backendDir, true);
5104
- migrateSpinner.succeed("Migrations complete");
5105
- } catch (error) {
5106
- migrateSpinner.fail("Migration failed");
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
- const syncSpinner = spinner("Syncing OpenAPI schema...");
5111
- try {
5112
- const schemaPath = path8.join(frontendDir, "_schema.yml");
5113
- await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
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
- await exec(process.execPath, [path8.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
5123
- cwd: frontendDir,
5124
- silent: true
5125
- });
5126
- } finally {
5127
- fs6.writeFileSync(configPath, configBackup, "utf-8");
5128
- if (fs6.existsSync(schemaPath)) fs6.unlinkSync(schemaPath);
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
- const pathSpinner = spinner("Registering route path...");
5162
- try {
5163
- const pathsFile = path8.join(frontendDir, "src", "router", "paths.ts");
5164
- insertBeforeMarker(
5165
- pathsFile,
5166
- "// blacksmith:path",
5167
- ` ${names.Names} = '/${names.kebabs}',`
5168
- );
5169
- pathSpinner.succeed("Registered route path");
5170
- } catch {
5171
- pathSpinner.warn("Could not auto-register path. Add it manually to frontend/src/router/paths.ts");
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
- const routeSpinner = spinner("Registering frontend routes...");
5174
- try {
5175
- const routesPath = path8.join(frontendDir, "src", "router", "routes.tsx");
5176
- insertBeforeMarker(
5177
- routesPath,
5178
- "// blacksmith:import",
5179
- `import { ${names.names}Routes } from '@/pages/${names.kebabs}'`
5180
- );
5181
- insertBeforeMarker(
5182
- routesPath,
5183
- "// blacksmith:routes",
5184
- ` ...${names.names}Routes,`
5185
- );
5186
- routeSpinner.succeed("Registered frontend routes");
5187
- } catch {
5188
- routeSpinner.warn("Could not auto-register routes. Add them manually to frontend/src/router/routes.tsx");
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 backendDir = getBackendDir(root);
5206
- const frontendDir = getFrontendDir(root);
5207
- const frontendSpinner = spinner("Building frontend...");
5208
- try {
5209
- await exec("npm", ["run", "build"], { cwd: frontendDir, silent: true });
5210
- frontendSpinner.succeed("Frontend built \u2192 frontend/dist/");
5211
- } catch (error) {
5212
- frontendSpinner.fail("Frontend build failed");
5213
- log.error(error.message || error);
5214
- process.exit(1);
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
- const backendSpinner = spinner("Collecting static files...");
5217
- try {
5218
- await execPython(
5219
- ["manage.py", "collectstatic", "--noinput"],
5220
- backendDir,
5221
- true
5222
- );
5223
- backendSpinner.succeed("Static files collected");
5224
- } catch (error) {
5225
- backendSpinner.fail("Failed to collect static files");
5226
- log.error(error.message || error);
5227
- process.exit(1);
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: frontend/dist/");
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
- log.step("Your project is now a standard Django + React project.");
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
- log.step("Backend: cd backend && ./venv/bin/python manage.py runserver");
5261
- log.step("Frontend: cd frontend && npm run dev");
5262
- log.step("Codegen: cd frontend && npx openapi-ts");
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);