@stacksjs/ts-cloud 0.7.93 → 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 (68) hide show
  1. package/dist/bin/cli.js +1188 -1184
  2. package/dist/bin/dashboard-server.js +413 -409
  3. package/dist/{chunk-y5wrqyyz.js → chunk-0htae1c2.js} +365 -1
  4. package/dist/{chunk-fbwcv2vf.js → chunk-bs3jfnzp.js} +451 -41
  5. package/dist/deploy/dashboard-database.d.ts +91 -3
  6. package/dist/deploy/dashboard-vitess.d.ts +154 -0
  7. package/dist/deploy/dashboard-vitess.test.d.ts +1 -0
  8. package/dist/deploy/index.js +2 -2
  9. package/dist/drivers/hetzner/client.d.ts +96 -0
  10. package/dist/drivers/hetzner/migrate-server.d.ts +124 -0
  11. package/dist/drivers/hetzner/migrate-server.test.d.ts +1 -0
  12. package/dist/drivers/hetzner/role-swap.d.ts +139 -0
  13. package/dist/drivers/hetzner/role-swap.test.d.ts +1 -0
  14. package/dist/drivers/index.js +1 -1
  15. package/dist/drivers/shared/db-provision.d.ts +4 -0
  16. package/dist/drivers/shared/deploy-script.d.ts +17 -1
  17. package/dist/drivers/shared/package-manager.d.ts +2 -0
  18. package/dist/drivers/shared/releases.d.ts +41 -2
  19. package/dist/drivers/shared/vitess-provision.d.ts +133 -0
  20. package/dist/drivers/shared/vitess-provision.test.d.ts +1 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +6 -2
  23. package/dist/ui/access-denied.html +2 -2
  24. package/dist/ui/account/automation.html +4 -4
  25. package/dist/ui/account/security.html +2 -2
  26. package/dist/ui/applications/compose.html +4 -4
  27. package/dist/ui/data/backups.html +4 -4
  28. package/dist/ui/data/services.html +4 -4
  29. package/dist/ui/data/volumes.html +4 -4
  30. package/dist/ui/index.html +4 -4
  31. package/dist/ui/operations/alerts.html +4 -4
  32. package/dist/ui/operations/configuration.html +4 -4
  33. package/dist/ui/operations/jobs.html +4 -4
  34. package/dist/ui/operations/maintenance.html +4 -4
  35. package/dist/ui/operations/observability.html +4 -4
  36. package/dist/ui/operations/previews.html +4 -4
  37. package/dist/ui/operations/queue.html +4 -4
  38. package/dist/ui/operations/regions.html +4 -4
  39. package/dist/ui/operations/releases.html +3 -3
  40. package/dist/ui/operations/workloads.html +3 -3
  41. package/dist/ui/server/actions.html +4 -4
  42. package/dist/ui/server/activity.html +2 -2
  43. package/dist/ui/server/capacity.html +4 -4
  44. package/dist/ui/server/database.html +124 -8
  45. package/dist/ui/server/deployments.html +4 -4
  46. package/dist/ui/server/diagnostics.html +2 -2
  47. package/dist/ui/server/firewall.html +3 -3
  48. package/dist/ui/server/fleet.html +4 -4
  49. package/dist/ui/server/logs.html +4 -4
  50. package/dist/ui/server/metrics.html +4 -4
  51. package/dist/ui/server/security.html +2 -2
  52. package/dist/ui/server/sites.html +3 -3
  53. package/dist/ui/server/ssh-keys.html +4 -4
  54. package/dist/ui/server/team.html +3 -3
  55. package/dist/ui/serverless/alarms.html +4 -4
  56. package/dist/ui/serverless/assets.html +2 -2
  57. package/dist/ui/serverless/cost.html +2 -2
  58. package/dist/ui/serverless/data.html +4 -4
  59. package/dist/ui/serverless/firewall.html +2 -2
  60. package/dist/ui/serverless/functions.html +4 -4
  61. package/dist/ui/serverless/logs.html +4 -4
  62. package/dist/ui/serverless/metrics.html +2 -2
  63. package/dist/ui/serverless/queues.html +4 -4
  64. package/dist/ui/serverless/secrets.html +4 -4
  65. package/dist/ui/serverless/traces.html +4 -4
  66. package/dist/ui/serverless.html +4 -4
  67. package/dist/ui-src/pages/server/database.stx +257 -9
  68. package/package.json +3 -3
