@geekmidas/cli 0.48.0 → 0.50.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 (80) hide show
  1. package/dist/deploy/sniffer-envkit-patch.cjs +27 -0
  2. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -0
  3. package/dist/deploy/sniffer-envkit-patch.d.cts +46 -0
  4. package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -0
  5. package/dist/deploy/sniffer-envkit-patch.d.mts +46 -0
  6. package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -0
  7. package/dist/deploy/sniffer-envkit-patch.mjs +20 -0
  8. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -0
  9. package/dist/deploy/sniffer-hooks.cjs +25 -0
  10. package/dist/deploy/sniffer-hooks.cjs.map +1 -0
  11. package/dist/deploy/sniffer-hooks.d.cts +27 -0
  12. package/dist/deploy/sniffer-hooks.d.cts.map +1 -0
  13. package/dist/deploy/sniffer-hooks.d.mts +27 -0
  14. package/dist/deploy/sniffer-hooks.d.mts.map +1 -0
  15. package/dist/deploy/sniffer-hooks.mjs +24 -0
  16. package/dist/deploy/sniffer-hooks.mjs.map +1 -0
  17. package/dist/deploy/sniffer-loader.cjs +16 -0
  18. package/dist/deploy/sniffer-loader.cjs.map +1 -0
  19. package/dist/deploy/sniffer-loader.d.cts +1 -0
  20. package/dist/deploy/sniffer-loader.d.mts +1 -0
  21. package/dist/deploy/sniffer-loader.mjs +15 -0
  22. package/dist/deploy/sniffer-loader.mjs.map +1 -0
  23. package/dist/deploy/sniffer-worker.cjs +42 -0
  24. package/dist/deploy/sniffer-worker.cjs.map +1 -0
  25. package/dist/deploy/sniffer-worker.d.cts +9 -0
  26. package/dist/deploy/sniffer-worker.d.cts.map +1 -0
  27. package/dist/deploy/sniffer-worker.d.mts +9 -0
  28. package/dist/deploy/sniffer-worker.d.mts.map +1 -0
  29. package/dist/deploy/sniffer-worker.mjs +41 -0
  30. package/dist/deploy/sniffer-worker.mjs.map +1 -0
  31. package/dist/{dokploy-api-DvzIDxTj.mjs → dokploy-api-94KzmTVf.mjs} +4 -4
  32. package/dist/dokploy-api-94KzmTVf.mjs.map +1 -0
  33. package/dist/dokploy-api-CItuaWTq.mjs +3 -0
  34. package/dist/dokploy-api-DBNE8MDt.cjs +3 -0
  35. package/dist/{dokploy-api-BDLu0qWi.cjs → dokploy-api-YD8WCQfW.cjs} +4 -4
  36. package/dist/dokploy-api-YD8WCQfW.cjs.map +1 -0
  37. package/dist/index.cjs +2415 -1893
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.mjs +2411 -1889
  40. package/dist/index.mjs.map +1 -1
  41. package/package.json +8 -6
  42. package/src/build/__tests__/handler-templates.spec.ts +947 -0
  43. package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +24 -0
  44. package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +24 -0
  45. package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +12 -0
  46. package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +14 -0
  47. package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +16 -0
  48. package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +10 -0
  49. package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +18 -0
  50. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +18 -0
  51. package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +16 -0
  52. package/src/deploy/__tests__/dns-verification.spec.ts +229 -0
  53. package/src/deploy/__tests__/dokploy-api.spec.ts +2 -3
  54. package/src/deploy/__tests__/domain.spec.ts +7 -3
  55. package/src/deploy/__tests__/env-resolver.spec.ts +469 -0
  56. package/src/deploy/__tests__/index.spec.ts +12 -12
  57. package/src/deploy/__tests__/secrets.spec.ts +4 -1
  58. package/src/deploy/__tests__/sniffer.spec.ts +326 -1
  59. package/src/deploy/__tests__/state.spec.ts +844 -0
  60. package/src/deploy/dns/hostinger-api.ts +4 -1
  61. package/src/deploy/dns/index.ts +113 -1
  62. package/src/deploy/docker.ts +1 -2
  63. package/src/deploy/dokploy-api.ts +18 -9
  64. package/src/deploy/domain.ts +5 -4
  65. package/src/deploy/env-resolver.ts +278 -0
  66. package/src/deploy/index.ts +525 -119
  67. package/src/deploy/secrets.ts +7 -2
  68. package/src/deploy/sniffer-envkit-patch.ts +59 -0
  69. package/src/deploy/sniffer-hooks.ts +57 -0
  70. package/src/deploy/sniffer-loader.ts +28 -0
  71. package/src/deploy/sniffer-worker.ts +74 -0
  72. package/src/deploy/sniffer.ts +170 -14
  73. package/src/deploy/state.ts +162 -1
  74. package/src/init/versions.ts +3 -3
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/tsdown.config.ts +5 -0
  77. package/dist/dokploy-api-BDLu0qWi.cjs.map +0 -1
  78. package/dist/dokploy-api-BN3V57z1.mjs +0 -3
  79. package/dist/dokploy-api-BdCKjFDA.cjs +0 -3
  80. package/dist/dokploy-api-DvzIDxTj.mjs.map +0 -1
