@stacksjs/ts-cloud 0.7.3 → 0.7.5
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 +361 -361
- package/dist/drivers/aws/driver.d.ts +9 -0
- package/dist/drivers/hetzner/provision.d.ts +53 -0
- package/dist/drivers/index.d.ts +5 -1
- package/dist/drivers/shared/remote-exec.d.ts +47 -0
- package/dist/index.js +110 -45
- package/dist/ui/index.html +3 -3
- package/dist/ui/server/actions.html +3 -3
- package/dist/ui/server/backups.html +3 -3
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +3 -3
- package/dist/ui/server/firewall.html +3 -3
- package/dist/ui/server/logs.html +3 -3
- package/dist/ui/server/services.html +3 -3
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/terminal.html +3 -3
- package/dist/ui/server/workers.html +3 -3
- package/dist/ui/serverless/alarms.html +3 -3
- package/dist/ui/serverless/assets.html +3 -3
- package/dist/ui/serverless/data.html +3 -3
- package/dist/ui/serverless/deployments.html +3 -3
- package/dist/ui/serverless/functions.html +3 -3
- package/dist/ui/serverless/logs.html +3 -3
- package/dist/ui/serverless/queues.html +3 -3
- package/dist/ui/serverless/scheduler.html +3 -3
- package/dist/ui/serverless/secrets.html +3 -3
- package/dist/ui/serverless/traces.html +3 -3
- package/dist/ui/serverless.html +3 -3
- package/package.json +3 -3
|
@@ -2,6 +2,14 @@ import type { CloudDriver, ComputeStackOutputs, ComputeTarget, FindComputeTarget
|
|
|
2
2
|
export interface AwsDriverOptions {
|
|
3
3
|
region?: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Local-state pin (parity with the Hetzner driver's shared-box support): a
|
|
7
|
+
* project riding an instance whose tags belong to another project records
|
|
8
|
+
* `{ "instanceId": "i-..." }` in `.ts-cloud/state/<stack>.json`, and target
|
|
9
|
+
* lookups trust that record when the tag scan finds nothing. Exported for
|
|
10
|
+
* tests.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readPinnedInstanceId(stackName: string): string | null;
|
|
5
13
|
export declare class AwsDriver implements CloudDriver {
|
|
6
14
|
readonly name: "aws";
|
|
7
15
|
readonly usesCloudFormation = true;
|
|
@@ -26,6 +34,7 @@ export declare class AwsDriver implements CloudDriver {
|
|
|
26
34
|
getComputeOutputs(options: ProvisionComputeOptions): Promise<ComputeStackOutputs>;
|
|
27
35
|
uploadRelease(options: UploadReleaseOptions): Promise<UploadReleaseResult>;
|
|
28
36
|
findComputeTargets(options: FindComputeTargetsOptions): Promise<ComputeTarget[]>;
|
|
37
|
+
private reservationsToTargets;
|
|
29
38
|
/**
|
|
30
39
|
* SSM AWS-RunShellScript executes the joined commands with `/bin/sh` (dash on
|
|
31
40
|
* Ubuntu), which rejects bash-only syntax like `set -o pipefail`. Our deploy
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent find-or-create provisioning helpers on top of the Hetzner client.
|
|
3
|
+
*
|
|
4
|
+
* These are the building blocks for "deploy X onto a Hetzner box" scripts:
|
|
5
|
+
* each helper reuses an existing resource when one matches (by name, or by
|
|
6
|
+
* key body for SSH keys) and creates it otherwise, so re-running a deploy
|
|
7
|
+
* converges instead of failing on duplicates.
|
|
8
|
+
*/
|
|
9
|
+
import type { CreateServerOptions, HetznerClient, HetznerFirewallRule, HetznerServer } from './client';
|
|
10
|
+
export interface EnsuredResource {
|
|
11
|
+
id: number;
|
|
12
|
+
name: string;
|
|
13
|
+
created: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface EnsureSshKeyOptions {
|
|
16
|
+
/** Name used if the key needs to be registered. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** OpenSSH public key (`<type> <base64> [comment]`). */
|
|
19
|
+
publicKey: string;
|
|
20
|
+
labels?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Register an SSH public key, reusing any already-registered key with the
|
|
24
|
+
* same body (comment/whitespace differences ignored) regardless of its name.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensureSshKey(client: HetznerClient, options: EnsureSshKeyOptions): Promise<EnsuredResource>;
|
|
27
|
+
export interface EnsureFirewallOptions {
|
|
28
|
+
name: string;
|
|
29
|
+
rules: HetznerFirewallRule[];
|
|
30
|
+
labels?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Find a firewall by name and sync its rules to the given set, or create it.
|
|
34
|
+
* Rules are replaced in-place on reuse so the firewall always matches the
|
|
35
|
+
* declared config.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureFirewall(client: HetznerClient, options: EnsureFirewallOptions): Promise<EnsuredResource>;
|
|
38
|
+
export interface EnsureServerOptions extends CreateServerOptions {
|
|
39
|
+
/** Wait until the (created or reused) server reaches `running`. @default true */
|
|
40
|
+
waitForRunning?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface EnsuredServer {
|
|
43
|
+
server: HetznerServer;
|
|
44
|
+
created: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Find a server by name or create it, waiting until it is running by default.
|
|
48
|
+
* Creation options (`userData`, `sshKeys`, `firewalls`, ...) only apply when
|
|
49
|
+
* the server does not exist yet — an existing server is reused as-is.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureServer(client: HetznerClient, options: EnsureServerOptions): Promise<EnsuredServer>;
|
|
52
|
+
/** The public IPv4 of a server, throwing when it has none. */
|
|
53
|
+
export declare function serverPublicIpv4(server: HetznerServer): string;
|
package/dist/drivers/index.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ export * from './factory';
|
|
|
2
2
|
export { AwsDriver } from './aws/driver';
|
|
3
3
|
export { HetznerDriver } from './hetzner/driver';
|
|
4
4
|
export { isBoxMode, LocalBoxDriver } from './local-box/driver';
|
|
5
|
-
export { HetznerClient, resolveHetznerApiToken } from './hetzner/client';
|
|
5
|
+
export { HetznerClient, normalizeSshPublicKey, resolveHetznerApiToken } from './hetzner/client';
|
|
6
6
|
export { generateUbuntuAppCloudInit, wrapCloudInitUserData } from './hetzner/cloud-init';
|
|
7
|
+
export { ensureFirewall, ensureServer, ensureSshKey, serverPublicIpv4 } from './hetzner/provision';
|
|
8
|
+
export type { EnsuredResource, EnsuredServer, EnsureFirewallOptions, EnsureServerOptions, EnsureSshKeyOptions, } from './hetzner/provision';
|
|
9
|
+
export { buildSshArgs, scpUpload, sshExec, sshExecOrThrow, waitForCloudInit, waitForSsh } from './shared/remote-exec';
|
|
10
|
+
export type { RemoteExecOptions, RemoteExecResult, WaitOptions } from './shared/remote-exec';
|
|
7
11
|
export { buildAwsArtifactFetch, buildLocalArtifactFetch, buildSiteDeployScript, buildStaticSiteDeployScript, resolveExecStart, } from './shared/deploy-script';
|
|
8
12
|
export { deployAllComputeSites, deploySiteRelease, reloadRpxGateway } from './shared/compute-deploy';
|
|
9
13
|
export { buildRpxConfig, buildRpxLbConfig, buildRpxProvisionScript, deriveRouteId, normalizeRoutePath, renderRpxLauncher, DEFAULT_RPX_CERTS_DIR, RPX_DIR, RPX_LAUNCHER_PATH, RPX_SERVICE_NAME, } from './shared/rpx-gateway';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SSH/scp helpers for driving a freshly-provisioned box from a deploy
|
|
3
|
+
* script, plus wait loops for the boot milestones every provider shares
|
|
4
|
+
* (sshd accepting connections, cloud-init finished).
|
|
5
|
+
*
|
|
6
|
+
* Uses the system `ssh`/`scp` binaries via Bun.spawn — no keys or agents are
|
|
7
|
+
* managed here; pass `identityFile` or rely on the ambient SSH config.
|
|
8
|
+
*/
|
|
9
|
+
export interface RemoteExecOptions {
|
|
10
|
+
/** SSH user. @default 'root' */
|
|
11
|
+
user?: string;
|
|
12
|
+
/** Private key passed as `ssh -i`. Omit to use the ambient SSH config/agent. */
|
|
13
|
+
identityFile?: string;
|
|
14
|
+
/** SSH `ConnectTimeout` in seconds. @default 10 */
|
|
15
|
+
connectTimeoutSec?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RemoteExecResult {
|
|
18
|
+
code: number;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The ssh/scp CLI arguments implied by {@link RemoteExecOptions}. Host key
|
|
24
|
+
* checking is disabled because these helpers target freshly-created servers
|
|
25
|
+
* whose host keys cannot be known in advance.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildSshArgs(options?: RemoteExecOptions): string[];
|
|
28
|
+
/** Run a command on the host over SSH, capturing exit code and output. */
|
|
29
|
+
export declare function sshExec(host: string, command: string, options?: RemoteExecOptions): Promise<RemoteExecResult>;
|
|
30
|
+
/** Like {@link sshExec}, but throws on non-zero exit and returns stdout. */
|
|
31
|
+
export declare function sshExecOrThrow(host: string, command: string, options?: RemoteExecOptions): Promise<string>;
|
|
32
|
+
/** Copy local files into a directory on the host via scp. */
|
|
33
|
+
export declare function scpUpload(host: string, localPaths: string[], remoteDir: string, options?: RemoteExecOptions): Promise<void>;
|
|
34
|
+
export interface WaitOptions extends RemoteExecOptions {
|
|
35
|
+
/** Give up after this long. */
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
/** Delay between attempts. @default 5000 */
|
|
38
|
+
pollIntervalMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Poll until sshd accepts the connection (box booted + key authorized). */
|
|
41
|
+
export declare function waitForSsh(host: string, options?: WaitOptions): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Poll `cloud-init status` until it reports done. Throws when cloud-init
|
|
44
|
+
* reports an error so a broken first boot fails the deploy instead of
|
|
45
|
+
* surfacing later as missing packages.
|
|
46
|
+
*/
|
|
47
|
+
export declare function waitForCloudInit(host: string, options?: WaitOptions): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -84592,12 +84592,12 @@ import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
|
|
|
84592
84592
|
import { readFile as readFile2, writeFile as writeFile6 } from "node:fs/promises";
|
|
84593
84593
|
import { mkdtempSync as mkdtempSync6 } from "node:fs";
|
|
84594
84594
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
84595
|
-
import { dirname as dirname9, extname as extname3, join as
|
|
84595
|
+
import { dirname as dirname9, extname as extname3, join as join25, normalize } from "node:path";
|
|
84596
84596
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
84597
84597
|
|
|
84598
84598
|
// src/deploy/dashboard-data-server.ts
|
|
84599
84599
|
import { existsSync as existsSync24, readFileSync as readFileSync18 } from "node:fs";
|
|
84600
|
-
import { join as
|
|
84600
|
+
import { join as join23 } from "node:path";
|
|
84601
84601
|
|
|
84602
84602
|
// src/drivers/factory.ts
|
|
84603
84603
|
await init_dist2();
|
|
@@ -84606,6 +84606,7 @@ await init_dist2();
|
|
|
84606
84606
|
init_cloudformation();
|
|
84607
84607
|
await init_dist2();
|
|
84608
84608
|
import { readFileSync as readFileSync15 } from "node:fs";
|
|
84609
|
+
import { join as join18 } from "node:path";
|
|
84609
84610
|
init_s3();
|
|
84610
84611
|
|
|
84611
84612
|
// src/drivers/shared/package-manager.ts
|
|
@@ -85716,6 +85717,16 @@ function resolveAwsImageId(config6) {
|
|
|
85716
85717
|
}
|
|
85717
85718
|
|
|
85718
85719
|
// src/drivers/aws/driver.ts
|
|
85720
|
+
function readPinnedInstanceId(stackName) {
|
|
85721
|
+
try {
|
|
85722
|
+
const raw = readFileSync15(join18(process.cwd(), ".ts-cloud/state", `${stackName}.json`), "utf8");
|
|
85723
|
+
const state = JSON.parse(raw);
|
|
85724
|
+
return typeof state.instanceId === "string" && state.instanceId.length > 0 ? state.instanceId : null;
|
|
85725
|
+
} catch {
|
|
85726
|
+
return null;
|
|
85727
|
+
}
|
|
85728
|
+
}
|
|
85729
|
+
|
|
85719
85730
|
class AwsDriver {
|
|
85720
85731
|
name = "aws";
|
|
85721
85732
|
usesCloudFormation = true;
|
|
@@ -85893,8 +85904,33 @@ class AwsDriver {
|
|
|
85893
85904
|
{ Name: "instance-state-name", Values: ["running", "pending"] }
|
|
85894
85905
|
];
|
|
85895
85906
|
const result = await ec22.describeInstances({ Filters: filters });
|
|
85907
|
+
const targets = this.reservationsToTargets(result.Reservations);
|
|
85908
|
+
if (targets.length > 0 || (options.role || "app") !== "app")
|
|
85909
|
+
return targets;
|
|
85910
|
+
const stackName = options.stackName ?? `${options.slug}-${options.environment}`;
|
|
85911
|
+
let pinnedId = readPinnedInstanceId(stackName);
|
|
85912
|
+
if (!pinnedId) {
|
|
85913
|
+
try {
|
|
85914
|
+
pinnedId = (await new CloudFormationClient(region).getStackOutputs(stackName)).appInstanceId ?? null;
|
|
85915
|
+
} catch {
|
|
85916
|
+
pinnedId = null;
|
|
85917
|
+
}
|
|
85918
|
+
}
|
|
85919
|
+
if (!pinnedId)
|
|
85920
|
+
return [];
|
|
85921
|
+
try {
|
|
85922
|
+
const pinned = await ec22.describeInstances({
|
|
85923
|
+
InstanceIds: [pinnedId],
|
|
85924
|
+
Filters: [{ Name: "instance-state-name", Values: ["running", "pending"] }]
|
|
85925
|
+
});
|
|
85926
|
+
return this.reservationsToTargets(pinned.Reservations);
|
|
85927
|
+
} catch {
|
|
85928
|
+
return [];
|
|
85929
|
+
}
|
|
85930
|
+
}
|
|
85931
|
+
reservationsToTargets(reservations) {
|
|
85896
85932
|
const targets = [];
|
|
85897
|
-
for (const reservation of
|
|
85933
|
+
for (const reservation of reservations || []) {
|
|
85898
85934
|
for (const instance of reservation.Instances || []) {
|
|
85899
85935
|
if (!instance.InstanceId)
|
|
85900
85936
|
continue;
|
|
@@ -85987,7 +86023,7 @@ class AwsDriver {
|
|
|
85987
86023
|
await init_dist2();
|
|
85988
86024
|
import { existsSync as existsSync20, readFileSync as readFileSync16 } from "node:fs";
|
|
85989
86025
|
import { homedir as homedir8 } from "node:os";
|
|
85990
|
-
import { join as
|
|
86026
|
+
import { join as join20 } from "node:path";
|
|
85991
86027
|
import { execSync as execSync3 } from "node:child_process";
|
|
85992
86028
|
|
|
85993
86029
|
// src/drivers/hetzner/client.ts
|
|
@@ -86291,6 +86327,17 @@ function buildRpxConfigInternal(sites, options, appBoxes) {
|
|
|
86291
86327
|
}
|
|
86292
86328
|
domains.add(site.domain);
|
|
86293
86329
|
}
|
|
86330
|
+
if (options.proxy.autoWww !== false) {
|
|
86331
|
+
for (const domain of [...domains]) {
|
|
86332
|
+
if (domain.split(".").length !== 2)
|
|
86333
|
+
continue;
|
|
86334
|
+
const wwwDomain = `www.${domain}`;
|
|
86335
|
+
if (domains.has(wwwDomain))
|
|
86336
|
+
continue;
|
|
86337
|
+
proxies.push({ to: wwwDomain, redirect: { to: `https://${domain}` }, id: deriveRouteId(wwwDomain) });
|
|
86338
|
+
domains.add(wwwDomain);
|
|
86339
|
+
}
|
|
86340
|
+
}
|
|
86294
86341
|
proxies.sort((a, b) => {
|
|
86295
86342
|
if (a.to !== b.to)
|
|
86296
86343
|
return a.to.localeCompare(b.to);
|
|
@@ -86607,10 +86654,10 @@ function matchesTsCloudLabels(labels, slug, environment, role = "app") {
|
|
|
86607
86654
|
|
|
86608
86655
|
// src/drivers/hetzner/state.ts
|
|
86609
86656
|
import { mkdir as mkdir4, readFile, writeFile as writeFile4 } from "node:fs/promises";
|
|
86610
|
-
import { join as
|
|
86657
|
+
import { join as join19 } from "node:path";
|
|
86611
86658
|
var STATE_DIR = ".ts-cloud/state";
|
|
86612
86659
|
function driverStatePath(stackName) {
|
|
86613
|
-
return
|
|
86660
|
+
return join19(process.cwd(), STATE_DIR, `${stackName}.json`);
|
|
86614
86661
|
}
|
|
86615
86662
|
async function readDriverState(stackName) {
|
|
86616
86663
|
try {
|
|
@@ -86622,7 +86669,7 @@ async function readDriverState(stackName) {
|
|
|
86622
86669
|
}
|
|
86623
86670
|
async function writeDriverState(stackName, state) {
|
|
86624
86671
|
const path = driverStatePath(stackName);
|
|
86625
|
-
await mkdir4(
|
|
86672
|
+
await mkdir4(join19(process.cwd(), STATE_DIR), { recursive: true });
|
|
86626
86673
|
await writeFile4(path, `${JSON.stringify(state, null, 2)}
|
|
86627
86674
|
`, "utf8");
|
|
86628
86675
|
}
|
|
@@ -86630,7 +86677,7 @@ async function writeDriverState(stackName, state) {
|
|
|
86630
86677
|
// src/drivers/hetzner/driver.ts
|
|
86631
86678
|
var SSH_MAX_BUFFER = 1024 * 1024 * 256;
|
|
86632
86679
|
function expandHome(path) {
|
|
86633
|
-
return path.startsWith("~/") ?
|
|
86680
|
+
return path.startsWith("~/") ? join20(homedir8(), path.slice(2)) : path;
|
|
86634
86681
|
}
|
|
86635
86682
|
|
|
86636
86683
|
class HetznerDriver {
|
|
@@ -87195,7 +87242,8 @@ class HetznerDriver {
|
|
|
87195
87242
|
const targets = await this.findComputeTargets({
|
|
87196
87243
|
slug: options.config.project.slug,
|
|
87197
87244
|
environment: options.environment,
|
|
87198
|
-
role: "app"
|
|
87245
|
+
role: "app",
|
|
87246
|
+
stackName: resolveProjectStackName(options.config, options.environment)
|
|
87199
87247
|
});
|
|
87200
87248
|
const first = targets[0];
|
|
87201
87249
|
return {
|
|
@@ -87209,7 +87257,8 @@ class HetznerDriver {
|
|
|
87209
87257
|
const targets = options.targets?.length ? options.targets : await this.findComputeTargets({
|
|
87210
87258
|
slug: options.config.project.slug,
|
|
87211
87259
|
environment: options.environment,
|
|
87212
|
-
role: "app"
|
|
87260
|
+
role: "app",
|
|
87261
|
+
stackName: resolveProjectStackName(options.config, options.environment)
|
|
87213
87262
|
});
|
|
87214
87263
|
if (targets.length === 0) {
|
|
87215
87264
|
throw new Error("No Hetzner compute targets found for release upload");
|
|
@@ -87237,6 +87286,20 @@ class HetznerDriver {
|
|
|
87237
87286
|
const exact = servers.filter((server) => matchesTsCloudLabels(server.labels, options.slug, options.environment, role));
|
|
87238
87287
|
if (exact.length > 0)
|
|
87239
87288
|
return exact.map(toTarget);
|
|
87289
|
+
if (role === "app") {
|
|
87290
|
+
const state = await readDriverState(options.stackName ?? `${options.slug}-${options.environment}`);
|
|
87291
|
+
const pinnedIds = [state?.serverId, ...state?.appServerIds ?? []].filter((id) => typeof id === "number");
|
|
87292
|
+
if (pinnedIds.length > 0) {
|
|
87293
|
+
const pinned = [];
|
|
87294
|
+
for (const id of pinnedIds) {
|
|
87295
|
+
const server = servers.find((candidate) => candidate.id === id) ?? await this.tryGetServer(id);
|
|
87296
|
+
if (server && server.status !== "off")
|
|
87297
|
+
pinned.push(server);
|
|
87298
|
+
}
|
|
87299
|
+
if (pinned.length > 0)
|
|
87300
|
+
return pinned.map(toTarget);
|
|
87301
|
+
}
|
|
87302
|
+
}
|
|
87240
87303
|
if (role === "app") {
|
|
87241
87304
|
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");
|
|
87242
87305
|
if (candidates.length === 1)
|
|
@@ -87719,10 +87782,10 @@ import { execSync as execSync5 } from "node:child_process";
|
|
|
87719
87782
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
87720
87783
|
import { chmodSync, existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "node:fs";
|
|
87721
87784
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
87722
|
-
import { dirname as dirname8, isAbsolute as isAbsolute5, join as
|
|
87785
|
+
import { dirname as dirname8, isAbsolute as isAbsolute5, join as join21 } from "node:path";
|
|
87723
87786
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
87724
87787
|
var MANAGEMENT_DASHBOARD_SITE = "dashboard";
|
|
87725
|
-
var DASHBOARD_CREDENTIALS_FILE =
|
|
87788
|
+
var DASHBOARD_CREDENTIALS_FILE = join21(".ts-cloud", "dashboard-credentials.json");
|
|
87726
87789
|
function generatePassword() {
|
|
87727
87790
|
return randomBytes6(24).toString("base64url");
|
|
87728
87791
|
}
|
|
@@ -87732,7 +87795,7 @@ function resolveDashboardAuth(cwd, username, logger4) {
|
|
|
87732
87795
|
return { password: explicit, source: "env" };
|
|
87733
87796
|
if (truthy(process.env.TS_CLOUD_UI_PUBLIC))
|
|
87734
87797
|
return { password: undefined, source: "public" };
|
|
87735
|
-
const file =
|
|
87798
|
+
const file = join21(cwd, DASHBOARD_CREDENTIALS_FILE);
|
|
87736
87799
|
try {
|
|
87737
87800
|
if (existsSync21(file)) {
|
|
87738
87801
|
const saved = JSON.parse(readFileSync17(file, "utf8"));
|
|
@@ -87757,18 +87820,18 @@ function truthy(v) {
|
|
|
87757
87820
|
return v != null && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
87758
87821
|
}
|
|
87759
87822
|
function resolveUiSource(cwd) {
|
|
87760
|
-
if (existsSync21(
|
|
87823
|
+
if (existsSync21(join21(cwd, "packages", "ui", "pages")) || existsSync21(join21(cwd, "packages", "ui", "package.json"))) {
|
|
87761
87824
|
return { uiRoot: "packages/ui/dist", build: "cd packages/ui && bun install && bun run build" };
|
|
87762
87825
|
}
|
|
87763
87826
|
const here = dirname8(fileURLToPath4(import.meta.url));
|
|
87764
87827
|
const candidates = [
|
|
87765
|
-
|
|
87766
|
-
|
|
87767
|
-
|
|
87768
|
-
|
|
87828
|
+
join21(here, "ui"),
|
|
87829
|
+
join21(here, "..", "ui"),
|
|
87830
|
+
join21(here, "..", "..", "ui"),
|
|
87831
|
+
join21(here, "..", "dist", "ui")
|
|
87769
87832
|
];
|
|
87770
87833
|
for (const dir of candidates) {
|
|
87771
|
-
if (existsSync21(
|
|
87834
|
+
if (existsSync21(join21(dir, "index.html")) || existsSync21(join21(dir, "serverless.html")))
|
|
87772
87835
|
return { uiRoot: dir, build: false };
|
|
87773
87836
|
}
|
|
87774
87837
|
return null;
|
|
@@ -87821,12 +87884,12 @@ function buildManagementDashboardArtifact(site, options) {
|
|
|
87821
87884
|
logger4.info(`Management dashboard: building UI (${site.build})`);
|
|
87822
87885
|
execSync5(site.build, { cwd, stdio: "inherit" });
|
|
87823
87886
|
}
|
|
87824
|
-
const root = isAbsolute5(site.root) ? site.root :
|
|
87887
|
+
const root = isAbsolute5(site.root) ? site.root : join21(cwd, site.root);
|
|
87825
87888
|
if (!existsSync21(root)) {
|
|
87826
87889
|
logger4.warn(`Management dashboard: build output not found at ${root} — skipping dashboard artifact.`);
|
|
87827
87890
|
return null;
|
|
87828
87891
|
}
|
|
87829
|
-
const tarball =
|
|
87892
|
+
const tarball = join21(tmpdir6(), `${options.slug}-${MANAGEMENT_DASHBOARD_SITE}-${options.sha}.tar.gz`);
|
|
87830
87893
|
execSync5(`tar czf "${tarball}" -C "${root}" .`, { stdio: "inherit" });
|
|
87831
87894
|
return tarball;
|
|
87832
87895
|
} catch (error2) {
|
|
@@ -88355,10 +88418,11 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger2) {
|
|
|
88355
88418
|
const targets = await driver.findComputeTargets({
|
|
88356
88419
|
slug,
|
|
88357
88420
|
environment,
|
|
88358
|
-
role: "app"
|
|
88421
|
+
role: "app",
|
|
88422
|
+
stackName
|
|
88359
88423
|
});
|
|
88360
88424
|
if (targets.length === 0) {
|
|
88361
|
-
const hint = driver.name === "aws" ? `Stack '${stackName}' has no EC2 instances tagged Project=${slug} Environment=${environment} Role=app.` : `No Hetzner servers labeled ts-cloud/project=${slug} ts-cloud/environment=${environment} ts-cloud/role=app.`;
|
|
88425
|
+
const hint = driver.name === "aws" ? `Stack '${stackName}' has no EC2 instances tagged Project=${slug} Environment=${environment} Role=app, and .ts-cloud/state/${stackName}.json pins no live instance. For a shared box, record its instanceId there.` : `No Hetzner servers labeled ts-cloud/project=${slug} ts-cloud/environment=${environment} ts-cloud/role=app, and .ts-cloud/state/${stackName}.json pins no live server. For a shared box, record its serverId there.`;
|
|
88362
88426
|
return { success: false, error: hint };
|
|
88363
88427
|
}
|
|
88364
88428
|
if (isPhpSite(site)) {
|
|
@@ -88588,7 +88652,8 @@ async function reloadRpxGateway(options) {
|
|
|
88588
88652
|
const targets = await driver.findComputeTargets({
|
|
88589
88653
|
slug: config6.project.slug,
|
|
88590
88654
|
environment,
|
|
88591
|
-
role: "app"
|
|
88655
|
+
role: "app",
|
|
88656
|
+
stackName: resolveProjectStackName(config6, environment)
|
|
88592
88657
|
});
|
|
88593
88658
|
if (targets.length === 0) {
|
|
88594
88659
|
logger4.warn("rpx gateway: no compute targets found — skipping gateway reload.");
|
|
@@ -88877,7 +88942,7 @@ function configuredRegion(config6) {
|
|
|
88877
88942
|
return config6.project.region ?? "us-east-1";
|
|
88878
88943
|
}
|
|
88879
88944
|
function loadLocalState(config6, environment) {
|
|
88880
|
-
const statePath =
|
|
88945
|
+
const statePath = join23(process.cwd(), ".ts-cloud", "state", `${config6.project.slug}-${environment}.json`);
|
|
88881
88946
|
if (!existsSync24(statePath))
|
|
88882
88947
|
return null;
|
|
88883
88948
|
try {
|
|
@@ -90838,10 +90903,10 @@ function selectedEnvironment(config6, requested) {
|
|
|
90838
90903
|
}
|
|
90839
90904
|
async function loadLocalEnv(cwd) {
|
|
90840
90905
|
const candidates = [
|
|
90841
|
-
|
|
90842
|
-
|
|
90843
|
-
|
|
90844
|
-
|
|
90906
|
+
join25(here, "..", "..", "..", "..", ".env"),
|
|
90907
|
+
join25(cwd, ".env"),
|
|
90908
|
+
join25(cwd, ".env.local"),
|
|
90909
|
+
join25(cwd, ".env.production")
|
|
90845
90910
|
];
|
|
90846
90911
|
for (const file of candidates) {
|
|
90847
90912
|
if (!existsSync26(file))
|
|
@@ -90865,10 +90930,10 @@ async function loadLocalEnv(cwd) {
|
|
|
90865
90930
|
}
|
|
90866
90931
|
function resolveCloudConfigPath(cwd) {
|
|
90867
90932
|
const candidates = [
|
|
90868
|
-
|
|
90869
|
-
|
|
90870
|
-
|
|
90871
|
-
|
|
90933
|
+
join25(cwd, "config", "cloud.ts"),
|
|
90934
|
+
join25(cwd, "config", "cloud.js"),
|
|
90935
|
+
join25(cwd, "cloud.config.ts"),
|
|
90936
|
+
join25(cwd, "cloud.config.js")
|
|
90872
90937
|
];
|
|
90873
90938
|
return candidates.find((file) => existsSync26(file)) ?? null;
|
|
90874
90939
|
}
|
|
@@ -90908,15 +90973,15 @@ async function resolveLiveDashboardData(config6, environment) {
|
|
|
90908
90973
|
}
|
|
90909
90974
|
function resolveUiSourceDir(cwd) {
|
|
90910
90975
|
const candidates = [
|
|
90911
|
-
|
|
90912
|
-
|
|
90913
|
-
|
|
90914
|
-
|
|
90915
|
-
|
|
90916
|
-
|
|
90976
|
+
join25(cwd, "packages", "ui"),
|
|
90977
|
+
join25(here, "..", "..", "..", "ui"),
|
|
90978
|
+
join25(here, "..", "..", "ui"),
|
|
90979
|
+
join25(here, "..", "ui-src"),
|
|
90980
|
+
join25(here, "..", "..", "ui-src"),
|
|
90981
|
+
join25(here, "..", "..", "dist", "ui-src")
|
|
90917
90982
|
];
|
|
90918
90983
|
for (const dir of candidates) {
|
|
90919
|
-
if (existsSync26(
|
|
90984
|
+
if (existsSync26(join25(dir, "pages")) && existsSync26(join25(dir, "package.json")))
|
|
90920
90985
|
return dir;
|
|
90921
90986
|
}
|
|
90922
90987
|
return null;
|
|
@@ -90926,8 +90991,8 @@ async function buildLiveUi(cwd, data) {
|
|
|
90926
90991
|
if (!uiDir)
|
|
90927
90992
|
return null;
|
|
90928
90993
|
try {
|
|
90929
|
-
const outDir = mkdtempSync6(
|
|
90930
|
-
const localStx =
|
|
90994
|
+
const outDir = mkdtempSync6(join25(tmpdir7(), "ts-cloud-dashboard-"));
|
|
90995
|
+
const localStx = join25(uiDir, "node_modules", ".bin", "stx");
|
|
90931
90996
|
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"];
|
|
90932
90997
|
const proc = Bun.spawn(cmd, {
|
|
90933
90998
|
cwd: uiDir,
|
|
@@ -91067,13 +91132,13 @@ function staticPath(uiRoot, pathname) {
|
|
|
91067
91132
|
const normalized = normalize(wanted);
|
|
91068
91133
|
if (normalized.startsWith("..") || normalized.includes("/../"))
|
|
91069
91134
|
return null;
|
|
91070
|
-
const base =
|
|
91135
|
+
const base = join25(uiRoot, normalized);
|
|
91071
91136
|
if (existsSync26(base) && !statSync6(base).isDirectory())
|
|
91072
91137
|
return base;
|
|
91073
91138
|
if (!extname3(base) && existsSync26(`${base}.html`))
|
|
91074
91139
|
return `${base}.html`;
|
|
91075
|
-
if (!extname3(base) && existsSync26(
|
|
91076
|
-
return
|
|
91140
|
+
if (!extname3(base) && existsSync26(join25(base, "index.html")))
|
|
91141
|
+
return join25(base, "index.html");
|
|
91077
91142
|
return null;
|
|
91078
91143
|
}
|
|
91079
91144
|
async function serveStatic(uiRoot, pathname) {
|