@geekmidas/cli 0.12.0 → 0.14.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 (82) hide show
  1. package/dist/bundler-BjholBlA.cjs +131 -0
  2. package/dist/bundler-BjholBlA.cjs.map +1 -0
  3. package/dist/bundler-DWctKN1z.mjs +130 -0
  4. package/dist/bundler-DWctKN1z.mjs.map +1 -0
  5. package/dist/config.d.cts +1 -1
  6. package/dist/config.d.mts +1 -1
  7. package/dist/dokploy-api-B7KxOQr3.cjs +3 -0
  8. package/dist/dokploy-api-C7F9VykY.cjs +317 -0
  9. package/dist/dokploy-api-C7F9VykY.cjs.map +1 -0
  10. package/dist/dokploy-api-CaETb2L6.mjs +305 -0
  11. package/dist/dokploy-api-CaETb2L6.mjs.map +1 -0
  12. package/dist/dokploy-api-DHvfmWbi.mjs +3 -0
  13. package/dist/{encryption-Dyf_r1h-.cjs → encryption-D7Efcdi9.cjs} +1 -1
  14. package/dist/{encryption-Dyf_r1h-.cjs.map → encryption-D7Efcdi9.cjs.map} +1 -1
  15. package/dist/{encryption-C8H-38Yy.mjs → encryption-h4Nb6W-M.mjs} +1 -1
  16. package/dist/{encryption-C8H-38Yy.mjs.map → encryption-h4Nb6W-M.mjs.map} +1 -1
  17. package/dist/index.cjs +1520 -1136
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.mjs +1520 -1136
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/{openapi-Bt_1FDpT.cjs → openapi-C89hhkZC.cjs} +3 -3
  22. package/dist/{openapi-Bt_1FDpT.cjs.map → openapi-C89hhkZC.cjs.map} +1 -1
  23. package/dist/{openapi-BfFlOBCG.mjs → openapi-CZVcfxk-.mjs} +3 -3
  24. package/dist/{openapi-BfFlOBCG.mjs.map → openapi-CZVcfxk-.mjs.map} +1 -1
  25. package/dist/{openapi-react-query-B6XTeGqS.mjs → openapi-react-query-CM2_qlW9.mjs} +1 -1
  26. package/dist/{openapi-react-query-B6XTeGqS.mjs.map → openapi-react-query-CM2_qlW9.mjs.map} +1 -1
  27. package/dist/{openapi-react-query-B-sNWHFU.cjs → openapi-react-query-iKjfLzff.cjs} +1 -1
  28. package/dist/{openapi-react-query-B-sNWHFU.cjs.map → openapi-react-query-iKjfLzff.cjs.map} +1 -1
  29. package/dist/openapi-react-query.cjs +1 -1
  30. package/dist/openapi-react-query.mjs +1 -1
  31. package/dist/openapi.cjs +1 -1
  32. package/dist/openapi.d.cts +1 -1
  33. package/dist/openapi.d.mts +1 -1
  34. package/dist/openapi.mjs +1 -1
  35. package/dist/{storage-C9PU_30f.mjs → storage-BaOP55oq.mjs} +48 -2
  36. package/dist/storage-BaOP55oq.mjs.map +1 -0
  37. package/dist/{storage-BXoJvmv2.cjs → storage-Bn3K9Ccu.cjs} +59 -1
  38. package/dist/storage-Bn3K9Ccu.cjs.map +1 -0
  39. package/dist/storage-UfyTn7Zm.cjs +7 -0
  40. package/dist/storage-nkGIjeXt.mjs +3 -0
  41. package/dist/{types-BR0M2v_c.d.mts → types-BgaMXsUa.d.cts} +3 -1
  42. package/dist/{types-BR0M2v_c.d.mts.map → types-BgaMXsUa.d.cts.map} +1 -1
  43. package/dist/{types-BhkZc-vm.d.cts → types-iFk5ms7y.d.mts} +3 -1
  44. package/dist/{types-BhkZc-vm.d.cts.map → types-iFk5ms7y.d.mts.map} +1 -1
  45. package/package.json +4 -4
  46. package/src/auth/__tests__/credentials.spec.ts +127 -0
  47. package/src/auth/__tests__/index.spec.ts +69 -0
  48. package/src/auth/credentials.ts +33 -0
  49. package/src/auth/index.ts +57 -50
  50. package/src/build/__tests__/bundler.spec.ts +444 -0
  51. package/src/build/__tests__/endpoint-analyzer.spec.ts +623 -0
  52. package/src/build/__tests__/handler-templates.spec.ts +272 -0
  53. package/src/build/bundler.ts +126 -8
  54. package/src/build/index.ts +31 -0
  55. package/src/build/types.ts +6 -0
  56. package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
  57. package/src/deploy/__tests__/dokploy.spec.ts +196 -6
  58. package/src/deploy/__tests__/index.spec.ts +339 -0
  59. package/src/deploy/__tests__/init.spec.ts +147 -16
  60. package/src/deploy/docker.ts +32 -3
  61. package/src/deploy/dokploy-api.ts +581 -0
  62. package/src/deploy/dokploy.ts +66 -93
  63. package/src/deploy/index.ts +587 -32
  64. package/src/deploy/init.ts +192 -249
  65. package/src/deploy/types.ts +19 -1
  66. package/src/dev/__tests__/index.spec.ts +95 -0
  67. package/src/docker/__tests__/templates.spec.ts +144 -0
  68. package/src/docker/index.ts +96 -6
  69. package/src/docker/templates.ts +114 -27
  70. package/src/generators/EndpointGenerator.ts +2 -2
  71. package/src/index.ts +34 -13
  72. package/src/secrets/__tests__/storage.spec.ts +208 -0
  73. package/src/secrets/storage.ts +73 -0
  74. package/src/types.ts +2 -0
  75. package/dist/bundler-DRXCw_YR.mjs +0 -70
  76. package/dist/bundler-DRXCw_YR.mjs.map +0 -1
  77. package/dist/bundler-WsEvH_b2.cjs +0 -71
  78. package/dist/bundler-WsEvH_b2.cjs.map +0 -1
  79. package/dist/storage-BUYQJgz7.cjs +0 -4
  80. package/dist/storage-BXoJvmv2.cjs.map +0 -1
  81. package/dist/storage-C9PU_30f.mjs.map +0 -1
  82. package/dist/storage-DLJAYxzJ.mjs +0 -3
