mlgym-deploy 3.3.34 → 3.3.40
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/index.js +677 -20
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import crypto from 'crypto';
|
|
|
18
18
|
const execAsync = promisify(exec);
|
|
19
19
|
|
|
20
20
|
// Current version of this MCP server - INCREMENT FOR WORKFLOW FIXES
|
|
21
|
-
const CURRENT_VERSION = '3.3.
|
|
21
|
+
const CURRENT_VERSION = '3.3.40'; // Fix auth.jwt_token -> auth.token consistency bug
|
|
22
22
|
const PACKAGE_NAME = 'mlgym-deploy';
|
|
23
23
|
|
|
24
24
|
// Debug logging configuration - ENABLED BY DEFAULT
|
|
@@ -530,6 +530,11 @@ async function analyzeProject(local_path = '.') {
|
|
|
530
530
|
analysis.framework = 'nextjs';
|
|
531
531
|
analysis.build_command = packageJson.scripts?.build || 'npm run build';
|
|
532
532
|
analysis.start_command = packageJson.scripts?.start || 'npm start';
|
|
533
|
+
} else if (deps['@nestjs/core'] || deps['@nestjs/common']) {
|
|
534
|
+
// NestJS - TypeScript backend framework requiring build step
|
|
535
|
+
analysis.framework = 'nestjs';
|
|
536
|
+
analysis.build_command = packageJson.scripts?.build || 'npm run build';
|
|
537
|
+
analysis.start_command = packageJson.scripts?.['start:prod'] || 'node dist/main.js';
|
|
533
538
|
} else if (deps.express) {
|
|
534
539
|
analysis.framework = 'express';
|
|
535
540
|
analysis.start_command = packageJson.scripts?.start || 'node index.js';
|
|
@@ -613,6 +618,117 @@ async function analyzeProject(local_path = '.') {
|
|
|
613
618
|
} catch {}
|
|
614
619
|
}
|
|
615
620
|
|
|
621
|
+
// Check for Ruby project (Gemfile)
|
|
622
|
+
if (analysis.project_type === 'unknown') {
|
|
623
|
+
try {
|
|
624
|
+
await fs.access(path.join(absolutePath, 'Gemfile'));
|
|
625
|
+
analysis.project_type = 'ruby';
|
|
626
|
+
analysis.detected_files.push('Gemfile');
|
|
627
|
+
|
|
628
|
+
// Try to detect Rails vs Sinatra vs generic Ruby
|
|
629
|
+
try {
|
|
630
|
+
const gemfileContent = await fs.readFile(path.join(absolutePath, 'Gemfile'), 'utf8');
|
|
631
|
+
if (gemfileContent.includes("'rails'") || gemfileContent.includes('"rails"')) {
|
|
632
|
+
analysis.framework = 'rails';
|
|
633
|
+
analysis.start_command = 'rails server -b 0.0.0.0';
|
|
634
|
+
} else if (gemfileContent.includes("'sinatra'") || gemfileContent.includes('"sinatra"')) {
|
|
635
|
+
analysis.framework = 'sinatra';
|
|
636
|
+
analysis.start_command = 'ruby app.rb';
|
|
637
|
+
} else {
|
|
638
|
+
analysis.framework = 'ruby';
|
|
639
|
+
analysis.start_command = 'ruby app.rb';
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
analysis.framework = 'ruby';
|
|
643
|
+
}
|
|
644
|
+
} catch {}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Check for Rust project (Cargo.toml)
|
|
648
|
+
if (analysis.project_type === 'unknown') {
|
|
649
|
+
try {
|
|
650
|
+
await fs.access(path.join(absolutePath, 'Cargo.toml'));
|
|
651
|
+
analysis.project_type = 'rust';
|
|
652
|
+
analysis.detected_files.push('Cargo.toml');
|
|
653
|
+
analysis.framework = 'rust';
|
|
654
|
+
analysis.build_command = 'cargo build --release';
|
|
655
|
+
analysis.start_command = './target/release/app';
|
|
656
|
+
|
|
657
|
+
// Try to get binary name from Cargo.toml
|
|
658
|
+
try {
|
|
659
|
+
const cargoContent = await fs.readFile(path.join(absolutePath, 'Cargo.toml'), 'utf8');
|
|
660
|
+
const nameMatch = cargoContent.match(/name\s*=\s*["']([^"']+)["']/);
|
|
661
|
+
if (nameMatch) {
|
|
662
|
+
analysis.binary_name = nameMatch[1];
|
|
663
|
+
analysis.start_command = `./target/release/${nameMatch[1]}`;
|
|
664
|
+
}
|
|
665
|
+
} catch {}
|
|
666
|
+
} catch {}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Check for Java project (pom.xml for Maven)
|
|
670
|
+
if (analysis.project_type === 'unknown') {
|
|
671
|
+
try {
|
|
672
|
+
await fs.access(path.join(absolutePath, 'pom.xml'));
|
|
673
|
+
analysis.project_type = 'java';
|
|
674
|
+
analysis.detected_files.push('pom.xml');
|
|
675
|
+
analysis.framework = 'maven';
|
|
676
|
+
analysis.build_command = 'mvn clean package -DskipTests';
|
|
677
|
+
analysis.start_command = 'java -jar target/app.jar';
|
|
678
|
+
} catch {}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Check for Java project (build.gradle for Gradle)
|
|
682
|
+
if (analysis.project_type === 'unknown') {
|
|
683
|
+
try {
|
|
684
|
+
await fs.access(path.join(absolutePath, 'build.gradle'));
|
|
685
|
+
analysis.project_type = 'java';
|
|
686
|
+
analysis.detected_files.push('build.gradle');
|
|
687
|
+
analysis.framework = 'gradle';
|
|
688
|
+
analysis.build_command = './gradlew build -x test';
|
|
689
|
+
analysis.start_command = 'java -jar build/libs/app.jar';
|
|
690
|
+
} catch {}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Check for .NET project (*.csproj)
|
|
694
|
+
if (analysis.project_type === 'unknown') {
|
|
695
|
+
try {
|
|
696
|
+
const files = await fs.readdir(absolutePath);
|
|
697
|
+
const csprojFile = files.find(f => f.endsWith('.csproj'));
|
|
698
|
+
if (csprojFile) {
|
|
699
|
+
analysis.project_type = 'dotnet';
|
|
700
|
+
analysis.detected_files.push(csprojFile);
|
|
701
|
+
analysis.framework = 'aspnet';
|
|
702
|
+
analysis.build_command = 'dotnet publish -c Release -o out';
|
|
703
|
+
analysis.start_command = 'dotnet out/app.dll';
|
|
704
|
+
analysis.csproj_file = csprojFile;
|
|
705
|
+
}
|
|
706
|
+
} catch {}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Check for Elixir project (mix.exs)
|
|
710
|
+
if (analysis.project_type === 'unknown') {
|
|
711
|
+
try {
|
|
712
|
+
await fs.access(path.join(absolutePath, 'mix.exs'));
|
|
713
|
+
analysis.project_type = 'elixir';
|
|
714
|
+
analysis.detected_files.push('mix.exs');
|
|
715
|
+
|
|
716
|
+
// Check for Phoenix framework
|
|
717
|
+
try {
|
|
718
|
+
const mixContent = await fs.readFile(path.join(absolutePath, 'mix.exs'), 'utf8');
|
|
719
|
+
if (mixContent.includes(':phoenix')) {
|
|
720
|
+
analysis.framework = 'phoenix';
|
|
721
|
+
analysis.start_command = 'mix phx.server';
|
|
722
|
+
} else {
|
|
723
|
+
analysis.framework = 'elixir';
|
|
724
|
+
analysis.start_command = 'mix run --no-halt';
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
analysis.framework = 'elixir';
|
|
728
|
+
}
|
|
729
|
+
} catch {}
|
|
730
|
+
}
|
|
731
|
+
|
|
616
732
|
// Check for Scala/sbt project
|
|
617
733
|
if (analysis.project_type === 'unknown') {
|
|
618
734
|
try {
|
|
@@ -706,6 +822,24 @@ COPY --from=builder /app/package.json ./
|
|
|
706
822
|
COPY --from=builder /app/public ./public
|
|
707
823
|
EXPOSE 3000
|
|
708
824
|
CMD ["${packageManager}", "start"]`;
|
|
825
|
+
} else if (framework === 'nestjs') {
|
|
826
|
+
// NestJS - TypeScript backend framework requiring build step
|
|
827
|
+
dockerfile = `# Build stage
|
|
828
|
+
FROM node:18-alpine AS builder
|
|
829
|
+
WORKDIR /app
|
|
830
|
+
COPY package*.json ./
|
|
831
|
+
RUN ${packageManager} ${packageManager === 'npm' ? 'ci' : 'install --frozen-lockfile'}
|
|
832
|
+
COPY . .
|
|
833
|
+
RUN ${packageManager} run build
|
|
834
|
+
|
|
835
|
+
# Production stage
|
|
836
|
+
FROM node:18-alpine
|
|
837
|
+
WORKDIR /app
|
|
838
|
+
COPY --from=builder /app/dist ./dist
|
|
839
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
840
|
+
COPY --from=builder /app/package.json ./
|
|
841
|
+
EXPOSE 3000
|
|
842
|
+
CMD ["node", "dist/main.js"]`;
|
|
709
843
|
} else if (framework === 'express') {
|
|
710
844
|
dockerfile = `FROM node:18-alpine
|
|
711
845
|
WORKDIR /app
|
|
@@ -766,33 +900,222 @@ EXPOSE 8000
|
|
|
766
900
|
CMD ["python", "main.py"]`;
|
|
767
901
|
}
|
|
768
902
|
} else if (projectType === 'php') {
|
|
769
|
-
|
|
903
|
+
if (framework === 'composer' || framework === 'laravel') {
|
|
904
|
+
// Laravel/Composer PHP project
|
|
905
|
+
dockerfile = `FROM php:8.2-apache
|
|
906
|
+
|
|
907
|
+
# Install PHP extensions and Composer
|
|
908
|
+
RUN apt-get update && apt-get install -y \\
|
|
909
|
+
libpng-dev libjpeg-dev libfreetype6-dev libzip-dev unzip git \\
|
|
910
|
+
&& docker-php-ext-configure gd --with-freetype --with-jpeg \\
|
|
911
|
+
&& docker-php-ext-install gd pdo pdo_mysql zip \\
|
|
912
|
+
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \\
|
|
913
|
+
&& a2enmod rewrite \\
|
|
914
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
915
|
+
|
|
916
|
+
# Set document root to Laravel's public folder
|
|
917
|
+
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
|
|
918
|
+
RUN sed -ri -e 's!/var/www/html!\${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
|
|
919
|
+
RUN sed -ri -e 's!/var/www/!\${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
|
920
|
+
|
|
921
|
+
WORKDIR /var/www/html
|
|
922
|
+
COPY . .
|
|
923
|
+
|
|
924
|
+
# Install dependencies and set permissions
|
|
925
|
+
RUN composer install --no-dev --optimize-autoloader --no-interaction \\
|
|
926
|
+
&& chown -R www-data:www-data /var/www/html \\
|
|
927
|
+
&& chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null || true
|
|
928
|
+
|
|
929
|
+
EXPOSE 80
|
|
930
|
+
CMD ["apache2-foreground"]`;
|
|
931
|
+
} else {
|
|
932
|
+
// Simple PHP project
|
|
933
|
+
dockerfile = `FROM php:8.2-apache
|
|
770
934
|
WORKDIR /var/www/html
|
|
771
935
|
COPY . .
|
|
772
936
|
RUN chown -R www-data:www-data /var/www/html
|
|
773
937
|
EXPOSE 80
|
|
774
938
|
CMD ["apache2-foreground"]`;
|
|
939
|
+
}
|
|
775
940
|
} else if (projectType === 'static') {
|
|
776
941
|
dockerfile = `FROM nginx:alpine
|
|
777
942
|
COPY . /usr/share/nginx/html
|
|
778
943
|
EXPOSE 80
|
|
779
944
|
CMD ["nginx", "-g", "daemon off;"]`;
|
|
780
945
|
} else if (projectType === 'go') {
|
|
946
|
+
// Go project with CGO disabled for static binary
|
|
781
947
|
dockerfile = `# Build stage
|
|
782
948
|
FROM golang:1.21-alpine AS builder
|
|
783
949
|
WORKDIR /app
|
|
784
|
-
COPY go.mod go.sum ./
|
|
785
|
-
RUN go mod download
|
|
950
|
+
COPY go.mod go.sum* ./
|
|
951
|
+
RUN go mod download 2>/dev/null || true
|
|
786
952
|
COPY . .
|
|
787
|
-
RUN go build -o app
|
|
953
|
+
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
|
|
788
954
|
|
|
789
955
|
# Production stage
|
|
790
956
|
FROM alpine:latest
|
|
791
957
|
RUN apk --no-cache add ca-certificates
|
|
792
|
-
WORKDIR /
|
|
958
|
+
WORKDIR /app
|
|
793
959
|
COPY --from=builder /app/app .
|
|
794
960
|
EXPOSE 8080
|
|
795
961
|
CMD ["./app"]`;
|
|
962
|
+
} else if (projectType === 'ruby') {
|
|
963
|
+
if (framework === 'rails') {
|
|
964
|
+
// Ruby on Rails
|
|
965
|
+
dockerfile = `FROM ruby:3.2-slim
|
|
966
|
+
|
|
967
|
+
# Install dependencies
|
|
968
|
+
RUN apt-get update && apt-get install -y \\
|
|
969
|
+
build-essential libpq-dev nodejs npm \\
|
|
970
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
971
|
+
|
|
972
|
+
WORKDIR /app
|
|
973
|
+
COPY Gemfile Gemfile.lock* ./
|
|
974
|
+
RUN bundle install --without development test
|
|
975
|
+
|
|
976
|
+
COPY . .
|
|
977
|
+
RUN bundle exec rake assets:precompile 2>/dev/null || true
|
|
978
|
+
|
|
979
|
+
EXPOSE 3000
|
|
980
|
+
CMD ["rails", "server", "-b", "0.0.0.0"]`;
|
|
981
|
+
} else {
|
|
982
|
+
// Sinatra or generic Ruby
|
|
983
|
+
dockerfile = `FROM ruby:3.2-slim
|
|
984
|
+
|
|
985
|
+
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
|
986
|
+
|
|
987
|
+
WORKDIR /app
|
|
988
|
+
COPY Gemfile Gemfile.lock* ./
|
|
989
|
+
RUN bundle install
|
|
990
|
+
|
|
991
|
+
COPY . .
|
|
992
|
+
EXPOSE 4567
|
|
993
|
+
CMD ["ruby", "app.rb"]`;
|
|
994
|
+
}
|
|
995
|
+
} else if (projectType === 'rust') {
|
|
996
|
+
dockerfile = `# Build stage
|
|
997
|
+
FROM rust:1.75-slim AS builder
|
|
998
|
+
WORKDIR /app
|
|
999
|
+
COPY Cargo.toml Cargo.lock* ./
|
|
1000
|
+
COPY src ./src
|
|
1001
|
+
RUN cargo build --release
|
|
1002
|
+
|
|
1003
|
+
# Production stage
|
|
1004
|
+
FROM debian:bookworm-slim
|
|
1005
|
+
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
1006
|
+
WORKDIR /app
|
|
1007
|
+
COPY --from=builder /app/target/release/* ./
|
|
1008
|
+
EXPOSE 8080
|
|
1009
|
+
CMD ["./app"]`;
|
|
1010
|
+
} else if (projectType === 'java') {
|
|
1011
|
+
if (framework === 'maven') {
|
|
1012
|
+
dockerfile = `# Build stage
|
|
1013
|
+
FROM maven:3.9-eclipse-temurin-17 AS builder
|
|
1014
|
+
WORKDIR /app
|
|
1015
|
+
COPY pom.xml .
|
|
1016
|
+
RUN mvn dependency:go-offline -B
|
|
1017
|
+
COPY src ./src
|
|
1018
|
+
RUN mvn clean package -DskipTests -B
|
|
1019
|
+
|
|
1020
|
+
# Production stage
|
|
1021
|
+
FROM eclipse-temurin:17-jre-jammy
|
|
1022
|
+
WORKDIR /app
|
|
1023
|
+
COPY --from=builder /app/target/*.jar app.jar
|
|
1024
|
+
EXPOSE 8080
|
|
1025
|
+
CMD ["java", "-jar", "app.jar"]`;
|
|
1026
|
+
} else if (framework === 'gradle') {
|
|
1027
|
+
dockerfile = `# Build stage
|
|
1028
|
+
FROM gradle:8-jdk17 AS builder
|
|
1029
|
+
WORKDIR /app
|
|
1030
|
+
COPY build.gradle settings.gradle* gradle* ./
|
|
1031
|
+
COPY gradle ./gradle 2>/dev/null || true
|
|
1032
|
+
COPY src ./src
|
|
1033
|
+
RUN gradle build -x test --no-daemon
|
|
1034
|
+
|
|
1035
|
+
# Production stage
|
|
1036
|
+
FROM eclipse-temurin:17-jre-jammy
|
|
1037
|
+
WORKDIR /app
|
|
1038
|
+
COPY --from=builder /app/build/libs/*.jar app.jar
|
|
1039
|
+
EXPOSE 8080
|
|
1040
|
+
CMD ["java", "-jar", "app.jar"]`;
|
|
1041
|
+
} else {
|
|
1042
|
+
// Generic Java with Maven fallback
|
|
1043
|
+
dockerfile = `# Build stage
|
|
1044
|
+
FROM maven:3.9-eclipse-temurin-17 AS builder
|
|
1045
|
+
WORKDIR /app
|
|
1046
|
+
COPY pom.xml .
|
|
1047
|
+
COPY src ./src
|
|
1048
|
+
RUN mvn clean package -DskipTests -B
|
|
1049
|
+
|
|
1050
|
+
# Production stage
|
|
1051
|
+
FROM eclipse-temurin:17-jre-jammy
|
|
1052
|
+
WORKDIR /app
|
|
1053
|
+
COPY --from=builder /app/target/*.jar app.jar
|
|
1054
|
+
EXPOSE 8080
|
|
1055
|
+
CMD ["java", "-jar", "app.jar"]`;
|
|
1056
|
+
}
|
|
1057
|
+
} else if (projectType === 'dotnet') {
|
|
1058
|
+
dockerfile = `# Build stage
|
|
1059
|
+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder
|
|
1060
|
+
WORKDIR /app
|
|
1061
|
+
COPY *.csproj ./
|
|
1062
|
+
RUN dotnet restore
|
|
1063
|
+
COPY . .
|
|
1064
|
+
RUN dotnet publish -c Release -o out
|
|
1065
|
+
|
|
1066
|
+
# Production stage
|
|
1067
|
+
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
|
1068
|
+
WORKDIR /app
|
|
1069
|
+
COPY --from=builder /app/out .
|
|
1070
|
+
EXPOSE 8080
|
|
1071
|
+
ENV ASPNETCORE_URLS=http://+:8080
|
|
1072
|
+
ENTRYPOINT ["dotnet", "app.dll"]`;
|
|
1073
|
+
} else if (projectType === 'elixir') {
|
|
1074
|
+
if (framework === 'phoenix') {
|
|
1075
|
+
dockerfile = `# Build stage
|
|
1076
|
+
FROM elixir:1.16-alpine AS builder
|
|
1077
|
+
RUN apk add --no-cache build-base git nodejs npm
|
|
1078
|
+
WORKDIR /app
|
|
1079
|
+
|
|
1080
|
+
# Install hex and rebar
|
|
1081
|
+
RUN mix local.hex --force && mix local.rebar --force
|
|
1082
|
+
|
|
1083
|
+
# Install dependencies
|
|
1084
|
+
COPY mix.exs mix.lock ./
|
|
1085
|
+
RUN mix deps.get --only prod
|
|
1086
|
+
RUN MIX_ENV=prod mix deps.compile
|
|
1087
|
+
|
|
1088
|
+
# Build assets and release
|
|
1089
|
+
COPY . .
|
|
1090
|
+
RUN cd assets && npm install && npm run deploy 2>/dev/null || true
|
|
1091
|
+
RUN MIX_ENV=prod mix phx.digest 2>/dev/null || true
|
|
1092
|
+
RUN MIX_ENV=prod mix release
|
|
1093
|
+
|
|
1094
|
+
# Production stage
|
|
1095
|
+
FROM alpine:3.19
|
|
1096
|
+
RUN apk add --no-cache libstdc++ openssl ncurses-libs
|
|
1097
|
+
WORKDIR /app
|
|
1098
|
+
COPY --from=builder /app/_build/prod/rel/app ./
|
|
1099
|
+
EXPOSE 4000
|
|
1100
|
+
ENV PHX_HOST=localhost
|
|
1101
|
+
CMD ["bin/app", "start"]`;
|
|
1102
|
+
} else {
|
|
1103
|
+
dockerfile = `FROM elixir:1.16-alpine
|
|
1104
|
+
RUN apk add --no-cache build-base
|
|
1105
|
+
WORKDIR /app
|
|
1106
|
+
|
|
1107
|
+
RUN mix local.hex --force && mix local.rebar --force
|
|
1108
|
+
|
|
1109
|
+
COPY mix.exs mix.lock ./
|
|
1110
|
+
RUN mix deps.get
|
|
1111
|
+
RUN mix deps.compile
|
|
1112
|
+
|
|
1113
|
+
COPY . .
|
|
1114
|
+
RUN mix compile
|
|
1115
|
+
|
|
1116
|
+
EXPOSE 4000
|
|
1117
|
+
CMD ["mix", "run", "--no-halt"]`;
|
|
1118
|
+
}
|
|
796
1119
|
} else if (projectType === 'scala') {
|
|
797
1120
|
dockerfile = `# Build stage
|
|
798
1121
|
FROM sbtscala/scala-sbt:eclipse-temurin-jammy-17.0.10_7_1.10.2_2.13.15 AS builder
|
|
@@ -1384,6 +1707,248 @@ function randomizeVolumeNames(content, suffix) {
|
|
|
1384
1707
|
return { content: result, randomized };
|
|
1385
1708
|
}
|
|
1386
1709
|
|
|
1710
|
+
// Known valid Docker image version ranges
|
|
1711
|
+
const VALID_IMAGE_VERSIONS = {
|
|
1712
|
+
php: { min: 5, max: 8, latest: '8.3', variants: ['apache', 'fpm', 'cli', 'alpine', 'slim'] },
|
|
1713
|
+
node: { min: 10, max: 22, latest: '20', variants: ['alpine', 'slim', 'bullseye', 'bookworm'] },
|
|
1714
|
+
python: { min: 2, max: 3, latest: '3.12', subVersionMax: { 2: 7, 3: 12 } },
|
|
1715
|
+
ruby: { min: 2, max: 3, latest: '3.3', subVersionMax: { 2: 7, 3: 3 } },
|
|
1716
|
+
golang: { min: 1, max: 1, latest: '1.22', subVersionMax: { 1: 22 } },
|
|
1717
|
+
go: { min: 1, max: 1, latest: '1.22', subVersionMax: { 1: 22 } },
|
|
1718
|
+
java: { min: 8, max: 21, latest: '21' },
|
|
1719
|
+
openjdk: { min: 8, max: 21, latest: '21' },
|
|
1720
|
+
rust: { min: 1, max: 1, latest: '1.75', subVersionMax: { 1: 75 } },
|
|
1721
|
+
perl: { min: 5, max: 5, latest: '5.38', subVersionMax: { 5: 38 } },
|
|
1722
|
+
elixir: { min: 1, max: 1, latest: '1.16', subVersionMax: { 1: 16 } },
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
// Validate Docker base image version
|
|
1726
|
+
function validateBaseImage(baseImage) {
|
|
1727
|
+
if (!baseImage) return null;
|
|
1728
|
+
|
|
1729
|
+
// Extract image name and tag
|
|
1730
|
+
const parts = baseImage.split(':');
|
|
1731
|
+
const imageName = parts[0].split('/').pop(); // Handle registry/image:tag format
|
|
1732
|
+
const tag = parts[1] || 'latest';
|
|
1733
|
+
|
|
1734
|
+
// Check if we know this image type
|
|
1735
|
+
const imageConfig = VALID_IMAGE_VERSIONS[imageName];
|
|
1736
|
+
if (!imageConfig) return null; // Unknown image, can't validate
|
|
1737
|
+
|
|
1738
|
+
// Extract version number and variant from tag (e.g., "99-apache" -> version: 99, variant: "-apache")
|
|
1739
|
+
const versionMatch = tag.match(/^(\d+)(?:\.(\d+))?(.*)$/);
|
|
1740
|
+
if (!versionMatch) return null; // Can't parse version (e.g., 'latest', 'alpine')
|
|
1741
|
+
|
|
1742
|
+
const majorVersion = parseInt(versionMatch[1], 10);
|
|
1743
|
+
const minorVersion = versionMatch[2] ? parseInt(versionMatch[2], 10) : null;
|
|
1744
|
+
const variant = versionMatch[3] || ''; // Preserve variant like "-apache", "-alpine", "-slim"
|
|
1745
|
+
|
|
1746
|
+
// Build the suggested fix preserving the variant
|
|
1747
|
+
const suggestedTag = `${imageConfig.latest}${variant}`;
|
|
1748
|
+
|
|
1749
|
+
// Check if major version is in valid range
|
|
1750
|
+
if (majorVersion < imageConfig.min || majorVersion > imageConfig.max) {
|
|
1751
|
+
return {
|
|
1752
|
+
invalid: true,
|
|
1753
|
+
reason: `${imageName}:${tag} has invalid version ${majorVersion} (valid: ${imageConfig.min}-${imageConfig.max})`,
|
|
1754
|
+
suggestedFix: `${imageName}:${suggestedTag}`,
|
|
1755
|
+
autofix: 'fix_base_image'
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// Check minor version if we have subversion limits
|
|
1760
|
+
if (minorVersion !== null && imageConfig.subVersionMax && imageConfig.subVersionMax[majorVersion]) {
|
|
1761
|
+
if (minorVersion > imageConfig.subVersionMax[majorVersion]) {
|
|
1762
|
+
return {
|
|
1763
|
+
invalid: true,
|
|
1764
|
+
reason: `${imageName}:${tag} has invalid minor version (${majorVersion}.${minorVersion} doesn't exist)`,
|
|
1765
|
+
suggestedFix: `${imageName}:${suggestedTag}`,
|
|
1766
|
+
autofix: 'fix_base_image'
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
return null; // Valid
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Native file scanner - replaces glob dependency
|
|
1776
|
+
* @param {string} baseDir - Base directory to scan
|
|
1777
|
+
* @param {string[]} extensions - File extensions to match (e.g., ['.py', '.js'])
|
|
1778
|
+
* @param {string[]} ignoreDirs - Directory names to ignore (e.g., ['node_modules', 'venv'])
|
|
1779
|
+
* @returns {string[]} Array of relative file paths
|
|
1780
|
+
*/
|
|
1781
|
+
async function scanFilesNative(baseDir, extensions, ignoreDirs = []) {
|
|
1782
|
+
const results = [];
|
|
1783
|
+
const ignoreSet = new Set(ignoreDirs);
|
|
1784
|
+
|
|
1785
|
+
async function scanDir(dir, relativePath = '') {
|
|
1786
|
+
try {
|
|
1787
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1788
|
+
for (const entry of entries) {
|
|
1789
|
+
const fullPath = path.join(dir, entry.name);
|
|
1790
|
+
const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
1791
|
+
|
|
1792
|
+
if (entry.isDirectory()) {
|
|
1793
|
+
// Skip ignored directories
|
|
1794
|
+
if (!ignoreSet.has(entry.name) && !entry.name.startsWith('.')) {
|
|
1795
|
+
await scanDir(fullPath, relPath);
|
|
1796
|
+
}
|
|
1797
|
+
} else if (entry.isFile()) {
|
|
1798
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
1799
|
+
if (extensions.includes(ext)) {
|
|
1800
|
+
results.push(relPath);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
} catch (err) {
|
|
1805
|
+
// Ignore permission errors and other issues
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
await scanDir(baseDir);
|
|
1810
|
+
return results;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Detect port mismatch between application code and Dockerfile EXPOSE
|
|
1815
|
+
* Returns null if no mismatch, or object with details if mismatch found
|
|
1816
|
+
*/
|
|
1817
|
+
async function detectPortMismatch(projectPath, exposedPort) {
|
|
1818
|
+
if (!exposedPort) return null;
|
|
1819
|
+
|
|
1820
|
+
const exposedPortNum = parseInt(exposedPort, 10);
|
|
1821
|
+
const appPorts = [];
|
|
1822
|
+
|
|
1823
|
+
try {
|
|
1824
|
+
// Check Python files for Flask/FastAPI port
|
|
1825
|
+
const pythonFiles = await scanFilesNative(projectPath, ['.py'], ['venv', '__pycache__', '.venv']);
|
|
1826
|
+
for (const file of pythonFiles.slice(0, 10)) {
|
|
1827
|
+
const content = await fs.readFile(path.join(projectPath, file), 'utf-8').catch(() => '');
|
|
1828
|
+
// Flask: app.run(port=5000), app.run(host='0.0.0.0', port=5000)
|
|
1829
|
+
const flaskMatch = content.match(/\.run\s*\([^)]*port\s*=\s*(\d+)/);
|
|
1830
|
+
if (flaskMatch) appPorts.push({ port: parseInt(flaskMatch[1], 10), file, type: 'Flask' });
|
|
1831
|
+
// FastAPI/Uvicorn: uvicorn.run(..., port=8000)
|
|
1832
|
+
const uvicornMatch = content.match(/uvicorn\.run\s*\([^)]*port\s*=\s*(\d+)/);
|
|
1833
|
+
if (uvicornMatch) appPorts.push({ port: parseInt(uvicornMatch[1], 10), file, type: 'Uvicorn' });
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Check Node.js files for Express/HTTP port
|
|
1837
|
+
const jsFiles = await scanFilesNative(projectPath, ['.js', '.ts'], ['node_modules', 'dist', 'build']);
|
|
1838
|
+
for (const file of jsFiles.slice(0, 10)) {
|
|
1839
|
+
const content = await fs.readFile(path.join(projectPath, file), 'utf-8').catch(() => '');
|
|
1840
|
+
// Express: app.listen(3000), server.listen(3000)
|
|
1841
|
+
const listenMatch = content.match(/\.listen\s*\(\s*(\d+)/);
|
|
1842
|
+
if (listenMatch) appPorts.push({ port: parseInt(listenMatch[1], 10), file, type: 'Node.js' });
|
|
1843
|
+
// PORT env: process.env.PORT || 3000
|
|
1844
|
+
const portEnvMatch = content.match(/process\.env\.PORT\s*\|\|\s*(\d+)/);
|
|
1845
|
+
if (portEnvMatch) appPorts.push({ port: parseInt(portEnvMatch[1], 10), file, type: 'Node.js (env fallback)' });
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Check Ruby files for Sinatra/Rails port
|
|
1849
|
+
const rubyFiles = await scanFilesNative(projectPath, ['.rb'], ['vendor', 'bundle']);
|
|
1850
|
+
for (const file of rubyFiles.slice(0, 10)) {
|
|
1851
|
+
const content = await fs.readFile(path.join(projectPath, file), 'utf-8').catch(() => '');
|
|
1852
|
+
// Sinatra: set :port, 4567
|
|
1853
|
+
const sinatraMatch = content.match(/set\s+:port\s*,\s*(\d+)/);
|
|
1854
|
+
if (sinatraMatch) appPorts.push({ port: parseInt(sinatraMatch[1], 10), file, type: 'Sinatra' });
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Check Go files for HTTP port
|
|
1858
|
+
const goFiles = await scanFilesNative(projectPath, ['.go'], ['vendor']);
|
|
1859
|
+
for (const file of goFiles.slice(0, 10)) {
|
|
1860
|
+
const content = await fs.readFile(path.join(projectPath, file), 'utf-8').catch(() => '');
|
|
1861
|
+
// Go: http.ListenAndServe(":8080", ...)
|
|
1862
|
+
const goMatch = content.match(/ListenAndServe\s*\(\s*["':](\d+)/);
|
|
1863
|
+
if (goMatch) appPorts.push({ port: parseInt(goMatch[1], 10), file, type: 'Go HTTP' });
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Find mismatches
|
|
1867
|
+
const mismatches = appPorts.filter(p => p.port !== exposedPortNum);
|
|
1868
|
+
if (mismatches.length > 0) {
|
|
1869
|
+
const mismatch = mismatches[0];
|
|
1870
|
+
return {
|
|
1871
|
+
mismatch: true,
|
|
1872
|
+
exposedPort: exposedPortNum,
|
|
1873
|
+
appPort: mismatch.port,
|
|
1874
|
+
file: mismatch.file,
|
|
1875
|
+
type: mismatch.type,
|
|
1876
|
+
suggestedFix: mismatch.port,
|
|
1877
|
+
autofix: 'fix_expose_port'
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
log.warning(`MCP >>> Port detection failed: ${err.message}`);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Detect missing dependencies in Node.js projects
|
|
1889
|
+
* Compares require/import statements with package.json dependencies
|
|
1890
|
+
*/
|
|
1891
|
+
async function detectMissingNodeDependencies(projectPath) {
|
|
1892
|
+
try {
|
|
1893
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
1894
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8').catch(() => null);
|
|
1895
|
+
if (!packageJsonContent) return null;
|
|
1896
|
+
|
|
1897
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
1898
|
+
const declaredDeps = new Set([
|
|
1899
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
1900
|
+
...Object.keys(packageJson.devDependencies || {})
|
|
1901
|
+
]);
|
|
1902
|
+
|
|
1903
|
+
// Scan JS/TS files for imports
|
|
1904
|
+
const usedDeps = new Set();
|
|
1905
|
+
const jsFiles = await scanFilesNative(projectPath, ['.js', '.ts', '.jsx', '.tsx'], ['node_modules', 'dist', 'build']);
|
|
1906
|
+
|
|
1907
|
+
for (const file of jsFiles.slice(0, 20)) {
|
|
1908
|
+
const content = await fs.readFile(path.join(projectPath, file), 'utf-8').catch(() => '');
|
|
1909
|
+
|
|
1910
|
+
// CommonJS: require('package')
|
|
1911
|
+
const requireMatches = content.matchAll(/require\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g);
|
|
1912
|
+
for (const match of requireMatches) {
|
|
1913
|
+
const pkg = match[1].split('/')[0]; // Handle scoped packages and subpaths
|
|
1914
|
+
if (!pkg.startsWith('@')) usedDeps.add(pkg);
|
|
1915
|
+
else usedDeps.add(match[1].split('/').slice(0, 2).join('/')); // @scope/package
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// ES6: import ... from 'package'
|
|
1919
|
+
const importMatches = content.matchAll(/import\s+(?:[^'"]+\s+from\s+)?['"]([^'"./][^'"]*)['"]/g);
|
|
1920
|
+
for (const match of importMatches) {
|
|
1921
|
+
const pkg = match[1].split('/')[0];
|
|
1922
|
+
if (!pkg.startsWith('@')) usedDeps.add(pkg);
|
|
1923
|
+
else usedDeps.add(match[1].split('/').slice(0, 2).join('/'));
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// Built-in Node.js modules to ignore
|
|
1928
|
+
const builtins = new Set(['fs', 'path', 'http', 'https', 'crypto', 'os', 'url', 'util', 'stream', 'events', 'child_process', 'cluster', 'dns', 'net', 'tls', 'zlib', 'buffer', 'querystring', 'readline', 'assert', 'module', 'process', 'timers', 'vm', 'worker_threads']);
|
|
1929
|
+
|
|
1930
|
+
// Find missing dependencies
|
|
1931
|
+
const missing = [];
|
|
1932
|
+
for (const dep of usedDeps) {
|
|
1933
|
+
if (!builtins.has(dep) && !declaredDeps.has(dep)) {
|
|
1934
|
+
missing.push(dep);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if (missing.length > 0) {
|
|
1939
|
+
return {
|
|
1940
|
+
missing: true,
|
|
1941
|
+
packages: missing,
|
|
1942
|
+
autofix: 'add_node_deps'
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
log.warning(`MCP >>> Dependency detection failed: ${err.message}`);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1387
1952
|
// Validate Dockerfile for Coolify compliance
|
|
1388
1953
|
function validateDockerfile(content) {
|
|
1389
1954
|
const lines = content.split('\n');
|
|
@@ -1403,13 +1968,17 @@ function validateDockerfile(content) {
|
|
|
1403
1968
|
const trimmed = lines[i].trim();
|
|
1404
1969
|
const upper = trimmed.toUpperCase();
|
|
1405
1970
|
|
|
1406
|
-
// Track base image
|
|
1971
|
+
// Track base image (use the last non-builder FROM for final image)
|
|
1407
1972
|
if (upper.startsWith('FROM ')) {
|
|
1408
|
-
|
|
1973
|
+
const fromImage = trimmed.substring(5).split(' ')[0].toLowerCase();
|
|
1409
1974
|
// Check for Elixir multi-stage build
|
|
1410
|
-
if (
|
|
1975
|
+
if (fromImage.includes('elixir') && upper.includes(' AS ')) {
|
|
1411
1976
|
isElixirMultiStage = true;
|
|
1412
1977
|
}
|
|
1978
|
+
// Only track as base image if it's not a builder stage reference
|
|
1979
|
+
if (!fromImage.includes('builder') && !fromImage.includes('build')) {
|
|
1980
|
+
baseImage = fromImage;
|
|
1981
|
+
}
|
|
1413
1982
|
}
|
|
1414
1983
|
|
|
1415
1984
|
if (upper.startsWith('EXPOSE')) {
|
|
@@ -1438,6 +2007,17 @@ function validateDockerfile(content) {
|
|
|
1438
2007
|
}
|
|
1439
2008
|
}
|
|
1440
2009
|
|
|
2010
|
+
// Validate base image version
|
|
2011
|
+
const baseImageValidation = validateBaseImage(baseImage);
|
|
2012
|
+
if (baseImageValidation && baseImageValidation.invalid) {
|
|
2013
|
+
issues.push({
|
|
2014
|
+
issue: `Invalid Docker base image: ${baseImageValidation.reason}`,
|
|
2015
|
+
fix: `Change FROM to use: ${baseImageValidation.suggestedFix}`,
|
|
2016
|
+
autofix: baseImageValidation.autofix,
|
|
2017
|
+
suggestedImage: baseImageValidation.suggestedFix
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1441
2021
|
if (!hasExpose) {
|
|
1442
2022
|
issues.push({
|
|
1443
2023
|
issue: 'Dockerfile does not have an EXPOSE directive',
|
|
@@ -1527,6 +2107,41 @@ function autoFixDockerfile(content, issues) {
|
|
|
1527
2107
|
}
|
|
1528
2108
|
}
|
|
1529
2109
|
}
|
|
2110
|
+
|
|
2111
|
+
if (issue.autofix === 'fix_base_image') {
|
|
2112
|
+
// Replace invalid base image with valid one
|
|
2113
|
+
const suggestedImage = issue.suggestedImage || issue.suggestedFix;
|
|
2114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2115
|
+
const trimmed = lines[i].trim().toUpperCase();
|
|
2116
|
+
if (trimmed.startsWith('FROM ')) {
|
|
2117
|
+
const originalLine = lines[i];
|
|
2118
|
+
const indent = originalLine.match(/^(\s*)/)[1];
|
|
2119
|
+
const fromMatch = originalLine.match(/^(\s*FROM\s+)([^\s]+)(\s+AS\s+\S+)?(.*)$/i);
|
|
2120
|
+
if (fromMatch && suggestedImage) {
|
|
2121
|
+
const asClause = fromMatch[3] || '';
|
|
2122
|
+
const rest = fromMatch[4] || '';
|
|
2123
|
+
lines[i] = `${indent}FROM ${suggestedImage}${asClause}${rest}`;
|
|
2124
|
+
modified = true;
|
|
2125
|
+
log.success(`MCP >>> Auto-fixed Dockerfile: replaced invalid base image with ${suggestedImage}`);
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
if (issue.autofix === 'fix_expose_port') {
|
|
2133
|
+
// Replace existing EXPOSE with correct port
|
|
2134
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2135
|
+
const trimmed = lines[i].trim().toUpperCase();
|
|
2136
|
+
if (trimmed.startsWith('EXPOSE')) {
|
|
2137
|
+
const indent = lines[i].match(/^(\s*)/)[1];
|
|
2138
|
+
lines[i] = `${indent}EXPOSE ${issue.suggestedFix}`;
|
|
2139
|
+
modified = true;
|
|
2140
|
+
log.success(`MCP >>> Auto-fixed Dockerfile: changed EXPOSE to ${issue.suggestedFix}`);
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1530
2145
|
}
|
|
1531
2146
|
|
|
1532
2147
|
return { content: lines.join('\n'), modified };
|
|
@@ -1785,7 +2400,7 @@ async function initProject(args) {
|
|
|
1785
2400
|
projectData.docker_compose_content = composeContent;
|
|
1786
2401
|
log.info(`MCP >>> [initProject] Sending docker-compose content: ${composeContent.length} bytes`);
|
|
1787
2402
|
} else {
|
|
1788
|
-
log.
|
|
2403
|
+
log.warning('MCP >>> [initProject] docker-compose strategy but no compose file found!');
|
|
1789
2404
|
}
|
|
1790
2405
|
}
|
|
1791
2406
|
|
|
@@ -2641,6 +3256,48 @@ async function deployProject(args) {
|
|
|
2641
3256
|
} else {
|
|
2642
3257
|
log.success('MCP >>> Dockerfile is Coolify compliant');
|
|
2643
3258
|
}
|
|
3259
|
+
|
|
3260
|
+
// Check for port mismatch between app code and Dockerfile
|
|
3261
|
+
const currentContent = fsSync.readFileSync(dockerfilePath, 'utf8');
|
|
3262
|
+
const currentValidation = validateDockerfile(currentContent);
|
|
3263
|
+
if (currentValidation.exposedPort) {
|
|
3264
|
+
const portMismatch = await detectPortMismatch(local_path, currentValidation.exposedPort);
|
|
3265
|
+
if (portMismatch && portMismatch.mismatch) {
|
|
3266
|
+
log.warning(`MCP >>> Port mismatch detected: App runs on ${portMismatch.appPort} but EXPOSE is ${portMismatch.exposedPort}`);
|
|
3267
|
+
log.info(`MCP >>> Auto-fixing: Changing EXPOSE to ${portMismatch.appPort}`);
|
|
3268
|
+
const fixResult = autoFixDockerfile(currentContent, [{
|
|
3269
|
+
autofix: 'fix_expose_port',
|
|
3270
|
+
suggestedFix: portMismatch.appPort
|
|
3271
|
+
}]);
|
|
3272
|
+
if (fixResult.modified) {
|
|
3273
|
+
fsSync.writeFileSync(dockerfilePath, fixResult.content);
|
|
3274
|
+
log.success(`MCP >>> Fixed port mismatch: EXPOSE now ${portMismatch.appPort}`);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
// Step 4.5: Check for missing Node.js dependencies
|
|
3282
|
+
const packageJsonPath = path.join(local_path, 'package.json');
|
|
3283
|
+
if (fsSync.existsSync(packageJsonPath)) {
|
|
3284
|
+
log.info('MCP >>> Checking for missing Node.js dependencies...');
|
|
3285
|
+
const missingDeps = await detectMissingNodeDependencies(local_path);
|
|
3286
|
+
if (missingDeps && missingDeps.missing && missingDeps.packages.length > 0) {
|
|
3287
|
+
log.warning(`MCP >>> Missing dependencies detected: ${missingDeps.packages.join(', ')}`);
|
|
3288
|
+
log.info('MCP >>> Auto-fixing: Adding missing dependencies to package.json');
|
|
3289
|
+
|
|
3290
|
+
const packageJsonContent = fsSync.readFileSync(packageJsonPath, 'utf-8');
|
|
3291
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
3292
|
+
if (!packageJson.dependencies) packageJson.dependencies = {};
|
|
3293
|
+
|
|
3294
|
+
for (const pkg of missingDeps.packages) {
|
|
3295
|
+
packageJson.dependencies[pkg] = '*'; // Will be resolved to latest during npm install
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
fsSync.writeFileSync(packageJsonPath + '.backup', packageJsonContent);
|
|
3299
|
+
fsSync.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
3300
|
+
log.success(`MCP >>> Added missing dependencies: ${missingDeps.packages.join(', ')}`);
|
|
2644
3301
|
}
|
|
2645
3302
|
}
|
|
2646
3303
|
|
|
@@ -2884,11 +3541,11 @@ async function setEnvironmentVariables(args) {
|
|
|
2884
3541
|
|
|
2885
3542
|
// Ensure authentication
|
|
2886
3543
|
let auth = await loadAuth();
|
|
2887
|
-
if (!auth || !auth.
|
|
3544
|
+
if (!auth || !auth.token) {
|
|
2888
3545
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
2889
3546
|
}
|
|
2890
3547
|
|
|
2891
|
-
log.debug(`Using JWT token: ${auth.
|
|
3548
|
+
log.debug(`Using JWT token: ${auth.token.substring(0, 20)}...`);
|
|
2892
3549
|
|
|
2893
3550
|
// Determine project name
|
|
2894
3551
|
let finalProjectName = project_name;
|
|
@@ -2914,7 +3571,7 @@ async function setEnvironmentVariables(args) {
|
|
|
2914
3571
|
|
|
2915
3572
|
const projectResponse = await axios.get(backendUrl, {
|
|
2916
3573
|
headers: {
|
|
2917
|
-
'Authorization': `Bearer ${auth.
|
|
3574
|
+
'Authorization': `Bearer ${auth.token}`,
|
|
2918
3575
|
'Content-Type': 'application/json'
|
|
2919
3576
|
}
|
|
2920
3577
|
});
|
|
@@ -2986,7 +3643,7 @@ async function getApplicationUUID(projectName, auth) {
|
|
|
2986
3643
|
|
|
2987
3644
|
const projectResponse = await axios.get(backendUrl, {
|
|
2988
3645
|
headers: {
|
|
2989
|
-
'Authorization': `Bearer ${auth.
|
|
3646
|
+
'Authorization': `Bearer ${auth.token}`,
|
|
2990
3647
|
'Content-Type': 'application/json'
|
|
2991
3648
|
}
|
|
2992
3649
|
});
|
|
@@ -3007,7 +3664,7 @@ async function setHealthCheck(args) {
|
|
|
3007
3664
|
|
|
3008
3665
|
// Ensure authentication
|
|
3009
3666
|
let auth = await loadAuth();
|
|
3010
|
-
if (!auth || !auth.
|
|
3667
|
+
if (!auth || !auth.token) {
|
|
3011
3668
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
3012
3669
|
}
|
|
3013
3670
|
|
|
@@ -3072,7 +3729,7 @@ async function setDomain(args) {
|
|
|
3072
3729
|
log.info(`MCP >>> [setDomain] Setting domain for ${project_name || 'project'}`);
|
|
3073
3730
|
|
|
3074
3731
|
let auth = await loadAuth();
|
|
3075
|
-
if (!auth || !auth.
|
|
3732
|
+
if (!auth || !auth.token) {
|
|
3076
3733
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
3077
3734
|
}
|
|
3078
3735
|
|
|
@@ -3134,7 +3791,7 @@ async function setDeploymentCommands(args) {
|
|
|
3134
3791
|
log.info(`MCP >>> [setDeploymentCommands] Setting deployment commands for ${project_name || 'project'}`);
|
|
3135
3792
|
|
|
3136
3793
|
let auth = await loadAuth();
|
|
3137
|
-
if (!auth || !auth.
|
|
3794
|
+
if (!auth || !auth.token) {
|
|
3138
3795
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
3139
3796
|
}
|
|
3140
3797
|
|
|
@@ -3196,7 +3853,7 @@ async function manualDeploy(args) {
|
|
|
3196
3853
|
log.info(`MCP >>> [manualDeploy] Triggering manual deployment for ${project_name || 'project'}`);
|
|
3197
3854
|
|
|
3198
3855
|
let auth = await loadAuth();
|
|
3199
|
-
if (!auth || !auth.
|
|
3856
|
+
if (!auth || !auth.token) {
|
|
3200
3857
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
3201
3858
|
}
|
|
3202
3859
|
|
|
@@ -3264,7 +3921,7 @@ async function setOptions(args) {
|
|
|
3264
3921
|
log.info(`MCP >>> [setOptions] Setting options for ${project_name || 'project'}`);
|
|
3265
3922
|
|
|
3266
3923
|
let auth = await loadAuth();
|
|
3267
|
-
if (!auth || !auth.
|
|
3924
|
+
if (!auth || !auth.token) {
|
|
3268
3925
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
3269
3926
|
}
|
|
3270
3927
|
|
|
@@ -3326,7 +3983,7 @@ async function rollback(args) {
|
|
|
3326
3983
|
log.info(`MCP >>> [rollback] Rolling back ${project_name || 'project'}`);
|
|
3327
3984
|
|
|
3328
3985
|
let auth = await loadAuth();
|
|
3329
|
-
if (!auth || !auth.
|
|
3986
|
+
if (!auth || !auth.token) {
|
|
3330
3987
|
throw new Error('Not authenticated. Please run mlgym_deploy or mlgym_auth_login first');
|
|
3331
3988
|
}
|
|
3332
3989
|
|
package/package.json
CHANGED