@mgsoftwarebv/mg-dashboard-mcp 7.0.4 → 7.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -677,10 +677,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
677
677
  "trigger-run": "ci_cd"
678
678
  };
679
679
  async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
680
- const sql27 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
680
+ const sql28 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
681
681
  const cmd = [
682
682
  `PORT=$(docker port "${WA_CONTAINER}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
683
- `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql27}" 2>/dev/null | tr -d '[:space:]')`,
683
+ `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql28}" 2>/dev/null | tr -d '[:space:]')`,
684
684
  'echo "$PORT|$KEY"'
685
685
  ].join(" && ");
686
686
  const result = await sshExec2(conn, cmd, proxy);
@@ -701,8 +701,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
701
701
  return { port, apiKey: apiKey2 };
702
702
  }
703
703
  async function fetchRunLogs(runId, conn, proxy, sshExec2) {
704
- const sql27 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
705
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql27}" 2>/dev/null`;
704
+ const sql28 = `SELECT level, message, \\"isError\\", \\"createdAt\\" FROM \\"TaskEvent\\" WHERE \\"runId\\" = '${runId}' AND level IN ('INFO','WARN','ERROR','DEBUG','LOG','TRACE') ORDER BY \\"startTime\\" ASC LIMIT 200`;
705
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql28}" 2>/dev/null`;
706
706
  const result = await sshExec2(conn, cmd, proxy);
707
707
  const output = result.stdout.trim();
708
708
  if (!output) return "";
