@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.
- package/dist/bundler-BjholBlA.cjs +131 -0
- package/dist/bundler-BjholBlA.cjs.map +1 -0
- package/dist/bundler-DWctKN1z.mjs +130 -0
- package/dist/bundler-DWctKN1z.mjs.map +1 -0
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +3 -0
- package/dist/dokploy-api-C7F9VykY.cjs +317 -0
- package/dist/dokploy-api-C7F9VykY.cjs.map +1 -0
- package/dist/dokploy-api-CaETb2L6.mjs +305 -0
- package/dist/dokploy-api-CaETb2L6.mjs.map +1 -0
- package/dist/dokploy-api-DHvfmWbi.mjs +3 -0
- package/dist/{encryption-Dyf_r1h-.cjs → encryption-D7Efcdi9.cjs} +1 -1
- package/dist/{encryption-Dyf_r1h-.cjs.map → encryption-D7Efcdi9.cjs.map} +1 -1
- package/dist/{encryption-C8H-38Yy.mjs → encryption-h4Nb6W-M.mjs} +1 -1
- package/dist/{encryption-C8H-38Yy.mjs.map → encryption-h4Nb6W-M.mjs.map} +1 -1
- package/dist/index.cjs +1520 -1136
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1520 -1136
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Bt_1FDpT.cjs → openapi-C89hhkZC.cjs} +3 -3
- package/dist/{openapi-Bt_1FDpT.cjs.map → openapi-C89hhkZC.cjs.map} +1 -1
- package/dist/{openapi-BfFlOBCG.mjs → openapi-CZVcfxk-.mjs} +3 -3
- package/dist/{openapi-BfFlOBCG.mjs.map → openapi-CZVcfxk-.mjs.map} +1 -1
- package/dist/{openapi-react-query-B6XTeGqS.mjs → openapi-react-query-CM2_qlW9.mjs} +1 -1
- package/dist/{openapi-react-query-B6XTeGqS.mjs.map → openapi-react-query-CM2_qlW9.mjs.map} +1 -1
- package/dist/{openapi-react-query-B-sNWHFU.cjs → openapi-react-query-iKjfLzff.cjs} +1 -1
- package/dist/{openapi-react-query-B-sNWHFU.cjs.map → openapi-react-query-iKjfLzff.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{storage-C9PU_30f.mjs → storage-BaOP55oq.mjs} +48 -2
- package/dist/storage-BaOP55oq.mjs.map +1 -0
- package/dist/{storage-BXoJvmv2.cjs → storage-Bn3K9Ccu.cjs} +59 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +1 -0
- package/dist/storage-UfyTn7Zm.cjs +7 -0
- package/dist/storage-nkGIjeXt.mjs +3 -0
- package/dist/{types-BR0M2v_c.d.mts → types-BgaMXsUa.d.cts} +3 -1
- package/dist/{types-BR0M2v_c.d.mts.map → types-BgaMXsUa.d.cts.map} +1 -1
- package/dist/{types-BhkZc-vm.d.cts → types-iFk5ms7y.d.mts} +3 -1
- package/dist/{types-BhkZc-vm.d.cts.map → types-iFk5ms7y.d.mts.map} +1 -1
- package/package.json +4 -4
- package/src/auth/__tests__/credentials.spec.ts +127 -0
- package/src/auth/__tests__/index.spec.ts +69 -0
- package/src/auth/credentials.ts +33 -0
- package/src/auth/index.ts +57 -50
- package/src/build/__tests__/bundler.spec.ts +444 -0
- package/src/build/__tests__/endpoint-analyzer.spec.ts +623 -0
- package/src/build/__tests__/handler-templates.spec.ts +272 -0
- package/src/build/bundler.ts +126 -8
- package/src/build/index.ts +31 -0
- package/src/build/types.ts +6 -0
- package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
- package/src/deploy/__tests__/dokploy.spec.ts +196 -6
- package/src/deploy/__tests__/index.spec.ts +339 -0
- package/src/deploy/__tests__/init.spec.ts +147 -16
- package/src/deploy/docker.ts +32 -3
- package/src/deploy/dokploy-api.ts +581 -0
- package/src/deploy/dokploy.ts +66 -93
- package/src/deploy/index.ts +587 -32
- package/src/deploy/init.ts +192 -249
- package/src/deploy/types.ts +19 -1
- package/src/dev/__tests__/index.spec.ts +95 -0
- package/src/docker/__tests__/templates.spec.ts +144 -0
- package/src/docker/index.ts +96 -6
- package/src/docker/templates.ts +114 -27
- package/src/generators/EndpointGenerator.ts +2 -2
- package/src/index.ts +34 -13
- package/src/secrets/__tests__/storage.spec.ts +208 -0
- package/src/secrets/storage.ts +73 -0
- package/src/types.ts +2 -0
- package/dist/bundler-DRXCw_YR.mjs +0 -70
- package/dist/bundler-DRXCw_YR.mjs.map +0 -1
- package/dist/bundler-WsEvH_b2.cjs +0 -71
- package/dist/bundler-WsEvH_b2.cjs.map +0 -1
- package/dist/storage-BUYQJgz7.cjs +0 -4
- package/dist/storage-BXoJvmv2.cjs.map +0 -1
- package/dist/storage-C9PU_30f.mjs.map +0 -1
- 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
|
+
});
|
package/src/auth/credentials.ts
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
});
|