From ff7814970a94c7f31671049f4248e1830f28e821 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 27 Oct 2026 18:13:22 +0000
Subject: [PATCH 1/2] refactor(python-installer): break down cmd_install into
modular helper functions
Co-authored-by: Tcode-Motion <188012755+Tcode-Motion@users.noreply.github.com>
---
python-installer/LICENSE | 7 +
python-installer/README.md | 224 +++++++++++++++++
python-installer/pyproject.toml | 57 +++++
python-installer/techscript/__init__.py | 17 ++
python-installer/techscript/__main__.py | 225 +++++++++++++++++
python-installer/techscript/downloader.py | 154 ++++++++++++
python-installer/techscript/installer.py | 231 ++++++++++++++++++
python-installer/techscript/launcher.py | 43 ++++
.../techscript/platform_detect.py | 145 +++++++++++
python-installer/tests/__init__.py | 1 +
python-installer/tests/test_installer.py | 130 ++++++++++
python-installer/tests/test_main.py | 85 +++++++
12 files changed, 1319 insertions(+)
create mode 100644 python-installer/LICENSE
create mode 100644 python-installer/README.md
create mode 100644 python-installer/pyproject.toml
create mode 100644 python-installer/techscript/__init__.py
create mode 100644 python-installer/techscript/__main__.py
create mode 100644 python-installer/techscript/downloader.py
create mode 100644 python-installer/techscript/installer.py
create mode 100644 python-installer/techscript/launcher.py
create mode 100644 python-installer/techscript/platform_detect.py
create mode 100644 python-installer/tests/__init__.py
create mode 100644 python-installer/tests/test_installer.py
create mode 100644 python-installer/tests/test_main.py
diff --git a/python-installer/LICENSE b/python-installer/LICENSE
new file mode 100644
index 00000000..5bcba987
--- /dev/null
+++ b/python-installer/LICENSE
@@ -0,0 +1,7 @@
+Apache License 2.0
+
+Copyright 2026 Tcode-Motion and TechScript contributors.
+
+This package is distributed under the Apache License, Version 2.0. The complete license text is available in the repository root LICENSE file and at:
+
+https://www.apache.org/licenses/LICENSE-2.0
diff --git a/python-installer/README.md b/python-installer/README.md
new file mode 100644
index 00000000..07187e70
--- /dev/null
+++ b/python-installer/README.md
@@ -0,0 +1,224 @@
+# TechScript — Python Installer
+
+
+
+[](https://pypi.org/project/techscript/)
+[](https://pypi.org/project/techscript/)
+[](../LICENSE)
+[](https://github.com/Tcode-Motion/techscript)
+
+**The official Python-based installer for the TechScript 2.0 programming language.**
+
+
+
+---
+
+## What This Package Does
+
+This is a **lightweight bootstrapper only** — it contains no compiler binaries.
+
+When you run `techscript install`, it:
+1. Detects your OS and CPU architecture
+2. Fetches the correct native binary from [GitHub Releases](https://github.com/Tcode-Motion/techscript/releases/latest)
+3. Installs it into your PATH
+4. Verifies the installation by running `tsc version`
+
+> **The actual TechScript compiler remains distributed via GitHub Releases.**
+> This PyPI package is under 200 KB and contains only Python code.
+
+---
+
+## Supported Platforms
+
+| Platform | Architecture | Status |
+|---|---|---|
+| Windows | x64 | ✅ Supported |
+| Windows | ARM64 | ✅ Supported |
+| Linux | x64 | ✅ Supported |
+| Linux | ARM64 | ✅ Supported |
+| macOS | Intel (x64) | ✅ Supported |
+| macOS | Apple Silicon (ARM64) | ✅ Supported |
+| Android Termux | ARM64 | ✅ Supported |
+| Android Termux | ARMv7 | 🔄 Optional |
+
+---
+
+## Installation
+
+### Via pip (Recommended)
+
+```bash
+pip install techscript
+techscript install
+```
+
+### Via curl — Linux / macOS
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/Tcode-Motion/techscript/main/scripts/install.sh | bash
+```
+
+### Via installer.exe — Windows
+
+Download `TechScript_Setup.exe` from [GitHub Releases](https://github.com/Tcode-Motion/techscript/releases/latest).
+
+### Via Termux — Android
+
+```bash
+pkg update
+pkg install curl python
+# Option 1: Python installer
+pip install techscript
+techscript install
+
+# Option 2: Shell installer
+curl -fsSL https://raw.githubusercontent.com/Tcode-Motion/techscript/main/scripts/install.sh | bash
+```
+
+### Via Homebrew — macOS *(coming soon)*
+
+```bash
+brew install tcode-motion/tap/techscript
+```
+
+### Via Winget — Windows *(coming soon)*
+
+```powershell
+winget install Tcode-Motion.TechScript
+```
+
+### Via Scoop — Windows *(coming soon)*
+
+```powershell
+scoop install techscript
+```
+
+---
+
+## Usage
+
+### Install the compiler
+
+```bash
+techscript install
+```
+
+### Force reinstall / update
+
+```bash
+techscript install --force
+```
+
+### Check installed version
+
+```bash
+techscript version
+```
+
+### Check for updates
+
+```bash
+techscript check
+```
+
+### Uninstall
+
+```bash
+techscript uninstall
+```
+
+### Forward commands to tsc
+
+```bash
+# These are equivalent:
+techscript run myfile.txs
+tsc run myfile.txs
+```
+
+---
+
+## Quick Start
+
+After installing:
+
+```bash
+# Create a new project
+tsc new hello_world
+cd hello_world
+
+# Run it
+tsc run
+```
+
+Your first `src/main.txs`:
+
+```txs
+say "Hello, World! 🌍"
+name = ask "What is your name? "
+say $"Welcome, {name}! You are running TechScript 2.0."
+```
+
+---
+
+## Install Locations
+
+| Platform | Location |
+|---|---|
+| Windows | `%LOCALAPPDATA%\TechScript\bin\tsc.exe` |
+| Linux / macOS | `~/.local/bin/tsc` |
+| Termux | `$PREFIX/bin/tsc` |
+
+---
+
+## Debug Mode
+
+Enable verbose output and Python tracebacks:
+
+```bash
+techscript install --debug
+```
+
+---
+
+## Requirements
+
+- Python 3.8 or newer
+- Internet access (to download the compiler binary)
+- The `requests` library (installed automatically)
+
+---
+
+## Development
+
+```bash
+git clone https://github.com/Tcode-Motion/techscript
+cd techscript/python-installer
+
+# Install in editable mode
+pip install -e .
+
+# Run tests
+python -m pytest tests/ -v
+
+# Build wheel and sdist
+python -m build
+```
+
+---
+
+## Links
+
+| Resource | Link |
+|---|---|
+| 🌐 Website | [techscript.is-a.dev](https://techscript.is-a.dev) |
+| 📦 GitHub | [Tcode-Motion/techscript](https://github.com/Tcode-Motion/techscript) |
+| 🐍 PyPI | [pypi.org/project/techscript](https://pypi.org/project/techscript/) |
+| 💻 VS Code Extension | [marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=tanmoy.techscript) |
+| 🐛 Issues | [GitHub Issues](https://github.com/Tcode-Motion/techscript/issues) |
+| 💬 Discussions | [GitHub Discussions](https://github.com/Tcode-Motion/techscript/discussions) |
+
+---
+
+## License
+
+Apache License 2.0 — Copyright 2026 [Tcode-Motion](https://github.com/Tcode-Motion)
diff --git a/python-installer/pyproject.toml b/python-installer/pyproject.toml
new file mode 100644
index 00000000..f5dc04b9
--- /dev/null
+++ b/python-installer/pyproject.toml
@@ -0,0 +1,57 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "techscript"
+version = "2.0.0.1"
+description = "Official installer and launcher for the TechScript 2.0 programming language toolchain"
+readme = "README.md"
+license = { file = "LICENSE" }
+requires-python = ">=3.8"
+authors = [
+ { name = "Tcode-Motion", email = "tcode.motion@gmail.com" }
+]
+keywords = ["techscript", "compiler", "installer", "tsc", "programming-language"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Software Development :: Compilers",
+ "Topic :: System :: Installation/Setup",
+]
+dependencies = [
+ "requests>=2.28.0",
+]
+
+[project.urls]
+Homepage = "https://techscript.is-a.dev"
+Repository = "https://github.com/Tcode-Motion/techscript"
+Issues = "https://github.com/Tcode-Motion/techscript/issues"
+Changelog = "https://github.com/Tcode-Motion/techscript/blob/main/CHANGELOG.md"
+
+[project.scripts]
+techscript = "techscript.__main__:main"
+
+[tool.hatch.build.targets.wheel]
+packages = ["techscript"]
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "techscript/",
+ "tests/",
+ "README.md",
+ "LICENSE",
+ "pyproject.toml",
+]
+
+[project.optional-dependencies]
+dev = ["build", "hatchling"]
diff --git a/python-installer/techscript/__init__.py b/python-installer/techscript/__init__.py
new file mode 100644
index 00000000..9eb696be
--- /dev/null
+++ b/python-installer/techscript/__init__.py
@@ -0,0 +1,17 @@
+"""
+TechScript 2.0 — Official Python Installer / Bootstrapper
+
+Detects your platform, downloads the correct native binary from GitHub Releases,
+and installs the `tsc` compiler into the right location on your PATH.
+
+Usage:
+ techscript install # Install or update the compiler
+ techscript install --force # Force reinstall
+ techscript uninstall # Remove the compiler
+ techscript version # Show installed version
+ techscript check # Check for updates
+"""
+
+__version__ = "2.0.0.1"
+__author__ = "Tcode-Motion"
+__repo__ = "https://github.com/Tcode-Motion/techscript"
diff --git a/python-installer/techscript/__main__.py b/python-installer/techscript/__main__.py
new file mode 100644
index 00000000..904c811d
--- /dev/null
+++ b/python-installer/techscript/__main__.py
@@ -0,0 +1,225 @@
+"""
+TechScript CLI entry point.
+
+Usage:
+ techscript install Install or update the compiler
+ techscript install --force Force reinstall
+ techscript uninstall Remove the compiler
+ techscript version Show installed version
+ techscript check Check if an update is available
+ techscript run Forward a command to the installed tsc binary
+"""
+
+import shutil
+import sys
+import os
+from typing import Optional, Tuple, Any
+
+from . import __version__
+from .platform_detect import detect
+from .installer import (
+ _ok, _info, _warn, _err, GREEN, YELLOW, RED, CYAN, BOLD, RESET,
+ find_existing_tsc, prompt_existing_install, install_binary,
+ configure_path, verify_installation, uninstall, get_installed_version,
+)
+from .downloader import (
+ check_internet, get_latest_tag, download_and_extract,
+)
+from .launcher import launch
+
+
+BANNER = f"""\
+{CYAN}{BOLD}
+ ╔══════════════════════════════════════════╗
+ ║ TechScript 2.0 — Installer ║
+ ║ Write like a Human. Run like Rust. ║
+ ╚══════════════════════════════════════════╝
+{RESET}"""
+
+
+def _detect_and_verify_platform() -> Optional[Any]:
+ platform_info = detect()
+ _info(f"Platform detected: {platform_info.display_name}")
+ _info(f"Python : {'.'.join(str(x) for x in platform_info.python_version)}")
+
+ if not platform_info.is_supported:
+ _err(f"Unsupported platform: {platform_info.display_name}")
+ _err("Please open an issue: https://github.com/Tcode-Motion/techscript/issues")
+ return None
+ return platform_info
+
+
+def _handle_existing_install(platform_info: Any, force: bool) -> Optional[int]:
+ existing = find_existing_tsc()
+ if existing and not force:
+ action = prompt_existing_install(existing)
+ if action == "skip":
+ _info("Skipping installation. Existing install kept.")
+ return 0
+ elif action == "uninstall":
+ uninstall(platform_info)
+ return 0
+ print()
+ return None
+
+
+def _download_package(platform_info: Any, debug: bool) -> Tuple[Optional[str], Optional[str]]:
+ _info("Checking internet connectivity...")
+ if not check_internet():
+ _err("No internet connection. Cannot download TechScript.")
+ return None, None
+
+ _info("Fetching latest release information...")
+ tag = get_latest_tag()
+ _info(f"Target release: {tag}")
+
+ asset_name = platform_info.asset_name
+ _info(f"Package : {asset_name}")
+ print()
+
+ try:
+ binary_path, tmp_dir = download_and_extract(
+ tag, asset_name, platform_info.binary_name
+ )
+ return binary_path, tmp_dir
+ except RuntimeError as e:
+ _err(str(e))
+ if debug:
+ raise
+ return None, None
+
+
+def _install_and_configure(binary_path: str, tmp_dir: str, platform_info: Any) -> Optional[str]:
+ try:
+ print()
+ installed_path = install_binary(binary_path, platform_info)
+ configure_path(platform_info, platform_info.install_dir)
+ return installed_path
+ except PermissionError as e:
+ _err(f"Permission denied: {e}")
+ _warn("Try running with elevated privileges, or set a custom install dir.")
+ return None
+ finally:
+ import shutil as _shutil
+ _shutil.rmtree(tmp_dir, ignore_errors=True)
+
+
+def _verify_and_print_result(installed_path: str, platform_info: Any) -> int:
+ print()
+ ok = verify_installation(installed_path)
+
+ print()
+ if ok:
+ print(f" {GREEN}{BOLD}══════════════════════════════════════════{RESET}")
+ print(f" {GREEN}{BOLD} TechScript installed successfully! 🎉 {RESET}")
+ print(f" {GREEN}{BOLD}══════════════════════════════════════════{RESET}")
+ print()
+ print(" Create your first project:")
+ print(f" {CYAN}tsc new hello_world{RESET}")
+ print(f" {CYAN}cd hello_world{RESET}")
+ print(f" {CYAN}tsc run{RESET}")
+ print()
+ if platform_info.install_dir not in os.environ.get("PATH", ""):
+ print(f" {YELLOW}NOTE: Restart your terminal (or run `source ~/.bashrc`) to activate PATH changes.{RESET}")
+ return 0
+ else:
+ _err("Installation verification failed.")
+ _err("Try running: techscript install --force")
+ return 1
+
+
+def cmd_install(force: bool = False, debug: bool = False) -> int:
+ """Download and install the TechScript compiler."""
+ print(BANNER)
+
+ platform_info = _detect_and_verify_platform()
+ if not platform_info:
+ return 1
+
+ existing_res = _handle_existing_install(platform_info, force)
+ if existing_res is not None:
+ return existing_res
+
+ binary_path, tmp_dir = _download_package(platform_info, debug)
+ if not binary_path or not tmp_dir:
+ return 1
+
+ installed_path = _install_and_configure(binary_path, tmp_dir, platform_info)
+ if not installed_path:
+ return 1
+
+ return _verify_and_print_result(installed_path, platform_info)
+
+
+def cmd_uninstall(debug: bool = False) -> int:
+ platform_info = detect()
+ print(BANNER)
+ _info("Uninstalling TechScript...")
+ uninstall(platform_info)
+ return 0
+
+
+def cmd_version() -> int:
+ ver = get_installed_version()
+ if ver:
+ print(ver)
+ else:
+ _warn("TechScript is not installed.")
+ print(" Run: techscript install")
+ return 0
+
+
+def cmd_check() -> int:
+ print(BANNER)
+ _info("Checking for updates...")
+ installed_ver = get_installed_version()
+ latest_tag = get_latest_tag()
+ if installed_ver:
+ _info(f"Installed : {installed_ver}")
+ else:
+ _warn("TechScript is not installed.")
+ _info(f"Latest : {latest_tag}")
+ print()
+ if installed_ver and latest_tag.replace("release-", "v") in installed_ver:
+ _ok("You are up to date!")
+ else:
+ _warn("A newer version may be available.")
+ print(" Run: techscript install to update.")
+ return 0
+
+
+def main(argv=None) -> int:
+ args = argv if argv is not None else sys.argv[1:]
+ debug = "--debug" in args
+ args = [a for a in args if a != "--debug"]
+
+ if not args or args[0] in ("-h", "--help", "help"):
+ print(__doc__)
+ return 0
+
+ cmd = args[0]
+
+ if cmd == "install":
+ force = "--force" in args or "-f" in args
+ return cmd_install(force=force, debug=debug)
+
+ elif cmd == "uninstall":
+ return cmd_uninstall(debug=debug)
+
+ elif cmd in ("version", "--version", "-V"):
+ return cmd_version()
+
+ elif cmd == "check":
+ return cmd_check()
+
+ elif cmd == "run":
+ # Forward: techscript run → tsc run
+ return launch(["run"] + args[1:])
+
+ else:
+ # Forward any other subcommand directly to tsc
+ return launch(args)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/python-installer/techscript/downloader.py b/python-installer/techscript/downloader.py
new file mode 100644
index 00000000..afdba46a
--- /dev/null
+++ b/python-installer/techscript/downloader.py
@@ -0,0 +1,154 @@
+"""
+TechScript downloader module.
+Downloads the correct binary archive from GitHub Releases.
+"""
+
+import hashlib
+import os
+import shutil
+import sys
+import tempfile
+from typing import Optional
+from urllib.request import urlopen, Request
+from urllib.error import URLError, HTTPError
+
+GITHUB_REPO = "Tcode-Motion/techscript"
+RELEASES_API = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
+RELEASES_BASE = f"https://github.com/{GITHUB_REPO}/releases/download"
+FALLBACK_TAG = "release-2.0.0"
+
+# Try to use requests if available; otherwise fall back to urllib
+try:
+ import requests as _requests
+ _HAS_REQUESTS = True
+except ImportError:
+ _HAS_REQUESTS = False
+
+
+def _get(url: str, stream: bool = False):
+ """Unified GET that works with or without the requests library."""
+ if _HAS_REQUESTS:
+ resp = _requests.get(url, stream=stream, timeout=30, headers={"User-Agent": "techscript-installer/2.0"})
+ resp.raise_for_status()
+ return resp
+ # Fallback to urllib
+ req = Request(url, headers={"User-Agent": "techscript-installer/2.0"})
+ return urlopen(req, timeout=30)
+
+
+def check_internet() -> bool:
+ """Return True if a basic network connection is available."""
+ try:
+ urlopen("https://github.com", timeout=5)
+ return True
+ except Exception:
+ return False
+
+
+def get_latest_tag() -> str:
+ """Fetch the latest release tag from the GitHub API."""
+ try:
+ if _HAS_REQUESTS:
+ resp = _requests.get(RELEASES_API, timeout=10, headers={"User-Agent": "techscript-installer/2.0"})
+ resp.raise_for_status()
+ tag = resp.json().get("tag_name", "")
+ else:
+ import json
+ req = Request(RELEASES_API, headers={"User-Agent": "techscript-installer/2.0"})
+ with urlopen(req, timeout=10) as r:
+ tag = json.loads(r.read()).get("tag_name", "")
+
+ # Skip any v1.x tags
+ if tag and not tag.startswith("v1."):
+ return tag
+ except Exception:
+ pass
+ return FALLBACK_TAG
+
+
+def build_download_url(tag: str, asset_name: str) -> str:
+ return f"{RELEASES_BASE}/{tag}/{asset_name}"
+
+
+def download_file(url: str, dest_path: str, show_progress: bool = True) -> None:
+ """Download a file from url to dest_path with optional progress bar."""
+ try:
+ if _HAS_REQUESTS:
+ resp = _requests.get(url, stream=True, timeout=60,
+ headers={"User-Agent": "techscript-installer/2.0"})
+ resp.raise_for_status()
+ total = int(resp.headers.get("content-length", 0))
+ downloaded = 0
+ with open(dest_path, "wb") as f:
+ for chunk in resp.iter_content(chunk_size=65536):
+ f.write(chunk)
+ downloaded += len(chunk)
+ if show_progress and total:
+ pct = int(downloaded * 50 / total)
+ bar = "█" * pct + "░" * (50 - pct)
+ mb = downloaded / 1_048_576
+ total_mb = total / 1_048_576
+ sys.stdout.write(f"\r [{bar}] {mb:.1f}/{total_mb:.1f} MB")
+ sys.stdout.flush()
+ if show_progress:
+ print()
+ else:
+ req = Request(url, headers={"User-Agent": "techscript-installer/2.0"})
+ with urlopen(req, timeout=60) as resp, open(dest_path, "wb") as f:
+ shutil.copyfileobj(resp, f)
+ except HTTPError as e:
+ raise RuntimeError(f"Download failed (HTTP {e.code}): {url}") from e
+ except URLError as e:
+ raise RuntimeError(f"Network error: {e.reason}") from e
+
+
+def extract_archive(archive_path: str, dest_dir: str) -> None:
+ """Extract .tar.gz or .zip archive to dest_dir."""
+ if archive_path.endswith(".tar.gz") or archive_path.endswith(".tgz"):
+ import tarfile
+ with tarfile.open(archive_path, "r:gz") as tf:
+ tf.extractall(dest_dir)
+ elif archive_path.endswith(".zip"):
+ import zipfile
+ with zipfile.ZipFile(archive_path, "r") as zf:
+ zf.extractall(dest_dir)
+ else:
+ raise RuntimeError(f"Unknown archive format: {archive_path}")
+
+
+def find_binary_in_dir(directory: str, binary_name: str) -> Optional[str]:
+ """Recursively find the tsc binary inside an extracted directory."""
+ for root, _dirs, files in os.walk(directory):
+ for fname in files:
+ if fname == binary_name:
+ return os.path.join(root, fname)
+ return None
+
+
+def download_and_extract(tag: str, asset_name: str, binary_name: str,
+ show_progress: bool = True) -> str:
+ """
+ Download the release archive and extract the binary.
+ Returns the path to the extracted binary.
+ """
+ url = build_download_url(tag, asset_name)
+ tmp_dir = tempfile.mkdtemp(prefix="techscript_install_")
+ archive_path = os.path.join(tmp_dir, asset_name)
+
+ try:
+ print(f" Downloading: {url}")
+ download_file(url, archive_path, show_progress=show_progress)
+
+ extract_dir = os.path.join(tmp_dir, "extracted")
+ os.makedirs(extract_dir, exist_ok=True)
+ extract_archive(archive_path, extract_dir)
+
+ binary_path = find_binary_in_dir(extract_dir, binary_name)
+ if not binary_path:
+ raise RuntimeError(
+ f"Binary '{binary_name}' not found inside archive '{asset_name}'."
+ )
+ return binary_path, tmp_dir # caller must clean up tmp_dir
+ except Exception:
+ shutil.rmtree(tmp_dir, ignore_errors=True)
+ raise
diff --git a/python-installer/techscript/installer.py b/python-installer/techscript/installer.py
new file mode 100644
index 00000000..20515a30
--- /dev/null
+++ b/python-installer/techscript/installer.py
@@ -0,0 +1,231 @@
+"""
+TechScript installer module.
+Handles placing the binary, PATH configuration, and verification.
+"""
+
+import os
+import platform
+import shutil
+import stat
+import subprocess
+import sys
+from typing import Optional
+
+from .platform_detect import PlatformInfo, OS
+
+
+# ─── Colours ─────────────────────────────────────────────────────────────────
+
+def _supports_color() -> bool:
+ return sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
+
+_C = _supports_color()
+GREEN = "\033[92m" if _C else ""
+YELLOW = "\033[93m" if _C else ""
+RED = "\033[91m" if _C else ""
+CYAN = "\033[96m" if _C else ""
+BOLD = "\033[1m" if _C else ""
+RESET = "\033[0m" if _C else ""
+
+
+def _ok(msg: str) -> None: print(f" {GREEN}✔{RESET} {msg}")
+def _info(msg: str) -> None: print(f" {CYAN}ℹ{RESET} {msg}")
+def _warn(msg: str) -> None: print(f" {YELLOW}⚠{RESET} {msg}")
+def _err(msg: str) -> None: print(f" {RED}✘{RESET} {msg}")
+
+
+# ─── Existing install detection ───────────────────────────────────────────────
+
+def find_existing_tsc() -> Optional[str]:
+ """Return the path to an existing tsc binary, or None."""
+ return shutil.which("tsc")
+
+
+def get_installed_version(tsc_path: Optional[str] = None) -> Optional[str]:
+ """Return the installed tsc version string, or None."""
+ binary = tsc_path or shutil.which("tsc")
+ if not binary:
+ return None
+ try:
+ result = subprocess.run(
+ [binary, "version"],
+ capture_output=True, text=True, timeout=10
+ )
+ for line in result.stdout.splitlines():
+ if "Compiler Driver" in line or "v2." in line:
+ return line.strip()
+ return result.stdout.strip().splitlines()[0] if result.stdout.strip() else None
+ except Exception:
+ return None
+
+
+def prompt_existing_install(existing_path: str) -> str:
+ """
+ Ask the user what to do when a TechScript install is already found.
+ Returns one of: 'update', 'repair', 'reinstall', 'skip'
+ """
+ current_ver = get_installed_version(existing_path)
+ print(f"\n {YELLOW}{BOLD}TechScript is already installed!{RESET}")
+ print(f" Location: {existing_path}")
+ if current_ver:
+ print(f" Version : {current_ver}")
+ print()
+ print(" What would you like to do?")
+ print(" [1] Update — download the latest version")
+ print(" [2] Repair — redownload and reinstall current version")
+ print(" [3] Reinstall — remove and install fresh")
+ print(" [4] Skip — keep the existing install")
+ print()
+
+ while True:
+ try:
+ choice = input(" Enter choice [1-4]: ").strip()
+ except (EOFError, KeyboardInterrupt):
+ print()
+ return "skip"
+ mapping = {"1": "update", "2": "repair", "3": "reinstall", "4": "skip"}
+ if choice in mapping:
+ return mapping[choice]
+ print(" Please enter 1, 2, 3, or 4.")
+
+
+# ─── Installation ─────────────────────────────────────────────────────────────
+
+def ensure_install_dir(install_dir: str) -> None:
+ """Create the installation directory if it doesn't exist."""
+ os.makedirs(install_dir, exist_ok=True)
+
+
+def install_binary(src_binary: str, platform_info: PlatformInfo,
+ install_dir: Optional[str] = None) -> str:
+ """
+ Copy the binary to the installation directory.
+ Returns the path to the installed binary.
+ """
+ dest_dir = install_dir or platform_info.install_dir
+ ensure_install_dir(dest_dir)
+ dest_path = os.path.join(dest_dir, platform_info.binary_name)
+
+ _info(f"Installing to: {dest_path}")
+ shutil.copy2(src_binary, dest_path)
+
+ # Make executable on Unix
+ if platform_info.os.name != "WINDOWS":
+ st = os.stat(dest_path)
+ os.chmod(dest_path, st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
+
+ return dest_path
+
+
+# ─── PATH configuration ───────────────────────────────────────────────────────
+
+def _dir_in_path(directory: str) -> bool:
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
+ return any(os.path.normcase(d) == os.path.normcase(directory) for d in path_dirs)
+
+
+def configure_path_unix(install_dir: str) -> None:
+ """Append install_dir to the user's shell rc file if not already present."""
+ if _dir_in_path(install_dir):
+ _ok(f"{install_dir} is already in PATH")
+ return
+
+ shell = os.environ.get("SHELL", "")
+ if "zsh" in shell:
+ rc = os.path.expanduser("~/.zshrc")
+ elif "fish" in shell:
+ rc = os.path.expanduser("~/.config/fish/config.fish")
+ else:
+ rc = os.path.expanduser("~/.bashrc")
+
+ export_line = f'\nexport PATH="{install_dir}:$PATH" # TechScript toolchain\n'
+ if os.path.exists(rc):
+ content = open(rc).read()
+ if install_dir not in content:
+ with open(rc, "a") as f:
+ f.write(export_line)
+ _ok(f"Added {install_dir} to PATH in {rc}")
+ else:
+ _warn(f"Shell rc not found. Please add this to your shell profile manually:")
+ _warn(f' export PATH="{install_dir}:$PATH"')
+
+
+def configure_path_windows(install_dir: str) -> None:
+ """Add install_dir to the Windows user PATH via the registry."""
+ if _dir_in_path(install_dir):
+ _ok(f"{install_dir} is already in PATH")
+ return
+ try:
+ import winreg
+ key = winreg.OpenKey(
+ winreg.HKEY_CURRENT_USER,
+ r"Environment",
+ 0, winreg.KEY_READ | winreg.KEY_WRITE
+ )
+ try:
+ existing, _ = winreg.QueryValueEx(key, "Path")
+ except FileNotFoundError:
+ existing = ""
+
+ if install_dir.lower() not in existing.lower():
+ new_path = existing.rstrip(";") + ";" + install_dir
+ winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path)
+ _ok(f"Added {install_dir} to Windows user PATH (restart your terminal to apply)")
+ winreg.CloseKey(key)
+ except Exception as e:
+ _warn(f"Could not update PATH automatically: {e}")
+ _warn(f"Please add this directory to your PATH manually: {install_dir}")
+
+
+def configure_path(platform_info: PlatformInfo, install_dir: str) -> None:
+ if platform_info.os.name == "WINDOWS":
+ configure_path_windows(install_dir)
+ else:
+ configure_path_unix(install_dir)
+
+
+# ─── Verification ─────────────────────────────────────────────────────────────
+
+def verify_installation(installed_path: str) -> bool:
+ """Run tsc version to confirm it works. Returns True on success."""
+ try:
+ result = subprocess.run(
+ [installed_path, "version"],
+ capture_output=True, text=True, timeout=15
+ )
+ if result.returncode == 0:
+ _ok("TechScript installed successfully!")
+ for line in result.stdout.strip().splitlines()[:3]:
+ print(f" {line}")
+ return True
+ else:
+ _err(f"tsc returned exit code {result.returncode}")
+ return False
+ except FileNotFoundError:
+ _err(f"Binary not found at {installed_path}")
+ return False
+ except subprocess.TimeoutExpired:
+ _err("Verification timed out")
+ return False
+ except Exception as e:
+ _err(f"Verification failed: {e}")
+ return False
+
+
+# ─── Uninstall ────────────────────────────────────────────────────────────────
+
+def uninstall(platform_info: PlatformInfo, install_dir: Optional[str] = None) -> None:
+ """Remove the tsc binary from the installation directory."""
+ dest_dir = install_dir or platform_info.install_dir
+ dest_path = os.path.join(dest_dir, platform_info.binary_name)
+
+ if os.path.exists(dest_path):
+ os.remove(dest_path)
+ _ok(f"Removed: {dest_path}")
+ else:
+ existing = shutil.which("tsc")
+ if existing:
+ os.remove(existing)
+ _ok(f"Removed: {existing}")
+ else:
+ _warn("TechScript is not installed — nothing to remove.")
diff --git a/python-installer/techscript/launcher.py b/python-installer/techscript/launcher.py
new file mode 100644
index 00000000..2baeabc1
--- /dev/null
+++ b/python-installer/techscript/launcher.py
@@ -0,0 +1,43 @@
+"""
+TechScript launcher module.
+Used to forward `tsc` commands when invoked through `python -m techscript run`.
+"""
+
+import os
+import shutil
+import subprocess
+import sys
+
+
+def find_tsc() -> str:
+ """Find the tsc binary or raise an error."""
+ tsc = shutil.which("tsc")
+ if not tsc:
+ # Also check the default install locations explicitly
+ candidates = []
+ local_app = os.environ.get("LOCALAPPDATA", "")
+ if local_app:
+ candidates.append(os.path.join(local_app, "TechScript", "bin", "tsc.exe"))
+ candidates.append(os.path.expanduser("~/.local/bin/tsc"))
+ prefix = os.environ.get("PREFIX", "")
+ if prefix:
+ candidates.append(os.path.join(prefix, "bin", "tsc"))
+
+ for path in candidates:
+ if os.path.isfile(path) and os.access(path, os.X_OK):
+ return path
+
+ print("\n ✘ TechScript compiler (tsc) is not installed or not found in PATH.", file=sys.stderr)
+ print(" Run: techscript install", file=sys.stderr)
+ sys.exit(1)
+ return tsc
+
+
+def launch(args: list) -> int:
+ """Forward args to the tsc binary and return the exit code."""
+ tsc = find_tsc()
+ try:
+ result = subprocess.run([tsc] + args)
+ return result.returncode
+ except KeyboardInterrupt:
+ return 130
diff --git a/python-installer/techscript/platform_detect.py b/python-installer/techscript/platform_detect.py
new file mode 100644
index 00000000..9a5d0043
--- /dev/null
+++ b/python-installer/techscript/platform_detect.py
@@ -0,0 +1,145 @@
+"""
+TechScript platform detection module.
+Identifies OS, CPU architecture, Termux environment, and Python version.
+"""
+
+from __future__ import annotations
+
+import os
+import platform
+import posixpath
+import struct
+import sys
+from dataclasses import dataclass
+from enum import Enum, auto
+from typing import Optional
+
+
+class OS(Enum):
+ WINDOWS = auto()
+ LINUX = auto()
+ MACOS = auto()
+ UNKNOWN = auto()
+
+
+class Arch(Enum):
+ X64 = auto()
+ ARM64 = auto()
+ ARMV7 = auto()
+ UNKNOWN = auto()
+
+
+@dataclass
+class PlatformInfo:
+ os: OS
+ arch: Arch
+ is_termux: bool
+ termux_prefix: Optional[str]
+ python_version: tuple
+
+ @property
+ def display_name(self) -> str:
+ os_names = {OS.WINDOWS: "Windows", OS.LINUX: "Linux", OS.MACOS: "macOS", OS.UNKNOWN: "Unknown"}
+ arch_names = {Arch.X64: "x64", Arch.ARM64: "ARM64", Arch.ARMV7: "ARMv7", Arch.UNKNOWN: "Unknown"}
+ termux_tag = " (Termux)" if self.is_termux else ""
+ return f"{os_names[self.os]} {arch_names[self.arch]}{termux_tag}"
+
+ @property
+ def is_supported(self) -> bool:
+ return self.asset_name is not None
+
+ @property
+ def asset_name(self) -> Optional[str]:
+ """Return the GitHub Release asset filename for this platform."""
+ # Termux on Android
+ if self.is_termux:
+ if self.arch == Arch.ARM64:
+ return "techscript-linux-arm64.tar.gz"
+ elif self.arch == Arch.ARMV7:
+ return "techscript-linux-armv7.tar.gz"
+ return None
+
+ if self.os == OS.WINDOWS:
+ if self.arch == Arch.X64:
+ return "techscript-windows-x64.zip"
+ elif self.arch == Arch.ARM64:
+ return "techscript-windows-arm64.zip"
+
+ elif self.os == OS.LINUX:
+ if self.arch == Arch.X64:
+ return "techscript-linux-x64.tar.gz"
+ elif self.arch == Arch.ARM64:
+ return "techscript-linux-arm64.tar.gz"
+
+ elif self.os == OS.MACOS:
+ if self.arch == Arch.ARM64:
+ return "techscript-macos-arm64.tar.gz"
+ elif self.arch == Arch.X64:
+ return "techscript-macos-x64.tar.gz"
+
+ return None
+
+ @property
+ def install_dir(self) -> str:
+ """Return the default installation directory for this platform."""
+ if self.is_termux and self.termux_prefix:
+ return posixpath.join(self.termux_prefix, "bin")
+ if self.os == OS.WINDOWS:
+ local_app_data = os.environ.get("LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local"))
+ return os.path.join(local_app_data, "TechScript", "bin")
+ return posixpath.join(os.path.expanduser("~"), ".local", "bin")
+
+ @property
+ def binary_name(self) -> str:
+ """Return the name of the tsc binary file."""
+ return "tsc.exe" if self.os == OS.WINDOWS else "tsc"
+
+
+def _detect_os() -> OS:
+ system = platform.system().lower()
+ if system == "windows":
+ return OS.WINDOWS
+ elif system == "linux":
+ return OS.LINUX
+ elif system == "darwin":
+ return OS.MACOS
+ return OS.UNKNOWN
+
+
+def _detect_arch() -> Arch:
+ machine = platform.machine().lower()
+ # Normalize common aliases
+ if machine in ("x86_64", "amd64", "x64"):
+ return Arch.X64
+ elif machine in ("aarch64", "arm64"):
+ return Arch.ARM64
+ elif machine.startswith("armv7") or machine == "armhf":
+ return Arch.ARMV7
+ # Fallback: check pointer size
+ if struct.calcsize("P") == 8:
+ return Arch.X64
+ return Arch.UNKNOWN
+
+
+def _detect_termux() -> tuple[bool, Optional[str]]:
+ """Detect if running inside Termux on Android."""
+ prefix = os.environ.get("PREFIX", "")
+ if "com.termux" in prefix or "termux" in prefix.lower():
+ return True, prefix
+ # Also check for ANDROID_DATA which is always set on Android
+ if os.path.exists("/data/data/com.termux"):
+ termux_prefix = os.environ.get("PREFIX", "/data/data/com.termux/files/usr")
+ return True, termux_prefix
+ return False, None
+
+
+def detect() -> PlatformInfo:
+ """Detect the current platform and return a PlatformInfo object."""
+ is_termux, termux_prefix = _detect_termux()
+ return PlatformInfo(
+ os=_detect_os(),
+ arch=_detect_arch(),
+ is_termux=is_termux,
+ termux_prefix=termux_prefix,
+ python_version=sys.version_info[:3],
+ )
diff --git a/python-installer/tests/__init__.py b/python-installer/tests/__init__.py
new file mode 100644
index 00000000..35d55f5e
--- /dev/null
+++ b/python-installer/tests/__init__.py
@@ -0,0 +1 @@
+# Tests package init
diff --git a/python-installer/tests/test_installer.py b/python-installer/tests/test_installer.py
new file mode 100644
index 00000000..86ab1b7d
--- /dev/null
+++ b/python-installer/tests/test_installer.py
@@ -0,0 +1,130 @@
+"""Tests for TechScript Python installer package."""
+
+import os
+import sys
+import unittest
+from unittest.mock import patch, MagicMock
+
+# Ensure the package is importable
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+from techscript.platform_detect import (
+ detect, OS, Arch, PlatformInfo, _detect_os, _detect_arch, _detect_termux
+)
+from techscript.downloader import build_download_url, FALLBACK_TAG
+
+
+class TestPlatformDetection(unittest.TestCase):
+
+ def test_detect_returns_platform_info(self):
+ info = detect()
+ self.assertIsInstance(info, PlatformInfo)
+ self.assertIn(info.os, list(OS))
+ self.assertIn(info.arch, list(Arch))
+ self.assertIsInstance(info.is_termux, bool)
+
+ def test_windows_x64_asset(self):
+ info = PlatformInfo(OS.WINDOWS, Arch.X64, False, None, (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-windows-x64.zip")
+
+ def test_windows_arm64_asset(self):
+ info = PlatformInfo(OS.WINDOWS, Arch.ARM64, False, None, (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-windows-arm64.zip")
+
+ def test_linux_x64_asset(self):
+ info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-linux-x64.tar.gz")
+
+ def test_linux_arm64_asset(self):
+ info = PlatformInfo(OS.LINUX, Arch.ARM64, False, None, (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-linux-arm64.tar.gz")
+
+ def test_macos_x64_asset(self):
+ info = PlatformInfo(OS.MACOS, Arch.X64, False, None, (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-macos-x64.tar.gz")
+
+ def test_macos_arm64_asset(self):
+ info = PlatformInfo(OS.MACOS, Arch.ARM64, False, None, (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-macos-arm64.tar.gz")
+
+ def test_termux_arm64_asset(self):
+ info = PlatformInfo(OS.LINUX, Arch.ARM64, True, "/data/data/com.termux/files/usr", (3, 11, 0))
+ self.assertEqual(info.asset_name, "techscript-linux-arm64.tar.gz")
+
+ def test_unknown_platform_unsupported(self):
+ info = PlatformInfo(OS.UNKNOWN, Arch.UNKNOWN, False, None, (3, 11, 0))
+ self.assertFalse(info.is_supported)
+
+ def test_display_name_windows(self):
+ info = PlatformInfo(OS.WINDOWS, Arch.X64, False, None, (3, 11, 0))
+ self.assertIn("Windows", info.display_name)
+ self.assertIn("x64", info.display_name)
+
+ def test_display_name_termux(self):
+ info = PlatformInfo(OS.LINUX, Arch.ARM64, True, "/prefix", (3, 11, 0))
+ self.assertIn("Termux", info.display_name)
+
+
+class TestInstallDir(unittest.TestCase):
+
+ def test_windows_install_dir(self):
+ with patch.dict(os.environ, {"LOCALAPPDATA": "C:\\Users\\Test\\AppData\\Local"}):
+ info = PlatformInfo(OS.WINDOWS, Arch.X64, False, None, (3, 11, 0))
+ self.assertIn("TechScript", info.install_dir)
+ self.assertIn("bin", info.install_dir)
+
+ def test_linux_install_dir(self):
+ info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ self.assertIn(".local/bin", info.install_dir)
+
+ def test_termux_install_dir(self):
+ prefix = "/data/data/com.termux/files/usr"
+ info = PlatformInfo(OS.LINUX, Arch.ARM64, True, prefix, (3, 11, 0))
+ self.assertEqual(info.install_dir, f"{prefix}/bin")
+
+
+class TestBinaryName(unittest.TestCase):
+
+ def test_windows_binary_name(self):
+ info = PlatformInfo(OS.WINDOWS, Arch.X64, False, None, (3, 11, 0))
+ self.assertEqual(info.binary_name, "tsc.exe")
+
+ def test_linux_binary_name(self):
+ info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ self.assertEqual(info.binary_name, "tsc")
+
+ def test_macos_binary_name(self):
+ info = PlatformInfo(OS.MACOS, Arch.ARM64, False, None, (3, 11, 0))
+ self.assertEqual(info.binary_name, "tsc")
+
+
+class TestDownloadURL(unittest.TestCase):
+
+ def test_build_url_linux(self):
+ url = build_download_url("release-2.0.0", "techscript-linux-x64.tar.gz")
+ self.assertIn("release-2.0.0", url)
+ self.assertIn("techscript-linux-x64.tar.gz", url)
+ self.assertIn("github.com", url)
+
+ def test_fallback_tag_format(self):
+ self.assertTrue(FALLBACK_TAG.startswith("release-") or FALLBACK_TAG.startswith("v"))
+
+
+class TestTermuxDetection(unittest.TestCase):
+
+ def test_no_termux(self):
+ with patch.dict(os.environ, {"PREFIX": ""}, clear=False):
+ is_termux, prefix = _detect_termux()
+ # Can't guarantee False in all CI environments, just check types
+ self.assertIsInstance(is_termux, bool)
+
+ def test_termux_prefix_env(self):
+ fake_prefix = "/data/data/com.termux/files/usr"
+ with patch.dict(os.environ, {"PREFIX": fake_prefix}):
+ is_termux, prefix = _detect_termux()
+ self.assertTrue(is_termux)
+ self.assertEqual(prefix, fake_prefix)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/python-installer/tests/test_main.py b/python-installer/tests/test_main.py
new file mode 100644
index 00000000..6fcaa136
--- /dev/null
+++ b/python-installer/tests/test_main.py
@@ -0,0 +1,85 @@
+"""Tests for techscript.__main__ CLI module."""
+
+import os
+import sys
+import unittest
+from unittest.mock import patch, MagicMock
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+from techscript.__main__ import (
+ cmd_install,
+ _detect_and_verify_platform,
+ _handle_existing_install,
+ _download_package,
+ _install_and_configure,
+ _verify_and_print_result,
+ main,
+)
+from techscript.platform_detect import PlatformInfo, OS, Arch
+
+
+class TestMainCLI(unittest.TestCase):
+
+ @patch("techscript.__main__.detect")
+ def test_detect_and_verify_platform_supported(self, mock_detect):
+ mock_info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ mock_detect.return_value = mock_info
+ info = _detect_and_verify_platform()
+ self.assertEqual(info, mock_info)
+
+ @patch("techscript.__main__.detect")
+ def test_detect_and_verify_platform_unsupported(self, mock_detect):
+ mock_info = PlatformInfo(OS.UNKNOWN, Arch.UNKNOWN, False, None, (3, 11, 0))
+ mock_detect.return_value = mock_info
+ info = _detect_and_verify_platform()
+ self.assertIsNone(info)
+
+ @patch("techscript.__main__.find_existing_tsc")
+ def test_handle_existing_install_skip(self, mock_find):
+ mock_find.return_value = "/usr/local/bin/tsc"
+ mock_info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ with patch("techscript.__main__.prompt_existing_install", return_value="skip"):
+ res = _handle_existing_install(mock_info, force=False)
+ self.assertEqual(res, 0)
+
+ @patch("techscript.__main__.find_existing_tsc")
+ def test_handle_existing_install_force(self, mock_find):
+ mock_find.return_value = "/usr/local/bin/tsc"
+ mock_info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ res = _handle_existing_install(mock_info, force=True)
+ self.assertIsNone(res)
+
+ @patch("techscript.__main__.check_internet", return_value=False)
+ def test_download_package_no_internet(self, mock_check):
+ mock_info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ bin_path, tmp_dir = _download_package(mock_info, debug=False)
+ self.assertIsNone(bin_path)
+ self.assertIsNone(tmp_dir)
+
+ @patch("techscript.__main__._detect_and_verify_platform")
+ @patch("techscript.__main__._handle_existing_install")
+ @patch("techscript.__main__._download_package")
+ @patch("techscript.__main__._install_and_configure")
+ @patch("techscript.__main__._verify_and_print_result")
+ def test_cmd_install_success(
+ self, mock_verify, mock_install, mock_download, mock_existing, mock_detect
+ ):
+ mock_info = PlatformInfo(OS.LINUX, Arch.X64, False, None, (3, 11, 0))
+ mock_detect.return_value = mock_info
+ mock_existing.return_value = None
+ mock_download.return_value = ("/tmp/tsc", "/tmp/dir")
+ mock_install.return_value = "/usr/local/bin/tsc"
+ mock_verify.return_value = 0
+
+ exit_code = cmd_install(force=False, debug=False)
+ self.assertEqual(exit_code, 0)
+
+ def test_main_help(self):
+ with patch("sys.stdout"):
+ code = main(["--help"])
+ self.assertEqual(code, 0)
+
+
+if __name__ == "__main__":
+ unittest.main()
From 405500cc90cb0b3b0d7894961ca6fdd4d8a575c6 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 27 Oct 2026 19:35:30 +0000
Subject: [PATCH 2/2] refactor(python-installer): break down cmd_install into
modular helper functions
Co-authored-by: Tcode-Motion <188012755+Tcode-Motion@users.noreply.github.com>