@warlock.js/core 5.0.1 → 5.1.0

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 (120) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/esm/cli/cli-commands.manager.mjs +36 -1
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/dev-server.command.mjs +1 -1
  5. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +207 -0
  6. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -0
  7. package/esm/cli/commands/doctor/checks/config.check.mjs +5 -2
  8. package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
  9. package/esm/cli/commands/doctor/checks/connectors.check.mjs +38 -12
  10. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +8 -4
  12. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
  13. package/esm/cli/commands/doctor/checks/health.check.mjs +77 -11
  14. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  15. package/esm/cli/commands/doctor/checks/index.mjs +5 -0
  16. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  17. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +116 -53
  18. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +27 -9
  20. package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
  21. package/esm/cli/commands/doctor/checks/routes.check.mjs +51 -12
  22. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
  23. package/esm/cli/commands/doctor/doctor-command.action.mjs +16 -6
  24. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  25. package/esm/cli/commands/doctor/run-checks.mjs +15 -6
  26. package/esm/cli/commands/doctor/run-checks.mjs.map +1 -1
  27. package/esm/cli/commands/doctor.command.mjs +17 -9
  28. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  29. package/esm/connectors/describe-server-address.mjs +89 -6
  30. package/esm/connectors/describe-server-address.mjs.map +1 -1
  31. package/esm/connectors/http-connector.d.mts +18 -0
  32. package/esm/connectors/http-connector.d.mts.map +1 -1
  33. package/esm/connectors/http-connector.mjs +36 -3
  34. package/esm/connectors/http-connector.mjs.map +1 -1
  35. package/esm/dev-server/dev-logger.mjs +1 -4
  36. package/esm/dev-server/dev-logger.mjs.map +1 -1
  37. package/esm/dev-server/development-server.mjs +3 -7
  38. package/esm/dev-server/development-server.mjs.map +1 -1
  39. package/esm/dev-server/module-loader.mjs +1 -1
  40. package/esm/dev-server/ready-block.mjs +62 -0
  41. package/esm/dev-server/ready-block.mjs.map +1 -0
  42. package/esm/generations/add-command.action.mjs +2 -575
  43. package/esm/generations/add-command.action.mjs.map +1 -1
  44. package/esm/generations/features/access.feature.mjs +76 -0
  45. package/esm/generations/features/access.feature.mjs.map +1 -0
  46. package/esm/generations/features/ai-anthropic.feature.mjs +10 -0
  47. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -0
  48. package/esm/generations/features/ai-bedrock.feature.mjs +10 -0
  49. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -0
  50. package/esm/generations/features/ai-google.feature.mjs +10 -0
  51. package/esm/generations/features/ai-google.feature.mjs.map +1 -0
  52. package/esm/generations/features/ai-ollama.feature.mjs +10 -0
  53. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -0
  54. package/esm/generations/features/ai-openai.feature.mjs +10 -0
  55. package/esm/generations/features/ai-openai.feature.mjs.map +1 -0
  56. package/esm/generations/features/ai-panoptic.feature.mjs +13 -0
  57. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -0
  58. package/esm/generations/features/ai-tools.feature.mjs +13 -0
  59. package/esm/generations/features/ai-tools.feature.mjs.map +1 -0
  60. package/esm/generations/features/ai-workspace.feature.mjs +13 -0
  61. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -0
  62. package/esm/generations/features/ai.feature.mjs +15 -0
  63. package/esm/generations/features/ai.feature.mjs.map +1 -0
  64. package/esm/generations/features/herald.feature.mjs +19 -0
  65. package/esm/generations/features/herald.feature.mjs.map +1 -0
  66. package/esm/generations/features/image.feature.mjs +9 -0
  67. package/esm/generations/features/image.feature.mjs.map +1 -0
  68. package/esm/generations/features/index.mjs +72 -0
  69. package/esm/generations/features/index.mjs.map +1 -0
  70. package/esm/generations/features/mail.feature.mjs +10 -0
  71. package/esm/generations/features/mail.feature.mjs.map +1 -0
  72. package/esm/generations/features/mongodb.feature.mjs +9 -0
  73. package/esm/generations/features/mongodb.feature.mjs.map +1 -0
  74. package/esm/generations/features/mysql.feature.mjs +9 -0
  75. package/esm/generations/features/mysql.feature.mjs.map +1 -0
  76. package/esm/generations/features/notifications.feature.mjs +43 -0
  77. package/esm/generations/features/notifications.feature.mjs.map +1 -0
  78. package/esm/generations/features/postgres.feature.mjs +9 -0
  79. package/esm/generations/features/postgres.feature.mjs.map +1 -0
  80. package/esm/generations/features/react-email.feature.mjs +70 -0
  81. package/esm/generations/features/react-email.feature.mjs.map +1 -0
  82. package/esm/generations/features/react.feature.mjs +16 -0
  83. package/esm/generations/features/react.feature.mjs.map +1 -0
  84. package/esm/generations/features/redis.feature.mjs +9 -0
  85. package/esm/generations/features/redis.feature.mjs.map +1 -0
  86. package/esm/generations/features/s3.feature.mjs +13 -0
  87. package/esm/generations/features/s3.feature.mjs.map +1 -0
  88. package/esm/generations/features/scheduler.feature.mjs +9 -0
  89. package/esm/generations/features/scheduler.feature.mjs.map +1 -0
  90. package/esm/generations/features/ses.feature.mjs +9 -0
  91. package/esm/generations/features/ses.feature.mjs.map +1 -0
  92. package/esm/generations/features/shadcn.feature.mjs +397 -0
  93. package/esm/generations/features/shadcn.feature.mjs.map +1 -0
  94. package/esm/generations/features/shared/link-ai-package-import.mjs +37 -0
  95. package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -0
  96. package/esm/generations/features/shared/migration-timestamp.mjs +17 -0
  97. package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -0
  98. package/esm/generations/features/socket.feature.mjs +15 -0
  99. package/esm/generations/features/socket.feature.mjs.map +1 -0
  100. package/esm/generations/features/tailwind.feature.mjs +174 -0
  101. package/esm/generations/features/tailwind.feature.mjs.map +1 -0
  102. package/esm/generations/features/test.feature.mjs +96 -0
  103. package/esm/generations/features/test.feature.mjs.map +1 -0
  104. package/esm/generations/features/web.feature.mjs +179 -0
  105. package/esm/generations/features/web.feature.mjs.map +1 -0
  106. package/esm/generations/stubs.mjs +95 -8
  107. package/esm/generations/stubs.mjs.map +1 -1
  108. package/esm/http/index.d.mts +1 -0
  109. package/esm/http/index.mjs +1 -0
  110. package/esm/http/ready-report.d.mts +56 -0
  111. package/esm/http/ready-report.d.mts.map +1 -0
  112. package/esm/http/ready-report.mjs +24 -0
  113. package/esm/http/ready-report.mjs.map +1 -0
  114. package/esm/index.d.mts +2 -1
  115. package/esm/index.mjs +2 -1
  116. package/llms-full.txt +322 -1
  117. package/llms.txt +2 -0
  118. package/package.json +12 -12
  119. package/skills/use-request-locals/SKILL.md +24 -1
  120. package/skills/warlock-conventions/SKILL.md +5 -1
