@mgsoftwarebv/mg-dashboard-mcp 7.0.9 → 7.0.11

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
@@ -21,8 +21,7 @@ import { once } from 'events';
21
21
  import { lookup } from 'dns/promises';
22
22
  import { connect } from 'tls';
23
23
  import { pgTable, timestamp, jsonb, uuid, boolean, text, pgEnum, integer, uniqueIndex, index, bigint } from 'drizzle-orm/pg-core';
24
- import { tasks } from '@trigger.dev/sdk/v3';
25
- import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
24
+ import { HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, S3Client, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
26
25
 
27
26
  var __defProp = Object.defineProperty;
28
27
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -684,10 +683,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
684
683
  "trigger-run": "ci_cd"
685
684
  };
686
685
  async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
687
- const sql29 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
686
+ const sql23 = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
688
687
  const cmd = [
689
688
  `PORT=$(docker port "${WA_CONTAINER}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
690
- `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null | tr -d '[:space:]')`,
689
+ `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null | tr -d '[:space:]')`,
691
690
  'echo "$PORT|$KEY"'
692
691
  ].join(" && ");
693
692
  const result = await sshExec2(conn, cmd, proxy);
@@ -708,8 +707,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
708
707
  return { port, apiKey: apiKey2 };
709
708
  }
710
709
  async function fetchRunLogs(runId, conn, proxy, sshExec2) {
711
- const sql29 = `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`;
712
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null`;
710
+ const sql23 = `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`;
711
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null`;
713
712
  const result = await sshExec2(conn, cmd, proxy);
714
713
  const output = result.stdout.trim();
715
714
  if (!output) return "";
@@ -791,8 +790,8 @@ async function handleTriggerTool(name, args2, deps) {
791
790
  switch (name) {
792
791
  // -----------------------------------------------------------------
793
792
  case "trigger-list": {
794
- const sql29 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
795
- const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql29}" 2>/dev/null`;
793
+ const sql23 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
794
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql23}" 2>/dev/null`;
796
795
  const result = await sshExec2(conn, cmd, proxy);
797
796
  const output = result.stdout.trim();
798
797
  if (!output) {
@@ -5746,563 +5745,7 @@ pgTable(
5746
5745
  index("github_webhook_secret_owner_idx").on(table.owner)
5747
5746
  ]
5748
5747
  );
5749
- function assertPipelinePayload(payload) {
5750
- if (!payload.stepIds?.length) {
5751
- throw new Error("Cannot trigger execute-pipeline: stepIds is empty");
5752
- }
5753
- }
5754
- async function triggerPipeline(payload) {
5755
- assertPipelinePayload(payload);
5756
- return tasks.trigger(
5757
- "execute-pipeline",
5758
- payload
5759
- );
5760
- }
5761
-
5762
- // ../jobs/src/github-changed-files.ts
5763
5748
  var GITHUB_API = "https://api.github.com";
