@geekmidas/cli 0.39.0 → 0.41.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.
Files changed (81) hide show
  1. package/dist/{bundler-CyHg1v_T.cjs → bundler-BB-kETMd.cjs} +20 -49
  2. package/dist/bundler-BB-kETMd.cjs.map +1 -0
  3. package/dist/{bundler-DQIuE3Kn.mjs → bundler-DGry2vaR.mjs} +22 -51
  4. package/dist/bundler-DGry2vaR.mjs.map +1 -0
  5. package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
  6. package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
  7. package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
  8. package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
  9. package/dist/config.cjs +2 -2
  10. package/dist/config.d.cts +1 -1
  11. package/dist/config.d.mts +2 -2
  12. package/dist/config.mjs +2 -2
  13. package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
  14. package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
  15. package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
  16. package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
  17. package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
  18. package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
  19. package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
  20. package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
  21. package/dist/encryption-Biq0EZ4m.cjs +4 -0
  22. package/dist/encryption-CQXBZGkt.mjs +3 -0
  23. package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
  24. package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
  25. package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
  26. package/dist/index-CXa3odEw.d.mts.map +1 -0
  27. package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
  28. package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
  29. package/dist/index.cjs +698 -127
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.mjs +677 -106
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
  34. package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
  35. package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
  36. package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
  37. package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
  38. package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
  39. package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
  40. package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.map} +1 -1
  41. package/dist/openapi-react-query.cjs +1 -1
  42. package/dist/openapi-react-query.mjs +1 -1
  43. package/dist/openapi.cjs +3 -3
  44. package/dist/openapi.d.mts +1 -1
  45. package/dist/openapi.mjs +3 -3
  46. package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
  47. package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
  48. package/dist/workspace/index.cjs +1 -1
  49. package/dist/workspace/index.d.cts +2 -2
  50. package/dist/workspace/index.d.mts +3 -3
  51. package/dist/workspace/index.mjs +1 -1
  52. package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
  53. package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
  54. package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
  55. package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
  56. package/package.json +5 -5
  57. package/src/build/bundler.ts +27 -79
  58. package/src/deploy/__tests__/domain.spec.ts +231 -0
  59. package/src/deploy/__tests__/secrets.spec.ts +300 -0
  60. package/src/deploy/__tests__/sniffer.spec.ts +221 -0
  61. package/src/deploy/docker.ts +40 -11
  62. package/src/deploy/dokploy-api.ts +99 -0
  63. package/src/deploy/domain.ts +125 -0
  64. package/src/deploy/index.ts +366 -148
  65. package/src/deploy/secrets.ts +182 -0
  66. package/src/deploy/sniffer.ts +180 -0
  67. package/src/dev/index.ts +11 -0
  68. package/src/docker/index.ts +24 -5
  69. package/src/docker/templates.ts +187 -1
  70. package/src/init/templates/api.ts +4 -4
  71. package/src/init/versions.ts +2 -2
  72. package/src/workspace/index.ts +2 -0
  73. package/src/workspace/schema.ts +32 -6
  74. package/src/workspace/types.ts +64 -2
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/dist/bundler-CyHg1v_T.cjs.map +0 -1
  77. package/dist/bundler-DQIuE3Kn.mjs.map +0 -1
  78. package/dist/dokploy-api-B0w17y4_.mjs +0 -3
  79. package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
  80. package/dist/index-C7TkoYmt.d.mts.map +0 -1
  81. package/dist/index-CpchsC9w.d.cts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -48,11 +48,11 @@
48
48
  "lodash.kebabcase": "^4.1.1",
49
49
  "openapi-typescript": "^7.4.2",
50
50
  "prompts": "~2.4.2",
51
- "@geekmidas/constructs": "~0.6.0",
52
- "@geekmidas/envkit": "~0.5.0",
53
- "@geekmidas/schema": "~0.1.0",
51
+ "@geekmidas/logger": "~0.4.0",
52
+ "@geekmidas/constructs": "~0.7.0",
54
53
  "@geekmidas/errors": "~0.1.0",
