@stacksjs/ts-cloud 0.7.93 → 0.7.95

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 (71) 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-3d4204h1.js} +365 -1
  4. package/dist/{chunk-fbwcv2vf.js → chunk-xp3txtg9.js} +458 -42
  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 +3 -3
  25. package/dist/ui/applications/compose.html +4 -4
  26. package/dist/ui/applications/new.html +2 -2
  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/integrations.html +2 -2
  32. package/dist/ui/operations/alerts.html +4 -4
  33. package/dist/ui/operations/configuration.html +4 -4
  34. package/dist/ui/operations/jobs.html +4 -4
  35. package/dist/ui/operations/maintenance.html +4 -4
  36. package/dist/ui/operations/observability.html +4 -4
  37. package/dist/ui/operations/previews.html +4 -4
  38. package/dist/ui/operations/queue.html +4 -4
  39. package/dist/ui/operations/regions.html +4 -4
  40. package/dist/ui/operations/releases.html +4 -4
  41. package/dist/ui/operations/workloads.html +4 -4
  42. package/dist/ui/security.html +2 -2
  43. package/dist/ui/server/actions.html +4 -4
  44. package/dist/ui/server/activity.html +2 -2
  45. package/dist/ui/server/capacity.html +3 -3
  46. package/dist/ui/server/database.html +123 -7
  47. package/dist/ui/server/deployments.html +4 -4
  48. package/dist/ui/server/firewall.html +4 -4
  49. package/dist/ui/server/fleet.html +4 -4
  50. package/dist/ui/server/logs.html +4 -4
  51. package/dist/ui/server/metrics.html +4 -4
  52. package/dist/ui/server/security.html +2 -2
  53. package/dist/ui/server/services.html +2 -2
  54. package/dist/ui/server/sites.html +4 -4
  55. package/dist/ui/server/ssh-keys.html +4 -4
  56. package/dist/ui/server/team.html +4 -4
  57. package/dist/ui/server/terminal.html +2 -2
  58. package/dist/ui/serverless/alarms.html +4 -4
  59. package/dist/ui/serverless/assets.html +2 -2
  60. package/dist/ui/serverless/cost.html +2 -2
  61. package/dist/ui/serverless/data.html +4 -4
  62. package/dist/ui/serverless/firewall.html +2 -2
  63. package/dist/ui/serverless/functions.html +4 -4
  64. package/dist/ui/serverless/logs.html +4 -4
  65. package/dist/ui/serverless/metrics.html +2 -2
  66. package/dist/ui/serverless/queues.html +4 -4
  67. package/dist/ui/serverless/secrets.html +4 -4
  68. package/dist/ui/serverless/traces.html +4 -4
  69. package/dist/ui/serverless.html +4 -4
  70. package/dist/ui-src/pages/server/database.stx +257 -9
  71. 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@8.0.43");
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";
@@ -1420,7 +1704,13 @@ function buildCertManagementCommands(options) {
1420
1704
  `DOMAINS='${csv}'`,
1421
1705
  'before=$(cat "$CERTS"/*.crt 2>/dev/null | sha256sum)',
1422
1706
  `for d in ${spaced}; do`,
1423
- ' [ -s "$CERTS/$d.crt" ] || $TLSX acme:issue -d "$d" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --prod --email "$EMAIL" || echo "issue $d failed (non-fatal)"',
1707
+ ' if [ ! -s "$CERTS/$d.crt" ]; then',
1708
+ ' $TLSX acme:issue -d "$d" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --prod --email "$EMAIL" || echo "issue $d failed (non-fatal)"',
1709
+ " # rpx reloads its SNI set when a PEM appears. Complete that reload",
1710
+ " # before the next hostname starts http-01, or :80 can disappear in",
1711
+ " # the middle of the following challenge on a multi-domain deploy.",
1712
+ ` [ -s "$CERTS/$d.crt" ] && systemctl restart ${RPX_SERVICE_NAME}`,
1713
+ " fi",
1424
1714
  "done",
1425
1715
  '$TLSX acme:renew --domains "$DOMAINS" --method http-01 --webroot "$WEBROOT" --dir "$CERTS" --days 30 --prod --email "$EMAIL" || echo "renew: some domains failed (non-fatal)"',
1426
1716
  'rm -f "$CERTS"/*.chain.crt',
@@ -2516,6 +2806,65 @@ class HetznerClient {
2516
2806
  });
