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 +12 -0
- cldpm/__main__.py +6 -0
- cldpm/_banner.py +99 -0
- cldpm/cli.py +81 -0
- cldpm/commands/__init__.py +12 -0
- cldpm/commands/add.py +206 -0
- cldpm/commands/clone.py +184 -0
- cldpm/commands/create.py +418 -0
- cldpm/commands/get.py +375 -0
- cldpm/commands/init.py +331 -0
- cldpm/commands/link.py +320 -0
- cldpm/commands/remove.py +289 -0
- cldpm/commands/sync.py +91 -0
- cldpm/core/__init__.py +26 -0
- cldpm/core/config.py +182 -0
- cldpm/core/linker.py +265 -0
- cldpm/core/resolver.py +291 -0
- cldpm/schemas/__init__.py +13 -0
- cldpm/schemas/cldpm.py +32 -0
- cldpm/schemas/component.py +24 -0
- cldpm/schemas/project.py +42 -0
- cldpm/templates/CLAUDE.md.j2 +22 -0
- cldpm/templates/ROOT_CLAUDE.md.j2 +34 -0
- cldpm/templates/agent.md.j2 +22 -0
- cldpm/templates/gitignore.j2 +43 -0
- cldpm/templates/hook.md.j2 +20 -0
- cldpm/templates/rule.md.j2 +33 -0
- cldpm/templates/skill.md.j2 +15 -0
- cldpm/utils/__init__.py +27 -0
- cldpm/utils/fs.py +97 -0
- cldpm/utils/git.py +169 -0
- cldpm/utils/output.py +133 -0
- cldpm-0.1.0.dist-info/METADATA +15 -0
- cldpm-0.1.0.dist-info/RECORD +37 -0
- cldpm-0.1.0.dist-info/WHEEL +4 -0
- cldpm-0.1.0.dist-info/entry_points.txt +2 -0
- cldpm-0.1.0.dist-info/licenses/LICENSE +21 -0
cldpm/core/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Core functionality for CLDPM."""
|
|
2
|
+
|
|
3
|
+
from .config import load_cldpm_config, save_cldpm_config, load_project_config, save_project_config
|
|
4
|
+
from .linker import (
|
|
5
|
+
create_symlink,
|
|
6
|
+
sync_project_links,
|
|
7
|
+
remove_project_links,
|
|
8
|
+
get_local_components,
|
|
9
|
+
get_shared_components,
|
|
10
|
+
update_component_gitignore,
|
|
11
|
+
)
|
|
12
|
+
from .resolver import resolve_project
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"load_cldpm_config",
|
|
16
|
+
"save_cldpm_config",
|
|
17
|
+
"load_project_config",
|
|
18
|
+
"save_project_config",
|
|
19
|
+
"create_symlink",
|
|
20
|
+
"sync_project_links",
|
|
21
|
+
"remove_project_links",
|
|
22
|
+
"get_local_components",
|
|
23
|
+
"get_shared_components",
|
|
24
|
+
"update_component_gitignore",
|
|
25
|
+
"resolve_project",
|
|
26
|
+
]
|
cldpm/core/config.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Configuration loading and saving for CLDPM."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..schemas import CldpmConfig, ComponentMetadata, ProjectConfig
|
|
8
|
+
from ..utils.fs import find_repo_root
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_cldpm_config(repo_root: Optional[Path] = None) -> CldpmConfig:
|
|
12
|
+
"""Load the cldpm.json configuration.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
repo_root: Path to the repo root. If None, will search for it.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The loaded CldpmConfig.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
FileNotFoundError: If cldpm.json is not found.
|
|
22
|
+
ValueError: If cldpm.json is invalid.
|
|
23
|
+
"""
|
|
24
|
+
if repo_root is None:
|
|
25
|
+
repo_root = find_repo_root()
|
|
26
|
+
if repo_root is None:
|
|
27
|
+
raise FileNotFoundError(
|
|
28
|
+
"Not in a CLDPM mono repo. Run 'cldpm init' to create one."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
config_path = repo_root / "cldpm.json"
|
|
32
|
+
if not config_path.exists():
|
|
33
|
+
raise FileNotFoundError(f"cldpm.json not found at {config_path}")
|
|
34
|
+
|
|
35
|
+
with open(config_path, "r") as f:
|
|
36
|
+
data = json.load(f)
|
|
37
|
+
|
|
38
|
+
return CldpmConfig.model_validate(data)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_cldpm_config(config: CldpmConfig, repo_root: Path) -> None:
|
|
42
|
+
"""Save the cldpm.json configuration.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config: The CldpmConfig to save.
|
|
46
|
+
repo_root: Path to the repo root.
|
|
47
|
+
"""
|
|
48
|
+
config_path = repo_root / "cldpm.json"
|
|
49
|
+
with open(config_path, "w") as f:
|
|
50
|
+
json.dump(config.model_dump(by_alias=True), f, indent=2)
|
|
51
|
+
f.write("\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_project_config(project_path: Path) -> ProjectConfig:
|
|
55
|
+
"""Load a project.json configuration.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
project_path: Path to the project directory.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The loaded ProjectConfig.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
FileNotFoundError: If project.json is not found.
|
|
65
|
+
ValueError: If project.json is invalid.
|
|
66
|
+
"""
|
|
67
|
+
config_path = project_path / "project.json"
|
|
68
|
+
if not config_path.exists():
|
|
69
|
+
raise FileNotFoundError(f"project.json not found at {config_path}")
|
|
70
|
+
|
|
71
|
+
with open(config_path, "r") as f:
|
|
72
|
+
data = json.load(f)
|
|
73
|
+
|
|
74
|
+
return ProjectConfig.model_validate(data)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def save_project_config(config: ProjectConfig, project_path: Path) -> None:
|
|
78
|
+
"""Save a project.json configuration.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
config: The ProjectConfig to save.
|
|
82
|
+
project_path: Path to the project directory.
|
|
83
|
+
"""
|
|
84
|
+
config_path = project_path / "project.json"
|
|
85
|
+
with open(config_path, "w") as f:
|
|
86
|
+
json.dump(config.model_dump(exclude_none=True), f, indent=2)
|
|
87
|
+
f.write("\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_project_path(
|
|
91
|
+
project_name: str, repo_root: Optional[Path] = None
|
|
92
|
+
) -> Optional[Path]:
|
|
93
|
+
"""Get the path to a project by name.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
project_name: Name of the project.
|
|
97
|
+
repo_root: Path to the repo root. If None, will search for it.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Path to the project directory, or None if not found.
|
|
101
|
+
"""
|
|
102
|
+
if repo_root is None:
|
|
103
|
+
repo_root = find_repo_root()
|
|
104
|
+
if repo_root is None:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
config = load_cldpm_config(repo_root)
|
|
108
|
+
project_path = repo_root / config.projects_dir / project_name
|
|
109
|
+
|
|
110
|
+
if project_path.exists() and (project_path / "project.json").exists():
|
|
111
|
+
return project_path
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_projects(repo_root: Optional[Path] = None) -> list[Path]:
|
|
117
|
+
"""List all projects in the mono repo.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
repo_root: Path to the repo root. If None, will search for it.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of paths to project directories.
|
|
124
|
+
"""
|
|
125
|
+
if repo_root is None:
|
|
126
|
+
repo_root = find_repo_root()
|
|
127
|
+
if repo_root is None:
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
config = load_cldpm_config(repo_root)
|
|
131
|
+
projects_dir = repo_root / config.projects_dir
|
|
132
|
+
|
|
133
|
+
if not projects_dir.exists():
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
projects = []
|
|
137
|
+
for item in projects_dir.iterdir():
|
|
138
|
+
if item.is_dir() and (item / "project.json").exists():
|
|
139
|
+
projects.append(item)
|
|
140
|
+
|
|
141
|
+
return sorted(projects)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def load_component_metadata(
|
|
145
|
+
comp_type: str, comp_name: str, repo_root: Path
|
|
146
|
+
) -> Optional[ComponentMetadata]:
|
|
147
|
+
"""Load metadata for a shared component.
|
|
148
|
+
|
|
149
|
+
Looks for a metadata file (skill.json, agent.json, hook.json, rule.json)
|
|
150
|
+
in the component directory.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
comp_type: Component type (skills, agents, hooks, rules).
|
|
154
|
+
comp_name: Component name.
|
|
155
|
+
repo_root: Path to the repo root.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
ComponentMetadata if found, None otherwise.
|
|
159
|
+
"""
|
|
160
|
+
config = load_cldpm_config(repo_root)
|
|
161
|
+
shared_dir = repo_root / config.shared_dir
|
|
162
|
+
|
|
163
|
+
component_path = shared_dir / comp_type / comp_name
|
|
164
|
+
if not component_path.exists():
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# Try different metadata file names
|
|
168
|
+
singular_type = comp_type.rstrip("s") # skills -> skill
|
|
169
|
+
metadata_files = [
|
|
170
|
+
f"{singular_type}.json", # skill.json, agent.json, etc.
|
|
171
|
+
"metadata.json", # Generic fallback
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
for metadata_file in metadata_files:
|
|
175
|
+
metadata_path = component_path / metadata_file
|
|
176
|
+
if metadata_path.exists():
|
|
177
|
+
with open(metadata_path, "r") as f:
|
|
178
|
+
data = json.load(f)
|
|
179
|
+
return ComponentMetadata.model_validate(data)
|
|
180
|
+
|
|
181
|
+
# Return minimal metadata if no metadata file exists
|
|
182
|
+
return ComponentMetadata(name=comp_name)
|
cldpm/core/linker.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Symlink management for CLDPM projects."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..schemas import ProjectConfig
|
|
8
|
+
from ..utils.fs import ensure_dir, find_repo_root
|
|
9
|
+
from .config import load_cldpm_config, load_project_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_symlink(source: Path, target: Path) -> bool:
|
|
13
|
+
"""Create a symlink from target to source.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
source: The actual file/directory (symlink target).
|
|
17
|
+
target: The symlink path to create.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if successful, False otherwise.
|
|
21
|
+
"""
|
|
22
|
+
# Ensure parent directory exists
|
|
23
|
+
ensure_dir(target.parent)
|
|
24
|
+
|
|
25
|
+
# Remove existing symlink if present
|
|
26
|
+
if target.is_symlink():
|
|
27
|
+
target.unlink()
|
|
28
|
+
elif target.exists():
|
|
29
|
+
# Don't overwrite non-symlink files
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Create relative symlink
|
|
33
|
+
try:
|
|
34
|
+
# Calculate relative path from target to source
|
|
35
|
+
rel_path = os.path.relpath(source, target.parent)
|
|
36
|
+
target.symlink_to(rel_path)
|
|
37
|
+
return True
|
|
38
|
+
except OSError:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def remove_project_links(project_path: Path) -> None:
|
|
43
|
+
"""Remove all symlinks in a project's .claude directory.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
project_path: Path to the project directory.
|
|
47
|
+
"""
|
|
48
|
+
claude_dir = project_path / ".claude"
|
|
49
|
+
if not claude_dir.exists():
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
for subdir in ["skills", "agents", "hooks", "rules"]:
|
|
53
|
+
subdir_path = claude_dir / subdir
|
|
54
|
+
if not subdir_path.exists():
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
for item in subdir_path.iterdir():
|
|
58
|
+
if item.is_symlink():
|
|
59
|
+
item.unlink()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_component_gitignore(component_dir: Path, symlinked_names: list[str]) -> None:
|
|
63
|
+
"""Update the .gitignore file in a component directory to ignore only symlinks.
|
|
64
|
+
|
|
65
|
+
This allows project-specific components to be committed while shared
|
|
66
|
+
symlinked components are ignored.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
component_dir: Path to the component directory (e.g., .claude/skills/).
|
|
70
|
+
symlinked_names: List of symlinked component names to ignore.
|
|
71
|
+
"""
|
|
72
|
+
gitignore_path = component_dir / ".gitignore"
|
|
73
|
+
|
|
74
|
+
if not symlinked_names:
|
|
75
|
+
# No symlinks, remove .gitignore if it exists and only has our content
|
|
76
|
+
if gitignore_path.exists():
|
|
77
|
+
content = gitignore_path.read_text()
|
|
78
|
+
if content.startswith("# CLDPM shared components"):
|
|
79
|
+
gitignore_path.unlink()
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
# Generate .gitignore content
|
|
83
|
+
lines = [
|
|
84
|
+
"# CLDPM shared components (symlinks to shared/)",
|
|
85
|
+
"# These are regenerated via 'cldpm sync' - do not commit symlinks",
|
|
86
|
+
"# Project-specific components in this directory WILL be committed",
|
|
87
|
+
"",
|
|
88
|
+
]
|
|
89
|
+
lines.extend(sorted(symlinked_names))
|
|
90
|
+
lines.append("") # Trailing newline
|
|
91
|
+
|
|
92
|
+
gitignore_path.write_text("\n".join(lines))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sync_project_links(
|
|
96
|
+
project_path: Path, repo_root: Optional[Path] = None
|
|
97
|
+
) -> dict[str, list[str]]:
|
|
98
|
+
"""Synchronize symlinks for a project based on its dependencies.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
project_path: Path to the project directory.
|
|
102
|
+
repo_root: Path to the repo root. If None, will search for it.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary with lists of created, failed, and skipped links.
|
|
106
|
+
"""
|
|
107
|
+
if repo_root is None:
|
|
108
|
+
repo_root = find_repo_root(project_path)
|
|
109
|
+
if repo_root is None:
|
|
110
|
+
raise ValueError("Not in a CLDPM mono repo")
|
|
111
|
+
|
|
112
|
+
cldpm_config = load_cldpm_config(repo_root)
|
|
113
|
+
project_config = load_project_config(project_path)
|
|
114
|
+
|
|
115
|
+
shared_dir = repo_root / cldpm_config.shared_dir
|
|
116
|
+
claude_dir = project_path / ".claude"
|
|
117
|
+
|
|
118
|
+
result = {"created": [], "failed": [], "skipped": [], "missing": []}
|
|
119
|
+
|
|
120
|
+
# Process each dependency type
|
|
121
|
+
dep_types = [
|
|
122
|
+
("skills", project_config.dependencies.skills),
|
|
123
|
+
("agents", project_config.dependencies.agents),
|
|
124
|
+
("hooks", project_config.dependencies.hooks),
|
|
125
|
+
("rules", project_config.dependencies.rules),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
for dep_type, deps in dep_types:
|
|
129
|
+
target_dir = claude_dir / dep_type
|
|
130
|
+
ensure_dir(target_dir)
|
|
131
|
+
|
|
132
|
+
# Track successfully created symlinks for .gitignore
|
|
133
|
+
symlinked_names = []
|
|
134
|
+
|
|
135
|
+
for dep_name in deps:
|
|
136
|
+
source = shared_dir / dep_type / dep_name
|
|
137
|
+
target = target_dir / dep_name
|
|
138
|
+
|
|
139
|
+
if not source.exists():
|
|
140
|
+
result["missing"].append(f"{dep_type}/{dep_name}")
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if target.exists() and not target.is_symlink():
|
|
144
|
+
# Local component exists with same name - skip but warn
|
|
145
|
+
result["skipped"].append(f"{dep_type}/{dep_name}")
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
if create_symlink(source, target):
|
|
149
|
+
result["created"].append(f"{dep_type}/{dep_name}")
|
|
150
|
+
symlinked_names.append(dep_name)
|
|
151
|
+
else:
|
|
152
|
+
result["failed"].append(f"{dep_type}/{dep_name}")
|
|
153
|
+
|
|
154
|
+
# Also include existing symlinks that weren't just created
|
|
155
|
+
for item in target_dir.iterdir():
|
|
156
|
+
if item.is_symlink() and item.name not in symlinked_names:
|
|
157
|
+
symlinked_names.append(item.name)
|
|
158
|
+
|
|
159
|
+
# Update .gitignore in this component directory
|
|
160
|
+
update_component_gitignore(target_dir, symlinked_names)
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def add_component_link(
|
|
166
|
+
project_path: Path,
|
|
167
|
+
component_type: str,
|
|
168
|
+
component_name: str,
|
|
169
|
+
repo_root: Optional[Path] = None,
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""Add a single component symlink to a project.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
project_path: Path to the project directory.
|
|
175
|
+
component_type: Type of component (skills, agents, hooks, rules).
|
|
176
|
+
component_name: Name of the component.
|
|
177
|
+
repo_root: Path to the repo root. If None, will search for it.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if successful, False otherwise.
|
|
181
|
+
"""
|
|
182
|
+
if repo_root is None:
|
|
183
|
+
repo_root = find_repo_root(project_path)
|
|
184
|
+
if repo_root is None:
|
|
185
|
+
raise ValueError("Not in a CLDPM mono repo")
|
|
186
|
+
|
|
187
|
+
cldpm_config = load_cldpm_config(repo_root)
|
|
188
|
+
shared_dir = repo_root / cldpm_config.shared_dir
|
|
189
|
+
|
|
190
|
+
source = shared_dir / component_type / component_name
|
|
191
|
+
target = project_path / ".claude" / component_type / component_name
|
|
192
|
+
|
|
193
|
+
if not source.exists():
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
success = create_symlink(source, target)
|
|
197
|
+
|
|
198
|
+
if success:
|
|
199
|
+
# Update .gitignore in the component directory
|
|
200
|
+
component_dir = project_path / ".claude" / component_type
|
|
201
|
+
symlinked_names = [
|
|
202
|
+
item.name for item in component_dir.iterdir() if item.is_symlink()
|
|
203
|
+
]
|
|
204
|
+
update_component_gitignore(component_dir, symlinked_names)
|
|
205
|
+
|
|
206
|
+
return success
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_local_components(project_path: Path) -> dict[str, list[str]]:
|
|
210
|
+
"""Get list of local (non-symlinked) components in a project.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
project_path: Path to the project directory.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dictionary mapping component types to lists of local component names.
|
|
217
|
+
"""
|
|
218
|
+
claude_dir = project_path / ".claude"
|
|
219
|
+
result = {}
|
|
220
|
+
|
|
221
|
+
for component_type in ["skills", "agents", "hooks", "rules"]:
|
|
222
|
+
type_dir = claude_dir / component_type
|
|
223
|
+
if not type_dir.exists():
|
|
224
|
+
result[component_type] = []
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
local_components = []
|
|
228
|
+
for item in type_dir.iterdir():
|
|
229
|
+
# Skip .gitignore and symlinks
|
|
230
|
+
if item.name == ".gitignore":
|
|
231
|
+
continue
|
|
232
|
+
if not item.is_symlink():
|
|
233
|
+
local_components.append(item.name)
|
|
234
|
+
|
|
235
|
+
result[component_type] = sorted(local_components)
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_shared_components(project_path: Path) -> dict[str, list[str]]:
|
|
241
|
+
"""Get list of shared (symlinked) components in a project.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
project_path: Path to the project directory.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dictionary mapping component types to lists of shared component names.
|
|
248
|
+
"""
|
|
249
|
+
claude_dir = project_path / ".claude"
|
|
250
|
+
result = {}
|
|
251
|
+
|
|
252
|
+
for component_type in ["skills", "agents", "hooks", "rules"]:
|
|
253
|
+
type_dir = claude_dir / component_type
|
|
254
|
+
if not type_dir.exists():
|
|
255
|
+
result[component_type] = []
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
shared_components = []
|
|
259
|
+
for item in type_dir.iterdir():
|
|
260
|
+
if item.is_symlink():
|
|
261
|
+
shared_components.append(item.name)
|
|
262
|
+
|
|
263
|
+
result[component_type] = sorted(shared_components)
|
|
264
|
+
|
|
265
|
+
return result
|