@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
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Entry app fixture with async initialization.
3
+ * Tests that env vars are captured even with async code paths.
4
+ */
5
+ import { EnvironmentParser } from '@geekmidas/envkit';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ const _config = new EnvironmentParser(process.env as Record<string, string>)
9
+ .create((get) => ({
10
+ port: get('PORT').string().transform(Number),
11
+ databaseUrl: get('DATABASE_URL').string(),
12
+ }))
13
+ .parse();
14
+
15
+ // Simulate async initialization (fire-and-forget style)
16
+ const init = async () => {
17
+ await Promise.resolve();
18
+ // This would normally connect to database, etc.
19
+ };
20
+
21
+ // Fire-and-forget promise
22
+ init().catch(() => {
23
+ // Silently ignore - common pattern in entry apps
24
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Entry app fixture with nested configuration.
3
+ * Tests that nested env var access is tracked correctly.
4
+ */
5
+ import { EnvironmentParser } from '@geekmidas/envkit';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ const _config = new EnvironmentParser(process.env as Record<string, string>)
9
+ .create((get) => ({
10
+ server: {
11
+ port: get('PORT').string().transform(Number),
12
+ host: get('HOST').string(),
13
+ },
14
+ database: {
15
+ url: get('DATABASE_URL').string(),
16
+ poolSize: get('DB_POOL_SIZE').string().transform(Number),
17
+ },
18
+ auth: {
19
+ secret: get('BETTER_AUTH_SECRET').string(),
20
+ url: get('BETTER_AUTH_URL').string(),
21
+ trustedOrigins: get('BETTER_AUTH_TRUSTED_ORIGINS').string(),
22
+ },
23
+ }))
24
+ .parse();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Entry app fixture that doesn't use any environment variables.
3
+ * Tests the empty case.
4
+ */
5
+
6
+ // Simple app with hardcoded config
7
+ const config = {
8
+ port: 3000,
9
+ name: 'static-app',
10
+ };
11
+
12
+ export default config;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Simple entry app fixture that uses @geekmidas/envkit.
3
+ * Used for testing entry app sniffing via subprocess.
4
+ */
5
+ import { EnvironmentParser } from '@geekmidas/envkit';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ const _config = new EnvironmentParser(process.env as Record<string, string>)
9
+ .create((get) => ({
10
+ port: get('PORT').string().transform(Number),
11
+ databaseUrl: get('DATABASE_URL').string(),
12
+ redisUrl: get('REDIS_URL').string(),
13
+ }))
14
+ .parse();
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Entry app fixture that throws during initialization.
3
+ * Tests that env vars are still captured even when the entry throws.
4
+ */
5
+ import { EnvironmentParser } from '@geekmidas/envkit';
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ const _config = new EnvironmentParser(process.env as Record<string, string>)
9
+ .create((get) => ({
10
+ port: get('PORT').string().transform(Number),
11
+ apiKey: get('API_KEY').string(),
12
+ }))
13
+ .parse();
14
+
15
+ // Throw after env vars are accessed
16
+ throw new Error('Initialization failed: Missing required configuration');
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A fixture that exports a non-function for testing error handling.
3
+ */
4
+
5
+ // Export a string instead of a function
6
+ export const envParser = 'not a function';
7
+
8
+ // Default export is also not a function
9
+ const defaultValue = { notAFunction: true };
10
+ export default defaultValue;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * An envParser that returns a config with a parse() method.
3
+ */
4
+ import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
5
+
6
+ export function envParser(parser: SnifferEnvironmentParser) {
7
+ // Create a config that will fail on parse() due to missing env vars
8
+ // This tests the try/catch around result.parse()
9
+ const config = parser.create((get) => ({
10
+ port: get('PORT').string(),
11
+ databaseUrl: get('DATABASE_URL').string(),
12
+ apiKey: get('API_KEY').string(),
13
+ }));
14
+
15
+ return config;
16
+ }
17
+
18
+ export default envParser;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * An envParser that accesses some env vars then throws.
3
+ */
4
+ import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
5
+
6
+ export function envParser(parser: SnifferEnvironmentParser) {
7
+ const config = parser.create((get) => ({
8
+ port: get('PORT').string(),
9
+ apiKey: get('API_KEY').string(),
10
+ }));
11
+
12
+ // Throw after creating the parser but before returning
13
+ throw new Error('EnvParser initialization failed');
14
+
15
+ return config;
16
+ }
17
+
18
+ export default envParser;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A valid envParser fixture that returns a parseable config.
3
+ */
4
+ import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
5
+
6
+ export function envParser(parser: SnifferEnvironmentParser) {
7
+ return parser.create((get) => ({
8
+ port: get('PORT').string(),
9
+ database: {
10
+ url: get('DATABASE_URL').string(),
11
+ poolSize: get('DB_POOL_SIZE').string(),
12
+ },
13
+ }));
14
+ }
15
+
16
+ export default envParser;
@@ -0,0 +1,229 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createEmptyState, type DokployStageState } from '../state';
3
+
4
+ // Mock dns/promises lookup
5
+ vi.mock('node:dns/promises', () => ({
6
+ lookup: vi.fn(),
7
+ }));
8
+
9
+ // Import after mocking
10
+ import { lookup } from 'node:dns/promises';
11
+ import { verifyDnsRecords, resolveHostnameToIp } from '../dns/index';
12
+
13
+ describe('resolveHostnameToIp', () => {
14
+ const mockLookup = vi.mocked(lookup);
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ it('should resolve hostname to IPv4 address', async () => {
21
+ mockLookup.mockResolvedValue({ address: '1.2.3.4', family: 4 });
22
+
23
+ const ip = await resolveHostnameToIp('example.com');
24
+
25
+ expect(ip).toBe('1.2.3.4');
26
+ expect(mockLookup).toHaveBeenCalledWith('example.com', { family: 4 });
27
+ });
28
+
29
+ it('should throw error when resolution fails', async () => {
30
+ mockLookup.mockRejectedValue(new Error('NXDOMAIN'));
31
+
32
+ await expect(resolveHostnameToIp('invalid.example.com')).rejects.toThrow(
33
+ 'Failed to resolve IP for invalid.example.com: NXDOMAIN',
34
+ );
35
+ });
36
+ });
37
+
38
+ describe('verifyDnsRecords', () => {
39
+ const mockLookup = vi.mocked(lookup);
40
+ let state: DokployStageState;
41
+
42
+ // Suppress console.log during tests
43
+ const originalLog = console.log;
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ state = createEmptyState('production', 'env-123');
48
+ console.log = vi.fn();
49
+ });
50
+
51
+ afterEach(() => {
52
+ console.log = originalLog;
53
+ });
54
+
55
+ it('should verify DNS records that resolve correctly', async () => {
56
+ mockLookup.mockResolvedValue({ address: '1.2.3.4', family: 4 });
57
+
58
+ const appHostnames = new Map([
59
+ ['api', 'api.example.com'],
60
+ ['auth', 'auth.example.com'],
61
+ ]);
62
+
63
+ const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
64
+
65
+ expect(results).toHaveLength(2);
66
+ expect(results[0]).toMatchObject({
67
+ hostname: 'api.example.com',
68
+ appName: 'api',
69
+ verified: true,
70
+ resolvedIp: '1.2.3.4',
71
+ expectedIp: '1.2.3.4',
72
+ });
73
+ expect(results[1]).toMatchObject({
74
+ hostname: 'auth.example.com',
75
+ appName: 'auth',
76
+ verified: true,
77
+ resolvedIp: '1.2.3.4',
78
+ expectedIp: '1.2.3.4',
79
+ });
80
+
81
+ // Should have stored verification in state
82
+ expect(state.dnsVerified).toBeDefined();
83
+ expect(state.dnsVerified!['api.example.com']?.serverIp).toBe('1.2.3.4');
84
+ expect(state.dnsVerified!['auth.example.com']?.serverIp).toBe('1.2.3.4');
85
+ });
86
+
87
+ it('should skip verification for already-verified hostnames', async () => {
88
+ // Pre-populate state with verified hostname
89
+ state.dnsVerified = {
90
+ 'api.example.com': {
91
+ serverIp: '1.2.3.4',
92
+ verifiedAt: '2024-01-01T00:00:00.000Z',
93
+ },
94
+ };
95
+
96
+ const appHostnames = new Map([['api', 'api.example.com']]);
97
+
98
+ const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
99
+
100
+ expect(results).toHaveLength(1);
101
+ expect(results[0]).toMatchObject({
102
+ hostname: 'api.example.com',
103
+ appName: 'api',
104
+ verified: true,
105
+ skipped: true,
106
+ expectedIp: '1.2.3.4',
107
+ });
108
+
109
+ // Should NOT have called lookup for cached result
110
+ expect(mockLookup).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('should re-verify when server IP changes', async () => {
114
+ // Pre-populate state with different server IP
115
+ state.dnsVerified = {
116
+ 'api.example.com': {
117
+ serverIp: '9.9.9.9',
118
+ verifiedAt: '2024-01-01T00:00:00.000Z',
119
+ },
120
+ };
121
+
122
+ mockLookup.mockResolvedValue({ address: '1.2.3.4', family: 4 });
123
+
124
+ const appHostnames = new Map([['api', 'api.example.com']]);
125
+
126
+ const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
127
+
128
+ expect(results).toHaveLength(1);
129
+ expect(results[0]).toMatchObject({
130
+ hostname: 'api.example.com',
131
+ appName: 'api',
132
+ verified: true,
133
+ resolvedIp: '1.2.3.4',
134
+ });
135
+ // Should NOT be skipped (it was re-verified)
136
+ expect(results[0]?.skipped).toBeUndefined();
137
+
138
+ // Should have called lookup since IP changed
139
+ expect(mockLookup).toHaveBeenCalledTimes(1);
140
+
141
+ // Should have updated verification
142
+ expect(state.dnsVerified!['api.example.com']?.serverIp).toBe('1.2.3.4');
143
+ });
144
+
145
+ it('should handle DNS resolution failure gracefully', async () => {
146
+ mockLookup.mockRejectedValue(new Error('NXDOMAIN'));
147
+
148
+ const appHostnames = new Map([['api', 'api.example.com']]);
149
+
150
+ const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
151
+
152
+ expect(results).toHaveLength(1);
153
+ expect(results[0]).toMatchObject({
154
+ hostname: 'api.example.com',
155
+ appName: 'api',
156
+ verified: false,
157
+ expectedIp: '1.2.3.4',
158
+ error: expect.stringContaining('NXDOMAIN'),
159
+ });
160
+
161
+ // Should NOT have stored verification for failed lookup
162
+ expect(state.dnsVerified).toBeUndefined();
163
+ });
164
+
165
+ it('should detect when DNS resolves to wrong IP', async () => {
166
+ mockLookup.mockResolvedValue({ address: '9.9.9.9', family: 4 });
167
+
168
+ const appHostnames = new Map([['api', 'api.example.com']]);
169
+
170
+ const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
171
+
172
+ expect(results).toHaveLength(1);
173
+ expect(results[0]).toMatchObject({
174
+ hostname: 'api.example.com',
175
+ appName: 'api',
176
+ verified: false,
177
+ resolvedIp: '9.9.9.9',
178
+ expectedIp: '1.2.3.4',
179
+ });
180
+
181
+ // Should NOT have stored verification for wrong IP
182
+ expect(state.dnsVerified).toBeUndefined();
183
+ });
184
+
185
+ it('should handle mix of verified, cached, and pending hostnames', async () => {
186
+ // Pre-populate state with one verified hostname
187
+ state.dnsVerified = {
188
+ 'cached.example.com': {
189
+ serverIp: '1.2.3.4',
190
+ verifiedAt: '2024-01-01T00:00:00.000Z',
191
+ },
192
+ };
193
+
194
+ mockLookup
195
+ .mockResolvedValueOnce({ address: '1.2.3.4', family: 4 })
196
+ .mockRejectedValueOnce(new Error('NXDOMAIN'));
197
+
198
+ const appHostnames = new Map([
199
+ ['cached', 'cached.example.com'],
200
+ ['new-verified', 'new.example.com'],
201
+ ['pending', 'pending.example.com'],
202
+ ]);
203
+
204
+ const results = await verifyDnsRecords(appHostnames, '1.2.3.4', state);
205
+
206
+ expect(results).toHaveLength(3);
207
+
208
+ // Cached should be skipped
209
+ expect(results[0]).toMatchObject({
210
+ hostname: 'cached.example.com',
211
+ verified: true,
212
+ skipped: true,
213
+ });
214
+
215
+ // New should be verified
216
+ expect(results[1]).toMatchObject({
217
+ hostname: 'new.example.com',
218
+ verified: true,
219
+ resolvedIp: '1.2.3.4',
220
+ });
221
+
222
+ // Pending should fail
223
+ expect(results[2]).toMatchObject({
224
+ hostname: 'pending.example.com',
225
+ verified: false,
226
+ error: expect.stringContaining('NXDOMAIN'),
227
+ });
228
+ });
229
+ });
@@ -544,9 +544,8 @@ describe('DokployApi', () => {
544
544
  projectId: 'proj_1',
545
545
  environmentId: 'env_1',
546
546
  appName: 'mydb',
547
- databaseName: 'app',
548
547
  databaseUser: 'postgres',
549
- dockerImage: 'postgres:16-alpine',
548
+ dockerImage: 'postgres:18',
550
549
  });
551
550
  });
