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.
- dotbrain/__init__.py +7 -0
- dotbrain/_cli_reference.py +115 -0
- dotbrain/adopter_repos.py +573 -0
- dotbrain/beads.py +511 -0
- dotbrain/bootstrap.py +199 -0
- dotbrain/brainspaces.py +238 -0
- dotbrain/cli.py +660 -0
- dotbrain/config.py +428 -0
- dotbrain/doctor.py +279 -0
- dotbrain/hooks.py +84 -0
- dotbrain/migrate.py +304 -0
- dotbrain/paths.py +139 -0
- dotbrain/resource_loader.py +47 -0
- dotbrain/resources/__init__.py +1 -0
- dotbrain/resources/agents/claude/implementer.md +47 -0
- dotbrain/resources/agents/claude/investigator.md +35 -0
- dotbrain/resources/agents/claude/reviewer.md +38 -0
- dotbrain/resources/agents/claude/verifier.md +35 -0
- dotbrain/resources/agents/codex/implementer.toml +26 -0
- dotbrain/resources/agents/codex/investigator.toml +21 -0
- dotbrain/resources/agents/codex/reviewer.toml +27 -0
- dotbrain/resources/agents/codex/verifier.toml +21 -0
- dotbrain/resources/config.yaml +20 -0
- dotbrain/resources/core.yaml +18 -0
- dotbrain/resources/templates/brain/AGENTS.md +9 -0
- dotbrain/resources/templates/brain/DOTBRAIN.md +105 -0
- dotbrain/resources/templates/brain/adr/README.md +8 -0
- dotbrain/resources/templates/brain/designs/README.md +28 -0
- dotbrain/resources/templates/brain/docs/README.md +9 -0
- dotbrain/resources/templates/brain/project.yaml +27 -0
- dotbrain/resources/templates/gitignore +17 -0
- dotbrain/skills.py +264 -0
- dotbrain/subagents.py +252 -0
- dotbrain/updater.py +105 -0
- dotbrain/workflows.py +529 -0
- dotbrain-0.3.4.dist-info/METADATA +21 -0
- dotbrain-0.3.4.dist-info/RECORD +40 -0
- dotbrain-0.3.4.dist-info/WHEEL +4 -0
- dotbrain-0.3.4.dist-info/entry_points.txt +2 -0
- dotbrain-0.3.4.dist-info/licenses/LICENSE +21 -0
dotbrain/config.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Config store: global ``config.yaml`` + per-project ``.brain/project.yaml``.
|
|
2
|
+
|
|
3
|
+
``config.yaml`` (data root, version 3) holds global infrastructure defaults
|
|
4
|
+
(``beads.server``). ``brainspaces/<name>/.brain/project.yaml`` (version 1) holds
|
|
5
|
+
per-project identity (beads mode, database, remote). The old
|
|
6
|
+
``dotbrain.yaml`` is read transparently as a fallback when ``config.yaml``
|
|
7
|
+
is absent.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from dotbrain import paths
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Data model
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BeadsServer:
|
|
26
|
+
"""Shared beads sql-server defaults from ``beads.server`` in config.yaml."""
|
|
27
|
+
host: str = ""
|
|
28
|
+
port: str = "3307"
|
|
29
|
+
user: str = "beads"
|
|
30
|
+
ssh_host: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ProjectBeads:
|
|
35
|
+
"""Effective per-project beads config from ``.brain/project.yaml``."""
|
|
36
|
+
mode: str = "embedded" # embedded | server | none
|
|
37
|
+
remote: str = ""
|
|
38
|
+
database: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DEFAULT_PROJECT_AGENTS: tuple[str, ...] = ("claude", "codex")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DotbrainConfig:
|
|
46
|
+
"""Parsed representation of config.yaml (schema v3)."""
|
|
47
|
+
version: int = 3
|
|
48
|
+
beads_server: BeadsServer = field(default_factory=BeadsServer)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Global config (config.yaml)
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _config_path(dotbrain_home: Path) -> Path:
|
|
56
|
+
return Path(dotbrain_home) / "config.yaml"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _old_config_path(dotbrain_home: Path) -> Path:
|
|
60
|
+
return Path(dotbrain_home) / "dotbrain.yaml"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_config(dotbrain_home: Path) -> DotbrainConfig:
|
|
64
|
+
"""Load ``config.yaml``; transparently reads old ``dotbrain.yaml`` as fallback."""
|
|
65
|
+
import yaml # deferred: only needed when this function is called
|
|
66
|
+
|
|
67
|
+
path = _config_path(dotbrain_home)
|
|
68
|
+
if not path.is_file():
|
|
69
|
+
old = _old_config_path(dotbrain_home)
|
|
70
|
+
if old.is_file():
|
|
71
|
+
return _parse_old_format(old)
|
|
72
|
+
|
|
73
|
+
if not path.is_file():
|
|
74
|
+
return DotbrainConfig()
|
|
75
|
+
|
|
76
|
+
data: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
77
|
+
server = (data.get("beads") or {}).get("server") or {}
|
|
78
|
+
return DotbrainConfig(
|
|
79
|
+
version=int(data.get("version", 3)),
|
|
80
|
+
beads_server=BeadsServer(
|
|
81
|
+
host=str(server.get("host", "")),
|
|
82
|
+
port=str(server.get("port", "3307")),
|
|
83
|
+
user=str(server.get("user", "beads")),
|
|
84
|
+
ssh_host=str(server.get("ssh_host", "")),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_old_format(path: Path) -> DotbrainConfig:
|
|
90
|
+
"""Read beads.server from a legacy dotbrain.yaml, ignoring projects: section."""
|
|
91
|
+
import yaml
|
|
92
|
+
|
|
93
|
+
data: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
94
|
+
server = (data.get("beads") or {}).get("server") or {}
|
|
95
|
+
return DotbrainConfig(
|
|
96
|
+
version=3,
|
|
97
|
+
beads_server=BeadsServer(
|
|
98
|
+
host=str(server.get("host", "")),
|
|
99
|
+
port=str(server.get("port", "3307")),
|
|
100
|
+
user=str(server.get("user", "beads")),
|
|
101
|
+
ssh_host=str(server.get("ssh_host", "")),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Per-project config (<data-dir>/<name>/.brain/project.yaml)
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def _project_config_path(dotbrain_home: Path, name: str) -> Path:
|
|
111
|
+
"""Canonical ``.brain/project.yaml`` path."""
|
|
112
|
+
return paths.brainspace(dotbrain_home, name) / ".brain" / "project.yaml"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def default_beads_mode(dotbrain_home: Path) -> str:
|
|
116
|
+
"""Effective default beads mode for a project that doesn't set one.
|
|
117
|
+
|
|
118
|
+
Follows infrastructure: a configured shared sql-server (``beads.server.host``
|
|
119
|
+
in config.yaml) makes ``server`` the default, so every wired project uses it;
|
|
120
|
+
otherwise the default is ``embedded``. Projects override per-project by
|
|
121
|
+
setting ``mode`` explicitly in ``.brain/project.yaml``.
|
|
122
|
+
"""
|
|
123
|
+
return "server" if load_config(dotbrain_home).beads_server.host else "embedded"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_project_config(dotbrain_home: Path, name: str) -> ProjectBeads:
|
|
127
|
+
"""Read ``brainspaces/<name>/.brain/project.yaml``, resolving defaults.
|
|
128
|
+
|
|
129
|
+
The default mode follows :func:`default_beads_mode` (server when a shared
|
|
130
|
+
server is configured, else embedded); database defaults to the project name.
|
|
131
|
+
Old ``dotbrain.yaml`` ``projects:<name>`` entries take precedence over a
|
|
132
|
+
default (seeded) ``.brain/project.yaml``.
|
|
133
|
+
"""
|
|
134
|
+
import yaml
|
|
135
|
+
|
|
136
|
+
default_mode = default_beads_mode(dotbrain_home)
|
|
137
|
+
|
|
138
|
+
# Read .brain/project.yaml if it exists.
|
|
139
|
+
file_beads: ProjectBeads | None = None
|
|
140
|
+
path = _project_config_path(dotbrain_home, name)
|
|
141
|
+
if path.is_file():
|
|
142
|
+
data: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
143
|
+
beads = data.get("beads") or {}
|
|
144
|
+
file_beads = ProjectBeads(
|
|
145
|
+
mode=str(beads.get("mode", default_mode)),
|
|
146
|
+
remote=str(beads.get("remote", "")),
|
|
147
|
+
database=str(beads.get("database", name)),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Check old dotbrain.yaml — its explicit entries override a default .brain/project.yaml.
|
|
151
|
+
old = _old_config_path(dotbrain_home)
|
|
152
|
+
if old.is_file():
|
|
153
|
+
data: dict[str, Any] = yaml.safe_load(old.read_text(encoding="utf-8")) or {}
|
|
154
|
+
projects = data.get("projects") or {}
|
|
155
|
+
if name in projects:
|
|
156
|
+
entry = (projects[name] or {}).get("beads") or {}
|
|
157
|
+
old_beads = ProjectBeads(
|
|
158
|
+
mode=str(entry.get("mode", default_mode)),
|
|
159
|
+
remote=str(entry.get("remote", "")),
|
|
160
|
+
database=str(entry.get("database", name)),
|
|
161
|
+
)
|
|
162
|
+
# If .brain/project.yaml exists and deviates from defaults, it wins.
|
|
163
|
+
# If .brain/project.yaml is all defaults (seeded template), old format wins.
|
|
164
|
+
if file_beads is not None and _is_beads_deviation(dotbrain_home, name, file_beads):
|
|
165
|
+
return file_beads
|
|
166
|
+
return old_beads
|
|
167
|
+
|
|
168
|
+
if file_beads is not None:
|
|
169
|
+
return file_beads
|
|
170
|
+
return ProjectBeads(mode=default_mode, database=name)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_project_skills(dotbrain_home: Path, name: str) -> tuple[str, ...]:
|
|
174
|
+
"""Read the operator's per-project skill list from ``.brain/project.yaml`` ``skills:``.
|
|
175
|
+
"""
|
|
176
|
+
import yaml
|
|
177
|
+
|
|
178
|
+
from dotbrain import skills
|
|
179
|
+
|
|
180
|
+
path = _project_config_path(dotbrain_home, name)
|
|
181
|
+
if not path.is_file():
|
|
182
|
+
return ()
|
|
183
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
184
|
+
if not isinstance(data, dict):
|
|
185
|
+
return ()
|
|
186
|
+
return skills._clean(data.get("skills"))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def load_project_subagents(dotbrain_home: Path, name: str) -> tuple[str, ...]:
|
|
190
|
+
"""Read per-project subagent declarations from ``.brain/project.yaml``."""
|
|
191
|
+
import yaml
|
|
192
|
+
|
|
193
|
+
from dotbrain import skills
|
|
194
|
+
|
|
195
|
+
path = _project_config_path(dotbrain_home, name)
|
|
196
|
+
if not path.is_file():
|
|
197
|
+
return ()
|
|
198
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
199
|
+
if not isinstance(data, dict):
|
|
200
|
+
return ()
|
|
201
|
+
return skills._clean(data.get("subagents"))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def load_project_agents(dotbrain_home: Path, name: str) -> tuple[str, ...]:
|
|
205
|
+
"""Read declared agent workspaces from ``brainspaces/<name>/.brain/project.yaml``.
|
|
206
|
+
|
|
207
|
+
Missing ``agents`` preserves legacy behavior by enabling both packaged agent
|
|
208
|
+
workspaces. An explicit empty list disables agent workspace seeding.
|
|
209
|
+
"""
|
|
210
|
+
import yaml
|
|
211
|
+
|
|
212
|
+
path = _project_config_path(dotbrain_home, name)
|
|
213
|
+
if not path.is_file():
|
|
214
|
+
return DEFAULT_PROJECT_AGENTS
|
|
215
|
+
|
|
216
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
217
|
+
if not isinstance(data, dict):
|
|
218
|
+
return DEFAULT_PROJECT_AGENTS
|
|
219
|
+
|
|
220
|
+
raw_agents = data.get("agents")
|
|
221
|
+
if raw_agents is None:
|
|
222
|
+
return DEFAULT_PROJECT_AGENTS
|
|
223
|
+
|
|
224
|
+
entries = raw_agents if isinstance(raw_agents, list) else [raw_agents]
|
|
225
|
+
cleaned: list[str] = []
|
|
226
|
+
seen: set[str] = set()
|
|
227
|
+
for entry in entries:
|
|
228
|
+
agent = str(entry).strip().lower()
|
|
229
|
+
if not agent or agent in seen:
|
|
230
|
+
continue
|
|
231
|
+
seen.add(agent)
|
|
232
|
+
cleaned.append(agent)
|
|
233
|
+
return tuple(cleaned)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def write_project_config(dotbrain_home: Path, name: str, beads: ProjectBeads) -> str | None:
|
|
237
|
+
"""Write ``brainspaces/<name>/.brain/project.yaml``. Returns a log line or None if unchanged."""
|
|
238
|
+
import yaml
|
|
239
|
+
|
|
240
|
+
path = _project_config_path(dotbrain_home, name)
|
|
241
|
+
existing = load_project_config(dotbrain_home, name)
|
|
242
|
+
|
|
243
|
+
if (existing.mode == beads.mode
|
|
244
|
+
and existing.remote == beads.remote
|
|
245
|
+
and existing.database == (beads.database or name)):
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
resolved = ProjectBeads(
|
|
249
|
+
mode=beads.mode,
|
|
250
|
+
remote=beads.remote,
|
|
251
|
+
database=beads.database if beads.database != name else "",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
doc: dict[str, Any] = {"beads": {"mode": resolved.mode}}
|
|
255
|
+
if resolved.remote:
|
|
256
|
+
doc["beads"]["remote"] = resolved.remote
|
|
257
|
+
if resolved.database:
|
|
258
|
+
doc["beads"]["database"] = resolved.database
|
|
259
|
+
|
|
260
|
+
# This rewrite drops comments; carry the operator's per-project skills across
|
|
261
|
+
# so a beads-deviation write never strands them.
|
|
262
|
+
existing_skills = load_project_skills(dotbrain_home, name)
|
|
263
|
+
if existing_skills:
|
|
264
|
+
doc["skills"] = list(existing_skills)
|
|
265
|
+
existing_subagents = load_project_subagents(dotbrain_home, name)
|
|
266
|
+
if existing_subagents:
|
|
267
|
+
doc["subagents"] = list(existing_subagents)
|
|
268
|
+
|
|
269
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
path.write_text(
|
|
271
|
+
yaml.dump(doc, default_flow_style=False, sort_keys=False),
|
|
272
|
+
encoding="utf-8",
|
|
273
|
+
newline="\n",
|
|
274
|
+
)
|
|
275
|
+
return f"wrote beads config for {name} to {path}"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Public mutation helpers (used by wire / migrate / workflows)
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
def record_project_beads(dotbrain_home: Path, name: str, beads: ProjectBeads) -> str | None:
|
|
283
|
+
"""Persist a project's beads config in ``.brain/project.yaml``.
|
|
284
|
+
|
|
285
|
+
Only writes when the config deviates from the resolved defaults (mode differs
|
|
286
|
+
from the config-driven default mode, or a custom remote/database). A default
|
|
287
|
+
call never overwrites a manual declaration (e.g. mode: none) that already
|
|
288
|
+
exists. Returns a log line when the file changed, else None.
|
|
289
|
+
"""
|
|
290
|
+
if not _is_beads_deviation(dotbrain_home, name, beads):
|
|
291
|
+
return None
|
|
292
|
+
return write_project_config(dotbrain_home, name, beads)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def migrate_legacy_skill_manifest(dotbrain_home: Path, name: str) -> str | None:
|
|
296
|
+
"""Fold a legacy ``.brain/agents/skills.yaml`` into ``.brain/project.yaml`` and remove it.
|
|
297
|
+
|
|
298
|
+
The per-project skill list used to live in a tool-managed manifest under
|
|
299
|
+
``.brain/agents/``; it now lives in ``.brain/project.yaml`` ``skills:``. This folds any
|
|
300
|
+
extras across (without losing them) and deletes the stale file. Idempotent.
|
|
301
|
+
"""
|
|
302
|
+
import yaml
|
|
303
|
+
|
|
304
|
+
from dotbrain import skills
|
|
305
|
+
|
|
306
|
+
legacy = paths.brainspace(dotbrain_home, name) / ".brain" / "agents" / "skills.yaml"
|
|
307
|
+
if not legacy.is_file():
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
data = yaml.safe_load(legacy.read_text(encoding="utf-8")) or {}
|
|
311
|
+
extras: tuple[str, ...] = ()
|
|
312
|
+
if isinstance(data, dict):
|
|
313
|
+
extras = skills._clean(data.get("skills", data.get("extra")))
|
|
314
|
+
if extras:
|
|
315
|
+
_append_project_skills(dotbrain_home, name, extras)
|
|
316
|
+
legacy.unlink()
|
|
317
|
+
|
|
318
|
+
suffix = f" ({len(extras)} skill(s))" if extras else ""
|
|
319
|
+
return f"migrated legacy skills manifest for {name} into .brain/project.yaml{suffix}"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _append_project_skills(dotbrain_home: Path, name: str, extras: tuple[str, ...]) -> None:
|
|
323
|
+
"""Append a ``skills:`` block to ``.brain/project.yaml``, preserving existing content.
|
|
324
|
+
|
|
325
|
+
No-op when the file already declares ``skills:`` (the operator owns it then).
|
|
326
|
+
"""
|
|
327
|
+
import re
|
|
328
|
+
|
|
329
|
+
path = _project_config_path(dotbrain_home, name)
|
|
330
|
+
block = "skills:\n" + "".join(f" - {skill}\n" for skill in extras)
|
|
331
|
+
if path.is_file():
|
|
332
|
+
text = path.read_text(encoding="utf-8")
|
|
333
|
+
if re.search(r"(?m)^skills:", text):
|
|
334
|
+
return
|
|
335
|
+
if text and not text.endswith("\n"):
|
|
336
|
+
text += "\n"
|
|
337
|
+
path.write_text(text + "\n" + block, encoding="utf-8", newline="\n")
|
|
338
|
+
return
|
|
339
|
+
header = "# Per-project skills, linked on top of the brain-coupled required core.\n"
|
|
340
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
path.write_text(header + block, encoding="utf-8", newline="\n")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _is_beads_deviation(dotbrain_home: Path, name: str, beads: ProjectBeads) -> bool:
|
|
345
|
+
return (
|
|
346
|
+
beads.mode != default_beads_mode(dotbrain_home)
|
|
347
|
+
or bool(beads.remote)
|
|
348
|
+
or bool(beads.database and beads.database != name)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def remove_project_beads(dotbrain_home: Path, name: str) -> str | None:
|
|
353
|
+
"""Drop a project's per-project config. Returns a log line or None if absent."""
|
|
354
|
+
path = _project_config_path(dotbrain_home, name)
|
|
355
|
+
if path.is_file():
|
|
356
|
+
os.remove(path)
|
|
357
|
+
return f"removed .brain/project.yaml for {name}"
|
|
358
|
+
|
|
359
|
+
# Old-format cleanup: remove from dotbrain.yaml projects section
|
|
360
|
+
old = _old_config_path(dotbrain_home)
|
|
361
|
+
if old.is_file():
|
|
362
|
+
import yaml
|
|
363
|
+
data: dict[str, Any] = yaml.safe_load(old.read_text(encoding="utf-8")) or {}
|
|
364
|
+
projects = data.get("projects")
|
|
365
|
+
if projects and name in projects:
|
|
366
|
+
return _remove_from_old_projects_section(old, name)
|
|
367
|
+
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _remove_from_old_projects_section(path: Path, name: str) -> str | None:
|
|
372
|
+
"""Drop ``projects.<name>`` from a legacy dotbrain.yaml, preserving other text."""
|
|
373
|
+
import yaml
|
|
374
|
+
|
|
375
|
+
data: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
376
|
+
projects: dict[str, Any] = data.get("projects") or {}
|
|
377
|
+
if name not in projects:
|
|
378
|
+
return None
|
|
379
|
+
del projects[name]
|
|
380
|
+
|
|
381
|
+
text = path.read_text(encoding="utf-8")
|
|
382
|
+
kept: list[str] = []
|
|
383
|
+
in_section = False
|
|
384
|
+
in_entry = False
|
|
385
|
+
for line in text.splitlines():
|
|
386
|
+
if in_entry:
|
|
387
|
+
if line and not line[0].isspace():
|
|
388
|
+
in_entry = False
|
|
389
|
+
else:
|
|
390
|
+
continue
|
|
391
|
+
if in_section:
|
|
392
|
+
if line.startswith(f" {name}:"):
|
|
393
|
+
in_entry = True
|
|
394
|
+
continue
|
|
395
|
+
if line and not line[0].isspace():
|
|
396
|
+
in_section = False
|
|
397
|
+
|
|
398
|
+
if line.startswith("projects:"):
|
|
399
|
+
in_section = True
|
|
400
|
+
continue
|
|
401
|
+
kept.append(line)
|
|
402
|
+
|
|
403
|
+
while kept and not kept[-1].strip():
|
|
404
|
+
kept.pop()
|
|
405
|
+
out = "\n".join(kept) + "\n"
|
|
406
|
+
if projects:
|
|
407
|
+
out += "\n" + _render_old_projects_section(projects)
|
|
408
|
+
path.write_text(out, encoding="utf-8", newline="\n")
|
|
409
|
+
return f"removed beads deviation for {name} from dotbrain.yaml"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _render_old_projects_section(projects: dict[str, Any]) -> str:
|
|
413
|
+
lines = ["projects:"]
|
|
414
|
+
for pname in sorted(projects):
|
|
415
|
+
entry = projects[pname]
|
|
416
|
+
beads = (entry or {}).get("beads") or {}
|
|
417
|
+
lines.append(f" {pname}:")
|
|
418
|
+
lines.append(" beads:")
|
|
419
|
+
mode = beads.get("mode", "embedded")
|
|
420
|
+
if mode != "embedded":
|
|
421
|
+
lines.append(f" mode: {mode}")
|
|
422
|
+
remote = beads.get("remote", "")
|
|
423
|
+
if remote:
|
|
424
|
+
lines.append(f" remote: {remote}")
|
|
425
|
+
database = beads.get("database", "")
|
|
426
|
+
if database:
|
|
427
|
+
lines.append(f" database: {database}")
|
|
428
|
+
return "\n".join(lines) + "\n"
|