blacksmith-cli 0.1.1

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.
Files changed (103) hide show
  1. package/README.md +210 -0
  2. package/bin/blacksmith.js +20 -0
  3. package/dist/index.js +4404 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/templates/backend/.env.example.hbs +10 -0
  7. package/src/templates/backend/apps/__init__.py.hbs +0 -0
  8. package/src/templates/backend/apps/users/__init__.py.hbs +0 -0
  9. package/src/templates/backend/apps/users/admin.py.hbs +26 -0
  10. package/src/templates/backend/apps/users/managers.py.hbs +25 -0
  11. package/src/templates/backend/apps/users/models.py.hbs +25 -0
  12. package/src/templates/backend/apps/users/serializers.py.hbs +94 -0
  13. package/src/templates/backend/apps/users/tests.py.hbs +47 -0
  14. package/src/templates/backend/apps/users/urls.py.hbs +10 -0
  15. package/src/templates/backend/apps/users/views.py.hbs +175 -0
  16. package/src/templates/backend/config/__init__.py.hbs +0 -0
  17. package/src/templates/backend/config/asgi.py.hbs +9 -0
  18. package/src/templates/backend/config/settings/__init__.py.hbs +13 -0
  19. package/src/templates/backend/config/settings/base.py.hbs +117 -0
  20. package/src/templates/backend/config/settings/development.py.hbs +19 -0
  21. package/src/templates/backend/config/settings/production.py.hbs +31 -0
  22. package/src/templates/backend/config/urls.py.hbs +26 -0
  23. package/src/templates/backend/config/wsgi.py.hbs +9 -0
  24. package/src/templates/backend/manage.py.hbs +22 -0
  25. package/src/templates/backend/requirements.txt.hbs +7 -0
  26. package/src/templates/frontend/.env.hbs +1 -0
  27. package/src/templates/frontend/index.html.hbs +13 -0
  28. package/src/templates/frontend/openapi-ts.config.ts.hbs +29 -0
  29. package/src/templates/frontend/package.json.hbs +44 -0
  30. package/src/templates/frontend/postcss.config.js.hbs +6 -0
  31. package/src/templates/frontend/src/api/client.ts.hbs +110 -0
  32. package/src/templates/frontend/src/api/generated/.gitkeep +0 -0
  33. package/src/templates/frontend/src/api/generated/client.gen.ts +13 -0
  34. package/src/templates/frontend/src/api/query-client.ts.hbs +22 -0
  35. package/src/templates/frontend/src/app.tsx.hbs +30 -0
  36. package/src/templates/frontend/src/features/auth/adapter.ts.hbs +198 -0
  37. package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +32 -0
  38. package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +27 -0
  39. package/src/templates/frontend/src/features/auth/index.ts.hbs +3 -0
  40. package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +37 -0
  41. package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +36 -0
  42. package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +36 -0
  43. package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +41 -0
  44. package/src/templates/frontend/src/features/auth/routes.tsx.hbs +13 -0
  45. package/src/templates/frontend/src/main.tsx.hbs +10 -0
  46. package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +36 -0
  47. package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +69 -0
  48. package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +14 -0
  49. package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +21 -0
  50. package/src/templates/frontend/src/pages/dashboard/index.ts.hbs +1 -0
  51. package/src/templates/frontend/src/pages/dashboard/routes.tsx.hbs +7 -0
  52. package/src/templates/frontend/src/pages/home/components/features-grid.tsx.hbs +88 -0
  53. package/src/templates/frontend/src/pages/home/components/getting-started.tsx.hbs +88 -0
  54. package/src/templates/frontend/src/pages/home/components/hero-section.tsx.hbs +47 -0
  55. package/src/templates/frontend/src/pages/home/components/resources-section.tsx.hbs +34 -0
  56. package/src/templates/frontend/src/pages/home/home.tsx.hbs +20 -0
  57. package/src/templates/frontend/src/pages/home/index.ts.hbs +1 -0
  58. package/src/templates/frontend/src/pages/home/routes.tsx.hbs +7 -0
  59. package/src/templates/frontend/src/router/auth-guard.tsx.hbs +57 -0
  60. package/src/templates/frontend/src/router/error-boundary.tsx.hbs +61 -0
  61. package/src/templates/frontend/src/router/index.tsx.hbs +12 -0
  62. package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +68 -0
  63. package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +137 -0
  64. package/src/templates/frontend/src/router/paths.ts.hbs +38 -0
  65. package/src/templates/frontend/src/router/routes.tsx.hbs +64 -0
  66. package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +20 -0
  67. package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +31 -0
  68. package/src/templates/frontend/src/shared/hooks/api-error.ts.hbs +147 -0
  69. package/src/templates/frontend/src/shared/hooks/use-api-mutation.ts.hbs +88 -0
  70. package/src/templates/frontend/src/shared/hooks/use-api-query.ts.hbs +66 -0
  71. package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +10 -0
  72. package/src/templates/frontend/src/styles/globals.css.hbs +62 -0
  73. package/src/templates/frontend/src/vite-env.d.ts.hbs +1 -0
  74. package/src/templates/frontend/tailwind.config.js.hbs +73 -0
  75. package/src/templates/frontend/tsconfig.app.json.hbs +25 -0
  76. package/src/templates/frontend/tsconfig.json.hbs +7 -0
  77. package/src/templates/frontend/tsconfig.node.json.hbs +18 -0
  78. package/src/templates/frontend/vite.config.ts.hbs +21 -0
  79. package/src/templates/resource/backend/__init__.py.hbs +0 -0
  80. package/src/templates/resource/backend/admin.py.hbs +10 -0
  81. package/src/templates/resource/backend/models.py.hbs +24 -0
  82. package/src/templates/resource/backend/serializers.py.hbs +21 -0
  83. package/src/templates/resource/backend/tests.py.hbs +35 -0
  84. package/src/templates/resource/backend/urls.py.hbs +10 -0
  85. package/src/templates/resource/backend/views.py.hbs +32 -0
  86. package/src/templates/resource/frontend/components/{{kebab}}-card.tsx.hbs +39 -0
  87. package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +106 -0
  88. package/src/templates/resource/frontend/components/{{kebab}}-list.tsx.hbs +49 -0
  89. package/src/templates/resource/frontend/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
  90. package/src/templates/resource/frontend/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
  91. package/src/templates/resource/frontend/index.ts.hbs +6 -0
  92. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +33 -0
  93. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
  94. package/src/templates/resource/frontend/routes.tsx.hbs +15 -0
  95. package/src/templates/resource/pages/components/{{kebab}}-card.tsx.hbs +39 -0
  96. package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +106 -0
  97. package/src/templates/resource/pages/components/{{kebab}}-list.tsx.hbs +49 -0
  98. package/src/templates/resource/pages/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
  99. package/src/templates/resource/pages/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
  100. package/src/templates/resource/pages/index.ts.hbs +6 -0
  101. package/src/templates/resource/pages/routes.tsx.hbs +15 -0
  102. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +33 -0
  103. package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