552
551
 
@@ -632,7 +631,7 @@ describe('DokployApi', () => {
632
631
  projectId: 'proj_1',
633
632
  environmentId: 'env_1',
634
633
  appName: 'mycache',
635
- dockerImage: 'redis:7-alpine',
634
+ dockerImage: 'redis:8',
636
635
  });
637
636
  });
638
637
 
@@ -100,9 +100,7 @@ describe('resolveHost', () => {
100
100
  });
101
101
 
102
102
  describe('isMainFrontendApp', () => {
103
- const createApp = (
104
- type: 'backend' | 'frontend',
105
- ): NormalizedAppConfig => ({
103
+ const createApp = (type: 'backend' | 'frontend'): NormalizedAppConfig => ({
106
104
  type,
107
105
  path: 'apps/test',
108
106
  port: 3000,
@@ -145,6 +143,12 @@ describe('isMainFrontendApp', () => {
145
143
  };
146
144
  expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
147
145
  });
146
+
147
+ it('should return false when no frontend apps in allApps (edge case)', () => {
148
+ // Edge case: checking a frontend app against an empty allApps
149
+ const frontendApp = createApp('frontend');
150
+ expect(isMainFrontendApp('myapp', frontendApp, {})).toBe(false);
151
+ });
148
152
  });
149
153
 
150
154
  describe('generatePublicUrlBuildArgs', () => {