@geekmidas/cli 0.37.0 → 0.39.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/index.mjs CHANGED
@@ -26,7 +26,7 @@ import prompts from "prompts";
26
26
 
27
27
  //#region package.json
28
28
  var name = "@geekmidas/cli";
29
- var version = "0.37.0";
29
+ var version = "0.39.0";
30
30
  var description = "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs";
31
31
  var private$1 = false;
32
32
  var type = "module";
@@ -1541,19 +1541,43 @@ function findSecretsRoot(startDir) {
1541
1541
  return startDir;
1542
1542
  }
1543
1543
  /**
1544
- * Create a wrapper script that injects secrets before importing the entry file.
1545
- * @internal Exported for testing
1544
+ * Generate the credentials injection code snippet.
1545
+ * This is the common logic used by both entry wrapper and exec preload.
1546
+ * @internal
1546
1547
  */
1547
- async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
1548
- const credentialsInjection = secretsJsonPath ? `import { Credentials } from '@geekmidas/envkit/credentials';
1548
+ function generateCredentialsInjection(secretsJsonPath) {
1549
+ return `import { Credentials } from '@geekmidas/envkit/credentials';
1549
1550
  import { existsSync, readFileSync } from 'node:fs';
1550
1551
 
1551
- // Inject dev secrets into Credentials (before app import)
1552
+ // Inject dev secrets into Credentials
1552
1553
  const secretsPath = '${secretsJsonPath}';
1553
1554
  if (existsSync(secretsPath)) {
1554
- Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
1555
+ const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
1556
+ Object.assign(Credentials, secrets);
1557
+ // Debug: uncomment to verify preload is running
1558
+ // console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
1555
1559
  }
1556
-
1560
+ `;
1561
+ }
1562
+ /**
1563
+ * Create a preload script that injects secrets into Credentials.
1564
+ * Used by `gkm exec` to inject secrets before running any command.
1565
+ * @internal Exported for testing
1566
+ */
1567
+ async function createCredentialsPreload(preloadPath, secretsJsonPath) {
1568
+ const content = `/**
1569
+ * Credentials preload generated by 'gkm exec'
1570
+ * This file is loaded via NODE_OPTIONS="--import <path>"
1571
+ */
1572
+ ${generateCredentialsInjection(secretsJsonPath)}`;
1573
+ await writeFile(preloadPath, content);
1574
+ }
1575
+ /**
1576
+ * Create a wrapper script that injects secrets before importing the entry file.
1577
+ * @internal Exported for testing
1578
+ */
1579
+ async function createEntryWrapper(wrapperPath, entryPath, secretsJsonPath) {
1580
+ const credentialsInjection = secretsJsonPath ? `${generateCredentialsInjection(secretsJsonPath)}
1557
1581
  ` : "";
1558
1582
  const content = `#!/usr/bin/env node
