Coverage for little_loops / cli / loop / config_cmds.py: 0%
55 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-26 17:38 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-26 17:38 -0500
1"""ll-loop config subcommands: validate, install."""
3from __future__ import annotations
5import argparse
6from pathlib import Path
8from little_loops.cli.loop._helpers import get_builtin_loops_dir, resolve_loop_path
9from little_loops.logger import Logger
12def cmd_validate(
13 loop_name: str,
14 args: argparse.Namespace,
15 loops_dir: Path,
16 logger: Logger,
17) -> int:
18 """Validate a loop definition."""
19 from little_loops.fsm.validation import ValidationSeverity, load_and_validate
21 as_json = getattr(args, "json", False)
23 try:
24 path = resolve_loop_path(loop_name, loops_dir)
26 if as_json:
27 from little_loops.cli.output import print_json
29 fsm, violations = load_and_validate(path, raise_on_error=False)
30 has_errors = any(v.severity == ValidationSeverity.ERROR for v in violations)
31 print_json(
32 {
33 "loop": loop_name,
34 "valid": not has_errors,
35 "violations": [
36 {"severity": v.severity.value, "path": v.path, "message": v.message}
37 for v in violations
38 ],
39 }
40 )
41 return 1 if has_errors else 0
43 fsm, warnings = load_and_validate(path)
44 logger.success(f"{loop_name} is valid")
45 print(f" States: {', '.join(fsm.states.keys())}")
46 print(f" Initial: {fsm.initial}")
47 print(f" Max steps: {fsm.max_steps}")
48 if fsm.max_iterations is not None:
49 print(f" Max iterations: {fsm.max_iterations}")
50 for w in warnings:
51 print(f" ⚠ {w}")
52 return 0
53 except FileNotFoundError as e:
54 if as_json:
55 from little_loops.cli.output import print_json
57 print_json(
58 {
59 "loop": loop_name,
60 "valid": False,
61 "violations": [{"severity": "error", "path": "<root>", "message": str(e)}],
62 }
63 )
64 else:
65 logger.error(str(e))
66 return 1
67 except ValueError as e:
68 logger.error(f"{loop_name} is invalid: {e}")
69 return 1
72def cmd_install(
73 loop_name: str,
74 loops_dir: Path,
75 logger: Logger,
76) -> int:
77 """Copy a built-in loop to .loops/ for customization."""
78 import shutil
80 builtin_dir = get_builtin_loops_dir()
81 source = builtin_dir / f"{loop_name}.yaml"
83 if not source.exists():
84 available = [f.stem for f in builtin_dir.glob("*.yaml")] if builtin_dir.exists() else []
85 logger.error(f"No built-in loop named '{loop_name}'")
86 if available:
87 print(f"Available built-in loops: {', '.join(sorted(available))}")
88 return 1
90 loops_dir.mkdir(exist_ok=True)
91 dest = loops_dir / f"{loop_name}.yaml"
93 if dest.exists():
94 logger.error(f"Loop already exists: {dest}")
95 print("Remove it first or edit it directly.")
96 return 1
98 shutil.copy2(source, dest)
99 print(f"Installed {loop_name} to {dest}")
100 print("You can now customize it by editing the file.")
101 return 0