@geekmidas/cli 0.53.0 → 1.0.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/CHANGELOG.md +17 -0
- package/README.md +26 -5
- package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
- package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
- package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
- package/dist/Route53Provider-CpRIqu69.cjs +157 -0
- package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
- package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
- package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
- package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
- package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
- package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
- package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
- package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
- package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
- package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
- package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.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/credentials-C8DWtnMY.cjs +174 -0
- package/dist/credentials-C8DWtnMY.cjs.map +1 -0
- package/dist/credentials-DT1dSxIx.mjs +126 -0
- package/dist/credentials-DT1dSxIx.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
- package/dist/dokploy-api-CHa8G51l.mjs +3 -0
- package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
- package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
- package/dist/dokploy-api-CWc02yyg.cjs +3 -0
- package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
- package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
- package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/encryption-UUmaWAmz.mjs +3 -0
- package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
- package/dist/index-B5rGIc4g.d.cts.map +1 -0
- package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
- package/dist/index-KFEbMIRa.d.mts.map +1 -0
- package/dist/index.cjs +2242 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2219 -545
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
- package/dist/openapi-BMFmLnX6.mjs.map +1 -0
- package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
- package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
- package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
- package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.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.cts +1 -1
- package/dist/openapi.d.mts +2 -2
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
- package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
- package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
- package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
- package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
- package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
- package/dist/workspace-BFRUOOrh.cjs.map +1 -0
- package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
- package/dist/workspace-DAxG3_H2.mjs.map +1 -0
- package/package.json +12 -8
- package/src/build/__tests__/handler-templates.spec.ts +115 -47
- package/src/deploy/CachedStateProvider.ts +86 -0
- package/src/deploy/LocalStateProvider.ts +57 -0
- package/src/deploy/SSMStateProvider.ts +93 -0
- package/src/deploy/StateProvider.ts +171 -0
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
- package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
- package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +13 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +15 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +55 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
- package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
- package/src/deploy/__tests__/env-resolver.spec.ts +41 -17
- package/src/deploy/__tests__/sniffer.spec.ts +168 -10
- package/src/deploy/__tests__/state.spec.ts +13 -5
- package/src/deploy/dns/DnsProvider.ts +163 -0
- package/src/deploy/dns/HostingerProvider.ts +100 -0
- package/src/deploy/dns/Route53Provider.ts +256 -0
- package/src/deploy/dns/index.ts +257 -165
- package/src/deploy/env-resolver.ts +12 -5
- package/src/deploy/index.ts +16 -13
- package/src/deploy/sniffer-envkit-patch.ts +3 -1
- package/src/deploy/sniffer-routes-worker.ts +104 -0
- package/src/deploy/sniffer.ts +130 -5
- package/src/deploy/state-commands.ts +274 -0
- package/src/dev/__tests__/entry.spec.ts +8 -2
- package/src/dev/__tests__/index.spec.ts +1 -3
- package/src/dev/index.ts +9 -3
- package/src/docker/__tests__/templates.spec.ts +3 -1
- package/src/docker/templates.ts +3 -3
- package/src/index.ts +88 -0
- package/src/init/__tests__/generators.spec.ts +273 -0
- package/src/init/__tests__/init.spec.ts +3 -3
- package/src/init/generators/auth.ts +1 -0
- package/src/init/generators/config.ts +2 -0
- package/src/init/generators/models.ts +6 -1
- package/src/init/generators/monorepo.ts +3 -0
- package/src/init/generators/ui.ts +1472 -0
- package/src/init/generators/web.ts +134 -87
- package/src/init/index.ts +22 -3
- package/src/init/templates/api.ts +109 -3
- package/src/openapi.ts +99 -13
- package/src/workspace/__tests__/schema.spec.ts +107 -0
- package/src/workspace/schema.ts +314 -4
- package/src/workspace/types.ts +22 -36
- package/dist/dokploy-api-CItuaWTq.mjs +0 -3
- package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
- package/dist/encryption-CQXBZGkt.mjs +0 -3
- package/dist/index-A70abJ1m.d.mts.map +0 -1
- package/dist/index-pOA56MWT.d.cts.map +0 -1
- package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
- package/dist/openapi-D7WwlpPF.cjs.map +0 -1
- package/dist/workspace-CaVW6j2q.cjs.map +0 -1
- package/dist/workspace-DLFRaDc-.mjs.map +0 -1
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess worker for route-based app sniffing.
|
|
3
|
+
*
|
|
4
|
+
* This script is executed in a subprocess with tsx loader,
|
|
5
|
+
* which handles TypeScript compilation and path alias resolution.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node --import tsx ./sniffer-routes-worker.ts "/path/to/app" "./src/endpoints/**\/*.ts"
|
|
9
|
+
*
|
|
10
|
+
* Output (JSON to stdout):
|
|
11
|
+
* { "envVars": ["PORT", "DATABASE_URL", ...], "error": null }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Construct } from '@geekmidas/constructs';
|
|
15
|
+
import { Cron } from '@geekmidas/constructs/crons';
|
|
16
|
+
import { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
17
|
+
import { Function as GkmFunction } from '@geekmidas/constructs/functions';
|
|
18
|
+
import { Subscriber } from '@geekmidas/constructs/subscribers';
|
|
19
|
+
import fg from 'fast-glob';
|
|
20
|
+
|
|
21
|
+
// Get args from command line
|
|
22
|
+
const appPathArg = process.argv[2];
|
|
23
|
+
const routesPatternArg = process.argv[3];
|
|
24
|
+
|
|
25
|
+
if (!appPathArg || !routesPatternArg) {
|
|
26
|
+
console.log(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
envVars: [],
|
|
29
|
+
error: 'App path and routes pattern are required',
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// After validation, these are guaranteed to be strings
|
|
36
|
+
const appPath: string = appPathArg;
|
|
37
|
+
const routesPattern: string = routesPatternArg;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a value is a gkm construct.
|
|
41
|
+
*/
|
|
42
|
+
function isConstruct(value: unknown): value is Construct {
|
|
43
|
+
return (
|
|
44
|
+
Endpoint.isEndpoint(value) ||
|
|
45
|
+
GkmFunction.isFunction(value) ||
|
|
46
|
+
Cron.isCron(value) ||
|
|
47
|
+
Subscriber.isSubscriber(value)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Main sniffing function
|
|
53
|
+
*/
|
|
54
|
+
async function sniff(): Promise<void> {
|
|
55
|
+
const envVars = new Set<string>();
|
|
56
|
+
let error: string | null = null;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Find all route files matching the pattern
|
|
60
|
+
const files = await fg(routesPattern, {
|
|
61
|
+
cwd: appPath,
|
|
62
|
+
absolute: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Import each route file and find constructs
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
try {
|
|
68
|
+
const module = await import(file);
|
|
69
|
+
|
|
70
|
+
// Check all exports for constructs
|
|
71
|
+
for (const [, exportValue] of Object.entries(module)) {
|
|
72
|
+
if (isConstruct(exportValue)) {
|
|
73
|
+
try {
|
|
74
|
+
const constructEnvVars = await exportValue.getEnvironment();
|
|
75
|
+
constructEnvVars.forEach((v) => envVars.add(v));
|
|
76
|
+
} catch {
|
|
77
|
+
// Individual construct may fail, continue with others
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Log import errors but continue with other files
|
|
83
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
84
|
+
console.error(`[sniffer] Failed to import ${file}: ${msg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
error = e instanceof Error ? e.message : String(e);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Output result as JSON (last line of stdout)
|
|
92
|
+
console.log(JSON.stringify({ envVars: Array.from(envVars).sort(), error }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle unhandled rejections
|
|
96
|
+
process.on('unhandledRejection', () => {
|
|
97
|
+
// Silently ignore - we only care about env var capture
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Run the sniffer
|
|
101
|
+
sniff().catch((e) => {
|
|
102
|
+
console.log(JSON.stringify({ envVars: [], error: e.message || String(e) }));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { dirname, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
@@ -8,6 +9,15 @@ import type { NormalizedAppConfig } from '../workspace/types.js';
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = dirname(__filename);
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the tsx package path from the CLI package's dependencies.
|
|
14
|
+
* This ensures tsx is available regardless of whether the target project has it installed.
|
|
15
|
+
*/
|
|
16
|
+
function resolveTsxPath(): string {
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
return require.resolve('tsx');
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* Resolve the path to a sniffer helper file.
|
|
13
23
|
* Handles both dev (.ts with tsx) and production (.mjs from dist).
|
|
@@ -67,8 +77,9 @@ export interface SniffAppOptions {
|
|
|
67
77
|
* 1. Frontend apps: Returns empty (no server secrets)
|
|
68
78
|
* 2. Apps with `requiredEnv`: Uses explicit list from config
|
|
69
79
|
* 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
|
|
70
|
-
* 4.
|
|
71
|
-
* 5. Apps with
|
|
80
|
+
* 4. Route-based apps: Loads route files and calls getEnvironment() on each construct
|
|
81
|
+
* 5. Apps with `envParser` (no routes): Runs SnifferEnvironmentParser to detect usage
|
|
82
|
+
* 6. Apps with neither: Returns empty
|
|
72
83
|
*
|
|
73
84
|
* This function handles "fire and forget" async operations gracefully,
|
|
74
85
|
* capturing errors and unhandled rejections without failing the build.
|
|
@@ -110,7 +121,20 @@ export async function sniffAppEnvironment(
|
|
|
110
121
|
return { appName, requiredEnvVars: result.envVars };
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
// 4.
|
|
124
|
+
// 4. Route-based apps - load routes and call getEnvironment() on each construct
|
|
125
|
+
if (app.routes) {
|
|
126
|
+
const result = await sniffRouteFiles(app.routes, app.path, workspacePath);
|
|
127
|
+
|
|
128
|
+
if (logWarnings && result.error) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[sniffer] ${appName}: Route sniffing threw error (env vars still captured): ${result.error.message}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { appName, requiredEnvVars: result.envVars };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 5. Apps with envParser but no routes - run sniffer to detect env var usage
|
|
114
138
|
if (app.envParser) {
|
|
115
139
|
const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
|
|
116
140
|
|
|
@@ -232,6 +256,103 @@ async function sniffEntryFile(
|
|
|
232
256
|
});
|
|
233
257
|
}
|
|
234
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Sniff route files by loading constructs and calling getEnvironment().
|
|
261
|
+
*
|
|
262
|
+
* Route-based apps have endpoints, functions, crons, and subscribers that
|
|
263
|
+
* use services. Each service's register() method accesses environment variables.
|
|
264
|
+
*
|
|
265
|
+
* This runs in a subprocess with tsx loader to properly handle TypeScript
|
|
266
|
+
* compilation and path alias resolution (e.g., `src/...` imports).
|
|
267
|
+
*
|
|
268
|
+
* @param routes - Glob pattern(s) for route files
|
|
269
|
+
* @param appPath - The app's path relative to workspace (e.g., 'apps/api')
|
|
270
|
+
* @param workspacePath - Absolute path to workspace root
|
|
271
|
+
* @returns EntrySniffResult with env vars and optional error
|
|
272
|
+
*/
|
|
273
|
+
async function sniffRouteFiles(
|
|
274
|
+
routes: string | string[],
|
|
275
|
+
appPath: string,
|
|
276
|
+
workspacePath: string,
|
|
277
|
+
): Promise<EntrySniffResult> {
|
|
278
|
+
const fullAppPath = resolve(workspacePath, appPath);
|
|
279
|
+
const workerPath = resolveSnifferFile('sniffer-routes-worker');
|
|
280
|
+
const tsxPath = resolveTsxPath();
|
|
281
|
+
|
|
282
|
+
// Convert array of patterns to first pattern (worker handles glob internally)
|
|
283
|
+
const routesArray = Array.isArray(routes) ? routes : [routes];
|
|
284
|
+
const pattern = routesArray[0];
|
|
285
|
+
if (!pattern) {
|
|
286
|
+
return { envVars: [], error: new Error('No route patterns provided') };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return new Promise((resolvePromise) => {
|
|
290
|
+
const child = spawn(
|
|
291
|
+
'node',
|
|
292
|
+
['--import', tsxPath, workerPath, fullAppPath, pattern],
|
|
293
|
+
{
|
|
294
|
+
cwd: fullAppPath,
|
|
295
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
296
|
+
env: {
|
|
297
|
+
...process.env,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
let stdout = '';
|
|
303
|
+
let stderr = '';
|
|
304
|
+
|
|
305
|
+
child.stdout.on('data', (data) => {
|
|
306
|
+
stdout += data.toString();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
child.stderr.on('data', (data) => {
|
|
310
|
+
stderr += data.toString();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
child.on('close', (code) => {
|
|
314
|
+
// Log any stderr output (import errors, etc.)
|
|
315
|
+
if (stderr) {
|
|
316
|
+
stderr
|
|
317
|
+
.split('\n')
|
|
318
|
+
.filter((line) => line.trim())
|
|
319
|
+
.forEach((line) => console.warn(line));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Try to parse the JSON output from the worker
|
|
323
|
+
try {
|
|
324
|
+
// Find the last JSON object in stdout (worker may emit other output)
|
|
325
|
+
const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
|
|
326
|
+
if (jsonMatch) {
|
|
327
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
328
|
+
resolvePromise({
|
|
329
|
+
envVars: result.envVars || [],
|
|
330
|
+
error: result.error ? new Error(result.error) : undefined,
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
// JSON parse failed
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// If we couldn't parse the output, return empty with error info
|
|
339
|
+
resolvePromise({
|
|
340
|
+
envVars: [],
|
|
341
|
+
error: new Error(
|
|
342
|
+
`Failed to sniff route files (exit code ${code}): ${stderr || stdout || 'No output'}`,
|
|
343
|
+
),
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
child.on('error', (err) => {
|
|
348
|
+
resolvePromise({
|
|
349
|
+
envVars: [],
|
|
350
|
+
error: err,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
235
356
|
/**
|
|
236
357
|
* Run the SnifferEnvironmentParser on an envParser module to detect
|
|
237
358
|
* which environment variables it accesses.
|
|
@@ -333,4 +454,8 @@ export async function sniffAllApps(
|
|
|
333
454
|
}
|
|
334
455
|
|
|
335
456
|
// Export for testing
|
|
336
|
-
export {
|
|
457
|
+
export {
|
|
458
|
+
sniffEnvParser as _sniffEnvParser,
|
|
459
|
+
sniffEntryFile as _sniffEntryFile,
|
|
460
|
+
sniffRouteFiles as _sniffRouteFiles,
|
|
461
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Management CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* Commands for managing deployment state across local and remote providers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadWorkspaceConfig } from '../config';
|
|
8
|
+
import { CachedStateProvider } from './CachedStateProvider';
|
|
9
|
+
import { createStateProvider } from './StateProvider';
|
|
10
|
+
import type { DokployStageState } from './state';
|
|
11
|
+
|
|
12
|
+
export interface StateCommandOptions {
|
|
13
|
+
stage: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pull state from remote to local.
|
|
18
|
+
* `gkm state:pull --stage=<stage>`
|
|
19
|
+
*/
|
|
20
|
+
export async function statePullCommand(
|
|
21
|
+
options: StateCommandOptions,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
24
|
+
|
|
25
|
+
if (!workspace.state || workspace.state.provider === 'local') {
|
|
26
|
+
console.error('No remote state provider configured.');
|
|
27
|
+
console.error('Add a remote provider in gkm.config.ts:');
|
|
28
|
+
console.error(' state: { provider: "ssm", region: "us-east-1" }');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const provider = await createStateProvider({
|
|
33
|
+
config: workspace.state,
|
|
34
|
+
workspaceRoot: workspace.root,
|
|
35
|
+
workspaceName: workspace.name,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!(provider instanceof CachedStateProvider)) {
|
|
39
|
+
console.error('State provider does not support pull operation.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`Pulling state for stage: ${options.stage}...`);
|
|
44
|
+
const state = await provider.pull(options.stage);
|
|
45
|
+
|
|
46
|
+
if (state) {
|
|
47
|
+
console.log('State pulled successfully.');
|
|
48
|
+
printStateSummary(state);
|
|
49
|
+
} else {
|
|
50
|
+
console.log('No remote state found for this stage.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Push local state to remote.
|
|
56
|
+
* `gkm state:push --stage=<stage>`
|
|
57
|
+
*/
|
|
58
|
+
export async function statePushCommand(
|
|
59
|
+
options: StateCommandOptions,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
62
|
+
|
|
63
|
+
if (!workspace.state || workspace.state.provider === 'local') {
|
|
64
|
+
console.error('No remote state provider configured.');
|
|
65
|
+
console.error('Add a remote provider in gkm.config.ts:');
|
|
66
|
+
console.error(' state: { provider: "ssm", region: "us-east-1" }');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const provider = await createStateProvider({
|
|
71
|
+
config: workspace.state,
|
|
72
|
+
workspaceRoot: workspace.root,
|
|
73
|
+
workspaceName: workspace.name,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!(provider instanceof CachedStateProvider)) {
|
|
77
|
+
console.error('State provider does not support push operation.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`Pushing state for stage: ${options.stage}...`);
|
|
82
|
+
const state = await provider.push(options.stage);
|
|
83
|
+
|
|
84
|
+
if (state) {
|
|
85
|
+
console.log('State pushed successfully.');
|
|
86
|
+
printStateSummary(state);
|
|
87
|
+
} else {
|
|
88
|
+
console.log('No local state found for this stage.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Show current state.
|
|
94
|
+
* `gkm state:show --stage=<stage>`
|
|
95
|
+
*/
|
|
96
|
+
export async function stateShowCommand(
|
|
97
|
+
options: StateCommandOptions & { json?: boolean },
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
100
|
+
|
|
101
|
+
const provider = await createStateProvider({
|
|
102
|
+
config: workspace.state,
|
|
103
|
+
workspaceRoot: workspace.root,
|
|
104
|
+
workspaceName: workspace.name,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const state = await provider.read(options.stage);
|
|
108
|
+
|
|
109
|
+
if (!state) {
|
|
110
|
+
console.log(`No state found for stage: ${options.stage}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.json) {
|
|
115
|
+
console.log(JSON.stringify(state, null, 2));
|
|
116
|
+
} else {
|
|
117
|
+
printStateDetails(state);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Compare local and remote state.
|
|
123
|
+
* `gkm state:diff --stage=<stage>`
|
|
124
|
+
*/
|
|
125
|
+
export async function stateDiffCommand(
|
|
126
|
+
options: StateCommandOptions,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
129
|
+
|
|
130
|
+
if (!workspace.state || workspace.state.provider === 'local') {
|
|
131
|
+
console.error('No remote state provider configured.');
|
|
132
|
+
console.error('Diff requires a remote provider to compare against.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const provider = await createStateProvider({
|
|
137
|
+
config: workspace.state,
|
|
138
|
+
workspaceRoot: workspace.root,
|
|
139
|
+
workspaceName: workspace.name,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!(provider instanceof CachedStateProvider)) {
|
|
143
|
+
console.error('State provider does not support diff operation.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`Comparing state for stage: ${options.stage}...\n`);
|
|
148
|
+
const { local, remote } = await provider.diff(options.stage);
|
|
149
|
+
|
|
150
|
+
if (!local && !remote) {
|
|
151
|
+
console.log('No state found (local or remote).');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!local) {
|
|
156
|
+
console.log('Local: (none)');
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`Local: Last deployed ${local.lastDeployedAt}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!remote) {
|
|
162
|
+
console.log('Remote: (none)');
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`Remote: Last deployed ${remote.lastDeployedAt}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('');
|
|
168
|
+
|
|
169
|
+
// Compare applications
|
|
170
|
+
const localApps = local?.applications ?? {};
|
|
171
|
+
const remoteApps = remote?.applications ?? {};
|
|
172
|
+
const allApps = new Set([
|
|
173
|
+
...Object.keys(localApps),
|
|
174
|
+
...Object.keys(remoteApps),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
if (allApps.size > 0) {
|
|
178
|
+
console.log('Applications:');
|
|
179
|
+
for (const app of allApps) {
|
|
180
|
+
const localId = localApps[app];
|
|
181
|
+
const remoteId = remoteApps[app];
|
|
182
|
+
|
|
183
|
+
if (localId === remoteId) {
|
|
184
|
+
console.log(` ${app}: ${localId ?? '(none)'}`);
|
|
185
|
+
} else if (!localId) {
|
|
186
|
+
console.log(` ${app}: (none) -> ${remoteId} [REMOTE ONLY]`);
|
|
187
|
+
} else if (!remoteId) {
|
|
188
|
+
console.log(` ${app}: ${localId} -> (none) [LOCAL ONLY]`);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(
|
|
191
|
+
` ${app}: ${localId} (local) != ${remoteId} (remote) [MISMATCH]`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Compare services
|
|
198
|
+
const localServices = local?.services ?? {};
|
|
199
|
+
const remoteServices = remote?.services ?? {};
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
Object.keys(localServices).length > 0 ||
|
|
203
|
+
Object.keys(remoteServices).length > 0
|
|
204
|
+
) {
|
|
205
|
+
console.log('\nServices:');
|
|
206
|
+
const serviceKeys = new Set([
|
|
207
|
+
...Object.keys(localServices),
|
|
208
|
+
...Object.keys(remoteServices),
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
for (const key of serviceKeys) {
|
|
212
|
+
const localVal = localServices[key as keyof typeof localServices];
|
|
213
|
+
const remoteVal = remoteServices[key as keyof typeof remoteServices];
|
|
214
|
+
|
|
215
|
+
if (localVal === remoteVal) {
|
|
216
|
+
console.log(` ${key}: ${localVal ?? '(none)'}`);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(
|
|
219
|
+
` ${key}: ${localVal ?? '(none)'} (local) != ${remoteVal ?? '(none)'} (remote)`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printStateSummary(state: DokployStageState): void {
|
|
227
|
+
const appCount = Object.keys(state.applications).length;
|
|
228
|
+
const hasPostgres = !!state.services.postgresId;
|
|
229
|
+
const hasRedis = !!state.services.redisId;
|
|
230
|
+
|
|
231
|
+
console.log(` Stage: ${state.stage}`);
|
|
232
|
+
console.log(` Applications: ${appCount}`);
|
|
233
|
+
console.log(` Postgres: ${hasPostgres ? 'configured' : 'none'}`);
|
|
234
|
+
console.log(` Redis: ${hasRedis ? 'configured' : 'none'}`);
|
|
235
|
+
console.log(` Last deployed: ${state.lastDeployedAt}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function printStateDetails(state: DokployStageState): void {
|
|
239
|
+
console.log(`Stage: ${state.stage}`);
|
|
240
|
+
console.log(`Environment ID: ${state.environmentId}`);
|
|
241
|
+
console.log(`Last Deployed: ${state.lastDeployedAt}`);
|
|
242
|
+
console.log('');
|
|
243
|
+
|
|
244
|
+
console.log('Applications:');
|
|
245
|
+
const apps = Object.entries(state.applications);
|
|
246
|
+
if (apps.length === 0) {
|
|
247
|
+
console.log(' (none)');
|
|
248
|
+
} else {
|
|
249
|
+
for (const [name, id] of apps) {
|
|
250
|
+
console.log(` ${name}: ${id}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
console.log('');
|
|
254
|
+
|
|
255
|
+
console.log('Services:');
|
|
256
|
+
if (!state.services.postgresId && !state.services.redisId) {
|
|
257
|
+
console.log(' (none)');
|
|
258
|
+
} else {
|
|
259
|
+
if (state.services.postgresId) {
|
|
260
|
+
console.log(` Postgres: ${state.services.postgresId}`);
|
|
261
|
+
}
|
|
262
|
+
if (state.services.redisId) {
|
|
263
|
+
console.log(` Redis: ${state.services.redisId}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (state.dnsVerified && Object.keys(state.dnsVerified).length > 0) {
|
|
268
|
+
console.log('');
|
|
269
|
+
console.log('DNS Verified:');
|
|
270
|
+
for (const [hostname, info] of Object.entries(state.dnsVerified)) {
|
|
271
|
+
console.log(` ${hostname}: ${info.serverIp} (${info.verifiedAt})`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -8,7 +8,10 @@ describe('findSecretsRoot', () => {
|
|
|
8
8
|
let testDir: string;
|
|
9
9
|
|
|
10
10
|
beforeEach(async () => {
|
|
11
|
-
testDir = join(
|
|
11
|
+
testDir = join(
|
|
12
|
+
tmpdir(),
|
|
13
|
+
`gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
14
|
+
);
|
|
12
15
|
await mkdir(testDir, { recursive: true });
|
|
13
16
|
});
|
|
14
17
|
|
|
@@ -61,7 +64,10 @@ describe('createEntryWrapper', () => {
|
|
|
61
64
|
let testDir: string;
|
|
62
65
|
|
|
63
66
|
beforeEach(async () => {
|
|
64
|
-
testDir = join(
|
|
67
|
+
testDir = join(
|
|
68
|
+
tmpdir(),
|
|
69
|
+
`gkm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
70
|
+
);
|
|
65
71
|
await mkdir(testDir, { recursive: true });
|
|
66
72
|
});
|
|
67
73
|
|
|
@@ -1049,9 +1049,7 @@ describe('loadSecretsForApp', () => {
|
|
|
1049
1049
|
|
|
1050
1050
|
const secrets = await loadSecretsForApp(testDir);
|
|
1051
1051
|
|
|
1052
|
-
expect(secrets.DATABASE_URL).toBe(
|
|
1053
|
-
'postgresql://localhost/developmentdb',
|
|
1054
|
-
);
|
|
1052
|
+
expect(secrets.DATABASE_URL).toBe('postgresql://localhost/developmentdb');
|
|
1055
1053
|
});
|
|
1056
1054
|
|
|
1057
1055
|
it('should return empty object if no secrets exist', async () => {
|
package/src/dev/index.ts
CHANGED
|
@@ -340,7 +340,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
|
|
|
340
340
|
secretsRoot = appConfig.workspaceRoot;
|
|
341
341
|
workspaceAppName = appConfig.appName;
|
|
342
342
|
workspaceAppPort = appConfig.app.port;
|
|
343
|
-
logger.log(
|
|
343
|
+
logger.log(
|
|
344
|
+
`📦 Running app: ${appConfig.appName} on port ${workspaceAppPort}`,
|
|
345
|
+
);
|
|
344
346
|
|
|
345
347
|
// Check if app has an entry point (non-gkm app like better-auth)
|
|
346
348
|
if (appConfig.app.entry) {
|
|
@@ -1387,7 +1389,9 @@ export async function prepareEntryCredentials(options: {
|
|
|
1387
1389
|
appName = appConfig.appName;
|
|
1388
1390
|
} catch (error) {
|
|
1389
1391
|
// Not in a workspace - use defaults
|
|
1390
|
-
logger.log(
|
|
1392
|
+
logger.log(
|
|
1393
|
+
`⚠️ Could not load workspace config: ${(error as Error).message}`,
|
|
1394
|
+
);
|
|
1391
1395
|
secretsRoot = findSecretsRoot(cwd);
|
|
1392
1396
|
appName = getAppNameFromCwd(cwd) ?? undefined;
|
|
1393
1397
|
}
|
|
@@ -1457,7 +1461,9 @@ async function entryDevCommand(options: DevOptions): Promise<void> {
|
|
|
1457
1461
|
logger.log(`🚀 Starting entry file: ${entry} on port ${resolvedPort}`);
|
|
1458
1462
|
|
|
1459
1463
|
if (Object.keys(credentials).length > 1) {
|
|
1460
|
-
logger.log(
|
|
1464
|
+
logger.log(
|
|
1465
|
+
`🔐 Loaded ${Object.keys(credentials).length - 1} secret(s) + PORT`,
|
|
1466
|
+
);
|
|
1461
1467
|
}
|
|
1462
1468
|
|
|
1463
1469
|
// Create wrapper entry that injects secrets before importing user's file
|
|
@@ -614,7 +614,9 @@ describe('docker templates', () => {
|
|
|
614
614
|
const dockerfile = generateEntryDockerfile(baseOptions);
|
|
615
615
|
|
|
616
616
|
expect(dockerfile).toContain('import { createRequire } from "module"');
|
|
617
|
-
expect(dockerfile).toContain(
|
|
617
|
+
expect(dockerfile).toContain(
|
|
618
|
+
'const require = createRequire(import.meta.url)',
|
|
619
|
+
);
|
|
618
620
|
});
|
|
619
621
|
|
|
620
622
|
it('should output to dist/index.mjs', () => {
|
package/src/docker/templates.ts
CHANGED
|
@@ -293,7 +293,7 @@ WORKDIR /app
|
|
|
293
293
|
COPY . .
|
|
294
294
|
|
|
295
295
|
# Build production server using gkm
|
|
296
|
-
RUN
|
|
296
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
297
297
|
|
|
298
298
|
# Stage 3: Production
|
|
299
299
|
FROM ${baseImage} AS runner
|
|
@@ -384,7 +384,7 @@ WORKDIR /app
|
|
|
384
384
|
COPY --from=pruner /app/out/full/ ./
|
|
385
385
|
|
|
386
386
|
# Build production server using gkm
|
|
387
|
-
RUN
|
|
387
|
+
RUN ${pm.exec} gkm build --provider server --production
|
|
388
388
|
|
|
389
389
|
# Stage 4: Production
|
|
390
390
|
FROM ${baseImage} AS runner
|
|
@@ -756,7 +756,7 @@ RUN if [ -n "$GKM_ENCRYPTED_CREDENTIALS" ]; then \
|
|
|
756
756
|
fi
|
|
757
757
|
|
|
758
758
|
# Build production server using gkm
|
|
759
|
-
RUN cd ${appPath} &&
|
|
759
|
+
RUN cd ${appPath} && ${pm.exec} gkm build --provider server --production
|
|
760
760
|
|
|
761
761
|
# Stage 4: Production
|
|
762
762
|
FROM ${baseImage} AS runner
|