@mgsoftwarebv/mg-dashboard-mcp 7.0.5 → 7.0.7

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
@@ -20,6 +20,7 @@ import { sql } from 'drizzle-orm';
20
20
  import { once } from 'events';
21
21
  import { lookup } from 'dns/promises';
22
22
  import { connect } from 'tls';
23
+ import { pgTable, timestamp, jsonb, uuid, boolean, text, pgEnum, integer, uniqueIndex, index, bigint } from 'drizzle-orm/pg-core';
23
24
  import { tasks } from '@trigger.dev/sdk/v3';
24
25
  import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
25
26
 
@@ -677,10 +678,10 @@ var TRIGGER_TOOL_MODULE_MAP = {
677
678
  "trigger-run": "ci_cd"
678
679
  };
679
680
  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`;
681
+ 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
682
  const cmd = [
682
683
  `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:]')`,
684
+ `KEY=$(docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql28}" 2>/dev/null | tr -d '[:space:]')`,
684
685
  'echo "$PORT|$KEY"'
685
686
  ].join(" && ");
686
687
  const result = await sshExec2(conn, cmd, proxy);
@@ -701,8 +702,8 @@ async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
701
702
  return { port, apiKey: apiKey2 };
702
703
  }
703
704
  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`;
705
+ 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`;
706
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql28}" 2>/dev/null`;
706
707
  const result = await sshExec2(conn, cmd, proxy);
707
708
  const output = result.stdout.trim();
708
709
  if (!output) return "";
