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,472 @@
|
|
|
1
|
+
"""Package installation command and logic."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from aiconfigkit.ai_tools.capability_registry import get_capability
|
|
10
|
+
from aiconfigkit.ai_tools.translator import get_translator
|
|
11
|
+
from aiconfigkit.core.models import (
|
|
12
|
+
AIToolType,
|
|
13
|
+
ComponentStatus,
|
|
14
|
+
ComponentType,
|
|
15
|
+
ConflictResolution,
|
|
16
|
+
InstallationScope,
|
|
17
|
+
InstallationStatus,
|
|
18
|
+
InstalledComponent,
|
|
19
|
+
Package,
|
|
20
|
+
PackageInstallationRecord,
|
|
21
|
+
)
|
|
22
|
+
from aiconfigkit.core.package_manifest import PackageManifestParser
|
|
23
|
+
from aiconfigkit.storage.package_tracker import PackageTracker
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class InstallationResult:
|
|
30
|
+
"""Result of package installation operation."""
|
|
31
|
+
|
|
32
|
+
success: bool
|
|
33
|
+
status: InstallationStatus
|
|
34
|
+
package_name: str
|
|
35
|
+
version: str
|
|
36
|
+
installed_count: int = 0
|
|
37
|
+
skipped_count: int = 0
|
|
38
|
+
failed_count: int = 0
|
|
39
|
+
components_installed: dict[ComponentType, int] = field(default_factory=dict)
|
|
40
|
+
error_message: Optional[str] = None
|
|
41
|
+
is_reinstall: bool = False
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def total_components(self) -> int:
|
|
45
|
+
"""Total number of components processed."""
|
|
46
|
+
return self.installed_count + self.skipped_count + self.failed_count
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def install_package(
|
|
50
|
+
package_path: Path,
|
|
51
|
+
project_root: Path,
|
|
52
|
+
target_ide: AIToolType,
|
|
53
|
+
scope: InstallationScope = InstallationScope.PROJECT,
|
|
54
|
+
conflict_resolution: ConflictResolution = ConflictResolution.SKIP,
|
|
55
|
+
force: bool = False,
|
|
56
|
+
) -> InstallationResult:
|
|
57
|
+
"""
|
|
58
|
+
Install a package to a project for a specific IDE.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
package_path: Path to package directory containing manifest
|
|
62
|
+
project_root: Root directory of target project
|
|
63
|
+
target_ide: Target IDE for installation
|
|
64
|
+
scope: Installation scope (project or global)
|
|
65
|
+
conflict_resolution: How to handle file conflicts
|
|
66
|
+
force: Force reinstallation even if already installed
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
InstallationResult with details of installation
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
FileNotFoundError: If manifest not found
|
|
73
|
+
ValidationError: If manifest is invalid
|
|
74
|
+
"""
|
|
75
|
+
logger.info(f"Installing package from {package_path} to {project_root} for {target_ide.value}")
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Step 1: Parse and validate manifest
|
|
79
|
+
parser = PackageManifestParser(package_path)
|
|
80
|
+
package = parser.parse()
|
|
81
|
+
|
|
82
|
+
# Validate manifest
|
|
83
|
+
validation_errors = parser.validate(package)
|
|
84
|
+
if validation_errors:
|
|
85
|
+
error_msg = "; ".join(validation_errors)
|
|
86
|
+
return InstallationResult(
|
|
87
|
+
success=False,
|
|
88
|
+
status=InstallationStatus.FAILED,
|
|
89
|
+
package_name=package.name,
|
|
90
|
+
version=package.version,
|
|
91
|
+
error_message=f"Manifest validation failed: {error_msg}",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logger.info(f"Parsed package: {package.name} v{package.version}")
|
|
95
|
+
|
|
96
|
+
# Step 2: Check if already installed
|
|
97
|
+
tracker_file = project_root / ".ai-config-kit" / "packages.json"
|
|
98
|
+
tracker = PackageTracker(tracker_file)
|
|
99
|
+
is_reinstall = tracker.is_package_installed(package.name, scope)
|
|
100
|
+
|
|
101
|
+
if is_reinstall and not force:
|
|
102
|
+
logger.info(f"Package {package.name} already installed")
|
|
103
|
+
|
|
104
|
+
# Step 3: Get IDE capabilities
|
|
105
|
+
capability = get_capability(target_ide)
|
|
106
|
+
logger.debug(f"Target IDE capabilities: {capability.supported_components}")
|
|
107
|
+
|
|
108
|
+
# Step 4: Get component translator
|
|
109
|
+
translator = get_translator(target_ide)
|
|
110
|
+
|
|
111
|
+
# Step 5: Filter components by IDE capabilities and track skipped
|
|
112
|
+
installable_components = _filter_components_by_capability(package, capability)
|
|
113
|
+
|
|
114
|
+
# Calculate skipped components (filtered out by IDE capabilities)
|
|
115
|
+
total_in_package = package.components.total_count
|
|
116
|
+
installable_count = sum(len(comps) for comps in installable_components.values())
|
|
117
|
+
capability_skipped = total_in_package - installable_count
|
|
118
|
+
|
|
119
|
+
logger.info(
|
|
120
|
+
f"Found {installable_count} installable components, {capability_skipped} filtered by IDE capabilities"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Step 6: Install each component type
|
|
124
|
+
installed_components: list[InstalledComponent] = []
|
|
125
|
+
installed_count = 0
|
|
126
|
+
skipped_count = capability_skipped # Start with capability-filtered components
|
|
127
|
+
failed_count = 0
|
|
128
|
+
components_by_type: dict[ComponentType, int] = {}
|
|
129
|
+
|
|
130
|
+
# Install instructions
|
|
131
|
+
for instruction in installable_components.get("instructions", []):
|
|
132
|
+
result = _install_instruction_component(
|
|
133
|
+
instruction,
|
|
134
|
+
package_path,
|
|
135
|
+
project_root,
|
|
136
|
+
translator,
|
|
137
|
+
conflict_resolution,
|
|
138
|
+
)
|
|
139
|
+
if result:
|
|
140
|
+
installed_components.append(result)
|
|
141
|
+
installed_count += 1
|
|
142
|
+
components_by_type[ComponentType.INSTRUCTION] = components_by_type.get(ComponentType.INSTRUCTION, 0) + 1
|
|
143
|
+
else:
|
|
144
|
+
skipped_count += 1
|
|
145
|
+
|
|
146
|
+
# Install MCP servers
|
|
147
|
+
for mcp in installable_components.get("mcp_servers", []):
|
|
148
|
+
result = _install_mcp_component(mcp, package_path, project_root, translator, conflict_resolution)
|
|
149
|
+
if result:
|
|
150
|
+
installed_components.append(result)
|
|
151
|
+
installed_count += 1
|
|
152
|
+
components_by_type[ComponentType.MCP_SERVER] = components_by_type.get(ComponentType.MCP_SERVER, 0) + 1
|
|
153
|
+
else:
|
|
154
|
+
skipped_count += 1
|
|
155
|
+
|
|
156
|
+
# Install hooks
|
|
157
|
+
for hook in installable_components.get("hooks", []):
|
|
158
|
+
result = _install_hook_component(hook, package_path, project_root, translator, conflict_resolution)
|
|
159
|
+
if result:
|
|
160
|
+
installed_components.append(result)
|
|
161
|
+
installed_count += 1
|
|
162
|
+
components_by_type[ComponentType.HOOK] = components_by_type.get(ComponentType.HOOK, 0) + 1
|
|
163
|
+
else:
|
|
164
|
+
skipped_count += 1
|
|
165
|
+
|
|
166
|
+
# Install commands
|
|
167
|
+
for command in installable_components.get("commands", []):
|
|
168
|
+
result = _install_command_component(command, package_path, project_root, translator, conflict_resolution)
|
|
169
|
+
if result:
|
|
170
|
+
installed_components.append(result)
|
|
171
|
+
installed_count += 1
|
|
172
|
+
components_by_type[ComponentType.COMMAND] = components_by_type.get(ComponentType.COMMAND, 0) + 1
|
|
173
|
+
else:
|
|
174
|
+
skipped_count += 1
|
|
175
|
+
|
|
176
|
+
# Install resources
|
|
177
|
+
for resource in installable_components.get("resources", []):
|
|
178
|
+
result = _install_resource_component(resource, package_path, project_root, translator, conflict_resolution)
|
|
179
|
+
if result:
|
|
180
|
+
installed_components.append(result)
|
|
181
|
+
installed_count += 1
|
|
182
|
+
components_by_type[ComponentType.RESOURCE] = components_by_type.get(ComponentType.RESOURCE, 0) + 1
|
|
183
|
+
else:
|
|
184
|
+
skipped_count += 1
|
|
185
|
+
|
|
186
|
+
# Step 7: Determine installation status
|
|
187
|
+
total_in_package = package.components.total_count
|
|
188
|
+
if installed_count == 0:
|
|
189
|
+
status = InstallationStatus.FAILED
|
|
190
|
+
elif installed_count == total_in_package:
|
|
191
|
+
status = InstallationStatus.COMPLETE
|
|
192
|
+
else:
|
|
193
|
+
status = InstallationStatus.PARTIAL
|
|
194
|
+
|
|
195
|
+
# Step 8: Record installation
|
|
196
|
+
now = datetime.now()
|
|
197
|
+
# Get original install time for reinstalls
|
|
198
|
+
existing_record = tracker.get_package(package.name, scope) if is_reinstall else None
|
|
199
|
+
original_install_time = existing_record.installed_at if existing_record else now
|
|
200
|
+
|
|
201
|
+
installation_record = PackageInstallationRecord(
|
|
202
|
+
package_name=package.name,
|
|
203
|
+
namespace=package.namespace,
|
|
204
|
+
version=package.version,
|
|
205
|
+
installed_at=original_install_time,
|
|
206
|
+
updated_at=now,
|
|
207
|
+
scope=scope,
|
|
208
|
+
components=installed_components,
|
|
209
|
+
status=status,
|
|
210
|
+
)
|
|
211
|
+
tracker.record_installation(installation_record)
|
|
212
|
+
|
|
213
|
+
logger.info(
|
|
214
|
+
f"Installation complete: {installed_count} installed, {skipped_count} skipped, {failed_count} failed"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return InstallationResult(
|
|
218
|
+
success=True,
|
|
219
|
+
status=status,
|
|
220
|
+
package_name=package.name,
|
|
221
|
+
version=package.version,
|
|
222
|
+
installed_count=installed_count,
|
|
223
|
+
skipped_count=skipped_count,
|
|
224
|
+
failed_count=failed_count,
|
|
225
|
+
components_installed=components_by_type,
|
|
226
|
+
is_reinstall=is_reinstall,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(f"Installation failed: {e}", exc_info=True)
|
|
231
|
+
return InstallationResult(
|
|
232
|
+
success=False,
|
|
233
|
+
status=InstallationStatus.FAILED,
|
|
234
|
+
package_name=package_path.name if package_path else "unknown",
|
|
235
|
+
version="unknown",
|
|
236
|
+
error_message=str(e),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _filter_components_by_capability(package: Package, capability: Any) -> dict[str, list[Any]]:
|
|
241
|
+
"""Filter package components by IDE capability."""
|
|
242
|
+
filtered: dict[str, list[Any]] = {}
|
|
243
|
+
|
|
244
|
+
if capability.supports_component(ComponentType.INSTRUCTION):
|
|
245
|
+
filtered["instructions"] = package.components.instructions
|
|
246
|
+
|
|
247
|
+
if capability.supports_component(ComponentType.MCP_SERVER):
|
|
248
|
+
filtered["mcp_servers"] = package.components.mcp_servers
|
|
249
|
+
|
|
250
|
+
if capability.supports_component(ComponentType.HOOK):
|
|
251
|
+
filtered["hooks"] = package.components.hooks
|
|
252
|
+
|
|
253
|
+
if capability.supports_component(ComponentType.COMMAND):
|
|
254
|
+
filtered["commands"] = package.components.commands
|
|
255
|
+
|
|
256
|
+
if capability.supports_component(ComponentType.RESOURCE):
|
|
257
|
+
filtered["resources"] = package.components.resources
|
|
258
|
+
|
|
259
|
+
return filtered
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _install_instruction_component(
|
|
263
|
+
component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
|
|
264
|
+
) -> Optional[InstalledComponent]:
|
|
265
|
+
"""Install instruction component."""
|
|
266
|
+
try:
|
|
267
|
+
# Translate component
|
|
268
|
+
translated = translator.translate_instruction(component, package_path)
|
|
269
|
+
|
|
270
|
+
# Determine target path
|
|
271
|
+
target_file = project_root / translated.target_path
|
|
272
|
+
|
|
273
|
+
# Check for conflicts
|
|
274
|
+
if target_file.exists():
|
|
275
|
+
if conflict_resolution == ConflictResolution.SKIP:
|
|
276
|
+
logger.info(f"Skipping existing file: {target_file}")
|
|
277
|
+
return None
|
|
278
|
+
elif conflict_resolution == ConflictResolution.RENAME:
|
|
279
|
+
# Find available numbered suffix
|
|
280
|
+
counter = 1
|
|
281
|
+
stem = target_file.stem
|
|
282
|
+
suffix = target_file.suffix
|
|
283
|
+
while target_file.exists():
|
|
284
|
+
target_file = target_file.parent / f"{stem}-{counter}{suffix}"
|
|
285
|
+
counter += 1
|
|
286
|
+
logger.info(f"Renaming to avoid conflict: {target_file}")
|
|
287
|
+
|
|
288
|
+
# Install file
|
|
289
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
target_file.write_text(translated.content)
|
|
291
|
+
|
|
292
|
+
# Calculate checksum
|
|
293
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
294
|
+
|
|
295
|
+
checksum = calculate_file_checksum(str(target_file), "sha256")
|
|
296
|
+
|
|
297
|
+
return InstalledComponent(
|
|
298
|
+
type=ComponentType.INSTRUCTION,
|
|
299
|
+
name=component.name,
|
|
300
|
+
installed_path=str(target_file.relative_to(project_root)),
|
|
301
|
+
checksum=checksum,
|
|
302
|
+
status=ComponentStatus.INSTALLED,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Failed to install instruction {component.name}: {e}")
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _install_mcp_component(
|
|
311
|
+
component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
|
|
312
|
+
) -> Optional[InstalledComponent]:
|
|
313
|
+
"""Install MCP server component."""
|
|
314
|
+
try:
|
|
315
|
+
# Translate component
|
|
316
|
+
translated = translator.translate_mcp_server(component, package_path)
|
|
317
|
+
target_file = project_root / translated.target_path
|
|
318
|
+
|
|
319
|
+
# Check for conflicts
|
|
320
|
+
if target_file.exists() and conflict_resolution == ConflictResolution.SKIP:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
# Create target directory
|
|
324
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
|
|
326
|
+
# Write MCP config file
|
|
327
|
+
target_file.write_text(translated.content)
|
|
328
|
+
|
|
329
|
+
# Calculate checksum
|
|
330
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
331
|
+
|
|
332
|
+
checksum = calculate_file_checksum(str(target_file), "sha256")
|
|
333
|
+
|
|
334
|
+
return InstalledComponent(
|
|
335
|
+
type=ComponentType.MCP_SERVER,
|
|
336
|
+
name=component.name,
|
|
337
|
+
installed_path=str(target_file.relative_to(project_root)),
|
|
338
|
+
checksum=checksum,
|
|
339
|
+
status=ComponentStatus.INSTALLED,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(f"Failed to install MCP server {component.name}: {e}")
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _install_hook_component(
|
|
348
|
+
component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
|
|
349
|
+
) -> Optional[InstalledComponent]:
|
|
350
|
+
"""Install hook component."""
|
|
351
|
+
try:
|
|
352
|
+
translated = translator.translate_hook(component, package_path)
|
|
353
|
+
target_file = project_root / translated.target_path
|
|
354
|
+
|
|
355
|
+
# Check for conflicts
|
|
356
|
+
if target_file.exists():
|
|
357
|
+
if conflict_resolution == ConflictResolution.SKIP:
|
|
358
|
+
logger.info(f"Skipping existing file: {target_file}")
|
|
359
|
+
return None
|
|
360
|
+
elif conflict_resolution == ConflictResolution.RENAME:
|
|
361
|
+
# Find available numbered suffix
|
|
362
|
+
counter = 1
|
|
363
|
+
stem = target_file.stem
|
|
364
|
+
suffix = target_file.suffix
|
|
365
|
+
while target_file.exists():
|
|
366
|
+
target_file = target_file.parent / f"{stem}-{counter}{suffix}"
|
|
367
|
+
counter += 1
|
|
368
|
+
logger.info(f"Renaming to avoid conflict: {target_file}")
|
|
369
|
+
|
|
370
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
371
|
+
target_file.write_text(translated.content)
|
|
372
|
+
target_file.chmod(0o755) # Make executable
|
|
373
|
+
|
|
374
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
375
|
+
|
|
376
|
+
checksum = calculate_file_checksum(str(target_file), "sha256")
|
|
377
|
+
|
|
378
|
+
return InstalledComponent(
|
|
379
|
+
type=ComponentType.HOOK,
|
|
380
|
+
name=component.name,
|
|
381
|
+
installed_path=str(target_file.relative_to(project_root)),
|
|
382
|
+
checksum=checksum,
|
|
383
|
+
status=ComponentStatus.INSTALLED,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.error(f"Failed to install hook {component.name}: {e}")
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _install_command_component(
|
|
392
|
+
component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
|
|
393
|
+
) -> Optional[InstalledComponent]:
|
|
394
|
+
"""Install command component."""
|
|
395
|
+
try:
|
|
396
|
+
translated = translator.translate_command(component, package_path)
|
|
397
|
+
target_file = project_root / translated.target_path
|
|
398
|
+
|
|
399
|
+
# Check for conflicts
|
|
400
|
+
if target_file.exists():
|
|
401
|
+
if conflict_resolution == ConflictResolution.SKIP:
|
|
402
|
+
logger.info(f"Skipping existing file: {target_file}")
|
|
403
|
+
return None
|
|
404
|
+
elif conflict_resolution == ConflictResolution.RENAME:
|
|
405
|
+
# Find available numbered suffix
|
|
406
|
+
counter = 1
|
|
407
|
+
stem = target_file.stem
|
|
408
|
+
suffix = target_file.suffix
|
|
409
|
+
while target_file.exists():
|
|
410
|
+
target_file = target_file.parent / f"{stem}-{counter}{suffix}"
|
|
411
|
+
counter += 1
|
|
412
|
+
logger.info(f"Renaming to avoid conflict: {target_file}")
|
|
413
|
+
|
|
414
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
415
|
+
target_file.write_text(translated.content)
|
|
416
|
+
target_file.chmod(0o755) # Make executable
|
|
417
|
+
|
|
418
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
419
|
+
|
|
420
|
+
checksum = calculate_file_checksum(str(target_file), "sha256")
|
|
421
|
+
|
|
422
|
+
return InstalledComponent(
|
|
423
|
+
type=ComponentType.COMMAND,
|
|
424
|
+
name=component.name,
|
|
425
|
+
installed_path=str(target_file.relative_to(project_root)),
|
|
426
|
+
checksum=checksum,
|
|
427
|
+
status=ComponentStatus.INSTALLED,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
logger.error(f"Failed to install command {component.name}: {e}")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _install_resource_component(
|
|
436
|
+
component: Any, package_path: Path, project_root: Path, translator: Any, conflict_resolution: ConflictResolution
|
|
437
|
+
) -> Optional[InstalledComponent]:
|
|
438
|
+
"""Install resource component."""
|
|
439
|
+
try:
|
|
440
|
+
import shutil
|
|
441
|
+
|
|
442
|
+
translated = translator.translate_resource(component, package_path)
|
|
443
|
+
target_file = project_root / translated.target_path
|
|
444
|
+
|
|
445
|
+
if target_file.exists() and conflict_resolution == ConflictResolution.SKIP:
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
# Copy file directly (handles both text and binary)
|
|
449
|
+
source_path = translated.metadata.get("source_path")
|
|
450
|
+
if source_path:
|
|
451
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
452
|
+
shutil.copy2(source_path, target_file)
|
|
453
|
+
else:
|
|
454
|
+
# Fallback to writing content (for compatibility)
|
|
455
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
456
|
+
target_file.write_text(translated.content)
|
|
457
|
+
|
|
458
|
+
from aiconfigkit.core.checksum import calculate_file_checksum
|
|
459
|
+
|
|
460
|
+
checksum = calculate_file_checksum(str(target_file), "sha256")
|
|
461
|
+
|
|
462
|
+
return InstalledComponent(
|
|
463
|
+
type=ComponentType.RESOURCE,
|
|
464
|
+
name=component.name,
|
|
465
|
+
installed_path=str(target_file.relative_to(project_root)),
|
|
466
|
+
checksum=checksum,
|
|
467
|
+
status=ComponentStatus.INSTALLED,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
except Exception as e:
|
|
471
|
+
logger.error(f"Failed to install resource {component.name}: {e}")
|
|
472
|
+
return None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Template management commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
# Create template subcommand group
|
|
6
|
+
template_app = typer.Typer(
|
|
7
|
+
name="template",
|
|
8
|
+
help="Manage template repositories for consistent project standards",
|
|
9
|
+
add_completion=False,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@template_app.callback(invoke_without_command=True)
|
|
14
|
+
def template_callback(ctx: typer.Context) -> None:
|
|
15
|
+
"""Manage template repositories."""
|
|
16
|
+
# If no subcommand was provided, show help
|
|
17
|
+
if ctx.invoked_subcommand is None:
|
|
18
|
+
typer.echo(ctx.get_help())
|
|
19
|
+
raise typer.Exit(0)
|