@stacksjs/ts-cloud 0.7.92 → 0.7.94

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 (69) hide show
  1. package/dist/bin/cli.js +1235 -1183
  2. package/dist/bin/dashboard-server.js +451 -399
  3. package/dist/{chunk-29fwqkb7.js → chunk-0htae1c2.js} +456 -21
  4. package/dist/{chunk-fbwcv2vf.js → chunk-bs3jfnzp.js} +451 -41
  5. package/dist/control-plane/store.d.ts +1 -0
  6. package/dist/deploy/dashboard-database.d.ts +91 -3
  7. package/dist/deploy/dashboard-vitess.d.ts +154 -0
  8. package/dist/deploy/dashboard-vitess.test.d.ts +1 -0
  9. package/dist/deploy/index.js +2 -2
  10. package/dist/drivers/hetzner/client.d.ts +96 -0
  11. package/dist/drivers/hetzner/migrate-server.d.ts +124 -0
  12. package/dist/drivers/hetzner/migrate-server.test.d.ts +1 -0
  13. package/dist/drivers/hetzner/role-swap.d.ts +139 -0
  14. package/dist/drivers/hetzner/role-swap.test.d.ts +1 -0
  15. package/dist/drivers/index.js +1 -1
  16. package/dist/drivers/shared/db-provision.d.ts +4 -0
  17. package/dist/drivers/shared/deploy-script.d.ts +17 -1
  18. package/dist/drivers/shared/package-manager.d.ts +2 -0
  19. package/dist/drivers/shared/releases.d.ts +41 -2
  20. package/dist/drivers/shared/vitess-provision.d.ts +133 -0
  21. package/dist/drivers/shared/vitess-provision.test.d.ts +1 -0
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +6 -2
  24. package/dist/telemetry/store.d.ts +1 -0
  25. package/dist/ui/account/automation.html +4 -4
  26. package/dist/ui/account/security.html +2 -2
  27. package/dist/ui/applications/compose.html +4 -4
  28. package/dist/ui/data/backups.html +4 -4
  29. package/dist/ui/data/services.html +4 -4
  30. package/dist/ui/data/volumes.html +3 -3
  31. package/dist/ui/index.html +4 -4
  32. package/dist/ui/integrations.html +2 -2
  33. package/dist/ui/operations/alerts.html +4 -4
  34. package/dist/ui/operations/configuration.html +4 -4
  35. package/dist/ui/operations/jobs.html +4 -4
  36. package/dist/ui/operations/maintenance.html +3 -3
  37. package/dist/ui/operations/observability.html +4 -4
  38. package/dist/ui/operations/previews.html +4 -4
  39. package/dist/ui/operations/queue.html +4 -4
  40. package/dist/ui/operations/regions.html +4 -4
  41. package/dist/ui/operations/releases.html +4 -4
  42. package/dist/ui/operations/workloads.html +4 -4
  43. package/dist/ui/security.html +2 -2
  44. package/dist/ui/server/actions.html +4 -4
  45. package/dist/ui/server/activity.html +2 -2
  46. package/dist/ui/server/capacity.html +4 -4
  47. package/dist/ui/server/database.html +124 -8
  48. package/dist/ui/server/deployments.html +4 -4
  49. package/dist/ui/server/diagnostics.html +2 -2
  50. package/dist/ui/server/firewall.html +4 -4
  51. package/dist/ui/server/fleet.html +4 -4
  52. package/dist/ui/server/logs.html +4 -4
  53. package/dist/ui/server/metrics.html +4 -4
  54. package/dist/ui/server/services.html +2 -2
  55. package/dist/ui/server/sites.html +4 -4
  56. package/dist/ui/server/ssh-keys.html +4 -4
  57. package/dist/ui/server/team.html +4 -4
  58. package/dist/ui/server/terminal.html +2 -2
  59. package/dist/ui/serverless/alarms.html +4 -4
  60. package/dist/ui/serverless/data.html +3 -3
  61. package/dist/ui/serverless/firewall.html +2 -2
  62. package/dist/ui/serverless/functions.html +4 -4
  63. package/dist/ui/serverless/logs.html +4 -4
  64. package/dist/ui/serverless/queues.html +4 -4
  65. package/dist/ui/serverless/secrets.html +4 -4
  66. package/dist/ui/serverless/traces.html +4 -4
  67. package/dist/ui/serverless.html +3 -3
  68. package/dist/ui-src/pages/server/database.stx +257 -9
  69. package/package.json +3 -3
@@ -127,6 +127,8 @@ var PANTRY_PACKAGES = {
127
127
  mysql: "mysql.com",
128
128
  mariadb: "mariadb.com/server",
129
129
  postgres: "postgresql.org",
130
+ vitess: "vitess.io",
131
+ etcd: "etcd.io",
130
132
  redis: "redis.io",
131
133
  memcached: "memcached.org",
132
134
  meilisearch: "meilisearch.com",
@@ -171,8 +173,284 @@ function buildPantryServiceScript(services) {
171
173
  ]);
172
174
  }
173
175
 
