diff --git a/Journal.DevTool/DevTool.Engine.dll b/Journal.DevTool/DevTool.Engine.dll index 99c4dbf..df0b94e 100644 Binary files a/Journal.DevTool/DevTool.Engine.dll and b/Journal.DevTool/DevTool.Engine.dll differ diff --git a/Journal.DevTool/DevTool.Engine.pdb b/Journal.DevTool/DevTool.Engine.pdb index ededda9..597f4ed 100644 Binary files a/Journal.DevTool/DevTool.Engine.pdb and b/Journal.DevTool/DevTool.Engine.pdb differ diff --git a/Journal.DevTool/DevTool.Host.Bridge.dll b/Journal.DevTool/DevTool.Host.Bridge.dll index 5d60e0d..7469f47 100644 Binary files a/Journal.DevTool/DevTool.Host.Bridge.dll and b/Journal.DevTool/DevTool.Host.Bridge.dll differ diff --git a/Journal.DevTool/DevTool.Host.Bridge.pdb b/Journal.DevTool/DevTool.Host.Bridge.pdb index 71b9127..a2432da 100644 Binary files a/Journal.DevTool/DevTool.Host.Bridge.pdb and b/Journal.DevTool/DevTool.Host.Bridge.pdb differ diff --git a/Journal.DevTool/DevTool.Host.Tui.dll b/Journal.DevTool/DevTool.Host.Tui.dll index 52f5ce3..8d996dd 100644 Binary files a/Journal.DevTool/DevTool.Host.Tui.dll and b/Journal.DevTool/DevTool.Host.Tui.dll differ diff --git a/Journal.DevTool/DevTool.Host.Tui.pdb b/Journal.DevTool/DevTool.Host.Tui.pdb index 325d700..5133141 100644 Binary files a/Journal.DevTool/DevTool.Host.Tui.pdb and b/Journal.DevTool/DevTool.Host.Tui.pdb differ diff --git a/Journal.DevTool/DevTool.Runtime.dll b/Journal.DevTool/DevTool.Runtime.dll index 61e3279..4343853 100644 Binary files a/Journal.DevTool/DevTool.Runtime.dll and b/Journal.DevTool/DevTool.Runtime.dll differ diff --git a/Journal.DevTool/DevTool.Runtime.pdb b/Journal.DevTool/DevTool.Runtime.pdb index 4c8b134..b72103c 100644 Binary files a/Journal.DevTool/DevTool.Runtime.pdb and b/Journal.DevTool/DevTool.Runtime.pdb differ diff --git a/Journal.DevTool/scripts/README.md b/Journal.DevTool/scripts/README.md new file mode 100644 index 0000000..8a7d099 --- /dev/null +++ b/Journal.DevTool/scripts/README.md @@ -0,0 +1,63 @@ +# Scripts (Python-first, cross-platform) + +This folder now uses Python as the default runtime for orchestration and diagnostics. + +## Preferred scripts + +- `diag.py`: tool probing and install-plan generation (`dotnet`, `python`, `node`, `npm`, `cargo`, `tauri`) +- `build.py`: normalized build actions used by SDT workflows +- `dev_shell.py`: cross-platform shell bootstrap export/doctor helper +- `dotnet-min.py`: resilient `dotnet` wrapper with local cache env +- `pip-min.py`: resilient `pip` wrapper with local cache env and repo-local target default +- `npm-clean.py`: remove `node_modules` cross-platform +- `migration-gate.py`: build/test quality gate +- `nuget-export-cache.py`: archive `.nuget` cache +- `nuget-import-cache.py`: restore `.nuget` cache from archive +- `publish-app.py`: build web or tauri app (cross-platform) +- `publish-sidecar.py`: publish sidecar .NET service +- `publish-webgateway.py`: publish gateway .NET service and optional web assets +- `run-webgateway.py`: run gateway in dev or published-output mode +- `publish-output.py`: orchestrate sidecar/web/gateway/desktop publish steps +- `sync-output.py`: sweep newest build artifacts into `output/` +- `script_common.py`: shared helpers (repo root resolution, env shaping, command runner) + - `project.rootHints` supports glob markers (for example `*.sln`) and directory/file markers (`.git`, `package.json`) + - Windows PATH token expansion (`%NVM_HOME%`, `%NVM_SYMLINK%`, etc.) is applied during command resolution + +## Shell bootstrap wrappers + +- `dev-shell.ps1`: PowerShell wrapper over `dev_shell.py` +- `dev-shell.sh`: bash/zsh wrapper over `dev_shell.py` +- `dev-shell.cmd`: cmd wrapper over `dev_shell.py` + +## Legacy scripts + +Existing `.ps1` entrypoints are now compatibility wrappers that forward to Python scripts. +`script-common.ps1` is legacy-only compatibility and not used by active SDT workflows. + +Original PowerShell implementations are archived under `scripts/legacy/` as `*.legacy.ps1` for reference during transition. + +## Root Hint Semantics + +`project.rootHints` is evaluated in this order: +1. Exact marker exists at candidate root (file or directory) +2. Root-level glob match (`glob`) +3. Recursive glob match (`rglob`) + +Examples: +- `"*.sln"` +- `".git"` +- `"package.json"` +- `"src-tauri/tauri.conf.json"` + +## Quick usage + +```powershell +python scripts/diag.py probe --tool dotnet --json +python scripts/dotnet-min.py build +python scripts/migration-gate.py +python scripts/nuget-export-cache.py --output-zip nuget-cache-export.zip +python scripts/nuget-import-cache.py --input-zip nuget-cache-export.zip +python scripts/npm-clean.py --working-dir . +python scripts/dev_shell.py export --shell pwsh --json +python scripts/dev_shell.py doctor +``` diff --git a/Journal.DevTool/scripts/WORKFLOWS.md b/Journal.DevTool/scripts/WORKFLOWS.md new file mode 100644 index 0000000..8e5172c --- /dev/null +++ b/Journal.DevTool/scripts/WORKFLOWS.md @@ -0,0 +1,67 @@ +# Cross-Platform Script Workflows + +## 1) Probe toolchain availability + +```powershell +python scripts/diag.py probe --tool dotnet --json +python scripts/diag.py probe --tool python --json +python scripts/diag.py probe --tool node --json +python scripts/diag.py probe --tool npm --json +python scripts/diag.py probe --tool cargo --json +python scripts/diag.py probe --tool tauri --json +python scripts/diag.py probe --tool git --json +python scripts/diag.py probe --tool docker --json +``` + +## Shell bootstrap (cross-platform) + +```powershell +python scripts/dev_shell.py export --shell pwsh --json +python scripts/dev_shell.py doctor +``` + +## 2) Build and run SDT + +```powershell +python scripts/dotnet-min.py build +dotnet run --project DevTool.csproj +``` + +## 3) Run migration gate + +```powershell +python scripts/migration-gate.py +``` + +## 3.1) Verify workflow route resolution (path + optional execution) + +```powershell +# Static route checks only +python scripts/verify-workflow-routes.py --project-root . + +# Static + headless execution checks for selected workflows +python scripts/verify-workflow-routes.py --project-root . --workflow build --workflow tauri --execute --env-profile dev +``` + +## 4) Manage NuGet cache + +```powershell +python scripts/nuget-export-cache.py --output-zip nuget-cache-export.zip +python scripts/nuget-import-cache.py --input-zip nuget-cache-export.zip +``` + +## 5) Clean Node modules + +```powershell +python scripts/npm-clean.py --working-dir . +``` + +## 6) Build app/gateway bundles + +```powershell +python scripts/publish-app.py --target web +python scripts/publish-sidecar.py --project path/to/sidecar.csproj +python scripts/publish-webgateway.py --project path/to/gateway.csproj --skip-web-assets +python scripts/publish-output.py --dry-run +python scripts/sync-output.py +``` diff --git a/Journal.DevTool/scripts/build.py b/Journal.DevTool/scripts/build.py index 39676ce..dee4403 100644 --- a/Journal.DevTool/scripts/build.py +++ b/Journal.DevTool/scripts/build.py @@ -8,11 +8,15 @@ import shutil import subprocess import sys import time -from script_common import resolve_command +from typing import Any + +from script_common import resolve_command, SdtResult # type: ignore + +StepResult = dict[str, Any] -def run_step(command, args, cwd): - resolved = resolve_command(command) +def run_step(command: str, args: list[str], cwd: str) -> StepResult: + resolved = str(resolve_command(command)) if shutil.which(resolved) is None and not pathlib.Path(resolved).exists(): return { "command": resolved, @@ -38,7 +42,7 @@ def run_step(command, args, cwd): } -def resolve_python_executable(): +def resolve_python_executable() -> str: candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"] for c in candidates: if shutil.which(c): @@ -46,20 +50,20 @@ def resolve_python_executable(): 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 parse_common(parser: argparse.ArgumentParser) -> None: + _ = 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): +def resolve_cwd(project_root: str, working_dir: str) -> str: 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): +def discover_dotnet_target(project_root: str, cwd: str) -> str | None: # Prefer local solution first (.slnx, then .sln), then csproj, then bounded scan from project root. local_slnx = sorted(pathlib.Path(cwd).glob("*.slnx")) if len(local_slnx) == 1: @@ -88,9 +92,9 @@ def discover_dotnet_target(project_root: str, cwd: str): return None -def bounded_find_files(root: str, extension: str, max_depth: int): +def bounded_find_files(root: str, extension: str, max_depth: int) -> list[str]: root_path = pathlib.Path(root).resolve() - results = [] + results: list[str] = [] for current_root, dirs, files in os.walk(root_path): rel = pathlib.Path(current_root).resolve().relative_to(root_path) depth = len(rel.parts) @@ -105,7 +109,7 @@ def bounded_find_files(root: str, extension: str, max_depth: int): return sorted(results) -def run_dotnet_action(project_root, working_dir, verb): +def run_dotnet_action(project_root: str, working_dir: str, verb: str) -> tuple[int, StepResult]: cwd = resolve_cwd(project_root, working_dir) target = discover_dotnet_target(project_root, cwd) if not target: @@ -124,10 +128,14 @@ def run_dotnet_action(project_root, working_dir, verb): args = [verb, target] step = run_step("dotnet", args, cwd) step["resolved_target"] = target - return 0 if step["exit_code"] == 0 else step["exit_code"], step + + # Explicitly ensure the first return value is an integer and narrow the step result + exit_val = step.get("exit_code", 1) + exit_code = int(exit_val) if isinstance(exit_val, (int, float, str)) and str(exit_val).isdigit() else 1 + return exit_code, step -def _deps_hash(app_root): +def _deps_hash(app_root: str) -> str: h = hashlib.sha256() for name in ("package.json", "package-lock.json"): p = pathlib.Path(app_root) / name @@ -136,7 +144,7 @@ def _deps_hash(app_root): return h.hexdigest() -def ensure_npm_dependencies(app_root): +def ensure_npm_dependencies(app_root: str) -> dict[str, Any]: package_json = pathlib.Path(app_root) / "package.json" if not package_json.exists(): return {"installed": False, "reason": "not_applicable"} @@ -174,7 +182,7 @@ def ensure_npm_dependencies(app_root): return {"installed": True, "reason": "installed", "step": install_step} -def read_package_json(cwd: str): +def read_package_json(cwd: str) -> dict[str, Any] | None: package_json = pathlib.Path(cwd) / "package.json" if not package_json.exists(): return None @@ -194,23 +202,23 @@ def has_npm_script(cwd: str, script_name: str) -> bool: return script_name in scripts and isinstance(scripts.get(script_name), str) -def action_dotnet_build(args): +def action_dotnet_build(args: argparse.Namespace) -> tuple[int, StepResult]: return run_dotnet_action(args.project_root, args.working_dir, "build") -def action_dotnet_restore(args): +def action_dotnet_restore(args: argparse.Namespace) -> tuple[int, StepResult]: return run_dotnet_action(args.project_root, args.working_dir, "restore") -def action_dotnet_test(args): +def action_dotnet_test(args: argparse.Namespace) -> tuple[int, StepResult]: return run_dotnet_action(args.project_root, args.working_dir, "test") -def action_dotnet_publish(args): +def action_dotnet_publish(args: argparse.Namespace) -> tuple[int, StepResult]: return run_dotnet_action(args.project_root, args.working_dir, "publish") -def action_npm_install(args): +def action_npm_install(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "package.json").exists(): return 0, { @@ -224,10 +232,10 @@ def action_npm_install(args): "skip_reason": "not_applicable_no_package_json", } step = run_step("npm", ["install"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_npm_ci(args): +def action_npm_ci(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "package.json").exists(): return 0, { @@ -241,10 +249,10 @@ def action_npm_ci(args): "skip_reason": "not_applicable_no_package_json", } step = run_step("npm", ["ci"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_npm_build(args): +def action_npm_build(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "package.json").exists(): return 0, { @@ -283,12 +291,12 @@ def action_npm_build(args): if deps.get("reason") == "install_failed": step = deps["step"] step["failure_reason"] = "deps_install_failed" - return step["exit_code"], step + return int(step["exit_code"]), step step = run_step("npm", ["run", "build"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_npm_test(args): +def action_npm_test(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "package.json").exists(): return 0, { @@ -327,12 +335,12 @@ def action_npm_test(args): if deps.get("reason") == "install_failed": step = deps["step"] step["failure_reason"] = "deps_install_failed" - return step["exit_code"], step + return int(step["exit_code"]), step step = run_step("npm", ["test"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_npm_audit(args): +def action_npm_audit(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "package.json").exists(): return 0, { @@ -346,37 +354,37 @@ def action_npm_audit(args): "skip_reason": "not_applicable_no_package_json", } step = run_step("npm", ["audit"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_python_venv_create(args): +def action_python_venv_create(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, ".") - venv_dir = args.venv_dir or ".venv" + venv_dir = str(args.venv_dir) if hasattr(args, "venv_dir") else ".venv" step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_python_pip_install(args): +def action_python_pip_install(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, ".") - req = args.requirements + req = str(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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_python_pip_sync(args): +def action_python_pip_sync(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, ".") - req = args.requirements + req = str(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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_python_pytest(args): +def action_python_pytest(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_cargo_build(args): +def action_cargo_build(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "Cargo.toml").exists(): return 0, { @@ -390,10 +398,10 @@ def action_cargo_build(args): "skip_reason": "not_applicable_no_cargo_toml", } step = run_step("cargo", ["build"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_cargo_test(args): +def action_cargo_test(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) if not (pathlib.Path(cwd) / "Cargo.toml").exists(): return 0, { @@ -407,10 +415,10 @@ def action_cargo_test(args): "skip_reason": "not_applicable_no_cargo_toml", } step = run_step("cargo", ["test"], cwd) - return 0 if step["exit_code"] == 0 else step["exit_code"], step + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_tauri_build(args): +def action_tauri_build(args: argparse.Namespace) -> tuple[int, StepResult]: cwd = resolve_cwd(args.project_root, args.working_dir) tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json" if not tauri_conf.exists(): @@ -431,133 +439,94 @@ def action_tauri_build(args): if deps.get("reason") == "install_failed": step = deps["step"] step["failure_reason"] = "deps_install_failed" - return step["exit_code"], step + return int(step["exit_code"]), step tauri_args = ["run", "tauri", "build"] - if args.no_bundle: + if hasattr(args, "no_bundle") and 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_git_status(args): +def action_git_status(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_git_fetch(args): +def action_git_fetch(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_git_pull(args): +def action_git_pull(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_git_clean(args): +def action_git_clean(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_docker_build(args): +def action_docker_build(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_docker_compose_up(args): +def action_docker_compose_up(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def action_docker_compose_down(args): +def action_docker_compose_down(args: argparse.Namespace) -> tuple[int, StepResult]: 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 + return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step -def main(): +def main() -> int: 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) + 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) - p1 = sub.add_parser("dotnet-build") - parse_common(p1) + p4 = sub.add_parser("python-venv-create"); parse_common(p4) + _ = p4.add_argument("--venv-dir", default=".venv") - p1b = sub.add_parser("dotnet-test") - parse_common(p1b) + p5 = sub.add_parser("python-pip-install"); parse_common(p5) + _ = p5.add_argument("--requirements", required=True) - p1c = sub.add_parser("dotnet-publish") - parse_common(p1c) + p5b = sub.add_parser("python-pip-sync"); parse_common(p5b) + _ = p5b.add_argument("--requirements", required=True) - p2 = sub.add_parser("npm-install") - parse_common(p2) + 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) - p2b = sub.add_parser("npm-ci") - parse_common(p2b) + p7 = sub.add_parser("tauri-build"); parse_common(p7) + _ = p7.add_argument("--no-bundle", action="store_true") - 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) + 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() @@ -590,7 +559,8 @@ def main(): code, summary = handlers[args.action](args) if args.json: print(json.dumps(summary)) - return code + + return int(code) if __name__ == "__main__": diff --git a/Journal.DevTool/scripts/publish-app.py b/Journal.DevTool/scripts/publish-app.py index 9f82a8f..7a0cb5c 100644 --- a/Journal.DevTool/scripts/publish-app.py +++ b/Journal.DevTool/scripts/publish-app.py @@ -1,90 +1,42 @@ #!/usr/bin/env python3 import argparse - - -from script_common import find_node_app_root, resolve_repo_root, run, sha256_files +from typing import cast +from script_common import ensure_npm_build, find_node_app_root, resolve_repo_root 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") + _ = 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) + repo_root_val = cast(str | None, args.repo_root) + repo_root = resolve_repo_root(repo_root_val) + app_root_val = cast(str | None, args.app_root) + app_root = find_node_app_root(repo_root, app_root_val) 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) + # If dry-run is requested, we just print intent. + if args.dry_run: + print(f"Dry-run: Would build {args.target} ({args.configuration}) in {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]) + res = ensure_npm_build( + app_root=app_root, + target=str(args.target), + configuration=str(args.configuration), + tauri_bundles=str(args.tauri_bundles) + ) - print("$ npm " + " ".join(tauri_cmd)) - if not args.dry_run: - return run("npm", tauri_cmd, app_root) - - return 0 + return int(res["exit_code"]) if __name__ == "__main__": diff --git a/Journal.DevTool/scripts/publish-output.py b/Journal.DevTool/scripts/publish-output.py index 82c4aaa..8dcfd16 100644 --- a/Journal.DevTool/scripts/publish-output.py +++ b/Journal.DevTool/scripts/publish-output.py @@ -1,22 +1,11 @@ #!/usr/bin/env python3 import argparse -import json import shutil -import subprocess import sys +import os +import json from pathlib import Path - -from script_common import find_csproj_by_keyword, 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 - +from script_common import find_csproj_by_keyword, find_node_app_root, resolve_repo_root, run # type: ignore def has_package_script(app_root: Path, script_name: str) -> bool: package_json = app_root / "package.json" @@ -35,18 +24,18 @@ def has_package_script(app_root: Path, script_name: str) -> bool: 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") + _ = 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) @@ -56,45 +45,48 @@ def main() -> int: sidecar_project = (repo_root / args.sidecar_project).resolve() if args.sidecar_project else find_csproj_by_keyword(repo_root, ["sidecar"]) gateway_project = (repo_root / args.gateway_project).resolve() if args.gateway_project else find_csproj_by_keyword(repo_root, ["webgateway", "gateway"]) app_root = (repo_root / args.app_root).resolve() if args.app_root else find_node_app_root(repo_root, None) + tauri_conf = None if app_root is not None: - candidate_a = app_root / "src-tauri" / "tauri.conf.json" - candidate_b = app_root / "tauri.conf.json" - if candidate_a.exists(): - tauri_conf = candidate_a - elif candidate_b.exists(): - tauri_conf = candidate_b + tauri_conf = next((p for p in [app_root/"src-tauri"/"tauri.conf.json", app_root/"tauri.conf.json"] if p.exists()), None) py = sys.executable + scripts_dir = Path(__file__).parent + if not args.skip_sidecar: if sidecar_project is None: print("Skipping sidecar: no sidecar csproj detected.") else: - cmd = [py, "scripts/publish-sidecar.py", "--configuration", args.configuration, "--runtime", args.runtime] - cmd.extend(["--project", str(sidecar_project)]) - code = run_step("Publish sidecar", cmd, repo_root, args.dry_run) - if code != 0: - return code + cmd = ["-m", "scripts.publish-sidecar" if __package__ else "publish-sidecar", + "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(sidecar_project)] + print(f"\n> Publishing Sidecar\n$ {py} {' '.join(cmd)}") + if not args.dry_run: + code = run(py, cmd, scripts_dir) + if code != 0: return code if not args.skip_web: if app_root is None: - print("Skipping web: no app root with package.json detected.") + print("Skipping web: no app root detected.") elif not has_package_script(app_root, "build"): print("Skipping web: package.json has no 'build' script.") else: - cmd = [py, "scripts/publish-app.py", "--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)] - code = run_step("Build web", cmd, repo_root, args.dry_run) - if code != 0: - return code + cmd = ["-m", "scripts.publish-app" if __package__ else "publish-app", + "--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)] + print(f"\n> Building Web\n$ {py} {' '.join(cmd)}") + if not args.dry_run: + code = run(py, cmd, scripts_dir) + if code != 0: return code if not args.skip_webgateway: if gateway_project is None: print("Skipping web gateway: no gateway csproj detected.") else: - cmd = [py, "scripts/publish-webgateway.py", "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)] - code = run_step("Publish web gateway", cmd, repo_root, args.dry_run) - if code != 0: - return code + cmd = ["-m", "scripts.publish-webgateway" if __package__ else "publish-webgateway", + "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)] + print(f"\n> Publishing Web Gateway\n$ {py} {' '.join(cmd)}") + if not args.dry_run: + code = run(py, cmd, scripts_dir) + if code != 0: return code if not args.skip_tauri: if app_root is None or tauri_conf is None: @@ -102,17 +94,20 @@ def main() -> int: elif not has_package_script(app_root, "tauri"): print("Skipping tauri: package.json has no 'tauri' script.") else: - cmd = [py, "scripts/publish-app.py", "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)] - code = run_step("Build tauri", cmd, repo_root, args.dry_run) - if code != 0: - return code + cmd = ["-m", "scripts.publish-app" if __package__ else "publish-app", + "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)] + print(f"\n> Building Tauri\n$ {py} {' '.join(cmd)}") + if not args.dry_run: + code = run(py, cmd, scripts_dir) + if code != 0: return code 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) + pattern = "*.exe" if os.name == "nt" else "*" + exes = sorted((p for p in target_dir.glob(pattern) if p.is_file() and (os.name == "nt" or not p.suffix)), 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}") + if args.dry_run: print(f"Would copy: {exes[0]} -> {staged}") else: shutil.copy2(exes[0], staged) print(f"Staged desktop executable: {staged}") diff --git a/Journal.DevTool/scripts/publish-sidecar.ps1 b/Journal.DevTool/scripts/publish-sidecar.ps1 new file mode 100644 index 0000000..692ff55 --- /dev/null +++ b/Journal.DevTool/scripts/publish-sidecar.ps1 @@ -0,0 +1,10 @@ +param( + [Parameter(ValueFromRemainingArguments = $($true))] + [string[]]$ForwardArgs +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1') +Invoke-SdtPythonScript -ScriptName 'publish-sidecar.py' -ForwardArgs $ForwardArgs diff --git a/Journal.DevTool/scripts/publish-sidecar.py b/Journal.DevTool/scripts/publish-sidecar.py index 964f750..bbd4514 100644 --- a/Journal.DevTool/scripts/publish-sidecar.py +++ b/Journal.DevTool/scripts/publish-sidecar.py @@ -1,51 +1,48 @@ #!/usr/bin/env python3 import argparse - - -from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run +from typing import cast +from script_common import ensure_dotnet_publish, find_csproj_by_keyword, resolve_repo_root, SdtResult 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") + _ = 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() + repo_root_val = cast(str | None, args.repo_root) + repo_root = resolve_repo_root(repo_root_val) + output_dir_val = cast(str, args.output_dir) + output_dir = (repo_root / output_dir_val).resolve() output_dir.mkdir(parents=True, exist_ok=True) - if args.project: - csproj = (repo_root / args.project).resolve() + project_val = cast(str | None, args.project) + if project_val: + csproj = (repo_root / project_val).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 + res: SdtResult = ensure_dotnet_publish( + csproj=csproj, + output_dir=output_dir, + configuration=str(args.configuration), + runtime=str(args.runtime), + single_file=True, + self_contained=True + ) + + if res["exit_code"] != 0: + return int(res["exit_code"]) - binary_name = csproj.stem + (".exe" if args.runtime.startswith("win-") else "") + runtime_val = str(args.runtime) + binary_name = csproj.stem + (".exe" if runtime_val.startswith("win-") else "") binary_path = output_dir / binary_name if binary_path.exists(): print(f"Published executable: {binary_path}") diff --git a/Journal.DevTool/scripts/publish-webgateway.py b/Journal.DevTool/scripts/publish-webgateway.py index 6a2c9c0..9884b84 100644 --- a/Journal.DevTool/scripts/publish-webgateway.py +++ b/Journal.DevTool/scripts/publish-webgateway.py @@ -1,74 +1,67 @@ #!/usr/bin/env python3 import argparse import shutil - -from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run +from typing import cast +from script_common import ensure_dotnet_publish, find_csproj_by_keyword, resolve_repo_root, SdtResult 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") + _ = 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() + repo_root_val = cast(str | None, args.repo_root) + repo_root = resolve_repo_root(repo_root_val) + output_dir_val = cast(str, args.output_dir) + output_dir = (repo_root / output_dir_val).resolve() output_dir.mkdir(parents=True, exist_ok=True) - if args.project: - csproj = (repo_root / args.project).resolve() + project_val = cast(str | None, args.project) + if project_val: + csproj = (repo_root / project_val).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 + res: SdtResult = ensure_dotnet_publish( + csproj=csproj, + output_dir=output_dir, + configuration=str(args.configuration), + runtime=str(args.runtime), + self_contained=bool(args.self_contained), + single_file=False + ) + + if res["exit_code"] != 0: + return int(res["exit_code"]) if not args.skip_web_assets: - if args.web_build_dir: - web_build_dir = (repo_root / args.web_build_dir).resolve() + web_build_dir_val = cast(str | None, args.web_build_dir) + if web_build_dir_val: + web_build_dir = (repo_root / web_build_dir_val).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" + # Look for recent web build output + # (Note: rglob is costly but necessary for discovery here) + web_pj = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None) + web_build_dir = web_pj / "build" if web_pj else None 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"Copying web assets: {web_build_dir} -> {web_out}") + shutil.copytree(web_build_dir, web_out, dirs_exist_ok=True) + print(f"Copied web assets to {web_out}") print(f"Publish completed: {output_dir}") return 0 diff --git a/Journal.DevTool/scripts/script-common.ps1 b/Journal.DevTool/scripts/script-common.ps1 new file mode 100644 index 0000000..2e329bd --- /dev/null +++ b/Journal.DevTool/scripts/script-common.ps1 @@ -0,0 +1,133 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Legacy compatibility helper only. +# Active SDT workflows and shell bootstrap now use Python scripts. + +function Clear-SdtProxyEnv { + Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:ALL_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:http_proxy -ErrorAction SilentlyContinue + Remove-Item Env:https_proxy -ErrorAction SilentlyContinue + Remove-Item Env:all_proxy -ErrorAction SilentlyContinue + Remove-Item Env:GIT_HTTP_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:GIT_HTTPS_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:PIP_NO_INDEX -ErrorAction SilentlyContinue +} + +function Test-SdtConfigExists { + param([string]$Path) + if (Test-Path (Join-Path $Path "devtool.json")) { + return $true + } + $sdtConfigs = Get-ChildItem -Path $Path -Filter "sdtconfig-*.json" -File -ErrorAction SilentlyContinue + return ($null -ne $sdtConfigs) -and ($sdtConfigs.Count -gt 0) +} + +function Resolve-SdtRepoRoot { + param([string]$StartPath) + + $candidateStarts = @() + if (-not [string]::IsNullOrWhiteSpace($StartPath)) { + $candidateStarts += $StartPath + } + $cwd = (Get-Location).Path + if (-not [string]::IsNullOrWhiteSpace($cwd) -and ($candidateStarts -notcontains $cwd)) { + $candidateStarts += $cwd + } + + $override = $env:SDT_REPO_ROOT + if ([string]::IsNullOrWhiteSpace($override)) { + $override = $env:JOURNAL_REPO_ROOT # backward compatibility + } + if (-not [string]::IsNullOrWhiteSpace($override)) { + $overridePath = [System.IO.Path]::GetFullPath($override) + if (Test-SdtConfigExists -Path $overridePath) { + return $overridePath + } + } + + foreach ($start in $candidateStarts) { + $cursor = [System.IO.Path]::GetFullPath($start) + while (-not [string]::IsNullOrWhiteSpace($cursor)) { + if (Test-SdtConfigExists -Path $cursor) { + return $cursor + } + $parent = [System.IO.Directory]::GetParent($cursor) + if ($null -eq $parent -or $parent.FullName -eq $cursor) { + break + } + $cursor = $parent.FullName + } + } + + if (Get-Command git -ErrorAction SilentlyContinue) { + foreach ($start in $candidateStarts) { + try { + $gitRoot = & git -C $start rev-parse --show-toplevel 2>$null + if ($? -and -not [string]::IsNullOrWhiteSpace($gitRoot)) { + return [System.IO.Path]::GetFullPath($gitRoot.Trim()) + } + } + catch {} + } + } + + throw "Could not locate repository root. Ensure a project config (sdtconfig-*.json or devtool.json) exists in the project root." +} + +function Initialize-SdtDotnetEnv { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $dotnetCliHome = Join-Path $RepoRoot ".dotnet_home" + $nugetPackages = Join-Path $RepoRoot ".nuget\packages" + $nugetHttpCachePath = Join-Path $RepoRoot ".nuget\http-cache" + + $env:DOTNET_CLI_HOME = $dotnetCliHome + $env:NUGET_PACKAGES = $nugetPackages + $env:NUGET_HTTP_CACHE_PATH = $nugetHttpCachePath + $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" + + New-Item -ItemType Directory -Force -Path $dotnetCliHome, $nugetPackages, $nugetHttpCachePath | Out-Null +} + +function Initialize-SdtPipEnv { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $pipCacheDir = Join-Path $RepoRoot ".pip\cache" + $pipTempDir = Join-Path $RepoRoot ".tmp\pip-temp" + + $env:PIP_CACHE_DIR = $pipCacheDir + $env:TEMP = $pipTempDir + $env:TMP = $pipTempDir + $env:PIP_DISABLE_PIP_VERSION_CHECK = "1" + $env:PIP_DEFAULT_TIMEOUT = "30" + $env:PIP_RETRIES = "2" + + New-Item -ItemType Directory -Force -Path $pipCacheDir, $pipTempDir | Out-Null +} + +function Initialize-SdtHuggingFaceEnv { + param([Parameter(Mandatory = $true)][string]$RepoRoot) + + $hfHome = Join-Path $RepoRoot ".cache\huggingface" + $hfHubCache = Join-Path $hfHome "hub" + + $env:HF_HOME = $hfHome + $env:HUGGINGFACE_HUB_CACHE = $hfHubCache + $env:HF_HUB_DISABLE_SYMLINKS_WARNING = "1" + + New-Item -ItemType Directory -Force -Path $hfHubCache | Out-Null +} + +# Backward-compatible aliases (legacy script calls) +Set-Alias -Name Clear-JournalProxyEnv -Value Clear-SdtProxyEnv -Scope Script +Set-Alias -Name Resolve-JournalRepoRoot -Value Resolve-SdtRepoRoot -Scope Script +Set-Alias -Name Initialize-JournalDotnetEnv -Value Initialize-SdtDotnetEnv -Scope Script +Set-Alias -Name Initialize-JournalPipEnv -Value Initialize-SdtPipEnv -Scope Script +Set-Alias -Name Initialize-JournalHuggingFaceEnv -Value Initialize-SdtHuggingFaceEnv -Scope Script diff --git a/Journal.DevTool/scripts/script_common.py b/Journal.DevTool/scripts/script_common.py index 03faa87..7946702 100644 --- a/Journal.DevTool/scripts/script_common.py +++ b/Journal.DevTool/scripts/script_common.py @@ -6,357 +6,298 @@ import pathlib import shutil import subprocess import sys -from typing import Dict, Iterable, List, Sequence +import time +from typing import Any, Iterable, Optional, Sequence +# --- Domain: SDT Types --- +# SDT Normalized Result Object +# result["exit_code"]: int +# result["status"]: "ok" | "failed" | "skipped" +# result["elapsed_seconds"]: float +# result["failure_reason"]: Optional[str] +# result["skip_reason"]: Optional[str] +SdtResult = dict[str, Any] PROXY_VARS = [ - "HTTP_PROXY", - "HTTPS_PROXY", - "ALL_PROXY", - "http_proxy", - "https_proxy", - "all_proxy", - "GIT_HTTP_PROXY", - "GIT_HTTPS_PROXY", - "PIP_NO_INDEX", + "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy", + "GIT_HTTP_PROXY", "GIT_HTTPS_PROXY", "PIP_NO_INDEX" ] +# --- Domain: FS Utilities --- -def resolve_repo_root(start: str | None = None) -> pathlib.Path: - base = pathlib.Path(start or os.getcwd()).resolve() +def sha256_files(paths: Iterable[pathlib.Path]) -> str: + h = hashlib.sha256() + for p in sorted(paths): + if p.exists(): h.update(p.read_bytes()) + return h.hexdigest() - # 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: +def first_existing(paths: Iterable[pathlib.Path]) -> Optional[pathlib.Path]: for p in paths: - p.mkdir(parents=True, exist_ok=True) + if p.exists(): return p + return None +def newest_file(search_root: pathlib.Path, pattern: str) -> Optional[pathlib.Path]: + hits = sorted(search_root.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True) + return hits[0] if hits else None -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 ensure_dirs(paths: list[pathlib.Path]) -> None: + for p in paths: p.mkdir(parents=True, exist_ok=True) 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: - def _read_package_json(package_json: pathlib.Path) -> dict | None: - if not package_json.exists(): - return None - try: - data = json.loads(package_json.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else None - except Exception: - return None - - def _has_scripts(package_json: pathlib.Path, names: Sequence[str]) -> bool: - data = _read_package_json(package_json) - if not data: - return False - scripts = data.get("scripts") - if not isinstance(scripts, dict): - return False - for name in names: - value = scripts.get(name) - if isinstance(value, str) and value.strip(): + try: + has_glob = any(ch in h for ch in ("*", "?", "[")) + if has_glob: + if any(root.glob(h)): return True + return any(root.rglob(h)) + + marker = root / h + if marker.exists(): + return True + + # If hint is a plain filename marker, allow bounded search in root tree. + if not any(sep in h for sep in ("/", "\\")): + return any(p.name == h for p in root.rglob(h)) + + return False + except: return False - def _is_tauri_root(candidate_dir: pathlib.Path) -> bool: - return (candidate_dir / "src-tauri" / "tauri.conf.json").exists() or (candidate_dir / "tauri.conf.json").exists() +# --- Domain: Project Discovery --- - def _iter_package_jsons() -> list[pathlib.Path]: - excluded = {"node_modules", "dist", "build", ".git", ".sdt", ".venv", "venv", "bin", "obj"} - found: list[pathlib.Path] = [] - for current_root, dirs, files in os.walk(repo_root): - dirs[:] = [d for d in dirs if d not in excluded] - if "package.json" in files: - found.append(pathlib.Path(current_root) / "package.json") - found.sort(key=lambda p: len(p.parts)) - return found +def resolve_repo_root(start: str | None = None) -> pathlib.Path: + base = pathlib.Path(start or os.getcwd()).resolve() + for cur in [base, *base.parents]: + sdt_configs = list(cur.glob("sdtconfig-*.json")) + cfg = sdt_configs[0] if sdt_configs else (cur / "devtool.json") + if cfg.exists(): + hints = load_project_root_hints(cur, cfg) + if not hints or any(_hint_matches(cur, hint) for hint in hints): return cur + try: + proc = subprocess.run(["git", "-C", str(base), "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=False) + if proc.returncode == 0 and proc.stdout.strip(): return pathlib.Path(proc.stdout.strip()).resolve() + except: pass + return base - if preferred: - p = (repo_root / preferred).resolve() - package_json = p / "package.json" - if package_json.exists(): - # Keep explicit preferred root only when it appears runnable for node workflows. - if _is_tauri_root(p) or _has_scripts(package_json, ("build", "dev", "start", "test", "tauri")): - return p +def load_project_root_hints(repo_root: pathlib.Path, cfg: pathlib.Path | None = None) -> list[str]: + if cfg is None: + sdt_configs = list(repo_root.glob("sdtconfig-*.json")) + cfg = sdt_configs[0] if sdt_configs else (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: return [] - package_files = _iter_package_jsons() - if not package_files: - return None - - # Strong preference: a tauri app root with tauri config and package.json. - tauri_candidates = [p.parent for p in package_files if _is_tauri_root(p.parent)] - if len(tauri_candidates) == 1: - return tauri_candidates[0] - if len(tauri_candidates) > 1: - tauri_candidates.sort(key=lambda p: len(p.parts)) - return tauri_candidates[0] - - runnable_candidates = [ - p.parent for p in package_files if _has_scripts(p, ("build", "dev", "start", "test", "tauri")) - ] - if len(runnable_candidates) == 1: - return runnable_candidates[0] - if len(runnable_candidates) > 1: - runnable_candidates.sort(key=lambda p: len(p.parts)) - return runnable_candidates[0] - - # As a last fallback, return unique package root only. - if len(package_files) == 1: - return package_files[0].parent +def _resolve_project_config_path(repo_root: pathlib.Path) -> Optional[pathlib.Path]: + sdt_configs = sorted(repo_root.glob("sdtconfig-*.json"), key=lambda p: p.name.lower()) + if sdt_configs: + return sdt_configs[0] + legacy = repo_root / "devtool.json" + if legacy.exists(): + return legacy return None +def load_node_working_dir(repo_root: pathlib.Path) -> Optional[str]: + cfg = _resolve_project_config_path(repo_root) + if cfg is None: + return None + try: + data = json.loads(cfg.read_text(encoding="utf-8")) + node = data.get("toolchains", {}).get("node", {}) + value = node.get("workingDir") + if isinstance(value, str) and value.strip(): + return value.strip() + except: + pass + 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] +def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> Optional[pathlib.Path]: + if hints: + for h in hints: + p = (repo_root / h).resolve() + if p.exists() and p.suffix == ".csproj": return p + hits = [h for h in repo_root.rglob("*.csproj") if not any(x in h.parts for x in [".git", "node_modules", "bin", "obj"])] + return hits[0] if len(hits) == 1 else None + +def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> Optional[pathlib.Path]: + hits = [h for h in repo_root.rglob("*.csproj") if not any(x in h.parts for x in [".git", "node_modules", "bin", "obj"])] + for kw in keywords: + matches = [h for h in hits if kw.lower() in h.name.lower()] + if len(matches) == 1: return matches[0] + return None + +def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> Optional[pathlib.Path]: + def _read_pj(p: pathlib.Path): + try: return json.loads(p.read_text(encoding="utf-8")) + except: return None + def _has_scr(p: pathlib.Path, names: Sequence[str]): + src = _read_pj(p) + return any(src.get("scripts", {}).get(n) for n in names) if src else False + def _is_tauri(d: pathlib.Path): return (d / "src-tauri" / "tauri.conf.json").exists() or (d / "tauri.conf.json").exists() + def _iter_pj(): + excluded = {".git", "node_modules", ".sdt", "dist", "build", ".venv", "venv", "bin", "obj"} + for p in repo_root.rglob("package.json"): + if any(x in p.parts for x in excluded): + continue + yield p + + if not preferred: + preferred = load_node_working_dir(repo_root) + if preferred: + p = (repo_root / preferred).resolve() + p = p.parent if p.is_file() else p + if (p / "package.json").exists(): + return p + + tauri = [p.parent for p in _iter_pj() if _is_tauri(p.parent)] + if len(tauri) == 1: return tauri[0] + if len(tauri) > 1: + tauri = sorted(set(tauri), key=lambda d: (len(d.parts), str(d).lower())) + return tauri[0] + scripts = [p.parent for p in _iter_pj() if _has_scr(p, ["tauri", "build"])] + if len(scripts) == 1: return scripts[0] + if len(scripts) > 1: + scripts = sorted(set(scripts), key=lambda d: (len(d.parts), str(d).lower())) + return scripts[0] + + all_pj = [p.parent for p in _iter_pj()] + return all_pj[0] if len(all_pj) == 1 else None + +# --- Domain: Environment Setup --- + +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) + h, p, c = repo_root/".dotnet_home", repo_root/".nuget"/"packages", repo_root/".nuget"/"http-cache" + ensure_dirs([h, p, c]) + env.update({"DOTNET_CLI_HOME":str(h), "NUGET_PACKAGES":str(p), "NUGET_HTTP_CACHE_PATH":str(c), + "DOTNET_SKIP_FIRST_TIME_EXPERIENCE":"1", "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH":"0", + "DOTNET_GENERATE_ASPNET_CERTIFICATE":"0", + "DOTNET_CLI_TELEMETRY_OPTOUT":"1", "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) + c, t = repo_root/".pip"/"cache", repo_root/".tmp"/"pip-temp" + ensure_dirs([c, t]) + env.update({"PIP_CACHE_DIR":str(c), "PIP_DISABLE_PIP_VERSION_CHECK":"1", "PIP_DEFAULT_TIMEOUT":"30", "PIP_RETRIES":"2", "TEMP":str(t), "TMP":str(t)}) + return env + +# --- Domain: Process Execution --- + +def resolve_command(command: str) -> str: + if not command or os.name != "nt" or any(s in command for s in ("\\", "/")): return command + if pathlib.Path(command).suffix: return shutil.which(command) or command + low = command.lower() + cands = [f"{command}.cmd", f"{command}.exe", f"{command}.bat", command] if low in ("npm", "npx", "pnpm", "yarn", "tauri") else [command] + for c in cands: + found = _which_windows(c) + if found: + if pathlib.Path(found).name.lower() in ("npm", "npx", "pnpm", "yarn", "tauri"): + shim = pathlib.Path(found).with_name(pathlib.Path(found).name.lower() + ".cmd") + if shim.exists(): return str(shim) + return found + if low in ("npm", "npx", "pnpm", "yarn"): + node = _which_windows("node.exe") or _which_windows("node") + if node and (pathlib.Path(node).parent / f"{low}.cmd").exists(): return str(pathlib.Path(node).parent / f"{low}.cmd") + return cands[-1] + +def _which_windows(command: str) -> Optional[str]: + found = shutil.which(command) + if found: return str(pathlib.Path(found).resolve()) + for p in ["C:\\Program Files\\dotnet", "C:\\Program Files\\nodejs", "C:\\Program Files\\Git\\bin", "C:\\Program Files\\Git\\usr\\bin"]: + if (pathlib.Path(p) / command).exists(): return str((pathlib.Path(p) / command).resolve()) + return None + +def run(command: str, args: list[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> int: + try: + proc = subprocess.run([resolve_command(command), *args], cwd=str(cwd), env=env, check=False) + return proc.returncode + except: return 127 + +def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> tuple[int, str, str]: + try: + proc = subprocess.run([resolve_command(command), *args], cwd=str(cwd), env=env, capture_output=True, text=True, check=False, encoding="utf-8", errors="replace") + return proc.returncode, proc.stdout, proc.stderr + except: return 127, "", "Command not found" + +def _safe_stream_write(stream: Any, text: str) -> None: + if not text: + return + try: + stream.write(text) + return + except UnicodeEncodeError: + pass + + encoding = getattr(stream, "encoding", None) or "utf-8" + data = text.encode(encoding, errors="replace") + buffer = getattr(stream, "buffer", None) + if buffer is not None: + buffer.write(data) + buffer.flush() + return + + # Last resort when no binary buffer is exposed. + stream.write(data.decode(encoding, errors="replace")) + +def run_step_with_summary(label: str, command: str, args: Sequence[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> SdtResult: + print(f"\n> {label}\n$ {command} {' '.join(args)}") + start = time.time() + code, out, err = run_capture(command, args, cwd, env) + elapsed = round(time.time() - start, 3) + if out: _safe_stream_write(sys.stdout, out) + if err: _safe_stream_write(sys.stderr, err) + return {"exit_code": code, "status": "ok" if code == 0 else "failed", "elapsed_seconds": elapsed, "stdout": out, "stderr": err, "failure_reason": None, "skip_reason": None} + +# --- Domain: High-Level Build Logic --- + +def ensure_dotnet_publish(csproj: pathlib.Path, output_dir: pathlib.Path, configuration: str = "Release", runtime: str = "win-x64", single_file: bool = False, self_contained: bool = True) -> SdtResult: + repo_root = resolve_repo_root(str(csproj.parent)) + env = dotnet_env(repo_root) + args = ["publish", str(csproj), "-c", configuration, "-r", runtime, "--self-contained", "true" if self_contained else "false", + f"-p:PublishSingleFile={'true' if single_file else 'false'}", "-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false", "-o", str(output_dir)] + if single_file: args.append("-p:IncludeNativeLibrariesForSelfExtract=true") + res = run_step_with_summary(f"Publishing {csproj.name}", "dotnet", args, repo_root, env) + if res["exit_code"] != 0 and single_file: + comb = (res["stdout"] + res["stderr"]).lower() + if "generatebundle" in comb and "same bundlerelativepath" in comb: + print("Duplicate bundle entries detected; retrying without single-file optimization...") + retry = [a if "PublishSingleFile=true" not in a else "-p:PublishSingleFile=false" for a in args] + res = run_step_with_summary(f"Publishing {csproj.name} (retry)", "dotnet", retry, repo_root, env) + if res["exit_code"] != 0: + comb = (res["stdout"] + res["stderr"]).lower() + if "netsdk1152" in comb and "multiple publish output files with the same relative path" in comb: + print("Duplicate publish output files detected (NETSDK1152); retrying with ErrorOnDuplicatePublishOutputFiles=false...") + retry = list(args) + if not any("ErrorOnDuplicatePublishOutputFiles" in a for a in retry): + retry.append("-p:ErrorOnDuplicatePublishOutputFiles=false") + res = run_step_with_summary(f"Publishing {csproj.name} (dedupe retry)", "dotnet", retry, repo_root, env) + return res + +def ensure_npm_build(app_root: pathlib.Path, target: str = "web", configuration: str = "Release", tauri_bundles: str = "none") -> SdtResult: + pj, lock = app_root / "package.json", first_existing([app_root / "package-lock.json", app_root / "npm-shrinkwrap.json"]) + nm = app_root / "node_modules" + h_file, exp_h = nm / ".sdt-deps.sha256", sha256_files([pj, lock] if lock else [pj]) + should_inst = not nm.exists() or not h_file.exists() or h_file.read_text(encoding="utf-8").strip() != exp_h + if should_inst: + args = ["ci", "--no-audit", "--fund=false"] if lock else ["install", "--no-audit", "--fund=false"] + res = run_step_with_summary("Installing NPM dependencies", "npm", args, app_root) + if res["exit_code"] != 0 and lock and args[0] == "ci": + res = run_step_with_summary("Installing NPM dependencies (retry)", "npm", ["install", "--no-audit", "--fund=false"], app_root) + if res["exit_code"] != 0: return res + ensure_dirs([nm]); h_file.write_text(exp_h, encoding="utf-8") + if target == "web": return run_step_with_summary(f"Building Web ({configuration})", "npm", ["run", "build"], app_root) + t_cmd = ["run", "tauri", "build"] + tail = (["--no-bundle"] if tauri_bundles == "none" else ["--bundles", tauri_bundles]) + (["--debug"] if configuration == "Debug" else []) + if tail: t_cmd.extend(["--", *tail]) + return run_step_with_summary(f"Building Tauri ({configuration})", "npm", t_cmd, app_root) diff --git a/Journal.DevTool/scripts/sync-output.py b/Journal.DevTool/scripts/sync-output.py index 8a28a2b..10c1dc0 100644 --- a/Journal.DevTool/scripts/sync-output.py +++ b/Journal.DevTool/scripts/sync-output.py @@ -4,7 +4,7 @@ import os import shutil from pathlib import Path -from script_common import newest_file, resolve_repo_root +from script_common import newest_file, resolve_repo_root # type: ignore def copy_tree_contents(src: Path, dst: Path) -> None: @@ -21,55 +21,79 @@ def copy_tree_contents(src: Path, dst: Path) -> None: 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") + _ = 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 + web_build_dir_val = args.web_build_dir + web_build = (repo_root / web_build_dir_val).resolve() if web_build_dir_val 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 + sidecar_bin_dir_val = args.sidecar_bin_dir + sidecar_bin = (repo_root / sidecar_bin_dir_val).resolve() if sidecar_bin_dir_val 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) + sidecar_exe = None + if os.name == "nt": + sidecar_exe = newest_file(sidecar_bin, "*.exe") + else: + candidates = [p for p in sidecar_bin.rglob("*") if p.is_file() and not p.suffix and os.access(p, os.X_OK)] + sidecar_exe = max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None + 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 + gateway_bin_dir_val = args.gateway_bin_dir + gateway_bin = (repo_root / gateway_bin_dir_val).resolve() if gateway_bin_dir_val 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) + gw_exe = None + if os.name == "nt": + gw_exe = newest_file(gateway_bin, "*.exe") + else: + candidates = [p for p in gateway_bin.rglob("*") if p.is_file() and not p.suffix and os.access(p, os.X_OK)] + gw_exe = max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None + 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 + tauri_target_dir_val = args.tauri_target_dir + tauri_target = (repo_root / tauri_target_dir_val).resolve() if tauri_target_dir_val 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 + tauri_src = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None) + tauri_target = tauri_src / "target" if tauri_src else None + if tauri_target is not None: - app_exe = newest_file(tauri_target, "*.exe") + app_exe = None + if os.name == "nt": + app_exe = newest_file(tauri_target, "*.exe") + else: + candidates = [p for p in tauri_target.rglob("*") if p.is_file() and not p.suffix and os.access(p, os.X_OK)] + app_exe = max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None + 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}") diff --git a/Journal.DevTool/scripts/verify-workflow-routes.py b/Journal.DevTool/scripts/verify-workflow-routes.py index c03cbc5..1ca02ab 100644 --- a/Journal.DevTool/scripts/verify-workflow-routes.py +++ b/Journal.DevTool/scripts/verify-workflow-routes.py @@ -5,23 +5,23 @@ import pathlib import shutil import subprocess import sys -from typing import Any, Dict, List, Optional, Sequence, Tuple - +from typing import Any, Optional, Sequence from script_common import resolve_command, resolve_repo_root -def load_config(project_root: pathlib.Path) -> dict: - config_path = project_root / "devtool.json" +def load_config(project_root: pathlib.Path) -> dict[str, Any]: + sdt_configs = list(project_root.glob("sdtconfig-*.json")) + config_path = sdt_configs[0] if sdt_configs else (project_root / "devtool.json") if not config_path.exists(): - raise FileNotFoundError(f"devtool.json not found at: {config_path}") + raise FileNotFoundError(f"Project config not found at: {config_path}") return json.loads(config_path.read_text(encoding="utf-8")) -def iter_workflows(config: dict, selected: Optional[set[str]]) -> List[dict]: +def iter_workflows(config: dict[str, Any], selected: Optional[set[str]]) -> list[dict[str, Any]]: workflows = config.get("workflows", []) if not isinstance(workflows, list): return [] - normalized: List[dict] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)] + normalized: list[dict[str, Any]] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)] if selected: normalized = [w for w in normalized if w["id"] in selected] return normalized @@ -45,21 +45,25 @@ def resolve_script_arg(project_root: pathlib.Path, working_dir: pathlib.Path, ar return b -def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict: - result = { +def static_check_workflow(project_root: pathlib.Path, workflow: dict[str, Any]) -> dict[str, Any]: + result: dict[str, Any] = { "workflowId": workflow.get("id"), "ok": True, "issues": [], "steps": [], } - for step in workflow.get("steps", []): + steps = workflow.get("steps", []) + if not isinstance(steps, list): + return result + + for step in steps: if not isinstance(step, dict): continue step_id = step.get("id", "") - step_result = {"stepId": step_id, "ok": True, "issues": []} + step_result: dict[str, Any] = {"stepId": step_id, "ok": True, "issues": []} - working_dir_rel = step.get("workingDir") or "." + working_dir_rel = str(step.get("workingDir") or ".") working_dir = (project_root / working_dir_rel).resolve() if not working_dir.exists(): step_result["ok"] = False @@ -75,14 +79,13 @@ def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict: step_result["issues"].append(f"command_not_found:{command}") if command.lower() in ("python", "py", "python3", "python.exe", "py.exe"): - if args and isinstance(args[0], str) and args[0].endswith(".py"): + if isinstance(args, list) and args and isinstance(args[0], str) and args[0].endswith(".py"): script_path = resolve_script_arg(project_root, working_dir, args[0]) if not script_path.exists(): step_result["ok"] = False step_result["issues"].append(f"python_script_not_found:{script_path}") if isinstance(action, str) and action.strip(): - # Action-based steps still require workingDir existence for reliable execution. if not working_dir.exists(): step_result["ok"] = False step_result["issues"].append("action_working_dir_not_found") @@ -96,8 +99,8 @@ def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict: return result -def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]: - attempts: List[List[str]] = [] +def sdt_attempts(repo_root: pathlib.Path) -> list[list[str]]: + attempts: list[list[str]] = [] attempts.append(["sdt"]) if sys.platform.startswith("win"): attempts.append(["sdt.exe"]) @@ -110,9 +113,8 @@ def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]: if devtool_csproj.exists(): attempts.append(["dotnet", "run", "--project", str(devtool_csproj), "--"]) - # Preserve order but dedupe exact attempts. - seen = set() - unique: List[List[str]] = [] + seen: set[tuple[str, ...]] = set() + unique: list[list[str]] = [] for a in attempts: key = tuple(a) if key in seen: @@ -126,8 +128,8 @@ def try_run_sdt( repo_root: pathlib.Path, command_args: Sequence[str], timeout_seconds: int, -) -> Tuple[Optional[subprocess.CompletedProcess], Optional[str]]: - errors: List[str] = [] +) -> tuple[Optional[subprocess.CompletedProcess[str]], Optional[str]]: + errors: list[str] = [] for base in sdt_attempts(repo_root): cmd = [*base, *command_args] try: @@ -147,7 +149,7 @@ def try_run_sdt( return None, "; ".join(errors) if errors else "no_sdt_attempts" -def parse_headless_summary(stdout: str) -> Optional[Dict[str, Any]]: +def parse_headless_summary(stdout: str) -> Optional[dict[str, Any]]: lines = [line.strip() for line in stdout.splitlines() if line.strip()] for line in reversed(lines): if not line.startswith("{"): @@ -167,8 +169,8 @@ def execute_check_workflow( workflow_id: str, env_profile: Optional[str], timeout_seconds: int, -) -> dict: - args = [ +) -> dict[str, Any]: + run_args = [ "run", workflow_id, "--json", @@ -177,9 +179,9 @@ def execute_check_workflow( "--non-interactive", ] if env_profile: - args.extend(["--env-profile", env_profile]) + run_args.extend(["--env-profile", env_profile]) - proc, attempted = try_run_sdt(repo_root, args, timeout_seconds) + proc, attempted = try_run_sdt(repo_root, run_args, timeout_seconds) if proc is None: return { "workflowId": workflow_id, @@ -215,13 +217,13 @@ def main() -> int: parser = argparse.ArgumentParser( description="Verify SDT workflow routes (static path checks + optional headless execution)." ) - parser.add_argument("--repo-root", default=None, help="Repo root containing DevTool.csproj") - parser.add_argument("--project-root", default=".", help="Project root containing devtool.json") - parser.add_argument("--workflow", action="append", default=[], help="Workflow id to verify (repeatable)") - parser.add_argument("--execute", action="store_true", help="Also run each workflow via `sdt run ... --json`") - parser.add_argument("--env-profile", default=None) - parser.add_argument("--timeout-seconds", type=int, default=600) - parser.add_argument("--output-json", default=None, help="Write full report JSON to file") + _ = parser.add_argument("--repo-root", default=None, help="Repo root containing DevTool.csproj") + _ = parser.add_argument("--project-root", default=".", help="Project root containing devtool.json") + _ = parser.add_argument("--workflow", action="append", default=[], help="Workflow id to verify (repeatable)") + _ = parser.add_argument("--execute", action="store_true", help="Also run each workflow via `sdt run ... --json`") + _ = parser.add_argument("--env-profile", default=None) + _ = parser.add_argument("--timeout-seconds", type=int, default=600) + _ = parser.add_argument("--output-json", default=None, help="Write full report JSON to file") args = parser.parse_args() repo_root = resolve_repo_root(args.repo_root) @@ -235,10 +237,10 @@ def main() -> int: return 2 static_results = [static_check_workflow(project_root, w) for w in workflows] - execute_results: List[dict] = [] + execute_results: list[dict[str, Any]] = [] if args.execute: for w in workflows: - wid = w["id"] + wid = str(w["id"]) execute_results.append( execute_check_workflow( repo_root=repo_root, @@ -252,7 +254,7 @@ def main() -> int: static_failures = [r for r in static_results if not r["ok"]] exec_failures = [r for r in execute_results if not r["ok"]] - report = { + report: dict[str, Any] = { "repoRoot": str(repo_root), "projectRoot": str(project_root), "totalWorkflows": len(workflows), @@ -283,13 +285,13 @@ def main() -> int: if static_failures: print("\nStatic failures:") - for f in static_failures: - print(f"- {f['workflowId']}: {', '.join(f['issues'])}") + for sf in static_failures: + print(f"- {sf['workflowId']}: {', '.join(sf['issues'])}") if exec_failures: print("\nExecution failures:") - for f in exec_failures: - print(f"- {f['workflowId']}: stopReason={f.get('stopReason')} message={f.get('message')}") + for ef in exec_failures: + print(f"- {ef['workflowId']}: stopReason={ef.get('stopReason')} message={ef.get('message')}") return 1 if static_failures or exec_failures else 0 diff --git a/Journal.DevTool/sdt.dll b/Journal.DevTool/sdt.dll index faa199f..28942fd 100644 Binary files a/Journal.DevTool/sdt.dll and b/Journal.DevTool/sdt.dll differ diff --git a/Journal.DevTool/sdt.exe b/Journal.DevTool/sdt.exe index a8a4842..01e772a 100644 Binary files a/Journal.DevTool/sdt.exe and b/Journal.DevTool/sdt.exe differ diff --git a/Journal.DevTool/sdt.pdb b/Journal.DevTool/sdt.pdb index dca518d..99c890f 100644 Binary files a/Journal.DevTool/sdt.pdb and b/Journal.DevTool/sdt.pdb differ diff --git a/devtool.json b/devtool.json deleted file mode 100644 index a3aa0a8..0000000 --- a/devtool.json +++ /dev/null @@ -1,786 +0,0 @@ -{ - "name": "Project Journal", - "version": "0.1.0", - "targets": [], - "workflows": [ - { - "id": "sidecar", - "label": "Publish Sidecar", - "description": "Build Journal.Sidecar as self-contained exe \u2192 output/", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "sidecar:run", - "label": "Publish Sidecar", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-sidecar.ps1", - "-Configuration", - "Release", - "-Runtime", - "win-x64" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "web", - "label": "Build Web UI", - "description": "Build SvelteKit bundle \u2192 Journal.App/build/", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "web:run", - "label": "Build Web UI", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-app.ps1", - "-Target", - "web" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "sync-output", - "label": "Sync Build Assets to Output", - "description": "Sweep repo for newest builds (Web/Sidecar/Gateway/Tauri) and copy them to the output dir", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "sync-output:run", - "label": "Sync Build Assets to Output", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/sync-output.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "webgateway", - "label": "Publish WebGateway", - "description": "Publish ASP.NET host with embedded web UI \u2192 output/webgateway/", - "group": "Build", - "dependsOn": [ - "web" - ], - "steps": [ - { - "id": "webgateway:run", - "label": "Publish WebGateway", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-webgateway.ps1", - "-Configuration", - "Release", - "-Runtime", - "win-x64" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "tauri", - "label": "Build Tauri Desktop App", - "description": "Build desktop exe (no installer) \u2192 Journal.App/src-tauri/target/release/", - "group": "Build", - "dependsOn": [ - "sidecar" - ], - "steps": [ - { - "id": "tauri:run", - "label": "Build Tauri Desktop App", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-app.ps1", - "-Target", - "tauri", - "-TauriBundles", - "none" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - }, - { - "tool": "cargo", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "tauri-nsis", - "label": "Build Tauri \u002B NSIS Installer", - "description": "Build desktop exe with NSIS installer package", - "group": "Build", - "dependsOn": [ - "sidecar" - ], - "steps": [ - { - "id": "tauri-nsis:run", - "label": "Build Tauri \u002B NSIS Installer", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-app.ps1", - "-Target", - "tauri", - "-TauriBundles", - "nsis" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - }, - { - "tool": "cargo", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "build-dotnet", - "label": "Build .NET Projects", - "description": "dotnet build \u2014 all C# projects in solution", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "build-dotnet:run", - "label": "Build .NET Projects", - "command": "dotnet", - "args": [ - "build" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "all", - "label": "Full Release Build \u2726", - "description": "Sidecar \u2192 Web \u2192 WebGateway \u2192 Tauri, in dependency order", - "group": "Build", - "dependsOn": [ - "sidecar", - "web", - "webgateway", - "tauri" - ], - "steps": [] - }, - { - "id": "run-gateway-dev", - "label": "Run WebGateway Server (Dev)", - "description": "Start HTTP gateway via \u0027dotnet run\u0027 at http://localhost:5180", - "group": "Dev", - "dependsOn": [], - "steps": [ - { - "id": "run-gateway-dev:run", - "label": "Run WebGateway Server (Dev)", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/run-webgateway.ps1", - "-Mode", - "Dev" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "run-gateway-prod", - "label": "Run WebGateway Server (Output)", - "description": "Start compiled gateway from output/webgateway at http://localhost:5180", - "group": "Dev", - "dependsOn": [], - "steps": [ - { - "id": "run-gateway-prod:run", - "label": "Run WebGateway Server (Output)", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/run-webgateway.ps1", - "-Mode", - "Output" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "test", - "label": "Run Smoke Tests", - "description": "Run all ~80 integration tests in Journal.SmokeTests", - "group": "Test", - "dependsOn": [], - "steps": [ - { - "id": "test:run", - "label": "Run Smoke Tests", - "command": "dotnet", - "args": [ - "run", - "--project", - "Journal.SmokeTests/Journal.SmokeTests.csproj" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "gate", - "label": "Run Migration Gate", - "description": "Full build \u002B smoke tests \u002B parity check", - "group": "Test", - "dependsOn": [], - "steps": [ - { - "id": "gate:run", - "label": "Run Migration Gate", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/migration-gate.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "nuget-export", - "label": "Export NuGet Cache", - "description": "Prime and export .nuget cache to zip for offline use", - "group": "Cache", - "dependsOn": [], - "steps": [ - { - "id": "nuget-export:run", - "label": "Export NuGet Cache", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/nuget-export-cache.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "nuget-import", - "label": "Import NuGet Cache", - "description": "Import cache zip and validate restore", - "group": "Cache", - "dependsOn": [], - "steps": [ - { - "id": "nuget-import:run", - "label": "Import NuGet Cache", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/nuget-import-cache.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "npm-clean", - "label": "Clean Node Modules", - "description": "Remove Journal.App node_modules (kills node/tauri first)", - "group": "System", - "dependsOn": [], - "steps": [ - { - "id": "npm-clean:run", - "label": "Clean Node Modules", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/npm-clean.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "stage-output", - "label": "Stage Output Bundle", - "description": "Publish sidecar \u002B web \u002B webgateway \u002B tauri, then stage journalapp.exe into output/", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "stage-output:run", - "label": "Stage Output Bundle", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-output.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - }, - { - "tool": "cargo", - "installPolicy": "Prompt" - } - ] - } - ] - } - ], - "env": [ - { - "key": "JOURNAL_AI_PROVIDER", - "description": "AI provider bridge mode", - "default": "none", - "options": [ - "none", - "python-sidecar" - ] - }, - { - "key": "JOURNAL_LOG_LEVEL", - "description": "Log verbosity for C# backend", - "default": "warning", - "options": [ - "trace", - "debug", - "information", - "warning", - "error", - "critical" - ] - }, - { - "key": "JOURNAL_NLP_BACKEND", - "description": "Python NLP backend selection", - "default": "auto", - "options": [ - "auto", - "spacy", - "fallback" - ] - }, - { - "key": "JOURNAL_PROJECT_ROOT", - "description": "Override project root path (blank = auto-detect)", - "default": "", - "options": [] - }, - { - "key": "JOURNAL_VAULT_DIR", - "description": "Override vault directory path", - "default": "", - "options": [] - }, - { - "key": "JOURNAL_DATA_DIR", - "description": "Override decrypted data directory path", - "default": "", - "options": [] - }, - { - "key": "SDT_ENV_PROFILE", - "description": "Active SDT runtime environment profile", - "default": "dev", - "options": [ - "dev", - "ci", - "release" - ] - }, - { - "key": "SDT_LOG_LEVEL", - "description": "CLI log verbosity", - "default": "information", - "options": [ - "trace", - "debug", - "information", - "warning", - "error", - "critical" - ] - } - ], - "envProfiles": { - "active": "dev", - "profiles": [ - { - "id": "dev", - "description": "Local development defaults", - "inherits": [], - "values": { - "SDT_ENV_PROFILE": "dev", - "SDT_LOG_LEVEL": "information" - } - }, - { - "id": "ci", - "description": "Continuous integration defaults", - "inherits": [ - "dev" - ], - "values": { - "SDT_ENV_PROFILE": "ci", - "CI": "false", - "SDT_LOG_LEVEL": "warning" - } - }, - { - "id": "release", - "description": "Release build defaults", - "inherits": [ - "dev" - ], - "values": { - "SDT_ENV_PROFILE": "release", - "SDT_LOG_LEVEL": "warning" - } - } - ] - }, - "toolchains": { - "python": { - "executable": "python3.14", - "windowsExecutable": "py", - "launcherVersion": "-3.14", - "venvDir": ".venv", - "profiles": [ - { - "id": "cpu", - "label": "CPU only (default)", - "requirementsFile": "requirements_cpu_only.txt", - "extraIndexUrl": "https://download.pytorch.org/whl/cpu", - "postInstallCommands": [] - }, - { - "id": "gpu", - "label": "GPU / CUDA", - "requirementsFile": "requirements_gpu.txt", - "extraIndexUrl": null, - "postInstallCommands": [] - }, - { - "id": "nlp", - "label": "NLP / spaCy (optional)", - "requirementsFile": "requirements_nlp_optional.txt", - "extraIndexUrl": null, - "postInstallCommands": [ - "spacy download en_core_web_sm" - ] - } - ], - "pipScript": "scripts/pip-min.ps1" - }, - "node": { - "packageManager": "npm", - "workingDir": "Journal.App" - } - }, - "tooling": { - "tools": [ - { - "tool": "cargo", - "preferredInstallCommands": [], - "executables": [] - }, - { - "tool": "dotnet", - "preferredInstallCommands": [], - "executables": [] - }, - { - "tool": "node", - "preferredInstallCommands": [], - "executables": [] - }, - { - "tool": "npm", - "preferredInstallCommands": [], - "executables": [] - }, - { - "tool": "python", - "preferredInstallCommands": [], - "executables": [] - } - ] - }, - "project": null, - "debug": { - "profiles": [], - "diagnostics": { - "enabled": true, - "outputDir": ".sdt/debug", - "includeAllEnv": false, - "captureEnvKeys": [], - "redactSensitive": true, - "sensitiveKeyPatterns": [ - "TOKEN", - "SECRET", - "PASSWORD", - "PWD", - "CREDENTIAL", - "API_KEY", - "ACCESS_KEY", - "PRIVATE_KEY" - ], - "redactionAllowKeys": [], - "bundleOnFailure": true - } - } -} \ No newline at end of file diff --git a/devtool.json.bak-20260301-155008 b/devtool.json.bak-20260301-155008 deleted file mode 100644 index 0ef1cdd..0000000 --- a/devtool.json.bak-20260301-155008 +++ /dev/null @@ -1,677 +0,0 @@ -{ - "name": "Project Journal", - "version": "0.1.0", - "targets": [], - "workflows": [ - { - "id": "sidecar", - "label": "Publish Sidecar", - "description": "Build Journal.Sidecar as self-contained exe \u2192 output/", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "sidecar:run", - "label": "Publish Sidecar", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-sidecar.ps1", - "-Configuration", - "Release", - "-Runtime", - "win-x64" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "web", - "label": "Build Web UI", - "description": "Build SvelteKit bundle \u2192 Journal.App/build/", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "web:run", - "label": "Build Web UI", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-app.ps1", - "-Target", - "web" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "sync-output", - "label": "Sync Build Assets to Output", - "description": "Sweep repo for newest builds (Web/Sidecar/Gateway/Tauri) and copy them to the output dir", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "sync-output:run", - "label": "Sync Build Assets to Output", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/sync-output.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "webgateway", - "label": "Publish WebGateway", - "description": "Publish ASP.NET host with embedded web UI \u2192 output/webgateway/", - "group": "Build", - "dependsOn": [ - "web" - ], - "steps": [ - { - "id": "webgateway:run", - "label": "Publish WebGateway", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-webgateway.ps1", - "-Configuration", - "Release", - "-Runtime", - "win-x64" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "tauri", - "label": "Build Tauri Desktop App", - "description": "Build desktop exe (no installer) \u2192 Journal.App/src-tauri/target/release/", - "group": "Build", - "dependsOn": [ - "sidecar" - ], - "steps": [ - { - "id": "tauri:run", - "label": "Build Tauri Desktop App", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-app.ps1", - "-Target", - "tauri", - "-TauriBundles", - "none" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - }, - { - "tool": "cargo", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "tauri-nsis", - "label": "Build Tauri \u002B NSIS Installer", - "description": "Build desktop exe with NSIS installer package", - "group": "Build", - "dependsOn": [ - "sidecar" - ], - "steps": [ - { - "id": "tauri-nsis:run", - "label": "Build Tauri \u002B NSIS Installer", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-app.ps1", - "-Target", - "tauri", - "-TauriBundles", - "nsis" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - }, - { - "tool": "cargo", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "build-dotnet", - "label": "Build .NET Projects", - "description": "dotnet build \u2014 all C# projects in solution", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "build-dotnet:run", - "label": "Build .NET Projects", - "command": "dotnet", - "args": [ - "build" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "all", - "label": "Full Release Build \u2726", - "description": "Sidecar \u2192 Web \u2192 WebGateway \u2192 Tauri, in dependency order", - "group": "Build", - "dependsOn": [ - "sidecar", - "web", - "webgateway", - "tauri" - ], - "steps": [] - }, - { - "id": "run-gateway-dev", - "label": "Run WebGateway Server (Dev)", - "description": "Start HTTP gateway via \u0027dotnet run\u0027 at http://localhost:5180", - "group": "Dev", - "dependsOn": [], - "steps": [ - { - "id": "run-gateway-dev:run", - "label": "Run WebGateway Server (Dev)", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/run-webgateway.ps1", - "-Mode", - "Dev" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "run-gateway-prod", - "label": "Run WebGateway Server (Output)", - "description": "Start compiled gateway from output/webgateway at http://localhost:5180", - "group": "Dev", - "dependsOn": [], - "steps": [ - { - "id": "run-gateway-prod:run", - "label": "Run WebGateway Server (Output)", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/run-webgateway.ps1", - "-Mode", - "Output" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "test", - "label": "Run Smoke Tests", - "description": "Run all ~80 integration tests in Journal.SmokeTests", - "group": "Test", - "dependsOn": [], - "steps": [ - { - "id": "test:run", - "label": "Run Smoke Tests", - "command": "dotnet", - "args": [ - "run", - "--project", - "Journal.SmokeTests/Journal.SmokeTests.csproj" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "gate", - "label": "Run Migration Gate", - "description": "Full build \u002B smoke tests \u002B parity check", - "group": "Test", - "dependsOn": [], - "steps": [ - { - "id": "gate:run", - "label": "Run Migration Gate", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/migration-gate.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "nuget-export", - "label": "Export NuGet Cache", - "description": "Prime and export .nuget cache to zip for offline use", - "group": "Cache", - "dependsOn": [], - "steps": [ - { - "id": "nuget-export:run", - "label": "Export NuGet Cache", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/nuget-export-cache.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "nuget-import", - "label": "Import NuGet Cache", - "description": "Import cache zip and validate restore", - "group": "Cache", - "dependsOn": [], - "steps": [ - { - "id": "nuget-import:run", - "label": "Import NuGet Cache", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/nuget-import-cache.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "npm-clean", - "label": "Clean Node Modules", - "description": "Remove Journal.App node_modules (kills node/tauri first)", - "group": "System", - "dependsOn": [], - "steps": [ - { - "id": "npm-clean:run", - "label": "Clean Node Modules", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/npm-clean.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - } - ] - } - ] - }, - { - "id": "stage-output", - "label": "Stage Output Bundle", - "description": "Publish sidecar \u002B web \u002B webgateway \u002B tauri, then stage journalapp.exe into output/", - "group": "Build", - "dependsOn": [], - "steps": [ - { - "id": "stage-output:run", - "label": "Stage Output Bundle", - "command": "pwsh", - "args": [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - "scripts/publish-output.ps1" - ], - "workingDir": ".", - "action": null, - "actionArgs": [], - "requires": [ - { - "tool": "python", - "installPolicy": "Prompt" - }, - { - "tool": "dotnet", - "installPolicy": "Prompt" - }, - { - "tool": "node", - "installPolicy": "Prompt" - }, - { - "tool": "npm", - "installPolicy": "Prompt" - }, - { - "tool": "cargo", - "installPolicy": "Prompt" - } - ] - } - ] - } - ], - "env": [ - { - "key": "JOURNAL_AI_PROVIDER", - "description": "AI provider bridge mode", - "default": "none", - "options": [ - "none", - "python-sidecar" - ] - }, - { - "key": "JOURNAL_LOG_LEVEL", - "description": "Log verbosity for C# backend", - "default": "warning", - "options": [ - "trace", - "debug", - "information", - "warning", - "error", - "critical" - ] - }, - { - "key": "JOURNAL_NLP_BACKEND", - "description": "Python NLP backend selection", - "default": "auto", - "options": [ - "auto", - "spacy", - "fallback" - ] - }, - { - "key": "JOURNAL_PROJECT_ROOT", - "description": "Override project root path (blank = auto-detect)", - "default": "", - "options": [] - }, - { - "key": "JOURNAL_VAULT_DIR", - "description": "Override vault directory path", - "default": "", - "options": [] - }, - { - "key": "JOURNAL_DATA_DIR", - "description": "Override decrypted data directory path", - "default": "", - "options": [] - } - ], - "toolchains": { - "python": { - "executable": "python3.14", - "windowsExecutable": "py", - "launcherVersion": "-3.14", - "venvDir": ".venv", - "profiles": [ - { - "id": "cpu", - "label": "CPU only (default)", - "requirementsFile": "requirements_cpu_only.txt", - "extraIndexUrl": "https://download.pytorch.org/whl/cpu", - "postInstallCommands": [] - }, - { - "id": "gpu", - "label": "GPU / CUDA", - "requirementsFile": "requirements_gpu.txt", - "extraIndexUrl": null, - "postInstallCommands": [] - }, - { - "id": "nlp", - "label": "NLP / spaCy (optional)", - "requirementsFile": "requirements_nlp_optional.txt", - "extraIndexUrl": null, - "postInstallCommands": [ - "spacy download en_core_web_sm" - ] - } - ], - "pipScript": "scripts/pip-min.ps1" - }, - "node": { - "packageManager": "npm", - "workingDir": "Journal.App" - } - }, - "tooling": null, - "project": null, - "debug": null -} diff --git a/sdtconfig-journal.json b/sdtconfig-journal.json new file mode 100644 index 0000000..bf921d7 --- /dev/null +++ b/sdtconfig-journal.json @@ -0,0 +1,558 @@ +{ + "name": "journal", + "version": "0.1.0", + "targets": [], + "workflows": [ + { + "id": "sidecar", + "label": "Publish Sidecar", + "description": "Publish sidecar service", + "group": "Build", + "dependsOn": [], + "requireFiles": [ + "Journal.DevTool\\scripts\\publish-sidecar.py" + ], + "steps": [ + { + "id": "sidecar:run", + "label": "python Journal.DevTool\\scripts\\publish-sidecar.py", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\publish-sidecar.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "web", + "label": "Build Web UI", + "description": "Build frontend assets", + "group": "Build", + "dependsOn": [], + "requireFiles": [ + "Journal.DevTool\\scripts\\publish-app.py" + ], + "steps": [ + { + "id": "web:run", + "label": "python Journal.DevTool\\scripts\\publish-app.py --target web", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\publish-app.py", + "--target", + "web" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "tauri", + "label": "Build Tauri Desktop App", + "description": "Build desktop binary", + "group": "Build", + "dependsOn": [ + "sidecar" + ], + "requireFiles": [ + "Journal.DevTool\\scripts\\publish-app.py", + "Journal.App/src-tauri/tauri.conf.json" + ], + "steps": [ + { + "id": "tauri:run", + "label": "python Journal.DevTool\\scripts\\publish-app.py --target tauri --tauri-bundles none", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\publish-app.py", + "--target", + "tauri", + "--tauri-bundles", + "none" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "webgateway", + "label": "Publish WebGateway", + "description": "Publish ASP.NET gateway", + "group": "Build", + "dependsOn": [ + "web" + ], + "requireFiles": [ + "Journal.DevTool\\scripts\\publish-webgateway.py" + ], + "steps": [ + { + "id": "webgateway:run", + "label": "python Journal.DevTool\\scripts\\publish-webgateway.py", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\publish-webgateway.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "sync-output", + "label": "Sync Output", + "description": "Sync newest artifacts to output", + "group": "Build", + "dependsOn": [], + "requireFiles": [ + "Journal.DevTool\\scripts\\sync-output.py" + ], + "steps": [ + { + "id": "sync-output:run", + "label": "python Journal.DevTool\\scripts\\sync-output.py", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\sync-output.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "stage-output", + "label": "Stage Output Bundle", + "description": "Publish and stage distributable output", + "group": "Build", + "dependsOn": [], + "requireFiles": [ + "Journal.DevTool\\scripts\\publish-output.py" + ], + "steps": [ + { + "id": "stage-output:run", + "label": "python Journal.DevTool\\scripts\\publish-output.py", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\publish-output.py" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "run-gateway-dev", + "label": "Run WebGateway Server (Dev)", + "description": "Run gateway in development mode", + "group": "Dev", + "dependsOn": [], + "requireFiles": [ + "Journal.DevTool\\scripts\\run-webgateway.py" + ], + "steps": [ + { + "id": "run-gateway-dev:run", + "label": "python Journal.DevTool\\scripts\\run-webgateway.py --mode Dev", + "command": "python", + "args": [ + "Journal.DevTool\\scripts\\run-webgateway.py", + "--mode", + "Dev" + ], + "workingDir": ".", + "action": null, + "actionArgs": [], + "requires": [ + { + "tool": "python", + "installPolicy": "Prompt" + } + ] + } + ] + }, + { + "id": "build", + "label": "Build", + "description": "Build detected project stacks", + "group": "Build", + "dependsOn": [], + "requireFiles": [], + "steps": [ + { + "id": "dotnet-build", + "label": "dotnet build", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-build", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-build", + "label": "npm run build", + "command": null, + "args": [], + "workingDir": "Journal.App", + "action": "npm-build", + "actionArgs": [], + "requires": [] + }, + { + "id": "cargo-build", + "label": "cargo build", + "command": null, + "args": [], + "workingDir": ".", + "action": "cargo-build", + "actionArgs": [], + "requires": [] + }, + { + "id": "tauri-build", + "label": "tauri build", + "command": null, + "args": [], + "workingDir": "Journal.App", + "action": "tauri-build", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "deps-refresh", + "label": "Refresh Dependencies", + "description": "Restore/install dependency stacks", + "group": "Deps", + "dependsOn": [], + "requireFiles": [], + "steps": [ + { + "id": "dotnet-restore", + "label": "dotnet restore", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-restore", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-ci", + "label": "npm ci", + "command": null, + "args": [], + "workingDir": "Journal.App", + "action": "npm-ci", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "test", + "label": "Run Tests", + "description": "Run detected test stacks", + "group": "Test", + "dependsOn": [], + "requireFiles": [], + "steps": [ + { + "id": "dotnet-test", + "label": "dotnet test", + "command": null, + "args": [], + "workingDir": ".", + "action": "dotnet-test", + "actionArgs": [], + "requires": [] + }, + { + "id": "npm-test", + "label": "npm test", + "command": null, + "args": [], + "workingDir": "Journal.App", + "action": "npm-test", + "actionArgs": [], + "requires": [] + }, + { + "id": "cargo-test", + "label": "cargo test", + "command": null, + "args": [], + "workingDir": ".", + "action": "cargo-test", + "actionArgs": [], + "requires": [] + } + ] + }, + { + "id": "repo-health", + "label": "Repo Health", + "description": "Check repo status and fetch remotes", + "group": "Repo", + "dependsOn": [], + "requireFiles": [], + "steps": [ + { + "id": "git-status", + "label": "git status", + "command": null, + "args": [], + "workingDir": ".", + "action": "git-status", + "actionArgs": [], + "requires": [] + }, + { + "id": "git-fetch", + "label": "git fetch", + "command": null, + "args": [], + "workingDir": ".", + "action": "git-fetch", + "actionArgs": [], + "requires": [] + } + ] + } + ], + "env": [ + { + "key": "SDT_LOG_LEVEL", + "description": "CLI log verbosity", + "default": "information", + "options": [ + "trace", + "debug", + "information", + "warning", + "error", + "critical" + ] + } + ], + "envProfiles": { + "active": "dev", + "profiles": [ + { + "id": "dev", + "description": "Local development defaults", + "inherits": [], + "values": { + "SDT_ENV_PROFILE": "dev", + "SDT_LOG_LEVEL": "information" + } + }, + { + "id": "ci", + "description": "Continuous integration defaults", + "inherits": [ + "dev" + ], + "values": { + "SDT_ENV_PROFILE": "ci", + "CI": "true", + "SDT_LOG_LEVEL": "warning" + } + }, + { + "id": "release", + "description": "Release build defaults", + "inherits": [ + "dev" + ], + "values": { + "SDT_ENV_PROFILE": "release", + "SDT_LOG_LEVEL": "warning" + } + } + ] + }, + "toolchains": { + "python": null, + "node": { + "packageManager": "npm", + "workingDir": "Journal.App" + } + }, + "tooling": { + "defaultInstallPolicy": "Prompt", + "tools": [ + { + "tool": "cargo", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "dotnet", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "git", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "node", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "npm", + "preferredInstallCommands": [], + "executables": [] + }, + { + "tool": "tauri", + "preferredInstallCommands": [], + "executables": [] + } + ] + }, + "project": { + "type": "tauri", + "rootHints": [ + "*.sln", + ".git", + "Cargo.toml", + "package.json", + "tauri.conf.json" + ], + "artifacts": [ + "bin", + "obj", + ".sdt/debug" + ] + }, + "debug": { + "profiles": [ + { + "id": "dotnet-run", + "label": "Run .NET app", + "type": "dotnet", + "command": "dotnet", + "args": [ + "run" + ], + "workingDir": ".", + "env": {}, + "requires": [ + { + "tool": "dotnet", + "installPolicy": "Prompt" + } + ], + "attach": { + "kind": "manual", + "port": null, + "processName": null, + "note": "Attach your IDE debugger to the running dotnet process." + } + }, + { + "id": "npm-dev", + "label": "Run npm dev server", + "type": "node", + "command": "npm", + "args": [ + "run", + "dev" + ], + "workingDir": "Journal.App", + "env": {}, + "requires": [ + { + "tool": "node", + "installPolicy": "Prompt" + }, + { + "tool": "npm", + "installPolicy": "Prompt" + } + ], + "attach": null + } + ], + "diagnostics": { + "enabled": true, + "outputDir": ".sdt/debug", + "includeAllEnv": false, + "captureEnvKeys": [ + "SDT_LOG_LEVEL", + "DOTNET_CLI_HOME", + "NUGET_PACKAGES", + "PIP_CACHE_DIR", + "NVM_HOME", + "NVM_SYMLINK" + ], + "redactSensitive": true, + "sensitiveKeyPatterns": [ + "TOKEN", + "SECRET", + "PASSWORD", + "PWD", + "CREDENTIAL", + "API_KEY", + "ACCESS_KEY", + "PRIVATE_KEY" + ], + "redactionAllowKeys": [], + "bundleOnFailure": true + } + } +}