@geekmidas/cli 0.54.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +26 -5
  3. package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
  4. package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
  5. package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
  6. package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
  7. package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
  8. package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
  9. package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
  10. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
  11. package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
  12. package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
  13. package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
  14. package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
  15. package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
  16. package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
  17. package/dist/Route53Provider-CpRIqu69.cjs +157 -0
  18. package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
  19. package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
  20. package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
  21. package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
  22. package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
  23. package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
  24. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
  25. package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
  26. package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
  27. package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
  28. package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
  29. package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
  30. package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
  31. package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
  32. package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
  33. package/dist/config.cjs +2 -2
  34. package/dist/config.d.cts +1 -1
  35. package/dist/config.d.mts +2 -2
  36. package/dist/config.mjs +2 -2
  37. package/dist/credentials-C8DWtnMY.cjs +174 -0
  38. package/dist/credentials-C8DWtnMY.cjs.map +1 -0
  39. package/dist/credentials-DT1dSxIx.mjs +126 -0
  40. package/dist/credentials-DT1dSxIx.mjs.map +1 -0
  41. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  42. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  43. package/dist/deploy/sniffer-loader.cjs +1 -1
  44. package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
  45. package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
  46. package/dist/dokploy-api-CHa8G51l.mjs +3 -0
  47. package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
  48. package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
  49. package/dist/dokploy-api-CWc02yyg.cjs +3 -0
  50. package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
  51. package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
  52. package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
  53. package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
  54. package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  55. package/dist/encryption-UUmaWAmz.mjs +3 -0
  56. package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
  57. package/dist/index-B5rGIc4g.d.cts.map +1 -0
  58. package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
  59. package/dist/index-KFEbMIRa.d.mts.map +1 -0
  60. package/dist/index.cjs +2265 -658
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2242 -635
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
  65. package/dist/openapi-BMFmLnX6.mjs.map +1 -0
  66. package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
  67. package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
  68. package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
  69. package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
  70. package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
  71. package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
  72. package/dist/openapi-react-query.cjs +1 -1
  73. package/dist/openapi-react-query.mjs +1 -1
  74. package/dist/openapi.cjs +3 -3
  75. package/dist/openapi.d.cts +1 -1
  76. package/dist/openapi.d.mts +2 -2
  77. package/dist/openapi.mjs +3 -3
  78. package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
  79. package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
  80. package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
  81. package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
  82. package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
  83. package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
  84. package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
  85. package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
  86. package/dist/workspace/index.cjs +1 -1
  87. package/dist/workspace/index.d.cts +1 -1
  88. package/dist/workspace/index.d.mts +2 -2
  89. package/dist/workspace/index.mjs +1 -1
  90. package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
  91. package/dist/workspace-BFRUOOrh.cjs.map +1 -0
  92. package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
  93. package/dist/workspace-DAxG3_H2.mjs.map +1 -0
  94. package/package.json +14 -8
  95. package/scripts/sync-versions.ts +86 -0
  96. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  97. package/src/deploy/CachedStateProvider.ts +86 -0
  98. package/src/deploy/LocalStateProvider.ts +57 -0
  99. package/src/deploy/SSMStateProvider.ts +93 -0
  100. package/src/deploy/StateProvider.ts +171 -0
  101. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  102. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  103. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  104. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  105. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  106. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  107. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  108. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  109. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  110. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  111. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  112. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  113. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  114. package/src/deploy/__tests__/state.spec.ts +13 -5
  115. package/src/deploy/dns/DnsProvider.ts +163 -0
  116. package/src/deploy/dns/HostingerProvider.ts +100 -0
  117. package/src/deploy/dns/Route53Provider.ts +256 -0
  118. package/src/deploy/dns/index.ts +257 -165
  119. package/src/deploy/env-resolver.ts +12 -5
  120. package/src/deploy/index.ts +16 -13
  121. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  122. package/src/deploy/sniffer-routes-worker.ts +104 -0
  123. package/src/deploy/sniffer.ts +77 -55
  124. package/src/deploy/state-commands.ts +274 -0
  125. package/src/dev/__tests__/entry.spec.ts +8 -2
  126. package/src/dev/__tests__/index.spec.ts +1 -3
  127. package/src/dev/index.ts +9 -3
  128. package/src/docker/__tests__/templates.spec.ts +3 -1
  129. package/src/index.ts +88 -0
  130. package/src/init/__tests__/generators.spec.ts +273 -0
  131. package/src/init/__tests__/init.spec.ts +3 -3
  132. package/src/init/generators/auth.ts +1 -0
  133. package/src/init/generators/config.ts +2 -0
  134. package/src/init/generators/models.ts +6 -1
  135. package/src/init/generators/monorepo.ts +3 -0
  136. package/src/init/generators/ui.ts +1472 -0
  137. package/src/init/generators/web.ts +134 -87
  138. package/src/init/index.ts +22 -3
  139. package/src/init/templates/api.ts +109 -3
  140. package/src/init/versions.ts +25 -53
  141. package/src/openapi.ts +99 -13
  142. package/src/workspace/__tests__/schema.spec.ts +107 -0
  143. package/src/workspace/schema.ts +314 -4
  144. package/src/workspace/types.ts +22 -36
  145. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  146. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  147. package/dist/encryption-CQXBZGkt.mjs +0 -3
  148. package/dist/index-A70abJ1m.d.mts.map +0 -1
  149. package/dist/index-pOA56MWT.d.cts.map +0 -1
  150. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  151. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  152. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  153. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  154. package/tsconfig.tsbuildinfo +0 -1
