dotbrain 0.3.4__py3-none-any.whl

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 (40) hide show
  1. dotbrain/__init__.py +7 -0
  2. dotbrain/_cli_reference.py +115 -0
  3. dotbrain/adopter_repos.py +573 -0
  4. dotbrain/beads.py +511 -0
  5. dotbrain/bootstrap.py +199 -0
  6. dotbrain/brainspaces.py +238 -0
  7. dotbrain/cli.py +660 -0
  8. dotbrain/config.py +428 -0
  9. dotbrain/doctor.py +279 -0
  10. dotbrain/hooks.py +84 -0
  11. dotbrain/migrate.py +304 -0
  12. dotbrain/paths.py +139 -0
  13. dotbrain/resource_loader.py +47 -0
  14. dotbrain/resources/__init__.py +1 -0
  15. dotbrain/resources/agents/claude/implementer.md +47 -0
  16. dotbrain/resources/agents/claude/investigator.md +35 -0
  17. dotbrain/resources/agents/claude/reviewer.md +38 -0
  18. dotbrain/resources/agents/claude/verifier.md +35 -0
  19. dotbrain/resources/agents/codex/implementer.toml +26 -0
  20. dotbrain/resources/agents/codex/investigator.toml +21 -0
  21. dotbrain/resources/agents/codex/reviewer.toml +27 -0
  22. dotbrain/resources/agents/codex/verifier.toml +21 -0
  23. dotbrain/resources/config.yaml +20 -0
  24. dotbrain/resources/core.yaml +18 -0
  25. dotbrain/resources/templates/brain/AGENTS.md +9 -0
  26. dotbrain/resources/templates/brain/DOTBRAIN.md +105 -0
  27. dotbrain/resources/templates/brain/adr/README.md +8 -0
  28. dotbrain/resources/templates/brain/designs/README.md +28 -0
  29. dotbrain/resources/templates/brain/docs/README.md +9 -0
  30. dotbrain/resources/templates/brain/project.yaml +27 -0
  31. dotbrain/resources/templates/gitignore +17 -0
  32. dotbrain/skills.py +264 -0
  33. dotbrain/subagents.py +252 -0
  34. dotbrain/updater.py +105 -0
  35. dotbrain/workflows.py +529 -0
  36. dotbrain-0.3.4.dist-info/METADATA +21 -0
  37. dotbrain-0.3.4.dist-info/RECORD +40 -0
  38. dotbrain-0.3.4.dist-info/WHEEL +4 -0
  39. dotbrain-0.3.4.dist-info/entry_points.txt +2 -0
  40. dotbrain-0.3.4.dist-info/licenses/LICENSE +21 -0
