agent-brain-cli 9.4.0__tar.gz → 9.5.0__tar.gz
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.
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/PKG-INFO +2 -2
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/__init__.py +1 -1
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/config.py +205 -3
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/types.py +3 -1
- agent_brain_cli-9.5.0/agent_brain_cli/config_migrate.py +197 -0
- agent_brain_cli-9.5.0/agent_brain_cli/config_schema.py +490 -0
- agent_brain_cli-9.5.0/agent_brain_cli/runtime/opencode_converter.py +234 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/parser.py +3 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/tool_maps.py +19 -3
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/types.py +3 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/pyproject.toml +2 -2
- agent_brain_cli-9.4.0/agent_brain_cli/runtime/opencode_converter.py +0 -146
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/README.md +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/cli.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/client/__init__.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/client/api_client.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/__init__.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/cache.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/folders.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/index.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/init.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/inject.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/install_agent.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/jobs.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/list_cmd.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/query.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/reset.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/start.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/status.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/stop.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/commands/uninstall.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/config.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/migration.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/__init__.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/claude_converter.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/codex_converter.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/converter_base.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/gemini_converter.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/runtime/skill_runtime_converter.py +0 -0
- {agent_brain_cli-9.4.0 → agent_brain_cli-9.5.0}/agent_brain_cli/xdg_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: agent-brain-cli
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.5.0
|
|
4
4
|
Summary: Agent Brain CLI - Command-line interface for managing AI agent memory and knowledge retrieval
|
|
5
5
|
Home-page: https://github.com/SpillwaveSolutions/agent-brain
|
|
6
6
|
License: MIT
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Requires-Dist: agent-brain-rag (>=9.
|
|
18
|
+
Requires-Dist: agent-brain-rag (>=9.5.0,<10.0.0)
|
|
19
19
|
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
20
20
|
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
21
21
|
Requires-Dist: pydantic (>=2.10.0,<3.0.0)
|
|
@@ -12,6 +12,15 @@ import yaml
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
|
+
from agent_brain_cli.config_migrate import (
|
|
16
|
+
MigrationResult,
|
|
17
|
+
diff_config_file,
|
|
18
|
+
migrate_config_file,
|
|
19
|
+
)
|
|
20
|
+
from agent_brain_cli.config_schema import (
|
|
21
|
+
format_validation_errors,
|
|
22
|
+
validate_config_file,
|
|
23
|
+
)
|
|
15
24
|
from agent_brain_cli.xdg_paths import get_xdg_config_dir
|
|
16
25
|
|
|
17
26
|
console = Console()
|
|
@@ -152,9 +161,12 @@ def config_group() -> None:
|
|
|
152
161
|
|
|
153
162
|
\b
|
|
154
163
|
Commands:
|
|
155
|
-
show
|
|
156
|
-
path
|
|
157
|
-
wizard
|
|
164
|
+
show - Display active configuration
|
|
165
|
+
path - Show config file location
|
|
166
|
+
wizard - Create/update config interactively
|
|
167
|
+
validate - Validate config against schema
|
|
168
|
+
migrate - Upgrade config to current schema
|
|
169
|
+
diff - Preview what migrate would change
|
|
158
170
|
"""
|
|
159
171
|
pass
|
|
160
172
|
|
|
@@ -162,6 +174,17 @@ def config_group() -> None:
|
|
|
162
174
|
@config_group.command("wizard")
|
|
163
175
|
def wizard() -> None:
|
|
164
176
|
"""Interactive configuration wizard for Agent Brain providers."""
|
|
177
|
+
# Check existing config for validation issues before prompting
|
|
178
|
+
existing_config = _find_config_file()
|
|
179
|
+
if existing_config:
|
|
180
|
+
existing_errors = validate_config_file(existing_config)
|
|
181
|
+
if existing_errors:
|
|
182
|
+
console.print(
|
|
183
|
+
"\n[bold yellow]Warning:[/] Existing config has validation issues:"
|
|
184
|
+
)
|
|
185
|
+
console.print(format_validation_errors(existing_errors))
|
|
186
|
+
console.print()
|
|
187
|
+
|
|
165
188
|
embed_provider: str = click.prompt(
|
|
166
189
|
"Embedding provider",
|
|
167
190
|
type=click.Choice(["openai", "ollama", "cohere"]),
|
|
@@ -288,6 +311,19 @@ def wizard() -> None:
|
|
|
288
311
|
|
|
289
312
|
console.print(f"[green]Config written to {config_path}[/]")
|
|
290
313
|
|
|
314
|
+
# Validate the written config and warn if issues found
|
|
315
|
+
post_write_errors = validate_config_file(config_path)
|
|
316
|
+
if post_write_errors:
|
|
317
|
+
console.print(
|
|
318
|
+
"\n[bold yellow]Warning:[/] The generated config has validation issues:\n"
|
|
319
|
+
)
|
|
320
|
+
console.print(format_validation_errors(post_write_errors))
|
|
321
|
+
if not click.confirm("Continue with this config anyway?", default=False):
|
|
322
|
+
console.print(
|
|
323
|
+
"[red]Config wizard aborted." " Please fix the issues and try again.[/]"
|
|
324
|
+
)
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
291
327
|
|
|
292
328
|
@config_group.command("show")
|
|
293
329
|
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
@@ -408,3 +444,169 @@ def config_path(json_output: bool) -> None:
|
|
|
408
444
|
console.print(f"[green]{config_path}[/]")
|
|
409
445
|
else:
|
|
410
446
|
console.print("[yellow]No config file found[/]")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@config_group.command("validate")
|
|
450
|
+
@click.option(
|
|
451
|
+
"--file",
|
|
452
|
+
"config_file",
|
|
453
|
+
type=click.Path(exists=True),
|
|
454
|
+
default=None,
|
|
455
|
+
help="Path to config file (default: auto-detect)",
|
|
456
|
+
)
|
|
457
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
458
|
+
def validate_config(config_file: str | None, json_output: bool) -> None:
|
|
459
|
+
"""Validate config.yaml against the Agent Brain schema.
|
|
460
|
+
|
|
461
|
+
Checks for unknown keys, invalid provider values, deprecated fields,
|
|
462
|
+
and type errors. Reports line numbers and fix suggestions.
|
|
463
|
+
|
|
464
|
+
\b
|
|
465
|
+
Examples:
|
|
466
|
+
agent-brain config validate
|
|
467
|
+
agent-brain config validate --file ./my-config.yaml
|
|
468
|
+
agent-brain config validate --json
|
|
469
|
+
"""
|
|
470
|
+
if config_file is not None:
|
|
471
|
+
path: Path | None = Path(config_file)
|
|
472
|
+
else:
|
|
473
|
+
path = _find_config_file()
|
|
474
|
+
|
|
475
|
+
if path is None:
|
|
476
|
+
if json_output:
|
|
477
|
+
click.echo(
|
|
478
|
+
json.dumps(
|
|
479
|
+
{
|
|
480
|
+
"valid": None,
|
|
481
|
+
"config_file": None,
|
|
482
|
+
"errors": [],
|
|
483
|
+
"message": "No config file found",
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
else:
|
|
488
|
+
console.print("[yellow]No config file found. Nothing to validate.[/]")
|
|
489
|
+
sys.exit(0)
|
|
490
|
+
|
|
491
|
+
errors = validate_config_file(path)
|
|
492
|
+
|
|
493
|
+
if json_output:
|
|
494
|
+
output: dict[str, Any] = {
|
|
495
|
+
"valid": len(errors) == 0,
|
|
496
|
+
"config_file": str(path),
|
|
497
|
+
"errors": [
|
|
498
|
+
{
|
|
499
|
+
"field": e.field,
|
|
500
|
+
"message": e.message,
|
|
501
|
+
"line_number": e.line_number,
|
|
502
|
+
"suggestion": e.suggestion,
|
|
503
|
+
}
|
|
504
|
+
for e in errors
|
|
505
|
+
],
|
|
506
|
+
}
|
|
507
|
+
click.echo(json.dumps(output, indent=2))
|
|
508
|
+
if errors:
|
|
509
|
+
sys.exit(1)
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
if not errors:
|
|
513
|
+
console.print(f"[green]Config is valid[/] ({path})")
|
|
514
|
+
sys.exit(0)
|
|
515
|
+
|
|
516
|
+
console.print(format_validation_errors(errors))
|
|
517
|
+
sys.exit(1)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@config_group.command("migrate")
|
|
521
|
+
@click.option(
|
|
522
|
+
"--file",
|
|
523
|
+
"config_file",
|
|
524
|
+
type=click.Path(exists=True),
|
|
525
|
+
default=None,
|
|
526
|
+
help="Path to config file (default: auto-detect)",
|
|
527
|
+
)
|
|
528
|
+
@click.option(
|
|
529
|
+
"--dry-run", is_flag=True, help="Show what would change without modifying"
|
|
530
|
+
)
|
|
531
|
+
def migrate_config_cmd(config_file: str | None, dry_run: bool) -> None:
|
|
532
|
+
"""Migrate config.yaml to the current schema version.
|
|
533
|
+
|
|
534
|
+
Upgrades deprecated keys and restructures config sections.
|
|
535
|
+
Use --dry-run to preview changes without modifying the file.
|
|
536
|
+
|
|
537
|
+
\b
|
|
538
|
+
Examples:
|
|
539
|
+
agent-brain config migrate
|
|
540
|
+
agent-brain config migrate --dry-run
|
|
541
|
+
agent-brain config migrate --file ./old-config.yaml
|
|
542
|
+
"""
|
|
543
|
+
if config_file is not None:
|
|
544
|
+
path: Path | None = Path(config_file)
|
|
545
|
+
else:
|
|
546
|
+
path = _find_config_file()
|
|
547
|
+
|
|
548
|
+
if path is None:
|
|
549
|
+
console.print("[yellow]No config file found.[/]")
|
|
550
|
+
sys.exit(0)
|
|
551
|
+
|
|
552
|
+
if dry_run:
|
|
553
|
+
diff = diff_config_file(path)
|
|
554
|
+
if diff:
|
|
555
|
+
console.print(diff)
|
|
556
|
+
else:
|
|
557
|
+
console.print("[green]Config is already up to date. No changes needed.[/]")
|
|
558
|
+
sys.exit(0)
|
|
559
|
+
|
|
560
|
+
result: MigrationResult = migrate_config_file(path)
|
|
561
|
+
if result.already_current:
|
|
562
|
+
console.print("[green]Config is already up to date[/]")
|
|
563
|
+
sys.exit(0)
|
|
564
|
+
|
|
565
|
+
for change in result.changes:
|
|
566
|
+
console.print(f" [cyan]->[/] {change}")
|
|
567
|
+
console.print(f"\n[green]Config migrated successfully[/] ({path})")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@config_group.command("diff")
|
|
571
|
+
@click.option(
|
|
572
|
+
"--file",
|
|
573
|
+
"config_file",
|
|
574
|
+
type=click.Path(exists=True),
|
|
575
|
+
default=None,
|
|
576
|
+
help="Path to config file (default: auto-detect)",
|
|
577
|
+
)
|
|
578
|
+
def diff_config_cmd(config_file: str | None) -> None:
|
|
579
|
+
"""Show what 'config migrate' would change.
|
|
580
|
+
|
|
581
|
+
Displays a unified diff of the current config vs the migrated version.
|
|
582
|
+
|
|
583
|
+
\b
|
|
584
|
+
Examples:
|
|
585
|
+
agent-brain config diff
|
|
586
|
+
agent-brain config diff --file ./config.yaml
|
|
587
|
+
"""
|
|
588
|
+
if config_file is not None:
|
|
589
|
+
path: Path | None = Path(config_file)
|
|
590
|
+
else:
|
|
591
|
+
path = _find_config_file()
|
|
592
|
+
|
|
593
|
+
if path is None:
|
|
594
|
+
console.print("[yellow]No config file found.[/]")
|
|
595
|
+
sys.exit(0)
|
|
596
|
+
|
|
597
|
+
diff = diff_config_file(path)
|
|
598
|
+
if not diff:
|
|
599
|
+
console.print("[green]Config is already up to date. No changes needed.[/]")
|
|
600
|
+
sys.exit(0)
|
|
601
|
+
|
|
602
|
+
for line in diff.splitlines():
|
|
603
|
+
if line.startswith("---") or line.startswith("+++"):
|
|
604
|
+
console.print(f"[bold]{line}[/]")
|
|
605
|
+
elif line.startswith("-"):
|
|
606
|
+
console.print(f"[red]{line}[/]")
|
|
607
|
+
elif line.startswith("+"):
|
|
608
|
+
console.print(f"[green]{line}[/]")
|
|
609
|
+
elif line.startswith("@@"):
|
|
610
|
+
console.print(f"[cyan]{line}[/]")
|
|
611
|
+
else:
|
|
612
|
+
console.print(line)
|
|
@@ -24,7 +24,8 @@ FILE_TYPE_PRESETS: dict[str, list[str]] = {
|
|
|
24
24
|
"rust": ["*.rs"],
|
|
25
25
|
"java": ["*.java"],
|
|
26
26
|
"csharp": ["*.cs"],
|
|
27
|
-
"pascal": ["*.pas", "*.pp", "*.lpr", "*.dpr"],
|
|
27
|
+
"pascal": ["*.pas", "*.pp", "*.lpr", "*.dpr", "*.dpk"],
|
|
28
|
+
"object-pascal": ["*.pas", "*.pp", "*.lpr", "*.dpr", "*.dpk"],
|
|
28
29
|
"c": ["*.c", "*.h"],
|
|
29
30
|
"cpp": ["*.cpp", "*.hpp", "*.cc", "*.hh"],
|
|
30
31
|
"web": ["*.html", "*.css", "*.scss", "*.jsx", "*.tsx"],
|
|
@@ -49,6 +50,7 @@ FILE_TYPE_PRESETS: dict[str, list[str]] = {
|
|
|
49
50
|
"*.pp",
|
|
50
51
|
"*.lpr",
|
|
51
52
|
"*.dpr",
|
|
53
|
+
"*.dpk",
|
|
52
54
|
"*.c",
|
|
53
55
|
"*.h",
|
|
54
56
|
"*.cpp",
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Config migration engine for Agent Brain YAML config files.
|
|
2
|
+
|
|
3
|
+
Provides versioned migration steps that upgrade deprecated config keys to the
|
|
4
|
+
current schema, and a diff utility to preview changes before applying them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import difflib
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# MigrationResult dataclass
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class MigrationResult:
|
|
24
|
+
"""Result of running migration steps against a config dict.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
original: Deep copy of the original config dict before migration.
|
|
28
|
+
migrated: Config dict after all migration steps have been applied.
|
|
29
|
+
changes: Human-readable descriptions of each change applied.
|
|
30
|
+
already_current: True when no migration steps produced any changes.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
original: dict[str, Any]
|
|
34
|
+
migrated: dict[str, Any]
|
|
35
|
+
changes: list[str]
|
|
36
|
+
already_current: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Migration functions
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _migrate_use_llm_extraction(
|
|
45
|
+
config: dict[str, Any],
|
|
46
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
47
|
+
"""Migrate graphrag.use_llm_extraction -> graphrag.doc_extractor.
|
|
48
|
+
|
|
49
|
+
Phase 34 decision: `use_llm_extraction` (bool) was renamed to
|
|
50
|
+
`doc_extractor` (str: "langextract" | "none").
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
config: Config dict (will NOT be mutated; a deep copy is made).
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (updated config dict, list of change descriptions).
|
|
57
|
+
"""
|
|
58
|
+
changes: list[str] = []
|
|
59
|
+
config = copy.deepcopy(config)
|
|
60
|
+
graphrag = config.get("graphrag", {})
|
|
61
|
+
|
|
62
|
+
if "use_llm_extraction" in graphrag:
|
|
63
|
+
old_val = graphrag.pop("use_llm_extraction")
|
|
64
|
+
if old_val:
|
|
65
|
+
graphrag["doc_extractor"] = "langextract"
|
|
66
|
+
changes.append(
|
|
67
|
+
"graphrag.use_llm_extraction=True -> graphrag.doc_extractor=langextract"
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
changes.append(
|
|
71
|
+
"graphrag.use_llm_extraction=False -> removed"
|
|
72
|
+
" (default: no doc extraction)"
|
|
73
|
+
)
|
|
74
|
+
config["graphrag"] = graphrag
|
|
75
|
+
|
|
76
|
+
return config, changes
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# MIGRATIONS list — applied in order by migrate_config
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
MigrationFn = Callable[[dict[str, Any]], tuple[dict[str, Any], list[str]]]
|
|
84
|
+
|
|
85
|
+
MIGRATIONS: list[MigrationFn] = [
|
|
86
|
+
_migrate_use_llm_extraction,
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Public API
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def migrate_config(config: dict[str, Any]) -> MigrationResult:
|
|
96
|
+
"""Apply all migration steps to a config dict.
|
|
97
|
+
|
|
98
|
+
Does NOT read or write any files. Mutations are applied to a deep copy,
|
|
99
|
+
so the original dict is never modified.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
config: Parsed YAML config dictionary.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
:class:`MigrationResult` describing the changes (or lack thereof).
|
|
106
|
+
"""
|
|
107
|
+
original = copy.deepcopy(config)
|
|
108
|
+
current = copy.deepcopy(config)
|
|
109
|
+
all_changes: list[str] = []
|
|
110
|
+
|
|
111
|
+
for migration_fn in MIGRATIONS:
|
|
112
|
+
current, changes = migration_fn(current)
|
|
113
|
+
all_changes.extend(changes)
|
|
114
|
+
|
|
115
|
+
return MigrationResult(
|
|
116
|
+
original=original,
|
|
117
|
+
migrated=current,
|
|
118
|
+
changes=all_changes,
|
|
119
|
+
already_current=len(all_changes) == 0,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def migrate_config_file(path: Path) -> MigrationResult:
|
|
124
|
+
"""Apply all migrations to a YAML config file and write results to disk.
|
|
125
|
+
|
|
126
|
+
Reads the YAML from *path*, calls :func:`migrate_config`, and if any
|
|
127
|
+
changes were made, writes the migrated dict back to *path* using
|
|
128
|
+
``yaml.safe_dump`` with ``default_flow_style=False, sort_keys=False``.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
path: Path to the YAML config file.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
:class:`MigrationResult` describing the changes (or lack thereof).
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
OSError: If the file cannot be read or written.
|
|
138
|
+
yaml.YAMLError: If the YAML cannot be parsed.
|
|
139
|
+
"""
|
|
140
|
+
yaml_text = path.read_text(encoding="utf-8")
|
|
141
|
+
config: dict[str, Any] = yaml.safe_load(yaml_text) or {}
|
|
142
|
+
|
|
143
|
+
result = migrate_config(config)
|
|
144
|
+
|
|
145
|
+
if not result.already_current:
|
|
146
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
147
|
+
yaml.safe_dump(
|
|
148
|
+
result.migrated, fh, default_flow_style=False, sort_keys=False
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def diff_config(config: dict[str, Any]) -> str:
|
|
155
|
+
"""Return a unified diff of what :func:`migrate_config` would change.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
config: Parsed YAML config dictionary.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Unified diff string (empty if no changes needed).
|
|
162
|
+
"""
|
|
163
|
+
result = migrate_config(config)
|
|
164
|
+
if result.already_current:
|
|
165
|
+
return ""
|
|
166
|
+
|
|
167
|
+
original_yaml = yaml.safe_dump(result.original, default_flow_style=False)
|
|
168
|
+
migrated_yaml = yaml.safe_dump(result.migrated, default_flow_style=False)
|
|
169
|
+
|
|
170
|
+
diff_lines = list(
|
|
171
|
+
difflib.unified_diff(
|
|
172
|
+
original_yaml.splitlines(keepends=True),
|
|
173
|
+
migrated_yaml.splitlines(keepends=True),
|
|
174
|
+
fromfile="config.yaml (current)",
|
|
175
|
+
tofile="config.yaml (migrated)",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return "".join(diff_lines)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def diff_config_file(path: Path) -> str:
|
|
183
|
+
"""Return a unified diff of what migrating *path* would change.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
path: Path to the YAML config file.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Unified diff string (empty if no changes needed).
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
OSError: If the file cannot be read.
|
|
193
|
+
yaml.YAMLError: If the YAML cannot be parsed.
|
|
194
|
+
"""
|
|
195
|
+
yaml_text = path.read_text(encoding="utf-8")
|
|
196
|
+
config: dict[str, Any] = yaml.safe_load(yaml_text) or {}
|
|
197
|
+
return diff_config(config)
|