@geekmidas/cli 0.39.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 +674 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +653 -101
- 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/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 +40 -11
- package/src/deploy/dokploy-api.ts +99 -0
- package/src/deploy/domain.ts +125 -0
- package/src/deploy/index.ts +366 -148
- package/src/deploy/secrets.ts +182 -0
- package/src/deploy/sniffer.ts +180 -0
- package/src/dev/index.ts +11 -0
- package/src/docker/index.ts +17 -2
- package/src/docker/templates.ts +171 -1
- 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();
|
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',
|
package/src/docker/templates.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface FrontendDockerfileOptions {
|
|
|
25
25
|
turboPackage: string;
|
|
26
26
|
/** Detected package manager */
|
|
27
27
|
packageManager: PackageManager;
|
|
28
|
+
/**
|
|
29
|
+
* Public URL build args to include in the Dockerfile.
|
|
30
|
+
* These will be declared as ARG and converted to ENV for Next.js build.
|
|
31
|
+
* Example: ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL']
|
|
32
|
+
*/
|
|
33
|
+
publicUrlArgs?: string[];
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export interface MultiStageDockerfileOptions extends DockerTemplateOptions {
|
|
@@ -568,7 +574,14 @@ export function resolveDockerConfig(
|
|
|
568
574
|
export function generateNextjsDockerfile(
|
|
569
575
|
options: FrontendDockerfileOptions,
|
|
570
576
|
): string {
|
|
571
|
-
const {
|
|
577
|
+
const {
|
|
578
|
+
baseImage,
|
|
579
|
+
port,
|
|
580
|
+
appPath,
|
|
581
|
+
turboPackage,
|
|
582
|
+
packageManager,
|
|
583
|
+
publicUrlArgs = ['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL'],
|
|
584
|
+
} = options;
|
|
572
585
|
|
|
573
586
|
const pm = getPmConfig(packageManager);
|
|
574
587
|
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
@@ -580,6 +593,14 @@ export function generateNextjsDockerfile(
|
|
|
580
593
|
// Use pnpm dlx for pnpm (avoids global bin dir issues in Docker)
|
|
581
594
|
const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
|
|
582
595
|
|
|
596
|
+
// Generate ARG and ENV declarations for public URLs
|
|
597
|
+
const publicUrlArgDeclarations = publicUrlArgs
|
|
598
|
+
.map((arg) => `ARG ${arg}=""`)
|
|
599
|
+
.join('\n');
|
|
600
|
+
const publicUrlEnvDeclarations = publicUrlArgs
|
|
601
|
+
.map((arg) => `ENV ${arg}=$${arg}`)
|
|
602
|
+
.join('\n');
|
|
603
|
+
|
|
583
604
|
return `# syntax=docker/dockerfile:1
|
|
584
605
|
# Next.js standalone Dockerfile with turbo prune optimization
|
|
585
606
|
|
|
@@ -615,6 +636,13 @@ FROM deps AS builder
|
|
|
615
636
|
|
|
616
637
|
WORKDIR /app
|
|
617
638
|
|
|
639
|
+
# Build-time args for public API URLs (populated by gkm deploy)
|
|
640
|
+
# These get baked into the Next.js build as public environment variables
|
|
641
|
+
${publicUrlArgDeclarations}
|
|
642
|
+
|
|
643
|
+
# Convert ARGs to ENVs for Next.js build
|
|
644
|
+
${publicUrlEnvDeclarations}
|
|
645
|
+
|
|
618
646
|
# Copy pruned source
|
|
619
647
|
COPY --from=pruner /app/out/full/ ./
|
|
620
648
|
|
|
@@ -717,9 +745,20 @@ FROM deps AS builder
|
|
|
717
745
|
|
|
718
746
|
WORKDIR /app
|
|
719
747
|
|
|
748
|
+
# Build-time args for encrypted secrets
|
|
749
|
+
ARG GKM_ENCRYPTED_CREDENTIALS=""
|
|
750
|
+
ARG GKM_CREDENTIALS_IV=""
|
|
751
|
+
|
|
720
752
|
# Copy pruned source
|
|
721
753
|
COPY --from=pruner /app/out/full/ ./
|
|
722
754
|
|
|
755
|
+
# Write encrypted credentials for gkm build to embed
|
|
756
|
+
RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
|
|
757
|
+
mkdir -p ${appPath}/.gkm && \
|
|
758
|
+
echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
|
|
759
|
+
echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
|
|
760
|
+
fi
|
|
761
|
+
|
|
723
762
|
# Build production server using gkm
|
|
724
763
|
RUN cd ${appPath} && ./node_modules/.bin/gkm build --provider server --production
|
|
725
764
|
|
|
@@ -750,3 +789,134 @@ ENTRYPOINT ["/sbin/tini", "--"]
|
|
|
750
789
|
CMD ["node", "server.mjs"]
|
|
751
790
|
`;
|
|
752
791
|
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Options for entry-based Dockerfile generation.
|
|
795
|
+
*/
|
|
796
|
+
export interface EntryDockerfileOptions {
|
|
797
|
+
imageName: string;
|
|
798
|
+
baseImage: string;
|
|
799
|
+
port: number;
|
|
800
|
+
/** App path relative to workspace root */
|
|
801
|
+
appPath: string;
|
|
802
|
+
/** Entry file path relative to app path (e.g., './src/index.ts') */
|
|
803
|
+
entry: string;
|
|
804
|
+
/** Package name for turbo prune */
|
|
805
|
+
turboPackage: string;
|
|
806
|
+
/** Detected package manager */
|
|
807
|
+
packageManager: PackageManager;
|
|
808
|
+
/** Health check path (default: '/health') */
|
|
809
|
+
healthCheckPath?: string;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Generate a Dockerfile for apps with a custom entry point.
|
|
814
|
+
* Uses tsdown to bundle the entry point into dist/index.mjs.
|
|
815
|
+
* This is used for apps that don't use gkm routes (e.g., Better Auth servers).
|
|
816
|
+
* @internal Exported for testing
|
|
817
|
+
*/
|
|
818
|
+
export function generateEntryDockerfile(options: EntryDockerfileOptions): string {
|
|
819
|
+
const {
|
|
820
|
+
baseImage,
|
|
821
|
+
port,
|
|
822
|
+
appPath,
|
|
823
|
+
entry,
|
|
824
|
+
turboPackage,
|
|
825
|
+
packageManager,
|
|
826
|
+
healthCheckPath = '/health',
|
|
827
|
+
} = options;
|
|
828
|
+
|
|
829
|
+
const pm = getPmConfig(packageManager);
|
|
830
|
+
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
831
|
+
const turboInstallCmd = getTurboInstallCmd(packageManager);
|
|
832
|
+
const turboCmd = packageManager === 'pnpm' ? 'pnpm dlx turbo' : 'npx turbo';
|
|
833
|
+
|
|
834
|
+
return `# syntax=docker/dockerfile:1
|
|
835
|
+
# Entry-based Dockerfile with turbo prune + tsdown bundling
|
|
836
|
+
|
|
837
|
+
# Stage 1: Prune monorepo
|
|
838
|
+
FROM ${baseImage} AS pruner
|
|
839
|
+
|
|
840
|
+
WORKDIR /app
|
|
841
|
+
|
|
842
|
+
${installPm}
|
|
843
|
+
|
|
844
|
+
COPY . .
|
|
845
|
+
|
|
846
|
+
# Prune to only include necessary packages
|
|
847
|
+
RUN ${turboCmd} prune ${turboPackage} --docker
|
|
848
|
+
|
|
849
|
+
# Stage 2: Install dependencies
|
|
850
|
+
FROM ${baseImage} AS deps
|
|
851
|
+
|
|
852
|
+
WORKDIR /app
|
|
853
|
+
|
|
854
|
+
${installPm}
|
|
855
|
+
|
|
856
|
+
# Copy pruned lockfile and package.jsons
|
|
857
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
858
|
+
COPY --from=pruner /app/out/json/ ./
|
|
859
|
+
|
|
860
|
+
# Install dependencies
|
|
861
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
862
|
+
${turboInstallCmd}
|
|
863
|
+
|
|
864
|
+
# Stage 3: Build with tsdown
|
|
865
|
+
FROM deps AS builder
|
|
866
|
+
|
|
867
|
+
WORKDIR /app
|
|
868
|
+
|
|
869
|
+
# Build-time args for encrypted secrets
|
|
870
|
+
ARG GKM_ENCRYPTED_CREDENTIALS=""
|
|
871
|
+
ARG GKM_CREDENTIALS_IV=""
|
|
872
|
+
|
|
873
|
+
# Copy pruned source
|
|
874
|
+
COPY --from=pruner /app/out/full/ ./
|
|
875
|
+
|
|
876
|
+
# Write encrypted credentials for tsdown to embed via define
|
|
877
|
+
RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
|
|
878
|
+
mkdir -p ${appPath}/.gkm && \
|
|
879
|
+
echo "$GKM_ENCRYPTED_CREDENTIALS" > ${appPath}/.gkm/credentials.enc && \
|
|
880
|
+
echo "$GKM_CREDENTIALS_IV" > ${appPath}/.gkm/credentials.iv; \
|
|
881
|
+
fi
|
|
882
|
+
|
|
883
|
+
# Bundle entry point with tsdown (outputs to dist/index.mjs)
|
|
884
|
+
# Use define to embed credentials if present
|
|
885
|
+
RUN cd ${appPath} && \
|
|
886
|
+
if [ -f .gkm/credentials.enc ]; then \
|
|
887
|
+
CREDS=$(cat .gkm/credentials.enc) && \
|
|
888
|
+
IV=$(cat .gkm/credentials.iv) && \
|
|
889
|
+
npx tsdown ${entry} --outDir dist --format esm \
|
|
890
|
+
--define __GKM_ENCRYPTED_CREDENTIALS__="'\\"$CREDS\\"'" \
|
|
891
|
+
--define __GKM_CREDENTIALS_IV__="'\\"$IV\\"'"; \
|
|
892
|
+
else \
|
|
893
|
+
npx tsdown ${entry} --outDir dist --format esm; \
|
|
894
|
+
fi
|
|
895
|
+
|
|
896
|
+
# Stage 4: Production
|
|
897
|
+
FROM ${baseImage} AS runner
|
|
898
|
+
|
|
899
|
+
WORKDIR /app
|
|
900
|
+
|
|
901
|
+
RUN apk add --no-cache tini
|
|
902
|
+
|
|
903
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
904
|
+
adduser --system --uid 1001 app
|
|
905
|
+
|
|
906
|
+
# Copy bundled output only (no node_modules needed - fully bundled)
|
|
907
|
+
COPY --from=builder --chown=app:nodejs /app/${appPath}/dist/index.mjs ./
|
|
908
|
+
|
|
909
|
+
ENV NODE_ENV=production
|
|
910
|
+
ENV PORT=${port}
|
|
911
|
+
|
|
912
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
913
|
+
CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
|
|
914
|
+
|
|
915
|
+
USER app
|
|
916
|
+
|
|
917
|
+
EXPOSE ${port}
|
|
918
|
+
|
|
919
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
920
|
+
CMD ["node", "index.mjs"]
|
|
921
|
+
`;
|
|
922
|
+
}
|
package/src/init/versions.ts
CHANGED
|
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
32
32
|
'@geekmidas/cli': CLI_VERSION,
|
|
33
33
|
'@geekmidas/client': '~0.5.0',
|
|
34
34
|
'@geekmidas/cloud': '~0.2.0',
|
|
35
|
-
'@geekmidas/constructs': '~0.
|
|
35
|
+
'@geekmidas/constructs': '~0.7.0',
|
|
36
36
|
'@geekmidas/db': '~0.3.0',
|
|
37
37
|
'@geekmidas/emailkit': '~0.2.0',
|
|
38
|
-
'@geekmidas/envkit': '~0.
|
|
38
|
+
'@geekmidas/envkit': '~0.6.0',
|
|
39
39
|
'@geekmidas/errors': '~0.1.0',
|
|
40
40
|
'@geekmidas/events': '~0.2.0',
|
|
41
41
|
'@geekmidas/logger': '~0.4.0',
|
package/src/workspace/index.ts
CHANGED
|
@@ -34,11 +34,13 @@ export type {
|
|
|
34
34
|
AppConfigInput,
|
|
35
35
|
AppInput,
|
|
36
36
|
AppsRecord,
|
|
37
|
+
BackendFramework,
|
|
37
38
|
ClientConfig,
|
|
38
39
|
ConstrainedApps,
|
|
39
40
|
DeployConfig,
|
|
40
41
|
DeployTarget,
|
|
41
42
|
DokployWorkspaceConfig,
|
|
43
|
+
FrontendFramework,
|
|
42
44
|
InferAppNames,
|
|
43
45
|
InferredWorkspaceConfig,
|
|
44
46
|
LoadedConfig,
|