pawpy-cli 1.0.0b0__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.
pawpy/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Pawpy – The Most Powerful Educational Wordlist Generator.
2
+
3
+ An open-source, terminal-based wordlist generator designed for
4
+ authorised security testing, password audits, and educational purposes.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+ __author__ = "Pawpy Dev"
pawpy/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running as ``python -m pawpy``."""
2
+
3
+ from pawpy.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
pawpy/api/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """API and web dashboard subsystem."""
pawpy/api/dashboard.py ADDED
@@ -0,0 +1,183 @@
1
+ """Web dashboard with WebSocket real-time logging.
2
+
3
+ Provides a simple browser-based interface to trigger wordlist generation
4
+ and watch progress in real time.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ from typing import Set
12
+
13
+ logger = logging.getLogger("pawpy.dashboard")
14
+
15
+ _connections: Set = set()
16
+
17
+
18
+ # Minimal HTML for the dashboard
19
+ _DASHBOARD_HTML = """<!DOCTYPE html>
20
+ <html lang="en">
21
+ <head>
22
+ <meta charset="UTF-8">
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
+ <title>Pawpy Dashboard</title>
25
+ <style>
26
+ * { box-sizing: border-box; margin: 0; padding: 0; }
27
+ body {
28
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
29
+ background: #0d1117; color: #c9d1d9;
30
+ display: flex; min-height: 100vh;
31
+ }
32
+ .sidebar {
33
+ width: 280px; background: #161b22; padding: 20px;
34
+ border-right: 1px solid #30363d;
35
+ }
36
+ .sidebar h1 { color: #58a6ff; font-size: 1.5rem; margin-bottom: 20px; }
37
+ .sidebar label { display: block; color: #8b949e; margin: 12px 0 4px; font-size: 0.85rem; }
38
+ .sidebar input[type="file"], .sidebar input[type="text"],
39
+ .sidebar select { width: 100%; padding: 8px; background: #0d1117;
40
+ border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; }
41
+ .sidebar button { width: 100%; padding: 10px; margin-top: 16px;
42
+ background: #238636; color: #fff; border: none; border-radius: 6px;
43
+ cursor: pointer; font-size: 1rem; font-weight: bold; }
44
+ .sidebar button:hover { background: #2ea043; }
45
+ .sidebar .checkbox { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
46
+ .sidebar .checkbox input { width: auto; }
47
+ .main { flex: 1; display: flex; flex-direction: column; }
48
+ .header { padding: 16px 20px; background: #161b22;
49
+ border-bottom: 1px solid #30363d; font-size: 1.1rem; }
50
+ .log-area { flex: 1; padding: 16px; overflow-y: auto; font-family: 'Fira Code', monospace;
51
+ font-size: 0.85rem; line-height: 1.6; }
52
+ .log-line { padding: 2px 0; }
53
+ .log-line.info { color: #58a6ff; }
54
+ .log-line.success { color: #3fb950; }
55
+ .log-line.warning { color: #d29922; }
56
+ .log-line.error { color: #f85149; }
57
+ .stats { padding: 12px 20px; background: #161b22;
58
+ border-top: 1px solid #30363d; display: flex; gap: 24px; }
59
+ .stat-item span.label { color: #8b949e; font-size: 0.8rem; }
60
+ .stat-item span.value { color: #58a6ff; font-size: 1.2rem; font-weight: bold; }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="sidebar">
65
+ <h1>Pawpy</h1>
66
+ <label>Profile JSON</label>
67
+ <input type="file" id="profileFile" accept=".json">
68
+ <label>Output File</label>
69
+ <input type="text" id="outputFile" placeholder="pawpy_wordlist.txt">
70
+ <label>Mode</label>
71
+ <select id="mode">
72
+ <option value="normal">Normal</option>
73
+ <option value="lite">Lite (fast)</option>
74
+ <option value="extreme">Extreme (all mutations)</option>
75
+ </select>
76
+ <div class="checkbox"><input type="checkbox" id="markov"> <label for="markov" style="margin:0">Markov Blending</label></div>
77
+ <div class="checkbox"><input type="checkbox" id="scoring"> <label for="scoring" style="margin:0">zxcvbn Scoring</label></div>
78
+ <button onclick="startGeneration()">Generate Wordlist</button>
79
+ </div>
80
+ <div class="main">
81
+ <div class="header">Generation Log</div>
82
+ <div class="log-area" id="logArea"></div>
83
+ <div class="stats">
84
+ <div class="stat-item"><span class="label">Candidates</span><br><span class="value" id="statCandidates">0</span></div>
85
+ <div class="stat-item"><span class="label">Output Size</span><br><span class="value" id="statSize">0 B</span></div>
86
+ <div class="stat-item"><span class="label">Status</span><br><span class="value" id="statStatus">Idle</span></div>
87
+ </div>
88
+ </div>
89
+ <script>
90
+ const ws = new WebSocket(`ws://${window.location.host}/ws`);
91
+ ws.onmessage = (e) => {
92
+ const msg = JSON.parse(e.data);
93
+ const logArea = document.getElementById('logArea');
94
+ const line = document.createElement('div');
95
+ line.className = `log-line ${msg.level || 'info'}`;
96
+ line.textContent = msg.text;
97
+ logArea.appendChild(line);
98
+ logArea.scrollTop = logArea.scrollHeight;
99
+ if (msg.candidates) document.getElementById('statCandidates').textContent = Number(msg.candidates).toLocaleString();
100
+ if (msg.size) document.getElementById('statSize').textContent = (msg.size / (1024*1024)).toFixed(2) + ' MB';
101
+ if (msg.status) document.getElementById('statStatus').textContent = msg.status;
102
+ };
103
+ ws.onclose = () => { addLog('WebSocket disconnected', 'error'); };
104
+ function addLog(text, level) {
105
+ const logArea = document.getElementById('logArea');
106
+ const line = document.createElement('div');
107
+ line.className = `log-line ${level}`;
108
+ line.textContent = text;
109
+ logArea.appendChild(line);
110
+ }
111
+ async function startGeneration() {
112
+ const fileInput = document.getElementById('profileFile');
113
+ if (!fileInput.files.length) { addLog('Please select a profile JSON file.', 'warning'); return; }
114
+ const formData = new FormData();
115
+ formData.append('profile', fileInput.files[0]);
116
+ formData.append('output', document.getElementById('outputFile').value || 'pawpy_wordlist.txt');
117
+ const mode = document.getElementById('mode').value;
118
+ formData.append('lite', mode === 'lite');
119
+ formData.append('extreme', mode === 'extreme');
120
+ formData.append('markov', document.getElementById('markov').checked);
121
+ formData.append('min_strength', document.getElementById('scoring').checked ? '2' : '');
122
+ addLog('Starting generation...', 'info');
123
+ try {
124
+ const resp = await fetch('/generate', { method: 'POST', body: formData });
125
+ const data = await resp.json();
126
+ if (data.download_url) {
127
+ addLog('Done! Download: ' + data.download_url, 'success');
128
+ } else {
129
+ addLog('Error: ' + (data.message || 'Unknown'), 'error');
130
+ }
131
+ } catch (err) { addLog('Fetch error: ' + err.message, 'error'); }
132
+ }
133
+ </script>
134
+ </body>
135
+ </html>
136
+ """
137
+
138
+
139
+ async def _websocket_handler(websocket):
140
+ """Handle a single WebSocket connection for real-time logging."""
141
+ _connections.add(websocket)
142
+ try:
143
+ async for message in websocket:
144
+ pass # Dashboard only receives; no client commands over WS
145
+ finally:
146
+ _connections.discard(websocket)
147
+
148
+
149
+ async def broadcast_log(text: str, level: str = "info", **extra):
150
+ """Broadcast a log message to all connected WebSocket clients."""
151
+ msg = json.dumps({"text": text, "level": level, **extra})
152
+ for ws in list(_connections):
153
+ try:
154
+ await ws.send_text(msg)
155
+ except Exception:
156
+ _connections.discard(ws)
157
+
158
+
159
+ def run_dashboard(host: str = "127.0.0.1", port: int = 8080):
160
+ """Launch the web dashboard with both HTTP and WebSocket support."""
161
+ try:
162
+ import uvicorn
163
+ from fastapi import FastAPI, WebSocket
164
+ from fastapi.responses import HTMLResponse
165
+ except ImportError:
166
+ raise ImportError(
167
+ "FastAPI and uvicorn are required for dashboard mode. "
168
+ "Install them with: pip install fastapi uvicorn websockets"
169
+ )
170
+
171
+ app = FastAPI(title="Pawpy Dashboard")
172
+
173
+ @app.get("/", response_class=HTMLResponse)
174
+ async def dashboard_page():
175
+ return _DASHBOARD_HTML
176
+
177
+ @app.websocket("/ws")
178
+ async def websocket_endpoint(websocket: WebSocket):
179
+ await websocket.accept()
180
+ await _websocket_handler(websocket)
181
+
182
+ logger.info("Starting Pawpy Dashboard on http://%s:%d", host, port)
183
+ uvicorn.run(app, host=host, port=port)
pawpy/api/rest.py ADDED
@@ -0,0 +1,145 @@
1
+ """FastAPI REST endpoints for Pawpy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import tempfile
8
+ import uuid
9
+ from typing import Any, Dict, Optional
10
+
11
+ logger = logging.getLogger("pawpy.api")
12
+
13
+ _app = None
14
+
15
+
16
+ def get_app():
17
+ """Lazily create and return the FastAPI app."""
18
+ global _app
19
+ if _app is not None:
20
+ return _app
21
+
22
+ try:
23
+ from fastapi import FastAPI, File, Form, UploadFile
24
+ from fastapi.responses import FileResponse, JSONResponse
25
+ except ImportError:
26
+ raise ImportError(
27
+ "FastAPI is required for API mode. "
28
+ "Install it with: pip install fastapi uvicorn"
29
+ )
30
+
31
+ app = FastAPI(
32
+ title="Pawpy API",
33
+ description="Educational Wordlist Generator – REST API",
34
+ version="1.0.0",
35
+ )
36
+
37
+ # In-memory job store (simple; production would use a database)
38
+ _jobs: Dict[str, Dict[str, Any]] = {}
39
+
40
+ @app.get("/")
41
+ async def root():
42
+ return {"name": "Pawpy API", "version": "1.0.0", "status": "running"}
43
+
44
+ @app.post("/generate")
45
+ async def generate_wordlist(
46
+ profile: UploadFile = File(...),
47
+ output: Optional[str] = Form(None),
48
+ lite: bool = Form(False),
49
+ extreme: bool = Form(False),
50
+ min_length: Optional[int] = Form(None),
51
+ min_strength: Optional[int] = Form(None),
52
+ markov: bool = Form(False),
53
+ ):
54
+ """Generate a wordlist from an uploaded profile JSON file."""
55
+ job_id = str(uuid.uuid4())[:8]
56
+ out_file = output or f"pawpy_{job_id}.txt"
57
+
58
+ # Save uploaded profile to temp file
59
+ tmp_profile = tempfile.NamedTemporaryFile(
60
+ mode="w", suffix=".json", delete=False, prefix="pawpy_profile_"
61
+ )
62
+ content = await profile.read()
63
+ tmp_profile.write(content.decode("utf-8", errors="ignore"))
64
+ tmp_profile.close()
65
+
66
+ try:
67
+ from pawpy.config import PawpyConfig
68
+ from pawpy.generator.core import PipelineOrchestrator
69
+ from pawpy.profile.base import ProfileCollector
70
+
71
+ config = PawpyConfig(
72
+ output_file=out_file,
73
+ profile_json=tmp_profile.name,
74
+ lite=lite,
75
+ extreme=extreme,
76
+ min_length=min_length,
77
+ min_strength=min_strength,
78
+ markov=markov,
79
+ )
80
+
81
+ collector = ProfileCollector()
82
+ prof = collector.run(config)
83
+ orchestrator = PipelineOrchestrator(config, prof)
84
+ result_path = orchestrator.run()
85
+
86
+ _jobs[job_id] = {
87
+ "status": "completed",
88
+ "output": result_path,
89
+ "size": os.path.getsize(result_path),
90
+ }
91
+
92
+ return JSONResponse(
93
+ content={
94
+ "job_id": job_id,
95
+ "status": "completed",
96
+ "download_url": f"/download/{job_id}",
97
+ }
98
+ )
99
+
100
+ except Exception as e:
101
+ logger.exception("Generation failed")
102
+ return JSONResponse(
103
+ status_code=500,
104
+ content={"job_id": job_id, "status": "error", "message": str(e)},
105
+ )
106
+ finally:
107
+ try:
108
+ os.unlink(tmp_profile.name)
109
+ except Exception:
110
+ pass
111
+
112
+ @app.get("/download/{job_id}")
113
+ async def download_wordlist(job_id: str):
114
+ """Download a generated wordlist by job ID."""
115
+ job = _jobs.get(job_id)
116
+ if not job or job["status"] != "completed":
117
+ return JSONResponse(
118
+ status_code=404, content={"error": "Job not found or not completed"}
119
+ )
120
+ return FileResponse(
121
+ job["output"],
122
+ media_type="text/plain",
123
+ filename=os.path.basename(job["output"]),
124
+ )
125
+
126
+ @app.get("/jobs")
127
+ async def list_jobs():
128
+ """List all generation jobs."""
129
+ return {"jobs": _jobs}
130
+
131
+ _app = app
132
+ return app
133
+
134
+
135
+ def run_api(host: str = "127.0.0.1", port: int = 8000):
136
+ """Start the API server using uvicorn."""
137
+ app = get_app()
138
+ try:
139
+ import uvicorn
140
+ except ImportError:
141
+ raise ImportError(
142
+ "uvicorn is required for API mode. " "Install it with: pip install uvicorn"
143
+ )
144
+ logger.info("Starting Pawpy API on %s:%d", host, port)
145
+ uvicorn.run(app, host=host, port=port)
pawpy/cli.py ADDED
@@ -0,0 +1,341 @@
1
+ """Pawpy command-line interface.
2
+
3
+ Parses all CLI options, displays the banner, orchestrates profile collection
4
+ and the generation pipeline, and handles sub-commands (update-passwords,
5
+ api, dashboard).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import logging
12
+ import sys
13
+
14
+ from pawpy import __version__
15
+ from pawpy.config import PawpyConfig
16
+ from pawpy.utils import confirm_ethical_use, console, print_banner, setup_logging
17
+
18
+ logger = logging.getLogger("pawpy")
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ """Build and return the argument parser with all options."""
23
+ parser = argparse.ArgumentParser(
24
+ prog="pawpy",
25
+ description="Pawpy – The Most Powerful Educational Wordlist Generator",
26
+ epilog=(
27
+ "Examples:\n"
28
+ " pawpy # Interactive mode\n"
29
+ " pawpy -j profile.json -o wordlist.txt # From JSON profile\n"
30
+ " pawpy --multi targets.json --extreme # Multi-target extreme mode\n"
31
+ " pawpy --rules best64.rule --markov # Rules + Markov blending\n"
32
+ " pawpy --hybrid-left ?l?d -o hybrid.txt # Hybrid mask attack\n"
33
+ " pawpy --min-length 8 --require-upper # Policy-filtered output\n"
34
+ " pawpy update-passwords # Download latest SecLists\n"
35
+ " pawpy api # Start REST API\n"
36
+ " pawpy dashboard # Launch web dashboard\n"
37
+ ),
38
+ formatter_class=argparse.RawDescriptionHelpFormatter,
39
+ )
40
+
41
+ # --- I/O ---
42
+ parser.add_argument(
43
+ "-o",
44
+ "--output",
45
+ default="pawpy_wordlist.txt",
46
+ help="Output file path (default: pawpy_wordlist.txt)",
47
+ )
48
+ parser.add_argument(
49
+ "-j",
50
+ "--import-json",
51
+ dest="profile_json",
52
+ help="Import a single target profile from a JSON file",
53
+ )
54
+ parser.add_argument(
55
+ "--multi",
56
+ help="Import multiple profiles from a JSON array file",
57
+ )
58
+ parser.add_argument(
59
+ "--rules",
60
+ help="Load a hashcat/John-style .rule file",
61
+ )
62
+ parser.add_argument(
63
+ "--template",
64
+ action="append",
65
+ default=[],
66
+ dest="templates",
67
+ help="Custom pattern template, e.g. [FirstName][Year][!] (repeatable)",
68
+ )
69
+
70
+ # --- Hybrid masks ---
71
+ parser.add_argument(
72
+ "--hybrid-left",
73
+ help="Left mask for hybrid attack (hashcat -a 7), e.g. ?l?d",
74
+ )
75
+ parser.add_argument(
76
+ "--hybrid-right",
77
+ help="Right mask for hybrid attack (hashcat -a 6), e.g. ?d?d?d",
78
+ )
79
+
80
+ # --- Markov ---
81
+ parser.add_argument(
82
+ "--markov",
83
+ action="store_true",
84
+ help="Enable Markov chain blending",
85
+ )
86
+ parser.add_argument(
87
+ "--markov-order",
88
+ type=int,
89
+ default=2,
90
+ help="Markov chain order (default: 2)",
91
+ )
92
+ parser.add_argument(
93
+ "--markov-count",
94
+ type=int,
95
+ default=5000,
96
+ help="Number of Markov-generated words (default: 5000)",
97
+ )
98
+
99
+ # --- Scoring / filtering ---
100
+ parser.add_argument(
101
+ "--min-strength",
102
+ type=int,
103
+ choices=[0, 1, 2, 3, 4],
104
+ help="Minimum zxcvbn strength score (0-4). Requires zxcvbn installed.",
105
+ )
106
+ parser.add_argument(
107
+ "--min-length",
108
+ type=int,
109
+ help="Minimum password length",
110
+ )
111
+ parser.add_argument(
112
+ "--require-upper",
113
+ action="store_true",
114
+ help="Policy: require at least one uppercase letter",
115
+ )
116
+ parser.add_argument(
117
+ "--require-lower",
118
+ action="store_true",
119
+ help="Policy: require at least one lowercase letter",
120
+ )
121
+ parser.add_argument(
122
+ "--require-digit",
123
+ action="store_true",
124
+ help="Policy: require at least one digit",
125
+ )
126
+ parser.add_argument(
127
+ "--require-special",
128
+ action="store_true",
129
+ help="Policy: require at least one special character",
130
+ )
131
+
132
+ # --- Modes ---
133
+ mode_group = parser.add_mutually_exclusive_group()
134
+ mode_group.add_argument(
135
+ "--lite",
136
+ action="store_true",
137
+ help="Fast mode: skip heavy combinations (two-word, Markov, large hybrid)",
138
+ )
139
+ mode_group.add_argument(
140
+ "--extreme",
141
+ action="store_true",
142
+ help="Enable all heavy mutations (year blends, Markov, dynamic keyboard walks)",
143
+ )
144
+
145
+ # --- Performance ---
146
+ parser.add_argument(
147
+ "--gpu",
148
+ action="store_true",
149
+ help="Use GPU acceleration if CuPy is available",
150
+ )
151
+ parser.add_argument(
152
+ "-t",
153
+ "--threads",
154
+ type=int,
155
+ default=None,
156
+ help="Number of worker processes (default: CPU count)",
157
+ )
158
+
159
+ # --- Meta ---
160
+ parser.add_argument(
161
+ "-v",
162
+ "--version",
163
+ action="version",
164
+ version=f"%(prog)s {__version__}",
165
+ )
166
+ parser.add_argument(
167
+ "--verbose",
168
+ action="store_true",
169
+ help="Enable verbose (debug) logging",
170
+ )
171
+
172
+ return parser
173
+
174
+
175
+ def build_config(args: argparse.Namespace) -> PawpyConfig:
176
+ """Convert parsed arguments into a PawpyConfig object."""
177
+ import multiprocessing
178
+
179
+ config = PawpyConfig(
180
+ output_file=args.output,
181
+ profile_json=args.profile_json,
182
+ multi_json=args.multi,
183
+ rule_file=args.rules,
184
+ templates=args.templates,
185
+ hybrid_left=args.hybrid_left,
186
+ hybrid_right=args.hybrid_right,
187
+ markov=args.markov,
188
+ markov_order=args.markov_order,
189
+ markov_count=args.markov_count,
190
+ min_strength=args.min_strength,
191
+ min_length=args.min_length,
192
+ require_upper=args.require_upper,
193
+ require_lower=args.require_lower,
194
+ require_digit=args.require_digit,
195
+ require_special=args.require_special,
196
+ lite=args.lite,
197
+ extreme=args.extreme,
198
+ gpu=args.gpu,
199
+ threads=args.threads if args.threads else multiprocessing.cpu_count() or 4,
200
+ )
201
+ return config
202
+
203
+
204
+ def handle_update_passwords() -> None:
205
+ """Handle the 'update-passwords' sub-command."""
206
+ from pawpy.data.updater import update_common_passwords
207
+
208
+ console.print("[cyan]Downloading latest common passwords from SecLists...[/cyan]")
209
+ try:
210
+ path = update_common_passwords()
211
+ console.print(f"[green]Updated common passwords saved to:[/green] {path}")
212
+ except Exception as e:
213
+ console.print(f"[red]Failed to update:[/red] {e}")
214
+ sys.exit(1)
215
+
216
+
217
+ def handle_api() -> None:
218
+ """Handle the 'api' sub-command."""
219
+ from pawpy.api.rest import run_api
220
+
221
+ console.print("[cyan]Starting Pawpy REST API...[/cyan]")
222
+ try:
223
+ run_api()
224
+ except ImportError as e:
225
+ console.print(f"[red]Missing dependency:[/red] {e}")
226
+ sys.exit(1)
227
+
228
+
229
+ def handle_dashboard() -> None:
230
+ """Handle the 'dashboard' sub-command."""
231
+ from pawpy.api.dashboard import run_dashboard
232
+
233
+ console.print("[cyan]Starting Pawpy Web Dashboard...[/cyan]")
234
+ try:
235
+ run_dashboard()
236
+ except ImportError as e:
237
+ console.print(f"[red]Missing dependency:[/red] {e}")
238
+ sys.exit(1)
239
+
240
+
241
+ def main(argv: list = None) -> None:
242
+ """Main entry point for the Pawpy CLI."""
243
+ # Handle sub-commands first (they don't need the full parser)
244
+ if argv is None:
245
+ argv = sys.argv[1:]
246
+
247
+ if not argv:
248
+ # No args → show banner, then interactive mode
249
+ pass
250
+ elif argv[0] == "update-passwords":
251
+ handle_update_passwords()
252
+ return
253
+ elif argv[0] == "api":
254
+ handle_api()
255
+ return
256
+ elif argv[0] == "dashboard":
257
+ handle_dashboard()
258
+ return
259
+
260
+ # Parse arguments
261
+ parser = build_parser()
262
+ args = parser.parse_args(argv)
263
+
264
+ setup_logging()
265
+
266
+ # Print banner and ethical warning
267
+ print_banner()
268
+
269
+ # For interactive mode, confirm ethical use
270
+ if not args.profile_json and not args.multi:
271
+ if not confirm_ethical_use():
272
+ console.print("[yellow]Authorisation not confirmed. Exiting.[/yellow]")
273
+ sys.exit(0)
274
+ else:
275
+ console.print(
276
+ "[dim]Note: Ensure you have explicit authorisation to test "
277
+ "target accounts.[/dim]\n"
278
+ )
279
+
280
+ # Build configuration
281
+ config = build_config(args)
282
+
283
+ # Collect profile
284
+ from pawpy.generator.core import PipelineOrchestrator
285
+ from pawpy.profile.base import ProfileCollector
286
+ from pawpy.profile.multi import (
287
+ extract_merged_base_words,
288
+ load_multi_profiles,
289
+ merge_profiles,
290
+ )
291
+
292
+ if config.multi_json:
293
+ console.print(
294
+ f"[cyan]Loading multi-target profiles from:[/cyan] {config.multi_json}"
295
+ )
296
+ profiles = load_multi_profiles(config.multi_json)
297
+ merged = merge_profiles(profiles)
298
+ profile = merged
299
+ base_words = extract_merged_base_words(merged)
300
+ console.print(
301
+ f"[green]Extracted {len(base_words)} unique base words from {len(profiles)} profiles.[/green]\n"
302
+ )
303
+ else:
304
+ collector = ProfileCollector()
305
+ profile = collector.run(config)
306
+ base_words = ProfileCollector.extract_base_words(profile)
307
+ if base_words:
308
+ console.print(
309
+ f"[green]Extracted {len(base_words)} base words from profile.[/green]\n"
310
+ )
311
+
312
+ # GPU availability check
313
+ if config.gpu:
314
+ from pawpy.generator.gpu import is_gpu_available
315
+
316
+ if is_gpu_available():
317
+ console.print("[green]GPU acceleration enabled (CuPy detected).[/green]\n")
318
+ else:
319
+ console.print(
320
+ "[yellow]CuPy not installed. GPU mode requested but falling back to CPU.[/yellow]\n"
321
+ )
322
+ console.print(
323
+ "[dim]Install CuPy for GPU support: pip install cupy-cuda11x (or cupy-cuda12x)[/dim]\n"
324
+ )
325
+
326
+ # Run the generation pipeline
327
+ try:
328
+ orchestrator = PipelineOrchestrator(config, profile)
329
+ output_path = orchestrator.run()
330
+ console.print(f"\n[bold green]Wordlist saved to: {output_path}[/bold green]")
331
+ except KeyboardInterrupt:
332
+ console.print("\n[yellow]Generation interrupted by user.[/yellow]")
333
+ sys.exit(130)
334
+ except Exception as e:
335
+ logger.exception("Generation failed")
336
+ console.print(f"\n[bold red]Error:[/bold red] {e}")
337
+ sys.exit(1)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()