@stacksjs/ts-cloud 0.7.90 → 0.7.92
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 +388 -377
- package/dist/bin/dashboard-server.js +254 -243
- package/dist/{chunk-hcdewpt6.js → chunk-29fwqkb7.js} +1 -1
- package/dist/{chunk-gcehnzty.js → chunk-fbwcv2vf.js} +90 -17
- package/dist/deploy/index.js +2 -2
- package/dist/drivers/hetzner/driver.d.ts +7 -0
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/shared/rpx-gateway.d.ts +4 -0
- package/dist/index.js +2 -2
- package/dist/ui/access-denied.html +2 -2
- package/dist/ui/account/automation.html +4 -4
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +3 -3
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +4 -4
- package/dist/ui/index.html +4 -4
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +4 -4
- package/dist/ui/operations/maintenance.html +3 -3
- package/dist/ui/operations/observability.html +4 -4
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +4 -4
- package/dist/ui/operations/releases.html +3 -3
- package/dist/ui/operations/workloads.html +3 -3
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +4 -4
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +4 -4
- package/dist/ui/server/fleet.html +4 -4
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +4 -4
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +4 -4
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +3 -3
- package/dist/ui/serverless.html +4 -4
- package/package.json +3 -3
|
@@ -1347,6 +1347,35 @@ function writeFileHeredoc(path, content, delimiter, mode = "0644") {
|
|
|
1347
1347
|
`chmod ${mode} ${path}`
|
|
1348
1348
|
];
|
|
1349
1349
|
}
|
|
1350
|
+
function shellSingleQuote(value) {
|
|
1351
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1352
|
+
}
|
|
1353
|
+
function writeRpxFragment(path, content, delimiter, options) {
|
|
1354
|
+
if (!options.preserveManagementDashboardRoutes)
|
|
1355
|
+
return writeFileHeredoc(path, content, delimiter, "0600");
|
|
1356
|
+
const mergeScript = `
|
|
1357
|
+
const { readFileSync, writeFileSync } = require('node:fs')
|
|
1358
|
+
const currentPath = process.env.TS_CLOUD_RPX_CURRENT_FRAGMENT
|
|
1359
|
+
const candidatePath = process.env.TS_CLOUD_RPX_CANDIDATE_FRAGMENT
|
|
1360
|
+
const isDashboard = route => /^(?:dashboard|cloud)\\./i.test(String(route?.to || ''))
|
|
1361
|
+
const current = JSON.parse(readFileSync(currentPath, 'utf8'))
|
|
1362
|
+
const candidate = JSON.parse(readFileSync(candidatePath, 'utf8'))
|
|
1363
|
+
const retained = Array.isArray(current.proxies) ? current.proxies.filter(isDashboard) : []
|
|
1364
|
+
const next = Array.isArray(candidate.proxies) ? candidate.proxies.filter(route => !isDashboard(route)) : []
|
|
1365
|
+
candidate.proxies = [...next, ...retained]
|
|
1366
|
+
writeFileSync(candidatePath, JSON.stringify(candidate, null, 2) + '\\n', { mode: 0o600 })
|
|
1367
|
+
`.trim();
|
|
1368
|
+
return [
|
|
1369
|
+
`__tsc_fragment_candidate="$(mktemp "${path}.candidate.XXXXXX")"`,
|
|
1370
|
+
`cat > "$__tsc_fragment_candidate" <<'${delimiter}'`,
|
|
1371
|
+
content,
|
|
1372
|
+
delimiter,
|
|
1373
|
+
'chmod 0600 "$__tsc_fragment_candidate"',
|
|
1374
|
+
`if [ -f ${path} ]; then TS_CLOUD_RPX_CURRENT_FRAGMENT=${path} TS_CLOUD_RPX_CANDIDATE_FRAGMENT="$__tsc_fragment_candidate" ${options.bunBin} -e ${shellSingleQuote(mergeScript)}; fi`,
|
|
1375
|
+
`mv -f "$__tsc_fragment_candidate" ${path}`,
|
|
1376
|
+
`chmod 0600 ${path}`
|
|
1377
|
+
];
|
|
1378
|
+
}
|
|
1350
1379
|
function rpxCertRenewServiceName(slug) {
|
|
1351
1380
|
const safeSlug = (slug || "app").replace(/[^a-z0-9._-]+/gi, "-");
|
|
1352
1381
|
return `rpx-cert-renew-${safeSlug}.service`;
|
|
@@ -1459,7 +1488,10 @@ function buildRpxProvisionScript(options) {
|
|
|
1459
1488
|
`mv ${RPX_INSTALL_DIR}.next ${RPX_INSTALL_DIR}`,
|
|
1460
1489
|
`rm -rf ${RPX_INSTALL_DIR}.prev`,
|
|
1461
1490
|
`ln -sfn ${RPX_INSTALL_DIR}/node_modules ${RPX_DIR}/node_modules`,
|
|
1462
|
-
...
|
|
1491
|
+
...writeRpxFragment(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF", {
|
|
1492
|
+
bunBin,
|
|
1493
|
+
preserveManagementDashboardRoutes: options.preserveManagementDashboardRoutes
|
|
1494
|
+
}),
|
|
1463
1495
|
...writeFileHeredoc(RPX_LAUNCHER_PATH, assembler, "TS_CLOUD_RPX_EOF"),
|
|
1464
1496
|
`${bunBin} build --production --compile --outfile ${RPX_BINARY_PATH}.next ${RPX_LAUNCHER_PATH}`,
|
|
1465
1497
|
`chmod 0755 ${RPX_BINARY_PATH}.next`,
|
|
@@ -1508,7 +1540,10 @@ function buildRpxFragmentRefreshScript(options) {
|
|
|
1508
1540
|
return [
|
|
1509
1541
|
"set -euo pipefail",
|
|
1510
1542
|
`mkdir -p ${RPX_SITES_DIR}`,
|
|
1511
|
-
...
|
|
1543
|
+
...writeRpxFragment(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF", {
|
|
1544
|
+
bunBin: "/usr/local/bin/bun",
|
|
1545
|
+
preserveManagementDashboardRoutes: options.preserveManagementDashboardRoutes
|
|
1546
|
+
}),
|
|
1512
1547
|
`systemctl restart ${RPX_SERVICE_NAME}`
|
|
1513
1548
|
];
|
|
1514
1549
|
}
|
|
@@ -2649,11 +2684,17 @@ class HetznerDriver {
|
|
|
2649
2684
|
sshPublicKeyPath;
|
|
2650
2685
|
sshUser;
|
|
2651
2686
|
location;
|
|
2687
|
+
stateOnly;
|
|
2652
2688
|
waitForBoot;
|
|
2653
2689
|
bootWait;
|
|
2654
2690
|
constructor(options = {}) {
|
|
2691
|
+
const apiToken = resolveHetznerApiToken(options.apiToken);
|
|
2692
|
+
this.stateOnly = !options.client && !apiToken && options.allowStateOnly === true;
|
|
2693
|
+
if (!options.client && !apiToken && !this.stateOnly) {
|
|
2694
|
+
throw new Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");
|
|
2695
|
+
}
|
|
2655
2696
|
this.client = options.client ?? new HetznerClient({
|
|
2656
|
-
apiToken:
|
|
2697
|
+
apiToken: apiToken ?? ""
|
|
2657
2698
|
});
|
|
2658
2699
|
const settings = resolveHetznerSettings(undefined, {
|
|
2659
2700
|
sshPrivateKeyPath: options.sshPrivateKeyPath,
|
|
@@ -3259,7 +3300,6 @@ class HetznerDriver {
|
|
|
3259
3300
|
return { artifactRef: remotePath };
|
|
3260
3301
|
}
|
|
3261
3302
|
async findComputeTargets(options) {
|
|
3262
|
-
const servers = await this.client.listServers();
|
|
3263
3303
|
const role = options.role || "app";
|
|
3264
3304
|
const toTarget = (server) => ({
|
|
3265
3305
|
id: String(server.id),
|
|
@@ -3274,25 +3314,48 @@ class HetznerDriver {
|
|
|
3274
3314
|
const pinnedIds = [
|
|
3275
3315
|
...new Set(role === "app" ? [state?.serverId, ...state?.appServerIds ?? []] : role === "lb" ? [state?.lbServerId] : [state?.servicesServerId])
|
|
3276
3316
|
].filter((id) => typeof id === "number");
|
|
3317
|
+
if (this.stateOnly) {
|
|
3318
|
+
if (pinnedIds.length > 0 && state?.publicIp) {
|
|
3319
|
+
return pinnedIds.map((id) => ({
|
|
3320
|
+
id: String(id),
|
|
3321
|
+
name: id === state.serverId ? state.serverName : undefined,
|
|
3322
|
+
publicIp: state.publicIp,
|
|
3323
|
+
publicIpv6: state.publicIpv6,
|
|
3324
|
+
status: "running"
|
|
3325
|
+
}));
|
|
3326
|
+
}
|
|
3327
|
+
if (role !== "app")
|
|
3328
|
+
return [];
|
|
3329
|
+
throw new Error(`Hetzner API token required because '${options.stackName ?? `${options.slug}-${options.environment}`}' has no complete local compute state pin.`);
|
|
3330
|
+
}
|
|
3331
|
+
const servers2 = await this.client.listServers();
|
|
3277
3332
|
if (pinnedIds.length > 0) {
|
|
3278
3333
|
const pinned = [];
|
|
3279
3334
|
for (const id of pinnedIds) {
|
|
3280
|
-
const server =
|
|
3335
|
+
const server = servers2.find((candidate) => candidate.id === id) ?? await this.tryGetServer(id);
|
|
3281
3336
|
if (server && server.status !== "off")
|
|
3282
3337
|
pinned.push(server);
|
|
3283
3338
|
}
|
|
3284
3339
|
if (pinned.length > 0)
|
|
3285
3340
|
return pinned.map(toTarget);
|
|
3286
3341
|
}
|
|
3342
|
+
const exact2 = servers2.filter((server) => matchesTsCloudLabels(server.labels, options.slug, options.environment, role));
|
|
3343
|
+
if (exact2.length > 0)
|
|
3344
|
+
return exact2.map(toTarget);
|
|
3345
|
+
if (role === "app") {
|
|
3346
|
+
const candidates = servers2.filter((server) => server.status !== "off" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/managed-by`] === "ts-cloud" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/environment`] === options.environment && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/role`] === "app");
|
|
3347
|
+
if (candidates.length === 1)
|
|
3348
|
+
return candidates.map(toTarget);
|
|
3349
|
+
}
|
|
3350
|
+
return [];
|
|
3351
|
+
}
|
|
3352
|
+
if (this.stateOnly) {
|
|
3353
|
+
throw new Error(`Hetzner API token required to discover '${role}' compute targets.`);
|
|
3287
3354
|
}
|
|
3355
|
+
const servers = await this.client.listServers();
|
|
3288
3356
|
const exact = servers.filter((server) => matchesTsCloudLabels(server.labels, options.slug, options.environment, role));
|
|
3289
3357
|
if (exact.length > 0)
|
|
3290
3358
|
return exact.map(toTarget);
|
|
3291
|
-
if (role === "app") {
|
|
3292
|
-
const candidates = servers.filter((server) => server.status !== "off" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/managed-by`] === "ts-cloud" && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/environment`] === options.environment && server.labels?.[`${TS_CLOUD_LABEL_PREFIX}/role`] === "app");
|
|
3293
|
-
if (candidates.length === 1)
|
|
3294
|
-
return candidates.map(toTarget);
|
|
3295
|
-
}
|
|
3296
3359
|
return [];
|
|
3297
3360
|
}
|
|
3298
3361
|
async attachToComputeInfrastructure(ownerSlug, config, environment) {
|
|
@@ -3604,6 +3667,7 @@ function createCloudDriver(options) {
|
|
|
3604
3667
|
case "hetzner":
|
|
3605
3668
|
return new HetznerDriver({
|
|
3606
3669
|
apiToken: options.config.hetzner?.apiToken,
|
|
3670
|
+
allowStateOnly: Boolean(options.config.cloud?.attachTo),
|
|
3607
3671
|
sshPrivateKeyPath: options.config.hetzner?.sshPrivateKeyPath,
|
|
3608
3672
|
sshPublicKeyPath: options.config.hetzner?.sshPublicKeyPath,
|
|
3609
3673
|
sshUser: options.config.hetzner?.sshUser,
|
|
@@ -5880,16 +5944,16 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
5880
5944
|
perInstance: result.perInstance
|
|
5881
5945
|
};
|
|
5882
5946
|
}
|
|
5883
|
-
function
|
|
5947
|
+
function shellSingleQuote2(value) {
|
|
5884
5948
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
5885
5949
|
}
|
|
5886
5950
|
function buildManagementDashboardServiceReconciliationScript(slug, desiredSiteNames, serverOwner = false, desiredDomains = []) {
|
|
5887
5951
|
const prefix = `${slug}-dashboard-`;
|
|
5888
5952
|
const desiredUnits = desiredSiteNames.map((siteName) => `${slug}-${siteName}.service`);
|
|
5889
5953
|
const routeReconciliation = serverOwner ? [
|
|
5890
|
-
`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${
|
|
5954
|
+
`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${shellSingleQuote2(JSON.stringify(desiredDomains))}`,
|
|
5891
5955
|
"rm -f /run/ts-cloud-dashboard-routes-changed",
|
|
5892
|
-
`/usr/local/bin/bun --bun -e ${
|
|
5956
|
+
`/usr/local/bin/bun --bun -e ${shellSingleQuote2(`
|
|
5893
5957
|
const { chmodSync, existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } = require('node:fs')
|
|
5894
5958
|
const { basename, join } = require('node:path')
|
|
5895
5959
|
const root = '/etc/rpx/sites.d'
|
|
@@ -5925,9 +5989,9 @@ function buildManagementDashboardServiceReconciliationScript(slug, desiredSiteNa
|
|
|
5925
5989
|
] : [];
|
|
5926
5990
|
return [
|
|
5927
5991
|
"set -euo pipefail",
|
|
5928
|
-
`TS_CLOUD_DASHBOARD_PREFIX=${
|
|
5992
|
+
`TS_CLOUD_DASHBOARD_PREFIX=${shellSingleQuote2(prefix)}`,
|
|
5929
5993
|
`TS_CLOUD_DASHBOARD_SERVER_OWNER=${serverOwner ? "1" : "0"}`,
|
|
5930
|
-
`TS_CLOUD_DASHBOARD_DESIRED=${
|
|
5994
|
+
`TS_CLOUD_DASHBOARD_DESIRED=${shellSingleQuote2(` ${desiredUnits.join(" ")} `)}`,
|
|
5931
5995
|
"for TS_CLOUD_UNIT_FILE in /etc/systemd/system/*.service; do",
|
|
5932
5996
|
' [ -e "$TS_CLOUD_UNIT_FILE" ] || continue',
|
|
5933
5997
|
' TS_CLOUD_UNIT=$(basename "$TS_CLOUD_UNIT_FILE")',
|
|
@@ -6140,7 +6204,11 @@ async function reloadRpxGateway(options) {
|
|
|
6140
6204
|
logger.step(`Reloading the load balancer's rpx gateway with ${lbConfig.proxies.length} route(s)...`);
|
|
6141
6205
|
const result2 = await driver.runRemoteDeploy({
|
|
6142
6206
|
targets: lbTargets,
|
|
6143
|
-
commands: buildRpxFragmentRefreshScript({
|
|
6207
|
+
commands: buildRpxFragmentRefreshScript({
|
|
6208
|
+
config: lbConfig,
|
|
6209
|
+
slug,
|
|
6210
|
+
preserveManagementDashboardRoutes: options.managementDashboard === false
|
|
6211
|
+
}),
|
|
6144
6212
|
comment: `ts-cloud rpx gateway reload ${slug}`,
|
|
6145
6213
|
tags: {
|
|
6146
6214
|
Project: slug,
|
|
@@ -6171,7 +6239,12 @@ async function reloadRpxGateway(options) {
|
|
|
6171
6239
|
return true;
|
|
6172
6240
|
}
|
|
6173
6241
|
logger.step(`Reloading rpx gateway with ${rpxConfig.proxies.length} route(s)...`);
|
|
6174
|
-
const script = buildRpxProvisionScript({
|
|
6242
|
+
const script = buildRpxProvisionScript({
|
|
6243
|
+
proxy,
|
|
6244
|
+
config: rpxConfig,
|
|
6245
|
+
slug,
|
|
6246
|
+
preserveManagementDashboardRoutes: options.managementDashboard === false
|
|
6247
|
+
});
|
|
6175
6248
|
const result = await driver.runRemoteDeploy({
|
|
6176
6249
|
targets,
|
|
6177
6250
|
commands: script,
|
package/dist/deploy/index.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
synchronizeDashboardUsers,
|
|
31
31
|
trackDashboardOperation,
|
|
32
32
|
verifyStaticApiOrigin
|
|
33
|
-
} from "../chunk-
|
|
33
|
+
} from "../chunk-29fwqkb7.js";
|
|
34
34
|
import {
|
|
35
35
|
deleteStaticSite,
|
|
36
36
|
deployStaticSite,
|
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
siteInstallBase,
|
|
71
71
|
validateDeploymentConfig,
|
|
72
72
|
verifyAddressRecord
|
|
73
|
-
} from "../chunk-
|
|
73
|
+
} from "../chunk-fbwcv2vf.js";
|
|
74
74
|
import"../chunk-hmehkeqx.js";
|
|
75
75
|
import"../chunk-7m60qnc8.js";
|
|
76
76
|
import"../chunk-4cjrg98a.js";
|
|
@@ -3,6 +3,12 @@ import { HetznerClient } from './client';
|
|
|
3
3
|
export declare function formatSshFailure(error: unknown): string;
|
|
4
4
|
export interface HetznerDriverOptions {
|
|
5
5
|
apiToken?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Permit SSH-only deploy operations to use an existing local state pin when
|
|
8
|
+
* no provider token is available. Intended for tenants attached to compute
|
|
9
|
+
* owned and provisioned by another project.
|
|
10
|
+
*/
|
|
11
|
+
allowStateOnly?: boolean;
|
|
6
12
|
sshPrivateKeyPath?: string;
|
|
7
13
|
sshPublicKeyPath?: string;
|
|
8
14
|
sshUser?: string;
|
|
@@ -38,6 +44,7 @@ export declare class HetznerDriver implements CloudDriver {
|
|
|
38
44
|
private sshPublicKeyPath;
|
|
39
45
|
private sshUser;
|
|
40
46
|
private location;
|
|
47
|
+
private stateOnly;
|
|
41
48
|
private waitForBoot;
|
|
42
49
|
private bootWait;
|
|
43
50
|
constructor(options?: HetznerDriverOptions);
|
package/dist/drivers/index.js
CHANGED
|
@@ -272,6 +272,8 @@ export interface BuildRpxProvisionOptions {
|
|
|
272
272
|
slug?: string;
|
|
273
273
|
/** Absolute path to the `bun` binary on the box. @default '/usr/local/bin/bun' */
|
|
274
274
|
bunBin?: string;
|
|
275
|
+
/** Keep the dashboard route currently running on the box during an app-only deploy. */
|
|
276
|
+
preserveManagementDashboardRoutes?: boolean;
|
|
275
277
|
}
|
|
276
278
|
export declare const RPX_CERT_RENEW_SCRIPT = "/etc/rpx/renew-certs.sh";
|
|
277
279
|
export declare const RPX_CERT_RENEW_SERVICE = "rpx-cert-renew.service";
|
|
@@ -310,6 +312,8 @@ export interface BuildRpxFragmentRefreshOptions {
|
|
|
310
312
|
* Defaults to `'app'`.
|
|
311
313
|
*/
|
|
312
314
|
slug?: string;
|
|
315
|
+
/** Keep the dashboard route currently running on the box during an app-only deploy. */
|
|
316
|
+
preserveManagementDashboardRoutes?: boolean;
|
|
313
317
|
}
|
|
314
318
|
/**
|
|
315
319
|
* Build the shell commands that rewrite ONLY this app's rpx route fragment
|
package/dist/index.js
CHANGED
|
@@ -283,7 +283,7 @@ import {
|
|
|
283
283
|
volumeCapabilities,
|
|
284
284
|
webhookEndpoint,
|
|
285
285
|
zeroCapacity
|
|
286
|
-
} from "./chunk-
|
|
286
|
+
} from "./chunk-29fwqkb7.js";
|
|
287
287
|
import {
|
|
288
288
|
deleteStaticSite,
|
|
289
289
|
deployStaticSite,
|
|
@@ -403,7 +403,7 @@ import {
|
|
|
403
403
|
waitForCloudInit,
|
|
404
404
|
waitForSsh,
|
|
405
405
|
wrapCloudInitUserData
|
|
406
|
-
} from "./chunk-
|
|
406
|
+
} from "./chunk-fbwcv2vf.js";
|
|
407
407
|
import {
|
|
408
408
|
ABTestManager,
|
|
409
409
|
AI,
|