@@ -166,3 +166,72 @@ describe('URL normalization', () => {
166
166
  expect(() => new URL('invalid-url')).toThrow();
167
167
  });
168
168
  });
169
+
170
+ describe('logoutCommand', () => {
171
+ let tempDir: string;
172
+
173
+ beforeEach(async () => {
174
+ tempDir = join(tmpdir(), `gkm-logout-test-${Date.now()}`);
175
+ await mkdir(tempDir, { recursive: true });
176
+ });
177
+
178
+ afterEach(async () => {
179
+ if (existsSync(tempDir)) {
180
+ await rm(tempDir, { recursive: true });
181
+ }
182
+ });
183
+
184
+ it('should remove dokploy credentials', async () => {
185
+ // First store credentials
186
+ await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
187
+ root: tempDir,
188
+ });
189
+
190
+ // Verify they exist
191
+ let creds = await getDokployCredentials({ root: tempDir });
192
+ expect(creds).not.toBeNull();
193
+
194
+ // Remove credentials
195
+ const removed = await removeDokployCredentials({ root: tempDir });
196
+ expect(removed).toBe(true);
197
+
198
+ // Verify they're gone
199
+ creds = await getDokployCredentials({ root: tempDir });
200
+ expect(creds).toBeNull();
201
+ });
202
+
203
+ it('should return false when no credentials to remove', async () => {
204
+ const removed = await removeDokployCredentials({ root: tempDir });
205
+ expect(removed).toBe(false);
206
+ });
207
+ });
208
+
209
+ describe('whoamiCommand helpers', () => {
210
+ let tempDir: string;
211
+
212
+ beforeEach(async () => {
213
+ tempDir = join(tmpdir(), `gkm-whoami-test-${Date.now()}`);
214
+ await mkdir(tempDir, { recursive: true });
215
+ });
216
+
217
+ afterEach(async () => {
218
+ if (existsSync(tempDir)) {
219
+ await rm(tempDir, { recursive: true });
220
+ }
221
+ });
222
+
223
+ it('should return null when no credentials stored', async () => {
224
+ const creds = await getDokployCredentials({ root: tempDir });
225
+ expect(creds).toBeNull();
226
+ });
227
+
228
+ it('should return credentials when stored', async () => {
229
+ await storeDokployCredentials('test-token', 'https://test.example.com', {
230
+ root: tempDir,
231
+ });
232
+
233
+ const creds = await getDokployCredentials({ root: tempDir });
234
+ expect(creds).not.toBeNull();
235
+ expect(creds!.endpoint).toBe('https://test.example.com');
236
+ });
237
+ });
@@ -12,6 +12,8 @@ export interface StoredCredentials {
12
12
  token: string;
13
13
  /** Dokploy endpoint URL */
14
14
  endpoint: string;
15
+ /** Registry ID in Dokploy (for Docker image pulls) */
16
+ registryId?: string;
15
17
  /** When the credentials were stored */
16
18
  storedAt: string;
17
19
  };