55
- "@geekmidas/logger": "~0.4.0"
54
+ "@geekmidas/schema": "~0.1.0",
55
+ "@geekmidas/envkit": "~0.6.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/lodash.kebabcase": "^4.1.9",
@@ -1,50 +1,15 @@
1
- import { execSync, spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { mkdir, rename, writeFile } from 'node:fs/promises';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
4
3
  import { join } from 'node:path';
5
4
  import type { Construct } from '@geekmidas/constructs';
6
5
 
7
- const MIN_TSDOWN_VERSION = '0.11.0';
8
-
9
6
  /**
10
- * Check if tsdown is installed and meets minimum version requirement
7
+ * Banner to inject into ESM bundle for CJS compatibility.
8
+ * Creates a `require` function using Node's createRequire for packages
9
+ * that internally use CommonJS require() for Node builtins.
11
10
  */
12
- function checkTsdownVersion(): void {
13
- try {
14
- const result = execSync('npx tsdown --version', {
15
- encoding: 'utf-8',
16
- stdio: ['pipe', 'pipe', 'pipe'],
17
- });
18
- // Output format: "tsdown/0.12.8 darwin-arm64 node-v22.21.1"
19
- const match = result.match(/tsdown\/(\d+\.\d+\.\d+)/);
20
- if (match) {
21
- const version = match[1]!;
22
- const [major, minor] = version.split('.').map(Number) as [number, number];
23
- const [minMajor, minMinor] = MIN_TSDOWN_VERSION.split('.').map(
24
- Number,
25
- ) as [number, number];
26
-
27
- if (major < minMajor || (major === minMajor && minor < minMinor)) {
28
- throw new Error(
29
- `tsdown version ${version} is too old. Please upgrade to ${MIN_TSDOWN_VERSION} or later:\n` +
30
- ' npm install -D tsdown@latest\n' +
31
- ' # or\n' +
32
- ' pnpm add -D tsdown@latest',
33
- );
34
- }
35
- }
36
- } catch (error) {
37
- if (error instanceof Error && error.message.includes('too old')) {
38
- throw error;
39
- }
40
- throw new Error(
41
- 'tsdown is required for bundling. Please install it:\n' +
42
- ' npm install -D tsdown@latest\n' +
43
- ' # or\n' +
44
- ' pnpm add -D tsdown@latest',
45
- );
46
- }
47
- }
11
+ const ESM_CJS_COMPAT_BANNER =
12
+ 'import { createRequire } from "module"; const require = createRequire(import.meta.url);';
48
13
 
49
14
  export interface BundleOptions {
50
15
  /** Entry point file (e.g., .gkm/server/server.ts) */
@@ -97,11 +62,13 @@ async function collectRequiredEnvVars(
97
62
  }
98
63
 
99
64
  /**
100
- * Bundle the server application using tsdown
65
+ * Bundle the server application using esbuild.
66
+ * Creates a fully standalone bundle with all dependencies included.
101
67
  *
102
68
  * @param options - Bundle configuration options
103
69
  * @returns Bundle result with output path and optional master key
104
70
  */
71
+
105
72
  /** Default env var values for docker compose services */
106
73
  const DOCKER_SERVICE_ENV_VARS: Record<string, Record<string, string>> = {
107
74
  postgres: {
@@ -129,27 +96,23 @@ export async function bundleServer(
129
96
  dockerServices,
130
97
  } = options;
131
98
 
132
- // Check tsdown version first
133
- checkTsdownVersion();
134
-
135
99
  // Ensure output directory exists
136
100
  await mkdir(outputDir, { recursive: true });
137
101
 
138
- // Build command-line arguments for tsdown
102
+ const mjsOutput = join(outputDir, 'server.mjs');
103
+
104
+ // Build command-line arguments for esbuild
139
105
  const args = [
140
106
  'npx',
141
- 'tsdown',
107
+ 'esbuild',
142
108
  entryPoint,
143
- '--no-config', // Don't use any config file from workspace
144
- '--out-dir',
145
- outputDir,
146
- '--format',
147
- 'esm',
148
- '--platform',
149
- 'node',
150
- '--target',
151
- 'node22',
152
- '--clean',
109
+ '--bundle',
110
+ '--platform=node',
111
+ '--target=node22',
112
+ '--format=esm',
113
+ `--outfile=${mjsOutput}`,
114
+ '--packages=bundle', // Bundle all dependencies for standalone output
115
+ `--banner:js=${ESM_CJS_COMPAT_BANNER}`, // CJS compatibility for packages like pino
153
116
  ];
154
117
 
155
118
  if (minify) {
@@ -160,14 +123,11 @@ export async function bundleServer(
160
123
  args.push('--sourcemap');
161
124
  }
162
125
 
163
- // Add external packages
126
+ // Add external packages (user-specified)
164
127
  for (const ext of external) {
165
- args.push('--external', ext);
128
+ args.push(`--external:${ext}`);
166
129
  }
167
130
 
168
- // Always exclude node: builtins
169
- args.push('--external', 'node:*');
170
-
171
131
  // Handle secrets injection if stage is provided
172
132
  let masterKey: string | undefined;
173
133
 
@@ -252,21 +212,17 @@ export async function bundleServer(
252
212
  const encrypted = encryptSecrets(embeddable);
253
213
  masterKey = encrypted.masterKey;
254
214
 
255
- // Add define options for build-time injection using tsdown's --env.* format
215
+ // Add define options for build-time injection using esbuild's --define:KEY=VALUE format
256
216
  const defines = generateDefineOptions(encrypted);
257
217
  for (const [key, value] of Object.entries(defines)) {
258
- args.push(`--env.${key}`, value);
218
+ args.push(`--define:${key}=${JSON.stringify(value)}`);
259
219
  }
260
220
 
261
221
  console.log(` Secrets encrypted for stage "${stage}"`);
262
222
  }
263
223
 
264
- const mjsOutput = join(outputDir, 'server.mjs');
265
-
266
224
  try {
267
- // Run tsdown with command-line arguments
268
- // Use spawnSync with args array to avoid shell escaping issues with --define values
269
- // args is always populated with ['npx', 'tsdown', ...] so cmd is never undefined
225
+ // Run esbuild with command-line arguments
270
226
  const [cmd, ...cmdArgs] = args as [string, ...string[]];
271
227
  const result = spawnSync(cmd, cmdArgs, {
272
228
  cwd: process.cwd(),
@@ -278,15 +234,7 @@ export async function bundleServer(
278
234
  throw result.error;
279
235
  }
280
236
  if (result.status !== 0) {
281
- throw new Error(`tsdown exited with code ${result.status}`);
282
- }
283
-
284
- // Rename output to .mjs for explicit ESM
285
- // tsdown outputs as server.js for ESM format
286
- const jsOutput = join(outputDir, 'server.js');
287
-
288
- if (existsSync(jsOutput)) {
289
- await rename(jsOutput, mjsOutput);
237
+ throw new Error(`esbuild exited with code ${result.status}`);
290
238
  }
291
239
 
292
240
  // Add shebang to the bundled file
@@ -0,0 +1,231 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { NormalizedAppConfig } from '../../workspace/types';
3
+ import {
4
+ generatePublicUrlBuildArgs,
5
+ getPublicUrlArgNames,
6
+ isMainFrontendApp,
7
+ resolveHost,
8
+ } from '../domain';
9
+
10
+ describe('resolveHost', () => {
11
+ const dokployConfig = {
12
+ endpoint: 'https://dokploy.example.com',
13
+ projectId: 'test-project',
14
+ domains: {
15
+ development: 'dev.myapp.com',
16
+ staging: 'staging.myapp.com',
17
+ production: 'myapp.com',
18
+ },
19
+ };
20
+
21
+ const createApp = (
22
+ overrides: Partial<NormalizedAppConfig> = {},
23
+ ): NormalizedAppConfig => ({
24
+ type: 'backend',
25
+ path: 'apps/api',
26
+ port: 3000,
27
+ dependencies: [],
28
+ resolvedDeployTarget: 'dokploy',
29
+ ...overrides,
30
+ });
31
+
32
+ it('should return explicit app domain override (string)', () => {
33
+ const app = createApp({ domain: 'api.custom.com' });
34
+ const host = resolveHost('api', app, 'production', dokployConfig, false);
35
+ expect(host).toBe('api.custom.com');
36
+ });
37
+
38
+ it('should return stage-specific domain override', () => {
39
+ const app = createApp({
40
+ domain: {
41
+ production: 'login.myapp.com',
42
+ staging: 'login.staging.myapp.com',
43
+ },
44
+ });
45
+ const host = resolveHost('auth', app, 'production', dokployConfig, false);
46
+ expect(host).toBe('login.myapp.com');
47
+ });
48
+
49
+ it('should fallback to base domain pattern when no stage match in override', () => {
50
+ const app = createApp({
51
+ domain: { production: 'custom.myapp.com' },
52
+ });
53
+ const host = resolveHost('api', app, 'development', dokployConfig, false);
54
+ expect(host).toBe('api.dev.myapp.com');
55
+ });
56
+
57
+ it('should return base domain for main frontend app', () => {
58
+ const app = createApp({ type: 'frontend' });
59
+ const host = resolveHost('web', app, 'production', dokployConfig, true);
60
+ expect(host).toBe('myapp.com');
61
+ });
62
+
63
+ it('should return prefixed domain for non-main apps', () => {
64
+ const app = createApp();
65
+ const host = resolveHost('api', app, 'production', dokployConfig, false);
66
+ expect(host).toBe('api.myapp.com');
67
+ });
68
+
69
+ it('should use correct base domain for each stage', () => {
70
+ const app = createApp();
71
+
72
+ expect(resolveHost('api', app, 'development', dokployConfig, false)).toBe(
73
+ 'api.dev.myapp.com',
74
+ );
75
+ expect(resolveHost('api', app, 'staging', dokployConfig, false)).toBe(
76
+ 'api.staging.myapp.com',
77
+ );
78
+ expect(resolveHost('api', app, 'production', dokployConfig, false)).toBe(
79
+ 'api.myapp.com',
80
+ );
81
+ });
82
+
83
+ it('should throw error when no domain configured for stage', () => {
84
+ const app = createApp();
85
+ expect(() =>
86
+ resolveHost('api', app, 'unknown-stage', dokployConfig, false),
87
+ ).toThrow('No domain configured for stage "unknown-stage"');
88
+ });
89
+
90
+ it('should throw error when dokployConfig has no domains', () => {
91
+ const app = createApp();
92
+ const configWithoutDomains = {
93
+ endpoint: 'https://dokploy.example.com',
94
+ projectId: 'test-project',
95
+ };
96
+ expect(() =>
97
+ resolveHost('api', app, 'production', configWithoutDomains, false),
98
+ ).toThrow('No domain configured for stage "production"');
99
+ });
100
+ });
101
+
102
+ describe('isMainFrontendApp', () => {
103
+ const createApp = (
104
+ type: 'backend' | 'frontend',
105
+ ): NormalizedAppConfig => ({
106
+ type,
107
+ path: 'apps/test',
108
+ port: 3000,
109
+ dependencies: [],
110
+ resolvedDeployTarget: 'dokploy',
111
+ });
112
+
113
+ it('should return false for backend apps', () => {
114
+ const apps = {
115
+ api: createApp('backend'),
116
+ web: createApp('frontend'),
117
+ };
118
+ expect(isMainFrontendApp('api', apps.api, apps)).toBe(false);
119
+ });
120
+
121
+ it('should return true for app named "web" if it is frontend', () => {
122
+ const apps = {
123
+ api: createApp('backend'),
124
+ web: createApp('frontend'),
125
+ admin: createApp('frontend'),
126
+ };
127
+ expect(isMainFrontendApp('web', apps.web, apps)).toBe(true);
128
+ });
129
+
130
+ it('should return true for first frontend app when no "web" app', () => {
131
+ const apps = {
132
+ api: createApp('backend'),
133
+ dashboard: createApp('frontend'),
134
+ admin: createApp('frontend'),
135
+ };
136
+ expect(isMainFrontendApp('dashboard', apps.dashboard, apps)).toBe(true);
137
+ expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
138
+ });
139
+
140
+ it('should return false for non-first frontend when no "web" app', () => {
141
+ const apps = {
142
+ api: createApp('backend'),
143
+ dashboard: createApp('frontend'),
144
+ admin: createApp('frontend'),
145
+ };
146
+ expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
147
+ });
148
+ });
149
+
150
+ describe('generatePublicUrlBuildArgs', () => {
151
+ const createApp = (dependencies: string[]): NormalizedAppConfig => ({
152
+ type: 'frontend',
153
+ path: 'apps/web',
154
+ port: 3001,
155
+ dependencies,
156
+ resolvedDeployTarget: 'dokploy',
157
+ });
158
+
159
+ it('should generate build args for dependencies', () => {
160
+ const app = createApp(['api', 'auth']);
161
+ const deployedUrls = {
162
+ api: 'https://api.myapp.com',
163
+ auth: 'https://auth.myapp.com',
164
+ };
165
+
166
+ const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
167
+
168
+ expect(buildArgs).toEqual([
169
+ 'NEXT_PUBLIC_API_URL=https://api.myapp.com',
170
+ 'NEXT_PUBLIC_AUTH_URL=https://auth.myapp.com',
171
+ ]);
172
+ });
173
+
174
+ it('should skip missing dependencies', () => {
175
+ const app = createApp(['api', 'auth', 'missing']);
176
+ const deployedUrls = {
177
+ api: 'https://api.myapp.com',
178
+ // auth and missing are not deployed yet
179
+ };
180
+
181
+ const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
182
+
183
+ expect(buildArgs).toEqual(['NEXT_PUBLIC_API_URL=https://api.myapp.com']);
184
+ });
185
+
186
+ it('should return empty array when no dependencies', () => {
187
+ const app = createApp([]);
188
+ const deployedUrls = { api: 'https://api.myapp.com' };
189
+
190
+ const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
191
+
192
+ expect(buildArgs).toEqual([]);
193
+ });
194
+
195
+ it('should handle uppercase conversion correctly', () => {
196
+ const app = createApp(['my-api', 'auth-service']);
197
+ const deployedUrls = {
198
+ 'my-api': 'https://my-api.myapp.com',
199
+ 'auth-service': 'https://auth-service.myapp.com',
200
+ };
201
+
202
+ const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
203
+
204
+ expect(buildArgs).toEqual([
205
+ 'NEXT_PUBLIC_MY-API_URL=https://my-api.myapp.com',
206
+ 'NEXT_PUBLIC_AUTH-SERVICE_URL=https://auth-service.myapp.com',
207
+ ]);
208
+ });
209
+ });
210
+
211
+ describe('getPublicUrlArgNames', () => {
212
+ const createApp = (dependencies: string[]): NormalizedAppConfig => ({
213
+ type: 'frontend',
214
+ path: 'apps/web',
215
+ port: 3001,
216
+ dependencies,
217
+ resolvedDeployTarget: 'dokploy',
218
+ });
219
+
220
+ it('should return arg names for dependencies', () => {
221
+ const app = createApp(['api', 'auth']);
222
+ const argNames = getPublicUrlArgNames(app);
223
+ expect(argNames).toEqual(['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL']);
224
+ });
225
+
226
+ it('should return empty array when no dependencies', () => {
227
+ const app = createApp([]);
228
+ const argNames = getPublicUrlArgNames(app);
229
+ expect(argNames).toEqual([]);
230
+ });
231
+ });