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
aiconfigkit/utils/ui.py
ADDED
|
@@ -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.
|