Compare commits

...

3 Commits

Author SHA1 Message Date
06c0d30aaa SDT update 2026-03-04 16:42:04 -06:00
Jacob Schmidt
27cc379eb8 feat: add Vulkan GPU backend and fix GpuLayerCount config
- Downgrade LLamaSharp packages to 0.25.0 to match Vulkan backend availability
- Add LLamaSharp.Backend.Vulkan for AMD/Intel/NVIDIA GPU acceleration
- Fix _gpuLayers bug: was reading LlamaCppTimeout instead of a dedicated field
- Add GpuLayerCount to JournalConfig, sourced from JOURNAL_GPU_LAYERS env var
- Document AI/LLM notes in README (version pinning, known vulkaninfo issue)

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-03-02 22:44:01 -06:00
b4fa65c881 gitignore. 2026-03-02 22:44:01 -06:00
33 changed files with 1453 additions and 2192 deletions

2
.gitignore vendored
View File

@ -56,3 +56,5 @@ journalapp(1).exe
Journal.DevTool/scripts/__pycache__/
.sdt/
devtool.backup.json
Journal.App/node_modules.old/
scripts/__pycache__/

View File

@ -10,7 +10,8 @@
<PackageVersion Include="NAudio" Version="2.2.1" />
<PackageVersion Include="Whisper.net" Version="1.9.0" />
<PackageVersion Include="Whisper.net.Runtime" Version="1.9.0" />
<PackageVersion Include="LLamaSharp" Version="0.26.0" />
<PackageVersion Include="LLamaSharp.Backend.Cpu" Version="0.26.0" />
<PackageVersion Include="LLamaSharp" Version="0.25.0" />
<PackageVersion Include="LLamaSharp.Backend.Cpu" Version="0.25.0" />
<PackageVersion Include="LLamaSharp.Backend.Vulkan" Version="0.25.0" />
</ItemGroup>
</Project>

View File

@ -3,6 +3,7 @@
<ItemGroup>
<PackageReference Include="LLamaSharp" />
<PackageReference Include="LLamaSharp.Backend.Cpu" />
<PackageReference Include="LLamaSharp.Backend.Vulkan" />
</ItemGroup>
<ItemGroup>

View File

@ -18,7 +18,7 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
private readonly string _configuredModelPath = config.GgufModelPath;
private readonly uint _contextSize = (uint)Math.Clamp(config.ModelContextTokens, 512, 4096);
private readonly int _gpuLayers = config.LlamaCppTimeout;
private readonly int _gpuLayers = config.GpuLayerCount;
private readonly Lock _sync = new();
private string? _resolvedModelPath;

View File