176
+ // src/drivers/shared/vitess-provision.ts
177
+ var PANTRY_BIN = `${PANTRY_PROJECT_DIR}/pantry/.bin`;
178
+ var VTGATE_MYSQL_PORT = 15306;
179
+ var VTGATE_GRPC_PORT = 15991;
180
+ var VTCTLD_GRPC_PORT = 15999;
181
+ var VTTABLET_GRPC_PORT = 16101;
182
+ var ETCD_CLIENT_PORT = 2379;
183
+ var VITESS_ROOT = "/var/lib/vitess";
184
+ var VITESS_TABLET_UID = 100;
185
+ function enabled(value) {
186
+ return value === true || typeof value === "object" && value !== null;
187
+ }
188
+ function settings(value) {
189
+ return typeof value === "object" && value !== null ? value : {};
190
+ }
191
+ function sh2(value) {
192
+ return `'${String(value).split("'").join("'\\''")}'`;
193
+ }
194
+ function vitessPackages(config) {
195
+ const version = config.version ? `vitess.io@${config.version}` : "vitess.io";
196
+ const packages = [version];
197
+ if (config.mode !== "combo" && !config.etcdEndpoint)
198
+ packages.push("etcd.io");
199
+ packages.push("mysql.com");
200
+ return packages;
201
+ }
202
+ var VITESS_USER = "vitess";
203
+ var UNIT_PATH = `${PANTRY_BIN}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`;
204
+ var VITESS_LIB = "/usr/local/lib/vitess";
205
+ var launchers = new Map;
206
+ function launcherPath(name) {
207
+ return `${VITESS_LIB}/${name}.sh`;
208
+ }
209
+ function launcherFor(name, command) {
210
+ launchers.set(name, launcherScript(command));
211
+ return `/bin/sh ${launcherPath(name)}`;
212
+ }
213
+ function buildLauncherScripts() {
214
+ const out = [`mkdir -p ${VITESS_LIB}`];
215
+ for (const [name, body] of launchers) {
216
+ out.push(`cat > ${launcherPath(name)} <<'TS_CLOUD_VITESS_LAUNCHER_EOF'`, body, "TS_CLOUD_VITESS_LAUNCHER_EOF", `chmod 0755 ${launcherPath(name)}`);
217
+ }
218
+ return out;
219
+ }
220
+ function launcherScript(command) {
221
+ return [
222
+ "#!/bin/sh",
223
+ "set -e",
224
+ `cd ${PANTRY_PROJECT_DIR}`,
225
+ 'eval "$(pantry env 2>/dev/null)" || true',
226
+ `VT_MYSQL_ROOT="$(dirname "$(dirname "$(readlink -f ${PANTRY_BIN}/mysqld 2>/dev/null)")")"`,
227
+ `VTROOT="$(dirname "$(dirname "$(readlink -f ${PANTRY_BIN}/mysqlctld 2>/dev/null)")")"`,
228
+ "export VT_MYSQL_ROOT VTROOT",
229
+ `exec ${command.join(" ")}`
230
+ ].join(`
231
+ `);
232
+ }
233
+ function systemdUnit(opts) {
234
+ const lines = [
235
+ "[Unit]",
236
+ `Description=${opts.description}`,
237
+ `After=network.target${opts.after?.length ? ` ${opts.after.join(" ")}` : ""}`
238
+ ];
239
+ if (opts.requires?.length)
240
+ lines.push(`Requires=${opts.requires.join(" ")}`);
241
+ lines.push("", "[Service]", "Type=simple", `User=${VITESS_USER}`, `Group=${VITESS_USER}`, `Environment="PATH=${UNIT_PATH}"`, `Environment="VTDATAROOT=${VITESS_ROOT}"`, ...Object.entries(opts.environment ?? {}).map(([k, v]) => `Environment="${k}=${v}"`), ...(opts.execStartPre ?? []).map((c) => `ExecStartPre=${c}`), `ExecStart=${opts.execStart}`, "Restart=always", "RestartSec=5", "LimitNOFILE=65535", "", "[Install]", "WantedBy=multi-user.target");
242
+ return lines.join(`
243
+ `);
244
+ }
245
+ function writeUnit(name, body) {
246
+ return [
247
+ `cat > /etc/systemd/system/${name}.service <<'TS_CLOUD_VITESS_UNIT_EOF'`,
248
+ body,
249
+ "TS_CLOUD_VITESS_UNIT_EOF"
250
+ ];
251
+ }
252
+ function buildVtcomboUnit(config) {
253
+ const cell = config.cell ?? "zone1";
254
+ const port = config.vtgatePort ?? VTGATE_MYSQL_PORT;
255
+ const keyspaces = ((config.keyspaces?.length) ? config.keyspaces : [{ name: "app" }]).map((k) => {
256
+ const shards = (k.sharded ? ["-80", "80-"] : ["0"]).map((s) => `shards:{name:"${s}"}`).join(" ");
257
+ return `keyspaces:{name:"${k.name}" ${shards}}`;
258
+ }).join(" ");
259
+ const topology = `${keyspaces} cells:"${cell}"`;
260
+ return systemdUnit({
261
+ description: "Vitess (vtcombo, single-process development stack)",
262
+ execStart: launcherFor("vtcombo", [
263
+ `${PANTRY_BIN}/vtcombo`,
264
+ `--cell ${cell}`,
265
+ `--proto-topo ${sh2(topology)}`,
266
+ `--mysql-server-port ${port}`,
267
+ "--mysql-server-bind-address 127.0.0.1",
268
+ "--mysql-auth-server-impl none",
269
+ "--start-mysql",
270
+ `--port ${VTGATE_GRPC_PORT}`,
271
+ `--grpc-port ${VTCTLD_GRPC_PORT}`
272
+ ])
273
+ });
274
+ }
275
+ function buildEtcdUnit() {
276
+ return systemdUnit({
277
+ description: "etcd (Vitess topology store)",
278
+ execStart: launcherFor("etcd", [
279
+ `${PANTRY_BIN}/etcd`,
280
+ `--data-dir ${VITESS_ROOT}/etcd`,
281
+ `--listen-client-urls http://127.0.0.1:${ETCD_CLIENT_PORT}`,
282
+ `--advertise-client-urls http://127.0.0.1:${ETCD_CLIENT_PORT}`
283
+ ]),
284
+ execStartPre: [`+/bin/mkdir -p ${VITESS_ROOT}/etcd`, `+/bin/chown ${VITESS_USER}:${VITESS_USER} ${VITESS_ROOT}/etcd`]
285
+ });
286
+ }
287
+ function topoFlags(config) {
288
+ const endpoint = config.etcdEndpoint ?? `http://127.0.0.1:${ETCD_CLIENT_PORT}`;
289
+ return [
290
+ "--topo-implementation etcd2",
291
+ `--topo-global-server-address ${endpoint}`,
292
+ "--topo-global-root /vitess/global"
293
+ ];
294
+ }
295
+ function buildVtctldUnit(config) {
296
+ const requiresEtcd = !config.etcdEndpoint;
297
+ return systemdUnit({
298
+ description: "Vitess vtctld (control plane)",
299
+ after: requiresEtcd ? ["vitess-etcd.service"] : [],
300
+ requires: requiresEtcd ? ["vitess-etcd.service"] : [],
301
+ execStart: launcherFor("vtctld", [
302
+ `${PANTRY_BIN}/vtctld`,
303
+ ...topoFlags(config),
304
+ `--cell ${config.cell ?? "zone1"}`,
305
+ `--service-map grpc-vtctl,grpc-vtctld`,
306
+ `--grpc-port ${VTCTLD_GRPC_PORT}`,
307
+ `--port ${VTCTLD_GRPC_PORT + 1}`
308
+ ])
309
+ });
310
+ }
311
+ function buildVttabletUnit(config, keyspace, shard) {
312
+ const cell = config.cell ?? "zone1";
313
+ return systemdUnit({
314
+ description: `Vitess vttablet (${keyspace}/${shard})`,
315
+ after: ["vitess-vtctld.service", "vitess-mysqlctld.service"],
316
+ requires: ["vitess-mysqlctld.service"],
317
+ execStart: launcherFor("vttablet", [
318
+ `${PANTRY_BIN}/vttablet`,
319
+ ...topoFlags(config),
320
+ `--tablet-path ${cell}-${String(VITESS_TABLET_UID).padStart(10, "0")}`,
321
+ `--init-keyspace ${keyspace}`,
322
+ `--init-shard ${shard}`,
323
+ "--init-tablet-type replica",
324
+ `--service-map ${sh2("grpc-queryservice,grpc-tabletmanager,grpc-updatestream")}`,
325
+ `--port ${VTTABLET_GRPC_PORT}`,
326
+ `--grpc-port ${VTTABLET_GRPC_PORT + 1}`,
327
+ `--db-port ${config.mysqlPort ?? 3306}`,
328
+ `--mycnf-mysql-port ${config.mysqlPort ?? 3306}`
329
+ ])
330
+ });
331
+ }
332
+ function buildMysqlctldUnit(config) {
333
+ return systemdUnit({
334
+ description: "Vitess mysqlctld (managed mysqld)",
335
+ execStart: launcherFor("mysqlctld", [
336
+ `${PANTRY_BIN}/mysqlctld`,
337
+ `--tablet-uid ${VITESS_TABLET_UID}`,
338
+ `--mysql-port ${config.mysqlPort ?? 3306}`,
339
+ '--init-db-sql-file "$VTROOT/config/init_db.sql"',
340
+ "--wait-time 2m"
341
+ ]),
342
+ execStartPre: [`+/bin/mkdir -p ${VITESS_ROOT}`, `+/bin/chown ${VITESS_USER}:${VITESS_USER} ${VITESS_ROOT}`]
343
+ });
344
+ }
345
+ var VITESS_AUTH_FILE = "/etc/vitess/auth.json";
346
+ function buildVitessAuthFileScript(config) {
347
+ const user = config.username ?? "vitess";
348
+ const password = config.password ?? "";
349
+ const auth = JSON.stringify({ [user]: [{ MysqlNativePassword: "", Password: password, UserData: user }] });
350
+ return [
351
+ "mkdir -p /etc/vitess",
352
+ `cat > ${VITESS_AUTH_FILE} <<'TS_CLOUD_VITESS_AUTH_EOF'`,
353
+ auth,
354
+ "TS_CLOUD_VITESS_AUTH_EOF",
355
+ `chown ${VITESS_USER}:${VITESS_USER} ${VITESS_AUTH_FILE}`,
356
+ `chmod 0600 ${VITESS_AUTH_FILE}`
357
+ ];
358
+ }
359
+ function buildVtgateUnit(config) {
360
+ const cell = config.cell ?? "zone1";
361
+ return systemdUnit({
362
+ description: "Vitess vtgate (query router)",
363
+ after: ["vitess-vtctld.service"],
364
+ execStart: launcherFor("vtgate", [
365
+ `${PANTRY_BIN}/vtgate`,
366
+ ...topoFlags(config),
367
+ `--cell ${cell}`,
368
+ `--cells-to-watch ${cell}`,
369
+ "--tablet-types-to-wait PRIMARY,REPLICA",
370
+ `--service-map ${sh2("grpc-vtgateservice")}`,
371
+ `--mysql-server-port ${config.vtgatePort ?? VTGATE_MYSQL_PORT}`,
372
+ `--mysql-server-bind-address ${config.bindAddress ?? "127.0.0.1"}`,
373
+ "--mysql-auth-server-impl static",
374
+ `--mysql-auth-server-static-file ${VITESS_AUTH_FILE}`,
375
+ `--port ${VTGATE_GRPC_PORT}`,
376
+ `--grpc-port ${VTGATE_GRPC_PORT + 1}`
377
+ ])
378
+ });
379
+ }
380
+ function buildVitessBootstrapScript(config) {
381
+ const cell = config.cell ?? "zone1";
382
+ const endpoint = config.etcdEndpoint ?? `http://127.0.0.1:${ETCD_CLIENT_PORT}`;
383
+ const client = `${PANTRY_BIN}/vtctldclient --server 127.0.0.1:${VTCTLD_GRPC_PORT}`;
384
+ const out = [
385
+ `for i in $(seq 1 30); do ${client} GetCellInfoNames >/dev/null 2>&1 && break; sleep 2; done`,
386
+ `if ! ${client} GetCellInfoNames 2>/dev/null | grep -qx ${sh2(cell)}; then`,
387
+ ` ${client} AddCellInfo --root ${sh2(`/vitess/${cell}`)} --server-address ${sh2(endpoint)} ${sh2(cell)}`,
388
+ "fi"
389
+ ];
390
+ for (const keyspace of config.keyspaces ?? []) {
391
+ const sharded = keyspace.sharded ? " --sharded" : "";
392
+ out.push(`if ! ${client} GetKeyspace ${sh2(keyspace.name)} >/dev/null 2>&1; then`, ` ${client} CreateKeyspace${sharded} ${sh2(keyspace.name)}`, "fi");
393
+ const alias = `${cell}-${String(VITESS_TABLET_UID).padStart(10, "0")}`;
394
+ for (const shard of keyspace.sharded ? ["-80", "80-"] : ["0"]) {
395
+ const target = `${keyspace.name}/${shard}`;
396
+ out.push(`for i in $(seq 1 30); do ${client} GetTablet ${sh2(alias)} >/dev/null 2>&1 && break; sleep 2; done`, `if ! ${client} GetShard ${sh2(target)} 2>/dev/null | grep -q '"primary_alias"'; then`, ` ${client} PlannedReparentShard ${sh2(target)} --new-primary ${sh2(alias)} || ${client} InitShardPrimary --force ${sh2(target)} ${sh2(alias)}`, "fi");
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+ function buildVitessHealthCheck(config) {
402
+ const port = config.vtgatePort ?? VTGATE_MYSQL_PORT;
403
+ return [
404
+ `for i in $(seq 1 60); do`,
405
+ ` if (cd ${PANTRY_PROJECT_DIR} && eval "$(pantry env 2>/dev/null)"; MYSQL_PWD=${sh2(config.password ?? "")} ${PANTRY_BIN}/mysql -h 127.0.0.1 -P ${port} -u ${sh2(config.username ?? "vitess")} --connect-timeout=2 -e 'SHOW KEYSPACES') >/dev/null 2>&1; then`,
406
+ ` echo "vtgate is serving on ${port}"; exit 0`,
407
+ " fi",
408
+ " sleep 2",
409
+ "done",
410
+ `echo "vtgate did not become ready on port ${port}" >&2`,
411
+ "exit 1"
412
+ ];
413
+ }
414
+ function buildVitessProvisionScript(value) {
415
+ if (!enabled(value))
416
+ return [];
417
+ const config = settings(value);
418
+ const combo = config.mode === "combo";
419
+ launchers.clear();
420
+ const out = [
421
+ ...buildPantryInstallScript(vitessPackages(config)),
422
+ `id -u ${VITESS_USER} >/dev/null 2>&1 || useradd --system --home-dir ${VITESS_ROOT} --shell /usr/sbin/nologin ${VITESS_USER}`,
423
+ `mkdir -p ${VITESS_ROOT}`,
424
+ `chown -R ${VITESS_USER}:${VITESS_USER} ${VITESS_ROOT}`
425
+ ];
426
+ if (combo) {
427
+ out.push(...writeUnit("vitess-vtcombo", buildVtcomboUnit(config)), ...buildLauncherScripts(), "systemctl daemon-reload", "systemctl enable --now vitess-vtcombo.service", ...buildVitessHealthCheck(config));
428
+ return out;
429
+ }
430
+ out.push(...buildVitessAuthFileScript(config));
431
+ const units = [];
432
+ if (!config.etcdEndpoint)
433
+ units.push(["vitess-etcd", buildEtcdUnit()]);
434
+ units.push(["vitess-vtctld", buildVtctldUnit(config)], ["vitess-mysqlctld", buildMysqlctldUnit(config)], ["vitess-vtgate", buildVtgateUnit(config)]);
435
+ for (const keyspace of config.keyspaces ?? []) {
436
+ for (const shard of keyspace.sharded ? ["-80", "80-"] : ["0"])
437
+ units.push([`vitess-vttablet-${keyspace.name}-${shard.replace(/-/g, "x")}`, buildVttabletUnit(config, keyspace.name, shard)]);
438
+ }
439
+ for (const [name, body] of units)
440
+ out.push(...writeUnit(name, body));
441
+ out.push(...buildLauncherScripts());
442
+ out.push("systemctl daemon-reload");
443
+ for (const [name] of units)
444
+ out.push(`systemctl enable --now ${name}.service`);
445
+ out.push(...buildVitessBootstrapScript(config), ...buildVitessHealthCheck(config));
446
+ return out;
447
+ }
448
+
174
449
  // src/drivers/shared/db-provision.ts
450
+ var ALWAYS_EXTERNAL_ENGINES = new Set(["singlestore", "vitess"]);
175
451
  function isLocalDatabase(database) {
452
+ if (database?.engine && ALWAYS_EXTERNAL_ENGINES.has(database.engine))
453
+ return false;
176
454
  return !database?.host || database.host === "127.0.0.1" || database.host === "localhost";
177
455
  }
178
456
  function pgAdminCommand(database, tool = "psql") {
@@ -183,32 +461,32 @@ function pgAdminCommand(database, tool = "psql") {
183
461
  const env = database?.password ? `PGPASSWORD='${database.password.replace(/'/g, `'\\''`)}' ` : "";
184
462
  return `${env}${tool} -h ${database.host} -p ${port} -U ${user} -w`;
185
463
  }
186
- function enabled(spec) {
464
+ function enabled2(spec) {
187
465
  return spec === true || typeof spec === "object" && spec != null;
188
466
  }
189
467
  function planServices(services) {
190
468
  const packages = [];
191
469
  const names = [];
192
- if (enabled(services.mysql)) {
470
+ if (enabled2(services.mysql)) {
193
471
  packages.push(typeof services.mysql === "object" && services.mysql.version ? `mysql.com@${services.mysql.version}` : "mysql.com@8.0.43");
194
472
  names.push("mysql");
195
- } else if (enabled(services.mariadb)) {
473
+ } else if (enabled2(services.mariadb)) {
196
474
  packages.push(PANTRY_PACKAGES.mariadb);
197
475
  names.push("mariadb");
198
476
  }
199
- if (enabled(services.postgres)) {
477
+ if (enabled2(services.postgres)) {
200
478
  packages.push("postgresql.org");
201
479
  names.push("postgres");
202
480
  }
203
- if (enabled(services.redis)) {
481
+ if (enabled2(services.redis)) {
204
482
  packages.push("redis.io");
205
483
  names.push("redis");
206
484
  }
207
- if (enabled(services.memcached)) {
485
+ if (enabled2(services.memcached)) {
208
486
  packages.push("memcached.org");
209
487
  names.push("memcached");
210
488
  }
211
- if (enabled(services.meilisearch)) {
489
+ if (enabled2(services.meilisearch)) {
212
490
  packages.push("meilisearch.com");
213
491
  names.push("meilisearch");
214
492
  }
@@ -216,9 +494,14 @@ function planServices(services) {
216
494
  }
217
495
  function buildServicesProvisionScript(services = {}, _options = {}) {
218
496
  const plan = planServices(services);
497
+ const vitess = buildVitessProvisionScript(services.vitess);
219
498
  if (plan.packages.length === 0)
220
- return [];
221
- return [...buildPantryInstallScript(plan.packages), ...buildPantryServiceScript(plan.services)];
499
+ return vitess;
500
+ return [
501
+ ...buildPantryInstallScript(plan.packages),
502
+ ...buildPantryServiceScript(plan.services),
503
+ ...vitess
504
+ ];
222
505
  }
223
506
  function buildDatabaseSetupScript(database, services = {}) {
224
507
  if (!database?.name)
@@ -228,9 +511,9 @@ function buildDatabaseSetupScript(database, services = {}) {
228
511
  const name = database.name;
229
512
  const user = database.username || name;
230
513
  const pass = database.password || "";
231
- const usePostgres = enabled(services.postgres) || database.engine === "postgres";
232
- const useMariadb = enabled(services.mariadb) || database.engine === "mariadb";
233
- const useMysql = enabled(services.mysql) || database.engine === "mysql";
514
+ const usePostgres = enabled2(services.postgres) || database.engine === "postgres";
515
+ const useMariadb = enabled2(services.mariadb) || database.engine === "mariadb";
516
+ const useMysql = enabled2(services.mysql) || database.engine === "mysql";
234
517
  if (usePostgres && !useMysql && !useMariadb) {
235
518
  const pgIdent = (v) => `"${v.replace(/"/g, '""')}"`;
236
519
  const pgLit = (v) => `'${v.replace(/'/g, "''")}'`;
@@ -300,8 +583,9 @@ function buildManagedDbEnv(database) {
300
583
  if (!database?.name)
301
584
  return {};
302
585
  const isSingleStore = database.engine === "singlestore";
303
- const connection = database.engine === "postgres" ? "pgsql" : isSingleStore ? "singlestore" : "mysql";
304
- const port = database.port ?? (database.engine === "postgres" ? 5432 : 3306);
586
+ const isVitess = database.engine === "vitess";
587
+ const connection = database.engine === "postgres" ? "pgsql" : isSingleStore ? "singlestore" : isVitess ? "vitess" : "mysql";
588
+ const port = database.port ?? (database.engine === "postgres" ? 5432 : isVitess ? 15306 : 3306);
305
589
  const env = {
306
590
  DB_CONNECTION: connection,
307
591
  DB_HOST: database.host || "127.0.0.1",
@@ -312,7 +596,7 @@ function buildManagedDbEnv(database) {
312
596
  env.DB_USERNAME = database.username;
313
597
  if (database.password)
314
598
  env.DB_PASSWORD = database.password;
315
- if (isSingleStore && database.ssl !== false)
599
+ if ((isSingleStore || isVitess) && database.ssl !== false)
316
600
  env.DB_SSL = "true";
317
601
  return env;
318
602
  }
@@ -433,8 +717,8 @@ function buildBackupRestoreScript(database, options = {}) {
433
717
  }
434
718
 
435
719
  // src/drivers/shared/maintenance.ts
436
- function buildAutoUpdatesScript(enabled2 = true) {
437
- if (!enabled2)
720
+ function buildAutoUpdatesScript(enabled3 = true) {
721
+ if (!enabled3)
438
722
  return [];
439
723
  return [
440
724
  "export DEBIAN_FRONTEND=noninteractive",
@@ -468,17 +752,17 @@ var SERVICE_PROBES = [
468
752
  ];
469
753
  function resolveMonitoring(monitoring = true) {
470
754
  const obj = typeof monitoring === "object" ? monitoring : {};
471
- const enabled2 = typeof monitoring === "boolean" ? monitoring : monitoring.enabled !== false;
755
+ const enabled3 = typeof monitoring === "boolean" ? monitoring : monitoring.enabled !== false;
472
756
  return {
473
- enabled: enabled2,
757
+ enabled: enabled3,
474
758
  cpuLoadPerCore: obj.alerts?.cpuLoadPerCore ?? DEFAULT_CPU_LOAD_PER_CORE,
475
759
  memPercent: obj.alerts?.memPercent ?? DEFAULT_MEM_PERCENT,
476
760
  diskPercent: obj.alerts?.diskPercent ?? DEFAULT_DISK_PERCENT
477
761
  };
478
762
  }
479
763
  function buildMonitoringScript(monitoring = true) {
480
- const { enabled: enabled2, cpuLoadPerCore, memPercent, diskPercent } = resolveMonitoring(monitoring);
481
- if (!enabled2)
764
+ const { enabled: enabled3, cpuLoadPerCore, memPercent, diskPercent } = resolveMonitoring(monitoring);
765
+ if (!enabled3)
482
766
  return [];
483
767
  const probeLines = SERVICE_PROBES.map(([name, port]) => `SVC_${name.toUpperCase()}=$(probe ${port})`);
484
768
  const servicesJson = SERVICE_PROBES.map(([name]) => `"${name}":"$SVC_${name.toUpperCase()}"`).join(",");
@@ -765,7 +1049,7 @@ function buildNginxVhost(options) {
765
1049
  }
766
1050
  function buildNginxVhostScript(options) {
767
1051
  const available = `/etc/nginx/sites-available/${options.siteName}`;
768
- const enabled2 = `/etc/nginx/sites-enabled/${options.siteName}`;
1052
+ const enabled3 = `/etc/nginx/sites-enabled/${options.siteName}`;
769
1053
  const vhost = buildNginxVhost(options);
770
1054
  const out = [...buildNginxServiceScript()];
771
1055
  if (options.auth) {
@@ -775,7 +1059,7 @@ function buildNginxVhostScript(options) {
775
1059
  const user = sq(options.auth.username);
776
1060
  out.push(`TS_CLOUD_HTPASS=$(openssl passwd -apr1 '${pw}')`, `printf '%s:%s\\n' '${user}' "$TS_CLOUD_HTPASS" > ${file}`, `chmod 640 ${file}`, `chown root:www-data ${file} 2>/dev/null || true`);
777
1061
  }
778
- out.push(`cat > ${available} <<'TS_CLOUD_NGINX_EOF'`, vhost.replace(/\n$/, ""), "TS_CLOUD_NGINX_EOF", `ln -sf ${available} ${enabled2}`, "rm -f /etc/nginx/sites-enabled/default", `${NGINX_WRAPPER} -t`, "systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true");
1062
+ out.push(`cat > ${available} <<'TS_CLOUD_NGINX_EOF'`, vhost.replace(/\n$/, ""), "TS_CLOUD_NGINX_EOF", `ln -sf ${available} ${enabled3}`, "rm -f /etc/nginx/sites-enabled/default", `${NGINX_WRAPPER} -t`, "systemctl reload ts-cloud-nginx 2>/dev/null || systemctl restart ts-cloud-nginx 2>/dev/null || true");
779
1063
  return out;
780
1064
  }
781
1065
  var NGINX_WRAPPER = "/usr/local/bin/ts-cloud-nginx";
@@ -2516,6 +2800,65 @@ class HetznerClient {
2516
2800
  });
2517
2801
  return data.action;
2518
2802
  }
2803
+ async powerOffServer(serverId) {
2804
+ const data = await this.request("POST", `/servers/${serverId}/actions/poweroff`, {});
2805
+ return data.action;
2806
+ }
2807
+ async stopServer(serverId, options) {
2808
+ const server = await this.getServer(serverId);
2809
+ if (server.status === "off")
2810
+ return;
2811
+ await this.shutdownServer(serverId);
2812
+ const grace = options?.gracefulWaitMs ?? 120000;
2813
+ const poll = options?.pollIntervalMs ?? 3000;
2814
+ const start = Date.now();
2815
+ while (Date.now() - start < grace) {
2816
+ if ((await this.getServer(serverId)).status === "off")
2817
+ return;
2818
+ await new Promise((resolve) => setTimeout(resolve, poll));
2819
+ }
2820
+ const action = await this.powerOffServer(serverId);
2821
+ await this.waitForAction(action.id);
2822
+ await this.waitForServerStatus(serverId, "off");
2823
+ }
2824
+ async renameServer(serverId, name) {
2825
+ const data = await this.request("PUT", `/servers/${serverId}`, { name });
2826
+ return data.server;
2827
+ }
2828
+ async listImages(options) {
2829
+ const query = options?.type ? `?type=${encodeURIComponent(options.type)}` : "";
2830
+ return this.requestAll(`/images${query}`, "images");
2831
+ }
2832
+ async getImage(id) {
2833
+ const data = await this.request("GET", `/images/${id}`);
2834
+ return data.image;
2835
+ }
2836
+ async createImage(serverId, options) {
2837
+ return this.request("POST", `/servers/${serverId}/actions/create_image`, { description: options.description, type: options.type ?? "snapshot", labels: options.labels });
2838
+ }
2839
+ async deleteImage(imageId) {
2840
+ await this.request("DELETE", `/images/${imageId}`);
2841
+ }
2842
+ async rebuildServer(serverId, image) {
2843
+ const data = await this.request("POST", `/servers/${serverId}/actions/rebuild`, {
2844
+ image
2845
+ });
2846
+ return data.action;
2847
+ }
2848
+ async listPrimaryIps() {
2849
+ return this.requestAll("/primary_ips", "primary_ips");
2850
+ }
2851
+ async unassignPrimaryIp(ipId) {
2852
+ const data = await this.request("POST", `/primary_ips/${ipId}/actions/unassign`, {});
2853
+ return data.action;
2854
+ }
2855
+ async assignPrimaryIp(ipId, serverId) {
2856
+ const data = await this.request("POST", `/primary_ips/${ipId}/actions/assign`, {
2857
+ assignee_id: serverId,
2858
+ assignee_type: "server"
2859
+ });
2860
+ return data.action;
2861
+ }
2519
2862
  async waitForAction(actionId, options) {
2520
2863
  const pollInterval = options?.pollIntervalMs ?? 2000;
2521
2864
  const maxWait = options?.maxWaitMs ?? 300000;
@@ -2696,16 +3039,16 @@ class HetznerDriver {
2696
3039
  this.client = options.client ?? new HetznerClient({
2697
3040
  apiToken: apiToken ?? ""
2698
3041
  });
2699
- const settings = resolveHetznerSettings(undefined, {
3042
+ const settings2 = resolveHetznerSettings(undefined, {
2700
3043
  sshPrivateKeyPath: options.sshPrivateKeyPath,
2701
3044
  sshPublicKeyPath: options.sshPublicKeyPath,
2702
3045
  sshUser: options.sshUser,
2703
3046
  location: options.location
2704
3047
  });
2705
- this.sshPrivateKeyPath = settings.sshPrivateKeyPath;
2706
- this.sshPublicKeyPath = settings.sshPublicKeyPath;
2707
- this.sshUser = settings.sshUser;
2708
- this.location = settings.location;
3048
+ this.sshPrivateKeyPath = settings2.sshPrivateKeyPath;
3049
+ this.sshPublicKeyPath = settings2.sshPublicKeyPath;
3050
+ this.sshUser = settings2.sshUser;
3051
+ this.location = settings2.location;
2709
3052
  this.waitForBoot = options.waitForBoot ?? true;
2710
3053
  this.bootWait = {
2711
3054
  sshIntervalMs: options.bootWait?.sshIntervalMs ?? 5000,
@@ -4690,6 +5033,9 @@ function deployHistoryPath(base) {
4690
5033
  function deployLogPath(base, releaseId) {
4691
5034
  return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
4692
5035
  }
5036
+ function sharedPathsManifestPath(base) {
5037
+ return `${deployMetaDir(base)}/shared-paths`;
5038
+ }
4693
5039
  function releasePaths(base, releaseId) {
4694
5040
  const root = base.replace(/\/+$/, "");
4695
5041
  return {
@@ -4706,17 +5052,59 @@ function isFileSharedPath(p) {
4706
5052
  return true;
4707
5053
  return /\.[a-z0-9]+$/i.test(name);
4708
5054
  }
5055
+ function sharedPathOf(entry) {
5056
+ return typeof entry === "string" ? entry : entry.path;
5057
+ }
5058
+ function dedupeSharedPaths(entries) {
5059
+ const byPath = new Map;
5060
+ for (const entry of entries)
5061
+ byPath.set(sharedPathOf(entry), entry);
5062
+ return [...byPath.values()];
5063
+ }
5064
+ function resolveSharedPath(paths, entry) {
5065
+ if (typeof entry === "string")
5066
+ return { path: entry, target: `${paths.shared}/${entry}`, seed: true };
5067
+ return {
5068
+ path: entry.path,
5069
+ target: entry.target ?? `${paths.shared}/${entry.path}`,
5070
+ seed: entry.seed !== false
5071
+ };
5072
+ }
5073
+ function buildAdoptSharedPathFn(paths) {
5074
+ return [
5075
+ "ts_cloud_adopt_shared() {",
5076
+ ' TS_CLOUD_SP="$1"',
5077
+ ' TS_CLOUD_DST="$2"',
5078
+ ` TS_CLOUD_SRC="${paths.current}/$TS_CLOUD_SP"`,
5079
+ ' if [ -e "$TS_CLOUD_DST" ]; then return 0; fi',
5080
+ ' if [ -L "$TS_CLOUD_SRC" ] || [ ! -e "$TS_CLOUD_SRC" ]; then return 0; fi',
5081
+ ' mkdir -p "$(dirname "$TS_CLOUD_DST")"',
5082
+ ' cp -a "$TS_CLOUD_SRC" "$TS_CLOUD_DST"',
5083
+ " for TS_CLOUD_SIDECAR in -wal -shm; do",
5084
+ ' if [ -f "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" ]; then cp -a "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" "$TS_CLOUD_DST$TS_CLOUD_SIDECAR"; fi',
5085
+ " done",
5086
+ ' echo "[ts-cloud] adopted $TS_CLOUD_SP from the live release into shared/ — it now survives deploys"',
5087
+ " return 0",
5088
+ "}"
5089
+ ];
5090
+ }
4709
5091
  function buildEnsureReleaseLayout(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
4710
- const lines = [`mkdir -p ${paths.releases} ${paths.shared}`];
4711
- for (const p of sharedPaths) {
5092
+ const lines = [`mkdir -p ${paths.releases} ${paths.shared}`, ...buildAdoptSharedPathFn(paths)];
5093
+ const resolved = sharedPaths.map((entry) => resolveSharedPath(paths, entry));
5094
+ for (const { path: p, target, seed } of resolved) {
5095
+ if (!seed)
5096
+ continue;
5097
+ lines.push(`ts_cloud_adopt_shared '${p}' '${target}'`);
4712
5098
  if (isFileSharedPath(p)) {
4713
- lines.push(`mkdir -p "$(dirname ${paths.shared}/${p})"`, `touch ${paths.shared}/${p}`);
5099
+ lines.push(`mkdir -p "$(dirname ${target})"`, `touch ${target}`);
4714
5100
  } else if (p === "storage") {
4715
- 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`);
5101
+ lines.push(`mkdir -p ${target}/app/public`, `mkdir -p ${target}/framework/cache/data`, `mkdir -p ${target}/framework/sessions`, `mkdir -p ${target}/framework/testing`, `mkdir -p ${target}/framework/views`, `mkdir -p ${target}/logs`);
4716
5102
  } else {
4717
- lines.push(`mkdir -p ${paths.shared}/${p}`);
5103
+ lines.push(`mkdir -p ${target}`);
4718
5104
  }
4719
5105
  }
5106
+ const manifest = sharedPathsManifestPath(paths.base);
5107
+ lines.push(`mkdir -p ${deployMetaDir(paths.base)}`, `cat > ${manifest} <<'TS_CLOUD_SHARED_PATHS_EOF'`, ...resolved.map((entry) => `${entry.path} ${entry.target}`), "TS_CLOUD_SHARED_PATHS_EOF");
4720
5108
  return lines;
4721
5109
  }
4722
5110
  function buildDeployLock(paths, waitSeconds = 900) {
@@ -4769,25 +5157,47 @@ function buildPromoteStagedRelease(paths) {
4769
5157
  }
4770
5158
  function buildLinkSharedPaths(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
4771
5159
  const lines = [];
4772
- for (const p of sharedPaths) {
4773
- const target = `${paths.shared}/${p}`;
5160
+ for (const entry of sharedPaths) {
5161
+ const { path: p, target, seed } = resolveSharedPath(paths, entry);
4774
5162
  const link = `${paths.release}/${p}`;
4775
- lines.push(`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`);
5163
+ const relink = [`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`];
5164
+ if (seed) {
5165
+ lines.push(...relink);
5166
+ continue;
5167
+ }
5168
+ lines.push(`if [ -e ${target} ]; then`, ...relink.map((l) => ` ${l}`), "fi");
4776
5169
  }
4777
5170
  return lines;
4778
5171
  }
5172
+ function buildRelinkSharedPaths(paths, releaseExpr) {
5173
+ const manifest = sharedPathsManifestPath(paths.base);
5174
+ return [
5175
+ `if [ -f ${manifest} ]; then`,
5176
+ ' while IFS="\t" read -r TS_CLOUD_SP TS_CLOUD_TGT; do',
5177
+ ' [ -n "$TS_CLOUD_SP" ] || continue',
5178
+ ` [ -n "$TS_CLOUD_TGT" ] || TS_CLOUD_TGT="${paths.shared}/$TS_CLOUD_SP"`,
5179
+ ' [ -e "$TS_CLOUD_TGT" ] || continue',
5180
+ ` rm -rf ${releaseExpr}/"$TS_CLOUD_SP"`,
5181
+ ` mkdir -p "$(dirname ${releaseExpr}/"$TS_CLOUD_SP")"`,
5182
+ ` ln -sfn "$TS_CLOUD_TGT" ${releaseExpr}/"$TS_CLOUD_SP"`,
5183
+ ` done < ${manifest}`,
5184
+ "fi"
5185
+ ];
5186
+ }
4779
5187
  function buildActivateRelease(paths) {
4780
5188
  return [`ln -sfn ${paths.release} ${paths.current}.tmp`, `mv -Tf ${paths.current}.tmp ${paths.current}`];
4781
5189
  }
4782
5190
  function buildRollbackScript(paths, options = {}) {
4783
5191
  const flip = options.to ? [
4784
5192
  `[ -d ${paths.releases}/${options.to} ] || { echo "rollback target ${paths.releases}/${options.to} not found" >&2; exit 1; }`,
5193
+ ...buildRelinkSharedPaths(paths, `${paths.releases}/${options.to}`),
4785
5194
  `ln -sfn ${paths.releases}/${options.to} ${paths.current}.tmp`,
4786
5195
  `mv -Tf ${paths.current}.tmp ${paths.current}`
4787
5196
  ] : [
4788
5197
  `TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
4789
5198
  `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)',
4790
5199
  '[ -n "$TS_CLOUD_PREV" ] || { echo "no previous release to roll back to" >&2; exit 1; }',
5200
+ ...buildRelinkSharedPaths(paths, '"$TS_CLOUD_PREV"'),
4791
5201
  `ln -sfn "$TS_CLOUD_PREV" ${paths.current}.tmp`,
4792
5202
  `mv -Tf ${paths.current}.tmp ${paths.current}`,
4793
5203
  'echo "rolled back to $TS_CLOUD_PREV"'
@@ -4875,7 +5285,7 @@ function buildSiteDeployScript(options) {
4875
5285
  const unitBase = `${slug}-${siteName}`;
4876
5286
  const serviceName = `${unitBase}.service`;
4877
5287
  const tarball = releaseTarballTmpPath(slug, siteName, releaseId);
4878
- const sharedPaths = [...new Set([".env", ...options.sharedPaths ?? []])];
5288
+ const sharedPaths = dedupeSharedPaths([".env", ...options.sharedPaths ?? []]);
4879
5289
  const envFile = formatEnvFile(envEntries);
4880
5290
  const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
4881
5291
  const stageRelease = [
@@ -5353,7 +5763,7 @@ function queueUnitName(slug, siteName, index) {
5353
5763
  function daemonUnitName(slug, siteName, daemon, index) {
5354
5764
  return `${slug}-${siteName}-daemon-${daemon.name ? slugify(daemon.name) : slugify(daemon.command).slice(0, 32) || String(index)}`;
5355
5765
  }
5356
- function systemdUnit(opts) {
5766
+ function systemdUnit2(opts) {
5357
5767
  const lines = [
5358
5768
  "[Unit]",
5359
5769
  `Description=${opts.description}`,
@@ -5400,7 +5810,7 @@ function buildSiteServicesScript(options) {
5400
5810
  for (let p = 0;p < processes; p++) {
5401
5811
  const name = queueUnitName(slug, siteName, desiredUnits.length);
5402
5812
  desiredUnits.push(name);
5403
- out.push(...writeUnitScript(name, systemdUnit({
5813
+ out.push(...writeUnitScript(name, systemdUnit2({
5404
5814
  description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
5405
5815
  workingDir: current,
5406
5816
  execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
@@ -5415,7 +5825,7 @@ function buildSiteServicesScript(options) {
5415
5825
  for (let p = 0;p < processes; p++) {
5416
5826
  const name = `${daemonUnitName(slug, siteName, daemon, dIndex)}-${p}`;
5417
5827
  desiredUnits.push(name);
5418
- out.push(...writeUnitScript(name, systemdUnit({
5828
+ out.push(...writeUnitScript(name, systemdUnit2({
5419
5829
  description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
5420
5830
  workingDir: daemon.directory || current,
5421
5831
  execStart: driver.wrapExec(daemon.command),
@@ -5430,7 +5840,7 @@ function buildSiteServicesScript(options) {
5430
5840
  const schedulerUnit = `${slug}-${siteName}-scheduler`;
5431
5841
  if (schedulerEnabled && driver.schedulerMode === "daemon") {
5432
5842
  desiredUnits.push(schedulerUnit);
5433
- out.push(...writeUnitScript(schedulerUnit, systemdUnit({
5843
+ out.push(...writeUnitScript(schedulerUnit, systemdUnit2({
5434
5844
  description: `${siteName} scheduler (managed by ts-cloud)`,
5435
5845
  workingDir: current,
5436
5846
  execStart: driver.wrapExec(driver.schedulerCommand(ctx)),