1559
1583
  /**
@@ -1579,7 +1603,8 @@ async function prepareEntryCredentials(options) {
1579
1603
  workspaceAppPort = appConfig.app.port;
1580
1604
  secretsRoot = appConfig.workspaceRoot;
1581
1605
  appName = appConfig.appName;
1582
- } catch {
1606
+ } catch (error) {
1607
+ logger$8.log(`⚠️ Could not load workspace config: ${error.message}`);
1583
1608
  secretsRoot = findSecretsRoot(cwd);
1584
1609
  appName = getAppNameFromCwd(cwd) ?? void 0;
1585
1610
  }
@@ -1609,7 +1634,7 @@ async function entryDevCommand(options) {
1609
1634
  if (!existsSync(entryPath)) throw new Error(`Entry file not found: ${entryPath}`);
1610
1635
  const defaultEnv = loadEnvFiles(".env");
1611
1636
  if (defaultEnv.loaded.length > 0) logger$8.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1612
- const { credentials, resolvedPort, secretsJsonPath, appName } = await prepareEntryCredentials({ explicitPort: options.port });
1637
+ const { credentials, resolvedPort, secretsJsonPath, appName } = await prepareEntryCredentials({ explicitPort: options.portExplicit ? options.port : void 0 });
1613
1638
  if (appName) logger$8.log(`📦 App: ${appName} (port ${resolvedPort})`);
1614
1639
  logger$8.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
1615
1640
  if (Object.keys(credentials).length > 1) logger$8.log(`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`);
@@ -1858,6 +1883,59 @@ start({
1858
1883
  await fsWriteFile(serverPath, content);
1859
1884
  }
1860
1885
  };
1886
+ /**
1887
+ * Run a command with secrets injected into Credentials.
1888
+ * Uses Node's --import flag to preload a script that populates Credentials
1889
+ * before the command loads any modules that depend on them.
1890
+ *
1891
+ * @example
1892
+ * ```bash
1893
+ * gkm exec -- npx @better-auth/cli migrate
1894
+ * gkm exec -- npx prisma migrate dev
1895
+ * ```
1896
+ */
1897
+ async function execCommand(commandArgs, options = {}) {
1898
+ const cwd = options.cwd ?? process.cwd();
1899
+ if (commandArgs.length === 0) throw new Error("No command specified. Usage: gkm exec -- <command>");
1900
+ const defaultEnv = loadEnvFiles(".env");
1901
+ if (defaultEnv.loaded.length > 0) logger$8.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
1902
+ const { credentials, secretsJsonPath, appName } = await prepareEntryCredentials({ cwd });
1903
+ if (appName) logger$8.log(`📦 App: ${appName}`);
1904
+ const secretCount = Object.keys(credentials).filter((k) => k !== "PORT").length;
1905
+ if (secretCount > 0) logger$8.log(`🔐 Loaded ${secretCount} secret(s)`);
1906
+ const preloadDir = join(cwd, ".gkm");
1907
+ await mkdir(preloadDir, { recursive: true });
1908
+ const preloadPath = join(preloadDir, "credentials-preload.ts");
1909
+ await createCredentialsPreload(preloadPath, secretsJsonPath);
1910
+ const [cmd, ...args] = commandArgs;
1911
+ if (!cmd) throw new Error("No command specified");
1912
+ logger$8.log(`🚀 Running: ${commandArgs.join(" ")}`);
1913
+ const existingNodeOptions = process.env.NODE_OPTIONS ?? "";
1914
+ const tsxImport = "--import tsx";
1915
+ const preloadImport = `--import ${preloadPath}`;
1916
+ const nodeOptions = [
1917
+ existingNodeOptions,
1918
+ tsxImport,
1919
+ preloadImport
1920
+ ].filter(Boolean).join(" ");
1921
+ const child = spawn(cmd, args, {
1922
+ cwd,
1923
+ stdio: "inherit",
1924
+ env: {
1925
+ ...process.env,
1926
+ ...credentials,
1927
+ NODE_OPTIONS: nodeOptions
1928
+ }
1929
+ });
1930
+ const exitCode = await new Promise((resolve$1) => {
1931
+ child.on("close", (code) => resolve$1(code ?? 0));
1932
+ child.on("error", (error) => {
1933
+ logger$8.error(`Failed to run command: ${error.message}`);
1934
+ resolve$1(1);
1935
+ });
1936
+ });
1937
+ if (exitCode !== 0) process.exit(exitCode);
1938
+ }
1861
1939
 
1862
1940
  //#endregion
1863
1941
  //#region src/build/manifests.ts
