pomera-ai-commander 1.2.1 → 1.2.3

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.
@@ -0,0 +1,33 @@
1
+ # Minimal requirements for Pomera AI Commander
2
+ # Only the absolutely essential libraries
3
+
4
+ # Core GUI and functionality
5
+ requests>=2.25.0
6
+ reportlab>=3.6.0
7
+ python-docx>=0.8.11
8
+ aiohttp>=3.9.0
9
+
10
+ # AI Tools (optional but core feature)
11
+ huggingface-hub>=0.16.0
12
+
13
+ # Google AI SDK (recommended for better error handling and streaming)
14
+ google-genai>=1.0.0
15
+
16
+ # Azure AI SDK (recommended - Microsoft recommends migrating to this)
17
+ azure-ai-inference>=1.0.0b1
18
+ azure-core>=1.30.0
19
+
20
+ # Retry logic with exponential backoff
21
+ tenacity>=8.2.0
22
+
23
+ # Encryption for AI tools
24
+ cryptography>=3.4.0
25
+
26
+ # Image processing (required by reportlab)
27
+ Pillow>=8.0.0
28
+
29
+ # XML processing (required by python-docx)
30
+ lxml>=4.6.0
31
+
32
+ # Build tool
33
+ pyinstaller>=5.0.0
@@ -0,0 +1,28 @@
1
+ @echo off
2
+ echo Testing Linux executable in Docker container...
3
+ echo.
4
+
5
+ REM Check if the Linux executable exists
6
+ if not exist "dist-docker\pomera-linux" (
7
+ echo ERROR: Linux executable not found!
8
+ echo Please build it first using: build-docker.bat
9
+ pause
10
+ exit /b 1
11
+ )
12
+
13
+ echo Starting Ubuntu container with your Linux executable...
14
+ echo.
15
+ echo Commands you can try inside the container:
16
+ echo ./pomera-linux --help
17
+ echo ./pomera-linux --version
18
+ echo ls -la pomera-linux
19
+ echo file pomera-linux
20
+ echo.
21
+ echo Type 'exit' to return to Windows
22
+ echo.
23
+
24
+ docker run -it --rm -v "%cd%\dist-docker:/app" -w /app ubuntu:22.04 /bin/bash
25
+
26
+ echo.
27
+ echo Returned to Windows
28
+ pause
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Release Workflow Validation Script
4
+
5
+ This script validates the release workflow components locally before running
6
+ the full GitHub Actions workflow. It performs basic checks on:
7
+ - PyInstaller build capability
8
+ - Executable validation
9
+ - Checksum generation
10
+ - File naming conventions
11
+
12
+ Usage:
13
+ python scripts/validate-release-workflow.py [--test-tag v0.0.1-test]
14
+ """
15
+
16
+ import argparse
17
+ import hashlib
18
+ import os
19
+ import platform
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ import tempfile
24
+ from pathlib import Path
25
+
26
+
27
+ class Colors:
28
+ """ANSI color codes for terminal output"""
29
+ GREEN = '\033[92m'
30
+ RED = '\033[91m'
31
+ YELLOW = '\033[93m'
32
+ BLUE = '\033[94m'
33
+ BOLD = '\033[1m'
34
+ END = '\033[0m'
35
+
36
+
37
+ def print_success(message):
38
+ print(f"{Colors.GREEN}✅ {message}{Colors.END}")
39
+
40
+
41
+ def print_error(message):
42
+ print(f"{Colors.RED}❌ {message}{Colors.END}")
43
+
44
+
45
+ def print_warning(message):
46
+ print(f"{Colors.YELLOW}⚠️ {message}{Colors.END}")
47
+
48
+
49
+ def print_info(message):
50
+ print(f"{Colors.BLUE}ℹ️ {message}{Colors.END}")
51
+
52
+
53
+ def print_header(message):
54
+ print(f"\n{Colors.BOLD}{Colors.BLUE}=== {message} ==={Colors.END}")
55
+
56
+
57
+ class ReleaseWorkflowValidator:
58
+ def __init__(self, test_tag="v0.0.1-test"):
59
+ self.test_tag = test_tag
60
+ self.platform_name = self._get_platform_name()
61
+ self.executable_extension = ".exe" if platform.system() == "Windows" else ""
62
+ self.temp_dir = None
63
+ self.errors = []
64
+ self.warnings = []
65
+
66
+ def _get_platform_name(self):
67
+ """Get platform name matching GitHub Actions matrix"""
68
+ system = platform.system().lower()
69
+ if system == "windows":
70
+ return "windows"
71
+ elif system == "darwin":
72
+ return "macos"
73
+ elif system == "linux":
74
+ return "linux"
75
+ else:
76
+ return system
77
+
78
+ def validate_prerequisites(self):
79
+ """Validate that required tools are available"""
80
+ print_header("Validating Prerequisites")
81
+
82
+ # Check Python version
83
+ python_version = sys.version_info
84
+ if python_version < (3, 8):
85
+ self.errors.append(f"Python 3.8+ required, found {python_version.major}.{python_version.minor}")
86
+ print_error(f"Python version: {python_version.major}.{python_version.minor}.{python_version.micro}")
87
+ else:
88
+ print_success(f"Python version: {python_version.major}.{python_version.minor}.{python_version.micro}")
89
+
90
+ # Check PyInstaller availability
91
+ try:
92
+ result = subprocess.run(["pyinstaller", "--version"],
93
+ capture_output=True, text=True, timeout=10)
94
+ if result.returncode == 0:
95
+ version = result.stdout.strip()
96
+ print_success(f"PyInstaller available: {version}")
97
+ else:
98
+ self.errors.append("PyInstaller not working properly")
99
+ print_error("PyInstaller not working properly")
100
+ except (subprocess.TimeoutExpired, FileNotFoundError):
101
+ self.errors.append("PyInstaller not found - install with: pip install pyinstaller")
102
+ print_error("PyInstaller not found")
103
+
104
+ # Check main application file
105
+ if not os.path.exists("pomera.py"):
106
+ self.errors.append("pomera.py not found in current directory")
107
+ print_error("pomera.py not found")
108
+ else:
109
+ print_success("pomera.py found")
110
+
111
+ # Check for requirements.txt
112
+ if os.path.exists("requirements.txt"):
113
+ print_success("requirements.txt found")
114
+ else:
115
+ print_warning("requirements.txt not found - dependencies may not be installed")
116
+ self.warnings.append("requirements.txt not found")
117
+
118
+ def validate_tag_format(self):
119
+ """Validate test tag format"""
120
+ print_header("Validating Tag Format")
121
+
122
+ import re
123
+ pattern = r'^v\d+\.\d+\.\d+(-test)?$'
124
+
125
+ if re.match(pattern, self.test_tag):
126
+ print_success(f"Tag format valid: {self.test_tag}")
127
+ else:
128
+ self.errors.append(f"Invalid tag format: {self.test_tag} (should match v*.*.*-test)")
129
+ print_error(f"Invalid tag format: {self.test_tag}")
130
+
131
+ def test_pyinstaller_build(self):
132
+ """Test PyInstaller build process"""
133
+ print_header("Testing PyInstaller Build")
134
+
135
+ # Create temporary directory in project directory to avoid cross-drive issues on Windows
136
+ import uuid
137
+ temp_name = f"release_test_{uuid.uuid4().hex[:8]}"
138
+ self.temp_dir = os.path.join(os.getcwd(), temp_name)
139
+ os.makedirs(self.temp_dir, exist_ok=True)
140
+ print_info(f"Using temporary directory: {self.temp_dir}")
141
+
142
+ try:
143
+ # Prepare executable name
144
+ executable_name = f"pomera-{self.test_tag}-{self.platform_name}"
145
+
146
+ # Build PyInstaller command
147
+ cmd = [
148
+ sys.executable, "-m", "PyInstaller",
149
+ "--onefile",
150
+ "--log-level", "INFO",
151
+ "--name", executable_name,
152
+ "--distpath", os.path.join(self.temp_dir, "dist"),
153
+ "--workpath", os.path.join(self.temp_dir, "build"),
154
+ "--specpath", os.path.join(self.temp_dir, "spec"),
155
+ "--optimize", "2", # Maximum Python optimization
156
+ "--strip", # Strip debug symbols (Unix)
157
+ "--noupx", # We'll handle UPX separately for better control
158
+ "--exclude-module", "pytest",
159
+ "--exclude-module", "test",
160
+ "--exclude-module", "tests",
161
+ "--exclude-module", "matplotlib",
162
+ "--exclude-module", "scipy",
163
+ "--exclude-module", "pandas",
164
+ "--exclude-module", "jupyter",
165
+ "--exclude-module", "IPython",
166
+ "--exclude-module", "torch",
167
+ "--exclude-module", "torchvision",
168
+ "--exclude-module", "torchaudio",
169
+ "--exclude-module", "tensorflow",
170
+ "--exclude-module", "sklearn",
171
+ "--exclude-module", "cv2",
172
+ "--exclude-module", "numpy",
173
+ "--exclude-module", "pygame",
174
+ "--exclude-module", "nltk",
175
+ "--exclude-module", "spacy",
176
+ "--exclude-module", "yt_dlp",
177
+ "--exclude-module", "transformers",
178
+ "--exclude-module", "boto3",
179
+ "--exclude-module", "botocore",
180
+ "--exclude-module", "grpc",
181
+ "--exclude-module", "onnxruntime",
182
+ "--exclude-module", "opentelemetry",
183
+ "--exclude-module", "timm",
184
+ "--exclude-module", "emoji",
185
+ "--exclude-module", "pygments",
186
+ "--exclude-module", "jinja2",
187
+ "--exclude-module", "anyio",
188
+ "--exclude-module", "orjson",
189
+ "--exclude-module", "uvicorn",
190
+ "--exclude-module", "fsspec",
191
+ "--exclude-module", "websockets",
192
+ "--exclude-module", "psutil",
193
+ "--exclude-module", "regex",
194
+ "--exclude-module", "pydantic",
195
+ "--exclude-module", "dateutil",
196
+ "--exclude-module", "pytz",
197
+ "--exclude-module", "six",
198
+ "--exclude-module", "pkg_resources",
199
+ "pomera.py"
200
+ ]
201
+
202
+ # Add windowed mode for GUI applications
203
+ if self.platform_name in ["windows", "macos"]:
204
+ cmd.insert(-1, "--windowed")
205
+
206
+ print_info(f"Running: {' '.join(cmd)}")
207
+ print_info("PyInstaller output:")
208
+
209
+ # Run PyInstaller with live output (don't capture output)
210
+ result = subprocess.run(cmd, timeout=300)
211
+
212
+ # Try to compress with UPX if available
213
+ if result.returncode == 0:
214
+ expected_exe = os.path.join(self.temp_dir, "dist",
215
+ f"{executable_name}{self.executable_extension}")
216
+ if os.path.exists(expected_exe):
217
+ self._try_upx_compression(expected_exe)
218
+
219
+ if result.returncode == 0:
220
+ print_success("PyInstaller build completed successfully")
221
+
222
+ # Check if executable was created
223
+ expected_exe = os.path.join(self.temp_dir, "dist",
224
+ f"{executable_name}{self.executable_extension}")
225
+
226
+ if os.path.exists(expected_exe):
227
+ print_success(f"Executable created: {expected_exe}")
228
+ return expected_exe
229
+ else:
230
+ self.errors.append(f"Executable not found at expected location: {expected_exe}")
231
+ print_error(f"Executable not found: {expected_exe}")
232
+ # List what was actually created
233
+ dist_dir = os.path.join(self.temp_dir, "dist")
234
+ if os.path.exists(dist_dir):
235
+ files = os.listdir(dist_dir)
236
+ print_info(f"Files in dist directory: {files}")
237
+ return None
238
+ else:
239
+ self.errors.append(f"PyInstaller build failed with return code: {result.returncode}")
240
+ print_error(f"PyInstaller build failed with return code: {result.returncode}")
241
+ print_error("Check the output above for error details")
242
+ return None
243
+
244
+ except subprocess.TimeoutExpired:
245
+ self.errors.append("PyInstaller build timed out (5 minutes)")
246
+ print_error("PyInstaller build timed out")
247
+ return None
248
+ except Exception as e:
249
+ self.errors.append(f"PyInstaller build error: {str(e)}")
250
+ print_error(f"Build error: {str(e)}")
251
+ return None
252
+
253
+ def _try_upx_compression(self, executable_path):
254
+ """Try to compress executable with UPX if available"""
255
+ try:
256
+ # Check if UPX is available
257
+ result = subprocess.run(["upx", "--version"], capture_output=True, text=True, timeout=5)
258
+ if result.returncode == 0:
259
+ print_info("UPX found, attempting compression...")
260
+ original_size = os.path.getsize(executable_path)
261
+
262
+ # Compress with UPX (--best for maximum compression)
263
+ upx_result = subprocess.run(["upx", "--best", "--lzma", executable_path],
264
+ capture_output=True, text=True, timeout=60)
265
+
266
+ if upx_result.returncode == 0:
267
+ new_size = os.path.getsize(executable_path)
268
+ reduction = ((original_size - new_size) / original_size) * 100
269
+ print_success(f"UPX compression successful: {original_size:,} → {new_size:,} bytes ({reduction:.1f}% reduction)")
270
+ else:
271
+ print_warning(f"UPX compression failed: {upx_result.stderr}")
272
+ else:
273
+ print_info("UPX not available (optional compression tool)")
274
+ except (subprocess.TimeoutExpired, FileNotFoundError):
275
+ print_info("UPX not found (optional compression tool)")
276
+ except Exception as e:
277
+ print_warning(f"UPX compression error: {e}")
278
+
279
+ def validate_executable(self, executable_path):
280
+ """Validate the built executable"""
281
+ if not executable_path or not os.path.exists(executable_path):
282
+ return False
283
+
284
+ print_header("Validating Executable")
285
+
286
+ # Check file size
287
+ file_size = os.path.getsize(executable_path)
288
+ min_size = 1024 * 1024 # 1 MB
289
+ max_size = 500 * 1024 * 1024 # 500 MB (increased for development testing)
290
+
291
+ if file_size < min_size:
292
+ self.errors.append(f"Executable too small: {file_size} bytes (minimum: {min_size})")
293
+ print_error(f"File too small: {file_size} bytes")
294
+ elif file_size > max_size:
295
+ self.errors.append(f"Executable too large: {file_size} bytes (maximum: {max_size})")
296
+ print_error(f"File too large: {file_size} bytes")
297
+ else:
298
+ print_success(f"File size OK: {file_size:,} bytes ({file_size/1024/1024:.1f} MB)")
299
+
300
+ # Check file permissions (Unix systems)
301
+ if platform.system() != "Windows":
302
+ if os.access(executable_path, os.X_OK):
303
+ print_success("Executable permissions OK")
304
+ else:
305
+ print_warning("Executable not marked as executable")
306
+ # Try to fix permissions
307
+ try:
308
+ os.chmod(executable_path, 0o755)
309
+ print_info("Fixed executable permissions")
310
+ except Exception as e:
311
+ self.warnings.append(f"Could not fix permissions: {e}")
312
+
313
+ # Basic smoke test
314
+ try:
315
+ print_info("Running smoke test...")
316
+ # Try to run with --help flag and timeout quickly
317
+ result = subprocess.run([executable_path, "--help"],
318
+ capture_output=True, text=True, timeout=10)
319
+ print_success("Smoke test completed (executable can start)")
320
+ if result.stdout:
321
+ print_info(f"Output preview: {result.stdout[:100]}...")
322
+ except subprocess.TimeoutExpired:
323
+ print_success("Smoke test completed (executable started, timed out as expected)")
324
+ except Exception as e:
325
+ self.warnings.append(f"Smoke test failed: {e}")
326
+ print_warning(f"Smoke test failed: {e}")
327
+
328
+ return True
329
+
330
+ def test_checksum_generation(self, executable_path):
331
+ """Test SHA256 checksum generation"""
332
+ if not executable_path or not os.path.exists(executable_path):
333
+ return None
334
+
335
+ print_header("Testing Checksum Generation")
336
+
337
+ try:
338
+ # Generate SHA256 checksum
339
+ sha256_hash = hashlib.sha256()
340
+ with open(executable_path, "rb") as f:
341
+ for chunk in iter(lambda: f.read(4096), b""):
342
+ sha256_hash.update(chunk)
343
+
344
+ checksum = sha256_hash.hexdigest()
345
+
346
+ # Validate checksum format
347
+ if len(checksum) == 64 and all(c in '0123456789abcdef' for c in checksum):
348
+ print_success(f"Checksum generated: {checksum}")
349
+
350
+ # Create checksum file
351
+ executable_name = os.path.basename(executable_path)
352
+ checksum_file = f"{executable_path}.sha256"
353
+
354
+ with open(checksum_file, 'w') as f:
355
+ f.write(f"{checksum} {executable_name}\n")
356
+
357
+ if os.path.exists(checksum_file):
358
+ print_success(f"Checksum file created: {checksum_file}")
359
+ return checksum
360
+ else:
361
+ self.errors.append("Failed to create checksum file")
362
+ print_error("Failed to create checksum file")
363
+ else:
364
+ self.errors.append(f"Invalid checksum format: {checksum}")
365
+ print_error(f"Invalid checksum format: {checksum}")
366
+
367
+ except Exception as e:
368
+ self.errors.append(f"Checksum generation failed: {e}")
369
+ print_error(f"Checksum generation failed: {e}")
370
+
371
+ return None
372
+
373
+ def cleanup(self):
374
+ """Clean up temporary files"""
375
+ if self.temp_dir and os.path.exists(self.temp_dir):
376
+ try:
377
+ shutil.rmtree(self.temp_dir)
378
+ print_info(f"Cleaned up temporary directory: {self.temp_dir}")
379
+ except Exception as e:
380
+ print_warning(f"Could not clean up temporary directory: {e}")
381
+
382
+ def run_validation(self):
383
+ """Run complete validation process"""
384
+ print_header(f"Release Workflow Validation - {self.platform_name}")
385
+ print_info(f"Test tag: {self.test_tag}")
386
+ print_info(f"Platform: {self.platform_name}")
387
+
388
+ try:
389
+ # Run validation steps
390
+ self.validate_prerequisites()
391
+ self.validate_tag_format()
392
+
393
+ if self.errors:
394
+ print_error("Prerequisites failed - cannot continue")
395
+ return False
396
+
397
+ executable_path = self.test_pyinstaller_build()
398
+
399
+ if executable_path:
400
+ self.validate_executable(executable_path)
401
+ self.test_checksum_generation(executable_path)
402
+
403
+ # Print summary
404
+ self.print_summary()
405
+
406
+ return len(self.errors) == 0
407
+
408
+ finally:
409
+ self.cleanup()
410
+
411
+ def print_summary(self):
412
+ """Print validation summary"""
413
+ print_header("Validation Summary")
414
+
415
+ if self.errors:
416
+ print_error(f"Found {len(self.errors)} error(s):")
417
+ for error in self.errors:
418
+ print(f" • {error}")
419
+
420
+ if self.warnings:
421
+ print_warning(f"Found {len(self.warnings)} warning(s):")
422
+ for warning in self.warnings:
423
+ print(f" • {warning}")
424
+
425
+ if not self.errors and not self.warnings:
426
+ print_success("All validations passed! ✨")
427
+ print_info("The release workflow should work correctly on this platform.")
428
+ elif not self.errors:
429
+ print_success("Validation passed with warnings")
430
+ print_info("The release workflow should work, but review the warnings above.")
431
+ else:
432
+ print_error("Validation failed")
433
+ print_info("Fix the errors above before running the release workflow.")
434
+
435
+
436
+ def main():
437
+ parser = argparse.ArgumentParser(description="Validate release workflow components locally")
438
+ parser.add_argument("--test-tag", default="v0.0.1-test",
439
+ help="Test tag to use for validation (default: v0.0.1-test)")
440
+
441
+ args = parser.parse_args()
442
+
443
+ validator = ReleaseWorkflowValidator(args.test_tag)
444
+ success = validator.run_validation()
445
+
446
+ sys.exit(0 if success else 1)
447
+
448
+
449
+ if __name__ == "__main__":
450
+ main()