@geekmidas/cli 0.54.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +26 -5
  3. package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
  4. package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
  5. package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
  6. package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
  7. package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
  8. package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
  9. package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
  10. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
  11. package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
  12. package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
  13. package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
  14. package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
  15. package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
  16. package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
  17. package/dist/Route53Provider-CpRIqu69.cjs +157 -0
  18. package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
  19. package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
  20. package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
  21. package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
  22. package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
  23. package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
  24. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
  25. package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
  26. package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
  27. package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
  28. package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
  29. package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
  30. package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
  31. package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
  32. package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
  33. package/dist/config.cjs +2 -2
  34. package/dist/config.d.cts +1 -1
  35. package/dist/config.d.mts +2 -2
  36. package/dist/config.mjs +2 -2
  37. package/dist/credentials-C8DWtnMY.cjs +174 -0
  38. package/dist/credentials-C8DWtnMY.cjs.map +1 -0
  39. package/dist/credentials-DT1dSxIx.mjs +126 -0
  40. package/dist/credentials-DT1dSxIx.mjs.map +1 -0
  41. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  42. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  43. package/dist/deploy/sniffer-loader.cjs +1 -1
  44. package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
  45. package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
  46. package/dist/dokploy-api-CHa8G51l.mjs +3 -0
  47. package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
  48. package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
  49. package/dist/dokploy-api-CWc02yyg.cjs +3 -0
  50. package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
  51. package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
  52. package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
  53. package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
  54. package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  55. package/dist/encryption-UUmaWAmz.mjs +3 -0
  56. package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
  57. package/dist/index-B5rGIc4g.d.cts.map +1 -0
  58. package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
  59. package/dist/index-KFEbMIRa.d.mts.map +1 -0
  60. package/dist/index.cjs +2265 -658
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2242 -635
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
  65. package/dist/openapi-BMFmLnX6.mjs.map +1 -0
  66. package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
  67. package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
  68. package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
  69. package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
  70. package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
  71. package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
  72. package/dist/openapi-react-query.cjs +1 -1
  73. package/dist/openapi-react-query.mjs +1 -1
  74. package/dist/openapi.cjs +3 -3
  75. package/dist/openapi.d.cts +1 -1
  76. package/dist/openapi.d.mts +2 -2
  77. package/dist/openapi.mjs +3 -3
  78. package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
  79. package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
  80. package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
  81. package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
  82. package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
  83. package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
  84. package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
  85. package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
  86. package/dist/workspace/index.cjs +1 -1
  87. package/dist/workspace/index.d.cts +1 -1
  88. package/dist/workspace/index.d.mts +2 -2
  89. package/dist/workspace/index.mjs +1 -1
  90. package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
  91. package/dist/workspace-BFRUOOrh.cjs.map +1 -0
  92. package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
  93. package/dist/workspace-DAxG3_H2.mjs.map +1 -0
  94. package/package.json +14 -8
  95. package/scripts/sync-versions.ts +86 -0
  96. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  97. package/src/deploy/CachedStateProvider.ts +86 -0
  98. package/src/deploy/LocalStateProvider.ts +57 -0
  99. package/src/deploy/SSMStateProvider.ts +93 -0
  100. package/src/deploy/StateProvider.ts +171 -0
  101. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  102. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  103. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  104. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  105. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  106. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  107. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  108. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  109. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  110. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  111. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  112. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  113. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  114. package/src/deploy/__tests__/state.spec.ts +13 -5
  115. package/src/deploy/dns/DnsProvider.ts +163 -0
  116. package/src/deploy/dns/HostingerProvider.ts +100 -0
  117. package/src/deploy/dns/Route53Provider.ts +256 -0
  118. package/src/deploy/dns/index.ts +257 -165
  119. package/src/deploy/env-resolver.ts +12 -5
  120. package/src/deploy/index.ts +16 -13
  121. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  122. package/src/deploy/sniffer-routes-worker.ts +104 -0
  123. package/src/deploy/sniffer.ts +77 -55
  124. package/src/deploy/state-commands.ts +274 -0
  125. package/src/dev/__tests__/entry.spec.ts +8 -2
  126. package/src/dev/__tests__/index.spec.ts +1 -3
  127. package/src/dev/index.ts +9 -3
  128. package/src/docker/__tests__/templates.spec.ts +3 -1
  129. package/src/index.ts +88 -0
  130. package/src/init/__tests__/generators.spec.ts +273 -0
  131. package/src/init/__tests__/init.spec.ts +3 -3
  132. package/src/init/generators/auth.ts +1 -0
  133. package/src/init/generators/config.ts +2 -0
  134. package/src/init/generators/models.ts +6 -1
  135. package/src/init/generators/monorepo.ts +3 -0
  136. package/src/init/generators/ui.ts +1472 -0
  137. package/src/init/generators/web.ts +134 -87
  138. package/src/init/index.ts +22 -3
  139. package/src/init/templates/api.ts +109 -3
  140. package/src/init/versions.ts +25 -53
  141. package/src/openapi.ts +99 -13
  142. package/src/workspace/__tests__/schema.spec.ts +107 -0
  143. package/src/workspace/schema.ts +314 -4
  144. package/src/workspace/types.ts +22 -36
  145. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  146. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  147. package/dist/encryption-CQXBZGkt.mjs +0 -3
  148. package/dist/index-A70abJ1m.d.mts.map +0 -1
  149. package/dist/index-pOA56MWT.d.cts.map +0 -1
  150. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  151. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  152. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  153. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  154. package/tsconfig.tsbuildinfo +0 -1
