@geekmidas/cli 1.10.16 → 1.10.18

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 (41) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{bundler-B4AackW5.mjs → bundler-C5xkxnyr.mjs} +2 -2
  3. package/dist/{bundler-B4AackW5.mjs.map → bundler-C5xkxnyr.mjs.map} +1 -1
  4. package/dist/{bundler-BhhfkI9T.cjs → bundler-i-az1DZ2.cjs} +2 -2
  5. package/dist/{bundler-BhhfkI9T.cjs.map → bundler-i-az1DZ2.cjs.map} +1 -1
  6. package/dist/index.cjs +699 -712
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +686 -699
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/{openapi-BYxAWwok.cjs → openapi-CsCNpSf8.cjs} +1 -1
  11. package/dist/{openapi-BYxAWwok.cjs.map → openapi-CsCNpSf8.cjs.map} +1 -1
  12. package/dist/{openapi-DenF-okj.mjs → openapi-kvwpKbNe.mjs} +1 -1
  13. package/dist/{openapi-DenF-okj.mjs.map → openapi-kvwpKbNe.mjs.map} +1 -1
  14. package/dist/openapi.cjs +1 -1
  15. package/dist/openapi.mjs +1 -1
  16. package/dist/{storage-DOEtT2Hr.cjs → storage-ChVQI_G7.cjs} +1 -1
  17. package/dist/{storage-dbb9RyBl.mjs → storage-CpMNB77O.mjs} +1 -1
  18. package/dist/{storage-dbb9RyBl.mjs.map → storage-CpMNB77O.mjs.map} +1 -1
  19. package/dist/{storage-B1wvztiJ.cjs → storage-DLEb8Dkd.cjs} +1 -1
  20. package/dist/{storage-B1wvztiJ.cjs.map → storage-DLEb8Dkd.cjs.map} +1 -1
  21. package/dist/{storage-Cs4WBsc4.mjs → storage-mwbL7PhP.mjs} +1 -1
  22. package/dist/{sync-DGXXSk2v.cjs → sync-BWD_I5Ai.cjs} +2 -2
  23. package/dist/{sync-DGXXSk2v.cjs.map → sync-BWD_I5Ai.cjs.map} +1 -1
  24. package/dist/sync-ByaRPBxh.cjs +4 -0
  25. package/dist/{sync-COnAugP-.mjs → sync-CYBVB64f.mjs} +1 -1
  26. package/dist/{sync-D_NowTkZ.mjs → sync-lExOTa9t.mjs} +2 -2
  27. package/dist/{sync-D_NowTkZ.mjs.map → sync-lExOTa9t.mjs.map} +1 -1
  28. package/package.json +3 -3
  29. package/src/credentials/__tests__/fullDockerPorts.spec.ts +144 -0
  30. package/src/credentials/__tests__/helpers.ts +112 -0
  31. package/src/credentials/__tests__/prepareEntryCredentials.spec.ts +125 -0
  32. package/src/credentials/__tests__/readonlyDockerPorts.spec.ts +190 -0
  33. package/src/credentials/__tests__/workspaceCredentials.spec.ts +209 -0
  34. package/src/credentials/index.ts +826 -0
  35. package/src/dev/__tests__/index.spec.ts +68 -0
  36. package/src/dev/index.ts +44 -821
  37. package/src/exec/index.ts +120 -0
  38. package/src/init/versions.ts +1 -1
  39. package/src/setup/index.ts +4 -1
  40. package/src/test/index.ts +32 -109
  41. package/dist/sync-D1Pa30oV.cjs +0 -4
package/src/dev/index.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  import { type ChildProcess, execSync, spawn } from 'node:child_process';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
- import { createServer } from 'node:net';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
5
4
  import { dirname, join, resolve } from 'node:path';
6
5
  import chokidar from 'chokidar';
7
- import { config as dotenvConfig } from 'dotenv';
8
6
  import fg from 'fast-glob';
9
- import { parse as parseYaml } from 'yaml';
10
7
  import { resolveProviders } from '../build/providerResolver';
