mlgym-deploy 3.3.42 → 3.3.44
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/ADD_TO_CURSOR_SETTINGS.json +1 -1
- package/ADD_TO_CURSOR_SETTINGS.json.example +28 -0
- package/CHANGELOG-v3.3.42.md +707 -0
- package/Dockerfile +7 -0
- package/README.md +35 -0
- package/claude-desktop-config.json +1 -1
- package/claude-desktop-config.json.example +8 -0
- package/cursor-config.json +1 -1
- package/cursor-config.json.example +13 -0
- package/docs/CURSOR_SETUP.md +204 -0
- package/docs/NPM_RELEASE.md +230 -0
- package/index.js +9 -4
- package/mcp.json.example +13 -0
- package/package.json +8 -4
- package/tests/TEST_RESULTS.md +518 -0
- package/tests/archived/README.md +71 -0
- package/{deploy-hello-world.sh → tests/archived/deploy-hello-world.sh} +9 -6
- package/tests/deploy_dollie_test.sh +11 -7
- package/tests/misc/check-sdk-version.js +50 -0
- package/tests/mlgym_auth_login_test.sh +13 -9
- package/tests/mlgym_deploy_logs_test.sh +339 -0
- package/tests/mlgym_deploy_test.sh +341 -0
- package/tests/mlgym_status_test.sh +281 -0
- package/tests/mlgym_user_create_test.sh +35 -29
- package/tests/run-all-tests.sh +135 -41
- package/CURSOR_SETUP.md +0 -119
- package/index.js.backup-atomic +0 -1358
- /package/{DEBUG.md → docs/DEBUG.md} +0 -0
- /package/{SECURITY-UPDATE-v2.4.0.md → tests/archived/SECURITY-UPDATE-v2.4.0.md} +0 -0
- /package/{cursor-integration.js → tests/archived/cursor-integration.js} +0 -0
- /package/tests/{mlgym_auth_logout_test.sh → archived/mlgym_auth_logout_test.sh} +0 -0
- /package/tests/{mlgym_deployments_test.sh → archived/mlgym_deployments_test.sh} +0 -0
- /package/tests/{mlgym_project_init_test.sh → archived/mlgym_project_init_test.sh} +0 -0
- /package/tests/{mlgym_projects_get_test.sh → archived/mlgym_projects_get_test.sh} +0 -0
- /package/tests/{mlgym_projects_list_test.sh → archived/mlgym_projects_list_test.sh} +0 -0
package/index.js.backup-atomic
DELETED
|
@@ -1,1358 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import {
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListToolsRequestSchema
|
|
8
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import axios from 'axios';
|
|
10
|
-
import fs from 'fs/promises';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import os from 'os';
|
|
13
|
-
import { exec } from 'child_process';
|
|
14
|
-
import { promisify } from 'util';
|
|
15
|
-
import crypto from 'crypto';
|
|
16
|
-
|
|
17
|
-
const execAsync = promisify(exec);
|
|
18
|
-
|
|
19
|
-
// Current version of this MCP server - INCREMENT FOR WORKFLOW FIXES
|
|
20
|
-
const CURRENT_VERSION = '2.10.0'; // Fixed SSH key handling: generate and include in user creation request
|
|
21
|
-
const PACKAGE_NAME = 'mlgym-deploy';
|
|
22
|
-
|
|
23
|
-
// Version check state
|
|
24
|
-
let versionCheckResult = null;
|
|
25
|
-
let lastVersionCheck = 0;
|
|
26
|
-
const VERSION_CHECK_INTERVAL = 3600000; // Check once per hour
|
|
27
|
-
|
|
28
|
-
// Configuration
|
|
29
|
-
const CONFIG = {
|
|
30
|
-
backend_url: process.env.MLGYM_API_ENDPOINT || 'https://backend.eu.ezb.net',
|
|
31
|
-
gitlab_url: 'https://git.mlgym.io',
|
|
32
|
-
coolify_url: 'https://coolify.eu.ezb.net',
|
|
33
|
-
config_file: path.join(os.homedir(), '.mlgym', 'mcp_config.json')
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Helper to load/save authentication
|
|
37
|
-
async function loadAuth() {
|
|
38
|
-
try {
|
|
39
|
-
const data = await fs.readFile(CONFIG.config_file, 'utf8');
|
|
40
|
-
return JSON.parse(data);
|
|
41
|
-
} catch {
|
|
42
|
-
return { token: null, email: null };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function saveAuth(email, token) {
|
|
47
|
-
const dir = path.dirname(CONFIG.config_file);
|
|
48
|
-
await fs.mkdir(dir, { recursive: true });
|
|
49
|
-
await fs.writeFile(
|
|
50
|
-
CONFIG.config_file,
|
|
51
|
-
JSON.stringify({ email, token, timestamp: new Date().toISOString() }, null, 2)
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// API client helper
|
|
56
|
-
async function apiRequest(method, endpoint, data = null, useAuth = true) {
|
|
57
|
-
const config = {
|
|
58
|
-
method,
|
|
59
|
-
url: `${CONFIG.backend_url}${endpoint}`,
|
|
60
|
-
headers: {
|
|
61
|
-
'Content-Type': 'application/json'
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
if (useAuth) {
|
|
66
|
-
const auth = await loadAuth();
|
|
67
|
-
if (auth.token) {
|
|
68
|
-
config.headers['Authorization'] = `Bearer ${auth.token}`;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (data) {
|
|
73
|
-
config.data = data;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
const response = await axios(config);
|
|
78
|
-
return { success: true, data: response.data };
|
|
79
|
-
} catch (error) {
|
|
80
|
-
const errorData = error.response?.data || { error: error.message };
|
|
81
|
-
return { success: false, error: errorData.error || errorData.message || 'Request failed' };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Helper to generate random password
|
|
86
|
-
function generateRandomPassword() {
|
|
87
|
-
const chars = 'ABCDEFGHJKLMNPQRSTWXYZabcdefghjkmnpqrstwxyz23456789!@#$%^&*';
|
|
88
|
-
let password = '';
|
|
89
|
-
for (let i = 0; i < 16; i++) {
|
|
90
|
-
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
91
|
-
}
|
|
92
|
-
return password;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// SSH key generation
|
|
96
|
-
async function generateSSHKeyPair(email) {
|
|
97
|
-
const sshDir = path.join(os.homedir(), '.ssh');
|
|
98
|
-
await fs.mkdir(sshDir, { recursive: true, mode: 0o700 });
|
|
99
|
-
|
|
100
|
-
const sanitizedEmail = email.replace('@', '_at_').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
101
|
-
const keyPath = path.join(sshDir, `mlgym_${sanitizedEmail}`);
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
await fs.access(keyPath);
|
|
105
|
-
console.error(`SSH key already exists at ${keyPath}, using existing key`);
|
|
106
|
-
const publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
|
|
107
|
-
return { publicKey: publicKey.trim(), privateKeyPath: keyPath };
|
|
108
|
-
} catch {
|
|
109
|
-
// Key doesn't exist, generate new one
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const { stdout, stderr } = await execAsync(
|
|
113
|
-
`ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "${email}"`,
|
|
114
|
-
{ timeout: 10000 }
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
if (stderr && !stderr.includes('Generating public/private')) {
|
|
118
|
-
throw new Error(`SSH key generation failed: ${stderr}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
await execAsync(`chmod 600 "${keyPath}"`);
|
|
122
|
-
await execAsync(`chmod 644 "${keyPath}.pub"`);
|
|
123
|
-
|
|
124
|
-
const publicKey = await fs.readFile(`${keyPath}.pub`, 'utf8');
|
|
125
|
-
|
|
126
|
-
// Add to SSH config
|
|
127
|
-
const configPath = path.join(sshDir, 'config');
|
|
128
|
-
const configEntry = `
|
|
129
|
-
# MLGym GitLab (added by mlgym-deploy)
|
|
130
|
-
Host git.mlgym.io
|
|
131
|
-
User git
|
|
132
|
-
Port 22
|
|
133
|
-
IdentityFile ${keyPath}
|
|
134
|
-
StrictHostKeyChecking no
|
|
135
|
-
`;
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const existingConfig = await fs.readFile(configPath, 'utf8');
|
|
139
|
-
if (!existingConfig.includes('Host git.mlgym.io')) {
|
|
140
|
-
await fs.appendFile(configPath, configEntry);
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
await fs.writeFile(configPath, configEntry, { mode: 0o600 });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return { publicKey: publicKey.trim(), privateKeyPath: keyPath };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// SECURE CONSOLIDATED AUTHENTICATION FUNCTION
|
|
150
|
-
async function authenticate(args) {
|
|
151
|
-
const { email, password, create_if_not_exists = false, full_name, accept_terms } = args;
|
|
152
|
-
|
|
153
|
-
// Validate required fields
|
|
154
|
-
if (!email || !password) {
|
|
155
|
-
return {
|
|
156
|
-
content: [{
|
|
157
|
-
type: 'text',
|
|
158
|
-
text: JSON.stringify({
|
|
159
|
-
status: 'error',
|
|
160
|
-
message: 'Email and password are required',
|
|
161
|
-
required_fields: {
|
|
162
|
-
email: email ? '✓ provided' : '✗ missing',
|
|
163
|
-
password: password ? '✓ provided' : '✗ missing'
|
|
164
|
-
}
|
|
165
|
-
}, null, 2)
|
|
166
|
-
}]
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
console.error(`Attempting authentication for: ${email}`);
|
|
171
|
-
|
|
172
|
-
// Step 1: Always try login first (never reveal if account exists)
|
|
173
|
-
try {
|
|
174
|
-
const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
|
|
175
|
-
email,
|
|
176
|
-
password
|
|
177
|
-
}, false);
|
|
178
|
-
|
|
179
|
-
if (loginResult.success && loginResult.data.token) {
|
|
180
|
-
// Login successful - save token and setup SSH if needed
|
|
181
|
-
await saveAuth(email, loginResult.data.token);
|
|
182
|
-
|
|
183
|
-
// Check for SSH keys
|
|
184
|
-
const keysResult = await apiRequest('GET', '/api/v1/keys', null, true);
|
|
185
|
-
const sshKeys = keysResult.success ? keysResult.data : [];
|
|
186
|
-
|
|
187
|
-
// If no SSH keys, generate and add one
|
|
188
|
-
if (sshKeys.length === 0) {
|
|
189
|
-
console.error('No SSH keys found, generating new key pair...');
|
|
190
|
-
const { publicKey, privateKeyPath } = await generateSSHKeyPair(email);
|
|
191
|
-
|
|
192
|
-
const keyTitle = `mlgym-${new Date().toISOString().split('T')[0]}`;
|
|
193
|
-
const keyResult = await apiRequest('POST', '/api/v1/keys', {
|
|
194
|
-
title: keyTitle,
|
|
195
|
-
key: publicKey
|
|
196
|
-
}, true);
|
|
197
|
-
|
|
198
|
-
if (keyResult.success) {
|
|
199
|
-
return {
|
|
200
|
-
content: [{
|
|
201
|
-
type: 'text',
|
|
202
|
-
text: JSON.stringify({
|
|
203
|
-
status: 'authenticated',
|
|
204
|
-
message: 'Successfully logged in and SSH key configured',
|
|
205
|
-
email: email,
|
|
206
|
-
user_id: loginResult.data.user?.user_id,
|
|
207
|
-
gitlab_username: loginResult.data.user?.gitlab_username,
|
|
208
|
-
ssh_key_added: true,
|
|
209
|
-
ssh_key_path: privateKeyPath,
|
|
210
|
-
next_steps: [
|
|
211
|
-
'Authentication successful',
|
|
212
|
-
`SSH key generated at: ${privateKeyPath}`,
|
|
213
|
-
'You can now create projects and deploy'
|
|
214
|
-
]
|
|
215
|
-
}, null, 2)
|
|
216
|
-
}]
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// User has SSH keys already
|
|
222
|
-
return {
|
|
223
|
-
content: [{
|
|
224
|
-
type: 'text',
|
|
225
|
-
text: JSON.stringify({
|
|
226
|
-
status: 'authenticated',
|
|
227
|
-
message: 'Successfully logged in',
|
|
228
|
-
email: email,
|
|
229
|
-
user_id: loginResult.data.user?.user_id,
|
|
230
|
-
gitlab_username: loginResult.data.user?.gitlab_username,
|
|
231
|
-
ssh_keys_count: sshKeys.length,
|
|
232
|
-
next_steps: [
|
|
233
|
-
'Authentication successful',
|
|
234
|
-
'You can now create projects and deploy'
|
|
235
|
-
]
|
|
236
|
-
}, null, 2)
|
|
237
|
-
}]
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
} catch (error) {
|
|
241
|
-
// Login failed - continue to next step
|
|
242
|
-
console.error('Login attempt failed:', error.message);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Step 2: Login failed - check if we should create account
|
|
246
|
-
if (create_if_not_exists) {
|
|
247
|
-
// Validate required fields for account creation
|
|
248
|
-
if (!full_name) {
|
|
249
|
-
return {
|
|
250
|
-
content: [{
|
|
251
|
-
type: 'text',
|
|
252
|
-
text: JSON.stringify({
|
|
253
|
-
status: 'error',
|
|
254
|
-
message: 'Account creation requires full_name',
|
|
255
|
-
note: 'Login failed. To create a new account, provide full_name and accept_terms=true'
|
|
256
|
-
}, null, 2)
|
|
257
|
-
}]
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (!accept_terms) {
|
|
262
|
-
return {
|
|
263
|
-
content: [{
|
|
264
|
-
type: 'text',
|
|
265
|
-
text: JSON.stringify({
|
|
266
|
-
status: 'error',
|
|
267
|
-
message: 'You must accept the terms and conditions to create an account',
|
|
268
|
-
note: 'Set accept_terms=true to proceed with account creation'
|
|
269
|
-
}, null, 2)
|
|
270
|
-
}]
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
console.error(`Creating new account for: ${email}`);
|
|
275
|
-
|
|
276
|
-
// Try to create account
|
|
277
|
-
try {
|
|
278
|
-
// Generate SSH key BEFORE creating user
|
|
279
|
-
console.error('Generating SSH key for new user...');
|
|
280
|
-
const { publicKey, privateKeyPath } = await generateSSHKeyPair(email);
|
|
281
|
-
|
|
282
|
-
// Create user with SSH key included
|
|
283
|
-
const createResult = await apiRequest('POST', '/api/v1/users', {
|
|
284
|
-
email,
|
|
285
|
-
name: full_name,
|
|
286
|
-
password,
|
|
287
|
-
ssh_key: publicKey // Include SSH key in user creation
|
|
288
|
-
}, false);
|
|
289
|
-
|
|
290
|
-
if (createResult.success) {
|
|
291
|
-
// Account created, now login
|
|
292
|
-
const loginResult = await apiRequest('POST', '/api/v1/auth/login', {
|
|
293
|
-
email,
|
|
294
|
-
password
|
|
295
|
-
}, false);
|
|
296
|
-
|
|
297
|
-
if (loginResult.success && loginResult.data.token) {
|
|
298
|
-
await saveAuth(email, loginResult.data.token);
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
content: [{
|
|
302
|
-
type: 'text',
|
|
303
|
-
text: JSON.stringify({
|
|
304
|
-
status: 'authenticated',
|
|
305
|
-
message: 'Account created successfully and logged in',
|
|
306
|
-
email: email,
|
|
307
|
-
user_id: loginResult.data.user?.user_id,
|
|
308
|
-
gitlab_username: loginResult.data.user?.gitlab_username,
|
|
309
|
-
ssh_key_path: privateKeyPath,
|
|
310
|
-
ssh_key_status: createResult.data.ssh_key_status || 'SSH key configured',
|
|
311
|
-
next_steps: [
|
|
312
|
-
'New account created',
|
|
313
|
-
`SSH key generated at: ${privateKeyPath}`,
|
|
314
|
-
'SSH key automatically uploaded to GitLab',
|
|
315
|
-
'You can now create projects and deploy'
|
|
316
|
-
]
|
|
317
|
-
}, null, 2)
|
|
318
|
-
}]
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
} catch (error) {
|
|
323
|
-
// Account creation failed - generic error
|
|
324
|
-
console.error('Account creation failed:', error.message);
|
|
325
|
-
return {
|
|
326
|
-
content: [{
|
|
327
|
-
type: 'text',
|
|
328
|
-
text: JSON.stringify({
|
|
329
|
-
status: 'error',
|
|
330
|
-
message: 'Account creation failed. The email may already be in use or password requirements not met.',
|
|
331
|
-
requirements: {
|
|
332
|
-
password: 'Minimum 8 characters with mixed case, numbers, and special characters',
|
|
333
|
-
email: 'Valid email address not already registered'
|
|
334
|
-
}
|
|
335
|
-
}, null, 2)
|
|
336
|
-
}]
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Step 3: Generic authentication failure (never reveal if account exists)
|
|
342
|
-
return {
|
|
343
|
-
content: [{
|
|
344
|
-
type: 'text',
|
|
345
|
-
text: JSON.stringify({
|
|
346
|
-
status: 'error',
|
|
347
|
-
message: 'Authentication failed. Invalid credentials.',
|
|
348
|
-
hint: 'If you need to create a new account, set create_if_not_exists=true and provide full_name and accept_terms=true'
|
|
349
|
-
}, null, 2)
|
|
350
|
-
}]
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Analyze project to detect type, framework, and configuration
|
|
355
|
-
async function analyzeProject(local_path = '.') {
|
|
356
|
-
const absolutePath = path.resolve(local_path);
|
|
357
|
-
const dirName = path.basename(absolutePath);
|
|
358
|
-
|
|
359
|
-
const analysis = {
|
|
360
|
-
project_type: 'unknown',
|
|
361
|
-
detected_files: [],
|
|
362
|
-
suggested_name: dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
363
|
-
has_dockerfile: false,
|
|
364
|
-
has_git: false,
|
|
365
|
-
framework: null,
|
|
366
|
-
build_command: null,
|
|
367
|
-
start_command: null,
|
|
368
|
-
package_manager: null
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
// Check for git
|
|
373
|
-
try {
|
|
374
|
-
await execAsync('git status', { cwd: absolutePath });
|
|
375
|
-
analysis.has_git = true;
|
|
376
|
-
} catch {}
|
|
377
|
-
|
|
378
|
-
// Check for Dockerfile
|
|
379
|
-
try {
|
|
380
|
-
await fs.access(path.join(absolutePath, 'Dockerfile'));
|
|
381
|
-
analysis.has_dockerfile = true;
|
|
382
|
-
analysis.detected_files.push('Dockerfile');
|
|
383
|
-
} catch {}
|
|
384
|
-
|
|
385
|
-
// Check for Node.js project
|
|
386
|
-
try {
|
|
387
|
-
const packageJsonPath = path.join(absolutePath, 'package.json');
|
|
388
|
-
await fs.access(packageJsonPath);
|
|
389
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
390
|
-
|
|
391
|
-
analysis.project_type = 'nodejs';
|
|
392
|
-
analysis.detected_files.push('package.json');
|
|
393
|
-
analysis.suggested_name = packageJson.name || analysis.suggested_name;
|
|
394
|
-
|
|
395
|
-
// Detect package manager
|
|
396
|
-
try {
|
|
397
|
-
await fs.access(path.join(absolutePath, 'package-lock.json'));
|
|
398
|
-
analysis.package_manager = 'npm';
|
|
399
|
-
analysis.detected_files.push('package-lock.json');
|
|
400
|
-
} catch {
|
|
401
|
-
try {
|
|
402
|
-
await fs.access(path.join(absolutePath, 'yarn.lock'));
|
|
403
|
-
analysis.package_manager = 'yarn';
|
|
404
|
-
analysis.detected_files.push('yarn.lock');
|
|
405
|
-
} catch {
|
|
406
|
-
analysis.package_manager = 'npm'; // default
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Detect framework
|
|
411
|
-
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
412
|
-
if (deps.next) {
|
|
413
|
-
analysis.framework = 'nextjs';
|
|
414
|
-
analysis.build_command = packageJson.scripts?.build || 'npm run build';
|
|
415
|
-
analysis.start_command = packageJson.scripts?.start || 'npm start';
|
|
416
|
-
} else if (deps.express) {
|
|
417
|
-
analysis.framework = 'express';
|
|
418
|
-
analysis.start_command = packageJson.scripts?.start || 'node index.js';
|
|
419
|
-
} else if (deps.react) {
|
|
420
|
-
analysis.framework = 'react';
|
|
421
|
-
analysis.build_command = packageJson.scripts?.build || 'npm run build';
|
|
422
|
-
} else if (deps.vue) {
|
|
423
|
-
analysis.framework = 'vue';
|
|
424
|
-
analysis.build_command = packageJson.scripts?.build || 'npm run build';
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Use package.json scripts as fallback
|
|
428
|
-
if (!analysis.build_command && packageJson.scripts?.build) {
|
|
429
|
-
analysis.build_command = 'npm run build';
|
|
430
|
-
}
|
|
431
|
-
if (!analysis.start_command && packageJson.scripts?.start) {
|
|
432
|
-
analysis.start_command = 'npm start';
|
|
433
|
-
}
|
|
434
|
-
} catch {}
|
|
435
|
-
|
|
436
|
-
// Check for Python project
|
|
437
|
-
if (analysis.project_type === 'unknown') {
|
|
438
|
-
try {
|
|
439
|
-
await fs.access(path.join(absolutePath, 'requirements.txt'));
|
|
440
|
-
analysis.project_type = 'python';
|
|
441
|
-
analysis.detected_files.push('requirements.txt');
|
|
442
|
-
|
|
443
|
-
// Check for specific Python files
|
|
444
|
-
try {
|
|
445
|
-
await fs.access(path.join(absolutePath, 'app.py'));
|
|
446
|
-
analysis.framework = 'flask';
|
|
447
|
-
analysis.start_command = 'python app.py';
|
|
448
|
-
analysis.detected_files.push('app.py');
|
|
449
|
-
} catch {
|
|
450
|
-
try {
|
|
451
|
-
await fs.access(path.join(absolutePath, 'main.py'));
|
|
452
|
-
analysis.framework = 'fastapi';
|
|
453
|
-
analysis.start_command = 'uvicorn main:app --host 0.0.0.0';
|
|
454
|
-
analysis.detected_files.push('main.py');
|
|
455
|
-
} catch {}
|
|
456
|
-
}
|
|
457
|
-
} catch {}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Check for static HTML project
|
|
461
|
-
if (analysis.project_type === 'unknown') {
|
|
462
|
-
try {
|
|
463
|
-
await fs.access(path.join(absolutePath, 'index.html'));
|
|
464
|
-
analysis.project_type = 'static';
|
|
465
|
-
analysis.framework = 'html';
|
|
466
|
-
analysis.detected_files.push('index.html');
|
|
467
|
-
} catch {}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Check for Go project
|
|
471
|
-
if (analysis.project_type === 'unknown') {
|
|
472
|
-
try {
|
|
473
|
-
await fs.access(path.join(absolutePath, 'go.mod'));
|
|
474
|
-
analysis.project_type = 'go';
|
|
475
|
-
analysis.detected_files.push('go.mod');
|
|
476
|
-
analysis.build_command = 'go build -o app';
|
|
477
|
-
analysis.start_command = './app';
|
|
478
|
-
} catch {}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
} catch (error) {
|
|
482
|
-
console.error('Project analysis error:', error);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return analysis;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Check for existing MLGym project in current directory
|
|
489
|
-
async function checkExistingProject(local_path = '.') {
|
|
490
|
-
const absolutePath = path.resolve(local_path);
|
|
491
|
-
|
|
492
|
-
// Check for git repository
|
|
493
|
-
try {
|
|
494
|
-
const { stdout: remotes } = await execAsync('git remote -v', { cwd: absolutePath });
|
|
495
|
-
|
|
496
|
-
// Check for mlgym remote
|
|
497
|
-
if (remotes.includes('mlgym') && remotes.includes('git.mlgym.io')) {
|
|
498
|
-
// Extract project info from remote URL
|
|
499
|
-
const match = remotes.match(/mlgym\s+git@git\.mlgym\.io:([^\/]+)\/([^\.]+)\.git/);
|
|
500
|
-
if (match) {
|
|
501
|
-
const [, namespace, projectName] = match;
|
|
502
|
-
return {
|
|
503
|
-
exists: true,
|
|
504
|
-
name: projectName,
|
|
505
|
-
namespace: namespace,
|
|
506
|
-
configured: true,
|
|
507
|
-
message: `Project '${projectName}' already configured for MLGym deployment`
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Git repo exists but no mlgym remote
|
|
513
|
-
return {
|
|
514
|
-
exists: false,
|
|
515
|
-
has_git: true,
|
|
516
|
-
configured: false,
|
|
517
|
-
message: 'Git repository exists but no MLGym remote configured'
|
|
518
|
-
};
|
|
519
|
-
} catch {
|
|
520
|
-
// No git repository
|
|
521
|
-
return {
|
|
522
|
-
exists: false,
|
|
523
|
-
has_git: false,
|
|
524
|
-
configured: false,
|
|
525
|
-
message: 'No git repository found in current directory'
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Generate appropriate Dockerfile based on project type
|
|
531
|
-
function generateDockerfile(projectType, framework, packageManager = 'npm') {
|
|
532
|
-
let dockerfile = '';
|
|
533
|
-
|
|
534
|
-
if (projectType === 'nodejs') {
|
|
535
|
-
if (framework === 'nextjs') {
|
|
536
|
-
dockerfile = `# Build stage
|
|
537
|
-
FROM node:18-alpine AS builder
|
|
538
|
-
WORKDIR /app
|
|
539
|
-
COPY package*.json ./
|
|
540
|
-
RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
|
|
541
|
-
COPY . .
|
|
542
|
-
RUN ${packageManager} run build
|
|
543
|
-
|
|
544
|
-
# Production stage
|
|
545
|
-
FROM node:18-alpine
|
|
546
|
-
WORKDIR /app
|
|
547
|
-
COPY --from=builder /app/.next ./.next
|
|
548
|
-
COPY --from=builder /app/node_modules ./node_modules
|
|
549
|
-
COPY --from=builder /app/package.json ./
|
|
550
|
-
COPY --from=builder /app/public ./public
|
|
551
|
-
EXPOSE 3000
|
|
552
|
-
CMD ["${packageManager}", "start"]`;
|
|
553
|
-
} else if (framework === 'express') {
|
|
554
|
-
dockerfile = `FROM node:18-alpine
|
|
555
|
-
WORKDIR /app
|
|
556
|
-
COPY package*.json ./
|
|
557
|
-
RUN ${packageManager} ${packageManager === 'npm' ? 'ci --only=production' : 'install --frozen-lockfile --production'}
|
|
558
|
-
COPY . .
|
|
559
|
-
EXPOSE 3000
|
|
560
|
-
CMD ["node", "index.js"]`;
|
|
561
|
-
} else if (framework === 'react' || framework === 'vue') {
|
|
562
|
-
dockerfile = `# Build stage
|
|
563
|
-
FROM node:18-alpine AS builder
|
|
564
|
-
WORKDIR /app
|
|
565
|
-
COPY package*.json ./
|
|
566
|
-
RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
|
|
567
|
-
COPY . .
|
|
568
|
-
RUN ${packageManager} run build
|
|
569
|
-
|
|
570
|
-
# Production stage
|
|
571
|
-
FROM nginx:alpine
|
|
572
|
-
COPY --from=builder /app/${framework === 'react' ? 'build' : 'dist'} /usr/share/nginx/html
|
|
573
|
-
EXPOSE 80
|
|
574
|
-
CMD ["nginx", "-g", "daemon off;"]`;
|
|
575
|
-
} else {
|
|
576
|
-
// Generic Node.js
|
|
577
|
-
dockerfile = `FROM node:18-alpine
|
|
578
|
-
WORKDIR /app
|
|
579
|
-
COPY package*.json ./
|
|
580
|
-
RUN ${packageManager} ${packageManager === 'npm' ? 'ci --only=production' : 'install --frozen-lockfile --production'}
|
|
581
|
-
COPY . .
|
|
582
|
-
EXPOSE 3000
|
|
583
|
-
CMD ["${packageManager}", "start"]`;
|
|
584
|
-
}
|
|
585
|
-
} else if (projectType === 'python') {
|
|
586
|
-
if (framework === 'flask') {
|
|
587
|
-
dockerfile = `FROM python:3.11-slim
|
|
588
|
-
WORKDIR /app
|
|
589
|
-
COPY requirements.txt .
|
|
590
|
-
RUN pip install --no-cache-dir -r requirements.txt
|
|
591
|
-
COPY . .
|
|
592
|
-
EXPOSE 5000
|
|
593
|
-
CMD ["python", "app.py"]`;
|
|
594
|
-
} else if (framework === 'fastapi') {
|
|
595
|
-
dockerfile = `FROM python:3.11-slim
|
|
596
|
-
WORKDIR /app
|
|
597
|
-
COPY requirements.txt .
|
|
598
|
-
RUN pip install --no-cache-dir -r requirements.txt
|
|
599
|
-
COPY . .
|
|
600
|
-
EXPOSE 8000
|
|
601
|
-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]`;
|
|
602
|
-
} else {
|
|
603
|
-
// Generic Python
|
|
604
|
-
dockerfile = `FROM python:3.11-slim
|
|
605
|
-
WORKDIR /app
|
|
606
|
-
COPY requirements.txt .
|
|
607
|
-
RUN pip install --no-cache-dir -r requirements.txt
|
|
608
|
-
COPY . .
|
|
609
|
-
EXPOSE 8000
|
|
610
|
-
CMD ["python", "main.py"]`;
|
|
611
|
-
}
|
|
612
|
-
} else if (projectType === 'static') {
|
|
613
|
-
dockerfile = `FROM nginx:alpine
|
|
614
|
-
COPY . /usr/share/nginx/html
|
|
615
|
-
EXPOSE 80
|
|
616
|
-
CMD ["nginx", "-g", "daemon off;"]`;
|
|
617
|
-
} else if (projectType === 'go') {
|
|
618
|
-
dockerfile = `# Build stage
|
|
619
|
-
FROM golang:1.21-alpine AS builder
|
|
620
|
-
WORKDIR /app
|
|
621
|
-
COPY go.mod go.sum ./
|
|
622
|
-
RUN go mod download
|
|
623
|
-
COPY . .
|
|
624
|
-
RUN go build -o app
|
|
625
|
-
|
|
626
|
-
# Production stage
|
|
627
|
-
FROM alpine:latest
|
|
628
|
-
RUN apk --no-cache add ca-certificates
|
|
629
|
-
WORKDIR /root/
|
|
630
|
-
COPY --from=builder /app/app .
|
|
631
|
-
EXPOSE 8080
|
|
632
|
-
CMD ["./app"]`;
|
|
633
|
-
} else {
|
|
634
|
-
// Unknown type - basic Alpine with shell
|
|
635
|
-
dockerfile = `FROM alpine:latest
|
|
636
|
-
WORKDIR /app
|
|
637
|
-
COPY . .
|
|
638
|
-
RUN echo "Unknown project type - please configure manually"
|
|
639
|
-
CMD ["/bin/sh"]`;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return dockerfile;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Prepare project for deployment
|
|
646
|
-
async function prepareProject(args) {
|
|
647
|
-
const { local_path = '.', project_type, framework, package_manager } = args;
|
|
648
|
-
const absolutePath = path.resolve(local_path);
|
|
649
|
-
|
|
650
|
-
const actions = [];
|
|
651
|
-
|
|
652
|
-
try {
|
|
653
|
-
// Check if Dockerfile exists
|
|
654
|
-
const dockerfilePath = path.join(absolutePath, 'Dockerfile');
|
|
655
|
-
let dockerfileExists = false;
|
|
656
|
-
|
|
657
|
-
try {
|
|
658
|
-
await fs.access(dockerfilePath);
|
|
659
|
-
dockerfileExists = true;
|
|
660
|
-
actions.push('Dockerfile already exists - skipping generation');
|
|
661
|
-
} catch {}
|
|
662
|
-
|
|
663
|
-
// Generate Dockerfile if missing
|
|
664
|
-
if (!dockerfileExists && project_type !== 'unknown') {
|
|
665
|
-
const dockerfile = generateDockerfile(project_type, framework, package_manager);
|
|
666
|
-
await fs.writeFile(dockerfilePath, dockerfile);
|
|
667
|
-
actions.push(`Generated Dockerfile for ${project_type}/${framework || 'generic'}`);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Check/create .gitignore
|
|
671
|
-
const gitignorePath = path.join(absolutePath, '.gitignore');
|
|
672
|
-
let gitignoreExists = false;
|
|
673
|
-
|
|
674
|
-
try {
|
|
675
|
-
await fs.access(gitignorePath);
|
|
676
|
-
gitignoreExists = true;
|
|
677
|
-
} catch {}
|
|
678
|
-
|
|
679
|
-
if (!gitignoreExists) {
|
|
680
|
-
let gitignoreContent = '';
|
|
681
|
-
|
|
682
|
-
if (project_type === 'nodejs') {
|
|
683
|
-
gitignoreContent = `node_modules/
|
|
684
|
-
.env
|
|
685
|
-
.env.local
|
|
686
|
-
dist/
|
|
687
|
-
build/
|
|
688
|
-
.next/
|
|
689
|
-
*.log`;
|
|
690
|
-
} else if (project_type === 'python') {
|
|
691
|
-
gitignoreContent = `__pycache__/
|
|
692
|
-
*.py[cod]
|
|
693
|
-
*$py.class
|
|
694
|
-
.env
|
|
695
|
-
venv/
|
|
696
|
-
env/
|
|
697
|
-
.venv/`;
|
|
698
|
-
} else {
|
|
699
|
-
gitignoreContent = `.env
|
|
700
|
-
*.log
|
|
701
|
-
.DS_Store`;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
await fs.writeFile(gitignorePath, gitignoreContent);
|
|
705
|
-
actions.push('Created .gitignore file');
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return {
|
|
709
|
-
status: 'success',
|
|
710
|
-
message: 'Project prepared for deployment',
|
|
711
|
-
actions: actions,
|
|
712
|
-
dockerfile_created: !dockerfileExists && project_type !== 'unknown',
|
|
713
|
-
project_type: project_type,
|
|
714
|
-
framework: framework
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
} catch (error) {
|
|
718
|
-
return {
|
|
719
|
-
status: 'error',
|
|
720
|
-
message: 'Failed to prepare project',
|
|
721
|
-
error: error.message,
|
|
722
|
-
actions: actions
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Check authentication status
|
|
728
|
-
async function checkAuthStatus() {
|
|
729
|
-
const auth = await loadAuth();
|
|
730
|
-
|
|
731
|
-
if (!auth.token) {
|
|
732
|
-
return {
|
|
733
|
-
content: [{
|
|
734
|
-
type: 'text',
|
|
735
|
-
text: JSON.stringify({
|
|
736
|
-
status: 'not_authenticated',
|
|
737
|
-
message: 'No authentication found. Please use mlgym_authenticate first.',
|
|
738
|
-
next_step: 'Call mlgym_authenticate with email and password'
|
|
739
|
-
}, null, 2)
|
|
740
|
-
}]
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Verify token is still valid
|
|
745
|
-
try {
|
|
746
|
-
const result = await apiRequest('GET', '/api/v1/user', null, true);
|
|
747
|
-
|
|
748
|
-
if (result.success) {
|
|
749
|
-
return {
|
|
750
|
-
content: [{
|
|
751
|
-
type: 'text',
|
|
752
|
-
text: JSON.stringify({
|
|
753
|
-
status: 'authenticated',
|
|
754
|
-
email: auth.email,
|
|
755
|
-
message: 'Authentication valid',
|
|
756
|
-
user: result.data,
|
|
757
|
-
next_step: 'You can now use mlgym_project_init to create a project'
|
|
758
|
-
}, null, 2)
|
|
759
|
-
}]
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
} catch (error) {
|
|
763
|
-
// Token might be expired
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
return {
|
|
767
|
-
content: [{
|
|
768
|
-
type: 'text',
|
|
769
|
-
text: JSON.stringify({
|
|
770
|
-
status: 'token_expired',
|
|
771
|
-
message: 'Authentication token expired. Please authenticate again.',
|
|
772
|
-
next_step: 'Call mlgym_authenticate with email and password'
|
|
773
|
-
}, null, 2)
|
|
774
|
-
}]
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Smart deployment initialization that follows the correct workflow
|
|
779
|
-
async function smartDeploy(args) {
|
|
780
|
-
const { local_path = '.' } = args;
|
|
781
|
-
const absolutePath = path.resolve(local_path);
|
|
782
|
-
|
|
783
|
-
const steps = [];
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
// Step 1: Check authentication
|
|
787
|
-
steps.push({ step: 'auth_check', status: 'running' });
|
|
788
|
-
const auth = await loadAuth();
|
|
789
|
-
if (!auth.token) {
|
|
790
|
-
steps[steps.length - 1].status = 'failed';
|
|
791
|
-
return {
|
|
792
|
-
content: [{
|
|
793
|
-
type: 'text',
|
|
794
|
-
text: JSON.stringify({
|
|
795
|
-
status: 'error',
|
|
796
|
-
message: 'Not authenticated. Please use mlgym_authenticate first',
|
|
797
|
-
workflow_steps: steps
|
|
798
|
-
}, null, 2)
|
|
799
|
-
}]
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
steps[steps.length - 1].status = 'completed';
|
|
803
|
-
|
|
804
|
-
// Step 2: Analyze project
|
|
805
|
-
steps.push({ step: 'project_analysis', status: 'running' });
|
|
806
|
-
const analysis = await analyzeProject(local_path);
|
|
807
|
-
steps[steps.length - 1].status = 'completed';
|
|
808
|
-
steps[steps.length - 1].result = {
|
|
809
|
-
type: analysis.project_type,
|
|
810
|
-
framework: analysis.framework,
|
|
811
|
-
suggested_name: analysis.suggested_name
|
|
812
|
-
};
|
|
813
|
-
|
|
814
|
-
// Step 3: Check existing project
|
|
815
|
-
steps.push({ step: 'check_existing', status: 'running' });
|
|
816
|
-
const projectStatus = await checkExistingProject(local_path);
|
|
817
|
-
steps[steps.length - 1].status = 'completed';
|
|
818
|
-
|
|
819
|
-
if (projectStatus.configured) {
|
|
820
|
-
return {
|
|
821
|
-
content: [{
|
|
822
|
-
type: 'text',
|
|
823
|
-
text: JSON.stringify({
|
|
824
|
-
status: 'info',
|
|
825
|
-
message: projectStatus.message,
|
|
826
|
-
project_name: projectStatus.name,
|
|
827
|
-
git_remote: `git@git.mlgym.io:${projectStatus.namespace}/${projectStatus.name}.git`,
|
|
828
|
-
next_steps: [
|
|
829
|
-
'Project already configured',
|
|
830
|
-
'Run: git push mlgym main'
|
|
831
|
-
],
|
|
832
|
-
workflow_steps: steps
|
|
833
|
-
}, null, 2)
|
|
834
|
-
}]
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Step 4: Prepare project (generate Dockerfile if needed)
|
|
839
|
-
steps.push({ step: 'prepare_project', status: 'running' });
|
|
840
|
-
if (!analysis.has_dockerfile && analysis.project_type !== 'unknown') {
|
|
841
|
-
const prepResult = await prepareProject({
|
|
842
|
-
local_path,
|
|
843
|
-
project_type: analysis.project_type,
|
|
844
|
-
framework: analysis.framework,
|
|
845
|
-
package_manager: analysis.package_manager
|
|
846
|
-
});
|
|
847
|
-
steps[steps.length - 1].status = 'completed';
|
|
848
|
-
steps[steps.length - 1].result = prepResult.actions;
|
|
849
|
-
} else {
|
|
850
|
-
steps[steps.length - 1].status = 'skipped';
|
|
851
|
-
steps[steps.length - 1].result = 'Dockerfile already exists or project type unknown';
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Return analysis and next steps
|
|
855
|
-
return {
|
|
856
|
-
content: [{
|
|
857
|
-
type: 'text',
|
|
858
|
-
text: JSON.stringify({
|
|
859
|
-
status: 'ready',
|
|
860
|
-
message: 'Project analyzed and prepared. Ready for MLGym initialization.',
|
|
861
|
-
analysis: {
|
|
862
|
-
project_type: analysis.project_type,
|
|
863
|
-
framework: analysis.framework,
|
|
864
|
-
suggested_name: analysis.suggested_name,
|
|
865
|
-
has_dockerfile: analysis.has_dockerfile || true
|
|
866
|
-
},
|
|
867
|
-
next_step: 'Use mlgym_project_init with project details to create MLGym project',
|
|
868
|
-
suggested_params: {
|
|
869
|
-
name: analysis.suggested_name,
|
|
870
|
-
description: `${analysis.framework || analysis.project_type} application`,
|
|
871
|
-
enable_deployment: true,
|
|
872
|
-
hostname: analysis.suggested_name
|
|
873
|
-
},
|
|
874
|
-
workflow_steps: steps
|
|
875
|
-
}, null, 2)
|
|
876
|
-
}]
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
} catch (error) {
|
|
880
|
-
return {
|
|
881
|
-
content: [{
|
|
882
|
-
type: 'text',
|
|
883
|
-
text: JSON.stringify({
|
|
884
|
-
status: 'error',
|
|
885
|
-
message: 'Smart deploy failed',
|
|
886
|
-
error: error.message,
|
|
887
|
-
workflow_steps: steps
|
|
888
|
-
}, null, 2)
|
|
889
|
-
}]
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Initialize Project (requires authentication)
|
|
895
|
-
async function initProject(args) {
|
|
896
|
-
let { name, description, enable_deployment = true, hostname, local_path = '.' } = args;
|
|
897
|
-
|
|
898
|
-
// Validate required fields
|
|
899
|
-
if (!name || !description) {
|
|
900
|
-
return {
|
|
901
|
-
content: [{
|
|
902
|
-
type: 'text',
|
|
903
|
-
text: JSON.stringify({
|
|
904
|
-
status: 'error',
|
|
905
|
-
message: 'Project name and description are required',
|
|
906
|
-
required_fields: {
|
|
907
|
-
name: name ? '✓ provided' : '✗ missing',
|
|
908
|
-
description: description ? '✓ provided' : '✗ missing'
|
|
909
|
-
}
|
|
910
|
-
}, null, 2)
|
|
911
|
-
}]
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
if (enable_deployment && !hostname) {
|
|
916
|
-
return {
|
|
917
|
-
content: [{
|
|
918
|
-
type: 'text',
|
|
919
|
-
text: JSON.stringify({
|
|
920
|
-
status: 'error',
|
|
921
|
-
message: 'Hostname is required when deployment is enabled',
|
|
922
|
-
required_fields: {
|
|
923
|
-
hostname: '✗ missing - will be used as subdomain (e.g., "myapp" for myapp.ezb.net)'
|
|
924
|
-
}
|
|
925
|
-
}, null, 2)
|
|
926
|
-
}]
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Check authentication
|
|
931
|
-
const auth = await loadAuth();
|
|
932
|
-
if (!auth.token) {
|
|
933
|
-
return {
|
|
934
|
-
content: [{
|
|
935
|
-
type: 'text',
|
|
936
|
-
text: JSON.stringify({
|
|
937
|
-
status: 'error',
|
|
938
|
-
message: 'Not authenticated. Please use mlgym_authenticate first'
|
|
939
|
-
}, null, 2)
|
|
940
|
-
}]
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
console.error(`Creating project: ${name}`);
|
|
945
|
-
|
|
946
|
-
// Create project via backend API with FLAT structure (matching CLI)
|
|
947
|
-
const projectData = {
|
|
948
|
-
name: name,
|
|
949
|
-
description: description
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
if (enable_deployment) {
|
|
953
|
-
// Generate a secure webhook secret for deployments
|
|
954
|
-
const webhookSecret = Array.from(
|
|
955
|
-
crypto.getRandomValues(new Uint8Array(32)),
|
|
956
|
-
byte => byte.toString(16).padStart(2, '0')
|
|
957
|
-
).join('');
|
|
958
|
-
|
|
959
|
-
// Use FLAT structure exactly like CLI does - no nested deployment_info
|
|
960
|
-
projectData.enable_deployment = true;
|
|
961
|
-
projectData.webhook_secret = webhookSecret;
|
|
962
|
-
projectData.hostname = hostname;
|
|
963
|
-
projectData.local_path = local_path;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const result = await apiRequest('POST', '/api/v1/projects', projectData, true);
|
|
967
|
-
|
|
968
|
-
if (!result.success) {
|
|
969
|
-
return {
|
|
970
|
-
content: [{
|
|
971
|
-
type: 'text',
|
|
972
|
-
text: JSON.stringify({
|
|
973
|
-
status: 'error',
|
|
974
|
-
message: 'Failed to create project',
|
|
975
|
-
error: result.error
|
|
976
|
-
}, null, 2)
|
|
977
|
-
}]
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
const project = result.data;
|
|
982
|
-
|
|
983
|
-
// Initialize local git repository if needed
|
|
984
|
-
const absolutePath = path.resolve(local_path);
|
|
985
|
-
try {
|
|
986
|
-
await execAsync('git status', { cwd: absolutePath });
|
|
987
|
-
console.error('Directory is already a git repository');
|
|
988
|
-
} catch {
|
|
989
|
-
console.error('Initializing git repository...');
|
|
990
|
-
await execAsync('git init', { cwd: absolutePath });
|
|
991
|
-
await execAsync('git branch -M main', { cwd: absolutePath });
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Add GitLab remote
|
|
995
|
-
const gitUrl = project.ssh_url || `git@git.mlgym.io:${project.namespace}/${project.name}.git`;
|
|
996
|
-
try {
|
|
997
|
-
await execAsync('git remote remove mlgym', { cwd: absolutePath });
|
|
998
|
-
} catch {}
|
|
999
|
-
|
|
1000
|
-
await execAsync(`git remote add mlgym ${gitUrl}`, { cwd: absolutePath });
|
|
1001
|
-
|
|
1002
|
-
// Create initial commit and push (like CLI does)
|
|
1003
|
-
const gitSteps = [];
|
|
1004
|
-
|
|
1005
|
-
try {
|
|
1006
|
-
// Check if there are any files to commit
|
|
1007
|
-
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: absolutePath });
|
|
1008
|
-
|
|
1009
|
-
if (statusOutput.trim()) {
|
|
1010
|
-
// There are uncommitted changes
|
|
1011
|
-
gitSteps.push('Adding files to git');
|
|
1012
|
-
await execAsync('git add .', { cwd: absolutePath });
|
|
1013
|
-
|
|
1014
|
-
gitSteps.push('Creating initial commit');
|
|
1015
|
-
await execAsync('git commit -m "Initial MLGym deployment"', { cwd: absolutePath });
|
|
1016
|
-
} else {
|
|
1017
|
-
// Check if there are any commits at all
|
|
1018
|
-
try {
|
|
1019
|
-
await execAsync('git rev-parse HEAD', { cwd: absolutePath });
|
|
1020
|
-
gitSteps.push('Repository already has commits');
|
|
1021
|
-
} catch {
|
|
1022
|
-
// No commits yet, create an initial one
|
|
1023
|
-
gitSteps.push('Creating README for initial commit');
|
|
1024
|
-
const readmePath = path.join(absolutePath, 'README.md');
|
|
1025
|
-
if (!await fs.access(readmePath).then(() => true).catch(() => false)) {
|
|
1026
|
-
await fs.writeFile(readmePath, `# ${name}\n\n${description}\n\nDeployed with MLGym\n`);
|
|
1027
|
-
}
|
|
1028
|
-
await execAsync('git add .', { cwd: absolutePath });
|
|
1029
|
-
await execAsync('git commit -m "Initial MLGym deployment"', { cwd: absolutePath });
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Push to trigger webhook and deployment
|
|
1034
|
-
gitSteps.push('Pushing to GitLab to trigger deployment');
|
|
1035
|
-
await execAsync('git push -u mlgym main', { cwd: absolutePath });
|
|
1036
|
-
|
|
1037
|
-
} catch (pushError) {
|
|
1038
|
-
console.error('Git operations warning:', pushError.message);
|
|
1039
|
-
gitSteps.push(`Warning: ${pushError.message}`);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Monitor deployment if enabled (like CLI does)
|
|
1043
|
-
let deploymentStatus = null;
|
|
1044
|
-
if (enable_deployment) {
|
|
1045
|
-
console.error('Monitoring deployment status...');
|
|
1046
|
-
|
|
1047
|
-
// Poll for deployment status (up to 60 seconds)
|
|
1048
|
-
for (let i = 0; i < 12; i++) {
|
|
1049
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
|
|
1050
|
-
|
|
1051
|
-
const statusResult = await apiRequest('GET', `/api/v1/projects/${project.id}/deployment`, null, true);
|
|
1052
|
-
if (statusResult.success && statusResult.data) {
|
|
1053
|
-
const status = statusResult.data.status;
|
|
1054
|
-
console.error(`Deployment status: ${status}`);
|
|
1055
|
-
|
|
1056
|
-
if (status === 'deployed' || status === 'running') {
|
|
1057
|
-
deploymentStatus = {
|
|
1058
|
-
status: 'deployed',
|
|
1059
|
-
url: statusResult.data.url || `https://${hostname}.ezb.net`,
|
|
1060
|
-
message: 'Application successfully deployed'
|
|
1061
|
-
};
|
|
1062
|
-
break;
|
|
1063
|
-
} else if (status === 'failed') {
|
|
1064
|
-
deploymentStatus = {
|
|
1065
|
-
status: 'failed',
|
|
1066
|
-
message: 'Deployment failed. Check Coolify logs for details.'
|
|
1067
|
-
};
|
|
1068
|
-
break;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
if (!deploymentStatus) {
|
|
1074
|
-
deploymentStatus = {
|
|
1075
|
-
status: 'pending',
|
|
1076
|
-
message: 'Deployment is still in progress. Check status later.',
|
|
1077
|
-
expected_url: `https://${hostname}.ezb.net`
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
return {
|
|
1083
|
-
content: [{
|
|
1084
|
-
type: 'text',
|
|
1085
|
-
text: JSON.stringify({
|
|
1086
|
-
status: 'success',
|
|
1087
|
-
message: 'Project created and pushed successfully',
|
|
1088
|
-
project: {
|
|
1089
|
-
id: project.id,
|
|
1090
|
-
name: project.name,
|
|
1091
|
-
description: project.description,
|
|
1092
|
-
git_url: gitUrl,
|
|
1093
|
-
deployment_enabled: enable_deployment,
|
|
1094
|
-
deployment_url: hostname ? `https://${hostname}.ezb.net` : null
|
|
1095
|
-
},
|
|
1096
|
-
git_operations: gitSteps,
|
|
1097
|
-
deployment: deploymentStatus,
|
|
1098
|
-
next_steps: enable_deployment && deploymentStatus?.status === 'deployed' ? [
|
|
1099
|
-
`✅ Your application is live at: ${deploymentStatus.url}`,
|
|
1100
|
-
'Future updates: git push mlgym main'
|
|
1101
|
-
] : [
|
|
1102
|
-
'To update: git push mlgym main',
|
|
1103
|
-
enable_deployment ? 'Deployment will trigger automatically' : null
|
|
1104
|
-
].filter(Boolean)
|
|
1105
|
-
}, null, 2)
|
|
1106
|
-
}]
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// Create the MCP server
|
|
1111
|
-
const server = new Server(
|
|
1112
|
-
{
|
|
1113
|
-
name: 'mlgym-deploy',
|
|
1114
|
-
version: CURRENT_VERSION,
|
|
1115
|
-
},
|
|
1116
|
-
{
|
|
1117
|
-
capabilities: {
|
|
1118
|
-
tools: {},
|
|
1119
|
-
},
|
|
1120
|
-
}
|
|
1121
|
-
);
|
|
1122
|
-
|
|
1123
|
-
// Handle tool listing
|
|
1124
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1125
|
-
return {
|
|
1126
|
-
tools: [
|
|
1127
|
-
{
|
|
1128
|
-
name: 'mlgym_auth_status',
|
|
1129
|
-
description: 'ALWAYS CALL THIS FIRST! Check authentication status before any other operation.',
|
|
1130
|
-
inputSchema: {
|
|
1131
|
-
type: 'object',
|
|
1132
|
-
properties: {}
|
|
1133
|
-
}
|
|
1134
|
-
},
|
|
1135
|
-
{
|
|
1136
|
-
name: 'mlgym_authenticate',
|
|
1137
|
-
description: 'PHASE 1: Authentication ONLY. Get email, password, and existing account status in ONE interaction. Never ask for project details here!',
|
|
1138
|
-
inputSchema: {
|
|
1139
|
-
type: 'object',
|
|
1140
|
-
properties: {
|
|
1141
|
-
email: {
|
|
1142
|
-
type: 'string',
|
|
1143
|
-
description: 'Email address',
|
|
1144
|
-
pattern: '^[^@]+@[^@]+\\.[^@]+$'
|
|
1145
|
-
},
|
|
1146
|
-
password: {
|
|
1147
|
-
type: 'string',
|
|
1148
|
-
description: 'Password (min 8 characters)',
|
|
1149
|
-
minLength: 8
|
|
1150
|
-
},
|
|
1151
|
-
create_if_not_exists: {
|
|
1152
|
-
type: 'boolean',
|
|
1153
|
-
description: 'If true and login fails, attempt to create new account',
|
|
1154
|
-
default: false
|
|
1155
|
-
},
|
|
1156
|
-
full_name: {
|
|
1157
|
-
type: 'string',
|
|
1158
|
-
description: 'Full name (required only for new account creation)',
|
|
1159
|
-
minLength: 2
|
|
1160
|
-
},
|
|
1161
|
-
accept_terms: {
|
|
1162
|
-
type: 'boolean',
|
|
1163
|
-
description: 'Accept terms and conditions (required only for new account creation)',
|
|
1164
|
-
default: false
|
|
1165
|
-
}
|
|
1166
|
-
},
|
|
1167
|
-
required: ['email', 'password']
|
|
1168
|
-
}
|
|
1169
|
-
},
|
|
1170
|
-
{
|
|
1171
|
-
name: 'mlgym_project_analyze',
|
|
1172
|
-
description: 'PHASE 2: Analyze project to detect type, framework, and configuration. Call BEFORE creating project.',
|
|
1173
|
-
inputSchema: {
|
|
1174
|
-
type: 'object',
|
|
1175
|
-
properties: {
|
|
1176
|
-
local_path: {
|
|
1177
|
-
type: 'string',
|
|
1178
|
-
description: 'Local directory path (defaults to current directory)',
|
|
1179
|
-
default: '.'
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
name: 'mlgym_project_status',
|
|
1186
|
-
description: 'PHASE 2: Check if MLGym project exists in current directory.',
|
|
1187
|
-
inputSchema: {
|
|
1188
|
-
type: 'object',
|
|
1189
|
-
properties: {
|
|
1190
|
-
local_path: {
|
|
1191
|
-
type: 'string',
|
|
1192
|
-
description: 'Local directory path (defaults to current directory)',
|
|
1193
|
-
default: '.'
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
},
|
|
1198
|
-
{
|
|
1199
|
-
name: 'mlgym_project_init',
|
|
1200
|
-
description: 'PHASE 2: Create project ONLY after checking project status. Never ask for email/password here - only project details!',
|
|
1201
|
-
inputSchema: {
|
|
1202
|
-
type: 'object',
|
|
1203
|
-
properties: {
|
|
1204
|
-
name: {
|
|
1205
|
-
type: 'string',
|
|
1206
|
-
description: 'Project name (lowercase alphanumeric with hyphens)',
|
|
1207
|
-
pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$',
|
|
1208
|
-
minLength: 3
|
|
1209
|
-
},
|
|
1210
|
-
description: {
|
|
1211
|
-
type: 'string',
|
|
1212
|
-
description: 'Project description',
|
|
1213
|
-
minLength: 10
|
|
1214
|
-
},
|
|
1215
|
-
enable_deployment: {
|
|
1216
|
-
type: 'boolean',
|
|
1217
|
-
description: 'Enable automatic deployment via Coolify',
|
|
1218
|
-
default: true
|
|
1219
|
-
},
|
|
1220
|
-
hostname: {
|
|
1221
|
-
type: 'string',
|
|
1222
|
-
description: 'Hostname for deployment (required if deployment enabled, will be subdomain)',
|
|
1223
|
-
pattern: '^[a-z][a-z0-9-]*[a-z0-9]$',
|
|
1224
|
-
minLength: 3,
|
|
1225
|
-
maxLength: 63
|
|
1226
|
-
},
|
|
1227
|
-
local_path: {
|
|
1228
|
-
type: 'string',
|
|
1229
|
-
description: 'Local directory path (defaults to current directory)',
|
|
1230
|
-
default: '.'
|
|
1231
|
-
}
|
|
1232
|
-
},
|
|
1233
|
-
required: ['name', 'description']
|
|
1234
|
-
}
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
name: 'mlgym_project_prepare',
|
|
1238
|
-
description: 'PHASE 2: Prepare project for deployment by generating Dockerfile and config files.',
|
|
1239
|
-
inputSchema: {
|
|
1240
|
-
type: 'object',
|
|
1241
|
-
properties: {
|
|
1242
|
-
local_path: {
|
|
1243
|
-
type: 'string',
|
|
1244
|
-
description: 'Local directory path (defaults to current directory)',
|
|
1245
|
-
default: '.'
|
|
1246
|
-
},
|
|
1247
|
-
project_type: {
|
|
1248
|
-
type: 'string',
|
|
1249
|
-
description: 'Project type from analysis',
|
|
1250
|
-
enum: ['nodejs', 'python', 'static', 'go', 'unknown']
|
|
1251
|
-
},
|
|
1252
|
-
framework: {
|
|
1253
|
-
type: 'string',
|
|
1254
|
-
description: 'Framework from analysis',
|
|
1255
|
-
enum: ['nextjs', 'express', 'react', 'vue', 'flask', 'fastapi', 'html', null]
|
|
1256
|
-
},
|
|
1257
|
-
package_manager: {
|
|
1258
|
-
type: 'string',
|
|
1259
|
-
description: 'Package manager for Node.js projects',
|
|
1260
|
-
enum: ['npm', 'yarn'],
|
|
1261
|
-
default: 'npm'
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
name: 'mlgym_smart_deploy',
|
|
1268
|
-
description: 'RECOMMENDED: Smart deployment workflow that automatically analyzes, prepares, and guides you through the entire deployment process. Use this for new projects!',
|
|
1269
|
-
inputSchema: {
|
|
1270
|
-
type: 'object',
|
|
1271
|
-
properties: {
|
|
1272
|
-
local_path: {
|
|
1273
|
-
type: 'string',
|
|
1274
|
-
description: 'Local directory path (defaults to current directory)',
|
|
1275
|
-
default: '.'
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
]
|
|
1281
|
-
};
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
// Handle tool execution
|
|
1285
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1286
|
-
const { name, arguments: args } = request.params;
|
|
1287
|
-
|
|
1288
|
-
console.error(`Tool called: ${name}`);
|
|
1289
|
-
|
|
1290
|
-
try {
|
|
1291
|
-
switch (name) {
|
|
1292
|
-
case 'mlgym_auth_status':
|
|
1293
|
-
return await checkAuthStatus();
|
|
1294
|
-
|
|
1295
|
-
case 'mlgym_authenticate':
|
|
1296
|
-
return await authenticate(args);
|
|
1297
|
-
|
|
1298
|
-
case 'mlgym_project_analyze':
|
|
1299
|
-
const analysis = await analyzeProject(args.local_path);
|
|
1300
|
-
return {
|
|
1301
|
-
content: [{
|
|
1302
|
-
type: 'text',
|
|
1303
|
-
text: JSON.stringify(analysis, null, 2)
|
|
1304
|
-
}]
|
|
1305
|
-
};
|
|
1306
|
-
|
|
1307
|
-
case 'mlgym_project_status':
|
|
1308
|
-
const projectStatus = await checkExistingProject(args.local_path);
|
|
1309
|
-
return {
|
|
1310
|
-
content: [{
|
|
1311
|
-
type: 'text',
|
|
1312
|
-
text: JSON.stringify(projectStatus, null, 2)
|
|
1313
|
-
}]
|
|
1314
|
-
};
|
|
1315
|
-
|
|
1316
|
-
case 'mlgym_project_init':
|
|
1317
|
-
return await initProject(args);
|
|
1318
|
-
|
|
1319
|
-
case 'mlgym_project_prepare':
|
|
1320
|
-
const prepResult = await prepareProject(args);
|
|
1321
|
-
return {
|
|
1322
|
-
content: [{
|
|
1323
|
-
type: 'text',
|
|
1324
|
-
text: JSON.stringify(prepResult, null, 2)
|
|
1325
|
-
}]
|
|
1326
|
-
};
|
|
1327
|
-
|
|
1328
|
-
case 'mlgym_smart_deploy':
|
|
1329
|
-
return await smartDeploy(args);
|
|
1330
|
-
|
|
1331
|
-
default:
|
|
1332
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1333
|
-
}
|
|
1334
|
-
} catch (error) {
|
|
1335
|
-
console.error(`Tool execution failed:`, error);
|
|
1336
|
-
return {
|
|
1337
|
-
content: [{
|
|
1338
|
-
type: 'text',
|
|
1339
|
-
text: JSON.stringify({
|
|
1340
|
-
status: 'error',
|
|
1341
|
-
message: `Tool execution failed: ${error.message}`
|
|
1342
|
-
}, null, 2)
|
|
1343
|
-
}]
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
// Start the server
|
|
1349
|
-
async function main() {
|
|
1350
|
-
const transport = new StdioServerTransport();
|
|
1351
|
-
await server.connect(transport);
|
|
1352
|
-
console.error(`MLGym MCP Server v${CURRENT_VERSION} started`);
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
main().catch((error) => {
|
|
1356
|
-
console.error('Server error:', error);
|
|
1357
|
-
process.exit(1);
|
|
1358
|
-
});
|