@geekmidas/cli 0.14.0 → 0.15.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-DWctKN1z.mjs → bundler-D7cM_FWw.mjs} +9 -4
- package/dist/bundler-D7cM_FWw.mjs.map +1 -0
- package/dist/{bundler-BjholBlA.cjs → bundler-Nuew7Xcn.cjs} +9 -4
- package/dist/bundler-Nuew7Xcn.cjs.map +1 -0
- package/dist/index.cjs +73 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +74 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/build/__tests__/bundler.spec.ts +4 -3
- package/src/build/bundler.ts +8 -4
- package/src/deploy/__tests__/docker.spec.ts +44 -6
- package/src/deploy/__tests__/index.spec.ts +62 -0
- package/src/deploy/docker.ts +77 -2
- package/src/deploy/index.ts +63 -20
- package/src/deploy/types.ts +5 -1
- package/dist/bundler-BjholBlA.cjs.map +0 -1
- package/dist/bundler-DWctKN1z.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@geekmidas/constructs": "~0.5.0",
|
|
57
|
-
"@geekmidas/envkit": "~0.3.0",
|
|
58
57
|
"@geekmidas/schema": "~0.1.0",
|
|
59
58
|
"@geekmidas/telescope": "~0.4.0",
|
|
59
|
+
"@geekmidas/envkit": "~0.3.0",
|
|
60
60
|
"@geekmidas/logger": "~0.4.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
@@ -371,10 +371,10 @@ describe('bundleServer environment validation', () => {
|
|
|
371
371
|
);
|
|
372
372
|
|
|
373
373
|
itWithDir(
|
|
374
|
-
'should
|
|
374
|
+
'should auto-initialize secrets and throw for missing env vars',
|
|
375
375
|
async ({ dir }) => {
|
|
376
376
|
const entryPoint = await createEntryPoint(dir);
|
|
377
|
-
// Don't create secrets file
|
|
377
|
+
// Don't create secrets file - it should be auto-initialized
|
|
378
378
|
|
|
379
379
|
const constructs = [createMockConstruct(['DATABASE_URL'])];
|
|
380
380
|
|
|
@@ -382,6 +382,7 @@ describe('bundleServer environment validation', () => {
|
|
|
382
382
|
process.chdir(dir);
|
|
383
383
|
|
|
384
384
|
try {
|
|
385
|
+
// Should auto-initialize secrets but then fail because DATABASE_URL is required
|
|
385
386
|
await expect(
|
|
386
387
|
bundleServer({
|
|
387
388
|
entryPoint,
|
|
@@ -392,7 +393,7 @@ describe('bundleServer environment validation', () => {
|
|
|
392
393
|
stage: 'production',
|
|
393
394
|
constructs,
|
|
394
395
|
}),
|
|
395
|
-
).rejects.toThrow('
|
|
396
|
+
).rejects.toThrow('Missing environment variables');
|
|
396
397
|
} finally {
|
|
397
398
|
process.chdir(originalCwd);
|
|
398
399
|
}
|
package/src/build/bundler.ts
CHANGED
|
@@ -131,17 +131,21 @@ export async function bundleServer(
|
|
|
131
131
|
readStageSecrets,
|
|
132
132
|
toEmbeddableSecrets,
|
|
133
133
|
validateEnvironmentVariables,
|
|
134
|
+
initStageSecrets,
|
|
135
|
+
writeStageSecrets,
|
|
134
136
|
} = await import('../secrets/storage');
|
|
135
137
|
const { encryptSecrets, generateDefineOptions } = await import(
|
|
136
138
|
'../secrets/encryption'
|
|
137
139
|
);
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
let secrets = await readStageSecrets(stage);
|
|
140
142
|
|
|
141
143
|
if (!secrets) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
144
|
+
// Auto-initialize secrets for the stage
|
|
145
|
+
console.log(` Initializing secrets for stage "${stage}"...`);
|
|
146
|
+
secrets = initStageSecrets(stage);
|
|
147
|
+
await writeStageSecrets(secrets);
|
|
148
|
+
console.log(` ✓ Created .gkm/secrets/${stage}.json`);
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
// Auto-populate env vars from docker compose services
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { GkmConfig } from '../../types';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getAppNameFromCwd,
|
|
5
|
+
getAppNameFromPackageJson,
|
|
6
|
+
getImageRef,
|
|
7
|
+
resolveDockerConfig,
|
|
8
|
+
} from '../docker';
|
|
4
9
|
|
|
5
10
|
describe('getImageRef', () => {
|
|
6
11
|
it('should return image with registry prefix', () => {
|
|
@@ -36,8 +41,26 @@ describe('getImageRef', () => {
|
|
|
36
41
|
});
|
|
37
42
|
});
|
|
38
43
|
|
|
44
|
+
describe('getAppNameFromCwd', () => {
|
|
45
|
+
it('should return app name from package.json in current directory', () => {
|
|
46
|
+
// Tests run from the monorepo root, so cwd is the root directory
|
|
47
|
+
const appName = getAppNameFromCwd();
|
|
48
|
+
// The root package.json has name "@geekmidas/toolbox", so it should strip the scope
|
|
49
|
+
expect(appName).toBe('toolbox');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('getAppNameFromPackageJson', () => {
|
|
54
|
+
it('should return app name from package.json adjacent to lockfile', () => {
|
|
55
|
+
// This test runs in the toolbox monorepo, so it should find the root package.json
|
|
56
|
+
const appName = getAppNameFromPackageJson();
|
|
57
|
+
// The root package.json has name "@geekmidas/toolbox", so it should strip the scope
|
|
58
|
+
expect(appName).toBe('toolbox');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
39
62
|
describe('resolveDockerConfig', () => {
|
|
40
|
-
it('should
|
|
63
|
+
it('should fallback to package.json name when docker not configured', () => {
|
|
41
64
|
const config: GkmConfig = {
|
|
42
65
|
routes: './src/endpoints',
|
|
43
66
|
envParser: './src/env',
|
|
@@ -47,7 +70,8 @@ describe('resolveDockerConfig', () => {
|
|
|
47
70
|
const dockerConfig = resolveDockerConfig(config);
|
|
48
71
|
|
|
49
72
|
expect(dockerConfig.registry).toBeUndefined();
|
|
50
|
-
|
|
73
|
+
// Should fallback to package.json name or 'app'
|
|
74
|
+
expect(dockerConfig.imageName).toBeDefined();
|
|
51
75
|
});
|
|
52
76
|
|
|
53
77
|
it('should return registry from docker config', () => {
|
|
@@ -97,7 +121,7 @@ describe('resolveDockerConfig', () => {
|
|
|
97
121
|
expect(dockerConfig.imageName).toBe('backend-api');
|
|
98
122
|
});
|
|
99
123
|
|
|
100
|
-
it('should
|
|
124
|
+
it('should fallback to package.json name when docker object is empty', () => {
|
|
101
125
|
const config: GkmConfig = {
|
|
102
126
|
routes: './src/endpoints',
|
|
103
127
|
docker: {},
|
|
@@ -106,6 +130,20 @@ describe('resolveDockerConfig', () => {
|
|
|
106
130
|
const dockerConfig = resolveDockerConfig(config);
|
|
107
131
|
|
|
108
132
|
expect(dockerConfig.registry).toBeUndefined();
|
|
109
|
-
|
|
133
|
+
// Should fallback to package.json name or 'app'
|
|
134
|
+
expect(dockerConfig.imageName).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should prefer explicit imageName over package.json', () => {
|
|
138
|
+
const config: GkmConfig = {
|
|
139
|
+
routes: './src/endpoints',
|
|
140
|
+
docker: {
|
|
141
|
+
imageName: 'explicit-name',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const dockerConfig = resolveDockerConfig(config);
|
|
146
|
+
|
|
147
|
+
expect(dockerConfig.imageName).toBe('explicit-name');
|
|
110
148
|
});
|
|
111
149
|
});
|
|
@@ -133,6 +133,39 @@ describe('provisionServices', () => {
|
|
|
133
133
|
);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
it('should provision postgres and return individual connection parameters', async () => {
|
|
137
|
+
server.use(
|
|
138
|
+
http.post(`${BASE_URL}/api/postgres.create`, async ({ request }) => {
|
|
139
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
140
|
+
return HttpResponse.json({
|
|
141
|
+
postgresId: 'pg_123',
|
|
142
|
+
name: 'myapp-db',
|
|
143
|
+
appName: 'myapp-db',
|
|
144
|
+
databaseName: 'mydb',
|
|
145
|
+
databaseUser: 'dbuser',
|
|
146
|
+
databasePassword: body.databasePassword,
|
|
147
|
+
applicationStatus: 'idle',
|
|
148
|
+
});
|
|
149
|
+
}),
|
|
150
|
+
http.post(`${BASE_URL}/api/postgres.deploy`, () => {
|
|
151
|
+
return HttpResponse.json({ success: true });
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
156
|
+
|
|
157
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
158
|
+
postgres: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
expect(result?.DATABASE_HOST).toBe('myapp-db');
|
|
163
|
+
expect(result?.DATABASE_PORT).toBe('5432');
|
|
164
|
+
expect(result?.DATABASE_NAME).toBe('mydb');
|
|
165
|
+
expect(result?.DATABASE_USER).toBe('dbuser');
|
|
166
|
+
expect(result?.DATABASE_PASSWORD).toMatch(/^[a-f0-9]{32}$/);
|
|
167
|
+
});
|
|
168
|
+
|
|
136
169
|
it('should provision redis and return REDIS_URL', async () => {
|
|
137
170
|
server.use(
|
|
138
171
|
http.post(`${BASE_URL}/api/redis.create`, async ({ request }) => {
|
|
@@ -162,6 +195,35 @@ describe('provisionServices', () => {
|
|
|
162
195
|
);
|
|
163
196
|
});
|
|
164
197
|
|
|
198
|
+
it('should provision redis and return individual connection parameters', async () => {
|
|
199
|
+
server.use(
|
|
200
|
+
http.post(`${BASE_URL}/api/redis.create`, async ({ request }) => {
|
|
201
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
202
|
+
return HttpResponse.json({
|
|
203
|
+
redisId: 'redis_123',
|
|
204
|
+
name: 'myapp-cache',
|
|
205
|
+
appName: 'myapp-cache',
|
|
206
|
+
databasePassword: body.databasePassword,
|
|
207
|
+
applicationStatus: 'idle',
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
http.post(`${BASE_URL}/api/redis.deploy`, () => {
|
|
211
|
+
return HttpResponse.json({ success: true });
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
216
|
+
|
|
217
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
218
|
+
redis: true,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result).toBeDefined();
|
|
222
|
+
expect(result?.REDIS_HOST).toBe('myapp-cache');
|
|
223
|
+
expect(result?.REDIS_PORT).toBe('6379');
|
|
224
|
+
expect(result?.REDIS_PASSWORD).toMatch(/^[a-f0-9]{32}$/);
|
|
225
|
+
});
|
|
226
|
+
|
|
165
227
|
it('should provision both postgres and redis', async () => {
|
|
166
228
|
server.use(
|
|
167
229
|
http.post(`${BASE_URL}/api/postgres.create`, async ({ request }) => {
|
package/src/deploy/docker.ts
CHANGED
|
@@ -1,8 +1,68 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
import type { GkmConfig } from '../config';
|
|
3
5
|
import { dockerCommand, findLockfilePath, isMonorepo } from '../docker';
|
|
4
6
|
import type { DeployResult, DockerDeployConfig } from './types';
|
|
5
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Get app name from package.json in the current working directory
|
|
10
|
+
* Used for Dokploy app/project naming
|
|
11
|
+
*/
|
|
12
|
+
export function getAppNameFromCwd(): string | undefined {
|
|
13
|
+
const packageJsonPath = join(process.cwd(), 'package.json');
|
|
14
|
+
|
|
15
|
+
if (!existsSync(packageJsonPath)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
21
|
+
if (pkg.name) {
|
|
22
|
+
// Strip org scope if present (e.g., @myorg/app -> app)
|
|
23
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Ignore parse errors
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get app name from package.json adjacent to the lockfile (project root)
|
|
34
|
+
* Used for Docker image naming
|
|
35
|
+
*/
|
|
36
|
+
export function getAppNameFromPackageJson(): string | undefined {
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
|
|
39
|
+
// Find the lockfile to determine the project root
|
|
40
|
+
const lockfilePath = findLockfilePath(cwd);
|
|
41
|
+
if (!lockfilePath) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Use the package.json adjacent to the lockfile
|
|
46
|
+
const projectRoot = dirname(lockfilePath);
|
|
47
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
48
|
+
|
|
49
|
+
if (!existsSync(packageJsonPath)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
55
|
+
if (pkg.name) {
|
|
56
|
+
// Strip org scope if present (e.g., @myorg/app -> app)
|
|
57
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignore parse errors
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
6
66
|
const logger = console;
|
|
7
67
|
|
|
8
68
|
export interface DockerDeployOptions {
|
|
@@ -110,7 +170,8 @@ export async function deployDocker(
|
|
|
110
170
|
): Promise<DeployResult> {
|
|
111
171
|
const { stage, tag, skipPush, masterKey, config } = options;
|
|
112
172
|
|
|
113
|
-
|
|
173
|
+
// imageName should always be set by resolveDockerConfig
|
|
174
|
+
const imageName = config.imageName!;
|
|
114
175
|
const imageRef = getImageRef(config.registry, imageName, tag);
|
|
115
176
|
|
|
116
177
|
// Build image
|
|
@@ -148,10 +209,24 @@ export async function deployDocker(
|
|
|
148
209
|
|
|
149
210
|
/**
|
|
150
211
|
* Resolve Docker deploy config from gkm config
|
|
212
|
+
* - imageName: from config, or cwd package.json, or 'app' (for Docker image)
|
|
213
|
+
* - projectName: from root package.json, or 'app' (for Dokploy project)
|
|
214
|
+
* - appName: from cwd package.json, or projectName (for Dokploy app within project)
|
|
151
215
|
*/
|
|
152
216
|
export function resolveDockerConfig(config: GkmConfig): DockerDeployConfig {
|
|
217
|
+
// projectName comes from root package.json (monorepo name)
|
|
218
|
+
const projectName = getAppNameFromPackageJson() ?? 'app';
|
|
219
|
+
|
|
220
|
+
// appName comes from cwd package.json (the app being deployed)
|
|
221
|
+
const appName = getAppNameFromCwd() ?? projectName;
|
|
222
|
+
|
|
223
|
+
// imageName defaults to appName (cwd package.json)
|
|
224
|
+
const imageName = config.docker?.imageName ?? appName;
|
|
225
|
+
|
|
153
226
|
return {
|
|
154
227
|
registry: config.docker?.registry,
|
|
155
|
-
imageName
|
|
228
|
+
imageName,
|
|
229
|
+
projectName,
|
|
230
|
+
appName,
|
|
156
231
|
};
|
|
157
232
|
}
|
package/src/deploy/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
DeployOptions,
|
|
18
18
|
DeployProvider,
|
|
19
19
|
DeployResult,
|
|
20
|
+
DockerDeployConfig,
|
|
20
21
|
DokployDeployConfig,
|
|
21
22
|
} from './types';
|
|
22
23
|
|
|
@@ -76,15 +77,28 @@ interface DockerComposeServices {
|
|
|
76
77
|
rabbitmq?: boolean;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Service URLs including both connection URLs and individual parameters
|
|
82
|
+
*/
|
|
83
|
+
interface ServiceUrls {
|
|
84
|
+
DATABASE_URL?: string;
|
|
85
|
+
DATABASE_HOST?: string;
|
|
86
|
+
DATABASE_PORT?: string;
|
|
87
|
+
DATABASE_NAME?: string;
|
|
88
|
+
DATABASE_USER?: string;
|
|
89
|
+
DATABASE_PASSWORD?: string;
|
|
90
|
+
REDIS_URL?: string;
|
|
91
|
+
REDIS_HOST?: string;
|
|
92
|
+
REDIS_PORT?: string;
|
|
93
|
+
REDIS_PASSWORD?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
/**
|
|
80
97
|
* Result of Dokploy setup including provisioned service URLs
|
|
81
98
|
*/
|
|
82
99
|
interface DokploySetupResult {
|
|
83
100
|
config: DokployDeployConfig;
|
|
84
|
-
serviceUrls?:
|
|
85
|
-
DATABASE_URL?: string;
|
|
86
|
-
REDIS_URL?: string;
|
|
87
|
-
};
|
|
101
|
+
serviceUrls?: ServiceUrls;
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
/**
|
|
@@ -97,8 +111,8 @@ export async function provisionServices(
|
|
|
97
111
|
environmentId: string | undefined,
|
|
98
112
|
appName: string,
|
|
99
113
|
services?: DockerComposeServices,
|
|
100
|
-
existingUrls?:
|
|
101
|
-
): Promise<
|
|
114
|
+
existingUrls?: Pick<ServiceUrls, 'DATABASE_URL' | 'REDIS_URL'>,
|
|
115
|
+
): Promise<ServiceUrls | undefined> {
|
|
102
116
|
logger.log(
|
|
103
117
|
`\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
|
|
104
118
|
);
|
|
@@ -107,7 +121,7 @@ export async function provisionServices(
|
|
|
107
121
|
return undefined;
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
const serviceUrls:
|
|
124
|
+
const serviceUrls: ServiceUrls = {};
|
|
111
125
|
|
|
112
126
|
if (services.postgres) {
|
|
113
127
|
// Skip if DATABASE_URL already exists in secrets
|
|
@@ -134,9 +148,16 @@ export async function provisionServices(
|
|
|
134
148
|
await api.deployPostgres(postgres.postgresId);
|
|
135
149
|
logger.log(' ✓ PostgreSQL deployed');
|
|
136
150
|
|
|
151
|
+
// Store individual connection parameters
|
|
152
|
+
serviceUrls.DATABASE_HOST = postgres.appName;
|
|
153
|
+
serviceUrls.DATABASE_PORT = '5432';
|
|
154
|
+
serviceUrls.DATABASE_NAME = postgres.databaseName;
|
|
155
|
+
serviceUrls.DATABASE_USER = postgres.databaseUser;
|
|
156
|
+
serviceUrls.DATABASE_PASSWORD = postgres.databasePassword;
|
|
157
|
+
|
|
137
158
|
// Construct connection URL using internal docker network hostname
|
|
138
159
|
serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
|
|
139
|
-
logger.log(` ✓
|
|
160
|
+
logger.log(` ✓ Database credentials configured`);
|
|
140
161
|
} catch (error) {
|
|
141
162
|
const message =
|
|
142
163
|
error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -179,12 +200,19 @@ export async function provisionServices(
|
|
|
179
200
|
await api.deployRedis(redis.redisId);
|
|
180
201
|
logger.log(' ✓ Redis deployed');
|
|
181
202
|
|
|
203
|
+
// Store individual connection parameters
|
|
204
|
+
serviceUrls.REDIS_HOST = redis.appName;
|
|
205
|
+
serviceUrls.REDIS_PORT = '6379';
|
|
206
|
+
if (redis.databasePassword) {
|
|
207
|
+
serviceUrls.REDIS_PASSWORD = redis.databasePassword;
|
|
208
|
+
}
|
|
209
|
+
|
|
182
210
|
// Construct connection URL
|
|
183
211
|
const password = redis.databasePassword
|
|
184
212
|
? `:${redis.databasePassword}@`
|
|
185
213
|
: '';
|
|
186
214
|
serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
|
|
187
|
-
logger.log(` ✓
|
|
215
|
+
logger.log(` ✓ Redis credentials configured`);
|
|
188
216
|
} catch (error) {
|
|
189
217
|
const message =
|
|
190
218
|
error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -208,7 +236,7 @@ export async function provisionServices(
|
|
|
208
236
|
*/
|
|
209
237
|
async function ensureDokploySetup(
|
|
210
238
|
config: GkmConfig,
|
|
211
|
-
dockerConfig:
|
|
239
|
+
dockerConfig: DockerDeployConfig,
|
|
212
240
|
stage: string,
|
|
213
241
|
services?: DockerComposeServices,
|
|
214
242
|
): Promise<DokploySetupResult> {
|
|
@@ -301,7 +329,7 @@ async function ensureDokploySetup(
|
|
|
301
329
|
api,
|
|
302
330
|
existingConfig.projectId,
|
|
303
331
|
environmentId,
|
|
304
|
-
dockerConfig.
|
|
332
|
+
dockerConfig.appName!,
|
|
305
333
|
services,
|
|
306
334
|
existingUrls,
|
|
307
335
|
);
|
|
@@ -323,7 +351,7 @@ async function ensureDokploySetup(
|
|
|
323
351
|
|
|
324
352
|
// Step 3: Find or create project
|
|
325
353
|
logger.log('\n📁 Looking for project...');
|
|
326
|
-
const projectName = dockerConfig.
|
|
354
|
+
const projectName = dockerConfig.projectName!;
|
|
327
355
|
const projects = await api.listProjects();
|
|
328
356
|
let project = projects.find(
|
|
329
357
|
(p) => p.name.toLowerCase() === projectName.toLowerCase(),
|
|
@@ -369,7 +397,7 @@ async function ensureDokploySetup(
|
|
|
369
397
|
|
|
370
398
|
// Step 5: Find or create application
|
|
371
399
|
logger.log('\n📦 Looking for application...');
|
|
372
|
-
const appName = dockerConfig.
|
|
400
|
+
const appName = dockerConfig.appName!;
|
|
373
401
|
|
|
374
402
|
let applicationId: string;
|
|
375
403
|
|
|
@@ -502,7 +530,7 @@ async function ensureDokploySetup(
|
|
|
502
530
|
api,
|
|
503
531
|
project.projectId,
|
|
504
532
|
environmentId,
|
|
505
|
-
dockerConfig.
|
|
533
|
+
dockerConfig.appName!,
|
|
506
534
|
services,
|
|
507
535
|
existingUrls,
|
|
508
536
|
);
|
|
@@ -541,7 +569,7 @@ export async function deployCommand(
|
|
|
541
569
|
|
|
542
570
|
// Resolve docker config for image reference
|
|
543
571
|
const dockerConfig = resolveDockerConfig(config);
|
|
544
|
-
const imageName = dockerConfig.imageName
|
|
572
|
+
const imageName = dockerConfig.imageName!;
|
|
545
573
|
const registry = dockerConfig.registry;
|
|
546
574
|
const imageRef = registry
|
|
547
575
|
? `${registry}/${imageName}:${imageTag}`
|
|
@@ -594,12 +622,27 @@ export async function deployCommand(
|
|
|
594
622
|
}
|
|
595
623
|
|
|
596
624
|
let updated = false;
|
|
625
|
+
// URL fields go to secrets.urls, individual params go to secrets.custom
|
|
626
|
+
const urlFields = ['DATABASE_URL', 'REDIS_URL', 'RABBITMQ_URL'] as const;
|
|
627
|
+
|
|
597
628
|
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
629
|
+
if (!value) continue;
|
|
630
|
+
|
|
631
|
+
if (urlFields.includes(key as (typeof urlFields)[number])) {
|
|
632
|
+
// URL fields
|
|
633
|
+
const urlKey = key as keyof typeof secrets.urls;
|
|
634
|
+
if (!secrets.urls[urlKey]) {
|
|
635
|
+
secrets.urls[urlKey] = value;
|
|
636
|
+
logger.log(` Saved ${key} to secrets.urls`);
|
|
637
|
+
updated = true;
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
// Individual parameters (HOST, PORT, NAME, USER, PASSWORD)
|
|
641
|
+
if (!secrets.custom[key]) {
|
|
642
|
+
secrets.custom[key] = value;
|
|
643
|
+
logger.log(` Saved ${key} to secrets.custom`);
|
|
644
|
+
updated = true;
|
|
645
|
+
}
|
|
603
646
|
}
|
|
604
647
|
}
|
|
605
648
|
if (updated) {
|
package/src/deploy/types.ts
CHANGED
|
@@ -31,8 +31,12 @@ export interface DeployResult {
|
|
|
31
31
|
export interface DockerDeployConfig {
|
|
32
32
|
/** Container registry URL */
|
|
33
33
|
registry?: string;
|
|
34
|
-
/** Image name (default: from package.json) */
|
|
34
|
+
/** Image name for Docker (default: from root package.json) */
|
|
35
35
|
imageName?: string;
|
|
36
|
+
/** Project name for Dokploy (default: from root package.json) */
|
|
37
|
+
projectName?: string;
|
|
38
|
+
/** App name within Dokploy project (default: from cwd package.json) */
|
|
39
|
+
appName?: string;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/** Dokploy provider configuration */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundler-BjholBlA.cjs","names":["constructs: Construct[]","DOCKER_SERVICE_ENV_VARS: Record<string, Record<string, string>>","options: BundleOptions","masterKey: string | undefined"],"sources":["../src/build/bundler.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { Construct } from '@geekmidas/constructs';\n\nexport interface BundleOptions {\n\t/** Entry point file (e.g., .gkm/server/server.ts) */\n\tentryPoint: string;\n\t/** Output directory for bundled files */\n\toutputDir: string;\n\t/** Minify the output (default: true) */\n\tminify: boolean;\n\t/** Generate sourcemaps (default: false) */\n\tsourcemap: boolean;\n\t/** Packages to exclude from bundling */\n\texternal: string[];\n\t/** Stage for secrets injection (optional) */\n\tstage?: string;\n\t/** Constructs to validate environment variables for */\n\tconstructs?: Construct[];\n\t/** Docker compose services configured (for auto-populating env vars) */\n\tdockerServices?: {\n\t\tpostgres?: boolean;\n\t\tredis?: boolean;\n\t\trabbitmq?: boolean;\n\t};\n}\n\nexport interface BundleResult {\n\t/** Path to the bundled output */\n\toutputPath: string;\n\t/** Ephemeral master key for deployment (only if stage was provided) */\n\tmasterKey?: string;\n}\n\n/**\n * Collect all required environment variables from constructs.\n * Uses the SnifferEnvironmentParser to detect which env vars each service needs.\n *\n * @param constructs - Array of constructs to analyze\n * @returns Deduplicated array of required environment variable names\n */\nasync function collectRequiredEnvVars(\n\tconstructs: Construct[],\n): Promise<string[]> {\n\tconst allEnvVars = new Set<string>();\n\n\tfor (const construct of constructs) {\n\t\tconst envVars = await construct.getEnvironment();\n\t\tenvVars.forEach((v) => allEnvVars.add(v));\n\t}\n\n\treturn Array.from(allEnvVars).sort();\n}\n\n/**\n * Bundle the server application using tsdown\n *\n * @param options - Bundle configuration options\n * @returns Bundle result with output path and optional master key\n */\n/** Default env var values for docker compose services */\nconst DOCKER_SERVICE_ENV_VARS: Record<string, Record<string, string>> = {\n\tpostgres: {\n\t\tDATABASE_URL: 'postgresql://postgres:postgres@postgres:5432/app',\n\t},\n\tredis: {\n\t\tREDIS_URL: 'redis://redis:6379',\n\t},\n\trabbitmq: {\n\t\tRABBITMQ_URL: 'amqp://rabbitmq:5672',\n\t},\n};\n\nexport async function bundleServer(\n\toptions: BundleOptions,\n): Promise<BundleResult> {\n\tconst {\n\t\tentryPoint,\n\t\toutputDir,\n\t\tminify,\n\t\tsourcemap,\n\t\texternal,\n\t\tstage,\n\t\tconstructs,\n\t\tdockerServices,\n\t} = options;\n\n\t// Ensure output directory exists\n\tawait mkdir(outputDir, { recursive: true });\n\n\t// Build command-line arguments for tsdown\n\tconst args = [\n\t\t'npx',\n\t\t'tsdown',\n\t\tentryPoint,\n\t\t'--no-config', // Don't use any config file from workspace\n\t\t'--out-dir',\n\t\toutputDir,\n\t\t'--format',\n\t\t'esm',\n\t\t'--platform',\n\t\t'node',\n\t\t'--target',\n\t\t'node22',\n\t\t'--clean',\n\t];\n\n\tif (minify) {\n\t\targs.push('--minify');\n\t}\n\n\tif (sourcemap) {\n\t\targs.push('--sourcemap');\n\t}\n\n\t// Add external packages\n\tfor (const ext of external) {\n\t\targs.push('--external', ext);\n\t}\n\n\t// Always exclude node: builtins\n\targs.push('--external', 'node:*');\n\n\t// Handle secrets injection if stage is provided\n\tlet masterKey: string | undefined;\n\n\tif (stage) {\n\t\tconst {\n\t\t\treadStageSecrets,\n\t\t\ttoEmbeddableSecrets,\n\t\t\tvalidateEnvironmentVariables,\n\t\t} = await import('../secrets/storage');\n\t\tconst { encryptSecrets, generateDefineOptions } = await import(\n\t\t\t'../secrets/encryption'\n\t\t);\n\n\t\tconst secrets = await readStageSecrets(stage);\n\n\t\tif (!secrets) {\n\t\t\tthrow new Error(\n\t\t\t\t`No secrets found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t\t);\n\t\t}\n\n\t\t// Auto-populate env vars from docker compose services\n\t\tif (dockerServices) {\n\t\t\tfor (const [service, enabled] of Object.entries(dockerServices)) {\n\t\t\t\tif (enabled && DOCKER_SERVICE_ENV_VARS[service]) {\n\t\t\t\t\tfor (const [envVar, defaultValue] of Object.entries(\n\t\t\t\t\t\tDOCKER_SERVICE_ENV_VARS[service],\n\t\t\t\t\t)) {\n\t\t\t\t\t\t// Check if not already in urls or custom\n\t\t\t\t\t\tconst urlKey = envVar as keyof typeof secrets.urls;\n\t\t\t\t\t\tif (!secrets.urls[urlKey] && !secrets.custom[envVar]) {\n\t\t\t\t\t\t\tsecrets.urls[urlKey] = defaultValue;\n\t\t\t\t\t\t\tconsole.log(` Auto-populated ${envVar} from docker compose`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate environment variables if constructs are provided\n\t\tif (constructs && constructs.length > 0) {\n\t\t\tconsole.log(' Analyzing environment variable requirements...');\n\t\t\tconst requiredVars = await collectRequiredEnvVars(constructs);\n\n\t\t\tif (requiredVars.length > 0) {\n\t\t\t\tconst validation = validateEnvironmentVariables(requiredVars, secrets);\n\n\t\t\t\tif (!validation.valid) {\n\t\t\t\t\tconst errorMessage = [\n\t\t\t\t\t\t`Missing environment variables for stage \"${stage}\":`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t...validation.missing.map((v) => ` ❌ ${v}`),\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'To fix this, either:',\n\t\t\t\t\t\t` 1. Add the missing variables to .gkm/secrets/${stage}.json using:`,\n\t\t\t\t\t\t` gkm secrets:set <KEY> <VALUE> --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t` 2. Or import from a JSON file:`,\n\t\t\t\t\t\t` gkm secrets:import secrets.json --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'Required variables:',\n\t\t\t\t\t\t...validation.required.map((v) =>\n\t\t\t\t\t\t\tvalidation.missing.includes(v) ? ` ❌ ${v}` : ` ✓ ${v}`,\n\t\t\t\t\t\t),\n\t\t\t\t\t].join('\\n');\n\n\t\t\t\t\tthrow new Error(errorMessage);\n\t\t\t\t}\n\n\t\t\t\tconsole.log(\n\t\t\t\t\t` ✓ All ${requiredVars.length} required environment variables found`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Convert to embeddable format and encrypt\n\t\tconst embeddable = toEmbeddableSecrets(secrets);\n\t\tconst encrypted = encryptSecrets(embeddable);\n\t\tmasterKey = encrypted.masterKey;\n\n\t\t// Add define options for build-time injection using tsdown's --env.* format\n\t\tconst defines = generateDefineOptions(encrypted);\n\t\tfor (const [key, value] of Object.entries(defines)) {\n\t\t\targs.push(`--env.${key}`, value);\n\t\t}\n\n\t\tconsole.log(` Secrets encrypted for stage \"${stage}\"`);\n\t}\n\n\tconst mjsOutput = join(outputDir, 'server.mjs');\n\n\ttry {\n\t\t// Run tsdown with command-line arguments\n\t\t// Use spawnSync with args array to avoid shell escaping issues with --define values\n\t\t// args is always populated with ['npx', 'tsdown', ...] so cmd is never undefined\n\t\tconst [cmd, ...cmdArgs] = args as [string, ...string[]];\n\t\tconst result = spawnSync(cmd, cmdArgs, {\n\t\t\tcwd: process.cwd(),\n\t\t\tstdio: 'inherit',\n\t\t\tshell: process.platform === 'win32', // Only use shell on Windows for npx resolution\n\t\t});\n\n\t\tif (result.error) {\n\t\t\tthrow result.error;\n\t\t}\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`tsdown exited with code ${result.status}`);\n\t\t}\n\n\t\t// Rename output to .mjs for explicit ESM\n\t\t// tsdown outputs as server.js for ESM format\n\t\tconst jsOutput = join(outputDir, 'server.js');\n\n\t\tif (existsSync(jsOutput)) {\n\t\t\tawait rename(jsOutput, mjsOutput);\n\t\t}\n\n\t\t// Add shebang to the bundled file\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst content = await readFile(mjsOutput, 'utf-8');\n\t\tif (!content.startsWith('#!')) {\n\t\t\tawait writeFile(mjsOutput, `#!/usr/bin/env node\\n${content}`);\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Failed to bundle server: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t);\n\t}\n\n\treturn {\n\t\toutputPath: mjsOutput,\n\t\tmasterKey,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundler-DWctKN1z.mjs","names":["constructs: Construct[]","DOCKER_SERVICE_ENV_VARS: Record<string, Record<string, string>>","options: BundleOptions","masterKey: string | undefined"],"sources":["../src/build/bundler.ts"],"sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { Construct } from '@geekmidas/constructs';\n\nexport interface BundleOptions {\n\t/** Entry point file (e.g., .gkm/server/server.ts) */\n\tentryPoint: string;\n\t/** Output directory for bundled files */\n\toutputDir: string;\n\t/** Minify the output (default: true) */\n\tminify: boolean;\n\t/** Generate sourcemaps (default: false) */\n\tsourcemap: boolean;\n\t/** Packages to exclude from bundling */\n\texternal: string[];\n\t/** Stage for secrets injection (optional) */\n\tstage?: string;\n\t/** Constructs to validate environment variables for */\n\tconstructs?: Construct[];\n\t/** Docker compose services configured (for auto-populating env vars) */\n\tdockerServices?: {\n\t\tpostgres?: boolean;\n\t\tredis?: boolean;\n\t\trabbitmq?: boolean;\n\t};\n}\n\nexport interface BundleResult {\n\t/** Path to the bundled output */\n\toutputPath: string;\n\t/** Ephemeral master key for deployment (only if stage was provided) */\n\tmasterKey?: string;\n}\n\n/**\n * Collect all required environment variables from constructs.\n * Uses the SnifferEnvironmentParser to detect which env vars each service needs.\n *\n * @param constructs - Array of constructs to analyze\n * @returns Deduplicated array of required environment variable names\n */\nasync function collectRequiredEnvVars(\n\tconstructs: Construct[],\n): Promise<string[]> {\n\tconst allEnvVars = new Set<string>();\n\n\tfor (const construct of constructs) {\n\t\tconst envVars = await construct.getEnvironment();\n\t\tenvVars.forEach((v) => allEnvVars.add(v));\n\t}\n\n\treturn Array.from(allEnvVars).sort();\n}\n\n/**\n * Bundle the server application using tsdown\n *\n * @param options - Bundle configuration options\n * @returns Bundle result with output path and optional master key\n */\n/** Default env var values for docker compose services */\nconst DOCKER_SERVICE_ENV_VARS: Record<string, Record<string, string>> = {\n\tpostgres: {\n\t\tDATABASE_URL: 'postgresql://postgres:postgres@postgres:5432/app',\n\t},\n\tredis: {\n\t\tREDIS_URL: 'redis://redis:6379',\n\t},\n\trabbitmq: {\n\t\tRABBITMQ_URL: 'amqp://rabbitmq:5672',\n\t},\n};\n\nexport async function bundleServer(\n\toptions: BundleOptions,\n): Promise<BundleResult> {\n\tconst {\n\t\tentryPoint,\n\t\toutputDir,\n\t\tminify,\n\t\tsourcemap,\n\t\texternal,\n\t\tstage,\n\t\tconstructs,\n\t\tdockerServices,\n\t} = options;\n\n\t// Ensure output directory exists\n\tawait mkdir(outputDir, { recursive: true });\n\n\t// Build command-line arguments for tsdown\n\tconst args = [\n\t\t'npx',\n\t\t'tsdown',\n\t\tentryPoint,\n\t\t'--no-config', // Don't use any config file from workspace\n\t\t'--out-dir',\n\t\toutputDir,\n\t\t'--format',\n\t\t'esm',\n\t\t'--platform',\n\t\t'node',\n\t\t'--target',\n\t\t'node22',\n\t\t'--clean',\n\t];\n\n\tif (minify) {\n\t\targs.push('--minify');\n\t}\n\n\tif (sourcemap) {\n\t\targs.push('--sourcemap');\n\t}\n\n\t// Add external packages\n\tfor (const ext of external) {\n\t\targs.push('--external', ext);\n\t}\n\n\t// Always exclude node: builtins\n\targs.push('--external', 'node:*');\n\n\t// Handle secrets injection if stage is provided\n\tlet masterKey: string | undefined;\n\n\tif (stage) {\n\t\tconst {\n\t\t\treadStageSecrets,\n\t\t\ttoEmbeddableSecrets,\n\t\t\tvalidateEnvironmentVariables,\n\t\t} = await import('../secrets/storage');\n\t\tconst { encryptSecrets, generateDefineOptions } = await import(\n\t\t\t'../secrets/encryption'\n\t\t);\n\n\t\tconst secrets = await readStageSecrets(stage);\n\n\t\tif (!secrets) {\n\t\t\tthrow new Error(\n\t\t\t\t`No secrets found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t\t);\n\t\t}\n\n\t\t// Auto-populate env vars from docker compose services\n\t\tif (dockerServices) {\n\t\t\tfor (const [service, enabled] of Object.entries(dockerServices)) {\n\t\t\t\tif (enabled && DOCKER_SERVICE_ENV_VARS[service]) {\n\t\t\t\t\tfor (const [envVar, defaultValue] of Object.entries(\n\t\t\t\t\t\tDOCKER_SERVICE_ENV_VARS[service],\n\t\t\t\t\t)) {\n\t\t\t\t\t\t// Check if not already in urls or custom\n\t\t\t\t\t\tconst urlKey = envVar as keyof typeof secrets.urls;\n\t\t\t\t\t\tif (!secrets.urls[urlKey] && !secrets.custom[envVar]) {\n\t\t\t\t\t\t\tsecrets.urls[urlKey] = defaultValue;\n\t\t\t\t\t\t\tconsole.log(` Auto-populated ${envVar} from docker compose`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate environment variables if constructs are provided\n\t\tif (constructs && constructs.length > 0) {\n\t\t\tconsole.log(' Analyzing environment variable requirements...');\n\t\t\tconst requiredVars = await collectRequiredEnvVars(constructs);\n\n\t\t\tif (requiredVars.length > 0) {\n\t\t\t\tconst validation = validateEnvironmentVariables(requiredVars, secrets);\n\n\t\t\t\tif (!validation.valid) {\n\t\t\t\t\tconst errorMessage = [\n\t\t\t\t\t\t`Missing environment variables for stage \"${stage}\":`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t...validation.missing.map((v) => ` ❌ ${v}`),\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'To fix this, either:',\n\t\t\t\t\t\t` 1. Add the missing variables to .gkm/secrets/${stage}.json using:`,\n\t\t\t\t\t\t` gkm secrets:set <KEY> <VALUE> --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t` 2. Or import from a JSON file:`,\n\t\t\t\t\t\t` gkm secrets:import secrets.json --stage ${stage}`,\n\t\t\t\t\t\t'',\n\t\t\t\t\t\t'Required variables:',\n\t\t\t\t\t\t...validation.required.map((v) =>\n\t\t\t\t\t\t\tvalidation.missing.includes(v) ? ` ❌ ${v}` : ` ✓ ${v}`,\n\t\t\t\t\t\t),\n\t\t\t\t\t].join('\\n');\n\n\t\t\t\t\tthrow new Error(errorMessage);\n\t\t\t\t}\n\n\t\t\t\tconsole.log(\n\t\t\t\t\t` ✓ All ${requiredVars.length} required environment variables found`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Convert to embeddable format and encrypt\n\t\tconst embeddable = toEmbeddableSecrets(secrets);\n\t\tconst encrypted = encryptSecrets(embeddable);\n\t\tmasterKey = encrypted.masterKey;\n\n\t\t// Add define options for build-time injection using tsdown's --env.* format\n\t\tconst defines = generateDefineOptions(encrypted);\n\t\tfor (const [key, value] of Object.entries(defines)) {\n\t\t\targs.push(`--env.${key}`, value);\n\t\t}\n\n\t\tconsole.log(` Secrets encrypted for stage \"${stage}\"`);\n\t}\n\n\tconst mjsOutput = join(outputDir, 'server.mjs');\n\n\ttry {\n\t\t// Run tsdown with command-line arguments\n\t\t// Use spawnSync with args array to avoid shell escaping issues with --define values\n\t\t// args is always populated with ['npx', 'tsdown', ...] so cmd is never undefined\n\t\tconst [cmd, ...cmdArgs] = args as [string, ...string[]];\n\t\tconst result = spawnSync(cmd, cmdArgs, {\n\t\t\tcwd: process.cwd(),\n\t\t\tstdio: 'inherit',\n\t\t\tshell: process.platform === 'win32', // Only use shell on Windows for npx resolution\n\t\t});\n\n\t\tif (result.error) {\n\t\t\tthrow result.error;\n\t\t}\n\t\tif (result.status !== 0) {\n\t\t\tthrow new Error(`tsdown exited with code ${result.status}`);\n\t\t}\n\n\t\t// Rename output to .mjs for explicit ESM\n\t\t// tsdown outputs as server.js for ESM format\n\t\tconst jsOutput = join(outputDir, 'server.js');\n\n\t\tif (existsSync(jsOutput)) {\n\t\t\tawait rename(jsOutput, mjsOutput);\n\t\t}\n\n\t\t// Add shebang to the bundled file\n\t\tconst { readFile } = await import('node:fs/promises');\n\t\tconst content = await readFile(mjsOutput, 'utf-8');\n\t\tif (!content.startsWith('#!')) {\n\t\t\tawait writeFile(mjsOutput, `#!/usr/bin/env node\\n${content}`);\n\t\t}\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Failed to bundle server: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t);\n\t}\n\n\treturn {\n\t\toutputPath: mjsOutput,\n\t\tmasterKey,\n\t};\n}\n"],"mappings":"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"}
|