Compare commits
No commits in common. "fe09c70e7556abee06c5f6b62561529624277ffb" and "ae70fbdae9a257ae1a8efdbccb75102d49b13c70" have entirely different histories.
fe09c70e75
...
ae70fbdae9
8
.gitignore
vendored
8
.gitignore
vendored
@ -56,4 +56,10 @@ journalapp(1).exe
|
||||
.cache/
|
||||
scripts/__pycache__/
|
||||
.sdt/
|
||||
devtool.backup.json
|
||||
devtool.backup.json
|
||||
sdt.deps.json
|
||||
sdt.dll
|
||||
sdt.exe
|
||||
sdt.pdb
|
||||
sdt.runtimeconfig.json
|
||||
Spectre.Console.dll
|
||||
|
||||
0
Journal.DevTool/.gitkeep
Normal file
0
Journal.DevTool/.gitkeep
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,283 +0,0 @@
|
||||
# SDT (Stan's Dev Tools)
|
||||
|
||||
Cross-platform terminal orchestrator for project workflows, toolchain checks, and prerequisite gating.
|
||||
|
||||
## Current State
|
||||
|
||||
- Standalone `.NET` TUI app (`net10.0`)
|
||||
- Domain-separated source projects under `src/`:
|
||||
- `DevTool.Engine` (workflow/config/orchestration services)
|
||||
- `DevTool.Runtime` (process/command execution primitives)
|
||||
- `DevTool.Host.Tui` (terminal UI host surface)
|
||||
- Workflow-first config model in `devtool.json`
|
||||
- Strict-by-default legacy migration (`targets`-only configs fail unless compat mode is enabled)
|
||||
- Python-first diagnostics/build script layer under `scripts/`
|
||||
- Fail-fast execution with install prompt gating for missing prerequisites
|
||||
- Debug profiles with attach metadata and diagnostics bundle generation
|
||||
- Workspace-first project switching with support for external project paths
|
||||
- Workspace-level defaults layering via `sdt-defaults.json` (ancestor defaults merged, project config wins)
|
||||
- Project status tracking is maintained in `ROADMAP.md`
|
||||
- Core run-event stream (`RunEvent`) shared by workflow + debug execution (TUI consumes it; GUI-ready)
|
||||
- Run events are persisted to JSONL at `.sdt/events/` for external tooling/GUI consumers
|
||||
- Run events now include versioned contract fields: `run_event_version`, `run_id`, `project_root`, `env_profile`, `timestamp_utc`, `event_type`
|
||||
- TUI includes `SYSTEM -> View run events` to inspect persisted JSONL event logs
|
||||
- `SYSTEM -> Run config doctor` can apply common autofixes (missing working dirs, legacy migration)
|
||||
- `SYSTEM -> Keybinding help` provides normalized cross-platform shortcut guidance
|
||||
- `SYSTEM -> Run history` supports rerun from prior execution context
|
||||
- First-run projects are prompted to run a setup wizard (doctor + autofix + optional toolchain setup)
|
||||
- Toolchain management now includes toolchain doctor + auto-fix flow with installer prompts and post-install verification
|
||||
- Env profiles (`envProfiles`) support deterministic inheritance (`dev`/`ci`/`release`) and runtime profile selection from `SYSTEM -> Select env profile`
|
||||
- Diagnostics bundles include managed secret redaction policy (env-key pattern redaction + output token redaction)
|
||||
- Workspace quick actions/favorites can run workflows across projects (auto switch-and-run)
|
||||
- Quick-action pinning is supported from workflow run results and events viewer
|
||||
- Bootstrap detects additional project stacks (`go`, `maven`, `gradle`) and sets `project.type` (`dotnet`, `node`, `python`, `rust`, `go`, `java`, `tauri`, `polyglot`, `generic`)
|
||||
- Headless execution mode is available for workflow/debug automation with JSON output
|
||||
- Terminal capability fallback modes supported via `NO_COLOR`/`SDT_NO_COLOR` and `SDT_NO_UNICODE`
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
dotnet run --project DevTool.csproj
|
||||
```
|
||||
|
||||
Run from any subdirectory inside a project; SDT walks up to find `devtool.json`.
|
||||
|
||||
If `devtool.json` is missing, SDT now offers to scan the repo and generate a default config.
|
||||
|
||||
Explicit bootstrap command:
|
||||
|
||||
```powershell
|
||||
dotnet run --project DevTool.csproj -- init
|
||||
```
|
||||
|
||||
Headless workflow/debug commands:
|
||||
|
||||
```powershell
|
||||
sdt run <workflowId> --json [--project-root <path>] [--env-profile <id>] [--non-interactive]
|
||||
sdt debug <profileId> --json [--project-root <path>] [--env-profile <id>] [--non-interactive]
|
||||
```
|
||||
|
||||
GUI bridge read/manage command:
|
||||
|
||||
```powershell
|
||||
sdt bridge --stdio [--project-root <path>]
|
||||
```
|
||||
|
||||
Workspace inventory scan (GUI/TUI shared discovery contract):
|
||||
|
||||
```powershell
|
||||
sdt workspace scan --json [--project-root <path>]
|
||||
```
|
||||
|
||||
`SDT_NONINTERACTIVE=1` globally enables non-interactive behavior for install prompts.
|
||||
|
||||
Bootstrap detects common stacks (`dotnet`, `npm/node`, `python`, `cargo/tauri`, `go`, `maven/gradle`, `git`, `docker`) and generates:
|
||||
|
||||
- default workflows
|
||||
- toolchain/tooling defaults
|
||||
- debug profiles + diagnostics defaults
|
||||
|
||||
## Config Model
|
||||
|
||||
SDT supports both:
|
||||
|
||||
- `workflows` (preferred)
|
||||
- `targets` (legacy; compat mode only)
|
||||
|
||||
### Legacy Migration Mode (v1.2)
|
||||
|
||||
- Default: strict mode
|
||||
- Behavior: `targets`-only config fails early with migration instructions
|
||||
- Preview file: SDT writes `devtool.generated.workflows.json` for migration help
|
||||
- Temporary rollback: set `SDT_LEGACY_MODE=compat`
|
||||
|
||||
Permanent fix (recommended):
|
||||
|
||||
1. Open `devtool.generated.workflows.json`
|
||||
2. Copy its `workflows` into `devtool.json`
|
||||
3. Remove or empty legacy `targets`
|
||||
4. Run `sdt.exe` again in strict mode
|
||||
|
||||
### Workflow shape (preferred)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "build",
|
||||
"label": "Build",
|
||||
"description": "Build project",
|
||||
"group": "Build",
|
||||
"dependsOn": [],
|
||||
"steps": [
|
||||
{
|
||||
"id": "dotnet-build",
|
||||
"label": "dotnet build",
|
||||
"action": "dotnet-build",
|
||||
"actionArgs": [],
|
||||
"workingDir": ".",
|
||||
"requires": [
|
||||
{ "tool": "dotnet", "installPolicy": "Prompt" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Extra sections
|
||||
|
||||
- `tooling.tools[].preferredInstallCommands`: preferred install commands per tool
|
||||
- `tooling.tools[].executables`: explicit executable candidates for non-standard PATH setups
|
||||
- `project.rootHints`: files/folders that identify project root
|
||||
- `env`: session-level environment variable editor values
|
||||
- `debug.profiles[]`: run/attach debug profiles
|
||||
- `debug.diagnostics`: diagnostics bundle policy (`.sdt/debug` by default)
|
||||
- secure default: allowlist-only environment capture
|
||||
- set `includeAllEnv=true` to opt into full environment capture
|
||||
|
||||
### Workspace Defaults Layering
|
||||
|
||||
If SDT finds `sdt-defaults.json` in the project directory tree (current project root or an ancestor), it merges it into the effective config before runtime:
|
||||
|
||||
- base layer: `sdt-defaults.json`
|
||||
- override layer: project `devtool.json` (project values win)
|
||||
|
||||
Merge behavior:
|
||||
|
||||
- objects merge recursively
|
||||
- arrays/scalars are replaced when project provides the property
|
||||
|
||||
This is useful for shared defaults like toolchains, diagnostics policies, and baseline env definitions across multiple projects in one workspace.
|
||||
|
||||
## Execution Behavior
|
||||
|
||||
For each workflow step:
|
||||
|
||||
1. Resolve dependencies (topological order)
|
||||
2. Probe required tools
|
||||
3. If missing, show install commands and prompt (`Prompt` policy)
|
||||
4. On decline/install failure/step failure, stop immediately
|
||||
5. Render step summary table with exit code + elapsed time
|
||||
6. On workflow/debug failure, generate diagnostics bundle when enabled
|
||||
|
||||
Installer command precedence:
|
||||
|
||||
1. `tooling.tools[].preferredInstallCommands`
|
||||
2. `scripts/diag.py install-plan`
|
||||
3. built-in C# fallback templates (used automatically if script planning fails)
|
||||
|
||||
When a tool probe fails, SDT now prints probe diagnostics (including command resolution source/path) in run output before prompting for installs.
|
||||
|
||||
Headless exit code contract:
|
||||
|
||||
- `0` success
|
||||
- `10` missing prerequisite
|
||||
- `11` install failed
|
||||
- `12` command failed
|
||||
- `13` validation/config error
|
||||
- `14` user-declined / non-interactive prompt refusal
|
||||
|
||||
## Scripts
|
||||
|
||||
See [scripts/README.md](/e:/stansshit/csharp/DevTool-master/scripts/README.md).
|
||||
|
||||
Primary Python entrypoints:
|
||||
|
||||
- `scripts/diag.py`
|
||||
- `scripts/build.py`
|
||||
- `scripts/dotnet-min.py`
|
||||
- `scripts/pip-min.py`
|
||||
- `scripts/publish-*.py`
|
||||
|
||||
## Workspace Support
|
||||
|
||||
- Uses `sdt-workspace.json` when present
|
||||
- If missing, can auto-discover nearby projects containing `devtool.json`
|
||||
- Workspace screen can add external project roots (absolute paths supported)
|
||||
- `projects[].disabled`, `projects[].tags`, and `projects[].toolFamilies` are supported
|
||||
- Hybrid inventory model discovers marker-only candidates (`.slnx/.sln/.csproj`) without silently mutating workspace config
|
||||
- TUI workspace screen supports:
|
||||
- `Add candidate`
|
||||
- `Add + initialize devtool.json`
|
||||
- `Ignore for now` (session-only)
|
||||
- Inventory snapshot is cached at `.sdt/workspace-inventory.json` for GUI-readiness
|
||||
|
||||
## GUI Direction
|
||||
|
||||
- Planned GUI stack for current phase: **Tauri-first**
|
||||
- Avalonia is deferred for re-evaluation after first GUI milestone ships on top of the same headless contracts
|
||||
- GUI workspace scaffold: [TauriShell README](/e:/stansshit/csharp/DevTool-master/src/DevTool.Host.Gui/TauriShell/README.md)
|
||||
- Hybrid GUI bridge is active:
|
||||
- execution: `sdt run/debug --json`
|
||||
- read/manage: `sdt bridge --stdio`
|
||||
- Bridge contract doc: [gui-bridge-contract.md](/e:/stansshit/csharp/DevTool-master/docs/gui-bridge-contract.md)
|
||||
- Parity manifest: [gui-tui-parity.json](/e:/stansshit/csharp/DevTool-master/docs/gui-tui-parity.json)
|
||||
- GUI will consume:
|
||||
- `sdt workspace scan --json` inventory payload
|
||||
- `run/debug --json` summaries
|
||||
- persisted run events from `.sdt/events/*.jsonl`
|
||||
|
||||
## Dev Shell Bootstrap
|
||||
|
||||
Python-first cross-shell dev environment bootstrap:
|
||||
|
||||
```powershell
|
||||
# PowerShell
|
||||
. ./scripts/dev-shell.ps1
|
||||
|
||||
# cmd
|
||||
scripts\dev-shell.cmd
|
||||
```
|
||||
|
||||
```bash
|
||||
# bash/zsh
|
||||
source ./scripts/dev-shell.sh
|
||||
```
|
||||
|
||||
Underlying implementation is `scripts/dev_shell.py`:
|
||||
|
||||
- `python scripts/dev_shell.py export --shell pwsh --json`
|
||||
- `python scripts/dev_shell.py doctor`
|
||||
|
||||
## Legacy PowerShell Compatibility
|
||||
|
||||
Legacy `.ps1` scripts remain for migration compatibility only. New functionality is implemented in Python scripts first. `script-common.ps1` is legacy-only.
|
||||
|
||||
Legacy runtime behavior in v1.2:
|
||||
|
||||
- strict mode rejects `targets`-only configs by default
|
||||
- compat mode (`SDT_LEGACY_MODE=compat`) temporarily allows legacy execution
|
||||
- TUI `SYSTEM` includes `Migrate legacy targets -> workflows` to apply migration in place (with backup)
|
||||
- Python reroute is authoritative for legacy `pwsh -File ...ps1` targets
|
||||
- `.ps1` fallback is opt-in only: set `SDT_PWSH_LEGACY_FALLBACK=1` for temporary compatibility
|
||||
|
||||
Deprecation target:
|
||||
|
||||
- v1.x: compatibility only (no new behavior guarantees)
|
||||
- v2.0: remove legacy `.ps1` scripts from default SDT workflows and docs
|
||||
|
||||
## Testing
|
||||
|
||||
Run unit/integration tests:
|
||||
|
||||
```powershell
|
||||
dotnet test tests/DevTool.Tests/DevTool.Tests.csproj
|
||||
```
|
||||
|
||||
Run Python script smoke checks:
|
||||
|
||||
```powershell
|
||||
python -m py_compile scripts/*.py
|
||||
```
|
||||
|
||||
Verify workflow route/path resolution:
|
||||
|
||||
```powershell
|
||||
python scripts/verify-workflow-routes.py --project-root .
|
||||
python scripts/verify-workflow-routes.py --project-root . --workflow build --workflow tauri --execute --env-profile dev
|
||||
```
|
||||
|
||||
## Reliability Matrix
|
||||
|
||||
- CI matrix workflow: [reliability-matrix.yml](/e:/stansshit/csharp/DevTool-master/.github/workflows/reliability-matrix.yml)
|
||||
- Runbook: [reliability-matrix-runbook.md](/e:/stansshit/csharp/DevTool-master/docs/reliability-matrix-runbook.md)
|
||||
- Results log: [reliability-matrix-results.md](/e:/stansshit/csharp/DevTool-master/docs/reliability-matrix-results.md)
|
||||
- Milestone status (Windows/Linux shipped, macOS delegated): [matrix-status.md](/e:/stansshit/csharp/DevTool-master/docs/matrix-status.md)
|
||||
Binary file not shown.
@ -1,47 +0,0 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Test-SdtIsWindows {
|
||||
if (Get-Variable -Name IsWindows -Scope Global -ErrorAction SilentlyContinue) {
|
||||
return [bool]$global:IsWindows
|
||||
}
|
||||
|
||||
return $env:OS -eq 'Windows_NT'
|
||||
}
|
||||
|
||||
function Resolve-SdtPython {
|
||||
$candidates = @('python')
|
||||
if (Test-SdtIsWindows) { $candidates += 'py' } else { $candidates += 'python3' }
|
||||
foreach ($c in $candidates) {
|
||||
try {
|
||||
& $c --version *> $null
|
||||
if ($LASTEXITCODE -eq 0) { return $c }
|
||||
} catch {}
|
||||
}
|
||||
return 'python'
|
||||
}
|
||||
|
||||
function Resolve-SdtScriptPath {
|
||||
param([Parameter(Mandatory=$true)][string]$ScriptName)
|
||||
|
||||
$bundled = Join-Path $PSScriptRoot $ScriptName
|
||||
if (Test-Path $bundled) { return $bundled }
|
||||
|
||||
$project = Join-Path (Join-Path $PSScriptRoot '..') ('scripts\\' + $ScriptName)
|
||||
if (Test-Path $project) { return (Resolve-Path $project).Path }
|
||||
|
||||
throw "Python helper script not found: $ScriptName"
|
||||
}
|
||||
|
||||
function Invoke-SdtPythonScript {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ScriptName,
|
||||
[string[]]$ForwardArgs = @()
|
||||
)
|
||||
|
||||
$python = Resolve-SdtPython
|
||||
$scriptPath = Resolve-SdtScriptPath -ScriptName $ScriptName
|
||||
|
||||
& $python $scriptPath @ForwardArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
@ -1,597 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from script_common import resolve_command
|
||||
|
||||
|
||||
def run_step(command, args, cwd):
|
||||
resolved = resolve_command(command)
|
||||
if shutil.which(resolved) is None and not pathlib.Path(resolved).exists():
|
||||
return {
|
||||
"command": resolved,
|
||||
"args": args,
|
||||
"cwd": cwd,
|
||||
"exit_code": 127,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "failed",
|
||||
"failure_reason": f"command_not_found:{resolved}",
|
||||
}
|
||||
|
||||
started = time.time()
|
||||
proc = subprocess.run([resolved, *args], cwd=cwd, check=False)
|
||||
elapsed = round(time.time() - started, 3)
|
||||
return {
|
||||
"command": resolved,
|
||||
"args": args,
|
||||
"cwd": cwd,
|
||||
"exit_code": proc.returncode,
|
||||
"elapsed_seconds": elapsed,
|
||||
"status": "ok" if proc.returncode == 0 else "failed",
|
||||
"failure_reason": None if proc.returncode == 0 else f"non_zero_exit:{proc.returncode}",
|
||||
}
|
||||
|
||||
|
||||
def resolve_python_executable():
|
||||
candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"]
|
||||
for c in candidates:
|
||||
if shutil.which(c):
|
||||
return c
|
||||
return "python"
|
||||
|
||||
|
||||
def parse_common(parser):
|
||||
parser.add_argument("--project-root", required=True)
|
||||
parser.add_argument("--working-dir", default=".")
|
||||
parser.add_argument("--json", action="store_true")
|
||||
|
||||
|
||||
def resolve_cwd(project_root, working_dir):
|
||||
return os.path.abspath(os.path.join(project_root, working_dir))
|
||||
|
||||
|
||||
EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"}
|
||||
|
||||
|
||||
def discover_dotnet_target(project_root: str, cwd: str):
|
||||
# Prefer local solution first (.slnx, then .sln), then csproj, then bounded scan from project root.
|
||||
local_slnx = sorted(pathlib.Path(cwd).glob("*.slnx"))
|
||||
if len(local_slnx) == 1:
|
||||
return str(local_slnx[0])
|
||||
|
||||
local_sln = sorted(pathlib.Path(cwd).glob("*.sln"))
|
||||
if len(local_sln) == 1:
|
||||
return str(local_sln[0])
|
||||
|
||||
local_csproj = sorted(pathlib.Path(cwd).glob("*.csproj"))
|
||||
if len(local_csproj) == 1:
|
||||
return str(local_csproj[0])
|
||||
|
||||
slnx_hits = bounded_find_files(project_root, ".slnx", max_depth=4)
|
||||
if len(slnx_hits) == 1:
|
||||
return slnx_hits[0]
|
||||
|
||||
sln_hits = bounded_find_files(project_root, ".sln", max_depth=4)
|
||||
if len(sln_hits) == 1:
|
||||
return sln_hits[0]
|
||||
|
||||
csproj_hits = bounded_find_files(project_root, ".csproj", max_depth=4)
|
||||
if len(csproj_hits) == 1:
|
||||
return csproj_hits[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def bounded_find_files(root: str, extension: str, max_depth: int):
|
||||
root_path = pathlib.Path(root).resolve()
|
||||
results = []
|
||||
for current_root, dirs, files in os.walk(root_path):
|
||||
rel = pathlib.Path(current_root).resolve().relative_to(root_path)
|
||||
depth = len(rel.parts)
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDED_SCAN_DIRS]
|
||||
if depth > max_depth:
|
||||
dirs[:] = []
|
||||
continue
|
||||
|
||||
for name in files:
|
||||
if name.lower().endswith(extension.lower()):
|
||||
results.append(str(pathlib.Path(current_root) / name))
|
||||
return sorted(results)
|
||||
|
||||
|
||||
def run_dotnet_action(project_root, working_dir, verb):
|
||||
cwd = resolve_cwd(project_root, working_dir)
|
||||
target = discover_dotnet_target(project_root, cwd)
|
||||
if not target:
|
||||
return 0, {
|
||||
"command": "dotnet",
|
||||
"args": [verb],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_dotnet_target",
|
||||
"message": "No .slnx/.sln/.csproj found for this step. Skipping dotnet action.",
|
||||
}
|
||||
|
||||
args = [verb, target]
|
||||
step = run_step("dotnet", args, cwd)
|
||||
step["resolved_target"] = target
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def _deps_hash(app_root):
|
||||
h = hashlib.sha256()
|
||||
for name in ("package.json", "package-lock.json"):
|
||||
p = pathlib.Path(app_root) / name
|
||||
if p.exists():
|
||||
h.update(p.read_bytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def ensure_npm_dependencies(app_root):
|
||||
package_json = pathlib.Path(app_root) / "package.json"
|
||||
if not package_json.exists():
|
||||
return {"installed": False, "reason": "not_applicable"}
|
||||
|
||||
node_modules = pathlib.Path(app_root) / "node_modules"
|
||||
deps_hash_file = node_modules / ".sdt-deps.sha256"
|
||||
expected = _deps_hash(app_root)
|
||||
|
||||
should_install = not node_modules.exists()
|
||||
if not should_install:
|
||||
if not deps_hash_file.exists():
|
||||
should_install = True
|
||||
else:
|
||||
current = deps_hash_file.read_text(encoding="utf-8").strip()
|
||||
should_install = current != expected
|
||||
|
||||
if not should_install:
|
||||
return {"installed": False, "reason": "deps_unchanged"}
|
||||
|
||||
lock_exists = (pathlib.Path(app_root) / "package-lock.json").exists()
|
||||
install_args = ["ci", "--no-audit", "--fund=false"] if lock_exists else ["install", "--no-audit", "--fund=false"]
|
||||
install_step = run_step("npm", install_args, app_root)
|
||||
if install_step["exit_code"] != 0:
|
||||
if lock_exists and install_args[0] == "ci":
|
||||
fallback = run_step("npm", ["install", "--no-audit", "--fund=false"], app_root)
|
||||
if fallback["exit_code"] != 0:
|
||||
fallback["failure_reason"] = "deps_install_failed_after_ci_fallback"
|
||||
return {"installed": True, "reason": "install_failed", "step": fallback}
|
||||
install_step = fallback
|
||||
else:
|
||||
return {"installed": True, "reason": "install_failed", "step": install_step}
|
||||
|
||||
node_modules.mkdir(parents=True, exist_ok=True)
|
||||
deps_hash_file.write_text(expected, encoding="utf-8")
|
||||
return {"installed": True, "reason": "installed", "step": install_step}
|
||||
|
||||
|
||||
def read_package_json(cwd: str):
|
||||
package_json = pathlib.Path(cwd) / "package.json"
|
||||
if not package_json.exists():
|
||||
return None
|
||||
try:
|
||||
return json.loads(package_json.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def has_npm_script(cwd: str, script_name: str) -> bool:
|
||||
data = read_package_json(cwd)
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
scripts = data.get("scripts")
|
||||
if not isinstance(scripts, dict):
|
||||
return False
|
||||
return script_name in scripts and isinstance(scripts.get(script_name), str)
|
||||
|
||||
|
||||
def action_dotnet_build(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "build")
|
||||
|
||||
|
||||
def action_dotnet_restore(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "restore")
|
||||
|
||||
|
||||
def action_dotnet_test(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "test")
|
||||
|
||||
|
||||
def action_dotnet_publish(args):
|
||||
return run_dotnet_action(args.project_root, args.working_dir, "publish")
|
||||
|
||||
|
||||
def action_npm_install(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["install"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
step = run_step("npm", ["install"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_npm_ci(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["ci"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
step = run_step("npm", ["ci"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_npm_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if not has_npm_script(cwd, "build"):
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_missing_build_script",
|
||||
}
|
||||
deps = ensure_npm_dependencies(cwd)
|
||||
if deps.get("reason") == "not_applicable":
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if deps.get("reason") == "install_failed":
|
||||
step = deps["step"]
|
||||
step["failure_reason"] = "deps_install_failed"
|
||||
return step["exit_code"], step
|
||||
step = run_step("npm", ["run", "build"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_npm_test(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if not has_npm_script(cwd, "test"):
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_missing_test_script",
|
||||
}
|
||||
deps = ensure_npm_dependencies(cwd)
|
||||
if deps.get("reason") == "not_applicable":
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
if deps.get("reason") == "install_failed":
|
||||
step = deps["step"]
|
||||
step["failure_reason"] = "deps_install_failed"
|
||||
return step["exit_code"], step
|
||||
step = run_step("npm", ["test"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_npm_audit(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["audit"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_package_json",
|
||||
}
|
||||
step = run_step("npm", ["audit"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_venv_create(args):
|
||||
cwd = resolve_cwd(args.project_root, ".")
|
||||
venv_dir = args.venv_dir or ".venv"
|
||||
step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_pip_install(args):
|
||||
cwd = resolve_cwd(args.project_root, ".")
|
||||
req = args.requirements
|
||||
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_pip_sync(args):
|
||||
cwd = resolve_cwd(args.project_root, ".")
|
||||
req = args.requirements
|
||||
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_python_pytest(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_cargo_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
||||
return 0, {
|
||||
"command": "cargo",
|
||||
"args": ["build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_cargo_toml",
|
||||
}
|
||||
step = run_step("cargo", ["build"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_cargo_test(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
||||
return 0, {
|
||||
"command": "cargo",
|
||||
"args": ["test"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_cargo_toml",
|
||||
}
|
||||
step = run_step("cargo", ["test"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_tauri_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json"
|
||||
if not tauri_conf.exists():
|
||||
tauri_conf = pathlib.Path(cwd) / "tauri.conf.json"
|
||||
if not tauri_conf.exists() or not (pathlib.Path(cwd) / "package.json").exists():
|
||||
return 0, {
|
||||
"command": "npm",
|
||||
"args": ["run", "tauri", "build"],
|
||||
"cwd": cwd,
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 0.0,
|
||||
"status": "skipped",
|
||||
"failure_reason": None,
|
||||
"skip_reason": "not_applicable_no_tauri_project",
|
||||
}
|
||||
|
||||
deps = ensure_npm_dependencies(cwd)
|
||||
if deps.get("reason") == "install_failed":
|
||||
step = deps["step"]
|
||||
step["failure_reason"] = "deps_install_failed"
|
||||
return step["exit_code"], step
|
||||
|
||||
tauri_args = ["run", "tauri", "build"]
|
||||
if args.no_bundle:
|
||||
tauri_args.extend(["--", "--no-bundle"])
|
||||
step = run_step("npm", tauri_args, cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_status(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["status"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_fetch(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["fetch"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_pull(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["pull"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_git_clean(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("git", ["clean", "-fd"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_docker_build(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("docker", ["build", "."], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_docker_compose_up(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("docker", ["compose", "up", "-d"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def action_docker_compose_down(args):
|
||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||
step = run_step("docker", ["compose", "down"], cwd)
|
||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SDT normalized build actions")
|
||||
sub = parser.add_subparsers(dest="action", required=True)
|
||||
|
||||
p0 = sub.add_parser("dotnet-restore")
|
||||
parse_common(p0)
|
||||
|
||||
p1 = sub.add_parser("dotnet-build")
|
||||
parse_common(p1)
|
||||
|
||||
p1b = sub.add_parser("dotnet-test")
|
||||
parse_common(p1b)
|
||||
|
||||
p1c = sub.add_parser("dotnet-publish")
|
||||
parse_common(p1c)
|
||||
|
||||
p2 = sub.add_parser("npm-install")
|
||||
parse_common(p2)
|
||||
|
||||
p2b = sub.add_parser("npm-ci")
|
||||
parse_common(p2b)
|
||||
|
||||
p3 = sub.add_parser("npm-build")
|
||||
parse_common(p3)
|
||||
|
||||
p3b = sub.add_parser("npm-test")
|
||||
parse_common(p3b)
|
||||
|
||||
p3c = sub.add_parser("npm-audit")
|
||||
parse_common(p3c)
|
||||
|
||||
p4 = sub.add_parser("python-venv-create")
|
||||
parse_common(p4)
|
||||
p4.add_argument("--venv-dir", default=".venv")
|
||||
|
||||
p5 = sub.add_parser("python-pip-install")
|
||||
parse_common(p5)
|
||||
p5.add_argument("--requirements", required=True)
|
||||
|
||||
p5b = sub.add_parser("python-pip-sync")
|
||||
parse_common(p5b)
|
||||
p5b.add_argument("--requirements", required=True)
|
||||
|
||||
p5c = sub.add_parser("python-pytest")
|
||||
parse_common(p5c)
|
||||
|
||||
p6 = sub.add_parser("cargo-build")
|
||||
parse_common(p6)
|
||||
|
||||
p6b = sub.add_parser("cargo-test")
|
||||
parse_common(p6b)
|
||||
|
||||
p7 = sub.add_parser("tauri-build")
|
||||
parse_common(p7)
|
||||
p7.add_argument("--no-bundle", action="store_true")
|
||||
|
||||
p8 = sub.add_parser("git-status")
|
||||
parse_common(p8)
|
||||
|
||||
p9 = sub.add_parser("git-fetch")
|
||||
parse_common(p9)
|
||||
|
||||
p10 = sub.add_parser("git-pull")
|
||||
parse_common(p10)
|
||||
|
||||
p11 = sub.add_parser("git-clean")
|
||||
parse_common(p11)
|
||||
|
||||
p12 = sub.add_parser("docker-build")
|
||||
parse_common(p12)
|
||||
|
||||
p13 = sub.add_parser("docker-compose-up")
|
||||
parse_common(p13)
|
||||
|
||||
p14 = sub.add_parser("docker-compose-down")
|
||||
parse_common(p14)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
handlers = {
|
||||
"dotnet-restore": action_dotnet_restore,
|
||||
"dotnet-build": action_dotnet_build,
|
||||
"dotnet-test": action_dotnet_test,
|
||||
"dotnet-publish": action_dotnet_publish,
|
||||
"npm-install": action_npm_install,
|
||||
"npm-ci": action_npm_ci,
|
||||
"npm-build": action_npm_build,
|
||||
"npm-test": action_npm_test,
|
||||
"npm-audit": action_npm_audit,
|
||||
"python-venv-create": action_python_venv_create,
|
||||
"python-pip-install": action_python_pip_install,
|
||||
"python-pip-sync": action_python_pip_sync,
|
||||
"python-pytest": action_python_pytest,
|
||||
"cargo-build": action_cargo_build,
|
||||
"cargo-test": action_cargo_test,
|
||||
"tauri-build": action_tauri_build,
|
||||
"git-status": action_git_status,
|
||||
"git-fetch": action_git_fetch,
|
||||
"git-pull": action_git_pull,
|
||||
"git-clean": action_git_clean,
|
||||
"docker-build": action_docker_build,
|
||||
"docker-compose-up": action_docker_compose_up,
|
||||
"docker-compose-down": action_docker_compose_down,
|
||||
}
|
||||
|
||||
code, summary = handlers[args.action](args)
|
||||
if args.json:
|
||||
print(json.dumps(summary))
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,17 +0,0 @@
|
||||
@echo off
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
|
||||
where py >nul 2>nul
|
||||
if %ERRORLEVEL%==0 (
|
||||
set "PYEXE=py"
|
||||
) else (
|
||||
where python >nul 2>nul
|
||||
if not %ERRORLEVEL%==0 (
|
||||
echo python not found.
|
||||
exit /b 1
|
||||
)
|
||||
set "PYEXE=python"
|
||||
)
|
||||
|
||||
for /f "usebackq delims=" %%L in (`"%PYEXE%" "%SCRIPT_DIR%dev_shell.py" export --shell cmd`) do %%L
|
||||
echo Development shell initialized from Python bootstrap script.
|
||||
@ -1,21 +0,0 @@
|
||||
# Run this in PowerShell before development commands:
|
||||
# . ./scripts/dev-shell.ps1
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot '_pwsh-python-shim.ps1')
|
||||
|
||||
$scriptPath = Resolve-SdtScriptPath -ScriptName 'dev_shell.py'
|
||||
$python = Resolve-SdtPython
|
||||
|
||||
$lines = & $python $scriptPath export --shell pwsh
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to initialize development shell via dev_shell.py"
|
||||
}
|
||||
|
||||
foreach ($line in $lines) {
|
||||
Invoke-Expression $line
|
||||
}
|
||||
|
||||
Write-Host "Development shell initialized from Python bootstrap script."
|
||||
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON_EXE="python3"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PYTHON_EXE="python"
|
||||
else
|
||||
echo "python3/python not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eval "$("$PYTHON_EXE" "$SCRIPT_DIR/dev_shell.py" export --shell bash)"
|
||||
echo "Development shell initialized from Python bootstrap script."
|
||||
@ -1,148 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
from script_common import PROXY_VARS, clean_proxy_env, dotnet_env, ensure_dirs, pip_env, resolve_repo_root
|
||||
|
||||
|
||||
def huggingface_env(repo_root: pathlib.Path) -> dict[str, str]:
|
||||
env = {}
|
||||
hf_home = repo_root / ".cache" / "huggingface"
|
||||
hf_hub_cache = hf_home / "hub"
|
||||
ensure_dirs([hf_hub_cache])
|
||||
env["HF_HOME"] = str(hf_home)
|
||||
env["HUGGINGFACE_HUB_CACHE"] = str(hf_hub_cache)
|
||||
env["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
||||
return env
|
||||
|
||||
|
||||
def resolved_env(repo_root: pathlib.Path) -> dict[str, str]:
|
||||
env = {}
|
||||
dotnet = dotnet_env(repo_root)
|
||||
pip = pip_env(repo_root)
|
||||
hf = huggingface_env(repo_root)
|
||||
|
||||
dotnet_keys = [
|
||||
"DOTNET_CLI_HOME",
|
||||
"NUGET_PACKAGES",
|
||||
"NUGET_HTTP_CACHE_PATH",
|
||||
"DOTNET_SKIP_FIRST_TIME_EXPERIENCE",
|
||||
"DOTNET_ADD_GLOBAL_TOOLS_TO_PATH",
|
||||
"DOTNET_GENERATE_ASPNET_CERTIFICATE",
|
||||
"DOTNET_CLI_TELEMETRY_OPTOUT",
|
||||
"NUGET_CERT_REVOCATION_MODE",
|
||||
]
|
||||
pip_keys = [
|
||||
"PIP_CACHE_DIR",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK",
|
||||
"PIP_DEFAULT_TIMEOUT",
|
||||
"PIP_RETRIES",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
]
|
||||
for key in dotnet_keys:
|
||||
env[key] = dotnet[key]
|
||||
for key in pip_keys:
|
||||
env[key] = pip[key]
|
||||
env.update(hf)
|
||||
clean_proxy_env(env)
|
||||
return env
|
||||
|
||||
|
||||
def export_lines(shell: str, env_map: dict[str, str]) -> list[str]:
|
||||
def sh_quote(value: str) -> str:
|
||||
return "'" + value.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
if shell == "pwsh":
|
||||
lines = [f"Remove-Item Env:{k} -ErrorAction SilentlyContinue" for k in PROXY_VARS]
|
||||
lines.extend(f"$env:{k} = \"{v.replace('\"', '`\"')}\"" for k, v in env_map.items())
|
||||
return lines
|
||||
if shell in ("bash", "zsh"):
|
||||
lines = [f"unset {k}" for k in PROXY_VARS]
|
||||
lines.extend(f"export {k}={sh_quote(v)}" for k, v in env_map.items())
|
||||
return lines
|
||||
if shell == "cmd":
|
||||
lines = [f"set {k}=" for k in PROXY_VARS]
|
||||
lines.extend(f"set {k}={v}" for k, v in env_map.items())
|
||||
return lines
|
||||
raise ValueError(shell)
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
try:
|
||||
repo_root = resolve_repo_root(args.project_root)
|
||||
except Exception as ex:
|
||||
print(f"Failed to resolve project root: {ex}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
env_map = resolved_env(repo_root)
|
||||
payload = {
|
||||
"projectRoot": str(repo_root),
|
||||
"env": env_map,
|
||||
"createdDirs": [
|
||||
str(repo_root / ".dotnet_home"),
|
||||
str(repo_root / ".nuget" / "packages"),
|
||||
str(repo_root / ".nuget" / "http-cache"),
|
||||
str(repo_root / ".pip" / "cache"),
|
||||
str(repo_root / ".tmp" / "pip-temp"),
|
||||
str(repo_root / ".cache" / "huggingface" / "hub"),
|
||||
],
|
||||
"warnings": [],
|
||||
}
|
||||
|
||||
try:
|
||||
lines = export_lines(args.shell, env_map)
|
||||
except ValueError:
|
||||
print(f"Unsupported shell target: {args.shell}", file=sys.stderr)
|
||||
return 3
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(payload))
|
||||
else:
|
||||
for line in lines:
|
||||
print(line)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_doctor(args):
|
||||
try:
|
||||
repo_root = resolve_repo_root(args.project_root)
|
||||
except Exception as ex:
|
||||
print(f"Failed to resolve project root: {ex}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
env_map = resolved_env(repo_root)
|
||||
checks = {
|
||||
"repo_root": str(repo_root),
|
||||
"dotnet_home_exists": (repo_root / ".dotnet_home").exists(),
|
||||
"nuget_cache_exists": (repo_root / ".nuget" / "packages").exists(),
|
||||
"pip_cache_exists": (repo_root / ".pip" / "cache").exists(),
|
||||
"hf_cache_exists": (repo_root / ".cache" / "huggingface" / "hub").exists(),
|
||||
"env_count": len(env_map),
|
||||
}
|
||||
print(json.dumps(checks))
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SDT cross-platform shell bootstrap helper")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_export = sub.add_parser("export", help="Print env exports for a shell")
|
||||
p_export.add_argument("--shell", required=True)
|
||||
p_export.add_argument("--project-root")
|
||||
p_export.add_argument("--json", action="store_true")
|
||||
|
||||
p_doctor = sub.add_parser("doctor", help="Validate env bootstrap paths")
|
||||
p_doctor.add_argument("--project-root")
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "export":
|
||||
return cmd_export(args)
|
||||
return cmd_doctor(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from script_common import resolve_command
|
||||
|
||||
|
||||
def run_capture(cmd):
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
out = (proc.stdout or "").strip()
|
||||
err = (proc.stderr or "").strip()
|
||||
text = out if out else err
|
||||
return proc.returncode == 0, text
|
||||
except Exception as ex:
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
def probe_tool(tool):
|
||||
mapping = {
|
||||
"dotnet": ["dotnet", "--version"],
|
||||
"node": ["node", "--version"],
|
||||
"npm": ["npm", "--version"],
|
||||
"python": ["python", "--version"],
|
||||
"cargo": ["cargo", "--version"],
|
||||
"tauri": ["tauri", "--version"],
|
||||
"git": ["git", "--version"],
|
||||
"docker": ["docker", "--version"],
|
||||
}
|
||||
cmd = mapping.get(tool, [tool, "--version"])
|
||||
resolved = resolve_command(cmd[0])
|
||||
if shutil.which(resolved) is None and not os.path.exists(resolved):
|
||||
return {"tool": tool, "available": False, "version": None, "details": f"{cmd[0]} not found in PATH"}
|
||||
cmd = [resolved, *cmd[1:]]
|
||||
ok, text = run_capture(cmd)
|
||||
return {"tool": tool, "available": ok, "version": text if ok else None, "details": None if ok else text}
|
||||
|
||||
|
||||
def install_plan(tool):
|
||||
is_windows = platform.system().lower().startswith("win")
|
||||
if is_windows:
|
||||
plans = {
|
||||
"dotnet": [("winget", ["install", "Microsoft.DotNet.SDK.10"])],
|
||||
"node": [("winget", ["install", "OpenJS.NodeJS.LTS"])],
|
||||
"npm": [("winget", ["install", "OpenJS.NodeJS.LTS"])],
|
||||
"python": [("winget", ["install", "Python.Python.3.12"])],
|
||||
"cargo": [("winget", ["install", "Rustlang.Rustup"])],
|
||||
"tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])],
|
||||
"git": [("winget", ["install", "Git.Git"])],
|
||||
"docker": [("winget", ["install", "Docker.DockerDesktop"])],
|
||||
}
|
||||
else:
|
||||
plans = {
|
||||
"dotnet": [("sh", ["-c", "echo install dotnet sdk with your package manager"])],
|
||||
"node": [("sh", ["-c", "echo install nodejs with your package manager"])],
|
||||
"npm": [("sh", ["-c", "echo install npm with your package manager"])],
|
||||
"python": [("sh", ["-c", "echo install python3 with your package manager"])],
|
||||
"cargo": [("sh", ["-c", "curl https://sh.rustup.rs -sSf | sh"])],
|
||||
"tauri": [("npm", ["install", "-g", "@tauri-apps/cli"])],
|
||||
"git": [("sh", ["-c", "echo install git with your package manager"])],
|
||||
"docker": [("sh", ["-c", "echo install docker with your package manager"])],
|
||||
}
|
||||
|
||||
cmds = plans.get(tool, [])
|
||||
return {
|
||||
"tool": tool,
|
||||
"supported": len(cmds) > 0,
|
||||
"summary": f"Install plan for {tool} on {platform.system()}",
|
||||
"commands": [{"command": c, "args": a} for c, a in cmds],
|
||||
}
|
||||
|
||||
|
||||
def run_install(tool):
|
||||
plan = install_plan(tool)
|
||||
if not plan["supported"]:
|
||||
return 2
|
||||
for cmd in plan["commands"]:
|
||||
proc = subprocess.run([cmd["command"], *cmd["args"]], check=False)
|
||||
if proc.returncode != 0:
|
||||
return proc.returncode
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="SDT diagnostics and install planner")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_probe = sub.add_parser("probe")
|
||||
p_probe.add_argument("--tool", required=True)
|
||||
p_probe.add_argument("--json", action="store_true")
|
||||
|
||||
p_plan = sub.add_parser("install-plan")
|
||||
p_plan.add_argument("--tool", required=True)
|
||||
p_plan.add_argument("--json", action="store_true")
|
||||
|
||||
p_run = sub.add_parser("install-run")
|
||||
p_run.add_argument("--tool", required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cmd == "probe":
|
||||
result = probe_tool(args.tool.lower())
|
||||
if args.json:
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(result)
|
||||
return 0 if result["available"] else 1
|
||||
|
||||
if args.cmd == "install-plan":
|
||||
result = install_plan(args.tool.lower())
|
||||
if args.json:
|
||||
print(json.dumps(result))
|
||||
else:
|
||||
print(result)
|
||||
return 0 if result["supported"] else 2
|
||||
|
||||
if args.cmd == "install-run":
|
||||
return run_install(args.tool.lower())
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from script_common import dotnet_env, resolve_repo_root, run
|
||||
|
||||
|
||||
DOTNET_SAFE_CMDS = {"restore", "build", "run", "test", "publish", "pack"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform minimal dotnet wrapper")
|
||||
parser.add_argument("dotnet_args", nargs=argparse.REMAINDER)
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.dotnet_args:
|
||||
print("Usage: python scripts/dotnet-min.py <dotnet args>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
dotnet_args = list(args.dotnet_args)
|
||||
cmd = dotnet_args[0].lower()
|
||||
|
||||
if cmd in DOTNET_SAFE_CMDS:
|
||||
dotnet_args.extend(["-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false"])
|
||||
if cmd == "restore":
|
||||
dotnet_args.append("--ignore-failed-sources")
|
||||
|
||||
return run("dotnet", dotnet_args, repo_root, env=dotnet_env(repo_root))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,44 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def run_step(repo_root: Path, title: str, command: list[str]) -> int:
|
||||
print(f"\n== {title} ==")
|
||||
print("$", " ".join(command))
|
||||
proc = subprocess.run(command, cwd=str(repo_root), check=False)
|
||||
return proc.returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform migration quality gate")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--skip-tests", action="store_true")
|
||||
parser.add_argument("--test-project", default=None, help="Optional test csproj path")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
|
||||
code = run_step(repo_root, "Build", [sys.executable, "scripts/dotnet-min.py", "build"])
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_tests:
|
||||
if args.test_project:
|
||||
test_cmd = [sys.executable, "scripts/dotnet-min.py", "test", args.test_project]
|
||||
else:
|
||||
test_cmd = [sys.executable, "scripts/dotnet-min.py", "test"]
|
||||
code = run_step(repo_root, "Tests", test_cmd)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
print("\nMigration gate passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform node_modules cleanup")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--working-dir", default=".")
|
||||
parser.add_argument("--also-cache", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
work_dir = (repo_root / args.working_dir).resolve()
|
||||
node_modules = work_dir / "node_modules"
|
||||
if node_modules.exists():
|
||||
shutil.rmtree(node_modules)
|
||||
print(f"Removed: {node_modules}")
|
||||
else:
|
||||
print(f"Not found: {node_modules}")
|
||||
|
||||
if args.also_cache:
|
||||
npm_cache = repo_root / ".npm" / "cache"
|
||||
if npm_cache.exists():
|
||||
shutil.rmtree(npm_cache)
|
||||
print(f"Removed: {npm_cache}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Export local NuGet cache to zip")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--output-zip", default="nuget-cache-export.zip")
|
||||
parser.add_argument("--include-dotnet-home", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_zip = (repo_root / args.output_zip).resolve()
|
||||
|
||||
nuget_dir = repo_root / ".nuget"
|
||||
dotnet_home = repo_root / ".dotnet_home"
|
||||
if not nuget_dir.exists():
|
||||
print(f"NuGet cache not found: {nuget_dir}")
|
||||
return 2
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
stage = Path(td) / "cache-export"
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(nuget_dir, stage / ".nuget")
|
||||
if args.include_dotnet_home and dotnet_home.exists():
|
||||
shutil.copytree(dotnet_home, stage / ".dotnet_home")
|
||||
manifest = stage / "nuget-cache-manifest.txt"
|
||||
manifest.write_text("exported_by=nuget-export-cache.py\n", encoding="utf-8")
|
||||
archive_base = str(output_zip.with_suffix(""))
|
||||
shutil.make_archive(archive_base, "zip", root_dir=str(stage))
|
||||
|
||||
print(f"Exported cache: {output_zip}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
|
||||
from script_common import resolve_repo_root
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Import NuGet cache from zip")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--input-zip", default="nuget-cache-export.zip")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
input_zip = (repo_root / args.input_zip).resolve()
|
||||
if not input_zip.exists():
|
||||
print(f"Input zip not found: {input_zip}")
|
||||
return 2
|
||||
|
||||
shutil.unpack_archive(str(input_zip), extract_dir=str(repo_root))
|
||||
print(f"Imported cache from: {input_zip}")
|
||||
print("Run `python scripts/dotnet-min.py restore` to validate restore in this repo.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from script_common import pip_env, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform minimal pip wrapper")
|
||||
parser.add_argument("pip_args", nargs=argparse.REMAINDER)
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.pip_args:
|
||||
print("Usage: python scripts/pip-min.py <pip args>", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
pip_args = list(args.pip_args)
|
||||
|
||||
# Preserve legacy behavior: for bare install, default target to repo-local deps.
|
||||
if pip_args and pip_args[0].lower() == "install":
|
||||
has_target = any(a in ("--target", "--prefix") for a in pip_args)
|
||||
if not has_target:
|
||||
pip_args = [a for a in pip_args if a != "--user"]
|
||||
target = repo_root / ".pydeps" / f"py{sys.version_info.major}{sys.version_info.minor}"
|
||||
os.makedirs(target, exist_ok=True)
|
||||
pip_args.extend(["--target", str(target)])
|
||||
|
||||
return run(sys.executable, ["-m", "pip", *pip_args], repo_root, env=pip_env(repo_root))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,46 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
|
||||
def _mkdtemp_compat(
|
||||
suffix: str | None = None,
|
||||
prefix: str | None = None,
|
||||
dir: str | None = None,
|
||||
) -> str:
|
||||
# Python 3.14 on some Windows hosts creates mkdtemp dirs that are
|
||||
# immediately non-writable by the same process when mode=0o700 is used.
|
||||
# pip relies heavily on tempfile; force 0o777 for compatibility.
|
||||
if dir is None:
|
||||
dir = tempfile.gettempdir()
|
||||
if prefix is None:
|
||||
prefix = tempfile.template
|
||||
if suffix is None:
|
||||
suffix = ""
|
||||
|
||||
names = tempfile._get_candidate_names()
|
||||
for _ in range(tempfile.TMP_MAX):
|
||||
name = next(names)
|
||||
path = os.path.join(dir, f"{prefix}{name}{suffix}")
|
||||
try:
|
||||
os.mkdir(path, 0o777)
|
||||
return path
|
||||
except FileExistsError:
|
||||
continue
|
||||
|
||||
raise FileExistsError("No usable temporary directory name found.")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
tempfile.mkdtemp = _mkdtemp_compat # type: ignore[assignment]
|
||||
|
||||
from pip._internal.cli.main import main as pip_main
|
||||
|
||||
return int(pip_main(argv))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(__import__("sys").argv[1:]))
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
|
||||
|
||||
from script_common import find_node_app_root, resolve_repo_root, run, sha256_files
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper")
|
||||
parser.add_argument("--target", choices=["web", "tauri"], default="web")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none")
|
||||
parser.add_argument("--install-deps", action="store_true")
|
||||
parser.add_argument("--skip-install", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
app_root = find_node_app_root(repo_root, args.app_root)
|
||||
if app_root is None:
|
||||
print("Unable to locate app root (no unique package.json found).")
|
||||
return 2
|
||||
|
||||
package_json = app_root / "package.json"
|
||||
lock_file = app_root / "package-lock.json"
|
||||
node_modules = app_root / "node_modules"
|
||||
deps_hash_file = node_modules / ".sdt-deps.sha256"
|
||||
expected_hash = sha256_files([package_json, lock_file])
|
||||
|
||||
should_install = args.install_deps or not node_modules.exists()
|
||||
if not should_install and not args.skip_install:
|
||||
if not deps_hash_file.exists():
|
||||
should_install = True
|
||||
else:
|
||||
current = deps_hash_file.read_text(encoding="utf-8").strip()
|
||||
should_install = current != expected_hash
|
||||
if args.skip_install:
|
||||
should_install = False
|
||||
|
||||
print(f"App root: {app_root}")
|
||||
print(f"Target: {args.target} ({args.configuration})")
|
||||
|
||||
if should_install:
|
||||
install_args = ["ci", "--no-audit", "--fund=false"] if lock_file.exists() else ["install", "--no-audit", "--fund=false"]
|
||||
print("$ npm " + " ".join(install_args))
|
||||
if not args.dry_run:
|
||||
code = run("npm", install_args, app_root)
|
||||
if code != 0:
|
||||
if lock_file.exists() and install_args[0] == "ci":
|
||||
print("npm ci failed (likely lockfile out of sync). Falling back to npm install...")
|
||||
fallback_args = ["install", "--no-audit", "--fund=false"]
|
||||
print("$ npm " + " ".join(fallback_args))
|
||||
code = run("npm", fallback_args, app_root)
|
||||
if code != 0:
|
||||
return code
|
||||
else:
|
||||
return code
|
||||
node_modules.mkdir(parents=True, exist_ok=True)
|
||||
deps_hash_file.write_text(expected_hash, encoding="utf-8")
|
||||
else:
|
||||
print("Skipping dependency install.")
|
||||
|
||||
if args.target == "web":
|
||||
cmd = ["run", "build"]
|
||||
print("$ npm " + " ".join(cmd))
|
||||
if not args.dry_run:
|
||||
return run("npm", cmd, app_root)
|
||||
return 0
|
||||
|
||||
tauri_cmd = ["run", "tauri", "build"]
|
||||
tauri_tail: list[str] = []
|
||||
if args.tauri_bundles == "none":
|
||||
tauri_tail.extend(["--no-bundle"])
|
||||
else:
|
||||
tauri_tail.extend(["--bundles", args.tauri_bundles])
|
||||
if args.configuration == "Debug":
|
||||
tauri_tail.append("--debug")
|
||||
if tauri_tail:
|
||||
tauri_cmd.extend(["--", *tauri_tail])
|
||||
|
||||
print("$ npm " + " ".join(tauri_cmd))
|
||||
if not args.dry_run:
|
||||
return run("npm", tauri_cmd, app_root)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,125 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import find_csproj_by_keyword, find_node_app_root, resolve_repo_root
|
||||
|
||||
|
||||
def run_step(label: str, cmd: list[str], cwd: Path, dry_run: bool) -> int:
|
||||
print(f"\n> {label}")
|
||||
print("$", " ".join(cmd))
|
||||
if dry_run:
|
||||
return 0
|
||||
proc = subprocess.run(cmd, cwd=str(cwd), check=False)
|
||||
return proc.returncode
|
||||
|
||||
|
||||
def has_package_script(app_root: Path, script_name: str) -> bool:
|
||||
package_json = app_root / "package.json"
|
||||
if not package_json.exists():
|
||||
return False
|
||||
try:
|
||||
data = json.loads(package_json.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return False
|
||||
scripts = data.get("scripts")
|
||||
if not isinstance(scripts, dict):
|
||||
return False
|
||||
value = scripts.get(script_name)
|
||||
return isinstance(value, str) and value.strip() != ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--runtime", default="win-x64")
|
||||
parser.add_argument("--skip-sidecar", action="store_true")
|
||||
parser.add_argument("--skip-web", action="store_true")
|
||||
parser.add_argument("--skip-webgateway", action="store_true")
|
||||
parser.add_argument("--skip-tauri", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--sidecar-project", default=None)
|
||||
parser.add_argument("--gateway-project", default=None)
|
||||
parser.add_argument("--app-root", default=None)
|
||||
parser.add_argument("--output-dir", default="output")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_root = (repo_root / args.output_dir).resolve()
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
sidecar_project = (repo_root / args.sidecar_project).resolve() if args.sidecar_project else find_csproj_by_keyword(repo_root, ["sidecar"])
|
||||
gateway_project = (repo_root / args.gateway_project).resolve() if args.gateway_project else find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
||||
app_root = (repo_root / args.app_root).resolve() if args.app_root else find_node_app_root(repo_root, None)
|
||||
tauri_conf = None
|
||||
if app_root is not None:
|
||||
candidate_a = app_root / "src-tauri" / "tauri.conf.json"
|
||||
candidate_b = app_root / "tauri.conf.json"
|
||||
if candidate_a.exists():
|
||||
tauri_conf = candidate_a
|
||||
elif candidate_b.exists():
|
||||
tauri_conf = candidate_b
|
||||
|
||||
py = sys.executable
|
||||
if not args.skip_sidecar:
|
||||
if sidecar_project is None:
|
||||
print("Skipping sidecar: no sidecar csproj detected.")
|
||||
else:
|
||||
cmd = [py, "scripts/publish-sidecar.py", "--configuration", args.configuration, "--runtime", args.runtime]
|
||||
cmd.extend(["--project", str(sidecar_project)])
|
||||
code = run_step("Publish sidecar", cmd, repo_root, args.dry_run)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_web:
|
||||
if app_root is None:
|
||||
print("Skipping web: no app root with package.json detected.")
|
||||
elif not has_package_script(app_root, "build"):
|
||||
print("Skipping web: package.json has no 'build' script.")
|
||||
else:
|
||||
cmd = [py, "scripts/publish-app.py", "--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)]
|
||||
code = run_step("Build web", cmd, repo_root, args.dry_run)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_webgateway:
|
||||
if gateway_project is None:
|
||||
print("Skipping web gateway: no gateway csproj detected.")
|
||||
else:
|
||||
cmd = [py, "scripts/publish-webgateway.py", "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)]
|
||||
code = run_step("Publish web gateway", cmd, repo_root, args.dry_run)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_tauri:
|
||||
if app_root is None or tauri_conf is None:
|
||||
print("Skipping tauri: tauri app not detected.")
|
||||
elif not has_package_script(app_root, "tauri"):
|
||||
print("Skipping tauri: package.json has no 'tauri' script.")
|
||||
else:
|
||||
cmd = [py, "scripts/publish-app.py", "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)]
|
||||
code = run_step("Build tauri", cmd, repo_root, args.dry_run)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release")
|
||||
exes = sorted(target_dir.glob("*.exe"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if exes:
|
||||
staged = output_root / exes[0].name
|
||||
if args.dry_run:
|
||||
print(f"Would copy: {exes[0]} -> {staged}")
|
||||
else:
|
||||
shutil.copy2(exes[0], staged)
|
||||
print(f"Staged desktop executable: {staged}")
|
||||
|
||||
print("\nPublish output workflow complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
|
||||
|
||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper")
|
||||
parser.add_argument("--configuration", default="Release")
|
||||
parser.add_argument("--runtime", default="win-x64")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj")
|
||||
parser.add_argument("--output-dir", default="output")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_dir = (repo_root / args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.project:
|
||||
csproj = (repo_root / args.project).resolve()
|
||||
else:
|
||||
csproj = find_csproj_by_keyword(repo_root, ["sidecar"])
|
||||
if csproj is None or not csproj.exists():
|
||||
print("Could not locate sidecar project. Pass --project <path/to/project.csproj>.")
|
||||
return 2
|
||||
|
||||
publish_args = [
|
||||
"publish",
|
||||
str(csproj),
|
||||
"-c",
|
||||
args.configuration,
|
||||
"-r",
|
||||
args.runtime,
|
||||
"--self-contained",
|
||||
"-p:PublishSingleFile=true",
|
||||
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
"-o",
|
||||
str(output_dir),
|
||||
]
|
||||
code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root))
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
binary_name = csproj.stem + (".exe" if args.runtime.startswith("win-") else "")
|
||||
binary_path = output_dir / binary_name
|
||||
if binary_path.exists():
|
||||
print(f"Published executable: {binary_path}")
|
||||
else:
|
||||
print(f"Publish completed. Output directory: {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--runtime", default="win-x64")
|
||||
parser.add_argument("--self-contained", action="store_true")
|
||||
parser.add_argument("--skip-web-assets", action="store_true")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj")
|
||||
parser.add_argument("--web-build-dir", default=None, help="Relative path to web build assets root")
|
||||
parser.add_argument("--output-dir", default="output/webgateway")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_dir = (repo_root / args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.project:
|
||||
csproj = (repo_root / args.project).resolve()
|
||||
else:
|
||||
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
||||
if csproj is None or not csproj.exists():
|
||||
print("Could not locate web gateway project. Pass --project <path/to/project.csproj>.")
|
||||
return 2
|
||||
|
||||
publish_args = [
|
||||
"publish",
|
||||
str(csproj),
|
||||
"-c",
|
||||
args.configuration,
|
||||
"-r",
|
||||
args.runtime,
|
||||
"--self-contained",
|
||||
"true" if args.self_contained else "false",
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
"-o",
|
||||
str(output_dir),
|
||||
]
|
||||
code = run("dotnet", publish_args, repo_root, env=dotnet_env(repo_root))
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
if not args.skip_web_assets:
|
||||
if args.web_build_dir:
|
||||
web_build_dir = (repo_root / args.web_build_dir).resolve()
|
||||
else:
|
||||
web_build_dir = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None)
|
||||
if web_build_dir is not None:
|
||||
web_build_dir = web_build_dir / "build"
|
||||
|
||||
if web_build_dir is None or not web_build_dir.exists():
|
||||
print("Web assets not found. Skip with --skip-web-assets or pass --web-build-dir.")
|
||||
else:
|
||||
web_out = output_dir / "wwwroot"
|
||||
web_out.mkdir(parents=True, exist_ok=True)
|
||||
for item in web_build_dir.iterdir():
|
||||
dst = web_out / item.name
|
||||
if item.is_dir():
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(item, dst)
|
||||
else:
|
||||
shutil.copy2(item, dst)
|
||||
print(f"Copied web assets: {web_out}")
|
||||
|
||||
print(f"Publish completed: {output_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run gateway in dev or output mode")
|
||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||
parser.add_argument("--urls", default="http://0.0.0.0:5180")
|
||||
parser.add_argument("--project-root", default=None, help="Runtime project root exposed via SDT_PROJECT_ROOT")
|
||||
parser.add_argument("--mode", choices=["Dev", "Output"], default="Dev")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--project", default=None, help="Gateway csproj path")
|
||||
parser.add_argument("--output-exe", default=None, help="Published gateway executable path")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
effective_project_root = Path(args.project_root).resolve() if args.project_root else repo_root
|
||||
if not effective_project_root.exists():
|
||||
print(f"Project root does not exist: {effective_project_root}")
|
||||
return 2
|
||||
|
||||
env = dotnet_env(repo_root)
|
||||
env["SDT_PROJECT_ROOT"] = str(effective_project_root)
|
||||
|
||||
if args.mode == "Output":
|
||||
exe_path = Path(args.output_exe).resolve() if args.output_exe else (repo_root / "output" / "webgateway" / ("webgateway.exe" if os.name == "nt" else "webgateway"))
|
||||
if not exe_path.exists():
|
||||
print(f"Output executable not found: {exe_path}")
|
||||
return 2
|
||||
return run(str(exe_path), ["--urls", args.urls], repo_root, env=env)
|
||||
|
||||
if args.project:
|
||||
csproj = (repo_root / args.project).resolve()
|
||||
else:
|
||||
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
||||
if csproj is None or not csproj.exists():
|
||||
print("Could not locate gateway project. Pass --project <path/to/project.csproj>.")
|
||||
return 2
|
||||
|
||||
run_args = [
|
||||
"run",
|
||||
"--project",
|
||||
str(csproj),
|
||||
"-c",
|
||||
args.configuration,
|
||||
"--no-launch-profile",
|
||||
"--urls",
|
||||
args.urls,
|
||||
"-p:RestoreIgnoreFailedSources=true",
|
||||
"-p:NuGetAudit=false",
|
||||
]
|
||||
return run("dotnet", run_args, repo_root, env=env)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,362 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, Iterable, List, Sequence
|
||||
|
||||
|
||||
PROXY_VARS = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
"GIT_HTTP_PROXY",
|
||||
"GIT_HTTPS_PROXY",
|
||||
"PIP_NO_INDEX",
|
||||
]
|
||||
|
||||
|
||||
def resolve_repo_root(start: str | None = None) -> pathlib.Path:
|
||||
base = pathlib.Path(start or os.getcwd()).resolve()
|
||||
|
||||
# Preferred marker for SDT-managed projects.
|
||||
for cur in [base, *base.parents]:
|
||||
cfg = cur / "devtool.json"
|
||||
if cfg.exists():
|
||||
hints = load_project_root_hints(cur)
|
||||
if not hints:
|
||||
return cur
|
||||
if any(_hint_matches(cur, hint) for hint in hints):
|
||||
return cur
|
||||
|
||||
# Fall back to git root when available.
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", str(base), "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
git_root = proc.stdout.strip()
|
||||
if git_root:
|
||||
return pathlib.Path(git_root).resolve()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return base
|
||||
|
||||
|
||||
def load_project_root_hints(repo_root: pathlib.Path) -> list[str]:
|
||||
cfg = repo_root / "devtool.json"
|
||||
if not cfg.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(cfg.read_text(encoding="utf-8"))
|
||||
hints = data.get("project", {}).get("rootHints", [])
|
||||
return [str(x) for x in hints if isinstance(x, str) and x.strip()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def ensure_dirs(paths: List[pathlib.Path]) -> None:
|
||||
for p in paths:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def clean_proxy_env(env: Dict[str, str]) -> None:
|
||||
for k in PROXY_VARS:
|
||||
env.pop(k, None)
|
||||
|
||||
|
||||
def dotnet_env(repo_root: pathlib.Path) -> Dict[str, str]:
|
||||
env = dict(os.environ)
|
||||
clean_proxy_env(env)
|
||||
dotnet_cli_home = repo_root / ".dotnet_home"
|
||||
nuget_packages = repo_root / ".nuget" / "packages"
|
||||
nuget_http_cache = repo_root / ".nuget" / "http-cache"
|
||||
ensure_dirs([dotnet_cli_home, nuget_packages, nuget_http_cache])
|
||||
env["DOTNET_CLI_HOME"] = str(dotnet_cli_home)
|
||||
env["NUGET_PACKAGES"] = str(nuget_packages)
|
||||
env["NUGET_HTTP_CACHE_PATH"] = str(nuget_http_cache)
|
||||
env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"
|
||||
env["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "0"
|
||||
env["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "0"
|
||||
env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
|
||||
env["NUGET_CERT_REVOCATION_MODE"] = "offline"
|
||||
return env
|
||||
|
||||
|
||||
def pip_env(repo_root: pathlib.Path) -> Dict[str, str]:
|
||||
env = dict(os.environ)
|
||||
clean_proxy_env(env)
|
||||
pip_cache = repo_root / ".pip" / "cache"
|
||||
pip_tmp = repo_root / ".tmp" / "pip-temp"
|
||||
ensure_dirs([pip_cache, pip_tmp])
|
||||
env["PIP_CACHE_DIR"] = str(pip_cache)
|
||||
env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
|
||||
env["PIP_DEFAULT_TIMEOUT"] = "30"
|
||||
env["PIP_RETRIES"] = "2"
|
||||
env["TEMP"] = str(pip_tmp)
|
||||
env["TMP"] = str(pip_tmp)
|
||||
return env
|
||||
|
||||
|
||||
def run(command: str, args: List[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> int:
|
||||
resolved = resolve_command(command)
|
||||
try:
|
||||
proc = subprocess.run([resolved, *args], cwd=str(cwd), env=env, check=False)
|
||||
return proc.returncode
|
||||
except FileNotFoundError:
|
||||
print(f"Command not found: {resolved}", file=sys.stderr)
|
||||
return 127
|
||||
|
||||
|
||||
def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: Dict[str, str] | None = None) -> tuple[int, str, str]:
|
||||
resolved = resolve_command(command)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[resolved, *args],
|
||||
cwd=str(cwd),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
except FileNotFoundError:
|
||||
return 127, "", f"Command not found: {resolved}"
|
||||
|
||||
|
||||
def resolve_command(command: str) -> str:
|
||||
if not command:
|
||||
return command
|
||||
|
||||
if os.name != "nt":
|
||||
return command
|
||||
|
||||
if any(sep in command for sep in ("\\", "/")):
|
||||
return command
|
||||
|
||||
if pathlib.Path(command).suffix:
|
||||
found = shutil.which(command)
|
||||
return found or command
|
||||
|
||||
candidates = []
|
||||
lowered = command.lower()
|
||||
if lowered in ("npm", "npx", "pnpm", "yarn", "tauri"):
|
||||
candidates.extend([f"{command}.cmd", f"{command}.exe", f"{command}.bat", command])
|
||||
else:
|
||||
candidates.append(command)
|
||||
|
||||
for c in candidates:
|
||||
found = _which_windows(c)
|
||||
if found:
|
||||
name = pathlib.Path(found).name.lower()
|
||||
if name in ("npm", "npx", "pnpm", "yarn", "tauri"):
|
||||
shim = pathlib.Path(found).with_name(name + ".cmd")
|
||||
if shim.exists():
|
||||
return str(shim)
|
||||
return found
|
||||
|
||||
if lowered in ("npm", "npx", "pnpm", "yarn"):
|
||||
node = _which_windows("node.exe") or _which_windows("node")
|
||||
if node:
|
||||
node_dir = pathlib.Path(node).parent
|
||||
shim = node_dir / f"{lowered}.cmd"
|
||||
if shim.exists():
|
||||
return str(shim)
|
||||
|
||||
return candidates[-1]
|
||||
|
||||
|
||||
def _hint_matches(root: pathlib.Path, hint: str) -> bool:
|
||||
h = hint.strip()
|
||||
if not h:
|
||||
return False
|
||||
|
||||
has_glob = any(ch in h for ch in ("*", "?", "["))
|
||||
if has_glob:
|
||||
# Match both anywhere in root and directly at root-level for common hints like "*.sln".
|
||||
if any(root.glob(h)):
|
||||
return True
|
||||
return any(root.rglob(h))
|
||||
|
||||
marker = root / h
|
||||
if marker.exists():
|
||||
return True
|
||||
|
||||
# If hint is just a filename marker, look bounded in tree.
|
||||
if not any(sep in h for sep in ("\\", "/")):
|
||||
return any(p.name == h for p in root.rglob(h))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _expand_windows_path_segment(segment: str) -> str:
|
||||
expanded = segment
|
||||
# Expand %VAR% tokens repeatedly for nested references.
|
||||
for _ in range(4):
|
||||
next_value = os.path.expandvars(expanded)
|
||||
if next_value == expanded:
|
||||
break
|
||||
expanded = next_value
|
||||
return expanded
|
||||
|
||||
|
||||
def _which_windows(command: str) -> str | None:
|
||||
found = shutil.which(command)
|
||||
if found:
|
||||
return found
|
||||
|
||||
if os.name != "nt":
|
||||
return None
|
||||
|
||||
path_value = os.environ.get("PATH", "")
|
||||
pathext = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD")
|
||||
exts = [e.lower() for e in pathext.split(";") if e]
|
||||
|
||||
has_ext = pathlib.Path(command).suffix != ""
|
||||
names = [command] if has_ext else [command, *(command + e.lower() for e in exts)]
|
||||
|
||||
for raw_segment in path_value.split(os.pathsep):
|
||||
segment = _expand_windows_path_segment(raw_segment.strip())
|
||||
if not segment:
|
||||
continue
|
||||
base = pathlib.Path(segment)
|
||||
for name in names:
|
||||
candidate = base / name
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def sha256_files(paths: Iterable[pathlib.Path]) -> str:
|
||||
h = hashlib.sha256()
|
||||
for p in paths:
|
||||
if not p.exists():
|
||||
continue
|
||||
h.update(p.read_bytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def first_existing(paths: Iterable[pathlib.Path]) -> pathlib.Path | None:
|
||||
for p in paths:
|
||||
if p.exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> pathlib.Path | None:
|
||||
if hints:
|
||||
for hint in hints:
|
||||
candidate = (repo_root / hint).resolve()
|
||||
if candidate.exists() and candidate.suffix.lower() == ".csproj":
|
||||
return candidate
|
||||
|
||||
csprojs = sorted(repo_root.rglob("*.csproj"))
|
||||
if not csprojs:
|
||||
return None
|
||||
if len(csprojs) == 1:
|
||||
return csprojs[0]
|
||||
return None
|
||||
|
||||
|
||||
def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> pathlib.Path | None:
|
||||
kws = [k.lower() for k in keywords]
|
||||
matches: list[pathlib.Path] = []
|
||||
for p in repo_root.rglob("*.csproj"):
|
||||
text = str(p).lower()
|
||||
if any(k in text for k in kws):
|
||||
matches.append(p)
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
return None
|
||||
|
||||
|
||||
def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> pathlib.Path | None:
|
||||
def _read_package_json(package_json: pathlib.Path) -> dict | None:
|
||||
if not package_json.exists():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(package_json.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, dict) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _has_scripts(package_json: pathlib.Path, names: Sequence[str]) -> bool:
|
||||
data = _read_package_json(package_json)
|
||||
if not data:
|
||||
return False
|
||||
scripts = data.get("scripts")
|
||||
if not isinstance(scripts, dict):
|
||||
return False
|
||||
for name in names:
|
||||
value = scripts.get(name)
|
||||
if isinstance(value, str) and value.strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_tauri_root(candidate_dir: pathlib.Path) -> bool:
|
||||
return (candidate_dir / "src-tauri" / "tauri.conf.json").exists() or (candidate_dir / "tauri.conf.json").exists()
|
||||
|
||||
def _iter_package_jsons() -> list[pathlib.Path]:
|
||||
excluded = {"node_modules", "dist", "build", ".git", ".sdt", ".venv", "venv", "bin", "obj"}
|
||||
found: list[pathlib.Path] = []
|
||||
for current_root, dirs, files in os.walk(repo_root):
|
||||
dirs[:] = [d for d in dirs if d not in excluded]
|
||||
if "package.json" in files:
|
||||
found.append(pathlib.Path(current_root) / "package.json")
|
||||
found.sort(key=lambda p: len(p.parts))
|
||||
return found
|
||||
|
||||
if preferred:
|
||||
p = (repo_root / preferred).resolve()
|
||||
package_json = p / "package.json"
|
||||
if package_json.exists():
|
||||
# Keep explicit preferred root only when it appears runnable for node workflows.
|
||||
if _is_tauri_root(p) or _has_scripts(package_json, ("build", "dev", "start", "test", "tauri")):
|
||||
return p
|
||||
|
||||
package_files = _iter_package_jsons()
|
||||
if not package_files:
|
||||
return None
|
||||
|
||||
# Strong preference: a tauri app root with tauri config and package.json.
|
||||
tauri_candidates = [p.parent for p in package_files if _is_tauri_root(p.parent)]
|
||||
if len(tauri_candidates) == 1:
|
||||
return tauri_candidates[0]
|
||||
if len(tauri_candidates) > 1:
|
||||
tauri_candidates.sort(key=lambda p: len(p.parts))
|
||||
return tauri_candidates[0]
|
||||
|
||||
runnable_candidates = [
|
||||
p.parent for p in package_files if _has_scripts(p, ("build", "dev", "start", "test", "tauri"))
|
||||
]
|
||||
if len(runnable_candidates) == 1:
|
||||
return runnable_candidates[0]
|
||||
if len(runnable_candidates) > 1:
|
||||
runnable_candidates.sort(key=lambda p: len(p.parts))
|
||||
return runnable_candidates[0]
|
||||
|
||||
# As a last fallback, return unique package root only.
|
||||
if len(package_files) == 1:
|
||||
return package_files[0].parent
|
||||
return None
|
||||
|
||||
|
||||
def newest_file(search_root: pathlib.Path, pattern: str) -> pathlib.Path | None:
|
||||
if not search_root.exists():
|
||||
return None
|
||||
files = [p for p in search_root.rglob(pattern) if p.is_file() and "\\obj\\" not in str(p).replace("/", "\\")]
|
||||
if not files:
|
||||
return None
|
||||
files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
return files[0]
|
||||
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from script_common import newest_file, resolve_repo_root
|
||||
|
||||
|
||||
def copy_tree_contents(src: Path, dst: Path) -> None:
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
for item in src.iterdir():
|
||||
target = dst / item.name
|
||||
if item.is_dir():
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
shutil.copytree(item, target)
|
||||
else:
|
||||
shutil.copy2(item, target)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Sync newest built assets into output folder")
|
||||
parser.add_argument("--repo-root", default=None)
|
||||
parser.add_argument("--output-dir", default="output")
|
||||
parser.add_argument("--web-build-dir", default=None, help="Path to web build output")
|
||||
parser.add_argument("--sidecar-bin-dir", default=None, help="Path to sidecar bin root")
|
||||
parser.add_argument("--gateway-bin-dir", default=None, help="Path to gateway bin root")
|
||||
parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
output_dir = (repo_root / args.output_dir).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
web_build = (repo_root / args.web_build_dir).resolve() if args.web_build_dir else None
|
||||
if web_build is None:
|
||||
web_build = next((p for p in repo_root.rglob("build") if (p.parent / "package.json").exists()), None)
|
||||
if web_build is not None and web_build.exists():
|
||||
web_out = output_dir / "webgateway" / "wwwroot"
|
||||
copy_tree_contents(web_build, web_out)
|
||||
print(f"Synced web assets -> {web_out}")
|
||||
|
||||
sidecar_bin = (repo_root / args.sidecar_bin_dir).resolve() if args.sidecar_bin_dir else None
|
||||
if sidecar_bin is None:
|
||||
sidecar_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "sidecar" in str(p).lower()), None)
|
||||
sidecar_bin = sidecar_proj / "bin" if sidecar_proj else None
|
||||
if sidecar_bin is not None:
|
||||
sidecar_pattern = "*.exe" if os.name == "nt" else "*"
|
||||
sidecar_exe = newest_file(sidecar_bin, sidecar_pattern)
|
||||
if sidecar_exe is not None:
|
||||
copy_tree_contents(sidecar_exe.parent, output_dir)
|
||||
print(f"Synced sidecar -> {output_dir}")
|
||||
|
||||
gateway_bin = (repo_root / args.gateway_bin_dir).resolve() if args.gateway_bin_dir else None
|
||||
if gateway_bin is None:
|
||||
gateway_proj = next((p.parent for p in repo_root.rglob("*.csproj") if "gateway" in str(p).lower()), None)
|
||||
gateway_bin = gateway_proj / "bin" if gateway_proj else None
|
||||
if gateway_bin is not None:
|
||||
gateway_pattern = "*.exe" if os.name == "nt" else "*"
|
||||
gw_exe = newest_file(gateway_bin, gateway_pattern)
|
||||
if gw_exe is not None:
|
||||
gw_out = output_dir / "webgateway"
|
||||
copy_tree_contents(gw_exe.parent, gw_out)
|
||||
print(f"Synced gateway -> {gw_out}")
|
||||
|
||||
tauri_target = (repo_root / args.tauri_target_dir).resolve() if args.tauri_target_dir else None
|
||||
if tauri_target is None:
|
||||
tauri_target = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None)
|
||||
tauri_target = tauri_target / "target" if tauri_target else None
|
||||
if tauri_target is not None:
|
||||
app_exe = newest_file(tauri_target, "*.exe")
|
||||
if app_exe is not None:
|
||||
shutil.copy2(app_exe, output_dir / app_exe.name)
|
||||
print(f"Synced desktop app ({app_exe.name}) -> {output_dir}")
|
||||
|
||||
print("Sync complete.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from script_common import resolve_command, resolve_repo_root
|
||||
|
||||
|
||||
def load_config(project_root: pathlib.Path) -> dict:
|
||||
config_path = project_root / "devtool.json"
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"devtool.json not found at: {config_path}")
|
||||
return json.loads(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def iter_workflows(config: dict, selected: Optional[set[str]]) -> List[dict]:
|
||||
workflows = config.get("workflows", [])
|
||||
if not isinstance(workflows, list):
|
||||
return []
|
||||
normalized: List[dict] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)]
|
||||
if selected:
|
||||
normalized = [w for w in normalized if w["id"] in selected]
|
||||
return normalized
|
||||
|
||||
|
||||
def is_command_available(command: str) -> bool:
|
||||
resolved = resolve_command(command)
|
||||
if pathlib.Path(resolved).is_file():
|
||||
return True
|
||||
return shutil.which(resolved) is not None
|
||||
|
||||
|
||||
def resolve_script_arg(project_root: pathlib.Path, working_dir: pathlib.Path, arg: str) -> pathlib.Path:
|
||||
p = pathlib.Path(arg)
|
||||
if p.is_absolute():
|
||||
return p
|
||||
a = working_dir / p
|
||||
if a.exists():
|
||||
return a
|
||||
b = project_root / p
|
||||
return b
|
||||
|
||||
|
||||
def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
|
||||
result = {
|
||||
"workflowId": workflow.get("id"),
|
||||
"ok": True,
|
||||
"issues": [],
|
||||
"steps": [],
|
||||
}
|
||||
|
||||
for step in workflow.get("steps", []):
|
||||
if not isinstance(step, dict):
|
||||
continue
|
||||
step_id = step.get("id", "<unknown>")
|
||||
step_result = {"stepId": step_id, "ok": True, "issues": []}
|
||||
|
||||
working_dir_rel = step.get("workingDir") or "."
|
||||
working_dir = (project_root / working_dir_rel).resolve()
|
||||
if not working_dir.exists():
|
||||
step_result["ok"] = False
|
||||
step_result["issues"].append(f"workingDir_not_found:{working_dir}")
|
||||
|
||||
command = step.get("command")
|
||||
args = step.get("args") or []
|
||||
action = step.get("action")
|
||||
|
||||
if isinstance(command, str) and command.strip():
|
||||
if not is_command_available(command):
|
||||
step_result["ok"] = False
|
||||
step_result["issues"].append(f"command_not_found:{command}")
|
||||
|
||||
if command.lower() in ("python", "py", "python3", "python.exe", "py.exe"):
|
||||
if args and isinstance(args[0], str) and args[0].endswith(".py"):
|
||||
script_path = resolve_script_arg(project_root, working_dir, args[0])
|
||||
if not script_path.exists():
|
||||
step_result["ok"] = False
|
||||
step_result["issues"].append(f"python_script_not_found:{script_path}")
|
||||
|
||||
if isinstance(action, str) and action.strip():
|
||||
# Action-based steps still require workingDir existence for reliable execution.
|
||||
if not working_dir.exists():
|
||||
step_result["ok"] = False
|
||||
step_result["issues"].append("action_working_dir_not_found")
|
||||
|
||||
if not step_result["ok"]:
|
||||
result["ok"] = False
|
||||
result["issues"].extend(step_result["issues"])
|
||||
|
||||
result["steps"].append(step_result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]:
|
||||
attempts: List[List[str]] = []
|
||||
attempts.append(["sdt"])
|
||||
if sys.platform.startswith("win"):
|
||||
attempts.append(["sdt.exe"])
|
||||
|
||||
local_exe = repo_root / ("sdt.exe" if sys.platform.startswith("win") else "sdt")
|
||||
if local_exe.exists():
|
||||
attempts.append([str(local_exe)])
|
||||
|
||||
devtool_csproj = repo_root / "DevTool.csproj"
|
||||
if devtool_csproj.exists():
|
||||
attempts.append(["dotnet", "run", "--project", str(devtool_csproj), "--"])
|
||||
|
||||
# Preserve order but dedupe exact attempts.
|
||||
seen = set()
|
||||
unique: List[List[str]] = []
|
||||
for a in attempts:
|
||||
key = tuple(a)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(a)
|
||||
return unique
|
||||
|
||||
|
||||
def try_run_sdt(
|
||||
repo_root: pathlib.Path,
|
||||
command_args: Sequence[str],
|
||||
timeout_seconds: int,
|
||||
) -> Tuple[Optional[subprocess.CompletedProcess], Optional[str]]:
|
||||
errors: List[str] = []
|
||||
for base in sdt_attempts(repo_root):
|
||||
cmd = [*base, *command_args]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
cwd=str(repo_root),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
return proc, " ".join(cmd)
|
||||
except FileNotFoundError:
|
||||
errors.append(f"not_found:{' '.join(cmd)}")
|
||||
except subprocess.TimeoutExpired:
|
||||
errors.append(f"timeout:{' '.join(cmd)}")
|
||||
return None, "; ".join(errors) if errors else "no_sdt_attempts"
|
||||
|
||||
|
||||
def parse_headless_summary(stdout: str) -> Optional[Dict[str, Any]]:
|
||||
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
||||
for line in reversed(lines):
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(payload, dict) and "runId" in payload and "success" in payload:
|
||||
return payload
|
||||
return None
|
||||
|
||||
|
||||
def execute_check_workflow(
|
||||
repo_root: pathlib.Path,
|
||||
project_root: pathlib.Path,
|
||||
workflow_id: str,
|
||||
env_profile: Optional[str],
|
||||
timeout_seconds: int,
|
||||
) -> dict:
|
||||
args = [
|
||||
"run",
|
||||
workflow_id,
|
||||
"--json",
|
||||
"--project-root",
|
||||
str(project_root),
|
||||
"--non-interactive",
|
||||
]
|
||||
if env_profile:
|
||||
args.extend(["--env-profile", env_profile])
|
||||
|
||||
proc, attempted = try_run_sdt(repo_root, args, timeout_seconds)
|
||||
if proc is None:
|
||||
return {
|
||||
"workflowId": workflow_id,
|
||||
"ok": False,
|
||||
"attempted": attempted,
|
||||
"exitCode": None,
|
||||
"stopReason": "sdt_not_runnable",
|
||||
"message": attempted,
|
||||
}
|
||||
|
||||
summary = parse_headless_summary(proc.stdout)
|
||||
if summary is None:
|
||||
return {
|
||||
"workflowId": workflow_id,
|
||||
"ok": False,
|
||||
"attempted": attempted,
|
||||
"exitCode": proc.returncode,
|
||||
"stopReason": "missing_summary",
|
||||
"message": (proc.stderr or proc.stdout).strip(),
|
||||
}
|
||||
|
||||
return {
|
||||
"workflowId": workflow_id,
|
||||
"ok": bool(summary.get("success", False)),
|
||||
"attempted": attempted,
|
||||
"exitCode": summary.get("exitCode"),
|
||||
"stopReason": summary.get("stopReason"),
|
||||
"message": summary.get("message"),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Verify SDT workflow routes (static path checks + optional headless execution)."
|
||||
)
|
||||
parser.add_argument("--repo-root", default=None, help="Repo root containing DevTool.csproj")
|
||||
parser.add_argument("--project-root", default=".", help="Project root containing devtool.json")
|
||||
parser.add_argument("--workflow", action="append", default=[], help="Workflow id to verify (repeatable)")
|
||||
parser.add_argument("--execute", action="store_true", help="Also run each workflow via `sdt run ... --json`")
|
||||
parser.add_argument("--env-profile", default=None)
|
||||
parser.add_argument("--timeout-seconds", type=int, default=600)
|
||||
parser.add_argument("--output-json", default=None, help="Write full report JSON to file")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = resolve_repo_root(args.repo_root)
|
||||
project_root = (repo_root / args.project_root).resolve() if not pathlib.Path(args.project_root).is_absolute() else pathlib.Path(args.project_root).resolve()
|
||||
selected = set(args.workflow) if args.workflow else None
|
||||
|
||||
config = load_config(project_root)
|
||||
workflows = iter_workflows(config, selected)
|
||||
if not workflows:
|
||||
print("No workflows selected/found.")
|
||||
return 2
|
||||
|
||||
static_results = [static_check_workflow(project_root, w) for w in workflows]
|
||||
execute_results: List[dict] = []
|
||||
if args.execute:
|
||||
for w in workflows:
|
||||
wid = w["id"]
|
||||
execute_results.append(
|
||||
execute_check_workflow(
|
||||
repo_root=repo_root,
|
||||
project_root=project_root,
|
||||
workflow_id=wid,
|
||||
env_profile=args.env_profile,
|
||||
timeout_seconds=args.timeout_seconds,
|
||||
)
|
||||
)
|
||||
|
||||
static_failures = [r for r in static_results if not r["ok"]]
|
||||
exec_failures = [r for r in execute_results if not r["ok"]]
|
||||
|
||||
report = {
|
||||
"repoRoot": str(repo_root),
|
||||
"projectRoot": str(project_root),
|
||||
"totalWorkflows": len(workflows),
|
||||
"static": {
|
||||
"checked": len(static_results),
|
||||
"failed": len(static_failures),
|
||||
"results": static_results,
|
||||
},
|
||||
"execute": {
|
||||
"enabled": args.execute,
|
||||
"checked": len(execute_results),
|
||||
"failed": len(exec_failures),
|
||||
"results": execute_results,
|
||||
},
|
||||
}
|
||||
|
||||
if args.output_json:
|
||||
out_path = pathlib.Path(args.output_json)
|
||||
if not out_path.is_absolute():
|
||||
out_path = repo_root / out_path
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"Report written: {out_path}")
|
||||
|
||||
print(f"Static checks: {len(static_results)} workflow(s), failures={len(static_failures)}")
|
||||
if args.execute:
|
||||
print(f"Execution checks: {len(execute_results)} workflow(s), failures={len(exec_failures)}")
|
||||
|
||||
if static_failures:
|
||||
print("\nStatic failures:")
|
||||
for f in static_failures:
|
||||
print(f"- {f['workflowId']}: {', '.join(f['issues'])}")
|
||||
|
||||
if exec_failures:
|
||||
print("\nExecution failures:")
|
||||
for f in exec_failures:
|
||||
print(f"- {f['workflowId']}: stopReason={f.get('stopReason')} message={f.get('message')}")
|
||||
|
||||
return 1 if static_failures or exec_failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@ -1,108 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v10.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v10.0": {
|
||||
"sdt/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Engine": "1.0.0",
|
||||
"DevTool.Host.Bridge": "1.0.0",
|
||||
"DevTool.Host.Tui": "1.0.0",
|
||||
"DevTool.Runtime": "1.0.0",
|
||||
"Spectre.Console": "0.49.1"
|
||||
},
|
||||
"runtime": {
|
||||
"sdt.dll": {}
|
||||
}
|
||||
},
|
||||
"Spectre.Console/0.49.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Spectre.Console.dll": {
|
||||
"assemblyVersion": "0.0.0.0",
|
||||
"fileVersion": "0.49.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Engine/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Runtime": "1.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"DevTool.Engine.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Host.Bridge/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Engine": "1.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"DevTool.Host.Bridge.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Host.Tui/1.0.0": {
|
||||
"dependencies": {
|
||||
"DevTool.Engine": "1.0.0",
|
||||
"DevTool.Runtime": "1.0.0",
|
||||
"Spectre.Console": "0.49.1"
|
||||
},
|
||||
"runtime": {
|
||||
"DevTool.Host.Tui.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DevTool.Runtime/1.0.0": {
|
||||
"runtime": {
|
||||
"DevTool.Runtime.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"sdt/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Spectre.Console/0.49.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA==",
|
||||
"path": "spectre.console/0.49.1",
|
||||
"hashPath": "spectre.console.0.49.1.nupkg.sha512"
|
||||
},
|
||||
"DevTool.Engine/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DevTool.Host.Bridge/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DevTool.Host.Tui/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DevTool.Runtime/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,13 +0,0 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net10.0",
|
||||
"framework": {
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "10.0.0"
|
||||
},
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -676,7 +676,7 @@
|
||||
],
|
||||
"values": {
|
||||
"SDT_ENV_PROFILE": "ci",
|
||||
"CI": "false",
|
||||
"CI": "true",
|
||||
"SDT_LOG_LEVEL": "warning"
|
||||
}
|
||||
},
|
||||
@ -783,4 +783,4 @@
|
||||
"bundleOnFailure": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user