blacksmith-cli 0.1.5 → 0.1.7

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 (37) hide show
  1. package/dist/index.js +1989 -690
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -1
  4. package/src/templates/frontend/package.json.hbs +13 -5
  5. package/src/templates/frontend/src/__tests__/setup.ts.hbs +21 -0
  6. package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +81 -0
  7. package/src/templates/frontend/src/app.tsx.hbs +13 -9
  8. package/src/templates/frontend/src/features/auth/adapter.ts.hbs +7 -7
  9. package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +91 -11
  10. package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +3 -4
  11. package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +76 -12
  12. package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +84 -11
  13. package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +85 -14
  14. package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +63 -12
  15. package/src/templates/frontend/src/features/auth/types.ts.hbs +32 -0
  16. package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +19 -18
  17. package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +33 -31
  18. package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +5 -5
  19. package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +5 -5
  20. package/src/templates/frontend/src/pages/home/home.tsx.hbs +48 -52
  21. package/src/templates/frontend/src/router/auth-guard.tsx.hbs +10 -7
  22. package/src/templates/frontend/src/router/error-boundary.tsx.hbs +16 -12
  23. package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +12 -12
  24. package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +62 -55
  25. package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +6 -6
  26. package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +1 -1
  27. package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +18 -2
  28. package/src/templates/frontend/src/styles/globals.css.hbs +3 -1
  29. package/src/templates/frontend/tailwind.config.js.hbs +1 -1
  30. package/src/templates/frontend/tsconfig.app.json.hbs +1 -0
  31. package/src/templates/frontend/vite.config.ts.hbs +8 -0
  32. package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +3 -2
  33. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +3 -2
  34. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
  35. package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +3 -2
  36. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +3 -2
  37. package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
