@geekmidas/cli 0.38.0 → 0.40.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-DQIuE3Kn.mjs → bundler-Db83tLti.mjs} +2 -2
- package/dist/{bundler-DQIuE3Kn.mjs.map → bundler-Db83tLti.mjs.map} +1 -1
- package/dist/{bundler-CyHg1v_T.cjs → bundler-DsXfFSCU.cjs} +2 -2
- package/dist/{bundler-CyHg1v_T.cjs.map → bundler-DsXfFSCU.cjs.map} +1 -1
- package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
- package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
- package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
- package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
- package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
- package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
- package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
- package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
- package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
- package/dist/encryption-Biq0EZ4m.cjs +4 -0
- package/dist/encryption-CQXBZGkt.mjs +3 -0
- package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
- package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
- package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
- package/dist/index-CXa3odEw.d.mts.map +1 -0
- package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
- package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
- package/dist/index.cjs +787 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +767 -125
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
- package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
- package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
- package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
- package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
- package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
- package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
- package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/build/index.ts +23 -6
- package/src/deploy/__tests__/domain.spec.ts +231 -0
- package/src/deploy/__tests__/secrets.spec.ts +300 -0
- package/src/deploy/__tests__/sniffer.spec.ts +221 -0
- package/src/deploy/docker.ts +58 -29
- package/src/deploy/dokploy-api.ts +99 -0
- package/src/deploy/domain.ts +125 -0
- package/src/deploy/index.ts +364 -145
- package/src/deploy/secrets.ts +182 -0
- package/src/deploy/sniffer.ts +180 -0
- package/src/dev/index.ts +155 -9
- package/src/docker/index.ts +17 -2
- package/src/docker/templates.ts +171 -1
- package/src/index.ts +18 -1
- package/src/init/generators/auth.ts +2 -0
- package/src/init/versions.ts +2 -2
- package/src/workspace/index.ts +2 -0
- package/src/workspace/schema.ts +32 -6
- package/src/workspace/types.ts +64 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-B0w17y4_.mjs +0 -3
- package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
- package/dist/index-C7TkoYmt.d.mts.map +0 -1
- package/dist/index-CpchsC9w.d.cts.map +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { encryptSecrets } from '../secrets/encryption.js';
|
|
2
|
+
import { toEmbeddableSecrets } from '../secrets/storage.js';
|
|
3
|
+
import type { EmbeddableSecrets, EncryptedPayload, StageSecrets } from '../secrets/types.js';
|
|
4
|
+
import type { SniffedEnvironment } from './sniffer.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Result of filtering secrets for an app.
|
|
8
|
+
*/
|
|
9
|
+
export interface FilteredAppSecrets {
|
|
10
|
+
appName: string;
|
|
11
|
+
/** Secrets filtered to only include what the app needs */
|
|
12
|
+
secrets: EmbeddableSecrets;
|
|
13
|
+
/** List of required env vars that were found in secrets */
|
|
14
|
+
found: string[];
|
|
15
|
+
/** List of required env vars that were NOT found in secrets */
|
|
16
|
+
missing: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Filter secrets to only include the env vars that an app requires.
|
|
21
|
+
*
|
|
22
|
+
* @param stageSecrets - All secrets for the stage
|
|
23
|
+
* @param sniffedEnv - The sniffed environment requirements for the app
|
|
24
|
+
* @returns Filtered secrets with found/missing tracking
|
|
25
|
+
*/
|
|
26
|
+
export function filterSecretsForApp(
|
|
27
|
+
stageSecrets: StageSecrets,
|
|
28
|
+
sniffedEnv: SniffedEnvironment,
|
|
29
|
+
): FilteredAppSecrets {
|
|
30
|
+
// Convert stage secrets to flat embeddable format
|
|
31
|
+
const allSecrets = toEmbeddableSecrets(stageSecrets);
|
|
32
|
+
const filtered: EmbeddableSecrets = {};
|
|
33
|
+
const found: string[] = [];
|
|
34
|
+
const missing: string[] = [];
|
|
35
|
+
|
|
36
|
+
// Filter to only required env vars
|
|
37
|
+
for (const key of sniffedEnv.requiredEnvVars) {
|
|
38
|
+
if (key in allSecrets) {
|
|
39
|
+
filtered[key] = allSecrets[key]!;
|
|
40
|
+
found.push(key);
|
|
41
|
+
} else {
|
|
42
|
+
missing.push(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
appName: sniffedEnv.appName,
|
|
48
|
+
secrets: filtered,
|
|
49
|
+
found: found.sort(),
|
|
50
|
+
missing: missing.sort(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Result of encrypting secrets for an app.
|
|
56
|
+
*/
|
|
57
|
+
export interface EncryptedAppSecrets {
|
|
58
|
+
appName: string;
|
|
59
|
+
/** Encrypted payload with credentials and IV */
|
|
60
|
+
payload: EncryptedPayload;
|
|
61
|
+
/** Master key for runtime decryption (hex encoded) */
|
|
62
|
+
masterKey: string;
|
|
63
|
+
/** Number of secrets encrypted */
|
|
64
|
+
secretCount: number;
|
|
65
|
+
/** List of required env vars that were NOT found in secrets */
|
|
66
|
+
missingSecrets: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encrypt filtered secrets for an app.
|
|
71
|
+
* Generates an ephemeral master key that should be injected into Dokploy.
|
|
72
|
+
*
|
|
73
|
+
* @param filteredSecrets - The filtered secrets for the app
|
|
74
|
+
* @returns Encrypted payload with master key
|
|
75
|
+
*/
|
|
76
|
+
export function encryptSecretsForApp(
|
|
77
|
+
filteredSecrets: FilteredAppSecrets,
|
|
78
|
+
): EncryptedAppSecrets {
|
|
79
|
+
const payload = encryptSecrets(filteredSecrets.secrets);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
appName: filteredSecrets.appName,
|
|
83
|
+
payload,
|
|
84
|
+
masterKey: payload.masterKey,
|
|
85
|
+
secretCount: Object.keys(filteredSecrets.secrets).length,
|
|
86
|
+
missingSecrets: filteredSecrets.missing,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Filter and encrypt secrets for an app in one step.
|
|
92
|
+
*
|
|
93
|
+
* @param stageSecrets - All secrets for the stage
|
|
94
|
+
* @param sniffedEnv - The sniffed environment requirements for the app
|
|
95
|
+
* @returns Encrypted secrets with master key
|
|
96
|
+
*/
|
|
97
|
+
export function prepareSecretsForApp(
|
|
98
|
+
stageSecrets: StageSecrets,
|
|
99
|
+
sniffedEnv: SniffedEnvironment,
|
|
100
|
+
): EncryptedAppSecrets {
|
|
101
|
+
const filtered = filterSecretsForApp(stageSecrets, sniffedEnv);
|
|
102
|
+
return encryptSecretsForApp(filtered);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Prepare secrets for multiple apps.
|
|
107
|
+
*
|
|
108
|
+
* @param stageSecrets - All secrets for the stage
|
|
109
|
+
* @param sniffedApps - Map of app name to sniffed environment
|
|
110
|
+
* @returns Map of app name to encrypted secrets
|
|
111
|
+
*/
|
|
112
|
+
export function prepareSecretsForAllApps(
|
|
113
|
+
stageSecrets: StageSecrets,
|
|
114
|
+
sniffedApps: Map<string, SniffedEnvironment>,
|
|
115
|
+
): Map<string, EncryptedAppSecrets> {
|
|
116
|
+
const results = new Map<string, EncryptedAppSecrets>();
|
|
117
|
+
|
|
118
|
+
for (const [appName, sniffedEnv] of sniffedApps) {
|
|
119
|
+
// Only prepare secrets for apps that have required env vars
|
|
120
|
+
if (sniffedEnv.requiredEnvVars.length > 0) {
|
|
121
|
+
const encrypted = prepareSecretsForApp(stageSecrets, sniffedEnv);
|
|
122
|
+
results.set(appName, encrypted);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Report on secrets preparation status for all apps.
|
|
131
|
+
*/
|
|
132
|
+
export interface SecretsReport {
|
|
133
|
+
/** Total number of apps processed */
|
|
134
|
+
totalApps: number;
|
|
135
|
+
/** Apps with encrypted secrets */
|
|
136
|
+
appsWithSecrets: string[];
|
|
137
|
+
/** Apps without secrets (frontends or no env requirements) */
|
|
138
|
+
appsWithoutSecrets: string[];
|
|
139
|
+
/** Apps with missing secrets (warnings) */
|
|
140
|
+
appsWithMissingSecrets: Array<{
|
|
141
|
+
appName: string;
|
|
142
|
+
missing: string[];
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate a report on secrets preparation.
|
|
148
|
+
*/
|
|
149
|
+
export function generateSecretsReport(
|
|
150
|
+
encryptedApps: Map<string, EncryptedAppSecrets>,
|
|
151
|
+
sniffedApps: Map<string, SniffedEnvironment>,
|
|
152
|
+
): SecretsReport {
|
|
153
|
+
const appsWithSecrets: string[] = [];
|
|
154
|
+
const appsWithoutSecrets: string[] = [];
|
|
155
|
+
const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> = [];
|
|
156
|
+
|
|
157
|
+
for (const [appName, sniffedEnv] of sniffedApps) {
|
|
158
|
+
if (sniffedEnv.requiredEnvVars.length === 0) {
|
|
159
|
+
appsWithoutSecrets.push(appName);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const encrypted = encryptedApps.get(appName);
|
|
164
|
+
if (encrypted) {
|
|
165
|
+
appsWithSecrets.push(appName);
|
|
166
|
+
|
|
167
|
+
if (encrypted.missingSecrets.length > 0) {
|
|
168
|
+
appsWithMissingSecrets.push({
|
|
169
|
+
appName,
|
|
170
|
+
missing: encrypted.missingSecrets,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
totalApps: sniffedApps.size,
|
|
178
|
+
appsWithSecrets: appsWithSecrets.sort(),
|
|
179
|
+
appsWithoutSecrets: appsWithoutSecrets.sort(),
|
|
180
|
+
appsWithMissingSecrets,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
4
|
+
import type { NormalizedAppConfig } from '../workspace/types.js';
|
|
5
|
+
|
|
6
|
+
// Re-export SniffResult for consumers
|
|
7
|
+
export type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Result of sniffing an app's environment requirements.
|
|
11
|
+
*/
|
|
12
|
+
export interface SniffedEnvironment {
|
|
13
|
+
appName: string;
|
|
14
|
+
requiredEnvVars: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for sniffing an app's environment.
|
|
19
|
+
*/
|
|
20
|
+
export interface SniffAppOptions {
|
|
21
|
+
/** Whether to log warnings for errors encountered during sniffing. Defaults to true. */
|
|
22
|
+
logWarnings?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get required environment variables for an app.
|
|
27
|
+
*
|
|
28
|
+
* Detection strategy:
|
|
29
|
+
* - Frontend apps: Returns empty (no server secrets)
|
|
30
|
+
* - Apps with `requiredEnv`: Uses explicit list from config
|
|
31
|
+
* - Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
|
|
32
|
+
* - Apps with neither: Returns empty
|
|
33
|
+
*
|
|
34
|
+
* This function handles "fire and forget" async operations gracefully,
|
|
35
|
+
* capturing errors and unhandled rejections without failing the build.
|
|
36
|
+
*
|
|
37
|
+
* @param app - The normalized app configuration
|
|
38
|
+
* @param appName - The name of the app
|
|
39
|
+
* @param workspacePath - Absolute path to the workspace root
|
|
40
|
+
* @param options - Optional configuration for sniffing behavior
|
|
41
|
+
* @returns The sniffed environment with required variables
|
|
42
|
+
*/
|
|
43
|
+
export async function sniffAppEnvironment(
|
|
44
|
+
app: NormalizedAppConfig,
|
|
45
|
+
appName: string,
|
|
46
|
+
workspacePath: string,
|
|
47
|
+
options: SniffAppOptions = {},
|
|
48
|
+
): Promise<SniffedEnvironment> {
|
|
49
|
+
const { logWarnings = true } = options;
|
|
50
|
+
|
|
51
|
+
// Frontend apps don't have server-side secrets
|
|
52
|
+
if (app.type === 'frontend') {
|
|
53
|
+
return { appName, requiredEnvVars: [] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Entry-based apps with explicit env list
|
|
57
|
+
if (app.requiredEnv && app.requiredEnv.length > 0) {
|
|
58
|
+
return { appName, requiredEnvVars: [...app.requiredEnv] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Apps with envParser - run sniffer to detect env var usage
|
|
62
|
+
if (app.envParser) {
|
|
63
|
+
const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
|
|
64
|
+
|
|
65
|
+
// Log any issues for debugging
|
|
66
|
+
if (logWarnings) {
|
|
67
|
+
if (result.error) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`[sniffer] ${appName}: envParser threw error during sniffing (env vars still captured): ${result.error.message}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (result.unhandledRejections.length > 0) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`[sniffer] ${appName}: Fire-and-forget rejections during sniffing (suppressed): ${result.unhandledRejections.map((e) => e.message).join(', ')}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { appName, requiredEnvVars: result.envVars };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// No env detection method available
|
|
83
|
+
return { appName, requiredEnvVars: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
88
|
+
* which environment variables it accesses.
|
|
89
|
+
*
|
|
90
|
+
* This function handles "fire and forget" async operations by using
|
|
91
|
+
* the shared sniffWithFireAndForget utility from @geekmidas/envkit.
|
|
92
|
+
*
|
|
93
|
+
* @param envParserPath - The envParser config (e.g., './src/config/env#envParser')
|
|
94
|
+
* @param appPath - The app's path relative to workspace
|
|
95
|
+
* @param workspacePath - Absolute path to workspace root
|
|
96
|
+
* @returns SniffResult with env vars and any errors encountered
|
|
97
|
+
*/
|
|
98
|
+
async function sniffEnvParser(
|
|
99
|
+
envParserPath: string,
|
|
100
|
+
appPath: string,
|
|
101
|
+
workspacePath: string,
|
|
102
|
+
): Promise<SniffResult> {
|
|
103
|
+
// Parse the envParser path: './src/config/env#envParser' or './src/config/env'
|
|
104
|
+
const [modulePath, exportName = 'default'] = envParserPath.split('#');
|
|
105
|
+
if (!modulePath) {
|
|
106
|
+
return { envVars: [], unhandledRejections: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Resolve the full path to the module
|
|
110
|
+
const fullPath = resolve(workspacePath, appPath, modulePath);
|
|
111
|
+
|
|
112
|
+
// Dynamically import the sniffer utilities
|
|
113
|
+
let SnifferEnvironmentParser: any;
|
|
114
|
+
let sniffWithFireAndForget: any;
|
|
115
|
+
try {
|
|
116
|
+
const envkitModule = await import('@geekmidas/envkit/sniffer');
|
|
117
|
+
SnifferEnvironmentParser = envkitModule.SnifferEnvironmentParser;
|
|
118
|
+
sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
console.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
|
|
122
|
+
return { envVars: [], unhandledRejections: [] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sniffer = new SnifferEnvironmentParser();
|
|
126
|
+
|
|
127
|
+
return sniffWithFireAndForget(sniffer, async () => {
|
|
128
|
+
// Import the envParser module
|
|
129
|
+
const moduleUrl = pathToFileURL(fullPath).href;
|
|
130
|
+
const module = await import(moduleUrl);
|
|
131
|
+
|
|
132
|
+
// Get the envParser function
|
|
133
|
+
const envParser = module[exportName];
|
|
134
|
+
if (typeof envParser !== 'function') {
|
|
135
|
+
console.warn(
|
|
136
|
+
`[sniffer] Export "${exportName}" from "${modulePath}" is not a function`,
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The envParser function typically creates and configures an EnvironmentParser.
|
|
142
|
+
// We pass our sniffer which implements the same interface.
|
|
143
|
+
const result = envParser(sniffer);
|
|
144
|
+
|
|
145
|
+
// If the result is a ConfigParser, call parse() to trigger env var access
|
|
146
|
+
if (result && typeof result.parse === 'function') {
|
|
147
|
+
try {
|
|
148
|
+
result.parse();
|
|
149
|
+
} catch {
|
|
150
|
+
// Parsing may fail due to mock values, that's expected
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sniff environment requirements for multiple apps.
|
|
158
|
+
*
|
|
159
|
+
* @param apps - Map of app name to app config
|
|
160
|
+
* @param workspacePath - Absolute path to workspace root
|
|
161
|
+
* @param options - Optional configuration for sniffing behavior
|
|
162
|
+
* @returns Map of app name to sniffed environment
|
|
163
|
+
*/
|
|
164
|
+
export async function sniffAllApps(
|
|
165
|
+
apps: Record<string, NormalizedAppConfig>,
|
|
166
|
+
workspacePath: string,
|
|
167
|
+
options: SniffAppOptions = {},
|
|
168
|
+
): Promise<Map<string, SniffedEnvironment>> {
|
|
169
|
+
const results = new Map<string, SniffedEnvironment>();
|
|
170
|
+
|
|
171
|
+
for (const [appName, app] of Object.entries(apps)) {
|
|
172
|
+
const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
|
|
173
|
+
results.set(appName, sniffed);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Export for testing
|
|
180
|
+
export { sniffEnvParser as _sniffEnvParser };
|
package/src/dev/index.ts
CHANGED
|
@@ -341,6 +341,17 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
341
341
|
workspaceAppName = appConfig.appName;
|
|
342
342
|
workspaceAppPort = appConfig.app.port;
|
|
343
343
|
logger.log(`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`);
|
|
344
|
+
|
|
345
|
+
// Check if app has an entry point (non-gkm app like better-auth)
|
|
346
|
+
if (appConfig.app.entry) {
|
|
347
|
+
logger.log(`📄 Using entry point: ${appConfig.app.entry}`);
|
|
348
|
+
return entryDevCommand({
|
|
349
|
+
...options,
|
|
350
|
+
entry: appConfig.app.entry,
|
|
351
|
+
port: workspaceAppPort,
|
|
352
|
+
portExplicit: true,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
344
355
|
} catch {
|
|
345
356
|
// Not in a workspace or app not found in workspace - fall back to regular loading
|
|
346
357
|
const loadedConfig = await loadWorkspaceConfig();
|
|
@@ -1272,6 +1283,44 @@ export function findSecretsRoot(startDir: string): string {
|
|
|
1272
1283
|
return startDir;
|
|
1273
1284
|
}
|
|
1274
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* Generate the credentials injection code snippet.
|
|
1288
|
+
* This is the common logic used by both entry wrapper and exec preload.
|
|
1289
|
+
* @internal
|
|
1290
|
+
*/
|
|
1291
|
+
function generateCredentialsInjection(secretsJsonPath: string): string {
|
|
1292
|
+
return `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
1293
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1294
|
+
|
|
1295
|
+
// Inject dev secrets into Credentials
|
|
1296
|
+
const secretsPath = '${secretsJsonPath}';
|
|
1297
|
+
if (existsSync(secretsPath)) {
|
|
1298
|
+
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1299
|
+
Object.assign(Credentials, secrets);
|
|
1300
|
+
// Debug: uncomment to verify preload is running
|
|
1301
|
+
// console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
|
|
1302
|
+
}
|
|
1303
|
+
`;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Create a preload script that injects secrets into Credentials.
|
|
1308
|
+
* Used by `gkm exec` to inject secrets before running any command.
|
|
1309
|
+
* @internal Exported for testing
|
|
1310
|
+
*/
|
|
1311
|
+
export async function createCredentialsPreload(
|
|
1312
|
+
preloadPath: string,
|
|
1313
|
+
secretsJsonPath: string,
|
|
1314
|
+
): Promise<void> {
|
|
1315
|
+
const content = `/**
|
|
1316
|
+
* Credentials preload generated by 'gkm exec'
|
|
1317
|
+
* This file is loaded via NODE_OPTIONS="--import <path>"
|
|
1318
|
+
*/
|
|
1319
|
+
${generateCredentialsInjection(secretsJsonPath)}`;
|
|
1320
|
+
|
|
1321
|
+
await writeFile(preloadPath, content);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1275
1324
|
/**
|
|
1276
1325
|
* Create a wrapper script that injects secrets before importing the entry file.
|
|
1277
1326
|
* @internal Exported for testing
|
|
@@ -1282,15 +1331,7 @@ export async function createEntryWrapper(
|
|
|
1282
1331
|
secretsJsonPath?: string,
|
|
1283
1332
|
): Promise<void> {
|
|
1284
1333
|
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
|
-
|
|
1334
|
+
? `${generateCredentialsInjection(secretsJsonPath)}
|
|
1294
1335
|
`
|
|
1295
1336
|
: '';
|
|
1296
1337
|
|
|
@@ -1789,3 +1830,108 @@ start({
|
|
|
1789
1830
|
await fsWriteFile(serverPath, content);
|
|
1790
1831
|
}
|
|
1791
1832
|
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Options for the exec command.
|
|
1836
|
+
*/
|
|
1837
|
+
export interface ExecOptions {
|
|
1838
|
+
/** Working directory */
|
|
1839
|
+
cwd?: string;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Run a command with secrets injected into Credentials.
|
|
1844
|
+
* Uses Node's --import flag to preload a script that populates Credentials
|
|
1845
|
+
* before the command loads any modules that depend on them.
|
|
1846
|
+
*
|
|
1847
|
+
* @example
|
|
1848
|
+
* ```bash
|
|
1849
|
+
* gkm exec -- npx @better-auth/cli migrate
|
|
1850
|
+
* gkm exec -- npx prisma migrate dev
|
|
1851
|
+
* ```
|
|
1852
|
+
*/
|
|
1853
|
+
export async function execCommand(
|
|
1854
|
+
commandArgs: string[],
|
|
1855
|
+
options: ExecOptions = {},
|
|
1856
|
+
): Promise<void> {
|
|
1857
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1858
|
+
|
|
1859
|
+
if (commandArgs.length === 0) {
|
|
1860
|
+
throw new Error('No command specified. Usage: gkm exec -- <command>');
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// Load .env files
|
|
1864
|
+
const defaultEnv = loadEnvFiles('.env');
|
|
1865
|
+
if (defaultEnv.loaded.length > 0) {
|
|
1866
|
+
logger.log(`📦 Loaded env: ${defaultEnv.loaded.join(', ')}`);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Prepare credentials (loads workspace config and secrets)
|
|
1870
|
+
// Don't inject PORT for exec since we're not running a server
|
|
1871
|
+
const { credentials, secretsJsonPath, appName } =
|
|
1872
|
+
await prepareEntryCredentials({ cwd });
|
|
1873
|
+
|
|
1874
|
+
if (appName) {
|
|
1875
|
+
logger.log(`📦 App: ${appName}`);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const secretCount = Object.keys(credentials).filter(
|
|
1879
|
+
(k) => k !== 'PORT',
|
|
1880
|
+
).length;
|
|
1881
|
+
if (secretCount > 0) {
|
|
1882
|
+
logger.log(`🔐 Loaded ${secretCount} secret(s)`);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Create preload script that injects Credentials
|
|
1886
|
+
// Create in cwd so package resolution works (finds node_modules in app directory)
|
|
1887
|
+
const preloadDir = join(cwd, '.gkm');
|
|
1888
|
+
await mkdir(preloadDir, { recursive: true });
|
|
1889
|
+
const preloadPath = join(preloadDir, 'credentials-preload.ts');
|
|
1890
|
+
await createCredentialsPreload(preloadPath, secretsJsonPath);
|
|
1891
|
+
|
|
1892
|
+
// Build command
|
|
1893
|
+
const [cmd, ...args] = commandArgs;
|
|
1894
|
+
|
|
1895
|
+
if (!cmd) {
|
|
1896
|
+
throw new Error('No command specified');
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
logger.log(`🚀 Running: ${commandArgs.join(' ')}`);
|
|
1900
|
+
|
|
1901
|
+
// Merge NODE_OPTIONS with existing value (if any)
|
|
1902
|
+
// Add tsx loader first so our .ts preload can be loaded
|
|
1903
|
+
const existingNodeOptions = process.env.NODE_OPTIONS ?? '';
|
|
1904
|
+
const tsxImport = '--import tsx';
|
|
1905
|
+
const preloadImport = `--import ${preloadPath}`;
|
|
1906
|
+
|
|
1907
|
+
// Build NODE_OPTIONS: existing + tsx loader + our preload
|
|
1908
|
+
const nodeOptions = [existingNodeOptions, tsxImport, preloadImport]
|
|
1909
|
+
.filter(Boolean)
|
|
1910
|
+
.join(' ');
|
|
1911
|
+
|
|
1912
|
+
// Spawn the command with secrets in both:
|
|
1913
|
+
// 1. Environment variables (for tools that read process.env directly)
|
|
1914
|
+
// 2. Preload script (for tools that use Credentials object)
|
|
1915
|
+
const child = spawn(cmd, args, {
|
|
1916
|
+
cwd,
|
|
1917
|
+
stdio: 'inherit',
|
|
1918
|
+
env: {
|
|
1919
|
+
...process.env,
|
|
1920
|
+
...credentials, // Inject secrets as env vars
|
|
1921
|
+
NODE_OPTIONS: nodeOptions,
|
|
1922
|
+
},
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// Wait for the command to complete
|
|
1926
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
1927
|
+
child.on('close', (code: number | null) => resolve(code ?? 0));
|
|
1928
|
+
child.on('error', (error: Error) => {
|
|
1929
|
+
logger.error(`Failed to run command: ${error.message}`);
|
|
1930
|
+
resolve(1);
|
|
1931
|
+
});
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
if (exitCode !== 0) {
|
|
1935
|
+
process.exit(exitCode);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
package/src/docker/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
generateBackendDockerfile,
|
|
16
16
|
generateDockerEntrypoint,
|
|
17
17
|
generateDockerignore,
|
|
18
|
+
generateEntryDockerfile,
|
|
18
19
|
generateMultiStageDockerfile,
|
|
19
20
|
generateNextjsDockerfile,
|
|
20
21
|
generateSlimDockerfile,
|
|
@@ -400,7 +401,9 @@ export async function workspaceDockerCommand(
|
|
|
400
401
|
// Determine image name
|
|
401
402
|
const imageName = appName;
|
|
402
403
|
|
|
403
|
-
|
|
404
|
+
const hasEntry = !!app.entry;
|
|
405
|
+
const buildType = hasEntry ? 'entry' : app.type;
|
|
406
|
+
logger.log(`\n 📄 Generating Dockerfile for ${appName} (${buildType})`);
|
|
404
407
|
|
|
405
408
|
let dockerfile: string;
|
|
406
409
|
|
|
@@ -414,8 +417,20 @@ export async function workspaceDockerCommand(
|
|
|
414
417
|
turboPackage,
|
|
415
418
|
packageManager,
|
|
416
419
|
});
|
|
420
|
+
} else if (app.entry) {
|
|
421
|
+
// Backend with custom entry point - use tsdown bundling
|
|
422
|
+
dockerfile = generateEntryDockerfile({
|
|
423
|
+
imageName,
|
|
424
|
+
baseImage: 'node:22-alpine',
|
|
425
|
+
port: app.port,
|
|
426
|
+
appPath,
|
|
427
|
+
entry: app.entry,
|
|
428
|
+
turboPackage,
|
|
429
|
+
packageManager,
|
|
430
|
+
healthCheckPath: '/health',
|
|
431
|
+
});
|
|
417
432
|
} else {
|
|
418
|
-
//
|
|
433
|
+
// Backend with gkm routes - use gkm build
|
|
419
434
|
dockerfile = generateBackendDockerfile({
|
|
420
435
|
imageName,
|
|
421
436
|
baseImage: 'node:22-alpine',
|