@stacksjs/ts-cloud 0.7.3 → 0.7.4

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.
@@ -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
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 join23, normalize } from "node:path";
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 join21 } from "node:path";
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 result.Reservations || []) {
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 join19 } from "node:path";
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 join18 } from "node:path";
86657
+ import { join as join19 } from "node:path";
86611
86658
  var STATE_DIR = ".ts-cloud/state";
86612
86659
  function driverStatePath(stackName) {
86613
- return join18(process.cwd(), STATE_DIR, `${stackName}.json`);
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(join18(process.cwd(), STATE_DIR), { recursive: true });
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("~/") ? join19(homedir8(), path.slice(2)) : path;
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 join20 } from "node:path";
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 = join20(".ts-cloud", "dashboard-credentials.json");
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 = join20(cwd, DASHBOARD_CREDENTIALS_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(join20(cwd, "packages", "ui", "pages")) || existsSync21(join20(cwd, "packages", "ui", "package.json"))) {
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
- join20(here, "ui"),
87766
- join20(here, "..", "ui"),
87767
- join20(here, "..", "..", "ui"),
87768
- join20(here, "..", "dist", "ui")
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(join20(dir, "index.html")) || existsSync21(join20(dir, "serverless.html")))
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 : join20(cwd, 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 = join20(tmpdir6(), `${options.slug}-${MANAGEMENT_DASHBOARD_SITE}-${options.sha}.tar.gz`);
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 = join21(process.cwd(), ".ts-cloud", "state", `${config6.project.slug}-${environment}.json`);
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
- join23(here, "..", "..", "..", "..", ".env"),
90842
- join23(cwd, ".env"),
90843
- join23(cwd, ".env.local"),
90844
- join23(cwd, ".env.production")
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
- join23(cwd, "config", "cloud.ts"),
90869
- join23(cwd, "config", "cloud.js"),
90870
- join23(cwd, "cloud.config.ts"),
90871
- join23(cwd, "cloud.config.js")
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
- join23(cwd, "packages", "ui"),
90912
- join23(here, "..", "..", "..", "ui"),
90913
- join23(here, "..", "..", "ui"),
90914
- join23(here, "..", "ui-src"),
90915
- join23(here, "..", "..", "ui-src"),
90916
- join23(here, "..", "..", "dist", "ui-src")
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(join23(dir, "pages")) && existsSync26(join23(dir, "package.json")))
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(join23(tmpdir7(), "ts-cloud-dashboard-"));
90930
- const localStx = join23(uiDir, "node_modules", ".bin", "stx");
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 = join23(uiRoot, normalized);
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(join23(base, "index.html")))
91076
- return join23(base, "index.html");
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) {