@geekmidas/cli 0.38.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.cjs +154 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +135 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/build/index.ts +23 -6
- package/src/deploy/docker.ts +20 -20
- package/src/deploy/index.ts +20 -19
- package/src/dev/index.ts +144 -9
- package/src/index.ts +18 -1
- package/src/init/generators/auth.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"lodash.kebabcase": "^4.1.1",
|
|
49
49
|
"openapi-typescript": "^7.4.2",
|
|
50
50
|
"prompts": "~2.4.2",
|
|
51
|
-
"@geekmidas/schema": "~0.1.0",
|
|
52
51
|
"@geekmidas/constructs": "~0.6.0",
|
|
53
52
|
"@geekmidas/envkit": "~0.5.0",
|
|
53
|
+
"@geekmidas/schema": "~0.1.0",
|
|
54
54
|
"@geekmidas/errors": "~0.1.0",
|
|
55
55
|
"@geekmidas/logger": "~0.4.0"
|
|
56
56
|
},
|
package/src/build/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
|
-
import { join, relative } from 'node:path';
|
|
4
|
+
import { join, relative, resolve } from 'node:path';
|
|
5
5
|
import type { Cron } from '@geekmidas/constructs/crons';
|
|
6
6
|
import type { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
7
7
|
import type { Function } from '@geekmidas/constructs/functions';
|
|
8
8
|
import type { Subscriber } from '@geekmidas/constructs/subscribers';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
loadAppConfig,
|
|
11
|
+
loadConfig,
|
|
12
|
+
loadWorkspaceConfig,
|
|
13
|
+
parseModuleConfig,
|
|
14
|
+
} from '../config';
|
|
10
15
|
import {
|
|
11
16
|
getProductionConfigFromGkm,
|
|
12
17
|
normalizeHooksConfig,
|
|
@@ -49,13 +54,25 @@ export async function buildCommand(
|
|
|
49
54
|
const loadedConfig = await loadWorkspaceConfig();
|
|
50
55
|
|
|
51
56
|
// Route to workspace build mode for multi-app workspaces
|
|
57
|
+
// BUT only if we're at the workspace root (prevents recursive builds when
|
|
58
|
+
// Turbo runs gkm build in each app subdirectory)
|
|
52
59
|
if (loadedConfig.type === 'workspace') {
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const cwd = resolve(process.cwd());
|
|
61
|
+
const workspaceRoot = resolve(loadedConfig.workspace.root);
|
|
62
|
+
const isAtWorkspaceRoot = cwd === workspaceRoot;
|
|
63
|
+
|
|
64
|
+
if (isAtWorkspaceRoot) {
|
|
65
|
+
logger.log('š¦ Detected workspace configuration');
|
|
66
|
+
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
67
|
+
}
|
|
68
|
+
// When running from inside an app directory, use app-specific config
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
// Single-app build - use
|
|
58
|
-
const config =
|
|
71
|
+
// Single-app build - use app config if in workspace, otherwise legacy config
|
|
72
|
+
const config =
|
|
73
|
+
loadedConfig.type === 'workspace'
|
|
74
|
+
? (await loadAppConfig()).gkmConfig
|
|
75
|
+
: await loadConfig();
|
|
59
76
|
|
|
60
77
|
// Resolve providers from new config format
|
|
61
78
|
const resolved = resolveProviders(config, options);
|
package/src/deploy/docker.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import type { GkmConfig } from '../config';
|
|
5
|
-
import { dockerCommand, findLockfilePath
|
|
5
|
+
import { dockerCommand, findLockfilePath } from '../docker';
|
|
6
6
|
import type { DeployResult, DockerDeployConfig } from './types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -94,15 +94,19 @@ export function getImageRef(
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Build Docker image
|
|
97
|
+
* @param imageRef - Full image reference (registry/name:tag)
|
|
98
|
+
* @param appName - Name of the app (used for Dockerfile.{appName} in workspaces)
|
|
97
99
|
*/
|
|
98
|
-
async function buildImage(imageRef: string): Promise<void> {
|
|
100
|
+
async function buildImage(imageRef: string, appName?: string): Promise<void> {
|
|
99
101
|
logger.log(`\nšØ Building Docker image: ${imageRef}`);
|
|
100
102
|
|
|
101
103
|
const cwd = process.cwd();
|
|
102
|
-
const
|
|
104
|
+
const lockfilePath = findLockfilePath(cwd);
|
|
105
|
+
const lockfileDir = lockfilePath ? dirname(lockfilePath) : cwd;
|
|
106
|
+
const inMonorepo = lockfileDir !== cwd;
|
|
103
107
|
|
|
104
108
|
// Generate appropriate Dockerfile
|
|
105
|
-
if (inMonorepo) {
|
|
109
|
+
if (appName || inMonorepo) {
|
|
106
110
|
logger.log(' Generating Dockerfile for monorepo (turbo prune)...');
|
|
107
111
|
} else {
|
|
108
112
|
logger.log(' Generating Dockerfile...');
|
|
@@ -110,19 +114,15 @@ async function buildImage(imageRef: string): Promise<void> {
|
|
|
110
114
|
await dockerCommand({});
|
|
111
115
|
|
|
112
116
|
// Determine build context and Dockerfile path
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
dockerfilePath = join(appRelPath, '.gkm/docker/Dockerfile');
|
|
123
|
-
buildCwd = monorepoRoot;
|
|
124
|
-
logger.log(` Building from monorepo root: ${monorepoRoot}`);
|
|
125
|
-
}
|
|
117
|
+
// For workspaces with multiple apps, use per-app Dockerfile (Dockerfile.api, etc.)
|
|
118
|
+
const dockerfileSuffix = appName ? `.${appName}` : '';
|
|
119
|
+
const dockerfilePath = `.gkm/docker/Dockerfile${dockerfileSuffix}`;
|
|
120
|
+
|
|
121
|
+
// Build from workspace/monorepo root when we have a lockfile elsewhere or appName is provided
|
|
122
|
+
const buildCwd =
|
|
123
|
+
lockfilePath && (inMonorepo || appName) ? lockfileDir : cwd;
|
|
124
|
+
if (buildCwd !== cwd) {
|
|
125
|
+
logger.log(` Building from workspace root: ${buildCwd}`);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
try {
|
|
@@ -174,8 +174,8 @@ export async function deployDocker(
|
|
|
174
174
|
const imageName = config.imageName!;
|
|
175
175
|
const imageRef = getImageRef(config.registry, imageName, tag);
|
|
176
176
|
|
|
177
|
-
// Build image
|
|
178
|
-
await buildImage(imageRef);
|
|
177
|
+
// Build image (pass appName for workspace Dockerfile selection)
|
|
178
|
+
await buildImage(imageRef, config.appName);
|
|
179
179
|
|
|
180
180
|
// Push to registry if not skipped
|
|
181
181
|
if (!skipPush) {
|
package/src/deploy/index.ts
CHANGED
|
@@ -781,13 +781,29 @@ export async function workspaceDeployCommand(
|
|
|
781
781
|
// Track deployed app URLs for environment variable injection
|
|
782
782
|
const deployedAppUrls: Record<string, string> = {};
|
|
783
783
|
|
|
784
|
+
// Build the entire workspace once (not per-app to avoid Turbo/Next.js lock conflicts)
|
|
785
|
+
if (!skipBuild) {
|
|
786
|
+
logger.log('\nšļø Building workspace...');
|
|
787
|
+
try {
|
|
788
|
+
await buildCommand({
|
|
789
|
+
provider: 'server',
|
|
790
|
+
production: true,
|
|
791
|
+
stage,
|
|
792
|
+
});
|
|
793
|
+
logger.log(' ā Workspace build complete');
|
|
794
|
+
} catch (error) {
|
|
795
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
796
|
+
logger.log(` ā Workspace build failed: ${message}`);
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
784
801
|
// Deploy apps in dependency order
|
|
785
802
|
logger.log('\nš¦ Deploying applications...');
|
|
786
803
|
const results: AppDeployResult[] = [];
|
|
787
804
|
|
|
788
805
|
for (const appName of appsToDeployNames) {
|
|
789
806
|
const app = workspace.apps[appName]!;
|
|
790
|
-
const appPath = app.path;
|
|
791
807
|
|
|
792
808
|
logger.log(
|
|
793
809
|
`\n ${app.type === 'backend' ? 'āļø' : 'š'} Deploying ${appName}...`,
|
|
@@ -822,24 +838,8 @@ export async function workspaceDeployCommand(
|
|
|
822
838
|
}
|
|
823
839
|
}
|
|
824
840
|
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
logger.log(` Building ${appName}...`);
|
|
828
|
-
// For workspace, we need to build from the app directory
|
|
829
|
-
const originalCwd = process.cwd();
|
|
830
|
-
const fullAppPath = `${workspace.root}/${appPath}`;
|
|
831
|
-
|
|
832
|
-
try {
|
|
833
|
-
process.chdir(fullAppPath);
|
|
834
|
-
await buildCommand({
|
|
835
|
-
provider: 'server',
|
|
836
|
-
production: true,
|
|
837
|
-
stage,
|
|
838
|
-
});
|
|
839
|
-
} finally {
|
|
840
|
-
process.chdir(originalCwd);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
841
|
+
// Note: Workspace was already built once at the start of deployment
|
|
842
|
+
// to avoid Turbo/Next.js lock conflicts from concurrent builds
|
|
843
843
|
|
|
844
844
|
// Build Docker image
|
|
845
845
|
const imageName = `${workspace.name}-${appName}`;
|
|
@@ -856,6 +856,7 @@ export async function workspaceDeployCommand(
|
|
|
856
856
|
config: {
|
|
857
857
|
registry,
|
|
858
858
|
imageName,
|
|
859
|
+
appName, // Pass appName for Dockerfile.{appName} selection
|
|
859
860
|
},
|
|
860
861
|
});
|
|
861
862
|
|
package/src/dev/index.ts
CHANGED
|
@@ -1272,6 +1272,44 @@ export function findSecretsRoot(startDir: string): string {
|
|
|
1272
1272
|
return startDir;
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
|
+
/**
|
|
1276
|
+
* Generate the credentials injection code snippet.
|
|
1277
|
+
* This is the common logic used by both entry wrapper and exec preload.
|
|
1278
|
+
* @internal
|
|
1279
|
+
*/
|
|
1280
|
+
function generateCredentialsInjection(secretsJsonPath: string): string {
|
|
1281
|
+
return `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
1282
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1283
|
+
|
|
1284
|
+
// Inject dev secrets into Credentials
|
|
1285
|
+
const secretsPath = '${secretsJsonPath}';
|
|
1286
|
+
if (existsSync(secretsPath)) {
|
|
1287
|
+
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1288
|
+
Object.assign(Credentials, secrets);
|
|
1289
|
+
// Debug: uncomment to verify preload is running
|
|
1290
|
+
// console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
|
|
1291
|
+
}
|
|
1292
|
+
`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Create a preload script that injects secrets into Credentials.
|
|
1297
|
+
* Used by `gkm exec` to inject secrets before running any command.
|
|
1298
|
+
* @internal Exported for testing
|
|
1299
|
+
*/
|
|
1300
|
+
export async function createCredentialsPreload(
|
|
1301
|
+
preloadPath: string,
|
|
1302
|
+
secretsJsonPath: string,
|
|
1303
|
+
): Promise<void> {
|
|
1304
|
+
const content = `/**
|
|
1305
|
+
* Credentials preload generated by 'gkm exec'
|
|
1306
|
+
* This file is loaded via NODE_OPTIONS="--import <path>"
|
|
1307
|
+
*/
|
|
1308
|
+
${generateCredentialsInjection(secretsJsonPath)}`;
|
|
1309
|
+
|
|
1310
|
+
await writeFile(preloadPath, content);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1275
1313
|
/**
|
|
1276
1314
|
* Create a wrapper script that injects secrets before importing the entry file.
|
|
1277
1315
|
* @internal Exported for testing
|
|
@@ -1282,15 +1320,7 @@ export async function createEntryWrapper(
|
|
|
1282
1320
|
secretsJsonPath?: string,
|
|
1283
1321
|
): Promise<void> {
|
|
1284
1322
|
const credentialsInjection = secretsJsonPath
|
|
1285
|
-
?
|
|
1286
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1287
|
-
|
|
1288
|
-
// Inject dev secrets into Credentials (before app import)
|
|
1289
|
-
const secretsPath = '${secretsJsonPath}';
|
|
1290
|
-
if (existsSync(secretsPath)) {
|
|
1291
|
-
Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1323
|
+
? `${generateCredentialsInjection(secretsJsonPath)}
|
|
1294
1324
|
`
|
|
1295
1325
|
: '';
|
|
1296
1326
|
|
|
@@ -1789,3 +1819,108 @@ start({
|
|
|
1789
1819
|
await fsWriteFile(serverPath, content);
|
|
1790
1820
|
}
|
|
1791
1821
|
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Options for the exec command.
|
|
1825
|
+
*/
|
|
1826
|
+
export interface ExecOptions {
|
|
1827
|
+
/** Working directory */
|
|
1828
|
+
cwd?: string;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Run a command with secrets injected into Credentials.
|
|
1833
|
+
* Uses Node's --import flag to preload a script that populates Credentials
|
|
1834
|
+
* before the command loads any modules that depend on them.
|
|
1835
|
+
*
|
|
1836
|
+
* @example
|
|
1837
|
+
* ```bash
|
|
1838
|
+
* gkm exec -- npx @better-auth/cli migrate
|
|
1839
|
+
* gkm exec -- npx prisma migrate dev
|
|
1840
|
+
* ```
|
|
1841
|
+
*/
|
|
1842
|
+
export async function execCommand(
|
|
1843
|
+
commandArgs: string[],
|
|
1844
|
+
options: ExecOptions = {},
|
|
1845
|
+
): Promise<void> {
|
|
1846
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1847
|
+
|
|
1848
|
+
if (commandArgs.length === 0) {
|
|
1849
|
+
throw new Error('No command specified. Usage: gkm exec -- <command>');
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// Load .env files
|
|
1853
|
+
const defaultEnv = loadEnvFiles('.env');
|
|
1854
|
+
if (defaultEnv.loaded.length > 0) {
|
|
1855
|
+
logger.log(`š¦ Loaded env: ${defaultEnv.loaded.join(', ')}`);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Prepare credentials (loads workspace config and secrets)
|
|
1859
|
+
// Don't inject PORT for exec since we're not running a server
|
|
1860
|
+
const { credentials, secretsJsonPath, appName } =
|
|
1861
|
+
await prepareEntryCredentials({ cwd });
|
|
1862
|
+
|
|
1863
|
+
if (appName) {
|
|
1864
|
+
logger.log(`š¦ App: ${appName}`);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const secretCount = Object.keys(credentials).filter(
|
|
1868
|
+
(k) => k !== 'PORT',
|
|
1869
|
+
).length;
|
|
1870
|
+
if (secretCount > 0) {
|
|
1871
|
+
logger.log(`š Loaded ${secretCount} secret(s)`);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// Create preload script that injects Credentials
|
|
1875
|
+
// Create in cwd so package resolution works (finds node_modules in app directory)
|
|
1876
|
+
const preloadDir = join(cwd, '.gkm');
|
|
1877
|
+
await mkdir(preloadDir, { recursive: true });
|
|
1878
|
+
const preloadPath = join(preloadDir, 'credentials-preload.ts');
|
|
1879
|
+
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1880
|
+
|
|
1881
|
+
// Build command
|
|
1882
|
+
const [cmd, ...args] = commandArgs;
|
|
1883
|
+
|
|
1884
|
+
if (!cmd) {
|
|
1885
|
+
throw new Error('No command specified');
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
logger.log(`š Running: ${commandArgs.join(' ')}`);
|
|
1889
|
+
|
|
1890
|
+
// Merge NODE_OPTIONS with existing value (if any)
|
|
1891
|
+
// Add tsx loader first so our .ts preload can be loaded
|
|
1892
|
+
const existingNodeOptions = process.env.NODE_OPTIONS ?? '';
|
|
1893
|
+
const tsxImport = '--import tsx';
|
|
1894
|
+
const preloadImport = `--import ${preloadPath}`;
|
|
1895
|
+
|
|
1896
|
+
// Build NODE_OPTIONS: existing + tsx loader + our preload
|
|
1897
|
+
const nodeOptions = [existingNodeOptions, tsxImport, preloadImport]
|
|
1898
|
+
.filter(Boolean)
|
|
1899
|
+
.join(' ');
|
|
1900
|
+
|
|
1901
|
+
// Spawn the command with secrets in both:
|
|
1902
|
+
// 1. Environment variables (for tools that read process.env directly)
|
|
1903
|
+
// 2. Preload script (for tools that use Credentials object)
|
|
1904
|
+
const child = spawn(cmd, args, {
|
|
1905
|
+
cwd,
|
|
1906
|
+
stdio: 'inherit',
|
|
1907
|
+
env: {
|
|
1908
|
+
...process.env,
|
|
1909
|
+
...credentials, // Inject secrets as env vars
|
|
1910
|
+
NODE_OPTIONS: nodeOptions,
|
|
1911
|
+
},
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// Wait for the command to complete
|
|
1915
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
1916
|
+
child.on('close', (code: number | null) => resolve(code ?? 0));
|
|
1917
|
+
child.on('error', (error: Error) => {
|
|
1918
|
+
logger.error(`Failed to run command: ${error.message}`);
|
|
1919
|
+
resolve(1);
|
|
1920
|
+
});
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
if (exitCode !== 0) {
|
|
1924
|
+
process.exit(exitCode);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { loginCommand, logoutCommand, whoamiCommand } from './auth';
|
|
|
6
6
|
import { buildCommand } from './build/index';
|
|
7
7
|
import { type DeployProvider, deployCommand } from './deploy/index';
|
|
8
8
|
import { deployInitCommand, deployListCommand } from './deploy/init';
|
|
9
|
-
import { devCommand } from './dev/index';
|
|
9
|
+
import { devCommand, execCommand } from './dev/index';
|
|
10
10
|
import { type DockerOptions, dockerCommand } from './docker/index';
|
|
11
11
|
import { type InitOptions, initCommand } from './init/index';
|
|
12
12
|
import { openapiCommand } from './openapi';
|
|
@@ -173,6 +173,23 @@ program
|
|
|
173
173
|
},
|
|
174
174
|
);
|
|
175
175
|
|
|
176
|
+
program
|
|
177
|
+
.command('exec')
|
|
178
|
+
.description('Run a command with secrets injected into Credentials')
|
|
179
|
+
.argument('<command...>', 'Command to run (use -- before command)')
|
|
180
|
+
.action(async (commandArgs: string[]) => {
|
|
181
|
+
try {
|
|
182
|
+
const globalOptions = program.opts();
|
|
183
|
+
if (globalOptions.cwd) {
|
|
184
|
+
process.chdir(globalOptions.cwd);
|
|
185
|
+
}
|
|
186
|
+
await execCommand(commandArgs);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
176
193
|
program
|
|
177
194
|
.command('test')
|
|
178
195
|
.description('Run tests with secrets loaded from environment')
|
|
@@ -26,6 +26,8 @@ export function generateAuthAppFiles(
|
|
|
26
26
|
build: 'tsc',
|
|
27
27
|
start: 'node dist/index.js',
|
|
28
28
|
typecheck: 'tsc --noEmit',
|
|
29
|
+
'db:migrate': 'gkm exec -- npx @better-auth/cli migrate',
|
|
30
|
+
'db:generate': 'gkm exec -- npx @better-auth/cli generate',
|
|
29
31
|
},
|
|
30
32
|
dependencies: {
|
|
31
33
|
[modelsPackage]: 'workspace:*',
|