@@ -784,8 +784,8 @@ async function handleTriggerTool(name, args2, deps) {
784
784
  switch (name) {
785
785
  // -----------------------------------------------------------------
786
786
  case "trigger-list": {
787
- const sql27 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
788
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql27}" 2>/dev/null`;
787
+ const sql28 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
788
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql28}" 2>/dev/null`;
789
789
  const result = await sshExec2(conn, cmd, proxy);
790
790
  const output = result.stdout.trim();
791
791
  if (!output) {
@@ -1024,6 +1024,39 @@ async function getMgBoilerGitHubToken() {
1024
1024
  return { token: null, error: "Failed to decrypt GitHub token" };
1025
1025
  }
1026
1026
  }
1027
+ async function getGitHubTokenById(id) {
1028
+ try {
1029
+ const rows = await getDb().execute(sql`
1030
+ SELECT id, label, owner, notes, token_encrypted, created_at, updated_at
1031
+ FROM github_token
1032
+ WHERE id = ${id}
1033
+ LIMIT 1
1034
+ `);
1035
+ const row = rows[0];
1036
+ if (!row)
1037
+ return { token: null, owner: null, error: null };
1038
+ return {
1039
+ token: decrypt(row.token_encrypted),
1040
+ owner: row.owner ?? null,
1041
+ error: null
1042
+ };
1043
+ } catch (error) {
1044
+ console.error("[getGitHubTokenById] error:", error);
1045
+ return { token: null, owner: null, error: "Failed to decrypt GitHub token" };
1046
+ }
1047
+ }
1048
+ async function resolveReleaseGitHubToken(githubTokenId) {
1049
+ if (githubTokenId) {
1050
+ const { token, error } = await getGitHubTokenById(githubTokenId);
1051
+ if (token)
1052
+ return { token, error: null };
1053
+ return {
1054
+ token: null,
1055
+ error: error || "Configured GitHub token not found"
1056
+ };
1057
+ }
1058
+ return getMgBoilerGitHubToken();
1059
+ }
1027
1060
 
1028
1061
  // ../../node_modules/zod/dist/esm/v3/external.js
1029
1062
  var external_exports = {};
@@ -5061,7 +5094,7 @@ var coerce = {
5061
5094
  var NEVER = INVALID;
5062
5095
 
5063
5096
  // ../platform/dist/utils/litespeed-vhost.js
5064
- var LITESPEED_STANDARD_APP_PATH = /^apps\/(backoffice|portal|web|api)(\/[\w-]+)?$/;
5097
+ var LITESPEED_STANDARD_APP_PATH = /^(\.|apps\/(backoffice|portal|web|api)(\/[\w-]+)?)$/;
5065
5098
  var LitespeedVhostMappingSchema = external_exports.object({
5066
5099
  domain: external_exports.string().min(1).transform((value) => normalizeVhostDomain(value)),
5067
5100
  appPath: external_exports.string().min(1).regex(LITESPEED_STANDARD_APP_PATH, "Unsupported appPath")
@@ -5473,7 +5506,9 @@ async function createReleaseForStage(userId, params) {
5473
5506
  "BAD_REQUEST"
5474
5507
  );
5475
5508
  }
5476
- const { token } = await getMgBoilerGitHubToken();
5509
+ const { token } = await resolveReleaseGitHubToken(
5510
+ profile.github_token_id ?? null
5511
+ );
5477
5512
  if (!token) {
5478
5513
  throw new CreateReleaseError(
5479
5514
  "GitHub token is not configured",
@@ -5612,6 +5647,449 @@ async function createReleaseForStage(userId, params) {
5612
5647
  authorName: triggerAuthorName
5613
5648
  };
5614
5649
  }
5650
+ var GITHUB_API3 = "https://api.github.com";
5651
+ var ReleaseProfileQuickCreateError = class extends Error {
5652
+ constructor(message, code = "INTERNAL_SERVER_ERROR") {
5653
+ super(message);
5654
+ this.code = code;
5655
+ this.name = "ReleaseProfileQuickCreateError";
5656
+ }
5657
+ };
5658
+ function inferDeployMethod(folderName, hasNextConfig, hasDockerfile, hasPm2Ecosystem) {
5659
+ if (hasPm2Ecosystem && hasNextConfig) return "pm2";
5660
+ if (hasPm2Ecosystem && !hasDockerfile && !folderName.toLowerCase().includes("api"))
5661
+ return "pm2";
5662
+ if (hasNextConfig) return "pm2";
5663
+ if (hasDockerfile || folderName.toLowerCase().includes("api"))
5664
+ return "docker";
5665
+ return "none";
5666
+ }
5667
+ function toLabel(folderName) {
5668
+ return folderName.charAt(0).toUpperCase() + folderName.slice(1);
5669
+ }
5670
+ async function scanRepoForApps(repoFullName, githubToken, ref = "main") {
5671
+ const headers = {
5672
+ Accept: "application/vnd.github.v3+json",
5673
+ Authorization: `Bearer ${githubToken}`,
5674
+ "Content-Type": "application/json"
5675
+ };
5676
+ async function fetchContents(path) {
5677
+ const res = await fetch(
5678
+ `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
5679
+ { headers }
5680
+ );
5681
+ if (!res.ok) return null;
5682
+ const data = await res.json();
5683
+ return Array.isArray(data) ? data : null;
5684
+ }
5685
+ async function fileExists(path) {
5686
+ const res = await fetch(
5687
+ `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
5688
+ { headers, method: "HEAD" }
5689
+ );
5690
+ return res.ok;
5691
+ }
5692
+ const apps = [];
5693
+ const appsDir = await fetchContents("apps");
5694
+ const isMonorepo = appsDir !== null && appsDir.length > 0;
5695
+ const hasPm2Ecosystem = await Promise.any([
5696
+ fileExists("ecosystem.config.cjs"),
5697
+ fileExists("ecosystem.config.js"),
5698
+ fileExists("ecosystem.config.mjs")
5699
+ ]).catch(() => false);
5700
+ if (isMonorepo) {
5701
+ const subfolders = appsDir.filter((item) => item.type === "dir");
5702
+ const detections = await Promise.all(
5703
+ subfolders.map(async (folder) => {
5704
+ const folderPath = `apps/${folder.name}`;
5705
+ const [hasNextMjs, hasNextJs, hasNextTs, hasDockerfile, hasPkgJson] = await Promise.all([
5706
+ fileExists(`${folderPath}/next.config.mjs`),
5707
+ fileExists(`${folderPath}/next.config.js`),
5708
+ fileExists(`${folderPath}/next.config.ts`),
5709
+ fileExists(`${folderPath}/Dockerfile`),
5710
+ fileExists(`${folderPath}/package.json`)
5711
+ ]);
5712
+ const hasNextConfig = hasNextMjs || hasNextJs || hasNextTs;
5713
+ const deployMethod = inferDeployMethod(
5714
+ folder.name,
5715
+ hasNextConfig,
5716
+ hasDockerfile,
5717
+ !!hasPm2Ecosystem
5718
+ );
5719
+ return {
5720
+ path: folderPath,
5721
+ label: toLabel(folder.name),
5722
+ deployMethod,
5723
+ hasNextConfig,
5724
+ hasDockerfile,
5725
+ hasPackageJson: hasPkgJson,
5726
+ enabled: true
5727
+ };
5728
+ })
5729
+ );
5730
+ apps.push(...detections);
5731
+ } else {
5732
+ const [hasNextMjs, hasNextJs, hasNextTs, hasDockerfile, hasPkgJson] = await Promise.all([
5733
+ fileExists("next.config.mjs"),
5734
+ fileExists("next.config.js"),
5735
+ fileExists("next.config.ts"),
5736
+ fileExists("Dockerfile"),
5737
+ fileExists("package.json")
5738
+ ]);
5739
+ const hasNextConfig = hasNextMjs || hasNextJs || hasNextTs;
5740
+ const repoName = repoFullName.split("/").pop() ?? "app";
5741
+ const deployMethod = inferDeployMethod(
5742
+ repoName,
5743
+ hasNextConfig,
5744
+ hasDockerfile,
5745
+ !!hasPm2Ecosystem
5746
+ );
5747
+ apps.push({
5748
+ path: ".",
5749
+ label: toLabel(repoName),
5750
+ deployMethod,
5751
+ hasNextConfig,
5752
+ hasDockerfile,
5753
+ hasPackageJson: hasPkgJson,
5754
+ enabled: true
5755
+ });
5756
+ }
5757
+ const workDirectory = isMonorepo ? apps.find((a) => a.deployMethod !== "none")?.path ?? "." : ".";
5758
+ const hasTriggerDev = await Promise.any([
5759
+ fileExists("packages/jobs/trigger.config.ts"),
5760
+ fileExists("trigger.config.ts")
5761
+ ]).catch(() => false);
5762
+ return {
5763
+ isMonorepo,
5764
+ apps,
5765
+ workDirectory,
5766
+ hasTriggerDev: !!hasTriggerDev
5767
+ };
5768
+ }
5769
+ var QUICK_STAGE_DEFAULTS = {
5770
+ dev: {
5771
+ name: "Development",
5772
+ releaseBranch: "release-dev",
5773
+ triggerMode: "pr_webhook",
5774
+ autoApprove: true,
5775
+ requireReview: false,
5776
+ backupDb: false,
5777
+ migrateDb: true
5778
+ },
5779
+ staging: {
5780
+ name: "Staging",
5781
+ releaseBranch: "release-staging",
5782
+ triggerMode: "manual",
5783
+ autoApprove: false,
5784
+ requireReview: false,
5785
+ backupDb: true,
5786
+ migrateDb: true
5787
+ },
5788
+ prod: {
5789
+ name: "Production",
5790
+ releaseBranch: "release-prod",
5791
+ triggerMode: "manual",
5792
+ autoApprove: false,
5793
+ requireReview: false,
5794
+ backupDb: true,
5795
+ migrateDb: true
5796
+ }
5797
+ };
5798
+ var QUICK_STAGES_FOR_TYPE = {
5799
+ dev_only: ["dev"],
5800
+ prod_only: ["prod"],
5801
+ both: ["dev", "prod"]
5802
+ };
5803
+ function deriveProfileName(repoFullName) {
5804
+ const repoName = repoFullName.split("/").pop() ?? repoFullName;
5805
+ return repoName.split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
5806
+ }
5807
+ function toStageApps(apps) {
5808
+ return apps.filter((a) => a.enabled).map((a) => ({
5809
+ path: a.path,
5810
+ label: a.label,
5811
+ deployMethod: a.deployMethod,
5812
+ deploymentAppId: null,
5813
+ deploymentServerId: null,
5814
+ deployPath: null,
5815
+ deployScript: null,
5816
+ enabled: a.enabled
5817
+ }));
5818
+ }
5819
+ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, githubToken) {
5820
+ const headers = {
5821
+ Accept: "application/vnd.github.v3+json",
5822
+ Authorization: `Bearer ${githubToken}`,
5823
+ "Content-Type": "application/json"
5824
+ };
5825
+ const checkRes = await fetch(
5826
+ `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${branchName}`,
5827
+ { headers }
5828
+ );
5829
+ if (checkRes.ok) return;
5830
+ let sha;
5831
+ const sourceRes = await fetch(
5832
+ `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
5833
+ { headers }
5834
+ );
5835
+ if (sourceRes.ok) {
5836
+ const data = await sourceRes.json();
5837
+ sha = data.object?.sha;
5838
+ } else {
5839
+ const repoRes = await fetch(`${GITHUB_API3}/repos/${repoFullName}`, {
5840
+ headers
5841
+ });
5842
+ if (!repoRes.ok) {
5843
+ throw new Error(`Could not access repository ${repoFullName}`);
5844
+ }
5845
+ const repo = await repoRes.json();
5846
+ const defaultBranch = repo.default_branch ?? "main";
5847
+ const defaultRes = await fetch(
5848
+ `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
5849
+ { headers }
5850
+ );
5851
+ if (!defaultRes.ok) {
5852
+ throw new Error(`Could not resolve a source branch for ${repoFullName}`);
5853
+ }
5854
+ const data = await defaultRes.json();
5855
+ sha = data.object?.sha;
5856
+ }
5857
+ if (!sha) {
5858
+ throw new Error(`Could not determine SHA to create branch ${branchName}`);
5859
+ }
5860
+ const createRes = await fetch(
5861
+ `${GITHUB_API3}/repos/${repoFullName}/git/refs`,
5862
+ {
5863
+ method: "POST",
5864
+ headers,
5865
+ body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha })
5866
+ }
5867
+ );
5868
+ if (!createRes.ok && createRes.status !== 422) {
5869
+ const body = await createRes.text().catch(() => "");
5870
+ throw new Error(
5871
+ `Failed to create branch ${branchName}: ${createRes.status} ${body}`
5872
+ );
5873
+ }
5874
+ }
5875
+ async function insertQuickStages(profileId, releaseType, stageApps) {
5876
+ const db2 = getDb();
5877
+ const stages = [];
5878
+ const wanted = QUICK_STAGES_FOR_TYPE[releaseType];
5879
+ for (let i = 0; i < wanted.length; i++) {
5880
+ const stageType = wanted[i];
5881
+ const defaults = QUICK_STAGE_DEFAULTS[stageType];
5882
+ const rows = await db2.execute(sql`
5883
+ INSERT INTO release_profile_stage (
5884
+ release_profile_id, name, stage, stage_order, release_branch,
5885
+ trigger_mode, auto_approve, require_review,
5886
+ backup_db, migrate_db, stage_apps, enabled,
5887
+ trigger_branch, trigger_event
5888
+ ) VALUES (
5889
+ ${profileId}, ${defaults.name}, ${stageType}, ${i + 1},
5890
+ ${defaults.releaseBranch},
5891
+ ${defaults.triggerMode}, ${defaults.autoApprove}, ${defaults.requireReview},
5892
+ ${defaults.backupDb}, ${defaults.migrateDb},
5893
+ ${JSON.stringify(stageApps)}::jsonb, true,
5894
+ ${defaults.releaseBranch}, 'push'
5895
+ )
5896
+ RETURNING id, name, stage, release_branch, trigger_mode
5897
+ `);
5898
+ if (rows[0]) stages.push(rows[0]);
5899
+ }
5900
+ return stages;
5901
+ }
5902
+ async function quickCreateReleaseProfile(userId, options) {
5903
+ const db2 = getDb();
5904
+ const repoFullName = options.repoFullName.trim();
5905
+ if (!repoFullName.includes("/")) {
5906
+ throw new ReleaseProfileQuickCreateError(
5907
+ `Repository must be in "owner/name" format, got "${repoFullName}"`,
5908
+ "BAD_REQUEST"
5909
+ );
5910
+ }
5911
+ const existing = await db2.execute(sql`
5912
+ SELECT id, name FROM release_profile
5913
+ WHERE lower(repo_full_name) = ${repoFullName.toLowerCase()}
5914
+ LIMIT 1
5915
+ `);
5916
+ if (existing[0]) {
5917
+ throw new ReleaseProfileQuickCreateError(
5918
+ `Repository ${repoFullName} already has a release profile ("${existing[0].name}")`,
5919
+ "CONFLICT"
5920
+ );
5921
+ }
5922
+ const githubTokenId = options.githubTokenId ?? null;
5923
+ const { token, error: tokenError } = await resolveReleaseGitHubToken(githubTokenId);
5924
+ if (!token) {
5925
+ throw new ReleaseProfileQuickCreateError(
5926
+ tokenError || "GitHub token is not configured",
5927
+ "PRECONDITION_FAILED"
5928
+ );
5929
+ }
5930
+ let scan;
5931
+ try {
5932
+ scan = await scanRepoForApps(repoFullName, token);
5933
+ } catch (err) {
5934
+ throw new ReleaseProfileQuickCreateError(
5935
+ `Failed to scan repository ${repoFullName}: ${err instanceof Error ? err.message : String(err)}`,
5936
+ "BAD_REQUEST"
5937
+ );
5938
+ }
5939
+ const name = options.name?.trim() || deriveProfileName(repoFullName);
5940
+ const releaseType = options.releaseType ?? "both";
5941
+ const detectedApps = toStageApps(scan.apps);
5942
+ let profile;
5943
+ try {
5944
+ const rows = await db2.execute(sql`
5945
+ INSERT INTO release_profile (
5946
+ name, repo_full_name, work_directory, enabled, release_type,
5947
+ current_version, active_version, created_by, use_changes_branch,
5948
+ detected_apps, has_trigger_dev, github_token_id
5949
+ ) VALUES (
5950
+ ${name}, ${repoFullName}, ${scan.workDirectory}, true, ${releaseType},
5951
+ NULL, NULL, ${userId}, false,
5952
+ ${JSON.stringify(detectedApps)}::jsonb, ${scan.hasTriggerDev},
5953
+ ${githubTokenId}
5954
+ )
5955
+ RETURNING *
5956
+ `);
5957
+ profile = rows[0];
5958
+ } catch (err) {
5959
+ throw new ReleaseProfileQuickCreateError(
5960
+ `Failed to create release profile: ${err instanceof Error ? err.message : String(err)}`,
5961
+ "INTERNAL_SERVER_ERROR"
5962
+ );
5963
+ }
5964
+ if (!profile) {
5965
+ throw new ReleaseProfileQuickCreateError(
5966
+ "Release profile insert returned no rows",
5967
+ "INTERNAL_SERVER_ERROR"
5968
+ );
5969
+ }
5970
+ const stages = await insertQuickStages(profile.id, releaseType, detectedApps);
5971
+ const branchWarnings = [];
5972
+ for (const stage of stages) {
5973
+ if (!stage.release_branch) continue;
5974
+ try {
5975
+ await ensureGitHubBranchExists(
5976
+ repoFullName,
5977
+ stage.release_branch,
5978
+ "main",
5979
+ token
5980
+ );
5981
+ } catch (err) {
5982
+ branchWarnings.push(
5983
+ `Could not create branch ${stage.release_branch}: ${err instanceof Error ? err.message : String(err)}`
5984
+ );
5985
+ }
5986
+ }
5987
+ return { profile, stages, scan, branchWarnings };
5988
+ }
5989
+ async function updateReleaseProfileSettings(profileId, fields) {
5990
+ const db2 = getDb();
5991
+ const profileRows = await db2.execute(sql`
5992
+ SELECT id, release_type, detected_apps
5993
+ FROM release_profile WHERE id = ${profileId} LIMIT 1
5994
+ `);
5995
+ const current = profileRows[0];
5996
+ if (!current) {
5997
+ throw new ReleaseProfileQuickCreateError(
5998
+ "Release profile not found",
5999
+ "NOT_FOUND"
6000
+ );
6001
+ }
6002
+ const setExprs = [];
6003
+ if (fields.name !== void 0) setExprs.push(sql`name = ${fields.name}`);
6004
+ if (fields.enabled !== void 0)
6005
+ setExprs.push(sql`enabled = ${fields.enabled}`);
6006
+ if (fields.releaseType !== void 0)
6007
+ setExprs.push(sql`release_type = ${fields.releaseType}`);
6008
+ if (fields.githubTokenId !== void 0)
6009
+ setExprs.push(sql`github_token_id = ${fields.githubTokenId}`);
6010
+ if (fields.useChangesBranch !== void 0)
6011
+ setExprs.push(sql`use_changes_branch = ${fields.useChangesBranch}`);
6012
+ if (fields.hasTriggerDev !== void 0)
6013
+ setExprs.push(sql`has_trigger_dev = ${fields.hasTriggerDev}`);
6014
+ if (fields.workDirectory !== void 0)
6015
+ setExprs.push(sql`work_directory = ${fields.workDirectory}`);
6016
+ if (setExprs.length === 0) {
6017
+ throw new ReleaseProfileQuickCreateError(
6018
+ "No fields to update",
6019
+ "BAD_REQUEST"
6020
+ );
6021
+ }
6022
+ const rows = await db2.execute(sql`
6023
+ UPDATE release_profile
6024
+ SET ${sql.join(setExprs, sql`, `)}
6025
+ WHERE id = ${profileId}
6026
+ RETURNING *
6027
+ `);
6028
+ const profile = rows[0];
6029
+ if (!profile) {
6030
+ throw new ReleaseProfileQuickCreateError(
6031
+ "Release profile update returned no rows",
6032
+ "INTERNAL_SERVER_ERROR"
6033
+ );
6034
+ }
6035
+ let stagesReconfigured = false;
6036
+ if (fields.releaseType !== void 0 && fields.releaseType !== current.release_type) {
6037
+ await reconfigureQuickStages(
6038
+ profileId,
6039
+ fields.releaseType,
6040
+ current.detected_apps ?? []
6041
+ );
6042
+ stagesReconfigured = true;
6043
+ }
6044
+ return { profile, stagesReconfigured };
6045
+ }
6046
+ async function reconfigureQuickStages(profileId, releaseType, detectedApps) {
6047
+ const db2 = getDb();
6048
+ const wanted = QUICK_STAGES_FOR_TYPE[releaseType];
6049
+ const existingStages = await db2.execute(sql`
6050
+ SELECT id, stage, release_branch
6051
+ FROM release_profile_stage
6052
+ WHERE release_profile_id = ${profileId}
6053
+ `);
6054
+ const existingByStage = new Map(existingStages.map((s) => [s.stage, s]));
6055
+ for (let i = 0; i < wanted.length; i++) {
6056
+ const stageType = wanted[i];
6057
+ const defaults = QUICK_STAGE_DEFAULTS[stageType];
6058
+ const existing = existingByStage.get(stageType);
6059
+ if (existing) {
6060
+ await db2.execute(sql`
6061
+ UPDATE release_profile_stage
6062
+ SET stage_order = ${i + 1},
6063
+ release_branch = ${existing.release_branch ?? defaults.releaseBranch},
6064
+ enabled = true
6065
+ WHERE id = ${existing.id}
6066
+ `);
6067
+ } else {
6068
+ await db2.execute(sql`
6069
+ INSERT INTO release_profile_stage (
6070
+ release_profile_id, name, stage, stage_order, release_branch,
6071
+ trigger_mode, auto_approve, require_review,
6072
+ backup_db, migrate_db, stage_apps, enabled,
6073
+ trigger_branch, trigger_event
6074
+ ) VALUES (
6075
+ ${profileId}, ${defaults.name}, ${stageType}, ${i + 1},
6076
+ ${defaults.releaseBranch},
6077
+ ${defaults.triggerMode}, ${defaults.autoApprove}, ${defaults.requireReview},
6078
+ ${defaults.backupDb}, ${defaults.migrateDb},
6079
+ ${JSON.stringify(detectedApps)}::jsonb, true,
6080
+ ${defaults.releaseBranch}, 'push'
6081
+ )
6082
+ `);
6083
+ }
6084
+ }
6085
+ const wantedSet = new Set(wanted);
6086
+ for (const existing of existingStages) {
6087
+ if (wantedSet.has(existing.stage)) continue;
6088
+ await db2.execute(sql`
6089
+ UPDATE release_profile_stage SET enabled = false WHERE id = ${existing.id}
6090
+ `);
6091
+ }
6092
+ }
5615
6093
  var args = process.argv.slice(2);
