cl-preset 0.1.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (24) hide show
  1. cl_preset-0.3.0/.moai/config/sections/git-strategy.yaml +46 -0
  2. {cl_preset-0.1.0 → cl_preset-0.3.0}/PKG-INFO +22 -1
  3. {cl_preset-0.1.0 → cl_preset-0.3.0}/README.md +20 -0
  4. {cl_preset-0.1.0 → cl_preset-0.3.0}/pyproject.toml +2 -1
  5. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/cli.py +60 -42
  6. cl_preset-0.3.0/src/cl_preset/interactive.py +384 -0
  7. {cl_preset-0.1.0 → cl_preset-0.3.0}/.gitignore +0 -0
  8. {cl_preset-0.1.0 → cl_preset-0.3.0}/.python-version +0 -0
  9. {cl_preset-0.1.0 → cl_preset-0.3.0}/CHANGELOG.md +0 -0
  10. {cl_preset-0.1.0 → cl_preset-0.3.0}/LICENSE +0 -0
  11. {cl_preset-0.1.0 → cl_preset-0.3.0}/MANIFEST.in +0 -0
  12. {cl_preset-0.1.0 → cl_preset-0.3.0}/examples/presets/web-development-strategy/README.md +0 -0
  13. {cl_preset-0.1.0 → cl_preset-0.3.0}/examples/presets/web-development-strategy/agents/expert-web-backend.md +0 -0
  14. {cl_preset-0.1.0 → cl_preset-0.3.0}/examples/presets/web-development-strategy/skills/web-dev-skills.md +0 -0
  15. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/__init__.py +0 -0
  16. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/git_strategy.py +0 -0
  17. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/manager.py +0 -0
  18. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/models.py +0 -0
  19. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/py.typed +0 -0
  20. {cl_preset-0.1.0 → cl_preset-0.3.0}/src/cl_preset/templates/git_strategy_template.yaml +0 -0
  21. {cl_preset-0.1.0 → cl_preset-0.3.0}/tests/__init__.py +0 -0
  22. {cl_preset-0.1.0 → cl_preset-0.3.0}/tests/test_cli.py +0 -0
  23. {cl_preset-0.1.0 → cl_preset-0.3.0}/tests/test_manager.py +0 -0
  24. {cl_preset-0.1.0 → cl_preset-0.3.0}/tests/test_models.py +0 -0
