IncludeCPP 4.5.2__py3-none-any.whl → 4.9.3__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.
Files changed (35) hide show
  1. includecpp/CHANGELOG.md +241 -0
  2. includecpp/__init__.py +89 -3
  3. includecpp/__init__.pyi +2 -1
  4. includecpp/cli/commands.py +1747 -266
  5. includecpp/cli/config_parser.py +1 -1
  6. includecpp/core/build_manager.py +64 -13
  7. includecpp/core/cpp_api_extensions.pyi +43 -270
  8. includecpp/core/cssl/CSSL_DOCUMENTATION.md +1799 -1445
  9. includecpp/core/cssl/cpp/build/api.pyd +0 -0
  10. includecpp/core/cssl/cpp/build/api.pyi +274 -0
  11. includecpp/core/cssl/cpp/build/cssl_core.pyi +0 -99
  12. includecpp/core/cssl/cpp/cssl_core.cp +2 -23
  13. includecpp/core/cssl/cssl_builtins.py +2116 -171
  14. includecpp/core/cssl/cssl_builtins.pyi +1324 -104
  15. includecpp/core/cssl/cssl_compiler.py +4 -1
  16. includecpp/core/cssl/cssl_modules.py +605 -6
  17. includecpp/core/cssl/cssl_optimizer.py +12 -1
  18. includecpp/core/cssl/cssl_parser.py +1048 -52
  19. includecpp/core/cssl/cssl_runtime.py +2041 -131
  20. includecpp/core/cssl/cssl_syntax.py +405 -277
  21. includecpp/core/cssl/cssl_types.py +5891 -1655
  22. includecpp/core/cssl_bridge.py +429 -3
  23. includecpp/core/error_catalog.py +54 -10
  24. includecpp/core/homeserver.py +1037 -0
  25. includecpp/generator/parser.cpp +203 -39
  26. includecpp/generator/parser.h +15 -1
  27. includecpp/templates/cpp.proj.template +1 -1
  28. includecpp/vscode/cssl/snippets/cssl.snippets.json +163 -0
  29. includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +87 -12
  30. {includecpp-4.5.2.dist-info → includecpp-4.9.3.dist-info}/METADATA +81 -10
  31. {includecpp-4.5.2.dist-info → includecpp-4.9.3.dist-info}/RECORD +35 -33
  32. {includecpp-4.5.2.dist-info → includecpp-4.9.3.dist-info}/WHEEL +1 -1
  33. {includecpp-4.5.2.dist-info → includecpp-4.9.3.dist-info}/entry_points.txt +0 -0
  34. {includecpp-4.5.2.dist-info → includecpp-4.9.3.dist-info}/licenses/LICENSE +0 -0
  35. {includecpp-4.5.2.dist-info → includecpp-4.9.3.dist-info}/top_level.txt +0 -0
@@ -66,10 +66,13 @@ def _supports_unicode():
66
66
  """Check if terminal supports Unicode output."""
67
67
  if sys.platform == 'win32':
68
68
  try:
69
+ # Handle frozen PyInstaller where stdout may be None
70
+ if sys.stdout is None:
71
+ return False
69
72
  # Test if we can encode Unicode box characters
70
73
  '╔═╗'.encode(sys.stdout.encoding or 'utf-8')
71
74
  return True
72
- except (UnicodeEncodeError, LookupError):
75
+ except (UnicodeEncodeError, LookupError, AttributeError):
73
76
  return False
74
77
  return True
75
78
 
@@ -430,11 +433,590 @@ def _show_doc(search_term: str = None):
430
433
  _safe_echo("=" * 70)
431
434
 
432
435
 
