claude-dev-cli 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.2.0"
12
+ __version__ = "0.7.0"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -25,6 +25,7 @@ from claude_dev_cli.usage import UsageTracker
25
25
  from claude_dev_cli import toon_utils
26
26
  from claude_dev_cli.plugins import load_plugins
27
27
  from claude_dev_cli.history import ConversationHistory, Conversation
28
+ from claude_dev_cli.template_manager import TemplateManager, Template
28
29
 
29
30
  console = Console()
30
31
 
@@ -446,12 +447,14 @@ def generate() -> None:
446
447
  @click.argument('file_path', type=click.Path(exists=True))
447
448
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
448
449
  @click.option('-a', '--api', help='API config to use')
450
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
449
451
  @click.pass_context
450
452
  def gen_tests(
451
453
  ctx: click.Context,
452
454
  file_path: str,
453
455
  output: Optional[str],
454
- api: Optional[str]
456
+ api: Optional[str],
457
+ interactive: bool
455
458
  ) -> None:
456
459
  """Generate pytest tests for a Python file."""
457
460
  console = ctx.obj['console']
@@ -460,11 +463,48 @@ def gen_tests(
460
463
  with console.status("[bold blue]Generating tests..."):
461
464
  result = generate_tests(file_path, api_config_name=api)
462
465
 
466
+ if interactive:
467
+ # Show initial result
468
+ console.print("\n[bold]Initial Tests:[/bold]\n")
469
+ console.print(result)
470
+
471
+ # Interactive refinement loop
472
+ client = ClaudeClient(api_config_name=api)
473
+ conversation_context = [result]
474
+
475
+ while True:
476
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
477
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
478
+
479
+ if user_input.lower() == 'exit':
480
+ console.print("[yellow]Discarded changes[/yellow]")
481
+ return
482
+
483
+ if user_input.lower() == 'save':
484
+ result = conversation_context[-1]
485
+ break
486
+
487
+ if not user_input:
488
+ continue
489
+
490
+ # Get refinement
491
+ refinement_prompt = f"Previous tests:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated tests."
492
+
493
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
494
+ response_parts = []
495
+ for chunk in client.call_streaming(refinement_prompt):
496
+ console.print(chunk, end='')
497
+ response_parts.append(chunk)
498
+ console.print()
499
+
500
+ result = ''.join(response_parts)
501
+ conversation_context.append(result)
502
+
463
503
  if output:
464
504
  with open(output, 'w') as f:
465
505
  f.write(result)
466
- console.print(f"[green]✓[/green] Tests saved to: {output}")
467
- else:
506
+ console.print(f"\n[green]✓[/green] Tests saved to: {output}")
507
+ elif not interactive:
468
508
  console.print(result)
469
509
 
470
510
  except Exception as e:
@@ -476,12 +516,14 @@ def gen_tests(
476
516
  @click.argument('file_path', type=click.Path(exists=True))
477
517
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
478
518
  @click.option('-a', '--api', help='API config to use')
519
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
479
520
  @click.pass_context
480
521
  def gen_docs(
481
522
  ctx: click.Context,
482
523
  file_path: str,
483
524
  output: Optional[str],
484
- api: Optional[str]
525
+ api: Optional[str],
526
+ interactive: bool
485
527
  ) -> None:
486
528
  """Generate documentation for a Python file."""
487
529
  console = ctx.obj['console']
@@ -490,11 +532,46 @@ def gen_docs(
490
532
  with console.status("[bold blue]Generating documentation..."):
491
533
  result = generate_docs(file_path, api_config_name=api)
492
534
 
535
+ if interactive:
536
+ console.print("\n[bold]Initial Documentation:[/bold]\n")
537
+ md = Markdown(result)
538
+ console.print(md)
539
+
540
+ client = ClaudeClient(api_config_name=api)
541
+ conversation_context = [result]
542
+
543
+ while True:
544
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
545
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
546
+
547
+ if user_input.lower() == 'exit':
548
+ console.print("[yellow]Discarded changes[/yellow]")
549
+ return
550
+
551
+ if user_input.lower() == 'save':
552
+ result = conversation_context[-1]
553
+ break
554
+
555
+ if not user_input:
556
+ continue
557
+
558
+ refinement_prompt = f"Previous documentation:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated documentation."
559
+
560
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
561
+ response_parts = []
562
+ for chunk in client.call_streaming(refinement_prompt):
563
+ console.print(chunk, end='')
564
+ response_parts.append(chunk)
565
+ console.print()
566
+
567
+ result = ''.join(response_parts)
568
+ conversation_context.append(result)
569
+
493
570
  if output:
494
571
  with open(output, 'w') as f:
495
572
  f.write(result)
496
- console.print(f"[green]✓[/green] Documentation saved to: {output}")
497
- else:
573
+ console.print(f"\n[green]✓[/green] Documentation saved to: {output}")
574
+ elif not interactive:
498
575
  md = Markdown(result)
499
576
  console.print(md)
500
577
 
@@ -506,11 +583,13 @@ def gen_docs(
506
583
  @main.command('review')
507
584
  @click.argument('file_path', type=click.Path(exists=True))
508
585
  @click.option('-a', '--api', help='API config to use')
586
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive follow-up questions')
509
587
  @click.pass_context
510
588
  def review(
511
589
  ctx: click.Context,
512
590
  file_path: str,
513
- api: Optional[str]
591
+ api: Optional[str],
592
+ interactive: bool
514
593
  ) -> None:
515
594
  """Review code for bugs and improvements."""
516
595
  console = ctx.obj['console']
@@ -521,6 +600,29 @@ def review(
521
600
 
522
601
  md = Markdown(result)
523
602
  console.print(md)
603
+
604
+ if interactive:
605
+ client = ClaudeClient(api_config_name=api)
606
+ with open(file_path, 'r') as f:
607
+ file_content = f.read()
608
+
609
+ console.print("\n[dim]Ask follow-up questions about the review, or 'exit' to quit[/dim]")
610
+
611
+ while True:
612
+ user_input = console.input("\n[cyan]You:[/cyan] ").strip()
613
+
614
+ if user_input.lower() == 'exit':
615
+ break
616
+
617
+ if not user_input:
618
+ continue
619
+
620
+ follow_up_prompt = f"Code review:\n\n{result}\n\nOriginal code:\n\n{file_content}\n\nUser question: {user_input}"
621
+
622
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
623
+ for chunk in client.call_streaming(follow_up_prompt):
624
+ console.print(chunk, end='')
625
+ console.print()
524
626
 
525
627
  except Exception as e:
526
628
  console.print(f"[red]Error: {e}[/red]")
@@ -566,12 +668,14 @@ def debug(
566
668
  @click.argument('file_path', type=click.Path(exists=True))
567
669
  @click.option('-o', '--output', type=click.Path(), help='Output file path')
568
670
  @click.option('-a', '--api', help='API config to use')
671
+ @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
569
672
  @click.pass_context
570
673
  def refactor(
571
674
  ctx: click.Context,
572
675
  file_path: str,
573
676
  output: Optional[str],
574
- api: Optional[str]
677
+ api: Optional[str],
678
+ interactive: bool
575
679
  ) -> None:
576
680
  """Suggest refactoring improvements."""
577
681
  console = ctx.obj['console']
@@ -580,11 +684,46 @@ def refactor(
580
684
  with console.status("[bold blue]Analyzing code..."):
581
685
  result = refactor_code(file_path, api_config_name=api)
582
686
 
687
+ if interactive:
688
+ console.print("\n[bold]Initial Refactoring:[/bold]\n")
689
+ md = Markdown(result)
690
+ console.print(md)
691
+
692
+ client = ClaudeClient(api_config_name=api)
693
+ conversation_context = [result]
694
+
695
+ while True:
696
+ console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
697
+ user_input = console.input("[cyan]You:[/cyan] ").strip()
698
+
699
+ if user_input.lower() == 'exit':
700
+ console.print("[yellow]Discarded changes[/yellow]")
701
+ return
702
+
703
+ if user_input.lower() == 'save':
704
+ result = conversation_context[-1]
705
+ break
706
+
707
+ if not user_input:
708
+ continue
709
+
710
+ refinement_prompt = f"Previous refactoring:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated refactoring suggestions."
711
+
712
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
713
+ response_parts = []
714
+ for chunk in client.call_streaming(refinement_prompt):
715
+ console.print(chunk, end='')
716
+ response_parts.append(chunk)
717
+ console.print()
718
+
719
+ result = ''.join(response_parts)
720
+ conversation_context.append(result)
721
+
583
722
  if output:
584
723
  with open(output, 'w') as f:
585
724
  f.write(result)
586
- console.print(f"[green]✓[/green] Refactored code saved to: {output}")
587
- else:
725
+ console.print(f"\n[green]✓[/green] Refactored code saved to: {output}")
726
+ elif not interactive:
588
727
  md = Markdown(result)
589
728
  console.print(md)
590
729
 
@@ -753,5 +892,424 @@ def toon_info(ctx: click.Context) -> None:
753
892
  console.print("• Same data, fewer tokens")
754
893
 
755
894
 
895
+ @main.group()
896
+ def template() -> None:
897
+ """Manage custom prompt templates."""
898
+ pass
899
+
900
+
901
+ @template.command('list')
902
+ @click.option('-c', '--category', help='Filter by category')
903
+ @click.option('--builtin', is_flag=True, help='Show only built-in templates')
904
+ @click.option('--user', is_flag=True, help='Show only user templates')
905
+ @click.pass_context
906
+ def template_list(
907
+ ctx: click.Context,
908
+ category: Optional[str],
909
+ builtin: bool,
910
+ user: bool
911
+ ) -> None:
912
+ """List available templates."""
913
+ console = ctx.obj['console']
914
+ config = Config()
915
+ manager = TemplateManager(config.config_dir)
916
+
917
+ templates = manager.list_templates(
918
+ category=category,
919
+ builtin_only=builtin,
920
+ user_only=user
921
+ )
922
+
923
+ if not templates:
924
+ console.print("[yellow]No templates found.[/yellow]")
925
+ return
926
+
927
+ from rich.table import Table
928
+
929
+ table = Table(show_header=True, header_style="bold magenta")
930
+ table.add_column("Name", style="cyan")
931
+ table.add_column("Category", style="green")
932
+ table.add_column("Variables", style="yellow")
933
+ table.add_column("Type", style="blue")
934
+ table.add_column("Description")
935
+
936
+ for tmpl in templates:
937
+ vars_display = ", ".join(tmpl.variables) if tmpl.variables else "-"
938
+ type_display = "🔒 Built-in" if tmpl.builtin else "📝 User"
939
+ table.add_row(
940
+ tmpl.name,
941
+ tmpl.category,
942
+ vars_display,
943
+ type_display,
944
+ tmpl.description
945
+ )
946
+
947
+ console.print(table)
948
+
949
+ # Show categories
950
+ categories = manager.get_categories()
951
+ console.print(f"\n[dim]Categories: {', '.join(categories)}[/dim]")
952
+
953
+
954
+ @template.command('show')
955
+ @click.argument('name')
956
+ @click.pass_context
957
+ def template_show(ctx: click.Context, name: str) -> None:
958
+ """Show template details."""
959
+ console = ctx.obj['console']
960
+ config = Config()
961
+ manager = TemplateManager(config.config_dir)
962
+
963
+ tmpl = manager.get_template(name)
964
+ if not tmpl:
965
+ console.print(f"[red]Template not found: {name}[/red]")
966
+ sys.exit(1)
967
+
968
+ console.print(Panel(
969
+ f"[bold]{tmpl.name}[/bold]\n\n"
970
+ f"[dim]{tmpl.description}[/dim]\n\n"
971
+ f"Category: [green]{tmpl.category}[/green]\n"
972
+ f"Type: {'🔒 Built-in' if tmpl.builtin else '📝 User'}\n"
973
+ f"Variables: [yellow]{', '.join(tmpl.variables) if tmpl.variables else 'None'}[/yellow]",
974
+ title="Template Info",
975
+ border_style="blue"
976
+ ))
977
+
978
+ console.print("\n[bold]Content:[/bold]\n")
979
+ console.print(Panel(tmpl.content, border_style="dim"))
980
+
981
+
982
+ @template.command('add')
983
+ @click.argument('name')
984
+ @click.option('-c', '--content', help='Template content (or use stdin)')
985
+ @click.option('-d', '--description', help='Template description')
986
+ @click.option('--category', default='general', help='Template category')
987
+ @click.pass_context
988
+ def template_add(
989
+ ctx: click.Context,
990
+ name: str,
991
+ content: Optional[str],
992
+ description: Optional[str],
993
+ category: str
994
+ ) -> None:
995
+ """Add a new template."""
996
+ console = ctx.obj['console']
997
+ config = Config()
998
+ manager = TemplateManager(config.config_dir)
999
+
1000
+ # Get content from stdin if not provided
1001
+ if not content:
1002
+ if sys.stdin.isatty():
1003
+ console.print("[yellow]Enter template content (Ctrl+D to finish):[/yellow]")
1004
+ content = sys.stdin.read().strip()
1005
+
1006
+ if not content:
1007
+ console.print("[red]Error: No content provided[/red]")
1008
+ sys.exit(1)
1009
+
1010
+ try:
1011
+ tmpl = Template(
1012
+ name=name,
1013
+ content=content,
1014
+ description=description,
1015
+ category=category
1016
+ )
1017
+ manager.add_template(tmpl)
1018
+
1019
+ console.print(f"[green]✓[/green] Template added: {name}")
1020
+ if tmpl.variables:
1021
+ console.print(f"[dim]Variables: {', '.join(tmpl.variables)}[/dim]")
1022
+
1023
+ except ValueError as e:
1024
+ console.print(f"[red]Error: {e}[/red]")
1025
+ sys.exit(1)
1026
+
1027
+
1028
+ @template.command('delete')
1029
+ @click.argument('name')
1030
+ @click.pass_context
1031
+ def template_delete(ctx: click.Context, name: str) -> None:
1032
+ """Delete a user template."""
1033
+ console = ctx.obj['console']
1034
+ config = Config()
1035
+ manager = TemplateManager(config.config_dir)
1036
+
1037
+ try:
1038
+ if manager.delete_template(name):
1039
+ console.print(f"[green]✓[/green] Template deleted: {name}")
1040
+ else:
1041
+ console.print(f"[red]Template not found: {name}[/red]")
1042
+ sys.exit(1)
1043
+
1044
+ except ValueError as e:
1045
+ console.print(f"[red]Error: {e}[/red]")
1046
+ sys.exit(1)
1047
+
1048
+
1049
+ @template.command('use')
1050
+ @click.argument('name')
1051
+ @click.option('-a', '--api', help='API config to use')
1052
+ @click.option('-m', '--model', help='Claude model to use')
1053
+ @click.pass_context
1054
+ def template_use(ctx: click.Context, name: str, api: Optional[str], model: Optional[str]) -> None:
1055
+ """Use a template with interactive variable input."""
1056
+ console = ctx.obj['console']
1057
+ config = Config()
1058
+ manager = TemplateManager(config.config_dir)
1059
+
1060
+ tmpl = manager.get_template(name)
1061
+ if not tmpl:
1062
+ console.print(f"[red]Template not found: {name}[/red]")
1063
+ sys.exit(1)
1064
+
1065
+ # Get variable values
1066
+ variables = {}
1067
+ if tmpl.variables:
1068
+ console.print(f"\n[bold]Template: {name}[/bold]")
1069
+ console.print(f"[dim]{tmpl.description}[/dim]\n")
1070
+
1071
+ for var in tmpl.variables:
1072
+ value = console.input(f"[cyan]{var}:[/cyan] ").strip()
1073
+ variables[var] = value
1074
+
1075
+ # Check for missing variables
1076
+ missing = tmpl.get_missing_variables(**variables)
1077
+ if missing:
1078
+ console.print(f"[red]Missing required variables: {', '.join(missing)}[/red]")
1079
+ sys.exit(1)
1080
+
1081
+ # Render template
1082
+ prompt = tmpl.render(**variables)
1083
+
1084
+ # Call Claude
1085
+ try:
1086
+ client = ClaudeClient(api_config_name=api)
1087
+
1088
+ console.print("\n[bold green]Claude:[/bold green] ", end='')
1089
+ for chunk in client.call_streaming(prompt, model=model):
1090
+ console.print(chunk, end='')
1091
+ console.print()
1092
+
1093
+ except Exception as e:
1094
+ console.print(f"\n[red]Error: {e}[/red]")
1095
+ sys.exit(1)
1096
+
1097
+
1098
+ @main.group()
1099
+ def warp() -> None:
1100
+ """Warp terminal integration."""
1101
+ pass
1102
+
1103
+
1104
+ @warp.command('export-workflows')
1105
+ @click.option('-o', '--output', type=click.Path(), help='Output directory')
1106
+ @click.pass_context
1107
+ def warp_export_workflows(ctx: click.Context, output: Optional[str]) -> None:
1108
+ """Export Warp workflows for claude-dev-cli commands."""
1109
+ console = ctx.obj['console']
1110
+
1111
+ try:
1112
+ from claude_dev_cli.warp_integration import export_builtin_workflows
1113
+
1114
+ config = Config()
1115
+ output_dir = Path(output) if output else config.config_dir / "warp" / "workflows"
1116
+
1117
+ created_files = export_builtin_workflows(output_dir)
1118
+
1119
+ console.print(f"[green]✓[/green] Exported {len(created_files)} Warp workflows to:")
1120
+ console.print(f" {output_dir}")
1121
+ console.print("\n[bold]Workflows:[/bold]")
1122
+ for file in created_files:
1123
+ console.print(f" • {file.name}")
1124
+
1125
+ except Exception as e:
1126
+ console.print(f"[red]Error: {e}[/red]")
1127
+ sys.exit(1)
1128
+
1129
+
1130
+ @warp.command('export-launch-configs')
1131
+ @click.option('-o', '--output', type=click.Path(), help='Output file path')
1132
+ @click.pass_context
1133
+ def warp_export_launch_configs(ctx: click.Context, output: Optional[str]) -> None:
1134
+ """Export Warp launch configurations."""
1135
+ console = ctx.obj['console']
1136
+
1137
+ try:
1138
+ from claude_dev_cli.warp_integration import export_launch_configs
1139
+
1140
+ config = Config()
1141
+ output_path = Path(output) if output else config.config_dir / "warp" / "launch_configs.json"
1142
+
1143
+ export_launch_configs(output_path)
1144
+
1145
+ console.print(f"[green]✓[/green] Exported Warp launch configurations to:")
1146
+ console.print(f" {output_path}")
1147
+
1148
+ except Exception as e:
1149
+ console.print(f"[red]Error: {e}[/red]")
1150
+ sys.exit(1)
1151
+
1152
+
1153
+ @main.group()
1154
+ def workflow() -> None:
1155
+ """Manage and run workflows."""
1156
+ pass
1157
+
1158
+
1159
+ @workflow.command('run')
1160
+ @click.argument('workflow_file', type=click.Path(exists=True))
1161
+ @click.option('--var', '-v', multiple=True, help='Set variables (key=value)')
1162
+ @click.pass_context
1163
+ def workflow_run(
1164
+ ctx: click.Context,
1165
+ workflow_file: str,
1166
+ var: tuple
1167
+ ) -> None:
1168
+ """Run a workflow from YAML file."""
1169
+ console = ctx.obj['console']
1170
+
1171
+ # Parse variables
1172
+ variables = {}
1173
+ for v in var:
1174
+ if '=' in v:
1175
+ key, value = v.split('=', 1)
1176
+ variables[key] = value
1177
+
1178
+ try:
1179
+ from claude_dev_cli.workflows import WorkflowEngine
1180
+
1181
+ engine = WorkflowEngine(console=console)
1182
+ workflow_path = Path(workflow_file)
1183
+
1184
+ context = engine.execute(workflow_path, initial_vars=variables)
1185
+
1186
+ # Show summary
1187
+ if context.step_results:
1188
+ console.print("\n[bold]Results Summary:[/bold]")
1189
+ for step_name, result in context.step_results.items():
1190
+ status = "[green]✓[/green]" if result.success else "[red]✗[/red]"
1191
+ console.print(f"{status} {step_name}")
1192
+
1193
+ except Exception as e:
1194
+ console.print(f"[red]Error: {e}[/red]")
1195
+ sys.exit(1)
1196
+
1197
+
1198
+ @workflow.command('list')
1199
+ @click.pass_context
1200
+ def workflow_list(ctx: click.Context) -> None:
1201
+ """List available workflows."""
1202
+ console = ctx.obj['console']
1203
+ config = Config()
1204
+ workflow_dir = config.config_dir / "workflows"
1205
+
1206
+ from claude_dev_cli.workflows import list_workflows
1207
+ workflows = list_workflows(workflow_dir)
1208
+
1209
+ if not workflows:
1210
+ console.print("[yellow]No workflows found.[/yellow]")
1211
+ console.print(f"\nCreate workflows in: {workflow_dir}")
1212
+ return
1213
+
1214
+ from rich.table import Table
1215
+
1216
+ table = Table(show_header=True, header_style="bold magenta")
1217
+ table.add_column("Name", style="cyan")
1218
+ table.add_column("Steps", style="yellow")
1219
+ table.add_column("Description")
1220
+
1221
+ for wf in workflows:
1222
+ table.add_row(
1223
+ wf['name'],
1224
+ str(wf['steps']),
1225
+ wf['description']
1226
+ )
1227
+
1228
+ console.print(table)
1229
+ console.print(f"\n[dim]Workflow directory: {workflow_dir}[/dim]")
1230
+
1231
+
1232
+ @workflow.command('show')
1233
+ @click.argument('workflow_file', type=click.Path(exists=True))
1234
+ @click.pass_context
1235
+ def workflow_show(ctx: click.Context, workflow_file: str) -> None:
1236
+ """Show workflow details."""
1237
+ console = ctx.obj['console']
1238
+
1239
+ try:
1240
+ from claude_dev_cli.workflows import WorkflowEngine
1241
+
1242
+ engine = WorkflowEngine(console=console)
1243
+ workflow = engine.load_workflow(Path(workflow_file))
1244
+
1245
+ console.print(Panel(
1246
+ f"[bold]{workflow.get('name', 'Unnamed')}[/bold]\n\n"
1247
+ f"[dim]{workflow.get('description', 'No description')}[/dim]\n\n"
1248
+ f"Steps: [yellow]{len(workflow.get('steps', []))}[/yellow]",
1249
+ title="Workflow Info",
1250
+ border_style="blue"
1251
+ ))
1252
+
1253
+ # Show steps
1254
+ steps = workflow.get('steps', [])
1255
+ if steps:
1256
+ console.print("\n[bold]Steps:[/bold]\n")
1257
+ for i, step in enumerate(steps, 1):
1258
+ step_name = step.get('name', f'step-{i}')
1259
+ step_type = 'command' if 'command' in step else 'shell' if 'shell' in step else 'set'
1260
+ console.print(f" {i}. [cyan]{step_name}[/cyan] ({step_type})")
1261
+
1262
+ if step.get('approval_required'):
1263
+ console.print(f" [yellow]⚠ Requires approval[/yellow]")
1264
+ if 'if' in step:
1265
+ console.print(f" [dim]Condition: {step['if']}[/dim]")
1266
+
1267
+ except Exception as e:
1268
+ console.print(f"[red]Error: {e}[/red]")
1269
+ sys.exit(1)
1270
+
1271
+
1272
+ @workflow.command('validate')
1273
+ @click.argument('workflow_file', type=click.Path(exists=True))
1274
+ @click.pass_context
1275
+ def workflow_validate(ctx: click.Context, workflow_file: str) -> None:
1276
+ """Validate workflow syntax."""
1277
+ console = ctx.obj['console']
1278
+
1279
+ try:
1280
+ from claude_dev_cli.workflows import WorkflowEngine
1281
+
1282
+ engine = WorkflowEngine(console=console)
1283
+ workflow = engine.load_workflow(Path(workflow_file))
1284
+
1285
+ # Basic validation
1286
+ errors = []
1287
+
1288
+ if 'name' not in workflow:
1289
+ errors.append("Missing 'name' field")
1290
+
1291
+ if 'steps' not in workflow:
1292
+ errors.append("Missing 'steps' field")
1293
+ elif not isinstance(workflow['steps'], list):
1294
+ errors.append("'steps' must be a list")
1295
+ else:
1296
+ for i, step in enumerate(workflow['steps'], 1):
1297
+ if not any(k in step for k in ['command', 'shell', 'set']):
1298
+ errors.append(f"Step {i}: Must have 'command', 'shell', or 'set'")
1299
+
1300
+ if errors:
1301
+ console.print("[red]✗ Validation failed:[/red]\n")
1302
+ for error in errors:
1303
+ console.print(f" • {error}")
1304
+ sys.exit(1)
1305
+ else:
1306
+ console.print(f"[green]✓[/green] Workflow is valid: {workflow.get('name')}")
1307
+ console.print(f" Steps: {len(workflow.get('steps', []))}")
1308
+
1309
+ except Exception as e:
1310
+ console.print(f"[red]Error: {e}[/red]")
1311
+ sys.exit(1)
1312
+
1313
+
756
1314
  if __name__ == '__main__':
757
1315
  main(obj={})