package/dist/index.mjs CHANGED
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env -S npx tsx
2
- import { __require, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-DLFRaDc-.mjs";
3
- import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-C3LSBNSl.mjs";
4
- import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, OpenApiTsGenerator, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-C3C-BzIZ.mjs";
5
- import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-Dhst7BhI.mjs";
6
- import { DokployApi } from "./dokploy-api-94KzmTVf.mjs";
7
- import { encryptSecrets } from "./encryption-BC4MAODn.mjs";
8
- import { generateReactQueryCommand } from "./openapi-react-query-ZoP9DPbY.mjs";
2
+ import { __require, getAppBuildOrder, getDependencyEnvVars, getDeployTargetError, isDeployTargetSupported } from "./workspace-DAxG3_H2.mjs";
3
+ import { getAppNameFromCwd, loadAppConfig, loadConfig, loadWorkspaceConfig, parseModuleConfig } from "./config-C6awcFBx.mjs";
4
+ import { getCredentialsPath, getDokployCredentials, getDokployRegistryId, getDokployToken, removeDokployCredentials, storeDokployCredentials, storeDokployRegistryId } from "./credentials-DT1dSxIx.mjs";
5
+ import { ConstructGenerator, EndpointGenerator, OPENAPI_OUTPUT_PATH, OpenApiTsGenerator, generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi-BMFmLnX6.mjs";
6
+ import { getKeyPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets } from "./storage-BMW6yLu3.mjs";
7
+ import { DokployApi } from "./dokploy-api-7k3t7_zd.mjs";
8
+ import { encryptSecrets } from "./encryption-JtMsiGNp.mjs";
9
+ import { CachedStateProvider } from "./CachedStateProvider-DVyKfaMm.mjs";
10
+ import { generateReactQueryCommand } from "./openapi-react-query-DGEkD39r.mjs";
9
11
  import { createRequire } from "node:module";
10
- import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
12
+ import { copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
11
13
  import { basename, dirname, join, parse, relative, resolve } from "node:path";
12
14
  import { Command } from "commander";
13
15
  import { stdin, stdout } from "node:process";
14
16
  import * as readline from "node:readline/promises";
15
17
  import { mkdir, readFile, writeFile } from "node:fs/promises";
16
- import { homedir } from "node:os";
17
18
  import { execSync, spawn } from "node:child_process";
18
19
  import { createServer } from "node:net";
19
20
  import chokidar from "chokidar";
20
21
  import { config } from "dotenv";
21
22
  import fg from "fast-glob";
22
23
  import { Cron } from "@geekmidas/constructs/crons";
23
- import { Endpoint } from "@geekmidas/constructs/endpoints";
24
24
  import { Function } from "@geekmidas/constructs/functions";
25
25
  import { Subscriber } from "@geekmidas/constructs/subscribers";
26
26
  import { createHash, randomBytes } from "node:crypto";
@@ -31,7 +31,7 @@ import prompts from "prompts";
31
31
 
32
32
  //#region package.json
33
33
  var name = "@geekmidas/cli";
34
- var version = "0.54.0";
34
+ var version = "1.0.0";
35
35
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
36
36
  var private$1 = false;
37
37
  var type = "module";
@@ -65,6 +65,8 @@ var exports = {
65
65
  var bin = { "gkm": "./dist/index.cjs" };
66
66
  var scripts = {
67
67
  "ts": "tsc --noEmit --skipLibCheck src/**/*.ts",
68
+ "sync-versions": "tsx scripts/sync-versions.ts",
69
+ "prebuild": "pnpm sync-versions",
68
70
  "test": "vitest",
69
71
  "test:once": "vitest run",
70
72
  "test:coverage": "vitest run --coverage"
@@ -75,6 +77,9 @@ var repository = {
75
77
  };
76
78
  var dependencies = {
77
79
  "@apidevtools/swagger-parser": "^10.1.0",
80
+ "@aws-sdk/client-route-53": "~3.971.0",
81
+ "@aws-sdk/client-ssm": "~3.971.0",
82
+ "@aws-sdk/credential-providers": "~3.971.0",
78
83
  "@geekmidas/constructs": "workspace:~",
79
84
  "@geekmidas/envkit": "workspace:~",
80
85
  "@geekmidas/errors": "workspace:~",
@@ -88,7 +93,8 @@ var dependencies = {
88
93
  "lodash.kebabcase": "^4.1.1",
89
94
  "openapi-typescript": "^7.4.2",
90
95
  "pg": "~8.17.1",
91
- "prompts": "~2.4.2"
96
+ "prompts": "~2.4.2",
97
+ "tsx": "~4.20.3"
92
98
  };
93
99
  var devDependencies = {
94
100
  "@geekmidas/testkit": "workspace:*",
@@ -118,138 +124,6 @@ var package_default = {
118
124
  peerDependenciesMeta
119
125
  };
120
126
 
121
- //#endregion
122
- //#region src/auth/credentials.ts
123
- /**
124
- * Get the path to the credentials directory
125
- */
126
- function getCredentialsDir(options) {
127
- const root = options?.root ?? homedir();
128
- return join(root, ".gkm");
129
- }
130
- /**
131
- * Get the path to the credentials file
132
- */
133
- function getCredentialsPath(options) {
134
- return join(getCredentialsDir(options), "credentials.json");
135
- }
136
- /**
137
- * Ensure the credentials directory exists
138
- */
139
- function ensureCredentialsDir(options) {
140
- const dir = getCredentialsDir(options);
141
- if (!existsSync(dir)) mkdirSync(dir, {
142
- recursive: true,
143
- mode: 448
144
- });
145
- }
146
- /**
147
- * Read stored credentials from disk
148
- */
149
- async function readCredentials(options) {
150
- const path = getCredentialsPath(options);
151
- if (!existsSync(path)) return {};
152
- try {
153
- const content = await readFile(path, "utf-8");
154
- return JSON.parse(content);
155
- } catch {
156
- return {};
157
- }
158
- }
159
- /**
160
- * Write credentials to disk
161
- */
162
- async function writeCredentials(credentials, options) {
163
- ensureCredentialsDir(options);
164
- const path = getCredentialsPath(options);
165
- await writeFile(path, JSON.stringify(credentials, null, 2), { mode: 384 });
166
- }
167
- /**
168
- * Store Dokploy credentials
169
- */
170
- async function storeDokployCredentials(token, endpoint, options) {
171
- const credentials = await readCredentials(options);
172
- credentials.dokploy = {
173
- token,
174
- endpoint,
175
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
176
- };
177
- await writeCredentials(credentials, options);
178
- }
179
- /**
180
- * Get stored Dokploy credentials
181
- */
182
- async function getDokployCredentials(options) {
183
- const credentials = await readCredentials(options);
184
- if (!credentials.dokploy) return null;
185
- return {
186
- token: credentials.dokploy.token,
187
- endpoint: credentials.dokploy.endpoint,
188
- registryId: credentials.dokploy.registryId
189
- };
190
- }
191
- /**
192
- * Remove Dokploy credentials
193
- */
194
- async function removeDokployCredentials(options) {
195
- const credentials = await readCredentials(options);
196
- if (!credentials.dokploy) return false;
197
- delete credentials.dokploy;
198
- await writeCredentials(credentials, options);
199
- return true;
200
- }
201
- /**
202
- * Get Dokploy API token, checking stored credentials first, then environment
203
- */
204
- async function getDokployToken(options) {
205
- const envToken = process.env.DOKPLOY_API_TOKEN;
206
- if (envToken) return envToken;
207
- const stored = await getDokployCredentials(options);
208
- if (stored) return stored.token;
209
- return null;
210
- }
211
- /**
212
- * Store Dokploy registry ID
213
- */
214
- async function storeDokployRegistryId(registryId, options) {
215
- const credentials = await readCredentials(options);
216
- if (!credentials.dokploy) throw new Error("Dokploy credentials not found. Run \"gkm login --service dokploy\" first.");
217
- credentials.dokploy.registryId = registryId;
218
- await writeCredentials(credentials, options);
219
- }
220
- /**
221
- * Get Dokploy registry ID from stored credentials
222
- */
223
- async function getDokployRegistryId(options) {
224
- const stored = await getDokployCredentials(options);
225
- return stored?.registryId ?? void 0;
226
- }
227
- /**
228
- * Store Hostinger API token
229
- *
230
- * @param token - API token from hpanel.hostinger.com/profile/api
231
- */
232
- async function storeHostingerToken(token, options) {
233
- const credentials = await readCredentials(options);
234
- credentials.hostinger = {
235
- token,
236
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
237
- };
238
- await writeCredentials(credentials, options);
239
- }
240
- /**
241
- * Get stored Hostinger API token
242
- *
243
- * Checks environment variable first (HOSTINGER_API_TOKEN),
244
- * then falls back to stored credentials.
245
- */
246
- async function getHostingerToken(options) {
247
- const envToken = process.env.HOSTINGER_API_TOKEN;
248
- if (envToken) return envToken;
249
- const credentials = await readCredentials(options);
250
- return credentials.hostinger?.token ?? null;
251
- }
252
-
253
127
  //#endregion
254
128
  //#region src/auth/index.ts
255
129
  const logger$11 = console;
@@ -257,7 +131,7 @@ const logger$11 = console;
257
131
  * Validate Dokploy token by making a test API call
258
132
  */
259
133
  async function validateDokployToken(endpoint, token) {
260
- const { DokployApi: DokployApi$1 } = await import("./dokploy-api-CItuaWTq.mjs");
134
+ const { DokployApi: DokployApi$1 } = await import("./dokploy-api-CHa8G51l.mjs");
261
135
  const api = new DokployApi$1({
262
136
  baseUrl: endpoint,
263
137
  token
@@ -1251,13 +1125,13 @@ async function validateFrontendApp(appName, appPath, workspaceRoot) {
1251
1125
  if (!hasConfigFile) errors.push(`Next.js config file not found. Expected one of: ${NEXTJS_CONFIG_FILES.join(", ")}`);
1252
1126
  const packageJsonPath = join(fullPath, "package.json");
1253
1127
  if (existsSync(packageJsonPath)) try {
1254
- const pkg$1 = __require(packageJsonPath);
1128
+ const pkg = __require(packageJsonPath);
1255
1129
  const deps = {
1256
- ...pkg$1.dependencies,
1257
- ...pkg$1.devDependencies
1130
+ ...pkg.dependencies,
1131
+ ...pkg.devDependencies
1258
1132
  };
1259
1133
  if (!deps.next) errors.push("Next.js not found in dependencies. Run: pnpm add next react react-dom");
1260
- if (!pkg$1.scripts?.dev) warnings.push("No \"dev\" script found in package.json. Turbo expects a \"dev\" script to run.");
1134
+ if (!pkg.scripts?.dev) warnings.push("No \"dev\" script found in package.json. Turbo expects a \"dev\" script to run.");
1261
1135
  } catch {
1262
1136
  errors.push(`Failed to read package.json at ${packageJsonPath}`);
1263
1137
  }
@@ -2152,7 +2026,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2152
2026
  let masterKey;
2153
2027
  if (context.production?.bundle && !skipBundle) {
2154
2028
  logger$7.log(`\n📦 Bundling production server...`);
2155
- const { bundleServer } = await import("./bundler-DGry2vaR.mjs");
2029
+ const { bundleServer } = await import("./bundler-BqTN5Dj5.mjs");
2156
2030
  const allConstructs = [
2157
2031
  ...endpoints.map((e) => e.construct),
2158
2032
  ...functions.map((f) => f.construct),
@@ -2280,37 +2154,6 @@ function getAppOutputPath(workspace, _appName, app) {
2280
2154
  //#endregion
2281
2155
  //#region src/deploy/state.ts
2282
2156
  /**
2283
- * Get the state file path for a stage
2284
- */
2285
- function getStateFilePath(workspaceRoot, stage) {
2286
- return join(workspaceRoot, ".gkm", `deploy-${stage}.json`);
2287
- }
2288
- /**
2289
- * Read the deploy state for a stage
2290
- * Returns null if state file doesn't exist
2291
- */
2292
- async function readStageState(workspaceRoot, stage) {
2293
- const filePath = getStateFilePath(workspaceRoot, stage);
2294
- try {
2295
- const content = await readFile(filePath, "utf-8");
2296
- return JSON.parse(content);
2297
- } catch (error) {
2298
- if (error.code === "ENOENT") return null;
2299
- console.warn(`Warning: Could not read deploy state: ${error}`);
2300
- return null;
2301
- }
2302
- }
2303
- /**
2304
- * Write the deploy state for a stage
2305
- */
2306
- async function writeStageState(workspaceRoot, stage, state) {
2307
- const filePath = getStateFilePath(workspaceRoot, stage);
2308
- const dir = join(workspaceRoot, ".gkm");
2309
- await mkdir(dir, { recursive: true });
2310
- state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2311
- await writeFile(filePath, JSON.stringify(state, null, 2));
2312
- }
2313
- /**
2314
2157
  * Create a new empty state for a stage
2315
2158
  */
2316
2159
  function createEmptyState(stage, environmentId) {
@@ -2405,155 +2248,90 @@ function isDnsVerified(state, hostname, serverIp) {
2405
2248
  }
2406
2249
 
2407
2250
  //#endregion
2408
- //#region src/deploy/dns/hostinger-api.ts
2251
+ //#region src/deploy/dns/DnsProvider.ts
2409
2252
  /**
2410
- * Hostinger DNS API client
2253
+ * Check if value is a DnsProvider implementation.
2254
+ */
2255
+ function isDnsProvider(value) {
2256
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function";
2257
+ }
2258
+ /**
2259
+ * Create a DNS provider based on configuration.
2411
2260
  *
2412
- * API Documentation: https://developers.hostinger.com/
2413
- * Authentication: Bearer token from hpanel.hostinger.com/profile/api
2261
+ * - 'hostinger': HostingerProvider
2262
+ * - 'route53': Route53Provider
2263
+ * - 'manual': Returns null (user handles DNS)
2264
+ * - Custom: Use provided DnsProvider implementation
2265
+ */
2266
+ async function createDnsProvider(options) {
2267
+ const { config: config$1 } = options;
2268
+ if (config$1.provider === "manual") return null;
2269
+ if (isDnsProvider(config$1.provider)) return config$1.provider;
2270
+ const provider = config$1.provider;
2271
+ if (provider === "hostinger") {
2272
+ const { HostingerProvider } = await import("./HostingerProvider-DqUq6e9i.mjs");
2273
+ return new HostingerProvider();
2274
+ }
2275
+ if (provider === "route53") {
2276
+ const { Route53Provider } = await import("./Route53Provider-KUAX3vz9.mjs");
2277
+ const route53Config = config$1;
2278
+ return new Route53Provider({
2279
+ region: route53Config.region,
2280
+ profile: route53Config.profile,
2281
+ hostedZoneId: route53Config.hostedZoneId
2282
+ });
2283
+ }
2284
+ if (provider === "cloudflare") throw new Error("Cloudflare DNS provider not yet implemented");
2285
+ throw new Error(`Unknown DNS provider: ${JSON.stringify(config$1)}`);
2286
+ }
2287
+
2288
+ //#endregion
2289
+ //#region src/deploy/dns/index.ts
2290
+ const logger$6 = console;
2291
+ /**
2292
+ * Check if DNS config is legacy format (single domain with `domain` property)
2414
2293
  */
2415
- const HOSTINGER_API_BASE = "https://developers.hostinger.com";
2294
+ function isLegacyDnsConfig(config$1) {
2295
+ return typeof config$1 === "object" && config$1 !== null && "provider" in config$1 && "domain" in config$1;
2296
+ }
2416
2297
  /**
2417
- * Hostinger API error
2298
+ * Normalize DNS config to new multi-domain format
2418
2299
  */
2419
- var HostingerApiError = class extends Error {
2420
- constructor(message, status, statusText, errors) {
2421
- super(message);
2422
- this.status = status;
2423
- this.statusText = statusText;
2424
- this.errors = errors;
2425
- this.name = "HostingerApiError";
2300
+ function normalizeDnsConfig(config$1) {
2301
+ if (isLegacyDnsConfig(config$1)) {
2302
+ const { domain,...providerConfig } = config$1;
2303
+ return { [domain]: providerConfig };
2426
2304
  }
2427
- };
2305
+ return config$1;
2306
+ }
2428
2307
  /**
2429
- * Hostinger DNS API client
2308
+ * Find the root domain for a hostname from available DNS configs
2430
2309
  *
2431
2310
  * @example
2432
- * ```ts
2433
- * const api = new HostingerApi(token);
2434
- *
2435
- * // Get all records for a domain
2436
- * const records = await api.getRecords('traflabs.io');
2437
- *
2438
- * // Create/update records
2439
- * await api.upsertRecords('traflabs.io', [
2440
- * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }
2441
- * ]);
2442
- * ```
2311
+ * findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
2312
+ * // Returns 'geekmidas.com'
2443
2313
  */
2444
- var HostingerApi = class {
2445
- token;
2446
- constructor(token) {
2447
- this.token = token;
2448
- }
2449
- /**
2450
- * Make a request to the Hostinger API
2451
- */
2452
- async request(method, endpoint, body) {
2453
- const url = `${HOSTINGER_API_BASE}${endpoint}`;
2454
- const response = await fetch(url, {
2455
- method,
2456
- headers: {
2457
- "Content-Type": "application/json",
2458
- Authorization: `Bearer ${this.token}`
2459
- },
2460
- body: body ? JSON.stringify(body) : void 0
2461
- });
2462
- if (!response.ok) {
2463
- let errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;
2464
- let errors;
2465
- try {
2466
- const errorBody = await response.json();
2467
- if (errorBody.message) errorMessage = `Hostinger API error: ${errorBody.message}`;
2468
- errors = errorBody.errors;
2469
- } catch {}
2470
- throw new HostingerApiError(errorMessage, response.status, response.statusText, errors);
2314
+ function findRootDomain(hostname, dnsConfig) {
2315
+ const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
2316
+ for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
2317
+ return null;
2318
+ }
2319
+ /**
2320
+ * Group hostnames by their root domain
2321
+ */
2322
+ function groupHostnamesByDomain(appHostnames, dnsConfig) {
2323
+ const grouped = /* @__PURE__ */ new Map();
2324
+ for (const [appName, hostname] of appHostnames) {
2325
+ const rootDomain = findRootDomain(hostname, dnsConfig);
2326
+ if (!rootDomain) {
2327
+ logger$6.log(` ⚠ No DNS config found for hostname: ${hostname}`);
2328
+ continue;
2471
2329
  }
2472
- const text = await response.text();
2473
- if (!text || text.trim() === "") return void 0;
2474
- return JSON.parse(text);
2475
- }
2476
- /**
2477
- * Get all DNS records for a domain
2478
- *
2479
- * @param domain - Root domain (e.g., 'traflabs.io')
2480
- */
2481
- async getRecords(domain) {
2482
- const response = await this.request("GET", `/api/dns/v1/zones/${domain}`);
2483
- return response.data || [];
2484
- }
2485
- /**
2486
- * Create or update DNS records
2487
- *
2488
- * @param domain - Root domain (e.g., 'traflabs.io')
2489
- * @param records - Records to create/update
2490
- * @param overwrite - If true, replaces all existing records. If false, merges with existing.
2491
- */
2492
- async upsertRecords(domain, records, overwrite = false) {
2493
- await this.request("PUT", `/api/dns/v1/zones/${domain}`, {
2494
- overwrite,
2495
- zone: records
2496
- });
2497
- }
2498
- /**
2499
- * Validate DNS records before applying
2500
- *
2501
- * @param domain - Root domain (e.g., 'traflabs.io')
2502
- * @param records - Records to validate
2503
- * @returns true if valid, throws if invalid
2504
- */
2505
- async validateRecords(domain, records) {
2506
- await this.request("POST", `/api/dns/v1/zones/${domain}/validate`, {
2507
- overwrite: false,
2508
- zone: records
2509
- });
2510
- return true;
2511
- }
2512
- /**
2513
- * Delete specific DNS records
2514
- *
2515
- * @param domain - Root domain (e.g., 'traflabs.io')
2516
- * @param filters - Filters to match records for deletion
2517
- */
2518
- async deleteRecords(domain, filters) {
2519
- await this.request("DELETE", `/api/dns/v1/zones/${domain}`, { filters });
2520
- }
2521
- /**
2522
- * Check if a specific record exists
2523
- *
2524
- * @param domain - Root domain (e.g., 'traflabs.io')
2525
- * @param name - Subdomain name (e.g., 'api.joemoer')
2526
- * @param type - Record type (e.g., 'A')
2527
- */
2528
- async recordExists(domain, name$1, type$1 = "A") {
2529
- const records = await this.getRecords(domain);
2530
- return records.some((r) => r.name === name$1 && r.type === type$1);
2531
- }
2532
- /**
2533
- * Create a single A record if it doesn't exist
2534
- *
2535
- * @param domain - Root domain (e.g., 'traflabs.io')
2536
- * @param subdomain - Subdomain name (e.g., 'api.joemoer')
2537
- * @param ip - IP address to point to
2538
- * @param ttl - TTL in seconds (default: 300)
2539
- * @returns true if created, false if already exists
2540
- */
2541
- async createARecordIfNotExists(domain, subdomain, ip, ttl = 300) {
2542
- const exists = await this.recordExists(domain, subdomain, "A");
2543
- if (exists) return false;
2544
- await this.upsertRecords(domain, [{
2545
- name: subdomain,
2546
- type: "A",
2547
- ttl,
2548
- records: [{ content: ip }]
2549
- }]);
2550
- return true;
2330
+ if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
2331
+ grouped.get(rootDomain).set(appName, hostname);
2551
2332
  }
2552
- };
2553
-
2554
- //#endregion
2555
- //#region src/deploy/dns/index.ts
2556
- const logger$6 = console;
2333
+ return grouped;
2334
+ }
2557
2335
  /**
2558
2336
  * Resolve IP address from a hostname
2559
2337
  */
@@ -2625,127 +2403,91 @@ function printDnsRecordsSimple(records, rootDomain) {
2625
2403
  logger$6.log("");
2626
2404
  }
2627
2405
  /**
2628
- * Prompt for input (reuse from deploy/index.ts pattern)
2629
- */
2630
- async function promptForToken(message) {
2631
- const { stdin: stdin$1, stdout: stdout$1 } = await import("node:process");
2632
- if (!stdin$1.isTTY) throw new Error("Interactive input required for Hostinger token.");
2633
- stdout$1.write(message);
2634
- return new Promise((resolve$1) => {
2635
- let value = "";
2636
- const onData = (char) => {
2637
- const c = char.toString();
2638
- if (c === "\n" || c === "\r") {
2639
- stdin$1.setRawMode(false);
2640
- stdin$1.pause();
2641
- stdin$1.removeListener("data", onData);
2642
- stdout$1.write("\n");
2643
- resolve$1(value);
2644
- } else if (c === "") {
2645
- stdin$1.setRawMode(false);
2646
- stdin$1.pause();
2647
- stdout$1.write("\n");
2648
- process.exit(1);
2649
- } else if (c === "" || c === "\b") {
2650
- if (value.length > 0) value = value.slice(0, -1);
2651
- } else value += c;
2652
- };
2653
- stdin$1.setRawMode(true);
2654
- stdin$1.resume();
2655
- stdin$1.on("data", onData);
2656
- });
2657
- }
2658
- /**
2659
- * Create DNS records using the configured provider
2660
- */
2661
- async function createDnsRecords(records, dnsConfig) {
2662
- const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
2663
- if (provider === "manual") return records.map((r) => ({
2664
- ...r,
2665
- created: false,
2666
- existed: false
2667
- }));
2668
- if (provider === "hostinger") return createHostingerRecords(records, rootDomain, ttl);
2669
- if (provider === "cloudflare") {
2670
- logger$6.log(" ⚠ Cloudflare DNS integration not yet implemented");
2671
- return records.map((r) => ({
2672
- ...r,
2673
- error: "Cloudflare not implemented"
2674
- }));
2675
- }
2676
- return records;
2677
- }
2678
- /**
2679
- * Create DNS records at Hostinger
2406
+ * Create DNS records for a single domain using its configured provider
2680
2407
  */
2681
- async function createHostingerRecords(records, rootDomain, ttl) {
2682
- let token = await getHostingerToken();
2683
- if (!token) {
2684
- logger$6.log("\n 📋 Hostinger API token not found.");
2685
- logger$6.log(" Get your token from: https://hpanel.hostinger.com/profile/api\n");
2686
- try {
2687
- token = await promptForToken(" Hostinger API Token: ");
2688
- await storeHostingerToken(token);
2689
- logger$6.log(" ✓ Token saved");
2690
- } catch {
2691
- logger$6.log(" ⚠ Could not get token, skipping DNS creation");
2692
- return records.map((r) => ({
2693
- ...r,
2694
- error: "No API token"
2695
- }));
2696
- }
2697
- }
2698
- const api = new HostingerApi(token);
2699
- const results = [];
2700
- let existingRecords = [];
2408
+ async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
2409
+ const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
2410
+ let provider;
2701
2411
  try {
2702
- existingRecords = await api.getRecords(rootDomain);
2412
+ provider = await createDnsProvider({ config: providerConfig });
2703
2413
  } catch (error) {
2704
2414
  const message = error instanceof Error ? error.message : "Unknown error";
2705
- logger$6.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
2415
+ logger$6.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
2706
2416
  return records.map((r) => ({
2707
2417
  ...r,
2708
2418
  error: message
2709
2419
  }));
2710
2420
  }
2711
- for (const record of records) {
2712
- const existing = existingRecords.find((r) => r.name === record.subdomain && r.type === "A");
2713
- if (existing) {
2714
- results.push({
2715
- ...record,
2421
+ if (!provider) return records.map((r) => ({
2422
+ ...r,
2423
+ created: false,
2424
+ existed: false
2425
+ }));
2426
+ const results = [];
2427
+ const upsertRecords = records.map((r) => ({
2428
+ name: r.subdomain,
2429
+ type: r.type,
2430
+ ttl,
2431
+ value: r.value
2432
+ }));
2433
+ try {
2434
+ const upsertResults = await provider.upsertRecords(rootDomain, upsertRecords);
2435
+ for (const [i, record] of records.entries()) {
2436
+ const result = upsertResults[i];
2437
+ if (!result) {
2438
+ results.push({
2439
+ hostname: record.hostname,
2440
+ subdomain: record.subdomain,
2441
+ type: record.type,
2442
+ value: record.value,
2443
+ appName: record.appName,
2444
+ error: "No result returned from provider"
2445
+ });
2446
+ continue;
2447
+ }
2448
+ if (result.unchanged) results.push({
2449
+ hostname: record.hostname,
2450
+ subdomain: record.subdomain,
2451
+ type: record.type,
2452
+ value: record.value,
2453
+ appName: record.appName,
2716
2454
  existed: true,
2717
2455
  created: false
2718
2456
  });
2719
- continue;
2720
- }
2721
- try {
2722
- await api.upsertRecords(rootDomain, [{
2723
- name: record.subdomain,
2724
- type: "A",
2725
- ttl,
2726
- records: [{ content: record.value }]
2727
- }]);
2728
- results.push({
2729
- ...record,
2730
- created: true,
2731
- existed: false
2732
- });
2733
- } catch (error) {
2734
- const message = error instanceof Error ? error.message : "Unknown error";
2735
- results.push({
2736
- ...record,
2737
- error: message
2457
+ else results.push({
2458
+ hostname: record.hostname,
2459
+ subdomain: record.subdomain,
2460
+ type: record.type,
2461
+ value: record.value,
2462
+ appName: record.appName,
2463
+ created: result.created,
2464
+ existed: !result.created
2738
2465
  });
2739
2466
  }
2467
+ } catch (error) {
2468
+ const message = error instanceof Error ? error.message : "Unknown error";
2469
+ logger$6.log(` ⚠ Failed to create DNS records for ${rootDomain}: ${message}`);
2470
+ return records.map((r) => ({
2471
+ hostname: r.hostname,
2472
+ subdomain: r.subdomain,
2473
+ type: r.type,
2474
+ value: r.value,
2475
+ appName: r.appName,
2476
+ error: message
2477
+ }));
2740
2478
  }
2741
2479
  return results;
2742
2480
  }
2743
2481
  /**
2744
2482
  * Main DNS orchestration function for deployments
2483
+ *
2484
+ * Supports both legacy single-domain format and new multi-domain format:
2485
+ * - Legacy: { provider: 'hostinger', domain: 'example.com' }
2486
+ * - Multi: { 'example.com': { provider: 'hostinger' }, 'example.dev': { provider: 'route53' } }
2745
2487
  */
2746
2488
  async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint) {
2747
2489
  if (!dnsConfig) return null;
2748
- const { domain: rootDomain, autoCreate = true } = dnsConfig;
2490
+ const normalizedConfig = normalizeDnsConfig(dnsConfig);
2749
2491
  logger$6.log("\n🌐 Setting up DNS records...");
2750
2492
  let serverIp;
2751
2493
  try {
@@ -2757,31 +2499,43 @@ async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint) {
2757
2499
  logger$6.log(` ⚠ Failed to resolve server IP: ${message}`);
2758
2500
  return null;
2759
2501
  }
2760
- const requiredRecords = generateRequiredRecords(appHostnames, rootDomain, serverIp);
2761
- if (requiredRecords.length === 0) {
2762
- logger$6.log(" No DNS records needed");
2502
+ const groupedHostnames = groupHostnamesByDomain(appHostnames, normalizedConfig);
2503
+ if (groupedHostnames.size === 0) {
2504
+ logger$6.log(" No DNS records needed (no hostnames match configured domains)");
2763
2505
  return {
2764
2506
  records: [],
2765
2507
  success: true,
2766
2508
  serverIp
2767
2509
  };
2768
2510
  }
2769
- let finalRecords;
2770
- if (autoCreate && dnsConfig.provider !== "manual") {
2771
- logger$6.log(` Creating DNS records at ${dnsConfig.provider}...`);
2772
- finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
2773
- const created = finalRecords.filter((r) => r.created).length;
2774
- const existed = finalRecords.filter((r) => r.existed).length;
2775
- const failed = finalRecords.filter((r) => r.error).length;
2776
- if (created > 0) logger$6.log(` ✓ Created ${created} DNS record(s)`);
2777
- if (existed > 0) logger$6.log(` ✓ ${existed} record(s) already exist`);
2778
- if (failed > 0) logger$6.log(` ⚠ ${failed} record(s) failed`);
2779
- } else finalRecords = requiredRecords;
2780
- printDnsRecordsTable(finalRecords, rootDomain);
2781
- const hasFailures = finalRecords.some((r) => r.error);
2782
- if (dnsConfig.provider === "manual" || hasFailures) printDnsRecordsSimple(finalRecords.filter((r) => !r.created && !r.existed), rootDomain);
2511
+ const allRecords = [];
2512
+ let hasFailures = false;
2513
+ for (const [rootDomain, domainHostnames] of groupedHostnames) {
2514
+ const providerConfig = normalizedConfig[rootDomain];
2515
+ if (!providerConfig) {
2516
+ logger$6.log(` ⚠ No provider config for ${rootDomain}`);
2517
+ continue;
2518
+ }
2519
+ const providerName = typeof providerConfig.provider === "string" ? providerConfig.provider : "custom";
2520
+ const requiredRecords = generateRequiredRecords(domainHostnames, rootDomain, serverIp);
2521
+ if (requiredRecords.length === 0) continue;
2522
+ logger$6.log(` Creating DNS records for ${rootDomain} (${providerName})...`);
2523
+ const domainRecords = await createDnsRecordsForDomain(requiredRecords, rootDomain, providerConfig);
2524
+ allRecords.push(...domainRecords);
2525
+ const created = domainRecords.filter((r) => r.created).length;
2526
+ const existed = domainRecords.filter((r) => r.existed).length;
2527
+ const failed = domainRecords.filter((r) => r.error).length;
2528
+ if (created > 0) logger$6.log(` ✓ Created ${created} DNS record(s) for ${rootDomain}`);
2529
+ if (existed > 0) logger$6.log(` ✓ ${existed} record(s) already exist for ${rootDomain}`);
2530
+ if (failed > 0) {
2531
+ logger$6.log(` ⚠ ${failed} record(s) failed for ${rootDomain}`);
2532
+ hasFailures = true;
2533
+ }
2534
+ printDnsRecordsTable(domainRecords, rootDomain);
2535
+ if (providerConfig.provider === "manual" || failed > 0) printDnsRecordsSimple(domainRecords.filter((r) => !r.created && !r.existed), rootDomain);
2536
+ }
2783
2537
  return {
2784
- records: finalRecords,
2538
+ records: allRecords,
2785
2539
  success: !hasFailures,
2786
2540
  serverIp
2787
2541
  };
@@ -3610,8 +3364,8 @@ function resolveDockerConfig$1(config$1) {
3610
3364
  const docker = config$1.docker ?? {};
3611
3365
  let defaultImageName = "api";
3612
3366
  try {
3613
- const pkg$1 = __require(`${process.cwd()}/package.json`);
3614
- if (pkg$1.name) defaultImageName = pkg$1.name.replace(/^@[^/]+\//, "");
3367
+ const pkg = __require(`${process.cwd()}/package.json`);
3368
+ if (pkg.name) defaultImageName = pkg.name.replace(/^@[^/]+\//, "");
3615
3369
  } catch {}
3616
3370
  return {
3617
3371
  registry: docker.registry ?? "",
@@ -3967,9 +3721,9 @@ async function dockerCommand(options) {
3967
3721
  } else throw new Error("Monorepo detected but turbo.json not found.\n\nDocker builds in monorepos require Turborepo for proper dependency isolation.\n\nTo fix this:\n 1. Install turbo: pnpm add -Dw turbo\n 2. Create turbo.json in your monorepo root\n 3. Run this command again\n\nSee: https://turbo.build/repo/docs/guides/tools/docker");
3968
3722
  let turboPackage = options.turboPackage ?? dockerConfig.imageName;
3969
3723
  if (useTurbo && !options.turboPackage) try {
3970
- const pkg$1 = __require(`${process.cwd()}/package.json`);
3971
- if (pkg$1.name) {
3972
- turboPackage = pkg$1.name;
3724
+ const pkg = __require(`${process.cwd()}/package.json`);
3725
+ if (pkg.name) {
3726
+ turboPackage = pkg.name;
3973
3727
  logger$5.log(` Turbo package: ${turboPackage}`);
3974
3728
  }
3975
3729
  } catch {}
@@ -4094,8 +3848,8 @@ function getAppPackageName(appPath) {
4094
3848
  const pkgPath = join(appPath, "package.json");
4095
3849
  if (!existsSync(pkgPath)) return void 0;
4096
3850
  const content = readFileSync(pkgPath, "utf-8");
4097
- const pkg$1 = JSON.parse(content);
4098
- return pkg$1.name;
3851
+ const pkg = JSON.parse(content);
3852
+ return pkg.name;
4099
3853
  } catch {
4100
3854
  return void 0;
4101
3855
  }
@@ -4191,8 +3945,8 @@ function getAppNameFromCwd$1() {
4191
3945
  const packageJsonPath = join(process.cwd(), "package.json");
4192
3946
  if (!existsSync(packageJsonPath)) return void 0;
4193
3947
  try {
4194
- const pkg$1 = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
4195
- if (pkg$1.name) return pkg$1.name.replace(/^@[^/]+\//, "");
3948
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
3949
+ if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
4196
3950
  } catch {}
4197
3951
  return void 0;
4198
3952
  }
@@ -4208,8 +3962,8 @@ function getAppNameFromPackageJson() {
4208
3962
  const packageJsonPath = join(projectRoot, "package.json");
4209
3963
  if (!existsSync(packageJsonPath)) return void 0;
4210
3964
  try {
4211
- const pkg$1 = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
4212
- if (pkg$1.name) return pkg$1.name.replace(/^@[^/]+\//, "");
3965
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
3966
+ if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
4213
3967
  } catch {}
4214
3968
  return void 0;
4215
3969
  }
@@ -4756,6 +4510,48 @@ async function deployListCommand(options) {
4756
4510
  }
4757
4511
  }
4758
4512
 
4513
+ //#endregion
4514
+ //#region src/deploy/StateProvider.ts
4515
+ /**
4516
+ * Check if value is a StateProvider implementation.
4517
+ */
4518
+ function isStateProvider(value) {
4519
+ return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
4520
+ }
4521
+ /**
4522
+ * Create a state provider based on configuration.
4523
+ *
4524
+ * - 'local': LocalStateProvider (default)
4525
+ * - 'ssm': CachedStateProvider with SSM as source of truth
4526
+ * - Custom: Use provided StateProvider implementation
4527
+ */
4528
+ async function createStateProvider(options) {
4529
+ const { config: config$1, workspaceRoot, workspaceName } = options;
4530
+ if (!config$1) {
4531
+ const { LocalStateProvider } = await import("./LocalStateProvider-DxoSaWUV.mjs");
4532
+ return new LocalStateProvider(workspaceRoot);
4533
+ }
4534
+ if (isStateProvider(config$1.provider)) return config$1.provider;
4535
+ const provider = config$1.provider;
4536
+ if (provider === "local") {
4537
+ const { LocalStateProvider } = await import("./LocalStateProvider-DxoSaWUV.mjs");
4538
+ return new LocalStateProvider(workspaceRoot);
4539
+ }
4540
+ if (provider === "ssm") {
4541
+ if (!workspaceName) throw new Error("Workspace name is required for SSM state provider. Set \"name\" in gkm.config.ts.");
4542
+ const { LocalStateProvider } = await import("./LocalStateProvider-DxoSaWUV.mjs");
4543
+ const { SSMStateProvider } = await import("./SSMStateProvider-C4wp4AZe.mjs");
4544
+ const { CachedStateProvider: CachedStateProvider$1 } = await import("./CachedStateProvider-OiFUGr7p.mjs");
4545
+ const local = new LocalStateProvider(workspaceRoot);
4546
+ const ssm = new SSMStateProvider({
4547
+ workspaceName,
4548
+ region: config$1.region
4549
+ });
4550
+ return new CachedStateProvider$1(ssm, local);
4551
+ }
4552
+ throw new Error(`Unknown state provider: ${JSON.stringify(config$1)}`);
4553
+ }
4554
+
4759
4555
  //#endregion
4760
4556
  //#region src/deploy/secrets.ts
4761
4557
  /**
@@ -4858,10 +4654,12 @@ function generateSecretsReport(encryptedApps, sniffedApps) {
4858
4654
  const __filename = fileURLToPath(import.meta.url);
4859
4655
  const __dirname = dirname(__filename);
4860
4656
  /**
4861
- * Check if a value is a gkm construct (Endpoint, Function, Cron, or Subscriber).
4657
+ * Resolve the tsx package path from the CLI package's dependencies.
4658
+ * This ensures tsx is available regardless of whether the target project has it installed.
4862
4659
  */
4863
- function isConstruct(value) {
4864
- return Endpoint.isEndpoint(value) || Function.isFunction(value) || Cron.isCron(value) || Subscriber.isSubscriber(value);
4660
+ function resolveTsxPath() {
4661
+ const require$1 = createRequire(import.meta.url);
4662
+ return require$1.resolve("tsx");
4865
4663
  }
4866
4664
  /**
4867
4665
  * Resolve the path to a sniffer helper file.
@@ -5022,7 +4820,9 @@ async function sniffEntryFile(entryPath, appPath, workspacePath) {
5022
4820
  *
5023
4821
  * Route-based apps have endpoints, functions, crons, and subscribers that
5024
4822
  * use services. Each service's register() method accesses environment variables.
5025
- * This function mimics what the bundler does during build to capture those vars.
4823
+ *
4824
+ * This runs in a subprocess with tsx loader to properly handle TypeScript
4825
+ * compilation and path alias resolution (e.g., `src/...` imports).
5026
4826
  *
5027
4827
  * @param routes - Glob pattern(s) for route files
5028
4828
  * @param appPath - The app's path relative to workspace (e.g., 'apps/api')
@@ -5031,32 +4831,63 @@ async function sniffEntryFile(entryPath, appPath, workspacePath) {
5031
4831
  */
5032
4832
  async function sniffRouteFiles(routes, appPath, workspacePath) {
5033
4833
  const fullAppPath = resolve(workspacePath, appPath);
5034
- const patterns = Array.isArray(routes) ? routes : [routes];
5035
- const envVars = /* @__PURE__ */ new Set();
5036
- let error;
5037
- try {
5038
- const files = await fg(patterns, {
4834
+ const workerPath = resolveSnifferFile("sniffer-routes-worker");
4835
+ const tsxPath = resolveTsxPath();
4836
+ const routesArray = Array.isArray(routes) ? routes : [routes];
4837
+ const pattern = routesArray[0];
4838
+ if (!pattern) return {
4839
+ envVars: [],
4840
+ error: new Error("No route patterns provided")
4841
+ };
4842
+ return new Promise((resolvePromise) => {
4843
+ const child = spawn("node", [
4844
+ "--import",
4845
+ tsxPath,
4846
+ workerPath,
4847
+ fullAppPath,
4848
+ pattern
4849
+ ], {
5039
4850
  cwd: fullAppPath,
5040
- absolute: true
4851
+ stdio: [
4852
+ "ignore",
4853
+ "pipe",
4854
+ "pipe"
4855
+ ],
4856
+ env: { ...process.env }
5041
4857
  });
5042
- for (const file of files) try {
5043
- const module = await import(file);
5044
- for (const [, exportValue] of Object.entries(module)) if (isConstruct(exportValue)) try {
5045
- const constructEnvVars = await exportValue.getEnvironment();
5046
- constructEnvVars.forEach((v) => envVars.add(v));
5047
- } catch (e) {
5048
- console.warn(`[sniffer] Failed to get environment for construct in ${file}: ${e instanceof Error ? e.message : String(e)}`);
5049
- }
5050
- } catch (e) {
5051
- console.warn(`[sniffer] Failed to import ${file}: ${e instanceof Error ? e.message : String(e)}`);
5052
- }
5053
- } catch (e) {
5054
- error = e instanceof Error ? e : new Error(String(e));
5055
- }
5056
- return {
5057
- envVars: Array.from(envVars).sort(),
5058
- error
5059
- };
4858
+ let stdout$1 = "";
4859
+ let stderr = "";
4860
+ child.stdout.on("data", (data) => {
4861
+ stdout$1 += data.toString();
4862
+ });
4863
+ child.stderr.on("data", (data) => {
4864
+ stderr += data.toString();
4865
+ });
4866
+ child.on("close", (code) => {
4867
+ if (stderr) stderr.split("\n").filter((line) => line.trim()).forEach((line) => console.warn(line));
4868
+ try {
4869
+ const jsonMatch = stdout$1.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
4870
+ if (jsonMatch) {
4871
+ const result = JSON.parse(jsonMatch[0]);
4872
+ resolvePromise({
4873
+ envVars: result.envVars || [],
4874
+ error: result.error ? new Error(result.error) : void 0
4875
+ });
4876
+ return;
4877
+ }
4878
+ } catch {}
4879
+ resolvePromise({
4880
+ envVars: [],
4881
+ error: new Error(`Failed to sniff route files (exit code ${code}): ${stderr || stdout$1 || "No output"}`)
4882
+ });
4883
+ });
4884
+ child.on("error", (err) => {
4885
+ resolvePromise({
4886
+ envVars: [],
4887
+ error: err
4888
+ });
4889
+ });
4890
+ });
5060
4891
  }
5061
4892
  /**
5062
4893
  * Run the SnifferEnvironmentParser on an envParser module to detect
@@ -5668,7 +5499,12 @@ async function workspaceDeployCommand(workspace, options) {
5668
5499
  logger$1.log(` ✓ Created project: ${project.projectId}`);
5669
5500
  }
5670
5501
  logger$1.log("\n📋 Loading deploy state...");
5671
- let state = await readStageState(workspace.root, stage);
5502
+ const stateProvider = await createStateProvider({
5503
+ config: workspace.state,
5504
+ workspaceRoot: workspace.root,
5505
+ workspaceName: workspace.name
5506
+ });
5507
+ let state = await stateProvider.read(stage);
5672
5508
  if (state) {
5673
5509
  logger$1.log(` Found existing state for stage "${stage}"`);
5674
5510
  if (state.environmentId !== environmentId) {
@@ -6003,14 +5839,14 @@ async function workspaceDeployCommand(workspace, options) {
6003
5839
  }
6004
5840
  }
6005
5841
  logger$1.log("\n📋 Saving deploy state...");
6006
- await writeStageState(workspace.root, stage, state);
6007
- logger$1.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
5842
+ await stateProvider.write(stage, state);
5843
+ logger$1.log(" ✓ State saved");
6008
5844
  const dnsConfig = workspace.deploy.dns;
6009
5845
  if (dnsConfig && appHostnames.size > 0) {
6010
5846
  const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
6011
5847
  if (dnsResult?.serverIp && appHostnames.size > 0) {
6012
5848
  await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
6013
- await writeStageState(workspace.root, stage, state);
5849
+ await stateProvider.write(stage, state);
6014
5850
  }
6015
5851
  if (dnsResult?.success && appHostnames.size > 0) {
6016
5852
  logger$1.log("\n🔒 Validating domains for SSL certificates...");
@@ -6079,7 +5915,7 @@ async function deployCommand(options) {
6079
5915
  dokployConfig = setupResult.config;
6080
5916
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6081
5917
  if (setupResult.serviceUrls) {
6082
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-DNj_I11J.mjs");
5918
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await import("./storage-D8XzjVaO.mjs");
6083
5919
  let secrets = await readStageSecrets$1(stage);
6084
5920
  if (!secrets) {
6085
5921
  logger$1.log(` Creating secrets file for stage "${stage}"...`);
@@ -6169,36 +6005,204 @@ async function deployCommand(options) {
6169
6005
  }
6170
6006
 
6171
6007
  //#endregion
6172
- //#region src/secrets/generator.ts
6008
+ //#region src/deploy/state-commands.ts
6173
6009
  /**
6174
- * Generate a secure random password using URL-safe base64 characters.
6175
- * @param length Password length (default: 32)
6010
+ * Pull state from remote to local.
6011
+ * `gkm state:pull --stage=<stage>`
6176
6012
  */
6177
- function generateSecurePassword(length = 32) {
6178
- return randomBytes(Math.ceil(length * 3 / 4)).toString("base64url").slice(0, length);
6179
- }
6180
- /** Default service configurations */
6181
- const SERVICE_DEFAULTS = {
6182
- postgres: {
6183
- host: "postgres",
6184
- port: 5432,
6185
- username: "app",
6186
- database: "app"
6187
- },
6188
- redis: {
6189
- host: "redis",
6190
- port: 6379,
6191
- username: "default"
6192
- },
6193
- rabbitmq: {
6194
- host: "rabbitmq",
6195
- port: 5672,
6196
- username: "app",
6197
- vhost: "/"
6013
+ async function statePullCommand(options) {
6014
+ const { workspace } = await loadWorkspaceConfig();
6015
+ if (!workspace.state || workspace.state.provider === "local") {
6016
+ console.error("No remote state provider configured.");
6017
+ console.error("Add a remote provider in gkm.config.ts:");
6018
+ console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
6019
+ process.exit(1);
6198
6020
  }
6199
- };
6200
- /**
6201
- * Generate credentials for a specific service.
6021
+ const provider = await createStateProvider({
6022
+ config: workspace.state,
6023
+ workspaceRoot: workspace.root,
6024
+ workspaceName: workspace.name
6025
+ });
6026
+ if (!(provider instanceof CachedStateProvider)) {
6027
+ console.error("State provider does not support pull operation.");
6028
+ process.exit(1);
6029
+ }
6030
+ console.log(`Pulling state for stage: ${options.stage}...`);
6031
+ const state = await provider.pull(options.stage);
6032
+ if (state) {
6033
+ console.log("State pulled successfully.");
6034
+ printStateSummary(state);
6035
+ } else console.log("No remote state found for this stage.");
6036
+ }
6037
+ /**
6038
+ * Push local state to remote.
6039
+ * `gkm state:push --stage=<stage>`
6040
+ */
6041
+ async function statePushCommand(options) {
6042
+ const { workspace } = await loadWorkspaceConfig();
6043
+ if (!workspace.state || workspace.state.provider === "local") {
6044
+ console.error("No remote state provider configured.");
6045
+ console.error("Add a remote provider in gkm.config.ts:");
6046
+ console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
6047
+ process.exit(1);
6048
+ }
6049
+ const provider = await createStateProvider({
6050
+ config: workspace.state,
6051
+ workspaceRoot: workspace.root,
6052
+ workspaceName: workspace.name
6053
+ });
6054
+ if (!(provider instanceof CachedStateProvider)) {
6055
+ console.error("State provider does not support push operation.");
6056
+ process.exit(1);
6057
+ }
6058
+ console.log(`Pushing state for stage: ${options.stage}...`);
6059
+ const state = await provider.push(options.stage);
6060
+ if (state) {
6061
+ console.log("State pushed successfully.");
6062
+ printStateSummary(state);
6063
+ } else console.log("No local state found for this stage.");
6064
+ }
6065
+ /**
6066
+ * Show current state.
6067
+ * `gkm state:show --stage=<stage>`
6068
+ */
6069
+ async function stateShowCommand(options) {
6070
+ const { workspace } = await loadWorkspaceConfig();
6071
+ const provider = await createStateProvider({
6072
+ config: workspace.state,
6073
+ workspaceRoot: workspace.root,
6074
+ workspaceName: workspace.name
6075
+ });
6076
+ const state = await provider.read(options.stage);
6077
+ if (!state) {
6078
+ console.log(`No state found for stage: ${options.stage}`);
6079
+ return;
6080
+ }
6081
+ if (options.json) console.log(JSON.stringify(state, null, 2));
6082
+ else printStateDetails(state);
6083
+ }
6084
+ /**
6085
+ * Compare local and remote state.
6086
+ * `gkm state:diff --stage=<stage>`
6087
+ */
6088
+ async function stateDiffCommand(options) {
6089
+ const { workspace } = await loadWorkspaceConfig();
6090
+ if (!workspace.state || workspace.state.provider === "local") {
6091
+ console.error("No remote state provider configured.");
6092
+ console.error("Diff requires a remote provider to compare against.");
6093
+ process.exit(1);
6094
+ }
6095
+ const provider = await createStateProvider({
6096
+ config: workspace.state,
6097
+ workspaceRoot: workspace.root,
6098
+ workspaceName: workspace.name
6099
+ });
6100
+ if (!(provider instanceof CachedStateProvider)) {
6101
+ console.error("State provider does not support diff operation.");
6102
+ process.exit(1);
6103
+ }
6104
+ console.log(`Comparing state for stage: ${options.stage}...\n`);
6105
+ const { local, remote } = await provider.diff(options.stage);
6106
+ if (!local && !remote) {
6107
+ console.log("No state found (local or remote).");
6108
+ return;
6109
+ }
6110
+ if (!local) console.log("Local: (none)");
6111
+ else console.log(`Local: Last deployed ${local.lastDeployedAt}`);
6112
+ if (!remote) console.log("Remote: (none)");
6113
+ else console.log(`Remote: Last deployed ${remote.lastDeployedAt}`);
6114
+ console.log("");
6115
+ const localApps = local?.applications ?? {};
6116
+ const remoteApps = remote?.applications ?? {};
6117
+ const allApps = new Set([...Object.keys(localApps), ...Object.keys(remoteApps)]);
6118
+ if (allApps.size > 0) {
6119
+ console.log("Applications:");
6120
+ for (const app of allApps) {
6121
+ const localId = localApps[app];
6122
+ const remoteId = remoteApps[app];
6123
+ if (localId === remoteId) console.log(` ${app}: ${localId ?? "(none)"}`);
6124
+ else if (!localId) console.log(` ${app}: (none) -> ${remoteId} [REMOTE ONLY]`);
6125
+ else if (!remoteId) console.log(` ${app}: ${localId} -> (none) [LOCAL ONLY]`);
6126
+ else console.log(` ${app}: ${localId} (local) != ${remoteId} (remote) [MISMATCH]`);
6127
+ }
6128
+ }
6129
+ const localServices = local?.services ?? {};
6130
+ const remoteServices = remote?.services ?? {};
6131
+ if (Object.keys(localServices).length > 0 || Object.keys(remoteServices).length > 0) {
6132
+ console.log("\nServices:");
6133
+ const serviceKeys = new Set([...Object.keys(localServices), ...Object.keys(remoteServices)]);
6134
+ for (const key of serviceKeys) {
6135
+ const localVal = localServices[key];
6136
+ const remoteVal = remoteServices[key];
6137
+ if (localVal === remoteVal) console.log(` ${key}: ${localVal ?? "(none)"}`);
6138
+ else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
6139
+ }
6140
+ }
6141
+ }
6142
+ function printStateSummary(state) {
6143
+ const appCount = Object.keys(state.applications).length;
6144
+ const hasPostgres = !!state.services.postgresId;
6145
+ const hasRedis = !!state.services.redisId;
6146
+ console.log(` Stage: ${state.stage}`);
6147
+ console.log(` Applications: ${appCount}`);
6148
+ console.log(` Postgres: ${hasPostgres ? "configured" : "none"}`);
6149
+ console.log(` Redis: ${hasRedis ? "configured" : "none"}`);
6150
+ console.log(` Last deployed: ${state.lastDeployedAt}`);
6151
+ }
6152
+ function printStateDetails(state) {
6153
+ console.log(`Stage: ${state.stage}`);
6154
+ console.log(`Environment ID: ${state.environmentId}`);
6155
+ console.log(`Last Deployed: ${state.lastDeployedAt}`);
6156
+ console.log("");
6157
+ console.log("Applications:");
6158
+ const apps = Object.entries(state.applications);
6159
+ if (apps.length === 0) console.log(" (none)");
6160
+ else for (const [name$1, id] of apps) console.log(` ${name$1}: ${id}`);
6161
+ console.log("");
6162
+ console.log("Services:");
6163
+ if (!state.services.postgresId && !state.services.redisId) console.log(" (none)");
6164
+ else {
6165
+ if (state.services.postgresId) console.log(` Postgres: ${state.services.postgresId}`);
6166
+ if (state.services.redisId) console.log(` Redis: ${state.services.redisId}`);
6167
+ }
6168
+ if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
6169
+ console.log("");
6170
+ console.log("DNS Verified:");
6171
+ for (const [hostname, info] of Object.entries(state.dnsVerified)) console.log(` ${hostname}: ${info.serverIp} (${info.verifiedAt})`);
6172
+ }
6173
+ }
6174
+
6175
+ //#endregion
6176
+ //#region src/secrets/generator.ts
6177
+ /**
6178
+ * Generate a secure random password using URL-safe base64 characters.
6179
+ * @param length Password length (default: 32)
6180
+ */
6181
+ function generateSecurePassword(length = 32) {
6182
+ return randomBytes(Math.ceil(length * 3 / 4)).toString("base64url").slice(0, length);
6183
+ }
6184
+ /** Default service configurations */
6185
+ const SERVICE_DEFAULTS = {
6186
+ postgres: {
6187
+ host: "postgres",
6188
+ port: 5432,
6189
+ username: "app",
6190
+ database: "app"
6191
+ },
6192
+ redis: {
6193
+ host: "redis",
6194
+ port: 6379,
6195
+ username: "default"
6196
+ },
6197
+ rabbitmq: {
6198
+ host: "rabbitmq",
6199
+ port: 5672,
6200
+ username: "app",
6201
+ vhost: "/"
6202
+ }
6203
+ };
6204
+ /**
6205
+ * Generate credentials for a specific service.
6202
6206
  */
6203
6207
  function generateServiceCredentials(service) {
6204
6208
  const defaults = SERVICE_DEFAULTS[service];
@@ -6287,45 +6291,33 @@ function rotateServicePassword(secrets, service) {
6287
6291
 
6288
6292
  //#endregion
6289
6293
  //#region src/init/versions.ts
6290
- const require$1 = createRequire(import.meta.url);
6291
- function loadPackageJson() {
6292
- try {
6293
- return require$1("../package.json");
6294
- } catch {
6295
- return require$1("../../package.json");
6296
- }
6297
- }
6298
- const pkg = loadPackageJson();
6299
- /**
6300
- * CLI version from package.json (used for scaffolded projects)
6301
- */
6302
- const CLI_VERSION = `~${pkg.version}`;
6303
6294
  /**
6304
- * Current released versions of @geekmidas packages
6305
- * Update these when publishing new versions
6306
- * Note: CLI version is read from package.json via CLI_VERSION
6295
+ * Package versions for @geekmidas packages
6296
+ *
6297
+ * AUTO-GENERATED - Do not edit manually
6298
+ * Run: pnpm --filter @geekmidas/cli sync-versions
6307
6299
  */
6308
6300
  const GEEKMIDAS_VERSIONS = {
6309
- "@geekmidas/audit": "~0.2.0",
6310
- "@geekmidas/auth": "~0.2.0",
6311
- "@geekmidas/cache": "~0.2.0",
6312
- "@geekmidas/cli": CLI_VERSION,
6313
- "@geekmidas/client": "~0.5.0",
6314
- "@geekmidas/cloud": "~0.2.0",
6315
- "@geekmidas/constructs": "~0.8.0",
6316
- "@geekmidas/db": "~0.3.0",
6317
- "@geekmidas/emailkit": "~0.2.0",
6318
- "@geekmidas/envkit": "~0.7.0",
6319
- "@geekmidas/errors": "~0.1.0",
6320
- "@geekmidas/events": "~0.2.0",
6321
- "@geekmidas/logger": "~0.4.0",
6322
- "@geekmidas/rate-limit": "~0.3.0",
6323
- "@geekmidas/schema": "~0.1.0",
6324
- "@geekmidas/services": "~0.2.0",
6325
- "@geekmidas/storage": "~0.1.0",
6326
- "@geekmidas/studio": "~0.4.0",
6327
- "@geekmidas/telescope": "~0.6.0",
6328
- "@geekmidas/testkit": "~0.6.0"
6301
+ "@geekmidas/audit": "~1.0.0",
6302
+ "@geekmidas/auth": "~1.0.0",
6303
+ "@geekmidas/cache": "~1.0.0",
6304
+ "@geekmidas/cli": "~1.0.0",
6305
+ "@geekmidas/client": "~1.0.0",
6306
+ "@geekmidas/cloud": "~1.0.0",
6307
+ "@geekmidas/constructs": "~1.0.0",
6308
+ "@geekmidas/db": "~1.0.0",
6309
+ "@geekmidas/emailkit": "~1.0.0",
6310
+ "@geekmidas/envkit": "~1.0.0",
6311
+ "@geekmidas/errors": "~1.0.0",
6312
+ "@geekmidas/events": "~1.0.0",
6313
+ "@geekmidas/logger": "~1.0.0",
6314
+ "@geekmidas/rate-limit": "~1.0.0",
6315
+ "@geekmidas/schema": "~1.0.0",
6316
+ "@geekmidas/services": "~1.0.0",
6317
+ "@geekmidas/storage": "~1.0.0",
6318
+ "@geekmidas/studio": "~1.0.0",
6319
+ "@geekmidas/telescope": "~1.0.0",
6320
+ "@geekmidas/testkit": "~1.0.0"
6329
6321
  };
6330
6322
 
6331
6323
  //#endregion
@@ -6374,7 +6366,10 @@ function generateAuthAppFiles(options) {
6374
6366
  compilerOptions: {
6375
6367
  noEmit: true,
6376
6368
  baseUrl: ".",
6377
- paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
6369
+ paths: {
6370
+ "~/*": ["./src/*"],
6371
+ [`@${options.name}/*`]: ["../../packages/*/src"]
6372
+ }
6378
6373
  },
6379
6374
  include: ["src/**/*.ts"],
6380
6375
  exclude: ["node_modules", "dist"]
@@ -6584,7 +6579,10 @@ export default defineConfig({
6584
6579
  compilerOptions: {
6585
6580
  noEmit: true,
6586
6581
  baseUrl: ".",
6587
- paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
6582
+ paths: {
6583
+ "~/*": ["./src/*"],
6584
+ [`@${options.name}/*`]: ["../../packages/*/src"]
6585
+ }
6588
6586
  },
6589
6587
  include: ["src/**/*.ts"],
6590
6588
  exclude: ["node_modules", "dist"]
@@ -6704,7 +6702,10 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
6704
6702
  compilerOptions: {
6705
6703
  noEmit: true,
6706
6704
  baseUrl: ".",
6707
- paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
6705
+ paths: {
6706
+ "~/*": ["./src/*"],
6707
+ [`@${options.name}/*`]: ["../../packages/*/src"]
6708
+ }
6708
6709
  },
6709
6710
  include: ["src/**/*.ts"],
6710
6711
  exclude: ["node_modules", "dist"]
@@ -7002,7 +7003,11 @@ function generateModelsPackage(options) {
7002
7003
  // Common Schemas
7003
7004
  // ============================================
7004
7005
 
7005
- export const IdSchema = z.string().uuid();
7006
+ export const IdSchema = z.uuid();
7007
+
7008
+ export const IdParamsSchema = z.object({
7009
+ id: IdSchema,
7010
+ });
7006
7011
 
7007
7012
  export const TimestampsSchema = z.object({
7008
7013
  createdAt: z.coerce.date(),
@@ -7028,6 +7033,7 @@ export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =
7028
7033
  // ============================================
7029
7034
 
7030
7035
  export type Id = z.infer<typeof IdSchema>;
7036
+ export type IdParams = z.infer<typeof IdParamsSchema>;
7031
7037
  export type Timestamps = z.infer<typeof TimestampsSchema>;
7032
7038
  export type Pagination = z.infer<typeof PaginationSchema>;
7033
7039
  `;
@@ -7120,6 +7126,7 @@ function generateMonorepoFiles(options, _template) {
7120
7126
  lint: "biome lint .",
7121
7127
  fmt: "biome format . --write",
7122
7128
  "fmt:check": "biome format .",
7129
+ ...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
7123
7130
  ...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
7124
7131
  },
7125
7132
  dependencies: { zod: "~4.1.0" },
@@ -7515,7 +7522,20 @@ export const config = envParser
7515
7522
  },
7516
7523
  {
7517
7524
  path: getRoutePath("health.ts"),
7518
- content: `import { e } from '@geekmidas/constructs/endpoints';
7525
+ content: monorepo ? `import { z } from 'zod';
7526
+ import { publicRouter } from '~/router';
7527
+
7528
+ export const healthEndpoint = publicRouter
7529
+ .get('/health')
7530
+ .output(z.object({
7531
+ status: z.string(),
7532
+ timestamp: z.string(),
7533
+ }))
7534
+ .handle(async () => ({
7535
+ status: 'ok',
7536
+ timestamp: new Date().toISOString(),
7537
+ }));
7538
+ ` : `import { e } from '@geekmidas/constructs/endpoints';
7519
7539
  import { z } from 'zod';
7520
7540
 
7521
7541
  export const healthEndpoint = e
@@ -7568,12 +7588,12 @@ export const listUsersEndpoint = e
7568
7588
  {
7569
7589
  path: getRoutePath("users/get.ts"),
7570
7590
  content: modelsImport ? `import { e } from '@geekmidas/constructs/endpoints';
7571
- import { IdSchema } from '${modelsImport}/common';
7591
+ import { IdParamsSchema } from '${modelsImport}/common';
7572
7592
  import { UserResponseSchema } from '${modelsImport}/user';
7573
7593
 
7574
7594
  export const getUserEndpoint = e
7575
7595
  .get('/users/:id')
7576
- .params({ id: IdSchema })
7596
+ .params(IdParamsSchema)
7577
7597
  .output(UserResponseSchema)
7578
7598
  .handle(async ({ params }) => ({
7579
7599
  id: params.id,
@@ -7599,6 +7619,91 @@ export const getUserEndpoint = e
7599
7619
  `
7600
7620
  }
7601
7621
  ];
7622
+ if (options.monorepo) {
7623
+ files.push({
7624
+ path: "src/services/auth.ts",
7625
+ content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
7626
+
7627
+ export interface Session {
7628
+ user: {
7629
+ id: string;
7630
+ email: string;
7631
+ name: string;
7632
+ };
7633
+ }
7634
+
7635
+ export interface AuthClient {
7636
+ getSession: (cookie: string) => Promise<Session | null>;
7637
+ }
7638
+
7639
+ export const authService = {
7640
+ serviceName: 'auth' as const,
7641
+ async register({ envParser, context }: ServiceRegisterOptions) {
7642
+ const logger = context.getLogger();
7643
+
7644
+ const config = envParser
7645
+ .create((get) => ({
7646
+ url: get('AUTH_URL').string(),
7647
+ }))
7648
+ .parse();
7649
+
7650
+ logger.info({ authUrl: config.url }, 'Auth service configured');
7651
+
7652
+ return {
7653
+ getSession: async (cookie: string): Promise<Session | null> => {
7654
+ const res = await fetch(\`\${config.url}/api/auth/get-session\`, {
7655
+ headers: { cookie },
7656
+ });
7657
+ if (!res.ok) return null;
7658
+ return res.json();
7659
+ },
7660
+ };
7661
+ },
7662
+ } satisfies Service<'auth', AuthClient>;
7663
+ `
7664
+ });
7665
+ files.push({
7666
+ path: "src/router.ts",
7667
+ content: `import { e } from '@geekmidas/constructs/endpoints';
7668
+ import { UnauthorizedError } from '@geekmidas/errors';
7669
+ import { authService, type Session } from './services/auth.js';
7670
+ import { logger } from './config/logger.js';
7671
+
7672
+ // Public router - no auth required
7673
+ export const publicRouter = e.logger(logger);
7674
+
7675
+ // Router with auth service available (but session not enforced)
7676
+ export const r = publicRouter.services([authService]);
7677
+
7678
+ // Session router - requires active session, throws if not authenticated
7679
+ export const sessionRouter = r.session<Session>(async ({ services, header }) => {
7680
+ const cookie = header('cookie') || '';
7681
+ const session = await services.auth.getSession(cookie);
7682
+
7683
+ if (!session?.user) {
7684
+ throw new UnauthorizedError('No active session');
7685
+ }
7686
+
7687
+ return session;
7688
+ });
7689
+ `
7690
+ });
7691
+ files.push({
7692
+ path: getRoutePath("profile.ts"),
7693
+ content: `import { z } from 'zod';
7694
+ import { sessionRouter } from '~/router';
7695
+
7696
+ export const profileEndpoint = sessionRouter
7697
+ .get('/profile')
7698
+ .output(z.object({
7699
+ id: z.string(),
7700
+ email: z.string(),
7701
+ name: z.string(),
7702
+ }))
7703
+ .handle(async ({ session }) => session.user);
7704
+ `
7705
+ });
7706
+ }
7602
7707
  if (options.database) files.push({
7603
7708
  path: "src/services/database.ts",
7604
7709
  content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
@@ -8361,6 +8466,1419 @@ function generateSourceFiles(options, template) {
8361
8466
  return template.files(options);
8362
8467
  }
8363
8468
 
8469
+ //#endregion
8470
+ //#region src/init/generators/ui.ts
8471
+ /**
8472
+ * Generate UI package files for fullstack template
8473
+ * Based on @geekmidas/ui with shadcn/ui, Tailwind CSS v4, and Storybook
8474
+ */
8475
+ function generateUiPackageFiles(options) {
8476
+ if (!options.monorepo || options.template !== "fullstack") return [];
8477
+ const packageName = `@${options.name}/ui`;
8478
+ const packageJson = {
8479
+ name: packageName,
8480
+ version: "0.0.1",
8481
+ private: true,
8482
+ type: "module",
8483
+ exports: {
8484
+ ".": "./src/index.ts",
8485
+ "./components": "./src/components/index.ts",
8486
+ "./lib/utils": "./src/lib/utils.ts",
8487
+ "./styles": "./src/styles/globals.css"
8488
+ },
8489
+ scripts: {
8490
+ "ts:check": "tsc --noEmit",
8491
+ storybook: "storybook dev -p 6006",
8492
+ "build:storybook": "storybook build -o dist/storybook"
8493
+ },
8494
+ dependencies: {
8495
+ "@radix-ui/react-dialog": "~1.1.4",
8496
+ "@radix-ui/react-label": "~2.1.2",
8497
+ "@radix-ui/react-separator": "~1.1.2",
8498
+ "@radix-ui/react-slot": "~1.2.4",
8499
+ "@radix-ui/react-tabs": "~1.1.2",
8500
+ "@radix-ui/react-tooltip": "~1.1.6",
8501
+ "class-variance-authority": "~0.7.1",
8502
+ clsx: "^2.1.1",
8503
+ "lucide-react": "~0.562.0",
8504
+ "tailwind-merge": "~3.4.0"
8505
+ },
8506
+ devDependencies: {
8507
+ "@storybook/addon-a11y": "^8.4.7",
8508
+ "@storybook/addon-essentials": "^8.4.7",
8509
+ "@storybook/addon-interactions": "^8.4.7",
8510
+ "@storybook/react": "^8.4.7",
8511
+ "@storybook/react-vite": "^8.4.7",
8512
+ "@tailwindcss/vite": "^4.0.0",
8513
+ "@types/react": "^19.0.0",
8514
+ "@types/react-dom": "^19.0.0",
8515
+ react: "^19.0.0",
8516
+ "react-dom": "^19.0.0",
8517
+ storybook: "^8.4.7",
8518
+ tailwindcss: "^4.0.0",
8519
+ typescript: "^5.8.2",
8520
+ vite: "^6.0.0"
8521
+ },
8522
+ peerDependencies: {
8523
+ react: ">=18.0.0",
8524
+ "react-dom": ">=18.0.0",
8525
+ tailwindcss: ">=4.0.0"
8526
+ }
8527
+ };
8528
+ const tsConfig = {
8529
+ extends: "../../tsconfig.json",
8530
+ compilerOptions: {
8531
+ jsx: "react-jsx",
8532
+ lib: [
8533
+ "ES2023",
8534
+ "DOM",
8535
+ "DOM.Iterable"
8536
+ ],
8537
+ noEmit: true,
8538
+ baseUrl: ".",
8539
+ paths: { "~/*": ["./src/*"] }
8540
+ },
8541
+ include: ["src/**/*"],
8542
+ exclude: [
8543
+ "node_modules",
8544
+ "dist",
8545
+ "**/*.stories.tsx"
8546
+ ]
8547
+ };
8548
+ const componentsJson = {
8549
+ $schema: "https://ui.shadcn.com/schema.json",
8550
+ style: "new-york",
8551
+ rsc: false,
8552
+ tsx: true,
8553
+ tailwind: {
8554
+ config: "",
8555
+ css: "src/styles/globals.css",
8556
+ baseColor: "neutral",
8557
+ cssVariables: true,
8558
+ prefix: ""
8559
+ },
8560
+ aliases: {
8561
+ components: "~/components",
8562
+ utils: "~/lib/utils",
8563
+ ui: "~/components/ui",
8564
+ lib: "~/lib",
8565
+ hooks: "~/hooks"
8566
+ },
8567
+ iconLibrary: "lucide"
8568
+ };
8569
+ const storybookMain = `import type { StorybookConfig } from '@storybook/react-vite';
8570
+
8571
+ const config: StorybookConfig = {
8572
+ stories: ['../src/**/*.stories.@(ts|tsx)'],
8573
+ addons: [
8574
+ '@storybook/addon-essentials',
8575
+ '@storybook/addon-interactions',
8576
+ '@storybook/addon-a11y',
8577
+ ],
8578
+ framework: {
8579
+ name: '@storybook/react-vite',
8580
+ options: {},
8581
+ },
8582
+ docs: {
8583
+ autodocs: 'tag',
8584
+ },
8585
+ viteFinal: async (config) => {
8586
+ // Add Tailwind CSS v4 plugin
8587
+ const tailwindcss = await import('@tailwindcss/vite');
8588
+ config.plugins = config.plugins || [];
8589
+ config.plugins.push(tailwindcss.default());
8590
+ return config;
8591
+ },
8592
+ };
8593
+
8594
+ export default config;
8595
+ `;
8596
+ const storybookPreview = `import type { Preview } from '@storybook/react';
8597
+ import '../src/styles/globals.css';
8598
+
8599
+ const preview: Preview = {
8600
+ parameters: {
8601
+ backgrounds: {
8602
+ default: 'dark',
8603
+ values: [
8604
+ { name: 'dark', value: '#171717' },
8605
+ { name: 'surface', value: '#1c1c1c' },
8606
+ { name: 'light', value: '#fafafa' },
8607
+ ],
8608
+ },
8609
+ controls: {
8610
+ matchers: {
8611
+ color: /(background|color)$/i,
8612
+ date: /Date$/i,
8613
+ },
8614
+ },
8615
+ layout: 'centered',
8616
+ },
8617
+ };
8618
+
8619
+ export default preview;
8620
+ `;
8621
+ const globalsCss = `@import "tailwindcss";
8622
+
8623
+ @theme {
8624
+ --color-background: hsl(var(--background));
8625
+ --color-foreground: hsl(var(--foreground));
8626
+ --color-card: hsl(var(--card));
8627
+ --color-card-foreground: hsl(var(--card-foreground));
8628
+ --color-popover: hsl(var(--popover));
8629
+ --color-popover-foreground: hsl(var(--popover-foreground));
8630
+ --color-primary: hsl(var(--primary));
8631
+ --color-primary-foreground: hsl(var(--primary-foreground));
8632
+ --color-secondary: hsl(var(--secondary));
8633
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
8634
+ --color-muted: hsl(var(--muted));
8635
+ --color-muted-foreground: hsl(var(--muted-foreground));
8636
+ --color-accent: hsl(var(--accent));
8637
+ --color-accent-foreground: hsl(var(--accent-foreground));
8638
+ --color-destructive: hsl(var(--destructive));
8639
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
8640
+ --color-border: hsl(var(--border));
8641
+ --color-input: hsl(var(--input));
8642
+ --color-ring: hsl(var(--ring));
8643
+ --radius-sm: calc(var(--radius) - 4px);
8644
+ --radius-md: calc(var(--radius) - 2px);
8645
+ --radius-lg: var(--radius);
8646
+ --radius-xl: calc(var(--radius) + 4px);
8647
+ }
8648
+
8649
+ @layer base {
8650
+ :root {
8651
+ --background: 0 0% 100%;
8652
+ --foreground: 0 0% 3.9%;
8653
+ --card: 0 0% 100%;
8654
+ --card-foreground: 0 0% 3.9%;
8655
+ --popover: 0 0% 100%;
8656
+ --popover-foreground: 0 0% 3.9%;
8657
+ --primary: 160 84% 39%;
8658
+ --primary-foreground: 0 0% 98%;
8659
+ --secondary: 0 0% 96.1%;
8660
+ --secondary-foreground: 0 0% 9%;
8661
+ --muted: 0 0% 96.1%;
8662
+ --muted-foreground: 0 0% 45.1%;
8663
+ --accent: 0 0% 96.1%;
8664
+ --accent-foreground: 0 0% 9%;
8665
+ --destructive: 0 84.2% 60.2%;
8666
+ --destructive-foreground: 0 0% 98%;
8667
+ --border: 0 0% 89.8%;
8668
+ --input: 0 0% 89.8%;
8669
+ --ring: 160 84% 39%;
8670
+ --radius: 0.5rem;
8671
+ }
8672
+
8673
+ .dark {
8674
+ --background: 0 0% 9%;
8675
+ --foreground: 0 0% 98%;
8676
+ --card: 0 0% 11%;
8677
+ --card-foreground: 0 0% 98%;
8678
+ --popover: 0 0% 11%;
8679
+ --popover-foreground: 0 0% 98%;
8680
+ --primary: 160 84% 52%;
8681
+ --primary-foreground: 0 0% 9%;
8682
+ --secondary: 0 0% 15%;
8683
+ --secondary-foreground: 0 0% 98%;
8684
+ --muted: 0 0% 15%;
8685
+ --muted-foreground: 0 0% 64%;
8686
+ --accent: 0 0% 15%;
8687
+ --accent-foreground: 0 0% 98%;
8688
+ --destructive: 0 62.8% 50.6%;
8689
+ --destructive-foreground: 0 0% 98%;
8690
+ --border: 0 0% 18%;
8691
+ --input: 0 0% 18%;
8692
+ --ring: 160 84% 52%;
8693
+ }
8694
+ }
8695
+
8696
+ @layer base {
8697
+ * {
8698
+ @apply border-border;
8699
+ }
8700
+ body {
8701
+ @apply bg-background text-foreground;
8702
+ }
8703
+ }
8704
+ `;
8705
+ const utilsTs = `import { type ClassValue, clsx } from 'clsx';
8706
+ import { twMerge } from 'tailwind-merge';
8707
+
8708
+ export function cn(...inputs: ClassValue[]) {
8709
+ return twMerge(clsx(inputs));
8710
+ }
8711
+ `;
8712
+ const buttonTsx = `import { Slot } from '@radix-ui/react-slot';
8713
+ import { cva, type VariantProps } from 'class-variance-authority';
8714
+ import * as React from 'react';
8715
+
8716
+ import { cn } from '~/lib/utils';
8717
+
8718
+ const buttonVariants = cva(
8719
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
8720
+ {
8721
+ variants: {
8722
+ variant: {
8723
+ default:
8724
+ 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
8725
+ destructive:
8726
+ 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
8727
+ outline:
8728
+ 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
8729
+ secondary:
8730
+ 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
8731
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
8732
+ link: 'text-primary underline-offset-4 hover:underline',
8733
+ },
8734
+ size: {
8735
+ default: 'h-9 px-4 py-2',
8736
+ sm: 'h-8 rounded-md px-3 text-xs',
8737
+ lg: 'h-10 rounded-md px-8',
8738
+ icon: 'h-9 w-9',
8739
+ },
8740
+ },
8741
+ defaultVariants: {
8742
+ variant: 'default',
8743
+ size: 'default',
8744
+ },
8745
+ },
8746
+ );
8747
+
8748
+ export interface ButtonProps
8749
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
8750
+ VariantProps<typeof buttonVariants> {
8751
+ asChild?: boolean;
8752
+ }
8753
+
8754
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
8755
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
8756
+ const Comp = asChild ? Slot : 'button';
8757
+ return (
8758
+ <Comp
8759
+ className={cn(buttonVariants({ variant, size, className }))}
8760
+ ref={ref}
8761
+ {...props}
8762
+ />
8763
+ );
8764
+ },
8765
+ );
8766
+ Button.displayName = 'Button';
8767
+
8768
+ export { Button, buttonVariants };
8769
+ `;
8770
+ const buttonStories = `import type { Meta, StoryObj } from '@storybook/react';
8771
+ import { Button } from '.';
8772
+
8773
+ const meta: Meta<typeof Button> = {
8774
+ title: 'Components/Button',
8775
+ component: Button,
8776
+ tags: ['autodocs'],
8777
+ argTypes: {
8778
+ variant: {
8779
+ control: 'select',
8780
+ options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
8781
+ },
8782
+ size: {
8783
+ control: 'select',
8784
+ options: ['default', 'sm', 'lg', 'icon'],
8785
+ },
8786
+ },
8787
+ };
8788
+
8789
+ export default meta;
8790
+ type Story = StoryObj<typeof Button>;
8791
+
8792
+ export const Default: Story = {
8793
+ args: {
8794
+ children: 'Button',
8795
+ variant: 'default',
8796
+ size: 'default',
8797
+ },
8798
+ };
8799
+
8800
+ export const Secondary: Story = {
8801
+ args: {
8802
+ children: 'Secondary',
8803
+ variant: 'secondary',
8804
+ },
8805
+ };
8806
+
8807
+ export const Destructive: Story = {
8808
+ args: {
8809
+ children: 'Destructive',
8810
+ variant: 'destructive',
8811
+ },
8812
+ };
8813
+
8814
+ export const Outline: Story = {
8815
+ args: {
8816
+ children: 'Outline',
8817
+ variant: 'outline',
8818
+ },
8819
+ };
8820
+
8821
+ export const Ghost: Story = {
8822
+ args: {
8823
+ children: 'Ghost',
8824
+ variant: 'ghost',
8825
+ },
8826
+ };
8827
+
8828
+ export const Link: Story = {
8829
+ args: {
8830
+ children: 'Link',
8831
+ variant: 'link',
8832
+ },
8833
+ };
8834
+ `;
8835
+ const inputTsx = `import * as React from 'react';
8836
+
8837
+ import { cn } from '~/lib/utils';
8838
+
8839
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
8840
+ ({ className, type, ...props }, ref) => {
8841
+ return (
8842
+ <input
8843
+ type={type}
8844
+ className={cn(
8845
+ 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
8846
+ className,
8847
+ )}
8848
+ ref={ref}
8849
+ {...props}
8850
+ />
8851
+ );
8852
+ },
8853
+ );
8854
+ Input.displayName = 'Input';
8855
+
8856
+ export { Input };
8857
+ `;
8858
+ const cardTsx = `import * as React from 'react';
8859
+
8860
+ import { cn } from '~/lib/utils';
8861
+
8862
+ const Card = React.forwardRef<
8863
+ HTMLDivElement,
8864
+ React.HTMLAttributes<HTMLDivElement>
8865
+ >(({ className, ...props }, ref) => (
8866
+ <div
8867
+ ref={ref}
8868
+ className={cn(
8869
+ 'rounded-xl border bg-card text-card-foreground shadow',
8870
+ className,
8871
+ )}
8872
+ {...props}
8873
+ />
8874
+ ));
8875
+ Card.displayName = 'Card';
8876
+
8877
+ const CardHeader = React.forwardRef<
8878
+ HTMLDivElement,
8879
+ React.HTMLAttributes<HTMLDivElement>
8880
+ >(({ className, ...props }, ref) => (
8881
+ <div
8882
+ ref={ref}
8883
+ className={cn('flex flex-col space-y-1.5 p-6', className)}
8884
+ {...props}
8885
+ />
8886
+ ));
8887
+ CardHeader.displayName = 'CardHeader';
8888
+
8889
+ const CardTitle = React.forwardRef<
8890
+ HTMLDivElement,
8891
+ React.HTMLAttributes<HTMLDivElement>
8892
+ >(({ className, ...props }, ref) => (
8893
+ <div
8894
+ ref={ref}
8895
+ className={cn('font-semibold leading-none tracking-tight', className)}
8896
+ {...props}
8897
+ />
8898
+ ));
8899
+ CardTitle.displayName = 'CardTitle';
8900
+
8901
+ const CardDescription = React.forwardRef<
8902
+ HTMLDivElement,
8903
+ React.HTMLAttributes<HTMLDivElement>
8904
+ >(({ className, ...props }, ref) => (
8905
+ <div
8906
+ ref={ref}
8907
+ className={cn('text-sm text-muted-foreground', className)}
8908
+ {...props}
8909
+ />
8910
+ ));
8911
+ CardDescription.displayName = 'CardDescription';
8912
+
8913
+ const CardContent = React.forwardRef<
8914
+ HTMLDivElement,
8915
+ React.HTMLAttributes<HTMLDivElement>
8916
+ >(({ className, ...props }, ref) => (
8917
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
8918
+ ));
8919
+ CardContent.displayName = 'CardContent';
8920
+
8921
+ const CardFooter = React.forwardRef<
8922
+ HTMLDivElement,
8923
+ React.HTMLAttributes<HTMLDivElement>
8924
+ >(({ className, ...props }, ref) => (
8925
+ <div
8926
+ ref={ref}
8927
+ className={cn('flex items-center p-6 pt-0', className)}
8928
+ {...props}
8929
+ />
8930
+ ));
8931
+ CardFooter.displayName = 'CardFooter';
8932
+
8933
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
8934
+ `;
8935
+ const inputStories = `import type { Meta, StoryObj } from '@storybook/react';
8936
+ import { Input } from '.';
8937
+
8938
+ const meta: Meta<typeof Input> = {
8939
+ title: 'Components/Input',
8940
+ component: Input,
8941
+ tags: ['autodocs'],
8942
+ argTypes: {
8943
+ type: {
8944
+ control: 'select',
8945
+ options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url'],
8946
+ },
8947
+ disabled: {
8948
+ control: 'boolean',
8949
+ },
8950
+ },
8951
+ };
8952
+
8953
+ export default meta;
8954
+ type Story = StoryObj<typeof Input>;
8955
+
8956
+ export const Default: Story = {
8957
+ args: {
8958
+ placeholder: 'Enter text...',
8959
+ },
8960
+ };
8961
+
8962
+ export const Email: Story = {
8963
+ args: {
8964
+ type: 'email',
8965
+ placeholder: 'Enter email...',
8966
+ },
8967
+ };
8968
+
8969
+ export const Password: Story = {
8970
+ args: {
8971
+ type: 'password',
8972
+ placeholder: 'Enter password...',
8973
+ },
8974
+ };
8975
+
8976
+ export const Disabled: Story = {
8977
+ args: {
8978
+ placeholder: 'Disabled input',
8979
+ disabled: true,
8980
+ },
8981
+ };
8982
+
8983
+ export const WithValue: Story = {
8984
+ args: {
8985
+ defaultValue: 'Hello World',
8986
+ },
8987
+ };
8988
+ `;
8989
+ const cardStories = `import type { Meta, StoryObj } from '@storybook/react';
8990
+ import { Button } from '../button';
8991
+ import {
8992
+ Card,
8993
+ CardContent,
8994
+ CardDescription,
8995
+ CardFooter,
8996
+ CardHeader,
8997
+ CardTitle,
8998
+ } from '.';
8999
+ import { Input } from '../input';
9000
+
9001
+ const meta: Meta<typeof Card> = {
9002
+ title: 'Components/Card',
9003
+ component: Card,
9004
+ tags: ['autodocs'],
9005
+ };
9006
+
9007
+ export default meta;
9008
+ type Story = StoryObj<typeof Card>;
9009
+
9010
+ export const Default: Story = {
9011
+ render: () => (
9012
+ <Card className="w-[350px]">
9013
+ <CardHeader>
9014
+ <CardTitle>Card Title</CardTitle>
9015
+ <CardDescription>Card description goes here.</CardDescription>
9016
+ </CardHeader>
9017
+ <CardContent>
9018
+ <p>Card content goes here.</p>
9019
+ </CardContent>
9020
+ </Card>
9021
+ ),
9022
+ };
9023
+
9024
+ export const WithFooter: Story = {
9025
+ render: () => (
9026
+ <Card className="w-[350px]">
9027
+ <CardHeader>
9028
+ <CardTitle>Create Account</CardTitle>
9029
+ <CardDescription>Enter your details below.</CardDescription>
9030
+ </CardHeader>
9031
+ <CardContent className="space-y-4">
9032
+ <Input placeholder="Email" type="email" />
9033
+ <Input placeholder="Password" type="password" />
9034
+ </CardContent>
9035
+ <CardFooter className="flex justify-between">
9036
+ <Button variant="outline">Cancel</Button>
9037
+ <Button>Create</Button>
9038
+ </CardFooter>
9039
+ </Card>
9040
+ ),
9041
+ };
9042
+
9043
+ export const Simple: Story = {
9044
+ render: () => (
9045
+ <Card className="w-[350px] p-6">
9046
+ <p>Simple card with just content.</p>
9047
+ </Card>
9048
+ ),
9049
+ };
9050
+ `;
9051
+ const labelTsx = `import * as LabelPrimitive from '@radix-ui/react-label';
9052
+ import { cva, type VariantProps } from 'class-variance-authority';
9053
+ import * as React from 'react';
9054
+
9055
+ import { cn } from '~/lib/utils';
9056
+
9057
+ const labelVariants = cva(
9058
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
9059
+ );
9060
+
9061
+ const Label = React.forwardRef<
9062
+ React.ElementRef<typeof LabelPrimitive.Root>,
9063
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
9064
+ VariantProps<typeof labelVariants>
9065
+ >(({ className, ...props }, ref) => (
9066
+ <LabelPrimitive.Root
9067
+ ref={ref}
9068
+ className={cn(labelVariants(), className)}
9069
+ {...props}
9070
+ />
9071
+ ));
9072
+ Label.displayName = LabelPrimitive.Root.displayName;
9073
+
9074
+ export { Label };
9075
+ `;
9076
+ const labelStories = `import type { Meta, StoryObj } from '@storybook/react';
9077
+ import { Input } from '../input';
9078
+ import { Label } from '.';
9079
+
9080
+ const meta: Meta<typeof Label> = {
9081
+ title: 'Components/Label',
9082
+ component: Label,
9083
+ tags: ['autodocs'],
9084
+ };
9085
+
9086
+ export default meta;
9087
+ type Story = StoryObj<typeof Label>;
9088
+
9089
+ export const Default: Story = {
9090
+ args: {
9091
+ children: 'Label',
9092
+ },
9093
+ };
9094
+
9095
+ export const WithInput: Story = {
9096
+ render: () => (
9097
+ <div className="grid w-full max-w-sm items-center gap-1.5">
9098
+ <Label htmlFor="email">Email</Label>
9099
+ <Input type="email" id="email" placeholder="Email" />
9100
+ </div>
9101
+ ),
9102
+ };
9103
+
9104
+ export const Disabled: Story = {
9105
+ render: () => (
9106
+ <div className="grid w-full max-w-sm items-center gap-1.5">
9107
+ <Label htmlFor="disabled" className="peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
9108
+ Disabled
9109
+ </Label>
9110
+ <Input type="text" id="disabled" placeholder="Disabled" disabled className="peer" />
9111
+ </div>
9112
+ ),
9113
+ };
9114
+ `;
9115
+ const badgeTsx = `import { cva, type VariantProps } from 'class-variance-authority';
9116
+ import * as React from 'react';
9117
+
9118
+ import { cn } from '~/lib/utils';
9119
+
9120
+ const badgeVariants = cva(
9121
+ 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
9122
+ {
9123
+ variants: {
9124
+ variant: {
9125
+ default:
9126
+ 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
9127
+ secondary:
9128
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
9129
+ destructive:
9130
+ 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
9131
+ outline: 'text-foreground',
9132
+ },
9133
+ },
9134
+ defaultVariants: {
9135
+ variant: 'default',
9136
+ },
9137
+ },
9138
+ );
9139
+
9140
+ export interface BadgeProps
9141
+ extends React.HTMLAttributes<HTMLDivElement>,
9142
+ VariantProps<typeof badgeVariants> {}
9143
+
9144
+ function Badge({ className, variant, ...props }: BadgeProps) {
9145
+ return (
9146
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
9147
+ );
9148
+ }
9149
+
9150
+ export { Badge, badgeVariants };
9151
+ `;
9152
+ const badgeStories = `import type { Meta, StoryObj } from '@storybook/react';
9153
+ import { Badge } from '.';
9154
+
9155
+ const meta: Meta<typeof Badge> = {
9156
+ title: 'Components/Badge',
9157
+ component: Badge,
9158
+ tags: ['autodocs'],
9159
+ argTypes: {
9160
+ variant: {
9161
+ control: 'select',
9162
+ options: ['default', 'secondary', 'destructive', 'outline'],
9163
+ },
9164
+ },
9165
+ };
9166
+
9167
+ export default meta;
9168
+ type Story = StoryObj<typeof Badge>;
9169
+
9170
+ export const Default: Story = {
9171
+ args: {
9172
+ children: 'Badge',
9173
+ variant: 'default',
9174
+ },
9175
+ };
9176
+
9177
+ export const Secondary: Story = {
9178
+ args: {
9179
+ children: 'Secondary',
9180
+ variant: 'secondary',
9181
+ },
9182
+ };
9183
+
9184
+ export const Destructive: Story = {
9185
+ args: {
9186
+ children: 'Destructive',
9187
+ variant: 'destructive',
9188
+ },
9189
+ };
9190
+
9191
+ export const Outline: Story = {
9192
+ args: {
9193
+ children: 'Outline',
9194
+ variant: 'outline',
9195
+ },
9196
+ };
9197
+ `;
9198
+ const separatorTsx = `import * as SeparatorPrimitive from '@radix-ui/react-separator';
9199
+ import * as React from 'react';
9200
+
9201
+ import { cn } from '~/lib/utils';
9202
+
9203
+ const Separator = React.forwardRef<
9204
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
9205
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9206
+ >(
9207
+ (
9208
+ { className, orientation = 'horizontal', decorative = true, ...props },
9209
+ ref,
9210
+ ) => (
9211
+ <SeparatorPrimitive.Root
9212
+ ref={ref}
9213
+ decorative={decorative}
9214
+ orientation={orientation}
9215
+ className={cn(
9216
+ 'shrink-0 bg-border',
9217
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
9218
+ className,
9219
+ )}
9220
+ {...props}
9221
+ />
9222
+ ),
9223
+ );
9224
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
9225
+
9226
+ export { Separator };
9227
+ `;
9228
+ const separatorStories = `import type { Meta, StoryObj } from '@storybook/react';
9229
+ import { Separator } from '.';
9230
+
9231
+ const meta: Meta<typeof Separator> = {
9232
+ title: 'Components/Separator',
9233
+ component: Separator,
9234
+ tags: ['autodocs'],
9235
+ argTypes: {
9236
+ orientation: {
9237
+ control: 'select',
9238
+ options: ['horizontal', 'vertical'],
9239
+ },
9240
+ },
9241
+ };
9242
+
9243
+ export default meta;
9244
+ type Story = StoryObj<typeof Separator>;
9245
+
9246
+ export const Horizontal: Story = {
9247
+ render: () => (
9248
+ <div className="w-[300px]">
9249
+ <div className="space-y-1">
9250
+ <h4 className="text-sm font-medium leading-none">Radix Primitives</h4>
9251
+ <p className="text-sm text-muted-foreground">
9252
+ An open-source UI component library.
9253
+ </p>
9254
+ </div>
9255
+ <Separator className="my-4" />
9256
+ <div className="flex h-5 items-center space-x-4 text-sm">
9257
+ <div>Blog</div>
9258
+ <Separator orientation="vertical" />
9259
+ <div>Docs</div>
9260
+ <Separator orientation="vertical" />
9261
+ <div>Source</div>
9262
+ </div>
9263
+ </div>
9264
+ ),
9265
+ };
9266
+
9267
+ export const Vertical: Story = {
9268
+ render: () => (
9269
+ <div className="flex h-5 items-center space-x-4 text-sm">
9270
+ <div>Blog</div>
9271
+ <Separator orientation="vertical" />
9272
+ <div>Docs</div>
9273
+ <Separator orientation="vertical" />
9274
+ <div>Source</div>
9275
+ </div>
9276
+ ),
9277
+ };
9278
+ `;
9279
+ const tabsTsx = `import * as TabsPrimitive from '@radix-ui/react-tabs';
9280
+ import * as React from 'react';
9281
+
9282
+ import { cn } from '~/lib/utils';
9283
+
9284
+ const Tabs = TabsPrimitive.Root;
9285
+
9286
+ const TabsList = React.forwardRef<
9287
+ React.ElementRef<typeof TabsPrimitive.List>,
9288
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
9289
+ >(({ className, ...props }, ref) => (
9290
+ <TabsPrimitive.List
9291
+ ref={ref}
9292
+ className={cn(
9293
+ 'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
9294
+ className,
9295
+ )}
9296
+ {...props}
9297
+ />
9298
+ ));
9299
+ TabsList.displayName = TabsPrimitive.List.displayName;
9300
+
9301
+ const TabsTrigger = React.forwardRef<
9302
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
9303
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
9304
+ >(({ className, ...props }, ref) => (
9305
+ <TabsPrimitive.Trigger
9306
+ ref={ref}
9307
+ className={cn(
9308
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
9309
+ className,
9310
+ )}
9311
+ {...props}
9312
+ />
9313
+ ));
9314
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
9315
+
9316
+ const TabsContent = React.forwardRef<
9317
+ React.ElementRef<typeof TabsPrimitive.Content>,
9318
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
9319
+ >(({ className, ...props }, ref) => (
9320
+ <TabsPrimitive.Content
9321
+ ref={ref}
9322
+ className={cn(
9323
+ 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
9324
+ className,
9325
+ )}
9326
+ {...props}
9327
+ />
9328
+ ));
9329
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
9330
+
9331
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
9332
+ `;
9333
+ const tabsStories = `import type { Meta, StoryObj } from '@storybook/react';
9334
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '.';
9335
+ import { Button } from '../button';
9336
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../card';
9337
+ import { Input } from '../input';
9338
+ import { Label } from '../label';
9339
+
9340
+ const meta: Meta<typeof Tabs> = {
9341
+ title: 'Components/Tabs',
9342
+ component: Tabs,
9343
+ tags: ['autodocs'],
9344
+ };
9345
+
9346
+ export default meta;
9347
+ type Story = StoryObj<typeof Tabs>;
9348
+
9349
+ export const Default: Story = {
9350
+ render: () => (
9351
+ <Tabs defaultValue="account" className="w-[400px]">
9352
+ <TabsList>
9353
+ <TabsTrigger value="account">Account</TabsTrigger>
9354
+ <TabsTrigger value="password">Password</TabsTrigger>
9355
+ </TabsList>
9356
+ <TabsContent value="account">
9357
+ <Card>
9358
+ <CardHeader>
9359
+ <CardTitle>Account</CardTitle>
9360
+ <CardDescription>
9361
+ Make changes to your account here. Click save when you're done.
9362
+ </CardDescription>
9363
+ </CardHeader>
9364
+ <CardContent className="space-y-2">
9365
+ <div className="space-y-1">
9366
+ <Label htmlFor="name">Name</Label>
9367
+ <Input id="name" defaultValue="Pedro Duarte" />
9368
+ </div>
9369
+ <div className="space-y-1">
9370
+ <Label htmlFor="username">Username</Label>
9371
+ <Input id="username" defaultValue="@peduarte" />
9372
+ </div>
9373
+ </CardContent>
9374
+ <CardFooter>
9375
+ <Button>Save changes</Button>
9376
+ </CardFooter>
9377
+ </Card>
9378
+ </TabsContent>
9379
+ <TabsContent value="password">
9380
+ <Card>
9381
+ <CardHeader>
9382
+ <CardTitle>Password</CardTitle>
9383
+ <CardDescription>
9384
+ Change your password here. After saving, you'll be logged out.
9385
+ </CardDescription>
9386
+ </CardHeader>
9387
+ <CardContent className="space-y-2">
9388
+ <div className="space-y-1">
9389
+ <Label htmlFor="current">Current password</Label>
9390
+ <Input id="current" type="password" />
9391
+ </div>
9392
+ <div className="space-y-1">
9393
+ <Label htmlFor="new">New password</Label>
9394
+ <Input id="new" type="password" />
9395
+ </div>
9396
+ </CardContent>
9397
+ <CardFooter>
9398
+ <Button>Save password</Button>
9399
+ </CardFooter>
9400
+ </Card>
9401
+ </TabsContent>
9402
+ </Tabs>
9403
+ ),
9404
+ };
9405
+ `;
9406
+ const tooltipTsx = `import * as TooltipPrimitive from '@radix-ui/react-tooltip';
9407
+ import * as React from 'react';
9408
+
9409
+ import { cn } from '~/lib/utils';
9410
+
9411
+ const TooltipProvider = TooltipPrimitive.Provider;
9412
+
9413
+ const Tooltip = TooltipPrimitive.Root;
9414
+
9415
+ const TooltipTrigger = TooltipPrimitive.Trigger;
9416
+
9417
+ const TooltipContent = React.forwardRef<
9418
+ React.ElementRef<typeof TooltipPrimitive.Content>,
9419
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
9420
+ >(({ className, sideOffset = 4, ...props }, ref) => (
9421
+ <TooltipPrimitive.Portal>
9422
+ <TooltipPrimitive.Content
9423
+ ref={ref}
9424
+ sideOffset={sideOffset}
9425
+ className={cn(
9426
+ 'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
9427
+ className,
9428
+ )}
9429
+ {...props}
9430
+ />
9431
+ </TooltipPrimitive.Portal>
9432
+ ));
9433
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
9434
+
9435
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
9436
+ `;
9437
+ const tooltipStories = `import type { Meta, StoryObj } from '@storybook/react';
9438
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.';
9439
+ import { Button } from '../button';
9440
+
9441
+ const meta: Meta<typeof Tooltip> = {
9442
+ title: 'Components/Tooltip',
9443
+ component: Tooltip,
9444
+ tags: ['autodocs'],
9445
+ decorators: [
9446
+ (Story) => (
9447
+ <TooltipProvider>
9448
+ <Story />
9449
+ </TooltipProvider>
9450
+ ),
9451
+ ],
9452
+ };
9453
+
9454
+ export default meta;
9455
+ type Story = StoryObj<typeof Tooltip>;
9456
+
9457
+ export const Default: Story = {
9458
+ render: () => (
9459
+ <Tooltip>
9460
+ <TooltipTrigger asChild>
9461
+ <Button variant="outline">Hover me</Button>
9462
+ </TooltipTrigger>
9463
+ <TooltipContent>
9464
+ <p>Add to library</p>
9465
+ </TooltipContent>
9466
+ </Tooltip>
9467
+ ),
9468
+ };
9469
+
9470
+ export const Positions: Story = {
9471
+ render: () => (
9472
+ <div className="flex gap-4">
9473
+ <Tooltip>
9474
+ <TooltipTrigger asChild>
9475
+ <Button variant="outline">Top</Button>
9476
+ </TooltipTrigger>
9477
+ <TooltipContent side="top">
9478
+ <p>Top tooltip</p>
9479
+ </TooltipContent>
9480
+ </Tooltip>
9481
+ <Tooltip>
9482
+ <TooltipTrigger asChild>
9483
+ <Button variant="outline">Bottom</Button>
9484
+ </TooltipTrigger>
9485
+ <TooltipContent side="bottom">
9486
+ <p>Bottom tooltip</p>
9487
+ </TooltipContent>
9488
+ </Tooltip>
9489
+ <Tooltip>
9490
+ <TooltipTrigger asChild>
9491
+ <Button variant="outline">Left</Button>
9492
+ </TooltipTrigger>
9493
+ <TooltipContent side="left">
9494
+ <p>Left tooltip</p>
9495
+ </TooltipContent>
9496
+ </Tooltip>
9497
+ <Tooltip>
9498
+ <TooltipTrigger asChild>
9499
+ <Button variant="outline">Right</Button>
9500
+ </TooltipTrigger>
9501
+ <TooltipContent side="right">
9502
+ <p>Right tooltip</p>
9503
+ </TooltipContent>
9504
+ </Tooltip>
9505
+ </div>
9506
+ ),
9507
+ };
9508
+ `;
9509
+ const dialogTsx = `import * as DialogPrimitive from '@radix-ui/react-dialog';
9510
+ import { X } from 'lucide-react';
9511
+ import * as React from 'react';
9512
+
9513
+ import { cn } from '~/lib/utils';
9514
+
9515
+ const Dialog = DialogPrimitive.Root;
9516
+
9517
+ const DialogTrigger = DialogPrimitive.Trigger;
9518
+
9519
+ const DialogPortal = DialogPrimitive.Portal;
9520
+
9521
+ const DialogClose = DialogPrimitive.Close;
9522
+
9523
+ const DialogOverlay = React.forwardRef<
9524
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
9525
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
9526
+ >(({ className, ...props }, ref) => (
9527
+ <DialogPrimitive.Overlay
9528
+ ref={ref}
9529
+ className={cn(
9530
+ 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
9531
+ className,
9532
+ )}
9533
+ {...props}
9534
+ />
9535
+ ));
9536
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
9537
+
9538
+ const DialogContent = React.forwardRef<
9539
+ React.ElementRef<typeof DialogPrimitive.Content>,
9540
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
9541
+ >(({ className, children, ...props }, ref) => (
9542
+ <DialogPortal>
9543
+ <DialogOverlay />
9544
+ <DialogPrimitive.Content
9545
+ ref={ref}
9546
+ className={cn(
9547
+ 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
9548
+ className,
9549
+ )}
9550
+ {...props}
9551
+ >
9552
+ {children}
9553
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
9554
+ <X className="h-4 w-4" />
9555
+ <span className="sr-only">Close</span>
9556
+ </DialogPrimitive.Close>
9557
+ </DialogPrimitive.Content>
9558
+ </DialogPortal>
9559
+ ));
9560
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
9561
+
9562
+ const DialogHeader = ({
9563
+ className,
9564
+ ...props
9565
+ }: React.HTMLAttributes<HTMLDivElement>) => (
9566
+ <div
9567
+ className={cn(
9568
+ 'flex flex-col space-y-1.5 text-center sm:text-left',
9569
+ className,
9570
+ )}
9571
+ {...props}
9572
+ />
9573
+ );
9574
+ DialogHeader.displayName = 'DialogHeader';
9575
+
9576
+ const DialogFooter = ({
9577
+ className,
9578
+ ...props
9579
+ }: React.HTMLAttributes<HTMLDivElement>) => (
9580
+ <div
9581
+ className={cn(
9582
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
9583
+ className,
9584
+ )}
9585
+ {...props}
9586
+ />
9587
+ );
9588
+ DialogFooter.displayName = 'DialogFooter';
9589
+
9590
+ const DialogTitle = React.forwardRef<
9591
+ React.ElementRef<typeof DialogPrimitive.Title>,
9592
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
9593
+ >(({ className, ...props }, ref) => (
9594
+ <DialogPrimitive.Title
9595
+ ref={ref}
9596
+ className={cn(
9597
+ 'text-lg font-semibold leading-none tracking-tight',
9598
+ className,
9599
+ )}
9600
+ {...props}
9601
+ />
9602
+ ));
9603
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
9604
+
9605
+ const DialogDescription = React.forwardRef<
9606
+ React.ElementRef<typeof DialogPrimitive.Description>,
9607
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
9608
+ >(({ className, ...props }, ref) => (
9609
+ <DialogPrimitive.Description
9610
+ ref={ref}
9611
+ className={cn('text-sm text-muted-foreground', className)}
9612
+ {...props}
9613
+ />
9614
+ ));
9615
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
9616
+
9617
+ export {
9618
+ Dialog,
9619
+ DialogPortal,
9620
+ DialogOverlay,
9621
+ DialogTrigger,
9622
+ DialogClose,
9623
+ DialogContent,
9624
+ DialogHeader,
9625
+ DialogFooter,
9626
+ DialogTitle,
9627
+ DialogDescription,
9628
+ };
9629
+ `;
9630
+ const dialogStories = `import type { Meta, StoryObj } from '@storybook/react';
9631
+ import {
9632
+ Dialog,
9633
+ DialogContent,
9634
+ DialogDescription,
9635
+ DialogFooter,
9636
+ DialogHeader,
9637
+ DialogTitle,
9638
+ DialogTrigger,
9639
+ } from '.';
9640
+ import { Button } from '../button';
9641
+ import { Input } from '../input';
9642
+ import { Label } from '../label';
9643
+
9644
+ const meta: Meta<typeof Dialog> = {
9645
+ title: 'Components/Dialog',
9646
+ component: Dialog,
9647
+ tags: ['autodocs'],
9648
+ };
9649
+
9650
+ export default meta;
9651
+ type Story = StoryObj<typeof Dialog>;
9652
+
9653
+ export const Default: Story = {
9654
+ render: () => (
9655
+ <Dialog>
9656
+ <DialogTrigger asChild>
9657
+ <Button variant="outline">Edit Profile</Button>
9658
+ </DialogTrigger>
9659
+ <DialogContent className="sm:max-w-[425px]">
9660
+ <DialogHeader>
9661
+ <DialogTitle>Edit profile</DialogTitle>
9662
+ <DialogDescription>
9663
+ Make changes to your profile here. Click save when you're done.
9664
+ </DialogDescription>
9665
+ </DialogHeader>
9666
+ <div className="grid gap-4 py-4">
9667
+ <div className="grid grid-cols-4 items-center gap-4">
9668
+ <Label htmlFor="name" className="text-right">
9669
+ Name
9670
+ </Label>
9671
+ <Input id="name" defaultValue="Pedro Duarte" className="col-span-3" />
9672
+ </div>
9673
+ <div className="grid grid-cols-4 items-center gap-4">
9674
+ <Label htmlFor="username" className="text-right">
9675
+ Username
9676
+ </Label>
9677
+ <Input id="username" defaultValue="@peduarte" className="col-span-3" />
9678
+ </div>
9679
+ </div>
9680
+ <DialogFooter>
9681
+ <Button type="submit">Save changes</Button>
9682
+ </DialogFooter>
9683
+ </DialogContent>
9684
+ </Dialog>
9685
+ ),
9686
+ };
9687
+
9688
+ export const Alert: Story = {
9689
+ render: () => (
9690
+ <Dialog>
9691
+ <DialogTrigger asChild>
9692
+ <Button variant="destructive">Delete Account</Button>
9693
+ </DialogTrigger>
9694
+ <DialogContent>
9695
+ <DialogHeader>
9696
+ <DialogTitle>Are you absolutely sure?</DialogTitle>
9697
+ <DialogDescription>
9698
+ This action cannot be undone. This will permanently delete your
9699
+ account and remove your data from our servers.
9700
+ </DialogDescription>
9701
+ </DialogHeader>
9702
+ <DialogFooter>
9703
+ <Button variant="outline">Cancel</Button>
9704
+ <Button variant="destructive">Delete</Button>
9705
+ </DialogFooter>
9706
+ </DialogContent>
9707
+ </Dialog>
9708
+ ),
9709
+ };
9710
+ `;
9711
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button';
9712
+ export { Input } from './input';
9713
+ export {
9714
+ Card,
9715
+ CardHeader,
9716
+ CardFooter,
9717
+ CardTitle,
9718
+ CardDescription,
9719
+ CardContent,
9720
+ } from './card';
9721
+ export { Label } from './label';
9722
+ export { Badge, type BadgeProps, badgeVariants } from './badge';
9723
+ export { Separator } from './separator';
9724
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
9725
+ export {
9726
+ Tooltip,
9727
+ TooltipTrigger,
9728
+ TooltipContent,
9729
+ TooltipProvider,
9730
+ } from './tooltip';
9731
+ export {
9732
+ Dialog,
9733
+ DialogPortal,
9734
+ DialogOverlay,
9735
+ DialogTrigger,
9736
+ DialogClose,
9737
+ DialogContent,
9738
+ DialogHeader,
9739
+ DialogFooter,
9740
+ DialogTitle,
9741
+ DialogDescription,
9742
+ } from './dialog';
9743
+ `;
9744
+ const buttonIndexTsx = buttonTsx;
9745
+ const inputIndexTsx = inputTsx;
9746
+ const cardIndexTsx = cardTsx;
9747
+ const componentsIndex = `export * from './ui';
9748
+ `;
9749
+ const indexTs = `// @${options.name}/ui - Shared UI component library
9750
+
9751
+ // shadcn/ui components
9752
+ export * from './components';
9753
+
9754
+ // Utilities
9755
+ export { cn } from './lib/utils';
9756
+ `;
9757
+ const gitignore = `node_modules/
9758
+ dist/
9759
+ storybook-static/
9760
+ *.log
9761
+ `;
9762
+ return [
9763
+ {
9764
+ path: "packages/ui/package.json",
9765
+ content: `${JSON.stringify(packageJson, null, 2)}\n`
9766
+ },
9767
+ {
9768
+ path: "packages/ui/tsconfig.json",
9769
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`
9770
+ },
9771
+ {
9772
+ path: "packages/ui/components.json",
9773
+ content: `${JSON.stringify(componentsJson, null, 2)}\n`
9774
+ },
9775
+ {
9776
+ path: "packages/ui/.storybook/main.ts",
9777
+ content: storybookMain
9778
+ },
9779
+ {
9780
+ path: "packages/ui/.storybook/preview.ts",
9781
+ content: storybookPreview
9782
+ },
9783
+ {
9784
+ path: "packages/ui/src/styles/globals.css",
9785
+ content: globalsCss
9786
+ },
9787
+ {
9788
+ path: "packages/ui/src/lib/utils.ts",
9789
+ content: utilsTs
9790
+ },
9791
+ {
9792
+ path: "packages/ui/src/components/ui/button/index.tsx",
9793
+ content: buttonIndexTsx
9794
+ },
9795
+ {
9796
+ path: "packages/ui/src/components/ui/button/button.stories.tsx",
9797
+ content: buttonStories
9798
+ },
9799
+ {
9800
+ path: "packages/ui/src/components/ui/input/index.tsx",
9801
+ content: inputIndexTsx
9802
+ },
9803
+ {
9804
+ path: "packages/ui/src/components/ui/input/input.stories.tsx",
9805
+ content: inputStories
9806
+ },
9807
+ {
9808
+ path: "packages/ui/src/components/ui/card/index.tsx",
9809
+ content: cardIndexTsx
9810
+ },
9811
+ {
9812
+ path: "packages/ui/src/components/ui/card/card.stories.tsx",
9813
+ content: cardStories
9814
+ },
9815
+ {
9816
+ path: "packages/ui/src/components/ui/label/index.tsx",
9817
+ content: labelTsx
9818
+ },
9819
+ {
9820
+ path: "packages/ui/src/components/ui/label/label.stories.tsx",
9821
+ content: labelStories
9822
+ },
9823
+ {
9824
+ path: "packages/ui/src/components/ui/badge/index.tsx",
9825
+ content: badgeTsx
9826
+ },
9827
+ {
9828
+ path: "packages/ui/src/components/ui/badge/badge.stories.tsx",
9829
+ content: badgeStories
9830
+ },
9831
+ {
9832
+ path: "packages/ui/src/components/ui/separator/index.tsx",
9833
+ content: separatorTsx
9834
+ },
9835
+ {
9836
+ path: "packages/ui/src/components/ui/separator/separator.stories.tsx",
9837
+ content: separatorStories
9838
+ },
9839
+ {
9840
+ path: "packages/ui/src/components/ui/tabs/index.tsx",
9841
+ content: tabsTsx
9842
+ },
9843
+ {
9844
+ path: "packages/ui/src/components/ui/tabs/tabs.stories.tsx",
9845
+ content: tabsStories
9846
+ },
9847
+ {
9848
+ path: "packages/ui/src/components/ui/tooltip/index.tsx",
9849
+ content: tooltipTsx
9850
+ },
9851
+ {
9852
+ path: "packages/ui/src/components/ui/tooltip/tooltip.stories.tsx",
9853
+ content: tooltipStories
9854
+ },
9855
+ {
9856
+ path: "packages/ui/src/components/ui/dialog/index.tsx",
9857
+ content: dialogTsx
9858
+ },
9859
+ {
9860
+ path: "packages/ui/src/components/ui/dialog/dialog.stories.tsx",
9861
+ content: dialogStories
9862
+ },
9863
+ {
9864
+ path: "packages/ui/src/components/ui/index.ts",
9865
+ content: componentsUiIndex
9866
+ },
9867
+ {
9868
+ path: "packages/ui/src/components/index.ts",
9869
+ content: componentsIndex
9870
+ },
9871
+ {
9872
+ path: "packages/ui/src/index.ts",
9873
+ content: indexTs
9874
+ },
9875
+ {
9876
+ path: "packages/ui/.gitignore",
9877
+ content: gitignore
9878
+ }
9879
+ ];
9880
+ }
9881
+
8364
9882
  //#endregion
8365
9883
  //#region src/init/generators/web.ts
8366
9884
  /**
@@ -8370,29 +9888,35 @@ function generateWebAppFiles(options) {
8370
9888
  if (!options.monorepo || options.template !== "fullstack") return [];
8371
9889
  const packageName = `@${options.name}/web`;
8372
9890
  const modelsPackage = `@${options.name}/models`;
9891
+ const uiPackage = `@${options.name}/ui`;
8373
9892
  const packageJson = {
8374
9893
  name: packageName,
8375
9894
  version: "0.0.1",
8376
9895
  private: true,
8377
9896
  type: "module",
8378
9897
  scripts: {
8379
- dev: "next dev -p 3001",
8380
- build: "next build",
9898
+ dev: "gkm exec -- next dev --turbopack",
9899
+ build: "gkm exec -- next build",
8381
9900
  start: "next start",
8382
9901
  typecheck: "tsc --noEmit"
8383
9902
  },
8384
9903
  dependencies: {
8385
9904
  [modelsPackage]: "workspace:*",
9905
+ [uiPackage]: "workspace:*",
8386
9906
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
8387
9907
  "@tanstack/react-query": "~5.80.0",
9908
+ "better-auth": "~1.2.0",
8388
9909
  next: "~16.1.0",
8389
9910
  react: "~19.2.0",
8390
9911
  "react-dom": "~19.2.0"
8391
9912
  },
8392
9913
  devDependencies: {
9914
+ "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
9915
+ "@tailwindcss/postcss": "^4.0.0",
8393
9916
  "@types/node": "~22.0.0",
8394
9917
  "@types/react": "~19.0.0",
8395
9918
  "@types/react-dom": "~19.0.0",
9919
+ tailwindcss: "^4.0.0",
8396
9920
  typescript: "~5.8.2"
8397
9921
  }
8398
9922
  };
@@ -8401,10 +9925,16 @@ function generateWebAppFiles(options) {
8401
9925
  const nextConfig: NextConfig = {
8402
9926
  output: 'standalone',
8403
9927
  reactStrictMode: true,
8404
- transpilePackages: ['${modelsPackage}'],
9928
+ transpilePackages: ['${modelsPackage}', '${uiPackage}'],
8405
9929
  };
8406
9930
 
8407
9931
  export default nextConfig;
9932
+ `;
9933
+ const postcssConfig = `export default {
9934
+ plugins: {
9935
+ '@tailwindcss/postcss': {},
9936
+ },
9937
+ };
8408
9938
  `;
8409
9939
  const tsConfig = {
8410
9940
  extends: "../../tsconfig.json",
@@ -8427,9 +9957,11 @@ export default nextConfig;
8427
9957
  incremental: true,
8428
9958
  plugins: [{ name: "next" }],
8429
9959
  paths: {
8430
- "@/*": ["./src/*"],
9960
+ "~/*": ["./src/*"],
8431
9961
  [`${modelsPackage}`]: ["../../packages/models/src"],
8432
- [`${modelsPackage}/*`]: ["../../packages/models/src/*"]
9962
+ [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
9963
+ [`${uiPackage}`]: ["../../packages/ui/src"],
9964
+ [`${uiPackage}/*`]: ["../../packages/ui/src/*"]
8433
9965
  },
8434
9966
  baseUrl: "."
8435
9967
  },
@@ -8441,68 +9973,66 @@ export default nextConfig;
8441
9973
  ],
8442
9974
  exclude: ["node_modules"]
8443
9975
  };
9976
+ const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
9977
+
9978
+ function makeQueryClient() {
9979
+ return new QueryClient({
9980
+ defaultOptions: {
9981
+ queries: {
9982
+ staleTime: 60 * 1000,
9983
+ },
9984
+ },
9985
+ });
9986
+ }
9987
+
9988
+ let browserQueryClient: QueryClient | undefined = undefined;
9989
+
9990
+ export function getQueryClient() {
9991
+ if (typeof window === 'undefined') {
9992
+ // Server: always make a new query client
9993
+ return makeQueryClient();
9994
+ }
9995
+ // Browser: reuse existing query client
9996
+ if (!browserQueryClient) browserQueryClient = makeQueryClient();
9997
+ return browserQueryClient;
9998
+ }
9999
+ `;
10000
+ const authClientTs = `import { createAuthClient } from 'better-auth/react';
10001
+ import { magicLinkClient } from 'better-auth/client/plugins';
10002
+
10003
+ export const authClient = createAuthClient({
10004
+ baseURL: process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:3002',
10005
+ plugins: [magicLinkClient()],
10006
+ });
10007
+
10008
+ export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
10009
+ `;
8444
10010
  const providersTsx = `'use client';
8445
10011
 
8446
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
8447
- import { useState } from 'react';
10012
+ import { QueryClientProvider } from '@tanstack/react-query';
10013
+ import { getQueryClient } from '~/lib/query-client';
8448
10014
 
8449
10015
  export function Providers({ children }: { children: React.ReactNode }) {
8450
- const [queryClient] = useState(
8451
- () =>
8452
- new QueryClient({
8453
- defaultOptions: {
8454
- queries: {
8455
- staleTime: 60 * 1000,
8456
- },
8457
- },
8458
- }),
8459
- );
10016
+ const queryClient = getQueryClient();
8460
10017
 
8461
10018
  return (
8462
10019
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
8463
10020
  );
8464
10021
  }
8465
10022
  `;
8466
- const apiIndexTs = `import { TypedFetcher } from '@geekmidas/client/fetcher';
8467
- import { createEndpointHooks } from '@geekmidas/client/endpoint-hooks';
8468
-
8469
- // TODO: Run 'gkm openapi' to generate typed paths from your API
8470
- // This is a placeholder that will be replaced by the generated openapi.ts
8471
- interface paths {
8472
- '/health': {
8473
- get: {
8474
- responses: {
8475
- 200: {
8476
- content: {
8477
- 'application/json': { status: string; timestamp: string };
8478
- };
8479
- };
8480
- };
8481
- };
8482
- };
8483
- '/users': {
8484
- get: {
8485
- responses: {
8486
- 200: {
8487
- content: {
8488
- 'application/json': { users: Array<{ id: string; name: string }> };
8489
- };
8490
- };
8491
- };
8492
- };
8493
- };
8494
- }
8495
-
8496
- const baseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
10023
+ const apiIndexTs = `import { createApi } from './openapi';
10024
+ import { getQueryClient } from '~/lib/query-client';
8497
10025
 
8498
- const fetcher = new TypedFetcher<paths>({ baseURL });
8499
-
8500
- const hooks = createEndpointHooks<paths>(fetcher.request.bind(fetcher));
8501
-
8502
- export const api = Object.assign(fetcher.request.bind(fetcher), hooks);
10026
+ export const api = createApi({
10027
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
10028
+ queryClient: getQueryClient(),
10029
+ });
10030
+ `;
10031
+ const globalsCss = `@import '${uiPackage}/styles';
8503
10032
  `;
8504
10033
  const layoutTsx = `import type { Metadata } from 'next';
8505
10034
  import { Providers } from './providers';
10035
+ import './globals.css';
8506
10036
 
8507
10037
  export const metadata: Metadata = {
8508
10038
  title: '${options.name}',
@@ -8523,42 +10053,59 @@ export default function RootLayout({
8523
10053
  );
8524
10054
  }
8525
10055
  `;
8526
- const pageTsx = `import { api } from '@/api';
10056
+ const pageTsx = `import { api } from '~/api';
10057
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '${uiPackage}/components';
8527
10058
 
8528
10059
  export default async function Home() {
8529
10060
  // Type-safe API call using the generated client
8530
10061
  const health = await api('GET /health').catch(() => null);
8531
10062
 
8532
10063
  return (
8533
- <main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
8534
- <h1>Welcome to ${options.name}</h1>
8535
-
8536
- <section style={{ marginTop: '2rem' }}>
8537
- <h2>API Status</h2>
8538
- {health ? (
8539
- <pre style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px' }}>
8540
- {JSON.stringify(health, null, 2)}
8541
- </pre>
8542
- ) : (
8543
- <p>Unable to connect to API</p>
8544
- )}
8545
- </section>
8546
-
8547
- <section style={{ marginTop: '2rem' }}>
8548
- <h2>Next Steps</h2>
8549
- <ul>
8550
- <li>Run <code>gkm openapi</code> to generate typed API client</li>
8551
- <li>Edit <code>apps/web/src/app/page.tsx</code> to customize this page</li>
8552
- <li>Add API routes in <code>apps/api/src/endpoints/</code></li>
8553
- <li>Define shared schemas in <code>packages/models/src/</code></li>
8554
- </ul>
8555
- </section>
10064
+ <main className="min-h-screen bg-background p-8">
10065
+ <div className="mx-auto max-w-4xl space-y-8">
10066
+ <div className="space-y-2">
10067
+ <h1 className="text-4xl font-bold tracking-tight">Welcome to ${options.name}</h1>
10068
+ <p className="text-muted-foreground">Your fullstack application is ready.</p>
10069
+ </div>
10070
+
10071
+ <Card>
10072
+ <CardHeader>
10073
+ <CardTitle>API Status</CardTitle>
10074
+ <CardDescription>Connection to your backend API</CardDescription>
10075
+ </CardHeader>
10076
+ <CardContent>
10077
+ {health ? (
10078
+ <pre className="rounded-lg bg-muted p-4 text-sm">
10079
+ {JSON.stringify(health, null, 2)}
10080
+ </pre>
10081
+ ) : (
10082
+ <p className="text-destructive">Unable to connect to API</p>
10083
+ )}
10084
+ </CardContent>
10085
+ </Card>
10086
+
10087
+ <Card>
10088
+ <CardHeader>
10089
+ <CardTitle>Next Steps</CardTitle>
10090
+ <CardDescription>Get started with your project</CardDescription>
10091
+ </CardHeader>
10092
+ <CardContent className="space-y-4">
10093
+ <ul className="list-inside list-disc space-y-2 text-muted-foreground">
10094
+ <li>Run <code className="rounded bg-muted px-1">gkm openapi</code> to generate typed API client</li>
10095
+ <li>Edit <code className="rounded bg-muted px-1">apps/web/src/app/page.tsx</code> to customize this page</li>
10096
+ <li>Add API routes in <code className="rounded bg-muted px-1">apps/api/src/endpoints/</code></li>
10097
+ <li>Add UI components with <code className="rounded bg-muted px-1">npx shadcn@latest add</code> in packages/ui</li>
10098
+ </ul>
10099
+ <div className="flex gap-4">
10100
+ <Button>Get Started</Button>
10101
+ <Button variant="outline">Documentation</Button>
10102
+ </div>
10103
+ </CardContent>
10104
+ </Card>
10105
+ </div>
8556
10106
  </main>
8557
10107
  );
8558
10108
  }
8559
- `;
8560
- const envLocal = `# API URL for client-side requests
8561
- NEXT_PUBLIC_API_URL=http://localhost:3000
8562
10109
  `;
8563
10110
  const gitignore = `.next/
8564
10111
  node_modules/
@@ -8574,10 +10121,18 @@ node_modules/
8574
10121
  path: "apps/web/next.config.ts",
8575
10122
  content: nextConfig
8576
10123
  },
10124
+ {
10125
+ path: "apps/web/postcss.config.mjs",
10126
+ content: postcssConfig
10127
+ },
8577
10128
  {
8578
10129
  path: "apps/web/tsconfig.json",
8579
10130
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
8580
10131
  },
10132
+ {
10133
+ path: "apps/web/src/app/globals.css",
10134
+ content: globalsCss
10135
+ },
8581
10136
  {
8582
10137
  path: "apps/web/src/app/layout.tsx",
8583
10138
  content: layoutTsx
@@ -8591,12 +10146,16 @@ node_modules/
8591
10146
  content: pageTsx
8592
10147
  },
8593
10148
  {
8594
- path: "apps/web/src/api/index.ts",
8595
- content: apiIndexTs
10149
+ path: "apps/web/src/lib/query-client.ts",
10150
+ content: queryClientTs
8596
10151
  },
8597
10152
  {
8598
- path: "apps/web/.env.local",
8599
- content: envLocal
10153
+ path: "apps/web/src/lib/auth-client.ts",
10154
+ content: authClientTs
10155
+ },
10156
+ {
10157
+ path: "apps/web/src/api/index.ts",
10158
+ content: apiIndexTs
8600
10159
  },
8601
10160
  {
8602
10161
  path: "apps/web/.gitignore",
@@ -8835,6 +10394,7 @@ async function initCommand(projectName, options = {}) {
8835
10394
  const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
8836
10395
  const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
8837
10396
  const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
10397
+ const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
8838
10398
  for (const { path, content } of rootFiles) {
8839
10399
  const fullPath = join(targetDir, path);
8840
10400
  await mkdir(dirname(fullPath), { recursive: true });
@@ -8860,6 +10420,11 @@ async function initCommand(projectName, options = {}) {
8860
10420
  await mkdir(dirname(fullPath), { recursive: true });
8861
10421
  await writeFile(fullPath, content);
8862
10422
  }
10423
+ for (const { path, content } of uiPackageFiles) {
10424
+ const fullPath = join(targetDir, path);
10425
+ await mkdir(dirname(fullPath), { recursive: true });
10426
+ await writeFile(fullPath, content);
10427
+ }
8863
10428
  console.log("🔐 Initializing encrypted secrets...\n");
8864
10429
  const secretServices = [];
8865
10430
  if (services.db) secretServices.push("postgres");
@@ -8879,6 +10444,7 @@ async function initCommand(projectName, options = {}) {
8879
10444
  customSecrets[passwordKey] = app.password;
8880
10445
  }
8881
10446
  customSecrets.AUTH_PORT = "3002";
10447
+ customSecrets.AUTH_URL = "http://localhost:3002";
8882
10448
  customSecrets.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8883
10449
  customSecrets.BETTER_AUTH_URL = "http://localhost:3002";
8884
10450
  customSecrets.BETTER_AUTH_TRUSTED_ORIGINS = "http://localhost:3000,http://localhost:3001";
@@ -8955,7 +10521,8 @@ function printNextSteps(projectName, options, pkgManager) {
8955
10521
  console.log(` │ └── web/ # Next.js frontend`);
8956
10522
  }
8957
10523
  console.log(` ├── packages/`);
8958
- console.log(` │ └── models/ # Shared Zod schemas`);
10524
+ console.log(` │ ├── models/ # Shared Zod schemas`);
10525
+ if (isFullstackTemplate(options.template)) console.log(` │ └── ui/ # Shared UI components`);
8959
10526
  console.log(` ├── .gkm/secrets/ # Encrypted secrets`);
8960
10527
  console.log(` ├── gkm.config.ts # Workspace config`);
8961
10528
  console.log(` └── turbo.json # Turbo config`);
@@ -8971,7 +10538,7 @@ function printNextSteps(projectName, options, pkgManager) {
8971
10538
  console.log(` ${getRunCommand(pkgManager, "deploy")}`);
8972
10539
  console.log("");
8973
10540
  }
8974
- console.log("📚 Documentation: https://docs.geekmidas.dev");
10541
+ console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
8975
10542
  console.log("");
8976
10543
  }
8977
10544
 
@@ -9523,6 +11090,46 @@ program.command("whoami").description("Show current authentication status").acti
9523
11090
  process.exit(1);
9524
11091
  }
9525
11092
  });
11093
+ program.command("state:pull").description("Pull deployment state from remote to local").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
11094
+ try {
11095
+ const globalOptions = program.opts();
11096
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11097
+ await statePullCommand(options);
11098
+ } catch (error) {
11099
+ console.error(error instanceof Error ? error.message : "Command failed");
11100
+ process.exit(1);
11101
+ }
11102
+ });
11103
+ program.command("state:push").description("Push deployment state from local to remote").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
11104
+ try {
11105
+ const globalOptions = program.opts();
11106
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11107
+ await statePushCommand(options);
11108
+ } catch (error) {
11109
+ console.error(error instanceof Error ? error.message : "Command failed");
11110
+ process.exit(1);
11111
+ }
11112
+ });
11113
+ program.command("state:show").description("Show deployment state for a stage").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").option("--json", "Output as JSON").action(async (options) => {
11114
+ try {
11115
+ const globalOptions = program.opts();
11116
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11117
+ await stateShowCommand(options);
11118
+ } catch (error) {
11119
+ console.error(error instanceof Error ? error.message : "Command failed");
11120
+ process.exit(1);
11121
+ }
11122
+ });
11123
+ program.command("state:diff").description("Compare local and remote deployment state").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
11124
+ try {
11125
+ const globalOptions = program.opts();
11126
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11127
+ await stateDiffCommand(options);
11128
+ } catch (error) {
11129
+ console.error(error instanceof Error ? error.message : "Command failed");
11130
+ process.exit(1);
11131
+ }
11132
+ });
9526
11133
  program.parse();
9527
11134
 
9528
11135
  //#endregion