@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.
Files changed (58) hide show
  1. package/dist/bin/cli.js +388 -377
  2. package/dist/bin/dashboard-server.js +254 -243
  3. package/dist/{chunk-hcdewpt6.js → chunk-29fwqkb7.js} +1 -1
  4. package/dist/{chunk-gcehnzty.js → chunk-fbwcv2vf.js} +90 -17
  5. package/dist/deploy/index.js +2 -2
  6. package/dist/drivers/hetzner/driver.d.ts +7 -0
  7. package/dist/drivers/index.js +1 -1
  8. package/dist/drivers/shared/rpx-gateway.d.ts +4 -0
  9. package/dist/index.js +2 -2
  10. package/dist/ui/access-denied.html +2 -2
  11. package/dist/ui/account/automation.html +4 -4
  12. package/dist/ui/account/security.html +2 -2
  13. package/dist/ui/applications/compose.html +3 -3
  14. package/dist/ui/applications/new.html +2 -2
  15. package/dist/ui/data/backups.html +4 -4
  16. package/dist/ui/data/services.html +4 -4
  17. package/dist/ui/data/volumes.html +4 -4
  18. package/dist/ui/index.html +4 -4
  19. package/dist/ui/integrations.html +2 -2
  20. package/dist/ui/operations/alerts.html +4 -4
  21. package/dist/ui/operations/configuration.html +4 -4
  22. package/dist/ui/operations/jobs.html +4 -4
  23. package/dist/ui/operations/maintenance.html +3 -3
  24. package/dist/ui/operations/observability.html +4 -4
  25. package/dist/ui/operations/previews.html +4 -4
  26. package/dist/ui/operations/queue.html +4 -4
  27. package/dist/ui/operations/regions.html +4 -4
  28. package/dist/ui/operations/releases.html +3 -3
  29. package/dist/ui/operations/workloads.html +3 -3
  30. package/dist/ui/security.html +2 -2
  31. package/dist/ui/server/actions.html +3 -3
  32. package/dist/ui/server/activity.html +2 -2
  33. package/dist/ui/server/capacity.html +4 -4
  34. package/dist/ui/server/database.html +4 -4
  35. package/dist/ui/server/deployments.html +4 -4
  36. package/dist/ui/server/diagnostics.html +2 -2
  37. package/dist/ui/server/firewall.html +4 -4
  38. package/dist/ui/server/fleet.html +4 -4
  39. package/dist/ui/server/logs.html +4 -4
  40. package/dist/ui/server/metrics.html +4 -4
  41. package/dist/ui/server/services.html +2 -2
  42. package/dist/ui/server/sites.html +4 -4
  43. package/dist/ui/server/ssh-keys.html +4 -4
  44. package/dist/ui/server/team.html +4 -4
  45. package/dist/ui/server/terminal.html +2 -2
  46. package/dist/ui/serverless/alarms.html +4 -4
  47. package/dist/ui/serverless/assets.html +2 -2
  48. package/dist/ui/serverless/cost.html +2 -2
  49. package/dist/ui/serverless/data.html +4 -4
  50. package/dist/ui/serverless/firewall.html +2 -2
  51. package/dist/ui/serverless/functions.html +4 -4
  52. package/dist/ui/serverless/logs.html +4 -4
  53. package/dist/ui/serverless/metrics.html +2 -2
  54. package/dist/ui/serverless/queues.html +4 -4
  55. package/dist/ui/serverless/secrets.html +4 -4
  56. package/dist/ui/serverless/traces.html +3 -3
  57. package/dist/ui/serverless.html +4 -4
  58. package/package.json +3 -3
@@ -45,7 +45,7 @@ import {
45
45
  resolveSiteKind,
46
46
  resolveUiSource,
47
47
  siteInstallBase
48
- } from "./chunk-gcehnzty.js";
48
+ } from "./chunk-fbwcv2vf.js";
49
49
  import {
50
50
  artifactKey,
51
51
  buildCloudFormationTemplate,
@@ -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
- ...writeFileHeredoc(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF", "0600"),
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
- ...writeFileHeredoc(`${RPX_SITES_DIR}/${slug}.json`, fragment, "TS_CLOUD_RPX_FRAGMENT_EOF", "0600"),
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: resolveHetznerApiToken2(options.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 = servers.find((candidate) => candidate.id === id) ?? await this.tryGetServer(id);
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 shellSingleQuote(value) {
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=${shellSingleQuote(JSON.stringify(desiredDomains))}`,
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 ${shellSingleQuote(`
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=${shellSingleQuote(prefix)}`,
5992
+ `TS_CLOUD_DASHBOARD_PREFIX=${shellSingleQuote2(prefix)}`,
5929
5993
  `TS_CLOUD_DASHBOARD_SERVER_OWNER=${serverOwner ? "1" : "0"}`,
5930
- `TS_CLOUD_DASHBOARD_DESIRED=${shellSingleQuote(` ${desiredUnits.join(" ")} `)}`,
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({ config: lbConfig, slug }),
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({ proxy, config: rpxConfig, slug });
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,
@@ -30,7 +30,7 @@ import {
30
30
  synchronizeDashboardUsers,
31
31
  trackDashboardOperation,
32
32
  verifyStaticApiOrigin
33
- } from "../chunk-hcdewpt6.js";
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-gcehnzty.js";
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);
@@ -68,7 +68,7 @@ import {
68
68
  waitForSsh,
69
69
  wrapCloudInitUserData,
70
70
  writeResizeCheckpoint
71
- } from "../chunk-gcehnzty.js";
71
+ } from "../chunk-fbwcv2vf.js";
72
72
  import"../chunk-hmehkeqx.js";
73
73
  import"../chunk-7m60qnc8.js";
74
74
  import"../chunk-4cjrg98a.js";
@@ -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-hcdewpt6.js";
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-gcehnzty.js";
406
+ } from "./chunk-fbwcv2vf.js";
407
407
  import {
408
408
  ABTestManager,
409
409
  AI,