IncludeCPP 4.6.0__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.
- includecpp/CHANGELOG.md +241 -0
- includecpp/__init__.py +89 -3
- includecpp/__init__.pyi +2 -1
- includecpp/cli/commands.py +1747 -266
- includecpp/cli/config_parser.py +1 -1
- includecpp/core/build_manager.py +64 -13
- includecpp/core/cpp_api_extensions.pyi +43 -270
- includecpp/core/cssl/CSSL_DOCUMENTATION.md +1799 -1445
- includecpp/core/cssl/cpp/build/api.pyd +0 -0
- includecpp/core/cssl/cpp/build/api.pyi +274 -0
- includecpp/core/cssl/cpp/build/cssl_core.pyi +0 -99
- includecpp/core/cssl/cpp/cssl_core.cp +2 -23
- includecpp/core/cssl/cssl_builtins.py +2116 -171
- includecpp/core/cssl/cssl_builtins.pyi +1324 -104
- includecpp/core/cssl/cssl_compiler.py +4 -1
- includecpp/core/cssl/cssl_modules.py +605 -6
- includecpp/core/cssl/cssl_optimizer.py +12 -1
- includecpp/core/cssl/cssl_parser.py +1048 -52
- includecpp/core/cssl/cssl_runtime.py +2041 -131
- includecpp/core/cssl/cssl_syntax.py +405 -277
- includecpp/core/cssl/cssl_types.py +5891 -1655
- includecpp/core/cssl_bridge.py +427 -4
- includecpp/core/error_catalog.py +54 -10
- includecpp/core/homeserver.py +1037 -0
- includecpp/generator/parser.cpp +203 -39
- includecpp/generator/parser.h +15 -1
- includecpp/templates/cpp.proj.template +1 -1
- includecpp/vscode/cssl/snippets/cssl.snippets.json +163 -0
- includecpp/vscode/cssl/syntaxes/cssl.tmLanguage.json +87 -12
- {includecpp-4.6.0.dist-info → includecpp-4.9.3.dist-info}/METADATA +81 -10
- {includecpp-4.6.0.dist-info → includecpp-4.9.3.dist-info}/RECORD +35 -33
- {includecpp-4.6.0.dist-info → includecpp-4.9.3.dist-info}/WHEEL +1 -1
- {includecpp-4.6.0.dist-info → includecpp-4.9.3.dist-info}/entry_points.txt +0 -0
- {includecpp-4.6.0.dist-info → includecpp-4.9.3.dist-info}/licenses/LICENSE +0 -0
- {includecpp-4.6.0.dist-info → includecpp-4.9.3.dist-info}/top_level.txt +0 -0
includecpp/cli/commands.py
CHANGED
|
@@ -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,
|
|
450
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
901
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
3517
|
+
# v4.6.6: Detect array fields and capture size
|
|
3518
|
+
array_size = 0
|
|
2902
3519
|
if '[' in name:
|
|
2903
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3080
|
-
|
|
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
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
|
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
|
|
3134
|
-
|
|
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'
|
|
3178
|
-
@click.
|
|
3179
|
-
def bug(message,
|
|
3180
|
-
"""Report a bug
|
|
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"
|
|
3184
|
-
includecpp bug
|
|
3878
|
+
includecpp bug "Description of the bug"
|
|
3879
|
+
includecpp bug "Crash when loading module" ./include/mylib.cpp ./app.py
|
|
3185
3880
|
"""
|
|
3186
|
-
import
|
|
3187
|
-
import urllib.
|
|
3188
|
-
import
|
|
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
|
-
|
|
3206
|
-
|
|
3886
|
+
click.echo("=" * 60)
|
|
3887
|
+
click.secho("Bug Report", fg='cyan', bold=True)
|
|
3888
|
+
click.echo("=" * 60)
|
|
3889
|
+
click.echo()
|
|
3207
3890
|
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
headers=headers
|
|
3211
|
-
)
|
|
3891
|
+
# Build issue title
|
|
3892
|
+
title = f"[Bug] {message[:50]}{'...' if len(message) > 50 else ''}"
|
|
3212
3893
|
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
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
|
-
|
|
3219
|
-
|
|
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
|
-
|
|
3222
|
-
closed_count = len(issues) - open_count
|
|
3927
|
+
body_parts.append(f"- `{filepath}` (file not found)")
|
|
3223
3928
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3929
|
+
body_parts.append("")
|
|
3930
|
+
body_parts.append("---")
|
|
3931
|
+
body_parts.append("*Submitted via `includecpp bug` command*")
|
|
3226
3932
|
|
|
3227
|
-
|
|
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
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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
|
-
|
|
3949
|
+
click.echo(" Opening GitHub Issues...", nl=False)
|
|
3270
3950
|
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
click.secho("
|
|
3951
|
+
try:
|
|
3952
|
+
webbrowser.open(issue_url)
|
|
3953
|
+
click.secho(" OK", fg='green')
|
|
3274
3954
|
click.echo()
|
|
3275
|
-
click.
|
|
3276
|
-
|
|
3277
|
-
click.
|
|
3278
|
-
|
|
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
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
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
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
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 '
|
|
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
|
-
#
|
|
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
|
-
|
|
7600
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7706
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
7763
|
-
|
|
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("
|
|
8239
|
-
click.secho("
|
|
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
|
-
|
|
8243
|
-
click.
|
|
8244
|
-
click.
|
|
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
|
-
|
|
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
|
-
|
|
8249
|
-
click.
|
|
8250
|
-
click.echo("
|
|
8251
|
-
click.echo("
|
|
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("
|
|
8256
|
-
click.echo("
|
|
8257
|
-
click.echo("
|
|
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
|
-
|
|
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
|
-
|
|
8262
|
-
|
|
8263
|
-
click.
|
|
8264
|
-
click.
|
|
8265
|
-
click.
|
|
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("
|
|
8269
|
-
click.echo("
|
|
8270
|
-
click.echo("
|
|
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("
|
|
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
|
-
|
|
8275
|
-
click.
|
|
8276
|
-
click.echo("
|
|
8277
|
-
click.echo("
|
|
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
|
-
|
|
8282
|
-
click.
|
|
8283
|
-
click.
|
|
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
|
-
|
|
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
|
-
|
|
8288
|
-
click.
|
|
8289
|
-
click.echo("
|
|
8290
|
-
click.echo("
|
|
8291
|
-
click.echo("
|
|
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
|
# ============================================================================
|