5616
6094
  function getArg2(name) {
5617
6095
  return args.find((a) => a.startsWith(`--${name}=`))?.split("=").slice(1).join("=");
@@ -5853,6 +6331,9 @@ var TOOL_MODULE_MAP = {
5853
6331
  "env-get": "ci_cd",
5854
6332
  "env-store": "ci_cd",
5855
6333
  "release-trigger": "ci_cd",
6334
+ "release-profile-list": "ci_cd",
6335
+ "release-profile-create": "ci_cd",
6336
+ "release-profile-update": "ci_cd",
5856
6337
  "domain-list": "domains",
5857
6338
  "dns-list": "domains",
5858
6339
  "dns-record": "domains",
@@ -6233,6 +6714,23 @@ async function resolveReleaseProfileStageIds(profileName) {
6233
6714
  }
6234
6715
  return { stageIds: stages.map((s) => s.id), profileId: profile.id };
6235
6716
  }
6717
+ async function resolveGitHubTokenIdByKey(key) {
6718
+ const trimmed = key.trim();
6719
+ const rows = await db.execute(sql`
6720
+ SELECT id FROM github_token
6721
+ WHERE label ILIKE ${trimmed} OR owner ILIKE ${trimmed} OR id::text = ${trimmed}
6722
+ ORDER BY label
6723
+ LIMIT 1
6724
+ `);
6725
+ if (rows[0]) return rows[0].id;
6726
+ const all = await db.execute(
6727
+ sql`SELECT label, owner FROM github_token ORDER BY label`
6728
+ );
6729
+ const available = all.map((t) => t.owner ? `${t.label} (${t.owner})` : t.label).join(", ");
6730
+ throw new Error(
6731
+ `GitHub key "${key}" not found. Available keys: ${available || "(none)"}. Omit githubKey to use the default MG Software token.`
6732
+ );
6733
+ }
6236
6734
  async function resolveReleaseTriggerStage(profileName, stageFilter) {
6237
6735
  const profileRows = await db.execute(sql`
6238
6736
  SELECT id, name FROM release_profile
@@ -8397,11 +8895,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
8397
8895
  applied_by TEXT
8398
8896
  );
8399
8897
  `.trim();
8400
- function normaliseMigrationSql(sql27) {
8401
- return sql27.replace(/\r\n/g, "\n").trim() + "\n";
8898
+ function normaliseMigrationSql(sql28) {
8899
+ return sql28.replace(/\r\n/g, "\n").trim() + "\n";
8402
8900
  }
8403
- function migrationSha256(sql27) {
8404
- return createHash("sha256").update(normaliseMigrationSql(sql27), "utf8").digest("hex");
8901
+ function migrationSha256(sql28) {
8902
+ return createHash("sha256").update(normaliseMigrationSql(sql28), "utf8").digest("hex");
8405
8903
  }
8406
8904
  function dollarQuoteTag(value) {
8407
8905
  let tag = "_mcp";
@@ -9468,6 +9966,79 @@ var TOOLS = [
9468
9966
  required: ["releaseProfile"]
9469
9967
  }
9470
9968
  },
9969
+ {
9970
+ name: "release-profile-list",
9971
+ description: "List all release profiles with repo, enabled state, release type, GitHub key and a summary of their stages. Requires ci_cd permission.",
9972
+ inputSchema: {
9973
+ type: "object",
9974
+ properties: {},
9975
+ required: []
9976
+ }
9977
+ },
9978
+ {
9979
+ name: "release-profile-create",
9980
+ description: "One-step release profile creation: scans the GitHub repo, detects apps (Next.js/Docker/PM2), creates the profile plus dev/prod stages with smart defaults (dev: release-dev + PR webhook + auto-approve, prod: release-prod + manual + DB backup) and seeds the stage apps. Release branches are created on GitHub automatically. Deployment servers must be assigned afterwards in the dashboard (Releases \u2192 Profiles). Requires ci_cd permission.",
9981
+ inputSchema: {
9982
+ type: "object",
9983
+ properties: {
9984
+ repoFullName: {
9985
+ type: "string",
9986
+ description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard).'
9987
+ },
9988
+ githubKey: {
9989
+ type: "string",
9990
+ description: 'Named GitHub key (github_token label or owner, e.g. "AVARC Solutions") used for all GitHub API calls for this profile. Omit for the default MG Software token.'
9991
+ },
9992
+ name: {
9993
+ type: "string",
9994
+ description: 'Profile name override. Default: title-cased repo name (mg-dashboard \u2192 "Mg Dashboard").'
9995
+ },
9996
+ releaseType: {
9997
+ type: "string",
9998
+ enum: ["dev_only", "prod_only", "both"],
9999
+ description: "Which stages to create (default: both = dev + prod)."
10000
+ }
10001
+ },
10002
+ required: ["repoFullName"]
10003
+ }
10004
+ },
10005
+ {
10006
+ name: "release-profile-update",
10007
+ description: "Update release profile settings. Locate the profile by name or repo. Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Requires ci_cd permission.",
10008
+ inputSchema: {
10009
+ type: "object",
10010
+ properties: {
10011
+ releaseProfile: {
10012
+ type: "string",
10013
+ description: "Profile name or repo full name to locate the profile."
10014
+ },
10015
+ name: { type: "string", description: "New profile name." },
10016
+ enabled: { type: "boolean", description: "Enable/disable the profile." },
10017
+ releaseType: {
10018
+ type: "string",
10019
+ enum: ["dev_only", "prod_only", "both"],
10020
+ description: "New release type \u2014 reconfigures stages accordingly."
10021
+ },
10022
+ githubKey: {
10023
+ type: "string",
10024
+ description: 'Named GitHub key (github_token label or owner). Pass "default" to reset to the default MG Software token.'
10025
+ },
10026
+ useChangesBranch: {
10027
+ type: "boolean",
10028
+ description: "Use a long-lived -changes accumulation branch."
10029
+ },
10030
+ hasTriggerDev: {
10031
+ type: "boolean",
10032
+ description: "Deploy Trigger.dev jobs during production releases."
10033
+ },
10034
+ workDirectory: {
10035
+ type: "string",
10036
+ description: 'Working directory within the repo (e.g. ".").'
10037
+ }
10038
+ },
10039
+ required: ["releaseProfile"]
10040
+ }
10041
+ },
9471
10042
  {
9472
10043
  name: "cache-purge",
9473
10044
  description: 'Purge caches on a server. Default mode is **safe** (recommended): LiteSpeed file cache, WordPress/LiteSpeed plugin caches via wp-cli, PrestaShop file caches, graceful LiteSpeed reload \u2014 does NOT kill lsphp, FLUSHALL Redis, or hard-restart the web server.\n\nUse `mode: "full"` only when safe purge is insufficient: kills all lsphp (OPcache reset), clears Redis/Memcached entirely, and may hard-restart LiteSpeed \u2014 can briefly take sites offline or serve stale error pages until caches warm up (VCA multi-site risk).',
@@ -9567,7 +10138,7 @@ var TOOLS = [
9567
10138
  // ----- Trigger.dev -----
9568
10139
  ...TRIGGER_TOOLS
9569
10140
  ];
9570
- var MCP_VERSION = "7.0.0";
10141
+ var MCP_VERSION = "7.0.5";
9571
10142
  async function handleListTools() {
9572
10143
  if (!authContext) return { tools: TOOLS };
9573
10144
  const accessible = TOOLS.filter((tool) => {
@@ -12009,6 +12580,170 @@ LIMIT ${limit};
12009
12580
  throw err;
12010
12581
  }
12011
12582
  }
12583
+ case "release-profile-list": {
12584
+ const profiles = await db.execute(sql`
12585
+ SELECT
12586
+ p.id, p.name, p.repo_full_name, p.enabled, p.release_type,
12587
+ p.work_directory, p.use_changes_branch, p.has_trigger_dev,
12588
+ gt.label AS github_key,
12589
+ (
12590
+ SELECT string_agg(
12591
+ s.stage || ' (' || COALESCE(s.release_branch, '-') ||
12592
+ ', ' || COALESCE(s.trigger_mode, 'manual') ||
12593
+ CASE WHEN s.enabled THEN '' ELSE ', disabled' END || ')',
12594
+ '; ' ORDER BY s.stage_order
12595
+ )
12596
+ FROM release_profile_stage s
12597
+ WHERE s.release_profile_id = p.id
12598
+ ) AS stages
12599
+ FROM release_profile p
12600
+ LEFT JOIN github_token gt ON gt.id = p.github_token_id
12601
+ ORDER BY p.name
12602
+ `);
12603
+ if (profiles.length === 0) {
12604
+ return {
12605
+ content: [{ type: "text", text: "No release profiles found" }]
12606
+ };
12607
+ }
12608
+ const lines = profiles.map((p) => {
12609
+ const flags = [
12610
+ p.enabled ? "enabled" : "disabled",
12611
+ `type=${p.release_type ?? "both"}`,
12612
+ `key=${p.github_key ?? "MG Software (default)"}`,
12613
+ p.use_changes_branch ? "changes-branch" : null,
12614
+ p.has_trigger_dev ? "trigger.dev" : null
12615
+ ].filter(Boolean).join(", ");
12616
+ return `${p.name} \u2014 ${p.repo_full_name} [${flags}]
12617
+ stages: ${p.stages ?? "(none)"}`;
12618
+ });
12619
+ return {
12620
+ content: [
12621
+ {
12622
+ type: "text",
12623
+ text: `${profiles.length} release profile(s):
12624
+
12625
+ ${lines.join("\n\n")}`
12626
+ }
12627
+ ]
12628
+ };
12629
+ }
12630
+ case "release-profile-create": {
12631
+ const repoFullName = String(a.repoFullName ?? "").trim();
12632
+ const githubTokenId = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
12633
+ const releaseType = a.releaseType ? String(a.releaseType) : null;
12634
+ if (releaseType !== null && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
12635
+ throw new Error(
12636
+ `Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
12637
+ );
12638
+ }
12639
+ try {
12640
+ const result = await quickCreateReleaseProfile(
12641
+ authContext.userId,
12642
+ {
12643
+ repoFullName,
12644
+ githubTokenId,
12645
+ name: a.name ? String(a.name) : null,
12646
+ releaseType
12647
+ }
12648
+ );
12649
+ return {
12650
+ content: [
12651
+ {
12652
+ type: "text",
12653
+ text: JSON.stringify(
12654
+ {
12655
+ profileId: result.profile.id,
12656
+ name: result.profile.name,
12657
+ repo: repoFullName,
12658
+ releaseType: releaseType ?? "both",
12659
+ appsDetected: result.scan.apps.map((app) => ({
12660
+ path: app.path,
12661
+ label: app.label,
12662
+ deployMethod: app.deployMethod
12663
+ })),
12664
+ stages: result.stages.map((s) => ({
12665
+ stage: s.stage,
12666
+ name: s.name,
12667
+ releaseBranch: s.release_branch,
12668
+ triggerMode: s.trigger_mode
12669
+ })),
12670
+ branchWarnings: result.branchWarnings,
12671
+ hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
12672
+ },
12673
+ null,
12674
+ 2
12675
+ )
12676
+ }
12677
+ ]
12678
+ };
12679
+ } catch (err) {
12680
+ if (err instanceof ReleaseProfileQuickCreateError) {
12681
+ throw new Error(err.message);
12682
+ }
12683
+ throw err;
12684
+ }
12685
+ }
12686
+ case "release-profile-update": {
12687
+ const identifier = String(a.releaseProfile ?? "").trim();
12688
+ const profileRows = await db.execute(sql`
12689
+ SELECT id, name FROM release_profile
12690
+ WHERE name ILIKE ${identifier} OR repo_full_name ILIKE ${identifier}
12691
+ LIMIT 1
12692
+ `);
12693
+ const profile = profileRows[0];
12694
+ if (!profile) {
12695
+ const all = await db.execute(
12696
+ sql`SELECT name FROM release_profile ORDER BY name`
12697
+ );
12698
+ throw new Error(
12699
+ `Release profile "${identifier}" not found. Available profiles: ${all.map((p) => p.name).join(", ") || "(none)"}`
12700
+ );
12701
+ }
12702
+ const releaseType = a.releaseType ? String(a.releaseType) : void 0;
12703
+ if (releaseType !== void 0 && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
12704
+ throw new Error(
12705
+ `Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
12706
+ );
12707
+ }
12708
+ let githubTokenId;
12709
+ if (a.githubKey !== void 0) {
12710
+ const key = String(a.githubKey).trim();
12711
+ githubTokenId = !key || key.toLowerCase() === "default" ? null : await resolveGitHubTokenIdByKey(key);
12712
+ }
12713
+ try {
12714
+ const result = await updateReleaseProfileSettings(profile.id, {
12715
+ name: a.name !== void 0 ? String(a.name) : void 0,
12716
+ enabled: a.enabled !== void 0 ? a.enabled === true : void 0,
12717
+ releaseType,
12718
+ githubTokenId,
12719
+ useChangesBranch: a.useChangesBranch !== void 0 ? a.useChangesBranch === true : void 0,
12720
+ hasTriggerDev: a.hasTriggerDev !== void 0 ? a.hasTriggerDev === true : void 0,
12721
+ workDirectory: a.workDirectory !== void 0 ? String(a.workDirectory) : void 0
12722
+ });
12723
+ return {
12724
+ content: [
12725
+ {
12726
+ type: "text",
12727
+ text: JSON.stringify(
12728
+ {
12729
+ profileId: profile.id,
12730
+ name: result.profile.name,
12731
+ updated: true,
12732
+ stagesReconfigured: result.stagesReconfigured
12733
+ },
12734
+ null,
12735
+ 2
12736
+ )
12737
+ }
12738
+ ]
12739
+ };
12740
+ } catch (err) {
12741
+ if (err instanceof ReleaseProfileQuickCreateError) {
12742
+ throw new Error(err.message);
12743
+ }
12744
+ throw err;
12745
+ }
12746
+ }
12012
12747
  // ----- Cache Purge -----
12013
12748
  case "cache-purge": {
12014
12749
  const { conn, proxy } = await getServerConnection(String(a.serverId));