@ -13,6 +13,7 @@ public sealed record JournalConfig(
string LlamaCppUrl,
string LlamaCppModel,
int LlamaCppTimeout,
int GpuLayerCount,
string EmbeddingApiUrl,
string EmbeddingModelName,
int ModelContextTokens,

View File

@ -38,6 +38,7 @@ public sealed class JournalConfigService : IJournalConfigService
LlamaCppUrl: Environment.GetEnvironmentVariable("LLAMA_CPP_URL") ?? "http://127.0.0.1:8085/v1/completions",
LlamaCppModel: Environment.GetEnvironmentVariable("LLAMA_CPP_MODEL") ?? "qwen/qwen3-4b",
LlamaCppTimeout: ParseInt("LLAMA_CPP_TIMEOUT", 6000),
GpuLayerCount: ParseInt("JOURNAL_GPU_LAYERS", -1),
EmbeddingApiUrl: Environment.GetEnvironmentVariable("EMBEDDING_API_URL") ?? "http://127.0.0.1:8086/v1/embeddings",
EmbeddingModelName: Environment.GetEnvironmentVariable("EMBEDDING_MODEL_NAME") ?? "text-embedding-nomic-embed-text-v2-moe",
ModelContextTokens: ParseInt("MODEL_CONTEXT_TOKENS", 131072),

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,63 @@
# Scripts (Python-first, cross-platform)
This folder now uses Python as the default runtime for orchestration and diagnostics.
## Preferred scripts
- `diag.py`: tool probing and install-plan generation (`dotnet`, `python`, `node`, `npm`, `cargo`, `tauri`)
- `build.py`: normalized build actions used by SDT workflows
- `dev_shell.py`: cross-platform shell bootstrap export/doctor helper
- `dotnet-min.py`: resilient `dotnet` wrapper with local cache env
- `pip-min.py`: resilient `pip` wrapper with local cache env and repo-local target default
- `npm-clean.py`: remove `node_modules` cross-platform
- `migration-gate.py`: build/test quality gate
- `nuget-export-cache.py`: archive `.nuget` cache
- `nuget-import-cache.py`: restore `.nuget` cache from archive
- `publish-app.py`: build web or tauri app (cross-platform)
- `publish-sidecar.py`: publish sidecar .NET service
- `publish-webgateway.py`: publish gateway .NET service and optional web assets
- `run-webgateway.py`: run gateway in dev or published-output mode
- `publish-output.py`: orchestrate sidecar/web/gateway/desktop publish steps
- `sync-output.py`: sweep newest build artifacts into `output/`
- `script_common.py`: shared helpers (repo root resolution, env shaping, command runner)
- `project.rootHints` supports glob markers (for example `*.sln`) and directory/file markers (`.git`, `package.json`)
- Windows PATH token expansion (`%NVM_HOME%`, `%NVM_SYMLINK%`, etc.) is applied during command resolution
## Shell bootstrap wrappers
- `dev-shell.ps1`: PowerShell wrapper over `dev_shell.py`
- `dev-shell.sh`: bash/zsh wrapper over `dev_shell.py`
- `dev-shell.cmd`: cmd wrapper over `dev_shell.py`
## Legacy scripts
Existing `.ps1` entrypoints are now compatibility wrappers that forward to Python scripts.
`script-common.ps1` is legacy-only compatibility and not used by active SDT workflows.
Original PowerShell implementations are archived under `scripts/legacy/` as `*.legacy.ps1` for reference during transition.
## Root Hint Semantics
`project.rootHints` is evaluated in this order:
1. Exact marker exists at candidate root (file or directory)
2. Root-level glob match (`glob`)
3. Recursive glob match (`rglob`)
Examples:
- `"*.sln"`
- `".git"`
- `"package.json"`
- `"src-tauri/tauri.conf.json"`
## Quick usage
```powershell
python scripts/diag.py probe --tool dotnet --json
python scripts/dotnet-min.py build
python scripts/migration-gate.py
python scripts/nuget-export-cache.py --output-zip nuget-cache-export.zip
python scripts/nuget-import-cache.py --input-zip nuget-cache-export.zip
python scripts/npm-clean.py --working-dir .
python scripts/dev_shell.py export --shell pwsh --json
python scripts/dev_shell.py doctor
```

View File

@ -0,0 +1,67 @@
# Cross-Platform Script Workflows
## 1) Probe toolchain availability
```powershell
python scripts/diag.py probe --tool dotnet --json
python scripts/diag.py probe --tool python --json
python scripts/diag.py probe --tool node --json
python scripts/diag.py probe --tool npm --json
python scripts/diag.py probe --tool cargo --json
python scripts/diag.py probe --tool tauri --json
python scripts/diag.py probe --tool git --json
python scripts/diag.py probe --tool docker --json
```
## Shell bootstrap (cross-platform)
```powershell
python scripts/dev_shell.py export --shell pwsh --json
python scripts/dev_shell.py doctor
```
## 2) Build and run SDT
```powershell
python scripts/dotnet-min.py build
dotnet run --project DevTool.csproj
```
## 3) Run migration gate
```powershell
python scripts/migration-gate.py
```
## 3.1) Verify workflow route resolution (path + optional execution)
```powershell
# Static route checks only
python scripts/verify-workflow-routes.py --project-root .
# Static + headless execution checks for selected workflows
python scripts/verify-workflow-routes.py --project-root . --workflow build --workflow tauri --execute --env-profile dev
```
## 4) Manage NuGet cache
```powershell
python scripts/nuget-export-cache.py --output-zip nuget-cache-export.zip
python scripts/nuget-import-cache.py --input-zip nuget-cache-export.zip
```
## 5) Clean Node modules
```powershell
python scripts/npm-clean.py --working-dir .
```
## 6) Build app/gateway bundles
```powershell
python scripts/publish-app.py --target web
python scripts/publish-sidecar.py --project path/to/sidecar.csproj
python scripts/publish-webgateway.py --project path/to/gateway.csproj --skip-web-assets
python scripts/publish-output.py --dry-run
python scripts/sync-output.py
```

View File

@ -8,11 +8,15 @@ import shutil
import subprocess
import sys
import time
from script_common import resolve_command
from typing import Any
from script_common import resolve_command, SdtResult # type: ignore
StepResult = dict[str, Any]
def run_step(command, args, cwd):
resolved = resolve_command(command)
def run_step(command: str, args: list[str], cwd: str) -> StepResult:
resolved = str(resolve_command(command))
if shutil.which(resolved) is None and not pathlib.Path(resolved).exists():
return {
"command": resolved,
@ -38,7 +42,7 @@ def run_step(command, args, cwd):
}
def resolve_python_executable():
def resolve_python_executable() -> str:
candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"]
for c in candidates:
if shutil.which(c):
@ -46,20 +50,20 @@ def resolve_python_executable():
return "python"
def parse_common(parser):
parser.add_argument("--project-root", required=True)
parser.add_argument("--working-dir", default=".")
parser.add_argument("--json", action="store_true")
def parse_common(parser: argparse.ArgumentParser) -> None:
_ = parser.add_argument("--project-root", required=True)
_ = parser.add_argument("--working-dir", default=".")
_ = parser.add_argument("--json", action="store_true")
def resolve_cwd(project_root, working_dir):
def resolve_cwd(project_root: str, working_dir: str) -> str:
return os.path.abspath(os.path.join(project_root, working_dir))
EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"}
def discover_dotnet_target(project_root: str, cwd: str):
def discover_dotnet_target(project_root: str, cwd: str) -> str | None:
# Prefer local solution first (.slnx, then .sln), then csproj, then bounded scan from project root.
local_slnx = sorted(pathlib.Path(cwd).glob("*.slnx"))
if len(local_slnx) == 1:
@ -88,9 +92,9 @@ def discover_dotnet_target(project_root: str, cwd: str):
return None
def bounded_find_files(root: str, extension: str, max_depth: int):
def bounded_find_files(root: str, extension: str, max_depth: int) -> list[str]:
root_path = pathlib.Path(root).resolve()
results = []
results: list[str] = []
for current_root, dirs, files in os.walk(root_path):
rel = pathlib.Path(current_root).resolve().relative_to(root_path)
depth = len(rel.parts)
@ -105,7 +109,7 @@ def bounded_find_files(root: str, extension: str, max_depth: int):
return sorted(results)
def run_dotnet_action(project_root, working_dir, verb):
def run_dotnet_action(project_root: str, working_dir: str, verb: str) -> tuple[int, StepResult]:
cwd = resolve_cwd(project_root, working_dir)
target = discover_dotnet_target(project_root, cwd)
if not target:
@ -124,10 +128,14 @@ def run_dotnet_action(project_root, working_dir, verb):
args = [verb, target]
step = run_step("dotnet", args, cwd)
step["resolved_target"] = target
return 0 if step["exit_code"] == 0 else step["exit_code"], step
# Explicitly ensure the first return value is an integer and narrow the step result
exit_val = step.get("exit_code", 1)
exit_code = int(exit_val) if isinstance(exit_val, (int, float, str)) and str(exit_val).isdigit() else 1
return exit_code, step
def _deps_hash(app_root):
def _deps_hash(app_root: str) -> str:
h = hashlib.sha256()
for name in ("package.json", "package-lock.json"):
p = pathlib.Path(app_root) / name
@ -136,7 +144,7 @@ def _deps_hash(app_root):
return h.hexdigest()
def ensure_npm_dependencies(app_root):
def ensure_npm_dependencies(app_root: str) -> dict[str, Any]:
package_json = pathlib.Path(app_root) / "package.json"
if not package_json.exists():
return {"installed": False, "reason": "not_applicable"}
@ -174,7 +182,7 @@ def ensure_npm_dependencies(app_root):
return {"installed": True, "reason": "installed", "step": install_step}
def read_package_json(cwd: str):
def read_package_json(cwd: str) -> dict[str, Any] | None:
package_json = pathlib.Path(cwd) / "package.json"
if not package_json.exists():
return None
@ -194,23 +202,23 @@ def has_npm_script(cwd: str, script_name: str) -> bool:
return script_name in scripts and isinstance(scripts.get(script_name), str)
def action_dotnet_build(args):
def action_dotnet_build(args: argparse.Namespace) -> tuple[int, StepResult]:
return run_dotnet_action(args.project_root, args.working_dir, "build")
def action_dotnet_restore(args):
def action_dotnet_restore(args: argparse.Namespace) -> tuple[int, StepResult]:
return run_dotnet_action(args.project_root, args.working_dir, "restore")
def action_dotnet_test(args):
def action_dotnet_test(args: argparse.Namespace) -> tuple[int, StepResult]:
return run_dotnet_action(args.project_root, args.working_dir, "test")
def action_dotnet_publish(args):
def action_dotnet_publish(args: argparse.Namespace) -> tuple[int, StepResult]:
return run_dotnet_action(args.project_root, args.working_dir, "publish")
def action_npm_install(args):
def action_npm_install(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "package.json").exists():
return 0, {
@ -224,10 +232,10 @@ def action_npm_install(args):
"skip_reason": "not_applicable_no_package_json",
}
step = run_step("npm", ["install"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_npm_ci(args):
def action_npm_ci(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "package.json").exists():
return 0, {
@ -241,10 +249,10 @@ def action_npm_ci(args):
"skip_reason": "not_applicable_no_package_json",
}
step = run_step("npm", ["ci"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_npm_build(args):
def action_npm_build(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "package.json").exists():
return 0, {
@ -283,12 +291,12 @@ def action_npm_build(args):
if deps.get("reason") == "install_failed":
step = deps["step"]
step["failure_reason"] = "deps_install_failed"
return step["exit_code"], step
return int(step["exit_code"]), step
step = run_step("npm", ["run", "build"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_npm_test(args):
def action_npm_test(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "package.json").exists():
return 0, {
@ -327,12 +335,12 @@ def action_npm_test(args):
if deps.get("reason") == "install_failed":
step = deps["step"]
step["failure_reason"] = "deps_install_failed"
return step["exit_code"], step
return int(step["exit_code"]), step
step = run_step("npm", ["test"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_npm_audit(args):
def action_npm_audit(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "package.json").exists():
return 0, {
@ -346,37 +354,37 @@ def action_npm_audit(args):
"skip_reason": "not_applicable_no_package_json",
}
step = run_step("npm", ["audit"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_python_venv_create(args):
def action_python_venv_create(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, ".")
venv_dir = args.venv_dir or ".venv"
venv_dir = str(args.venv_dir) if hasattr(args, "venv_dir") else ".venv"
step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_python_pip_install(args):
def action_python_pip_install(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, ".")
req = args.requirements
req = str(args.requirements)
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_python_pip_sync(args):
def action_python_pip_sync(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, ".")
req = args.requirements
req = str(args.requirements)
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_python_pytest(args):
def action_python_pytest(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_cargo_build(args):
def action_cargo_build(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
return 0, {
@ -390,10 +398,10 @@ def action_cargo_build(args):
"skip_reason": "not_applicable_no_cargo_toml",
}
step = run_step("cargo", ["build"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_cargo_test(args):
def action_cargo_test(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
return 0, {
@ -407,10 +415,10 @@ def action_cargo_test(args):
"skip_reason": "not_applicable_no_cargo_toml",
}
step = run_step("cargo", ["test"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_tauri_build(args):
def action_tauri_build(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json"
if not tauri_conf.exists():
@ -431,133 +439,94 @@ def action_tauri_build(args):
if deps.get("reason") == "install_failed":
step = deps["step"]
step["failure_reason"] = "deps_install_failed"
return step["exit_code"], step
return int(step["exit_code"]), step
tauri_args = ["run", "tauri", "build"]
if args.no_bundle:
if hasattr(args, "no_bundle") and args.no_bundle:
tauri_args.extend(["--", "--no-bundle"])
step = run_step("npm", tauri_args, cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_git_status(args):
def action_git_status(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("git", ["status"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_git_fetch(args):
def action_git_fetch(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("git", ["fetch"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_git_pull(args):
def action_git_pull(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("git", ["pull"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_git_clean(args):
def action_git_clean(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("git", ["clean", "-fd"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_docker_build(args):
def action_docker_build(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("docker", ["build", "."], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_docker_compose_up(args):
def action_docker_compose_up(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("docker", ["compose", "up", "-d"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def action_docker_compose_down(args):
def action_docker_compose_down(args: argparse.Namespace) -> tuple[int, StepResult]:
cwd = resolve_cwd(args.project_root, args.working_dir)
step = run_step("docker", ["compose", "down"], cwd)
return 0 if step["exit_code"] == 0 else step["exit_code"], step
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
def main():
def main() -> int:
parser = argparse.ArgumentParser(description="SDT normalized build actions")
sub = parser.add_subparsers(dest="action", required=True)
p0 = sub.add_parser("dotnet-restore")
parse_common(p0)
p0 = sub.add_parser("dotnet-restore"); parse_common(p0)
p1 = sub.add_parser("dotnet-build"); parse_common(p1)
p1b = sub.add_parser("dotnet-test"); parse_common(p1b)
p1c = sub.add_parser("dotnet-publish"); parse_common(p1c)
p2 = sub.add_parser("npm-install"); parse_common(p2)
p2b = sub.add_parser("npm-ci"); parse_common(p2b)
p3 = sub.add_parser("npm-build"); parse_common(p3)
p3b = sub.add_parser("npm-test"); parse_common(p3b)
p3c = sub.add_parser("npm-audit"); parse_common(p3c)
p1 = sub.add_parser("dotnet-build")
parse_common(p1)
p4 = sub.add_parser("python-venv-create"); parse_common(p4)
_ = p4.add_argument("--venv-dir", default=".venv")
p1b = sub.add_parser("dotnet-test")
parse_common(p1b)
p5 = sub.add_parser("python-pip-install"); parse_common(p5)
_ = p5.add_argument("--requirements", required=True)
p1c = sub.add_parser("dotnet-publish")
parse_common(p1c)
p5b = sub.add_parser("python-pip-sync"); parse_common(p5b)
_ = p5b.add_argument("--requirements", required=True)
p2 = sub.add_parser("npm-install")
parse_common(p2)
p5c = sub.add_parser("python-pytest"); parse_common(p5c)
p6 = sub.add_parser("cargo-build"); parse_common(p6)
p6b = sub.add_parser("cargo-test"); parse_common(p6b)
p2b = sub.add_parser("npm-ci")
parse_common(p2b)
p7 = sub.add_parser("tauri-build"); parse_common(p7)
_ = p7.add_argument("--no-bundle", action="store_true")
p3 = sub.add_parser("npm-build")
parse_common(p3)
p3b = sub.add_parser("npm-test")
parse_common(p3b)
p3c = sub.add_parser("npm-audit")
parse_common(p3c)
p4 = sub.add_parser("python-venv-create")
parse_common(p4)
p4.add_argument("--venv-dir", default=".venv")
p5 = sub.add_parser("python-pip-install")
parse_common(p5)
p5.add_argument("--requirements", required=True)
p5b = sub.add_parser("python-pip-sync")
parse_common(p5b)
p5b.add_argument("--requirements", required=True)
p5c = sub.add_parser("python-pytest")
parse_common(p5c)
p6 = sub.add_parser("cargo-build")
parse_common(p6)
p6b = sub.add_parser("cargo-test")
parse_common(p6b)
p7 = sub.add_parser("tauri-build")
parse_common(p7)
p7.add_argument("--no-bundle", action="store_true")
p8 = sub.add_parser("git-status")
parse_common(p8)
p9 = sub.add_parser("git-fetch")
parse_common(p9)
p10 = sub.add_parser("git-pull")
parse_common(p10)
p11 = sub.add_parser("git-clean")
parse_common(p11)
p12 = sub.add_parser("docker-build")
parse_common(p12)
p13 = sub.add_parser("docker-compose-up")
parse_common(p13)
p14 = sub.add_parser("docker-compose-down")
parse_common(p14)
p8 = sub.add_parser("git-status"); parse_common(p8)
p9 = sub.add_parser("git-fetch"); parse_common(p9)
p10 = sub.add_parser("git-pull"); parse_common(p10)
p11 = sub.add_parser("git-clean"); parse_common(p11)
p12 = sub.add_parser("docker-build"); parse_common(p12)
p13 = sub.add_parser("docker-compose-up"); parse_common(p13)
p14 = sub.add_parser("docker-compose-down"); parse_common(p14)
args = parser.parse_args()
@ -590,7 +559,8 @@ def main():
code, summary = handlers[args.action](args)
if args.json:
print(json.dumps(summary))
return code
return int(code)
if __name__ == "__main__":

View File

@ -1,90 +1,42 @@
#!/usr/bin/env python3
import argparse
from script_common import find_node_app_root, resolve_repo_root, run, sha256_files
from typing import cast
from script_common import ensure_npm_build, find_node_app_root, resolve_repo_root
def main() -> int:
parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper")
parser.add_argument("--target", choices=["web", "tauri"], default="web")
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none")
parser.add_argument("--install-deps", action="store_true")
parser.add_argument("--skip-install", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--repo-root", default=None)
parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json")
_ = parser.add_argument("--target", choices=["web", "tauri"], default="web")
_ = parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
_ = parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none")
_ = parser.add_argument("--install-deps", action="store_true")
_ = parser.add_argument("--skip-install", action="store_true")
_ = parser.add_argument("--dry-run", action="store_true")
_ = parser.add_argument("--repo-root", default=None)
_ = parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json")
args = parser.parse_args()
repo_root = resolve_repo_root(args.repo_root)
app_root = find_node_app_root(repo_root, args.app_root)
repo_root_val = cast(str | None, args.repo_root)
repo_root = resolve_repo_root(repo_root_val)
app_root_val = cast(str | None, args.app_root)
app_root = find_node_app_root(repo_root, app_root_val)
if app_root is None:
print("Unable to locate app root (no unique package.json found).")
return 2
package_json = app_root / "package.json"
lock_file = app_root / "package-lock.json"
node_modules = app_root / "node_modules"
deps_hash_file = node_modules / ".sdt-deps.sha256"
expected_hash = sha256_files([package_json, lock_file])
should_install = args.install_deps or not node_modules.exists()
if not should_install and not args.skip_install:
if not deps_hash_file.exists():
should_install = True
else:
current = deps_hash_file.read_text(encoding="utf-8").strip()
should_install = current != expected_hash
if args.skip_install:
should_install = False
print(f"App root: {app_root}")
print(f"Target: {args.target} ({args.configuration})")
if should_install:
install_args = ["ci", "--no-audit", "--fund=false"] if lock_file.exists() else ["install", "--no-audit", "--fund=false"]
print("$ npm " + " ".join(install_args))
if not args.dry_run:
code = run("npm", install_args, app_root)
if code != 0:
if lock_file.exists() and install_args[0] == "ci":
print("npm ci failed (likely lockfile out of sync). Falling back to npm install...")
fallback_args = ["install", "--no-audit", "--fund=false"]
print("$ npm " + " ".join(fallback_args))
code = run("npm", fallback_args, app_root)
if code != 0:
return code
else:
return code
node_modules.mkdir(parents=True, exist_ok=True)
deps_hash_file.write_text(expected_hash, encoding="utf-8")
else:
print("Skipping dependency install.")
if args.target == "web":
cmd = ["run", "build"]
print("$ npm " + " ".join(cmd))
if not args.dry_run:
return run("npm", cmd, app_root)
# If dry-run is requested, we just print intent.
if args.dry_run:
print(f"Dry-run: Would build {args.target} ({args.configuration}) in {app_root}")
return 0
tauri_cmd = ["run", "tauri", "build"]
tauri_tail: list[str] = []
if args.tauri_bundles == "none":
tauri_tail.extend(["--no-bundle"])
else:
tauri_tail.extend(["--bundles", args.tauri_bundles])
if args.configuration == "Debug":
tauri_tail.append("--debug")
if tauri_tail:
tauri_cmd.extend(["--", *tauri_tail])
res = ensure_npm_build(
app_root=app_root,
target=str(args.target),
configuration=str(args.configuration),
tauri_bundles=str(args.tauri_bundles)
)
print("$ npm " + " ".join(tauri_cmd))
if not args.dry_run:
return run("npm", tauri_cmd, app_root)
return 0
return int(res["exit_code"])
if __name__ == "__main__":

View File

@ -1,22 +1,11 @@
#!/usr/bin/env python3
import argparse
import json
import shutil
import subprocess
import sys
import os
import json
from pathlib import Path
from script_common import find_csproj_by_keyword, find_node_app_root, resolve_repo_root
def run_step(label: str, cmd: list[str], cwd: Path, dry_run: bool) -> int:
print(f"\n> {label}")
print("$", " ".join(cmd))
if dry_run:
return 0
proc = subprocess.run(cmd, cwd=str(cwd), check=False)
return proc.returncode
from script_common import find_csproj_by_keyword, find_node_app_root, resolve_repo_root, run # type: ignore
def has_package_script(app_root: Path, script_name: str) -> bool:
package_json = app_root / "package.json"
@ -35,18 +24,18 @@ def has_package_script(app_root: Path, script_name: str) -> bool:
def main() -> int:
parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints")
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
parser.add_argument("--runtime", default="win-x64")
parser.add_argument("--skip-sidecar", action="store_true")
parser.add_argument("--skip-web", action="store_true")
parser.add_argument("--skip-webgateway", action="store_true")
parser.add_argument("--skip-tauri", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--repo-root", default=None)
parser.add_argument("--sidecar-project", default=None)
parser.add_argument("--gateway-project", default=None)
parser.add_argument("--app-root", default=None)
parser.add_argument("--output-dir", default="output")
_ = parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
_ = parser.add_argument("--runtime", default="win-x64")
_ = parser.add_argument("--skip-sidecar", action="store_true")
_ = parser.add_argument("--skip-web", action="store_true")
_ = parser.add_argument("--skip-webgateway", action="store_true")
_ = parser.add_argument("--skip-tauri", action="store_true")
_ = parser.add_argument("--dry-run", action="store_true")
_ = parser.add_argument("--repo-root", default=None)
_ = parser.add_argument("--sidecar-project", default=None)
_ = parser.add_argument("--gateway-project", default=None)
_ = parser.add_argument("--app-root", default=None)
_ = parser.add_argument("--output-dir", default="output")
args = parser.parse_args()
repo_root = resolve_repo_root(args.repo_root)
@ -56,45 +45,48 @@ def main() -> int:
sidecar_project = (repo_root / args.sidecar_project).resolve() if args.sidecar_project else find_csproj_by_keyword(repo_root, ["sidecar"])
gateway_project = (repo_root / args.gateway_project).resolve() if args.gateway_project else find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
app_root = (repo_root / args.app_root).resolve() if args.app_root else find_node_app_root(repo_root, None)
tauri_conf = None
if app_root is not None:
candidate_a = app_root / "src-tauri" / "tauri.conf.json"
candidate_b = app_root / "tauri.conf.json"
if candidate_a.exists():
tauri_conf = candidate_a
elif candidate_b.exists():
tauri_conf = candidate_b
tauri_conf = next((p for p in [app_root/"src-tauri"/"tauri.conf.json", app_root/"tauri.conf.json"] if p.exists()), None)
py = sys.executable
scripts_dir = Path(__file__).parent
if not args.skip_sidecar:
if sidecar_project is None:
print("Skipping sidecar: no sidecar csproj detected.")
else:
cmd = [py, "scripts/publish-sidecar.py", "--configuration", args.configuration, "--runtime", args.runtime]
cmd.extend(["--project", str(sidecar_project)])
code = run_step("Publish sidecar", cmd, repo_root, args.dry_run)
if code != 0:
return code
cmd = ["-m", "scripts.publish-sidecar" if __package__ else "publish-sidecar",
"--configuration", args.configuration, "--runtime", args.runtime, "--project", str(sidecar_project)]
print(f"\n> Publishing Sidecar\n$ {py} {' '.join(cmd)}")
if not args.dry_run:
code = run(py, cmd, scripts_dir)
if code != 0: return code
if not args.skip_web:
if app_root is None:
print("Skipping web: no app root with package.json detected.")
print("Skipping web: no app root detected.")
elif not has_package_script(app_root, "build"):
print("Skipping web: package.json has no 'build' script.")
else:
cmd = [py, "scripts/publish-app.py", "--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)]
code = run_step("Build web", cmd, repo_root, args.dry_run)
if code != 0:
return code
cmd = ["-m", "scripts.publish-app" if __package__ else "publish-app",
"--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)]
print(f"\n> Building Web\n$ {py} {' '.join(cmd)}")
if not args.dry_run:
code = run(py, cmd, scripts_dir)
if code != 0: return code
if not args.skip_webgateway:
if gateway_project is None:
print("Skipping web gateway: no gateway csproj detected.")
else:
cmd = [py, "scripts/publish-webgateway.py", "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)]
code = run_step("Publish web gateway", cmd, repo_root, args.dry_run)
if code != 0:
return code
cmd = ["-m", "scripts.publish-webgateway" if __package__ else "publish-webgateway",
"--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)]
print(f"\n> Publishing Web Gateway\n$ {py} {' '.join(cmd)}")
if not args.dry_run:
code = run(py, cmd, scripts_dir)
if code != 0: return code
if not args.skip_tauri:
if app_root is None or tauri_conf is None:
@ -102,17 +94,20 @@ def main() -> int:
elif not has_package_script(app_root, "tauri"):
print("Skipping tauri: package.json has no 'tauri' script.")
else:
cmd = [py, "scripts/publish-app.py", "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)]
code = run_step("Build tauri", cmd, repo_root, args.dry_run)
if code != 0:
return code
cmd = ["-m", "scripts.publish-app" if __package__ else "publish-app",
"--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)]
print(f"\n> Building Tauri\n$ {py} {' '.join(cmd)}")
if not args.dry_run:
code = run(py, cmd, scripts_dir)
if code != 0: return code
target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release")
exes = sorted(target_dir.glob("*.exe"), key=lambda p: p.stat().st_mtime, reverse=True)
pattern = "*.exe" if os.name == "nt" else "*"
exes = sorted((p for p in target_dir.glob(pattern) if p.is_file() and (os.name == "nt" or not p.suffix)), key=lambda p: p.stat().st_mtime, reverse=True)
if exes:
staged = output_root / exes[0].name
if args.dry_run:
print(f"Would copy: {exes[0]} -> {staged}")
if args.dry_run: print(f"Would copy: {exes[0]} -> {staged}")
else:
shutil.copy2(exes[0], staged)
print(f"Staged desktop executable: {staged}")

View File

@ -0,0 +1,10 @@
param(
[Parameter(ValueFromRemainingArguments = $($true))]
[string[]]$ForwardArgs
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1')
Invoke-SdtPythonScript -ScriptName 'publish-sidecar.py' -ForwardArgs $ForwardArgs

View File

@ -1,51 +1,48 @@
#!/usr/bin/env python3
import argparse
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
from typing import cast
from script_common import ensure_dotnet_publish, find_csproj_by_keyword, resolve_repo_root, SdtResult
def main() -> int:
parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper")
parser.add_argument("--configuration", default="Release")
parser.add_argument("--runtime", default="win-x64")
parser.add_argument("--repo-root", default=None)
parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj")
parser.add_argument("--output-dir", default="output")
_ = parser.add_argument("--configuration", default="Release")
_ = parser.add_argument("--runtime", default="win-x64")
_ = parser.add_argument("--repo-root", default=None)
_ = parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj")
_ = parser.add_argument("--output-dir", default="output")
args = parser.parse_args()
repo_root = resolve_repo_root(args.repo_root)
output_dir = (repo_root / args.output_dir).resolve()
repo_root_val = cast(str | None, args.repo_root)
repo_root = resolve_repo_root(repo_root_val)
output_dir_val = cast(str, args.output_dir)
output_dir = (repo_root / output_dir_val).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
if args.project:
csproj = (repo_root / args.project).resolve()
project_val = cast(str | None, args.project)
if project_val:
csproj = (repo_root / project_val).resolve()
else:
csproj = find_csproj_by_keyword(repo_root, ["sidecar"])
if csproj is None or not csproj.exists():
print("Could not locate sidecar project. Pass --project <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
res: SdtResult = ensure_dotnet_publish(
csproj=csproj,
output_dir=output_dir,
configuration=str(args.configuration),
runtime=str(args.runtime),
single_file=True,
self_contained=True
)
binary_name = csproj.stem + (".exe" if args.runtime.startswith("win-") else "")
if res["exit_code"] != 0:
return int(res["exit_code"])
runtime_val = str(args.runtime)
binary_name = csproj.stem + (".exe" if runtime_val.startswith("win-") else "")
binary_path = output_dir / binary_name
if binary_path.exists():
print(f"Published executable: {binary_path}")

View File

@ -1,74 +1,67 @@
#!/usr/bin/env python3
import argparse
import shutil
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
from typing import cast
from script_common import ensure_dotnet_publish, find_csproj_by_keyword, resolve_repo_root, SdtResult
def main() -> int:
parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper")
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
parser.add_argument("--runtime", default="win-x64")
parser.add_argument("--self-contained", action="store_true")
parser.add_argument("--skip-web-assets", action="store_true")
parser.add_argument("--repo-root", default=None)
parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj")
parser.add_argument("--web-build-dir", default=None, help="Relative path to web build assets root")
parser.add_argument("--output-dir", default="output/webgateway")
_ = parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
_ = parser.add_argument("--runtime", default="win-x64")
_ = parser.add_argument("--self-contained", action="store_true")
_ = parser.add_argument("--skip-web-assets", action="store_true")
_ = parser.add_argument("--repo-root", default=None)
_ = parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj")
_ = parser.add_argument("--web-build-dir", default=None, help="Relative path to web build assets root")
_ = parser.add_argument("--output-dir", default="output/webgateway")
args = parser.parse_args()
repo_root = resolve_repo_root(args.repo_root)
output_dir = (repo_root / args.output_dir).resolve()
repo_root_val = cast(str | None, args.repo_root)
repo_root = resolve_repo_root(repo_root_val)
output_dir_val = cast(str, args.output_dir)
output_dir = (repo_root / output_dir_val).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
if args.project:
csproj = (repo_root / args.project).resolve()
project_val = cast(str | None, args.project)
if project_val:
csproj = (repo_root / project_val).resolve()
else:
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
if csproj is None or not csproj.exists():
print("Could not locate web gateway project. Pass --project <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
res: SdtResult = ensure_dotnet_publish(
csproj=csproj,
output_dir=output_dir,
configuration=str(args.configuration),
runtime=str(args.runtime),
self_contained=bool(args.self_contained),
single_file=False
)
if res["exit_code"] != 0:
return int(res["exit_code"])
if not args.skip_web_assets:
if args.web_build_dir:
web_build_dir = (repo_root / args.web_build_dir).resolve()
web_build_dir_val = cast(str | None, args.web_build_dir)
if web_build_dir_val:
web_build_dir = (repo_root / web_build_dir_val).resolve()
else:
web_build_dir = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None)
if web_build_dir is not None:
web_build_dir = web_build_dir / "build"
# Look for recent web build output
# (Note: rglob is costly but necessary for discovery here)
web_pj = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None)
web_build_dir = web_pj / "build" if web_pj else None
if web_build_dir is None or not web_build_dir.exists():
print("Web assets not found. Skip with --skip-web-assets or pass --web-build-dir.")
else:
web_out = output_dir / "wwwroot"
web_out.mkdir(parents=True, exist_ok=True)
for item in web_build_dir.iterdir():
dst = web_out / item.name
if item.is_dir():
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(item, dst)
else:
shutil.copy2(item, dst)
print(f"Copied web assets: {web_out}")
print(f"Copying web assets: {web_build_dir} -> {web_out}")
shutil.copytree(web_build_dir, web_out, dirs_exist_ok=True)
print(f"Copied web assets to {web_out}")
print(f"Publish completed: {output_dir}")
return 0

View File

@ -0,0 +1,133 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Legacy compatibility helper only.
# Active SDT workflows and shell bootstrap now use Python scripts.
function Clear-SdtProxyEnv {
Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:ALL_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:http_proxy -ErrorAction SilentlyContinue
Remove-Item Env:https_proxy -ErrorAction SilentlyContinue
Remove-Item Env:all_proxy -ErrorAction SilentlyContinue
Remove-Item Env:GIT_HTTP_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:GIT_HTTPS_PROXY -ErrorAction SilentlyContinue
Remove-Item Env:PIP_NO_INDEX -ErrorAction SilentlyContinue
}
function Test-SdtConfigExists {
param([string]$Path)
if (Test-Path (Join-Path $Path "devtool.json")) {
return $true
}
$sdtConfigs = Get-ChildItem -Path $Path -Filter "sdtconfig-*.json" -File -ErrorAction SilentlyContinue
return ($null -ne $sdtConfigs) -and ($sdtConfigs.Count -gt 0)
}
function Resolve-SdtRepoRoot {
param([string]$StartPath)
$candidateStarts = @()
if (-not [string]::IsNullOrWhiteSpace($StartPath)) {
$candidateStarts += $StartPath
}
$cwd = (Get-Location).Path
if (-not [string]::IsNullOrWhiteSpace($cwd) -and ($candidateStarts -notcontains $cwd)) {
$candidateStarts += $cwd
}
$override = $env:SDT_REPO_ROOT
if ([string]::IsNullOrWhiteSpace($override)) {
$override = $env:JOURNAL_REPO_ROOT # backward compatibility
}
if (-not [string]::IsNullOrWhiteSpace($override)) {
$overridePath = [System.IO.Path]::GetFullPath($override)
if (Test-SdtConfigExists -Path $overridePath) {
return $overridePath
}
}
foreach ($start in $candidateStarts) {
$cursor = [System.IO.Path]::GetFullPath($start)
while (-not [string]::IsNullOrWhiteSpace($cursor)) {
if (Test-SdtConfigExists -Path $cursor) {
return $cursor
}
$parent = [System.IO.Directory]::GetParent($cursor)
if ($null -eq $parent -or $parent.FullName -eq $cursor) {
break
}
$cursor = $parent.FullName
}
}
if (Get-Command git -ErrorAction SilentlyContinue) {
foreach ($start in $candidateStarts) {
try {
$gitRoot = & git -C $start rev-parse --show-toplevel 2>$null
if ($? -and -not [string]::IsNullOrWhiteSpace($gitRoot)) {
return [System.IO.Path]::GetFullPath($gitRoot.Trim())
}
}
catch {}
}
}
throw "Could not locate repository root. Ensure a project config (sdtconfig-*.json or devtool.json) exists in the project root."
}
function Initialize-SdtDotnetEnv {
param([Parameter(Mandatory = $true)][string]$RepoRoot)
$dotnetCliHome = Join-Path $RepoRoot ".dotnet_home"
$nugetPackages = Join-Path $RepoRoot ".nuget\packages"
$nugetHttpCachePath = Join-Path $RepoRoot ".nuget\http-cache"
$env:DOTNET_CLI_HOME = $dotnetCliHome
$env:NUGET_PACKAGES = $nugetPackages
$env:NUGET_HTTP_CACHE_PATH = $nugetHttpCachePath
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1"
$env:DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = "0"
$env:DOTNET_GENERATE_ASPNET_CERTIFICATE = "0"
$env:DOTNET_CLI_TELEMETRY_OPTOUT = "1"
$env:NUGET_CERT_REVOCATION_MODE = "offline"
New-Item -ItemType Directory -Force -Path $dotnetCliHome, $nugetPackages, $nugetHttpCachePath | Out-Null
}
function Initialize-SdtPipEnv {
param([Parameter(Mandatory = $true)][string]$RepoRoot)
$pipCacheDir = Join-Path $RepoRoot ".pip\cache"
$pipTempDir = Join-Path $RepoRoot ".tmp\pip-temp"
$env:PIP_CACHE_DIR = $pipCacheDir
$env:TEMP = $pipTempDir
$env:TMP = $pipTempDir
$env:PIP_DISABLE_PIP_VERSION_CHECK = "1"
$env:PIP_DEFAULT_TIMEOUT = "30"
$env:PIP_RETRIES = "2"
New-Item -ItemType Directory -Force -Path $pipCacheDir, $pipTempDir | Out-Null
}
function Initialize-SdtHuggingFaceEnv {
param([Parameter(Mandatory = $true)][string]$RepoRoot)
$hfHome = Join-Path $RepoRoot ".cache\huggingface"
$hfHubCache = Join-Path $hfHome "hub"
$env:HF_HOME = $hfHome
$env:HUGGINGFACE_HUB_CACHE = $hfHubCache
$env:HF_HUB_DISABLE_SYMLINKS_WARNING = "1"
New-Item -ItemType Directory -Force -Path $hfHubCache | Out-Null
}
# Backward-compatible aliases (legacy script calls)
Set-Alias -Name Clear-JournalProxyEnv -Value Clear-SdtProxyEnv -Scope Script
Set-Alias -Name Resolve-JournalRepoRoot -Value Resolve-SdtRepoRoot -Scope Script
Set-Alias -Name Initialize-JournalDotnetEnv -Value Initialize-SdtDotnetEnv -Scope Script
Set-Alias -Name Initialize-JournalPipEnv -Value Initialize-SdtPipEnv -Scope Script
Set-Alias -Name Initialize-JournalHuggingFaceEnv -Value Initialize-SdtHuggingFaceEnv -Scope Script

View File

@ -6,184 +6,50 @@ import pathlib
import shutil
import subprocess
import sys
from typing import Dict, Iterable, List, Sequence
import time
from typing import Any, Iterable, Optional, Sequence
# --- Domain: SDT Types ---
# SDT Normalized Result Object
# result["exit_code"]: int
# result["status"]: "ok" | "failed" | "skipped"
# result["elapsed_seconds"]: float
# result["failure_reason"]: Optional[str]
# result["skip_reason"]: Optional[str]
SdtResult = dict[str, Any]
PROXY_VARS = [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
"GIT_HTTP_PROXY",
"GIT_HTTPS_PROXY",
"PIP_NO_INDEX",
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy",
"GIT_HTTP_PROXY", "GIT_HTTPS_PROXY", "PIP_NO_INDEX"
]
# --- Domain: FS Utilities ---
def resolve_repo_root(start: str | None = None) -> pathlib.Path:
base = pathlib.Path(start or os.getcwd()).resolve()
def sha256_files(paths: Iterable[pathlib.Path]) -> str:
h = hashlib.sha256()
for p in sorted(paths):
if p.exists(): h.update(p.read_bytes())
return h.hexdigest()
# Preferred marker for SDT-managed projects.
for cur in [base, *base.parents]:
cfg = cur / "devtool.json"
if cfg.exists():
hints = load_project_root_hints(cur)
if not hints:
return cur
if any(_hint_matches(cur, hint) for hint in hints):
return cur
# Fall back to git root when available.
try:
proc = subprocess.run(
["git", "-C", str(base), "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
check=False,
)
if proc.returncode == 0:
git_root = proc.stdout.strip()
if git_root:
return pathlib.Path(git_root).resolve()
except Exception:
pass
return base
def load_project_root_hints(repo_root: pathlib.Path) -> list[str]:
cfg = repo_root / "devtool.json"
if not cfg.exists():
return []
try:
data = json.loads(cfg.read_text(encoding="utf-8"))
hints = data.get("project", {}).get("rootHints", [])
return [str(x) for x in hints if isinstance(x, str) and x.strip()]
except Exception:
return []
def ensure_dirs(paths: List[pathlib.Path]) -> None:
def first_existing(paths: Iterable[pathlib.Path]) -> Optional[pathlib.Path]:
for p in paths:
p.mkdir(parents=True, exist_ok=True)
if p.exists(): return p
return None
def newest_file(search_root: pathlib.Path, pattern: str) -> Optional[pathlib.Path]:
hits = sorted(search_root.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True)
return hits[0] if hits else None
def clean_proxy_env(env: Dict[str, str]) -> None:
for k in PROXY_VARS:
env.pop(k, None)
def dotnet_env(repo_root: pathlib.Path) -> Dict[str, str]:
env = dict(os.environ)
clean_proxy_env(env)
dotnet_cli_home = repo_root / ".dotnet_home"
nuget_packages = repo_root / ".nuget" / "packages"
nuget_http_cache = repo_root / ".nuget" / "http-cache"
ensure_dirs([dotnet_cli_home, nuget_packages, nuget_http_cache])
env["DOTNET_CLI_HOME"] = str(dotnet_cli_home)
env["NUGET_PACKAGES"] = str(nuget_packages)
env["NUGET_HTTP_CACHE_PATH"] = str(nuget_http_cache)
env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"
env["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "0"
env["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "0"
env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
env["NUGET_CERT_REVOCATION_MODE"] = "offline"
return env
def pip_env(repo_root: pathlib.Path) -> Dict[str, str]:
env = dict(os.environ)
clean_proxy_env(env)
pip_cache = repo_root / ".pip" / "cache"
pip_tmp = repo_root / ".tmp" / "pip-temp"
ensure_dirs([pip_cache, pip_tmp])
env["PIP_CACHE_DIR"] = str(pip_cache)
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
env["PIP_DEFAULT_TIMEOUT"] = "30"
env["PIP_RETRIES"] = "2"
env["TEMP"] = str(pip_tmp)
env["TMP"] = str(pip_tmp)
return env
def run(command: str, args: List[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> int:
resolved = resolve_command(command)
try:
proc = subprocess.run([resolved, *args], cwd=str(cwd), env=env, check=False)
return proc.returncode
except FileNotFoundError:
print(f"Command not found: {resolved}", file=sys.stderr)
return 127
def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> tuple[int, str, str]:
resolved = resolve_command(command)
try:
proc = subprocess.run(
[resolved, *args],
cwd=str(cwd),
env=env,
capture_output=True,
text=True,
check=False,
)
return proc.returncode, proc.stdout, proc.stderr
except FileNotFoundError:
return 127, "", f"Command not found: {resolved}"
def resolve_command(command: str) -> str:
if not command:
return command
if os.name != "nt":
return command
if any(sep in command for sep in ("\\", "/")):
return command
if pathlib.Path(command).suffix:
found = shutil.which(command)
return found or command
candidates = []
lowered = command.lower()
if lowered in ("npm", "npx", "pnpm", "yarn", "tauri"):
candidates.extend([f"{command}.cmd", f"{command}.exe", f"{command}.bat", command])
else:
candidates.append(command)
for c in candidates:
found = _which_windows(c)
if found:
name = pathlib.Path(found).name.lower()
if name in ("npm", "npx", "pnpm", "yarn", "tauri"):
shim = pathlib.Path(found).with_name(name + ".cmd")
if shim.exists():
return str(shim)
return found
if lowered in ("npm", "npx", "pnpm", "yarn"):
node = _which_windows("node.exe") or _which_windows("node")
if node:
node_dir = pathlib.Path(node).parent
shim = node_dir / f"{lowered}.cmd"
if shim.exists():
return str(shim)
return candidates[-1]
def ensure_dirs(paths: list[pathlib.Path]) -> None:
for p in paths: p.mkdir(parents=True, exist_ok=True)
def _hint_matches(root: pathlib.Path, hint: str) -> bool:
h = hint.strip()
if not h:
return False
try:
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))
@ -192,171 +58,246 @@ def _hint_matches(root: pathlib.Path, hint: str) -> bool:
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 ("\\", "/")):
# If hint is a plain filename marker, allow bounded search in root tree.
if not any(sep in h for sep in ("/", "\\")):
return any(p.name == h for p in root.rglob(h))
return False
except:
return False
# --- Domain: Project Discovery ---
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 resolve_repo_root(start: str | None = None) -> pathlib.Path:
base = pathlib.Path(start or os.getcwd()).resolve()
for cur in [base, *base.parents]:
sdt_configs = list(cur.glob("sdtconfig-*.json"))
cfg = sdt_configs[0] if sdt_configs else (cur / "devtool.json")
if cfg.exists():
hints = load_project_root_hints(cur, cfg)
if not hints or any(_hint_matches(cur, hint) for hint in hints): return cur
try:
proc = subprocess.run(["git", "-C", str(base), "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=False)
if proc.returncode == 0 and proc.stdout.strip(): return pathlib.Path(proc.stdout.strip()).resolve()
except: pass
return base
def load_project_root_hints(repo_root: pathlib.Path, cfg: pathlib.Path | None = None) -> list[str]:
if cfg is None:
sdt_configs = list(repo_root.glob("sdtconfig-*.json"))
cfg = sdt_configs[0] if sdt_configs else (repo_root / "devtool.json")
if not cfg.exists(): return []
try:
data = json.loads(cfg.read_text(encoding="utf-8"))
hints = data.get("project", {}).get("rootHints", [])
return [str(x) for x in hints if isinstance(x, str) and x.strip()]
except: return []
def _which_windows(command: str) -> str | None:
found = shutil.which(command)
if found:
return found
if os.name != "nt":
def _resolve_project_config_path(repo_root: pathlib.Path) -> Optional[pathlib.Path]:
sdt_configs = sorted(repo_root.glob("sdtconfig-*.json"), key=lambda p: p.name.lower())
if sdt_configs:
return sdt_configs[0]
legacy = repo_root / "devtool.json"
if legacy.exists():
return legacy
return None
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():
def load_node_working_dir(repo_root: pathlib.Path) -> Optional[str]:
cfg = _resolve_project_config_path(repo_root)
if cfg is None:
return None
try:
data = json.loads(package_json.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else None
except Exception:
data = json.loads(cfg.read_text(encoding="utf-8"))
node = data.get("toolchains", {}).get("node", {})
value = node.get("workingDir")
if isinstance(value, str) and value.strip():
return value.strip()
except:
pass
return None
def _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 find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> Optional[pathlib.Path]:
if hints:
for h in hints:
p = (repo_root / h).resolve()
if p.exists() and p.suffix == ".csproj": return p
hits = [h for h in repo_root.rglob("*.csproj") if not any(x in h.parts for x in [".git", "node_modules", "bin", "obj"])]
return hits[0] if len(hits) == 1 else None
def _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 find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> Optional[pathlib.Path]:
hits = [h for h in repo_root.rglob("*.csproj") if not any(x in h.parts for x in [".git", "node_modules", "bin", "obj"])]
for kw in keywords:
matches = [h for h in hits if kw.lower() in h.name.lower()]
if len(matches) == 1: return matches[0]
return None
def _iter_package_jsons() -> list[pathlib.Path]:
excluded = {"node_modules", "dist", "build", ".git", ".sdt", ".venv", "venv", "bin", "obj"}
found: list[pathlib.Path] = []
for current_root, dirs, files in os.walk(repo_root):
dirs[:] = [d for d in dirs if d not in excluded]
if "package.json" in files:
found.append(pathlib.Path(current_root) / "package.json")
found.sort(key=lambda p: len(p.parts))
return found
def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> Optional[pathlib.Path]:
def _read_pj(p: pathlib.Path):
try: return json.loads(p.read_text(encoding="utf-8"))
except: return None
def _has_scr(p: pathlib.Path, names: Sequence[str]):
src = _read_pj(p)
return any(src.get("scripts", {}).get(n) for n in names) if src else False
def _is_tauri(d: pathlib.Path): return (d / "src-tauri" / "tauri.conf.json").exists() or (d / "tauri.conf.json").exists()
def _iter_pj():
excluded = {".git", "node_modules", ".sdt", "dist", "build", ".venv", "venv", "bin", "obj"}
for p in repo_root.rglob("package.json"):
if any(x in p.parts for x in excluded):
continue
yield p
if not preferred:
preferred = load_node_working_dir(repo_root)
if preferred:
p = (repo_root / preferred).resolve()
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")):
p = p.parent if p.is_file() else p
if (p / "package.json").exists():
return p
package_files = _iter_package_jsons()
if not package_files:
tauri = [p.parent for p in _iter_pj() if _is_tauri(p.parent)]
if len(tauri) == 1: return tauri[0]
if len(tauri) > 1:
tauri = sorted(set(tauri), key=lambda d: (len(d.parts), str(d).lower()))
return tauri[0]
scripts = [p.parent for p in _iter_pj() if _has_scr(p, ["tauri", "build"])]
if len(scripts) == 1: return scripts[0]
if len(scripts) > 1:
scripts = sorted(set(scripts), key=lambda d: (len(d.parts), str(d).lower()))
return scripts[0]
all_pj = [p.parent for p in _iter_pj()]
return all_pj[0] if len(all_pj) == 1 else None
# --- Domain: Environment Setup ---
def clean_proxy_env(env: dict[str, str]) -> None:
for k in PROXY_VARS: env.pop(k, None)
def dotnet_env(repo_root: pathlib.Path) -> dict[str, str]:
env = dict(os.environ)
clean_proxy_env(env)
h, p, c = repo_root/".dotnet_home", repo_root/".nuget"/"packages", repo_root/".nuget"/"http-cache"
ensure_dirs([h, p, c])
env.update({"DOTNET_CLI_HOME":str(h), "NUGET_PACKAGES":str(p), "NUGET_HTTP_CACHE_PATH":str(c),
"DOTNET_SKIP_FIRST_TIME_EXPERIENCE":"1", "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH":"0",
"DOTNET_GENERATE_ASPNET_CERTIFICATE":"0",
"DOTNET_CLI_TELEMETRY_OPTOUT":"1", "NUGET_CERT_REVOCATION_MODE":"offline"})
return env
def pip_env(repo_root: pathlib.Path) -> dict[str, str]:
env = dict(os.environ)
clean_proxy_env(env)
c, t = repo_root/".pip"/"cache", repo_root/".tmp"/"pip-temp"
ensure_dirs([c, t])
env.update({"PIP_CACHE_DIR":str(c), "PIP_DISABLE_PIP_VERSION_CHECK":"1", "PIP_DEFAULT_TIMEOUT":"30", "PIP_RETRIES":"2", "TEMP":str(t), "TMP":str(t)})
return env
# --- Domain: Process Execution ---
def resolve_command(command: str) -> str:
if not command or os.name != "nt" or any(s in command for s in ("\\", "/")): return command
if pathlib.Path(command).suffix: return shutil.which(command) or command
low = command.lower()
cands = [f"{command}.cmd", f"{command}.exe", f"{command}.bat", command] if low in ("npm", "npx", "pnpm", "yarn", "tauri") else [command]
for c in cands:
found = _which_windows(c)
if found:
if pathlib.Path(found).name.lower() in ("npm", "npx", "pnpm", "yarn", "tauri"):
shim = pathlib.Path(found).with_name(pathlib.Path(found).name.lower() + ".cmd")
if shim.exists(): return str(shim)
return found
if low in ("npm", "npx", "pnpm", "yarn"):
node = _which_windows("node.exe") or _which_windows("node")
if node and (pathlib.Path(node).parent / f"{low}.cmd").exists(): return str(pathlib.Path(node).parent / f"{low}.cmd")
return cands[-1]
def _which_windows(command: str) -> Optional[str]:
found = shutil.which(command)
if found: return str(pathlib.Path(found).resolve())
for p in ["C:\\Program Files\\dotnet", "C:\\Program Files\\nodejs", "C:\\Program Files\\Git\\bin", "C:\\Program Files\\Git\\usr\\bin"]:
if (pathlib.Path(p) / command).exists(): return str((pathlib.Path(p) / command).resolve())
return None
# 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]
def run(command: str, args: list[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> int:
try:
proc = subprocess.run([resolve_command(command), *args], cwd=str(cwd), env=env, check=False)
return proc.returncode
except: return 127
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]
def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> tuple[int, str, str]:
try:
proc = subprocess.run([resolve_command(command), *args], cwd=str(cwd), env=env, capture_output=True, text=True, check=False, encoding="utf-8", errors="replace")
return proc.returncode, proc.stdout, proc.stderr
except: return 127, "", "Command not found"
# As a last fallback, return unique package root only.
if len(package_files) == 1:
return package_files[0].parent
return None
def _safe_stream_write(stream: Any, text: str) -> None:
if not text:
return
try:
stream.write(text)
return
except UnicodeEncodeError:
pass
encoding = getattr(stream, "encoding", None) or "utf-8"
data = text.encode(encoding, errors="replace")
buffer = getattr(stream, "buffer", None)
if buffer is not None:
buffer.write(data)
buffer.flush()
return
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]
# Last resort when no binary buffer is exposed.
stream.write(data.decode(encoding, errors="replace"))
def run_step_with_summary(label: str, command: str, args: Sequence[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> SdtResult:
print(f"\n> {label}\n$ {command} {' '.join(args)}")
start = time.time()
code, out, err = run_capture(command, args, cwd, env)
elapsed = round(time.time() - start, 3)
if out: _safe_stream_write(sys.stdout, out)
if err: _safe_stream_write(sys.stderr, err)
return {"exit_code": code, "status": "ok" if code == 0 else "failed", "elapsed_seconds": elapsed, "stdout": out, "stderr": err, "failure_reason": None, "skip_reason": None}
# --- Domain: High-Level Build Logic ---
def ensure_dotnet_publish(csproj: pathlib.Path, output_dir: pathlib.Path, configuration: str = "Release", runtime: str = "win-x64", single_file: bool = False, self_contained: bool = True) -> SdtResult:
repo_root = resolve_repo_root(str(csproj.parent))
env = dotnet_env(repo_root)
args = ["publish", str(csproj), "-c", configuration, "-r", runtime, "--self-contained", "true" if self_contained else "false",
f"-p:PublishSingleFile={'true' if single_file else 'false'}", "-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false", "-o", str(output_dir)]
if single_file: args.append("-p:IncludeNativeLibrariesForSelfExtract=true")
res = run_step_with_summary(f"Publishing {csproj.name}", "dotnet", args, repo_root, env)
if res["exit_code"] != 0 and single_file:
comb = (res["stdout"] + res["stderr"]).lower()
if "generatebundle" in comb and "same bundlerelativepath" in comb:
print("Duplicate bundle entries detected; retrying without single-file optimization...")
retry = [a if "PublishSingleFile=true" not in a else "-p:PublishSingleFile=false" for a in args]
res = run_step_with_summary(f"Publishing {csproj.name} (retry)", "dotnet", retry, repo_root, env)
if res["exit_code"] != 0:
comb = (res["stdout"] + res["stderr"]).lower()
if "netsdk1152" in comb and "multiple publish output files with the same relative path" in comb:
print("Duplicate publish output files detected (NETSDK1152); retrying with ErrorOnDuplicatePublishOutputFiles=false...")
retry = list(args)
if not any("ErrorOnDuplicatePublishOutputFiles" in a for a in retry):
retry.append("-p:ErrorOnDuplicatePublishOutputFiles=false")
res = run_step_with_summary(f"Publishing {csproj.name} (dedupe retry)", "dotnet", retry, repo_root, env)
return res
def ensure_npm_build(app_root: pathlib.Path, target: str = "web", configuration: str = "Release", tauri_bundles: str = "none") -> SdtResult:
pj, lock = app_root / "package.json", first_existing([app_root / "package-lock.json", app_root / "npm-shrinkwrap.json"])
nm = app_root / "node_modules"
h_file, exp_h = nm / ".sdt-deps.sha256", sha256_files([pj, lock] if lock else [pj])
should_inst = not nm.exists() or not h_file.exists() or h_file.read_text(encoding="utf-8").strip() != exp_h
if should_inst:
args = ["ci", "--no-audit", "--fund=false"] if lock else ["install", "--no-audit", "--fund=false"]
res = run_step_with_summary("Installing NPM dependencies", "npm", args, app_root)
if res["exit_code"] != 0 and lock and args[0] == "ci":
res = run_step_with_summary("Installing NPM dependencies (retry)", "npm", ["install", "--no-audit", "--fund=false"], app_root)
if res["exit_code"] != 0: return res
ensure_dirs([nm]); h_file.write_text(exp_h, encoding="utf-8")
if target == "web": return run_step_with_summary(f"Building Web ({configuration})", "npm", ["run", "build"], app_root)
t_cmd = ["run", "tauri", "build"]
tail = (["--no-bundle"] if tauri_bundles == "none" else ["--bundles", tauri_bundles]) + (["--debug"] if configuration == "Debug" else [])
if tail: t_cmd.extend(["--", *tail])
return run_step_with_summary(f"Building Tauri ({configuration})", "npm", t_cmd, app_root)

View File

@ -4,7 +4,7 @@ import os
import shutil
from pathlib import Path
from script_common import newest_file, resolve_repo_root
from script_common import newest_file, resolve_repo_root # type: ignore
def copy_tree_contents(src: Path, dst: Path) -> None:
@ -21,55 +21,79 @@ def copy_tree_contents(src: Path, dst: Path) -> None:
def main() -> int:
parser = argparse.ArgumentParser(description="Sync newest built assets into output folder")
parser.add_argument("--repo-root", default=None)
parser.add_argument("--output-dir", default="output")
parser.add_argument("--web-build-dir", default=None, help="Path to web build output")
parser.add_argument("--sidecar-bin-dir", default=None, help="Path to sidecar bin root")
parser.add_argument("--gateway-bin-dir", default=None, help="Path to gateway bin root")
parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root")
_ = parser.add_argument("--repo-root", default=None)
_ = parser.add_argument("--output-dir", default="output")
_ = parser.add_argument("--web-build-dir", default=None, help="Path to web build output")
_ = parser.add_argument("--sidecar-bin-dir", default=None, help="Path to sidecar bin root")
_ = parser.add_argument("--gateway-bin-dir", default=None, help="Path to gateway bin root")
_ = parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root")
args = parser.parse_args()
repo_root = resolve_repo_root(args.repo_root)
output_dir = (repo_root / args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
web_build = (repo_root / args.web_build_dir).resolve() if args.web_build_dir else None
web_build_dir_val = args.web_build_dir
web_build = (repo_root / web_build_dir_val).resolve() if web_build_dir_val else None
if web_build is None:
web_build = next((p for p in repo_root.rglob("build") if (p.parent / "package.json").exists()), None)
if web_build is not None and web_build.exists():
web_out = output_dir / "webgateway" / "wwwroot"
copy_tree_contents(web_build, web_out)
print(f"Synced web assets -> {web_out}")
sidecar_bin = (repo_root / args.sidecar_bin_dir).resolve() if args.sidecar_bin_dir else None
sidecar_bin_dir_val = args.sidecar_bin_dir
sidecar_bin = (repo_root / sidecar_bin_dir_val).resolve() if sidecar_bin_dir_val else None
if sidecar_bin is None:
sidecar_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "sidecar" in str(p).lower()), None)
sidecar_bin = sidecar_proj / "bin" if sidecar_proj else None
if sidecar_bin is not None:
sidecar_pattern = "*.exe" if os.name == "nt" else "*"
sidecar_exe = newest_file(sidecar_bin, sidecar_pattern)
sidecar_exe = None
if os.name == "nt":
sidecar_exe = newest_file(sidecar_bin, "*.exe")
else:
candidates = [p for p in sidecar_bin.rglob("*") if p.is_file() and not p.suffix and os.access(p, os.X_OK)]
sidecar_exe = max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None
if sidecar_exe is not None:
copy_tree_contents(sidecar_exe.parent, output_dir)
print(f"Synced sidecar -> {output_dir}")
gateway_bin = (repo_root / args.gateway_bin_dir).resolve() if args.gateway_bin_dir else None
gateway_bin_dir_val = args.gateway_bin_dir
gateway_bin = (repo_root / gateway_bin_dir_val).resolve() if gateway_bin_dir_val else None
if gateway_bin is None:
gateway_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "gateway" in str(p).lower()), None)
gateway_bin = gateway_proj / "bin" if gateway_proj else None
if gateway_bin is not None:
gateway_pattern = "*.exe" if os.name == "nt" else "*"
gw_exe = newest_file(gateway_bin, gateway_pattern)
gw_exe = None
if os.name == "nt":
gw_exe = newest_file(gateway_bin, "*.exe")
else:
candidates = [p for p in gateway_bin.rglob("*") if p.is_file() and not p.suffix and os.access(p, os.X_OK)]
gw_exe = max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None
if gw_exe is not None:
gw_out = output_dir / "webgateway"
copy_tree_contents(gw_exe.parent, gw_out)
print(f"Synced gateway -> {gw_out}")
tauri_target = (repo_root / args.tauri_target_dir).resolve() if args.tauri_target_dir else None
tauri_target_dir_val = args.tauri_target_dir
tauri_target = (repo_root / tauri_target_dir_val).resolve() if tauri_target_dir_val else None
if tauri_target is None:
tauri_target = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None)
tauri_target = tauri_target / "target" if tauri_target else None
tauri_src = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None)
tauri_target = tauri_src / "target" if tauri_src else None
if tauri_target is not None:
app_exe = None
if os.name == "nt":
app_exe = newest_file(tauri_target, "*.exe")
else:
candidates = [p for p in tauri_target.rglob("*") if p.is_file() and not p.suffix and os.access(p, os.X_OK)]
app_exe = max(candidates, key=lambda p: p.stat().st_mtime) if candidates else None
if app_exe is not None:
shutil.copy2(app_exe, output_dir / app_exe.name)
print(f"Synced desktop app ({app_exe.name}) -> {output_dir}")

View File

@ -5,23 +5,23 @@ import pathlib
import shutil
import subprocess
import sys
from typing import Any, Dict, List, Optional, Sequence, Tuple
from typing import Any, Optional, Sequence
from script_common import resolve_command, resolve_repo_root
def load_config(project_root: pathlib.Path) -> dict:
config_path = project_root / "devtool.json"
def load_config(project_root: pathlib.Path) -> dict[str, Any]:
sdt_configs = list(project_root.glob("sdtconfig-*.json"))
config_path = sdt_configs[0] if sdt_configs else (project_root / "devtool.json")
if not config_path.exists():
raise FileNotFoundError(f"devtool.json not found at: {config_path}")
raise FileNotFoundError(f"Project config not found at: {config_path}")
return json.loads(config_path.read_text(encoding="utf-8"))
def iter_workflows(config: dict, selected: Optional[set[str]]) -> List[dict]:
def iter_workflows(config: dict[str, Any], selected: Optional[set[str]]) -> list[dict[str, Any]]:
workflows = config.get("workflows", [])
if not isinstance(workflows, list):
return []
normalized: List[dict] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)]
normalized: list[dict[str, Any]] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)]
if selected:
normalized = [w for w in normalized if w["id"] in selected]
return normalized
@ -45,21 +45,25 @@ def resolve_script_arg(project_root: pathlib.Path, working_dir: pathlib.Path, ar
return b
def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
result = {
def static_check_workflow(project_root: pathlib.Path, workflow: dict[str, Any]) -> dict[str, Any]:
result: dict[str, Any] = {
"workflowId": workflow.get("id"),
"ok": True,
"issues": [],
"steps": [],
}
for step in workflow.get("steps", []):
steps = workflow.get("steps", [])
if not isinstance(steps, list):
return result
for step in steps:
if not isinstance(step, dict):
continue
step_id = step.get("id", "<unknown>")
step_result = {"stepId": step_id, "ok": True, "issues": []}
step_result: dict[str, Any] = {"stepId": step_id, "ok": True, "issues": []}
working_dir_rel = step.get("workingDir") or "."
working_dir_rel = str(step.get("workingDir") or ".")
working_dir = (project_root / working_dir_rel).resolve()
if not working_dir.exists():
step_result["ok"] = False
@ -75,14 +79,13 @@ def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
step_result["issues"].append(f"command_not_found:{command}")
if command.lower() in ("python", "py", "python3", "python.exe", "py.exe"):
if args and isinstance(args[0], str) and args[0].endswith(".py"):
if isinstance(args, list) and args and isinstance(args[0], str) and args[0].endswith(".py"):
script_path = resolve_script_arg(project_root, working_dir, args[0])
if not script_path.exists():
step_result["ok"] = False
step_result["issues"].append(f"python_script_not_found:{script_path}")
if isinstance(action, str) and action.strip():
# Action-based steps still require workingDir existence for reliable execution.
if not working_dir.exists():
step_result["ok"] = False
step_result["issues"].append("action_working_dir_not_found")
@ -96,8 +99,8 @@ def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
return result
def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]:
attempts: List[List[str]] = []
def sdt_attempts(repo_root: pathlib.Path) -> list[list[str]]:
attempts: list[list[str]] = []
attempts.append(["sdt"])
if sys.platform.startswith("win"):
attempts.append(["sdt.exe"])
@ -110,9 +113,8 @@ def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]:
if devtool_csproj.exists():
attempts.append(["dotnet", "run", "--project", str(devtool_csproj), "--"])
# Preserve order but dedupe exact attempts.
seen = set()
unique: List[List[str]] = []
seen: set[tuple[str, ...]] = set()
unique: list[list[str]] = []
for a in attempts:
key = tuple(a)
if key in seen:
@ -126,8 +128,8 @@ def try_run_sdt(
repo_root: pathlib.Path,
command_args: Sequence[str],
timeout_seconds: int,
) -> Tuple[Optional[subprocess.CompletedProcess], Optional[str]]:
errors: List[str] = []
) -> tuple[Optional[subprocess.CompletedProcess[str]], Optional[str]]:
errors: list[str] = []
for base in sdt_attempts(repo_root):
cmd = [*base, *command_args]
try:
@ -147,7 +149,7 @@ def try_run_sdt(
return None, "; ".join(errors) if errors else "no_sdt_attempts"
def parse_headless_summary(stdout: str) -> Optional[Dict[str, Any]]:
def parse_headless_summary(stdout: str) -> Optional[dict[str, Any]]:
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
for line in reversed(lines):
if not line.startswith("{"):
@ -167,8 +169,8 @@ def execute_check_workflow(
workflow_id: str,
env_profile: Optional[str],
timeout_seconds: int,
) -> dict:
args = [
) -> dict[str, Any]:
run_args = [
"run",
workflow_id,
"--json",
@ -177,9 +179,9 @@ def execute_check_workflow(
"--non-interactive",
]
if env_profile:
args.extend(["--env-profile", env_profile])
run_args.extend(["--env-profile", env_profile])
proc, attempted = try_run_sdt(repo_root, args, timeout_seconds)
proc, attempted = try_run_sdt(repo_root, run_args, timeout_seconds)
if proc is None:
return {
"workflowId": workflow_id,
@ -215,13 +217,13 @@ def main() -> int:
parser = argparse.ArgumentParser(
description="Verify SDT workflow routes (static path checks + optional headless execution)."
)
parser.add_argument("--repo-root", default=None, help="Repo root containing DevTool.csproj")
parser.add_argument("--project-root", default=".", help="Project root containing devtool.json")
parser.add_argument("--workflow", action="append", default=[], help="Workflow id to verify (repeatable)")
parser.add_argument("--execute", action="store_true", help="Also run each workflow via `sdt run ... --json`")
parser.add_argument("--env-profile", default=None)
parser.add_argument("--timeout-seconds", type=int, default=600)
parser.add_argument("--output-json", default=None, help="Write full report JSON to file")
_ = parser.add_argument("--repo-root", default=None, help="Repo root containing DevTool.csproj")
_ = parser.add_argument("--project-root", default=".", help="Project root containing devtool.json")
_ = parser.add_argument("--workflow", action="append", default=[], help="Workflow id to verify (repeatable)")
_ = parser.add_argument("--execute", action="store_true", help="Also run each workflow via `sdt run ... --json`")
_ = parser.add_argument("--env-profile", default=None)
_ = parser.add_argument("--timeout-seconds", type=int, default=600)
_ = parser.add_argument("--output-json", default=None, help="Write full report JSON to file")
args = parser.parse_args()
repo_root = resolve_repo_root(args.repo_root)
@ -235,10 +237,10 @@ def main() -> int:
return 2
static_results = [static_check_workflow(project_root, w) for w in workflows]
execute_results: List[dict] = []
execute_results: list[dict[str, Any]] = []
if args.execute:
for w in workflows:
wid = w["id"]
wid = str(w["id"])
execute_results.append(
execute_check_workflow(
repo_root=repo_root,
@ -252,7 +254,7 @@ def main() -> int:
static_failures = [r for r in static_results if not r["ok"]]
exec_failures = [r for r in execute_results if not r["ok"]]
report = {
report: dict[str, Any] = {
"repoRoot": str(repo_root),
"projectRoot": str(project_root),
"totalWorkflows": len(workflows),
@ -283,13 +285,13 @@ def main() -> int:
if static_failures:
print("\nStatic failures:")
for f in static_failures:
print(f"- {f['workflowId']}: {', '.join(f['issues'])}")
for sf in static_failures:
print(f"- {sf['workflowId']}: {', '.join(sf['issues'])}")
if exec_failures:
print("\nExecution failures:")
for f in exec_failures:
print(f"- {f['workflowId']}: stopReason={f.get('stopReason')} message={f.get('message')}")
for ef in exec_failures:
print(f"- {ef['workflowId']}: stopReason={ef.get('stopReason')} message={ef.get('message')}")
return 1 if static_failures or exec_failures else 0

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -165,7 +165,7 @@ dotnet run --project Journal.SmokeTests
NuGet package versions are managed centrally in `Directory.Packages.props`. Project-level `.csproj` files reference packages without version numbers.
- `Journal.Core``Microsoft.Data.Sqlite.Core`, `SQLitePCLRaw.bundle_e_sqlcipher`, `Microsoft.Extensions.DependencyInjection.Abstractions`
- `Journal.AI``LLamaSharp`, `LLamaSharp.Backend.Cpu` + references `Journal.Core`
- `Journal.AI``LLamaSharp`, `LLamaSharp.Backend.Cpu`, `LLamaSharp.Backend.Vulkan` + references `Journal.Core`
- `Journal.Sidecar``Microsoft.Extensions.DependencyInjection`, `NAudio`, `Whisper.net` + references `Journal.Core`, `Journal.AI`
- `Journal.WebGateway``Microsoft.NET.Sdk.Web` + references `Journal.Core`, `Journal.AI`
- `Journal.SmokeTests` — references `Journal.Core`
@ -187,6 +187,7 @@ NuGet package versions are managed centrally in `Directory.Packages.props`. Proj
| `JOURNAL_VAULT_DIR` | `<root>/journal/vault` | Override vault directory path |
| `JOURNAL_DATA_DIR` | _(empty)_ | Override decrypted data directory path |
| `JOURNAL_AI_PROVIDER` | `none` | AI provider mode (`none`, `llamasharp`) |
| `JOURNAL_GPU_LAYERS` | `-1` (all) | Number of model layers to offload to GPU (`-1` = all, `0` = CPU only) |
| `JOURNAL_LOG_LEVEL` | `warning` | Log verbosity (`trace`, `debug`, `information`, `warning`, `error`, `critical`) |
| `JOURNAL_WEB_DIST` | auto | Override web UI dist path for WebGateway |
@ -199,6 +200,18 @@ NuGet package versions are managed centrally in `Directory.Packages.props`. Proj
---
## AI / LLM Notes
The `Journal.AI` project uses **LLamaSharp** for local LLM inference.
- **CPU backend** (`LLamaSharp.Backend.Cpu`) is always installed as a fallback.
- **Vulkan backend** (`LLamaSharp.Backend.Vulkan`) provides GPU acceleration for AMD, Intel, and NVIDIA GPUs. LLamaSharp picks the best available backend at runtime.
- All backend packages must share the **same version**. Currently pinned to **0.25.0** because `LLamaSharp.Backend.Vulkan` has not yet published a 0.26.0 release. Watch the [NuGet page](https://www.nuget.org/packages/LLamaSharp.Backend.Vulkan) and upgrade all three packages together when a new version ships.
- **Known issue**: on some machines the Vulkan backend falls back to CPU because the internal `vulkaninfo --summary` detection times out at 1 second. If you see CPU-only inference despite having a Vulkan-capable GPU, this is likely the cause. The LLamaSharp team has acknowledged the issue ([#930](https://github.com/SciSharp/LLamaSharp/issues/930)).
- Set `JOURNAL_GPU_LAYERS=-1` (the default) to offload all model layers to the GPU, or `0` to force CPU-only.
---
## Journal.WebGateway
An ASP.NET Core minimal API that wraps `Journal.Core` for browser use.

View File

@ -1,786 +0,0 @@
{
"name": "Project Journal",
"version": "0.1.0",
"targets": [],
"workflows": [
{
"id": "sidecar",
"label": "Publish Sidecar",
"description": "Build Journal.Sidecar as self-contained exe \u2192 output/",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "sidecar:run",
"label": "Publish Sidecar",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-sidecar.ps1",
"-Configuration",
"Release",
"-Runtime",
"win-x64"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "web",
"label": "Build Web UI",
"description": "Build SvelteKit bundle \u2192 Journal.App/build/",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "web:run",
"label": "Build Web UI",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-app.ps1",
"-Target",
"web"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "sync-output",
"label": "Sync Build Assets to Output",
"description": "Sweep repo for newest builds (Web/Sidecar/Gateway/Tauri) and copy them to the output dir",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "sync-output:run",
"label": "Sync Build Assets to Output",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/sync-output.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "webgateway",
"label": "Publish WebGateway",
"description": "Publish ASP.NET host with embedded web UI \u2192 output/webgateway/",
"group": "Build",
"dependsOn": [
"web"
],
"steps": [
{
"id": "webgateway:run",
"label": "Publish WebGateway",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-webgateway.ps1",
"-Configuration",
"Release",
"-Runtime",
"win-x64"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "tauri",
"label": "Build Tauri Desktop App",
"description": "Build desktop exe (no installer) \u2192 Journal.App/src-tauri/target/release/",
"group": "Build",
"dependsOn": [
"sidecar"
],
"steps": [
{
"id": "tauri:run",
"label": "Build Tauri Desktop App",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-app.ps1",
"-Target",
"tauri",
"-TauriBundles",
"none"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
},
{
"tool": "cargo",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "tauri-nsis",
"label": "Build Tauri \u002B NSIS Installer",
"description": "Build desktop exe with NSIS installer package",
"group": "Build",
"dependsOn": [
"sidecar"
],
"steps": [
{
"id": "tauri-nsis:run",
"label": "Build Tauri \u002B NSIS Installer",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-app.ps1",
"-Target",
"tauri",
"-TauriBundles",
"nsis"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
},
{
"tool": "cargo",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "build-dotnet",
"label": "Build .NET Projects",
"description": "dotnet build \u2014 all C# projects in solution",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "build-dotnet:run",
"label": "Build .NET Projects",
"command": "dotnet",
"args": [
"build"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "all",
"label": "Full Release Build \u2726",
"description": "Sidecar \u2192 Web \u2192 WebGateway \u2192 Tauri, in dependency order",
"group": "Build",
"dependsOn": [
"sidecar",
"web",
"webgateway",
"tauri"
],
"steps": []
},
{
"id": "run-gateway-dev",
"label": "Run WebGateway Server (Dev)",
"description": "Start HTTP gateway via \u0027dotnet run\u0027 at http://localhost:5180",
"group": "Dev",
"dependsOn": [],
"steps": [
{
"id": "run-gateway-dev:run",
"label": "Run WebGateway Server (Dev)",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/run-webgateway.ps1",
"-Mode",
"Dev"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "run-gateway-prod",
"label": "Run WebGateway Server (Output)",
"description": "Start compiled gateway from output/webgateway at http://localhost:5180",
"group": "Dev",
"dependsOn": [],
"steps": [
{
"id": "run-gateway-prod:run",
"label": "Run WebGateway Server (Output)",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/run-webgateway.ps1",
"-Mode",
"Output"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "test",
"label": "Run Smoke Tests",
"description": "Run all ~80 integration tests in Journal.SmokeTests",
"group": "Test",
"dependsOn": [],
"steps": [
{
"id": "test:run",
"label": "Run Smoke Tests",
"command": "dotnet",
"args": [
"run",
"--project",
"Journal.SmokeTests/Journal.SmokeTests.csproj"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "gate",
"label": "Run Migration Gate",
"description": "Full build \u002B smoke tests \u002B parity check",
"group": "Test",
"dependsOn": [],
"steps": [
{
"id": "gate:run",
"label": "Run Migration Gate",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/migration-gate.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "nuget-export",
"label": "Export NuGet Cache",
"description": "Prime and export .nuget cache to zip for offline use",
"group": "Cache",
"dependsOn": [],
"steps": [
{
"id": "nuget-export:run",
"label": "Export NuGet Cache",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/nuget-export-cache.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "nuget-import",
"label": "Import NuGet Cache",
"description": "Import cache zip and validate restore",
"group": "Cache",
"dependsOn": [],
"steps": [
{
"id": "nuget-import:run",
"label": "Import NuGet Cache",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/nuget-import-cache.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "npm-clean",
"label": "Clean Node Modules",
"description": "Remove Journal.App node_modules (kills node/tauri first)",
"group": "System",
"dependsOn": [],
"steps": [
{
"id": "npm-clean:run",
"label": "Clean Node Modules",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/npm-clean.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "stage-output",
"label": "Stage Output Bundle",
"description": "Publish sidecar \u002B web \u002B webgateway \u002B tauri, then stage journalapp.exe into output/",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "stage-output:run",
"label": "Stage Output Bundle",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-output.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
},
{
"tool": "cargo",
"installPolicy": "Prompt"
}
]
}
]
}
],
"env": [
{
"key": "JOURNAL_AI_PROVIDER",
"description": "AI provider bridge mode",
"default": "none",
"options": [
"none",
"python-sidecar"
]
},
{
"key": "JOURNAL_LOG_LEVEL",
"description": "Log verbosity for C# backend",
"default": "warning",
"options": [
"trace",
"debug",
"information",
"warning",
"error",
"critical"
]
},
{
"key": "JOURNAL_NLP_BACKEND",
"description": "Python NLP backend selection",
"default": "auto",
"options": [
"auto",
"spacy",
"fallback"
]
},
{
"key": "JOURNAL_PROJECT_ROOT",
"description": "Override project root path (blank = auto-detect)",
"default": "",
"options": []
},
{
"key": "JOURNAL_VAULT_DIR",
"description": "Override vault directory path",
"default": "",
"options": []
},
{
"key": "JOURNAL_DATA_DIR",
"description": "Override decrypted data directory path",
"default": "",
"options": []
},
{
"key": "SDT_ENV_PROFILE",
"description": "Active SDT runtime environment profile",
"default": "dev",
"options": [
"dev",
"ci",
"release"
]
},
{
"key": "SDT_LOG_LEVEL",
"description": "CLI log verbosity",
"default": "information",
"options": [
"trace",
"debug",
"information",
"warning",
"error",
"critical"
]
}
],
"envProfiles": {
"active": "dev",
"profiles": [
{
"id": "dev",
"description": "Local development defaults",
"inherits": [],
"values": {
"SDT_ENV_PROFILE": "dev",
"SDT_LOG_LEVEL": "information"
}
},
{
"id": "ci",
"description": "Continuous integration defaults",
"inherits": [
"dev"
],
"values": {
"SDT_ENV_PROFILE": "ci",
"CI": "false",
"SDT_LOG_LEVEL": "warning"
}
},
{
"id": "release",
"description": "Release build defaults",
"inherits": [
"dev"
],
"values": {
"SDT_ENV_PROFILE": "release",
"SDT_LOG_LEVEL": "warning"
}
}
]
},
"toolchains": {
"python": {
"executable": "python3.14",
"windowsExecutable": "py",
"launcherVersion": "-3.14",
"venvDir": ".venv",
"profiles": [
{
"id": "cpu",
"label": "CPU only (default)",
"requirementsFile": "requirements_cpu_only.txt",
"extraIndexUrl": "https://download.pytorch.org/whl/cpu",
"postInstallCommands": []
},
{
"id": "gpu",
"label": "GPU / CUDA",
"requirementsFile": "requirements_gpu.txt",
"extraIndexUrl": null,
"postInstallCommands": []
},
{
"id": "nlp",
"label": "NLP / spaCy (optional)",
"requirementsFile": "requirements_nlp_optional.txt",
"extraIndexUrl": null,
"postInstallCommands": [
"spacy download en_core_web_sm"
]
}
],
"pipScript": "scripts/pip-min.ps1"
},
"node": {
"packageManager": "npm",
"workingDir": "Journal.App"
}
},
"tooling": {
"tools": [
{
"tool": "cargo",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "dotnet",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "node",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "npm",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "python",
"preferredInstallCommands": [],
"executables": []
}
]
},
"project": null,
"debug": {
"profiles": [],
"diagnostics": {
"enabled": true,
"outputDir": ".sdt/debug",
"includeAllEnv": false,
"captureEnvKeys": [],
"redactSensitive": true,
"sensitiveKeyPatterns": [
"TOKEN",
"SECRET",
"PASSWORD",
"PWD",
"CREDENTIAL",
"API_KEY",
"ACCESS_KEY",
"PRIVATE_KEY"
],
"redactionAllowKeys": [],
"bundleOnFailure": true
}
}
}

View File

@ -1,677 +0,0 @@
{
"name": "Project Journal",
"version": "0.1.0",
"targets": [],
"workflows": [
{
"id": "sidecar",
"label": "Publish Sidecar",
"description": "Build Journal.Sidecar as self-contained exe \u2192 output/",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "sidecar:run",
"label": "Publish Sidecar",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-sidecar.ps1",
"-Configuration",
"Release",
"-Runtime",
"win-x64"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "web",
"label": "Build Web UI",
"description": "Build SvelteKit bundle \u2192 Journal.App/build/",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "web:run",
"label": "Build Web UI",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-app.ps1",
"-Target",
"web"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "sync-output",
"label": "Sync Build Assets to Output",
"description": "Sweep repo for newest builds (Web/Sidecar/Gateway/Tauri) and copy them to the output dir",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "sync-output:run",
"label": "Sync Build Assets to Output",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/sync-output.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "webgateway",
"label": "Publish WebGateway",
"description": "Publish ASP.NET host with embedded web UI \u2192 output/webgateway/",
"group": "Build",
"dependsOn": [
"web"
],
"steps": [
{
"id": "webgateway:run",
"label": "Publish WebGateway",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-webgateway.ps1",
"-Configuration",
"Release",
"-Runtime",
"win-x64"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "tauri",
"label": "Build Tauri Desktop App",
"description": "Build desktop exe (no installer) \u2192 Journal.App/src-tauri/target/release/",
"group": "Build",
"dependsOn": [
"sidecar"
],
"steps": [
{
"id": "tauri:run",
"label": "Build Tauri Desktop App",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-app.ps1",
"-Target",
"tauri",
"-TauriBundles",
"none"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
},
{
"tool": "cargo",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "tauri-nsis",
"label": "Build Tauri \u002B NSIS Installer",
"description": "Build desktop exe with NSIS installer package",
"group": "Build",
"dependsOn": [
"sidecar"
],
"steps": [
{
"id": "tauri-nsis:run",
"label": "Build Tauri \u002B NSIS Installer",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-app.ps1",
"-Target",
"tauri",
"-TauriBundles",
"nsis"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
},
{
"tool": "cargo",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "build-dotnet",
"label": "Build .NET Projects",
"description": "dotnet build \u2014 all C# projects in solution",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "build-dotnet:run",
"label": "Build .NET Projects",
"command": "dotnet",
"args": [
"build"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "all",
"label": "Full Release Build \u2726",
"description": "Sidecar \u2192 Web \u2192 WebGateway \u2192 Tauri, in dependency order",
"group": "Build",
"dependsOn": [
"sidecar",
"web",
"webgateway",
"tauri"
],
"steps": []
},
{
"id": "run-gateway-dev",
"label": "Run WebGateway Server (Dev)",
"description": "Start HTTP gateway via \u0027dotnet run\u0027 at http://localhost:5180",
"group": "Dev",
"dependsOn": [],
"steps": [
{
"id": "run-gateway-dev:run",
"label": "Run WebGateway Server (Dev)",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/run-webgateway.ps1",
"-Mode",
"Dev"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "run-gateway-prod",
"label": "Run WebGateway Server (Output)",
"description": "Start compiled gateway from output/webgateway at http://localhost:5180",
"group": "Dev",
"dependsOn": [],
"steps": [
{
"id": "run-gateway-prod:run",
"label": "Run WebGateway Server (Output)",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/run-webgateway.ps1",
"-Mode",
"Output"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "test",
"label": "Run Smoke Tests",
"description": "Run all ~80 integration tests in Journal.SmokeTests",
"group": "Test",
"dependsOn": [],
"steps": [
{
"id": "test:run",
"label": "Run Smoke Tests",
"command": "dotnet",
"args": [
"run",
"--project",
"Journal.SmokeTests/Journal.SmokeTests.csproj"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "gate",
"label": "Run Migration Gate",
"description": "Full build \u002B smoke tests \u002B parity check",
"group": "Test",
"dependsOn": [],
"steps": [
{
"id": "gate:run",
"label": "Run Migration Gate",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/migration-gate.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "nuget-export",
"label": "Export NuGet Cache",
"description": "Prime and export .nuget cache to zip for offline use",
"group": "Cache",
"dependsOn": [],
"steps": [
{
"id": "nuget-export:run",
"label": "Export NuGet Cache",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/nuget-export-cache.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "nuget-import",
"label": "Import NuGet Cache",
"description": "Import cache zip and validate restore",
"group": "Cache",
"dependsOn": [],
"steps": [
{
"id": "nuget-import:run",
"label": "Import NuGet Cache",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/nuget-import-cache.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "npm-clean",
"label": "Clean Node Modules",
"description": "Remove Journal.App node_modules (kills node/tauri first)",
"group": "System",
"dependsOn": [],
"steps": [
{
"id": "npm-clean:run",
"label": "Clean Node Modules",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/npm-clean.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "stage-output",
"label": "Stage Output Bundle",
"description": "Publish sidecar \u002B web \u002B webgateway \u002B tauri, then stage journalapp.exe into output/",
"group": "Build",
"dependsOn": [],
"steps": [
{
"id": "stage-output:run",
"label": "Stage Output Bundle",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/publish-output.ps1"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
},
{
"tool": "dotnet",
"installPolicy": "Prompt"
},
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
},
{
"tool": "cargo",
"installPolicy": "Prompt"
}
]
}
]
}
],
"env": [
{
"key": "JOURNAL_AI_PROVIDER",
"description": "AI provider bridge mode",
"default": "none",
"options": [
"none",
"python-sidecar"
]
},
{
"key": "JOURNAL_LOG_LEVEL",
"description": "Log verbosity for C# backend",
"default": "warning",
"options": [
"trace",
"debug",
"information",
"warning",
"error",
"critical"
]
},
{
"key": "JOURNAL_NLP_BACKEND",
"description": "Python NLP backend selection",
"default": "auto",
"options": [
"auto",
"spacy",
"fallback"
]
},
{
"key": "JOURNAL_PROJECT_ROOT",
"description": "Override project root path (blank = auto-detect)",
"default": "",
"options": []
},
{
"key": "JOURNAL_VAULT_DIR",
"description": "Override vault directory path",
"default": "",
"options": []
},
{
"key": "JOURNAL_DATA_DIR",
"description": "Override decrypted data directory path",
"default": "",
"options": []
}
],
"toolchains": {
"python": {
"executable": "python3.14",
"windowsExecutable": "py",
"launcherVersion": "-3.14",
"venvDir": ".venv",
"profiles": [
{
"id": "cpu",
"label": "CPU only (default)",
"requirementsFile": "requirements_cpu_only.txt",
"extraIndexUrl": "https://download.pytorch.org/whl/cpu",
"postInstallCommands": []
},
{
"id": "gpu",
"label": "GPU / CUDA",
"requirementsFile": "requirements_gpu.txt",
"extraIndexUrl": null,
"postInstallCommands": []
},
{
"id": "nlp",
"label": "NLP / spaCy (optional)",
"requirementsFile": "requirements_nlp_optional.txt",
"extraIndexUrl": null,
"postInstallCommands": [
"spacy download en_core_web_sm"
]
}
],
"pipScript": "scripts/pip-min.ps1"
},
"node": {
"packageManager": "npm",
"workingDir": "Journal.App"
}
},
"tooling": null,
"project": null,
"debug": null
}

558
sdtconfig-journal.json Normal file
View File

@ -0,0 +1,558 @@
{
"name": "journal",
"version": "0.1.0",
"targets": [],
"workflows": [
{
"id": "sidecar",
"label": "Publish Sidecar",
"description": "Publish sidecar service",
"group": "Build",
"dependsOn": [],
"requireFiles": [
"Journal.DevTool\\scripts\\publish-sidecar.py"
],
"steps": [
{
"id": "sidecar:run",
"label": "python Journal.DevTool\\scripts\\publish-sidecar.py",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\publish-sidecar.py"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "web",
"label": "Build Web UI",
"description": "Build frontend assets",
"group": "Build",
"dependsOn": [],
"requireFiles": [
"Journal.DevTool\\scripts\\publish-app.py"
],
"steps": [
{
"id": "web:run",
"label": "python Journal.DevTool\\scripts\\publish-app.py --target web",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\publish-app.py",
"--target",
"web"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "tauri",
"label": "Build Tauri Desktop App",
"description": "Build desktop binary",
"group": "Build",
"dependsOn": [
"sidecar"
],
"requireFiles": [
"Journal.DevTool\\scripts\\publish-app.py",
"Journal.App/src-tauri/tauri.conf.json"
],
"steps": [
{
"id": "tauri:run",
"label": "python Journal.DevTool\\scripts\\publish-app.py --target tauri --tauri-bundles none",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\publish-app.py",
"--target",
"tauri",
"--tauri-bundles",
"none"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "webgateway",
"label": "Publish WebGateway",
"description": "Publish ASP.NET gateway",
"group": "Build",
"dependsOn": [
"web"
],
"requireFiles": [
"Journal.DevTool\\scripts\\publish-webgateway.py"
],
"steps": [
{
"id": "webgateway:run",
"label": "python Journal.DevTool\\scripts\\publish-webgateway.py",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\publish-webgateway.py"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "sync-output",
"label": "Sync Output",
"description": "Sync newest artifacts to output",
"group": "Build",
"dependsOn": [],
"requireFiles": [
"Journal.DevTool\\scripts\\sync-output.py"
],
"steps": [
{
"id": "sync-output:run",
"label": "python Journal.DevTool\\scripts\\sync-output.py",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\sync-output.py"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "stage-output",
"label": "Stage Output Bundle",
"description": "Publish and stage distributable output",
"group": "Build",
"dependsOn": [],
"requireFiles": [
"Journal.DevTool\\scripts\\publish-output.py"
],
"steps": [
{
"id": "stage-output:run",
"label": "python Journal.DevTool\\scripts\\publish-output.py",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\publish-output.py"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "run-gateway-dev",
"label": "Run WebGateway Server (Dev)",
"description": "Run gateway in development mode",
"group": "Dev",
"dependsOn": [],
"requireFiles": [
"Journal.DevTool\\scripts\\run-webgateway.py"
],
"steps": [
{
"id": "run-gateway-dev:run",
"label": "python Journal.DevTool\\scripts\\run-webgateway.py --mode Dev",
"command": "python",
"args": [
"Journal.DevTool\\scripts\\run-webgateway.py",
"--mode",
"Dev"
],
"workingDir": ".",
"action": null,
"actionArgs": [],
"requires": [
{
"tool": "python",
"installPolicy": "Prompt"
}
]
}
]
},
{
"id": "build",
"label": "Build",
"description": "Build detected project stacks",
"group": "Build",
"dependsOn": [],
"requireFiles": [],
"steps": [
{
"id": "dotnet-build",
"label": "dotnet build",
"command": null,
"args": [],
"workingDir": ".",
"action": "dotnet-build",
"actionArgs": [],
"requires": []
},
{
"id": "npm-build",
"label": "npm run build",
"command": null,
"args": [],
"workingDir": "Journal.App",
"action": "npm-build",
"actionArgs": [],
"requires": []
},
{
"id": "cargo-build",
"label": "cargo build",
"command": null,
"args": [],
"workingDir": ".",
"action": "cargo-build",
"actionArgs": [],
"requires": []
},
{
"id": "tauri-build",
"label": "tauri build",
"command": null,
"args": [],
"workingDir": "Journal.App",
"action": "tauri-build",
"actionArgs": [],
"requires": []
}
]
},
{
"id": "deps-refresh",
"label": "Refresh Dependencies",
"description": "Restore/install dependency stacks",
"group": "Deps",
"dependsOn": [],
"requireFiles": [],
"steps": [
{
"id": "dotnet-restore",
"label": "dotnet restore",
"command": null,
"args": [],
"workingDir": ".",
"action": "dotnet-restore",
"actionArgs": [],
"requires": []
},
{
"id": "npm-ci",
"label": "npm ci",
"command": null,
"args": [],
"workingDir": "Journal.App",
"action": "npm-ci",
"actionArgs": [],
"requires": []
}
]
},
{
"id": "test",
"label": "Run Tests",
"description": "Run detected test stacks",
"group": "Test",
"dependsOn": [],
"requireFiles": [],
"steps": [
{
"id": "dotnet-test",
"label": "dotnet test",
"command": null,
"args": [],
"workingDir": ".",
"action": "dotnet-test",
"actionArgs": [],
"requires": []
},
{
"id": "npm-test",
"label": "npm test",
"command": null,
"args": [],
"workingDir": "Journal.App",
"action": "npm-test",
"actionArgs": [],
"requires": []
},
{
"id": "cargo-test",
"label": "cargo test",
"command": null,
"args": [],
"workingDir": ".",
"action": "cargo-test",
"actionArgs": [],
"requires": []
}
]
},
{
"id": "repo-health",
"label": "Repo Health",
"description": "Check repo status and fetch remotes",
"group": "Repo",
"dependsOn": [],
"requireFiles": [],
"steps": [
{
"id": "git-status",
"label": "git status",
"command": null,
"args": [],
"workingDir": ".",
"action": "git-status",
"actionArgs": [],
"requires": []
},
{
"id": "git-fetch",
"label": "git fetch",
"command": null,
"args": [],
"workingDir": ".",
"action": "git-fetch",
"actionArgs": [],
"requires": []
}
]
}
],
"env": [
{
"key": "SDT_LOG_LEVEL",
"description": "CLI log verbosity",
"default": "information",
"options": [
"trace",
"debug",
"information",
"warning",
"error",
"critical"
]
}
],
"envProfiles": {
"active": "dev",
"profiles": [
{
"id": "dev",
"description": "Local development defaults",
"inherits": [],
"values": {
"SDT_ENV_PROFILE": "dev",
"SDT_LOG_LEVEL": "information"
}
},
{
"id": "ci",
"description": "Continuous integration defaults",
"inherits": [
"dev"
],
"values": {
"SDT_ENV_PROFILE": "ci",
"CI": "true",
"SDT_LOG_LEVEL": "warning"
}
},
{
"id": "release",
"description": "Release build defaults",
"inherits": [
"dev"
],
"values": {
"SDT_ENV_PROFILE": "release",
"SDT_LOG_LEVEL": "warning"
}
}
]
},
"toolchains": {
"python": null,
"node": {
"packageManager": "npm",
"workingDir": "Journal.App"
}
},
"tooling": {
"defaultInstallPolicy": "Prompt",
"tools": [
{
"tool": "cargo",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "dotnet",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "git",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "node",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "npm",
"preferredInstallCommands": [],
"executables": []
},
{
"tool": "tauri",
"preferredInstallCommands": [],
"executables": []
}
]
},
"project": {
"type": "tauri",
"rootHints": [
"*.sln",
".git",
"Cargo.toml",
"package.json",
"tauri.conf.json"
],
"artifacts": [
"bin",
"obj",
".sdt/debug"
]
},
"debug": {
"profiles": [
{
"id": "dotnet-run",
"label": "Run .NET app",
"type": "dotnet",
"command": "dotnet",
"args": [
"run"
],
"workingDir": ".",
"env": {},
"requires": [
{
"tool": "dotnet",
"installPolicy": "Prompt"
}
],
"attach": {
"kind": "manual",
"port": null,
"processName": null,
"note": "Attach your IDE debugger to the running dotnet process."
}
},
{
"id": "npm-dev",
"label": "Run npm dev server",
"type": "node",
"command": "npm",
"args": [
"run",
"dev"
],
"workingDir": "Journal.App",
"env": {},
"requires": [
{
"tool": "node",
"installPolicy": "Prompt"
},
{
"tool": "npm",
"installPolicy": "Prompt"
}
],
"attach": null
}
],
"diagnostics": {
"enabled": true,
"outputDir": ".sdt/debug",
"includeAllEnv": false,
"captureEnvKeys": [
"SDT_LOG_LEVEL",
"DOTNET_CLI_HOME",
"NUGET_PACKAGES",
"PIP_CACHE_DIR",
"NVM_HOME",
"NVM_SYMLINK"
],
"redactSensitive": true,
"sensitiveKeyPatterns": [
"TOKEN",
"SECRET",
"PASSWORD",
"PWD",
"CREDENTIAL",
"API_KEY",
"ACCESS_KEY",
"PRIVATE_KEY"
],
"redactionAllowKeys": [],
"bundleOnFailure": true
}
}
}