11
8
  import type {
12
9
  BuildContext,
@@ -18,10 +15,20 @@ import type {
18
15
  import {
19
16
  getAppNameFromCwd,
20
17
  loadAppConfig,
21
- loadWorkspaceAppInfo,
22
18
  loadWorkspaceConfig,
23
19
  parseModuleConfig,
24
20
  } from '../config';
21
+ import {
22
+ createEntryWrapper,
23
+ findAvailablePort,
24
+ isPortAvailable,
25
+ loadEnvFiles,
26
+ loadSecretsForApp,
27
+ prepareEntryCredentials,
28
+ resolveServicePorts,
29
+ rewriteUrlsWithPorts,
30
+ startWorkspaceServices,
31
+ } from '../credentials';
25
32
  import {
26
33
  CronGenerator,
27
34
  EndpointGenerator,
@@ -58,369 +65,38 @@ import {
58
65
  type NormalizedWorkspace,
59
66
  } from '../workspace/index.js';
60
67
 
61
- const logger = console;
62
-
63
- /**
64
- * Load environment files
65
- * @internal Exported for testing
66
- */
67
- export function loadEnvFiles(
68
- envConfig: string | string[] | undefined,
69
- cwd: string = process.cwd(),
70
- ): { loaded: string[]; missing: string[] } {
71
- const loaded: string[] = [];
72
- const missing: string[] = [];
73
-
74
- // Normalize to array
75
- const envFiles = envConfig
76
- ? Array.isArray(envConfig)
77
- ? envConfig
78
- : [envConfig]
79
- : ['.env'];
80
-
81
- // Load each env file in order (later files override earlier)
82
- for (const envFile of envFiles) {
83
- const envPath = resolve(cwd, envFile);
84
- if (existsSync(envPath)) {
85
- dotenvConfig({ path: envPath, override: true, quiet: true });
86
- loaded.push(envFile);
87
- } else if (envConfig) {
88
- // Only report as missing if explicitly configured
89
- missing.push(envFile);
90
- }
91
- }
92
-
93
- return { loaded, missing };
94
- }
95
-
96
- /**
97
- * Check if a port is available
98
- * @internal Exported for testing
99
- */
100
- export async function isPortAvailable(port: number): Promise<boolean> {
101
- return new Promise((resolve) => {
102
- const server = createServer();
103
-
104
- server.once('error', (err: NodeJS.ErrnoException) => {
105
- if (err.code === 'EADDRINUSE') {
106
- resolve(false);
107
- } else {
108
- resolve(false);
109
- }
110
- });
111
-
112
- server.once('listening', () => {
113
- server.close();
114
- resolve(true);
115
- });
116
-
117
- server.listen(port);
118
- });
119
- }
120
-
121
- /**
122
- * Find an available port starting from the preferred port
123
- * @internal Exported for testing
124
- */
125
- export async function findAvailablePort(
126
- preferredPort: number,
127
- maxAttempts = 10,
128
- ): Promise<number> {
129
- for (let i = 0; i < maxAttempts; i++) {
130
- const port = preferredPort + i;
131
- if (await isPortAvailable(port)) {
132
- return port;
133
- }
134
- logger.log(`⚠️ Port ${port} is in use, trying ${port + 1}...`);
135
- }
136
-
137
- throw new Error(
138
- `Could not find an available port after trying ${maxAttempts} ports starting from ${preferredPort}`,
139
- );
140
- }
141
-
142
- /**
143
- * A port mapping extracted from docker-compose.yml.
144
- * Only entries using env var interpolation (e.g., `${VAR:-default}:container`) are captured.
145
- */
146
- export interface ComposePortMapping {
147
- service: string;
148
- envVar: string;
149
- defaultPort: number;
150
- containerPort: number;
151
- }
152
-
153
- /** Port state persisted to .gkm/ports.json, keyed by env var name. */
154
- export type PortState = Record<string, number>;
155
-
156
- export interface ResolvedServicePorts {
157
- dockerEnv: Record<string, string>;
158
- ports: PortState;
159
- mappings: ComposePortMapping[];
160
- }
161
-
162
- const PORT_STATE_PATH = '.gkm/ports.json';
163
-
164
- /**
165
- * Parse docker-compose.yml and extract all port mappings that use env var interpolation.
166
- * Entries like `'${POSTGRES_HOST_PORT:-5432}:5432'` are captured.
167
- * Fixed port mappings like `'5050:80'` are skipped.
168
- * @internal Exported for testing
169
- */
170
- export function parseComposePortMappings(
171
- composePath: string,
172
- ): ComposePortMapping[] {
173
- if (!existsSync(composePath)) {
174
- return [];
175
- }
176
-
177
- const content = readFileSync(composePath, 'utf-8');
178
- const compose = parseYaml(content) as {
179
- services?: Record<string, { ports?: string[] }>;
180
- };
181
-
182
- if (!compose?.services) {
183
- return [];
184
- }
185
-
186
- const results: ComposePortMapping[] = [];
187
-
188
- for (const [serviceName, serviceConfig] of Object.entries(compose.services)) {
189
- for (const portMapping of serviceConfig?.ports ?? []) {
190
- const match = String(portMapping).match(/\$\{(\w+):-(\d+)\}:(\d+)/);
191
- if (match?.[1] && match[2] && match[3]) {
192
- results.push({
193
- service: serviceName,
194
- envVar: match[1],
195
- defaultPort: Number(match[2]),
196
- containerPort: Number(match[3]),
197
- });
198
- }
199
- }
200
- }
201
-
202
- return results;
203
- }
204
-
205
- /**
206
- * Load saved port state from .gkm/ports.json.
207
- * @internal Exported for testing
208
- */
209
- export async function loadPortState(workspaceRoot: string): Promise<PortState> {
210
- try {
211
- const raw = await readFile(join(workspaceRoot, PORT_STATE_PATH), 'utf-8');
212
- return JSON.parse(raw) as PortState;
213
- } catch {
214
- return {};
215
- }
216
- }
217
-
218
- /**
219
- * Save port state to .gkm/ports.json.
220
- * @internal Exported for testing
221
- */
222
- export async function savePortState(
223
- workspaceRoot: string,
224
- ports: PortState,
225
- ): Promise<void> {
226
- const dir = join(workspaceRoot, '.gkm');
227
- await mkdir(dir, { recursive: true });
228
- await writeFile(
229
- join(workspaceRoot, PORT_STATE_PATH),
230
- `${JSON.stringify(ports, null, 2)}\n`,
231
- );
232
- }
233
-
234
- /**
235
- * Check if a project's own Docker container is running and return its host port.
236
- * Uses `docker compose port` scoped to the project's compose file.
237
- * @internal Exported for testing
238
- */
239
- export function getContainerHostPort(
240
- workspaceRoot: string,
241
- service: string,
242
- containerPort: number,
243
- ): number | null {
244
- try {
245
- const result = execSync(`docker compose port ${service} ${containerPort}`, {
246
- cwd: workspaceRoot,
247
- stdio: 'pipe',
248
- })
249
- .toString()
250
- .trim();
251
- const match = result.match(/:(\d+)$/);
252
- return match ? Number(match[1]) : null;
253
- } catch {
254
- return null;
255
- }
256
- }
68
+ // Re-export shared utilities from credentials module so existing imports
69
+ // from '../dev' or '../dev/index' continue to work.
70
+ export {
71
+ buildDockerComposeEnv,
72
+ type ComposePortMapping,
73
+ createCredentialsPreload,
74
+ createEntryWrapper,
75
+ type EntryCredentialsResult,
76
+ findAvailablePort,
77
+ findSecretsRoot,
78
+ getContainerHostPort,
79
+ isPortAvailable,
80
+ loadEnvFiles,
81
+ loadPortState,
82
+ loadSecretsForApp,
83
+ type PortState,
84
+ parseComposePortMappings,
85
+ parseComposeServiceNames,
86
+ prepareEntryCredentials,
87
+ type ResolvedServicePorts,
88
+ replacePortInUrl,
89
+ resolveServicePorts,
90
+ rewriteUrlsWithPorts,
91
+ savePortState,
92
+ startComposeServices,
93
+ startWorkspaceServices,
94
+ } from '../credentials';
95
+
96
+ // Re-export execCommand from its own module
97
+ export { type ExecOptions, execCommand } from '../exec';
257
98
 
258
- /**
259
- * Resolve host ports for Docker services by parsing docker-compose.yml.
260
- * Priority: running container → saved state → find available port.
261
- * Persists resolved ports to .gkm/ports.json.
262
- * @internal Exported for testing
263
- */
264
- export async function resolveServicePorts(
265
- workspaceRoot: string,
266
- ): Promise<ResolvedServicePorts> {
267
- const composePath = join(workspaceRoot, 'docker-compose.yml');
268
- const mappings = parseComposePortMappings(composePath);
269
-
270
- if (mappings.length === 0) {
271
- return { dockerEnv: {}, ports: {}, mappings: [] };
272
- }
273
-
274
- const savedState = await loadPortState(workspaceRoot);
275
- const dockerEnv: Record<string, string> = {};
276
- const ports: PortState = {};
277
- // Track ports assigned in this cycle to avoid duplicates
278
- const assignedPorts = new Set<number>();
279
-
280
- logger.log('\n🔌 Resolving service ports...');
281
-
282
- for (const mapping of mappings) {
283
- // 1. Check if own container is already running
284
- const containerPort = getContainerHostPort(
285
- workspaceRoot,
286
- mapping.service,
287
- mapping.containerPort,
288
- );
289
- if (containerPort !== null) {
290
- ports[mapping.envVar] = containerPort;
291
- dockerEnv[mapping.envVar] = String(containerPort);
292
- assignedPorts.add(containerPort);
293
- logger.log(
294
- ` 🔄 ${mapping.service}:${mapping.containerPort}: reusing existing container on port ${containerPort}`,
295
- );
296
- continue;
297
- }
298
-
299
- // 2. Check saved port state
300
- const savedPort = savedState[mapping.envVar];
301
- if (
302
- savedPort &&
303
- !assignedPorts.has(savedPort) &&
304
- (await isPortAvailable(savedPort))
305
- ) {
306
- ports[mapping.envVar] = savedPort;
307
- dockerEnv[mapping.envVar] = String(savedPort);
308
- assignedPorts.add(savedPort);
309
- logger.log(
310
- ` 💾 ${mapping.service}:${mapping.containerPort}: using saved port ${savedPort}`,
311
- );
312
- continue;
313
- }
314
-
315
- // 3. Find available port (skipping ports already assigned this cycle)
316
- let resolvedPort = await findAvailablePort(mapping.defaultPort);
317
- while (assignedPorts.has(resolvedPort)) {
318
- resolvedPort = await findAvailablePort(resolvedPort + 1);
319
- }
320
- ports[mapping.envVar] = resolvedPort;
321
- dockerEnv[mapping.envVar] = String(resolvedPort);
322
- assignedPorts.add(resolvedPort);
323
-
324
- if (resolvedPort !== mapping.defaultPort) {
325
- logger.log(
326
- ` ⚡ ${mapping.service}:${mapping.containerPort}: port ${mapping.defaultPort} occupied, using port ${resolvedPort}`,
327
- );
328
- } else {
329
- logger.log(
330
- ` ✅ ${mapping.service}:${mapping.containerPort}: using default port ${resolvedPort}`,
331
- );
332
- }
333
- }
334
-
335
- await savePortState(workspaceRoot, ports);
336
-
337
- return { dockerEnv, ports, mappings };
338
- }
339
-
340
- /**
341
- * Replace a port in a URL string.
342
- * Handles both `hostname:port` and `localhost:port` patterns.
343
- * @internal Exported for testing
344
- */
345
- export function replacePortInUrl(
346
- url: string,
347
- oldPort: number,
348
- newPort: number,
349
- ): string {
350
- if (oldPort === newPort) return url;
351
- return url.replace(new RegExp(`:${oldPort}(?=/|$)`, 'g'), `:${newPort}`);
352
- }
353
-
354
- /**
355
- * Rewrite connection URLs and port vars in secrets with resolved ports.
356
- * Uses the parsed compose mappings to determine which default ports to replace.
357
- * Pure transform — does not modify secrets on disk.
358
- * @internal Exported for testing
359
- */
360
- export function rewriteUrlsWithPorts(
361
- secrets: Record<string, string>,
362
- resolvedPorts: ResolvedServicePorts,
363
- ): Record<string, string> {
364
- const { ports, mappings } = resolvedPorts;
365
- const result = { ...secrets };
366
-
367
- // Build a map of defaultPort → resolvedPort for all changed ports
368
- const portReplacements: { defaultPort: number; resolvedPort: number }[] = [];
369
- // Collect Docker service names for hostname rewriting
370
- const serviceNames = new Set<string>();
371
- for (const mapping of mappings) {
372
- serviceNames.add(mapping.service);
373
- const resolved = ports[mapping.envVar];
374
- if (resolved !== undefined) {
375
- portReplacements.push({
376
- defaultPort: mapping.defaultPort,
377
- resolvedPort: resolved,
378
- });
379
- }
380
- }
381
-
382
- // Rewrite _HOST env vars that use Docker service names
383
- for (const [key, value] of Object.entries(result)) {
384
- if (!key.endsWith('_HOST')) continue;
385
- if (serviceNames.has(value)) {
386
- result[key] = 'localhost';
387
- }
388
- }
389
-
390
- // Rewrite _PORT env vars whose values match a default port
391
- for (const [key, value] of Object.entries(result)) {
392
- if (!key.endsWith('_PORT')) continue;
393
- for (const { defaultPort, resolvedPort } of portReplacements) {
394
- if (value === String(defaultPort)) {
395
- result[key] = String(resolvedPort);
396
- }
397
- }
398
- }
399
-
400
- // Rewrite URLs: replace Docker service hostnames with localhost and fix ports
401
- for (const [key, value] of Object.entries(result)) {
402
- if (
403
- !key.endsWith('_URL') &&
404
- !key.endsWith('_ENDPOINT') &&
405
- key !== 'DATABASE_URL'
406
- )
407
- continue;
408
-
409
- let rewritten = value;
410
- for (const name of serviceNames) {
411
- rewritten = rewritten.replace(
412
- new RegExp(`@${name}:`, 'g'),
413
- '@localhost:',
414
- );
415
- }
416
- for (const { defaultPort, resolvedPort } of portReplacements) {
417
- rewritten = replacePortInUrl(rewritten, defaultPort, resolvedPort);
418
- }
419
- result[key] = rewritten;
420
- }
421
-
422
- return result;
423
- }
99
+ const logger = console;
424
100
 
425
101
  /**
426
102
  * Normalize telescope configuration
@@ -1080,165 +756,6 @@ export async function loadDevSecrets(
1080
756
  return {};
1081
757
  }
1082
758
 
1083
- /**
1084
- * Load secrets from a path for dev mode.
1085
- * For single app: returns secrets as-is.
1086
- * For workspace app: maps {APP}_DATABASE_URL → DATABASE_URL.
1087
- * @internal Exported for testing
1088
- */
1089
- export async function loadSecretsForApp(
1090
- secretsRoot: string,
1091
- appName?: string,
1092
- ): Promise<Record<string, string>> {
1093
- // Try 'dev' stage first, then 'development'
1094
- const stages = ['dev', 'development'];
1095
-
1096
- let secrets: Record<string, string> = {};
1097
-
1098
- for (const stage of stages) {
1099
- if (secretsExist(stage, secretsRoot)) {
1100
- const stageSecrets = await readStageSecrets(stage, secretsRoot);
1101
- if (stageSecrets) {
1102
- logger.log(`🔐 Loading secrets from stage: ${stage}`);
1103
- secrets = toEmbeddableSecrets(stageSecrets);
1104
- break;
1105
- }
1106
- }
1107
- }
1108
-
1109
- if (Object.keys(secrets).length === 0) {
1110
- return {};
1111
- }
1112
-
1113
- // Single app mode - no mapping needed
1114
- if (!appName) {
1115
- return secrets;
1116
- }
1117
-
1118
- // Workspace app mode - map {APP}_* to generic names
1119
- const prefix = appName.toUpperCase();
1120
- const mapped = { ...secrets };
1121
-
1122
- // Map {APP}_DATABASE_URL → DATABASE_URL
1123
- const appDbUrl = secrets[`${prefix}_DATABASE_URL`];
1124
- if (appDbUrl) {
1125
- mapped.DATABASE_URL = appDbUrl;
1126
- }
1127
-
1128
- return mapped;
1129
- }
1130
-
1131
- /**
1132
- * Build the environment variables to pass to `docker compose up`.
1133
- * Merges process.env, secrets, and port mappings so that Docker Compose
1134
- * can interpolate variables like ${POSTGRES_USER} correctly.
1135
- * @internal Exported for testing
1136
- */
1137
- export function buildDockerComposeEnv(
1138
- secretsEnv?: Record<string, string>,
1139
- portEnv?: Record<string, string>,
1140
- ): Record<string, string | undefined> {
1141
- return { ...process.env, ...secretsEnv, ...portEnv };
1142
- }
1143
-
1144
- /**
1145
- * Parse all service names from a docker-compose.yml file.
1146
- * @internal Exported for testing
1147
- */
1148
- export function parseComposeServiceNames(composePath: string): string[] {
1149
- if (!existsSync(composePath)) {
1150
- return [];
1151
- }
1152
-
1153
- const content = readFileSync(composePath, 'utf-8');
1154
- const compose = parseYaml(content) as {
1155
- services?: Record<string, unknown>;
1156
- };
1157
-
1158
- return Object.keys(compose?.services ?? {});
1159
- }
1160
-
1161
- /**
1162
- * Start docker-compose services for the workspace.
1163
- * Parses the docker-compose.yml to discover all services and starts
1164
- * everything except app services (which are managed by turbo).
1165
- * This ensures manually added services are always started.
1166
- * @internal Exported for testing
1167
- */
1168
- /**
1169
- * Start docker-compose services for a single-app project (no workspace config).
1170
- * Starts all services defined in docker-compose.yml.
1171
- */
1172
- export async function startComposeServices(
1173
- cwd: string,
1174
- portEnv?: Record<string, string>,
1175
- secretsEnv?: Record<string, string>,
1176
- ): Promise<void> {
1177
- const composeFile = join(cwd, 'docker-compose.yml');
1178
- if (!existsSync(composeFile)) {
1179
- return;
1180
- }
1181
-
1182
- const servicesToStart = parseComposeServiceNames(composeFile);
1183
- if (servicesToStart.length === 0) {
1184
- return;
1185
- }
1186
-
1187
- logger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);
1188
-
1189
- try {
1190
- execSync(`docker compose up -d ${servicesToStart.join(' ')}`, {
1191
- cwd,
1192
- stdio: 'inherit',
1193
- env: buildDockerComposeEnv(secretsEnv, portEnv),
1194
- });
1195
-
1196
- logger.log('✅ Services started');
1197
- } catch (error) {
1198
- logger.error('❌ Failed to start services:', (error as Error).message);
1199
- throw error;
1200
- }
1201
- }
1202
-
1203
- export async function startWorkspaceServices(
1204
- workspace: NormalizedWorkspace,
1205
- portEnv?: Record<string, string>,
1206
- secretsEnv?: Record<string, string>,
1207
- ): Promise<void> {
1208
- const composeFile = join(workspace.root, 'docker-compose.yml');
1209
- if (!existsSync(composeFile)) {
1210
- return;
1211
- }
1212
-
1213
- // Discover all services from docker-compose.yml
1214
- const allServices = parseComposeServiceNames(composeFile);
1215
-
1216
- // Exclude app services (managed by turbo, not docker)
1217
- const appNames = new Set(Object.keys(workspace.apps));
1218
- const servicesToStart = allServices.filter((name) => !appNames.has(name));
1219
-
1220
- if (servicesToStart.length === 0) {
1221
- return;
1222
- }
1223
-
1224
- logger.log(`🐳 Starting services: ${servicesToStart.join(', ')}`);
1225
-
1226
- try {
1227
- // Start services with docker-compose, passing secrets so that
1228
- // POSTGRES_USER, POSTGRES_PASSWORD, etc. are interpolated correctly
1229
- execSync(`docker compose up -d ${servicesToStart.join(' ')}`, {
1230
- cwd: workspace.root,
1231
- stdio: 'inherit',
1232
- env: buildDockerComposeEnv(secretsEnv, portEnv),
1233
- });
1234
-
1235
- logger.log('✅ Services started');
1236
- } catch (error) {
1237
- logger.error('❌ Failed to start services:', (error as Error).message);
1238
- throw error;
1239
- }
1240
- }
1241
-
1242
759
  /**
1243
760
  * Workspace dev command - orchestrates multi-app development using Turbo.
1244
761
  *
@@ -1599,163 +1116,6 @@ async function buildServer(
1599
1116
  ]);
1600
1117
  }
1601
1118
 
1602
- /**
1603
- * Find the directory containing .gkm/secrets/.
1604
- * Walks up from cwd until it finds one, or returns cwd.
1605
- * @internal Exported for testing
1606
- */
1607
- export function findSecretsRoot(startDir: string): string {
1608
- let dir = startDir;
1609
- while (dir !== '/') {
1610
- if (existsSync(join(dir, '.gkm', 'secrets'))) {
1611
- return dir;
1612
- }
1613
- const parent = dirname(dir);
1614
- if (parent === dir) break;
1615
- dir = parent;
1616
- }
1617
- return startDir;
1618
- }
1619
-
1620
- /**
1621
- * Generate the credentials injection code snippet.
1622
- * This is the common logic used by both entry wrapper and exec preload.
1623
- * @internal
1624
- */
1625
- function generateCredentialsInjection(secretsJsonPath: string): string {
1626
- return `import { existsSync, readFileSync } from 'node:fs';
1627
-
1628
- // Inject dev secrets via globalThis and process.env
1629
- // Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
1630
- // Object.assign on the Credentials export only mutates one module copy.
1631
- const secretsPath = '${secretsJsonPath}';
1632
- if (existsSync(secretsPath)) {
1633
- const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1634
- globalThis.__gkm_credentials__ = secrets;
1635
- Object.assign(process.env, secrets);
1636
- }
1637
- `;
1638
- }
1639
-
1640
- /**
1641
- * Create a preload script that injects secrets into Credentials.
1642
- * Used by `gkm exec` to inject secrets before running any command.
1643
- * @internal Exported for testing
1644
- */
1645
- export async function createCredentialsPreload(
1646
- preloadPath: string,
1647
- secretsJsonPath: string,
1648
- ): Promise<void> {
1649
- const content = `/**
1650
- * Credentials preload generated by 'gkm exec'
1651
- * This file is loaded via NODE_OPTIONS="--import <path>"
1652
- */
1653
- ${generateCredentialsInjection(secretsJsonPath)}`;
1654
-
1655
- await writeFile(preloadPath, content);
1656
- }
1657
-
1658
- /**
1659
- * Create a wrapper script that injects secrets before importing the entry file.
1660
- * @internal Exported for testing
1661
- */
1662
- export async function createEntryWrapper(
1663
- wrapperPath: string,
1664
- entryPath: string,
1665
- secretsJsonPath?: string,
1666
- ): Promise<void> {
1667
- const credentialsInjection = secretsJsonPath
1668
- ? `${generateCredentialsInjection(secretsJsonPath)}
1669
- `
1670
- : '';
1671
-
1672
- // Use dynamic import() to ensure secrets are assigned before the entry file loads
1673
- // Static imports are hoisted, so Object.assign would run after the entry file is loaded
1674
- const content = `#!/usr/bin/env node
1675
- /**
1676
- * Entry wrapper generated by 'gkm dev --entry'
1677
- */
1678
- ${credentialsInjection}// Import and run the user's entry file (dynamic import ensures secrets load first)
1679
- await import('${entryPath}');
1680
- `;
1681
-
1682
- await writeFile(wrapperPath, content);
1683
- }
1684
-
1685
- /**
1686
- * Result of preparing entry credentials for dev mode.
1687
- */
1688
- export interface EntryCredentialsResult {
1689
- /** Credentials to inject (secrets + PORT) */
1690
- credentials: Record<string, string>;
1691
- /** Resolved port (from --port, workspace config, or default 3000) */
1692
- resolvedPort: number;
1693
- /** Path where credentials JSON was written */
1694
- secretsJsonPath: string;
1695
- /** Resolved app name (if in workspace) */
1696
- appName: string | undefined;
1697
- /** Secrets root directory */
1698
- secretsRoot: string;
1699
- }
1700
-
1701
- /**
1702
- * Prepare credentials for entry dev mode.
1703
- * Loads workspace config, secrets, and injects PORT.
1704
- * @internal Exported for testing
1705
- */
1706
- export async function prepareEntryCredentials(options: {
1707
- explicitPort?: number;
1708
- cwd?: string;
1709
- }): Promise<EntryCredentialsResult> {
1710
- const cwd = options.cwd ?? process.cwd();
1711
-
1712
- // Try to get workspace app config for port and secrets
1713
- let workspaceAppPort: number | undefined;
1714
- let secretsRoot: string = cwd;
1715
- let appName: string | undefined;
1716
-
1717
- try {
1718
- const appInfo = await loadWorkspaceAppInfo(cwd);
1719
- workspaceAppPort = appInfo.app.port;
1720
- secretsRoot = appInfo.workspaceRoot;
1721
- appName = appInfo.appName;
1722
- } catch (error) {
1723
- // Not in a workspace - use defaults
1724
- logger.log(
1725
- `⚠️ Could not load workspace config: ${(error as Error).message}`,
1726
- );
1727
- secretsRoot = findSecretsRoot(cwd);
1728
- appName = getAppNameFromCwd(cwd) ?? undefined;
1729
- }
1730
-
1731
- // Determine port: explicit --port > workspace config > default 3000
1732
- const resolvedPort = options.explicitPort ?? workspaceAppPort ?? 3000;
1733
-
1734
- // Load secrets and inject PORT
1735
- const credentials = await loadSecretsForApp(secretsRoot, appName);
1736
-
1737
- // Always inject PORT into credentials so apps can read it
1738
- credentials.PORT = String(resolvedPort);
1739
-
1740
- // Write secrets to temp JSON file (always write since we have PORT)
1741
- // Use app-specific filename to avoid race conditions when running multiple apps via turbo
1742
- const secretsDir = join(secretsRoot, '.gkm');
1743
- await mkdir(secretsDir, { recursive: true });
1744
- const secretsFileName = appName
1745
- ? `dev-secrets-${appName}.json`
1746
- : 'dev-secrets.json';
1747
- const secretsJsonPath = join(secretsDir, secretsFileName);
1748
- await writeFile(secretsJsonPath, JSON.stringify(credentials, null, 2));
1749
-
1750
- return {
1751
- credentials,
1752
- resolvedPort,
1753
- secretsJsonPath,
1754
- appName,
1755
- secretsRoot,
1756
- };
1757
- }
1758
-
1759
1119
  /**
1760
1120
  * Run any TypeScript file with secret injection.
1761
1121
  * Does not require gkm.config.ts.
@@ -2194,140 +1554,3 @@ class DevServer {
2194
1554
  await fsWriteFile(serverPath, content);
2195
1555
  }
2196
1556
  }
2197
-
2198
- /**
2199
- * Options for the exec command.
2200
- */
2201
- export interface ExecOptions {
2202
- /** Working directory */
2203
- cwd?: string;
2204
- }
2205
-
2206
- /**
2207
- * Run a command with secrets injected into Credentials.
2208
- * Uses Node's --import flag to preload a script that populates Credentials
2209
- * before the command loads any modules that depend on them.
2210
- *
2211
- * @example
2212
- * ```bash
2213
- * gkm exec -- npx @better-auth/cli migrate
2214
- * gkm exec -- npx prisma migrate dev
2215
- * ```
2216
- */
2217
- export async function execCommand(
2218
- commandArgs: string[],
2219
- options: ExecOptions = {},
2220
- ): Promise<void> {
2221
- const cwd = options.cwd ?? process.cwd();
2222
-
2223
- if (commandArgs.length === 0) {
2224
- throw new Error('No command specified. Usage: gkm exec -- <command>');
2225
- }
2226
-
2227
- // Load .env files
2228
- const defaultEnv = loadEnvFiles('.env');
2229
- if (defaultEnv.loaded.length > 0) {
2230
- logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
2231
- }
2232
-
2233
- // Prepare credentials (loads workspace config and secrets)
2234
- // Don't inject PORT for exec since we're not running a server
2235
- const { credentials, secretsJsonPath, appName, secretsRoot } =
2236
- await prepareEntryCredentials({ cwd });
2237
-
2238
- if (appName) {
2239
- logger.log(`📦 App: ${appName}`);
2240
- }
2241
-
2242
- const secretCount = Object.keys(credentials).filter(
2243
- (k) => k !== 'PORT',
2244
- ).length;
2245
- if (secretCount > 0) {
2246
- logger.log(`🔐 Loaded ${secretCount} secret(s)`);
2247
- }
2248
-
2249
- // Rewrite URLs with resolved Docker ports (from gkm dev)
2250
- const composePath = join(secretsRoot, 'docker-compose.yml');
2251
- const mappings = parseComposePortMappings(composePath);
2252
- if (mappings.length > 0) {
2253
- const ports = await loadPortState(secretsRoot);
2254
- if (Object.keys(ports).length > 0) {
2255
- const rewritten = rewriteUrlsWithPorts(credentials, {
2256
- dockerEnv: {},
2257
- ports,
2258
- mappings,
2259
- });
2260
- Object.assign(credentials, rewritten);
2261
- logger.log(`🔌 Applied ${Object.keys(ports).length} port mapping(s)`);
2262
- }
2263
- }
2264
-
2265
- // Inject dependency URLs (works for both frontend and backend apps)
2266
- try {
2267
- const appInfo = await loadWorkspaceAppInfo(cwd);
2268
- if (appInfo.appName) {
2269
- const depEnv = getDependencyEnvVars(appInfo.workspace, appInfo.appName);
2270
- Object.assign(credentials, depEnv);
2271
- }
2272
- } catch {
2273
- // Not in a workspace — skip dependency URL injection
2274
- }
2275
-
2276
- // Create preload script that injects Credentials
2277
- // Create in cwd so package resolution works (finds node_modules in app directory)
2278
- const preloadDir = join(cwd, '.gkm');
2279
- await mkdir(preloadDir, { recursive: true });
2280
- const preloadPath = join(preloadDir, 'credentials-preload.ts');
2281
- await createCredentialsPreload(preloadPath, secretsJsonPath);
2282
-
2283
- // Build command
2284
- const [cmd, ...rawArgs] = commandArgs;
2285
-
2286
- if (!cmd) {
2287
- throw new Error('No command specified');
2288
- }
2289
-
2290
- // Replace template variables in command args (e.g. $PORT -> resolved port)
2291
- const args = rawArgs.map((arg) =>
2292
- arg.replace(/\$PORT\b/g, credentials.PORT ?? '3000'),
2293
- );
2294
-
2295
- logger.log(`🚀 Running: ${[cmd, ...args].join(' ')}`);
2296
-
2297
- // Merge NODE_OPTIONS with existing value (if any)
2298
- // Add tsx loader first so our .ts preload can be loaded
2299
- const existingNodeOptions = process.env.NODE_OPTIONS ?? '';
2300
- const tsxImport = '--import=tsx';
2301
- const preloadImport = `--import=${preloadPath}`;
2302
-
2303
- // Build NODE_OPTIONS: existing + tsx loader + our preload
2304
- const nodeOptions = [existingNodeOptions, tsxImport, preloadImport]
2305
- .filter(Boolean)
2306
- .join(' ');
2307
-
2308
- // Spawn the command with secrets in both:
2309
- // 1. Environment variables (for tools that read process.env directly)
2310
- // 2. Preload script (for tools that use Credentials object)
2311
- const child = spawn(cmd, args, {
2312
- cwd,
2313
- stdio: 'inherit',
2314
- env: {
2315
- ...process.env,
2316
- ...credentials, // Inject secrets as env vars
2317
- NODE_OPTIONS: nodeOptions,
2318
- },
2319
- });
2320
-
2321
- // Wait for the command to complete
2322
- const exitCode = await new Promise<number>((resolve) => {
2323
- child.on('close', (code: number | null) => resolve(code ?? 0));
2324
- child.on('error', (error: Error) => {
2325
- logger.error(`Failed to run command: ${error.message}`);
2326
- resolve(1);
2327
- });
2328
- });
2329
-
2330
- if (exitCode !== 0) {
2331
- process.exit(exitCode);
2332
- }
2333
- }