cl-preset 0.1.0__py3-none-any.whl → 0.2.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.
cl_preset/cli.py CHANGED
@@ -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,347 @@
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_strategy(self) -> dict | None:
97
+ """
98
+ Show interactive strategy selection prompt.
99
+
100
+ Returns:
101
+ Selected strategy dictionary 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 cancel option
123
+ choices.append(questionary.Separator())
124
+ choices.append(questionary.Choice(title="Cancel", value="cancel"))
125
+
126
+ selected_name = questionary.select(
127
+ "Select a strategy to install:",
128
+ choices=choices,
129
+ qmark=">",
130
+ pointer=">",
131
+ ).ask()
132
+
133
+ if selected_name == "cancel" or selected_name is None:
134
+ return None
135
+
136
+ # Find the selected strategy
137
+ for strategy in strategies:
138
+ if strategy["name"] == selected_name:
139
+ return strategy
140
+
141
+ return None
142
+
143
+ def select_scope(self) -> PresetScope | None:
144
+ """
145
+ Show interactive scope selection prompt.
146
+
147
+ Returns:
148
+ Selected PresetScope or None if cancelled
149
+ """
150
+ scope_choices = [
151
+ questionary.Choice(
152
+ title="global (System-wide installation)",
153
+ value="global",
154
+ description="Install for all users on the system",
155
+ ),
156
+ questionary.Choice(
157
+ title="user (User-level installation)",
158
+ value="user",
159
+ description="Install for current user only (recommended)",
160
+ ),
161
+ questionary.Choice(
162
+ title="project (Project-specific installation)",
163
+ value="project",
164
+ description="Install for current project only",
165
+ ),
166
+ questionary.Choice(title="Cancel", value="cancel"),
167
+ ]
168
+
169
+ selected_scope = questionary.select(
170
+ "Select installation scope:",
171
+ choices=scope_choices,
172
+ qmark=">",
173
+ pointer=">",
174
+ default="user",
175
+ ).ask()
176
+
177
+ if selected_scope == "cancel" or selected_scope is None:
178
+ return None
179
+
180
+ return PresetScope(selected_scope)
181
+
182
+ def install_preset(self, strategy: dict, scope: PresetScope) -> bool:
183
+ """
184
+ Install a preset strategy.
185
+
186
+ Args:
187
+ strategy: Strategy dictionary
188
+ scope: Installation scope
189
+
190
+ Returns:
191
+ True if successful, False otherwise
192
+ """
193
+ from cl_preset.manager import Preset
194
+
195
+ if "path" not in strategy:
196
+ self.console.print("[red]Strategy path not found.[/red]")
197
+ return False
198
+
199
+ source_path = strategy["path"]
200
+
201
+ # Try to read preset metadata
202
+ metadata_path = source_path / "preset.json"
203
+ if metadata_path.exists():
204
+ import json
205
+
206
+ metadata_data = json.loads(metadata_path.read_text())
207
+ metadata = PresetMetadata(**metadata_data)
208
+ else:
209
+ # Create metadata from directory name
210
+ metadata = PresetMetadata(
211
+ name=strategy["name"],
212
+ version="1.0.0",
213
+ description=strategy["description"],
214
+ preset_type=PresetType.STRATEGY,
215
+ )
216
+
217
+ preset = Preset(
218
+ config={
219
+ "metadata": metadata,
220
+ "source_path": source_path,
221
+ "scope": scope,
222
+ }
223
+ )
224
+
225
+ return self.preset_manager.install(preset)
226
+
227
+ def apply_git_strategy(self, strategy: dict, scope: PresetScope) -> bool:
228
+ """
229
+ Apply a git strategy.
230
+
231
+ Args:
232
+ strategy: Strategy dictionary
233
+ scope: Installation scope (for git strategies, affects where config is written)
234
+
235
+ Returns:
236
+ True if successful, False otherwise
237
+ """
238
+ import subprocess
239
+
240
+ name = strategy["name"]
241
+ git_strategy = self.git_manager.get_strategy(name)
242
+
243
+ if git_strategy is None:
244
+ self.console.print(f"[red]Strategy '{name}' not found.[/red]")
245
+ return False
246
+
247
+ self.console.print(f"[cyan]Applying git strategy: {git_strategy.name}[/cyan]")
248
+ self.console.print(f"[dim]{git_strategy.description}[/dim]")
249
+ self.console.print()
250
+
251
+ # Check if we're in a git repository
252
+ try:
253
+ result = subprocess.run(
254
+ ["git", "rev-parse", "--git-dir"],
255
+ capture_output=True,
256
+ text=True,
257
+ cwd=Path.cwd(),
258
+ )
259
+ if result.returncode != 0:
260
+ self.console.print(
261
+ "[red]Not in a git repository. Initialize one first with 'git init'[/red]"
262
+ )
263
+ return False
264
+
265
+ except Exception as e:
266
+ self.console.print(f"[red]Error checking git repository: {e}[/red]")
267
+ return False
268
+
269
+ # Create .moai/config/sections directory
270
+ config_dir = Path.cwd() / ".moai" / "config" / "sections"
271
+ config_dir.mkdir(parents=True, exist_ok=True)
272
+
273
+ # Export strategy config
274
+ strategy_file = config_dir / "git-strategy.yaml"
275
+ success = self.git_manager.export_strategy_yaml(name, strategy_file)
276
+
277
+ if success:
278
+ self.console.print()
279
+ self.console.print(
280
+ Panel(
281
+ f"[green]Strategy configuration written to {strategy_file}[/green]\n\n"
282
+ f"Main branch: [cyan]{git_strategy.main_branch}[/cyan]\n"
283
+ f"Feature pattern: [cyan]{git_strategy.branch_patterns.feature}[/cyan]",
284
+ title="Git Strategy Applied",
285
+ border_style="green",
286
+ )
287
+ )
288
+ return True
289
+
290
+ return False
291
+
292
+ def run(self) -> None:
293
+ """Run the interactive CLI flow."""
294
+ self.show_welcome()
295
+
296
+ # Step 1: Select strategy
297
+ strategy = self.select_strategy()
298
+ if strategy is None:
299
+ self.console.print("[dim]Cancelled.[/dim]")
300
+ return
301
+
302
+ self.console.print()
303
+
304
+ # Step 2: Select scope
305
+ scope = self.select_scope()
306
+ if scope is None:
307
+ self.console.print("[dim]Cancelled.[/dim]")
308
+ return
309
+
310
+ self.console.print()
311
+
312
+ # Step 3: Install/Apply
313
+ strategy_type = strategy.get("type", "")
314
+ strategy_name = strategy["name"]
315
+
316
+ if strategy_type == "preset":
317
+ self.console.print(f"[cyan]Installing {strategy_name} to {scope.value} scope...[/cyan]")
318
+ success = self.install_preset(strategy, scope)
319
+ else: # git-strategy
320
+ self.console.print(
321
+ f"[cyan]Applying git strategy {strategy_name} to {scope.value} scope...[/cyan]"
322
+ )
323
+ success = self.apply_git_strategy(strategy, scope)
324
+
325
+ self.console.print()
326
+
327
+ # Step 4: Show result
328
+ if success:
329
+ self.console.print(
330
+ Panel(
331
+ "[bold green]Strategy installed successfully![/bold green]\n\n"
332
+ f"Type: [cyan]{strategy_type}[/cyan]\n"
333
+ f"Name: [cyan]{strategy_name}[/cyan]\n"
334
+ f"Scope: [cyan]{scope.value}[/cyan]",
335
+ title="Success",
336
+ border_style="green",
337
+ )
338
+ )
339
+ else:
340
+ self.console.print(
341
+ Panel(
342
+ "[bold red]Installation failed.[/bold red]\n\n"
343
+ "Please check the error messages above and try again.",
344
+ title="Error",
345
+ border_style="red",
346
+ )
347
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cl-preset
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -1,12 +1,13 @@
1
1
  cl_preset/__init__.py,sha256=rGIQS-1vuBzwnPj7NtC2N8hRIoMHUmSEMbWfnhjHjKQ,462
2
- cl_preset/cli.py,sha256=XldMCDe1-HEeX1IQQZU0J-2ECABpWLbq5yWXd-eIswY,15762
2
+ cl_preset/cli.py,sha256=f-WqvlfomDQbAaCmSfWLMtfs9KP-jm9E66NZEJC80OM,16003
3
3
  cl_preset/git_strategy.py,sha256=FuiNUMaTwAyNA5nS8EvLhAntldJOBgRMO5FrUUgJ1K4,17375
4
+ cl_preset/interactive.py,sha256=O20q7GLjNvtY-4voRsqHIBM_b484IYuxOu8InzviWh8,11509
4
5
  cl_preset/manager.py,sha256=OojhbTxXZ0cPt44PPZE1NqSBAXsGssB6ZuYyfoKwPa4,6192
5
6
  cl_preset/models.py,sha256=UWx68lb0ac3VFCRpj3zOqc7qGwJpbX2EZcw_OYHBKWg,2241
6
7
  cl_preset/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
8
  cl_preset/templates/git_strategy_template.yaml,sha256=2oF97WOHq2crSRoaLzWodBvzANmQ-9FPzM-A0jui2iw,1328
8
- cl_preset-0.1.0.dist-info/METADATA,sha256=Oon9IbfPw1sIDSGnLyYMVFv4-3Zi6wiwdcFTMDtIn38,8939
9
- cl_preset-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
- cl_preset-0.1.0.dist-info/entry_points.txt,sha256=JAW_u5lrqylE516FDGO5audUKuCsxGYypQ7d6cA_9uY,49
11
- cl_preset-0.1.0.dist-info/licenses/LICENSE,sha256=5b1_XulzMGBmVHcNKAHMo0Mqfqm5xJix4O5AErotK0o,1069
12
- cl_preset-0.1.0.dist-info/RECORD,,
9
+ cl_preset-0.2.0.dist-info/METADATA,sha256=ohb0hWBIjOvk0zXFQkBr7Uf8QCnl11u8AEzTuQwwEAo,9782
10
+ cl_preset-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ cl_preset-0.2.0.dist-info/entry_points.txt,sha256=JAW_u5lrqylE516FDGO5audUKuCsxGYypQ7d6cA_9uY,49
12
+ cl_preset-0.2.0.dist-info/licenses/LICENSE,sha256=5b1_XulzMGBmVHcNKAHMo0Mqfqm5xJix4O5AErotK0o,1069
13
+ cl_preset-0.2.0.dist-info/RECORD,,