@@ -1,6 +1,10 @@
1
1
  import { encryptSecrets } from '../secrets/encryption.js';
2
2
  import { toEmbeddableSecrets } from '../secrets/storage.js';
3
- import type { EmbeddableSecrets, EncryptedPayload, StageSecrets } from '../secrets/types.js';
3
+ import type {
4
+ EmbeddableSecrets,
5
+ EncryptedPayload,
6
+ StageSecrets,
7
+ } from '../secrets/types.js';
4
8
  import type { SniffedEnvironment } from './sniffer.js';
5
9
 
6
10
  /**
@@ -152,7 +156,8 @@ export function generateSecretsReport(
152
156
  ): SecretsReport {
153
157
  const appsWithSecrets: string[] = [];
154
158
  const appsWithoutSecrets: string[] = [];
155
- const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> = [];
159
+ const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> =
160
+ [];
156
161
 
157
162
  for (const [appName, sniffedEnv] of sniffedApps) {
158
163
  if (sniffedEnv.requiredEnvVars.length === 0) {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Patched @geekmidas/envkit module for entry app sniffing.
3
+ *
4
+ * This module re-exports the SnifferEnvironmentParser as EnvironmentParser,
5
+ * allowing entry apps to be imported while capturing their env var usage.
6
+ *
7
+ * The actual sniffer instance is stored in globalThis.__envSniffer
8
+ * so the worker script can retrieve the captured variables.
9
+ */
10
+
11
+ import { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
12
+
13
+ // Extend globalThis type for the sniffer instance
14
+ declare global {
15
+ // eslint-disable-next-line no-var
16
+ var __envSniffer: SnifferEnvironmentParser | undefined;
17
+ }
18
+
19
+ // Create a shared sniffer instance that will be used by all imports
20
+ // This is stored globally so the worker script can access it
21
+ if (!globalThis.__envSniffer) {
22
+ globalThis.__envSniffer = new SnifferEnvironmentParser();
23
+ }
24
+
25
+ // Type for the config parser returned by create()
26
+ interface ConfigParser<T> {
27
+ parse(): T;
28
+ safeParse(): { success: true; data: T } | { success: false; error: Error };
29
+ }
30
+
31
+ // Type for the env fetcher function
32
+ type EnvFetcher = (name: string) => {
33
+ string(): { parse(): string; safeParse(): unknown };
34
+ number(): { parse(): number; safeParse(): unknown };
35
+ boolean(): { parse(): boolean; safeParse(): unknown };
36
+ optional(): EnvFetcher;
37
+ default(value: unknown): EnvFetcher;
38
+ };
39
+
40
+ /**
41
+ * Patched EnvironmentParser that uses the global sniffer instance.
42
+ *
43
+ * This class wraps the global sniffer to maintain API compatibility
44
+ * with the real EnvironmentParser. The constructor accepts an env
45
+ * parameter for API compatibility but ignores it since we're sniffing.
46
+ */
47
+ class PatchedEnvironmentParser {
48
+ create<TReturn extends Record<string, unknown>>(
49
+ builder: (get: EnvFetcher) => TReturn,
50
+ ): ConfigParser<TReturn> {
51
+ return globalThis.__envSniffer!.create(builder) as ConfigParser<TReturn>;
52
+ }
53
+ }
54
+
55
+ // Export the patched parser as EnvironmentParser
56
+ export { PatchedEnvironmentParser as EnvironmentParser };
57
+
58
+ // Re-export other envkit exports that entry apps might use
59
+ export { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Module loader hooks for entry app sniffing.
3
+ *
4
+ * This module provides the resolve hook that intercepts '@geekmidas/envkit'
5
+ * imports and redirects them to the patched sniffer version.
6
+ *
7
+ * This file is registered via module.register() from sniffer-loader.ts.
8
+ */
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ // Resolve path to the patched envkit module
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ // Try .mjs first (production dist), then .ts (development)
19
+ const mjsPath = join(__dirname, 'sniffer-envkit-patch.mjs');
20
+ const tsPath = join(__dirname, 'sniffer-envkit-patch.ts');
21
+ const patchedEnvkitPath = existsSync(mjsPath) ? mjsPath : tsPath;
22
+
23
+ type ResolveContext = {
24
+ conditions: string[];
25
+ importAttributes: Record<string, string>;
26
+ parentURL?: string;
27
+ };
28
+
29
+ type ResolveResult = {
30
+ url: string;
31
+ shortCircuit?: boolean;
32
+ format?: string;
33
+ };
34
+
35
+ type NextResolve = (
36
+ specifier: string,
37
+ context: ResolveContext,
38
+ ) => Promise<ResolveResult>;
39
+
40
+ /**
41
+ * Resolve hook - intercepts module resolution for @geekmidas/envkit
42
+ */
43
+ export async function resolve(
44
+ specifier: string,
45
+ context: ResolveContext,
46
+ nextResolve: NextResolve,
47
+ ): Promise<ResolveResult> {
48
+ // Intercept @geekmidas/envkit imports
49
+ if (specifier === '@geekmidas/envkit') {
50
+ return {
51
+ url: `file://${patchedEnvkitPath}`,
52
+ shortCircuit: true,
53
+ };
54
+ }
55
+
56
+ return nextResolve(specifier, context);
57
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Node.js module loader registration for entry app sniffing.
3
+ *
4
+ * This module registers a custom loader hook that intercepts imports of
5
+ * '@geekmidas/envkit' and replaces the EnvironmentParser with
6
+ * SnifferEnvironmentParser, allowing us to capture which environment
7
+ * variables an entry app accesses.
8
+ *
9
+ * Usage:
10
+ * node --import tsx --import ./sniffer-loader.mjs ./sniffer-worker.mjs /path/to/entry.ts
11
+ */
12
+
13
+ import { existsSync } from 'node:fs';
14
+ import { register } from 'node:module';
15
+ import { dirname, join } from 'node:path';
16
+ import { fileURLToPath, pathToFileURL } from 'node:url';
17
+
18
+ // Resolve path to the loader hooks module
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ // Try .mjs first (production dist), then .ts (development)
23
+ const mjsPath = join(__dirname, 'sniffer-hooks.mjs');
24
+ const tsPath = join(__dirname, 'sniffer-hooks.ts');
25
+ const hooksPath = existsSync(mjsPath) ? mjsPath : tsPath;
26
+
27
+ // Register the loader hooks
28
+ register(pathToFileURL(hooksPath).href, import.meta.url);
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Subprocess worker for entry app sniffing.
3
+ *
4
+ * This script is executed in a subprocess with the sniffer-loader.ts
5
+ * registered, which intercepts @geekmidas/envkit imports.
6
+ *
7
+ * Usage:
8
+ * node --import tsx --import ./sniffer-loader.ts ./sniffer-worker.ts /path/to/entry.ts
9
+ *
10
+ * Output (JSON to stdout):
11
+ * { "envVars": ["PORT", "DATABASE_URL", ...], "error": null }
12
+ */
13
+
14
+ import { pathToFileURL } from 'node:url';
15
+ import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
16
+
17
+ // Extend globalThis type for the sniffer instance
18
+ declare global {
19
+ // eslint-disable-next-line no-var
20
+ var __envSniffer: SnifferEnvironmentParser | undefined;
21
+ }
22
+
23
+ // Get the entry file path from command line args
24
+ const entryPath = process.argv[2] as string | undefined;
25
+
26
+ if (!entryPath) {
27
+ console.log(
28
+ JSON.stringify({ envVars: [], error: 'No entry file path provided' }),
29
+ );
30
+ process.exit(1);
31
+ }
32
+
33
+ // entryPath is guaranteed to be defined after the check above
34
+ const validEntryPath: string = entryPath;
35
+
36
+ /**
37
+ * Main sniffing function
38
+ */
39
+ async function sniff(): Promise<void> {
40
+ let error: string | null = null;
41
+
42
+ try {
43
+ // Import the entry file - this triggers:
44
+ // 1. Entry imports config module
45
+ // 2. Config module imports @geekmidas/envkit (intercepted by loader)
46
+ // 3. Config creates EnvironmentParser (actually SnifferEnvironmentParser)
47
+ // 4. Config calls .create() and .parse()
48
+ // 5. Sniffer captures all accessed env var names
49
+ const entryUrl = pathToFileURL(validEntryPath).href;
50
+ await import(entryUrl);
51
+ } catch (e) {
52
+ // Entry may fail due to missing env vars or other runtime issues.
53
+ // This is expected - we still capture the env vars that were accessed.
54
+ error = e instanceof Error ? e.message : String(e);
55
+ }
56
+
57
+ // Retrieve captured env vars from the global sniffer
58
+ const sniffer = globalThis.__envSniffer;
59
+ const envVars = sniffer ? sniffer.getEnvironmentVariables() : [];
60
+
61
+ // Output result as JSON
62
+ console.log(JSON.stringify({ envVars, error }));
63
+ }
64
+
65
+ // Handle unhandled rejections (fire-and-forget promises)
66
+ process.on('unhandledRejection', () => {
67
+ // Silently ignore - we only care about env var capture
68
+ });
69
+
70
+ // Run the sniffer
71
+ sniff().catch((e) => {
72
+ console.log(JSON.stringify({ envVars: [], error: e.message || String(e) }));
73
+ process.exit(1);
74
+ });
@@ -1,8 +1,46 @@
1
- import { resolve } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
1
+ import { existsSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
5
  import type { SniffResult } from '@geekmidas/envkit/sniffer';
4
6
  import type { NormalizedAppConfig } from '../workspace/types.js';
5
7
 
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ /**
12
+ * Resolve the path to a sniffer helper file.
13
+ * Handles both dev (.ts with tsx) and production (.mjs from dist).
14
+ *
15
+ * In production: sniffer.ts is bundled into dist/index.mjs, but sniffer helper
16
+ * files are output to dist/deploy/ as standalone modules for subprocess loading.
17
+ *
18
+ * In development: All files are in src/deploy/ and loaded via tsx.
19
+ */
20
+ function resolveSnifferFile(baseName: string): string {
21
+ // Try deploy/ subdirectory first (production: bundled code is at dist/index.mjs,
22
+ // but sniffer files are at dist/deploy/)
23
+ const deployMjsPath = resolve(__dirname, 'deploy', `${baseName}.mjs`);
24
+ if (existsSync(deployMjsPath)) {
25
+ return deployMjsPath;
26
+ }
27
+
28
+ // Try same directory .mjs (production: if running from dist/deploy/ directly)
29
+ const mjsPath = resolve(__dirname, `${baseName}.mjs`);
30
+ if (existsSync(mjsPath)) {
31
+ return mjsPath;
32
+ }
33
+
34
+ // Try same directory .ts (development with tsx: all files in src/deploy/)
35
+ const tsPath = resolve(__dirname, `${baseName}.ts`);
36
+ if (existsSync(tsPath)) {
37
+ return tsPath;
38
+ }
39
+
40
+ // Fallback to .ts (will error if neither exists)
41
+ return tsPath;
42
+ }
43
+
6
44
  // Re-export SniffResult for consumers
7
45
  export type { SniffResult } from '@geekmidas/envkit/sniffer';
8
46
 
@@ -25,11 +63,12 @@ export interface SniffAppOptions {
25
63
  /**
26
64
  * Get required environment variables for an app.
27
65
  *
28
- * Detection strategy:
29
- * - Frontend apps: Returns empty (no server secrets)
30
- * - Apps with `requiredEnv`: Uses explicit list from config
31
- * - Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
32
- * - Apps with neither: Returns empty
66
+ * Detection strategy (in order):
67
+ * 1. Frontend apps: Returns empty (no server secrets)
68
+ * 2. Apps with `requiredEnv`: Uses explicit list from config
69
+ * 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
33
72
  *
34
73
  * This function handles "fire and forget" async operations gracefully,
35
74
  * capturing errors and unhandled rejections without failing the build.
@@ -48,17 +87,30 @@ export async function sniffAppEnvironment(
48
87
  ): Promise<SniffedEnvironment> {
49
88
  const { logWarnings = true } = options;
50
89
 
51
- // Frontend apps don't have server-side secrets
90
+ // 1. Frontend apps don't have server-side secrets
52
91
  if (app.type === 'frontend') {
53
92
  return { appName, requiredEnvVars: [] };
54
93
  }
55
94
 
56
- // Entry-based apps with explicit env list
95
+ // 2. Entry-based apps with explicit env list
57
96
  if (app.requiredEnv && app.requiredEnv.length > 0) {
58
97
  return { appName, requiredEnvVars: [...app.requiredEnv] };
59
98
  }
60
99
 
61
- // Apps with envParser - run sniffer to detect env var usage
100
+ // 3. Entry apps - import entry file in subprocess to trigger config.parse()
101
+ if (app.entry) {
102
+ const result = await sniffEntryFile(app.entry, app.path, workspacePath);
103
+
104
+ if (logWarnings && result.error) {
105
+ console.warn(
106
+ `[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`,
107
+ );
108
+ }
109
+
110
+ return { appName, requiredEnvVars: result.envVars };
111
+ }
112
+
113
+ // 4. Apps with envParser - run sniffer to detect env var usage
62
114
  if (app.envParser) {
63
115
  const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
64
116
 
@@ -79,10 +131,107 @@ export async function sniffAppEnvironment(
79
131
  return { appName, requiredEnvVars: result.envVars };
80
132
  }
81
133
 
82
- // No env detection method available
134
+ // 5. No env detection method available
83
135
  return { appName, requiredEnvVars: [] };
84
136
  }
85
137
 
138
+ /**
139
+ * Result from sniffing an entry file.
140
+ */
141
+ interface EntrySniffResult {
142
+ envVars: string[];
143
+ error?: Error;
144
+ }
145
+
146
+ /**
147
+ * Sniff an entry file by importing it in a subprocess.
148
+ *
149
+ * Entry apps call `config.parse()` at module load time. To capture which
150
+ * env vars are accessed, we:
151
+ * 1. Spawn a subprocess with a module loader hook
152
+ * 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
153
+ * with SnifferEnvironmentParser
154
+ * 3. Import the entry file (triggers config.parse())
155
+ * 4. Capture and return the accessed env var names
156
+ *
157
+ * This approach provides process isolation - each app is sniffed in its own
158
+ * subprocess, preventing module cache pollution.
159
+ *
160
+ * @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
161
+ * @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
162
+ * @param workspacePath - Absolute path to workspace root
163
+ * @returns EntrySniffResult with env vars and optional error
164
+ */
165
+ async function sniffEntryFile(
166
+ entryPath: string,
167
+ appPath: string,
168
+ workspacePath: string,
169
+ ): Promise<EntrySniffResult> {
170
+ const fullEntryPath = resolve(workspacePath, appPath, entryPath);
171
+ const loaderPath = resolveSnifferFile('sniffer-loader');
172
+ const workerPath = resolveSnifferFile('sniffer-worker');
173
+
174
+ return new Promise((resolvePromise) => {
175
+ const child = spawn(
176
+ 'node',
177
+ ['--import', loaderPath, workerPath, fullEntryPath],
178
+ {
179
+ cwd: resolve(workspacePath, appPath),
180
+ stdio: ['ignore', 'pipe', 'pipe'],
181
+ env: {
182
+ ...process.env,
183
+ // Ensure tsx is available for TypeScript entry files
184
+ NODE_OPTIONS: '--import tsx',
185
+ },
186
+ },
187
+ );
188
+
189
+ let stdout = '';
190
+ let stderr = '';
191
+
192
+ child.stdout.on('data', (data) => {
193
+ stdout += data.toString();
194
+ });
195
+
196
+ child.stderr.on('data', (data) => {
197
+ stderr += data.toString();
198
+ });
199
+
200
+ child.on('close', (code) => {
201
+ // Try to parse the JSON output from the worker
202
+ try {
203
+ // Find the last JSON object in stdout (worker may emit other output)
204
+ const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
205
+ if (jsonMatch) {
206
+ const result = JSON.parse(jsonMatch[0]);
207
+ resolvePromise({
208
+ envVars: result.envVars || [],
209
+ error: result.error ? new Error(result.error) : undefined,
210
+ });
211
+ return;
212
+ }
213
+ } catch {
214
+ // JSON parse failed
215
+ }
216
+
217
+ // If we couldn't parse the output, return empty with error info
218
+ resolvePromise({
219
+ envVars: [],
220
+ error: new Error(
221
+ `Failed to sniff entry file (exit code ${code}): ${stderr || stdout || 'No output'}`,
222
+ ),
223
+ });
224
+ });
225
+
226
+ child.on('error', (err) => {
227
+ resolvePromise({
228
+ envVars: [],
229
+ error: err,
230
+ });
231
+ });
232
+ });
233
+ }
234
+
86
235
  /**
87
236
  * Run the SnifferEnvironmentParser on an envParser module to detect
88
237
  * which environment variables it accesses.
@@ -118,7 +267,9 @@ async function sniffEnvParser(
118
267
  sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
119
268
  } catch (error) {
120
269
  const message = error instanceof Error ? error.message : String(error);
121
- console.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
270
+ console.warn(
271
+ `[sniffer] Failed to import SnifferEnvironmentParser: ${message}`,
272
+ );
122
273
  return { envVars: [], unhandledRejections: [] };
123
274
  }
124
275
 
@@ -169,7 +320,12 @@ export async function sniffAllApps(
169
320
  const results = new Map<string, SniffedEnvironment>();
170
321
 
171
322
  for (const [appName, app] of Object.entries(apps)) {
172
- const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
323
+ const sniffed = await sniffAppEnvironment(
324
+ app,
325
+ appName,
326
+ workspacePath,
327
+ options,
328
+ );
173
329
  results.set(appName, sniffed);
174
330
  }
175
331
 
@@ -177,4 +333,4 @@ export async function sniffAllApps(
177
333
  }
178
334
 
179
335
  // Export for testing
180
- export { sniffEnvParser as _sniffEnvParser };
336
+ export { sniffEnvParser as _sniffEnvParser, sniffEntryFile as _sniffEntryFile };
@@ -8,6 +8,22 @@
8
8
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
9
9
  import { join } from 'node:path';
10
10
 
11
+ /**
12
+ * Per-app database credentials
13
+ */
14
+ export interface AppDbCredentials {
15
+ dbUser: string;
16
+ dbPassword: string;
17
+ }
18
+
19
+ /**
20
+ * DNS verification record for a hostname
21
+ */
22
+ export interface DnsVerificationRecord {
23
+ serverIp: string;
24
+ verifiedAt: string;
25
+ }
26
+
11
27
  /**
12
28
  * State for a single stage deployment
13
29
  */
@@ -20,6 +36,12 @@ export interface DokployStageState {
20
36
  postgresId?: string;
21
37
  redisId?: string;
22
38
  };
39
+ /** Per-app database credentials for reuse on subsequent deploys */
40
+ appCredentials?: Record<string, AppDbCredentials>;
41
+ /** Auto-generated secrets per app (e.g., BETTER_AUTH_SECRET) */
42
+ generatedSecrets?: Record<string, Record<string, string>>;
43
+ /** DNS verification state per hostname */
44
+ dnsVerified?: Record<string, DnsVerificationRecord>;
23
45
  lastDeployedAt: string;
24
46
  }
25
47
 
@@ -134,7 +156,9 @@ export function setPostgresId(
134
156
  /**
135
157
  * Get redis ID from state
136
158
  */
137
- export function getRedisId(state: DokployStageState | null): string | undefined {
159
+ export function getRedisId(
160
+ state: DokployStageState | null,
161
+ ): string | undefined {
138
162
  return state?.services.redisId;
139
163
  }
140
164
 
@@ -144,3 +168,140 @@ export function getRedisId(state: DokployStageState | null): string | undefined
144
168
  export function setRedisId(state: DokployStageState, redisId: string): void {
145
169
  state.services.redisId = redisId;
146
170
  }
171
+
172
+ /**
173
+ * Get app credentials from state
174
+ */
175
+ export function getAppCredentials(
176
+ state: DokployStageState | null,
177
+ appName: string,
178
+ ): AppDbCredentials | undefined {
179
+ return state?.appCredentials?.[appName];
180
+ }
181
+
182
+ /**
183
+ * Set app credentials in state (mutates state)
184
+ */
185
+ export function setAppCredentials(
186
+ state: DokployStageState,
187
+ appName: string,
188
+ credentials: AppDbCredentials,
189
+ ): void {
190
+ if (!state.appCredentials) {
191
+ state.appCredentials = {};
192
+ }
193
+ state.appCredentials[appName] = credentials;
194
+ }
195
+
196
+ /**
197
+ * Get all app credentials from state
198
+ */
199
+ export function getAllAppCredentials(
200
+ state: DokployStageState | null,
201
+ ): Record<string, AppDbCredentials> {
202
+ return state?.appCredentials ?? {};
203
+ }
204
+
205
+ // ============================================================================
206
+ // Generated Secrets
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Get a generated secret for an app
211
+ */
212
+ export function getGeneratedSecret(
213
+ state: DokployStageState | null,
214
+ appName: string,
215
+ secretName: string,
216
+ ): string | undefined {
217
+ return state?.generatedSecrets?.[appName]?.[secretName];
218
+ }
219
+
220
+ /**
221
+ * Set a generated secret for an app (mutates state)
222
+ */
223
+ export function setGeneratedSecret(
224
+ state: DokployStageState,
225
+ appName: string,
226
+ secretName: string,
227
+ value: string,
228
+ ): void {
229
+ if (!state.generatedSecrets) {
230
+ state.generatedSecrets = {};
231
+ }
232
+ if (!state.generatedSecrets[appName]) {
233
+ state.generatedSecrets[appName] = {};
234
+ }
235
+ state.generatedSecrets[appName][secretName] = value;
236
+ }
237
+
238
+ /**
239
+ * Get all generated secrets for an app
240
+ */
241
+ export function getAppGeneratedSecrets(
242
+ state: DokployStageState | null,
243
+ appName: string,
244
+ ): Record<string, string> {
245
+ return state?.generatedSecrets?.[appName] ?? {};
246
+ }
247
+
248
+ /**
249
+ * Get all generated secrets from state
250
+ */
251
+ export function getAllGeneratedSecrets(
252
+ state: DokployStageState | null,
253
+ ): Record<string, Record<string, string>> {
254
+ return state?.generatedSecrets ?? {};
255
+ }
256
+
257
+ // ============================================================================
258
+ // DNS Verification
259
+ // ============================================================================
260
+
261
+ /**
262
+ * Get DNS verification record for a hostname
263
+ */
264
+ export function getDnsVerification(
265
+ state: DokployStageState | null,
266
+ hostname: string,
267
+ ): DnsVerificationRecord | undefined {
268
+ return state?.dnsVerified?.[hostname];
269
+ }
270
+
271
+ /**
272
+ * Set DNS verification record for a hostname (mutates state)
273
+ */
274
+ export function setDnsVerification(
275
+ state: DokployStageState,
276
+ hostname: string,
277
+ serverIp: string,
278
+ ): void {
279
+ if (!state.dnsVerified) {
280
+ state.dnsVerified = {};
281
+ }
282
+ state.dnsVerified[hostname] = {
283
+ serverIp,
284
+ verifiedAt: new Date().toISOString(),
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Check if a hostname is already verified with the given IP
290
+ */
291
+ export function isDnsVerified(
292
+ state: DokployStageState | null,
293
+ hostname: string,
294
+ serverIp: string,
295
+ ): boolean {
296
+ const record = state?.dnsVerified?.[hostname];
297
+ return record?.serverIp === serverIp;
298
+ }
299
+
300
+ /**
301
+ * Get all DNS verification records from state
302
+ */
303
+ export function getAllDnsVerifications(
304
+ state: DokployStageState | null,
305
+ ): Record<string, DnsVerificationRecord> {
306
+ return state?.dnsVerified ?? {};
307
+ }
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
32
32
  '@geekmidas/cli': CLI_VERSION,
33
33
  '@geekmidas/client': '~0.5.0',
34
34
  '@geekmidas/cloud': '~0.2.0',
35
- '@geekmidas/constructs': '~0.7.0',
35
+ '@geekmidas/constructs': '~0.8.0',
36
36
  '@geekmidas/db': '~0.3.0',
37
37
  '@geekmidas/emailkit': '~0.2.0',
38
- '@geekmidas/envkit': '~0.6.0',
38
+ '@geekmidas/envkit': '~0.7.0',
39
39
  '@geekmidas/errors': '~0.1.0',
40
40
  '@geekmidas/events': '~0.2.0',
41
41
  '@geekmidas/logger': '~0.4.0',
@@ -44,7 +44,7 @@ export const GEEKMIDAS_VERSIONS = {
44
44
  '@geekmidas/services': '~0.2.0',
45
45
  '@geekmidas/storage': '~0.1.0',
46
46
  '@geekmidas/studio': '~0.4.0',
47
- '@geekmidas/telescope': '~0.5.0',
47
+ '@geekmidas/telescope': '~0.6.0',
48
48
  '@geekmidas/testkit': '~0.6.0',
49
49
  };
50
50