@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
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Subprocess worker for route-based app sniffing.
3
+ *
4
+ * This script is executed in a subprocess with tsx loader,
5
+ * which handles TypeScript compilation and path alias resolution.
6
+ *
7
+ * Usage:
8
+ * node --import tsx ./sniffer-routes-worker.ts "/path/to/app" "./src/endpoints/**\/*.ts"
9
+ *
10
+ * Output (JSON to stdout):
11
+ * { "envVars": ["PORT", "DATABASE_URL", ...], "error": null }
12
+ */
13
+
14
+ import type { Construct } from '@geekmidas/constructs';
15
+ import { Cron } from '@geekmidas/constructs/crons';
16
+ import { Endpoint } from '@geekmidas/constructs/endpoints';
17
+ import { Function as GkmFunction } from '@geekmidas/constructs/functions';
18
+ import { Subscriber } from '@geekmidas/constructs/subscribers';
19
+ import fg from 'fast-glob';
20
+
21
+ // Get args from command line
22
+ const appPathArg = process.argv[2];
23
+ const routesPatternArg = process.argv[3];
24
+
25
+ if (!appPathArg || !routesPatternArg) {
26
+ console.log(
27
+ JSON.stringify({
28
+ envVars: [],
29
+ error: 'App path and routes pattern are required',
30
+ }),
31
+ );
32
+ process.exit(1);
33
+ }
34
+
35
+ // After validation, these are guaranteed to be strings
36
+ const appPath: string = appPathArg;
37
+ const routesPattern: string = routesPatternArg;
38
+
39
+ /**
40
+ * Check if a value is a gkm construct.
41
+ */
42
+ function isConstruct(value: unknown): value is Construct {
43
+ return (
44
+ Endpoint.isEndpoint(value) ||
45
+ GkmFunction.isFunction(value) ||
46
+ Cron.isCron(value) ||
47
+ Subscriber.isSubscriber(value)
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Main sniffing function
53
+ */
54
+ async function sniff(): Promise<void> {
55
+ const envVars = new Set<string>();
56
+ let error: string | null = null;
57
+
58
+ try {
59
+ // Find all route files matching the pattern
60
+ const files = await fg(routesPattern, {
61
+ cwd: appPath,
62
+ absolute: true,
63
+ });
64
+
65
+ // Import each route file and find constructs
66
+ for (const file of files) {
67
+ try {
68
+ const module = await import(file);
69
+
70
+ // Check all exports for constructs
71
+ for (const [, exportValue] of Object.entries(module)) {
72
+ if (isConstruct(exportValue)) {
73
+ try {
74
+ const constructEnvVars = await exportValue.getEnvironment();
75
+ constructEnvVars.forEach((v) => envVars.add(v));
76
+ } catch {
77
+ // Individual construct may fail, continue with others
78
+ }
79
+ }
80
+ }
81
+ } catch (e) {
82
+ // Log import errors but continue with other files
83
+ const msg = e instanceof Error ? e.message : String(e);
84
+ console.error(`[sniffer] Failed to import ${file}: ${msg}`);
85
+ }
86
+ }
87
+ } catch (e) {
88
+ error = e instanceof Error ? e.message : String(e);
89
+ }
90
+
91
+ // Output result as JSON (last line of stdout)
92
+ console.log(JSON.stringify({ envVars: Array.from(envVars).sort(), error }));
93
+ }
94
+
95
+ // Handle unhandled rejections
96
+ process.on('unhandledRejection', () => {
97
+ // Silently ignore - we only care about env var capture
98
+ });
99
+
100
+ // Run the sniffer
101
+ sniff().catch((e) => {
102
+ console.log(JSON.stringify({ envVars: [], error: e.message || String(e) }));
103
+ process.exit(1);
104
+ });
@@ -1,5 +1,6 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import { dirname, resolve } from 'node:path';
4
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import type { SniffResult } from '@geekmidas/envkit/sniffer';
@@ -8,6 +9,15 @@ import type { NormalizedAppConfig } from '../workspace/types.js';
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
10
11
 
12
+ /**
13
+ * Resolve the tsx package path from the CLI package's dependencies.
14
+ * This ensures tsx is available regardless of whether the target project has it installed.
15
+ */
16
+ function resolveTsxPath(): string {
17
+ const require = createRequire(import.meta.url);
18
+ return require.resolve('tsx');
19
+ }
20
+
11
21
  /**
12
22
  * Resolve the path to a sniffer helper file.
13
23
  * Handles both dev (.ts with tsx) and production (.mjs from dist).
@@ -67,8 +77,9 @@ export interface SniffAppOptions {
67
77
  * 1. Frontend apps: Returns empty (no server secrets)
68
78
  * 2. Apps with `requiredEnv`: Uses explicit list from config
69
79
  * 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
70
- * 4. Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
71
- * 5. Apps with neither: Returns empty
80
+ * 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
81
+ * 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
82
+ * 6. Apps with neither: Returns empty
72
83
  *
73
84
  * This function handles "fire and forget" async operations gracefully,
74
85
  * capturing errors and unhandled rejections without failing the build.
@@ -110,7 +121,20 @@ export async function sniffAppEnvironment(
110
121
  return { appName, requiredEnvVars: result.envVars };
111
122
  }
112
123
 
113
- // 4. Apps with envParser - run sniffer to detect env var usage
124
+ // 4. Route-based apps - load routes and call getEnvironment() on each construct
125
+ if (app.routes) {
126
+ const result = await sniffRouteFiles(app.routes, app.path, workspacePath);
127
+
128
+ if (logWarnings && result.error) {
129
+ console.warn(
130
+ `[sniffer] ${appName}: Route sniffing threw error (env vars still captured): ${result.error.message}`,
131
+ );
132
+ }
133
+
134
+ return { appName, requiredEnvVars: result.envVars };
135
+ }
136
+
137
+ // 5. Apps with envParser but no routes - run sniffer to detect env var usage
114
138
  if (app.envParser) {
115
139
  const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
116
140
 
@@ -232,6 +256,103 @@ async function sniffEntryFile(
232
256
  });
233
257
  }
234
258
 
259
+ /**
260
+ * Sniff route files by loading constructs and calling getEnvironment().
261
+ *
262
+ * Route-based apps have endpoints, functions, crons, and subscribers that
263
+ * use services. Each service's register() method accesses environment variables.
264
+ *
265
+ * This runs in a subprocess with tsx loader to properly handle TypeScript
266
+ * compilation and path alias resolution (e.g., `src/...` imports).
267
+ *
268
+ * @param routes - Glob pattern(s) for route files
269
+ * @param appPath - The app's path relative to workspace (e.g., 'apps/api')
270
+ * @param workspacePath - Absolute path to workspace root
271
+ * @returns EntrySniffResult with env vars and optional error
272
+ */
273
+ async function sniffRouteFiles(
274
+ routes: string | string[],
275
+ appPath: string,
276
+ workspacePath: string,
277
+ ): Promise<EntrySniffResult> {
278
+ const fullAppPath = resolve(workspacePath, appPath);
279
+ const workerPath = resolveSnifferFile('sniffer-routes-worker');
280
+ const tsxPath = resolveTsxPath();
281
+
282
+ // Convert array of patterns to first pattern (worker handles glob internally)
283
+ const routesArray = Array.isArray(routes) ? routes : [routes];
284
+ const pattern = routesArray[0];
285
+ if (!pattern) {
286
+ return { envVars: [], error: new Error('No route patterns provided') };
287
+ }
288
+
289
+ return new Promise((resolvePromise) => {
290
+ const child = spawn(
291
+ 'node',
292
+ ['--import', tsxPath, workerPath, fullAppPath, pattern],
293
+ {
294
+ cwd: fullAppPath,
295
+ stdio: ['ignore', 'pipe', 'pipe'],
296
+ env: {
297
+ ...process.env,
298
+ },
299
+ },
300
+ );
301
+
302
+ let stdout = '';
303
+ let stderr = '';
304
+
305
+ child.stdout.on('data', (data) => {
306
+ stdout += data.toString();
307
+ });
308
+
309
+ child.stderr.on('data', (data) => {
310
+ stderr += data.toString();
311
+ });
312
+
313
+ child.on('close', (code) => {
314
+ // Log any stderr output (import errors, etc.)
315
+ if (stderr) {
316
+ stderr
317
+ .split('\n')
318
+ .filter((line) => line.trim())
319
+ .forEach((line) => console.warn(line));
320
+ }
321
+
322
+ // Try to parse the JSON output from the worker
323
+ try {
324
+ // Find the last JSON object in stdout (worker may emit other output)
325
+ const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
326
+ if (jsonMatch) {
327
+ const result = JSON.parse(jsonMatch[0]);
328
+ resolvePromise({
329
+ envVars: result.envVars || [],
330
+ error: result.error ? new Error(result.error) : undefined,
331
+ });
332
+ return;
333
+ }
334
+ } catch {
335
+ // JSON parse failed
336
+ }
337
+
338
+ // If we couldn't parse the output, return empty with error info
339
+ resolvePromise({
340
+ envVars: [],
341
+ error: new Error(
342
+ `Failed to sniff route files (exit code ${code}): ${stderr || stdout || 'No output'}`,
343
+ ),
344
+ });
345
+ });
346
+
347
+ child.on('error', (err) => {
348
+ resolvePromise({
349
+ envVars: [],
350
+ error: err,
351
+ });
352
+ });
353
+ });
354
+ }
355
+
235
356
  /**
236
357
  * Run the SnifferEnvironmentParser on an envParser module to detect
237
358
  * which environment variables it accesses.
@@ -333,4 +454,8 @@ export async function sniffAllApps(
333
454
  }
334
455
 
335
456
  // Export for testing
336
- export { sniffEnvParser as _sniffEnvParser, sniffEntryFile as _sniffEntryFile };
457
+ export {
458
+ sniffEnvParser as _sniffEnvParser,
459
+ sniffEntryFile as _sniffEntryFile,
460
+ sniffRouteFiles as _sniffRouteFiles,
461
+ };
@@ -0,0 +1,274 @@
1
+ /**
2
+ * State Management CLI Commands
3
+ *
4
+ * Commands for managing deployment state across local and remote providers.
5
+ */
6
+
7
+ import { loadWorkspaceConfig } from '../config';
8
+ import { CachedStateProvider } from './CachedStateProvider';
9
+ import { createStateProvider } from './StateProvider';
10
+ import type { DokployStageState } from './state';
11
+
12
+ export interface StateCommandOptions {
13
+ stage: string;
14
+ }
15
+
16
+ /**
17
+ * Pull state from remote to local.
18
+ * `gkm state:pull --stage=<stage>`
19
+ */
20
+ export async function statePullCommand(
21
+ options: StateCommandOptions,
22
+ ): Promise<void> {
23
+ const { workspace } = await loadWorkspaceConfig();
24
+
25
+ if (!workspace.state || workspace.state.provider === 'local') {
26
+ console.error('No remote state provider configured.');
27
+ console.error('Add a remote provider in gkm.config.ts:');
28
+ console.error(' state: { provider: "ssm", region: "us-east-1" }');
29
+ process.exit(1);
30
+ }
31
+
32
+ const provider = await createStateProvider({
33
+ config: workspace.state,
34
+ workspaceRoot: workspace.root,
35
+ workspaceName: workspace.name,
36
+ });
37
+
38
+ if (!(provider instanceof CachedStateProvider)) {
39
+ console.error('State provider does not support pull operation.');
40
+ process.exit(1);
41
+ }
42
+
43
+ console.log(`Pulling state for stage: ${options.stage}...`);
44
+ const state = await provider.pull(options.stage);
45
+
46
+ if (state) {
47
+ console.log('State pulled successfully.');
48
+ printStateSummary(state);
49
+ } else {
50
+ console.log('No remote state found for this stage.');
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Push local state to remote.
56
+ * `gkm state:push --stage=<stage>`
57
+ */
58
+ export async function statePushCommand(
59
+ options: StateCommandOptions,
60
+ ): Promise<void> {
61
+ const { workspace } = await loadWorkspaceConfig();
62
+
63
+ if (!workspace.state || workspace.state.provider === 'local') {
64
+ console.error('No remote state provider configured.');
65
+ console.error('Add a remote provider in gkm.config.ts:');
66
+ console.error(' state: { provider: "ssm", region: "us-east-1" }');
67
+ process.exit(1);
68
+ }
69
+
70
+ const provider = await createStateProvider({
71
+ config: workspace.state,
72
+ workspaceRoot: workspace.root,
73
+ workspaceName: workspace.name,
74
+ });
75
+
76
+ if (!(provider instanceof CachedStateProvider)) {
77
+ console.error('State provider does not support push operation.');
78
+ process.exit(1);
79
+ }
80
+
81
+ console.log(`Pushing state for stage: ${options.stage}...`);
82
+ const state = await provider.push(options.stage);
83
+
84
+ if (state) {
85
+ console.log('State pushed successfully.');
86
+ printStateSummary(state);
87
+ } else {
88
+ console.log('No local state found for this stage.');
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Show current state.
94
+ * `gkm state:show --stage=<stage>`
95
+ */
96
+ export async function stateShowCommand(
97
+ options: StateCommandOptions & { json?: boolean },
98
+ ): Promise<void> {
99
+ const { workspace } = await loadWorkspaceConfig();
100
+
101
+ const provider = await createStateProvider({
102
+ config: workspace.state,
103
+ workspaceRoot: workspace.root,
104
+ workspaceName: workspace.name,
105
+ });
106
+
107
+ const state = await provider.read(options.stage);
108
+
109
+ if (!state) {
110
+ console.log(`No state found for stage: ${options.stage}`);
111
+ return;
112
+ }
113
+
114
+ if (options.json) {
115
+ console.log(JSON.stringify(state, null, 2));
116
+ } else {
117
+ printStateDetails(state);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Compare local and remote state.
123
+ * `gkm state:diff --stage=<stage>`
124
+ */
125
+ export async function stateDiffCommand(
126
+ options: StateCommandOptions,
127
+ ): Promise<void> {
128
+ const { workspace } = await loadWorkspaceConfig();
129
+
130
+ if (!workspace.state || workspace.state.provider === 'local') {
131
+ console.error('No remote state provider configured.');
132
+ console.error('Diff requires a remote provider to compare against.');
133
+ process.exit(1);
134
+ }
135
+
136
+ const provider = await createStateProvider({
137
+ config: workspace.state,
138
+ workspaceRoot: workspace.root,
139
+ workspaceName: workspace.name,
140
+ });
141
+
142
+ if (!(provider instanceof CachedStateProvider)) {
143
+ console.error('State provider does not support diff operation.');
144
+ process.exit(1);
145
+ }
146
+
147
+ console.log(`Comparing state for stage: ${options.stage}...\n`);
148
+ const { local, remote } = await provider.diff(options.stage);
149
+
150
+ if (!local && !remote) {
151
+ console.log('No state found (local or remote).');
152
+ return;
153
+ }
154
+
155
+ if (!local) {
156
+ console.log('Local: (none)');
157
+ } else {
158
+ console.log(`Local: Last deployed ${local.lastDeployedAt}`);
159
+ }
160
+
161
+ if (!remote) {
162
+ console.log('Remote: (none)');
163
+ } else {
164
+ console.log(`Remote: Last deployed ${remote.lastDeployedAt}`);
165
+ }
166
+
167
+ console.log('');
168
+
169
+ // Compare applications
170
+ const localApps = local?.applications ?? {};
171
+ const remoteApps = remote?.applications ?? {};
172
+ const allApps = new Set([
173
+ ...Object.keys(localApps),
174
+ ...Object.keys(remoteApps),
175
+ ]);
176
+
177
+ if (allApps.size > 0) {
178
+ console.log('Applications:');
179
+ for (const app of allApps) {
180
+ const localId = localApps[app];
181
+ const remoteId = remoteApps[app];
182
+
183
+ if (localId === remoteId) {
184
+ console.log(` ${app}: ${localId ?? '(none)'}`);
185
+ } else if (!localId) {
186
+ console.log(` ${app}: (none) -> ${remoteId} [REMOTE ONLY]`);
187
+ } else if (!remoteId) {
188
+ console.log(` ${app}: ${localId} -> (none) [LOCAL ONLY]`);
189
+ } else {
190
+ console.log(
191
+ ` ${app}: ${localId} (local) != ${remoteId} (remote) [MISMATCH]`,
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ // Compare services
198
+ const localServices = local?.services ?? {};
199
+ const remoteServices = remote?.services ?? {};
200
+
201
+ if (
202
+ Object.keys(localServices).length > 0 ||
203
+ Object.keys(remoteServices).length > 0
204
+ ) {
205
+ console.log('\nServices:');
206
+ const serviceKeys = new Set([
207
+ ...Object.keys(localServices),
208
+ ...Object.keys(remoteServices),
209
+ ]);
210
+
211
+ for (const key of serviceKeys) {
212
+ const localVal = localServices[key as keyof typeof localServices];
213
+ const remoteVal = remoteServices[key as keyof typeof remoteServices];
214
+
215
+ if (localVal === remoteVal) {
216
+ console.log(` ${key}: ${localVal ?? '(none)'}`);
217
+ } else {
218
+ console.log(
219
+ ` ${key}: ${localVal ?? '(none)'} (local) != ${remoteVal ?? '(none)'} (remote)`,
220
+ );
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ function printStateSummary(state: DokployStageState): void {
227
+ const appCount = Object.keys(state.applications).length;
228
+ const hasPostgres = !!state.services.postgresId;
229
+ const hasRedis = !!state.services.redisId;
230
+
231
+ console.log(` Stage: ${state.stage}`);
232
+ console.log(` Applications: ${appCount}`);
233
+ console.log(` Postgres: ${hasPostgres ? 'configured' : 'none'}`);
234
+ console.log(` Redis: ${hasRedis ? 'configured' : 'none'}`);
235
+ console.log(` Last deployed: ${state.lastDeployedAt}`);
236
+ }
237
+
238
+ function printStateDetails(state: DokployStageState): void {
239
+ console.log(`Stage: ${state.stage}`);
240
+ console.log(`Environment ID: ${state.environmentId}`);
241
+ console.log(`Last Deployed: ${state.lastDeployedAt}`);
242
+ console.log('');
243
+
244
+ console.log('Applications:');
245
+ const apps = Object.entries(state.applications);
246
+ if (apps.length === 0) {
247
+ console.log(' (none)');
248
+ } else {
249
+ for (const [name, id] of apps) {
250
+ console.log(` ${name}: ${id}`);
251
+ }
252
+ }
253
+ console.log('');
254
+
255
+ console.log('Services:');
256
+ if (!state.services.postgresId && !state.services.redisId) {
257
+ console.log(' (none)');
258
+ } else {
259
+ if (state.services.postgresId) {
260
+ console.log(` Postgres: ${state.services.postgresId}`);
261
+ }
262
+ if (state.services.redisId) {
263
+ console.log(` Redis: ${state.services.redisId}`);
264
+ }
265
+ }
266
+
267
+ if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
268
+ console.log('');
269
+ console.log('DNS Verified:');
270
+ for (const [hostname, info] of Object.entries(state.dnsVerified)) {
271
+ console.log(` ${hostname}: ${info.serverIp} (${info.verifiedAt})`);
272
+ }
273
+ }
274
+ }
@@ -8,7 +8,10 @@ describe('findSecretsRoot', () => {
8
8
  let testDir: string;
9
9
 
10
10
  beforeEach(async () => {
11
- testDir = join(tmpdir(), `gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
11
+ testDir = join(
12
+ tmpdir(),
13
+ `gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
14
+ );
12
15
  await mkdir(testDir, { recursive: true });
13
16
  });
14
17
 
@@ -61,7 +64,10 @@ describe('createEntryWrapper', () => {
61
64
  let testDir: string;
62
65
 
63
66
  beforeEach(async () => {
64
- testDir = join(tmpdir(), `gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
67
+ testDir = join(
68
+ tmpdir(),
69
+ `gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
70
+ );
65
71
  await mkdir(testDir, { recursive: true });
66
72
  });
67
73
 
@@ -1049,9 +1049,7 @@ describe('loadSecretsForApp', () => {
1049
1049
 
1050
1050
  const secrets = await loadSecretsForApp(testDir);
1051
1051
 
1052
- expect(secrets.DATABASE_URL).toBe(
1053
- 'postgresql://localhost/developmentdb',
1054
- );
1052
+ expect(secrets.DATABASE_URL).toBe('postgresql://localhost/developmentdb');
1055
1053
  });
1056
1054
 
1057
1055
  it('should return empty object if no secrets exist', async () => {
package/src/dev/index.ts CHANGED
@@ -340,7 +340,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
340
340
  secretsRoot = appConfig.workspaceRoot;
341
341
  workspaceAppName = appConfig.appName;
342
342
  workspaceAppPort = appConfig.app.port;
343
- logger.log(`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`);
343
+ logger.log(
344
+ `📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`,
345
+ );
344
346
 
345
347
  // Check if app has an entry point (non-gkm app like better-auth)
346
348
  if (appConfig.app.entry) {
@@ -1387,7 +1389,9 @@ export async function prepareEntryCredentials(options: {
1387
1389
  appName = appConfig.appName;
1388
1390
  } catch (error) {
1389
1391
  // Not in a workspace - use defaults
1390
- logger.log(`⚠️ Could not load workspace config: ${(error as Error).message}`);
1392
+ logger.log(
1393
+ `⚠️ Could not load workspace config: ${(error as Error).message}`,
1394
+ );
1391
1395
  secretsRoot = findSecretsRoot(cwd);
1392
1396
  appName = getAppNameFromCwd(cwd) ?? undefined;
1393
1397
  }
@@ -1457,7 +1461,9 @@ async function entryDevCommand(options: DevOptions): Promise<void> {
1457
1461
  logger.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
1458
1462
 
1459
1463
  if (Object.keys(credentials).length > 1) {
1460
- logger.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
1464
+ logger.log(
1465
+ `🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`,
1466
+ );
1461
1467
  }
1462
1468
 
1463
1469
  // Create wrapper entry that injects secrets before importing user's file
@@ -614,7 +614,9 @@ describe('docker templates', () => {
614
614
  const dockerfile = generateEntryDockerfile(baseOptions);
615
615
 
616
616
  expect(dockerfile).toContain('import { createRequire } from "module"');
617
- expect(dockerfile).toContain('const require = createRequire(import.meta.url)');
617
+ expect(dockerfile).toContain(
618
+ 'const require = createRequire(import.meta.url)',
619
+ );
618
620
  });
619
621
 
620
622
  it('should output to dist/index.mjs', () => {
@@ -293,7 +293,7 @@ WORKDIR /app
293
293
  COPY . .
294
294
 
295
295
  # Build production server using gkm
296
- RUN pnpm exec gkm build --provider server --production
296
+ RUN ${pm.exec} gkm build --provider server --production
297
297
 
298
298
  # Stage 3: Production
299
299
  FROM ${baseImage} AS runner
@@ -384,7 +384,7 @@ WORKDIR /app
384
384
  COPY --from=pruner /app/out/full/ ./
385
385
 
386
386
  # Build production server using gkm
387
- RUN pnpm exec gkm build --provider server --production
387
+ RUN ${pm.exec} gkm build --provider server --production
388
388
 
389
389
  # Stage 4: Production
390
390
  FROM ${baseImage} AS runner
@@ -756,7 +756,7 @@ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
756
756
  fi
757
757
 
758
758
  # Build production server using gkm
759
- RUN cd ${appPath} && pnpm exec gkm build --provider server --production
759
+ RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
760
760
 
761
761
  # Stage 4: Production
762
762
  FROM ${baseImage} AS runner