Coverage for little_loops / cli / loop / config_cmds.py: 0%

53 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-15 17:27 -0500

1"""ll-loop config subcommands: validate, install.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6from pathlib import Path 

7 

8from little_loops.cli.loop._helpers import get_builtin_loops_dir, resolve_loop_path 

9from little_loops.logger import Logger 

10 

11 

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 

20 

21 as_json = getattr(args, "json", False) 

22 

23 try: 

24 path = resolve_loop_path(loop_name, loops_dir) 

25 

26 if as_json: 

27 from little_loops.cli.output import print_json 

28 

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 

42 

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 iterations: {fsm.max_iterations}") 

48 for w in warnings: 

49 print(f"{w}") 

50 return 0 

51 except FileNotFoundError as e: 

52 if as_json: 

53 from little_loops.cli.output import print_json 

54 

55 print_json( 

56 { 

57 "loop": loop_name, 

58 "valid": False, 

59 "violations": [{"severity": "error", "path": "<root>", "message": str(e)}], 

60 } 

61 ) 

62 else: 

63 logger.error(str(e)) 

64 return 1 

65 except ValueError as e: 

66 logger.error(f"{loop_name} is invalid: {e}") 

67 return 1 

68 

69 

70def cmd_install( 

71 loop_name: str, 

72 loops_dir: Path, 

73 logger: Logger, 

74) -> int: 

75 """Copy a built-in loop to .loops/ for customization.""" 

76 import shutil 

77 

78 builtin_dir = get_builtin_loops_dir() 

79 source = builtin_dir / f"{loop_name}.yaml" 

80 

81 if not source.exists(): 

82 available = [f.stem for f in builtin_dir.glob("*.yaml")] if builtin_dir.exists() else [] 

83 logger.error(f"No built-in loop named '{loop_name}'") 

84 if available: 

85 print(f"Available built-in loops: {', '.join(sorted(available))}") 

86 return 1 

87 

88 loops_dir.mkdir(exist_ok=True) 

89 dest = loops_dir / f"{loop_name}.yaml" 

90 

91 if dest.exists(): 

92 logger.error(f"Loop already exists: {dest}") 

93 print("Remove it first or edit it directly.") 

94 return 1 

95 

96 shutil.copy2(source, dest) 

97 print(f"Installed {loop_name} to {dest}") 

98 print("You can now customize it by editing the file.") 

99 return 0