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,194 @@
1
+ """UI utilities for terminal output."""
2
+
3
+ from typing import Optional
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ from aiconfigkit.core.models import InstallationRecord, Instruction, InstructionBundle
9
+
10
+
11
+ def format_instructions_table(
12
+ instructions: list[Instruction], bundles: list[InstructionBundle], show_bundles: bool = True
13
+ ) -> Table:
14
+ """
15
+ Format instructions and bundles as a Rich table.
16
+
17
+ Args:
18
+ instructions: List of instructions to display
19
+ bundles: List of bundles to display
20
+ show_bundles: Whether to show bundles section
21
+
22
+ Returns:
23
+ Rich Table object
24
+ """
25
+ table = Table(title="Available Instructions", show_header=True, header_style="bold cyan")
26
+
27
+ table.add_column("Name", style="cyan", no_wrap=True)
28
+ table.add_column("Type", style="magenta")
29
+ table.add_column("Description")
30
+ table.add_column("Tags", style="yellow")
31
+
32
+ # Add instructions
33
+ for inst in sorted(instructions, key=lambda x: x.name):
34
+ tags_str = ", ".join(inst.tags) if inst.tags else "-"
35
+ table.add_row(inst.name, "Instruction", inst.description, tags_str)
36
+
37
+ # Add bundles
38
+ if show_bundles:
39
+ for bundle in sorted(bundles, key=lambda x: x.name):
40
+ tags_str = ", ".join(bundle.tags) if bundle.tags else "-"
41
+ inst_count = f"{len(bundle.instructions)} instructions"
42
+ table.add_row(bundle.name, "Bundle", f"{bundle.description} ({inst_count})", tags_str)
43
+
44
+ return table
45
+
46
+
47
+ def format_installed_table(records: list[InstallationRecord], group_by_tool: bool = True) -> Table:
48
+ """
49
+ Format installed instructions as a Rich table.
50
+
51
+ Args:
52
+ records: List of installation records
53
+ group_by_tool: Whether to group by AI tool
54
+
55
+ Returns:
56
+ Rich Table object
57
+ """
58
+ table = Table(title="Installed Instructions", show_header=True, header_style="bold green")
59
+
60
+ if group_by_tool:
61
+ table.add_column("AI Tool", style="cyan", no_wrap=True)
62
+
63
+ table.add_column("Instruction", style="green", no_wrap=True)
64
+ table.add_column("Scope", style="blue", no_wrap=True)
65
+ table.add_column("Source Repository")
66
+ table.add_column("Version", style="magenta", no_wrap=True)
67
+ table.add_column("Installed", style="yellow")
68
+ table.add_column("Bundle", style="dim")
69
+
70
+ # Sort records
71
+ if group_by_tool:
72
+ sorted_records = sorted(records, key=lambda x: (x.ai_tool.value, x.instruction_name))
73
+ else:
74
+ sorted_records = sorted(records, key=lambda x: x.instruction_name)
75
+
76
+ # Add rows
77
+ current_tool = None
78
+ for record in sorted_records:
79
+ # Format installed date
80
+ installed_date = record.installed_at.strftime("%Y-%m-%d")
81
+
82
+ # Shorten repo URL for display
83
+ repo_display = _shorten_url(record.source_repo, max_length=50)
84
+
85
+ # Bundle name or "-"
86
+ bundle_display = record.bundle_name if record.bundle_name else "-"
87
+
88
+ # Scope display
89
+ scope_display = record.scope.value.capitalize()
90
+
91
+ # Version display with badge
92
+ if record.source_ref and record.source_ref_type:
93
+ ref_type_badges = {
94
+ "tag": "📌",
95
+ "branch": "🌿",
96
+ "commit": "📍",
97
+ }
98
+ badge = ref_type_badges.get(record.source_ref_type.value, "")
99
+ version_display = f"{badge} {record.source_ref}"
100
+ else:
101
+ version_display = "-"
102
+
103
+ if group_by_tool:
104
+ # Show tool name only on first occurrence
105
+ tool_display = record.ai_tool.value.capitalize() if record.ai_tool != current_tool else ""
106
+ current_tool = record.ai_tool
107
+
108
+ table.add_row(
109
+ tool_display,
110
+ record.instruction_name,
111
+ scope_display,
112
+ repo_display,
113
+ version_display,
114
+ installed_date,
115
+ bundle_display,
116
+ )
117
+ else:
118
+ table.add_row(
119
+ record.instruction_name, scope_display, repo_display, version_display, installed_date, bundle_display
120
+ )
121
+
122
+ return table
123
+
124
+
125
+ def format_bundle_details(bundle: InstructionBundle, instructions: list[Instruction]) -> Table:
126
+ """
127
+ Format bundle details with its instructions.
128
+
129
+ Args:
130
+ bundle: Bundle to display
131
+ instructions: Instructions in the bundle
132
+
133
+ Returns:
134
+ Rich Table object
135
+ """
136
+ table = Table(title=f"Bundle: {bundle.name}", show_header=True, header_style="bold cyan")
137
+
138
+ table.add_column("#", style="dim", width=4)
139
+ table.add_column("Instruction", style="cyan")
140
+ table.add_column("Description")
141
+ table.add_column("Tags", style="yellow")
142
+
143
+ for idx, inst in enumerate(instructions, 1):
144
+ tags_str = ", ".join(inst.tags) if inst.tags else "-"
145
+ table.add_row(str(idx), inst.name, inst.description, tags_str)
146
+
147
+ return table
148
+
149
+
150
+ def print_success(message: str, console: Optional[Console] = None) -> None:
151
+ """Print success message."""
152
+ if console is None:
153
+ console = Console()
154
+ console.print(f"[green]✓[/green] {message}")
155
+
156
+
157
+ def print_error(message: str, console: Optional[Console] = None) -> None:
158
+ """Print error message."""
159
+ if console is None:
160
+ console = Console()
161
+ console.print(f"[red]Error:[/red] {message}")
162
+
163
+
164
+ def print_warning(message: str, console: Optional[Console] = None) -> None:
165
+ """Print warning message."""
166
+ if console is None:
167
+ console = Console()
168
+ console.print(f"[yellow]Warning:[/yellow] {message}")
169
+
170
+
171
+ def print_info(message: str, console: Optional[Console] = None) -> None:
172
+ """Print info message."""
173
+ if console is None:
174
+ console = Console()
175
+ console.print(f"[cyan]ℹ[/cyan] {message}")
176
+
177
+
178
+ def _shorten_url(url: str, max_length: int = 50) -> str:
179
+ """Shorten URL for display."""
180
+ if len(url) <= max_length:
181
+ return url
182
+
183
+ # Try to keep the important parts: domain and repo name
184
+ if "://" in url:
185
+ protocol, rest = url.split("://", 1)
186
+ if "/" in rest:
187
+ parts = rest.split("/")
188
+ if len(parts) >= 3:
189
+ # Keep domain and last 2 parts
190
+ shortened = f"{parts[0]}/.../{'/'.join(parts[-2:])}"
191
+ return shortened
192
+
193
+ # Fallback: truncate with ellipsis
194
+ return url[: max_length - 3] + "..."
@@ -0,0 +1,187 @@
1
+ """Input validation utilities for CLI arguments and data."""
2
+
3
+ import re
4
+ from typing import Optional
5
+ from urllib.parse import urlparse
6
+
7
+
8
+ def is_valid_git_url(url: str) -> bool:
9
+ """
10
+ Validate if a string is a valid Git repository URL or local path.
11
+
12
+ Supports:
13
+ - HTTPS: https://github.com/user/repo.git
14
+ - SSH: git@github.com:user/repo.git
15
+ - Git protocol: git://github.com/user/repo.git
16
+ - File: file:///path/to/repo
17
+ - Local absolute paths: /path/to/repo
18
+ - Local relative paths: ./path/to/repo or ../path/to/repo or path/to/repo
19
+ """
20
+ if not url or not isinstance(url, str):
21
+ return False
22
+
23
+ # HTTPS URLs
24
+ if url.startswith(("https://", "http://")):
25
+ try:
26
+ parsed = urlparse(url)
27
+ return bool(parsed.netloc and parsed.path)
28
+ except Exception:
29
+ return False
30
+
31
+ # SSH URLs (git@host:path)
32
+ ssh_pattern = r"^[\w\-\.]+@[\w\-\.]+:[\w\-\.\/]+$"
33
+ if "@" in url and ":" in url:
34
+ return bool(re.match(ssh_pattern, url))
35
+
36
+ # Git protocol URLs
37
+ if url.startswith("git://"):
38
+ try:
39
+ parsed = urlparse(url)
40
+ return bool(parsed.netloc and parsed.path)
41
+ except Exception:
42
+ return False
43
+
44
+ # File URLs and local paths (absolute and relative)
45
+ if url.startswith(("file://", "/", "./", "../")):
46
+ return True
47
+
48
+ # Relative paths without ./ prefix (e.g., "my-repo" or "path/to/repo")
49
+ # Check if it looks like a local path (no protocol, no @host:)
50
+ if "://" not in url and "@" not in url:
51
+ return True
52
+
53
+ return False
54
+
55
+
56
+ def is_valid_instruction_name(name: str) -> bool:
57
+ """
58
+ Validate instruction name follows naming conventions.
59
+
60
+ Rules:
61
+ - Only lowercase letters, numbers, hyphens
62
+ - Must start with letter
63
+ - 3-50 characters
64
+ """
65
+ if not name or not isinstance(name, str):
66
+ return False
67
+
68
+ pattern = r"^[a-z][a-z0-9\-]{2,49}$"
69
+ return bool(re.match(pattern, name))
70
+
71
+
72
+ def is_valid_tag(tag: str) -> bool:
73
+ """
74
+ Validate tag format.
75
+
76
+ Rules:
77
+ - Only lowercase letters, numbers, hyphens
78
+ - 2-30 characters
79
+ """
80
+ if not tag or not isinstance(tag, str):
81
+ return False
82
+
83
+ pattern = r"^[a-z0-9\-]{2,30}$"
84
+ return bool(re.match(pattern, tag))
85
+
86
+
87
+ def is_valid_checksum(checksum: str, algorithm: str = "sha256") -> bool:
88
+ """
89
+ Validate checksum format.
90
+
91
+ Args:
92
+ checksum: The checksum string to validate
93
+ algorithm: Hash algorithm (sha256, sha1, md5)
94
+ """
95
+ if not checksum or not isinstance(checksum, str):
96
+ return False
97
+
98
+ expected_lengths = {
99
+ "sha256": 64,
100
+ "sha1": 40,
101
+ "md5": 32,
102
+ }
103
+
104
+ expected_length = expected_lengths.get(algorithm.lower())
105
+ if not expected_length:
106
+ return False
107
+
108
+ # Checksum should be hex string of expected length
109
+ pattern = f"^[a-f0-9]{{{expected_length}}}$"
110
+ return bool(re.match(pattern, checksum.lower()))
111
+
112
+
113
+ def sanitize_instruction_name(name: str) -> str:
114
+ """
115
+ Sanitize an instruction name to make it valid.
116
+
117
+ Converts to lowercase, replaces invalid chars with hyphens,
118
+ removes leading/trailing hyphens.
119
+ """
120
+ # Convert to lowercase
121
+ sanitized = name.lower()
122
+
123
+ # Replace invalid characters with hyphens
124
+ sanitized = re.sub(r"[^a-z0-9\-]", "-", sanitized)
125
+
126
+ # Remove leading/trailing hyphens
127
+ sanitized = sanitized.strip("-")
128
+
129
+ # Collapse multiple consecutive hyphens
130
+ sanitized = re.sub(r"-+", "-", sanitized)
131
+
132
+ # Ensure starts with letter
133
+ if sanitized and not sanitized[0].isalpha():
134
+ sanitized = "inst-" + sanitized
135
+
136
+ # Truncate to max length
137
+ if len(sanitized) > 50:
138
+ sanitized = sanitized[:50].rstrip("-")
139
+
140
+ return sanitized
141
+
142
+
143
+ def validate_file_path(path: str) -> Optional[str]:
144
+ """
145
+ Validate file path is safe (no directory traversal).
146
+
147
+ Returns:
148
+ None if valid, error message if invalid
149
+ """
150
+ if not path or not isinstance(path, str):
151
+ return "Path must be a non-empty string"
152
+
153
+ # Check for directory traversal attempts
154
+ if ".." in path:
155
+ return "Path cannot contain '..' (directory traversal)"
156
+
157
+ # Check for absolute paths
158
+ if path.startswith("/") or (len(path) > 1 and path[1] == ":"):
159
+ return "Path must be relative (not absolute)"
160
+
161
+ # Check for unsafe characters
162
+ unsafe_chars = ["<", ">", "|", "\0"]
163
+ for char in unsafe_chars:
164
+ if char in path:
165
+ return f"Path contains unsafe character: {char}"
166
+
167
+ return None
168
+
169
+
170
+ def normalize_repo_url(url: str) -> str:
171
+ """
172
+ Normalize repository URL for consistent comparison.
173
+
174
+ - Removes trailing .git
175
+ - Removes trailing slashes
176
+ - Converts to lowercase for hostname
177
+ """
178
+ normalized = url.strip()
179
+
180
+ # Remove trailing slashes first
181
+ normalized = normalized.rstrip("/")
182
+
183
+ # Remove trailing .git
184
+ if normalized.endswith(".git"):
185
+ normalized = normalized[:-4]
186
+
187
+ return normalized
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 InstructionKit Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.