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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- 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
|