@geekmidas/cli 0.13.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-DskIqW2t.mjs → bundler-D7cM_FWw.mjs} +34 -10
- package/dist/bundler-D7cM_FWw.mjs.map +1 -0
- package/dist/{bundler-B1qy9b-j.cjs → bundler-Nuew7Xcn.cjs} +33 -9
- package/dist/bundler-Nuew7Xcn.cjs.map +1 -0
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +3 -0
- package/dist/dokploy-api-C7F9VykY.cjs +317 -0
- package/dist/dokploy-api-C7F9VykY.cjs.map +1 -0
- package/dist/dokploy-api-CaETb2L6.mjs +305 -0
- package/dist/dokploy-api-CaETb2L6.mjs.map +1 -0
- package/dist/dokploy-api-DHvfmWbi.mjs +3 -0
- package/dist/{encryption-Dyf_r1h-.cjs → encryption-D7Efcdi9.cjs} +1 -1
- package/dist/{encryption-Dyf_r1h-.cjs.map → encryption-D7Efcdi9.cjs.map} +1 -1
- package/dist/{encryption-C8H-38Yy.mjs → encryption-h4Nb6W-M.mjs} +1 -1
- package/dist/{encryption-C8H-38Yy.mjs.map → encryption-h4Nb6W-M.mjs.map} +1 -1
- package/dist/index.cjs +1508 -1073
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1508 -1073
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Bt_1FDpT.cjs → openapi-C89hhkZC.cjs} +3 -3
- package/dist/{openapi-Bt_1FDpT.cjs.map → openapi-C89hhkZC.cjs.map} +1 -1
- package/dist/{openapi-BfFlOBCG.mjs → openapi-CZVcfxk-.mjs} +3 -3
- package/dist/{openapi-BfFlOBCG.mjs.map → openapi-CZVcfxk-.mjs.map} +1 -1
- package/dist/{openapi-react-query-B6XTeGqS.mjs → openapi-react-query-CM2_qlW9.mjs} +1 -1
- package/dist/{openapi-react-query-B6XTeGqS.mjs.map → openapi-react-query-CM2_qlW9.mjs.map} +1 -1
- package/dist/{openapi-react-query-B-sNWHFU.cjs → openapi-react-query-iKjfLzff.cjs} +1 -1
- package/dist/{openapi-react-query-B-sNWHFU.cjs.map → openapi-react-query-iKjfLzff.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{storage-kSxTjkNb.mjs → storage-BaOP55oq.mjs} +16 -2
- package/dist/storage-BaOP55oq.mjs.map +1 -0
- package/dist/{storage-Bj1E26lU.cjs → storage-Bn3K9Ccu.cjs} +21 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +1 -0
- package/dist/storage-UfyTn7Zm.cjs +7 -0
- package/dist/storage-nkGIjeXt.mjs +3 -0
- package/dist/{types-BhkZc-vm.d.cts → types-BgaMXsUa.d.cts} +3 -1
- package/dist/{types-BR0M2v_c.d.mts.map → types-BgaMXsUa.d.cts.map} +1 -1
- package/dist/{types-BR0M2v_c.d.mts → types-iFk5ms7y.d.mts} +3 -1
- package/dist/{types-BhkZc-vm.d.cts.map → types-iFk5ms7y.d.mts.map} +1 -1
- package/package.json +4 -4
- package/src/auth/__tests__/credentials.spec.ts +127 -0
- package/src/auth/__tests__/index.spec.ts +69 -0
- package/src/auth/credentials.ts +33 -0
- package/src/auth/index.ts +57 -50
- package/src/build/__tests__/bundler.spec.ts +5 -4
- package/src/build/__tests__/endpoint-analyzer.spec.ts +623 -0
- package/src/build/__tests__/handler-templates.spec.ts +272 -0
- package/src/build/bundler.ts +61 -8
- package/src/build/index.ts +21 -0
- package/src/build/types.ts +6 -0
- package/src/deploy/__tests__/docker.spec.ts +44 -6
- package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
- package/src/deploy/__tests__/dokploy.spec.ts +196 -6
- package/src/deploy/__tests__/index.spec.ts +401 -0
- package/src/deploy/__tests__/init.spec.ts +147 -16
- package/src/deploy/docker.ts +109 -5
- package/src/deploy/dokploy-api.ts +581 -0
- package/src/deploy/dokploy.ts +66 -93
- package/src/deploy/index.ts +630 -32
- package/src/deploy/init.ts +192 -249
- package/src/deploy/types.ts +24 -2
- package/src/dev/__tests__/index.spec.ts +95 -0
- package/src/docker/__tests__/templates.spec.ts +144 -0
- package/src/docker/index.ts +96 -6
- package/src/docker/templates.ts +114 -27
- package/src/generators/EndpointGenerator.ts +2 -2
- package/src/index.ts +34 -13
- package/src/secrets/storage.ts +15 -0
- package/src/types.ts +2 -0
- package/dist/bundler-B1qy9b-j.cjs.map +0 -1
- package/dist/bundler-DskIqW2t.mjs.map +0 -1
- package/dist/storage-BOOpAF8N.cjs +0 -5
- package/dist/storage-Bj1E26lU.cjs.map +0 -1
- package/dist/storage-kSxTjkNb.mjs.map +0 -1
- package/dist/storage-tgZSUnKl.mjs +0 -3
package/src/deploy/index.ts
CHANGED
|
@@ -1,15 +1,550 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import * as readline from 'node:readline/promises';
|
|
3
|
+
import {
|
|
4
|
+
getDokployCredentials,
|
|
5
|
+
getDokployRegistryId,
|
|
6
|
+
storeDokployCredentials,
|
|
7
|
+
validateDokployToken,
|
|
8
|
+
} from '../auth';
|
|
9
|
+
import { storeDokployRegistryId } from '../auth/credentials';
|
|
1
10
|
import { buildCommand } from '../build/index';
|
|
2
|
-
import { loadConfig } from '../config';
|
|
11
|
+
import { type GkmConfig, loadConfig } from '../config';
|
|
3
12
|
import { deployDocker, resolveDockerConfig } from './docker';
|
|
4
|
-
import { deployDokploy
|
|
5
|
-
import
|
|
13
|
+
import { deployDokploy } from './dokploy';
|
|
14
|
+
import { DokployApi } from './dokploy-api';
|
|
15
|
+
import { updateConfig } from './init';
|
|
16
|
+
import type {
|
|
17
|
+
DeployOptions,
|
|
18
|
+
DeployProvider,
|
|
19
|
+
DeployResult,
|
|
20
|
+
DockerDeployConfig,
|
|
21
|
+
DokployDeployConfig,
|
|
22
|
+
} from './types';
|
|
6
23
|
|
|
7
24
|
const logger = console;
|
|
8
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Prompt for input
|
|
28
|
+
*/
|
|
29
|
+
async function prompt(message: string, hidden = false): Promise<string> {
|
|
30
|
+
if (!process.stdin.isTTY) {
|
|
31
|
+
throw new Error('Interactive input required. Please configure manually.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (hidden) {
|
|
35
|
+
process.stdout.write(message);
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let value = '';
|
|
38
|
+
const onData = (char: Buffer) => {
|
|
39
|
+
const c = char.toString();
|
|
40
|
+
if (c === '\n' || c === '\r') {
|
|
41
|
+
process.stdin.setRawMode(false);
|
|
42
|
+
process.stdin.pause();
|
|
43
|
+
process.stdin.removeListener('data', onData);
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
resolve(value);
|
|
46
|
+
} else if (c === '\u0003') {
|
|
47
|
+
process.stdin.setRawMode(false);
|
|
48
|
+
process.stdin.pause();
|
|
49
|
+
process.stdout.write('\n');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
} else if (c === '\u007F' || c === '\b') {
|
|
52
|
+
if (value.length > 0) value = value.slice(0, -1);
|
|
53
|
+
} else {
|
|
54
|
+
value += c;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
process.stdin.setRawMode(true);
|
|
58
|
+
process.stdin.resume();
|
|
59
|
+
process.stdin.on('data', onData);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rl = readline.createInterface({ input, output });
|
|
64
|
+
try {
|
|
65
|
+
return await rl.question(message);
|
|
66
|
+
} finally {
|
|
67
|
+
rl.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Docker compose services that can be provisioned
|
|
73
|
+
*/
|
|
74
|
+
interface DockerComposeServices {
|
|
75
|
+
postgres?: boolean;
|
|
76
|
+
redis?: boolean;
|
|
77
|
+
rabbitmq?: boolean;
|
|
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
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Result of Dokploy setup including provisioned service URLs
|
|
98
|
+
*/
|
|
99
|
+
interface DokploySetupResult {
|
|
100
|
+
config: DokployDeployConfig;
|
|
101
|
+
serviceUrls?: ServiceUrls;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Provision docker compose services in Dokploy
|
|
106
|
+
* @internal Exported for testing
|
|
107
|
+
*/
|
|
108
|
+
export async function provisionServices(
|
|
109
|
+
api: DokployApi,
|
|
110
|
+
projectId: string,
|
|
111
|
+
environmentId: string | undefined,
|
|
112
|
+
appName: string,
|
|
113
|
+
services?: DockerComposeServices,
|
|
114
|
+
existingUrls?: Pick<ServiceUrls, 'DATABASE_URL' | 'REDIS_URL'>,
|
|
115
|
+
): Promise<ServiceUrls | undefined> {
|
|
116
|
+
logger.log(
|
|
117
|
+
`\n🔍 provisionServices called: services=${JSON.stringify(services)}, envId=${environmentId}`,
|
|
118
|
+
);
|
|
119
|
+
if (!services || !environmentId) {
|
|
120
|
+
logger.log(' Skipping: no services or no environmentId');
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const serviceUrls: ServiceUrls = {};
|
|
125
|
+
|
|
126
|
+
if (services.postgres) {
|
|
127
|
+
// Skip if DATABASE_URL already exists in secrets
|
|
128
|
+
if (existingUrls?.DATABASE_URL) {
|
|
129
|
+
logger.log('\n🐘 PostgreSQL: Already configured (skipping)');
|
|
130
|
+
} else {
|
|
131
|
+
logger.log('\n🐘 Provisioning PostgreSQL...');
|
|
132
|
+
const postgresName = `${appName}-db`;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Generate a random password for the database
|
|
136
|
+
const { randomBytes } = await import('node:crypto');
|
|
137
|
+
const databasePassword = randomBytes(16).toString('hex');
|
|
138
|
+
|
|
139
|
+
const postgres = await api.createPostgres(
|
|
140
|
+
postgresName,
|
|
141
|
+
projectId,
|
|
142
|
+
environmentId,
|
|
143
|
+
{ databasePassword },
|
|
144
|
+
);
|
|
145
|
+
logger.log(` ✓ Created PostgreSQL: ${postgres.postgresId}`);
|
|
146
|
+
|
|
147
|
+
// Deploy the database
|
|
148
|
+
await api.deployPostgres(postgres.postgresId);
|
|
149
|
+
logger.log(' ✓ PostgreSQL deployed');
|
|
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
|
+
|
|
158
|
+
// Construct connection URL using internal docker network hostname
|
|
159
|
+
serviceUrls.DATABASE_URL = `postgresql://${postgres.databaseUser}:${postgres.databasePassword}@${postgres.appName}:5432/${postgres.databaseName}`;
|
|
160
|
+
logger.log(` ✓ Database credentials configured`);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const message =
|
|
163
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
164
|
+
if (
|
|
165
|
+
message.includes('already exists') ||
|
|
166
|
+
message.includes('duplicate')
|
|
167
|
+
) {
|
|
168
|
+
logger.log(` ℹ PostgreSQL already exists`);
|
|
169
|
+
} else {
|
|
170
|
+
logger.log(` ⚠ Failed to provision PostgreSQL: ${message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (services.redis) {
|
|
177
|
+
// Skip if REDIS_URL already exists in secrets
|
|
178
|
+
if (existingUrls?.REDIS_URL) {
|
|
179
|
+
logger.log('\n🔴 Redis: Already configured (skipping)');
|
|
180
|
+
} else {
|
|
181
|
+
logger.log('\n🔴 Provisioning Redis...');
|
|
182
|
+
const redisName = `${appName}-cache`;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Generate a random password for Redis
|
|
186
|
+
const { randomBytes } = await import('node:crypto');
|
|
187
|
+
const databasePassword = randomBytes(16).toString('hex');
|
|
188
|
+
|
|
189
|
+
const redis = await api.createRedis(
|
|
190
|
+
redisName,
|
|
191
|
+
projectId,
|
|
192
|
+
environmentId,
|
|
193
|
+
{
|
|
194
|
+
databasePassword,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
logger.log(` ✓ Created Redis: ${redis.redisId}`);
|
|
198
|
+
|
|
199
|
+
// Deploy the redis instance
|
|
200
|
+
await api.deployRedis(redis.redisId);
|
|
201
|
+
logger.log(' ✓ Redis deployed');
|
|
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
|
+
|
|
210
|
+
// Construct connection URL
|
|
211
|
+
const password = redis.databasePassword
|
|
212
|
+
? `:${redis.databasePassword}@`
|
|
213
|
+
: '';
|
|
214
|
+
serviceUrls.REDIS_URL = `redis://${password}${redis.appName}:6379`;
|
|
215
|
+
logger.log(` ✓ Redis credentials configured`);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message =
|
|
218
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
219
|
+
if (
|
|
220
|
+
message.includes('already exists') ||
|
|
221
|
+
message.includes('duplicate')
|
|
222
|
+
) {
|
|
223
|
+
logger.log(` ℹ Redis already exists`);
|
|
224
|
+
} else {
|
|
225
|
+
logger.log(` ⚠ Failed to provision Redis: ${message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Object.keys(serviceUrls).length > 0 ? serviceUrls : undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Ensure Dokploy is fully configured, recovering/creating resources as needed
|
|
236
|
+
*/
|
|
237
|
+
async function ensureDokploySetup(
|
|
238
|
+
config: GkmConfig,
|
|
239
|
+
dockerConfig: DockerDeployConfig,
|
|
240
|
+
stage: string,
|
|
241
|
+
services?: DockerComposeServices,
|
|
242
|
+
): Promise<DokploySetupResult> {
|
|
243
|
+
logger.log('\n🔧 Checking Dokploy setup...');
|
|
244
|
+
|
|
245
|
+
// Read existing secrets to check if services are already configured
|
|
246
|
+
const { readStageSecrets } = await import('../secrets/storage');
|
|
247
|
+
const existingSecrets = await readStageSecrets(stage);
|
|
248
|
+
const existingUrls: { DATABASE_URL?: string; REDIS_URL?: string } = {
|
|
249
|
+
DATABASE_URL: existingSecrets?.urls?.DATABASE_URL,
|
|
250
|
+
REDIS_URL: existingSecrets?.urls?.REDIS_URL,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Step 1: Ensure we have Dokploy credentials
|
|
254
|
+
let creds = await getDokployCredentials();
|
|
255
|
+
|
|
256
|
+
if (!creds) {
|
|
257
|
+
logger.log("\n📋 Dokploy credentials not found. Let's set them up.");
|
|
258
|
+
const endpoint = await prompt(
|
|
259
|
+
'Dokploy URL (e.g., https://dokploy.example.com): ',
|
|
260
|
+
);
|
|
261
|
+
const normalizedEndpoint = endpoint.replace(/\/$/, '');
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
new URL(normalizedEndpoint);
|
|
265
|
+
} catch {
|
|
266
|
+
throw new Error('Invalid URL format');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
logger.log(
|
|
270
|
+
`\nGenerate a token at: ${normalizedEndpoint}/settings/profile\n`,
|
|
271
|
+
);
|
|
272
|
+
const token = await prompt('API Token: ', true);
|
|
273
|
+
|
|
274
|
+
logger.log('\nValidating credentials...');
|
|
275
|
+
const isValid = await validateDokployToken(normalizedEndpoint, token);
|
|
276
|
+
if (!isValid) {
|
|
277
|
+
throw new Error('Invalid credentials. Please check your token.');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await storeDokployCredentials(token, normalizedEndpoint);
|
|
281
|
+
creds = { token, endpoint: normalizedEndpoint };
|
|
282
|
+
logger.log('✓ Credentials saved');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const api = new DokployApi({ baseUrl: creds.endpoint, token: creds.token });
|
|
286
|
+
|
|
287
|
+
// Step 2: Check if we have config in gkm.config.ts
|
|
288
|
+
const existingConfig = config.providers?.dokploy;
|
|
289
|
+
if (
|
|
290
|
+
existingConfig &&
|
|
291
|
+
typeof existingConfig !== 'boolean' &&
|
|
292
|
+
existingConfig.applicationId &&
|
|
293
|
+
existingConfig.projectId
|
|
294
|
+
) {
|
|
295
|
+
logger.log('✓ Dokploy config found in gkm.config.ts');
|
|
296
|
+
|
|
297
|
+
// Verify the application still exists
|
|
298
|
+
try {
|
|
299
|
+
const projectDetails = await api.getProject(existingConfig.projectId);
|
|
300
|
+
logger.log('✓ Project verified');
|
|
301
|
+
|
|
302
|
+
// Get registry ID from config first, then from local storage
|
|
303
|
+
const storedRegistryId =
|
|
304
|
+
existingConfig.registryId ?? (await getDokployRegistryId());
|
|
305
|
+
|
|
306
|
+
// Get environment ID for service provisioning (match by stage name)
|
|
307
|
+
const environments = projectDetails.environments ?? [];
|
|
308
|
+
let environment = environments.find(
|
|
309
|
+
(e) => e.name.toLowerCase() === stage.toLowerCase(),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Create environment if it doesn't exist for this stage
|
|
313
|
+
if (!environment) {
|
|
314
|
+
logger.log(` Creating "${stage}" environment...`);
|
|
315
|
+
environment = await api.createEnvironment(
|
|
316
|
+
existingConfig.projectId,
|
|
317
|
+
stage,
|
|
318
|
+
);
|
|
319
|
+
logger.log(` ✓ Created environment: ${environment.environmentId}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const environmentId = environment.environmentId;
|
|
323
|
+
|
|
324
|
+
// Provision services if configured
|
|
325
|
+
logger.log(
|
|
326
|
+
` Services config: ${JSON.stringify(services)}, envId: ${environmentId}`,
|
|
327
|
+
);
|
|
328
|
+
const serviceUrls = await provisionServices(
|
|
329
|
+
api,
|
|
330
|
+
existingConfig.projectId,
|
|
331
|
+
environmentId,
|
|
332
|
+
dockerConfig.appName!,
|
|
333
|
+
services,
|
|
334
|
+
existingUrls,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
config: {
|
|
339
|
+
endpoint: existingConfig.endpoint,
|
|
340
|
+
projectId: existingConfig.projectId,
|
|
341
|
+
applicationId: existingConfig.applicationId,
|
|
342
|
+
registry: existingConfig.registry,
|
|
343
|
+
registryId: storedRegistryId ?? undefined,
|
|
344
|
+
},
|
|
345
|
+
serviceUrls,
|
|
346
|
+
};
|
|
347
|
+
} catch {
|
|
348
|
+
logger.log('⚠ Project not found, will recover...');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Step 3: Find or create project
|
|
353
|
+
logger.log('\n📁 Looking for project...');
|
|
354
|
+
const projectName = dockerConfig.projectName!;
|
|
355
|
+
const projects = await api.listProjects();
|
|
356
|
+
let project = projects.find(
|
|
357
|
+
(p) => p.name.toLowerCase() === projectName.toLowerCase(),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
let environmentId: string;
|
|
361
|
+
|
|
362
|
+
if (project) {
|
|
363
|
+
logger.log(
|
|
364
|
+
` Found existing project: ${project.name} (${project.projectId})`,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Step 4: Get or create environment for existing project (match by stage)
|
|
368
|
+
const projectDetails = await api.getProject(project.projectId);
|
|
369
|
+
const environments = projectDetails.environments ?? [];
|
|
370
|
+
const matchingEnv = environments.find(
|
|
371
|
+
(e) => e.name.toLowerCase() === stage.toLowerCase(),
|
|
372
|
+
);
|
|
373
|
+
if (matchingEnv) {
|
|
374
|
+
environmentId = matchingEnv.environmentId;
|
|
375
|
+
logger.log(` Using environment: ${matchingEnv.name}`);
|
|
376
|
+
} else {
|
|
377
|
+
logger.log(` Creating "${stage}" environment...`);
|
|
378
|
+
const env = await api.createEnvironment(project.projectId, stage);
|
|
379
|
+
environmentId = env.environmentId;
|
|
380
|
+
logger.log(` ✓ Created environment: ${stage}`);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
logger.log(` Creating project: ${projectName}`);
|
|
384
|
+
const result = await api.createProject(projectName);
|
|
385
|
+
project = result.project;
|
|
386
|
+
// Rename the default environment to match stage if different
|
|
387
|
+
if (result.environment.name.toLowerCase() !== stage.toLowerCase()) {
|
|
388
|
+
logger.log(` Creating "${stage}" environment...`);
|
|
389
|
+
const env = await api.createEnvironment(project.projectId, stage);
|
|
390
|
+
environmentId = env.environmentId;
|
|
391
|
+
} else {
|
|
392
|
+
environmentId = result.environment.environmentId;
|
|
393
|
+
}
|
|
394
|
+
logger.log(` ✓ Created project: ${project.projectId}`);
|
|
395
|
+
logger.log(` ✓ Using environment: ${stage}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Step 5: Find or create application
|
|
399
|
+
logger.log('\n📦 Looking for application...');
|
|
400
|
+
const appName = dockerConfig.appName!;
|
|
401
|
+
|
|
402
|
+
let applicationId: string;
|
|
403
|
+
|
|
404
|
+
// Try to find existing app from config
|
|
405
|
+
if (
|
|
406
|
+
existingConfig &&
|
|
407
|
+
typeof existingConfig !== 'boolean' &&
|
|
408
|
+
existingConfig.applicationId
|
|
409
|
+
) {
|
|
410
|
+
applicationId = existingConfig.applicationId;
|
|
411
|
+
logger.log(` Using application from config: ${applicationId}`);
|
|
412
|
+
} else {
|
|
413
|
+
// Create new application
|
|
414
|
+
logger.log(` Creating application: ${appName}`);
|
|
415
|
+
const app = await api.createApplication(
|
|
416
|
+
appName,
|
|
417
|
+
project.projectId,
|
|
418
|
+
environmentId,
|
|
419
|
+
);
|
|
420
|
+
applicationId = app.applicationId;
|
|
421
|
+
logger.log(` ✓ Created application: ${applicationId}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Step 6: Ensure registry is set up
|
|
425
|
+
logger.log('\n🐳 Checking registry...');
|
|
426
|
+
let registryId = await getDokployRegistryId();
|
|
427
|
+
|
|
428
|
+
if (registryId) {
|
|
429
|
+
// Verify stored registry still exists
|
|
430
|
+
try {
|
|
431
|
+
const registry = await api.getRegistry(registryId);
|
|
432
|
+
logger.log(` Using registry: ${registry.registryName}`);
|
|
433
|
+
} catch {
|
|
434
|
+
logger.log(' ⚠ Stored registry not found, clearing...');
|
|
435
|
+
registryId = undefined;
|
|
436
|
+
await storeDokployRegistryId('');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!registryId) {
|
|
441
|
+
const registries = await api.listRegistries();
|
|
442
|
+
|
|
443
|
+
if (registries.length === 0) {
|
|
444
|
+
// No registries exist
|
|
445
|
+
if (dockerConfig.registry) {
|
|
446
|
+
logger.log(" No registries found in Dokploy. Let's create one.");
|
|
447
|
+
logger.log(` Registry URL: ${dockerConfig.registry}`);
|
|
448
|
+
|
|
449
|
+
const username = await prompt('Registry username: ');
|
|
450
|
+
const password = await prompt('Registry password/token: ', true);
|
|
451
|
+
|
|
452
|
+
const registry = await api.createRegistry(
|
|
453
|
+
'Default Registry',
|
|
454
|
+
dockerConfig.registry,
|
|
455
|
+
username,
|
|
456
|
+
password,
|
|
457
|
+
);
|
|
458
|
+
registryId = registry.registryId;
|
|
459
|
+
await storeDokployRegistryId(registryId);
|
|
460
|
+
logger.log(` ✓ Registry created: ${registryId}`);
|
|
461
|
+
} else {
|
|
462
|
+
logger.log(
|
|
463
|
+
' ⚠ No registry configured. Set docker.registry in gkm.config.ts',
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// Show available registries and let user select or create new
|
|
468
|
+
logger.log(' Available registries:');
|
|
469
|
+
registries.forEach((reg, i) => {
|
|
470
|
+
logger.log(` ${i + 1}. ${reg.registryName} (${reg.registryUrl})`);
|
|
471
|
+
});
|
|
472
|
+
if (dockerConfig.registry) {
|
|
473
|
+
logger.log(` ${registries.length + 1}. Create new registry`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const maxOption = dockerConfig.registry
|
|
477
|
+
? registries.length + 1
|
|
478
|
+
: registries.length;
|
|
479
|
+
const selection = await prompt(` Select registry (1-${maxOption}): `);
|
|
480
|
+
const index = parseInt(selection, 10) - 1;
|
|
481
|
+
|
|
482
|
+
if (index >= 0 && index < registries.length) {
|
|
483
|
+
// Selected existing registry
|
|
484
|
+
registryId = registries[index]!.registryId;
|
|
485
|
+
await storeDokployRegistryId(registryId);
|
|
486
|
+
logger.log(` ✓ Selected: ${registries[index]!.registryName}`);
|
|
487
|
+
} else if (dockerConfig.registry && index === registries.length) {
|
|
488
|
+
// Create new registry
|
|
489
|
+
logger.log(`\n Creating new registry...`);
|
|
490
|
+
logger.log(` Registry URL: ${dockerConfig.registry}`);
|
|
491
|
+
|
|
492
|
+
const username = await prompt(' Registry username: ');
|
|
493
|
+
const password = await prompt(' Registry password/token: ', true);
|
|
494
|
+
|
|
495
|
+
const registry = await api.createRegistry(
|
|
496
|
+
dockerConfig.registry.replace(/^https?:\/\//, ''),
|
|
497
|
+
dockerConfig.registry,
|
|
498
|
+
username,
|
|
499
|
+
password,
|
|
500
|
+
);
|
|
501
|
+
registryId = registry.registryId;
|
|
502
|
+
await storeDokployRegistryId(registryId);
|
|
503
|
+
logger.log(` ✓ Registry created: ${registryId}`);
|
|
504
|
+
} else {
|
|
505
|
+
logger.log(' ⚠ Invalid selection, skipping registry setup');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Step 7: Build and save config
|
|
511
|
+
const dokployConfig: DokployDeployConfig = {
|
|
512
|
+
endpoint: creds.endpoint,
|
|
513
|
+
projectId: project.projectId,
|
|
514
|
+
applicationId,
|
|
515
|
+
registryId: registryId ?? undefined,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// Update gkm.config.ts
|
|
519
|
+
await updateConfig(dokployConfig);
|
|
520
|
+
|
|
521
|
+
logger.log('\n✅ Dokploy setup complete!');
|
|
522
|
+
logger.log(` Project: ${project.projectId}`);
|
|
523
|
+
logger.log(` Application: ${applicationId}`);
|
|
524
|
+
if (registryId) {
|
|
525
|
+
logger.log(` Registry: ${registryId}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Step 8: Provision docker compose services if configured
|
|
529
|
+
const serviceUrls = await provisionServices(
|
|
530
|
+
api,
|
|
531
|
+
project.projectId,
|
|
532
|
+
environmentId,
|
|
533
|
+
dockerConfig.appName!,
|
|
534
|
+
services,
|
|
535
|
+
existingUrls,
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
config: dokployConfig,
|
|
540
|
+
serviceUrls,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
9
544
|
/**
|
|
10
545
|
* Generate image tag from stage and timestamp
|
|
11
546
|
*/
|
|
12
|
-
function generateTag(stage: string): string {
|
|
547
|
+
export function generateTag(stage: string): string {
|
|
13
548
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
14
549
|
return `${stage}-${timestamp}`;
|
|
15
550
|
}
|
|
@@ -32,6 +567,90 @@ export async function deployCommand(
|
|
|
32
567
|
const imageTag = tag ?? generateTag(stage);
|
|
33
568
|
logger.log(` Tag: ${imageTag}`);
|
|
34
569
|
|
|
570
|
+
// Resolve docker config for image reference
|
|
571
|
+
const dockerConfig = resolveDockerConfig(config);
|
|
572
|
+
const imageName = dockerConfig.imageName!;
|
|
573
|
+
const registry = dockerConfig.registry;
|
|
574
|
+
const imageRef = registry
|
|
575
|
+
? `${registry}/${imageName}:${imageTag}`
|
|
576
|
+
: `${imageName}:${imageTag}`;
|
|
577
|
+
|
|
578
|
+
// For Dokploy, set up services BEFORE build so URLs are available
|
|
579
|
+
let dokployConfig: DokployDeployConfig | undefined;
|
|
580
|
+
let finalRegistry = registry;
|
|
581
|
+
|
|
582
|
+
if (provider === 'dokploy') {
|
|
583
|
+
// Extract docker compose services config
|
|
584
|
+
const composeServices = config.docker?.compose?.services;
|
|
585
|
+
logger.log(
|
|
586
|
+
`\n🔍 Docker compose config: ${JSON.stringify(config.docker?.compose)}`,
|
|
587
|
+
);
|
|
588
|
+
const dockerServices: DockerComposeServices | undefined = composeServices
|
|
589
|
+
? Array.isArray(composeServices)
|
|
590
|
+
? {
|
|
591
|
+
postgres: composeServices.includes('postgres'),
|
|
592
|
+
redis: composeServices.includes('redis'),
|
|
593
|
+
rabbitmq: composeServices.includes('rabbitmq'),
|
|
594
|
+
}
|
|
595
|
+
: {
|
|
596
|
+
postgres: Boolean(composeServices.postgres),
|
|
597
|
+
redis: Boolean(composeServices.redis),
|
|
598
|
+
rabbitmq: Boolean(composeServices.rabbitmq),
|
|
599
|
+
}
|
|
600
|
+
: undefined;
|
|
601
|
+
|
|
602
|
+
// Ensure Dokploy is fully set up (credentials, project, app, registry, services)
|
|
603
|
+
const setupResult = await ensureDokploySetup(
|
|
604
|
+
config,
|
|
605
|
+
dockerConfig,
|
|
606
|
+
stage,
|
|
607
|
+
dockerServices,
|
|
608
|
+
);
|
|
609
|
+
dokployConfig = setupResult.config;
|
|
610
|
+
finalRegistry = dokployConfig.registry ?? dockerConfig.registry;
|
|
611
|
+
|
|
612
|
+
// Save provisioned service URLs to secrets before build
|
|
613
|
+
if (setupResult.serviceUrls) {
|
|
614
|
+
const { readStageSecrets, writeStageSecrets, initStageSecrets } =
|
|
615
|
+
await import('../secrets/storage');
|
|
616
|
+
let secrets = await readStageSecrets(stage);
|
|
617
|
+
|
|
618
|
+
// Create secrets file if it doesn't exist
|
|
619
|
+
if (!secrets) {
|
|
620
|
+
logger.log(` Creating secrets file for stage "${stage}"...`);
|
|
621
|
+
secrets = initStageSecrets(stage);
|
|
622
|
+
}
|
|
623
|
+
|
|
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
|
+
|
|
628
|
+
for (const [key, value] of Object.entries(setupResult.serviceUrls)) {
|
|
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
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (updated) {
|
|
649
|
+
await writeStageSecrets(secrets);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
35
654
|
// Build for production with secrets injection (unless skipped)
|
|
36
655
|
let masterKey: string | undefined;
|
|
37
656
|
if (!skipBuild) {
|
|
@@ -46,14 +665,6 @@ export async function deployCommand(
|
|
|
46
665
|
logger.log(`\n⏭️ Skipping build (--skip-build)`);
|
|
47
666
|
}
|
|
48
667
|
|
|
49
|
-
// Resolve docker config for image reference
|
|
50
|
-
const dockerConfig = resolveDockerConfig(config);
|
|
51
|
-
const imageName = dockerConfig.imageName ?? 'app';
|
|
52
|
-
const registry = dockerConfig.registry;
|
|
53
|
-
const imageRef = registry
|
|
54
|
-
? `${registry}/${imageName}:${imageTag}`
|
|
55
|
-
: `${imageName}:${imageTag}`;
|
|
56
|
-
|
|
57
668
|
// Deploy based on provider
|
|
58
669
|
let result: DeployResult;
|
|
59
670
|
|
|
@@ -70,25 +681,12 @@ export async function deployCommand(
|
|
|
70
681
|
}
|
|
71
682
|
|
|
72
683
|
case 'dokploy': {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (typeof dokployConfigRaw === 'boolean' || !dokployConfigRaw) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
'Dokploy provider requires configuration.\n' +
|
|
78
|
-
'Configure in gkm.config.ts:\n' +
|
|
79
|
-
' providers: {\n' +
|
|
80
|
-
' dokploy: {\n' +
|
|
81
|
-
" endpoint: 'https://dokploy.example.com',\n" +
|
|
82
|
-
" projectId: 'proj_xxx',\n" +
|
|
83
|
-
" applicationId: 'app_xxx',\n" +
|
|
84
|
-
' },\n' +
|
|
85
|
-
' }',
|
|
86
|
-
);
|
|
684
|
+
if (!dokployConfig) {
|
|
685
|
+
throw new Error('Dokploy config not initialized');
|
|
87
686
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const dokployConfig = dokployConfigRaw;
|
|
687
|
+
const finalImageRef = finalRegistry
|
|
688
|
+
? `${finalRegistry}/${imageName}:${imageTag}`
|
|
689
|
+
: `${imageName}:${imageTag}`;
|
|
92
690
|
|
|
93
691
|
// First build and push the Docker image
|
|
94
692
|
await deployDocker({
|
|
@@ -97,7 +695,7 @@ export async function deployCommand(
|
|
|
97
695
|
skipPush: false, // Dokploy needs the image in registry
|
|
98
696
|
masterKey,
|
|
99
697
|
config: {
|
|
100
|
-
registry:
|
|
698
|
+
registry: finalRegistry,
|
|
101
699
|
imageName: dockerConfig.imageName,
|
|
102
700
|
},
|
|
103
701
|
});
|
|
@@ -106,7 +704,7 @@ export async function deployCommand(
|
|
|
106
704
|
result = await deployDokploy({
|
|
107
705
|
stage,
|
|
108
706
|
tag: imageTag,
|
|
109
|
-
imageRef,
|
|
707
|
+
imageRef: finalImageRef,
|
|
110
708
|
masterKey,
|
|
111
709
|
config: dokployConfig,
|
|
112
710
|
});
|