ida-hcli 0.7.7.dev2__tar.gz → 0.7.7.dev4__tar.gz
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.
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/PKG-INFO +1 -1
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/pyproject.toml +1 -1
- ida_hcli-0.7.7.dev4/src/hcli/__init__.py +1 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/key/create.py +1 -1
- ida_hcli-0.7.7.dev4/src/hcli/commands/download.py +233 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/ida/install.py +1 -1
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/auth/__init__.py +40 -1
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/commands/__init__.py +18 -3
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/ida_hcli.egg-info/PKG-INFO +1 -1
- ida_hcli-0.7.7.dev2/src/hcli/__init__.py +0 -1
- ida_hcli-0.7.7.dev2/src/hcli/commands/download.py +0 -171
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/LICENSE +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/README.md +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/setup.cfg +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/default.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/key/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/key/install.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/key/list.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/key/revoke.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/list.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/auth/switch.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/commands.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/common.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/extension/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/extension/create.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/extension/list.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/ida/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/license/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/license/common.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/license/get.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/license/install.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/license/list.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/login.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/logout.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/share/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/share/delete.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/share/get.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/share/list.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/share/put.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/update.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/commands/whoami.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/env.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/asset.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/auth.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/common.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/customer.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/index.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/keys.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/api/license.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/config/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/console.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/constants/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/constants/auth.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/constants/cli.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/extensions/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/ida/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/update/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/update/release.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/update/version.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/util/__init__.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/util/crc32.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/util/io.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/util/output.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/util/python.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/lib/util/string.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/hcli/main.py +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/ida_hcli.egg-info/SOURCES.txt +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/ida_hcli.egg-info/dependency_links.txt +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/ida_hcli.egg-info/entry_points.txt +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/ida_hcli.egg-info/requires.txt +0 -0
- {ida_hcli-0.7.7.dev2 → ida_hcli-0.7.7.dev4}/src/ida_hcli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.7-dev.4"
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import questionary
|
|
7
|
+
import rich_click as click
|
|
8
|
+
from questionary import Choice
|
|
9
|
+
|
|
10
|
+
from hcli.commands.common import safe_ask_async
|
|
11
|
+
from hcli.lib.api.asset import Asset, TreeNode
|
|
12
|
+
from hcli.lib.api.asset import asset as asset_api
|
|
13
|
+
from hcli.lib.api.common import get_api_client
|
|
14
|
+
from hcli.lib.commands import async_command, auth_command
|
|
15
|
+
from hcli.lib.console import console
|
|
16
|
+
from hcli.lib.constants import cli
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BackNavigationResult:
|
|
20
|
+
"""Special result class to indicate backspace navigation."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
BACK_NAVIGATION = BackNavigationResult()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def select_asset(nodes: List[TreeNode], current_path: str = "") -> Optional[Asset]:
|
|
29
|
+
"""Alternative traverse using questionary.select with hierarchical navigation."""
|
|
30
|
+
|
|
31
|
+
async def _traverse_recursive(current_nodes: List[TreeNode], path_stack: List[str]) -> Optional[Asset]:
|
|
32
|
+
# Get folders and files at current level
|
|
33
|
+
folders = [node for node in current_nodes if node.type == "folder" and node.children]
|
|
34
|
+
files = [node for node in current_nodes if node.type == "file"]
|
|
35
|
+
|
|
36
|
+
# Build choices using questionary Choice objects
|
|
37
|
+
choices = []
|
|
38
|
+
|
|
39
|
+
# Add "Go back" option if not at root - create a special TreeNode for this
|
|
40
|
+
if path_stack:
|
|
41
|
+
dummy_asset = Asset(filename="", key="")
|
|
42
|
+
back_node = TreeNode(name="← Go back", type="back", asset=dummy_asset)
|
|
43
|
+
choices.append(Choice("← Go back", value=back_node))
|
|
44
|
+
|
|
45
|
+
# Add folders
|
|
46
|
+
for folder in folders:
|
|
47
|
+
choices.append(Choice(f"📁 {folder.name}", value=folder))
|
|
48
|
+
|
|
49
|
+
# Add files
|
|
50
|
+
for file in files:
|
|
51
|
+
# Get display info from asset metadata if available
|
|
52
|
+
display_name = file.name
|
|
53
|
+
if file.asset and file.asset.metadata and "operating_system" in file.asset.metadata:
|
|
54
|
+
display_name = f"{file.asset.metadata['name']} ({file.asset.key.split('/')[-1]})"
|
|
55
|
+
choices.append(Choice(f"📄 {display_name}", value=file))
|
|
56
|
+
|
|
57
|
+
if not choices:
|
|
58
|
+
console.print("[red]No items found at this path[/red]")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
# Show current path
|
|
62
|
+
path_display = "/" + "/".join(path_stack) if path_stack else "/"
|
|
63
|
+
console.print(f"[blue]Current path: {path_display}[/blue]")
|
|
64
|
+
|
|
65
|
+
# Get user selection
|
|
66
|
+
selected_node = await safe_ask_async(
|
|
67
|
+
questionary.select(
|
|
68
|
+
"Select an item to navigate or download:",
|
|
69
|
+
choices=choices,
|
|
70
|
+
use_jk_keys=False,
|
|
71
|
+
use_search_filter=True,
|
|
72
|
+
style=cli.SELECT_STYLE,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not selected_node:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Handle selection based on node type
|
|
80
|
+
if selected_node.type == "back":
|
|
81
|
+
# Go back one level by removing last item from path stack
|
|
82
|
+
return await _traverse_recursive(_get_nodes_at_path(nodes, path_stack[:-1]), path_stack[:-1])
|
|
83
|
+
elif selected_node.type == "folder":
|
|
84
|
+
# Navigate into folder
|
|
85
|
+
if selected_node.children:
|
|
86
|
+
new_path_stack = path_stack + [selected_node.name]
|
|
87
|
+
return await _traverse_recursive(selected_node.children, new_path_stack)
|
|
88
|
+
return None
|
|
89
|
+
elif selected_node.type == "file":
|
|
90
|
+
# File selected - return the asset key
|
|
91
|
+
return selected_node.asset if selected_node.asset else None
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def _get_nodes_at_path(root_nodes: List[TreeNode], path_stack: List[str]) -> List[TreeNode]:
|
|
96
|
+
"""Helper to get nodes at a specific path in the tree."""
|
|
97
|
+
current_nodes = root_nodes
|
|
98
|
+
for path_part in path_stack:
|
|
99
|
+
# Find the folder with matching name
|
|
100
|
+
folder = next((node for node in current_nodes if node.type == "folder" and node.name == path_part), None)
|
|
101
|
+
if not folder or not folder.children:
|
|
102
|
+
return []
|
|
103
|
+
current_nodes = folder.children
|
|
104
|
+
return current_nodes
|
|
105
|
+
|
|
106
|
+
return await _traverse_recursive(nodes, [])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def collect_all_assets(nodes: List[TreeNode]) -> List[Asset]:
|
|
110
|
+
"""Recursively collect all assets from the tree nodes."""
|
|
111
|
+
assets = []
|
|
112
|
+
|
|
113
|
+
for node in nodes:
|
|
114
|
+
if node.type == "file" and node.asset:
|
|
115
|
+
assets.append(node.asset)
|
|
116
|
+
elif node.type == "folder" and node.children:
|
|
117
|
+
assets.extend(collect_all_assets(node.children))
|
|
118
|
+
|
|
119
|
+
return assets
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def filter_assets_by_pattern(assets: List[Asset], pattern: str) -> List[Asset]:
|
|
123
|
+
"""Filter assets by regex pattern matching their keys."""
|
|
124
|
+
try:
|
|
125
|
+
compiled_pattern = re.compile(pattern, re.IGNORECASE)
|
|
126
|
+
return [asset for asset in assets if compiled_pattern.search(asset.key)]
|
|
127
|
+
except re.error as e:
|
|
128
|
+
console.print(f"[red]Invalid regex pattern: {e}[/red]")
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_pattern_for_direct_mode(ctx, param, value):
|
|
133
|
+
mode = ctx.params.get('mode')
|
|
134
|
+
if mode == 'direct' and not value:
|
|
135
|
+
raise click.BadParameter('--pattern is required when --mode is "direct"')
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
@auth_command()
|
|
139
|
+
@click.option("-f", "--force", is_flag=True, help="Skip cache")
|
|
140
|
+
@click.option("--mode", "mode", default="interactive", help="One of interactive or direct")
|
|
141
|
+
@click.option("--output-dir", "output_dir", default="./", help="Output path")
|
|
142
|
+
@click.option("--pattern", "pattern", default=None, help="Pattern to search for assets",callback=validate_pattern_for_direct_mode)
|
|
143
|
+
@click.argument("key", required=False)
|
|
144
|
+
@async_command
|
|
145
|
+
async def download(
|
|
146
|
+
force: bool = False,
|
|
147
|
+
output_dir: str = "./",
|
|
148
|
+
mode: str = "interactive",
|
|
149
|
+
pattern: Optional[str] = None,
|
|
150
|
+
key: Optional[str] = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Download IDA binaries, SDK, utilities and more.
|
|
153
|
+
|
|
154
|
+
KEY: The asset key for direct download eg. release/9.1/ida-pro/ida-pro_91_x64linux.run (optional)
|
|
155
|
+
|
|
156
|
+
\b
|
|
157
|
+
When using direct mode, a pattern is required.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
if pattern:
|
|
162
|
+
mode = "direct"
|
|
163
|
+
|
|
164
|
+
if key:
|
|
165
|
+
selected_keys = [key]
|
|
166
|
+
else:
|
|
167
|
+
console.print("[yellow]Fetching available downloads...[/yellow]")
|
|
168
|
+
|
|
169
|
+
if mode == "direct" and pattern:
|
|
170
|
+
# Get downloads from API
|
|
171
|
+
assets = await asset_api.get_files("installers")
|
|
172
|
+
|
|
173
|
+
filtered_assets = filter_assets_by_pattern(assets.items, pattern)
|
|
174
|
+
|
|
175
|
+
if not filtered_assets:
|
|
176
|
+
console.print(f"[red]No assets found matching pattern: {pattern}[/red]")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
console.print(f"[green]Found {len(filtered_assets)} assets matching pattern:[/green]")
|
|
180
|
+
for asset in filtered_assets:
|
|
181
|
+
console.print(f" • {asset.key}")
|
|
182
|
+
|
|
183
|
+
selected_keys = [asset.key for asset in filtered_assets]
|
|
184
|
+
else:
|
|
185
|
+
# Get downloads from API
|
|
186
|
+
assets = await asset_api.get_files_tree("installers")
|
|
187
|
+
|
|
188
|
+
# Interactive navigation
|
|
189
|
+
selected_asset = await select_asset(assets, "")
|
|
190
|
+
|
|
191
|
+
if not selected_asset:
|
|
192
|
+
console.print("[yellow]Download cancelled[/yellow]")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
selected_keys = [selected_asset.key]
|
|
196
|
+
|
|
197
|
+
# Download files
|
|
198
|
+
client = await get_api_client()
|
|
199
|
+
downloaded_files = []
|
|
200
|
+
|
|
201
|
+
for selected_key in selected_keys:
|
|
202
|
+
console.print(f"[yellow]Getting download URL for: {selected_key}[/yellow]")
|
|
203
|
+
try:
|
|
204
|
+
download_asset: Optional[Asset] = await asset_api.get_file("installers", selected_key)
|
|
205
|
+
if not download_asset:
|
|
206
|
+
console.print(f"[red]Asset '{selected_key}' not found[/red]")
|
|
207
|
+
continue
|
|
208
|
+
except Exception as e:
|
|
209
|
+
console.print(f"[red]Failed to get download URL for {selected_key}: {e}[/red]")
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if not download_asset.url:
|
|
213
|
+
console.print(f"[red]Error: No download URL available for asset {selected_key}[/red]")
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Download the file
|
|
217
|
+
console.print(f"[yellow]Starting download of {selected_key}...[/yellow]")
|
|
218
|
+
try:
|
|
219
|
+
target_path = await client.download_file(download_asset.url, target_dir=output_dir, force=force, auth=True)
|
|
220
|
+
downloaded_files.append(target_path)
|
|
221
|
+
console.print(f"[green]Download complete! File saved to: {target_path}[/green]")
|
|
222
|
+
except Exception as e:
|
|
223
|
+
console.print(f"[red]Failed to download {selected_key}: {e}[/red]")
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
if downloaded_files:
|
|
227
|
+
console.print(f"[green]Successfully downloaded {len(downloaded_files)} file(s)[/green]")
|
|
228
|
+
else:
|
|
229
|
+
console.print("[red]No files were downloaded[/red]")
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
console.print(f"[red]Download failed: {e}[/red]")
|
|
233
|
+
raise
|
|
@@ -46,7 +46,7 @@ async def install(
|
|
|
46
46
|
enforce_login()
|
|
47
47
|
|
|
48
48
|
if download_slug:
|
|
49
|
-
await download.callback(output_dir=tmp_dir,
|
|
49
|
+
await download.callback(output_dir=tmp_dir, key=download_slug)
|
|
50
50
|
# Find the downloaded installer file
|
|
51
51
|
installer_path = Path(tmp_dir) / Path(download_slug).name
|
|
52
52
|
installer = str(installer_path)
|
|
@@ -193,8 +193,47 @@ class AuthService:
|
|
|
193
193
|
if self._current_source.type == CredentialType.KEY:
|
|
194
194
|
return bool(self._current_source.token)
|
|
195
195
|
elif self._current_source.type == CredentialType.INTERACTIVE:
|
|
196
|
-
|
|
196
|
+
# For interactive auth, check if session is valid by attempting to get user
|
|
197
|
+
if self.session is not None and self.session.user is not None:
|
|
198
|
+
return True
|
|
199
|
+
# If we have credentials but no valid session, try to refresh
|
|
200
|
+
if self._current_source.token:
|
|
201
|
+
try:
|
|
202
|
+
user_response = self.supabase.auth.get_user()
|
|
203
|
+
if user_response.user:
|
|
204
|
+
self.user = user_response.user
|
|
205
|
+
self.session = self.supabase.auth.get_session()
|
|
206
|
+
return True
|
|
207
|
+
except Exception:
|
|
208
|
+
# Token exists but is expired/invalid
|
|
209
|
+
return False
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
return False
|
|
197
213
|
|
|
214
|
+
def has_expired_session(self) -> bool:
|
|
215
|
+
"""Check if user has credentials but session is expired."""
|
|
216
|
+
# No expired session for environment API key
|
|
217
|
+
if ENV.HCLI_API_KEY:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# No expired session if no credentials exist
|
|
221
|
+
if not self._current_source:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
# Only interactive auth can have expired sessions
|
|
225
|
+
if self._current_source.type != CredentialType.INTERACTIVE:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
# Has credentials but session is invalid/expired
|
|
229
|
+
if self._current_source.token and (self.session is None or self.session.user is None):
|
|
230
|
+
try:
|
|
231
|
+
# Try to verify if token is actually expired
|
|
232
|
+
user_response = self.supabase.auth.get_user()
|
|
233
|
+
return user_response.user is None
|
|
234
|
+
except Exception:
|
|
235
|
+
return True
|
|
236
|
+
|
|
198
237
|
return False
|
|
199
238
|
|
|
200
239
|
def get_auth_type(self) -> Dict[str, str]:
|
|
@@ -20,7 +20,12 @@ def require_auth(f: Callable) -> Callable:
|
|
|
20
20
|
|
|
21
21
|
# Check if user is logged in
|
|
22
22
|
if not auth_service.is_logged_in():
|
|
23
|
-
|
|
23
|
+
if auth_service.has_expired_session():
|
|
24
|
+
current_source = auth_service.get_current_credentials()
|
|
25
|
+
email = current_source.email if current_source else "unknown"
|
|
26
|
+
console.print(f"[red]Your session {email} has expired, use 'hcli login'.[/red]")
|
|
27
|
+
else:
|
|
28
|
+
console.print("[red]You are not logged in. Use 'hcli login'.[/red]")
|
|
24
29
|
sys.exit(1)
|
|
25
30
|
|
|
26
31
|
return f(*args, **kwargs)
|
|
@@ -52,7 +57,12 @@ def enforce_login() -> bool:
|
|
|
52
57
|
auth_service = get_auth_service()
|
|
53
58
|
|
|
54
59
|
if not auth_service.is_logged_in():
|
|
55
|
-
|
|
60
|
+
if auth_service.has_expired_session():
|
|
61
|
+
current_source = auth_service.get_current_credentials()
|
|
62
|
+
email = current_source.email if current_source else "unknown"
|
|
63
|
+
console.print(f"[red]Your session {email} has expired, use 'hcli login'.[/red]")
|
|
64
|
+
else:
|
|
65
|
+
console.print("[red]You are not logged in. Use 'hcli login'.[/red]")
|
|
56
66
|
sys.exit(1)
|
|
57
67
|
|
|
58
68
|
return True
|
|
@@ -97,7 +107,12 @@ class AuthCommand(BaseCommand):
|
|
|
97
107
|
|
|
98
108
|
# Check if user is logged in
|
|
99
109
|
if not auth_service.is_logged_in():
|
|
100
|
-
|
|
110
|
+
if auth_service.has_expired_session():
|
|
111
|
+
current_source = auth_service.get_current_credentials()
|
|
112
|
+
email = current_source.email if current_source else "unknown"
|
|
113
|
+
console.print(f"[red]Your session {email} has expired, use 'hcli login'.[/red]")
|
|
114
|
+
else:
|
|
115
|
+
console.print("[red]You are not logged in. Use 'hcli login'.[/red]")
|
|
101
116
|
sys.exit(1)
|
|
102
117
|
|
|
103
118
|
# Validate forced credentials exists
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.7-dev.2"
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
|
|
5
|
-
import questionary
|
|
6
|
-
import rich_click as click
|
|
7
|
-
from questionary import Choice
|
|
8
|
-
|
|
9
|
-
from hcli.commands.common import safe_ask_async
|
|
10
|
-
from hcli.lib.api.asset import Asset, TreeNode
|
|
11
|
-
from hcli.lib.api.asset import asset as asset_api
|
|
12
|
-
from hcli.lib.api.common import get_api_client
|
|
13
|
-
from hcli.lib.commands import async_command, auth_command
|
|
14
|
-
from hcli.lib.console import console
|
|
15
|
-
from hcli.lib.constants import cli
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class BackNavigationResult:
|
|
19
|
-
"""Special result class to indicate backspace navigation."""
|
|
20
|
-
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
BACK_NAVIGATION = BackNavigationResult()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async def select_asset(nodes: List[TreeNode], current_path: str = "") -> Optional[Asset]:
|
|
28
|
-
"""Alternative traverse using questionary.select with hierarchical navigation."""
|
|
29
|
-
|
|
30
|
-
async def _traverse_recursive(current_nodes: List[TreeNode], path_stack: List[str]) -> Optional[Asset]:
|
|
31
|
-
# Get folders and files at current level
|
|
32
|
-
folders = [node for node in current_nodes if node.type == "folder" and node.children]
|
|
33
|
-
files = [node for node in current_nodes if node.type == "file"]
|
|
34
|
-
|
|
35
|
-
# Build choices using questionary Choice objects
|
|
36
|
-
choices = []
|
|
37
|
-
|
|
38
|
-
# Add "Go back" option if not at root - create a special TreeNode for this
|
|
39
|
-
if path_stack:
|
|
40
|
-
dummy_asset = Asset(filename="", key="")
|
|
41
|
-
back_node = TreeNode(name="← Go back", type="back", asset=dummy_asset)
|
|
42
|
-
choices.append(Choice("← Go back", value=back_node))
|
|
43
|
-
|
|
44
|
-
# Add folders
|
|
45
|
-
for folder in folders:
|
|
46
|
-
choices.append(Choice(f"📁 {folder.name}", value=folder))
|
|
47
|
-
|
|
48
|
-
# Add files
|
|
49
|
-
for file in files:
|
|
50
|
-
# Get display info from asset metadata if available
|
|
51
|
-
display_name = file.name
|
|
52
|
-
if file.asset and file.asset.metadata and "operating_system" in file.asset.metadata:
|
|
53
|
-
display_name = f"{file.asset.metadata['name']} ({file.asset.metadata['operating_system']})"
|
|
54
|
-
choices.append(Choice(f"📄 {display_name}", value=file))
|
|
55
|
-
|
|
56
|
-
if not choices:
|
|
57
|
-
console.print("[red]No items found at this path[/red]")
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
# Show current path
|
|
61
|
-
path_display = "/" + "/".join(path_stack) if path_stack else "/"
|
|
62
|
-
console.print(f"[blue]Current path: {path_display}[/blue]")
|
|
63
|
-
|
|
64
|
-
# Get user selection
|
|
65
|
-
selected_node = await safe_ask_async(
|
|
66
|
-
questionary.select(
|
|
67
|
-
"Select an item to navigate or download:",
|
|
68
|
-
choices=choices,
|
|
69
|
-
use_jk_keys=False,
|
|
70
|
-
use_search_filter=True,
|
|
71
|
-
style=cli.SELECT_STYLE,
|
|
72
|
-
)
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
if not selected_node:
|
|
76
|
-
return None
|
|
77
|
-
|
|
78
|
-
# Handle selection based on node type
|
|
79
|
-
if selected_node.type == "back":
|
|
80
|
-
# Go back one level by removing last item from path stack
|
|
81
|
-
return await _traverse_recursive(_get_nodes_at_path(nodes, path_stack[:-1]), path_stack[:-1])
|
|
82
|
-
elif selected_node.type == "folder":
|
|
83
|
-
# Navigate into folder
|
|
84
|
-
if selected_node.children:
|
|
85
|
-
new_path_stack = path_stack + [selected_node.name]
|
|
86
|
-
return await _traverse_recursive(selected_node.children, new_path_stack)
|
|
87
|
-
return None
|
|
88
|
-
elif selected_node.type == "file":
|
|
89
|
-
# File selected - return the asset key
|
|
90
|
-
return selected_node.asset if selected_node.asset else None
|
|
91
|
-
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
def _get_nodes_at_path(root_nodes: List[TreeNode], path_stack: List[str]) -> List[TreeNode]:
|
|
95
|
-
"""Helper to get nodes at a specific path in the tree."""
|
|
96
|
-
current_nodes = root_nodes
|
|
97
|
-
for path_part in path_stack:
|
|
98
|
-
# Find the folder with matching name
|
|
99
|
-
folder = next((node for node in current_nodes if node.type == "folder" and node.name == path_part), None)
|
|
100
|
-
if not folder or not folder.children:
|
|
101
|
-
return []
|
|
102
|
-
current_nodes = folder.children
|
|
103
|
-
return current_nodes
|
|
104
|
-
|
|
105
|
-
return await _traverse_recursive(nodes, [])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@auth_command()
|
|
109
|
-
@click.option("-f", "--force", is_flag=True, help="Skip cache")
|
|
110
|
-
@click.option("--output-dir", "output_dir", default="./", help="Output path")
|
|
111
|
-
@click.argument("slug", required=False)
|
|
112
|
-
@async_command
|
|
113
|
-
async def download(
|
|
114
|
-
force: bool = False,
|
|
115
|
-
output_dir: str = "./",
|
|
116
|
-
version_filter: Optional[str] = None,
|
|
117
|
-
latest: bool = False,
|
|
118
|
-
category_filter: Optional[str] = None,
|
|
119
|
-
slug: Optional[str] = None,
|
|
120
|
-
) -> None:
|
|
121
|
-
"""Download IDA binaries, SDK, utilities and more."""
|
|
122
|
-
try:
|
|
123
|
-
selected_key: Optional[str]
|
|
124
|
-
|
|
125
|
-
if slug:
|
|
126
|
-
selected_key = slug
|
|
127
|
-
else:
|
|
128
|
-
# Get downloads from API
|
|
129
|
-
console.print("[yellow]Fetching available downloads...[/yellow]")
|
|
130
|
-
assets = await asset_api.get_files_tree("installers")
|
|
131
|
-
|
|
132
|
-
if not assets:
|
|
133
|
-
console.print("[red]No downloads available or unable to fetch downloads[/red]")
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
# Interactive navigation
|
|
137
|
-
selected_asset = await select_asset(assets, "")
|
|
138
|
-
|
|
139
|
-
if not selected_asset:
|
|
140
|
-
console.print("[yellow]Download cancelled[/yellow]")
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
selected_key = selected_asset.key
|
|
144
|
-
|
|
145
|
-
# Get download URL
|
|
146
|
-
console.print(f"[yellow]Getting download URL for: {selected_key}[/yellow]")
|
|
147
|
-
try:
|
|
148
|
-
asset = await asset_api.get_file("installers", selected_key)
|
|
149
|
-
except Exception as e:
|
|
150
|
-
console.print(f"[red]Failed to get download URL: {e}[/red]")
|
|
151
|
-
return
|
|
152
|
-
|
|
153
|
-
if not asset:
|
|
154
|
-
console.print(f"[red]Asset '{selected_key}' not found[/red]")
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
# Download the file
|
|
158
|
-
console.print("[yellow]Starting download...[/yellow]")
|
|
159
|
-
client = await get_api_client()
|
|
160
|
-
|
|
161
|
-
if not asset.url:
|
|
162
|
-
console.print("[red]Error: No download URL available for asset[/red]")
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
target_path = await client.download_file(asset.url, target_dir=output_dir, force=force, auth=True)
|
|
166
|
-
|
|
167
|
-
console.print(f"[green]Download complete! File saved to: {target_path}[/green]")
|
|
168
|
-
|
|
169
|
-
except Exception as e:
|
|
170
|
-
console.print(f"[red]Download failed: {e}[/red]")
|
|
171
|
-
raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|