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/core/resolver.py ADDED
@@ -0,0 +1,291 @@
1
+ """Project resolution for CLDPM."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Optional
5
+
6
+ from ..utils.fs import find_repo_root
7
+ from .config import (
8
+ get_project_path,
9
+ load_cldpm_config,
10
+ load_component_metadata,
11
+ load_project_config,
12
+ )
13
+
14
+
15
+ def resolve_component(
16
+ component_type: str, component_name: str, shared_dir: Path
17
+ ) -> Optional[dict[str, Any]]:
18
+ """Resolve a single component from the shared directory.
19
+
20
+ Args:
21
+ component_type: Type of component (skills, agents, hooks, rules).
22
+ component_name: Name of the component.
23
+ shared_dir: Path to the shared directory.
24
+
25
+ Returns:
26
+ Dictionary with component info, or None if not found.
27
+ """
28
+ component_path = shared_dir / component_type / component_name
29
+
30
+ if not component_path.exists():
31
+ return None
32
+
33
+ # Get list of files in the component
34
+ if component_path.is_dir():
35
+ files = [f.name for f in component_path.iterdir() if f.is_file()]
36
+ else:
37
+ files = [component_path.name]
38
+ component_path = component_path.parent
39
+
40
+ return {
41
+ "name": component_name,
42
+ "type": "shared",
43
+ "sourcePath": str(component_path.relative_to(shared_dir.parent)),
44
+ "files": sorted(files),
45
+ }
46
+
47
+
48
+ def resolve_local_component(
49
+ component_type: str, component_name: str, project_path: Path
50
+ ) -> Optional[dict[str, Any]]:
51
+ """Resolve a single local component from the project directory.
52
+
53
+ Args:
54
+ component_type: Type of component (skills, agents, hooks, rules).
55
+ component_name: Name of the component.
56
+ project_path: Path to the project directory.
57
+
58
+ Returns:
59
+ Dictionary with component info, or None if not found.
60
+ """
61
+ component_path = project_path / ".claude" / component_type / component_name
62
+
63
+ if not component_path.exists() or component_path.is_symlink():
64
+ return None
65
+
66
+ # Get list of files in the component
67
+ if component_path.is_dir():
68
+ files = [f.name for f in component_path.iterdir() if f.is_file()]
69
+ else:
70
+ files = [component_path.name]
71
+
72
+ return {
73
+ "name": component_name,
74
+ "type": "local",
75
+ "sourcePath": f".claude/{component_type}/{component_name}",
76
+ "files": sorted(files),
77
+ }
78
+
79
+
80
+ def get_local_components_in_project(project_path: Path) -> dict[str, list[dict[str, Any]]]:
81
+ """Get all local (non-symlinked) components in a project.
82
+
83
+ Args:
84
+ project_path: Path to the project directory.
85
+
86
+ Returns:
87
+ Dictionary mapping component types to lists of component info.
88
+ """
89
+ claude_dir = project_path / ".claude"
90
+ result = {"skills": [], "agents": [], "hooks": [], "rules": []}
91
+
92
+ for component_type in result.keys():
93
+ type_dir = claude_dir / component_type
94
+ if not type_dir.exists():
95
+ continue
96
+
97
+ for item in type_dir.iterdir():
98
+ # Skip .gitignore and symlinks
99
+ if item.name == ".gitignore" or item.is_symlink():
100
+ continue
101
+
102
+ component = resolve_local_component(component_type, item.name, project_path)
103
+ if component:
104
+ result[component_type].append(component)
105
+
106
+ return result
107
+
108
+
109
+ def resolve_project(
110
+ project_path_or_name: str, repo_root: Optional[Path] = None
111
+ ) -> dict[str, Any]:
112
+ """Resolve a project and all its dependencies.
113
+
114
+ Args:
115
+ project_path_or_name: Path to the project directory or project name.
116
+ repo_root: Path to the repo root. If None, will search for it.
117
+
118
+ Returns:
119
+ Dictionary with resolved project info.
120
+
121
+ Raises:
122
+ FileNotFoundError: If the project is not found.
123
+ """
124
+ if repo_root is None:
125
+ repo_root = find_repo_root()
126
+ if repo_root is None:
127
+ raise FileNotFoundError("Not in a CLDPM mono repo")
128
+
129
+ # Try to resolve as path first
130
+ project_path = Path(project_path_or_name)
131
+ if not project_path.is_absolute():
132
+ # Check if it's a relative path
133
+ full_path = repo_root / project_path_or_name
134
+ if full_path.exists() and (full_path / "project.json").exists():
135
+ project_path = full_path
136
+ else:
137
+ # Try as project name
138
+ project_path = get_project_path(project_path_or_name, repo_root)
139
+
140
+ if project_path is None or not project_path.exists():
141
+ raise FileNotFoundError(f"Project not found: {project_path_or_name}")
142
+
143
+ cldpm_config = load_cldpm_config(repo_root)
144
+ project_config = load_project_config(project_path)
145
+
146
+ shared_dir = repo_root / cldpm_config.shared_dir
147
+
148
+ # Resolve shared dependencies
149
+ shared = {"skills": [], "agents": [], "hooks": [], "rules": []}
150
+
151
+ dep_types = [
152
+ ("skills", project_config.dependencies.skills),
153
+ ("agents", project_config.dependencies.agents),
154
+ ("hooks", project_config.dependencies.hooks),
155
+ ("rules", project_config.dependencies.rules),
156
+ ]
157
+
158
+ for dep_type, deps in dep_types:
159
+ for dep_name in deps:
160
+ component = resolve_component(dep_type, dep_name, shared_dir)
161
+ if component:
162
+ shared[dep_type].append(component)
163
+
164
+ # Get local components
165
+ local = get_local_components_in_project(project_path)
166
+
167
+ return {
168
+ "name": project_config.name,
169
+ "path": str(project_path.resolve()),
170
+ "config": project_config.model_dump(exclude_none=True),
171
+ "shared": shared,
172
+ "local": local,
173
+ }
174
+
175
+
176
+ def list_shared_components(
177
+ repo_root: Optional[Path] = None,
178
+ ) -> dict[str, list[str]]:
179
+ """List all shared components in the mono repo.
180
+
181
+ Args:
182
+ repo_root: Path to the repo root. If None, will search for it.
183
+
184
+ Returns:
185
+ Dictionary mapping component types to lists of component names.
186
+ """
187
+ if repo_root is None:
188
+ repo_root = find_repo_root()
189
+ if repo_root is None:
190
+ return {"skills": [], "agents": [], "hooks": [], "rules": []}
191
+
192
+ cldpm_config = load_cldpm_config(repo_root)
193
+ shared_dir = repo_root / cldpm_config.shared_dir
194
+
195
+ result = {}
196
+ for component_type in ["skills", "agents", "hooks", "rules"]:
197
+ type_dir = shared_dir / component_type
198
+ if type_dir.exists():
199
+ result[component_type] = sorted(
200
+ [d.name for d in type_dir.iterdir() if d.is_dir()]
201
+ )
202
+ else:
203
+ result[component_type] = []
204
+
205
+ return result
206
+
207
+
208
+ def resolve_component_dependencies(
209
+ comp_type: str,
210
+ comp_name: str,
211
+ repo_root: Path,
212
+ resolved: Optional[set[str]] = None,
213
+ ) -> list[tuple[str, str]]:
214
+ """Recursively resolve all dependencies of a component.
215
+
216
+ Args:
217
+ comp_type: Component type (skills, agents, hooks, rules).
218
+ comp_name: Component name.
219
+ repo_root: Path to the repo root.
220
+ resolved: Set of already resolved components (for circular detection).
221
+
222
+ Returns:
223
+ List of (comp_type, comp_name) tuples for all dependencies.
224
+
225
+ Raises:
226
+ ValueError: If circular dependency detected.
227
+ """
228
+ if resolved is None:
229
+ resolved = set()
230
+
231
+ component_key = f"{comp_type}:{comp_name}"
232
+
233
+ if component_key in resolved:
234
+ return [] # Already resolved
235
+
236
+ resolved.add(component_key)
237
+
238
+ metadata = load_component_metadata(comp_type, comp_name, repo_root)
239
+ if metadata is None:
240
+ return []
241
+
242
+ dependencies = []
243
+
244
+ # Process each dependency type
245
+ for dep_type in ["skills", "agents", "hooks", "rules"]:
246
+ dep_list = getattr(metadata.dependencies, dep_type, [])
247
+ for dep_name in dep_list:
248
+ dep_key = f"{dep_type}:{dep_name}"
249
+
250
+ # Check for circular dependency in current resolution path
251
+ if dep_key in resolved:
252
+ # It's only circular if we're currently resolving it
253
+ # (not if it was resolved in a different branch)
254
+ continue
255
+
256
+ dependencies.append((dep_type, dep_name))
257
+
258
+ # Recursively resolve sub-dependencies
259
+ sub_deps = resolve_component_dependencies(
260
+ dep_type, dep_name, repo_root, resolved.copy()
261
+ )
262
+ dependencies.extend(sub_deps)
263
+
264
+ return dependencies
265
+
266
+
267
+ def get_all_dependencies_for_component(
268
+ comp_type: str, comp_name: str, repo_root: Path
269
+ ) -> dict[str, list[str]]:
270
+ """Get all dependencies for a component, organized by type.
271
+
272
+ Args:
273
+ comp_type: Component type (skills, agents, hooks, rules).
274
+ comp_name: Component name.
275
+ repo_root: Path to the repo root.
276
+
277
+ Returns:
278
+ Dictionary mapping component types to lists of component names.
279
+ """
280
+ deps = resolve_component_dependencies(comp_type, comp_name, repo_root)
281
+
282
+ result: dict[str, list[str]] = {"skills": [], "agents": [], "hooks": [], "rules": []}
283
+
284
+ seen = set()
285
+ for dep_type, dep_name in deps:
286
+ key = f"{dep_type}:{dep_name}"
287
+ if key not in seen:
288
+ seen.add(key)
289
+ result[dep_type].append(dep_name)
290
+
291
+ return result
@@ -0,0 +1,13 @@
1
+ """Pydantic schemas for CLDPM configuration files."""
2
+
3
+ from .cldpm import CldpmConfig
4
+ from .component import ComponentDependencies, ComponentMetadata
5
+ from .project import ProjectConfig, ProjectDependencies
6
+
7
+ __all__ = [
8
+ "CldpmConfig",
9
+ "ComponentDependencies",
10
+ "ComponentMetadata",
11
+ "ProjectConfig",
12
+ "ProjectDependencies",
13
+ ]
cldpm/schemas/cldpm.py ADDED
@@ -0,0 +1,32 @@
1
+ """Pydantic model for cldpm.json (root configuration)."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class CldpmConfig(BaseModel):
7
+ """Root configuration for a CLDPM mono repo."""
8
+
9
+ name: str = Field(..., description="Name of the mono repo")
10
+ version: str = Field(default="1.0.0", description="Version of the mono repo")
11
+ projects_dir: str = Field(
12
+ default="projects",
13
+ alias="projectsDir",
14
+ description="Directory containing projects",
15
+ )
16
+ shared_dir: str = Field(
17
+ default="shared",
18
+ alias="sharedDir",
19
+ description="Directory containing shared components",
20
+ )
21
+
22
+ model_config = {
23
+ "populate_by_name": True,
24
+ "json_schema_extra": {
25
+ "example": {
26
+ "name": "my-monorepo",
27
+ "version": "1.0.0",
28
+ "projectsDir": "projects",
29
+ "sharedDir": "shared",
30
+ }
31
+ },
32
+ }
@@ -0,0 +1,24 @@
1
+ """Pydantic schema for component metadata."""
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class ComponentDependencies(BaseModel):
9
+ """Dependencies for a component."""
10
+
11
+ skills: list[str] = Field(default_factory=list)
12
+ agents: list[str] = Field(default_factory=list)
13
+ hooks: list[str] = Field(default_factory=list)
14
+ rules: list[str] = Field(default_factory=list)
15
+
16
+
17
+ class ComponentMetadata(BaseModel):
18
+ """Metadata for a shared component (skill, agent, hook, rule)."""
19
+
20
+ model_config = ConfigDict(extra="allow")
21
+
22
+ name: str
23
+ description: Optional[str] = None
24
+ dependencies: ComponentDependencies = Field(default_factory=ComponentDependencies)
@@ -0,0 +1,42 @@
1
+ """Pydantic model for project.json (project configuration)."""
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ProjectDependencies(BaseModel):
9
+ """Dependencies for a project."""
10
+
11
+ skills: list[str] = Field(default_factory=list, description="List of skill names")
12
+ agents: list[str] = Field(default_factory=list, description="List of agent names")
13
+ hooks: list[str] = Field(default_factory=list, description="List of hook names")
14
+ rules: list[str] = Field(default_factory=list, description="List of rule names")
15
+
16
+
17
+ class ProjectConfig(BaseModel):
18
+ """Configuration for a CLDPM project."""
19
+
20
+ name: str = Field(..., description="Name of the project")
21
+ description: Optional[str] = Field(
22
+ default=None, description="Description of the project"
23
+ )
24
+ dependencies: ProjectDependencies = Field(
25
+ default_factory=ProjectDependencies,
26
+ description="Project dependencies on shared components",
27
+ )
28
+
29
+ model_config = {
30
+ "json_schema_extra": {
31
+ "example": {
32
+ "name": "my-project",
33
+ "description": "Project description",
34
+ "dependencies": {
35
+ "skills": ["my-skill"],
36
+ "agents": ["my-agent"],
37
+ "hooks": ["my-hook"],
38
+ "rules": ["my-rule"],
39
+ },
40
+ }
41
+ },
42
+ }
@@ -0,0 +1,22 @@
1
+ # {{ project_name }}
2
+
3
+ {% if description %}
4
+ {{ description }}
5
+ {% endif %}
6
+
7
+ ## Project Overview
8
+
9
+ This is a CLDPM-managed project. Dependencies are managed via `project.json` and symlinked from the shared directory.
10
+
11
+ ## Getting Started
12
+
13
+ 1. Review the project configuration in `project.json`
14
+ 2. Check available skills in `.claude/skills/`
15
+ 3. Check available agents in `.claude/agents/`
16
+
17
+ ## Project Structure
18
+
19
+ - `project.json` - Project configuration and dependencies
20
+ - `CLAUDE.md` - This file, project instructions
21
+ - `.claude/` - Claude Code configuration and components
22
+ - `outputs/` - Project outputs and artifacts
@@ -0,0 +1,34 @@
1
+ # {{ repo_name }}
2
+
3
+ This is a CLDPM-managed mono repo containing multiple Claude Code projects.
4
+
5
+ ## Structure
6
+
7
+ - `projects/` - Individual projects
8
+ - `shared/` - Shared components (skills, agents, hooks, rules)
9
+ - `cldpm.json` - Mono repo configuration
10
+
11
+ ## Using CLDPM
12
+
13
+ ```bash
14
+ # Create a new project
15
+ cldpm create project my-project
16
+
17
+ # Add shared components to a project
18
+ cldpm add skill:my-skill --to my-project
19
+
20
+ # Get project info
21
+ cldpm get my-project
22
+
23
+ # Sync symlinks after git clone
24
+ cldpm sync --all
25
+ ```
26
+
27
+ ## Shared Components
28
+
29
+ Shared components are stored in the `shared/` directory and can be referenced by multiple projects:
30
+
31
+ - `shared/skills/` - Reusable skills
32
+ - `shared/agents/` - Reusable agent configurations
33
+ - `shared/hooks/` - Reusable hooks
34
+ - `shared/rules/` - Reusable rules
@@ -0,0 +1,22 @@
1
+ # {{ name }}
2
+
3
+ {% if description %}{{ description }}{% else %}A shared agent.{% endif %}
4
+
5
+ ## Overview
6
+
7
+ Describe what this agent does and its purpose.
8
+
9
+ ## Capabilities
10
+
11
+ - Capability 1
12
+ - Capability 2
13
+
14
+ ## Instructions
15
+
16
+ <!-- Add your agent instructions here -->
17
+
18
+ ## Workflow
19
+
20
+ 1. Step 1
21
+ 2. Step 2
22
+ 3. Step 3
@@ -0,0 +1,43 @@
1
+ # CLDPM Note: Shared component symlinks are managed per-directory
2
+ # Each .claude/{skills,agents,hooks,rules}/ has its own .gitignore
3
+ # that only ignores symlinked shared components.
4
+ # Project-specific components in those directories ARE committed.
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.so
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+
28
+ # Virtual environments
29
+ .env
30
+ .venv
31
+ env/
32
+ venv/
33
+ ENV/
34
+
35
+ # IDE
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+ *.swo
40
+
41
+ # OS
42
+ .DS_Store
43
+ Thumbs.db
@@ -0,0 +1,20 @@
1
+ # {{ name }}
2
+
3
+ {% if description %}{{ description }}{% else %}A shared hook.{% endif %}
4
+
5
+ ## Overview
6
+
7
+ Describe when this hook triggers and what it does.
8
+
9
+ ## Trigger
10
+
11
+ - Event: `<event-name>`
12
+ - Conditions: Describe when this hook should run
13
+
14
+ ## Actions
15
+
16
+ <!-- Describe what actions this hook performs -->
17
+
18
+ ## Configuration
19
+
20
+ <!-- Document any configuration options -->
@@ -0,0 +1,33 @@
1
+ # {{ name }}
2
+
3
+ {% if description %}{{ description }}{% else %}A shared rule.{% endif %}
4
+
5
+ ## Overview
6
+
7
+ Describe the purpose of this rule.
8
+
9
+ ## Guidelines
10
+
11
+ ### Do
12
+
13
+ - Guideline 1
14
+ - Guideline 2
15
+
16
+ ### Don't
17
+
18
+ - Anti-pattern 1
19
+ - Anti-pattern 2
20
+
21
+ ## Examples
22
+
23
+ ### Good
24
+
25
+ ```
26
+ Example of following this rule
27
+ ```
28
+
29
+ ### Bad
30
+
31
+ ```
32
+ Example of violating this rule
33
+ ```
@@ -0,0 +1,15 @@
1
+ # {{ name }}
2
+
3
+ {% if description %}{{ description }}{% else %}A shared skill.{% endif %}
4
+
5
+ ## Overview
6
+
7
+ Describe what this skill does and when to use it.
8
+
9
+ ## Instructions
10
+
11
+ <!-- Add your skill instructions here -->
12
+
13
+ ## Examples
14
+
15
+ <!-- Add usage examples here -->
@@ -0,0 +1,27 @@
1
+ """Utility modules for CLDPM."""
2
+
3
+ from .fs import find_repo_root, ensure_dir, is_symlink
4
+ from .git import (
5
+ get_github_token,
6
+ parse_repo_url,
7
+ clone_repo,
8
+ clone_to_temp,
9
+ cleanup_temp_dir,
10
+ )
11
+ from .output import console, print_error, print_success, print_warning, print_tree
12
+
13
+ __all__ = [
14
+ "find_repo_root",
15
+ "ensure_dir",
16
+ "is_symlink",
17
+ "get_github_token",
18
+ "parse_repo_url",
19
+ "clone_repo",
20
+ "clone_to_temp",
21
+ "cleanup_temp_dir",
22
+ "console",
23
+ "print_error",
24
+ "print_success",
25
+ "print_warning",
26
+ "print_tree",
27
+ ]