@@ -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,29 +1,21 @@
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
- import type { Construct } from '@geekmidas/constructs';
6
- import { Endpoint } from '@geekmidas/constructs/endpoints';
7
- import { Function as GkmFunction } from '@geekmidas/constructs/functions';
8
- import { Cron } from '@geekmidas/constructs/crons';
9
- import { Subscriber } from '@geekmidas/constructs/subscribers';
10
6
  import type { SniffResult } from '@geekmidas/envkit/sniffer';
11
- import fg from 'fast-glob';
12
7
  import type { NormalizedAppConfig } from '../workspace/types.js';
13
8
 
14
9
  const __filename = fileURLToPath(import.meta.url);
15
10
  const __dirname = dirname(__filename);
16
11
 
17
12
  /**
18
- * Check if a value is a gkm construct (Endpoint, Function, Cron, or Subscriber).
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.
19
15
  */
20
- function isConstruct(value: unknown): value is Construct {
21
- return (
22
- Endpoint.isEndpoint(value) ||
23
- GkmFunction.isFunction(value) ||
24
- Cron.isCron(value) ||
25
- Subscriber.isSubscriber(value)
26
- );
16
+ function resolveTsxPath(): string {
17
+ const require = createRequire(import.meta.url);
18
+ return require.resolve('tsx');
27
19
  }
28
20
 
29
21
  /**
@@ -269,7 +261,9 @@ async function sniffEntryFile(
269
261
  *
270
262
  * Route-based apps have endpoints, functions, crons, and subscribers that
271
263
  * use services. Each service's register() method accesses environment variables.
272
- * This function mimics what the bundler does during build to capture those vars.
264
+ *
265
+ * This runs in a subprocess with tsx loader to properly handle TypeScript
266
+ * compilation and path alias resolution (e.g., `src/...` imports).
273
267
  *
274
268
  * @param routes - Glob pattern(s) for route files
275
269
  * @param appPath - The app's path relative to workspace (e.g., 'apps/api')
@@ -282,53 +276,81 @@ async function sniffRouteFiles(
282
276
  workspacePath: string,
283
277
  ): Promise<EntrySniffResult> {
284
278
  const fullAppPath = resolve(workspacePath, appPath);
285
- const patterns = Array.isArray(routes) ? routes : [routes];
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
+ }
286
288
 
287
- const envVars = new Set<string>();
288
- let error: Error | undefined;
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
+ );
289
301
 
290
- try {
291
- // Find all route files matching the patterns
292
- const files = await fg(patterns, {
293
- cwd: fullAppPath,
294
- absolute: true,
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();
295
311
  });
296
312
 
297
- // Import each file and find constructs
298
- for (const file of files) {
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
299
323
  try {
300
- const module = await import(file);
301
-
302
- // Check all exports for constructs
303
- for (const [, exportValue] of Object.entries(module)) {
304
- if (isConstruct(exportValue)) {
305
- // Call getEnvironment() to capture env vars from services
306
- try {
307
- const constructEnvVars = await exportValue.getEnvironment();
308
- constructEnvVars.forEach((v) => envVars.add(v));
309
- } catch (e) {
310
- // Individual construct may fail, continue with others
311
- console.warn(
312
- `[sniffer] Failed to get environment for construct in ${file}: ${e instanceof Error ? e.message : String(e)}`,
313
- );
314
- }
315
- }
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;
316
333
  }
317
- } catch (e) {
318
- // Individual file import may fail, continue with others
319
- console.warn(
320
- `[sniffer] Failed to import ${file}: ${e instanceof Error ? e.message : String(e)}`,
321
- );
334
+ } catch {
335
+ // JSON parse failed
322
336
  }
323
- }
324
- } catch (e) {
325
- error = e instanceof Error ? e : new Error(String(e));
326
- }
327
337
 
328
- return {
329
- envVars: Array.from(envVars).sort(),
330
- error,
331
- };
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
+ });
332
354
  }
333
355
 
334
356
  /**
@@ -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', () => {