2517
2807
  return data.action;
2518
2808
  }
2809
+ async powerOffServer(serverId) {
2810
+ const data = await this.request("POST", `/servers/${serverId}/actions/poweroff`, {});
2811
+ return data.action;
2812
+ }
2813
+ async stopServer(serverId, options) {
2814
+ const server = await this.getServer(serverId);
2815
+ if (server.status === "off")
2816
+ return;
2817
+ await this.shutdownServer(serverId);
2818
+ const grace = options?.gracefulWaitMs ?? 120000;
2819
+ const poll = options?.pollIntervalMs ?? 3000;
2820
+ const start = Date.now();
2821
+ while (Date.now() - start < grace) {
2822
+ if ((await this.getServer(serverId)).status === "off")
2823
+ return;
2824
+ await new Promise((resolve) => setTimeout(resolve, poll));
2825
+ }
2826
+ const action = await this.powerOffServer(serverId);
2827
+ await this.waitForAction(action.id);
2828
+ await this.waitForServerStatus(serverId, "off");
2829
+ }
2830
+ async renameServer(serverId, name) {
2831
+ const data = await this.request("PUT", `/servers/${serverId}`, { name });
2832
+ return data.server;
2833
+ }
2834
+ async listImages(options) {
2835
+ const query = options?.type ? `?type=${encodeURIComponent(options.type)}` : "";
2836
+ return this.requestAll(`/images${query}`, "images");
2837
+ }
2838
+ async getImage(id) {
2839
+ const data = await this.request("GET", `/images/${id}`);
2840
+ return data.image;
2841
+ }
2842
+ async createImage(serverId, options) {
2843
+ return this.request("POST", `/servers/${serverId}/actions/create_image`, { description: options.description, type: options.type ?? "snapshot", labels: options.labels });
2844
+ }
2845
+ async deleteImage(imageId) {
2846
+ await this.request("DELETE", `/images/${imageId}`);
2847
+ }
2848
+ async rebuildServer(serverId, image) {
2849
+ const data = await this.request("POST", `/servers/${serverId}/actions/rebuild`, {
2850
+ image
2851
+ });
2852
+ return data.action;
2853
+ }
2854
+ async listPrimaryIps() {
2855
+ return this.requestAll("/primary_ips", "primary_ips");
2856
+ }
2857
+ async unassignPrimaryIp(ipId) {
2858
+ const data = await this.request("POST", `/primary_ips/${ipId}/actions/unassign`, {});
2859
+ return data.action;
2860
+ }
2861
+ async assignPrimaryIp(ipId, serverId) {
2862
+ const data = await this.request("POST", `/primary_ips/${ipId}/actions/assign`, {
2863
+ assignee_id: serverId,
2864
+ assignee_type: "server"
2865
+ });
2866
+ return data.action;
2867
+ }
2519
2868
  async waitForAction(actionId, options) {
2520
2869
  const pollInterval = options?.pollIntervalMs ?? 2000;
2521
2870
  const maxWait = options?.maxWaitMs ?? 300000;
@@ -2696,16 +3045,16 @@ class HetznerDriver {
2696
3045
  this.client = options.client ?? new HetznerClient({
2697
3046
  apiToken: apiToken ?? ""
2698
3047
  });
2699
- const settings = resolveHetznerSettings(undefined, {
3048
+ const settings2 = resolveHetznerSettings(undefined, {
2700
3049
  sshPrivateKeyPath: options.sshPrivateKeyPath,
2701
3050
  sshPublicKeyPath: options.sshPublicKeyPath,
2702
3051
  sshUser: options.sshUser,
2703
3052
  location: options.location
2704
3053
  });
2705
- this.sshPrivateKeyPath = settings.sshPrivateKeyPath;
2706
- this.sshPublicKeyPath = settings.sshPublicKeyPath;
2707
- this.sshUser = settings.sshUser;
2708
- this.location = settings.location;
3054
+ this.sshPrivateKeyPath = settings2.sshPrivateKeyPath;
3055
+ this.sshPublicKeyPath = settings2.sshPublicKeyPath;
3056
+ this.sshUser = settings2.sshUser;
3057
+ this.location = settings2.location;
2709
3058
  this.waitForBoot = options.waitForBoot ?? true;
2710
3059
  this.bootWait = {
2711
3060
  sshIntervalMs: options.bootWait?.sshIntervalMs ?? 5000,
@@ -4690,6 +5039,9 @@ function deployHistoryPath(base) {
4690
5039
  function deployLogPath(base, releaseId) {
4691
5040
  return `${deployMetaDir(base)}/deploys/${releaseId}.log`;
4692
5041
  }
5042
+ function sharedPathsManifestPath(base) {
5043
+ return `${deployMetaDir(base)}/shared-paths`;
5044
+ }
4693
5045
  function releasePaths(base, releaseId) {
4694
5046
  const root = base.replace(/\/+$/, "");
4695
5047
  return {
@@ -4706,17 +5058,59 @@ function isFileSharedPath(p) {
4706
5058
  return true;
4707
5059
  return /\.[a-z0-9]+$/i.test(name);
4708
5060
  }
5061
+ function sharedPathOf(entry) {
5062
+ return typeof entry === "string" ? entry : entry.path;
5063
+ }
5064
+ function dedupeSharedPaths(entries) {
5065
+ const byPath = new Map;
5066
+ for (const entry of entries)
5067
+ byPath.set(sharedPathOf(entry), entry);
5068
+ return [...byPath.values()];
5069
+ }
5070
+ function resolveSharedPath(paths, entry) {
5071
+ if (typeof entry === "string")
5072
+ return { path: entry, target: `${paths.shared}/${entry}`, seed: true };
5073
+ return {
5074
+ path: entry.path,
5075
+ target: entry.target ?? `${paths.shared}/${entry.path}`,
5076
+ seed: entry.seed !== false
5077
+ };
5078
+ }
5079
+ function buildAdoptSharedPathFn(paths) {
5080
+ return [
5081
+ "ts_cloud_adopt_shared() {",
5082
+ ' TS_CLOUD_SP="$1"',
5083
+ ' TS_CLOUD_DST="$2"',
5084
+ ` TS_CLOUD_SRC="${paths.current}/$TS_CLOUD_SP"`,
5085
+ ' if [ -e "$TS_CLOUD_DST" ]; then return 0; fi',
5086
+ ' if [ -L "$TS_CLOUD_SRC" ] || [ ! -e "$TS_CLOUD_SRC" ]; then return 0; fi',
5087
+ ' mkdir -p "$(dirname "$TS_CLOUD_DST")"',
5088
+ ' cp -a "$TS_CLOUD_SRC" "$TS_CLOUD_DST"',
5089
+ " for TS_CLOUD_SIDECAR in -wal -shm; do",
5090
+ ' if [ -f "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" ]; then cp -a "$TS_CLOUD_SRC$TS_CLOUD_SIDECAR" "$TS_CLOUD_DST$TS_CLOUD_SIDECAR"; fi',
5091
+ " done",
5092
+ ' echo "[ts-cloud] adopted $TS_CLOUD_SP from the live release into shared/ — it now survives deploys"',
5093
+ " return 0",
5094
+ "}"
5095
+ ];
5096
+ }
4709
5097
  function buildEnsureReleaseLayout(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
4710
- const lines = [`mkdir -p ${paths.releases} ${paths.shared}`];
4711
- for (const p of sharedPaths) {
5098
+ const lines = [`mkdir -p ${paths.releases} ${paths.shared}`, ...buildAdoptSharedPathFn(paths)];
5099
+ const resolved = sharedPaths.map((entry) => resolveSharedPath(paths, entry));
5100
+ for (const { path: p, target, seed } of resolved) {
5101
+ if (!seed)
5102
+ continue;
5103
+ lines.push(`ts_cloud_adopt_shared '${p}' '${target}'`);
4712
5104
  if (isFileSharedPath(p)) {
4713
- lines.push(`mkdir -p "$(dirname ${paths.shared}/${p})"`, `touch ${paths.shared}/${p}`);
5105
+ lines.push(`mkdir -p "$(dirname ${target})"`, `touch ${target}`);
4714
5106
  } 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`);
5107
+ 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
5108
  } else {
4717
- lines.push(`mkdir -p ${paths.shared}/${p}`);
5109
+ lines.push(`mkdir -p ${target}`);
4718
5110
  }
4719
5111
  }
5112
+ const manifest = sharedPathsManifestPath(paths.base);
5113
+ 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
5114
  return lines;
4721
5115
  }
4722
5116
  function buildDeployLock(paths, waitSeconds = 900) {
@@ -4769,25 +5163,47 @@ function buildPromoteStagedRelease(paths) {
4769
5163
  }
4770
5164
  function buildLinkSharedPaths(paths, sharedPaths = DEFAULT_SHARED_PATHS) {
4771
5165
  const lines = [];
4772
- for (const p of sharedPaths) {
4773
- const target = `${paths.shared}/${p}`;
5166
+ for (const entry of sharedPaths) {
5167
+ const { path: p, target, seed } = resolveSharedPath(paths, entry);
4774
5168
  const link = `${paths.release}/${p}`;
4775
- lines.push(`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`);
5169
+ const relink = [`rm -rf ${link}`, `mkdir -p "$(dirname ${link})"`, `ln -sfn ${target} ${link}`];
5170
+ if (seed) {
5171
+ lines.push(...relink);
5172
+ continue;
5173
+ }
5174
+ lines.push(`if [ -e ${target} ]; then`, ...relink.map((l) => ` ${l}`), "fi");
4776
5175
  }
4777
5176
  return lines;
4778
5177
  }
5178
+ function buildRelinkSharedPaths(paths, releaseExpr) {
5179
+ const manifest = sharedPathsManifestPath(paths.base);
5180
+ return [
5181
+ `if [ -f ${manifest} ]; then`,
5182
+ ' while IFS="\t" read -r TS_CLOUD_SP TS_CLOUD_TGT; do',
5183
+ ' [ -n "$TS_CLOUD_SP" ] || continue',
5184
+ ` [ -n "$TS_CLOUD_TGT" ] || TS_CLOUD_TGT="${paths.shared}/$TS_CLOUD_SP"`,
5185
+ ' [ -e "$TS_CLOUD_TGT" ] || continue',
5186
+ ` rm -rf ${releaseExpr}/"$TS_CLOUD_SP"`,
5187
+ ` mkdir -p "$(dirname ${releaseExpr}/"$TS_CLOUD_SP")"`,
5188
+ ` ln -sfn "$TS_CLOUD_TGT" ${releaseExpr}/"$TS_CLOUD_SP"`,
5189
+ ` done < ${manifest}`,
5190
+ "fi"
5191
+ ];
5192
+ }
4779
5193
  function buildActivateRelease(paths) {
4780
5194
  return [`ln -sfn ${paths.release} ${paths.current}.tmp`, `mv -Tf ${paths.current}.tmp ${paths.current}`];
4781
5195
  }
4782
5196
  function buildRollbackScript(paths, options = {}) {
4783
5197
  const flip = options.to ? [
4784
5198
  `[ -d ${paths.releases}/${options.to} ] || { echo "rollback target ${paths.releases}/${options.to} not found" >&2; exit 1; }`,
5199
+ ...buildRelinkSharedPaths(paths, `${paths.releases}/${options.to}`),
4785
5200
  `ln -sfn ${paths.releases}/${options.to} ${paths.current}.tmp`,
4786
5201
  `mv -Tf ${paths.current}.tmp ${paths.current}`
4787
5202
  ] : [
4788
5203
  `TS_CLOUD_CURRENT=$(readlink -f ${paths.current} 2>/dev/null || true)`,
4789
5204
  `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
5205
  '[ -n "$TS_CLOUD_PREV" ] || { echo "no previous release to roll back to" >&2; exit 1; }',
5206
+ ...buildRelinkSharedPaths(paths, '"$TS_CLOUD_PREV"'),
4791
5207
  `ln -sfn "$TS_CLOUD_PREV" ${paths.current}.tmp`,
4792
5208
  `mv -Tf ${paths.current}.tmp ${paths.current}`,
4793
5209
  'echo "rolled back to $TS_CLOUD_PREV"'
@@ -4875,7 +5291,7 @@ function buildSiteDeployScript(options) {
4875
5291
  const unitBase = `${slug}-${siteName}`;
4876
5292
  const serviceName = `${unitBase}.service`;
4877
5293
  const tarball = releaseTarballTmpPath(slug, siteName, releaseId);
4878
- const sharedPaths = [...new Set([".env", ...options.sharedPaths ?? []])];
5294
+ const sharedPaths = dedupeSharedPaths([".env", ...options.sharedPaths ?? []]);
4879
5295
  const envFile = formatEnvFile(envEntries);
4880
5296
  const preStart = preStartCommands.length > 0 ? [`cd ${paths.release}`, ...preStartCommands] : [];
4881
5297
  const stageRelease = [
@@ -5353,7 +5769,7 @@ function queueUnitName(slug, siteName, index) {
5353
5769
  function daemonUnitName(slug, siteName, daemon, index) {
5354
5770
  return `${slug}-${siteName}-daemon-${daemon.name ? slugify(daemon.name) : slugify(daemon.command).slice(0, 32) || String(index)}`;
5355
5771
  }
5356
- function systemdUnit(opts) {
5772
+ function systemdUnit2(opts) {
5357
5773
  const lines = [
5358
5774
  "[Unit]",
5359
5775
  `Description=${opts.description}`,
@@ -5400,7 +5816,7 @@ function buildSiteServicesScript(options) {
5400
5816
  for (let p = 0;p < processes; p++) {
5401
5817
  const name = queueUnitName(slug, siteName, desiredUnits.length);
5402
5818
  desiredUnits.push(name);
5403
- out.push(...writeUnitScript(name, systemdUnit({
5819
+ out.push(...writeUnitScript(name, systemdUnit2({
5404
5820
  description: `${siteName} queue worker ${qIndex}.${p} (managed by ts-cloud)`,
5405
5821
  workingDir: current,
5406
5822
  execStart: driver.wrapExec(driver.queueWorkerCommand(worker, ctx)),
@@ -5415,7 +5831,7 @@ function buildSiteServicesScript(options) {
5415
5831
  for (let p = 0;p < processes; p++) {
5416
5832
  const name = `${daemonUnitName(slug, siteName, daemon, dIndex)}-${p}`;
5417
5833
  desiredUnits.push(name);
5418
- out.push(...writeUnitScript(name, systemdUnit({
5834
+ out.push(...writeUnitScript(name, systemdUnit2({
5419
5835
  description: `${siteName} daemon ${daemon.name || daemon.command} (managed by ts-cloud)`,
5420
5836
  workingDir: daemon.directory || current,
5421
5837
  execStart: driver.wrapExec(daemon.command),
@@ -5430,7 +5846,7 @@ function buildSiteServicesScript(options) {
5430
5846
  const schedulerUnit = `${slug}-${siteName}-scheduler`;
5431
5847
  if (schedulerEnabled && driver.schedulerMode === "daemon") {
5432
5848
  desiredUnits.push(schedulerUnit);
5433
- out.push(...writeUnitScript(schedulerUnit, systemdUnit({
5849
+ out.push(...writeUnitScript(schedulerUnit, systemdUnit2({
5434
5850
  description: `${siteName} scheduler (managed by ts-cloud)`,
5435
5851
  workingDir: current,
5436
5852
  execStart: driver.wrapExec(driver.schedulerCommand(ctx)),