|
17 | 17 | from .utils.colors import ColorFormatter |
18 | 18 |
|
19 | 19 | try: |
20 | | - from asciidag.graph import Graph |
21 | | - from asciidag.node import Node |
22 | | - ASCIIDAG_AVAILABLE = True |
| 20 | + import networkx as nx |
| 21 | + from phart import ASCIIRenderer |
| 22 | + PHART_AVAILABLE = True |
23 | 23 | except ImportError: |
24 | | - ASCIIDAG_AVAILABLE = False |
| 24 | + PHART_AVAILABLE = False |
25 | 25 |
|
26 | 26 |
|
27 | 27 | class CLI: |
@@ -208,14 +208,11 @@ def run(self, argv: Optional[list[str]] = None) -> int: |
208 | 208 | print(f"{'❌' if not args.no_color else '✗'} {color.error('Error:')} No markdown files found matching pattern: {args.defs}") |
209 | 209 | return 1 |
210 | 210 |
|
211 | | - print(f"{'📁' if not args.no_color else '▸'} {color.dim('Found')} {color.bold(str(len(md_files)))} {color.dim('definition file(s)')}") |
212 | | - |
213 | 211 | # Parse markdown files |
214 | | - print(f"{'📝' if not args.no_color else '▸'} {color.dim('Parsing definitions...')}") |
215 | 212 | parser = MarkdownParser() |
216 | 213 | document = parser.parse_files(md_files) |
217 | 214 |
|
218 | | - print(f"{'✓' if not args.no_color else '✓'} {color.dim('Loaded')} {color.bold(str(len(document.actions)))} {color.dim('action(s)')}") |
| 215 | + print(f"{'📚' if not args.no_color else '▸'} {color.dim('Found')} {color.bold(str(len(md_files)))} {color.dim('definition file(s) with')} {color.bold(str(len(document.actions)))} {color.dim('actions')}") |
219 | 216 |
|
220 | 217 | # Handle --list-actions |
221 | 218 | if args.list_actions: |
@@ -252,33 +249,30 @@ def run(self, argv: Optional[list[str]] = None) -> int: |
252 | 249 | custom_args[arg_name] = arg_def.default_value |
253 | 250 |
|
254 | 251 | # Build DAG |
255 | | - print(f"\n{'🔨' if not args.no_color else '▸'} {color.dim('Building dependency graph...')}") |
256 | 252 | builder = DAGBuilder(document) |
257 | 253 | builder.validate_goals(goals) |
258 | 254 | graph = builder.build_graph(goals, axis_values) |
259 | 255 |
|
260 | 256 | # Prune to goals |
261 | 257 | pruned_graph = graph.prune_to_goals() |
262 | | - print(f"{'📊' if not args.no_color else '▸'} {color.dim('Graph contains')} {color.bold(str(len(pruned_graph.nodes)))} {color.dim('required action(s)')}") |
263 | 258 |
|
264 | 259 | # Validate |
265 | | - print(f"{'🔍' if not args.no_color else '▸'} {color.dim('Validating...')}") |
266 | 260 | validator = DAGValidator(document, pruned_graph) |
267 | 261 |
|
268 | 262 | # Initialize flags with all defined flags |
269 | 263 | all_flags = {name: False for name in document.flags} |
270 | 264 | all_flags.update(custom_flags) |
271 | 265 |
|
272 | 266 | validator.validate_all(custom_args, all_flags, axis_values) |
273 | | - print(f"{'✅' if not args.no_color else '✓'} {color.success('Validation passed')}") |
| 267 | + print(f"{'✅' if not args.no_color else '✓'} {color.dim('Built plan graph with')} {color.bold(str(len(pruned_graph.nodes)))} {color.dim('required action(s)')}") |
274 | 268 |
|
275 | 269 | # Show execution plan |
276 | 270 | execution_order = pruned_graph.get_execution_order() |
277 | 271 | print(f"\n{'📋' if not args.no_color else '▸'} {color.bold('Execution plan:')}") |
278 | 272 |
|
279 | | - if ASCIIDAG_AVAILABLE and not args.no_color: |
280 | | - # Use asciidag to visualize the DAG |
281 | | - self._visualize_execution_plan_dag(pruned_graph, execution_order, goals, color) |
| 273 | + if PHART_AVAILABLE and not args.no_color: |
| 274 | + # Use phart to visualize the DAG |
| 275 | + self._visualize_execution_plan_phart(pruned_graph, execution_order, goals, color) |
282 | 276 | else: |
283 | 277 | # Fallback to simple text output |
284 | 278 | for i, action_name in enumerate(execution_order, 1): |
@@ -365,59 +359,42 @@ def run(self, argv: Optional[list[str]] = None) -> int: |
365 | 359 | traceback.print_exc() |
366 | 360 | return 1 |
367 | 361 |
|
368 | | - def _visualize_execution_plan_dag(self, graph, execution_order: list[str], goals: list[str], color) -> None: |
369 | | - """Visualize execution plan using asciidag. |
| 362 | + def _visualize_execution_plan_phart(self, graph, execution_order: list[str], goals: list[str], color) -> None: |
| 363 | + """Visualize execution plan using phart. |
370 | 364 |
|
371 | 365 | Args: |
372 | 366 | graph: The execution graph |
373 | 367 | execution_order: List of actions in execution order |
374 | 368 | goals: List of goal actions |
375 | 369 | color: Color formatter |
376 | 370 | """ |
377 | | - from asciidag.graph import Graph as AsciiGraph |
378 | | - from asciidag.node import Node as AsciiNode |
| 371 | + import networkx as nx |
| 372 | + from phart import ASCIIRenderer |
379 | 373 |
|
380 | | - # Create a mapping of action names to asciidag nodes |
381 | | - node_map = {} |
| 374 | + # Create a NetworkX DiGraph |
| 375 | + # Use labels as node IDs so they show up in the visualization |
| 376 | + G = nx.DiGraph() |
382 | 377 |
|
383 | | - # Build asciidag nodes in execution order |
| 378 | + # Create a mapping from action names to labels |
| 379 | + label_map = {} |
384 | 380 | for action_name in execution_order: |
385 | | - exec_node = graph.get_node(action_name) |
386 | | - |
387 | | - # Get parent nodes (dependencies) |
388 | | - parent_nodes = [] |
389 | | - for dep in sorted(exec_node.dependencies): |
390 | | - if dep in node_map: |
391 | | - parent_nodes.append(node_map[dep]) |
392 | | - |
393 | | - # Create label with execution order number and goal marker |
394 | 381 | order_num = execution_order.index(action_name) + 1 |
395 | 382 | is_goal = action_name in goals |
396 | 383 | goal_marker = " 🎯" if is_goal else "" |
397 | 384 | label = f"{order_num}. {action_name}{goal_marker}" |
| 385 | + label_map[action_name] = label |
| 386 | + G.add_node(label) |
398 | 387 |
|
399 | | - # Create asciidag node |
400 | | - ascii_node = AsciiNode(label, parents=parent_nodes) |
401 | | - node_map[action_name] = ascii_node |
402 | | - |
403 | | - # Get goal nodes (tips of the DAG to display) |
404 | | - goal_nodes = [node_map[goal] for goal in goals if goal in node_map] |
405 | | - |
406 | | - # If no specific goals or all actions shown, use actions with no dependents |
407 | | - if not goal_nodes: |
408 | | - # Find actions with no dependents (leaf nodes) |
409 | | - has_dependents = set() |
410 | | - for action_name in execution_order: |
411 | | - exec_node = graph.get_node(action_name) |
412 | | - for dep in exec_node.dependencies: |
413 | | - has_dependents.add(dep) |
414 | | - |
415 | | - goal_nodes = [node_map[action] for action in execution_order |
416 | | - if action not in has_dependents and action in node_map] |
| 388 | + # Add edges using labels |
| 389 | + for action_name in execution_order: |
| 390 | + exec_node = graph.get_node(action_name) |
| 391 | + for dep in exec_node.dependencies: |
| 392 | + # In a dependency graph, edges go from dependency to dependent |
| 393 | + G.add_edge(label_map[dep], label_map[action_name]) |
417 | 394 |
|
418 | | - # Render the DAG |
419 | | - ascii_graph = AsciiGraph() |
420 | | - ascii_graph.show_nodes(goal_nodes) |
| 395 | + # Render the graph with better spacing |
| 396 | + renderer = ASCIIRenderer(G, node_spacing=1, layer_spacing=1) |
| 397 | + print(renderer.render()) |
421 | 398 |
|
422 | 399 | def _list_actions(self, document: ParsedDocument) -> None: |
423 | 400 | """List all available actions.""" |
|
0 commit comments