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

2

3 from __future__ import annotations

4

5 import argparse

6 from pathlib import Path

7

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

9 from little_loops.logger import Logger

10

11

12 def 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 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

56

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

70

71

72 def 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

79

80 builtin_dir = get_builtin_loops_dir()

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

82

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

89

90 loops_dir.mkdir(exist_ok=True)

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

92

93 if dest.exists():

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

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

96 return 1

97

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