@@ -0,0 +1,9 @@
1
+ //#region ../core/src/generations/features/mysql.feature.ts
2
+ const mysqlFeature = {
3
+ description: "Installs mysql2 for MySQL database driver (Cascade Package)",
4
+ dependencies: { mysql2: "^3.5.0" }
5
+ };
6
+
7
+ //#endregion
8
+ export { mysqlFeature };
9
+ //# sourceMappingURL=mysql.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/mysql.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const mysqlFeature: FeatureDefinition = {\r\n description: \"Installs mysql2 for MySQL database driver (Cascade Package)\",\r\n dependencies: {\r\n mysql2: \"^3.5.0\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,eAAkC;CAC7C,aAAa;CACb,cAAc,EACZ,QAAQ,SACV;AACF"}
@@ -0,0 +1,43 @@
1
+ import { srcPath } from "../../utils/paths.mjs";
2
+ import "../../utils/index.mjs";
3
+ import { notificationControllersStub, notificationMigrationStub, notificationModelStub, notificationRoutesStub, notificationsConfigStub } from "../stubs.mjs";
4
+ import { migrationTimestamp } from "./shared/migration-timestamp.mjs";
5
+ import { colors } from "@mongez/copper";
6
+ import { ensureDirectoryAsync, fileExistsAsync, putFileAsync } from "@warlock.js/fs";
7
+
8
+ //#region ../core/src/generations/features/notifications.feature.ts
9
+ async function completeNotificationsInstallation(_options) {
10
+ const modelPath = srcPath("app/notifications/notification.model.ts");
11
+ if (await fileExistsAsync(modelPath)) {
12
+ console.log(`${colors.yellowBright("src/app/notifications")} already scaffolded, skipping model + migration...`);
13
+ return;
14
+ }
15
+ await ensureDirectoryAsync(srcPath("app/notifications"));
16
+ await putFileAsync(modelPath, notificationModelStub);
17
+ console.log(`${colors.green("✓")} Created src/app/notifications/notification.model.ts`);
18
+ await ensureDirectoryAsync(srcPath("app/notifications/migrations"));
19
+ const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;
20
+ await putFileAsync(srcPath("app/notifications/migrations", migrationFile), notificationMigrationStub);
21
+ console.log(`${colors.green("✓")} Created src/app/notifications/migrations/${migrationFile}`);
22
+ await ensureDirectoryAsync(srcPath("app/notifications/controllers"));
23
+ await putFileAsync(srcPath("app/notifications/controllers/notifications.controller.ts"), notificationControllersStub);
24
+ await putFileAsync(srcPath("app/notifications/routes.ts"), notificationRoutesStub);
25
+ console.log(`${colors.green("✓")} Created src/app/notifications/routes.ts + controllers`);
26
+ }
27
+ const notificationsFeature = {
28
+ description: "Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications",
29
+ requires: ["mail"],
30
+ dependencies: {
31
+ "@warlock.js/notifications": "~4.0.0",
32
+ "@warlock.js/auth": "~4.0.0"
33
+ },
34
+ ejectConfig: {
35
+ content: notificationsConfigStub,
36
+ name: "notifications"
37
+ },
38
+ onExecuting: completeNotificationsInstallation
39
+ };
40
+
41
+ //#endregion
42
+ export { notificationsFeature };
43
+ //# sourceMappingURL=notifications.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport { CommandActionData } from \"../../commands/types\";\r\nimport { srcPath } from \"../../utils\";\r\nimport {\r\n notificationControllersStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationRoutesStub,\r\n notificationsConfigStub,\r\n} from \"../stubs\";\r\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\r\nimport { FeatureDefinition } from \"./types\";\r\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`,\r\n );\r\n\r\n // 3. HTTP surface — the in-app read/dismiss endpoints (routes + controllers),\r\n // gated by authMiddleware. Delete if the app exposes notifications another way.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/notifications/controllers/notifications.controller.ts\"),\r\n notificationControllersStub,\r\n );\r\n await putFileAsync(srcPath(\"app/notifications/routes.ts\"), notificationRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/routes.ts + controllers`);\r\n}\r\n\r\nexport const notificationsFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default (needs nodemailer,\r\n // via the `mail` feature); the scaffolded routes are gated by\r\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": \"~4.0.0\",\r\n \"@warlock.js/auth\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n};\r\n"],"mappings":";;;;;;;;AAcA,eAAe,kCAAkC,UAA6B;CAC5E,MAAM,YAAY,QAAQ,yCAAyC;CAKnE,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,QAAQ,IACN,GAAG,OAAO,aAAa,uBAAuB,EAAE,mDAClD;EACA;CACF;CAGA,MAAM,qBAAqB,QAAQ,mBAAmB,CAAC;CACvD,MAAM,aAAa,WAAW,qBAAqB;CACnD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,qDAAqD;CAKtF,MAAM,qBAAqB,QAAQ,8BAA8B,CAAC;CAElE,MAAM,gBAAgB,GAAG,mBAAmB,EAAE;CAE9C,MAAM,aACJ,QAAQ,gCAAgC,aAAa,GACrD,yBACF;CACA,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,4CAA4C,eACnE;CAIA,MAAM,qBAAqB,QAAQ,+BAA+B,CAAC;CACnE,MAAM,aACJ,QAAQ,2DAA2D,GACnE,2BACF;CACA,MAAM,aAAa,QAAQ,6BAA6B,GAAG,sBAAsB;CACjF,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,uDAAuD;AAC1F;AAEA,MAAa,uBAA0C;CACrD,aACE;CAIF,UAAU,CAAC,MAAM;CACjB,cAAc;EACZ,6BAA6B;EAC7B,oBAAoB;CACtB;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;CACA,aAAa;AACf"}
@@ -0,0 +1,9 @@
1
+ //#region ../core/src/generations/features/postgres.feature.ts
2
+ const postgresFeature = {
3
+ description: "Installs pg for Postgres database (Cascade Package)",
4
+ dependencies: { pg: "^8.11.0" }
5
+ };
6
+
7
+ //#endregion
8
+ export { postgresFeature };
9
+ //# sourceMappingURL=postgres.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/postgres.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const postgresFeature: FeatureDefinition = {\r\n description: \"Installs pg for Postgres database (Cascade Package)\",\r\n dependencies: {\r\n pg: \"^8.11.0\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,kBAAqC;CAChD,aAAa;CACb,cAAc,EACZ,IAAI,UACN;AACF"}
@@ -0,0 +1,70 @@
1
+ import { rootPath } from "../../utils/paths.mjs";
2
+ import "../../utils/index.mjs";
3
+ import { colors } from "@mongez/copper";
4
+ import { ensureDirectoryAsync, fileExistsAsync, getJsonFileAsync, putFileAsync, putJsonFileAsync } from "@warlock.js/fs";
5
+
6
+ //#region ../core/src/generations/features/react-email.feature.ts
7
+ async function completeReactEmailInstallation(_options) {
8
+ const emailsFolderPath = rootPath("emails");
9
+ const sampleEmailPath = rootPath("emails/welcome-email.tsx");
10
+ if (!await fileExistsAsync(sampleEmailPath)) {
11
+ await ensureDirectoryAsync(emailsFolderPath);
12
+ await putFileAsync(sampleEmailPath, `import { Body, Container, Head, Html, Text } from "@react-email/components";
13
+ import { Tailwind } from "@react-email/tailwind";
14
+
15
+ interface WelcomeEmailProps {
16
+ name: string;
17
+ }
18
+
19
+ /**
20
+ * Sample welcome email component.
21
+ * Preview with: yarn email:preview
22
+ */
23
+ export default function WelcomeEmail({ name }: WelcomeEmailProps) {
24
+ return (
25
+ <Html>
26
+ <Head />
27
+ <Tailwind>
28
+ <Body className="bg-gray-100 font-sans">
29
+ <Container className="mx-auto max-w-xl py-8 px-4">
30
+ <Text className="text-2xl font-bold text-gray-900">
31
+ Welcome, {name}!
32
+ </Text>
33
+ <Text className="text-gray-600 mt-2">
34
+ You're all set. We're glad to have you on board.
35
+ </Text>
36
+ </Container>
37
+ </Body>
38
+ </Tailwind>
39
+ </Html>
40
+ );
41
+ }
42
+ `);
43
+ console.log(`${colors.green("✓")} Created emails/welcome-email.tsx`);
44
+ }
45
+ const tsconfigPath = rootPath("tsconfig.json");
46
+ const tsconfig = await getJsonFileAsync(tsconfigPath);
47
+ if (!tsconfig.include) tsconfig.include = [];
48
+ if (!tsconfig.include.includes("emails")) {
49
+ tsconfig.include.push("emails");
50
+ await putJsonFileAsync(tsconfigPath, tsconfig);
51
+ console.log(`${colors.green("✓")} Added "emails" to tsconfig.json include`);
52
+ }
53
+ }
54
+ const reactEmailFeature = {
55
+ description: "Installs react-email for building email templates with React and Tailwind",
56
+ requires: ["mail", "react"],
57
+ dependencies: {
58
+ "react-email": "^5.2.10",
59
+ "@react-email/components": "^1.0.11",
60
+ "@react-email/render": "^2.0.5",
61
+ "@react-email/tailwind": "^2.0.7"
62
+ },
63
+ devDependencies: { "@react-email/preview-server": "5.2.10" },
64
+ script: { "email:preview": "npx react-email dev" },
65
+ onExecuting: completeReactEmailInstallation
66
+ };
67
+
68
+ //#endregion
69
+ export { reactEmailFeature };
70
+ //# sourceMappingURL=react-email.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-email.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/react-email.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath } from \"../../utils\";\r\nimport { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The part of a project `tsconfig.json` this action patches.\r\n */\r\ntype ProjectTsConfig = {\r\n include?: string[];\r\n};\r\n\r\nasync function completeReactEmailInstallation(_options: CommandActionData) {\r\n // 1. Create emails/ folder with a sample component\r\n const emailsFolderPath = rootPath(\"emails\");\r\n const sampleEmailPath = rootPath(\"emails/welcome-email.tsx\");\r\n\r\n if (!(await fileExistsAsync(sampleEmailPath))) {\r\n await ensureDirectoryAsync(emailsFolderPath);\r\n await putFileAsync(\r\n sampleEmailPath,\r\n `import { Body, Container, Head, Html, Text } from \"@react-email/components\";\r\nimport { Tailwind } from \"@react-email/tailwind\";\r\n\r\ninterface WelcomeEmailProps {\r\n name: string;\r\n}\r\n\r\n/**\r\n * Sample welcome email component.\r\n * Preview with: yarn email:preview\r\n */\r\nexport default function WelcomeEmail({ name }: WelcomeEmailProps) {\r\n return (\r\n <Html>\r\n <Head />\r\n <Tailwind>\r\n <Body className=\"bg-gray-100 font-sans\">\r\n <Container className=\"mx-auto max-w-xl py-8 px-4\">\r\n <Text className=\"text-2xl font-bold text-gray-900\">\r\n Welcome, {name}!\r\n </Text>\r\n <Text className=\"text-gray-600 mt-2\">\r\n You're all set. We're glad to have you on board.\r\n </Text>\r\n </Container>\r\n </Body>\r\n </Tailwind>\r\n </Html>\r\n );\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created emails/welcome-email.tsx`);\r\n }\r\n\r\n // 2. Patch tsconfig.json — add \"emails\" to include if missing\r\n const tsconfigPath = rootPath(\"tsconfig.json\");\r\n const tsconfig = await getJsonFileAsync<ProjectTsConfig>(tsconfigPath);\r\n\r\n if (!tsconfig.include) {\r\n tsconfig.include = [];\r\n }\r\n\r\n if (!tsconfig.include.includes(\"emails\")) {\r\n tsconfig.include.push(\"emails\");\r\n await putJsonFileAsync(tsconfigPath, tsconfig);\r\n console.log(`${colors.green(\"✓\")} Added \"emails\" to tsconfig.json include`);\r\n }\r\n}\r\n\r\nexport const reactEmailFeature: FeatureDefinition = {\r\n description: \"Installs react-email for building email templates with React and Tailwind\",\r\n requires: [\"mail\", \"react\"],\r\n dependencies: {\r\n \"react-email\": \"^5.2.10\",\r\n \"@react-email/components\": \"^1.0.11\",\r\n \"@react-email/render\": \"^2.0.5\",\r\n \"@react-email/tailwind\": \"^2.0.7\",\r\n },\r\n devDependencies: {\r\n \"@react-email/preview-server\": \"5.2.10\",\r\n },\r\n script: {\r\n \"email:preview\": \"npx react-email dev\",\r\n },\r\n onExecuting: completeReactEmailInstallation,\r\n};\r\n"],"mappings":";;;;;;AAmBA,eAAe,+BAA+B,UAA6B;CAEzE,MAAM,mBAAmB,SAAS,QAAQ;CAC1C,MAAM,kBAAkB,SAAS,0BAA0B;CAE3D,IAAI,CAAE,MAAM,gBAAgB,eAAe,GAAI;EAC7C,MAAM,qBAAqB,gBAAgB;EAC3C,MAAM,aACJ,iBACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kCAAkC;CACrE;CAGA,MAAM,eAAe,SAAS,eAAe;CAC7C,MAAM,WAAW,MAAM,iBAAkC,YAAY;CAErE,IAAI,CAAC,SAAS,SACZ,SAAS,UAAU,CAAC;CAGtB,IAAI,CAAC,SAAS,QAAQ,SAAS,QAAQ,GAAG;EACxC,SAAS,QAAQ,KAAK,QAAQ;EAC9B,MAAM,iBAAiB,cAAc,QAAQ;EAC7C,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yCAAyC;CAC5E;AACF;AAEA,MAAa,oBAAuC;CAClD,aAAa;CACb,UAAU,CAAC,QAAQ,OAAO;CAC1B,cAAc;EACZ,eAAe;EACf,2BAA2B;EAC3B,uBAAuB;EACvB,yBAAyB;CAC3B;CACA,iBAAiB,EACf,+BAA+B,SACjC;CACA,QAAQ,EACN,iBAAiB,sBACnB;CACA,aAAa;AACf"}
@@ -0,0 +1,16 @@
1
+ //#region ../core/src/generations/features/react.feature.ts
2
+ const reactFeature = {
3
+ description: "Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML",
4
+ dependencies: {
5
+ react: "^19.2.3",
6
+ "react-dom": "^19.2.3"
7
+ },
8
+ devDependencies: {
9
+ "@types/react": "^19.2.7",
10
+ "@types/react-dom": "^19.2.3"
11
+ }
12
+ };
13
+
14
+ //#endregion
15
+ export { reactFeature };
16
+ //# sourceMappingURL=react.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/react.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const reactFeature: FeatureDefinition = {\r\n description:\r\n \"Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML\",\r\n dependencies: {\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,eAAkC;CAC7C,aACE;CACF,cAAc;EACZ,OAAO;EACP,aAAa;CACf;CACA,iBAAiB;EACf,gBAAgB;EAChB,oBAAoB;CACtB;AACF"}
@@ -0,0 +1,9 @@
1
+ //#region ../core/src/generations/features/redis.feature.ts
2
+ const redisFeature = {
3
+ description: "Installs redis for Redis cache driver (Cache Package)",
4
+ dependencies: { redis: "^4.6.13" }
5
+ };
6
+
7
+ //#endregion
8
+ export { redisFeature };
9
+ //# sourceMappingURL=redis.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/redis.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const redisFeature: FeatureDefinition = {\r\n description: \"Installs redis for Redis cache driver (Cache Package)\",\r\n dependencies: {\r\n redis: \"^4.6.13\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,eAAkC;CAC7C,aAAa;CACb,cAAc,EACZ,OAAO,UACT;AACF"}
@@ -0,0 +1,13 @@
1
+ //#region ../core/src/generations/features/s3.feature.ts
2
+ const s3Feature = {
3
+ description: "Installs AWS SDK for Cloud storage (Storage Package)",
4
+ dependencies: {
5
+ "@aws-sdk/client-s3": "^3.955.0",
6
+ "@aws-sdk/lib-storage": "^3.955.0",
7
+ "@aws-sdk/s3-request-presigner": "^3.955.0"
8
+ }
9
+ };
10
+
11
+ //#endregion
12
+ export { s3Feature };
13
+ //# sourceMappingURL=s3.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/s3.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const s3Feature: FeatureDefinition = {\r\n description: \"Installs AWS SDK for Cloud storage (Storage Package)\",\r\n dependencies: {\r\n \"@aws-sdk/client-s3\": \"^3.955.0\",\r\n \"@aws-sdk/lib-storage\": \"^3.955.0\",\r\n \"@aws-sdk/s3-request-presigner\": \"^3.955.0\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,YAA+B;CAC1C,aAAa;CACb,cAAc;EACZ,sBAAsB;EACtB,wBAAwB;EACxB,iCAAiC;CACnC;AACF"}
@@ -0,0 +1,9 @@
1
+ //#region ../core/src/generations/features/scheduler.feature.ts
2
+ const schedulerFeature = {
3
+ description: "Installs warlock scheduler for scheduling tasks",
4
+ dependencies: { "@warlock.js/scheduler": "~4.0.0" }
5
+ };
6
+
7
+ //#endregion
8
+ export { schedulerFeature };
9
+ //# sourceMappingURL=scheduler.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/scheduler.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const schedulerFeature: FeatureDefinition = {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": \"~4.0.0\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,mBAAsC;CACjD,aAAa;CACb,cAAc,EACZ,yBAAyB,SAC3B;AACF"}
@@ -0,0 +1,9 @@
1
+ //#region ../core/src/generations/features/ses.feature.ts
2
+ const sesFeature = {
3
+ description: "Installs AWS SES SDK for sending emails via Amazon SES",
4
+ dependencies: { "@aws-sdk/client-sesv2": "^3.1025.0" }
5
+ };
6
+
7
+ //#endregion
8
+ export { sesFeature };
9
+ //# sourceMappingURL=ses.feature.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ses.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/ses.feature.ts"],"sourcesContent":["import { FeatureDefinition } from \"./types\";\r\n\r\nexport const sesFeature: FeatureDefinition = {\r\n description: \"Installs AWS SES SDK for sending emails via Amazon SES\",\r\n dependencies: {\r\n \"@aws-sdk/client-sesv2\": \"^3.1025.0\",\r\n },\r\n};\r\n"],"mappings":";AAEA,MAAa,aAAgC;CAC3C,aAAa;CACb,cAAc,EACZ,yBAAyB,YAC3B;AACF"}
@@ -0,0 +1,397 @@
1
+ import { rootPath, srcPath } from "../../utils/paths.mjs";
2
+ import "../../utils/index.mjs";
3
+ import { colors } from "@mongez/copper";
4
+ import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
5
+
6
+ //#region ../core/src/generations/features/shadcn.feature.ts
7
+ /**
8
+ * `warlock add shadcn` installs the PREREQUISITES for shadcn/ui. It does not
9
+ * wrap `shadcn add`, and it never will.
10
+ *
11
+ * shadcn/ui is a copy-in generator with its own CLI and its own registry, not a
12
+ * dependency: components are written into your source tree and become yours the
13
+ * moment they land. Wrapping their CLI would make their moving target our bug
14
+ * reports, for a command that adds nothing but a rename.
15
+ *
16
+ * What is worth owning is the part their CLI gets wrong here. Measured against
17
+ * this framework's layout on 2026-08-25, `shadcn add button card` exited 0 and
18
+ * produced components that rendered COMPLETELY UNSTYLED. It writes components
19
+ * that import `cn` and reference `bg-primary` / `ring-ring`, but only
20
+ * `shadcn init` creates `lib/utils` and the theme tokens those names resolve
21
+ * against. The class was in the generated CSS and the element carried it, and
22
+ * the rule still evaluated to an empty `var()` — a silent failure with a zero
23
+ * exit code, which is the worst kind to hand a user.
24
+ *
25
+ * So this feature ships the five things that make `npx shadcn add <component>`
26
+ * work the first time:
27
+ *
28
+ * 1. `components.json` written for OUR layout (their defaults assume `src/app`
29
+ * or a bare `components/`, neither of which is where pages live here).
30
+ * 2. `src/web/lib/utils.ts` exporting `cn`.
31
+ * 3. The design tokens appended to `src/web/app.css`.
32
+ * 4. A `web/*` entry in tsconfig `paths`, so the generated imports typecheck.
33
+ * 5. The packages `init` would have installed — see `printNextStep` below.
34
+ * Skipping `init` is correct; skipping its dependency list was a bug, and
35
+ * it cost a generated button that imports `cva` and cannot compile.
36
+ *
37
+ * After that, the user talks to shadcn directly, and their docs are true.
38
+ */
39
+ /**
40
+ * The aliases shadcn's CLI rewrites every generated import against.
41
+ *
42
+ * These are the whole reason this feature exists. shadcn's defaults do not fit
43
+ * `src/web`, and no user would guess this mapping: the alias keys are shadcn's
44
+ * vocabulary, the values are tsconfig `paths` prefixes (hence `web/...`, not
45
+ * `src/web/...`), and `tailwind.css` is a real path from the project root
46
+ * (hence `src/web/app.css`, WITH the `src`). Getting one of them wrong produces
47
+ * components in the wrong folder importing `cn` from somewhere that does not
48
+ * exist.
49
+ *
50
+ * `tailwind.config` is deliberately empty: v4 is CSS-first and there is no
51
+ * config file for it to point at. `rsc: false` because these are SSR React
52
+ * pages rendered by the Warlock HTTP server, not React Server Components.
53
+ */
54
+ const componentsJsonStub = `{
55
+ "$schema": "https://ui.shadcn.com/schema.json",
56
+ "style": "new-york",
57
+ "rsc": false,
58
+ "tsx": true,
59
+ "tailwind": {
60
+ "config": "",
61
+ "css": "src/web/app.css",
62
+ "baseColor": "neutral",
63
+ "cssVariables": true,
64
+ "prefix": ""
65
+ },
66
+ "aliases": {
67
+ "components": "web/components",
68
+ "utils": "web/lib/utils",
69
+ "ui": "web/components/ui",
70
+ "lib": "web/lib",
71
+ "hooks": "web/hooks"
72
+ },
73
+ "iconLibrary": "lucide"
74
+ }
75
+ `;
76
+ /**
77
+ * `cn` — the one import every single shadcn component makes.
78
+ *
79
+ * `clsx` resolves the conditional/array/object class syntax, and `tailwind-merge`
80
+ * then de-duplicates conflicting Tailwind utilities so a caller's `className`
81
+ * actually beats the component's own default rather than depending on which one
82
+ * happens to come later in the generated stylesheet. Both halves are required:
83
+ * clsx alone leaves `px-2 px-4` in the attribute and the loser wins at random.
84
+ */
85
+ const cnUtilStub = `import { clsx, type ClassValue } from "clsx";
86
+ import { twMerge } from "tailwind-merge";
87
+
88
+ /**
89
+ * Merge class names, with later Tailwind utilities winning over earlier ones.
90
+ *
91
+ * Every shadcn/ui component imports this. Keep the export name and signature.
92
+ */
93
+ export function cn(...inputs: ClassValue[]) {
94
+ return twMerge(clsx(inputs));
95
+ }
96
+ `;
97
+ /**
98
+ * Marker for "the tokens are already in this stylesheet".
99
+ *
100
+ * A comment rather than a token name, because a user is free to retune every
101
+ * value below and we must still recognise our own block on a re-run.
102
+ */
103
+ const SHADCN_TOKENS_MARKER = "shadcn/ui design tokens";
104
+ /**
105
+ * The shadcn token set, as a plain `@theme` block with LITERAL values.
106
+ *
107
+ * Not `@theme inline` over `:root` custom properties, which is what shadcn's own
108
+ * `init` writes. That form was verified against this stylesheet pipeline on
109
+ * 2026-08-25 and it produces utilities that resolve to nothing: the class is in
110
+ * the generated CSS, the class is on the element, and the rule still evaluates
111
+ * to an empty `var()`. Literal values in `@theme` are what make `bg-primary`
112
+ * emit a colour instead of a dangling reference.
113
+ *
114
+ * Dark mode still works, and works the way shadcn expects: `@theme` emits these
115
+ * as real custom properties, so the `.dark` block re-declares the same
116
+ * `--color-*` names and the cascade does the rest. `@custom-variant dark` is
117
+ * what points Tailwind's `dark:` prefix at that class instead of the OS setting.
118
+ *
119
+ * Values are shadcn's `neutral` base. They are the starting point, not the
120
+ * answer — this is the block to edit when the project gets a real palette.
121
+ */
122
+ const shadcnTokensStub = `
123
+ /* ${SHADCN_TOKENS_MARKER} — edit freely, this is your palette now.
124
+
125
+ These are LITERAL values in a plain @theme block, on purpose. shadcn's own
126
+ \`init\` writes \`@theme inline\` over :root custom properties; that form emits
127
+ utilities which resolve to an empty var() here, so every component renders
128
+ unstyled while the class sits right there on the element. Do not convert it.
129
+ */
130
+ @custom-variant dark (&:is(.dark *));
131
+
132
+ @theme {
133
+ --color-background: oklch(1 0 0);
134
+ --color-foreground: oklch(0.145 0 0);
135
+ --color-card: oklch(1 0 0);
136
+ --color-card-foreground: oklch(0.145 0 0);
137
+ --color-popover: oklch(1 0 0);
138
+ --color-popover-foreground: oklch(0.145 0 0);
139
+ --color-primary: oklch(0.205 0 0);
140
+ --color-primary-foreground: oklch(0.985 0 0);
141
+ --color-secondary: oklch(0.97 0 0);
142
+ --color-secondary-foreground: oklch(0.205 0 0);
143
+ --color-muted: oklch(0.97 0 0);
144
+ --color-muted-foreground: oklch(0.556 0 0);
145
+ --color-accent: oklch(0.97 0 0);
146
+ --color-accent-foreground: oklch(0.205 0 0);
147
+ --color-destructive: oklch(0.577 0.245 27.325);
148
+ --color-destructive-foreground: oklch(0.985 0 0);
149
+ --color-border: oklch(0.922 0 0);
150
+ --color-input: oklch(0.922 0 0);
151
+ --color-ring: oklch(0.708 0 0);
152
+
153
+ --color-chart-1: oklch(0.646 0.222 41.116);
154
+ --color-chart-2: oklch(0.6 0.118 184.704);
155
+ --color-chart-3: oklch(0.398 0.07 227.392);
156
+ --color-chart-4: oklch(0.828 0.189 84.429);
157
+ --color-chart-5: oklch(0.769 0.188 70.08);
158
+
159
+ --color-sidebar: oklch(0.985 0 0);
160
+ --color-sidebar-foreground: oklch(0.145 0 0);
161
+ --color-sidebar-primary: oklch(0.205 0 0);
162
+ --color-sidebar-primary-foreground: oklch(0.985 0 0);
163
+ --color-sidebar-accent: oklch(0.97 0 0);
164
+ --color-sidebar-accent-foreground: oklch(0.205 0 0);
165
+ --color-sidebar-border: oklch(0.922 0 0);
166
+ --color-sidebar-ring: oklch(0.708 0 0);
167
+
168
+ /* shadcn components reach for rounded-lg/md/sm and expect them to track one
169
+ radius. Changing --radius-lg here also retunes Tailwind's own rounded-lg,
170
+ which is the intended trade: one radius scale per project, not two. */
171
+ --radius-sm: 0.375rem;
172
+ --radius-md: 0.5rem;
173
+ --radius-lg: 0.625rem;
174
+ --radius-xl: 1rem;
175
+ }
176
+
177
+ .dark {
178
+ --color-background: oklch(0.145 0 0);
179
+ --color-foreground: oklch(0.985 0 0);
180
+ --color-card: oklch(0.205 0 0);
181
+ --color-card-foreground: oklch(0.985 0 0);
182
+ --color-popover: oklch(0.205 0 0);
183
+ --color-popover-foreground: oklch(0.985 0 0);
184
+ --color-primary: oklch(0.922 0 0);
185
+ --color-primary-foreground: oklch(0.205 0 0);
186
+ --color-secondary: oklch(0.269 0 0);
187
+ --color-secondary-foreground: oklch(0.985 0 0);
188
+ --color-muted: oklch(0.269 0 0);
189
+ --color-muted-foreground: oklch(0.708 0 0);
190
+ --color-accent: oklch(0.269 0 0);
191
+ --color-accent-foreground: oklch(0.985 0 0);
192
+ --color-destructive: oklch(0.704 0.191 22.216);
193
+ --color-destructive-foreground: oklch(0.985 0 0);
194
+ --color-border: oklch(1 0 0 / 10%);
195
+ --color-input: oklch(1 0 0 / 15%);
196
+ --color-ring: oklch(0.556 0 0);
197
+
198
+ --color-chart-1: oklch(0.488 0.243 264.376);
199
+ --color-chart-2: oklch(0.696 0.17 162.48);
200
+ --color-chart-3: oklch(0.769 0.188 70.08);
201
+ --color-chart-4: oklch(0.627 0.265 303.9);
202
+ --color-chart-5: oklch(0.645 0.246 16.439);
203
+
204
+ --color-sidebar: oklch(0.205 0 0);
205
+ --color-sidebar-foreground: oklch(0.985 0 0);
206
+ --color-sidebar-primary: oklch(0.488 0.243 264.376);
207
+ --color-sidebar-primary-foreground: oklch(0.985 0 0);
208
+ --color-sidebar-accent: oklch(0.269 0 0);
209
+ --color-sidebar-accent-foreground: oklch(0.985 0 0);
210
+ --color-sidebar-border: oklch(1 0 0 / 10%);
211
+ --color-sidebar-ring: oklch(0.556 0 0);
212
+ }
213
+ `;
214
+ /**
215
+ * Write `components.json` — the sentinel for "this feature already ran".
216
+ *
217
+ * Nothing in the project template creates this file, so its presence means
218
+ * `add shadcn` has been here and a human may since have retuned the aliases,
219
+ * the style, or the base colour. It is never rewritten: shadcn's CLI reads this
220
+ * file on every `add`, so overwriting it would silently relocate a project's
221
+ * component folder out from under the components already in it.
222
+ */
223
+ async function writeComponentsJson() {
224
+ const componentsJsonPath = rootPath("components.json");
225
+ if (await fileExistsAsync(componentsJsonPath)) {
226
+ console.log(`${colors.yellowBright("components.json")} already exists, skipping...`);
227
+ return;
228
+ }
229
+ await putFileAsync(componentsJsonPath, componentsJsonStub);
230
+ console.log(`${colors.green("✓")} Created components.json`);
231
+ }
232
+ /**
233
+ * Write `src/web/lib/utils.ts`.
234
+ *
235
+ * Guarded on its own rather than on the sentinel above, because `lib/utils` is
236
+ * a name a project may well already own — and if it does, whatever is in there
237
+ * is user code with other callers. We print instead of merging.
238
+ */
239
+ async function writeCnUtil() {
240
+ const utilsFile = srcPath("web/lib/utils.ts");
241
+ if (await fileExistsAsync(utilsFile)) {
242
+ const current = await getFileAsync(utilsFile).catch(() => "");
243
+ if (/export\s+(function|const)\s+cn\b/.test(current)) console.log(`${colors.yellowBright("src/web/lib/utils.ts")} already exports cn, skipping...`);
244
+ else console.log(`${colors.yellowBright("!")} ${colors.yellowBright("src/web/lib/utils.ts")} exists but does not export ${colors.yellowBright("cn")} — add it yourself:\n export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
245
+ Every shadcn component imports it, and none of them will compile until it is there.`);
246
+ return;
247
+ }
248
+ await ensureDirectoryAsync(srcPath("web/lib"));
249
+ await putFileAsync(utilsFile, cnUtilStub);
250
+ console.log(`${colors.green("✓")} Created src/web/lib/utils.ts`);
251
+ }
252
+ /**
253
+ * Append the token block to `src/web/app.css`.
254
+ *
255
+ * The stylesheet is guaranteed to exist by the time this runs: `requires:
256
+ * ["tailwind"]` makes the add command resolve `tailwind` first and run its
257
+ * `onExecuting` ahead of this one, and that is what creates the file. The check
258
+ * below is for a stylesheet a human has since moved or deleted — worth a
259
+ * printed instruction, not a failure.
260
+ *
261
+ * Appended, never rewritten. Everything already in that file is either
262
+ * Tailwind's own `@import` or the project's design system.
263
+ */
264
+ async function appendThemeTokens() {
265
+ const cssFile = srcPath("web/app.css");
266
+ if (!await fileExistsAsync(cssFile)) {
267
+ console.log(`${colors.yellowBright("!")} ${colors.yellowBright("src/web/app.css")} not found — append the shadcn token block to your Tailwind stylesheet yourself.
268
+ Without the tokens, shadcn components render unstyled: the classes are emitted and applied, but \`bg-primary\` and friends resolve to an empty var().`);
269
+ return;
270
+ }
271
+ const current = await getFileAsync(cssFile);
272
+ if (current.includes(SHADCN_TOKENS_MARKER) || current.includes("--color-primary-foreground")) {
273
+ console.log(`${colors.yellowBright("src/web/app.css")} already has the tokens, skipping...`);
274
+ return;
275
+ }
276
+ await putFileAsync(cssFile, `${current.trimEnd()}\n${shadcnTokensStub}`);
277
+ console.log(`${colors.green("✓")} Appended the shadcn tokens to src/web/app.css`);
278
+ }
279
+ /** The tsconfig `paths` entry the generated imports resolve through. */
280
+ const WEB_PATH_ALIAS = "\"web/*\": [\"./src/web/*\"]";
281
+ /**
282
+ * Add `web/*` to tsconfig `compilerOptions.paths`.
283
+ *
284
+ * The template declares only `app/*`, so every import shadcn generates against
285
+ * the aliases above (`web/lib/utils`, `web/components/ui/button`) would fail to
286
+ * typecheck the moment it lands. This is the first `add` feature to patch
287
+ * `paths` rather than `include`.
288
+ *
289
+ * String surgery, and NOT the parse-and-write that the `include` patches use.
290
+ * The project template's `tsconfig.json` carries `//` comments — it is JSONC,
291
+ * and `JSON.parse` throws on it — so a parse-first patch would fail on exactly
292
+ * the projects this feature is for. Editing the text also preserves those
293
+ * comments, which are load-bearing documentation in that file.
294
+ */
295
+ async function addWebPathAlias() {
296
+ const tsconfigPath = rootPath("tsconfig.json");
297
+ const printManualInstruction = (reason) => {
298
+ console.log(`${colors.yellowBright("!")} ${colors.yellowBright("tsconfig.json")} ${reason} — add this to \`compilerOptions.paths\` yourself:
299
+ ${WEB_PATH_ALIAS}\n Without it, every import shadcn generates against the \`web/*\` aliases fails to typecheck.`);
300
+ };
301
+ if (!await fileExistsAsync(tsconfigPath)) {
302
+ printManualInstruction("not found");
303
+ return;
304
+ }
305
+ const current = await getFileAsync(tsconfigPath);
306
+ if (/["']web\/\*["']\s*:/.test(current)) {
307
+ console.log(`${colors.yellowBright("tsconfig.json")} already maps web/*, skipping...`);
308
+ return;
309
+ }
310
+ let next;
311
+ if (/"paths"\s*:\s*\{/.test(current)) next = current.replace(/"paths"\s*:\s*\{/, `$&\n ${WEB_PATH_ALIAS},`);
312
+ else if (/"compilerOptions"\s*:\s*\{/.test(current)) next = current.replace(/"compilerOptions"\s*:\s*\{/, `$&\n "paths": {\n ${WEB_PATH_ALIAS}\n },`);
313
+ else {
314
+ printManualInstruction("has no recognisable compilerOptions block");
315
+ return;
316
+ }
317
+ await putFileAsync(tsconfigPath, next);
318
+ console.log(`${colors.green("✓")} Added ${WEB_PATH_ALIAS} to tsconfig.json paths`);
319
+ }
320
+ /**
321
+ * Tell the user the next command is theirs to run.
322
+ *
323
+ * This is the seam. Everything above is prerequisite; from here the shadcn docs
324
+ * apply verbatim, which is the entire point of not wrapping their CLI.
325
+ *
326
+ * WHY THIS FEATURE DECLARES cva AND lucide-react, and where the list came from.
327
+ *
328
+ * shadcn's registry splits dependencies across two levels. Each component item
329
+ * (`.../new-york-v4/button.json`) declares only what that file pulls beyond the
330
+ * baseline — for button, `radix-ui` and nothing else. Everything the baseline
331
+ * assumes lives on the STYLE INDEX (`.../new-york-v4/index.json`), which is
332
+ * fetched by `init` and only by `init`:
333
+ *
334
+ * dependencies: class-variance-authority, lucide-react, radix-ui
335
+ * devDependencies: tw-animate-css, shadcn
336
+ *
337
+ * We skip `init` on purpose — it would rewrite components.json and replace the
338
+ * literal tokens with the `@theme inline` block that resolves to nothing here.
339
+ * Skipping it is right; inheriting nothing from it was the bug. `shadcn add
340
+ * button` exits 0 and writes `import { cva } from "class-variance-authority"`
341
+ * against a package no one installed. Zero exit code, TS2307, blank page.
342
+ *
343
+ * `radix-ui` stays off our list: it is the one style-index dependency that is
344
+ * ALSO declared per-component, so `add` really does install it on demand, and
345
+ * declaring it here would pull the whole primitive set into projects using two
346
+ * components. `shadcn` itself stays off too — it is the CLI, and the user is
347
+ * invoking it via `npx`.
348
+ *
349
+ * `tw-animate-css` was checked and deliberately EXCLUDED. It is not imported by
350
+ * any component; it is a plain stylesheet whose only entry point is the
351
+ * `@import "tw-animate-css"` line that `init` writes into the CSS — and we do
352
+ * not write that line, so the package would install and never load. Nothing
353
+ * fails to compile or render without it. What you lose is the enter/exit
354
+ * animation on overlay components (dialog, dropdown, tooltip, sheet): their
355
+ * `animate-in` / `fade-in-0` classes are simply never generated, so the overlay
356
+ * appears instantly instead of fading. That is opt-in, and the note below is how
357
+ * a user opts in — the `@import` has to go at the TOP of app.css, next to
358
+ * Tailwind's own, which is why this feature cannot append it to the token block.
359
+ */
360
+ function printNextStep() {
361
+ console.log(`\n${colors.green("✓")} shadcn/ui prerequisites are in place. Add components with shadcn's own CLI:\n ${colors.yellowBright("npx shadcn@latest add button card")}\n Skip \`shadcn init\` — this feature did its job, and running it would rewrite components.json
362
+ and replace the theme tokens with an \`@theme inline\` block that resolves to nothing here.
363
+ ${colors.yellowBright("class-variance-authority")} and ${colors.yellowBright("lucide-react")} are already installed: shadcn declares\n them on the style index that only \`init\` reads, so \`add\` would never install them for you.
364
+ ${colors.yellowBright("radix-ui")} (the unified package, not @radix-ui/react-*) IS declared per component,\n so shadcn's CLI installs that one itself as each component needs it.
365
+ For overlay animations, add ${colors.yellowBright("tw-animate-css")} and put ${colors.yellowBright("@import \"tw-animate-css\";")}\n at the TOP of src/web/app.css, under the Tailwind import. Without it dialogs and dropdowns
366
+ still work, they just appear instantly instead of animating.`);
367
+ }
368
+ /**
369
+ * Lay the ground shadcn's CLI expects to find, and nothing more.
370
+ *
371
+ * Four files, none of which needs `node_modules` to be populated: on the
372
+ * `create-warlock` path this runs under `--no-install`, so the dependencies
373
+ * declared below are only recorded in `package.json` and nothing here may
374
+ * import, resolve, or execute shadcn, clsx, or Tailwind.
375
+ */
376
+ async function completeShadcnInstallation(_options) {
377
+ await writeComponentsJson();
378
+ await writeCnUtil();
379
+ await appendThemeTokens();
380
+ await addWebPathAlias();
381
+ printNextStep();
382
+ }
383
+ const shadcnFeature = {
384
+ description: "Sets up the prerequisites for shadcn/ui so `npx shadcn add <component>` works first time: components.json aliased to src/web, src/web/lib/utils.ts (cn), the design tokens in src/web/app.css, and a web/* tsconfig path. It does NOT wrap shadcn's CLI — components stay theirs to generate and yours to own.",
385
+ requires: ["tailwind"],
386
+ dependencies: {
387
+ clsx: "^2.1.1",
388
+ "tailwind-merge": "^3.3.1",
389
+ "class-variance-authority": "^0.7.1",
390
+ "lucide-react": "^1.34.0"
391
+ },
392
+ onExecuting: completeShadcnInstallation
393
+ };
394
+
395
+ //#endregion
396
+ export { shadcnFeature };
397
+ //# sourceMappingURL=shadcn.feature.mjs.map