cl-preset 0.1.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/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """
2
+ cl-preset: A package for managing Claude Code configuration presets and strategies.
3
+
4
+ This package allows users to package their Claude Code strategies and install them
5
+ with different scopes (global, project, etc.).
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from cl_preset.manager import PresetManager
11
+ from cl_preset.models import Preset, PresetConfig, PresetScope
12
+
13
+ __all__ = [
14
+ "__version__",
15
+ "Preset",
16
+ "PresetScope",
17
+ "PresetConfig",
18
+ "PresetManager",
19
+ ]
cl_preset/cli.py ADDED
@@ -0,0 +1,479 @@
1
+ """
2
+ CLI interface for cl-preset package.
3
+ """
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ from cl_preset.git_strategy import GitStrategyManager
13
+ from cl_preset.manager import PresetManager
14
+ from cl_preset.models import PresetMetadata, PresetScope, PresetType
15
+
16
+ console = Console()
17
+
18
+
19
+ @click.group()
20
+ @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
24
+
25
+
26
+ @main.command()
27
+ @click.argument("source_path", type=click.Path(exists=True, path_type=Path))
28
+ @click.option("--name", "-n", required=True, help="Unique name for the preset")
29
+ @click.option("--version", "-v", default="1.0.0", help="Version (default: 1.0.0)")
30
+ @click.option("--description", "-d", required=True, help="Description of the preset")
31
+ @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
+ def install(
41
+ source_path: Path,
42
+ name: str,
43
+ version: str,
44
+ description: str,
45
+ author: str,
46
+ type: str,
47
+ scope: str
48
+ ) -> None:
49
+ """Install a preset from a source directory.
50
+
51
+ SOURCE_PATH is the path to the directory containing the preset files.
52
+
53
+ Example:
54
+ cl-preset install ./my-strategy --name my-strategy --description "My custom strategy"
55
+ """
56
+ manager = PresetManager()
57
+
58
+ metadata = PresetMetadata(
59
+ name=name,
60
+ version=version,
61
+ description=description,
62
+ author=author,
63
+ preset_type=PresetType(type)
64
+ )
65
+
66
+ preset = manager.create_from_directory(
67
+ source_path=source_path,
68
+ metadata=metadata,
69
+ scope=PresetScope(scope)
70
+ )
71
+
72
+ manager.install(preset)
73
+
74
+
75
+ @main.command()
76
+ @click.argument("name")
77
+ @click.option("--scope", "-s",
78
+ type=click.Choice(["global", "user", "project"]),
79
+ help="Filter by scope")
80
+ def uninstall(name: str, scope: str | None) -> None:
81
+ """Uninstall a preset by name.
82
+
83
+ NAME is the name of the preset to uninstall.
84
+
85
+ Example:
86
+ cl-preset uninstall my-strategy
87
+ """
88
+ manager = PresetManager()
89
+ scope_enum = PresetScope(scope) if scope else None
90
+ manager.uninstall(name, scope_enum)
91
+
92
+
93
+ @main.command()
94
+ @click.option("--scope", "-s",
95
+ type=click.Choice(["global", "user", "project"]),
96
+ help="Filter by scope")
97
+ @click.option("--json", "json_output", is_flag=True, help="Output as JSON")
98
+ def list_cmd(scope: str | None, json_output: bool) -> None:
99
+ """List installed presets.
100
+
101
+ Example:
102
+ cl-preset list
103
+ cl-preset list --scope user
104
+ """
105
+ manager = PresetManager()
106
+ scope_enum = PresetScope(scope) if scope else None
107
+
108
+ if json_output:
109
+ presets = manager.list(scope_enum)
110
+ console.print_json(json.dumps(presets, indent=2))
111
+ else:
112
+ manager.print_list(scope_enum)
113
+
114
+
115
+ @main.command()
116
+ @click.argument("name")
117
+ @click.option("--output", "-o", type=click.Path(path_type=Path),
118
+ default=None,
119
+ help="Output path for preset metadata")
120
+ def init(name: str, output: Path | None) -> None:
121
+ """Initialize a new preset scaffold.
122
+
123
+ Creates a template directory structure for a new preset.
124
+
125
+ NAME is the name of the preset to create.
126
+
127
+ Example:
128
+ cl-preset init my-strategy
129
+ """
130
+ if output is None:
131
+ output = Path.cwd() / "preset.json"
132
+
133
+ preset_dir = Path.cwd() / name
134
+ preset_dir.mkdir(exist_ok=True)
135
+
136
+ # Create directory structure
137
+ (preset_dir / "agents").mkdir(exist_ok=True)
138
+ (preset_dir / "skills").mkdir(exist_ok=True)
139
+ (preset_dir / "commands").mkdir(exist_ok=True)
140
+ (preset_dir / "examples").mkdir(exist_ok=True)
141
+
142
+ # Create README template
143
+ readme_content = f"""# {name}
144
+
145
+ ## Description
146
+
147
+ Add your description here.
148
+
149
+ ## Installation
150
+
151
+ ```bash
152
+ cl-preset install . --name {name} --description "Your description"
153
+ ```
154
+
155
+ ## Structure
156
+
157
+ - `agents/` - Agent definitions
158
+ - `skills/` - Skill definitions
159
+ - `commands/` - Command definitions
160
+ - `examples/` - Usage examples
161
+
162
+ ## Usage
163
+
164
+ After installation, the preset will be available in your Claude Code environment.
165
+
166
+ ## Author
167
+
168
+ Add your name here
169
+ """
170
+ (preset_dir / "README.md").write_text(readme_content)
171
+
172
+ # Create preset.json metadata template
173
+ metadata = {
174
+ "name": name,
175
+ "version": "1.0.0",
176
+ "description": "Add your description here",
177
+ "author": "",
178
+ "license": "MIT",
179
+ "preset_type": "strategy",
180
+ "tags": []
181
+ }
182
+ output.write_text(json.dumps(metadata, indent=2))
183
+
184
+ console.print(f"[green]Created preset scaffold at {preset_dir}[/green]")
185
+ console.print(f"[cyan]Metadata written to {output}[/cyan]")
186
+ 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'")
188
+
189
+
190
+ @main.command()
191
+ @click.argument("name")
192
+ def info(name: str) -> None:
193
+ """Show detailed information about an installed preset.
194
+
195
+ NAME is the name of the preset.
196
+
197
+ Example:
198
+ cl-preset info my-strategy
199
+ """
200
+ manager = PresetManager()
201
+ registry = manager._load_registry()
202
+
203
+ installed = registry.get("installed", [])
204
+ preset_info = None
205
+
206
+ for preset in installed:
207
+ if preset["name"] == name:
208
+ preset_info = preset
209
+ break
210
+
211
+ if preset_info is None:
212
+ console.print(f"[red]Preset '{name}' not found.[/red]")
213
+ return
214
+
215
+ # Try to load detailed metadata
216
+ preset_path = Path(preset_info["path"])
217
+ metadata_path = preset_path / "preset.json"
218
+
219
+ if metadata_path.exists():
220
+ metadata = json.loads(metadata_path.read_text())
221
+ console.print(f"[cyan]Name:[/cyan] {metadata.get('name', 'N/A')}")
222
+ console.print(f"[cyan]Version:[/cyan] {metadata.get('version', 'N/A')}")
223
+ console.print(f"[cyan]Description:[/cyan] {metadata.get('description', 'N/A')}")
224
+ console.print(f"[cyan]Author:[/cyan] {metadata.get('author', 'N/A')}")
225
+ console.print(f"[cyan]License:[/cyan] {metadata.get('license', 'N/A')}")
226
+ console.print(f"[cyan]Type:[/cyan] {metadata.get('preset_type', 'N/A')}")
227
+ console.print(f"[cyan]Tags:[/cyan] {', '.join(metadata.get('tags', [])) or 'None'}")
228
+ else:
229
+ console.print(f"[cyan]Name:[/cyan] {preset_info['name']}")
230
+ console.print(f"[cyan]Version:[/cyan] {preset_info['version']}")
231
+ console.print(f"[cyan]Scope:[/cyan] {preset_info['scope']}")
232
+ console.print(f"[cyan]Path:[/cyan] {preset_info['path']}")
233
+
234
+
235
+ # ============================================================================
236
+ # Git Strategy Commands
237
+ # ============================================================================
238
+
239
+ @click.group()
240
+ def git() -> None:
241
+ """Git strategy management commands."""
242
+ pass
243
+
244
+
245
+ @git.command("list")
246
+ @click.option("--builtin/--no-builtin", default=True, help="Include built-in strategies")
247
+ def git_list(builtin: bool) -> None:
248
+ """List available git strategies.
249
+
250
+ Example:
251
+ cl-preset git list
252
+ cl-preset git list --no-builtin
253
+ """
254
+ manager = GitStrategyManager()
255
+ manager.print_list(include_builtin=builtin)
256
+
257
+
258
+ @git.command("info")
259
+ @click.argument("name")
260
+ def git_info(name: str) -> None:
261
+ """Show detailed information about a git strategy.
262
+
263
+ NAME is the name of the strategy.
264
+
265
+ Example:
266
+ cl-preset git info dual-repo
267
+ """
268
+ manager = GitStrategyManager()
269
+ strategy = manager.get_strategy(name)
270
+
271
+ if strategy is None:
272
+ console.print(f"[red]Strategy '{name}' not found.[/red]")
273
+ return
274
+
275
+ # Handle strategy_type being either a string or enum (due to use_enum_values=True)
276
+ strategy_type = (
277
+ strategy.strategy_type if isinstance(strategy.strategy_type, str)
278
+ else strategy.strategy_type.value
279
+ )
280
+
281
+ console.print(Panel.fit(f"[bold cyan]{strategy.name}[/bold cyan]", title="Strategy"))
282
+ console.print(f"[cyan]Type:[/cyan] {strategy_type}")
283
+ console.print(f"[cyan]Description:[/cyan] {strategy.description}")
284
+ console.print()
285
+
286
+ console.print("[bold]Branch Configuration:[/bold]")
287
+ console.print(f" Main Branch: [green]{strategy.main_branch}[/green]")
288
+ console.print(f" Develop Branch: [green]{strategy.develop_branch}[/green]")
289
+ console.print()
290
+
291
+ console.print("[bold]Branch Patterns:[/bold]")
292
+ console.print(f" Feature: [green]{strategy.branch_patterns.feature}[/green]")
293
+ console.print(f" Bugfix: [green]{strategy.branch_patterns.bugfix}[/green]")
294
+ console.print(f" Release: [green]{strategy.branch_patterns.release}[/green]")
295
+ console.print(f" Hotfix: [green]{strategy.branch_patterns.hotfix}[/green]")
296
+ console.print()
297
+
298
+ console.print("[bold]Merge Rules:[/bold]")
299
+ console.print(f" Require PR: [green]{strategy.merge_rules.require_pr}[/green]")
300
+ console.print(f" Require Approval: [green]{strategy.merge_rules.require_approval}[/green]")
301
+ console.print(f" Allow Squash: [green]{strategy.merge_rules.allow_squash}[/green]")
302
+ console.print(f" Allow Merge Commit: [green]{strategy.merge_rules.allow_merge_commit}[/green]")
303
+ console.print(f" Allow Rebase: [green]{strategy.merge_rules.allow_rebase}[/green]")
304
+ console.print()
305
+
306
+ if strategy.protection_rules:
307
+ console.print("[bold]Protection Rules:[/bold]")
308
+ for branch, rules in strategy.protection_rules.items():
309
+ status = "[green]enabled[/green]" if rules.enabled else "[red]disabled[/red]"
310
+ console.print(f" {branch}: {status}")
311
+ if rules.enabled:
312
+ if rules.required_check_names:
313
+ console.print(f" Required checks: {', '.join(rules.required_check_names)}")
314
+
315
+ if strategy.dev_repository:
316
+ console.print()
317
+ console.print("[bold]Dev Repository:[/bold]")
318
+ for key, value in strategy.dev_repository.items():
319
+ console.print(f" {key}: [green]{value}[/green]")
320
+
321
+ if strategy.release_repository:
322
+ console.print()
323
+ console.print("[bold]Release Repository:[/bold]")
324
+ for key, value in strategy.release_repository.items():
325
+ console.print(f" {key}: [green]{value}[/green]")
326
+
327
+
328
+ @git.command("export")
329
+ @click.argument("name")
330
+ @click.option("--output", "-o", type=click.Path(path_type=Path), default=None,
331
+ help="Output path (default: ./<name>.yaml)")
332
+ def git_export(name: str, output: Path | None) -> None:
333
+ """Export a git strategy to YAML file.
334
+
335
+ NAME is the name of the strategy to export.
336
+
337
+ Example:
338
+ cl-preset git export dual-repo
339
+ cl-preset git export dual-repo -o ./my-strategy.yaml
340
+ """
341
+ manager = GitStrategyManager()
342
+
343
+ if output is None:
344
+ output = Path.cwd() / f"{name}.yaml"
345
+
346
+ success = manager.export_strategy_yaml(name, output)
347
+ if not success:
348
+ raise click.ClickException(f"Failed to export strategy '{name}'")
349
+
350
+
351
+ @git.command("apply")
352
+ @click.argument("name")
353
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
354
+ def git_apply(name: str, dry_run: bool) -> None:
355
+ """Apply a git strategy to the current project.
356
+
357
+ NAME is the name of the strategy to apply.
358
+
359
+ Example:
360
+ cl-preset git apply github-flow
361
+ cl-preset git apply github-flow --dry-run
362
+ """
363
+ import subprocess
364
+
365
+ manager = GitStrategyManager()
366
+ strategy = manager.get_strategy(name)
367
+
368
+ if strategy is None:
369
+ console.print(f"[red]Strategy '{name}' not found.[/red]")
370
+ raise click.Abort()
371
+
372
+ console.print(f"[cyan]Applying strategy: {strategy.name}[/cyan]")
373
+ console.print(f"[dim]{strategy.description}[/dim]")
374
+ console.print()
375
+
376
+ if dry_run:
377
+ console.print("[yellow]Dry run mode - no changes will be made[/yellow]")
378
+ console.print()
379
+
380
+ # Show what would be configured
381
+ console.print("[bold]Branch Configuration:[/bold]")
382
+ console.print(f" Main branch: [green]{strategy.main_branch}[/green]")
383
+ if strategy.develop_branch:
384
+ console.print(f" Develop branch: [green]{strategy.develop_branch}[/green]")
385
+ console.print()
386
+
387
+ console.print("[bold]Branch Patterns:[/bold]")
388
+ console.print(f" Feature: [green]{strategy.branch_patterns.feature}[/green]")
389
+ console.print(f" Bugfix: [green]{strategy.branch_patterns.bugfix}[/green]")
390
+ console.print(f" Release: [green]{strategy.branch_patterns.release}[/green]")
391
+ console.print(f" Hotfix: [green]{strategy.branch_patterns.hotfix}[/green]")
392
+ console.print()
393
+
394
+ if not dry_run:
395
+ # Check if we're in a git repository
396
+ try:
397
+ result = subprocess.run(
398
+ ["git", "rev-parse", "--git-dir"],
399
+ capture_output=True,
400
+ text=True,
401
+ cwd=Path.cwd()
402
+ )
403
+ if result.returncode != 0:
404
+ console.print("[red]Not in a git repository. Initialize one first with 'git init'[/red]")
405
+ raise click.Abort()
406
+
407
+ # Create .moai/config/sections directory
408
+ config_dir = Path.cwd() / ".moai" / "config" / "sections"
409
+ config_dir.mkdir(parents=True, exist_ok=True)
410
+
411
+ # Export strategy config
412
+ strategy_file = config_dir / "git-strategy.yaml"
413
+ success = manager.export_strategy_yaml(name, strategy_file)
414
+
415
+ if success:
416
+ console.print(f"[green]Strategy configuration written to {strategy_file}[/green]")
417
+ console.print()
418
+ console.print("[bold]Next Steps:[/bold]")
419
+ console.print(" 1. Review the strategy configuration")
420
+ console.print(" 2. Create required branches if needed")
421
+ console.print(" 3. Configure branch protection rules in your Git host")
422
+
423
+ except Exception as e:
424
+ console.print(f"[red]Error applying strategy: {e}[/red]")
425
+ raise click.Abort()
426
+
427
+
428
+ @git.command("validate")
429
+ @click.argument("name")
430
+ def git_validate(name: str) -> None:
431
+ """Validate current git setup against a strategy.
432
+
433
+ NAME is the name of the expected strategy.
434
+
435
+ Example:
436
+ cl-preset git validate github-flow
437
+ """
438
+ manager = GitStrategyManager()
439
+ results = manager.validate_current_setup(name)
440
+
441
+ console.print(f"[cyan]Validating against strategy: {name}[/cyan]")
442
+ console.print()
443
+
444
+ if results.get("info"):
445
+ info = results["info"]
446
+ if "current_branch" in info:
447
+ console.print(f"[dim]Current branch: {info['current_branch']}[/dim]")
448
+
449
+ console.print()
450
+
451
+ if results["valid"]:
452
+ console.print("[green]Validation passed![/green]")
453
+ else:
454
+ console.print("[red]Validation failed![/red]")
455
+
456
+ if results["errors"]:
457
+ console.print()
458
+ console.print("[bold red]Errors:[/bold red]")
459
+ for error in results["errors"]:
460
+ console.print(f" [red]x[/red] {error}")
461
+
462
+ if results["warnings"]:
463
+ console.print()
464
+ console.print("[bold yellow]Warnings:[/bold yellow]")
465
+ for warning in results["warnings"]:
466
+ console.print(f" [yellow]![/yellow] {warning}")
467
+
468
+ if not results["errors"] and not results["warnings"]:
469
+ console.print("[dim]No issues found.[/dim]")
470
+
471
+
472
+ # Add git group to main
473
+ main.add_command(git)
474
+
475
+ # Rename list command to avoid conflict with Python's list built-in
476
+ main.add_command(list_cmd, name="list")
477
+
478
+ if __name__ == "__main__":
479
+ main()