@@ -1928,10 +2006,15 @@ const logger$6 = console;
1928
2006
  async function buildCommand(options) {
1929
2007
  const loadedConfig = await loadWorkspaceConfig();
1930
2008
  if (loadedConfig.type === "workspace") {
1931
- logger$6.log("📦 Detected workspace configuration");
1932
- return workspaceBuildCommand(loadedConfig.workspace, options);
2009
+ const cwd = resolve(process.cwd());
2010
+ const workspaceRoot = resolve(loadedConfig.workspace.root);
2011
+ const isAtWorkspaceRoot = cwd === workspaceRoot;
2012
+ if (isAtWorkspaceRoot) {
2013
+ logger$6.log("📦 Detected workspace configuration");
2014
+ return workspaceBuildCommand(loadedConfig.workspace, options);
2015
+ }
1933
2016
  }
1934
- const config$1 = await loadConfig();
2017
+ const config$1 = loadedConfig.type === "workspace" ? (await loadAppConfig()).gkmConfig : await loadConfig();
1935
2018
  const resolved = resolveProviders(config$1, options);
1936
2019
  const productionConfigFromGkm = getProductionConfigFromGkm(config$1);
1937
2020
  const production = normalizeProductionConfig(options.production ?? false, productionConfigFromGkm);
@@ -3375,26 +3458,22 @@ function getImageRef(registry, imageName, tag) {
3375
3458
  }
3376
3459
  /**
3377
3460
  * Build Docker image
3461
+ * @param imageRef - Full image reference (registry/name:tag)
3462
+ * @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
3378
3463
  */
3379
- async function buildImage(imageRef) {
3464
+ async function buildImage(imageRef, appName) {
3380
3465
  logger$4.log(`\n🔨 Building Docker image: ${imageRef}`);
3381
3466
  const cwd = process.cwd();
3382
- const inMonorepo = isMonorepo(cwd);
3383
- if (inMonorepo) logger$4.log(" Generating Dockerfile for monorepo (turbo prune)...");
3467
+ const lockfilePath = findLockfilePath(cwd);
3468
+ const lockfileDir = lockfilePath ? dirname(lockfilePath) : cwd;
3469
+ const inMonorepo = lockfileDir !== cwd;
3470
+ if (appName || inMonorepo) logger$4.log(" Generating Dockerfile for monorepo (turbo prune)...");
3384
3471
  else logger$4.log(" Generating Dockerfile...");
3385
3472
  await dockerCommand({});
3386
- let buildCwd = cwd;
3387
- let dockerfilePath = ".gkm/docker/Dockerfile";
3388
- if (inMonorepo) {
3389
- const lockfilePath = findLockfilePath(cwd);
3390
- if (lockfilePath) {
3391
- const monorepoRoot = dirname(lockfilePath);
3392
- const appRelPath = relative(monorepoRoot, cwd);
3393
- dockerfilePath = join(appRelPath, ".gkm/docker/Dockerfile");
3394
- buildCwd = monorepoRoot;
3395
- logger$4.log(` Building from monorepo root: ${monorepoRoot}`);
3396
- }
3397
- }
3473
+ const dockerfileSuffix = appName ? `.${appName}` : "";
3474
+ const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
3475
+ const buildCwd = lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
3476
+ if (buildCwd !== cwd) logger$4.log(` Building from workspace root: ${buildCwd}`);
3398
3477
  try {
3399
3478
  execSync(`DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -f ${dockerfilePath} -t ${imageRef} .`, {
3400
3479
  cwd: buildCwd,
@@ -3431,7 +3510,7 @@ async function deployDocker(options) {
3431
3510
  const { stage, tag, skipPush, masterKey, config: config$1 } = options;
3432
3511
  const imageName = config$1.imageName;
3433
3512
  const imageRef = getImageRef(config$1.registry, imageName, tag);
3434
- await buildImage(imageRef);
3513
+ await buildImage(imageRef, config$1.appName);
3435
3514
  if (!skipPush) if (!config$1.registry) logger$4.warn("\n⚠️ No registry configured. Use --skip-push or configure docker.registry in gkm.config.ts");
3436
3515
  else await pushImage(imageRef);
3437
3516
  logger$4.log("\n✅ Docker deployment ready!");
@@ -4143,11 +4222,25 @@ async function workspaceDeployCommand(workspace, options) {
4143
4222
  await provisionServices(api, project.projectId, environmentId, workspace.name, dockerServices);
4144
4223
  }
4145
4224
  const deployedAppUrls = {};
4225
+ if (!skipBuild) {
4226
+ logger$1.log("\n🏗️ Building workspace...");
4227
+ try {
4228
+ await buildCommand({
4229
+ provider: "server",
4230
+ production: true,
4231
+ stage
4232
+ });
4233
+ logger$1.log(" ✓ Workspace build complete");
4234
+ } catch (error) {
4235
+ const message = error instanceof Error ? error.message : "Unknown error";
4236
+ logger$1.log(` ✗ Workspace build failed: ${message}`);
4237
+ throw error;
4238
+ }
4239
+ }
4146
4240
  logger$1.log("\n📦 Deploying applications...");
4147
4241
  const results = [];
4148
4242
  for (const appName of appsToDeployNames) {
4149
4243
  const app = workspace.apps[appName];
4150
- const appPath = app.path;
4151
4244
  logger$1.log(`\n ${app.type === "backend" ? "⚙️" : "🌐"} Deploying ${appName}...`);
4152
4245
  try {
4153
4246
  const dokployAppName = `${workspace.name}-${appName}`;
@@ -4160,21 +4253,6 @@ async function workspaceDeployCommand(workspace, options) {
4160
4253
  if (message.includes("already exists") || message.includes("duplicate")) logger$1.log(` Application already exists`);
4161
4254
  else throw error;
4162
4255
  }
4163
- if (!skipBuild) {
4164
- logger$1.log(` Building ${appName}...`);
4165
- const originalCwd = process.cwd();
4166
- const fullAppPath = `${workspace.root}/${appPath}`;
4167
- try {
4168
- process.chdir(fullAppPath);
4169
- await buildCommand({
4170
- provider: "server",
4171
- production: true,
4172
- stage
4173
- });
4174
- } finally {
4175
- process.chdir(originalCwd);
4176
- }
4177
- }
4178
4256
  const imageName = `${workspace.name}-${appName}`;
4179
4257
  const imageRef = registry ? `${registry}/${imageName}:${imageTag}` : `${imageName}:${imageTag}`;
4180
4258
  logger$1.log(` Building Docker image: ${imageRef}`);
@@ -4184,7 +4262,8 @@ async function workspaceDeployCommand(workspace, options) {
4184
4262
  skipPush: false,
4185
4263
  config: {
4186
4264
  registry,
4187
- imageName
4265
+ imageName,
4266
+ appName
4188
4267
  }
4189
4268
  });
4190
4269
  const envVars = [`NODE_ENV=production`, `PORT=${app.port}`];
@@ -4552,7 +4631,9 @@ function generateAuthAppFiles(options) {
4552
4631
  dev: "gkm dev --entry ./src/index.ts",
4553
4632
  build: "tsc",
4554
4633
  start: "node dist/index.js",
4555
- typecheck: "tsc --noEmit"
4634
+ typecheck: "tsc --noEmit",
4635
+ "db:migrate": "gkm exec -- npx @better-auth/cli migrate",
4636
+ "db:generate": "gkm exec -- npx @better-auth/cli generate"
4556
4637
  },
4557
4638
  dependencies: {
4558
4639
  [modelsPackage]: "workspace:*",
@@ -7466,6 +7547,16 @@ program.command("dev").description("Start development server with automatic relo
7466
7547
  process.exit(1);
7467
7548
  }
7468
7549
  });
7550
+ program.command("exec").description("Run a command with secrets injected into Credentials").argument("<command...>", "Command to run (use -- before command)").action(async (commandArgs) => {
7551
+ try {
7552
+ const globalOptions = program.opts();
7553
+ if (globalOptions.cwd) process.chdir(globalOptions.cwd);
7554
+ await execCommand(commandArgs);
7555
+ } catch (error) {
7556
+ console.error(error instanceof Error ? error.message : "Command failed");
7557
+ process.exit(1);
7558
+ }
7559
+ });
7469
7560
  program.command("test").description("Run tests with secrets loaded from environment").option("--stage <stage>", "Stage to load secrets from", "development").option("--run", "Run tests once without watch mode").option("--watch", "Enable watch mode").option("--coverage", "Generate coverage report").option("--ui", "Open Vitest UI").argument("[pattern]", "Pattern to filter tests").action(async (pattern, options) => {
7470
7561
  try {
7471
7562
  const globalOptions = program.opts();