@voodocs/cli 0.4.2 → 1.0.1
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.
- package/CHANGELOG.md +431 -0
- package/lib/cli/__init__.py +53 -0
- package/lib/cli/benchmark.py +311 -0
- package/lib/cli/fix.py +244 -0
- package/lib/cli/generate.py +310 -0
- package/lib/cli/test_cli.py +215 -0
- package/lib/cli/validate.py +364 -0
- package/lib/darkarts/__init__.py +11 -5
- package/lib/darkarts/annotations/__init__.py +11 -3
- package/lib/darkarts/annotations/darkarts_parser.py +1 -1
- package/lib/darkarts/annotations/translator.py +32 -5
- package/lib/darkarts/annotations/types.py +15 -2
- package/lib/darkarts/cli_darkarts.py +143 -15
- package/lib/darkarts/context/__init__.py +11 -3
- package/lib/darkarts/context/ai_integrations.py +7 -21
- package/lib/darkarts/context/commands.py +1 -1
- package/lib/darkarts/context/diagram.py +8 -22
- package/lib/darkarts/context/models.py +7 -22
- package/lib/darkarts/context/module_utils.py +1 -1
- package/lib/darkarts/context/ui.py +1 -1
- package/lib/darkarts/context/validation.py +1 -1
- package/lib/darkarts/context/yaml_utils.py +8 -23
- package/lib/darkarts/core/__init__.py +12 -2
- package/lib/darkarts/core/interface.py +15 -1
- package/lib/darkarts/core/loader.py +16 -1
- package/lib/darkarts/core/plugin.py +15 -2
- package/lib/darkarts/core/registry.py +16 -1
- package/lib/darkarts/exceptions.py +16 -2
- package/lib/darkarts/plugins/voodocs/__init__.py +12 -2
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +15 -4
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +15 -2
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +15 -2
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +15 -2
- package/lib/darkarts/plugins/voodocs/html_exporter.py +15 -2
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +1 -1
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +15 -2
- package/lib/darkarts/plugins/voodocs/test_generator.py +15 -2
- package/lib/darkarts/telemetry.py +15 -2
- package/lib/darkarts/validation/README.md +147 -0
- package/lib/darkarts/validation/__init__.py +91 -0
- package/lib/darkarts/validation/autofix.py +297 -0
- package/lib/darkarts/validation/benchmark.py +426 -0
- package/lib/darkarts/validation/benchmark_wrapper.py +22 -0
- package/lib/darkarts/validation/config.py +257 -0
- package/lib/darkarts/validation/performance.py +412 -0
- package/lib/darkarts/validation/performance_wrapper.py +37 -0
- package/lib/darkarts/validation/semantic.py +461 -0
- package/lib/darkarts/validation/semantic_wrapper.py +77 -0
- package/lib/darkarts/validation/test_validation.py +160 -0
- package/lib/darkarts/validation/types.py +97 -0
- package/lib/darkarts/validation/watch.py +239 -0
- package/package.json +19 -6
- package/voodocs_cli.py +28 -0
- package/cli.py +0 -1646
- package/lib/darkarts/cli.py +0 -128
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""@darkarts
|
|
2
|
+
⊢validation:types
|
|
3
|
+
∂{dataclasses,typing}
|
|
4
|
+
⚠{python≥3.7}
|
|
5
|
+
⊨{∀type→immutable,∀result→serializable}
|
|
6
|
+
🔒{read-only:dataclasses}
|
|
7
|
+
⚡{O(1):creation}
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
Shared type definitions for validation module.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Set, List, Dict, Optional, Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ValidationResult:
|
|
20
|
+
"""Result of semantic validation."""
|
|
21
|
+
file_path: str
|
|
22
|
+
is_valid: bool
|
|
23
|
+
missing_deps: Set[str] = field(default_factory=set)
|
|
24
|
+
extra_deps: Set[str] = field(default_factory=set)
|
|
25
|
+
errors: List[str] = field(default_factory=list)
|
|
26
|
+
warnings: List[str] = field(default_factory=list)
|
|
27
|
+
suggestions: List[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
30
|
+
"""Convert to dictionary for serialization."""
|
|
31
|
+
return {
|
|
32
|
+
"file_path": self.file_path,
|
|
33
|
+
"is_valid": self.is_valid,
|
|
34
|
+
"missing_deps": list(self.missing_deps),
|
|
35
|
+
"extra_deps": list(self.extra_deps),
|
|
36
|
+
"errors": self.errors,
|
|
37
|
+
"warnings": self.warnings,
|
|
38
|
+
"suggestions": self.suggestions,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class PerformanceResult:
|
|
44
|
+
"""Result of performance validation."""
|
|
45
|
+
file_path: str
|
|
46
|
+
is_valid: bool
|
|
47
|
+
claimed_complexity: str
|
|
48
|
+
actual_complexity: str
|
|
49
|
+
loop_count: int
|
|
50
|
+
max_depth: int
|
|
51
|
+
has_recursion: bool
|
|
52
|
+
errors: List[str] = field(default_factory=list)
|
|
53
|
+
warnings: List[str] = field(default_factory=list)
|
|
54
|
+
suggestions: List[str] = field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
"""Convert to dictionary for serialization."""
|
|
58
|
+
return {
|
|
59
|
+
"file_path": self.file_path,
|
|
60
|
+
"is_valid": self.is_valid,
|
|
61
|
+
"claimed_complexity": self.claimed_complexity,
|
|
62
|
+
"actual_complexity": self.actual_complexity,
|
|
63
|
+
"loop_count": self.loop_count,
|
|
64
|
+
"max_depth": self.max_depth,
|
|
65
|
+
"has_recursion": self.has_recursion,
|
|
66
|
+
"errors": self.errors,
|
|
67
|
+
"warnings": self.warnings,
|
|
68
|
+
"suggestions": self.suggestions,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class BenchmarkResult:
|
|
74
|
+
"""Result of performance benchmarking."""
|
|
75
|
+
function_name: str
|
|
76
|
+
input_sizes: List[int]
|
|
77
|
+
execution_times: List[float]
|
|
78
|
+
claimed_complexity: str
|
|
79
|
+
fitted_complexity: str
|
|
80
|
+
is_valid: bool
|
|
81
|
+
confidence: float
|
|
82
|
+
errors: List[str] = field(default_factory=list)
|
|
83
|
+
warnings: List[str] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
86
|
+
"""Convert to dictionary for serialization."""
|
|
87
|
+
return {
|
|
88
|
+
"function_name": self.function_name,
|
|
89
|
+
"input_sizes": self.input_sizes,
|
|
90
|
+
"execution_times": self.execution_times,
|
|
91
|
+
"claimed_complexity": self.claimed_complexity,
|
|
92
|
+
"fitted_complexity": self.fitted_complexity,
|
|
93
|
+
"is_valid": self.is_valid,
|
|
94
|
+
"confidence": self.confidence,
|
|
95
|
+
"errors": self.errors,
|
|
96
|
+
"warnings": self.warnings,
|
|
97
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""@darkarts
|
|
2
|
+
⊢watch:validation.continuous-monitoring
|
|
3
|
+
∂{time,pathlib,typing,hashlib,semantic_validator}
|
|
4
|
+
⚠{python≥3.7,filesystem:accessible}
|
|
5
|
+
⊨{∀change→validated,∀error→reported,continuous:non-blocking,responsive:sub-second}
|
|
6
|
+
🔒{read-only:files,¬write,¬network,¬exec}
|
|
7
|
+
⚡{O(n²):per-check|n=watched-files,hash-based:change-detection}
|
|
8
|
+
|
|
9
|
+
Watch Mode for Continuous Validation
|
|
10
|
+
|
|
11
|
+
Monitors files for changes and validates annotations in real-time with:
|
|
12
|
+
- File system monitoring (efficient hash-based change detection)
|
|
13
|
+
- Instant validation (validates on save)
|
|
14
|
+
- Live feedback (immediate error reporting)
|
|
15
|
+
- Selective watching (specific files or directories)
|
|
16
|
+
- Debouncing (prevents validation spam)
|
|
17
|
+
- Background operation (non-blocking)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
import hashlib
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, Set, Optional, Callable
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
from semantic_validator import validate_file, ValidationResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FileState:
|
|
32
|
+
"""State of a monitored file."""
|
|
33
|
+
path: Path
|
|
34
|
+
hash: str
|
|
35
|
+
last_validated: datetime
|
|
36
|
+
last_result: Optional[ValidationResult] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FileWatcher:
|
|
40
|
+
"""Watches Python files for changes and validates annotations."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
paths: Set[Path],
|
|
45
|
+
interval: float = 1.0,
|
|
46
|
+
on_change: Optional[Callable[[Path, ValidationResult], None]] = None,
|
|
47
|
+
debounce: float = 0.5
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize the file watcher.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
paths: Set of file or directory paths to watch
|
|
54
|
+
interval: Check interval in seconds (default: 1.0)
|
|
55
|
+
on_change: Callback function called when a file changes
|
|
56
|
+
debounce: Minimum time between validations of the same file
|
|
57
|
+
"""
|
|
58
|
+
self.paths = paths
|
|
59
|
+
self.interval = interval
|
|
60
|
+
self.on_change = on_change
|
|
61
|
+
self.debounce = debounce
|
|
62
|
+
|
|
63
|
+
self.file_states: Dict[Path, FileState] = {}
|
|
64
|
+
self.running = False
|
|
65
|
+
|
|
66
|
+
# Initialize file states
|
|
67
|
+
self._discover_files()
|
|
68
|
+
|
|
69
|
+
def _discover_files(self):
|
|
70
|
+
"""Discover all Python files in the watched paths."""
|
|
71
|
+
for path in self.paths:
|
|
72
|
+
if path.is_file() and path.suffix == '.py':
|
|
73
|
+
self._add_file(path)
|
|
74
|
+
elif path.is_dir():
|
|
75
|
+
for py_file in path.rglob("*.py"):
|
|
76
|
+
if not py_file.name.startswith('.'):
|
|
77
|
+
self._add_file(py_file)
|
|
78
|
+
|
|
79
|
+
def _add_file(self, file_path: Path):
|
|
80
|
+
"""Add a file to the watch list."""
|
|
81
|
+
if file_path not in self.file_states:
|
|
82
|
+
file_hash = self._compute_hash(file_path)
|
|
83
|
+
self.file_states[file_path] = FileState(
|
|
84
|
+
path=file_path,
|
|
85
|
+
hash=file_hash,
|
|
86
|
+
last_validated=datetime.now()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _compute_hash(self, file_path: Path) -> str:
|
|
90
|
+
"""Compute SHA256 hash of a file."""
|
|
91
|
+
try:
|
|
92
|
+
content = file_path.read_bytes()
|
|
93
|
+
return hashlib.sha256(content).hexdigest()
|
|
94
|
+
except Exception:
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
def _check_file(self, file_path: Path) -> Optional[ValidationResult]:
|
|
98
|
+
"""
|
|
99
|
+
Check if a file has changed and validate if needed.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
file_path: Path to the file
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ValidationResult if file changed and was validated, None otherwise
|
|
106
|
+
"""
|
|
107
|
+
if not file_path.exists():
|
|
108
|
+
# File was deleted
|
|
109
|
+
if file_path in self.file_states:
|
|
110
|
+
del self.file_states[file_path]
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
current_hash = self._compute_hash(file_path)
|
|
114
|
+
state = self.file_states.get(file_path)
|
|
115
|
+
|
|
116
|
+
if state is None:
|
|
117
|
+
# New file discovered
|
|
118
|
+
self._add_file(file_path)
|
|
119
|
+
state = self.file_states[file_path]
|
|
120
|
+
|
|
121
|
+
# Check if file changed
|
|
122
|
+
if current_hash != state.hash:
|
|
123
|
+
# Check debounce
|
|
124
|
+
time_since_last = (datetime.now() - state.last_validated).total_seconds()
|
|
125
|
+
if time_since_last < self.debounce:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# File changed - validate it
|
|
129
|
+
try:
|
|
130
|
+
result = validate_file(file_path)
|
|
131
|
+
|
|
132
|
+
# Update state
|
|
133
|
+
state.hash = current_hash
|
|
134
|
+
state.last_validated = datetime.now()
|
|
135
|
+
state.last_result = result
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"Error validating {file_path}: {e}")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def watch(self):
|
|
145
|
+
"""Start watching files for changes."""
|
|
146
|
+
self.running = True
|
|
147
|
+
|
|
148
|
+
print(f"👀 Watching {len(self.file_states)} files...")
|
|
149
|
+
print(f" Interval: {self.interval}s")
|
|
150
|
+
print(f" Debounce: {self.debounce}s")
|
|
151
|
+
print(" Press Ctrl+C to stop\n")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
while self.running:
|
|
155
|
+
# Check all files
|
|
156
|
+
for file_path in list(self.file_states.keys()):
|
|
157
|
+
result = self._check_file(file_path)
|
|
158
|
+
|
|
159
|
+
if result is not None:
|
|
160
|
+
# File changed and was validated
|
|
161
|
+
self._report_result(file_path, result)
|
|
162
|
+
|
|
163
|
+
# Call callback if provided
|
|
164
|
+
if self.on_change:
|
|
165
|
+
self.on_change(file_path, result)
|
|
166
|
+
|
|
167
|
+
# Sleep before next check
|
|
168
|
+
time.sleep(self.interval)
|
|
169
|
+
|
|
170
|
+
except KeyboardInterrupt:
|
|
171
|
+
print("\n\n⏹️ Watch stopped")
|
|
172
|
+
self.running = False
|
|
173
|
+
|
|
174
|
+
def _report_result(self, file_path: Path, result: ValidationResult):
|
|
175
|
+
"""Report validation result to console."""
|
|
176
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
177
|
+
|
|
178
|
+
if result.is_valid:
|
|
179
|
+
print(f"[{timestamp}] ✅ {file_path.name}: Valid")
|
|
180
|
+
else:
|
|
181
|
+
print(f"[{timestamp}] ❌ {file_path.name}: Invalid")
|
|
182
|
+
|
|
183
|
+
if result.missing_deps:
|
|
184
|
+
print(f" Missing: {', '.join(sorted(result.missing_deps))}")
|
|
185
|
+
|
|
186
|
+
if result.extra_deps:
|
|
187
|
+
print(f" Extra: {', '.join(sorted(result.extra_deps))}")
|
|
188
|
+
|
|
189
|
+
if result.suggestions:
|
|
190
|
+
for suggestion in result.suggestions[:2]: # Show first 2 suggestions
|
|
191
|
+
print(f" 💡 {suggestion}")
|
|
192
|
+
|
|
193
|
+
print()
|
|
194
|
+
|
|
195
|
+
def stop(self):
|
|
196
|
+
"""Stop watching files."""
|
|
197
|
+
self.running = False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def watch_with_autofix(
|
|
201
|
+
paths: Set[Path],
|
|
202
|
+
interval: float = 1.0,
|
|
203
|
+
auto_fix: bool = False
|
|
204
|
+
):
|
|
205
|
+
"""
|
|
206
|
+
Watch files and optionally auto-fix on change.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
paths: Set of paths to watch
|
|
210
|
+
interval: Check interval in seconds
|
|
211
|
+
auto_fix: Whether to automatically fix invalid annotations
|
|
212
|
+
"""
|
|
213
|
+
if auto_fix:
|
|
214
|
+
from autofix import AutoFixer
|
|
215
|
+
fixer = AutoFixer(dry_run=False)
|
|
216
|
+
|
|
217
|
+
def on_change_callback(file_path: Path, result: ValidationResult):
|
|
218
|
+
if not result.is_valid:
|
|
219
|
+
print(f" 🔧 Auto-fixing {file_path.name}...")
|
|
220
|
+
fix_result = fixer.fix_file(file_path)
|
|
221
|
+
if fix_result.success:
|
|
222
|
+
print(f" ✅ Fixed!")
|
|
223
|
+
else:
|
|
224
|
+
print(f" ❌ Fix failed: {fix_result.error}")
|
|
225
|
+
print()
|
|
226
|
+
|
|
227
|
+
watcher = FileWatcher(
|
|
228
|
+
paths=paths,
|
|
229
|
+
interval=interval,
|
|
230
|
+
on_change=on_change_callback
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
watcher = FileWatcher(paths=paths, interval=interval)
|
|
234
|
+
|
|
235
|
+
watcher.watch()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# CLI interface removed - use darkarts.validation API or voodocs CLI instead
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voodocs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI-Native Documentation System -
|
|
5
|
-
"main": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "AI-Native Documentation System with Validation - The only documentation tool that validates your annotations and guarantees accuracy",
|
|
5
|
+
"main": "voodocs_cli.py",
|
|
6
6
|
"bin": {
|
|
7
|
-
"voodocs": "./
|
|
7
|
+
"voodocs": "./voodocs_cli.py"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"postinstall": "cd lib/darkarts/parsers/typescript && npm install && npm run build",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"documentation",
|
|
16
|
+
"validation",
|
|
16
17
|
"ai",
|
|
17
18
|
"voodocs",
|
|
18
19
|
"darkarts",
|
|
@@ -24,7 +25,13 @@
|
|
|
24
25
|
"cursor",
|
|
25
26
|
"claude",
|
|
26
27
|
"property-based-testing",
|
|
27
|
-
"hypothesis"
|
|
28
|
+
"hypothesis",
|
|
29
|
+
"annotation-validation",
|
|
30
|
+
"semantic-validation",
|
|
31
|
+
"performance-validation",
|
|
32
|
+
"benchmarking",
|
|
33
|
+
"auto-fix",
|
|
34
|
+
"ci-cd"
|
|
28
35
|
],
|
|
29
36
|
"author": "3vilEnterprises <contact@3vil.enterprises>",
|
|
30
37
|
"license": "Commercial",
|
|
@@ -44,11 +51,13 @@
|
|
|
44
51
|
"dependencies": {},
|
|
45
52
|
"devDependencies": {},
|
|
46
53
|
"files": [
|
|
47
|
-
"
|
|
54
|
+
"voodocs_cli.py",
|
|
55
|
+
"lib/cli/",
|
|
48
56
|
"lib/darkarts/cli_darkarts.py",
|
|
49
57
|
"lib/darkarts/annotations/",
|
|
50
58
|
"lib/darkarts/context/",
|
|
51
59
|
"lib/darkarts/core/",
|
|
60
|
+
"lib/darkarts/validation/",
|
|
52
61
|
"lib/darkarts/exceptions.py",
|
|
53
62
|
"lib/darkarts/telemetry.py",
|
|
54
63
|
"lib/darkarts/parsers/typescript/dist/",
|
|
@@ -58,6 +67,9 @@
|
|
|
58
67
|
"lib/darkarts/parsers/typescript/tsconfig.json",
|
|
59
68
|
"lib/darkarts/plugins/voodocs/",
|
|
60
69
|
"lib/darkarts/__init__.py",
|
|
70
|
+
"docs/darkarts/USER_GUIDE.md",
|
|
71
|
+
"docs/darkarts/API_REFERENCE.md",
|
|
72
|
+
"docs/darkarts/TUTORIALS.md",
|
|
61
73
|
"examples/",
|
|
62
74
|
"templates/",
|
|
63
75
|
"README.md",
|
|
@@ -65,6 +77,7 @@
|
|
|
65
77
|
"USAGE.md",
|
|
66
78
|
"PRIVACY.md",
|
|
67
79
|
"CHANGELOG.md",
|
|
80
|
+
"RELEASE_NOTES_v1.0.0.md",
|
|
68
81
|
"requirements.txt"
|
|
69
82
|
],
|
|
70
83
|
"os": [
|
package/voodocs_cli.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""@darkarts
|
|
3
|
+
⊢voodocs:cli.entry
|
|
4
|
+
∂{sys,pathlib}
|
|
5
|
+
⚠{python≥3.7}
|
|
6
|
+
⊨{∀command→executable}
|
|
7
|
+
🔒{read-only:cli-entry}
|
|
8
|
+
⚡{O(1):startup}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
VooDocs CLI Entry Point
|
|
13
|
+
|
|
14
|
+
This is the main entry point for the voodocs command-line tool.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Add lib to path
|
|
21
|
+
lib_path = Path(__file__).parent / "lib"
|
|
22
|
+
sys.path.insert(0, str(lib_path))
|
|
23
|
+
|
|
24
|
+
# Import and run CLI
|
|
25
|
+
from cli import main
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|