@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.
Files changed (118) hide show
  1. package/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
  2. package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
  3. package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
  4. package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
  5. package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
  6. package/dist/config-BaYqrF3n.mjs.map +1 -0
  7. package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
  8. package/dist/config-CxrLu8ia.cjs.map +1 -0
  9. package/dist/config.cjs +4 -1
  10. package/dist/config.d.cts +27 -2
  11. package/dist/config.d.cts.map +1 -1
  12. package/dist/config.d.mts +27 -2
  13. package/dist/config.d.mts.map +1 -1
  14. package/dist/config.mjs +3 -2
  15. package/dist/dokploy-api-B0w17y4_.mjs +3 -0
  16. package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
  17. package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
  18. package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
  19. package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
  20. package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
  21. package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
  22. package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
  23. package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
  24. package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  25. package/dist/index-CWN-bgrO.d.mts +495 -0
  26. package/dist/index-CWN-bgrO.d.mts.map +1 -0
  27. package/dist/index-DEWYvYvg.d.cts +495 -0
  28. package/dist/index-DEWYvYvg.d.cts.map +1 -0
  29. package/dist/index.cjs +2639 -563
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +2634 -563
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
  34. package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
  35. package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
  36. package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
  38. package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
  39. package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
  40. package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -2
  44. package/dist/openapi.d.cts +1 -1
  45. package/dist/openapi.d.mts +1 -1
  46. package/dist/openapi.mjs +3 -2
  47. package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
  48. package/dist/storage-BPRgh3DU.cjs.map +1 -0
  49. package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
  50. package/dist/storage-Dhst7BhI.mjs +272 -0
  51. package/dist/storage-Dhst7BhI.mjs.map +1 -0
  52. package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
  53. package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
  54. package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
  55. package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
  56. package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
  57. package/dist/workspace/index.cjs +19 -0
  58. package/dist/workspace/index.d.cts +3 -0
  59. package/dist/workspace/index.d.mts +3 -0
  60. package/dist/workspace/index.mjs +3 -0
  61. package/dist/workspace-CPLEZDZf.mjs +3788 -0
  62. package/dist/workspace-CPLEZDZf.mjs.map +1 -0
  63. package/dist/workspace-iWgBlX6h.cjs +3885 -0
  64. package/dist/workspace-iWgBlX6h.cjs.map +1 -0
  65. package/package.json +8 -3
  66. package/src/build/__tests__/workspace-build.spec.ts +215 -0
  67. package/src/build/index.ts +189 -1
  68. package/src/config.ts +71 -14
  69. package/src/deploy/__tests__/docker.spec.ts +1 -1
  70. package/src/deploy/__tests__/index.spec.ts +305 -1
  71. package/src/deploy/index.ts +426 -4
  72. package/src/deploy/types.ts +32 -0
  73. package/src/dev/__tests__/index.spec.ts +572 -1
  74. package/src/dev/index.ts +582 -2
  75. package/src/docker/__tests__/compose.spec.ts +425 -0
  76. package/src/docker/__tests__/templates.spec.ts +145 -0
  77. package/src/docker/compose.ts +248 -0
  78. package/src/docker/index.ts +159 -3
  79. package/src/docker/templates.ts +219 -4
  80. package/src/index.ts +24 -0
  81. package/src/init/__tests__/generators.spec.ts +17 -24
  82. package/src/init/__tests__/init.spec.ts +157 -5
  83. package/src/init/generators/auth.ts +220 -0
  84. package/src/init/generators/config.ts +61 -4
  85. package/src/init/generators/docker.ts +115 -8
  86. package/src/init/generators/env.ts +7 -127
  87. package/src/init/generators/index.ts +1 -0
  88. package/src/init/generators/models.ts +3 -1
  89. package/src/init/generators/monorepo.ts +154 -10
  90. package/src/init/generators/package.ts +5 -3
  91. package/src/init/generators/web.ts +213 -0
  92. package/src/init/index.ts +290 -58
  93. package/src/init/templates/api.ts +38 -29
  94. package/src/init/templates/index.ts +132 -4
  95. package/src/init/templates/minimal.ts +33 -35
  96. package/src/init/templates/serverless.ts +16 -19
  97. package/src/init/templates/worker.ts +50 -25
  98. package/src/init/versions.ts +47 -0
  99. package/src/secrets/keystore.ts +144 -0
  100. package/src/secrets/storage.ts +109 -6
  101. package/src/test/index.ts +97 -0
  102. package/src/workspace/__tests__/client-generator.spec.ts +357 -0
  103. package/src/workspace/__tests__/index.spec.ts +543 -0
  104. package/src/workspace/__tests__/schema.spec.ts +519 -0
  105. package/src/workspace/__tests__/type-inference.spec.ts +251 -0
  106. package/src/workspace/client-generator.ts +307 -0
  107. package/src/workspace/index.ts +372 -0
  108. package/src/workspace/schema.ts +368 -0
  109. package/src/workspace/types.ts +336 -0
  110. package/tsconfig.tsbuildinfo +1 -1
  111. package/tsdown.config.ts +1 -0
  112. package/dist/config-AmInkU7k.cjs.map +0 -1
  113. package/dist/config-DYULeEv8.mjs.map +0 -1
  114. package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
  115. package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
  116. package/dist/storage-BaOP55oq.mjs +0 -147
  117. package/dist/storage-BaOP55oq.mjs.map +0 -1
  118. 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
+ }
@@ -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
- * Read secrets for a stage.
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
- return JSON.parse(content) as StageSecrets;
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
- // Write with pretty formatting
77
- await writeFile(path, JSON.stringify(secrets, null, 2), 'utf-8');
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
+ }