package/dist/index.js ADDED
@@ -0,0 +1,4404 @@
1
+ // src/index.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/utils/logger.ts
5
+ import chalk from "chalk";
6
+ import ora from "ora";
7
+ import { createInterface } from "readline";
8
+ var log = {
9
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
10
+ success: (msg) => console.log(chalk.green("\u2713"), msg),
11
+ warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
12
+ error: (msg) => console.log(chalk.red("\u2717"), msg),
13
+ step: (msg) => console.log(chalk.cyan("\u2192"), msg),
14
+ blank: () => console.log()
15
+ };
16
+ function promptText(label, defaultValue) {
17
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
18
+ const def = defaultValue ? chalk.dim(` (${defaultValue})`) : "";
19
+ const question = ` ${chalk.cyan("?")} ${chalk.bold(label)}${def}${chalk.dim(":")} `;
20
+ return new Promise((resolve) => {
21
+ rl.question(question, (answer) => {
22
+ rl.close();
23
+ resolve(answer.trim() || defaultValue || "");
24
+ });
25
+ });
26
+ }
27
+ function promptYesNo(label, defaultValue = false) {
28
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
29
+ const hint = defaultValue ? chalk.dim(" (Y/n)") : chalk.dim(" (y/N)");
30
+ const question = ` ${chalk.cyan("?")} ${chalk.bold(label)}${hint}${chalk.dim(":")} `;
31
+ return new Promise((resolve) => {
32
+ rl.question(question, (answer) => {
33
+ rl.close();
34
+ const val = answer.trim().toLowerCase();
35
+ if (!val) return resolve(defaultValue);
36
+ resolve(["y", "yes"].includes(val));
37
+ });
38
+ });
39
+ }
40
+ function promptSelect(label, options, defaultValue) {
41
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
42
+ const optionList = options.map((opt, i) => `${chalk.dim(` ${i + 1}.`)} ${opt}`).join("\n");
43
+ const def = defaultValue ? chalk.dim(` (${defaultValue})`) : "";
44
+ const question = ` ${chalk.cyan("?")} ${chalk.bold(label)}${def}
45
+ ${optionList}
46
+ ${chalk.dim("Choice:")} `;
47
+ return new Promise((resolve) => {
48
+ rl.question(question, (answer) => {
49
+ rl.close();
50
+ const trimmed = answer.trim();
51
+ if (!trimmed && defaultValue) return resolve(defaultValue);
52
+ const index = parseInt(trimmed, 10);
53
+ if (index >= 1 && index <= options.length) return resolve(options[index - 1]);
54
+ const match = options.find((opt) => opt.toLowerCase() === trimmed.toLowerCase());
55
+ resolve(match || defaultValue || options[0]);
56
+ });
57
+ });
58
+ }
59
+ function printConfig(config) {
60
+ const bar = chalk.dim("\u2502");
61
+ console.log();
62
+ console.log(` ${chalk.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")}`);
63
+ console.log(` ${bar} ${chalk.bold.white("Configuration")}${" ".repeat(23)}${bar}`);
64
+ console.log(` ${chalk.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524")}`);
65
+ for (const [key, value] of Object.entries(config)) {
66
+ const padded = `${chalk.dim(key + ":")} ${chalk.white(value)}`;
67
+ const rawLen = `${key}: ${value}`.length;
68
+ const padding = " ".repeat(Math.max(1, 36 - rawLen));
69
+ console.log(` ${bar} ${padded}${padding}${bar}`);
70
+ }
71
+ console.log(` ${chalk.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")}`);
72
+ console.log();
73
+ }
74
+ function spinner(text) {
75
+ return ora({ text, color: "cyan" }).start();
76
+ }
77
+ function banner() {
78
+ const logo = [
79
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
80
+ " \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D",
81
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2554\u255D ",
82
+ " \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2588\u2588\u2557 ",
83
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2557",
84
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D",
85
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
86
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551",
87
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551",
88
+ " \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
89
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
90
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
91
+ ];
92
+ console.log();
93
+ for (const line of logo) {
94
+ console.log(chalk.cyan(line));
95
+ }
96
+ console.log();
97
+ console.log(chalk.dim(" Welcome to Blacksmith \u2014 forge fullstack apps with one command."));
98
+ console.log();
99
+ }
100
+ function printNextSteps(projectName, backendPort = 8e3, frontendPort = 5173) {
101
+ log.blank();
102
+ log.success("Project created successfully!");
103
+ log.blank();
104
+ console.log(chalk.bold(" Next steps:"));
105
+ console.log();
106
+ console.log(` ${chalk.cyan("cd")} ${projectName}`);
107
+ console.log(` ${chalk.cyan("blacksmith dev")} ${chalk.dim("# Start development servers")}`);
108
+ console.log();
109
+ console.log(chalk.dim(` Django: http://localhost:${backendPort}`));
110
+ console.log(chalk.dim(` React: http://localhost:${frontendPort}`));
111
+ console.log(chalk.dim(` Swagger: http://localhost:${backendPort}/api/docs/`));
112
+ console.log(chalk.dim(` ReDoc: http://localhost:${backendPort}/api/redoc/`));
113
+ log.blank();
114
+ }
115
+
116
+ // src/commands/init.ts
117
+ import path4 from "path";
118
+ import fs4 from "fs";
119
+ import { spawn } from "child_process";
120
+
121
+ // src/utils/template.ts
122
+ import fs from "fs";
123
+ import path from "path";
124
+ import Handlebars from "handlebars";
125
+ Handlebars.registerHelper("eq", (a, b) => a === b);
126
+ Handlebars.registerHelper("ne", (a, b) => a !== b);
127
+ Handlebars.registerHelper("upper", (str) => str?.toUpperCase());
128
+ Handlebars.registerHelper("lower", (str) => str?.toLowerCase());
129
+ function renderTemplate(templateStr, context) {
130
+ let safeStr = templateStr.replace(/\{(\s*)(?=\{\{[^{])/g, "BLACKSMITH_OB$1").replace(/([^}]\}\})(\s*)\}/g, "$1$2BLACKSMITH_CB");
131
+ const template = Handlebars.compile(safeStr, { noEscape: true });
132
+ const rendered = template(context);
133
+ return rendered.replace(/BLACKSMITH_OB/g, "{").replace(/BLACKSMITH_CB/g, "}");
134
+ }
135
+ function renderTemplateFile(templatePath, context) {
136
+ const templateStr = fs.readFileSync(templatePath, "utf-8");
137
+ return renderTemplate(templateStr, context);
138
+ }
139
+ function renderToFile(templatePath, destPath, context) {
140
+ const rendered = renderTemplateFile(templatePath, context);
141
+ const destDir = path.dirname(destPath);
142
+ if (!fs.existsSync(destDir)) {
143
+ fs.mkdirSync(destDir, { recursive: true });
144
+ }
145
+ fs.writeFileSync(destPath, rendered, "utf-8");
146
+ }
147
+ function renderDirectory(srcDir, destDir, context) {
148
+ if (!fs.existsSync(srcDir)) {
149
+ throw new Error(`Template directory not found: ${srcDir}`);
150
+ }
151
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
152
+ for (const entry of entries) {
153
+ const renderedName = renderTemplate(entry.name, context);
154
+ const srcPath = path.join(srcDir, entry.name);
155
+ if (entry.isDirectory()) {
156
+ const destSubDir = path.join(destDir, renderedName);
157
+ renderDirectory(srcPath, destSubDir, context);
158
+ } else if (entry.name.endsWith(".hbs")) {
159
+ const outputName = renderedName.replace(/\.hbs$/, "");
160
+ const destPath = path.join(destDir, outputName);
161
+ renderToFile(srcPath, destPath, context);
162
+ } else {
163
+ const destPath = path.join(destDir, renderedName);
164
+ const destDirPath = path.dirname(destPath);
165
+ if (!fs.existsSync(destDirPath)) {
166
+ fs.mkdirSync(destDirPath, { recursive: true });
167
+ }
168
+ fs.copyFileSync(srcPath, destPath);
169
+ }
170
+ }
171
+ }
172
+ function appendAfterMarker(filePath, marker, content) {
173
+ const fileContent = fs.readFileSync(filePath, "utf-8");
174
+ const lines = fileContent.split("\n");
175
+ const markerIndex = lines.findIndex((line) => line.includes(marker));
176
+ if (markerIndex === -1) {
177
+ throw new Error(`Marker "${marker}" not found in ${filePath}`);
178
+ }
179
+ lines.splice(markerIndex + 1, 0, content);
180
+ fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
181
+ }
182
+ function insertBeforeMarker(filePath, marker, content) {
183
+ const fileContent = fs.readFileSync(filePath, "utf-8");
184
+ const lines = fileContent.split("\n");
185
+ const markerIndex = lines.findIndex((line) => line.includes(marker));
186
+ if (markerIndex === -1) {
187
+ throw new Error(`Marker "${marker}" not found in ${filePath}`);
188
+ }
189
+ lines.splice(markerIndex, 0, content);
190
+ fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
191
+ }
192
+
193
+ // src/utils/exec.ts
194
+ import { execa } from "execa";
195
+ async function exec(command, args, options = {}) {
196
+ const { cwd, silent = false, env } = options;
197
+ try {
198
+ const result = await execa(command, args, {
199
+ cwd,
200
+ env: { ...process.env, ...env },
201
+ stdio: silent ? "pipe" : "inherit"
202
+ });
203
+ return result;
204
+ } catch (error) {
205
+ if (!silent) {
206
+ log.error(`Command failed: ${command} ${args.join(" ")}`);
207
+ if (error.stderr) {
208
+ log.error(error.stderr);
209
+ }
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+ async function commandExists(command) {
215
+ try {
216
+ await execa("which", [command], { stdio: "pipe" });
217
+ return true;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+ async function execPython(args, cwd, silent = false) {
223
+ const venvPython = `${cwd}/venv/bin/python`;
224
+ return exec(venvPython, args, { cwd, silent });
225
+ }
226
+ async function execPip(args, cwd, silent = false) {
227
+ const venvPip = `${cwd}/venv/bin/pip`;
228
+ return exec(venvPip, args, { cwd, silent });
229
+ }
230
+
231
+ // src/utils/paths.ts
232
+ import path2 from "path";
233
+ import fs2 from "fs";
234
+ import { fileURLToPath } from "url";
235
+ var __filename2 = fileURLToPath(import.meta.url);
236
+ var __dirname2 = path2.dirname(__filename2);
237
+ function getTemplatesDir() {
238
+ const devPath = path2.resolve(__dirname2, "..", "templates");
239
+ const prodPath = path2.resolve(__dirname2, "..", "src", "templates");
240
+ if (fs2.existsSync(devPath)) return devPath;
241
+ if (fs2.existsSync(prodPath)) return prodPath;
242
+ throw new Error("Templates directory not found. Make sure the CLI is properly installed.");
243
+ }
244
+ function findProjectRoot(startDir) {
245
+ let dir = startDir || process.cwd();
246
+ while (dir !== path2.dirname(dir)) {
247
+ if (fs2.existsSync(path2.join(dir, "blacksmith.config.json"))) {
248
+ return dir;
249
+ }
250
+ dir = path2.dirname(dir);
251
+ }
252
+ throw new Error(
253
+ 'Not inside a Blacksmith project. Run "blacksmith init <name>" to create one, or navigate to an existing Blacksmith project.'
254
+ );
255
+ }
256
+ function getBackendDir(projectRoot) {
257
+ const root = projectRoot || findProjectRoot();
258
+ return path2.join(root, "backend");
259
+ }
260
+ function getFrontendDir(projectRoot) {
261
+ const root = projectRoot || findProjectRoot();
262
+ return path2.join(root, "frontend");
263
+ }
264
+ function loadConfig(projectRoot) {
265
+ const root = projectRoot || findProjectRoot();
266
+ const configPath = path2.join(root, "blacksmith.config.json");
267
+ return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
268
+ }
269
+
270
+ // src/commands/ai-setup.ts
271
+ import path3 from "path";
272
+ import fs3 from "fs";
273
+
274
+ // src/skills/core-rules.ts
275
+ var coreRulesSkill = {
276
+ id: "core-rules",
277
+ // No `name` → content is inlined directly into CLAUDE.md, not a separate file
278
+ render(_ctx) {
279
+ return `## Critical Rules
280
+
281
+ > **These rules are mandatory. Violating them produces broken, inconsistent code.**
282
+
283
+ ### 1. Use \`@blacksmith-ui/react\` for ALL UI
284
+ - **Layout**: Use \`Stack\`, \`Flex\`, \`Grid\`, \`Box\`, \`Container\` \u2014 NEVER \`<div className="flex ...">\` or \`<div className="grid ...">\`
285
+ - **Typography**: Use \`Typography\` and \`Text\` \u2014 NEVER raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, or \`<span>\` with text classes
286
+ - **Separators**: Use \`Divider\` \u2014 NEVER \`<hr>\` or \`<Separator>\`
287
+ - **Everything else**: \`Button\`, \`Card\`, \`Badge\`, \`Input\`, \`Table\`, \`Dialog\`, \`Alert\`, \`Skeleton\`, \`EmptyState\`, \`StatCard\`, etc.
288
+ - See the \`blacksmith-ui-react\` skill for the full 60+ component list
289
+
290
+ ### 2. Pages Are Thin Orchestrators
291
+ - A page file should be ~20-30 lines: import components, call hooks, compose JSX
292
+ - Break every page into child components in a \`components/\` folder
293
+ - See the \`page-structure\` skill for the full pattern with examples
294
+
295
+ ### 3. Components Render, Hooks Think
296
+ - Extract ALL logic into hooks in a \`hooks/\` folder \u2014 API calls, mutations, form setup, filtering, pagination, debouncing, computed state
297
+ - Components should contain only JSX composition, prop passing, and simple event handler wiring
298
+ - The only \`useState\` acceptable inline in a component is a simple UI toggle (e.g. modal open/close)
299
+ - If a component has more than one \`useState\`, one \`useEffect\`, or any \`useApiQuery\`/\`useApiMutation\` \u2014 extract to a hook
300
+
301
+ ### 4. Use the \`Path\` Enum \u2014 Never Hardcode Paths
302
+ - All route paths are in \`src/router/paths.ts\` as a \`Path\` enum
303
+ - Use \`Path.Login\`, \`Path.Dashboard\`, etc. in \`navigate()\`, \`<Link to={}>\`, and route definitions
304
+ - When adding a new page, add its path to the enum before \`// blacksmith:path\`
305
+ - Use \`buildPath(Path.ResetPassword, { token })\` for dynamic segments
306
+
307
+ ### 5. Follow the Page/Feature Folder Structure
308
+ \`\`\`
309
+ pages/<page>/
310
+ \u251C\u2500\u2500 <page>.tsx # Thin orchestrator (default export)
311
+ \u251C\u2500\u2500 routes.tsx # RouteObject[] using Path enum
312
+ \u251C\u2500\u2500 index.ts # Re-exports public API
313
+ \u251C\u2500\u2500 components/ # Child components
314
+ \u2514\u2500\u2500 hooks/ # Data hooks
315
+ \`\`\`
316
+ - See the \`page-structure\` skill for full conventions
317
+ `;
318
+ }
319
+ };
320
+
321
+ // src/skills/project-overview.ts
322
+ var projectOverviewSkill = {
323
+ id: "project-overview",
324
+ name: "Project Overview",
325
+ description: "Overview of the project structure, commands, and development workflow.",
326
+ render(ctx) {
327
+ return `# ${ctx.projectName}
328
+
329
+ A fullstack web application built with **Django** (backend) and **React** (frontend), scaffolded by **Blacksmith CLI**.
330
+
331
+ ## Project Structure
332
+
333
+ \`\`\`
334
+ ${ctx.projectName}/
335
+ \u251C\u2500\u2500 backend/ # Django project
336
+ \u2502 \u251C\u2500\u2500 apps/ # Django apps (one per resource)
337
+ \u2502 \u2502 \u2514\u2500\u2500 users/ # Built-in user app
338
+ \u2502 \u251C\u2500\u2500 config/ # Django settings, urls, wsgi/asgi
339
+ \u2502 \u2502 \u2514\u2500\u2500 settings/ # Split settings (base, development, production)
340
+ \u2502 \u251C\u2500\u2500 manage.py
341
+ \u2502 \u251C\u2500\u2500 requirements.txt
342
+ \u2502 \u2514\u2500\u2500 venv/ # Python virtual environment
343
+ \u251C\u2500\u2500 frontend/ # React + Vite project
344
+ \u2502 \u251C\u2500\u2500 src/
345
+ \u2502 \u2502 \u251C\u2500\u2500 api/ # API client (auto-generated from OpenAPI)
346
+ \u2502 \u2502 \u251C\u2500\u2500 features/ # Feature modules (auth, etc.)
347
+ \u2502 \u2502 \u251C\u2500\u2500 pages/ # Top-level pages
348
+ \u2502 \u2502 \u251C\u2500\u2500 router/ # React Router setup with guards
349
+ \u2502 \u2502 \u251C\u2500\u2500 shared/ # Shared components and hooks
350
+ \u2502 \u2502 \u2514\u2500\u2500 styles/ # Global styles (Tailwind)
351
+ \u2502 \u251C\u2500\u2500 package.json
352
+ \u2502 \u2514\u2500\u2500 tailwind.config.js
353
+ \u251C\u2500\u2500 blacksmith.config.json
354
+ \u2514\u2500\u2500 CLAUDE.md # This file
355
+ \`\`\`
356
+
357
+ ## Commands
358
+
359
+ - \`blacksmith dev\` \u2014 Start Django + Vite + OpenAPI sync in parallel
360
+ - \`blacksmith sync\` \u2014 Regenerate frontend API types from Django OpenAPI schema
361
+ - \`blacksmith make:resource <Name>\` \u2014 Scaffold a full resource (model, serializer, viewset, hooks, pages)
362
+ - \`blacksmith build\` \u2014 Production build (frontend + collectstatic)
363
+ - \`blacksmith eject\` \u2014 Remove Blacksmith, keep a clean Django + React project
364
+
365
+ ## Development Workflow
366
+
367
+ 1. Define models in \`backend/apps/<app>/models.py\`
368
+ 2. Create serializers in \`backend/apps/<app>/serializers.py\`
369
+ 3. Add viewsets in \`backend/apps/<app>/views.py\` and register URLs in \`backend/apps/<app>/urls.py\`
370
+ 4. Run \`blacksmith sync\` to generate TypeScript types and API client
371
+ 5. Build frontend features using generated hooks in \`frontend/src/features/\`
372
+ `;
373
+ }
374
+ };
375
+
376
+ // src/skills/django.ts
377
+ var djangoSkill = {
378
+ id: "django",
379
+ name: "Django Backend Conventions",
380
+ description: "Models, serializers, views, URLs, settings, migrations, and testing patterns for the Django backend.",
381
+ render(_ctx) {
382
+ return `## Django Backend Conventions
383
+
384
+ ### Models
385
+ - Models live in \`backend/apps/<app>/models.py\`
386
+ - Use Django's ORM. Inherit from \`models.Model\`
387
+ - Use \`TimeStampedModel\` pattern: add \`created_at\` and \`updated_at\` fields with \`auto_now_add\` and \`auto_now\`
388
+ - Register models in \`backend/apps/<app>/admin.py\` for Django admin
389
+ - Use descriptive \`verbose_name\` and \`verbose_name_plural\` in \`Meta\`
390
+ - Define \`__str__\` on every model for readable admin and debugging output
391
+ - Use \`related_name\` on all ForeignKey and ManyToManyField declarations
392
+ - Prefer \`TextField\` over \`CharField\` when there is no strict max length requirement
393
+
394
+ ### Serializers
395
+ - Use Django REST Framework serializers in \`backend/apps/<app>/serializers.py\`
396
+ - Prefer \`ModelSerializer\` for standard CRUD operations
397
+ - Use \`serializers.Serializer\` for custom input/output that does not map to a model
398
+ - Add per-field validation via \`validate_<field>(self, value)\` methods
399
+ - Add cross-field validation via \`validate(self, attrs)\`
400
+ - Use \`SerializerMethodField\` for computed read-only fields
401
+ - Nest related serializers for read endpoints; use PrimaryKeyRelatedField for write endpoints
402
+ - Keep serializers thin \u2014 move business logic to model methods or service functions
403
+
404
+ ### Views
405
+ - Use DRF \`ModelViewSet\` for standard CRUD endpoints
406
+ - Use \`@action(detail=True|False)\` decorator for custom non-CRUD endpoints
407
+ - Apply permissions with \`permission_classes\` at the class or action level
408
+ - Use \`@extend_schema\` from \`drf-spectacular\` to document every endpoint \u2014 this powers the OpenAPI sync that generates frontend types
409
+ - Use \`filterset_fields\`, \`search_fields\`, and \`ordering_fields\` for queryable list endpoints
410
+ - Override \`get_queryset()\` to scope data to the current user when needed
411
+ - Override \`perform_create()\` to inject \`request.user\` or other context into the serializer save
412
+
413
+ ### URLs
414
+ - Each app has its own \`urls.py\` with a \`DefaultRouter\`
415
+ - Register viewsets on the router: \`router.register('resources', ResourceViewSet)\`
416
+ - App URLs are included in \`backend/config/urls.py\` under \`/api/\`
417
+ - URL pattern: \`/api/<resource>/\` (list/create), \`/api/<resource>/<id>/\` (retrieve/update/delete)
418
+
419
+ ### Settings
420
+ - Split settings: \`base.py\` (shared), \`development.py\` (local dev), \`production.py\` (deployment)
421
+ - Environment variables loaded from \`.env\` via \`django-environ\`
422
+ - Database: SQLite in development, configurable in production via \`DATABASE_URL\`
423
+ - \`INSTALLED_APPS\` is declared in \`base.py\` \u2014 add new apps there
424
+ - CORS, allowed hosts, and debug flags are environment-specific
425
+
426
+ ### Migrations
427
+ - Run \`./venv/bin/python manage.py makemigrations <app>\` after model changes
428
+ - Run \`./venv/bin/python manage.py migrate\` to apply
429
+ - Never edit auto-generated migration files unless resolving a conflict
430
+ - Use \`RunPython\` in data migrations for one-time data transformations
431
+
432
+ ### Testing
433
+ - Tests live in \`backend/apps/<app>/tests.py\` (or a \`tests/\` package for larger apps)
434
+ - Use \`APITestCase\` from DRF for API endpoint tests
435
+ - Use \`APIClient\` with \`force_authenticate(user)\` for authenticated requests
436
+ - Test both success and error paths (400, 401, 403, 404)
437
+ - Run all tests: \`cd backend && ./venv/bin/python manage.py test\`
438
+ - Run a single app: \`cd backend && ./venv/bin/python manage.py test apps.<app>\`
439
+
440
+ ### Adding a New App Manually
441
+ 1. Create the app directory under \`backend/apps/\` with \`__init__.py\`, \`models.py\`, \`views.py\`, \`serializers.py\`, \`urls.py\`, \`admin.py\`, \`tests.py\`
442
+ 2. Add \`'apps.<app>'\` to \`INSTALLED_APPS\` in \`backend/config/settings/base.py\`
443
+ 3. Include URLs in \`backend/config/urls.py\`: \`path('api/<app>/', include('apps.<app>.urls'))\`
444
+ 4. Run \`makemigrations\` and \`migrate\`
445
+ 5. Run \`blacksmith sync\` to update frontend types
446
+
447
+ ### Common Patterns
448
+ - **Soft delete**: Add an \`is_active\` BooleanField and override \`get_queryset()\` to filter
449
+ - **Pagination**: Configured globally in \`REST_FRAMEWORK\` settings \u2014 default is \`PageNumberPagination\`
450
+ - **Permissions**: Use \`IsAuthenticated\` as default; create custom permissions in \`permissions.py\`
451
+ - **Signals**: Use sparingly; prefer explicit calls in serializer/view logic
452
+ - **Management commands**: Place in \`backend/apps/<app>/management/commands/\` for CLI tasks
453
+ `;
454
+ }
455
+ };
456
+
457
+ // src/skills/django-rest-advanced.ts
458
+ var djangoRestAdvancedSkill = {
459
+ id: "django-rest-advanced",
460
+ name: "Advanced Django REST Framework",
461
+ description: "Senior-level DRF patterns: service layer, query optimization, custom permissions, filters, caching, and testing.",
462
+ render(_ctx) {
463
+ return `## Advanced Django REST Framework \u2014 Senior-Level Patterns
464
+
465
+ > **RULE: Follow these patterns for production-grade, scalable, and maintainable DRF APIs.**
466
+ > These build on top of the base Django conventions. Apply them when building non-trivial features.
467
+
468
+ ### Architecture: Service Layer Pattern
469
+
470
+ Keep views and serializers thin. Extract business logic into service modules.
471
+
472
+ \`\`\`
473
+ backend/apps/<app>/
474
+ \u251C\u2500\u2500 models.py # Data + model-level methods only
475
+ \u251C\u2500\u2500 serializers.py # Validation + representation only
476
+ \u251C\u2500\u2500 views.py # HTTP glue + permissions only
477
+ \u251C\u2500\u2500 services.py # Business logic lives here
478
+ \u251C\u2500\u2500 selectors.py # Complex read queries
479
+ \u251C\u2500\u2500 permissions.py # Custom permission classes
480
+ \u251C\u2500\u2500 filters.py # Custom filter backends
481
+ \u251C\u2500\u2500 signals.py # Signal handlers (use sparingly)
482
+ \u251C\u2500\u2500 tasks.py # Celery/background tasks
483
+ \u2514\u2500\u2500 tests/
484
+ \u251C\u2500\u2500 test_views.py
485
+ \u251C\u2500\u2500 test_services.py
486
+ \u2514\u2500\u2500 test_selectors.py
487
+ \`\`\`
488
+
489
+ \`\`\`python
490
+ # services.py \u2014 Business logic
491
+ from django.db import transaction
492
+
493
+ class OrderService:
494
+ @staticmethod
495
+ @transaction.atomic
496
+ def place_order(*, user, items, shipping_address):
497
+ """Place an order with inventory validation and payment."""
498
+ order = Order.objects.create(user=user, shipping_address=shipping_address)
499
+ for item in items:
500
+ if item['product'].stock < item['quantity']:
501
+ raise ValidationError(f"Insufficient stock for {item['product'].name}")
502
+ OrderItem.objects.create(order=order, **item)
503
+ item['product'].stock -= item['quantity']
504
+ item['product'].save(update_fields=['stock'])
505
+ PaymentService.charge(user=user, amount=order.total)
506
+ return order
507
+ \`\`\`
508
+
509
+ \`\`\`python
510
+ # selectors.py \u2014 Complex read queries
511
+ from django.db.models import Q, Count, Prefetch
512
+
513
+ class OrderSelector:
514
+ @staticmethod
515
+ def list_for_user(*, user, status=None, search=None):
516
+ qs = (
517
+ Order.objects
518
+ .filter(user=user)
519
+ .select_related('user', 'shipping_address')
520
+ .prefetch_related(
521
+ Prefetch('items', queryset=OrderItem.objects.select_related('product'))
522
+ )
523
+ .annotate(item_count=Count('items'))
524
+ )
525
+ if status:
526
+ qs = qs.filter(status=status)
527
+ if search:
528
+ qs = qs.filter(Q(id__icontains=search) | Q(items__product__name__icontains=search))
529
+ return qs.distinct().order_by('-created_at')
530
+ \`\`\`
531
+
532
+ ### Serializers: Advanced Patterns
533
+
534
+ **Separate read and write serializers:**
535
+ \`\`\`python
536
+ class OrderListSerializer(serializers.ModelSerializer):
537
+ """Lightweight serializer for list endpoints."""
538
+ item_count = serializers.IntegerField(read_only=True)
539
+ user = UserMinimalSerializer(read_only=True)
540
+
541
+ class Meta:
542
+ model = Order
543
+ fields = ['id', 'status', 'total', 'item_count', 'user', 'created_at']
544
+
545
+
546
+ class OrderDetailSerializer(serializers.ModelSerializer):
547
+ """Full serializer for retrieve endpoints."""
548
+ items = OrderItemSerializer(many=True, read_only=True)
549
+ user = UserSerializer(read_only=True)
550
+ shipping_address = AddressSerializer(read_only=True)
551
+
552
+ class Meta:
553
+ model = Order
554
+ fields = ['id', 'status', 'total', 'items', 'user', 'shipping_address', 'created_at', 'updated_at']
555
+
556
+
557
+ class OrderCreateSerializer(serializers.Serializer):
558
+ """Write serializer \u2014 validates input, delegates to service."""
559
+ items = OrderItemInputSerializer(many=True)
560
+ shipping_address_id = serializers.PrimaryKeyRelatedField(queryset=Address.objects.all())
561
+
562
+ def create(self, validated_data):
563
+ return OrderService.place_order(
564
+ user=self.context['request'].user,
565
+ items=validated_data['items'],
566
+ shipping_address=validated_data['shipping_address_id'],
567
+ )
568
+ \`\`\`
569
+
570
+ **Writable nested serializers:**
571
+ \`\`\`python
572
+ class ProjectSerializer(serializers.ModelSerializer):
573
+ tags = TagSerializer(many=True, required=False)
574
+
575
+ class Meta:
576
+ model = Project
577
+ fields = ['id', 'name', 'description', 'tags']
578
+
579
+ def create(self, validated_data):
580
+ tags_data = validated_data.pop('tags', [])
581
+ project = Project.objects.create(**validated_data)
582
+ for tag_data in tags_data:
583
+ tag, _ = Tag.objects.get_or_create(**tag_data)
584
+ project.tags.add(tag)
585
+ return project
586
+
587
+ def update(self, instance, validated_data):
588
+ tags_data = validated_data.pop('tags', None)
589
+ instance = super().update(instance, validated_data)
590
+ if tags_data is not None:
591
+ instance.tags.clear()
592
+ for tag_data in tags_data:
593
+ tag, _ = Tag.objects.get_or_create(**tag_data)
594
+ instance.tags.add(tag)
595
+ return instance
596
+ \`\`\`
597
+
598
+ **Dynamic field serializers:**
599
+ \`\`\`python
600
+ class DynamicFieldsSerializer(serializers.ModelSerializer):
601
+ """Pass ?fields=id,name,email to limit response fields."""
602
+ def __init__(self, *args, **kwargs):
603
+ fields = kwargs.pop('fields', None)
604
+ super().__init__(*args, **kwargs)
605
+ if fields is not None:
606
+ allowed = set(fields)
607
+ for field_name in set(self.fields) - allowed:
608
+ self.fields.pop(field_name)
609
+ \`\`\`
610
+
611
+ ### ViewSets: Advanced Patterns
612
+
613
+ **Use \`get_serializer_class()\` for action-specific serializers:**
614
+ \`\`\`python
615
+ class OrderViewSet(ModelViewSet):
616
+ permission_classes = [IsAuthenticated]
617
+ filterset_class = OrderFilterSet
618
+ search_fields = ['items__product__name']
619
+ ordering_fields = ['created_at', 'total']
620
+ ordering = ['-created_at']
621
+
622
+ def get_queryset(self):
623
+ return OrderSelector.list_for_user(user=self.request.user)
624
+
625
+ def get_serializer_class(self):
626
+ if self.action == 'list':
627
+ return OrderListSerializer
628
+ if self.action == 'retrieve':
629
+ return OrderDetailSerializer
630
+ if self.action in ('create',):
631
+ return OrderCreateSerializer
632
+ return OrderUpdateSerializer
633
+
634
+ def perform_create(self, serializer):
635
+ serializer.save() # Service called inside serializer.create()
636
+
637
+ @extend_schema(request=None, responses={200: OrderDetailSerializer})
638
+ @action(detail=True, methods=['post'])
639
+ def cancel(self, request, pk=None):
640
+ order = self.get_object()
641
+ OrderService.cancel_order(order=order, user=request.user)
642
+ return Response(OrderDetailSerializer(order).data)
643
+ \`\`\`
644
+
645
+ **Bulk operations:**
646
+ \`\`\`python
647
+ class BulkActionSerializer(serializers.Serializer):
648
+ ids = serializers.ListField(child=serializers.IntegerField(), min_length=1, max_length=100)
649
+ action = serializers.ChoiceField(choices=['archive', 'delete', 'export'])
650
+
651
+ @extend_schema(request=BulkActionSerializer, responses={200: None})
652
+ @action(detail=False, methods=['post'])
653
+ def bulk_action(self, request):
654
+ serializer = BulkActionSerializer(data=request.data)
655
+ serializer.is_valid(raise_exception=True)
656
+ qs = self.get_queryset().filter(id__in=serializer.validated_data['ids'])
657
+ action = serializer.validated_data['action']
658
+ if action == 'archive':
659
+ qs.update(status='archived')
660
+ elif action == 'delete':
661
+ qs.delete()
662
+ return Response(status=status.HTTP_200_OK)
663
+ \`\`\`
664
+
665
+ ### QuerySet Optimization
666
+
667
+ **ALWAYS optimize queries. N+1 queries are unacceptable.**
668
+
669
+ \`\`\`python
670
+ # BAD \u2014 N+1 queries
671
+ orders = Order.objects.all()
672
+ for order in orders:
673
+ print(order.user.email) # 1 query per order
674
+ for item in order.items.all(): # 1 query per order
675
+ print(item.product.name) # 1 query per item
676
+
677
+ # GOOD \u2014 3 queries total
678
+ orders = (
679
+ Order.objects
680
+ .select_related('user')
681
+ .prefetch_related(
682
+ Prefetch('items', queryset=OrderItem.objects.select_related('product'))
683
+ )
684
+ )
685
+ \`\`\`
686
+
687
+ **Use \`only()\` / \`defer()\` for large tables:**
688
+ \`\`\`python
689
+ # Only load fields you need for list views
690
+ Product.objects.only('id', 'name', 'price', 'thumbnail').filter(is_active=True)
691
+ \`\`\`
692
+
693
+ **Use \`Subquery\` and \`OuterRef\` for correlated queries:**
694
+ \`\`\`python
695
+ from django.db.models import Subquery, OuterRef
696
+
697
+ latest_comment = Comment.objects.filter(
698
+ post=OuterRef('pk')
699
+ ).order_by('-created_at')
700
+
701
+ posts = Post.objects.annotate(
702
+ latest_comment_text=Subquery(latest_comment.values('text')[:1])
703
+ )
704
+ \`\`\`
705
+
706
+ ### Custom Permissions
707
+
708
+ \`\`\`python
709
+ # permissions.py
710
+ from rest_framework.permissions import BasePermission
711
+
712
+ class IsOwner(BasePermission):
713
+ """Object-level permission: only the owner can modify."""
714
+ def has_object_permission(self, request, view, obj):
715
+ return obj.user == request.user
716
+
717
+
718
+ class IsAdminOrReadOnly(BasePermission):
719
+ def has_permission(self, request, view):
720
+ if request.method in ('GET', 'HEAD', 'OPTIONS'):
721
+ return True
722
+ return request.user and request.user.is_staff
723
+
724
+
725
+ class HasRole(BasePermission):
726
+ """Usage: permission_classes = [HasRole('manager')]"""
727
+ def __init__(self, role):
728
+ self.role = role
729
+
730
+ def has_permission(self, request, view):
731
+ return hasattr(request.user, 'role') and request.user.role == self.role
732
+ \`\`\`
733
+
734
+ **Combine permissions per action:**
735
+ \`\`\`python
736
+ class ProjectViewSet(ModelViewSet):
737
+ def get_permissions(self):
738
+ if self.action in ('update', 'partial_update', 'destroy'):
739
+ return [IsAuthenticated(), IsOwner()]
740
+ if self.action == 'create':
741
+ return [IsAuthenticated()]
742
+ return [AllowAny()]
743
+ \`\`\`
744
+
745
+ ### Custom Filters with django-filter
746
+
747
+ \`\`\`python
748
+ # filters.py
749
+ import django_filters
750
+ from .models import Order
751
+
752
+ class OrderFilterSet(django_filters.FilterSet):
753
+ min_total = django_filters.NumberFilter(field_name='total', lookup_expr='gte')
754
+ max_total = django_filters.NumberFilter(field_name='total', lookup_expr='lte')
755
+ created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
756
+ created_before = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
757
+ status = django_filters.MultipleChoiceFilter(choices=Order.STATUS_CHOICES)
758
+
759
+ class Meta:
760
+ model = Order
761
+ fields = ['status', 'min_total', 'max_total', 'created_after', 'created_before']
762
+ \`\`\`
763
+
764
+ ### Pagination: Cursor-Based for Large Datasets
765
+
766
+ \`\`\`python
767
+ # pagination.py
768
+ from rest_framework.pagination import CursorPagination
769
+
770
+ class TimelinePagination(CursorPagination):
771
+ page_size = 50
772
+ ordering = '-created_at'
773
+ cursor_query_param = 'cursor'
774
+ \`\`\`
775
+
776
+ Use in viewset: \`pagination_class = TimelinePagination\`
777
+
778
+ ### Throttling
779
+
780
+ \`\`\`python
781
+ # In settings
782
+ REST_FRAMEWORK = {
783
+ 'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.ScopedRateThrottle'],
784
+ 'DEFAULT_THROTTLE_RATES': {
785
+ 'auth': '5/min',
786
+ 'uploads': '20/hour',
787
+ 'burst': '60/min',
788
+ },
789
+ }
790
+
791
+ # In view
792
+ class LoginView(APIView):
793
+ throttle_scope = 'auth'
794
+ \`\`\`
795
+
796
+ ### Caching
797
+
798
+ \`\`\`python
799
+ from django.views.decorators.cache import cache_page
800
+ from django.utils.decorators import method_decorator
801
+
802
+ class ProductViewSet(ModelViewSet):
803
+ @method_decorator(cache_page(60 * 15)) # 15 min cache
804
+ def list(self, request, *args, **kwargs):
805
+ return super().list(request, *args, **kwargs)
806
+ \`\`\`
807
+
808
+ **Conditional caching with ETags:**
809
+ \`\`\`python
810
+ from rest_framework_condition import condition
811
+ from hashlib import md5
812
+
813
+ def product_etag(request, pk=None):
814
+ product = Product.objects.only('updated_at').get(pk=pk)
815
+ return md5(str(product.updated_at).encode()).hexdigest()
816
+
817
+ class ProductViewSet(ModelViewSet):
818
+ @condition(etag_func=product_etag)
819
+ def retrieve(self, request, *args, **kwargs):
820
+ return super().retrieve(request, *args, **kwargs)
821
+ \`\`\`
822
+
823
+ ### Error Handling
824
+
825
+ \`\`\`python
826
+ # exceptions.py
827
+ from rest_framework.views import exception_handler
828
+ from rest_framework.response import Response
829
+
830
+ def custom_exception_handler(exc, context):
831
+ response = exception_handler(exc, context)
832
+ if response is not None:
833
+ response.data = {
834
+ 'error': {
835
+ 'code': response.status_code,
836
+ 'message': response.data.get('detail', response.data),
837
+ }
838
+ }
839
+ return response
840
+ \`\`\`
841
+
842
+ Register in settings: \`'EXCEPTION_HANDLER': 'config.exceptions.custom_exception_handler'\`
843
+
844
+ ### Versioning
845
+
846
+ \`\`\`python
847
+ # settings
848
+ REST_FRAMEWORK = {
849
+ 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
850
+ 'ALLOWED_VERSIONS': ['v1', 'v2'],
851
+ 'DEFAULT_VERSION': 'v1',
852
+ }
853
+
854
+ # urls.py
855
+ urlpatterns = [
856
+ path('api/<version>/', include('apps.core.urls')),
857
+ ]
858
+
859
+ # views.py \u2014 Version-specific behavior
860
+ class UserViewSet(ModelViewSet):
861
+ def get_serializer_class(self):
862
+ if self.request.version == 'v2':
863
+ return UserV2Serializer
864
+ return UserV1Serializer
865
+ \`\`\`
866
+
867
+ ### Signals \u2014 Use Responsibly
868
+
869
+ \`\`\`python
870
+ # signals.py \u2014 Only for cross-cutting concerns (audit logs, cache invalidation)
871
+ from django.db.models.signals import post_save
872
+ from django.dispatch import receiver
873
+
874
+ @receiver(post_save, sender=Order)
875
+ def notify_on_order_placed(sender, instance, created, **kwargs):
876
+ if created:
877
+ NotificationService.send_order_confirmation(order=instance)
878
+ \`\`\`
879
+
880
+ Register in \`apps.py\`:
881
+ \`\`\`python
882
+ class OrdersConfig(AppConfig):
883
+ def ready(self):
884
+ import apps.orders.signals # noqa: F401
885
+ \`\`\`
886
+
887
+ > **Prefer explicit service calls over signals for business logic.** Signals make flow hard to trace.
888
+
889
+ ### Testing: Senior-Level Patterns
890
+
891
+ \`\`\`python
892
+ import factory
893
+ from rest_framework.test import APITestCase, APIClient
894
+
895
+ # factories.py \u2014 Use factory_boy for test data
896
+ class UserFactory(factory.django.DjangoModelFactory):
897
+ class Meta:
898
+ model = User
899
+ email = factory.Sequence(lambda n: f'user{n}@example.com')
900
+ password = factory.PostGenerationMethodCall('set_password', 'testpass123')
901
+
902
+
903
+ class OrderFactory(factory.django.DjangoModelFactory):
904
+ class Meta:
905
+ model = Order
906
+ user = factory.SubFactory(UserFactory)
907
+ status = 'pending'
908
+
909
+
910
+ # test_views.py
911
+ class OrderViewSetTest(APITestCase):
912
+ def setUp(self):
913
+ self.user = UserFactory()
914
+ self.client = APIClient()
915
+ self.client.force_authenticate(self.user)
916
+
917
+ def test_list_returns_only_own_orders(self):
918
+ OrderFactory.create_batch(3, user=self.user)
919
+ OrderFactory.create_batch(2) # Other user's orders
920
+ response = self.client.get('/api/orders/')
921
+ self.assertEqual(response.status_code, 200)
922
+ self.assertEqual(len(response.data['results']), 3)
923
+
924
+ def test_create_validates_stock(self):
925
+ product = ProductFactory(stock=0)
926
+ response = self.client.post('/api/orders/', {
927
+ 'items': [{'product_id': product.id, 'quantity': 1}],
928
+ 'shipping_address_id': AddressFactory(user=self.user).id,
929
+ }, format='json')
930
+ self.assertEqual(response.status_code, 400)
931
+
932
+ def test_cancel_forbidden_for_non_owner(self):
933
+ order = OrderFactory() # Different user
934
+ response = self.client.post(f'/api/orders/{order.id}/cancel/')
935
+ self.assertEqual(response.status_code, 403)
936
+ \`\`\`
937
+
938
+ **Test query count to prevent N+1 regressions:**
939
+ \`\`\`python
940
+ from django.test.utils import override_settings
941
+
942
+ def test_list_query_count(self):
943
+ OrderFactory.create_batch(10, user=self.user)
944
+ with self.assertNumQueries(3): # 1 count + 1 orders + 1 prefetch items
945
+ self.client.get('/api/orders/')
946
+ \`\`\`
947
+
948
+ ### API Documentation with drf-spectacular
949
+
950
+ \`\`\`python
951
+ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample
952
+
953
+ @extend_schema_view(
954
+ list=extend_schema(
955
+ summary="List orders",
956
+ parameters=[
957
+ OpenApiParameter('status', str, description='Filter by status'),
958
+ OpenApiParameter('search', str, description='Search by product name'),
959
+ ],
960
+ ),
961
+ create=extend_schema(summary="Place a new order"),
962
+ cancel=extend_schema(summary="Cancel an order", responses={200: OrderDetailSerializer}),
963
+ )
964
+ class OrderViewSet(ModelViewSet):
965
+ ...
966
+ \`\`\`
967
+
968
+ ### Key Principles
969
+
970
+ 1. **Fat services, thin views** \u2014 Views handle HTTP; services handle logic
971
+ 2. **Optimize every queryset** \u2014 Use \`select_related\`, \`prefetch_related\`, \`only\`, \`annotate\`
972
+ 3. **Separate read/write serializers** \u2014 List views are lightweight, detail views are rich, write views validate input
973
+ 4. **Test behavior, not implementation** \u2014 Test API contracts, permissions, and edge cases
974
+ 5. **Use \`transaction.atomic\`** \u2014 Wrap multi-step mutations to prevent partial writes
975
+ 6. **Document with \`extend_schema\`** \u2014 Every endpoint needs OpenAPI docs for frontend type generation
976
+ 7. **Scope querysets to user** \u2014 Never return data the user shouldn't see
977
+ 8. **Use cursor pagination for large datasets** \u2014 Offset pagination degrades at scale
978
+ 9. **Throttle sensitive endpoints** \u2014 Auth, uploads, and expensive operations
979
+ 10. **Version your API** \u2014 Plan for breaking changes from the start
980
+ `;
981
+ }
982
+ };
983
+
984
+ // src/skills/api-documentation.ts
985
+ var apiDocumentationSkill = {
986
+ id: "api-documentation",
987
+ name: "API Documentation",
988
+ description: "drf-spectacular OpenAPI/Swagger documentation conventions for all API endpoints.",
989
+ render(_ctx) {
990
+ return `## API Documentation \u2014 drf-spectacular (OpenAPI / Swagger)
991
+
992
+ > **RULE: Every API endpoint MUST be documented with \`@extend_schema\` from \`drf-spectacular\`.**
993
+ > Undocumented endpoints break the frontend type generation pipeline (\`blacksmith sync\`).
994
+ > The OpenAPI schema powers auto-generated TypeScript types \u2014 accurate docs = accurate frontend types.
995
+
996
+ ### Setup
997
+
998
+ drf-spectacular is already configured in \`backend/config/settings/base.py\`:
999
+
1000
+ \`\`\`python
1001
+ INSTALLED_APPS = [
1002
+ ...
1003
+ 'drf_spectacular',
1004
+ ]
1005
+
1006
+ REST_FRAMEWORK = {
1007
+ 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
1008
+ }
1009
+
1010
+ SPECTACULAR_SETTINGS = {
1011
+ 'TITLE': 'API',
1012
+ 'DESCRIPTION': 'API documentation',
1013
+ 'VERSION': '1.0.0',
1014
+ 'SERVE_INCLUDE_SCHEMA': False,
1015
+ }
1016
+ \`\`\`
1017
+
1018
+ Docs URLs in \`backend/config/urls.py\`:
1019
+ \`\`\`python
1020
+ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
1021
+
1022
+ urlpatterns = [
1023
+ path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
1024
+ path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
1025
+ path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
1026
+ ]
1027
+ \`\`\`
1028
+
1029
+ ### Decorating ViewSets \u2014 MANDATORY
1030
+
1031
+ **Use \`@extend_schema_view\` on every ViewSet:**
1032
+ \`\`\`python
1033
+ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample, OpenApiResponse
1034
+
1035
+ @extend_schema_view(
1036
+ list=extend_schema(
1037
+ summary="List projects",
1038
+ description="Returns paginated list of projects for the authenticated user.",
1039
+ parameters=[
1040
+ OpenApiParameter('status', str, enum=['active', 'archived'], description='Filter by status'),
1041
+ OpenApiParameter('search', str, description='Search by name or description'),
1042
+ OpenApiParameter('ordering', str, description='Sort field (prefix with - for desc)', enum=['created_at', '-created_at', 'name', '-name']),
1043
+ ],
1044
+ responses={200: ProjectListSerializer},
1045
+ ),
1046
+ retrieve=extend_schema(
1047
+ summary="Get project details",
1048
+ responses={200: ProjectDetailSerializer},
1049
+ ),
1050
+ create=extend_schema(
1051
+ summary="Create a project",
1052
+ request=ProjectCreateSerializer,
1053
+ responses={201: ProjectDetailSerializer},
1054
+ examples=[
1055
+ OpenApiExample(
1056
+ 'Create project',
1057
+ value={'name': 'My Project', 'description': 'A new project'},
1058
+ request_only=True,
1059
+ ),
1060
+ ],
1061
+ ),
1062
+ update=extend_schema(
1063
+ summary="Update a project",
1064
+ request=ProjectUpdateSerializer,
1065
+ responses={200: ProjectDetailSerializer},
1066
+ ),
1067
+ partial_update=extend_schema(
1068
+ summary="Partially update a project",
1069
+ request=ProjectUpdateSerializer,
1070
+ responses={200: ProjectDetailSerializer},
1071
+ ),
1072
+ destroy=extend_schema(
1073
+ summary="Delete a project",
1074
+ responses={204: None},
1075
+ ),
1076
+ )
1077
+ class ProjectViewSet(ModelViewSet):
1078
+ ...
1079
+ \`\`\`
1080
+
1081
+ **Custom actions MUST also be decorated:**
1082
+ \`\`\`python
1083
+ @extend_schema(
1084
+ summary="Archive a project",
1085
+ request=None,
1086
+ responses={200: ProjectDetailSerializer},
1087
+ )
1088
+ @action(detail=True, methods=['post'])
1089
+ def archive(self, request, pk=None):
1090
+ project = self.get_object()
1091
+ ProjectService.archive(project=project)
1092
+ return Response(ProjectDetailSerializer(project).data)
1093
+
1094
+
1095
+ @extend_schema(
1096
+ summary="Bulk delete projects",
1097
+ request=BulkDeleteSerializer,
1098
+ responses={204: None},
1099
+ )
1100
+ @action(detail=False, methods=['post'])
1101
+ def bulk_delete(self, request):
1102
+ ...
1103
+ \`\`\`
1104
+
1105
+ ### Decorating APIViews
1106
+
1107
+ \`\`\`python
1108
+ class DashboardStatsView(APIView):
1109
+ @extend_schema(
1110
+ summary="Get dashboard statistics",
1111
+ responses={200: DashboardStatsSerializer},
1112
+ )
1113
+ def get(self, request):
1114
+ stats = DashboardSelector.get_stats(user=request.user)
1115
+ return Response(DashboardStatsSerializer(stats).data)
1116
+ \`\`\`
1117
+
1118
+ ### Serializer Documentation
1119
+
1120
+ **Use \`help_text\` on serializer fields \u2014 these become field descriptions in the schema:**
1121
+ \`\`\`python
1122
+ class ProjectCreateSerializer(serializers.Serializer):
1123
+ name = serializers.CharField(max_length=255, help_text="The project name. Must be unique per user.")
1124
+ description = serializers.CharField(required=False, help_text="Optional project description.")
1125
+ status = serializers.ChoiceField(
1126
+ choices=['active', 'archived'],
1127
+ default='active',
1128
+ help_text="Initial project status.",
1129
+ )
1130
+ tags = serializers.ListField(
1131
+ child=serializers.CharField(),
1132
+ required=False,
1133
+ help_text="List of tag names to attach.",
1134
+ )
1135
+ \`\`\`
1136
+
1137
+ **Use \`@extend_schema_serializer\` for serializer-level docs:**
1138
+ \`\`\`python
1139
+ from drf_spectacular.utils import extend_schema_serializer, OpenApiExample
1140
+
1141
+ @extend_schema_serializer(
1142
+ examples=[
1143
+ OpenApiExample(
1144
+ 'Project response',
1145
+ value={
1146
+ 'id': 1,
1147
+ 'name': 'My Project',
1148
+ 'status': 'active',
1149
+ 'created_at': '2025-01-15T10:30:00Z',
1150
+ },
1151
+ response_only=True,
1152
+ ),
1153
+ ]
1154
+ )
1155
+ class ProjectDetailSerializer(serializers.ModelSerializer):
1156
+ class Meta:
1157
+ model = Project
1158
+ fields = ['id', 'name', 'description', 'status', 'created_at', 'updated_at']
1159
+ \`\`\`
1160
+
1161
+ ### Response Types
1162
+
1163
+ **Explicitly declare all possible response codes:**
1164
+ \`\`\`python
1165
+ @extend_schema(
1166
+ summary="Place an order",
1167
+ request=OrderCreateSerializer,
1168
+ responses={
1169
+ 201: OrderDetailSerializer,
1170
+ 400: OpenApiResponse(description="Validation error (insufficient stock, invalid address, etc.)"),
1171
+ 401: OpenApiResponse(description="Authentication required"),
1172
+ 403: OpenApiResponse(description="Insufficient permissions"),
1173
+ },
1174
+ )
1175
+ def create(self, request, *args, **kwargs):
1176
+ ...
1177
+ \`\`\`
1178
+
1179
+ ### Enum and Choice Fields
1180
+
1181
+ **Use \`@extend_schema_field\` for custom field types:**
1182
+ \`\`\`python
1183
+ from drf_spectacular.utils import extend_schema_field
1184
+ from drf_spectacular.types import OpenApiTypes
1185
+
1186
+ @extend_schema_field(OpenApiTypes.STR)
1187
+ class ColorField(serializers.Field):
1188
+ ...
1189
+ \`\`\`
1190
+
1191
+ ### Polymorphic / Union Responses
1192
+
1193
+ \`\`\`python
1194
+ from drf_spectacular.utils import PolymorphicProxySerializer
1195
+
1196
+ @extend_schema(
1197
+ responses=PolymorphicProxySerializer(
1198
+ component_name='Notification',
1199
+ serializers={
1200
+ 'email': EmailNotificationSerializer,
1201
+ 'sms': SmsNotificationSerializer,
1202
+ 'push': PushNotificationSerializer,
1203
+ },
1204
+ resource_type_field_name='type',
1205
+ )
1206
+ )
1207
+ def list(self, request):
1208
+ ...
1209
+ \`\`\`
1210
+
1211
+ ### Pagination in Schema
1212
+
1213
+ drf-spectacular auto-wraps list responses with pagination. If using custom pagination:
1214
+ \`\`\`python
1215
+ from drf_spectacular.utils import extend_schema
1216
+
1217
+ @extend_schema(
1218
+ summary="List items",
1219
+ responses=ItemSerializer(many=True), # Pagination wrapper is auto-applied
1220
+ )
1221
+ def list(self, request, *args, **kwargs):
1222
+ ...
1223
+ \`\`\`
1224
+
1225
+ ### Tags for Grouping
1226
+
1227
+ **Group endpoints by feature using tags:**
1228
+ \`\`\`python
1229
+ @extend_schema_view(
1230
+ list=extend_schema(tags=['Orders']),
1231
+ create=extend_schema(tags=['Orders']),
1232
+ retrieve=extend_schema(tags=['Orders']),
1233
+ )
1234
+ class OrderViewSet(ModelViewSet):
1235
+ ...
1236
+ \`\`\`
1237
+
1238
+ Or set a default tag via \`SPECTACULAR_SETTINGS\`:
1239
+ \`\`\`python
1240
+ SPECTACULAR_SETTINGS = {
1241
+ 'TAGS': [
1242
+ {'name': 'Auth', 'description': 'Authentication endpoints'},
1243
+ {'name': 'Orders', 'description': 'Order management'},
1244
+ {'name': 'Products', 'description': 'Product catalog'},
1245
+ ],
1246
+ }
1247
+ \`\`\`
1248
+
1249
+ ### Authentication in Schema
1250
+
1251
+ \`\`\`python
1252
+ SPECTACULAR_SETTINGS = {
1253
+ 'SECURITY': [{'jwtAuth': []}],
1254
+ 'APPEND_COMPONENTS': {
1255
+ 'securitySchemes': {
1256
+ 'jwtAuth': {
1257
+ 'type': 'http',
1258
+ 'scheme': 'bearer',
1259
+ 'bearerFormat': 'JWT',
1260
+ }
1261
+ }
1262
+ },
1263
+ }
1264
+ \`\`\`
1265
+
1266
+ ### Excluding Endpoints
1267
+
1268
+ \`\`\`python
1269
+ @extend_schema(exclude=True)
1270
+ @action(detail=False, methods=['get'])
1271
+ def internal_health_check(self, request):
1272
+ ...
1273
+ \`\`\`
1274
+
1275
+ ### Generating and Validating the Schema
1276
+
1277
+ \`\`\`bash
1278
+ # Generate schema file
1279
+ ./venv/bin/python manage.py spectacular --file schema.yml
1280
+
1281
+ # Validate schema for errors
1282
+ ./venv/bin/python manage.py spectacular --validate
1283
+ \`\`\`
1284
+
1285
+ > **Always run \`--validate\` after adding new endpoints.** Fix any warnings before committing.
1286
+
1287
+ ### Rules
1288
+
1289
+ 1. **Every ViewSet** must have \`@extend_schema_view\` with summaries for all actions
1290
+ 2. **Every custom \`@action\`** must have its own \`@extend_schema\` decorator
1291
+ 3. **Every serializer field** that isn't self-explanatory must have \`help_text\`
1292
+ 4. **Request and response serializers** must be explicitly declared \u2014 do not rely on auto-detection for non-trivial endpoints
1293
+ 5. **All error responses** (400, 401, 403, 404) should be documented with \`OpenApiResponse\`
1294
+ 6. **Run \`manage.py spectacular --validate\`** before committing to catch schema issues early
1295
+ 7. **Use examples** (\`OpenApiExample\`) for complex request/response bodies
1296
+ 8. **Group endpoints with tags** to keep Swagger UI organized
1297
+ `;
1298
+ }
1299
+ };
1300
+
1301
+ // src/skills/react.ts
1302
+ var reactSkill = {
1303
+ id: "react",
1304
+ name: "React Frontend Conventions",
1305
+ description: "Tech stack, project structure, state management, component patterns, styling, and testing for the React frontend.",
1306
+ render(_ctx) {
1307
+ return `## React Frontend Conventions
1308
+
1309
+ ### Tech Stack
1310
+ - React 19 + TypeScript (strict mode)
1311
+ - Vite for bundling and dev server
1312
+ - TanStack React Query for server state management
1313
+ - React Router v7 for client-side routing
1314
+ - React Hook Form + Zod for forms and validation
1315
+ - Tailwind CSS for styling
1316
+ - \`@hey-api/openapi-ts\` for auto-generating API client from Django's OpenAPI schema
1317
+ - \`lucide-react\` for icons
1318
+
1319
+ ### API Layer
1320
+ - Auto-generated client in \`frontend/src/api/generated/\` \u2014 **never edit these files manually**
1321
+ - Custom API configuration (base URL, interceptors, auth headers) in \`frontend/src/api/client.ts\`
1322
+ - Query client setup and default options in \`frontend/src/api/query-client.ts\`
1323
+ - After any backend API change, run \`blacksmith sync\` to regenerate the client
1324
+
1325
+ ### Project Structure
1326
+ - See the \`page-structure\` skill for page folders, feature modules, routing, and route composition conventions
1327
+ - Shared, cross-feature code lives in \`frontend/src/shared/\`
1328
+
1329
+ ### State Management
1330
+ - **Server state**: TanStack React Query \u2014 see the \`react-query\` skill for full conventions on \`useApiQuery\` and \`useApiMutation\`
1331
+ - **Form state**: React Hook Form \u2014 manages form values, validation, submission
1332
+ - **Local UI state**: React \`useState\` / \`useReducer\` for component-scoped state
1333
+ - Avoid global state libraries unless there is a clear cross-cutting concern not covered by React Query
1334
+
1335
+ ### Component Patterns
1336
+ - Use functional components with named exports (not default exports for components)
1337
+ - Co-locate component, hook, and type in the same feature directory
1338
+ - Keep components focused \u2014 extract sub-components when a file exceeds ~150 lines
1339
+ - Use custom hooks to encapsulate data fetching and mutation logic
1340
+ - Prefer composition over prop drilling \u2014 use context for deeply shared state
1341
+ - **Pages must be thin orchestrators** \u2014 break into child components in \`components/\`, extract logic into \`hooks/\`. See the \`page-structure\` skill for the full pattern
1342
+
1343
+ ### UI Components
1344
+ - **All UI must use \`@blacksmith-ui/react\` components** \u2014 see the \`blacksmith-ui-react\` skill for the full component list
1345
+ - Use \`Stack\`, \`Flex\`, \`Grid\`, \`Box\` for layout \u2014 never raw \`<div>\` with flex/grid classes
1346
+ - Use \`Typography\` and \`Text\` for headings and text \u2014 never raw \`<h1>\`\u2013\`<h6>\` or \`<p>\`
1347
+ - Use \`Divider\` instead of \`<Separator>\` or \`<hr>\`
1348
+ - Use \`StatCard\`, \`EmptyState\`, \`Skeleton\` instead of building custom equivalents
1349
+
1350
+ ### Route Paths
1351
+ - All route paths live in the \`Path\` enum at \`src/router/paths.ts\` \u2014 **never hardcode path strings**
1352
+ - Use \`Path\` in route definitions, \`navigate()\`, and \`<Link to={}>\`
1353
+ - Use \`buildPath()\` for dynamic segments \u2014 see the \`page-structure\` skill for details
1354
+
1355
+ ### Styling
1356
+ - Use Tailwind CSS utility classes for all styling
1357
+ - Use the \`cn()\` helper (from \`clsx\` + \`tailwind-merge\`) for conditional and merged classes
1358
+ - Theming via HSL CSS variables defined in \`frontend/src/styles/globals.css\`
1359
+ - Dark mode is supported via the \`class\` strategy on \`<html>\`
1360
+ - Use responsive prefixes (\`sm:\`, \`md:\`, \`lg:\`) for responsive layouts
1361
+ - Avoid inline \`style\` attributes \u2014 use Tailwind classes instead
1362
+
1363
+ ### Path Aliases
1364
+ - \`@/\` maps to \`frontend/src/\`
1365
+ - Always use the alias for imports: \`import { useAuth } from '@/features/auth'\`
1366
+ - Never use relative paths that go up more than one level (\`../../\`)
1367
+
1368
+ ### Error Handling
1369
+ - Use React Error Boundary (\`frontend/src/router/error-boundary.tsx\`) for render errors
1370
+ - API errors are handled by \`useApiQuery\` / \`useApiMutation\` \u2014 see the \`react-query\` skill for error display patterns
1371
+ - Display user-facing errors using the project's feedback components (Alert, Toast)
1372
+
1373
+ ### Testing
1374
+ - Run all tests: \`cd frontend && npm test\`
1375
+ - Run a specific test: \`cd frontend && npm test -- --grep "test name"\`
1376
+ - Test files live alongside the code they test (\`component.test.tsx\`)
1377
+ `;
1378
+ }
1379
+ };
1380
+
1381
+ // src/skills/react-query.ts
1382
+ var reactQuerySkill = {
1383
+ id: "react-query",
1384
+ name: "TanStack React Query",
1385
+ description: "API data fetching conventions using useApiQuery and useApiMutation wrappers.",
1386
+ render(_ctx) {
1387
+ return `## TanStack React Query \u2014 API Data Fetching
1388
+
1389
+ > **RULE: Always use \`useApiQuery\` and \`useApiMutation\` instead of raw \`useQuery\` / \`useMutation\`.**
1390
+ > These wrappers live in \`@/shared/hooks/\` and handle DRF error parsing, smart retry, and cache invalidation automatically.
1391
+
1392
+ ### Queries \u2014 \`useApiQuery\`
1393
+
1394
+ Import: \`import { useApiQuery } from '@/shared/hooks/use-api-query'\`
1395
+
1396
+ Wraps \`useQuery\` with:
1397
+ - **Smart retry** \u2014 skips 400, 401, 403, 404, 405, 409, 422 (retries others up to 2 times)
1398
+ - **Parsed errors** \u2014 \`errorMessage\` (string) and \`apiError\` (structured) derived from \`result.error\`
1399
+
1400
+ \`\`\`tsx
1401
+ // Basic list query using generated options
1402
+ import { postsListOptions } from '@/api/generated/@tanstack/react-query.gen'
1403
+
1404
+ const { data, isLoading, errorMessage } = useApiQuery({
1405
+ ...postsListOptions({ query: { page: 1 } }),
1406
+ })
1407
+
1408
+ // With select to transform data
1409
+ const { data, errorMessage } = useApiQuery({
1410
+ ...postsListOptions({ query: { page: 1 } }),
1411
+ select: (data: any) => ({
1412
+ posts: data.results ?? [],
1413
+ total: data.count ?? 0,
1414
+ }),
1415
+ })
1416
+
1417
+ // Detail query
1418
+ import { postsRetrieveOptions } from '@/api/generated/@tanstack/react-query.gen'
1419
+
1420
+ const { data: post, isLoading, errorMessage } = useApiQuery({
1421
+ ...postsRetrieveOptions({ path: { id: Number(id) } }),
1422
+ })
1423
+
1424
+ // Conditional query (skip until id is available)
1425
+ const { data } = useApiQuery({
1426
+ ...postsRetrieveOptions({ path: { id: Number(id) } }),
1427
+ enabled: !!id,
1428
+ })
1429
+ \`\`\`
1430
+
1431
+ **Return type extends \`UseQueryResult\` with:**
1432
+ | Field | Type | Description |
1433
+ |-------|------|-------------|
1434
+ | \`errorMessage\` | \`string \\| null\` | Human-readable error message |
1435
+ | \`apiError\` | \`ApiError \\| null\` | Structured error with \`status\`, \`message\`, \`fieldErrors\` |
1436
+
1437
+ ### Mutations \u2014 \`useApiMutation\`
1438
+
1439
+ Import: \`import { useApiMutation } from '@/shared/hooks/use-api-mutation'\`
1440
+
1441
+ Wraps \`useMutation\` with:
1442
+ - **DRF error parsing** \u2014 \`fieldErrors\`, \`errorMessage\`, \`apiError\` derived from \`mutation.error\` (no local state)
1443
+ - **Cache invalidation** \u2014 pass \`invalidateKeys\` to auto-invalidate queries on success
1444
+
1445
+ \`\`\`tsx
1446
+ // Create mutation with cache invalidation
1447
+ import {
1448
+ postsCreateMutation,
1449
+ postsListQueryKey,
1450
+ } from '@/api/generated/@tanstack/react-query.gen'
1451
+
1452
+ const createPost = useApiMutation({
1453
+ ...postsCreateMutation(),
1454
+ invalidateKeys: [postsListQueryKey()],
1455
+ })
1456
+
1457
+ // Trigger the mutation
1458
+ createPost.mutate({ body: { title: 'Hello', content: '...' } })
1459
+
1460
+ // Update mutation \u2014 invalidate both list and detail caches
1461
+ import {
1462
+ postsUpdateMutation,
1463
+ postsRetrieveQueryKey,
1464
+ } from '@/api/generated/@tanstack/react-query.gen'
1465
+
1466
+ const updatePost = useApiMutation({
1467
+ ...postsUpdateMutation(),
1468
+ invalidateKeys: [
1469
+ postsListQueryKey(),
1470
+ postsRetrieveQueryKey({ path: { id } }),
1471
+ ],
1472
+ })
1473
+
1474
+ // Delete with async/await
1475
+ const deletePost = useApiMutation({
1476
+ ...postsDestroyMutation(),
1477
+ invalidateKeys: [postsListQueryKey()],
1478
+ })
1479
+
1480
+ const handleDelete = async () => {
1481
+ await deletePost.mutateAsync({ path: { id: Number(id) } })
1482
+ navigate('/posts')
1483
+ }
1484
+ \`\`\`
1485
+
1486
+ **Return type extends \`UseMutationResult\` with:**
1487
+ | Field | Type | Description |
1488
+ |-------|------|-------------|
1489
+ | \`fieldErrors\` | \`Record<string, string[]>\` | Per-field validation errors from DRF |
1490
+ | \`errorMessage\` | \`string \\| null\` | General error message |
1491
+ | \`apiError\` | \`ApiError \\| null\` | Full structured error |
1492
+
1493
+ ### Error Display Patterns
1494
+
1495
+ \`\`\`tsx
1496
+ // General error banner
1497
+ {mutation.errorMessage && (
1498
+ <Alert variant="destructive">
1499
+ <AlertDescription>{mutation.errorMessage}</AlertDescription>
1500
+ </Alert>
1501
+ )}
1502
+
1503
+ // Inline field errors in forms
1504
+ const getFieldError = (field: string): string | undefined => {
1505
+ // Client-side (react-hook-form) errors take priority
1506
+ const clientError = form.formState.errors[field]?.message
1507
+ if (clientError) return clientError
1508
+ // Fall back to server-side field errors
1509
+ return mutation.fieldErrors[field]?.[0]
1510
+ }
1511
+
1512
+ // Query error on a page
1513
+ const { data, isLoading, errorMessage } = useApiQuery({ ... })
1514
+
1515
+ if (errorMessage) {
1516
+ return (
1517
+ <Alert variant="destructive">
1518
+ <AlertDescription>{errorMessage}</AlertDescription>
1519
+ </Alert>
1520
+ )
1521
+ }
1522
+ \`\`\`
1523
+
1524
+ ### Creating Resource Hook Files
1525
+
1526
+ When building hooks for a resource, create two files:
1527
+
1528
+ **\`use-<resources>.ts\`** \u2014 List query hook:
1529
+ \`\`\`tsx
1530
+ import { useApiQuery } from '@/shared/hooks/use-api-query'
1531
+ import { postsListOptions } from '@/api/generated/@tanstack/react-query.gen'
1532
+
1533
+ interface UsePostsParams {
1534
+ page?: number
1535
+ search?: string
1536
+ ordering?: string
1537
+ }
1538
+
1539
+ export function usePosts(params: UsePostsParams = {}) {
1540
+ return useApiQuery({
1541
+ ...postsListOptions({
1542
+ query: {
1543
+ page: params.page ?? 1,
1544
+ search: params.search,
1545
+ ordering: params.ordering ?? '-created_at',
1546
+ },
1547
+ }),
1548
+ select: (data: any) => ({
1549
+ posts: data.results ?? [],
1550
+ total: data.count ?? 0,
1551
+ hasNext: !!data.next,
1552
+ hasPrev: !!data.previous,
1553
+ }),
1554
+ })
1555
+ }
1556
+ \`\`\`
1557
+
1558
+ **\`use-<resource>-mutations.ts\`** \u2014 Create/update/delete hooks:
1559
+ \`\`\`tsx
1560
+ import { useApiMutation } from '@/shared/hooks/use-api-mutation'
1561
+ import {
1562
+ postsCreateMutation,
1563
+ postsUpdateMutation,
1564
+ postsDestroyMutation,
1565
+ postsListQueryKey,
1566
+ postsRetrieveQueryKey,
1567
+ } from '@/api/generated/@tanstack/react-query.gen'
1568
+
1569
+ export function useCreatePost() {
1570
+ return useApiMutation({
1571
+ ...postsCreateMutation(),
1572
+ invalidateKeys: [postsListQueryKey()],
1573
+ })
1574
+ }
1575
+
1576
+ export function useUpdatePost(id: number) {
1577
+ return useApiMutation({
1578
+ ...postsUpdateMutation(),
1579
+ invalidateKeys: [
1580
+ postsListQueryKey(),
1581
+ postsRetrieveQueryKey({ path: { id } }),
1582
+ ],
1583
+ })
1584
+ }
1585
+
1586
+ export function useDeletePost() {
1587
+ return useApiMutation({
1588
+ ...postsDestroyMutation(),
1589
+ invalidateKeys: [postsListQueryKey()],
1590
+ })
1591
+ }
1592
+ \`\`\`
1593
+
1594
+ ### Key Rules
1595
+
1596
+ 1. **Never use raw \`useQuery\` or \`useMutation\`** \u2014 always go through \`useApiQuery\` / \`useApiMutation\`
1597
+ 2. **Never manage API error state with \`useState\`** \u2014 error state is derived from TanStack Query's \`error\` field
1598
+ 3. **Always pass \`invalidateKeys\`** on mutations that modify data \u2014 ensures the UI stays in sync
1599
+ 4. **Use generated options/mutations** from \`@/api/generated/@tanstack/react-query.gen\` \u2014 never write \`queryFn\` manually
1600
+ 5. **Use \`select\`** to reshape API responses at the hook level, not in components
1601
+ 6. **Use \`enabled\`** for conditional queries (e.g. waiting for an ID from URL params)
1602
+ 7. **Spread generated options first** (\`...postsListOptions()\`), then add overrides \u2014 this preserves the generated \`queryKey\` and \`queryFn\`
1603
+ `;
1604
+ }
1605
+ };
1606
+
1607
+ // src/skills/page-structure.ts
1608
+ var pageStructureSkill = {
1609
+ id: "page-structure",
1610
+ name: "Page & Route Structure",
1611
+ description: "Page folders, feature modules, routing conventions, and route composition patterns.",
1612
+ render(_ctx) {
1613
+ return `## Page & Route Structure
1614
+
1615
+ > **RULE: Every page and feature owns its own \`routes.tsx\`. The central router only composes them \u2014 never import page components directly into \`routes.tsx\`.**
1616
+
1617
+ ### Standalone Pages (\`src/pages/\`)
1618
+
1619
+ Each page gets its own folder:
1620
+
1621
+ \`\`\`
1622
+ pages/<page>/
1623
+ \u251C\u2500\u2500 <page>.tsx # Page component (default export)
1624
+ \u251C\u2500\u2500 routes.tsx # Exports RouteObject[] for this page
1625
+ \u251C\u2500\u2500 index.ts # Re-exports public members (routes)
1626
+ \u251C\u2500\u2500 components/ # Components private to this page (optional)
1627
+ \u2514\u2500\u2500 hooks/ # Hooks private to this page (optional)
1628
+ \`\`\`
1629
+
1630
+ **\`routes.tsx\`** \u2014 defines the route config using the \`Path\` enum:
1631
+ \`\`\`tsx
1632
+ import type { RouteObject } from 'react-router-dom'
1633
+ import { Path } from '@/router/paths'
1634
+ import SettingsPage from './settings'
1635
+
1636
+ export const settingsRoutes: RouteObject[] = [
1637
+ { path: Path.Settings, element: <SettingsPage /> },
1638
+ ]
1639
+ \`\`\`
1640
+
1641
+ **\`index.ts\`** \u2014 re-exports only public members:
1642
+ \`\`\`ts
1643
+ export { settingsRoutes } from './routes'
1644
+ \`\`\`
1645
+
1646
+ ### Feature Pages (\`src/features/\`)
1647
+
1648
+ Features that have pages include a \`routes.tsx\` at the feature root:
1649
+
1650
+ \`\`\`
1651
+ features/<feature>/
1652
+ \u251C\u2500\u2500 components/ # UI components scoped to this feature
1653
+ \u251C\u2500\u2500 hooks/ # Custom hooks (queries, mutations, logic)
1654
+ \u251C\u2500\u2500 pages/ # Page components (default exports)
1655
+ \u251C\u2500\u2500 routes.tsx # RouteObject[] for all pages in this feature
1656
+ \u2514\u2500\u2500 index.ts # Re-exports routes + public API
1657
+ \`\`\`
1658
+
1659
+ **\`routes.tsx\`** \u2014 groups related routes using the \`Path\` enum:
1660
+ \`\`\`tsx
1661
+ import { Outlet, type RouteObject } from 'react-router-dom'
1662
+ import { Path } from '@/router/paths'
1663
+ import PostsPage from './pages/posts-page'
1664
+ import PostDetailPage from './pages/post-detail-page'
1665
+
1666
+ export const postsRoutes: RouteObject[] = [
1667
+ {
1668
+ path: Path.Posts,
1669
+ element: <Outlet />,
1670
+ children: [
1671
+ { index: true, element: <PostsPage /> },
1672
+ { path: ':id', element: <PostDetailPage /> },
1673
+ ],
1674
+ },
1675
+ ]
1676
+ \`\`\`
1677
+
1678
+ **\`index.ts\`** \u2014 exports routes first:
1679
+ \`\`\`ts
1680
+ export { postsRoutes } from './routes'
1681
+ export { usePosts } from './hooks/use-posts'
1682
+ export { useCreatePost, useUpdatePost, useDeletePost } from './hooks/use-post-mutations'
1683
+ \`\`\`
1684
+
1685
+ ### Route Paths (\`src/router/paths.ts\`)
1686
+
1687
+ All route paths are defined in a central \`Path\` enum \u2014 **never use hardcoded path strings**:
1688
+
1689
+ \`\`\`ts
1690
+ export enum Path {
1691
+ Home = '/',
1692
+ Login = '/login',
1693
+ Register = '/register',
1694
+ ForgotPassword = '/forgot-password',
1695
+ ResetPassword = '/reset-password/:token',
1696
+ Dashboard = '/dashboard',
1697
+ // blacksmith:path
1698
+ }
1699
+ \`\`\`
1700
+
1701
+ Use \`Path\` everywhere \u2014 in route definitions, \`navigate()\`, \`<Link to={}\`\`, etc.:
1702
+ \`\`\`tsx
1703
+ import { Path } from '@/router/paths'
1704
+
1705
+ // In routes
1706
+ { path: Path.Dashboard, element: <DashboardPage /> }
1707
+
1708
+ // In navigation
1709
+ navigate(Path.Login)
1710
+ <Link to={Path.Home}>Home</Link>
1711
+ \`\`\`
1712
+
1713
+ For dynamic paths, use the \`buildPath\` helper:
1714
+ \`\`\`ts
1715
+ import { Path, buildPath } from '@/router/paths'
1716
+
1717
+ buildPath(Path.ResetPassword, { token: 'abc123' })
1718
+ // => '/reset-password/abc123'
1719
+ \`\`\`
1720
+
1721
+ The \`Path\` enum is re-exported from \`@/router\` along with \`buildPath\`.
1722
+
1723
+ ### Central Router (\`src/router/routes.tsx\`)
1724
+
1725
+ The central router imports and spreads route arrays \u2014 it never imports page components directly:
1726
+
1727
+ \`\`\`tsx
1728
+ import { homeRoutes } from '@/pages/home'
1729
+ import { dashboardRoutes } from '@/pages/dashboard'
1730
+ import { authRoutes } from '@/features/auth'
1731
+ import { postsRoutes } from '@/features/posts'
1732
+ // blacksmith:import
1733
+
1734
+ const publicRoutes: RouteObject[] = [
1735
+ ...homeRoutes,
1736
+ ]
1737
+
1738
+ const privateRoutes: RouteObject[] = [
1739
+ ...dashboardRoutes,
1740
+ ...postsRoutes,
1741
+ // blacksmith:routes
1742
+ ]
1743
+ \`\`\`
1744
+
1745
+ ### Auto-Registration
1746
+
1747
+ \`blacksmith make:resource\` automatically registers routes using marker comments:
1748
+ - \`// blacksmith:path\` \u2014 new \`Path\` enum entry is inserted above this marker in \`paths.ts\`
1749
+ - \`// blacksmith:import\` \u2014 new import line is inserted above this marker in \`routes.tsx\`
1750
+ - \`// blacksmith:routes\` \u2014 new spread line is inserted above this marker in \`routes.tsx\`
1751
+
1752
+ Never remove these markers. They must stay in the \`Path\` enum, \`privateRoutes\` array, and import block.
1753
+
1754
+ ### When to Use Pages vs Features
1755
+
1756
+ | Use \`pages/<page>/\` | Use \`features/<feature>/\` |
1757
+ |---|---|
1758
+ | Standalone pages (home, dashboard, settings) | CRUD resources with multiple pages |
1759
+ | No shared hooks or components | Has hooks, components, and pages that belong together |
1760
+ | Single route | Multiple related routes (list + detail + edit) |
1761
+
1762
+ ### Component Decomposition
1763
+
1764
+ > **RULE: Pages are orchestrators, not monoliths. Break every page into small, focused child components stored in \`components/\`.**
1765
+
1766
+ A page component should read data (via hooks), pass it down as props, and compose child components. It should contain minimal JSX itself.
1767
+
1768
+ \`\`\`
1769
+ pages/dashboard/
1770
+ \u251C\u2500\u2500 dashboard.tsx # Page: composes children, calls hooks
1771
+ \u251C\u2500\u2500 components/
1772
+ \u2502 \u251C\u2500\u2500 stats-cards.tsx # Renders the stats grid
1773
+ \u2502 \u251C\u2500\u2500 recent-activity.tsx # Renders activity feed
1774
+ \u2502 \u2514\u2500\u2500 quick-actions.tsx # Renders action buttons
1775
+ \u251C\u2500\u2500 hooks/
1776
+ \u2502 \u2514\u2500\u2500 use-dashboard-data.ts # All data fetching for this page
1777
+ \u251C\u2500\u2500 routes.tsx
1778
+ \u2514\u2500\u2500 index.ts
1779
+ \`\`\`
1780
+
1781
+ \`\`\`tsx
1782
+ // dashboard.tsx \u2014 thin orchestrator using @blacksmith-ui/react layout
1783
+ import { Stack, Grid, Divider } from '@blacksmith-ui/react'
1784
+ import { StatsCards } from './components/stats-cards'
1785
+ import { RecentActivity } from './components/recent-activity'
1786
+ import { QuickActions } from './components/quick-actions'
1787
+ import { useDashboardData } from './hooks/use-dashboard-data'
1788
+
1789
+ export default function DashboardPage() {
1790
+ const { stats, activity, isLoading } = useDashboardData()
1791
+
1792
+ return (
1793
+ <Stack gap={6}>
1794
+ <StatsCards stats={stats} isLoading={isLoading} />
1795
+ <Divider />
1796
+ <Grid columns={{ base: 1, lg: 3 }} gap={6}>
1797
+ <RecentActivity items={activity} isLoading={isLoading} className="lg:col-span-2" />
1798
+ <QuickActions />
1799
+ </Grid>
1800
+ </Stack>
1801
+ )
1802
+ }
1803
+ \`\`\`
1804
+
1805
+ **When to extract a child component:**
1806
+ - A section of JSX exceeds ~30 lines
1807
+ - A block has its own loading/error state
1808
+ - A block is logically independent (e.g. a table, a form, a sidebar)
1809
+ - A block could be reused on another page (move to \`shared/\` or the feature's \`components/\`)
1810
+
1811
+ **When NOT to extract:**
1812
+ - A few lines of simple, static markup (headings, wrappers)
1813
+ - Extracting would just move props through another layer with no clarity gain
1814
+
1815
+ ### Separating Logic into Hooks
1816
+
1817
+ > **RULE: Components render. Hooks think. Never mix data fetching, transformations, or complex state logic into component bodies.**
1818
+
1819
+ Extract logic into hooks in the \`hooks/\` folder co-located with the page or feature:
1820
+
1821
+ \`\`\`tsx
1822
+ // BAD \u2014 logic mixed into the component
1823
+ export default function OrdersPage() {
1824
+ const [page, setPage] = useState(1)
1825
+ const [search, setSearch] = useState('')
1826
+ const debouncedSearch = useDebounce(search, 300)
1827
+ const { data, isLoading } = useApiQuery({
1828
+ ...ordersListOptions({ query: { page, search: debouncedSearch } }),
1829
+ select: (d: any) => ({ orders: d.results ?? [], total: d.count ?? 0 }),
1830
+ })
1831
+
1832
+ const deleteOrder = useApiMutation({
1833
+ ...ordersDestroyMutation(),
1834
+ invalidateKeys: [ordersListQueryKey()],
1835
+ })
1836
+
1837
+ return ( /* 200 lines of JSX using all of the above */ )
1838
+ }
1839
+ \`\`\`
1840
+
1841
+ \`\`\`tsx
1842
+ // GOOD \u2014 logic in a hook, component just renders
1843
+ // hooks/use-orders-page.ts
1844
+ export function useOrdersPage() {
1845
+ const [page, setPage] = useState(1)
1846
+ const [search, setSearch] = useState('')
1847
+ const debouncedSearch = useDebounce(search, 300)
1848
+
1849
+ const { data, isLoading, errorMessage } = useOrders({
1850
+ page,
1851
+ search: debouncedSearch,
1852
+ })
1853
+
1854
+ const deleteOrder = useDeleteOrder()
1855
+
1856
+ return { orders: data?.orders ?? [], total: data?.total ?? 0, isLoading, errorMessage, page, setPage, search, setSearch, deleteOrder }
1857
+ }
1858
+
1859
+ // orders-page.tsx
1860
+ import { Stack } from '@blacksmith-ui/react'
1861
+ import { useOrdersPage } from './hooks/use-orders-page'
1862
+ import { OrdersTable } from './components/orders-table'
1863
+ import { OrdersToolbar } from './components/orders-toolbar'
1864
+
1865
+ export default function OrdersPage() {
1866
+ const { orders, total, isLoading, page, setPage, search, setSearch, deleteOrder } = useOrdersPage()
1867
+
1868
+ return (
1869
+ <Stack gap={4}>
1870
+ <OrdersToolbar search={search} onSearchChange={setSearch} />
1871
+ <OrdersTable orders={orders} isLoading={isLoading} onDelete={(id) => deleteOrder.mutate({ path: { id } })} />
1872
+ </Stack>
1873
+ )
1874
+ }
1875
+ \`\`\`
1876
+
1877
+ **What goes into a hook:**
1878
+ - API queries and mutations
1879
+ - Derived/computed state
1880
+ - Debouncing, pagination, filtering logic
1881
+ - Form setup (\`useForm\`, schema, submit handler)
1882
+ - Any \`useEffect\` or \`useState\` beyond a simple UI toggle
1883
+
1884
+ **What stays in the component:**
1885
+ - Simple UI toggles (\`useState\` for a modal open/close is fine inline)
1886
+ - JSX composition and prop passing
1887
+ - Event handler wiring (calling \`hook.mutate()\`, \`navigate()\`, etc.)
1888
+
1889
+ ### Key Rules
1890
+
1891
+ 1. **Every page/feature owns its routes** \u2014 the route config lives next to the page, not in the central router
1892
+ 2. **Use the \`Path\` enum for all route paths** \u2014 never hardcode path strings; import \`Path\` from \`@/router/paths\`
1893
+ 3. **\`index.ts\` is the public API** \u2014 only export what other modules need (routes, hooks, components)
1894
+ 4. **Page components use default exports** \u2014 this is the one exception to the named-export convention
1895
+ 5. **Routes use named exports** \u2014 \`export const settingsRoutes\` not \`export default\`
1896
+ 6. **Private components/hooks stay in the page folder** \u2014 if only one page uses it, co-locate it there
1897
+ 7. **Never import across page folders** \u2014 if something is shared, move it to \`shared/\` or a feature
1898
+ 8. **Keep marker comments intact** \u2014 \`// blacksmith:path\`, \`// blacksmith:import\`, and \`// blacksmith:routes\` are required for auto-registration
1899
+ 9. **Pages are orchestrators** \u2014 break pages into child components in \`components/\`, extract logic into hooks in \`hooks/\`
1900
+ 10. **Components render, hooks think** \u2014 never put data fetching, transformations, or complex state directly in component bodies
1901
+ `;
1902
+ }
1903
+ };
1904
+
1905
+ // src/skills/blacksmith-ui-react.ts
1906
+ var blacksmithUiReactSkill = {
1907
+ id: "blacksmith-ui-react",
1908
+ name: "@blacksmith-ui/react",
1909
+ description: "Core UI component library \u2014 60+ components for layout, typography, inputs, data display, overlays, feedback, media, and navigation.",
1910
+ render(_ctx) {
1911
+ return `## @blacksmith-ui/react \u2014 Core UI Components (60+)
1912
+
1913
+ > **CRITICAL RULE: Every UI element MUST be built using \`@blacksmith-ui/react\` components \u2014 including layout and typography.**
1914
+ > Do NOT use raw HTML elements when a Blacksmith-UI component exists for that purpose.
1915
+ > This includes layout: use \`Flex\`, \`Stack\`, \`Grid\`, \`Box\`, \`Container\` instead of \`<div>\` with flex/grid classes.
1916
+ > This includes typography: use \`Text\` and \`Typography\` instead of raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, \`<span>\`.
1917
+
1918
+ ### Layout
1919
+
1920
+ | Component | Use instead of | Description |
1921
+ |-----------|---------------|-------------|
1922
+ | \`Box\` | \`<div>\` | Base layout primitive with style props |
1923
+ | \`Flex\` | \`<div className="flex ...">\` | Flexbox container with style props (\`direction\`, \`align\`, \`justify\`, \`gap\`, \`wrap\`) |
1924
+ | \`Grid\` | \`<div className="grid ...">\` | CSS Grid container (\`columns\`, \`rows\`, \`gap\`) |
1925
+ | \`Stack\` | \`<div className="flex flex-col gap-...">\` | Vertical/horizontal stack (\`direction\`, \`gap\`) |
1926
+ | \`Container\` | \`<div className="max-w-7xl mx-auto px-...">\` | Max-width centered container |
1927
+ | \`Divider\` | \`<hr>\` or border hacks | Visual separator (horizontal/vertical) |
1928
+ | \`AspectRatio\` | padding-bottom trick | Maintain aspect ratio for content |
1929
+ | \`Resizable\` | custom resize logic | Resizable panel groups |
1930
+ | \`ScrollArea\` | \`overflow-auto\` divs | Custom scrollbar container |
1931
+
1932
+ ### Typography
1933
+
1934
+ | Component | Use instead of | Description |
1935
+ |-----------|---------------|-------------|
1936
+ | \`Text\` | \`<p>\`, \`<span>\` | Text display with style props (\`size\`, \`weight\`, \`color\`, \`align\`) |
1937
+ | \`Typography\` | \`<h1>\`\u2013\`<h6>\`, \`<p>\` | Semantic heading/paragraph elements (\`variant\`: h1\u2013h6, p, lead, muted, etc.) |
1938
+ | \`Label\` | \`<label>\` | Form label with accessibility support |
1939
+
1940
+ ### Cards & Containers
1941
+
1942
+ - \`Card\`, \`CardHeader\`, \`CardTitle\`, \`CardDescription\`, \`CardContent\`, \`CardFooter\` \u2014 Use instead of styled \`<div>\` containers
1943
+ - \`StatCard\` \u2014 Use for metric/stat display (value, label, trend)
1944
+ - \`EmptyState\` \u2014 Use for empty content placeholders instead of custom empty divs
1945
+
1946
+ ### Actions
1947
+
1948
+ - \`Button\` \u2014 Use instead of \`<button>\` or \`<a>\` styled as buttons
1949
+ - Variants: \`default\`, \`secondary\`, \`destructive\`, \`outline\`, \`ghost\`, \`link\`
1950
+ - Sizes: \`sm\`, \`default\`, \`lg\`, \`icon\`
1951
+ - \`Toggle\`, \`ToggleGroup\` \u2014 Use for toggle buttons
1952
+ - \`DropdownMenu\`, \`DropdownMenuTrigger\`, \`DropdownMenuContent\`, \`DropdownMenuItem\`, \`DropdownMenuSeparator\`, \`DropdownMenuLabel\` \u2014 Use for action menus
1953
+ - \`ContextMenu\` \u2014 Use for right-click menus
1954
+ - \`Menubar\` \u2014 Use for application menu bars
1955
+ - \`AlertDialog\`, \`AlertDialogTrigger\`, \`AlertDialogContent\`, \`AlertDialogAction\`, \`AlertDialogCancel\` \u2014 Use for destructive action confirmations
1956
+
1957
+ ### Data Entry
1958
+
1959
+ - \`Input\` \u2014 Use instead of \`<input>\`
1960
+ - \`SearchInput\` \u2014 Use for search fields (has built-in search icon)
1961
+ - \`Textarea\` \u2014 Use instead of \`<textarea>\`
1962
+ - \`NumberInput\` \u2014 Use for numeric inputs with increment/decrement
1963
+ - \`Select\`, \`SelectTrigger\`, \`SelectContent\`, \`SelectItem\`, \`SelectValue\` \u2014 Use instead of \`<select>\`
1964
+ - \`Checkbox\` \u2014 Use instead of \`<input type="checkbox">\`
1965
+ - \`RadioGroup\`, \`RadioGroupItem\` \u2014 Use instead of \`<input type="radio">\`
1966
+ - \`Switch\` \u2014 Use for toggle switches
1967
+ - \`Slider\` \u2014 Use for single range inputs
1968
+ - \`RangeSlider\` \u2014 Use for dual-handle range selection
1969
+ - \`DatePicker\` \u2014 Use for date selection with calendar popup
1970
+ - \`PinInput\` / \`InputOTP\` \u2014 Use for PIN/OTP code entry
1971
+ - \`ColorPicker\` \u2014 Use for color selection
1972
+ - \`FileUpload\` \u2014 Use for file upload with drag & drop
1973
+ - \`TagInput\` \u2014 Use for tag/chip input with add/remove
1974
+ - \`Rating\` \u2014 Use for star/icon rating selection
1975
+ - \`Label\` \u2014 Use instead of \`<label>\`
1976
+
1977
+ ### Data Display
1978
+
1979
+ - \`Table\`, \`TableHeader\`, \`TableBody\`, \`TableRow\`, \`TableHead\`, \`TableCell\` \u2014 Use instead of \`<table>\` elements
1980
+ - \`DataTable\` \u2014 Use for feature-rich tables with sorting, filtering, and pagination
1981
+ - \`Badge\` \u2014 Use for status indicators, tags, counts (variants: \`default\`, \`secondary\`, \`destructive\`, \`outline\`)
1982
+ - \`Avatar\`, \`AvatarImage\`, \`AvatarFallback\` \u2014 Use for user profile images
1983
+ - \`Tooltip\`, \`TooltipTrigger\`, \`TooltipContent\`, \`TooltipProvider\` \u2014 Use for hover hints
1984
+ - \`HoverCard\` \u2014 Use for rich hover content
1985
+ - \`Calendar\` \u2014 Use for full calendar display
1986
+ - \`Chart\` \u2014 Use for data visualization (powered by Recharts)
1987
+ - \`Timeline\` \u2014 Use for chronological event display
1988
+ - \`Tree\` \u2014 Use for hierarchical tree views
1989
+ - \`List\` \u2014 Use for structured list display instead of \`<ul>\`/\`<ol>\`
1990
+ - \`Skeleton\` \u2014 Use for loading placeholders
1991
+ - \`Spinner\` \u2014 Use for loading indicators
1992
+ - \`Progress\` \u2014 Use for progress bars
1993
+ - \`Pagination\`, \`PaginationContent\`, \`PaginationItem\`, \`PaginationLink\`, \`PaginationNext\`, \`PaginationPrevious\` \u2014 Use for paginated lists
1994
+
1995
+ ### Tabs & Accordion
1996
+
1997
+ - \`Tabs\`, \`TabsList\`, \`TabsTrigger\`, \`TabsContent\` \u2014 Use for tabbed interfaces
1998
+ - \`Accordion\`, \`AccordionItem\`, \`AccordionTrigger\`, \`AccordionContent\` \u2014 Use for collapsible sections
1999
+
2000
+ ### Overlays
2001
+
2002
+ - \`Dialog\`, \`DialogTrigger\`, \`DialogContent\`, \`DialogHeader\`, \`DialogTitle\`, \`DialogDescription\`, \`DialogFooter\` \u2014 Use for modals
2003
+ - \`AlertDialog\` \u2014 Use for confirmation dialogs
2004
+ - \`Drawer\` / \`Sheet\`, \`SheetTrigger\`, \`SheetContent\`, \`SheetHeader\`, \`SheetTitle\`, \`SheetDescription\` \u2014 Use for slide-out panels
2005
+ - \`Popover\` \u2014 Use for floating content panels
2006
+ - \`CommandPalette\` \u2014 Use for searchable command menus (cmdk-based)
2007
+
2008
+ ### Navigation
2009
+
2010
+ - \`Breadcrumb\`, \`BreadcrumbList\`, \`BreadcrumbItem\`, \`BreadcrumbLink\`, \`BreadcrumbSeparator\` \u2014 Use for breadcrumb trails
2011
+ - \`NavigationMenu\`, \`NavigationMenuList\`, \`NavigationMenuItem\`, \`NavigationMenuTrigger\`, \`NavigationMenuContent\` \u2014 Use for site navigation
2012
+ - \`Sidebar\` \u2014 Use for app sidebars
2013
+ - \`Dock\` \u2014 Use for macOS-style dock navigation
2014
+ - \`BackToTop\` \u2014 Use for scroll-to-top buttons
2015
+
2016
+ ### Feedback
2017
+
2018
+ - \`Alert\`, \`AlertTitle\`, \`AlertDescription\` \u2014 Use for inline messages/warnings
2019
+ - \`AlertBanner\` \u2014 Use for full-width alert banners
2020
+ - \`Toast\` / \`Toaster\` / \`useToast\` \u2014 Use for transient notifications
2021
+ - \`SonnerToaster\` \u2014 Sonner-based toast notifications
2022
+
2023
+ ### Media
2024
+
2025
+ - \`Image\` \u2014 Use instead of \`<img>\` for optimized image display
2026
+ - \`VideoPlayer\` \u2014 Use for video playback
2027
+ - \`CodeBlock\` \u2014 Use for syntax-highlighted code (Shiki-powered)
2028
+ - \`Carousel\` \u2014 Use for image/content carousels
2029
+ - \`Lightbox\` \u2014 Use for full-screen media viewers
2030
+
2031
+ ### Specialized
2032
+
2033
+ - \`Stepper\` / \`Wizard\` \u2014 Use for multi-step workflows
2034
+ - \`NotificationCenter\` / \`useNotificationCenter\` \u2014 Use for notification management
2035
+ - \`SpotlightTour\` \u2014 Use for guided feature tours
2036
+
2037
+ ### Utilities & Hooks
2038
+
2039
+ - \`cn()\` \u2014 Merge class names (clsx + tailwind-merge)
2040
+ - \`useToast()\` \u2014 Programmatic toast notifications
2041
+ - \`useMobile()\` \u2014 Responsive breakpoint detection
2042
+ - \`useDarkMode()\` \u2014 Dark mode toggle. Returns \`{ isDark, toggle }\`
2043
+
2044
+ ---
2045
+
2046
+ ### Component-First Rules
2047
+
2048
+ 1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<Grid>\`, \`<Stack>\`, \`<Box>\` from \`@blacksmith-ui/react\`.
2049
+ 2. **Centering/max-width**: NEVER use \`<div className="max-w-7xl mx-auto px-...">\`. Use \`<Container>\`.
2050
+ 3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<Typography variant="h2">\` or \`<Text>\`.
2051
+ 4. **Separators**: NEVER use \`<hr>\` or border hacks. Use \`<Divider>\`.
2052
+ 5. **Images**: NEVER use raw \`<img>\`. Use \`<Image>\` from \`@blacksmith-ui/react\` (use \`Avatar\` for profile pictures).
2053
+ 6. **Lists**: NEVER use \`<ul>\`/\`<ol>\` for structured display lists. Use \`<List>\` from \`@blacksmith-ui/react\`. Plain \`<ul>\`/\`<ol>\` is only acceptable for simple inline content lists.
2054
+ 7. **Buttons**: NEVER use \`<button>\` or \`<a>\` styled as a button. Use \`<Button>\`.
2055
+ 8. **Inputs**: NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` directly. Use the Blacksmith-UI equivalents.
2056
+ 9. **Cards**: NEVER use a styled \`<div>\` as a card. Use \`Card\` + sub-components.
2057
+ 10. **Tables**: NEVER use raw \`<table>\` HTML. Use \`Table\` or \`DataTable\`.
2058
+ 11. **Loading**: NEVER use custom \`animate-pulse\` divs. Use \`Skeleton\` or \`Spinner\`.
2059
+ 12. **Modals**: NEVER build custom modals. Use \`Dialog\`, \`AlertDialog\`, \`Drawer\`, or \`Sheet\`.
2060
+ 13. **Feedback**: NEVER use plain styled text for errors/warnings. Use \`Alert\` or \`useToast\`.
2061
+ 14. **Empty states**: NEVER build custom empty-state UIs. Use \`EmptyState\`.
2062
+ 15. **Metrics**: NEVER build custom stat/metric cards. Use \`StatCard\`.
2063
+
2064
+ ### When Raw HTML IS Acceptable
2065
+
2066
+ - \`<main>\`, \`<section>\`, \`<header>\`, \`<footer>\`, \`<nav>\`, \`<article>\`, \`<aside>\` \u2014 semantic HTML landmarks for page structure (but use \`Flex\`/\`Stack\`/\`Grid\` inside them for layout)
2067
+ - \`<Link>\` from react-router-dom \u2014 for page navigation (use \`<Button asChild><Link>...</Link></Button>\` if it needs button styling)
2068
+ - Icon components from \`lucide-react\`
2069
+ - \`<form>\` element when used with React Hook Form (but use \`@blacksmith-ui/forms\` components inside)
2070
+
2071
+ ### Design Tokens & Theming
2072
+
2073
+ - \`ThemeProvider\` \u2014 Wrap app to apply preset or custom theme
2074
+ - Built-in presets: \`default\`, \`blue\`, \`green\`, \`violet\`, \`red\`, \`neutral\`
2075
+ - All components use HSL CSS variables (\`--background\`, \`--foreground\`, \`--primary\`, etc.)
2076
+ - Dark mode: \`.dark\` class strategy on \`<html>\`, or \`<ThemeProvider mode="dark">\`
2077
+ - Border radius: controlled by \`--radius\` CSS variable
2078
+ - Extend with \`className\` prop + \`cn()\` utility for custom styles
2079
+ - Global styles: \`@import '@blacksmith-ui/react/styles.css'\` in app entry
2080
+
2081
+ ### Example: HowItWorks Section (Correct Way)
2082
+
2083
+ \`\`\`tsx
2084
+ import { Container, Stack, Flex, Grid, Text, Typography, Image } from '@blacksmith-ui/react'
2085
+ import { howItWorksSteps } from '../data'
2086
+
2087
+ export function HowItWorks() {
2088
+ return (
2089
+ <Box as="section" className="py-16 sm:py-20">
2090
+ <Container>
2091
+ <Stack gap={3} align="center" className="mb-12">
2092
+ <Typography variant="h2">How It Works</Typography>
2093
+ <Text color="muted">Book your stay in three simple steps</Text>
2094
+ </Stack>
2095
+
2096
+ <Grid columns={{ base: 1, md: 3 }} gap={8} className="max-w-4xl mx-auto">
2097
+ {howItWorksSteps.map((item) => (
2098
+ <Stack key={item.step} align="center" gap={4}>
2099
+ <Box className="relative">
2100
+ <Flex align="center" justify="center" className="h-16 w-16 rounded-full bg-primary text-primary-foreground shadow-lg shadow-primary/30">
2101
+ <item.icon className="h-7 w-7" />
2102
+ </Flex>
2103
+ <Flex align="center" justify="center" className="absolute -top-1 -right-1 h-6 w-6 rounded-full bg-background border-2 border-primary">
2104
+ <Text size="xs" weight="bold" color="primary">{item.step}</Text>
2105
+ </Flex>
2106
+ </Box>
2107
+ <Stack gap={2} align="center">
2108
+ <Text size="lg" weight="bold">{item.title}</Text>
2109
+ <Text size="sm" color="muted" align="center" className="max-w-xs">
2110
+ {item.description}
2111
+ </Text>
2112
+ </Stack>
2113
+ </Stack>
2114
+ ))}
2115
+ </Grid>
2116
+ </Container>
2117
+ </Box>
2118
+ )
2119
+ }
2120
+ \`\`\`
2121
+
2122
+ ### Example: Resource List Page (Correct Way)
2123
+
2124
+ \`\`\`tsx
2125
+ import {
2126
+ Stack, Flex,
2127
+ Card, CardHeader, CardTitle, CardContent,
2128
+ Button, Badge, Skeleton,
2129
+ Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
2130
+ DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
2131
+ AlertDialog, AlertDialogTrigger, AlertDialogContent,
2132
+ AlertDialogAction, AlertDialogCancel,
2133
+ } from '@blacksmith-ui/react'
2134
+ import { MoreHorizontal, Plus, Trash2, Edit } from 'lucide-react'
2135
+ import { Link } from 'react-router-dom'
2136
+
2137
+ function ResourceListPage({ resources, isLoading, onDelete }) {
2138
+ if (isLoading) {
2139
+ return (
2140
+ <Card>
2141
+ <CardContent className="p-6">
2142
+ <Stack gap={4}>
2143
+ {Array.from({ length: 5 }).map((_, i) => (
2144
+ <Skeleton key={i} className="h-12 w-full" />
2145
+ ))}
2146
+ </Stack>
2147
+ </CardContent>
2148
+ </Card>
2149
+ )
2150
+ }
2151
+
2152
+ return (
2153
+ <Card>
2154
+ <CardHeader>
2155
+ <Flex align="center" justify="between">
2156
+ <CardTitle>Resources</CardTitle>
2157
+ <Button asChild>
2158
+ <Link to="/resources/new"><Plus className="mr-2 h-4 w-4" /> Create</Link>
2159
+ </Button>
2160
+ </Flex>
2161
+ </CardHeader>
2162
+ <CardContent>
2163
+ <Table>
2164
+ <TableHeader>
2165
+ <TableRow>
2166
+ <TableHead>Title</TableHead>
2167
+ <TableHead>Status</TableHead>
2168
+ <TableHead className="w-12" />
2169
+ </TableRow>
2170
+ </TableHeader>
2171
+ <TableBody>
2172
+ {resources.map((r) => (
2173
+ <TableRow key={r.id}>
2174
+ <TableCell>{r.title}</TableCell>
2175
+ <TableCell><Badge variant="outline">{r.status}</Badge></TableCell>
2176
+ <TableCell>
2177
+ <DropdownMenu>
2178
+ <DropdownMenuTrigger asChild>
2179
+ <Button variant="ghost" size="icon">
2180
+ <MoreHorizontal className="h-4 w-4" />
2181
+ </Button>
2182
+ </DropdownMenuTrigger>
2183
+ <DropdownMenuContent>
2184
+ <DropdownMenuItem asChild>
2185
+ <Link to={\`/resources/\${r.id}/edit\`}>
2186
+ <Edit className="mr-2 h-4 w-4" /> Edit
2187
+ </Link>
2188
+ </DropdownMenuItem>
2189
+ <AlertDialog>
2190
+ <AlertDialogTrigger asChild>
2191
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
2192
+ <Trash2 className="mr-2 h-4 w-4" /> Delete
2193
+ </DropdownMenuItem>
2194
+ </AlertDialogTrigger>
2195
+ <AlertDialogContent>
2196
+ <AlertDialogAction onClick={() => onDelete(r.id)}>
2197
+ Delete
2198
+ </AlertDialogAction>
2199
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
2200
+ </AlertDialogContent>
2201
+ </AlertDialog>
2202
+ </DropdownMenuContent>
2203
+ </DropdownMenu>
2204
+ </TableCell>
2205
+ </TableRow>
2206
+ ))}
2207
+ </TableBody>
2208
+ </Table>
2209
+ </CardContent>
2210
+ </Card>
2211
+ )
2212
+ }
2213
+ \`\`\`
2214
+ `;
2215
+ }
2216
+ };
2217
+
2218
+ // src/skills/blacksmith-ui-forms.ts
2219
+ var blacksmithUiFormsSkill = {
2220
+ id: "blacksmith-ui-forms",
2221
+ name: "@blacksmith-ui/forms",
2222
+ description: "Form components using React Hook Form + Zod for validation and submission.",
2223
+ render(_ctx) {
2224
+ return `## @blacksmith-ui/forms \u2014 Form Components (React Hook Form + Zod)
2225
+
2226
+ > **RULE: ALWAYS use these for forms.** Do NOT build forms with raw \`<form>\`, \`<input>\`, \`<label>\`, or manual error display.
2227
+
2228
+ \`\`\`tsx
2229
+ import { Form, FormField, FormInput, FormTextarea, FormSelect } from '@blacksmith-ui/forms'
2230
+ \`\`\`
2231
+
2232
+ ### Components
2233
+
2234
+ - \`Form\` \u2014 Wraps the entire form. Props: \`form\` (useForm instance), \`onSubmit\`
2235
+ - \`FormField\` \u2014 Wraps each field. Props: \`name\`, \`label\`, \`description?\`
2236
+ - \`FormInput\` \u2014 Text input within FormField. Props: \`type\`, \`placeholder\`
2237
+ - \`FormTextarea\` \u2014 Textarea within FormField. Props: \`rows\`, \`placeholder\`
2238
+ - \`FormSelect\` \u2014 Select within FormField. Props: \`options\`, \`placeholder\`
2239
+ - \`FormCheckbox\` \u2014 Checkbox within FormField
2240
+ - \`FormSwitch\` \u2014 Toggle switch within FormField
2241
+ - \`FormRadioGroup\` \u2014 Radio group within FormField. Props: \`options\`
2242
+ - \`FormDatePicker\` \u2014 Date picker within FormField
2243
+ - \`FormError\` \u2014 Displays field-level validation error (auto-handled by FormField)
2244
+ - \`FormDescription\` \u2014 Displays helper text below a field
2245
+
2246
+ ### Rules
2247
+ - NEVER use raw \`<form>\` with manual \`<label>\` and error \`<p>\` tags. Always use \`Form\` + \`FormField\`.
2248
+ - NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` inside forms. Use \`FormInput\`, \`FormTextarea\`, \`FormSelect\`.
2249
+
2250
+ ### Form Pattern \u2014 ALWAYS follow this:
2251
+ \`\`\`tsx
2252
+ import { Form, FormField, FormInput, FormTextarea, FormSelect } from '@blacksmith-ui/forms'
2253
+ import { Button } from '@blacksmith-ui/react'
2254
+ import { useForm } from 'react-hook-form'
2255
+ import { zodResolver } from '@hookform/resolvers/zod'
2256
+ import { z } from 'zod'
2257
+
2258
+ const schema = z.object({
2259
+ title: z.string().min(1, 'Title is required'),
2260
+ description: z.string().optional(),
2261
+ status: z.enum(['draft', 'published']),
2262
+ })
2263
+
2264
+ type FormData = z.infer<typeof schema>
2265
+
2266
+ function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
2267
+ const form = useForm<FormData>({
2268
+ resolver: zodResolver(schema),
2269
+ defaultValues: { title: '', description: '', status: 'draft', ...defaultValues },
2270
+ })
2271
+
2272
+ return (
2273
+ <Form form={form} onSubmit={onSubmit}>
2274
+ <FormField name="title" label="Title">
2275
+ <FormInput placeholder="Enter title" />
2276
+ </FormField>
2277
+ <FormField name="description" label="Description">
2278
+ <FormTextarea rows={4} placeholder="Enter description" />
2279
+ </FormField>
2280
+ <FormField name="status" label="Status">
2281
+ <FormSelect options={[
2282
+ { label: 'Draft', value: 'draft' },
2283
+ { label: 'Published', value: 'published' },
2284
+ ]} />
2285
+ </FormField>
2286
+ <Button type="submit" disabled={isSubmitting}>
2287
+ {isSubmitting ? 'Saving...' : 'Save'}
2288
+ </Button>
2289
+ </Form>
2290
+ )
2291
+ }
2292
+ \`\`\`
2293
+
2294
+ ### Example: Detail Page with Edit Dialog
2295
+ \`\`\`tsx
2296
+ import {
2297
+ Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
2298
+ Button, Badge, Separator,
2299
+ Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter,
2300
+ Alert, AlertTitle, AlertDescription,
2301
+ } from '@blacksmith-ui/react'
2302
+ import { Form, FormField, FormInput, FormTextarea } from '@blacksmith-ui/forms'
2303
+ import { useForm } from 'react-hook-form'
2304
+ import { zodResolver } from '@hookform/resolvers/zod'
2305
+ import { z } from 'zod'
2306
+ import { Edit, ArrowLeft } from 'lucide-react'
2307
+ import { Link } from 'react-router-dom'
2308
+
2309
+ const editSchema = z.object({
2310
+ title: z.string().min(1, 'Required'),
2311
+ description: z.string().optional(),
2312
+ })
2313
+
2314
+ function ResourceDetailPage({ resource, onUpdate, error }) {
2315
+ const form = useForm({
2316
+ resolver: zodResolver(editSchema),
2317
+ defaultValues: { title: resource.title, description: resource.description },
2318
+ })
2319
+
2320
+ return (
2321
+ <Card>
2322
+ <CardHeader>
2323
+ <div className="flex items-center justify-between">
2324
+ <div>
2325
+ <CardTitle>{resource.title}</CardTitle>
2326
+ <CardDescription>Created {new Date(resource.created_at).toLocaleDateString()}</CardDescription>
2327
+ </div>
2328
+ <div className="flex gap-2">
2329
+ <Button variant="outline" asChild>
2330
+ <Link to="/resources"><ArrowLeft className="mr-2 h-4 w-4" /> Back</Link>
2331
+ </Button>
2332
+ <Dialog>
2333
+ <DialogTrigger asChild>
2334
+ <Button><Edit className="mr-2 h-4 w-4" /> Edit</Button>
2335
+ </DialogTrigger>
2336
+ <DialogContent>
2337
+ <DialogHeader>
2338
+ <DialogTitle>Edit Resource</DialogTitle>
2339
+ </DialogHeader>
2340
+ <Form form={form} onSubmit={onUpdate}>
2341
+ <FormField name="title" label="Title">
2342
+ <FormInput />
2343
+ </FormField>
2344
+ <FormField name="description" label="Description">
2345
+ <FormTextarea rows={4} />
2346
+ </FormField>
2347
+ <DialogFooter>
2348
+ <Button type="submit">Save Changes</Button>
2349
+ </DialogFooter>
2350
+ </Form>
2351
+ </DialogContent>
2352
+ </Dialog>
2353
+ </div>
2354
+ </div>
2355
+ </CardHeader>
2356
+ <Separator />
2357
+ <CardContent className="pt-6">
2358
+ {error && (
2359
+ <Alert variant="destructive" className="mb-4">
2360
+ <AlertTitle>Error</AlertTitle>
2361
+ <AlertDescription>{error}</AlertDescription>
2362
+ </Alert>
2363
+ )}
2364
+ <p>{resource.description || 'No description provided.'}</p>
2365
+ </CardContent>
2366
+ <CardFooter>
2367
+ <Badge>{resource.status}</Badge>
2368
+ </CardFooter>
2369
+ </Card>
2370
+ )
2371
+ }
2372
+ \`\`\`
2373
+ `;
2374
+ }
2375
+ };
2376
+
2377
+ // src/skills/blacksmith-ui-auth.ts
2378
+ var blacksmithUiAuthSkill = {
2379
+ id: "blacksmith-ui-auth",
2380
+ name: "@blacksmith-ui/auth",
2381
+ description: "Authentication UI components and hooks for login, registration, and password reset.",
2382
+ render(_ctx) {
2383
+ return `## @blacksmith-ui/auth \u2014 Authentication UI
2384
+
2385
+ > **RULE: ALWAYS use these for auth pages.** Do NOT build custom login/register forms.
2386
+
2387
+ \`\`\`tsx
2388
+ import { AuthProvider, LoginForm, RegisterForm, useAuth } from '@blacksmith-ui/auth'
2389
+ \`\`\`
2390
+
2391
+ ### Components
2392
+
2393
+ - \`AuthProvider\` \u2014 Context provider wrapping the app. Props: \`config: { adapter, socialProviders? }\`
2394
+ - \`LoginForm\` \u2014 Complete login form with email/password fields, validation, and links
2395
+ - Props: \`onSubmit: (data: { email, password }) => void\`, \`onRegisterClick\`, \`onForgotPasswordClick\`, \`error\`, \`loading\`
2396
+ - \`RegisterForm\` \u2014 Registration form with email, password, and display name
2397
+ - Props: \`onSubmit: (data: { email, password, displayName }) => void\`, \`onLoginClick\`, \`error\`, \`loading\`
2398
+ - \`ForgotPasswordForm\` \u2014 Password reset email request
2399
+ - Props: \`onSubmit: (data: { email }) => void\`, \`onLoginClick\`, \`error\`, \`loading\`
2400
+ - \`ResetPasswordForm\` \u2014 Set new password form
2401
+ - Props: \`onSubmit: (data: { password, code }) => void\`, \`code\`, \`onLoginClick\`, \`error\`, \`loading\`
2402
+
2403
+ ### Hooks
2404
+
2405
+ - \`useAuth\` \u2014 Hook for auth state and actions
2406
+ - Returns: \`user\`, \`loading\`, \`error\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)\`, \`socialProviders\`
2407
+
2408
+ ### Adapter
2409
+
2410
+ - \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter already configured in \`frontend/src/features/auth/adapter.ts\`)
2411
+
2412
+ ### Rules
2413
+ - NEVER build custom login/register forms. Use \`LoginForm\`, \`RegisterForm\`, etc. from \`@blacksmith-ui/auth\`.
2414
+ - NEVER manage auth state manually. Use \`useAuth\` hook.
2415
+ `;
2416
+ }
2417
+ };
2418
+
2419
+ // src/skills/blacksmith-hooks.ts
2420
+ var blacksmithHooksSkill = {
2421
+ id: "blacksmith-hooks",
2422
+ name: "@blacksmith-ui/hooks",
2423
+ description: "74 production-ready React hooks for state, DOM, timers, async, browser APIs, and layout.",
2424
+ render(_ctx) {
2425
+ return `## @blacksmith-ui/hooks \u2014 React Hooks Library
2426
+
2427
+ A collection of 74 production-ready React hooks. SSR-safe, fully typed, zero dependencies, tree-shakeable.
2428
+
2429
+ > **RULE: Use \`@blacksmith-ui/hooks\` instead of writing custom hooks when one exists for that purpose.**
2430
+ > Before creating a new hook, check if one already exists below.
2431
+
2432
+ \`\`\`tsx
2433
+ import { useToggle, useLocalStorage, useDebounce, useClickOutside } from '@blacksmith-ui/hooks'
2434
+ \`\`\`
2435
+
2436
+ ### State & Data
2437
+
2438
+ | Hook | Description |
2439
+ |------|-------------|
2440
+ | \`useToggle\` | Boolean state with \`toggle\`, \`on\`, \`off\` actions |
2441
+ | \`useDisclosure\` | Open/close/toggle state for modals, drawers, etc. |
2442
+ | \`useCounter\` | Numeric counter with optional min/max clamping |
2443
+ | \`useList\` | Array state with push, remove, update, insert, filter, clear |
2444
+ | \`useMap\` | Map state with set, remove, clear helpers |
2445
+ | \`useSet\` | Set state with add, remove, toggle, has, clear helpers |
2446
+ | \`useHistoryState\` | State with undo/redo history |
2447
+ | \`useDefault\` | State that falls back to a default when set to null/undefined |
2448
+ | \`useQueue\` | FIFO queue data structure |
2449
+ | \`useStack\` | LIFO stack data structure |
2450
+ | \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
2451
+ | \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
2452
+ | \`useUncontrolled\` | Controlled/uncontrolled component pattern helper |
2453
+
2454
+ ### Values & Memoization
2455
+
2456
+ | Hook | Description |
2457
+ |------|-------------|
2458
+ | \`useDebounce\` | Debounce a value with configurable delay |
2459
+ | \`useDebouncedCallback\` | Debounce a callback function |
2460
+ | \`useThrottle\` | Throttle a value with configurable interval |
2461
+ | \`useThrottledCallback\` | Throttle a callback function |
2462
+ | \`usePrevious\` | Track the previous value of a variable |
2463
+ | \`useLatest\` | Ref that always points to the latest value |
2464
+ | \`useConst\` | Compute a value once and return it on every render |
2465
+ | \`useSyncedRef\` | Keep a ref synchronized with the latest value |
2466
+
2467
+ ### DOM & Browser
2468
+
2469
+ | Hook | Description |
2470
+ |------|-------------|
2471
+ | \`useClickOutside\` | Detect clicks outside a ref element |
2472
+ | \`useEventListener\` | Attach event listeners to window or elements |
2473
+ | \`useElementSize\` | Track element width/height via ResizeObserver |
2474
+ | \`useHover\` | Track mouse hover state |
2475
+ | \`useKeyPress\` | Listen for a specific key press |
2476
+ | \`useKeyCombo\` | Listen for key + modifier combinations |
2477
+ | \`useLongPress\` | Detect long press gestures |
2478
+ | \`useFullscreen\` | Manage the Fullscreen API |
2479
+ | \`useTextSelection\` | Track currently selected text |
2480
+ | \`useFocusWithin\` | Track whether focus is inside a container |
2481
+ | \`useFocusTrap\` | Trap Tab/Shift+Tab focus within a container |
2482
+ | \`useBoundingClientRect\` | Track element bounding rect via ResizeObserver |
2483
+ | \`useSwipe\` | Detect touch swipe direction |
2484
+ | \`useDrag\` | Track mouse drag with position and delta |
2485
+ | \`useElementVisibility\` | Check if an element is in the viewport |
2486
+ | \`useScrollPosition\` | Track window scroll position |
2487
+ | \`useScrollLock\` | Lock/unlock body scroll |
2488
+ | \`useMutationObserver\` | Observe DOM mutations |
2489
+ | \`useIntersectionObserver\` | Observe element intersection with viewport |
2490
+
2491
+ ### Timers & Lifecycle
2492
+
2493
+ | Hook | Description |
2494
+ |------|-------------|
2495
+ | \`useInterval\` | setInterval wrapper with pause support |
2496
+ | \`useTimeout\` | setTimeout wrapper with manual clear |
2497
+ | \`useCountdown\` | Countdown timer with start/pause/reset |
2498
+ | \`useStopwatch\` | Stopwatch with lap support |
2499
+ | \`useIdleTimer\` | Detect user idle time |
2500
+ | \`useUpdateEffect\` | useEffect that skips the initial render |
2501
+ | \`useIsomorphicLayoutEffect\` | SSR-safe useLayoutEffect |
2502
+ | \`useIsMounted\` | Check if component is currently mounted |
2503
+ | \`useIsFirstRender\` | Check if this is the first render |
2504
+
2505
+ ### Async & Network
2506
+
2507
+ | Hook | Description |
2508
+ |------|-------------|
2509
+ | \`useFetch\` | Declarative data fetching with loading/error states (use for external URLs; use TanStack Query for API calls) |
2510
+ | \`useAsync\` | Execute async functions with status tracking |
2511
+ | \`useScript\` | Dynamically load external scripts |
2512
+ | \`useWebSocket\` | WebSocket connection with auto-reconnect |
2513
+ | \`useSSE\` | Server-Sent Events (EventSource) wrapper |
2514
+ | \`usePolling\` | Poll an async function at a fixed interval |
2515
+ | \`useAbortController\` | Manage AbortController lifecycle |
2516
+ | \`useRetry\` | Retry async operations with exponential backoff |
2517
+ | \`useSearch\` | Filter arrays with debounced search |
2518
+
2519
+ ### Browser APIs
2520
+
2521
+ | Hook | Description |
2522
+ |------|-------------|
2523
+ | \`useMediaQuery\` | Reactive CSS media query matching |
2524
+ | \`useColorScheme\` | Detect system color scheme preference |
2525
+ | \`useCopyToClipboard\` | Copy text to clipboard with status feedback |
2526
+ | \`useOnline\` | Track network connectivity |
2527
+ | \`useWindowSize\` | Track window dimensions |
2528
+ | \`usePageVisibility\` | Detect page visibility state |
2529
+ | \`usePageLeave\` | Detect when the user leaves the page |
2530
+ | \`useFavicon\` | Dynamically change the favicon |
2531
+ | \`useReducedMotion\` | Respect prefers-reduced-motion |
2532
+ | \`useBreakpoint\` | Responsive breakpoint detection |
2533
+ | \`useIsClient\` | SSR-safe client-side detection |
2534
+
2535
+ ### Layout & UI
2536
+
2537
+ | Hook | Description |
2538
+ |------|-------------|
2539
+ | \`useStickyHeader\` | Detect when header should be sticky |
2540
+ | \`useVirtualList\` | Virtualized list rendering for large datasets |
2541
+ | \`useInfiniteScroll\` | Infinite scroll with threshold detection |
2542
+ | \`useCollapse\` | Collapse/expand animation with prop getters |
2543
+ | \`useSteps\` | Multi-step flow navigation |
2544
+
2545
+ ### Common Patterns
2546
+
2547
+ **Modal with click-outside dismiss:**
2548
+ \`\`\`tsx
2549
+ import { useDisclosure, useClickOutside } from '@blacksmith-ui/hooks'
2550
+
2551
+ function MyComponent() {
2552
+ const [opened, { open, close }] = useDisclosure(false)
2553
+ const ref = useClickOutside<HTMLDivElement>(close)
2554
+
2555
+ return (
2556
+ <>
2557
+ <Button onClick={open}>Open</Button>
2558
+ {opened && <div ref={ref}>Modal content</div>}
2559
+ </>
2560
+ )
2561
+ }
2562
+ \`\`\`
2563
+
2564
+ **Debounced search:**
2565
+ \`\`\`tsx
2566
+ import { useDebounce, useSearch } from '@blacksmith-ui/hooks'
2567
+
2568
+ function SearchPage({ items }) {
2569
+ const [query, setQuery] = useState('')
2570
+ const debouncedQuery = useDebounce(query, 300)
2571
+ const results = useSearch(items, debouncedQuery, ['title', 'description'])
2572
+
2573
+ return (
2574
+ <>
2575
+ <Input value={query} onChange={(e) => setQuery(e.target.value)} />
2576
+ {results.map(item => <div key={item.id}>{item.title}</div>)}
2577
+ </>
2578
+ )
2579
+ }
2580
+ \`\`\`
2581
+
2582
+ **Persisted state with undo:**
2583
+ \`\`\`tsx
2584
+ import { useLocalStorage, useHistoryState } from '@blacksmith-ui/hooks'
2585
+
2586
+ function Editor() {
2587
+ const [saved, setSaved] = useLocalStorage('draft', '')
2588
+ const [content, { set, undo, redo, canUndo, canRedo }] = useHistoryState(saved)
2589
+
2590
+ const handleSave = () => setSaved(content)
2591
+ }
2592
+ \`\`\`
2593
+
2594
+ **Responsive layout:**
2595
+ \`\`\`tsx
2596
+ import { useBreakpoint, useWindowSize } from '@blacksmith-ui/hooks'
2597
+
2598
+ function Layout({ children }) {
2599
+ const breakpoint = useBreakpoint({ sm: 640, md: 768, lg: 1024 })
2600
+ const isMobile = breakpoint === 'sm'
2601
+
2602
+ return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
2603
+ }
2604
+ \`\`\`
2605
+ `;
2606
+ }
2607
+ };
2608
+
2609
+ // src/skills/blacksmith-cli.ts
2610
+ var blacksmithCliSkill = {
2611
+ id: "blacksmith-cli",
2612
+ name: "Blacksmith CLI",
2613
+ description: "CLI commands, configuration, and workflows for project scaffolding and management.",
2614
+ render(_ctx) {
2615
+ return `## Blacksmith CLI
2616
+
2617
+ Blacksmith is the CLI that scaffolded and manages this project. It lives outside the project directory as a globally installed npm package.
2618
+
2619
+ ### Commands Reference
2620
+
2621
+ | Command | Description |
2622
+ |---|---|
2623
+ | \`blacksmith init [name]\` | Create a new project (interactive prompts if no flags) |
2624
+ | \`blacksmith dev\` | Start Django + Vite + OpenAPI watcher in parallel |
2625
+ | \`blacksmith sync\` | Regenerate frontend API client from Django OpenAPI schema |
2626
+ | \`blacksmith make:resource <Name>\` | Scaffold a full CRUD resource across backend and frontend |
2627
+ | \`blacksmith build\` | Production build (Vite build + Django collectstatic) |
2628
+ | \`blacksmith eject\` | Remove Blacksmith dependency, keep a clean project |
2629
+ | \`blacksmith setup:ai\` | Generate CLAUDE.md with AI development skills |
2630
+ | \`blacksmith skills\` | List all available AI development skills |
2631
+
2632
+ ### Configuration
2633
+
2634
+ Project settings are stored in \`blacksmith.config.json\` at the project root:
2635
+
2636
+ \`\`\`json
2637
+ {
2638
+ "name": "my-app",
2639
+ "version": "0.1.0",
2640
+ "backend": { "port": 8000 },
2641
+ "frontend": { "port": 5173 }
2642
+ }
2643
+ \`\`\`
2644
+
2645
+ - **Ports** are read by \`blacksmith dev\` and \`blacksmith sync\` \u2014 change them here, not in code
2646
+ - The CLI finds the project root by walking up directories looking for this file
2647
+
2648
+ ### How \`blacksmith dev\` Works
2649
+
2650
+ Runs three concurrent processes:
2651
+ 1. **Django** \u2014 \`./venv/bin/python manage.py runserver 0.0.0.0:<backend-port>\`
2652
+ 2. **Vite** \u2014 \`npm run dev\` in the frontend directory
2653
+ 3. **OpenAPI watcher** \u2014 watches \`.py\` files in backend, runs \`npx openapi-ts\` on changes (2s debounce)
2654
+
2655
+ All three are managed by \`concurrently\` and stop together on Ctrl+C.
2656
+
2657
+ ### How \`blacksmith make:resource\` Works
2658
+
2659
+ Given a PascalCase name (e.g. \`BlogPost\`), it generates:
2660
+
2661
+ **Backend:**
2662
+ - \`backend/apps/blog_posts/models.py\` \u2014 Django model with timestamps
2663
+ - \`backend/apps/blog_posts/serializers.py\` \u2014 DRF ModelSerializer
2664
+ - \`backend/apps/blog_posts/views.py\` \u2014 DRF ModelViewSet with drf-spectacular schemas
2665
+ - \`backend/apps/blog_posts/urls.py\` \u2014 DefaultRouter registration
2666
+ - \`backend/apps/blog_posts/admin.py\` \u2014 Admin registration
2667
+ - Wires the app into \`INSTALLED_APPS\` and \`config/urls.py\`
2668
+ - Runs \`makemigrations\` and \`migrate\`
2669
+
2670
+ **Frontend:**
2671
+ - \`frontend/src/features/blog-posts/\` \u2014 Feature module with hooks and components
2672
+ - \`frontend/src/pages/blog-posts/\` \u2014 List and detail pages
2673
+ - Registers route path in \`frontend/src/router/paths.ts\` (\`Path\` enum)
2674
+ - Registers routes in \`frontend/src/router/routes.tsx\`
2675
+
2676
+ Then runs \`blacksmith sync\` to generate the TypeScript API client.
2677
+
2678
+ ### How \`blacksmith sync\` Works
2679
+
2680
+ 1. Fetches the OpenAPI schema from \`http://localhost:<backend-port>/api/schema/\`
2681
+ 2. Runs \`openapi-ts\` to generate TypeScript types, Zod schemas, SDK functions, and TanStack Query hooks
2682
+ 3. Output goes to \`frontend/src/api/generated/\` \u2014 never edit these files manually
2683
+
2684
+ ### Init Flags
2685
+
2686
+ \`blacksmith init\` supports both interactive prompts and CLI flags:
2687
+
2688
+ \`\`\`bash
2689
+ # Fully interactive
2690
+ blacksmith init
2691
+
2692
+ # Skip prompts with flags
2693
+ blacksmith init my-app -b 9000 -f 3000 --ai
2694
+ \`\`\`
2695
+
2696
+ | Flag | Description |
2697
+ |---|---|
2698
+ | \`-b, --backend-port <port>\` | Django port (default: 8000) |
2699
+ | \`-f, --frontend-port <port>\` | Vite port (default: 5173) |
2700
+ | \`--ai\` | Generate CLAUDE.md with project skills |
2701
+ | \`--no-blacksmith-ui-skill\` | Exclude blacksmith-ui skill from CLAUDE.md |
2702
+ `;
2703
+ }
2704
+ };
2705
+
2706
+ // src/skills/ui-design.ts
2707
+ var uiDesignSkill = {
2708
+ id: "ui-design",
2709
+ name: "UI/UX Design System",
2710
+ description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the BlacksmithUI design language.",
2711
+ render(_ctx) {
2712
+ return `## UI/UX Design System \u2014 Modern Flat Design
2713
+
2714
+ > **Design philosophy: Clean, flat, content-first.**
2715
+ > BlacksmithUI follows the same design language as 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.
2716
+
2717
+ ### Core Principles
2718
+
2719
+ 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).
2720
+ 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.
2721
+ 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.
2722
+ 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.
2723
+ 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.
2724
+
2725
+ ### Spacing System
2726
+
2727
+ Use Tailwind's spacing scale consistently. Do NOT use arbitrary values (\`p-[13px]\`) \u2014 stick to the system.
2728
+
2729
+ | Scale | Value | Use for |
2730
+ |-------|-------|---------|
2731
+ | \`1\`\u2013\`2\` | 4\u20138px | Inline gaps, icon-to-text spacing, tight badge padding |
2732
+ | \`3\`\u2013\`4\` | 12\u201316px | Inner component padding, gap between related items |
2733
+ | \`5\`\u2013\`6\` | 20\u201324px | Card padding, section inner spacing |
2734
+ | \`8\` | 32px | Gap between sections within a page |
2735
+ | \`10\`\u2013\`12\` | 40\u201348px | Gap between major page sections |
2736
+ | \`16\`\u2013\`20\` | 64\u201380px | Page-level vertical padding (hero, landing sections) |
2737
+
2738
+ **Rules:**
2739
+ - Use \`gap\` (via \`Flex\`, \`Stack\`, \`Grid\`) for spacing between siblings \u2014 not margin on individual items
2740
+ - Use \`Stack gap={...}\` for vertical rhythm within a section
2741
+ - Page content padding: \`px-4 sm:px-6 lg:px-8\` (use \`Container\` which handles this)
2742
+ - Card body padding: \`p-6\` standard, \`p-4\` for compact cards
2743
+ - Never mix spacing approaches in the same context \u2014 pick gap OR margin, not both
2744
+
2745
+ ### Typography
2746
+
2747
+ Use \`Typography\` and \`Text\` components from \`@blacksmith-ui/react\`. Do NOT style raw HTML headings.
2748
+
2749
+ **Hierarchy:**
2750
+ | Level | Component | Use for |
2751
+ |-------|-----------|---------|
2752
+ | Page title | \`<Typography variant="h1">\` | One per page. The main heading. |
2753
+ | Section title | \`<Typography variant="h2">\` | Major sections within a page |
2754
+ | Sub-section | \`<Typography variant="h3">\` | Groups within a section |
2755
+ | Card title | \`<Typography variant="h4">\` or \`CardTitle\` | Card headings |
2756
+ | Body | \`<Text>\` | Paragraphs, descriptions |
2757
+ | Caption/label | \`<Text size="sm" color="muted">\` | Secondary info, metadata, timestamps |
2758
+ | Overline | \`<Text size="xs" weight="medium" className="uppercase tracking-wide">\` | Category labels, section overlines |
2759
+
2760
+ **Rules:**
2761
+ - One \`h1\` per page \u2014 it's the page title
2762
+ - Headings should never skip levels (h1 \u2192 h3 without h2)
2763
+ - Body text: \`text-sm\` (14px) for dense UIs (tables, sidebars), \`text-base\` (16px) for reading content
2764
+ - Line height: use Tailwind defaults (\`leading-relaxed\` for body copy, \`leading-tight\` for headings)
2765
+ - Max reading width: \`max-w-prose\` (~65ch) for long-form text. Never let paragraphs stretch full-width
2766
+ - Use \`text-muted-foreground\` for secondary text, never gray hardcoded values
2767
+ - Font weight: \`font-medium\` (500) for labels and emphasis, \`font-semibold\` (600) for headings, \`font-bold\` (700) sparingly
2768
+
2769
+ ### Color
2770
+
2771
+ Use design tokens (CSS variables), never hardcoded colors.
2772
+
2773
+ **Semantic palette:**
2774
+ | Token | Usage |
2775
+ |-------|-------|
2776
+ | \`primary\` | Primary actions (buttons, links, active states) |
2777
+ | \`secondary\` | Secondary actions, subtle backgrounds |
2778
+ | \`destructive\` | Delete, error, danger states |
2779
+ | \`muted\` | Backgrounds for subtle sections, disabled states |
2780
+ | \`accent\` | Highlights, hover states, focus rings |
2781
+ | \`foreground\` | Primary text |
2782
+ | \`muted-foreground\` | Secondary/helper text |
2783
+ | \`border\` | Borders, dividers |
2784
+ | \`card\` | Card backgrounds |
2785
+ | \`background\` | Page background |
2786
+
2787
+ **Rules:**
2788
+ - NEVER use Tailwind color literals (\`text-gray-500\`, \`bg-blue-600\`, \`border-slate-200\`, \`bg-white\`, \`bg-black\`). Always use semantic tokens (\`text-muted-foreground\`, \`bg-primary\`, \`border-border\`, \`bg-background\`). This is non-negotiable \u2014 hardcoded colors break dark mode.
2789
+ - Status colors: use \`Badge\` variants (\`default\`, \`secondary\`, \`destructive\`, \`outline\`) \u2014 don't hand-roll colored pills.
2790
+ - Maximum 2\u20133 colors visible at any time (primary + foreground + muted). Colorful UIs feel noisy.
2791
+ - Every UI must render correctly in both light and dark mode. See the Dark Mode section below for the full rules.
2792
+
2793
+ ### Layout Patterns
2794
+
2795
+ **Page layout:**
2796
+ \`\`\`tsx
2797
+ <Box as="main">
2798
+ <Container>
2799
+ <Stack gap={8}>
2800
+ {/* Page header */}
2801
+ <Flex align="center" justify="between">
2802
+ <Stack gap={1}>
2803
+ <Typography variant="h1">Page Title</Typography>
2804
+ <Text color="muted">Brief description of this page</Text>
2805
+ </Stack>
2806
+ <Button>Primary Action</Button>
2807
+ </Flex>
2808
+
2809
+ {/* Page content sections */}
2810
+ <Stack gap={6}>
2811
+ {/* ... */}
2812
+ </Stack>
2813
+ </Stack>
2814
+ </Container>
2815
+ </Box>
2816
+ \`\`\`
2817
+
2818
+ **Card-based content:**
2819
+ \`\`\`tsx
2820
+ <Grid columns={{ base: 1, md: 2, lg: 3 }} gap={6}>
2821
+ {items.map((item) => (
2822
+ <Card key={item.id}>
2823
+ <CardHeader>
2824
+ <CardTitle>{item.title}</CardTitle>
2825
+ <CardDescription>{item.description}</CardDescription>
2826
+ </CardHeader>
2827
+ <CardContent>
2828
+ {/* Content */}
2829
+ </CardContent>
2830
+ </Card>
2831
+ ))}
2832
+ </Grid>
2833
+ \`\`\`
2834
+
2835
+ **Sidebar + main content:**
2836
+ \`\`\`tsx
2837
+ <Flex className="min-h-screen">
2838
+ <Sidebar>{/* Nav items */}</Sidebar>
2839
+ <Box as="main" className="flex-1">
2840
+ <Container>{/* Page content */}</Container>
2841
+ </Box>
2842
+ </Flex>
2843
+ \`\`\`
2844
+
2845
+ **Section with centered content (landing pages):**
2846
+ \`\`\`tsx
2847
+ <Box as="section" className="py-16 sm:py-20">
2848
+ <Container>
2849
+ <Stack gap={4} align="center" className="text-center">
2850
+ <Typography variant="h2">Section Title</Typography>
2851
+ <Text color="muted" className="max-w-2xl">
2852
+ A concise description that explains the value proposition.
2853
+ </Text>
2854
+ </Stack>
2855
+ <Grid columns={{ base: 1, md: 3 }} gap={8} className="mt-12">
2856
+ {/* Feature cards or content */}
2857
+ </Grid>
2858
+ </Container>
2859
+ </Box>
2860
+ \`\`\`
2861
+
2862
+ ### Component Patterns
2863
+
2864
+ **Empty states:**
2865
+ \`\`\`tsx
2866
+ // GOOD \u2014 uses EmptyState component
2867
+ <EmptyState
2868
+ icon={Inbox}
2869
+ title="No messages yet"
2870
+ description="Messages from your team will appear here."
2871
+ action={<Button>Send a message</Button>}
2872
+ />
2873
+
2874
+ // BAD \u2014 hand-rolled empty state
2875
+ <div className="flex flex-col items-center justify-center py-12 text-center">
2876
+ <Inbox className="h-12 w-12 text-gray-400 mb-4" />
2877
+ <h3 className="text-lg font-medium">No messages yet</h3>
2878
+ <p className="text-gray-500 mt-1">Messages from your team will appear here.</p>
2879
+ </div>
2880
+ \`\`\`
2881
+
2882
+ **Stats/metrics:**
2883
+ \`\`\`tsx
2884
+ // GOOD \u2014 uses StatCard
2885
+ <Grid columns={{ base: 1, sm: 2, lg: 4 }} gap={4}>
2886
+ <StatCard label="Total Users" value="2,847" trend="+12%" />
2887
+ <StatCard label="Revenue" value="$48,290" trend="+8%" />
2888
+ </Grid>
2889
+
2890
+ // BAD \u2014 hand-rolled stat cards
2891
+ <div className="grid grid-cols-4 gap-4">
2892
+ <div className="bg-white rounded-lg p-6 shadow">
2893
+ <p className="text-sm text-gray-500">Total Users</p>
2894
+ <p className="text-2xl font-bold">2,847</p>
2895
+ </div>
2896
+ </div>
2897
+ \`\`\`
2898
+
2899
+ **Loading states:**
2900
+ \`\`\`tsx
2901
+ // GOOD \u2014 Skeleton matches the layout structure
2902
+ <Stack gap={4}>
2903
+ <Skeleton className="h-8 w-48" /> {/* Title */}
2904
+ <Skeleton className="h-4 w-96" /> {/* Description */}
2905
+ <Grid columns={3} gap={4}>
2906
+ {Array.from({ length: 3 }).map((_, i) => (
2907
+ <Skeleton key={i} className="h-32" />
2908
+ ))}
2909
+ </Grid>
2910
+ </Stack>
2911
+
2912
+ // BAD \u2014 generic spinner with no layout hint
2913
+ <div className="flex justify-center py-12">
2914
+ <div className="animate-spin h-8 w-8 border-2 border-blue-500 rounded-full" />
2915
+ </div>
2916
+ \`\`\`
2917
+
2918
+ ### Dark Mode & Light Mode
2919
+
2920
+ > **CRITICAL: Every screen, component, and custom style MUST look correct in both light and dark mode. No exceptions.**
2921
+
2922
+ BlacksmithUI uses the \`.dark\` class strategy on \`<html>\`. All semantic CSS variables automatically switch between light and dark values. Your job is to never break this.
2923
+
2924
+ **Rules:**
2925
+ - NEVER hardcode colors. \`text-gray-500\`, \`bg-white\`, \`bg-slate-900\`, \`border-gray-200\` \u2014 all of these break in one mode or the other. Use semantic tokens: \`text-muted-foreground\`, \`bg-background\`, \`bg-card\`, \`border-border\`.
2926
+ - NEVER use \`bg-white\` or \`bg-black\`. Use \`bg-background\` (page), \`bg-card\` (elevated surfaces), \`bg-muted\` (subtle sections).
2927
+ - NEVER use \`text-black\` or \`text-white\`. Use \`text-foreground\` (primary text), \`text-muted-foreground\` (secondary), \`text-primary-foreground\` (text on primary-colored backgrounds).
2928
+ - NEVER use hardcoded shadows like \`shadow-[0_2px_8px_rgba(0,0,0,0.1)]\`. Use Tailwind shadow utilities (\`shadow-sm\`, \`shadow-md\`) which respect the theme.
2929
+ - NEVER use opacity-based overlays with hardcoded colors (\`bg-black/50\`). Use \`bg-background/80\` or let overlay components (\`Dialog\`, \`Sheet\`) handle it.
2930
+ - SVG fills and strokes: use \`currentColor\` or \`fill-foreground\` / \`stroke-border\` \u2014 never \`fill-black\` or \`stroke-gray-300\`.
2931
+ - Image assets: if you use decorative images or illustrations, ensure they work on both backgrounds or use \`dark:hidden\` / \`hidden dark:block\` to swap variants.
2932
+
2933
+ **Safe color tokens (always use these):**
2934
+ | Need | Light mode maps to | Dark mode maps to | Use |
2935
+ |------|----|----|-----|
2936
+ | Page background | white/light gray | near-black | \`bg-background\` |
2937
+ | Card/surface | white | dark gray | \`bg-card\` |
2938
+ | Subtle background | light gray | darker gray | \`bg-muted\` |
2939
+ | Primary text | near-black | near-white | \`text-foreground\` |
2940
+ | Secondary text | medium gray | lighter gray | \`text-muted-foreground\` |
2941
+ | Borders | light gray | dark gray | \`border-border\` |
2942
+ | Input borders | light gray | dark gray | \`border-input\` |
2943
+ | Focus ring | brand color | brand color | \`ring-ring\` |
2944
+ | Primary action | brand color | brand color | \`bg-primary text-primary-foreground\` |
2945
+ | Destructive | red | red | \`bg-destructive text-destructive-foreground\` |
2946
+
2947
+ **Testing checklist (mental model):**
2948
+ Before considering any UI complete, verify these in your head:
2949
+ 1. Does every text element use \`foreground\`, \`muted-foreground\`, or \`*-foreground\` tokens?
2950
+ 2. Does every background use \`background\`, \`card\`, \`muted\`, or \`primary\`/\`secondary\`/\`accent\` tokens?
2951
+ 3. Does every border use \`border\`, \`input\`, or \`ring\` tokens?
2952
+ 4. Are there ANY hex values, rgb values, or Tailwind color names (gray, slate, blue, etc.) in the code? If yes, replace them.
2953
+ 5. Do hover/focus/active states also use semantic tokens? (\`hover:bg-muted\` not \`hover:bg-gray-100\`)
2954
+
2955
+ ### Interactions & Feedback
2956
+
2957
+ - **Hover states**: Subtle background change (\`hover:bg-muted\`) \u2014 not color shifts or scale transforms
2958
+ - **Focus**: Use focus-visible ring (\`focus-visible:ring-2 ring-ring\`). BlacksmithUI components handle this automatically
2959
+ - **Transitions**: \`transition-colors duration-150\` for color changes. No bounces, no springs, no dramatic animations
2960
+ - **Click feedback**: Use \`active:scale-[0.98]\` only on buttons and interactive cards, never on text or static elements
2961
+ - **Loading feedback**: Show \`Spinner\` on buttons during async actions. Use \`Skeleton\` for content areas. Never leave the user without feedback during loading
2962
+ - **Success/error feedback**: Use \`useToast()\` for transient confirmations. Use \`Alert\` for persistent messages. Never use \`window.alert()\`
2963
+ - **Confirmation before destructive actions**: Always use \`AlertDialog\` for delete/remove actions. Never delete on single click
2964
+
2965
+ ### Responsive Design
2966
+
2967
+ - **Mobile-first**: Write base styles for mobile, add \`sm:\`/\`md:\`/\`lg:\` for larger screens
2968
+ - **Breakpoints**: \`sm\` (640px), \`md\` (768px), \`lg\` (1024px), \`xl\` (1280px)
2969
+ - **Grid collapse**: \`Grid columns={{ base: 1, md: 2, lg: 3 }}\` \u2014 single column on mobile, expand on larger screens
2970
+ - **Hide/show**: Use \`hidden md:block\` / \`md:hidden\` to toggle elements across breakpoints
2971
+ - **Touch targets**: Minimum 44\xD744px for interactive elements on mobile. Use \`Button size="lg"\` and adequate padding
2972
+ - **Stack on mobile, row on desktop**: Use \`Flex direction={{ base: 'column', md: 'row' }}\` or \`Stack\` that switches direction
2973
+ - **Container**: Always wrap page content in \`<Container>\` \u2014 it handles responsive horizontal padding
2974
+
2975
+ ### Anti-Patterns \u2014 NEVER Do These
2976
+
2977
+ | Anti-pattern | What to do instead |
2978
+ |---|---|
2979
+ | Hardcoded colors (\`text-gray-500\`, \`bg-blue-600\`) | Use semantic tokens (\`text-muted-foreground\`, \`bg-primary\`) |
2980
+ | Heavy box shadows (\`shadow-xl\`, \`shadow-2xl\`) | Use \`shadow-sm\` on cards, \`shadow-md\` on elevated overlays only |
2981
+ | Rounded pill shapes (\`rounded-full\`) on cards/containers | Use \`rounded-lg\` or \`rounded-md\` (controlled by \`--radius\`) |
2982
+ | Gradient backgrounds on surfaces | Use solid \`bg-card\` or \`bg-background\` |
2983
+ | Decorative borders (\`border-l-4 border-blue-500\`) | Use \`Divider\` or \`border-border\` |
2984
+ | Custom scrollbars with CSS hacks | Use \`ScrollArea\` |
2985
+ | Animated entrances (fade-in, slide-up on mount) | Content should appear instantly. Only animate user-triggered changes |
2986
+ | Centering with \`absolute inset-0 flex items-center\` | Use \`Flex align="center" justify="center"\` |
2987
+ | Using \`<br />\` for spacing | Use \`Stack gap={...}\` or margin utilities |
2988
+ | Multiple font sizes in close proximity | Keep nearby text within 1\u20132 size steps |
2989
+ | Dense walls of text | Break into sections with headings, cards, or spacing |
2990
+ | Colored backgrounds on every section | Use \`bg-background\` as default, \`bg-muted\` sparingly for contrast |
2991
+ | Over-using badges/tags on everything | Badges are for status and categories, not decoration |
2992
+ | Inline styles (\`style={{ ... }}\`) | Use Tailwind classes via \`className\` |
2993
+ | \`bg-white\` / \`bg-black\` / \`bg-slate-*\` | Use \`bg-background\`, \`bg-card\`, \`bg-muted\` |
2994
+ | \`text-black\` / \`text-white\` / \`text-gray-*\` | Use \`text-foreground\`, \`text-muted-foreground\` |
2995
+ | \`border-gray-*\` / \`border-slate-*\` | Use \`border-border\`, \`border-input\` |
2996
+ | Hex/rgb values in className or style | Use CSS variable tokens exclusively |
2997
+ | UI that only looks right in light mode | Always verify both modes \u2014 use semantic tokens throughout |
2998
+ `;
2999
+ }
3000
+ };
3001
+
3002
+ // src/skills/programming-paradigms.ts
3003
+ var programmingParadigmsSkill = {
3004
+ id: "programming-paradigms",
3005
+ name: "Programming Paradigms",
3006
+ description: "Functional programming for React frontend development, OOP + functional patterns for Django backend development.",
3007
+ render(_ctx) {
3008
+ return `## Programming Paradigms
3009
+
3010
+ > **Frontend (React/TypeScript): Functional programming.** Pure functions, immutability, composition, declarative UI.
3011
+ > **Backend (Django/Python): OOP with functional touches.** Classes for structure, pure functions for logic, no mutation where avoidable.
3012
+
3013
+ ---
3014
+
3015
+ ## Frontend \u2014 Functional Programming
3016
+
3017
+ React is a functional framework. Write it functionally. No classes, no imperative mutation, no object-oriented patterns.
3018
+
3019
+ ### Core Rules
3020
+
3021
+ 1. **Functions, not classes** \u2014 Every component is a function. Every hook is a function. Every utility is a function. Never use \`class\` in frontend code.
3022
+ 2. **Pure by default** \u2014 A component given the same props should render the same output. A utility given the same arguments should return the same result. Side effects belong in hooks (\`useEffect\`, \`useMutation\`), never in render logic.
3023
+ 3. **Immutable data** \u2014 Never mutate state, props, or variables. Always return new values.
3024
+ 4. **Declarative over imperative** \u2014 Describe *what* to render, not *how* to render it. Use \`map\`, \`filter\`, ternaries, and composition \u2014 not \`for\` loops, \`if/else\` chains, or DOM manipulation.
3025
+ 5. **Composition over inheritance** \u2014 Build complex behavior by composing small functions and components, not by extending base classes.
3026
+
3027
+ ### Immutability
3028
+
3029
+ \`\`\`tsx
3030
+ // BAD \u2014 mutation
3031
+ const handleAdd = (item) => {
3032
+ items.push(item) // mutates array
3033
+ setItems(items) // React won't re-render (same reference)
3034
+ }
3035
+
3036
+ user.name = 'New Name' // mutates object
3037
+ setUser(user)
3038
+
3039
+ // GOOD \u2014 immutable updates
3040
+ const handleAdd = (item) => {
3041
+ setItems((prev) => [...prev, item])
3042
+ }
3043
+
3044
+ setUser((prev) => ({ ...prev, name: 'New Name' }))
3045
+
3046
+ // GOOD \u2014 immutable array operations
3047
+ const removeItem = (id) => setItems((prev) => prev.filter((i) => i.id !== id))
3048
+ const updateItem = (id, data) => setItems((prev) =>
3049
+ prev.map((i) => (i.id === id ? { ...i, ...data } : i))
3050
+ )
3051
+ \`\`\`
3052
+
3053
+ ### Pure Functions & Composition
3054
+
3055
+ \`\`\`tsx
3056
+ // BAD \u2014 impure, relies on external state
3057
+ let taxRate = 0.1
3058
+ const calculateTotal = (price) => price * (1 + taxRate)
3059
+
3060
+ // GOOD \u2014 pure, all inputs explicit
3061
+ const calculateTotal = (price: number, taxRate: number) => price * (1 + taxRate)
3062
+
3063
+ // GOOD \u2014 compose small functions
3064
+ const formatCurrency = (amount: number) => \`$\${amount.toFixed(2)}\`
3065
+ const calculateTax = (price: number, rate: number) => price * rate
3066
+ const formatPriceWithTax = (price: number, rate: number) =>
3067
+ formatCurrency(price + calculateTax(price, rate))
3068
+ \`\`\`
3069
+
3070
+ ### Declarative UI Patterns
3071
+
3072
+ \`\`\`tsx
3073
+ // BAD \u2014 imperative rendering
3074
+ function UserList({ users }) {
3075
+ const items = []
3076
+ for (let i = 0; i < users.length; i++) {
3077
+ if (users[i].isActive) {
3078
+ items.push(<UserCard key={users[i].id} user={users[i]} />)
3079
+ }
3080
+ }
3081
+ return <div>{items}</div>
3082
+ }
3083
+
3084
+ // GOOD \u2014 declarative rendering
3085
+ function UserList({ users }) {
3086
+ return (
3087
+ <Stack gap={4}>
3088
+ {users
3089
+ .filter((user) => user.isActive)
3090
+ .map((user) => <UserCard key={user.id} user={user} />)
3091
+ }
3092
+ </Stack>
3093
+ )
3094
+ }
3095
+
3096
+ // BAD \u2014 imperative conditional
3097
+ function Status({ isOnline }) {
3098
+ let badge
3099
+ if (isOnline) {
3100
+ badge = <Badge>Online</Badge>
3101
+ } else {
3102
+ badge = <Badge variant="secondary">Offline</Badge>
3103
+ }
3104
+ return badge
3105
+ }
3106
+
3107
+ // GOOD \u2014 declarative conditional
3108
+ function Status({ isOnline }) {
3109
+ return isOnline
3110
+ ? <Badge>Online</Badge>
3111
+ : <Badge variant="secondary">Offline</Badge>
3112
+ }
3113
+ \`\`\`
3114
+
3115
+ ### Hooks as Functional Composition
3116
+
3117
+ \`\`\`tsx
3118
+ // BAD \u2014 logic in component body
3119
+ function OrdersPage() {
3120
+ const [page, setPage] = useState(1)
3121
+ const [search, setSearch] = useState('')
3122
+ const debounced = useDebounce(search, 300)
3123
+ const { data } = useApiQuery(ordersListOptions({ query: { page, search: debounced } }))
3124
+ const deleteOrder = useApiMutation(ordersDestroyMutation())
3125
+
3126
+ // ... 20 lines of derived state and handlers
3127
+
3128
+ return ( /* JSX */ )
3129
+ }
3130
+
3131
+ // GOOD \u2014 compose hooks, component just renders
3132
+ function OrdersPage() {
3133
+ const { orders, pagination, search, deleteOrder } = useOrdersPage()
3134
+
3135
+ return (
3136
+ <Stack gap={4}>
3137
+ <OrdersToolbar search={search} />
3138
+ <OrdersTable orders={orders} onDelete={deleteOrder} />
3139
+ <Pagination {...pagination} />
3140
+ </Stack>
3141
+ )
3142
+ }
3143
+
3144
+ // The hook composes smaller hooks
3145
+ function useOrdersPage() {
3146
+ const search = useSearchFilter()
3147
+ const pagination = usePagination()
3148
+ const { data } = useOrders({ page: pagination.page, search: search.debounced })
3149
+ const deleteOrder = useDeleteOrder()
3150
+
3151
+ return {
3152
+ orders: data?.results ?? [],
3153
+ pagination: { ...pagination, total: data?.count ?? 0 },
3154
+ search,
3155
+ deleteOrder: (id: number) => deleteOrder.mutate({ path: { id } }),
3156
+ }
3157
+ }
3158
+ \`\`\`
3159
+
3160
+ ### Data Transformation \u2014 Functional Style
3161
+
3162
+ \`\`\`tsx
3163
+ // BAD \u2014 imperative transformation
3164
+ function getActiveUserNames(users) {
3165
+ const result = []
3166
+ for (const user of users) {
3167
+ if (user.isActive) {
3168
+ result.push(user.name.toUpperCase())
3169
+ }
3170
+ }
3171
+ return result
3172
+ }
3173
+
3174
+ // GOOD \u2014 functional pipeline
3175
+ const getActiveUserNames = (users: User[]) =>
3176
+ users
3177
+ .filter((u) => u.isActive)
3178
+ .map((u) => u.name.toUpperCase())
3179
+
3180
+ // GOOD \u2014 derive state without mutation
3181
+ const sortedItems = useMemo(
3182
+ () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
3183
+ [items]
3184
+ )
3185
+
3186
+ const groupedByStatus = useMemo(
3187
+ () => items.reduce<Record<string, Item[]>>((acc, item) => ({
3188
+ ...acc,
3189
+ [item.status]: [...(acc[item.status] ?? []), item],
3190
+ }), {}),
3191
+ [items]
3192
+ )
3193
+ \`\`\`
3194
+
3195
+ ### What to Avoid in Frontend Code
3196
+
3197
+ | Anti-pattern | Functional alternative |
3198
+ |---|---|
3199
+ | \`class MyComponent extends React.Component\` | \`function MyComponent()\` |
3200
+ | \`this.state\`, \`this.setState\` | \`useState\`, \`useReducer\` |
3201
+ | \`array.push()\`, \`object.key = value\` | Spread: \`[...arr, item]\`, \`{ ...obj, key: value }\` |
3202
+ | \`for\` / \`while\` loops in render | \`.map()\`, \`.filter()\`, \`.reduce()\` |
3203
+ | \`let\` for derived values | \`const\` + \`useMemo\` or inline computation |
3204
+ | Mutable ref for state (\`useRef\` to track values) | \`useState\` or \`useReducer\` |
3205
+ | HOCs (\`withAuth\`, \`withTheme\`) | Custom hooks (\`useAuth\`, \`useTheme\`) |
3206
+ | Render props for logic sharing | Custom hooks |
3207
+ | \`if/else\` chains for rendering | Ternaries, \`&&\`, early returns, lookup objects |
3208
+ | Singleton services / global mutable state | Context + hooks, React Query for server state |
3209
+
3210
+ ---
3211
+
3212
+ ## Backend \u2014 OOP with Functional Patterns
3213
+
3214
+ Django is object-oriented by design. Lean into it for structure (models, views, serializers, services), but use functional patterns for pure logic and data transformations.
3215
+
3216
+ ### OOP for Structure
3217
+
3218
+ **Models** \u2014 Encapsulate data and behavior together:
3219
+ \`\`\`python
3220
+ class Order(TimeStampedModel):
3221
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders')
3222
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
3223
+ total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
3224
+
3225
+ class Meta:
3226
+ ordering = ['-created_at']
3227
+
3228
+ def __str__(self):
3229
+ return f"Order #{self.pk} \u2014 {self.user}"
3230
+
3231
+ @property
3232
+ def is_cancellable(self):
3233
+ return self.status in ('pending', 'confirmed')
3234
+
3235
+ def recalculate_total(self):
3236
+ self.total = self.items.aggregate(
3237
+ total=Sum(F('quantity') * F('unit_price'))
3238
+ )['total'] or 0
3239
+ self.save(update_fields=['total'])
3240
+ \`\`\`
3241
+
3242
+ **Services** \u2014 Classes for complex business operations with multiple related methods:
3243
+ \`\`\`python
3244
+ class OrderService:
3245
+ @staticmethod
3246
+ @transaction.atomic
3247
+ def place_order(*, user, items, shipping_address):
3248
+ order = Order.objects.create(user=user, shipping_address=shipping_address)
3249
+ for item_data in items:
3250
+ OrderItem.objects.create(order=order, **item_data)
3251
+ order.recalculate_total()
3252
+ NotificationService.send_order_confirmation(order=order)
3253
+ return order
3254
+
3255
+ @staticmethod
3256
+ @transaction.atomic
3257
+ def cancel_order(*, order, user):
3258
+ if not order.is_cancellable:
3259
+ raise ValidationError("Order cannot be cancelled in its current state.")
3260
+ if order.user != user:
3261
+ raise PermissionDenied("You can only cancel your own orders.")
3262
+ order.status = 'cancelled'
3263
+ order.save(update_fields=['status'])
3264
+ InventoryService.restore_stock(order=order)
3265
+ return order
3266
+ \`\`\`
3267
+
3268
+ **ViewSets** \u2014 Inherit, extend, override:
3269
+ \`\`\`python
3270
+ class OrderViewSet(ModelViewSet):
3271
+ permission_classes = [IsAuthenticated]
3272
+
3273
+ def get_queryset(self):
3274
+ return OrderSelector.list_for_user(user=self.request.user)
3275
+
3276
+ def get_serializer_class(self):
3277
+ return {
3278
+ 'list': OrderListSerializer,
3279
+ 'retrieve': OrderDetailSerializer,
3280
+ 'create': OrderCreateSerializer,
3281
+ }.get(self.action, OrderUpdateSerializer)
3282
+
3283
+ def perform_create(self, serializer):
3284
+ serializer.save()
3285
+ \`\`\`
3286
+
3287
+ **Custom permissions, filters, pagination** \u2014 All class-based, inheriting from DRF base classes:
3288
+ \`\`\`python
3289
+ class IsOwner(BasePermission):
3290
+ def has_object_permission(self, request, view, obj):
3291
+ return obj.user == request.user
3292
+
3293
+ class OrderFilterSet(django_filters.FilterSet):
3294
+ class Meta:
3295
+ model = Order
3296
+ fields = ['status', 'created_at']
3297
+ \`\`\`
3298
+
3299
+ ### Functional Patterns in Python
3300
+
3301
+ Use functional style for pure logic, data transformation, and utilities \u2014 anywhere you don't need state or inheritance.
3302
+
3303
+ **Selectors** \u2014 Pure query builders (can be functions or static methods):
3304
+ \`\`\`python
3305
+ # selectors.py \u2014 pure functions that build querysets
3306
+ def get_active_orders(*, user, status=None):
3307
+ qs = (
3308
+ Order.objects
3309
+ .filter(user=user, is_active=True)
3310
+ .select_related('user')
3311
+ .prefetch_related('items__product')
3312
+ )
3313
+ if status:
3314
+ qs = qs.filter(status=status)
3315
+ return qs.order_by('-created_at')
3316
+
3317
+ def get_order_summary(*, user):
3318
+ return (
3319
+ Order.objects
3320
+ .filter(user=user)
3321
+ .values('status')
3322
+ .annotate(count=Count('id'), total=Sum('total'))
3323
+ )
3324
+ \`\`\`
3325
+
3326
+ **Data transformations** \u2014 Use comprehensions, \`map\`, pure functions:
3327
+ \`\`\`python
3328
+ # BAD \u2014 imperative mutation
3329
+ def format_export_data(orders):
3330
+ result = []
3331
+ for order in orders:
3332
+ row = {}
3333
+ row['id'] = order.id
3334
+ row['total'] = str(order.total)
3335
+ row['items'] = ', '.join([i.product.name for i in order.items.all()])
3336
+ result.append(row)
3337
+ return result
3338
+
3339
+ # GOOD \u2014 functional transformation
3340
+ def format_export_data(orders):
3341
+ return [
3342
+ {
3343
+ 'id': order.id,
3344
+ 'total': str(order.total),
3345
+ 'items': ', '.join(i.product.name for i in order.items.all()),
3346
+ }
3347
+ for order in orders
3348
+ ]
3349
+ \`\`\`
3350
+
3351
+ **Utility functions** \u2014 Pure, no side effects:
3352
+ \`\`\`python
3353
+ # utils.py \u2014 all pure functions
3354
+ def calculate_discount(price: Decimal, percentage: int) -> Decimal:
3355
+ return price * (Decimal(percentage) / 100)
3356
+
3357
+ def slugify_unique(name: str, existing_slugs: set[str]) -> str:
3358
+ base = slugify(name)
3359
+ slug = base
3360
+ counter = 1
3361
+ while slug in existing_slugs:
3362
+ slug = f"{base}-{counter}"
3363
+ counter += 1
3364
+ return slug
3365
+
3366
+ def paginate_list(items: list, page: int, page_size: int = 20) -> list:
3367
+ start = (page - 1) * page_size
3368
+ return items[start:start + page_size]
3369
+ \`\`\`
3370
+
3371
+ **Decorators** \u2014 Functional composition for cross-cutting concerns:
3372
+ \`\`\`python
3373
+ import functools
3374
+ import logging
3375
+
3376
+ logger = logging.getLogger(__name__)
3377
+
3378
+ def log_service_call(func):
3379
+ @functools.wraps(func)
3380
+ def wrapper(*args, **kwargs):
3381
+ logger.info(f"Calling {func.__name__} with kwargs={kwargs}")
3382
+ result = func(*args, **kwargs)
3383
+ logger.info(f"{func.__name__} completed successfully")
3384
+ return result
3385
+ return wrapper
3386
+
3387
+ # Usage
3388
+ class OrderService:
3389
+ @staticmethod
3390
+ @log_service_call
3391
+ @transaction.atomic
3392
+ def place_order(*, user, items, shipping_address):
3393
+ ...
3394
+ \`\`\`
3395
+
3396
+ ### When to Use What
3397
+
3398
+ | Pattern | Use OOP (class) | Use Functional (function) |
3399
+ |---------|-----------------|---------------------------|
3400
+ | **Models** | Always \u2014 Django models are classes | Model methods can be property-style pure computations |
3401
+ | **Views** | Always \u2014 ViewSets, APIViews | \u2014 |
3402
+ | **Serializers** | Always \u2014 DRF serializers are classes | \u2014 |
3403
+ | **Services** | Business logic with multiple related operations | Single-purpose operations can be standalone functions |
3404
+ | **Selectors** | Either \u2014 class with static methods or module-level functions | Preferred \u2014 pure functions that return querysets |
3405
+ | **Permissions** | Always \u2014 DRF permissions are class-based | \u2014 |
3406
+ | **Filters** | Always \u2014 django-filter uses classes | \u2014 |
3407
+ | **Utilities** | Never \u2014 don't wrap utilities in classes | Always \u2014 pure functions |
3408
+ | **Data transforms** | Never | Always \u2014 comprehensions, map, pure functions |
3409
+ | **Validators** | DRF validator classes for reusable validation | Simple validation functions for one-off checks |
3410
+ | **Signals** | Receiver functions (decorated functions) | \u2014 |
3411
+ | **Tests** | Test classes inheriting APITestCase | Individual test functions with pytest are also fine |
3412
+
3413
+ ### Backend Anti-Patterns
3414
+
3415
+ | Anti-pattern | Correct approach |
3416
+ |---|---|
3417
+ | God class with 20+ methods | Split into focused Service + Selector + utils |
3418
+ | Utility class with only static methods | Use module-level functions instead |
3419
+ | Mixin soup (\`class View(A, B, C, D, E)\`) | Compose with max 1-2 mixins, prefer explicit overrides |
3420
+ | Business logic in views | Move to services |
3421
+ | Business logic in serializers | Serializers validate, services execute |
3422
+ | Mutable default arguments (\`def f(items=[])\`) | Use \`None\` default: \`def f(items=None)\` \u2192 \`items = items or []\` |
3423
+ | Nested \`for\` loops for data building | List/dict comprehensions |
3424
+ | Raw SQL for simple queries | Django ORM with \`annotate\`, \`Subquery\`, \`F\` expressions |
3425
+ | Global mutable state | Pass dependencies explicitly, use Django settings for config |
3426
+ | Deep inheritance chains | Prefer composition, keep inheritance to 1-2 levels |
3427
+ `;
3428
+ }
3429
+ };
3430
+
3431
+ // src/skills/clean-code.ts
3432
+ var cleanCodeSkill = {
3433
+ id: "clean-code",
3434
+ name: "Clean Code Principles",
3435
+ description: "Naming, functions, components, file organization, conditionals, error handling, and DRY guidelines.",
3436
+ render(_ctx) {
3437
+ return `## Clean Code Principles
3438
+
3439
+ Write code that is easy to read, easy to change, and easy to delete. Treat clarity as a feature.
3440
+
3441
+ ### Naming
3442
+ - Names should reveal intent. A reader should understand what a variable, function, or class does without reading its implementation
3443
+ - Booleans: prefix with \`is\`, \`has\`, \`can\`, \`should\` \u2014 e.g. \`isLoading\`, \`hasPermission\`, \`canEdit\`
3444
+ - Functions: use verb phrases that describe the action \u2014 e.g. \`fetchUsers\`, \`createOrder\`, \`validateEmail\`
3445
+ - Event handlers: prefix with \`handle\` in components, \`on\` in props \u2014 e.g. \`handleSubmit\`, \`onSubmit\`
3446
+ - Collections: use plural nouns \u2014 e.g. \`users\`, \`orderItems\`, not \`userList\` or \`data\`
3447
+ - Avoid abbreviations. \`transaction\` not \`txn\`, \`button\` not \`btn\`, \`message\` not \`msg\`
3448
+ - Avoid generic names like \`data\`, \`info\`, \`item\`, \`result\`, \`value\` unless the scope is trivially small (e.g. a one-line callback)
3449
+
3450
+ ### Functions
3451
+ - A function should do one thing. If you can describe what it does with "and", split it
3452
+ - Keep functions short \u2014 aim for under 20 lines. If a function is longer, look for sections you can extract
3453
+ - Prefer early returns to reduce nesting. Guard clauses at the top, happy path at the bottom
3454
+ - Limit parameters to 3. Beyond that, pass an options object
3455
+ - Pure functions are easier to test, reason about, and reuse. Prefer them where possible
3456
+ - Don't use flags (boolean parameters) to make a function do two different things \u2014 write two functions instead
3457
+
3458
+ ### Components (React-specific)
3459
+ - One component per file. The file name should match the component name
3460
+ - Keep components under 100 lines of JSX. Extract sub-components when they grow beyond this
3461
+ - Separate data logic (hooks) from presentation (components). A component should mostly be JSX, not logic
3462
+ - **Page components are orchestrators** \u2014 they should be ~20-30 lines, composing child components from \`components/\` and calling hooks from \`hooks/\`. Never build a 200-line page monolith
3463
+ - Props interfaces should be explicit and narrow \u2014 accept only what the component needs, not entire objects
3464
+ - Avoid prop drilling beyond 2 levels \u2014 use context or restructure the component tree
3465
+ - Destructure props in the function signature for clarity
3466
+ - Use \`@blacksmith-ui/react\` layout components (\`Stack\`, \`Flex\`, \`Grid\`) \u2014 never raw \`<div>\` with flex/grid classes
3467
+
3468
+ ### File Organization
3469
+ - Keep files short. If a file exceeds 200 lines, it is likely doing too much \u2014 split it
3470
+ - Group by feature, not by type. \`features/orders/\` is better than \`components/\`, \`hooks/\`, \`utils/\` at the top level
3471
+ - Co-locate related code. A component's hook, types, and test should live next to it
3472
+ - One export per file for components and hooks. Use \`index.ts\` barrel files only at the feature boundary
3473
+
3474
+ ### Conditionals & Logic
3475
+ - Prefer positive conditionals: \`if (isValid)\` over \`if (!isInvalid)\`
3476
+ - Extract complex conditions into well-named variables or functions:
3477
+ \`\`\`ts
3478
+ // Bad
3479
+ if (user.role === 'admin' && user.isActive && !user.isSuspended) { ... }
3480
+
3481
+ // Good
3482
+ const canAccessAdminPanel = user.role === 'admin' && user.isActive && !user.isSuspended
3483
+ if (canAccessAdminPanel) { ... }
3484
+ \`\`\`
3485
+ - Avoid deeply nested if/else trees. Use early returns, guard clauses, or lookup objects
3486
+ - Prefer \`switch\` or object maps over long \`if/else if\` chains:
3487
+ \`\`\`ts
3488
+ // Bad
3489
+ if (status === 'active') return 'green'
3490
+ else if (status === 'pending') return 'yellow'
3491
+ else if (status === 'inactive') return 'gray'
3492
+
3493
+ // Good
3494
+ const statusColor = { active: 'green', pending: 'yellow', inactive: 'gray' }
3495
+ return statusColor[status]
3496
+ \`\`\`
3497
+
3498
+ ### Error Handling
3499
+ - Handle errors at the right level \u2014 close to where they occur and where you can do something meaningful
3500
+ - Provide useful error messages that help the developer (or user) understand what went wrong and what to do
3501
+ - Don't swallow errors silently. If you catch, log or handle. Never write empty \`catch {}\` blocks
3502
+ - Use typed errors. In Python, raise specific exceptions. In TypeScript, return discriminated unions or throw typed errors
3503
+
3504
+ ### Comments
3505
+ - Don't comment what the code does \u2014 make the code readable enough to not need it
3506
+ - Do comment why \u2014 explain business decisions, workarounds, non-obvious constraints
3507
+ - Delete commented-out code. Version control remembers it
3508
+ - TODOs are acceptable but should include context: \`// TODO(auth): rate-limit login attempts after v1 launch\`
3509
+
3510
+ ### DRY Without Overengineering
3511
+ - Don't repeat the same logic in multiple places \u2014 extract it once you see the third occurrence
3512
+ - But don't over-abstract. Two similar blocks of code are fine if they serve different purposes and are likely to diverge
3513
+ - Premature abstraction is worse than duplication. Wait for patterns to emerge before creating shared utilities
3514
+ - Helper functions should be genuinely reusable. A "helper" called from one place is just indirection
3515
+
3516
+ ### Python-Specific (Django)
3517
+ - Use \`f-strings\` for string formatting, not \`.format()\` or \`%\`
3518
+ - Use list/dict/set comprehensions when they are clearer than loops \u2014 but don't nest them
3519
+ - Use \`dataclasses\` or typed dicts for structured data outside of Django models
3520
+ - Keep view methods thin \u2014 push business logic into model methods, serializer validation, or service functions
3521
+ - Use \`get_object_or_404\` instead of manual \`try/except DoesNotExist\`
3522
+
3523
+ ### TypeScript-Specific (React)
3524
+ - Use strict TypeScript. Don't use \`any\` \u2014 use \`unknown\` and narrow, or define a proper type
3525
+ - Define interfaces for component props, API responses, and form schemas
3526
+ - Use \`const\` by default. Only use \`let\` when reassignment is necessary. Never use \`var\`
3527
+ - Prefer \`map\`, \`filter\`, \`reduce\` over imperative loops for data transformation
3528
+ - Use optional chaining (\`?.\`) and nullish coalescing (\`??\`) instead of manual null checks
3529
+ - Keep type definitions close to where they are used. Don't create a global \`types.ts\` file
3530
+ `;
3531
+ }
3532
+ };
3533
+
3534
+ // src/skills/ai-guidelines.ts
3535
+ var aiGuidelinesSkill = {
3536
+ id: "ai-guidelines",
3537
+ name: "AI Development Guidelines",
3538
+ description: "Guidelines for developing the project using AI, including when to use code generation, code style, environment setup, and a checklist before finishing tasks.",
3539
+ render(_ctx) {
3540
+ return `## AI Development Guidelines
3541
+
3542
+ ### When Adding Features
3543
+ 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
3544
+ 2. After any backend API change (new endpoint, changed schema, new field), run \`blacksmith sync\` to regenerate the frontend API client and types
3545
+ 3. Never manually edit files in \`frontend/src/api/generated/\` \u2014 they are overwritten on every sync
3546
+
3547
+ ### Code Style
3548
+ - **Backend**: Follow PEP 8. Use Django and DRF conventions. Docstrings on models, serializers, and non-obvious view methods
3549
+ - **Frontend**: TypeScript strict mode. Functional components. Named exports (not default, except for page components used in routes). Descriptive variable names
3550
+ - Use existing patterns in the codebase as reference before inventing new ones
3551
+
3552
+ ### Frontend Architecture (Mandatory)
3553
+ - **Use \`@blacksmith-ui/react\` for ALL UI** \u2014 \`Stack\`, \`Flex\`, \`Grid\` for layout; \`Typography\`, \`Text\` for text; \`Card\`, \`Button\`, \`Badge\`, etc. for all elements. Never use raw HTML (\`<div>\`, \`<h1>\`, \`<p>\`, \`<button>\`) when a Blacksmith-UI component exists
3554
+ - **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
3555
+ - **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings like \`'/login'\` or \`'/dashboard'\`
3556
+ - **Add new paths to the enum** \u2014 when creating a new page, add its path to the \`Path\` enum before the \`// blacksmith:path\` marker
3557
+
3558
+ ### Environment
3559
+ - Backend: \`http://localhost:8000\`
3560
+ - Frontend: \`http://localhost:5173\`
3561
+ - API docs: \`http://localhost:8000/api/docs/\` (Swagger UI) or \`/api/redoc/\` (ReDoc)
3562
+ - Python venv: \`backend/venv/\` \u2014 always use \`./venv/bin/python\` or \`./venv/bin/pip\`
3563
+ - Start everything: \`blacksmith dev\`
3564
+
3565
+ ### Checklist Before Finishing a Task
3566
+ 1. Backend tests pass: \`cd backend && ./venv/bin/python manage.py test\`
3567
+ 2. Frontend builds: \`cd frontend && npm run build\`
3568
+ 3. API types are in sync: \`blacksmith sync\`
3569
+ 4. No lint errors in modified files
3570
+ 5. All UI uses \`@blacksmith-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
3571
+ 6. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
3572
+ 7. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
3573
+ 8. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
3574
+ 9. New routes have a corresponding \`Path\` enum entry
3575
+ `;
3576
+ }
3577
+ };
3578
+
3579
+ // src/commands/ai-setup.ts
3580
+ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill }) {
3581
+ const aiSpinner = spinner("Setting up AI development environment...");
3582
+ try {
3583
+ const skills = [
3584
+ coreRulesSkill,
3585
+ projectOverviewSkill,
3586
+ djangoSkill,
3587
+ djangoRestAdvancedSkill,
3588
+ apiDocumentationSkill,
3589
+ reactSkill,
3590
+ reactQuerySkill,
3591
+ pageStructureSkill
3592
+ ];
3593
+ if (includeBlacksmithUiSkill) {
3594
+ skills.push(blacksmithUiReactSkill);
3595
+ skills.push(blacksmithUiFormsSkill);
3596
+ skills.push(blacksmithUiAuthSkill);
3597
+ skills.push(blacksmithHooksSkill);
3598
+ skills.push(uiDesignSkill);
3599
+ }
3600
+ skills.push(blacksmithCliSkill);
3601
+ skills.push(programmingParadigmsSkill);
3602
+ skills.push(cleanCodeSkill);
3603
+ skills.push(aiGuidelinesSkill);
3604
+ const ctx = { projectName };
3605
+ const inlineSkills = skills.filter((s) => !s.name);
3606
+ const fileSkills = skills.filter((s) => s.name);
3607
+ const skillsDir = path3.join(projectDir, ".claude", "skills");
3608
+ if (fs3.existsSync(skillsDir)) {
3609
+ for (const entry of fs3.readdirSync(skillsDir)) {
3610
+ const entryPath = path3.join(skillsDir, entry);
3611
+ const stat = fs3.statSync(entryPath);
3612
+ if (stat.isDirectory()) {
3613
+ fs3.rmSync(entryPath, { recursive: true });
3614
+ } else if (entry.endsWith(".md")) {
3615
+ fs3.unlinkSync(entryPath);
3616
+ }
3617
+ }
3618
+ }
3619
+ fs3.mkdirSync(skillsDir, { recursive: true });
3620
+ for (const skill of fileSkills) {
3621
+ const skillDir = path3.join(skillsDir, skill.id);
3622
+ fs3.mkdirSync(skillDir, { recursive: true });
3623
+ const frontmatter = `---
3624
+ name: ${skill.name}
3625
+ description: ${skill.description}
3626
+ ---
3627
+
3628
+ `;
3629
+ const content = skill.render(ctx).trim();
3630
+ fs3.writeFileSync(path3.join(skillDir, "SKILL.md"), frontmatter + content + "\n", "utf-8");
3631
+ }
3632
+ const inlineContent = inlineSkills.map((s) => s.render(ctx)).join("\n");
3633
+ const skillsList = fileSkills.map((s) => `- \`.claude/skills/${s.id}/SKILL.md\` \u2014 ${s.name}`).join("\n");
3634
+ const claudeMd = [
3635
+ inlineContent.trim(),
3636
+ "",
3637
+ "## AI Skills",
3638
+ "",
3639
+ "Detailed skills and conventions are in `.claude/skills/`:",
3640
+ "",
3641
+ skillsList,
3642
+ "",
3643
+ "These files are auto-loaded by Claude Code. Run `blacksmith setup:ai` to regenerate.",
3644
+ ""
3645
+ ].join("\n");
3646
+ fs3.writeFileSync(path3.join(projectDir, "CLAUDE.md"), claudeMd, "utf-8");
3647
+ const skillNames = skills.filter((s) => s.id !== "project-overview" && s.id !== "ai-guidelines").map((s) => s.id).join(" + ");
3648
+ aiSpinner.succeed(`AI dev environment ready (${skillNames} skills)`);
3649
+ } catch (error) {
3650
+ aiSpinner.fail("Failed to set up AI development environment");
3651
+ log.error(error.message);
3652
+ }
3653
+ }
3654
+
3655
+ // src/commands/init.ts
3656
+ function parsePort(value, label) {
3657
+ const port = parseInt(value, 10);
3658
+ if (isNaN(port) || port < 1 || port > 65535) {
3659
+ log.error(`Invalid ${label} port: ${value}`);
3660
+ process.exit(1);
3661
+ }
3662
+ return port;
3663
+ }
3664
+ var THEME_PRESETS = ["default", "blue", "green", "violet", "red", "neutral"];
3665
+ async function init(name, options) {
3666
+ if (!name) {
3667
+ name = await promptText("Project name");
3668
+ if (!name) {
3669
+ log.error("Project name is required.");
3670
+ process.exit(1);
3671
+ }
3672
+ }
3673
+ if (!options.backendPort) {
3674
+ options.backendPort = await promptText("Backend port", "8000");
3675
+ }
3676
+ if (!options.frontendPort) {
3677
+ options.frontendPort = await promptText("Frontend port", "5173");
3678
+ }
3679
+ if (!options.themeColor) {
3680
+ options.themeColor = await promptSelect("Theme preset", THEME_PRESETS, "default");
3681
+ }
3682
+ if (options.ai === void 0) {
3683
+ options.ai = await promptYesNo("Set up AI coding support");
3684
+ }
3685
+ const backendPort = parsePort(options.backendPort, "backend");
3686
+ const frontendPort = parsePort(options.frontendPort, "frontend");
3687
+ const themePreset = THEME_PRESETS.includes(options.themeColor) ? options.themeColor : "default";
3688
+ printConfig({
3689
+ "Project": name,
3690
+ "Backend": `Django on :${backendPort}`,
3691
+ "Frontend": `React on :${frontendPort}`,
3692
+ "Theme": themePreset,
3693
+ "AI support": options.ai ? "Yes" : "No"
3694
+ });
3695
+ const projectDir = path4.resolve(process.cwd(), name);
3696
+ const backendDir = path4.join(projectDir, "backend");
3697
+ const frontendDir = path4.join(projectDir, "frontend");
3698
+ const templatesDir = getTemplatesDir();
3699
+ if (fs4.existsSync(projectDir)) {
3700
+ log.error(`Directory "${name}" already exists.`);
3701
+ process.exit(1);
3702
+ }
3703
+ const checkSpinner = spinner("Checking prerequisites...");
3704
+ const hasPython = await commandExists("python3");
3705
+ const hasNode = await commandExists("node");
3706
+ const hasNpm = await commandExists("npm");
3707
+ if (!hasPython) {
3708
+ checkSpinner.fail("Python 3 is required but not found. Install it from https://python.org");
3709
+ process.exit(1);
3710
+ }
3711
+ if (!hasNode || !hasNpm) {
3712
+ checkSpinner.fail("Node.js and npm are required but not found. Install from https://nodejs.org");
3713
+ process.exit(1);
3714
+ }
3715
+ checkSpinner.succeed("Prerequisites OK (Python 3, Node.js, npm)");
3716
+ const context = {
3717
+ projectName: name,
3718
+ backendPort,
3719
+ frontendPort,
3720
+ themePreset
3721
+ };
3722
+ fs4.mkdirSync(projectDir, { recursive: true });
3723
+ fs4.writeFileSync(
3724
+ path4.join(projectDir, "blacksmith.config.json"),
3725
+ JSON.stringify(
3726
+ {
3727
+ name,
3728
+ version: "0.1.0",
3729
+ backend: { port: backendPort },
3730
+ frontend: { port: frontendPort }
3731
+ },
3732
+ null,
3733
+ 2
3734
+ )
3735
+ );
3736
+ const backendSpinner = spinner("Generating Django backend...");
3737
+ try {
3738
+ renderDirectory(
3739
+ path4.join(templatesDir, "backend"),
3740
+ backendDir,
3741
+ context
3742
+ );
3743
+ fs4.copyFileSync(
3744
+ path4.join(backendDir, ".env.example"),
3745
+ path4.join(backendDir, ".env")
3746
+ );
3747
+ backendSpinner.succeed("Django backend generated");
3748
+ } catch (error) {
3749
+ backendSpinner.fail("Failed to generate backend");
3750
+ log.error(error.message);
3751
+ process.exit(1);
3752
+ }
3753
+ const venvSpinner = spinner("Creating Python virtual environment...");
3754
+ try {
3755
+ await exec("python3", ["-m", "venv", "venv"], { cwd: backendDir, silent: true });
3756
+ venvSpinner.succeed("Virtual environment created");
3757
+ } catch (error) {
3758
+ venvSpinner.fail("Failed to create virtual environment");
3759
+ log.error(error.message);
3760
+ process.exit(1);
3761
+ }
3762
+ const pipSpinner = spinner("Installing Python dependencies...");
3763
+ try {
3764
+ await execPip(
3765
+ ["install", "-r", "requirements.txt"],
3766
+ backendDir,
3767
+ true
3768
+ );
3769
+ pipSpinner.succeed("Python dependencies installed");
3770
+ } catch (error) {
3771
+ pipSpinner.fail("Failed to install Python dependencies");
3772
+ log.error(error.message);
3773
+ process.exit(1);
3774
+ }
3775
+ const migrateSpinner = spinner("Running initial migrations...");
3776
+ try {
3777
+ await execPython(["manage.py", "makemigrations", "users"], backendDir, true);
3778
+ await execPython(["manage.py", "migrate"], backendDir, true);
3779
+ migrateSpinner.succeed("Database migrated");
3780
+ } catch (error) {
3781
+ migrateSpinner.fail("Failed to run migrations");
3782
+ log.error(error.message);
3783
+ process.exit(1);
3784
+ }
3785
+ const frontendSpinner = spinner("Generating React frontend...");
3786
+ try {
3787
+ renderDirectory(
3788
+ path4.join(templatesDir, "frontend"),
3789
+ frontendDir,
3790
+ context
3791
+ );
3792
+ frontendSpinner.succeed("React frontend generated");
3793
+ } catch (error) {
3794
+ frontendSpinner.fail("Failed to generate frontend");
3795
+ log.error(error.message);
3796
+ process.exit(1);
3797
+ }
3798
+ const npmSpinner = spinner("Installing Node.js dependencies...");
3799
+ try {
3800
+ await exec("npm", ["install"], { cwd: frontendDir, silent: true });
3801
+ npmSpinner.succeed("Node.js dependencies installed");
3802
+ } catch (error) {
3803
+ npmSpinner.fail("Failed to install Node.js dependencies");
3804
+ log.error(error.message);
3805
+ process.exit(1);
3806
+ }
3807
+ const syncSpinner = spinner("Running initial OpenAPI sync...");
3808
+ try {
3809
+ const djangoProcess = spawn(
3810
+ "./venv/bin/python",
3811
+ ["manage.py", "runserver", `0.0.0.0:${backendPort}`, "--noreload"],
3812
+ {
3813
+ cwd: backendDir,
3814
+ stdio: "ignore",
3815
+ detached: true
3816
+ }
3817
+ );
3818
+ djangoProcess.unref();
3819
+ await new Promise((resolve) => setTimeout(resolve, 4e3));
3820
+ try {
3821
+ await exec(process.execPath, [path4.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
3822
+ syncSpinner.succeed("OpenAPI types synced");
3823
+ } catch {
3824
+ syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
3825
+ }
3826
+ try {
3827
+ if (djangoProcess.pid) {
3828
+ process.kill(-djangoProcess.pid);
3829
+ }
3830
+ } catch {
3831
+ }
3832
+ } catch {
3833
+ syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
3834
+ }
3835
+ const generatedDir = path4.join(frontendDir, "src", "api", "generated");
3836
+ const stubFile = path4.join(generatedDir, "client.gen.ts");
3837
+ if (!fs4.existsSync(stubFile)) {
3838
+ if (!fs4.existsSync(generatedDir)) {
3839
+ fs4.mkdirSync(generatedDir, { recursive: true });
3840
+ }
3841
+ fs4.writeFileSync(
3842
+ stubFile,
3843
+ [
3844
+ "/**",
3845
+ " * Auto-generated API Client",
3846
+ " *",
3847
+ " * This is a stub file that allows the app to boot before",
3848
+ " * the first OpenAPI sync. Run `blacksmith sync` or `blacksmith dev`",
3849
+ " * to generate the real client from your Django API schema.",
3850
+ " *",
3851
+ " * Generated by Blacksmith. This file will be overwritten by openapi-ts.",
3852
+ " */",
3853
+ "",
3854
+ "import { createClient } from '@hey-api/client-fetch'",
3855
+ "",
3856
+ "export const client = createClient()",
3857
+ ""
3858
+ ].join("\n"),
3859
+ "utf-8"
3860
+ );
3861
+ }
3862
+ if (options.ai) {
3863
+ await setupAiDev({
3864
+ projectDir,
3865
+ projectName: name,
3866
+ includeBlacksmithUiSkill: options.blacksmithUiSkill !== false
3867
+ });
3868
+ }
3869
+ printNextSteps(name, backendPort, frontendPort);
3870
+ }
3871
+
3872
+ // src/commands/dev.ts
3873
+ import net from "net";
3874
+ import concurrently from "concurrently";
3875
+ import path5 from "path";
3876
+ function isPortAvailable(port) {
3877
+ return new Promise((resolve) => {
3878
+ const server = net.createServer();
3879
+ server.once("error", () => resolve(false));
3880
+ server.once("listening", () => {
3881
+ server.close(() => resolve(true));
3882
+ });
3883
+ server.listen(port);
3884
+ });
3885
+ }
3886
+ async function findAvailablePort(startPort) {
3887
+ let port = startPort;
3888
+ while (port < startPort + 100) {
3889
+ if (await isPortAvailable(port)) return port;
3890
+ port++;
3891
+ }
3892
+ throw new Error(`No available port found in range ${startPort}-${port - 1}`);
3893
+ }
3894
+ async function dev() {
3895
+ let root;
3896
+ try {
3897
+ root = findProjectRoot();
3898
+ } catch {
3899
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
3900
+ process.exit(1);
3901
+ }
3902
+ const config = loadConfig(root);
3903
+ const backendDir = getBackendDir(root);
3904
+ const frontendDir = getFrontendDir(root);
3905
+ let backendPort;
3906
+ let frontendPort;
3907
+ try {
3908
+ ;
3909
+ [backendPort, frontendPort] = await Promise.all([
3910
+ findAvailablePort(config.backend.port),
3911
+ findAvailablePort(config.frontend.port)
3912
+ ]);
3913
+ } catch (err) {
3914
+ log.error(err.message);
3915
+ process.exit(1);
3916
+ }
3917
+ if (backendPort !== config.backend.port) {
3918
+ log.step(`Backend port ${config.backend.port} in use, using ${backendPort}`);
3919
+ }
3920
+ if (frontendPort !== config.frontend.port) {
3921
+ log.step(`Frontend port ${config.frontend.port} in use, using ${frontendPort}`);
3922
+ }
3923
+ log.info("Starting development servers...");
3924
+ log.blank();
3925
+ log.step(`Django \u2192 http://localhost:${backendPort}`);
3926
+ log.step(`Vite \u2192 http://localhost:${frontendPort}`);
3927
+ log.step(`Swagger \u2192 http://localhost:${backendPort}/api/docs/`);
3928
+ log.step("OpenAPI sync \u2192 watching backend .py files");
3929
+ log.blank();
3930
+ const syncCmd = `${process.execPath} ${path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
3931
+ const watcherCode = [
3932
+ `const{watch}=require("fs"),{exec}=require("child_process");`,
3933
+ `let t=null,s=false;`,
3934
+ `watch(${JSON.stringify(backendDir)},{recursive:true},(e,f)=>{`,
3935
+ `if(!f||!f.endsWith(".py"))return;`,
3936
+ `if(f.startsWith("venv/")||f.includes("__pycache__")||f.includes("/migrations/"))return;`,
3937
+ `if(t)clearTimeout(t);`,
3938
+ `t=setTimeout(()=>{`,
3939
+ `if(s)return;s=true;`,
3940
+ `console.log("Backend change detected \u2014 syncing OpenAPI types...");`,
3941
+ `exec(${JSON.stringify(syncCmd)},{cwd:${JSON.stringify(frontendDir)}},(err,o,se)=>{`,
3942
+ `s=false;`,
3943
+ `if(err)console.error("Sync failed:",se||err.message);`,
3944
+ `else console.log("OpenAPI types synced");`,
3945
+ `})`,
3946
+ `},2000)});`,
3947
+ `console.log("Watching for .py changes...");`
3948
+ ].join("");
3949
+ const { result } = concurrently(
3950
+ [
3951
+ {
3952
+ command: `./venv/bin/python manage.py runserver 0.0.0.0:${backendPort}`,
3953
+ name: "django",
3954
+ cwd: backendDir,
3955
+ prefixColor: "green"
3956
+ },
3957
+ {
3958
+ command: "npm run dev",
3959
+ name: "vite",
3960
+ cwd: frontendDir,
3961
+ prefixColor: "blue"
3962
+ },
3963
+ {
3964
+ command: `node -e '${watcherCode}'`,
3965
+ name: "sync",
3966
+ cwd: frontendDir,
3967
+ prefixColor: "yellow"
3968
+ }
3969
+ ],
3970
+ {
3971
+ prefix: "name",
3972
+ killOthers: ["failure"],
3973
+ restartTries: 3
3974
+ }
3975
+ );
3976
+ const shutdown = () => {
3977
+ log.blank();
3978
+ log.info("Development servers stopped.");
3979
+ process.exit(0);
3980
+ };
3981
+ process.on("SIGINT", shutdown);
3982
+ process.on("SIGTERM", shutdown);
3983
+ try {
3984
+ await result;
3985
+ } catch {
3986
+ }
3987
+ }
3988
+
3989
+ // src/commands/sync.ts
3990
+ import path6 from "path";
3991
+ import fs5 from "fs";
3992
+ async function sync() {
3993
+ let root;
3994
+ try {
3995
+ root = findProjectRoot();
3996
+ } catch {
3997
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
3998
+ process.exit(1);
3999
+ }
4000
+ const backendDir = getBackendDir(root);
4001
+ const frontendDir = getFrontendDir(root);
4002
+ const s = spinner("Syncing OpenAPI schema to frontend...");
4003
+ try {
4004
+ const schemaPath = path6.join(frontendDir, "_schema.yml");
4005
+ await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
4006
+ const configPath = path6.join(frontendDir, "openapi-ts.config.ts");
4007
+ const configBackup = fs5.readFileSync(configPath, "utf-8");
4008
+ const configWithFile = configBackup.replace(
4009
+ /path:\s*['"]http[^'"]+['"]/,
4010
+ `path: './_schema.yml'`
4011
+ );
4012
+ fs5.writeFileSync(configPath, configWithFile, "utf-8");
4013
+ try {
4014
+ await exec(process.execPath, [path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4015
+ cwd: frontendDir,
4016
+ silent: true
4017
+ });
4018
+ } finally {
4019
+ fs5.writeFileSync(configPath, configBackup, "utf-8");
4020
+ if (fs5.existsSync(schemaPath)) fs5.unlinkSync(schemaPath);
4021
+ }
4022
+ s.succeed("Frontend types, schemas, and hooks synced from OpenAPI spec");
4023
+ log.blank();
4024
+ log.step("Generated files in frontend/src/api/generated/:");
4025
+ log.step(" types.gen.ts \u2192 TypeScript interfaces");
4026
+ log.step(" zod.gen.ts \u2192 Zod validation schemas");
4027
+ log.step(" sdk.gen.ts \u2192 API client functions");
4028
+ log.step(" @tanstack/react-query.gen.ts \u2192 TanStack Query hooks");
4029
+ log.blank();
4030
+ } catch (error) {
4031
+ s.fail("Failed to sync OpenAPI schema");
4032
+ log.error(error.message || error);
4033
+ process.exit(1);
4034
+ }
4035
+ }
4036
+
4037
+ // src/commands/make-resource.ts
4038
+ import path7 from "path";
4039
+ import fs6 from "fs";
4040
+
4041
+ // src/utils/names.ts
4042
+ import { pascalCase, snakeCase, kebabCase, camelCase } from "change-case";
4043
+ import pluralize from "pluralize";
4044
+ function generateNames(input) {
4045
+ const singular = pascalCase(input);
4046
+ const plural = pluralize(singular);
4047
+ return {
4048
+ Name: singular,
4049
+ Names: plural,
4050
+ name: camelCase(singular),
4051
+ names: camelCase(plural),
4052
+ snake: snakeCase(singular),
4053
+ snakes: snakeCase(plural),
4054
+ kebab: kebabCase(singular),
4055
+ kebabs: kebabCase(plural),
4056
+ UPPER: snakeCase(singular).toUpperCase(),
4057
+ UPPERS: snakeCase(plural).toUpperCase()
4058
+ };
4059
+ }
4060
+
4061
+ // src/commands/make-resource.ts
4062
+ async function makeResource(name) {
4063
+ let root;
4064
+ try {
4065
+ root = findProjectRoot();
4066
+ } catch {
4067
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
4068
+ process.exit(1);
4069
+ }
4070
+ const names = generateNames(name);
4071
+ const backendDir = getBackendDir(root);
4072
+ const frontendDir = getFrontendDir(root);
4073
+ const templatesDir = getTemplatesDir();
4074
+ const backendAppDir = path7.join(backendDir, "apps", names.snakes);
4075
+ if (fs6.existsSync(backendAppDir)) {
4076
+ log.error(`Backend app "${names.snakes}" already exists.`);
4077
+ process.exit(1);
4078
+ }
4079
+ const frontendPageDir = path7.join(frontendDir, "src", "pages", names.kebabs);
4080
+ if (fs6.existsSync(frontendPageDir)) {
4081
+ log.error(`Frontend page "${names.kebabs}" already exists.`);
4082
+ process.exit(1);
4083
+ }
4084
+ const context = { ...names, projectName: name };
4085
+ const backendSpinner = spinner(`Creating backend app: apps/${names.snakes}/`);
4086
+ try {
4087
+ renderDirectory(
4088
+ path7.join(templatesDir, "resource", "backend"),
4089
+ backendAppDir,
4090
+ context
4091
+ );
4092
+ backendSpinner.succeed(`Created backend/apps/${names.snakes}/`);
4093
+ } catch (error) {
4094
+ backendSpinner.fail("Failed to create backend app");
4095
+ log.error(error.message);
4096
+ process.exit(1);
4097
+ }
4098
+ const registerSpinner = spinner("Registering app in Django settings...");
4099
+ try {
4100
+ const settingsPath = path7.join(backendDir, "config", "settings", "base.py");
4101
+ appendAfterMarker(
4102
+ settingsPath,
4103
+ "# blacksmith:apps",
4104
+ ` 'apps.${names.snakes}',`
4105
+ );
4106
+ registerSpinner.succeed("Registered in INSTALLED_APPS");
4107
+ } catch (error) {
4108
+ registerSpinner.fail("Failed to register app in settings");
4109
+ log.error(error.message);
4110
+ process.exit(1);
4111
+ }
4112
+ const urlSpinner = spinner("Registering API URLs...");
4113
+ try {
4114
+ const urlsPath = path7.join(backendDir, "config", "urls.py");
4115
+ insertBeforeMarker(
4116
+ urlsPath,
4117
+ "# blacksmith:urls",
4118
+ ` path('api/${names.snakes}/', include('apps.${names.snakes}.urls')),`
4119
+ );
4120
+ urlSpinner.succeed("Registered API URLs");
4121
+ } catch (error) {
4122
+ urlSpinner.fail("Failed to register URLs");
4123
+ log.error(error.message);
4124
+ process.exit(1);
4125
+ }
4126
+ const migrateSpinner = spinner("Running migrations...");
4127
+ try {
4128
+ await execPython(["manage.py", "makemigrations", names.snakes], backendDir, true);
4129
+ await execPython(["manage.py", "migrate"], backendDir, true);
4130
+ migrateSpinner.succeed("Migrations complete");
4131
+ } catch (error) {
4132
+ migrateSpinner.fail("Migration failed");
4133
+ log.error(error.message);
4134
+ process.exit(1);
4135
+ }
4136
+ const syncSpinner = spinner("Syncing OpenAPI schema...");
4137
+ try {
4138
+ const schemaPath = path7.join(frontendDir, "_schema.yml");
4139
+ await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
4140
+ const configPath = path7.join(frontendDir, "openapi-ts.config.ts");
4141
+ const configBackup = fs6.readFileSync(configPath, "utf-8");
4142
+ const configWithFile = configBackup.replace(
4143
+ /path:\s*['"]http[^'"]+['"]/,
4144
+ `path: './_schema.yml'`
4145
+ );
4146
+ fs6.writeFileSync(configPath, configWithFile, "utf-8");
4147
+ try {
4148
+ await exec(process.execPath, [path7.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4149
+ cwd: frontendDir,
4150
+ silent: true
4151
+ });
4152
+ } finally {
4153
+ fs6.writeFileSync(configPath, configBackup, "utf-8");
4154
+ if (fs6.existsSync(schemaPath)) fs6.unlinkSync(schemaPath);
4155
+ }
4156
+ syncSpinner.succeed("Frontend types and hooks regenerated");
4157
+ } catch {
4158
+ syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
4159
+ }
4160
+ const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
4161
+ try {
4162
+ renderDirectory(
4163
+ path7.join(templatesDir, "resource", "pages"),
4164
+ frontendPageDir,
4165
+ context
4166
+ );
4167
+ frontendSpinner.succeed(`Created frontend/src/pages/${names.kebabs}/`);
4168
+ } catch (error) {
4169
+ frontendSpinner.fail("Failed to create frontend page");
4170
+ log.error(error.message);
4171
+ process.exit(1);
4172
+ }
4173
+ const pathSpinner = spinner("Registering route path...");
4174
+ try {
4175
+ const pathsFile = path7.join(frontendDir, "src", "router", "paths.ts");
4176
+ insertBeforeMarker(
4177
+ pathsFile,
4178
+ "// blacksmith:path",
4179
+ ` ${names.Names} = '/${names.kebabs}',`
4180
+ );
4181
+ pathSpinner.succeed("Registered route path");
4182
+ } catch {
4183
+ pathSpinner.warn("Could not auto-register path. Add it manually to frontend/src/router/paths.ts");
4184
+ }
4185
+ const routeSpinner = spinner("Registering frontend routes...");
4186
+ try {
4187
+ const routesPath = path7.join(frontendDir, "src", "router", "routes.tsx");
4188
+ insertBeforeMarker(
4189
+ routesPath,
4190
+ "// blacksmith:import",
4191
+ `import { ${names.names}Routes } from '@/pages/${names.kebabs}'`
4192
+ );
4193
+ insertBeforeMarker(
4194
+ routesPath,
4195
+ "// blacksmith:routes",
4196
+ ` ...${names.names}Routes,`
4197
+ );
4198
+ routeSpinner.succeed("Registered frontend routes");
4199
+ } catch {
4200
+ routeSpinner.warn("Could not auto-register routes. Add them manually to frontend/src/router/routes.tsx");
4201
+ }
4202
+ log.blank();
4203
+ log.success(`Resource "${names.Name}" created successfully!`);
4204
+ log.blank();
4205
+ }
4206
+
4207
+ // src/commands/build.ts
4208
+ async function build() {
4209
+ let root;
4210
+ try {
4211
+ root = findProjectRoot();
4212
+ } catch {
4213
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
4214
+ process.exit(1);
4215
+ }
4216
+ const backendDir = getBackendDir(root);
4217
+ const frontendDir = getFrontendDir(root);
4218
+ const frontendSpinner = spinner("Building frontend...");
4219
+ try {
4220
+ await exec("npm", ["run", "build"], { cwd: frontendDir, silent: true });
4221
+ frontendSpinner.succeed("Frontend built \u2192 frontend/dist/");
4222
+ } catch (error) {
4223
+ frontendSpinner.fail("Frontend build failed");
4224
+ log.error(error.message || error);
4225
+ process.exit(1);
4226
+ }
4227
+ const backendSpinner = spinner("Collecting static files...");
4228
+ try {
4229
+ await execPython(
4230
+ ["manage.py", "collectstatic", "--noinput"],
4231
+ backendDir,
4232
+ true
4233
+ );
4234
+ backendSpinner.succeed("Static files collected");
4235
+ } catch (error) {
4236
+ backendSpinner.fail("Failed to collect static files");
4237
+ log.error(error.message || error);
4238
+ process.exit(1);
4239
+ }
4240
+ log.blank();
4241
+ log.success("Production build complete!");
4242
+ log.blank();
4243
+ log.step("Frontend assets: frontend/dist/");
4244
+ log.step("Backend ready for deployment");
4245
+ log.blank();
4246
+ }
4247
+
4248
+ // src/commands/eject.ts
4249
+ import fs7 from "fs";
4250
+ import path8 from "path";
4251
+ async function eject() {
4252
+ let root;
4253
+ try {
4254
+ root = findProjectRoot();
4255
+ } catch {
4256
+ log.error("Not inside a Blacksmith project.");
4257
+ process.exit(1);
4258
+ }
4259
+ const configPath = path8.join(root, "blacksmith.config.json");
4260
+ if (fs7.existsSync(configPath)) {
4261
+ fs7.unlinkSync(configPath);
4262
+ }
4263
+ log.success("Blacksmith has been ejected.");
4264
+ log.blank();
4265
+ log.step("Your project is now a standard Django + React project.");
4266
+ log.step("All generated code remains in place and is fully owned by you.");
4267
+ log.step("The blacksmith CLI commands will no longer work in this directory.");
4268
+ log.blank();
4269
+ log.info("To continue development without Blacksmith:");
4270
+ log.step("Backend: cd backend && ./venv/bin/python manage.py runserver");
4271
+ log.step("Frontend: cd frontend && npm run dev");
4272
+ log.step("Codegen: cd frontend && npx openapi-ts");
4273
+ log.blank();
4274
+ }
4275
+
4276
+ // src/commands/skills.ts
4277
+ import fs8 from "fs";
4278
+ var allSkills = [
4279
+ projectOverviewSkill,
4280
+ djangoSkill,
4281
+ djangoRestAdvancedSkill,
4282
+ apiDocumentationSkill,
4283
+ reactSkill,
4284
+ blacksmithUiReactSkill,
4285
+ blacksmithUiFormsSkill,
4286
+ blacksmithUiAuthSkill,
4287
+ blacksmithHooksSkill,
4288
+ blacksmithCliSkill,
4289
+ cleanCodeSkill,
4290
+ aiGuidelinesSkill
4291
+ ];
4292
+ async function setupSkills(options) {
4293
+ let root;
4294
+ try {
4295
+ root = findProjectRoot();
4296
+ } catch {
4297
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
4298
+ process.exit(1);
4299
+ }
4300
+ const config = loadConfig(root);
4301
+ await setupAiDev({
4302
+ projectDir: root,
4303
+ projectName: config.name,
4304
+ includeBlacksmithUiSkill: options.blacksmithUiSkill !== false
4305
+ });
4306
+ log.blank();
4307
+ log.success("AI skills generated:");
4308
+ log.step(" CLAUDE.md \u2192 project overview + guidelines");
4309
+ log.step(" .claude/skills/*/SKILL.md \u2192 detailed skill files");
4310
+ }
4311
+ function listSkills() {
4312
+ let root;
4313
+ try {
4314
+ root = findProjectRoot();
4315
+ } catch {
4316
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
4317
+ process.exit(1);
4318
+ }
4319
+ const hasClaude = fs8.existsSync(`${root}/CLAUDE.md`);
4320
+ const hasSkillsDir = fs8.existsSync(`${root}/.claude/skills`);
4321
+ const inlineSkills = allSkills.filter((s) => !s.name);
4322
+ const fileSkills = allSkills.filter((s) => s.name);
4323
+ log.info("Inline skills (in CLAUDE.md):");
4324
+ for (const skill of inlineSkills) {
4325
+ log.step(` ${skill.id}`);
4326
+ }
4327
+ log.blank();
4328
+ log.info("File-based skills (in .claude/skills/):");
4329
+ for (const skill of fileSkills) {
4330
+ const exists = hasSkillsDir && fs8.existsSync(`${root}/.claude/skills/${skill.id}/SKILL.md`);
4331
+ const status = exists ? "\u2713" : "\u2717";
4332
+ log.step(` ${status} ${skill.id}/SKILL.md \u2014 ${skill.name}`);
4333
+ }
4334
+ log.blank();
4335
+ if (hasClaude && hasSkillsDir) {
4336
+ log.success('AI skills are set up. Run "blacksmith setup:ai" to regenerate.');
4337
+ } else {
4338
+ log.info('Run "blacksmith setup:ai" to generate AI skills.');
4339
+ }
4340
+ }
4341
+
4342
+ // src/commands/backend.ts
4343
+ async function backend(args) {
4344
+ let root;
4345
+ try {
4346
+ root = findProjectRoot();
4347
+ } catch {
4348
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
4349
+ process.exit(1);
4350
+ }
4351
+ if (args.length === 0) {
4352
+ log.error("Please provide a Django management command.");
4353
+ log.step("Usage: blacksmith backend <command> [args...]");
4354
+ log.step("Example: blacksmith backend createsuperuser");
4355
+ process.exit(1);
4356
+ }
4357
+ const backendDir = getBackendDir(root);
4358
+ try {
4359
+ await execPython(["manage.py", ...args], backendDir);
4360
+ } catch {
4361
+ process.exit(1);
4362
+ }
4363
+ }
4364
+
4365
+ // src/commands/frontend.ts
4366
+ async function frontend(args) {
4367
+ let root;
4368
+ try {
4369
+ root = findProjectRoot();
4370
+ } catch {
4371
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
4372
+ process.exit(1);
4373
+ }
4374
+ if (args.length === 0) {
4375
+ log.error("Please provide an npm command.");
4376
+ log.step("Usage: blacksmith frontend <command> [args...]");
4377
+ log.step("Example: blacksmith frontend install axios");
4378
+ process.exit(1);
4379
+ }
4380
+ const frontendDir = getFrontendDir(root);
4381
+ try {
4382
+ await exec("npm", args, { cwd: frontendDir });
4383
+ } catch {
4384
+ process.exit(1);
4385
+ }
4386
+ }
4387
+
4388
+ // src/index.ts
4389
+ var program = new Command();
4390
+ program.name("blacksmith").description("Fullstack Django + React framework").version("0.1.0").hook("preAction", () => {
4391
+ banner();
4392
+ });
4393
+ program.command("init").argument("[name]", "Project name").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-blacksmith-ui-skill", "Disable blacksmith-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);
4394
+ program.command("dev").description("Start development servers (Django + Vite + OpenAPI sync)").action(dev);
4395
+ program.command("sync").description("Sync OpenAPI schema to frontend types, schemas, and hooks").action(sync);
4396
+ program.command("make:resource").argument("<name>", "Resource name (PascalCase, e.g. BlogPost)").description("Create a new resource (model, serializer, viewset, hooks, pages)").action(makeResource);
4397
+ program.command("build").description("Build both frontend and backend for production").action(build);
4398
+ program.command("eject").description("Remove Blacksmith, keep a clean Django + React project").action(eject);
4399
+ program.command("setup:ai").description("Generate CLAUDE.md with AI development skills for the project").option("--no-blacksmith-ui-skill", "Exclude blacksmith-ui skill").action(setupSkills);
4400
+ program.command("skills").description("List all available AI development skills").action(listSkills);
4401
+ program.command("backend").argument("[args...]", "Django management command and arguments").description("Run a Django management command (e.g. blacksmith backend createsuperuser)").allowUnknownOption().action(backend);
4402
+ program.command("frontend").argument("[args...]", "npm command and arguments").description("Run an npm command in the frontend (e.g. blacksmith frontend install axios)").allowUnknownOption().action(frontend);
4403
+ program.parse();
4404
+ //# sourceMappingURL=index.js.map