package/dist/index.js CHANGED
@@ -1,7 +1,768 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // node_modules/tsup/assets/esm_shims.js
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var init_esm_shims = __esm({
15
+ "node_modules/tsup/assets/esm_shims.js"() {
16
+ "use strict";
17
+ }
18
+ });
19
+
20
+ // node_modules/is-docker/index.js
21
+ import fs10 from "fs";
22
+ function hasDockerEnv() {
23
+ try {
24
+ fs10.statSync("/.dockerenv");
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ function hasDockerCGroup() {
31
+ try {
32
+ return fs10.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ function isDocker() {
38
+ if (isDockerCached === void 0) {
39
+ isDockerCached = hasDockerEnv() || hasDockerCGroup();
40
+ }
41
+ return isDockerCached;
42
+ }
43
+ var isDockerCached;
44
+ var init_is_docker = __esm({
45
+ "node_modules/is-docker/index.js"() {
46
+ "use strict";
47
+ init_esm_shims();
48
+ }
49
+ });
50
+
51
+ // node_modules/is-inside-container/index.js
52
+ import fs11 from "fs";
53
+ function isInsideContainer() {
54
+ if (cachedResult === void 0) {
55
+ cachedResult = hasContainerEnv() || isDocker();
56
+ }
57
+ return cachedResult;
58
+ }
59
+ var cachedResult, hasContainerEnv;
60
+ var init_is_inside_container = __esm({
61
+ "node_modules/is-inside-container/index.js"() {
62
+ "use strict";
63
+ init_esm_shims();
64
+ init_is_docker();
65
+ hasContainerEnv = () => {
66
+ try {
67
+ fs11.statSync("/run/.containerenv");
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ };
73
+ }
74
+ });
75
+
76
+ // node_modules/is-wsl/index.js
77
+ import process2 from "process";
78
+ import os from "os";
79
+ import fs12 from "fs";
80
+ var isWsl, is_wsl_default;
81
+ var init_is_wsl = __esm({
82
+ "node_modules/is-wsl/index.js"() {
83
+ "use strict";
84
+ init_esm_shims();
85
+ init_is_inside_container();
86
+ isWsl = () => {
87
+ if (process2.platform !== "linux") {
88
+ return false;
89
+ }
90
+ if (os.release().toLowerCase().includes("microsoft")) {
91
+ if (isInsideContainer()) {
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ try {
97
+ if (fs12.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
98
+ return !isInsideContainer();
99
+ }
100
+ } catch {
101
+ }
102
+ if (fs12.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs12.existsSync("/run/WSL")) {
103
+ return !isInsideContainer();
104
+ }
105
+ return false;
106
+ };
107
+ is_wsl_default = process2.env.__IS_WSL_TEST__ ? isWsl : isWsl();
108
+ }
109
+ });
110
+
111
+ // node_modules/powershell-utils/index.js
112
+ import process3 from "process";
113
+ import { Buffer as Buffer2 } from "buffer";
114
+ import { promisify } from "util";
115
+ import childProcess from "child_process";
116
+ import fs13, { constants as fsConstants } from "fs/promises";
117
+ var execFile, powerShellPath, executePowerShell;
118
+ var init_powershell_utils = __esm({
119
+ "node_modules/powershell-utils/index.js"() {
120
+ "use strict";
121
+ init_esm_shims();
122
+ execFile = promisify(childProcess.execFile);
123
+ powerShellPath = () => `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
124
+ executePowerShell = async (command, options = {}) => {
125
+ const {
126
+ powerShellPath: psPath,
127
+ ...execFileOptions
128
+ } = options;
129
+ const encodedCommand = executePowerShell.encodeCommand(command);
130
+ return execFile(
131
+ psPath ?? powerShellPath(),
132
+ [
133
+ ...executePowerShell.argumentsPrefix,
134
+ encodedCommand
135
+ ],
136
+ {
137
+ encoding: "utf8",
138
+ ...execFileOptions
139
+ }
140
+ );
141
+ };
142
+ executePowerShell.argumentsPrefix = [
143
+ "-NoProfile",
144
+ "-NonInteractive",
145
+ "-ExecutionPolicy",
146
+ "Bypass",
147
+ "-EncodedCommand"
148
+ ];
149
+ executePowerShell.encodeCommand = (command) => Buffer2.from(command, "utf16le").toString("base64");
150
+ executePowerShell.escapeArgument = (value) => `'${String(value).replaceAll("'", "''")}'`;
151
+ }
152
+ });
153
+
154
+ // node_modules/wsl-utils/utilities.js
155
+ function parseMountPointFromConfig(content) {
156
+ for (const line of content.split("\n")) {
157
+ if (/^\s*#/.test(line)) {
158
+ continue;
159
+ }
160
+ const match = /^\s*root\s*=\s*(?<mountPoint>"[^"]*"|'[^']*'|[^#]*)/.exec(line);
161
+ if (!match) {
162
+ continue;
163
+ }
164
+ return match.groups.mountPoint.trim().replaceAll(/^["']|["']$/g, "");
165
+ }
166
+ }
167
+ var init_utilities = __esm({
168
+ "node_modules/wsl-utils/utilities.js"() {
169
+ "use strict";
170
+ init_esm_shims();
171
+ }
172
+ });
173
+
174
+ // node_modules/wsl-utils/index.js
175
+ import { promisify as promisify2 } from "util";
176
+ import childProcess2 from "child_process";
177
+ import fs14, { constants as fsConstants2 } from "fs/promises";
178
+ var execFile2, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath2, canAccessPowerShellPromise, canAccessPowerShell, wslDefaultBrowser, convertWslPathToWindows;
179
+ var init_wsl_utils = __esm({
180
+ "node_modules/wsl-utils/index.js"() {
181
+ "use strict";
182
+ init_esm_shims();
183
+ init_is_wsl();
184
+ init_powershell_utils();
185
+ init_utilities();
186
+ init_is_wsl();
187
+ execFile2 = promisify2(childProcess2.execFile);
188
+ wslDrivesMountPoint = /* @__PURE__ */ (() => {
189
+ const defaultMountPoint = "/mnt/";
190
+ let mountPoint;
191
+ return async function() {
192
+ if (mountPoint) {
193
+ return mountPoint;
194
+ }
195
+ const configFilePath = "/etc/wsl.conf";
196
+ let isConfigFileExists = false;
197
+ try {
198
+ await fs14.access(configFilePath, fsConstants2.F_OK);
199
+ isConfigFileExists = true;
200
+ } catch {
201
+ }
202
+ if (!isConfigFileExists) {
203
+ return defaultMountPoint;
204
+ }
205
+ const configContent = await fs14.readFile(configFilePath, { encoding: "utf8" });
206
+ const parsedMountPoint = parseMountPointFromConfig(configContent);
207
+ if (parsedMountPoint === void 0) {
208
+ return defaultMountPoint;
209
+ }
210
+ mountPoint = parsedMountPoint;
211
+ mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
212
+ return mountPoint;
213
+ };
214
+ })();
215
+ powerShellPathFromWsl = async () => {
216
+ const mountPoint = await wslDrivesMountPoint();
217
+ return `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
218
+ };
219
+ powerShellPath2 = is_wsl_default ? powerShellPathFromWsl : powerShellPath;
220
+ canAccessPowerShell = async () => {
221
+ canAccessPowerShellPromise ??= (async () => {
222
+ try {
223
+ const psPath = await powerShellPath2();
224
+ await fs14.access(psPath, fsConstants2.X_OK);
225
+ return true;
226
+ } catch {
227
+ return false;
228
+ }
229
+ })();
230
+ return canAccessPowerShellPromise;
231
+ };
232
+ wslDefaultBrowser = async () => {
233
+ const psPath = await powerShellPath2();
234
+ const command = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
235
+ const { stdout } = await executePowerShell(command, { powerShellPath: psPath });
236
+ return stdout.trim();
237
+ };
238
+ convertWslPathToWindows = async (path12) => {
239
+ if (/^[a-z]+:\/\//i.test(path12)) {
240
+ return path12;
241
+ }
242
+ try {
243
+ const { stdout } = await execFile2("wslpath", ["-aw", path12], { encoding: "utf8" });
244
+ return stdout.trim();
245
+ } catch {
246
+ return path12;
247
+ }
248
+ };
249
+ }
250
+ });
251
+
252
+ // node_modules/define-lazy-prop/index.js
253
+ function defineLazyProperty(object, propertyName, valueGetter) {
254
+ const define = (value) => Object.defineProperty(object, propertyName, { value, enumerable: true, writable: true });
255
+ Object.defineProperty(object, propertyName, {
256
+ configurable: true,
257
+ enumerable: true,
258
+ get() {
259
+ const result = valueGetter();
260
+ define(result);
261
+ return result;
262
+ },
263
+ set(value) {
264
+ define(value);
265
+ }
266
+ });
267
+ return object;
268
+ }
269
+ var init_define_lazy_prop = __esm({
270
+ "node_modules/define-lazy-prop/index.js"() {
271
+ "use strict";
272
+ init_esm_shims();
273
+ }
274
+ });
275
+
276
+ // node_modules/default-browser-id/index.js
277
+ import { promisify as promisify3 } from "util";
278
+ import process4 from "process";
279
+ import { execFile as execFile3 } from "child_process";
280
+ async function defaultBrowserId() {
281
+ if (process4.platform !== "darwin") {
282
+ throw new Error("macOS only");
283
+ }
284
+ const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
285
+ const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
286
+ const browserId = match?.groups.id ?? "com.apple.Safari";
287
+ if (browserId === "com.apple.safari") {
288
+ return "com.apple.Safari";
289
+ }
290
+ return browserId;
291
+ }
292
+ var execFileAsync;
293
+ var init_default_browser_id = __esm({
294
+ "node_modules/default-browser-id/index.js"() {
295
+ "use strict";
296
+ init_esm_shims();
297
+ execFileAsync = promisify3(execFile3);
298
+ }
299
+ });
300
+
301
+ // node_modules/run-applescript/index.js
302
+ import process5 from "process";
303
+ import { promisify as promisify4 } from "util";
304
+ import { execFile as execFile4, execFileSync } from "child_process";
305
+ async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
306
+ if (process5.platform !== "darwin") {
307
+ throw new Error("macOS only");
308
+ }
309
+ const outputArguments = humanReadableOutput ? [] : ["-ss"];
310
+ const execOptions = {};
311
+ if (signal) {
312
+ execOptions.signal = signal;
313
+ }
314
+ const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
315
+ return stdout.trim();
316
+ }
317
+ var execFileAsync2;
318
+ var init_run_applescript = __esm({
319
+ "node_modules/run-applescript/index.js"() {
320
+ "use strict";
321
+ init_esm_shims();
322
+ execFileAsync2 = promisify4(execFile4);
323
+ }
324
+ });
325
+
326
+ // node_modules/bundle-name/index.js
327
+ async function bundleName(bundleId) {
328
+ return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string
329
+ tell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
330
+ }
331
+ var init_bundle_name = __esm({
332
+ "node_modules/bundle-name/index.js"() {
333
+ "use strict";
334
+ init_esm_shims();
335
+ init_run_applescript();
336
+ }
337
+ });
338
+
339
+ // node_modules/default-browser/windows.js
340
+ import { promisify as promisify5 } from "util";
341
+ import { execFile as execFile5 } from "child_process";
342
+ async function defaultBrowser(_execFileAsync = execFileAsync3) {
343
+ const { stdout } = await _execFileAsync("reg", [
344
+ "QUERY",
345
+ " HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
346
+ "/v",
347
+ "ProgId"
348
+ ]);
349
+ const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
350
+ if (!match) {
351
+ throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
352
+ }
353
+ const { id } = match.groups;
354
+ const dotIndex = id.lastIndexOf(".");
355
+ const hyphenIndex = id.lastIndexOf("-");
356
+ const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
357
+ const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
358
+ return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? { name: id, id };
359
+ }
360
+ var execFileAsync3, windowsBrowserProgIds, _windowsBrowserProgIdMap, UnknownBrowserError;
361
+ var init_windows = __esm({
362
+ "node_modules/default-browser/windows.js"() {
363
+ "use strict";
364
+ init_esm_shims();
365
+ execFileAsync3 = promisify5(execFile5);
366
+ windowsBrowserProgIds = {
367
+ MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
368
+ // The missing `L` is correct.
369
+ MSEdgeBHTML: { name: "Edge Beta", id: "com.microsoft.edge.beta" },
370
+ MSEdgeDHTML: { name: "Edge Dev", id: "com.microsoft.edge.dev" },
371
+ AppXq0fevzme2pys62n3e0fbqa7peapykr8v: { name: "Edge", id: "com.microsoft.edge.old" },
372
+ ChromeHTML: { name: "Chrome", id: "com.google.chrome" },
373
+ ChromeBHTML: { name: "Chrome Beta", id: "com.google.chrome.beta" },
374
+ ChromeDHTML: { name: "Chrome Dev", id: "com.google.chrome.dev" },
375
+ ChromiumHTM: { name: "Chromium", id: "org.chromium.Chromium" },
376
+ BraveHTML: { name: "Brave", id: "com.brave.Browser" },
377
+ BraveBHTML: { name: "Brave Beta", id: "com.brave.Browser.beta" },
378
+ BraveDHTML: { name: "Brave Dev", id: "com.brave.Browser.dev" },
379
+ BraveSSHTM: { name: "Brave Nightly", id: "com.brave.Browser.nightly" },
380
+ FirefoxURL: { name: "Firefox", id: "org.mozilla.firefox" },
381
+ OperaStable: { name: "Opera", id: "com.operasoftware.Opera" },
382
+ VivaldiHTM: { name: "Vivaldi", id: "com.vivaldi.Vivaldi" },
383
+ "IE.HTTP": { name: "Internet Explorer", id: "com.microsoft.ie" }
384
+ };
385
+ _windowsBrowserProgIdMap = new Map(Object.entries(windowsBrowserProgIds));
386
+ UnknownBrowserError = class extends Error {
387
+ };
388
+ }
389
+ });
390
+
391
+ // node_modules/default-browser/index.js
392
+ import { promisify as promisify6 } from "util";
393
+ import process6 from "process";
394
+ import { execFile as execFile6 } from "child_process";
395
+ async function defaultBrowser2() {
396
+ if (process6.platform === "darwin") {
397
+ const id = await defaultBrowserId();
398
+ const name = await bundleName(id);
399
+ return { name, id };
400
+ }
401
+ if (process6.platform === "linux") {
402
+ const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
403
+ const id = stdout.trim();
404
+ const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
405
+ return { name, id };
406
+ }
407
+ if (process6.platform === "win32") {
408
+ return defaultBrowser();
409
+ }
410
+ throw new Error("Only macOS, Linux, and Windows are supported");
411
+ }
412
+ var execFileAsync4, titleize;
413
+ var init_default_browser = __esm({
414
+ "node_modules/default-browser/index.js"() {
415
+ "use strict";
416
+ init_esm_shims();
417
+ init_default_browser_id();
418
+ init_bundle_name();
419
+ init_windows();
420
+ init_windows();
421
+ execFileAsync4 = promisify6(execFile6);
422
+ titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
423
+ }
424
+ });
425
+
426
+ // node_modules/is-in-ssh/index.js
427
+ import process7 from "process";
428
+ var isInSsh, is_in_ssh_default;
429
+ var init_is_in_ssh = __esm({
430
+ "node_modules/is-in-ssh/index.js"() {
431
+ "use strict";
432
+ init_esm_shims();
433
+ isInSsh = Boolean(process7.env.SSH_CONNECTION || process7.env.SSH_CLIENT || process7.env.SSH_TTY);
434
+ is_in_ssh_default = isInSsh;
435
+ }
436
+ });
437
+
438
+ // node_modules/open/index.js
439
+ var open_exports = {};
440
+ __export(open_exports, {
441
+ apps: () => apps,
442
+ default: () => open_default,
443
+ openApp: () => openApp
444
+ });
445
+ import process8 from "process";
446
+ import path11 from "path";
447
+ import { fileURLToPath as fileURLToPath3 } from "url";
448
+ import childProcess3 from "child_process";
449
+ import fs15, { constants as fsConstants3 } from "fs/promises";
450
+ function detectArchBinary(binary) {
451
+ if (typeof binary === "string" || Array.isArray(binary)) {
452
+ return binary;
453
+ }
454
+ const { [arch]: archBinary } = binary;
455
+ if (!archBinary) {
456
+ throw new Error(`${arch} is not supported`);
457
+ }
458
+ return archBinary;
459
+ }
460
+ function detectPlatformBinary({ [platform]: platformBinary }, { wsl } = {}) {
461
+ if (wsl && is_wsl_default) {
462
+ return detectArchBinary(wsl);
463
+ }
464
+ if (!platformBinary) {
465
+ throw new Error(`${platform} is not supported`);
466
+ }
467
+ return detectArchBinary(platformBinary);
468
+ }
469
+ var fallbackAttemptSymbol, __dirname3, localXdgOpenPath, platform, arch, tryEachApp, baseOpen, open, openApp, apps, open_default;
470
+ var init_open = __esm({
471
+ "node_modules/open/index.js"() {
472
+ "use strict";
473
+ init_esm_shims();
474
+ init_wsl_utils();
475
+ init_powershell_utils();
476
+ init_define_lazy_prop();
477
+ init_default_browser();
478
+ init_is_inside_container();
479
+ init_is_in_ssh();
480
+ fallbackAttemptSymbol = /* @__PURE__ */ Symbol("fallbackAttempt");
481
+ __dirname3 = import.meta.url ? path11.dirname(fileURLToPath3(import.meta.url)) : "";
482
+ localXdgOpenPath = path11.join(__dirname3, "xdg-open");
483
+ ({ platform, arch } = process8);
484
+ tryEachApp = async (apps2, opener) => {
485
+ if (apps2.length === 0) {
486
+ return;
487
+ }
488
+ const errors = [];
489
+ for (const app of apps2) {
490
+ try {
491
+ return await opener(app);
492
+ } catch (error) {
493
+ errors.push(error);
494
+ }
495
+ }
496
+ throw new AggregateError(errors, "Failed to open in all supported apps");
497
+ };
498
+ baseOpen = async (options) => {
499
+ options = {
500
+ wait: false,
501
+ background: false,
502
+ newInstance: false,
503
+ allowNonzeroExitCode: false,
504
+ ...options
505
+ };
506
+ const isFallbackAttempt = options[fallbackAttemptSymbol] === true;
507
+ delete options[fallbackAttemptSymbol];
508
+ if (Array.isArray(options.app)) {
509
+ return tryEachApp(options.app, (singleApp) => baseOpen({
510
+ ...options,
511
+ app: singleApp,
512
+ [fallbackAttemptSymbol]: true
513
+ }));
514
+ }
515
+ let { name: app, arguments: appArguments = [] } = options.app ?? {};
516
+ appArguments = [...appArguments];
517
+ if (Array.isArray(app)) {
518
+ return tryEachApp(app, (appName) => baseOpen({
519
+ ...options,
520
+ app: {
521
+ name: appName,
522
+ arguments: appArguments
523
+ },
524
+ [fallbackAttemptSymbol]: true
525
+ }));
526
+ }
527
+ if (app === "browser" || app === "browserPrivate") {
528
+ const ids = {
529
+ "com.google.chrome": "chrome",
530
+ "google-chrome.desktop": "chrome",
531
+ "com.brave.browser": "brave",
532
+ "org.mozilla.firefox": "firefox",
533
+ "firefox.desktop": "firefox",
534
+ "com.microsoft.msedge": "edge",
535
+ "com.microsoft.edge": "edge",
536
+ "com.microsoft.edgemac": "edge",
537
+ "microsoft-edge.desktop": "edge",
538
+ "com.apple.safari": "safari"
539
+ };
540
+ const flags = {
541
+ chrome: "--incognito",
542
+ brave: "--incognito",
543
+ firefox: "--private-window",
544
+ edge: "--inPrivate"
545
+ // Safari doesn't support private mode via command line
546
+ };
547
+ let browser;
548
+ if (is_wsl_default) {
549
+ const progId = await wslDefaultBrowser();
550
+ const browserInfo = _windowsBrowserProgIdMap.get(progId);
551
+ browser = browserInfo ?? {};
552
+ } else {
553
+ browser = await defaultBrowser2();
554
+ }
555
+ if (browser.id in ids) {
556
+ const browserName = ids[browser.id.toLowerCase()];
557
+ if (app === "browserPrivate") {
558
+ if (browserName === "safari") {
559
+ throw new Error("Safari doesn't support opening in private mode via command line");
560
+ }
561
+ appArguments.push(flags[browserName]);
562
+ }
563
+ return baseOpen({
564
+ ...options,
565
+ app: {
566
+ name: apps[browserName],
567
+ arguments: appArguments
568
+ }
569
+ });
570
+ }
571
+ throw new Error(`${browser.name} is not supported as a default browser`);
572
+ }
573
+ let command;
574
+ const cliArguments = [];
575
+ const childProcessOptions = {};
576
+ let shouldUseWindowsInWsl = false;
577
+ if (is_wsl_default && !isInsideContainer() && !is_in_ssh_default && !app) {
578
+ shouldUseWindowsInWsl = await canAccessPowerShell();
579
+ }
580
+ if (platform === "darwin") {
581
+ command = "open";
582
+ if (options.wait) {
583
+ cliArguments.push("--wait-apps");
584
+ }
585
+ if (options.background) {
586
+ cliArguments.push("--background");
587
+ }
588
+ if (options.newInstance) {
589
+ cliArguments.push("--new");
590
+ }
591
+ if (app) {
592
+ cliArguments.push("-a", app);
593
+ }
594
+ } else if (platform === "win32" || shouldUseWindowsInWsl) {
595
+ command = await powerShellPath2();
596
+ cliArguments.push(...executePowerShell.argumentsPrefix);
597
+ if (!is_wsl_default) {
598
+ childProcessOptions.windowsVerbatimArguments = true;
599
+ }
600
+ if (is_wsl_default && options.target) {
601
+ options.target = await convertWslPathToWindows(options.target);
602
+ }
603
+ const encodedArguments = ["$ProgressPreference = 'SilentlyContinue';", "Start"];
604
+ if (options.wait) {
605
+ encodedArguments.push("-Wait");
606
+ }
607
+ if (app) {
608
+ encodedArguments.push(executePowerShell.escapeArgument(app));
609
+ if (options.target) {
610
+ appArguments.push(options.target);
611
+ }
612
+ } else if (options.target) {
613
+ encodedArguments.push(executePowerShell.escapeArgument(options.target));
614
+ }
615
+ if (appArguments.length > 0) {
616
+ appArguments = appArguments.map((argument) => executePowerShell.escapeArgument(argument));
617
+ encodedArguments.push("-ArgumentList", appArguments.join(","));
618
+ }
619
+ options.target = executePowerShell.encodeCommand(encodedArguments.join(" "));
620
+ if (!options.wait) {
621
+ childProcessOptions.stdio = "ignore";
622
+ }
623
+ } else {
624
+ if (app) {
625
+ command = app;
626
+ } else {
627
+ const isBundled = !__dirname3 || __dirname3 === "/";
628
+ let exeLocalXdgOpen = false;
629
+ try {
630
+ await fs15.access(localXdgOpenPath, fsConstants3.X_OK);
631
+ exeLocalXdgOpen = true;
632
+ } catch {
633
+ }
634
+ const useSystemXdgOpen = process8.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
635
+ command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
636
+ }
637
+ if (appArguments.length > 0) {
638
+ cliArguments.push(...appArguments);
639
+ }
640
+ if (!options.wait) {
641
+ childProcessOptions.stdio = "ignore";
642
+ childProcessOptions.detached = true;
643
+ }
644
+ }
645
+ if (platform === "darwin" && appArguments.length > 0) {
646
+ cliArguments.push("--args", ...appArguments);
647
+ }
648
+ if (options.target) {
649
+ cliArguments.push(options.target);
650
+ }
651
+ const subprocess = childProcess3.spawn(command, cliArguments, childProcessOptions);
652
+ if (options.wait) {
653
+ return new Promise((resolve, reject) => {
654
+ subprocess.once("error", reject);
655
+ subprocess.once("close", (exitCode) => {
656
+ if (!options.allowNonzeroExitCode && exitCode !== 0) {
657
+ reject(new Error(`Exited with code ${exitCode}`));
658
+ return;
659
+ }
660
+ resolve(subprocess);
661
+ });
662
+ });
663
+ }
664
+ if (isFallbackAttempt) {
665
+ return new Promise((resolve, reject) => {
666
+ subprocess.once("error", reject);
667
+ subprocess.once("spawn", () => {
668
+ subprocess.once("close", (exitCode) => {
669
+ subprocess.off("error", reject);
670
+ if (exitCode !== 0) {
671
+ reject(new Error(`Exited with code ${exitCode}`));
672
+ return;
673
+ }
674
+ subprocess.unref();
675
+ resolve(subprocess);
676
+ });
677
+ });
678
+ });
679
+ }
680
+ subprocess.unref();
681
+ return new Promise((resolve, reject) => {
682
+ subprocess.once("error", reject);
683
+ subprocess.once("spawn", () => {
684
+ subprocess.off("error", reject);
685
+ resolve(subprocess);
686
+ });
687
+ });
688
+ };
689
+ open = (target, options) => {
690
+ if (typeof target !== "string") {
691
+ throw new TypeError("Expected a `target`");
692
+ }
693
+ return baseOpen({
694
+ ...options,
695
+ target
696
+ });
697
+ };
698
+ openApp = (name, options) => {
699
+ if (typeof name !== "string" && !Array.isArray(name)) {
700
+ throw new TypeError("Expected a valid `name`");
701
+ }
702
+ const { arguments: appArguments = [] } = options ?? {};
703
+ if (appArguments !== void 0 && appArguments !== null && !Array.isArray(appArguments)) {
704
+ throw new TypeError("Expected `appArguments` as Array type");
705
+ }
706
+ return baseOpen({
707
+ ...options,
708
+ app: {
709
+ name,
710
+ arguments: appArguments
711
+ }
712
+ });
713
+ };
714
+ apps = {
715
+ browser: "browser",
716
+ browserPrivate: "browserPrivate"
717
+ };
718
+ defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
719
+ darwin: "google chrome",
720
+ win32: "chrome",
721
+ // `chromium-browser` is the older deb package name used by Ubuntu/Debian before snap.
722
+ linux: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]
723
+ }, {
724
+ wsl: {
725
+ ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
726
+ x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
727
+ }
728
+ }));
729
+ defineLazyProperty(apps, "brave", () => detectPlatformBinary({
730
+ darwin: "brave browser",
731
+ win32: "brave",
732
+ linux: ["brave-browser", "brave"]
733
+ }, {
734
+ wsl: {
735
+ ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
736
+ x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
737
+ }
738
+ }));
739
+ defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
740
+ darwin: "firefox",
741
+ win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
742
+ linux: "firefox"
743
+ }, {
744
+ wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe"
745
+ }));
746
+ defineLazyProperty(apps, "edge", () => detectPlatformBinary({
747
+ darwin: "microsoft edge",
748
+ win32: "msedge",
749
+ linux: ["microsoft-edge", "microsoft-edge-dev"]
750
+ }, {
751
+ wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
752
+ }));
753
+ defineLazyProperty(apps, "safari", () => detectPlatformBinary({
754
+ darwin: "Safari"
755
+ }));
756
+ open_default = open;
757
+ }
758
+ });
759
+
1
760
  // src/index.ts
761
+ init_esm_shims();
2
762
  import { Command } from "commander";
3
763
 
4
764
  // src/utils/logger.ts
765
+ init_esm_shims();
5
766
  import chalk from "chalk";
6
767
  import ora from "ora";
7
768
  import { createInterface } from "readline";
@@ -114,13 +875,15 @@ function printNextSteps(projectName, backendPort = 8e3, frontendPort = 5173) {
114
875
  }
115
876
 
116
877
  // src/commands/init.ts
117
- import path4 from "path";
878
+ init_esm_shims();
879
+ import path5 from "path";
118
880
  import fs4 from "fs";
119
881
  import { spawn } from "child_process";
120
882
 
121
883
  // src/utils/template.ts
884
+ init_esm_shims();
122
885
  import fs from "fs";
123
- import path from "path";
886
+ import path2 from "path";
124
887
  import Handlebars from "handlebars";
125
888
  Handlebars.registerHelper("eq", (a, b) => a === b);
126
889
  Handlebars.registerHelper("ne", (a, b) => a !== b);
@@ -138,7 +901,7 @@ function renderTemplateFile(templatePath, context) {
138
901
  }
139
902
  function renderToFile(templatePath, destPath, context) {
140
903
  const rendered = renderTemplateFile(templatePath, context);
141
- const destDir = path.dirname(destPath);
904
+ const destDir = path2.dirname(destPath);
142
905
  if (!fs.existsSync(destDir)) {
143
906
  fs.mkdirSync(destDir, { recursive: true });
144
907
  }
@@ -151,17 +914,17 @@ function renderDirectory(srcDir, destDir, context) {
151
914
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
152
915
  for (const entry of entries) {
153
916
  const renderedName = renderTemplate(entry.name, context);
154
- const srcPath = path.join(srcDir, entry.name);
917
+ const srcPath = path2.join(srcDir, entry.name);
155
918
  if (entry.isDirectory()) {
156
- const destSubDir = path.join(destDir, renderedName);
919
+ const destSubDir = path2.join(destDir, renderedName);
157
920
  renderDirectory(srcPath, destSubDir, context);
158
921
  } else if (entry.name.endsWith(".hbs")) {
159
922
  const outputName = renderedName.replace(/\.hbs$/, "");
160
- const destPath = path.join(destDir, outputName);
923
+ const destPath = path2.join(destDir, outputName);
161
924
  renderToFile(srcPath, destPath, context);
162
925
  } else {
163
- const destPath = path.join(destDir, renderedName);
164
- const destDirPath = path.dirname(destPath);
926
+ const destPath = path2.join(destDir, renderedName);
927
+ const destDirPath = path2.dirname(destPath);
165
928
  if (!fs.existsSync(destDirPath)) {
166
929
  fs.mkdirSync(destDirPath, { recursive: true });
167
930
  }
@@ -191,6 +954,7 @@ function insertBeforeMarker(filePath, marker, content) {
191
954
  }
192
955
 
193
956
  // src/utils/exec.ts
957
+ init_esm_shims();
194
958
  import { execa } from "execa";
195
959
  async function exec(command, args, options = {}) {
196
960
  const { cwd, silent = false, env } = options;
@@ -229,25 +993,26 @@ async function execPip(args, cwd, silent = false) {
229
993
  }
230
994
 
231
995
  // src/utils/paths.ts
232
- import path2 from "path";
996
+ init_esm_shims();
997
+ import path3 from "path";
233
998
  import fs2 from "fs";
234
- import { fileURLToPath } from "url";
235
- var __filename2 = fileURLToPath(import.meta.url);
236
- var __dirname2 = path2.dirname(__filename2);
999
+ import { fileURLToPath as fileURLToPath2 } from "url";
1000
+ var __filename2 = fileURLToPath2(import.meta.url);
1001
+ var __dirname2 = path3.dirname(__filename2);
237
1002
  function getTemplatesDir() {
238
- const devPath = path2.resolve(__dirname2, "..", "templates");
239
- const prodPath = path2.resolve(__dirname2, "..", "src", "templates");
1003
+ const devPath = path3.resolve(__dirname2, "..", "templates");
1004
+ const prodPath = path3.resolve(__dirname2, "..", "src", "templates");
240
1005
  if (fs2.existsSync(devPath)) return devPath;
241
1006
  if (fs2.existsSync(prodPath)) return prodPath;
242
1007
  throw new Error("Templates directory not found. Make sure the CLI is properly installed.");
243
1008
  }
244
1009
  function findProjectRoot(startDir) {
245
1010
  let dir = startDir || process.cwd();
246
- while (dir !== path2.dirname(dir)) {
247
- if (fs2.existsSync(path2.join(dir, "blacksmith.config.json"))) {
1011
+ while (dir !== path3.dirname(dir)) {
1012
+ if (fs2.existsSync(path3.join(dir, "blacksmith.config.json"))) {
248
1013
  return dir;
249
1014
  }
250
- dir = path2.dirname(dir);
1015
+ dir = path3.dirname(dir);
251
1016
  }
252
1017
  throw new Error(
253
1018
  'Not inside a Blacksmith project. Run "blacksmith init <name>" to create one, or navigate to an existing Blacksmith project.'
@@ -255,23 +1020,25 @@ function findProjectRoot(startDir) {
255
1020
  }
256
1021
  function getBackendDir(projectRoot) {
257
1022
  const root = projectRoot || findProjectRoot();
258
- return path2.join(root, "backend");
1023
+ return path3.join(root, "backend");
259
1024
  }
260
1025
  function getFrontendDir(projectRoot) {
261
1026
  const root = projectRoot || findProjectRoot();
262
- return path2.join(root, "frontend");
1027
+ return path3.join(root, "frontend");
263
1028
  }
264
1029
  function loadConfig(projectRoot) {
265
1030
  const root = projectRoot || findProjectRoot();
266
- const configPath = path2.join(root, "blacksmith.config.json");
1031
+ const configPath = path3.join(root, "blacksmith.config.json");
267
1032
  return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
268
1033
  }
269
1034
 
270
1035
  // src/commands/ai-setup.ts
271
- import path3 from "path";
1036
+ init_esm_shims();
1037
+ import path4 from "path";
272
1038
  import fs3 from "fs";
273
1039
 
274
1040
  // src/skills/core-rules.ts
1041
+ init_esm_shims();
275
1042
  var coreRulesSkill = {
276
1043
  id: "core-rules",
277
1044
  // No `name` → content is inlined directly into CLAUDE.md, not a separate file
@@ -280,12 +1047,12 @@ var coreRulesSkill = {
280
1047
 
281
1048
  > **These rules are mandatory. Violating them produces broken, inconsistent code.**
282
1049
 
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
1050
+ ### 1. Use \`@chakra-ui/react\` for ALL UI
1051
+ - **Layout**: Use \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`, \`Box\`, \`Container\` \u2014 NEVER \`<div className="flex ...">\` or \`<div className="grid ...">\`
1052
+ - **Typography**: Use \`Heading\` and \`Text\` \u2014 NEVER raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, or \`<span>\` with text classes
1053
+ - **Separators**: Use \`Divider\` \u2014 NEVER \`<hr>\`
1054
+ - **Everything else**: \`Button\`, \`Card\`, \`Badge\`, \`Input\`, \`Table\`, \`Modal\`, \`Alert\`, \`Skeleton\`, \`Stat\`, etc.
1055
+ - See the \`chakra-ui-react\` skill for the full component list
289
1056
 
290
1057
  ### 2. Pages Are Thin Orchestrators
291
1058
  - A page file should be ~20-30 lines: import components, call hooks, compose JSX
@@ -319,6 +1086,7 @@ pages/<page>/
319
1086
  };
320
1087
 
321
1088
  // src/skills/project-overview.ts
1089
+ init_esm_shims();
322
1090
  var projectOverviewSkill = {
323
1091
  id: "project-overview",
324
1092
  name: "Project Overview",
@@ -374,6 +1142,7 @@ ${ctx.projectName}/
374
1142
  };
375
1143
 
376
1144
  // src/skills/django.ts
1145
+ init_esm_shims();
377
1146
  var djangoSkill = {
378
1147
  id: "django",
379
1148
  name: "Django Backend Conventions",
@@ -455,6 +1224,7 @@ var djangoSkill = {
455
1224
  };
456
1225
 
457
1226
  // src/skills/django-rest-advanced.ts
1227
+ init_esm_shims();
458
1228
  var djangoRestAdvancedSkill = {
459
1229
  id: "django-rest-advanced",
460
1230
  name: "Advanced Django REST Framework",
@@ -982,6 +1752,7 @@ class OrderViewSet(ModelViewSet):
982
1752
  };
983
1753
 
984
1754
  // src/skills/api-documentation.ts
1755
+ init_esm_shims();
985
1756
  var apiDocumentationSkill = {
986
1757
  id: "api-documentation",
987
1758
  name: "API Documentation",
@@ -1299,6 +2070,7 @@ def internal_health_check(self, request):
1299
2070
  };
1300
2071
 
1301
2072
  // src/skills/react.ts
2073
+ init_esm_shims();
1302
2074
  var reactSkill = {
1303
2075
  id: "react",
1304
2076
  name: "React Frontend Conventions",
@@ -1312,7 +2084,8 @@ var reactSkill = {
1312
2084
  - TanStack React Query for server state management
1313
2085
  - React Router v7 for client-side routing
1314
2086
  - React Hook Form + Zod for forms and validation
1315
- - Tailwind CSS for styling
2087
+ - Chakra UI v2 for component library and theming
2088
+ - Tailwind CSS for additional styling
1316
2089
  - \`@hey-api/openapi-ts\` for auto-generating API client from Django's OpenAPI schema
1317
2090
  - \`lucide-react\` for icons
1318
2091
 
@@ -1341,11 +2114,11 @@ var reactSkill = {
1341
2114
  - **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
2115
 
1343
2116
  ### 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
2117
+ - **All UI must use \`@chakra-ui/react\` components** \u2014 see the \`chakra-ui-react\` skill for the full component list
2118
+ - Use \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`, \`Box\` for layout \u2014 never raw \`<div>\` with flex/grid classes
2119
+ - Use \`Heading\` and \`Text\` for headings and text \u2014 never raw \`<h1>\`\u2013\`<h6>\` or \`<p>\`
2120
+ - Use \`Divider\` instead of \`<hr>\`
2121
+ - Use \`Stat\`, \`Skeleton\` instead of building custom equivalents
1349
2122
 
1350
2123
  ### Route Paths
1351
2124
  - All route paths live in the \`Path\` enum at \`src/router/paths.ts\` \u2014 **never hardcode path strings**
@@ -1353,12 +2126,12 @@ var reactSkill = {
1353
2126
  - Use \`buildPath()\` for dynamic segments \u2014 see the \`page-structure\` skill for details
1354
2127
 
1355
2128
  ### 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
2129
+ - Use Chakra UI style props as the primary styling approach
2130
+ - Use Tailwind CSS utility classes for additional styling needs
2131
+ - Theming via Chakra UI \`extendTheme()\` and design tokens
2132
+ - Color mode is supported via Chakra UI \`useColorMode()\` hook
2133
+ - Use responsive props (\`{{ base: ..., md: ..., lg: ... }}\`) for responsive layouts
2134
+ - Avoid inline \`style\` attributes \u2014 use Chakra style props or Tailwind classes instead
1362
2135
 
1363
2136
  ### Path Aliases
1364
2137
  - \`@/\` maps to \`frontend/src/\`
@@ -1368,17 +2141,19 @@ var reactSkill = {
1368
2141
  ### Error Handling
1369
2142
  - Use React Error Boundary (\`frontend/src/router/error-boundary.tsx\`) for render errors
1370
2143
  - 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)
2144
+ - Display user-facing errors using the project's feedback components (Alert, useToast)
1372
2145
 
1373
2146
  ### 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\`)
2147
+ - See the \`frontend-testing\` skill for full conventions on test placement, utilities, mocking, and what to test
2148
+ - **Every code change must include corresponding tests** \u2014 see the \`frontend-testing\` skill for the complete rules
2149
+ - Tests use \`.spec.tsx\` / \`.spec.ts\` and live in \`__tests__/\` folders co-located with source code
2150
+ - Always use \`renderWithProviders\` from \`@/__tests__/test-utils\` \u2014 never import \`render\` from \`@testing-library/react\` directly
1377
2151
  `;
1378
2152
  }
1379
2153
  };
1380
2154
 
1381
2155
  // src/skills/react-query.ts
2156
+ init_esm_shims();
1382
2157
  var reactQuerySkill = {
1383
2158
  id: "react-query",
1384
2159
  name: "TanStack React Query",
@@ -1605,6 +2380,7 @@ export function useDeletePost() {
1605
2380
  };
1606
2381
 
1607
2382
  // src/skills/page-structure.ts
2383
+ init_esm_shims();
1608
2384
  var pageStructureSkill = {
1609
2385
  id: "page-structure",
1610
2386
  name: "Page & Route Structure",
@@ -1778,8 +2554,8 @@ pages/dashboard/
1778
2554
  \`\`\`
1779
2555
 
1780
2556
  \`\`\`tsx
1781
- // dashboard.tsx \u2014 thin orchestrator using @blacksmith-ui/react layout
1782
- import { Stack, Grid, Divider } from '@blacksmith-ui/react'
2557
+ // dashboard.tsx \u2014 thin orchestrator using @chakra-ui/react layout
2558
+ import { VStack, SimpleGrid, Divider } from '@chakra-ui/react'
1783
2559
  import { StatsCards } from './components/stats-cards'
1784
2560
  import { RecentActivity } from './components/recent-activity'
1785
2561
  import { QuickActions } from './components/quick-actions'
@@ -1789,14 +2565,14 @@ export default function DashboardPage() {
1789
2565
  const { stats, activity, isLoading } = useDashboardData()
1790
2566
 
1791
2567
  return (
1792
- <Stack gap={6}>
2568
+ <VStack spacing={6} align="stretch">
1793
2569
  <StatsCards stats={stats} isLoading={isLoading} />
1794
2570
  <Divider />
1795
- <Grid columns={{ base: 1, lg: 3 }} gap={6}>
1796
- <RecentActivity items={activity} isLoading={isLoading} className="lg:col-span-2" />
2571
+ <SimpleGrid columns={{ base: 1, lg: 3 }} spacing={6}>
2572
+ <RecentActivity items={activity} isLoading={isLoading} gridColumn={{ lg: 'span 2' }} />
1797
2573
  <QuickActions />
1798
- </Grid>
1799
- </Stack>
2574
+ </SimpleGrid>
2575
+ </VStack>
1800
2576
  )
1801
2577
  }
1802
2578
  \`\`\`
@@ -1856,7 +2632,7 @@ export function useOrdersPage() {
1856
2632
  }
1857
2633
 
1858
2634
  // orders-page.tsx
1859
- import { Stack } from '@blacksmith-ui/react'
2635
+ import { VStack } from '@chakra-ui/react'
1860
2636
  import { useOrdersPage } from './hooks/use-orders-page'
1861
2637
  import { OrdersTable } from './components/orders-table'
1862
2638
  import { OrdersToolbar } from './components/orders-toolbar'
@@ -1865,10 +2641,10 @@ export default function OrdersPage() {
1865
2641
  const { orders, total, isLoading, page, setPage, search, setSearch, deleteOrder } = useOrdersPage()
1866
2642
 
1867
2643
  return (
1868
- <Stack gap={4}>
2644
+ <VStack spacing={4} align="stretch">
1869
2645
  <OrdersToolbar search={search} onSearchChange={setSearch} />
1870
2646
  <OrdersTable orders={orders} isLoading={isLoading} onDelete={(id) => deleteOrder.mutate({ path: { id } })} />
1871
- </Stack>
2647
+ </VStack>
1872
2648
  )
1873
2649
  }
1874
2650
  \`\`\`
@@ -1901,217 +2677,176 @@ export default function OrdersPage() {
1901
2677
  }
1902
2678
  };
1903
2679
 
1904
- // src/skills/blacksmith-ui-react.ts
1905
- var blacksmithUiReactSkill = {
1906
- id: "blacksmith-ui-react",
1907
- name: "@blacksmith-ui/react",
1908
- description: "Core UI component library \u2014 60+ components for layout, typography, inputs, data display, overlays, feedback, media, and navigation.",
2680
+ // src/skills/chakra-ui-react.ts
2681
+ init_esm_shims();
2682
+ var chakraUiReactSkill = {
2683
+ id: "chakra-ui-react",
2684
+ name: "Chakra UI React",
2685
+ description: "Core UI component library \u2014 Chakra UI v2 components for layout, typography, inputs, data display, overlays, feedback, media, and navigation.",
1909
2686
  render(_ctx) {
1910
- return `## @blacksmith-ui/react \u2014 Core UI Components (60+)
2687
+ return `## Chakra UI React \u2014 Core UI Components
1911
2688
 
1912
- > **CRITICAL RULE: Every UI element MUST be built using \`@blacksmith-ui/react\` components \u2014 including layout and typography.**
1913
- > Do NOT use raw HTML elements when a Blacksmith-UI component exists for that purpose.
1914
- > This includes layout: use \`Flex\`, \`Stack\`, \`Grid\`, \`Box\`, \`Container\` instead of \`<div>\` with flex/grid classes.
1915
- > This includes typography: use \`Text\` and \`Typography\` instead of raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, \`<span>\`.
2689
+ > **CRITICAL RULE: Every UI element MUST be built using \`@chakra-ui/react\` components \u2014 including layout and typography.**
2690
+ > Do NOT use raw HTML elements when a Chakra UI component exists for that purpose.
2691
+ > This includes layout: use \`HStack\`, \`VStack\`, \`SimpleGrid\`, \`Box\`, \`Container\` instead of \`<div>\` with flex/grid classes.
2692
+ > This includes typography: use \`Heading\` and \`Text\` instead of raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, \`<span>\`.
1916
2693
 
1917
2694
  ### Layout
1918
2695
 
1919
2696
  | Component | Use instead of | Description |
1920
2697
  |-----------|---------------|-------------|
1921
2698
  | \`Box\` | \`<div>\` | Base layout primitive with style props |
1922
- | \`Flex\` | \`<div className="flex ...">\` | Flexbox container with style props (\`direction\`, \`align\`, \`justify\`, \`gap\`, \`wrap\`) |
1923
- | \`Grid\` | \`<div className="grid ...">\` | CSS Grid container (\`columns\`, \`rows\`, \`gap\`) |
1924
- | \`Stack\` | \`<div className="flex flex-col gap-...">\` | Vertical/horizontal stack (\`direction\`, \`gap\`) |
2699
+ | \`Flex\` / \`HStack\` | \`<div className="flex ...">\` | Flexbox container with style props (\`direction\`, \`align\`, \`justify\`, \`gap\`, \`wrap\`) |
2700
+ | \`SimpleGrid\` | \`<div className="grid ...">\` | CSS Grid container (\`columns\`, \`spacing\`) |
2701
+ | \`VStack\` | \`<div className="flex flex-col gap-...">\` | Vertical stack (\`spacing\`) |
2702
+ | \`HStack\` | \`<div className="flex flex-row gap-...">\` | Horizontal stack (\`spacing\`) |
1925
2703
  | \`Container\` | \`<div className="max-w-7xl mx-auto px-...">\` | Max-width centered container |
1926
2704
  | \`Divider\` | \`<hr>\` or border hacks | Visual separator (horizontal/vertical) |
1927
2705
  | \`AspectRatio\` | padding-bottom trick | Maintain aspect ratio for content |
1928
- | \`Resizable\` | custom resize logic | Resizable panel groups |
1929
- | \`ScrollArea\` | \`overflow-auto\` divs | Custom scrollbar container |
1930
2706
 
1931
2707
  ### Typography
1932
2708
 
1933
2709
  | Component | Use instead of | Description |
1934
2710
  |-----------|---------------|-------------|
1935
- | \`Text\` | \`<p>\`, \`<span>\` | Text display with style props (\`size\`, \`weight\`, \`color\`, \`align\`) |
1936
- | \`Typography\` | \`<h1>\`\u2013\`<h6>\`, \`<p>\` | Semantic heading/paragraph elements (\`variant\`: h1\u2013h6, p, lead, muted, etc.) |
1937
- | \`Label\` | \`<label>\` | Form label with accessibility support |
2711
+ | \`Text\` | \`<p>\`, \`<span>\` | Text display with style props (\`fontSize\`, \`fontWeight\`, \`color\`, \`textAlign\`) |
2712
+ | \`Heading\` | \`<h1>\`\u2013\`<h6>\`, \`<p>\` | Semantic heading elements (\`as\`: h1\u2013h6, \`size\`: 2xl, xl, lg, md, sm, xs) |
2713
+ | \`FormLabel\` | \`<label>\` | Form label with accessibility support |
1938
2714
 
1939
2715
  ### Cards & Containers
1940
2716
 
1941
- - \`Card\`, \`CardHeader\`, \`CardTitle\`, \`CardDescription\`, \`CardContent\`, \`CardFooter\` \u2014 Use instead of styled \`<div>\` containers
1942
- - \`StatCard\` \u2014 Use for metric/stat display (value, label, trend)
1943
- - \`EmptyState\` \u2014 Use for empty content placeholders instead of custom empty divs
2717
+ - \`Card\`, \`CardHeader\`, \`CardBody\`, \`CardFooter\` \u2014 Use instead of styled \`<div>\` containers. Use \`Heading\` and \`Text\` inside for title/description.
1944
2718
 
1945
2719
  ### Actions
1946
2720
 
1947
2721
  - \`Button\` \u2014 Use instead of \`<button>\` or \`<a>\` styled as buttons
1948
- - Variants: \`default\`, \`secondary\`, \`destructive\`, \`outline\`, \`ghost\`, \`link\`
1949
- - Sizes: \`sm\`, \`default\`, \`lg\`, \`icon\`
1950
- - \`Toggle\`, \`ToggleGroup\` \u2014 Use for toggle buttons
1951
- - \`DropdownMenu\`, \`DropdownMenuTrigger\`, \`DropdownMenuContent\`, \`DropdownMenuItem\`, \`DropdownMenuSeparator\`, \`DropdownMenuLabel\` \u2014 Use for action menus
1952
- - \`ContextMenu\` \u2014 Use for right-click menus
1953
- - \`Menubar\` \u2014 Use for application menu bars
1954
- - \`AlertDialog\`, \`AlertDialogTrigger\`, \`AlertDialogContent\`, \`AlertDialogAction\`, \`AlertDialogCancel\` \u2014 Use for destructive action confirmations
2722
+ - Variants: \`solid\`, \`outline\`, \`ghost\`, \`link\`
2723
+ - Sizes: \`xs\`, \`sm\`, \`md\`, \`lg\`
2724
+ - Use \`colorScheme\` for color: \`blue\`, \`red\`, \`green\`, \`gray\`, etc.
2725
+ - \`IconButton\` \u2014 Use for icon-only buttons
2726
+ - \`Menu\`, \`MenuButton\`, \`MenuList\`, \`MenuItem\`, \`MenuDivider\`, \`MenuGroup\` \u2014 Use for action menus
2727
+ - \`AlertDialog\`, \`AlertDialogOverlay\`, \`AlertDialogContent\`, \`AlertDialogHeader\`, \`AlertDialogBody\`, \`AlertDialogFooter\` \u2014 Use for destructive action confirmations
1955
2728
 
1956
2729
  ### Data Entry
1957
2730
 
1958
2731
  - \`Input\` \u2014 Use instead of \`<input>\`
1959
- - \`SearchInput\` \u2014 Use for search fields (has built-in search icon)
2732
+ - \`InputGroup\`, \`InputLeftElement\`, \`InputRightElement\` \u2014 Use for input with icons/addons
1960
2733
  - \`Textarea\` \u2014 Use instead of \`<textarea>\`
1961
- - \`NumberInput\` \u2014 Use for numeric inputs with increment/decrement
1962
- - \`Select\`, \`SelectTrigger\`, \`SelectContent\`, \`SelectItem\`, \`SelectValue\` \u2014 Use instead of \`<select>\`
1963
- - \`Checkbox\` \u2014 Use instead of \`<input type="checkbox">\`
1964
- - \`RadioGroup\`, \`RadioGroupItem\` \u2014 Use instead of \`<input type="radio">\`
2734
+ - \`NumberInput\`, \`NumberInputField\`, \`NumberInputStepper\`, \`NumberIncrementStepper\`, \`NumberDecrementStepper\` \u2014 Use for numeric inputs
2735
+ - \`Select\` \u2014 Use instead of \`<select>\`
2736
+ - \`Checkbox\`, \`CheckboxGroup\` \u2014 Use instead of \`<input type="checkbox">\`
2737
+ - \`Radio\`, \`RadioGroup\` \u2014 Use instead of \`<input type="radio">\`
1965
2738
  - \`Switch\` \u2014 Use for toggle switches
1966
- - \`Slider\` \u2014 Use for single range inputs
1967
- - \`RangeSlider\` \u2014 Use for dual-handle range selection
1968
- - \`DatePicker\` \u2014 Use for date selection with calendar popup
1969
- - \`PinInput\` / \`InputOTP\` \u2014 Use for PIN/OTP code entry
1970
- - \`ColorPicker\` \u2014 Use for color selection
1971
- - \`FileUpload\` \u2014 Use for file upload with drag & drop
1972
- - \`TagInput\` \u2014 Use for tag/chip input with add/remove
1973
- - \`Rating\` \u2014 Use for star/icon rating selection
1974
- - \`Label\` \u2014 Use instead of \`<label>\`
2739
+ - \`Slider\`, \`SliderTrack\`, \`SliderFilledTrack\`, \`SliderThumb\` \u2014 Use for range inputs
2740
+ - \`PinInput\`, \`PinInputField\` \u2014 Use for PIN/OTP code entry
2741
+ - \`FormLabel\` \u2014 Use instead of \`<label>\`
1975
2742
 
1976
2743
  ### Data Display
1977
2744
 
1978
- - \`Table\`, \`TableHeader\`, \`TableBody\`, \`TableRow\`, \`TableHead\`, \`TableCell\` \u2014 Use instead of \`<table>\` elements
1979
- - \`DataTable\` \u2014 Use for feature-rich tables with sorting, filtering, and pagination
1980
- - \`Badge\` \u2014 Use for status indicators, tags, counts (variants: \`default\`, \`secondary\`, \`destructive\`, \`outline\`)
1981
- - \`Avatar\`, \`AvatarImage\`, \`AvatarFallback\` \u2014 Use for user profile images
1982
- - \`Tooltip\`, \`TooltipTrigger\`, \`TooltipContent\`, \`TooltipProvider\` \u2014 Use for hover hints
1983
- - \`HoverCard\` \u2014 Use for rich hover content
1984
- - \`Calendar\` \u2014 Use for full calendar display
1985
- - \`Chart\` \u2014 Use for data visualization (powered by Recharts)
1986
- - \`Timeline\` \u2014 Use for chronological event display
1987
- - \`Tree\` \u2014 Use for hierarchical tree views
1988
- - \`List\` \u2014 Use for structured list display instead of \`<ul>\`/\`<ol>\`
1989
- - \`Skeleton\` \u2014 Use for loading placeholders
2745
+ - \`Table\`, \`Thead\`, \`Tbody\`, \`Tr\`, \`Th\`, \`Td\`, \`TableContainer\` \u2014 Use instead of \`<table>\` elements
2746
+ - \`Badge\` \u2014 Use for status indicators, tags, counts (\`colorScheme\`: \`green\`, \`red\`, \`blue\`, \`gray\`, etc.)
2747
+ - \`Avatar\` \u2014 Use for user profile images (use \`name\` prop for fallback initials)
2748
+ - \`Tooltip\` \u2014 Use for hover hints (\`label\` prop for content)
2749
+ - \`Stat\`, \`StatLabel\`, \`StatNumber\`, \`StatHelpText\`, \`StatArrow\` \u2014 Use for metric/stat display
2750
+ - \`Skeleton\`, \`SkeletonText\`, \`SkeletonCircle\` \u2014 Use for loading placeholders
1990
2751
  - \`Spinner\` \u2014 Use for loading indicators
1991
2752
  - \`Progress\` \u2014 Use for progress bars
1992
- - \`Pagination\`, \`PaginationContent\`, \`PaginationItem\`, \`PaginationLink\`, \`PaginationNext\`, \`PaginationPrevious\` \u2014 Use for paginated lists
2753
+ - \`Tag\`, \`TagLabel\`, \`TagCloseButton\` \u2014 Use for removable tags
1993
2754
 
1994
2755
  ### Tabs & Accordion
1995
2756
 
1996
- - \`Tabs\`, \`TabsList\`, \`TabsTrigger\`, \`TabsContent\` \u2014 Use for tabbed interfaces
1997
- - \`Accordion\`, \`AccordionItem\`, \`AccordionTrigger\`, \`AccordionContent\` \u2014 Use for collapsible sections
2757
+ - \`Tabs\`, \`TabList\`, \`Tab\`, \`TabPanels\`, \`TabPanel\` \u2014 Use for tabbed interfaces
2758
+ - \`Accordion\`, \`AccordionItem\`, \`AccordionButton\`, \`AccordionPanel\`, \`AccordionIcon\` \u2014 Use for collapsible sections
1998
2759
 
1999
2760
  ### Overlays
2000
2761
 
2001
- - \`Dialog\`, \`DialogTrigger\`, \`DialogContent\`, \`DialogHeader\`, \`DialogTitle\`, \`DialogDescription\`, \`DialogFooter\` \u2014 Use for modals
2762
+ - \`Modal\`, \`ModalOverlay\`, \`ModalContent\`, \`ModalHeader\`, \`ModalBody\`, \`ModalFooter\`, \`ModalCloseButton\` \u2014 Use for modals
2002
2763
  - \`AlertDialog\` \u2014 Use for confirmation dialogs
2003
- - \`Drawer\` / \`Sheet\`, \`SheetTrigger\`, \`SheetContent\`, \`SheetHeader\`, \`SheetTitle\`, \`SheetDescription\` \u2014 Use for slide-out panels
2004
- - \`Popover\` \u2014 Use for floating content panels
2005
- - \`CommandPalette\` \u2014 Use for searchable command menus (cmdk-based)
2764
+ - \`Drawer\`, \`DrawerOverlay\`, \`DrawerContent\`, \`DrawerHeader\`, \`DrawerBody\`, \`DrawerFooter\`, \`DrawerCloseButton\` \u2014 Use for slide-out panels
2765
+ - \`Popover\`, \`PopoverTrigger\`, \`PopoverContent\`, \`PopoverHeader\`, \`PopoverBody\`, \`PopoverArrow\`, \`PopoverCloseButton\` \u2014 Use for floating content panels
2006
2766
 
2007
2767
  ### Navigation
2008
2768
 
2009
- - \`Breadcrumb\`, \`BreadcrumbList\`, \`BreadcrumbItem\`, \`BreadcrumbLink\`, \`BreadcrumbSeparator\` \u2014 Use for breadcrumb trails
2010
- - \`NavigationMenu\`, \`NavigationMenuList\`, \`NavigationMenuItem\`, \`NavigationMenuTrigger\`, \`NavigationMenuContent\` \u2014 Use for site navigation
2011
- - \`Sidebar\` \u2014 Use for app sidebars
2012
- - \`Dock\` \u2014 Use for macOS-style dock navigation
2013
- - \`BackToTop\` \u2014 Use for scroll-to-top buttons
2769
+ - \`Breadcrumb\`, \`BreadcrumbItem\`, \`BreadcrumbLink\` \u2014 Use for breadcrumb trails
2014
2770
 
2015
2771
  ### Feedback
2016
2772
 
2017
- - \`Alert\`, \`AlertTitle\`, \`AlertDescription\` \u2014 Use for inline messages/warnings
2018
- - \`AlertBanner\` \u2014 Use for full-width alert banners
2019
- - \`Toast\` / \`Toaster\` / \`useToast\` \u2014 Use for transient notifications
2020
- - \`SonnerToaster\` \u2014 Sonner-based toast notifications
2021
-
2022
- ### Media
2023
-
2024
- - \`Image\` \u2014 Use instead of \`<img>\` for optimized image display
2025
- - \`VideoPlayer\` \u2014 Use for video playback
2026
- - \`CodeBlock\` \u2014 Use for syntax-highlighted code (Shiki-powered)
2027
- - \`Carousel\` \u2014 Use for image/content carousels
2028
- - \`Lightbox\` \u2014 Use for full-screen media viewers
2029
-
2030
- ### Specialized
2031
-
2032
- - \`Stepper\` / \`Wizard\` \u2014 Use for multi-step workflows
2033
- - \`NotificationCenter\` / \`useNotificationCenter\` \u2014 Use for notification management
2034
- - \`SpotlightTour\` \u2014 Use for guided feature tours
2773
+ - \`Alert\`, \`AlertIcon\`, \`AlertTitle\`, \`AlertDescription\` \u2014 Use for inline messages/warnings (\`status\`: \`error\`, \`warning\`, \`success\`, \`info\`)
2774
+ - \`useToast\` \u2014 Use for transient notifications
2035
2775
 
2036
2776
  ### Utilities & Hooks
2037
2777
 
2038
- - \`cn()\` \u2014 Merge class names (clsx + tailwind-merge)
2778
+ - \`useColorMode()\` \u2014 Color mode toggle. Returns \`{ colorMode, toggleColorMode }\`
2779
+ - \`useDisclosure()\` \u2014 Open/close state for modals, drawers, etc. Returns \`{ isOpen, onOpen, onClose, onToggle }\`
2780
+ - \`useBreakpointValue()\` \u2014 Responsive values based on breakpoint
2039
2781
  - \`useToast()\` \u2014 Programmatic toast notifications
2040
- - \`useMobile()\` \u2014 Responsive breakpoint detection
2041
- - \`useDarkMode()\` \u2014 Dark mode toggle. Returns \`{ isDark, toggle }\`
2782
+ - \`useMediaQuery()\` \u2014 CSS media query matching
2042
2783
 
2043
2784
  ---
2044
2785
 
2045
2786
  ### Component-First Rules
2046
2787
 
2047
- 1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<Grid>\`, \`<Stack>\`, \`<Box>\` from \`@blacksmith-ui/react\`.
2788
+ 1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<SimpleGrid>\`, \`<VStack>\`, \`<HStack>\`, \`<Box>\` from \`@chakra-ui/react\`.
2048
2789
  2. **Centering/max-width**: NEVER use \`<div className="max-w-7xl mx-auto px-...">\`. Use \`<Container>\`.
2049
- 3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<Typography variant="h2">\` or \`<Text>\`.
2790
+ 3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<Heading as="h2" size="lg">\` or \`<Text>\`.
2050
2791
  4. **Separators**: NEVER use \`<hr>\` or border hacks. Use \`<Divider>\`.
2051
- 5. **Images**: NEVER use raw \`<img>\`. Use \`<Image>\` from \`@blacksmith-ui/react\` (use \`Avatar\` for profile pictures).
2052
- 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.
2053
- 7. **Buttons**: NEVER use \`<button>\` or \`<a>\` styled as a button. Use \`<Button>\`.
2054
- 8. **Inputs**: NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` directly. Use the Blacksmith-UI equivalents.
2055
- 9. **Cards**: NEVER use a styled \`<div>\` as a card. Use \`Card\` + sub-components.
2056
- 10. **Tables**: NEVER use raw \`<table>\` HTML. Use \`Table\` or \`DataTable\`.
2057
- 11. **Loading**: NEVER use custom \`animate-pulse\` divs. Use \`Skeleton\` or \`Spinner\`.
2058
- 12. **Modals**: NEVER build custom modals. Use \`Dialog\`, \`AlertDialog\`, \`Drawer\`, or \`Sheet\`.
2059
- 13. **Feedback**: NEVER use plain styled text for errors/warnings. Use \`Alert\` or \`useToast\`.
2060
- 14. **Empty states**: NEVER build custom empty-state UIs. Use \`EmptyState\`.
2061
- 15. **Metrics**: NEVER build custom stat/metric cards. Use \`StatCard\`.
2792
+ 5. **Buttons**: NEVER use \`<button>\` or \`<a>\` styled as a button. Use \`<Button>\`.
2793
+ 6. **Inputs**: NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` directly. Use the Chakra UI equivalents.
2794
+ 7. **Cards**: NEVER use a styled \`<div>\` as a card. Use \`Card\` + sub-components.
2795
+ 8. **Tables**: NEVER use raw \`<table>\` HTML. Use Chakra UI \`Table\` components.
2796
+ 9. **Loading**: NEVER use custom \`animate-pulse\` divs. Use \`Skeleton\` or \`Spinner\`.
2797
+ 10. **Modals**: NEVER build custom modals. Use \`Modal\`, \`AlertDialog\`, or \`Drawer\`.
2798
+ 11. **Feedback**: NEVER use plain styled text for errors/warnings. Use \`Alert\` or \`useToast\`.
2062
2799
 
2063
2800
  ### When Raw HTML IS Acceptable
2064
2801
 
2065
- - \`<main>\`, \`<section>\`, \`<header>\`, \`<footer>\`, \`<nav>\`, \`<article>\`, \`<aside>\` \u2014 semantic HTML landmarks for page structure (but use \`Flex\`/\`Stack\`/\`Grid\` inside them for layout)
2066
- - \`<Link>\` from react-router-dom \u2014 for page navigation (use \`<Button asChild><Link>...</Link></Button>\` if it needs button styling)
2802
+ - \`<main>\`, \`<section>\`, \`<header>\`, \`<footer>\`, \`<nav>\`, \`<article>\`, \`<aside>\` \u2014 semantic HTML landmarks for page structure (but use \`Flex\`/\`VStack\`/\`SimpleGrid\` inside them for layout)
2803
+ - \`<Link>\` from react-router-dom \u2014 for page navigation (wrap with \`<Button as={Link}>...\` if it needs button styling)
2067
2804
  - Icon components from \`lucide-react\`
2068
- - \`<form>\` element when used with React Hook Form (but use \`@blacksmith-ui/forms\` components inside)
2805
+ - \`<form>\` element when used with React Hook Form (but use Chakra UI form components inside)
2069
2806
 
2070
2807
  ### Design Tokens & Theming
2071
2808
 
2072
- - \`ThemeProvider\` \u2014 Wrap app to apply preset or custom theme
2073
- - Built-in presets: \`default\`, \`blue\`, \`green\`, \`violet\`, \`red\`, \`neutral\`
2074
- - All components use HSL CSS variables (\`--background\`, \`--foreground\`, \`--primary\`, etc.)
2075
- - Dark mode: \`.dark\` class strategy on \`<html>\`, or \`<ThemeProvider mode="dark">\`
2076
- - Border radius: controlled by \`--radius\` CSS variable
2077
- - Extend with \`className\` prop + \`cn()\` utility for custom styles
2078
- - Global styles: \`@import '@blacksmith-ui/react/styles.css'\` in app entry
2809
+ - \`ChakraProvider\` \u2014 Wrap app with \`theme\` prop to apply preset or custom theme
2810
+ - Extend theme with \`extendTheme()\` from \`@chakra-ui/react\`
2811
+ - All components use design tokens from the theme
2812
+ - Color mode: \`useColorMode()\` hook, or \`ColorModeScript\` in document head
2813
+ - Extend with \`sx\` prop or \`style props\` for custom styles
2079
2814
 
2080
2815
  ### Example: HowItWorks Section (Correct Way)
2081
2816
 
2082
2817
  \`\`\`tsx
2083
- import { Container, Stack, Flex, Grid, Text, Typography, Image } from '@blacksmith-ui/react'
2818
+ import { Container, VStack, Flex, SimpleGrid, Text, Heading, Box } from '@chakra-ui/react'
2084
2819
  import { howItWorksSteps } from '../data'
2085
2820
 
2086
2821
  export function HowItWorks() {
2087
2822
  return (
2088
- <Box as="section" className="py-16 sm:py-20">
2089
- <Container>
2090
- <Stack gap={3} align="center" className="mb-12">
2091
- <Typography variant="h2">How It Works</Typography>
2092
- <Text color="muted">Book your stay in three simple steps</Text>
2093
- </Stack>
2094
-
2095
- <Grid columns={{ base: 1, md: 3 }} gap={8} className="max-w-4xl mx-auto">
2823
+ <Box as="section" py={{ base: 16, sm: 20 }}>
2824
+ <Container maxW="container.xl">
2825
+ <VStack spacing={3} align="center" mb={12}>
2826
+ <Heading as="h2" size="xl">How It Works</Heading>
2827
+ <Text color="gray.500">Book your stay in three simple steps</Text>
2828
+ </VStack>
2829
+
2830
+ <SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} maxW="4xl" mx="auto">
2096
2831
  {howItWorksSteps.map((item) => (
2097
- <Stack key={item.step} align="center" gap={4}>
2098
- <Box className="relative">
2099
- <Flex align="center" justify="center" className="h-16 w-16 rounded-full bg-primary text-primary-foreground shadow-lg shadow-primary/30">
2100
- <item.icon className="h-7 w-7" />
2832
+ <VStack key={item.step} align="center" spacing={4}>
2833
+ <Box position="relative">
2834
+ <Flex align="center" justify="center" h={16} w={16} rounded="full" bg="blue.500" color="white" shadow="lg">
2835
+ <item.icon size={28} />
2101
2836
  </Flex>
2102
- <Flex align="center" justify="center" className="absolute -top-1 -right-1 h-6 w-6 rounded-full bg-background border-2 border-primary">
2103
- <Text size="xs" weight="bold" color="primary">{item.step}</Text>
2837
+ <Flex align="center" justify="center" position="absolute" top={-1} right={-1} h={6} w={6} rounded="full" bg="white" borderWidth={2} borderColor="blue.500">
2838
+ <Text fontSize="xs" fontWeight="bold" color="blue.500">{item.step}</Text>
2104
2839
  </Flex>
2105
2840
  </Box>
2106
- <Stack gap={2} align="center">
2107
- <Text size="lg" weight="bold">{item.title}</Text>
2108
- <Text size="sm" color="muted" align="center" className="max-w-xs">
2841
+ <VStack spacing={2} align="center">
2842
+ <Text fontSize="lg" fontWeight="bold">{item.title}</Text>
2843
+ <Text fontSize="sm" color="gray.500" textAlign="center" maxW="xs">
2109
2844
  {item.description}
2110
2845
  </Text>
2111
- </Stack>
2112
- </Stack>
2846
+ </VStack>
2847
+ </VStack>
2113
2848
  ))}
2114
- </Grid>
2849
+ </SimpleGrid>
2115
2850
  </Container>
2116
2851
  </Box>
2117
2852
  )
@@ -2122,28 +2857,34 @@ export function HowItWorks() {
2122
2857
 
2123
2858
  \`\`\`tsx
2124
2859
  import {
2125
- Stack, Flex,
2126
- Card, CardHeader, CardTitle, CardContent,
2127
- Button, Badge, Skeleton,
2128
- Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
2129
- DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
2130
- AlertDialog, AlertDialogTrigger, AlertDialogContent,
2131
- AlertDialogAction, AlertDialogCancel,
2132
- } from '@blacksmith-ui/react'
2860
+ VStack, Flex, Box,
2861
+ Card, CardHeader, CardBody,
2862
+ Button, Badge, Skeleton, Heading,
2863
+ Table, Thead, Tbody, Tr, Th, Td, TableContainer,
2864
+ Menu, MenuButton, MenuList, MenuItem, MenuDivider,
2865
+ AlertDialog, AlertDialogOverlay, AlertDialogContent,
2866
+ AlertDialogHeader, AlertDialogBody, AlertDialogFooter,
2867
+ IconButton, useDisclosure,
2868
+ } from '@chakra-ui/react'
2133
2869
  import { MoreHorizontal, Plus, Trash2, Edit } from 'lucide-react'
2134
2870
  import { Link } from 'react-router-dom'
2871
+ import { useRef } from 'react'
2135
2872
 
2136
2873
  function ResourceListPage({ resources, isLoading, onDelete }) {
2874
+ const { isOpen, onOpen, onClose } = useDisclosure()
2875
+ const cancelRef = useRef()
2876
+ const [deleteId, setDeleteId] = useState(null)
2877
+
2137
2878
  if (isLoading) {
2138
2879
  return (
2139
2880
  <Card>
2140
- <CardContent className="p-6">
2141
- <Stack gap={4}>
2881
+ <CardBody p={6}>
2882
+ <VStack spacing={4}>
2142
2883
  {Array.from({ length: 5 }).map((_, i) => (
2143
- <Skeleton key={i} className="h-12 w-full" />
2884
+ <Skeleton key={i} h="48px" w="full" />
2144
2885
  ))}
2145
- </Stack>
2146
- </CardContent>
2886
+ </VStack>
2887
+ </CardBody>
2147
2888
  </Card>
2148
2889
  )
2149
2890
  }
@@ -2151,61 +2892,63 @@ function ResourceListPage({ resources, isLoading, onDelete }) {
2151
2892
  return (
2152
2893
  <Card>
2153
2894
  <CardHeader>
2154
- <Flex align="center" justify="between">
2155
- <CardTitle>Resources</CardTitle>
2156
- <Button asChild>
2157
- <Link to="/resources/new"><Plus className="mr-2 h-4 w-4" /> Create</Link>
2895
+ <Flex align="center" justify="space-between">
2896
+ <Heading size="md">Resources</Heading>
2897
+ <Button as={Link} to="/resources/new" leftIcon={<Plus size={16} />} colorScheme="blue">
2898
+ Create
2158
2899
  </Button>
2159
2900
  </Flex>
2160
2901
  </CardHeader>
2161
- <CardContent>
2162
- <Table>
2163
- <TableHeader>
2164
- <TableRow>
2165
- <TableHead>Title</TableHead>
2166
- <TableHead>Status</TableHead>
2167
- <TableHead className="w-12" />
2168
- </TableRow>
2169
- </TableHeader>
2170
- <TableBody>
2171
- {resources.map((r) => (
2172
- <TableRow key={r.id}>
2173
- <TableCell>{r.title}</TableCell>
2174
- <TableCell><Badge variant="outline">{r.status}</Badge></TableCell>
2175
- <TableCell>
2176
- <DropdownMenu>
2177
- <DropdownMenuTrigger asChild>
2178
- <Button variant="ghost" size="icon">
2179
- <MoreHorizontal className="h-4 w-4" />
2180
- </Button>
2181
- </DropdownMenuTrigger>
2182
- <DropdownMenuContent>
2183
- <DropdownMenuItem asChild>
2184
- <Link to={\`/resources/\${r.id}/edit\`}>
2185
- <Edit className="mr-2 h-4 w-4" /> Edit
2186
- </Link>
2187
- </DropdownMenuItem>
2188
- <AlertDialog>
2189
- <AlertDialogTrigger asChild>
2190
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
2191
- <Trash2 className="mr-2 h-4 w-4" /> Delete
2192
- </DropdownMenuItem>
2193
- </AlertDialogTrigger>
2194
- <AlertDialogContent>
2195
- <AlertDialogAction onClick={() => onDelete(r.id)}>
2196
- Delete
2197
- </AlertDialogAction>
2198
- <AlertDialogCancel>Cancel</AlertDialogCancel>
2199
- </AlertDialogContent>
2200
- </AlertDialog>
2201
- </DropdownMenuContent>
2202
- </DropdownMenu>
2203
- </TableCell>
2204
- </TableRow>
2205
- ))}
2206
- </TableBody>
2207
- </Table>
2208
- </CardContent>
2902
+ <CardBody>
2903
+ <TableContainer>
2904
+ <Table>
2905
+ <Thead>
2906
+ <Tr>
2907
+ <Th>Title</Th>
2908
+ <Th>Status</Th>
2909
+ <Th w="48px" />
2910
+ </Tr>
2911
+ </Thead>
2912
+ <Tbody>
2913
+ {resources.map((r) => (
2914
+ <Tr key={r.id}>
2915
+ <Td>{r.title}</Td>
2916
+ <Td><Badge variant="outline">{r.status}</Badge></Td>
2917
+ <Td>
2918
+ <Menu>
2919
+ <MenuButton as={IconButton} icon={<MoreHorizontal size={16} />} variant="ghost" size="sm" />
2920
+ <MenuList>
2921
+ <MenuItem as={Link} to={\`/resources/\${r.id}/edit\`} icon={<Edit size={16} />}>
2922
+ Edit
2923
+ </MenuItem>
2924
+ <MenuDivider />
2925
+ <MenuItem icon={<Trash2 size={16} />} color="red.500" onClick={() => { setDeleteId(r.id); onOpen() }}>
2926
+ Delete
2927
+ </MenuItem>
2928
+ </MenuList>
2929
+ </Menu>
2930
+ </Td>
2931
+ </Tr>
2932
+ ))}
2933
+ </Tbody>
2934
+ </Table>
2935
+ </TableContainer>
2936
+ </CardBody>
2937
+
2938
+ <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
2939
+ <AlertDialogOverlay>
2940
+ <AlertDialogContent>
2941
+ <AlertDialogHeader>Delete Resource</AlertDialogHeader>
2942
+ <AlertDialogBody>Are you sure? This cannot be undone.</AlertDialogBody>
2943
+ <AlertDialogFooter>
2944
+ <Button ref={cancelRef} onClick={onClose}>Cancel</Button>
2945
+ <Button colorScheme="red" onClick={() => { onDelete(deleteId); onClose() }} ml={3}>
2946
+ Delete
2947
+ </Button>
2948
+ </AlertDialogFooter>
2949
+ </AlertDialogContent>
2950
+ </AlertDialogOverlay>
2951
+ </AlertDialog>
2209
2952
  </Card>
2210
2953
  )
2211
2954
  }
@@ -2214,42 +2957,50 @@ function ResourceListPage({ resources, isLoading, onDelete }) {
2214
2957
  }
2215
2958
  };
2216
2959
 
2217
- // src/skills/blacksmith-ui-forms.ts
2218
- var blacksmithUiFormsSkill = {
2219
- id: "blacksmith-ui-forms",
2220
- name: "@blacksmith-ui/forms",
2221
- description: "Form components using React Hook Form + Zod for validation and submission.",
2960
+ // src/skills/chakra-ui-forms.ts
2961
+ init_esm_shims();
2962
+ var chakraUiFormsSkill = {
2963
+ id: "chakra-ui-forms",
2964
+ name: "Chakra UI Forms",
2965
+ description: "Form components using Chakra UI + React Hook Form + Zod for validation and submission.",
2222
2966
  render(_ctx) {
2223
- return `## @blacksmith-ui/forms \u2014 Form Components (React Hook Form + Zod)
2967
+ return `## Chakra UI Forms \u2014 Form Components (React Hook Form + Zod)
2224
2968
 
2225
- > **RULE: ALWAYS use these for forms.** Do NOT build forms with raw \`<form>\`, \`<input>\`, \`<label>\`, or manual error display.
2969
+ > **RULE: ALWAYS use Chakra UI form components for forms.** Do NOT build forms with raw \`<form>\`, \`<input>\`, \`<label>\`, or manual error display.
2226
2970
 
2227
2971
  \`\`\`tsx
2228
- import { Form, FormField, FormInput, FormTextarea, FormSelect } from '@blacksmith-ui/forms'
2972
+ import {
2973
+ FormControl, FormLabel, FormErrorMessage, FormHelperText,
2974
+ Input, Textarea, Select, Checkbox, Switch, NumberInput,
2975
+ NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper,
2976
+ Radio, RadioGroup, Button,
2977
+ } from '@chakra-ui/react'
2229
2978
  \`\`\`
2230
2979
 
2231
2980
  ### Components
2232
2981
 
2233
- - \`Form\` \u2014 Wraps the entire form. Props: \`form\` (useForm instance), \`onSubmit\`
2234
- - \`FormField\` \u2014 Wraps each field. Props: \`name\`, \`label\`, \`description?\`
2235
- - \`FormInput\` \u2014 Text input within FormField. Props: \`type\`, \`placeholder\`
2236
- - \`FormTextarea\` \u2014 Textarea within FormField. Props: \`rows\`, \`placeholder\`
2237
- - \`FormSelect\` \u2014 Select within FormField. Props: \`options\`, \`placeholder\`
2238
- - \`FormCheckbox\` \u2014 Checkbox within FormField
2239
- - \`FormSwitch\` \u2014 Toggle switch within FormField
2240
- - \`FormRadioGroup\` \u2014 Radio group within FormField. Props: \`options\`
2241
- - \`FormDatePicker\` \u2014 Date picker within FormField
2242
- - \`FormError\` \u2014 Displays field-level validation error (auto-handled by FormField)
2243
- - \`FormDescription\` \u2014 Displays helper text below a field
2982
+ - \`FormControl\` \u2014 Wraps each field. Props: \`isInvalid\`, \`isRequired\`, \`isDisabled\`
2983
+ - \`FormLabel\` \u2014 Label for a form field
2984
+ - \`FormErrorMessage\` \u2014 Displays field-level validation error (shown when \`FormControl\` has \`isInvalid\`)
2985
+ - \`FormHelperText\` \u2014 Displays helper text below a field
2986
+ - \`Input\` \u2014 Text input. Props: \`type\`, \`placeholder\`, \`size\`
2987
+ - \`Textarea\` \u2014 Textarea. Props: \`rows\`, \`placeholder\`
2988
+ - \`Select\` \u2014 Select dropdown. Props: \`placeholder\`
2989
+ - \`Checkbox\` \u2014 Checkbox input
2990
+ - \`Switch\` \u2014 Toggle switch
2991
+ - \`RadioGroup\` + \`Radio\` \u2014 Radio group
2992
+ - \`NumberInput\` \u2014 Numeric input with stepper
2244
2993
 
2245
2994
  ### Rules
2246
- - NEVER use raw \`<form>\` with manual \`<label>\` and error \`<p>\` tags. Always use \`Form\` + \`FormField\`.
2247
- - NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` inside forms. Use \`FormInput\`, \`FormTextarea\`, \`FormSelect\`.
2995
+ - NEVER use raw \`<form>\` with manual \`<label>\` and error \`<p>\` tags. Always use \`FormControl\` + \`FormLabel\` + \`FormErrorMessage\`.
2996
+ - NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` inside forms. Use Chakra UI \`Input\`, \`Textarea\`, \`Select\`.
2248
2997
 
2249
2998
  ### Form Pattern \u2014 ALWAYS follow this:
2250
2999
  \`\`\`tsx
2251
- import { Form, FormField, FormInput, FormTextarea, FormSelect } from '@blacksmith-ui/forms'
2252
- import { Button } from '@blacksmith-ui/react'
3000
+ import {
3001
+ FormControl, FormLabel, FormErrorMessage,
3002
+ Input, Textarea, Select, Button, VStack,
3003
+ } from '@chakra-ui/react'
2253
3004
  import { useForm } from 'react-hook-form'
2254
3005
  import { zodResolver } from '@hookform/resolvers/zod'
2255
3006
  import { z } from 'zod'
@@ -2263,29 +3014,40 @@ const schema = z.object({
2263
3014
  type FormData = z.infer<typeof schema>
2264
3015
 
2265
3016
  function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
2266
- const form = useForm<FormData>({
3017
+ const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
2267
3018
  resolver: zodResolver(schema),
2268
3019
  defaultValues: { title: '', description: '', status: 'draft', ...defaultValues },
2269
3020
  })
2270
3021
 
2271
3022
  return (
2272
- <Form form={form} onSubmit={onSubmit}>
2273
- <FormField name="title" label="Title">
2274
- <FormInput placeholder="Enter title" />
2275
- </FormField>
2276
- <FormField name="description" label="Description">
2277
- <FormTextarea rows={4} placeholder="Enter description" />
2278
- </FormField>
2279
- <FormField name="status" label="Status">
2280
- <FormSelect options={[
2281
- { label: 'Draft', value: 'draft' },
2282
- { label: 'Published', value: 'published' },
2283
- ]} />
2284
- </FormField>
2285
- <Button type="submit" disabled={isSubmitting}>
2286
- {isSubmitting ? 'Saving...' : 'Save'}
2287
- </Button>
2288
- </Form>
3023
+ <form onSubmit={handleSubmit(onSubmit)}>
3024
+ <VStack spacing={4} align="stretch">
3025
+ <FormControl isInvalid={!!errors.title} isRequired>
3026
+ <FormLabel>Title</FormLabel>
3027
+ <Input placeholder="Enter title" {...register('title')} />
3028
+ <FormErrorMessage>{errors.title?.message}</FormErrorMessage>
3029
+ </FormControl>
3030
+
3031
+ <FormControl isInvalid={!!errors.description}>
3032
+ <FormLabel>Description</FormLabel>
3033
+ <Textarea rows={4} placeholder="Enter description" {...register('description')} />
3034
+ <FormErrorMessage>{errors.description?.message}</FormErrorMessage>
3035
+ </FormControl>
3036
+
3037
+ <FormControl isInvalid={!!errors.status}>
3038
+ <FormLabel>Status</FormLabel>
3039
+ <Select {...register('status')}>
3040
+ <option value="draft">Draft</option>
3041
+ <option value="published">Published</option>
3042
+ </Select>
3043
+ <FormErrorMessage>{errors.status?.message}</FormErrorMessage>
3044
+ </FormControl>
3045
+
3046
+ <Button type="submit" colorScheme="blue" isLoading={isSubmitting}>
3047
+ Save
3048
+ </Button>
3049
+ </VStack>
3050
+ </form>
2289
3051
  )
2290
3052
  }
2291
3053
  \`\`\`
@@ -2293,12 +3055,13 @@ function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
2293
3055
  ### Example: Detail Page with Edit Dialog
2294
3056
  \`\`\`tsx
2295
3057
  import {
2296
- Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
2297
- Button, Badge, Separator,
2298
- Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter,
2299
- Alert, AlertTitle, AlertDescription,
2300
- } from '@blacksmith-ui/react'
2301
- import { Form, FormField, FormInput, FormTextarea } from '@blacksmith-ui/forms'
3058
+ Card, CardHeader, CardBody, CardFooter,
3059
+ Button, Badge, Divider, Heading, Text,
3060
+ Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalCloseButton,
3061
+ Alert, AlertIcon, AlertTitle, AlertDescription,
3062
+ FormControl, FormLabel, FormErrorMessage,
3063
+ Input, Textarea, Flex, HStack, useDisclosure,
3064
+ } from '@chakra-ui/react'
2302
3065
  import { useForm } from 'react-hook-form'
2303
3066
  import { zodResolver } from '@hookform/resolvers/zod'
2304
3067
  import { z } from 'zod'
@@ -2311,7 +3074,8 @@ const editSchema = z.object({
2311
3074
  })
2312
3075
 
2313
3076
  function ResourceDetailPage({ resource, onUpdate, error }) {
2314
- const form = useForm({
3077
+ const { isOpen, onOpen, onClose } = useDisclosure()
3078
+ const { register, handleSubmit, formState: { errors } } = useForm({
2315
3079
  resolver: zodResolver(editSchema),
2316
3080
  defaultValues: { title: resource.title, description: resource.description },
2317
3081
  })
@@ -2319,52 +3083,62 @@ function ResourceDetailPage({ resource, onUpdate, error }) {
2319
3083
  return (
2320
3084
  <Card>
2321
3085
  <CardHeader>
2322
- <div className="flex items-center justify-between">
2323
- <div>
2324
- <CardTitle>{resource.title}</CardTitle>
2325
- <CardDescription>Created {new Date(resource.created_at).toLocaleDateString()}</CardDescription>
2326
- </div>
2327
- <div className="flex gap-2">
2328
- <Button variant="outline" asChild>
2329
- <Link to="/resources"><ArrowLeft className="mr-2 h-4 w-4" /> Back</Link>
3086
+ <Flex align="center" justify="space-between">
3087
+ <Box>
3088
+ <Heading size="md">{resource.title}</Heading>
3089
+ <Text fontSize="sm" color="gray.500">Created {new Date(resource.created_at).toLocaleDateString()}</Text>
3090
+ </Box>
3091
+ <HStack spacing={2}>
3092
+ <Button variant="outline" as={Link} to="/resources" leftIcon={<ArrowLeft size={16} />}>
3093
+ Back
3094
+ </Button>
3095
+ <Button leftIcon={<Edit size={16} />} colorScheme="blue" onClick={onOpen}>
3096
+ Edit
2330
3097
  </Button>
2331
- <Dialog>
2332
- <DialogTrigger asChild>
2333
- <Button><Edit className="mr-2 h-4 w-4" /> Edit</Button>
2334
- </DialogTrigger>
2335
- <DialogContent>
2336
- <DialogHeader>
2337
- <DialogTitle>Edit Resource</DialogTitle>
2338
- </DialogHeader>
2339
- <Form form={form} onSubmit={onUpdate}>
2340
- <FormField name="title" label="Title">
2341
- <FormInput />
2342
- </FormField>
2343
- <FormField name="description" label="Description">
2344
- <FormTextarea rows={4} />
2345
- </FormField>
2346
- <DialogFooter>
2347
- <Button type="submit">Save Changes</Button>
2348
- </DialogFooter>
2349
- </Form>
2350
- </DialogContent>
2351
- </Dialog>
2352
- </div>
2353
- </div>
3098
+ </HStack>
3099
+ </Flex>
2354
3100
  </CardHeader>
2355
- <Separator />
2356
- <CardContent className="pt-6">
3101
+ <Divider />
3102
+ <CardBody>
2357
3103
  {error && (
2358
- <Alert variant="destructive" className="mb-4">
3104
+ <Alert status="error" mb={4}>
3105
+ <AlertIcon />
2359
3106
  <AlertTitle>Error</AlertTitle>
2360
3107
  <AlertDescription>{error}</AlertDescription>
2361
3108
  </Alert>
2362
3109
  )}
2363
- <p>{resource.description || 'No description provided.'}</p>
2364
- </CardContent>
3110
+ <Text>{resource.description || 'No description provided.'}</Text>
3111
+ </CardBody>
2365
3112
  <CardFooter>
2366
3113
  <Badge>{resource.status}</Badge>
2367
3114
  </CardFooter>
3115
+
3116
+ <Modal isOpen={isOpen} onClose={onClose}>
3117
+ <ModalOverlay />
3118
+ <ModalContent>
3119
+ <ModalHeader>Edit Resource</ModalHeader>
3120
+ <ModalCloseButton />
3121
+ <form onSubmit={handleSubmit(onUpdate)}>
3122
+ <ModalBody>
3123
+ <VStack spacing={4}>
3124
+ <FormControl isInvalid={!!errors.title}>
3125
+ <FormLabel>Title</FormLabel>
3126
+ <Input {...register('title')} />
3127
+ <FormErrorMessage>{errors.title?.message}</FormErrorMessage>
3128
+ </FormControl>
3129
+ <FormControl isInvalid={!!errors.description}>
3130
+ <FormLabel>Description</FormLabel>
3131
+ <Textarea rows={4} {...register('description')} />
3132
+ <FormErrorMessage>{errors.description?.message}</FormErrorMessage>
3133
+ </FormControl>
3134
+ </VStack>
3135
+ </ModalBody>
3136
+ <ModalFooter>
3137
+ <Button type="submit" colorScheme="blue">Save Changes</Button>
3138
+ </ModalFooter>
3139
+ </form>
3140
+ </ModalContent>
3141
+ </Modal>
2368
3142
  </Card>
2369
3143
  )
2370
3144
  }
@@ -2373,232 +3147,222 @@ function ResourceDetailPage({ resource, onUpdate, error }) {
2373
3147
  }
2374
3148
  };
2375
3149
 
2376
- // src/skills/blacksmith-ui-auth.ts
2377
- var blacksmithUiAuthSkill = {
2378
- id: "blacksmith-ui-auth",
2379
- name: "@blacksmith-ui/auth",
2380
- description: "Authentication UI components and hooks for login, registration, and password reset.",
3150
+ // src/skills/chakra-ui-auth.ts
3151
+ init_esm_shims();
3152
+ var chakraUiAuthSkill = {
3153
+ id: "chakra-ui-auth",
3154
+ name: "Custom Auth System",
3155
+ description: "Custom authentication context, hooks, and types for login, registration, and password reset.",
2381
3156
  render(_ctx) {
2382
- return `## @blacksmith-ui/auth \u2014 Authentication UI
3157
+ return `## Custom Auth System \u2014 Authentication Context & Hooks
2383
3158
 
2384
- > **RULE: ALWAYS use these for auth pages.** Do NOT build custom login/register forms.
3159
+ > **RULE: Use the local AuthProvider and useAuth hook for all auth functionality.**
3160
+ > Auth components (LoginForm, RegisterForm, etc.) are custom implementations in \`frontend/src/features/auth/\`.
2385
3161
 
2386
3162
  \`\`\`tsx
2387
- import { AuthProvider, LoginForm, RegisterForm, useAuth } from '@blacksmith-ui/auth'
3163
+ import { AuthProvider } from '@/features/auth/context'
3164
+ import { useAuth } from '@/features/auth/hooks/use-auth'
3165
+ import type { User, AuthState } from '@/features/auth/types'
2388
3166
  \`\`\`
2389
3167
 
2390
- ### Components
3168
+ ### Context & Provider
3169
+
3170
+ - \`AuthProvider\` \u2014 Context provider wrapping the app. Manages auth state, token storage, and session lifecycle.
3171
+ - Place at the app root, inside \`ChakraProvider\`.
3172
+ - Props: \`config?: { adapter?: AuthAdapter }\`
3173
+
3174
+ ### Types
3175
+
3176
+ \`\`\`tsx
3177
+ interface User {
3178
+ id: string
3179
+ email: string
3180
+ displayName?: string
3181
+ avatar?: string
3182
+ }
2391
3183
 
2392
- - \`AuthProvider\` \u2014 Context provider wrapping the app. Props: \`config: { adapter, socialProviders? }\`
2393
- - \`LoginForm\` \u2014 Complete login form with email/password fields, validation, and links
2394
- - Props: \`onSubmit: (data: { email, password }) => void\`, \`onRegisterClick\`, \`onForgotPasswordClick\`, \`error\`, \`loading\`
2395
- - \`RegisterForm\` \u2014 Registration form with email, password, and display name
2396
- - Props: \`onSubmit: (data: { email, password, displayName }) => void\`, \`onLoginClick\`, \`error\`, \`loading\`
2397
- - \`ForgotPasswordForm\` \u2014 Password reset email request
2398
- - Props: \`onSubmit: (data: { email }) => void\`, \`onLoginClick\`, \`error\`, \`loading\`
2399
- - \`ResetPasswordForm\` \u2014 Set new password form
2400
- - Props: \`onSubmit: (data: { password, code }) => void\`, \`code\`, \`onLoginClick\`, \`error\`, \`loading\`
3184
+ interface AuthState {
3185
+ user: User | null
3186
+ loading: boolean
3187
+ error: string | null
3188
+ isAuthenticated: boolean
3189
+ }
3190
+ \`\`\`
2401
3191
 
2402
3192
  ### Hooks
2403
3193
 
2404
- - \`useAuth\` \u2014 Hook for auth state and actions
2405
- - Returns: \`user\`, \`loading\`, \`error\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)\`, \`socialProviders\`
3194
+ - \`useAuth()\` \u2014 Hook for auth state and actions
3195
+ - Returns: \`user\`, \`loading\`, \`error\`, \`isAuthenticated\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)\`
3196
+
3197
+ ### Auth Pages
3198
+
3199
+ Auth pages are custom implementations in \`frontend/src/features/auth/pages/\`:
3200
+
3201
+ - \`LoginPage\` \u2014 Login form with email/password, validation, and navigation links
3202
+ - \`RegisterPage\` \u2014 Registration form with email, password, and display name
3203
+ - \`ForgotPasswordPage\` \u2014 Password reset email request
3204
+ - \`ResetPasswordPage\` \u2014 Set new password form
3205
+
3206
+ Each auth page uses Chakra UI form components (\`FormControl\`, \`FormLabel\`, \`Input\`, \`Button\`, etc.) with React Hook Form + Zod validation.
2406
3207
 
2407
3208
  ### Adapter
2408
3209
 
2409
- - \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter already configured in \`frontend/src/features/auth/adapter.ts\`)
3210
+ - \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter in \`frontend/src/features/auth/adapter.ts\`)
2410
3211
 
2411
3212
  ### Rules
2412
- - NEVER build custom login/register forms. Use \`LoginForm\`, \`RegisterForm\`, etc. from \`@blacksmith-ui/auth\`.
2413
- - NEVER manage auth state manually. Use \`useAuth\` hook.
3213
+ - NEVER manage auth state manually. Use \`useAuth()\` hook.
3214
+ - Auth pages live in \`frontend/src/features/auth/pages/\` and use Chakra UI components.
3215
+ - Use the \`Path\` enum for auth route paths (\`Path.Login\`, \`Path.Register\`, etc.).
2414
3216
  `;
2415
3217
  }
2416
3218
  };
2417
3219
 
2418
3220
  // src/skills/blacksmith-hooks.ts
3221
+ init_esm_shims();
2419
3222
  var blacksmithHooksSkill = {
2420
3223
  id: "blacksmith-hooks",
2421
- name: "@blacksmith-ui/hooks",
2422
- description: "74 production-ready React hooks for state, DOM, timers, async, browser APIs, and layout.",
3224
+ name: "Custom Hooks & Chakra UI Hooks",
3225
+ description: "Custom React hooks (local implementations) and Chakra UI built-in hooks for state, UI, layout, and responsiveness.",
2423
3226
  render(_ctx) {
2424
- return `## @blacksmith-ui/hooks \u2014 React Hooks Library
3227
+ return `## Custom Hooks & Chakra UI Hooks
2425
3228
 
2426
- A collection of 74 production-ready React hooks. SSR-safe, fully typed, zero dependencies, tree-shakeable.
3229
+ A combination of local custom hooks and Chakra UI built-in hooks for common UI patterns.
2427
3230
 
2428
- > **RULE: Use \`@blacksmith-ui/hooks\` instead of writing custom hooks when one exists for that purpose.**
3231
+ > **RULE: Use Chakra UI hooks when available, and local custom hooks for additional utilities.**
2429
3232
  > Before creating a new hook, check if one already exists below.
2430
3233
 
3234
+ ### Chakra UI Built-in Hooks
3235
+
2431
3236
  \`\`\`tsx
2432
- import { useToggle, useLocalStorage, useDebounce, useClickOutside } from '@blacksmith-ui/hooks'
3237
+ import { useColorMode, useDisclosure, useBreakpointValue, useMediaQuery, useToast } from '@chakra-ui/react'
2433
3238
  \`\`\`
2434
3239
 
2435
- ### State & Data
2436
-
2437
3240
  | Hook | Description |
2438
3241
  |------|-------------|
2439
- | \`useToggle\` | Boolean state with \`toggle\`, \`on\`, \`off\` actions |
2440
- | \`useDisclosure\` | Open/close/toggle state for modals, drawers, etc. |
2441
- | \`useCounter\` | Numeric counter with optional min/max clamping |
2442
- | \`useList\` | Array state with push, remove, update, insert, filter, clear |
2443
- | \`useMap\` | Map state with set, remove, clear helpers |
2444
- | \`useSet\` | Set state with add, remove, toggle, has, clear helpers |
2445
- | \`useHistoryState\` | State with undo/redo history |
2446
- | \`useDefault\` | State that falls back to a default when set to null/undefined |
2447
- | \`useQueue\` | FIFO queue data structure |
2448
- | \`useStack\` | LIFO stack data structure |
2449
- | \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
2450
- | \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
2451
- | \`useUncontrolled\` | Controlled/uncontrolled component pattern helper |
3242
+ | \`useColorMode\` | Color mode toggle. Returns \`{ colorMode, toggleColorMode, setColorMode }\` |
3243
+ | \`useColorModeValue\` | Returns a value based on current color mode: \`useColorModeValue(lightValue, darkValue)\` |
3244
+ | \`useDisclosure\` | Open/close/toggle state for modals, drawers, popovers. Returns \`{ isOpen, onOpen, onClose, onToggle }\` |
3245
+ | \`useBreakpointValue\` | Responsive values: \`useBreakpointValue({ base: 1, md: 2, lg: 3 })\` |
3246
+ | \`useMediaQuery\` | CSS media query matching: \`useMediaQuery('(min-width: 768px)')\` |
3247
+ | \`useToast\` | Programmatic toast notifications |
3248
+ | \`useClipboard\` | Copy text to clipboard with status feedback |
3249
+ | \`useBoolean\` | Boolean state with \`on\`, \`off\`, \`toggle\` actions |
3250
+ | \`useOutsideClick\` | Detect clicks outside a ref element |
3251
+ | \`useControllable\` | Controlled/uncontrolled component pattern helper |
3252
+ | \`useMergeRefs\` | Merge multiple refs into one |
3253
+ | \`useTheme\` | Access the current Chakra UI theme object |
3254
+
3255
+ ### Custom Local Hooks
3256
+
3257
+ These are implemented locally in the project (e.g., in \`frontend/src/shared/hooks/\`):
2452
3258
 
2453
- ### Values & Memoization
3259
+ \`\`\`tsx
3260
+ import { useDebounce } from '@/shared/hooks/use-debounce'
3261
+ import { useLocalStorage } from '@/shared/hooks/use-local-storage'
3262
+ \`\`\`
2454
3263
 
2455
3264
  | Hook | Description |
2456
3265
  |------|-------------|
2457
3266
  | \`useDebounce\` | Debounce a value with configurable delay |
2458
3267
  | \`useDebouncedCallback\` | Debounce a callback function |
2459
- | \`useThrottle\` | Throttle a value with configurable interval |
2460
- | \`useThrottledCallback\` | Throttle a callback function |
3268
+ | \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
3269
+ | \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
2461
3270
  | \`usePrevious\` | Track the previous value of a variable |
2462
- | \`useLatest\` | Ref that always points to the latest value |
2463
- | \`useConst\` | Compute a value once and return it on every render |
2464
- | \`useSyncedRef\` | Keep a ref synchronized with the latest value |
2465
-
2466
- ### DOM & Browser
2467
-
2468
- | Hook | Description |
2469
- |------|-------------|
2470
- | \`useClickOutside\` | Detect clicks outside a ref element |
3271
+ | \`useInterval\` | setInterval wrapper with pause support |
3272
+ | \`useTimeout\` | setTimeout wrapper with manual clear |
2471
3273
  | \`useEventListener\` | Attach event listeners to window or elements |
2472
3274
  | \`useElementSize\` | Track element width/height via ResizeObserver |
2473
3275
  | \`useHover\` | Track mouse hover state |
2474
3276
  | \`useKeyPress\` | Listen for a specific key press |
2475
- | \`useKeyCombo\` | Listen for key + modifier combinations |
2476
- | \`useLongPress\` | Detect long press gestures |
2477
- | \`useFullscreen\` | Manage the Fullscreen API |
2478
- | \`useTextSelection\` | Track currently selected text |
2479
- | \`useFocusWithin\` | Track whether focus is inside a container |
2480
- | \`useFocusTrap\` | Trap Tab/Shift+Tab focus within a container |
2481
- | \`useBoundingClientRect\` | Track element bounding rect via ResizeObserver |
2482
- | \`useSwipe\` | Detect touch swipe direction |
2483
- | \`useDrag\` | Track mouse drag with position and delta |
2484
- | \`useElementVisibility\` | Check if an element is in the viewport |
2485
3277
  | \`useScrollPosition\` | Track window scroll position |
2486
3278
  | \`useScrollLock\` | Lock/unlock body scroll |
2487
- | \`useMutationObserver\` | Observe DOM mutations |
2488
- | \`useIntersectionObserver\` | Observe element intersection with viewport |
2489
-
2490
- ### Timers & Lifecycle
2491
-
2492
- | Hook | Description |
2493
- |------|-------------|
2494
- | \`useInterval\` | setInterval wrapper with pause support |
2495
- | \`useTimeout\` | setTimeout wrapper with manual clear |
2496
- | \`useCountdown\` | Countdown timer with start/pause/reset |
2497
- | \`useStopwatch\` | Stopwatch with lap support |
2498
- | \`useIdleTimer\` | Detect user idle time |
2499
- | \`useUpdateEffect\` | useEffect that skips the initial render |
2500
- | \`useIsomorphicLayoutEffect\` | SSR-safe useLayoutEffect |
2501
- | \`useIsMounted\` | Check if component is currently mounted |
2502
- | \`useIsFirstRender\` | Check if this is the first render |
2503
-
2504
- ### Async & Network
2505
-
2506
- | Hook | Description |
2507
- |------|-------------|
2508
- | \`useFetch\` | Declarative data fetching with loading/error states (use for external URLs; use TanStack Query for API calls) |
2509
- | \`useAsync\` | Execute async functions with status tracking |
2510
- | \`useScript\` | Dynamically load external scripts |
2511
- | \`useWebSocket\` | WebSocket connection with auto-reconnect |
2512
- | \`useSSE\` | Server-Sent Events (EventSource) wrapper |
2513
- | \`usePolling\` | Poll an async function at a fixed interval |
2514
- | \`useAbortController\` | Manage AbortController lifecycle |
2515
- | \`useRetry\` | Retry async operations with exponential backoff |
2516
- | \`useSearch\` | Filter arrays with debounced search |
2517
-
2518
- ### Browser APIs
2519
-
2520
- | Hook | Description |
2521
- |------|-------------|
2522
- | \`useMediaQuery\` | Reactive CSS media query matching |
2523
- | \`useColorScheme\` | Detect system color scheme preference |
2524
- | \`useCopyToClipboard\` | Copy text to clipboard with status feedback |
2525
3279
  | \`useOnline\` | Track network connectivity |
2526
3280
  | \`useWindowSize\` | Track window dimensions |
2527
3281
  | \`usePageVisibility\` | Detect page visibility state |
2528
- | \`usePageLeave\` | Detect when the user leaves the page |
2529
- | \`useFavicon\` | Dynamically change the favicon |
2530
- | \`useReducedMotion\` | Respect prefers-reduced-motion |
2531
- | \`useBreakpoint\` | Responsive breakpoint detection |
2532
- | \`useIsClient\` | SSR-safe client-side detection |
2533
-
2534
- ### Layout & UI
2535
-
2536
- | Hook | Description |
2537
- |------|-------------|
2538
- | \`useStickyHeader\` | Detect when header should be sticky |
2539
- | \`useVirtualList\` | Virtualized list rendering for large datasets |
3282
+ | \`useIsMounted\` | Check if component is currently mounted |
3283
+ | \`useIsFirstRender\` | Check if this is the first render |
3284
+ | \`useUpdateEffect\` | useEffect that skips the initial render |
3285
+ | \`useIntersectionObserver\` | Observe element intersection with viewport |
2540
3286
  | \`useInfiniteScroll\` | Infinite scroll with threshold detection |
2541
- | \`useCollapse\` | Collapse/expand animation with prop getters |
2542
- | \`useSteps\` | Multi-step flow navigation |
2543
3287
 
2544
3288
  ### Common Patterns
2545
3289
 
2546
- **Modal with click-outside dismiss:**
3290
+ **Modal with Chakra disclosure:**
2547
3291
  \`\`\`tsx
2548
- import { useDisclosure, useClickOutside } from '@blacksmith-ui/hooks'
3292
+ import { useDisclosure } from '@chakra-ui/react'
3293
+ import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Button } from '@chakra-ui/react'
2549
3294
 
2550
3295
  function MyComponent() {
2551
- const [opened, { open, close }] = useDisclosure(false)
2552
- const ref = useClickOutside<HTMLDivElement>(close)
3296
+ const { isOpen, onOpen, onClose } = useDisclosure()
2553
3297
 
2554
3298
  return (
2555
3299
  <>
2556
- <Button onClick={open}>Open</Button>
2557
- {opened && <div ref={ref}>Modal content</div>}
3300
+ <Button onClick={onOpen}>Open</Button>
3301
+ <Modal isOpen={isOpen} onClose={onClose}>
3302
+ <ModalOverlay />
3303
+ <ModalContent>
3304
+ <ModalHeader>Modal Title</ModalHeader>
3305
+ <ModalCloseButton />
3306
+ <ModalBody>Modal content here</ModalBody>
3307
+ </ModalContent>
3308
+ </Modal>
2558
3309
  </>
2559
3310
  )
2560
3311
  }
2561
3312
  \`\`\`
2562
3313
 
2563
- **Debounced search:**
3314
+ **Debounced search (custom hook):**
2564
3315
  \`\`\`tsx
2565
- import { useDebounce, useSearch } from '@blacksmith-ui/hooks'
3316
+ import { useDebounce } from '@/shared/hooks/use-debounce'
3317
+ import { Input } from '@chakra-ui/react'
2566
3318
 
2567
3319
  function SearchPage({ items }) {
2568
3320
  const [query, setQuery] = useState('')
2569
3321
  const debouncedQuery = useDebounce(query, 300)
2570
- const results = useSearch(items, debouncedQuery, ['title', 'description'])
3322
+ // Use debouncedQuery for API calls or filtering
2571
3323
 
2572
3324
  return (
2573
- <>
2574
- <Input value={query} onChange={(e) => setQuery(e.target.value)} />
2575
- {results.map(item => <div key={item.id}>{item.title}</div>)}
2576
- </>
3325
+ <Input
3326
+ value={query}
3327
+ onChange={(e) => setQuery(e.target.value)}
3328
+ placeholder="Search..."
3329
+ />
2577
3330
  )
2578
3331
  }
2579
3332
  \`\`\`
2580
3333
 
2581
- **Persisted state with undo:**
3334
+ **Responsive layout with Chakra hook:**
2582
3335
  \`\`\`tsx
2583
- import { useLocalStorage, useHistoryState } from '@blacksmith-ui/hooks'
3336
+ import { useBreakpointValue } from '@chakra-ui/react'
2584
3337
 
2585
- function Editor() {
2586
- const [saved, setSaved] = useLocalStorage('draft', '')
2587
- const [content, { set, undo, redo, canUndo, canRedo }] = useHistoryState(saved)
3338
+ function Layout({ children }) {
3339
+ const columns = useBreakpointValue({ base: 1, md: 2, lg: 3 })
3340
+ const isMobile = useBreakpointValue({ base: true, md: false })
2588
3341
 
2589
- const handleSave = () => setSaved(content)
3342
+ return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
2590
3343
  }
2591
3344
  \`\`\`
2592
3345
 
2593
- **Responsive layout:**
3346
+ **Color mode toggle:**
2594
3347
  \`\`\`tsx
2595
- import { useBreakpoint, useWindowSize } from '@blacksmith-ui/hooks'
3348
+ import { useColorMode, useColorModeValue, IconButton } from '@chakra-ui/react'
3349
+ import { Sun, Moon } from 'lucide-react'
2596
3350
 
2597
- function Layout({ children }) {
2598
- const breakpoint = useBreakpoint({ sm: 640, md: 768, lg: 1024 })
2599
- const isMobile = breakpoint === 'sm'
3351
+ function ColorModeToggle() {
3352
+ const { colorMode, toggleColorMode } = useColorMode()
3353
+ const icon = colorMode === 'light' ? <Moon size={16} /> : <Sun size={16} />
2600
3354
 
2601
- return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
3355
+ return <IconButton aria-label="Toggle color mode" icon={icon} onClick={toggleColorMode} variant="ghost" />
3356
+ }
3357
+ \`\`\`
3358
+
3359
+ **Persisted state with local storage:**
3360
+ \`\`\`tsx
3361
+ import { useLocalStorage } from '@/shared/hooks/use-local-storage'
3362
+
3363
+ function Editor() {
3364
+ const [saved, setSaved] = useLocalStorage('draft', '')
3365
+ // saved persists across page refreshes
2602
3366
  }
2603
3367
  \`\`\`
2604
3368
  `;
@@ -2606,6 +3370,7 @@ function Layout({ children }) {
2606
3370
  };
2607
3371
 
2608
3372
  // src/skills/blacksmith-cli.ts
3373
+ init_esm_shims();
2609
3374
  var blacksmithCliSkill = {
2610
3375
  id: "blacksmith-cli",
2611
3376
  name: "Blacksmith CLI",
@@ -2697,21 +3462,22 @@ blacksmith init my-app -b 9000 -f 3000 --ai
2697
3462
  | \`-b, --backend-port <port>\` | Django port (default: 8000) |
2698
3463
  | \`-f, --frontend-port <port>\` | Vite port (default: 5173) |
2699
3464
  | \`--ai\` | Generate CLAUDE.md with project skills |
2700
- | \`--no-blacksmith-ui-skill\` | Exclude blacksmith-ui skill from CLAUDE.md |
3465
+ | \`--no-chakra-ui-skill\` | Exclude Chakra UI skill from CLAUDE.md |
2701
3466
  `;
2702
3467
  }
2703
3468
  };
2704
3469
 
2705
3470
  // src/skills/ui-design.ts
3471
+ init_esm_shims();
2706
3472
  var uiDesignSkill = {
2707
3473
  id: "ui-design",
2708
3474
  name: "UI/UX Design System",
2709
- description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the BlacksmithUI design language.",
3475
+ description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the Chakra UI design language.",
2710
3476
  render(_ctx) {
2711
3477
  return `## UI/UX Design System \u2014 Modern Flat Design
2712
3478
 
2713
3479
  > **Design philosophy: Clean, flat, content-first.**
2714
- > 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.
3480
+ > Chakra UI follows a clean design language similar to Anthropic, Apple, Linear, Vercel, and OpenAI \u2014 minimal chrome, generous whitespace, subtle depth, and purposeful motion. Every UI you build must conform to this standard.
2715
3481
 
2716
3482
  ### Core Principles
2717
3483
 
@@ -2723,7 +3489,7 @@ var uiDesignSkill = {
2723
3489
 
2724
3490
  ### Spacing System
2725
3491
 
2726
- Use Tailwind's spacing scale consistently. Do NOT use arbitrary values (\`p-[13px]\`) \u2014 stick to the system.
3492
+ Use Chakra UI's spacing scale consistently. Chakra uses a numeric spacing scale (1 = 4px, 2 = 8px, etc.).
2727
3493
 
2728
3494
  | Scale | Value | Use for |
2729
3495
  |-------|-------|---------|
@@ -2735,57 +3501,51 @@ Use Tailwind's spacing scale consistently. Do NOT use arbitrary values (\`p-[13p
2735
3501
  | \`16\`\u2013\`20\` | 64\u201380px | Page-level vertical padding (hero, landing sections) |
2736
3502
 
2737
3503
  **Rules:**
2738
- - Use \`gap\` (via \`Flex\`, \`Stack\`, \`Grid\`) for spacing between siblings \u2014 not margin on individual items
2739
- - Use \`Stack gap={...}\` for vertical rhythm within a section
2740
- - Page content padding: \`px-4 sm:px-6 lg:px-8\` (use \`Container\` which handles this)
2741
- - Card body padding: \`p-6\` standard, \`p-4\` for compact cards
2742
- - Never mix spacing approaches in the same context \u2014 pick gap OR margin, not both
3504
+ - Use \`spacing\` (via \`VStack\`, \`HStack\`, \`SimpleGrid\`) for spacing between siblings \u2014 not margin on individual items
3505
+ - Use \`VStack spacing={...}\` for vertical rhythm within a section
3506
+ - Page content padding: use \`Container\` which handles responsive horizontal padding
3507
+ - Card body padding: \`p={6}\` standard, \`p={4}\` for compact cards
3508
+ - Never mix spacing approaches in the same context \u2014 pick spacing OR margin, not both
2743
3509
 
2744
3510
  ### Typography
2745
3511
 
2746
- Use \`Typography\` and \`Text\` components from \`@blacksmith-ui/react\`. Do NOT style raw HTML headings.
3512
+ Use \`Heading\` and \`Text\` components from \`@chakra-ui/react\`. Do NOT style raw HTML headings.
2747
3513
 
2748
3514
  **Hierarchy:**
2749
3515
  | Level | Component | Use for |
2750
3516
  |-------|-----------|---------|
2751
- | Page title | \`<Typography variant="h1">\` | One per page. The main heading. |
2752
- | Section title | \`<Typography variant="h2">\` | Major sections within a page |
2753
- | Sub-section | \`<Typography variant="h3">\` | Groups within a section |
2754
- | Card title | \`<Typography variant="h4">\` or \`CardTitle\` | Card headings |
3517
+ | Page title | \`<Heading as="h1" size="2xl">\` | One per page. The main heading. |
3518
+ | Section title | \`<Heading as="h2" size="xl">\` | Major sections within a page |
3519
+ | Sub-section | \`<Heading as="h3" size="lg">\` | Groups within a section |
3520
+ | Card title | \`<Heading as="h4" size="md">\` | Card headings |
2755
3521
  | Body | \`<Text>\` | Paragraphs, descriptions |
2756
- | Caption/label | \`<Text size="sm" color="muted">\` | Secondary info, metadata, timestamps |
2757
- | Overline | \`<Text size="xs" weight="medium" className="uppercase tracking-wide">\` | Category labels, section overlines |
3522
+ | Caption/label | \`<Text fontSize="sm" color="gray.500">\` | Secondary info, metadata, timestamps |
3523
+ | Overline | \`<Text fontSize="xs" fontWeight="medium" textTransform="uppercase" letterSpacing="wide">\` | Category labels, section overlines |
2758
3524
 
2759
3525
  **Rules:**
2760
3526
  - One \`h1\` per page \u2014 it's the page title
2761
- - Headings should never skip levels (h1 \u2192 h3 without h2)
2762
- - Body text: \`text-sm\` (14px) for dense UIs (tables, sidebars), \`text-base\` (16px) for reading content
2763
- - Line height: use Tailwind defaults (\`leading-relaxed\` for body copy, \`leading-tight\` for headings)
2764
- - Max reading width: \`max-w-prose\` (~65ch) for long-form text. Never let paragraphs stretch full-width
2765
- - Use \`text-muted-foreground\` for secondary text, never gray hardcoded values
2766
- - Font weight: \`font-medium\` (500) for labels and emphasis, \`font-semibold\` (600) for headings, \`font-bold\` (700) sparingly
3527
+ - Headings should never skip levels (h1 -> h3 without h2)
3528
+ - Body text: \`fontSize="sm"\` (14px) for dense UIs (tables, sidebars), \`fontSize="md"\` (16px) for reading content
3529
+ - Max reading width: \`maxW="prose"\` (~65ch) for long-form text. Never let paragraphs stretch full-width
3530
+ - Use \`color="gray.500"\` or theme-aware \`useColorModeValue('gray.600', 'gray.400')\` for secondary text
3531
+ - Font weight: \`fontWeight="medium"\` (500) for labels, \`fontWeight="semibold"\` (600) for headings, \`fontWeight="bold"\` (700) sparingly
2767
3532
 
2768
3533
  ### Color
2769
3534
 
2770
- Use design tokens (CSS variables), never hardcoded colors.
3535
+ Use Chakra UI's theme-aware color tokens, never hardcoded colors.
2771
3536
 
2772
- **Semantic palette:**
3537
+ **Semantic palette (via colorScheme and theme tokens):**
2773
3538
  | Token | Usage |
2774
3539
  |-------|-------|
2775
- | \`primary\` | Primary actions (buttons, links, active states) |
2776
- | \`secondary\` | Secondary actions, subtle backgrounds |
2777
- | \`destructive\` | Delete, error, danger states |
2778
- | \`muted\` | Backgrounds for subtle sections, disabled states |
2779
- | \`accent\` | Highlights, hover states, focus rings |
2780
- | \`foreground\` | Primary text |
2781
- | \`muted-foreground\` | Secondary/helper text |
2782
- | \`border\` | Borders, dividers |
2783
- | \`card\` | Card backgrounds |
2784
- | \`background\` | Page background |
3540
+ | \`blue\` (colorScheme) | Primary actions (buttons, links, active states) |
3541
+ | \`gray\` (colorScheme) | Secondary actions, subtle backgrounds |
3542
+ | \`red\` (colorScheme) | Delete, error, danger states |
3543
+ | \`green\` (colorScheme) | Success states |
3544
+ | \`yellow\` / \`orange\` | Warning states |
2785
3545
 
2786
3546
  **Rules:**
2787
- - 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.
2788
- - Status colors: use \`Badge\` variants (\`default\`, \`secondary\`, \`destructive\`, \`outline\`) \u2014 don't hand-roll colored pills.
3547
+ - Use \`useColorModeValue()\` for any colors that need to adapt between light and dark mode
3548
+ - Status colors: use \`Badge\` with \`colorScheme\` (\`green\`, \`red\`, \`blue\`, \`gray\`) \u2014 don't hand-roll colored pills.
2789
3549
  - Maximum 2\u20133 colors visible at any time (primary + foreground + muted). Colorful UIs feel noisy.
2790
3550
  - Every UI must render correctly in both light and dark mode. See the Dark Mode section below for the full rules.
2791
3551
 
@@ -2794,66 +3554,66 @@ Use design tokens (CSS variables), never hardcoded colors.
2794
3554
  **Page layout:**
2795
3555
  \`\`\`tsx
2796
3556
  <Box as="main">
2797
- <Container>
2798
- <Stack gap={8}>
3557
+ <Container maxW="container.xl">
3558
+ <VStack spacing={8} align="stretch">
2799
3559
  {/* Page header */}
2800
- <Flex align="center" justify="between">
2801
- <Stack gap={1}>
2802
- <Typography variant="h1">Page Title</Typography>
2803
- <Text color="muted">Brief description of this page</Text>
2804
- </Stack>
2805
- <Button>Primary Action</Button>
3560
+ <Flex align="center" justify="space-between">
3561
+ <VStack spacing={1} align="start">
3562
+ <Heading as="h1" size="2xl">Page Title</Heading>
3563
+ <Text color="gray.500">Brief description of this page</Text>
3564
+ </VStack>
3565
+ <Button colorScheme="blue">Primary Action</Button>
2806
3566
  </Flex>
2807
3567
 
2808
3568
  {/* Page content sections */}
2809
- <Stack gap={6}>
3569
+ <VStack spacing={6} align="stretch">
2810
3570
  {/* ... */}
2811
- </Stack>
2812
- </Stack>
3571
+ </VStack>
3572
+ </VStack>
2813
3573
  </Container>
2814
3574
  </Box>
2815
3575
  \`\`\`
2816
3576
 
2817
3577
  **Card-based content:**
2818
3578
  \`\`\`tsx
2819
- <Grid columns={{ base: 1, md: 2, lg: 3 }} gap={6}>
3579
+ <SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
2820
3580
  {items.map((item) => (
2821
3581
  <Card key={item.id}>
2822
3582
  <CardHeader>
2823
- <CardTitle>{item.title}</CardTitle>
2824
- <CardDescription>{item.description}</CardDescription>
3583
+ <Heading size="md">{item.title}</Heading>
3584
+ <Text fontSize="sm" color="gray.500">{item.description}</Text>
2825
3585
  </CardHeader>
2826
- <CardContent>
3586
+ <CardBody>
2827
3587
  {/* Content */}
2828
- </CardContent>
3588
+ </CardBody>
2829
3589
  </Card>
2830
3590
  ))}
2831
- </Grid>
3591
+ </SimpleGrid>
2832
3592
  \`\`\`
2833
3593
 
2834
3594
  **Sidebar + main content:**
2835
3595
  \`\`\`tsx
2836
- <Flex className="min-h-screen">
2837
- <Sidebar>{/* Nav items */}</Sidebar>
2838
- <Box as="main" className="flex-1">
2839
- <Container>{/* Page content */}</Container>
3596
+ <Flex minH="100vh">
3597
+ <Box as="aside" w="250px">{/* Nav items */}</Box>
3598
+ <Box as="main" flex={1}>
3599
+ <Container maxW="container.xl">{/* Page content */}</Container>
2840
3600
  </Box>
2841
3601
  </Flex>
2842
3602
  \`\`\`
2843
3603
 
2844
3604
  **Section with centered content (landing pages):**
2845
3605
  \`\`\`tsx
2846
- <Box as="section" className="py-16 sm:py-20">
2847
- <Container>
2848
- <Stack gap={4} align="center" className="text-center">
2849
- <Typography variant="h2">Section Title</Typography>
2850
- <Text color="muted" className="max-w-2xl">
3606
+ <Box as="section" py={{ base: 16, sm: 20 }}>
3607
+ <Container maxW="container.xl">
3608
+ <VStack spacing={4} align="center" textAlign="center">
3609
+ <Heading as="h2" size="xl">Section Title</Heading>
3610
+ <Text color="gray.500" maxW="2xl">
2851
3611
  A concise description that explains the value proposition.
2852
3612
  </Text>
2853
- </Stack>
2854
- <Grid columns={{ base: 1, md: 3 }} gap={8} className="mt-12">
3613
+ </VStack>
3614
+ <SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} mt={12}>
2855
3615
  {/* Feature cards or content */}
2856
- </Grid>
3616
+ </SimpleGrid>
2857
3617
  </Container>
2858
3618
  </Box>
2859
3619
  \`\`\`
@@ -2862,15 +3622,15 @@ Use design tokens (CSS variables), never hardcoded colors.
2862
3622
 
2863
3623
  **Empty states:**
2864
3624
  \`\`\`tsx
2865
- // GOOD \u2014 uses EmptyState component
2866
- <EmptyState
2867
- icon={Inbox}
2868
- title="No messages yet"
2869
- description="Messages from your team will appear here."
2870
- action={<Button>Send a message</Button>}
2871
- />
2872
-
2873
- // BAD \u2014 hand-rolled empty state
3625
+ // GOOD \u2014 uses a well-structured empty state with Chakra components
3626
+ <VStack spacing={4} align="center" py={12} textAlign="center">
3627
+ <Icon as={Inbox} boxSize={12} color="gray.400" />
3628
+ <Heading size="md">No messages yet</Heading>
3629
+ <Text color="gray.500">Messages from your team will appear here.</Text>
3630
+ <Button colorScheme="blue">Send a message</Button>
3631
+ </VStack>
3632
+
3633
+ // BAD \u2014 hand-rolled empty state with raw HTML
2874
3634
  <div className="flex flex-col items-center justify-center py-12 text-center">
2875
3635
  <Inbox className="h-12 w-12 text-gray-400 mb-4" />
2876
3636
  <h3 className="text-lg font-medium">No messages yet</h3>
@@ -2880,33 +3640,33 @@ Use design tokens (CSS variables), never hardcoded colors.
2880
3640
 
2881
3641
  **Stats/metrics:**
2882
3642
  \`\`\`tsx
2883
- // GOOD \u2014 uses StatCard
2884
- <Grid columns={{ base: 1, sm: 2, lg: 4 }} gap={4}>
2885
- <StatCard label="Total Users" value="2,847" trend="+12%" />
2886
- <StatCard label="Revenue" value="$48,290" trend="+8%" />
2887
- </Grid>
2888
-
2889
- // BAD \u2014 hand-rolled stat cards
2890
- <div className="grid grid-cols-4 gap-4">
2891
- <div className="bg-white rounded-lg p-6 shadow">
2892
- <p className="text-sm text-gray-500">Total Users</p>
2893
- <p className="text-2xl font-bold">2,847</p>
2894
- </div>
2895
- </div>
3643
+ // GOOD \u2014 uses Chakra Stat component
3644
+ <SimpleGrid columns={{ base: 1, sm: 2, lg: 4 }} spacing={4}>
3645
+ <Stat>
3646
+ <StatLabel>Total Users</StatLabel>
3647
+ <StatNumber>2,847</StatNumber>
3648
+ <StatHelpText><StatArrow type="increase" />12%</StatHelpText>
3649
+ </Stat>
3650
+ <Stat>
3651
+ <StatLabel>Revenue</StatLabel>
3652
+ <StatNumber>$48,290</StatNumber>
3653
+ <StatHelpText><StatArrow type="increase" />8%</StatHelpText>
3654
+ </Stat>
3655
+ </SimpleGrid>
2896
3656
  \`\`\`
2897
3657
 
2898
3658
  **Loading states:**
2899
3659
  \`\`\`tsx
2900
3660
  // GOOD \u2014 Skeleton matches the layout structure
2901
- <Stack gap={4}>
2902
- <Skeleton className="h-8 w-48" /> {/* Title */}
2903
- <Skeleton className="h-4 w-96" /> {/* Description */}
2904
- <Grid columns={3} gap={4}>
3661
+ <VStack spacing={4} align="stretch">
3662
+ <Skeleton h="32px" w="200px" />
3663
+ <SkeletonText noOfLines={2} />
3664
+ <SimpleGrid columns={3} spacing={4}>
2905
3665
  {Array.from({ length: 3 }).map((_, i) => (
2906
- <Skeleton key={i} className="h-32" />
3666
+ <Skeleton key={i} h="128px" />
2907
3667
  ))}
2908
- </Grid>
2909
- </Stack>
3668
+ </SimpleGrid>
3669
+ </VStack>
2910
3670
 
2911
3671
  // BAD \u2014 generic spinner with no layout hint
2912
3672
  <div className="flex justify-center py-12">
@@ -2918,87 +3678,52 @@ Use design tokens (CSS variables), never hardcoded colors.
2918
3678
 
2919
3679
  > **CRITICAL: Every screen, component, and custom style MUST look correct in both light and dark mode. No exceptions.**
2920
3680
 
2921
- 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.
3681
+ Chakra UI supports color mode via \`useColorMode()\` and \`useColorModeValue()\`. All built-in components automatically adapt.
2922
3682
 
2923
3683
  **Rules:**
2924
- - 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\`.
2925
- - NEVER use \`bg-white\` or \`bg-black\`. Use \`bg-background\` (page), \`bg-card\` (elevated surfaces), \`bg-muted\` (subtle sections).
2926
- - NEVER use \`text-black\` or \`text-white\`. Use \`text-foreground\` (primary text), \`text-muted-foreground\` (secondary), \`text-primary-foreground\` (text on primary-colored backgrounds).
2927
- - 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.
2928
- - NEVER use opacity-based overlays with hardcoded colors (\`bg-black/50\`). Use \`bg-background/80\` or let overlay components (\`Dialog\`, \`Sheet\`) handle it.
2929
- - SVG fills and strokes: use \`currentColor\` or \`fill-foreground\` / \`stroke-border\` \u2014 never \`fill-black\` or \`stroke-gray-300\`.
2930
- - 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.
2931
-
2932
- **Safe color tokens (always use these):**
2933
- | Need | Light mode maps to | Dark mode maps to | Use |
2934
- |------|----|----|-----|
2935
- | Page background | white/light gray | near-black | \`bg-background\` |
2936
- | Card/surface | white | dark gray | \`bg-card\` |
2937
- | Subtle background | light gray | darker gray | \`bg-muted\` |
2938
- | Primary text | near-black | near-white | \`text-foreground\` |
2939
- | Secondary text | medium gray | lighter gray | \`text-muted-foreground\` |
2940
- | Borders | light gray | dark gray | \`border-border\` |
2941
- | Input borders | light gray | dark gray | \`border-input\` |
2942
- | Focus ring | brand color | brand color | \`ring-ring\` |
2943
- | Primary action | brand color | brand color | \`bg-primary text-primary-foreground\` |
2944
- | Destructive | red | red | \`bg-destructive text-destructive-foreground\` |
2945
-
2946
- **Testing checklist (mental model):**
2947
- Before considering any UI complete, verify these in your head:
2948
- 1. Does every text element use \`foreground\`, \`muted-foreground\`, or \`*-foreground\` tokens?
2949
- 2. Does every background use \`background\`, \`card\`, \`muted\`, or \`primary\`/\`secondary\`/\`accent\` tokens?
2950
- 3. Does every border use \`border\`, \`input\`, or \`ring\` tokens?
2951
- 4. Are there ANY hex values, rgb values, or Tailwind color names (gray, slate, blue, etc.) in the code? If yes, replace them.
2952
- 5. Do hover/focus/active states also use semantic tokens? (\`hover:bg-muted\` not \`hover:bg-gray-100\`)
3684
+ - Use \`useColorModeValue()\` for any custom colors that need to differ between modes
3685
+ - NEVER hardcode colors that only work in one mode. Use theme tokens or \`useColorModeValue()\`.
3686
+ - NEVER use \`bg="white"\` or \`bg="black"\`. Use \`bg={useColorModeValue('white', 'gray.800')}\` or Chakra semantic tokens.
3687
+ - All Chakra UI components automatically adapt to color mode \u2014 leverage this.
2953
3688
 
2954
3689
  ### Interactions & Feedback
2955
3690
 
2956
- - **Hover states**: Subtle background change (\`hover:bg-muted\`) \u2014 not color shifts or scale transforms
2957
- - **Focus**: Use focus-visible ring (\`focus-visible:ring-2 ring-ring\`). BlacksmithUI components handle this automatically
2958
- - **Transitions**: \`transition-colors duration-150\` for color changes. No bounces, no springs, no dramatic animations
2959
- - **Click feedback**: Use \`active:scale-[0.98]\` only on buttons and interactive cards, never on text or static elements
2960
- - **Loading feedback**: Show \`Spinner\` on buttons during async actions. Use \`Skeleton\` for content areas. Never leave the user without feedback during loading
3691
+ - **Hover states**: Subtle background change \u2014 Chakra components handle this automatically
3692
+ - **Focus**: Chakra components include accessible focus rings by default
3693
+ - **Loading feedback**: Show \`Spinner\` on buttons via \`isLoading\` prop. Use \`Skeleton\` for content areas. Never leave the user without feedback during loading
2961
3694
  - **Success/error feedback**: Use \`useToast()\` for transient confirmations. Use \`Alert\` for persistent messages. Never use \`window.alert()\`
2962
3695
  - **Confirmation before destructive actions**: Always use \`AlertDialog\` for delete/remove actions. Never delete on single click
2963
3696
 
2964
3697
  ### Responsive Design
2965
3698
 
2966
- - **Mobile-first**: Write base styles for mobile, add \`sm:\`/\`md:\`/\`lg:\` for larger screens
2967
- - **Breakpoints**: \`sm\` (640px), \`md\` (768px), \`lg\` (1024px), \`xl\` (1280px)
2968
- - **Grid collapse**: \`Grid columns={{ base: 1, md: 2, lg: 3 }}\` \u2014 single column on mobile, expand on larger screens
2969
- - **Hide/show**: Use \`hidden md:block\` / \`md:hidden\` to toggle elements across breakpoints
2970
- - **Touch targets**: Minimum 44\xD744px for interactive elements on mobile. Use \`Button size="lg"\` and adequate padding
2971
- - **Stack on mobile, row on desktop**: Use \`Flex direction={{ base: 'column', md: 'row' }}\` or \`Stack\` that switches direction
3699
+ - **Mobile-first**: Chakra's responsive props use mobile-first breakpoints
3700
+ - **Breakpoints**: \`sm\` (480px), \`md\` (768px), \`lg\` (992px), \`xl\` (1280px), \`2xl\` (1536px)
3701
+ - **Responsive props**: \`columns={{ base: 1, md: 2, lg: 3 }}\` \u2014 single column on mobile, expand on larger screens
3702
+ - **Hide/show**: Use Chakra's \`Show\` and \`Hide\` components or \`display={{ base: 'none', md: 'block' }}\`
3703
+ - **Touch targets**: Minimum 44x44px for interactive elements on mobile. Use \`Button size="lg"\` and adequate padding
3704
+ - **Stack direction**: Use \`Stack direction={{ base: 'column', md: 'row' }}\` for responsive stacking
2972
3705
  - **Container**: Always wrap page content in \`<Container>\` \u2014 it handles responsive horizontal padding
2973
3706
 
2974
3707
  ### Anti-Patterns \u2014 NEVER Do These
2975
3708
 
2976
3709
  | Anti-pattern | What to do instead |
2977
3710
  |---|---|
2978
- | Hardcoded colors (\`text-gray-500\`, \`bg-blue-600\`) | Use semantic tokens (\`text-muted-foreground\`, \`bg-primary\`) |
2979
- | Heavy box shadows (\`shadow-xl\`, \`shadow-2xl\`) | Use \`shadow-sm\` on cards, \`shadow-md\` on elevated overlays only |
2980
- | Rounded pill shapes (\`rounded-full\`) on cards/containers | Use \`rounded-lg\` or \`rounded-md\` (controlled by \`--radius\`) |
2981
- | Gradient backgrounds on surfaces | Use solid \`bg-card\` or \`bg-background\` |
2982
- | Decorative borders (\`border-l-4 border-blue-500\`) | Use \`Divider\` or \`border-border\` |
2983
- | Custom scrollbars with CSS hacks | Use \`ScrollArea\` |
2984
- | Animated entrances (fade-in, slide-up on mount) | Content should appear instantly. Only animate user-triggered changes |
2985
- | Centering with \`absolute inset-0 flex items-center\` | Use \`Flex align="center" justify="center"\` |
2986
- | Using \`<br />\` for spacing | Use \`Stack gap={...}\` or margin utilities |
2987
- | Multiple font sizes in close proximity | Keep nearby text within 1\u20132 size steps |
2988
- | Dense walls of text | Break into sections with headings, cards, or spacing |
2989
- | Colored backgrounds on every section | Use \`bg-background\` as default, \`bg-muted\` sparingly for contrast |
2990
- | Over-using badges/tags on everything | Badges are for status and categories, not decoration |
2991
- | Inline styles (\`style={{ ... }}\`) | Use Tailwind classes via \`className\` |
2992
- | \`bg-white\` / \`bg-black\` / \`bg-slate-*\` | Use \`bg-background\`, \`bg-card\`, \`bg-muted\` |
2993
- | \`text-black\` / \`text-white\` / \`text-gray-*\` | Use \`text-foreground\`, \`text-muted-foreground\` |
2994
- | \`border-gray-*\` / \`border-slate-*\` | Use \`border-border\`, \`border-input\` |
2995
- | Hex/rgb values in className or style | Use CSS variable tokens exclusively |
2996
- | UI that only looks right in light mode | Always verify both modes \u2014 use semantic tokens throughout |
3711
+ | Raw \`<div>\` with flex/grid classes | Use \`Flex\`, \`VStack\`, \`HStack\`, \`SimpleGrid\` |
3712
+ | Raw \`<h1>\`-\`<h6>\` tags | Use \`Heading\` with \`as\` and \`size\` props |
3713
+ | Raw \`<p>\` tags | Use \`Text\` |
3714
+ | Heavy box shadows | Use Chakra's built-in shadow prop: \`shadow="sm"\`, \`shadow="md"\` |
3715
+ | Gradient backgrounds on surfaces | Use solid backgrounds |
3716
+ | Custom scrollbar CSS hacks | Use Chakra's styling system |
3717
+ | Animated entrances (fade-in, slide-up) | Content should appear instantly. Only animate user-triggered changes |
3718
+ | Using \`<br />\` for spacing | Use \`VStack spacing={...}\` or Chakra spacing props |
3719
+ | Inline styles (\`style={{ ... }}\`) | Use Chakra style props (\`p\`, \`m\`, \`bg\`, \`color\`, etc.) |
3720
+ | Hardcoded color values | Use theme tokens and \`useColorModeValue()\` |
2997
3721
  `;
2998
3722
  }
2999
3723
  };
3000
3724
 
3001
3725
  // src/skills/programming-paradigms.ts
3726
+ init_esm_shims();
3002
3727
  var programmingParadigmsSkill = {
3003
3728
  id: "programming-paradigms",
3004
3729
  name: "Programming Paradigms",
@@ -3427,7 +4152,249 @@ class OrderService:
3427
4152
  }
3428
4153
  };
3429
4154
 
4155
+ // src/skills/frontend-testing.ts
4156
+ init_esm_shims();
4157
+ var frontendTestingSkill = {
4158
+ id: "frontend-testing",
4159
+ name: "Frontend Testing Conventions",
4160
+ description: "Test infrastructure, file placement, test utilities, and rules for when and how to write frontend tests.",
4161
+ render(_ctx) {
4162
+ return `## Frontend Testing Conventions
4163
+
4164
+ ### Stack
4165
+ - **Vitest** \u2014 test runner (configured in \`vite.config.ts\`)
4166
+ - **jsdom** \u2014 browser environment
4167
+ - **React Testing Library** \u2014 component rendering and queries
4168
+ - **\`@testing-library/user-event\`** \u2014 user interaction simulation
4169
+ - **\`@testing-library/jest-dom\`** \u2014 DOM assertion matchers (e.g. \`toBeInTheDocument\`)
4170
+
4171
+ ### Running Tests
4172
+ - Run all tests: \`cd frontend && npm test\`
4173
+ - Watch mode: \`cd frontend && npm run test:watch\`
4174
+ - Run a specific file: \`cd frontend && npx vitest run src/pages/home/__tests__/home.spec.tsx\`
4175
+ - Coverage: \`cd frontend && npm run test:coverage\`
4176
+
4177
+ ### File Placement \u2014 Tests Live Next to the Code
4178
+
4179
+ > **RULE: Every test file goes in a \`__tests__/\` folder co-located with the code it tests. Never put tests in a top-level \`tests/\` directory.**
4180
+
4181
+ \`\`\`
4182
+ pages/customers/
4183
+ \u251C\u2500\u2500 customers-page.tsx
4184
+ \u251C\u2500\u2500 customer-detail-page.tsx
4185
+ \u251C\u2500\u2500 __tests__/ # Page integration tests
4186
+ \u2502 \u251C\u2500\u2500 customers-page.spec.tsx
4187
+ \u2502 \u2514\u2500\u2500 customer-detail-page.spec.tsx
4188
+ \u251C\u2500\u2500 components/
4189
+ \u2502 \u251C\u2500\u2500 customer-card.tsx
4190
+ \u2502 \u251C\u2500\u2500 customer-list.tsx
4191
+ \u2502 \u251C\u2500\u2500 customer-form.tsx
4192
+ \u2502 \u2514\u2500\u2500 __tests__/ # Component unit tests
4193
+ \u2502 \u251C\u2500\u2500 customer-card.spec.tsx
4194
+ \u2502 \u251C\u2500\u2500 customer-list.spec.tsx
4195
+ \u2502 \u2514\u2500\u2500 customer-form.spec.tsx
4196
+ \u2514\u2500\u2500 hooks/
4197
+ \u251C\u2500\u2500 use-customers-page.ts
4198
+ \u2514\u2500\u2500 __tests__/ # Hook tests
4199
+ \u2514\u2500\u2500 use-customers-page.spec.ts
4200
+ \`\`\`
4201
+
4202
+ The same pattern applies to \`features/\`, \`shared/\`, and \`router/\`:
4203
+ \`\`\`
4204
+ features/auth/
4205
+ \u251C\u2500\u2500 pages/
4206
+ \u2502 \u251C\u2500\u2500 login-page.tsx
4207
+ \u2502 \u2514\u2500\u2500 __tests__/
4208
+ \u2502 \u2514\u2500\u2500 login-page.spec.tsx
4209
+ \u251C\u2500\u2500 hooks/
4210
+ \u2502 \u2514\u2500\u2500 __tests__/
4211
+ shared/
4212
+ \u251C\u2500\u2500 components/
4213
+ \u2502 \u2514\u2500\u2500 __tests__/
4214
+ \u251C\u2500\u2500 hooks/
4215
+ \u2502 \u2514\u2500\u2500 __tests__/
4216
+ router/
4217
+ \u251C\u2500\u2500 __tests__/
4218
+ \u2502 \u251C\u2500\u2500 paths.spec.ts
4219
+ \u2502 \u2514\u2500\u2500 auth-guard.spec.tsx
4220
+ \`\`\`
4221
+
4222
+ ### Test File Naming
4223
+ - Use \`.spec.tsx\` for component/page tests (JSX)
4224
+ - Use \`.spec.ts\` for pure logic tests (hooks, utilities, no JSX)
4225
+ - Name matches the source file: \`customer-card.tsx\` \u2192 \`customer-card.spec.tsx\`
4226
+
4227
+ ### Always Use \`renderWithProviders\`
4228
+
4229
+ > **RULE: Never import \`render\` from \`@testing-library/react\` directly. Always use \`renderWithProviders\` from \`@/__tests__/test-utils\`.**
4230
+
4231
+ \`renderWithProviders\` wraps components with all app providers (ChakraProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
4232
+
4233
+ \`\`\`tsx
4234
+ import { screen } from '@/__tests__/test-utils'
4235
+ import { renderWithProviders } from '@/__tests__/test-utils'
4236
+ import { MyComponent } from '../my-component'
4237
+
4238
+ describe('MyComponent', () => {
4239
+ it('renders correctly', () => {
4240
+ renderWithProviders(<MyComponent />)
4241
+ expect(screen.getByText('Hello')).toBeInTheDocument()
4242
+ })
4243
+ })
4244
+ \`\`\`
4245
+
4246
+ **Options:**
4247
+ \`\`\`tsx
4248
+ renderWithProviders(<MyComponent />, {
4249
+ routerEntries: ['/customers/1'], // Set initial route
4250
+ queryClient: customQueryClient, // Custom query client
4251
+ })
4252
+ \`\`\`
4253
+
4254
+ **User interactions:**
4255
+ \`\`\`tsx
4256
+ const { user } = renderWithProviders(<MyComponent />)
4257
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
4258
+ await user.type(screen.getByLabelText('Email'), 'test@example.com')
4259
+ \`\`\`
4260
+
4261
+ ### When to Write Tests
4262
+
4263
+ > **RULE: Every code change that touches pages, components, hooks, or utilities must include corresponding test updates.**
4264
+
4265
+ | What changed | Test required |
4266
+ |---|---|
4267
+ | New page | Add \`__tests__/<page>.spec.tsx\` with integration test |
4268
+ | New component | Add \`__tests__/<component>.spec.tsx\` |
4269
+ | New hook (with logic) | Add \`__tests__/<hook>.spec.ts\` |
4270
+ | New utility function | Add \`__tests__/<util>.spec.ts\` |
4271
+ | Modified page/component | Update existing test or add new test cases |
4272
+ | Bug fix | Add regression test that would have caught the bug |
4273
+ | Deleted page/component | Delete corresponding test file |
4274
+
4275
+ ### What to Test
4276
+
4277
+ **Page integration tests** \u2014 test the page as a whole:
4278
+ - Renders correct heading/title
4279
+ - Loading state shows skeleton or spinner
4280
+ - Error state shows error message
4281
+ - Data renders correctly (mock the API hooks)
4282
+ - User interactions (navigation, form submission, delete confirmation)
4283
+
4284
+ **Component unit tests** \u2014 test the component in isolation:
4285
+ - Renders with required props
4286
+ - Handles optional props correctly (present vs absent)
4287
+ - Displays correct content based on props
4288
+ - User interactions trigger correct callbacks
4289
+ - Conditional rendering (empty state, loading state)
4290
+
4291
+ **Hook tests** \u2014 test custom hooks with logic:
4292
+ - Returns correct initial state
4293
+ - Transforms data correctly
4294
+ - Side effects fire as expected
4295
+
4296
+ **Utility/pure function tests** \u2014 test input/output:
4297
+ - Happy path
4298
+ - Edge cases (empty input, null, special characters)
4299
+ - Error cases
4300
+
4301
+ ### Mocking Patterns
4302
+
4303
+ **Mock hooks (for page tests):**
4304
+ \`\`\`tsx
4305
+ vi.mock('@/api/hooks/customers')
4306
+ vi.mock('@/features/auth/hooks/use-auth')
4307
+
4308
+ import { useCustomers } from '@/api/hooks/customers'
4309
+
4310
+ beforeEach(() => {
4311
+ vi.mocked(useCustomers).mockReturnValue({
4312
+ data: { customers: mockCustomers, total: 2 },
4313
+ isLoading: false,
4314
+ errorMessage: null,
4315
+ } as any)
4316
+ })
4317
+ \`\`\`
4318
+
4319
+ **Mock auth hook (for auth page tests):**
4320
+ \`\`\`tsx
4321
+ vi.mock('@/features/auth/hooks/use-auth', () => ({
4322
+ useAuth: () => ({
4323
+ user: null,
4324
+ loading: false,
4325
+ error: null,
4326
+ isAuthenticated: false,
4327
+ signInWithEmail: vi.fn(),
4328
+ signOut: vi.fn(),
4329
+ }),
4330
+ }))
4331
+ \`\`\`
4332
+
4333
+ **Mock react-router-dom hooks (for detail pages):**
4334
+ \`\`\`tsx
4335
+ const mockNavigate = vi.fn()
4336
+ vi.mock('react-router-dom', async () => {
4337
+ const actual = await vi.importActual('react-router-dom')
4338
+ return { ...actual, useParams: () => ({ id: '1' }), useNavigate: () => mockNavigate }
4339
+ })
4340
+ \`\`\`
4341
+
4342
+ ### Test Structure
4343
+ \`\`\`tsx
4344
+ import { screen, waitFor } from '@/__tests__/test-utils'
4345
+ import { renderWithProviders } from '@/__tests__/test-utils'
4346
+
4347
+ // Mocks at the top, before imports of modules that use them
4348
+ vi.mock('@/api/hooks/customers')
4349
+
4350
+ import { useCustomers } from '@/api/hooks/customers'
4351
+ import CustomersPage from '../customers-page'
4352
+
4353
+ const mockCustomers = [
4354
+ { id: '1', title: 'Acme Corp', created_at: '2024-01-15T10:00:00Z' },
4355
+ ]
4356
+
4357
+ describe('CustomersPage', () => {
4358
+ beforeEach(() => {
4359
+ vi.mocked(useCustomers).mockReturnValue({ ... } as any)
4360
+ })
4361
+
4362
+ it('renders page heading', () => {
4363
+ renderWithProviders(<CustomersPage />)
4364
+ expect(screen.getByText('Customers')).toBeInTheDocument()
4365
+ })
4366
+
4367
+ it('shows error message when API fails', () => {
4368
+ vi.mocked(useCustomers).mockReturnValue({
4369
+ data: undefined,
4370
+ isLoading: false,
4371
+ errorMessage: 'Failed to load',
4372
+ } as any)
4373
+
4374
+ renderWithProviders(<CustomersPage />)
4375
+ expect(screen.getByText('Failed to load')).toBeInTheDocument()
4376
+ })
4377
+ })
4378
+ \`\`\`
4379
+
4380
+ ### Key Rules
4381
+
4382
+ 1. **Tests live next to code** \u2014 \`__tests__/\` folder alongside the source, not in a separate top-level directory
4383
+ 2. **Always use \`renderWithProviders\`** \u2014 never import render from \`@testing-library/react\` directly
4384
+ 3. **Every page gets an integration test** \u2014 at minimum: renders heading, handles loading, handles errors
4385
+ 4. **Every component gets a unit test** \u2014 at minimum: renders with required props, handles optional props
4386
+ 5. **Mock at the hook level** \u2014 mock \`useCustomers\`, not \`fetch\`. Mock \`useAuth\`, not the auth adapter
4387
+ 6. **Test behavior, not implementation** \u2014 query by role, text, or label, not by class names or internal state
4388
+ 7. **No test-only IDs unless necessary** \u2014 prefer \`getByRole\`, \`getByText\`, \`getByLabelText\` over \`getByTestId\`
4389
+ 8. **Keep tests focused** \u2014 each \`it()\` tests one behavior. Don't assert 10 things in one test
4390
+ 9. **Clean up mocks** \u2014 use \`beforeEach\` to reset mock return values so tests don't leak state
4391
+ 10. **Update tests when code changes** \u2014 if you modify a component, update its tests. If you delete a component, delete its tests
4392
+ `;
4393
+ }
4394
+ };
4395
+
3430
4396
  // src/skills/clean-code.ts
4397
+ init_esm_shims();
3431
4398
  var cleanCodeSkill = {
3432
4399
  id: "clean-code",
3433
4400
  name: "Clean Code Principles",
@@ -3462,7 +4429,7 @@ Write code that is easy to read, easy to change, and easy to delete. Treat clari
3462
4429
  - Props interfaces should be explicit and narrow \u2014 accept only what the component needs, not entire objects
3463
4430
  - Avoid prop drilling beyond 2 levels \u2014 use context or restructure the component tree
3464
4431
  - Destructure props in the function signature for clarity
3465
- - Use \`@blacksmith-ui/react\` layout components (\`Stack\`, \`Flex\`, \`Grid\`) \u2014 never raw \`<div>\` with flex/grid classes
4432
+ - Use \`@chakra-ui/react\` layout components (\`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`) \u2014 never raw \`<div>\` with flex/grid classes
3466
4433
 
3467
4434
  ### File Organization
3468
4435
  - Keep files short. If a file exceeds 200 lines, it is likely doing too much \u2014 split it
@@ -3531,6 +4498,7 @@ Write code that is easy to read, easy to change, and easy to delete. Treat clari
3531
4498
  };
3532
4499
 
3533
4500
  // src/skills/ai-guidelines.ts
4501
+ init_esm_shims();
3534
4502
  var aiGuidelinesSkill = {
3535
4503
  id: "ai-guidelines",
3536
4504
  name: "AI Development Guidelines",
@@ -3549,7 +4517,7 @@ var aiGuidelinesSkill = {
3549
4517
  - Use existing patterns in the codebase as reference before inventing new ones
3550
4518
 
3551
4519
  ### Frontend Architecture (Mandatory)
3552
- - **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
4520
+ - **Use \`@chakra-ui/react\` for ALL UI** \u2014 \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\` for layout; \`Heading\`, \`Text\` for text; \`Card\`, \`Button\`, \`Badge\`, etc. for all elements. Never use raw HTML (\`<div>\`, \`<h1>\`, \`<p>\`, \`<button>\`) when a Chakra UI component exists
3553
4521
  - **Pages are thin orchestrators** \u2014 compose child components from \`components/\`, extract logic into \`hooks/\`. A page file should be ~20-30 lines, not a monolith
3554
4522
  - **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings like \`'/login'\` or \`'/dashboard'\`
3555
4523
  - **Add new paths to the enum** \u2014 when creating a new page, add its path to the \`Path\` enum before the \`// blacksmith:path\` marker
@@ -3563,20 +4531,22 @@ var aiGuidelinesSkill = {
3563
4531
 
3564
4532
  ### Checklist Before Finishing a Task
3565
4533
  1. Backend tests pass: \`cd backend && ./venv/bin/python manage.py test\`
3566
- 2. Frontend builds: \`cd frontend && npm run build\`
3567
- 3. API types are in sync: \`blacksmith sync\`
3568
- 4. No lint errors in modified files
3569
- 5. All UI uses \`@blacksmith-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
3570
- 6. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
3571
- 7. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
3572
- 8. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
3573
- 9. New routes have a corresponding \`Path\` enum entry
4534
+ 2. Frontend tests pass: \`cd frontend && npm test\`
4535
+ 3. Frontend builds: \`cd frontend && npm run build\`
4536
+ 4. API types are in sync: \`blacksmith sync\`
4537
+ 5. No lint errors in modified files
4538
+ 6. All UI uses \`@chakra-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
4539
+ 7. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
4540
+ 8. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
4541
+ 9. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
4542
+ 10. New routes have a corresponding \`Path\` enum entry
4543
+ 11. **Tests are co-located** \u2014 every new or modified page, component, or hook has a corresponding \`.spec.tsx\` / \`.spec.ts\` in a \`__tests__/\` folder next to the source file (see the \`frontend-testing\` skill)
3574
4544
  `;
3575
4545
  }
3576
4546
  };
3577
4547
 
3578
4548
  // src/commands/ai-setup.ts
3579
- async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill }) {
4549
+ async function setupAiDev({ projectDir, projectName, includeChakraUiSkill }) {
3580
4550
  const aiSpinner = spinner("Setting up AI development environment...");
3581
4551
  try {
3582
4552
  const skills = [
@@ -3589,24 +4559,25 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
3589
4559
  reactQuerySkill,
3590
4560
  pageStructureSkill
3591
4561
  ];
3592
- if (includeBlacksmithUiSkill) {
3593
- skills.push(blacksmithUiReactSkill);
3594
- skills.push(blacksmithUiFormsSkill);
3595
- skills.push(blacksmithUiAuthSkill);
4562
+ if (includeChakraUiSkill) {
4563
+ skills.push(chakraUiReactSkill);
4564
+ skills.push(chakraUiFormsSkill);
4565
+ skills.push(chakraUiAuthSkill);
3596
4566
  skills.push(blacksmithHooksSkill);
3597
4567
  skills.push(uiDesignSkill);
3598
4568
  }
3599
4569
  skills.push(blacksmithCliSkill);
4570
+ skills.push(frontendTestingSkill);
3600
4571
  skills.push(programmingParadigmsSkill);
3601
4572
  skills.push(cleanCodeSkill);
3602
4573
  skills.push(aiGuidelinesSkill);
3603
4574
  const ctx = { projectName };
3604
4575
  const inlineSkills = skills.filter((s) => !s.name);
3605
4576
  const fileSkills = skills.filter((s) => s.name);
3606
- const skillsDir = path3.join(projectDir, ".claude", "skills");
4577
+ const skillsDir = path4.join(projectDir, ".claude", "skills");
3607
4578
  if (fs3.existsSync(skillsDir)) {
3608
4579
  for (const entry of fs3.readdirSync(skillsDir)) {
3609
- const entryPath = path3.join(skillsDir, entry);
4580
+ const entryPath = path4.join(skillsDir, entry);
3610
4581
  const stat = fs3.statSync(entryPath);
3611
4582
  if (stat.isDirectory()) {
3612
4583
  fs3.rmSync(entryPath, { recursive: true });
@@ -3617,7 +4588,7 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
3617
4588
  }
3618
4589
  fs3.mkdirSync(skillsDir, { recursive: true });
3619
4590
  for (const skill of fileSkills) {
3620
- const skillDir = path3.join(skillsDir, skill.id);
4591
+ const skillDir = path4.join(skillsDir, skill.id);
3621
4592
  fs3.mkdirSync(skillDir, { recursive: true });
3622
4593
  const frontmatter = `---
3623
4594
  name: ${skill.name}
@@ -3626,7 +4597,7 @@ description: ${skill.description}
3626
4597
 
3627
4598
  `;
3628
4599
  const content = skill.render(ctx).trim();
3629
- fs3.writeFileSync(path3.join(skillDir, "SKILL.md"), frontmatter + content + "\n", "utf-8");
4600
+ fs3.writeFileSync(path4.join(skillDir, "SKILL.md"), frontmatter + content + "\n", "utf-8");
3630
4601
  }
3631
4602
  const inlineContent = inlineSkills.map((s) => s.render(ctx)).join("\n");
3632
4603
  const skillsList = fileSkills.map((s) => `- \`.claude/skills/${s.id}/SKILL.md\` \u2014 ${s.name}`).join("\n");
@@ -3642,7 +4613,7 @@ description: ${skill.description}
3642
4613
  "These files are auto-loaded by Claude Code. Run `blacksmith setup:ai` to regenerate.",
3643
4614
  ""
3644
4615
  ].join("\n");
3645
- fs3.writeFileSync(path3.join(projectDir, "CLAUDE.md"), claudeMd, "utf-8");
4616
+ fs3.writeFileSync(path4.join(projectDir, "CLAUDE.md"), claudeMd, "utf-8");
3646
4617
  const skillNames = skills.filter((s) => s.id !== "project-overview" && s.id !== "ai-guidelines").map((s) => s.id).join(" + ");
3647
4618
  aiSpinner.succeed(`AI dev environment ready (${skillNames} skills)`);
3648
4619
  } catch (error) {
@@ -3691,9 +4662,9 @@ async function init(name, options) {
3691
4662
  "Theme": themePreset,
3692
4663
  "AI support": options.ai ? "Yes" : "No"
3693
4664
  });
3694
- const projectDir = path4.resolve(process.cwd(), name);
3695
- const backendDir = path4.join(projectDir, "backend");
3696
- const frontendDir = path4.join(projectDir, "frontend");
4665
+ const projectDir = path5.resolve(process.cwd(), name);
4666
+ const backendDir = path5.join(projectDir, "backend");
4667
+ const frontendDir = path5.join(projectDir, "frontend");
3697
4668
  const templatesDir = getTemplatesDir();
3698
4669
  if (fs4.existsSync(projectDir)) {
3699
4670
  log.error(`Directory "${name}" already exists.`);
@@ -3720,7 +4691,7 @@ async function init(name, options) {
3720
4691
  };
3721
4692
  fs4.mkdirSync(projectDir, { recursive: true });
3722
4693
  fs4.writeFileSync(
3723
- path4.join(projectDir, "blacksmith.config.json"),
4694
+ path5.join(projectDir, "blacksmith.config.json"),
3724
4695
  JSON.stringify(
3725
4696
  {
3726
4697
  name,
@@ -3735,13 +4706,13 @@ async function init(name, options) {
3735
4706
  const backendSpinner = spinner("Generating Django backend...");
3736
4707
  try {
3737
4708
  renderDirectory(
3738
- path4.join(templatesDir, "backend"),
4709
+ path5.join(templatesDir, "backend"),
3739
4710
  backendDir,
3740
4711
  context
3741
4712
  );
3742
4713
  fs4.copyFileSync(
3743
- path4.join(backendDir, ".env.example"),
3744
- path4.join(backendDir, ".env")
4714
+ path5.join(backendDir, ".env.example"),
4715
+ path5.join(backendDir, ".env")
3745
4716
  );
3746
4717
  backendSpinner.succeed("Django backend generated");
3747
4718
  } catch (error) {
@@ -3784,7 +4755,7 @@ async function init(name, options) {
3784
4755
  const frontendSpinner = spinner("Generating React frontend...");
3785
4756
  try {
3786
4757
  renderDirectory(
3787
- path4.join(templatesDir, "frontend"),
4758
+ path5.join(templatesDir, "frontend"),
3788
4759
  frontendDir,
3789
4760
  context
3790
4761
  );
@@ -3817,7 +4788,7 @@ async function init(name, options) {
3817
4788
  djangoProcess.unref();
3818
4789
  await new Promise((resolve) => setTimeout(resolve, 4e3));
3819
4790
  try {
3820
- await exec(process.execPath, [path4.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
4791
+ await exec(process.execPath, [path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
3821
4792
  syncSpinner.succeed("OpenAPI types synced");
3822
4793
  } catch {
3823
4794
  syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
@@ -3831,8 +4802,8 @@ async function init(name, options) {
3831
4802
  } catch {
3832
4803
  syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
3833
4804
  }
3834
- const generatedDir = path4.join(frontendDir, "src", "api", "generated");
3835
- const stubFile = path4.join(generatedDir, "client.gen.ts");
4805
+ const generatedDir = path5.join(frontendDir, "src", "api", "generated");
4806
+ const stubFile = path5.join(generatedDir, "client.gen.ts");
3836
4807
  if (!fs4.existsSync(stubFile)) {
3837
4808
  if (!fs4.existsSync(generatedDir)) {
3838
4809
  fs4.mkdirSync(generatedDir, { recursive: true });
@@ -3862,16 +4833,17 @@ async function init(name, options) {
3862
4833
  await setupAiDev({
3863
4834
  projectDir,
3864
4835
  projectName: name,
3865
- includeBlacksmithUiSkill: options.blacksmithUiSkill !== false
4836
+ includeChakraUiSkill: options.chakraUiSkill !== false
3866
4837
  });
3867
4838
  }
3868
4839
  printNextSteps(name, backendPort, frontendPort);
3869
4840
  }
3870
4841
 
3871
4842
  // src/commands/dev.ts
4843
+ init_esm_shims();
3872
4844
  import net from "net";
3873
4845
  import concurrently from "concurrently";
3874
- import path5 from "path";
4846
+ import path6 from "path";
3875
4847
  function isPortAvailable(port) {
3876
4848
  return new Promise((resolve) => {
3877
4849
  const server = net.createServer();
@@ -3926,7 +4898,7 @@ async function dev() {
3926
4898
  log.step(`Swagger \u2192 http://localhost:${backendPort}/api/docs/`);
3927
4899
  log.step("OpenAPI sync \u2192 watching backend .py files");
3928
4900
  log.blank();
3929
- const syncCmd = `${process.execPath} ${path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
4901
+ const syncCmd = `${process.execPath} ${path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
3930
4902
  const watcherCode = [
3931
4903
  `const{watch}=require("fs"),{exec}=require("child_process");`,
3932
4904
  `let t=null,s=false;`,
@@ -3986,7 +4958,8 @@ async function dev() {
3986
4958
  }
3987
4959
 
3988
4960
  // src/commands/sync.ts
3989
- import path6 from "path";
4961
+ init_esm_shims();
4962
+ import path7 from "path";
3990
4963
  import fs5 from "fs";
3991
4964
  async function sync() {
3992
4965
  let root;
@@ -4000,9 +4973,9 @@ async function sync() {
4000
4973
  const frontendDir = getFrontendDir(root);
4001
4974
  const s = spinner("Syncing OpenAPI schema to frontend...");
4002
4975
  try {
4003
- const schemaPath = path6.join(frontendDir, "_schema.yml");
4976
+ const schemaPath = path7.join(frontendDir, "_schema.yml");
4004
4977
  await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
4005
- const configPath = path6.join(frontendDir, "openapi-ts.config.ts");
4978
+ const configPath = path7.join(frontendDir, "openapi-ts.config.ts");
4006
4979
  const configBackup = fs5.readFileSync(configPath, "utf-8");
4007
4980
  const configWithFile = configBackup.replace(
4008
4981
  /path:\s*['"]http[^'"]+['"]/,
@@ -4010,7 +4983,7 @@ async function sync() {
4010
4983
  );
4011
4984
  fs5.writeFileSync(configPath, configWithFile, "utf-8");
4012
4985
  try {
4013
- await exec(process.execPath, [path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4986
+ await exec(process.execPath, [path7.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4014
4987
  cwd: frontendDir,
4015
4988
  silent: true
4016
4989
  });
@@ -4034,10 +5007,12 @@ async function sync() {
4034
5007
  }
4035
5008
 
4036
5009
  // src/commands/make-resource.ts
4037
- import path7 from "path";
5010
+ init_esm_shims();
5011
+ import path8 from "path";
4038
5012
  import fs6 from "fs";
4039
5013
 
4040
5014
  // src/utils/names.ts
5015
+ init_esm_shims();
4041
5016
  import { pascalCase, snakeCase, kebabCase, camelCase } from "change-case";
4042
5017
  import pluralize from "pluralize";
4043
5018
  function generateNames(input) {
@@ -4070,12 +5045,12 @@ async function makeResource(name) {
4070
5045
  const backendDir = getBackendDir(root);
4071
5046
  const frontendDir = getFrontendDir(root);
4072
5047
  const templatesDir = getTemplatesDir();
4073
- const backendAppDir = path7.join(backendDir, "apps", names.snakes);
5048
+ const backendAppDir = path8.join(backendDir, "apps", names.snakes);
4074
5049
  if (fs6.existsSync(backendAppDir)) {
4075
5050
  log.error(`Backend app "${names.snakes}" already exists.`);
4076
5051
  process.exit(1);
4077
5052
  }
4078
- const frontendPageDir = path7.join(frontendDir, "src", "pages", names.kebabs);
5053
+ const frontendPageDir = path8.join(frontendDir, "src", "pages", names.kebabs);
4079
5054
  if (fs6.existsSync(frontendPageDir)) {
4080
5055
  log.error(`Frontend page "${names.kebabs}" already exists.`);
4081
5056
  process.exit(1);
@@ -4084,7 +5059,7 @@ async function makeResource(name) {
4084
5059
  const backendSpinner = spinner(`Creating backend app: apps/${names.snakes}/`);
4085
5060
  try {
4086
5061
  renderDirectory(
4087
- path7.join(templatesDir, "resource", "backend"),
5062
+ path8.join(templatesDir, "resource", "backend"),
4088
5063
  backendAppDir,
4089
5064
  context
4090
5065
  );
@@ -4096,7 +5071,7 @@ async function makeResource(name) {
4096
5071
  }
4097
5072
  const registerSpinner = spinner("Registering app in Django settings...");
4098
5073
  try {
4099
- const settingsPath = path7.join(backendDir, "config", "settings", "base.py");
5074
+ const settingsPath = path8.join(backendDir, "config", "settings", "base.py");
4100
5075
  appendAfterMarker(
4101
5076
  settingsPath,
4102
5077
  "# blacksmith:apps",
@@ -4110,7 +5085,7 @@ async function makeResource(name) {
4110
5085
  }
4111
5086
  const urlSpinner = spinner("Registering API URLs...");
4112
5087
  try {
4113
- const urlsPath = path7.join(backendDir, "config", "urls.py");
5088
+ const urlsPath = path8.join(backendDir, "config", "urls.py");
4114
5089
  insertBeforeMarker(
4115
5090
  urlsPath,
4116
5091
  "# blacksmith:urls",
@@ -4134,9 +5109,9 @@ async function makeResource(name) {
4134
5109
  }
4135
5110
  const syncSpinner = spinner("Syncing OpenAPI schema...");
4136
5111
  try {
4137
- const schemaPath = path7.join(frontendDir, "_schema.yml");
5112
+ const schemaPath = path8.join(frontendDir, "_schema.yml");
4138
5113
  await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
4139
- const configPath = path7.join(frontendDir, "openapi-ts.config.ts");
5114
+ const configPath = path8.join(frontendDir, "openapi-ts.config.ts");
4140
5115
  const configBackup = fs6.readFileSync(configPath, "utf-8");
4141
5116
  const configWithFile = configBackup.replace(
4142
5117
  /path:\s*['"]http[^'"]+['"]/,
@@ -4144,7 +5119,7 @@ async function makeResource(name) {
4144
5119
  );
4145
5120
  fs6.writeFileSync(configPath, configWithFile, "utf-8");
4146
5121
  try {
4147
- await exec(process.execPath, [path7.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
5122
+ await exec(process.execPath, [path8.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4148
5123
  cwd: frontendDir,
4149
5124
  silent: true
4150
5125
  });
@@ -4156,11 +5131,11 @@ async function makeResource(name) {
4156
5131
  } catch {
4157
5132
  syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
4158
5133
  }
4159
- const apiHooksDir = path7.join(frontendDir, "src", "api", "hooks", names.kebabs);
5134
+ const apiHooksDir = path8.join(frontendDir, "src", "api", "hooks", names.kebabs);
4160
5135
  const apiHooksSpinner = spinner(`Creating API hooks: api/hooks/${names.kebabs}/`);
4161
5136
  try {
4162
5137
  renderDirectory(
4163
- path7.join(templatesDir, "resource", "api-hooks"),
5138
+ path8.join(templatesDir, "resource", "api-hooks"),
4164
5139
  apiHooksDir,
4165
5140
  context
4166
5141
  );
@@ -4173,7 +5148,7 @@ async function makeResource(name) {
4173
5148
  const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
4174
5149
  try {
4175
5150
  renderDirectory(
4176
- path7.join(templatesDir, "resource", "pages"),
5151
+ path8.join(templatesDir, "resource", "pages"),
4177
5152
  frontendPageDir,
4178
5153
  context
4179
5154
  );
@@ -4185,7 +5160,7 @@ async function makeResource(name) {
4185
5160
  }
4186
5161
  const pathSpinner = spinner("Registering route path...");
4187
5162
  try {
4188
- const pathsFile = path7.join(frontendDir, "src", "router", "paths.ts");
5163
+ const pathsFile = path8.join(frontendDir, "src", "router", "paths.ts");
4189
5164
  insertBeforeMarker(
4190
5165
  pathsFile,
4191
5166
  "// blacksmith:path",
@@ -4197,7 +5172,7 @@ async function makeResource(name) {
4197
5172
  }
4198
5173
  const routeSpinner = spinner("Registering frontend routes...");
4199
5174
  try {
4200
- const routesPath = path7.join(frontendDir, "src", "router", "routes.tsx");
5175
+ const routesPath = path8.join(frontendDir, "src", "router", "routes.tsx");
4201
5176
  insertBeforeMarker(
4202
5177
  routesPath,
4203
5178
  "// blacksmith:import",
@@ -4218,6 +5193,7 @@ async function makeResource(name) {
4218
5193
  }
4219
5194
 
4220
5195
  // src/commands/build.ts
5196
+ init_esm_shims();
4221
5197
  async function build() {
4222
5198
  let root;
4223
5199
  try {
@@ -4259,8 +5235,9 @@ async function build() {
4259
5235
  }
4260
5236
 
4261
5237
  // src/commands/eject.ts
5238
+ init_esm_shims();
4262
5239
  import fs7 from "fs";
4263
- import path8 from "path";
5240
+ import path9 from "path";
4264
5241
  async function eject() {
4265
5242
  let root;
4266
5243
  try {
@@ -4269,7 +5246,7 @@ async function eject() {
4269
5246
  log.error("Not inside a Blacksmith project.");
4270
5247
  process.exit(1);
4271
5248
  }
4272
- const configPath = path8.join(root, "blacksmith.config.json");
5249
+ const configPath = path9.join(root, "blacksmith.config.json");
4273
5250
  if (fs7.existsSync(configPath)) {
4274
5251
  fs7.unlinkSync(configPath);
4275
5252
  }
@@ -4287,6 +5264,7 @@ async function eject() {
4287
5264
  }
4288
5265
 
4289
5266
  // src/commands/skills.ts
5267
+ init_esm_shims();
4290
5268
  import fs8 from "fs";
4291
5269
  var allSkills = [
4292
5270
  projectOverviewSkill,
@@ -4294,11 +5272,12 @@ var allSkills = [
4294
5272
  djangoRestAdvancedSkill,
4295
5273
  apiDocumentationSkill,
4296
5274
  reactSkill,
4297
- blacksmithUiReactSkill,
4298
- blacksmithUiFormsSkill,
4299
- blacksmithUiAuthSkill,
5275
+ chakraUiReactSkill,
5276
+ chakraUiFormsSkill,
5277
+ chakraUiAuthSkill,
4300
5278
  blacksmithHooksSkill,
4301
5279
  blacksmithCliSkill,
5280
+ frontendTestingSkill,
4302
5281
  cleanCodeSkill,
4303
5282
  aiGuidelinesSkill
4304
5283
  ];
@@ -4314,7 +5293,7 @@ async function setupSkills(options) {
4314
5293
  await setupAiDev({
4315
5294
  projectDir: root,
4316
5295
  projectName: config.name,
4317
- includeBlacksmithUiSkill: options.blacksmithUiSkill !== false
5296
+ includeChakraUiSkill: options.chakraUiSkill !== false
4318
5297
  });
4319
5298
  log.blank();
4320
5299
  log.success("AI skills generated:");
@@ -4352,7 +5331,324 @@ function listSkills() {
4352
5331
  }
4353
5332
  }
4354
5333
 
5334
+ // src/commands/mcp-setup.ts
5335
+ init_esm_shims();
5336
+ import fs9 from "fs";
5337
+ import path10 from "path";
5338
+ var DONE_OPTION = "Done \u2014 save and exit";
5339
+ function getPresets(projectRoot) {
5340
+ return [
5341
+ {
5342
+ id: "filesystem",
5343
+ name: "Filesystem",
5344
+ description: "Read and write project files",
5345
+ command: "npx",
5346
+ args: ["-y", "@modelcontextprotocol/server-filesystem"],
5347
+ prompts: [
5348
+ {
5349
+ label: "Allowed directory path",
5350
+ defaultValue: projectRoot,
5351
+ target: "args"
5352
+ }
5353
+ ]
5354
+ },
5355
+ {
5356
+ id: "postgres",
5357
+ name: "PostgreSQL",
5358
+ description: "Query PostgreSQL databases",
5359
+ command: "npx",
5360
+ args: ["-y", "@modelcontextprotocol/server-postgres"],
5361
+ prompts: [
5362
+ {
5363
+ label: "PostgreSQL connection string (e.g. postgresql://user:pass@localhost:5432/dbname)",
5364
+ target: "args"
5365
+ }
5366
+ ]
5367
+ },
5368
+ {
5369
+ id: "fetch",
5370
+ name: "Fetch",
5371
+ description: "Make HTTP requests and fetch web content",
5372
+ command: "npx",
5373
+ args: ["-y", "@modelcontextprotocol/server-fetch"]
5374
+ },
5375
+ {
5376
+ id: "github",
5377
+ name: "GitHub",
5378
+ description: "Interact with GitHub repos, issues, and PRs",
5379
+ command: "npx",
5380
+ args: ["-y", "@modelcontextprotocol/server-github"],
5381
+ prompts: [
5382
+ {
5383
+ label: "GitHub personal access token",
5384
+ target: "env",
5385
+ envVar: "GITHUB_PERSONAL_ACCESS_TOKEN"
5386
+ }
5387
+ ]
5388
+ },
5389
+ {
5390
+ id: "chakra-ui-docs",
5391
+ name: "Chakra UI Docs",
5392
+ description: "Chakra UI component documentation for AI assistance",
5393
+ command: "npx",
5394
+ args: [
5395
+ "-y",
5396
+ "@anthropic-ai/mcp-docs-server",
5397
+ "--url",
5398
+ "https://www.chakra-ui.com/docs",
5399
+ "--name",
5400
+ "chakra-ui-docs"
5401
+ ]
5402
+ },
5403
+ {
5404
+ id: "sentry",
5405
+ name: "Sentry",
5406
+ description: "Query errors, view issues, and manage releases in Sentry",
5407
+ command: "npx",
5408
+ args: ["-y", "@sentry/mcp-server"],
5409
+ prompts: [
5410
+ {
5411
+ label: "Sentry auth token",
5412
+ target: "env",
5413
+ envVar: "SENTRY_AUTH_TOKEN"
5414
+ },
5415
+ {
5416
+ label: "Sentry organization slug",
5417
+ target: "env",
5418
+ envVar: "SENTRY_ORG"
5419
+ }
5420
+ ]
5421
+ },
5422
+ {
5423
+ id: "puppeteer",
5424
+ name: "Puppeteer",
5425
+ description: "Browser automation for testing, screenshots, and scraping",
5426
+ command: "npx",
5427
+ args: ["-y", "@modelcontextprotocol/server-puppeteer"]
5428
+ },
5429
+ {
5430
+ id: "memory",
5431
+ name: "Memory",
5432
+ description: "Persistent knowledge graph for cross-session context",
5433
+ command: "npx",
5434
+ args: ["-y", "@modelcontextprotocol/server-memory"]
5435
+ },
5436
+ {
5437
+ id: "slack",
5438
+ name: "Slack",
5439
+ description: "Read/send messages, search channels, and manage threads",
5440
+ command: "npx",
5441
+ args: ["-y", "@anthropic-ai/mcp-server-slack"],
5442
+ prompts: [
5443
+ {
5444
+ label: "Slack bot token (xoxb-...)",
5445
+ target: "env",
5446
+ envVar: "SLACK_BOT_TOKEN"
5447
+ },
5448
+ {
5449
+ label: "Slack team ID",
5450
+ target: "env",
5451
+ envVar: "SLACK_TEAM_ID"
5452
+ }
5453
+ ]
5454
+ },
5455
+ {
5456
+ id: "redis",
5457
+ name: "Redis",
5458
+ description: "Query and manage Redis cache",
5459
+ command: "npx",
5460
+ args: ["-y", "@modelcontextprotocol/server-redis"],
5461
+ prompts: [
5462
+ {
5463
+ label: "Redis URL (e.g. redis://localhost:6379)",
5464
+ defaultValue: "redis://localhost:6379",
5465
+ target: "env",
5466
+ envVar: "REDIS_URL"
5467
+ }
5468
+ ]
5469
+ }
5470
+ ];
5471
+ }
5472
+ function readSettings(settingsPath) {
5473
+ if (!fs9.existsSync(settingsPath)) {
5474
+ return {};
5475
+ }
5476
+ try {
5477
+ return JSON.parse(fs9.readFileSync(settingsPath, "utf-8"));
5478
+ } catch {
5479
+ return {};
5480
+ }
5481
+ }
5482
+ function buildServerConfig(preset, promptValues) {
5483
+ const args = [...preset.args];
5484
+ const env = { ...preset.env };
5485
+ let valueIndex = 0;
5486
+ for (const prompt of preset.prompts || []) {
5487
+ const value = promptValues[valueIndex++];
5488
+ if (prompt.target === "args") {
5489
+ args.push(value);
5490
+ } else if (prompt.target === "env" && prompt.envVar) {
5491
+ env[prompt.envVar] = value;
5492
+ }
5493
+ }
5494
+ const config = { command: preset.command, args };
5495
+ if (Object.keys(env).length > 0) {
5496
+ config.env = env;
5497
+ }
5498
+ return config;
5499
+ }
5500
+ async function setupMcp() {
5501
+ let root;
5502
+ try {
5503
+ root = findProjectRoot();
5504
+ } catch {
5505
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
5506
+ process.exit(1);
5507
+ }
5508
+ const settingsPath = path10.join(root, ".claude", "settings.local.json");
5509
+ const settings = readSettings(settingsPath);
5510
+ const existingServers = settings.mcpServers || {};
5511
+ const newServers = {};
5512
+ const presets = getPresets(root);
5513
+ log.blank();
5514
+ log.info("Configure MCP servers for Claude Code.");
5515
+ log.info('Select servers to add, then choose "Done" to save.');
5516
+ log.blank();
5517
+ while (true) {
5518
+ const options = presets.map((p) => {
5519
+ const configured = existingServers[p.id] || newServers[p.id];
5520
+ const suffix = configured ? " (configured)" : "";
5521
+ return `${p.name} \u2014 ${p.description}${suffix}`;
5522
+ });
5523
+ options.push(DONE_OPTION);
5524
+ const choice = await promptSelect("Select an MCP server to configure", options);
5525
+ if (choice === DONE_OPTION) {
5526
+ break;
5527
+ }
5528
+ const selectedIndex = options.indexOf(choice);
5529
+ const preset = presets[selectedIndex];
5530
+ if (!preset) break;
5531
+ if (existingServers[preset.id] || newServers[preset.id]) {
5532
+ const overwrite = await promptYesNo(
5533
+ `${preset.name} is already configured. Overwrite?`,
5534
+ false
5535
+ );
5536
+ if (!overwrite) continue;
5537
+ }
5538
+ const values = [];
5539
+ let skipped = false;
5540
+ for (const prompt of preset.prompts || []) {
5541
+ const value = await promptText(prompt.label, prompt.defaultValue);
5542
+ if (!value && !prompt.defaultValue) {
5543
+ log.warn(`Skipping ${preset.name} \u2014 required value not provided.`);
5544
+ skipped = true;
5545
+ break;
5546
+ }
5547
+ values.push(value);
5548
+ }
5549
+ if (skipped) continue;
5550
+ newServers[preset.id] = buildServerConfig(preset, values);
5551
+ log.success(`${preset.name} configured.`);
5552
+ log.blank();
5553
+ }
5554
+ if (Object.keys(newServers).length === 0) {
5555
+ log.info("No new servers configured.");
5556
+ return;
5557
+ }
5558
+ settings.mcpServers = { ...existingServers, ...newServers };
5559
+ try {
5560
+ fs9.mkdirSync(path10.join(root, ".claude"), { recursive: true });
5561
+ fs9.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
5562
+ } catch (error) {
5563
+ log.error(`Failed to write settings: ${error.message}`);
5564
+ process.exit(1);
5565
+ }
5566
+ log.blank();
5567
+ log.success("MCP servers configured:");
5568
+ for (const id of Object.keys(newServers)) {
5569
+ const preset = presets.find((p) => p.id === id);
5570
+ log.step(` ${preset?.name || id}`);
5571
+ }
5572
+ log.blank();
5573
+ log.info(`Settings written to ${path10.relative(root, settingsPath)}`);
5574
+ log.blank();
5575
+ }
5576
+
5577
+ // src/commands/studio.ts
5578
+ init_esm_shims();
5579
+ import net2 from "net";
5580
+ var DEFAULT_PORT = 3939;
5581
+ function isPortAvailable2(port) {
5582
+ return new Promise((resolve) => {
5583
+ const server = net2.createServer();
5584
+ server.once("error", () => resolve(false));
5585
+ server.once("listening", () => {
5586
+ server.close(() => resolve(true));
5587
+ });
5588
+ server.listen(port);
5589
+ });
5590
+ }
5591
+ async function findAvailablePort2(startPort) {
5592
+ let port = startPort;
5593
+ while (port < startPort + 100) {
5594
+ if (await isPortAvailable2(port)) return port;
5595
+ port++;
5596
+ }
5597
+ throw new Error(`No available port found in range ${startPort}-${port - 1}`);
5598
+ }
5599
+ async function studio(options) {
5600
+ let root;
5601
+ try {
5602
+ root = findProjectRoot();
5603
+ } catch {
5604
+ log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
5605
+ process.exit(1);
5606
+ }
5607
+ const config = loadConfig(root);
5608
+ const requestedPort = options.port ? parseInt(options.port, 10) : DEFAULT_PORT;
5609
+ let port;
5610
+ try {
5611
+ port = await findAvailablePort2(requestedPort);
5612
+ } catch (err) {
5613
+ log.error(err.message);
5614
+ process.exit(1);
5615
+ }
5616
+ if (port !== requestedPort) {
5617
+ log.step(`Port ${requestedPort} in use, using ${port}`);
5618
+ }
5619
+ log.info(`Starting Blacksmith Studio for "${config.name}"...`);
5620
+ log.blank();
5621
+ try {
5622
+ const { createStudioServer } = await import("@blacksmith/studio");
5623
+ const { server } = await createStudioServer({ projectRoot: root, port });
5624
+ const url = `http://localhost:${port}`;
5625
+ log.success("Blacksmith Studio is running!");
5626
+ log.blank();
5627
+ log.step(`Studio \u2192 ${url}`);
5628
+ log.step(`Project \u2192 ${root}`);
5629
+ log.blank();
5630
+ log.info("Press Ctrl+C to stop.");
5631
+ try {
5632
+ const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
5633
+ await open2(url);
5634
+ } catch {
5635
+ }
5636
+ const shutdown = () => {
5637
+ log.blank();
5638
+ log.info("Blacksmith Studio stopped.");
5639
+ server.close();
5640
+ process.exit(0);
5641
+ };
5642
+ process.on("SIGINT", shutdown);
5643
+ process.on("SIGTERM", shutdown);
5644
+ } catch (error) {
5645
+ log.error(`Failed to start Studio: ${error.message}`);
5646
+ process.exit(1);
5647
+ }
5648
+ }
5649
+
4355
5650
  // src/commands/backend.ts
5651
+ init_esm_shims();
4356
5652
  async function backend(args) {
4357
5653
  let root;
4358
5654
  try {
@@ -4376,6 +5672,7 @@ async function backend(args) {
4376
5672
  }
4377
5673
 
4378
5674
  // src/commands/frontend.ts
5675
+ init_esm_shims();
4379
5676
  async function frontend(args) {
4380
5677
  let root;
4381
5678
  try {
@@ -4403,13 +5700,15 @@ var program = new Command();
4403
5700
  program.name("blacksmith").description("Fullstack Django + React framework").version("0.1.0").hook("preAction", () => {
4404
5701
  banner();
4405
5702
  });
4406
- 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);
5703
+ program.command("init").argument("[name]", "Project name").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-chakra-ui-skill", "Disable Chakra UI skill when using --ai").option("-b, --backend-port <port>", "Django backend port (default: 8000)").option("-f, --frontend-port <port>", "Vite frontend port (default: 5173)").option("-t, --theme-color <color>", "Theme color (zinc, slate, blue, green, orange, red, violet)").description("Create a new Blacksmith project").action(init);
4407
5704
  program.command("dev").description("Start development servers (Django + Vite + OpenAPI sync)").action(dev);
4408
5705
  program.command("sync").description("Sync OpenAPI schema to frontend types, schemas, and hooks").action(sync);
4409
5706
  program.command("make:resource").argument("<name>", "Resource name (PascalCase, e.g. BlogPost)").description("Create a new resource (model, serializer, viewset, hooks, pages)").action(makeResource);
4410
5707
  program.command("build").description("Build both frontend and backend for production").action(build);
4411
5708
  program.command("eject").description("Remove Blacksmith, keep a clean Django + React project").action(eject);
4412
- 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);
5709
+ program.command("setup:ai").description("Generate CLAUDE.md with AI development skills for the project").option("--no-chakra-ui-skill", "Exclude Chakra UI skill").action(setupSkills);
5710
+ program.command("setup:mcp").description("Configure MCP servers for Claude Code AI integration").action(setupMcp);
5711
+ program.command("studio").description("Launch Blacksmith Studio \u2014 web UI for Claude Code").option("-p, --port <port>", "Port for the Studio server (default: 3939)").action(studio);
4413
5712
  program.command("skills").description("List all available AI development skills").action(listSkills);
4414
5713
  program.command("backend").argument("[args...]", "Django management command and arguments").description("Run a Django management command (e.g. blacksmith backend createsuperuser)").allowUnknownOption().action(backend);
4415
5714
  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);