dotbrain/beads.py ADDED
@@ -0,0 +1,511 @@
1
+ """Beads tracker setup, hydration, and admin.
2
+
3
+ Everything that drives the ``bd`` CLI and the ``.beads`` workspace, in three bands:
4
+
5
+ - **init**: ``bd init --stealth`` in a Brainspace, server-mode metadata
6
+ (write/normalize/attach), and the project-#0 root-``.beads`` hijack guard.
7
+ - **load/hydrate**: bring a Brainspace's ``.beads`` into existence from ``config.yaml`` and pull
8
+ remote state, plus the dry-run preview.
9
+ - **remote admin**: list/drop databases on the shared Dolt sql-server.
10
+
11
+ The embedded-to-server migration workflow stays in ``migrate.py`` (it composes these helpers).
12
+ This module depends only on ``config`` and ``paths``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import shlex
21
+ import shutil
22
+ import subprocess
23
+ from collections.abc import Callable, Sequence
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+
27
+ from dotbrain import config, paths
28
+
29
+ # A subprocess seam: same shape as ``subprocess.run`` but easy to fake in tests.
30
+ Runner = Callable[..., "subprocess.CompletedProcess[str]"]
31
+
32
+ _DB_NAME_RE = re.compile(r"^[A-Za-z0-9_-]+$")
33
+ _PROTECTED_DATABASES = {"dotbrain"}
34
+
35
+
36
+ @dataclass
37
+ class BootstrapResult:
38
+ wired: list[str] = field(default_factory=list)
39
+ pulled: list[str] = field(default_factory=list)
40
+ logs: list[str] = field(default_factory=list)
41
+ warnings: list[str] = field(default_factory=list)
42
+
43
+
44
+ def _default_run(
45
+ argv: Sequence[str], *, cwd: Path | None = None, env: dict | None = None, check: bool = True
46
+ ) -> "subprocess.CompletedProcess[str]":
47
+ # stdin=DEVNULL is load-bearing: bd auto-enables non-interactive mode on a non-TTY stdin,
48
+ # so destructive steps (e.g. bd init --reinit-local) skip their confirmation prompt instead
49
+ # of blocking forever on terminal input while capture_output swallows the prompt text.
50
+ return subprocess.run(
51
+ list(argv), cwd=cwd, env=env, check=check,
52
+ capture_output=True, encoding="utf-8", stdin=subprocess.DEVNULL,
53
+ )
54
+
55
+
56
+ # --------------------------------------------------------------------------- init & server metadata
57
+
58
+
59
+ def write_server_beads_metadata(
60
+ beads_dir: Path, *, host: str, port: str, user: str, database: str
61
+ ) -> None:
62
+ """Write the server-mode ``.beads/metadata.json`` + ``dolt-server.port`` (shape).
63
+
64
+ Also normalizes the ``.beads`` dir to ``0700`` — bd expects a private beads dir and warns
65
+ otherwise; this matches what ``bd init`` creates.
66
+ """
67
+ beads_dir = Path(beads_dir)
68
+ beads_dir.chmod(0o700)
69
+ (beads_dir / "metadata.json").write_text(
70
+ json.dumps({
71
+ "database": "dolt",
72
+ "backend": "dolt",
73
+ "dolt_mode": "server",
74
+ "dolt_server_host": host,
75
+ "dolt_server_user": user,
76
+ "dolt_database": database,
77
+ }, indent=2) + "\n",
78
+ encoding="utf-8",
79
+ newline="\n",
80
+ )
81
+ (beads_dir / "dolt-server.port").write_text(
82
+ f"{port}\n", encoding="utf-8", newline="\n"
83
+ )
84
+
85
+
86
+ def normalize_server_beads_metadata(beads_dir: Path, port: str) -> None:
87
+ """Make ``.beads/dolt-server.port`` the source of truth for the port and drop the deprecated
88
+ ``dolt_server_port`` key from metadata.json (bd warns it can leak data across projects). Edits
89
+ metadata in place so identity fields like ``project_id`` survive."""
90
+ beads_dir = Path(beads_dir)
91
+ meta_file = beads_dir / "metadata.json"
92
+ if meta_file.is_file():
93
+ data = json.loads(meta_file.read_text(encoding="utf-8"))
94
+ if data.pop("dolt_server_port", None) is not None:
95
+ meta_file.write_text(
96
+ json.dumps(data, indent=2) + "\n",
97
+ encoding="utf-8",
98
+ newline="\n",
99
+ )
100
+ (beads_dir / "dolt-server.port").write_text(
101
+ f"{port}\n", encoding="utf-8", newline="\n"
102
+ )
103
+
104
+
105
+ def _is_server_db_exists_error(exc: subprocess.CalledProcessError) -> bool:
106
+ """True when ``bd init`` failed only because the server database already exists."""
107
+ return "database exists" in (exc.stderr or "").lower()
108
+
109
+
110
+ def attach_existing_server_beads(
111
+ brainspace: Path,
112
+ project: str,
113
+ *,
114
+ host: str,
115
+ port: str,
116
+ user: str,
117
+ database: str,
118
+ run: Runner = _default_run,
119
+ ) -> str:
120
+ """Attach to an already-existing server beads DB via metadata hydration.
121
+
122
+ When ``bd init`` reports the server database already exists, a create is wrong: write the
123
+ server-mode ``.beads`` scaffolding (metadata.json + port) and verify the
124
+ connection instead. Any partial ``.beads`` left by the failed ``bd init`` is overwritten — the
125
+ data lives on the server, so nothing local is at risk.
126
+
127
+ Verification is ``bd dolt test`` (the server-mode check), not ``bd dolt pull``: in server mode no
128
+ Dolt remote is configured, so a pull is a no-op, whereas ``test`` actually confirms the attach
129
+ reaches the server and fails cleanly when it can't.
130
+ """
131
+ beads = Path(brainspace) / ".beads"
132
+ beads.mkdir(parents=True, exist_ok=True)
133
+ write_server_beads_metadata(beads, host=host, port=port, user=user, database=database)
134
+ run(["bd", "dolt", "test"], cwd=brainspace, env=_beads_env(brainspace), check=True)
135
+ return f"attached to existing server beads DB {host}:{port}/{database} (hydrated metadata)"
136
+
137
+
138
+ def init_beads(
139
+ brainspace: Path,
140
+ project: str,
141
+ dotbrain_home: Path,
142
+ *,
143
+ run_beads: bool = True,
144
+ remote: str = "",
145
+ server_host: str = "",
146
+ server_port: str = "3307",
147
+ server_user: str = "beads",
148
+ database: str = "",
149
+ run: Runner = _default_run,
150
+ ) -> str | None:
151
+ """Run ``bd init --stealth`` in the Brainspace. Returns a log line or None.
152
+
153
+ bd runs in stealth mode so it never commits into the dotbrain monorepo; dotbrain owns all git
154
+ writes here. ``_hide_root_beads`` / ``_restore_root_beads`` still guard bd's repo-root ``.beads``
155
+ workspace hijack, which is workspace resolution, not a commit.
156
+
157
+ In server mode, an existing server database makes ``bd init`` fail with a create error; that
158
+ case falls back to attaching via metadata hydration rather than a destructive
159
+ re-init. Any other ``bd init`` failure surfaces as a clean ``RuntimeError`` carrying bd's stderr.
160
+ """
161
+ brainspace = Path(brainspace)
162
+ dotbrain_home = Path(dotbrain_home)
163
+ if not run_beads or (brainspace / ".beads").is_dir():
164
+ return None
165
+ if remote and server_host:
166
+ raise ValueError("--beads-remote and --beads-server-host are mutually exclusive")
167
+
168
+ hidden = _hide_root_beads(brainspace, dotbrain_home)
169
+ try:
170
+ try:
171
+ run(_bd_init_args(project, remote, server_host, server_port,
172
+ server_user, database),
173
+ cwd=brainspace, env=_beads_env(brainspace), check=True)
174
+ except subprocess.CalledProcessError as exc:
175
+ if server_host and _is_server_db_exists_error(exc):
176
+ return attach_existing_server_beads(
177
+ brainspace, project,
178
+ host=server_host, port=server_port,
179
+ user=server_user, database=database or project,
180
+ run=run,
181
+ )
182
+ stderr = (exc.stderr or "").strip()
183
+ raise RuntimeError(f"bd init failed: {stderr}" if stderr else "bd init failed") from exc
184
+ finally:
185
+ _restore_root_beads(hidden, dotbrain_home)
186
+
187
+ if server_host:
188
+ _configure_dolt_server(brainspace, server_host, server_port, server_user,
189
+ database or project, run)
190
+ normalize_server_beads_metadata(brainspace / ".beads", server_port)
191
+ return None
192
+
193
+
194
+ def _bd_init_args(
195
+ project: str,
196
+ remote: str,
197
+ server_host: str,
198
+ server_port: str,
199
+ server_user: str,
200
+ database: str,
201
+ *,
202
+ reinit_local: bool = False,
203
+ destroy_token: str | None = None,
204
+ ) -> list[str]:
205
+ # --stealth: bd must never git-commit into the dotbrain monorepo. It suppresses the
206
+ # "bd init: initialize beads issue tracking" commit and sets no-git-ops for bd's other git ops.
207
+ args = ["bd", "init", "--stealth", "--prefix", project,
208
+ "--skip-agents", "--skip-hooks", "--non-interactive"]
209
+ if server_host:
210
+ args += [
211
+ "--server", "--external",
212
+ "--server-host", server_host,
213
+ "--server-port", server_port,
214
+ "--server-user", server_user,
215
+ "--database", database or project,
216
+ ]
217
+ if reinit_local:
218
+ args += ["--reinit-local"]
219
+ if destroy_token:
220
+ args += ["--destroy-token", destroy_token]
221
+ args += ["--remote", remote]
222
+ return args
223
+
224
+
225
+ def _beads_env(brainspace: Path) -> dict:
226
+ return {**os.environ, "BEADS_DIR": str(Path(brainspace) / ".beads"), "BD_NON_INTERACTIVE": "1"}
227
+
228
+
229
+ def _hide_root_beads(brainspace: Path, dotbrain_home: Path) -> Path | None:
230
+ """bd init runs in the Brainspace but git sees dotbrain; hide the root .beads symlink first."""
231
+ root_beads = Path(dotbrain_home) / ".beads"
232
+ if brainspace == paths.brainspace(dotbrain_home, "dotbrain"):
233
+ return None
234
+ if not root_beads.is_symlink():
235
+ return None
236
+ hidden = Path(dotbrain_home) / f".beads.wire-project.{os.getpid()}"
237
+ root_beads.rename(hidden)
238
+ return hidden
239
+
240
+
241
+ def _restore_root_beads(hidden: Path | None, dotbrain_home: Path) -> None:
242
+ """Always put project #0's repo-root .beads symlink back.
243
+
244
+ bd init runs with the git top-level still resolving to dotbrain, so it may recreate a .beads at
245
+ the repo root pointing at the *wired* project. Remove whatever it left and restore the original
246
+ target unconditionally — a stale ``not root_beads.exists()`` guard here is what let the wired
247
+ project hijack project #0's tracker.
248
+
249
+ Uses ``os.path.lexists`` rather than ``Path.exists()``: the latter follows the symlink and
250
+ checks the *target*, and on Windows that check can spuriously fail for a relative-target
251
+ symlink resolved through a renamed path even when the symlink itself and its target are both
252
+ fine — the wrong question for "did we actually hide something to restore"."""
253
+ if hidden is None or not os.path.lexists(hidden):
254
+ return
255
+ root_beads = Path(dotbrain_home) / ".beads"
256
+ if root_beads.is_symlink() or root_beads.is_file():
257
+ root_beads.unlink()
258
+ elif root_beads.is_dir():
259
+ shutil.rmtree(root_beads)
260
+ hidden.rename(root_beads)
261
+
262
+
263
+ def _configure_dolt_server(
264
+ brainspace: Path, host: str, port: str, user: str, database: str, run: Runner
265
+ ) -> None:
266
+ env = _beads_env(brainspace)
267
+ for key, value in (("host", host), ("port", port), ("user", user), ("database", database)):
268
+ run(["bd", "dolt", "set", key, value], cwd=brainspace, env=env, check=True)
269
+
270
+
271
+ # --------------------------------------------------------------------------- load / hydrate
272
+
273
+
274
+ def ensure_server_beads_metadata(
275
+ repo: Path,
276
+ name: str,
277
+ *,
278
+ server_host: str = "",
279
+ server_port: str = "3307",
280
+ server_user: str = "beads",
281
+ database: str = "",
282
+ run: Callable[..., subprocess.CompletedProcess] = subprocess.run,
283
+ ) -> str | None:
284
+ """Hydrate server-mode ``metadata.json`` from config.yaml defaults, if needed.
285
+
286
+ Creates the ``.beads`` directory when absent: it is never git-tracked, so on a
287
+ fresh clone hydration is what brings it into existence.
288
+ """
289
+ metadata = repo / ".beads" / "metadata.json"
290
+ port_file = repo / ".beads" / "dolt-server.port"
291
+ if metadata.is_file() or not server_host:
292
+ return None
293
+
294
+ database = database or name
295
+ port = server_port or "3307"
296
+ user = server_user or "beads"
297
+
298
+ (repo / ".beads").mkdir(mode=0o700, parents=True, exist_ok=True)
299
+ write_server_beads_metadata(
300
+ repo / ".beads", host=server_host, port=port, user=user, database=database
301
+ )
302
+ try:
303
+ run(["bd", "-C", str(repo), "dolt", "test"], check=True)
304
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
305
+ metadata.unlink(missing_ok=True)
306
+ port_file.unlink(missing_ok=True)
307
+ raise
308
+ return f"hydrated server beads metadata for {name} at {server_host}:{port}/{database}"
309
+
310
+
311
+ def ensure_embedded_beads(
312
+ brainspace: Path,
313
+ dotbrain_home: Path,
314
+ *,
315
+ remote: str = "",
316
+ run: Runner = _default_run,
317
+ ) -> tuple[str | None, str | None]:
318
+ """Hydrate a declared-embedded Brainspace via ``bd init --stealth``. Returns (log, warning).
319
+
320
+ With a declared remote the tracker is cloned from it; without one only an empty tracker can
321
+ be created, since embedded data was never recoverable from git.
322
+ """
323
+ if (brainspace / ".beads").is_dir():
324
+ return None, None
325
+ init_beads(brainspace, brainspace.name, dotbrain_home, remote=remote, run=run)
326
+ if remote:
327
+ return f"hydrated embedded beads for {brainspace.name} from {remote}", None
328
+ return (
329
+ f"initialized empty embedded beads for {brainspace.name}",
330
+ f"{brainspace.name}: declared embedded with no remote; tracker starts empty",
331
+ )
332
+
333
+
334
+ def _preview_load(brainspace: Path, beads_cfg, cfg) -> list[str]:
335
+ """Pure dry-run preview of what :func:`pull_beads_for_all` would do for one Brainspace.
336
+
337
+ Mirrors the live branch selection (embedded vs server, already-hydrated skip) without touching
338
+ the filesystem or invoking ``bd``, then always notes the pull. Keeping this free of mutators is
339
+ what makes ``--dry-run`` provably side-effect-free (unwire-dry-run lesson, 9cfc44f).
340
+ """
341
+ name = brainspace.name
342
+ lines: list[str] = []
343
+ if beads_cfg.mode == "embedded":
344
+ if not (brainspace / ".beads").is_dir():
345
+ if beads_cfg.remote:
346
+ lines.append(f"would hydrate embedded beads for {name} from {beads_cfg.remote}")
347
+ else:
348
+ lines.append(f"would initialize empty embedded beads for {name}")
349
+ elif not (brainspace / ".beads" / "metadata.json").is_file() and cfg.beads_server.host:
350
+ database = beads_cfg.database or name
351
+ port = cfg.beads_server.port or "3307"
352
+ lines.append(
353
+ f"would hydrate server beads metadata for {name} "
354
+ f"at {cfg.beads_server.host}:{port}/{database}"
355
+ )
356
+ lines.append(f"would pull beads for {name}")
357
+ return lines
358
+
359
+
360
+ def pull_beads_for_all(
361
+ dotbrain_home: Path,
362
+ run: Runner = _default_run,
363
+ bd_timeout: int = 20,
364
+ *,
365
+ projects: Sequence[str] | None = None,
366
+ dry_run: bool = False,
367
+ ) -> BootstrapResult:
368
+ """Hydrate and pull beads state for Brainspaces declared to use beads.
369
+
370
+ Drives off the resolved config.yaml config, not an existing ``.beads`` directory: brainspace
371
+ roots' ``.beads`` are never git-tracked, so on a fresh clone hydration creates them. Targets
372
+ each Brainspace directly, so repo-less projects are hydrated too.
373
+
374
+ ``projects`` restricts the run to the named Brainspaces (``None`` = all); a requested name
375
+ with no Brainspace yields a warning. ``dry_run`` only previews via :func:`_preview_load`,
376
+ reaching no filesystem or ``bd`` write by construction.
377
+ """
378
+ dotbrain_home = Path(dotbrain_home).resolve()
379
+ result = BootstrapResult()
380
+ cfg = config.load_config(dotbrain_home)
381
+
382
+ if not shutil.which("bd"):
383
+ result.warnings.append("bd is not installed; skipping beads pulls")
384
+ return result
385
+
386
+ brainspaces = paths.brainspaces(dotbrain_home)
387
+ if projects is not None:
388
+ by_name = {c.name: c for c in brainspaces}
389
+ brainspaces = []
390
+ for name in projects:
391
+ brainspace = by_name.get(name)
392
+ if brainspace is None:
393
+ result.warnings.append(f"no Brainspace: {paths.data_dir(dotbrain_home).name}/{name}")
394
+ continue
395
+ brainspaces.append(brainspace)
396
+
397
+ for brainspace in brainspaces:
398
+ beads_cfg = config.load_project_config(dotbrain_home, brainspace.name)
399
+ if beads_cfg.mode == "none":
400
+ continue
401
+
402
+ if dry_run:
403
+ result.logs += _preview_load(brainspace, beads_cfg, cfg)
404
+ continue
405
+
406
+ try:
407
+ if beads_cfg.mode == "embedded":
408
+ log, warning = ensure_embedded_beads(
409
+ brainspace, dotbrain_home, remote=beads_cfg.remote, run=run
410
+ )
411
+ if warning:
412
+ result.warnings.append(warning)
413
+ else:
414
+ log = ensure_server_beads_metadata(
415
+ brainspace,
416
+ brainspace.name,
417
+ server_host=cfg.beads_server.host,
418
+ server_port=cfg.beads_server.port,
419
+ server_user=cfg.beads_server.user,
420
+ database=beads_cfg.database,
421
+ run=run,
422
+ )
423
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
424
+ OSError, RuntimeError) as exc:
425
+ result.warnings.append(f"failed to hydrate beads metadata for {brainspace}: {exc}")
426
+ else:
427
+ if log:
428
+ result.logs.append(log)
429
+
430
+ if beads_cfg.mode == "embedded":
431
+ try:
432
+ subprocess.run(
433
+ ["bd", "-C", str(brainspace), "dolt", "pull"],
434
+ check=True, capture_output=True, encoding="utf-8", timeout=bd_timeout,
435
+ )
436
+ result.pulled.append(str(brainspace))
437
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
438
+ result.warnings.append(f"bd dolt pull failed for {brainspace}")
439
+
440
+ return result
441
+
442
+
443
+ # --------------------------------------------------------------------------- remote admin
444
+
445
+
446
+ def _mysql_argv(
447
+ query: str, *, server_host: str, server_port: str, server_user: str, ssh_host: str
448
+ ) -> tuple[list[str], str]:
449
+ """Build the argv to run ``query`` against the sql-server with the mysql client, optionally
450
+ over an ssh hop. Returns (argv, human-readable target). A bare ``dolt sql`` against the data
451
+ dir fails when a server holds it (client mode dials 127.0.0.1), so we use the mysql client."""
452
+ if not server_host:
453
+ raise ValueError("a Dolt sql-server host is required")
454
+ mysql_argv = [
455
+ "mysql", "--host", server_host, "--port", str(server_port),
456
+ "-u", server_user, "-e", query,
457
+ ]
458
+ if ssh_host:
459
+ return ["ssh", ssh_host, " ".join(shlex.quote(a) for a in mysql_argv)], \
460
+ f"{ssh_host} -> {server_host}:{server_port}"
461
+ return mysql_argv, f"{server_host}:{server_port}"
462
+
463
+
464
+ def drop_remote_beads_database(
465
+ project: str,
466
+ *,
467
+ database: str = "",
468
+ server_host: str,
469
+ server_port: str = "3307",
470
+ server_user: str = "beads",
471
+ ssh_host: str = "",
472
+ dry_run: bool = False,
473
+ run: Runner = _default_run,
474
+ ) -> str:
475
+ """Drop a project's beads database on the shared Dolt sql-server (server backend only).
476
+
477
+ ``ssh_host`` is the optional hop that can reach the server; empty means connect directly.
478
+ """
479
+ db = database or project
480
+ if not _DB_NAME_RE.fullmatch(db):
481
+ raise ValueError(f"unsafe beads database name: {db!r}")
482
+ if db in _PROTECTED_DATABASES:
483
+ raise ValueError(f"refusing to drop protected beads database: {db}")
484
+
485
+ argv, via = _mysql_argv(
486
+ f"DROP DATABASE IF EXISTS `{db}`;",
487
+ server_host=server_host, server_port=server_port, server_user=server_user, ssh_host=ssh_host,
488
+ )
489
+ if dry_run:
490
+ return f"would drop remote beads database {db} via {via}"
491
+ run(argv, check=True)
492
+ return f"dropped remote beads database: {db}"
493
+
494
+
495
+ def list_remote_beads_databases(
496
+ *,
497
+ server_host: str,
498
+ server_port: str = "3307",
499
+ server_user: str = "beads",
500
+ ssh_host: str = "",
501
+ run: Runner = _default_run,
502
+ ) -> list[str]:
503
+ """Return the database names on the shared Dolt sql-server (the `SHOW DATABASES` rows)."""
504
+ argv, _ = _mysql_argv(
505
+ "SHOW DATABASES;",
506
+ server_host=server_host, server_port=server_port, server_user=server_user, ssh_host=ssh_host,
507
+ )
508
+ out = run(argv, check=True)
509
+ lines = (out.stdout or "").splitlines()
510
+ # mysql prints a "Database" header row; drop it.
511
+ return [l.strip() for l in lines[1:] if l.strip()]