@@ -784,8 +785,8 @@ async function handleTriggerTool(name, args2, deps) {
784
785
  switch (name) {
785
786
  // -----------------------------------------------------------------
786
787
  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`;
788
+ const sql28 = 'SELECT slug, name FROM \\"Project\\" ORDER BY name';
789
+ const cmd = `docker exec "${PG_CONTAINER}" psql -U postgres -d main -t -A -c "${sql28}" 2>/dev/null`;
789
790
  const result = await sshExec2(conn, cmd, proxy);
790
791
  const output = result.stdout.trim();
791
792
  if (!output) {
@@ -943,9 +944,9 @@ async function fetchAndFormatRun(conn, proxy, sshExec2, instance, runId) {
943
944
  return { content: [{ type: "text", text: `Invalid API response:
944
945
  ${rawJson.substring(0, 500)}` }] };
945
946
  }
946
- let text = formatRunDetail(run);
947
- if (logs) text += "\n\n--- Logs ---\n" + logs;
948
- return { content: [{ type: "text", text }] };
947
+ let text6 = formatRunDetail(run);
948
+ if (logs) text6 += "\n\n--- Logs ---\n" + logs;
949
+ return { content: [{ type: "text", text: text6 }] };
949
950
  }
950
951
  async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSeconds) {
951
952
  const pollInterval = 3e3;
@@ -967,10 +968,10 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
967
968
  continue;
968
969
  }
969
970
  if (TERMINAL_STATUSES.has(run.status)) {
970
- let text = formatRunDetail(run);
971
+ let text6 = formatRunDetail(run);
971
972
  const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
972
- if (logs) text += "\n\n--- Logs ---\n" + logs;
973
- return { content: [{ type: "text", text }] };
973
+ if (logs) text6 += "\n\n--- Logs ---\n" + logs;
974
+ return { content: [{ type: "text", text: text6 }] };
974
975
  }
975
976
  }
976
977
  return {
@@ -1024,6 +1025,39 @@ async function getMgBoilerGitHubToken() {
1024
1025
  return { token: null, error: "Failed to decrypt GitHub token" };
1025
1026
  }
1026
1027
  }
1028
+ async function getGitHubTokenById(id) {
1029
+ try {
1030
+ const rows = await getDb().execute(sql`
1031
+ SELECT id, label, owner, notes, token_encrypted, created_at, updated_at
1032
+ FROM github_token
1033
+ WHERE id = ${id}
1034
+ LIMIT 1
1035
+ `);
1036
+ const row = rows[0];
1037
+ if (!row)
1038
+ return { token: null, owner: null, error: null };
1039
+ return {
1040
+ token: decrypt(row.token_encrypted),
1041
+ owner: row.owner ?? null,
1042
+ error: null
1043
+ };
1044
+ } catch (error) {
1045
+ console.error("[getGitHubTokenById] error:", error);
1046
+ return { token: null, owner: null, error: "Failed to decrypt GitHub token" };
1047
+ }
1048
+ }
1049
+ async function resolveReleaseGitHubToken(githubTokenId) {
1050
+ if (githubTokenId) {
1051
+ const { token, error } = await getGitHubTokenById(githubTokenId);
1052
+ if (token)
1053
+ return { token, error: null };
1054
+ return {
1055
+ token: null,
1056
+ error: error || "Configured GitHub token not found"
1057
+ };
1058
+ }
1059
+ return getMgBoilerGitHubToken();
1060
+ }
1027
1061
 
1028
1062
  // ../../node_modules/zod/dist/esm/v3/external.js
1029
1063
  var external_exports = {};
@@ -3624,10 +3658,10 @@ var ZodObject = class _ZodObject extends ZodType {
3624
3658
  // }) as any;
3625
3659
  // return merged;
3626
3660
  // }
3627
- catchall(index) {
3661
+ catchall(index4) {
3628
3662
  return new _ZodObject({
3629
3663
  ...this._def,
3630
- catchall: index
3664
+ catchall: index4
3631
3665
  });
3632
3666
  }
3633
3667
  pick(mask) {
@@ -3945,9 +3979,9 @@ function mergeValues(a, b) {
3945
3979
  return { valid: false };
3946
3980
  }
3947
3981
  const newArray = [];
3948
- for (let index = 0; index < a.length; index++) {
3949
- const itemA = a[index];
3950
- const itemB = b[index];
3982
+ for (let index4 = 0; index4 < a.length; index4++) {
3983
+ const itemA = a[index4];
3984
+ const itemB = b[index4];
3951
3985
  const sharedValue = mergeValues(itemA, itemB);
3952
3986
  if (!sharedValue.valid) {
3953
3987
  return { valid: false };
@@ -4153,10 +4187,10 @@ var ZodMap = class extends ZodType {
4153
4187
  }
4154
4188
  const keyType = this._def.keyType;
4155
4189
  const valueType = this._def.valueType;
4156
- const pairs = [...ctx.data.entries()].map(([key, value], index) => {
4190
+ const pairs = [...ctx.data.entries()].map(([key, value], index4) => {
4157
4191
  return {
4158
- key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])),
4159
- value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"]))
4192
+ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index4, "key"])),
4193
+ value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index4, "value"]))
4160
4194
  };
4161
4195
  });
4162
4196
  if (ctx.common.async) {
@@ -5061,7 +5095,7 @@ var coerce = {
5061
5095
  var NEVER = INVALID;
5062
5096
 
5063
5097
  // ../platform/dist/utils/litespeed-vhost.js
5064
- var LITESPEED_STANDARD_APP_PATH = /^apps\/(backoffice|portal|web|api)(\/[\w-]+)?$/;
5098
+ var LITESPEED_STANDARD_APP_PATH = /^(\.|apps\/(backoffice|portal|web|api)(\/[\w-]+)?)$/;
5065
5099
  var LitespeedVhostMappingSchema = external_exports.object({
5066
5100
  domain: external_exports.string().min(1).transform((value) => normalizeVhostDomain(value)),
5067
5101
  appPath: external_exports.string().min(1).regex(LITESPEED_STANDARD_APP_PATH, "Unsupported appPath")
@@ -5070,6 +5104,618 @@ external_exports.array(LitespeedVhostMappingSchema);
5070
5104
  function normalizeVhostDomain(value) {
5071
5105
  return value.trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/\.+$/, "").toLowerCase();
5072
5106
  }
5107
+ var users = pgTable("user", {
5108
+ id: uuid("id").primaryKey().defaultRandom(),
5109
+ email: text("email").notNull().unique(),
5110
+ fullName: text("full_name"),
5111
+ avatarUrl: text("avatar_url"),
5112
+ emailVerified: boolean("email_verified").notNull().default(false),
5113
+ roleId: uuid("role_id"),
5114
+ permissions: jsonb("permissions"),
5115
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5116
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5117
+ });
5118
+ pgTable("session", {
5119
+ id: uuid("id").primaryKey().defaultRandom(),
5120
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
5121
+ token: text("token").notNull().unique(),
5122
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
5123
+ ipAddress: text("ip_address"),
5124
+ userAgent: text("user_agent"),
5125
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5126
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5127
+ });
5128
+ pgTable("account", {
5129
+ id: uuid("id").primaryKey().defaultRandom(),
5130
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
5131
+ providerId: text("provider_id").notNull(),
5132
+ accountId: text("account_id").notNull(),
5133
+ password: text("password"),
5134
+ accessToken: text("access_token"),
5135
+ refreshToken: text("refresh_token"),
5136
+ accessTokenExpiresAt: timestamp("access_token_expires_at", {
5137
+ withTimezone: true
5138
+ }),
5139
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
5140
+ withTimezone: true
5141
+ }),
5142
+ scope: text("scope"),
5143
+ idToken: text("id_token"),
5144
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5145
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5146
+ });
5147
+ pgTable("verification", {
5148
+ id: uuid("id").primaryKey().defaultRandom(),
5149
+ identifier: text("identifier").notNull(),
5150
+ value: text("value").notNull(),
5151
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
5152
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5153
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5154
+ });
5155
+ pgTable("two_factor", {
5156
+ id: uuid("id").primaryKey().defaultRandom(),
5157
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
5158
+ secret: text("secret").notNull(),
5159
+ backupCodes: text("backup_codes")
5160
+ });
5161
+ var managedServerOs = pgEnum("managed_server_os", [
5162
+ "linux",
5163
+ "windows",
5164
+ "unknown"
5165
+ ]);
5166
+ var serverAgentStatus = pgEnum("server_agent_status", [
5167
+ "not_installed",
5168
+ "installing",
5169
+ "online",
5170
+ "degraded",
5171
+ "offline",
5172
+ "error"
5173
+ ]);
5174
+ var resourceKind = pgEnum("resource_kind", [
5175
+ "wordpress",
5176
+ "prestashop",
5177
+ "postgres_cluster",
5178
+ "postgres_database",
5179
+ "mysql_database",
5180
+ "mariadb_database",
5181
+ "boiler_project",
5182
+ "boiler_web_project",
5183
+ "static_site",
5184
+ "docker_app",
5185
+ "domain",
5186
+ "backup_policy",
5187
+ "server_service"
5188
+ ]);
5189
+ var resourceStatus = pgEnum("resource_status", [
5190
+ "unknown",
5191
+ "healthy",
5192
+ "degraded",
5193
+ "failed",
5194
+ "missing",
5195
+ "provisioning",
5196
+ "disabled"
5197
+ ]);
5198
+ var resourceOwnership = pgEnum("resource_ownership", [
5199
+ "managed",
5200
+ "discovered",
5201
+ "external"
5202
+ ]);
5203
+ var operationStatus = pgEnum("operation_status", [
5204
+ "queued",
5205
+ "running",
5206
+ "succeeded",
5207
+ "failed",
5208
+ "cancelled"
5209
+ ]);
5210
+ var alertSeverity = pgEnum("alert_severity", [
5211
+ "info",
5212
+ "warning",
5213
+ "critical"
5214
+ ]);
5215
+ var managedServer = pgTable(
5216
+ "managed_server",
5217
+ {
5218
+ id: uuid("id").primaryKey().defaultRandom(),
5219
+ name: text("name").notNull(),
5220
+ hostname: text("hostname").notNull(),
5221
+ port: integer("port").notNull().default(22),
5222
+ username: text("username").notNull(),
5223
+ authMethod: text("auth_method").notNull().default("ssh_key"),
5224
+ os: managedServerOs("os").notNull().default("unknown"),
5225
+ provider: text("provider"),
5226
+ region: text("region"),
5227
+ tags: jsonb("tags").$type().notNull().default([]),
5228
+ agentStatus: serverAgentStatus("agent_status").notNull().default("not_installed"),
5229
+ agentVersion: text("agent_version"),
5230
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }),
5231
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5232
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5233
+ },
5234
+ (table) => [
5235
+ uniqueIndex("managed_server_hostname_port_uidx").on(
5236
+ table.hostname,
5237
+ table.port
5238
+ ),
5239
+ index("managed_server_agent_status_idx").on(table.agentStatus)
5240
+ ]
5241
+ );
5242
+ pgTable("server_credential", {
5243
+ serverId: uuid("server_id").primaryKey().references(() => managedServer.id, { onDelete: "cascade" }),
5244
+ passwordEncrypted: text("password_encrypted"),
5245
+ sshKeyEncrypted: text("ssh_key_encrypted"),
5246
+ sshKeyPassphraseEncrypted: text("ssh_key_passphrase_encrypted"),
5247
+ dbRootPasswordEncrypted: text("db_root_password_encrypted"),
5248
+ monitoringApiKeyEncrypted: text("monitoring_api_key_encrypted"),
5249
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5250
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5251
+ });
5252
+ pgTable(
5253
+ "server_connection_log",
5254
+ {
5255
+ id: uuid("id").primaryKey().defaultRandom(),
5256
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5257
+ userId: uuid("user_id").notNull(),
5258
+ connectedAt: timestamp("connected_at", { withTimezone: true }).notNull().defaultNow(),
5259
+ disconnectedAt: timestamp("disconnected_at", { withTimezone: true }),
5260
+ durationSeconds: integer("duration_seconds"),
5261
+ status: text("status").notNull(),
5262
+ errorMessage: text("error_message"),
5263
+ clientIp: text("client_ip"),
5264
+ terminalCols: integer("terminal_cols"),
5265
+ terminalRows: integer("terminal_rows"),
5266
+ outputSizeBytes: bigint("output_size_bytes", { mode: "number" }).notNull().default(0),
5267
+ inputSizeBytes: bigint("input_size_bytes", { mode: "number" }).notNull().default(0),
5268
+ commandCount: integer("command_count").notNull().default(0),
5269
+ connectionType: text("connection_type").notNull().default("terminal"),
5270
+ action: text("action"),
5271
+ rawOutput: text("raw_output"),
5272
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5273
+ },
5274
+ (table) => [
5275
+ index("server_connection_log_server_idx").on(table.serverId),
5276
+ index("server_connection_log_user_idx").on(table.userId),
5277
+ index("server_connection_log_connected_idx").on(table.connectedAt),
5278
+ index("server_connection_log_status_idx").on(table.status)
5279
+ ]
5280
+ );
5281
+ pgTable(
5282
+ "mcp_audit_log",
5283
+ {
5284
+ id: uuid("id").primaryKey().defaultRandom(),
5285
+ apiKeyId: uuid("api_key_id"),
5286
+ userId: uuid("user_id"),
5287
+ toolName: text("tool_name"),
5288
+ arguments: jsonb("arguments"),
5289
+ ipAddress: text("ip_address"),
5290
+ serverId: uuid("server_id"),
5291
+ resultStatus: text("result_status").notNull().default("success"),
5292
+ errorMessage: text("error_message"),
5293
+ durationMs: integer("duration_ms"),
5294
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5295
+ },
5296
+ (table) => [
5297
+ index("mcp_audit_log_created_at_idx").on(table.createdAt),
5298
+ index("mcp_audit_log_user_idx").on(table.userId),
5299
+ index("mcp_audit_log_tool_name_idx").on(table.toolName),
5300
+ index("mcp_audit_log_server_idx").on(table.serverId),
5301
+ index("mcp_audit_log_api_key_idx").on(table.apiKeyId),
5302
+ index("mcp_audit_log_result_status_idx").on(table.resultStatus)
5303
+ ]
5304
+ );
5305
+ var resource = pgTable(
5306
+ "resource",
5307
+ {
5308
+ id: uuid("id").primaryKey().defaultRandom(),
5309
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5310
+ parentResourceId: uuid("parent_resource_id"),
5311
+ kind: resourceKind("kind").notNull(),
5312
+ slug: text("slug").notNull(),
5313
+ displayName: text("display_name").notNull(),
5314
+ spec: jsonb("spec").$type().notNull().default({}),
5315
+ state: jsonb("state").$type().notNull().default({}),
5316
+ status: resourceStatus("status").notNull().default("unknown"),
5317
+ ownership: resourceOwnership("ownership").notNull().default("managed"),
5318
+ lastDetectedAt: timestamp("last_detected_at", { withTimezone: true }),
5319
+ lastEventId: uuid("last_event_id"),
5320
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5321
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5322
+ },
5323
+ (table) => [
5324
+ uniqueIndex("resource_server_kind_slug_uidx").on(
5325
+ table.serverId,
5326
+ table.kind,
5327
+ table.slug
5328
+ ),
5329
+ index("resource_server_idx").on(table.serverId),
5330
+ index("resource_parent_idx").on(table.parentResourceId),
5331
+ index("resource_kind_status_idx").on(table.kind, table.status)
5332
+ ]
5333
+ );
5334
+ pgTable(
5335
+ "resource_event",
5336
+ {
5337
+ id: uuid("id").primaryKey().defaultRandom(),
5338
+ resourceId: uuid("resource_id").notNull().references(() => resource.id, { onDelete: "cascade" }),
5339
+ kind: text("kind").notNull(),
5340
+ before: jsonb("before").$type(),
5341
+ after: jsonb("after").$type(),
5342
+ actorUserId: text("actor_user_id"),
5343
+ operationId: uuid("operation_id"),
5344
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5345
+ },
5346
+ (table) => [
5347
+ index("resource_event_resource_created_idx").on(
5348
+ table.resourceId,
5349
+ table.createdAt
5350
+ ),
5351
+ index("resource_event_operation_idx").on(table.operationId)
5352
+ ]
5353
+ );
5354
+ var operation = pgTable(
5355
+ "operation",
5356
+ {
5357
+ id: uuid("id").primaryKey().defaultRandom(),
5358
+ kind: text("kind").notNull(),
5359
+ resourceId: uuid("resource_id").references(() => resource.id, {
5360
+ onDelete: "set null"
5361
+ }),
5362
+ serverId: uuid("server_id").references(() => managedServer.id, {
5363
+ onDelete: "set null"
5364
+ }),
5365
+ status: operationStatus("status").notNull().default("queued"),
5366
+ steps: jsonb("steps").$type().notNull().default([]),
5367
+ idempotencyKey: text("idempotency_key").notNull(),
5368
+ triggerRunId: text("trigger_run_id"),
5369
+ logsR2Key: text("logs_r2_key"),
5370
+ logs: text("logs"),
5371
+ startedBy: text("started_by"),
5372
+ startedAt: timestamp("started_at", { withTimezone: true }),
5373
+ endedAt: timestamp("ended_at", { withTimezone: true }),
5374
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5375
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5376
+ },
5377
+ (table) => [
5378
+ uniqueIndex("operation_idempotency_uidx").on(table.idempotencyKey),
5379
+ index("operation_resource_idx").on(table.resourceId),
5380
+ index("operation_server_idx").on(table.serverId),
5381
+ index("operation_status_created_idx").on(table.status, table.createdAt)
5382
+ ]
5383
+ );
5384
+ pgTable(
5385
+ "agent_installation",
5386
+ {
5387
+ id: uuid("id").primaryKey().defaultRandom(),
5388
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5389
+ agentId: text("agent_id"),
5390
+ version: text("version").notNull(),
5391
+ installPath: text("install_path").notNull().default("/usr/local/bin/mg-agent"),
5392
+ configHash: text("config_hash").notNull(),
5393
+ status: serverAgentStatus("status").notNull().default("installing"),
5394
+ lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
5395
+ lastError: text("last_error"),
5396
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5397
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5398
+ },
5399
+ (table) => [
5400
+ uniqueIndex("agent_installation_server_uidx").on(table.serverId),
5401
+ index("agent_installation_status_idx").on(table.status)
5402
+ ]
5403
+ );
5404
+ pgTable(
5405
+ "monitoring_sample",
5406
+ {
5407
+ id: uuid("id").primaryKey().defaultRandom(),
5408
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5409
+ resourceId: uuid("resource_id").references(() => resource.id, {
5410
+ onDelete: "cascade"
5411
+ }),
5412
+ metric: text("metric").notNull(),
5413
+ value: integer("value").notNull(),
5414
+ unit: text("unit").notNull(),
5415
+ tags: jsonb("tags").$type().notNull().default({}),
5416
+ sampledAt: timestamp("sampled_at", { withTimezone: true }).notNull().defaultNow()
5417
+ },
5418
+ (table) => [
5419
+ index("monitoring_sample_server_metric_time_idx").on(
5420
+ table.serverId,
5421
+ table.metric,
5422
+ table.sampledAt
5423
+ ),
5424
+ // Descending on sampled_at so "latest metric per (server, metric)" lookups
5425
+ // (DISTINCT ON ... ORDER BY sampled_at DESC) are served by an index-only
5426
+ // scan instead of a full-table sort. The migration also adds
5427
+ // INCLUDE (value, unit) to make it covering for the overview snapshot query
5428
+ // (Drizzle 0.44 can't express INCLUDE here). See migration 20260529210000.
5429
+ index("monitoring_sample_server_metric_time_desc_idx").on(
5430
+ table.serverId,
5431
+ table.metric,
5432
+ table.sampledAt.desc()
5433
+ ),
5434
+ index("monitoring_sample_resource_metric_time_idx").on(
5435
+ table.resourceId,
5436
+ table.metric,
5437
+ table.sampledAt
5438
+ )
5439
+ ]
5440
+ );
5441
+ var appLogSource = pgTable(
5442
+ "app_log_source",
5443
+ {
5444
+ id: uuid("id").primaryKey().defaultRandom(),
5445
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5446
+ resourceId: uuid("resource_id").references(() => resource.id, {
5447
+ onDelete: "set null"
5448
+ }),
5449
+ sourceType: text("source_type").notNull(),
5450
+ sourceKey: text("source_key").notNull(),
5451
+ displayName: text("display_name").notNull(),
5452
+ path: text("path").notNull(),
5453
+ enabled: boolean("enabled").notNull().default(true),
5454
+ mutedUntil: timestamp("muted_until", { withTimezone: true }),
5455
+ metadata: jsonb("metadata").$type().notNull().default({}),
5456
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
5457
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5458
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5459
+ },
5460
+ (table) => [
5461
+ uniqueIndex("app_log_source_server_type_key_uidx").on(
5462
+ table.serverId,
5463
+ table.sourceType,
5464
+ table.sourceKey
5465
+ ),
5466
+ index("app_log_source_server_idx").on(table.serverId),
5467
+ index("app_log_source_resource_idx").on(table.resourceId),
5468
+ index("app_log_source_type_seen_idx").on(
5469
+ table.sourceType,
5470
+ table.lastSeenAt
5471
+ )
5472
+ ]
5473
+ );
5474
+ pgTable(
5475
+ "app_error_event",
5476
+ {
5477
+ id: uuid("id").primaryKey().defaultRandom(),
5478
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5479
+ resourceId: uuid("resource_id").references(() => resource.id, {
5480
+ onDelete: "set null"
5481
+ }),
5482
+ logSourceId: uuid("log_source_id").references(() => appLogSource.id, {
5483
+ onDelete: "set null"
5484
+ }),
5485
+ fingerprint: text("fingerprint").notNull(),
5486
+ severity: text("severity").notNull().default("critical"),
5487
+ category: text("category").notNull(),
5488
+ sourceType: text("source_type").notNull(),
5489
+ sourcePath: text("source_path").notNull(),
5490
+ sampleMessage: text("sample_message").notNull(),
5491
+ sampleLines: jsonb("sample_lines").$type().notNull().default([]),
5492
+ signals: jsonb("signals").$type().notNull().default({}),
5493
+ occurrences: integer("occurrences").notNull().default(1),
5494
+ firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(),
5495
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
5496
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
5497
+ },
5498
+ (table) => [
5499
+ index("app_error_event_fingerprint_created_idx").on(
5500
+ table.fingerprint,
5501
+ table.createdAt
5502
+ ),
5503
+ index("app_error_event_server_created_idx").on(
5504
+ table.serverId,
5505
+ table.createdAt
5506
+ ),
5507
+ index("app_error_event_resource_created_idx").on(
5508
+ table.resourceId,
5509
+ table.createdAt
5510
+ ),
5511
+ index("app_error_event_severity_created_idx").on(
5512
+ table.severity,
5513
+ table.createdAt
5514
+ )
5515
+ ]
5516
+ );
5517
+ pgTable(
5518
+ "app_error_alert",
5519
+ {
5520
+ id: uuid("id").primaryKey().defaultRandom(),
5521
+ fingerprint: text("fingerprint").notNull(),
5522
+ serverId: uuid("server_id").notNull().references(() => managedServer.id, { onDelete: "cascade" }),
5523
+ resourceId: uuid("resource_id").references(() => resource.id, {
5524
+ onDelete: "set null"
5525
+ }),
5526
+ logSourceId: uuid("log_source_id").references(() => appLogSource.id, {
5527
+ onDelete: "set null"
5528
+ }),
5529
+ severity: text("severity").notNull().default("critical"),
5530
+ status: text("status").notNull().default("open"),
5531
+ category: text("category").notNull(),
5532
+ title: text("title").notNull(),
5533
+ lastError: text("last_error").notNull(),
5534
+ sampleLines: jsonb("sample_lines").$type().notNull().default([]),
5535
+ signals: jsonb("signals").$type().notNull().default({}),
5536
+ failureCount: integer("failure_count").notNull().default(1),
5537
+ firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).notNull().defaultNow(),
5538
+ lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).notNull().defaultNow(),
5539
+ lastAlertAt: timestamp("last_alert_at", { withTimezone: true }),
5540
+ resolvedAt: timestamp("resolved_at", { withTimezone: true }),
5541
+ mutedUntil: timestamp("muted_until", { withTimezone: true }),
5542
+ cursorPrompt: text("cursor_prompt"),
5543
+ telegramChatId: text("telegram_chat_id"),
5544
+ telegramMessageId: bigint("telegram_message_id", { mode: "number" }),
5545
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5546
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5547
+ },
5548
+ (table) => [
5549
+ uniqueIndex("app_error_alert_fingerprint_uidx").on(table.fingerprint),
5550
+ index("app_error_alert_status_seen_idx").on(
5551
+ table.status,
5552
+ table.lastSeenAt
5553
+ ),
5554
+ index("app_error_alert_server_seen_idx").on(
5555
+ table.serverId,
5556
+ table.lastSeenAt
5557
+ ),
5558
+ index("app_error_alert_resource_seen_idx").on(
5559
+ table.resourceId,
5560
+ table.lastSeenAt
5561
+ ),
5562
+ index("app_error_alert_alerted_idx").on(table.lastAlertAt)
5563
+ ]
5564
+ );
5565
+ pgTable(
5566
+ "alert_rule",
5567
+ {
5568
+ id: uuid("id").primaryKey().defaultRandom(),
5569
+ serverId: uuid("server_id").references(() => managedServer.id, {
5570
+ onDelete: "cascade"
5571
+ }),
5572
+ resourceId: uuid("resource_id").references(() => resource.id, {
5573
+ onDelete: "cascade"
5574
+ }),
5575
+ metric: text("metric").notNull(),
5576
+ expression: text("expression").notNull(),
5577
+ severity: alertSeverity("severity").notNull().default("warning"),
5578
+ cooldownSeconds: integer("cooldown_seconds").notNull().default(900),
5579
+ enabled: boolean("enabled").notNull().default(true),
5580
+ routes: jsonb("routes").$type().notNull().default({}),
5581
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5582
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5583
+ },
5584
+ (table) => [
5585
+ index("alert_rule_server_idx").on(table.serverId),
5586
+ index("alert_rule_resource_idx").on(table.resourceId),
5587
+ index("alert_rule_enabled_severity_idx").on(table.enabled, table.severity)
5588
+ ]
5589
+ );
5590
+ pgTable(
5591
+ "deployment_project_server",
5592
+ {
5593
+ id: uuid("id").primaryKey().defaultRandom(),
5594
+ releaseProfileStageId: uuid("release_profile_stage_id").notNull(),
5595
+ sshServerId: uuid("ssh_server_id").notNull(),
5596
+ deployPath: text("deploy_path").notNull(),
5597
+ deployPathNormalized: text("deploy_path_normalized"),
5598
+ pm2PortBase: integer("pm2_port_base").notNull(),
5599
+ postgresHost: text("postgres_host"),
5600
+ postgresPort: integer("postgres_port"),
5601
+ buildFilters: text("build_filters"),
5602
+ envSymlinkDirs: jsonb("env_symlink_dirs"),
5603
+ litespeedVhosts: jsonb("litespeed_vhosts"),
5604
+ litespeedVhostsCheckedAt: timestamp("litespeed_vhosts_checked_at", {
5605
+ withTimezone: true
5606
+ }),
5607
+ litespeedVhostsCheckOk: boolean("litespeed_vhosts_check_ok"),
5608
+ ecosystemConfig: text("ecosystem_config").notNull().default("ecosystem.config.cjs"),
5609
+ usePipelineDeploy: boolean("use_pipeline_deploy").notNull().default(true),
5610
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5611
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5612
+ },
5613
+ (table) => [
5614
+ uniqueIndex("deployment_project_server_stage_server_uidx").on(
5615
+ table.releaseProfileStageId,
5616
+ table.sshServerId
5617
+ ),
5618
+ uniqueIndex("deployment_project_server_server_path_uidx").on(
5619
+ table.sshServerId,
5620
+ table.deployPathNormalized
5621
+ ),
5622
+ uniqueIndex("deployment_project_server_server_port_uidx").on(
5623
+ table.sshServerId,
5624
+ table.pm2PortBase
5625
+ )
5626
+ ]
5627
+ );
5628
+ var dnsMigrationStatus = pgEnum("dns_migration_status", [
5629
+ "pending",
5630
+ "scanned",
5631
+ "imported",
5632
+ "verified"
5633
+ ]);
5634
+ var dnsMigration = pgTable(
5635
+ "dns_migration",
5636
+ {
5637
+ id: uuid("id").primaryKey().defaultRandom(),
5638
+ domain: text("domain").notNull(),
5639
+ sourceProvider: text("source_provider").notNull().default("hostnet"),
5640
+ status: dnsMigrationStatus("status").notNull().default("pending"),
5641
+ scannedRecords: jsonb("scanned_records").$type().notNull().default([]),
5642
+ sourceNameservers: jsonb("source_nameservers").$type().notNull().default([]),
5643
+ currentNameservers: jsonb("current_nameservers").$type().notNull().default([]),
5644
+ importResult: jsonb("import_result").$type(),
5645
+ emailChecklist: jsonb("email_checklist").$type().notNull().default({}),
5646
+ directadminHost: text("directadmin_host"),
5647
+ directadminUsername: text("directadmin_username"),
5648
+ directadminPasswordEncrypted: text("directadmin_password_encrypted"),
5649
+ syncServerId: uuid("sync_server_id").references(() => managedServer.id, {
5650
+ onDelete: "set null"
5651
+ }),
5652
+ syncOperationId: uuid("sync_operation_id").references(() => operation.id, {
5653
+ onDelete: "set null"
5654
+ }),
5655
+ mailRecordsImportedAt: timestamp("mail_records_imported_at", {
5656
+ withTimezone: true
5657
+ }),
5658
+ notes: text("notes"),
5659
+ lastError: text("last_error"),
5660
+ lastScannedAt: timestamp("last_scanned_at", { withTimezone: true }),
5661
+ importedAt: timestamp("imported_at", { withTimezone: true }),
5662
+ verifiedAt: timestamp("verified_at", { withTimezone: true }),
5663
+ createdBy: text("created_by"),
5664
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5665
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5666
+ },
5667
+ (table) => [
5668
+ index("dns_migration_domain_idx").on(table.domain),
5669
+ index("dns_migration_status_created_idx").on(
5670
+ table.status,
5671
+ table.createdAt
5672
+ )
5673
+ ]
5674
+ );
5675
+ var dnsMigrationMailboxStatus = pgEnum(
5676
+ "dns_migration_mailbox_status",
5677
+ ["pending", "provisioned", "syncing", "synced", "delta_synced", "failed"]
5678
+ );
5679
+ pgTable(
5680
+ "dns_migration_mailbox",
5681
+ {
5682
+ id: uuid("id").primaryKey().defaultRandom(),
5683
+ migrationId: uuid("migration_id").notNull().references(() => dnsMigration.id, { onDelete: "cascade" }),
5684
+ address: text("address").notNull(),
5685
+ sourcePasswordEncrypted: text("source_password_encrypted"),
5686
+ destPasswordEncrypted: text("dest_password_encrypted"),
5687
+ status: dnsMigrationMailboxStatus("status").notNull().default("pending"),
5688
+ syncStats: jsonb("sync_stats").$type(),
5689
+ lastError: text("last_error"),
5690
+ provisionedAt: timestamp("provisioned_at", { withTimezone: true }),
5691
+ lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }),
5692
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5693
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5694
+ },
5695
+ (table) => [
5696
+ uniqueIndex("dns_migration_mailbox_migration_address_uidx").on(
5697
+ table.migrationId,
5698
+ table.address
5699
+ ),
5700
+ index("dns_migration_mailbox_migration_idx").on(table.migrationId)
5701
+ ]
5702
+ );
5703
+ pgTable(
5704
+ "github_token",
5705
+ {
5706
+ id: uuid("id").primaryKey().defaultRandom(),
5707
+ label: text("label").notNull(),
5708
+ owner: text("owner"),
5709
+ notes: text("notes"),
5710
+ tokenEncrypted: text("token_encrypted").notNull(),
5711
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
5712
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
5713
+ },
5714
+ (table) => [
5715
+ index("github_token_label_idx").on(table.label),
5716
+ index("github_token_owner_idx").on(table.owner)
5717
+ ]
5718
+ );
5073
5719
  async function triggerPipeline(payload) {
5074
5720
  return tasks.trigger(
5075
5721
  "execute-pipeline",
@@ -5473,7 +6119,9 @@ async function createReleaseForStage(userId, params) {
5473
6119
  "BAD_REQUEST"
5474
6120
  );
5475
6121
  }
5476
- const { token } = await getMgBoilerGitHubToken();
6122
+ const { token } = await resolveReleaseGitHubToken(
6123
+ profile.github_token_id ?? null
6124
+ );
5477
6125
  if (!token) {
5478
6126
  throw new CreateReleaseError(
5479
6127
  "GitHub token is not configured",
@@ -5612,6 +6260,449 @@ async function createReleaseForStage(userId, params) {
5612
6260
  authorName: triggerAuthorName
5613
6261
  };
5614
6262
  }
6263
+ var GITHUB_API3 = "https://api.github.com";
6264
+ var ReleaseProfileQuickCreateError = class extends Error {
6265
+ constructor(message, code = "INTERNAL_SERVER_ERROR") {
6266
+ super(message);
6267
+ this.code = code;
6268
+ this.name = "ReleaseProfileQuickCreateError";
6269
+ }
6270
+ };
6271
+ function inferDeployMethod(folderName, hasNextConfig, hasDockerfile, hasPm2Ecosystem) {
6272
+ if (hasPm2Ecosystem && hasNextConfig) return "pm2";
6273
+ if (hasPm2Ecosystem && !hasDockerfile && !folderName.toLowerCase().includes("api"))
6274
+ return "pm2";
6275
+ if (hasNextConfig) return "pm2";
6276
+ if (hasDockerfile || folderName.toLowerCase().includes("api"))
6277
+ return "docker";
6278
+ return "none";
6279
+ }
6280
+ function toLabel(folderName) {
6281
+ return folderName.charAt(0).toUpperCase() + folderName.slice(1);
6282
+ }
6283
+ async function scanRepoForApps(repoFullName, githubToken2, ref = "main") {
6284
+ const headers = {
6285
+ Accept: "application/vnd.github.v3+json",
6286
+ Authorization: `Bearer ${githubToken2}`,
6287
+ "Content-Type": "application/json"
6288
+ };
6289
+ async function fetchContents(path) {
6290
+ const res = await fetch(
6291
+ `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
6292
+ { headers }
6293
+ );
6294
+ if (!res.ok) return null;
6295
+ const data = await res.json();
6296
+ return Array.isArray(data) ? data : null;
6297
+ }
6298
+ async function fileExists(path) {
6299
+ const res = await fetch(
6300
+ `${GITHUB_API3}/repos/${repoFullName}/contents/${path}?ref=${ref}`,
6301
+ { headers, method: "HEAD" }
6302
+ );
6303
+ return res.ok;
6304
+ }
6305
+ const apps = [];
6306
+ const appsDir = await fetchContents("apps");
6307
+ const isMonorepo = appsDir !== null && appsDir.length > 0;
6308
+ const hasPm2Ecosystem = await Promise.any([
6309
+ fileExists("ecosystem.config.cjs"),
6310
+ fileExists("ecosystem.config.js"),
6311
+ fileExists("ecosystem.config.mjs")
6312
+ ]).catch(() => false);
6313
+ if (isMonorepo) {
6314
+ const subfolders = appsDir.filter((item) => item.type === "dir");
6315
+ const detections = await Promise.all(
6316
+ subfolders.map(async (folder) => {
6317
+ const folderPath = `apps/${folder.name}`;
6318
+ const [hasNextMjs, hasNextJs, hasNextTs, hasDockerfile, hasPkgJson] = await Promise.all([
6319
+ fileExists(`${folderPath}/next.config.mjs`),
6320
+ fileExists(`${folderPath}/next.config.js`),
6321
+ fileExists(`${folderPath}/next.config.ts`),
6322
+ fileExists(`${folderPath}/Dockerfile`),
6323
+ fileExists(`${folderPath}/package.json`)
6324
+ ]);
6325
+ const hasNextConfig = hasNextMjs || hasNextJs || hasNextTs;
6326
+ const deployMethod = inferDeployMethod(
6327
+ folder.name,
6328
+ hasNextConfig,
6329
+ hasDockerfile,
6330
+ !!hasPm2Ecosystem
6331
+ );
6332
+ return {
6333
+ path: folderPath,
6334
+ label: toLabel(folder.name),
6335
+ deployMethod,
6336
+ hasNextConfig,
6337
+ hasDockerfile,
6338
+ hasPackageJson: hasPkgJson,
6339
+ enabled: true
6340
+ };
6341
+ })
6342
+ );
6343
+ apps.push(...detections);
6344
+ } else {
6345
+ const [hasNextMjs, hasNextJs, hasNextTs, hasDockerfile, hasPkgJson] = await Promise.all([
6346
+ fileExists("next.config.mjs"),
6347
+ fileExists("next.config.js"),
6348
+ fileExists("next.config.ts"),
6349
+ fileExists("Dockerfile"),
6350
+ fileExists("package.json")
6351
+ ]);
6352
+ const hasNextConfig = hasNextMjs || hasNextJs || hasNextTs;
6353
+ const repoName = repoFullName.split("/").pop() ?? "app";
6354
+ const deployMethod = inferDeployMethod(
6355
+ repoName,
6356
+ hasNextConfig,
6357
+ hasDockerfile,
6358
+ !!hasPm2Ecosystem
6359
+ );
6360
+ apps.push({
6361
+ path: ".",
6362
+ label: toLabel(repoName),
6363
+ deployMethod,
6364
+ hasNextConfig,
6365
+ hasDockerfile,
6366
+ hasPackageJson: hasPkgJson,
6367
+ enabled: true
6368
+ });
6369
+ }
6370
+ const workDirectory = isMonorepo ? apps.find((a) => a.deployMethod !== "none")?.path ?? "." : ".";
6371
+ const hasTriggerDev = await Promise.any([
6372
+ fileExists("packages/jobs/trigger.config.ts"),
6373
+ fileExists("trigger.config.ts")
6374
+ ]).catch(() => false);
6375
+ return {
6376
+ isMonorepo,
6377
+ apps,
6378
+ workDirectory,
6379
+ hasTriggerDev: !!hasTriggerDev
6380
+ };
6381
+ }
6382
+ var QUICK_STAGE_DEFAULTS = {
6383
+ dev: {
6384
+ name: "Development",
6385
+ releaseBranch: "release-dev",
6386
+ triggerMode: "pr_webhook",
6387
+ autoApprove: true,
6388
+ requireReview: false,
6389
+ backupDb: false,
6390
+ migrateDb: true
6391
+ },
6392
+ staging: {
6393
+ name: "Staging",
6394
+ releaseBranch: "release-staging",
6395
+ triggerMode: "manual",
6396
+ autoApprove: false,
6397
+ requireReview: false,
6398
+ backupDb: true,
6399
+ migrateDb: true
6400
+ },
6401
+ prod: {
6402
+ name: "Production",
6403
+ releaseBranch: "release-prod",
6404
+ triggerMode: "manual",
6405
+ autoApprove: false,
6406
+ requireReview: false,
6407
+ backupDb: true,
6408
+ migrateDb: true
6409
+ }
6410
+ };
6411
+ var QUICK_STAGES_FOR_TYPE = {
6412
+ dev_only: ["dev"],
6413
+ prod_only: ["prod"],
6414
+ both: ["dev", "prod"]
6415
+ };
6416
+ function deriveProfileName(repoFullName) {
6417
+ const repoName = repoFullName.split("/").pop() ?? repoFullName;
6418
+ return repoName.split(/[-_]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
6419
+ }
6420
+ function toStageApps(apps) {
6421
+ return apps.filter((a) => a.enabled).map((a) => ({
6422
+ path: a.path,
6423
+ label: a.label,
6424
+ deployMethod: a.deployMethod,
6425
+ deploymentAppId: null,
6426
+ deploymentServerId: null,
6427
+ deployPath: null,
6428
+ deployScript: null,
6429
+ enabled: a.enabled
6430
+ }));
6431
+ }
6432
+ async function ensureGitHubBranchExists(repoFullName, branchName, fromBranch, githubToken2) {
6433
+ const headers = {
6434
+ Accept: "application/vnd.github.v3+json",
6435
+ Authorization: `Bearer ${githubToken2}`,
6436
+ "Content-Type": "application/json"
6437
+ };
6438
+ const checkRes = await fetch(
6439
+ `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${branchName}`,
6440
+ { headers }
6441
+ );
6442
+ if (checkRes.ok) return;
6443
+ let sha;
6444
+ const sourceRes = await fetch(
6445
+ `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${fromBranch}`,
6446
+ { headers }
6447
+ );
6448
+ if (sourceRes.ok) {
6449
+ const data = await sourceRes.json();
6450
+ sha = data.object?.sha;
6451
+ } else {
6452
+ const repoRes = await fetch(`${GITHUB_API3}/repos/${repoFullName}`, {
6453
+ headers
6454
+ });
6455
+ if (!repoRes.ok) {
6456
+ throw new Error(`Could not access repository ${repoFullName}`);
6457
+ }
6458
+ const repo = await repoRes.json();
6459
+ const defaultBranch = repo.default_branch ?? "main";
6460
+ const defaultRes = await fetch(
6461
+ `${GITHUB_API3}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`,
6462
+ { headers }
6463
+ );
6464
+ if (!defaultRes.ok) {
6465
+ throw new Error(`Could not resolve a source branch for ${repoFullName}`);
6466
+ }
6467
+ const data = await defaultRes.json();
6468
+ sha = data.object?.sha;
6469
+ }
6470
+ if (!sha) {
6471
+ throw new Error(`Could not determine SHA to create branch ${branchName}`);
6472
+ }
6473
+ const createRes = await fetch(
6474
+ `${GITHUB_API3}/repos/${repoFullName}/git/refs`,
6475
+ {
6476
+ method: "POST",
6477
+ headers,
6478
+ body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha })
6479
+ }
6480
+ );
6481
+ if (!createRes.ok && createRes.status !== 422) {
6482
+ const body = await createRes.text().catch(() => "");
6483
+ throw new Error(
6484
+ `Failed to create branch ${branchName}: ${createRes.status} ${body}`
6485
+ );
6486
+ }
6487
+ }
6488
+ async function insertQuickStages(profileId, releaseType, stageApps) {
6489
+ const db2 = getDb();
6490
+ const stages = [];
6491
+ const wanted = QUICK_STAGES_FOR_TYPE[releaseType];
6492
+ for (let i = 0; i < wanted.length; i++) {
6493
+ const stageType = wanted[i];
6494
+ const defaults = QUICK_STAGE_DEFAULTS[stageType];
6495
+ const rows = await db2.execute(sql`
6496
+ INSERT INTO release_profile_stage (
6497
+ release_profile_id, name, stage, stage_order, release_branch,
6498
+ trigger_mode, auto_approve, require_review,
6499
+ backup_db, migrate_db, stage_apps, enabled,
6500
+ trigger_branch, trigger_event
6501
+ ) VALUES (
6502
+ ${profileId}, ${defaults.name}, ${stageType}, ${i + 1},
6503
+ ${defaults.releaseBranch},
6504
+ ${defaults.triggerMode}, ${defaults.autoApprove}, ${defaults.requireReview},
6505
+ ${defaults.backupDb}, ${defaults.migrateDb},
6506
+ ${JSON.stringify(stageApps)}::jsonb, true,
6507
+ ${defaults.releaseBranch}, 'push'
6508
+ )
6509
+ RETURNING id, name, stage, release_branch, trigger_mode
6510
+ `);
6511
+ if (rows[0]) stages.push(rows[0]);
6512
+ }
6513
+ return stages;
6514
+ }
6515
+ async function quickCreateReleaseProfile(userId, options) {
6516
+ const db2 = getDb();
6517
+ const repoFullName = options.repoFullName.trim();
6518
+ if (!repoFullName.includes("/")) {
6519
+ throw new ReleaseProfileQuickCreateError(
6520
+ `Repository must be in "owner/name" format, got "${repoFullName}"`,
6521
+ "BAD_REQUEST"
6522
+ );
6523
+ }
6524
+ const existing = await db2.execute(sql`
6525
+ SELECT id, name FROM release_profile
6526
+ WHERE lower(repo_full_name) = ${repoFullName.toLowerCase()}
6527
+ LIMIT 1
6528
+ `);
6529
+ if (existing[0]) {
6530
+ throw new ReleaseProfileQuickCreateError(
6531
+ `Repository ${repoFullName} already has a release profile ("${existing[0].name}")`,
6532
+ "CONFLICT"
6533
+ );
6534
+ }
6535
+ const githubTokenId = options.githubTokenId ?? null;
6536
+ const { token, error: tokenError } = await resolveReleaseGitHubToken(githubTokenId);
6537
+ if (!token) {
6538
+ throw new ReleaseProfileQuickCreateError(
6539
+ tokenError || "GitHub token is not configured",
6540
+ "PRECONDITION_FAILED"
6541
+ );
6542
+ }
6543
+ let scan;
6544
+ try {
6545
+ scan = await scanRepoForApps(repoFullName, token);
6546
+ } catch (err) {
6547
+ throw new ReleaseProfileQuickCreateError(
6548
+ `Failed to scan repository ${repoFullName}: ${err instanceof Error ? err.message : String(err)}`,
6549
+ "BAD_REQUEST"
6550
+ );
6551
+ }
6552
+ const name = options.name?.trim() || deriveProfileName(repoFullName);
6553
+ const releaseType = options.releaseType ?? "both";
6554
+ const detectedApps = toStageApps(scan.apps);
6555
+ let profile;
6556
+ try {
6557
+ const rows = await db2.execute(sql`
6558
+ INSERT INTO release_profile (
6559
+ name, repo_full_name, work_directory, enabled, release_type,
6560
+ current_version, active_version, created_by, use_changes_branch,
6561
+ detected_apps, has_trigger_dev, github_token_id
6562
+ ) VALUES (
6563
+ ${name}, ${repoFullName}, ${scan.workDirectory}, true, ${releaseType},
6564
+ NULL, NULL, ${userId}, false,
6565
+ ${JSON.stringify(detectedApps)}::jsonb, ${scan.hasTriggerDev},
6566
+ ${githubTokenId}
6567
+ )
6568
+ RETURNING *
6569
+ `);
6570
+ profile = rows[0];
6571
+ } catch (err) {
6572
+ throw new ReleaseProfileQuickCreateError(
6573
+ `Failed to create release profile: ${err instanceof Error ? err.message : String(err)}`,
6574
+ "INTERNAL_SERVER_ERROR"
6575
+ );
6576
+ }
6577
+ if (!profile) {
6578
+ throw new ReleaseProfileQuickCreateError(
6579
+ "Release profile insert returned no rows",
6580
+ "INTERNAL_SERVER_ERROR"
6581
+ );
6582
+ }
6583
+ const stages = await insertQuickStages(profile.id, releaseType, detectedApps);
6584
+ const branchWarnings = [];
6585
+ for (const stage of stages) {
6586
+ if (!stage.release_branch) continue;
6587
+ try {
6588
+ await ensureGitHubBranchExists(
6589
+ repoFullName,
6590
+ stage.release_branch,
6591
+ "main",
6592
+ token
6593
+ );
6594
+ } catch (err) {
6595
+ branchWarnings.push(
6596
+ `Could not create branch ${stage.release_branch}: ${err instanceof Error ? err.message : String(err)}`
6597
+ );
6598
+ }
6599
+ }
6600
+ return { profile, stages, scan, branchWarnings };
6601
+ }
6602
+ async function updateReleaseProfileSettings(profileId, fields) {
6603
+ const db2 = getDb();
6604
+ const profileRows = await db2.execute(sql`
6605
+ SELECT id, release_type, detected_apps
6606
+ FROM release_profile WHERE id = ${profileId} LIMIT 1
6607
+ `);
6608
+ const current = profileRows[0];
6609
+ if (!current) {
6610
+ throw new ReleaseProfileQuickCreateError(
6611
+ "Release profile not found",
6612
+ "NOT_FOUND"
6613
+ );
6614
+ }
6615
+ const setExprs = [];
6616
+ if (fields.name !== void 0) setExprs.push(sql`name = ${fields.name}`);
6617
+ if (fields.enabled !== void 0)
6618
+ setExprs.push(sql`enabled = ${fields.enabled}`);
6619
+ if (fields.releaseType !== void 0)
6620
+ setExprs.push(sql`release_type = ${fields.releaseType}`);
6621
+ if (fields.githubTokenId !== void 0)
6622
+ setExprs.push(sql`github_token_id = ${fields.githubTokenId}`);
6623
+ if (fields.useChangesBranch !== void 0)
6624
+ setExprs.push(sql`use_changes_branch = ${fields.useChangesBranch}`);
6625
+ if (fields.hasTriggerDev !== void 0)
6626
+ setExprs.push(sql`has_trigger_dev = ${fields.hasTriggerDev}`);
6627
+ if (fields.workDirectory !== void 0)
6628
+ setExprs.push(sql`work_directory = ${fields.workDirectory}`);
6629
+ if (setExprs.length === 0) {
6630
+ throw new ReleaseProfileQuickCreateError(
6631
+ "No fields to update",
6632
+ "BAD_REQUEST"
6633
+ );
6634
+ }
6635
+ const rows = await db2.execute(sql`
6636
+ UPDATE release_profile
6637
+ SET ${sql.join(setExprs, sql`, `)}
6638
+ WHERE id = ${profileId}
6639
+ RETURNING *
6640
+ `);
6641
+ const profile = rows[0];
6642
+ if (!profile) {
6643
+ throw new ReleaseProfileQuickCreateError(
6644
+ "Release profile update returned no rows",
6645
+ "INTERNAL_SERVER_ERROR"
6646
+ );
6647
+ }
6648
+ let stagesReconfigured = false;
6649
+ if (fields.releaseType !== void 0 && fields.releaseType !== current.release_type) {
6650
+ await reconfigureQuickStages(
6651
+ profileId,
6652
+ fields.releaseType,
6653
+ current.detected_apps ?? []
6654
+ );
6655
+ stagesReconfigured = true;
6656
+ }
6657
+ return { profile, stagesReconfigured };
6658
+ }
6659
+ async function reconfigureQuickStages(profileId, releaseType, detectedApps) {
6660
+ const db2 = getDb();
6661
+ const wanted = QUICK_STAGES_FOR_TYPE[releaseType];
6662
+ const existingStages = await db2.execute(sql`
6663
+ SELECT id, stage, release_branch
6664
+ FROM release_profile_stage
6665
+ WHERE release_profile_id = ${profileId}
6666
+ `);
6667
+ const existingByStage = new Map(existingStages.map((s) => [s.stage, s]));
6668
+ for (let i = 0; i < wanted.length; i++) {
6669
+ const stageType = wanted[i];
6670
+ const defaults = QUICK_STAGE_DEFAULTS[stageType];
6671
+ const existing = existingByStage.get(stageType);
6672
+ if (existing) {
6673
+ await db2.execute(sql`
6674
+ UPDATE release_profile_stage
6675
+ SET stage_order = ${i + 1},
6676
+ release_branch = ${existing.release_branch ?? defaults.releaseBranch},
6677
+ enabled = true
6678
+ WHERE id = ${existing.id}
6679
+ `);
6680
+ } else {
6681
+ await db2.execute(sql`
6682
+ INSERT INTO release_profile_stage (
6683
+ release_profile_id, name, stage, stage_order, release_branch,
6684
+ trigger_mode, auto_approve, require_review,
6685
+ backup_db, migrate_db, stage_apps, enabled,
6686
+ trigger_branch, trigger_event
6687
+ ) VALUES (
6688
+ ${profileId}, ${defaults.name}, ${stageType}, ${i + 1},
6689
+ ${defaults.releaseBranch},
6690
+ ${defaults.triggerMode}, ${defaults.autoApprove}, ${defaults.requireReview},
6691
+ ${defaults.backupDb}, ${defaults.migrateDb},
6692
+ ${JSON.stringify(detectedApps)}::jsonb, true,
6693
+ ${defaults.releaseBranch}, 'push'
6694
+ )
6695
+ `);
6696
+ }
6697
+ }
6698
+ const wantedSet = new Set(wanted);
6699
+ for (const existing of existingStages) {
6700
+ if (wantedSet.has(existing.stage)) continue;
6701
+ await db2.execute(sql`
6702
+ UPDATE release_profile_stage SET enabled = false WHERE id = ${existing.id}
6703
+ `);
6704
+ }
6705
+ }
5615
6706
  var args = process.argv.slice(2);
