@geekmidas/cli 0.18.0 → 0.19.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-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2639 -563
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2634 -563
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +8 -3
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
// Path is ../package.json from dist/ (bundled output is flat)
|
|
5
|
+
const pkg = require('../package.json') as { version: string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CLI version from package.json (used for scaffolded projects)
|
|
9
|
+
*/
|
|
10
|
+
export const CLI_VERSION = `~${pkg.version}`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Current released versions of @geekmidas packages
|
|
14
|
+
* Update these when publishing new versions
|
|
15
|
+
* Note: CLI version is read from package.json via CLI_VERSION
|
|
16
|
+
*/
|
|
17
|
+
export const GEEKMIDAS_VERSIONS = {
|
|
18
|
+
'@geekmidas/audit': '~0.2.0',
|
|
19
|
+
'@geekmidas/auth': '~0.2.0',
|
|
20
|
+
'@geekmidas/cache': '~0.2.0',
|
|
21
|
+
'@geekmidas/cli': CLI_VERSION,
|
|
22
|
+
'@geekmidas/client': '~0.5.0',
|
|
23
|
+
'@geekmidas/cloud': '~0.2.0',
|
|
24
|
+
'@geekmidas/constructs': '~0.6.0',
|
|
25
|
+
'@geekmidas/db': '~0.3.0',
|
|
26
|
+
'@geekmidas/emailkit': '~0.2.0',
|
|
27
|
+
'@geekmidas/envkit': '~0.4.0',
|
|
28
|
+
'@geekmidas/errors': '~0.1.0',
|
|
29
|
+
'@geekmidas/events': '~0.2.0',
|
|
30
|
+
'@geekmidas/logger': '~0.4.0',
|
|
31
|
+
'@geekmidas/rate-limit': '~0.3.0',
|
|
32
|
+
'@geekmidas/schema': '~0.1.0',
|
|
33
|
+
'@geekmidas/services': '~0.2.0',
|
|
34
|
+
'@geekmidas/storage': '~0.1.0',
|
|
35
|
+
'@geekmidas/studio': '~0.4.0',
|
|
36
|
+
'@geekmidas/telescope': '~0.4.0',
|
|
37
|
+
'@geekmidas/testkit': '~0.6.0',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type GeekmidasPackage = keyof typeof GEEKMIDAS_VERSIONS;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the version for a @geekmidas package
|
|
44
|
+
*/
|
|
45
|
+
export function getPackageVersion(pkg: GeekmidasPackage): string {
|
|
46
|
+
return GEEKMIDAS_VERSIONS[pkg]!;
|
|
47
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/** Key length for AES-256 encryption */
|
|
8
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the keystore directory for a project.
|
|
12
|
+
* Keys are stored at ~/.gkm/{project-name}/
|
|
13
|
+
*
|
|
14
|
+
* @param projectName - Name of the project (defaults to current directory name)
|
|
15
|
+
* @returns Path to the keystore directory
|
|
16
|
+
*/
|
|
17
|
+
export function getKeystoreDir(projectName?: string): string {
|
|
18
|
+
const name = projectName ?? basename(process.cwd());
|
|
19
|
+
return join(homedir(), '.gkm', name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the path to a stage's encryption key.
|
|
24
|
+
*
|
|
25
|
+
* @param stage - Stage name (e.g., 'development', 'production')
|
|
26
|
+
* @param projectName - Name of the project (defaults to current directory name)
|
|
27
|
+
* @returns Path to the key file
|
|
28
|
+
*/
|
|
29
|
+
export function getKeyPath(stage: string, projectName?: string): string {
|
|
30
|
+
return join(getKeystoreDir(projectName), `${stage}.key`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a key exists for a stage.
|
|
35
|
+
*/
|
|
36
|
+
export function keyExists(stage: string, projectName?: string): boolean {
|
|
37
|
+
return existsSync(getKeyPath(stage, projectName));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a new encryption key for a stage.
|
|
42
|
+
* The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.
|
|
43
|
+
*
|
|
44
|
+
* @param stage - Stage name
|
|
45
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
46
|
+
* @returns The generated key as a hex string
|
|
47
|
+
*/
|
|
48
|
+
export async function generateKey(
|
|
49
|
+
stage: string,
|
|
50
|
+
projectName?: string,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
const keyDir = getKeystoreDir(projectName);
|
|
53
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
54
|
+
|
|
55
|
+
// Ensure keystore directory exists with restricted permissions
|
|
56
|
+
await mkdir(keyDir, { recursive: true, mode: 0o700 });
|
|
57
|
+
|
|
58
|
+
// Generate random key
|
|
59
|
+
const key = randomBytes(KEY_LENGTH).toString('hex');
|
|
60
|
+
|
|
61
|
+
// Write key with restricted permissions (owner read/write only)
|
|
62
|
+
await writeFile(keyPath, key, { mode: 0o600, encoding: 'utf-8' });
|
|
63
|
+
|
|
64
|
+
// Ensure permissions are set correctly (in case file existed)
|
|
65
|
+
await chmod(keyPath, 0o600);
|
|
66
|
+
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read an encryption key for a stage.
|
|
72
|
+
*
|
|
73
|
+
* @param stage - Stage name
|
|
74
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
75
|
+
* @returns The key as a hex string, or null if not found
|
|
76
|
+
*/
|
|
77
|
+
export async function readKey(
|
|
78
|
+
stage: string,
|
|
79
|
+
projectName?: string,
|
|
80
|
+
): Promise<string | null> {
|
|
81
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
82
|
+
|
|
83
|
+
if (!existsSync(keyPath)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const key = await readFile(keyPath, 'utf-8');
|
|
88
|
+
return key.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read an encryption key for a stage, throwing if not found.
|
|
93
|
+
*/
|
|
94
|
+
export async function requireKey(
|
|
95
|
+
stage: string,
|
|
96
|
+
projectName?: string,
|
|
97
|
+
): Promise<string> {
|
|
98
|
+
const key = await readKey(stage, projectName);
|
|
99
|
+
|
|
100
|
+
if (!key) {
|
|
101
|
+
const name = projectName ?? basename(process.cwd());
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Encryption key not found for stage "${stage}" in project "${name}". ` +
|
|
104
|
+
`Expected key at: ${getKeyPath(stage, projectName)}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return key;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Delete a key for a stage.
|
|
113
|
+
*/
|
|
114
|
+
export async function deleteKey(
|
|
115
|
+
stage: string,
|
|
116
|
+
projectName?: string,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const keyPath = getKeyPath(stage, projectName);
|
|
119
|
+
|
|
120
|
+
if (existsSync(keyPath)) {
|
|
121
|
+
await rm(keyPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get or create a key for a stage.
|
|
127
|
+
* If the key already exists, it is returned. Otherwise, a new key is generated.
|
|
128
|
+
*
|
|
129
|
+
* @param stage - Stage name
|
|
130
|
+
* @param projectName - Project name (defaults to current directory name)
|
|
131
|
+
* @returns The key as a hex string
|
|
132
|
+
*/
|
|
133
|
+
export async function getOrCreateKey(
|
|
134
|
+
stage: string,
|
|
135
|
+
projectName?: string,
|
|
136
|
+
): Promise<string> {
|
|
137
|
+
const existingKey = await readKey(stage, projectName);
|
|
138
|
+
|
|
139
|
+
if (existingKey) {
|
|
140
|
+
return existingKey;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return generateKey(stage, projectName);
|
|
144
|
+
}
|
package/src/secrets/storage.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { getOrCreateKey, readKey } from './keystore';
|
|
4
6
|
import type { EmbeddableSecrets, StageSecrets } from './types';
|
|
5
7
|
|
|
6
8
|
/** Default secrets directory relative to project root */
|
|
7
9
|
const SECRETS_DIR = '.gkm/secrets';
|
|
8
10
|
|
|
11
|
+
/** AES-256-GCM configuration */
|
|
12
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
13
|
+
const IV_LENGTH = 12; // 96 bits for GCM
|
|
14
|
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
15
|
+
|
|
16
|
+
/** Encrypted secrets file structure */
|
|
17
|
+
interface EncryptedSecretsFile {
|
|
18
|
+
/** Version for future format changes */
|
|
19
|
+
version: 1;
|
|
20
|
+
/** Base64 encoded encrypted data (ciphertext + auth tag) */
|
|
21
|
+
encrypted: string;
|
|
22
|
+
/** Hex encoded IV */
|
|
23
|
+
iv: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
/**
|
|
10
27
|
* Get the secrets directory path.
|
|
11
28
|
*/
|
|
@@ -43,7 +60,69 @@ export function initStageSecrets(stage: string): StageSecrets {
|
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
/**
|
|
46
|
-
*
|
|
63
|
+
* Encrypt secrets using a key.
|
|
64
|
+
*/
|
|
65
|
+
function encryptSecretsData(
|
|
66
|
+
secrets: StageSecrets,
|
|
67
|
+
keyHex: string,
|
|
68
|
+
): EncryptedSecretsFile {
|
|
69
|
+
const key = Buffer.from(keyHex, 'hex');
|
|
70
|
+
const iv = randomBytes(IV_LENGTH);
|
|
71
|
+
|
|
72
|
+
// Serialize secrets to JSON
|
|
73
|
+
const plaintext = JSON.stringify(secrets);
|
|
74
|
+
|
|
75
|
+
// Encrypt
|
|
76
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
77
|
+
const ciphertext = Buffer.concat([
|
|
78
|
+
cipher.update(plaintext, 'utf-8'),
|
|
79
|
+
cipher.final(),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Get auth tag
|
|
83
|
+
const authTag = cipher.getAuthTag();
|
|
84
|
+
|
|
85
|
+
// Combine ciphertext + auth tag
|
|
86
|
+
const combined = Buffer.concat([ciphertext, authTag]);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
version: 1,
|
|
90
|
+
encrypted: combined.toString('base64'),
|
|
91
|
+
iv: iv.toString('hex'),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decrypt secrets using a key.
|
|
97
|
+
*/
|
|
98
|
+
function decryptSecretsData(
|
|
99
|
+
data: EncryptedSecretsFile,
|
|
100
|
+
keyHex: string,
|
|
101
|
+
): StageSecrets {
|
|
102
|
+
const key = Buffer.from(keyHex, 'hex');
|
|
103
|
+
const ivBuffer = Buffer.from(data.iv, 'hex');
|
|
104
|
+
const combined = Buffer.from(data.encrypted, 'base64');
|
|
105
|
+
|
|
106
|
+
// Split ciphertext and auth tag
|
|
107
|
+
const ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);
|
|
108
|
+
const authTag = combined.subarray(-AUTH_TAG_LENGTH);
|
|
109
|
+
|
|
110
|
+
// Decrypt
|
|
111
|
+
const decipher = createDecipheriv(ALGORITHM, key, ivBuffer);
|
|
112
|
+
decipher.setAuthTag(authTag);
|
|
113
|
+
|
|
114
|
+
const plaintext = Buffer.concat([
|
|
115
|
+
decipher.update(ciphertext),
|
|
116
|
+
decipher.final(),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
return JSON.parse(plaintext.toString('utf-8')) as StageSecrets;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read secrets for a stage (encrypted).
|
|
124
|
+
* Requires the decryption key to be present at ~/.gkm/{project}/{stage}.key
|
|
125
|
+
*
|
|
47
126
|
* @returns StageSecrets or null if not found
|
|
48
127
|
*/
|
|
49
128
|
export async function readStageSecrets(
|
|
@@ -57,11 +136,30 @@ export async function readStageSecrets(
|
|
|
57
136
|
}
|
|
58
137
|
|
|
59
138
|
const content = await readFile(path, 'utf-8');
|
|
60
|
-
|
|
139
|
+
const data = JSON.parse(content);
|
|
140
|
+
|
|
141
|
+
// Check if this is an encrypted file (has version field)
|
|
142
|
+
if (data.version === 1 && data.encrypted && data.iv) {
|
|
143
|
+
const projectName = basename(cwd);
|
|
144
|
+
const key = await readKey(stage, projectName);
|
|
145
|
+
|
|
146
|
+
if (!key) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Decryption key not found for stage "${stage}". ` +
|
|
149
|
+
`Expected key at: ~/.gkm/${projectName}/${stage}.key`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return decryptSecretsData(data as EncryptedSecretsFile, key);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Legacy: unencrypted format (for backwards compatibility)
|
|
157
|
+
return data as StageSecrets;
|
|
61
158
|
}
|
|
62
159
|
|
|
63
160
|
/**
|
|
64
|
-
* Write secrets for a stage.
|
|
161
|
+
* Write secrets for a stage (encrypted).
|
|
162
|
+
* Creates or uses existing encryption key at ~/.gkm/{project}/{stage}.key
|
|
65
163
|
*/
|
|
66
164
|
export async function writeStageSecrets(
|
|
67
165
|
secrets: StageSecrets,
|
|
@@ -69,12 +167,17 @@ export async function writeStageSecrets(
|
|
|
69
167
|
): Promise<void> {
|
|
70
168
|
const dir = getSecretsDir(cwd);
|
|
71
169
|
const path = getSecretsPath(secrets.stage, cwd);
|
|
170
|
+
const projectName = basename(cwd);
|
|
72
171
|
|
|
73
172
|
// Ensure directory exists
|
|
74
173
|
await mkdir(dir, { recursive: true });
|
|
75
174
|
|
|
76
|
-
//
|
|
77
|
-
await
|
|
175
|
+
// Get or create encryption key
|
|
176
|
+
const key = await getOrCreateKey(secrets.stage, projectName);
|
|
177
|
+
|
|
178
|
+
// Encrypt and write
|
|
179
|
+
const encrypted = encryptSecretsData(secrets, key);
|
|
180
|
+
await writeFile(path, JSON.stringify(encrypted, null, 2), 'utf-8');
|
|
78
181
|
}
|
|
79
182
|
|
|
80
183
|
/**
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readStageSecrets, toEmbeddableSecrets } from '../secrets/storage';
|
|
3
|
+
|
|
4
|
+
export interface TestOptions {
|
|
5
|
+
/** Stage to load secrets from (default: development) */
|
|
6
|
+
stage?: string;
|
|
7
|
+
/** Run tests once without watch mode */
|
|
8
|
+
run?: boolean;
|
|
9
|
+
/** Enable watch mode */
|
|
10
|
+
watch?: boolean;
|
|
11
|
+
/** Generate coverage report */
|
|
12
|
+
coverage?: boolean;
|
|
13
|
+
/** Open Vitest UI */
|
|
14
|
+
ui?: boolean;
|
|
15
|
+
/** Pattern to filter tests */
|
|
16
|
+
pattern?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run tests with secrets loaded from the specified stage.
|
|
21
|
+
* Secrets are decrypted and injected into the environment.
|
|
22
|
+
*/
|
|
23
|
+
export async function testCommand(options: TestOptions = {}): Promise<void> {
|
|
24
|
+
const stage = options.stage ?? 'development';
|
|
25
|
+
|
|
26
|
+
console.log(`\n🧪 Running tests with ${stage} secrets...\n`);
|
|
27
|
+
|
|
28
|
+
// Load and decrypt secrets
|
|
29
|
+
let envVars: Record<string, string> = {};
|
|
30
|
+
try {
|
|
31
|
+
const secrets = await readStageSecrets(stage);
|
|
32
|
+
if (secrets) {
|
|
33
|
+
envVars = toEmbeddableSecrets(secrets);
|
|
34
|
+
console.log(
|
|
35
|
+
` Loaded ${Object.keys(envVars).length} secrets from ${stage}\n`,
|
|
36
|
+
);
|
|
37
|
+
} else {
|
|
38
|
+
console.log(` No secrets found for ${stage}, running without secrets\n`);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof Error && error.message.includes('key not found')) {
|
|
42
|
+
console.log(
|
|
43
|
+
` Decryption key not found for ${stage}, running without secrets\n`,
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build vitest args
|
|
51
|
+
const args: string[] = [];
|
|
52
|
+
|
|
53
|
+
if (options.run) {
|
|
54
|
+
args.push('run');
|
|
55
|
+
} else if (options.watch) {
|
|
56
|
+
args.push('--watch');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.coverage) {
|
|
60
|
+
args.push('--coverage');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.ui) {
|
|
64
|
+
args.push('--ui');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.pattern) {
|
|
68
|
+
args.push(options.pattern);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Run vitest with secrets in environment
|
|
72
|
+
const vitestProcess = spawn('npx', ['vitest', ...args], {
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
env: {
|
|
76
|
+
...process.env,
|
|
77
|
+
...envVars,
|
|
78
|
+
// Ensure NODE_ENV is set to test
|
|
79
|
+
NODE_ENV: 'test',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Wait for vitest to complete
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
vitestProcess.on('close', (code) => {
|
|
86
|
+
if (code === 0) {
|
|
87
|
+
resolve();
|
|
88
|
+
} else {
|
|
89
|
+
reject(new Error(`Tests failed with exit code ${code}`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
vitestProcess.on('error', (error) => {
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|