@stacksjs/ts-cloud 0.5.34 → 0.6.0
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/aws/cloudwatch.d.ts +2 -0
- package/dist/aws/xray.d.ts +45 -0
- package/dist/bin/cli.js +723 -701
- package/dist/deploy/dashboard-database.d.ts +25 -0
- package/dist/deploy/dashboard-operations.d.ts +7 -0
- package/dist/deploy/firewall-config-editor.d.ts +15 -0
- package/dist/deploy/firewall-config-editor.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/management-dashboard.d.ts +26 -3
- package/dist/deploy/serverless-operations.d.ts +150 -0
- package/dist/deploy/serverless-operations.test.d.ts +1 -0
- package/dist/deploy/site-config-editor.d.ts +6 -0
- package/dist/deploy/ssh-config-editor.d.ts +1 -0
- package/dist/deploy/terminal-session.d.ts +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1008 -33
- package/dist/ui/index.html +4 -4
- package/dist/ui/server/actions.html +34 -5
- package/dist/ui/server/activity.html +1 -1
- package/dist/ui/server/backups.html +4 -4
- package/dist/ui/server/database.html +27 -5
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +1 -1
- package/dist/ui/server/firewall.html +1233 -0
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +1 -1
- package/dist/ui/server/security.html +1 -1
- package/dist/ui/server/services.html +4 -4
- package/dist/ui/server/sites.html +24 -5
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/terminal.html +1191 -0
- package/dist/ui/server/workers.html +4 -4
- package/dist/ui/serverless/alarms.html +1236 -0
- package/dist/ui/serverless/assets.html +53 -1
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +58 -1
- package/dist/ui/serverless/deployments.html +53 -1
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +63 -1
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +94 -1
- package/dist/ui/serverless/scheduler.html +55 -1
- package/dist/ui/serverless/secrets.html +99 -1
- package/dist/ui/serverless/traces.html +1177 -0
- package/dist/ui/serverless.html +87 -1
- package/dist/ui-src/pages/index.stx +1 -1
- package/dist/ui-src/pages/partials/head.stx +6 -6
- package/dist/ui-src/pages/partials/nav.stx +4 -3
- package/dist/ui-src/pages/server/actions.stx +34 -1
- package/dist/ui-src/pages/server/backups.stx +1 -1
- package/dist/ui-src/pages/server/database.stx +32 -9
- package/dist/ui-src/pages/server/deployments.stx +2 -2
- package/dist/ui-src/pages/server/firewall.stx +134 -0
- package/dist/ui-src/pages/server/logs.stx +2 -2
- package/dist/ui-src/pages/server/metrics.stx +5 -5
- package/dist/ui-src/pages/server/security.stx +1 -1
- package/dist/ui-src/pages/server/services.stx +1 -1
- package/dist/ui-src/pages/server/sites.stx +24 -3
- package/dist/ui-src/pages/server/ssh-keys.stx +2 -2
- package/dist/ui-src/pages/server/terminal.stx +65 -0
- package/dist/ui-src/pages/server/workers.stx +1 -1
- package/dist/ui-src/pages/serverless/alarms.stx +153 -0
- package/dist/ui-src/pages/serverless/assets.stx +29 -2
- package/dist/ui-src/pages/serverless/cost.stx +4 -4
- package/dist/ui-src/pages/serverless/data.stx +56 -3
- package/dist/ui-src/pages/serverless/deployments.stx +30 -6
- package/dist/ui-src/pages/serverless/firewall.stx +1 -1
- package/dist/ui-src/pages/serverless/functions.stx +62 -4
- package/dist/ui-src/pages/serverless/logs.stx +2 -2
- package/dist/ui-src/pages/serverless/metrics.stx +5 -1
- package/dist/ui-src/pages/serverless/queues.stx +90 -26
- package/dist/ui-src/pages/serverless/scheduler.stx +31 -2
- package/dist/ui-src/pages/serverless/secrets.stx +94 -14
- package/dist/ui-src/pages/serverless/traces.stx +88 -0
- package/dist/ui-src/pages/serverless.stx +87 -24
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -697,6 +697,30 @@ function isNodeCryptoAvailable() {
|
|
|
697
697
|
function isWebCryptoAvailable() {
|
|
698
698
|
return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined";
|
|
699
699
|
}
|
|
700
|
+
function detectDeploymentTargets(config6) {
|
|
701
|
+
const server = config6.infrastructure?.compute != null;
|
|
702
|
+
const serverless = Object.values(config6.environments ?? {}).some((env) => env?.app != null);
|
|
703
|
+
return { server, serverless };
|
|
704
|
+
}
|
|
705
|
+
function resolveDeploymentMode(config6) {
|
|
706
|
+
if (config6.mode === "server" || config6.mode === "serverless")
|
|
707
|
+
return config6.mode;
|
|
708
|
+
const { serverless } = detectDeploymentTargets(config6);
|
|
709
|
+
return serverless ? "serverless" : "server";
|
|
710
|
+
}
|
|
711
|
+
function deploymentCoexistenceError(config6) {
|
|
712
|
+
const { server, serverless } = detectDeploymentTargets(config6);
|
|
713
|
+
if (server && serverless) {
|
|
714
|
+
return "A project cannot be both a server and a serverless deployment. " + "Found `infrastructure.compute` (server) AND `environments.<env>.app` (serverless Lambda). " + "Keep one: remove `infrastructure.compute` for a serverless app, or remove `environments.<env>.app` for a server.";
|
|
715
|
+
}
|
|
716
|
+
if (config6.mode === "server" && serverless && !server) {
|
|
717
|
+
return "Config sets `mode: 'server'` but declares a serverless app (`environments.<env>.app`). " + "Remove the app or change the mode.";
|
|
718
|
+
}
|
|
719
|
+
if (config6.mode === "serverless" && server && !serverless) {
|
|
720
|
+
return "Config sets `mode: 'serverless'` but declares a server (`infrastructure.compute`). " + "Remove the compute block or change the mode.";
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
700
724
|
function resolveCloudProvider(config6) {
|
|
701
725
|
if (config6.cloud?.provider)
|
|
702
726
|
return config6.cloud.provider;
|
|
@@ -6052,7 +6076,7 @@ function createFullStackAppPreset(options) {
|
|
|
6052
6076
|
slug,
|
|
6053
6077
|
region: "us-east-1"
|
|
6054
6078
|
},
|
|
6055
|
-
mode: "
|
|
6079
|
+
mode: "server",
|
|
6056
6080
|
environments: {
|
|
6057
6081
|
production: {
|
|
6058
6082
|
type: "production",
|
|
@@ -9729,8 +9753,8 @@ function validateAgainstSchema(config6) {
|
|
|
9729
9753
|
if (config6.project?.region && !validRegions.includes(config6.project.region)) {
|
|
9730
9754
|
errors.push(`Invalid region: ${config6.project.region}. Must be one of: ${validRegions.join(", ")}`);
|
|
9731
9755
|
}
|
|
9732
|
-
if (config6.mode && !["server", "serverless"
|
|
9733
|
-
errors.push("Invalid mode: must be one of server
|
|
9756
|
+
if (config6.mode && !["server", "serverless"].includes(config6.mode)) {
|
|
9757
|
+
errors.push("Invalid mode: must be one of server or serverless");
|
|
9734
9758
|
}
|
|
9735
9759
|
return {
|
|
9736
9760
|
valid: errors.length === 0,
|
|
@@ -42108,9 +42132,8 @@ Details:`;
|
|
|
42108
42132
|
},
|
|
42109
42133
|
mode: {
|
|
42110
42134
|
type: "string",
|
|
42111
|
-
description: "Deployment mode",
|
|
42112
|
-
enum: ["server", "serverless"
|
|
42113
|
-
default: "serverless"
|
|
42135
|
+
description: "Deployment mode (server and serverless are mutually exclusive; auto-detected when omitted)",
|
|
42136
|
+
enum: ["server", "serverless"]
|
|
42114
42137
|
},
|
|
42115
42138
|
environments: {
|
|
42116
42139
|
type: "object",
|
|
@@ -55774,6 +55797,11 @@ var init_email = __esm(() => {
|
|
|
55774
55797
|
});
|
|
55775
55798
|
|
|
55776
55799
|
// src/aws/rds.ts
|
|
55800
|
+
var exports_rds = {};
|
|
55801
|
+
__export(exports_rds, {
|
|
55802
|
+
RDSClient: () => RDSClient
|
|
55803
|
+
});
|
|
55804
|
+
|
|
55777
55805
|
class RDSClient {
|
|
55778
55806
|
client;
|
|
55779
55807
|
region;
|
|
@@ -83323,6 +83351,7 @@ class AcmeClient {
|
|
|
83323
83351
|
}
|
|
83324
83352
|
}
|
|
83325
83353
|
// src/deploy/site-target.ts
|
|
83354
|
+
await init_dist2();
|
|
83326
83355
|
var PHP_SITE_TYPES = new Set([
|
|
83327
83356
|
"laravel",
|
|
83328
83357
|
"php",
|
|
@@ -83359,6 +83388,9 @@ function validateDeploymentConfig(config6) {
|
|
|
83359
83388
|
const warnings = [];
|
|
83360
83389
|
const sites = config6.sites || {};
|
|
83361
83390
|
const computeConfigured = hasComputeConfigured(config6);
|
|
83391
|
+
const coexistence = deploymentCoexistenceError(config6);
|
|
83392
|
+
if (coexistence)
|
|
83393
|
+
errors.push(coexistence);
|
|
83362
83394
|
const portOwners = new Map;
|
|
83363
83395
|
for (const [name, site] of Object.entries(sites)) {
|
|
83364
83396
|
if (!site) {
|
|
@@ -84034,6 +84066,24 @@ async function serverlessInfo(config6, environment) {
|
|
|
84034
84066
|
lastRelease: release ? { sha: release.sha, timestamp: release.timestamp } : undefined
|
|
84035
84067
|
};
|
|
84036
84068
|
}
|
|
84069
|
+
function serverlessClusterId(slug, environment) {
|
|
84070
|
+
return `${slug}-${environment}-db`;
|
|
84071
|
+
}
|
|
84072
|
+
async function scaleServerlessDatabase(config6, environment, minCapacity, maxCapacity) {
|
|
84073
|
+
const ctx = resolveContext(config6, environment);
|
|
84074
|
+
if (ctx.app.database?.connection !== "aurora-serverless")
|
|
84075
|
+
throw new Error("No Aurora Serverless v2 database is configured for this environment.");
|
|
84076
|
+
const { RDSClient: RDSClient2 } = await Promise.resolve().then(() => (init_rds(), exports_rds));
|
|
84077
|
+
const rds2 = new RDSClient2(ctx.region);
|
|
84078
|
+
const id = serverlessClusterId(ctx.slug, environment);
|
|
84079
|
+
step(`Scaling ${id} → ${minCapacity}–${maxCapacity} ACUs`);
|
|
84080
|
+
await rds2.modifyDBCluster({
|
|
84081
|
+
DBClusterIdentifier: id,
|
|
84082
|
+
ServerlessV2ScalingConfiguration: { MinCapacity: minCapacity, MaxCapacity: maxCapacity },
|
|
84083
|
+
ApplyImmediately: true
|
|
84084
|
+
});
|
|
84085
|
+
success(`Scaling applied to ${id} (takes effect shortly).`);
|
|
84086
|
+
}
|
|
84037
84087
|
|
|
84038
84088
|
// src/deploy/index.ts
|
|
84039
84089
|
await init_serverless_image();
|
|
@@ -84165,6 +84215,15 @@ class CloudWatchClient {
|
|
|
84165
84215
|
});
|
|
84166
84216
|
await this.query(params);
|
|
84167
84217
|
}
|
|
84218
|
+
async deleteAlarms(alarmNames) {
|
|
84219
|
+
if (!alarmNames.length)
|
|
84220
|
+
return;
|
|
84221
|
+
const params = { Action: "DeleteAlarms" };
|
|
84222
|
+
alarmNames.forEach((name, i) => {
|
|
84223
|
+
params[`AlarmNames.member.${i + 1}`] = name;
|
|
84224
|
+
});
|
|
84225
|
+
await this.query(params);
|
|
84226
|
+
}
|
|
84168
84227
|
}
|
|
84169
84228
|
|
|
84170
84229
|
// src/deploy/dashboard-data.ts
|
|
@@ -84500,6 +84559,17 @@ async function resolveDashboardData(config6, environment) {
|
|
|
84500
84559
|
} catch {}
|
|
84501
84560
|
} catch {}
|
|
84502
84561
|
}
|
|
84562
|
+
const dataServices = [];
|
|
84563
|
+
if (out.aurora)
|
|
84564
|
+
dataServices.push({ name: "Aurora Serverless v2", detail: `${out.aurora.id} · ${out.aurora.minAcu}-${out.aurora.maxAcu} ACU`, status: out.aurora.status });
|
|
84565
|
+
if (out.proxy)
|
|
84566
|
+
dataServices.push({ name: "RDS Proxy", detail: out.proxy.name, status: out.proxy.status });
|
|
84567
|
+
if (out.redis)
|
|
84568
|
+
dataServices.push({ name: `ElastiCache (${out.redis.engine})`, detail: `${out.redis.node} · ${out.redis.hitRate}% hit rate`, status: out.redis.status });
|
|
84569
|
+
if (out.efs)
|
|
84570
|
+
dataServices.push({ name: "EFS", detail: `${out.efs.mount} · ${out.efs.sizeMb} MB`, status: out.efs.status });
|
|
84571
|
+
if (dataServices.length)
|
|
84572
|
+
out.dataServices = dataServices;
|
|
84503
84573
|
if (info2.scheduler !== "off" && app?.kind === "php") {
|
|
84504
84574
|
try {
|
|
84505
84575
|
const out2 = await runRemoteCommand(config6, environment, "schedule:list");
|
|
@@ -84517,6 +84587,7 @@ async function resolveDashboardData(config6, environment) {
|
|
|
84517
84587
|
return out;
|
|
84518
84588
|
}
|
|
84519
84589
|
// src/deploy/local-dashboard-server.ts
|
|
84590
|
+
await init_dist2();
|
|
84520
84591
|
import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
|
|
84521
84592
|
import { readFile as readFile2, writeFile as writeFile6 } from "node:fs/promises";
|
|
84522
84593
|
import { mkdtempSync as mkdtempSync6 } from "node:fs";
|
|
@@ -84525,7 +84596,7 @@ import { dirname as dirname9, extname as extname3, join as join23, normalize } f
|
|
|
84525
84596
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
84526
84597
|
|
|
84527
84598
|
// src/deploy/dashboard-data-server.ts
|
|
84528
|
-
import { existsSync as existsSync24, readFileSync as
|
|
84599
|
+
import { existsSync as existsSync24, readFileSync as readFileSync18 } from "node:fs";
|
|
84529
84600
|
import { join as join21 } from "node:path";
|
|
84530
84601
|
|
|
84531
84602
|
// src/drivers/factory.ts
|
|
@@ -85507,6 +85578,11 @@ ${servicesProvision.join(`
|
|
|
85507
85578
|
}
|
|
85508
85579
|
if (runtime === "bun") {
|
|
85509
85580
|
script += `
|
|
85581
|
+
# cloud-init runs this with \`set -u\` and NO $HOME set. bun's install.sh
|
|
85582
|
+
# dereferences $HOME even when BUN_INSTALL is set, so without this the install
|
|
85583
|
+
# aborts with "HOME: unbound variable" and /usr/local/bin/bun never appears —
|
|
85584
|
+
# breaking a from-scratch provision (adopting an existing box masked it).
|
|
85585
|
+
export HOME="\${HOME:-/root}"
|
|
85510
85586
|
export BUN_INSTALL="/root/.bun"
|
|
85511
85587
|
curl -fsSL https://bun.sh/install | bash${runtimeVersion === "latest" ? "" : ` -s "bun-v${runtimeVersion}"`}
|
|
85512
85588
|
ln -sf /root/.bun/bin/bun /usr/local/bin/bun
|
|
@@ -87396,11 +87472,42 @@ await init_dist2();
|
|
|
87396
87472
|
// src/deploy/management-dashboard.ts
|
|
87397
87473
|
await init_dist2();
|
|
87398
87474
|
import { execSync as execSync5 } from "node:child_process";
|
|
87399
|
-
import {
|
|
87475
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
87476
|
+
import { chmodSync, existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "node:fs";
|
|
87400
87477
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
87401
87478
|
import { dirname as dirname8, isAbsolute as isAbsolute5, join as join20 } from "node:path";
|
|
87402
87479
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
87403
87480
|
var MANAGEMENT_DASHBOARD_SITE = "dashboard";
|
|
87481
|
+
var DASHBOARD_CREDENTIALS_FILE = join20(".ts-cloud", "dashboard-credentials.json");
|
|
87482
|
+
function generatePassword() {
|
|
87483
|
+
return randomBytes6(24).toString("base64url");
|
|
87484
|
+
}
|
|
87485
|
+
function resolveDashboardAuth(cwd, username, logger4) {
|
|
87486
|
+
const explicit = process.env.TS_CLOUD_UI_PASSWORD?.trim();
|
|
87487
|
+
if (explicit)
|
|
87488
|
+
return { password: explicit, source: "env" };
|
|
87489
|
+
if (truthy(process.env.TS_CLOUD_UI_PUBLIC))
|
|
87490
|
+
return { password: undefined, source: "public" };
|
|
87491
|
+
const file = join20(cwd, DASHBOARD_CREDENTIALS_FILE);
|
|
87492
|
+
try {
|
|
87493
|
+
if (existsSync21(file)) {
|
|
87494
|
+
const saved = JSON.parse(readFileSync17(file, "utf8"));
|
|
87495
|
+
if (saved?.password)
|
|
87496
|
+
return { password: saved.password, source: "generated" };
|
|
87497
|
+
}
|
|
87498
|
+
} catch {}
|
|
87499
|
+
const password = generatePassword();
|
|
87500
|
+
try {
|
|
87501
|
+
mkdirSync9(dirname8(file), { recursive: true });
|
|
87502
|
+
writeFileSync12(file, `${JSON.stringify({ username, password, generatedAt: new Date().toISOString() }, null, 2)}
|
|
87503
|
+
`);
|
|
87504
|
+
chmodSync(file, 384);
|
|
87505
|
+
logger4.info(`Management dashboard: generated a password and saved it to ${DASHBOARD_CREDENTIALS_FILE} (user: ${username}, pass: ${password}). Set TS_CLOUD_UI_PASSWORD to pin your own, or TS_CLOUD_UI_PUBLIC=1 to serve without auth.`);
|
|
87506
|
+
} catch (error2) {
|
|
87507
|
+
logger4.warn(`Management dashboard: could not persist the generated password (${error2?.message ?? error2}). Using it for this deploy only — pass: ${password}`);
|
|
87508
|
+
}
|
|
87509
|
+
return { password, source: "generated" };
|
|
87510
|
+
}
|
|
87404
87511
|
var noopLogger = { info: () => {}, warn: () => {} };
|
|
87405
87512
|
function truthy(v) {
|
|
87406
87513
|
return v != null && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
@@ -87436,7 +87543,8 @@ function ensureManagementDashboard(config6, options = {}) {
|
|
|
87436
87543
|
logger4.info("Management dashboard: UI not found (no local ui/ or packaged dist/ui) — skipping auto-deploy.");
|
|
87437
87544
|
return config6;
|
|
87438
87545
|
}
|
|
87439
|
-
const
|
|
87546
|
+
const username = process.env.TS_CLOUD_UI_USERNAME?.trim() || "admin";
|
|
87547
|
+
const auth = resolveDashboardAuth(cwd, username, logger4);
|
|
87440
87548
|
const environment = config6.environments && Object.keys(config6.environments)[0];
|
|
87441
87549
|
const live = truthy(process.env.TS_CLOUD_UI_LIVE);
|
|
87442
87550
|
const port = Number(process.env.TS_CLOUD_UI_PORT) || undefined;
|
|
@@ -87444,8 +87552,8 @@ function ensureManagementDashboard(config6, options = {}) {
|
|
|
87444
87552
|
uiRoot: ui.uiRoot,
|
|
87445
87553
|
build: ui.build,
|
|
87446
87554
|
domain: process.env.TS_CLOUD_UI_DOMAIN?.trim() || undefined,
|
|
87447
|
-
username
|
|
87448
|
-
password,
|
|
87555
|
+
username,
|
|
87556
|
+
password: auth.password,
|
|
87449
87557
|
realm: process.env.TS_CLOUD_UI_REALM?.trim() || undefined,
|
|
87450
87558
|
live,
|
|
87451
87559
|
port
|
|
@@ -87455,7 +87563,8 @@ function ensureManagementDashboard(config6, options = {}) {
|
|
|
87455
87563
|
return config6;
|
|
87456
87564
|
}
|
|
87457
87565
|
config6.sites = { ...config6.sites ?? {}, [resolved.name]: resolved.site };
|
|
87458
|
-
|
|
87566
|
+
const authNote = auth.source === "public" ? "NO AUTH — TS_CLOUD_UI_PUBLIC is set (dashboard is publicly reachable)" : auth.source === "env" ? "htpasswd-protected (TS_CLOUD_UI_PASSWORD)" : `htpasswd-protected (auto-generated — see ${DASHBOARD_CREDENTIALS_FILE})`;
|
|
87567
|
+
logger4.info(`Management dashboard → https://${resolved.site.domain} (${authNote})`);
|
|
87459
87568
|
return config6;
|
|
87460
87569
|
}
|
|
87461
87570
|
function buildManagementDashboardArtifact(site, options) {
|
|
@@ -88525,7 +88634,7 @@ function loadLocalState(config6, environment) {
|
|
|
88525
88634
|
if (!existsSync24(statePath))
|
|
88526
88635
|
return null;
|
|
88527
88636
|
try {
|
|
88528
|
-
return JSON.parse(
|
|
88637
|
+
return JSON.parse(readFileSync18(statePath, "utf8"));
|
|
88529
88638
|
} catch {
|
|
88530
88639
|
return null;
|
|
88531
88640
|
}
|
|
@@ -89034,6 +89143,9 @@ function resolveConfigOnlyServerDashboardData(config6, environment) {
|
|
|
89034
89143
|
root: kind === "server-static" ? `/var/www/${s.name}` : `/var/www/${s.name}/current`,
|
|
89035
89144
|
branch: kind === "server-static" ? "build artifact" : "main",
|
|
89036
89145
|
build: site.build,
|
|
89146
|
+
php: site.php ?? site.phpVersion,
|
|
89147
|
+
aliases: Array.isArray(site.aliases) ? site.aliases : [],
|
|
89148
|
+
redirects: site.redirects && typeof site.redirects === "object" ? site.redirects : {},
|
|
89037
89149
|
envKeys: Object.keys(site.env ?? site.environment ?? {})
|
|
89038
89150
|
};
|
|
89039
89151
|
}),
|
|
@@ -89307,6 +89419,45 @@ async function createDatabaseUser(config6, environment, input) {
|
|
|
89307
89419
|
const engine = resolveDbEngine(config6);
|
|
89308
89420
|
return runDb(config6, environment, buildCreateUserScript(engine, input), `ts-cloud db:user ${input.username}`);
|
|
89309
89421
|
}
|
|
89422
|
+
var DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
|
|
89423
|
+
function buildBackupScript(engine, name, destDir = DB_BACKUP_DIR) {
|
|
89424
|
+
const file = `${destDir}/${name}-$(date +%Y%m%d-%H%M%S).sql.gz`;
|
|
89425
|
+
const mkdir5 = `mkdir -p ${destDir}`;
|
|
89426
|
+
if (engine === "postgres")
|
|
89427
|
+
return [mkdir5, `pg_dump -h 127.0.0.1 -p 5432 -U postgres ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
|
|
89428
|
+
const sock = engine === "mariadb" ? SOCKETS.mariadb : SOCKETS.mysql;
|
|
89429
|
+
return [mkdir5, `mysqldump --socket=${sock} -u root ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
|
|
89430
|
+
}
|
|
89431
|
+
function buildListBackupsScript(destDir = DB_BACKUP_DIR) {
|
|
89432
|
+
return [`ls -1t ${destDir}/*.sql.gz 2>/dev/null | head -50 | sed 's|^|BACKUP=|' || true`];
|
|
89433
|
+
}
|
|
89434
|
+
function parseBackups(output) {
|
|
89435
|
+
const out = [];
|
|
89436
|
+
for (const rawLine of output.split(`
|
|
89437
|
+
`)) {
|
|
89438
|
+
const line = rawLine.trim();
|
|
89439
|
+
if (!line.startsWith("BACKUP="))
|
|
89440
|
+
continue;
|
|
89441
|
+
const file = line.slice(7);
|
|
89442
|
+
const base = file.split("/").pop() ?? file;
|
|
89443
|
+
const database = base.replace(/-\d{8}-\d{6}\.sql\.gz$/, "");
|
|
89444
|
+
if (file)
|
|
89445
|
+
out.push({ file, database });
|
|
89446
|
+
}
|
|
89447
|
+
return out;
|
|
89448
|
+
}
|
|
89449
|
+
async function backupDatabase(config6, environment, name) {
|
|
89450
|
+
if (!isValidDbIdentifier(name))
|
|
89451
|
+
return { ok: false, error: "Database name must be a valid identifier.", database: name };
|
|
89452
|
+
const engine = resolveDbEngine(config6);
|
|
89453
|
+
const r = await runDb(config6, environment, buildBackupScript(engine, name), `ts-cloud db:backup ${name}`);
|
|
89454
|
+
return { ...r, database: name };
|
|
89455
|
+
}
|
|
89456
|
+
async function listDatabaseBackups(config6, environment) {
|
|
89457
|
+
const r = await runDb(config6, environment, buildListBackupsScript(), "ts-cloud db:backups");
|
|
89458
|
+
const backups = r.ok && r.stdout ? parseBackups(r.stdout) : [];
|
|
89459
|
+
return { ...r, backups };
|
|
89460
|
+
}
|
|
89310
89461
|
|
|
89311
89462
|
// src/drivers/shared/compute-ops.ts
|
|
89312
89463
|
var noopLogger3 = {
|
|
@@ -89425,8 +89576,17 @@ function buildDashboardOperations(config6, data) {
|
|
|
89425
89576
|
for (const [name, site] of Object.entries(config6.sites ?? {})) {
|
|
89426
89577
|
if (!site || !isSafeSiteName(name))
|
|
89427
89578
|
continue;
|
|
89428
|
-
if (site.scheduler || site.schedule)
|
|
89429
|
-
|
|
89579
|
+
if (site.scheduler || site.schedule) {
|
|
89580
|
+
const laravel = resolveSiteFramework(site) === "laravel";
|
|
89581
|
+
ops.push({
|
|
89582
|
+
id: `scheduler:run:${name}`,
|
|
89583
|
+
label: `${laravel ? "Run" : "Restart"} scheduler (${name})`,
|
|
89584
|
+
group: "scheduler",
|
|
89585
|
+
target: name,
|
|
89586
|
+
mutates: true,
|
|
89587
|
+
confirm: name
|
|
89588
|
+
});
|
|
89589
|
+
}
|
|
89430
89590
|
}
|
|
89431
89591
|
const backups = config6.infrastructure?.compute?.backups;
|
|
89432
89592
|
if (backups?.enabled) {
|
|
@@ -89454,6 +89614,28 @@ function siteArtisanCommand(site, artisan) {
|
|
|
89454
89614
|
`cd ${current} && ${pantryEnvEval()} && php artisan ${artisan}`
|
|
89455
89615
|
];
|
|
89456
89616
|
}
|
|
89617
|
+
function workerRestartCommand(framework, slug, site) {
|
|
89618
|
+
if (framework === "laravel")
|
|
89619
|
+
return siteArtisanCommand(site, "queue:restart");
|
|
89620
|
+
const pattern = `${slug}-${site}-queue-*.service`;
|
|
89621
|
+
return [
|
|
89622
|
+
"set -uo pipefail",
|
|
89623
|
+
`units=$(systemctl list-units --type=service --all --no-legend '${pattern}' 2>/dev/null | awk '{print $1}')`,
|
|
89624
|
+
`[ -n "$units" ] || { echo "no queue-worker units found for ${site} (${pattern})" >&2; exit 1; }`,
|
|
89625
|
+
"systemctl restart $units",
|
|
89626
|
+
`systemctl --no-legend --type=service list-units '${pattern}' 2>/dev/null || true`
|
|
89627
|
+
];
|
|
89628
|
+
}
|
|
89629
|
+
function schedulerRunCommand(framework, slug, site) {
|
|
89630
|
+
if (framework === "laravel")
|
|
89631
|
+
return siteArtisanCommand(site, "schedule:run");
|
|
89632
|
+
const unit = `${slug}-${site}-scheduler.service`;
|
|
89633
|
+
return [
|
|
89634
|
+
"set -uo pipefail",
|
|
89635
|
+
`systemctl restart ${unit}`,
|
|
89636
|
+
`systemctl is-active ${unit} 2>/dev/null || true`
|
|
89637
|
+
];
|
|
89638
|
+
}
|
|
89457
89639
|
function backupRunCommand() {
|
|
89458
89640
|
return [
|
|
89459
89641
|
"set -uo pipefail",
|
|
@@ -89498,11 +89680,13 @@ async function runDashboardOperation(config6, environment, operation, options =
|
|
|
89498
89680
|
commands = serviceCommand(verb, rest.join(":"));
|
|
89499
89681
|
command = `systemctl ${verb} ${operation.target}`;
|
|
89500
89682
|
} else if (operation.group === "worker") {
|
|
89501
|
-
|
|
89502
|
-
|
|
89683
|
+
const framework = resolveSiteFramework(config6.sites?.[operation.target] ?? {});
|
|
89684
|
+
commands = workerRestartCommand(framework, slug, operation.target);
|
|
89685
|
+
command = framework === "laravel" ? `queue:restart (${operation.target})` : `restart queue units (${operation.target})`;
|
|
89503
89686
|
} else if (operation.group === "scheduler") {
|
|
89504
|
-
|
|
89505
|
-
|
|
89687
|
+
const framework = resolveSiteFramework(config6.sites?.[operation.target] ?? {});
|
|
89688
|
+
commands = schedulerRunCommand(framework, slug, operation.target);
|
|
89689
|
+
command = framework === "laravel" ? `schedule:run (${operation.target})` : `restart scheduler (${operation.target})`;
|
|
89506
89690
|
} else if (operation.id === "backup:run") {
|
|
89507
89691
|
commands = backupRunCommand();
|
|
89508
89692
|
command = "ts-cloud backup";
|
|
@@ -89523,6 +89707,519 @@ async function runDashboardOperation(config6, environment, operation, options =
|
|
|
89523
89707
|
stderr: clampOutput(result.perInstance?.[0]?.error ?? result.error ?? "")
|
|
89524
89708
|
};
|
|
89525
89709
|
}
|
|
89710
|
+
async function runServerShellCommand(config6, environment, command) {
|
|
89711
|
+
const trimmed = command.trim();
|
|
89712
|
+
if (!trimmed)
|
|
89713
|
+
return { operation: "command", ok: false, error: "A command is required." };
|
|
89714
|
+
let driver;
|
|
89715
|
+
try {
|
|
89716
|
+
driver = createCloudDriver({ config: config6 });
|
|
89717
|
+
} catch (error2) {
|
|
89718
|
+
return { operation: "command", ok: false, error: `Could not initialize the cloud driver: ${error2?.message ?? error2}` };
|
|
89719
|
+
}
|
|
89720
|
+
const slug = config6.project.slug;
|
|
89721
|
+
const targets = await driver.findComputeTargets({ slug, environment, role: "app" });
|
|
89722
|
+
if (!targets.length)
|
|
89723
|
+
return { operation: "command", ok: false, error: "No app server target was found for this environment." };
|
|
89724
|
+
const result = await driver.runRemoteDeploy({
|
|
89725
|
+
targets: [targets[0]],
|
|
89726
|
+
commands: ["set -o pipefail", trimmed],
|
|
89727
|
+
comment: "ts-cloud dashboard:command",
|
|
89728
|
+
tags: { Project: slug, Environment: environment, Role: "app" }
|
|
89729
|
+
});
|
|
89730
|
+
return {
|
|
89731
|
+
operation: "command",
|
|
89732
|
+
command: trimmed,
|
|
89733
|
+
ok: result.success,
|
|
89734
|
+
stdout: clampOutput(result.perInstance?.[0]?.output ?? ""),
|
|
89735
|
+
stderr: clampOutput(result.perInstance?.[0]?.error ?? result.error ?? "")
|
|
89736
|
+
};
|
|
89737
|
+
}
|
|
89738
|
+
|
|
89739
|
+
// src/deploy/firewall-config-editor.ts
|
|
89740
|
+
var ALWAYS_OPEN = new Set([22, 80, 443]);
|
|
89741
|
+
function isValidPort(port) {
|
|
89742
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
89743
|
+
}
|
|
89744
|
+
function normalizePorts(ports) {
|
|
89745
|
+
return [...new Set(ports.filter((p) => isValidPort(p) && !ALWAYS_OPEN.has(p)))].sort((a, b) => a - b);
|
|
89746
|
+
}
|
|
89747
|
+
function addFirewallPort(configText, port, existing = []) {
|
|
89748
|
+
if (!isValidPort(port))
|
|
89749
|
+
throw new Error("Port must be an integer between 1 and 65535.");
|
|
89750
|
+
if (ALWAYS_OPEN.has(port))
|
|
89751
|
+
throw new Error(`Port ${port} (SSH/HTTP/HTTPS) is always open and is not managed here.`);
|
|
89752
|
+
return setFirewallPorts({ configText, ports: normalizePorts([...existing, port]) });
|
|
89753
|
+
}
|
|
89754
|
+
function removeFirewallPort(configText, port, existing = []) {
|
|
89755
|
+
return setFirewallPorts({ configText, ports: normalizePorts(existing.filter((p) => p !== port)) });
|
|
89756
|
+
}
|
|
89757
|
+
function findBlock(text, start, end, keyword, open) {
|
|
89758
|
+
const body = text.slice(start, end);
|
|
89759
|
+
const match = new RegExp(`\\b${keyword}\\s*:\\s*\\${open}`).exec(body);
|
|
89760
|
+
if (!match)
|
|
89761
|
+
return null;
|
|
89762
|
+
const propStart = start + match.index;
|
|
89763
|
+
const openIdx = text.indexOf(open, propStart);
|
|
89764
|
+
return { propStart, open: openIdx, close: findMatching(text, openIdx, open, open === "{" ? "}" : "]") };
|
|
89765
|
+
}
|
|
89766
|
+
function renderPortsArray(ports) {
|
|
89767
|
+
return `allowedPorts: [${ports.join(", ")}]`;
|
|
89768
|
+
}
|
|
89769
|
+
function setFirewallPorts(input) {
|
|
89770
|
+
const { configText } = input;
|
|
89771
|
+
const ports = normalizePorts(input.ports);
|
|
89772
|
+
const computeMatch = /\bcompute\s*:\s*{/.exec(configText);
|
|
89773
|
+
if (!computeMatch)
|
|
89774
|
+
throw new Error("Could not find infrastructure.compute in the cloud config.");
|
|
89775
|
+
const computeOpen = configText.indexOf("{", computeMatch.index);
|
|
89776
|
+
const computeClose = findMatching(configText, computeOpen, "{", "}");
|
|
89777
|
+
const firewall = findBlock(configText, computeOpen + 1, computeClose, "firewall", "{");
|
|
89778
|
+
if (firewall) {
|
|
89779
|
+
const portsArr = findBlock(configText, firewall.open + 1, firewall.close, "allowedPorts", "[");
|
|
89780
|
+
if (portsArr) {
|
|
89781
|
+
let lineStart = portsArr.propStart;
|
|
89782
|
+
while (lineStart > 0 && configText[lineStart - 1] !== `
|
|
89783
|
+
` && /\s/.test(configText[lineStart - 1]))
|
|
89784
|
+
lineStart--;
|
|
89785
|
+
const indent = configText.slice(lineStart, portsArr.propStart);
|
|
89786
|
+
return `${configText.slice(0, lineStart)}${indent}${renderPortsArray(ports)}${configText.slice(portsArr.close + 1)}`;
|
|
89787
|
+
}
|
|
89788
|
+
const before2 = configText.slice(0, firewall.close).trimEnd();
|
|
89789
|
+
const after2 = configText.slice(firewall.close);
|
|
89790
|
+
const sep2 = before2.endsWith(",") || before2.endsWith("{") ? "" : ",";
|
|
89791
|
+
return `${before2}${sep2}
|
|
89792
|
+
${renderPortsArray(ports)},
|
|
89793
|
+
${after2}`;
|
|
89794
|
+
}
|
|
89795
|
+
const before = configText.slice(0, computeClose).trimEnd();
|
|
89796
|
+
const after = configText.slice(computeClose);
|
|
89797
|
+
const sep = before.endsWith(",") || before.endsWith("{") ? "" : ",";
|
|
89798
|
+
return `${before}${sep}
|
|
89799
|
+
firewall: {
|
|
89800
|
+
enabled: true,
|
|
89801
|
+
${renderPortsArray(ports)},
|
|
89802
|
+
},
|
|
89803
|
+
${after}`;
|
|
89804
|
+
}
|
|
89805
|
+
|
|
89806
|
+
// src/deploy/serverless-operations.ts
|
|
89807
|
+
init_cloudformation();
|
|
89808
|
+
init_cloudfront();
|
|
89809
|
+
await init_dist2();
|
|
89810
|
+
|
|
89811
|
+
// src/aws/xray.ts
|
|
89812
|
+
init_client();
|
|
89813
|
+
|
|
89814
|
+
class XRayClient {
|
|
89815
|
+
client;
|
|
89816
|
+
region;
|
|
89817
|
+
constructor(region = "us-east-1") {
|
|
89818
|
+
this.region = region;
|
|
89819
|
+
this.client = new AWSClient;
|
|
89820
|
+
}
|
|
89821
|
+
async getTraceSummaries(options) {
|
|
89822
|
+
const body = {
|
|
89823
|
+
StartTime: Math.floor(options.startTime.getTime() / 1000),
|
|
89824
|
+
EndTime: Math.floor(options.endTime.getTime() / 1000),
|
|
89825
|
+
TimeRangeType: "Event",
|
|
89826
|
+
Sampling: false
|
|
89827
|
+
};
|
|
89828
|
+
if (options.filterExpression)
|
|
89829
|
+
body.FilterExpression = options.filterExpression;
|
|
89830
|
+
if (options.nextToken)
|
|
89831
|
+
body.NextToken = options.nextToken;
|
|
89832
|
+
const res = await this.client.request({
|
|
89833
|
+
service: "xray",
|
|
89834
|
+
region: this.region,
|
|
89835
|
+
method: "POST",
|
|
89836
|
+
path: "/TraceSummaries",
|
|
89837
|
+
headers: { "Content-Type": "application/json" },
|
|
89838
|
+
body: JSON.stringify(body)
|
|
89839
|
+
});
|
|
89840
|
+
return { summaries: res?.TraceSummaries ?? [], nextToken: res?.NextToken };
|
|
89841
|
+
}
|
|
89842
|
+
async batchGetTraces(traceIds) {
|
|
89843
|
+
if (!traceIds.length)
|
|
89844
|
+
return [];
|
|
89845
|
+
const res = await this.client.request({
|
|
89846
|
+
service: "xray",
|
|
89847
|
+
region: this.region,
|
|
89848
|
+
method: "POST",
|
|
89849
|
+
path: "/Traces",
|
|
89850
|
+
headers: { "Content-Type": "application/json" },
|
|
89851
|
+
body: JSON.stringify({ TraceIds: traceIds.slice(0, 5) })
|
|
89852
|
+
});
|
|
89853
|
+
return res?.Traces ?? [];
|
|
89854
|
+
}
|
|
89855
|
+
}
|
|
89856
|
+
|
|
89857
|
+
// src/deploy/serverless-operations.ts
|
|
89858
|
+
var MAX_OUTPUT_BYTES2 = 64 * 1024;
|
|
89859
|
+
function clampOutput2(output) {
|
|
89860
|
+
return output.length <= MAX_OUTPUT_BYTES2 ? output : `${output.slice(0, MAX_OUTPUT_BYTES2)}
|
|
89861
|
+
|
|
89862
|
+
[output truncated]`;
|
|
89863
|
+
}
|
|
89864
|
+
function serverlessApp(config6, environment) {
|
|
89865
|
+
return config6.environments?.[environment]?.app;
|
|
89866
|
+
}
|
|
89867
|
+
function buildServerlessOperations(config6, environment, data) {
|
|
89868
|
+
const ops = [];
|
|
89869
|
+
const app = serverlessApp(config6, environment);
|
|
89870
|
+
ops.push({ id: "redeploy", label: "Redeploy current build", group: "deploy", target: "app", mutates: true, confirm: "redeploy" });
|
|
89871
|
+
ops.push({ id: "rollback", label: "Roll back to previous build", group: "deploy", target: "app", mutates: true, confirm: "rollback", danger: true });
|
|
89872
|
+
const maint = data.maintenance?.enabled;
|
|
89873
|
+
if (maint !== true)
|
|
89874
|
+
ops.push({ id: "maintenance:on", label: "Enable maintenance mode", group: "maintenance", target: "app", mutates: true, confirm: "maintenance", danger: true });
|
|
89875
|
+
if (maint !== false)
|
|
89876
|
+
ops.push({ id: "maintenance:off", label: "Disable maintenance mode", group: "maintenance", target: "app", mutates: true, confirm: "live" });
|
|
89877
|
+
if (app?.assets || data.assetsInfo)
|
|
89878
|
+
ops.push({ id: "assets:invalidate", label: "Invalidate CDN cache", group: "assets", target: "assets", mutates: true, confirm: "invalidate" });
|
|
89879
|
+
if (app?.database?.connection === "aurora-serverless") {
|
|
89880
|
+
ops.push({
|
|
89881
|
+
id: "db:scale",
|
|
89882
|
+
label: "Scale Aurora capacity",
|
|
89883
|
+
group: "database",
|
|
89884
|
+
target: "database",
|
|
89885
|
+
mutates: true,
|
|
89886
|
+
confirm: "scale",
|
|
89887
|
+
inputs: [
|
|
89888
|
+
{ name: "min", label: "Min ACUs", placeholder: String(app.database.minCapacity ?? 0.5) },
|
|
89889
|
+
{ name: "max", label: "Max ACUs", placeholder: String(app.database.maxCapacity ?? 4) }
|
|
89890
|
+
]
|
|
89891
|
+
});
|
|
89892
|
+
}
|
|
89893
|
+
for (const q of data.queues ?? []) {
|
|
89894
|
+
const short = String(q?.name ?? "").trim();
|
|
89895
|
+
if (short && /^[A-Za-z0-9_-]+$/.test(short))
|
|
89896
|
+
ops.push({ id: `queue:purge:${short}`, label: `Purge queue (${short})`, group: "queue", target: short, mutates: true, confirm: short, danger: true });
|
|
89897
|
+
}
|
|
89898
|
+
return ops;
|
|
89899
|
+
}
|
|
89900
|
+
function resolveServerlessOperation(id, config6, environment, data) {
|
|
89901
|
+
return buildServerlessOperations(config6, environment, data).find((op) => op.id === id);
|
|
89902
|
+
}
|
|
89903
|
+
async function assetsDistributionId(config6, environment, region) {
|
|
89904
|
+
try {
|
|
89905
|
+
const cf = new CloudFormationClient(region);
|
|
89906
|
+
const { Stacks } = await cf.describeStacks({ stackName: resolveServerlessAppStackName(config6, environment) });
|
|
89907
|
+
const outputs = {};
|
|
89908
|
+
for (const o of Stacks?.[0]?.Outputs ?? [])
|
|
89909
|
+
if (o.OutputKey)
|
|
89910
|
+
outputs[o.OutputKey] = o.OutputValue ?? "";
|
|
89911
|
+
const domain = outputs.AssetsCdnDomain;
|
|
89912
|
+
if (!domain)
|
|
89913
|
+
return null;
|
|
89914
|
+
const dist = await new CloudFrontClient(region).findDistributionByDomain(domain);
|
|
89915
|
+
return dist?.Id ?? null;
|
|
89916
|
+
} catch {
|
|
89917
|
+
return null;
|
|
89918
|
+
}
|
|
89919
|
+
}
|
|
89920
|
+
async function runServerlessOperation(config6, environment, operation, options = {}) {
|
|
89921
|
+
try {
|
|
89922
|
+
if (operation.id === "redeploy") {
|
|
89923
|
+
await redeployServerlessApp(config6, environment);
|
|
89924
|
+
return { operation: operation.id, command: "serverless redeploy", ok: true, stdout: "Redeploy complete." };
|
|
89925
|
+
}
|
|
89926
|
+
if (operation.id === "rollback") {
|
|
89927
|
+
await rollbackServerlessApp(config6, environment);
|
|
89928
|
+
return { operation: operation.id, command: "serverless rollback", ok: true, stdout: "Rollback complete." };
|
|
89929
|
+
}
|
|
89930
|
+
if (operation.id === "maintenance:on") {
|
|
89931
|
+
await setMaintenance(config6, environment, true);
|
|
89932
|
+
return { operation: operation.id, command: "maintenance on", ok: true, stdout: "Application is now in maintenance mode." };
|
|
89933
|
+
}
|
|
89934
|
+
if (operation.id === "maintenance:off") {
|
|
89935
|
+
await setMaintenance(config6, environment, false);
|
|
89936
|
+
return { operation: operation.id, command: "maintenance off", ok: true, stdout: "Application is live." };
|
|
89937
|
+
}
|
|
89938
|
+
if (operation.id === "db:scale") {
|
|
89939
|
+
const min = Number(options.min);
|
|
89940
|
+
const max = Number(options.max);
|
|
89941
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min <= 0 || max < min)
|
|
89942
|
+
return { operation: operation.id, ok: false, error: "Provide valid Aurora capacities (0 < min ≤ max)." };
|
|
89943
|
+
await scaleServerlessDatabase(config6, environment, min, max);
|
|
89944
|
+
return { operation: operation.id, command: `db:scale ${min}-${max} ACU`, ok: true, stdout: `Scaling applied (${min}-${max} ACUs); takes effect shortly.` };
|
|
89945
|
+
}
|
|
89946
|
+
if (operation.id === "assets:invalidate") {
|
|
89947
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
89948
|
+
const distId = await assetsDistributionId(config6, environment, info2.region);
|
|
89949
|
+
if (!distId)
|
|
89950
|
+
return { operation: operation.id, ok: false, error: "Could not resolve the asset CloudFront distribution for this environment." };
|
|
89951
|
+
const cf = new CloudFrontClient(info2.region);
|
|
89952
|
+
const res = await cf.invalidateAll(distId);
|
|
89953
|
+
return { operation: operation.id, command: `cloudfront invalidate ${distId} /*`, ok: true, stdout: `Invalidation ${res?.Id ?? "created"} for ${distId} (/*).` };
|
|
89954
|
+
}
|
|
89955
|
+
if (operation.group === "queue" && operation.id.startsWith("queue:purge:")) {
|
|
89956
|
+
const short = operation.target;
|
|
89957
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
89958
|
+
const sqs2 = new SQSClient(info2.region);
|
|
89959
|
+
const queueName = `${info2.slug}-${environment}-${short}`;
|
|
89960
|
+
const { QueueUrl } = await sqs2.getQueueUrl(queueName);
|
|
89961
|
+
await sqs2.purgeQueue(QueueUrl);
|
|
89962
|
+
return { operation: operation.id, command: `sqs purge ${queueName}`, ok: true, stdout: `Purged queue ${queueName}.` };
|
|
89963
|
+
}
|
|
89964
|
+
return { operation: operation.id, ok: false, error: "Unknown or unavailable serverless operation." };
|
|
89965
|
+
} catch (error2) {
|
|
89966
|
+
return { operation: operation.id, ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
89967
|
+
}
|
|
89968
|
+
}
|
|
89969
|
+
async function runServerlessCommand(config6, environment, command) {
|
|
89970
|
+
const trimmed = command.trim();
|
|
89971
|
+
if (!trimmed)
|
|
89972
|
+
return { operation: "command", ok: false, error: "A command is required." };
|
|
89973
|
+
try {
|
|
89974
|
+
const output = await runRemoteCommand(config6, environment, trimmed);
|
|
89975
|
+
return { operation: "command", command: trimmed, ok: true, stdout: clampOutput2(output || "(no output)") };
|
|
89976
|
+
} catch (error2) {
|
|
89977
|
+
return { operation: "command", command: trimmed, ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
89978
|
+
}
|
|
89979
|
+
}
|
|
89980
|
+
var DLQ_UNAVAILABLE = "No dead-letter queue is provisioned for this environment.";
|
|
89981
|
+
async function dlqUrl(sqs2, slug, environment) {
|
|
89982
|
+
try {
|
|
89983
|
+
const { QueueUrl } = await sqs2.getQueueUrl(`${slug}-${environment}-dlq`);
|
|
89984
|
+
return QueueUrl;
|
|
89985
|
+
} catch {
|
|
89986
|
+
return null;
|
|
89987
|
+
}
|
|
89988
|
+
}
|
|
89989
|
+
async function listDlqMessages(config6, environment, max = 10) {
|
|
89990
|
+
try {
|
|
89991
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
89992
|
+
const sqs2 = new SQSClient(info2.region);
|
|
89993
|
+
const url = await dlqUrl(sqs2, info2.slug, environment);
|
|
89994
|
+
if (!url)
|
|
89995
|
+
return { ok: false, messages: [], error: DLQ_UNAVAILABLE };
|
|
89996
|
+
const received = await sqs2.receiveMessages({
|
|
89997
|
+
queueUrl: url,
|
|
89998
|
+
maxMessages: Math.min(10, Math.max(1, max)),
|
|
89999
|
+
waitTimeSeconds: 1,
|
|
90000
|
+
visibilityTimeout: 2
|
|
90001
|
+
});
|
|
90002
|
+
const messages = (received.Messages ?? []).map((m) => ({
|
|
90003
|
+
id: m.MessageId,
|
|
90004
|
+
receiptHandle: m.ReceiptHandle,
|
|
90005
|
+
body: clampOutput2(String(m.Body ?? ""))
|
|
90006
|
+
}));
|
|
90007
|
+
return { ok: true, messages };
|
|
90008
|
+
} catch (error2) {
|
|
90009
|
+
return { ok: false, messages: [], error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90010
|
+
}
|
|
90011
|
+
}
|
|
90012
|
+
async function redriveDlq(config6, environment, opts = {}) {
|
|
90013
|
+
try {
|
|
90014
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90015
|
+
const app = serverlessApp(config6, environment);
|
|
90016
|
+
const queues = app ? resolveQueues(app, info2.slug, environment) : [];
|
|
90017
|
+
const shortNames = queues.map((q) => q.name.replace(`${info2.slug}-${environment}-`, ""));
|
|
90018
|
+
const target = opts.targetQueue && shortNames.includes(opts.targetQueue) ? opts.targetQueue : shortNames[0];
|
|
90019
|
+
if (!target)
|
|
90020
|
+
return { operation: "dlq:redrive", ok: false, error: "No source queue is configured to redrive into." };
|
|
90021
|
+
const sqs2 = new SQSClient(info2.region);
|
|
90022
|
+
const dlq = await dlqUrl(sqs2, info2.slug, environment);
|
|
90023
|
+
if (!dlq)
|
|
90024
|
+
return { operation: "dlq:redrive", ok: false, error: DLQ_UNAVAILABLE };
|
|
90025
|
+
const { QueueUrl: targetUrl } = await sqs2.getQueueUrl(`${info2.slug}-${environment}-${target}`);
|
|
90026
|
+
const cap = Math.min(50, Math.max(1, opts.max ?? 10));
|
|
90027
|
+
let moved = 0;
|
|
90028
|
+
while (moved < cap) {
|
|
90029
|
+
const batch2 = await sqs2.receiveMessages({ queueUrl: dlq, maxMessages: Math.min(10, cap - moved), waitTimeSeconds: 1, visibilityTimeout: 30 });
|
|
90030
|
+
const msgs = batch2.Messages ?? [];
|
|
90031
|
+
if (!msgs.length)
|
|
90032
|
+
break;
|
|
90033
|
+
for (const m of msgs) {
|
|
90034
|
+
await sqs2.sendMessage({ queueUrl: targetUrl, messageBody: String(m.Body ?? "") });
|
|
90035
|
+
await sqs2.deleteMessage(dlq, m.ReceiptHandle);
|
|
90036
|
+
moved++;
|
|
90037
|
+
}
|
|
90038
|
+
}
|
|
90039
|
+
return { operation: "dlq:redrive", command: `redrive → ${target}`, ok: true, stdout: `Moved ${moved} message(s) from the DLQ to ${target}.` };
|
|
90040
|
+
} catch (error2) {
|
|
90041
|
+
return { operation: "dlq:redrive", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90042
|
+
}
|
|
90043
|
+
}
|
|
90044
|
+
async function purgeDlq(config6, environment) {
|
|
90045
|
+
try {
|
|
90046
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90047
|
+
const sqs2 = new SQSClient(info2.region);
|
|
90048
|
+
const url = await dlqUrl(sqs2, info2.slug, environment);
|
|
90049
|
+
if (!url)
|
|
90050
|
+
return { operation: "dlq:purge", ok: false, error: DLQ_UNAVAILABLE };
|
|
90051
|
+
await sqs2.purgeQueue(url);
|
|
90052
|
+
return { operation: "dlq:purge", command: "sqs purge dlq", ok: true, stdout: "Dead-letter queue purged." };
|
|
90053
|
+
} catch (error2) {
|
|
90054
|
+
return { operation: "dlq:purge", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90055
|
+
}
|
|
90056
|
+
}
|
|
90057
|
+
function configuredSecretIds(config6, environment) {
|
|
90058
|
+
const app = serverlessApp(config6, environment);
|
|
90059
|
+
if (!app?.secrets)
|
|
90060
|
+
return [];
|
|
90061
|
+
return Array.isArray(app.secrets) ? app.secrets.map((name) => ({ key: name.split("/").pop().toUpperCase().replace(/[^A-Z0-9_]/g, "_"), secretId: name })) : Object.entries(app.secrets).map(([key, secretId]) => ({ key, secretId: String(secretId) }));
|
|
90062
|
+
}
|
|
90063
|
+
async function setServerlessSecret(config6, environment, secretId, value) {
|
|
90064
|
+
if (!secretId.trim())
|
|
90065
|
+
return { operation: "secret:set", ok: false, error: "A secret id is required." };
|
|
90066
|
+
try {
|
|
90067
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90068
|
+
const sm = new SecretsManagerClient(info2.region);
|
|
90069
|
+
try {
|
|
90070
|
+
await sm.putSecretValue({ SecretId: secretId, SecretString: value });
|
|
90071
|
+
} catch {
|
|
90072
|
+
await sm.createSecret({ Name: secretId, SecretString: value });
|
|
90073
|
+
}
|
|
90074
|
+
return { operation: "secret:set", command: `secret set ${secretId}`, ok: true, stdout: `Secret ${secretId} updated. Redeploy to apply it to the functions.` };
|
|
90075
|
+
} catch (error2) {
|
|
90076
|
+
return { operation: "secret:set", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90077
|
+
}
|
|
90078
|
+
}
|
|
90079
|
+
async function deleteServerlessSecret(config6, environment, secretId) {
|
|
90080
|
+
if (!secretId.trim())
|
|
90081
|
+
return { operation: "secret:delete", ok: false, error: "A secret id is required." };
|
|
90082
|
+
try {
|
|
90083
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90084
|
+
const sm = new SecretsManagerClient(info2.region);
|
|
90085
|
+
await sm.deleteSecret({ SecretId: secretId, RecoveryWindowInDays: 7 });
|
|
90086
|
+
return { operation: "secret:delete", command: `secret delete ${secretId}`, ok: true, stdout: `Secret ${secretId} scheduled for deletion (recoverable for 7 days).` };
|
|
90087
|
+
} catch (error2) {
|
|
90088
|
+
return { operation: "secret:delete", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90089
|
+
}
|
|
90090
|
+
}
|
|
90091
|
+
var FUNCTION_MODES = new Set(["http", "queue", "cli"]);
|
|
90092
|
+
async function updateFunctionConfig(config6, environment, mode, opts) {
|
|
90093
|
+
if (!FUNCTION_MODES.has(mode))
|
|
90094
|
+
return { operation: "function:config", ok: false, error: `Unknown function mode '${mode}'.` };
|
|
90095
|
+
const memory = opts.memory == null ? undefined : Number(opts.memory);
|
|
90096
|
+
const timeout = opts.timeout == null ? undefined : Number(opts.timeout);
|
|
90097
|
+
if (memory != null && (!Number.isInteger(memory) || memory < 128 || memory > 10240))
|
|
90098
|
+
return { operation: "function:config", ok: false, error: "Memory must be an integer between 128 and 10240 MB." };
|
|
90099
|
+
if (timeout != null && (!Number.isInteger(timeout) || timeout < 1 || timeout > 900))
|
|
90100
|
+
return { operation: "function:config", ok: false, error: "Timeout must be an integer between 1 and 900 seconds." };
|
|
90101
|
+
if (memory == null && timeout == null)
|
|
90102
|
+
return { operation: "function:config", ok: false, error: "Provide a memory and/or timeout value." };
|
|
90103
|
+
try {
|
|
90104
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90105
|
+
const lambda2 = new LambdaClient(info2.region);
|
|
90106
|
+
const name = `${info2.slug}-${environment}-${mode}`;
|
|
90107
|
+
await lambda2.updateFunctionConfiguration({
|
|
90108
|
+
FunctionName: name,
|
|
90109
|
+
...memory != null ? { MemorySize: memory } : {},
|
|
90110
|
+
...timeout != null ? { Timeout: timeout } : {}
|
|
90111
|
+
});
|
|
90112
|
+
const parts = [memory != null ? `${memory} MB` : "", timeout != null ? `${timeout}s` : ""].filter(Boolean).join(", ");
|
|
90113
|
+
return { operation: "function:config", command: `update ${name} (${parts})`, ok: true, stdout: `Updated ${name}: ${parts}.` };
|
|
90114
|
+
} catch (error2) {
|
|
90115
|
+
return { operation: "function:config", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90116
|
+
}
|
|
90117
|
+
}
|
|
90118
|
+
var ALARM_PRESETS = [
|
|
90119
|
+
{ key: "http-errors", label: "HTTP function errors", namespace: "AWS/Lambda", metricName: "Errors", statistic: "Sum", comparison: "GreaterThanThreshold", fnMode: "http", unit: "errors / 5 min" },
|
|
90120
|
+
{ key: "http-throttles", label: "HTTP function throttles", namespace: "AWS/Lambda", metricName: "Throttles", statistic: "Sum", comparison: "GreaterThanThreshold", fnMode: "http", unit: "throttles / 5 min" },
|
|
90121
|
+
{ key: "http-duration", label: "HTTP function duration (avg)", namespace: "AWS/Lambda", metricName: "Duration", statistic: "Average", comparison: "GreaterThanThreshold", fnMode: "http", unit: "ms" },
|
|
90122
|
+
{ key: "http-concurrency", label: "HTTP concurrent executions", namespace: "AWS/Lambda", metricName: "ConcurrentExecutions", statistic: "Maximum", comparison: "GreaterThanThreshold", fnMode: "http", unit: "concurrent" },
|
|
90123
|
+
{ key: "queue-errors", label: "Queue function errors", namespace: "AWS/Lambda", metricName: "Errors", statistic: "Sum", comparison: "GreaterThanThreshold", fnMode: "queue", unit: "errors / 5 min" }
|
|
90124
|
+
];
|
|
90125
|
+
function resolveAlarmPreset(key) {
|
|
90126
|
+
return ALARM_PRESETS.find((p) => p.key === key);
|
|
90127
|
+
}
|
|
90128
|
+
async function listAlarms(config6, environment) {
|
|
90129
|
+
try {
|
|
90130
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90131
|
+
const cw = new CloudWatchClient(info2.region);
|
|
90132
|
+
const all = await cw.describeAlarms({ AlarmNamePrefix: `${info2.slug}-${environment}-`, MaxRecords: 100 });
|
|
90133
|
+
return { ok: true, alarms: all, presets: ALARM_PRESETS };
|
|
90134
|
+
} catch (error2) {
|
|
90135
|
+
return { ok: false, alarms: [], presets: ALARM_PRESETS, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90136
|
+
}
|
|
90137
|
+
}
|
|
90138
|
+
async function createAlarm(config6, environment, presetKey, threshold) {
|
|
90139
|
+
const preset = resolveAlarmPreset(presetKey);
|
|
90140
|
+
if (!preset)
|
|
90141
|
+
return { operation: "alarm:create", ok: false, error: `Unknown alarm metric '${presetKey}'.` };
|
|
90142
|
+
if (!Number.isFinite(threshold) || threshold < 0)
|
|
90143
|
+
return { operation: "alarm:create", ok: false, error: "Provide a non-negative threshold." };
|
|
90144
|
+
try {
|
|
90145
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90146
|
+
const cw = new CloudWatchClient(info2.region);
|
|
90147
|
+
const alarmName = `${info2.slug}-${environment}-${preset.key}`;
|
|
90148
|
+
const dims = preset.fnMode ? [{ Name: "FunctionName", Value: `${info2.slug}-${environment}-${preset.fnMode}` }] : [];
|
|
90149
|
+
await cw.putMetricAlarm({
|
|
90150
|
+
AlarmName: alarmName,
|
|
90151
|
+
Namespace: preset.namespace,
|
|
90152
|
+
MetricName: preset.metricName,
|
|
90153
|
+
ComparisonOperator: preset.comparison,
|
|
90154
|
+
Threshold: threshold,
|
|
90155
|
+
EvaluationPeriods: 1,
|
|
90156
|
+
Period: 300,
|
|
90157
|
+
Statistic: preset.statistic,
|
|
90158
|
+
Dimensions: dims,
|
|
90159
|
+
AlarmDescription: `ts-cloud: ${preset.label} > ${threshold} ${preset.unit}`
|
|
90160
|
+
});
|
|
90161
|
+
return { operation: "alarm:create", command: `alarm ${alarmName}`, ok: true, stdout: `Alarm ${alarmName} armed at ${threshold} ${preset.unit}.` };
|
|
90162
|
+
} catch (error2) {
|
|
90163
|
+
return { operation: "alarm:create", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90164
|
+
}
|
|
90165
|
+
}
|
|
90166
|
+
async function deleteAlarm(config6, environment, alarmName) {
|
|
90167
|
+
try {
|
|
90168
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90169
|
+
if (!alarmName.startsWith(`${info2.slug}-${environment}-`))
|
|
90170
|
+
return { operation: "alarm:delete", ok: false, error: "Refusing to delete an alarm outside this environment." };
|
|
90171
|
+
const cw = new CloudWatchClient(info2.region);
|
|
90172
|
+
await cw.deleteAlarms([alarmName]);
|
|
90173
|
+
return { operation: "alarm:delete", command: `delete alarm ${alarmName}`, ok: true, stdout: `Alarm ${alarmName} deleted.` };
|
|
90174
|
+
} catch (error2) {
|
|
90175
|
+
return { operation: "alarm:delete", ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90176
|
+
}
|
|
90177
|
+
}
|
|
90178
|
+
function shapeTrace(s) {
|
|
90179
|
+
const status = s.HasFault ? "fault" : s.HasError ? "error" : s.HasThrottle ? "throttle" : "ok";
|
|
90180
|
+
return {
|
|
90181
|
+
id: String(s.Id ?? ""),
|
|
90182
|
+
durationMs: Math.round((s.Duration ?? 0) * 1000),
|
|
90183
|
+
responseMs: Math.round((s.ResponseTime ?? 0) * 1000),
|
|
90184
|
+
status,
|
|
90185
|
+
method: s.Http?.HttpMethod ?? "-",
|
|
90186
|
+
url: s.Http?.HttpURL ?? "-",
|
|
90187
|
+
httpStatus: s.Http?.HttpStatus ?? 0
|
|
90188
|
+
};
|
|
90189
|
+
}
|
|
90190
|
+
async function listTraces(config6, environment, minutes = 30) {
|
|
90191
|
+
try {
|
|
90192
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90193
|
+
const xray = new XRayClient(info2.region);
|
|
90194
|
+
const end = new Date;
|
|
90195
|
+
const start = new Date(end.getTime() - Math.min(360, Math.max(1, minutes)) * 60000);
|
|
90196
|
+
const httpFn = `${info2.slug}-${environment}-http`;
|
|
90197
|
+
let res = await xray.getTraceSummaries({ startTime: start, endTime: end, filterExpression: `service("${httpFn}")` });
|
|
90198
|
+
if (!res.summaries.length)
|
|
90199
|
+
res = await xray.getTraceSummaries({ startTime: start, endTime: end });
|
|
90200
|
+
return { ok: true, traces: res.summaries.slice(0, 100).map(shapeTrace) };
|
|
90201
|
+
} catch (error2) {
|
|
90202
|
+
return { ok: false, traces: [], error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90203
|
+
}
|
|
90204
|
+
}
|
|
90205
|
+
async function controlScheduler(config6, environment, action) {
|
|
90206
|
+
try {
|
|
90207
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
90208
|
+
const ruleName = `${info2.slug}-${environment}-scheduler`;
|
|
90209
|
+
if (action === "run") {
|
|
90210
|
+
const output = await runRemoteCommand(config6, environment, "schedule:run");
|
|
90211
|
+
return { operation: "scheduler:run", command: "schedule:run", ok: true, stdout: clampOutput2(output || "Scheduler run triggered.") };
|
|
90212
|
+
}
|
|
90213
|
+
const eb = new EventBridgeClient(info2.region);
|
|
90214
|
+
if (action === "enable")
|
|
90215
|
+
await eb.enableRule({ Name: ruleName });
|
|
90216
|
+
else
|
|
90217
|
+
await eb.disableRule({ Name: ruleName });
|
|
90218
|
+
return { operation: `scheduler:${action}`, command: `${action} ${ruleName}`, ok: true, stdout: `Scheduler rule ${ruleName} ${action}d.` };
|
|
90219
|
+
} catch (error2) {
|
|
90220
|
+
return { operation: `scheduler:${action}`, ok: false, error: clampOutput2(error2?.message ?? String(error2)) };
|
|
90221
|
+
}
|
|
90222
|
+
}
|
|
89526
90223
|
|
|
89527
90224
|
// src/deploy/site-config-editor.ts
|
|
89528
90225
|
function addSiteToCloudConfig(input) {
|
|
@@ -89612,6 +90309,28 @@ ${entries.map(([key, value]) => ` ${key}: '${escapeSingle(String(value))}
|
|
|
89612
90309
|
`)}
|
|
89613
90310
|
}`;
|
|
89614
90311
|
}
|
|
90312
|
+
function isValidHostname(value) {
|
|
90313
|
+
return /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(value.trim());
|
|
90314
|
+
}
|
|
90315
|
+
function renderAliasesValue(aliases) {
|
|
90316
|
+
const list = [...new Set(aliases.map((a) => a.trim().toLowerCase()).filter(Boolean))];
|
|
90317
|
+
for (const host of list) {
|
|
90318
|
+
if (!isValidHostname(host))
|
|
90319
|
+
throw new Error(`Alias '${host}' is not a valid hostname.`);
|
|
90320
|
+
}
|
|
90321
|
+
if (list.length === 0)
|
|
90322
|
+
return "[]";
|
|
90323
|
+
return `[${list.map((a) => `'${escapeSingle(a)}'`).join(", ")}]`;
|
|
90324
|
+
}
|
|
90325
|
+
function renderRedirectsValue(redirects) {
|
|
90326
|
+
const entries = Object.entries(redirects).map(([from, to]) => [from.trim(), String(to).trim()]).filter(([from, to]) => from && to);
|
|
90327
|
+
if (entries.length === 0)
|
|
90328
|
+
return "{}";
|
|
90329
|
+
return `{
|
|
90330
|
+
${entries.map(([from, to]) => ` '${escapeSingle(from)}': '${escapeSingle(to)}',`).join(`
|
|
90331
|
+
`)}
|
|
90332
|
+
}`;
|
|
90333
|
+
}
|
|
89615
90334
|
function scanValueEnd(text, start, limit) {
|
|
89616
90335
|
let i = start;
|
|
89617
90336
|
while (i < limit && /\s/.test(text[i]))
|
|
@@ -89788,10 +90507,57 @@ function findMatchingBrace(text, start) {
|
|
|
89788
90507
|
throw new Error("Could not find the closing brace for sites: { ... } in cloud.config.ts");
|
|
89789
90508
|
}
|
|
89790
90509
|
|
|
90510
|
+
// src/deploy/terminal-session.ts
|
|
90511
|
+
function createTerminalSession(onData, options = {}) {
|
|
90512
|
+
const shell = options.shell || (process.platform === "win32" ? "powershell.exe" : "bash");
|
|
90513
|
+
const proc = Bun.spawn([shell, "-i"], {
|
|
90514
|
+
stdin: "pipe",
|
|
90515
|
+
stdout: "pipe",
|
|
90516
|
+
stderr: "pipe",
|
|
90517
|
+
cwd: options.cwd,
|
|
90518
|
+
env: { ...process.env, TERM: "xterm-256color", PS1: "\\w $ " }
|
|
90519
|
+
});
|
|
90520
|
+
const decoder = new TextDecoder;
|
|
90521
|
+
const pump = async (stream) => {
|
|
90522
|
+
if (!stream)
|
|
90523
|
+
return;
|
|
90524
|
+
const reader = stream.getReader();
|
|
90525
|
+
try {
|
|
90526
|
+
for (;; ) {
|
|
90527
|
+
const { done, value } = await reader.read();
|
|
90528
|
+
if (done)
|
|
90529
|
+
break;
|
|
90530
|
+
if (value)
|
|
90531
|
+
onData(decoder.decode(value));
|
|
90532
|
+
}
|
|
90533
|
+
} catch {}
|
|
90534
|
+
};
|
|
90535
|
+
pump(proc.stdout);
|
|
90536
|
+
pump(proc.stderr);
|
|
90537
|
+
if (options.onExit)
|
|
90538
|
+
proc.exited.then((code) => options.onExit(code)).catch(() => {});
|
|
90539
|
+
return {
|
|
90540
|
+
write(data) {
|
|
90541
|
+
try {
|
|
90542
|
+
proc.stdin.write(data);
|
|
90543
|
+
proc.stdin.flush();
|
|
90544
|
+
} catch {}
|
|
90545
|
+
},
|
|
90546
|
+
close() {
|
|
90547
|
+
try {
|
|
90548
|
+
proc.stdin.end();
|
|
90549
|
+
} catch {}
|
|
90550
|
+
try {
|
|
90551
|
+
proc.kill();
|
|
90552
|
+
} catch {}
|
|
90553
|
+
}
|
|
90554
|
+
};
|
|
90555
|
+
}
|
|
90556
|
+
|
|
89791
90557
|
// src/deploy/local-dashboard-server.ts
|
|
89792
90558
|
var DEFAULT_HOST = "127.0.0.1";
|
|
89793
90559
|
var DEFAULT_PORT = 7676;
|
|
89794
|
-
var
|
|
90560
|
+
var MAX_OUTPUT_BYTES3 = 64 * 1024;
|
|
89795
90561
|
var here = dirname9(fileURLToPath6(import.meta.url));
|
|
89796
90562
|
var contentTypes = {
|
|
89797
90563
|
".html": "text/html; charset=utf-8",
|
|
@@ -89871,16 +90637,20 @@ function replaceSiteConfig(config6, name, site) {
|
|
|
89871
90637
|
const sites = config6.sites ?? (config6.sites = {});
|
|
89872
90638
|
sites[name] = site;
|
|
89873
90639
|
}
|
|
90640
|
+
function computeFirewallPorts(config6) {
|
|
90641
|
+
return normalizePorts(config6.infrastructure?.compute?.firewall?.allowedPorts ?? []);
|
|
90642
|
+
}
|
|
90643
|
+
function replaceFirewallPorts(config6, ports) {
|
|
90644
|
+
const infra = config6.infrastructure ?? (config6.infrastructure = {});
|
|
90645
|
+
const compute = infra.compute ?? (infra.compute = {});
|
|
90646
|
+
const firewall = compute.firewall ?? (compute.firewall = {});
|
|
90647
|
+
firewall.allowedPorts = ports;
|
|
90648
|
+
}
|
|
89874
90649
|
async function readJsonBody(req) {
|
|
89875
90650
|
return await req.json().catch(() => ({}));
|
|
89876
90651
|
}
|
|
89877
|
-
function resolveDashboardMode(config6) {
|
|
89878
|
-
if (config6.mode === "hybrid")
|
|
89879
|
-
return "hybrid";
|
|
89880
|
-
return config6.infrastructure?.compute ? "server" : "serverless";
|
|
89881
|
-
}
|
|
89882
90652
|
async function resolveLiveDashboardData(config6, environment) {
|
|
89883
|
-
const mode =
|
|
90653
|
+
const mode = resolveDeploymentMode(config6);
|
|
89884
90654
|
const meta = { mode, environment, environments: Object.keys(config6.environments ?? {}) };
|
|
89885
90655
|
try {
|
|
89886
90656
|
const data = mode === "serverless" ? await resolveDashboardData(config6, environment) : await resolveServerDashboardData(config6, environment);
|
|
@@ -90016,10 +90786,10 @@ function dashboardActions(environment) {
|
|
|
90016
90786
|
function resolveDashboardAction(id, environment) {
|
|
90017
90787
|
return dashboardActions(environment).find((action) => action.id === id);
|
|
90018
90788
|
}
|
|
90019
|
-
function
|
|
90020
|
-
if (output.length <=
|
|
90789
|
+
function clampOutput3(output) {
|
|
90790
|
+
if (output.length <= MAX_OUTPUT_BYTES3)
|
|
90021
90791
|
return output;
|
|
90022
|
-
return `${output.slice(0,
|
|
90792
|
+
return `${output.slice(0, MAX_OUTPUT_BYTES3)}
|
|
90023
90793
|
|
|
90024
90794
|
[output truncated]`;
|
|
90025
90795
|
}
|
|
@@ -90040,8 +90810,8 @@ async function runAction(action, options) {
|
|
|
90040
90810
|
command: ["cloud", ...action.command].join(" "),
|
|
90041
90811
|
exitCode,
|
|
90042
90812
|
ok: exitCode === 0,
|
|
90043
|
-
stdout:
|
|
90044
|
-
stderr:
|
|
90813
|
+
stdout: clampOutput3(stdout),
|
|
90814
|
+
stderr: clampOutput3(stderr)
|
|
90045
90815
|
};
|
|
90046
90816
|
}
|
|
90047
90817
|
function staticPath(uiRoot, pathname) {
|
|
@@ -90089,12 +90859,49 @@ async function startLocalDashboardServer(options = {}) {
|
|
|
90089
90859
|
let activeUiRoot = liveUiRoot ?? packagedUi?.uiRoot;
|
|
90090
90860
|
if (!activeUiRoot)
|
|
90091
90861
|
throw new Error("ts-cloud dashboard UI not found. Run `bun run build` in ts-cloud or reinstall the package.");
|
|
90862
|
+
const terminalSessions = new WeakMap;
|
|
90863
|
+
const terminalEnabled = process.env.TS_CLOUD_DASHBOARD_TERMINAL !== "0";
|
|
90092
90864
|
const server = Bun.serve({
|
|
90093
90865
|
hostname: host,
|
|
90094
90866
|
port,
|
|
90095
|
-
|
|
90867
|
+
websocket: {
|
|
90868
|
+
open(ws) {
|
|
90869
|
+
if (!terminalEnabled) {
|
|
90870
|
+
ws.send(`The web terminal is disabled (TS_CLOUD_DASHBOARD_TERMINAL=0).\r
|
|
90871
|
+
`);
|
|
90872
|
+
ws.close();
|
|
90873
|
+
return;
|
|
90874
|
+
}
|
|
90875
|
+
const session = createTerminalSession((chunk2) => ws.send(chunk2), {
|
|
90876
|
+
cwd,
|
|
90877
|
+
onExit: () => {
|
|
90878
|
+
try {
|
|
90879
|
+
ws.close();
|
|
90880
|
+
} catch {}
|
|
90881
|
+
}
|
|
90882
|
+
});
|
|
90883
|
+
terminalSessions.set(ws, session);
|
|
90884
|
+
ws.send(`Connected to ${host === "127.0.0.1" ? "localhost" : host} shell. This is a line-oriented shell (no full-screen apps).\r
|
|
90885
|
+
`);
|
|
90886
|
+
},
|
|
90887
|
+
message(ws, message) {
|
|
90888
|
+
terminalSessions.get(ws)?.write(typeof message === "string" ? message : new TextDecoder().decode(message));
|
|
90889
|
+
},
|
|
90890
|
+
close(ws) {
|
|
90891
|
+
terminalSessions.get(ws)?.close();
|
|
90892
|
+
terminalSessions.delete(ws);
|
|
90893
|
+
}
|
|
90894
|
+
},
|
|
90895
|
+
async fetch(req, server2) {
|
|
90096
90896
|
const url = new URL(req.url);
|
|
90097
90897
|
try {
|
|
90898
|
+
if (url.pathname === "/api/terminal") {
|
|
90899
|
+
if (!terminalEnabled)
|
|
90900
|
+
return json({ ok: false, error: "The web terminal is disabled." }, 403);
|
|
90901
|
+
if (server2.upgrade(req))
|
|
90902
|
+
return;
|
|
90903
|
+
return text("WebSocket upgrade failed", 400);
|
|
90904
|
+
}
|
|
90098
90905
|
if (url.pathname === "/api/health") {
|
|
90099
90906
|
return json({
|
|
90100
90907
|
ok: true,
|
|
@@ -90103,6 +90910,7 @@ async function startLocalDashboardServer(options = {}) {
|
|
|
90103
90910
|
environments: availableEnvironments,
|
|
90104
90911
|
uiRoot: activeUiRoot,
|
|
90105
90912
|
liveData: !!liveUiRoot,
|
|
90913
|
+
terminal: terminalEnabled,
|
|
90106
90914
|
localPackage: import.meta.url.includes("/Code/Libraries/ts-cloud/")
|
|
90107
90915
|
});
|
|
90108
90916
|
}
|
|
@@ -90163,6 +90971,42 @@ async function startLocalDashboardServer(options = {}) {
|
|
|
90163
90971
|
replaceComputeSshKeys(config6, currentKeys.filter((key) => key.name !== name.trim()));
|
|
90164
90972
|
return json({ ok: true, configPath, keys: describeSshKeys(computeSshKeys(config6)) });
|
|
90165
90973
|
}
|
|
90974
|
+
if (url.pathname === "/api/firewall" && req.method === "GET")
|
|
90975
|
+
return json({ configPath, alwaysOpen: [22, 80, 443], ports: computeFirewallPorts(config6) });
|
|
90976
|
+
if (url.pathname === "/api/firewall" && req.method === "POST") {
|
|
90977
|
+
if (!configPath)
|
|
90978
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90979
|
+
const body = await readJsonBody(req);
|
|
90980
|
+
const port2 = Number(body.port);
|
|
90981
|
+
if (!isValidPort(port2))
|
|
90982
|
+
return json({ ok: false, error: "Port must be an integer between 1 and 65535." }, 422);
|
|
90983
|
+
const current = computeFirewallPorts(config6);
|
|
90984
|
+
const before = await readFile2(configPath, "utf8");
|
|
90985
|
+
try {
|
|
90986
|
+
await writeFile6(configPath, addFirewallPort(before, port2, current));
|
|
90987
|
+
} catch (error2) {
|
|
90988
|
+
return json({ ok: false, error: error2?.message ?? String(error2) }, 422);
|
|
90989
|
+
}
|
|
90990
|
+
const ports = normalizePorts([...current, port2]);
|
|
90991
|
+
replaceFirewallPorts(config6, ports);
|
|
90992
|
+
const apply = body.apply === false ? null : await runServerShellCommand(config6, environment, `ufw allow ${port2}/tcp`).catch((e) => ({ ok: false, error: String(e?.message ?? e) }));
|
|
90993
|
+
return json({ ok: true, configPath, ports, apply });
|
|
90994
|
+
}
|
|
90995
|
+
if (url.pathname === "/api/firewall" && req.method === "DELETE") {
|
|
90996
|
+
if (!configPath)
|
|
90997
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90998
|
+
const body = await readJsonBody(req);
|
|
90999
|
+
const port2 = Number(body.port ?? url.searchParams.get("port"));
|
|
91000
|
+
if (!isValidPort(port2))
|
|
91001
|
+
return json({ ok: false, error: "Port must be an integer between 1 and 65535." }, 422);
|
|
91002
|
+
const current = computeFirewallPorts(config6);
|
|
91003
|
+
const before = await readFile2(configPath, "utf8");
|
|
91004
|
+
await writeFile6(configPath, removeFirewallPort(before, port2, current));
|
|
91005
|
+
const ports = normalizePorts(current.filter((p) => p !== port2));
|
|
91006
|
+
replaceFirewallPorts(config6, ports);
|
|
91007
|
+
const apply = body.apply === false ? null : await runServerShellCommand(config6, environment, `ufw delete allow ${port2}/tcp`).catch((e) => ({ ok: false, error: String(e?.message ?? e) }));
|
|
91008
|
+
return json({ ok: true, configPath, ports, apply });
|
|
91009
|
+
}
|
|
90166
91010
|
if (url.pathname === "/api/sites" && req.method === "POST") {
|
|
90167
91011
|
if (!configPath)
|
|
90168
91012
|
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
@@ -90236,7 +91080,16 @@ async function startLocalDashboardServer(options = {}) {
|
|
|
90236
91080
|
set("env", renderEnvValue(body.env));
|
|
90237
91081
|
existing.env = body.env;
|
|
90238
91082
|
}
|
|
90239
|
-
|
|
91083
|
+
if (body.aliases !== undefined && Array.isArray(body.aliases)) {
|
|
91084
|
+
const aliases = body.aliases.map((a) => String(a));
|
|
91085
|
+
set("aliases", renderAliasesValue(aliases));
|
|
91086
|
+
existing.aliases = aliases.map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
91087
|
+
}
|
|
91088
|
+
if (body.redirects !== undefined && body.redirects && typeof body.redirects === "object") {
|
|
91089
|
+
set("redirects", renderRedirectsValue(body.redirects));
|
|
91090
|
+
existing.redirects = body.redirects;
|
|
91091
|
+
}
|
|
91092
|
+
for (const key of ["domain", "path", "build", "start", "type", "root", "php"]) {
|
|
90240
91093
|
if (typeof body[key] === "string" && body[key].trim()) {
|
|
90241
91094
|
set(key, renderStringValue(String(body[key]).trim()));
|
|
90242
91095
|
existing[key] = String(body[key]).trim();
|
|
@@ -90279,6 +91132,17 @@ async function startLocalDashboardServer(options = {}) {
|
|
|
90279
91132
|
return json({ ok: false, error: "Database name must be a valid identifier (letters, numbers, underscore; not starting with a digit)." }, 422);
|
|
90280
91133
|
return json({ ...await createDatabase(config6, environment, name), name });
|
|
90281
91134
|
}
|
|
91135
|
+
if (url.pathname === "/api/databases/backups" && req.method === "GET")
|
|
91136
|
+
return json(await listDatabaseBackups(config6, environment));
|
|
91137
|
+
if (url.pathname === "/api/databases/backup" && req.method === "POST") {
|
|
91138
|
+
const body = await readJsonBody(req);
|
|
91139
|
+
const name = String(body.database ?? "").trim();
|
|
91140
|
+
if (!isValidDbIdentifier(name))
|
|
91141
|
+
return json({ ok: false, error: "Database name must be a valid identifier." }, 422);
|
|
91142
|
+
if (body.confirm !== name)
|
|
91143
|
+
return json({ ok: false, error: `Type "${name}" to back up this database.` }, 409);
|
|
91144
|
+
return json(await backupDatabase(config6, environment, name));
|
|
91145
|
+
}
|
|
90282
91146
|
if (url.pathname === "/api/databases/users" && req.method === "POST") {
|
|
90283
91147
|
const body = await readJsonBody(req);
|
|
90284
91148
|
const username = String(body.username ?? "").trim();
|
|
@@ -90315,6 +91179,108 @@ async function startLocalDashboardServer(options = {}) {
|
|
|
90315
91179
|
return json({ ok: false, error: `Type "${operation.confirm}" to run this operation.` }, 409);
|
|
90316
91180
|
return json(await runDashboardOperation(config6, environment, operation, { to: body.to }));
|
|
90317
91181
|
}
|
|
91182
|
+
if (url.pathname === "/api/server/command" && req.method === "POST") {
|
|
91183
|
+
const body = await req.json().catch(() => ({}));
|
|
91184
|
+
if (body.confirm !== "run")
|
|
91185
|
+
return json({ ok: false, error: 'Type "run" to execute this command on the server.' }, 409);
|
|
91186
|
+
return json(await runServerShellCommand(config6, environment, String(body.command ?? "")));
|
|
91187
|
+
}
|
|
91188
|
+
if (url.pathname === "/api/serverless/operations")
|
|
91189
|
+
return json(buildServerlessOperations(config6, environment, latestData));
|
|
91190
|
+
if (url.pathname === "/api/serverless/operations/run" && req.method === "POST") {
|
|
91191
|
+
const body = await req.json().catch(() => ({}));
|
|
91192
|
+
const operation = body.operation ? resolveServerlessOperation(body.operation, config6, environment, latestData) : undefined;
|
|
91193
|
+
if (!operation)
|
|
91194
|
+
return json({ ok: false, error: "Unknown or unavailable serverless operation." }, 404);
|
|
91195
|
+
if (operation.mutates && body.confirm !== operation.confirm)
|
|
91196
|
+
return json({ ok: false, error: `Type "${operation.confirm}" to run this operation.` }, 409);
|
|
91197
|
+
const result = await runServerlessOperation(config6, environment, operation, { min: body.min, max: body.max });
|
|
91198
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
91199
|
+
return json(result);
|
|
91200
|
+
}
|
|
91201
|
+
if (url.pathname === "/api/serverless/command" && req.method === "POST") {
|
|
91202
|
+
const body = await req.json().catch(() => ({}));
|
|
91203
|
+
if (body.confirm !== "run")
|
|
91204
|
+
return json({ ok: false, error: 'Type "run" to execute this command.' }, 409);
|
|
91205
|
+
return json(await runServerlessCommand(config6, environment, String(body.command ?? "")));
|
|
91206
|
+
}
|
|
91207
|
+
if (url.pathname === "/api/serverless/dlq" && req.method === "GET") {
|
|
91208
|
+
const max = Number(url.searchParams.get("max")) || 10;
|
|
91209
|
+
return json(await listDlqMessages(config6, environment, max));
|
|
91210
|
+
}
|
|
91211
|
+
if (url.pathname === "/api/serverless/dlq/redrive" && req.method === "POST") {
|
|
91212
|
+
const body = await req.json().catch(() => ({}));
|
|
91213
|
+
if (body.confirm !== "redrive")
|
|
91214
|
+
return json({ ok: false, error: 'Type "redrive" to move messages back onto a source queue.' }, 409);
|
|
91215
|
+
return json(await redriveDlq(config6, environment, { max: body.max, targetQueue: body.targetQueue }));
|
|
91216
|
+
}
|
|
91217
|
+
if (url.pathname === "/api/serverless/dlq/purge" && req.method === "POST") {
|
|
91218
|
+
const body = await req.json().catch(() => ({}));
|
|
91219
|
+
if (body.confirm !== "purge")
|
|
91220
|
+
return json({ ok: false, error: 'Type "purge" to permanently discard every DLQ message.' }, 409);
|
|
91221
|
+
return json(await purgeDlq(config6, environment));
|
|
91222
|
+
}
|
|
91223
|
+
if (url.pathname === "/api/serverless/secrets" && req.method === "GET")
|
|
91224
|
+
return json({ secrets: configuredSecretIds(config6, environment) });
|
|
91225
|
+
if (url.pathname === "/api/serverless/secrets" && req.method === "POST") {
|
|
91226
|
+
const body = await req.json().catch(() => ({}));
|
|
91227
|
+
const secretId = String(body.secretId ?? "").trim();
|
|
91228
|
+
if (!secretId)
|
|
91229
|
+
return json({ ok: false, error: "A secret id is required." }, 422);
|
|
91230
|
+
if (typeof body.value !== "string" || body.value === "")
|
|
91231
|
+
return json({ ok: false, error: "A non-empty value is required." }, 422);
|
|
91232
|
+
return json(await setServerlessSecret(config6, environment, secretId, body.value));
|
|
91233
|
+
}
|
|
91234
|
+
if (url.pathname === "/api/serverless/secrets" && req.method === "DELETE") {
|
|
91235
|
+
const body = await req.json().catch(() => ({}));
|
|
91236
|
+
const secretId = String(body.secretId ?? "").trim();
|
|
91237
|
+
if (!secretId)
|
|
91238
|
+
return json({ ok: false, error: "A secret id is required." }, 422);
|
|
91239
|
+
if (body.confirm !== secretId)
|
|
91240
|
+
return json({ ok: false, error: `Type "${secretId}" to delete this secret.` }, 409);
|
|
91241
|
+
return json(await deleteServerlessSecret(config6, environment, secretId));
|
|
91242
|
+
}
|
|
91243
|
+
if (url.pathname === "/api/serverless/functions/config" && req.method === "POST") {
|
|
91244
|
+
const body = await req.json().catch(() => ({}));
|
|
91245
|
+
const mode = String(body.mode ?? "").trim();
|
|
91246
|
+
if (body.confirm !== mode)
|
|
91247
|
+
return json({ ok: false, error: `Type "${mode}" to update this function.` }, 409);
|
|
91248
|
+
return json(await updateFunctionConfig(config6, environment, mode, { memory: body.memory, timeout: body.timeout }));
|
|
91249
|
+
}
|
|
91250
|
+
if (url.pathname === "/api/serverless/alarms" && req.method === "GET")
|
|
91251
|
+
return json(await listAlarms(config6, environment));
|
|
91252
|
+
if (url.pathname === "/api/serverless/alarms" && req.method === "POST") {
|
|
91253
|
+
const body = await req.json().catch(() => ({}));
|
|
91254
|
+
const preset = String(body.preset ?? "").trim();
|
|
91255
|
+
const threshold = Number(body.threshold);
|
|
91256
|
+
if (!preset)
|
|
91257
|
+
return json({ ok: false, error: "An alarm metric is required." }, 422);
|
|
91258
|
+
if (!Number.isFinite(threshold))
|
|
91259
|
+
return json({ ok: false, error: "A numeric threshold is required." }, 422);
|
|
91260
|
+
return json(await createAlarm(config6, environment, preset, threshold));
|
|
91261
|
+
}
|
|
91262
|
+
if (url.pathname === "/api/serverless/alarms" && req.method === "DELETE") {
|
|
91263
|
+
const body = await req.json().catch(() => ({}));
|
|
91264
|
+
const name = String(body.name ?? "").trim();
|
|
91265
|
+
if (!name)
|
|
91266
|
+
return json({ ok: false, error: "An alarm name is required." }, 422);
|
|
91267
|
+
if (body.confirm !== name)
|
|
91268
|
+
return json({ ok: false, error: `Type "${name}" to delete this alarm.` }, 409);
|
|
91269
|
+
return json(await deleteAlarm(config6, environment, name));
|
|
91270
|
+
}
|
|
91271
|
+
if (url.pathname === "/api/serverless/traces" && req.method === "GET") {
|
|
91272
|
+
const minutes = Number(url.searchParams.get("minutes")) || 30;
|
|
91273
|
+
return json(await listTraces(config6, environment, minutes));
|
|
91274
|
+
}
|
|
91275
|
+
if (url.pathname === "/api/serverless/scheduler" && req.method === "POST") {
|
|
91276
|
+
const body = await req.json().catch(() => ({}));
|
|
91277
|
+
const action = String(body.action ?? "").trim();
|
|
91278
|
+
if (action !== "enable" && action !== "disable" && action !== "run")
|
|
91279
|
+
return json({ ok: false, error: "Unknown scheduler action." }, 422);
|
|
91280
|
+
if (body.confirm !== action)
|
|
91281
|
+
return json({ ok: false, error: `Type "${action}" to ${action} the scheduler.` }, 409);
|
|
91282
|
+
return json(await controlScheduler(config6, environment, action));
|
|
91283
|
+
}
|
|
90318
91284
|
const requestedEnv = url.searchParams.get("env");
|
|
90319
91285
|
if (requestedEnv && availableEnvironments.includes(requestedEnv) && requestedEnv !== environment) {
|
|
90320
91286
|
environment = requestedEnv;
|
|
@@ -90393,6 +91359,7 @@ export {
|
|
|
90393
91359
|
rollbackServerlessApp,
|
|
90394
91360
|
responseToResult,
|
|
90395
91361
|
resourceManagementManager,
|
|
91362
|
+
resolveUiSource,
|
|
90396
91363
|
resolveStorageBucketName,
|
|
90397
91364
|
resolveSiteStackName,
|
|
90398
91365
|
resolveSiteResourceName,
|
|
@@ -90411,8 +91378,10 @@ export {
|
|
|
90411
91378
|
resolveManagementDashboardSite,
|
|
90412
91379
|
resolveHetznerApiToken,
|
|
90413
91380
|
resolveExecStart,
|
|
91381
|
+
resolveDeploymentMode,
|
|
90414
91382
|
resolveDeployBucketName,
|
|
90415
91383
|
resolveDashboardDomain,
|
|
91384
|
+
resolveDashboardAuth,
|
|
90416
91385
|
resolveDashboardAction,
|
|
90417
91386
|
resolveCredentials,
|
|
90418
91387
|
resolveCloudProvider,
|
|
@@ -90558,6 +91527,7 @@ export {
|
|
|
90558
91527
|
fifoQueueManager,
|
|
90559
91528
|
extendPreset,
|
|
90560
91529
|
eventToRequest,
|
|
91530
|
+
ensureManagementDashboard,
|
|
90561
91531
|
emailTemplateManager,
|
|
90562
91532
|
emailAnalyticsManager,
|
|
90563
91533
|
drManager,
|
|
@@ -90568,6 +91538,8 @@ export {
|
|
|
90568
91538
|
detectServiceRegion,
|
|
90569
91539
|
detectMisconfigurations,
|
|
90570
91540
|
detectDnsProvider,
|
|
91541
|
+
detectDeploymentTargets,
|
|
91542
|
+
deploymentCoexistenceError,
|
|
90571
91543
|
deployStaticSiteWithExternalDnsFull,
|
|
90572
91544
|
deployStaticSiteWithExternalDns,
|
|
90573
91545
|
deployStaticSiteFull,
|
|
@@ -90637,6 +91609,7 @@ export {
|
|
|
90637
91609
|
buildPhpRuntimeLayerZip,
|
|
90638
91610
|
buildOptimizationManager,
|
|
90639
91611
|
buildNodeRuntimeLayerZip,
|
|
91612
|
+
buildManagementDashboardArtifact,
|
|
90640
91613
|
buildFunctionEnv,
|
|
90641
91614
|
buildCloudFormationTemplate,
|
|
90642
91615
|
buildBunRuntimeLayerZip,
|
|
@@ -90744,6 +91717,7 @@ export {
|
|
|
90744
91717
|
MigrationManager,
|
|
90745
91718
|
MetricsManager,
|
|
90746
91719
|
Messaging,
|
|
91720
|
+
MANAGEMENT_DASHBOARD_SITE,
|
|
90747
91721
|
MANAGED_NODE_VERSIONS,
|
|
90748
91722
|
LogsManager,
|
|
90749
91723
|
LambdaVersionsManager,
|
|
@@ -90797,6 +91771,7 @@ export {
|
|
|
90797
91771
|
DNS,
|
|
90798
91772
|
DLQMonitoringManager,
|
|
90799
91773
|
DEFAULT_SERVICE_LIMITS,
|
|
91774
|
+
DASHBOARD_CREDENTIALS_FILE,
|
|
90800
91775
|
CrossRegionReferenceManager,
|
|
90801
91776
|
CredentialError,
|
|
90802
91777
|
ContainerRegistryManager,
|