@@ -112,6 +114,7 @@ export async function getDokployCredentials(
112
114
  ): Promise<{
113
115
  token: string;
114
116
  endpoint: string;
117
+ registryId?: string;
115
118
  } | null> {
116
119
  const credentials = await readCredentials(options);
117
120
 
@@ -122,6 +125,7 @@ export async function getDokployCredentials(
122
125
  return {
123
126
  token: credentials.dokploy.token,
124
127
  endpoint: credentials.dokploy.endpoint,
128
+ registryId: credentials.dokploy.registryId,
125
129
  };
126
130
  }
127
131
 
@@ -185,3 +189,32 @@ export async function getDokployEndpoint(
185
189
  const stored = await getDokployCredentials(options);
186
190
  return stored?.endpoint ?? null;
187
191
  }
192
+
193
+ /**
194
+ * Store Dokploy registry ID
195
+ */
196
+ export async function storeDokployRegistryId(
197
+ registryId: string,
198
+ options?: CredentialOptions,
199
+ ): Promise<void> {
200
+ const credentials = await readCredentials(options);
201
+
202
+ if (!credentials.dokploy) {
203
+ throw new Error(
204
+ 'Dokploy credentials not found. Run "gkm login --service dokploy" first.',
205
+ );
206
+ }
207
+
208
+ credentials.dokploy.registryId = registryId;
209
+ await writeCredentials(credentials, options);
210
+ }
211
+
212
+ /**
213
+ * Get Dokploy registry ID from stored credentials
214
+ */
215
+ export async function getDokployRegistryId(
216
+ options?: CredentialOptions,
217
+ ): Promise<string | undefined> {
218
+ const stored = await getDokployCredentials(options);
219
+ return stored?.registryId ?? undefined;
220
+ }
package/src/auth/index.ts CHANGED
@@ -30,19 +30,9 @@ export async function validateDokployToken(
30
30
  endpoint: string,
31
31
  token: string,
32
32
  ): Promise<boolean> {
33
- try {
34
- const response = await fetch(`${endpoint}/api/project.all`, {
35
- method: 'GET',
36
- headers: {
37
- 'Content-Type': 'application/json',
38
- Authorization: `Bearer ${token}`,
39
- },
40
- });
41
-
42
- return response.ok;
43
- } catch {
44
- return false;
45
- }
33
+ const { DokployApi } = await import('../deploy/dokploy-api');
34
+ const api = new DokployApi({ baseUrl: endpoint, token });
35
+ return api.validateToken();
46
36
  }
47
37
 
48
38
  /**
@@ -55,46 +45,60 @@ async function prompt(message: string, hidden = false): Promise<string> {
55
45
  );
56
46
  }
57
47
 
58
- const rl = readline.createInterface({ input, output });
59
-
60
- try {
61
- if (hidden) {
62
- // For hidden input, we need to handle it differently
63
- process.stdout.write(message);
64
-
65
- return new Promise((resolve) => {
66
- let value = '';
67
-
68
- const onData = (char: Buffer) => {
69
- const c = char.toString();
70
-
71
- if (c === '\n' || c === '\r') {
72
- process.stdin.removeListener('data', onData);
73
- process.stdin.setRawMode(false);
74
- process.stdout.write('\n');
75
- resolve(value);
76
- } else if (c === '\u0003') {
77
- // Ctrl+C
78
- process.exit(1);
79
- } else if (c === '\u007F' || c === '\b') {
80
- // Backspace
81
- if (value.length > 0) {
82
- value = value.slice(0, -1);
83
- }
84
- } else {
85
- value += c;
48
+ if (hidden) {
49
+ // For hidden input, use raw mode directly without readline
50
+ process.stdout.write(message);
51
+
52
+ return new Promise((resolve, reject) => {
53
+ let value = '';
54
+
55
+ const cleanup = () => {
56
+ process.stdin.setRawMode(false);
57
+ process.stdin.pause();
58
+ process.stdin.removeListener('data', onData);
59
+ process.stdin.removeListener('error', onError);
60
+ };
61
+
62
+ const onError = (err: Error) => {
63
+ cleanup();
64
+ reject(err);
65
+ };
66
+
67
+ const onData = (char: Buffer) => {
68
+ const c = char.toString();
69
+
70
+ if (c === '\n' || c === '\r') {
71
+ cleanup();
72
+ process.stdout.write('\n');
73
+ resolve(value);
74
+ } else if (c === '\u0003') {
75
+ // Ctrl+C
76
+ cleanup();
77
+ process.stdout.write('\n');
78
+ process.exit(1);
79
+ } else if (c === '\u007F' || c === '\b') {
80
+ // Backspace
81
+ if (value.length > 0) {
82
+ value = value.slice(0, -1);
86
83
  }
87
- };
88
-
89
- process.stdin.setRawMode(true);
90
- process.stdin.resume();
91
- process.stdin.on('data', onData);
92
- });
93
- } else {
84
+ } else {
85
+ value += c;
86
+ }
87
+ };
88
+
89
+ process.stdin.setRawMode(true);
90
+ process.stdin.resume();
91
+ process.stdin.on('data', onData);
92
+ process.stdin.on('error', onError);
93
+ });
94
+ } else {
95
+ // For visible input, use readline
96
+ const rl = readline.createInterface({ input, output });
97
+ try {
94
98
  return await rl.question(message);
99
+ } finally {
100
+ rl.close();
95
101
  }
96
- } finally {
97
- rl.close();
98
102
  }
99
103
  }
100
104
 
@@ -222,5 +226,8 @@ export function maskToken(token: string): string {
222
226
  export {
223
227
  getDokployCredentials,
224
228
  getDokployEndpoint,
229
+ getDokployRegistryId,
225
230
  getDokployToken,
231
+ storeDokployCredentials,
232
+ storeDokployRegistryId,
226
233
  } from './credentials';
@@ -0,0 +1,444 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type { Construct } from '@geekmidas/constructs';
4
+ import { itWithDir } from '@geekmidas/testkit/os';
5
+ import { beforeEach, describe, expect, vi } from 'vitest';
6
+ import type { StageSecrets } from '../../secrets/types';
7
+ import { bundleServer } from '../bundler';
8
+
9
+ // Mock child_process to avoid actually running tsdown
10
+ vi.mock('node:child_process', () => ({
11
+ spawnSync: vi.fn().mockReturnValue({ status: 0, error: null }),
12
+ }));
13
+
14
+ // Mock construct that returns specific environment variables
15
+ function createMockConstruct(envVars: string[]): Construct {
16
+ return {
17
+ getEnvironment: vi.fn().mockResolvedValue(envVars),
18
+ } as unknown as Construct;
19
+ }
20
+
21
+ // Helper to create a minimal secrets file
22
+ async function createSecretsFile(
23
+ dir: string,
24
+ stage: string,
25
+ secrets: Partial<StageSecrets>,
26
+ ): Promise<void> {
27
+ const secretsDir = join(dir, '.gkm', 'secrets');
28
+ await mkdir(secretsDir, { recursive: true });
29
+
30
+ const fullSecrets: StageSecrets = {
31
+ stage,
32
+ createdAt: new Date().toISOString(),
33
+ updatedAt: new Date().toISOString(),
34
+ services: {},
35
+ urls: {},
36
+ custom: {},
37
+ ...secrets,
38
+ };
39
+
40
+ await writeFile(
41
+ join(secretsDir, `${stage}.json`),
42
+ JSON.stringify(fullSecrets, null, 2),
43
+ );
44
+ }
45
+
46
+ // Helper to create a minimal entry point file and mock the bundle output
47
+ async function createEntryPoint(dir: string): Promise<string> {
48
+ const outputDir = join(dir, '.gkm', 'server');
49
+ const distDir = join(outputDir, 'dist');
50
+ await mkdir(outputDir, { recursive: true });
51
+ await mkdir(distDir, { recursive: true });
52
+
53
+ const entryPoint = join(outputDir, 'server.ts');
54
+ await writeFile(entryPoint, 'console.log("hello");');
55
+
56
+ // Create the output file that tsdown would normally create
57
+ // (since we're mocking execSync, the file won't be created automatically)
58
+ await writeFile(join(distDir, 'server.js'), 'console.log("bundled");');
59
+
60
+ return entryPoint;
61
+ }
62
+
63
+ describe('bundleServer environment validation', () => {
64
+ beforeEach(() => {
65
+ vi.clearAllMocks();
66
+ });
67
+
68
+ itWithDir(
69
+ 'should pass validation when all required env vars are present',
70
+ async ({ dir }) => {
71
+ const entryPoint = await createEntryPoint(dir);
72
+ await createSecretsFile(dir, 'production', {
73
+ urls: { DATABASE_URL: 'postgresql://localhost/db' },
74
+ custom: { API_KEY: 'sk_test_123' },
75
+ });
76
+
77
+ const constructs = [
78
+ createMockConstruct(['DATABASE_URL']),
79
+ createMockConstruct(['API_KEY']),
80
+ ];
81
+
82
+ const originalCwd = process.cwd();
83
+ process.chdir(dir);
84
+
85
+ try {
86
+ // Should not throw
87
+ const result = await bundleServer({
88
+ entryPoint,
89
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
90
+ minify: false,
91
+ sourcemap: false,
92
+ external: [],
93
+ stage: 'production',
94
+ constructs,
95
+ });
96
+
97
+ expect(result.masterKey).toBeDefined();
98
+ expect(constructs[0].getEnvironment).toHaveBeenCalled();
99
+ expect(constructs[1].getEnvironment).toHaveBeenCalled();
100
+ } finally {
101
+ process.chdir(originalCwd);
102
+ }
103
+ },
104
+ );
105
+
106
+ itWithDir(
107
+ 'should throw error when required env vars are missing',
108
+ async ({ dir }) => {
109
+ const entryPoint = await createEntryPoint(dir);
110
+ await createSecretsFile(dir, 'production', {
111
+ urls: { DATABASE_URL: 'postgresql://localhost/db' },
112
+ custom: {},
113
+ });
114
+
115
+ const constructs = [
116
+ createMockConstruct(['DATABASE_URL', 'API_KEY', 'JWT_SECRET']),
117
+ ];
118
+
119
+ const originalCwd = process.cwd();
120
+ process.chdir(dir);
121
+
122
+ try {
123
+ await expect(
124
+ bundleServer({
125
+ entryPoint,
126
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
127
+ minify: false,
128
+ sourcemap: false,
129
+ external: [],
130
+ stage: 'production',
131
+ constructs,
132
+ }),
133
+ ).rejects.toThrow('Missing environment variables');
134
+ } finally {
135
+ process.chdir(originalCwd);
136
+ }
137
+ },
138
+ );
139
+
140
+ itWithDir(
141
+ 'should include missing variables in error message',
142
+ async ({ dir }) => {
143
+ const entryPoint = await createEntryPoint(dir);
144
+ await createSecretsFile(dir, 'staging', {
145
+ custom: { EXISTING_VAR: 'value' },
146
+ });
147
+
148
+ const constructs = [
149
+ createMockConstruct(['EXISTING_VAR', 'MISSING_VAR_1', 'MISSING_VAR_2']),
150
+ ];
151
+
152
+ const originalCwd = process.cwd();
153
+ process.chdir(dir);
154
+
155
+ try {
156
+ await expect(
157
+ bundleServer({
158
+ entryPoint,
159
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
160
+ minify: false,
161
+ sourcemap: false,
162
+ external: [],
163
+ stage: 'staging',
164
+ constructs,
165
+ }),
166
+ ).rejects.toThrow(/MISSING_VAR_1/);
167
+
168
+ await expect(
169
+ bundleServer({
170
+ entryPoint,
171
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
172
+ minify: false,
173
+ sourcemap: false,
174
+ external: [],
175
+ stage: 'staging',
176
+ constructs,
177
+ }),
178
+ ).rejects.toThrow(/MISSING_VAR_2/);
179
+ } finally {
180
+ process.chdir(originalCwd);
181
+ }
182
+ },
183
+ );
184
+
185
+ itWithDir(
186
+ 'should collect env vars from multiple constructs',
187
+ async ({ dir }) => {
188
+ const entryPoint = await createEntryPoint(dir);
189
+ await createSecretsFile(dir, 'production', {
190
+ urls: { DATABASE_URL: 'postgresql://localhost/db' },
191
+ custom: {
192
+ API_KEY: 'key',
193
+ REDIS_URL: 'redis://localhost',
194
+ JWT_SECRET: 'secret',
195
+ },
196
+ });
197
+
198
+ const constructs = [
199
+ createMockConstruct(['DATABASE_URL']),
200
+ createMockConstruct(['API_KEY', 'REDIS_URL']),
201
+ createMockConstruct(['JWT_SECRET']),
202
+ ];
203
+
204
+ const originalCwd = process.cwd();
205
+ process.chdir(dir);
206
+
207
+ try {
208
+ const result = await bundleServer({
209
+ entryPoint,
210
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
211
+ minify: false,
212
+ sourcemap: false,
213
+ external: [],
214
+ stage: 'production',
215
+ constructs,
216
+ });
217
+
218
+ expect(result.masterKey).toBeDefined();
219
+
220
+ // All constructs should have been checked
221
+ for (const construct of constructs) {
222
+ expect(construct.getEnvironment).toHaveBeenCalled();
223
+ }
224
+ } finally {
225
+ process.chdir(originalCwd);
226
+ }
227
+ },
228
+ );
229
+
230
+ itWithDir(
231
+ 'should deduplicate env vars from multiple constructs',
232
+ async ({ dir }) => {
233
+ const entryPoint = await createEntryPoint(dir);
234
+ await createSecretsFile(dir, 'production', {
235
+ custom: { SHARED_VAR: 'value' },
236
+ });
237
+
238
+ // Multiple constructs requiring the same variable
239
+ const constructs = [
240
+ createMockConstruct(['SHARED_VAR']),
241
+ createMockConstruct(['SHARED_VAR']),
242
+ createMockConstruct(['SHARED_VAR']),
243
+ ];
244
+
245
+ const originalCwd = process.cwd();
246
+ process.chdir(dir);
247
+
248
+ try {
249
+ // Should pass since SHARED_VAR is provided once
250
+ const result = await bundleServer({
251
+ entryPoint,
252
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
253
+ minify: false,
254
+ sourcemap: false,
255
+ external: [],
256
+ stage: 'production',
257
+ constructs,
258
+ });
259
+
260
+ expect(result.masterKey).toBeDefined();
261
+ } finally {
262
+ process.chdir(originalCwd);
263
+ }
264
+ },
265
+ );
266
+
267
+ itWithDir(
268
+ 'should skip validation when no constructs provided',
269
+ async ({ dir }) => {
270
+ const entryPoint = await createEntryPoint(dir);
271
+ await createSecretsFile(dir, 'production', {
272
+ custom: {},
273
+ });
274
+
275
+ const originalCwd = process.cwd();
276
+ process.chdir(dir);
277
+
278
+ try {
279
+ // Should not throw even with empty secrets
280
+ const result = await bundleServer({
281
+ entryPoint,
282
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
283
+ minify: false,
284
+ sourcemap: false,
285
+ external: [],
286
+ stage: 'production',
287
+ constructs: [],
288
+ });
289
+
290
+ expect(result.masterKey).toBeDefined();
291
+ } finally {
292
+ process.chdir(originalCwd);
293
+ }
294
+ },
295
+ );
296
+
297
+ itWithDir(
298
+ 'should skip validation when constructs is undefined',
299
+ async ({ dir }) => {
300
+ const entryPoint = await createEntryPoint(dir);
301
+ await createSecretsFile(dir, 'production', {
302
+ custom: {},
303
+ });
304
+
305
+ const originalCwd = process.cwd();
306
+ process.chdir(dir);
307
+
308
+ try {
309
+ const result = await bundleServer({
310
+ entryPoint,
311
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
312
+ minify: false,
313
+ sourcemap: false,
314
+ external: [],
315
+ stage: 'production',
316
+ // No constructs provided
317
+ });
318
+
319
+ expect(result.masterKey).toBeDefined();
320
+ } finally {
321
+ process.chdir(originalCwd);
322
+ }
323
+ },
324
+ );
325
+
326
+ itWithDir(
327
+ 'should recognize service credentials as provided',
328
+ async ({ dir }) => {
329
+ const entryPoint = await createEntryPoint(dir);
330
+ await createSecretsFile(dir, 'production', {
331
+ services: {
332
+ postgres: {
333
+ host: 'localhost',
334
+ port: 5432,
335
+ username: 'app',
336
+ password: 'secret',
337
+ database: 'mydb',
338
+ },
339
+ },
340
+ });
341
+
342
+ const constructs = [
343
+ createMockConstruct([
344
+ 'POSTGRES_HOST',
345
+ 'POSTGRES_PORT',
346
+ 'POSTGRES_USER',
347
+ 'POSTGRES_PASSWORD',
348
+ 'POSTGRES_DB',
349
+ ]),
350
+ ];
351
+
352
+ const originalCwd = process.cwd();
353
+ process.chdir(dir);
354
+
355
+ try {
356
+ const result = await bundleServer({
357
+ entryPoint,
358
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
359
+ minify: false,
360
+ sourcemap: false,
361
+ external: [],
362
+ stage: 'production',
363
+ constructs,
364
+ });
365
+
366
+ expect(result.masterKey).toBeDefined();
367
+ } finally {
368
+ process.chdir(originalCwd);
369
+ }
370
+ },
371
+ );
372
+
373
+ itWithDir(
374
+ 'should throw when secrets file does not exist',
375
+ async ({ dir }) => {
376
+ const entryPoint = await createEntryPoint(dir);
377
+ // Don't create secrets file
378
+
379
+ const constructs = [createMockConstruct(['DATABASE_URL'])];
380
+
381
+ const originalCwd = process.cwd();
382
+ process.chdir(dir);
383
+
384
+ try {
385
+ await expect(
386
+ bundleServer({
387
+ entryPoint,
388
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
389
+ minify: false,
390
+ sourcemap: false,
391
+ external: [],
392
+ stage: 'production',
393
+ constructs,
394
+ }),
395
+ ).rejects.toThrow('No secrets found for stage "production"');
396
+ } finally {
397
+ process.chdir(originalCwd);
398
+ }
399
+ },
400
+ );
401
+
402
+ itWithDir(
403
+ 'should include helpful instructions in error message',
404
+ async ({ dir }) => {
405
+ const entryPoint = await createEntryPoint(dir);
406
+ await createSecretsFile(dir, 'myapp', {
407
+ custom: {},
408
+ });
409
+
410
+ const constructs = [createMockConstruct(['MISSING_VAR'])];
411
+
412
+ const originalCwd = process.cwd();
413
+ process.chdir(dir);
414
+
415
+ try {
416
+ await expect(
417
+ bundleServer({
418
+ entryPoint,
419
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
420
+ minify: false,
421
+ sourcemap: false,
422
+ external: [],
423
+ stage: 'myapp',
424
+ constructs,
425
+ }),
426
+ ).rejects.toThrow(/gkm secrets:set/);
427
+
428
+ await expect(
429
+ bundleServer({
430
+ entryPoint,
431
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
432
+ minify: false,
433
+ sourcemap: false,
434
+ external: [],
435
+ stage: 'myapp',
436
+ constructs,
437
+ }),
438
+ ).rejects.toThrow(/gkm secrets:import/);
439
+ } finally {
440
+ process.chdir(originalCwd);
441
+ }
442
+ },
443
+ );
444
+ });