@geekmidas/cli 0.54.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +17 -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 +2223 -606
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2200 -583
  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 +12 -8
  95. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  96. package/src/deploy/CachedStateProvider.ts +86 -0
  97. package/src/deploy/LocalStateProvider.ts +57 -0
  98. package/src/deploy/SSMStateProvider.ts +93 -0
  99. package/src/deploy/StateProvider.ts +171 -0
  100. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  101. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  102. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  103. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  104. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  105. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  106. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  107. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  108. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  109. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  110. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  111. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  112. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  113. package/src/deploy/__tests__/state.spec.ts +13 -5
  114. package/src/deploy/dns/DnsProvider.ts +163 -0
  115. package/src/deploy/dns/HostingerProvider.ts +100 -0
  116. package/src/deploy/dns/Route53Provider.ts +256 -0
  117. package/src/deploy/dns/index.ts +257 -165
  118. package/src/deploy/env-resolver.ts +12 -5
  119. package/src/deploy/index.ts +16 -13
  120. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  121. package/src/deploy/sniffer-routes-worker.ts +104 -0
  122. package/src/deploy/sniffer.ts +77 -55
  123. package/src/deploy/state-commands.ts +274 -0
  124. package/src/dev/__tests__/entry.spec.ts +8 -2
  125. package/src/dev/__tests__/index.spec.ts +1 -3
  126. package/src/dev/index.ts +9 -3
  127. package/src/docker/__tests__/templates.spec.ts +3 -1
  128. package/src/index.ts +88 -0
  129. package/src/init/__tests__/generators.spec.ts +273 -0
  130. package/src/init/__tests__/init.spec.ts +3 -3
  131. package/src/init/generators/auth.ts +1 -0
  132. package/src/init/generators/config.ts +2 -0
  133. package/src/init/generators/models.ts +6 -1
  134. package/src/init/generators/monorepo.ts +3 -0
  135. package/src/init/generators/ui.ts +1472 -0
  136. package/src/init/generators/web.ts +134 -87
  137. package/src/init/index.ts +22 -3
  138. package/src/init/templates/api.ts +109 -3
  139. package/src/openapi.ts +99 -13
  140. package/src/workspace/__tests__/schema.spec.ts +107 -0
  141. package/src/workspace/schema.ts +314 -4
  142. package/src/workspace/types.ts +22 -36
  143. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  144. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  145. package/dist/encryption-CQXBZGkt.mjs +0 -3
  146. package/dist/index-A70abJ1m.d.mts.map +0 -1
  147. package/dist/index-pOA56MWT.d.cts.map +0 -1
  148. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  149. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  150. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  151. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  152. package/tsconfig.tsbuildinfo +0 -1
package/dist/index.cjs CHANGED
@@ -1,34 +1,34 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  const require_chunk = require('./chunk-CUT6urMc.cjs');
3
- const require_workspace = require('./workspace-CaVW6j2q.cjs');
4
- const require_config = require('./config-HYiM3iQJ.cjs');
5
- const require_openapi = require('./openapi-D7WwlpPF.cjs');
6
- const require_storage = require('./storage-BPRgh3DU.cjs');
7
- const require_dokploy_api = require('./dokploy-api-YD8WCQfW.cjs');
8
- const require_encryption = require('./encryption-DaCB_NmS.cjs');
9
- const require_openapi_react_query = require('./openapi-react-query-C_MxpBgF.cjs');
3
+ const require_workspace = require('./workspace-BFRUOOrh.cjs');
4
+ const require_config = require('./config-BGeJsW1r.cjs');
5
+ const require_credentials = require('./credentials-C8DWtnMY.cjs');
6
+ const require_openapi = require('./openapi-D1KXv2Ml.cjs');
7
+ const require_storage = require('./storage-CoCNe0Pt.cjs');
8
+ const require_dokploy_api = require('./dokploy-api-CQvhV6Hd.cjs');
9
+ const require_encryption = require('./encryption-BE0UOb8j.cjs');
10
+ const require_CachedStateProvider = require('./CachedStateProvider-D73dCqfH.cjs');
11
+ const require_openapi_react_query = require('./openapi-react-query-BeXvk-wa.cjs');
10
12
  const node_fs = require_chunk.__toESM(require("node:fs"));
11
13
  const node_path = require_chunk.__toESM(require("node:path"));
12
14
  const commander = require_chunk.__toESM(require("commander"));
13
15
  const node_process = require_chunk.__toESM(require("node:process"));
14
16
  const node_readline_promises = require_chunk.__toESM(require("node:readline/promises"));
15
17
  const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
16
- const node_os = require_chunk.__toESM(require("node:os"));
17
18
  const node_child_process = require_chunk.__toESM(require("node:child_process"));
18
19
  const node_net = require_chunk.__toESM(require("node:net"));
19
20
  const chokidar = require_chunk.__toESM(require("chokidar"));
20
21
  const dotenv = require_chunk.__toESM(require("dotenv"));
21
22
  const fast_glob = require_chunk.__toESM(require("fast-glob"));
22
23
  const __geekmidas_constructs_crons = require_chunk.__toESM(require("@geekmidas/constructs/crons"));
23
- const __geekmidas_constructs_endpoints = require_chunk.__toESM(require("@geekmidas/constructs/endpoints"));
24
24
  const __geekmidas_constructs_functions = require_chunk.__toESM(require("@geekmidas/constructs/functions"));
25
25
  const __geekmidas_constructs_subscribers = require_chunk.__toESM(require("@geekmidas/constructs/subscribers"));
26
26
  const node_crypto = require_chunk.__toESM(require("node:crypto"));
27
27
  const pg = require_chunk.__toESM(require("pg"));
28
28
  const node_dns_promises = require_chunk.__toESM(require("node:dns/promises"));
29
+ const node_module = require_chunk.__toESM(require("node:module"));
29
30
  const node_url = require_chunk.__toESM(require("node:url"));
30
31
  const prompts = require_chunk.__toESM(require("prompts"));
31
- const node_module = require_chunk.__toESM(require("node:module"));
32
32
 
33
33
  //#region package.json
34
34
  var name = "@geekmidas/cli";
@@ -76,6 +76,9 @@ var repository = {
76
76
  };
