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.
@@ -0,0 +1,418 @@
1
+ """Implementation of cldpm create command."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import click
8
+ from jinja2 import Environment, PackageLoader
9
+
10
+ from ..schemas import ComponentDependencies, ComponentMetadata, ProjectConfig, ProjectDependencies
11
+ from ..core.config import load_cldpm_config, save_project_config
12
+ from ..core.linker import sync_project_links
13
+ from ..utils.fs import ensure_dir, find_repo_root
14
+ from ..utils.output import print_success, print_error, print_dir_tree, console
15
+
16
+
17
+ def parse_dependency_list(deps_str: Optional[str]) -> list[str]:
18
+ """Parse a comma-separated dependency string into a list."""
19
+ if not deps_str:
20
+ return []
21
+ return [d.strip() for d in deps_str.split(",") if d.strip()]
22
+
23
+
24
+ def save_component_metadata(
25
+ metadata: ComponentMetadata,
26
+ component_path: Path,
27
+ component_type: str,
28
+ ) -> None:
29
+ """Save component metadata to a JSON file.
30
+
31
+ Args:
32
+ metadata: The ComponentMetadata to save.
33
+ component_path: Path to the component directory.
34
+ component_type: Type of component (skills, agents, hooks, rules).
35
+ """
36
+ singular_type = component_type.rstrip("s") # skills -> skill
37
+ metadata_path = component_path / f"{singular_type}.json"
38
+
39
+ data = {"name": metadata.name}
40
+ if metadata.description:
41
+ data["description"] = metadata.description
42
+
43
+ # Only include dependencies if any exist
44
+ deps = metadata.dependencies
45
+ if deps.skills or deps.agents or deps.hooks or deps.rules:
46
+ data["dependencies"] = {}
47
+ if deps.skills:
48
+ data["dependencies"]["skills"] = deps.skills
49
+ if deps.agents:
50
+ data["dependencies"]["agents"] = deps.agents
51
+ if deps.hooks:
52
+ data["dependencies"]["hooks"] = deps.hooks
53
+ if deps.rules:
54
+ data["dependencies"]["rules"] = deps.rules
55
+
56
+ with open(metadata_path, "w") as f:
57
+ json.dump(data, f, indent=2)
58
+ f.write("\n")
59
+
60
+
61
+ @click.group()
62
+ def create() -> None:
63
+ """Create new projects or components."""
64
+ pass
65
+
66
+
67
+ @create.command()
68
+ @click.argument("name")
69
+ @click.option("--description", "-d", help="Project description")
70
+ @click.option("--skills", "-s", help="Comma-separated list of shared skills to add")
71
+ @click.option("--agents", "-a", help="Comma-separated list of shared agents to add")
72
+ def project(
73
+ name: str,
74
+ description: Optional[str],
75
+ skills: Optional[str],
76
+ agents: Optional[str],
77
+ ) -> None:
78
+ """Create a new project in the mono repo.
79
+
80
+ Creates a project with the standard structure for Claude Code, including
81
+ directories for both shared (symlinked) and local (project-specific)
82
+ components.
83
+
84
+ \b
85
+ Structure created:
86
+ project.json - Project manifest (tracks shared dependencies)
87
+ CLAUDE.md - Project instructions
88
+ .claude/ - Components directory
89
+ skills/ - Skills (shared symlinks + local)
90
+ agents/ - Agents (shared symlinks + local)
91
+ hooks/ - Hooks (shared symlinks + local)
92
+ rules/ - Rules (shared symlinks + local)
93
+ outputs/ - Project outputs
94
+
95
+ \b
96
+ Examples:
97
+ cldpm create project my-app
98
+ cldpm create project my-app -d "My application"
99
+ cldpm create project my-app --skills skill1,skill2
100
+ cldpm create project my-app -s skill1 -a agent1
101
+ """
102
+ # Find repo root
103
+ repo_root = find_repo_root()
104
+ if repo_root is None:
105
+ print_error("Not in a CLDPM mono repo. Run 'cldpm init' first.")
106
+ raise SystemExit(1)
107
+
108
+ # Load config
109
+ cldpm_config = load_cldpm_config(repo_root)
110
+
111
+ # Create project directory
112
+ project_path = repo_root / cldpm_config.projects_dir / name
113
+
114
+ if project_path.exists():
115
+ print_error(f"Project already exists: {name}")
116
+ raise SystemExit(1)
117
+
118
+ ensure_dir(project_path)
119
+
120
+ # Parse dependencies
121
+ deps = ProjectDependencies()
122
+ if skills:
123
+ deps.skills = [s.strip() for s in skills.split(",") if s.strip()]
124
+ if agents:
125
+ deps.agents = [a.strip() for a in agents.split(",") if a.strip()]
126
+
127
+ # Create project config
128
+ project_config = ProjectConfig(
129
+ name=name,
130
+ description=description,
131
+ dependencies=deps,
132
+ )
133
+ save_project_config(project_config, project_path)
134
+
135
+ # Create .claude directory structure
136
+ claude_dir = project_path / ".claude"
137
+ ensure_dir(claude_dir)
138
+ ensure_dir(claude_dir / "skills")
139
+ ensure_dir(claude_dir / "agents")
140
+ ensure_dir(claude_dir / "hooks")
141
+ ensure_dir(claude_dir / "rules")
142
+
143
+ # Create settings.json placeholder
144
+ (claude_dir / "settings.json").write_text("{}\n")
145
+
146
+ # Create outputs directory
147
+ ensure_dir(project_path / "outputs")
148
+
149
+ # Create CLAUDE.md from template
150
+ env = Environment(loader=PackageLoader("cldpm", "templates"))
151
+ template = env.get_template("CLAUDE.md.j2")
152
+ claude_md = template.render(
153
+ project_name=name,
154
+ description=description or "",
155
+ )
156
+ (project_path / "CLAUDE.md").write_text(claude_md)
157
+
158
+ print_success(f"Created project: {name}")
159
+
160
+ # Sync symlinks if dependencies were specified
161
+ if deps.skills or deps.agents:
162
+ result = sync_project_links(project_path, repo_root)
163
+ if result["created"]:
164
+ console.print(f" Linked: {', '.join(result['created'])}")
165
+ if result["missing"]:
166
+ console.print(f" [yellow]Missing:[/yellow] {', '.join(result['missing'])}")
167
+
168
+ console.print()
169
+ print_dir_tree(project_path, max_depth=2)
170
+
171
+
172
+ @create.command()
173
+ @click.argument("name")
174
+ @click.option("--description", "-d", help="Skill description")
175
+ @click.option("--skills", "-s", help="Comma-separated list of skill dependencies")
176
+ @click.option("--hooks", "-h", "hooks_deps", help="Comma-separated list of hook dependencies")
177
+ @click.option("--rules", "-r", help="Comma-separated list of rule dependencies")
178
+ def skill(
179
+ name: str,
180
+ description: Optional[str],
181
+ skills: Optional[str],
182
+ hooks_deps: Optional[str],
183
+ rules: Optional[str],
184
+ ) -> None:
185
+ """Create a new shared skill.
186
+
187
+ Creates a skill in the shared/skills/ directory with optional dependencies
188
+ on other shared components.
189
+
190
+ \b
191
+ Structure created:
192
+ shared/skills/<name>/
193
+ SKILL.md - Skill instructions
194
+ skill.json - Skill metadata and dependencies
195
+
196
+ \b
197
+ Examples:
198
+ cldpm create skill code-review
199
+ cldpm create skill code-review -d "Code review assistant"
200
+ cldpm create skill advanced-review --skills base-review,utils
201
+ cldpm create skill full-check -s lint-check -h pre-commit -r security
202
+ """
203
+ _create_component("skills", name, description, skills, None, hooks_deps, rules)
204
+
205
+
206
+ @create.command()
207
+ @click.argument("name")
208
+ @click.option("--description", "-d", help="Agent description")
209
+ @click.option("--skills", "-s", help="Comma-separated list of skill dependencies")
210
+ @click.option("--agents", "-a", help="Comma-separated list of agent dependencies")
211
+ @click.option("--hooks", "-h", "hooks_deps", help="Comma-separated list of hook dependencies")
212
+ @click.option("--rules", "-r", help="Comma-separated list of rule dependencies")
213
+ def agent(
214
+ name: str,
215
+ description: Optional[str],
216
+ skills: Optional[str],
217
+ agents: Optional[str],
218
+ hooks_deps: Optional[str],
219
+ rules: Optional[str],
220
+ ) -> None:
221
+ """Create a new shared agent.
222
+
223
+ Creates an agent in the shared/agents/ directory with optional dependencies
224
+ on other shared components.
225
+
226
+ \b
227
+ Structure created:
228
+ shared/agents/<name>/
229
+ AGENT.md - Agent instructions
230
+ agent.json - Agent metadata and dependencies
231
+
232
+ \b
233
+ Examples:
234
+ cldpm create agent debugger
235
+ cldpm create agent debugger -d "Debugging assistant"
236
+ cldpm create agent security-audit --skills vuln-scan,code-review
237
+ cldpm create agent full-audit -s scan -a reviewer -h pre-commit -r security
238
+ """
239
+ _create_component("agents", name, description, skills, agents, hooks_deps, rules)
240
+
241
+
242
+ @create.command()
243
+ @click.argument("name")
244
+ @click.option("--description", "-d", help="Hook description")
245
+ @click.option("--skills", "-s", help="Comma-separated list of skill dependencies")
246
+ @click.option("--hooks", "-h", "hooks_deps", help="Comma-separated list of hook dependencies")
247
+ @click.option("--rules", "-r", help="Comma-separated list of rule dependencies")
248
+ def hook(
249
+ name: str,
250
+ description: Optional[str],
251
+ skills: Optional[str],
252
+ hooks_deps: Optional[str],
253
+ rules: Optional[str],
254
+ ) -> None:
255
+ """Create a new shared hook.
256
+
257
+ Creates a hook in the shared/hooks/ directory with optional dependencies
258
+ on other shared components.
259
+
260
+ \b
261
+ Structure created:
262
+ shared/hooks/<name>/
263
+ HOOK.md - Hook instructions
264
+ hook.json - Hook metadata and dependencies
265
+
266
+ \b
267
+ Examples:
268
+ cldpm create hook pre-commit
269
+ cldpm create hook pre-commit -d "Pre-commit validation"
270
+ cldpm create hook full-validate --skills lint,format --rules style
271
+ """
272
+ _create_component("hooks", name, description, skills, None, hooks_deps, rules)
273
+
274
+
275
+ @create.command()
276
+ @click.argument("name")
277
+ @click.option("--description", "-d", help="Rule description")
278
+ @click.option("--rules", "-r", help="Comma-separated list of rule dependencies")
279
+ def rule(
280
+ name: str,
281
+ description: Optional[str],
282
+ rules: Optional[str],
283
+ ) -> None:
284
+ """Create a new shared rule.
285
+
286
+ Creates a rule in the shared/rules/ directory with optional dependencies
287
+ on other rules.
288
+
289
+ \b
290
+ Structure created:
291
+ shared/rules/<name>/
292
+ RULE.md - Rule instructions
293
+ rule.json - Rule metadata and dependencies
294
+
295
+ \b
296
+ Examples:
297
+ cldpm create rule security
298
+ cldpm create rule security -d "Security guidelines"
299
+ cldpm create rule full-compliance --rules security,privacy,logging
300
+ """
301
+ _create_component("rules", name, description, None, None, None, rules)
302
+
303
+
304
+ def _create_component(
305
+ component_type: str,
306
+ name: str,
307
+ description: Optional[str],
308
+ skills: Optional[str],
309
+ agents: Optional[str],
310
+ hooks: Optional[str],
311
+ rules: Optional[str],
312
+ ) -> None:
313
+ """Create a shared component.
314
+
315
+ Args:
316
+ component_type: Type of component (skills, agents, hooks, rules).
317
+ name: Component name.
318
+ description: Component description.
319
+ skills: Comma-separated skill dependencies.
320
+ agents: Comma-separated agent dependencies.
321
+ hooks: Comma-separated hook dependencies.
322
+ rules: Comma-separated rule dependencies.
323
+ """
324
+ # Find repo root
325
+ repo_root = find_repo_root()
326
+ if repo_root is None:
327
+ print_error("Not in a CLDPM mono repo. Run 'cldpm init' first.")
328
+ raise SystemExit(1)
329
+
330
+ # Load config
331
+ cldpm_config = load_cldpm_config(repo_root)
332
+
333
+ # Create component directory
334
+ component_path = repo_root / cldpm_config.shared_dir / component_type / name
335
+
336
+ if component_path.exists():
337
+ print_error(f"Component already exists: {component_type}/{name}")
338
+ raise SystemExit(1)
339
+
340
+ ensure_dir(component_path)
341
+
342
+ # Parse dependencies
343
+ deps = ComponentDependencies(
344
+ skills=parse_dependency_list(skills),
345
+ agents=parse_dependency_list(agents),
346
+ hooks=parse_dependency_list(hooks),
347
+ rules=parse_dependency_list(rules),
348
+ )
349
+
350
+ # Create metadata
351
+ metadata = ComponentMetadata(
352
+ name=name,
353
+ description=description,
354
+ dependencies=deps,
355
+ )
356
+ save_component_metadata(metadata, component_path, component_type)
357
+
358
+ # Create content file from template
359
+ singular_type = component_type.rstrip("s") # skills -> skill
360
+ content_filename = f"{singular_type.upper()}.md"
361
+
362
+ env = Environment(loader=PackageLoader("cldpm", "templates"))
363
+
364
+ # Try to load component-specific template, fall back to generic
365
+ try:
366
+ template = env.get_template(f"{singular_type}.md.j2")
367
+ except Exception:
368
+ # Use generic template
369
+ template_content = f"""# {name}
370
+
371
+ {description or f"A shared {singular_type}."}
372
+
373
+ ## Overview
374
+
375
+ Describe what this {singular_type} does.
376
+
377
+ ## Usage
378
+
379
+ Explain how to use this {singular_type}.
380
+ """
381
+ (component_path / content_filename).write_text(template_content)
382
+ _print_component_success(component_type, name, deps, component_path)
383
+ return
384
+
385
+ content = template.render(
386
+ name=name,
387
+ description=description or "",
388
+ )
389
+ (component_path / content_filename).write_text(content)
390
+
391
+ _print_component_success(component_type, name, deps, component_path)
392
+
393
+
394
+ def _print_component_success(
395
+ component_type: str,
396
+ name: str,
397
+ deps: ComponentDependencies,
398
+ component_path: Path,
399
+ ) -> None:
400
+ """Print success message for component creation."""
401
+ singular_type = component_type.rstrip("s")
402
+ print_success(f"Created {singular_type}: {name}")
403
+
404
+ # Show dependencies if any
405
+ all_deps = []
406
+ if deps.skills:
407
+ all_deps.extend([f"skills/{s}" for s in deps.skills])
408
+ if deps.agents:
409
+ all_deps.extend([f"agents/{a}" for a in deps.agents])
410
+ if deps.hooks:
411
+ all_deps.extend([f"hooks/{h}" for h in deps.hooks])
412
+ if deps.rules:
413
+ all_deps.extend([f"rules/{r}" for r in deps.rules])
414
+
415
+ if all_deps:
416
+ console.print(f" Dependencies: {', '.join(all_deps)}")
417
+
418
+ console.print(f" Location: {component_path}")