@@ -45,7 +45,7 @@ import {
45
45
  resolveSiteKind,
46
46
  resolveUiSource,
47
47
  siteInstallBase
48
- } from "./chunk-fbwcv2vf.js";
48
+ } from "./chunk-bs3jfnzp.js";
49
49
  import {
50
50
  artifactKey,
51
51
  buildCloudFormationTemplate,
@@ -39487,6 +39487,10 @@ async function resolveServerDashboardData(config6, environment2, options = {}) {
39487
39487
  }
39488
39488
 
39489
39489
  // src/deploy/dashboard-database.ts
39490
+ var EXTERNAL_ENGINES = new Set(["singlestore", "vitess"]);
39491
+ function isExternalEngine(engine) {
39492
+ return EXTERNAL_ENGINES.has(engine);
39493
+ }
39490
39494
  var SOCKETS = {
39491
39495
  mysql: "/var/lib/pantry/mysql/mysqld.sock",
39492
39496
  mariadb: "/var/lib/pantry/mariadb/mariadbd.sock"
@@ -39499,6 +39503,10 @@ function normalizeEngine(engine) {
39499
39503
  return "postgres";
39500
39504
  if (engine === "mariadb")
39501
39505
  return "mariadb";
39506
+ if (engine === "singlestore")
39507
+ return "singlestore";
39508
+ if (engine === "vitess")
39509
+ return "vitess";
39502
39510
  return "mysql";
39503
39511
  }
39504
39512
  function resolveDbEngine(config6) {
@@ -39525,6 +39533,8 @@ function pgExec(sql, database) {
39525
39533
  return [`${pgAdminCommand(database)} -tA <<'TS_CLOUD_PG_EOF'`, ...sql, "TS_CLOUD_PG_EOF"];
39526
39534
  }
39527
39535
  function buildListScript(engine, database) {
39536
+ if (engine === "vitess")
39537
+ return buildVitessListScript(database);
39528
39538
  if (engine === "postgres") {
39529
39539
  return pgExec([
39530
39540
  "SELECT 'DB=' || datname FROM pg_database WHERE datistemplate = false;",
@@ -39536,6 +39546,72 @@ function buildListScript(engine, database) {
39536
39546
  "SELECT DISTINCT CONCAT('USER=', User) FROM mysql.user WHERE User NOT IN ('root', 'mysql.sys', 'mysql.session', 'mysql.infoschema', 'debian-sys-maint');"
39537
39547
  ]);
39538
39548
  }
39549
+ function externalMysqlExec(sql, database, port) {
39550
+ const host2 = database?.host ?? "127.0.0.1";
39551
+ const user = database?.username ?? "root";
39552
+ const pass = database?.password ?? "";
39553
+ const tls = database?.ssl === false ? "" : " --ssl-mode=PREFERRED";
39554
+ const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
39555
+ const env = pass ? `MYSQL_PWD=${shq(pass)} ` : "";
39556
+ return [
39557
+ `${env}mysql -h ${shq(host2)} -P ${database?.port ?? port} -u ${shq(user)}${tls} --batch --skip-column-names <<'TS_CLOUD_SQL_EOF'`,
39558
+ ...sql,
39559
+ "TS_CLOUD_SQL_EOF"
39560
+ ];
39561
+ }
39562
+ var VTGATE_DEFAULT_PORT = 15306;
39563
+ function buildVitessListScript(database) {
39564
+ const script = externalMysqlExec(["SHOW KEYSPACES;"], database, VTGATE_DEFAULT_PORT);
39565
+ script[0] = `${script[0]} | sed 's/^/DB=/'`;
39566
+ return script;
39567
+ }
39568
+ function buildVitessTopologyScript(database) {
39569
+ return externalMysqlExec([
39570
+ `SHOW KEYSPACES;`,
39571
+ `SHOW VITESS_SHARDS;`,
39572
+ `SHOW VITESS_TABLETS;`
39573
+ ], database, VTGATE_DEFAULT_PORT);
39574
+ }
39575
+ function parseVitessTopology(output) {
39576
+ const keyspaces = [];
39577
+ const shards = [];
39578
+ const tablets = [];
39579
+ for (const rawLine of output.split(`
39580
+ `)) {
39581
+ const line = rawLine.trim();
39582
+ if (!line)
39583
+ continue;
39584
+ const cols = line.split("\t").map((c) => c.trim()).filter(Boolean);
39585
+ if (cols.length >= 6) {
39586
+ const [cell, keyspace, shard, type, state, alias, hostname] = cols;
39587
+ tablets.push({
39588
+ cell: cell ?? "",
39589
+ keyspace: keyspace ?? "",
39590
+ shard: shard ?? "",
39591
+ type: type ?? "",
39592
+ state: state ?? "",
39593
+ alias: alias ?? "",
39594
+ hostname: hostname ?? ""
39595
+ });
39596
+ continue;
39597
+ }
39598
+ if (cols.length === 1) {
39599
+ const only = cols[0];
39600
+ if (only.includes("/")) {
39601
+ const [keyspace, shard] = only.split("/");
39602
+ if (keyspace && shard)
39603
+ shards.push({ keyspace, shard });
39604
+ } else {
39605
+ keyspaces.push(only);
39606
+ }
39607
+ }
39608
+ }
39609
+ return {
39610
+ keyspaces: [...new Set(keyspaces)].filter(Boolean).sort(),
39611
+ shards,
39612
+ tablets
39613
+ };
39614
+ }
39539
39615
  function buildCreateDatabaseScript(engine, name, database) {
39540
39616
  if (engine === "postgres") {
39541
39617
  return pgExec([
@@ -39612,18 +39688,31 @@ async function runDb(config6, environment2, commands, comment) {
39612
39688
  stderr: result2.perInstance?.[0]?.error ?? result2.error ?? ""
39613
39689
  };
39614
39690
  }
39691
+ function externalEngineRefusal(engine, action) {
39692
+ const where = engine === "vitess" ? "Create a keyspace with `vtctldclient CreateKeyspace`, and manage vtgate credentials on the cluster." : "Use the provider console or the cluster's own admin connection.";
39693
+ return {
39694
+ ok: false,
39695
+ error: `${action} is not available for ${engine}: it is an external cluster, so this box has no engine to administer. ${where}`
39696
+ };
39697
+ }
39615
39698
  async function listDatabases(config6, environment2) {
39616
39699
  const engine = resolveDbEngine(config6);
39700
+ if (isExternalEngine(engine) && engine !== "vitess")
39701
+ return { ...externalEngineRefusal(engine, "Listing databases"), engine, databases: [], users: [] };
39617
39702
  const r = await runDb(config6, environment2, buildListScript(engine, resolveAppDatabase(config6)), "ts-cloud db:list");
39618
39703
  const parsed = r.ok && r.stdout ? parseDbList(r.stdout) : { databases: [], users: [] };
39619
39704
  return { ...r, engine, ...parsed };
39620
39705
  }
39621
39706
  async function createDatabase(config6, environment2, name) {
39622
39707
  const engine = resolveDbEngine(config6);
39708
+ if (isExternalEngine(engine))
39709
+ return externalEngineRefusal(engine, "Creating a database");
39623
39710
  return runDb(config6, environment2, buildCreateDatabaseScript(engine, name, resolveAppDatabase(config6)), `ts-cloud db:create ${name}`);
39624
39711
  }
39625
39712
  async function createDatabaseUser(config6, environment2, input) {
39626
39713
  const engine = resolveDbEngine(config6);
39714
+ if (isExternalEngine(engine))
39715
+ return externalEngineRefusal(engine, "Creating a database user");
39627
39716
  return runDb(config6, environment2, buildCreateUserScript(engine, input, resolveAppDatabase(config6)), `ts-cloud db:user ${input.username}`);
39628
39717
  }
39629
39718
  var DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
@@ -39667,6 +39756,8 @@ async function backupDatabase(config6, environment2, name) {
39667
39756
  if (!isValidDbIdentifier(name))
39668
39757
  return { ok: false, error: "Database name must be a valid identifier.", database: name };
39669
39758
  const engine = resolveDbEngine(config6);
39759
+ if (isExternalEngine(engine))
39760
+ return { ...externalEngineRefusal(engine, "Backing up"), database: name };
39670
39761
  const r = await runDb(config6, environment2, buildBackupScript(engine, name, DB_BACKUP_DIR, resolveAppDatabase(config6)), `ts-cloud db:backup ${name}`);
39671
39762
  return { ...r, database: name };
39672
39763
  }
@@ -39675,6 +39766,233 @@ async function listDatabaseBackups(config6, environment2) {
39675
39766
  const backups = r.ok && r.stdout ? parseBackups(r.stdout) : [];
39676
39767
  return { ...r, backups };
39677
39768
  }
39769
+ async function describeVitess(config6, environment2) {
39770
+ const engine = resolveDbEngine(config6);
39771
+ const empty = { keyspaces: [], shards: [], tablets: [] };
39772
+ if (engine !== "vitess")
39773
+ return { ok: false, error: "The configured database engine is not Vitess.", engine, ...empty };
39774
+ const r = await runDb(config6, environment2, buildVitessTopologyScript(resolveAppDatabase(config6)), "ts-cloud vitess:topology");
39775
+ const parsed = r.ok && r.stdout ? parseVitessTopology(r.stdout) : empty;
39776
+ return { ...r, engine, ...parsed };
39777
+ }
39778
+ function unhealthyTablets(tablets) {
39779
+ return tablets.filter((t) => t.state.toUpperCase() !== "SERVING");
39780
+ }
39781
+ function shardsMissingPrimary(topology2) {
39782
+ const withPrimary = new Set(topology2.tablets.filter((t) => t.type.toUpperCase() === "PRIMARY" && t.state.toUpperCase() === "SERVING").map((t) => `${t.keyspace}/${t.shard}`));
39783
+ return topology2.shards.filter((s) => !withPrimary.has(`${s.keyspace}/${s.shard}`));
39784
+ }
39785
+
39786
+ // src/deploy/dashboard-vitess.ts
39787
+ var VTCTLDCLIENT_BIN = "/usr/local/bin/vtctldclient";
39788
+ var DEFAULT_VTCTLDCLIENT_VERSION = "21.0.0";
39789
+ function sh(value2) {
39790
+ return `'${String(value2).split("'").join("'\\''")}'`;
39791
+ }
39792
+ function resolveVitessConfig(config6) {
39793
+ return resolveAppDatabase(config6)?.vitess ?? {};
39794
+ }
39795
+ function isValidKeyspaceName(value2) {
39796
+ return /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(value2);
39797
+ }
39798
+ function isValidMigrationUuid(value2) {
39799
+ return /^[0-9a-f]{8}(?:_[0-9a-f]{4}){3}_[0-9a-f]{12}$/i.test(value2);
39800
+ }
39801
+ function buildVtctldClientInstallScript(version3 = DEFAULT_VTCTLDCLIENT_VERSION) {
39802
+ const v = sh(version3);
39803
+ return [
39804
+ `if ${VTCTLDCLIENT_BIN} --version 2>/dev/null | grep -q ${v}; then echo "vtctldclient ${version3} already installed"; else`,
39805
+ " command -v curl >/dev/null 2>&1 || (apt-get update -y && apt-get install -y curl ca-certificates)",
39806
+ ' if [ "$(uname -m)" != "x86_64" ]; then',
39807
+ ' echo "vtctldclient: Vitess publishes only an x86_64 release tarball; this box is $(uname -m). Build from source or install via pantry." >&2',
39808
+ " exit 1",
39809
+ " fi",
39810
+ ' tmp="$(mktemp -d)"',
39811
+ ` api="https://api.github.com/repos/vitessio/vitess/releases/tags/v${version3}"`,
39812
+ ` url="$(curl -fsSL "$api" | grep -oE '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]+vitess-[0-9][^"]*\\.tar\\.gz"' | head -1 | sed -E 's/.*"(https[^"]+)"/\\1/')"`,
39813
+ ' if [ -z "$url" ]; then echo "vtctldclient: no release tarball found for v' + version3 + '" >&2; rm -rf "$tmp"; exit 1; fi',
39814
+ ` curl -fsSL "$url" | tar -xz -C "$tmp" --strip-components=2 --wildcards --occurrence=1 '*/bin/vtctldclient' || true`,
39815
+ ' if [ ! -s "$tmp/vtctldclient" ]; then',
39816
+ ' echo "vtctldclient: could not extract the binary from $url" >&2',
39817
+ ' rm -rf "$tmp"; exit 1',
39818
+ " fi",
39819
+ ` install -m 0755 "$tmp/vtctldclient" ${VTCTLDCLIENT_BIN}`,
39820
+ ' rm -rf "$tmp"',
39821
+ ` echo "installed vtctldclient ${version3}"`,
39822
+ "fi"
39823
+ ];
39824
+ }
39825
+ function vtctldCommand(vitess) {
39826
+ if (!vitess.vtctldAddr)
39827
+ return null;
39828
+ return `${VTCTLDCLIENT_BIN} --server ${sh(vitess.vtctldAddr)}`;
39829
+ }
39830
+ function buildCreateKeyspaceScript(vitess, name, options = {}) {
39831
+ const base = vtctldCommand(vitess);
39832
+ if (!base)
39833
+ return null;
39834
+ const sharded = options.sharded ? " --sharded" : "";
39835
+ return [`${base} CreateKeyspace${sharded} ${sh(name)}`];
39836
+ }
39837
+ function buildApplyVSchemaScript(vitess, keyspace, vschemaJson) {
39838
+ const base = vtctldCommand(vitess);
39839
+ if (!base)
39840
+ return null;
39841
+ return [
39842
+ 'vschema_tmp="$(mktemp)"',
39843
+ `cat > "$vschema_tmp" <<'TS_CLOUD_VSCHEMA_EOF'`,
39844
+ vschemaJson,
39845
+ "TS_CLOUD_VSCHEMA_EOF",
39846
+ `${base} ApplyVSchema --vschema-file "$vschema_tmp" ${sh(keyspace)}`,
39847
+ 'rm -f "$vschema_tmp"'
39848
+ ];
39849
+ }
39850
+ function buildApplySchemaScript(database, sql, strategy2 = "vitess") {
39851
+ return externalMysqlExec([`SET @@ddl_strategy='${strategy2}';`, sql.endsWith(";") ? sql : `${sql};`], database, VTGATE_DEFAULT_PORT);
39852
+ }
39853
+ function buildMigrationsScript(database, keyspace) {
39854
+ const statements = keyspace ? [`USE ${keyspace.replace(/[^A-Z0-9_]/gi, "")};`, "SHOW VITESS_MIGRATIONS;"] : ["SHOW VITESS_MIGRATIONS;"];
39855
+ const script = externalMysqlExec(statements, database, VTGATE_DEFAULT_PORT);
39856
+ script[0] = script[0].replace(" --skip-column-names", "");
39857
+ return script;
39858
+ }
39859
+ function parseMigrations(output) {
39860
+ const lines = output.split(`
39861
+ `).map((l) => l.trimEnd()).filter((l) => l.trim().length > 0);
39862
+ if (lines.length === 0)
39863
+ return [];
39864
+ const header3 = lines[0].split("\t").map((c) => c.trim());
39865
+ const col = (name) => header3.indexOf(name);
39866
+ const columns = {
39867
+ uuid: col("migration_uuid"),
39868
+ keyspace: col("keyspace"),
39869
+ shard: col("shard"),
39870
+ table: col("mysql_table"),
39871
+ status: col("migration_status"),
39872
+ strategy: col("strategy"),
39873
+ added: col("added_timestamp"),
39874
+ completed: col("completed_timestamp"),
39875
+ progress: col("progress")
39876
+ };
39877
+ if (columns.uuid === -1)
39878
+ return [];
39879
+ const rows = [];
39880
+ for (const line of lines.slice(1)) {
39881
+ const cells = line.split("\t");
39882
+ const at = (i) => i >= 0 ? (cells[i] ?? "").trim() : "";
39883
+ const uuid = at(columns.uuid);
39884
+ if (!uuid)
39885
+ continue;
39886
+ rows.push({
39887
+ uuid,
39888
+ keyspace: at(columns.keyspace),
39889
+ shard: at(columns.shard),
39890
+ table: at(columns.table),
39891
+ status: at(columns.status),
39892
+ strategy: at(columns.strategy),
39893
+ added: at(columns.added),
39894
+ completed: at(columns.completed),
39895
+ progress: at(columns.progress)
39896
+ });
39897
+ }
39898
+ return rows;
39899
+ }
39900
+ function failedMigrations(migrations2) {
39901
+ return migrations2.filter((m) => m.status.toLowerCase() === "failed");
39902
+ }
39903
+ function runningMigrations(migrations2) {
39904
+ return migrations2.filter((m) => ["running", "queued", "ready"].includes(m.status.toLowerCase()));
39905
+ }
39906
+ function buildMigrationActionScript(database, uuid, action) {
39907
+ const verb = action.toUpperCase();
39908
+ return externalMysqlExec([`ALTER VITESS_MIGRATION '${uuid}' ${verb};`], database, VTGATE_DEFAULT_PORT);
39909
+ }
39910
+ function requireVitess(config6) {
39911
+ if (resolveDbEngine(config6) !== "vitess")
39912
+ return { ok: false, error: "The configured database engine is not Vitess." };
39913
+ return null;
39914
+ }
39915
+ function requireControlPlane(vitess) {
39916
+ if (!vitess.vtctldAddr) {
39917
+ return {
39918
+ ok: false,
39919
+ error: "No vtctld address is configured. Creating keyspaces and applying a VSchema are topology operations that vtgate cannot perform. " + "Set `infrastructure.appDatabase.vitess.vtctldAddr` (for example `vtctld.internal:15999`) and redeploy."
39920
+ };
39921
+ }
39922
+ return null;
39923
+ }
39924
+ async function installVtctldClient(config6, environment2) {
39925
+ const wrong = requireVitess(config6);
39926
+ if (wrong)
39927
+ return wrong;
39928
+ const vitess = resolveVitessConfig(config6);
39929
+ return runDb(config6, environment2, buildVtctldClientInstallScript(vitess.clientVersion), "ts-cloud vitess:install-client");
39930
+ }
39931
+ async function createKeyspace(config6, environment2, name, options = {}) {
39932
+ const wrong = requireVitess(config6);
39933
+ if (wrong)
39934
+ return { ...wrong, keyspace: name };
39935
+ if (!isValidKeyspaceName(name))
39936
+ return { ok: false, error: "Keyspace name must be a valid identifier.", keyspace: name };
39937
+ const vitess = resolveVitessConfig(config6);
39938
+ const missing = requireControlPlane(vitess);
39939
+ if (missing)
39940
+ return { ...missing, keyspace: name };
39941
+ const script = buildCreateKeyspaceScript(vitess, name, options);
39942
+ if (!script)
39943
+ return { ...requireControlPlane({}), keyspace: name };
39944
+ const r = await runDb(config6, environment2, script, `ts-cloud vitess:create-keyspace ${name}`);
39945
+ return { ...r, keyspace: name };
39946
+ }
39947
+ async function applyVSchema(config6, environment2, keyspace, vschemaJson) {
39948
+ const wrong = requireVitess(config6);
39949
+ if (wrong)
39950
+ return { ...wrong, keyspace };
39951
+ if (!isValidKeyspaceName(keyspace))
39952
+ return { ok: false, error: "Keyspace name must be a valid identifier.", keyspace };
39953
+ try {
39954
+ const parsed = JSON.parse(vschemaJson);
39955
+ if (!parsed || typeof parsed !== "object")
39956
+ return { ok: false, error: "The VSchema must be a JSON object.", keyspace };
39957
+ } catch (error2) {
39958
+ return { ok: false, error: `The VSchema is not valid JSON: ${error2?.message ?? error2}`, keyspace };
39959
+ }
39960
+ const vitess = resolveVitessConfig(config6);
39961
+ const missing = requireControlPlane(vitess);
39962
+ if (missing)
39963
+ return { ...missing, keyspace };
39964
+ const script = buildApplyVSchemaScript(vitess, keyspace, vschemaJson);
39965
+ if (!script)
39966
+ return { ...requireControlPlane({}), keyspace };
39967
+ const r = await runDb(config6, environment2, script, `ts-cloud vitess:apply-vschema ${keyspace}`);
39968
+ return { ...r, keyspace };
39969
+ }
39970
+ async function applySchemaChange(config6, environment2, sql) {
39971
+ const wrong = requireVitess(config6);
39972
+ if (wrong)
39973
+ return wrong;
39974
+ if (!sql.trim())
39975
+ return { ok: false, error: "No SQL was provided." };
39976
+ const vitess = resolveVitessConfig(config6);
39977
+ return runDb(config6, environment2, buildApplySchemaScript(resolveAppDatabase(config6), sql, vitess.ddlStrategy ?? "vitess"), "ts-cloud vitess:apply-schema");
39978
+ }
39979
+ async function listMigrations(config6, environment2, keyspace) {
39980
+ const wrong = requireVitess(config6);
39981
+ if (wrong)
39982
+ return { ...wrong, migrations: [], failed: [], running: [] };
39983
+ const r = await runDb(config6, environment2, buildMigrationsScript(resolveAppDatabase(config6), keyspace), "ts-cloud vitess:migrations");
39984
+ const migrations2 = r.ok && r.stdout ? parseMigrations(r.stdout) : [];
39985
+ return { ...r, migrations: migrations2, failed: failedMigrations(migrations2), running: runningMigrations(migrations2) };
39986
+ }
39987
+ async function actOnMigration(config6, environment2, uuid, action) {
39988
+ const wrong = requireVitess(config6);
39989
+ if (wrong)
39990
+ return { ...wrong, uuid };
39991
+ if (!isValidMigrationUuid(uuid))
39992
+ return { ok: false, error: "Migration UUID is not in the expected format.", uuid };
39993
+ const r = await runDb(config6, environment2, buildMigrationActionScript(resolveAppDatabase(config6), uuid, action), `ts-cloud vitess:migration ${action} ${uuid}`);
39994
+ return { ...r, uuid };
39995
+ }
39678
39996
 
39679
39997
  // src/deploy/dashboard-policy.ts
39680
39998
  var FAIL_CLOSED = {
@@ -39914,6 +40232,13 @@ var POLICIES = {
39914
40232
  "DELETE /api/firewall": { capability: "fleet:manage" },
39915
40233
  "GET /api/databases": { capability: "data:read" },
39916
40234
  "POST /api/databases": { capability: "data:admin" },
40235
+ "GET /api/databases/vitess": { capability: "data:read" },
40236
+ "GET /api/databases/vitess/migrations": { capability: "data:read" },
40237
+ "POST /api/databases/vitess/migrations/action": { capability: "data:admin" },
40238
+ "POST /api/databases/vitess/keyspaces": { capability: "data:admin" },
40239
+ "POST /api/databases/vitess/vschema": { capability: "data:admin" },
40240
+ "POST /api/databases/vitess/schema": { capability: "data:admin" },
40241
+ "POST /api/databases/vitess/install-client": { capability: "data:admin" },
39917
40242
  "GET /api/databases/backups": { capability: "backups:read" },
39918
40243
  "POST /api/databases/backup": { capability: "backups:create" },
39919
40244
  "POST /api/databases/users": { capability: "data:admin" },
@@ -48887,6 +49212,45 @@ data: ${JSON.stringify({ cursor: position, at: new Date().toISOString() })}
48887
49212
  }, 422);
48888
49213
  return json15({ ...await createDatabase(config6, environment2, name), name });
48889
49214
  }
49215
+ if (url.pathname === "/api/databases/vitess" && req.method === "GET") {
49216
+ const topology2 = await describeVitess(config6, environment2);
49217
+ return json15({
49218
+ ...topology2,
49219
+ unhealthy: unhealthyTablets(topology2.tablets),
49220
+ missingPrimary: shardsMissingPrimary(topology2)
49221
+ });
49222
+ }
49223
+ if (url.pathname === "/api/databases/vitess/migrations" && req.method === "GET") {
49224
+ const keyspace = url.searchParams.get("keyspace") ?? undefined;
49225
+ return json15(await listMigrations(config6, environment2, keyspace));
49226
+ }
49227
+ if (url.pathname === "/api/databases/vitess/migrations/action" && req.method === "POST") {
49228
+ const body = await readJsonBody(req);
49229
+ const uuid = String(body.uuid ?? "").trim();
49230
+ const action = String(body.action ?? "");
49231
+ if (!["retry", "cancel", "cleanup", "complete"].includes(action))
49232
+ return json15({ ok: false, error: "Unknown migration action." }, 422);
49233
+ return json15(await actOnMigration(config6, environment2, uuid, action));
49234
+ }
49235
+ if (url.pathname === "/api/databases/vitess/keyspaces" && req.method === "POST") {
49236
+ const body = await readJsonBody(req);
49237
+ const name = String(body.name ?? "").trim();
49238
+ const sharded = body.sharded === true;
49239
+ return json15(await createKeyspace(config6, environment2, name, { sharded }));
49240
+ }
49241
+ if (url.pathname === "/api/databases/vitess/vschema" && req.method === "POST") {
49242
+ const body = await readJsonBody(req);
49243
+ const keyspace = String(body.keyspace ?? "").trim();
49244
+ const vschema = typeof body.vschema === "string" ? body.vschema : JSON.stringify(body.vschema ?? null);
49245
+ return json15(await applyVSchema(config6, environment2, keyspace, vschema));
49246
+ }
49247
+ if (url.pathname === "/api/databases/vitess/schema" && req.method === "POST") {
49248
+ const body = await readJsonBody(req);
49249
+ const sql = String(body.sql ?? "");
49250
+ return json15(await applySchemaChange(config6, environment2, sql));
49251
+ }
49252
+ if (url.pathname === "/api/databases/vitess/install-client" && req.method === "POST")
49253
+ return json15(await installVtctldClient(config6, environment2));
48890
49254
  if (url.pathname === "/api/databases/backups" && req.method === "GET")
48891
49255
  return json15(await listDatabaseBackups(config6, environment2));
48892
49256
  if (url.pathname === "/api/databases/backup" && req.method === "POST") {