@stacksjs/ts-cloud 0.5.26 → 0.5.28
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/bin/cli.js +1223 -7366
- package/dist/deploy/dashboard-data-server.d.ts +27 -5
- package/dist/deploy/dashboard-database.d.ts +38 -0
- package/dist/deploy/dashboard-database.test.d.ts +1 -0
- package/dist/deploy/dashboard-operations.d.ts +48 -0
- package/dist/deploy/dashboard-operations.test.d.ts +1 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/local-dashboard-server.d.ts +31 -0
- package/dist/deploy/management-dashboard.d.ts +21 -1
- package/dist/deploy/site-config-editor.d.ts +37 -0
- package/dist/deploy/site-target.d.ts +3 -1
- package/dist/deploy/ssh-config-editor.d.ts +22 -0
- package/dist/deploy/ssh-config-editor.test.d.ts +1 -0
- package/dist/drivers/factory.d.ts +4 -0
- package/dist/drivers/index.d.ts +1 -0
- package/dist/drivers/local-box/driver.d.ts +20 -0
- package/dist/drivers/shared/compute-deploy.d.ts +2 -0
- package/dist/drivers/shared/deploy-script.d.ts +28 -12
- package/dist/drivers/shared/rpx-gateway.d.ts +99 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3381 -210
- package/dist/ui/index.html +83 -1
- package/dist/ui/server/actions.html +1193 -0
- package/dist/ui/server/activity.html +1132 -0
- package/dist/ui/server/backups.html +57 -1
- package/dist/ui/server/database.html +1208 -0
- package/dist/ui/server/deployments.html +79 -1
- package/dist/ui/server/diagnostics.html +1132 -0
- package/dist/ui/server/logs.html +1165 -0
- package/dist/ui/server/metrics.html +1 -1
- package/dist/ui/server/security.html +1132 -0
- package/dist/ui/server/services.html +57 -1
- package/dist/ui/server/sites.html +141 -1
- package/dist/ui/server/ssh-keys.html +58 -1
- package/dist/ui/server/workers.html +57 -1
- package/dist/ui/serverless/assets.html +1 -1
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +1 -1
- package/dist/ui/serverless/deployments.html +1 -1
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +1 -1
- package/dist/ui/serverless/logs.html +1165 -0
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +1 -1
- package/dist/ui/serverless/scheduler.html +1 -1
- package/dist/ui/serverless/secrets.html +1 -1
- package/dist/ui/serverless.html +1 -1
- package/dist/ui-src/package.json +5 -0
- package/dist/ui-src/pages/index.stx +308 -0
- package/dist/ui-src/pages/partials/head.stx +294 -0
- package/dist/ui-src/pages/partials/nav.stx +54 -0
- package/dist/ui-src/pages/partials/op-confirm.stx +7 -0
- package/dist/ui-src/pages/server/actions.stx +60 -0
- package/dist/ui-src/pages/server/activity.stx +55 -0
- package/dist/ui-src/pages/server/backups.stx +91 -0
- package/dist/ui-src/pages/server/database.stx +119 -0
- package/dist/ui-src/pages/server/deployments.stx +106 -0
- package/dist/ui-src/pages/server/diagnostics.stx +88 -0
- package/dist/ui-src/pages/server/logs.stx +84 -0
- package/dist/ui-src/pages/server/metrics.stx +183 -0
- package/dist/ui-src/pages/server/security.stx +127 -0
- package/dist/ui-src/pages/server/services.stx +82 -0
- package/dist/ui-src/pages/server/sites.stx +143 -0
- package/dist/ui-src/pages/server/ssh-keys.stx +93 -0
- package/dist/ui-src/pages/server/workers.stx +99 -0
- package/dist/ui-src/pages/serverless/assets.stx +66 -0
- package/dist/ui-src/pages/serverless/cost.stx +149 -0
- package/dist/ui-src/pages/serverless/data.stx +76 -0
- package/dist/ui-src/pages/serverless/deployments.stx +56 -0
- package/dist/ui-src/pages/serverless/firewall.stx +58 -0
- package/dist/ui-src/pages/serverless/functions.stx +118 -0
- package/dist/ui-src/pages/serverless/logs.stx +84 -0
- package/dist/ui-src/pages/serverless/metrics.stx +76 -0
- package/dist/ui-src/pages/serverless/queues.stx +81 -0
- package/dist/ui-src/pages/serverless/scheduler.stx +60 -0
- package/dist/ui-src/pages/serverless/secrets.stx +55 -0
- package/dist/ui-src/pages/serverless.stx +222 -0
- package/dist/ui-src/src/charts.ts +17333 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -18,8 +18,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
18
18
|
|
|
19
19
|
// ../core/dist/index.js
|
|
20
20
|
import { createRequire as createRequire2 } from "node:module";
|
|
21
|
-
import { existsSync as
|
|
22
|
-
import { join as
|
|
21
|
+
import { existsSync as existsSync14, readdirSync as readdirSync6 } from "node:fs";
|
|
22
|
+
import { join as join9 } from "node:path";
|
|
23
23
|
import { createHash } from "node:crypto";
|
|
24
24
|
import { readFileSync as readFileSync2, readdirSync as readdirSync22, statSync as statSync3, existsSync as existsSync23, writeFileSync as writeFileSync8, copyFileSync } from "node:fs";
|
|
25
25
|
import { join as join22, basename, dirname as dirname5, extname as extname2 } from "node:path";
|
|
@@ -50,7 +50,7 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
|
50
50
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
51
51
|
import { existsSync as existsSync52, mkdirSync as mkdirSync22, mkdtempSync as mkdtempSync3, readdirSync as readdirSync52, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync32, writeFileSync as writeFileSync52 } from "node:fs";
|
|
52
52
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
53
|
-
import { join as
|
|
53
|
+
import { join as join92, relative as relative22 } from "node:path";
|
|
54
54
|
import { execSync as execSync2 } from "node:child_process";
|
|
55
55
|
import { createHash as createHash5 } from "node:crypto";
|
|
56
56
|
import { readdirSync as readdirSync62, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
@@ -7693,6 +7693,21 @@ function resolveManagementDashboardSite(config6, environment, opts) {
|
|
|
7693
7693
|
const domain = resolveDashboardDomain(config6, environment, opts.domain);
|
|
7694
7694
|
if (!domain)
|
|
7695
7695
|
return null;
|
|
7696
|
+
const auth2 = opts.password ? { auth: { username: opts.username || "admin", password: opts.password, realm: opts.realm } } : {};
|
|
7697
|
+
if (opts.live) {
|
|
7698
|
+
const port = opts.port ?? 7676;
|
|
7699
|
+
const site2 = {
|
|
7700
|
+
root: opts.uiRoot,
|
|
7701
|
+
deploy: "server",
|
|
7702
|
+
domain,
|
|
7703
|
+
start: `cloud dashboard:serve --box --host 127.0.0.1 --port ${port}`,
|
|
7704
|
+
port,
|
|
7705
|
+
ssl: { provider: "letsencrypt" },
|
|
7706
|
+
...opts.build === false || opts.build === undefined ? {} : { build: opts.build },
|
|
7707
|
+
...auth2
|
|
7708
|
+
};
|
|
7709
|
+
return { name: "dashboard", site: site2 };
|
|
7710
|
+
}
|
|
7696
7711
|
const site = {
|
|
7697
7712
|
root: opts.uiRoot,
|
|
7698
7713
|
deploy: "server",
|
|
@@ -7700,7 +7715,7 @@ function resolveManagementDashboardSite(config6, environment, opts) {
|
|
|
7700
7715
|
domain,
|
|
7701
7716
|
ssl: { provider: "letsencrypt" },
|
|
7702
7717
|
...opts.build === false || opts.build === undefined ? {} : { build: opts.build },
|
|
7703
|
-
...
|
|
7718
|
+
...auth2
|
|
7704
7719
|
};
|
|
7705
7720
|
return { name: "dashboard", site };
|
|
7706
7721
|
}
|
|
@@ -26931,7 +26946,7 @@ function laravelServerlessEnvDefaults(opts = {}) {
|
|
|
26931
26946
|
}
|
|
26932
26947
|
function* walk(dir) {
|
|
26933
26948
|
for (const entry of readdirSync52(dir)) {
|
|
26934
|
-
const full =
|
|
26949
|
+
const full = join92(dir, entry);
|
|
26935
26950
|
if (statSync32(full).isDirectory())
|
|
26936
26951
|
yield* walk(full);
|
|
26937
26952
|
else
|
|
@@ -26942,22 +26957,22 @@ function buildPhpRuntimeLayerZip(options = {}) {
|
|
|
26942
26957
|
const architecture = options.architecture ?? "x86_64";
|
|
26943
26958
|
const platform = options.platform ?? (architecture === "arm64" ? "linux/arm64" : "linux/amd64");
|
|
26944
26959
|
const step = options.onStep ?? (() => {});
|
|
26945
|
-
const stage = mkdtempSync3(
|
|
26960
|
+
const stage = mkdtempSync3(join92(tmpdir3(), "tscloud-php-layer-"));
|
|
26946
26961
|
const imageTag = "tscloud-php-layer:build";
|
|
26947
26962
|
try {
|
|
26948
|
-
writeFileSync52(
|
|
26963
|
+
writeFileSync52(join92(stage, "Dockerfile"), generatePhpLayerDockerfile(options));
|
|
26949
26964
|
step("Building PHP runtime image (docker)");
|
|
26950
26965
|
execFileSync2("docker", ["build", "--platform", platform, "-t", imageTag, stage], { stdio: "inherit" });
|
|
26951
26966
|
step("Extracting /opt/php from image");
|
|
26952
26967
|
const cid = execFileSync2("docker", ["create", "--platform", platform, imageTag], { encoding: "utf-8" }).trim();
|
|
26953
|
-
const optDir =
|
|
26968
|
+
const optDir = join92(stage, "opt");
|
|
26954
26969
|
mkdirSync22(optDir, { recursive: true });
|
|
26955
26970
|
try {
|
|
26956
|
-
execFileSync2("docker", ["cp", `${cid}:/opt/php`,
|
|
26971
|
+
execFileSync2("docker", ["cp", `${cid}:/opt/php`, join92(optDir, "php")], { stdio: "inherit" });
|
|
26957
26972
|
} finally {
|
|
26958
26973
|
execFileSync2("docker", ["rm", cid], { stdio: "ignore" });
|
|
26959
26974
|
}
|
|
26960
|
-
if (!existsSync52(
|
|
26975
|
+
if (!existsSync52(join92(optDir, "php", "bin", "php")))
|
|
26961
26976
|
throw new Error("layer build produced no /opt/php/bin/php");
|
|
26962
26977
|
const entries = [];
|
|
26963
26978
|
for (const file of walk(optDir)) {
|
|
@@ -29480,12 +29495,12 @@ var init_dist2 = __esm(async () => {
|
|
|
29480
29495
|
docsPaths = ["docs", "documentation", "doc"]
|
|
29481
29496
|
} = options;
|
|
29482
29497
|
for (const docsPath of docsPaths) {
|
|
29483
|
-
const fullPath =
|
|
29484
|
-
if (
|
|
29498
|
+
const fullPath = join9(projectRoot, docsPath);
|
|
29499
|
+
if (existsSync14(fullPath)) {
|
|
29485
29500
|
const distPaths = ["dist", "build", ".bunpress/dist", "_site", "out", "public"];
|
|
29486
29501
|
for (const distPath of distPaths) {
|
|
29487
|
-
const fullDistPath =
|
|
29488
|
-
if (
|
|
29502
|
+
const fullDistPath = join9(fullPath, distPath);
|
|
29503
|
+
if (existsSync14(fullDistPath)) {
|
|
29489
29504
|
try {
|
|
29490
29505
|
const files = readdirSync6(fullDistPath);
|
|
29491
29506
|
if (files.length > 0) {
|
|
@@ -29602,16 +29617,16 @@ var init_dist2 = __esm(async () => {
|
|
|
29602
29617
|
const privatePath = paths.private || "private";
|
|
29603
29618
|
return {
|
|
29604
29619
|
web: {
|
|
29605
|
-
exists:
|
|
29606
|
-
path:
|
|
29620
|
+
exists: existsSync14(join9(projectRoot, webPath)),
|
|
29621
|
+
path: join9(projectRoot, webPath)
|
|
29607
29622
|
},
|
|
29608
29623
|
docs: {
|
|
29609
|
-
exists:
|
|
29610
|
-
path:
|
|
29624
|
+
exists: existsSync14(join9(projectRoot, docsPath)),
|
|
29625
|
+
path: join9(projectRoot, docsPath)
|
|
29611
29626
|
},
|
|
29612
29627
|
private: {
|
|
29613
|
-
exists:
|
|
29614
|
-
path:
|
|
29628
|
+
exists: existsSync14(join9(projectRoot, privatePath)),
|
|
29629
|
+
path: join9(projectRoot, privatePath)
|
|
29615
29630
|
}
|
|
29616
29631
|
};
|
|
29617
29632
|
}
|
|
@@ -47221,7 +47236,7 @@ __export(exports_client, {
|
|
|
47221
47236
|
AWSClient: () => AWSClient
|
|
47222
47237
|
});
|
|
47223
47238
|
import * as crypto2 from "node:crypto";
|
|
47224
|
-
import { existsSync as
|
|
47239
|
+
import { existsSync as existsSync16, readFileSync as readFileSync8 } from "node:fs";
|
|
47225
47240
|
import { homedir as homedir6 } from "node:os";
|
|
47226
47241
|
import { join as join11 } from "node:path";
|
|
47227
47242
|
function resolveS3Endpoint(options) {
|
|
@@ -47282,7 +47297,7 @@ class AWSClient {
|
|
|
47282
47297
|
loadCredentialsFromFile() {
|
|
47283
47298
|
const profile = process.env.AWS_PROFILE || "default";
|
|
47284
47299
|
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join11(homedir6(), ".aws", "credentials");
|
|
47285
|
-
if (!
|
|
47300
|
+
if (!existsSync16(credentialsPath)) {
|
|
47286
47301
|
return null;
|
|
47287
47302
|
}
|
|
47288
47303
|
try {
|
|
@@ -47781,7 +47796,7 @@ function detectCredentialSource() {
|
|
|
47781
47796
|
}
|
|
47782
47797
|
const profile = process.env.AWS_PROFILE || "default";
|
|
47783
47798
|
const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join11(homedir6(), ".aws", "credentials");
|
|
47784
|
-
if (
|
|
47799
|
+
if (existsSync16(credentialsPath)) {
|
|
47785
47800
|
return {
|
|
47786
47801
|
source: "file",
|
|
47787
47802
|
profile,
|
|
@@ -50354,8 +50369,8 @@ class CloudFrontClient {
|
|
|
50354
50369
|
returnHeaders: true
|
|
50355
50370
|
});
|
|
50356
50371
|
const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
|
|
50357
|
-
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig;
|
|
50358
|
-
if (!currentConfig) {
|
|
50372
|
+
const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
|
|
50373
|
+
if (!currentConfig || currentConfig.Enabled === undefined) {
|
|
50359
50374
|
throw new Error("Failed to get current distribution config");
|
|
50360
50375
|
}
|
|
50361
50376
|
if (customErrorResponses.length === 0) {
|
|
@@ -50416,8 +50431,8 @@ class CloudFrontClient {
|
|
|
50416
50431
|
returnHeaders: true
|
|
50417
50432
|
});
|
|
50418
50433
|
const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
|
|
50419
|
-
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig;
|
|
50420
|
-
if (!currentConfig) {
|
|
50434
|
+
const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
|
|
50435
|
+
if (!currentConfig || currentConfig.Enabled === undefined) {
|
|
50421
50436
|
throw new Error("Failed to get current distribution config");
|
|
50422
50437
|
}
|
|
50423
50438
|
if (aliases && aliases.length > 0) {
|
|
@@ -51064,8 +51079,8 @@ ${Object.entries(config6).filter(([k]) => !k.startsWith("@_")).map(([key, val])
|
|
|
51064
51079
|
returnHeaders: true
|
|
51065
51080
|
});
|
|
51066
51081
|
const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
|
|
51067
|
-
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig;
|
|
51068
|
-
if (!currentConfig) {
|
|
51082
|
+
const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
|
|
51083
|
+
if (!currentConfig || currentConfig.Enabled === undefined) {
|
|
51069
51084
|
throw new Error("Failed to get current distribution config");
|
|
51070
51085
|
}
|
|
51071
51086
|
currentConfig.Enabled = false;
|
|
@@ -51125,8 +51140,8 @@ ${Object.entries(config6).filter(([k]) => !k.startsWith("@_")).map(([key, val])
|
|
|
51125
51140
|
returnHeaders: true
|
|
51126
51141
|
});
|
|
51127
51142
|
const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
|
|
51128
|
-
const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig;
|
|
51129
|
-
if (!currentConfig) {
|
|
51143
|
+
const currentConfig = getResult.body?.DistributionConfig || (getResult.body?.Enabled !== undefined ? getResult.body : undefined) || getResult.DistributionConfig;
|
|
51144
|
+
if (!currentConfig || currentConfig.Enabled === undefined) {
|
|
51130
51145
|
throw new Error("Failed to get current distribution config");
|
|
51131
51146
|
}
|
|
51132
51147
|
let items = [];
|
|
@@ -58116,14 +58131,14 @@ __export(exports_serverless_image, {
|
|
|
58116
58131
|
buildAndPushServerlessImage: () => buildAndPushServerlessImage
|
|
58117
58132
|
});
|
|
58118
58133
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
58119
|
-
import { mkdirSync as
|
|
58134
|
+
import { mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync4, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "node:fs";
|
|
58120
58135
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
58121
|
-
import { dirname as dirname7, join as
|
|
58136
|
+
import { dirname as dirname7, join as join16 } from "node:path";
|
|
58122
58137
|
function writeEntries(baseDir, entries) {
|
|
58123
58138
|
for (const e of entries) {
|
|
58124
|
-
const dest =
|
|
58125
|
-
|
|
58126
|
-
|
|
58139
|
+
const dest = join16(baseDir, e.name);
|
|
58140
|
+
mkdirSync8(dirname7(dest), { recursive: true });
|
|
58141
|
+
writeFileSync11(dest, e.data, e.mode ? { mode: e.mode } : undefined);
|
|
58127
58142
|
}
|
|
58128
58143
|
}
|
|
58129
58144
|
async function buildAndPushServerlessImage(opts) {
|
|
@@ -58132,10 +58147,10 @@ async function buildAndPushServerlessImage(opts) {
|
|
|
58132
58147
|
const platform = arch === "arm64" ? "linux/arm64" : "linux/amd64";
|
|
58133
58148
|
const isPhp = app.kind === "php";
|
|
58134
58149
|
const step2 = opts.onStep ?? (() => {});
|
|
58135
|
-
const stage = mkdtempSync4(
|
|
58150
|
+
const stage = mkdtempSync4(join16(tmpdir4(), "tscloud-img-"));
|
|
58136
58151
|
try {
|
|
58137
|
-
const appDir =
|
|
58138
|
-
|
|
58152
|
+
const appDir = join16(stage, "app");
|
|
58153
|
+
mkdirSync8(appDir, { recursive: true });
|
|
58139
58154
|
let handlers;
|
|
58140
58155
|
let contentHash;
|
|
58141
58156
|
if (isPhp) {
|
|
@@ -58143,19 +58158,19 @@ async function buildAndPushServerlessImage(opts) {
|
|
|
58143
58158
|
step2("staging application tree");
|
|
58144
58159
|
const entries = collectPhpAppEntries(projectRoot);
|
|
58145
58160
|
writeEntries(appDir, entries);
|
|
58146
|
-
writeEntries(
|
|
58161
|
+
writeEntries(join16(stage, "runtime"), phpRuntimeLayerAssets().map((a) => ({ name: a.path, data: a.contents, mode: a.mode })));
|
|
58147
58162
|
const handler8 = app.handlers?.http ?? "public/index.php";
|
|
58148
58163
|
handlers = { http: handler8, queue: app.handlers?.queue ?? handler8, cli: app.handlers?.cli ?? handler8 };
|
|
58149
58164
|
contentHash = sha256(entries.map((e) => `${e.name}:${e.data.length}`).join("|"));
|
|
58150
58165
|
} else {
|
|
58151
58166
|
step2("bundling application");
|
|
58152
58167
|
const artifact = await packageServerlessApp({ projectRoot, app, skipBuild: opts.skipBuild, onStep: step2 });
|
|
58153
|
-
|
|
58168
|
+
writeFileSync11(join16(appDir, "index.mjs"), artifact.bundle);
|
|
58154
58169
|
handlers = artifact.handlers;
|
|
58155
58170
|
contentHash = artifact.sha256;
|
|
58156
58171
|
}
|
|
58157
58172
|
const tag = contentHash.slice(0, 16);
|
|
58158
|
-
|
|
58173
|
+
writeFileSync11(join16(stage, "Dockerfile"), generateAppImageDockerfile({
|
|
58159
58174
|
kind: app.kind ?? "node",
|
|
58160
58175
|
phpVersion: app.phpVersion,
|
|
58161
58176
|
architecture: arch
|
|
@@ -58184,14 +58199,14 @@ async function buildAndPushServerlessImage(opts) {
|
|
|
58184
58199
|
if (!data?.authorizationToken || !data.proxyEndpoint)
|
|
58185
58200
|
throw new Error("failed to get an ECR authorization token");
|
|
58186
58201
|
const registry = data.proxyEndpoint.replace(/^https?:\/\//, "");
|
|
58187
|
-
const dockerConfig =
|
|
58188
|
-
|
|
58189
|
-
|
|
58202
|
+
const dockerConfig = join16(stage, ".docker");
|
|
58203
|
+
mkdirSync8(dockerConfig, { recursive: true });
|
|
58204
|
+
writeFileSync11(join16(dockerConfig, "config.json"), JSON.stringify({ auths: { [registry]: { auth: data.authorizationToken } } }));
|
|
58190
58205
|
execFileSync3("docker", ["push", imageUri], { stdio: "inherit", env: { ...process.env, DOCKER_CONFIG: dockerConfig } });
|
|
58191
58206
|
info(`Image: ${imageUri}`);
|
|
58192
58207
|
return { imageUri, tag, handlers };
|
|
58193
58208
|
} finally {
|
|
58194
|
-
|
|
58209
|
+
rmSync6(stage, { recursive: true, force: true });
|
|
58195
58210
|
}
|
|
58196
58211
|
}
|
|
58197
58212
|
var init_serverless_image = __esm(async () => {
|
|
@@ -58200,6 +58215,11 @@ var init_serverless_image = __esm(async () => {
|
|
|
58200
58215
|
await init_dist2();
|
|
58201
58216
|
});
|
|
58202
58217
|
|
|
58218
|
+
// src/config.ts
|
|
58219
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
58220
|
+
import { join as join7 } from "node:path";
|
|
58221
|
+
import { pathToFileURL } from "node:url";
|
|
58222
|
+
|
|
58203
58223
|
// ../../node_modules/bunfig/dist/index.js
|
|
58204
58224
|
import { existsSync, statSync } from "fs";
|
|
58205
58225
|
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync, writeFileSync as writeFileSync7 } from "fs";
|
|
@@ -66891,12 +66911,33 @@ var defaultConfig4 = {
|
|
|
66891
66911
|
}
|
|
66892
66912
|
};
|
|
66893
66913
|
var _config = null;
|
|
66914
|
+
function isDefaultConfig(config5) {
|
|
66915
|
+
return config5.project?.slug === defaultConfig4.project?.slug && Object.keys(config5.environments ?? {}).length === 1 && !!config5.environments?.production;
|
|
66916
|
+
}
|
|
66917
|
+
async function loadStacksCloudConfig() {
|
|
66918
|
+
const candidates = [
|
|
66919
|
+
join7(process.cwd(), "config", "cloud.ts"),
|
|
66920
|
+
join7(process.cwd(), "config", "cloud.js")
|
|
66921
|
+
];
|
|
66922
|
+
for (const file of candidates) {
|
|
66923
|
+
if (!existsSync11(file))
|
|
66924
|
+
continue;
|
|
66925
|
+
const mod = await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
66926
|
+
const config5 = mod.tsCloud ?? mod.default?.tsCloud ?? mod.default ?? mod.cloud ?? mod.config;
|
|
66927
|
+
if (config5)
|
|
66928
|
+
return config5;
|
|
66929
|
+
}
|
|
66930
|
+
return null;
|
|
66931
|
+
}
|
|
66894
66932
|
async function getConfig() {
|
|
66895
66933
|
if (!_config) {
|
|
66896
66934
|
_config = await loadConfig6({
|
|
66897
66935
|
name: "cloud",
|
|
66898
66936
|
defaultConfig: defaultConfig4
|
|
66899
66937
|
});
|
|
66938
|
+
if (isDefaultConfig(_config)) {
|
|
66939
|
+
_config = await loadStacksCloudConfig() ?? _config;
|
|
66940
|
+
}
|
|
66900
66941
|
}
|
|
66901
66942
|
return _config;
|
|
66902
66943
|
}
|
|
@@ -70411,9 +70452,143 @@ init_cloudformation();
|
|
|
70411
70452
|
init_client();
|
|
70412
70453
|
|
|
70413
70454
|
// src/aws/cost-explorer-cache.ts
|
|
70455
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
70456
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync8, readFileSync as readFileSync11, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "node:fs";
|
|
70457
|
+
import { homedir as homedir7 } from "node:os";
|
|
70458
|
+
import { join as join14 } from "node:path";
|
|
70459
|
+
var CACHE_SCHEMA_VERSION = 1;
|
|
70414
70460
|
var HOUR_MS = 60 * 60 * 1000;
|
|
70415
70461
|
var DAY_MS = 24 * HOUR_MS;
|
|
70462
|
+
var OPEN_PERIOD_TTL_MS = HOUR_MS;
|
|
70416
70463
|
var CLOSED_PERIOD_TTL_MS = 30 * DAY_MS;
|
|
70464
|
+
function cacheRoot() {
|
|
70465
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
70466
|
+
return join14(xdg && xdg.trim() ? xdg : join14(homedir7(), ".cache"), "ts-cloud", "cost-explorer");
|
|
70467
|
+
}
|
|
70468
|
+
function profileDir(profile) {
|
|
70469
|
+
return join14(cacheRoot(), profile ?? "__default__");
|
|
70470
|
+
}
|
|
70471
|
+
function hashKey(key) {
|
|
70472
|
+
const canonical = {
|
|
70473
|
+
s: key.start,
|
|
70474
|
+
e: key.end,
|
|
70475
|
+
g: key.granularity,
|
|
70476
|
+
m: [...key.metrics].sort(),
|
|
70477
|
+
gb: [...key.groupBy].map((g) => `${g.Type}:${g.Key}`).sort()
|
|
70478
|
+
};
|
|
70479
|
+
return createHash9("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 24);
|
|
70480
|
+
}
|
|
70481
|
+
function isClosedPeriod(start, end) {
|
|
70482
|
+
const endTs = Date.parse(`${end}T00:00:00Z`);
|
|
70483
|
+
if (Number.isNaN(endTs))
|
|
70484
|
+
return false;
|
|
70485
|
+
const now = new Date;
|
|
70486
|
+
const firstOfThisMonth = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1);
|
|
70487
|
+
return endTs <= firstOfThisMonth;
|
|
70488
|
+
}
|
|
70489
|
+
function ttlForPeriod(key) {
|
|
70490
|
+
return isClosedPeriod(key.start, key.end) ? CLOSED_PERIOD_TTL_MS : OPEN_PERIOD_TTL_MS;
|
|
70491
|
+
}
|
|
70492
|
+
function loadCache(profile, key) {
|
|
70493
|
+
const file = join14(profileDir(profile), `${hashKey(key)}.json`);
|
|
70494
|
+
if (!existsSync17(file))
|
|
70495
|
+
return null;
|
|
70496
|
+
let entry;
|
|
70497
|
+
try {
|
|
70498
|
+
entry = JSON.parse(readFileSync11(file, "utf-8"));
|
|
70499
|
+
} catch {
|
|
70500
|
+
return null;
|
|
70501
|
+
}
|
|
70502
|
+
if (entry.schema !== CACHE_SCHEMA_VERSION)
|
|
70503
|
+
return null;
|
|
70504
|
+
const ageMs = Date.now() - entry.savedAt;
|
|
70505
|
+
if (ageMs < 0 || ageMs > ttlForPeriod(key))
|
|
70506
|
+
return null;
|
|
70507
|
+
return { response: entry.response, ageSeconds: Math.floor(ageMs / 1000) };
|
|
70508
|
+
}
|
|
70509
|
+
function saveCache(profile, key, response) {
|
|
70510
|
+
const dir = profileDir(profile);
|
|
70511
|
+
mkdirSync7(dir, { recursive: true });
|
|
70512
|
+
const entry = {
|
|
70513
|
+
schema: CACHE_SCHEMA_VERSION,
|
|
70514
|
+
savedAt: Date.now(),
|
|
70515
|
+
key,
|
|
70516
|
+
response
|
|
70517
|
+
};
|
|
70518
|
+
writeFileSync10(join14(dir, `${hashKey(key)}.json`), JSON.stringify(entry));
|
|
70519
|
+
}
|
|
70520
|
+
|
|
70521
|
+
// src/aws/cost-explorer.ts
|
|
70522
|
+
class CostExplorerClient {
|
|
70523
|
+
client;
|
|
70524
|
+
profile;
|
|
70525
|
+
useCache;
|
|
70526
|
+
lastCacheAgeSeconds = null;
|
|
70527
|
+
constructor(profile, options) {
|
|
70528
|
+
this.profile = profile;
|
|
70529
|
+
this.useCache = options?.useCache ?? true;
|
|
70530
|
+
this.client = new AWSClient(resolveCredentials2(profile));
|
|
70531
|
+
}
|
|
70532
|
+
async getCostByService(params) {
|
|
70533
|
+
const { start, end, granularity = "MONTHLY" } = params;
|
|
70534
|
+
const metrics = ["UnblendedCost"];
|
|
70535
|
+
const groupBy = [{ Type: "DIMENSION", Key: "SERVICE" }];
|
|
70536
|
+
const cacheKey = { start, end, granularity, metrics, groupBy };
|
|
70537
|
+
if (this.useCache) {
|
|
70538
|
+
const hit = loadCache(this.profile, cacheKey);
|
|
70539
|
+
if (hit) {
|
|
70540
|
+
this.lastCacheAgeSeconds = hit.ageSeconds;
|
|
70541
|
+
return hit.response;
|
|
70542
|
+
}
|
|
70543
|
+
}
|
|
70544
|
+
this.lastCacheAgeSeconds = null;
|
|
70545
|
+
const result = await this.client.request({
|
|
70546
|
+
service: "ce",
|
|
70547
|
+
region: "us-east-1",
|
|
70548
|
+
method: "POST",
|
|
70549
|
+
path: "/",
|
|
70550
|
+
headers: {
|
|
70551
|
+
"content-type": "application/x-amz-json-1.1",
|
|
70552
|
+
"x-amz-target": "AWSInsightsIndexService.GetCostAndUsage"
|
|
70553
|
+
},
|
|
70554
|
+
body: JSON.stringify({
|
|
70555
|
+
TimePeriod: { Start: start, End: end },
|
|
70556
|
+
Granularity: granularity,
|
|
70557
|
+
Metrics: metrics,
|
|
70558
|
+
GroupBy: groupBy
|
|
70559
|
+
})
|
|
70560
|
+
});
|
|
70561
|
+
const groups = result?.ResultsByTime?.[0]?.Groups ?? [];
|
|
70562
|
+
const services = groups.map((g) => ({
|
|
70563
|
+
service: g.Keys?.[0] ?? "Unknown",
|
|
70564
|
+
amount: Number.parseFloat(g.Metrics?.UnblendedCost?.Amount ?? "0"),
|
|
70565
|
+
unit: g.Metrics?.UnblendedCost?.Unit ?? "USD"
|
|
70566
|
+
})).filter((g) => g.amount > 0).sort((a, b) => b.amount - a.amount);
|
|
70567
|
+
if (this.useCache) {
|
|
70568
|
+
saveCache(this.profile, cacheKey, services);
|
|
70569
|
+
}
|
|
70570
|
+
return services;
|
|
70571
|
+
}
|
|
70572
|
+
async getDailyTotals(params) {
|
|
70573
|
+
const result = await this.client.request({
|
|
70574
|
+
service: "ce",
|
|
70575
|
+
region: "us-east-1",
|
|
70576
|
+
method: "POST",
|
|
70577
|
+
path: "/",
|
|
70578
|
+
headers: {
|
|
70579
|
+
"content-type": "application/x-amz-json-1.1",
|
|
70580
|
+
"x-amz-target": "AWSInsightsIndexService.GetCostAndUsage"
|
|
70581
|
+
},
|
|
70582
|
+
body: JSON.stringify({
|
|
70583
|
+
TimePeriod: { Start: params.start, End: params.end },
|
|
70584
|
+
Granularity: "DAILY",
|
|
70585
|
+
Metrics: ["UnblendedCost"]
|
|
70586
|
+
})
|
|
70587
|
+
});
|
|
70588
|
+
const byTime = result?.ResultsByTime ?? [];
|
|
70589
|
+
return byTime.map((t) => Number.parseFloat(t.Total?.UnblendedCost?.Amount ?? "0"));
|
|
70590
|
+
}
|
|
70591
|
+
}
|
|
70417
70592
|
// src/aws/ec2.ts
|
|
70418
70593
|
init_client();
|
|
70419
70594
|
|
|
@@ -78946,7 +79121,7 @@ class ApplicationAutoScalingClient {
|
|
|
78946
79121
|
init_s3();
|
|
78947
79122
|
import * as net from "node:net";
|
|
78948
79123
|
import * as tls from "node:tls";
|
|
78949
|
-
import { readFileSync as
|
|
79124
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
78950
79125
|
import * as crypto4 from "node:crypto";
|
|
78951
79126
|
|
|
78952
79127
|
class ImapServer {
|
|
@@ -78981,8 +79156,8 @@ class ImapServer {
|
|
|
78981
79156
|
});
|
|
78982
79157
|
if (this.config.tls?.key && this.config.tls?.cert) {
|
|
78983
79158
|
const tlsOptions = {
|
|
78984
|
-
key:
|
|
78985
|
-
cert:
|
|
79159
|
+
key: readFileSync12(this.config.tls.key),
|
|
79160
|
+
cert: readFileSync12(this.config.tls.cert)
|
|
78986
79161
|
};
|
|
78987
79162
|
this.tlsServer = tls.createServer(tlsOptions, (socket) => {
|
|
78988
79163
|
this.handleConnection(socket);
|
|
@@ -79786,8 +79961,8 @@ class ImapServer {
|
|
|
79786
79961
|
}
|
|
79787
79962
|
this.send(session, `${tag} OK Begin TLS negotiation`);
|
|
79788
79963
|
const tlsOptions = {
|
|
79789
|
-
key:
|
|
79790
|
-
cert:
|
|
79964
|
+
key: readFileSync12(this.config.tls.key),
|
|
79965
|
+
cert: readFileSync12(this.config.tls.cert),
|
|
79791
79966
|
isServer: true
|
|
79792
79967
|
};
|
|
79793
79968
|
const tlsSocket = new tls.TLSSocket(session.socket, tlsOptions);
|
|
@@ -80380,7 +80555,7 @@ init_ses();
|
|
|
80380
80555
|
init_s3();
|
|
80381
80556
|
import * as net2 from "node:net";
|
|
80382
80557
|
import * as tls2 from "node:tls";
|
|
80383
|
-
import { readFileSync as
|
|
80558
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
80384
80559
|
import * as crypto5 from "node:crypto";
|
|
80385
80560
|
|
|
80386
80561
|
class SmtpServer {
|
|
@@ -80411,8 +80586,8 @@ class SmtpServer {
|
|
|
80411
80586
|
});
|
|
80412
80587
|
if (this.config.tls?.key && this.config.tls?.cert) {
|
|
80413
80588
|
const tlsOptions = {
|
|
80414
|
-
key:
|
|
80415
|
-
cert:
|
|
80589
|
+
key: readFileSync13(this.config.tls.key),
|
|
80590
|
+
cert: readFileSync13(this.config.tls.cert)
|
|
80416
80591
|
};
|
|
80417
80592
|
this.tlsServer = tls2.createServer(tlsOptions, (socket) => {
|
|
80418
80593
|
this.handleConnection(socket, true);
|
|
@@ -80546,8 +80721,8 @@ class SmtpServer {
|
|
|
80546
80721
|
}
|
|
80547
80722
|
this.send(session, "220 Ready to start TLS");
|
|
80548
80723
|
const tlsOptions = {
|
|
80549
|
-
key:
|
|
80550
|
-
cert:
|
|
80724
|
+
key: readFileSync13(this.config.tls.key),
|
|
80725
|
+
cert: readFileSync13(this.config.tls.cert),
|
|
80551
80726
|
isServer: true
|
|
80552
80727
|
};
|
|
80553
80728
|
const tlsSocket = new tls2.TLSSocket(session.socket, tlsOptions);
|
|
@@ -82896,7 +83071,7 @@ function needsRenewal(certPath) {
|
|
|
82896
83071
|
}
|
|
82897
83072
|
}
|
|
82898
83073
|
// src/ssl/acme-client.ts
|
|
82899
|
-
import { createHash as
|
|
83074
|
+
import { createHash as createHash10, createSign, generateKeyPairSync } from "node:crypto";
|
|
82900
83075
|
var ACME_DIRECTORIES = {
|
|
82901
83076
|
production: "https://acme-v02.api.letsencrypt.org/directory",
|
|
82902
83077
|
staging: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
@@ -82966,7 +83141,7 @@ class AcmeClient {
|
|
|
82966
83141
|
x: jwk.x,
|
|
82967
83142
|
y: jwk.y
|
|
82968
83143
|
});
|
|
82969
|
-
const hash2 =
|
|
83144
|
+
const hash2 = createHash10("sha256").update(canonical).digest();
|
|
82970
83145
|
return this.base64UrlEncode(hash2);
|
|
82971
83146
|
}
|
|
82972
83147
|
base64UrlEncode(data) {
|
|
@@ -83079,7 +83254,7 @@ class AcmeClient {
|
|
|
83079
83254
|
identifier: `/.well-known/acme-challenge/${c.token}`
|
|
83080
83255
|
};
|
|
83081
83256
|
} else {
|
|
83082
|
-
const dnsValue = this.base64UrlEncode(
|
|
83257
|
+
const dnsValue = this.base64UrlEncode(createHash10("sha256").update(keyAuthorization).digest());
|
|
83083
83258
|
return {
|
|
83084
83259
|
type: "dns-01",
|
|
83085
83260
|
token: c.token,
|
|
@@ -83167,6 +83342,8 @@ function resolveSiteDeployTarget(site) {
|
|
|
83167
83342
|
return "bucket";
|
|
83168
83343
|
}
|
|
83169
83344
|
function resolveSiteKind(site) {
|
|
83345
|
+
if (site.redirect)
|
|
83346
|
+
return "redirect";
|
|
83170
83347
|
if (isPhpSite(site))
|
|
83171
83348
|
return "server-php";
|
|
83172
83349
|
const target = resolveSiteDeployTarget(site);
|
|
@@ -83189,6 +83366,24 @@ function validateDeploymentConfig(config6) {
|
|
|
83189
83366
|
}
|
|
83190
83367
|
const target = resolveSiteDeployTarget(site);
|
|
83191
83368
|
const kind = resolveSiteKind(site);
|
|
83369
|
+
if (kind === "redirect") {
|
|
83370
|
+
if (!site.domain)
|
|
83371
|
+
errors.push(`Site '${name}' is a redirect site but has no \`domain\` to redirect from.`);
|
|
83372
|
+
const to = typeof site.redirect === "string" ? site.redirect : site.redirect?.to;
|
|
83373
|
+
if (!to)
|
|
83374
|
+
errors.push(`Site '${name}' is a redirect site but has no redirect target (\`redirect\` / \`redirect.to\`).`);
|
|
83375
|
+
if (!computeConfigured) {
|
|
83376
|
+
errors.push(`Site '${name}' is a redirect site but no \`infrastructure.compute\` is configured to host the gateway that serves the redirect.`);
|
|
83377
|
+
}
|
|
83378
|
+
const serverOnly = [];
|
|
83379
|
+
if (site.start)
|
|
83380
|
+
serverOnly.push("start");
|
|
83381
|
+
if (site.root)
|
|
83382
|
+
serverOnly.push("root");
|
|
83383
|
+
if (serverOnly.length > 0)
|
|
83384
|
+
warnings.push(`Site '${name}' is a redirect site but also sets ${serverOnly.join(", ")}. These are ignored.`);
|
|
83385
|
+
continue;
|
|
83386
|
+
}
|
|
83192
83387
|
if (target === "server" && !site.start && !site.root) {
|
|
83193
83388
|
errors.push(`Site '${name}' sets deploy:'server' but declares neither \`start\` (dynamic app) nor \`root\` (static site to serve). Add one.`);
|
|
83194
83389
|
continue;
|
|
@@ -83245,12 +83440,12 @@ init_static_site_external_dns();
|
|
|
83245
83440
|
// src/deploy/static-site-helper.ts
|
|
83246
83441
|
init_static_site_external_dns();
|
|
83247
83442
|
import process19 from "node:process";
|
|
83248
|
-
import { existsSync as
|
|
83249
|
-
import { join as
|
|
83443
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
83444
|
+
import { join as join15 } from "node:path";
|
|
83250
83445
|
async function deploySite(config6) {
|
|
83251
83446
|
const start = Date.now();
|
|
83252
83447
|
const sourceDir = config6.sourceDir ?? "dist";
|
|
83253
|
-
if (!
|
|
83448
|
+
if (!existsSync18(join15(sourceDir, "index.html"))) {
|
|
83254
83449
|
return fail(start, `${sourceDir}/ has no index.html — build first.`);
|
|
83255
83450
|
}
|
|
83256
83451
|
if (!process19.env.AWS_ACCESS_KEY_ID || !process19.env.AWS_SECRET_ACCESS_KEY) {
|
|
@@ -83321,8 +83516,8 @@ function fail(start, message) {
|
|
|
83321
83516
|
// src/deploy/serverless-app.ts
|
|
83322
83517
|
init_cloudformation();
|
|
83323
83518
|
await init_dist2();
|
|
83324
|
-
import { existsSync as
|
|
83325
|
-
import { join as
|
|
83519
|
+
import { existsSync as existsSync19, readdirSync as readdirSync9, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
83520
|
+
import { join as join17, relative as relative6 } from "node:path";
|
|
83326
83521
|
init_s3();
|
|
83327
83522
|
init_cli();
|
|
83328
83523
|
function resolveContext(config6, environment) {
|
|
@@ -83461,8 +83656,8 @@ function contentType(file) {
|
|
|
83461
83656
|
return map[ext] ?? "application/octet-stream";
|
|
83462
83657
|
}
|
|
83463
83658
|
function* walk3(dir) {
|
|
83464
|
-
for (const entry of
|
|
83465
|
-
const full =
|
|
83659
|
+
for (const entry of readdirSync9(dir)) {
|
|
83660
|
+
const full = join17(dir, entry);
|
|
83466
83661
|
if (statSync5(full).isDirectory())
|
|
83467
83662
|
yield* walk3(full);
|
|
83468
83663
|
else
|
|
@@ -83478,7 +83673,7 @@ async function uploadAssets(s32, bucket, dir, prefix, includeDotfiles = false) {
|
|
|
83478
83673
|
await s32.putObject({
|
|
83479
83674
|
bucket,
|
|
83480
83675
|
key: `${prefix}/${rel}`,
|
|
83481
|
-
body:
|
|
83676
|
+
body: readFileSync14(file),
|
|
83482
83677
|
contentType: contentType(file),
|
|
83483
83678
|
cacheControl: "public, max-age=31536000, immutable"
|
|
83484
83679
|
});
|
|
@@ -83610,8 +83805,8 @@ async function deployServerlessApp(config6, environment, opts = {}) {
|
|
|
83610
83805
|
const outputs = await cfn.getStackOutputs(stackName).catch(() => ({}));
|
|
83611
83806
|
let assetUrl;
|
|
83612
83807
|
if (app.assets) {
|
|
83613
|
-
const assetsDir3 =
|
|
83614
|
-
if (
|
|
83808
|
+
const assetsDir3 = join17(projectRoot, app.assets);
|
|
83809
|
+
if (existsSync19(assetsDir3)) {
|
|
83615
83810
|
const cdn = app.assetDomain || outputs.AssetsCdnDomain;
|
|
83616
83811
|
assetUrl = cdn ? `https://${cdn}/${artifactSha}` : undefined;
|
|
83617
83812
|
step(`Syncing assets from ${app.assets}`);
|
|
@@ -83791,6 +83986,54 @@ async function runRemoteCommand(config6, environment, command) {
|
|
|
83791
83986
|
throw new Error(`Command failed: ${res.Payload}`);
|
|
83792
83987
|
return res.Payload ?? "";
|
|
83793
83988
|
}
|
|
83989
|
+
async function serverlessInfo(config6, environment) {
|
|
83990
|
+
const ctx = resolveContext(config6, environment);
|
|
83991
|
+
const cf = new CloudFormationClient(ctx.region);
|
|
83992
|
+
const lambda2 = new LambdaClient(ctx.region);
|
|
83993
|
+
const s32 = new S3Client2(ctx.region);
|
|
83994
|
+
let stackStatus = "not deployed";
|
|
83995
|
+
const outputs = {};
|
|
83996
|
+
try {
|
|
83997
|
+
const { Stacks } = await cf.describeStacks({ stackName: ctx.stackName });
|
|
83998
|
+
stackStatus = Stacks?.[0]?.StackStatus ?? stackStatus;
|
|
83999
|
+
for (const o of Stacks?.[0]?.Outputs ?? [])
|
|
84000
|
+
if (o.OutputKey)
|
|
84001
|
+
outputs[o.OutputKey] = o.OutputValue ?? "";
|
|
84002
|
+
} catch {}
|
|
84003
|
+
const queues = resolveQueueNames(ctx.app, ctx.slug, environment);
|
|
84004
|
+
const pc = ctx.app.provisionedConcurrency ?? 0;
|
|
84005
|
+
const modes = ["http", "cli", ...queues.length ? ["queue"] : []];
|
|
84006
|
+
const base = `${ctx.slug}-${environment}`;
|
|
84007
|
+
const functions = [];
|
|
84008
|
+
for (const mode of modes) {
|
|
84009
|
+
const name = `${base}-${mode}`;
|
|
84010
|
+
const info2 = { mode, name, version: "$LATEST" };
|
|
84011
|
+
if (pc > 0) {
|
|
84012
|
+
const alias = await lambda2.getAlias(name, "live").catch(() => null);
|
|
84013
|
+
if (alias?.FunctionVersion) {
|
|
84014
|
+
info2.version = alias.FunctionVersion;
|
|
84015
|
+
const pcc = await lambda2.getProvisionedConcurrencyConfig(name, "live").catch(() => null);
|
|
84016
|
+
if (pcc)
|
|
84017
|
+
info2.provisioned = { status: pcc.Status ?? "-", allocated: pcc.AllocatedProvisionedConcurrentExecutions ?? 0, requested: pcc.RequestedProvisionedConcurrentExecutions ?? 0 };
|
|
84018
|
+
}
|
|
84019
|
+
}
|
|
84020
|
+
functions.push(info2);
|
|
84021
|
+
}
|
|
84022
|
+
const release = await readRelease(s32, ctx.artifactBucket, ctx.slug, environment).catch(() => null);
|
|
84023
|
+
return {
|
|
84024
|
+
slug: ctx.slug,
|
|
84025
|
+
environment,
|
|
84026
|
+
region: ctx.region,
|
|
84027
|
+
stackStatus,
|
|
84028
|
+
endpoint: outputs.HttpApiEndpoint,
|
|
84029
|
+
assetUrl: release?.assetUrl,
|
|
84030
|
+
scheduler: ctx.app.scheduler ?? "on",
|
|
84031
|
+
queues,
|
|
84032
|
+
provisionedConcurrency: pc,
|
|
84033
|
+
functions,
|
|
84034
|
+
lastRelease: release ? { sha: release.sha, timestamp: release.timestamp } : undefined
|
|
84035
|
+
};
|
|
84036
|
+
}
|
|
83794
84037
|
|
|
83795
84038
|
// src/deploy/index.ts
|
|
83796
84039
|
await init_serverless_image();
|
|
@@ -83802,18 +84045,496 @@ await init_dist2();
|
|
|
83802
84045
|
// src/aws/cloudwatch.ts
|
|
83803
84046
|
init_client();
|
|
83804
84047
|
|
|
84048
|
+
class CloudWatchClient {
|
|
84049
|
+
client;
|
|
84050
|
+
region;
|
|
84051
|
+
constructor(region = "us-east-1") {
|
|
84052
|
+
this.region = region;
|
|
84053
|
+
this.client = new AWSClient;
|
|
84054
|
+
}
|
|
84055
|
+
async query(params) {
|
|
84056
|
+
const body = new URLSearchParams(buildQueryParams({ Version: "2010-08-01", ...params })).toString();
|
|
84057
|
+
return this.client.request({
|
|
84058
|
+
service: "monitoring",
|
|
84059
|
+
region: this.region,
|
|
84060
|
+
method: "POST",
|
|
84061
|
+
path: "/",
|
|
84062
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
84063
|
+
body
|
|
84064
|
+
});
|
|
84065
|
+
}
|
|
84066
|
+
async getMetricStatistics(options) {
|
|
84067
|
+
const params = {
|
|
84068
|
+
Action: "GetMetricStatistics",
|
|
84069
|
+
Namespace: options.Namespace,
|
|
84070
|
+
MetricName: options.MetricName,
|
|
84071
|
+
StartTime: options.StartTime.toISOString(),
|
|
84072
|
+
EndTime: options.EndTime.toISOString(),
|
|
84073
|
+
Period: options.Period
|
|
84074
|
+
};
|
|
84075
|
+
(options.Statistics ?? []).forEach((s, i) => {
|
|
84076
|
+
params[`Statistics.member.${i + 1}`] = s;
|
|
84077
|
+
});
|
|
84078
|
+
(options.ExtendedStatistics ?? []).forEach((s, i) => {
|
|
84079
|
+
params[`ExtendedStatistics.member.${i + 1}`] = s;
|
|
84080
|
+
});
|
|
84081
|
+
(options.Dimensions ?? []).forEach((d, i) => {
|
|
84082
|
+
params[`Dimensions.member.${i + 1}.Name`] = d.Name;
|
|
84083
|
+
params[`Dimensions.member.${i + 1}.Value`] = d.Value;
|
|
84084
|
+
});
|
|
84085
|
+
const result = await this.query(params);
|
|
84086
|
+
const response = result.GetMetricStatisticsResult || result;
|
|
84087
|
+
let points = response?.Datapoints?.member ?? [];
|
|
84088
|
+
if (!Array.isArray(points))
|
|
84089
|
+
points = points ? [points] : [];
|
|
84090
|
+
return points.map((p) => {
|
|
84091
|
+
const percentiles = {};
|
|
84092
|
+
let entries = p.ExtendedStatistics?.entry;
|
|
84093
|
+
if (entries) {
|
|
84094
|
+
if (!Array.isArray(entries))
|
|
84095
|
+
entries = [entries];
|
|
84096
|
+
for (const e of entries)
|
|
84097
|
+
if (e?.key != null)
|
|
84098
|
+
percentiles[e.key] = Number(e.value);
|
|
84099
|
+
}
|
|
84100
|
+
return {
|
|
84101
|
+
Timestamp: p.Timestamp,
|
|
84102
|
+
Sum: p.Sum != null ? Number(p.Sum) : undefined,
|
|
84103
|
+
Average: p.Average != null ? Number(p.Average) : undefined,
|
|
84104
|
+
Maximum: p.Maximum != null ? Number(p.Maximum) : undefined,
|
|
84105
|
+
Minimum: p.Minimum != null ? Number(p.Minimum) : undefined,
|
|
84106
|
+
Percentiles: Object.keys(percentiles).length ? percentiles : undefined,
|
|
84107
|
+
Unit: p.Unit
|
|
84108
|
+
};
|
|
84109
|
+
});
|
|
84110
|
+
}
|
|
84111
|
+
async getMetricSeries(options) {
|
|
84112
|
+
const isPct = /^p\d/.test(options.Stat);
|
|
84113
|
+
const pts = await this.getMetricStatistics({
|
|
84114
|
+
Namespace: options.Namespace,
|
|
84115
|
+
MetricName: options.MetricName,
|
|
84116
|
+
Dimensions: options.Dimensions,
|
|
84117
|
+
StartTime: options.StartTime,
|
|
84118
|
+
EndTime: options.EndTime,
|
|
84119
|
+
Period: options.Period,
|
|
84120
|
+
Statistics: isPct ? [] : [options.Stat],
|
|
84121
|
+
ExtendedStatistics: isPct ? [options.Stat] : []
|
|
84122
|
+
});
|
|
84123
|
+
return pts.slice().sort((a, b) => new Date(a.Timestamp ?? 0).getTime() - new Date(b.Timestamp ?? 0).getTime()).map((p) => isPct ? p.Percentiles?.[options.Stat] ?? 0 : p[options.Stat] ?? 0);
|
|
84124
|
+
}
|
|
84125
|
+
async describeAlarms(options) {
|
|
84126
|
+
const params = { Action: "DescribeAlarms" };
|
|
84127
|
+
if (options?.AlarmNamePrefix)
|
|
84128
|
+
params.AlarmNamePrefix = options.AlarmNamePrefix;
|
|
84129
|
+
if (options?.MaxRecords)
|
|
84130
|
+
params.MaxRecords = options.MaxRecords;
|
|
84131
|
+
const result = await this.query(params);
|
|
84132
|
+
const response = result.DescribeAlarmsResult || result;
|
|
84133
|
+
let alarms = response?.MetricAlarms?.member ?? [];
|
|
84134
|
+
if (!Array.isArray(alarms))
|
|
84135
|
+
alarms = alarms ? [alarms] : [];
|
|
84136
|
+
return alarms.map((a) => ({
|
|
84137
|
+
AlarmName: a.AlarmName,
|
|
84138
|
+
StateValue: a.StateValue,
|
|
84139
|
+
MetricName: a.MetricName,
|
|
84140
|
+
ComparisonOperator: a.ComparisonOperator,
|
|
84141
|
+
Threshold: a.Threshold != null ? Number(a.Threshold) : undefined,
|
|
84142
|
+
Namespace: a.Namespace
|
|
84143
|
+
}));
|
|
84144
|
+
}
|
|
84145
|
+
async putMetricAlarm(options) {
|
|
84146
|
+
const params = {
|
|
84147
|
+
Action: "PutMetricAlarm",
|
|
84148
|
+
AlarmName: options.AlarmName,
|
|
84149
|
+
Namespace: options.Namespace,
|
|
84150
|
+
MetricName: options.MetricName,
|
|
84151
|
+
ComparisonOperator: options.ComparisonOperator,
|
|
84152
|
+
Threshold: options.Threshold,
|
|
84153
|
+
EvaluationPeriods: options.EvaluationPeriods,
|
|
84154
|
+
Period: options.Period,
|
|
84155
|
+
Statistic: options.Statistic
|
|
84156
|
+
};
|
|
84157
|
+
if (options.AlarmDescription)
|
|
84158
|
+
params.AlarmDescription = options.AlarmDescription;
|
|
84159
|
+
(options.Dimensions ?? []).forEach((d, i) => {
|
|
84160
|
+
params[`Dimensions.member.${i + 1}.Name`] = d.Name;
|
|
84161
|
+
params[`Dimensions.member.${i + 1}.Value`] = d.Value;
|
|
84162
|
+
});
|
|
84163
|
+
(options.AlarmActions ?? []).forEach((a, i) => {
|
|
84164
|
+
params[`AlarmActions.member.${i + 1}`] = a;
|
|
84165
|
+
});
|
|
84166
|
+
await this.query(params);
|
|
84167
|
+
}
|
|
84168
|
+
}
|
|
84169
|
+
|
|
83805
84170
|
// src/deploy/dashboard-data.ts
|
|
83806
84171
|
init_s3();
|
|
83807
84172
|
|
|
83808
84173
|
// src/aws/wafv2.ts
|
|
83809
84174
|
init_client();
|
|
84175
|
+
|
|
84176
|
+
class WAFv2Client {
|
|
84177
|
+
client;
|
|
84178
|
+
region;
|
|
84179
|
+
constructor(region = "us-east-1") {
|
|
84180
|
+
this.region = region;
|
|
84181
|
+
this.client = new AWSClient;
|
|
84182
|
+
}
|
|
84183
|
+
async call(target, body) {
|
|
84184
|
+
return this.client.request({
|
|
84185
|
+
service: "wafv2",
|
|
84186
|
+
region: this.region,
|
|
84187
|
+
method: "POST",
|
|
84188
|
+
path: "/",
|
|
84189
|
+
headers: { "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": `AWSWAF_20190729.${target}` },
|
|
84190
|
+
body: JSON.stringify(body)
|
|
84191
|
+
});
|
|
84192
|
+
}
|
|
84193
|
+
async listWebACLs(scope = "REGIONAL") {
|
|
84194
|
+
const r = await this.call("ListWebACLs", { Scope: scope, Limit: 100 });
|
|
84195
|
+
return r?.WebACLs ?? [];
|
|
84196
|
+
}
|
|
84197
|
+
async getWebACLRules(name, id, scope = "REGIONAL") {
|
|
84198
|
+
const r = await this.call("GetWebACL", { Name: name, Id: id, Scope: scope });
|
|
84199
|
+
const rules = r?.WebACL?.Rules ?? [];
|
|
84200
|
+
return rules.map((rule) => ({
|
|
84201
|
+
Name: rule.Name,
|
|
84202
|
+
Priority: rule.Priority,
|
|
84203
|
+
Action: rule.Action ? Object.keys(rule.Action)[0] : rule.OverrideAction ? Object.keys(rule.OverrideAction)[0] : "count"
|
|
84204
|
+
}));
|
|
84205
|
+
}
|
|
84206
|
+
}
|
|
84207
|
+
|
|
84208
|
+
// src/deploy/dashboard-data.ts
|
|
84209
|
+
function pad(n) {
|
|
84210
|
+
return String(n).padStart(2, "0");
|
|
84211
|
+
}
|
|
84212
|
+
function isoDate(d) {
|
|
84213
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
84214
|
+
}
|
|
84215
|
+
function relativeLogTime(ms) {
|
|
84216
|
+
const seconds = Math.max(0, Math.round((Date.now() - ms) / 1000));
|
|
84217
|
+
if (seconds < 60)
|
|
84218
|
+
return `${seconds}s ago`;
|
|
84219
|
+
const minutes = Math.round(seconds / 60);
|
|
84220
|
+
if (minutes < 60)
|
|
84221
|
+
return `${minutes}m ago`;
|
|
84222
|
+
const hours = Math.round(minutes / 60);
|
|
84223
|
+
if (hours < 48)
|
|
84224
|
+
return `${hours}h ago`;
|
|
84225
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
84226
|
+
}
|
|
84227
|
+
async function lambdaMetric(cw, fn, name, kind, start, end) {
|
|
84228
|
+
const pts = await cw.getMetricStatistics({
|
|
84229
|
+
Namespace: "AWS/Lambda",
|
|
84230
|
+
MetricName: name,
|
|
84231
|
+
Dimensions: [{ Name: "FunctionName", Value: fn }],
|
|
84232
|
+
StartTime: start,
|
|
84233
|
+
EndTime: end,
|
|
84234
|
+
Period: 86400,
|
|
84235
|
+
Statistics: [kind]
|
|
84236
|
+
}).catch(() => []);
|
|
84237
|
+
if (!pts.length)
|
|
84238
|
+
return 0;
|
|
84239
|
+
if (kind === "Sum")
|
|
84240
|
+
return pts.reduce((n, p) => n + (p.Sum ?? 0), 0);
|
|
84241
|
+
if (kind === "Maximum")
|
|
84242
|
+
return Math.max(...pts.map((p) => p.Maximum ?? 0));
|
|
84243
|
+
return pts.reduce((n, p) => n + (p.Average ?? 0), 0) / pts.length;
|
|
84244
|
+
}
|
|
84245
|
+
async function cwMetric(cw, namespace, name, dims, kind, start, end) {
|
|
84246
|
+
const pts = await cw.getMetricStatistics({ Namespace: namespace, MetricName: name, Dimensions: dims, StartTime: start, EndTime: end, Period: 86400, Statistics: [kind] }).catch(() => []);
|
|
84247
|
+
if (!pts.length)
|
|
84248
|
+
return 0;
|
|
84249
|
+
if (kind === "Sum")
|
|
84250
|
+
return pts.reduce((n, p) => n + (p.Sum ?? 0), 0);
|
|
84251
|
+
if (kind === "Maximum")
|
|
84252
|
+
return Math.max(...pts.map((p) => p.Maximum ?? 0));
|
|
84253
|
+
return pts.reduce((n, p) => n + (p.Average ?? 0), 0) / pts.length;
|
|
84254
|
+
}
|
|
84255
|
+
async function durationPct(cw, fn, p, start, end) {
|
|
84256
|
+
const pts = await cw.getMetricStatistics({
|
|
84257
|
+
Namespace: "AWS/Lambda",
|
|
84258
|
+
MetricName: "Duration",
|
|
84259
|
+
Dimensions: [{ Name: "FunctionName", Value: fn }],
|
|
84260
|
+
StartTime: start,
|
|
84261
|
+
EndTime: end,
|
|
84262
|
+
Period: 86400,
|
|
84263
|
+
ExtendedStatistics: [p]
|
|
84264
|
+
}).catch(() => []);
|
|
84265
|
+
return Math.round(pts.reduce((n, x) => n + (x.Percentiles?.[p] ?? 0), 0));
|
|
84266
|
+
}
|
|
84267
|
+
async function resolveDashboardData(config6, environment) {
|
|
84268
|
+
const info2 = await serverlessInfo(config6, environment);
|
|
84269
|
+
const region = info2.region;
|
|
84270
|
+
const cw = new CloudWatchClient(region);
|
|
84271
|
+
const lambda2 = new LambdaClient(region);
|
|
84272
|
+
const logs = new CloudWatchLogsClient(region);
|
|
84273
|
+
const end = new Date;
|
|
84274
|
+
const start = new Date(end.getTime() - 86400000);
|
|
84275
|
+
const app = config6.environments?.[environment]?.app;
|
|
84276
|
+
const out = {};
|
|
84277
|
+
out.app = {
|
|
84278
|
+
name: info2.slug,
|
|
84279
|
+
env: environment,
|
|
84280
|
+
region,
|
|
84281
|
+
runtime: app?.kind === "php" ? `php-${app?.phpVersion ?? "8.3"} (fpm)` : app?.kind ?? "node",
|
|
84282
|
+
url: info2.endpoint ?? "",
|
|
84283
|
+
build: info2.lastRelease?.sha?.slice(0, 7) ?? "—",
|
|
84284
|
+
deployedAt: info2.lastRelease?.timestamp ?? "—"
|
|
84285
|
+
};
|
|
84286
|
+
out.maintenance = { enabled: false };
|
|
84287
|
+
out.scheduler = { enabled: info2.scheduler !== "off", expression: info2.scheduler === "sub-minute" ? "rate(1 minute) · sub-minute" : "rate(1 minute)", lastRun: "—" };
|
|
84288
|
+
let totalInvocations = 0, totalErrors = 0, maxDur = 0;
|
|
84289
|
+
const fns = [];
|
|
84290
|
+
const fnsDetail = [];
|
|
84291
|
+
const serverlessLogs = [];
|
|
84292
|
+
for (const f of info2.functions) {
|
|
84293
|
+
const [invocations, errors, throttles, durAvg, cfg, p50, p95, p99, spark] = await Promise.all([
|
|
84294
|
+
lambdaMetric(cw, f.name, "Invocations", "Sum", start, end),
|
|
84295
|
+
lambdaMetric(cw, f.name, "Errors", "Sum", start, end),
|
|
84296
|
+
lambdaMetric(cw, f.name, "Throttles", "Sum", start, end),
|
|
84297
|
+
lambdaMetric(cw, f.name, "Duration", "Average", start, end),
|
|
84298
|
+
lambda2.getFunction(f.name).catch(() => null),
|
|
84299
|
+
durationPct(cw, f.name, "p50", start, end),
|
|
84300
|
+
durationPct(cw, f.name, "p95", start, end),
|
|
84301
|
+
durationPct(cw, f.name, "p99", start, end),
|
|
84302
|
+
cw.getMetricSeries({ Namespace: "AWS/Lambda", MetricName: "Invocations", Dimensions: [{ Name: "FunctionName", Value: f.name }], StartTime: new Date(end.getTime() - 12 * 3600000), EndTime: end, Period: 3600, Stat: "Sum" }).catch(() => [])
|
|
84303
|
+
]);
|
|
84304
|
+
totalInvocations += invocations;
|
|
84305
|
+
totalErrors += errors;
|
|
84306
|
+
maxDur = Math.max(maxDur, durAvg);
|
|
84307
|
+
const memory = cfg?.Configuration?.MemorySize ?? 0;
|
|
84308
|
+
const env2 = Object.keys(cfg?.Configuration?.Environment?.Variables ?? {});
|
|
84309
|
+
let recent = [];
|
|
84310
|
+
let maxMem = 0;
|
|
84311
|
+
try {
|
|
84312
|
+
const { events = [] } = await logs.filterLogEvents({ logGroupName: `/aws/lambda/${f.name}`, startTime: start.getTime(), limit: 120 });
|
|
84313
|
+
for (const e of events) {
|
|
84314
|
+
const msg = (e.message ?? "").trim();
|
|
84315
|
+
const mm = /Max Memory Used:\s*(\d+)\s*MB/.exec(msg);
|
|
84316
|
+
if (mm)
|
|
84317
|
+
maxMem = Math.max(maxMem, Number(mm[1]));
|
|
84318
|
+
if (msg && !/^(?:START|END|REPORT|INIT_START)/.test(msg)) {
|
|
84319
|
+
serverlessLogs.push({
|
|
84320
|
+
source: f.mode || f.name,
|
|
84321
|
+
timestamp: new Date(e.timestamp ?? 0).toISOString(),
|
|
84322
|
+
when: relativeLogTime(e.timestamp ?? 0),
|
|
84323
|
+
message: msg.slice(0, 400),
|
|
84324
|
+
level: /error|exception|fatal|panic|traceback/i.test(msg) ? "error" : /warn|warning|timeout|retr(?:y|ies)|throttl/i.test(msg) ? "warn" : "info"
|
|
84325
|
+
});
|
|
84326
|
+
}
|
|
84327
|
+
}
|
|
84328
|
+
recent = events.filter((e) => !/^(?:START|END|REPORT|INIT_START)/.test((e.message ?? "").trim())).slice(-6).map((e) => ({ ts: new Date(e.timestamp ?? 0).toISOString().slice(11, 19), msg: (e.message ?? "").trim().slice(0, 120), err: /error|exception|fatal/i.test(e.message ?? "") }));
|
|
84329
|
+
} catch {}
|
|
84330
|
+
const maxSpark = Math.max(1, ...spark);
|
|
84331
|
+
fns.push({ key: f.mode, name: f.name, version: f.version, memory, timeout: cfg?.Configuration?.Timeout ?? 0, runtime: cfg?.Configuration?.Runtime ?? "provided.al2023", invocations: Math.round(invocations), errors: Math.round(errors), p95: p95 || Math.round(durAvg), status: "active", provisioned: f.provisioned ? `${f.provisioned.allocated}/${f.provisioned.requested}` : undefined });
|
|
84332
|
+
fnsDetail.push({
|
|
84333
|
+
key: f.mode,
|
|
84334
|
+
name: f.name,
|
|
84335
|
+
runtime: cfg?.Configuration?.Runtime ?? "provided.al2023",
|
|
84336
|
+
arch: cfg?.Configuration?.Architectures?.[0] ?? "x86_64",
|
|
84337
|
+
memory,
|
|
84338
|
+
timeout: cfg?.Configuration?.Timeout ?? 0,
|
|
84339
|
+
ephemeral: cfg?.Configuration?.EphemeralStorage?.Size ?? 512,
|
|
84340
|
+
version: f.version,
|
|
84341
|
+
concurrency: app?.concurrency ?? "—",
|
|
84342
|
+
invocations: Math.round(invocations),
|
|
84343
|
+
errors: Math.round(errors),
|
|
84344
|
+
throttles: Math.round(throttles),
|
|
84345
|
+
p50: p50 || Math.round(durAvg),
|
|
84346
|
+
p95: p95 || Math.round(durAvg),
|
|
84347
|
+
p99: p99 || Math.round(durAvg),
|
|
84348
|
+
maxMem,
|
|
84349
|
+
spark: spark.length ? spark.map((v) => Math.round(v / maxSpark * 100)) : [0],
|
|
84350
|
+
env: env2.length ? env2 : ["(none)"],
|
|
84351
|
+
recent: recent.length ? recent : [{ ts: "—", msg: "no recent events", err: false }]
|
|
84352
|
+
});
|
|
84353
|
+
}
|
|
84354
|
+
out.functions = fns;
|
|
84355
|
+
out.functionsDetail = fnsDetail;
|
|
84356
|
+
out.serverlessLogs = serverlessLogs.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp))).slice(0, 600);
|
|
84357
|
+
out.serverlessLogsEmptyReason = serverlessLogs.length ? undefined : "No recent function logs were found in CloudWatch for this environment.";
|
|
84358
|
+
const errorRatePct = totalInvocations ? Number((totalErrors / totalInvocations * 100).toFixed(2)) : 0;
|
|
84359
|
+
const concurrency = Math.round(await lambdaMetric(cw, info2.functions[0]?.name ?? "", "ConcurrentExecutions", "Maximum", start, end));
|
|
84360
|
+
out.metrics = { invocations: Math.round(totalInvocations), errorRatePct, p95Ms: Math.round(maxDur), coldStartPct: 0, concurrency, estCostUsd: 0 };
|
|
84361
|
+
try {
|
|
84362
|
+
const sqs2 = new SQSClient(region);
|
|
84363
|
+
const overview = [];
|
|
84364
|
+
const detail = [];
|
|
84365
|
+
let dlqDepth = 0;
|
|
84366
|
+
try {
|
|
84367
|
+
const { QueueUrl } = await sqs2.getQueueUrl(`${info2.slug}-${environment}-dlq`);
|
|
84368
|
+
const { Attributes } = await sqs2.getQueueAttributes(QueueUrl);
|
|
84369
|
+
dlqDepth = Number(Attributes.ApproximateNumberOfMessages ?? 0);
|
|
84370
|
+
} catch {}
|
|
84371
|
+
const resolvedQueues = app ? resolveQueues(app, info2.slug, environment) : [];
|
|
84372
|
+
for (const q of resolvedQueues) {
|
|
84373
|
+
const short = q.name.replace(`${info2.slug}-${environment}-`, "");
|
|
84374
|
+
let visible = 0, inFlight = 0;
|
|
84375
|
+
try {
|
|
84376
|
+
const { QueueUrl } = await sqs2.getQueueUrl(q.name);
|
|
84377
|
+
const { Attributes } = await sqs2.getQueueAttributes(QueueUrl);
|
|
84378
|
+
visible = Number(Attributes.ApproximateNumberOfMessages ?? 0);
|
|
84379
|
+
inFlight = Number(Attributes.ApproximateNumberOfMessagesNotVisible ?? 0);
|
|
84380
|
+
} catch {}
|
|
84381
|
+
const processed = Math.round(await cwMetric(cw, "AWS/SQS", "NumberOfMessagesDeleted", [{ Name: "QueueName", Value: q.name }], "Sum", start, end));
|
|
84382
|
+
overview.push({ name: short, visible, inFlight, processed, dlq: dlqDepth });
|
|
84383
|
+
detail.push({ name: q.name, short, visible, inFlight, delayed: 0, processed, oldestSec: 0, concurrency: q.concurrency ?? app?.queueConcurrency ?? "—", tries: app?.queueTries ?? 3, visTimeout: (app?.queueTimeout ?? 120) * 6, dlq: dlqDepth });
|
|
84384
|
+
}
|
|
84385
|
+
if (overview.length)
|
|
84386
|
+
out.queues = overview;
|
|
84387
|
+
if (detail.length)
|
|
84388
|
+
out.queuesDetail = detail;
|
|
84389
|
+
out.dlqItems = [];
|
|
84390
|
+
} catch {}
|
|
84391
|
+
try {
|
|
84392
|
+
const cf = new CloudFormationClient(region);
|
|
84393
|
+
const { Stacks } = await cf.describeStacks({ stackName: resolveServerlessAppStackName(config6, environment) });
|
|
84394
|
+
const outputs = {};
|
|
84395
|
+
for (const o of Stacks?.[0]?.Outputs ?? [])
|
|
84396
|
+
if (o.OutputKey)
|
|
84397
|
+
outputs[o.OutputKey] = o.OutputValue ?? "";
|
|
84398
|
+
if (app?.database?.connection === "aurora-serverless") {
|
|
84399
|
+
const clusterId = `${info2.slug}-${environment}-db`;
|
|
84400
|
+
const dim4 = [{ Name: "DBClusterIdentifier", Value: clusterId }];
|
|
84401
|
+
const [acu, conns] = await Promise.all([
|
|
84402
|
+
cwMetric(cw, "AWS/RDS", "ServerlessDatabaseCapacity", dim4, "Average", start, end),
|
|
84403
|
+
cwMetric(cw, "AWS/RDS", "DatabaseConnections", dim4, "Maximum", start, end)
|
|
84404
|
+
]);
|
|
84405
|
+
out.aurora = { id: clusterId, engine: "aurora-mysql", minAcu: app?.database?.minCapacity ?? 0.5, maxAcu: app?.database?.maxCapacity ?? 4, currentAcu: acu ? Number(acu.toFixed(1)) : app?.database?.minCapacity ?? 0.5, database: "app", connections: Math.round(conns), status: outputs.DbEndpoint ? "available" : "creating" };
|
|
84406
|
+
if (app?.rdsProxy)
|
|
84407
|
+
out.proxy = { name: `${info2.slug}-${environment}-proxy`, endpoint: outputs.DbProxyEndpoint ?? "—", pooledConns: Math.round(conns), status: "available" };
|
|
84408
|
+
}
|
|
84409
|
+
if (app?.cache?.driver === "elasticache") {
|
|
84410
|
+
const rgId = `${info2.slug}-${environment}-cache`;
|
|
84411
|
+
const dim4 = [{ Name: "CacheClusterId", Value: `${rgId}-001` }];
|
|
84412
|
+
const [hits, misses] = await Promise.all([
|
|
84413
|
+
cwMetric(cw, "AWS/ElastiCache", "CacheHits", dim4, "Sum", start, end),
|
|
84414
|
+
cwMetric(cw, "AWS/ElastiCache", "CacheMisses", dim4, "Sum", start, end)
|
|
84415
|
+
]);
|
|
84416
|
+
const hitRate = hits + misses > 0 ? Number((hits / (hits + misses) * 100).toFixed(1)) : 0;
|
|
84417
|
+
out.redis = { id: rgId, node: "cache.t4g.micro", engine: "redis", hitRate, status: outputs.CacheEndpoint ? "available" : "creating" };
|
|
84418
|
+
}
|
|
84419
|
+
if (app?.efs) {
|
|
84420
|
+
let sizeMb = 0;
|
|
84421
|
+
try {
|
|
84422
|
+
if (outputs.EfsFileSystemId) {
|
|
84423
|
+
const efs2 = new EFSClient(region);
|
|
84424
|
+
const { FileSystems } = await efs2.describeFileSystems({ FileSystemId: outputs.EfsFileSystemId });
|
|
84425
|
+
sizeMb = Number(((FileSystems?.[0]?.SizeInBytes?.Value ?? 0) / 1048576).toFixed(1));
|
|
84426
|
+
}
|
|
84427
|
+
} catch {}
|
|
84428
|
+
out.efs = { id: outputs.EfsFileSystemId ?? "efs", mount: "/mnt/local", sizeMb, status: "available" };
|
|
84429
|
+
}
|
|
84430
|
+
out.assetsInfo = outputs.AssetsCdnDomain ? { bucket: outputs.AssetsBucketName ?? `${info2.slug}-${environment}-assets`, cdn: outputs.AssetsCdnDomain, customDomain: app?.assetDomain ?? "—", assetUrl: info2.assetUrl ?? "—", files: 0, sizeMb: 0, cacheHitPct: 0, build: out.app.build } : undefined;
|
|
84431
|
+
} catch {}
|
|
84432
|
+
if (out.assetsInfo) {
|
|
84433
|
+
try {
|
|
84434
|
+
const s32 = new S3Client2(region);
|
|
84435
|
+
const objs = await s32.list({ bucket: out.assetsInfo.bucket, maxKeys: 1000 });
|
|
84436
|
+
out.assetsInfo.files = objs.length;
|
|
84437
|
+
out.assetsInfo.sizeMb = Number((objs.reduce((n, o) => n + (o.Size ?? 0), 0) / 1048576).toFixed(1));
|
|
84438
|
+
out.assetsRecent = objs.slice(0, 8).map((o) => ({ path: `/${o.Key}`, size: `${Math.max(1, Math.round((o.Size ?? 0) / 1024))} KB`, type: o.Key.split(".").pop() ?? "" }));
|
|
84439
|
+
} catch {}
|
|
84440
|
+
}
|
|
84441
|
+
try {
|
|
84442
|
+
if (app?.secrets) {
|
|
84443
|
+
const list = Array.isArray(app.secrets) ? app.secrets.map((s) => ({ key: s.split("/").pop().toUpperCase().replace(/[^A-Z0-9_]/g, "_"), source: s })) : Object.entries(app.secrets).map(([k, v]) => ({ key: k, source: String(v) }));
|
|
84444
|
+
out.secretsList = list.map((s) => ({ ...s, updated: "—" }));
|
|
84445
|
+
}
|
|
84446
|
+
} catch {}
|
|
84447
|
+
try {
|
|
84448
|
+
const s32 = new S3Client2(region);
|
|
84449
|
+
const bucket = `${info2.slug}-${environment}-deployments`;
|
|
84450
|
+
const hist = await s32.getObjectJson(bucket, `deployments/${info2.slug}/${environment}/history.json`).catch(() => null);
|
|
84451
|
+
if (hist?.records?.length) {
|
|
84452
|
+
out.deploymentsDetail = hist.records.slice(-12).reverse().map((r) => ({ sha: (r.sha ?? "").slice(0, 7), status: r.status ?? "success", when: r.timestamp ?? "—", took: r.took ?? "—", by: r.by ?? "—", version: r.version ?? "—", hooks: r.hooks ?? [] }));
|
|
84453
|
+
}
|
|
84454
|
+
} catch {}
|
|
84455
|
+
out.metricsTotals = { invocations: out.metrics.invocations, errors: Math.round(totalErrors), errorRatePct, p95Ms: out.metrics.p95Ms, throttles: fnsDetail.reduce((n, f) => n + f.throttles, 0), coldStartPct: 0, estCostUsd: 0, concurrency };
|
|
84456
|
+
out.metricsPerFn = fnsDetail.map((f) => ({ key: f.key, invocations: f.invocations, errors: f.errors, throttles: f.throttles, avgMs: f.p50, maxMs: f.p99, costUsd: 0 }));
|
|
84457
|
+
try {
|
|
84458
|
+
out.invSpark = await cw.getMetricSeries({ Namespace: "AWS/Lambda", MetricName: "Invocations", Dimensions: [{ Name: "FunctionName", Value: info2.functions.find((f) => f.mode === "http")?.name ?? "" }], StartTime: new Date(end.getTime() - 24 * 3600000), EndTime: end, Period: 3600, Stat: "Sum" });
|
|
84459
|
+
} catch {}
|
|
84460
|
+
try {
|
|
84461
|
+
const ce = new CostExplorerClient;
|
|
84462
|
+
const now = end;
|
|
84463
|
+
const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
84464
|
+
const prevStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
|
|
84465
|
+
const [thisMonth, lastMonth] = await Promise.all([
|
|
84466
|
+
ce.getCostByService({ start: isoDate(monthStart), end: isoDate(now), granularity: "MONTHLY" }).catch(() => []),
|
|
84467
|
+
ce.getCostByService({ start: isoDate(prevStart), end: isoDate(monthStart), granularity: "MONTHLY" }).catch(() => [])
|
|
84468
|
+
]);
|
|
84469
|
+
if (thisMonth.length) {
|
|
84470
|
+
const mtd = thisMonth.reduce((n, s) => n + s.amount, 0);
|
|
84471
|
+
const lastTotal = lastMonth.reduce((n, s) => n + s.amount, 0);
|
|
84472
|
+
const dayOfMonth = now.getUTCDate();
|
|
84473
|
+
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
|
|
84474
|
+
out.costSummary = { monthToDateUsd: Number(mtd.toFixed(2)), projectedUsd: Number((mtd / dayOfMonth * daysInMonth).toFixed(2)), lastMonthUsd: Number(lastTotal.toFixed(2)), dailyAvgUsd: Number((mtd / dayOfMonth).toFixed(2)) };
|
|
84475
|
+
out.costServices = thisMonth.filter((s) => s.amount > 0).sort((a, b) => b.amount - a.amount).slice(0, 12).map((s) => ({ name: s.service, usd: Number(s.amount.toFixed(2)), note: "" }));
|
|
84476
|
+
out.metrics.estCostUsd = Number((mtd / dayOfMonth).toFixed(2));
|
|
84477
|
+
out.metricsTotals.estCostUsd = out.metrics.estCostUsd;
|
|
84478
|
+
out.costTrend = await ce.getDailyTotals({ start: isoDate(monthStart), end: isoDate(now) }).catch(() => []);
|
|
84479
|
+
const lambdaCost = thisMonth.find((s) => /lambda/i.test(s.service))?.amount ?? 0;
|
|
84480
|
+
const gbSec = (f) => f.invocations * (f.p50 / 1000) * (f.memory / 1024);
|
|
84481
|
+
const totalGbSec = fnsDetail.reduce((n, f) => n + gbSec(f), 0) || 1;
|
|
84482
|
+
out.costPerFn = fnsDetail.map((f) => ({ key: f.key, requests: f.invocations, gbSec: Math.round(gbSec(f)), usd: Number((lambdaCost * (gbSec(f) / totalGbSec)).toFixed(2)) }));
|
|
84483
|
+
}
|
|
84484
|
+
} catch {}
|
|
84485
|
+
if (app?.firewall) {
|
|
84486
|
+
try {
|
|
84487
|
+
const blocked = await cw.getMetricStatistics({ Namespace: "AWS/WAFV2", MetricName: "BlockedRequests", StartTime: start, EndTime: end, Period: 86400, Statistics: ["Sum"] }).catch(() => []);
|
|
84488
|
+
const allowed = await cw.getMetricStatistics({ Namespace: "AWS/WAFV2", MetricName: "AllowedRequests", StartTime: start, EndTime: end, Period: 86400, Statistics: ["Sum"] }).catch(() => []);
|
|
84489
|
+
out.waf = { enabled: true, scope: "REGIONAL", acl: `${info2.slug}-${environment}-waf`, allowed24h: Math.round(allowed.reduce((n, p) => n + (p.Sum ?? 0), 0)), blocked24h: Math.round(blocked.reduce((n, p) => n + (p.Sum ?? 0), 0)) };
|
|
84490
|
+
try {
|
|
84491
|
+
const waf = new WAFv2Client(region);
|
|
84492
|
+
const acls = await waf.listWebACLs("REGIONAL");
|
|
84493
|
+
const acl = acls.find((a) => a.Name?.includes(`${info2.slug}-${environment}`)) ?? acls[0];
|
|
84494
|
+
if (acl?.Name && acl.Id) {
|
|
84495
|
+
out.waf.acl = acl.Name;
|
|
84496
|
+
const rules = await waf.getWebACLRules(acl.Name, acl.Id, "REGIONAL");
|
|
84497
|
+
if (rules.length)
|
|
84498
|
+
out.wafRules = rules.map((r) => ({ name: r.Name ?? "—", detail: `priority ${r.Priority ?? 0}`, action: r.Action ?? "count", blocked24h: 0 }));
|
|
84499
|
+
}
|
|
84500
|
+
} catch {}
|
|
84501
|
+
} catch {}
|
|
84502
|
+
}
|
|
84503
|
+
if (info2.scheduler !== "off" && app?.kind === "php") {
|
|
84504
|
+
try {
|
|
84505
|
+
const out2 = await runRemoteCommand(config6, environment, "schedule:list");
|
|
84506
|
+
const tasks = [];
|
|
84507
|
+
for (const line of (out2 || "").split(`
|
|
84508
|
+
`)) {
|
|
84509
|
+
const m = /^\s*([\d*/,\-\s]+?)\s{2,}(.+?)(?:\s{2,}Next Due.*)?$/.exec(line);
|
|
84510
|
+
if (m && /[*\d]/.test(m[1]) && m[1].trim().split(/\s+/).length === 5)
|
|
84511
|
+
tasks.push({ command: m[2].trim().replace(/^php artisan /, ""), cron: m[1].trim(), desc: "", lastRun: "—", lastStatus: "ok" });
|
|
84512
|
+
}
|
|
84513
|
+
if (tasks.length)
|
|
84514
|
+
out.schedulerTasks = tasks;
|
|
84515
|
+
} catch {}
|
|
84516
|
+
}
|
|
84517
|
+
return out;
|
|
84518
|
+
}
|
|
84519
|
+
// src/deploy/local-dashboard-server.ts
|
|
84520
|
+
import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
|
|
84521
|
+
import { readFile as readFile2, writeFile as writeFile6 } from "node:fs/promises";
|
|
84522
|
+
import { mkdtempSync as mkdtempSync6 } from "node:fs";
|
|
84523
|
+
import { tmpdir as tmpdir7 } from "node:os";
|
|
84524
|
+
import { dirname as dirname9, extname as extname3, join as join23, normalize } from "node:path";
|
|
84525
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
84526
|
+
|
|
84527
|
+
// src/deploy/dashboard-data-server.ts
|
|
84528
|
+
import { existsSync as existsSync24, readFileSync as readFileSync17 } from "node:fs";
|
|
84529
|
+
import { join as join21 } from "node:path";
|
|
84530
|
+
|
|
83810
84531
|
// src/drivers/factory.ts
|
|
83811
84532
|
await init_dist2();
|
|
83812
84533
|
|
|
83813
84534
|
// src/drivers/aws/driver.ts
|
|
83814
84535
|
init_cloudformation();
|
|
83815
84536
|
await init_dist2();
|
|
83816
|
-
import { readFileSync as
|
|
84537
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
83817
84538
|
init_s3();
|
|
83818
84539
|
|
|
83819
84540
|
// src/drivers/shared/package-manager.ts
|
|
@@ -84671,6 +85392,26 @@ function buildBackupProvisionScript(options) {
|
|
|
84671
85392
|
`chmod 644 ${BACKUP_CRON_PATH}`
|
|
84672
85393
|
];
|
|
84673
85394
|
}
|
|
85395
|
+
function engineSocket(engine) {
|
|
85396
|
+
return engine === "mariadb" ? "/var/lib/pantry/mariadb/mariadbd.sock" : "/var/lib/pantry/mysql/mysqld.sock";
|
|
85397
|
+
}
|
|
85398
|
+
function buildBackupRestoreScript(database, options = {}) {
|
|
85399
|
+
if (!database?.name)
|
|
85400
|
+
return [];
|
|
85401
|
+
const name = database.name;
|
|
85402
|
+
const isPg = database.engine === "postgres";
|
|
85403
|
+
const locate = options.from ? `TS_CLOUD_DUMP="${options.from}"` : `TS_CLOUD_DUMP="$(find ${BACKUP_OUTPUT_DIR} -type f -name '*${name}*.sql' -o -type f -name '*${name}*.sql.gz' 2>/dev/null | xargs -r ls -1t 2>/dev/null | head -1)"`;
|
|
85404
|
+
const client2 = isPg ? `psql -h 127.0.0.1 -p ${database.port ?? 5432} -U postgres -d "${name}"` : `mysql --socket=${engineSocket(database.engine)} -u root "${name}"`;
|
|
85405
|
+
return [
|
|
85406
|
+
"set -uo pipefail",
|
|
85407
|
+
'eval "$(cd /opt/pantry && pantry env 2>/dev/null)" || true',
|
|
85408
|
+
locate,
|
|
85409
|
+
'[ -n "$TS_CLOUD_DUMP" ] && [ -f "$TS_CLOUD_DUMP" ] || { echo "no backup dump found to restore" >&2; exit 1; }',
|
|
85410
|
+
'echo "[ts-cloud] restoring ' + name + ' from $TS_CLOUD_DUMP"',
|
|
85411
|
+
`case "$TS_CLOUD_DUMP" in *.gz) gunzip -c "$TS_CLOUD_DUMP" ;; *) cat "$TS_CLOUD_DUMP" ;; esac | ${client2}`,
|
|
85412
|
+
'echo "restore complete"'
|
|
85413
|
+
];
|
|
85414
|
+
}
|
|
84674
85415
|
|
|
84675
85416
|
// src/drivers/shared/compute-provision.ts
|
|
84676
85417
|
function buildComputeProvisionScripts(config6) {
|
|
@@ -85050,7 +85791,7 @@ class AwsDriver {
|
|
|
85050
85791
|
await s32.putObject({
|
|
85051
85792
|
bucket,
|
|
85052
85793
|
key: options.remoteKey,
|
|
85053
|
-
body:
|
|
85794
|
+
body: readFileSync15(options.localPath),
|
|
85054
85795
|
contentType: "application/gzip"
|
|
85055
85796
|
});
|
|
85056
85797
|
return { artifactRef: `s3://${bucket}/${options.remoteKey}` };
|
|
@@ -85157,9 +85898,9 @@ class AwsDriver {
|
|
|
85157
85898
|
|
|
85158
85899
|
// src/drivers/hetzner/driver.ts
|
|
85159
85900
|
await init_dist2();
|
|
85160
|
-
import { existsSync as
|
|
85161
|
-
import { homedir as
|
|
85162
|
-
import { join as
|
|
85901
|
+
import { existsSync as existsSync20, readFileSync as readFileSync16 } from "node:fs";
|
|
85902
|
+
import { homedir as homedir8 } from "node:os";
|
|
85903
|
+
import { join as join19 } from "node:path";
|
|
85163
85904
|
import { execSync as execSync3 } from "node:child_process";
|
|
85164
85905
|
|
|
85165
85906
|
// src/drivers/hetzner/client.ts
|
|
@@ -85379,6 +86120,27 @@ runcmd:
|
|
|
85379
86120
|
|
|
85380
86121
|
// src/drivers/shared/rpx-gateway.ts
|
|
85381
86122
|
var DEFAULT_RPX_CERTS_DIR = "/etc/rpx/certs";
|
|
86123
|
+
var DEFAULT_ACME_WEBROOT = "/var/www/acme-challenge";
|
|
86124
|
+
function normalizeSiteRedirect(input) {
|
|
86125
|
+
if (typeof input === "string")
|
|
86126
|
+
return { to: input };
|
|
86127
|
+
const out = { to: input.to };
|
|
86128
|
+
if (input.status != null)
|
|
86129
|
+
out.status = input.status;
|
|
86130
|
+
if (input.preservePath != null)
|
|
86131
|
+
out.preservePath = input.preservePath;
|
|
86132
|
+
return out;
|
|
86133
|
+
}
|
|
86134
|
+
function resolveRouteAuth(site) {
|
|
86135
|
+
const auth = site.auth;
|
|
86136
|
+
if (!auth || auth.enabled === false || !auth.password)
|
|
86137
|
+
return;
|
|
86138
|
+
return {
|
|
86139
|
+
username: auth.username || "admin",
|
|
86140
|
+
password: auth.password,
|
|
86141
|
+
...auth.realm ? { realm: auth.realm } : {}
|
|
86142
|
+
};
|
|
86143
|
+
}
|
|
85382
86144
|
function normalizeRoutePath(path) {
|
|
85383
86145
|
if (!path || path === "/")
|
|
85384
86146
|
return;
|
|
@@ -85405,17 +86167,24 @@ function buildRpxConfig(sites, options) {
|
|
|
85405
86167
|
continue;
|
|
85406
86168
|
const path = normalizeRoutePath(site.path);
|
|
85407
86169
|
const id = deriveRouteId(site.domain, path);
|
|
86170
|
+
const auth = resolveRouteAuth(site);
|
|
86171
|
+
if (kind === "redirect") {
|
|
86172
|
+
proxies.push({ to: site.domain, path, redirect: normalizeSiteRedirect(site.redirect), id, ...auth ? { auth } : {} });
|
|
86173
|
+
domains.add(site.domain);
|
|
86174
|
+
continue;
|
|
86175
|
+
}
|
|
85408
86176
|
if (kind === "server-app") {
|
|
85409
86177
|
if (typeof site.port !== "number")
|
|
85410
86178
|
continue;
|
|
85411
|
-
proxies.push({ to: site.domain, path, from: `localhost:${site.port}`, id });
|
|
86179
|
+
proxies.push({ to: site.domain, path, from: `localhost:${site.port}`, id, ...auth ? { auth } : {} });
|
|
85412
86180
|
} else {
|
|
85413
86181
|
proxies.push({
|
|
85414
86182
|
to: site.domain,
|
|
85415
86183
|
path,
|
|
85416
|
-
static: `${wwwRoot}/${name}`,
|
|
86184
|
+
static: `${wwwRoot}/${name}/current`,
|
|
85417
86185
|
cleanUrls: site.pathRewriteStyle !== "flat",
|
|
85418
86186
|
spa: site.spa ?? false,
|
|
86187
|
+
...auth ? { auth } : {},
|
|
85419
86188
|
id
|
|
85420
86189
|
});
|
|
85421
86190
|
}
|
|
@@ -85441,6 +86210,9 @@ function buildRpxConfig(sites, options) {
|
|
|
85441
86210
|
certsDir
|
|
85442
86211
|
};
|
|
85443
86212
|
}
|
|
86213
|
+
if (options.proxy.onDemandTls) {
|
|
86214
|
+
config6.acmeChallengeWebroot = options.proxy.acmeWebroot ?? DEFAULT_ACME_WEBROOT;
|
|
86215
|
+
}
|
|
85444
86216
|
const cdn = options.proxy.cdn;
|
|
85445
86217
|
if (cdn?.secret && cdn.frontedHosts.length > 0) {
|
|
85446
86218
|
config6.originGuard = {
|
|
@@ -85451,20 +86223,61 @@ function buildRpxConfig(sites, options) {
|
|
|
85451
86223
|
}
|
|
85452
86224
|
return config6;
|
|
85453
86225
|
}
|
|
85454
|
-
function renderRpxLauncher(config6) {
|
|
85455
|
-
const json = JSON.stringify(config6, null, 2);
|
|
85456
|
-
return `// Generated by ts-cloud — rpx reverse-proxy gateway.
|
|
85457
|
-
// Routes are derived from the \`sites\` model on every \`buddy deploy\`.
|
|
85458
|
-
import { startProxies } from '@stacksjs/rpx'
|
|
85459
|
-
|
|
85460
|
-
const config = ${json} as const
|
|
85461
|
-
|
|
85462
|
-
await startProxies(config as any)
|
|
85463
|
-
`;
|
|
85464
|
-
}
|
|
85465
86226
|
var RPX_DIR = "/etc/rpx";
|
|
86227
|
+
var RPX_INSTALL_DIR = "/opt/rpx-gateway";
|
|
85466
86228
|
var RPX_LAUNCHER_PATH = "/etc/rpx/gateway.ts";
|
|
85467
86229
|
var RPX_SERVICE_NAME = "rpx-gateway.service";
|
|
86230
|
+
var RPX_SITES_DIR = "/etc/rpx/sites.d";
|
|
86231
|
+
function renderRpxAssembler(sitesDir = RPX_SITES_DIR, defaultCertsDir = DEFAULT_RPX_CERTS_DIR) {
|
|
86232
|
+
return `// Generated by ts-cloud — rpx gateway assembler.
|
|
86233
|
+
// Merges every app's fragment in ${sitesDir} so independent deploys compose
|
|
86234
|
+
// without clobbering each other. Each deploy writes only its own <slug>.json.
|
|
86235
|
+
import { startProxies } from '@stacksjs/rpx'
|
|
86236
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
86237
|
+
|
|
86238
|
+
const dir = ${JSON.stringify(sitesDir)}
|
|
86239
|
+
const proxies = []
|
|
86240
|
+
const seen = new Set()
|
|
86241
|
+
const suffixes = new Set()
|
|
86242
|
+
const guardHosts = new Set()
|
|
86243
|
+
let email
|
|
86244
|
+
let certsDir = ${JSON.stringify(defaultCertsDir)}
|
|
86245
|
+
let acmeChallengeWebroot
|
|
86246
|
+
let guard
|
|
86247
|
+
let files = []
|
|
86248
|
+
try { files = readdirSync(dir).filter(n => n.endsWith('.json')).sort() } catch {}
|
|
86249
|
+
for (const f of files) {
|
|
86250
|
+
let frag
|
|
86251
|
+
try { frag = JSON.parse(readFileSync(dir + '/' + f, 'utf8')) } catch { continue }
|
|
86252
|
+
for (const p of frag.proxies ?? []) {
|
|
86253
|
+
const key = p.id || (p.to + (p.path ?? ''))
|
|
86254
|
+
if (seen.has(key)) continue
|
|
86255
|
+
seen.add(key)
|
|
86256
|
+
proxies.push(p)
|
|
86257
|
+
}
|
|
86258
|
+
for (const s of frag.onDemandTls?.allowedSuffixes ?? []) suffixes.add(s)
|
|
86259
|
+
email ??= frag.onDemandTls?.email
|
|
86260
|
+
if (frag.productionCerts?.certsDir) certsDir = frag.productionCerts.certsDir
|
|
86261
|
+
acmeChallengeWebroot ??= frag.acmeChallengeWebroot
|
|
86262
|
+
if (frag.originGuard) {
|
|
86263
|
+
guard ??= { header: frag.originGuard.header, value: frag.originGuard.value }
|
|
86264
|
+
for (const h of frag.originGuard.hosts ?? []) guardHosts.add(h)
|
|
86265
|
+
}
|
|
86266
|
+
}
|
|
86267
|
+
const config = {
|
|
86268
|
+
proxies,
|
|
86269
|
+
productionCerts: { certsDir },
|
|
86270
|
+
https: true,
|
|
86271
|
+
hostsManagement: false,
|
|
86272
|
+
cleanup: { hosts: false, certs: false },
|
|
86273
|
+
...(suffixes.size > 0 ? { onDemandTls: { enabled: true, allowedSuffixes: [...suffixes], email, certsDir } } : {}),
|
|
86274
|
+
...(acmeChallengeWebroot ? { acmeChallengeWebroot } : {}),
|
|
86275
|
+
...(guard ? { originGuard: { header: guard.header, value: guard.value, hosts: [...guardHosts] } } : {}),
|
|
86276
|
+
}
|
|
86277
|
+
|
|
86278
|
+
await startProxies(config)
|
|
86279
|
+
`;
|
|
86280
|
+
}
|
|
85468
86281
|
function writeFileHeredoc(path, content, delimiter) {
|
|
85469
86282
|
return [
|
|
85470
86283
|
`cat > ${path} <<'${delimiter}'`,
|
|
@@ -85472,21 +86285,110 @@ function writeFileHeredoc(path, content, delimiter) {
|
|
|
85472
86285
|
delimiter
|
|
85473
86286
|
];
|
|
85474
86287
|
}
|
|
86288
|
+
function certDomainsForConfig(config6) {
|
|
86289
|
+
const seen = new Set;
|
|
86290
|
+
for (const r of config6.proxies) {
|
|
86291
|
+
const host = r.to;
|
|
86292
|
+
if (!host || host.startsWith("*") || host.includes(":") || !host.includes("."))
|
|
86293
|
+
continue;
|
|
86294
|
+
seen.add(host);
|
|
86295
|
+
}
|
|
86296
|
+
return [...seen];
|
|
86297
|
+
}
|
|
86298
|
+
function buildCertManagementCommands(options) {
|
|
86299
|
+
const { config: config6, proxy } = options;
|
|
86300
|
+
const webroot = config6.acmeChallengeWebroot;
|
|
86301
|
+
const domains = certDomainsForConfig(config6);
|
|
86302
|
+
if (!proxy.onDemandTls || !webroot || domains.length === 0)
|
|
86303
|
+
return [];
|
|
86304
|
+
const bunBin = options.bunBin ?? "/usr/local/bin/bun";
|
|
86305
|
+
const version2 = proxy.version ?? "latest";
|
|
86306
|
+
const certsDir = config6.productionCerts.certsDir;
|
|
86307
|
+
const email3 = proxy.onDemandTlsEmail ?? `webmaster@${domains[0]}`;
|
|
86308
|
+
const tlsxCli = `${bunBin} ${RPX_INSTALL_DIR}/node_modules/@stacksjs/tlsx/dist/bin/cli.js`;
|
|
86309
|
+
const csv = domains.join(",");
|
|
86310
|
+
const spaced = domains.join(" ");
|
|
86311
|
+
const slug = (options.slug || "app").replace(/[^a-z0-9._-]+/gi, "-");
|
|
86312
|
+
const renewScriptPath = `${RPX_DIR}/renew-certs-${slug}.sh`;
|
|
86313
|
+
const renewServiceName = `rpx-cert-renew-${slug}.service`;
|
|
86314
|
+
const renewTimerName = `rpx-cert-renew-${slug}.timer`;
|
|
86315
|
+
const renewScript = [
|
|
86316
|
+
"#!/bin/sh",
|
|
86317
|
+
"# Generated by ts-cloud — issue/renew rpx gateway TLS certs via tlsx http-01.",
|
|
86318
|
+
"# The running gateway serves the challenge from $WEBROOT on :80, so this needs",
|
|
86319
|
+
"# no downtime and no DNS credentials. Reloads the gateway only if a cert changed.",
|
|
86320
|
+
"set -u",
|
|
86321
|
+
`CERTS='${certsDir}'`,
|
|
86322
|
+
`WEBROOT='${webroot}'`,
|
|
86323
|
+
`EMAIL='${email3}'`,
|
|
86324
|
+
`TLSX="${tlsxCli}"`,
|
|
86325
|
+
`DOMAINS='${csv}'`,
|
|
86326
|
+
'before=$(cat "$CERTS"/*.crt 2>/dev/null | sha256sum)',
|
|
86327
|
+
`for d in ${spaced}; do`,
|
|
86328
|
+
' [ -s "$CERTS/$d.crt" ] || $TLSX acme:issue -d "$d" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --prod --email "$EMAIL" || echo "issue $d failed (non-fatal)"',
|
|
86329
|
+
"done",
|
|
86330
|
+
'$TLSX acme:renew --domains "$DOMAINS" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --days 30 --prod --email "$EMAIL" || echo "renew: some domains failed (non-fatal)"',
|
|
86331
|
+
'rm -f "$CERTS"/*.chain.crt',
|
|
86332
|
+
'after=$(cat "$CERTS"/*.crt 2>/dev/null | sha256sum)',
|
|
86333
|
+
`[ "$before" = "$after" ] || systemctl restart ${RPX_SERVICE_NAME}`
|
|
86334
|
+
].join(`
|
|
86335
|
+
`);
|
|
86336
|
+
const renewService = [
|
|
86337
|
+
"[Unit]",
|
|
86338
|
+
`Description=Issue/renew rpx gateway TLS certs for ${slug} (tlsx http-01)`,
|
|
86339
|
+
`After=network-online.target ${RPX_SERVICE_NAME}`,
|
|
86340
|
+
"Wants=network-online.target",
|
|
86341
|
+
"",
|
|
86342
|
+
"[Service]",
|
|
86343
|
+
"Type=oneshot",
|
|
86344
|
+
`ExecStart=${renewScriptPath}`
|
|
86345
|
+
].join(`
|
|
86346
|
+
`);
|
|
86347
|
+
const renewTimer = [
|
|
86348
|
+
"[Unit]",
|
|
86349
|
+
`Description=Daily rpx gateway TLS cert issuance/renewal for ${slug}`,
|
|
86350
|
+
"",
|
|
86351
|
+
"[Timer]",
|
|
86352
|
+
"OnCalendar=*-*-* 03:30:00",
|
|
86353
|
+
"RandomizedDelaySec=1h",
|
|
86354
|
+
"Persistent=true",
|
|
86355
|
+
"",
|
|
86356
|
+
"[Install]",
|
|
86357
|
+
"WantedBy=timers.target"
|
|
86358
|
+
].join(`
|
|
86359
|
+
`);
|
|
86360
|
+
return [
|
|
86361
|
+
`mkdir -p ${webroot}`,
|
|
86362
|
+
`(cd ${RPX_INSTALL_DIR} && ${bunBin} add @stacksjs/tlsx@${version2}) || true`,
|
|
86363
|
+
...writeFileHeredoc(renewScriptPath, renewScript, "TS_CLOUD_RENEW_EOF"),
|
|
86364
|
+
`chmod +x ${renewScriptPath}`,
|
|
86365
|
+
...writeFileHeredoc(`/etc/systemd/system/${renewServiceName}`, renewService, "TS_CLOUD_RENEW_SVC_EOF"),
|
|
86366
|
+
...writeFileHeredoc(`/etc/systemd/system/${renewTimerName}`, renewTimer, "TS_CLOUD_RENEW_TIMER_EOF"),
|
|
86367
|
+
"systemctl daemon-reload",
|
|
86368
|
+
`systemctl enable --now ${renewTimerName} || true`,
|
|
86369
|
+
`${renewScriptPath} || true`
|
|
86370
|
+
];
|
|
86371
|
+
}
|
|
85475
86372
|
function buildRpxProvisionScript(options) {
|
|
85476
86373
|
const { config: config6, proxy } = options;
|
|
85477
86374
|
const bunBin = options.bunBin ?? "/usr/local/bin/bun";
|
|
85478
86375
|
const version2 = proxy.version ?? "latest";
|
|
85479
86376
|
const certsDir = config6.productionCerts.certsDir;
|
|
85480
|
-
const
|
|
86377
|
+
const slug = (options.slug || "app").replace(/[^a-z0-9._-]+/gi, "-");
|
|
86378
|
+
const fragment = JSON.stringify({ slug, ...config6 }, null, 2);
|
|
86379
|
+
const assembler = renderRpxAssembler(RPX_SITES_DIR, certsDir);
|
|
85481
86380
|
const upstreamTimeout = proxy.upstreamTimeout ?? 60;
|
|
85482
86381
|
const poolEnv = [`Environment=RPX_UPSTREAM_TIMEOUT=${upstreamTimeout}`];
|
|
85483
86382
|
if (typeof proxy.maxUpstreamConns === "number")
|
|
85484
86383
|
poolEnv.push(`Environment=RPX_MAX_UPSTREAM_CONNS=${proxy.maxUpstreamConns}`);
|
|
85485
86384
|
return [
|
|
85486
86385
|
"set -euo pipefail",
|
|
85487
|
-
`mkdir -p ${RPX_DIR} ${certsDir}`,
|
|
85488
|
-
`
|
|
85489
|
-
|
|
86386
|
+
`mkdir -p ${RPX_DIR} ${RPX_SITES_DIR} ${certsDir} ${RPX_INSTALL_DIR}`,
|
|
86387
|
+
`rm -rf ${RPX_INSTALL_DIR}/node_modules ${RPX_INSTALL_DIR}/bun.lock ${RPX_INSTALL_DIR}/package.json`,
|
|
86388
|
+
`(cd ${RPX_INSTALL_DIR} && ${bunBin} add @stacksjs/rpx@${version2})`,
|
|
86389
|
+
`ln -sfn ${RPX_INSTALL_DIR}/node_modules ${RPX_DIR}/node_modules`,
|
|
86390
|
+
...writeFileHeredoc(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF"),
|
|
86391
|
+
...writeFileHeredoc(RPX_LAUNCHER_PATH, assembler, "TS_CLOUD_RPX_EOF"),
|
|
85490
86392
|
...writeFileHeredoc(`/etc/systemd/system/${RPX_SERVICE_NAME}`, [
|
|
85491
86393
|
"[Unit]",
|
|
85492
86394
|
"Description=rpx reverse-proxy gateway (managed by ts-cloud)",
|
|
@@ -85496,6 +86398,7 @@ function buildRpxProvisionScript(options) {
|
|
|
85496
86398
|
"[Service]",
|
|
85497
86399
|
"Type=simple",
|
|
85498
86400
|
`ExecStart=${bunBin} ${RPX_LAUNCHER_PATH}`,
|
|
86401
|
+
`WorkingDirectory=${RPX_INSTALL_DIR}`,
|
|
85499
86402
|
`Environment=BUN_INSTALL=/root/.bun`,
|
|
85500
86403
|
...poolEnv,
|
|
85501
86404
|
"Restart=always",
|
|
@@ -85511,7 +86414,8 @@ function buildRpxProvisionScript(options) {
|
|
|
85511
86414
|
"systemctl disable --now bun-gateway.service 2>/dev/null || true",
|
|
85512
86415
|
"systemctl disable --now ts-cloud-nginx.service 2>/dev/null || true",
|
|
85513
86416
|
`systemctl enable ${RPX_SERVICE_NAME}`,
|
|
85514
|
-
`systemctl restart ${RPX_SERVICE_NAME}
|
|
86417
|
+
`systemctl restart ${RPX_SERVICE_NAME}`,
|
|
86418
|
+
...buildCertManagementCommands(options)
|
|
85515
86419
|
];
|
|
85516
86420
|
}
|
|
85517
86421
|
|
|
@@ -85596,10 +86500,10 @@ function matchesTsCloudLabels(labels, slug, environment, role = "app") {
|
|
|
85596
86500
|
|
|
85597
86501
|
// src/drivers/hetzner/state.ts
|
|
85598
86502
|
import { mkdir as mkdir4, readFile, writeFile as writeFile4 } from "node:fs/promises";
|
|
85599
|
-
import { join as
|
|
86503
|
+
import { join as join18 } from "node:path";
|
|
85600
86504
|
var STATE_DIR = ".ts-cloud/state";
|
|
85601
86505
|
function driverStatePath(stackName) {
|
|
85602
|
-
return
|
|
86506
|
+
return join18(process.cwd(), STATE_DIR, `${stackName}.json`);
|
|
85603
86507
|
}
|
|
85604
86508
|
async function readDriverState(stackName) {
|
|
85605
86509
|
try {
|
|
@@ -85611,7 +86515,7 @@ async function readDriverState(stackName) {
|
|
|
85611
86515
|
}
|
|
85612
86516
|
async function writeDriverState(stackName, state) {
|
|
85613
86517
|
const path = driverStatePath(stackName);
|
|
85614
|
-
await mkdir4(
|
|
86518
|
+
await mkdir4(join18(process.cwd(), STATE_DIR), { recursive: true });
|
|
85615
86519
|
await writeFile4(path, `${JSON.stringify(state, null, 2)}
|
|
85616
86520
|
`, "utf8");
|
|
85617
86521
|
}
|
|
@@ -85619,7 +86523,7 @@ async function writeDriverState(stackName, state) {
|
|
|
85619
86523
|
// src/drivers/hetzner/driver.ts
|
|
85620
86524
|
var SSH_MAX_BUFFER = 1024 * 1024 * 256;
|
|
85621
86525
|
function expandHome(path) {
|
|
85622
|
-
return path.startsWith("~/") ?
|
|
86526
|
+
return path.startsWith("~/") ? join19(homedir8(), path.slice(2)) : path;
|
|
85623
86527
|
}
|
|
85624
86528
|
|
|
85625
86529
|
class HetznerDriver {
|
|
@@ -85687,6 +86591,7 @@ class HetznerDriver {
|
|
|
85687
86591
|
const rpxProvision = compute.proxy?.engine === "rpx" ? buildRpxProvisionScript({
|
|
85688
86592
|
proxy: compute.proxy,
|
|
85689
86593
|
config: buildRpxConfig(sites, { proxy: compute.proxy }),
|
|
86594
|
+
slug: config6.project.slug,
|
|
85690
86595
|
bunBin: compute.runtime === "node" || compute.runtime === "deno" ? undefined : "/usr/local/bin/bun"
|
|
85691
86596
|
}) : undefined;
|
|
85692
86597
|
const provision = buildComputeProvisionScripts(config6);
|
|
@@ -86049,10 +86954,10 @@ class HetznerDriver {
|
|
|
86049
86954
|
};
|
|
86050
86955
|
}
|
|
86051
86956
|
async ensureSshKey(slug, environment, labels) {
|
|
86052
|
-
if (!
|
|
86957
|
+
if (!existsSync20(this.sshPublicKeyPath)) {
|
|
86053
86958
|
throw new Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. ` + `Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);
|
|
86054
86959
|
}
|
|
86055
|
-
const publicKey =
|
|
86960
|
+
const publicKey = readFileSync16(this.sshPublicKeyPath, "utf8").trim();
|
|
86056
86961
|
const normalized = normalizeSshPublicKey(publicKey);
|
|
86057
86962
|
const existing = await this.client.listSshKeys();
|
|
86058
86963
|
const match = existing.find((key) => normalizeSshPublicKey(key.public_key) === normalized);
|
|
@@ -86184,8 +87089,53 @@ ${out}`);
|
|
|
86184
87089
|
}
|
|
86185
87090
|
}
|
|
86186
87091
|
|
|
87092
|
+
// src/drivers/local-box/driver.ts
|
|
87093
|
+
class LocalBoxDriver {
|
|
87094
|
+
name = "hetzner";
|
|
87095
|
+
usesCloudFormation = false;
|
|
87096
|
+
async getComputeOutputs() {
|
|
87097
|
+
return { appPublicIp: "127.0.0.1", sshUser: "root" };
|
|
87098
|
+
}
|
|
87099
|
+
async uploadRelease(options) {
|
|
87100
|
+
return { artifactRef: options.localPath };
|
|
87101
|
+
}
|
|
87102
|
+
async findComputeTargets(_options) {
|
|
87103
|
+
return [{ id: "localhost", name: "localhost", publicIp: "127.0.0.1", status: "running" }];
|
|
87104
|
+
}
|
|
87105
|
+
async runRemoteDeploy(options) {
|
|
87106
|
+
const script = options.commands.join(`
|
|
87107
|
+
`);
|
|
87108
|
+
try {
|
|
87109
|
+
const proc = Bun.spawn(["bash", "-c", script], { stdout: "pipe", stderr: "pipe", env: process.env });
|
|
87110
|
+
const [output, error2, exitCode] = await Promise.all([
|
|
87111
|
+
new Response(proc.stdout).text(),
|
|
87112
|
+
new Response(proc.stderr).text(),
|
|
87113
|
+
proc.exited
|
|
87114
|
+
]);
|
|
87115
|
+
return {
|
|
87116
|
+
success: exitCode === 0,
|
|
87117
|
+
instanceCount: 1,
|
|
87118
|
+
perInstance: [{ instanceId: "localhost", status: exitCode === 0 ? "Success" : "Failed", output, error: error2 }]
|
|
87119
|
+
};
|
|
87120
|
+
} catch (err) {
|
|
87121
|
+
return {
|
|
87122
|
+
success: false,
|
|
87123
|
+
instanceCount: 1,
|
|
87124
|
+
perInstance: [{ instanceId: "localhost", status: "Failed", error: err?.message ?? String(err) }],
|
|
87125
|
+
error: err?.message ?? String(err)
|
|
87126
|
+
};
|
|
87127
|
+
}
|
|
87128
|
+
}
|
|
87129
|
+
}
|
|
87130
|
+
function isBoxMode() {
|
|
87131
|
+
const v = process.env.TS_CLOUD_DASHBOARD_BOX;
|
|
87132
|
+
return v != null && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
87133
|
+
}
|
|
87134
|
+
|
|
86187
87135
|
// src/drivers/factory.ts
|
|
86188
87136
|
function createCloudDriver(options) {
|
|
87137
|
+
if (isBoxMode())
|
|
87138
|
+
return new LocalBoxDriver;
|
|
86189
87139
|
const provider = options.provider ?? resolveCloudProvider(options.config);
|
|
86190
87140
|
switch (provider) {
|
|
86191
87141
|
case "aws":
|
|
@@ -86226,6 +87176,111 @@ function quoteEnvValue(value) {
|
|
|
86226
87176
|
return `"${escaped}"`;
|
|
86227
87177
|
}
|
|
86228
87178
|
|
|
87179
|
+
// src/drivers/shared/releases.ts
|
|
87180
|
+
var DEFAULT_SHARED_PATHS = ["storage", ".env"];
|
|
87181
|
+
var DEFAULT_KEEP_RELEASES = 4;
|
|
87182
|
+
var DEFAULT_KEEP_DEPLOY_LOGS = 20;
|
|
87183
|
+
function deployMetaDir(base) {
|
|
87184
|
+
return `${base.replace(/\/+$/, "")}/.ts-cloud`;
|
|
87185
|
+
}
|
|
87186
|
+
function deployHistoryPath(base) {
|
|
87187
|
+
return `${deployMetaDir(base)}/deploy-history.log`;
|
|
87188
|
+
}
|
|
87189
|
+
function deployLogPath(base, releaseId) {
|
|
87190
|
+
return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
|
|
87191
|
+
}
|
|
87192
|
+
function releasePaths(base, releaseId) {
|
|
87193
|
+
const root = base.replace(/\/+$/, "");
|
|
87194
|
+
return {
|
|
87195
|
+
base: root,
|
|
87196
|
+
releases: `${root}/releases`,
|
|
87197
|
+
shared: `${root}/shared`,
|
|
87198
|
+
current: `${root}/current`,
|
|
87199
|
+
release: `${root}/releases/${releaseId}`
|
|
87200
|
+
};
|
|
87201
|
+
}
|
|
87202
|
+
function isFileSharedPath(p) {
|
|
87203
|
+
const name = p.split("/").pop() || p;
|
|
87204
|
+
return name.startsWith(".") || /\.[a-z0-9]+$/i.test(name);
|
|
87205
|
+
}
|
|
87206
|
+
function buildEnsureReleaseLayout(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
87207
|
+
const lines = [
|
|
87208
|
+
`mkdir -p ${paths.releases} ${paths.shared}`
|
|
87209
|
+
];
|
|
87210
|
+
for (const p of sharedPaths) {
|
|
87211
|
+
if (isFileSharedPath(p)) {
|
|
87212
|
+
lines.push(`mkdir -p "$(dirname ${paths.shared}/${p})"`, `touch ${paths.shared}/${p}`);
|
|
87213
|
+
} else if (p === "storage") {
|
|
87214
|
+
lines.push(`mkdir -p ${paths.shared}/storage/app/public`, `mkdir -p ${paths.shared}/storage/framework/cache/data`, `mkdir -p ${paths.shared}/storage/framework/sessions`, `mkdir -p ${paths.shared}/storage/framework/testing`, `mkdir -p ${paths.shared}/storage/framework/views`, `mkdir -p ${paths.shared}/storage/logs`);
|
|
87215
|
+
} else {
|
|
87216
|
+
lines.push(`mkdir -p ${paths.shared}/${p}`);
|
|
87217
|
+
}
|
|
87218
|
+
}
|
|
87219
|
+
return lines;
|
|
87220
|
+
}
|
|
87221
|
+
function buildLinkSharedPaths(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
87222
|
+
const lines = [];
|
|
87223
|
+
for (const p of sharedPaths) {
|
|
87224
|
+
const target = `${paths.shared}/${p}`;
|
|
87225
|
+
const link = `${paths.release}/${p}`;
|
|
87226
|
+
lines.push(`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`);
|
|
87227
|
+
}
|
|
87228
|
+
return lines;
|
|
87229
|
+
}
|
|
87230
|
+
function buildActivateRelease(paths) {
|
|
87231
|
+
return [
|
|
87232
|
+
`ln -sfn ${paths.release} ${paths.current}.tmp`,
|
|
87233
|
+
`mv -Tf ${paths.current}.tmp ${paths.current}`
|
|
87234
|
+
];
|
|
87235
|
+
}
|
|
87236
|
+
function buildRollbackScript(paths, options = {}) {
|
|
87237
|
+
if (options.to) {
|
|
87238
|
+
const target = `${paths.releases}/${options.to}`;
|
|
87239
|
+
return [
|
|
87240
|
+
`[ -d ${target} ] || { echo "rollback target ${target} not found" >&2; exit 1; }`,
|
|
87241
|
+
`ln -sfn ${target} ${paths.current}.tmp`,
|
|
87242
|
+
`mv -Tf ${paths.current}.tmp ${paths.current}`
|
|
87243
|
+
];
|
|
87244
|
+
}
|
|
87245
|
+
return [
|
|
87246
|
+
`TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
|
|
87247
|
+
`TS_CLOUD_PREV=$(ls -1dt ${paths.releases}/*/ 2>/dev/null | sed 's#/$##' | while read -r r; do ` + '[ "$(readlink -f "$r")" != "$TS_CLOUD_CURRENT" ] && { echo "$r"; break; }; done)',
|
|
87248
|
+
'[ -n "$TS_CLOUD_PREV" ] || { echo "no previous release to roll back to" >&2; exit 1; }',
|
|
87249
|
+
`ln -sfn "$TS_CLOUD_PREV" ${paths.current}.tmp`,
|
|
87250
|
+
`mv -Tf ${paths.current}.tmp ${paths.current}`,
|
|
87251
|
+
'echo "rolled back to $TS_CLOUD_PREV"'
|
|
87252
|
+
];
|
|
87253
|
+
}
|
|
87254
|
+
function buildPruneReleases(paths, keep = DEFAULT_KEEP_RELEASES) {
|
|
87255
|
+
const n = Math.max(1, keep);
|
|
87256
|
+
return [
|
|
87257
|
+
`TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
|
|
87258
|
+
`ls -1dt ${paths.releases}/*/ 2>/dev/null | sed 's#/$##' | tail -n +${n + 1} | while read -r TS_CLOUD_OLD; do`,
|
|
87259
|
+
' [ "$(readlink -f "$TS_CLOUD_OLD")" = "$TS_CLOUD_CURRENT" ] || rm -rf "$TS_CLOUD_OLD"',
|
|
87260
|
+
"done"
|
|
87261
|
+
];
|
|
87262
|
+
}
|
|
87263
|
+
function buildDeployHistoryHeader(base, options) {
|
|
87264
|
+
const meta = deployMetaDir(base);
|
|
87265
|
+
const log4 = deployLogPath(base, options.releaseId);
|
|
87266
|
+
const history = deployHistoryPath(base);
|
|
87267
|
+
const keepLogs = Math.max(1, options.keepLogs ?? DEFAULT_KEEP_DEPLOY_LOGS);
|
|
87268
|
+
const commit = options.commit || "";
|
|
87269
|
+
const branch = options.branch || "";
|
|
87270
|
+
return [
|
|
87271
|
+
`mkdir -p ${meta}/deploys`,
|
|
87272
|
+
`exec > >(tee -a ${log4}) 2>&1`,
|
|
87273
|
+
`echo "[ts-cloud] deploy ${options.releaseId} commit=${commit} branch=${branch} starting $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
|
|
87274
|
+
"ts_cloud_record_deploy() {",
|
|
87275
|
+
" TS_CLOUD_RC=$?",
|
|
87276
|
+
' if [ "$TS_CLOUD_RC" -eq 0 ]; then TS_CLOUD_ST=success; else TS_CLOUD_ST=failed; fi',
|
|
87277
|
+
` printf '%s\\t%s\\t%s\\t%s\\trc=%s\\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${options.releaseId}" "${commit}" "$TS_CLOUD_ST" "$TS_CLOUD_RC" >> ${history}`,
|
|
87278
|
+
"}",
|
|
87279
|
+
"trap ts_cloud_record_deploy EXIT",
|
|
87280
|
+
`ls -1t ${meta}/deploys/*.log 2>/dev/null | tail -n +${keepLogs + 1} | while read -r TS_CLOUD_OLDLOG; do rm -f "$TS_CLOUD_OLDLOG"; done`
|
|
87281
|
+
];
|
|
87282
|
+
}
|
|
87283
|
+
|
|
86229
87284
|
// src/drivers/shared/deploy-script.ts
|
|
86230
87285
|
function resolveExecStart(start, runtime) {
|
|
86231
87286
|
const bin = runtime === "bun" ? "/usr/local/bin/bun" : runtime === "deno" ? "/usr/local/bin/deno" : "/usr/local/bin/node";
|
|
@@ -86237,25 +87292,31 @@ function buildSiteDeployScript(options) {
|
|
|
86237
87292
|
siteName,
|
|
86238
87293
|
slug,
|
|
86239
87294
|
artifactFetch,
|
|
87295
|
+
releaseId,
|
|
86240
87296
|
execStart,
|
|
86241
87297
|
envEntries,
|
|
86242
87298
|
port,
|
|
87299
|
+
keepReleases = DEFAULT_KEEP_RELEASES,
|
|
86243
87300
|
preStartCommands = []
|
|
86244
87301
|
} = options;
|
|
86245
|
-
const
|
|
87302
|
+
const base = options.appDir ?? `/var/www/${siteName}`;
|
|
87303
|
+
const paths = releasePaths(base, releaseId);
|
|
86246
87304
|
const serviceName = `${slug}-${siteName}.service`;
|
|
87305
|
+
const sharedPaths = [".env"];
|
|
86247
87306
|
const envFile = formatEnvFile(envEntries);
|
|
86248
|
-
const preStart = preStartCommands.length > 0 ? [`cd ${
|
|
87307
|
+
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
86249
87308
|
return [
|
|
86250
87309
|
"set -euo pipefail",
|
|
86251
87310
|
...artifactFetch,
|
|
86252
|
-
|
|
86253
|
-
`
|
|
86254
|
-
`
|
|
86255
|
-
`
|
|
87311
|
+
...buildEnsureReleaseLayout(paths, sharedPaths),
|
|
87312
|
+
`rm -rf ${paths.release}`,
|
|
87313
|
+
`mkdir -p ${paths.release}`,
|
|
87314
|
+
`tar xzf /tmp/${siteName}-release.tar.gz -C ${paths.release}`,
|
|
87315
|
+
`cat > ${paths.shared}/.env <<'TS_CLOUD_ENV_EOF'`,
|
|
86256
87316
|
envFile,
|
|
86257
87317
|
"TS_CLOUD_ENV_EOF",
|
|
86258
|
-
`chmod 600 ${
|
|
87318
|
+
`chmod 600 ${paths.shared}/.env`,
|
|
87319
|
+
...buildLinkSharedPaths(paths, sharedPaths),
|
|
86259
87320
|
...preStart,
|
|
86260
87321
|
`cat > /etc/systemd/system/${serviceName} <<'TS_CLOUD_UNIT_EOF'`,
|
|
86261
87322
|
"[Unit]",
|
|
@@ -86264,11 +87325,11 @@ function buildSiteDeployScript(options) {
|
|
|
86264
87325
|
"",
|
|
86265
87326
|
"[Service]",
|
|
86266
87327
|
"Type=simple",
|
|
86267
|
-
`WorkingDirectory=${
|
|
87328
|
+
`WorkingDirectory=${paths.current}`,
|
|
86268
87329
|
`ExecStart=${execStart}`,
|
|
86269
87330
|
"Restart=always",
|
|
86270
87331
|
"RestartSec=5",
|
|
86271
|
-
`EnvironmentFile=${
|
|
87332
|
+
`EnvironmentFile=${paths.current}/.env`,
|
|
86272
87333
|
...port ? [`Environment=PORT=${port}`] : [],
|
|
86273
87334
|
"",
|
|
86274
87335
|
"[Install]",
|
|
@@ -86276,21 +87337,27 @@ function buildSiteDeployScript(options) {
|
|
|
86276
87337
|
"TS_CLOUD_UNIT_EOF",
|
|
86277
87338
|
"systemctl daemon-reload",
|
|
86278
87339
|
`systemctl enable ${serviceName}`,
|
|
87340
|
+
...buildActivateRelease(paths),
|
|
86279
87341
|
`systemctl restart ${serviceName}`,
|
|
86280
|
-
`systemctl is-active ${serviceName}
|
|
87342
|
+
`systemctl is-active ${serviceName}`,
|
|
87343
|
+
...buildPruneReleases(paths, keepReleases)
|
|
86281
87344
|
];
|
|
86282
87345
|
}
|
|
86283
87346
|
function buildStaticSiteDeployScript(options) {
|
|
86284
|
-
const { siteName, artifactFetch, preStartCommands = [] } = options;
|
|
86285
|
-
const
|
|
86286
|
-
const
|
|
87347
|
+
const { siteName, artifactFetch, releaseId, keepReleases = DEFAULT_KEEP_RELEASES, preStartCommands = [] } = options;
|
|
87348
|
+
const base = options.appDir ?? `/var/www/${siteName}`;
|
|
87349
|
+
const paths = releasePaths(base, releaseId);
|
|
87350
|
+
const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
|
|
86287
87351
|
return [
|
|
86288
87352
|
"set -euo pipefail",
|
|
86289
87353
|
...artifactFetch,
|
|
86290
|
-
|
|
86291
|
-
`
|
|
86292
|
-
`
|
|
86293
|
-
|
|
87354
|
+
...buildEnsureReleaseLayout(paths, []),
|
|
87355
|
+
`rm -rf ${paths.release}`,
|
|
87356
|
+
`mkdir -p ${paths.release}`,
|
|
87357
|
+
`tar xzf /tmp/${siteName}-release.tar.gz -C ${paths.release}`,
|
|
87358
|
+
...preStart,
|
|
87359
|
+
...buildActivateRelease(paths),
|
|
87360
|
+
...buildPruneReleases(paths, keepReleases)
|
|
86294
87361
|
];
|
|
86295
87362
|
}
|
|
86296
87363
|
function buildAwsArtifactFetch(bucket, key, region, siteName) {
|
|
@@ -86306,6 +87373,95 @@ function buildLocalArtifactFetch(localPath, siteName) {
|
|
|
86306
87373
|
// src/drivers/shared/compute-deploy.ts
|
|
86307
87374
|
await init_dist2();
|
|
86308
87375
|
|
|
87376
|
+
// src/deploy/management-dashboard.ts
|
|
87377
|
+
await init_dist2();
|
|
87378
|
+
import { execSync as execSync5 } from "node:child_process";
|
|
87379
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
87380
|
+
import { tmpdir as tmpdir6 } from "node:os";
|
|
87381
|
+
import { dirname as dirname8, isAbsolute as isAbsolute5, join as join20 } from "node:path";
|
|
87382
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
87383
|
+
var MANAGEMENT_DASHBOARD_SITE = "dashboard";
|
|
87384
|
+
var noopLogger = { info: () => {}, warn: () => {} };
|
|
87385
|
+
function truthy(v) {
|
|
87386
|
+
return v != null && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
87387
|
+
}
|
|
87388
|
+
function resolveUiSource(cwd) {
|
|
87389
|
+
if (existsSync21(join20(cwd, "packages", "ui", "pages")) || existsSync21(join20(cwd, "packages", "ui", "package.json"))) {
|
|
87390
|
+
return { uiRoot: "packages/ui/dist", build: "cd packages/ui && bun install && bun run build" };
|
|
87391
|
+
}
|
|
87392
|
+
const here = dirname8(fileURLToPath4(import.meta.url));
|
|
87393
|
+
const candidates = [
|
|
87394
|
+
join20(here, "ui"),
|
|
87395
|
+
join20(here, "..", "ui"),
|
|
87396
|
+
join20(here, "..", "..", "ui"),
|
|
87397
|
+
join20(here, "..", "dist", "ui")
|
|
87398
|
+
];
|
|
87399
|
+
for (const dir of candidates) {
|
|
87400
|
+
if (existsSync21(join20(dir, "index.html")) || existsSync21(join20(dir, "serverless.html")))
|
|
87401
|
+
return { uiRoot: dir, build: false };
|
|
87402
|
+
}
|
|
87403
|
+
return null;
|
|
87404
|
+
}
|
|
87405
|
+
function ensureManagementDashboard(config6, options = {}) {
|
|
87406
|
+
const logger4 = options.logger ?? noopLogger;
|
|
87407
|
+
const cwd = options.cwd ?? process.cwd();
|
|
87408
|
+
if (truthy(process.env.TS_CLOUD_UI_DISABLE)) {
|
|
87409
|
+
logger4.info("Management dashboard: skipped (TS_CLOUD_UI_DISABLE set).");
|
|
87410
|
+
return config6;
|
|
87411
|
+
}
|
|
87412
|
+
if (hasManagementDashboardSite(config6))
|
|
87413
|
+
return config6;
|
|
87414
|
+
const ui = resolveUiSource(cwd);
|
|
87415
|
+
if (!ui) {
|
|
87416
|
+
logger4.info("Management dashboard: UI not found (no local ui/ or packaged dist/ui) — skipping auto-deploy.");
|
|
87417
|
+
return config6;
|
|
87418
|
+
}
|
|
87419
|
+
const password = process.env.TS_CLOUD_UI_PASSWORD?.trim() || undefined;
|
|
87420
|
+
const environment = config6.environments && Object.keys(config6.environments)[0];
|
|
87421
|
+
const live = truthy(process.env.TS_CLOUD_UI_LIVE);
|
|
87422
|
+
const port = Number(process.env.TS_CLOUD_UI_PORT) || undefined;
|
|
87423
|
+
const resolved = resolveManagementDashboardSite(config6, environment ?? "production", {
|
|
87424
|
+
uiRoot: ui.uiRoot,
|
|
87425
|
+
build: ui.build,
|
|
87426
|
+
domain: process.env.TS_CLOUD_UI_DOMAIN?.trim() || undefined,
|
|
87427
|
+
username: process.env.TS_CLOUD_UI_USERNAME?.trim() || undefined,
|
|
87428
|
+
password,
|
|
87429
|
+
realm: process.env.TS_CLOUD_UI_REALM?.trim() || undefined,
|
|
87430
|
+
live,
|
|
87431
|
+
port
|
|
87432
|
+
});
|
|
87433
|
+
if (!resolved) {
|
|
87434
|
+
logger4.info("Management dashboard: no domain resolved (set TS_CLOUD_UI_DOMAIN or configure a site domain) — skipping.");
|
|
87435
|
+
return config6;
|
|
87436
|
+
}
|
|
87437
|
+
config6.sites = { ...config6.sites ?? {}, [resolved.name]: resolved.site };
|
|
87438
|
+
logger4.info(`Management dashboard → https://${resolved.site.domain} (${password ? "htpasswd-protected" : "NO AUTH — set TS_CLOUD_UI_PASSWORD"})`);
|
|
87439
|
+
return config6;
|
|
87440
|
+
}
|
|
87441
|
+
function buildManagementDashboardArtifact(site, options) {
|
|
87442
|
+
if (!site?.root)
|
|
87443
|
+
return null;
|
|
87444
|
+
const cwd = options.cwd ?? process.cwd();
|
|
87445
|
+
const logger4 = options.logger ?? noopLogger;
|
|
87446
|
+
try {
|
|
87447
|
+
if (typeof site.build === "string" && site.build.trim()) {
|
|
87448
|
+
logger4.info(`Management dashboard: building UI (${site.build})`);
|
|
87449
|
+
execSync5(site.build, { cwd, stdio: "inherit" });
|
|
87450
|
+
}
|
|
87451
|
+
const root = isAbsolute5(site.root) ? site.root : join20(cwd, site.root);
|
|
87452
|
+
if (!existsSync21(root)) {
|
|
87453
|
+
logger4.warn(`Management dashboard: build output not found at ${root} — skipping dashboard artifact.`);
|
|
87454
|
+
return null;
|
|
87455
|
+
}
|
|
87456
|
+
const tarball = join20(tmpdir6(), `${options.slug}-${MANAGEMENT_DASHBOARD_SITE}-${options.sha}.tar.gz`);
|
|
87457
|
+
execSync5(`tar czf "${tarball}" -C "${root}" .`, { stdio: "inherit" });
|
|
87458
|
+
return tarball;
|
|
87459
|
+
} catch (error2) {
|
|
87460
|
+
logger4.warn(`Management dashboard: failed to build artifact — ${error2?.message ?? error2}. Skipping dashboard deploy.`);
|
|
87461
|
+
return null;
|
|
87462
|
+
}
|
|
87463
|
+
}
|
|
87464
|
+
|
|
86309
87465
|
// src/drivers/shared/certbot.ts
|
|
86310
87466
|
function resolveSslProvider(site) {
|
|
86311
87467
|
if (site.ssl?.provider)
|
|
@@ -86427,93 +87583,6 @@ function shellQuote(value) {
|
|
|
86427
87583
|
return `'${escaped}'`;
|
|
86428
87584
|
}
|
|
86429
87585
|
|
|
86430
|
-
// src/drivers/shared/releases.ts
|
|
86431
|
-
var DEFAULT_SHARED_PATHS = ["storage", ".env"];
|
|
86432
|
-
var DEFAULT_KEEP_RELEASES = 4;
|
|
86433
|
-
var DEFAULT_KEEP_DEPLOY_LOGS = 20;
|
|
86434
|
-
function deployMetaDir(base) {
|
|
86435
|
-
return `${base.replace(/\/+$/, "")}/.ts-cloud`;
|
|
86436
|
-
}
|
|
86437
|
-
function deployHistoryPath(base) {
|
|
86438
|
-
return `${deployMetaDir(base)}/deploy-history.log`;
|
|
86439
|
-
}
|
|
86440
|
-
function deployLogPath(base, releaseId) {
|
|
86441
|
-
return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
|
|
86442
|
-
}
|
|
86443
|
-
function releasePaths(base, releaseId) {
|
|
86444
|
-
const root = base.replace(/\/+$/, "");
|
|
86445
|
-
return {
|
|
86446
|
-
base: root,
|
|
86447
|
-
releases: `${root}/releases`,
|
|
86448
|
-
shared: `${root}/shared`,
|
|
86449
|
-
current: `${root}/current`,
|
|
86450
|
-
release: `${root}/releases/${releaseId}`
|
|
86451
|
-
};
|
|
86452
|
-
}
|
|
86453
|
-
function isFileSharedPath(p) {
|
|
86454
|
-
const name = p.split("/").pop() || p;
|
|
86455
|
-
return name.startsWith(".") || /\.[a-z0-9]+$/i.test(name);
|
|
86456
|
-
}
|
|
86457
|
-
function buildEnsureReleaseLayout(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
86458
|
-
const lines = [
|
|
86459
|
-
`mkdir -p ${paths.releases} ${paths.shared}`
|
|
86460
|
-
];
|
|
86461
|
-
for (const p of sharedPaths) {
|
|
86462
|
-
if (isFileSharedPath(p)) {
|
|
86463
|
-
lines.push(`mkdir -p "$(dirname ${paths.shared}/${p})"`, `touch ${paths.shared}/${p}`);
|
|
86464
|
-
} else if (p === "storage") {
|
|
86465
|
-
lines.push(`mkdir -p ${paths.shared}/storage/app/public`, `mkdir -p ${paths.shared}/storage/framework/cache/data`, `mkdir -p ${paths.shared}/storage/framework/sessions`, `mkdir -p ${paths.shared}/storage/framework/testing`, `mkdir -p ${paths.shared}/storage/framework/views`, `mkdir -p ${paths.shared}/storage/logs`);
|
|
86466
|
-
} else {
|
|
86467
|
-
lines.push(`mkdir -p ${paths.shared}/${p}`);
|
|
86468
|
-
}
|
|
86469
|
-
}
|
|
86470
|
-
return lines;
|
|
86471
|
-
}
|
|
86472
|
-
function buildLinkSharedPaths(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
|
|
86473
|
-
const lines = [];
|
|
86474
|
-
for (const p of sharedPaths) {
|
|
86475
|
-
const target = `${paths.shared}/${p}`;
|
|
86476
|
-
const link = `${paths.release}/${p}`;
|
|
86477
|
-
lines.push(`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`);
|
|
86478
|
-
}
|
|
86479
|
-
return lines;
|
|
86480
|
-
}
|
|
86481
|
-
function buildActivateRelease(paths) {
|
|
86482
|
-
return [
|
|
86483
|
-
`ln -sfn ${paths.release} ${paths.current}.tmp`,
|
|
86484
|
-
`mv -Tf ${paths.current}.tmp ${paths.current}`
|
|
86485
|
-
];
|
|
86486
|
-
}
|
|
86487
|
-
function buildPruneReleases(paths, keep = DEFAULT_KEEP_RELEASES) {
|
|
86488
|
-
const n = Math.max(1, keep);
|
|
86489
|
-
return [
|
|
86490
|
-
`TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
|
|
86491
|
-
`ls -1dt ${paths.releases}/*/ 2>/dev/null | sed 's#/$##' | tail -n +${n + 1} | while read -r TS_CLOUD_OLD; do`,
|
|
86492
|
-
' [ "$(readlink -f "$TS_CLOUD_OLD")" = "$TS_CLOUD_CURRENT" ] || rm -rf "$TS_CLOUD_OLD"',
|
|
86493
|
-
"done"
|
|
86494
|
-
];
|
|
86495
|
-
}
|
|
86496
|
-
function buildDeployHistoryHeader(base, options) {
|
|
86497
|
-
const meta = deployMetaDir(base);
|
|
86498
|
-
const log4 = deployLogPath(base, options.releaseId);
|
|
86499
|
-
const history = deployHistoryPath(base);
|
|
86500
|
-
const keepLogs = Math.max(1, options.keepLogs ?? DEFAULT_KEEP_DEPLOY_LOGS);
|
|
86501
|
-
const commit = options.commit || "";
|
|
86502
|
-
const branch = options.branch || "";
|
|
86503
|
-
return [
|
|
86504
|
-
`mkdir -p ${meta}/deploys`,
|
|
86505
|
-
`exec > >(tee -a ${log4}) 2>&1`,
|
|
86506
|
-
`echo "[ts-cloud] deploy ${options.releaseId} commit=${commit} branch=${branch} starting $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
|
|
86507
|
-
"ts_cloud_record_deploy() {",
|
|
86508
|
-
" TS_CLOUD_RC=$?",
|
|
86509
|
-
' if [ "$TS_CLOUD_RC" -eq 0 ]; then TS_CLOUD_ST=success; else TS_CLOUD_ST=failed; fi',
|
|
86510
|
-
` printf '%s\\t%s\\t%s\\t%s\\trc=%s\\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${options.releaseId}" "${commit}" "$TS_CLOUD_ST" "$TS_CLOUD_RC" >> ${history}`,
|
|
86511
|
-
"}",
|
|
86512
|
-
"trap ts_cloud_record_deploy EXIT",
|
|
86513
|
-
`ls -1t ${meta}/deploys/*.log 2>/dev/null | tail -n +${keepLogs + 1} | while read -r TS_CLOUD_OLDLOG; do rm -f "$TS_CLOUD_OLDLOG"; done`
|
|
86514
|
-
];
|
|
86515
|
-
}
|
|
86516
|
-
|
|
86517
87586
|
// src/drivers/shared/laravel-deploy.ts
|
|
86518
87587
|
var MACRO_CREATE_RELEASE = "$CREATE_RELEASE";
|
|
86519
87588
|
var MACRO_ACTIVATE_RELEASE = "$ACTIVATE_RELEASE";
|
|
@@ -86837,14 +87906,14 @@ function buildPhpFpmPoolScript(options) {
|
|
|
86837
87906
|
}
|
|
86838
87907
|
|
|
86839
87908
|
// src/drivers/shared/compute-deploy.ts
|
|
86840
|
-
var
|
|
87909
|
+
var noopLogger2 = {
|
|
86841
87910
|
info: () => {},
|
|
86842
87911
|
warn: () => {},
|
|
86843
87912
|
error: () => {},
|
|
86844
87913
|
step: () => {},
|
|
86845
87914
|
success: () => {}
|
|
86846
87915
|
};
|
|
86847
|
-
async function deploySiteRelease(driver, options, logger4 =
|
|
87916
|
+
async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
|
|
86848
87917
|
const {
|
|
86849
87918
|
config: config6,
|
|
86850
87919
|
environment,
|
|
@@ -86944,11 +88013,13 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86944
88013
|
const baseScript = kind === "server-static" ? buildStaticSiteDeployScript({
|
|
86945
88014
|
siteName,
|
|
86946
88015
|
artifactFetch,
|
|
88016
|
+
releaseId: sha,
|
|
86947
88017
|
preStartCommands: site.preStart
|
|
86948
88018
|
}) : buildSiteDeployScript({
|
|
86949
88019
|
siteName,
|
|
86950
88020
|
slug,
|
|
86951
88021
|
artifactFetch,
|
|
88022
|
+
releaseId: sha,
|
|
86952
88023
|
execStart: resolveExecStart(site.start, runtime),
|
|
86953
88024
|
envEntries: site.env || {},
|
|
86954
88025
|
port: site.port,
|
|
@@ -86961,7 +88032,7 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86961
88032
|
domain: site.domain,
|
|
86962
88033
|
aliases: site.aliases,
|
|
86963
88034
|
type: site.type === "spa" ? "spa" : "static",
|
|
86964
|
-
appDir: `/var/www/${siteName}`,
|
|
88035
|
+
appDir: `/var/www/${siteName}/current`,
|
|
86965
88036
|
webDirectory: "",
|
|
86966
88037
|
redirects: site.redirects,
|
|
86967
88038
|
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
@@ -86972,7 +88043,16 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86972
88043
|
security: site.security
|
|
86973
88044
|
}) : [];
|
|
86974
88045
|
const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
|
|
86975
|
-
const remoteScript = [
|
|
88046
|
+
const remoteScript = [
|
|
88047
|
+
...buildDeployHistoryHeader(`/var/www/${siteName}`, {
|
|
88048
|
+
releaseId: sha,
|
|
88049
|
+
commit: sha,
|
|
88050
|
+
branch: site.branch ?? "main"
|
|
88051
|
+
}),
|
|
88052
|
+
...baseScript,
|
|
88053
|
+
...staticVhost,
|
|
88054
|
+
...staticSsl
|
|
88055
|
+
];
|
|
86976
88056
|
logger4.step(`Deploying to ${targets.length} target(s)...`);
|
|
86977
88057
|
const result = await driver.runRemoteDeploy({
|
|
86978
88058
|
targets,
|
|
@@ -86999,8 +88079,21 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86999
88079
|
};
|
|
87000
88080
|
}
|
|
87001
88081
|
async function deployAllComputeSites(options) {
|
|
87002
|
-
const { config: config6, environment, driver, sha, runtime, tarballForSite, logger: logger4 =
|
|
88082
|
+
const { config: config6, environment, driver, sha, runtime, tarballForSite, logger: logger4 = noopLogger2 } = options;
|
|
87003
88083
|
const slug = config6.project.slug;
|
|
88084
|
+
const cwd = options.cwd ?? process.cwd();
|
|
88085
|
+
const hadDashboard = hasManagementDashboardSite(config6);
|
|
88086
|
+
ensureManagementDashboard(config6, { cwd, logger: { info: logger4.info, warn: logger4.warn } });
|
|
88087
|
+
const injectedDashboard = !hadDashboard && hasManagementDashboardSite(config6);
|
|
88088
|
+
let dashboardTarball = null;
|
|
88089
|
+
if (injectedDashboard) {
|
|
88090
|
+
dashboardTarball = buildManagementDashboardArtifact(config6.sites?.[MANAGEMENT_DASHBOARD_SITE], { cwd, slug, sha, logger: { info: logger4.info, warn: logger4.warn } });
|
|
88091
|
+
if (!dashboardTarball) {
|
|
88092
|
+
logger4.warn("Management dashboard: no artifact available — skipping dashboard site for this deploy.");
|
|
88093
|
+
if (config6.sites)
|
|
88094
|
+
delete config6.sites[MANAGEMENT_DASHBOARD_SITE];
|
|
88095
|
+
}
|
|
88096
|
+
}
|
|
87004
88097
|
const sites = config6.sites || {};
|
|
87005
88098
|
const deployable = Object.entries(sites).filter(([name, site]) => {
|
|
87006
88099
|
if (!site)
|
|
@@ -87010,13 +88103,16 @@ async function deployAllComputeSites(options) {
|
|
|
87010
88103
|
logger4.warn(`Site '${name}' targets a bucket — skipping (handled by the static-site path, not compute).`);
|
|
87011
88104
|
return false;
|
|
87012
88105
|
}
|
|
88106
|
+
if (kind === "redirect")
|
|
88107
|
+
return false;
|
|
87013
88108
|
return true;
|
|
87014
88109
|
});
|
|
87015
88110
|
if (deployable.length === 0)
|
|
87016
88111
|
return true;
|
|
88112
|
+
const tarballFor = (siteName) => siteName === MANAGEMENT_DASHBOARD_SITE && dashboardTarball ? dashboardTarball : tarballForSite(siteName);
|
|
87017
88113
|
for (const [siteName, site] of deployable) {
|
|
87018
88114
|
logger4.step(`Deploying site: ${siteName}`);
|
|
87019
|
-
const localTarballPath = isPhpSite(site) ? undefined :
|
|
88115
|
+
const localTarballPath = isPhpSite(site) ? undefined : tarballFor(siteName);
|
|
87020
88116
|
const result = await deploySiteRelease(driver, {
|
|
87021
88117
|
config: config6,
|
|
87022
88118
|
environment,
|
|
@@ -87049,7 +88145,7 @@ async function deployAllComputeSites(options) {
|
|
|
87049
88145
|
return true;
|
|
87050
88146
|
}
|
|
87051
88147
|
async function reloadRpxGateway(options) {
|
|
87052
|
-
const { config: config6, environment, driver, logger: logger4 =
|
|
88148
|
+
const { config: config6, environment, driver, logger: logger4 = noopLogger2 } = options;
|
|
87053
88149
|
const proxy = config6.infrastructure?.compute?.proxy;
|
|
87054
88150
|
if (proxy?.engine !== "rpx")
|
|
87055
88151
|
return true;
|
|
@@ -87069,7 +88165,7 @@ async function reloadRpxGateway(options) {
|
|
|
87069
88165
|
return true;
|
|
87070
88166
|
}
|
|
87071
88167
|
logger4.step(`Reloading rpx gateway with ${rpxConfig.proxies.length} route(s)...`);
|
|
87072
|
-
const script = buildRpxProvisionScript({ proxy, config: rpxConfig });
|
|
88168
|
+
const script = buildRpxProvisionScript({ proxy, config: rpxConfig, slug: config6.project.slug });
|
|
87073
88169
|
const result = await driver.runRemoteDeploy({
|
|
87074
88170
|
targets,
|
|
87075
88171
|
commands: script,
|
|
@@ -87087,6 +88183,2077 @@ async function reloadRpxGateway(options) {
|
|
|
87087
88183
|
logger4.success(`rpx gateway reloaded on ${result.instanceCount} target(s)`);
|
|
87088
88184
|
return true;
|
|
87089
88185
|
}
|
|
88186
|
+
// src/deploy/ssh-config-editor.ts
|
|
88187
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
88188
|
+
function addSshKeyToCloudConfig(input) {
|
|
88189
|
+
const name = normalizeKeyName(input.name);
|
|
88190
|
+
const publicKey = normalizePublicKey(input.publicKey);
|
|
88191
|
+
const keys = dedupeKeys([...input.existingKeys ?? [], { name, publicKey }]);
|
|
88192
|
+
return updateComputeSshKeys(input.configText, keys);
|
|
88193
|
+
}
|
|
88194
|
+
function removeSshKeyFromCloudConfig(input) {
|
|
88195
|
+
const name = normalizeKeyName(input.name);
|
|
88196
|
+
const keys = (input.existingKeys ?? []).filter((key) => key.name !== name);
|
|
88197
|
+
if ((input.existingKeys ?? []).length === keys.length)
|
|
88198
|
+
throw new Error(`SSH key '${name}' does not exist in compute.sshKeys`);
|
|
88199
|
+
return updateComputeSshKeys(input.configText, keys);
|
|
88200
|
+
}
|
|
88201
|
+
function describeSshKeys(keys = []) {
|
|
88202
|
+
return keys.map((key) => ({
|
|
88203
|
+
name: key.name,
|
|
88204
|
+
publicKey: key.publicKey,
|
|
88205
|
+
type: sshKeyType(key.publicKey),
|
|
88206
|
+
fingerprint: sshFingerprint(key.publicKey),
|
|
88207
|
+
added: "configured"
|
|
88208
|
+
}));
|
|
88209
|
+
}
|
|
88210
|
+
function updateComputeSshKeys(configText, keys) {
|
|
88211
|
+
const compute = findPropertyObject(configText, "compute");
|
|
88212
|
+
const rendered = renderSshKeys(keys);
|
|
88213
|
+
const existing = findPropertyArray(configText, compute.start, compute.end, "sshKeys");
|
|
88214
|
+
if (existing) {
|
|
88215
|
+
let lineStart = existing.propertyStart;
|
|
88216
|
+
while (lineStart > 0 && configText[lineStart - 1] !== `
|
|
88217
|
+
` && /\s/.test(configText[lineStart - 1]))
|
|
88218
|
+
lineStart--;
|
|
88219
|
+
return `${configText.slice(0, lineStart)}${rendered}${configText.slice(existing.end + 1)}`;
|
|
88220
|
+
}
|
|
88221
|
+
const before = configText.slice(0, compute.end).trimEnd();
|
|
88222
|
+
const after = configText.slice(compute.end);
|
|
88223
|
+
const sep = before.endsWith(",") || before.endsWith("{") ? "" : ",";
|
|
88224
|
+
return `${before}${sep}
|
|
88225
|
+
${rendered},
|
|
88226
|
+
${after}`;
|
|
88227
|
+
}
|
|
88228
|
+
function renderSshKeys(keys) {
|
|
88229
|
+
if (keys.length === 0)
|
|
88230
|
+
return " sshKeys: []";
|
|
88231
|
+
const body = keys.map((key) => [
|
|
88232
|
+
" {",
|
|
88233
|
+
` name: '${escapeString(key.name)}',`,
|
|
88234
|
+
` publicKey: '${escapeString(normalizePublicKey(key.publicKey))}',`,
|
|
88235
|
+
" },"
|
|
88236
|
+
].join(`
|
|
88237
|
+
`)).join(`
|
|
88238
|
+
`);
|
|
88239
|
+
return ` sshKeys: [
|
|
88240
|
+
${body}
|
|
88241
|
+
]`;
|
|
88242
|
+
}
|
|
88243
|
+
function dedupeKeys(keys) {
|
|
88244
|
+
const seen = new Map;
|
|
88245
|
+
for (const key of keys) {
|
|
88246
|
+
const name = normalizeKeyName(key.name);
|
|
88247
|
+
if (seen.has(name))
|
|
88248
|
+
throw new Error(`SSH key '${name}' already exists in compute.sshKeys`);
|
|
88249
|
+
seen.set(name, { name, publicKey: normalizePublicKey(key.publicKey) });
|
|
88250
|
+
}
|
|
88251
|
+
return [...seen.values()];
|
|
88252
|
+
}
|
|
88253
|
+
function normalizeKeyName(name) {
|
|
88254
|
+
const normalized = name.trim();
|
|
88255
|
+
if (!/^[\w@.+-]{2,80}$/.test(normalized))
|
|
88256
|
+
throw new Error("SSH key name must be 2-80 characters and use letters, numbers, @, ., +, _, or -.");
|
|
88257
|
+
return normalized;
|
|
88258
|
+
}
|
|
88259
|
+
function normalizePublicKey(publicKey) {
|
|
88260
|
+
const normalized = publicKey.trim().replace(/\s+/g, " ");
|
|
88261
|
+
const [type, body] = normalized.split(" ");
|
|
88262
|
+
if (!/^(?:ssh-(?:ed25519|rsa)|ecdsa-sha2-nistp(?:256|384|521))$/.test(type ?? "") || !body)
|
|
88263
|
+
throw new Error("Public key must be an OpenSSH public key, e.g. ssh-ed25519 AAAA... name@host.");
|
|
88264
|
+
return normalized;
|
|
88265
|
+
}
|
|
88266
|
+
function sshKeyType(publicKey) {
|
|
88267
|
+
return publicKey.trim().split(/\s+/)[0] || "ssh";
|
|
88268
|
+
}
|
|
88269
|
+
function sshFingerprint(publicKey) {
|
|
88270
|
+
const body = publicKey.trim().split(/\s+/)[1];
|
|
88271
|
+
if (!body)
|
|
88272
|
+
return "-";
|
|
88273
|
+
try {
|
|
88274
|
+
return `SHA256:${createHash11("sha256").update(Buffer.from(body, "base64")).digest("base64").replace(/=+$/, "")}`;
|
|
88275
|
+
} catch {
|
|
88276
|
+
return "-";
|
|
88277
|
+
}
|
|
88278
|
+
}
|
|
88279
|
+
function escapeString(value) {
|
|
88280
|
+
return value.replace(/\\/g, "\\\\").replaceAll(String.fromCharCode(39), "\\'");
|
|
88281
|
+
}
|
|
88282
|
+
function findPropertyObject(configText, property) {
|
|
88283
|
+
const match = new RegExp(`\\b${property}\\s*:\\s*{`, "g").exec(configText);
|
|
88284
|
+
if (!match)
|
|
88285
|
+
throw new Error(`Could not find ${property}: { ... } in cloud config.`);
|
|
88286
|
+
const start = configText.indexOf("{", match.index);
|
|
88287
|
+
return { propertyStart: match.index, start, end: findMatching(configText, start, "{", "}") };
|
|
88288
|
+
}
|
|
88289
|
+
function findPropertyArray(configText, start, end, property) {
|
|
88290
|
+
const body = configText.slice(start + 1, end);
|
|
88291
|
+
const match = new RegExp(`\\b${property}\\s*:\\s*\\[`, "g").exec(body);
|
|
88292
|
+
if (!match)
|
|
88293
|
+
return null;
|
|
88294
|
+
const propertyStart = start + 1 + match.index;
|
|
88295
|
+
const arrayStart = configText.indexOf("[", propertyStart);
|
|
88296
|
+
return { propertyStart, start: arrayStart, end: findMatching(configText, arrayStart, "[", "]") };
|
|
88297
|
+
}
|
|
88298
|
+
function findMatching(text, start, open, close) {
|
|
88299
|
+
let depth = 0;
|
|
88300
|
+
let quote;
|
|
88301
|
+
let escaped = false;
|
|
88302
|
+
let lineComment = false;
|
|
88303
|
+
let blockComment = false;
|
|
88304
|
+
for (let index = start;index < text.length; index++) {
|
|
88305
|
+
const char = text[index];
|
|
88306
|
+
const next = text[index + 1];
|
|
88307
|
+
if (lineComment) {
|
|
88308
|
+
if (char === `
|
|
88309
|
+
`)
|
|
88310
|
+
lineComment = false;
|
|
88311
|
+
continue;
|
|
88312
|
+
}
|
|
88313
|
+
if (blockComment) {
|
|
88314
|
+
if (char === "*" && next === "/") {
|
|
88315
|
+
blockComment = false;
|
|
88316
|
+
index++;
|
|
88317
|
+
}
|
|
88318
|
+
continue;
|
|
88319
|
+
}
|
|
88320
|
+
if (quote) {
|
|
88321
|
+
if (escaped) {
|
|
88322
|
+
escaped = false;
|
|
88323
|
+
continue;
|
|
88324
|
+
}
|
|
88325
|
+
if (char === "\\") {
|
|
88326
|
+
escaped = true;
|
|
88327
|
+
continue;
|
|
88328
|
+
}
|
|
88329
|
+
if (char === quote)
|
|
88330
|
+
quote = undefined;
|
|
88331
|
+
continue;
|
|
88332
|
+
}
|
|
88333
|
+
if (char === "/" && next === "/") {
|
|
88334
|
+
lineComment = true;
|
|
88335
|
+
index++;
|
|
88336
|
+
continue;
|
|
88337
|
+
}
|
|
88338
|
+
if (char === "/" && next === "*") {
|
|
88339
|
+
blockComment = true;
|
|
88340
|
+
index++;
|
|
88341
|
+
continue;
|
|
88342
|
+
}
|
|
88343
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
88344
|
+
quote = char;
|
|
88345
|
+
continue;
|
|
88346
|
+
}
|
|
88347
|
+
if (char === open)
|
|
88348
|
+
depth++;
|
|
88349
|
+
if (char === close) {
|
|
88350
|
+
depth--;
|
|
88351
|
+
if (depth === 0)
|
|
88352
|
+
return index;
|
|
88353
|
+
}
|
|
88354
|
+
}
|
|
88355
|
+
throw new Error(`Could not find matching ${close} in cloud config.`);
|
|
88356
|
+
}
|
|
88357
|
+
|
|
88358
|
+
// src/deploy/dashboard-data-server.ts
|
|
88359
|
+
var PROBED_SERVICES = ["rpx-gateway", "nginx", "php8.3-fpm", "php8.2-fpm", "mysql", "mariadb", "postgresql", "redis", "redis-server", "meilisearch"];
|
|
88360
|
+
var UNKNOWN = "-";
|
|
88361
|
+
function metricsScript() {
|
|
88362
|
+
return [
|
|
88363
|
+
"set +e",
|
|
88364
|
+
'echo "CPUS=$(nproc 2>/dev/null || echo 1)"',
|
|
88365
|
+
`echo "LOAD=$(cut -d' ' -f1 /proc/loadavg 2>/dev/null || echo 0)"`,
|
|
88366
|
+
`echo "MEMTOTAL=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}')"`,
|
|
88367
|
+
`echo "MEMUSED=$(free -m 2>/dev/null | awk '/^Mem:/{print $3}')"`,
|
|
88368
|
+
`echo "DISKPCT=$(df -P / 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')"`,
|
|
88369
|
+
`echo "DISKUSEDG=$(df -BG / 2>/dev/null | awk 'NR==2{gsub("G","",$3);print $3}')"`,
|
|
88370
|
+
`echo "DISKTOTG=$(df -BG / 2>/dev/null | awk 'NR==2{gsub("G","",$2);print $2}')"`,
|
|
88371
|
+
`echo "UPTIME=$(uptime -p 2>/dev/null | sed 's/^up //' || echo unknown)"`,
|
|
88372
|
+
'echo "OS=$(. /etc/os-release 2>/dev/null; echo "$PRETTY_NAME")"',
|
|
88373
|
+
`for s in ${PROBED_SERVICES.join(" ")}; do st=$(systemctl is-active "$s" 2>/dev/null); if [ -n "$st" ] && [ "$st" != "inactive" ] && [ "$st" != "unknown" ]; then mem=$(systemctl show "$s" -p MemoryCurrent --value 2>/dev/null); en=$(systemctl is-enabled "$s" 2>/dev/null); since=$(systemctl show "$s" -p ActiveEnterTimestamp --value 2>/dev/null); echo "SVC=$s=$st=$mem=$en=$since"; fi; done`,
|
|
88374
|
+
"true"
|
|
88375
|
+
];
|
|
88376
|
+
}
|
|
88377
|
+
function parseBlock(output) {
|
|
88378
|
+
const kv = { services: [] };
|
|
88379
|
+
for (const line of output.split(`
|
|
88380
|
+
`)) {
|
|
88381
|
+
const l = line.trim();
|
|
88382
|
+
if (l.startsWith("SVC=")) {
|
|
88383
|
+
const [, name, status, memBytes, enabled2, since] = /^SVC=([^=]+)=([^=]+)(?:=([^=]*)(?:=([^=]*)(?:=(.*))?)?)?$/.exec(l) ?? [];
|
|
88384
|
+
if (name) {
|
|
88385
|
+
kv.services.push({
|
|
88386
|
+
name,
|
|
88387
|
+
status,
|
|
88388
|
+
memBytes: Number(memBytes || 0),
|
|
88389
|
+
enabled: enabled2 || UNKNOWN,
|
|
88390
|
+
since: since || UNKNOWN
|
|
88391
|
+
});
|
|
88392
|
+
}
|
|
88393
|
+
} else {
|
|
88394
|
+
const eq = l.indexOf("=");
|
|
88395
|
+
if (eq > 0)
|
|
88396
|
+
kv[l.slice(0, eq)] = l.slice(eq + 1);
|
|
88397
|
+
}
|
|
88398
|
+
}
|
|
88399
|
+
return kv;
|
|
88400
|
+
}
|
|
88401
|
+
function configuredServices(config6) {
|
|
88402
|
+
const compute = config6.infrastructure?.compute;
|
|
88403
|
+
const services = [];
|
|
88404
|
+
if (compute?.webServer === "rpx" || compute?.proxy?.engine === "rpx")
|
|
88405
|
+
services.push({ name: "rpx-gateway", status: "configured" });
|
|
88406
|
+
else if (compute?.webServer === "nginx" || compute?.webServer == null)
|
|
88407
|
+
services.push({ name: "ts-cloud-nginx", status: "configured" });
|
|
88408
|
+
const phpVersions = compute?.php?.versions ?? (compute?.runtime === "php" ? ["8.3"] : []);
|
|
88409
|
+
for (const version2 of phpVersions)
|
|
88410
|
+
services.push({ name: `php${version2}-fpm`, status: "configured" });
|
|
88411
|
+
const managed = compute?.managedServices ?? {};
|
|
88412
|
+
const serviceNames = [
|
|
88413
|
+
["mysql", "mysql"],
|
|
88414
|
+
["mariadb", "mariadb"],
|
|
88415
|
+
["postgres", "postgresql"],
|
|
88416
|
+
["redis", "redis"],
|
|
88417
|
+
["memcached", "memcached"],
|
|
88418
|
+
["meilisearch", "meilisearch"]
|
|
88419
|
+
];
|
|
88420
|
+
for (const [key, name] of serviceNames) {
|
|
88421
|
+
if (managed[key])
|
|
88422
|
+
services.push({ name, status: "configured" });
|
|
88423
|
+
}
|
|
88424
|
+
return services;
|
|
88425
|
+
}
|
|
88426
|
+
function configuredBackup(config6) {
|
|
88427
|
+
const backup = config6.infrastructure?.compute?.backups;
|
|
88428
|
+
const enabled2 = !!backup?.enabled;
|
|
88429
|
+
const schedule = backup?.schedule ?? "0 2 * * *";
|
|
88430
|
+
return {
|
|
88431
|
+
enabled: enabled2,
|
|
88432
|
+
schedule,
|
|
88433
|
+
destination: enabled2 ? backup?.bucket ?? "local" : "off",
|
|
88434
|
+
retention: enabled2 ? backup?.retentionCount ?? 5 : 0,
|
|
88435
|
+
last: enabled2 ? "pending first run" : "not configured",
|
|
88436
|
+
size: enabled2 ? UNKNOWN : "0 MB"
|
|
88437
|
+
};
|
|
88438
|
+
}
|
|
88439
|
+
function configuredProvider(config6) {
|
|
88440
|
+
return config6.infrastructure?.compute?.provider ?? config6.cloud?.provider ?? config6.provider ?? "aws";
|
|
88441
|
+
}
|
|
88442
|
+
function configuredRegion(config6) {
|
|
88443
|
+
const provider = configuredProvider(config6);
|
|
88444
|
+
if (provider === "hetzner") {
|
|
88445
|
+
return config6.hetzner?.location ?? process.env.HCLOUD_LOCATION ?? process.env.HETZNER_LOCATION ?? "fsn1";
|
|
88446
|
+
}
|
|
88447
|
+
return config6.project.region ?? "us-east-1";
|
|
88448
|
+
}
|
|
88449
|
+
function loadLocalState(config6, environment) {
|
|
88450
|
+
const statePath = join21(process.cwd(), ".ts-cloud", "state", `${config6.project.slug}-${environment}.json`);
|
|
88451
|
+
if (!existsSync24(statePath))
|
|
88452
|
+
return null;
|
|
88453
|
+
try {
|
|
88454
|
+
return JSON.parse(readFileSync17(statePath, "utf8"));
|
|
88455
|
+
} catch {
|
|
88456
|
+
return null;
|
|
88457
|
+
}
|
|
88458
|
+
}
|
|
88459
|
+
function configuredWorkers(config6) {
|
|
88460
|
+
const sites = config6.sites ?? {};
|
|
88461
|
+
const workers = [];
|
|
88462
|
+
for (const [siteName, site] of Object.entries(sites)) {
|
|
88463
|
+
const queues = site.queues ?? site.workers;
|
|
88464
|
+
if (Array.isArray(queues)) {
|
|
88465
|
+
for (const queue of queues) {
|
|
88466
|
+
if (typeof queue === "string")
|
|
88467
|
+
workers.push({ name: `${siteName}:${queue}`, site: siteName, processes: 1, status: "configured" });
|
|
88468
|
+
else if (queue?.name)
|
|
88469
|
+
workers.push({ name: `${siteName}:${queue.name}`, site: siteName, processes: queue.processes ?? 1, status: "configured" });
|
|
88470
|
+
}
|
|
88471
|
+
}
|
|
88472
|
+
}
|
|
88473
|
+
return workers;
|
|
88474
|
+
}
|
|
88475
|
+
function normalizeSitePath(path) {
|
|
88476
|
+
if (!path || path === "")
|
|
88477
|
+
return "/";
|
|
88478
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
88479
|
+
}
|
|
88480
|
+
function siteRoute(site) {
|
|
88481
|
+
const path = normalizeSitePath(site.path);
|
|
88482
|
+
if (!site.domain)
|
|
88483
|
+
return { route: "internal" };
|
|
88484
|
+
return {
|
|
88485
|
+
route: path === "/" ? site.domain : `${site.domain}${path}`,
|
|
88486
|
+
href: `https://${site.domain}${path === "/" ? "" : path}`
|
|
88487
|
+
};
|
|
88488
|
+
}
|
|
88489
|
+
function siteKindLabel(name, site) {
|
|
88490
|
+
const kind = resolveSiteKind(site);
|
|
88491
|
+
const build = String(site.build ?? "").toLowerCase();
|
|
88492
|
+
const start = String(site.start ?? "").toLowerCase();
|
|
88493
|
+
if (name === "main" || start.includes("buddy/src/cli.ts serve"))
|
|
88494
|
+
return "stacks";
|
|
88495
|
+
if (name === "api" || start.includes("/serve/api"))
|
|
88496
|
+
return "api";
|
|
88497
|
+
if (build.includes("buildblog"))
|
|
88498
|
+
return "bunpress blog";
|
|
88499
|
+
if (build.includes("bunpress"))
|
|
88500
|
+
return "bunpress";
|
|
88501
|
+
if (build.includes("site:build") || build.includes("stx"))
|
|
88502
|
+
return "stx static";
|
|
88503
|
+
if (kind === "server-static")
|
|
88504
|
+
return site.spa ? "spa" : "static";
|
|
88505
|
+
if (kind === "server-app")
|
|
88506
|
+
return "app";
|
|
88507
|
+
if (kind === "server-php")
|
|
88508
|
+
return site.type ?? "php";
|
|
88509
|
+
return "bucket";
|
|
88510
|
+
}
|
|
88511
|
+
function siteRuntime(site) {
|
|
88512
|
+
const kind = resolveSiteKind(site);
|
|
88513
|
+
const command = `${site.start ?? ""} ${site.build ?? ""}`.toLowerCase();
|
|
88514
|
+
if (kind === "server-static")
|
|
88515
|
+
return command.includes("bunpress") || command.includes("bun ") || command.includes("bunx ") ? "static/bun" : "static";
|
|
88516
|
+
if (kind === "server-app")
|
|
88517
|
+
return command.includes("bun") ? "bun" : "node";
|
|
88518
|
+
if (kind === "server-php")
|
|
88519
|
+
return `php ${site.php ?? site.phpVersion ?? "8.3"}`;
|
|
88520
|
+
return "static";
|
|
88521
|
+
}
|
|
88522
|
+
function siteDeployLabel(site) {
|
|
88523
|
+
const kind = resolveSiteKind(site);
|
|
88524
|
+
if (kind === "server-app")
|
|
88525
|
+
return "service";
|
|
88526
|
+
if (kind === "server-static")
|
|
88527
|
+
return "server static";
|
|
88528
|
+
if (kind === "server-php")
|
|
88529
|
+
return "php release";
|
|
88530
|
+
return "bucket";
|
|
88531
|
+
}
|
|
88532
|
+
function configuredSites(config6) {
|
|
88533
|
+
const seenRoutes = new Map;
|
|
88534
|
+
return Object.entries(config6.sites ?? {}).map(([name, site]) => {
|
|
88535
|
+
const path = normalizeSitePath(site.path);
|
|
88536
|
+
const { route, href } = siteRoute(site);
|
|
88537
|
+
const routeKey = site.domain ? `${site.domain}${path}` : "";
|
|
88538
|
+
const shadowedBy = routeKey ? seenRoutes.get(routeKey) : undefined;
|
|
88539
|
+
if (routeKey && !shadowedBy)
|
|
88540
|
+
seenRoutes.set(routeKey, name);
|
|
88541
|
+
return {
|
|
88542
|
+
name,
|
|
88543
|
+
route,
|
|
88544
|
+
href,
|
|
88545
|
+
domain: site.domain ?? "internal",
|
|
88546
|
+
path,
|
|
88547
|
+
kind: siteKindLabel(name, site),
|
|
88548
|
+
type: siteKindLabel(name, site),
|
|
88549
|
+
runtime: siteRuntime(site),
|
|
88550
|
+
deploy: siteDeployLabel(site),
|
|
88551
|
+
tls: site.domain ? site.ssl === false ? "http" : "https" : "loopback",
|
|
88552
|
+
ssl: site.domain ? site.ssl !== false : false,
|
|
88553
|
+
status: shadowedBy ? "shadowed" : "live",
|
|
88554
|
+
shadowedBy,
|
|
88555
|
+
lastDeploy: UNKNOWN
|
|
88556
|
+
};
|
|
88557
|
+
});
|
|
88558
|
+
}
|
|
88559
|
+
function shellSingleQuote(value) {
|
|
88560
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
88561
|
+
}
|
|
88562
|
+
function sedReplacement(value) {
|
|
88563
|
+
return value.replace(/[\\&/]/g, "\\$&");
|
|
88564
|
+
}
|
|
88565
|
+
function deployHistoryScript(siteNames) {
|
|
88566
|
+
if (siteNames.length === 0)
|
|
88567
|
+
return ["true"];
|
|
88568
|
+
return [
|
|
88569
|
+
"set +e",
|
|
88570
|
+
...siteNames.map((siteName) => {
|
|
88571
|
+
const historyPath = shellSingleQuote(`/var/www/${siteName}/.ts-cloud/deploy-history.log`);
|
|
88572
|
+
const prefix = sedReplacement(siteName);
|
|
88573
|
+
return `TS_CLOUD_HISTORY=${historyPath}; { [ -f "$TS_CLOUD_HISTORY" ] && tail -n 24 "$TS_CLOUD_HISTORY" | sed "s/^/DEPLOY=${prefix}\\t/"; } || true`;
|
|
88574
|
+
}),
|
|
88575
|
+
"true"
|
|
88576
|
+
];
|
|
88577
|
+
}
|
|
88578
|
+
function siteDomains(config6) {
|
|
88579
|
+
return [...new Set(Object.values(config6.sites ?? {}).map((site) => site.domain).filter((domain) => typeof domain === "string" && domain.length > 0))];
|
|
88580
|
+
}
|
|
88581
|
+
function securityScript(config6) {
|
|
88582
|
+
const domains = siteDomains(config6);
|
|
88583
|
+
return [
|
|
88584
|
+
"set +e",
|
|
88585
|
+
'if command -v ss >/dev/null 2>&1; then ss -H -lntup 2>/dev/null | while read -r proto _state _recv _send local _peer rest; do [ -n "$proto" ] && [ -n "$local" ] && printf "PORT=%s\\t%s\\t%s\\n" "$proto" "$local" "$rest"; done; fi',
|
|
88586
|
+
'if command -v ufw >/dev/null 2>&1; then ufw status numbered 2>/dev/null | sed "s/^/FIREWALL=/"; else echo "FIREWALL=ufw unavailable"; fi',
|
|
88587
|
+
'journalctl _COMM=sshd --no-pager -n 30 -o short-iso 2>/dev/null | sed "s/^/AUTH=/" || true',
|
|
88588
|
+
...domains.map((domain) => {
|
|
88589
|
+
const quoted = shellSingleQuote(domain);
|
|
88590
|
+
return `TS_CLOUD_DOMAIN=${quoted}; TS_CLOUD_EXPIRY=$(echo | timeout 8 openssl s_client -servername "$TS_CLOUD_DOMAIN" -connect "$TS_CLOUD_DOMAIN:443" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2-); [ -n "$TS_CLOUD_EXPIRY" ] && printf "CERT=%s\\t%s\\n" "$TS_CLOUD_DOMAIN" "$TS_CLOUD_EXPIRY" || printf "CERT=%s\\tunavailable\\n" "$TS_CLOUD_DOMAIN"`;
|
|
88591
|
+
}),
|
|
88592
|
+
"true"
|
|
88593
|
+
];
|
|
88594
|
+
}
|
|
88595
|
+
function serverLogSources(config6) {
|
|
88596
|
+
const byLabel = new Map;
|
|
88597
|
+
const add = (pattern, label) => {
|
|
88598
|
+
if (pattern && label && !byLabel.has(label))
|
|
88599
|
+
byLabel.set(label, { pattern, label });
|
|
88600
|
+
};
|
|
88601
|
+
const compute = config6.infrastructure?.compute;
|
|
88602
|
+
const slug = config6.project.slug;
|
|
88603
|
+
if (compute?.webServer === "rpx" || compute?.proxy?.engine === "rpx")
|
|
88604
|
+
add("rpx-gateway", "rpx-gateway");
|
|
88605
|
+
else
|
|
88606
|
+
add("nginx", "nginx");
|
|
88607
|
+
for (const svc of configuredServices(config6))
|
|
88608
|
+
add(svc.name, svc.name);
|
|
88609
|
+
for (const [siteName, site] of Object.entries(config6.sites ?? {})) {
|
|
88610
|
+
if (resolveSiteKind(site) !== "server-app")
|
|
88611
|
+
continue;
|
|
88612
|
+
add(`${slug}-${siteName}`, `${slug}-${siteName}`);
|
|
88613
|
+
if (Array.isArray(site.queues ?? site.workers) && (site.queues ?? site.workers).length)
|
|
88614
|
+
add(`${slug}-${siteName}-queue-*`, `${slug}-${siteName}-queues`);
|
|
88615
|
+
if (Array.isArray(site.daemons) && site.daemons.length)
|
|
88616
|
+
add(`${slug}-${siteName}-daemon-*`, `${slug}-${siteName}-daemons`);
|
|
88617
|
+
}
|
|
88618
|
+
return [...byLabel.values()];
|
|
88619
|
+
}
|
|
88620
|
+
function serverLogsScript(config6) {
|
|
88621
|
+
const sources = serverLogSources(config6);
|
|
88622
|
+
if (sources.length === 0)
|
|
88623
|
+
return ["true"];
|
|
88624
|
+
return [
|
|
88625
|
+
"set +e",
|
|
88626
|
+
...sources.map((source) => {
|
|
88627
|
+
const quoted = shellSingleQuote(source.pattern);
|
|
88628
|
+
const prefix = sedReplacement(source.label);
|
|
88629
|
+
return `journalctl -u ${quoted} --no-pager -n 150 -o short-iso 2>/dev/null | sed "s/^/LOG=${prefix}\\t/" || true`;
|
|
88630
|
+
}),
|
|
88631
|
+
`[ -f /var/log/ts-cloud-backup.log ] && tail -n 60 /var/log/ts-cloud-backup.log 2>/dev/null | sed "s/^/LOG=backups\\t/" || true`,
|
|
88632
|
+
"true"
|
|
88633
|
+
];
|
|
88634
|
+
}
|
|
88635
|
+
function relativeTime(iso) {
|
|
88636
|
+
const date = new Date(iso);
|
|
88637
|
+
if (Number.isNaN(date.getTime()))
|
|
88638
|
+
return iso || UNKNOWN;
|
|
88639
|
+
const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));
|
|
88640
|
+
if (seconds < 60)
|
|
88641
|
+
return `${seconds}s ago`;
|
|
88642
|
+
const minutes = Math.round(seconds / 60);
|
|
88643
|
+
if (minutes < 60)
|
|
88644
|
+
return `${minutes}m ago`;
|
|
88645
|
+
const hours = Math.round(minutes / 60);
|
|
88646
|
+
if (hours < 48)
|
|
88647
|
+
return `${hours}h ago`;
|
|
88648
|
+
const days = Math.round(hours / 24);
|
|
88649
|
+
return `${days}d ago`;
|
|
88650
|
+
}
|
|
88651
|
+
function inferLogLevel(message) {
|
|
88652
|
+
if (/(?:error|failed|panic|fatal|exception|denied|unhealthy)/i.test(message))
|
|
88653
|
+
return "error";
|
|
88654
|
+
if (/(?:warn|warning|retry|restart|deprecated|timeout)/i.test(message))
|
|
88655
|
+
return "warn";
|
|
88656
|
+
return "info";
|
|
88657
|
+
}
|
|
88658
|
+
function parseServerLogs(output) {
|
|
88659
|
+
const records = [];
|
|
88660
|
+
for (const rawLine of output.split(`
|
|
88661
|
+
`)) {
|
|
88662
|
+
const line = rawLine.trim();
|
|
88663
|
+
if (!line.startsWith("LOG="))
|
|
88664
|
+
continue;
|
|
88665
|
+
const tab = line.indexOf("\t");
|
|
88666
|
+
if (tab < 0)
|
|
88667
|
+
continue;
|
|
88668
|
+
const source = line.slice(4, tab);
|
|
88669
|
+
const raw = line.slice(tab + 1);
|
|
88670
|
+
const match = /^(\d{4}-\d{2}-\d{2}T[^\s]+)\s+(\S+)\s+(.*)$/.exec(raw);
|
|
88671
|
+
const timestamp = match?.[1] ?? "";
|
|
88672
|
+
const host = match?.[2] ?? "";
|
|
88673
|
+
const message = (match?.[3] ?? raw).trim();
|
|
88674
|
+
if (!message)
|
|
88675
|
+
continue;
|
|
88676
|
+
records.push({
|
|
88677
|
+
source,
|
|
88678
|
+
timestamp,
|
|
88679
|
+
when: timestamp ? relativeTime(timestamp) : UNKNOWN,
|
|
88680
|
+
host,
|
|
88681
|
+
message,
|
|
88682
|
+
level: inferLogLevel(message)
|
|
88683
|
+
});
|
|
88684
|
+
}
|
|
88685
|
+
return records.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp))).slice(0, 600);
|
|
88686
|
+
}
|
|
88687
|
+
function listenerExposure(listen) {
|
|
88688
|
+
if (/^(?:127\.|localhost:|\[?::1\]?)/.test(listen))
|
|
88689
|
+
return "loopback";
|
|
88690
|
+
if (/^(?:10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|169\.254\.|\[?f[cd][0-9a-f:])/i.test(listen))
|
|
88691
|
+
return "private";
|
|
88692
|
+
return "public";
|
|
88693
|
+
}
|
|
88694
|
+
function portTone(listen) {
|
|
88695
|
+
const exposure = listenerExposure(listen);
|
|
88696
|
+
if (exposure !== "public")
|
|
88697
|
+
return "ok";
|
|
88698
|
+
if (/:(?:22|80|443)$/.test(listen) || /^\*:(?:22|80|443)$/.test(listen))
|
|
88699
|
+
return "ok";
|
|
88700
|
+
return "warn";
|
|
88701
|
+
}
|
|
88702
|
+
function certStatus(daysRemaining) {
|
|
88703
|
+
if (daysRemaining == null)
|
|
88704
|
+
return "warn";
|
|
88705
|
+
if (daysRemaining < 8)
|
|
88706
|
+
return "bad";
|
|
88707
|
+
if (daysRemaining < 30)
|
|
88708
|
+
return "warn";
|
|
88709
|
+
return "ok";
|
|
88710
|
+
}
|
|
88711
|
+
function parseServerSecurity(output) {
|
|
88712
|
+
const ports = [];
|
|
88713
|
+
const firewallLines = [];
|
|
88714
|
+
const authEvents = [];
|
|
88715
|
+
const tlsCertificates = [];
|
|
88716
|
+
for (const rawLine of output.split(`
|
|
88717
|
+
`)) {
|
|
88718
|
+
const line = rawLine.trim();
|
|
88719
|
+
if (line.startsWith("PORT=")) {
|
|
88720
|
+
const [proto = "", listen = "", process21 = ""] = line.slice(5).split("\t");
|
|
88721
|
+
if (proto && listen) {
|
|
88722
|
+
ports.push({
|
|
88723
|
+
proto,
|
|
88724
|
+
listen,
|
|
88725
|
+
processName: process21 || UNKNOWN,
|
|
88726
|
+
exposure: listenerExposure(listen),
|
|
88727
|
+
tone: portTone(listen)
|
|
88728
|
+
});
|
|
88729
|
+
}
|
|
88730
|
+
} else if (line.startsWith("FIREWALL=")) {
|
|
88731
|
+
const value = line.slice(9).trim();
|
|
88732
|
+
if (value)
|
|
88733
|
+
firewallLines.push(value);
|
|
88734
|
+
} else if (line.startsWith("AUTH=")) {
|
|
88735
|
+
const raw = line.slice(5);
|
|
88736
|
+
const match = /^(\d{4}-\d{2}-\d{2}T[^\s]+)\s+(\S+)\s+(.*)$/.exec(raw);
|
|
88737
|
+
const message = (match?.[3] ?? raw).trim();
|
|
88738
|
+
if (message) {
|
|
88739
|
+
authEvents.push({
|
|
88740
|
+
timestamp: match?.[1] ?? "",
|
|
88741
|
+
when: match?.[1] ? relativeTime(match[1]) : UNKNOWN,
|
|
88742
|
+
host: match?.[2] ?? "",
|
|
88743
|
+
message,
|
|
88744
|
+
level: inferLogLevel(message)
|
|
88745
|
+
});
|
|
88746
|
+
}
|
|
88747
|
+
} else if (line.startsWith("CERT=")) {
|
|
88748
|
+
const [domain = "", expiresRaw = ""] = line.slice(5).split("\t");
|
|
88749
|
+
if (!domain)
|
|
88750
|
+
continue;
|
|
88751
|
+
const expiresAt = new Date(expiresRaw);
|
|
88752
|
+
const daysRemaining = Number.isNaN(expiresAt.getTime()) ? null : Math.ceil((expiresAt.getTime() - Date.now()) / 86400000);
|
|
88753
|
+
tlsCertificates.push({
|
|
88754
|
+
domain,
|
|
88755
|
+
expires: Number.isNaN(expiresAt.getTime()) ? expiresRaw || UNKNOWN : expiresAt.toISOString().slice(0, 10),
|
|
88756
|
+
daysRemaining,
|
|
88757
|
+
status: certStatus(daysRemaining)
|
|
88758
|
+
});
|
|
88759
|
+
}
|
|
88760
|
+
}
|
|
88761
|
+
const firewallEnabled = firewallLines.some((line) => /Status:\s*active/i.test(line));
|
|
88762
|
+
const firewallUnavailable = firewallLines.some((line) => /unavailable/i.test(line));
|
|
88763
|
+
return {
|
|
88764
|
+
ports: ports.slice(0, 80),
|
|
88765
|
+
firewall: {
|
|
88766
|
+
status: firewallEnabled ? "active" : firewallUnavailable ? "unavailable" : "inactive",
|
|
88767
|
+
summary: firewallEnabled ? "ufw active" : firewallUnavailable ? "ufw unavailable" : "ufw inactive or not configured",
|
|
88768
|
+
rules: firewallLines.filter((line) => line && !/^Status:/i.test(line)).slice(0, 60)
|
|
88769
|
+
},
|
|
88770
|
+
tlsCertificates,
|
|
88771
|
+
authEvents: authEvents.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp))).slice(0, 30)
|
|
88772
|
+
};
|
|
88773
|
+
}
|
|
88774
|
+
function configuredSecurity(config6) {
|
|
88775
|
+
const compute = config6.infrastructure?.compute;
|
|
88776
|
+
const firewall = compute?.firewall ?? {};
|
|
88777
|
+
const allowedPorts = [...new Set([22, 80, 443, ...firewall.allowedPorts ?? []])];
|
|
88778
|
+
const domains = siteDomains(config6);
|
|
88779
|
+
return {
|
|
88780
|
+
ports: allowedPorts.map((port) => ({
|
|
88781
|
+
proto: "tcp",
|
|
88782
|
+
listen: `0.0.0.0:${port}`,
|
|
88783
|
+
processName: "configured firewall",
|
|
88784
|
+
exposure: "public",
|
|
88785
|
+
tone: [22, 80, 443].includes(Number(port)) ? "ok" : "warn"
|
|
88786
|
+
})),
|
|
88787
|
+
firewall: {
|
|
88788
|
+
status: firewall.enabled === false ? "disabled" : "configured",
|
|
88789
|
+
summary: firewall.enabled === false ? "host firewall disabled in config" : "host firewall configured declaratively",
|
|
88790
|
+
rules: allowedPorts.map((port) => `ALLOW ${port}/tcp`)
|
|
88791
|
+
},
|
|
88792
|
+
tlsCertificates: domains.map((domain) => ({
|
|
88793
|
+
domain,
|
|
88794
|
+
expires: UNKNOWN,
|
|
88795
|
+
daysRemaining: null,
|
|
88796
|
+
status: "warn"
|
|
88797
|
+
})),
|
|
88798
|
+
authEvents: []
|
|
88799
|
+
};
|
|
88800
|
+
}
|
|
88801
|
+
function diagnosticChecks(config6, data) {
|
|
88802
|
+
const security = data.security ?? configuredSecurity(config6);
|
|
88803
|
+
const live = !!data._serverReachable && !data.metricsUnavailable;
|
|
88804
|
+
const sites = data.sites ?? [];
|
|
88805
|
+
const shadowed = sites.filter((site) => site.status === "shadowed");
|
|
88806
|
+
const failedServices = (data.services ?? []).filter((service) => ["failed", "stopped"].includes(service.status));
|
|
88807
|
+
const expiringCerts = (security.tlsCertificates ?? []).filter((cert) => cert.status !== "ok");
|
|
88808
|
+
const sshKeys = data.sshKeys ?? [];
|
|
88809
|
+
return [
|
|
88810
|
+
{
|
|
88811
|
+
name: "Live server probe",
|
|
88812
|
+
status: live ? "pass" : "warn",
|
|
88813
|
+
detail: live ? "Metrics and remote checks are coming from the compute box." : "The dashboard is rendering config/state data until the box probe succeeds."
|
|
88814
|
+
},
|
|
88815
|
+
{
|
|
88816
|
+
name: "Managed services",
|
|
88817
|
+
status: failedServices.length ? "fail" : "pass",
|
|
88818
|
+
detail: failedServices.length ? `${failedServices.length} service(s) need attention.` : `${(data.services ?? []).length} service(s) reported healthy or configured.`
|
|
88819
|
+
},
|
|
88820
|
+
{
|
|
88821
|
+
name: "Route conflicts",
|
|
88822
|
+
status: shadowed.length ? "warn" : "pass",
|
|
88823
|
+
detail: shadowed.length ? `${shadowed.map((site) => site.name).join(", ")} is shadowed by an earlier route.` : `${sites.length} site route(s) are unshadowed.`
|
|
88824
|
+
},
|
|
88825
|
+
{
|
|
88826
|
+
name: "SSH access",
|
|
88827
|
+
status: sshKeys.length ? "pass" : "warn",
|
|
88828
|
+
detail: sshKeys.length ? `${sshKeys.length} declarative authorized key(s) configured.` : "No declarative SSH keys are configured."
|
|
88829
|
+
},
|
|
88830
|
+
{
|
|
88831
|
+
name: "Firewall",
|
|
88832
|
+
status: ["active", "configured"].includes(security.firewall?.status) ? "pass" : "warn",
|
|
88833
|
+
detail: security.firewall?.summary ?? "Firewall status unavailable."
|
|
88834
|
+
},
|
|
88835
|
+
{
|
|
88836
|
+
name: "TLS certificates",
|
|
88837
|
+
status: expiringCerts.length ? "warn" : "pass",
|
|
88838
|
+
detail: expiringCerts.length ? `${expiringCerts.length} certificate(s) need renewal visibility.` : `${(security.tlsCertificates ?? []).length} certificate(s) look healthy.`
|
|
88839
|
+
}
|
|
88840
|
+
];
|
|
88841
|
+
}
|
|
88842
|
+
function activityFeed(data) {
|
|
88843
|
+
const activity = [];
|
|
88844
|
+
for (const deploy of data.serverDeploymentsDetail ?? data.serverDeployments ?? []) {
|
|
88845
|
+
activity.push({
|
|
88846
|
+
type: "deploy",
|
|
88847
|
+
tone: deploy.status === "failed" ? "bad" : "ok",
|
|
88848
|
+
title: `${deploy.site} deployed ${deploy.sha}`,
|
|
88849
|
+
detail: `${deploy.branch} · ${deploy.status}`,
|
|
88850
|
+
when: deploy.when,
|
|
88851
|
+
timestamp: deploy.timestamp
|
|
88852
|
+
});
|
|
88853
|
+
}
|
|
88854
|
+
for (const log4 of data.serverLogs ?? []) {
|
|
88855
|
+
if (log4.level === "error" || log4.level === "warn") {
|
|
88856
|
+
activity.push({
|
|
88857
|
+
type: "log",
|
|
88858
|
+
tone: log4.level === "error" ? "bad" : "warn",
|
|
88859
|
+
title: `${log4.source} ${log4.level}`,
|
|
88860
|
+
detail: log4.message,
|
|
88861
|
+
when: log4.when,
|
|
88862
|
+
timestamp: log4.timestamp
|
|
88863
|
+
});
|
|
88864
|
+
}
|
|
88865
|
+
}
|
|
88866
|
+
for (const key of data.sshKeys ?? []) {
|
|
88867
|
+
activity.push({
|
|
88868
|
+
type: "ssh",
|
|
88869
|
+
tone: "ok",
|
|
88870
|
+
title: `${key.name} authorized`,
|
|
88871
|
+
detail: key.fingerprint,
|
|
88872
|
+
when: key.added ?? UNKNOWN,
|
|
88873
|
+
timestamp: ""
|
|
88874
|
+
});
|
|
88875
|
+
}
|
|
88876
|
+
return activity.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp))).slice(0, 80);
|
|
88877
|
+
}
|
|
88878
|
+
function parseDeployHistory(output, sites = {}) {
|
|
88879
|
+
const records = [];
|
|
88880
|
+
for (const rawLine of output.split(`
|
|
88881
|
+
`)) {
|
|
88882
|
+
const line = rawLine.trim();
|
|
88883
|
+
if (!line.startsWith("DEPLOY="))
|
|
88884
|
+
continue;
|
|
88885
|
+
const parts = line.split("\t");
|
|
88886
|
+
const site = parts[0]?.replace(/^DEPLOY=/, "") ?? "";
|
|
88887
|
+
const [timestamp, releaseId, commit, status, rcPart] = parts.slice(1);
|
|
88888
|
+
if (!site || !timestamp || !releaseId)
|
|
88889
|
+
continue;
|
|
88890
|
+
const siteConfig = sites[site] ?? {};
|
|
88891
|
+
const kind = resolveSiteKind(siteConfig);
|
|
88892
|
+
const sha = (commit || releaseId).slice(0, 7);
|
|
88893
|
+
records.push({
|
|
88894
|
+
sha,
|
|
88895
|
+
release: releaseId,
|
|
88896
|
+
commit: commit || releaseId,
|
|
88897
|
+
site,
|
|
88898
|
+
branch: siteConfig.branch ?? (kind === "server-static" ? "build artifact" : "main"),
|
|
88899
|
+
status: status || "unknown",
|
|
88900
|
+
when: relativeTime(timestamp),
|
|
88901
|
+
timestamp,
|
|
88902
|
+
took: "-",
|
|
88903
|
+
by: "ts-cloud",
|
|
88904
|
+
rc: rcPart?.replace(/^rc=/, "") ?? "",
|
|
88905
|
+
steps: kind === "server-static" ? ["upload artifact", "publish static files"] : ["upload artifact", "restart service"]
|
|
88906
|
+
});
|
|
88907
|
+
}
|
|
88908
|
+
return records.sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)));
|
|
88909
|
+
}
|
|
88910
|
+
function resolveConfigOnlyServerDashboardData(config6, environment) {
|
|
88911
|
+
const state = loadLocalState(config6, environment);
|
|
88912
|
+
const services = configuredServices(config6);
|
|
88913
|
+
const sites = configuredSites(config6);
|
|
88914
|
+
const diskSize = Math.max(1, Number(config6.infrastructure?.compute?.disk?.size ?? 1));
|
|
88915
|
+
const out = {
|
|
88916
|
+
server: {
|
|
88917
|
+
name: state?.serverName ?? `${config6.project.slug}-${environment}-app`,
|
|
88918
|
+
provider: state?.provider ?? configuredProvider(config6),
|
|
88919
|
+
region: configuredRegion(config6),
|
|
88920
|
+
ip: state?.publicIp ?? UNKNOWN,
|
|
88921
|
+
os: "Linux",
|
|
88922
|
+
uptime: UNKNOWN,
|
|
88923
|
+
probeStatus: "unavailable"
|
|
88924
|
+
},
|
|
88925
|
+
systemMetrics: {
|
|
88926
|
+
load: 0,
|
|
88927
|
+
cpus: Math.max(1, Number(config6.infrastructure?.compute?.instances ?? 1)),
|
|
88928
|
+
memUsedMb: 0,
|
|
88929
|
+
memTotalMb: 0,
|
|
88930
|
+
diskUsedPct: 0,
|
|
88931
|
+
diskUsedGb: 0,
|
|
88932
|
+
diskTotalGb: diskSize
|
|
88933
|
+
},
|
|
88934
|
+
metricsUnavailable: true,
|
|
88935
|
+
services,
|
|
88936
|
+
servicesDetail: services.map((s) => ({ ...s, since: UNKNOWN, memMb: 0, auto: true })),
|
|
88937
|
+
backup: configuredBackup(config6),
|
|
88938
|
+
backupHistory: [],
|
|
88939
|
+
workers: configuredWorkers(config6),
|
|
88940
|
+
serverScheduler: { enabled: false, lastRun: "not configured" },
|
|
88941
|
+
serverDeployments: [],
|
|
88942
|
+
serverDeploymentsDetail: [],
|
|
88943
|
+
deploymentsEmptyReason: "No deployment history has been recorded yet. Future server deploys will write /var/www/<site>/.ts-cloud/deploy-history.log.",
|
|
88944
|
+
serverLogs: [],
|
|
88945
|
+
serverLogsEmptyReason: "Live server logs are unavailable until the dashboard can reach the compute box.",
|
|
88946
|
+
security: configuredSecurity(config6),
|
|
88947
|
+
securityEmptyReason: "Live security checks are unavailable until the dashboard can reach the compute box.",
|
|
88948
|
+
sites,
|
|
88949
|
+
sitesDetail: sites.map((s) => {
|
|
88950
|
+
const site = config6.sites?.[s.name] ?? {};
|
|
88951
|
+
const kind = resolveSiteKind(site);
|
|
88952
|
+
return {
|
|
88953
|
+
...s,
|
|
88954
|
+
root: kind === "server-static" ? `/var/www/${s.name}` : `/var/www/${s.name}/current`,
|
|
88955
|
+
branch: kind === "server-static" ? "build artifact" : "main",
|
|
88956
|
+
build: site.build,
|
|
88957
|
+
envKeys: Object.keys(site.env ?? site.environment ?? {})
|
|
88958
|
+
};
|
|
88959
|
+
}),
|
|
88960
|
+
sshKeys: describeSshKeys(config6.infrastructure?.compute?.sshKeys ?? []),
|
|
88961
|
+
_serverReachable: false,
|
|
88962
|
+
_metricsStatus: "unavailable"
|
|
88963
|
+
};
|
|
88964
|
+
out.diagnostics = diagnosticChecks(config6, out);
|
|
88965
|
+
out.activity = activityFeed(out);
|
|
88966
|
+
return out;
|
|
88967
|
+
}
|
|
88968
|
+
async function resolveServerDashboardData(config6, environment) {
|
|
88969
|
+
if (!config6.infrastructure?.compute)
|
|
88970
|
+
return null;
|
|
88971
|
+
let driver = null;
|
|
88972
|
+
try {
|
|
88973
|
+
driver = createCloudDriver({ config: config6 });
|
|
88974
|
+
} catch {
|
|
88975
|
+
driver = null;
|
|
88976
|
+
}
|
|
88977
|
+
let parsed = null;
|
|
88978
|
+
let instanceCount = 0;
|
|
88979
|
+
let targets = [];
|
|
88980
|
+
if (driver)
|
|
88981
|
+
try {
|
|
88982
|
+
targets = await driver.findComputeTargets({ slug: config6.project.slug, environment, role: "app" });
|
|
88983
|
+
instanceCount = targets.length;
|
|
88984
|
+
if (targets.length) {
|
|
88985
|
+
const result = await driver.runRemoteDeploy({
|
|
88986
|
+
targets: [targets[0]],
|
|
88987
|
+
commands: metricsScript(),
|
|
88988
|
+
comment: `ts-cloud dashboard:build ${config6.project.slug}`,
|
|
88989
|
+
tags: { Project: config6.project.slug, Environment: environment, Role: "app" }
|
|
88990
|
+
});
|
|
88991
|
+
const output = result.perInstance?.[0]?.output;
|
|
88992
|
+
if (output)
|
|
88993
|
+
parsed = parseBlock(output);
|
|
88994
|
+
}
|
|
88995
|
+
} catch {}
|
|
88996
|
+
const out = resolveConfigOnlyServerDashboardData(config6, environment);
|
|
88997
|
+
const num = (v, d = 0) => {
|
|
88998
|
+
const n = Number(v);
|
|
88999
|
+
return Number.isFinite(n) ? n : d;
|
|
89000
|
+
};
|
|
89001
|
+
out.server.os = parsed?.OS || out.server.os;
|
|
89002
|
+
out.server.uptime = parsed?.UPTIME || out.server.uptime;
|
|
89003
|
+
if (parsed) {
|
|
89004
|
+
out.server.probeStatus = "live";
|
|
89005
|
+
out.metricsUnavailable = false;
|
|
89006
|
+
out._metricsStatus = "live";
|
|
89007
|
+
out.systemMetrics = {
|
|
89008
|
+
load: num(parsed.LOAD),
|
|
89009
|
+
cpus: num(parsed.CPUS, 1),
|
|
89010
|
+
memUsedMb: num(parsed.MEMUSED),
|
|
89011
|
+
memTotalMb: num(parsed.MEMTOTAL, 1),
|
|
89012
|
+
diskUsedPct: num(parsed.DISKPCT),
|
|
89013
|
+
diskUsedGb: num(parsed.DISKUSEDG),
|
|
89014
|
+
diskTotalGb: num(parsed.DISKTOTG, 1)
|
|
89015
|
+
};
|
|
89016
|
+
if (parsed.services.length) {
|
|
89017
|
+
out.services = parsed.services.map((s) => ({ name: s.name, status: s.status === "active" ? "running" : s.status }));
|
|
89018
|
+
out.servicesDetail = parsed.services.map((s) => ({
|
|
89019
|
+
name: s.name,
|
|
89020
|
+
status: s.status === "active" ? "running" : s.status,
|
|
89021
|
+
since: s.since && s.since !== UNKNOWN ? s.since : out.server.uptime,
|
|
89022
|
+
memMb: Math.max(0, Math.round(Number(s.memBytes ?? 0) / 1024 / 1024)),
|
|
89023
|
+
auto: s.enabled ? !["disabled", "static", "masked"].includes(s.enabled) : true
|
|
89024
|
+
}));
|
|
89025
|
+
}
|
|
89026
|
+
}
|
|
89027
|
+
if (driver && targets.length) {
|
|
89028
|
+
try {
|
|
89029
|
+
const siteNames = Object.keys(config6.sites ?? {});
|
|
89030
|
+
const historyResult = await driver.runRemoteDeploy({
|
|
89031
|
+
targets: [targets[0]],
|
|
89032
|
+
commands: deployHistoryScript(siteNames),
|
|
89033
|
+
comment: `ts-cloud dashboard:deploy-history ${config6.project.slug}`,
|
|
89034
|
+
tags: { Project: config6.project.slug, Environment: environment, Role: "app" }
|
|
89035
|
+
});
|
|
89036
|
+
const historyOutput = historyResult.perInstance?.[0]?.output ?? "";
|
|
89037
|
+
const records = parseDeployHistory(historyOutput, config6.sites);
|
|
89038
|
+
out.serverDeployments = records.slice(0, 5);
|
|
89039
|
+
out.serverDeploymentsDetail = records.slice(0, 50);
|
|
89040
|
+
out.deploymentsEmptyReason = records.length ? undefined : "No deployment history was found on the server yet. Deploy again to populate this timeline.";
|
|
89041
|
+
} catch {
|
|
89042
|
+
out.deploymentsEmptyReason = "Deployment history could not be read from the server.";
|
|
89043
|
+
}
|
|
89044
|
+
} else if (driver && instanceCount === 0) {
|
|
89045
|
+
out.deploymentsEmptyReason = "No app server target was found for this environment.";
|
|
89046
|
+
}
|
|
89047
|
+
if (driver && targets.length) {
|
|
89048
|
+
try {
|
|
89049
|
+
const logsResult = await driver.runRemoteDeploy({
|
|
89050
|
+
targets: [targets[0]],
|
|
89051
|
+
commands: serverLogsScript(config6),
|
|
89052
|
+
comment: `ts-cloud dashboard:server-logs ${config6.project.slug}`,
|
|
89053
|
+
tags: { Project: config6.project.slug, Environment: environment, Role: "app" }
|
|
89054
|
+
});
|
|
89055
|
+
const logsOutput = logsResult.perInstance?.[0]?.output ?? "";
|
|
89056
|
+
const records = parseServerLogs(logsOutput);
|
|
89057
|
+
out.serverLogs = records;
|
|
89058
|
+
out.serverLogsEmptyReason = records.length ? undefined : "No recent journal entries were found for the managed server units.";
|
|
89059
|
+
} catch {
|
|
89060
|
+
out.serverLogsEmptyReason = "Server logs could not be read from the box.";
|
|
89061
|
+
}
|
|
89062
|
+
}
|
|
89063
|
+
if (driver && targets.length) {
|
|
89064
|
+
try {
|
|
89065
|
+
const securityResult = await driver.runRemoteDeploy({
|
|
89066
|
+
targets: [targets[0]],
|
|
89067
|
+
commands: securityScript(config6),
|
|
89068
|
+
comment: `ts-cloud dashboard:security ${config6.project.slug}`,
|
|
89069
|
+
tags: { Project: config6.project.slug, Environment: environment, Role: "app" }
|
|
89070
|
+
});
|
|
89071
|
+
const securityOutput = securityResult.perInstance?.[0]?.output ?? "";
|
|
89072
|
+
const liveSecurity = parseServerSecurity(securityOutput);
|
|
89073
|
+
const declaredSecurity = configuredSecurity(config6);
|
|
89074
|
+
if (!liveSecurity.ports.length)
|
|
89075
|
+
liveSecurity.ports = declaredSecurity.ports;
|
|
89076
|
+
if (!liveSecurity.tlsCertificates.length)
|
|
89077
|
+
liveSecurity.tlsCertificates = declaredSecurity.tlsCertificates;
|
|
89078
|
+
out.security = liveSecurity;
|
|
89079
|
+
out.securityEmptyReason = undefined;
|
|
89080
|
+
} catch {
|
|
89081
|
+
out.securityEmptyReason = "Server security checks could not be read from the box.";
|
|
89082
|
+
}
|
|
89083
|
+
}
|
|
89084
|
+
const sshKeys = config6.infrastructure?.compute?.sshKeys ?? config6.infrastructure?.ssh?.keys ?? config6.infrastructure?.sshKeys;
|
|
89085
|
+
if (Array.isArray(sshKeys) && sshKeys.length) {
|
|
89086
|
+
out.sshKeys = describeSshKeys(sshKeys);
|
|
89087
|
+
}
|
|
89088
|
+
out._serverReachable = instanceCount > 0 && !!parsed;
|
|
89089
|
+
out.diagnostics = diagnosticChecks(config6, out);
|
|
89090
|
+
out.activity = activityFeed(out);
|
|
89091
|
+
return out;
|
|
89092
|
+
}
|
|
89093
|
+
|
|
89094
|
+
// src/deploy/dashboard-database.ts
|
|
89095
|
+
var SOCKETS = {
|
|
89096
|
+
mysql: "/var/lib/pantry/mysql/mysqld.sock",
|
|
89097
|
+
mariadb: "/var/lib/pantry/mariadb/mariadbd.sock"
|
|
89098
|
+
};
|
|
89099
|
+
function isValidDbIdentifier(value) {
|
|
89100
|
+
return /^[A-Za-z_][A-Za-z0-9_]{0,62}$/.test(value);
|
|
89101
|
+
}
|
|
89102
|
+
function normalizeEngine(engine) {
|
|
89103
|
+
if (engine === "postgres" || engine === "pgsql")
|
|
89104
|
+
return "postgres";
|
|
89105
|
+
if (engine === "mariadb")
|
|
89106
|
+
return "mariadb";
|
|
89107
|
+
return "mysql";
|
|
89108
|
+
}
|
|
89109
|
+
function resolveDbEngine(config6) {
|
|
89110
|
+
const compute = config6.infrastructure?.compute;
|
|
89111
|
+
const declared = config6.infrastructure?.appDatabase?.engine;
|
|
89112
|
+
if (declared)
|
|
89113
|
+
return normalizeEngine(declared);
|
|
89114
|
+
const managed = compute?.managedServices ?? {};
|
|
89115
|
+
if (managed.postgres)
|
|
89116
|
+
return "postgres";
|
|
89117
|
+
if (managed.mariadb)
|
|
89118
|
+
return "mariadb";
|
|
89119
|
+
return "mysql";
|
|
89120
|
+
}
|
|
89121
|
+
var mysqlIdent = (v) => v.replace(/`/g, "``");
|
|
89122
|
+
var mysqlLit = (v) => v.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
89123
|
+
var pgIdent = (v) => `"${v.replace(/"/g, '""')}"`;
|
|
89124
|
+
var pgLit = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
89125
|
+
function mysqlExec(engine, sql) {
|
|
89126
|
+
const sock = engine === "mariadb" ? SOCKETS.mariadb : SOCKETS.mysql;
|
|
89127
|
+
return [`mysql --socket=${sock} -u root <<'TS_CLOUD_SQL_EOF'`, ...sql, "TS_CLOUD_SQL_EOF"];
|
|
89128
|
+
}
|
|
89129
|
+
function pgExec(sql) {
|
|
89130
|
+
return ["psql -h 127.0.0.1 -p 5432 -U postgres -tA <<'TS_CLOUD_PG_EOF'", ...sql, "TS_CLOUD_PG_EOF"];
|
|
89131
|
+
}
|
|
89132
|
+
function buildListScript(engine) {
|
|
89133
|
+
if (engine === "postgres") {
|
|
89134
|
+
return pgExec([
|
|
89135
|
+
"SELECT 'DB=' || datname FROM pg_database WHERE datistemplate = false;",
|
|
89136
|
+
"SELECT 'USER=' || usename FROM pg_user;"
|
|
89137
|
+
]);
|
|
89138
|
+
}
|
|
89139
|
+
return mysqlExec(engine, [
|
|
89140
|
+
"SELECT CONCAT('DB=', schema_name) FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys');",
|
|
89141
|
+
"SELECT DISTINCT CONCAT('USER=', User) FROM mysql.user WHERE User NOT IN ('root', 'mysql.sys', 'mysql.session', 'mysql.infoschema', 'debian-sys-maint');"
|
|
89142
|
+
]);
|
|
89143
|
+
}
|
|
89144
|
+
function buildCreateDatabaseScript(engine, name) {
|
|
89145
|
+
if (engine === "postgres") {
|
|
89146
|
+
return pgExec([
|
|
89147
|
+
`SELECT 'CREATE DATABASE ${pgIdent(name)}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${pgLit(name)})\\gexec`
|
|
89148
|
+
]);
|
|
89149
|
+
}
|
|
89150
|
+
return mysqlExec(engine, [
|
|
89151
|
+
`CREATE DATABASE IF NOT EXISTS \`${mysqlIdent(name)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`
|
|
89152
|
+
]);
|
|
89153
|
+
}
|
|
89154
|
+
function buildCreateUserScript(engine, input) {
|
|
89155
|
+
const { username, password, database, access: access4 } = input;
|
|
89156
|
+
if (engine === "postgres") {
|
|
89157
|
+
const lines2 = [
|
|
89158
|
+
"DO $$ BEGIN",
|
|
89159
|
+
` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${pgLit(username)}) THEN CREATE ROLE ${pgIdent(username)} LOGIN PASSWORD ${pgLit(password)};`,
|
|
89160
|
+
` ELSE ALTER ROLE ${pgIdent(username)} LOGIN PASSWORD ${pgLit(password)}; END IF;`,
|
|
89161
|
+
"END $$;"
|
|
89162
|
+
];
|
|
89163
|
+
if (database) {
|
|
89164
|
+
lines2.push(access4 === "readonly" ? `GRANT CONNECT ON DATABASE ${pgIdent(database)} TO ${pgIdent(username)};` : `GRANT ALL PRIVILEGES ON DATABASE ${pgIdent(database)} TO ${pgIdent(username)};`);
|
|
89165
|
+
}
|
|
89166
|
+
return pgExec(lines2);
|
|
89167
|
+
}
|
|
89168
|
+
const priv = access4 === "readonly" ? "SELECT" : "ALL PRIVILEGES";
|
|
89169
|
+
const lines = [
|
|
89170
|
+
`CREATE USER IF NOT EXISTS '${mysqlLit(username)}'@'%' IDENTIFIED BY '${mysqlLit(password)}';`,
|
|
89171
|
+
`CREATE USER IF NOT EXISTS '${mysqlLit(username)}'@'localhost' IDENTIFIED BY '${mysqlLit(password)}';`,
|
|
89172
|
+
`ALTER USER '${mysqlLit(username)}'@'%' IDENTIFIED BY '${mysqlLit(password)}';`,
|
|
89173
|
+
`ALTER USER '${mysqlLit(username)}'@'localhost' IDENTIFIED BY '${mysqlLit(password)}';`
|
|
89174
|
+
];
|
|
89175
|
+
if (database) {
|
|
89176
|
+
lines.push(`GRANT ${priv} ON \`${mysqlIdent(database)}\`.* TO '${mysqlLit(username)}'@'%';`, `GRANT ${priv} ON \`${mysqlIdent(database)}\`.* TO '${mysqlLit(username)}'@'localhost';`);
|
|
89177
|
+
}
|
|
89178
|
+
lines.push("FLUSH PRIVILEGES;");
|
|
89179
|
+
return mysqlExec(engine, lines);
|
|
89180
|
+
}
|
|
89181
|
+
function parseDbList(output) {
|
|
89182
|
+
const databases = [];
|
|
89183
|
+
const users = [];
|
|
89184
|
+
for (const rawLine of output.split(`
|
|
89185
|
+
`)) {
|
|
89186
|
+
const line = rawLine.trim();
|
|
89187
|
+
if (line.startsWith("DB="))
|
|
89188
|
+
databases.push(line.slice(3));
|
|
89189
|
+
else if (line.startsWith("USER="))
|
|
89190
|
+
users.push(line.slice(5));
|
|
89191
|
+
}
|
|
89192
|
+
return {
|
|
89193
|
+
databases: [...new Set(databases)].filter(Boolean).sort(),
|
|
89194
|
+
users: [...new Set(users)].filter(Boolean).sort()
|
|
89195
|
+
};
|
|
89196
|
+
}
|
|
89197
|
+
async function runDb(config6, environment, commands, comment) {
|
|
89198
|
+
let driver;
|
|
89199
|
+
try {
|
|
89200
|
+
driver = createCloudDriver({ config: config6 });
|
|
89201
|
+
} catch (error2) {
|
|
89202
|
+
return { ok: false, error: `Could not initialize the cloud driver: ${error2?.message ?? error2}` };
|
|
89203
|
+
}
|
|
89204
|
+
const slug = config6.project.slug;
|
|
89205
|
+
const targets = await driver.findComputeTargets({ slug, environment, role: "app" });
|
|
89206
|
+
if (!targets.length)
|
|
89207
|
+
return { ok: false, error: "No app server target was found for this environment." };
|
|
89208
|
+
const result = await driver.runRemoteDeploy({
|
|
89209
|
+
targets: [targets[0]],
|
|
89210
|
+
commands: ["set -uo pipefail", ...commands],
|
|
89211
|
+
comment,
|
|
89212
|
+
tags: { Project: slug, Environment: environment, Role: "app" }
|
|
89213
|
+
});
|
|
89214
|
+
return { ok: result.success, stdout: result.perInstance?.[0]?.output ?? "", stderr: result.perInstance?.[0]?.error ?? result.error ?? "" };
|
|
89215
|
+
}
|
|
89216
|
+
async function listDatabases(config6, environment) {
|
|
89217
|
+
const engine = resolveDbEngine(config6);
|
|
89218
|
+
const r = await runDb(config6, environment, buildListScript(engine), "ts-cloud db:list");
|
|
89219
|
+
const parsed = r.ok && r.stdout ? parseDbList(r.stdout) : { databases: [], users: [] };
|
|
89220
|
+
return { ...r, engine, ...parsed };
|
|
89221
|
+
}
|
|
89222
|
+
async function createDatabase(config6, environment, name) {
|
|
89223
|
+
const engine = resolveDbEngine(config6);
|
|
89224
|
+
return runDb(config6, environment, buildCreateDatabaseScript(engine, name), `ts-cloud db:create ${name}`);
|
|
89225
|
+
}
|
|
89226
|
+
async function createDatabaseUser(config6, environment, input) {
|
|
89227
|
+
const engine = resolveDbEngine(config6);
|
|
89228
|
+
return runDb(config6, environment, buildCreateUserScript(engine, input), `ts-cloud db:user ${input.username}`);
|
|
89229
|
+
}
|
|
89230
|
+
|
|
89231
|
+
// src/drivers/shared/compute-ops.ts
|
|
89232
|
+
var noopLogger3 = {
|
|
89233
|
+
info: () => {},
|
|
89234
|
+
warn: () => {},
|
|
89235
|
+
error: () => {},
|
|
89236
|
+
step: () => {},
|
|
89237
|
+
success: () => {}
|
|
89238
|
+
};
|
|
89239
|
+
async function findTargets(ctx) {
|
|
89240
|
+
const targets = await ctx.driver.findComputeTargets({
|
|
89241
|
+
slug: ctx.slug,
|
|
89242
|
+
environment: ctx.environment,
|
|
89243
|
+
role: ctx.role || "app"
|
|
89244
|
+
});
|
|
89245
|
+
return targets;
|
|
89246
|
+
}
|
|
89247
|
+
async function rollbackComputeSite(ctx, options) {
|
|
89248
|
+
const logger4 = ctx.logger || noopLogger3;
|
|
89249
|
+
const targets = await findTargets(ctx);
|
|
89250
|
+
if (targets.length === 0)
|
|
89251
|
+
return { success: false, error: `No '${ctx.role || "app"}' servers found for ${ctx.slug}/${ctx.environment}.` };
|
|
89252
|
+
const appBase = `/var/www/${options.siteName}`;
|
|
89253
|
+
const paths = releasePaths(appBase, options.to || "unused");
|
|
89254
|
+
const commands = [
|
|
89255
|
+
"set -uo pipefail",
|
|
89256
|
+
...buildRollbackScript(paths, options.to ? { to: options.to } : {}),
|
|
89257
|
+
`(cd ${PANTRY_PROJECT_DIR} && pantry restart php-fpm) 2>/dev/null || true`,
|
|
89258
|
+
`(cd ${paths.current} && eval "$(cd ${PANTRY_PROJECT_DIR} && pantry env 2>/dev/null)" && php artisan queue:restart) 2>/dev/null || true`
|
|
89259
|
+
];
|
|
89260
|
+
logger4.step(`Rolling back ${options.siteName}${options.to ? ` to ${options.to}` : " to the previous release"} on ${targets.length} server(s)...`);
|
|
89261
|
+
const result = await ctx.driver.runRemoteDeploy({
|
|
89262
|
+
targets,
|
|
89263
|
+
commands,
|
|
89264
|
+
comment: `ts-cloud rollback ${ctx.slug}/${options.siteName}`,
|
|
89265
|
+
tags: { Project: ctx.slug, Environment: ctx.environment, Role: ctx.role || "app" }
|
|
89266
|
+
});
|
|
89267
|
+
if (!result.success)
|
|
89268
|
+
logger4.error(`Rollback failed: ${result.error || "unknown error"}`);
|
|
89269
|
+
else
|
|
89270
|
+
logger4.success(`Rolled back ${options.siteName}.`);
|
|
89271
|
+
return { success: result.success, error: result.error, perInstance: result.perInstance };
|
|
89272
|
+
}
|
|
89273
|
+
async function restoreDatabaseBackup(ctx, options) {
|
|
89274
|
+
const logger4 = ctx.logger || noopLogger3;
|
|
89275
|
+
if (!options.database?.name)
|
|
89276
|
+
return { success: false, error: "No appDatabase configured to restore." };
|
|
89277
|
+
const targets = await findTargets(ctx);
|
|
89278
|
+
if (targets.length === 0)
|
|
89279
|
+
return { success: false, error: `No '${ctx.role || "app"}' servers found for ${ctx.slug}/${ctx.environment}.` };
|
|
89280
|
+
logger4.step(`Restoring database '${options.database.name}'${options.from ? ` from ${options.from}` : " from the latest backup"} on ${targets.length} server(s)...`);
|
|
89281
|
+
const result = await ctx.driver.runRemoteDeploy({
|
|
89282
|
+
targets,
|
|
89283
|
+
commands: buildBackupRestoreScript(options.database, { from: options.from }),
|
|
89284
|
+
comment: `ts-cloud db:restore ${ctx.slug}/${options.database.name}`,
|
|
89285
|
+
tags: { Project: ctx.slug, Environment: ctx.environment, Role: ctx.role || "app" }
|
|
89286
|
+
});
|
|
89287
|
+
if (!result.success)
|
|
89288
|
+
logger4.error(`Restore failed: ${result.error || "unknown error"}`);
|
|
89289
|
+
else
|
|
89290
|
+
logger4.success(`Restored ${options.database.name}.`);
|
|
89291
|
+
return { success: result.success, error: result.error, perInstance: result.perInstance };
|
|
89292
|
+
}
|
|
89293
|
+
|
|
89294
|
+
// src/deploy/dashboard-operations.ts
|
|
89295
|
+
var SERVICE_VERBS = ["restart", "reload", "start", "stop", "enable", "disable"];
|
|
89296
|
+
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
89297
|
+
function clampOutput(output) {
|
|
89298
|
+
return output.length <= MAX_OUTPUT_BYTES ? output : `${output.slice(0, MAX_OUTPUT_BYTES)}
|
|
89299
|
+
|
|
89300
|
+
[output truncated]`;
|
|
89301
|
+
}
|
|
89302
|
+
function isSafeSystemdUnit(value) {
|
|
89303
|
+
return /^[A-Za-z0-9_.@:-]+$/.test(value);
|
|
89304
|
+
}
|
|
89305
|
+
function isSafeSiteName(value) {
|
|
89306
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
89307
|
+
}
|
|
89308
|
+
function capitalize(value) {
|
|
89309
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
89310
|
+
}
|
|
89311
|
+
function buildDashboardOperations(config6, data) {
|
|
89312
|
+
const ops = [];
|
|
89313
|
+
const services = data.servicesDetail ?? data.services ?? [];
|
|
89314
|
+
for (const service of services) {
|
|
89315
|
+
const name = String(service?.name ?? "");
|
|
89316
|
+
if (!name || !isSafeSystemdUnit(name))
|
|
89317
|
+
continue;
|
|
89318
|
+
for (const verb of SERVICE_VERBS) {
|
|
89319
|
+
ops.push({
|
|
89320
|
+
id: `${verb}:${name}`,
|
|
89321
|
+
label: `${capitalize(verb)} ${name}`,
|
|
89322
|
+
group: "service",
|
|
89323
|
+
target: name,
|
|
89324
|
+
mutates: true,
|
|
89325
|
+
confirm: name,
|
|
89326
|
+
danger: verb === "stop" || verb === "disable"
|
|
89327
|
+
});
|
|
89328
|
+
}
|
|
89329
|
+
}
|
|
89330
|
+
for (const [name, site] of Object.entries(config6.sites ?? {})) {
|
|
89331
|
+
if (!site || !isSafeSiteName(name))
|
|
89332
|
+
continue;
|
|
89333
|
+
const kind = resolveSiteKind(site);
|
|
89334
|
+
if (kind === "server-app" || kind === "server-php")
|
|
89335
|
+
ops.push({ id: `rollback:${name}`, label: `Rollback ${name}`, group: "deploy", target: name, mutates: true, confirm: name, danger: true });
|
|
89336
|
+
}
|
|
89337
|
+
const workerSites = new Set;
|
|
89338
|
+
for (const worker of data.workers ?? []) {
|
|
89339
|
+
const site = String(worker?.name ?? "").split(":")[0];
|
|
89340
|
+
if (site && isSafeSiteName(site))
|
|
89341
|
+
workerSites.add(site);
|
|
89342
|
+
}
|
|
89343
|
+
for (const site of workerSites)
|
|
89344
|
+
ops.push({ id: `worker:restart:${site}`, label: `Restart workers (${site})`, group: "worker", target: site, mutates: true, confirm: site });
|
|
89345
|
+
for (const [name, site] of Object.entries(config6.sites ?? {})) {
|
|
89346
|
+
if (!site || !isSafeSiteName(name))
|
|
89347
|
+
continue;
|
|
89348
|
+
if (site.scheduler || site.schedule)
|
|
89349
|
+
ops.push({ id: `scheduler:run:${name}`, label: `Run scheduler (${name})`, group: "scheduler", target: name, mutates: true, confirm: name });
|
|
89350
|
+
}
|
|
89351
|
+
const backups = config6.infrastructure?.compute?.backups;
|
|
89352
|
+
if (backups?.enabled) {
|
|
89353
|
+
ops.push({ id: "backup:run", label: "Run backup now", group: "backup", target: "backup", mutates: true, confirm: "backup" });
|
|
89354
|
+
if (config6.infrastructure?.appDatabase)
|
|
89355
|
+
ops.push({ id: "backup:restore", label: "Restore latest DB backup", group: "backup", target: "database", mutates: true, confirm: "restore", danger: true });
|
|
89356
|
+
}
|
|
89357
|
+
return ops;
|
|
89358
|
+
}
|
|
89359
|
+
function resolveDashboardOperation(id, config6, data) {
|
|
89360
|
+
return buildDashboardOperations(config6, data).find((op) => op.id === id);
|
|
89361
|
+
}
|
|
89362
|
+
function serviceCommand(verb, target) {
|
|
89363
|
+
const check = verb === "enable" || verb === "disable" ? `systemctl is-enabled ${target} 2>/dev/null || true` : `systemctl is-active ${target} 2>/dev/null || true`;
|
|
89364
|
+
return ["set -o pipefail", `systemctl ${verb} ${target}`, check];
|
|
89365
|
+
}
|
|
89366
|
+
function pantryEnvEval() {
|
|
89367
|
+
return `eval "$(cd ${PANTRY_PROJECT_DIR} && pantry env 2>/dev/null)"`;
|
|
89368
|
+
}
|
|
89369
|
+
function siteArtisanCommand(site, artisan) {
|
|
89370
|
+
const current = `/var/www/${site}/current`;
|
|
89371
|
+
return [
|
|
89372
|
+
"set -uo pipefail",
|
|
89373
|
+
`[ -d ${current} ] || { echo "site ${site} has no current release" >&2; exit 1; }`,
|
|
89374
|
+
`cd ${current} && ${pantryEnvEval()} && php artisan ${artisan}`
|
|
89375
|
+
];
|
|
89376
|
+
}
|
|
89377
|
+
function backupRunCommand() {
|
|
89378
|
+
return [
|
|
89379
|
+
"set -uo pipefail",
|
|
89380
|
+
`[ -x ${BACKUP_RUNNER_PATH} ] || { echo "scheduled backups are not provisioned on this server" >&2; exit 1; }`,
|
|
89381
|
+
BACKUP_RUNNER_PATH
|
|
89382
|
+
];
|
|
89383
|
+
}
|
|
89384
|
+
function fromComputeOps(id, command, r) {
|
|
89385
|
+
return {
|
|
89386
|
+
operation: id,
|
|
89387
|
+
command,
|
|
89388
|
+
ok: r.success,
|
|
89389
|
+
stdout: clampOutput(r.perInstance?.[0]?.output ?? ""),
|
|
89390
|
+
stderr: clampOutput(r.perInstance?.[0]?.error ?? r.error ?? "")
|
|
89391
|
+
};
|
|
89392
|
+
}
|
|
89393
|
+
async function runDashboardOperation(config6, environment, operation, options = {}) {
|
|
89394
|
+
const slug = config6.project.slug;
|
|
89395
|
+
let driver;
|
|
89396
|
+
try {
|
|
89397
|
+
driver = createCloudDriver({ config: config6 });
|
|
89398
|
+
} catch (error2) {
|
|
89399
|
+
return { operation: operation.id, ok: false, error: `Could not initialize the cloud driver: ${error2?.message ?? error2}` };
|
|
89400
|
+
}
|
|
89401
|
+
const ctx = { driver, slug, environment, role: "app" };
|
|
89402
|
+
if (operation.group === "deploy") {
|
|
89403
|
+
const to = options.to && /^[A-Za-z0-9._-]+$/.test(options.to) ? options.to : undefined;
|
|
89404
|
+
const r = await rollbackComputeSite(ctx, { siteName: operation.target, to });
|
|
89405
|
+
return fromComputeOps(operation.id, `rollback ${operation.target}${to ? ` → ${to}` : ""}`, r);
|
|
89406
|
+
}
|
|
89407
|
+
if (operation.id === "backup:restore") {
|
|
89408
|
+
const r = await restoreDatabaseBackup(ctx, { database: config6.infrastructure?.appDatabase });
|
|
89409
|
+
return fromComputeOps(operation.id, "db restore", r);
|
|
89410
|
+
}
|
|
89411
|
+
const targets = await driver.findComputeTargets({ slug, environment, role: "app" });
|
|
89412
|
+
if (!targets.length)
|
|
89413
|
+
return { operation: operation.id, ok: false, error: "No app server target was found for this environment." };
|
|
89414
|
+
let commands;
|
|
89415
|
+
let command;
|
|
89416
|
+
if (operation.group === "service") {
|
|
89417
|
+
const [verb, ...rest] = operation.id.split(":");
|
|
89418
|
+
commands = serviceCommand(verb, rest.join(":"));
|
|
89419
|
+
command = `systemctl ${verb} ${operation.target}`;
|
|
89420
|
+
} else if (operation.group === "worker") {
|
|
89421
|
+
commands = siteArtisanCommand(operation.target, "queue:restart");
|
|
89422
|
+
command = `queue:restart (${operation.target})`;
|
|
89423
|
+
} else if (operation.group === "scheduler") {
|
|
89424
|
+
commands = siteArtisanCommand(operation.target, "schedule:run");
|
|
89425
|
+
command = `schedule:run (${operation.target})`;
|
|
89426
|
+
} else if (operation.id === "backup:run") {
|
|
89427
|
+
commands = backupRunCommand();
|
|
89428
|
+
command = "ts-cloud backup";
|
|
89429
|
+
} else {
|
|
89430
|
+
return { operation: operation.id, ok: false, error: "Unknown operation." };
|
|
89431
|
+
}
|
|
89432
|
+
const result = await driver.runRemoteDeploy({
|
|
89433
|
+
targets: [targets[0]],
|
|
89434
|
+
commands,
|
|
89435
|
+
comment: `ts-cloud dashboard:${operation.id}`,
|
|
89436
|
+
tags: { Project: slug, Environment: environment, Role: "app" }
|
|
89437
|
+
});
|
|
89438
|
+
return {
|
|
89439
|
+
operation: operation.id,
|
|
89440
|
+
command,
|
|
89441
|
+
ok: result.success,
|
|
89442
|
+
stdout: clampOutput(result.perInstance?.[0]?.output ?? ""),
|
|
89443
|
+
stderr: clampOutput(result.perInstance?.[0]?.error ?? result.error ?? "")
|
|
89444
|
+
};
|
|
89445
|
+
}
|
|
89446
|
+
|
|
89447
|
+
// src/deploy/site-config-editor.ts
|
|
89448
|
+
function addSiteToCloudConfig(input) {
|
|
89449
|
+
const siteName = normalizeSiteName(input.name);
|
|
89450
|
+
const sitesObject = findSitesObject(input.configText);
|
|
89451
|
+
if (hasSite(input.configText, sitesObject.start, sitesObject.end, siteName)) {
|
|
89452
|
+
throw new Error(`Site '${siteName}' already exists in cloud.config.ts`);
|
|
89453
|
+
}
|
|
89454
|
+
const snippet = renderSiteSnippet({
|
|
89455
|
+
...input,
|
|
89456
|
+
name: siteName
|
|
89457
|
+
});
|
|
89458
|
+
const before = input.configText.slice(0, sitesObject.end).trimEnd();
|
|
89459
|
+
const after = input.configText.slice(sitesObject.end);
|
|
89460
|
+
const separator = before.endsWith("{") ? `
|
|
89461
|
+
` : before.endsWith(",") ? `
|
|
89462
|
+
|
|
89463
|
+
` : `,
|
|
89464
|
+
|
|
89465
|
+
`;
|
|
89466
|
+
return `${before}${separator}${snippet}
|
|
89467
|
+
${after}`;
|
|
89468
|
+
}
|
|
89469
|
+
function removeSiteFromCloudConfig(input) {
|
|
89470
|
+
const name = normalizeSiteName(input.name);
|
|
89471
|
+
const sites = findSitesObject(input.configText);
|
|
89472
|
+
const span = findSiteSpan(input.configText, sites.start, sites.end, name);
|
|
89473
|
+
if (!span)
|
|
89474
|
+
throw new Error(`Site '${name}' does not exist in cloud.config.ts`);
|
|
89475
|
+
const text = input.configText;
|
|
89476
|
+
let start = span.nameStart;
|
|
89477
|
+
while (start > 0 && text[start - 1] !== `
|
|
89478
|
+
` && /\s/.test(text[start - 1]))
|
|
89479
|
+
start--;
|
|
89480
|
+
let end = span.end + 1;
|
|
89481
|
+
if (text[end] === ",")
|
|
89482
|
+
end++;
|
|
89483
|
+
while (end < text.length && text[end] !== `
|
|
89484
|
+
` && /\s/.test(text[end]))
|
|
89485
|
+
end++;
|
|
89486
|
+
if (text[end] === `
|
|
89487
|
+
`)
|
|
89488
|
+
end++;
|
|
89489
|
+
return text.slice(0, start) + text.slice(end);
|
|
89490
|
+
}
|
|
89491
|
+
function setSitePropertyInCloudConfig(input) {
|
|
89492
|
+
const name = normalizeSiteName(input.siteName);
|
|
89493
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(input.key))
|
|
89494
|
+
throw new Error(`Invalid property key '${input.key}'`);
|
|
89495
|
+
const sites = findSitesObject(input.configText);
|
|
89496
|
+
const span = findSiteSpan(input.configText, sites.start, sites.end, name);
|
|
89497
|
+
if (!span)
|
|
89498
|
+
throw new Error(`Site '${name}' does not exist in cloud.config.ts`);
|
|
89499
|
+
const text = input.configText;
|
|
89500
|
+
const braceStart = text.indexOf("{", span.nameStart);
|
|
89501
|
+
const braceEnd = span.end;
|
|
89502
|
+
const body = text.slice(braceStart + 1, braceEnd);
|
|
89503
|
+
const propMatch = new RegExp(`(^|[\\s,{])(${escapeRegExp(input.key)})\\s*:`, "m").exec(body);
|
|
89504
|
+
if (propMatch) {
|
|
89505
|
+
const keyStart = braceStart + 1 + propMatch.index + propMatch[1].length;
|
|
89506
|
+
const colon = text.indexOf(":", keyStart);
|
|
89507
|
+
const valueEnd = scanValueEnd(text, colon + 1, braceEnd);
|
|
89508
|
+
return `${text.slice(0, keyStart)}${input.key}: ${input.valueText}${text.slice(valueEnd)}`;
|
|
89509
|
+
}
|
|
89510
|
+
const before = text.slice(0, braceEnd).trimEnd();
|
|
89511
|
+
const sep = before.endsWith("{") || before.endsWith(",") ? "" : ",";
|
|
89512
|
+
return `${before}${sep}
|
|
89513
|
+
${input.key}: ${input.valueText},
|
|
89514
|
+
${text.slice(braceEnd)}`;
|
|
89515
|
+
}
|
|
89516
|
+
function renderSslValue(ssl) {
|
|
89517
|
+
if (ssl === false)
|
|
89518
|
+
return "false";
|
|
89519
|
+
if (ssl === true)
|
|
89520
|
+
return "true";
|
|
89521
|
+
return `{ provider: '${escapeSingle(ssl.provider || "letsencrypt")}' }`;
|
|
89522
|
+
}
|
|
89523
|
+
function renderStringValue(value) {
|
|
89524
|
+
return `'${escapeSingle(value)}'`;
|
|
89525
|
+
}
|
|
89526
|
+
function renderEnvValue(env2) {
|
|
89527
|
+
const entries = Object.entries(env2).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key));
|
|
89528
|
+
if (entries.length === 0)
|
|
89529
|
+
return "{}";
|
|
89530
|
+
return `{
|
|
89531
|
+
${entries.map(([key, value]) => ` ${key}: '${escapeSingle(String(value))}',`).join(`
|
|
89532
|
+
`)}
|
|
89533
|
+
}`;
|
|
89534
|
+
}
|
|
89535
|
+
function scanValueEnd(text, start, limit) {
|
|
89536
|
+
let i = start;
|
|
89537
|
+
while (i < limit && /\s/.test(text[i]))
|
|
89538
|
+
i++;
|
|
89539
|
+
let depth = 0;
|
|
89540
|
+
let quote;
|
|
89541
|
+
let escaped = false;
|
|
89542
|
+
for (;i < limit; i++) {
|
|
89543
|
+
const c = text[i];
|
|
89544
|
+
if (quote) {
|
|
89545
|
+
if (escaped) {
|
|
89546
|
+
escaped = false;
|
|
89547
|
+
continue;
|
|
89548
|
+
}
|
|
89549
|
+
if (c === "\\") {
|
|
89550
|
+
escaped = true;
|
|
89551
|
+
continue;
|
|
89552
|
+
}
|
|
89553
|
+
if (c === quote)
|
|
89554
|
+
quote = undefined;
|
|
89555
|
+
continue;
|
|
89556
|
+
}
|
|
89557
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
89558
|
+
quote = c;
|
|
89559
|
+
continue;
|
|
89560
|
+
}
|
|
89561
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
89562
|
+
depth++;
|
|
89563
|
+
continue;
|
|
89564
|
+
}
|
|
89565
|
+
if (c === "}" || c === "]" || c === ")") {
|
|
89566
|
+
if (depth === 0)
|
|
89567
|
+
return i;
|
|
89568
|
+
depth--;
|
|
89569
|
+
continue;
|
|
89570
|
+
}
|
|
89571
|
+
if (c === "," && depth === 0)
|
|
89572
|
+
return i;
|
|
89573
|
+
}
|
|
89574
|
+
return limit;
|
|
89575
|
+
}
|
|
89576
|
+
function findSiteSpan(configText, start, end, siteName) {
|
|
89577
|
+
const body = configText.slice(start + 1, end);
|
|
89578
|
+
const match = new RegExp(`(^|[\\s,{])(${escapeRegExp(siteName)})\\s*:\\s*\\{`, "m").exec(body);
|
|
89579
|
+
if (!match)
|
|
89580
|
+
return null;
|
|
89581
|
+
const nameStart = start + 1 + match.index + match[1].length;
|
|
89582
|
+
const braceStart = configText.indexOf("{", nameStart);
|
|
89583
|
+
return { nameStart, end: findMatchingBrace(configText, braceStart) };
|
|
89584
|
+
}
|
|
89585
|
+
function renderSiteSnippet(input) {
|
|
89586
|
+
const siteName = normalizeSiteName(input.name);
|
|
89587
|
+
const lines = [` ${siteName}: {`];
|
|
89588
|
+
pushString(lines, "deploy", input.deploy);
|
|
89589
|
+
pushString(lines, "root", input.root);
|
|
89590
|
+
pushString(lines, "path", input.path);
|
|
89591
|
+
pushString(lines, "domain", input.domain);
|
|
89592
|
+
pushString(lines, "type", input.type);
|
|
89593
|
+
pushString(lines, "build", input.build);
|
|
89594
|
+
pushString(lines, "start", input.start);
|
|
89595
|
+
if (input.port !== undefined)
|
|
89596
|
+
lines.push(` port: ${input.port},`);
|
|
89597
|
+
pushString(lines, "pathRewriteStyle", input.pathRewriteStyle);
|
|
89598
|
+
if (input.ssl !== undefined) {
|
|
89599
|
+
if (input.ssl === false)
|
|
89600
|
+
lines.push(" ssl: false,");
|
|
89601
|
+
else if (input.ssl === true)
|
|
89602
|
+
lines.push(" ssl: true,");
|
|
89603
|
+
else
|
|
89604
|
+
lines.push(` ssl: { provider: '${escapeSingle(input.ssl.provider || "letsencrypt")}' },`);
|
|
89605
|
+
}
|
|
89606
|
+
if (input.env && Object.keys(input.env).length > 0) {
|
|
89607
|
+
lines.push(" env: {");
|
|
89608
|
+
for (const [key, value] of Object.entries(input.env)) {
|
|
89609
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
89610
|
+
continue;
|
|
89611
|
+
lines.push(` ${key}: '${escapeSingle(String(value))}',`);
|
|
89612
|
+
}
|
|
89613
|
+
lines.push(" },");
|
|
89614
|
+
}
|
|
89615
|
+
lines.push(" },");
|
|
89616
|
+
return lines.join(`
|
|
89617
|
+
`);
|
|
89618
|
+
}
|
|
89619
|
+
function escapeSingle(value) {
|
|
89620
|
+
return value.replace(/\\/g, "\\\\").replaceAll(String.fromCharCode(39), "\\'");
|
|
89621
|
+
}
|
|
89622
|
+
function normalizeSiteName(name) {
|
|
89623
|
+
const normalized = name.trim();
|
|
89624
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(normalized)) {
|
|
89625
|
+
throw new Error(`Invalid site name '${name}'. Use a valid JavaScript object key, e.g. docs or marketingSite.`);
|
|
89626
|
+
}
|
|
89627
|
+
return normalized;
|
|
89628
|
+
}
|
|
89629
|
+
function pushString(lines, key, value) {
|
|
89630
|
+
if (value === undefined || value === "")
|
|
89631
|
+
return;
|
|
89632
|
+
const escaped = value.replace(/\\/g, "\\\\").replaceAll(String.fromCharCode(39), "\\'");
|
|
89633
|
+
lines.push(` ${key}: '${escaped}',`);
|
|
89634
|
+
}
|
|
89635
|
+
function findSitesObject(configText) {
|
|
89636
|
+
const sitesMatch = /\bsites\s*:\s*{/g.exec(configText);
|
|
89637
|
+
if (!sitesMatch) {
|
|
89638
|
+
throw new Error("Could not find a top-level sites: { ... } block in cloud.config.ts");
|
|
89639
|
+
}
|
|
89640
|
+
const start = configText.indexOf("{", sitesMatch.index);
|
|
89641
|
+
const end = findMatchingBrace(configText, start);
|
|
89642
|
+
return { start, end };
|
|
89643
|
+
}
|
|
89644
|
+
function hasSite(configText, start, end, siteName) {
|
|
89645
|
+
const body = configText.slice(start + 1, end);
|
|
89646
|
+
return new RegExp(`(^|[\\s,{])${escapeRegExp(siteName)}\\s*:`, "m").test(body);
|
|
89647
|
+
}
|
|
89648
|
+
function escapeRegExp(value) {
|
|
89649
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89650
|
+
}
|
|
89651
|
+
function findMatchingBrace(text, start) {
|
|
89652
|
+
let depth = 0;
|
|
89653
|
+
let quote;
|
|
89654
|
+
let escaped = false;
|
|
89655
|
+
let lineComment = false;
|
|
89656
|
+
let blockComment = false;
|
|
89657
|
+
for (let index = start;index < text.length; index++) {
|
|
89658
|
+
const char = text[index];
|
|
89659
|
+
const next = text[index + 1];
|
|
89660
|
+
if (lineComment) {
|
|
89661
|
+
if (char === `
|
|
89662
|
+
`)
|
|
89663
|
+
lineComment = false;
|
|
89664
|
+
continue;
|
|
89665
|
+
}
|
|
89666
|
+
if (blockComment) {
|
|
89667
|
+
if (char === "*" && next === "/") {
|
|
89668
|
+
blockComment = false;
|
|
89669
|
+
index++;
|
|
89670
|
+
}
|
|
89671
|
+
continue;
|
|
89672
|
+
}
|
|
89673
|
+
if (quote) {
|
|
89674
|
+
if (escaped) {
|
|
89675
|
+
escaped = false;
|
|
89676
|
+
continue;
|
|
89677
|
+
}
|
|
89678
|
+
if (char === "\\") {
|
|
89679
|
+
escaped = true;
|
|
89680
|
+
continue;
|
|
89681
|
+
}
|
|
89682
|
+
if (char === quote)
|
|
89683
|
+
quote = undefined;
|
|
89684
|
+
continue;
|
|
89685
|
+
}
|
|
89686
|
+
if (char === "/" && next === "/") {
|
|
89687
|
+
lineComment = true;
|
|
89688
|
+
index++;
|
|
89689
|
+
continue;
|
|
89690
|
+
}
|
|
89691
|
+
if (char === "/" && next === "*") {
|
|
89692
|
+
blockComment = true;
|
|
89693
|
+
index++;
|
|
89694
|
+
continue;
|
|
89695
|
+
}
|
|
89696
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
89697
|
+
quote = char;
|
|
89698
|
+
continue;
|
|
89699
|
+
}
|
|
89700
|
+
if (char === "{")
|
|
89701
|
+
depth++;
|
|
89702
|
+
if (char === "}") {
|
|
89703
|
+
depth--;
|
|
89704
|
+
if (depth === 0)
|
|
89705
|
+
return index;
|
|
89706
|
+
}
|
|
89707
|
+
}
|
|
89708
|
+
throw new Error("Could not find the closing brace for sites: { ... } in cloud.config.ts");
|
|
89709
|
+
}
|
|
89710
|
+
|
|
89711
|
+
// src/deploy/local-dashboard-server.ts
|
|
89712
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
89713
|
+
var DEFAULT_PORT = 7676;
|
|
89714
|
+
var MAX_OUTPUT_BYTES2 = 64 * 1024;
|
|
89715
|
+
var here = dirname9(fileURLToPath6(import.meta.url));
|
|
89716
|
+
var contentTypes = {
|
|
89717
|
+
".html": "text/html; charset=utf-8",
|
|
89718
|
+
".css": "text/css; charset=utf-8",
|
|
89719
|
+
".js": "text/javascript; charset=utf-8",
|
|
89720
|
+
".json": "application/json; charset=utf-8",
|
|
89721
|
+
".svg": "image/svg+xml",
|
|
89722
|
+
".png": "image/png",
|
|
89723
|
+
".jpg": "image/jpeg",
|
|
89724
|
+
".jpeg": "image/jpeg",
|
|
89725
|
+
".webp": "image/webp",
|
|
89726
|
+
".ico": "image/x-icon"
|
|
89727
|
+
};
|
|
89728
|
+
function json(data, status = 200) {
|
|
89729
|
+
return new Response(JSON.stringify(data, null, 2), {
|
|
89730
|
+
status,
|
|
89731
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
89732
|
+
});
|
|
89733
|
+
}
|
|
89734
|
+
function text(data, status = 200) {
|
|
89735
|
+
return new Response(data, {
|
|
89736
|
+
status,
|
|
89737
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
89738
|
+
});
|
|
89739
|
+
}
|
|
89740
|
+
function selectedEnvironment(config6, requested) {
|
|
89741
|
+
if (requested)
|
|
89742
|
+
return requested;
|
|
89743
|
+
const envs = Object.keys(config6.environments ?? {});
|
|
89744
|
+
return envs[0] ?? "production";
|
|
89745
|
+
}
|
|
89746
|
+
async function loadLocalEnv(cwd) {
|
|
89747
|
+
const candidates = [
|
|
89748
|
+
join23(here, "..", "..", "..", "..", ".env"),
|
|
89749
|
+
join23(cwd, ".env"),
|
|
89750
|
+
join23(cwd, ".env.local"),
|
|
89751
|
+
join23(cwd, ".env.production")
|
|
89752
|
+
];
|
|
89753
|
+
for (const file of candidates) {
|
|
89754
|
+
if (!existsSync26(file))
|
|
89755
|
+
continue;
|
|
89756
|
+
const text2 = await readFile2(file, "utf8").catch(() => "");
|
|
89757
|
+
for (const line of text2.split(`
|
|
89758
|
+
`)) {
|
|
89759
|
+
const trimmed = line.trim();
|
|
89760
|
+
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("="))
|
|
89761
|
+
continue;
|
|
89762
|
+
const eq = trimmed.indexOf("=");
|
|
89763
|
+
const key = trimmed.slice(0, eq).trim();
|
|
89764
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
89765
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key) || process.env[key] != null)
|
|
89766
|
+
continue;
|
|
89767
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
|
|
89768
|
+
value = value.slice(1, -1);
|
|
89769
|
+
process.env[key] = value;
|
|
89770
|
+
}
|
|
89771
|
+
}
|
|
89772
|
+
}
|
|
89773
|
+
function resolveCloudConfigPath(cwd) {
|
|
89774
|
+
const candidates = [
|
|
89775
|
+
join23(cwd, "config", "cloud.ts"),
|
|
89776
|
+
join23(cwd, "config", "cloud.js"),
|
|
89777
|
+
join23(cwd, "cloud.config.ts"),
|
|
89778
|
+
join23(cwd, "cloud.config.js")
|
|
89779
|
+
];
|
|
89780
|
+
return candidates.find((file) => existsSync26(file)) ?? null;
|
|
89781
|
+
}
|
|
89782
|
+
function computeSshKeys(config6) {
|
|
89783
|
+
return config6.infrastructure?.compute?.sshKeys ?? [];
|
|
89784
|
+
}
|
|
89785
|
+
function replaceComputeSshKeys(config6, keys) {
|
|
89786
|
+
const infrastructure2 = config6.infrastructure ?? (config6.infrastructure = {});
|
|
89787
|
+
const compute = infrastructure2.compute ?? (infrastructure2.compute = {});
|
|
89788
|
+
compute.sshKeys = keys;
|
|
89789
|
+
}
|
|
89790
|
+
function replaceSiteConfig(config6, name, site) {
|
|
89791
|
+
const sites = config6.sites ?? (config6.sites = {});
|
|
89792
|
+
sites[name] = site;
|
|
89793
|
+
}
|
|
89794
|
+
async function readJsonBody(req) {
|
|
89795
|
+
return await req.json().catch(() => ({}));
|
|
89796
|
+
}
|
|
89797
|
+
function resolveDashboardMode(config6) {
|
|
89798
|
+
if (config6.mode === "hybrid")
|
|
89799
|
+
return "hybrid";
|
|
89800
|
+
return config6.infrastructure?.compute ? "server" : "serverless";
|
|
89801
|
+
}
|
|
89802
|
+
async function resolveLiveDashboardData(config6, environment) {
|
|
89803
|
+
const mode = resolveDashboardMode(config6);
|
|
89804
|
+
const meta = { mode, environment, environments: Object.keys(config6.environments ?? {}) };
|
|
89805
|
+
try {
|
|
89806
|
+
const data = mode === "serverless" ? await resolveDashboardData(config6, environment) : await resolveServerDashboardData(config6, environment);
|
|
89807
|
+
return { ...data ?? {}, ...meta };
|
|
89808
|
+
} catch {
|
|
89809
|
+
return { ...meta };
|
|
89810
|
+
}
|
|
89811
|
+
}
|
|
89812
|
+
function resolveUiSourceDir(cwd) {
|
|
89813
|
+
const candidates = [
|
|
89814
|
+
join23(cwd, "packages", "ui"),
|
|
89815
|
+
join23(here, "..", "..", "..", "ui"),
|
|
89816
|
+
join23(here, "..", "..", "ui"),
|
|
89817
|
+
join23(here, "..", "ui-src"),
|
|
89818
|
+
join23(here, "..", "..", "ui-src"),
|
|
89819
|
+
join23(here, "..", "..", "dist", "ui-src")
|
|
89820
|
+
];
|
|
89821
|
+
for (const dir of candidates) {
|
|
89822
|
+
if (existsSync26(join23(dir, "pages")) && existsSync26(join23(dir, "package.json")))
|
|
89823
|
+
return dir;
|
|
89824
|
+
}
|
|
89825
|
+
return null;
|
|
89826
|
+
}
|
|
89827
|
+
async function buildLiveUi(cwd, data) {
|
|
89828
|
+
const uiDir = resolveUiSourceDir(cwd);
|
|
89829
|
+
if (!uiDir)
|
|
89830
|
+
return null;
|
|
89831
|
+
try {
|
|
89832
|
+
const outDir = mkdtempSync6(join23(tmpdir7(), "ts-cloud-dashboard-"));
|
|
89833
|
+
const localStx = join23(uiDir, "node_modules", ".bin", "stx");
|
|
89834
|
+
const cmd = existsSync26(localStx) ? [localStx, "build", "--pages", "pages", "--out", outDir, "--no-sitemap", "--no-cache"] : ["bunx", "--bun", "@stacksjs/stx", "build", "--pages", "pages", "--out", outDir, "--no-sitemap", "--no-cache"];
|
|
89835
|
+
const proc = Bun.spawn(cmd, {
|
|
89836
|
+
cwd: uiDir,
|
|
89837
|
+
stdout: "pipe",
|
|
89838
|
+
stderr: "pipe",
|
|
89839
|
+
env: {
|
|
89840
|
+
...process.env,
|
|
89841
|
+
TSCLOUD_DASHBOARD_DATA: JSON.stringify(data)
|
|
89842
|
+
}
|
|
89843
|
+
});
|
|
89844
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
89845
|
+
new Response(proc.stdout).text(),
|
|
89846
|
+
new Response(proc.stderr).text(),
|
|
89847
|
+
proc.exited
|
|
89848
|
+
]);
|
|
89849
|
+
if (exitCode !== 0) {
|
|
89850
|
+
if (process.env.TSCLOUD_DASHBOARD_VERBOSE)
|
|
89851
|
+
console.warn(`ts-cloud dashboard: live UI build failed; serving the pre-built UI.
|
|
89852
|
+
${stdout}
|
|
89853
|
+
${stderr}`);
|
|
89854
|
+
return null;
|
|
89855
|
+
}
|
|
89856
|
+
return outDir;
|
|
89857
|
+
} catch (err) {
|
|
89858
|
+
if (process.env.TSCLOUD_DASHBOARD_VERBOSE)
|
|
89859
|
+
console.warn(`ts-cloud dashboard: live UI build errored; serving the pre-built UI. ${err.message}`);
|
|
89860
|
+
return null;
|
|
89861
|
+
}
|
|
89862
|
+
}
|
|
89863
|
+
function sanitizeCloudConfig(config6) {
|
|
89864
|
+
return {
|
|
89865
|
+
project: {
|
|
89866
|
+
name: config6.project?.name,
|
|
89867
|
+
slug: config6.project?.slug,
|
|
89868
|
+
region: config6.project?.region
|
|
89869
|
+
},
|
|
89870
|
+
provider: config6.provider,
|
|
89871
|
+
environments: Object.keys(config6.environments ?? {}),
|
|
89872
|
+
compute: config6.infrastructure?.compute ? {
|
|
89873
|
+
provider: config6.infrastructure.compute.provider,
|
|
89874
|
+
runtime: config6.infrastructure.compute.runtime,
|
|
89875
|
+
webServer: config6.infrastructure.compute.webServer,
|
|
89876
|
+
proxy: config6.infrastructure.compute.proxy ? {
|
|
89877
|
+
engine: config6.infrastructure.compute.proxy.engine,
|
|
89878
|
+
onDemandTls: config6.infrastructure.compute.proxy.onDemandTls,
|
|
89879
|
+
cdn: !!config6.infrastructure.compute.proxy.cdn
|
|
89880
|
+
} : undefined,
|
|
89881
|
+
managedServices: config6.infrastructure.compute.managedServices,
|
|
89882
|
+
sshKeys: describeSshKeys(computeSshKeys(config6)).map((key) => ({
|
|
89883
|
+
name: key.name,
|
|
89884
|
+
type: key.type,
|
|
89885
|
+
fingerprint: key.fingerprint,
|
|
89886
|
+
added: key.added
|
|
89887
|
+
}))
|
|
89888
|
+
} : undefined,
|
|
89889
|
+
sites: Object.fromEntries(Object.entries(config6.sites ?? {}).map(([name, site]) => [
|
|
89890
|
+
name,
|
|
89891
|
+
{
|
|
89892
|
+
domain: site.domain,
|
|
89893
|
+
path: site.path,
|
|
89894
|
+
type: site.type,
|
|
89895
|
+
deploy: site.deploy,
|
|
89896
|
+
root: site.root,
|
|
89897
|
+
port: site.port,
|
|
89898
|
+
ssl: site.ssl
|
|
89899
|
+
}
|
|
89900
|
+
]))
|
|
89901
|
+
};
|
|
89902
|
+
}
|
|
89903
|
+
function dashboardActions(environment) {
|
|
89904
|
+
return [
|
|
89905
|
+
{
|
|
89906
|
+
id: "status",
|
|
89907
|
+
label: "Refresh Cloud Status",
|
|
89908
|
+
description: "Runs the local ts-cloud status checks against configured providers.",
|
|
89909
|
+
command: ["status", "--env", environment],
|
|
89910
|
+
mutates: false
|
|
89911
|
+
},
|
|
89912
|
+
{
|
|
89913
|
+
id: "doctor",
|
|
89914
|
+
label: "Run Doctor",
|
|
89915
|
+
description: "Checks local tooling, credentials, and provider access.",
|
|
89916
|
+
command: ["doctor"],
|
|
89917
|
+
mutates: false
|
|
89918
|
+
},
|
|
89919
|
+
{
|
|
89920
|
+
id: "security-scan",
|
|
89921
|
+
label: "Security Scan",
|
|
89922
|
+
description: "Runs the pre-deploy scanner from this checkout.",
|
|
89923
|
+
command: ["deploy:security-scan", "--source", ".", "--fail-on", "critical"],
|
|
89924
|
+
mutates: false
|
|
89925
|
+
},
|
|
89926
|
+
{
|
|
89927
|
+
id: "deploy",
|
|
89928
|
+
label: "Deploy Environment",
|
|
89929
|
+
description: "Deploys the selected environment from the local checkout.",
|
|
89930
|
+
command: ["deploy", "--env", environment, "--yes"],
|
|
89931
|
+
mutates: true,
|
|
89932
|
+
confirm: "deploy"
|
|
89933
|
+
}
|
|
89934
|
+
];
|
|
89935
|
+
}
|
|
89936
|
+
function resolveDashboardAction(id, environment) {
|
|
89937
|
+
return dashboardActions(environment).find((action) => action.id === id);
|
|
89938
|
+
}
|
|
89939
|
+
function clampOutput2(output) {
|
|
89940
|
+
if (output.length <= MAX_OUTPUT_BYTES2)
|
|
89941
|
+
return output;
|
|
89942
|
+
return `${output.slice(0, MAX_OUTPUT_BYTES2)}
|
|
89943
|
+
|
|
89944
|
+
[output truncated]`;
|
|
89945
|
+
}
|
|
89946
|
+
async function runAction(action, options) {
|
|
89947
|
+
const proc = Bun.spawn([process.execPath, options.cliEntry, ...action.command], {
|
|
89948
|
+
cwd: options.cwd,
|
|
89949
|
+
stdout: "pipe",
|
|
89950
|
+
stderr: "pipe",
|
|
89951
|
+
env: process.env
|
|
89952
|
+
});
|
|
89953
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
89954
|
+
new Response(proc.stdout).text(),
|
|
89955
|
+
new Response(proc.stderr).text(),
|
|
89956
|
+
proc.exited
|
|
89957
|
+
]);
|
|
89958
|
+
return {
|
|
89959
|
+
action: action.id,
|
|
89960
|
+
command: ["cloud", ...action.command].join(" "),
|
|
89961
|
+
exitCode,
|
|
89962
|
+
ok: exitCode === 0,
|
|
89963
|
+
stdout: clampOutput2(stdout),
|
|
89964
|
+
stderr: clampOutput2(stderr)
|
|
89965
|
+
};
|
|
89966
|
+
}
|
|
89967
|
+
function staticPath(uiRoot, pathname) {
|
|
89968
|
+
const clean = decodeURIComponent(pathname).replace(/^\/+/, "");
|
|
89969
|
+
const wanted = clean === "" ? "index.html" : clean;
|
|
89970
|
+
const normalized = normalize(wanted);
|
|
89971
|
+
if (normalized.startsWith("..") || normalized.includes("/../"))
|
|
89972
|
+
return null;
|
|
89973
|
+
const base = join23(uiRoot, normalized);
|
|
89974
|
+
if (existsSync26(base) && !statSync6(base).isDirectory())
|
|
89975
|
+
return base;
|
|
89976
|
+
if (!extname3(base) && existsSync26(`${base}.html`))
|
|
89977
|
+
return `${base}.html`;
|
|
89978
|
+
if (!extname3(base) && existsSync26(join23(base, "index.html")))
|
|
89979
|
+
return join23(base, "index.html");
|
|
89980
|
+
return null;
|
|
89981
|
+
}
|
|
89982
|
+
async function serveStatic(uiRoot, pathname) {
|
|
89983
|
+
const file = staticPath(uiRoot, pathname);
|
|
89984
|
+
if (!file)
|
|
89985
|
+
return text("Not found", 404);
|
|
89986
|
+
if (statSync6(file).isDirectory())
|
|
89987
|
+
return text("Not found", 404);
|
|
89988
|
+
const body = await readFile2(file);
|
|
89989
|
+
const type = contentTypes[extname3(file)] ?? "application/octet-stream";
|
|
89990
|
+
return new Response(body, { headers: { "content-type": type } });
|
|
89991
|
+
}
|
|
89992
|
+
async function startLocalDashboardServer(options = {}) {
|
|
89993
|
+
const cwd = options.cwd ?? process.cwd();
|
|
89994
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
89995
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
89996
|
+
const cliEntry = options.cliEntry ?? process.argv[1];
|
|
89997
|
+
if (options.box)
|
|
89998
|
+
process.env.TS_CLOUD_DASHBOARD_BOX = "1";
|
|
89999
|
+
await loadLocalEnv(cwd);
|
|
90000
|
+
const config6 = await loadCloudConfig();
|
|
90001
|
+
const availableEnvironments = Object.keys(config6.environments ?? {});
|
|
90002
|
+
let environment = selectedEnvironment(config6, options.environment);
|
|
90003
|
+
let actions = dashboardActions(environment);
|
|
90004
|
+
const configPath = resolveCloudConfigPath(cwd);
|
|
90005
|
+
const initialData = await resolveLiveDashboardData(config6, environment);
|
|
90006
|
+
let latestData = initialData;
|
|
90007
|
+
const liveUiRoot = await buildLiveUi(cwd, initialData);
|
|
90008
|
+
const packagedUi = resolveUiSource(cwd);
|
|
90009
|
+
let activeUiRoot = liveUiRoot ?? packagedUi?.uiRoot;
|
|
90010
|
+
if (!activeUiRoot)
|
|
90011
|
+
throw new Error("ts-cloud dashboard UI not found. Run `bun run build` in ts-cloud or reinstall the package.");
|
|
90012
|
+
const server = Bun.serve({
|
|
90013
|
+
hostname: host,
|
|
90014
|
+
port,
|
|
90015
|
+
async fetch(req) {
|
|
90016
|
+
const url = new URL(req.url);
|
|
90017
|
+
try {
|
|
90018
|
+
if (url.pathname === "/api/health") {
|
|
90019
|
+
return json({
|
|
90020
|
+
ok: true,
|
|
90021
|
+
cwd,
|
|
90022
|
+
environment,
|
|
90023
|
+
environments: availableEnvironments,
|
|
90024
|
+
uiRoot: activeUiRoot,
|
|
90025
|
+
liveData: !!liveUiRoot,
|
|
90026
|
+
localPackage: import.meta.url.includes("/Code/Libraries/ts-cloud/")
|
|
90027
|
+
});
|
|
90028
|
+
}
|
|
90029
|
+
if (url.pathname === "/" && latestData?.mode === "serverless")
|
|
90030
|
+
return new Response(null, { status: 302, headers: { location: "/serverless" } });
|
|
90031
|
+
if (url.pathname === "/api/env" && req.method === "POST") {
|
|
90032
|
+
const body = await readJsonBody(req);
|
|
90033
|
+
const requested = String(body.env ?? "");
|
|
90034
|
+
if (!availableEnvironments.includes(requested))
|
|
90035
|
+
return json({ ok: false, error: `Unknown environment '${requested}'.`, environments: availableEnvironments }, 404);
|
|
90036
|
+
if (requested !== environment) {
|
|
90037
|
+
environment = requested;
|
|
90038
|
+
actions = dashboardActions(environment);
|
|
90039
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
90040
|
+
const rebuilt = await buildLiveUi(cwd, latestData);
|
|
90041
|
+
if (rebuilt)
|
|
90042
|
+
activeUiRoot = rebuilt;
|
|
90043
|
+
}
|
|
90044
|
+
return json({ ok: true, environment, environments: availableEnvironments });
|
|
90045
|
+
}
|
|
90046
|
+
if (url.pathname === "/api/config")
|
|
90047
|
+
return json({ ...sanitizeCloudConfig(config6), environment, environments: availableEnvironments });
|
|
90048
|
+
if (url.pathname === "/api/ssh-keys" && req.method === "GET") {
|
|
90049
|
+
return json({
|
|
90050
|
+
configPath,
|
|
90051
|
+
keys: describeSshKeys(computeSshKeys(config6))
|
|
90052
|
+
});
|
|
90053
|
+
}
|
|
90054
|
+
if (url.pathname === "/api/ssh-keys" && req.method === "POST") {
|
|
90055
|
+
if (!configPath)
|
|
90056
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90057
|
+
const body = await readJsonBody(req);
|
|
90058
|
+
const before = await readFile2(configPath, "utf8");
|
|
90059
|
+
const currentKeys = computeSshKeys(config6);
|
|
90060
|
+
const after = addSshKeyToCloudConfig({
|
|
90061
|
+
configText: before,
|
|
90062
|
+
name: String(body.name ?? ""),
|
|
90063
|
+
publicKey: String(body.publicKey ?? ""),
|
|
90064
|
+
existingKeys: currentKeys
|
|
90065
|
+
});
|
|
90066
|
+
await writeFile6(configPath, after);
|
|
90067
|
+
replaceComputeSshKeys(config6, [...currentKeys, { name: String(body.name ?? "").trim(), publicKey: String(body.publicKey ?? "").trim().replace(/\s+/g, " ") }]);
|
|
90068
|
+
return json({ ok: true, configPath, keys: describeSshKeys(computeSshKeys(config6)) });
|
|
90069
|
+
}
|
|
90070
|
+
if (url.pathname === "/api/ssh-keys" && req.method === "DELETE") {
|
|
90071
|
+
if (!configPath)
|
|
90072
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90073
|
+
const body = await readJsonBody(req);
|
|
90074
|
+
const name = String(body.name ?? url.searchParams.get("name") ?? "");
|
|
90075
|
+
const before = await readFile2(configPath, "utf8");
|
|
90076
|
+
const currentKeys = computeSshKeys(config6);
|
|
90077
|
+
const after = removeSshKeyFromCloudConfig({
|
|
90078
|
+
configText: before,
|
|
90079
|
+
name,
|
|
90080
|
+
existingKeys: currentKeys
|
|
90081
|
+
});
|
|
90082
|
+
await writeFile6(configPath, after);
|
|
90083
|
+
replaceComputeSshKeys(config6, currentKeys.filter((key) => key.name !== name.trim()));
|
|
90084
|
+
return json({ ok: true, configPath, keys: describeSshKeys(computeSshKeys(config6)) });
|
|
90085
|
+
}
|
|
90086
|
+
if (url.pathname === "/api/sites" && req.method === "POST") {
|
|
90087
|
+
if (!configPath)
|
|
90088
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90089
|
+
const body = await readJsonBody(req);
|
|
90090
|
+
const siteName = String(body.name ?? "").trim();
|
|
90091
|
+
const root = String(body.root ?? "").trim();
|
|
90092
|
+
if (!siteName || !root)
|
|
90093
|
+
return json({ ok: false, error: "Site name and root are required." }, 422);
|
|
90094
|
+
const site = {
|
|
90095
|
+
deploy: body.deploy === "bucket" ? "bucket" : "server",
|
|
90096
|
+
root,
|
|
90097
|
+
path: String(body.path ?? "").trim() || undefined,
|
|
90098
|
+
domain: String(body.domain ?? "").trim() || undefined,
|
|
90099
|
+
build: String(body.build ?? "").trim() || undefined,
|
|
90100
|
+
start: String(body.start ?? "").trim() || undefined,
|
|
90101
|
+
port: body.port ? Number(body.port) : undefined,
|
|
90102
|
+
type: String(body.type ?? "").trim() || undefined
|
|
90103
|
+
};
|
|
90104
|
+
if (site.port !== undefined && (!Number.isInteger(site.port) || site.port < 1 || site.port > 65535))
|
|
90105
|
+
return json({ ok: false, error: "Port must be a number between 1 and 65535." }, 422);
|
|
90106
|
+
const before = await readFile2(configPath, "utf8");
|
|
90107
|
+
const after = addSiteToCloudConfig({
|
|
90108
|
+
configText: before,
|
|
90109
|
+
name: siteName,
|
|
90110
|
+
root: site.root,
|
|
90111
|
+
domain: site.domain,
|
|
90112
|
+
path: site.path,
|
|
90113
|
+
deploy: site.deploy,
|
|
90114
|
+
build: site.build,
|
|
90115
|
+
start: site.start,
|
|
90116
|
+
port: site.port,
|
|
90117
|
+
type: site.type
|
|
90118
|
+
});
|
|
90119
|
+
await writeFile6(configPath, after);
|
|
90120
|
+
replaceSiteConfig(config6, siteName, Object.fromEntries(Object.entries(site).filter(([, value]) => value !== undefined)));
|
|
90121
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
90122
|
+
return json({ ok: true, configPath, site: siteName, data: latestData });
|
|
90123
|
+
}
|
|
90124
|
+
if (url.pathname === "/api/sites" && req.method === "DELETE") {
|
|
90125
|
+
if (!configPath)
|
|
90126
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90127
|
+
const body = await readJsonBody(req);
|
|
90128
|
+
const name = String(body.name ?? url.searchParams.get("name") ?? "").trim();
|
|
90129
|
+
if (!name || !config6.sites?.[name])
|
|
90130
|
+
return json({ ok: false, error: `Site '${name}' was not found.` }, 404);
|
|
90131
|
+
const before = await readFile2(configPath, "utf8");
|
|
90132
|
+
await writeFile6(configPath, removeSiteFromCloudConfig({ configText: before, name }));
|
|
90133
|
+
delete config6.sites[name];
|
|
90134
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
90135
|
+
return json({ ok: true, configPath, site: name, data: latestData });
|
|
90136
|
+
}
|
|
90137
|
+
if (url.pathname === "/api/sites" && req.method === "PATCH") {
|
|
90138
|
+
if (!configPath)
|
|
90139
|
+
return json({ ok: false, error: "No cloud config file was found in this checkout." }, 404);
|
|
90140
|
+
const body = await readJsonBody(req);
|
|
90141
|
+
const name = String(body.name ?? "").trim();
|
|
90142
|
+
const existing = config6.sites?.[name];
|
|
90143
|
+
if (!name || !existing)
|
|
90144
|
+
return json({ ok: false, error: `Site '${name}' was not found.` }, 404);
|
|
90145
|
+
if (body.port !== undefined && body.port !== null && body.port !== "" && (!Number.isInteger(Number(body.port)) || Number(body.port) < 1 || Number(body.port) > 65535))
|
|
90146
|
+
return json({ ok: false, error: "Port must be a number between 1 and 65535." }, 422);
|
|
90147
|
+
let text2 = await readFile2(configPath, "utf8");
|
|
90148
|
+
const set = (key, valueText) => {
|
|
90149
|
+
text2 = setSitePropertyInCloudConfig({ configText: text2, siteName: name, key, valueText });
|
|
90150
|
+
};
|
|
90151
|
+
if (body.ssl !== undefined) {
|
|
90152
|
+
set("ssl", renderSslValue(body.ssl));
|
|
90153
|
+
existing.ssl = body.ssl;
|
|
90154
|
+
}
|
|
90155
|
+
if (body.env !== undefined && body.env && typeof body.env === "object") {
|
|
90156
|
+
set("env", renderEnvValue(body.env));
|
|
90157
|
+
existing.env = body.env;
|
|
90158
|
+
}
|
|
90159
|
+
for (const key of ["domain", "path", "build", "start", "type", "root"]) {
|
|
90160
|
+
if (typeof body[key] === "string" && body[key].trim()) {
|
|
90161
|
+
set(key, renderStringValue(String(body[key]).trim()));
|
|
90162
|
+
existing[key] = String(body[key]).trim();
|
|
90163
|
+
}
|
|
90164
|
+
}
|
|
90165
|
+
if (body.port !== undefined && body.port !== null && body.port !== "") {
|
|
90166
|
+
set("port", String(Number(body.port)));
|
|
90167
|
+
existing.port = Number(body.port);
|
|
90168
|
+
}
|
|
90169
|
+
await writeFile6(configPath, text2);
|
|
90170
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
90171
|
+
return json({ ok: true, configPath, site: name, data: latestData });
|
|
90172
|
+
}
|
|
90173
|
+
if (url.pathname === "/api/sites/deploy" && req.method === "POST") {
|
|
90174
|
+
const body = await readJsonBody(req);
|
|
90175
|
+
const name = String(body.name ?? "").trim();
|
|
90176
|
+
if (!name || !config6.sites?.[name])
|
|
90177
|
+
return json({ ok: false, error: `Site '${name}' was not found.` }, 404);
|
|
90178
|
+
if (body.confirm !== name)
|
|
90179
|
+
return json({ ok: false, error: `Type "${name}" to deploy this site.` }, 409);
|
|
90180
|
+
const action = {
|
|
90181
|
+
id: `deploy:${name}`,
|
|
90182
|
+
label: `Deploy ${name}`,
|
|
90183
|
+
description: `Deploy the ${name} site for ${environment}.`,
|
|
90184
|
+
command: ["deploy", "--env", environment, "--site", name, "--yes"],
|
|
90185
|
+
mutates: true
|
|
90186
|
+
};
|
|
90187
|
+
return json(await runAction(action, { cwd, cliEntry }));
|
|
90188
|
+
}
|
|
90189
|
+
if (url.pathname === "/api/actions")
|
|
90190
|
+
return json(actions);
|
|
90191
|
+
if (url.pathname === "/api/server/operations")
|
|
90192
|
+
return json(buildDashboardOperations(config6, latestData));
|
|
90193
|
+
if (url.pathname === "/api/databases" && req.method === "GET")
|
|
90194
|
+
return json(await listDatabases(config6, environment));
|
|
90195
|
+
if (url.pathname === "/api/databases" && req.method === "POST") {
|
|
90196
|
+
const body = await readJsonBody(req);
|
|
90197
|
+
const name = String(body.name ?? "").trim();
|
|
90198
|
+
if (!isValidDbIdentifier(name))
|
|
90199
|
+
return json({ ok: false, error: "Database name must be a valid identifier (letters, numbers, underscore; not starting with a digit)." }, 422);
|
|
90200
|
+
return json({ ...await createDatabase(config6, environment, name), name });
|
|
90201
|
+
}
|
|
90202
|
+
if (url.pathname === "/api/databases/users" && req.method === "POST") {
|
|
90203
|
+
const body = await readJsonBody(req);
|
|
90204
|
+
const username = String(body.username ?? "").trim();
|
|
90205
|
+
const password = String(body.password ?? "");
|
|
90206
|
+
const database = String(body.database ?? "").trim() || undefined;
|
|
90207
|
+
const access4 = body.access === "readonly" ? "readonly" : "all";
|
|
90208
|
+
if (!isValidDbIdentifier(username))
|
|
90209
|
+
return json({ ok: false, error: "Username must be a valid identifier." }, 422);
|
|
90210
|
+
if (!password)
|
|
90211
|
+
return json({ ok: false, error: "Password is required." }, 422);
|
|
90212
|
+
if (database && !isValidDbIdentifier(database))
|
|
90213
|
+
return json({ ok: false, error: "Database must be a valid identifier." }, 422);
|
|
90214
|
+
return json({ ...await createDatabaseUser(config6, environment, { username, password, database, access: access4 }), username });
|
|
90215
|
+
}
|
|
90216
|
+
if (url.pathname === "/api/dashboard-data") {
|
|
90217
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
90218
|
+
return json(latestData);
|
|
90219
|
+
}
|
|
90220
|
+
if (url.pathname === "/api/actions/run" && req.method === "POST") {
|
|
90221
|
+
const body = await req.json().catch(() => ({}));
|
|
90222
|
+
const action = body.action ? resolveDashboardAction(body.action, environment) : undefined;
|
|
90223
|
+
if (!action)
|
|
90224
|
+
return json({ ok: false, error: "Unknown dashboard action." }, 404);
|
|
90225
|
+
if (action.mutates && body.confirm !== action.confirm)
|
|
90226
|
+
return json({ ok: false, error: `Type "${action.confirm}" to run this action.` }, 409);
|
|
90227
|
+
return json(await runAction(action, { cwd, cliEntry }));
|
|
90228
|
+
}
|
|
90229
|
+
if (url.pathname === "/api/server/operations/run" && req.method === "POST") {
|
|
90230
|
+
const body = await req.json().catch(() => ({}));
|
|
90231
|
+
const operation = body.operation ? resolveDashboardOperation(body.operation, config6, latestData) : undefined;
|
|
90232
|
+
if (!operation)
|
|
90233
|
+
return json({ ok: false, error: "Unknown or unavailable server operation." }, 404);
|
|
90234
|
+
if (operation.mutates && body.confirm !== operation.confirm)
|
|
90235
|
+
return json({ ok: false, error: `Type "${operation.confirm}" to run this operation.` }, 409);
|
|
90236
|
+
return json(await runDashboardOperation(config6, environment, operation, { to: body.to }));
|
|
90237
|
+
}
|
|
90238
|
+
const requestedEnv = url.searchParams.get("env");
|
|
90239
|
+
if (requestedEnv && availableEnvironments.includes(requestedEnv) && requestedEnv !== environment) {
|
|
90240
|
+
environment = requestedEnv;
|
|
90241
|
+
actions = dashboardActions(environment);
|
|
90242
|
+
latestData = await resolveLiveDashboardData(config6, environment);
|
|
90243
|
+
const rebuilt = await buildLiveUi(cwd, latestData);
|
|
90244
|
+
if (rebuilt)
|
|
90245
|
+
activeUiRoot = rebuilt;
|
|
90246
|
+
}
|
|
90247
|
+
return serveStatic(activeUiRoot, url.pathname);
|
|
90248
|
+
} catch (error2) {
|
|
90249
|
+
if (options.verbose)
|
|
90250
|
+
console.error(error2);
|
|
90251
|
+
return json({ ok: false, error: error2?.message ?? String(error2) }, 500);
|
|
90252
|
+
}
|
|
90253
|
+
}
|
|
90254
|
+
});
|
|
90255
|
+
return { server, url: `http://${host}:${server.port}/` };
|
|
90256
|
+
}
|
|
87090
90257
|
// src/index.ts
|
|
87091
90258
|
init_dns();
|
|
87092
90259
|
init_dist2();
|
|
@@ -87120,6 +90287,7 @@ export {
|
|
|
87120
90287
|
suggestCommand,
|
|
87121
90288
|
storageAdvancedManager,
|
|
87122
90289
|
staticSiteManager,
|
|
90290
|
+
startLocalDashboardServer,
|
|
87123
90291
|
stackDependencyManager,
|
|
87124
90292
|
signRequestAsync,
|
|
87125
90293
|
signRequest,
|
|
@@ -87136,6 +90304,7 @@ export {
|
|
|
87136
90304
|
secretsManager,
|
|
87137
90305
|
searchCommands,
|
|
87138
90306
|
sanitizeName,
|
|
90307
|
+
sanitizeCloudConfig,
|
|
87139
90308
|
runRemoteCommand,
|
|
87140
90309
|
runPhpBuildHooks,
|
|
87141
90310
|
runBuildHooks,
|
|
@@ -87164,6 +90333,7 @@ export {
|
|
|
87164
90333
|
resolveExecStart,
|
|
87165
90334
|
resolveDeployBucketName,
|
|
87166
90335
|
resolveDashboardDomain,
|
|
90336
|
+
resolveDashboardAction,
|
|
87167
90337
|
resolveCredentials,
|
|
87168
90338
|
resolveCloudProvider,
|
|
87169
90339
|
resolveApp,
|
|
@@ -87330,6 +90500,7 @@ export {
|
|
|
87330
90500
|
defaultLocalConfig,
|
|
87331
90501
|
defaultConfig4 as defaultConfig,
|
|
87332
90502
|
databaseUserManager,
|
|
90503
|
+
dashboardActions,
|
|
87333
90504
|
crossRegionReferenceManager,
|
|
87334
90505
|
createZip,
|
|
87335
90506
|
createWordPressPreset,
|