77
77
  var dependencies = {
78
78
  "@apidevtools/swagger-parser": "^10.1.0",
79
+ "@aws-sdk/client-route-53": "~3.971.0",
80
+ "@aws-sdk/client-ssm": "~3.971.0",
81
+ "@aws-sdk/credential-providers": "~3.971.0",
79
82
  "@geekmidas/constructs": "workspace:~",
80
83
  "@geekmidas/envkit": "workspace:~",
81
84
  "@geekmidas/errors": "workspace:~",
@@ -89,7 +92,8 @@ var dependencies = {
89
92
  "lodash.kebabcase": "^4.1.1",
90
93
  "openapi-typescript": "^7.4.2",
91
94
  "pg": "~8.17.1",
92
- "prompts": "~2.4.2"
95
+ "prompts": "~2.4.2",
96
+ "tsx": "~4.20.3"
93
97
  };
94
98
  var devDependencies = {
95
99
  "@geekmidas/testkit": "workspace:*",
@@ -119,138 +123,6 @@ var package_default = {
119
123
  peerDependenciesMeta
120
124
  };
121
125
 
122
- //#endregion
123
- //#region src/auth/credentials.ts
124
- /**
125
- * Get the path to the credentials directory
126
- */
127
- function getCredentialsDir(options) {
128
- const root = options?.root ?? (0, node_os.homedir)();
129
- return (0, node_path.join)(root, ".gkm");
130
- }
131
- /**
132
- * Get the path to the credentials file
133
- */
134
- function getCredentialsPath(options) {
135
- return (0, node_path.join)(getCredentialsDir(options), "credentials.json");
136
- }
137
- /**
138
- * Ensure the credentials directory exists
139
- */
140
- function ensureCredentialsDir(options) {
141
- const dir = getCredentialsDir(options);
142
- if (!(0, node_fs.existsSync)(dir)) (0, node_fs.mkdirSync)(dir, {
143
- recursive: true,
144
- mode: 448
145
- });
146
- }
147
- /**
148
- * Read stored credentials from disk
149
- */
150
- async function readCredentials(options) {
151
- const path = getCredentialsPath(options);
152
- if (!(0, node_fs.existsSync)(path)) return {};
153
- try {
154
- const content = await (0, node_fs_promises.readFile)(path, "utf-8");
155
- return JSON.parse(content);
156
- } catch {
157
- return {};
158
- }
159
- }
160
- /**
161
- * Write credentials to disk
162
- */
163
- async function writeCredentials(credentials, options) {
164
- ensureCredentialsDir(options);
165
- const path = getCredentialsPath(options);
166
- await (0, node_fs_promises.writeFile)(path, JSON.stringify(credentials, null, 2), { mode: 384 });
167
- }
168
- /**
169
- * Store Dokploy credentials
170
- */
171
- async function storeDokployCredentials(token, endpoint, options) {
172
- const credentials = await readCredentials(options);
173
- credentials.dokploy = {
174
- token,
175
- endpoint,
176
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
177
- };
178
- await writeCredentials(credentials, options);
179
- }
180
- /**
181
- * Get stored Dokploy credentials
182
- */
183
- async function getDokployCredentials(options) {
184
- const credentials = await readCredentials(options);
185
- if (!credentials.dokploy) return null;
186
- return {
187
- token: credentials.dokploy.token,
188
- endpoint: credentials.dokploy.endpoint,
189
- registryId: credentials.dokploy.registryId
190
- };
191
- }
192
- /**
193
- * Remove Dokploy credentials
194
- */
195
- async function removeDokployCredentials(options) {
196
- const credentials = await readCredentials(options);
197
- if (!credentials.dokploy) return false;
198
- delete credentials.dokploy;
199
- await writeCredentials(credentials, options);
200
- return true;
201
- }
202
- /**
203
- * Get Dokploy API token, checking stored credentials first, then environment
204
- */
205
- async function getDokployToken(options) {
206
- const envToken = process.env.DOKPLOY_API_TOKEN;
207
- if (envToken) return envToken;
208
- const stored = await getDokployCredentials(options);
209
- if (stored) return stored.token;
210
- return null;
211
- }
212
- /**
213
- * Store Dokploy registry ID
214
- */
215
- async function storeDokployRegistryId(registryId, options) {
216
- const credentials = await readCredentials(options);
217
- if (!credentials.dokploy) throw new Error("Dokploy credentials not found. Run \"gkm login --service dokploy\" first.");
218
- credentials.dokploy.registryId = registryId;
219
- await writeCredentials(credentials, options);
220
- }
221
- /**
222
- * Get Dokploy registry ID from stored credentials
223
- */
224
- async function getDokployRegistryId(options) {
225
- const stored = await getDokployCredentials(options);
226
- return stored?.registryId ?? void 0;
227
- }
228
- /**
229
- * Store Hostinger API token
230
- *
231
- * @param token - API token from hpanel.hostinger.com/profile/api
232
- */
233
- async function storeHostingerToken(token, options) {
234
- const credentials = await readCredentials(options);
235
- credentials.hostinger = {
236
- token,
237
- storedAt: (/* @__PURE__ */ new Date()).toISOString()
238
- };
239
- await writeCredentials(credentials, options);
240
- }
241
- /**
242
- * Get stored Hostinger API token
243
- *
244
- * Checks environment variable first (HOSTINGER_API_TOKEN),
245
- * then falls back to stored credentials.
246
- */
247
- async function getHostingerToken(options) {
248
- const envToken = process.env.HOSTINGER_API_TOKEN;
249
- if (envToken) return envToken;
250
- const credentials = await readCredentials(options);
251
- return credentials.hostinger?.token ?? null;
252
- }
253
-
254
126
  //#endregion
255
127
  //#region src/auth/index.ts
256
128
  const logger$11 = console;
@@ -258,7 +130,7 @@ const logger$11 = console;
258
130
  * Validate Dokploy token by making a test API call
259
131
  */
260
132
  async function validateDokployToken(endpoint, token) {
261
- const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-DBNE8MDt.cjs"));
133
+ const { DokployApi: DokployApi$1 } = await Promise.resolve().then(() => require("./dokploy-api-CWc02yyg.cjs"));
262
134
  const api = new DokployApi$1({
263
135
  baseUrl: endpoint,
264
136
  token
@@ -346,10 +218,10 @@ async function loginCommand(options) {
346
218
  logger$11.error("\n✗ Invalid credentials. Please check your token and try again.");
347
219
  process.exit(1);
348
220
  }
349
- await storeDokployCredentials(token, endpoint);
221
+ await require_credentials.storeDokployCredentials(token, endpoint);
350
222
  logger$11.log("\n✓ Successfully logged in to Dokploy!");
351
223
  logger$11.log(` Endpoint: ${endpoint}`);
352
- logger$11.log(` Credentials stored in: ${getCredentialsPath()}`);
224
+ logger$11.log(` Credentials stored in: ${require_credentials.getCredentialsPath()}`);
353
225
  logger$11.log("\nYou can now use deploy commands without setting DOKPLOY_API_TOKEN.");
354
226
  }
355
227
  }
@@ -359,13 +231,13 @@ async function loginCommand(options) {
359
231
  async function logoutCommand(options) {
360
232
  const { service = "dokploy" } = options;
361
233
  if (service === "all") {
362
- const dokployRemoved = await removeDokployCredentials();
234
+ const dokployRemoved = await require_credentials.removeDokployCredentials();
363
235
  if (dokployRemoved) logger$11.log("\n✓ Logged out from all services");
364
236
  else logger$11.log("\nNo stored credentials found");
365
237
  return;
366
238
  }
367
239
  if (service === "dokploy") {
368
- const removed = await removeDokployCredentials();
240
+ const removed = await require_credentials.removeDokployCredentials();
369
241
  if (removed) logger$11.log("\n✓ Logged out from Dokploy");
370
242
  else logger$11.log("\nNo Dokploy credentials found");
371
243
  }
@@ -375,13 +247,13 @@ async function logoutCommand(options) {
375
247
  */
376
248
  async function whoamiCommand() {
377
249
  logger$11.log("\n📋 Current credentials:\n");
378
- const dokploy = await getDokployCredentials();
250
+ const dokploy = await require_credentials.getDokployCredentials();
379
251
  if (dokploy) {
380
252
  logger$11.log(" Dokploy:");
381
253
  logger$11.log(` Endpoint: ${dokploy.endpoint}`);
382
254
  logger$11.log(` Token: ${maskToken(dokploy.token)}`);
383
255
  } else logger$11.log(" Dokploy: Not logged in");
384
- logger$11.log(`\n Credentials file: ${getCredentialsPath()}`);
256
+ logger$11.log(`\n Credentials file: ${require_credentials.getCredentialsPath()}`);
385
257
  }
386
258
  /**
387
259
  * Mask a token for display
@@ -2153,7 +2025,7 @@ async function buildForProvider(provider, context, rootOutputDir, endpointGenera
2153
2025
  let masterKey;
2154
2026
  if (context.production?.bundle && !skipBundle) {
2155
2027
  logger$7.log(`\n📦 Bundling production server...`);
2156
- const { bundleServer } = await Promise.resolve().then(() => require("./bundler-BB-kETMd.cjs"));
2028
+ const { bundleServer } = await Promise.resolve().then(() => require("./bundler-tHLLwYuU.cjs"));
2157
2029
  const allConstructs = [
2158
2030
  ...endpoints.map((e) => e.construct),
2159
2031
  ...functions.map((f) => f.construct),
@@ -2281,37 +2153,6 @@ function getAppOutputPath(workspace, _appName, app) {
2281
2153
  //#endregion
2282
2154
  //#region src/deploy/state.ts
2283
2155
  /**
2284
- * Get the state file path for a stage
2285
- */
2286
- function getStateFilePath(workspaceRoot, stage) {
2287
- return (0, node_path.join)(workspaceRoot, ".gkm", `deploy-${stage}.json`);
2288
- }
2289
- /**
2290
- * Read the deploy state for a stage
2291
- * Returns null if state file doesn't exist
2292
- */
2293
- async function readStageState(workspaceRoot, stage) {
2294
- const filePath = getStateFilePath(workspaceRoot, stage);
2295
- try {
2296
- const content = await (0, node_fs_promises.readFile)(filePath, "utf-8");
2297
- return JSON.parse(content);
2298
- } catch (error) {
2299
- if (error.code === "ENOENT") return null;
2300
- console.warn(`Warning: Could not read deploy state: ${error}`);
2301
- return null;
2302
- }
2303
- }
2304
- /**
2305
- * Write the deploy state for a stage
2306
- */
2307
- async function writeStageState(workspaceRoot, stage, state) {
2308
- const filePath = getStateFilePath(workspaceRoot, stage);
2309
- const dir = (0, node_path.join)(workspaceRoot, ".gkm");
2310
- await (0, node_fs_promises.mkdir)(dir, { recursive: true });
2311
- state.lastDeployedAt = (/* @__PURE__ */ new Date()).toISOString();
2312
- await (0, node_fs_promises.writeFile)(filePath, JSON.stringify(state, null, 2));
2313
- }
2314
- /**
2315
2156
  * Create a new empty state for a stage
2316
2157
  */
2317
2158
  function createEmptyState(stage, environmentId) {
@@ -2406,155 +2247,90 @@ function isDnsVerified(state, hostname, serverIp) {
2406
2247
  }
2407
2248
 
2408
2249
  //#endregion
2409
- //#region src/deploy/dns/hostinger-api.ts
2250
+ //#region src/deploy/dns/DnsProvider.ts
2251
+ /**
2252
+ * Check if value is a DnsProvider implementation.
2253
+ */
2254
+ function isDnsProvider(value) {
2255
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.getRecords === "function" && typeof value.upsertRecords === "function";
2256
+ }
2410
2257
  /**
2411
- * Hostinger DNS API client
2258
+ * Create a DNS provider based on configuration.
2412
2259
  *
2413
- * API Documentation: https://developers.hostinger.com/
2414
- * Authentication: Bearer token from hpanel.hostinger.com/profile/api
2260
+ * - 'hostinger': HostingerProvider
2261
+ * - 'route53': Route53Provider
2262
+ * - 'manual': Returns null (user handles DNS)
2263
+ * - Custom: Use provided DnsProvider implementation
2264
+ */
2265
+ async function createDnsProvider(options) {
2266
+ const { config } = options;
2267
+ if (config.provider === "manual") return null;
2268
+ if (isDnsProvider(config.provider)) return config.provider;
2269
+ const provider = config.provider;
2270
+ if (provider === "hostinger") {
2271
+ const { HostingerProvider } = await Promise.resolve().then(() => require("./HostingerProvider-DUV9-Tzg.cjs"));
2272
+ return new HostingerProvider();
2273
+ }
2274
+ if (provider === "route53") {
2275
+ const { Route53Provider } = await Promise.resolve().then(() => require("./Route53Provider-CpRIqu69.cjs"));
2276
+ const route53Config = config;
2277
+ return new Route53Provider({
2278
+ region: route53Config.region,
2279
+ profile: route53Config.profile,
2280
+ hostedZoneId: route53Config.hostedZoneId
2281
+ });
2282
+ }
2283
+ if (provider === "cloudflare") throw new Error("Cloudflare DNS provider not yet implemented");
2284
+ throw new Error(`Unknown DNS provider: ${JSON.stringify(config)}`);
2285
+ }
2286
+
2287
+ //#endregion
2288
+ //#region src/deploy/dns/index.ts
2289
+ const logger$6 = console;
2290
+ /**
2291
+ * Check if DNS config is legacy format (single domain with `domain` property)
2415
2292
  */
2416
- const HOSTINGER_API_BASE = "https://developers.hostinger.com";
2293
+ function isLegacyDnsConfig(config) {
2294
+ return typeof config === "object" && config !== null && "provider" in config && "domain" in config;
2295
+ }
2417
2296
  /**
2418
- * Hostinger API error
2297
+ * Normalize DNS config to new multi-domain format
2419
2298
  */
2420
- var HostingerApiError = class extends Error {
2421
- constructor(message, status, statusText, errors) {
2422
- super(message);
2423
- this.status = status;
2424
- this.statusText = statusText;
2425
- this.errors = errors;
2426
- this.name = "HostingerApiError";
2299
+ function normalizeDnsConfig(config) {
2300
+ if (isLegacyDnsConfig(config)) {
2301
+ const { domain,...providerConfig } = config;
2302
+ return { [domain]: providerConfig };
2427
2303
  }
2428
- };
2304
+ return config;
2305
+ }
2429
2306
  /**
2430
- * Hostinger DNS API client
2307
+ * Find the root domain for a hostname from available DNS configs
2431
2308
  *
2432
2309
  * @example
2433
- * ```ts
2434
- * const api = new HostingerApi(token);
2435
- *
2436
- * // Get all records for a domain
2437
- * const records = await api.getRecords('traflabs.io');
2438
- *
2439
- * // Create/update records
2440
- * await api.upsertRecords('traflabs.io', [
2441
- * { name: 'api.joemoer', type: 'A', ttl: 300, records: ['1.2.3.4'] }
2442
- * ]);
2443
- * ```
2310
+ * findRootDomain('api.geekmidas.com', { 'geekmidas.com': {...}, 'geekmidas.dev': {...} })
2311
+ * // Returns 'geekmidas.com'
2444
2312
  */
2445
- var HostingerApi = class {
2446
- token;
2447
- constructor(token) {
2448
- this.token = token;
2449
- }
2450
- /**
2451
- * Make a request to the Hostinger API
2452
- */
2453
- async request(method, endpoint, body) {
2454
- const url = `${HOSTINGER_API_BASE}${endpoint}`;
2455
- const response = await fetch(url, {
2456
- method,
2457
- headers: {
2458
- "Content-Type": "application/json",
2459
- Authorization: `Bearer ${this.token}`
2460
- },
2461
- body: body ? JSON.stringify(body) : void 0
2462
- });
2463
- if (!response.ok) {
2464
- let errorMessage = `Hostinger API error: ${response.status} ${response.statusText}`;
2465
- let errors;
2466
- try {
2467
- const errorBody = await response.json();
2468
- if (errorBody.message) errorMessage = `Hostinger API error: ${errorBody.message}`;
2469
- errors = errorBody.errors;
2470
- } catch {}
2471
- throw new HostingerApiError(errorMessage, response.status, response.statusText, errors);
2313
+ function findRootDomain(hostname, dnsConfig) {
2314
+ const domains = Object.keys(dnsConfig).sort((a, b) => b.length - a.length);
2315
+ for (const domain of domains) if (hostname === domain || hostname.endsWith(`.${domain}`)) return domain;
2316
+ return null;
2317
+ }
2318
+ /**
2319
+ * Group hostnames by their root domain
2320
+ */
2321
+ function groupHostnamesByDomain(appHostnames, dnsConfig) {
2322
+ const grouped = /* @__PURE__ */ new Map();
2323
+ for (const [appName, hostname] of appHostnames) {
2324
+ const rootDomain = findRootDomain(hostname, dnsConfig);
2325
+ if (!rootDomain) {
2326
+ logger$6.log(` ⚠ No DNS config found for hostname: ${hostname}`);
2327
+ continue;
2472
2328
  }
2473
- const text = await response.text();
2474
- if (!text || text.trim() === "") return void 0;
2475
- return JSON.parse(text);
2476
- }
2477
- /**
2478
- * Get all DNS records for a domain
2479
- *
2480
- * @param domain - Root domain (e.g., 'traflabs.io')
2481
- */
2482
- async getRecords(domain) {
2483
- const response = await this.request("GET", `/api/dns/v1/zones/${domain}`);
2484
- return response.data || [];
2485
- }
2486
- /**
2487
- * Create or update DNS records
2488
- *
2489
- * @param domain - Root domain (e.g., 'traflabs.io')
2490
- * @param records - Records to create/update
2491
- * @param overwrite - If true, replaces all existing records. If false, merges with existing.
2492
- */
2493
- async upsertRecords(domain, records, overwrite = false) {
2494
- await this.request("PUT", `/api/dns/v1/zones/${domain}`, {
2495
- overwrite,
2496
- zone: records
2497
- });
2498
- }
2499
- /**
2500
- * Validate DNS records before applying
2501
- *
2502
- * @param domain - Root domain (e.g., 'traflabs.io')
2503
- * @param records - Records to validate
2504
- * @returns true if valid, throws if invalid
2505
- */
2506
- async validateRecords(domain, records) {
2507
- await this.request("POST", `/api/dns/v1/zones/${domain}/validate`, {
2508
- overwrite: false,
2509
- zone: records
2510
- });
2511
- return true;
2329
+ if (!grouped.has(rootDomain)) grouped.set(rootDomain, /* @__PURE__ */ new Map());
2330
+ grouped.get(rootDomain).set(appName, hostname);
2512
2331
  }
2513
- /**
2514
- * Delete specific DNS records
2515
- *
2516
- * @param domain - Root domain (e.g., 'traflabs.io')
2517
- * @param filters - Filters to match records for deletion
2518
- */
2519
- async deleteRecords(domain, filters) {
2520
- await this.request("DELETE", `/api/dns/v1/zones/${domain}`, { filters });
2521
- }
2522
- /**
2523
- * Check if a specific record exists
2524
- *
2525
- * @param domain - Root domain (e.g., 'traflabs.io')
2526
- * @param name - Subdomain name (e.g., 'api.joemoer')
2527
- * @param type - Record type (e.g., 'A')
2528
- */
2529
- async recordExists(domain, name$1, type$1 = "A") {
2530
- const records = await this.getRecords(domain);
2531
- return records.some((r) => r.name === name$1 && r.type === type$1);
2532
- }
2533
- /**
2534
- * Create a single A record if it doesn't exist
2535
- *
2536
- * @param domain - Root domain (e.g., 'traflabs.io')
2537
- * @param subdomain - Subdomain name (e.g., 'api.joemoer')
2538
- * @param ip - IP address to point to
2539
- * @param ttl - TTL in seconds (default: 300)
2540
- * @returns true if created, false if already exists
2541
- */
2542
- async createARecordIfNotExists(domain, subdomain, ip, ttl = 300) {
2543
- const exists = await this.recordExists(domain, subdomain, "A");
2544
- if (exists) return false;
2545
- await this.upsertRecords(domain, [{
2546
- name: subdomain,
2547
- type: "A",
2548
- ttl,
2549
- records: [{ content: ip }]
2550
- }]);
2551
- return true;
2552
- }
2553
- };
2554
-
2555
- //#endregion
2556
- //#region src/deploy/dns/index.ts
2557
- const logger$6 = console;
2332
+ return grouped;
2333
+ }
2558
2334
  /**
2559
2335
  * Resolve IP address from a hostname
2560
2336
  */
@@ -2626,127 +2402,91 @@ function printDnsRecordsSimple(records, rootDomain) {
2626
2402
  logger$6.log("");
2627
2403
  }
2628
2404
  /**
2629
- * Prompt for input (reuse from deploy/index.ts pattern)
2630
- */
2631
- async function promptForToken(message) {
2632
- const { stdin, stdout } = await import("node:process");
2633
- if (!stdin.isTTY) throw new Error("Interactive input required for Hostinger token.");
2634
- stdout.write(message);
2635
- return new Promise((resolve$3) => {
2636
- let value = "";
2637
- const onData = (char) => {
2638
- const c = char.toString();
2639
- if (c === "\n" || c === "\r") {
2640
- stdin.setRawMode(false);
2641
- stdin.pause();
2642
- stdin.removeListener("data", onData);
2643
- stdout.write("\n");
2644
- resolve$3(value);
2645
- } else if (c === "") {
2646
- stdin.setRawMode(false);
2647
- stdin.pause();
2648
- stdout.write("\n");
2649
- process.exit(1);
2650
- } else if (c === "" || c === "\b") {
2651
- if (value.length > 0) value = value.slice(0, -1);
2652
- } else value += c;
2653
- };
2654
- stdin.setRawMode(true);
2655
- stdin.resume();
2656
- stdin.on("data", onData);
2657
- });
2658
- }
2659
- /**
2660
- * Create DNS records using the configured provider
2661
- */
2662
- async function createDnsRecords(records, dnsConfig) {
2663
- const { provider, domain: rootDomain, ttl = 300 } = dnsConfig;
2664
- if (provider === "manual") return records.map((r) => ({
2665
- ...r,
2666
- created: false,
2667
- existed: false
2668
- }));
2669
- if (provider === "hostinger") return createHostingerRecords(records, rootDomain, ttl);
2670
- if (provider === "cloudflare") {
2671
- logger$6.log(" ⚠ Cloudflare DNS integration not yet implemented");
2672
- return records.map((r) => ({
2673
- ...r,
2674
- error: "Cloudflare not implemented"
2675
- }));
2676
- }
2677
- return records;
2678
- }
2679
- /**
2680
- * Create DNS records at Hostinger
2405
+ * Create DNS records for a single domain using its configured provider
2681
2406
  */
2682
- async function createHostingerRecords(records, rootDomain, ttl) {
2683
- let token = await getHostingerToken();
2684
- if (!token) {
2685
- logger$6.log("\n 📋 Hostinger API token not found.");
2686
- logger$6.log(" Get your token from: https://hpanel.hostinger.com/profile/api\n");
2687
- try {
2688
- token = await promptForToken(" Hostinger API Token: ");
2689
- await storeHostingerToken(token);
2690
- logger$6.log(" ✓ Token saved");
2691
- } catch {
2692
- logger$6.log(" ⚠ Could not get token, skipping DNS creation");
2693
- return records.map((r) => ({
2694
- ...r,
2695
- error: "No API token"
2696
- }));
2697
- }
2698
- }
2699
- const api = new HostingerApi(token);
2700
- const results = [];
2701
- let existingRecords = [];
2407
+ async function createDnsRecordsForDomain(records, rootDomain, providerConfig) {
2408
+ const ttl = "ttl" in providerConfig && providerConfig.ttl ? providerConfig.ttl : 300;
2409
+ let provider;
2702
2410
  try {
2703
- existingRecords = await api.getRecords(rootDomain);
2411
+ provider = await createDnsProvider({ config: providerConfig });
2704
2412
  } catch (error) {
2705
2413
  const message = error instanceof Error ? error.message : "Unknown error";
2706
- logger$6.log(` ⚠ Failed to fetch existing DNS records: ${message}`);
2414
+ logger$6.log(` ⚠ Failed to create DNS provider for ${rootDomain}: ${message}`);
2707
2415
  return records.map((r) => ({
2708
2416
  ...r,
2709
2417
  error: message
2710
2418
  }));
2711
2419
  }
2712
- for (const record of records) {
2713
- const existing = existingRecords.find((r) => r.name === record.subdomain && r.type === "A");
2714
- if (existing) {
2715
- results.push({
2716
- ...record,
2420
+ if (!provider) return records.map((r) => ({
2421
+ ...r,
2422
+ created: false,
2423
+ existed: false
2424
+ }));
2425
+ const results = [];
2426
+ const upsertRecords = records.map((r) => ({
2427
+ name: r.subdomain,
2428
+ type: r.type,
2429
+ ttl,
2430
+ value: r.value
2431
+ }));
2432
+ try {
2433
+ const upsertResults = await provider.upsertRecords(rootDomain, upsertRecords);
2434
+ for (const [i, record] of records.entries()) {
2435
+ const result = upsertResults[i];
2436
+ if (!result) {
2437
+ results.push({
2438
+ hostname: record.hostname,
2439
+ subdomain: record.subdomain,
2440
+ type: record.type,
2441
+ value: record.value,
2442
+ appName: record.appName,
2443
+ error: "No result returned from provider"
2444
+ });
2445
+ continue;
2446
+ }
2447
+ if (result.unchanged) results.push({
2448
+ hostname: record.hostname,
2449
+ subdomain: record.subdomain,
2450
+ type: record.type,
2451
+ value: record.value,
2452
+ appName: record.appName,
2717
2453
  existed: true,
2718
2454
  created: false
2719
2455
  });
2720
- continue;
2721
- }
2722
- try {
2723
- await api.upsertRecords(rootDomain, [{
2724
- name: record.subdomain,
2725
- type: "A",
2726
- ttl,
2727
- records: [{ content: record.value }]
2728
- }]);
2729
- results.push({
2730
- ...record,
2731
- created: true,
2732
- existed: false
2733
- });
2734
- } catch (error) {
2735
- const message = error instanceof Error ? error.message : "Unknown error";
2736
- results.push({
2737
- ...record,
2738
- error: message
2456
+ else results.push({
2457
+ hostname: record.hostname,
2458
+ subdomain: record.subdomain,
2459
+ type: record.type,
2460
+ value: record.value,
2461
+ appName: record.appName,
2462
+ created: result.created,
2463
+ existed: !result.created
2739
2464
  });
2740
2465
  }
2466
+ } catch (error) {
2467
+ const message = error instanceof Error ? error.message : "Unknown error";
2468
+ logger$6.log(` ⚠ Failed to create DNS records for ${rootDomain}: ${message}`);
2469
+ return records.map((r) => ({
2470
+ hostname: r.hostname,
2471
+ subdomain: r.subdomain,
2472
+ type: r.type,
2473
+ value: r.value,
2474
+ appName: r.appName,
2475
+ error: message
2476
+ }));
2741
2477
  }
2742
2478
  return results;
2743
2479
  }
2744
2480
  /**
2745
2481
  * Main DNS orchestration function for deployments
2482
+ *
2483
+ * Supports both legacy single-domain format and new multi-domain format:
2484
+ * - Legacy: { provider: 'hostinger', domain: 'example.com' }
2485
+ * - Multi: { 'example.com': { provider: 'hostinger' }, 'example.dev': { provider: 'route53' } }
2746
2486
  */
2747
2487
  async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint) {
2748
2488
  if (!dnsConfig) return null;
2749
- const { domain: rootDomain, autoCreate = true } = dnsConfig;
2489
+ const normalizedConfig = normalizeDnsConfig(dnsConfig);
2750
2490
  logger$6.log("\n🌐 Setting up DNS records...");
2751
2491
  let serverIp;
2752
2492
  try {
@@ -2758,31 +2498,43 @@ async function orchestrateDns(appHostnames, dnsConfig, dokployEndpoint) {
2758
2498
  logger$6.log(` ⚠ Failed to resolve server IP: ${message}`);
2759
2499
  return null;
2760
2500
  }
2761
- const requiredRecords = generateRequiredRecords(appHostnames, rootDomain, serverIp);
2762
- if (requiredRecords.length === 0) {
2763
- logger$6.log(" No DNS records needed");
2501
+ const groupedHostnames = groupHostnamesByDomain(appHostnames, normalizedConfig);
2502
+ if (groupedHostnames.size === 0) {
2503
+ logger$6.log(" No DNS records needed (no hostnames match configured domains)");
2764
2504
  return {
2765
2505
  records: [],
2766
2506
  success: true,
2767
2507
  serverIp
2768
2508
  };
2769
2509
  }
2770
- let finalRecords;
2771
- if (autoCreate && dnsConfig.provider !== "manual") {
2772
- logger$6.log(` Creating DNS records at ${dnsConfig.provider}...`);
2773
- finalRecords = await createDnsRecords(requiredRecords, dnsConfig);
2774
- const created = finalRecords.filter((r) => r.created).length;
2775
- const existed = finalRecords.filter((r) => r.existed).length;
2776
- const failed = finalRecords.filter((r) => r.error).length;
2777
- if (created > 0) logger$6.log(` ✓ Created ${created} DNS record(s)`);
2778
- if (existed > 0) logger$6.log(` ✓ ${existed} record(s) already exist`);
2779
- if (failed > 0) logger$6.log(` ⚠ ${failed} record(s) failed`);
2780
- } else finalRecords = requiredRecords;
2781
- printDnsRecordsTable(finalRecords, rootDomain);
2782
- const hasFailures = finalRecords.some((r) => r.error);
2783
- if (dnsConfig.provider === "manual" || hasFailures) printDnsRecordsSimple(finalRecords.filter((r) => !r.created && !r.existed), rootDomain);
2510
+ const allRecords = [];
2511
+ let hasFailures = false;
2512
+ for (const [rootDomain, domainHostnames] of groupedHostnames) {
2513
+ const providerConfig = normalizedConfig[rootDomain];
2514
+ if (!providerConfig) {
2515
+ logger$6.log(` ⚠ No provider config for ${rootDomain}`);
2516
+ continue;
2517
+ }
2518
+ const providerName = typeof providerConfig.provider === "string" ? providerConfig.provider : "custom";
2519
+ const requiredRecords = generateRequiredRecords(domainHostnames, rootDomain, serverIp);
2520
+ if (requiredRecords.length === 0) continue;
2521
+ logger$6.log(` Creating DNS records for ${rootDomain} (${providerName})...`);
2522
+ const domainRecords = await createDnsRecordsForDomain(requiredRecords, rootDomain, providerConfig);
2523
+ allRecords.push(...domainRecords);
2524
+ const created = domainRecords.filter((r) => r.created).length;
2525
+ const existed = domainRecords.filter((r) => r.existed).length;
2526
+ const failed = domainRecords.filter((r) => r.error).length;
2527
+ if (created > 0) logger$6.log(` ✓ Created ${created} DNS record(s) for ${rootDomain}`);
2528
+ if (existed > 0) logger$6.log(` ✓ ${existed} record(s) already exist for ${rootDomain}`);
2529
+ if (failed > 0) {
2530
+ logger$6.log(` ⚠ ${failed} record(s) failed for ${rootDomain}`);
2531
+ hasFailures = true;
2532
+ }
2533
+ printDnsRecordsTable(domainRecords, rootDomain);
2534
+ if (providerConfig.provider === "manual" || failed > 0) printDnsRecordsSimple(domainRecords.filter((r) => !r.created && !r.existed), rootDomain);
2535
+ }
2784
2536
  return {
2785
- records: finalRecords,
2537
+ records: allRecords,
2786
2538
  success: !hasFailures,
2787
2539
  serverIp
2788
2540
  };
@@ -4329,7 +4081,7 @@ const logger$3 = console;
4329
4081
  * Get the Dokploy API token from stored credentials or environment
4330
4082
  */
4331
4083
  async function getApiToken$1() {
4332
- const token = await getDokployToken();
4084
+ const token = await require_credentials.getDokployToken();
4333
4085
  if (!token) throw new Error("Dokploy credentials not found.\nRun \"gkm login --service dokploy\" to authenticate, or set DOKPLOY_API_TOKEN.");
4334
4086
  return token;
4335
4087
  }
@@ -4358,7 +4110,7 @@ async function deployDokploy(options) {
4358
4110
  registryOptions.registryId = config.registryId;
4359
4111
  logger$3.log(` Using Dokploy registry: ${config.registryId}`);
4360
4112
  } else {
4361
- const storedRegistryId = await getDokployRegistryId();
4113
+ const storedRegistryId = await require_credentials.getDokployRegistryId();
4362
4114
  if (storedRegistryId) {
4363
4115
  registryOptions.registryId = storedRegistryId;
4364
4116
  logger$3.log(` Using stored Dokploy registry: ${storedRegistryId}`);
@@ -4591,7 +4343,7 @@ const logger$2 = console;
4591
4343
  * Get the Dokploy API token from stored credentials or environment
4592
4344
  */
4593
4345
  async function getApiToken() {
4594
- const token = await getDokployToken();
4346
+ const token = await require_credentials.getDokployToken();
4595
4347
  if (!token) throw new Error("Dokploy credentials not found.\nRun \"gkm login --service dokploy\" to authenticate, or set DOKPLOY_API_TOKEN.");
4596
4348
  return token;
4597
4349
  }
@@ -4600,7 +4352,7 @@ async function getApiToken() {
4600
4352
  */
4601
4353
  async function getEndpoint(providedEndpoint) {
4602
4354
  if (providedEndpoint) return providedEndpoint;
4603
- const stored = await getDokployCredentials();
4355
+ const stored = await require_credentials.getDokployCredentials();
4604
4356
  if (stored) return stored.endpoint;
4605
4357
  throw new Error("Dokploy endpoint not specified.\nEither run \"gkm login --service dokploy\" first, or provide --endpoint.");
4606
4358
  }
@@ -4745,7 +4497,7 @@ async function deployListCommand(options) {
4745
4497
  logger$2.log(" Run \"gkm registry:setup\" to configure a registry");
4746
4498
  return;
4747
4499
  }
4748
- const storedRegistryId = await getDokployRegistryId();
4500
+ const storedRegistryId = await require_credentials.getDokployRegistryId();
4749
4501
  for (const registry of registries) {
4750
4502
  const isDefault = registry.registryId === storedRegistryId;
4751
4503
  const marker = isDefault ? " (default)" : "";
@@ -4757,6 +4509,48 @@ async function deployListCommand(options) {
4757
4509
  }
4758
4510
  }
4759
4511
 
4512
+ //#endregion
4513
+ //#region src/deploy/StateProvider.ts
4514
+ /**
4515
+ * Check if value is a StateProvider implementation.
4516
+ */
4517
+ function isStateProvider(value) {
4518
+ return typeof value === "object" && value !== null && typeof value.read === "function" && typeof value.write === "function";
4519
+ }
4520
+ /**
4521
+ * Create a state provider based on configuration.
4522
+ *
4523
+ * - 'local': LocalStateProvider (default)
4524
+ * - 'ssm': CachedStateProvider with SSM as source of truth
4525
+ * - Custom: Use provided StateProvider implementation
4526
+ */
4527
+ async function createStateProvider(options) {
4528
+ const { config, workspaceRoot, workspaceName } = options;
4529
+ if (!config) {
4530
+ const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-CdspeSVL.cjs"));
4531
+ return new LocalStateProvider(workspaceRoot);
4532
+ }
4533
+ if (isStateProvider(config.provider)) return config.provider;
4534
+ const provider = config.provider;
4535
+ if (provider === "local") {
4536
+ const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-CdspeSVL.cjs"));
4537
+ return new LocalStateProvider(workspaceRoot);
4538
+ }
4539
+ if (provider === "ssm") {
4540
+ if (!workspaceName) throw new Error("Workspace name is required for SSM state provider. Set \"name\" in gkm.config.ts.");
4541
+ const { LocalStateProvider } = await Promise.resolve().then(() => require("./LocalStateProvider-CdspeSVL.cjs"));
4542
+ const { SSMStateProvider } = await Promise.resolve().then(() => require("./SSMStateProvider-BxAPU99a.cjs"));
4543
+ const { CachedStateProvider: CachedStateProvider$1 } = await Promise.resolve().then(() => require("./CachedStateProvider-D_uISMmJ.cjs"));
4544
+ const local = new LocalStateProvider(workspaceRoot);
4545
+ const ssm = new SSMStateProvider({
4546
+ workspaceName,
4547
+ region: config.region
4548
+ });
4549
+ return new CachedStateProvider$1(ssm, local);
4550
+ }
4551
+ throw new Error(`Unknown state provider: ${JSON.stringify(config)}`);
4552
+ }
4553
+
4760
4554
  //#endregion
4761
4555
  //#region src/deploy/secrets.ts
4762
4556
  /**
@@ -4859,10 +4653,12 @@ function generateSecretsReport(encryptedApps, sniffedApps) {
4859
4653
  const __filename$1 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
4860
4654
  const __dirname$1 = (0, node_path.dirname)(__filename$1);
4861
4655
  /**
4862
- * Check if a value is a gkm construct (Endpoint, Function, Cron, or Subscriber).
4656
+ * Resolve the tsx package path from the CLI package's dependencies.
4657
+ * This ensures tsx is available regardless of whether the target project has it installed.
4863
4658
  */
4864
- function isConstruct(value) {
4865
- return __geekmidas_constructs_endpoints.Endpoint.isEndpoint(value) || __geekmidas_constructs_functions.Function.isFunction(value) || __geekmidas_constructs_crons.Cron.isCron(value) || __geekmidas_constructs_subscribers.Subscriber.isSubscriber(value);
4659
+ function resolveTsxPath() {
4660
+ const require$2 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
4661
+ return require$2.resolve("tsx");
4866
4662
  }
4867
4663
  /**
4868
4664
  * Resolve the path to a sniffer helper file.
@@ -5023,7 +4819,9 @@ async function sniffEntryFile(entryPath, appPath, workspacePath) {
5023
4819
  *
5024
4820
  * Route-based apps have endpoints, functions, crons, and subscribers that
5025
4821
  * use services. Each service's register() method accesses environment variables.
5026
- * This function mimics what the bundler does during build to capture those vars.
4822
+ *
4823
+ * This runs in a subprocess with tsx loader to properly handle TypeScript
4824
+ * compilation and path alias resolution (e.g., `src/...` imports).
5027
4825
  *
5028
4826
  * @param routes - Glob pattern(s) for route files
5029
4827
  * @param appPath - The app's path relative to workspace (e.g., 'apps/api')
@@ -5032,32 +4830,63 @@ async function sniffEntryFile(entryPath, appPath, workspacePath) {
5032
4830
  */
5033
4831
  async function sniffRouteFiles(routes, appPath, workspacePath) {
5034
4832
  const fullAppPath = (0, node_path.resolve)(workspacePath, appPath);
5035
- const patterns = Array.isArray(routes) ? routes : [routes];
5036
- const envVars = /* @__PURE__ */ new Set();
5037
- let error;
5038
- try {
5039
- const files = await (0, fast_glob.default)(patterns, {
4833
+ const workerPath = resolveSnifferFile("sniffer-routes-worker");
4834
+ const tsxPath = resolveTsxPath();
4835
+ const routesArray = Array.isArray(routes) ? routes : [routes];
4836
+ const pattern = routesArray[0];
4837
+ if (!pattern) return {
4838
+ envVars: [],
4839
+ error: new Error("No route patterns provided")
4840
+ };
4841
+ return new Promise((resolvePromise) => {
4842
+ const child = (0, node_child_process.spawn)("node", [
4843
+ "--import",
4844
+ tsxPath,
4845
+ workerPath,
4846
+ fullAppPath,
4847
+ pattern
4848
+ ], {
5040
4849
  cwd: fullAppPath,
5041
- absolute: true
4850
+ stdio: [
4851
+ "ignore",
4852
+ "pipe",
4853
+ "pipe"
4854
+ ],
4855
+ env: { ...process.env }
5042
4856
  });
5043
- for (const file of files) try {
5044
- const module$1 = await import(file);
5045
- for (const [, exportValue] of Object.entries(module$1)) if (isConstruct(exportValue)) try {
5046
- const constructEnvVars = await exportValue.getEnvironment();
5047
- constructEnvVars.forEach((v) => envVars.add(v));
5048
- } catch (e) {
5049
- console.warn(`[sniffer] Failed to get environment for construct in ${file}: ${e instanceof Error ? e.message : String(e)}`);
5050
- }
5051
- } catch (e) {
5052
- console.warn(`[sniffer] Failed to import ${file}: ${e instanceof Error ? e.message : String(e)}`);
5053
- }
5054
- } catch (e) {
5055
- error = e instanceof Error ? e : new Error(String(e));
5056
- }
5057
- return {
5058
- envVars: Array.from(envVars).sort(),
5059
- error
5060
- };
4857
+ let stdout = "";
4858
+ let stderr = "";
4859
+ child.stdout.on("data", (data) => {
4860
+ stdout += data.toString();
4861
+ });
4862
+ child.stderr.on("data", (data) => {
4863
+ stderr += data.toString();
4864
+ });
4865
+ child.on("close", (code) => {
4866
+ if (stderr) stderr.split("\n").filter((line) => line.trim()).forEach((line) => console.warn(line));
4867
+ try {
4868
+ const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
4869
+ if (jsonMatch) {
4870
+ const result = JSON.parse(jsonMatch[0]);
4871
+ resolvePromise({
4872
+ envVars: result.envVars || [],
4873
+ error: result.error ? new Error(result.error) : void 0
4874
+ });
4875
+ return;
4876
+ }
4877
+ } catch {}
4878
+ resolvePromise({
4879
+ envVars: [],
4880
+ error: new Error(`Failed to sniff route files (exit code ${code}): ${stderr || stdout || "No output"}`)
4881
+ });
4882
+ });
4883
+ child.on("error", (err) => {
4884
+ resolvePromise({
4885
+ envVars: [],
4886
+ error: err
4887
+ });
4888
+ });
4889
+ });
5061
4890
  }
5062
4891
  /**
5063
4892
  * Run the SnifferEnvironmentParser on an envParser module to detect
@@ -5388,7 +5217,7 @@ async function provisionServices(api, projectId, environmentId, projectName, ser
5388
5217
  */
5389
5218
  async function ensureDokploySetup(config, dockerConfig, stage, services) {
5390
5219
  logger$1.log("\n🔧 Checking Dokploy setup...");
5391
- let creds = await getDokployCredentials();
5220
+ let creds = await require_credentials.getDokployCredentials();
5392
5221
  if (!creds) {
5393
5222
  logger$1.log("\n📋 Dokploy credentials not found. Let's set them up.");
5394
5223
  const endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ");
@@ -5403,7 +5232,7 @@ async function ensureDokploySetup(config, dockerConfig, stage, services) {
5403
5232
  logger$1.log("\nValidating credentials...");
5404
5233
  const isValid = await validateDokployToken(normalizedEndpoint, token);
5405
5234
  if (!isValid) throw new Error("Invalid credentials. Please check your token.");
5406
- await storeDokployCredentials(token, normalizedEndpoint);
5235
+ await require_credentials.storeDokployCredentials(token, normalizedEndpoint);
5407
5236
  creds = {
5408
5237
  token,
5409
5238
  endpoint: normalizedEndpoint
@@ -5420,7 +5249,7 @@ async function ensureDokploySetup(config, dockerConfig, stage, services) {
5420
5249
  try {
5421
5250
  const projectDetails = await api.getProject(existingConfig.projectId);
5422
5251
  logger$1.log("✓ Project verified");
5423
- const storedRegistryId = existingConfig.registryId ?? await getDokployRegistryId();
5252
+ const storedRegistryId = existingConfig.registryId ?? await require_credentials.getDokployRegistryId();
5424
5253
  const environments = projectDetails.environments ?? [];
5425
5254
  let environment = environments.find((e) => e.name.toLowerCase() === stage.toLowerCase());
5426
5255
  if (!environment) {
@@ -5489,14 +5318,14 @@ async function ensureDokploySetup(config, dockerConfig, stage, services) {
5489
5318
  logger$1.log(` ✓ Created application: ${applicationId}`);
5490
5319
  }
5491
5320
  logger$1.log("\n🐳 Checking registry...");
5492
- let registryId = await getDokployRegistryId();
5321
+ let registryId = await require_credentials.getDokployRegistryId();
5493
5322
  if (registryId) try {
5494
5323
  const registry = await api.getRegistry(registryId);
5495
5324
  logger$1.log(` Using registry: ${registry.registryName}`);
5496
5325
  } catch {
5497
5326
  logger$1.log(" ⚠ Stored registry not found, clearing...");
5498
5327
  registryId = void 0;
5499
- await storeDokployRegistryId("");
5328
+ await require_credentials.storeDokployRegistryId("");
5500
5329
  }
5501
5330
  if (!registryId) {
5502
5331
  const registries = await api.listRegistries();
@@ -5507,7 +5336,7 @@ async function ensureDokploySetup(config, dockerConfig, stage, services) {
5507
5336
  const password = await prompt("Registry password/token: ", true);
5508
5337
  const registry = await api.createRegistry("Default Registry", dockerConfig.registry, username, password);
5509
5338
  registryId = registry.registryId;
5510
- await storeDokployRegistryId(registryId);
5339
+ await require_credentials.storeDokployRegistryId(registryId);
5511
5340
  logger$1.log(` ✓ Registry created: ${registryId}`);
5512
5341
  } else logger$1.log(" ⚠ No registry configured. Set docker.registry in gkm.config.ts");
5513
5342
  else {
@@ -5521,7 +5350,7 @@ async function ensureDokploySetup(config, dockerConfig, stage, services) {
5521
5350
  const index = parseInt(selection, 10) - 1;
5522
5351
  if (index >= 0 && index < registries.length) {
5523
5352
  registryId = registries[index].registryId;
5524
- await storeDokployRegistryId(registryId);
5353
+ await require_credentials.storeDokployRegistryId(registryId);
5525
5354
  logger$1.log(` ✓ Selected: ${registries[index].registryName}`);
5526
5355
  } else if (dockerConfig.registry && index === registries.length) {
5527
5356
  logger$1.log(`\n Creating new registry...`);
@@ -5530,7 +5359,7 @@ async function ensureDokploySetup(config, dockerConfig, stage, services) {
5530
5359
  const password = await prompt(" Registry password/token: ", true);
5531
5360
  const registry = await api.createRegistry(dockerConfig.registry.replace(/^https?:\/\//, ""), dockerConfig.registry, username, password);
5532
5361
  registryId = registry.registryId;
5533
- await storeDokployRegistryId(registryId);
5362
+ await require_credentials.storeDokployRegistryId(registryId);
5534
5363
  logger$1.log(` ✓ Registry created: ${registryId}`);
5535
5364
  } else logger$1.log(" ⚠ Invalid selection, skipping registry setup");
5536
5365
  }
@@ -5612,7 +5441,7 @@ async function workspaceDeployCommand(workspace, options) {
5612
5441
  if (report.appsWithSecrets.length > 0) logger$1.log(` ✓ Encrypted secrets for: ${report.appsWithSecrets.join(", ")}`);
5613
5442
  if (report.appsWithMissingSecrets.length > 0) for (const { appName, missing } of report.appsWithMissingSecrets) logger$1.log(` ⚠️ ${appName}: Missing secrets: ${missing.join(", ")}`);
5614
5443
  }
5615
- let creds = await getDokployCredentials();
5444
+ let creds = await require_credentials.getDokployCredentials();
5616
5445
  if (!creds) {
5617
5446
  logger$1.log("\n📋 Dokploy credentials not found. Let's set them up.");
5618
5447
  const endpoint = await prompt("Dokploy URL (e.g., https://dokploy.example.com): ");
@@ -5627,7 +5456,7 @@ async function workspaceDeployCommand(workspace, options) {
5627
5456
  logger$1.log("\nValidating credentials...");
5628
5457
  const isValid = await validateDokployToken(normalizedEndpoint, token);
5629
5458
  if (!isValid) throw new Error("Invalid credentials. Please check your token.");
5630
- await storeDokployCredentials(token, normalizedEndpoint);
5459
+ await require_credentials.storeDokployCredentials(token, normalizedEndpoint);
5631
5460
  creds = {
5632
5461
  token,
5633
5462
  endpoint: normalizedEndpoint
@@ -5669,7 +5498,12 @@ async function workspaceDeployCommand(workspace, options) {
5669
5498
  logger$1.log(` ✓ Created project: ${project.projectId}`);
5670
5499
  }
5671
5500
  logger$1.log("\n📋 Loading deploy state...");
5672
- let state = await readStageState(workspace.root, stage);
5501
+ const stateProvider = await createStateProvider({
5502
+ config: workspace.state,
5503
+ workspaceRoot: workspace.root,
5504
+ workspaceName: workspace.name
5505
+ });
5506
+ let state = await stateProvider.read(stage);
5673
5507
  if (state) {
5674
5508
  logger$1.log(` Found existing state for stage "${stage}"`);
5675
5509
  if (state.environmentId !== environmentId) {
@@ -5681,7 +5515,7 @@ async function workspaceDeployCommand(workspace, options) {
5681
5515
  state = createEmptyState(stage, environmentId);
5682
5516
  }
5683
5517
  logger$1.log("\n🐳 Checking registry...");
5684
- let registryId = await getDokployRegistryId();
5518
+ let registryId = await require_credentials.getDokployRegistryId();
5685
5519
  const registry = workspace.deploy.dokploy?.registry;
5686
5520
  if (registryId) try {
5687
5521
  const reg = await api.getRegistry(registryId);
@@ -5689,13 +5523,13 @@ async function workspaceDeployCommand(workspace, options) {
5689
5523
  } catch {
5690
5524
  logger$1.log(" ⚠ Stored registry not found, clearing...");
5691
5525
  registryId = void 0;
5692
- await storeDokployRegistryId("");
5526
+ await require_credentials.storeDokployRegistryId("");
5693
5527
  }
5694
5528
  if (!registryId) {
5695
5529
  const registries = await api.listRegistries();
5696
5530
  if (registries.length > 0) {
5697
5531
  registryId = registries[0].registryId;
5698
- await storeDokployRegistryId(registryId);
5532
+ await require_credentials.storeDokployRegistryId(registryId);
5699
5533
  logger$1.log(` Using registry: ${registries[0].registryName}`);
5700
5534
  } else if (registry) {
5701
5535
  logger$1.log(" No registries found in Dokploy. Let's create one.");
@@ -5704,7 +5538,7 @@ async function workspaceDeployCommand(workspace, options) {
5704
5538
  const password = await prompt("Registry password/token: ", true);
5705
5539
  const reg = await api.createRegistry("Default Registry", registry, username, password);
5706
5540
  registryId = reg.registryId;
5707
- await storeDokployRegistryId(registryId);
5541
+ await require_credentials.storeDokployRegistryId(registryId);
5708
5542
  logger$1.log(` ✓ Registry created: ${registryId}`);
5709
5543
  } else logger$1.log(" ⚠ No registry configured. Set deploy.dokploy.registry in workspace config");
5710
5544
  }
@@ -6004,14 +5838,14 @@ async function workspaceDeployCommand(workspace, options) {
6004
5838
  }
6005
5839
  }
6006
5840
  logger$1.log("\n📋 Saving deploy state...");
6007
- await writeStageState(workspace.root, stage, state);
6008
- logger$1.log(` ✓ State saved to .gkm/deploy-${stage}.json`);
5841
+ await stateProvider.write(stage, state);
5842
+ logger$1.log(" ✓ State saved");
6009
5843
  const dnsConfig = workspace.deploy.dns;
6010
5844
  if (dnsConfig && appHostnames.size > 0) {
6011
5845
  const dnsResult = await orchestrateDns(appHostnames, dnsConfig, creds.endpoint);
6012
5846
  if (dnsResult?.serverIp && appHostnames.size > 0) {
6013
5847
  await verifyDnsRecords(appHostnames, dnsResult.serverIp, state);
6014
- await writeStageState(workspace.root, stage, state);
5848
+ await stateProvider.write(stage, state);
6015
5849
  }
6016
5850
  if (dnsResult?.success && appHostnames.size > 0) {
6017
5851
  logger$1.log("\n🔒 Validating domains for SSL certificates...");
@@ -6080,7 +5914,7 @@ async function deployCommand(options) {
6080
5914
  dokployConfig = setupResult.config;
6081
5915
  finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
6082
5916
  if (setupResult.serviceUrls) {
6083
- const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-fOR8dMu5.cjs"));
5917
+ const { readStageSecrets: readStageSecrets$1, writeStageSecrets: writeStageSecrets$1, initStageSecrets } = await Promise.resolve().then(() => require("./storage-C7pmBq1u.cjs"));
6084
5918
  let secrets = await readStageSecrets$1(stage);
6085
5919
  if (!secrets) {
6086
5920
  logger$1.log(` Creating secrets file for stage "${stage}"...`);
@@ -6170,34 +6004,202 @@ async function deployCommand(options) {
6170
6004
  }
6171
6005
 
6172
6006
  //#endregion
6173
- //#region src/secrets/generator.ts
6007
+ //#region src/deploy/state-commands.ts
6174
6008
  /**
6175
- * Generate a secure random password using URL-safe base64 characters.
6176
- * @param length Password length (default: 32)
6009
+ * Pull state from remote to local.
6010
+ * `gkm state:pull --stage=<stage>`
6177
6011
  */
6178
- function generateSecurePassword(length = 32) {
6179
- return (0, node_crypto.randomBytes)(Math.ceil(length * 3 / 4)).toString("base64url").slice(0, length);
6180
- }
6181
- /** Default service configurations */
6182
- const SERVICE_DEFAULTS = {
6183
- postgres: {
6184
- host: "postgres",
6185
- port: 5432,
6186
- username: "app",
6187
- database: "app"
6188
- },
6189
- redis: {
6190
- host: "redis",
6191
- port: 6379,
6192
- username: "default"
6193
- },
6194
- rabbitmq: {
6195
- host: "rabbitmq",
6196
- port: 5672,
6197
- username: "app",
6198
- vhost: "/"
6012
+ async function statePullCommand(options) {
6013
+ const { workspace } = await require_config.loadWorkspaceConfig();
6014
+ if (!workspace.state || workspace.state.provider === "local") {
6015
+ console.error("No remote state provider configured.");
6016
+ console.error("Add a remote provider in gkm.config.ts:");
6017
+ console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
6018
+ process.exit(1);
6199
6019
  }
6200
- };
6020
+ const provider = await createStateProvider({
6021
+ config: workspace.state,
6022
+ workspaceRoot: workspace.root,
6023
+ workspaceName: workspace.name
6024
+ });
6025
+ if (!(provider instanceof require_CachedStateProvider.CachedStateProvider)) {
6026
+ console.error("State provider does not support pull operation.");
6027
+ process.exit(1);
6028
+ }
6029
+ console.log(`Pulling state for stage: ${options.stage}...`);
6030
+ const state = await provider.pull(options.stage);
6031
+ if (state) {
6032
+ console.log("State pulled successfully.");
6033
+ printStateSummary(state);
6034
+ } else console.log("No remote state found for this stage.");
6035
+ }
6036
+ /**
6037
+ * Push local state to remote.
6038
+ * `gkm state:push --stage=<stage>`
6039
+ */
6040
+ async function statePushCommand(options) {
6041
+ const { workspace } = await require_config.loadWorkspaceConfig();
6042
+ if (!workspace.state || workspace.state.provider === "local") {
6043
+ console.error("No remote state provider configured.");
6044
+ console.error("Add a remote provider in gkm.config.ts:");
6045
+ console.error(" state: { provider: \"ssm\", region: \"us-east-1\" }");
6046
+ process.exit(1);
6047
+ }
6048
+ const provider = await createStateProvider({
6049
+ config: workspace.state,
6050
+ workspaceRoot: workspace.root,
6051
+ workspaceName: workspace.name
6052
+ });
6053
+ if (!(provider instanceof require_CachedStateProvider.CachedStateProvider)) {
6054
+ console.error("State provider does not support push operation.");
6055
+ process.exit(1);
6056
+ }
6057
+ console.log(`Pushing state for stage: ${options.stage}...`);
6058
+ const state = await provider.push(options.stage);
6059
+ if (state) {
6060
+ console.log("State pushed successfully.");
6061
+ printStateSummary(state);
6062
+ } else console.log("No local state found for this stage.");
6063
+ }
6064
+ /**
6065
+ * Show current state.
6066
+ * `gkm state:show --stage=<stage>`
6067
+ */
6068
+ async function stateShowCommand(options) {
6069
+ const { workspace } = await require_config.loadWorkspaceConfig();
6070
+ const provider = await createStateProvider({
6071
+ config: workspace.state,
6072
+ workspaceRoot: workspace.root,
6073
+ workspaceName: workspace.name
6074
+ });
6075
+ const state = await provider.read(options.stage);
6076
+ if (!state) {
6077
+ console.log(`No state found for stage: ${options.stage}`);
6078
+ return;
6079
+ }
6080
+ if (options.json) console.log(JSON.stringify(state, null, 2));
6081
+ else printStateDetails(state);
6082
+ }
6083
+ /**
6084
+ * Compare local and remote state.
6085
+ * `gkm state:diff --stage=<stage>`
6086
+ */
6087
+ async function stateDiffCommand(options) {
6088
+ const { workspace } = await require_config.loadWorkspaceConfig();
6089
+ if (!workspace.state || workspace.state.provider === "local") {
6090
+ console.error("No remote state provider configured.");
6091
+ console.error("Diff requires a remote provider to compare against.");
6092
+ process.exit(1);
6093
+ }
6094
+ const provider = await createStateProvider({
6095
+ config: workspace.state,
6096
+ workspaceRoot: workspace.root,
6097
+ workspaceName: workspace.name
6098
+ });
6099
+ if (!(provider instanceof require_CachedStateProvider.CachedStateProvider)) {
6100
+ console.error("State provider does not support diff operation.");
6101
+ process.exit(1);
6102
+ }
6103
+ console.log(`Comparing state for stage: ${options.stage}...\n`);
6104
+ const { local, remote } = await provider.diff(options.stage);
6105
+ if (!local && !remote) {
6106
+ console.log("No state found (local or remote).");
6107
+ return;
6108
+ }
6109
+ if (!local) console.log("Local: (none)");
6110
+ else console.log(`Local: Last deployed ${local.lastDeployedAt}`);
6111
+ if (!remote) console.log("Remote: (none)");
6112
+ else console.log(`Remote: Last deployed ${remote.lastDeployedAt}`);
6113
+ console.log("");
6114
+ const localApps = local?.applications ?? {};
6115
+ const remoteApps = remote?.applications ?? {};
6116
+ const allApps = new Set([...Object.keys(localApps), ...Object.keys(remoteApps)]);
6117
+ if (allApps.size > 0) {
6118
+ console.log("Applications:");
6119
+ for (const app of allApps) {
6120
+ const localId = localApps[app];
6121
+ const remoteId = remoteApps[app];
6122
+ if (localId === remoteId) console.log(` ${app}: ${localId ?? "(none)"}`);
6123
+ else if (!localId) console.log(` ${app}: (none) -> ${remoteId} [REMOTE ONLY]`);
6124
+ else if (!remoteId) console.log(` ${app}: ${localId} -> (none) [LOCAL ONLY]`);
6125
+ else console.log(` ${app}: ${localId} (local) != ${remoteId} (remote) [MISMATCH]`);
6126
+ }
6127
+ }
6128
+ const localServices = local?.services ?? {};
6129
+ const remoteServices = remote?.services ?? {};
6130
+ if (Object.keys(localServices).length > 0 || Object.keys(remoteServices).length > 0) {
6131
+ console.log("\nServices:");
6132
+ const serviceKeys = new Set([...Object.keys(localServices), ...Object.keys(remoteServices)]);
6133
+ for (const key of serviceKeys) {
6134
+ const localVal = localServices[key];
6135
+ const remoteVal = remoteServices[key];
6136
+ if (localVal === remoteVal) console.log(` ${key}: ${localVal ?? "(none)"}`);
6137
+ else console.log(` ${key}: ${localVal ?? "(none)"} (local) != ${remoteVal ?? "(none)"} (remote)`);
6138
+ }
6139
+ }
6140
+ }
6141
+ function printStateSummary(state) {
6142
+ const appCount = Object.keys(state.applications).length;
6143
+ const hasPostgres = !!state.services.postgresId;
6144
+ const hasRedis = !!state.services.redisId;
6145
+ console.log(` Stage: ${state.stage}`);
6146
+ console.log(` Applications: ${appCount}`);
6147
+ console.log(` Postgres: ${hasPostgres ? "configured" : "none"}`);
6148
+ console.log(` Redis: ${hasRedis ? "configured" : "none"}`);
6149
+ console.log(` Last deployed: ${state.lastDeployedAt}`);
6150
+ }
6151
+ function printStateDetails(state) {
6152
+ console.log(`Stage: ${state.stage}`);
6153
+ console.log(`Environment ID: ${state.environmentId}`);
6154
+ console.log(`Last Deployed: ${state.lastDeployedAt}`);
6155
+ console.log("");
6156
+ console.log("Applications:");
6157
+ const apps = Object.entries(state.applications);
6158
+ if (apps.length === 0) console.log(" (none)");
6159
+ else for (const [name$1, id] of apps) console.log(` ${name$1}: ${id}`);
6160
+ console.log("");
6161
+ console.log("Services:");
6162
+ if (!state.services.postgresId && !state.services.redisId) console.log(" (none)");
6163
+ else {
6164
+ if (state.services.postgresId) console.log(` Postgres: ${state.services.postgresId}`);
6165
+ if (state.services.redisId) console.log(` Redis: ${state.services.redisId}`);
6166
+ }
6167
+ if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
6168
+ console.log("");
6169
+ console.log("DNS Verified:");
6170
+ for (const [hostname, info] of Object.entries(state.dnsVerified)) console.log(` ${hostname}: ${info.serverIp} (${info.verifiedAt})`);
6171
+ }
6172
+ }
6173
+
6174
+ //#endregion
6175
+ //#region src/secrets/generator.ts
6176
+ /**
6177
+ * Generate a secure random password using URL-safe base64 characters.
6178
+ * @param length Password length (default: 32)
6179
+ */
6180
+ function generateSecurePassword(length = 32) {
6181
+ return (0, node_crypto.randomBytes)(Math.ceil(length * 3 / 4)).toString("base64url").slice(0, length);
6182
+ }
6183
+ /** Default service configurations */
6184
+ const SERVICE_DEFAULTS = {
6185
+ postgres: {
6186
+ host: "postgres",
6187
+ port: 5432,
6188
+ username: "app",
6189
+ database: "app"
6190
+ },
6191
+ redis: {
6192
+ host: "redis",
6193
+ port: 6379,
6194
+ username: "default"
6195
+ },
6196
+ rabbitmq: {
6197
+ host: "rabbitmq",
6198
+ port: 5672,
6199
+ username: "app",
6200
+ vhost: "/"
6201
+ }
6202
+ };
6201
6203
  /**
6202
6204
  * Generate credentials for a specific service.
6203
6205
  */
@@ -6375,7 +6377,10 @@ function generateAuthAppFiles(options) {
6375
6377
  compilerOptions: {
6376
6378
  noEmit: true,
6377
6379
  baseUrl: ".",
6378
- paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
6380
+ paths: {
6381
+ "~/*": ["./src/*"],
6382
+ [`@${options.name}/*`]: ["../../packages/*/src"]
6383
+ }
6379
6384
  },
6380
6385
  include: ["src/**/*.ts"],
6381
6386
  exclude: ["node_modules", "dist"]
@@ -6585,7 +6590,10 @@ export default defineConfig({
6585
6590
  compilerOptions: {
6586
6591
  noEmit: true,
6587
6592
  baseUrl: ".",
6588
- paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
6593
+ paths: {
6594
+ "~/*": ["./src/*"],
6595
+ [`@${options.name}/*`]: ["../../packages/*/src"]
6596
+ }
6589
6597
  },
6590
6598
  include: ["src/**/*.ts"],
6591
6599
  exclude: ["node_modules", "dist"]
@@ -6705,7 +6713,10 @@ function generateSingleAppConfigFiles(options, _template, _helpers) {
6705
6713
  compilerOptions: {
6706
6714
  noEmit: true,
6707
6715
  baseUrl: ".",
6708
- paths: { [`@${options.name}/*`]: ["../../packages/*/src"] }
6716
+ paths: {
6717
+ "~/*": ["./src/*"],
6718
+ [`@${options.name}/*`]: ["../../packages/*/src"]
6719
+ }
6709
6720
  },
6710
6721
  include: ["src/**/*.ts"],
6711
6722
  exclude: ["node_modules", "dist"]
@@ -7003,7 +7014,11 @@ function generateModelsPackage(options) {
7003
7014
  // Common Schemas
7004
7015
  // ============================================
7005
7016
 
7006
- export const IdSchema = z.string().uuid();
7017
+ export const IdSchema = z.uuid();
7018
+
7019
+ export const IdParamsSchema = z.object({
7020
+ id: IdSchema,
7021
+ });
7007
7022
 
7008
7023
  export const TimestampsSchema = z.object({
7009
7024
  createdAt: z.coerce.date(),
@@ -7029,6 +7044,7 @@ export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =
7029
7044
  // ============================================
7030
7045
 
7031
7046
  export type Id = z.infer<typeof IdSchema>;
7047
+ export type IdParams = z.infer<typeof IdParamsSchema>;
7032
7048
  export type Timestamps = z.infer<typeof TimestampsSchema>;
7033
7049
  export type Pagination = z.infer<typeof PaginationSchema>;
7034
7050
  `;
@@ -7121,6 +7137,7 @@ function generateMonorepoFiles(options, _template) {
7121
7137
  lint: "biome lint .",
7122
7138
  fmt: "biome format . --write",
7123
7139
  "fmt:check": "biome format .",
7140
+ ...isFullstack ? { storybook: "pnpm --filter ./packages/ui storybook" } : {},
7124
7141
  ...options.deployTarget === "dokploy" ? { deploy: "gkm deploy --provider dokploy --stage production" } : {}
7125
7142
  },
7126
7143
  dependencies: { zod: "~4.1.0" },
@@ -7516,7 +7533,20 @@ export const config = envParser
7516
7533
  },
7517
7534
  {
7518
7535
  path: getRoutePath("health.ts"),
7519
- content: `import { e } from '@geekmidas/constructs/endpoints';
7536
+ content: monorepo ? `import { z } from 'zod';
7537
+ import { publicRouter } from '~/router';
7538
+
7539
+ export const healthEndpoint = publicRouter
7540
+ .get('/health')
7541
+ .output(z.object({
7542
+ status: z.string(),
7543
+ timestamp: z.string(),
7544
+ }))
7545
+ .handle(async () => ({
7546
+ status: 'ok',
7547
+ timestamp: new Date().toISOString(),
7548
+ }));
7549
+ ` : `import { e } from '@geekmidas/constructs/endpoints';
7520
7550
  import { z } from 'zod';
7521
7551
 
7522
7552
  export const healthEndpoint = e
@@ -7569,12 +7599,12 @@ export const listUsersEndpoint = e
7569
7599
  {
7570
7600
  path: getRoutePath("users/get.ts"),
7571
7601
  content: modelsImport ? `import { e } from '@geekmidas/constructs/endpoints';
7572
- import { IdSchema } from '${modelsImport}/common';
7602
+ import { IdParamsSchema } from '${modelsImport}/common';
7573
7603
  import { UserResponseSchema } from '${modelsImport}/user';
7574
7604
 
7575
7605
  export const getUserEndpoint = e
7576
7606
  .get('/users/:id')
7577
- .params({ id: IdSchema })
7607
+ .params(IdParamsSchema)
7578
7608
  .output(UserResponseSchema)
7579
7609
  .handle(async ({ params }) => ({
7580
7610
  id: params.id,
@@ -7600,6 +7630,91 @@ export const getUserEndpoint = e
7600
7630
  `
7601
7631
  }
7602
7632
  ];
7633
+ if (options.monorepo) {
7634
+ files.push({
7635
+ path: "src/services/auth.ts",
7636
+ content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
7637
+
7638
+ export interface Session {
7639
+ user: {
7640
+ id: string;
7641
+ email: string;
7642
+ name: string;
7643
+ };
7644
+ }
7645
+
7646
+ export interface AuthClient {
7647
+ getSession: (cookie: string) => Promise<Session | null>;
7648
+ }
7649
+
7650
+ export const authService = {
7651
+ serviceName: 'auth' as const,
7652
+ async register({ envParser, context }: ServiceRegisterOptions) {
7653
+ const logger = context.getLogger();
7654
+
7655
+ const config = envParser
7656
+ .create((get) => ({
7657
+ url: get('AUTH_URL').string(),
7658
+ }))
7659
+ .parse();
7660
+
7661
+ logger.info({ authUrl: config.url }, 'Auth service configured');
7662
+
7663
+ return {
7664
+ getSession: async (cookie: string): Promise<Session | null> => {
7665
+ const res = await fetch(\`\${config.url}/api/auth/get-session\`, {
7666
+ headers: { cookie },
7667
+ });
7668
+ if (!res.ok) return null;
7669
+ return res.json();
7670
+ },
7671
+ };
7672
+ },
7673
+ } satisfies Service<'auth', AuthClient>;
7674
+ `
7675
+ });
7676
+ files.push({
7677
+ path: "src/router.ts",
7678
+ content: `import { e } from '@geekmidas/constructs/endpoints';
7679
+ import { UnauthorizedError } from '@geekmidas/errors';
7680
+ import { authService, type Session } from './services/auth.js';
7681
+ import { logger } from './config/logger.js';
7682
+
7683
+ // Public router - no auth required
7684
+ export const publicRouter = e.logger(logger);
7685
+
7686
+ // Router with auth service available (but session not enforced)
7687
+ export const r = publicRouter.services([authService]);
7688
+
7689
+ // Session router - requires active session, throws if not authenticated
7690
+ export const sessionRouter = r.session<Session>(async ({ services, header }) => {
7691
+ const cookie = header('cookie') || '';
7692
+ const session = await services.auth.getSession(cookie);
7693
+
7694
+ if (!session?.user) {
7695
+ throw new UnauthorizedError('No active session');
7696
+ }
7697
+
7698
+ return session;
7699
+ });
7700
+ `
7701
+ });
7702
+ files.push({
7703
+ path: getRoutePath("profile.ts"),
7704
+ content: `import { z } from 'zod';
7705
+ import { sessionRouter } from '~/router';
7706
+
7707
+ export const profileEndpoint = sessionRouter
7708
+ .get('/profile')
7709
+ .output(z.object({
7710
+ id: z.string(),
7711
+ email: z.string(),
7712
+ name: z.string(),
7713
+ }))
7714
+ .handle(async ({ session }) => session.user);
7715
+ `
7716
+ });
7717
+ }
7603
7718
  if (options.database) files.push({
7604
7719
  path: "src/services/database.ts",
7605
7720
  content: `import type { Service, ServiceRegisterOptions } from '@geekmidas/services';
@@ -8362,6 +8477,1419 @@ function generateSourceFiles(options, template) {
8362
8477
  return template.files(options);
8363
8478
  }
8364
8479
 
8480
+ //#endregion
8481
+ //#region src/init/generators/ui.ts
8482
+ /**
8483
+ * Generate UI package files for fullstack template
8484
+ * Based on @geekmidas/ui with shadcn/ui, Tailwind CSS v4, and Storybook
8485
+ */
8486
+ function generateUiPackageFiles(options) {
8487
+ if (!options.monorepo || options.template !== "fullstack") return [];
8488
+ const packageName = `@${options.name}/ui`;
8489
+ const packageJson = {
8490
+ name: packageName,
8491
+ version: "0.0.1",
8492
+ private: true,
8493
+ type: "module",
8494
+ exports: {
8495
+ ".": "./src/index.ts",
8496
+ "./components": "./src/components/index.ts",
8497
+ "./lib/utils": "./src/lib/utils.ts",
8498
+ "./styles": "./src/styles/globals.css"
8499
+ },
8500
+ scripts: {
8501
+ "ts:check": "tsc --noEmit",
8502
+ storybook: "storybook dev -p 6006",
8503
+ "build:storybook": "storybook build -o dist/storybook"
8504
+ },
8505
+ dependencies: {
8506
+ "@radix-ui/react-dialog": "~1.1.4",
8507
+ "@radix-ui/react-label": "~2.1.2",
8508
+ "@radix-ui/react-separator": "~1.1.2",
8509
+ "@radix-ui/react-slot": "~1.2.4",
8510
+ "@radix-ui/react-tabs": "~1.1.2",
8511
+ "@radix-ui/react-tooltip": "~1.1.6",
8512
+ "class-variance-authority": "~0.7.1",
8513
+ clsx: "^2.1.1",
8514
+ "lucide-react": "~0.562.0",
8515
+ "tailwind-merge": "~3.4.0"
8516
+ },
8517
+ devDependencies: {
8518
+ "@storybook/addon-a11y": "^8.4.7",
8519
+ "@storybook/addon-essentials": "^8.4.7",
8520
+ "@storybook/addon-interactions": "^8.4.7",
8521
+ "@storybook/react": "^8.4.7",
8522
+ "@storybook/react-vite": "^8.4.7",
8523
+ "@tailwindcss/vite": "^4.0.0",
8524
+ "@types/react": "^19.0.0",
8525
+ "@types/react-dom": "^19.0.0",
8526
+ react: "^19.0.0",
8527
+ "react-dom": "^19.0.0",
8528
+ storybook: "^8.4.7",
8529
+ tailwindcss: "^4.0.0",
8530
+ typescript: "^5.8.2",
8531
+ vite: "^6.0.0"
8532
+ },
8533
+ peerDependencies: {
8534
+ react: ">=18.0.0",
8535
+ "react-dom": ">=18.0.0",
8536
+ tailwindcss: ">=4.0.0"
8537
+ }
8538
+ };
8539
+ const tsConfig = {
8540
+ extends: "../../tsconfig.json",
8541
+ compilerOptions: {
8542
+ jsx: "react-jsx",
8543
+ lib: [
8544
+ "ES2023",
8545
+ "DOM",
8546
+ "DOM.Iterable"
8547
+ ],
8548
+ noEmit: true,
8549
+ baseUrl: ".",
8550
+ paths: { "~/*": ["./src/*"] }
8551
+ },
8552
+ include: ["src/**/*"],
8553
+ exclude: [
8554
+ "node_modules",
8555
+ "dist",
8556
+ "**/*.stories.tsx"
8557
+ ]
8558
+ };
8559
+ const componentsJson = {
8560
+ $schema: "https://ui.shadcn.com/schema.json",
8561
+ style: "new-york",
8562
+ rsc: false,
8563
+ tsx: true,
8564
+ tailwind: {
8565
+ config: "",
8566
+ css: "src/styles/globals.css",
8567
+ baseColor: "neutral",
8568
+ cssVariables: true,
8569
+ prefix: ""
8570
+ },
8571
+ aliases: {
8572
+ components: "~/components",
8573
+ utils: "~/lib/utils",
8574
+ ui: "~/components/ui",
8575
+ lib: "~/lib",
8576
+ hooks: "~/hooks"
8577
+ },
8578
+ iconLibrary: "lucide"
8579
+ };
8580
+ const storybookMain = `import type { StorybookConfig } from '@storybook/react-vite';
8581
+
8582
+ const config: StorybookConfig = {
8583
+ stories: ['../src/**/*.stories.@(ts|tsx)'],
8584
+ addons: [
8585
+ '@storybook/addon-essentials',
8586
+ '@storybook/addon-interactions',
8587
+ '@storybook/addon-a11y',
8588
+ ],
8589
+ framework: {
8590
+ name: '@storybook/react-vite',
8591
+ options: {},
8592
+ },
8593
+ docs: {
8594
+ autodocs: 'tag',
8595
+ },
8596
+ viteFinal: async (config) => {
8597
+ // Add Tailwind CSS v4 plugin
8598
+ const tailwindcss = await import('@tailwindcss/vite');
8599
+ config.plugins = config.plugins || [];
8600
+ config.plugins.push(tailwindcss.default());
8601
+ return config;
8602
+ },
8603
+ };
8604
+
8605
+ export default config;
8606
+ `;
8607
+ const storybookPreview = `import type { Preview } from '@storybook/react';
8608
+ import '../src/styles/globals.css';
8609
+
8610
+ const preview: Preview = {
8611
+ parameters: {
8612
+ backgrounds: {
8613
+ default: 'dark',
8614
+ values: [
8615
+ { name: 'dark', value: '#171717' },
8616
+ { name: 'surface', value: '#1c1c1c' },
8617
+ { name: 'light', value: '#fafafa' },
8618
+ ],
8619
+ },
8620
+ controls: {
8621
+ matchers: {
8622
+ color: /(background|color)$/i,
8623
+ date: /Date$/i,
8624
+ },
8625
+ },
8626
+ layout: 'centered',
8627
+ },
8628
+ };
8629
+
8630
+ export default preview;
8631
+ `;
8632
+ const globalsCss = `@import "tailwindcss";
8633
+
8634
+ @theme {
8635
+ --color-background: hsl(var(--background));
8636
+ --color-foreground: hsl(var(--foreground));
8637
+ --color-card: hsl(var(--card));
8638
+ --color-card-foreground: hsl(var(--card-foreground));
8639
+ --color-popover: hsl(var(--popover));
8640
+ --color-popover-foreground: hsl(var(--popover-foreground));
8641
+ --color-primary: hsl(var(--primary));
8642
+ --color-primary-foreground: hsl(var(--primary-foreground));
8643
+ --color-secondary: hsl(var(--secondary));
8644
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
8645
+ --color-muted: hsl(var(--muted));
8646
+ --color-muted-foreground: hsl(var(--muted-foreground));
8647
+ --color-accent: hsl(var(--accent));
8648
+ --color-accent-foreground: hsl(var(--accent-foreground));
8649
+ --color-destructive: hsl(var(--destructive));
8650
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
8651
+ --color-border: hsl(var(--border));
8652
+ --color-input: hsl(var(--input));
8653
+ --color-ring: hsl(var(--ring));
8654
+ --radius-sm: calc(var(--radius) - 4px);
8655
+ --radius-md: calc(var(--radius) - 2px);
8656
+ --radius-lg: var(--radius);
8657
+ --radius-xl: calc(var(--radius) + 4px);
8658
+ }
8659
+
8660
+ @layer base {
8661
+ :root {
8662
+ --background: 0 0% 100%;
8663
+ --foreground: 0 0% 3.9%;
8664
+ --card: 0 0% 100%;
8665
+ --card-foreground: 0 0% 3.9%;
8666
+ --popover: 0 0% 100%;
8667
+ --popover-foreground: 0 0% 3.9%;
8668
+ --primary: 160 84% 39%;
8669
+ --primary-foreground: 0 0% 98%;
8670
+ --secondary: 0 0% 96.1%;
8671
+ --secondary-foreground: 0 0% 9%;
8672
+ --muted: 0 0% 96.1%;
8673
+ --muted-foreground: 0 0% 45.1%;
8674
+ --accent: 0 0% 96.1%;
8675
+ --accent-foreground: 0 0% 9%;
8676
+ --destructive: 0 84.2% 60.2%;
8677
+ --destructive-foreground: 0 0% 98%;
8678
+ --border: 0 0% 89.8%;
8679
+ --input: 0 0% 89.8%;
8680
+ --ring: 160 84% 39%;
8681
+ --radius: 0.5rem;
8682
+ }
8683
+
8684
+ .dark {
8685
+ --background: 0 0% 9%;
8686
+ --foreground: 0 0% 98%;
8687
+ --card: 0 0% 11%;
8688
+ --card-foreground: 0 0% 98%;
8689
+ --popover: 0 0% 11%;
8690
+ --popover-foreground: 0 0% 98%;
8691
+ --primary: 160 84% 52%;
8692
+ --primary-foreground: 0 0% 9%;
8693
+ --secondary: 0 0% 15%;
8694
+ --secondary-foreground: 0 0% 98%;
8695
+ --muted: 0 0% 15%;
8696
+ --muted-foreground: 0 0% 64%;
8697
+ --accent: 0 0% 15%;
8698
+ --accent-foreground: 0 0% 98%;
8699
+ --destructive: 0 62.8% 50.6%;
8700
+ --destructive-foreground: 0 0% 98%;
8701
+ --border: 0 0% 18%;
8702
+ --input: 0 0% 18%;
8703
+ --ring: 160 84% 52%;
8704
+ }
8705
+ }
8706
+
8707
+ @layer base {
8708
+ * {
8709
+ @apply border-border;
8710
+ }
8711
+ body {
8712
+ @apply bg-background text-foreground;
8713
+ }
8714
+ }
8715
+ `;
8716
+ const utilsTs = `import { type ClassValue, clsx } from 'clsx';
8717
+ import { twMerge } from 'tailwind-merge';
8718
+
8719
+ export function cn(...inputs: ClassValue[]) {
8720
+ return twMerge(clsx(inputs));
8721
+ }
8722
+ `;
8723
+ const buttonTsx = `import { Slot } from '@radix-ui/react-slot';
8724
+ import { cva, type VariantProps } from 'class-variance-authority';
8725
+ import * as React from 'react';
8726
+
8727
+ import { cn } from '~/lib/utils';
8728
+
8729
+ const buttonVariants = cva(
8730
+ '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',
8731
+ {
8732
+ variants: {
8733
+ variant: {
8734
+ default:
8735
+ 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
8736
+ destructive:
8737
+ 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
8738
+ outline:
8739
+ 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
8740
+ secondary:
8741
+ 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
8742
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
8743
+ link: 'text-primary underline-offset-4 hover:underline',
8744
+ },
8745
+ size: {
8746
+ default: 'h-9 px-4 py-2',
8747
+ sm: 'h-8 rounded-md px-3 text-xs',
8748
+ lg: 'h-10 rounded-md px-8',
8749
+ icon: 'h-9 w-9',
8750
+ },
8751
+ },
8752
+ defaultVariants: {
8753
+ variant: 'default',
8754
+ size: 'default',
8755
+ },
8756
+ },
8757
+ );
8758
+
8759
+ export interface ButtonProps
8760
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
8761
+ VariantProps<typeof buttonVariants> {
8762
+ asChild?: boolean;
8763
+ }
8764
+
8765
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
8766
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
8767
+ const Comp = asChild ? Slot : 'button';
8768
+ return (
8769
+ <Comp
8770
+ className={cn(buttonVariants({ variant, size, className }))}
8771
+ ref={ref}
8772
+ {...props}
8773
+ />
8774
+ );
8775
+ },
8776
+ );
8777
+ Button.displayName = 'Button';
8778
+
8779
+ export { Button, buttonVariants };
8780
+ `;
8781
+ const buttonStories = `import type { Meta, StoryObj } from '@storybook/react';
8782
+ import { Button } from '.';
8783
+
8784
+ const meta: Meta<typeof Button> = {
8785
+ title: 'Components/Button',
8786
+ component: Button,
8787
+ tags: ['autodocs'],
8788
+ argTypes: {
8789
+ variant: {
8790
+ control: 'select',
8791
+ options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
8792
+ },
8793
+ size: {
8794
+ control: 'select',
8795
+ options: ['default', 'sm', 'lg', 'icon'],
8796
+ },
8797
+ },
8798
+ };
8799
+
8800
+ export default meta;
8801
+ type Story = StoryObj<typeof Button>;
8802
+
8803
+ export const Default: Story = {
8804
+ args: {
8805
+ children: 'Button',
8806
+ variant: 'default',
8807
+ size: 'default',
8808
+ },
8809
+ };
8810
+
8811
+ export const Secondary: Story = {
8812
+ args: {
8813
+ children: 'Secondary',
8814
+ variant: 'secondary',
8815
+ },
8816
+ };
8817
+
8818
+ export const Destructive: Story = {
8819
+ args: {
8820
+ children: 'Destructive',
8821
+ variant: 'destructive',
8822
+ },
8823
+ };
8824
+
8825
+ export const Outline: Story = {
8826
+ args: {
8827
+ children: 'Outline',
8828
+ variant: 'outline',
8829
+ },
8830
+ };
8831
+
8832
+ export const Ghost: Story = {
8833
+ args: {
8834
+ children: 'Ghost',
8835
+ variant: 'ghost',
8836
+ },
8837
+ };
8838
+
8839
+ export const Link: Story = {
8840
+ args: {
8841
+ children: 'Link',
8842
+ variant: 'link',
8843
+ },
8844
+ };
8845
+ `;
8846
+ const inputTsx = `import * as React from 'react';
8847
+
8848
+ import { cn } from '~/lib/utils';
8849
+
8850
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
8851
+ ({ className, type, ...props }, ref) => {
8852
+ return (
8853
+ <input
8854
+ type={type}
8855
+ className={cn(
8856
+ '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',
8857
+ className,
8858
+ )}
8859
+ ref={ref}
8860
+ {...props}
8861
+ />
8862
+ );
8863
+ },
8864
+ );
8865
+ Input.displayName = 'Input';
8866
+
8867
+ export { Input };
8868
+ `;
8869
+ const cardTsx = `import * as React from 'react';
8870
+
8871
+ import { cn } from '~/lib/utils';
8872
+
8873
+ const Card = React.forwardRef<
8874
+ HTMLDivElement,
8875
+ React.HTMLAttributes<HTMLDivElement>
8876
+ >(({ className, ...props }, ref) => (
8877
+ <div
8878
+ ref={ref}
8879
+ className={cn(
8880
+ 'rounded-xl border bg-card text-card-foreground shadow',
8881
+ className,
8882
+ )}
8883
+ {...props}
8884
+ />
8885
+ ));
8886
+ Card.displayName = 'Card';
8887
+
8888
+ const CardHeader = React.forwardRef<
8889
+ HTMLDivElement,
8890
+ React.HTMLAttributes<HTMLDivElement>
8891
+ >(({ className, ...props }, ref) => (
8892
+ <div
8893
+ ref={ref}
8894
+ className={cn('flex flex-col space-y-1.5 p-6', className)}
8895
+ {...props}
8896
+ />
8897
+ ));
8898
+ CardHeader.displayName = 'CardHeader';
8899
+
8900
+ const CardTitle = React.forwardRef<
8901
+ HTMLDivElement,
8902
+ React.HTMLAttributes<HTMLDivElement>
8903
+ >(({ className, ...props }, ref) => (
8904
+ <div
8905
+ ref={ref}
8906
+ className={cn('font-semibold leading-none tracking-tight', className)}
8907
+ {...props}
8908
+ />
8909
+ ));
8910
+ CardTitle.displayName = 'CardTitle';
8911
+
8912
+ const CardDescription = React.forwardRef<
8913
+ HTMLDivElement,
8914
+ React.HTMLAttributes<HTMLDivElement>
8915
+ >(({ className, ...props }, ref) => (
8916
+ <div
8917
+ ref={ref}
8918
+ className={cn('text-sm text-muted-foreground', className)}
8919
+ {...props}
8920
+ />
8921
+ ));
8922
+ CardDescription.displayName = 'CardDescription';
8923
+
8924
+ const CardContent = React.forwardRef<
8925
+ HTMLDivElement,
8926
+ React.HTMLAttributes<HTMLDivElement>
8927
+ >(({ className, ...props }, ref) => (
8928
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
8929
+ ));
8930
+ CardContent.displayName = 'CardContent';
8931
+
8932
+ const CardFooter = React.forwardRef<
8933
+ HTMLDivElement,
8934
+ React.HTMLAttributes<HTMLDivElement>
8935
+ >(({ className, ...props }, ref) => (
8936
+ <div
8937
+ ref={ref}
8938
+ className={cn('flex items-center p-6 pt-0', className)}
8939
+ {...props}
8940
+ />
8941
+ ));
8942
+ CardFooter.displayName = 'CardFooter';
8943
+
8944
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
8945
+ `;
8946
+ const inputStories = `import type { Meta, StoryObj } from '@storybook/react';
8947
+ import { Input } from '.';
8948
+
8949
+ const meta: Meta<typeof Input> = {
8950
+ title: 'Components/Input',
8951
+ component: Input,
8952
+ tags: ['autodocs'],
8953
+ argTypes: {
8954
+ type: {
8955
+ control: 'select',
8956
+ options: ['text', 'email', 'password', 'number', 'search', 'tel', 'url'],
8957
+ },
8958
+ disabled: {
8959
+ control: 'boolean',
8960
+ },
8961
+ },
8962
+ };
8963
+
8964
+ export default meta;
8965
+ type Story = StoryObj<typeof Input>;
8966
+
8967
+ export const Default: Story = {
8968
+ args: {
8969
+ placeholder: 'Enter text...',
8970
+ },
8971
+ };
8972
+
8973
+ export const Email: Story = {
8974
+ args: {
8975
+ type: 'email',
8976
+ placeholder: 'Enter email...',
8977
+ },
8978
+ };
8979
+
8980
+ export const Password: Story = {
8981
+ args: {
8982
+ type: 'password',
8983
+ placeholder: 'Enter password...',
8984
+ },
8985
+ };
8986
+
8987
+ export const Disabled: Story = {
8988
+ args: {
8989
+ placeholder: 'Disabled input',
8990
+ disabled: true,
8991
+ },
8992
+ };
8993
+
8994
+ export const WithValue: Story = {
8995
+ args: {
8996
+ defaultValue: 'Hello World',
8997
+ },
8998
+ };
8999
+ `;
9000
+ const cardStories = `import type { Meta, StoryObj } from '@storybook/react';
9001
+ import { Button } from '../button';
9002
+ import {
9003
+ Card,
9004
+ CardContent,
9005
+ CardDescription,
9006
+ CardFooter,
9007
+ CardHeader,
9008
+ CardTitle,
9009
+ } from '.';
9010
+ import { Input } from '../input';
9011
+
9012
+ const meta: Meta<typeof Card> = {
9013
+ title: 'Components/Card',
9014
+ component: Card,
9015
+ tags: ['autodocs'],
9016
+ };
9017
+
9018
+ export default meta;
9019
+ type Story = StoryObj<typeof Card>;
9020
+
9021
+ export const Default: Story = {
9022
+ render: () => (
9023
+ <Card className="w-[350px]">
9024
+ <CardHeader>
9025
+ <CardTitle>Card Title</CardTitle>
9026
+ <CardDescription>Card description goes here.</CardDescription>
9027
+ </CardHeader>
9028
+ <CardContent>
9029
+ <p>Card content goes here.</p>
9030
+ </CardContent>
9031
+ </Card>
9032
+ ),
9033
+ };
9034
+
9035
+ export const WithFooter: Story = {
9036
+ render: () => (
9037
+ <Card className="w-[350px]">
9038
+ <CardHeader>
9039
+ <CardTitle>Create Account</CardTitle>
9040
+ <CardDescription>Enter your details below.</CardDescription>
9041
+ </CardHeader>
9042
+ <CardContent className="space-y-4">
9043
+ <Input placeholder="Email" type="email" />
9044
+ <Input placeholder="Password" type="password" />
9045
+ </CardContent>
9046
+ <CardFooter className="flex justify-between">
9047
+ <Button variant="outline">Cancel</Button>
9048
+ <Button>Create</Button>
9049
+ </CardFooter>
9050
+ </Card>
9051
+ ),
9052
+ };
9053
+
9054
+ export const Simple: Story = {
9055
+ render: () => (
9056
+ <Card className="w-[350px] p-6">
9057
+ <p>Simple card with just content.</p>
9058
+ </Card>
9059
+ ),
9060
+ };
9061
+ `;
9062
+ const labelTsx = `import * as LabelPrimitive from '@radix-ui/react-label';
9063
+ import { cva, type VariantProps } from 'class-variance-authority';
9064
+ import * as React from 'react';
9065
+
9066
+ import { cn } from '~/lib/utils';
9067
+
9068
+ const labelVariants = cva(
9069
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
9070
+ );
9071
+
9072
+ const Label = React.forwardRef<
9073
+ React.ElementRef<typeof LabelPrimitive.Root>,
9074
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
9075
+ VariantProps<typeof labelVariants>
9076
+ >(({ className, ...props }, ref) => (
9077
+ <LabelPrimitive.Root
9078
+ ref={ref}
9079
+ className={cn(labelVariants(), className)}
9080
+ {...props}
9081
+ />
9082
+ ));
9083
+ Label.displayName = LabelPrimitive.Root.displayName;
9084
+
9085
+ export { Label };
9086
+ `;
9087
+ const labelStories = `import type { Meta, StoryObj } from '@storybook/react';
9088
+ import { Input } from '../input';
9089
+ import { Label } from '.';
9090
+
9091
+ const meta: Meta<typeof Label> = {
9092
+ title: 'Components/Label',
9093
+ component: Label,
9094
+ tags: ['autodocs'],
9095
+ };
9096
+
9097
+ export default meta;
9098
+ type Story = StoryObj<typeof Label>;
9099
+
9100
+ export const Default: Story = {
9101
+ args: {
9102
+ children: 'Label',
9103
+ },
9104
+ };
9105
+
9106
+ export const WithInput: Story = {
9107
+ render: () => (
9108
+ <div className="grid w-full max-w-sm items-center gap-1.5">
9109
+ <Label htmlFor="email">Email</Label>
9110
+ <Input type="email" id="email" placeholder="Email" />
9111
+ </div>
9112
+ ),
9113
+ };
9114
+
9115
+ export const Disabled: Story = {
9116
+ render: () => (
9117
+ <div className="grid w-full max-w-sm items-center gap-1.5">
9118
+ <Label htmlFor="disabled" className="peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
9119
+ Disabled
9120
+ </Label>
9121
+ <Input type="text" id="disabled" placeholder="Disabled" disabled className="peer" />
9122
+ </div>
9123
+ ),
9124
+ };
9125
+ `;
9126
+ const badgeTsx = `import { cva, type VariantProps } from 'class-variance-authority';
9127
+ import * as React from 'react';
9128
+
9129
+ import { cn } from '~/lib/utils';
9130
+
9131
+ const badgeVariants = cva(
9132
+ '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',
9133
+ {
9134
+ variants: {
9135
+ variant: {
9136
+ default:
9137
+ 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
9138
+ secondary:
9139
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
9140
+ destructive:
9141
+ 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
9142
+ outline: 'text-foreground',
9143
+ },
9144
+ },
9145
+ defaultVariants: {
9146
+ variant: 'default',
9147
+ },
9148
+ },
9149
+ );
9150
+
9151
+ export interface BadgeProps
9152
+ extends React.HTMLAttributes<HTMLDivElement>,
9153
+ VariantProps<typeof badgeVariants> {}
9154
+
9155
+ function Badge({ className, variant, ...props }: BadgeProps) {
9156
+ return (
9157
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
9158
+ );
9159
+ }
9160
+
9161
+ export { Badge, badgeVariants };
9162
+ `;
9163
+ const badgeStories = `import type { Meta, StoryObj } from '@storybook/react';
9164
+ import { Badge } from '.';
9165
+
9166
+ const meta: Meta<typeof Badge> = {
9167
+ title: 'Components/Badge',
9168
+ component: Badge,
9169
+ tags: ['autodocs'],
9170
+ argTypes: {
9171
+ variant: {
9172
+ control: 'select',
9173
+ options: ['default', 'secondary', 'destructive', 'outline'],
9174
+ },
9175
+ },
9176
+ };
9177
+
9178
+ export default meta;
9179
+ type Story = StoryObj<typeof Badge>;
9180
+
9181
+ export const Default: Story = {
9182
+ args: {
9183
+ children: 'Badge',
9184
+ variant: 'default',
9185
+ },
9186
+ };
9187
+
9188
+ export const Secondary: Story = {
9189
+ args: {
9190
+ children: 'Secondary',
9191
+ variant: 'secondary',
9192
+ },
9193
+ };
9194
+
9195
+ export const Destructive: Story = {
9196
+ args: {
9197
+ children: 'Destructive',
9198
+ variant: 'destructive',
9199
+ },
9200
+ };
9201
+
9202
+ export const Outline: Story = {
9203
+ args: {
9204
+ children: 'Outline',
9205
+ variant: 'outline',
9206
+ },
9207
+ };
9208
+ `;
9209
+ const separatorTsx = `import * as SeparatorPrimitive from '@radix-ui/react-separator';
9210
+ import * as React from 'react';
9211
+
9212
+ import { cn } from '~/lib/utils';
9213
+
9214
+ const Separator = React.forwardRef<
9215
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
9216
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9217
+ >(
9218
+ (
9219
+ { className, orientation = 'horizontal', decorative = true, ...props },
9220
+ ref,
9221
+ ) => (
9222
+ <SeparatorPrimitive.Root
9223
+ ref={ref}
9224
+ decorative={decorative}
9225
+ orientation={orientation}
9226
+ className={cn(
9227
+ 'shrink-0 bg-border',
9228
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
9229
+ className,
9230
+ )}
9231
+ {...props}
9232
+ />
9233
+ ),
9234
+ );
9235
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
9236
+
9237
+ export { Separator };
9238
+ `;
9239
+ const separatorStories = `import type { Meta, StoryObj } from '@storybook/react';
9240
+ import { Separator } from '.';
9241
+
9242
+ const meta: Meta<typeof Separator> = {
9243
+ title: 'Components/Separator',
9244
+ component: Separator,
9245
+ tags: ['autodocs'],
9246
+ argTypes: {
9247
+ orientation: {
9248
+ control: 'select',
9249
+ options: ['horizontal', 'vertical'],
9250
+ },
9251
+ },
9252
+ };
9253
+
9254
+ export default meta;
9255
+ type Story = StoryObj<typeof Separator>;
9256
+
9257
+ export const Horizontal: Story = {
9258
+ render: () => (
9259
+ <div className="w-[300px]">
9260
+ <div className="space-y-1">
9261
+ <h4 className="text-sm font-medium leading-none">Radix Primitives</h4>
9262
+ <p className="text-sm text-muted-foreground">
9263
+ An open-source UI component library.
9264
+ </p>
9265
+ </div>
9266
+ <Separator className="my-4" />
9267
+ <div className="flex h-5 items-center space-x-4 text-sm">
9268
+ <div>Blog</div>
9269
+ <Separator orientation="vertical" />
9270
+ <div>Docs</div>
9271
+ <Separator orientation="vertical" />
9272
+ <div>Source</div>
9273
+ </div>
9274
+ </div>
9275
+ ),
9276
+ };
9277
+
9278
+ export const Vertical: Story = {
9279
+ render: () => (
9280
+ <div className="flex h-5 items-center space-x-4 text-sm">
9281
+ <div>Blog</div>
9282
+ <Separator orientation="vertical" />
9283
+ <div>Docs</div>
9284
+ <Separator orientation="vertical" />
9285
+ <div>Source</div>
9286
+ </div>
9287
+ ),
9288
+ };
9289
+ `;
9290
+ const tabsTsx = `import * as TabsPrimitive from '@radix-ui/react-tabs';
9291
+ import * as React from 'react';
9292
+
9293
+ import { cn } from '~/lib/utils';
9294
+
9295
+ const Tabs = TabsPrimitive.Root;
9296
+
9297
+ const TabsList = React.forwardRef<
9298
+ React.ElementRef<typeof TabsPrimitive.List>,
9299
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
9300
+ >(({ className, ...props }, ref) => (
9301
+ <TabsPrimitive.List
9302
+ ref={ref}
9303
+ className={cn(
9304
+ 'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
9305
+ className,
9306
+ )}
9307
+ {...props}
9308
+ />
9309
+ ));
9310
+ TabsList.displayName = TabsPrimitive.List.displayName;
9311
+
9312
+ const TabsTrigger = React.forwardRef<
9313
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
9314
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
9315
+ >(({ className, ...props }, ref) => (
9316
+ <TabsPrimitive.Trigger
9317
+ ref={ref}
9318
+ className={cn(
9319
+ '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',
9320
+ className,
9321
+ )}
9322
+ {...props}
9323
+ />
9324
+ ));
9325
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
9326
+
9327
+ const TabsContent = React.forwardRef<
9328
+ React.ElementRef<typeof TabsPrimitive.Content>,
9329
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
9330
+ >(({ className, ...props }, ref) => (
9331
+ <TabsPrimitive.Content
9332
+ ref={ref}
9333
+ className={cn(
9334
+ 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
9335
+ className,
9336
+ )}
9337
+ {...props}
9338
+ />
9339
+ ));
9340
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
9341
+
9342
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
9343
+ `;
9344
+ const tabsStories = `import type { Meta, StoryObj } from '@storybook/react';
9345
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '.';
9346
+ import { Button } from '../button';
9347
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../card';
9348
+ import { Input } from '../input';
9349
+ import { Label } from '../label';
9350
+
9351
+ const meta: Meta<typeof Tabs> = {
9352
+ title: 'Components/Tabs',
9353
+ component: Tabs,
9354
+ tags: ['autodocs'],
9355
+ };
9356
+
9357
+ export default meta;
9358
+ type Story = StoryObj<typeof Tabs>;
9359
+
9360
+ export const Default: Story = {
9361
+ render: () => (
9362
+ <Tabs defaultValue="account" className="w-[400px]">
9363
+ <TabsList>
9364
+ <TabsTrigger value="account">Account</TabsTrigger>
9365
+ <TabsTrigger value="password">Password</TabsTrigger>
9366
+ </TabsList>
9367
+ <TabsContent value="account">
9368
+ <Card>
9369
+ <CardHeader>
9370
+ <CardTitle>Account</CardTitle>
9371
+ <CardDescription>
9372
+ Make changes to your account here. Click save when you're done.
9373
+ </CardDescription>
9374
+ </CardHeader>
9375
+ <CardContent className="space-y-2">
9376
+ <div className="space-y-1">
9377
+ <Label htmlFor="name">Name</Label>
9378
+ <Input id="name" defaultValue="Pedro Duarte" />
9379
+ </div>
9380
+ <div className="space-y-1">
9381
+ <Label htmlFor="username">Username</Label>
9382
+ <Input id="username" defaultValue="@peduarte" />
9383
+ </div>
9384
+ </CardContent>
9385
+ <CardFooter>
9386
+ <Button>Save changes</Button>
9387
+ </CardFooter>
9388
+ </Card>
9389
+ </TabsContent>
9390
+ <TabsContent value="password">
9391
+ <Card>
9392
+ <CardHeader>
9393
+ <CardTitle>Password</CardTitle>
9394
+ <CardDescription>
9395
+ Change your password here. After saving, you'll be logged out.
9396
+ </CardDescription>
9397
+ </CardHeader>
9398
+ <CardContent className="space-y-2">
9399
+ <div className="space-y-1">
9400
+ <Label htmlFor="current">Current password</Label>
9401
+ <Input id="current" type="password" />
9402
+ </div>
9403
+ <div className="space-y-1">
9404
+ <Label htmlFor="new">New password</Label>
9405
+ <Input id="new" type="password" />
9406
+ </div>
9407
+ </CardContent>
9408
+ <CardFooter>
9409
+ <Button>Save password</Button>
9410
+ </CardFooter>
9411
+ </Card>
9412
+ </TabsContent>
9413
+ </Tabs>
9414
+ ),
9415
+ };
9416
+ `;
9417
+ const tooltipTsx = `import * as TooltipPrimitive from '@radix-ui/react-tooltip';
9418
+ import * as React from 'react';
9419
+
9420
+ import { cn } from '~/lib/utils';
9421
+
9422
+ const TooltipProvider = TooltipPrimitive.Provider;
9423
+
9424
+ const Tooltip = TooltipPrimitive.Root;
9425
+
9426
+ const TooltipTrigger = TooltipPrimitive.Trigger;
9427
+
9428
+ const TooltipContent = React.forwardRef<
9429
+ React.ElementRef<typeof TooltipPrimitive.Content>,
9430
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
9431
+ >(({ className, sideOffset = 4, ...props }, ref) => (
9432
+ <TooltipPrimitive.Portal>
9433
+ <TooltipPrimitive.Content
9434
+ ref={ref}
9435
+ sideOffset={sideOffset}
9436
+ className={cn(
9437
+ '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',
9438
+ className,
9439
+ )}
9440
+ {...props}
9441
+ />
9442
+ </TooltipPrimitive.Portal>
9443
+ ));
9444
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
9445
+
9446
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
9447
+ `;
9448
+ const tooltipStories = `import type { Meta, StoryObj } from '@storybook/react';
9449
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.';
9450
+ import { Button } from '../button';
9451
+
9452
+ const meta: Meta<typeof Tooltip> = {
9453
+ title: 'Components/Tooltip',
9454
+ component: Tooltip,
9455
+ tags: ['autodocs'],
9456
+ decorators: [
9457
+ (Story) => (
9458
+ <TooltipProvider>
9459
+ <Story />
9460
+ </TooltipProvider>
9461
+ ),
9462
+ ],
9463
+ };
9464
+
9465
+ export default meta;
9466
+ type Story = StoryObj<typeof Tooltip>;
9467
+
9468
+ export const Default: Story = {
9469
+ render: () => (
9470
+ <Tooltip>
9471
+ <TooltipTrigger asChild>
9472
+ <Button variant="outline">Hover me</Button>
9473
+ </TooltipTrigger>
9474
+ <TooltipContent>
9475
+ <p>Add to library</p>
9476
+ </TooltipContent>
9477
+ </Tooltip>
9478
+ ),
9479
+ };
9480
+
9481
+ export const Positions: Story = {
9482
+ render: () => (
9483
+ <div className="flex gap-4">
9484
+ <Tooltip>
9485
+ <TooltipTrigger asChild>
9486
+ <Button variant="outline">Top</Button>
9487
+ </TooltipTrigger>
9488
+ <TooltipContent side="top">
9489
+ <p>Top tooltip</p>
9490
+ </TooltipContent>
9491
+ </Tooltip>
9492
+ <Tooltip>
9493
+ <TooltipTrigger asChild>
9494
+ <Button variant="outline">Bottom</Button>
9495
+ </TooltipTrigger>
9496
+ <TooltipContent side="bottom">
9497
+ <p>Bottom tooltip</p>
9498
+ </TooltipContent>
9499
+ </Tooltip>
9500
+ <Tooltip>
9501
+ <TooltipTrigger asChild>
9502
+ <Button variant="outline">Left</Button>
9503
+ </TooltipTrigger>
9504
+ <TooltipContent side="left">
9505
+ <p>Left tooltip</p>
9506
+ </TooltipContent>
9507
+ </Tooltip>
9508
+ <Tooltip>
9509
+ <TooltipTrigger asChild>
9510
+ <Button variant="outline">Right</Button>
9511
+ </TooltipTrigger>
9512
+ <TooltipContent side="right">
9513
+ <p>Right tooltip</p>
9514
+ </TooltipContent>
9515
+ </Tooltip>
9516
+ </div>
9517
+ ),
9518
+ };
9519
+ `;
9520
+ const dialogTsx = `import * as DialogPrimitive from '@radix-ui/react-dialog';
9521
+ import { X } from 'lucide-react';
9522
+ import * as React from 'react';
9523
+
9524
+ import { cn } from '~/lib/utils';
9525
+
9526
+ const Dialog = DialogPrimitive.Root;
9527
+
9528
+ const DialogTrigger = DialogPrimitive.Trigger;
9529
+
9530
+ const DialogPortal = DialogPrimitive.Portal;
9531
+
9532
+ const DialogClose = DialogPrimitive.Close;
9533
+
9534
+ const DialogOverlay = React.forwardRef<
9535
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
9536
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
9537
+ >(({ className, ...props }, ref) => (
9538
+ <DialogPrimitive.Overlay
9539
+ ref={ref}
9540
+ className={cn(
9541
+ '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',
9542
+ className,
9543
+ )}
9544
+ {...props}
9545
+ />
9546
+ ));
9547
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
9548
+
9549
+ const DialogContent = React.forwardRef<
9550
+ React.ElementRef<typeof DialogPrimitive.Content>,
9551
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
9552
+ >(({ className, children, ...props }, ref) => (
9553
+ <DialogPortal>
9554
+ <DialogOverlay />
9555
+ <DialogPrimitive.Content
9556
+ ref={ref}
9557
+ className={cn(
9558
+ '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',
9559
+ className,
9560
+ )}
9561
+ {...props}
9562
+ >
9563
+ {children}
9564
+ <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">
9565
+ <X className="h-4 w-4" />
9566
+ <span className="sr-only">Close</span>
9567
+ </DialogPrimitive.Close>
9568
+ </DialogPrimitive.Content>
9569
+ </DialogPortal>
9570
+ ));
9571
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
9572
+
9573
+ const DialogHeader = ({
9574
+ className,
9575
+ ...props
9576
+ }: React.HTMLAttributes<HTMLDivElement>) => (
9577
+ <div
9578
+ className={cn(
9579
+ 'flex flex-col space-y-1.5 text-center sm:text-left',
9580
+ className,
9581
+ )}
9582
+ {...props}
9583
+ />
9584
+ );
9585
+ DialogHeader.displayName = 'DialogHeader';
9586
+
9587
+ const DialogFooter = ({
9588
+ className,
9589
+ ...props
9590
+ }: React.HTMLAttributes<HTMLDivElement>) => (
9591
+ <div
9592
+ className={cn(
9593
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
9594
+ className,
9595
+ )}
9596
+ {...props}
9597
+ />
9598
+ );
9599
+ DialogFooter.displayName = 'DialogFooter';
9600
+
9601
+ const DialogTitle = React.forwardRef<
9602
+ React.ElementRef<typeof DialogPrimitive.Title>,
9603
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
9604
+ >(({ className, ...props }, ref) => (
9605
+ <DialogPrimitive.Title
9606
+ ref={ref}
9607
+ className={cn(
9608
+ 'text-lg font-semibold leading-none tracking-tight',
9609
+ className,
9610
+ )}
9611
+ {...props}
9612
+ />
9613
+ ));
9614
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
9615
+
9616
+ const DialogDescription = React.forwardRef<
9617
+ React.ElementRef<typeof DialogPrimitive.Description>,
9618
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
9619
+ >(({ className, ...props }, ref) => (
9620
+ <DialogPrimitive.Description
9621
+ ref={ref}
9622
+ className={cn('text-sm text-muted-foreground', className)}
9623
+ {...props}
9624
+ />
9625
+ ));
9626
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
9627
+
9628
+ export {
9629
+ Dialog,
9630
+ DialogPortal,
9631
+ DialogOverlay,
9632
+ DialogTrigger,
9633
+ DialogClose,
9634
+ DialogContent,
9635
+ DialogHeader,
9636
+ DialogFooter,
9637
+ DialogTitle,
9638
+ DialogDescription,
9639
+ };
9640
+ `;
9641
+ const dialogStories = `import type { Meta, StoryObj } from '@storybook/react';
9642
+ import {
9643
+ Dialog,
9644
+ DialogContent,
9645
+ DialogDescription,
9646
+ DialogFooter,
9647
+ DialogHeader,
9648
+ DialogTitle,
9649
+ DialogTrigger,
9650
+ } from '.';
9651
+ import { Button } from '../button';
9652
+ import { Input } from '../input';
9653
+ import { Label } from '../label';
9654
+
9655
+ const meta: Meta<typeof Dialog> = {
9656
+ title: 'Components/Dialog',
9657
+ component: Dialog,
9658
+ tags: ['autodocs'],
9659
+ };
9660
+
9661
+ export default meta;
9662
+ type Story = StoryObj<typeof Dialog>;
9663
+
9664
+ export const Default: Story = {
9665
+ render: () => (
9666
+ <Dialog>
9667
+ <DialogTrigger asChild>
9668
+ <Button variant="outline">Edit Profile</Button>
9669
+ </DialogTrigger>
9670
+ <DialogContent className="sm:max-w-[425px]">
9671
+ <DialogHeader>
9672
+ <DialogTitle>Edit profile</DialogTitle>
9673
+ <DialogDescription>
9674
+ Make changes to your profile here. Click save when you're done.
9675
+ </DialogDescription>
9676
+ </DialogHeader>
9677
+ <div className="grid gap-4 py-4">
9678
+ <div className="grid grid-cols-4 items-center gap-4">
9679
+ <Label htmlFor="name" className="text-right">
9680
+ Name
9681
+ </Label>
9682
+ <Input id="name" defaultValue="Pedro Duarte" className="col-span-3" />
9683
+ </div>
9684
+ <div className="grid grid-cols-4 items-center gap-4">
9685
+ <Label htmlFor="username" className="text-right">
9686
+ Username
9687
+ </Label>
9688
+ <Input id="username" defaultValue="@peduarte" className="col-span-3" />
9689
+ </div>
9690
+ </div>
9691
+ <DialogFooter>
9692
+ <Button type="submit">Save changes</Button>
9693
+ </DialogFooter>
9694
+ </DialogContent>
9695
+ </Dialog>
9696
+ ),
9697
+ };
9698
+
9699
+ export const Alert: Story = {
9700
+ render: () => (
9701
+ <Dialog>
9702
+ <DialogTrigger asChild>
9703
+ <Button variant="destructive">Delete Account</Button>
9704
+ </DialogTrigger>
9705
+ <DialogContent>
9706
+ <DialogHeader>
9707
+ <DialogTitle>Are you absolutely sure?</DialogTitle>
9708
+ <DialogDescription>
9709
+ This action cannot be undone. This will permanently delete your
9710
+ account and remove your data from our servers.
9711
+ </DialogDescription>
9712
+ </DialogHeader>
9713
+ <DialogFooter>
9714
+ <Button variant="outline">Cancel</Button>
9715
+ <Button variant="destructive">Delete</Button>
9716
+ </DialogFooter>
9717
+ </DialogContent>
9718
+ </Dialog>
9719
+ ),
9720
+ };
9721
+ `;
9722
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button';
9723
+ export { Input } from './input';
9724
+ export {
9725
+ Card,
9726
+ CardHeader,
9727
+ CardFooter,
9728
+ CardTitle,
9729
+ CardDescription,
9730
+ CardContent,
9731
+ } from './card';
9732
+ export { Label } from './label';
9733
+ export { Badge, type BadgeProps, badgeVariants } from './badge';
9734
+ export { Separator } from './separator';
9735
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
9736
+ export {
9737
+ Tooltip,
9738
+ TooltipTrigger,
9739
+ TooltipContent,
9740
+ TooltipProvider,
9741
+ } from './tooltip';
9742
+ export {
9743
+ Dialog,
9744
+ DialogPortal,
9745
+ DialogOverlay,
9746
+ DialogTrigger,
9747
+ DialogClose,
9748
+ DialogContent,
9749
+ DialogHeader,
9750
+ DialogFooter,
9751
+ DialogTitle,
9752
+ DialogDescription,
9753
+ } from './dialog';
9754
+ `;
9755
+ const buttonIndexTsx = buttonTsx;
9756
+ const inputIndexTsx = inputTsx;
9757
+ const cardIndexTsx = cardTsx;
9758
+ const componentsIndex = `export * from './ui';
9759
+ `;
9760
+ const indexTs = `// @${options.name}/ui - Shared UI component library
9761
+
9762
+ // shadcn/ui components
9763
+ export * from './components';
9764
+
9765
+ // Utilities
9766
+ export { cn } from './lib/utils';
9767
+ `;
9768
+ const gitignore = `node_modules/
9769
+ dist/
9770
+ storybook-static/
9771
+ *.log
9772
+ `;
9773
+ return [
9774
+ {
9775
+ path: "packages/ui/package.json",
9776
+ content: `${JSON.stringify(packageJson, null, 2)}\n`
9777
+ },
9778
+ {
9779
+ path: "packages/ui/tsconfig.json",
9780
+ content: `${JSON.stringify(tsConfig, null, 2)}\n`
9781
+ },
9782
+ {
9783
+ path: "packages/ui/components.json",
9784
+ content: `${JSON.stringify(componentsJson, null, 2)}\n`
9785
+ },
9786
+ {
9787
+ path: "packages/ui/.storybook/main.ts",
9788
+ content: storybookMain
9789
+ },
9790
+ {
9791
+ path: "packages/ui/.storybook/preview.ts",
9792
+ content: storybookPreview
9793
+ },
9794
+ {
9795
+ path: "packages/ui/src/styles/globals.css",
9796
+ content: globalsCss
9797
+ },
9798
+ {
9799
+ path: "packages/ui/src/lib/utils.ts",
9800
+ content: utilsTs
9801
+ },
9802
+ {
9803
+ path: "packages/ui/src/components/ui/button/index.tsx",
9804
+ content: buttonIndexTsx
9805
+ },
9806
+ {
9807
+ path: "packages/ui/src/components/ui/button/button.stories.tsx",
9808
+ content: buttonStories
9809
+ },
9810
+ {
9811
+ path: "packages/ui/src/components/ui/input/index.tsx",
9812
+ content: inputIndexTsx
9813
+ },
9814
+ {
9815
+ path: "packages/ui/src/components/ui/input/input.stories.tsx",
9816
+ content: inputStories
9817
+ },
9818
+ {
9819
+ path: "packages/ui/src/components/ui/card/index.tsx",
9820
+ content: cardIndexTsx
9821
+ },
9822
+ {
9823
+ path: "packages/ui/src/components/ui/card/card.stories.tsx",
9824
+ content: cardStories
9825
+ },
9826
+ {
9827
+ path: "packages/ui/src/components/ui/label/index.tsx",
9828
+ content: labelTsx
9829
+ },
9830
+ {
9831
+ path: "packages/ui/src/components/ui/label/label.stories.tsx",
9832
+ content: labelStories
9833
+ },
9834
+ {
9835
+ path: "packages/ui/src/components/ui/badge/index.tsx",
9836
+ content: badgeTsx
9837
+ },
9838
+ {
9839
+ path: "packages/ui/src/components/ui/badge/badge.stories.tsx",
9840
+ content: badgeStories
9841
+ },
9842
+ {
9843
+ path: "packages/ui/src/components/ui/separator/index.tsx",
9844
+ content: separatorTsx
9845
+ },
9846
+ {
9847
+ path: "packages/ui/src/components/ui/separator/separator.stories.tsx",
9848
+ content: separatorStories
9849
+ },
9850
+ {
9851
+ path: "packages/ui/src/components/ui/tabs/index.tsx",
9852
+ content: tabsTsx
9853
+ },
9854
+ {
9855
+ path: "packages/ui/src/components/ui/tabs/tabs.stories.tsx",
9856
+ content: tabsStories
9857
+ },
9858
+ {
9859
+ path: "packages/ui/src/components/ui/tooltip/index.tsx",
9860
+ content: tooltipTsx
9861
+ },
9862
+ {
9863
+ path: "packages/ui/src/components/ui/tooltip/tooltip.stories.tsx",
9864
+ content: tooltipStories
9865
+ },
9866
+ {
9867
+ path: "packages/ui/src/components/ui/dialog/index.tsx",
9868
+ content: dialogTsx
9869
+ },
9870
+ {
9871
+ path: "packages/ui/src/components/ui/dialog/dialog.stories.tsx",
9872
+ content: dialogStories
9873
+ },
9874
+ {
9875
+ path: "packages/ui/src/components/ui/index.ts",
9876
+ content: componentsUiIndex
9877
+ },
9878
+ {
9879
+ path: "packages/ui/src/components/index.ts",
9880
+ content: componentsIndex
9881
+ },
9882
+ {
9883
+ path: "packages/ui/src/index.ts",
9884
+ content: indexTs
9885
+ },
9886
+ {
9887
+ path: "packages/ui/.gitignore",
9888
+ content: gitignore
9889
+ }
9890
+ ];
9891
+ }
9892
+
8365
9893
  //#endregion
8366
9894
  //#region src/init/generators/web.ts
8367
9895
  /**
@@ -8371,29 +9899,35 @@ function generateWebAppFiles(options) {
8371
9899
  if (!options.monorepo || options.template !== "fullstack") return [];
8372
9900
  const packageName = `@${options.name}/web`;
8373
9901
  const modelsPackage = `@${options.name}/models`;
9902
+ const uiPackage = `@${options.name}/ui`;
8374
9903
  const packageJson = {
8375
9904
  name: packageName,
8376
9905
  version: "0.0.1",
8377
9906
  private: true,
8378
9907
  type: "module",
8379
9908
  scripts: {
8380
- dev: "next dev -p 3001",
8381
- build: "next build",
9909
+ dev: "gkm exec -- next dev --turbopack",
9910
+ build: "gkm exec -- next build",
8382
9911
  start: "next start",
8383
9912
  typecheck: "tsc --noEmit"
8384
9913
  },
8385
9914
  dependencies: {
8386
9915
  [modelsPackage]: "workspace:*",
9916
+ [uiPackage]: "workspace:*",
8387
9917
  "@geekmidas/client": GEEKMIDAS_VERSIONS["@geekmidas/client"],
8388
9918
  "@tanstack/react-query": "~5.80.0",
9919
+ "better-auth": "~1.2.0",
8389
9920
  next: "~16.1.0",
8390
9921
  react: "~19.2.0",
8391
9922
  "react-dom": "~19.2.0"
8392
9923
  },
8393
9924
  devDependencies: {
9925
+ "@geekmidas/cli": GEEKMIDAS_VERSIONS["@geekmidas/cli"],
9926
+ "@tailwindcss/postcss": "^4.0.0",
8394
9927
  "@types/node": "~22.0.0",
8395
9928
  "@types/react": "~19.0.0",
8396
9929
  "@types/react-dom": "~19.0.0",
9930
+ tailwindcss: "^4.0.0",
8397
9931
  typescript: "~5.8.2"
8398
9932
  }
8399
9933
  };
@@ -8402,10 +9936,16 @@ function generateWebAppFiles(options) {
8402
9936
  const nextConfig: NextConfig = {
8403
9937
  output: 'standalone',
8404
9938
  reactStrictMode: true,
8405
- transpilePackages: ['${modelsPackage}'],
9939
+ transpilePackages: ['${modelsPackage}', '${uiPackage}'],
8406
9940
  };
8407
9941
 
8408
9942
  export default nextConfig;
9943
+ `;
9944
+ const postcssConfig = `export default {
9945
+ plugins: {
9946
+ '@tailwindcss/postcss': {},
9947
+ },
9948
+ };
8409
9949
  `;
8410
9950
  const tsConfig = {
8411
9951
  extends: "../../tsconfig.json",
@@ -8428,9 +9968,11 @@ export default nextConfig;
8428
9968
  incremental: true,
8429
9969
  plugins: [{ name: "next" }],
8430
9970
  paths: {
8431
- "@/*": ["./src/*"],
9971
+ "~/*": ["./src/*"],
8432
9972
  [`${modelsPackage}`]: ["../../packages/models/src"],
8433
- [`${modelsPackage}/*`]: ["../../packages/models/src/*"]
9973
+ [`${modelsPackage}/*`]: ["../../packages/models/src/*"],
9974
+ [`${uiPackage}`]: ["../../packages/ui/src"],
9975
+ [`${uiPackage}/*`]: ["../../packages/ui/src/*"]
8434
9976
  },
8435
9977
  baseUrl: "."
8436
9978
  },
@@ -8442,68 +9984,66 @@ export default nextConfig;
8442
9984
  ],
8443
9985
  exclude: ["node_modules"]
8444
9986
  };
9987
+ const queryClientTs = `import { QueryClient } from '@tanstack/react-query';
9988
+
9989
+ function makeQueryClient() {
9990
+ return new QueryClient({
9991
+ defaultOptions: {
9992
+ queries: {
9993
+ staleTime: 60 * 1000,
9994
+ },
9995
+ },
9996
+ });
9997
+ }
9998
+
9999
+ let browserQueryClient: QueryClient | undefined = undefined;
10000
+
10001
+ export function getQueryClient() {
10002
+ if (typeof window === 'undefined') {
10003
+ // Server: always make a new query client
10004
+ return makeQueryClient();
10005
+ }
10006
+ // Browser: reuse existing query client
10007
+ if (!browserQueryClient) browserQueryClient = makeQueryClient();
10008
+ return browserQueryClient;
10009
+ }
10010
+ `;
10011
+ const authClientTs = `import { createAuthClient } from 'better-auth/react';
10012
+ import { magicLinkClient } from 'better-auth/client/plugins';
10013
+
10014
+ export const authClient = createAuthClient({
10015
+ baseURL: process.env.NEXT_PUBLIC_AUTH_URL || 'http://localhost:3002',
10016
+ plugins: [magicLinkClient()],
10017
+ });
10018
+
10019
+ export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
10020
+ `;
8445
10021
  const providersTsx = `'use client';
8446
10022
 
8447
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
8448
- import { useState } from 'react';
10023
+ import { QueryClientProvider } from '@tanstack/react-query';
10024
+ import { getQueryClient } from '~/lib/query-client';
8449
10025
 
8450
10026
  export function Providers({ children }: { children: React.ReactNode }) {
8451
- const [queryClient] = useState(
8452
- () =>
8453
- new QueryClient({
8454
- defaultOptions: {
8455
- queries: {
8456
- staleTime: 60 * 1000,
8457
- },
8458
- },
8459
- }),
8460
- );
10027
+ const queryClient = getQueryClient();
8461
10028
 
8462
10029
  return (
8463
10030
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
8464
10031
  );
8465
10032
  }
8466
10033
  `;
8467
- const apiIndexTs = `import { TypedFetcher } from '@geekmidas/client/fetcher';
8468
- import { createEndpointHooks } from '@geekmidas/client/endpoint-hooks';
8469
-
8470
- // TODO: Run 'gkm openapi' to generate typed paths from your API
8471
- // This is a placeholder that will be replaced by the generated openapi.ts
8472
- interface paths {
8473
- '/health': {
8474
- get: {
8475
- responses: {
8476
- 200: {
8477
- content: {
8478
- 'application/json': { status: string; timestamp: string };
8479
- };
8480
- };
8481
- };
8482
- };
8483
- };
8484
- '/users': {
8485
- get: {
8486
- responses: {
8487
- 200: {
8488
- content: {
8489
- 'application/json': { users: Array<{ id: string; name: string }> };
8490
- };
8491
- };
8492
- };
8493
- };
8494
- };
8495
- }
8496
-
8497
- const baseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
8498
-
8499
- const fetcher = new TypedFetcher<paths>({ baseURL });
8500
-
8501
- const hooks = createEndpointHooks<paths>(fetcher.request.bind(fetcher));
10034
+ const apiIndexTs = `import { createApi } from './openapi';
10035
+ import { getQueryClient } from '~/lib/query-client';
8502
10036
 
8503
- export const api = Object.assign(fetcher.request.bind(fetcher), hooks);
10037
+ export const api = createApi({
10038
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
10039
+ queryClient: getQueryClient(),
10040
+ });
10041
+ `;
10042
+ const globalsCss = `@import '${uiPackage}/styles';
8504
10043
  `;
8505
10044
  const layoutTsx = `import type { Metadata } from 'next';
8506
10045
  import { Providers } from './providers';
10046
+ import './globals.css';
8507
10047
 
8508
10048
  export const metadata: Metadata = {
8509
10049
  title: '${options.name}',
@@ -8524,42 +10064,59 @@ export default function RootLayout({
8524
10064
  );
8525
10065
  }
8526
10066
  `;
8527
- const pageTsx = `import { api } from '@/api';
10067
+ const pageTsx = `import { api } from '~/api';
10068
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '${uiPackage}/components';
8528
10069
 
8529
10070
  export default async function Home() {
8530
10071
  // Type-safe API call using the generated client
8531
10072
  const health = await api('GET /health').catch(() => null);
8532
10073
 
8533
10074
  return (
8534
- <main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
8535
- <h1>Welcome to ${options.name}</h1>
8536
-
8537
- <section style={{ marginTop: '2rem' }}>
8538
- <h2>API Status</h2>
8539
- {health ? (
8540
- <pre style={{ background: '#f0f0f0', padding: '1rem', borderRadius: '8px' }}>
8541
- {JSON.stringify(health, null, 2)}
8542
- </pre>
8543
- ) : (
8544
- <p>Unable to connect to API</p>
8545
- )}
8546
- </section>
8547
-
8548
- <section style={{ marginTop: '2rem' }}>
8549
- <h2>Next Steps</h2>
8550
- <ul>
8551
- <li>Run <code>gkm openapi</code> to generate typed API client</li>
8552
- <li>Edit <code>apps/web/src/app/page.tsx</code> to customize this page</li>
8553
- <li>Add API routes in <code>apps/api/src/endpoints/</code></li>
8554
- <li>Define shared schemas in <code>packages/models/src/</code></li>
8555
- </ul>
8556
- </section>
10075
+ <main className="min-h-screen bg-background p-8">
10076
+ <div className="mx-auto max-w-4xl space-y-8">
10077
+ <div className="space-y-2">
10078
+ <h1 className="text-4xl font-bold tracking-tight">Welcome to ${options.name}</h1>
10079
+ <p className="text-muted-foreground">Your fullstack application is ready.</p>
10080
+ </div>
10081
+
10082
+ <Card>
10083
+ <CardHeader>
10084
+ <CardTitle>API Status</CardTitle>
10085
+ <CardDescription>Connection to your backend API</CardDescription>
10086
+ </CardHeader>
10087
+ <CardContent>
10088
+ {health ? (
10089
+ <pre className="rounded-lg bg-muted p-4 text-sm">
10090
+ {JSON.stringify(health, null, 2)}
10091
+ </pre>
10092
+ ) : (
10093
+ <p className="text-destructive">Unable to connect to API</p>
10094
+ )}
10095
+ </CardContent>
10096
+ </Card>
10097
+
10098
+ <Card>
10099
+ <CardHeader>
10100
+ <CardTitle>Next Steps</CardTitle>
10101
+ <CardDescription>Get started with your project</CardDescription>
10102
+ </CardHeader>
10103
+ <CardContent className="space-y-4">
10104
+ <ul className="list-inside list-disc space-y-2 text-muted-foreground">
10105
+ <li>Run <code className="rounded bg-muted px-1">gkm openapi</code> to generate typed API client</li>
10106
+ <li>Edit <code className="rounded bg-muted px-1">apps/web/src/app/page.tsx</code> to customize this page</li>
10107
+ <li>Add API routes in <code className="rounded bg-muted px-1">apps/api/src/endpoints/</code></li>
10108
+ <li>Add UI components with <code className="rounded bg-muted px-1">npx shadcn@latest add</code> in packages/ui</li>
10109
+ </ul>
10110
+ <div className="flex gap-4">
10111
+ <Button>Get Started</Button>
10112
+ <Button variant="outline">Documentation</Button>
10113
+ </div>
10114
+ </CardContent>
10115
+ </Card>
10116
+ </div>
8557
10117
  </main>
8558
10118
  );
8559
10119
  }
8560
- `;
8561
- const envLocal = `# API URL for client-side requests
8562
- NEXT_PUBLIC_API_URL=http://localhost:3000
8563
10120
  `;
8564
10121
  const gitignore = `.next/
8565
10122
  node_modules/
@@ -8575,10 +10132,18 @@ node_modules/
8575
10132
  path: "apps/web/next.config.ts",
8576
10133
  content: nextConfig
8577
10134
  },
10135
+ {
10136
+ path: "apps/web/postcss.config.mjs",
10137
+ content: postcssConfig
10138
+ },
8578
10139
  {
8579
10140
  path: "apps/web/tsconfig.json",
8580
10141
  content: `${JSON.stringify(tsConfig, null, 2)}\n`
8581
10142
  },
10143
+ {
10144
+ path: "apps/web/src/app/globals.css",
10145
+ content: globalsCss
10146
+ },
8582
10147
  {
8583
10148
  path: "apps/web/src/app/layout.tsx",
8584
10149
  content: layoutTsx
@@ -8592,12 +10157,16 @@ node_modules/
8592
10157
  content: pageTsx
8593
10158
  },
8594
10159
  {
8595
- path: "apps/web/src/api/index.ts",
8596
- content: apiIndexTs
10160
+ path: "apps/web/src/lib/query-client.ts",
10161
+ content: queryClientTs
8597
10162
  },
8598
10163
  {
8599
- path: "apps/web/.env.local",
8600
- content: envLocal
10164
+ path: "apps/web/src/lib/auth-client.ts",
10165
+ content: authClientTs
10166
+ },
10167
+ {
10168
+ path: "apps/web/src/api/index.ts",
10169
+ content: apiIndexTs
8601
10170
  },
8602
10171
  {
8603
10172
  path: "apps/web/.gitignore",
@@ -8836,6 +10405,7 @@ async function initCommand(projectName, options = {}) {
8836
10405
  const rootFiles = baseTemplate ? [...generateMonorepoFiles(templateOptions, baseTemplate), ...generateModelsPackage(templateOptions)] : [];
8837
10406
  const webAppFiles = isFullstack ? generateWebAppFiles(templateOptions) : [];
8838
10407
  const authAppFiles = isFullstack ? generateAuthAppFiles(templateOptions) : [];
10408
+ const uiPackageFiles = isFullstack ? generateUiPackageFiles(templateOptions) : [];
8839
10409
  for (const { path, content } of rootFiles) {
8840
10410
  const fullPath = (0, node_path.join)(targetDir, path);
8841
10411
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
@@ -8861,6 +10431,11 @@ async function initCommand(projectName, options = {}) {
8861
10431
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
8862
10432
  await (0, node_fs_promises.writeFile)(fullPath, content);
8863
10433
  }
10434
+ for (const { path, content } of uiPackageFiles) {
10435
+ const fullPath = (0, node_path.join)(targetDir, path);
10436
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
10437
+ await (0, node_fs_promises.writeFile)(fullPath, content);
10438
+ }
8864
10439
  console.log("🔐 Initializing encrypted secrets...\n");
8865
10440
  const secretServices = [];
8866
10441
  if (services.db) secretServices.push("postgres");
@@ -8880,6 +10455,7 @@ async function initCommand(projectName, options = {}) {
8880
10455
  customSecrets[passwordKey] = app.password;
8881
10456
  }
8882
10457
  customSecrets.AUTH_PORT = "3002";
10458
+ customSecrets.AUTH_URL = "http://localhost:3002";
8883
10459
  customSecrets.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
8884
10460
  customSecrets.BETTER_AUTH_URL = "http://localhost:3002";
8885
10461
  customSecrets.BETTER_AUTH_TRUSTED_ORIGINS = "http://localhost:3000,http://localhost:3001";
@@ -8956,7 +10532,8 @@ function printNextSteps(projectName, options, pkgManager) {
8956
10532
  console.log(` │ └── web/ # Next.js frontend`);
8957
10533
  }
8958
10534
  console.log(` ├── packages/`);
8959
- console.log(` │ └── models/ # Shared Zod schemas`);
10535
+ console.log(` │ ├── models/ # Shared Zod schemas`);
10536
+ if (isFullstackTemplate(options.template)) console.log(` │ └── ui/ # Shared UI components`);
8960
10537
  console.log(` ├── .gkm/secrets/ # Encrypted secrets`);
8961
10538
  console.log(` ├── gkm.config.ts # Workspace config`);
8962
10539
  console.log(` └── turbo.json # Turbo config`);
@@ -8972,7 +10549,7 @@ function printNextSteps(projectName, options, pkgManager) {
8972
10549
  console.log(` ${getRunCommand(pkgManager, "deploy")}`);
8973
10550
  console.log("");
8974
10551
  }
8975
- console.log("📚 Documentation: https://docs.geekmidas.dev");
10552
+ console.log("📚 Documentation: https://geekmidas.github.io/toolbox/");
8976
10553
  console.log("");
8977
10554
  }
8978
10555
 
@@ -9524,6 +11101,46 @@ program.command("whoami").description("Show current authentication status").acti
9524
11101
  process.exit(1);
9525
11102
  }
9526
11103
  });
11104
+ program.command("state:pull").description("Pull deployment state from remote to local").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
11105
+ try {
11106
+ const globalOptions = program.opts();
11107
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11108
+ await statePullCommand(options);
11109
+ } catch (error) {
11110
+ console.error(error instanceof Error ? error.message : "Command failed");
11111
+ process.exit(1);
11112
+ }
11113
+ });
11114
+ program.command("state:push").description("Push deployment state from local to remote").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
11115
+ try {
11116
+ const globalOptions = program.opts();
11117
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11118
+ await statePushCommand(options);
11119
+ } catch (error) {
11120
+ console.error(error instanceof Error ? error.message : "Command failed");
11121
+ process.exit(1);
11122
+ }
11123
+ });
11124
+ 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) => {
11125
+ try {
11126
+ const globalOptions = program.opts();
11127
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11128
+ await stateShowCommand(options);
11129
+ } catch (error) {
11130
+ console.error(error instanceof Error ? error.message : "Command failed");
11131
+ process.exit(1);
11132
+ }
11133
+ });
11134
+ program.command("state:diff").description("Compare local and remote deployment state").requiredOption("--stage <stage>", "Deployment stage (e.g., production, staging)").action(async (options) => {
11135
+ try {
11136
+ const globalOptions = program.opts();
11137
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
11138
+ await stateDiffCommand(options);
11139
+ } catch (error) {
11140
+ console.error(error instanceof Error ? error.message : "Command failed");
11141
+ process.exit(1);
11142
+ }
11143
+ });
9527
11144
  program.parse();
9528
11145
 
9529
11146
  //#endregion