devsync 0.5.5__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.
Files changed (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. devsync-0.5.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,743 @@
1
+ """Package creator for generating shareable configuration packages."""
2
+
3
+ import json
4
+ import logging
5
+ import shutil
6
+ import subprocess
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import yaml
13
+
14
+ from aiconfigkit.core.component_detector import ComponentDetector, DetectedMCPServer, DetectionResult
15
+ from aiconfigkit.core.models import (
16
+ CommandComponent,
17
+ CredentialDescriptor,
18
+ HookComponent,
19
+ InstructionComponent,
20
+ MCPServerComponent,
21
+ MemoryFileComponent,
22
+ PackageComponents,
23
+ ResourceComponent,
24
+ SkillComponent,
25
+ WorkflowComponent,
26
+ )
27
+ from aiconfigkit.core.secret_detector import SecretDetector, template_secrets_in_config
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ @dataclass
33
+ class PackageMetadata:
34
+ """Metadata for the package being created.
35
+
36
+ Attributes:
37
+ name: Package identifier (lowercase, hyphenated)
38
+ version: Semantic version string
39
+ description: Human-readable description
40
+ author: Package author name
41
+ license: License identifier (e.g., MIT, Apache-2.0)
42
+ namespace: Repository namespace (e.g., owner/repo)
43
+ """
44
+
45
+ name: str
46
+ version: str = "1.0.0"
47
+ description: str = ""
48
+ author: str = ""
49
+ license: str = "MIT"
50
+ namespace: str = "local/local"
51
+
52
+
53
+ @dataclass
54
+ class PackageCreationResult:
55
+ """Result of package creation operation.
56
+
57
+ Attributes:
58
+ success: Whether package was created successfully
59
+ package_path: Path to created package directory
60
+ manifest_path: Path to generated manifest file
61
+ components_included: Number of components in package
62
+ secrets_templated: Number of secrets that were templated
63
+ warnings: Non-fatal issues encountered
64
+ error_message: Error description if failed
65
+ """
66
+
67
+ success: bool
68
+ package_path: Optional[Path] = None
69
+ manifest_path: Optional[Path] = None
70
+ components_included: int = 0
71
+ secrets_templated: int = 0
72
+ warnings: list[str] = field(default_factory=list)
73
+ error_message: Optional[str] = None
74
+
75
+
76
+ class PackageCreator:
77
+ """Creates shareable configuration packages from project components.
78
+
79
+ The creator scans a project for configuration components (instructions,
80
+ MCP servers, hooks, commands, resources), templates secrets, and generates
81
+ a package directory with manifest.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ project_root: Path,
87
+ output_dir: Path,
88
+ metadata: PackageMetadata,
89
+ scrub_secrets: bool = True,
90
+ ):
91
+ """Initialize package creator.
92
+
93
+ Args:
94
+ project_root: Path to source project
95
+ output_dir: Directory where package will be created
96
+ metadata: Package metadata
97
+ scrub_secrets: Whether to template secrets in MCP configs
98
+ """
99
+ self.project_root = project_root.resolve()
100
+ self.output_dir = output_dir.resolve()
101
+ self.metadata = metadata
102
+ self.scrub_secrets = scrub_secrets
103
+ self.secret_detector = SecretDetector()
104
+ self.component_detector = ComponentDetector(self.project_root)
105
+
106
+ def create(
107
+ self,
108
+ selected_components: Optional[PackageComponents] = None,
109
+ detection_result: Optional[DetectionResult] = None,
110
+ ) -> PackageCreationResult:
111
+ """Create package from project components.
112
+
113
+ Args:
114
+ selected_components: Specific components to include (None = detect all)
115
+ detection_result: Pre-scanned detection result (None = scan now)
116
+
117
+ Returns:
118
+ PackageCreationResult with operation outcome
119
+ """
120
+ warnings: list[str] = []
121
+ secrets_templated = 0
122
+
123
+ try:
124
+ if detection_result is None:
125
+ detection_result = self.component_detector.detect_all()
126
+
127
+ if selected_components is None:
128
+ selected_components = self.component_detector.to_package_components(detection_result)
129
+
130
+ if selected_components.total_count == 0:
131
+ return PackageCreationResult(
132
+ success=False,
133
+ error_message="No components to package",
134
+ warnings=warnings,
135
+ )
136
+
137
+ package_dir = self.output_dir / f"package-{self.metadata.name}"
138
+
139
+ if package_dir.exists():
140
+ return PackageCreationResult(
141
+ success=False,
142
+ error_message=f"Package directory already exists: {package_dir}",
143
+ warnings=warnings,
144
+ )
145
+
146
+ package_dir.mkdir(parents=True)
147
+
148
+ sub_dirs = ["instructions", "mcp", "hooks", "commands", "skills", "workflows", "memory_files", "resources"]
149
+ for sub_dir in sub_dirs:
150
+ (package_dir / sub_dir).mkdir(exist_ok=True)
151
+
152
+ copy_warnings = self._copy_component_files(detection_result, selected_components, package_dir)
153
+ warnings.extend(copy_warnings)
154
+
155
+ mcp_warnings, mcp_secrets = self._process_mcp_servers(detection_result.mcp_servers, package_dir)
156
+ warnings.extend(mcp_warnings)
157
+ secrets_templated = mcp_secrets
158
+
159
+ final_components = self._update_component_paths(selected_components, detection_result)
160
+
161
+ manifest = self._generate_manifest(final_components)
162
+ manifest_path = package_dir / "ai-config-kit-package.yaml"
163
+ with open(manifest_path, "w", encoding="utf-8") as f:
164
+ yaml.dump(manifest, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
165
+
166
+ readme_content = self._generate_readme(final_components)
167
+ readme_path = package_dir / "README.md"
168
+ with open(readme_path, "w", encoding="utf-8") as f:
169
+ f.write(readme_content)
170
+
171
+ return PackageCreationResult(
172
+ success=True,
173
+ package_path=package_dir,
174
+ manifest_path=manifest_path,
175
+ components_included=final_components.total_count,
176
+ secrets_templated=secrets_templated,
177
+ warnings=warnings,
178
+ )
179
+
180
+ except Exception as e:
181
+ logger.exception(f"Package creation failed: {e}")
182
+ return PackageCreationResult(
183
+ success=False,
184
+ error_message=str(e),
185
+ warnings=warnings,
186
+ )
187
+
188
+ def _copy_component_files(
189
+ self,
190
+ detection_result: DetectionResult,
191
+ components: PackageComponents,
192
+ package_dir: Path,
193
+ ) -> list[str]:
194
+ """Copy component files to package directory.
195
+
196
+ Args:
197
+ detection_result: Scan results with file paths
198
+ components: Selected components
199
+ package_dir: Destination package directory
200
+
201
+ Returns:
202
+ List of warning messages
203
+ """
204
+ warnings: list[str] = []
205
+
206
+ instruction_names = {inst.name for inst in components.instructions}
207
+ for inst in detection_result.instructions:
208
+ if inst.name not in instruction_names:
209
+ continue
210
+
211
+ src_path = inst.file_path
212
+ dest_path = package_dir / "instructions" / f"{inst.name}.md"
213
+
214
+ try:
215
+ shutil.copy2(src_path, dest_path)
216
+ except Exception as e:
217
+ warnings.append(f"Failed to copy instruction {inst.name}: {e}")
218
+
219
+ hook_names = {h.name for h in components.hooks}
220
+ for hook in detection_result.hooks:
221
+ if hook.name not in hook_names:
222
+ continue
223
+
224
+ src_path = hook.file_path
225
+ dest_path = package_dir / "hooks" / src_path.name
226
+
227
+ try:
228
+ shutil.copy2(src_path, dest_path)
229
+ except Exception as e:
230
+ warnings.append(f"Failed to copy hook {hook.name}: {e}")
231
+
232
+ cmd_names = {c.name for c in components.commands}
233
+ for cmd in detection_result.commands:
234
+ if cmd.name not in cmd_names:
235
+ continue
236
+
237
+ src_path = cmd.file_path
238
+ dest_path = package_dir / "commands" / src_path.name
239
+
240
+ try:
241
+ shutil.copy2(src_path, dest_path)
242
+ except Exception as e:
243
+ warnings.append(f"Failed to copy command {cmd.name}: {e}")
244
+
245
+ resource_names = {r.name for r in components.resources}
246
+ for res in detection_result.resources:
247
+ if res.name not in resource_names:
248
+ continue
249
+
250
+ src_path = res.file_path
251
+ dest_path = package_dir / "resources" / src_path.name
252
+
253
+ try:
254
+ shutil.copy2(src_path, dest_path)
255
+ except Exception as e:
256
+ warnings.append(f"Failed to copy resource {res.name}: {e}")
257
+
258
+ # Copy skills (directories)
259
+ skill_names = {s.name for s in components.skills}
260
+ for skill in detection_result.skills:
261
+ if skill.name not in skill_names:
262
+ continue
263
+
264
+ src_path = skill.dir_path
265
+ dest_path = package_dir / "skills" / skill.name
266
+
267
+ try:
268
+ shutil.copytree(src_path, dest_path)
269
+ except Exception as e:
270
+ warnings.append(f"Failed to copy skill {skill.name}: {e}")
271
+
272
+ # Copy workflows
273
+ workflow_names = {w.name for w in components.workflows}
274
+ for wf in detection_result.workflows:
275
+ if wf.name not in workflow_names:
276
+ continue
277
+
278
+ src_path = wf.file_path
279
+ dest_path = package_dir / "workflows" / src_path.name
280
+
281
+ try:
282
+ shutil.copy2(src_path, dest_path)
283
+ except Exception as e:
284
+ warnings.append(f"Failed to copy workflow {wf.name}: {e}")
285
+
286
+ # Copy memory files
287
+ memory_file_names = {m.name for m in components.memory_files}
288
+ for mem in detection_result.memory_files:
289
+ if mem.name not in memory_file_names:
290
+ continue
291
+
292
+ src_path = mem.file_path
293
+ # Use relative path for subdirectory memory files
294
+ if mem.is_root:
295
+ dest_path = package_dir / "memory_files" / "CLAUDE.md"
296
+ else:
297
+ # Preserve directory structure for subdirectory memory files
298
+ dest_path = package_dir / "memory_files" / mem.relative_path
299
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
300
+
301
+ try:
302
+ shutil.copy2(src_path, dest_path)
303
+ except Exception as e:
304
+ warnings.append(f"Failed to copy memory file {mem.name}: {e}")
305
+
306
+ return warnings
307
+
308
+ def _process_mcp_servers(
309
+ self,
310
+ detected_servers: list[DetectedMCPServer],
311
+ package_dir: Path,
312
+ ) -> tuple[list[str], int]:
313
+ """Process MCP server configurations and template secrets.
314
+
315
+ Args:
316
+ detected_servers: Detected MCP server configs
317
+ package_dir: Destination package directory
318
+
319
+ Returns:
320
+ Tuple of (warnings, count of templated secrets)
321
+ """
322
+ warnings: list[str] = []
323
+ total_secrets = 0
324
+
325
+ for server in detected_servers:
326
+ try:
327
+ if self.scrub_secrets:
328
+ templated_config, templated_keys = template_secrets_in_config(server.config, self.secret_detector)
329
+ total_secrets += len(templated_keys)
330
+ else:
331
+ templated_config = server.config
332
+
333
+ dest_path = package_dir / "mcp" / f"{server.name}.json"
334
+ with open(dest_path, "w", encoding="utf-8") as f:
335
+ json.dump(templated_config, f, indent=2)
336
+
337
+ except Exception as e:
338
+ warnings.append(f"Failed to process MCP server {server.name}: {e}")
339
+
340
+ return warnings, total_secrets
341
+
342
+ def _update_component_paths(
343
+ self,
344
+ components: PackageComponents,
345
+ detection_result: DetectionResult,
346
+ ) -> PackageComponents:
347
+ """Update component file paths to package-relative paths.
348
+
349
+ Args:
350
+ components: Original components with project paths
351
+ detection_result: Detection results for reference
352
+
353
+ Returns:
354
+ Updated PackageComponents with package-relative paths
355
+ """
356
+ updated_instructions = []
357
+ for inst in components.instructions:
358
+ updated_instructions.append(
359
+ InstructionComponent(
360
+ name=inst.name,
361
+ file=f"instructions/{inst.name}.md",
362
+ description=inst.description,
363
+ tags=inst.tags,
364
+ ide_support=inst.ide_support,
365
+ )
366
+ )
367
+
368
+ updated_mcp = []
369
+ mcp_map = {m.name: m for m in detection_result.mcp_servers}
370
+ for mcp in components.mcp_servers:
371
+ detected = mcp_map.get(mcp.name)
372
+ credentials: list[CredentialDescriptor] = []
373
+ if detected and self.scrub_secrets:
374
+ for env_var in detected.env_vars:
375
+ detection = self.secret_detector.detect("placeholder", env_var)
376
+ if detection.confidence.value in ("high", "medium"):
377
+ credentials.append(
378
+ CredentialDescriptor(
379
+ name=env_var,
380
+ description=f"Environment variable for {mcp.name}",
381
+ required=True,
382
+ )
383
+ )
384
+
385
+ updated_mcp.append(
386
+ MCPServerComponent(
387
+ name=mcp.name,
388
+ file=f"mcp/{mcp.name}.json",
389
+ description=mcp.description,
390
+ credentials=credentials,
391
+ ide_support=mcp.ide_support,
392
+ )
393
+ )
394
+
395
+ updated_hooks = []
396
+ hook_map = {h.name: h for h in detection_result.hooks}
397
+ for hook in components.hooks:
398
+ detected_hook = hook_map.get(hook.name)
399
+ hook_file_name = detected_hook.file_path.name if detected_hook else f"{hook.name}.sh"
400
+ updated_hooks.append(
401
+ HookComponent(
402
+ name=hook.name,
403
+ file=f"hooks/{hook_file_name}",
404
+ description=hook.description,
405
+ hook_type=hook.hook_type,
406
+ ide_support=hook.ide_support,
407
+ )
408
+ )
409
+
410
+ updated_commands = []
411
+ cmd_map = {c.name: c for c in detection_result.commands}
412
+ for cmd in components.commands:
413
+ detected_cmd = cmd_map.get(cmd.name)
414
+ cmd_file_name = detected_cmd.file_path.name if detected_cmd else f"{cmd.name}.sh"
415
+ updated_commands.append(
416
+ CommandComponent(
417
+ name=cmd.name,
418
+ file=f"commands/{cmd_file_name}",
419
+ description=cmd.description,
420
+ command_type=cmd.command_type,
421
+ ide_support=cmd.ide_support,
422
+ )
423
+ )
424
+
425
+ updated_resources = []
426
+ res_map = {r.name: r for r in detection_result.resources}
427
+ for res in components.resources:
428
+ detected_res = res_map.get(res.name)
429
+ res_file_name = detected_res.file_path.name if detected_res else res.name
430
+ res_checksum = detected_res.checksum if detected_res else res.checksum
431
+ res_size = detected_res.size if detected_res else res.size
432
+ updated_resources.append(
433
+ ResourceComponent(
434
+ name=res.name,
435
+ file=f"resources/{res_file_name}",
436
+ description=res.description,
437
+ install_path=f"resources/{res_file_name}",
438
+ checksum=f"sha256:{res_checksum}" if not res_checksum.startswith("sha256:") else res_checksum,
439
+ size=res_size,
440
+ )
441
+ )
442
+
443
+ updated_skills = []
444
+ skill_map = {s.name: s for s in detection_result.skills}
445
+ for skill in components.skills:
446
+ detected_skill = skill_map.get(skill.name)
447
+ updated_skills.append(
448
+ SkillComponent(
449
+ name=skill.name,
450
+ file=f"skills/{skill.name}",
451
+ description=detected_skill.description if detected_skill else skill.description,
452
+ ide_support=skill.ide_support,
453
+ )
454
+ )
455
+
456
+ updated_workflows = []
457
+ workflow_map = {w.name: w for w in detection_result.workflows}
458
+ for wf in components.workflows:
459
+ detected_wf = workflow_map.get(wf.name)
460
+ wf_file_name = detected_wf.file_path.name if detected_wf else f"{wf.name}.md"
461
+ updated_workflows.append(
462
+ WorkflowComponent(
463
+ name=wf.name,
464
+ file=f"workflows/{wf_file_name}",
465
+ description=detected_wf.description if detected_wf else wf.description,
466
+ ide_support=wf.ide_support,
467
+ )
468
+ )
469
+
470
+ updated_memory_files = []
471
+ memory_map = {m.name: m for m in detection_result.memory_files}
472
+ for mem in components.memory_files:
473
+ detected_mem = memory_map.get(mem.name)
474
+ if detected_mem:
475
+ if detected_mem.is_root:
476
+ mem_file = "memory_files/CLAUDE.md"
477
+ else:
478
+ mem_file = f"memory_files/{detected_mem.relative_path}"
479
+ else:
480
+ mem_file = f"memory_files/{mem.name}.md"
481
+ updated_memory_files.append(
482
+ MemoryFileComponent(
483
+ name=mem.name,
484
+ file=mem_file,
485
+ description=mem.description,
486
+ ide_support=mem.ide_support,
487
+ )
488
+ )
489
+
490
+ return PackageComponents(
491
+ instructions=updated_instructions,
492
+ mcp_servers=updated_mcp,
493
+ hooks=updated_hooks,
494
+ commands=updated_commands,
495
+ skills=updated_skills,
496
+ workflows=updated_workflows,
497
+ memory_files=updated_memory_files,
498
+ resources=updated_resources,
499
+ )
500
+
501
+ def _generate_manifest(self, components: PackageComponents) -> dict:
502
+ """Generate YAML manifest dictionary.
503
+
504
+ Args:
505
+ components: Package components
506
+
507
+ Returns:
508
+ Manifest dictionary for YAML serialization
509
+ """
510
+ manifest: dict = {
511
+ "name": self.metadata.name,
512
+ "version": self.metadata.version,
513
+ "description": self.metadata.description,
514
+ "author": self.metadata.author,
515
+ "license": self.metadata.license,
516
+ "namespace": self.metadata.namespace,
517
+ "created_at": datetime.now().isoformat(),
518
+ "components": {},
519
+ }
520
+
521
+ if components.instructions:
522
+ manifest["components"]["instructions"] = [
523
+ {
524
+ "name": inst.name,
525
+ "file": inst.file,
526
+ "description": inst.description,
527
+ "tags": inst.tags,
528
+ }
529
+ for inst in components.instructions
530
+ ]
531
+
532
+ if components.mcp_servers:
533
+ manifest["components"]["mcp_servers"] = [
534
+ {
535
+ "name": mcp.name,
536
+ "file": mcp.file,
537
+ "description": mcp.description,
538
+ "credentials": [
539
+ {
540
+ "name": cred.name,
541
+ "description": cred.description,
542
+ "required": cred.required,
543
+ }
544
+ for cred in mcp.credentials
545
+ ],
546
+ "ide_support": mcp.ide_support,
547
+ }
548
+ for mcp in components.mcp_servers
549
+ ]
550
+
551
+ if components.hooks:
552
+ manifest["components"]["hooks"] = [
553
+ {
554
+ "name": hook.name,
555
+ "file": hook.file,
556
+ "description": hook.description,
557
+ "hook_type": hook.hook_type,
558
+ "ide_support": hook.ide_support,
559
+ }
560
+ for hook in components.hooks
561
+ ]
562
+
563
+ if components.commands:
564
+ manifest["components"]["commands"] = [
565
+ {
566
+ "name": cmd.name,
567
+ "file": cmd.file,
568
+ "description": cmd.description,
569
+ "command_type": cmd.command_type,
570
+ "ide_support": cmd.ide_support,
571
+ }
572
+ for cmd in components.commands
573
+ ]
574
+
575
+ if components.resources:
576
+ manifest["components"]["resources"] = [
577
+ {
578
+ "name": res.name,
579
+ "file": res.file,
580
+ "description": res.description,
581
+ "install_path": res.install_path,
582
+ "checksum": res.checksum,
583
+ "size": res.size,
584
+ }
585
+ for res in components.resources
586
+ ]
587
+
588
+ if components.skills:
589
+ manifest["components"]["skills"] = [
590
+ {
591
+ "name": skill.name,
592
+ "file": skill.file,
593
+ "description": skill.description,
594
+ "ide_support": skill.ide_support,
595
+ }
596
+ for skill in components.skills
597
+ ]
598
+
599
+ if components.workflows:
600
+ manifest["components"]["workflows"] = [
601
+ {
602
+ "name": wf.name,
603
+ "file": wf.file,
604
+ "description": wf.description,
605
+ "ide_support": wf.ide_support,
606
+ }
607
+ for wf in components.workflows
608
+ ]
609
+
610
+ if components.memory_files:
611
+ manifest["components"]["memory_files"] = [
612
+ {
613
+ "name": mem.name,
614
+ "file": mem.file,
615
+ "description": mem.description,
616
+ "ide_support": mem.ide_support,
617
+ }
618
+ for mem in components.memory_files
619
+ ]
620
+
621
+ return manifest
622
+
623
+ def _generate_readme(self, components: PackageComponents) -> str:
624
+ """Generate README.md content for the package.
625
+
626
+ Args:
627
+ components: Package components
628
+
629
+ Returns:
630
+ README content string
631
+ """
632
+ lines = [
633
+ f"# {self.metadata.name}",
634
+ "",
635
+ self.metadata.description or "Configuration package created with Config Sync.",
636
+ "",
637
+ "## Installation",
638
+ "",
639
+ "```bash",
640
+ f"aiconfig package install ./package-{self.metadata.name} --ide claude",
641
+ "```",
642
+ "",
643
+ "## Components",
644
+ "",
645
+ ]
646
+
647
+ if components.instructions:
648
+ lines.append("### Instructions")
649
+ lines.append("")
650
+ for inst in components.instructions:
651
+ lines.append(f"- **{inst.name}**: {inst.description}")
652
+ lines.append("")
653
+
654
+ if components.mcp_servers:
655
+ lines.append("### MCP Servers")
656
+ lines.append("")
657
+ for mcp in components.mcp_servers:
658
+ lines.append(f"- **{mcp.name}**: {mcp.description}")
659
+ if mcp.credentials:
660
+ lines.append(" - Required credentials:")
661
+ for cred in mcp.credentials:
662
+ lines.append(f" - `{cred.name}`: {cred.description}")
663
+ lines.append("")
664
+
665
+ if components.hooks:
666
+ lines.append("### Hooks")
667
+ lines.append("")
668
+ for hook in components.hooks:
669
+ lines.append(f"- **{hook.name}** ({hook.hook_type}): {hook.description}")
670
+ lines.append("")
671
+
672
+ if components.commands:
673
+ lines.append("### Commands")
674
+ lines.append("")
675
+ for cmd in components.commands:
676
+ lines.append(f"- **{cmd.name}** ({cmd.command_type}): {cmd.description}")
677
+ lines.append("")
678
+
679
+ if components.resources:
680
+ lines.append("### Resources")
681
+ lines.append("")
682
+ for res in components.resources:
683
+ lines.append(f"- **{res.name}**: {res.description}")
684
+ lines.append("")
685
+
686
+ if components.skills:
687
+ lines.append("### Skills")
688
+ lines.append("")
689
+ for skill in components.skills:
690
+ lines.append(f"- **{skill.name}**: {skill.description}")
691
+ lines.append("")
692
+
693
+ if components.workflows:
694
+ lines.append("### Workflows")
695
+ lines.append("")
696
+ for wf in components.workflows:
697
+ lines.append(f"- **{wf.name}**: {wf.description}")
698
+ lines.append("")
699
+
700
+ if components.memory_files:
701
+ lines.append("### Memory Files")
702
+ lines.append("")
703
+ for mem in components.memory_files:
704
+ lines.append(f"- **{mem.name}**: {mem.description}")
705
+ lines.append("")
706
+
707
+ lines.extend(
708
+ [
709
+ "## Author",
710
+ "",
711
+ self.metadata.author or "Unknown",
712
+ "",
713
+ "## License",
714
+ "",
715
+ self.metadata.license,
716
+ "",
717
+ "---",
718
+ "",
719
+ f"*Generated with Config Sync on {datetime.now().strftime('%Y-%m-%d')}*",
720
+ ]
721
+ )
722
+
723
+ return "\n".join(lines)
724
+
725
+
726
+ def get_git_author() -> Optional[str]:
727
+ """Get the current git user name.
728
+
729
+ Returns:
730
+ Git user name or None if not configured
731
+ """
732
+ try:
733
+ result = subprocess.run(
734
+ ["git", "config", "user.name"],
735
+ capture_output=True,
736
+ text=True,
737
+ timeout=5,
738
+ )
739
+ if result.returncode == 0:
740
+ return result.stdout.strip()
741
+ except Exception:
742
+ pass
743
+ return None