5764
- async function fetchCompareChangedPaths(repoFullName, baseSha, headSha, token) {
5765
- if (baseSha === headSha) return [];
5766
- const res = await fetch(
5767
- `${GITHUB_API}/repos/${repoFullName}/compare/${baseSha}...${headSha}`,
5768
- {
5769
- headers: {
5770
- Accept: "application/vnd.github.v3+json",
5771
- Authorization: `Bearer ${token}`
5772
- }
5773
- }
5774
- );
5775
- if (!res.ok) {
5776
- const body = await res.text().catch(() => "");
5777
- throw new Error(
5778
- `GitHub compare ${baseSha.slice(0, 8)}...${headSha.slice(0, 8)} failed (${res.status}): ${body.slice(0, 200)}`
5779
- );
5780
- }
5781
- const detail = await res.json();
5782
- return (detail.files ?? []).map((file) => file.filename).filter((name) => Boolean(name));
5783
- }
5784
- function globToRegex(pattern) {
5785
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*");
5786
- return new RegExp(`^${escaped}$`);
5787
- }
5788
- function matchesPathPatterns(changedPaths, patterns) {
5789
- if (patterns.length === 0) return false;
5790
- const regexes = patterns.map(globToRegex);
5791
- return changedPaths.some((path) => regexes.some((regex) => regex.test(path)));
5792
- }
5793
- function parseScriptIdList(value) {
5794
- if (!Array.isArray(value)) return [];
5795
- return value.filter((item) => typeof item === "string");
5796
- }
5797
- async function fetchDeployRetryScripts(stageId) {
5798
- const rows = await getDb().execute(
5799
- sql`SELECT deploy_retry_scripts FROM release_profile_stage WHERE id = ${stageId} LIMIT 1`
5800
- );
5801
- return parseScriptIdList(rows[0]?.deploy_retry_scripts);
5802
- }
5803
- async function fetchLastSuccessfulScriptDeploySha(stageId, scriptId) {
5804
- const rows = await getDb().execute(
5805
- sql`SELECT r.trigger_sha
5806
- FROM release_step rs
5807
- JOIN release r ON r.id = rs.release_id
5808
- WHERE r.release_profile_stage_id = ${stageId}
5809
- AND rs.step_type = 'run_ssh_script'
5810
- AND rs.step_config->>'scriptId' = ${scriptId}
5811
- AND rs.status = 'success'
5812
- AND r.trigger_sha IS NOT NULL
5813
- ORDER BY rs.finished_at DESC NULLS LAST
5814
- LIMIT 1`
5815
- );
5816
- return rows[0]?.trigger_sha ?? null;
5817
- }
5818
-
5819
- // ../jobs/src/release-pipeline-config.ts
5820
- function shouldRunSshScript(script, context) {
5821
- if (script.always) return true;
5822
- if (!script.paths?.length) return false;
5823
- if (context.runAllConditional) return true;
5824
- if (context.latchedScriptIds?.includes(script.id)) return true;
5825
- const comparePaths = context.compareChangedPathsByScriptId?.[script.id];
5826
- if (comparePaths === null) return true;
5827
- if (comparePaths !== void 0) {
5828
- if (!comparePaths.length) return false;
5829
- return matchesPathPatterns(comparePaths, script.paths);
5830
- }
5831
- if (!context.changedPaths?.length) return false;
5832
- return matchesPathPatterns(context.changedPaths, script.paths);
5833
- }
5834
- function buildSkipReason(script) {
5835
- const patterns = script.paths?.join(", ") ?? "configured paths";
5836
- return `Skipped: no matching file changes (${patterns})`;
5837
- }
5838
- function parseReleasePipelineConfig(value) {
5839
- if (!value || typeof value !== "object") return null;
5840
- const config = value;
5841
- const hasSshScripts = Array.isArray(config.sshScripts) && config.sshScripts.length > 0;
5842
- const hasMigration = config.migration != null && typeof config.migration === "object";
5843
- const hasNpmPublish = Array.isArray(config.npmPublish) && config.npmPublish.length > 0;
5844
- if (!hasSshScripts && !hasMigration && !hasNpmPublish) return null;
5845
- return config;
5846
- }
5847
- function injectSshScriptSteps(baseSteps, config, context = {}) {
5848
- const scripts = config?.sshScripts;
5849
- if (!scripts?.length) return baseSteps;
5850
- const result = [];
5851
- for (const step of baseSteps) {
5852
- result.push(step);
5853
- for (const script of scripts) {
5854
- if (script.after !== step.type) continue;
5855
- const run = shouldRunSshScript(script, context);
5856
- result.push({
5857
- type: "run_ssh_script",
5858
- label: script.label,
5859
- stepConfig: {
5860
- scriptId: script.id,
5861
- script: script.script
5862
- },
5863
- initialStatus: run ? "pending" : "skipped",
5864
- skipReason: run ? void 0 : buildSkipReason(script)
5865
- });
5866
- }
5867
- }
5868
- return result;
5869
- }
5870
- function parseDeployRetryScripts(value) {
5871
- if (!Array.isArray(value)) return [];
5872
- return value.filter((item) => typeof item === "string");
5873
- }
5874
- async function resolveBuildStepListOptions(args2) {
5875
- const options = {};
5876
- const pipelineConfig = parseReleasePipelineConfig(
5877
- args2.profile.release_pipeline_config
5878
- );
5879
- if (!pipelineConfig?.sshScripts?.length) {
5880
- return options;
5881
- }
5882
- const latchedFromStage = args2.stageId ? await fetchDeployRetryScripts(args2.stageId) : parseDeployRetryScripts(args2.deployRetryScripts);
5883
- options.latchedScriptIds = latchedFromStage;
5884
- if (args2.isManual) {
5885
- options.runAllConditional = true;
5886
- return options;
5887
- }
5888
- if (!args2.triggerSha || !args2.profile.repo_full_name || !args2.githubToken) {
5889
- return options;
5890
- }
5891
- const compareChangedPathsByScriptId = {};
5892
- const conditionalScripts = pipelineConfig.sshScripts.filter(
5893
- (script) => !script.always && script.paths?.length
5894
- );
5895
- await Promise.all(
5896
- conditionalScripts.map(async (script) => {
5897
- if (latchedFromStage.includes(script.id)) return;
5898
- const stageId = args2.stageId;
5899
- const lastSuccessSha = stageId != null ? await fetchLastSuccessfulScriptDeploySha(stageId, script.id) : null;
5900
- if (!lastSuccessSha) {
5901
- compareChangedPathsByScriptId[script.id] = null;
5902
- return;
5903
- }
5904
- try {
5905
- compareChangedPathsByScriptId[script.id] = await fetchCompareChangedPaths(
5906
- args2.profile.repo_full_name,
5907
- lastSuccessSha,
5908
- args2.triggerSha,
5909
- args2.githubToken
5910
- );
5911
- } catch (err) {
5912
- console.warn(
5913
- `[release] compare failed for script ${script.id} (${args2.profile.repo_full_name}@${args2.triggerSha?.slice(0, 8)}):`,
5914
- err
5915
- );
5916
- compareChangedPathsByScriptId[script.id] = [];
5917
- }
5918
- })
5919
- );
5920
- options.compareChangedPathsByScriptId = compareChangedPathsByScriptId;
5921
- return options;
5922
- }
5923
-
5924
- // ../jobs/src/pm2-deploy-script.ts
5925
- var MG_DASHBOARD_REPO = "MGSoftwareBV/mg-dashboard";
5926
- var MG_BOILER_REPO = "MGSoftwareBV/mg-boiler";
5927
- function isBoilerPm2Build(buildFilters) {
5928
- return buildFilters.includes("@mgboiler/");
5929
- }
5930
- function shouldRunConventionTests(profile, ctx) {
5931
- const repo = profile.repo_full_name;
5932
- if (repo === MG_DASHBOARD_REPO || repo === MG_BOILER_REPO) return true;
5933
- return isBoilerPm2Build(ctx.buildFilters);
5934
- }
5935
- function npmPublishWatchPaths(config) {
5936
- if (config.paths?.length) return config.paths;
5937
- const dir = config.path.replace(/\/+$/, "");
5938
- return [`${dir}/**`];
5939
- }
5940
-
5941
- // ../jobs/src/build-step-list.ts
5942
- function buildStepList(stage, profile, options) {
5943
- const steps = [];
5944
- const dockerApps = (stage.stage_apps ?? []).filter(
5945
- (a) => a.enabled && a.deployMethod === "docker"
5946
- );
5947
- for (const _app of dockerApps) {
5948
- steps.push({ type: "deploy", label: "Deploy (Docker)" });
5949
- }
5950
- const pm2Apps = (stage.stage_apps ?? []).filter(
5951
- (a) => a.enabled && a.deployMethod === "pm2"
5952
- );
5953
- const pm2Servers = new Set(
5954
- pm2Apps.map((a) => a.deploymentServerId).filter(Boolean)
5955
- );
5956
- const boilerFilters = pm2Apps.map((a) => a.path ?? "").map((p) => p.match(/apps\/(backoffice|portal|web)/)?.[0]).filter((m) => Boolean(m)).map((m) => `--filter=@mgboiler/${m.replace("apps/", "")}`).join(" ");
5957
- const runConventions = profile?.repo_full_name != null && shouldRunConventionTests(
5958
- { repo_full_name: profile.repo_full_name },
5959
- { buildFilters: boilerFilters }
5960
- );
5961
- if (runConventions) {
5962
- for (const _server of pm2Servers) {
5963
- steps.push({ type: "run_conventions", label: "Convention tests" });
5964
- }
5965
- }
5966
- for (const _server of pm2Servers) {
5967
- steps.push({ type: "build_pm2", label: "Build (PM2)" });
5968
- }
5969
- if (stage.migrate_db) {
5970
- steps.push({ type: "migrate_db", label: "Apply Migrations" });
5971
- }
5972
- if (profile?.has_trigger_dev) {
5973
- steps.push({ type: "deploy_trigger_jobs" });
5974
- }
5975
- for (const _server of pm2Servers) {
5976
- steps.push({ type: "reload_pm2", label: "Reload PM2" });
5977
- }
5978
- const pipelineConfig = parseReleasePipelineConfig(
5979
- profile?.release_pipeline_config
5980
- );
5981
- for (const pkg of pipelineConfig?.npmPublish ?? []) {
5982
- if (!pkg.id || !pkg.path) continue;
5983
- steps.push({
5984
- type: "publish_npm",
5985
- label: pkg.label ?? `Publish npm (${pkg.path})`,
5986
- stepConfig: {
5987
- packageId: pkg.id,
5988
- packagePath: pkg.path,
5989
- watchPaths: npmPublishWatchPaths(pkg),
5990
- tokenEnvVar: pkg.tokenEnvVar
5991
- }
5992
- });
5993
- }
5994
- const withScripts = injectSshScriptSteps(steps, pipelineConfig, {
5995
- changedPaths: options?.changedPaths,
5996
- runAllConditional: options?.runAllConditional,
5997
- latchedScriptIds: options?.latchedScriptIds,
5998
- compareChangedPathsByScriptId: options?.compareChangedPathsByScriptId
5999
- });
6000
- if (stage.stage === "prod") {
6001
- withScripts.push({
6002
- type: "update_release_branch",
6003
- label: "Fast-forward main"
6004
- });
6005
- }
6006
- return withScripts;
6007
- }
6008
- async function insertReleaseSteps(releaseId, steps) {
6009
- const db2 = getDb();
6010
- const stepIds = [];
6011
- for (let i = 0; i < steps.length; i++) {
6012
- const step = steps[i];
6013
- const status = step.initialStatus ?? "pending";
6014
- const logs = status === "skipped" && step.skipReason ? step.skipReason : null;
6015
- const stepConfigJson = step.stepConfig ? JSON.stringify(step.stepConfig) : null;
6016
- const stepRows = await db2.execute(
6017
- stepConfigJson ? sql`INSERT INTO release_step (
6018
- release_id, step_type, step_order, status, step_label, step_config, logs
6019
- ) VALUES (
6020
- ${releaseId}, ${step.type}, ${i + 1}, ${status}, ${step.label ?? null},
6021
- ${stepConfigJson}::jsonb, ${logs}
6022
- )
6023
- RETURNING id` : sql`INSERT INTO release_step (
6024
- release_id, step_type, step_order, status, step_label, logs
6025
- ) VALUES (
6026
- ${releaseId}, ${step.type}, ${i + 1}, ${status}, ${step.label ?? null}, ${logs}
6027
- )
6028
- RETURNING id`
6029
- );
6030
- const id = stepRows[0]?.id;
6031
- if (id) stepIds.push(id);
6032
- }
6033
- return stepIds;
6034
- }
6035
- var ACTIVE_STATUSES = [
6036
- "pending",
6037
- "pending_approval",
6038
- "reviewing",
6039
- "running"
6040
- ];
6041
- async function cancelReleaseRow(releaseId, reason, allowedStatuses) {
6042
- const db2 = getDb();
6043
- const now = (/* @__PURE__ */ new Date()).toISOString();
6044
- const releaseResult = await db2.execute(sql`
6045
- UPDATE release
6046
- SET status = 'cancelled',
6047
- finished_at = ${now},
6048
- error_message = ${reason},
6049
- updated_at = ${now}
6050
- WHERE id = ${releaseId}
6051
- AND status IN ${allowedStatuses}
6052
- RETURNING id
6053
- `);
6054
- if (releaseResult.length === 0) return false;
6055
- await db2.execute(sql`
6056
- UPDATE release_step
6057
- SET status = 'skipped'
6058
- WHERE release_id = ${releaseId}
6059
- AND status = 'pending'
6060
- `);
6061
- return true;
6062
- }
6063
- async function cancelActiveReleasesForStage(stageId, reason) {
6064
- const db2 = getDb();
6065
- const active = await db2.execute(sql`
6066
- SELECT id FROM release
6067
- WHERE release_profile_stage_id = ${stageId}
6068
- AND status IN ${ACTIVE_STATUSES}
6069
- `);
6070
- const cancelled = [];
6071
- for (const row of active) {
6072
- if (await cancelReleaseRow(row.id, reason, ACTIVE_STATUSES)) {
6073
- cancelled.push(row.id);
6074
- }
6075
- }
6076
- return cancelled;
6077
- }
6078
- function isPostgresUniqueViolation(err) {
6079
- return typeof err === "object" && err !== null && "code" in err && err.code === "23505";
6080
- }
6081
- async function resolveUserDisplayName(userId) {
6082
- const rows = await getDb().execute(sql`
6083
- SELECT full_name, email
6084
- FROM "user"
6085
- WHERE id = ${userId}
6086
- LIMIT 1
6087
- `);
6088
- const user = rows[0];
6089
- if (!user) return null;
6090
- const fullName = user.full_name?.trim();
6091
- return fullName || user.email || null;
6092
- }
6093
-
6094
- // ../jobs/src/create-release-for-stage.ts
6095
- var GITHUB_API2 = "https://api.github.com";
6096
- var CreateReleaseError = class extends Error {
6097
- constructor(message, code = "INTERNAL_SERVER_ERROR") {
6098
- super(message);
6099
- this.code = code;
6100
- this.name = "CreateReleaseError";
6101
- }
6102
- };
6103
- async function resolveBranchHeadSha(repoFullName, branch, token) {
6104
- const headers = {
6105
- Accept: "application/vnd.github.v3+json",
6106
- Authorization: `Bearer ${token}`
6107
- };
6108
- const refRes = await fetch(
6109
- `${GITHUB_API2}/repos/${repoFullName}/git/ref/heads/${branch}`,
6110
- { headers }
6111
- );
6112
- if (!refRes.ok) {
6113
- throw new CreateReleaseError(
6114
- `Could not find branch ${branch} to deploy from`,
6115
- "BAD_REQUEST"
6116
- );
6117
- }
6118
- const refData = await refRes.json();
6119
- const sha = refData.object?.sha;
6120
- if (!sha) {
6121
- throw new CreateReleaseError(
6122
- `Could not determine SHA for branch ${branch}`,
6123
- "INTERNAL_SERVER_ERROR"
6124
- );
6125
- }
6126
- return sha;
6127
- }
6128
- async function createReleaseForStage(userId, params) {
6129
- const triggerType = params.triggerType ?? "manual";
6130
- const db2 = getDb();
6131
- const stageRows = await db2.execute(
6132
- sql`SELECT * FROM release_profile_stage WHERE id = ${params.stageId} LIMIT 1`
6133
- );
6134
- const stage = stageRows[0];
6135
- if (!stage) {
6136
- throw new CreateReleaseError("Stage not found", "NOT_FOUND");
6137
- }
6138
- const profileRows = await db2.execute(
6139
- sql`SELECT * FROM release_profile WHERE id = ${stage.release_profile_id} LIMIT 1`
6140
- );
6141
- const profile = profileRows[0];
6142
- if (!profile) {
6143
- throw new CreateReleaseError("Release profile not found", "NOT_FOUND");
6144
- }
6145
- if (!profile.enabled || !stage.enabled) {
6146
- throw new CreateReleaseError(
6147
- "Profile or stage is disabled",
6148
- "BAD_REQUEST"
6149
- );
6150
- }
6151
- const deployBranch = stage.trigger_branch?.trim() || stage.release_branch?.trim() || null;
6152
- if (!deployBranch) {
6153
- throw new CreateReleaseError(
6154
- "Stage has no release or trigger branch configured",
6155
- "BAD_REQUEST"
6156
- );
6157
- }
6158
- const { token } = await resolveReleaseGitHubToken(
6159
- profile.github_token_id ?? null
6160
- );
6161
- if (!token) {
6162
- throw new CreateReleaseError(
6163
- "GitHub token is not configured",
6164
- "PRECONDITION_FAILED"
6165
- );
6166
- }
6167
- const triggerSha = await resolveBranchHeadSha(
6168
- profile.repo_full_name,
6169
- deployBranch,
6170
- token
6171
- );
6172
- if (params.skipIfActiveSameSha) {
6173
- const existing = await db2.execute(sql`
6174
- SELECT
6175
- r.id,
6176
- r.status,
6177
- r.version,
6178
- r.trigger_author_name,
6179
- COUNT(rs.id)::int AS steps_count
6180
- FROM release r
6181
- LEFT JOIN release_step rs ON rs.release_id = r.id
6182
- WHERE r.release_profile_stage_id = ${params.stageId}
6183
- AND r.trigger_sha = ${triggerSha}
6184
- AND r.status IN ('pending', 'pending_approval', 'running', 'reviewing', 'success')
6185
- GROUP BY r.id
6186
- ORDER BY r.created_at DESC
6187
- LIMIT 1
6188
- `);
6189
- if (existing[0]) {
6190
- return {
6191
- id: existing[0].id,
6192
- status: existing[0].status ?? "running",
6193
- version: existing[0].version ?? null,
6194
- stepsCount: Number(existing[0].steps_count ?? 0),
6195
- authorName: existing[0].trigger_author_name ?? null
6196
- };
6197
- }
6198
- }
6199
- await cancelActiveReleasesForStage(
6200
- params.stageId,
6201
- "Superseded by newer manual trigger"
6202
- );
6203
- let triggerAuthorName = params.triggerAuthorName?.trim() || null;
6204
- if (!triggerAuthorName && userId) {
6205
- triggerAuthorName = await resolveUserDisplayName(userId);
6206
- }
6207
- const enabledSteps = buildStepList(
6208
- stage,
6209
- profile,
6210
- await resolveBuildStepListOptions({
6211
- profile,
6212
- triggerSha,
6213
- githubToken: token,
6214
- isManual: triggerType === "manual" || triggerType === "mcp",
6215
- stageId: params.stageId,
6216
- deployRetryScripts: stage.deploy_retry_scripts
6217
- })
6218
- );
6219
- if (enabledSteps.length === 0) {
6220
- throw new CreateReleaseError(
6221
- "No pipeline steps configured for this stage (enable a PM2/Docker stage_app or deployment_project_server)",
6222
- "PRECONDITION_FAILED"
6223
- );
6224
- }
6225
- const initialStatus = "running";
6226
- let release;
6227
- try {
6228
- const inserted = await db2.execute(sql`
6229
- INSERT INTO release (
6230
- release_profile_id, release_profile_stage_id, status, started_at,
6231
- version, version_type, trigger_type, skip_pr_flow, created_by,
6232
- trigger_kind, trigger_branch, trigger_sha, source_commit,
6233
- trigger_author_name
6234
- ) VALUES (
6235
- ${profile.id}, ${params.stageId}, ${initialStatus}, ${(/* @__PURE__ */ new Date()).toISOString()},
6236
- NULL, ${params.versionType}, ${triggerType}, ${params.skipPrFlow}, ${userId},
6237
- 'branch', ${deployBranch}, ${triggerSha}, ${triggerSha},
6238
- ${triggerAuthorName}
6239
- )
6240
- RETURNING *
6241
- `);
6242
- if (!inserted[0]) {
6243
- throw new Error("INSERT did not return a row");
6244
- }
6245
- release = inserted[0];
6246
- } catch (err) {
6247
- if (isPostgresUniqueViolation(err)) {
6248
- throw new CreateReleaseError(
6249
- "A release is already running for this stage.",
6250
- "CONFLICT"
6251
- );
6252
- }
6253
- throw new CreateReleaseError(
6254
- `Failed to create release: ${err instanceof Error ? err.message : String(err)}`,
6255
- "INTERNAL_SERVER_ERROR"
6256
- );
6257
- }
6258
- const stepIds = [];
6259
- try {
6260
- stepIds.push(...await insertReleaseSteps(release.id, enabledSteps));
6261
- } catch (err) {
6262
- const msg = err instanceof Error ? err.message : String(err);
6263
- await db2.execute(sql`
6264
- UPDATE release
6265
- SET status = 'failed',
6266
- error_message = ${`Failed to create steps: ${msg}`},
6267
- finished_at = ${(/* @__PURE__ */ new Date()).toISOString()}
6268
- WHERE id = ${release.id}
6269
- `);
6270
- throw new CreateReleaseError(
6271
- `Failed to create release steps: ${msg}`,
6272
- "INTERNAL_SERVER_ERROR"
6273
- );
6274
- }
6275
- try {
6276
- await triggerPipeline({
6277
- releaseId: release.id,
6278
- profileId: profile.id,
6279
- stageId: stage.id,
6280
- stepIds,
6281
- version: null
6282
- });
6283
- } catch (err) {
6284
- const msg = err instanceof Error ? err.message : String(err);
6285
- await db2.execute(sql`
6286
- UPDATE release
6287
- SET status = 'failed',
6288
- error_message = ${`Failed to start pipeline: ${msg}`},
6289
- finished_at = ${(/* @__PURE__ */ new Date()).toISOString()}
6290
- WHERE id = ${release.id}
6291
- `);
6292
- throw new CreateReleaseError(
6293
- `Failed to start pipeline: ${msg}`,
6294
- "INTERNAL_SERVER_ERROR"
6295
- );
6296
- }
6297
- return {
6298
- id: release.id,
6299
- status: initialStatus,
6300
- version: null,
6301
- stepsCount: enabledSteps.length,
6302
- authorName: triggerAuthorName
6303
- };
6304
- }
6305
- var GITHUB_API3 = "https://api.github.com";
6306
5749
  var ReleaseProfileQuickCreateError = class extends Error {
6307
5750
  constructor(message, code = "INTERNAL_SERVER_ERROR") {
6308
5751
  super(message);
@@ -6330,7 +5773,7 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
6330
5773
  };
6331
5774
  async function fetchContents(path) {
6332
5775
  const res = await fetch(
6333
- `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
5776
+ `${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
6334
5777
  { headers }
6335
5778
  );
6336
5779
  if (!res.ok) return null;
@@ -6339,7 +5782,7 @@ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
6339
5782
  }
6340
5783
  async function fileExists(path) {
6341
5784
  const res = await fetch(
6342
- `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
5785
+ `${GITHUB_API}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
6343
5786
  { headers, method: "HEAD" }
6344
5787
  );
6345
5788
  return res.ok;
@@ -6478,20 +5921,20 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
6478
5921
  "Content-Type": "application/json"
6479
5922
  };
6480
5923
  const checkRes = await fetch(
6481
- `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${branchName}`,
5924
+ `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${branchName}`,
6482
5925
  { headers }
6483
5926
  );
6484
5927
  if (checkRes.ok) return;
6485
5928
  let sha;
6486
5929
  const sourceRes = await fetch(
6487
- `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
5930
+ `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
6488
5931
  { headers }
6489
5932
  );
6490
5933
  if (sourceRes.ok) {
6491
5934
  const data = await sourceRes.json();
6492
5935
  sha = data.object?.sha;
6493
5936
  } else {
6494
- const repoRes = await fetch(`${GITHUB_API3}/repos/${repoFullName}`, {
5937
+ const repoRes = await fetch(`${GITHUB_API}/repos/${repoFullName}`, {
6495
5938
  headers
6496
5939
  });
6497
5940
  if (!repoRes.ok) {
@@ -6500,7 +5943,7 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
6500
5943
  const repo = await repoRes.json();
6501
5944
  const defaultBranch = repo.default_branch ?? "main";
6502
5945
  const defaultRes = await fetch(
6503
- `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
5946
+ `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
6504
5947
  { headers }
6505
5948
  );
6506
5949
  if (!defaultRes.ok) {
@@ -6513,7 +5956,7 @@ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, gi
6513
5956
  throw new Error(`Could not determine SHA to create branch ${branchName}`);
6514
5957
  }
6515
5958
  const createRes = await fetch(
6516
- `${GITHUB_API3}/repos/${repoFullName}/git/refs`,
5959
+ `${GITHUB_API}/repos/${repoFullName}/git/refs`,
6517
5960
  {
6518
5961
  method: "POST",
6519
5962
  headers,
@@ -6976,7 +6419,6 @@ var TOOL_MODULE_MAP = {
6976
6419
  "docker-logs": "ssh_servers",
6977
6420
  "docker-exec": "ssh_servers",
6978
6421
  "docker-compose": "ssh_servers",
6979
- "wait-for": "ssh_servers",
6980
6422
  "db-discover": "ssh_servers",
6981
6423
  "db-tables": "ssh_servers",
6982
6424
  "db-query": "ssh_servers",
@@ -6986,10 +6428,8 @@ var TOOL_MODULE_MAP = {
6986
6428
  "env-list": "ci_cd",
6987
6429
  "env-get": "ci_cd",
6988
6430
  "env-store": "ci_cd",
6989
- "release-trigger": "ci_cd",
6990
6431
  "release-profile-list": "ci_cd",
6991
- "release-profile-create": "ci_cd",
6992
- "release-profile-update": "ci_cd",
6432
+ "release-profile": "ci_cd",
6993
6433
  "domain-list": "domains",
6994
6434
  "dns-list": "domains",
6995
6435
  "dns-record": "domains",
@@ -9551,11 +8991,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
9551
8991
  applied_by TEXT
9552
8992
  );
9553
8993
  `.trim();
9554
- function normaliseMigrationSql(sql29) {
9555
- return sql29.replace(/\r\n/g, "\n").trim() + "\n";
8994
+ function normaliseMigrationSql(sql23) {
8995
+ return sql23.replace(/\r\n/g, "\n").trim() + "\n";
9556
8996
  }
9557
- function migrationSha256(sql29) {
9558
- return createHash("sha256").update(normaliseMigrationSql(sql29), "utf8").digest("hex");
8997
+ function migrationSha256(sql23) {
8998
+ return createHash("sha256").update(normaliseMigrationSql(sql23), "utf8").digest("hex");
9559
8999
  }
9560
9000
  function dollarQuoteTag(value) {
9561
9001
  let tag = "_mcp";
@@ -10295,48 +9735,6 @@ var TOOLS = [
10295
9735
  required: ["serverId", "projectPath", "action"]
10296
9736
  }
10297
9737
  },
10298
- {
10299
- name: "wait-for",
10300
- description: 'Poll until a condition holds (or timeout). Replaces 30+ ad-hoc loops the LLM would otherwise build itself. Four condition types:\n- `url`: `target: "https://..."`, `until: 200` (HTTP status, default 200). 10s per probe.\n- `container-health`: `serverId`, `target: "container-name"`, `until: "healthy" | "running" | "exited"` (default "healthy"). Uses `docker inspect`.\n- `file-exists`: `serverId`, `target: "/path/to/file"` \u2014 SSH stat check.\n- `sftp-exists`: same as file-exists but with `bucket: "name"` instead of serverId for R2.\n\nReturns a per-attempt trail so you can see how long it took to converge.',
10301
- inputSchema: {
10302
- type: "object",
10303
- properties: {
10304
- type: {
10305
- type: "string",
10306
- enum: ["url", "container-health", "file-exists", "sftp-exists"],
10307
- description: "Condition type to wait on."
10308
- },
10309
- target: {
10310
- type: "string",
10311
- description: "Target identifier: URL, container name, file path, or R2 key (depends on type)."
10312
- },
10313
- until: {
10314
- description: 'Expected end-state (depends on type): for url an HTTP status (default 200), for container-health a state string (default "healthy"). Omit for file-exists/sftp-exists.'
10315
- },
10316
- serverId: {
10317
- type: "string",
10318
- description: "SSH server UUID (required for container-health and file-exists)."
10319
- },
10320
- bucket: {
10321
- type: "string",
10322
- description: "R2 bucket name (used with sftp-exists when checking an R2 object)."
10323
- },
10324
- path: {
10325
- type: "string",
10326
- description: "Alias for `target` for file-exists/sftp-exists."
10327
- },
10328
- intervalSeconds: {
10329
- type: "number",
10330
- description: "Poll interval in seconds (default 3, max 60)."
10331
- },
10332
- timeout: {
10333
- type: "number",
10334
- description: "Total deadline in seconds (default 120, max 1800)."
10335
- }
10336
- },
10337
- required: ["type", "target"]
10338
- }
10339
- },
10340
9738
  {
10341
9739
  name: "db-discover",
10342
9740
  description: 'Discover databases available on an SSH server. Use this first to find what is reachable before running other db-* tools.\n\nModes (pass via `include` array, default `["www"]`):\n- `"www"` \u2014 scan `/var/www` for WordPress / PrestaShop / Laravel / generic .env apps. Returns sitePath, db name, user, host, port.\n- `"postgres-containers"` \u2014 enumerate running Docker containers that expose `POSTGRES_USER` / `POSTGRES_DB` env vars (vanilla `postgres:*` / `pgvector/*` images). Returns container name, db, user, host port mapping, and whether a password is set. Use the returned (container, db, user) tuple directly with `db-query containerName=\u2026` or `db-apply-migration`.\n\nPass multiple modes to scan in parallel: `{ include: ["www", "postgres-containers"] }`.',
@@ -10595,33 +9993,6 @@ var TOOLS = [
10595
9993
  required: ["appName", "environment", "content"]
10596
9994
  }
10597
9995
  },
10598
- {
10599
- name: "release-trigger",
10600
- description: "Start a release/deploy pipeline for a release profile stage. Attributes the deploy to the MCP API key owner. Requires ci_cd permission.",
10601
- inputSchema: {
10602
- type: "object",
10603
- properties: {
10604
- releaseProfile: {
10605
- type: "string",
10606
- description: "Release profile name (usually matches the project folder name or git repo name, e.g. mg-dashboard)."
10607
- },
10608
- stage: {
10609
- type: "string",
10610
- description: "Stage slug or name (e.g. dev, production). Required when the profile has multiple enabled stages."
10611
- },
10612
- versionType: {
10613
- type: "string",
10614
- enum: ["major", "minor", "patch"],
10615
- description: "Semver bump type when versioning applies (default: minor)."
10616
- },
10617
- skipPrFlow: {
10618
- type: "boolean",
10619
- description: "Skip PR approval flow (default: false)."
10620
- }
10621
- },
10622
- required: ["releaseProfile"]
10623
- }
10624
- },
10625
9996
  {
10626
9997
  name: "release-profile-list",
10627
9998
  description: "List all release profiles with repo, enabled state, release type, GitHub key and a summary of their stages. Requires ci_cd permission.",
@@ -10632,67 +10003,55 @@ var TOOLS = [
10632
10003
  }
10633
10004
  },
10634
10005
  {
10635
- name: "release-profile-create",
10636
- 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.",
10006
+ name: "release-profile",
10007
+ description: 'Create or update a release profile. Pick the operation with `action`:\n- "create": one-step creation \u2014 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. Required: repoFullName. Optional: githubKey, name, releaseType. Assign deployment servers afterwards in the dashboard (Releases \u2192 Profiles).\n- "update": change settings of an existing profile located by `releaseProfile` (name or repo). Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Required: releaseProfile. Optional: name, enabled, releaseType, githubKey, useChangesBranch, hasTriggerDev, workDirectory.\n\nRequires ci_cd permission.',
10637
10008
  inputSchema: {
10638
10009
  type: "object",
10639
10010
  properties: {
10011
+ action: {
10012
+ type: "string",
10013
+ enum: ["create", "update"],
10014
+ description: "Which operation to perform."
10015
+ },
10640
10016
  repoFullName: {
10641
10017
  type: "string",
10642
- description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard).'
10018
+ description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard). Required for action="create".'
10643
10019
  },
10644
- githubKey: {
10020
+ releaseProfile: {
10645
10021
  type: "string",
10646
- 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.'
10022
+ description: 'Profile name or repo full name to locate the profile. Required for action="update".'
10647
10023
  },
10648
10024
  name: {
10649
10025
  type: "string",
10650
- description: 'Profile name override. Default: title-cased repo name (mg-dashboard \u2192 "Mg Dashboard").'
10026
+ description: 'Profile name. For create: name override (default: title-cased repo name, e.g. mg-dashboard \u2192 "Mg Dashboard"). For update: new profile name.'
10651
10027
  },
10652
- releaseType: {
10653
- type: "string",
10654
- enum: ["dev_only", "prod_only", "both"],
10655
- description: "Which stages to create (default: both = dev + prod)."
10656
- }
10657
- },
10658
- required: ["repoFullName"]
10659
- }
10660
- },
10661
- {
10662
- name: "release-profile-update",
10663
- 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.",
10664
- inputSchema: {
10665
- type: "object",
10666
- properties: {
10667
- releaseProfile: {
10668
- type: "string",
10669
- description: "Profile name or repo full name to locate the profile."
10028
+ enabled: {
10029
+ type: "boolean",
10030
+ description: "Enable/disable the profile (update only)."
10670
10031
  },
10671
- name: { type: "string", description: "New profile name." },
10672
- enabled: { type: "boolean", description: "Enable/disable the profile." },
10673
10032
  releaseType: {
10674
10033
  type: "string",
10675
10034
  enum: ["dev_only", "prod_only", "both"],
10676
- description: "New release type \u2014 reconfigures stages accordingly."
10035
+ description: "Which stages exist (default for create: both = dev + prod). Changing it on update reconfigures the stages."
10677
10036
  },
10678
10037
  githubKey: {
10679
10038
  type: "string",
10680
- description: 'Named GitHub key (github_token label or owner). Pass "default" to reset to the default MG Software token.'
10039
+ 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. On update, pass "default" to reset to the default token.'
10681
10040
  },
10682
10041
  useChangesBranch: {
10683
10042
  type: "boolean",
10684
- description: "Use a long-lived -changes accumulation branch."
10043
+ description: "Use a long-lived -changes accumulation branch (update only)."
10685
10044
  },
10686
10045
  hasTriggerDev: {
10687
10046
  type: "boolean",
10688
- description: "Deploy Trigger.dev jobs during production releases."
10047
+ description: "Deploy Trigger.dev jobs during production releases (update only)."
10689
10048
  },
10690
10049
  workDirectory: {
10691
10050
  type: "string",
10692
- description: 'Working directory within the repo (e.g. ".").'
10051
+ description: 'Working directory within the repo (e.g. "."). Update only.'
10693
10052
  }
10694
10053
  },
10695
- required: ["releaseProfile"]
10054
+ required: ["action"]
10696
10055
  }
10697
10056
  },
10698
10057
  {
@@ -10819,6 +10178,24 @@ var TOOLS = [
10819
10178
  required: ["query"]
10820
10179
  }
10821
10180
  },
10181
+ {
10182
+ name: "get-team-memory",
10183
+ description: "Fetch the FULL stored text of past conversation(s) found via search-team-memory (pass the `id` from its results). search-team-memory returns only snippets; this returns the distilled problem\u2192solution transcript so you can reuse the actual fix/approach/code. Call this on the 1-2 most relevant hits before re-solving from scratch.",
10184
+ inputSchema: {
10185
+ type: "object",
10186
+ properties: {
10187
+ id: {
10188
+ type: "string",
10189
+ description: "A memory id from a search-team-memory result."
10190
+ },
10191
+ ids: {
10192
+ type: "array",
10193
+ items: { type: "string" },
10194
+ description: "Optional: up to 5 memory ids to fetch in one call."
10195
+ }
10196
+ }
10197
+ }
10198
+ },
10822
10199
  // ----- Trigger.dev -----
10823
10200
  ...TRIGGER_TOOLS
10824
10201
  ];
@@ -10936,19 +10313,81 @@ async function executeToolCall(name, a, _serverId) {
10936
10313
  const repo = hit.repo ?? "unknown repo";
10937
10314
  const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
10938
10315
  return `${index5 + 1}. [${repo} \xB7 ${when}${sim}] ${hit.title}
10316
+ id: ${hit.id}
10939
10317
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
10940
10318
  });
10941
10319
  return {
10942
10320
  content: [
10943
10321
  {
10944
10322
  type: "text",
10945
- text: `Found ${data.count} prior conversation(s) (${data.mode} search) \u2014 reuse this before solving from scratch:
10323
+ text: `Found ${data.count} prior conversation(s) (${data.mode} search) \u2014 reuse this before solving from scratch.
10324
+ These are SNIPPETS. For any hit worth reusing, call get-team-memory with its id to read the full problem\u2192solution transcript before re-solving.
10946
10325
 
10947
10326
  ` + lines.join("\n\n")
10948
10327
  }
10949
10328
  ]
10950
10329
  };
10951
10330
  }
10331
+ // ----- Team memory: full detail -----
10332
+ case "get-team-memory": {
10333
+ const ids = [];
10334
+ if (typeof a.id === "string" && a.id.trim()) ids.push(a.id.trim());
10335
+ if (Array.isArray(a.ids)) {
10336
+ for (const value of a.ids) {
10337
+ if (typeof value === "string" && value.trim()) ids.push(value.trim());
10338
+ }
10339
+ }
10340
+ if (ids.length === 0) {
10341
+ return {
10342
+ content: [
10343
+ { type: "text", text: "Error: id (or ids) is required" }
10344
+ ]
10345
+ };
10346
+ }
10347
+ const res = await fetch(`${dashboardBaseUrl}/api/team-memory/detail`, {
10348
+ method: "POST",
10349
+ headers: {
10350
+ "content-type": "application/json",
10351
+ authorization: `Bearer ${apiKey}`
10352
+ },
10353
+ body: JSON.stringify({ ids: ids.slice(0, 5) })
10354
+ });
10355
+ if (!res.ok) {
10356
+ const detail = await res.text().catch(() => "");
10357
+ return {
10358
+ content: [
10359
+ {
10360
+ type: "text",
10361
+ text: `Error: team-memory detail failed (${res.status}). ${detail.slice(0, 300)}`
10362
+ }
10363
+ ]
10364
+ };
10365
+ }
10366
+ const data = await res.json();
10367
+ if (data.count === 0) {
10368
+ return {
10369
+ content: [
10370
+ {
10371
+ type: "text",
10372
+ text: "No team-memory entries matched the given id(s)."
10373
+ }
10374
+ ]
10375
+ };
10376
+ }
10377
+ const blocks = data.results.map((r) => {
10378
+ const when = r.lastMessageAt ? new Date(r.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
10379
+ const repo = r.repo ?? "unknown repo";
10380
+ return `## ${r.title}
10381
+ [${repo} \xB7 ${when} \xB7 ${r.messageCount} msgs \xB7 ${r.source}]
10382
+
10383
+ ` + r.content;
10384
+ });
10385
+ return {
10386
+ content: [
10387
+ { type: "text", text: blocks.join("\n\n---\n\n") }
10388
+ ]
10389
+ };
10390
+ }
10952
10391
  // ----- Servers -----
10953
10392
  case "list-servers": {
10954
10393
  const data = ctx.allowedServerIds !== null ? await db.execute(sql`
@@ -12244,163 +11683,6 @@ ${stderrOut}`);
12244
11683
  ${result.stderr}`);
12245
11684
  return { content: [{ type: "text", text: output.join("\n") }] };
12246
11685
  }
12247
- // ----- wait-for: poll until a condition holds (one tool, four types) -----
12248
- case "wait-for": {
12249
- const type = String(a.type || "");
12250
- if (!["url", "container-health", "file-exists", "sftp-exists"].includes(
12251
- type
12252
- )) {
12253
- return {
12254
- content: [
12255
- {
12256
- type: "text",
12257
- text: "Error: type must be one of: url, container-health, file-exists, sftp-exists"
12258
- }
12259
- ]
12260
- };
12261
- }
12262
- const timeoutSeconds = Math.max(
12263
- 1,
12264
- Math.min(1800, Number(a.timeout) || 120)
12265
- );
12266
- const intervalSeconds = Math.max(
12267
- 1,
12268
- Math.min(60, Number(a.intervalSeconds) || 3)
12269
- );
12270
- const deadline = Date.now() + timeoutSeconds * 1e3;
12271
- const trail = [];
12272
- const check = async () => {
12273
- if (type === "url") {
12274
- const target = String(a.target || "");
12275
- if (!target) return { ok: false, observed: "no target url" };
12276
- const expectStatus = a.until !== void 0 ? Number(a.until) : 200;
12277
- try {
12278
- const ctrl = new AbortController();
12279
- const t = setTimeout(() => ctrl.abort(), 1e4);
12280
- const res = await fetch(target, {
12281
- signal: ctrl.signal,
12282
- redirect: "manual"
12283
- });
12284
- clearTimeout(t);
12285
- const ok2 = res.status === expectStatus || expectStatus === 200 && res.status >= 200 && res.status < 300;
12286
- return { ok: ok2, observed: `HTTP ${res.status}` };
12287
- } catch (e) {
12288
- return {
12289
- ok: false,
12290
- observed: e instanceof Error ? e.message : String(e)
12291
- };
12292
- }
12293
- }
12294
- if (type === "container-health") {
12295
- const containerName = String(a.target || "").replace(
12296
- /[^a-zA-Z0-9._-]/g,
12297
- ""
12298
- );
12299
- const until = String(a.until || "healthy");
12300
- if (!containerName)
12301
- return { ok: false, observed: "no container name" };
12302
- if (!a.serverId)
12303
- return {
12304
- ok: false,
12305
- observed: "serverId is required for container-health"
12306
- };
12307
- const { conn: conn2, proxy: proxy2 } = await getServerConnection(
12308
- String(a.serverId)
12309
- );
12310
- const cmd2 = `docker inspect --format '{{json .State}}' ${containerName} 2>&1`;
12311
- const r2 = await sshExec(conn2, cmd2, proxy2);
12312
- if (r2.exitCode !== 0)
12313
- return {
12314
- ok: false,
12315
- observed: r2.stderr.trim().slice(0, 80) || "inspect failed"
12316
- };
12317
- try {
12318
- const state = JSON.parse(r2.stdout.trim());
12319
- const healthStatus = state.Health?.Status;
12320
- if (until === "healthy") {
12321
- return {
12322
- ok: healthStatus === "healthy",
12323
- observed: `Status=${state.Status} Health=${healthStatus || "no-check"}`
12324
- };
12325
- }
12326
- return {
12327
- ok: state.Status === until,
12328
- observed: `Status=${state.Status}`
12329
- };
12330
- } catch (e) {
12331
- return {
12332
- ok: false,
12333
- observed: "parse error: " + (e instanceof Error ? e.message : String(e))
12334
- };
12335
- }
12336
- }
12337
- const bucket = typeof a.bucket === "string" && a.bucket ? a.bucket : "";
12338
- const path = String(a.target || a.path || "");
12339
- if (!path) return { ok: false, observed: "no path" };
12340
- if (bucket) {
12341
- try {
12342
- await getR2Client().send(
12343
- new HeadObjectCommand({ Bucket: bucket, Key: r2Key(path) })
12344
- );
12345
- return { ok: true, observed: `r2://${bucket}/${path} exists` };
12346
- } catch {
12347
- return {
12348
- ok: false,
12349
- observed: `r2://${bucket}/${path} not found`
12350
- };
12351
- }
12352
- }
12353
- if (!a.serverId)
12354
- return {
12355
- ok: false,
12356
- observed: "serverId or bucket is required for file-exists"
12357
- };
12358
- const { conn, proxy } = await getServerConnection(String(a.serverId));
12359
- const safe = sanitizePath(path);
12360
- const cmd = `test -e ${posixQuote(safe)} && echo OK || echo MISSING`;
12361
- const r = await sshExec(conn, cmd, proxy);
12362
- const ok = r.stdout.trim() === "OK";
12363
- return { ok, observed: ok ? `${safe} exists` : `${safe} missing` };
12364
- };
12365
- let attempts = 0;
12366
- let lastObserved = "";
12367
- while (Date.now() < deadline) {
12368
- attempts++;
12369
- const r = await check();
12370
- lastObserved = r.observed;
12371
- trail.push(
12372
- `[${attempts}] ${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${r.ok ? "OK" : ".."} ${r.observed}`
12373
- );
12374
- if (r.ok) {
12375
- return {
12376
- content: [
12377
- {
12378
- type: "text",
12379
- text: `Condition met after ${attempts} attempt(s) in ${((Date.now() - (deadline - timeoutSeconds * 1e3)) / 1e3).toFixed(1)}s.
12380
- Last observation: ${r.observed}
12381
-
12382
- --- trail ---
12383
- ${trail.join("\n")}`
12384
- }
12385
- ]
12386
- };
12387
- }
12388
- if (Date.now() + intervalSeconds * 1e3 >= deadline) break;
12389
- await new Promise((res) => setTimeout(res, intervalSeconds * 1e3));
12390
- }
12391
- return {
12392
- content: [
12393
- {
12394
- type: "text",
12395
- text: `Timed out after ${timeoutSeconds}s without seeing condition (${attempts} probe(s)).
12396
- Last observation: ${lastObserved}
12397
-
12398
- --- trail ---
12399
- ${trail.join("\n")}`
12400
- }
12401
- ]
12402
- };
12403
- }
12404
11686
  // ----- Database -----
12405
11687
  case "db-discover": {
12406
11688
  const { serverId, conn, proxy } = await getServerConnection(
@@ -13276,53 +12558,6 @@ LIMIT ${limit};
13276
12558
  }
13277
12559
  return { content: [{ type: "text", text: saveMsg }] };
13278
12560
  }
13279
- case "release-trigger": {
13280
- const releaseProfile = String(a.releaseProfile);
13281
- const stageFilter = a.stage ? String(a.stage) : null;
13282
- const versionType = String(a.versionType ?? "minor");
13283
- if (versionType !== "major" && versionType !== "minor" && versionType !== "patch") {
13284
- throw new Error(
13285
- `Invalid versionType "${versionType}". Expected major, minor, or patch.`
13286
- );
13287
- }
13288
- const skipPrFlow = a.skipPrFlow === true;
13289
- const resolved = await resolveReleaseTriggerStage(
13290
- releaseProfile,
13291
- stageFilter
13292
- );
13293
- try {
13294
- const result = await createReleaseForStage(authContext.userId, {
13295
- stageId: resolved.stageId,
13296
- versionType,
13297
- skipPrFlow,
13298
- triggerType: "mcp"
13299
- });
13300
- return {
13301
- content: [
13302
- {
13303
- type: "text",
13304
- text: JSON.stringify(
13305
- {
13306
- releaseId: result.id,
13307
- status: result.status,
13308
- stepsCount: result.stepsCount,
13309
- author: result.authorName,
13310
- profile: resolved.profileName,
13311
- stage: resolved.stageName
13312
- },
13313
- null,
13314
- 2
13315
- )
13316
- }
13317
- ]
13318
- };
13319
- } catch (err) {
13320
- if (err instanceof CreateReleaseError) {
13321
- throw new Error(err.message);
13322
- }
13323
- throw err;
13324
- }
13325
- }
13326
12561
  case "release-profile-list": {
13327
12562
  const profiles = await db.execute(sql`
13328
12563
  SELECT
@@ -13370,64 +12605,74 @@ ${lines.join("\n\n")}`
13370
12605
  ]
13371
12606
  };
13372
12607
  }
13373
- case "release-profile-create": {
13374
- const repoFullName = String(a.repoFullName ?? "").trim();
13375
- const githubTokenId = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
13376
- const releaseType = a.releaseType ? String(a.releaseType) : null;
13377
- if (releaseType !== null && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
13378
- throw new Error(
13379
- `Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
13380
- );
12608
+ case "release-profile": {
12609
+ const action = String(a.action);
12610
+ if (action !== "create" && action !== "update") {
12611
+ throw new Error("action must be one of: create, update");
13381
12612
  }
13382
- try {
13383
- const result = await quickCreateReleaseProfile(
13384
- authContext.userId,
13385
- {
13386
- repoFullName,
13387
- githubTokenId,
13388
- name: a.name ? String(a.name) : null,
13389
- releaseType
13390
- }
13391
- );
13392
- return {
13393
- content: [
12613
+ if (action === "create") {
12614
+ const repoFullName = String(a.repoFullName ?? "").trim();
12615
+ if (!repoFullName) {
12616
+ throw new Error('action="create" requires repoFullName');
12617
+ }
12618
+ const githubTokenId2 = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
12619
+ const releaseType2 = a.releaseType ? String(a.releaseType) : null;
12620
+ if (releaseType2 !== null && releaseType2 !== "dev_only" && releaseType2 !== "prod_only" && releaseType2 !== "both") {
12621
+ throw new Error(
12622
+ `Invalid releaseType "${releaseType2}". Expected dev_only, prod_only, or both.`
12623
+ );
12624
+ }
12625
+ try {
12626
+ const result = await quickCreateReleaseProfile(
12627
+ authContext.userId,
13394
12628
  {
13395
- type: "text",
13396
- text: JSON.stringify(
13397
- {
13398
- profileId: result.profile.id,
13399
- name: result.profile.name,
13400
- repo: repoFullName,
13401
- releaseType: releaseType ?? "both",
13402
- appsDetected: result.scan.apps.map((app) => ({
13403
- path: app.path,
13404
- label: app.label,
13405
- deployMethod: app.deployMethod
13406
- })),
13407
- stages: result.stages.map((s) => ({
13408
- stage: s.stage,
13409
- name: s.name,
13410
- releaseBranch: s.release_branch,
13411
- triggerMode: s.trigger_mode
13412
- })),
13413
- branchWarnings: result.branchWarnings,
13414
- hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
13415
- },
13416
- null,
13417
- 2
13418
- )
12629
+ repoFullName,
12630
+ githubTokenId: githubTokenId2,
12631
+ name: a.name ? String(a.name) : null,
12632
+ releaseType: releaseType2
13419
12633
  }
13420
- ]
13421
- };
13422
- } catch (err) {
13423
- if (err instanceof ReleaseProfileQuickCreateError) {
13424
- throw new Error(err.message);
12634
+ );
12635
+ return {
12636
+ content: [
12637
+ {
12638
+ type: "text",
12639
+ text: JSON.stringify(
12640
+ {
12641
+ profileId: result.profile.id,
12642
+ name: result.profile.name,
12643
+ repo: repoFullName,
12644
+ releaseType: releaseType2 ?? "both",
12645
+ appsDetected: result.scan.apps.map((app) => ({
12646
+ path: app.path,
12647
+ label: app.label,
12648
+ deployMethod: app.deployMethod
12649
+ })),
12650
+ stages: result.stages.map((s) => ({
12651
+ stage: s.stage,
12652
+ name: s.name,
12653
+ releaseBranch: s.release_branch,
12654
+ triggerMode: s.trigger_mode
12655
+ })),
12656
+ branchWarnings: result.branchWarnings,
12657
+ hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
12658
+ },
12659
+ null,
12660
+ 2
12661
+ )
12662
+ }
12663
+ ]
12664
+ };
12665
+ } catch (err) {
12666
+ if (err instanceof ReleaseProfileQuickCreateError) {
12667
+ throw new Error(err.message);
12668
+ }
12669
+ throw err;
13425
12670
  }
13426
- throw err;
13427
12671
  }
13428
- }
13429
- case "release-profile-update": {
13430
12672
  const identifier = String(a.releaseProfile ?? "").trim();
12673
+ if (!identifier) {
12674
+ throw new Error('action="update" requires releaseProfile');
12675
+ }
13431
12676
  const profileRows = await db.execute(sql`
13432
12677
  SELECT id, name FROM release_profile
13433
12678
  WHERE name ILIKE ${identifier} OR repo_full_name ILIKE ${identifier}