cldpm 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.
cldpm/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """CLDPM - Claude Project Manager.
2
+
3
+ An SDK and CLI for managing mono repos with multiple Claude Code projects.
4
+
5
+ Crafted by Transilience.ai
6
+ Authored by Aman Agarwal (https://github.com/amanagarwal041)
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+ __author__ = "Aman Agarwal"
11
+ __author_url__ = "https://github.com/amanagarwal041"
12
+ __crafted_by__ = "Transilience.ai"
cldpm/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for running cldpm as a module."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
cldpm/_banner.py ADDED
@@ -0,0 +1,99 @@
1
+ """CLDPM Banner Display Module.
2
+
3
+ Displays decorative information about CLDPM, author, and Transilience.
4
+ """
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ BANNER_ASCII = r"""
11
+ ██████╗██╗ ██████╗ ██████╗ ███╗ ███╗
12
+ ██╔════╝██║ ██╔══██╗██╔══██╗████╗ ████║
13
+ ██║ ██║ ██║ ██║██████╔╝██╔████╔██║
14
+ ██║ ██║ ██║ ██║██╔═══╝ ██║╚██╔╝██║
15
+ ╚██████╗███████╗██████╔╝██║ ██║ ╚═╝ ██║
16
+ ╚═════╝╚══════╝╚═════╝ ╚═╝ ╚═╝ ╚═╝
17
+ """
18
+
19
+
20
+ def print_banner(console: Console | None = None) -> None:
21
+ """Print the CLDPM installation/info banner."""
22
+ if console is None:
23
+ console = Console()
24
+
25
+ # ASCII art
26
+ ascii_text = Text(BANNER_ASCII, style="bold magenta")
27
+ console.print(ascii_text)
28
+
29
+ # Title
30
+ console.print(" [bold cyan]Claude Project Manager[/bold cyan]")
31
+ console.print(" [dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/dim]")
32
+ console.print()
33
+
34
+ # Description
35
+ console.print(" [white]Manage mono repos with multiple Claude Code[/white]")
36
+ console.print(" [white]projects. Share skills, agents, hooks, and[/white]")
37
+ console.print(" [white]rules across projects without duplication.[/white]")
38
+ console.print()
39
+ console.print(" [dim]─────────────────────────────────────────[/dim]")
40
+ console.print()
41
+
42
+ # Quick Start
43
+ console.print(" [yellow]Quick Start:[/yellow]")
44
+ console.print(" [dim]$[/dim] [white]cldpm init my-monorepo[/white]")
45
+ console.print(" [dim]$[/dim] [white]cldpm create project web-app[/white]")
46
+ console.print(" [dim]$[/dim] [white]cldpm create skill logging[/white]")
47
+ console.print(" [dim]$[/dim] [white]cldpm add skill:logging --to web-app[/white]")
48
+ console.print()
49
+ console.print(" [dim]─────────────────────────────────────────[/dim]")
50
+ console.print()
51
+
52
+ # Attribution
53
+ console.print(" [magenta]◆[/magenta] [dim]Crafted by[/dim] [cyan]Transilience.ai[/cyan]")
54
+ console.print(" [magenta]◆[/magenta] [dim]Authored by[/dim] [white]Aman Agarwal[/white]")
55
+ console.print(" [dim]github.com/amanagarwal041[/dim]")
56
+ console.print()
57
+ console.print(" [dim]─────────────────────────────────────────[/dim]")
58
+ console.print()
59
+
60
+ # Links
61
+ console.print(" [dim]Docs:[/dim] [cyan]https://cldpm.transilience.ai[/cyan]")
62
+ console.print(" [dim]GitHub:[/dim] [cyan]https://github.com/transilienceai/cldpm[/cyan]")
63
+ console.print()
64
+ console.print(" [dim]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/dim]")
65
+ console.print()
66
+
67
+
68
+ def get_banner_text() -> str:
69
+ """Get banner as plain text for non-TTY environments."""
70
+ return f"""
71
+ {BANNER_ASCII}
72
+ Claude Project Manager
73
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
+
75
+ Manage mono repos with multiple Claude Code
76
+ projects. Share skills, agents, hooks, and
77
+ rules across projects without duplication.
78
+
79
+ ─────────────────────────────────────────
80
+
81
+ Quick Start:
82
+ $ cldpm init my-monorepo
83
+ $ cldpm create project web-app
84
+ $ cldpm create skill logging
85
+ $ cldpm add skill:logging --to web-app
86
+
87
+ ─────────────────────────────────────────
88
+
89
+ ◆ Crafted by Transilience.ai
90
+ ◆ Authored by Aman Agarwal
91
+ github.com/amanagarwal041
92
+
93
+ ─────────────────────────────────────────
94
+
95
+ Docs: https://cldpm.transilience.ai
96
+ GitHub: https://github.com/transilienceai/cldpm
97
+
98
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99
+ """
cldpm/cli.py ADDED
@@ -0,0 +1,81 @@
1
+ """CLI entry point for CLDPM."""
2
+
3
+ import click
4
+
5
+ from .commands import init, create, add, remove, link, unlink, get, clone, sync
6
+ from ._banner import print_banner
7
+
8
+
9
+ def show_version(ctx: click.Context, param: click.Parameter, value: bool) -> None:
10
+ """Custom version callback that shows banner."""
11
+ if not value or ctx.resilient_parsing:
12
+ return
13
+ print_banner()
14
+ ctx.exit()
15
+
16
+
17
+ @click.group()
18
+ @click.option(
19
+ "--version", "-v",
20
+ is_flag=True,
21
+ callback=show_version,
22
+ expose_value=False,
23
+ is_eager=True,
24
+ help="Show version and info banner.",
25
+ )
26
+ def cli() -> None:
27
+ """CLDPM - Claude Project Manager.
28
+
29
+ An SDK and CLI for managing mono repos with multiple Claude Code projects.
30
+ Supports both shared components (reusable across projects) and local
31
+ components (project-specific).
32
+
33
+ \b
34
+ Component Types:
35
+ - Shared: Stored in shared/, symlinked to projects, reusable
36
+ - Local: Stored in .claude/, project-specific, committed directly
37
+
38
+ \b
39
+ Quick Start:
40
+ cldpm init my-monorepo # Create new mono repo
41
+ cldpm create project my-app # Create new project
42
+ cldpm add skill:common --to my-app # Add shared component
43
+ cldpm get my-app # View project info
44
+ cldpm clone my-app ./standalone # Export with all deps
45
+ cldpm sync --all # Restore symlinks after git clone
46
+
47
+ \b
48
+ Crafted by Transilience.ai
49
+ Authored by Aman Agarwal (https://github.com/amanagarwal041)
50
+ """
51
+ pass
52
+
53
+
54
+ # Register commands
55
+ cli.add_command(init)
56
+ cli.add_command(create)
57
+ cli.add_command(add)
58
+ cli.add_command(remove)
59
+ cli.add_command(link)
60
+ cli.add_command(unlink)
61
+ cli.add_command(get)
62
+ cli.add_command(clone)
63
+ cli.add_command(sync)
64
+
65
+
66
+ @cli.command()
67
+ def info() -> None:
68
+ """Show CLDPM information banner.
69
+
70
+ Displays version, quick start guide, and attribution.
71
+ """
72
+ print_banner()
73
+
74
+
75
+ def main() -> None:
76
+ """Main entry point."""
77
+ cli()
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -0,0 +1,12 @@
1
+ """CLI commands for CLDPM."""
2
+
3
+ from .init import init
4
+ from .create import create
5
+ from .add import add
6
+ from .remove import remove
7
+ from .link import link, unlink
8
+ from .get import get
9
+ from .clone import clone
10
+ from .sync import sync
11
+
12
+ __all__ = ["init", "create", "add", "remove", "link", "unlink", "get", "clone", "sync"]
cldpm/commands/add.py ADDED
@@ -0,0 +1,206 @@
1
+ """Implementation of cldpm add command."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import click
7
+
8
+ from ..core.config import (
9
+ get_project_path,
10
+ load_cldpm_config,
11
+ load_project_config,
12
+ save_project_config,
13
+ )
14
+ from ..core.linker import add_component_link
15
+ from ..core.resolver import get_all_dependencies_for_component
16
+ from ..utils.fs import find_repo_root
17
+ from ..utils.output import console, print_error, print_success, print_warning
18
+
19
+
20
+ def parse_component(component: str, repo_root: Path) -> tuple[str, str]:
21
+ """Parse a component specification into type and name.
22
+
23
+ Args:
24
+ component: Component spec like "skill:my-skill" or just "my-skill".
25
+ repo_root: Path to the repo root for auto-detection.
26
+
27
+ Returns:
28
+ Tuple of (component_type, component_name).
29
+
30
+ Raises:
31
+ ValueError: If component type cannot be determined.
32
+ """
33
+ # Check for explicit type prefix
34
+ if ":" in component:
35
+ comp_type, comp_name = component.split(":", 1)
36
+ # Normalize type to plural form
37
+ type_map = {
38
+ "skill": "skills",
39
+ "skills": "skills",
40
+ "agent": "agents",
41
+ "agents": "agents",
42
+ "hook": "hooks",
43
+ "hooks": "hooks",
44
+ "rule": "rules",
45
+ "rules": "rules",
46
+ }
47
+ if comp_type not in type_map:
48
+ raise ValueError(f"Unknown component type: {comp_type}")
49
+ return type_map[comp_type], comp_name
50
+
51
+ # Auto-detect type by searching shared directories
52
+ cldpm_config = load_cldpm_config(repo_root)
53
+ shared_dir = repo_root / cldpm_config.shared_dir
54
+
55
+ for comp_type in ["skills", "agents", "hooks", "rules"]:
56
+ if (shared_dir / comp_type / component).exists():
57
+ return comp_type, component
58
+
59
+ raise ValueError(
60
+ f"Component '{component}' not found. Use 'type:name' format or ensure it exists in shared/."
61
+ )
62
+
63
+
64
+ def add_single_component(
65
+ comp_type: str,
66
+ comp_name: str,
67
+ project_path: Path,
68
+ repo_root: Path,
69
+ is_dependency: bool = False,
70
+ ) -> bool:
71
+ """Add a single component to a project.
72
+
73
+ Args:
74
+ comp_type: Component type (skills, agents, hooks, rules).
75
+ comp_name: Component name.
76
+ project_path: Path to the project directory.
77
+ repo_root: Path to the repo root.
78
+ is_dependency: Whether this is being added as a dependency.
79
+
80
+ Returns:
81
+ True if added successfully, False if already exists or failed.
82
+ """
83
+ cldpm_config = load_cldpm_config(repo_root)
84
+ shared_dir = repo_root / cldpm_config.shared_dir
85
+ component_path = shared_dir / comp_type / comp_name
86
+
87
+ if not component_path.exists():
88
+ if is_dependency:
89
+ console.print(f" [yellow]![/yellow] {comp_type}/{comp_name} (dependency not found)")
90
+ return False
91
+
92
+ # Load project config
93
+ project_config = load_project_config(project_path)
94
+ deps_list = getattr(project_config.dependencies, comp_type)
95
+
96
+ # Check if already added
97
+ if comp_name in deps_list:
98
+ return False
99
+
100
+ # Add to dependencies
101
+ deps_list.append(comp_name)
102
+ save_project_config(project_config, project_path)
103
+
104
+ # Create symlink
105
+ if add_component_link(project_path, comp_type, comp_name, repo_root):
106
+ if is_dependency:
107
+ console.print(f" [green]✓[/green] {comp_type}/{comp_name} (dependency)")
108
+ return True
109
+ else:
110
+ if is_dependency:
111
+ console.print(f" [yellow]![/yellow] {comp_type}/{comp_name} (symlink failed)")
112
+ return False
113
+
114
+
115
+ @click.command()
116
+ @click.argument("component")
117
+ @click.option("--to", "-t", "project_name", required=True, help="Target project name")
118
+ @click.option("--no-deps", is_flag=True, help="Skip installing component dependencies")
119
+ def add(component: str, project_name: str, no_deps: bool) -> None:
120
+ """Add a shared component to a project.
121
+
122
+ Creates a symlink from the project's .claude/ directory to the shared
123
+ component, and updates project.json dependencies. The symlink is
124
+ automatically gitignored; only the reference in project.json is committed.
125
+
126
+ Components can have dependencies on other components. By default, all
127
+ dependencies are also added. Use --no-deps to skip dependencies.
128
+
129
+ \b
130
+ COMPONENT format:
131
+ type:name - Explicit type (skill, agent, hook, rule)
132
+ name - Auto-detect type from shared/ directory
133
+
134
+ \b
135
+ Note: For project-specific (local) components, create them directly in
136
+ projects/<name>/.claude/<type>/ - no 'cldpm add' needed.
137
+
138
+ \b
139
+ Examples:
140
+ cldpm add skill:my-skill --to my-project
141
+ cldpm add agent:pentester --to my-project
142
+ cldpm add hook:pre-commit -t my-project
143
+ cldpm add my-skill --to my-project # Auto-detect type
144
+ cldpm add agent:security --to my-project --no-deps
145
+ """
146
+ # Find repo root
147
+ repo_root = find_repo_root()
148
+ if repo_root is None:
149
+ print_error("Not in a CLDPM mono repo. Run 'cldpm init' first.")
150
+ raise SystemExit(1)
151
+
152
+ # Get project path
153
+ project_path = get_project_path(project_name, repo_root)
154
+ if project_path is None:
155
+ print_error(f"Project not found: {project_name}")
156
+ raise SystemExit(1)
157
+
158
+ # Parse component
159
+ try:
160
+ comp_type, comp_name = parse_component(component, repo_root)
161
+ except ValueError as e:
162
+ print_error(str(e))
163
+ raise SystemExit(1)
164
+
165
+ # Check if component exists
166
+ cldpm_config = load_cldpm_config(repo_root)
167
+ shared_dir = repo_root / cldpm_config.shared_dir
168
+ component_path = shared_dir / comp_type / comp_name
169
+
170
+ if not component_path.exists():
171
+ print_error(f"Component not found: {comp_type}/{comp_name}")
172
+ raise SystemExit(1)
173
+
174
+ # Load project config to check if already added
175
+ project_config = load_project_config(project_path)
176
+ deps_list = getattr(project_config.dependencies, comp_type)
177
+
178
+ if comp_name in deps_list:
179
+ print_warning(f"Component already in project: {comp_type}/{comp_name}")
180
+ return
181
+
182
+ # Add the main component
183
+ deps_list.append(comp_name)
184
+ save_project_config(project_config, project_path)
185
+
186
+ # Create symlink
187
+ if add_component_link(project_path, comp_type, comp_name, repo_root):
188
+ print_success(f"Added {comp_type}/{comp_name} to {project_name}")
189
+ else:
190
+ print_warning(f"Added to config but failed to create symlink for {comp_type}/{comp_name}")
191
+
192
+ # Add dependencies if not skipped
193
+ if not no_deps:
194
+ all_deps = get_all_dependencies_for_component(comp_type, comp_name, repo_root)
195
+ deps_added = False
196
+
197
+ for dep_type, dep_names in all_deps.items():
198
+ for dep_name in dep_names:
199
+ if add_single_component(
200
+ dep_type, dep_name, project_path, repo_root, is_dependency=True
201
+ ):
202
+ deps_added = True
203
+
204
+ if not deps_added and any(all_deps.values()):
205
+ # Dependencies exist but were already added
206
+ pass
@@ -0,0 +1,184 @@
1
+ """Implementation of cldpm clone command."""
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from ..core.config import load_cldpm_config, load_project_config
10
+ from ..core.resolver import resolve_project
11
+ from ..utils.fs import ensure_dir, find_repo_root
12
+ from ..utils.output import console, print_error, print_success, print_dir_tree
13
+
14
+
15
+ @click.command()
16
+ @click.argument("project_name")
17
+ @click.argument("directory")
18
+ @click.option(
19
+ "--include-shared",
20
+ is_flag=True,
21
+ help="Also copy the full shared/ directory structure",
22
+ )
23
+ @click.option(
24
+ "--preserve-links",
25
+ is_flag=True,
26
+ help="Keep symlinks instead of copying (requires shared/ to exist)",
27
+ )
28
+ def clone(
29
+ project_name: str,
30
+ directory: str,
31
+ include_shared: bool,
32
+ preserve_links: bool,
33
+ ) -> None:
34
+ """Clone a project to a standalone directory with all dependencies.
35
+
36
+ Creates a complete, standalone copy of the project:
37
+ - Shared components: Resolved from symlinks, copied as actual files
38
+ - Local components: Copied directly
39
+
40
+ \b
41
+ Use cases:
42
+ - Export project to work outside the mono repo
43
+ - Share project with someone without the mono repo
44
+ - Create a snapshot with all dependencies
45
+
46
+ \b
47
+ Examples:
48
+ cldpm clone my-project ./standalone
49
+ cldpm clone my-project /path/to/output
50
+ cldpm clone my-project ./export --include-shared
51
+ cldpm clone my-project ./linked --preserve-links
52
+ """
53
+ # Find repo root
54
+ repo_root = find_repo_root()
55
+ if repo_root is None:
56
+ print_error("Not in a CLDPM mono repo. Run 'cldpm init' first.")
57
+ raise SystemExit(1)
58
+
59
+ # Resolve project
60
+ try:
61
+ resolved = resolve_project(project_name, repo_root)
62
+ except FileNotFoundError as e:
63
+ print_error(str(e))
64
+ raise SystemExit(1)
65
+
66
+ # Get source project path
67
+ source_path = Path(resolved["path"])
68
+ target_path = Path(directory).resolve()
69
+
70
+ # Check if target exists
71
+ if target_path.exists():
72
+ print_error(f"Target directory already exists: {target_path}")
73
+ raise SystemExit(1)
74
+
75
+ # Create target directory
76
+ ensure_dir(target_path)
77
+
78
+ # Copy project files (excluding .claude subdirectories that are symlinks)
79
+ for item in source_path.iterdir():
80
+ dest = target_path / item.name
81
+
82
+ if item.name == ".claude":
83
+ # Handle .claude directory specially
84
+ ensure_dir(dest)
85
+ for claude_item in item.iterdir():
86
+ claude_dest = dest / claude_item.name
87
+
88
+ if claude_item.name in ["skills", "agents", "hooks", "rules"]:
89
+ # Create directory, will be populated below
90
+ ensure_dir(claude_dest)
91
+
92
+ # Copy local (non-symlink) components directly
93
+ for comp_item in claude_item.iterdir():
94
+ if comp_item.name == ".gitignore":
95
+ continue # Skip .gitignore, will regenerate if needed
96
+ if not comp_item.is_symlink():
97
+ comp_dest = claude_dest / comp_item.name
98
+ if comp_item.is_dir():
99
+ shutil.copytree(comp_item, comp_dest)
100
+ else:
101
+ shutil.copy2(comp_item, comp_dest)
102
+ elif claude_item.is_file():
103
+ shutil.copy2(claude_item, claude_dest)
104
+ elif claude_item.is_dir():
105
+ shutil.copytree(claude_item, claude_dest)
106
+ elif item.is_dir():
107
+ shutil.copytree(item, dest, symlinks=preserve_links)
108
+ else:
109
+ shutil.copy2(item, dest)
110
+
111
+ # Copy shared dependencies
112
+ cldpm_config = load_cldpm_config(repo_root)
113
+ shared_dir = repo_root / cldpm_config.shared_dir
114
+
115
+ shared_counts = {}
116
+ for dep_type in ["skills", "agents", "hooks", "rules"]:
117
+ count = 0
118
+ for component in resolved["shared"].get(dep_type, []):
119
+ comp_name = component["name"]
120
+ source_comp = shared_dir / dep_type / comp_name
121
+ target_comp = target_path / ".claude" / dep_type / comp_name
122
+
123
+ if source_comp.exists() and not target_comp.exists():
124
+ if preserve_links:
125
+ # Create symlink (will only work if shared/ is accessible)
126
+ target_comp.symlink_to(source_comp)
127
+ else:
128
+ # Copy actual files
129
+ if source_comp.is_dir():
130
+ shutil.copytree(source_comp, target_comp)
131
+ else:
132
+ shutil.copy2(source_comp, target_comp)
133
+ count += 1
134
+ shared_counts[dep_type] = count
135
+
136
+ # Count local components
137
+ local_counts = {
138
+ dep_type: len(resolved["local"].get(dep_type, []))
139
+ for dep_type in ["skills", "agents", "hooks", "rules"]
140
+ }
141
+
142
+ # Optionally copy full shared directory
143
+ if include_shared:
144
+ target_shared = target_path / "shared"
145
+ shutil.copytree(shared_dir, target_shared)
146
+
147
+ # Also copy cldpm.json for reference
148
+ shutil.copy2(repo_root / "cldpm.json", target_path / "cldpm.json")
149
+
150
+ # Update settings.json if it references hooks
151
+ settings_path = target_path / ".claude" / "settings.json"
152
+ if settings_path.exists():
153
+ try:
154
+ with open(settings_path) as f:
155
+ settings = json.load(f)
156
+
157
+ # Update any hook paths to be relative to the cloned project
158
+ # (This is a placeholder - actual hook path updates would depend on the hook format)
159
+
160
+ with open(settings_path, "w") as f:
161
+ json.dump(settings, f, indent=2)
162
+ f.write("\n")
163
+ except (json.JSONDecodeError, IOError):
164
+ pass # Ignore settings.json issues
165
+
166
+ print_success(f"Cloned {project_name} to {target_path}")
167
+
168
+ # Show what was copied
169
+ non_zero_shared = {k: v for k, v in shared_counts.items() if v > 0}
170
+ non_zero_local = {k: v for k, v in local_counts.items() if v > 0}
171
+
172
+ if non_zero_shared:
173
+ deps_str = ", ".join(f"{v} {k}" for k, v in non_zero_shared.items())
174
+ console.print(f" Shared: {deps_str}")
175
+
176
+ if non_zero_local:
177
+ deps_str = ", ".join(f"{v} {k}" for k, v in non_zero_local.items())
178
+ console.print(f" Local: {deps_str}")
179
+
180
+ if include_shared:
181
+ console.print(" Included: full shared/ directory")
182
+
183
+ console.print()
184
+ print_dir_tree(target_path, max_depth=3)