@@ -0,0 +1,46 @@
1
+ name: dual-repo
2
+ strategy_type: dual-repo
3
+ description: Dev/Release repository separation workflow. Dev repo for active development,
4
+ release repo for stable releases.
5
+ main_branch: main
6
+ develop_branch: develop
7
+ branch_patterns:
8
+ feature: feature/*
9
+ bugfix: bugfix/*
10
+ release: release/*
11
+ hotfix: hotfix/*
12
+ merge_rules:
13
+ require_pr: true
14
+ require_approval: true
15
+ allow_squash: true
16
+ allow_merge_commit: false
17
+ allow_rebase: false
18
+ protection_rules:
19
+ main:
20
+ enabled: true
21
+ require_status_checks: true
22
+ require_branch_up_to_date: true
23
+ required_check_names:
24
+ - ci
25
+ - tests
26
+ - lint
27
+ allow_force_pushes: false
28
+ allow_deletions: false
29
+ release/*:
30
+ enabled: true
31
+ require_status_checks: true
32
+ require_branch_up_to_date: true
33
+ required_check_names:
34
+ - ci
35
+ - tests
36
+ allow_force_pushes: false
37
+ allow_deletions: true
38
+ dev_repository:
39
+ remote: origin
40
+ branch_pattern: feature/*
41
+ description: Active development, feature branches, experimental work
42
+ release_repository:
43
+ remote: release
44
+ protected_branches:
45
+ - main
46
+ description: Production-ready code, stable releases, published documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cl-preset
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: A package for managing Claude Code configuration presets and strategies
5
5
  Project-URL: Homepage, https://github.com/yarang/cl-preset
6
6
  Project-URL: Documentation, https://github.com/yarang/cl-preset#readme
@@ -13,6 +13,7 @@ Requires-Python: >=3.10
13
13
  Requires-Dist: click>=8.1.0
14
14
  Requires-Dist: pydantic>=2.0.0
15
15
  Requires-Dist: pyyaml>=6.0.0
16
+ Requires-Dist: questionary>=2.0.0
16
17
  Requires-Dist: rich>=13.0.0
17
18
  Provides-Extra: dev
18
19
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -52,6 +53,26 @@ uv pip install cl-preset
52
53
 
53
54
  ## Quick Start
54
55
 
56
+ ### Interactive Mode (Recommended)
57
+
58
+ The easiest way to get started is the interactive mode:
59
+
60
+ ```bash
61
+ cl-preset
62
+ ```
63
+
64
+ This will launch an interactive selector that guides you through:
65
+ 1. **Welcome Screen** - Beautiful introduction to cl-preset
66
+ 2. **Strategy Selection** - Browse and select from available strategies (presets and git strategies)
67
+ 3. **Scope Selection** - Choose installation scope (global, user, or project)
68
+ 4. **Installation** - Automatic installation with progress feedback
69
+
70
+ Available strategies include:
71
+ - **web-development-strategy** - Preset for web development with FastAPI and React
72
+ - **dual-repo-git-strategy** - Dev/Release repository separation workflow
73
+ - **github-flow-git-strategy** - Simple branch-based workflow
74
+ - **git-flow-git-strategy** - Feature/release/hotfix branching model
75
+
55
76
  ### Create a New Preset
56
77
 
57
78
  ```bash
@@ -29,6 +29,26 @@ uv pip install cl-preset
29
29
 
30
30
  ## Quick Start
31
31
 
32
+ ### Interactive Mode (Recommended)
33
+
34
+ The easiest way to get started is the interactive mode:
35
+
36
+ ```bash
37
+ cl-preset
38
+ ```
39
+
40
+ This will launch an interactive selector that guides you through:
41
+ 1. **Welcome Screen** - Beautiful introduction to cl-preset
42
+ 2. **Strategy Selection** - Browse and select from available strategies (presets and git strategies)
43
+ 3. **Scope Selection** - Choose installation scope (global, user, or project)
44
+ 4. **Installation** - Automatic installation with progress feedback
45
+
46
+ Available strategies include:
47
+ - **web-development-strategy** - Preset for web development with FastAPI and React
48
+ - **dual-repo-git-strategy** - Dev/Release repository separation workflow
49
+ - **github-flow-git-strategy** - Simple branch-based workflow
50
+ - **git-flow-git-strategy** - Feature/release/hotfix branching model
51
+
32
52
  ### Create a New Preset
33
53
 
34
54
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cl-preset"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "A package for managing Claude Code configuration presets and strategies"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "pydantic>=2.0.0",
14
14
  "rich>=13.0.0",
15
15
  "pyyaml>=6.0.0",
16
+ "questionary>=2.0.0",
16
17
  ]
17
18
 
18
19
  [project.optional-dependencies]
@@ -10,17 +10,25 @@ from rich.console import Console
10
10
  from rich.panel import Panel
11
11
 
12
12
  from cl_preset.git_strategy import GitStrategyManager
13
+ from cl_preset.interactive import InteractiveCLI
13
14
  from cl_preset.manager import PresetManager
14
15
  from cl_preset.models import PresetMetadata, PresetScope, PresetType
15
16
 
16
17
  console = Console()
17
18
 
18
19
 
19
- @click.group()
20
+ @click.group(invoke_without_command=True)
20
21
  @click.version_option(version="0.1.0", prog_name="cl-preset")
21
- def main() -> None:
22
- """cl-preset: Manage Claude Code configuration presets and strategies."""
23
- pass
22
+ @click.pass_context
23
+ def main(ctx: click.Context) -> None:
24
+ """cl-preset: Manage Claude Code configuration presets and strategies.
25
+
26
+ Running without a command starts the interactive strategy selector.
27
+ """
28
+ # If no subcommand is provided, run interactive mode
29
+ if ctx.invoked_subcommand is None:
30
+ interactive = InteractiveCLI()
31
+ interactive.run()
24
32
 
25
33
 
26
34
  @main.command()
@@ -29,22 +37,22 @@ def main() -> None:
29
37
  @click.option("--version", "-v", default="1.0.0", help="Version (default: 1.0.0)")
30
38
  @click.option("--description", "-d", required=True, help="Description of the preset")
31
39
  @click.option("--author", "-a", default="", help="Author name")
32
- @click.option("--type", "-t",
33
- type=click.Choice(["agent", "skill", "command", "strategy"]),
34
- default="strategy",
35
- help="Type of preset (default: strategy)")
36
- @click.option("--scope", "-s",
37
- type=click.Choice(["global", "user", "project"]),
38
- default="user",
39
- help="Installation scope (default: user)")
40
+ @click.option(
41
+ "--type",
42
+ "-t",
43
+ type=click.Choice(["agent", "skill", "command", "strategy"]),
44
+ default="strategy",
45
+ help="Type of preset (default: strategy)",
46
+ )
47
+ @click.option(
48
+ "--scope",
49
+ "-s",
50
+ type=click.Choice(["global", "user", "project"]),
51
+ default="user",
52
+ help="Installation scope (default: user)",
53
+ )
40
54
  def install(
41
- source_path: Path,
42
- name: str,
43
- version: str,
44
- description: str,
45
- author: str,
46
- type: str,
47
- scope: str
55
+ source_path: Path, name: str, version: str, description: str, author: str, type: str, scope: str
48
56
  ) -> None:
49
57
  """Install a preset from a source directory.
50
58
 
@@ -60,13 +68,11 @@ def install(
60
68
  version=version,
61
69
  description=description,
62
70
  author=author,
63
- preset_type=PresetType(type)
71
+ preset_type=PresetType(type),
64
72
  )
65
73
 
66
74
  preset = manager.create_from_directory(
67
- source_path=source_path,
68
- metadata=metadata,
69
- scope=PresetScope(scope)
75
+ source_path=source_path, metadata=metadata, scope=PresetScope(scope)
70
76
  )
71
77
 
72
78
  manager.install(preset)
@@ -74,9 +80,9 @@ def install(
74
80
 
75
81
  @main.command()
76
82
  @click.argument("name")
77
- @click.option("--scope", "-s",
78
- type=click.Choice(["global", "user", "project"]),
79
- help="Filter by scope")
83
+ @click.option(
84
+ "--scope", "-s", type=click.Choice(["global", "user", "project"]), help="Filter by scope"
85
+ )
80
86
  def uninstall(name: str, scope: str | None) -> None:
81
87
  """Uninstall a preset by name.
82
88
 
@@ -91,9 +97,9 @@ def uninstall(name: str, scope: str | None) -> None:
91
97
 
92
98
 
93
99
  @main.command()
94
- @click.option("--scope", "-s",
95
- type=click.Choice(["global", "user", "project"]),
96
- help="Filter by scope")
100
+ @click.option(
101
+ "--scope", "-s", type=click.Choice(["global", "user", "project"]), help="Filter by scope"
102
+ )
97
103
  @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
98
104
  def list_cmd(scope: str | None, json_output: bool) -> None:
99
105
  """List installed presets.
@@ -114,9 +120,13 @@ def list_cmd(scope: str | None, json_output: bool) -> None:
114
120
 
115
121
  @main.command()
116
122
  @click.argument("name")
117
- @click.option("--output", "-o", type=click.Path(path_type=Path),
118
- default=None,
119
- help="Output path for preset metadata")
123
+ @click.option(
124
+ "--output",
125
+ "-o",
126
+ type=click.Path(path_type=Path),
127
+ default=None,
128
+ help="Output path for preset metadata",
129
+ )
120
130
  def init(name: str, output: Path | None) -> None:
121
131
  """Initialize a new preset scaffold.
122
132
 
@@ -177,14 +187,16 @@ Add your name here
177
187
  "author": "",
178
188
  "license": "MIT",
179
189
  "preset_type": "strategy",
180
- "tags": []
190
+ "tags": [],
181
191
  }
182
192
  output.write_text(json.dumps(metadata, indent=2))
183
193
 
184
194
  console.print(f"[green]Created preset scaffold at {preset_dir}[/green]")
185
195
  console.print(f"[cyan]Metadata written to {output}[/cyan]")
186
196
  console.print("\nEdit the metadata and preset files, then install with:")
187
- console.print(f" cl-preset install {preset_dir} --name {name} --description 'Your description'")
197
+ console.print(
198
+ f" cl-preset install {preset_dir} --name {name} --description 'Your description'"
199
+ )
188
200
 
189
201
 
190
202
  @main.command()
@@ -236,6 +248,7 @@ def info(name: str) -> None:
236
248
  # Git Strategy Commands
237
249
  # ============================================================================
238
250
 
251
+
239
252
  @click.group()
240
253
  def git() -> None:
241
254
  """Git strategy management commands."""
@@ -274,7 +287,8 @@ def git_info(name: str) -> None:
274
287
 
275
288
  # Handle strategy_type being either a string or enum (due to use_enum_values=True)
276
289
  strategy_type = (
277
- strategy.strategy_type if isinstance(strategy.strategy_type, str)
290
+ strategy.strategy_type
291
+ if isinstance(strategy.strategy_type, str)
278
292
  else strategy.strategy_type.value
279
293
  )
280
294
 
@@ -327,8 +341,13 @@ def git_info(name: str) -> None:
327
341
 
328
342
  @git.command("export")
329
343
  @click.argument("name")
330
- @click.option("--output", "-o", type=click.Path(path_type=Path), default=None,
331
- help="Output path (default: ./<name>.yaml)")
344
+ @click.option(
345
+ "--output",
346
+ "-o",
347
+ type=click.Path(path_type=Path),
348
+ default=None,
349
+ help="Output path (default: ./<name>.yaml)",
350
+ )
332
351
  def git_export(name: str, output: Path | None) -> None:
333
352
  """Export a git strategy to YAML file.
334
353
 
@@ -395,13 +414,12 @@ def git_apply(name: str, dry_run: bool) -> None:
395
414
  # Check if we're in a git repository
396
415
  try:
397
416
  result = subprocess.run(
398
- ["git", "rev-parse", "--git-dir"],
399
- capture_output=True,
400
- text=True,
401
- cwd=Path.cwd()
417
+ ["git", "rev-parse", "--git-dir"], capture_output=True, text=True, cwd=Path.cwd()
402
418
  )
403
419
  if result.returncode != 0:
404
- console.print("[red]Not in a git repository. Initialize one first with 'git init'[/red]")
420
+ console.print(
421
+ "[red]Not in a git repository. Initialize one first with 'git init'[/red]"
422
+ )
405
423
  raise click.Abort()
406
424
 
407
425
  # Create .moai/config/sections directory
@@ -0,0 +1,384 @@
1
+ """
2
+ Interactive CLI module for cl-preset package.
3
+
4
+ Provides beautiful interactive prompts for strategy selection and installation.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ import questionary
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.text import Text
14
+
15
+ from cl_preset.git_strategy import GitStrategyManager
16
+ from cl_preset.manager import PresetManager
17
+ from cl_preset.models import PresetMetadata, PresetScope, PresetType
18
+
19
+ if TYPE_CHECKING:
20
+ pass
21
+
22
+
23
+ class InteractiveCLI:
24
+ """Interactive CLI for strategy selection and installation."""
25
+
26
+ def __init__(self) -> None:
27
+ """Initialize the interactive CLI."""
28
+ self.console = Console()
29
+ self.preset_manager = PresetManager()
30
+ self.git_manager = GitStrategyManager()
31
+
32
+ def show_welcome(self) -> None:
33
+ """Show welcome message with styled panel."""
34
+ welcome_text = Text()
35
+ welcome_text.append("Welcome to ", style="white")
36
+ welcome_text.append("cl-preset", style="bold cyan")
37
+ welcome_text.append("! ", style="white")
38
+ welcome_text.append("Interactive Strategy Management", style="dim")
39
+
40
+ panel = Panel(
41
+ welcome_text,
42
+ border_style="cyan",
43
+ padding=(1, 2),
44
+ )
45
+ self.console.print(panel)
46
+ self.console.print()
47
+
48
+ def get_available_strategies(self) -> list[dict]:
49
+ """
50
+ Get all available strategies (presets and git strategies).
51
+
52
+ Returns:
53
+ List of strategy dictionaries with name, type, and description
54
+ """
55
+ strategies = []
56
+
57
+ # Add built-in presets from examples directory
58
+ examples_path = Path(__file__).parent.parent.parent / "examples" / "presets"
59
+ if examples_path.exists():
60
+ for preset_dir in examples_path.iterdir():
61
+ if preset_dir.is_dir():
62
+ readme_path = preset_dir / "README.md"
63
+ description = ""
64
+ if readme_path.exists():
65
+ description = readme_path.read_text()
66
+ # Extract first line after title
67
+ for line in description.split("\n")[1:10]:
68
+ line = line.strip()
69
+ if line and not line.startswith("#"):
70
+ description = line
71
+ break
72
+
73
+ strategies.append(
74
+ {
75
+ "name": preset_dir.name,
76
+ "type": "preset",
77
+ "description": description or f"Preset from {preset_dir.name}",
78
+ "path": preset_dir,
79
+ }
80
+ )
81
+
82
+ # Add git strategies
83
+ git_strategies = self.git_manager.list_strategies(include_builtin=True)
84
+ for strategy in git_strategies:
85
+ strategies.append(
86
+ {
87
+ "name": strategy["name"],
88
+ "type": "git-strategy",
89
+ "description": strategy["description"],
90
+ "source": strategy.get("source", "builtin"),
91
+ }
92
+ )
93
+
94
+ return strategies
95
+
96
+ def select_strategies(self) -> list[dict] | None:
97
+ """
98
+ Show interactive multi-select strategy selection prompt.
99
+
100
+ Returns:
101
+ List of selected strategy dictionaries or None if cancelled
102
+ """
103
+ strategies = self.get_available_strategies()
104
+
105
+ if not strategies:
106
+ self.console.print("[yellow]No strategies available.[/yellow]")
107
+ return None
108
+
109
+ # Format choices for questionary
110
+ choices = []
111
+ for strategy in strategies:
112
+ type_label = "[Preset]" if strategy["type"] == "preset" else "[Git]"
113
+ choice = questionary.Choice(
114
+ title=f"{type_label} {strategy['name']}",
115
+ value=strategy["name"],
116
+ description=strategy["description"][:70] + "..."
117
+ if len(strategy["description"]) > 70
118
+ else strategy["description"],
119
+ )
120
+ choices.append(choice)
121
+
122
+ # Add instruction separator
123
+ choices.insert(0, questionary.Separator("= Use space to select, enter to continue ="))
124
+
125
+ selected_names = questionary.checkbox(
126
+ "Select strategies to install (multiple selection allowed):",
127
+ choices=choices,
128
+ qmark=">",
129
+ pointer=">",
130
+ validate=lambda x: len(x) > 0 or "At least one strategy must be selected",
131
+ ).ask()
132
+
133
+ if selected_names is None or len(selected_names) == 0:
134
+ return None
135
+
136
+ # Find the selected strategies
137
+ selected_strategies = []
138
+ for strategy in strategies:
139
+ if strategy["name"] in selected_names:
140
+ selected_strategies.append(strategy)
141
+
142
+ return selected_strategies
143
+
144
+ def select_scope(self) -> PresetScope | None:
145
+ """
146
+ Show interactive scope selection prompt.
147
+
148
+ Returns:
149
+ Selected PresetScope or None if cancelled
150
+ """
151
+ scope_choices = [
152
+ questionary.Choice(
153
+ title="global (System-wide installation)",
154
+ value="global",
155
+ description="Install for all users on the system",
156
+ ),
157
+ questionary.Choice(
158
+ title="user (User-level installation)",
159
+ value="user",
160
+ description="Install for current user only (recommended)",
161
+ ),
162
+ questionary.Choice(
163
+ title="project (Project-specific installation)",
164
+ value="project",
165
+ description="Install for current project only",
166
+ ),
167
+ questionary.Choice(title="Cancel", value="cancel"),
168
+ ]
169
+
170
+ selected_scope = questionary.select(
171
+ "Select installation scope:",
172
+ choices=scope_choices,
173
+ qmark=">",
174
+ pointer=">",
175
+ default="user",
176
+ ).ask()
177
+
178
+ if selected_scope == "cancel" or selected_scope is None:
179
+ return None
180
+
181
+ return PresetScope(selected_scope)
182
+
183
+ def install_preset(self, strategy: dict, scope: PresetScope) -> bool:
184
+ """
185
+ Install a preset strategy.
186
+
187
+ Args:
188
+ strategy: Strategy dictionary
189
+ scope: Installation scope
190
+
191
+ Returns:
192
+ True if successful, False otherwise
193
+ """
194
+ from cl_preset.manager import Preset
195
+
196
+ if "path" not in strategy:
197
+ self.console.print("[red]Strategy path not found.[/red]")
198
+ return False
199
+
200
+ source_path = strategy["path"]
201
+
202
+ # Try to read preset metadata
203
+ metadata_path = source_path / "preset.json"
204
+ if metadata_path.exists():
205
+ import json
206
+
207
+ metadata_data = json.loads(metadata_path.read_text())
208
+ metadata = PresetMetadata(**metadata_data)
209
+ else:
210
+ # Create metadata from directory name
211
+ metadata = PresetMetadata(
212
+ name=strategy["name"],
213
+ version="1.0.0",
214
+ description=strategy["description"],
215
+ preset_type=PresetType.STRATEGY,
216
+ )
217
+
218
+ preset = Preset(
219
+ config={
220
+ "metadata": metadata,
221
+ "source_path": source_path,
222
+ "scope": scope,
223
+ }
224
+ )
225
+
226
+ return self.preset_manager.install(preset)
227
+
228
+ def apply_git_strategy(self, strategy: dict, scope: PresetScope) -> bool:
229
+ """
230
+ Apply a git strategy.
231
+
232
+ Args:
233
+ strategy: Strategy dictionary
234
+ scope: Installation scope (for git strategies, affects where config is written)
235
+
236
+ Returns:
237
+ True if successful, False otherwise
238
+ """
239
+ import subprocess
240
+
241
+ name = strategy["name"]
242
+ git_strategy = self.git_manager.get_strategy(name)
243
+
244
+ if git_strategy is None:
245
+ self.console.print(f"[red]Strategy '{name}' not found.[/red]")
246
+ return False
247
+
248
+ self.console.print(f"[cyan]Applying git strategy: {git_strategy.name}[/cyan]")
249
+ self.console.print(f"[dim]{git_strategy.description}[/dim]")
250
+ self.console.print()
251
+
252
+ # Check if we're in a git repository
253
+ try:
254
+ result = subprocess.run(
255
+ ["git", "rev-parse", "--git-dir"],
256
+ capture_output=True,
257
+ text=True,
258
+ cwd=Path.cwd(),
259
+ )
260
+ if result.returncode != 0:
261
+ self.console.print(
262
+ "[red]Not in a git repository. Initialize one first with 'git init'[/red]"
263
+ )
264
+ return False
265
+
266
+ except Exception as e:
267
+ self.console.print(f"[red]Error checking git repository: {e}[/red]")
268
+ return False
269
+
270
+ # Create .moai/config/sections directory
271
+ config_dir = Path.cwd() / ".moai" / "config" / "sections"
272
+ config_dir.mkdir(parents=True, exist_ok=True)
273
+
274
+ # Export strategy config
275
+ strategy_file = config_dir / "git-strategy.yaml"
276
+ success = self.git_manager.export_strategy_yaml(name, strategy_file)
277
+
278
+ if success:
279
+ self.console.print()
280
+ self.console.print(
281
+ Panel(
282
+ f"[green]Strategy configuration written to {strategy_file}[/green]\n\n"
283
+ f"Main branch: [cyan]{git_strategy.main_branch}[/cyan]\n"
284
+ f"Feature pattern: [cyan]{git_strategy.branch_patterns.feature}[/cyan]",
285
+ title="Git Strategy Applied",
286
+ border_style="green",
287
+ )
288
+ )
289
+ return True
290
+
291
+ return False
292
+
293
+ def run(self) -> None:
294
+ """Run the interactive CLI flow."""
295
+ self.show_welcome()
296
+
297
+ # Step 1: Select strategies (multiple)
298
+ strategies = self.select_strategies()
299
+ if strategies is None or len(strategies) == 0:
300
+ self.console.print("[dim]Cancelled.[/dim]")
301
+ return
302
+
303
+ self.console.print()
304
+
305
+ # Step 2: Select scope
306
+ scope = self.select_scope()
307
+ if scope is None:
308
+ self.console.print("[dim]Cancelled.[/dim]")
309
+ return
310
+
311
+ self.console.print()
312
+
313
+ # Step 3: Install/Apply all selected strategies
314
+ results = []
315
+ for strategy in strategies:
316
+ strategy_type = strategy.get("type", "")
317
+ strategy_name = strategy["name"]
318
+
319
+ if strategy_type == "preset":
320
+ self.console.print(
321
+ f"[cyan]Installing {strategy_name} to {scope.value} scope...[/cyan]"
322
+ )
323
+ success = self.install_preset(strategy, scope)
324
+ else: # git-strategy
325
+ self.console.print(
326
+ f"[cyan]Applying git strategy {strategy_name} to {scope.value} scope...[/cyan]"
327
+ )
328
+ success = self.apply_git_strategy(strategy, scope)
329
+
330
+ results.append(
331
+ {
332
+ "name": strategy_name,
333
+ "type": strategy_type,
334
+ "success": success,
335
+ }
336
+ )
337
+ self.console.print()
338
+
339
+ # Step 4: Show summary
340
+ successful = [r for r in results if r["success"]]
341
+ failed = [r for r in results if not r["success"]]
342
+
343
+ if len(successful) == len(results):
344
+ # All successful
345
+ success_text = "[bold green]All strategies installed successfully![/bold green]\n\n"
346
+ for r in results:
347
+ success_text += f" [cyan]{r['name']}[/cyan] ([dim]{r['type']}[/dim])\n"
348
+
349
+ self.console.print(
350
+ Panel(
351
+ success_text,
352
+ title=f"Success ({len(successful)}/{len(results)})",
353
+ border_style="green",
354
+ )
355
+ )
356
+ elif len(successful) > 0:
357
+ # Partial success
358
+ summary_text = f"[bold yellow]Partial success: {len(successful)}/{len(results)} installed[/bold yellow]\n\n"
359
+
360
+ summary_text += "[green]Successfully installed:[/green]\n"
361
+ for r in successful:
362
+ summary_text += f" [cyan]{r['name']}[/cyan]\n"
363
+
364
+ summary_text += "\n[red]Failed to install:[/red]\n"
365
+ for r in failed:
366
+ summary_text += f" [cyan]{r['name']}[/cyan]\n"
367
+
368
+ self.console.print(
369
+ Panel(
370
+ summary_text,
371
+ title="Partial Success",
372
+ border_style="yellow",
373
+ )
374
+ )
375
+ else:
376
+ # All failed
377
+ self.console.print(
378
+ Panel(
379
+ "[bold red]All installations failed.[/bold red]\n\n"
380
+ "Please check the error messages above and try again.",
381
+ title="Error",
382
+ border_style="red",
383
+ )
384
+ )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes