reveal-cli 0.8.0__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.
- plugins/c-header.yaml +89 -0
- plugins/gdscript.yaml +96 -0
- plugins/python.yaml +94 -0
- plugins/yaml.yaml +87 -0
- reveal/__init__.py +26 -0
- reveal/analyzers/__init__.py +39 -0
- reveal/analyzers/bash.py +44 -0
- reveal/analyzers/dockerfile.py +179 -0
- reveal/analyzers/gdscript.py +176 -0
- reveal/analyzers/go.py +13 -0
- reveal/analyzers/javascript.py +21 -0
- reveal/analyzers/jupyter_analyzer.py +230 -0
- reveal/analyzers/markdown.py +79 -0
- reveal/analyzers/nginx.py +185 -0
- reveal/analyzers/python.py +15 -0
- reveal/analyzers/rust.py +13 -0
- reveal/analyzers/toml.py +96 -0
- reveal/analyzers/typescript.py +24 -0
- reveal/analyzers/yaml_json.py +110 -0
- reveal/base.py +267 -0
- reveal/main.py +355 -0
- reveal/tests/__init__.py +1 -0
- reveal/tests/test_json_yaml_line_numbers.py +238 -0
- reveal/tests/test_line_numbers.py +151 -0
- reveal/tests/test_toml_analyzer.py +220 -0
- reveal/tree_view.py +105 -0
- reveal/treesitter.py +281 -0
- reveal_cli-0.8.0.dist-info/METADATA +352 -0
- reveal_cli-0.8.0.dist-info/RECORD +33 -0
- reveal_cli-0.8.0.dist-info/WHEEL +5 -0
- reveal_cli-0.8.0.dist-info/entry_points.txt +2 -0
- reveal_cli-0.8.0.dist-info/licenses/LICENSE +21 -0
- reveal_cli-0.8.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Nginx configuration file analyzer."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, List, Any, Optional
|
|
5
|
+
from ..base import FileAnalyzer, register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register('.conf', name='Nginx', icon='🌐')
|
|
9
|
+
class NginxAnalyzer(FileAnalyzer):
|
|
10
|
+
"""Nginx configuration file analyzer.
|
|
11
|
+
|
|
12
|
+
Extracts server blocks, locations, upstreams, and key directives.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
16
|
+
"""Extract nginx config structure."""
|
|
17
|
+
servers = []
|
|
18
|
+
locations = []
|
|
19
|
+
upstreams = []
|
|
20
|
+
comments = []
|
|
21
|
+
|
|
22
|
+
# Track current context for locations
|
|
23
|
+
current_server = None
|
|
24
|
+
in_server = False
|
|
25
|
+
brace_depth = 0
|
|
26
|
+
|
|
27
|
+
for i, line in enumerate(self.lines, 1):
|
|
28
|
+
stripped = line.strip()
|
|
29
|
+
|
|
30
|
+
# Track brace depth
|
|
31
|
+
brace_depth += stripped.count('{')
|
|
32
|
+
brace_depth -= stripped.count('}')
|
|
33
|
+
|
|
34
|
+
# Top-level comment headers (typically file documentation)
|
|
35
|
+
if stripped.startswith('#') and i <= 10 and len(stripped) > 3:
|
|
36
|
+
comments.append({
|
|
37
|
+
'line': i,
|
|
38
|
+
'text': stripped[1:].strip()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
# Server block
|
|
42
|
+
if 'server {' in stripped or stripped.startswith('server {'):
|
|
43
|
+
in_server = True
|
|
44
|
+
server_info = {
|
|
45
|
+
'line': i,
|
|
46
|
+
'name': 'unknown',
|
|
47
|
+
'port': 'unknown'
|
|
48
|
+
}
|
|
49
|
+
# Look ahead for server_name and listen
|
|
50
|
+
for j in range(i, min(i + 20, len(self.lines) + 1)):
|
|
51
|
+
next_line = self.lines[j-1].strip()
|
|
52
|
+
if next_line.startswith('server_name '):
|
|
53
|
+
match = re.match(r'server_name\s+(.*?);', next_line)
|
|
54
|
+
if match:
|
|
55
|
+
server_info['name'] = match.group(1)
|
|
56
|
+
elif next_line.startswith('listen '):
|
|
57
|
+
match = re.match(r'listen\s+(\S+)', next_line)
|
|
58
|
+
if match:
|
|
59
|
+
port = match.group(1).rstrip(';')
|
|
60
|
+
# Handle various listen formats
|
|
61
|
+
if port.startswith('443'):
|
|
62
|
+
server_info['port'] = '443 (SSL)'
|
|
63
|
+
elif port.startswith('80'):
|
|
64
|
+
server_info['port'] = '80'
|
|
65
|
+
else:
|
|
66
|
+
server_info['port'] = port
|
|
67
|
+
# Stop at closing brace of this server block
|
|
68
|
+
if next_line == '}' and j > i:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
servers.append(server_info)
|
|
72
|
+
current_server = server_info
|
|
73
|
+
|
|
74
|
+
# Location block (inside server)
|
|
75
|
+
elif in_server and brace_depth > 0 and ('location ' in stripped):
|
|
76
|
+
match = re.match(r'location\s+(.+?)\s*\{', stripped)
|
|
77
|
+
if match:
|
|
78
|
+
path = match.group(1)
|
|
79
|
+
loc_info = {
|
|
80
|
+
'line': i,
|
|
81
|
+
'path': path,
|
|
82
|
+
'server': current_server['name'] if current_server else 'unknown'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Look ahead for proxy_pass or root
|
|
86
|
+
for j in range(i, min(i + 15, len(self.lines) + 1)):
|
|
87
|
+
next_line = self.lines[j-1].strip()
|
|
88
|
+
if next_line.startswith('proxy_pass '):
|
|
89
|
+
match_proxy = re.match(r'proxy_pass\s+(.*?);', next_line)
|
|
90
|
+
if match_proxy:
|
|
91
|
+
loc_info['target'] = match_proxy.group(1)
|
|
92
|
+
break
|
|
93
|
+
elif next_line.startswith('root '):
|
|
94
|
+
match_root = re.match(r'root\s+(.*?);', next_line)
|
|
95
|
+
if match_root:
|
|
96
|
+
loc_info['target'] = f"static: {match_root.group(1)}"
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
locations.append(loc_info)
|
|
100
|
+
|
|
101
|
+
# Upstream block
|
|
102
|
+
elif 'upstream ' in stripped and '{' in stripped:
|
|
103
|
+
match = re.match(r'upstream\s+(\S+)\s*\{', stripped)
|
|
104
|
+
if match:
|
|
105
|
+
upstreams.append({
|
|
106
|
+
'line': i,
|
|
107
|
+
'name': match.group(1)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# Reset server context when we exit server block
|
|
111
|
+
if in_server and brace_depth == 0:
|
|
112
|
+
in_server = False
|
|
113
|
+
current_server = None
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
'comments': comments,
|
|
117
|
+
'servers': servers,
|
|
118
|
+
'locations': locations,
|
|
119
|
+
'upstreams': upstreams
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
|
|
123
|
+
"""Extract a server or location block.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
element_type: 'server', 'location', or 'upstream'
|
|
127
|
+
name: Name to find (server_name, location path, or upstream name)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dict with block content
|
|
131
|
+
"""
|
|
132
|
+
start_line = None
|
|
133
|
+
search_pattern = None
|
|
134
|
+
|
|
135
|
+
# Build search pattern based on element type
|
|
136
|
+
if element_type == 'server':
|
|
137
|
+
# Search for server block with this server_name
|
|
138
|
+
for i, line in enumerate(self.lines, 1):
|
|
139
|
+
if 'server {' in line or line.strip().startswith('server {'):
|
|
140
|
+
# Look ahead for server_name
|
|
141
|
+
for j in range(i, min(i + 20, len(self.lines) + 1)):
|
|
142
|
+
if f'server_name {name}' in self.lines[j-1]:
|
|
143
|
+
start_line = i
|
|
144
|
+
break
|
|
145
|
+
if start_line:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
elif element_type == 'location':
|
|
149
|
+
# Search for location block with this path
|
|
150
|
+
search_pattern = rf'location\s+{re.escape(name)}\s*\{{'
|
|
151
|
+
for i, line in enumerate(self.lines, 1):
|
|
152
|
+
if re.search(search_pattern, line):
|
|
153
|
+
start_line = i
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
elif element_type == 'upstream':
|
|
157
|
+
# Search for upstream block with this name
|
|
158
|
+
search_pattern = rf'upstream\s+{re.escape(name)}\s*\{{'
|
|
159
|
+
for i, line in enumerate(self.lines, 1):
|
|
160
|
+
if re.search(search_pattern, line):
|
|
161
|
+
start_line = i
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if not start_line:
|
|
165
|
+
return super().extract_element(element_type, name)
|
|
166
|
+
|
|
167
|
+
# Find matching closing brace
|
|
168
|
+
brace_depth = 0
|
|
169
|
+
end_line = start_line
|
|
170
|
+
for i in range(start_line - 1, len(self.lines)):
|
|
171
|
+
line = self.lines[i]
|
|
172
|
+
brace_depth += line.count('{')
|
|
173
|
+
brace_depth -= line.count('}')
|
|
174
|
+
if brace_depth == 0 and i >= start_line:
|
|
175
|
+
end_line = i + 1
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
source = '\n'.join(self.lines[start_line-1:end_line])
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
'name': name,
|
|
182
|
+
'line_start': start_line,
|
|
183
|
+
'line_end': end_line,
|
|
184
|
+
'source': source,
|
|
185
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Python file analyzer - tree-sitter based."""
|
|
2
|
+
|
|
3
|
+
from ..base import register
|
|
4
|
+
from ..treesitter import TreeSitterAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register('.py', name='Python', icon='🐍')
|
|
8
|
+
class PythonAnalyzer(TreeSitterAnalyzer):
|
|
9
|
+
"""Python file analyzer.
|
|
10
|
+
|
|
11
|
+
Gets structure + extraction for FREE from TreeSitterAnalyzer!
|
|
12
|
+
|
|
13
|
+
Just 3 lines of code for full Python support!
|
|
14
|
+
"""
|
|
15
|
+
language = 'python'
|
reveal/analyzers/rust.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Rust file analyzer - tree-sitter based."""
|
|
2
|
+
|
|
3
|
+
from ..base import register
|
|
4
|
+
from ..treesitter import TreeSitterAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register('.rs', name='Rust', icon='🦀')
|
|
8
|
+
class RustAnalyzer(TreeSitterAnalyzer):
|
|
9
|
+
"""Rust file analyzer.
|
|
10
|
+
|
|
11
|
+
Full Rust support in 3 lines!
|
|
12
|
+
"""
|
|
13
|
+
language = 'rust'
|
reveal/analyzers/toml.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""TOML file analyzer."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, List, Any, Optional
|
|
5
|
+
from ..base import FileAnalyzer, register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register('.toml', name='TOML', icon='📋')
|
|
9
|
+
class TomlAnalyzer(FileAnalyzer):
|
|
10
|
+
"""TOML file analyzer.
|
|
11
|
+
|
|
12
|
+
Extracts sections ([section]) and key-value pairs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
16
|
+
"""Extract TOML sections and top-level keys."""
|
|
17
|
+
sections = []
|
|
18
|
+
keys = []
|
|
19
|
+
|
|
20
|
+
current_section = None
|
|
21
|
+
|
|
22
|
+
for i, line in enumerate(self.lines, 1):
|
|
23
|
+
stripped = line.strip()
|
|
24
|
+
|
|
25
|
+
# Skip empty lines and comments
|
|
26
|
+
if not stripped or stripped.startswith('#'):
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
# Section header: [section] or [[array]]
|
|
30
|
+
section_match = re.match(r'^\[+([^\]]+)\]+', stripped)
|
|
31
|
+
if section_match:
|
|
32
|
+
section_name = section_match.group(1).strip()
|
|
33
|
+
sections.append({
|
|
34
|
+
'line': i,
|
|
35
|
+
'name': section_name,
|
|
36
|
+
})
|
|
37
|
+
current_section = section_name
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
# Key-value pair
|
|
41
|
+
key_match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_-]*)\s*=', stripped)
|
|
42
|
+
if key_match:
|
|
43
|
+
key_name = key_match.group(1)
|
|
44
|
+
|
|
45
|
+
# Only include top-level keys (no section)
|
|
46
|
+
if current_section is None:
|
|
47
|
+
keys.append({
|
|
48
|
+
'line': i,
|
|
49
|
+
'name': key_name,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
result = {}
|
|
53
|
+
if sections:
|
|
54
|
+
result['sections'] = sections
|
|
55
|
+
if keys:
|
|
56
|
+
result['keys'] = keys
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
|
|
61
|
+
"""Extract a TOML section or key.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
element_type: 'section' or 'key'
|
|
65
|
+
name: Section/key name to find
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict with section/key content
|
|
69
|
+
"""
|
|
70
|
+
# Try to find as section first
|
|
71
|
+
for i, line in enumerate(self.lines, 1):
|
|
72
|
+
stripped = line.strip()
|
|
73
|
+
|
|
74
|
+
# Match section header
|
|
75
|
+
section_match = re.match(r'^\[+([^\]]+)\]+', stripped)
|
|
76
|
+
if section_match and section_match.group(1).strip() == name:
|
|
77
|
+
start_line = i
|
|
78
|
+
|
|
79
|
+
# Find end of section (next section or EOF)
|
|
80
|
+
end_line = len(self.lines) + 1
|
|
81
|
+
for j in range(i, len(self.lines)):
|
|
82
|
+
if re.match(r'^\[+[^\]]+\]+', self.lines[j].strip()):
|
|
83
|
+
end_line = j + 1
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
source = '\n'.join(self.lines[start_line-1:end_line-1])
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
'name': name,
|
|
90
|
+
'line_start': start_line,
|
|
91
|
+
'line_end': end_line - 1,
|
|
92
|
+
'source': source,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Fall back to grep-based search
|
|
96
|
+
return super().extract_element(element_type, name)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""TypeScript file analyzer - tree-sitter based."""
|
|
2
|
+
|
|
3
|
+
from ..base import register
|
|
4
|
+
from ..treesitter import TreeSitterAnalyzer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register('.ts', name='TypeScript', icon='🔷')
|
|
8
|
+
@register('.tsx', name='TypeScript React', icon='⚛️')
|
|
9
|
+
class TypeScriptAnalyzer(TreeSitterAnalyzer):
|
|
10
|
+
"""TypeScript file analyzer.
|
|
11
|
+
|
|
12
|
+
Full TypeScript support via tree-sitter!
|
|
13
|
+
Extracts:
|
|
14
|
+
- Import/export statements (ES6 modules)
|
|
15
|
+
- Function declarations
|
|
16
|
+
- Class definitions
|
|
17
|
+
- Interfaces
|
|
18
|
+
- Type definitions
|
|
19
|
+
- Arrow functions
|
|
20
|
+
|
|
21
|
+
Supports both .ts and .tsx (React) files.
|
|
22
|
+
Works on all platforms (Windows, Linux, macOS).
|
|
23
|
+
"""
|
|
24
|
+
language = 'typescript'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""YAML and JSON file analyzers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Dict, List, Any, Optional
|
|
6
|
+
from ..base import FileAnalyzer, register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register('.yaml', '.yml', name='YAML', icon='📋')
|
|
10
|
+
class YamlAnalyzer(FileAnalyzer):
|
|
11
|
+
"""YAML file analyzer.
|
|
12
|
+
|
|
13
|
+
Extracts top-level keys.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
17
|
+
"""Extract YAML top-level keys."""
|
|
18
|
+
keys = []
|
|
19
|
+
|
|
20
|
+
for i, line in enumerate(self.lines, 1):
|
|
21
|
+
# Match top-level key (no indentation)
|
|
22
|
+
match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:', line)
|
|
23
|
+
if match:
|
|
24
|
+
key_name = match.group(1)
|
|
25
|
+
keys.append({
|
|
26
|
+
'line': i,
|
|
27
|
+
'name': key_name,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return {'keys': keys}
|
|
31
|
+
|
|
32
|
+
def extract_element(self, element_type: str, name: str) -> Optional[Dict[str, Any]]:
|
|
33
|
+
"""Extract a YAML key and its value.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
element_type: 'key'
|
|
37
|
+
name: Key name to find
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dict with key content
|
|
41
|
+
"""
|
|
42
|
+
# Find the key
|
|
43
|
+
start_line = None
|
|
44
|
+
|
|
45
|
+
for i, line in enumerate(self.lines, 1):
|
|
46
|
+
match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:', line)
|
|
47
|
+
if match and match.group(1) == name:
|
|
48
|
+
start_line = i
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if not start_line:
|
|
52
|
+
return super().extract_element(element_type, name)
|
|
53
|
+
|
|
54
|
+
# Find end of this key (next top-level key or end of file)
|
|
55
|
+
end_line = len(self.lines)
|
|
56
|
+
for i in range(start_line, len(self.lines)):
|
|
57
|
+
line = self.lines[i]
|
|
58
|
+
# Next top-level key?
|
|
59
|
+
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_-]*\s*:', line):
|
|
60
|
+
end_line = i
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
source = '\n'.join(self.lines[start_line-1:end_line])
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
'name': name,
|
|
67
|
+
'line_start': start_line,
|
|
68
|
+
'line_end': end_line,
|
|
69
|
+
'source': source,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@register('.json', name='JSON', icon='📊')
|
|
74
|
+
class JsonAnalyzer(FileAnalyzer):
|
|
75
|
+
"""JSON file analyzer.
|
|
76
|
+
|
|
77
|
+
Extracts top-level keys.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def get_structure(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
81
|
+
"""Extract JSON top-level keys."""
|
|
82
|
+
try:
|
|
83
|
+
data = json.loads(self.content)
|
|
84
|
+
|
|
85
|
+
if not isinstance(data, dict):
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
keys = []
|
|
89
|
+
for key in data.keys():
|
|
90
|
+
# Find line number by searching
|
|
91
|
+
line_num = self._find_key_line(key)
|
|
92
|
+
keys.append({
|
|
93
|
+
'line': line_num,
|
|
94
|
+
'name': key,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return {'keys': keys}
|
|
98
|
+
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
def _find_key_line(self, key: str) -> int:
|
|
103
|
+
"""Find line number where key is defined."""
|
|
104
|
+
pattern = rf'"{re.escape(key)}"\s*:'
|
|
105
|
+
|
|
106
|
+
for i, line in enumerate(self.lines, 1):
|
|
107
|
+
if re.search(pattern, line):
|
|
108
|
+
return i
|
|
109
|
+
|
|
110
|
+
return 1 # Default to line 1
|