SDT is standalone now, journal can use the bridge.
sdt can compile journal and other projects. it using a json config system. this program's Repo exists on the Gitea under stan. Readme included as well.
This commit is contained in:
parent
ae70fbdae9
commit
ad199c338c
8
.gitignore
vendored
8
.gitignore
vendored
@ -56,10 +56,4 @@ journalapp(1).exe
|
||||
.cache/
|
||||
scripts/__pycache__/
|
||||
.sdt/
|
||||
devtool.backup.json
|
||||
sdt.deps.json
|
||||
sdt.dll
|
||||
sdt.exe
|
||||
sdt.pdb
|
||||
sdt.runtimeconfig.json
|
||||
Spectre.Console.dll
|
||||
devtool.backup.json
|
||||
BIN
Journal.DevTool/DevTool.Engine.dll
Normal file
BIN
Journal.DevTool/DevTool.Engine.dll
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Engine.pdb
Normal file
BIN
Journal.DevTool/DevTool.Engine.pdb
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Host.Bridge.dll
Normal file
BIN
Journal.DevTool/DevTool.Host.Bridge.dll
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Host.Bridge.pdb
Normal file
BIN
Journal.DevTool/DevTool.Host.Bridge.pdb
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Host.Tui.dll
Normal file
BIN
Journal.DevTool/DevTool.Host.Tui.dll
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Host.Tui.pdb
Normal file
BIN
Journal.DevTool/DevTool.Host.Tui.pdb
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Runtime.dll
Normal file
BIN
Journal.DevTool/DevTool.Runtime.dll
Normal file
Binary file not shown.
BIN
Journal.DevTool/DevTool.Runtime.pdb
Normal file
BIN
Journal.DevTool/DevTool.Runtime.pdb
Normal file
Binary file not shown.
BIN
Journal.DevTool/Spectre.Console.dll
Normal file
BIN
Journal.DevTool/Spectre.Console.dll
Normal file
Binary file not shown.
47
Journal.DevTool/scripts/_pwsh-python-shim.ps1
Normal file
47
Journal.DevTool/scripts/_pwsh-python-shim.ps1
Normal file
@ -0,0 +1,47 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Test-SdtIsWindows {
|
||||
if (Get-Variable -Name IsWindows -Scope Global -ErrorAction SilentlyContinue) {
|
||||
return [bool]$global:IsWindows
|
||||
}
|
||||
|
||||
return $env:OS -eq 'Windows_NT'
|
||||
}
|
||||
|
||||
function Resolve-SdtPython {
|
||||
$candidates = @('python')
|
||||
if (Test-SdtIsWindows) { $candidates += 'py' } else { $candidates += 'python3' }
|
||||
foreach ($c in $candidates) {
|
||||
try {
|
||||
& $c --version *> $null
|
||||
if ($LASTEXITCODE -eq 0) { return $c }
|
||||
} catch {}
|
||||
}
|
||||
return 'python'
|
||||
}
|
||||
|
||||
function Resolve-SdtScriptPath {
|
||||
param([Parameter(Mandatory=$true)][string]$ScriptName)
|
||||
|
||||
$bundled = Join-Path $PSScriptRoot $ScriptName
|
||||
if (Test-Path $bundled) { return $bundled }
|
||||
|
||||
$project = Join-Path (Join-Path $PSScriptRoot '..') ('scripts\\' + $ScriptName)
|
||||
if (Test-Path $project) { return (Resolve-Path $project).Path }
|
||||
|
||||
throw "Python helper script not found: $ScriptName"
|
||||
}
|
||||
|
||||
function Invoke-SdtPythonScript {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ScriptName,
|
||||
[string[]]$ForwardArgs = @()
|
||||
)
|
||||
|
||||
$python = Resolve-SdtPython
|
||||
$scriptPath = Resolve-SdtScriptPath -ScriptName $ScriptName
|
||||
|
||||
& $python $scriptPath @ForwardArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
597
Journal.DevTool/scripts/build.py
Normal file
597
Journal.DevTool/scripts/build.py
Normal file
@ -0,0 +1,597 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from script_common import resolve_command
|
||||
|
||||
|
||||
def run_step(command, args, cwd):
|
||||
resolved = resolve_command(command)
|
||||
if shutil.which(resolved) is None and not pathlib.Path(resolved).exists():
|
||||
return {
|
||||
"command": resolved,
|
||||
"args": args,
|
||||
"cwd": cwd,
|
||||
"exit_code": 127,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "failed",
|
||||
"failure_reason": f"command_not_found:{resolved}",
|
||||
}
|
||||
|
||||
started = time.time()
|
||||
proc = subprocess.run([resolved, *args], cwd=cwd, check=False)
|
||||
elapsed = round(time.time() - started, 3)
|
||||
return {
|
||||
"command": resolved,
|
||||
"args": args,
|
||||
"cwd": cwd,
|
||||
"exit_code": proc.returncode,
|
||||
"elapsed_seconds": elapsed,
|
||||
"status": "ok" if proc.returncode == 0 else "failed",
|
||||
"failure_reason": None if proc.returncode == 0 else f"non_zero_exit:{proc.returncode}",
|
||||
}
|
||||
|
||||
|
||||
def resolve_python_executable():
|
||||
candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"]
|
||||
for c in candidates:
|
||||
if shutil.which(c):
|
||||
return c
|
||||
return "python"
|
||||
|
||||
|
||||
def parse_common(parser):
|
||||
parser.add_argument("--project-root", required=True)
|
||||
parser.add_argument("--working-dir", default=".")
|
||||
parser.add_argument("--json", action="store_true")
|
||||
|
||||
|
||||
def resolve_cwd(project_root, working_dir):
|
||||
return os.path.abspath(os.path.join(project_root, working_dir))
|
||||
|
||||
|
||||
EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"}
|
||||
|
||||
|
||||
def discover_dotnet_target(project_root: str, cwd: str):
|
||||
# Prefer 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:
|
||||
return str(local_slnx[0])
|
||||
|
||||
local_sln = sorted(pathlib.Path(cwd).glob("*.sln"))
|
||||
if len(local_sln) == 1:
|
||||
return str(local_sln[0])
|
||||
|
||||
local_csproj = sorted(pathlib.Path(cwd).glob("*.csproj"))
|
||||
if len(local_csproj) == 1:
|
||||
return str(local_csproj[0])
|
||||
|
||||
slnx_hits = bounded_find_files(project_root, ".slnx", max_depth=4)
|
||||
if len(slnx_hits) == 1:
|
||||
return slnx_hits[0]
|
||||
|
||||
sln_hits = bounded_find_files(project_root, ".sln", max_depth=4)
|
||||
if len(sln_hits) == 1:
|
||||
return sln_hits[0]
|
||||
|
||||
csproj_hits = bounded_find_files(project_root, ".csproj", max_depth=4)
|
||||
if len(csproj_hits) == 1:
|
||||
return csproj_hits[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def bounded_find_files(root: str, extension: str, max_depth: int):
|
||||
root_path = pathlib.Path(root).resolve()
|
||||
results = []
|
||||
for current_root, dirs, files in os.walk(root_path):
|
||||
rel = pathlib.Path(current_root).resolve().relative_to(root_path)
|
||||
depth = len(rel.parts)
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDED_SCAN_DIRS]
|
||||
if depth > max_depth:
|
||||
dirs[:] = []
|
||||
continue
|
||||
|
||||
for name in files:
|
||||
if name.lower().endswith(extension.lower()):
|
||||
results.append(str(pathlib.Path(current_root) / name))
|
||||
return sorted(results)
|
||||
|
||||
|
||||
def run_dotnet_action(project_root, working_dir, verb):
|
||||
cwd = resolve_cwd(project_root, working_dir)
|
||||
target = discover_dotnet_target(project_root, cwd)
|
||||
if not target:
|
||||
return 0, {
|
||||
"command": "dotnet",
|
||||
"args": [verb],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_dotnet_target",
|
||||
"message": "No .slnx/.sln/.csproj found for this step. Skipping dotnet action.",
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _deps_hash(app_root):
|
||||
h = hashlib.sha256()
|
||||
for name in ("package.json", "package-lock.json"):
|
||||
p = pathlib.Path(app_root) / name
|
||||
if p.exists():
|
||||
h.update(p.read_bytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def ensure_npm_dependencies(app_root):
|
||||
package_json = pathlib.Path(app_root) / "package.json"
|
||||
if not package_json.exists():
|
||||
return {"installed": False, "reason": "not_applicable"}
|
||||
|
||||
node_modules = pathlib.Path(app_root) / "node_modules"
|
||||
deps_hash_file = node_modules / ".sdt-deps.sha256"
|
||||
expected = _deps_hash(app_root)
|
||||
|
||||
should_install = not node_modules.exists()
|
||||
if not should_install:
|
||||
if not deps_hash_file.exists():
|
||||
should_install = True
|
||||
else:
|
||||
current = deps_hash_file.read_text(encoding="utf-8").strip()
|
||||
should_install = current != expected
|
||||
|
||||
if not should_install:
|
||||
return {"installed": False, "reason": "deps_unchanged"}
|
||||
|
||||
lock_exists = (pathlib.Path(app_root) / "package-lock.json").exists()
|
||||
install_args = ["ci", "--no-audit", "--fund=false"] if lock_exists else ["install", "--no-audit", "--fund=false"]
|
||||
install_step = run_step("npm", install_args, app_root)
|
||||
if install_step["exit_code"] != 0:
|
||||
if lock_exists and install_args[0] == "ci":
|
||||
fallback = run_step("npm", ["install", "--no-audit", "--fund=false"], app_root)
|
||||
if fallback["exit_code"] != 0:
|
||||
fallback["failure_reason"] = "deps_install_failed_after_ci_fallback"
|
||||
return {"installed": True, "reason": "install_failed", "step": fallback}
|
||||
install_step = fallback
|
||||
else:
|
||||
return {"installed": True, "reason": "install_failed", "step": install_step}
|
||||
|
||||
node_modules.mkdir(parents=True, exist_ok=True)
|
||||
deps_hash_file.write_text(expected, encoding="utf-8")
|
||||
return {"installed": True, "reason": "installed", "step": install_step}
|
||||
|
||||
|
||||
def read_package_json(cwd: str):
|
||||
package_json = pathlib.Path(cwd) / "package.json"
|
||||
if not package_json.exists():
|
||||
return None
|
||||
try:
|
||||
return json.loads(package_json.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def has_npm_script(cwd: str, script_name: str) -> bool:
|
||||
data = read_package_json(cwd)
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
scripts = data.get("scripts")
|
||||
if not isinstance(scripts, dict):
|
||||
return False
|
||||
return script_name in scripts and isinstance(scripts.get(script_name), str)
|
||||
|
||||
|
||||
def action_dotnet_build(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "build")
|
||||
|
||||
|
||||
def action_dotnet_restore(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "restore")
|
||||
|
||||
|
||||
def action_dotnet_test(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "test")
|
||||
|
||||
|
||||
def action_dotnet_publish(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "publish")
|
||||
|
||||
|
||||
def action_npm_install(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["install"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"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
|
||||
|
||||
|
||||
def action_npm_ci(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["ci"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"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
|
||||
|
||||
|
||||
def action_npm_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if not has_npm_script(cwd, "build"):
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_missing_build_script",
|
||||
}
|
||||
deps = ensure_npm_dependencies(cwd)
|
||||
if deps.get("reason") == "not_applicable":
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if deps.get("reason") == "install_failed":
|
||||
step = deps["step"]
|
||||
step["failure_reason"] = "deps_install_failed"
|
||||
return step["exit_code"], step
|
||||
step = run_step("npm", ["run", "build"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_npm_test(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if not has_npm_script(cwd, "test"):
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_missing_test_script",
|
||||
}
|
||||
deps = ensure_npm_dependencies(cwd)
|
||||
if deps.get("reason") == "not_applicable":
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if deps.get("reason") == "install_failed":
|
||||
step = deps["step"]
|
||||
step["failure_reason"] = "deps_install_failed"
|
||||
return step["exit_code"], step
|
||||
step = run_step("npm", ["test"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_npm_audit(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["audit"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"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
|
||||
|
||||
|
||||
def action_python_venv_create(args):
|
||||
cwd = resolve_cwd(args.project_root, ".")
|
||||
venv_dir = args.venv_dir or ".venv"
|
||||
step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_pip_install(args):
|
||||
cwd = resolve_cwd(args.project_root, ".")
|
||||
req = args.requirements
|
||||
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_pip_sync(args):
|
||||
cwd = resolve_cwd(args.project_root, ".")
|
||||
req = args.requirements
|
||||
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_pytest(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_cargo_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
||||
return 0, {
|
||||
"command": "cargo",
|
||||
"args": ["build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"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
|
||||
|
||||
|
||||
def action_cargo_test(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
||||
return 0, {
|
||||
"command": "cargo",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"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
|
||||
|
||||
|
||||
def action_tauri_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json"
|
||||
if not tauri_conf.exists():
|
||||
tauri_conf = pathlib.Path(cwd) / "tauri.conf.json"
|
||||
if not tauri_conf.exists() or not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "tauri", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_tauri_project",
|
||||
}
|
||||
|
||||
deps = ensure_npm_dependencies(cwd)
|
||||
if deps.get("reason") == "install_failed":
|
||||
step = deps["step"]
|
||||
step["failure_reason"] = "deps_install_failed"
|
||||
return step["exit_code"], step
|
||||
|
||||
tauri_args = ["run", "tauri", "build"]
|
||||
if args.no_bundle:
|
||||
tauri_args.extend(["--", "--no-bundle"])
|
||||
step = run_step("npm", tauri_args, cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_status(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["status"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_fetch(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["fetch"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_pull(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["pull"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_clean(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["clean", "-fd"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_docker_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("docker", ["build", "."], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_docker_compose_up(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("docker", ["compose", "up", "-d"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_docker_compose_down(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("docker", ["compose", "down"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SDT normalized build actions")
|
||||
sub = parser.add_subparsers(dest="action", required=True)
|
||||
|
||||
p0 = sub.add_parser("dotnet-restore")
|
||||
parse_common(p0)
|
||||
|
||||
p1 = sub.add_parser("dotnet-build")
|
||||
parse_common(p1)
|
||||
|
||||
p1b = sub.add_parser("dotnet-test")
|
||||
parse_common(p1b)
|
||||
|
||||
p1c = sub.add_parser("dotnet-publish")
|
||||
parse_common(p1c)
|
||||
|
||||
p2 = sub.add_parser("npm-install")
|
||||
parse_common(p2)
|
||||
|
||||
p2b = sub.add_parser("npm-ci")
|
||||
parse_common(p2b)
|
||||
|
||||
p3 = sub.add_parser("npm-build")
|
||||
parse_common(p3)
|
||||
|
||||
p3b = sub.add_parser("npm-test")
|
||||
parse_common(p3b)
|
||||
|
||||
p3c = sub.add_parser("npm-audit")
|
||||
parse_common(p3c)
|
||||
|
||||
p4 = sub.add_parser("python-venv-create")
|
||||
parse_common(p4)
|
||||
p4.add_argument("--venv-dir", default=".venv")
|
||||
|
||||
p5 = sub.add_parser("python-pip-install")
|
||||
parse_common(p5)
|
||||
p5.add_argument("--requirements", required=True)
|
||||
|
||||
p5b = sub.add_parser("python-pip-sync")
|
||||
parse_common(p5b)
|
||||
p5b.add_argument("--requirements", required=True)
|
||||
|
||||
p5c = sub.add_parser("python-pytest")
|
||||
parse_common(p5c)
|
||||
|
||||
p6 = sub.add_parser("cargo-build")
|
||||
parse_common(p6)
|
||||
|
||||
p6b = sub.add_parser("cargo-test")
|
||||
parse_common(p6b)
|
||||
|
||||
p7 = sub.add_parser("tauri-build")
|
||||
parse_common(p7)
|
||||
p7.add_argument("--no-bundle", action="store_true")
|
||||
|
||||
p8 = sub.add_parser("git-status")
|
||||
parse_common(p8)
|
||||
|
||||
p9 = sub.add_parser("git-fetch")
|
||||
parse_common(p9)
|
||||
|
||||
p10 = sub.add_parser("git-pull")
|
||||
parse_common(p10)
|
||||
|
||||
p11 = sub.add_parser("git-clean")
|
||||
parse_common(p11)
|
||||
|
||||
p12 = sub.add_parser("docker-build")
|
||||
parse_common(p12)
|
||||
|
||||
p13 = sub.add_parser("docker-compose-up")
|
||||
parse_common(p13)
|
||||
|
||||
p14 = sub.add_parser("docker-compose-down")
|
||||
parse_common(p14)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
handlers = {
|
||||
"dotnet-restore": action_dotnet_restore,
|
||||
"dotnet-build": action_dotnet_build,
|
||||
"dotnet-test": action_dotnet_test,
|
||||
"dotnet-publish": action_dotnet_publish,
|
||||
"npm-install": action_npm_install,
|
||||
"npm-ci": action_npm_ci,
|
||||
"npm-build": action_npm_build,
|
||||
"npm-test": action_npm_test,
|
||||
"npm-audit": action_npm_audit,
|
||||
"python-venv-create": action_python_venv_create,
|
||||
"python-pip-install": action_python_pip_install,
|
||||
"python-pip-sync": action_python_pip_sync,
|
||||
"python-pytest": action_python_pytest,
|
||||
"cargo-build": action_cargo_build,
|
||||
"cargo-test": action_cargo_test,
|
||||
"tauri-build": action_tauri_build,
|
||||
"git-status": action_git_status,
|
||||
"git-fetch": action_git_fetch,
|
||||
"git-pull": action_git_pull,
|
||||
"git-clean": action_git_clean,
|
||||
"docker-build": action_docker_build,
|
||||
"docker-compose-up": action_docker_compose_up,
|
||||
"docker-compose-down": action_docker_compose_down,
|
||||
}
|
||||
|
||||
code, summary = handlers[args.action](args)
|
||||
if args.json:
|
||||
print(json.dumps(summary))
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
17
Journal.DevTool/scripts/dev-shell.cmd
Normal file
17
Journal.DevTool/scripts/dev-shell.cmd
Normal file
@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
|
||||
where py >nul 2>nul
|
||||
if %ERRORLEVEL%==0 (
|
||||
set "PYEXE=py"
|
||||
) else (
|
||||
where python >nul 2>nul
|
||||
if not %ERRORLEVEL%==0 (
|
||||
echo python not found.
|
||||
exit /b 1
|
||||
)
|
||||
set "PYEXE=python"
|
||||
)
|
||||
|
||||
for /f "usebackq delims=" %%L in (`"%PYEXE%" "%SCRIPT_DIR%dev_shell.py" export --shell cmd`) do %%L
|
||||
echo Development shell initialized from Python bootstrap script.
|
||||
21
Journal.DevTool/scripts/dev-shell.ps1
Normal file
21
Journal.DevTool/scripts/dev-shell.ps1
Normal file
@ -0,0 +1,21 @@
|
||||
# Run this in PowerShell before development commands:
|
||||
# . ./scripts/dev-shell.ps1
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1')
|
||||
|
||||
$scriptPath = Resolve-SdtScriptPath -ScriptName 'dev_shell.py'
|
||||
$python = Resolve-SdtPython
|
||||
|
||||
$lines = & $python $scriptPath export --shell pwsh
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to initialize development shell via dev_shell.py"
|
||||
}
|
||||
|
||||
foreach ($line in $lines) {
|
||||
Invoke-Expression $line
|
||||
}
|
||||
|
||||
Write-Host "Development shell initialized from Python bootstrap script."
|
||||
16
Journal.DevTool/scripts/dev-shell.sh
Normal file
16
Journal.DevTool/scripts/dev-shell.sh
Normal file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON_EXE="python3"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PYTHON_EXE="python"
|
||||
else
|
||||
echo "python3/python not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eval "$("$PYTHON_EXE" "$SCRIPT_DIR/dev_shell.py" export --shell bash)"
|
||||
echo "Development shell initialized from Python bootstrap script."
|
||||
148
Journal.DevTool/scripts/dev_shell.py
Normal file
148
Journal.DevTool/scripts/dev_shell.py
Normal file
@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
from script_common import PROXY_VARS, clean_proxy_env, dotnet_env, ensure_dirs, pip_env, resolve_repo_root
|
||||
|
||||
|
||||
def huggingface_env(repo_root: pathlib.Path) -> dict[str, str]:
|
||||
env = {}
|
||||
hf_home = repo_root / ".cache" / "huggingface"
|
||||
hf_hub_cache = hf_home / "hub"
|
||||
ensure_dirs([hf_hub_cache])
|
||||
env["HF_HOME"] = str(hf_home)
|
||||
env["HUGGINGFACE_HUB_CACHE"] = str(hf_hub_cache)
|
||||
env["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
||||
return env
|
||||
|
||||
|
||||
def resolved_env(repo_root: pathlib.Path) -> dict[str, str]:
|
||||
env = {}
|
||||
dotnet = dotnet_env(repo_root)
|
||||
pip = pip_env(repo_root)
|
||||
hf = huggingface_env(repo_root)
|
||||
|
||||
dotnet_keys = [
|
||||
"DOTNET_CLI_HOME",
|
||||
"NUGET_PACKAGES",
|
||||
"NUGET_HTTP_CACHE_PATH",
|
||||
"DOTNET_SKIP_FIRST_TIME_EXPERIENCE",
|
||||
"DOTNET_ADD_GLOBAL_TOOLS_TO_PATH",
|
||||
"DOTNET_GENERATE_ASPNET_CERTIFICATE",
|
||||
"DOTNET_CLI_TELEMETRY_OPTOUT",
|
||||
"NUGET_CERT_REVOCATION_MODE",
|
||||
]
|
||||
pip_keys = [
|
||||
"PIP_CACHE_DIR",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK",
|
||||
"PIP_DEFAULT_TIMEOUT",
|
||||
"PIP_RETRIES",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
]
|
||||
for key in dotnet_keys:
|
||||
env[key] = dotnet[key]
|
||||
for key in pip_keys:
|
||||
env[key] = pip[key]
|
||||
env.update(hf)
|
||||
clean_proxy_env(env)
|
||||
return env
|
||||
|
||||
|
||||
def export_lines(shell: str, env_map: dict[str, str]) -> list[str]:
|
||||
def sh_quote(value: str) -> str:
|
||||
return "'" + value.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
if shell == "pwsh":
|
||||
lines = [f"Remove-Item Env:{k} -ErrorAction SilentlyContinue" for k in PROXY_VARS]
|
||||
lines.extend(f"$env:{k} = \"{v.replace('\"', '`\"')}\"" for k, v in env_map.items())
|
||||
return lines
|
||||
if shell in ("bash", "zsh"):
|
||||
lines = [f"unset {k}" for k in PROXY_VARS]
|
||||
lines.extend(f"export {k}={sh_quote(v)}" for k, v in env_map.items())
|
||||
return lines
|
||||
if shell == "cmd":
|
||||
lines = [f"set {k}=" for k in PROXY_VARS]
|
||||
lines.extend(f"set {k}={v}" for k, v in env_map.items())
|
||||
return lines
|
||||
raise ValueError(shell)
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
try:
|
||||
repo_root = resolve_repo_root(args.project_root)
|
||||
except Exception as ex:
|
||||
print(f"Failed to resolve project root: {ex}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
env_map = resolved_env(repo_root)
|
||||
payload = {
|
||||
"projectRoot": str(repo_root),
|
||||
"env": env_map,
|
||||
"createdDirs": [
|
||||
str(repo_root / ".dotnet_home"),
|
||||
str(repo_root / ".nuget" / "packages"),
|
||||
str(repo_root / ".nuget" / "http-cache"),
|
||||
str(repo_root / ".pip" / "cache"),
|
||||
str(repo_root / ".tmp" / "pip-temp"),
|
||||
str(repo_root / ".cache" / "huggingface" / "hub"),
|
||||
],
|
||||
"warnings": [],
|
||||
}
|
||||
|
||||
try:
|
||||
lines = export_lines(args.shell, env_map)
|
||||
except ValueError:
|
||||
print(f"Unsupported shell target: {args.shell}", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(payload))
|
||||
else:
|
||||
for line in lines:
|
||||
print(line)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_doctor(args):
|
||||
try:
|
||||
repo_root = resolve_repo_root(args.project_root)
|
||||
except Exception as ex:
|
||||
print(f"Failed to resolve project root: {ex}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
env_map = resolved_env(repo_root)
|
||||
checks = {
|
||||
"repo_root": str(repo_root),
|
||||
"dotnet_home_exists": (repo_root / ".dotnet_home").exists(),
|
||||
"nuget_cache_exists": (repo_root / ".nuget" / "packages").exists(),
|
||||
"pip_cache_exists": (repo_root / ".pip" / "cache").exists(),
|
||||
"hf_cache_exists": (repo_root / ".cache" / "huggingface" / "hub").exists(),
|
||||
"env_count": len(env_map),
|
||||
}
|
||||
print(json.dumps(checks))
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SDT cross-platform shell bootstrap helper")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_export = sub.add_parser("export", help="Print env exports for a shell")
|
||||
p_export.add_argument("--shell", required=True)
|
||||
p_export.add_argument("--project-root")
|
||||
p_export.add_argument("--json", action="store_true")
|
||||
|
||||
p_doctor = sub.add_parser("doctor", help="Validate env bootstrap paths")
|
||||
p_doctor.add_argument("--project-root")
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "export":
|
||||
return cmd_export(args)
|
||||
return cmd_doctor(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
128
Journal.DevTool/scripts/diag.py
Normal file
128
Journal.DevTool/scripts/diag.py
Normal file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from script_common import resolve_command
|
||||
|
||||
|
||||
def run_capture(cmd):
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
out = (proc.stdout or "").strip()
|
||||
err = (proc.stderr or "").strip()
|
||||
text = out if out else err
|
||||
return proc.returncode == 0, text
|
||||
except Exception as ex:
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
def probe_tool(tool):
|
||||
mapping = {
|
||||
"dotnet": ["dotnet", "--version"],
|
||||
"node": ["node", "--version"],
|
||||
"npm": ["npm", "--version"],
|
||||
"python": ["python", "--version"],
|
||||
"cargo": ["cargo", "--version"],
|
||||
"tauri": ["tauri", "--version"],
|
||||
"git": ["git", "--version"],
|
||||
"docker": ["docker", "--version"],
|
||||
}
|
||||
cmd = mapping.get(tool, [tool, "--version"])
|
||||
resolved = resolve_command(cmd[0])
|
||||
if shutil.which(resolved) is None and not os.path.exists(resolved):
|
||||
return {"tool": tool, "available": False, "version": None, "details": f"{cmd[0]} not found in PATH"}
|
||||
cmd = [resolved, *cmd[1:]]
|
||||
ok, text = run_capture(cmd)
|
||||
return {"tool": tool, "available": ok, "version": text if ok else None, "details": None if ok else text}
|
||||
|
||||
|
||||
def install_plan(tool):
|
||||
is_windows = platform.system().lower().startswith("win")
|
||||
if is_windows:
|
||||
plans = {
|
||||
"dotnet": [("winget", ["install", "Microsoft.DotNet.SDK.10"])],
|
||||
"node": [("winget", ["install", "OpenJS.NodeJS.LTS"])],
|
||||
"npm": [("winget", ["install", "OpenJS.NodeJS.LTS"])],
|
||||
"python": [("winget", ["install", "Python.Python.3.12"])],
|
||||
"cargo": [("winget", ["install", "Rustlang.Rustup"])],
|
||||
"tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])],
|
||||
"git": [("winget", ["install", "Git.Git"])],
|
||||
"docker": [("winget", ["install", "Docker.DockerDesktop"])],
|
||||
}
|
||||
else:
|
||||
plans = {
|
||||
"dotnet": [("sh", ["-c", "echo install dotnet sdk with your package manager"])],
|
||||
"node": [("sh", ["-c", "echo install nodejs with your package manager"])],
|
||||
"npm": [("sh", ["-c", "echo install npm with your package manager"])],
|
||||
"python": [("sh", ["-c", "echo install python3 with your package manager"])],
|
||||
"cargo": [("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh"])],
|
||||
"tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])],
|
||||
"git": [("sh", ["-c", "echo install git with your package manager"])],
|
||||
"docker": [("sh", ["-c", "echo install docker with your package manager"])],
|
||||
}
|
||||
|
||||
cmds = plans.get(tool, [])
|
||||
return {
|
||||
"tool": tool,
|
||||
"supported": len(cmds) > 0,
|
||||
"summary": f"Install plan for {tool} on {platform.system()}",
|
||||
"commands": [{"command": c, "args": a} for c, a in cmds],
|
||||
}
|
||||
|
||||
|
||||
def run_install(tool):
|
||||
plan = install_plan(tool)
|
||||
if not plan["supported"]:
|
||||
return 2
|
||||
for cmd in plan["commands"]:
|
||||
proc = subprocess.run([cmd["command"], *cmd["args"]], check=False)
|
||||
if proc.returncode != 0:
|
||||
return proc.returncode
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SDT diagnostics and install planner")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_probe = sub.add_parser("probe")
|
||||
p_probe.add_argument("--tool", required=True)
|
||||
p_probe.add_argument("--json", action="store_true")
|
||||
|
||||
p_plan = sub.add_parser("install-plan")
|
||||
p_plan.add_argument("--tool", required=True)
|
||||
p_plan.add_argument("--json", action="store_true")
|
||||
|
||||
p_run = sub.add_parser("install-run")
|
||||
p_run.add_argument("--tool", required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cmd == "probe":
|
||||
result = probe_tool(args.tool.lower())
|
||||
if args.json:
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(result)
|
||||
return 0 if result["available"] else 1
|
||||
|
||||
if args.cmd == "install-plan":
|
||||
result = install_plan(args.tool.lower())
|
||||
if args.json:
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(result)
|
||||
return 0 if result["supported"] else 2
|
||||
|
||||
if args.cmd == "install-run":
|
||||
return run_install(args.tool.lower())
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
34
Journal.DevTool/scripts/dotnet-min.py
Normal file
34
Journal.DevTool/scripts/dotnet-min.py
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from script_common import dotnet_env, resolve_repo_root, run
|
||||
|
||||
|
||||
DOTNET_SAFE_CMDS = {"restore", "build", "run", "test", "publish", "pack"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform minimal dotnet wrapper")
|
||||
parser.add_argument("dotnet_args", nargs=argparse.REMAINDER)
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.dotnet_args:
|
||||
print("Usage: python scripts/dotnet-min.py <dotnet args>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
dotnet_args = list(args.dotnet_args)
|
||||
cmd = dotnet_args[0].lower()
|
||||
|
||||
if cmd in DOTNET_SAFE_CMDS:
|
||||
dotnet_args.extend(["-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false"])
|
||||
if cmd == "restore":
|
||||
dotnet_args.append("--ignore-failed-sources")
|
||||
|
||||
return run("dotnet", dotnet_args, repo_root, env=dotnet_env(repo_root))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
44
Journal.DevTool/scripts/migration-gate.py
Normal file
44
Journal.DevTool/scripts/migration-gate.py
Normal file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def run_step(repo_root: Path, title: str, command: list[str]) -> int:
|
||||
print(f"\n== {title} ==")
|
||||
print("$", " ".join(command))
|
||||
proc = subprocess.run(command, cwd=str(repo_root), check=False)
|
||||
return proc.returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform migration quality gate")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--skip-tests", action="store_true")
|
||||
parser.add_argument("--test-project", default=None, help="Optional test csproj path")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
|
||||
code = run_step(repo_root, "Build", [sys.executable, "scripts/dotnet-min.py", "build"])
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_tests:
|
||||
if args.test_project:
|
||||
test_cmd = [sys.executable, "scripts/dotnet-min.py", "test", args.test_project]
|
||||
else:
|
||||
test_cmd = [sys.executable, "scripts/dotnet-min.py", "test"]
|
||||
code = run_step(repo_root, "Tests", test_cmd)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
print("\nMigration gate passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
35
Journal.DevTool/scripts/npm-clean.py
Normal file
35
Journal.DevTool/scripts/npm-clean.py
Normal file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform node_modules cleanup")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--working-dir", default=".")
|
||||
parser.add_argument("--also-cache", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
work_dir = (repo_root / args.working_dir).resolve()
|
||||
node_modules = work_dir / "node_modules"
|
||||
if node_modules.exists():
|
||||
shutil.rmtree(node_modules)
|
||||
print(f"Removed: {node_modules}")
|
||||
else:
|
||||
print(f"Not found: {node_modules}")
|
||||
|
||||
if args.also_cache:
|
||||
npm_cache = repo_root / ".npm" / "cache"
|
||||
if npm_cache.exists():
|
||||
shutil.rmtree(npm_cache)
|
||||
print(f"Removed: {npm_cache}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
42
Journal.DevTool/scripts/nuget-export-cache.py
Normal file
42
Journal.DevTool/scripts/nuget-export-cache.py
Normal file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Export local NuGet cache to zip")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--output-zip", default="nuget-cache-export.zip")
|
||||
parser.add_argument("--include-dotnet-home", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_zip = (repo_root / args.output_zip).resolve()
|
||||
|
||||
nuget_dir = repo_root / ".nuget"
|
||||
dotnet_home = repo_root / ".dotnet_home"
|
||||
if not nuget_dir.exists():
|
||||
print(f"NuGet cache not found: {nuget_dir}")
|
||||
return 2
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
stage = Path(td) / "cache-export"
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(nuget_dir, stage / ".nuget")
|
||||
if args.include_dotnet_home and dotnet_home.exists():
|
||||
shutil.copytree(dotnet_home, stage / ".dotnet_home")
|
||||
manifest = stage / "nuget-cache-manifest.txt"
|
||||
manifest.write_text("exported_by=nuget-export-cache.py\n", encoding="utf-8")
|
||||
archive_base = str(output_zip.with_suffix(""))
|
||||
shutil.make_archive(archive_base, "zip", root_dir=str(stage))
|
||||
|
||||
print(f"Exported cache: {output_zip}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
28
Journal.DevTool/scripts/nuget-import-cache.py
Normal file
28
Journal.DevTool/scripts/nuget-import-cache.py
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Import NuGet cache from zip")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--input-zip", default="nuget-cache-export.zip")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
input_zip = (repo_root / args.input_zip).resolve()
|
||||
if not input_zip.exists():
|
||||
print(f"Input zip not found: {input_zip}")
|
||||
return 2
|
||||
|
||||
shutil.unpack_archive(str(input_zip), extract_dir=str(repo_root))
|
||||
print(f"Imported cache from: {input_zip}")
|
||||
print("Run `python scripts/dotnet-min.py restore` to validate restore in this repo.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
35
Journal.DevTool/scripts/pip-min.py
Normal file
35
Journal.DevTool/scripts/pip-min.py
Normal file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from script_common import pip_env, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform minimal pip wrapper")
|
||||
parser.add_argument("pip_args", nargs=argparse.REMAINDER)
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.pip_args:
|
||||
print("Usage: python scripts/pip-min.py <pip args>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
pip_args = list(args.pip_args)
|
||||
|
||||
# Preserve legacy behavior: for bare install, default target to repo-local deps.
|
||||
if pip_args and pip_args[0].lower() == "install":
|
||||
has_target = any(a in ("--target", "--prefix") for a in pip_args)
|
||||
if not has_target:
|
||||
pip_args = [a for a in pip_args if a != "--user"]
|
||||
target = repo_root / ".pydeps" / f"py{sys.version_info.major}{sys.version_info.minor}"
|
||||
os.makedirs(target, exist_ok=True)
|
||||
pip_args.extend(["--target", str(target)])
|
||||
|
||||
return run(sys.executable, ["-m", "pip", *pip_args], repo_root, env=pip_env(repo_root))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
46
Journal.DevTool/scripts/pip_safe.py
Normal file
46
Journal.DevTool/scripts/pip_safe.py
Normal file
@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
|
||||
def _mkdtemp_compat(
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
) -> str:
|
||||
# Python 3.14 on some Windows hosts creates mkdtemp dirs that are
|
||||
# immediately non-writable by the same process when mode=0o700 is used.
|
||||
# pip relies heavily on tempfile; force 0o777 for compatibility.
|
||||
if dir is None:
|
||||
dir = tempfile.gettempdir()
|
||||
if prefix is None:
|
||||
prefix = tempfile.template
|
||||
if suffix is None:
|
||||
suffix = ""
|
||||
|
||||
names = tempfile._get_candidate_names()
|
||||
for _ in range(tempfile.TMP_MAX):
|
||||
name = next(names)
|
||||
path = os.path.join(dir, f"{prefix}{name}{suffix}")
|
||||
try:
|
||||
os.mkdir(path, 0o777)
|
||||
return path
|
||||
except FileExistsError:
|
||||
continue
|
||||
|
||||
raise FileExistsError("No usable temporary directory name found.")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
tempfile.mkdtemp = _mkdtemp_compat # type: ignore[assignment]
|
||||
|
||||
from pip._internal.cli.main import main as pip_main
|
||||
|
||||
return int(pip_main(argv))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(__import__("sys").argv[1:]))
|
||||
|
||||
91
Journal.DevTool/scripts/publish-app.py
Normal file
91
Journal.DevTool/scripts/publish-app.py
Normal file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
|
||||
|
||||
from script_common import find_node_app_root, resolve_repo_root, run, sha256_files
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper")
|
||||
parser.add_argument("--target", choices=["web", "tauri"], default="web")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none")
|
||||
parser.add_argument("--install-deps", action="store_true")
|
||||
parser.add_argument("--skip-install", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
app_root = find_node_app_root(repo_root, args.app_root)
|
||||
if app_root is None:
|
||||
print("Unable to locate app root (no unique package.json found).")
|
||||
return 2
|
||||
|
||||
package_json = app_root / "package.json"
|
||||
lock_file = app_root / "package-lock.json"
|
||||
node_modules = app_root / "node_modules"
|
||||
deps_hash_file = node_modules / ".sdt-deps.sha256"
|
||||
expected_hash = sha256_files([package_json, lock_file])
|
||||
|
||||
should_install = args.install_deps or not node_modules.exists()
|
||||
if not should_install and not args.skip_install:
|
||||
if not deps_hash_file.exists():
|
||||
should_install = True
|
||||
else:
|
||||
current = deps_hash_file.read_text(encoding="utf-8").strip()
|
||||
should_install = current != expected_hash
|
||||
if args.skip_install:
|
||||
should_install = False
|
||||
|
||||
print(f"App root: {app_root}")
|
||||
print(f"Target: {args.target} ({args.configuration})")
|
||||
|
||||
if should_install:
|
||||
install_args = ["ci", "--no-audit", "--fund=false"] if lock_file.exists() else ["install", "--no-audit", "--fund=false"]
|
||||
print("$ npm " + " ".join(install_args))
|
||||
if not args.dry_run:
|
||||
code = run("npm", install_args, app_root)
|
||||
if code != 0:
|
||||
if lock_file.exists() and install_args[0] == "ci":
|
||||
print("npm ci failed (likely lockfile out of sync). Falling back to npm install...")
|
||||
fallback_args = ["install", "--no-audit", "--fund=false"]
|
||||
print("$ npm " + " ".join(fallback_args))
|
||||
code = run("npm", fallback_args, app_root)
|
||||
if code != 0:
|
||||
return code
|
||||
else:
|
||||
return code
|
||||
node_modules.mkdir(parents=True, exist_ok=True)
|
||||
deps_hash_file.write_text(expected_hash, encoding="utf-8")
|
||||
else:
|
||||
print("Skipping dependency install.")
|
||||
|
||||
if args.target == "web":
|
||||
cmd = ["run", "build"]
|
||||
print("$ npm " + " ".join(cmd))
|
||||
if not args.dry_run:
|
||||
return run("npm", cmd, app_root)
|
||||
return 0
|
||||
|
||||
tauri_cmd = ["run", "tauri", "build"]
|
||||
tauri_tail: list[str] = []
|
||||
if args.tauri_bundles == "none":
|
||||
tauri_tail.extend(["--no-bundle"])
|
||||
else:
|
||||
tauri_tail.extend(["--bundles", args.tauri_bundles])
|
||||
if args.configuration == "Debug":
|
||||
tauri_tail.append("--debug")
|
||||
if tauri_tail:
|
||||
tauri_cmd.extend(["--", *tauri_tail])
|
||||
|
||||
print("$ npm " + " ".join(tauri_cmd))
|
||||
if not args.dry_run:
|
||||
return run("npm", tauri_cmd, app_root)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
125
Journal.DevTool/scripts/publish-output.py
Normal file
125
Journal.DevTool/scripts/publish-output.py
Normal file
@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
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
|
||||
|
||||
|
||||
def has_package_script(app_root: Path, script_name: str) -> bool:
|
||||
package_json = app_root / "package.json"
|
||||
if not package_json.exists():
|
||||
return False
|
||||
try:
|
||||
data = json.loads(package_json.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return False
|
||||
scripts = data.get("scripts")
|
||||
if not isinstance(scripts, dict):
|
||||
return False
|
||||
value = scripts.get(script_name)
|
||||
return isinstance(value, str) and value.strip() != ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--runtime", default="win-x64")
|
||||
parser.add_argument("--skip-sidecar", action="store_true")
|
||||
parser.add_argument("--skip-web", action="store_true")
|
||||
parser.add_argument("--skip-webgateway", action="store_true")
|
||||
parser.add_argument("--skip-tauri", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--sidecar-project", default=None)
|
||||
parser.add_argument("--gateway-project", default=None)
|
||||
parser.add_argument("--app-root", default=None)
|
||||
parser.add_argument("--output-dir", default="output")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_root = (repo_root / args.output_dir).resolve()
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
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
|
||||
|
||||
py = sys.executable
|
||||
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
|
||||
|
||||
if not args.skip_web:
|
||||
if app_root is None:
|
||||
print("Skipping web: no app root with package.json 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
|
||||
|
||||
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
|
||||
|
||||
if not args.skip_tauri:
|
||||
if app_root is None or tauri_conf is None:
|
||||
print("Skipping tauri: tauri app not detected.")
|
||||
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
|
||||
|
||||
target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release")
|
||||
exes = sorted(target_dir.glob("*.exe"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if exes:
|
||||
staged = output_root / exes[0].name
|
||||
if args.dry_run:
|
||||
print(f"Would copy: {exes[0]} -> {staged}")
|
||||
else:
|
||||
shutil.copy2(exes[0], staged)
|
||||
print(f"Staged desktop executable: {staged}")
|
||||
|
||||
print("\nPublish output workflow complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
58
Journal.DevTool/scripts/publish-sidecar.py
Normal file
58
Journal.DevTool/scripts/publish-sidecar.py
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
|
||||
|
||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper")
|
||||
parser.add_argument("--configuration", default="Release")
|
||||
parser.add_argument("--runtime", default="win-x64")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj")
|
||||
parser.add_argument("--output-dir", default="output")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_dir = (repo_root / args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.project:
|
||||
csproj = (repo_root / args.project).resolve()
|
||||
else:
|
||||
csproj = find_csproj_by_keyword(repo_root, ["sidecar"])
|
||||
if csproj is None or not csproj.exists():
|
||||
print("Could not locate sidecar project. Pass --project <path/to/project.csproj>.")
|
||||
return 2
|
||||
|
||||
publish_args = [
|
||||
"publish",
|
||||
str(csproj),
|
||||
"-c",
|
||||
args.configuration,
|
||||
"-r",
|
||||
args.runtime,
|
||||
"--self-contained",
|
||||
"-p:PublishSingleFile=true",
|
||||
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
"-o",
|
||||
str(output_dir),
|
||||
]
|
||||
code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root))
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
binary_name = csproj.stem + (".exe" if args.runtime.startswith("win-") else "")
|
||||
binary_path = output_dir / binary_name
|
||||
if binary_path.exists():
|
||||
print(f"Published executable: {binary_path}")
|
||||
else:
|
||||
print(f"Publish completed. Output directory: {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
78
Journal.DevTool/scripts/publish-webgateway.py
Normal file
78
Journal.DevTool/scripts/publish-webgateway.py
Normal file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--runtime", default="win-x64")
|
||||
parser.add_argument("--self-contained", action="store_true")
|
||||
parser.add_argument("--skip-web-assets", action="store_true")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj")
|
||||
parser.add_argument("--web-build-dir", default=None, help="Relative path to web build assets root")
|
||||
parser.add_argument("--output-dir", default="output/webgateway")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_dir = (repo_root / args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.project:
|
||||
csproj = (repo_root / args.project).resolve()
|
||||
else:
|
||||
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
||||
if csproj is None or not csproj.exists():
|
||||
print("Could not locate web gateway project. Pass --project <path/to/project.csproj>.")
|
||||
return 2
|
||||
|
||||
publish_args = [
|
||||
"publish",
|
||||
str(csproj),
|
||||
"-c",
|
||||
args.configuration,
|
||||
"-r",
|
||||
args.runtime,
|
||||
"--self-contained",
|
||||
"true" if args.self_contained else "false",
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
"-o",
|
||||
str(output_dir),
|
||||
]
|
||||
code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root))
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_web_assets:
|
||||
if args.web_build_dir:
|
||||
web_build_dir = (repo_root / args.web_build_dir).resolve()
|
||||
else:
|
||||
web_build_dir = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None)
|
||||
if web_build_dir is not None:
|
||||
web_build_dir = web_build_dir / "build"
|
||||
|
||||
if web_build_dir is None or not web_build_dir.exists():
|
||||
print("Web assets not found. Skip with --skip-web-assets or pass --web-build-dir.")
|
||||
else:
|
||||
web_out = output_dir / "wwwroot"
|
||||
web_out.mkdir(parents=True, exist_ok=True)
|
||||
for item in web_build_dir.iterdir():
|
||||
dst = web_out / item.name
|
||||
if item.is_dir():
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(item, dst)
|
||||
else:
|
||||
shutil.copy2(item, dst)
|
||||
print(f"Copied web assets: {web_out}")
|
||||
|
||||
print(f"Publish completed: {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
60
Journal.DevTool/scripts/run-webgateway.py
Normal file
60
Journal.DevTool/scripts/run-webgateway.py
Normal file
@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run gateway in dev or output mode")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--urls", default="http://0.0.0.0:5180")
|
||||
parser.add_argument("--project-root", default=None, help="Runtime project root exposed via SDT_PROJECT_ROOT")
|
||||
parser.add_argument("--mode", choices=["Dev", "Output"], default="Dev")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--project", default=None, help="Gateway csproj path")
|
||||
parser.add_argument("--output-exe", default=None, help="Published gateway executable path")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
effective_project_root = Path(args.project_root).resolve() if args.project_root else repo_root
|
||||
if not effective_project_root.exists():
|
||||
print(f"Project root does not exist: {effective_project_root}")
|
||||
return 2
|
||||
|
||||
env = dotnet_env(repo_root)
|
||||
env["SDT_PROJECT_ROOT"] = str(effective_project_root)
|
||||
|
||||
if args.mode == "Output":
|
||||
exe_path = Path(args.output_exe).resolve() if args.output_exe else (repo_root / "output" / "webgateway" / ("webgateway.exe" if os.name == "nt" else "webgateway"))
|
||||
if not exe_path.exists():
|
||||
print(f"Output executable not found: {exe_path}")
|
||||
return 2
|
||||
return run(str(exe_path), ["--urls", args.urls], repo_root, env=env)
|
||||
|
||||
if args.project:
|
||||
csproj = (repo_root / args.project).resolve()
|
||||
else:
|
||||
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
||||
if csproj is None or not csproj.exists():
|
||||
print("Could not locate gateway project. Pass --project <path/to/project.csproj>.")
|
||||
return 2
|
||||
|
||||
run_args = [
|
||||
"run",
|
||||
"--project",
|
||||
str(csproj),
|
||||
"-c",
|
||||
args.configuration,
|
||||
"--no-launch-profile",
|
||||
"--urls",
|
||||
args.urls,
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
]
|
||||
return run("dotnet", run_args, repo_root, env=env)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
362
Journal.DevTool/scripts/script_common.py
Normal file
362
Journal.DevTool/scripts/script_common.py
Normal file
@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, Iterable, List, Sequence
|
||||
|
||||
|
||||
PROXY_VARS = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
"GIT_HTTP_PROXY",
|
||||
"GIT_HTTPS_PROXY",
|
||||
"PIP_NO_INDEX",
|
||||
]
|
||||
|
||||
|
||||
def resolve_repo_root(start: str | None = None) -> pathlib.Path:
|
||||
base = pathlib.Path(start or os.getcwd()).resolve()
|
||||
|
||||
# Preferred marker for SDT-managed projects.
|
||||
for cur in [base, *base.parents]:
|
||||
cfg = cur / "devtool.json"
|
||||
if cfg.exists():
|
||||
hints = load_project_root_hints(cur)
|
||||
if not hints:
|
||||
return cur
|
||||
if any(_hint_matches(cur, hint) for hint in hints):
|
||||
return cur
|
||||
|
||||
# Fall back to git root when available.
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", str(base), "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
git_root = proc.stdout.strip()
|
||||
if git_root:
|
||||
return pathlib.Path(git_root).resolve()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return base
|
||||
|
||||
|
||||
def load_project_root_hints(repo_root: pathlib.Path) -> list[str]:
|
||||
cfg = repo_root / "devtool.json"
|
||||
if not cfg.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(cfg.read_text(encoding="utf-8"))
|
||||
hints = data.get("project", {}).get("rootHints", [])
|
||||
return [str(x) for x in hints if isinstance(x, str) and x.strip()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def ensure_dirs(paths: List[pathlib.Path]) -> None:
|
||||
for p in paths:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def clean_proxy_env(env: Dict[str, str]) -> None:
|
||||
for k in PROXY_VARS:
|
||||
env.pop(k, None)
|
||||
|
||||
|
||||
def dotnet_env(repo_root: pathlib.Path) -> Dict[str, str]:
|
||||
env = dict(os.environ)
|
||||
clean_proxy_env(env)
|
||||
dotnet_cli_home = repo_root / ".dotnet_home"
|
||||
nuget_packages = repo_root / ".nuget" / "packages"
|
||||
nuget_http_cache = repo_root / ".nuget" / "http-cache"
|
||||
ensure_dirs([dotnet_cli_home, nuget_packages, nuget_http_cache])
|
||||
env["DOTNET_CLI_HOME"] = str(dotnet_cli_home)
|
||||
env["NUGET_PACKAGES"] = str(nuget_packages)
|
||||
env["NUGET_HTTP_CACHE_PATH"] = str(nuget_http_cache)
|
||||
env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"
|
||||
env["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "0"
|
||||
env["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "0"
|
||||
env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
|
||||
env["NUGET_CERT_REVOCATION_MODE"] = "offline"
|
||||
return env
|
||||
|
||||
|
||||
def pip_env(repo_root: pathlib.Path) -> Dict[str, str]:
|
||||
env = dict(os.environ)
|
||||
clean_proxy_env(env)
|
||||
pip_cache = repo_root / ".pip" / "cache"
|
||||
pip_tmp = repo_root / ".tmp" / "pip-temp"
|
||||
ensure_dirs([pip_cache, pip_tmp])
|
||||
env["PIP_CACHE_DIR"] = str(pip_cache)
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
env["PIP_DEFAULT_TIMEOUT"] = "30"
|
||||
env["PIP_RETRIES"] = "2"
|
||||
env["TEMP"] = str(pip_tmp)
|
||||
env["TMP"] = str(pip_tmp)
|
||||
return env
|
||||
|
||||
|
||||
def run(command: str, args: List[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> int:
|
||||
resolved = resolve_command(command)
|
||||
try:
|
||||
proc = subprocess.run([resolved, *args], cwd=str(cwd), env=env, check=False)
|
||||
return proc.returncode
|
||||
except FileNotFoundError:
|
||||
print(f"Command not found: {resolved}", file=sys.stderr)
|
||||
return 127
|
||||
|
||||
|
||||
def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> tuple[int, str, str]:
|
||||
resolved = resolve_command(command)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[resolved, *args],
|
||||
cwd=str(cwd),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
except FileNotFoundError:
|
||||
return 127, "", f"Command not found: {resolved}"
|
||||
|
||||
|
||||
def resolve_command(command: str) -> str:
|
||||
if not command:
|
||||
return command
|
||||
|
||||
if os.name != "nt":
|
||||
return command
|
||||
|
||||
if any(sep in command for sep in ("\\", "/")):
|
||||
return command
|
||||
|
||||
if pathlib.Path(command).suffix:
|
||||
found = shutil.which(command)
|
||||
return found or command
|
||||
|
||||
candidates = []
|
||||
lowered = command.lower()
|
||||
if lowered in ("npm", "npx", "pnpm", "yarn", "tauri"):
|
||||
candidates.extend([f"{command}.cmd", f"{command}.exe", f"{command}.bat", command])
|
||||
else:
|
||||
candidates.append(command)
|
||||
|
||||
for c in candidates:
|
||||
found = _which_windows(c)
|
||||
if found:
|
||||
name = pathlib.Path(found).name.lower()
|
||||
if name in ("npm", "npx", "pnpm", "yarn", "tauri"):
|
||||
shim = pathlib.Path(found).with_name(name + ".cmd")
|
||||
if shim.exists():
|
||||
return str(shim)
|
||||
return found
|
||||
|
||||
if lowered in ("npm", "npx", "pnpm", "yarn"):
|
||||
node = _which_windows("node.exe") or _which_windows("node")
|
||||
if node:
|
||||
node_dir = pathlib.Path(node).parent
|
||||
shim = node_dir / f"{lowered}.cmd"
|
||||
if shim.exists():
|
||||
return str(shim)
|
||||
|
||||
return candidates[-1]
|
||||
|
||||
|
||||
def _hint_matches(root: pathlib.Path, hint: str) -> bool:
|
||||
h = hint.strip()
|
||||
if not h:
|
||||
return False
|
||||
|
||||
has_glob = any(ch in h for ch in ("*", "?", "["))
|
||||
if has_glob:
|
||||
# Match both anywhere in root and directly at root-level for common hints like "*.sln".
|
||||
if any(root.glob(h)):
|
||||
return True
|
||||
return any(root.rglob(h))
|
||||
|
||||
marker = root / h
|
||||
if marker.exists():
|
||||
return True
|
||||
|
||||
# If hint is just a filename marker, look bounded in tree.
|
||||
if not any(sep in h for sep in ("\\", "/")):
|
||||
return any(p.name == h for p in root.rglob(h))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _expand_windows_path_segment(segment: str) -> str:
|
||||
expanded = segment
|
||||
# Expand %VAR% tokens repeatedly for nested references.
|
||||
for _ in range(4):
|
||||
next_value = os.path.expandvars(expanded)
|
||||
if next_value == expanded:
|
||||
break
|
||||
expanded = next_value
|
||||
return expanded
|
||||
|
||||
|
||||
def _which_windows(command: str) -> str | None:
|
||||
found = shutil.which(command)
|
||||
if found:
|
||||
return found
|
||||
|
||||
if os.name != "nt":
|
||||
return None
|
||||
|
||||
path_value = os.environ.get("PATH", "")
|
||||
pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD")
|
||||
exts = [e.lower() for e in pathext.split(";") if e]
|
||||
|
||||
has_ext = pathlib.Path(command).suffix != ""
|
||||
names = [command] if has_ext else [command, *(command + e.lower() for e in exts)]
|
||||
|
||||
for raw_segment in path_value.split(os.pathsep):
|
||||
segment = _expand_windows_path_segment(raw_segment.strip())
|
||||
if not segment:
|
||||
continue
|
||||
base = pathlib.Path(segment)
|
||||
for name in names:
|
||||
candidate = base / name
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def sha256_files(paths: Iterable[pathlib.Path]) -> str:
|
||||
h = hashlib.sha256()
|
||||
for p in paths:
|
||||
if not p.exists():
|
||||
continue
|
||||
h.update(p.read_bytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def first_existing(paths: Iterable[pathlib.Path]) -> pathlib.Path | None:
|
||||
for p in paths:
|
||||
if p.exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> pathlib.Path | None:
|
||||
if hints:
|
||||
for hint in hints:
|
||||
candidate = (repo_root / hint).resolve()
|
||||
if candidate.exists() and candidate.suffix.lower() == ".csproj":
|
||||
return candidate
|
||||
|
||||
csprojs = sorted(repo_root.rglob("*.csproj"))
|
||||
if not csprojs:
|
||||
return None
|
||||
if len(csprojs) == 1:
|
||||
return csprojs[0]
|
||||
return None
|
||||
|
||||
|
||||
def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> pathlib.Path | None:
|
||||
kws = [k.lower() for k in keywords]
|
||||
matches: list[pathlib.Path] = []
|
||||
for p in repo_root.rglob("*.csproj"):
|
||||
text = str(p).lower()
|
||||
if any(k in text for k in kws):
|
||||
matches.append(p)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
return None
|
||||
|
||||
|
||||
def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> pathlib.Path | None:
|
||||
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():
|
||||
return True
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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]
|
||||
82
Journal.DevTool/scripts/sync-output.py
Normal file
82
Journal.DevTool/scripts/sync-output.py
Normal file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import newest_file, resolve_repo_root
|
||||
|
||||
|
||||
def copy_tree_contents(src: Path, dst: Path) -> None:
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
for item in src.iterdir():
|
||||
target = dst / item.name
|
||||
if item.is_dir():
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
shutil.copytree(item, target)
|
||||
else:
|
||||
shutil.copy2(item, target)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Sync newest built assets into output folder")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--output-dir", default="output")
|
||||
parser.add_argument("--web-build-dir", default=None, help="Path to web build output")
|
||||
parser.add_argument("--sidecar-bin-dir", default=None, help="Path to sidecar bin root")
|
||||
parser.add_argument("--gateway-bin-dir", default=None, help="Path to gateway bin root")
|
||||
parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_dir = (repo_root / args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
web_build = (repo_root / args.web_build_dir).resolve() if args.web_build_dir else None
|
||||
if web_build is None:
|
||||
web_build = next((p for p in repo_root.rglob("build") if (p.parent / "package.json").exists()), None)
|
||||
if web_build is not None and web_build.exists():
|
||||
web_out = output_dir / "webgateway" / "wwwroot"
|
||||
copy_tree_contents(web_build, web_out)
|
||||
print(f"Synced web assets -> {web_out}")
|
||||
|
||||
sidecar_bin = (repo_root / args.sidecar_bin_dir).resolve() if args.sidecar_bin_dir else None
|
||||
if sidecar_bin is None:
|
||||
sidecar_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "sidecar" in str(p).lower()), None)
|
||||
sidecar_bin = sidecar_proj / "bin" if sidecar_proj else None
|
||||
if sidecar_bin is not None:
|
||||
sidecar_pattern = "*.exe" if os.name == "nt" else "*"
|
||||
sidecar_exe = newest_file(sidecar_bin, sidecar_pattern)
|
||||
if sidecar_exe is not None:
|
||||
copy_tree_contents(sidecar_exe.parent, output_dir)
|
||||
print(f"Synced sidecar -> {output_dir}")
|
||||
|
||||
gateway_bin = (repo_root / args.gateway_bin_dir).resolve() if args.gateway_bin_dir else None
|
||||
if gateway_bin is None:
|
||||
gateway_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "gateway" in str(p).lower()), None)
|
||||
gateway_bin = gateway_proj / "bin" if gateway_proj else None
|
||||
if gateway_bin is not None:
|
||||
gateway_pattern = "*.exe" if os.name == "nt" else "*"
|
||||
gw_exe = newest_file(gateway_bin, gateway_pattern)
|
||||
if gw_exe is not None:
|
||||
gw_out = output_dir / "webgateway"
|
||||
copy_tree_contents(gw_exe.parent, gw_out)
|
||||
print(f"Synced gateway -> {gw_out}")
|
||||
|
||||
tauri_target = (repo_root / args.tauri_target_dir).resolve() if args.tauri_target_dir else None
|
||||
if tauri_target is None:
|
||||
tauri_target = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None)
|
||||
tauri_target = tauri_target / "target" if tauri_target else None
|
||||
if tauri_target is not None:
|
||||
app_exe = newest_file(tauri_target, "*.exe")
|
||||
if app_exe is not None:
|
||||
shutil.copy2(app_exe, output_dir / app_exe.name)
|
||||
print(f"Synced desktop app ({app_exe.name}) -> {output_dir}")
|
||||
|
||||
print("Sync complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
298
Journal.DevTool/scripts/verify-workflow-routes.py
Normal file
298
Journal.DevTool/scripts/verify-workflow-routes.py
Normal file
@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from script_common import resolve_command, resolve_repo_root
|
||||
|
||||
|
||||
def load_config(project_root: pathlib.Path) -> dict:
|
||||
config_path = project_root / "devtool.json"
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"devtool.json 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]:
|
||||
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)]
|
||||
if selected:
|
||||
normalized = [w for w in normalized if w["id"] in selected]
|
||||
return normalized
|
||||
|
||||
|
||||
def is_command_available(command: str) -> bool:
|
||||
resolved = resolve_command(command)
|
||||
if pathlib.Path(resolved).is_file():
|
||||
return True
|
||||
return shutil.which(resolved) is not None
|
||||
|
||||
|
||||
def resolve_script_arg(project_root: pathlib.Path, working_dir: pathlib.Path, arg: str) -> pathlib.Path:
|
||||
p = pathlib.Path(arg)
|
||||
if p.is_absolute():
|
||||
return p
|
||||
a = working_dir / p
|
||||
if a.exists():
|
||||
return a
|
||||
b = project_root / p
|
||||
return b
|
||||
|
||||
|
||||
def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
|
||||
result = {
|
||||
"workflowId": workflow.get("id"),
|
||||
"ok": True,
|
||||
"issues": [],
|
||||
"steps": [],
|
||||
}
|
||||
|
||||
for step in workflow.get("steps", []):
|
||||
if not isinstance(step, dict):
|
||||
continue
|
||||
step_id = step.get("id", "<unknown>")
|
||||
step_result = {"stepId": step_id, "ok": True, "issues": []}
|
||||
|
||||
working_dir_rel = step.get("workingDir") or "."
|
||||
working_dir = (project_root / working_dir_rel).resolve()
|
||||
if not working_dir.exists():
|
||||
step_result["ok"] = False
|
||||
step_result["issues"].append(f"workingDir_not_found:{working_dir}")
|
||||
|
||||
command = step.get("command")
|
||||
args = step.get("args") or []
|
||||
action = step.get("action")
|
||||
|
||||
if isinstance(command, str) and command.strip():
|
||||
if not is_command_available(command):
|
||||
step_result["ok"] = False
|
||||
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"):
|
||||
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")
|
||||
|
||||
if not step_result["ok"]:
|
||||
result["ok"] = False
|
||||
result["issues"].extend(step_result["issues"])
|
||||
|
||||
result["steps"].append(step_result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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"])
|
||||
|
||||
local_exe = repo_root / ("sdt.exe" if sys.platform.startswith("win") else "sdt")
|
||||
if local_exe.exists():
|
||||
attempts.append([str(local_exe)])
|
||||
|
||||
devtool_csproj = repo_root / "DevTool.csproj"
|
||||
if devtool_csproj.exists():
|
||||
attempts.append(["dotnet", "run", "--project", str(devtool_csproj), "--"])
|
||||
|
||||
# Preserve order but dedupe exact attempts.
|
||||
seen = set()
|
||||
unique: List[List[str]] = []
|
||||
for a in attempts:
|
||||
key = tuple(a)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(a)
|
||||
return unique
|
||||
|
||||
|
||||
def try_run_sdt(
|
||||
repo_root: pathlib.Path,
|
||||
command_args: Sequence[str],
|
||||
timeout_seconds: int,
|
||||
) -> Tuple[Optional[subprocess.CompletedProcess], Optional[str]]:
|
||||
errors: List[str] = []
|
||||
for base in sdt_attempts(repo_root):
|
||||
cmd = [*base, *command_args]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(repo_root),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
return proc, " ".join(cmd)
|
||||
except FileNotFoundError:
|
||||
errors.append(f"not_found:{' '.join(cmd)}")
|
||||
except subprocess.TimeoutExpired:
|
||||
errors.append(f"timeout:{' '.join(cmd)}")
|
||||
return None, "; ".join(errors) if errors else "no_sdt_attempts"
|
||||
|
||||
|
||||
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("{"):
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(payload, dict) and "runId" in payload and "success" in payload:
|
||||
return payload
|
||||
return None
|
||||
|
||||
|
||||
def execute_check_workflow(
|
||||
repo_root: pathlib.Path,
|
||||
project_root: pathlib.Path,
|
||||
workflow_id: str,
|
||||
env_profile: Optional[str],
|
||||
timeout_seconds: int,
|
||||
) -> dict:
|
||||
args = [
|
||||
"run",
|
||||
workflow_id,
|
||||
"--json",
|
||||
"--project-root",
|
||||
str(project_root),
|
||||
"--non-interactive",
|
||||
]
|
||||
if env_profile:
|
||||
args.extend(["--env-profile", env_profile])
|
||||
|
||||
proc, attempted = try_run_sdt(repo_root, args, timeout_seconds)
|
||||
if proc is None:
|
||||
return {
|
||||
"workflowId": workflow_id,
|
||||
"ok": False,
|
||||
"attempted": attempted,
|
||||
"exitCode": None,
|
||||
"stopReason": "sdt_not_runnable",
|
||||
"message": attempted,
|
||||
}
|
||||
|
||||
summary = parse_headless_summary(proc.stdout)
|
||||
if summary is None:
|
||||
return {
|
||||
"workflowId": workflow_id,
|
||||
"ok": False,
|
||||
"attempted": attempted,
|
||||
"exitCode": proc.returncode,
|
||||
"stopReason": "missing_summary",
|
||||
"message": (proc.stderr or proc.stdout).strip(),
|
||||
}
|
||||
|
||||
return {
|
||||
"workflowId": workflow_id,
|
||||
"ok": bool(summary.get("success", False)),
|
||||
"attempted": attempted,
|
||||
"exitCode": summary.get("exitCode"),
|
||||
"stopReason": summary.get("stopReason"),
|
||||
"message": summary.get("message"),
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
project_root = (repo_root / args.project_root).resolve() if not pathlib.Path(args.project_root).is_absolute() else pathlib.Path(args.project_root).resolve()
|
||||
selected = set(args.workflow) if args.workflow else None
|
||||
|
||||
config = load_config(project_root)
|
||||
workflows = iter_workflows(config, selected)
|
||||
if not workflows:
|
||||
print("No workflows selected/found.")
|
||||
return 2
|
||||
|
||||
static_results = [static_check_workflow(project_root, w) for w in workflows]
|
||||
execute_results: List[dict] = []
|
||||
if args.execute:
|
||||
for w in workflows:
|
||||
wid = w["id"]
|
||||
execute_results.append(
|
||||
execute_check_workflow(
|
||||
repo_root=repo_root,
|
||||
project_root=project_root,
|
||||
workflow_id=wid,
|
||||
env_profile=args.env_profile,
|
||||
timeout_seconds=args.timeout_seconds,
|
||||
)
|
||||
)
|
||||
|
||||
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 = {
|
||||
"repoRoot": str(repo_root),
|
||||
"projectRoot": str(project_root),
|
||||
"totalWorkflows": len(workflows),
|
||||
"static": {
|
||||
"checked": len(static_results),
|
||||
"failed": len(static_failures),
|
||||
"results": static_results,
|
||||
},
|
||||
"execute": {
|
||||
"enabled": args.execute,
|
||||
"checked": len(execute_results),
|
||||
"failed": len(exec_failures),
|
||||
"results": execute_results,
|
||||
},
|
||||
}
|
||||
|
||||
if args.output_json:
|
||||
out_path = pathlib.Path(args.output_json)
|
||||
if not out_path.is_absolute():
|
||||
out_path = repo_root / out_path
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"Report written: {out_path}")
|
||||
|
||||
print(f"Static checks: {len(static_results)} workflow(s), failures={len(static_failures)}")
|
||||
if args.execute:
|
||||
print(f"Execution checks: {len(execute_results)} workflow(s), failures={len(exec_failures)}")
|
||||
|
||||
if static_failures:
|
||||
print("\nStatic failures:")
|
||||
for f in static_failures:
|
||||
print(f"- {f['workflowId']}: {', '.join(f['issues'])}")
|
||||
|
||||
if exec_failures:
|
||||
print("\nExecution failures:")
|
||||
for f in exec_failures:
|
||||
print(f"- {f['workflowId']}: stopReason={f.get('stopReason')} message={f.get('message')}")
|
||||
|
||||
return 1 if static_failures or exec_failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
108
Journal.DevTool/sdt.deps.json
Normal file
108
Journal.DevTool/sdt.deps.json
Normal file
@ -0,0 +1,108 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v10.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v10.0": {
|
||||
"sdt/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Engine": "1.0.0",
|
||||
"DevTool.Host.Bridge": "1.0.0",
|
||||
"DevTool.Host.Tui": "1.0.0",
|
||||
"DevTool.Runtime": "1.0.0",
|
||||
"Spectre.Console": "0.49.1"
|
||||
},
|
||||
"runtime": {
|
||||
"sdt.dll": {}
|
||||
}
|
||||
},
|
||||
"Spectre.Console/0.49.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Spectre.Console.dll": {
|
||||
"assemblyVersion": "0.0.0.0",
|
||||
"fileVersion": "0.49.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Engine/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Runtime": "1.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"DevTool.Engine.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Host.Bridge/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Engine": "1.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"DevTool.Host.Bridge.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Host.Tui/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Engine": "1.0.0",
|
||||
"DevTool.Runtime": "1.0.0",
|
||||
"Spectre.Console": "0.49.1"
|
||||
},
|
||||
"runtime": {
|
||||
"DevTool.Host.Tui.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Runtime/1.0.0": {
|
||||
"runtime": {
|
||||
"DevTool.Runtime.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"sdt/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Spectre.Console/0.49.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA==",
|
||||
"path": "spectre.console/0.49.1",
|
||||
"hashPath": "spectre.console.0.49.1.nupkg.sha512"
|
||||
},
|
||||
"DevTool.Engine/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DevTool.Host.Bridge/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DevTool.Host.Tui/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DevTool.Runtime/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Journal.DevTool/sdt.dll
Normal file
BIN
Journal.DevTool/sdt.dll
Normal file
Binary file not shown.
BIN
Journal.DevTool/sdt.exe
Normal file
BIN
Journal.DevTool/sdt.exe
Normal file
Binary file not shown.
BIN
Journal.DevTool/sdt.pdb
Normal file
BIN
Journal.DevTool/sdt.pdb
Normal file
Binary file not shown.
13
Journal.DevTool/sdt.runtimeconfig.json
Normal file
13
Journal.DevTool/sdt.runtimeconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net10.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "10.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -676,7 +676,7 @@
|
||||
],
|
||||
"values": {
|
||||
"SDT_ENV_PROFILE": "ci",
|
||||
"CI": "true",
|
||||
"CI": "false",
|
||||
"SDT_LOG_LEVEL": "warning"
|
||||
}
|
||||
},
|
||||
@ -783,4 +783,4 @@
|
||||
"bundleOnFailure": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user