Coverage for flowr / cli / session_cmd.py: 100%
97 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-22 18:42 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-22 18:42 +0000
1"""Session subcommand group: init, show, set-state.
3Parses CLI args, dispatches to domain/infrastructure, formats output.
4"""
6import argparse
7import sys
8from pathlib import Path
9from typing import NoReturn
11from flowr.cli.output import format_json, format_text
12from flowr.cli.resolution import DefaultFlowNameResolver, FlowNameNotFoundError
13from flowr.domain.loader import load_flow_from_file, resolve_subflows
14from flowr.domain.session import SessionStackFrame
15from flowr.infrastructure.config import FlowrConfig
16from flowr.infrastructure.session_store import (
17 SessionAlreadyExistsError,
18 SessionNameNotFoundError,
19 SessionNotFoundError,
20 YamlSessionStore,
21)
24def add_session_parser(sub: argparse._SubParsersAction) -> None:
25 """Add the session subcommand group to the argument parser."""
26 session_parser = sub.add_parser("session", help="Manage workflow sessions")
27 session_sub = session_parser.add_subparsers(dest="session_command")
29 # session init
30 p_init = session_sub.add_parser("init", help="Initialize a new session")
31 p_init.add_argument("flow", help="Flow name or file path")
32 p_init.add_argument(
33 "--name", default=None, help="Session name or file path (default: from config)"
34 )
36 # session show
37 p_show = session_sub.add_parser("show", help="Show current session state")
38 p_show.add_argument(
39 "--name", default=None, help="Session name or file path (default: from config)"
40 )
41 p_show.add_argument(
42 "--format",
43 choices=["yaml", "json"],
44 default="json",
45 dest="output_format",
46 help="Output format (default: json)",
47 )
49 # session set-state
50 p_set = session_sub.add_parser("set-state", help="Update the current session state")
51 p_set.add_argument("state", help="New state ID")
52 p_set.add_argument(
53 "--name", default=None, help="Session name or file path (default: from config)"
54 )
56 # session list
57 p_list = session_sub.add_parser("list", help="List all sessions")
58 p_list.add_argument(
59 "--format",
60 choices=["yaml", "json"],
61 default="json",
62 dest="output_format",
63 help="Output format (default: json)",
64 )
67def _error(msg: str) -> NoReturn:
68 """Print error to stderr and exit with code 1."""
69 print(f"error: {msg}", file=sys.stderr) # noqa: T201
70 sys.exit(1)
73def cmd_session_init(
74 args: argparse.Namespace, config: FlowrConfig, resolver: DefaultFlowNameResolver
75) -> int:
76 """Run session init subcommand.
78 Returns:
79 Exit code: 0 on success, 1 on error.
80 """
81 flows_dir = config.flows_path()
82 sessions_dir = config.sessions_path()
83 name = args.name or config.default_session
85 try:
86 flow_path = resolver.resolve(args.flow, flows_dir)
87 except FlowNameNotFoundError as exc:
88 _error(str(exc))
90 store = YamlSessionStore(sessions_dir)
92 try:
93 session = store.init(flow_path, name)
94 except SessionAlreadyExistsError as exc:
95 _error(str(exc))
97 flow = load_flow_from_file(flow_path)
98 if flow.states and flow.states[0].flow is not None:
99 all_flows = resolve_subflows(flow, flow_path)
100 ref_stem = Path(flow.states[0].flow).stem
101 subflow = next((f for f in all_flows if f.flow == ref_stem), None)
102 if subflow is not None and subflow.states:
103 frame = SessionStackFrame(flow=session.flow, state=session.state)
104 initial_id = subflow.states[0].id
105 session = session.push_stack(frame, initial_id, new_flow=subflow.flow)
106 store.save(session)
108 output = {
109 "flow": session.flow,
110 "state": session.state,
111 "name": session.name,
112 "created_at": session.created_at,
113 }
114 print(format_text(output)) # noqa: T201
115 return 0
118def cmd_session_show(
119 args: argparse.Namespace, config: FlowrConfig, _resolver: DefaultFlowNameResolver
120) -> int:
121 """Run session show subcommand.
123 Returns:
124 Exit code: 0 on success, 1 on error.
125 """
126 sessions_dir = config.sessions_path()
127 name = args.name or config.default_session
129 store = YamlSessionStore(sessions_dir)
130 try:
131 session = store.load(name)
132 except (SessionNotFoundError, SessionNameNotFoundError) as exc:
133 _error(str(exc))
135 output: dict = {
136 "flow": session.flow,
137 "state": session.state,
138 "name": session.name,
139 "stack": [{"flow": f.flow, "state": f.state} for f in session.stack],
140 "created_at": session.created_at,
141 "updated_at": session.updated_at,
142 }
144 if args.output_format == "json":
145 print(format_json(output)) # noqa: T201
146 else:
147 print(format_text(output)) # noqa: T201
148 return 0
151def cmd_session_set_state(
152 args: argparse.Namespace, config: FlowrConfig, resolver: DefaultFlowNameResolver
153) -> int:
154 """Run session set-state subcommand.
156 Returns:
157 Exit code: 0 on success, 1 on error.
158 """
159 sessions_dir = config.sessions_path()
160 name = args.name or config.default_session
162 store = YamlSessionStore(sessions_dir)
163 try:
164 session = store.load(name)
165 except (SessionNotFoundError, SessionNameNotFoundError) as exc:
166 _error(str(exc))
168 # Validate that the requested state exists in the flow
169 flows_dir = config.flows_path()
170 try:
171 flow_path = resolver.resolve(session.flow, flows_dir)
172 except FlowNameNotFoundError as exc:
173 _error(str(exc))
175 flow = load_flow_from_file(flow_path)
176 state_ids = {s.id for s in flow.states}
177 if args.state not in state_ids:
178 _error(f"State '{args.state}' not found in flow '{session.flow}'")
180 updated = session.with_state(args.state)
181 store.save(updated)
183 output = {
184 "flow": updated.flow,
185 "state": updated.state,
186 "updated_at": updated.updated_at,
187 }
188 print(format_text(output)) # noqa: T201
189 return 0
192def cmd_session_list(
193 args: argparse.Namespace, config: FlowrConfig, _resolver: DefaultFlowNameResolver
194) -> int:
195 """Run session list subcommand.
197 Returns:
198 Exit code: 0 on success, 1 on error.
199 """
200 sessions_dir = config.sessions_path()
202 store = YamlSessionStore(sessions_dir)
203 sessions = store.list_sessions()
205 rows = [
206 {
207 "name": s.name,
208 "flow": s.flow,
209 "state": s.state,
210 "updated_at": s.updated_at,
211 }
212 for s in sessions
213 ]
215 if args.output_format == "json":
216 print(format_json(rows)) # noqa: T201
217 else:
218 print(format_text(rows)) # noqa: T201
219 return 0