436
+ def _show_pypi_stats():
437
+ """Show detailed PyPI download statistics with charts."""
438
+ from datetime import datetime, timedelta
439
+ import json
440
+ try:
441
+ import pypistats
442
+ except ImportError:
443
+ click.secho("Installing pypistats...", fg='yellow')
444
+ import subprocess
445
+ subprocess.run([sys.executable, '-m', 'pip', 'install', 'pypistats', '-q'])
446
+ import pypistats
447
+
448
+ package = 'includecpp'
449
+
450
+ # Header
451
+ click.echo()
452
+ click.secho("=" * 70, fg='cyan', bold=True)
453
+ click.secho(" INCLUDECPP - PyPI Download Statistics (CONFIDENTIAL)", fg='cyan', bold=True)
454
+ click.secho("=" * 70, fg='cyan', bold=True)
455
+ click.echo()
456
+
457
+ def draw_bar(value, max_value, width=40, fill_char='#', empty_char='.'):
458
+ """Draw a horizontal bar chart using ASCII."""
459
+ if max_value == 0:
460
+ return empty_char * width
461
+ filled = int((value / max_value) * width)
462
+ return fill_char * filled + empty_char * (width - filled)
463
+
464
+ def format_number(n):
465
+ """Format number with K/M suffix."""
466
+ if n >= 1_000_000:
467
+ return f"{n/1_000_000:.1f}M"
468
+ elif n >= 1_000:
469
+ return f"{n/1_000:.1f}K"
470
+ return str(n)
471
+
472
+ def section_header(title, color='green'):
473
+ """Print section header using ASCII."""
474
+ click.secho("+" + "-" * 68 + "+", fg=color)
475
+ click.secho(f"| {title:<65}|", fg=color, bold=True)
476
+ click.secho("+" + "-" * 68 + "+", fg=color)
477
+
478
+ try:
479
+ # === ALL TIME DOWNLOADS ===
480
+ section_header("ALL TIME DOWNLOADS (since first release)", 'green')
481
+
482
+ try:
483
+ overall = pypistats.overall(package, format='json')
484
+ overall_data = json.loads(overall)
485
+ total_with_mirrors = 0
486
+ total_without_mirrors = 0
487
+ for row in overall_data.get('data', []):
488
+ if row.get('category') == 'with_mirrors':
489
+ total_with_mirrors = row.get('downloads', 0)
490
+ elif row.get('category') == 'without_mirrors':
491
+ total_without_mirrors = row.get('downloads', 0)
492
+
493
+ click.echo()
494
+ click.echo(f" {'All downloads (incl. mirrors):':<35} {click.style(format_number(total_with_mirrors), fg='bright_white', bold=True):>10}")
495
+ click.echo(f" {'Excl. known mirrors:':<35} {click.style(format_number(total_without_mirrors), fg='bright_white', bold=True):>10}")
496
+ click.echo()
497
+ click.secho(" Note: 'Excl. mirrors' still includes CI/CD, Docker, reinstalls.", fg='bright_black')
498
+ click.secho(" Actual unique users is likely 5-20% of this number.", fg='bright_black')
499
+ click.echo()
500
+ except Exception as e:
501
+ click.secho(f" Could not fetch overall stats: {e}", fg='yellow')
502
+
503
+ # === RECENT DOWNLOADS (Last 30 days breakdown) ===
504
+ section_header("RECENT DOWNLOADS (Last 30 Days)", 'blue')
505
+
506
+ try:
507
+ recent = pypistats.recent(package, format='json')
508
+ recent_data = json.loads(recent) if isinstance(recent, str) else recent
509
+ # Handle both dict format {'data': [...]} and list format [...]
510
+ if isinstance(recent_data, dict):
511
+ rows = recent_data.get('data', [])
512
+ elif isinstance(recent_data, list):
513
+ rows = recent_data
514
+ else:
515
+ rows = []
516
+
517
+ click.echo()
518
+ # Find max for bar scaling
519
+ max_downloads = max((r.get('downloads', 0) for r in rows if isinstance(r, dict)), default=1)
520
+ for row in rows:
521
+ if not isinstance(row, dict):
522
+ continue
523
+ period = row.get('category', 'unknown')
524
+ downloads = row.get('downloads', 0)
525
+ period_display = {'last_day': 'Last Day', 'last_week': 'Last Week', 'last_month': 'Last Month'}.get(period, period)
526
+ bar = draw_bar(downloads, max_downloads, width=30)
527
+ click.echo(f" {period_display:<15} {click.style(bar, fg='green')} {click.style(format_number(downloads), fg='bright_white', bold=True):>10}")
528
+ click.echo()
529
+ except Exception as e:
530
+ click.secho(f" Could not fetch recent stats: {e}", fg='yellow')
531
+
532
+ # === PYTHON VERSION BREAKDOWN (Last 30 days with classification) ===
533
+ section_header("PYTHON VERSION (Last 30 Days - classified only)", 'magenta')
534
+
535
+ try:
536
+ python_major = pypistats.python_major(package, format='json')
537
+ major_data = json.loads(python_major)
538
+ click.echo()
539
+ versions = [(row.get('category'), row.get('downloads', 0)) for row in major_data.get('data', []) if row.get('category') != 'null']
540
+ versions.sort(key=lambda x: x[1], reverse=True)
541
+ max_dl = versions[0][1] if versions else 1
542
+ for version, downloads in versions[:5]:
543
+ bar = draw_bar(downloads, max_dl, width=35, fill_char='=', empty_char='.')
544
+ pct = (downloads / sum(d for _, d in versions)) * 100 if versions else 0
545
+ click.echo(f" Python {version:<6} {click.style(bar, fg='magenta')} {format_number(downloads):>8} ({pct:>5.1f}%)")
546
+ click.echo()
547
+ except Exception as e:
548
+ click.secho(f" Could not fetch Python version stats: {e}", fg='yellow')
549
+
550
+ # === PYTHON MINOR VERSION (3.10, 3.11, 3.12, etc.) ===
551
+ section_header("PYTHON MINOR VERSION (Last 30 Days - classified only)", 'yellow')
552
+
553
+ try:
554
+ python_minor = pypistats.python_minor(package, format='json')
555
+ minor_data = json.loads(python_minor)
556
+ click.echo()
557
+ versions = [(row.get('category'), row.get('downloads', 0)) for row in minor_data.get('data', []) if row.get('category') and row.get('category') != 'null']
558
+ versions.sort(key=lambda x: x[1], reverse=True)
559
+ max_dl = versions[0][1] if versions else 1
560
+ for version, downloads in versions[:8]:
561
+ bar = draw_bar(downloads, max_dl, width=30, fill_char='#', empty_char='.')
562
+ pct = (downloads / sum(d for _, d in versions)) * 100 if versions else 0
563
+ click.echo(f" {version:<8} {click.style(bar, fg='yellow')} {format_number(downloads):>8} ({pct:>5.1f}%)")
564
+ click.echo()
565
+ except Exception as e:
566
+ click.secho(f" Could not fetch Python minor version stats: {e}", fg='yellow')
567
+
568
+ # === SYSTEM/OS BREAKDOWN ===
569
+ section_header("OPERATING SYSTEM (Last 30 Days - classified only)", 'red')
570
+
571
+ try:
572
+ system_stats = pypistats.system(package, format='json')
573
+ system_data = json.loads(system_stats)
574
+ click.echo()
575
+ systems = [(row.get('category'), row.get('downloads', 0)) for row in system_data.get('data', []) if row.get('category') and row.get('category') != 'null']
576
+ systems.sort(key=lambda x: x[1], reverse=True)
577
+ max_dl = systems[0][1] if systems else 1
578
+ os_labels = {'Windows': '[Win]', 'Linux': '[Lin]', 'Darwin': '[Mac]', 'null': '[?]'}
579
+ for system, downloads in systems[:5]:
580
+ bar = draw_bar(downloads, max_dl, width=35, fill_char='=', empty_char='.')
581
+ pct = (downloads / sum(d for _, d in systems)) * 100 if systems else 0
582
+ label = os_labels.get(system, ' ')
583
+ click.echo(f" {label} {system:<10} {click.style(bar, fg='red')} {format_number(downloads):>8} ({pct:>5.1f}%)")
584
+ click.echo()
585
+ except Exception as e:
586
+ click.secho(f" Could not fetch system stats: {e}", fg='yellow')
587
+
588
+ # === DAILY DOWNLOADS TREND (Last 14 days) ===
589
+ section_header("DAILY DOWNLOADS TREND (Last 14 Days)", 'cyan')
590
+
591
+ try:
592
+ # Fetch daily data directly from pypistats.org API
593
+ import urllib.request
594
+ end_date = datetime.now().strftime('%Y-%m-%d')
595
+ start_date = (datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d')
596
+ api_url = f"https://pypistats.org/api/packages/{package}/overall?start_date={start_date}&end_date={end_date}"
597
+
598
+ req = urllib.request.Request(api_url, headers={'User-Agent': 'IncludeCPP-CLI'})
599
+ with urllib.request.urlopen(req, timeout=10) as response:
600
+ daily_data = json.loads(response.read().decode())
601
+
602
+ click.echo()
603
+ days = []
604
+ for row in daily_data.get('data', []):
605
+ date_val = row.get('date')
606
+ if date_val and row.get('category') == 'without_mirrors':
607
+ days.append((date_val, row.get('downloads', 0)))
608
+
609
+ if days:
610
+ days.sort(key=lambda x: x[0])
611
+ max_dl = max(d for _, d in days) if days else 1
612
+
613
+ # Draw sparkline-style chart
614
+ click.echo(" Date Downloads")
615
+ click.echo(" " + "-" * 50)
616
+ for date, downloads in days[-14:]:
617
+ bar = draw_bar(downloads, max_dl, width=30, fill_char='=', empty_char=' ')
618
+ date_short = date[5:] # MM-DD format
619
+ click.echo(f" {date_short} {click.style(bar, fg='cyan')} {format_number(downloads):>6}")
620
+
621
+ click.echo()
622
+ # Summary stats
623
+ total = sum(d for _, d in days)
624
+ avg = total / len(days) if days else 0
625
+ peak = max(d for _, d in days)
626
+ peak_date = [date for date, d in days if d == peak][0] if days else 'N/A'
627
+
628
+ click.echo(f" {'14-Day Total:':<20} {click.style(format_number(total), fg='bright_white', bold=True)}")
629
+ click.echo(f" {'Daily Average:':<20} {click.style(format_number(int(avg)), fg='bright_white')}")
630
+ click.echo(f" {'Peak Day:':<20} {click.style(f'{format_number(peak)} ({peak_date})', fg='green', bold=True)}")
631
+ else:
632
+ click.secho(" No daily data available yet (data updates daily)", fg='yellow')
633
+ click.echo()
634
+ except Exception as e:
635
+ click.secho(f" Could not fetch daily trend: {e}", fg='yellow')
636
+
637
+ # === REALISTIC ESTIMATE ===
638
+ section_header("REALISTIC USER ESTIMATE", 'bright_black')
639
+ click.echo()
640
+ click.secho(" PyPI download counts are NOT unique users. They include:", fg='bright_black')
641
+ click.secho(" * CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins)", fg='bright_black')
642
+ click.secho(" * Docker container builds", fg='bright_black')
643
+ click.secho(" * Package reinstalls and cache misses", fg='bright_black')
644
+ click.secho(" * Multiple machines per user (laptop, PC, VM, WSL)", fg='bright_black')
645
+ click.secho(" * Corporate proxies (Artifactory, devpi)", fg='bright_black')
646
+ click.echo()
647
+ click.secho(" Industry estimate: Actual unique humans ~ 5-15% of downloads", fg='yellow')
648
+ try:
649
+ if total_without_mirrors:
650
+ low_est = int(total_without_mirrors * 0.05)
651
+ high_est = int(total_without_mirrors * 0.15)
652
+ click.echo(f" For {format_number(total_without_mirrors)} downloads: ~{format_number(low_est)} - {format_number(high_est)} unique users")
653
+ except:
654
+ pass
655
+ click.echo()
656
+
657
+ # === FOOTER ===
658
+ click.echo()
659
+ click.secho("=" * 70, fg='cyan', bold=True)
660
+ click.secho(f" Report generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", fg='bright_black')
661
+ click.secho(f" Package: includecpp | Source: PyPI Stats API (BigQuery)", fg='bright_black')
662
+ click.secho("=" * 70, fg='cyan', bold=True)
663
+ click.echo()
664
+
665
+ except Exception as e:
666
+ click.secho(f"Error fetching PyPI stats: {e}", fg='red')
667
+ import traceback
668
+ traceback.print_exc()
669
+
670
+
671
+ def _make_executable(script_path: str, output_name: str = None, onefile: bool = True, console: bool = True, icon: str = None):
672
+ """Build .exe from Python script using PyInstaller.
673
+
674
+ Auto-detects dependencies, rebuilds includecpp if needed, and cleans up.
675
+
676
+ Args:
677
+ script_path: Path to the Python script
678
+ output_name: Optional custom name for output executable (without .exe)
679
+ onefile: Build as single file (True) or directory (False)
680
+ console: Show console window (True) or windowed mode (False)
681
+ icon: Path to icon file (.ico)
682
+ """
683
+ import re
684
+ import shutil
685
+ import ast
686
+
687
+ script_path = Path(script_path).resolve()
688
+ if not script_path.exists():
689
+ click.secho(f"Error: File not found: {script_path}", fg='red')
690
+ return
691
+
692
+ if script_path.suffix.lower() != '.py':
693
+ click.secho(f"Error: Not a Python file: {script_path}", fg='red')
694
+ return
695
+
696
+ script_dir = script_path.parent
697
+ script_name = output_name if output_name else script_path.stem
698
+
699
+ click.echo()
700
+ click.secho(f"Building executable from: {script_path.name}", fg='cyan', bold=True)
701
+ if output_name:
702
+ click.echo(f" Output name: {output_name}.exe")
703
+ click.echo("=" * 50)
704
+
705
+ # Check if PyInstaller is installed
706
+ try:
707
+ import PyInstaller
708
+ click.echo(f" PyInstaller: v{PyInstaller.__version__}")
709
+ except ImportError:
710
+ click.secho("Error: PyInstaller is not installed.", fg='red')
711
+ click.echo("Install it with: pip install pyinstaller")
712
+ return
713
+
714
+ # Read and analyze the script
715
+ try:
716
+ with open(script_path, 'r', encoding='utf-8') as f:
717
+ script_content = f.read()
718
+ except Exception as e:
719
+ click.secho(f"Error reading script: {e}", fg='red')
720
+ return
721
+
722
+ # Detect imports
723
+ detected_imports = set()
724
+ uses_includecpp = False
725
+ uses_pyqt6 = False
726
+ uses_pyqt5 = False
727
+ uses_tkinter = False
728
+ uses_pyside6 = False
729
+ uses_pyside2 = False
730
+
731
+ # Simple regex-based import detection
732
+ import_patterns = [
733
+ r'^import\s+(\w+)',
734
+ r'^from\s+(\w+)',
735
+ ]
736
+
737
+ for line in script_content.split('\n'):
738
+ line = line.strip()
739
+ for pattern in import_patterns:
740
+ match = re.match(pattern, line)
741
+ if match:
742
+ module = match.group(1)
743
+ detected_imports.add(module)
744
+ if module == 'includecpp':
745
+ uses_includecpp = True
746
+ elif module == 'PyQt6':
747
+ uses_pyqt6 = True
748
+ elif module == 'PyQt5':
749
+ uses_pyqt5 = True
750
+ elif module == 'PySide6':
751
+ uses_pyside6 = True
752
+ elif module == 'PySide2':
753
+ uses_pyside2 = True
754
+ elif module == 'tkinter':
755
+ uses_tkinter = True
756
+
757
+ click.echo(f" Detected imports: {len(detected_imports)}")
758
+
759
+ # Build data files list for PyInstaller
760
+ datas = []
761
+ binaries = [] # For .pyd/.so files
762
+ hidden_imports = []
763
+
764
+ # If includecpp is used, rebuild and include the compiled modules
765
+ includecpp_files_to_copy = [] # Initialize here for later use
766
+
767
+ if uses_includecpp:
768
+ click.echo()
769
+ click.secho(" IncludeCPP detected - checking for modules...", fg='yellow')
770
+
771
+ # Look for cpp.proj in script directory or parent directories
772
+ cpp_proj_path = None
773
+ search_dir = script_dir
774
+ for _ in range(5): # Search up to 5 levels up
775
+ candidate = search_dir / 'cpp.proj'
776
+ if candidate.exists():
777
+ cpp_proj_path = candidate
778
+ break
779
+ if search_dir.parent == search_dir:
780
+ break
781
+ search_dir = search_dir.parent
782
+
783
+ if cpp_proj_path:
784
+ click.echo(f" Found project: {cpp_proj_path}")
785
+
786
+ # Load project config to find build directory
787
+ try:
788
+ import json
789
+ with open(cpp_proj_path, 'r') as f:
790
+ proj_config = json.load(f)
791
+
792
+ proj_dir = cpp_proj_path.parent
793
+
794
+ # Rebuild the modules
795
+ click.echo(" Rebuilding C++ modules...")
796
+ try:
797
+ from ..core.build_manager import BuildManager
798
+ from .config_parser import CppProjectConfig
799
+ # detect_compiler is defined in this file
800
+
801
+ config = CppProjectConfig(config_path=cpp_proj_path)
802
+ project_root = cpp_proj_path.parent
803
+ compiler = detect_compiler()
804
+ build_dir_path = config.get_build_dir(compiler)
805
+ build_dir_path.mkdir(parents=True, exist_ok=True)
806
+
807
+ builder = BuildManager(project_root, build_dir_path, config)
808
+ success = builder.rebuild(incremental=True)
809
+
810
+ if success:
811
+ click.secho(" Rebuild complete!", fg='green')
812
+ else:
813
+ click.secho(" Warning: Rebuild had errors", fg='yellow')
814
+ except Exception as e:
815
+ click.secho(f" Warning: Rebuild failed: {e}", fg='yellow')
816
+
817
+ # Use the build directory from the project config (already computed above)
818
+ build_dir = build_dir_path
819
+
820
+ # Store paths for copying AFTER PyInstaller completes
821
+ # (Don't bundle into exe - create external includecpp.dll instead)
822
+ if build_dir.exists():
823
+ ext = '.pyd' if sys.platform == 'win32' else '.so'
824
+ bindings_dir = build_dir / 'bindings'
825
+
826
+ # Find api.pyd (or api.so)
827
+ pyd_files = list(bindings_dir.glob(f'*{ext}')) if bindings_dir.exists() else []
828
+ if not pyd_files:
829
+ pyd_files = list(build_dir.glob(f'**/*{ext}'))
830
+
831
+ if pyd_files:
832
+ # Store the main api.pyd path for creating includecpp.dll
833
+ api_pyd = pyd_files[0] # Usually api.pyd
834
+ includecpp_files_to_copy.append(('api', api_pyd))
835
+ click.echo(f" Found: {api_pyd.name} (will create includecpp.dll)")
836
+
837
+ # Find runtime DLLs
838
+ if bindings_dir.exists():
839
+ dll_files = list(bindings_dir.glob('*.dll'))
840
+ for dll_file in dll_files:
841
+ includecpp_files_to_copy.append(('dll', dll_file))
842
+ click.echo(f" Found: {dll_file.name}")
843
+
844
+ if pyd_files:
845
+ click.secho(f" Will copy {len(includecpp_files_to_copy)} file(s) to output directory", fg='green')
846
+ else:
847
+ click.secho(" Warning: No compiled modules found in build directory", fg='yellow')
848
+ else:
849
+ click.secho(" Warning: Build directory not found", fg='yellow')
850
+
851
+ except Exception as e:
852
+ click.secho(f" Warning: Could not process cpp.proj: {e}", fg='yellow')
853
+ else:
854
+ click.secho(" Warning: cpp.proj not found. C++ modules may not be included.", fg='yellow')
855
+
856
+ hidden_imports.append('includecpp')
857
+ hidden_imports.append('api') # The compiled module
858
+
859
+ # Add GUI-specific settings
860
+ if uses_pyqt6:
861
+ click.echo(" PyQt6 detected")
862
+ hidden_imports.extend(['PyQt6', 'PyQt6.QtCore', 'PyQt6.QtGui', 'PyQt6.QtWidgets'])
863
+ if uses_pyqt5:
864
+ click.echo(" PyQt5 detected")
865
+ hidden_imports.extend(['PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets'])
866
+ if uses_pyside6:
867
+ click.echo(" PySide6 detected")
868
+ hidden_imports.extend(['PySide6', 'PySide6.QtCore', 'PySide6.QtGui', 'PySide6.QtWidgets'])
869
+ if uses_pyside2:
870
+ click.echo(" PySide2 detected")
871
+ hidden_imports.extend(['PySide2', 'PySide2.QtCore', 'PySide2.QtGui', 'PySide2.QtWidgets'])
872
+
873
+ # Build PyInstaller command
874
+ click.echo()
875
+ click.secho("Building executable...", fg='cyan')
876
+
877
+ cmd = [
878
+ sys.executable, '-m', 'PyInstaller',
879
+ '--noconfirm',
880
+ '--clean',
881
+ ]
882
+
883
+ if onefile:
884
+ cmd.append('--onefile')
885
+ else:
886
+ cmd.append('--onedir')
887
+
888
+ if console:
889
+ cmd.append('--console')
890
+ else:
891
+ cmd.append('--windowed')
892
+
893
+ if icon:
894
+ cmd.extend(['--icon', str(Path(icon).resolve())])
895
+
896
+ # Set output name if specified
897
+ if output_name:
898
+ cmd.extend(['--name', output_name])
899
+
900
+ # Add data files
901
+ for src, dest in datas:
902
+ cmd.extend(['--add-data', f'{src}{os.pathsep}{dest}'])
903
+
904
+ # Add binary files (.pyd/.so)
905
+ for src, dest in binaries:
906
+ cmd.extend(['--add-binary', f'{src}{os.pathsep}{dest}'])
907
+
908
+ # Add hidden imports
909
+ for imp in hidden_imports:
910
+ cmd.extend(['--hidden-import', imp])
911
+
912
+ # Set output directory to script directory
913
+ cmd.extend(['--distpath', str(script_dir)])
914
+ cmd.extend(['--workpath', str(script_dir / 'build')])
915
+ cmd.extend(['--specpath', str(script_dir)])
916
+
917
+ # Add the script
918
+ cmd.append(str(script_path))
919
+
920
+ # Run PyInstaller
921
+ try:
922
+ result = subprocess.run(cmd, cwd=str(script_dir), capture_output=True, text=True)
923
+
924
+ if result.returncode != 0:
925
+ click.secho("PyInstaller failed!", fg='red')
926
+ if result.stderr:
927
+ click.echo(result.stderr[-2000:]) # Show last 2000 chars of error
928
+ return
929
+
930
+ click.secho("PyInstaller completed!", fg='green')
931
+
932
+ except Exception as e:
933
+ click.secho(f"Error running PyInstaller: {e}", fg='red')
934
+ return
935
+
936
+ # Copy includecpp.dll and runtime DLLs to output directory
937
+ if uses_includecpp and includecpp_files_to_copy:
938
+ click.echo()
939
+ click.echo("Creating includecpp.dll...")
940
+ try:
941
+ for file_type, file_path in includecpp_files_to_copy:
942
+ if file_type == 'api':
943
+ # Copy api.pyd as includecpp.dll
944
+ dll_output = script_dir / 'includecpp.dll'
945
+ shutil.copy2(file_path, dll_output)
946
+ click.secho(" Created: includecpp.dll", fg='green')
947
+ else:
948
+ # Copy runtime DLLs
949
+ dest = script_dir / file_path.name
950
+ shutil.copy2(file_path, dest)
951
+ click.echo(f" Copied: {file_path.name}")
952
+ except Exception as e:
953
+ click.secho(f" Warning: Could not copy files: {e}", fg='yellow')
954
+
955
+ # Clean up
956
+ click.echo()
957
+ click.echo("Cleaning up...")
958
+
959
+ # Remove build directory
960
+ build_path = script_dir / 'build'
961
+ if build_path.exists():
962
+ try:
963
+ shutil.rmtree(build_path)
964
+ click.echo(" Removed build/")
965
+ except Exception as e:
966
+ click.secho(f" Warning: Could not remove build/: {e}", fg='yellow')
967
+
968
+ # Remove .spec file
969
+ spec_path = script_dir / f'{script_name}.spec'
970
+ if spec_path.exists():
971
+ try:
972
+ spec_path.unlink()
973
+ click.echo(" Removed .spec file")
974
+ except Exception as e:
975
+ click.secho(f" Warning: Could not remove .spec: {e}", fg='yellow')
976
+
977
+ # Remove __pycache__ if created
978
+ pycache = script_dir / '__pycache__'
979
+ if pycache.exists():
980
+ try:
981
+ shutil.rmtree(pycache)
982
+ click.echo(" Removed __pycache__/")
983
+ except:
984
+ pass
985
+
986
+ # Check for output
987
+ if onefile:
988
+ exe_name = f'{script_name}.exe' if sys.platform == 'win32' else script_name
989
+ exe_path = script_dir / exe_name
990
+ else:
991
+ exe_name = script_name
992
+ exe_path = script_dir / exe_name
993
+
994
+ click.echo()
995
+ if exe_path.exists():
996
+ if exe_path.is_file():
997
+ size_mb = exe_path.stat().st_size / (1024 * 1024)
998
+ click.secho(f"Success! Created: {exe_path.name} ({size_mb:.1f} MB)", fg='green', bold=True)
999
+ else:
1000
+ click.secho(f"Success! Created: {exe_path.name}/ (directory)", fg='green', bold=True)
1001
+ else:
1002
+ click.secho("Warning: Executable not found at expected location", fg='yellow')
1003
+ # Try to find it
1004
+ if sys.platform == 'win32':
1005
+ found = list(script_dir.glob('*.exe'))
1006
+ if found:
1007
+ click.echo(f"Found: {found[0].name}")
1008
+
1009
+
433
1010
  @click.group(invoke_without_command=True)
434
1011
  @click.option('--doc', 'doc_search', default=None, help='Show documentation. Use --doc or --doc "term" to search.')
435
1012
  @click.option('-d', 'doc_flag', is_flag=True, help='Show full documentation (shorthand)')
436
1013
  @click.option('--changelog', 'show_changelog', is_flag=True, help='Show changelog (last 2 releases by default)')
437
1014
  @click.option('--all', 'changelog_all', is_flag=True, help='Show all changelog entries (use with --changelog)')
1015
+ @click.option('--make-exe', 'make_exe_path', type=str, help='Build .exe from script, or "gui" for wizard')
1016
+ @click.option('--exe-name', 'exe_name', type=str, help='Output executable name (without .exe)')
1017
+ @click.option('--onefile/--onedir', 'onefile', default=True, help='Build as single file or directory (default: onefile)')
1018
+ @click.option('--console/--windowed', 'console', default=True, help='Show console window or not (default: console)')
1019
+ @click.option('--icon', type=click.Path(exists=True), help='Icon file for executable (.ico)')
438
1020
  @click.option('--1', 'changelog_1', is_flag=True, hidden=True)
439
1021
  @click.option('--2', 'changelog_2', is_flag=True, hidden=True)
440
1022
  @click.option('--3', 'changelog_3', is_flag=True, hidden=True)
@@ -445,9 +1027,11 @@ def _show_doc(search_term: str = None):
445
1027
  @click.option('--8', 'changelog_8', is_flag=True, hidden=True)
446
1028
  @click.option('--9', 'changelog_9', is_flag=True, hidden=True)
447
1029
  @click.option('--10', 'changelog_10', is_flag=True, hidden=True)
1030
+ @click.option('--stats', 'stats_key', type=str, default=None, hidden=True)
448
1031
  @click.pass_context
449
- def cli(ctx, doc_search, doc_flag, show_changelog, changelog_all, changelog_1, changelog_2, changelog_3,
450
- changelog_4, changelog_5, changelog_6, changelog_7, changelog_8, changelog_9, changelog_10):
1032
+ def cli(ctx, doc_search, doc_flag, show_changelog, changelog_all, make_exe_path, exe_name, onefile, console, icon,
1033
+ changelog_1, changelog_2, changelog_3, changelog_4, changelog_5, changelog_6, changelog_7,
1034
+ changelog_8, changelog_9, changelog_10, stats_key):
451
1035
  """IncludeCPP - C++ Performance in Python, Zero Hassle
452
1036
 
453
1037
  \b
@@ -489,6 +1073,33 @@ def cli(ctx, doc_search, doc_flag, show_changelog, changelog_all, changelog_1, c
489
1073
  _show_doc(search_term)
490
1074
  ctx.exit(0)
491
1075
 
1076
+ # Handle --make-exe
1077
+ if make_exe_path:
1078
+ # Check if GUI mode requested
1079
+ if make_exe_path.lower() == 'gui':
1080
+ try:
1081
+ from ..gui.exe_wizard import run_wizard
1082
+ run_wizard()
1083
+ except ImportError as e:
1084
+ click.secho("PyQt6 required for GUI wizard. Install with: pip install PyQt6", fg='red')
1085
+ click.secho(f"Error: {e}", fg='bright_black')
1086
+ ctx.exit(0)
1087
+
1088
+ # Otherwise, build from path
1089
+ from pathlib import Path
1090
+ script_path = Path(make_exe_path)
1091
+ if not script_path.exists():
1092
+ click.secho(f"File not found: {make_exe_path}", fg='red')
1093
+ ctx.exit(1)
1094
+
1095
+ _make_executable(str(script_path), output_name=exe_name, onefile=onefile, console=console, icon=icon)
1096
+ ctx.exit(0)
1097
+
1098
+ # Handle hidden stats command
1099
+ if stats_key == 'secret':
1100
+ _show_pypi_stats()
1101
+ ctx.exit(0)
1102
+
492
1103
  # If no subcommand is given, show help
493
1104
  if ctx.invoked_subcommand is None:
494
1105
  click.echo(ctx.get_help())
@@ -497,11 +1108,12 @@ def _setup_global_command():
497
1108
  system = platform.system()
498
1109
 
499
1110
  if system == "Windows":
500
- bin_dir = Path(os.environ.get('LOCALAPPDATA', os.path.expanduser('~\\AppData\\Local'))) / 'IncludeCPP' / 'bin'
1111
+ bin_dir = Path(os.environ.get('LOCALAPPDATA', Path.home() / 'AppData' / 'Local')) / 'IncludeCPP' / 'bin'
501
1112
  bin_dir.mkdir(parents=True, exist_ok=True)
502
1113
 
503
1114
  script_path = bin_dir / 'includecpp.bat'
504
- with open(script_path, 'w', encoding='utf-8') as f:
1115
+ # Use \r\n for Windows batch files
1116
+ with open(script_path, 'w', encoding='utf-8', newline='\r\n') as f:
505
1117
  f.write('@echo off\n')
506
1118
  f.write(f'"{sys.executable}" -m includecpp %*\n')
507
1119
 
@@ -868,6 +1480,11 @@ def _check_for_updates_silent():
868
1480
  def _parse_cp_sources(cp_path):
869
1481
  """Parse SOURCE() and HEADER() paths from an existing .cp file.
870
1482
 
1483
+ Supports multiple formats:
1484
+ - Single SOURCE with multiple files: SOURCE(file1.cpp file2.cpp)
1485
+ - Multiple SOURCE declarations: SOURCE(file1.cpp) && SOURCE(file2.cpp)
1486
+ - Same for HEADER()
1487
+
871
1488
  Returns:
872
1489
  Tuple of (source_files, header_files) as lists of Path objects
873
1490
  """
@@ -881,11 +1498,10 @@ def _parse_cp_sources(cp_path):
881
1498
  content = cp_path.read_text()
882
1499
  project_root = cp_path.parent.parent # plugins/ -> project root
883
1500
 
884
- # Find SOURCE(...) declarations
885
- source_match = re.search(r'SOURCE\s*\(\s*([^)]+)\s*\)', content)
886
- if source_match:
887
- sources_str = source_match.group(1)
888
- # Handle multiple files: SOURCE(file1.cpp file2.cpp) or SOURCE(file1.cpp, file2.cpp)
1501
+ # Find ALL SOURCE(...) declarations (supports multiple SOURCE() && SOURCE())
1502
+ source_matches = re.findall(r'SOURCE\s*\(\s*([^)]+)\s*\)', content)
1503
+ for sources_str in source_matches:
1504
+ # Handle multiple files within one SOURCE(): SOURCE(file1.cpp file2.cpp) or SOURCE(file1.cpp, file2.cpp)
889
1505
  sources = re.split(r'[,\s]+', sources_str.strip())
890
1506
  for src in sources:
891
1507
  src = src.strip()
@@ -896,10 +1512,9 @@ def _parse_cp_sources(cp_path):
896
1512
  if p.exists():
897
1513
  source_files.append(p)
898
1514
 
899
- # Find HEADER(...) declarations
900
- header_match = re.search(r'HEADER\s*\(\s*([^)]+)\s*\)', content)
901
- if header_match:
902
- headers_str = header_match.group(1)
1515
+ # Find ALL HEADER(...) declarations
1516
+ header_matches = re.findall(r'HEADER\s*\(\s*([^)]+)\s*\)', content)
1517
+ for headers_str in header_matches:
903
1518
  headers = re.split(r'[,\s]+', headers_str.strip())
904
1519
  for hdr in headers:
905
1520
  hdr = hdr.strip()
@@ -943,10 +1558,16 @@ def rebuild(clean, keep, verbose, no_incremental, incremental, parallel, jobs, m
943
1558
  """Rebuild C++ modules with automatic generator updates."""
944
1559
  from ..core.build_manager import BuildManager
945
1560
  from ..core.error_formatter import BuildErrorFormatter, BuildSuccessFormatter
1561
+ from .. import __version__
946
1562
  import time
947
1563
  import json
948
1564
  from datetime import datetime
949
1565
 
1566
+ # Show version (X.X format)
1567
+ short_version = '.'.join(__version__.split('.')[:2])
1568
+ click.secho(f"[IncludeCPP v{short_version}] ", fg='cyan', nl=False)
1569
+ click.echo("Build starting...")
1570
+
950
1571
  # Combine -m modules with positional arguments
951
1572
  # Allows both: includecpp rebuild -m gamekit AND includecpp rebuild gamekit
952
1573
  all_modules = list(modules) + list(module_args)
@@ -1112,10 +1733,7 @@ def rebuild(clean, keep, verbose, no_incremental, incremental, parallel, jobs, m
1112
1733
  if fast_module_name.endswith('.cp'):
1113
1734
  fast_module_name = fast_module_name[:-3]
1114
1735
  # Strip any path prefix (plugins/math_utils -> math_utils)
1115
- if '/' in fast_module_name:
1116
- fast_module_name = fast_module_name.split('/')[-1]
1117
- if '\\' in fast_module_name:
1118
- fast_module_name = fast_module_name.split('\\')[-1]
1736
+ fast_module_name = Path(fast_module_name).name
1119
1737
 
1120
1738
  # Use the already-imported BuildManager (from line 115)
1121
1739
  temp_builder = BuildManager(project_root, build_dir, config)
@@ -1383,9 +2001,6 @@ def rebuild(clean, keep, verbose, no_incremental, incremental, parallel, jobs, m
1383
2001
  click.secho(f"AI analysis failed: {ai_response}", fg='yellow')
1384
2002
  click.echo(f"{'-'*60}")
1385
2003
 
1386
- final_msg = BuildErrorFormatter.get_final_message(error_msg, module_name)
1387
- click.echo("")
1388
- click.secho(final_msg, fg='red', bold=True, err=True)
1389
2004
  click.echo("")
1390
2005
  _safe_echo(_BOX_TOP, fg='red')
1391
2006
  _safe_echo(f"{_BOX_SIDE} {_CROSS} BUILD FAILED", fg='red')
@@ -2488,6 +3103,7 @@ def plugin(plugin_name, files, private):
2488
3103
  classes = {}
2489
3104
  functions = set()
2490
3105
  template_functions = {} # v3.1.6: {name: set of types}
3106
+ enums = {} # v4.6.5: {name: {'is_class': bool, 'values': list}}
2491
3107
  namespaces = set()
2492
3108
 
2493
3109
  def find_matching_brace(text, start_pos):
@@ -2898,15 +3514,27 @@ def plugin(plugin_name, files, private):
2898
3514
  if '&' not in actual_type:
2899
3515
  actual_type += '&'
2900
3516
 
2901
- # Remove array brackets if present
3517
+ # v4.6.6: Detect array fields and capture size
3518
+ array_size = 0
2902
3519
  if '[' in name:
2903
- name = name[:name.find('[')]
3520
+ # Extract array size: "magic[4]" -> size=4, name="magic"
3521
+ bracket_idx = name.find('[')
3522
+ end_bracket = name.find(']')
3523
+ if end_bracket > bracket_idx:
3524
+ size_str = name[bracket_idx+1:end_bracket].strip()
3525
+ try:
3526
+ array_size = int(size_str) if size_str else 0
3527
+ except ValueError:
3528
+ array_size = 0 # Unknown/variable size
3529
+ name = name[:bracket_idx].strip()
2904
3530
 
2905
3531
  # Validate field name
2906
3532
  if name and re.match(r'^[a-zA-Z_]\w*$', name):
2907
3533
  # Skip if name is a keyword
2908
3534
  if name.lower() not in cpp_keywords:
2909
- fields.append((actual_type, name))
3535
+ # v4.6.6: Include array info (type, name, array_size)
3536
+ # array_size=0 means not an array
3537
+ fields.append((actual_type, name, array_size))
2910
3538
 
2911
3539
  return fields
2912
3540
 
@@ -2946,6 +3574,11 @@ def plugin(plugin_name, files, private):
2946
3574
  is_struct = keyword == 'struct'
2947
3575
  brace_start = match.end() - 1
2948
3576
 
3577
+ # v4.6.5: Skip "enum class" declarations - check if preceded by "enum"
3578
+ text_before_match = content[max(0, match.start()-10):match.start()]
3579
+ if re.search(r'\benum\s*$', text_before_match):
3580
+ continue
3581
+
2949
3582
  text_before = content[:match.start()]
2950
3583
  open_braces = text_before.count('{')
2951
3584
  close_braces = text_before.count('}')
@@ -2987,8 +3620,11 @@ def plugin(plugin_name, files, private):
2987
3620
  existing_ctors.add(sig)
2988
3621
 
2989
3622
  # v3.2.2: Extract fields (including comma-separated declarations)
3623
+ # v4.6.6: Now returns (type, name, array_size) tuples
2990
3624
  field_list = extract_fields(public_section, class_name)
2991
- for field_type, field_name in field_list:
3625
+ for field_info in field_list:
3626
+ field_type, field_name = field_info[0], field_info[1]
3627
+ array_size = field_info[2] if len(field_info) > 2 else 0
2992
3628
  if (field_type, field_name) not in classes[class_name]['fields']:
2993
3629
  classes[class_name]['fields'].append((field_type, field_name))
2994
3630
 
@@ -3071,23 +3707,67 @@ def plugin(plugin_name, files, private):
3071
3707
  if func_name not in template_func_names:
3072
3708
  functions.add(func_name)
3073
3709
 
3710
+ # v4.6.5: Find enum declarations
3711
+ enum_pattern = re.compile(
3712
+ r'\benum\s+(class\s+)?(\w+)\s*\{([^}]*)\}',
3713
+ re.MULTILINE | re.DOTALL
3714
+ )
3715
+
3716
+ for match in enum_pattern.finditer(content):
3717
+ is_class_enum = bool(match.group(1))
3718
+ enum_name = match.group(2)
3719
+ enum_body = match.group(3)
3720
+
3721
+ # Parse enum values
3722
+ values = []
3723
+ # Split by comma and extract value names (ignoring assignments)
3724
+ for part in enum_body.split(','):
3725
+ part = part.strip()
3726
+ if not part:
3727
+ continue
3728
+ # Get the value name (before any = assignment)
3729
+ value_name = part.split('=')[0].strip()
3730
+ if value_name and re.match(r'^[a-zA-Z_]\w*$', value_name):
3731
+ values.append(value_name)
3732
+
3733
+ if enum_name not in enums and values:
3734
+ enums[enum_name] = {
3735
+ 'is_class': is_class_enum,
3736
+ 'values': values
3737
+ }
3738
+
3074
3739
  private_set = set(private) if private else set()
3075
3740
  public_functions = functions - private_set - set(classes.keys())
3076
3741
 
3077
3742
  cp_file = plugins_dir / f"{plugin_name}.cp"
3078
3743
 
3079
- cpp_sources = ' '.join(str(f.relative_to(project_root) if f.is_relative_to(project_root) else f).replace('\\', '/') for f in cpp_files)
3080
- h_headers = ' '.join(str(f.relative_to(project_root) if f.is_relative_to(project_root) else f).replace('\\', '/') for f in h_files) if h_files else None
3744
+ # Generate individual SOURCE() declarations for each file (cleaner format)
3745
+ cpp_paths = [str(f.relative_to(project_root) if f.is_relative_to(project_root) else f).replace('\\', '/') for f in cpp_files]
3746
+ h_paths = [str(f.relative_to(project_root) if f.is_relative_to(project_root) else f).replace('\\', '/') for f in h_files] if h_files else []
3081
3747
 
3082
3748
  with open(cp_file, 'w', encoding='utf-8') as f:
3083
- if h_headers:
3084
- f.write(f'SOURCE({cpp_sources}) && HEADER({h_headers}) {plugin_name}\n\n')
3085
- else:
3086
- f.write(f'SOURCE({cpp_sources}) {plugin_name}\n\n')
3749
+ # Build the declaration line: SOURCE(file1) && SOURCE(file2) && HEADER(h1) plugin_name
3750
+ parts = [f'SOURCE({p})' for p in cpp_paths]
3751
+ parts.extend([f'HEADER({p})' for p in h_paths])
3752
+ declaration = ' && '.join(parts) + f' {plugin_name}'
3753
+ f.write(declaration + '\n\n')
3087
3754
 
3088
- if classes or public_functions or template_functions:
3755
+ if classes or public_functions or template_functions or enums:
3089
3756
  f.write('PUBLIC(\n')
3090
3757
 
3758
+ # v4.6.5: Write enum bindings
3759
+ for enum_name in sorted(enums.keys()):
3760
+ enum_info = enums[enum_name]
3761
+ is_class = enum_info.get('is_class', False)
3762
+ values = enum_info.get('values', [])
3763
+
3764
+ class_kw = ' CLASS' if is_class else ''
3765
+ values_str = ' '.join(values)
3766
+ f.write(f' {plugin_name} ENUM({enum_name}){class_kw} {{ {values_str} }}\n')
3767
+
3768
+ if enums and classes:
3769
+ f.write('\n')
3770
+
3091
3771
  for cls_name in sorted(classes.keys()):
3092
3772
  cls_info = classes[cls_name]
3093
3773
  f.write(f' {plugin_name} CLASS({cls_name}) {{\n')
@@ -3128,10 +3808,22 @@ def plugin(plugin_name, files, private):
3128
3808
  for method in sorted(cls_info['methods']):
3129
3809
  f.write(f' METHOD({method})\n')
3130
3810
 
3131
- # v3.2.2: Write fields (name only, type not needed in .cp)
3811
+ # v3.2.2: Write fields
3812
+ # v4.6.6: Support array fields with FIELD_ARRAY(name, type, size)
3132
3813
  if cls_info.get('fields'):
3133
- for field_type, field_name in cls_info['fields']:
3134
- f.write(f' FIELD({field_name})\n')
3814
+ for field_info in cls_info['fields']:
3815
+ # Handle both old (type, name) and new (type, name, array_size) formats
3816
+ if len(field_info) >= 3:
3817
+ field_type, field_name, array_size = field_info
3818
+ else:
3819
+ field_type, field_name = field_info
3820
+ array_size = 0
3821
+
3822
+ if array_size > 0:
3823
+ # Array field: use FIELD_ARRAY with type info for proper binding
3824
+ f.write(f' FIELD_ARRAY({field_name}, {field_type}, {array_size})\n')
3825
+ else:
3826
+ f.write(f' FIELD({field_name})\n')
3135
3827
 
3136
3828
  f.write(f' }}\n')
3137
3829
 
@@ -3155,6 +3847,7 @@ def plugin(plugin_name, files, private):
3155
3847
  f.write(')\n')
3156
3848
 
3157
3849
  click.secho(f"Generated plugin: {cp_file}", fg='green', bold=True)
3850
+ click.echo(f"Enums found: {len(enums)}")
3158
3851
  click.echo(f"Classes found: {len(classes)}")
3159
3852
  total_fields = sum(len(c.get('fields', [])) for c in classes.values())
3160
3853
  if total_fields:
@@ -3174,187 +3867,113 @@ _GITHUB_TOKEN = "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"
3174
3867
  _GITHUB_REPO = "liliassg/IncludeCPP"
3175
3868
 
3176
3869
  @cli.command()
3177
- @click.argument('message', required=False)
3178
- @click.option('--get', 'get_bugs', is_flag=True, help='List all reported bugs')
3179
- def bug(message, get_bugs):
3180
- """Report a bug or view existing bug reports.
3870
+ @click.argument('message')
3871
+ @click.argument('files', nargs=-1, type=click.Path())
3872
+ def bug(message, files):
3873
+ """Report a bug on GitHub Issues.
3874
+
3875
+ Opens your browser to create a new issue with pre-filled system info.
3181
3876
 
3182
3877
  Usage:
3183
- includecpp bug "Description of the bug" Submit a new bug report
3184
- includecpp bug --get List all bug reports
3878
+ includecpp bug "Description of the bug"
3879
+ includecpp bug "Crash when loading module" ./include/mylib.cpp ./app.py
3185
3880
  """
3186
- import urllib.request
3187
- import urllib.error
3188
- import json
3189
-
3190
- api_base = f"https://api.github.com/repos/{_GITHUB_REPO}/issues"
3191
- headers = {
3192
- "Authorization": f"Bearer {_GITHUB_TOKEN}",
3193
- "Accept": "application/vnd.github+json",
3194
- "X-GitHub-Api-Version": "2022-11-28",
3195
- "User-Agent": "IncludeCPP-CLI"
3196
- }
3197
-
3198
- if get_bugs:
3199
- # List all bugs
3200
- click.echo("=" * 60)
3201
- click.secho("IncludeCPP Bug Reports", fg='cyan', bold=True)
3202
- click.echo("=" * 60)
3203
- click.echo()
3881
+ import webbrowser
3882
+ import urllib.parse
3883
+ import platform
3884
+ from .. import __version__
3204
3885
 
3205
- try:
3206
- click.echo(" Fetching bug reports...", nl=False)
3886
+ click.echo("=" * 60)
3887
+ click.secho("Bug Report", fg='cyan', bold=True)
3888
+ click.echo("=" * 60)
3889
+ click.echo()
3207
3890
 
3208
- req = urllib.request.Request(
3209
- f"{api_base}?state=all&labels=bug&per_page=50",
3210
- headers=headers
3211
- )
3891
+ # Build issue title
3892
+ title = f"[Bug] {message[:50]}{'...' if len(message) > 50 else ''}"
3212
3893
 
3213
- with urllib.request.urlopen(req) as response:
3214
- issues = json.loads(response.read())
3215
- click.secho(" OK", fg='green')
3216
- click.echo()
3894
+ # Build issue body with system info
3895
+ body_parts = [
3896
+ "## Bug Description",
3897
+ message,
3898
+ "",
3899
+ "## System Information",
3900
+ f"- **IncludeCPP Version:** {__version__}",
3901
+ f"- **Python:** {platform.python_version()}",
3902
+ f"- **OS:** {platform.system()} {platform.release()}",
3903
+ f"- **Architecture:** {platform.machine()}",
3904
+ ]
3217
3905
 
3218
- if not issues:
3219
- click.secho(" No bug reports found.", fg='yellow')
3906
+ # Add file info if provided
3907
+ if files:
3908
+ body_parts.append("")
3909
+ body_parts.append("## Related Files")
3910
+ for filepath in files:
3911
+ filepath = Path(filepath)
3912
+ if filepath.exists():
3913
+ size = filepath.stat().st_size
3914
+ ext = filepath.suffix
3915
+ body_parts.append(f"- `{filepath.name}` ({ext}, {size} bytes)")
3916
+
3917
+ # Include small file contents (< 2KB)
3918
+ if size < 2048 and ext in ['.py', '.cpp', '.h', '.hpp', '.cssl', '.txt']:
3919
+ try:
3920
+ content = filepath.read_text(errors='replace')[:1500]
3921
+ body_parts.append(f"```{ext[1:] if ext else 'text'}")
3922
+ body_parts.append(content)
3923
+ body_parts.append("```")
3924
+ except:
3925
+ pass
3220
3926
  else:
3221
- open_count = sum(1 for i in issues if i['state'] == 'open')
3222
- closed_count = len(issues) - open_count
3927
+ body_parts.append(f"- `{filepath}` (file not found)")
3223
3928
 
3224
- click.echo(f" Found {len(issues)} bug(s): {open_count} open, {closed_count} closed")
3225
- click.echo()
3929
+ body_parts.append("")
3930
+ body_parts.append("---")
3931
+ body_parts.append("*Submitted via `includecpp bug` command*")
3226
3932
 
3227
- for issue in issues:
3228
- number = issue['number']
3229
- title = issue['title']
3230
- state = issue['state']
3231
- created = issue['created_at'][:10]
3933
+ body = "\n".join(body_parts)
3232
3934
 
3233
- if state == 'open':
3234
- status = click.style("[OPEN]", fg='red')
3235
- else:
3236
- status = click.style("[CLOSED]", fg='green')
3237
-
3238
- click.echo(f" #{number} {status} {title}")
3239
- click.echo(f" Created: {created}")
3240
-
3241
- # Show body preview
3242
- body = issue.get('body', '')
3243
- if body:
3244
- preview = body[:80].replace('\n', ' ')
3245
- if len(body) > 80:
3246
- preview += "..."
3247
- click.secho(f" {preview}", fg='bright_black')
3248
- click.echo()
3935
+ # Build GitHub issue URL with pre-filled content
3936
+ params = urllib.parse.urlencode({
3937
+ 'title': title,
3938
+ 'body': body,
3939
+ 'labels': 'bug'
3940
+ })
3249
3941
 
3250
- click.echo("=" * 60)
3251
- click.echo(f"View online: https://github.com/{_GITHUB_REPO}/issues")
3942
+ issue_url = f"https://github.com/{_GITHUB_REPO}/issues/new?{params}"
3252
3943
 
3253
- except urllib.error.HTTPError as e:
3254
- click.secho(f" FAILED (HTTP {e.code})", fg='red', err=True)
3255
- click.echo()
3256
- if e.code == 401 or e.code == 403:
3257
- click.echo("The built-in bug reporting token has expired.")
3258
- click.echo()
3259
- click.secho("Please report bugs directly on GitHub:", fg='cyan')
3260
- click.echo(f" https://github.com/{_GITHUB_REPO}/issues/new")
3261
- else:
3262
- click.echo(f"GitHub API error: {e.code}")
3263
- except Exception as e:
3264
- click.secho(f" FAILED ({e})", fg='red', err=True)
3265
- click.echo()
3266
- click.secho("Please report bugs directly on GitHub:", fg='cyan')
3267
- click.echo(f" https://github.com/{_GITHUB_REPO}/issues/new")
3944
+ click.echo(f" Description: {message[:60]}{'...' if len(message) > 60 else ''}")
3945
+ if files:
3946
+ click.echo(f" Files: {len(files)} attached")
3947
+ click.echo()
3268
3948
 
3269
- return
3949
+ click.echo(" Opening GitHub Issues...", nl=False)
3270
3950
 
3271
- # Submit new bug
3272
- if not message:
3273
- click.secho("Error: Please provide a bug description.", fg='red', err=True)
3951
+ try:
3952
+ webbrowser.open(issue_url)
3953
+ click.secho(" OK", fg='green')
3274
3954
  click.echo()
3275
- click.echo("Usage:")
3276
- click.echo(' includecpp bug "Description of the bug"')
3277
- click.echo(' includecpp bug --get')
3278
- return
3955
+ click.secho("Browser opened! Complete your bug report on GitHub.", fg='green')
3956
+ except Exception as e:
3957
+ click.secho(f" FAILED", fg='red')
3958
+ click.echo()
3959
+ click.echo("Could not open browser. Please visit manually:")
3960
+ click.echo(f" https://github.com/{_GITHUB_REPO}/issues/new")
3961
+ click.echo()
3962
+ click.echo("Title:")
3963
+ click.secho(f" {title}", fg='yellow')
3964
+ click.echo()
3965
+ click.echo("Copy this description:")
3966
+ click.echo("-" * 40)
3967
+ click.echo(body[:500])
3968
+ if len(body) > 500:
3969
+ click.echo("...")
3279
3970
 
3280
- click.echo("=" * 60)
3281
- click.secho("Submitting Bug Report", fg='cyan', bold=True)
3282
- click.echo("=" * 60)
3283
3971
  click.echo()
3972
+ click.echo("=" * 60)
3284
3973
 
3285
- # Get system info
3286
- from .. import __version__
3287
- import platform
3288
-
3289
- system_info = f"""
3290
- **System Information:**
3291
- - IncludeCPP Version: {__version__}
3292
- - Python: {platform.python_version()}
3293
- - OS: {platform.system()} {platform.release()}
3294
- - Architecture: {platform.machine()}
3295
-
3296
- **Bug Description:**
3297
- {message}
3298
-
3299
- ---
3300
- *Submitted via `includecpp bug` command*
3301
- """
3302
-
3303
- issue_data = {
3304
- "title": f"[Bug] {message[:50]}{'...' if len(message) > 50 else ''}",
3305
- "body": system_info.strip(),
3306
- "labels": ["bug"]
3307
- }
3308
-
3309
- try:
3310
- click.echo(" Submitting to GitHub...", nl=False)
3311
-
3312
- req = urllib.request.Request(
3313
- api_base,
3314
- data=json.dumps(issue_data).encode('utf-8'),
3315
- headers={**headers, "Content-Type": "application/json"},
3316
- method="POST"
3317
- )
3318
-
3319
- with urllib.request.urlopen(req) as response:
3320
- result = json.loads(response.read())
3321
- click.secho(" OK", fg='green')
3322
- click.echo()
3323
-
3324
- issue_number = result['number']
3325
- issue_url = result['html_url']
3326
-
3327
- click.secho("Bug report submitted successfully!", fg='green', bold=True)
3328
- click.echo()
3329
- click.echo(f" Issue #{issue_number}")
3330
- click.echo(f" URL: {issue_url}")
3331
- click.echo()
3332
- click.echo("Thank you for reporting this issue!")
3333
-
3334
- except urllib.error.HTTPError as e:
3335
- click.secho(f" FAILED (HTTP {e.code})", fg='red', err=True)
3336
- click.echo()
3337
- if e.code == 401 or e.code == 403:
3338
- click.echo("The built-in bug reporting token has expired.")
3339
- click.echo()
3340
- click.secho("Please report this bug manually on GitHub:", fg='cyan')
3341
- click.echo(f" https://github.com/{_GITHUB_REPO}/issues/new")
3342
- click.echo()
3343
- click.echo("Copy this info:")
3344
- click.echo(f' Title: [Bug] {message[:50]}')
3345
- else:
3346
- click.echo(f"GitHub API error: {e.code}")
3347
- except Exception as e:
3348
- click.secho(f" FAILED ({e})", fg='red', err=True)
3349
- click.echo()
3350
- click.secho("Please report this bug manually on GitHub:", fg='cyan')
3351
- click.echo(f" https://github.com/{_GITHUB_REPO}/issues/new")
3352
-
3353
- click.echo("=" * 60)
3354
-
3355
- # =============================================================================
3356
- # MODULE UPLOAD COMMAND
3357
- # =============================================================================
3974
+ # =============================================================================
3975
+ # MODULE UPLOAD COMMAND
3976
+ # =============================================================================
3358
3977
 
3359
3978
  # Security patterns for upload validation
3360
3979
  _API_KEY_PATTERNS = [
@@ -6439,21 +7058,45 @@ def cppy_convert(files, to_cpp, to_py, no_header, output, ns, verbose, use_ai, u
6439
7058
 
6440
7059
 
6441
7060
  def _resolve_cp_source(cp_file: Path, project_root: Path) -> Path:
6442
- """Resolve SOURCE() directive from .cp file to actual source file."""
7061
+ """Resolve SOURCE() directive from .cp file to actual source file.
7062
+
7063
+ Returns the first valid source file found. Supports:
7064
+ - Single file: SOURCE(file.cpp)
7065
+ - Multiple files in one SOURCE: SOURCE(file1.cpp file2.cpp)
7066
+ - Multiple SOURCE declarations: SOURCE(file1.cpp) && SOURCE(file2.cpp)
7067
+ """
6443
7068
  try:
6444
7069
  content = cp_file.read_text(encoding='utf-8', errors='replace')
6445
7070
  import re
6446
- source_match = re.search(r'SOURCE\s*\(\s*([^)]+)\s*\)', content)
6447
- if source_match:
6448
- source_name = source_match.group(1).strip().strip('"\'')
6449
- include_dir = project_root / "include"
6450
- for ext in ['.cpp', '.h', '.hpp', '.cc', '.cxx']:
6451
- candidate = include_dir / (source_name.replace('.cpp', '').replace('.h', '') + ext)
6452
- if candidate.exists():
6453
- return candidate
6454
- direct = include_dir / source_name
6455
- if direct.exists():
6456
- return direct
7071
+
7072
+ # Find all SOURCE() declarations
7073
+ source_matches = re.findall(r'SOURCE\s*\(\s*([^)]+)\s*\)', content)
7074
+ for sources_str in source_matches:
7075
+ # Handle multiple files within one SOURCE()
7076
+ sources = re.split(r'[,\s]+', sources_str.strip())
7077
+ for source_name in sources:
7078
+ source_name = source_name.strip().strip('"\'')
7079
+ if not source_name:
7080
+ continue
7081
+
7082
+ include_dir = project_root / "include"
7083
+
7084
+ # Try direct path first (relative to project root)
7085
+ direct_from_root = project_root / source_name
7086
+ if direct_from_root.exists():
7087
+ return direct_from_root
7088
+
7089
+ # Try in include directory
7090
+ direct = include_dir / source_name
7091
+ if direct.exists():
7092
+ return direct
7093
+
7094
+ # Try with different extensions
7095
+ for ext in ['.cpp', '.h', '.hpp', '.cc', '.cxx']:
7096
+ base_name = source_name.replace('.cpp', '').replace('.h', '').replace('.hpp', '')
7097
+ candidate = include_dir / (base_name + ext)
7098
+ if candidate.exists():
7099
+ return candidate
6457
7100
  except Exception:
6458
7101
  pass
6459
7102
  return None
@@ -7479,7 +8122,7 @@ def exec_repl(lang, path, import_all):
7479
8122
  module_name = None
7480
8123
 
7481
8124
  # Check if it's a .cp plugin file
7482
- if path.endswith('.cp') or '/plugins/' in path or '\\plugins\\' in path:
8125
+ if path.endswith('.cp') or f'{os.sep}plugins{os.sep}' in path or '/plugins/' in path:
7483
8126
  # Extract module name from plugin path
7484
8127
  module_name = path_obj.stem
7485
8128
  elif path_obj.exists():
@@ -7594,16 +8237,16 @@ def exec_repl(lang, path, import_all):
7594
8237
  cssl_lang = CsslLang()
7595
8238
  result = cssl_lang.exec(full_code)
7596
8239
 
7597
- # Print any output from the execution
8240
+ # v4.8.6: Don't re-print output buffer - runtime.output() already prints to stdout
8241
+ # Just check if there was output for the "(no output)" message
7598
8242
  output = cssl_lang.get_output()
7599
- if output:
7600
- for line in output:
7601
- click.echo(line)
8243
+ had_output = bool(output)
8244
+ cssl_lang.clear_output() # Clear buffer for next execution
7602
8245
 
7603
8246
  if result is not None:
7604
8247
  click.echo(result)
7605
8248
 
7606
- if not output and result is None:
8249
+ if not had_output and result is None:
7607
8250
  click.secho("(no output)", fg='bright_black')
7608
8251
 
7609
8252
  except Exception as e:
@@ -7689,31 +8332,35 @@ def cssl():
7689
8332
  @cssl.command(name='run')
7690
8333
  @click.argument('path', required=False, type=click.Path())
7691
8334
  @click.option('--code', '-c', type=str, help='Execute code directly')
7692
- def cssl_run(path, code):
8335
+ @click.option('--python', '-p', is_flag=True, help='Force Python interpreter (full builtin support)')
8336
+ def cssl_run(path, code, python):
7693
8337
  """Run/execute CSSL code or file."""
7694
- _cssl_execute(path, code)
8338
+ _cssl_execute(path, code, force_python=python)
7695
8339
 
7696
8340
 
7697
8341
  @cssl.command(name='exec')
7698
8342
  @click.argument('path', required=False, type=click.Path())
7699
8343
  @click.option('--code', '-c', type=str, help='Execute code directly')
7700
- def cssl_exec(path, code):
8344
+ @click.option('--python', '-p', is_flag=True, help='Force Python interpreter (full builtin support)')
8345
+ def cssl_exec(path, code, python):
7701
8346
  """Execute CSSL code or file (alias for 'run')."""
7702
- _cssl_execute(path, code)
8347
+ _cssl_execute(path, code, force_python=python)
8348
+
7703
8349
 
8350
+ def _cssl_execute(path, code, force_python=False):
8351
+ """Internal: Execute CSSL code or file.
7704
8352
 
7705
- def _cssl_execute(path, code):
7706
- """Internal: Execute CSSL code or file."""
8353
+ v4.8.7: Uses CSSLRuntime directly for consistent behavior.
8354
+ CsslLang is an extra API layer for users, not needed for CLI execution.
8355
+ """
7707
8356
  from pathlib import Path as PathLib
7708
8357
 
7709
8358
  try:
7710
- from ..core.cssl_bridge import CsslLang
8359
+ from ..core.cssl.cssl_runtime import CSSLRuntime
7711
8360
  except ImportError as e:
7712
8361
  click.secho(f"CSSL runtime not available: {e}", fg='red')
7713
8362
  return
7714
8363
 
7715
- cssl_lang = CsslLang()
7716
-
7717
8364
  # Determine source
7718
8365
  if code:
7719
8366
  source = code
@@ -7754,13 +8401,31 @@ def _cssl_execute(path, code):
7754
8401
 
7755
8402
  source = '\n'.join(lines)
7756
8403
 
7757
- # Execute
8404
+ # Execute using CSSLRuntime directly
8405
+ # v4.9.1: Use execute_program for standalone scripts (not service format)
7758
8406
  try:
7759
- result = cssl_lang.run(source)
8407
+ runtime = CSSLRuntime()
8408
+ # v4.9.3: Set current file path for relative payload resolution
8409
+ if path:
8410
+ import os
8411
+ runtime._current_file_path = os.path.abspath(path)
8412
+ runtime._current_file = os.path.basename(path)
8413
+ # Auto-detect: service format starts with service-init/run/include
8414
+ stripped = source.lstrip()
8415
+ if stripped.startswith('service-init') or stripped.startswith('service-run') or stripped.startswith('service-include'):
8416
+ result = runtime.execute(source)
8417
+ else:
8418
+ result = runtime.execute_program(source)
7760
8419
  except Exception as e:
7761
8420
  error_msg = str(e)
7762
- # Clean display - single CSSL Error: prefix with colorama
7763
- click.echo(f"{Fore.RED}CSSL Error: {error_msg}{Style.RESET_ALL}")
8421
+ # v4.8.6: Handle Unicode encoding errors in error messages
8422
+ try:
8423
+ # Clean display - single CSSL Error: prefix with colorama
8424
+ click.echo(f"{Fore.RED}CSSL Error: {error_msg}{Style.RESET_ALL}")
8425
+ except UnicodeEncodeError:
8426
+ # Fallback: replace non-ASCII characters with placeholders
8427
+ safe_msg = error_msg.encode('ascii', 'replace').decode('ascii')
8428
+ click.echo(f"CSSL Error: {safe_msg}")
7764
8429
 
7765
8430
 
7766
8431
  @cssl.command(name='makemodule')
@@ -8235,61 +8900,212 @@ def _show_doc_globals():
8235
8900
 
8236
8901
  def _show_doc_injection():
8237
8902
  """Show code injection operators documentation."""
8238
- click.secho("Code Injection Operators", fg='red', bold=True)
8239
- click.secho("=" * 60, fg='red')
8903
+ click.secho("╔══════════════════════════════════════════════════════════════╗", fg='red', bold=True)
8904
+ click.secho("║ CODE INJECTION & FILTERING SYSTEM ║", fg='red', bold=True)
8905
+ click.secho("╚══════════════════════════════════════════════════════════════╝", fg='red', bold=True)
8240
8906
  click.echo()
8241
8907
 
8242
- click.secho("<== BruteInjection (Replace)", fg='cyan')
8243
- click.echo("-" * 40)
8244
- click.echo(" Completely replaces a method body.")
8908
+ # ===== SECTION 1: BASIC OPERATORS =====
8909
+ click.secho("┌─────────────────────────────────────────────────────────────┐", fg='yellow')
8910
+ click.secho(" 1. BASIC INJECTION OPERATORS │", fg='yellow', bold=True)
8911
+ click.secho("└─────────────────────────────────────────────────────────────┘", fg='yellow')
8245
8912
  click.echo()
8246
- click.echo(" Syntax: ClassName::method <== { new body }")
8913
+
8914
+ click.secho(" <== Replace (BruteInjection)", fg='cyan', bold=True)
8915
+ click.echo(" ─────────────────────────────────")
8916
+ click.echo(" target <== source; // Replace target with source")
8917
+ click.echo(" myList <== { 1, 2, 3 }; // Replace list contents")
8918
+ click.echo(" Player::update <== { ... }; // Replace method body")
8247
8919
  click.echo()
8248
- click.echo(" Example:")
8249
- click.echo(" Player::update <== {")
8250
- click.echo(" // Completely new implementation")
8251
- click.echo(" this->x += this->speed;")
8252
- click.echo(" }")
8920
+
8921
+ click.secho(" +<== Add/Append", fg='cyan', bold=True)
8922
+ click.echo(" ─────────────────────────────────")
8923
+ click.echo(" target +<== source; // Add source to target")
8924
+ click.echo(" myList +<== { 4, 5, 6 }; // Append items to list")
8925
+ click.echo(" myDict +<== { \"key\" = val }; // Merge dict into target")
8926
+ click.echo(" printl +<<== { log(msg); }; // Add code to function")
8253
8927
  click.echo()
8254
8928
 
8255
- click.secho("+<== BruteInjection (Append)", fg='cyan')
8256
- click.echo("-" * 40)
8257
- click.echo(" Appends code to end of existing method.")
8929
+ click.secho(" -<== Remove/Subtract", fg='cyan', bold=True)
8930
+ click.echo(" ─────────────────────────────────")
8931
+ click.echo(" target -<== source; // Remove source items from target")
8932
+ click.echo(" myList -<== { 2, 4, 6 }; // Remove specific items")
8933
+ click.echo(" myDict -<== { \"key\" }; // Remove keys from dict")
8258
8934
  click.echo()
8259
- click.echo(" Syntax: ClassName::method +<== { appended code }")
8935
+
8936
+ click.secho(" ==> Receive (Reverse Direction)", fg='cyan', bold=True)
8937
+ click.echo(" ─────────────────────────────────")
8938
+ click.echo(" source ==> target; // Move source to target")
8939
+ click.echo(" source ==>+ target; // Add source to target")
8940
+ click.echo(" source -==> target; // Move & clear source")
8260
8941
  click.echo()
8261
- click.echo(" Example:")
8262
- click.echo(" Player::update +<== {")
8263
- click.echo(" // This runs after original update()")
8264
- click.echo(" this->checkBounds();")
8265
- click.echo(" }")
8942
+
8943
+ # ===== SECTION 2: CODE INFUSION =====
8944
+ click.secho("┌─────────────────────────────────────────────────────────────┐", fg='yellow')
8945
+ click.secho("│ 2. CODE INFUSION OPERATORS │", fg='yellow', bold=True)
8946
+ click.secho("└─────────────────────────────────────────────────────────────┘", fg='yellow')
8266
8947
  click.echo()
8267
8948
 
8268
- click.secho("-<== BruteInjection (Prepend)", fg='cyan')
8269
- click.echo("-" * 40)
8270
- click.echo(" Prepends code to start of existing method.")
8949
+ click.secho(" <<== CodeInfusion (Function Hooking)", fg='magenta', bold=True)
8950
+ click.echo(" ─────────────────────────────────")
8951
+ click.echo(" func <<== { code }; // Replace function body")
8952
+ click.echo(" func +<<== { code }; // Add code AFTER function")
8953
+ click.echo(" func -<<== { code }; // Add code BEFORE function")
8271
8954
  click.echo()
8272
- click.echo(" Syntax: ClassName::method -<== { prepended code }")
8955
+ click.echo(" Example - Hook into printl:")
8956
+ click.secho(" printl +<<== { log(\"Called printl\"); };", fg='green')
8957
+ click.secho(" printl(\"Hello\"); // Also triggers hook", fg='green')
8273
8958
  click.echo()
8274
- click.echo(" Example:")
8275
- click.echo(" Player::update -<== {")
8276
- click.echo(" // This runs before original update()")
8277
- click.echo(" this->validateState();")
8278
- click.echo(" }")
8959
+
8960
+ click.secho(" ==>> Right-side Infusion", fg='magenta', bold=True)
8961
+ click.echo(" ─────────────────────────────────")
8962
+ click.echo(" { code } ==>> func; // Infuse code into function")
8279
8963
  click.echo()
8280
8964
 
8281
- click.secho("<<== CodeInfusion (Contextual Replace)", fg='cyan')
8282
- click.echo("-" * 40)
8283
- click.echo(" Like <== but preserves class context (this, super).")
8965
+ # ===== SECTION 3: FILTERING =====
8966
+ click.secho("┌─────────────────────────────────────────────────────────────┐", fg='yellow')
8967
+ click.secho(" 3. INJECTION FILTERS [type::helper=value] │", fg='yellow', bold=True)
8968
+ click.secho("└─────────────────────────────────────────────────────────────┘", fg='yellow')
8284
8969
  click.echo()
8285
- click.echo(" Syntax: ClassName::method <<== { new body }")
8970
+
8971
+ click.secho(" Filter Syntax:", fg='cyan', bold=True)
8972
+ click.echo(" target <==[filter] source;")
8973
+ click.echo(" target <==[f1][f2][f3] source; // Chained filters")
8974
+ click.echo(" source ==>[filter] target;")
8286
8975
  click.echo()
8287
- click.echo(" Example:")
8288
- click.echo(" Enemy::attack <<== {")
8289
- click.echo(" // Has access to this-> context")
8290
- click.echo(" this->damage = this->baseDamage * 2;")
8291
- click.echo(" super::attack(); // Call parent")
8976
+
8977
+ click.secho(" String Filters:", fg='green', bold=True)
8978
+ click.echo(" [string::where=VALUE] // Exact match")
8979
+ click.echo(" [string::contains=SUB] // Contains substring")
8980
+ click.echo(" [string::not=VALUE] // Exclude matches")
8981
+ click.echo(" [string::length=N] // Filter by length")
8982
+ click.echo(" [string::startswith=PRE] // Starts with prefix")
8983
+ click.echo(" [string::endswith=SUF] // Ends with suffix")
8984
+ click.echo()
8985
+
8986
+ click.secho(" Integer/Number Filters:", fg='green', bold=True)
8987
+ click.echo(" [integer::where=N] // Exact value match")
8988
+ click.echo(" [integer::gt=N] // Greater than N")
8989
+ click.echo(" [integer::lt=N] // Less than N")
8990
+ click.echo(" [integer::range=A,B] // Between A and B")
8991
+ click.echo()
8992
+
8993
+ click.secho(" Array/List/Vector Filters:", fg='green', bold=True)
8994
+ click.echo(" [array::index=N] // Get element at index N")
8995
+ click.echo(" [array::length=N] // Filter by length")
8996
+ click.echo(" [array::first] // Get first element")
8997
+ click.echo(" [array::last] // Get last element")
8998
+ click.echo(" [array::slice=A,B] // Get slice [A:B]")
8999
+ click.echo(" [vector::where=VAL] // Filter containing VAL")
9000
+ click.echo()
9001
+
9002
+ click.secho(" JSON/Dict Filters:", fg='green', bold=True)
9003
+ click.echo(" [json::key=KEY] // Extract value by key")
9004
+ click.echo(" [json::value=VAL] // Filter by value")
9005
+ click.echo(" [json::keys] // Get all keys")
9006
+ click.echo(" [json::values] // Get all values")
9007
+ click.echo()
9008
+
9009
+ click.secho(" Dynamic/Type Filters:", fg='green', bold=True)
9010
+ click.echo(" [dynamic::VarName=VAL] // Filter by variable value")
9011
+ click.echo(" [type::string] // Filter only strings")
9012
+ click.echo(" [type::int] // Filter only integers")
9013
+ click.echo()
9014
+
9015
+ click.secho(" Instance/Object Filters:", fg='green', bold=True)
9016
+ click.echo(" [instance::class] // Get classes from object")
9017
+ click.echo(" [instance::method] // Get methods from object")
9018
+ click.echo(" [instance::var] // Get variables from object")
9019
+ click.echo(" [instance::all] // Get all (categorized)")
9020
+ click.echo(" [instance::\"ClassName\"] // Get specific class")
9021
+ click.echo()
9022
+
9023
+ click.secho(" Name Filter:", fg='green', bold=True)
9024
+ click.echo(" [name::\"MyName\"] // Filter by name attribute")
9025
+ click.echo()
9026
+
9027
+ # ===== SECTION 4: CHAINED FILTERS =====
9028
+ click.secho("┌─────────────────────────────────────────────────────────────┐", fg='yellow')
9029
+ click.secho("│ 4. CHAINED FILTERS (Multiple Filters) │", fg='yellow', bold=True)
9030
+ click.secho("└─────────────────────────────────────────────────────────────┘", fg='yellow')
9031
+ click.echo()
9032
+
9033
+ click.echo(" Apply multiple filters in sequence:")
9034
+ click.echo()
9035
+ click.secho(" data <==[json::key=\"users\"][array::index=0] source;", fg='green')
9036
+ click.echo(" // Extract 'users' key, then get first element")
9037
+ click.echo()
9038
+ click.secho(" items <==[type::string][string::contains=\"error\"] logs;", fg='green')
9039
+ click.echo(" // Get strings containing 'error'")
9040
+ click.echo()
9041
+ click.secho(" nums <==[array::slice=0,10][integer::gt=5] data;", fg='green')
9042
+ click.echo(" // Get first 10 elements, then filter > 5")
9043
+ click.echo()
9044
+
9045
+ # ===== SECTION 5: EMBEDDED REPLACEMENT =====
9046
+ click.secho("┌─────────────────────────────────────────────────────────────┐", fg='yellow')
9047
+ click.secho("│ 5. EMBEDDED FUNCTION REPLACEMENT │", fg='yellow', bold=True)
9048
+ click.secho("└─────────────────────────────────────────────────────────────┘", fg='yellow')
9049
+ click.echo()
9050
+
9051
+ click.secho(" embedded define - Replace & Wrap Functions:", fg='magenta', bold=True)
9052
+ click.echo(" ─────────────────────────────────")
9053
+ click.echo(" embedded define NewFunc(args) &oldFunc {")
9054
+ click.echo(" // Pre-processing")
9055
+ click.echo(" result = %oldFunc(args); // Call original")
9056
+ click.echo(" // Post-processing")
9057
+ click.echo(" return result;")
9058
+ click.echo(" }")
9059
+ click.echo()
9060
+
9061
+ click.secho(" Example - Logging Wrapper:", fg='green')
9062
+ click.echo(" embedded define Logger(msg) &printl {")
9063
+ click.echo(" timestamp = now();")
9064
+ click.echo(" %printl(\"[\" + timestamp + \"] \" + msg);")
9065
+ click.echo(" }")
9066
+ click.echo()
9067
+
9068
+ click.secho(" Class Method Replacement:", fg='magenta', bold=True)
9069
+ click.echo(" ─────────────────────────────────")
9070
+ click.echo(" embedded define NewMethod() &Class::method {")
9071
+ click.echo(" // Replace Class::method")
8292
9072
  click.echo(" }")
9073
+ click.echo()
9074
+
9075
+ # ===== SECTION 6: PRACTICAL EXAMPLES =====
9076
+ click.secho("┌─────────────────────────────────────────────────────────────┐", fg='yellow')
9077
+ click.secho("│ 6. PRACTICAL EXAMPLES │", fg='yellow', bold=True)
9078
+ click.secho("└─────────────────────────────────────────────────────────────┘", fg='yellow')
9079
+ click.echo()
9080
+
9081
+ click.secho(" List Operations:", fg='cyan')
9082
+ click.echo(" list items = { 1, 2, 3, 4, 5 };")
9083
+ click.echo(" items +<== { 6, 7, 8 }; // [1,2,3,4,5,6,7,8]")
9084
+ click.echo(" items -<== { 2, 4, 6 }; // [1,3,5,7,8]")
9085
+ click.echo(" items <== { 10, 20 }; // [10,20] (replace)")
9086
+ click.echo()
9087
+
9088
+ click.secho(" Dict/DataStruct Operations:", fg='cyan')
9089
+ click.echo(" datastruct<dynamic> data;")
9090
+ click.echo(" data +<== { \"name\" = \"John\" };")
9091
+ click.echo(" data +<== { \"age\" = 30 };")
9092
+ click.echo(" config <==[json::key=\"name\"] data; // \"John\"")
9093
+ click.echo()
9094
+
9095
+ click.secho(" Function Hooking:", fg='cyan')
9096
+ click.echo(" // Add logging to all printl calls")
9097
+ click.echo(" printl +<<== { logToFile(msg); };")
9098
+ click.echo()
9099
+ click.echo(" // Validate before function runs")
9100
+ click.echo(" saveData -<<== { validate(data); };")
9101
+ click.echo()
9102
+
9103
+ click.secho(" Snapshot & Restore:", fg='cyan')
9104
+ click.echo(" list original = { 1, 2, 3 };")
9105
+ click.echo(" snapshot(original);")
9106
+ click.echo(" original +<== { 4, 5 };")
9107
+ click.echo(" printl(original); // [1,2,3,4,5]")
9108
+ click.echo(" printl(%original); // [1,2,3] (snapshot)")
8293
9109
 
8294
9110
 
8295
9111
  def _show_doc_open():
@@ -10170,6 +10986,671 @@ def _parse_cpp_params(params_str: str) -> str:
10170
10986
  return ', '.join(params)
10171
10987
 
10172
10988
 
10989
+ # ============================================================================
10990
+ # HomeServer Commands
10991
+ # ============================================================================
10992
+
10993
+ @cli.group()
10994
+ def server():
10995
+ """HomeServer - Local storage for modules and projects.
10996
+
10997
+ A lightweight background server for storing and sharing content.
10998
+ Default port: 2007
10999
+ """
11000
+ pass
11001
+
11002
+
11003
+ @server.command()
11004
+ def install():
11005
+ """Install and configure HomeServer for auto-start."""
11006
+ from ..core.homeserver import (
11007
+ HomeServerConfig, get_server_dir, setup_windows_autostart, start_server
11008
+ )
11009
+
11010
+ config = HomeServerConfig()
11011
+
11012
+ if config.is_installed():
11013
+ click.secho("HomeServer is already installed", fg='yellow')
11014
+ return
11015
+
11016
+ # Create directories
11017
+ server_dir = get_server_dir()
11018
+ server_dir.mkdir(parents=True, exist_ok=True)
11019
+
11020
+ # Mark as installed
11021
+ config.set_installed()
11022
+
11023
+ # Set up auto-start on Windows
11024
+ if sys.platform == 'win32':
11025
+ if setup_windows_autostart():
11026
+ click.secho("Auto-start configured for Windows", fg='green')
11027
+
11028
+ # Start the server
11029
+ success, port, message = start_server()
11030
+ if success:
11031
+ click.secho(f"HomeServer installed successfully", fg='green')
11032
+ click.secho(f"Running on port {port}", fg='cyan')
11033
+ else:
11034
+ click.secho(f"Installation complete but server failed to start: {message}", fg='yellow')
11035
+
11036
+
11037
+ @server.command()
11038
+ @click.option('--port', '-p', type=int, help='Port number to use')
11039
+ def start(port):
11040
+ """Start the HomeServer."""
11041
+ from ..core.homeserver import start_server
11042
+
11043
+ success, actual_port, message = start_server(port=port)
11044
+
11045
+ if success:
11046
+ click.secho(message, fg='green')
11047
+ else:
11048
+ click.secho(message, fg='red')
11049
+
11050
+
11051
+ @server.command()
11052
+ def stop():
11053
+ """Stop the HomeServer."""
11054
+ from ..core.homeserver import stop_server
11055
+
11056
+ success, message = stop_server()
11057
+
11058
+ if success:
11059
+ click.secho(message, fg='green')
11060
+ else:
11061
+ click.secho(message, fg='yellow')
11062
+
11063
+
11064
+ @server.command()
11065
+ def status():
11066
+ """Check HomeServer status."""
11067
+ from ..core.homeserver import is_server_running, HomeServerConfig, format_size, HomeServerDB
11068
+
11069
+ config = HomeServerConfig()
11070
+ running = is_server_running(config.port)
11071
+
11072
+ click.secho(f"Port: {config.port}", fg='cyan')
11073
+ click.secho(f"Status: ", nl=False)
11074
+
11075
+ if running:
11076
+ click.secho("Running", fg='green')
11077
+
11078
+ # Show storage stats
11079
+ try:
11080
+ db = HomeServerDB()
11081
+ items = db.get_all_items()
11082
+ total_size = sum(i.get('size_bytes', 0) for i in items)
11083
+ click.secho(f"Items: {len(items)}", fg='cyan')
11084
+ click.secho(f"Storage: {format_size(total_size)}", fg='cyan')
11085
+ except:
11086
+ pass
11087
+ else:
11088
+ click.secho("Stopped", fg='red')
11089
+
11090
+ click.secho(f"Auto-start: {'Enabled' if config.auto_start else 'Disabled'}", fg='cyan')
11091
+
11092
+
11093
+ @server.command('list')
11094
+ @click.option('--category', '-c', help='Filter by category')
11095
+ def server_list(category):
11096
+ """List all stored items."""
11097
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig, format_size
11098
+
11099
+ config = HomeServerConfig()
11100
+ if not is_server_running(config.port):
11101
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11102
+ return
11103
+
11104
+ try:
11105
+ client = HomeServerClient()
11106
+
11107
+ if category:
11108
+ items = client.get_items_by_category(category)
11109
+ click.secho(f"Category: {category}", fg='cyan', bold=True)
11110
+ else:
11111
+ items = client.list_items()
11112
+
11113
+ if not items:
11114
+ click.secho("No items stored", fg='yellow')
11115
+ return
11116
+
11117
+ click.secho(f"{'Name':<25} {'Type':<10} {'Category':<15} {'Size':<10}", fg='cyan', bold=True)
11118
+ click.secho("-" * 65, fg='white')
11119
+
11120
+ for item in items:
11121
+ name = item['name'][:23] + '..' if len(item['name']) > 25 else item['name']
11122
+ item_type = item['item_type']
11123
+ cat = item.get('category') or '-'
11124
+ cat = cat[:13] + '..' if len(cat) > 15 else cat
11125
+ size = format_size(item.get('size_bytes', 0))
11126
+ click.echo(f"{name:<25} {item_type:<10} {cat:<15} {size:<10}")
11127
+
11128
+ click.secho(f"\nTotal: {len(items)} items", fg='green')
11129
+
11130
+ # Show categories summary
11131
+ if not category:
11132
+ categories = client.list_categories()
11133
+ if categories:
11134
+ click.secho(f"Categories: {', '.join(categories)}", fg='cyan')
11135
+
11136
+ except ConnectionError as e:
11137
+ click.secho(str(e), fg='red')
11138
+
11139
+
11140
+ @server.command()
11141
+ @click.argument('name')
11142
+ @click.argument('path', type=click.Path(exists=True))
11143
+ @click.option('--project', '-p', is_flag=True, help='Upload as project (folder)')
11144
+ @click.option('--category', '-c', help='Category to organize the item')
11145
+ def upload(name, path, project, category):
11146
+ """Upload a file or project to HomeServer.
11147
+
11148
+ NAME: Unique name for the item
11149
+ PATH: Path to file or folder
11150
+ """
11151
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig, format_size
11152
+
11153
+ config = HomeServerConfig()
11154
+ if not is_server_running(config.port):
11155
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11156
+ return
11157
+
11158
+ path_obj = Path(path)
11159
+
11160
+ try:
11161
+ client = HomeServerClient()
11162
+
11163
+ if project or path_obj.is_dir():
11164
+ if not path_obj.is_dir():
11165
+ click.secho("--project flag requires a directory", fg='red')
11166
+ return
11167
+ click.secho(f"Uploading project '{name}'...", fg='cyan')
11168
+ result = client.upload_project(name, path_obj, category=category)
11169
+ else:
11170
+ click.secho(f"Uploading file '{name}'...", fg='cyan')
11171
+ result = client.upload_file(name, path_obj, category=category)
11172
+
11173
+ if result.get('success'):
11174
+ msg = f"Uploaded successfully: {name}"
11175
+ if category:
11176
+ msg += f" (category: {category})"
11177
+ click.secho(msg, fg='green')
11178
+ # Check if project path was auto-detected
11179
+ if not project and path_obj.suffix.lower() == '.py':
11180
+ saved_proj = client.get_project_path(name)
11181
+ if saved_proj:
11182
+ click.secho(f" Auto-detected project: {saved_proj}", fg='cyan')
11183
+ click.echo(" This will be used automatically with 'server run'")
11184
+ else:
11185
+ click.secho(f"Upload failed: {result.get('error', 'Unknown error')}", fg='red')
11186
+
11187
+ except ConnectionError as e:
11188
+ click.secho(str(e), fg='red')
11189
+ except Exception as e:
11190
+ click.secho(f"Error: {e}", fg='red')
11191
+
11192
+
11193
+ @server.command()
11194
+ @click.argument('name')
11195
+ @click.argument('output', type=click.Path(), required=False)
11196
+ def download(name, output):
11197
+ """Download a file or project from HomeServer.
11198
+
11199
+ NAME: Name of the item to download
11200
+ OUTPUT: Output path - directory (ends with /) or file path (default: current directory)
11201
+
11202
+ Examples:
11203
+ includecpp server download myfile.exe ./ # -> ./myfile.exe
11204
+ includecpp server download myfile.exe backup/ # -> backup/myfile.exe
11205
+ includecpp server download myfile.exe out.exe # -> out.exe
11206
+ """
11207
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11208
+
11209
+ config = HomeServerConfig()
11210
+ if not is_server_running(config.port):
11211
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11212
+ return
11213
+
11214
+ try:
11215
+ client = HomeServerClient()
11216
+
11217
+ # Get item info first
11218
+ item = client.get_item(name)
11219
+ if not item:
11220
+ click.secho(f"Item '{name}' not found", fg='red')
11221
+ return
11222
+
11223
+ # Determine output path and if it's a directory
11224
+ if output:
11225
+ # Check if user specified a directory (ends with separator or is existing dir)
11226
+ is_dir = output.endswith(('/', '\\', os.sep)) or Path(output).is_dir()
11227
+ output_path = Path(output.rstrip('/\\')) if is_dir else Path(output)
11228
+ else:
11229
+ # Default: current directory
11230
+ output_path = Path.cwd()
11231
+ is_dir = True
11232
+
11233
+ click.secho(f"Downloading '{name}'...", fg='cyan')
11234
+
11235
+ final_path = client.download_file(name, output_path, is_dir=is_dir)
11236
+ click.secho(f"Downloaded to: {final_path}", fg='green')
11237
+
11238
+ except ConnectionError as e:
11239
+ click.secho(str(e), fg='red')
11240
+ except Exception as e:
11241
+ click.secho(f"Error: {e}", fg='red')
11242
+
11243
+
11244
+ @server.command()
11245
+ @click.argument('name')
11246
+ @click.option('--force', '-f', is_flag=True, help='Skip confirmation')
11247
+ def delete(name, force):
11248
+ """Delete an item from HomeServer.
11249
+
11250
+ NAME: Name of the item to delete
11251
+ """
11252
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11253
+
11254
+ config = HomeServerConfig()
11255
+ if not is_server_running(config.port):
11256
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11257
+ return
11258
+
11259
+ try:
11260
+ client = HomeServerClient()
11261
+
11262
+ # Check item exists
11263
+ item = client.get_item(name)
11264
+ if not item:
11265
+ click.secho(f"Item '{name}' not found", fg='red')
11266
+ return
11267
+
11268
+ if not force:
11269
+ if not click.confirm(f"Delete '{name}'?"):
11270
+ return
11271
+
11272
+ result = client.delete_item(name)
11273
+
11274
+ if result.get('success'):
11275
+ click.secho(f"Deleted: {name}", fg='green')
11276
+ else:
11277
+ click.secho(f"Delete failed: {result.get('error', 'Unknown error')}", fg='red')
11278
+
11279
+ except ConnectionError as e:
11280
+ click.secho(str(e), fg='red')
11281
+
11282
+
11283
+ @server.command()
11284
+ @click.argument('target')
11285
+ @click.argument('args', nargs=-1)
11286
+ @click.option('--project', '-p', type=click.Path(exists=True),
11287
+ help='IncludeCPP project path for module imports')
11288
+ @click.option('--cwd', '-c', type=click.Path(exists=True),
11289
+ help='Working directory for execution')
11290
+ def run(target, args, project, cwd):
11291
+ """Run a stored executable, script, or project file.
11292
+
11293
+ TARGET: Item name, or name.path.to.file for project files
11294
+
11295
+ Examples:
11296
+ includecpp server run MyApp # Run single executable
11297
+ includecpp server run myproject.main # Run myproject/main.py
11298
+ includecpp server run proj.src.app # Run proj/src/app.py
11299
+
11300
+ # With includecpp project for module imports:
11301
+ includecpp server run myscript -p /path/to/project
11302
+ """
11303
+ from ..core.homeserver import (
11304
+ is_server_running, HomeServerClient, HomeServerConfig,
11305
+ get_server_dir, HomeServerDB
11306
+ )
11307
+ import tempfile
11308
+ import subprocess
11309
+
11310
+ config = HomeServerConfig()
11311
+ if not is_server_running(config.port):
11312
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11313
+ return
11314
+
11315
+ try:
11316
+ client = HomeServerClient()
11317
+ db = HomeServerDB()
11318
+
11319
+ # Parse target: name or name.path.to.file
11320
+ parts = target.split('.')
11321
+ item_name = parts[0]
11322
+ file_path = '.'.join(parts[1:]) if len(parts) > 1 else None
11323
+
11324
+ # Get item info
11325
+ item = client.get_item(item_name)
11326
+ if not item:
11327
+ click.secho(f"Item '{item_name}' not found", fg='red')
11328
+ return
11329
+
11330
+ storage_path = Path(item['storage_path'])
11331
+
11332
+ if item['item_type'] == 'project':
11333
+ # Project: find the file to run
11334
+ if file_path:
11335
+ # Convert dots to path separators
11336
+ rel_path = file_path.replace('.', os.sep)
11337
+ # Try with common extensions
11338
+ candidates = [
11339
+ storage_path / rel_path,
11340
+ storage_path / f"{rel_path}.py",
11341
+ storage_path / f"{rel_path}.bat",
11342
+ storage_path / f"{rel_path}.exe",
11343
+ storage_path / f"{rel_path}.sh",
11344
+ ]
11345
+ run_file = None
11346
+ for c in candidates:
11347
+ if c.exists():
11348
+ run_file = c
11349
+ break
11350
+
11351
+ if not run_file:
11352
+ click.secho(f"File not found: {file_path}", fg='red')
11353
+ click.secho(f"Tried: {', '.join(str(c.relative_to(storage_path)) for c in candidates)}", fg='yellow')
11354
+ return
11355
+ else:
11356
+ # Try to find main entry point
11357
+ for name in ['main.py', 'app.py', '__main__.py', 'run.py', 'main.bat', 'run.bat']:
11358
+ candidate = storage_path / name
11359
+ if candidate.exists():
11360
+ run_file = candidate
11361
+ break
11362
+ else:
11363
+ click.secho(f"No entry point found in project. Use: server run {item_name}.<path.to.file>", fg='red')
11364
+ # List available files
11365
+ files = [str(f.relative_to(storage_path)) for f in storage_path.rglob('*')
11366
+ if f.is_file() and f.suffix in ('.py', '.bat', '.exe', '.sh')][:10]
11367
+ if files:
11368
+ click.secho(f"Available files: {', '.join(files)}", fg='cyan')
11369
+ return
11370
+ else:
11371
+ # Single file
11372
+ run_file = storage_path
11373
+
11374
+ if not run_file.exists():
11375
+ click.secho(f"File not found: {run_file}", fg='red')
11376
+ return
11377
+
11378
+ # Determine how to run the file
11379
+ suffix = run_file.suffix.lower()
11380
+ click.secho(f"Running: {run_file.name}", fg='cyan')
11381
+
11382
+ # Set up environment for includecpp modules
11383
+ env = os.environ.copy()
11384
+
11385
+ # Auto-detect project path from metadata if not explicitly provided
11386
+ effective_project = project
11387
+ if not effective_project:
11388
+ saved_project = client.get_project_path(item_name)
11389
+ if saved_project and Path(saved_project).exists():
11390
+ effective_project = saved_project
11391
+ click.secho(f"Using saved project: {saved_project}", fg='cyan')
11392
+
11393
+ if effective_project:
11394
+ project_path = Path(effective_project).resolve()
11395
+ # Add project to PYTHONPATH so imports work
11396
+ pythonpath = env.get('PYTHONPATH', '')
11397
+ env['PYTHONPATH'] = f"{project_path}{os.pathsep}{pythonpath}" if pythonpath else str(project_path)
11398
+ # Set INCLUDECPP_PROJECT for the includecpp package to find modules
11399
+ env['INCLUDECPP_PROJECT'] = str(project_path)
11400
+
11401
+ # Determine working directory
11402
+ if cwd:
11403
+ work_dir = Path(cwd)
11404
+ elif effective_project:
11405
+ work_dir = Path(effective_project)
11406
+ else:
11407
+ work_dir = storage_path.parent if item['item_type'] == 'file' else storage_path
11408
+
11409
+ if suffix == '.py':
11410
+ cmd = [sys.executable, str(run_file)] + list(args)
11411
+ elif suffix == '.exe':
11412
+ cmd = [str(run_file)] + list(args)
11413
+ elif suffix == '.bat':
11414
+ cmd = ['cmd', '/c', str(run_file)] + list(args)
11415
+ elif suffix == '.sh':
11416
+ cmd = ['bash', str(run_file)] + list(args)
11417
+ elif suffix in ('.js', '.mjs'):
11418
+ cmd = ['node', str(run_file)] + list(args)
11419
+ else:
11420
+ # Try to execute directly
11421
+ cmd = [str(run_file)] + list(args)
11422
+
11423
+ # Run the command
11424
+ try:
11425
+ result = subprocess.run(cmd, cwd=work_dir, env=env)
11426
+ if result.returncode != 0:
11427
+ click.secho(f"Process exited with code {result.returncode}", fg='yellow')
11428
+ except FileNotFoundError:
11429
+ click.secho(f"Cannot execute: {run_file.name}", fg='red')
11430
+ except PermissionError:
11431
+ click.secho(f"Permission denied: {run_file.name}", fg='red')
11432
+
11433
+ except ConnectionError as e:
11434
+ click.secho(str(e), fg='red')
11435
+ except Exception as e:
11436
+ click.secho(f"Error: {e}", fg='red')
11437
+
11438
+
11439
+ @server.command()
11440
+ @click.argument('port_num', type=int)
11441
+ def port(port_num):
11442
+ """Change the server port.
11443
+
11444
+ PORT_NUM: New port number (1024-65535)
11445
+ """
11446
+ from ..core.homeserver import HomeServerConfig, is_server_running
11447
+
11448
+ if port_num < 1024 or port_num > 65535:
11449
+ click.secho("Port must be between 1024 and 65535", fg='red')
11450
+ return
11451
+
11452
+ config = HomeServerConfig()
11453
+ old_port = config.port
11454
+
11455
+ if is_server_running(old_port):
11456
+ click.secho("Stop the server first before changing port", fg='yellow')
11457
+ click.secho("Use: includecpp server stop", fg='cyan')
11458
+ return
11459
+
11460
+ config.port = port_num
11461
+ click.secho(f"Port changed from {old_port} to {port_num}", fg='green')
11462
+
11463
+
11464
+ @server.command()
11465
+ def deinstall():
11466
+ """Remove HomeServer completely."""
11467
+ from ..core.homeserver import (
11468
+ stop_server, remove_windows_autostart, get_server_dir, is_server_running,
11469
+ HomeServerConfig
11470
+ )
11471
+
11472
+ if not click.confirm("This will delete all stored data. Continue?"):
11473
+ return
11474
+
11475
+ config = HomeServerConfig()
11476
+
11477
+ # Stop if running
11478
+ if is_server_running(config.port):
11479
+ stop_server()
11480
+ click.secho("Server stopped", fg='cyan')
11481
+
11482
+ # Remove auto-start
11483
+ if sys.platform == 'win32':
11484
+ remove_windows_autostart()
11485
+ click.secho("Auto-start removed", fg='cyan')
11486
+
11487
+ # Delete all data
11488
+ server_dir = get_server_dir()
11489
+ if server_dir.exists():
11490
+ shutil.rmtree(server_dir)
11491
+ click.secho("Data deleted", fg='cyan')
11492
+
11493
+ click.secho("HomeServer deinstalled successfully", fg='green')
11494
+
11495
+
11496
+ # Category management subcommand group
11497
+ @server.group()
11498
+ def categories():
11499
+ """Manage categories for organizing items."""
11500
+ pass
11501
+
11502
+
11503
+ @categories.command('list')
11504
+ def categories_list():
11505
+ """List all categories."""
11506
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11507
+
11508
+ config = HomeServerConfig()
11509
+ if not is_server_running(config.port):
11510
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11511
+ return
11512
+
11513
+ try:
11514
+ client = HomeServerClient()
11515
+ cats = client.list_categories()
11516
+
11517
+ if not cats:
11518
+ click.secho("No categories", fg='yellow')
11519
+ return
11520
+
11521
+ click.secho("Categories:", fg='cyan', bold=True)
11522
+ for cat in cats:
11523
+ items = client.get_items_by_category(cat)
11524
+ click.echo(f" {cat} ({len(items)} items)")
11525
+
11526
+ except ConnectionError as e:
11527
+ click.secho(str(e), fg='red')
11528
+
11529
+
11530
+ @categories.command('add')
11531
+ @click.argument('name')
11532
+ def categories_add(name):
11533
+ """Create a new category."""
11534
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11535
+
11536
+ config = HomeServerConfig()
11537
+ if not is_server_running(config.port):
11538
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11539
+ return
11540
+
11541
+ try:
11542
+ client = HomeServerClient()
11543
+ result = client.add_category(name)
11544
+
11545
+ if result.get('success'):
11546
+ click.secho(f"Category '{name}' created", fg='green')
11547
+ else:
11548
+ click.secho(f"Failed: {result.get('error', 'Unknown error')}", fg='red')
11549
+
11550
+ except ConnectionError as e:
11551
+ click.secho(str(e), fg='red')
11552
+
11553
+
11554
+ @categories.command('delete')
11555
+ @click.argument('name')
11556
+ def categories_delete(name):
11557
+ """Delete a category (items become uncategorized)."""
11558
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11559
+
11560
+ config = HomeServerConfig()
11561
+ if not is_server_running(config.port):
11562
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11563
+ return
11564
+
11565
+ try:
11566
+ client = HomeServerClient()
11567
+ result = client.delete_category(name)
11568
+
11569
+ if result.get('success'):
11570
+ click.secho(f"Category '{name}' deleted", fg='green')
11571
+ else:
11572
+ click.secho(f"Failed: {result.get('error', 'Unknown error')}", fg='red')
11573
+
11574
+ except ConnectionError as e:
11575
+ click.secho(str(e), fg='red')
11576
+
11577
+
11578
+ @categories.command('move')
11579
+ @click.argument('item_name')
11580
+ @click.argument('category')
11581
+ def categories_move(item_name, category):
11582
+ """Move an item to a category.
11583
+
11584
+ Use '-' as category to uncategorize.
11585
+ """
11586
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11587
+
11588
+ config = HomeServerConfig()
11589
+ if not is_server_running(config.port):
11590
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11591
+ return
11592
+
11593
+ try:
11594
+ client = HomeServerClient()
11595
+ cat = None if category == '-' else category
11596
+ result = client.move_to_category(item_name, cat)
11597
+
11598
+ if result.get('success'):
11599
+ if cat:
11600
+ click.secho(f"Moved '{item_name}' to category '{cat}'", fg='green')
11601
+ else:
11602
+ click.secho(f"Removed '{item_name}' from category", fg='green')
11603
+ else:
11604
+ click.secho(f"Failed: {result.get('error', 'Unknown error')}", fg='red')
11605
+
11606
+ except ConnectionError as e:
11607
+ click.secho(str(e), fg='red')
11608
+
11609
+
11610
+ @categories.command('download')
11611
+ @click.argument('category')
11612
+ @click.argument('output', type=click.Path(), required=False)
11613
+ def categories_download(category, output):
11614
+ """Download all items in a category.
11615
+
11616
+ CATEGORY: Category name to download
11617
+ OUTPUT: Output directory (default: current directory)
11618
+ """
11619
+ from ..core.homeserver import is_server_running, HomeServerClient, HomeServerConfig
11620
+
11621
+ config = HomeServerConfig()
11622
+ if not is_server_running(config.port):
11623
+ click.secho("HomeServer is not running. Use 'includecpp server start' first.", fg='red')
11624
+ return
11625
+
11626
+ output_path = Path(output) if output else Path.cwd()
11627
+ output_path.mkdir(parents=True, exist_ok=True)
11628
+
11629
+ try:
11630
+ client = HomeServerClient()
11631
+ items = client.get_items_by_category(category)
11632
+
11633
+ if not items:
11634
+ click.secho(f"No items in category '{category}'", fg='yellow')
11635
+ return
11636
+
11637
+ click.secho(f"Downloading {len(items)} items from '{category}'...", fg='cyan')
11638
+
11639
+ downloaded = []
11640
+ for item in items:
11641
+ try:
11642
+ path = client.download_file(item['name'], output_path, is_dir=True)
11643
+ downloaded.append(item['name'])
11644
+ click.echo(f" Downloaded: {item['name']}")
11645
+ except Exception as e:
11646
+ click.secho(f" Failed: {item['name']} - {e}", fg='red')
11647
+
11648
+ click.secho(f"\nDownloaded {len(downloaded)}/{len(items)} items to {output_path}", fg='green')
11649
+
11650
+ except ConnectionError as e:
11651
+ click.secho(str(e), fg='red')
11652
+
11653
+
10173
11654
  # ============================================================================
10174
11655
  # Conditional Registration of Experimental Commands
10175
11656
  # ============================================================================