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