diff --git a/.gitignore b/.gitignore index 13d59ed..a857e36 100644 --- a/.gitignore +++ b/.gitignore @@ -51,9 +51,8 @@ node_modules/ # OTHER .just/ journalapp.exe -Journal.App/node_modules.old/@rollup/.rollup-win32-x64-msvc-IjiZshxL/rollup.win32-x64-msvc.node journalapp(1).exe .cache/ -scripts/__pycache__/ +Journal.DevTool/scripts/__pycache__/ .sdt/ devtool.backup.json \ No newline at end of file diff --git a/Journal.slnx b/Journal.slnx index 7f1be05..5e5cfd3 100644 --- a/Journal.slnx +++ b/Journal.slnx @@ -3,6 +3,5 @@ - diff --git a/scripts/_pwsh-python-shim.ps1 b/scripts/_pwsh-python-shim.ps1 deleted file mode 100644 index b07c4dc..0000000 --- a/scripts/_pwsh-python-shim.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Resolve-SdtPython { - $candidates = @('python') - if ($IsWindows) { $candidates += 'py' } else { $candidates += 'python3' } - foreach ($c in $candidates) { - try { - & $c --version *> $null - if ($LASTEXITCODE -eq 0) { return $c } - } catch {} - } - return 'python' -} - -function Resolve-SdtScriptPath { - param([Parameter(Mandatory=$true)][string]$ScriptName) - - $bundled = Join-Path $PSScriptRoot $ScriptName - if (Test-Path $bundled) { return $bundled } - - $project = Join-Path (Join-Path $PSScriptRoot '..') ('scripts\\' + $ScriptName) - if (Test-Path $project) { return (Resolve-Path $project).Path } - - throw "Python helper script not found: $ScriptName" -} - -function Invoke-SdtPythonScript { - param( - [Parameter(Mandatory=$true)][string]$ScriptName, - [string[]]$ForwardArgs = @() - ) - - $python = Resolve-SdtPython - $scriptPath = Resolve-SdtScriptPath -ScriptName $ScriptName - - & $python $scriptPath @ForwardArgs - exit $LASTEXITCODE -} diff --git a/scripts/build.py b/scripts/build.py deleted file mode 100644 index d71b5cc..0000000 --- a/scripts/build.py +++ /dev/null @@ -1,419 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import hashlib -import json -import os -import pathlib -import shutil -import subprocess -import sys -import time -from script_common import resolve_command - - -def run_step(command, args, cwd): - resolved = resolve_command(command) - if shutil.which(resolved) is None and not pathlib.Path(resolved).exists(): - return { - "command": resolved, - "args": args, - "cwd": cwd, - "exit_code": 127, - "elapsed_seconds": 0.0, - "status": "failed", - "failure_reason": f"command_not_found:{resolved}", - } - - started = time.time() - proc = subprocess.run([resolved, *args], cwd=cwd, check=False) - elapsed = round(time.time() - started, 3) - return { - "command": resolved, - "args": args, - "cwd": cwd, - "exit_code": proc.returncode, - "elapsed_seconds": elapsed, - "status": "ok" if proc.returncode == 0 else "failed", - "failure_reason": None if proc.returncode == 0 else f"non_zero_exit:{proc.returncode}", - } - - -def resolve_python_executable(): - candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"] - for c in candidates: - if shutil.which(c): - return c - return "python" - - -def parse_common(parser): - parser.add_argument("--project-root", required=True) - parser.add_argument("--working-dir", default=".") - parser.add_argument("--json", action="store_true") - - -def resolve_cwd(project_root, working_dir): - return os.path.abspath(os.path.join(project_root, working_dir)) - - -EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"} - - -def discover_dotnet_target(project_root: str, cwd: str): - # Prefer nearest/top-level solution from cwd, then csproj, then bounded scan from project root. - local_sln = sorted(pathlib.Path(cwd).glob("*.sln")) - if len(local_sln) == 1: - return str(local_sln[0]) - - local_csproj = sorted(pathlib.Path(cwd).glob("*.csproj")) - if len(local_csproj) == 1: - return str(local_csproj[0]) - - sln_hits = bounded_find_files(project_root, ".sln", max_depth=4) - if len(sln_hits) == 1: - return sln_hits[0] - - csproj_hits = bounded_find_files(project_root, ".csproj", max_depth=4) - if len(csproj_hits) == 1: - return csproj_hits[0] - - return None - - -def bounded_find_files(root: str, extension: str, max_depth: int): - root_path = pathlib.Path(root).resolve() - results = [] - for current_root, dirs, files in os.walk(root_path): - rel = pathlib.Path(current_root).resolve().relative_to(root_path) - depth = len(rel.parts) - dirs[:] = [d for d in dirs if d not in EXCLUDED_SCAN_DIRS] - if depth > max_depth: - dirs[:] = [] - continue - - for name in files: - if name.lower().endswith(extension.lower()): - results.append(str(pathlib.Path(current_root) / name)) - return sorted(results) - - -def run_dotnet_action(project_root, working_dir, verb): - cwd = resolve_cwd(project_root, working_dir) - args = [verb] - target = discover_dotnet_target(project_root, cwd) - if target: - args.append(target) - step = run_step("dotnet", args, cwd) - if target: - step["resolved_target"] = target - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def _deps_hash(app_root): - h = hashlib.sha256() - for name in ("package.json", "package-lock.json"): - p = pathlib.Path(app_root) / name - if p.exists(): - h.update(p.read_bytes()) - return h.hexdigest() - - -def ensure_npm_dependencies(app_root): - node_modules = pathlib.Path(app_root) / "node_modules" - deps_hash_file = node_modules / ".sdt-deps.sha256" - expected = _deps_hash(app_root) - - should_install = not node_modules.exists() - if not should_install: - if not deps_hash_file.exists(): - should_install = True - else: - current = deps_hash_file.read_text(encoding="utf-8").strip() - should_install = current != expected - - if not should_install: - return {"installed": False, "reason": "deps_unchanged"} - - lock_exists = (pathlib.Path(app_root) / "package-lock.json").exists() - install_args = ["ci", "--no-audit", "--fund=false"] if lock_exists else ["install", "--no-audit", "--fund=false"] - install_step = run_step("npm", install_args, app_root) - if install_step["exit_code"] != 0: - if lock_exists and install_args[0] == "ci": - fallback = run_step("npm", ["install", "--no-audit", "--fund=false"], app_root) - if fallback["exit_code"] != 0: - fallback["failure_reason"] = "deps_install_failed_after_ci_fallback" - return {"installed": True, "reason": "install_failed", "step": fallback} - install_step = fallback - else: - return {"installed": True, "reason": "install_failed", "step": install_step} - - node_modules.mkdir(parents=True, exist_ok=True) - deps_hash_file.write_text(expected, encoding="utf-8") - return {"installed": True, "reason": "installed", "step": install_step} - - -def action_dotnet_build(args): - return run_dotnet_action(args.project_root, args.working_dir, "build") - - -def action_dotnet_restore(args): - return run_dotnet_action(args.project_root, args.working_dir, "restore") - - -def action_dotnet_test(args): - return run_dotnet_action(args.project_root, args.working_dir, "test") - - -def action_dotnet_publish(args): - return run_dotnet_action(args.project_root, args.working_dir, "publish") - - -def action_npm_install(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("npm", ["install"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_npm_ci(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("npm", ["ci"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_npm_build(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - deps = ensure_npm_dependencies(cwd) - if deps.get("reason") == "install_failed": - step = deps["step"] - step["failure_reason"] = "deps_install_failed" - return step["exit_code"], step - step = run_step("npm", ["run", "build"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_npm_test(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - deps = ensure_npm_dependencies(cwd) - if deps.get("reason") == "install_failed": - step = deps["step"] - step["failure_reason"] = "deps_install_failed" - return step["exit_code"], step - step = run_step("npm", ["test"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_npm_audit(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("npm", ["audit"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_python_venv_create(args): - cwd = resolve_cwd(args.project_root, ".") - venv_dir = args.venv_dir or ".venv" - step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_python_pip_install(args): - cwd = resolve_cwd(args.project_root, ".") - req = args.requirements - step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_python_pip_sync(args): - cwd = resolve_cwd(args.project_root, ".") - req = args.requirements - step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_python_pytest(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_cargo_build(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("cargo", ["build"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_cargo_test(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("cargo", ["test"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_tauri_build(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - deps = ensure_npm_dependencies(cwd) - if deps.get("reason") == "install_failed": - step = deps["step"] - step["failure_reason"] = "deps_install_failed" - return step["exit_code"], step - - tauri_args = ["run", "tauri", "build"] - if args.no_bundle: - tauri_args.extend(["--", "--no-bundle"]) - step = run_step("npm", tauri_args, cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_git_status(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("git", ["status"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_git_fetch(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("git", ["fetch"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_git_pull(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("git", ["pull"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_git_clean(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("git", ["clean", "-fd"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_docker_build(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("docker", ["build", "."], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_docker_compose_up(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("docker", ["compose", "up", "-d"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def action_docker_compose_down(args): - cwd = resolve_cwd(args.project_root, args.working_dir) - step = run_step("docker", ["compose", "down"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step - - -def main(): - parser = argparse.ArgumentParser(description="SDT normalized build actions") - sub = parser.add_subparsers(dest="action", required=True) - - p0 = sub.add_parser("dotnet-restore") - parse_common(p0) - - p1 = sub.add_parser("dotnet-build") - parse_common(p1) - - p1b = sub.add_parser("dotnet-test") - parse_common(p1b) - - p1c = sub.add_parser("dotnet-publish") - parse_common(p1c) - - p2 = sub.add_parser("npm-install") - parse_common(p2) - - p2b = sub.add_parser("npm-ci") - parse_common(p2b) - - p3 = sub.add_parser("npm-build") - parse_common(p3) - - p3b = sub.add_parser("npm-test") - parse_common(p3b) - - p3c = sub.add_parser("npm-audit") - parse_common(p3c) - - p4 = sub.add_parser("python-venv-create") - parse_common(p4) - p4.add_argument("--venv-dir", default=".venv") - - p5 = sub.add_parser("python-pip-install") - parse_common(p5) - p5.add_argument("--requirements", required=True) - - p5b = sub.add_parser("python-pip-sync") - parse_common(p5b) - p5b.add_argument("--requirements", required=True) - - p5c = sub.add_parser("python-pytest") - parse_common(p5c) - - p6 = sub.add_parser("cargo-build") - parse_common(p6) - - p6b = sub.add_parser("cargo-test") - parse_common(p6b) - - p7 = sub.add_parser("tauri-build") - parse_common(p7) - p7.add_argument("--no-bundle", action="store_true") - - p8 = sub.add_parser("git-status") - parse_common(p8) - - p9 = sub.add_parser("git-fetch") - parse_common(p9) - - p10 = sub.add_parser("git-pull") - parse_common(p10) - - p11 = sub.add_parser("git-clean") - parse_common(p11) - - p12 = sub.add_parser("docker-build") - parse_common(p12) - - p13 = sub.add_parser("docker-compose-up") - parse_common(p13) - - p14 = sub.add_parser("docker-compose-down") - parse_common(p14) - - args = parser.parse_args() - - handlers = { - "dotnet-restore": action_dotnet_restore, - "dotnet-build": action_dotnet_build, - "dotnet-test": action_dotnet_test, - "dotnet-publish": action_dotnet_publish, - "npm-install": action_npm_install, - "npm-ci": action_npm_ci, - "npm-build": action_npm_build, - "npm-test": action_npm_test, - "npm-audit": action_npm_audit, - "python-venv-create": action_python_venv_create, - "python-pip-install": action_python_pip_install, - "python-pip-sync": action_python_pip_sync, - "python-pytest": action_python_pytest, - "cargo-build": action_cargo_build, - "cargo-test": action_cargo_test, - "tauri-build": action_tauri_build, - "git-status": action_git_status, - "git-fetch": action_git_fetch, - "git-pull": action_git_pull, - "git-clean": action_git_clean, - "docker-build": action_docker_build, - "docker-compose-up": action_docker_compose_up, - "docker-compose-down": action_docker_compose_down, - } - - code, summary = handlers[args.action](args) - if args.json: - print(json.dumps(summary)) - return code - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/dev-shell.cmd b/scripts/dev-shell.cmd deleted file mode 100644 index b1614b7..0000000 --- a/scripts/dev-shell.cmd +++ /dev/null @@ -1,17 +0,0 @@ -@echo off -set "SCRIPT_DIR=%~dp0" - -where py >nul 2>nul -if %ERRORLEVEL%==0 ( - set "PYEXE=py" -) else ( - where python >nul 2>nul - if not %ERRORLEVEL%==0 ( - echo python not found. - exit /b 1 - ) - set "PYEXE=python" -) - -for /f "usebackq delims=" %%L in (`"%PYEXE%" "%SCRIPT_DIR%dev_shell.py" export --shell cmd`) do %%L -echo Development shell initialized from Python bootstrap script. diff --git a/scripts/dev-shell.ps1 b/scripts/dev-shell.ps1 deleted file mode 100644 index 7f4a3f7..0000000 --- a/scripts/dev-shell.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -# Run this in PowerShell before development commands: -# . ./scripts/dev-shell.ps1 - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1') - -$scriptPath = Resolve-SdtScriptPath -ScriptName 'dev_shell.py' -$python = Resolve-SdtPython - -$lines = & $python $scriptPath export --shell pwsh -if ($LASTEXITCODE -ne 0) { - throw "Failed to initialize development shell via dev_shell.py" -} - -foreach ($line in $lines) { - Invoke-Expression $line -} - -Write-Host "Development shell initialized from Python bootstrap script." diff --git a/scripts/dev-shell.sh b/scripts/dev-shell.sh deleted file mode 100644 index 83468f7..0000000 --- a/scripts/dev-shell.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env sh -set -eu - -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" - -if command -v python3 >/dev/null 2>&1; then - PYTHON_EXE="python3" -elif command -v python >/dev/null 2>&1; then - PYTHON_EXE="python" -else - echo "python3/python not found." >&2 - exit 1 -fi - -eval "$("$PYTHON_EXE" "$SCRIPT_DIR/dev_shell.py" export --shell bash)" -echo "Development shell initialized from Python bootstrap script." diff --git a/scripts/dev_shell.py b/scripts/dev_shell.py deleted file mode 100644 index 1a5d8ea..0000000 --- a/scripts/dev_shell.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import json -import pathlib -import sys - -from script_common import PROXY_VARS, clean_proxy_env, dotnet_env, ensure_dirs, pip_env, resolve_repo_root - - -def huggingface_env(repo_root: pathlib.Path) -> dict[str, str]: - env = {} - hf_home = repo_root / ".cache" / "huggingface" - hf_hub_cache = hf_home / "hub" - ensure_dirs([hf_hub_cache]) - env["HF_HOME"] = str(hf_home) - env["HUGGINGFACE_HUB_CACHE"] = str(hf_hub_cache) - env["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" - return env - - -def resolved_env(repo_root: pathlib.Path) -> dict[str, str]: - env = {} - dotnet = dotnet_env(repo_root) - pip = pip_env(repo_root) - hf = huggingface_env(repo_root) - - dotnet_keys = [ - "DOTNET_CLI_HOME", - "NUGET_PACKAGES", - "NUGET_HTTP_CACHE_PATH", - "DOTNET_SKIP_FIRST_TIME_EXPERIENCE", - "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH", - "DOTNET_GENERATE_ASPNET_CERTIFICATE", - "DOTNET_CLI_TELEMETRY_OPTOUT", - "NUGET_CERT_REVOCATION_MODE", - ] - pip_keys = [ - "PIP_CACHE_DIR", - "PIP_DISABLE_PIP_VERSION_CHECK", - "PIP_DEFAULT_TIMEOUT", - "PIP_RETRIES", - "TEMP", - "TMP", - ] - for key in dotnet_keys: - env[key] = dotnet[key] - for key in pip_keys: - env[key] = pip[key] - env.update(hf) - clean_proxy_env(env) - return env - - -def export_lines(shell: str, env_map: dict[str, str]) -> list[str]: - def sh_quote(value: str) -> str: - return "'" + value.replace("'", "'\"'\"'") + "'" - - if shell == "pwsh": - lines = [f"Remove-Item Env:{k} -ErrorAction SilentlyContinue" for k in PROXY_VARS] - lines.extend(f"$env:{k} = \"{v.replace('\"', '`\"')}\"" for k, v in env_map.items()) - return lines - if shell in ("bash", "zsh"): - lines = [f"unset {k}" for k in PROXY_VARS] - lines.extend(f"export {k}={sh_quote(v)}" for k, v in env_map.items()) - return lines - if shell == "cmd": - lines = [f"set {k}=" for k in PROXY_VARS] - lines.extend(f"set {k}={v}" for k, v in env_map.items()) - return lines - raise ValueError(shell) - - -def cmd_export(args): - try: - repo_root = resolve_repo_root(args.project_root) - except Exception as ex: - print(f"Failed to resolve project root: {ex}", file=sys.stderr) - return 2 - - env_map = resolved_env(repo_root) - payload = { - "projectRoot": str(repo_root), - "env": env_map, - "createdDirs": [ - str(repo_root / ".dotnet_home"), - str(repo_root / ".nuget" / "packages"), - str(repo_root / ".nuget" / "http-cache"), - str(repo_root / ".pip" / "cache"), - str(repo_root / ".tmp" / "pip-temp"), - str(repo_root / ".cache" / "huggingface" / "hub"), - ], - "warnings": [], - } - - try: - lines = export_lines(args.shell, env_map) - except ValueError: - print(f"Unsupported shell target: {args.shell}", file=sys.stderr) - return 3 - - if args.json: - print(json.dumps(payload)) - else: - for line in lines: - print(line) - return 0 - - -def cmd_doctor(args): - try: - repo_root = resolve_repo_root(args.project_root) - except Exception as ex: - print(f"Failed to resolve project root: {ex}", file=sys.stderr) - return 2 - - env_map = resolved_env(repo_root) - checks = { - "repo_root": str(repo_root), - "dotnet_home_exists": (repo_root / ".dotnet_home").exists(), - "nuget_cache_exists": (repo_root / ".nuget" / "packages").exists(), - "pip_cache_exists": (repo_root / ".pip" / "cache").exists(), - "hf_cache_exists": (repo_root / ".cache" / "huggingface" / "hub").exists(), - "env_count": len(env_map), - } - print(json.dumps(checks)) - return 0 - - -def main(): - parser = argparse.ArgumentParser(description="SDT cross-platform shell bootstrap helper") - sub = parser.add_subparsers(dest="command", required=True) - - p_export = sub.add_parser("export", help="Print env exports for a shell") - p_export.add_argument("--shell", required=True) - p_export.add_argument("--project-root") - p_export.add_argument("--json", action="store_true") - - p_doctor = sub.add_parser("doctor", help="Validate env bootstrap paths") - p_doctor.add_argument("--project-root") - - args = parser.parse_args() - if args.command == "export": - return cmd_export(args) - return cmd_doctor(args) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/diag.py b/scripts/diag.py deleted file mode 100644 index 20bf41b..0000000 --- a/scripts/diag.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import json -import os -import platform -import shutil -import subprocess -import sys -from script_common import resolve_command - - -def run_capture(cmd): - try: - proc = subprocess.run(cmd, capture_output=True, text=True, check=False) - out = (proc.stdout or "").strip() - err = (proc.stderr or "").strip() - text = out if out else err - return proc.returncode == 0, text - except Exception as ex: - return False, str(ex) - - -def probe_tool(tool): - mapping = { - "dotnet": ["dotnet", "--version"], - "node": ["node", "--version"], - "npm": ["npm", "--version"], - "python": ["python", "--version"], - "cargo": ["cargo", "--version"], - "tauri": ["tauri", "--version"], - "git": ["git", "--version"], - "docker": ["docker", "--version"], - } - cmd = mapping.get(tool, [tool, "--version"]) - resolved = resolve_command(cmd[0]) - if shutil.which(resolved) is None and not os.path.exists(resolved): - return {"tool": tool, "available": False, "version": None, "details": f"{cmd[0]} not found in PATH"} - cmd = [resolved, *cmd[1:]] - ok, text = run_capture(cmd) - return {"tool": tool, "available": ok, "version": text if ok else None, "details": None if ok else text} - - -def install_plan(tool): - is_windows = platform.system().lower().startswith("win") - if is_windows: - plans = { - "dotnet": [("winget", ["install", "Microsoft.DotNet.SDK.10"])], - "node": [("winget", ["install", "OpenJS.NodeJS.LTS"])], - "npm": [("winget", ["install", "OpenJS.NodeJS.LTS"])], - "python": [("winget", ["install", "Python.Python.3.12"])], - "cargo": [("winget", ["install", "Rustlang.Rustup"])], - "tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])], - "git": [("winget", ["install", "Git.Git"])], - "docker": [("winget", ["install", "Docker.DockerDesktop"])], - } - else: - plans = { - "dotnet": [("sh", ["-c", "echo install dotnet sdk with your package manager"])], - "node": [("sh", ["-c", "echo install nodejs with your package manager"])], - "npm": [("sh", ["-c", "echo install npm with your package manager"])], - "python": [("sh", ["-c", "echo install python3 with your package manager"])], - "cargo": [("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh"])], - "tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])], - "git": [("sh", ["-c", "echo install git with your package manager"])], - "docker": [("sh", ["-c", "echo install docker with your package manager"])], - } - - cmds = plans.get(tool, []) - return { - "tool": tool, - "supported": len(cmds) > 0, - "summary": f"Install plan for {tool} on {platform.system()}", - "commands": [{"command": c, "args": a} for c, a in cmds], - } - - -def run_install(tool): - plan = install_plan(tool) - if not plan["supported"]: - return 2 - for cmd in plan["commands"]: - proc = subprocess.run([cmd["command"], *cmd["args"]], check=False) - if proc.returncode != 0: - return proc.returncode - return 0 - - -def main(): - parser = argparse.ArgumentParser(description="SDT diagnostics and install planner") - sub = parser.add_subparsers(dest="cmd", required=True) - - p_probe = sub.add_parser("probe") - p_probe.add_argument("--tool", required=True) - p_probe.add_argument("--json", action="store_true") - - p_plan = sub.add_parser("install-plan") - p_plan.add_argument("--tool", required=True) - p_plan.add_argument("--json", action="store_true") - - p_run = sub.add_parser("install-run") - p_run.add_argument("--tool", required=True) - - args = parser.parse_args() - - if args.cmd == "probe": - result = probe_tool(args.tool.lower()) - if args.json: - print(json.dumps(result)) - else: - print(result) - return 0 if result["available"] else 1 - - if args.cmd == "install-plan": - result = install_plan(args.tool.lower()) - if args.json: - print(json.dumps(result)) - else: - print(result) - return 0 if result["supported"] else 2 - - if args.cmd == "install-run": - return run_install(args.tool.lower()) - - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/dotnet-min.py b/scripts/dotnet-min.py deleted file mode 100644 index c8aa0f4..0000000 --- a/scripts/dotnet-min.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import sys - -from script_common import dotnet_env, resolve_repo_root, run - - -DOTNET_SAFE_CMDS = {"restore", "build", "run", "test", "publish", "pack"} - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform minimal dotnet wrapper") - parser.add_argument("dotnet_args", nargs=argparse.REMAINDER) - parser.add_argument("--repo-root", default=None) - args = parser.parse_args() - - if not args.dotnet_args: - print("Usage: python scripts/dotnet-min.py ", file=sys.stderr) - return 2 - - repo_root = resolve_repo_root(args.repo_root) - dotnet_args = list(args.dotnet_args) - cmd = dotnet_args[0].lower() - - if cmd in DOTNET_SAFE_CMDS: - dotnet_args.extend(["-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false"]) - if cmd == "restore": - dotnet_args.append("--ignore-failed-sources") - - return run("dotnet", dotnet_args, repo_root, env=dotnet_env(repo_root)) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/migration-gate.py b/scripts/migration-gate.py deleted file mode 100644 index 398cf09..0000000 --- a/scripts/migration-gate.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import subprocess -import sys -from pathlib import Path - -from script_common import resolve_repo_root - - -def run_step(repo_root: Path, title: str, command: list[str]) -> int: - print(f"\n== {title} ==") - print("$", " ".join(command)) - proc = subprocess.run(command, cwd=str(repo_root), check=False) - return proc.returncode - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform migration quality gate") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--skip-tests", action="store_true") - parser.add_argument("--test-project", default=None, help="Optional test csproj path") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - - code = run_step(repo_root, "Build", [sys.executable, "scripts/dotnet-min.py", "build"]) - if code != 0: - return code - - if not args.skip_tests: - if args.test_project: - test_cmd = [sys.executable, "scripts/dotnet-min.py", "test", args.test_project] - else: - test_cmd = [sys.executable, "scripts/dotnet-min.py", "test"] - code = run_step(repo_root, "Tests", test_cmd) - if code != 0: - return code - - print("\nMigration gate passed.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/npm-clean.py b/scripts/npm-clean.py deleted file mode 100644 index 0ea899f..0000000 --- a/scripts/npm-clean.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import shutil -from pathlib import Path - -from script_common import resolve_repo_root - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform node_modules cleanup") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--working-dir", default=".") - parser.add_argument("--also-cache", action="store_true") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - work_dir = (repo_root / args.working_dir).resolve() - node_modules = work_dir / "node_modules" - if node_modules.exists(): - shutil.rmtree(node_modules) - print(f"Removed: {node_modules}") - else: - print(f"Not found: {node_modules}") - - if args.also_cache: - npm_cache = repo_root / ".npm" / "cache" - if npm_cache.exists(): - shutil.rmtree(npm_cache) - print(f"Removed: {npm_cache}") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/nuget-export-cache.py b/scripts/nuget-export-cache.py deleted file mode 100644 index 17720f7..0000000 --- a/scripts/nuget-export-cache.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import shutil -import tempfile -from pathlib import Path - -from script_common import resolve_repo_root - - -def main() -> int: - parser = argparse.ArgumentParser(description="Export local NuGet cache to zip") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--output-zip", default="nuget-cache-export.zip") - parser.add_argument("--include-dotnet-home", action="store_true") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - output_zip = (repo_root / args.output_zip).resolve() - - nuget_dir = repo_root / ".nuget" - dotnet_home = repo_root / ".dotnet_home" - if not nuget_dir.exists(): - print(f"NuGet cache not found: {nuget_dir}") - return 2 - - with tempfile.TemporaryDirectory() as td: - stage = Path(td) / "cache-export" - stage.mkdir(parents=True, exist_ok=True) - shutil.copytree(nuget_dir, stage / ".nuget") - if args.include_dotnet_home and dotnet_home.exists(): - shutil.copytree(dotnet_home, stage / ".dotnet_home") - manifest = stage / "nuget-cache-manifest.txt" - manifest.write_text("exported_by=nuget-export-cache.py\n", encoding="utf-8") - archive_base = str(output_zip.with_suffix("")) - shutil.make_archive(archive_base, "zip", root_dir=str(stage)) - - print(f"Exported cache: {output_zip}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/nuget-import-cache.py b/scripts/nuget-import-cache.py deleted file mode 100644 index ec0fee1..0000000 --- a/scripts/nuget-import-cache.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import shutil -from pathlib import Path - -from script_common import resolve_repo_root - - -def main() -> int: - parser = argparse.ArgumentParser(description="Import NuGet cache from zip") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--input-zip", default="nuget-cache-export.zip") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - input_zip = (repo_root / args.input_zip).resolve() - if not input_zip.exists(): - print(f"Input zip not found: {input_zip}") - return 2 - - shutil.unpack_archive(str(input_zip), extract_dir=str(repo_root)) - print(f"Imported cache from: {input_zip}") - print("Run `python scripts/dotnet-min.py restore` to validate restore in this repo.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/pip-min.py b/scripts/pip-min.py deleted file mode 100644 index fd03343..0000000 --- a/scripts/pip-min.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys - -from script_common import pip_env, resolve_repo_root, run - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform minimal pip wrapper") - parser.add_argument("pip_args", nargs=argparse.REMAINDER) - parser.add_argument("--repo-root", default=None) - args = parser.parse_args() - - if not args.pip_args: - print("Usage: python scripts/pip-min.py ", file=sys.stderr) - return 2 - - repo_root = resolve_repo_root(args.repo_root) - pip_args = list(args.pip_args) - - # Preserve legacy behavior: for bare install, default target to repo-local deps. - if pip_args and pip_args[0].lower() == "install": - has_target = any(a in ("--target", "--prefix") for a in pip_args) - if not has_target: - pip_args = [a for a in pip_args if a != "--user"] - target = repo_root / ".pydeps" / f"py{sys.version_info.major}{sys.version_info.minor}" - os.makedirs(target, exist_ok=True) - pip_args.extend(["--target", str(target)]) - - return run(sys.executable, ["-m", "pip", *pip_args], repo_root, env=pip_env(repo_root)) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/pip_safe.py b/scripts/pip_safe.py deleted file mode 100644 index a02a307..0000000 --- a/scripts/pip_safe.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -import os -import tempfile -from typing import Callable - - -def _mkdtemp_compat( - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, -) -> str: - # Python 3.14 on some Windows hosts creates mkdtemp dirs that are - # immediately non-writable by the same process when mode=0o700 is used. - # pip relies heavily on tempfile; force 0o777 for compatibility. - if dir is None: - dir = tempfile.gettempdir() - if prefix is None: - prefix = tempfile.template - if suffix is None: - suffix = "" - - names = tempfile._get_candidate_names() - for _ in range(tempfile.TMP_MAX): - name = next(names) - path = os.path.join(dir, f"{prefix}{name}{suffix}") - try: - os.mkdir(path, 0o777) - return path - except FileExistsError: - continue - - raise FileExistsError("No usable temporary directory name found.") - - -def main(argv: list[str]) -> int: - tempfile.mkdtemp = _mkdtemp_compat # type: ignore[assignment] - - from pip._internal.cli.main import main as pip_main - - return int(pip_main(argv)) - - -if __name__ == "__main__": - raise SystemExit(main(__import__("sys").argv[1:])) - diff --git a/scripts/publish-app.py b/scripts/publish-app.py deleted file mode 100644 index b85ada7..0000000 --- a/scripts/publish-app.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -import argparse -from pathlib import Path - -from script_common import find_node_app_root, resolve_repo_root, run, sha256_files - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper") - parser.add_argument("--target", choices=["web", "tauri"], default="web") - parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") - parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none") - parser.add_argument("--install-deps", action="store_true") - parser.add_argument("--skip-install", action="store_true") - parser.add_argument("--dry-run", action="store_true") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - app_root = find_node_app_root(repo_root, args.app_root) - if app_root is None: - print("Unable to locate app root (no unique package.json found).") - return 2 - - package_json = app_root / "package.json" - lock_file = app_root / "package-lock.json" - node_modules = app_root / "node_modules" - deps_hash_file = node_modules / ".sdt-deps.sha256" - expected_hash = sha256_files([package_json, lock_file]) - - should_install = args.install_deps or not node_modules.exists() - if not should_install and not args.skip_install: - if not deps_hash_file.exists(): - should_install = True - else: - current = deps_hash_file.read_text(encoding="utf-8").strip() - should_install = current != expected_hash - if args.skip_install: - should_install = False - - print(f"App root: {app_root}") - print(f"Target: {args.target} ({args.configuration})") - - if should_install: - install_args = ["ci", "--no-audit", "--fund=false"] if lock_file.exists() else ["install", "--no-audit", "--fund=false"] - print("$ npm " + " ".join(install_args)) - if not args.dry_run: - code = run("npm", install_args, app_root) - if code != 0: - if lock_file.exists() and install_args[0] == "ci": - print("npm ci failed (likely lockfile out of sync). Falling back to npm install...") - fallback_args = ["install", "--no-audit", "--fund=false"] - print("$ npm " + " ".join(fallback_args)) - code = run("npm", fallback_args, app_root) - if code != 0: - return code - else: - return code - node_modules.mkdir(parents=True, exist_ok=True) - deps_hash_file.write_text(expected_hash, encoding="utf-8") - else: - print("Skipping dependency install.") - - if args.target == "web": - cmd = ["run", "build"] - print("$ npm " + " ".join(cmd)) - if not args.dry_run: - return run("npm", cmd, app_root) - return 0 - - tauri_cmd = ["run", "tauri", "build"] - tauri_tail: list[str] = [] - if args.tauri_bundles == "none": - tauri_tail.extend(["--no-bundle"]) - else: - tauri_tail.extend(["--bundles", args.tauri_bundles]) - if args.configuration == "Debug": - tauri_tail.append("--debug") - if tauri_tail: - tauri_cmd.extend(["--", *tauri_tail]) - - print("$ npm " + " ".join(tauri_cmd)) - if not args.dry_run: - return run("npm", tauri_cmd, app_root) - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/publish-output.py b/scripts/publish-output.py deleted file mode 100644 index bb87c9c..0000000 --- a/scripts/publish-output.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import shutil -import subprocess -import sys -from pathlib import Path - -from script_common import find_node_app_root, resolve_repo_root - - -def run_step(label: str, cmd: list[str], cwd: Path, dry_run: bool) -> int: - print(f"\n> {label}") - print("$", " ".join(cmd)) - if dry_run: - return 0 - proc = subprocess.run(cmd, cwd=str(cwd), check=False) - return proc.returncode - - -def main() -> int: - parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints") - parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") - parser.add_argument("--runtime", default="win-x64") - parser.add_argument("--skip-sidecar", action="store_true") - parser.add_argument("--skip-web", action="store_true") - parser.add_argument("--skip-webgateway", action="store_true") - parser.add_argument("--skip-tauri", action="store_true") - parser.add_argument("--dry-run", action="store_true") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--sidecar-project", default=None) - parser.add_argument("--gateway-project", default=None) - parser.add_argument("--app-root", default=None) - parser.add_argument("--output-dir", default="output") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - output_root = (repo_root / args.output_dir).resolve() - output_root.mkdir(parents=True, exist_ok=True) - - py = sys.executable - if not args.skip_sidecar: - cmd = [py, "scripts/publish-sidecar.py", "--configuration", args.configuration, "--runtime", args.runtime] - if args.sidecar_project: - cmd.extend(["--project", args.sidecar_project]) - code = run_step("Publish sidecar", cmd, repo_root, args.dry_run) - if code != 0: - return code - - if not args.skip_web: - cmd = [py, "scripts/publish-app.py", "--target", "web", "--configuration", args.configuration] - if args.app_root: - cmd.extend(["--app-root", args.app_root]) - code = run_step("Build web", cmd, repo_root, args.dry_run) - if code != 0: - return code - - if not args.skip_webgateway: - cmd = [py, "scripts/publish-webgateway.py", "--configuration", args.configuration, "--runtime", args.runtime] - if args.gateway_project: - cmd.extend(["--project", args.gateway_project]) - code = run_step("Publish web gateway", cmd, repo_root, args.dry_run) - if code != 0: - return code - - if not args.skip_tauri: - cmd = [py, "scripts/publish-app.py", "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none"] - if args.app_root: - cmd.extend(["--app-root", args.app_root]) - code = run_step("Build tauri", cmd, repo_root, args.dry_run) - if code != 0: - return code - - app_root = find_node_app_root(repo_root, args.app_root) - if app_root is not None: - target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release") - exes = sorted(target_dir.glob("*.exe"), key=lambda p: p.stat().st_mtime, reverse=True) - if exes: - staged = output_root / exes[0].name - if args.dry_run: - print(f"Would copy: {exes[0]} -> {staged}") - else: - shutil.copy2(exes[0], staged) - print(f"Staged desktop executable: {staged}") - - print("\nPublish output workflow complete.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/publish-sidecar.py b/scripts/publish-sidecar.py deleted file mode 100644 index c25a44a..0000000 --- a/scripts/publish-sidecar.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os - -from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper") - parser.add_argument("--configuration", default="Release") - parser.add_argument("--runtime", default="win-x64") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj") - parser.add_argument("--output-dir", default="output") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - output_dir = (repo_root / args.output_dir).resolve() - output_dir.mkdir(parents=True, exist_ok=True) - - if args.project: - csproj = (repo_root / args.project).resolve() - else: - csproj = find_csproj_by_keyword(repo_root, ["sidecar"]) - if csproj is None or not csproj.exists(): - print("Could not locate sidecar project. Pass --project .") - return 2 - - publish_args = [ - "publish", - str(csproj), - "-c", - args.configuration, - "-r", - args.runtime, - "--self-contained", - "-p:PublishSingleFile=true", - "-p:IncludeNativeLibrariesForSelfExtract=true", - "-p:RestoreIgnoreFailedSources=true", - "-p:NuGetAudit=false", - "-o", - str(output_dir), - ] - code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root)) - if code != 0: - return code - - binary_name = csproj.stem + (".exe" if args.runtime.startswith("win-") else "") - binary_path = output_dir / binary_name - if binary_path.exists(): - print(f"Published executable: {binary_path}") - else: - print(f"Publish completed. Output directory: {output_dir}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/publish-webgateway.py b/scripts/publish-webgateway.py deleted file mode 100644 index 6a2c9c0..0000000 --- a/scripts/publish-webgateway.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import shutil - -from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run - - -def main() -> int: - parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper") - parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") - parser.add_argument("--runtime", default="win-x64") - parser.add_argument("--self-contained", action="store_true") - parser.add_argument("--skip-web-assets", action="store_true") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj") - parser.add_argument("--web-build-dir", default=None, help="Relative path to web build assets root") - parser.add_argument("--output-dir", default="output/webgateway") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - output_dir = (repo_root / args.output_dir).resolve() - output_dir.mkdir(parents=True, exist_ok=True) - - if args.project: - csproj = (repo_root / args.project).resolve() - else: - csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"]) - if csproj is None or not csproj.exists(): - print("Could not locate web gateway project. Pass --project .") - return 2 - - publish_args = [ - "publish", - str(csproj), - "-c", - args.configuration, - "-r", - args.runtime, - "--self-contained", - "true" if args.self_contained else "false", - "-p:RestoreIgnoreFailedSources=true", - "-p:NuGetAudit=false", - "-o", - str(output_dir), - ] - code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root)) - if code != 0: - return code - - if not args.skip_web_assets: - if args.web_build_dir: - web_build_dir = (repo_root / args.web_build_dir).resolve() - else: - web_build_dir = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None) - if web_build_dir is not None: - web_build_dir = web_build_dir / "build" - - if web_build_dir is None or not web_build_dir.exists(): - print("Web assets not found. Skip with --skip-web-assets or pass --web-build-dir.") - else: - web_out = output_dir / "wwwroot" - web_out.mkdir(parents=True, exist_ok=True) - for item in web_build_dir.iterdir(): - dst = web_out / item.name - if item.is_dir(): - if dst.exists(): - shutil.rmtree(dst) - shutil.copytree(item, dst) - else: - shutil.copy2(item, dst) - print(f"Copied web assets: {web_out}") - - print(f"Publish completed: {output_dir}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/run-webgateway.py b/scripts/run-webgateway.py deleted file mode 100644 index 3f35dbc..0000000 --- a/scripts/run-webgateway.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -from pathlib import Path - -from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run - - -def main() -> int: - parser = argparse.ArgumentParser(description="Run gateway in dev or output mode") - parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release") - parser.add_argument("--urls", default="http://0.0.0.0:5180") - parser.add_argument("--project-root", default=None, help="Runtime project root exposed via SDT_PROJECT_ROOT") - parser.add_argument("--mode", choices=["Dev", "Output"], default="Dev") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--project", default=None, help="Gateway csproj path") - parser.add_argument("--output-exe", default=None, help="Published gateway executable path") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - effective_project_root = Path(args.project_root).resolve() if args.project_root else repo_root - if not effective_project_root.exists(): - print(f"Project root does not exist: {effective_project_root}") - return 2 - - env = dotnet_env(repo_root) - env["SDT_PROJECT_ROOT"] = str(effective_project_root) - - if args.mode == "Output": - exe_path = Path(args.output_exe).resolve() if args.output_exe else (repo_root / "output" / "webgateway" / ("webgateway.exe" if os.name == "nt" else "webgateway")) - if not exe_path.exists(): - print(f"Output executable not found: {exe_path}") - return 2 - return run(str(exe_path), ["--urls", args.urls], repo_root, env=env) - - if args.project: - csproj = (repo_root / args.project).resolve() - else: - csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"]) - if csproj is None or not csproj.exists(): - print("Could not locate gateway project. Pass --project .") - return 2 - - run_args = [ - "run", - "--project", - str(csproj), - "-c", - args.configuration, - "--no-launch-profile", - "--urls", - args.urls, - "-p:RestoreIgnoreFailedSources=true", - "-p:NuGetAudit=false", - ] - return run("dotnet", run_args, repo_root, env=env) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/script_common.py b/scripts/script_common.py deleted file mode 100644 index 76ac87d..0000000 --- a/scripts/script_common.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/env python3 -import hashlib -import json -import os -import pathlib -import shutil -import subprocess -import sys -from typing import Dict, Iterable, List, Sequence - - -PROXY_VARS = [ - "HTTP_PROXY", - "HTTPS_PROXY", - "ALL_PROXY", - "http_proxy", - "https_proxy", - "all_proxy", - "GIT_HTTP_PROXY", - "GIT_HTTPS_PROXY", - "PIP_NO_INDEX", -] - - -def resolve_repo_root(start: str | None = None) -> pathlib.Path: - base = pathlib.Path(start or os.getcwd()).resolve() - - # Preferred marker for SDT-managed projects. - for cur in [base, *base.parents]: - cfg = cur / "devtool.json" - if cfg.exists(): - hints = load_project_root_hints(cur) - if not hints: - return cur - if any(_hint_matches(cur, hint) for hint in hints): - return cur - - # Fall back to git root when available. - try: - proc = subprocess.run( - ["git", "-C", str(base), "rev-parse", "--show-toplevel"], - capture_output=True, - text=True, - check=False, - ) - if proc.returncode == 0: - git_root = proc.stdout.strip() - if git_root: - return pathlib.Path(git_root).resolve() - except Exception: - pass - - return base - - -def load_project_root_hints(repo_root: pathlib.Path) -> list[str]: - cfg = repo_root / "devtool.json" - if not cfg.exists(): - return [] - try: - data = json.loads(cfg.read_text(encoding="utf-8")) - hints = data.get("project", {}).get("rootHints", []) - return [str(x) for x in hints if isinstance(x, str) and x.strip()] - except Exception: - return [] - - -def ensure_dirs(paths: List[pathlib.Path]) -> None: - for p in paths: - p.mkdir(parents=True, exist_ok=True) - - -def clean_proxy_env(env: Dict[str, str]) -> None: - for k in PROXY_VARS: - env.pop(k, None) - - -def dotnet_env(repo_root: pathlib.Path) -> Dict[str, str]: - env = dict(os.environ) - clean_proxy_env(env) - dotnet_cli_home = repo_root / ".dotnet_home" - nuget_packages = repo_root / ".nuget" / "packages" - nuget_http_cache = repo_root / ".nuget" / "http-cache" - ensure_dirs([dotnet_cli_home, nuget_packages, nuget_http_cache]) - env["DOTNET_CLI_HOME"] = str(dotnet_cli_home) - env["NUGET_PACKAGES"] = str(nuget_packages) - env["NUGET_HTTP_CACHE_PATH"] = str(nuget_http_cache) - env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1" - env["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "0" - env["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "0" - env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" - env["NUGET_CERT_REVOCATION_MODE"] = "offline" - return env - - -def pip_env(repo_root: pathlib.Path) -> Dict[str, str]: - env = dict(os.environ) - clean_proxy_env(env) - pip_cache = repo_root / ".pip" / "cache" - pip_tmp = repo_root / ".tmp" / "pip-temp" - ensure_dirs([pip_cache, pip_tmp]) - env["PIP_CACHE_DIR"] = str(pip_cache) - env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" - env["PIP_DEFAULT_TIMEOUT"] = "30" - env["PIP_RETRIES"] = "2" - env["TEMP"] = str(pip_tmp) - env["TMP"] = str(pip_tmp) - return env - - -def run(command: str, args: List[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> int: - resolved = resolve_command(command) - try: - proc = subprocess.run([resolved, *args], cwd=str(cwd), env=env, check=False) - return proc.returncode - except FileNotFoundError: - print(f"Command not found: {resolved}", file=sys.stderr) - return 127 - - -def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> tuple[int, str, str]: - resolved = resolve_command(command) - try: - proc = subprocess.run( - [resolved, *args], - cwd=str(cwd), - env=env, - capture_output=True, - text=True, - check=False, - ) - return proc.returncode, proc.stdout, proc.stderr - except FileNotFoundError: - return 127, "", f"Command not found: {resolved}" - - -def resolve_command(command: str) -> str: - if not command: - return command - - if os.name != "nt": - return command - - if any(sep in command for sep in ("\\", "/")): - return command - - if pathlib.Path(command).suffix: - found = shutil.which(command) - return found or command - - candidates = [] - lowered = command.lower() - if lowered in ("npm", "npx", "pnpm", "yarn", "tauri"): - candidates.extend([f"{command}.cmd", f"{command}.exe", f"{command}.bat", command]) - else: - candidates.append(command) - - for c in candidates: - found = _which_windows(c) - if found: - name = pathlib.Path(found).name.lower() - if name in ("npm", "npx", "pnpm", "yarn", "tauri"): - shim = pathlib.Path(found).with_name(name + ".cmd") - if shim.exists(): - return str(shim) - return found - - if lowered in ("npm", "npx", "pnpm", "yarn"): - node = _which_windows("node.exe") or _which_windows("node") - if node: - node_dir = pathlib.Path(node).parent - shim = node_dir / f"{lowered}.cmd" - if shim.exists(): - return str(shim) - - return candidates[-1] - - -def _hint_matches(root: pathlib.Path, hint: str) -> bool: - h = hint.strip() - if not h: - return False - - has_glob = any(ch in h for ch in ("*", "?", "[")) - if has_glob: - # Match both anywhere in root and directly at root-level for common hints like "*.sln". - if any(root.glob(h)): - return True - return any(root.rglob(h)) - - marker = root / h - if marker.exists(): - return True - - # If hint is just a filename marker, look bounded in tree. - if not any(sep in h for sep in ("\\", "/")): - return any(p.name == h for p in root.rglob(h)) - - return False - - -def _expand_windows_path_segment(segment: str) -> str: - expanded = segment - # Expand %VAR% tokens repeatedly for nested references. - for _ in range(4): - next_value = os.path.expandvars(expanded) - if next_value == expanded: - break - expanded = next_value - return expanded - - -def _which_windows(command: str) -> str | None: - found = shutil.which(command) - if found: - return found - - if os.name != "nt": - return None - - path_value = os.environ.get("PATH", "") - pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD") - exts = [e.lower() for e in pathext.split(";") if e] - - has_ext = pathlib.Path(command).suffix != "" - names = [command] if has_ext else [command, *(command + e.lower() for e in exts)] - - for raw_segment in path_value.split(os.pathsep): - segment = _expand_windows_path_segment(raw_segment.strip()) - if not segment: - continue - base = pathlib.Path(segment) - for name in names: - candidate = base / name - if candidate.exists(): - return str(candidate) - - return None - - -def sha256_files(paths: Iterable[pathlib.Path]) -> str: - h = hashlib.sha256() - for p in paths: - if not p.exists(): - continue - h.update(p.read_bytes()) - return h.hexdigest() - - -def first_existing(paths: Iterable[pathlib.Path]) -> pathlib.Path | None: - for p in paths: - if p.exists(): - return p - return None - - -def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> pathlib.Path | None: - if hints: - for hint in hints: - candidate = (repo_root / hint).resolve() - if candidate.exists() and candidate.suffix.lower() == ".csproj": - return candidate - - csprojs = sorted(repo_root.rglob("*.csproj")) - if not csprojs: - return None - if len(csprojs) == 1: - return csprojs[0] - return None - - -def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> pathlib.Path | None: - kws = [k.lower() for k in keywords] - matches: list[pathlib.Path] = [] - for p in repo_root.rglob("*.csproj"): - text = str(p).lower() - if any(k in text for k in kws): - matches.append(p) - if len(matches) == 1: - return matches[0] - return None - - -def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> pathlib.Path | None: - if preferred: - p = (repo_root / preferred).resolve() - if (p / "package.json").exists(): - return p - - direct = repo_root / "package.json" - if direct.exists(): - return repo_root - - tauri_candidates = [] - for package_json in repo_root.rglob("package.json"): - d = package_json.parent - if (d / "src-tauri" / "tauri.conf.json").exists(): - tauri_candidates.append(d) - if len(tauri_candidates) == 1: - return tauri_candidates[0] - - all_candidates = [p.parent for p in repo_root.rglob("package.json")] - if len(all_candidates) == 1: - return all_candidates[0] - return None - - -def newest_file(search_root: pathlib.Path, pattern: str) -> pathlib.Path | None: - if not search_root.exists(): - return None - files = [p for p in search_root.rglob(pattern) if p.is_file() and "\\obj\\" not in str(p).replace("/", "\\")] - if not files: - return None - files.sort(key=lambda p: p.stat().st_mtime, reverse=True) - return files[0] diff --git a/scripts/sync-output.py b/scripts/sync-output.py deleted file mode 100644 index 8a28a2b..0000000 --- a/scripts/sync-output.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import shutil -from pathlib import Path - -from script_common import newest_file, resolve_repo_root - - -def copy_tree_contents(src: Path, dst: Path) -> None: - dst.mkdir(parents=True, exist_ok=True) - for item in src.iterdir(): - target = dst / item.name - if item.is_dir(): - if target.exists(): - shutil.rmtree(target) - shutil.copytree(item, target) - else: - shutil.copy2(item, target) - - -def main() -> int: - parser = argparse.ArgumentParser(description="Sync newest built assets into output folder") - parser.add_argument("--repo-root", default=None) - parser.add_argument("--output-dir", default="output") - parser.add_argument("--web-build-dir", default=None, help="Path to web build output") - parser.add_argument("--sidecar-bin-dir", default=None, help="Path to sidecar bin root") - parser.add_argument("--gateway-bin-dir", default=None, help="Path to gateway bin root") - parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root") - args = parser.parse_args() - - repo_root = resolve_repo_root(args.repo_root) - output_dir = (repo_root / args.output_dir).resolve() - output_dir.mkdir(parents=True, exist_ok=True) - - web_build = (repo_root / args.web_build_dir).resolve() if args.web_build_dir else None - if web_build is None: - web_build = next((p for p in repo_root.rglob("build") if (p.parent / "package.json").exists()), None) - if web_build is not None and web_build.exists(): - web_out = output_dir / "webgateway" / "wwwroot" - copy_tree_contents(web_build, web_out) - print(f"Synced web assets -> {web_out}") - - sidecar_bin = (repo_root / args.sidecar_bin_dir).resolve() if args.sidecar_bin_dir else None - if sidecar_bin is None: - sidecar_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "sidecar" in str(p).lower()), None) - sidecar_bin = sidecar_proj / "bin" if sidecar_proj else None - if sidecar_bin is not None: - sidecar_pattern = "*.exe" if os.name == "nt" else "*" - sidecar_exe = newest_file(sidecar_bin, sidecar_pattern) - if sidecar_exe is not None: - copy_tree_contents(sidecar_exe.parent, output_dir) - print(f"Synced sidecar -> {output_dir}") - - gateway_bin = (repo_root / args.gateway_bin_dir).resolve() if args.gateway_bin_dir else None - if gateway_bin is None: - gateway_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "gateway" in str(p).lower()), None) - gateway_bin = gateway_proj / "bin" if gateway_proj else None - if gateway_bin is not None: - gateway_pattern = "*.exe" if os.name == "nt" else "*" - gw_exe = newest_file(gateway_bin, gateway_pattern) - if gw_exe is not None: - gw_out = output_dir / "webgateway" - copy_tree_contents(gw_exe.parent, gw_out) - print(f"Synced gateway -> {gw_out}") - - tauri_target = (repo_root / args.tauri_target_dir).resolve() if args.tauri_target_dir else None - if tauri_target is None: - tauri_target = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None) - tauri_target = tauri_target / "target" if tauri_target else None - if tauri_target is not None: - app_exe = newest_file(tauri_target, "*.exe") - if app_exe is not None: - shutil.copy2(app_exe, output_dir / app_exe.name) - print(f"Synced desktop app ({app_exe.name}) -> {output_dir}") - - print("Sync complete.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())