5616
6707
  function getArg2(name) {
5617
6708
  return args.find((a) => a.startsWith(`--${name}=`))?.split("=").slice(1).join("=");
@@ -5853,6 +6944,9 @@ var TOOL_MODULE_MAP = {
5853
6944
  "env-get": "ci_cd",
5854
6945
  "env-store": "ci_cd",
5855
6946
  "release-trigger": "ci_cd",
6947
+ "release-profile-list": "ci_cd",
6948
+ "release-profile-create": "ci_cd",
6949
+ "release-profile-update": "ci_cd",
5856
6950
  "domain-list": "domains",
5857
6951
  "dns-list": "domains",
5858
6952
  "dns-record": "domains",
@@ -6233,6 +7327,23 @@ async function resolveReleaseProfileStageIds(profileName) {
6233
7327
  }
6234
7328
  return { stageIds: stages.map((s) => s.id), profileId: profile.id };
6235
7329
  }
7330
+ async function resolveGitHubTokenIdByKey(key) {
7331
+ const trimmed = key.trim();
7332
+ const rows = await db.execute(sql`
7333
+ SELECT id FROM github_token
7334
+ WHERE label ILIKE ${trimmed} OR owner ILIKE ${trimmed} OR id::text = ${trimmed}
7335
+ ORDER BY label
7336
+ LIMIT 1
7337
+ `);
7338
+ if (rows[0]) return rows[0].id;
7339
+ const all = await db.execute(
7340
+ sql`SELECT label, owner FROM github_token ORDER BY label`
7341
+ );
7342
+ const available = all.map((t) => t.owner ? `${t.label} (${t.owner})` : t.label).join(", ");
7343
+ throw new Error(
7344
+ `GitHub key "${key}" not found. Available keys: ${available || "(none)"}. Omit githubKey to use the default MG Software token.`
7345
+ );
7346
+ }
6236
7347
  async function resolveReleaseTriggerStage(profileName, stageFilter) {
6237
7348
  const profileRows = await db.execute(sql`
6238
7349
  SELECT id, name FROM release_profile
@@ -6324,7 +7435,7 @@ function getEncryptionKey() {
6324
7435
  throw new Error("ENCRYPTION_KEY must be a 64-character hex string");
6325
7436
  return buf;
6326
7437
  }
6327
- function encrypt(text) {
7438
+ function encrypt(text6) {
6328
7439
  const key = getEncryptionKey();
6329
7440
  const iv = randomBytes(ENC_IV_LENGTH);
6330
7441
  const cipher = createCipheriv(
@@ -6332,7 +7443,7 @@ function encrypt(text) {
6332
7443
  new Uint8Array(key),
6333
7444
  new Uint8Array(iv)
6334
7445
  );
6335
- let encrypted = cipher.update(text, "utf8", "hex");
7446
+ let encrypted = cipher.update(text6, "utf8", "hex");
6336
7447
  encrypted += cipher.final("hex");
6337
7448
  const authTag = cipher.getAuthTag();
6338
7449
  return Buffer.concat([
@@ -7052,10 +8163,10 @@ async function r2GetObjectRange(bucket, key, range) {
7052
8163
  const body = result.Body;
7053
8164
  if (!body?.transformToString)
7054
8165
  throw new Error("R2 returned no readable body");
7055
- const text = await body.transformToString();
8166
+ const text6 = await body.transformToString();
7056
8167
  const header = `# range: bytes ${range.offset}-${end} of ${size} (${effectiveLen} bytes)`;
7057
8168
  return `${header}
7058
- ${text}`;
8169
+ ${text6}`;
7059
8170
  } catch (e) {
7060
8171
  throw r2WrapError(bucket, key, e);
7061
8172
  }
@@ -7418,15 +8529,15 @@ async function sftpRead(opts, filePath, proxy, options) {
7418
8529
  clearTimeout(timer);
7419
8530
  cleanup?.();
7420
8531
  cleanup = void 0;
7421
- const text = Buffer.concat(
8532
+ const text6 = Buffer.concat(
7422
8533
  chunks.map((ch) => new Uint8Array(ch))
7423
8534
  ).toString("utf-8");
7424
8535
  if (!isWholeFileRequest) {
7425
8536
  const header = `# range: bytes ${offset}-${offset + effectiveLen - 1} of ${total} (${effectiveLen} bytes)`;
7426
8537
  resolve(`${header}
7427
- ${text}`);
8538
+ ${text6}`);
7428
8539
  } else {
7429
- resolve(text);
8540
+ resolve(text6);
7430
8541
  }
7431
8542
  });
7432
8543
  rs.on("error", (e) => {
@@ -7556,11 +8667,11 @@ function getKnownContainers(serverId, maxAgeMs = 5 * 6e4) {
7556
8667
  if (Date.now() - e.capturedAt > maxAgeMs) return void 0;
7557
8668
  return e.names;
7558
8669
  }
7559
- function truncateForLLM(text, maxBytes) {
7560
- const totalBytes = Buffer.byteLength(text, "utf8");
8670
+ function truncateForLLM(text6, maxBytes) {
8671
+ const totalBytes = Buffer.byteLength(text6, "utf8");
7561
8672
  if (totalBytes <= maxBytes)
7562
- return { text, truncated: false, totalBytes, shownBytes: totalBytes };
7563
- const buf = Buffer.from(text, "utf8");
8673
+ return { text: text6, truncated: false, totalBytes, shownBytes: totalBytes };
8674
+ const buf = Buffer.from(text6, "utf8");
7564
8675
  let cut = maxBytes;
7565
8676
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
7566
8677
  const head = buf.subarray(0, cut).toString("utf8");
@@ -7590,10 +8701,10 @@ function isTransientSshError(stderr, exitCode) {
7590
8701
  function postprocessResult(result, meta) {
7591
8702
  if (!result.content?.length) return result;
7592
8703
  const block = result.content[0];
7593
- let text = String(block.text ?? "");
7594
- const trunc = truncateForLLM(text, RESPONSE_MAX_BYTES);
8704
+ let text6 = String(block.text ?? "");
8705
+ const trunc = truncateForLLM(text6, RESPONSE_MAX_BYTES);
7595
8706
  if (trunc.truncated) {
7596
- text = trunc.text + "\n\n... " + buildTruncationHint(
8707
+ text6 = trunc.text + "\n\n... " + buildTruncationHint(
7597
8708
  meta.toolName,
7598
8709
  meta.args,
7599
8710
  trunc.totalBytes,
@@ -7607,11 +8718,11 @@ function postprocessResult(result, meta) {
7607
8718
  const parts = [`took ${tookStr}`, sizeStr];
7608
8719
  if (meta.serverIdLabel) parts.push(`server: ${meta.serverIdLabel}`);
7609
8720
  if (meta.cached) parts.push("cached");
7610
- text = `${text}
8721
+ text6 = `${text6}
7611
8722
 
7612
8723
  [${parts.join(", ")}]`;
7613
8724
  }
7614
- return { ...result, content: [{ ...block, text }] };
8725
+ return { ...result, content: [{ ...block, text: text6 }] };
7615
8726
  }
7616
8727
  function buildPipelineScript(commands, shell, marker, stopOnError) {
7617
8728
  if (shell === "powershell") {
@@ -8397,11 +9508,11 @@ CREATE TABLE IF NOT EXISTS _mcp_migrations (
8397
9508
  applied_by TEXT
8398
9509
  );
8399
9510
  `.trim();
8400
- function normaliseMigrationSql(sql27) {
8401
- return sql27.replace(/\r\n/g, "\n").trim() + "\n";
9511
+ function normaliseMigrationSql(sql28) {
9512
+ return sql28.replace(/\r\n/g, "\n").trim() + "\n";
8402
9513
  }
8403
- function migrationSha256(sql27) {
8404
- return createHash("sha256").update(normaliseMigrationSql(sql27), "utf8").digest("hex");
9514
+ function migrationSha256(sql28) {
9515
+ return createHash("sha256").update(normaliseMigrationSql(sql28), "utf8").digest("hex");
8405
9516
  }
8406
9517
  function dollarQuoteTag(value) {
8407
9518
  let tag = "_mcp";
@@ -9468,6 +10579,79 @@ var TOOLS = [
9468
10579
  required: ["releaseProfile"]
9469
10580
  }
9470
10581
  },
10582
+ {
10583
+ name: "release-profile-list",
10584
+ description: "List all release profiles with repo, enabled state, release type, GitHub key and a summary of their stages. Requires ci_cd permission.",
10585
+ inputSchema: {
10586
+ type: "object",
10587
+ properties: {},
10588
+ required: []
10589
+ }
10590
+ },
10591
+ {
10592
+ name: "release-profile-create",
10593
+ 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.",
10594
+ inputSchema: {
10595
+ type: "object",
10596
+ properties: {
10597
+ repoFullName: {
10598
+ type: "string",
10599
+ description: 'Repository in "owner/name" format (e.g. MGSoftwareBV/mg-dashboard).'
10600
+ },
10601
+ githubKey: {
10602
+ type: "string",
10603
+ 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.'
10604
+ },
10605
+ name: {
10606
+ type: "string",
10607
+ description: 'Profile name override. Default: title-cased repo name (mg-dashboard \u2192 "Mg Dashboard").'
10608
+ },
10609
+ releaseType: {
10610
+ type: "string",
10611
+ enum: ["dev_only", "prod_only", "both"],
10612
+ description: "Which stages to create (default: both = dev + prod)."
10613
+ }
10614
+ },
10615
+ required: ["repoFullName"]
10616
+ }
10617
+ },
10618
+ {
10619
+ name: "release-profile-update",
10620
+ 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.",
10621
+ inputSchema: {
10622
+ type: "object",
10623
+ properties: {
10624
+ releaseProfile: {
10625
+ type: "string",
10626
+ description: "Profile name or repo full name to locate the profile."
10627
+ },
10628
+ name: { type: "string", description: "New profile name." },
10629
+ enabled: { type: "boolean", description: "Enable/disable the profile." },
10630
+ releaseType: {
10631
+ type: "string",
10632
+ enum: ["dev_only", "prod_only", "both"],
10633
+ description: "New release type \u2014 reconfigures stages accordingly."
10634
+ },
10635
+ githubKey: {
10636
+ type: "string",
10637
+ description: 'Named GitHub key (github_token label or owner). Pass "default" to reset to the default MG Software token.'
10638
+ },
10639
+ useChangesBranch: {
10640
+ type: "boolean",
10641
+ description: "Use a long-lived -changes accumulation branch."
10642
+ },
10643
+ hasTriggerDev: {
10644
+ type: "boolean",
10645
+ description: "Deploy Trigger.dev jobs during production releases."
10646
+ },
10647
+ workDirectory: {
10648
+ type: "string",
10649
+ description: 'Working directory within the repo (e.g. ".").'
10650
+ }
10651
+ },
10652
+ required: ["releaseProfile"]
10653
+ }
10654
+ },
9471
10655
  {
9472
10656
  name: "cache-purge",
9473
10657
  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 +10751,7 @@ var TOOLS = [
9567
10751
  // ----- Trigger.dev -----
9568
10752
  ...TRIGGER_TOOLS
9569
10753
  ];
9570
- var MCP_VERSION = "7.0.0";
10754
+ var MCP_VERSION = "7.0.5";
9571
10755
  async function handleListTools() {
9572
10756
  if (!authContext) return { tools: TOOLS };
9573
10757
  const accessible = TOOLS.filter((tool) => {
@@ -10593,8 +11777,8 @@ ${sample.join("\n")}${files.length > 5 ? `
10593
11777
  };
10594
11778
  const filtered = sortRows(applyFilter(only.rows));
10595
11779
  if (format === "json") {
10596
- const text2 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
10597
- return { content: [{ type: "text", text: text2 }] };
11780
+ const text7 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
11781
+ return { content: [{ type: "text", text: text7 }] };
10598
11782
  }
10599
11783
  if (groupByProject) {
10600
11784
  const groups = /* @__PURE__ */ new Map();
@@ -10621,8 +11805,8 @@ ${sample.join("\n")}${files.length > 5 ? `
10621
11805
  }
10622
11806
  const header = `${"NAMES".padEnd(36)} ${"IMAGE".padEnd(40)} ${"STATUS".padEnd(24)} ${"HEALTH".padEnd(10)} ${"PORTS".padEnd(40)} PROJECT`;
10623
11807
  const body = filtered.map((r) => `${fmtRow(r)} ${r.Project || ""}`);
10624
- const text = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
10625
- return { content: [{ type: "text", text }] };
11808
+ const text6 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
11809
+ return { content: [{ type: "text", text: text6 }] };
10626
11810
  }
10627
11811
  if (format === "json") {
10628
11812
  const lines = [];
@@ -12009,6 +13193,170 @@ LIMIT ${limit};
12009
13193
  throw err;
12010
13194
  }
12011
13195
  }
13196
+ case "release-profile-list": {
13197
+ const profiles = await db.execute(sql`
13198
+ SELECT
13199
+ p.id, p.name, p.repo_full_name, p.enabled, p.release_type,
13200
+ p.work_directory, p.use_changes_branch, p.has_trigger_dev,
13201
+ gt.label AS github_key,
13202
+ (
13203
+ SELECT string_agg(
13204
+ s.stage || ' (' || COALESCE(s.release_branch, '-') ||
13205
+ ', ' || COALESCE(s.trigger_mode, 'manual') ||
13206
+ CASE WHEN s.enabled THEN '' ELSE ', disabled' END || ')',
13207
+ '; ' ORDER BY s.stage_order
13208
+ )
13209
+ FROM release_profile_stage s
13210
+ WHERE s.release_profile_id = p.id
13211
+ ) AS stages
13212
+ FROM release_profile p
13213
+ LEFT JOIN github_token gt ON gt.id = p.github_token_id
13214
+ ORDER BY p.name
13215
+ `);
13216
+ if (profiles.length === 0) {
13217
+ return {
13218
+ content: [{ type: "text", text: "No release profiles found" }]
13219
+ };
13220
+ }
13221
+ const lines = profiles.map((p) => {
13222
+ const flags = [
13223
+ p.enabled ? "enabled" : "disabled",
13224
+ `type=${p.release_type ?? "both"}`,
13225
+ `key=${p.github_key ?? "MG Software (default)"}`,
13226
+ p.use_changes_branch ? "changes-branch" : null,
13227
+ p.has_trigger_dev ? "trigger.dev" : null
13228
+ ].filter(Boolean).join(", ");
13229
+ return `${p.name} \u2014 ${p.repo_full_name} [${flags}]
13230
+ stages: ${p.stages ?? "(none)"}`;
13231
+ });
13232
+ return {
13233
+ content: [
13234
+ {
13235
+ type: "text",
13236
+ text: `${profiles.length} release profile(s):
13237
+
13238
+ ${lines.join("\n\n")}`
13239
+ }
13240
+ ]
13241
+ };
13242
+ }
13243
+ case "release-profile-create": {
13244
+ const repoFullName = String(a.repoFullName ?? "").trim();
13245
+ const githubTokenId = a.githubKey ? await resolveGitHubTokenIdByKey(String(a.githubKey)) : null;
13246
+ const releaseType = a.releaseType ? String(a.releaseType) : null;
13247
+ if (releaseType !== null && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
13248
+ throw new Error(
13249
+ `Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
13250
+ );
13251
+ }
13252
+ try {
13253
+ const result = await quickCreateReleaseProfile(
13254
+ authContext.userId,
13255
+ {
13256
+ repoFullName,
13257
+ githubTokenId,
13258
+ name: a.name ? String(a.name) : null,
13259
+ releaseType
13260
+ }
13261
+ );
13262
+ return {
13263
+ content: [
13264
+ {
13265
+ type: "text",
13266
+ text: JSON.stringify(
13267
+ {
13268
+ profileId: result.profile.id,
13269
+ name: result.profile.name,
13270
+ repo: repoFullName,
13271
+ releaseType: releaseType ?? "both",
13272
+ appsDetected: result.scan.apps.map((app) => ({
13273
+ path: app.path,
13274
+ label: app.label,
13275
+ deployMethod: app.deployMethod
13276
+ })),
13277
+ stages: result.stages.map((s) => ({
13278
+ stage: s.stage,
13279
+ name: s.name,
13280
+ releaseBranch: s.release_branch,
13281
+ triggerMode: s.trigger_mode
13282
+ })),
13283
+ branchWarnings: result.branchWarnings,
13284
+ hint: "Assign deployment servers per stage app in the dashboard (Releases \u2192 Profiles) before triggering a release."
13285
+ },
13286
+ null,
13287
+ 2
13288
+ )
13289
+ }
13290
+ ]
13291
+ };
13292
+ } catch (err) {
13293
+ if (err instanceof ReleaseProfileQuickCreateError) {
13294
+ throw new Error(err.message);
13295
+ }
13296
+ throw err;
13297
+ }
13298
+ }
13299
+ case "release-profile-update": {
13300
+ const identifier = String(a.releaseProfile ?? "").trim();
13301
+ const profileRows = await db.execute(sql`
13302
+ SELECT id, name FROM release_profile
13303
+ WHERE name ILIKE ${identifier} OR repo_full_name ILIKE ${identifier}
13304
+ LIMIT 1
13305
+ `);
13306
+ const profile = profileRows[0];
13307
+ if (!profile) {
13308
+ const all = await db.execute(
13309
+ sql`SELECT name FROM release_profile ORDER BY name`
13310
+ );
13311
+ throw new Error(
13312
+ `Release profile "${identifier}" not found. Available profiles: ${all.map((p) => p.name).join(", ") || "(none)"}`
13313
+ );
13314
+ }
13315
+ const releaseType = a.releaseType ? String(a.releaseType) : void 0;
13316
+ if (releaseType !== void 0 && releaseType !== "dev_only" && releaseType !== "prod_only" && releaseType !== "both") {
13317
+ throw new Error(
13318
+ `Invalid releaseType "${releaseType}". Expected dev_only, prod_only, or both.`
13319
+ );
13320
+ }
13321
+ let githubTokenId;
13322
+ if (a.githubKey !== void 0) {
13323
+ const key = String(a.githubKey).trim();
13324
+ githubTokenId = !key || key.toLowerCase() === "default" ? null : await resolveGitHubTokenIdByKey(key);
13325
+ }
13326
+ try {
13327
+ const result = await updateReleaseProfileSettings(profile.id, {
13328
+ name: a.name !== void 0 ? String(a.name) : void 0,
13329
+ enabled: a.enabled !== void 0 ? a.enabled === true : void 0,
13330
+ releaseType,
13331
+ githubTokenId,
13332
+ useChangesBranch: a.useChangesBranch !== void 0 ? a.useChangesBranch === true : void 0,
13333
+ hasTriggerDev: a.hasTriggerDev !== void 0 ? a.hasTriggerDev === true : void 0,
13334
+ workDirectory: a.workDirectory !== void 0 ? String(a.workDirectory) : void 0
13335
+ });
13336
+ return {
13337
+ content: [
13338
+ {
13339
+ type: "text",
13340
+ text: JSON.stringify(
13341
+ {
13342
+ profileId: profile.id,
13343
+ name: result.profile.name,
13344
+ updated: true,
13345
+ stagesReconfigured: result.stagesReconfigured
13346
+ },
13347
+ null,
13348
+ 2
13349
+ )
13350
+ }
13351
+ ]
13352
+ };
13353
+ } catch (err) {
13354
+ if (err instanceof ReleaseProfileQuickCreateError) {
13355
+ throw new Error(err.message);
13356
+ }
13357
+ throw err;
13358
+ }
13359
+ }
12012
13360
  // ----- Cache Purge -----
12013
13361
  case "cache-purge": {
12014
13362
  const { conn, proxy } = await getServerConnection(String(a.serverId));
@@ -12031,7 +13379,7 @@ LIMIT ${limit};
12031
13379
  // ----- Domains (mijn.host) -----
12032
13380
  case "domain-list": {
12033
13381
  const res = await mijnhostFetch(
12034
- "/domains"
13382
+ "/domains/"
12035
13383
  );
12036
13384
  const domains = res.data.domains;
12037
13385
  if (!domains.length) {