several bug fixes,
This commit is contained in:
parent
38d0afac68
commit
104c8eab91
14
Program.cs
14
Program.cs
@ -36,13 +36,13 @@ try
|
|||||||
|
|
||||||
if (projectResult is null)
|
if (projectResult is null)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine($"[bold {Theme.Red}]SDT:[/] [{Theme.Amber}]No devtool.json found[/] in current directory or any parent.");
|
AnsiConsole.MarkupLine($"[bold {Theme.Red}]SDT:[/] [{Theme.Amber}]No SDT project config found[/] (expected `sdtconfig-*.json` or `devtool.json`) in current directory or any parent.");
|
||||||
var bootstrap = AnsiConsole.Confirm(
|
var bootstrap = AnsiConsole.Confirm(
|
||||||
$"[{Theme.Amber}]Generate a default devtool.json for this project now?[/]",
|
$"[{Theme.Amber}]Generate a default SDT project config for this project now?[/]",
|
||||||
defaultValue: true);
|
defaultValue: true);
|
||||||
if (!bootstrap)
|
if (!bootstrap)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine(Theme.Faint("Create a devtool.json in your project root to get started."));
|
AnsiConsole.MarkupLine(Theme.Faint("Create an sdtconfig-<ProjectName>.json (or devtool.json) in your project root to get started."));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,10 +53,10 @@ try
|
|||||||
|
|
||||||
var generated = ConfigBootstrapper.BuildDefaultConfig(scan);
|
var generated = ConfigBootstrapper.BuildDefaultConfig(scan);
|
||||||
var preview = ConfigBootstrapper.ToJson(generated);
|
var preview = ConfigBootstrapper.ToJson(generated);
|
||||||
AnsiConsole.Write(new Panel(Markup.Escape(preview)).Header("Generated devtool.json preview").BorderStyle(Theme.DimStyle));
|
AnsiConsole.Write(new Panel(Markup.Escape(preview)).Header("Generated project config preview").BorderStyle(Theme.DimStyle));
|
||||||
|
|
||||||
var confirmWrite = AnsiConsole.Confirm(
|
var confirmWrite = AnsiConsole.Confirm(
|
||||||
$"[{Theme.Amber}]Write generated devtool.json to {scan.ProjectRoot}?[/]",
|
$"[{Theme.Amber}]Write generated project config to {scan.ProjectRoot}?[/]",
|
||||||
defaultValue: true);
|
defaultValue: true);
|
||||||
if (!confirmWrite)
|
if (!confirmWrite)
|
||||||
return 1;
|
return 1;
|
||||||
@ -110,7 +110,7 @@ try
|
|||||||
}
|
}
|
||||||
if (loaded is null)
|
if (loaded is null)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine(Theme.Fail($"No devtool.json found at: {result.NewProjectRoot}"));
|
AnsiConsole.MarkupLine(Theme.Fail($"No SDT project config found at: {result.NewProjectRoot}"));
|
||||||
AnsiConsole.MarkupLine(Theme.Faint("Press any key to stay on current project..."));
|
AnsiConsole.MarkupLine(Theme.Faint("Press any key to stay on current project..."));
|
||||||
Console.ReadKey(intercept: true);
|
Console.ReadKey(intercept: true);
|
||||||
continue;
|
continue;
|
||||||
@ -289,7 +289,7 @@ static async Task<int> RunHeadlessAsync(IReadOnlyList<string> cliArgs, string ki
|
|||||||
var loaded = ConfigLoader.FindAndLoad(startDir);
|
var loaded = ConfigLoader.FindAndLoad(startDir);
|
||||||
if (loaded is null)
|
if (loaded is null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("{\"success\":false,\"message\":\"No devtool.json found for headless command.\"}");
|
Console.WriteLine("{\"success\":false,\"message\":\"No SDT project config found (expected sdtconfig-*.json or devtool.json).\"}");
|
||||||
return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed);
|
return ExitCodeMapper.FromResult(false, ExecutionStopReason.ValidationFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
## In Progress (next focus)
|
## In Progress (next focus)
|
||||||
|
|
||||||
- [ ] Execute full OS matrix verification on Windows/Linux/macOS runners
|
- [ ] Execute full OS matrix verification on Linux runners
|
||||||
- [ ] Native GUI shell over headless core services (Tauri-first in v1.x; Avalonia re-evaluate later)
|
- [ ] Native GUI shell over headless core services (Tauri-first in v1.x; Avalonia re-evaluate later)
|
||||||
- [x] GUI parity phase 1 bridge foundation shipped (`sdt bridge --stdio` + `DevTool.Host.Bridge`)
|
- [x] GUI parity phase 1 bridge foundation shipped (`sdt bridge --stdio` + `DevTool.Host.Bridge`)
|
||||||
- [x] GUI debug + failure UX slice shipped (debug run, failure card, run context/lifecycle panel)
|
- [x] GUI debug + failure UX slice shipped (debug run, failure card, run context/lifecycle panel)
|
||||||
@ -66,7 +66,6 @@
|
|||||||
- [x] Bootstrap first Tauri command bridge: `sdt workspace scan --json`
|
- [x] Bootstrap first Tauri command bridge: `sdt workspace scan --json`
|
||||||
- [x] Structural refactor to domain-separated projects under `src/` (`DevTool.Engine`, `DevTool.Runtime`, `DevTool.Host.Tui`)
|
- [x] Structural refactor to domain-separated projects under `src/` (`DevTool.Engine`, `DevTool.Runtime`, `DevTool.Host.Tui`)
|
||||||
- [x] Add second Tauri bridge command: `sdt run <workflowId> --json` with live stream panel
|
- [x] Add second Tauri bridge command: `sdt run <workflowId> --json` with live stream panel
|
||||||
- [ ] Remove legacy PowerShell wrappers in v2
|
|
||||||
- [x] Add workspace project inventory model (all `.slnx/.sln/.csproj`) for GUI/TUI multi-project selector
|
- [x] Add workspace project inventory model (all `.slnx/.sln/.csproj`) for GUI/TUI multi-project selector
|
||||||
- [x] Expand GUI command palette coverage across workspace/run/setup/history/events/favorites actions
|
- [x] Expand GUI command palette coverage across workspace/run/setup/history/events/favorites actions
|
||||||
- [x] Add full GUI env var definition editor parity (`env[]` model editing with validation)
|
- [x] Add full GUI env var definition editor parity (`env[]` model editing with validation)
|
||||||
@ -88,7 +87,7 @@
|
|||||||
|
|
||||||
- [x] Add project-type matrix coverage tests (`dotnet`, `node/npm`, `tauri/cargo`)
|
- [x] Add project-type matrix coverage tests (`dotnet`, `node/npm`, `tauri/cargo`)
|
||||||
- [x] Add deterministic headless stop-reason/exit-code tests
|
- [x] Add deterministic headless stop-reason/exit-code tests
|
||||||
- [ ] Execute full OS matrix verification on Windows/Linux/macOS runners
|
- [ ] Execute Full OS matrix verification on Linux runners because the linux runner in theory should be able to be able to build/publish/release to windows/linux/ macos (but fuck apple.)
|
||||||
- [x] Publish reliability matrix runbook + results artifact in docs
|
- [x] Publish reliability matrix runbook + results artifact in docs
|
||||||
- Blocked in current local workspace: no `.git` repo context and no `gh` CLI/auth; execute from Git-connected checkout.
|
- Blocked in current local workspace: no `.git` repo context and no `gh` CLI/auth; execute from Git-connected checkout.
|
||||||
- v1.4 policy: ship Windows/Linux, macOS delegated with checklist in `docs/matrix-status.md`.
|
- v1.4 policy: ship Windows/Linux, macOS delegated with checklist in `docs/matrix-status.md`.
|
||||||
|
|||||||
487
devtool.json
487
devtool.json
@ -1,487 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "DevTool-master",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"targets": [],
|
|
||||||
"workflows": [
|
|
||||||
{
|
|
||||||
"id": "sidecar",
|
|
||||||
"label": "Publish Sidecar",
|
|
||||||
"description": "Publish sidecar service",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "sidecar:run",
|
|
||||||
"label": "python scripts/publish-sidecar.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-sidecar.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "web",
|
|
||||||
"label": "Build Web UI",
|
|
||||||
"description": "Build frontend assets",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "web:run",
|
|
||||||
"label": "python scripts/publish-app.py --target web",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-app.py",
|
|
||||||
"--target",
|
|
||||||
"web"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tauri",
|
|
||||||
"label": "Build Tauri Desktop App",
|
|
||||||
"description": "Build desktop binary",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [
|
|
||||||
"sidecar"
|
|
||||||
],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "tauri:run",
|
|
||||||
"label": "python scripts/publish-app.py --target tauri --tauri-bundles none",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-app.py",
|
|
||||||
"--target",
|
|
||||||
"tauri",
|
|
||||||
"--tauri-bundles",
|
|
||||||
"none"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "webgateway",
|
|
||||||
"label": "Publish WebGateway",
|
|
||||||
"description": "Publish ASP.NET gateway",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [
|
|
||||||
"web"
|
|
||||||
],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "webgateway:run",
|
|
||||||
"label": "python scripts/publish-webgateway.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-webgateway.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sync-output",
|
|
||||||
"label": "Sync Output",
|
|
||||||
"description": "Sync newest artifacts to output",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "sync-output:run",
|
|
||||||
"label": "python scripts/sync-output.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/sync-output.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "stage-output",
|
|
||||||
"label": "Stage Output Bundle",
|
|
||||||
"description": "Publish and stage distributable output",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "stage-output:run",
|
|
||||||
"label": "python scripts/publish-output.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-output.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "run-gateway-dev",
|
|
||||||
"label": "Run WebGateway Server (Dev)",
|
|
||||||
"description": "Run gateway in development mode",
|
|
||||||
"group": "Dev",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "run-gateway-dev:run",
|
|
||||||
"label": "python scripts/run-webgateway.py --mode Dev",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/run-webgateway.py",
|
|
||||||
"--mode",
|
|
||||||
"Dev"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "build",
|
|
||||||
"label": "Build",
|
|
||||||
"description": "Build detected project stacks",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-build",
|
|
||||||
"label": "dotnet build",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "dotnet-build",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-build",
|
|
||||||
"label": "npm run build",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "npm-build",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "deps-refresh",
|
|
||||||
"label": "Refresh Dependencies",
|
|
||||||
"description": "Restore/install dependency stacks",
|
|
||||||
"group": "Deps",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-restore",
|
|
||||||
"label": "dotnet restore",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "dotnet-restore",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-ci",
|
|
||||||
"label": "npm ci",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "npm-ci",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test",
|
|
||||||
"label": "Run Tests",
|
|
||||||
"description": "Run detected test stacks",
|
|
||||||
"group": "Test",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-test",
|
|
||||||
"label": "dotnet test",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "dotnet-test",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-test",
|
|
||||||
"label": "npm test",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "npm-test",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-pytest",
|
|
||||||
"label": "python -m pytest",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "python-pytest",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
{
|
|
||||||
"key": "SDT_LOG_LEVEL",
|
|
||||||
"description": "CLI log verbosity",
|
|
||||||
"default": "information",
|
|
||||||
"options": [
|
|
||||||
"trace",
|
|
||||||
"debug",
|
|
||||||
"information",
|
|
||||||
"warning",
|
|
||||||
"error",
|
|
||||||
"critical"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "SDT_ENV_PROFILE",
|
|
||||||
"description": "Active SDT runtime environment profile",
|
|
||||||
"default": "dev",
|
|
||||||
"options": [
|
|
||||||
"dev",
|
|
||||||
"ci",
|
|
||||||
"release"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"envProfiles": {
|
|
||||||
"active": "dev",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"id": "dev",
|
|
||||||
"description": "Local development defaults",
|
|
||||||
"inherits": [],
|
|
||||||
"values": {
|
|
||||||
"SDT_ENV_PROFILE": "dev",
|
|
||||||
"SDT_LOG_LEVEL": "information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ci",
|
|
||||||
"description": "Continuous integration defaults",
|
|
||||||
"inherits": [
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"values": {
|
|
||||||
"SDT_ENV_PROFILE": "ci",
|
|
||||||
"CI": "true",
|
|
||||||
"SDT_LOG_LEVEL": "warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "release",
|
|
||||||
"description": "Release build defaults",
|
|
||||||
"inherits": [
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"values": {
|
|
||||||
"SDT_ENV_PROFILE": "release",
|
|
||||||
"SDT_LOG_LEVEL": "warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"toolchains": {
|
|
||||||
"python": {
|
|
||||||
"executable": "python",
|
|
||||||
"windowsExecutable": "py",
|
|
||||||
"launcherVersion": null,
|
|
||||||
"venvDir": ".venv",
|
|
||||||
"profiles": [],
|
|
||||||
"pipScript": null
|
|
||||||
},
|
|
||||||
"node": {
|
|
||||||
"packageManager": "npm",
|
|
||||||
"workingDir": "."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tooling": {
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"tool": "dotnet",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "node",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "npm",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"project": {
|
|
||||||
"type": "polyglot",
|
|
||||||
"rootHints": [
|
|
||||||
"*.sln",
|
|
||||||
"package.json",
|
|
||||||
"scripts"
|
|
||||||
],
|
|
||||||
"artifacts": [
|
|
||||||
"bin",
|
|
||||||
"obj",
|
|
||||||
".sdt/debug"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-run",
|
|
||||||
"label": "Run .NET app",
|
|
||||||
"type": "dotnet",
|
|
||||||
"command": "dotnet",
|
|
||||||
"args": [
|
|
||||||
"run"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"env": {},
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "dotnet",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attach": {
|
|
||||||
"kind": "manual",
|
|
||||||
"port": null,
|
|
||||||
"processName": null,
|
|
||||||
"note": "Attach your IDE debugger to the running dotnet process."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-dev",
|
|
||||||
"label": "Run npm dev server",
|
|
||||||
"type": "node",
|
|
||||||
"command": "npm",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"env": {},
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "node",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "npm",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attach": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"diagnostics": {
|
|
||||||
"enabled": true,
|
|
||||||
"outputDir": ".sdt/debug",
|
|
||||||
"includeAllEnv": false,
|
|
||||||
"captureEnvKeys": [
|
|
||||||
"SDT_LOG_LEVEL",
|
|
||||||
"DOTNET_CLI_HOME",
|
|
||||||
"NUGET_PACKAGES",
|
|
||||||
"PIP_CACHE_DIR",
|
|
||||||
"NVM_HOME",
|
|
||||||
"NVM_SYMLINK"
|
|
||||||
],
|
|
||||||
"redactSensitive": true,
|
|
||||||
"sensitiveKeyPatterns": [
|
|
||||||
"TOKEN",
|
|
||||||
"SECRET",
|
|
||||||
"PASSWORD",
|
|
||||||
"PWD",
|
|
||||||
"CREDENTIAL",
|
|
||||||
"API_KEY",
|
|
||||||
"ACCESS_KEY",
|
|
||||||
"PRIVATE_KEY"
|
|
||||||
],
|
|
||||||
"redactionAllowKeys": [],
|
|
||||||
"bundleOnFailure": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,477 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "DevTool-master",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"targets": [],
|
|
||||||
"workflows": [
|
|
||||||
{
|
|
||||||
"id": "sidecar",
|
|
||||||
"label": "Publish Sidecar",
|
|
||||||
"description": "Publish sidecar service",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "sidecar:run",
|
|
||||||
"label": "python scripts/publish-sidecar.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-sidecar.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "web",
|
|
||||||
"label": "Build Web UI",
|
|
||||||
"description": "Build frontend assets",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "web:run",
|
|
||||||
"label": "python scripts/publish-app.py --target web",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-app.py",
|
|
||||||
"--target",
|
|
||||||
"web"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tauri",
|
|
||||||
"label": "Build Tauri Desktop App",
|
|
||||||
"description": "Build desktop binary",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [
|
|
||||||
"sidecar"
|
|
||||||
],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "tauri:run",
|
|
||||||
"label": "python scripts/publish-app.py --target tauri --tauri-bundles none",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-app.py",
|
|
||||||
"--target",
|
|
||||||
"tauri",
|
|
||||||
"--tauri-bundles",
|
|
||||||
"none"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "webgateway",
|
|
||||||
"label": "Publish WebGateway",
|
|
||||||
"description": "Publish ASP.NET gateway",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [
|
|
||||||
"web"
|
|
||||||
],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "webgateway:run",
|
|
||||||
"label": "python scripts/publish-webgateway.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-webgateway.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sync-output",
|
|
||||||
"label": "Sync Output",
|
|
||||||
"description": "Sync newest artifacts to output",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "sync-output:run",
|
|
||||||
"label": "python scripts/sync-output.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/sync-output.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "stage-output",
|
|
||||||
"label": "Stage Output Bundle",
|
|
||||||
"description": "Publish and stage distributable output",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "stage-output:run",
|
|
||||||
"label": "python scripts/publish-output.py",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/publish-output.py"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "run-gateway-dev",
|
|
||||||
"label": "Run WebGateway Server (Dev)",
|
|
||||||
"description": "Run gateway in development mode",
|
|
||||||
"group": "Dev",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "run-gateway-dev:run",
|
|
||||||
"label": "python scripts/run-webgateway.py --mode Dev",
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"scripts/run-webgateway.py",
|
|
||||||
"--mode",
|
|
||||||
"Dev"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": null,
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "build",
|
|
||||||
"label": "Build",
|
|
||||||
"description": "Build detected project stacks",
|
|
||||||
"group": "Build",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-build",
|
|
||||||
"label": "dotnet build",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "dotnet-build",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-build",
|
|
||||||
"label": "npm run build",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "npm-build",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "deps-refresh",
|
|
||||||
"label": "Refresh Dependencies",
|
|
||||||
"description": "Restore/install dependency stacks",
|
|
||||||
"group": "Deps",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-restore",
|
|
||||||
"label": "dotnet restore",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "dotnet-restore",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-ci",
|
|
||||||
"label": "npm ci",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "npm-ci",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "test",
|
|
||||||
"label": "Run Tests",
|
|
||||||
"description": "Run detected test stacks",
|
|
||||||
"group": "Test",
|
|
||||||
"dependsOn": [],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-test",
|
|
||||||
"label": "dotnet test",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "dotnet-test",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-test",
|
|
||||||
"label": "npm test",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "npm-test",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "python-pytest",
|
|
||||||
"label": "python -m pytest",
|
|
||||||
"command": null,
|
|
||||||
"args": [],
|
|
||||||
"workingDir": ".",
|
|
||||||
"action": "python-pytest",
|
|
||||||
"actionArgs": [],
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"env": [
|
|
||||||
{
|
|
||||||
"key": "SDT_LOG_LEVEL",
|
|
||||||
"description": "CLI log verbosity",
|
|
||||||
"default": "information",
|
|
||||||
"options": [
|
|
||||||
"trace",
|
|
||||||
"debug",
|
|
||||||
"information",
|
|
||||||
"warning",
|
|
||||||
"error",
|
|
||||||
"critical"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"envProfiles": {
|
|
||||||
"active": "dev",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"id": "dev",
|
|
||||||
"description": "Local development defaults",
|
|
||||||
"inherits": [],
|
|
||||||
"values": {
|
|
||||||
"SDT_ENV_PROFILE": "dev",
|
|
||||||
"SDT_LOG_LEVEL": "information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ci",
|
|
||||||
"description": "Continuous integration defaults",
|
|
||||||
"inherits": [
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"values": {
|
|
||||||
"SDT_ENV_PROFILE": "ci",
|
|
||||||
"CI": "true",
|
|
||||||
"SDT_LOG_LEVEL": "warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "release",
|
|
||||||
"description": "Release build defaults",
|
|
||||||
"inherits": [
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"values": {
|
|
||||||
"SDT_ENV_PROFILE": "release",
|
|
||||||
"SDT_LOG_LEVEL": "warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"toolchains": {
|
|
||||||
"python": {
|
|
||||||
"executable": "python",
|
|
||||||
"windowsExecutable": "py",
|
|
||||||
"launcherVersion": null,
|
|
||||||
"venvDir": ".venv",
|
|
||||||
"profiles": [],
|
|
||||||
"pipScript": null
|
|
||||||
},
|
|
||||||
"node": {
|
|
||||||
"packageManager": "npm",
|
|
||||||
"workingDir": "."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tooling": {
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"tool": "dotnet",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "node",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "npm",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "python",
|
|
||||||
"preferredInstallCommands": [],
|
|
||||||
"executables": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"project": {
|
|
||||||
"type": "polyglot",
|
|
||||||
"rootHints": [
|
|
||||||
"*.sln",
|
|
||||||
"package.json",
|
|
||||||
"scripts"
|
|
||||||
],
|
|
||||||
"artifacts": [
|
|
||||||
"bin",
|
|
||||||
"obj",
|
|
||||||
".sdt/debug"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"id": "dotnet-run",
|
|
||||||
"label": "Run .NET app",
|
|
||||||
"type": "dotnet",
|
|
||||||
"command": "dotnet",
|
|
||||||
"args": [
|
|
||||||
"run"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"env": {},
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "dotnet",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attach": {
|
|
||||||
"kind": "manual",
|
|
||||||
"port": null,
|
|
||||||
"processName": null,
|
|
||||||
"note": "Attach your IDE debugger to the running dotnet process."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "npm-dev",
|
|
||||||
"label": "Run npm dev server",
|
|
||||||
"type": "node",
|
|
||||||
"command": "npm",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"workingDir": ".",
|
|
||||||
"env": {},
|
|
||||||
"requires": [
|
|
||||||
{
|
|
||||||
"tool": "node",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tool": "npm",
|
|
||||||
"installPolicy": "Prompt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attach": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"diagnostics": {
|
|
||||||
"enabled": true,
|
|
||||||
"outputDir": ".sdt/debug",
|
|
||||||
"includeAllEnv": false,
|
|
||||||
"captureEnvKeys": [
|
|
||||||
"SDT_LOG_LEVEL",
|
|
||||||
"DOTNET_CLI_HOME",
|
|
||||||
"NUGET_PACKAGES",
|
|
||||||
"PIP_CACHE_DIR",
|
|
||||||
"NVM_HOME",
|
|
||||||
"NVM_SYMLINK"
|
|
||||||
],
|
|
||||||
"redactSensitive": true,
|
|
||||||
"sensitiveKeyPatterns": [
|
|
||||||
"TOKEN",
|
|
||||||
"SECRET",
|
|
||||||
"PASSWORD",
|
|
||||||
"PWD",
|
|
||||||
"CREDENTIAL",
|
|
||||||
"API_KEY",
|
|
||||||
"ACCESS_KEY",
|
|
||||||
"PRIVATE_KEY"
|
|
||||||
],
|
|
||||||
"redactionAllowKeys": [],
|
|
||||||
"bundleOnFailure": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
238
scripts/build.py
238
scripts/build.py
@ -8,11 +8,15 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from script_common import resolve_command
|
from typing import Any
|
||||||
|
|
||||||
|
from script_common import resolve_command, SdtResult # type: ignore
|
||||||
|
|
||||||
|
StepResult = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
def run_step(command, args, cwd):
|
def run_step(command: str, args: list[str], cwd: str) -> StepResult:
|
||||||
resolved = resolve_command(command)
|
resolved = str(resolve_command(command))
|
||||||
if shutil.which(resolved) is None and not pathlib.Path(resolved).exists():
|
if shutil.which(resolved) is None and not pathlib.Path(resolved).exists():
|
||||||
return {
|
return {
|
||||||
"command": resolved,
|
"command": resolved,
|
||||||
@ -38,7 +42,7 @@ def run_step(command, args, cwd):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def resolve_python_executable():
|
def resolve_python_executable() -> str:
|
||||||
candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"]
|
candidates = ["py", "python"] if os.name == "nt" else ["python3", "python"]
|
||||||
for c in candidates:
|
for c in candidates:
|
||||||
if shutil.which(c):
|
if shutil.which(c):
|
||||||
@ -46,20 +50,20 @@ def resolve_python_executable():
|
|||||||
return "python"
|
return "python"
|
||||||
|
|
||||||
|
|
||||||
def parse_common(parser):
|
def parse_common(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument("--project-root", required=True)
|
_ = parser.add_argument("--project-root", required=True)
|
||||||
parser.add_argument("--working-dir", default=".")
|
_ = parser.add_argument("--working-dir", default=".")
|
||||||
parser.add_argument("--json", action="store_true")
|
_ = parser.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
|
||||||
def resolve_cwd(project_root, working_dir):
|
def resolve_cwd(project_root: str, working_dir: str) -> str:
|
||||||
return os.path.abspath(os.path.join(project_root, working_dir))
|
return os.path.abspath(os.path.join(project_root, working_dir))
|
||||||
|
|
||||||
|
|
||||||
EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"}
|
EXCLUDED_SCAN_DIRS = {".git", "node_modules", "bin", "obj", ".venv", "venv", ".sdt", "dist", "build"}
|
||||||
|
|
||||||
|
|
||||||
def discover_dotnet_target(project_root: str, cwd: str):
|
def discover_dotnet_target(project_root: str, cwd: str) -> str | None:
|
||||||
# Prefer local solution first (.slnx, then .sln), then csproj, then bounded scan from project root.
|
# Prefer local solution first (.slnx, then .sln), then csproj, then bounded scan from project root.
|
||||||
local_slnx = sorted(pathlib.Path(cwd).glob("*.slnx"))
|
local_slnx = sorted(pathlib.Path(cwd).glob("*.slnx"))
|
||||||
if len(local_slnx) == 1:
|
if len(local_slnx) == 1:
|
||||||
@ -88,9 +92,9 @@ def discover_dotnet_target(project_root: str, cwd: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def bounded_find_files(root: str, extension: str, max_depth: int):
|
def bounded_find_files(root: str, extension: str, max_depth: int) -> list[str]:
|
||||||
root_path = pathlib.Path(root).resolve()
|
root_path = pathlib.Path(root).resolve()
|
||||||
results = []
|
results: list[str] = []
|
||||||
for current_root, dirs, files in os.walk(root_path):
|
for current_root, dirs, files in os.walk(root_path):
|
||||||
rel = pathlib.Path(current_root).resolve().relative_to(root_path)
|
rel = pathlib.Path(current_root).resolve().relative_to(root_path)
|
||||||
depth = len(rel.parts)
|
depth = len(rel.parts)
|
||||||
@ -105,7 +109,7 @@ def bounded_find_files(root: str, extension: str, max_depth: int):
|
|||||||
return sorted(results)
|
return sorted(results)
|
||||||
|
|
||||||
|
|
||||||
def run_dotnet_action(project_root, working_dir, verb):
|
def run_dotnet_action(project_root: str, working_dir: str, verb: str) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(project_root, working_dir)
|
cwd = resolve_cwd(project_root, working_dir)
|
||||||
target = discover_dotnet_target(project_root, cwd)
|
target = discover_dotnet_target(project_root, cwd)
|
||||||
if not target:
|
if not target:
|
||||||
@ -124,10 +128,14 @@ def run_dotnet_action(project_root, working_dir, verb):
|
|||||||
args = [verb, target]
|
args = [verb, target]
|
||||||
step = run_step("dotnet", args, cwd)
|
step = run_step("dotnet", args, cwd)
|
||||||
step["resolved_target"] = target
|
step["resolved_target"] = target
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
|
||||||
|
# Explicitly ensure the first return value is an integer and narrow the step result
|
||||||
|
exit_val = step.get("exit_code", 1)
|
||||||
|
exit_code = int(exit_val) if isinstance(exit_val, (int, float, str)) and str(exit_val).isdigit() else 1
|
||||||
|
return exit_code, step
|
||||||
|
|
||||||
|
|
||||||
def _deps_hash(app_root):
|
def _deps_hash(app_root: str) -> str:
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
for name in ("package.json", "package-lock.json"):
|
for name in ("package.json", "package-lock.json"):
|
||||||
p = pathlib.Path(app_root) / name
|
p = pathlib.Path(app_root) / name
|
||||||
@ -136,7 +144,7 @@ def _deps_hash(app_root):
|
|||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def ensure_npm_dependencies(app_root):
|
def ensure_npm_dependencies(app_root: str) -> dict[str, Any]:
|
||||||
package_json = pathlib.Path(app_root) / "package.json"
|
package_json = pathlib.Path(app_root) / "package.json"
|
||||||
if not package_json.exists():
|
if not package_json.exists():
|
||||||
return {"installed": False, "reason": "not_applicable"}
|
return {"installed": False, "reason": "not_applicable"}
|
||||||
@ -174,7 +182,7 @@ def ensure_npm_dependencies(app_root):
|
|||||||
return {"installed": True, "reason": "installed", "step": install_step}
|
return {"installed": True, "reason": "installed", "step": install_step}
|
||||||
|
|
||||||
|
|
||||||
def read_package_json(cwd: str):
|
def read_package_json(cwd: str) -> dict[str, Any] | None:
|
||||||
package_json = pathlib.Path(cwd) / "package.json"
|
package_json = pathlib.Path(cwd) / "package.json"
|
||||||
if not package_json.exists():
|
if not package_json.exists():
|
||||||
return None
|
return None
|
||||||
@ -194,23 +202,23 @@ def has_npm_script(cwd: str, script_name: str) -> bool:
|
|||||||
return script_name in scripts and isinstance(scripts.get(script_name), str)
|
return script_name in scripts and isinstance(scripts.get(script_name), str)
|
||||||
|
|
||||||
|
|
||||||
def action_dotnet_build(args):
|
def action_dotnet_build(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
return run_dotnet_action(args.project_root, args.working_dir, "build")
|
return run_dotnet_action(args.project_root, args.working_dir, "build")
|
||||||
|
|
||||||
|
|
||||||
def action_dotnet_restore(args):
|
def action_dotnet_restore(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
return run_dotnet_action(args.project_root, args.working_dir, "restore")
|
return run_dotnet_action(args.project_root, args.working_dir, "restore")
|
||||||
|
|
||||||
|
|
||||||
def action_dotnet_test(args):
|
def action_dotnet_test(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
return run_dotnet_action(args.project_root, args.working_dir, "test")
|
return run_dotnet_action(args.project_root, args.working_dir, "test")
|
||||||
|
|
||||||
|
|
||||||
def action_dotnet_publish(args):
|
def action_dotnet_publish(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
return run_dotnet_action(args.project_root, args.working_dir, "publish")
|
return run_dotnet_action(args.project_root, args.working_dir, "publish")
|
||||||
|
|
||||||
|
|
||||||
def action_npm_install(args):
|
def action_npm_install(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -224,10 +232,10 @@ def action_npm_install(args):
|
|||||||
"skip_reason": "not_applicable_no_package_json",
|
"skip_reason": "not_applicable_no_package_json",
|
||||||
}
|
}
|
||||||
step = run_step("npm", ["install"], cwd)
|
step = run_step("npm", ["install"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_npm_ci(args):
|
def action_npm_ci(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -241,10 +249,10 @@ def action_npm_ci(args):
|
|||||||
"skip_reason": "not_applicable_no_package_json",
|
"skip_reason": "not_applicable_no_package_json",
|
||||||
}
|
}
|
||||||
step = run_step("npm", ["ci"], cwd)
|
step = run_step("npm", ["ci"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_npm_build(args):
|
def action_npm_build(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -283,12 +291,12 @@ def action_npm_build(args):
|
|||||||
if deps.get("reason") == "install_failed":
|
if deps.get("reason") == "install_failed":
|
||||||
step = deps["step"]
|
step = deps["step"]
|
||||||
step["failure_reason"] = "deps_install_failed"
|
step["failure_reason"] = "deps_install_failed"
|
||||||
return step["exit_code"], step
|
return int(step["exit_code"]), step
|
||||||
step = run_step("npm", ["run", "build"], cwd)
|
step = run_step("npm", ["run", "build"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_npm_test(args):
|
def action_npm_test(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -327,12 +335,12 @@ def action_npm_test(args):
|
|||||||
if deps.get("reason") == "install_failed":
|
if deps.get("reason") == "install_failed":
|
||||||
step = deps["step"]
|
step = deps["step"]
|
||||||
step["failure_reason"] = "deps_install_failed"
|
step["failure_reason"] = "deps_install_failed"
|
||||||
return step["exit_code"], step
|
return int(step["exit_code"]), step
|
||||||
step = run_step("npm", ["test"], cwd)
|
step = run_step("npm", ["test"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_npm_audit(args):
|
def action_npm_audit(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "package.json").exists():
|
if not (pathlib.Path(cwd) / "package.json").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -346,37 +354,37 @@ def action_npm_audit(args):
|
|||||||
"skip_reason": "not_applicable_no_package_json",
|
"skip_reason": "not_applicable_no_package_json",
|
||||||
}
|
}
|
||||||
step = run_step("npm", ["audit"], cwd)
|
step = run_step("npm", ["audit"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_python_venv_create(args):
|
def action_python_venv_create(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, ".")
|
cwd = resolve_cwd(args.project_root, ".")
|
||||||
venv_dir = args.venv_dir or ".venv"
|
venv_dir = str(args.venv_dir) if hasattr(args, "venv_dir") else ".venv"
|
||||||
step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd)
|
step = run_step(resolve_python_executable(), ["-m", "venv", venv_dir], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_python_pip_install(args):
|
def action_python_pip_install(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, ".")
|
cwd = resolve_cwd(args.project_root, ".")
|
||||||
req = args.requirements
|
req = str(args.requirements)
|
||||||
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_python_pip_sync(args):
|
def action_python_pip_sync(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, ".")
|
cwd = resolve_cwd(args.project_root, ".")
|
||||||
req = args.requirements
|
req = str(args.requirements)
|
||||||
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
step = run_step(resolve_python_executable(), ["-m", "pip", "install", "-r", req], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_python_pytest(args):
|
def action_python_pytest(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd)
|
step = run_step(resolve_python_executable(), ["-m", "pytest"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_cargo_build(args):
|
def action_cargo_build(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -390,10 +398,10 @@ def action_cargo_build(args):
|
|||||||
"skip_reason": "not_applicable_no_cargo_toml",
|
"skip_reason": "not_applicable_no_cargo_toml",
|
||||||
}
|
}
|
||||||
step = run_step("cargo", ["build"], cwd)
|
step = run_step("cargo", ["build"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_cargo_test(args):
|
def action_cargo_test(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
if not (pathlib.Path(cwd) / "Cargo.toml").exists():
|
||||||
return 0, {
|
return 0, {
|
||||||
@ -407,10 +415,10 @@ def action_cargo_test(args):
|
|||||||
"skip_reason": "not_applicable_no_cargo_toml",
|
"skip_reason": "not_applicable_no_cargo_toml",
|
||||||
}
|
}
|
||||||
step = run_step("cargo", ["test"], cwd)
|
step = run_step("cargo", ["test"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_tauri_build(args):
|
def action_tauri_build(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json"
|
tauri_conf = pathlib.Path(cwd) / "src-tauri" / "tauri.conf.json"
|
||||||
if not tauri_conf.exists():
|
if not tauri_conf.exists():
|
||||||
@ -431,133 +439,94 @@ def action_tauri_build(args):
|
|||||||
if deps.get("reason") == "install_failed":
|
if deps.get("reason") == "install_failed":
|
||||||
step = deps["step"]
|
step = deps["step"]
|
||||||
step["failure_reason"] = "deps_install_failed"
|
step["failure_reason"] = "deps_install_failed"
|
||||||
return step["exit_code"], step
|
return int(step["exit_code"]), step
|
||||||
|
|
||||||
tauri_args = ["run", "tauri", "build"]
|
tauri_args = ["run", "tauri", "build"]
|
||||||
if args.no_bundle:
|
if hasattr(args, "no_bundle") and args.no_bundle:
|
||||||
tauri_args.extend(["--", "--no-bundle"])
|
tauri_args.extend(["--", "--no-bundle"])
|
||||||
step = run_step("npm", tauri_args, cwd)
|
step = run_step("npm", tauri_args, cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_git_status(args):
|
def action_git_status(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("git", ["status"], cwd)
|
step = run_step("git", ["status"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_git_fetch(args):
|
def action_git_fetch(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("git", ["fetch"], cwd)
|
step = run_step("git", ["fetch"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_git_pull(args):
|
def action_git_pull(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("git", ["pull"], cwd)
|
step = run_step("git", ["pull"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_git_clean(args):
|
def action_git_clean(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("git", ["clean", "-fd"], cwd)
|
step = run_step("git", ["clean", "-fd"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_docker_build(args):
|
def action_docker_build(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("docker", ["build", "."], cwd)
|
step = run_step("docker", ["build", "."], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_docker_compose_up(args):
|
def action_docker_compose_up(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("docker", ["compose", "up", "-d"], cwd)
|
step = run_step("docker", ["compose", "up", "-d"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def action_docker_compose_down(args):
|
def action_docker_compose_down(args: argparse.Namespace) -> tuple[int, StepResult]:
|
||||||
cwd = resolve_cwd(args.project_root, args.working_dir)
|
cwd = resolve_cwd(args.project_root, args.working_dir)
|
||||||
step = run_step("docker", ["compose", "down"], cwd)
|
step = run_step("docker", ["compose", "down"], cwd)
|
||||||
return 0 if step["exit_code"] == 0 else step["exit_code"], step
|
return 0 if step["exit_code"] == 0 else int(step["exit_code"]), step
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="SDT normalized build actions")
|
parser = argparse.ArgumentParser(description="SDT normalized build actions")
|
||||||
sub = parser.add_subparsers(dest="action", required=True)
|
sub = parser.add_subparsers(dest="action", required=True)
|
||||||
|
|
||||||
p0 = sub.add_parser("dotnet-restore")
|
p0 = sub.add_parser("dotnet-restore"); parse_common(p0)
|
||||||
parse_common(p0)
|
p1 = sub.add_parser("dotnet-build"); parse_common(p1)
|
||||||
|
p1b = sub.add_parser("dotnet-test"); parse_common(p1b)
|
||||||
|
p1c = sub.add_parser("dotnet-publish"); parse_common(p1c)
|
||||||
|
p2 = sub.add_parser("npm-install"); parse_common(p2)
|
||||||
|
p2b = sub.add_parser("npm-ci"); parse_common(p2b)
|
||||||
|
p3 = sub.add_parser("npm-build"); parse_common(p3)
|
||||||
|
p3b = sub.add_parser("npm-test"); parse_common(p3b)
|
||||||
|
p3c = sub.add_parser("npm-audit"); parse_common(p3c)
|
||||||
|
|
||||||
p1 = sub.add_parser("dotnet-build")
|
p4 = sub.add_parser("python-venv-create"); parse_common(p4)
|
||||||
parse_common(p1)
|
_ = p4.add_argument("--venv-dir", default=".venv")
|
||||||
|
|
||||||
p1b = sub.add_parser("dotnet-test")
|
p5 = sub.add_parser("python-pip-install"); parse_common(p5)
|
||||||
parse_common(p1b)
|
_ = p5.add_argument("--requirements", required=True)
|
||||||
|
|
||||||
p1c = sub.add_parser("dotnet-publish")
|
p5b = sub.add_parser("python-pip-sync"); parse_common(p5b)
|
||||||
parse_common(p1c)
|
_ = p5b.add_argument("--requirements", required=True)
|
||||||
|
|
||||||
p2 = sub.add_parser("npm-install")
|
p5c = sub.add_parser("python-pytest"); parse_common(p5c)
|
||||||
parse_common(p2)
|
p6 = sub.add_parser("cargo-build"); parse_common(p6)
|
||||||
|
p6b = sub.add_parser("cargo-test"); parse_common(p6b)
|
||||||
|
|
||||||
p2b = sub.add_parser("npm-ci")
|
p7 = sub.add_parser("tauri-build"); parse_common(p7)
|
||||||
parse_common(p2b)
|
_ = p7.add_argument("--no-bundle", action="store_true")
|
||||||
|
|
||||||
p3 = sub.add_parser("npm-build")
|
p8 = sub.add_parser("git-status"); parse_common(p8)
|
||||||
parse_common(p3)
|
p9 = sub.add_parser("git-fetch"); parse_common(p9)
|
||||||
|
p10 = sub.add_parser("git-pull"); parse_common(p10)
|
||||||
p3b = sub.add_parser("npm-test")
|
p11 = sub.add_parser("git-clean"); parse_common(p11)
|
||||||
parse_common(p3b)
|
p12 = sub.add_parser("docker-build"); parse_common(p12)
|
||||||
|
p13 = sub.add_parser("docker-compose-up"); parse_common(p13)
|
||||||
p3c = sub.add_parser("npm-audit")
|
p14 = sub.add_parser("docker-compose-down"); parse_common(p14)
|
||||||
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -590,7 +559,8 @@ def main():
|
|||||||
code, summary = handlers[args.action](args)
|
code, summary = handlers[args.action](args)
|
||||||
if args.json:
|
if args.json:
|
||||||
print(json.dumps(summary))
|
print(json.dumps(summary))
|
||||||
return code
|
|
||||||
|
return int(code)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -1,90 +1,42 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
|
from typing import cast
|
||||||
|
from script_common import ensure_npm_build, find_node_app_root, resolve_repo_root
|
||||||
from script_common import find_node_app_root, resolve_repo_root, run, sha256_files
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper")
|
parser = argparse.ArgumentParser(description="Cross-platform web/tauri publish helper")
|
||||||
parser.add_argument("--target", choices=["web", "tauri"], default="web")
|
_ = parser.add_argument("--target", choices=["web", "tauri"], default="web")
|
||||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
_ = parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||||
parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none")
|
_ = parser.add_argument("--tauri-bundles", choices=["none", "nsis", "msi"], default="none")
|
||||||
parser.add_argument("--install-deps", action="store_true")
|
_ = parser.add_argument("--install-deps", action="store_true")
|
||||||
parser.add_argument("--skip-install", action="store_true")
|
_ = parser.add_argument("--skip-install", action="store_true")
|
||||||
parser.add_argument("--dry-run", action="store_true")
|
_ = parser.add_argument("--dry-run", action="store_true")
|
||||||
parser.add_argument("--repo-root", default=None)
|
_ = parser.add_argument("--repo-root", default=None)
|
||||||
parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json")
|
_ = parser.add_argument("--app-root", default=None, help="Relative or absolute app root with package.json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = resolve_repo_root(args.repo_root)
|
repo_root_val = cast(str | None, args.repo_root)
|
||||||
app_root = find_node_app_root(repo_root, args.app_root)
|
repo_root = resolve_repo_root(repo_root_val)
|
||||||
|
app_root_val = cast(str | None, args.app_root)
|
||||||
|
app_root = find_node_app_root(repo_root, app_root_val)
|
||||||
if app_root is None:
|
if app_root is None:
|
||||||
print("Unable to locate app root (no unique package.json found).")
|
print("Unable to locate app root (no unique package.json found).")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
package_json = app_root / "package.json"
|
# If dry-run is requested, we just print intent.
|
||||||
lock_file = app_root / "package-lock.json"
|
if args.dry_run:
|
||||||
node_modules = app_root / "node_modules"
|
print(f"Dry-run: Would build {args.target} ({args.configuration}) in {app_root}")
|
||||||
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
|
return 0
|
||||||
|
|
||||||
tauri_cmd = ["run", "tauri", "build"]
|
res = ensure_npm_build(
|
||||||
tauri_tail: list[str] = []
|
app_root=app_root,
|
||||||
if args.tauri_bundles == "none":
|
target=str(args.target),
|
||||||
tauri_tail.extend(["--no-bundle"])
|
configuration=str(args.configuration),
|
||||||
else:
|
tauri_bundles=str(args.tauri_bundles)
|
||||||
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))
|
return int(res["exit_code"])
|
||||||
if not args.dry_run:
|
|
||||||
return run("npm", tauri_cmd, app_root)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -1,22 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from script_common import find_csproj_by_keyword, find_node_app_root, resolve_repo_root, run # type: ignore
|
||||||
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:
|
def has_package_script(app_root: Path, script_name: str) -> bool:
|
||||||
package_json = app_root / "package.json"
|
package_json = app_root / "package.json"
|
||||||
@ -35,18 +24,18 @@ def has_package_script(app_root: Path, script_name: str) -> bool:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints")
|
parser = argparse.ArgumentParser(description="Publish bundled outputs using Python script entrypoints")
|
||||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
_ = parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||||
parser.add_argument("--runtime", default="win-x64")
|
_ = parser.add_argument("--runtime", default="win-x64")
|
||||||
parser.add_argument("--skip-sidecar", action="store_true")
|
_ = parser.add_argument("--skip-sidecar", action="store_true")
|
||||||
parser.add_argument("--skip-web", action="store_true")
|
_ = parser.add_argument("--skip-web", action="store_true")
|
||||||
parser.add_argument("--skip-webgateway", action="store_true")
|
_ = parser.add_argument("--skip-webgateway", action="store_true")
|
||||||
parser.add_argument("--skip-tauri", action="store_true")
|
_ = parser.add_argument("--skip-tauri", action="store_true")
|
||||||
parser.add_argument("--dry-run", action="store_true")
|
_ = parser.add_argument("--dry-run", action="store_true")
|
||||||
parser.add_argument("--repo-root", default=None)
|
_ = parser.add_argument("--repo-root", default=None)
|
||||||
parser.add_argument("--sidecar-project", default=None)
|
_ = parser.add_argument("--sidecar-project", default=None)
|
||||||
parser.add_argument("--gateway-project", default=None)
|
_ = parser.add_argument("--gateway-project", default=None)
|
||||||
parser.add_argument("--app-root", default=None)
|
_ = parser.add_argument("--app-root", default=None)
|
||||||
parser.add_argument("--output-dir", default="output")
|
_ = parser.add_argument("--output-dir", default="output")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = resolve_repo_root(args.repo_root)
|
repo_root = resolve_repo_root(args.repo_root)
|
||||||
@ -56,45 +45,48 @@ def main() -> int:
|
|||||||
sidecar_project = (repo_root / args.sidecar_project).resolve() if args.sidecar_project else find_csproj_by_keyword(repo_root, ["sidecar"])
|
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"])
|
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)
|
app_root = (repo_root / args.app_root).resolve() if args.app_root else find_node_app_root(repo_root, None)
|
||||||
|
|
||||||
tauri_conf = None
|
tauri_conf = None
|
||||||
if app_root is not None:
|
if app_root is not None:
|
||||||
candidate_a = app_root / "src-tauri" / "tauri.conf.json"
|
tauri_conf = next((p for p in [app_root/"src-tauri"/"tauri.conf.json", app_root/"tauri.conf.json"] if p.exists()), None)
|
||||||
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
|
py = sys.executable
|
||||||
|
scripts_dir = Path(__file__).parent
|
||||||
|
|
||||||
if not args.skip_sidecar:
|
if not args.skip_sidecar:
|
||||||
if sidecar_project is None:
|
if sidecar_project is None:
|
||||||
print("Skipping sidecar: no sidecar csproj detected.")
|
print("Skipping sidecar: no sidecar csproj detected.")
|
||||||
else:
|
else:
|
||||||
cmd = [py, "scripts/publish-sidecar.py", "--configuration", args.configuration, "--runtime", args.runtime]
|
cmd = ["-m", "scripts.publish-sidecar" if __package__ else "publish-sidecar",
|
||||||
cmd.extend(["--project", str(sidecar_project)])
|
"--configuration", args.configuration, "--runtime", args.runtime, "--project", str(sidecar_project)]
|
||||||
code = run_step("Publish sidecar", cmd, repo_root, args.dry_run)
|
print(f"\n> Publishing Sidecar\n$ {py} {' '.join(cmd)}")
|
||||||
if code != 0:
|
if not args.dry_run:
|
||||||
return code
|
code = run(py, cmd, scripts_dir)
|
||||||
|
if code != 0: return code
|
||||||
|
|
||||||
if not args.skip_web:
|
if not args.skip_web:
|
||||||
if app_root is None:
|
if app_root is None:
|
||||||
print("Skipping web: no app root with package.json detected.")
|
print("Skipping web: no app root detected.")
|
||||||
elif not has_package_script(app_root, "build"):
|
elif not has_package_script(app_root, "build"):
|
||||||
print("Skipping web: package.json has no 'build' script.")
|
print("Skipping web: package.json has no 'build' script.")
|
||||||
else:
|
else:
|
||||||
cmd = [py, "scripts/publish-app.py", "--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)]
|
cmd = ["-m", "scripts.publish-app" if __package__ else "publish-app",
|
||||||
code = run_step("Build web", cmd, repo_root, args.dry_run)
|
"--target", "web", "--configuration", args.configuration, "--app-root", str(app_root)]
|
||||||
if code != 0:
|
print(f"\n> Building Web\n$ {py} {' '.join(cmd)}")
|
||||||
return code
|
if not args.dry_run:
|
||||||
|
code = run(py, cmd, scripts_dir)
|
||||||
|
if code != 0: return code
|
||||||
|
|
||||||
if not args.skip_webgateway:
|
if not args.skip_webgateway:
|
||||||
if gateway_project is None:
|
if gateway_project is None:
|
||||||
print("Skipping web gateway: no gateway csproj detected.")
|
print("Skipping web gateway: no gateway csproj detected.")
|
||||||
else:
|
else:
|
||||||
cmd = [py, "scripts/publish-webgateway.py", "--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)]
|
cmd = ["-m", "scripts.publish-webgateway" if __package__ else "publish-webgateway",
|
||||||
code = run_step("Publish web gateway", cmd, repo_root, args.dry_run)
|
"--configuration", args.configuration, "--runtime", args.runtime, "--project", str(gateway_project)]
|
||||||
if code != 0:
|
print(f"\n> Publishing Web Gateway\n$ {py} {' '.join(cmd)}")
|
||||||
return code
|
if not args.dry_run:
|
||||||
|
code = run(py, cmd, scripts_dir)
|
||||||
|
if code != 0: return code
|
||||||
|
|
||||||
if not args.skip_tauri:
|
if not args.skip_tauri:
|
||||||
if app_root is None or tauri_conf is None:
|
if app_root is None or tauri_conf is None:
|
||||||
@ -102,21 +94,20 @@ def main() -> int:
|
|||||||
elif not has_package_script(app_root, "tauri"):
|
elif not has_package_script(app_root, "tauri"):
|
||||||
print("Skipping tauri: package.json has no 'tauri' script.")
|
print("Skipping tauri: package.json has no 'tauri' script.")
|
||||||
else:
|
else:
|
||||||
cmd = [py, "scripts/publish-app.py", "--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)]
|
cmd = ["-m", "scripts.publish-app" if __package__ else "publish-app",
|
||||||
code = run_step("Build tauri", cmd, repo_root, args.dry_run)
|
"--target", "tauri", "--configuration", args.configuration, "--tauri-bundles", "none", "--app-root", str(app_root)]
|
||||||
if code != 0:
|
print(f"\n> Building Tauri\n$ {py} {' '.join(cmd)}")
|
||||||
return code
|
if not args.dry_run:
|
||||||
|
code = run(py, cmd, scripts_dir)
|
||||||
|
if code != 0: return code
|
||||||
|
|
||||||
target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release")
|
target_dir = app_root / "src-tauri" / "target" / ("debug" if args.configuration == "Debug" else "release")
|
||||||
if args.runtime.startswith("win-") or getattr(sys, "platform", "").startswith("win"):
|
pattern = "*.exe" if os.name == "nt" else "*"
|
||||||
exes = sorted(target_dir.glob("*.exe"), key=lambda p: p.stat().st_mtime, reverse=True)
|
exes = sorted((p for p in target_dir.glob(pattern) if p.is_file() and (os.name == "nt" or not p.suffix)), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
else:
|
|
||||||
exes = sorted((p for p in target_dir.glob("*") if p.is_file() and not p.suffix), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
||||||
|
|
||||||
if exes:
|
if exes:
|
||||||
staged = output_root / exes[0].name
|
staged = output_root / exes[0].name
|
||||||
if args.dry_run:
|
if args.dry_run: print(f"Would copy: {exes[0]} -> {staged}")
|
||||||
print(f"Would copy: {exes[0]} -> {staged}")
|
|
||||||
else:
|
else:
|
||||||
shutil.copy2(exes[0], staged)
|
shutil.copy2(exes[0], staged)
|
||||||
print(f"Staged desktop executable: {staged}")
|
print(f"Staged desktop executable: {staged}")
|
||||||
|
|||||||
@ -1,51 +1,48 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
|
from typing import cast
|
||||||
|
from script_common import ensure_dotnet_publish, find_csproj_by_keyword, resolve_repo_root, SdtResult
|
||||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper")
|
parser = argparse.ArgumentParser(description="Cross-platform .NET sidecar publish helper")
|
||||||
parser.add_argument("--configuration", default="Release")
|
_ = parser.add_argument("--configuration", default="Release")
|
||||||
parser.add_argument("--runtime", default="win-x64")
|
_ = parser.add_argument("--runtime", default="win-x64")
|
||||||
parser.add_argument("--repo-root", default=None)
|
_ = parser.add_argument("--repo-root", default=None)
|
||||||
parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj")
|
_ = parser.add_argument("--project", default=None, help="Relative/absolute path to sidecar csproj")
|
||||||
parser.add_argument("--output-dir", default="output")
|
_ = parser.add_argument("--output-dir", default="output")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = resolve_repo_root(args.repo_root)
|
repo_root_val = cast(str | None, args.repo_root)
|
||||||
output_dir = (repo_root / args.output_dir).resolve()
|
repo_root = resolve_repo_root(repo_root_val)
|
||||||
|
output_dir_val = cast(str, args.output_dir)
|
||||||
|
output_dir = (repo_root / output_dir_val).resolve()
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if args.project:
|
project_val = cast(str | None, args.project)
|
||||||
csproj = (repo_root / args.project).resolve()
|
if project_val:
|
||||||
|
csproj = (repo_root / project_val).resolve()
|
||||||
else:
|
else:
|
||||||
csproj = find_csproj_by_keyword(repo_root, ["sidecar"])
|
csproj = find_csproj_by_keyword(repo_root, ["sidecar"])
|
||||||
|
|
||||||
if csproj is None or not csproj.exists():
|
if csproj is None or not csproj.exists():
|
||||||
print("Could not locate sidecar project. Pass --project <path/to/project.csproj>.")
|
print("Could not locate sidecar project. Pass --project <path/to/project.csproj>.")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
publish_args = [
|
res: SdtResult = ensure_dotnet_publish(
|
||||||
"publish",
|
csproj=csproj,
|
||||||
str(csproj),
|
output_dir=output_dir,
|
||||||
"-c",
|
configuration=str(args.configuration),
|
||||||
args.configuration,
|
runtime=str(args.runtime),
|
||||||
"-r",
|
single_file=True,
|
||||||
args.runtime,
|
self_contained=True
|
||||||
"--self-contained",
|
)
|
||||||
"-p:PublishSingleFile=true",
|
|
||||||
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
if res["exit_code"] != 0:
|
||||||
"-p:RestoreIgnoreFailedSources=true",
|
return int(res["exit_code"])
|
||||||
"-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 "")
|
runtime_val = str(args.runtime)
|
||||||
|
binary_name = csproj.stem + (".exe" if runtime_val.startswith("win-") else "")
|
||||||
binary_path = output_dir / binary_name
|
binary_path = output_dir / binary_name
|
||||||
if binary_path.exists():
|
if binary_path.exists():
|
||||||
print(f"Published executable: {binary_path}")
|
print(f"Published executable: {binary_path}")
|
||||||
|
|||||||
@ -1,74 +1,67 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import cast
|
||||||
from script_common import dotnet_env, find_csproj_by_keyword, resolve_repo_root, run
|
from script_common import ensure_dotnet_publish, find_csproj_by_keyword, resolve_repo_root, SdtResult
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper")
|
parser = argparse.ArgumentParser(description="Cross-platform ASP.NET gateway publish helper")
|
||||||
parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
_ = parser.add_argument("--configuration", choices=["Release", "Debug"], default="Release")
|
||||||
parser.add_argument("--runtime", default="win-x64")
|
_ = parser.add_argument("--runtime", default="win-x64")
|
||||||
parser.add_argument("--self-contained", action="store_true")
|
_ = parser.add_argument("--self-contained", action="store_true")
|
||||||
parser.add_argument("--skip-web-assets", action="store_true")
|
_ = parser.add_argument("--skip-web-assets", action="store_true")
|
||||||
parser.add_argument("--repo-root", default=None)
|
_ = parser.add_argument("--repo-root", default=None)
|
||||||
parser.add_argument("--project", default=None, help="Relative/absolute path to gateway csproj")
|
_ = 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("--web-build-dir", default=None, help="Relative path to web build assets root")
|
||||||
parser.add_argument("--output-dir", default="output/webgateway")
|
_ = parser.add_argument("--output-dir", default="output/webgateway")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = resolve_repo_root(args.repo_root)
|
repo_root_val = cast(str | None, args.repo_root)
|
||||||
output_dir = (repo_root / args.output_dir).resolve()
|
repo_root = resolve_repo_root(repo_root_val)
|
||||||
|
output_dir_val = cast(str, args.output_dir)
|
||||||
|
output_dir = (repo_root / output_dir_val).resolve()
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if args.project:
|
project_val = cast(str | None, args.project)
|
||||||
csproj = (repo_root / args.project).resolve()
|
if project_val:
|
||||||
|
csproj = (repo_root / project_val).resolve()
|
||||||
else:
|
else:
|
||||||
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
csproj = find_csproj_by_keyword(repo_root, ["webgateway", "gateway"])
|
||||||
|
|
||||||
if csproj is None or not csproj.exists():
|
if csproj is None or not csproj.exists():
|
||||||
print("Could not locate web gateway project. Pass --project <path/to/project.csproj>.")
|
print("Could not locate web gateway project. Pass --project <path/to/project.csproj>.")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
publish_args = [
|
res: SdtResult = ensure_dotnet_publish(
|
||||||
"publish",
|
csproj=csproj,
|
||||||
str(csproj),
|
output_dir=output_dir,
|
||||||
"-c",
|
configuration=str(args.configuration),
|
||||||
args.configuration,
|
runtime=str(args.runtime),
|
||||||
"-r",
|
self_contained=bool(args.self_contained),
|
||||||
args.runtime,
|
single_file=False
|
||||||
"--self-contained",
|
)
|
||||||
"true" if args.self_contained else "false",
|
|
||||||
"-p:RestoreIgnoreFailedSources=true",
|
if res["exit_code"] != 0:
|
||||||
"-p:NuGetAudit=false",
|
return int(res["exit_code"])
|
||||||
"-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 not args.skip_web_assets:
|
||||||
if args.web_build_dir:
|
web_build_dir_val = cast(str | None, args.web_build_dir)
|
||||||
web_build_dir = (repo_root / args.web_build_dir).resolve()
|
if web_build_dir_val:
|
||||||
|
web_build_dir = (repo_root / web_build_dir_val).resolve()
|
||||||
else:
|
else:
|
||||||
web_build_dir = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None)
|
# Look for recent web build output
|
||||||
if web_build_dir is not None:
|
# (Note: rglob is costly but necessary for discovery here)
|
||||||
web_build_dir = web_build_dir / "build"
|
web_pj = next((p.parent for p in repo_root.rglob("package.json") if (p.parent / "build").exists()), None)
|
||||||
|
web_build_dir = web_pj / "build" if web_pj else None
|
||||||
|
|
||||||
if web_build_dir is None or not web_build_dir.exists():
|
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.")
|
print("Web assets not found. Skip with --skip-web-assets or pass --web-build-dir.")
|
||||||
else:
|
else:
|
||||||
web_out = output_dir / "wwwroot"
|
web_out = output_dir / "wwwroot"
|
||||||
web_out.mkdir(parents=True, exist_ok=True)
|
print(f"Copying web assets: {web_build_dir} -> {web_out}")
|
||||||
for item in web_build_dir.iterdir():
|
shutil.copytree(web_build_dir, web_out, dirs_exist_ok=True)
|
||||||
dst = web_out / item.name
|
print(f"Copied web assets to {web_out}")
|
||||||
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}")
|
print(f"Publish completed: {output_dir}")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@ -16,6 +16,15 @@ function Clear-SdtProxyEnv {
|
|||||||
Remove-Item Env:PIP_NO_INDEX -ErrorAction SilentlyContinue
|
Remove-Item Env:PIP_NO_INDEX -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-SdtConfigExists {
|
||||||
|
param([string]$Path)
|
||||||
|
if (Test-Path (Join-Path $Path "devtool.json")) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
$sdtConfigs = Get-ChildItem -Path $Path -Filter "sdtconfig-*.json" -File -ErrorAction SilentlyContinue
|
||||||
|
return ($null -ne $sdtConfigs) -and ($sdtConfigs.Count -gt 0)
|
||||||
|
}
|
||||||
|
|
||||||
function Resolve-SdtRepoRoot {
|
function Resolve-SdtRepoRoot {
|
||||||
param([string]$StartPath)
|
param([string]$StartPath)
|
||||||
|
|
||||||
@ -34,7 +43,7 @@ function Resolve-SdtRepoRoot {
|
|||||||
}
|
}
|
||||||
if (-not [string]::IsNullOrWhiteSpace($override)) {
|
if (-not [string]::IsNullOrWhiteSpace($override)) {
|
||||||
$overridePath = [System.IO.Path]::GetFullPath($override)
|
$overridePath = [System.IO.Path]::GetFullPath($override)
|
||||||
if (Test-Path (Join-Path $overridePath "devtool.json")) {
|
if (Test-SdtConfigExists -Path $overridePath) {
|
||||||
return $overridePath
|
return $overridePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +51,7 @@ function Resolve-SdtRepoRoot {
|
|||||||
foreach ($start in $candidateStarts) {
|
foreach ($start in $candidateStarts) {
|
||||||
$cursor = [System.IO.Path]::GetFullPath($start)
|
$cursor = [System.IO.Path]::GetFullPath($start)
|
||||||
while (-not [string]::IsNullOrWhiteSpace($cursor)) {
|
while (-not [string]::IsNullOrWhiteSpace($cursor)) {
|
||||||
if (Test-Path (Join-Path $cursor "devtool.json")) {
|
if (Test-SdtConfigExists -Path $cursor) {
|
||||||
return $cursor
|
return $cursor
|
||||||
}
|
}
|
||||||
$parent = [System.IO.Directory]::GetParent($cursor)
|
$parent = [System.IO.Directory]::GetParent($cursor)
|
||||||
@ -65,7 +74,7 @@ function Resolve-SdtRepoRoot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw "Could not locate repository root. Ensure a devtool.json exists in the project root."
|
throw "Could not locate repository root. Ensure a project config (sdtconfig-*.json or devtool.json) exists in the project root."
|
||||||
}
|
}
|
||||||
|
|
||||||
function Initialize-SdtDotnetEnv {
|
function Initialize-SdtDotnetEnv {
|
||||||
|
|||||||
@ -6,357 +6,298 @@ import pathlib
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, Iterable, List, Sequence
|
import time
|
||||||
|
from typing import Any, Iterable, Optional, Sequence
|
||||||
|
|
||||||
|
# --- Domain: SDT Types ---
|
||||||
|
# SDT Normalized Result Object
|
||||||
|
# result["exit_code"]: int
|
||||||
|
# result["status"]: "ok" | "failed" | "skipped"
|
||||||
|
# result["elapsed_seconds"]: float
|
||||||
|
# result["failure_reason"]: Optional[str]
|
||||||
|
# result["skip_reason"]: Optional[str]
|
||||||
|
SdtResult = dict[str, Any]
|
||||||
|
|
||||||
PROXY_VARS = [
|
PROXY_VARS = [
|
||||||
"HTTP_PROXY",
|
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy",
|
||||||
"HTTPS_PROXY",
|
"GIT_HTTP_PROXY", "GIT_HTTPS_PROXY", "PIP_NO_INDEX"
|
||||||
"ALL_PROXY",
|
|
||||||
"http_proxy",
|
|
||||||
"https_proxy",
|
|
||||||
"all_proxy",
|
|
||||||
"GIT_HTTP_PROXY",
|
|
||||||
"GIT_HTTPS_PROXY",
|
|
||||||
"PIP_NO_INDEX",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# --- Domain: FS Utilities ---
|
||||||
|
|
||||||
def resolve_repo_root(start: str | None = None) -> pathlib.Path:
|
def sha256_files(paths: Iterable[pathlib.Path]) -> str:
|
||||||
base = pathlib.Path(start or os.getcwd()).resolve()
|
h = hashlib.sha256()
|
||||||
|
for p in sorted(paths):
|
||||||
|
if p.exists(): h.update(p.read_bytes())
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
# Preferred marker for SDT-managed projects.
|
def first_existing(paths: Iterable[pathlib.Path]) -> Optional[pathlib.Path]:
|
||||||
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:
|
for p in paths:
|
||||||
p.mkdir(parents=True, exist_ok=True)
|
if p.exists(): return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
def newest_file(search_root: pathlib.Path, pattern: str) -> Optional[pathlib.Path]:
|
||||||
|
hits = sorted(search_root.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||||
|
return hits[0] if hits else None
|
||||||
|
|
||||||
def clean_proxy_env(env: Dict[str, str]) -> None:
|
def ensure_dirs(paths: list[pathlib.Path]) -> None:
|
||||||
for k in PROXY_VARS:
|
for p in paths: p.mkdir(parents=True, exist_ok=True)
|
||||||
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:
|
def _hint_matches(root: pathlib.Path, hint: str) -> bool:
|
||||||
h = hint.strip()
|
h = hint.strip()
|
||||||
if not h:
|
if not h:
|
||||||
return False
|
return False
|
||||||
|
try:
|
||||||
has_glob = any(ch in h for ch in ("*", "?", "["))
|
has_glob = any(ch in h for ch in ("*", "?", "["))
|
||||||
if has_glob:
|
if has_glob:
|
||||||
# Match both anywhere in root and directly at root-level for common hints like "*.sln".
|
if any(root.glob(h)):
|
||||||
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 True
|
||||||
|
return any(root.rglob(h))
|
||||||
|
|
||||||
|
marker = root / h
|
||||||
|
if marker.exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If hint is a plain filename marker, allow bounded search in root tree.
|
||||||
|
if not any(sep in h for sep in ("/", "\\")):
|
||||||
|
return any(p.name == h for p in root.rglob(h))
|
||||||
|
|
||||||
|
return False
|
||||||
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _is_tauri_root(candidate_dir: pathlib.Path) -> bool:
|
# --- Domain: Project Discovery ---
|
||||||
return (candidate_dir / "src-tauri" / "tauri.conf.json").exists() or (candidate_dir / "tauri.conf.json").exists()
|
|
||||||
|
|
||||||
def _iter_package_jsons() -> list[pathlib.Path]:
|
def resolve_repo_root(start: str | None = None) -> pathlib.Path:
|
||||||
excluded = {"node_modules", "dist", "build", ".git", ".sdt", ".venv", "venv", "bin", "obj"}
|
base = pathlib.Path(start or os.getcwd()).resolve()
|
||||||
found: list[pathlib.Path] = []
|
for cur in [base, *base.parents]:
|
||||||
for current_root, dirs, files in os.walk(repo_root):
|
sdt_configs = list(cur.glob("sdtconfig-*.json"))
|
||||||
dirs[:] = [d for d in dirs if d not in excluded]
|
cfg = sdt_configs[0] if sdt_configs else (cur / "devtool.json")
|
||||||
if "package.json" in files:
|
if cfg.exists():
|
||||||
found.append(pathlib.Path(current_root) / "package.json")
|
hints = load_project_root_hints(cur, cfg)
|
||||||
found.sort(key=lambda p: len(p.parts))
|
if not hints or any(_hint_matches(cur, hint) for hint in hints): return cur
|
||||||
return found
|
try:
|
||||||
|
proc = subprocess.run(["git", "-C", str(base), "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=False)
|
||||||
|
if proc.returncode == 0 and proc.stdout.strip(): return pathlib.Path(proc.stdout.strip()).resolve()
|
||||||
|
except: pass
|
||||||
|
return base
|
||||||
|
|
||||||
if preferred:
|
def load_project_root_hints(repo_root: pathlib.Path, cfg: pathlib.Path | None = None) -> list[str]:
|
||||||
p = (repo_root / preferred).resolve()
|
if cfg is None:
|
||||||
package_json = p / "package.json"
|
sdt_configs = list(repo_root.glob("sdtconfig-*.json"))
|
||||||
if package_json.exists():
|
cfg = sdt_configs[0] if sdt_configs else (repo_root / "devtool.json")
|
||||||
# Keep explicit preferred root only when it appears runnable for node workflows.
|
if not cfg.exists(): return []
|
||||||
if _is_tauri_root(p) or _has_scripts(package_json, ("build", "dev", "start", "test", "tauri")):
|
try:
|
||||||
return p
|
data = json.loads(cfg.read_text(encoding="utf-8"))
|
||||||
|
hints = data.get("project", {}).get("rootHints", [])
|
||||||
|
return [str(x) for x in hints if isinstance(x, str) and x.strip()]
|
||||||
|
except: return []
|
||||||
|
|
||||||
package_files = _iter_package_jsons()
|
def _resolve_project_config_path(repo_root: pathlib.Path) -> Optional[pathlib.Path]:
|
||||||
if not package_files:
|
sdt_configs = sorted(repo_root.glob("sdtconfig-*.json"), key=lambda p: p.name.lower())
|
||||||
return None
|
if sdt_configs:
|
||||||
|
return sdt_configs[0]
|
||||||
# Strong preference: a tauri app root with tauri config and package.json.
|
legacy = repo_root / "devtool.json"
|
||||||
tauri_candidates = [p.parent for p in package_files if _is_tauri_root(p.parent)]
|
if legacy.exists():
|
||||||
if len(tauri_candidates) == 1:
|
return legacy
|
||||||
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
|
return None
|
||||||
|
|
||||||
|
def load_node_working_dir(repo_root: pathlib.Path) -> Optional[str]:
|
||||||
|
cfg = _resolve_project_config_path(repo_root)
|
||||||
|
if cfg is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.loads(cfg.read_text(encoding="utf-8"))
|
||||||
|
node = data.get("toolchains", {}).get("node", {})
|
||||||
|
value = node.get("workingDir")
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
return value.strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
def newest_file(search_root: pathlib.Path, pattern: str) -> pathlib.Path | None:
|
def find_csproj(repo_root: pathlib.Path, hints: Sequence[str] | None = None) -> Optional[pathlib.Path]:
|
||||||
if not search_root.exists():
|
if hints:
|
||||||
return None
|
for h in hints:
|
||||||
files = [p for p in search_root.rglob(pattern) if p.is_file() and "\\obj\\" not in str(p).replace("/", "\\")]
|
p = (repo_root / h).resolve()
|
||||||
if not files:
|
if p.exists() and p.suffix == ".csproj": return p
|
||||||
return None
|
hits = [h for h in repo_root.rglob("*.csproj") if not any(x in h.parts for x in [".git", "node_modules", "bin", "obj"])]
|
||||||
files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
return hits[0] if len(hits) == 1 else None
|
||||||
return files[0]
|
|
||||||
|
def find_csproj_by_keyword(repo_root: pathlib.Path, keywords: Sequence[str]) -> Optional[pathlib.Path]:
|
||||||
|
hits = [h for h in repo_root.rglob("*.csproj") if not any(x in h.parts for x in [".git", "node_modules", "bin", "obj"])]
|
||||||
|
for kw in keywords:
|
||||||
|
matches = [h for h in hits if kw.lower() in h.name.lower()]
|
||||||
|
if len(matches) == 1: return matches[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_node_app_root(repo_root: pathlib.Path, preferred: str | None = None) -> Optional[pathlib.Path]:
|
||||||
|
def _read_pj(p: pathlib.Path):
|
||||||
|
try: return json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
except: return None
|
||||||
|
def _has_scr(p: pathlib.Path, names: Sequence[str]):
|
||||||
|
src = _read_pj(p)
|
||||||
|
return any(src.get("scripts", {}).get(n) for n in names) if src else False
|
||||||
|
def _is_tauri(d: pathlib.Path): return (d / "src-tauri" / "tauri.conf.json").exists() or (d / "tauri.conf.json").exists()
|
||||||
|
def _iter_pj():
|
||||||
|
excluded = {".git", "node_modules", ".sdt", "dist", "build", ".venv", "venv", "bin", "obj"}
|
||||||
|
for p in repo_root.rglob("package.json"):
|
||||||
|
if any(x in p.parts for x in excluded):
|
||||||
|
continue
|
||||||
|
yield p
|
||||||
|
|
||||||
|
if not preferred:
|
||||||
|
preferred = load_node_working_dir(repo_root)
|
||||||
|
if preferred:
|
||||||
|
p = (repo_root / preferred).resolve()
|
||||||
|
p = p.parent if p.is_file() else p
|
||||||
|
if (p / "package.json").exists():
|
||||||
|
return p
|
||||||
|
|
||||||
|
tauri = [p.parent for p in _iter_pj() if _is_tauri(p.parent)]
|
||||||
|
if len(tauri) == 1: return tauri[0]
|
||||||
|
if len(tauri) > 1:
|
||||||
|
tauri = sorted(set(tauri), key=lambda d: (len(d.parts), str(d).lower()))
|
||||||
|
return tauri[0]
|
||||||
|
scripts = [p.parent for p in _iter_pj() if _has_scr(p, ["tauri", "build"])]
|
||||||
|
if len(scripts) == 1: return scripts[0]
|
||||||
|
if len(scripts) > 1:
|
||||||
|
scripts = sorted(set(scripts), key=lambda d: (len(d.parts), str(d).lower()))
|
||||||
|
return scripts[0]
|
||||||
|
|
||||||
|
all_pj = [p.parent for p in _iter_pj()]
|
||||||
|
return all_pj[0] if len(all_pj) == 1 else None
|
||||||
|
|
||||||
|
# --- Domain: Environment Setup ---
|
||||||
|
|
||||||
|
def clean_proxy_env(env: dict[str, str]) -> None:
|
||||||
|
for k in PROXY_VARS: env.pop(k, None)
|
||||||
|
|
||||||
|
def dotnet_env(repo_root: pathlib.Path) -> dict[str, str]:
|
||||||
|
env = dict(os.environ)
|
||||||
|
clean_proxy_env(env)
|
||||||
|
h, p, c = repo_root/".dotnet_home", repo_root/".nuget"/"packages", repo_root/".nuget"/"http-cache"
|
||||||
|
ensure_dirs([h, p, c])
|
||||||
|
env.update({"DOTNET_CLI_HOME":str(h), "NUGET_PACKAGES":str(p), "NUGET_HTTP_CACHE_PATH":str(c),
|
||||||
|
"DOTNET_SKIP_FIRST_TIME_EXPERIENCE":"1", "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH":"0",
|
||||||
|
"DOTNET_GENERATE_ASPNET_CERTIFICATE":"0",
|
||||||
|
"DOTNET_CLI_TELEMETRY_OPTOUT":"1", "NUGET_CERT_REVOCATION_MODE":"offline"})
|
||||||
|
return env
|
||||||
|
|
||||||
|
def pip_env(repo_root: pathlib.Path) -> dict[str, str]:
|
||||||
|
env = dict(os.environ)
|
||||||
|
clean_proxy_env(env)
|
||||||
|
c, t = repo_root/".pip"/"cache", repo_root/".tmp"/"pip-temp"
|
||||||
|
ensure_dirs([c, t])
|
||||||
|
env.update({"PIP_CACHE_DIR":str(c), "PIP_DISABLE_PIP_VERSION_CHECK":"1", "PIP_DEFAULT_TIMEOUT":"30", "PIP_RETRIES":"2", "TEMP":str(t), "TMP":str(t)})
|
||||||
|
return env
|
||||||
|
|
||||||
|
# --- Domain: Process Execution ---
|
||||||
|
|
||||||
|
def resolve_command(command: str) -> str:
|
||||||
|
if not command or os.name != "nt" or any(s in command for s in ("\\", "/")): return command
|
||||||
|
if pathlib.Path(command).suffix: return shutil.which(command) or command
|
||||||
|
low = command.lower()
|
||||||
|
cands = [f"{command}.cmd", f"{command}.exe", f"{command}.bat", command] if low in ("npm", "npx", "pnpm", "yarn", "tauri") else [command]
|
||||||
|
for c in cands:
|
||||||
|
found = _which_windows(c)
|
||||||
|
if found:
|
||||||
|
if pathlib.Path(found).name.lower() in ("npm", "npx", "pnpm", "yarn", "tauri"):
|
||||||
|
shim = pathlib.Path(found).with_name(pathlib.Path(found).name.lower() + ".cmd")
|
||||||
|
if shim.exists(): return str(shim)
|
||||||
|
return found
|
||||||
|
if low in ("npm", "npx", "pnpm", "yarn"):
|
||||||
|
node = _which_windows("node.exe") or _which_windows("node")
|
||||||
|
if node and (pathlib.Path(node).parent / f"{low}.cmd").exists(): return str(pathlib.Path(node).parent / f"{low}.cmd")
|
||||||
|
return cands[-1]
|
||||||
|
|
||||||
|
def _which_windows(command: str) -> Optional[str]:
|
||||||
|
found = shutil.which(command)
|
||||||
|
if found: return str(pathlib.Path(found).resolve())
|
||||||
|
for p in ["C:\\Program Files\\dotnet", "C:\\Program Files\\nodejs", "C:\\Program Files\\Git\\bin", "C:\\Program Files\\Git\\usr\\bin"]:
|
||||||
|
if (pathlib.Path(p) / command).exists(): return str((pathlib.Path(p) / command).resolve())
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run(command: str, args: list[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> int:
|
||||||
|
try:
|
||||||
|
proc = subprocess.run([resolve_command(command), *args], cwd=str(cwd), env=env, check=False)
|
||||||
|
return proc.returncode
|
||||||
|
except: return 127
|
||||||
|
|
||||||
|
def run_capture(command: str, args: Sequence[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> tuple[int, str, str]:
|
||||||
|
try:
|
||||||
|
proc = subprocess.run([resolve_command(command), *args], cwd=str(cwd), env=env, capture_output=True, text=True, check=False, encoding="utf-8", errors="replace")
|
||||||
|
return proc.returncode, proc.stdout, proc.stderr
|
||||||
|
except: return 127, "", "Command not found"
|
||||||
|
|
||||||
|
def _safe_stream_write(stream: Any, text: str) -> None:
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
stream.write(text)
|
||||||
|
return
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
encoding = getattr(stream, "encoding", None) or "utf-8"
|
||||||
|
data = text.encode(encoding, errors="replace")
|
||||||
|
buffer = getattr(stream, "buffer", None)
|
||||||
|
if buffer is not None:
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Last resort when no binary buffer is exposed.
|
||||||
|
stream.write(data.decode(encoding, errors="replace"))
|
||||||
|
|
||||||
|
def run_step_with_summary(label: str, command: str, args: Sequence[str], cwd: pathlib.Path, env: dict[str, str] | None = None) -> SdtResult:
|
||||||
|
print(f"\n> {label}\n$ {command} {' '.join(args)}")
|
||||||
|
start = time.time()
|
||||||
|
code, out, err = run_capture(command, args, cwd, env)
|
||||||
|
elapsed = round(time.time() - start, 3)
|
||||||
|
if out: _safe_stream_write(sys.stdout, out)
|
||||||
|
if err: _safe_stream_write(sys.stderr, err)
|
||||||
|
return {"exit_code": code, "status": "ok" if code == 0 else "failed", "elapsed_seconds": elapsed, "stdout": out, "stderr": err, "failure_reason": None, "skip_reason": None}
|
||||||
|
|
||||||
|
# --- Domain: High-Level Build Logic ---
|
||||||
|
|
||||||
|
def ensure_dotnet_publish(csproj: pathlib.Path, output_dir: pathlib.Path, configuration: str = "Release", runtime: str = "win-x64", single_file: bool = False, self_contained: bool = True) -> SdtResult:
|
||||||
|
repo_root = resolve_repo_root(str(csproj.parent))
|
||||||
|
env = dotnet_env(repo_root)
|
||||||
|
args = ["publish", str(csproj), "-c", configuration, "-r", runtime, "--self-contained", "true" if self_contained else "false",
|
||||||
|
f"-p:PublishSingleFile={'true' if single_file else 'false'}", "-p:RestoreIgnoreFailedSources=true", "-p:NuGetAudit=false", "-o", str(output_dir)]
|
||||||
|
if single_file: args.append("-p:IncludeNativeLibrariesForSelfExtract=true")
|
||||||
|
res = run_step_with_summary(f"Publishing {csproj.name}", "dotnet", args, repo_root, env)
|
||||||
|
if res["exit_code"] != 0 and single_file:
|
||||||
|
comb = (res["stdout"] + res["stderr"]).lower()
|
||||||
|
if "generatebundle" in comb and "same bundlerelativepath" in comb:
|
||||||
|
print("Duplicate bundle entries detected; retrying without single-file optimization...")
|
||||||
|
retry = [a if "PublishSingleFile=true" not in a else "-p:PublishSingleFile=false" for a in args]
|
||||||
|
res = run_step_with_summary(f"Publishing {csproj.name} (retry)", "dotnet", retry, repo_root, env)
|
||||||
|
if res["exit_code"] != 0:
|
||||||
|
comb = (res["stdout"] + res["stderr"]).lower()
|
||||||
|
if "netsdk1152" in comb and "multiple publish output files with the same relative path" in comb:
|
||||||
|
print("Duplicate publish output files detected (NETSDK1152); retrying with ErrorOnDuplicatePublishOutputFiles=false...")
|
||||||
|
retry = list(args)
|
||||||
|
if not any("ErrorOnDuplicatePublishOutputFiles" in a for a in retry):
|
||||||
|
retry.append("-p:ErrorOnDuplicatePublishOutputFiles=false")
|
||||||
|
res = run_step_with_summary(f"Publishing {csproj.name} (dedupe retry)", "dotnet", retry, repo_root, env)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def ensure_npm_build(app_root: pathlib.Path, target: str = "web", configuration: str = "Release", tauri_bundles: str = "none") -> SdtResult:
|
||||||
|
pj, lock = app_root / "package.json", first_existing([app_root / "package-lock.json", app_root / "npm-shrinkwrap.json"])
|
||||||
|
nm = app_root / "node_modules"
|
||||||
|
h_file, exp_h = nm / ".sdt-deps.sha256", sha256_files([pj, lock] if lock else [pj])
|
||||||
|
should_inst = not nm.exists() or not h_file.exists() or h_file.read_text(encoding="utf-8").strip() != exp_h
|
||||||
|
if should_inst:
|
||||||
|
args = ["ci", "--no-audit", "--fund=false"] if lock else ["install", "--no-audit", "--fund=false"]
|
||||||
|
res = run_step_with_summary("Installing NPM dependencies", "npm", args, app_root)
|
||||||
|
if res["exit_code"] != 0 and lock and args[0] == "ci":
|
||||||
|
res = run_step_with_summary("Installing NPM dependencies (retry)", "npm", ["install", "--no-audit", "--fund=false"], app_root)
|
||||||
|
if res["exit_code"] != 0: return res
|
||||||
|
ensure_dirs([nm]); h_file.write_text(exp_h, encoding="utf-8")
|
||||||
|
if target == "web": return run_step_with_summary(f"Building Web ({configuration})", "npm", ["run", "build"], app_root)
|
||||||
|
t_cmd = ["run", "tauri", "build"]
|
||||||
|
tail = (["--no-bundle"] if tauri_bundles == "none" else ["--bundles", tauri_bundles]) + (["--debug"] if configuration == "Debug" else [])
|
||||||
|
if tail: t_cmd.extend(["--", *tail])
|
||||||
|
return run_step_with_summary(f"Building Tauri ({configuration})", "npm", t_cmd, app_root)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from script_common import newest_file, resolve_repo_root
|
from script_common import newest_file, resolve_repo_root # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def copy_tree_contents(src: Path, dst: Path) -> None:
|
def copy_tree_contents(src: Path, dst: Path) -> None:
|
||||||
@ -21,31 +21,36 @@ def copy_tree_contents(src: Path, dst: Path) -> None:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="Sync newest built assets into output folder")
|
parser = argparse.ArgumentParser(description="Sync newest built assets into output folder")
|
||||||
parser.add_argument("--repo-root", default=None)
|
_ = parser.add_argument("--repo-root", default=None)
|
||||||
parser.add_argument("--output-dir", default="output")
|
_ = parser.add_argument("--output-dir", default="output")
|
||||||
parser.add_argument("--web-build-dir", default=None, help="Path to web build 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("--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("--gateway-bin-dir", default=None, help="Path to gateway bin root")
|
||||||
parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root")
|
_ = parser.add_argument("--tauri-target-dir", default=None, help="Path to tauri target root")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = resolve_repo_root(args.repo_root)
|
repo_root = resolve_repo_root(args.repo_root)
|
||||||
output_dir = (repo_root / args.output_dir).resolve()
|
output_dir = (repo_root / args.output_dir).resolve()
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
web_build = (repo_root / args.web_build_dir).resolve() if args.web_build_dir else None
|
web_build_dir_val = args.web_build_dir
|
||||||
|
web_build = (repo_root / web_build_dir_val).resolve() if web_build_dir_val else None
|
||||||
if web_build is None:
|
if web_build is None:
|
||||||
web_build = next((p for p in repo_root.rglob("build") if (p.parent / "package.json").exists()), 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():
|
if web_build is not None and web_build.exists():
|
||||||
web_out = output_dir / "webgateway" / "wwwroot"
|
web_out = output_dir / "webgateway" / "wwwroot"
|
||||||
copy_tree_contents(web_build, web_out)
|
copy_tree_contents(web_build, web_out)
|
||||||
print(f"Synced web assets -> {web_out}")
|
print(f"Synced web assets -> {web_out}")
|
||||||
|
|
||||||
sidecar_bin = (repo_root / args.sidecar_bin_dir).resolve() if args.sidecar_bin_dir else None
|
sidecar_bin_dir_val = args.sidecar_bin_dir
|
||||||
|
sidecar_bin = (repo_root / sidecar_bin_dir_val).resolve() if sidecar_bin_dir_val else None
|
||||||
if sidecar_bin is None:
|
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_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
|
sidecar_bin = sidecar_proj / "bin" if sidecar_proj else None
|
||||||
|
|
||||||
if sidecar_bin is not None:
|
if sidecar_bin is not None:
|
||||||
|
sidecar_exe = None
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
sidecar_exe = newest_file(sidecar_bin, "*.exe")
|
sidecar_exe = newest_file(sidecar_bin, "*.exe")
|
||||||
else:
|
else:
|
||||||
@ -56,11 +61,14 @@ def main() -> int:
|
|||||||
copy_tree_contents(sidecar_exe.parent, output_dir)
|
copy_tree_contents(sidecar_exe.parent, output_dir)
|
||||||
print(f"Synced sidecar -> {output_dir}")
|
print(f"Synced sidecar -> {output_dir}")
|
||||||
|
|
||||||
gateway_bin = (repo_root / args.gateway_bin_dir).resolve() if args.gateway_bin_dir else None
|
gateway_bin_dir_val = args.gateway_bin_dir
|
||||||
|
gateway_bin = (repo_root / gateway_bin_dir_val).resolve() if gateway_bin_dir_val else None
|
||||||
if gateway_bin is None:
|
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_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
|
gateway_bin = gateway_proj / "bin" if gateway_proj else None
|
||||||
|
|
||||||
if gateway_bin is not None:
|
if gateway_bin is not None:
|
||||||
|
gw_exe = None
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
gw_exe = newest_file(gateway_bin, "*.exe")
|
gw_exe = newest_file(gateway_bin, "*.exe")
|
||||||
else:
|
else:
|
||||||
@ -72,11 +80,14 @@ def main() -> int:
|
|||||||
copy_tree_contents(gw_exe.parent, gw_out)
|
copy_tree_contents(gw_exe.parent, gw_out)
|
||||||
print(f"Synced gateway -> {gw_out}")
|
print(f"Synced gateway -> {gw_out}")
|
||||||
|
|
||||||
tauri_target = (repo_root / args.tauri_target_dir).resolve() if args.tauri_target_dir else None
|
tauri_target_dir_val = args.tauri_target_dir
|
||||||
|
tauri_target = (repo_root / tauri_target_dir_val).resolve() if tauri_target_dir_val else None
|
||||||
if tauri_target is None:
|
if tauri_target is None:
|
||||||
tauri_target = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None)
|
tauri_src = next((p for p in repo_root.rglob("src-tauri") if (p / "target").exists()), None)
|
||||||
tauri_target = tauri_target / "target" if tauri_target else None
|
tauri_target = tauri_src / "target" if tauri_src else None
|
||||||
|
|
||||||
if tauri_target is not None:
|
if tauri_target is not None:
|
||||||
|
app_exe = None
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
app_exe = newest_file(tauri_target, "*.exe")
|
app_exe = newest_file(tauri_target, "*.exe")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -5,23 +5,23 @@ import pathlib
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
from typing import Any, Optional, Sequence
|
||||||
|
|
||||||
from script_common import resolve_command, resolve_repo_root
|
from script_common import resolve_command, resolve_repo_root
|
||||||
|
|
||||||
|
|
||||||
def load_config(project_root: pathlib.Path) -> dict:
|
def load_config(project_root: pathlib.Path) -> dict[str, Any]:
|
||||||
config_path = project_root / "devtool.json"
|
sdt_configs = list(project_root.glob("sdtconfig-*.json"))
|
||||||
|
config_path = sdt_configs[0] if sdt_configs else (project_root / "devtool.json")
|
||||||
if not config_path.exists():
|
if not config_path.exists():
|
||||||
raise FileNotFoundError(f"devtool.json not found at: {config_path}")
|
raise FileNotFoundError(f"Project config not found at: {config_path}")
|
||||||
return json.loads(config_path.read_text(encoding="utf-8"))
|
return json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def iter_workflows(config: dict, selected: Optional[set[str]]) -> List[dict]:
|
def iter_workflows(config: dict[str, Any], selected: Optional[set[str]]) -> list[dict[str, Any]]:
|
||||||
workflows = config.get("workflows", [])
|
workflows = config.get("workflows", [])
|
||||||
if not isinstance(workflows, list):
|
if not isinstance(workflows, list):
|
||||||
return []
|
return []
|
||||||
normalized: List[dict] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)]
|
normalized: list[dict[str, Any]] = [w for w in workflows if isinstance(w, dict) and isinstance(w.get("id"), str)]
|
||||||
if selected:
|
if selected:
|
||||||
normalized = [w for w in normalized if w["id"] in selected]
|
normalized = [w for w in normalized if w["id"] in selected]
|
||||||
return normalized
|
return normalized
|
||||||
@ -45,21 +45,25 @@ def resolve_script_arg(project_root: pathlib.Path, working_dir: pathlib.Path, ar
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
|
def static_check_workflow(project_root: pathlib.Path, workflow: dict[str, Any]) -> dict[str, Any]:
|
||||||
result = {
|
result: dict[str, Any] = {
|
||||||
"workflowId": workflow.get("id"),
|
"workflowId": workflow.get("id"),
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"issues": [],
|
"issues": [],
|
||||||
"steps": [],
|
"steps": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
for step in workflow.get("steps", []):
|
steps = workflow.get("steps", [])
|
||||||
|
if not isinstance(steps, list):
|
||||||
|
return result
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
if not isinstance(step, dict):
|
if not isinstance(step, dict):
|
||||||
continue
|
continue
|
||||||
step_id = step.get("id", "<unknown>")
|
step_id = step.get("id", "<unknown>")
|
||||||
step_result = {"stepId": step_id, "ok": True, "issues": []}
|
step_result: dict[str, Any] = {"stepId": step_id, "ok": True, "issues": []}
|
||||||
|
|
||||||
working_dir_rel = step.get("workingDir") or "."
|
working_dir_rel = str(step.get("workingDir") or ".")
|
||||||
working_dir = (project_root / working_dir_rel).resolve()
|
working_dir = (project_root / working_dir_rel).resolve()
|
||||||
if not working_dir.exists():
|
if not working_dir.exists():
|
||||||
step_result["ok"] = False
|
step_result["ok"] = False
|
||||||
@ -75,14 +79,13 @@ def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
|
|||||||
step_result["issues"].append(f"command_not_found:{command}")
|
step_result["issues"].append(f"command_not_found:{command}")
|
||||||
|
|
||||||
if command.lower() in ("python", "py", "python3", "python.exe", "py.exe"):
|
if command.lower() in ("python", "py", "python3", "python.exe", "py.exe"):
|
||||||
if args and isinstance(args[0], str) and args[0].endswith(".py"):
|
if isinstance(args, list) and args and isinstance(args[0], str) and args[0].endswith(".py"):
|
||||||
script_path = resolve_script_arg(project_root, working_dir, args[0])
|
script_path = resolve_script_arg(project_root, working_dir, args[0])
|
||||||
if not script_path.exists():
|
if not script_path.exists():
|
||||||
step_result["ok"] = False
|
step_result["ok"] = False
|
||||||
step_result["issues"].append(f"python_script_not_found:{script_path}")
|
step_result["issues"].append(f"python_script_not_found:{script_path}")
|
||||||
|
|
||||||
if isinstance(action, str) and action.strip():
|
if isinstance(action, str) and action.strip():
|
||||||
# Action-based steps still require workingDir existence for reliable execution.
|
|
||||||
if not working_dir.exists():
|
if not working_dir.exists():
|
||||||
step_result["ok"] = False
|
step_result["ok"] = False
|
||||||
step_result["issues"].append("action_working_dir_not_found")
|
step_result["issues"].append("action_working_dir_not_found")
|
||||||
@ -96,8 +99,8 @@ def static_check_workflow(project_root: pathlib.Path, workflow: dict) -> dict:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]:
|
def sdt_attempts(repo_root: pathlib.Path) -> list[list[str]]:
|
||||||
attempts: List[List[str]] = []
|
attempts: list[list[str]] = []
|
||||||
attempts.append(["sdt"])
|
attempts.append(["sdt"])
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
attempts.append(["sdt.exe"])
|
attempts.append(["sdt.exe"])
|
||||||
@ -110,9 +113,8 @@ def sdt_attempts(repo_root: pathlib.Path) -> List[List[str]]:
|
|||||||
if devtool_csproj.exists():
|
if devtool_csproj.exists():
|
||||||
attempts.append(["dotnet", "run", "--project", str(devtool_csproj), "--"])
|
attempts.append(["dotnet", "run", "--project", str(devtool_csproj), "--"])
|
||||||
|
|
||||||
# Preserve order but dedupe exact attempts.
|
seen: set[tuple[str, ...]] = set()
|
||||||
seen = set()
|
unique: list[list[str]] = []
|
||||||
unique: List[List[str]] = []
|
|
||||||
for a in attempts:
|
for a in attempts:
|
||||||
key = tuple(a)
|
key = tuple(a)
|
||||||
if key in seen:
|
if key in seen:
|
||||||
@ -126,8 +128,8 @@ def try_run_sdt(
|
|||||||
repo_root: pathlib.Path,
|
repo_root: pathlib.Path,
|
||||||
command_args: Sequence[str],
|
command_args: Sequence[str],
|
||||||
timeout_seconds: int,
|
timeout_seconds: int,
|
||||||
) -> Tuple[Optional[subprocess.CompletedProcess], Optional[str]]:
|
) -> tuple[Optional[subprocess.CompletedProcess[str]], Optional[str]]:
|
||||||
errors: List[str] = []
|
errors: list[str] = []
|
||||||
for base in sdt_attempts(repo_root):
|
for base in sdt_attempts(repo_root):
|
||||||
cmd = [*base, *command_args]
|
cmd = [*base, *command_args]
|
||||||
try:
|
try:
|
||||||
@ -147,7 +149,7 @@ def try_run_sdt(
|
|||||||
return None, "; ".join(errors) if errors else "no_sdt_attempts"
|
return None, "; ".join(errors) if errors else "no_sdt_attempts"
|
||||||
|
|
||||||
|
|
||||||
def parse_headless_summary(stdout: str) -> Optional[Dict[str, Any]]:
|
def parse_headless_summary(stdout: str) -> Optional[dict[str, Any]]:
|
||||||
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
||||||
for line in reversed(lines):
|
for line in reversed(lines):
|
||||||
if not line.startswith("{"):
|
if not line.startswith("{"):
|
||||||
@ -167,8 +169,8 @@ def execute_check_workflow(
|
|||||||
workflow_id: str,
|
workflow_id: str,
|
||||||
env_profile: Optional[str],
|
env_profile: Optional[str],
|
||||||
timeout_seconds: int,
|
timeout_seconds: int,
|
||||||
) -> dict:
|
) -> dict[str, Any]:
|
||||||
args = [
|
run_args = [
|
||||||
"run",
|
"run",
|
||||||
workflow_id,
|
workflow_id,
|
||||||
"--json",
|
"--json",
|
||||||
@ -177,9 +179,9 @@ def execute_check_workflow(
|
|||||||
"--non-interactive",
|
"--non-interactive",
|
||||||
]
|
]
|
||||||
if env_profile:
|
if env_profile:
|
||||||
args.extend(["--env-profile", env_profile])
|
run_args.extend(["--env-profile", env_profile])
|
||||||
|
|
||||||
proc, attempted = try_run_sdt(repo_root, args, timeout_seconds)
|
proc, attempted = try_run_sdt(repo_root, run_args, timeout_seconds)
|
||||||
if proc is None:
|
if proc is None:
|
||||||
return {
|
return {
|
||||||
"workflowId": workflow_id,
|
"workflowId": workflow_id,
|
||||||
@ -215,13 +217,13 @@ def main() -> int:
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Verify SDT workflow routes (static path checks + optional headless execution)."
|
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("--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("--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("--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("--execute", action="store_true", help="Also run each workflow via `sdt run ... --json`")
|
||||||
parser.add_argument("--env-profile", default=None)
|
_ = parser.add_argument("--env-profile", default=None)
|
||||||
parser.add_argument("--timeout-seconds", type=int, default=600)
|
_ = parser.add_argument("--timeout-seconds", type=int, default=600)
|
||||||
parser.add_argument("--output-json", default=None, help="Write full report JSON to file")
|
_ = parser.add_argument("--output-json", default=None, help="Write full report JSON to file")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo_root = resolve_repo_root(args.repo_root)
|
repo_root = resolve_repo_root(args.repo_root)
|
||||||
@ -235,10 +237,10 @@ def main() -> int:
|
|||||||
return 2
|
return 2
|
||||||
|
|
||||||
static_results = [static_check_workflow(project_root, w) for w in workflows]
|
static_results = [static_check_workflow(project_root, w) for w in workflows]
|
||||||
execute_results: List[dict] = []
|
execute_results: list[dict[str, Any]] = []
|
||||||
if args.execute:
|
if args.execute:
|
||||||
for w in workflows:
|
for w in workflows:
|
||||||
wid = w["id"]
|
wid = str(w["id"])
|
||||||
execute_results.append(
|
execute_results.append(
|
||||||
execute_check_workflow(
|
execute_check_workflow(
|
||||||
repo_root=repo_root,
|
repo_root=repo_root,
|
||||||
@ -252,7 +254,7 @@ def main() -> int:
|
|||||||
static_failures = [r for r in static_results if not r["ok"]]
|
static_failures = [r for r in static_results if not r["ok"]]
|
||||||
exec_failures = [r for r in execute_results if not r["ok"]]
|
exec_failures = [r for r in execute_results if not r["ok"]]
|
||||||
|
|
||||||
report = {
|
report: dict[str, Any] = {
|
||||||
"repoRoot": str(repo_root),
|
"repoRoot": str(repo_root),
|
||||||
"projectRoot": str(project_root),
|
"projectRoot": str(project_root),
|
||||||
"totalWorkflows": len(workflows),
|
"totalWorkflows": len(workflows),
|
||||||
@ -283,13 +285,13 @@ def main() -> int:
|
|||||||
|
|
||||||
if static_failures:
|
if static_failures:
|
||||||
print("\nStatic failures:")
|
print("\nStatic failures:")
|
||||||
for f in static_failures:
|
for sf in static_failures:
|
||||||
print(f"- {f['workflowId']}: {', '.join(f['issues'])}")
|
print(f"- {sf['workflowId']}: {', '.join(sf['issues'])}")
|
||||||
|
|
||||||
if exec_failures:
|
if exec_failures:
|
||||||
print("\nExecution failures:")
|
print("\nExecution failures:")
|
||||||
for f in exec_failures:
|
for ef in exec_failures:
|
||||||
print(f"- {f['workflowId']}: stopReason={f.get('stopReason')} message={f.get('message')}")
|
print(f"- {ef['workflowId']}: stopReason={ef.get('stopReason')} message={ef.get('message')}")
|
||||||
|
|
||||||
return 1 if static_failures or exec_failures else 0
|
return 1 if static_failures or exec_failures else 0
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,16 @@ public static class ConfigBootstrapper
|
|||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> KnownScriptHelpers = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"publish-sidecar.py",
|
||||||
|
"publish-app.py",
|
||||||
|
"publish-webgateway.py",
|
||||||
|
"publish-output.py",
|
||||||
|
"sync-output.py",
|
||||||
|
"run-webgateway.py",
|
||||||
|
};
|
||||||
|
|
||||||
public static BootstrapScanResult Scan(string startDir)
|
public static BootstrapScanResult Scan(string startDir)
|
||||||
{
|
{
|
||||||
var root = FindProjectRoot(startDir);
|
var root = FindProjectRoot(startDir);
|
||||||
@ -166,12 +176,14 @@ public static class ConfigBootstrapper
|
|||||||
rootHints.Add("scripts");
|
rootHints.Add("scripts");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var projectName = new DirectoryInfo(root).Name;
|
||||||
|
|
||||||
if (rootHints.Count == 0)
|
if (rootHints.Count == 0)
|
||||||
rootHints.Add("devtool.json");
|
rootHints.Add($"sdtconfig-{projectName}.json");
|
||||||
|
|
||||||
return new BootstrapScanResult(
|
return new BootstrapScanResult(
|
||||||
ProjectRoot: root,
|
ProjectRoot: root,
|
||||||
ProjectName: new DirectoryInfo(root).Name,
|
ProjectName: projectName,
|
||||||
ProjectType: InferProjectType(toolFamilies),
|
ProjectType: InferProjectType(toolFamilies),
|
||||||
ToolFamilies: toolFamilies.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(),
|
ToolFamilies: toolFamilies.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(),
|
||||||
DotnetWorkingDir: dotnetWorkingDir,
|
DotnetWorkingDir: dotnetWorkingDir,
|
||||||
@ -299,11 +311,12 @@ public static class ConfigBootstrapper
|
|||||||
|
|
||||||
public static string ToJson(DevToolConfig config) => JsonSerializer.Serialize(config, JsonOptions) + Environment.NewLine;
|
public static string ToJson(DevToolConfig config) => JsonSerializer.Serialize(config, JsonOptions) + Environment.NewLine;
|
||||||
|
|
||||||
public static string WriteDefaultConfig(string projectRoot, DevToolConfig config, bool overwrite = false)
|
public static string WriteDefaultConfig(string projectRoot, DevToolConfig config, bool overwrite = false, bool legacyNaming = false)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(projectRoot, "devtool.json");
|
var fileName = legacyNaming ? "devtool.json" : $"sdtconfig-{config.Name}.json";
|
||||||
|
var path = Path.Combine(projectRoot, fileName);
|
||||||
if (File.Exists(path) && !overwrite)
|
if (File.Exists(path) && !overwrite)
|
||||||
throw new InvalidOperationException($"devtool.json already exists at {path}");
|
throw new InvalidOperationException($"{fileName} already exists at {path}");
|
||||||
|
|
||||||
File.WriteAllText(path, ToJson(config));
|
File.WriteAllText(path, ToJson(config));
|
||||||
return path;
|
return path;
|
||||||
@ -314,14 +327,14 @@ public static class ConfigBootstrapper
|
|||||||
var has = new Func<string, bool>(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase));
|
var has = new Func<string, bool>(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase));
|
||||||
var scripts = DetectScriptHelpers(scan.ProjectRoot);
|
var scripts = DetectScriptHelpers(scan.ProjectRoot);
|
||||||
|
|
||||||
if (scripts.Contains("publish-sidecar.py") ||
|
if (scripts.ContainsKey("publish-sidecar.py") ||
|
||||||
scripts.Contains("publish-app.py") ||
|
scripts.ContainsKey("publish-app.py") ||
|
||||||
scripts.Contains("publish-webgateway.py") ||
|
scripts.ContainsKey("publish-webgateway.py") ||
|
||||||
scripts.Contains("publish-output.py") ||
|
scripts.ContainsKey("publish-output.py") ||
|
||||||
scripts.Contains("sync-output.py") ||
|
scripts.ContainsKey("sync-output.py") ||
|
||||||
scripts.Contains("run-webgateway.py"))
|
scripts.ContainsKey("run-webgateway.py"))
|
||||||
{
|
{
|
||||||
foreach (var workflow in BuildScriptDrivenWorkflows(scripts))
|
foreach (var workflow in BuildScriptDrivenWorkflows(scripts, scan.NodeWorkingDir))
|
||||||
yield return workflow;
|
yield return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,32 +567,76 @@ public static class ConfigBootstrapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<string> DetectScriptHelpers(string projectRoot)
|
private static Dictionary<string, string> DetectScriptHelpers(string projectRoot)
|
||||||
{
|
{
|
||||||
var scriptsDir = Path.Combine(projectRoot, "scripts");
|
var candidates = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (!Directory.Exists(scriptsDir))
|
foreach (var file in EnumerateFilesBounded(projectRoot, "*.py", MaxScanDepth))
|
||||||
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
{
|
||||||
|
var fileName = Path.GetFileName(file);
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName) || !KnownScriptHelpers.Contains(fileName))
|
||||||
|
continue;
|
||||||
|
|
||||||
return Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly)
|
var dir = Path.GetDirectoryName(file);
|
||||||
.Select(Path.GetFileName)
|
if (string.IsNullOrWhiteSpace(dir))
|
||||||
.Where(f => !string.IsNullOrWhiteSpace(f))
|
continue;
|
||||||
.Cast<string>()
|
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var fullDir = Path.GetFullPath(dir);
|
||||||
|
if (!candidates.TryGetValue(fullDir, out var list))
|
||||||
|
{
|
||||||
|
list = [];
|
||||||
|
candidates[fullDir] = list;
|
||||||
|
}
|
||||||
|
list.Add(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var selectedDir = candidates
|
||||||
|
.OrderByDescending(kvp => kvp.Value.Distinct(StringComparer.OrdinalIgnoreCase).Count())
|
||||||
|
.ThenBy(kvp =>
|
||||||
|
{
|
||||||
|
var rel = Path.GetRelativePath(projectRoot, kvp.Key);
|
||||||
|
return rel.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||||
|
.Count(part => !string.IsNullOrWhiteSpace(part) && part != ".");
|
||||||
|
})
|
||||||
|
.ThenBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.First().Key;
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var file in Directory.EnumerateFiles(selectedDir, "*.py", SearchOption.TopDirectoryOnly))
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(file);
|
||||||
|
if (string.IsNullOrWhiteSpace(name) || !KnownScriptHelpers.Contains(name))
|
||||||
|
continue;
|
||||||
|
result[name] = Path.GetRelativePath(projectRoot, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<WorkflowDefinition> BuildScriptDrivenWorkflows(HashSet<string> scripts)
|
private static IEnumerable<WorkflowDefinition> BuildScriptDrivenWorkflows(
|
||||||
|
IReadOnlyDictionary<string, string> scripts,
|
||||||
|
string? nodeWorkingDir)
|
||||||
{
|
{
|
||||||
static WorkflowStep ScriptStep(string id, string label, params string[] scriptArgs) => new()
|
static WorkflowStep ScriptStep(string id, string label, string scriptPath, params string[] extraArgs)
|
||||||
{
|
{
|
||||||
Id = id,
|
var args = new List<string> { scriptPath };
|
||||||
Label = label,
|
args.AddRange(extraArgs);
|
||||||
Command = "python",
|
return new WorkflowStep
|
||||||
Args = scriptArgs.ToList(),
|
{
|
||||||
WorkingDir = ".",
|
Id = id,
|
||||||
Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }]
|
Label = label,
|
||||||
};
|
Command = "python",
|
||||||
|
Args = args,
|
||||||
|
WorkingDir = ".",
|
||||||
|
Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (scripts.Contains("publish-sidecar.py"))
|
var tauriConfigRequire = ResolveTauriConfigRequirePath(nodeWorkingDir);
|
||||||
|
|
||||||
|
if (scripts.TryGetValue("publish-sidecar.py", out var publishSidecarPath))
|
||||||
{
|
{
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
{
|
{
|
||||||
@ -587,11 +644,12 @@ public static class ConfigBootstrapper
|
|||||||
Label = "Publish Sidecar",
|
Label = "Publish Sidecar",
|
||||||
Description = "Publish sidecar service",
|
Description = "Publish sidecar service",
|
||||||
Group = "Build",
|
Group = "Build",
|
||||||
Steps = [ScriptStep("sidecar:run", "python scripts/publish-sidecar.py", "scripts/publish-sidecar.py")]
|
Steps = [ScriptStep("sidecar:run", $"python {publishSidecarPath}", publishSidecarPath)],
|
||||||
|
RequireFiles = [publishSidecarPath]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scripts.Contains("publish-app.py"))
|
if (scripts.TryGetValue("publish-app.py", out var publishAppPath))
|
||||||
{
|
{
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
{
|
{
|
||||||
@ -601,8 +659,9 @@ public static class ConfigBootstrapper
|
|||||||
Group = "Build",
|
Group = "Build",
|
||||||
Steps =
|
Steps =
|
||||||
[
|
[
|
||||||
ScriptStep("web:run", "python scripts/publish-app.py --target web", "scripts/publish-app.py", "--target", "web")
|
ScriptStep("web:run", $"python {publishAppPath} --target web", publishAppPath, "--target", "web")
|
||||||
]
|
],
|
||||||
|
RequireFiles = [publishAppPath]
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
@ -611,16 +670,17 @@ public static class ConfigBootstrapper
|
|||||||
Label = "Build Tauri Desktop App",
|
Label = "Build Tauri Desktop App",
|
||||||
Description = "Build desktop binary",
|
Description = "Build desktop binary",
|
||||||
Group = "Build",
|
Group = "Build",
|
||||||
DependsOn = scripts.Contains("publish-sidecar.py") ? ["sidecar"] : [],
|
DependsOn = scripts.ContainsKey("publish-sidecar.py") ? ["sidecar"] : [],
|
||||||
Steps =
|
Steps =
|
||||||
[
|
[
|
||||||
ScriptStep("tauri:run", "python scripts/publish-app.py --target tauri --tauri-bundles none",
|
ScriptStep("tauri:run", $"python {publishAppPath} --target tauri --tauri-bundles none",
|
||||||
"scripts/publish-app.py", "--target", "tauri", "--tauri-bundles", "none")
|
publishAppPath, "--target", "tauri", "--tauri-bundles", "none")
|
||||||
]
|
],
|
||||||
|
RequireFiles = [publishAppPath, tauriConfigRequire]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scripts.Contains("publish-webgateway.py"))
|
if (scripts.TryGetValue("publish-webgateway.py", out var publishWebgatewayPath))
|
||||||
{
|
{
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
{
|
{
|
||||||
@ -628,12 +688,13 @@ public static class ConfigBootstrapper
|
|||||||
Label = "Publish WebGateway",
|
Label = "Publish WebGateway",
|
||||||
Description = "Publish ASP.NET gateway",
|
Description = "Publish ASP.NET gateway",
|
||||||
Group = "Build",
|
Group = "Build",
|
||||||
DependsOn = scripts.Contains("publish-app.py") ? ["web"] : [],
|
DependsOn = scripts.ContainsKey("publish-app.py") ? ["web"] : [],
|
||||||
Steps = [ScriptStep("webgateway:run", "python scripts/publish-webgateway.py", "scripts/publish-webgateway.py")]
|
Steps = [ScriptStep("webgateway:run", $"python {publishWebgatewayPath}", publishWebgatewayPath)],
|
||||||
|
RequireFiles = [publishWebgatewayPath]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scripts.Contains("sync-output.py"))
|
if (scripts.TryGetValue("sync-output.py", out var syncOutputPath))
|
||||||
{
|
{
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
{
|
{
|
||||||
@ -641,11 +702,12 @@ public static class ConfigBootstrapper
|
|||||||
Label = "Sync Output",
|
Label = "Sync Output",
|
||||||
Description = "Sync newest artifacts to output",
|
Description = "Sync newest artifacts to output",
|
||||||
Group = "Build",
|
Group = "Build",
|
||||||
Steps = [ScriptStep("sync-output:run", "python scripts/sync-output.py", "scripts/sync-output.py")]
|
Steps = [ScriptStep("sync-output:run", $"python {syncOutputPath}", syncOutputPath)],
|
||||||
|
RequireFiles = [syncOutputPath]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scripts.Contains("publish-output.py"))
|
if (scripts.TryGetValue("publish-output.py", out var publishOutputPath))
|
||||||
{
|
{
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
{
|
{
|
||||||
@ -653,11 +715,12 @@ public static class ConfigBootstrapper
|
|||||||
Label = "Stage Output Bundle",
|
Label = "Stage Output Bundle",
|
||||||
Description = "Publish and stage distributable output",
|
Description = "Publish and stage distributable output",
|
||||||
Group = "Build",
|
Group = "Build",
|
||||||
Steps = [ScriptStep("stage-output:run", "python scripts/publish-output.py", "scripts/publish-output.py")]
|
Steps = [ScriptStep("stage-output:run", $"python {publishOutputPath}", publishOutputPath)],
|
||||||
|
RequireFiles = [publishOutputPath]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scripts.Contains("run-webgateway.py"))
|
if (scripts.TryGetValue("run-webgateway.py", out var runWebgatewayPath))
|
||||||
{
|
{
|
||||||
yield return new WorkflowDefinition
|
yield return new WorkflowDefinition
|
||||||
{
|
{
|
||||||
@ -667,13 +730,21 @@ public static class ConfigBootstrapper
|
|||||||
Group = "Dev",
|
Group = "Dev",
|
||||||
Steps =
|
Steps =
|
||||||
[
|
[
|
||||||
ScriptStep("run-gateway-dev:run", "python scripts/run-webgateway.py --mode Dev",
|
ScriptStep("run-gateway-dev:run", $"python {runWebgatewayPath} --mode Dev",
|
||||||
"scripts/run-webgateway.py", "--mode", "Dev")
|
runWebgatewayPath, "--mode", "Dev")
|
||||||
]
|
],
|
||||||
|
RequireFiles = [runWebgatewayPath]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveTauriConfigRequirePath(string? nodeWorkingDir)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nodeWorkingDir) || nodeWorkingDir == ".")
|
||||||
|
return "src-tauri/tauri.conf.json";
|
||||||
|
return Path.Combine(nodeWorkingDir, "src-tauri", "tauri.conf.json").Replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
private static string FindProjectRoot(string startDir)
|
private static string FindProjectRoot(string startDir)
|
||||||
{
|
{
|
||||||
var start = Path.GetFullPath(startDir);
|
var start = Path.GetFullPath(startDir);
|
||||||
@ -786,6 +857,18 @@ public static class ConfigBootstrapper
|
|||||||
if (candidates.Count == 0)
|
if (candidates.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
// Prefer app roots that include a Tauri config when available.
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (!HasNodeScripts(candidate))
|
||||||
|
continue;
|
||||||
|
var dir = Path.GetDirectoryName(candidate);
|
||||||
|
if (string.IsNullOrWhiteSpace(dir))
|
||||||
|
continue;
|
||||||
|
if (IsTauriNodeRoot(dir))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var candidate in candidates)
|
foreach (var candidate in candidates)
|
||||||
{
|
{
|
||||||
if (HasNodeScripts(candidate))
|
if (HasNodeScripts(candidate))
|
||||||
@ -822,6 +905,12 @@ public static class ConfigBootstrapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTauriNodeRoot(string nodeDir)
|
||||||
|
{
|
||||||
|
return File.Exists(Path.Combine(nodeDir, "src-tauri", "tauri.conf.json")) ||
|
||||||
|
File.Exists(Path.Combine(nodeDir, "tauri.conf.json"));
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateFilesBounded(string root, string pattern, int maxDepth)
|
private static IEnumerable<string> EnumerateFilesBounded(string root, string pattern, int maxDepth)
|
||||||
{
|
{
|
||||||
var queue = new Queue<(string Dir, int Depth)>();
|
var queue = new Queue<(string Dir, int Depth)>();
|
||||||
|
|||||||
@ -28,7 +28,7 @@ public static class ConfigLoader
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Walks up from <paramref name="startDir"/> (or CWD) until it finds devtool.json.
|
/// Walks up from <paramref name="startDir"/> (or CWD) until it finds sdtconfig-*.json or devtool.json.
|
||||||
/// Returns null if not found.
|
/// Returns null if not found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string? FindConfigPath(string? startDir = null)
|
public static string? FindConfigPath(string? startDir = null)
|
||||||
@ -36,10 +36,14 @@ public static class ConfigLoader
|
|||||||
var dir = new DirectoryInfo(startDir ?? Directory.GetCurrentDirectory());
|
var dir = new DirectoryInfo(startDir ?? Directory.GetCurrentDirectory());
|
||||||
while (dir is not null)
|
while (dir is not null)
|
||||||
{
|
{
|
||||||
|
var sdtCandidates = dir.GetFiles("sdtconfig-*.json");
|
||||||
|
if (sdtCandidates.Length > 0)
|
||||||
|
return sdtCandidates[0].FullName;
|
||||||
|
|
||||||
var candidate = Path.Combine(dir.FullName, "devtool.json");
|
var candidate = Path.Combine(dir.FullName, "devtool.json");
|
||||||
if (File.Exists(candidate))
|
if (File.Exists(candidate))
|
||||||
return candidate;
|
return candidate;
|
||||||
dir = dir.Parent!;
|
dir = dir.Parent;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -76,7 +80,7 @@ public static class ConfigLoader
|
|||||||
|
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Legacy targets-only config detected at {configPath}. Strict mode requires workflows. " +
|
$"Legacy targets-only config detected at {configPath}. Strict mode requires workflows. " +
|
||||||
"Use migration preview file 'devtool.generated.workflows.json' and migrate devtool.json. " +
|
"Use migration preview file 'devtool.generated.workflows.json' and migrate your config. " +
|
||||||
"Temporary rollback: set SDT_LEGACY_MODE=compat.");
|
"Temporary rollback: set SDT_LEGACY_MODE=compat.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +91,7 @@ public static class ConfigLoader
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Failed to parse devtool.json at {configPath}: {ex.Message}", ex);
|
$"Failed to parse config at {configPath}: {ex.Message}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +106,7 @@ public static class ConfigLoader
|
|||||||
|
|
||||||
var json = File.ReadAllText(configPath);
|
var json = File.ReadAllText(configPath);
|
||||||
var config = JsonSerializer.Deserialize<DevToolConfig>(json, JsonOptions)
|
var config = JsonSerializer.Deserialize<DevToolConfig>(json, JsonOptions)
|
||||||
?? throw new InvalidOperationException("devtool.json deserialized to null.");
|
?? throw new InvalidOperationException("Config deserialized to null.");
|
||||||
|
|
||||||
if (config.Targets.Count == 0)
|
if (config.Targets.Count == 0)
|
||||||
return new LegacyMigrationApplyResult(false, "No legacy targets found to migrate.", ConfigPath: configPath);
|
return new LegacyMigrationApplyResult(false, "No legacy targets found to migrate.", ConfigPath: configPath);
|
||||||
@ -200,7 +204,7 @@ public static class ConfigLoader
|
|||||||
private static DevToolConfig DeserializeConfig(JsonObject obj, string sourcePath)
|
private static DevToolConfig DeserializeConfig(JsonObject obj, string sourcePath)
|
||||||
{
|
{
|
||||||
return obj.Deserialize<DevToolConfig>(JsonOptions)
|
return obj.Deserialize<DevToolConfig>(JsonOptions)
|
||||||
?? throw new InvalidOperationException($"devtool.json at {sourcePath} deserialized to null.");
|
?? throw new InvalidOperationException($"Config at {sourcePath} deserialized to null.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonObject MergeObjects(JsonObject baseObj, JsonObject overlayObj)
|
private static JsonObject MergeObjects(JsonObject baseObj, JsonObject overlayObj)
|
||||||
|
|||||||
@ -67,6 +67,7 @@ public sealed class WorkflowDefinition
|
|||||||
public string Description { get; init; } = "";
|
public string Description { get; init; } = "";
|
||||||
public string Group { get; init; } = "General";
|
public string Group { get; init; } = "General";
|
||||||
public List<string> DependsOn { get; init; } = [];
|
public List<string> DependsOn { get; init; } = [];
|
||||||
|
public List<string> RequireFiles { get; init; } = [];
|
||||||
public List<WorkflowStep> Steps { get; init; } = [];
|
public List<WorkflowStep> Steps { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +101,9 @@ public enum InstallPolicy
|
|||||||
|
|
||||||
public sealed class ToolingConfig
|
public sealed class ToolingConfig
|
||||||
{
|
{
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public InstallPolicy DefaultInstallPolicy { get; init; } = InstallPolicy.Prompt;
|
||||||
|
|
||||||
public List<ToolInstallDefinition> Tools { get; init; } = [];
|
public List<ToolInstallDefinition> Tools { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ public sealed class WorkspaceProject
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Relative or absolute path to the project root
|
/// Relative or absolute path to the project root
|
||||||
/// (the directory containing devtool.json).
|
/// (the directory containing SDT project config: sdtconfig-*.json or devtool.json).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Path { get; init; } = "";
|
public string Path { get; init; } = "";
|
||||||
public List<string> Tags { get; init; } = [];
|
public List<string> Tags { get; init; } = [];
|
||||||
@ -65,5 +65,6 @@ public sealed class WorkspaceInventorySettings
|
|||||||
"*.sln",
|
"*.sln",
|
||||||
"*.csproj",
|
"*.csproj",
|
||||||
"devtool.json",
|
"devtool.json",
|
||||||
|
"sdtconfig-*.json",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,6 +143,13 @@ public sealed class WorkspaceInventoryService : IWorkspaceInventoryService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(marker, "sdtconfig-*.json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (Directory.EnumerateFiles(dir, "sdtconfig-*.json", SearchOption.TopDirectoryOnly).Any())
|
||||||
|
kinds.Add(WorkspaceInventoryKind.Devtool);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(marker, "*.slnx", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(marker, "*.slnx", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (Directory.EnumerateFiles(dir, "*.slnx", SearchOption.TopDirectoryOnly).Any())
|
if (Directory.EnumerateFiles(dir, "*.slnx", SearchOption.TopDirectoryOnly).Any())
|
||||||
@ -245,7 +252,7 @@ public sealed class WorkspaceInventoryService : IWorkspaceInventoryService
|
|||||||
|
|
||||||
var warnings = new List<string>();
|
var warnings = new List<string>();
|
||||||
if (!hasDevtool)
|
if (!hasDevtool)
|
||||||
warnings.Add("No devtool.json found.");
|
warnings.Add("No SDT project config found.");
|
||||||
if (full.Equals(currentRoot, StringComparison.OrdinalIgnoreCase))
|
if (full.Equals(currentRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
warnings.Add("Current project root.");
|
warnings.Add("Current project root.");
|
||||||
|
|
||||||
|
|||||||
@ -144,6 +144,7 @@ public sealed class PrereqInstallerService : IPrereqInstaller
|
|||||||
"java" => isWindows
|
"java" => isWindows
|
||||||
? new InstallCommand("winget", ["install", "Microsoft.OpenJDK.21"])
|
? new InstallCommand("winget", ["install", "Microsoft.OpenJDK.21"])
|
||||||
: new InstallCommand("sh", ["-c", "echo Install JDK via package manager"]),
|
: new InstallCommand("sh", ["-c", "echo Install JDK via package manager"]),
|
||||||
|
"pyinstaller" => new InstallCommand(PythonResolver.ResolveExecutable(), ["-m", "pip", "install", "pyinstaller"]),
|
||||||
_ => new InstallCommand("sh", ["-c", $"echo No installer template for '{tool}'"]),
|
_ => new InstallCommand("sh", ["-c", $"echo No installer template for '{tool}'"]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
247
src/DevTool.Engine/Core/ProjectScaffolder.cs
Normal file
247
src/DevTool.Engine/Core/ProjectScaffolder.cs
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Sdt.Config;
|
||||||
|
using Sdt.Runner;
|
||||||
|
|
||||||
|
namespace Sdt.Core;
|
||||||
|
|
||||||
|
public sealed record ScaffoldingOptions(
|
||||||
|
string ProjectName,
|
||||||
|
string Backend,
|
||||||
|
string Frontend
|
||||||
|
);
|
||||||
|
|
||||||
|
public sealed class ProjectScaffolder
|
||||||
|
{
|
||||||
|
private readonly ActionRunner _runner = new ActionRunner();
|
||||||
|
|
||||||
|
public async Task<bool> ScaffoldAsync(string projectRoot, ScaffoldingOptions options, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput($"Starting scaffolding for {options.ProjectName}...", false);
|
||||||
|
|
||||||
|
var srcDir = Path.Combine(projectRoot, "src");
|
||||||
|
var testDir = Path.Combine(projectRoot, "tests");
|
||||||
|
Directory.CreateDirectory(srcDir);
|
||||||
|
Directory.CreateDirectory(testDir);
|
||||||
|
|
||||||
|
if (options.Backend == "C# (.NET)")
|
||||||
|
{
|
||||||
|
await ScaffoldCSharpBackendAsync(projectRoot, options.ProjectName, onOutput);
|
||||||
|
}
|
||||||
|
else if (options.Backend == "Python")
|
||||||
|
{
|
||||||
|
await ScaffoldPythonBackendAsync(projectRoot, options.ProjectName, onOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.Frontend == "Tauri")
|
||||||
|
{
|
||||||
|
await ScaffoldTauriFrontendAsync(projectRoot, options.ProjectName, onOutput);
|
||||||
|
}
|
||||||
|
else if (options.Frontend == "Web UI")
|
||||||
|
{
|
||||||
|
await ScaffoldWebFrontendAsync(projectRoot, options.ProjectName, onOutput);
|
||||||
|
}
|
||||||
|
else if (options.Frontend == "PyQt")
|
||||||
|
{
|
||||||
|
await ScaffoldPyQtFrontendAsync(projectRoot, options.ProjectName, onOutput);
|
||||||
|
}
|
||||||
|
else if (options.Frontend == "Avalonia")
|
||||||
|
{
|
||||||
|
await ScaffoldAvaloniaFrontendAsync(projectRoot, options.ProjectName, onOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput("Generating initial SDT project configuration...", false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scanResult = ConfigBootstrapper.Scan(projectRoot);
|
||||||
|
var config = ConfigBootstrapper.BuildDefaultConfig(scanResult);
|
||||||
|
ConfigBootstrapper.WriteDefaultConfig(projectRoot, config, overwrite: true);
|
||||||
|
onOutput($"Generated initial project configuration at {projectRoot}", false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
onOutput($"WARNING: Failed to generate initial SDT config: {ex.Message}", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput("Scaffolding completed successfully.", false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScaffoldCSharpBackendAsync(string projectRoot, string projectName, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput("Scaffolding C# Backend...", false);
|
||||||
|
var srcDir = Path.Combine(projectRoot, "src", projectName);
|
||||||
|
var testDir = Path.Combine(projectRoot, "tests", $"{projectName}.Tests");
|
||||||
|
Directory.CreateDirectory(srcDir);
|
||||||
|
Directory.CreateDirectory(testDir);
|
||||||
|
|
||||||
|
// Run dotnet new sln
|
||||||
|
await RunCommandAsync("dotnet", ["new", "sln", "-n", projectName], projectRoot, onOutput);
|
||||||
|
|
||||||
|
// Run dotnet new webapi
|
||||||
|
await RunCommandAsync("dotnet", ["new", "webapi", "-n", projectName, "-o", srcDir], projectRoot, onOutput);
|
||||||
|
|
||||||
|
// Run dotnet new xunit
|
||||||
|
await RunCommandAsync("dotnet", ["new", "xunit", "-n", $"{projectName}.Tests", "-o", testDir], projectRoot, onOutput);
|
||||||
|
|
||||||
|
// Add to sln
|
||||||
|
await RunCommandAsync("dotnet", ["sln", "add", srcDir], projectRoot, onOutput);
|
||||||
|
await RunCommandAsync("dotnet", ["sln", "add", testDir], projectRoot, onOutput);
|
||||||
|
|
||||||
|
// Run dotnet restore
|
||||||
|
onOutput("Running dotnet restore...", false);
|
||||||
|
await RunCommandAsync("dotnet", ["restore"], projectRoot, onOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScaffoldPythonBackendAsync(string projectRoot, string projectName, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput("Scaffolding Python Backend...", false);
|
||||||
|
var srcName = projectName.ToLowerInvariant().Replace(" ", "_").Replace("-", "_");
|
||||||
|
var pkgDir = Path.Combine(projectRoot, "src", srcName);
|
||||||
|
Directory.CreateDirectory(pkgDir);
|
||||||
|
File.WriteAllText(Path.Combine(pkgDir, "__init__.py"), "");
|
||||||
|
File.WriteAllText(Path.Combine(pkgDir, "main.py"), "def main():\n print('Hello from Python backend!')\n\nif __name__ == '__main__':\n main()\n");
|
||||||
|
|
||||||
|
var testDir = Path.Combine(projectRoot, "tests");
|
||||||
|
File.WriteAllText(Path.Combine(testDir, "__init__.py"), "");
|
||||||
|
File.WriteAllText(Path.Combine(testDir, "test_main.py"), "def test_basic():\n assert True\n");
|
||||||
|
|
||||||
|
var pyproject = $@"
|
||||||
|
[project]
|
||||||
|
name = ""{srcName}""
|
||||||
|
version = ""0.1.0""
|
||||||
|
description = ""A Python project""
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = [""setuptools>=61.0""]
|
||||||
|
build-backend = ""setuptools.build_meta""
|
||||||
|
";
|
||||||
|
File.WriteAllText(Path.Combine(projectRoot, "pyproject.toml"), pyproject.Trim());
|
||||||
|
File.WriteAllText(Path.Combine(projectRoot, "requirements.txt"), "pytest\n");
|
||||||
|
|
||||||
|
onOutput("Creating Python virtual environment...", false);
|
||||||
|
await RunCommandAsync("python", ["-m", "venv", ".venv"], projectRoot, onOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScaffoldTauriFrontendAsync(string projectRoot, string projectName, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput("Scaffolding Tauri Frontend...", false);
|
||||||
|
var cmd = OperatingSystem.IsWindows() ? "npm.cmd" : "npm";
|
||||||
|
// To be completely non-interactive, there is unfortunately no great unified tauri skeleton that is purely args without prompts across all versions safely, so we might just create a vite app and add tauri, or use create-tauri-app
|
||||||
|
// Using `npm create vite@latest frontend -- --template react-ts` then adding tauri is safest
|
||||||
|
await RunCommandAsync(cmd, ["create", "vite@latest", "frontend", "--", "--template", "react-ts"], projectRoot, onOutput);
|
||||||
|
|
||||||
|
var frontendDir = Path.Combine(projectRoot, "frontend");
|
||||||
|
await RunCommandAsync(cmd, ["install"], frontendDir, onOutput);
|
||||||
|
await RunCommandAsync(cmd, ["install", "@tauri-apps/api", "@tauri-apps/plugin-core"], frontendDir, onOutput);
|
||||||
|
|
||||||
|
// NOTE: We don't fully initialize tauri here to avoid interactive prompts from `tauri init`.
|
||||||
|
// A minimal tauri.conf.json could be dumped directly.
|
||||||
|
Directory.CreateDirectory(Path.Combine(frontendDir, "src-tauri"));
|
||||||
|
var tauriConf = @"{
|
||||||
|
""build"": {
|
||||||
|
""beforeDevCommand"": ""npm run dev"",
|
||||||
|
""beforeBuildCommand"": ""npm run build"",
|
||||||
|
""devUrl"": ""http://localhost:5173"",
|
||||||
|
""frontendDist"": ""../dist""
|
||||||
|
},
|
||||||
|
""productName"": """ + projectName + @""",
|
||||||
|
""version"": ""0.1.0""
|
||||||
|
}";
|
||||||
|
File.WriteAllText(Path.Combine(frontendDir, "src-tauri", "tauri.conf.json"), tauriConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScaffoldWebFrontendAsync(string projectRoot, string projectName, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput("Scaffolding Web Frontend (React+Vite)...", false);
|
||||||
|
var cmd = OperatingSystem.IsWindows() ? "npm.cmd" : "npm";
|
||||||
|
await RunCommandAsync(cmd, ["create", "vite@latest", "frontend", "--", "--template", "react-ts"], projectRoot, onOutput);
|
||||||
|
|
||||||
|
onOutput("Running npm install...", false);
|
||||||
|
await RunCommandAsync(cmd, ["install"], Path.Combine(projectRoot, "frontend"), onOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScaffoldPyQtFrontendAsync(string projectRoot, string projectName, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput("Scaffolding PyQt Frontend...", false);
|
||||||
|
var reqPath = Path.Combine(projectRoot, "requirements.txt");
|
||||||
|
if (File.Exists(reqPath))
|
||||||
|
{
|
||||||
|
var reqs = File.ReadAllText(reqPath);
|
||||||
|
if (!reqs.Contains("PyQt6"))
|
||||||
|
File.AppendAllText(reqPath, "\nPyQt6\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.WriteAllText(reqPath, "PyQt6\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcName = projectName.ToLowerInvariant().Replace(" ", "_").Replace("-", "_");
|
||||||
|
var pkgDir = Path.Combine(projectRoot, "src", srcName);
|
||||||
|
Directory.CreateDirectory(pkgDir);
|
||||||
|
|
||||||
|
var code = @"
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel
|
||||||
|
|
||||||
|
def launch_gui():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
window = QMainWindow()
|
||||||
|
window.setWindowTitle('" + projectName + @"')
|
||||||
|
window.setCentralWidget(QLabel('Hello from PyQt!'))
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
launch_gui()
|
||||||
|
";
|
||||||
|
File.WriteAllText(Path.Combine(pkgDir, "gui.py"), code.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScaffoldAvaloniaFrontendAsync(string projectRoot, string projectName, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
onOutput("Scaffolding Avalonia Frontend...", false);
|
||||||
|
var srcDir = Path.Combine(projectRoot, "src", $"{projectName}.Desktop");
|
||||||
|
Directory.CreateDirectory(srcDir);
|
||||||
|
|
||||||
|
// Ensure templates are installed
|
||||||
|
await RunCommandAsync("dotnet", ["new", "install", "Avalonia.Templates"], projectRoot, onOutput);
|
||||||
|
|
||||||
|
await RunCommandAsync("dotnet", ["new", "avalonia.app", "-n", $"{projectName}.Desktop", "-o", srcDir], projectRoot, onOutput);
|
||||||
|
|
||||||
|
await RunCommandAsync("dotnet", ["sln", "add", srcDir], projectRoot, onOutput);
|
||||||
|
|
||||||
|
onOutput("Running dotnet restore...", false);
|
||||||
|
await RunCommandAsync("dotnet", ["restore", srcDir], projectRoot, onOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunCommandAsync(string command, string[] args, string workingDir, Action<string, bool> onOutput)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var step = new WorkflowStep
|
||||||
|
{
|
||||||
|
Command = command,
|
||||||
|
Args = args.ToList(),
|
||||||
|
WorkingDir = "." // Command runner will combine this with projectRoot
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _runner.RunStepAsync(
|
||||||
|
step,
|
||||||
|
workingDir,
|
||||||
|
onOutput);
|
||||||
|
|
||||||
|
if (result.ExitCode != 0)
|
||||||
|
{
|
||||||
|
onOutput($"WARNING: Command {command} exitted with code {result.ExitCode}", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
onOutput($"ERROR running {command}: {ex.Message}", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -52,7 +52,7 @@ public sealed class RequirementResolver : IRequirementResolver
|
|||||||
"npm" => [Req("node"), Req("npm")],
|
"npm" => [Req("node"), Req("npm")],
|
||||||
"pnpm" => [Req("node"), Req("pnpm")],
|
"pnpm" => [Req("node"), Req("pnpm")],
|
||||||
"yarn" => [Req("node"), Req("yarn")],
|
"yarn" => [Req("node"), Req("yarn")],
|
||||||
"python" or "py" => [Req("python")],
|
"python" or "py" => InferPythonRequirements(args),
|
||||||
"cargo" => [Req("cargo")],
|
"cargo" => [Req("cargo")],
|
||||||
"tauri" => [Req("cargo"), Req("node"), Req("npm")],
|
"tauri" => [Req("cargo"), Req("node"), Req("npm")],
|
||||||
"git" => [Req("git")],
|
"git" => [Req("git")],
|
||||||
@ -65,6 +65,18 @@ public sealed class RequirementResolver : IRequirementResolver
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ToolRequirement> InferPythonRequirements(IReadOnlyList<string> args)
|
||||||
|
{
|
||||||
|
var result = new List<ToolRequirement> { Req("python") };
|
||||||
|
if (args.Any(a => string.Equals(a, "PyInstaller", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
a.EndsWith("pyinstaller", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
a.EndsWith("pyinstaller.exe", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
result.Add(Req("pyinstaller"));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static ToolRequirement Req(string tool) => new()
|
private static ToolRequirement Req(string tool) => new()
|
||||||
{
|
{
|
||||||
Tool = tool,
|
Tool = tool,
|
||||||
|
|||||||
@ -75,6 +75,7 @@ public sealed class ToolProbeService : IToolProbe
|
|||||||
var command = tool.ToLowerInvariant() switch
|
var command = tool.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"python" => PythonResolver.ResolveExecutable(),
|
"python" => PythonResolver.ResolveExecutable(),
|
||||||
|
"pyinstaller" => PythonResolver.ResolveExecutable(),
|
||||||
"dotnet" => "dotnet",
|
"dotnet" => "dotnet",
|
||||||
"node" => "node",
|
"node" => "node",
|
||||||
"npm" => "npm",
|
"npm" => "npm",
|
||||||
@ -91,7 +92,7 @@ public sealed class ToolProbeService : IToolProbe
|
|||||||
var resolution = CommandResolver.ResolveWithTrace(command, config, tool);
|
var resolution = CommandResolver.ResolveWithTrace(command, config, tool);
|
||||||
command = resolution.Resolved;
|
command = resolution.Resolved;
|
||||||
|
|
||||||
var versionArg = command is "python" ? "--version" : "--version";
|
var versionArg = tool.ToLowerInvariant() == "pyinstaller" ? "-m PyInstaller --version" : "--version";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
@ -105,9 +106,21 @@ public sealed class ToolProbeService : IToolProbe
|
|||||||
if (envOverrides is not null)
|
if (envOverrides is not null)
|
||||||
{
|
{
|
||||||
foreach (var kvp in envOverrides)
|
foreach (var kvp in envOverrides)
|
||||||
|
{
|
||||||
psi.Environment[kvp.Key] = kvp.Value;
|
psi.Environment[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool.ToLowerInvariant() == "pyinstaller")
|
||||||
|
{
|
||||||
|
psi.ArgumentList.Add("-m");
|
||||||
|
psi.ArgumentList.Add("PyInstaller");
|
||||||
|
psi.ArgumentList.Add("--version");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
psi.ArgumentList.Add(versionArg);
|
||||||
}
|
}
|
||||||
psi.ArgumentList.Add(versionArg);
|
|
||||||
|
|
||||||
using var process = new Process { StartInfo = psi };
|
using var process = new Process { StartInfo = psi };
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Sdt.Config;
|
using Sdt.Config;
|
||||||
|
using Sdt.Runner;
|
||||||
|
|
||||||
namespace Sdt.Core;
|
namespace Sdt.Core;
|
||||||
|
|
||||||
@ -222,12 +223,73 @@ public sealed class WorkflowExecutor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var runResult = await _actionRunner.RunStepAsync(
|
RunResult runResult;
|
||||||
step,
|
int retryCount = 0;
|
||||||
projectRoot,
|
const int MaxRetries = 1;
|
||||||
onOutput,
|
|
||||||
envOverrides,
|
while (true)
|
||||||
cancellationToken).ConfigureAwait(false);
|
{
|
||||||
|
var capturedOutput = new List<string>();
|
||||||
|
runResult = await _actionRunner.RunStepAsync(
|
||||||
|
step,
|
||||||
|
projectRoot,
|
||||||
|
(line, isErr) =>
|
||||||
|
{
|
||||||
|
onOutput(line, isErr);
|
||||||
|
if (isErr) capturedOutput.Add(line);
|
||||||
|
},
|
||||||
|
envOverrides,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (runResult.Success || retryCount >= MaxRetries)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var missingTool = DetectMissingToolFromOutput(capturedOutput);
|
||||||
|
if (missingTool == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var policy = config.Tooling?.DefaultInstallPolicy ?? InstallPolicy.Prompt;
|
||||||
|
if (policy == InstallPolicy.Never)
|
||||||
|
{
|
||||||
|
onOutput($"Auto-recovery for '{missingTool}' skipped due to InstallPolicy.Never.", false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput($"Detected missing tool '{missingTool}' during execution. {(policy == InstallPolicy.Auto ? "Auto-installing..." : "Prompting for installation...")}", false);
|
||||||
|
|
||||||
|
var installPlan = await _installer.GetInstallPlanAsync(missingTool, projectRoot, config, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!installPlan.Supported || installPlan.Commands.Count == 0)
|
||||||
|
{
|
||||||
|
onOutput($"No installer plan available for auto-recovery of '{missingTool}'.", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var approved = policy == InstallPolicy.Auto
|
||||||
|
? true
|
||||||
|
: await confirmInstallAsync(missingTool, installPlan).ConfigureAwait(false);
|
||||||
|
if (!approved)
|
||||||
|
break;
|
||||||
|
|
||||||
|
bool installSuccess = true;
|
||||||
|
foreach (var installCommand in installPlan.Commands)
|
||||||
|
{
|
||||||
|
var installResult = await _installer.RunInstallAsync(installCommand, projectRoot, onOutput, envOverrides, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!installResult.Success)
|
||||||
|
{
|
||||||
|
installSuccess = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installSuccess)
|
||||||
|
{
|
||||||
|
onOutput($"Auto-recovery install failed for '{missingTool}'.", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOutput($"Auto-recovery successful for '{missingTool}'. Retrying step '{step.Label}'...", false);
|
||||||
|
retryCount++;
|
||||||
|
}
|
||||||
|
|
||||||
results.Add(new WorkflowStepResult(
|
results.Add(new WorkflowStepResult(
|
||||||
workflow.Id,
|
workflow.Id,
|
||||||
@ -273,4 +335,32 @@ public sealed class WorkflowExecutor(
|
|||||||
Message: "Workflow completed successfully.",
|
Message: "Workflow completed successfully.",
|
||||||
Steps: results);
|
Steps: results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? DetectMissingToolFromOutput(List<string> output)
|
||||||
|
{
|
||||||
|
foreach (var line in output)
|
||||||
|
{
|
||||||
|
// Python: No module named 'PyInstaller'
|
||||||
|
if (line.Contains("No module named", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(line, "No module named '?(\\w+)'?", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success) return match.Groups[1].Value.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows: 'cargo' is not recognized as an internal or external command
|
||||||
|
if (line.Contains("is not recognized as an internal or external command", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(line, "'([^']+)' is not recognized", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success) return match.Groups[1].Value.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux: cargo: command not found
|
||||||
|
if (line.Contains("command not found", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(line, "([^\\s:]+):\\s*command not found", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success) return match.Groups[1].Value.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -150,7 +150,7 @@ public sealed class BridgeStdioServer
|
|||||||
if (!Directory.Exists(absoluteCandidate))
|
if (!Directory.Exists(absoluteCandidate))
|
||||||
throw new BridgeValidationException($"Candidate path does not exist: {absoluteCandidate}");
|
throw new BridgeValidationException($"Candidate path does not exist: {absoluteCandidate}");
|
||||||
|
|
||||||
if (initConfig && !File.Exists(Path.Combine(absoluteCandidate, "devtool.json")))
|
if (initConfig && ConfigLoader.FindConfigPath(absoluteCandidate) is null)
|
||||||
{
|
{
|
||||||
var scan = ConfigBootstrapper.Scan(absoluteCandidate);
|
var scan = ConfigBootstrapper.Scan(absoluteCandidate);
|
||||||
var generated = ConfigBootstrapper.BuildDefaultConfig(scan);
|
var generated = ConfigBootstrapper.BuildDefaultConfig(scan);
|
||||||
@ -632,7 +632,7 @@ public sealed class BridgeStdioServer
|
|||||||
private LoadedProjectConfig LoadProject(JsonElement @params)
|
private LoadedProjectConfig LoadProject(JsonElement @params)
|
||||||
{
|
{
|
||||||
var startDir = GetString(@params, "projectRoot") ?? _startupProjectRoot ?? Directory.GetCurrentDirectory();
|
var startDir = GetString(@params, "projectRoot") ?? _startupProjectRoot ?? Directory.GetCurrentDirectory();
|
||||||
return ConfigLoader.FindAndLoad(startDir) ?? throw new BridgeValidationException("No devtool.json found for project.");
|
return ConfigLoader.FindAndLoad(startDir) ?? throw new BridgeValidationException("No project config found for project.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveProjectRootForProjectScopedMethod(JsonElement @params)
|
private string ResolveProjectRootForProjectScopedMethod(JsonElement @params)
|
||||||
@ -684,12 +684,12 @@ public sealed class BridgeStdioServer
|
|||||||
{
|
{
|
||||||
var path = ConfigLoader.FindConfigPath(projectRoot);
|
var path = ConfigLoader.FindConfigPath(projectRoot);
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
return new LegacyMigrationApplyResult(false, "Could not find devtool.json for saving.");
|
return new LegacyMigrationApplyResult(false, "Could not find project config for saving.");
|
||||||
|
|
||||||
var backup = path + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}";
|
var backup = path + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}";
|
||||||
File.Copy(path, backup, overwrite: false);
|
File.Copy(path, backup, overwrite: false);
|
||||||
File.WriteAllText(path, ConfigBootstrapper.ToJson(config));
|
File.WriteAllText(path, ConfigBootstrapper.ToJson(config));
|
||||||
return new LegacyMigrationApplyResult(true, "Saved updated devtool.json.", BackupPath: backup, ConfigPath: path);
|
return new LegacyMigrationApplyResult(true, "Saved updated project config.", BackupPath: backup, ConfigPath: path);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -51,7 +51,7 @@ public sealed class App
|
|||||||
_startupWorkflowId = startupWorkflowId;
|
_startupWorkflowId = startupWorkflowId;
|
||||||
_activeEnvProfile = config.EnvProfiles?.Active;
|
_activeEnvProfile = config.EnvProfiles?.Active;
|
||||||
var normalized = WorkflowModelBuilder.Normalize(config, ResolveLegacyMode(), _requirementResolver);
|
var normalized = WorkflowModelBuilder.Normalize(config, ResolveLegacyMode(), _requirementResolver);
|
||||||
_workflows = normalized.Workflows.ToList();
|
_workflows = normalized.Workflows.Where(IsWorkflowApplicable).ToList();
|
||||||
_warnings = [];
|
_warnings = [];
|
||||||
_debugRunner = new DebugProfileRunner(new ToolProbeService(), new PrereqInstallerService());
|
_debugRunner = new DebugProfileRunner(new ToolProbeService(), new PrereqInstallerService());
|
||||||
_diagnostics = new DiagnosticsBundleService();
|
_diagnostics = new DiagnosticsBundleService();
|
||||||
@ -60,6 +60,30 @@ public sealed class App
|
|||||||
_warnings.AddRange(normalized.Warnings);
|
_warnings.AddRange(normalized.Warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsWorkflowApplicable(WorkflowDefinition workflow)
|
||||||
|
{
|
||||||
|
if (workflow.RequireFiles is null || workflow.RequireFiles.Count == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
foreach (var req in workflow.RequireFiles)
|
||||||
|
{
|
||||||
|
var path = Path.GetFullPath(Path.Combine(_projectRoot, req));
|
||||||
|
if (!File.Exists(path) && !Directory.Exists(path))
|
||||||
|
{
|
||||||
|
if (req.Contains('*'))
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
if (string.IsNullOrWhiteSpace(dir)) dir = _projectRoot;
|
||||||
|
var file = Path.GetFileName(req);
|
||||||
|
if (Directory.Exists(dir) && Directory.EnumerateFileSystemEntries(dir, file).Any())
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static LegacyMode ResolveLegacyMode()
|
private static LegacyMode ResolveLegacyMode()
|
||||||
{
|
{
|
||||||
var raw = Environment.GetEnvironmentVariable("SDT_LEGACY_MODE");
|
var raw = Environment.GetEnvironmentVariable("SDT_LEGACY_MODE");
|
||||||
@ -364,7 +388,7 @@ public sealed class App
|
|||||||
|
|
||||||
if (_config.Targets.Count > 0)
|
if (_config.Targets.Count > 0)
|
||||||
systemItems.Insert(0, new MenuItem(
|
systemItems.Insert(0, new MenuItem(
|
||||||
$"[{Theme.Green}]⇪ Migrate legacy targets → workflows[/] [{Theme.GreenDim}]writes devtool.json + backup[/]",
|
$"[{Theme.Green}]⇪ Migrate legacy targets → workflows[/] [{Theme.GreenDim}]writes project config + backup[/]",
|
||||||
"__migrate_legacy__"));
|
"__migrate_legacy__"));
|
||||||
|
|
||||||
systemItems.Add(new MenuItem($"[{Theme.GreenDim}]✗ Quit[/]", "__quit__"));
|
systemItems.Add(new MenuItem($"[{Theme.GreenDim}]✗ Quit[/]", "__quit__"));
|
||||||
|
|||||||
@ -26,6 +26,37 @@ public sealed class SetupWizardScreen
|
|||||||
Emit(new RunEvent("setup", RunEventType.WorkflowStarted, "Setup wizard started."));
|
Emit(new RunEvent("setup", RunEventType.WorkflowStarted, "Setup wizard started."));
|
||||||
Emit(new RunEvent("setup", RunEventType.WorkflowPlanned, "Setup lifecycle initialized."));
|
Emit(new RunEvent("setup", RunEventType.WorkflowPlanned, "Setup lifecycle initialized."));
|
||||||
|
|
||||||
|
if (!nonInteractive)
|
||||||
|
{
|
||||||
|
bool isNewProject = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isNewProject = !Directory.EnumerateFileSystemEntries(projectRoot)
|
||||||
|
.Any(x => !x.EndsWith(".sdt") && !x.EndsWith(".git") && !x.EndsWith("sdtconfig"));
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
|
||||||
|
var runWizard = false;
|
||||||
|
if (isNewProject)
|
||||||
|
{
|
||||||
|
runWizard = AnsiConsole.Confirm($"\n[{Theme.Amber}]This appears to be an empty directory. Would you like to scaffold a new project?[/]", defaultValue: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
runWizard = AnsiConsole.Confirm($"\n[{Theme.Amber}]Would you like to scaffold a new project in this directory?[/]", defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runWizard)
|
||||||
|
{
|
||||||
|
await RunScaffoldingWizardAsync(projectRoot);
|
||||||
|
// Clear and re-render header because scaffolding can produce a lot of output
|
||||||
|
AnsiConsole.Clear();
|
||||||
|
AnsiConsole.Write(new FigletText("SDT").Color(Theme.GreenColor));
|
||||||
|
AnsiConsole.Write(new Rule($"[bold {Theme.GreenBold}]Setup Wizard[/]").RuleStyle(Theme.DimStyle));
|
||||||
|
AnsiConsole.MarkupLine(Theme.Faint($"root: {projectRoot}") + "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnsiConsole.MarkupLine(Theme.G("Step 1/4: Running config doctor..."));
|
AnsiConsole.MarkupLine(Theme.G("Step 1/4: Running config doctor..."));
|
||||||
Emit(new RunEvent("setup", RunEventType.WorkflowStepStarted, "Running config doctor.", StepId: "setup:doctor"));
|
Emit(new RunEvent("setup", RunEventType.WorkflowStepStarted, "Running config doctor.", StepId: "setup:doctor"));
|
||||||
var doctor = new ConfigDoctorService(new ToolProbeService(), new RequirementResolver());
|
var doctor = new ConfigDoctorService(new ToolProbeService(), new RequirementResolver());
|
||||||
@ -120,7 +151,7 @@ public sealed class SetupWizardScreen
|
|||||||
var applyConfig = nonInteractive
|
var applyConfig = nonInteractive
|
||||||
? true
|
? true
|
||||||
: AnsiConsole.Confirm(
|
: AnsiConsole.Confirm(
|
||||||
$"[{Theme.Amber}]Apply {update.Changes.Count} recommended config update(s) to devtool.json?[/]",
|
$"[{Theme.Amber}]Apply {update.Changes.Count} recommended update(s) to your project config?[/]",
|
||||||
defaultValue: true);
|
defaultValue: true);
|
||||||
if (applyConfig)
|
if (applyConfig)
|
||||||
{
|
{
|
||||||
@ -151,6 +182,38 @@ public sealed class SetupWizardScreen
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RunScaffoldingWizardAsync(string projectRoot)
|
||||||
|
{
|
||||||
|
AnsiConsole.Clear();
|
||||||
|
AnsiConsole.Write(Theme.SectionRule("NEW PROJECT WIZARD"));
|
||||||
|
|
||||||
|
var projectName = AnsiConsole.Ask<string>($"[{Theme.Green}]Project Name:[/]", new DirectoryInfo(projectRoot).Name);
|
||||||
|
|
||||||
|
var backend = AnsiConsole.Prompt(
|
||||||
|
new SelectionPrompt<string>()
|
||||||
|
.Title($"[{Theme.Green}]Select Backend:[/]")
|
||||||
|
.AddChoices("None", "C# (.NET)", "Python"));
|
||||||
|
|
||||||
|
var frontend = AnsiConsole.Prompt(
|
||||||
|
new SelectionPrompt<string>()
|
||||||
|
.Title($"[{Theme.Green}]Select Frontend:[/]")
|
||||||
|
.AddChoices("None", "Web UI", "Tauri", "PyQt", "Avalonia"));
|
||||||
|
|
||||||
|
var options = new ScaffoldingOptions(projectName, backend, frontend);
|
||||||
|
|
||||||
|
var scaffolder = new ProjectScaffolder();
|
||||||
|
await scaffolder.ScaffoldAsync(projectRoot, options, (line, isErr) =>
|
||||||
|
{
|
||||||
|
var escaped = Markup.Escape(line);
|
||||||
|
AnsiConsole.MarkupLine(isErr
|
||||||
|
? $"[{Theme.Amber}]{escaped}[/]"
|
||||||
|
: $"[{Theme.Faint}]{escaped}[/]");
|
||||||
|
});
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine(Theme.Ok("Scaffolding finished. Proceeding to config doctor..."));
|
||||||
|
Thread.Sleep(1500);
|
||||||
|
}
|
||||||
|
|
||||||
private static Task<bool> ConfirmInstallAsync(string tool, InstallPlan plan)
|
private static Task<bool> ConfirmInstallAsync(string tool, InstallPlan plan)
|
||||||
{
|
{
|
||||||
if (RuntimePolicy.IsNonInteractive())
|
if (RuntimePolicy.IsNonInteractive())
|
||||||
@ -176,12 +239,12 @@ public sealed class SetupWizardScreen
|
|||||||
{
|
{
|
||||||
var path = ConfigLoader.FindConfigPath(projectRoot);
|
var path = ConfigLoader.FindConfigPath(projectRoot);
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
return new LegacyMigrationApplyResult(false, "Could not find devtool.json for saving.");
|
return new LegacyMigrationApplyResult(false, "Could not find project config for saving.");
|
||||||
|
|
||||||
var backup = path + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}";
|
var backup = path + $".bak-{DateTimeOffset.Now:yyyyMMdd-HHmmss}";
|
||||||
File.Copy(path, backup, overwrite: false);
|
File.Copy(path, backup, overwrite: false);
|
||||||
File.WriteAllText(path, ConfigBootstrapper.ToJson(config));
|
File.WriteAllText(path, ConfigBootstrapper.ToJson(config));
|
||||||
return new LegacyMigrationApplyResult(true, "Saved updated devtool.json.", BackupPath: backup, ConfigPath: path);
|
return new LegacyMigrationApplyResult(true, "Saved updated project config.", BackupPath: backup, ConfigPath: path);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -23,7 +23,7 @@ public sealed class ToolchainScreen(DevToolConfig config, string projectRoot)
|
|||||||
var tc = _config.Toolchains;
|
var tc = _config.Toolchains;
|
||||||
if (tc is null)
|
if (tc is null)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine(Theme.Warn("No toolchains configured in devtool.json."));
|
AnsiConsole.MarkupLine(Theme.Warn("No toolchains configured in project config."));
|
||||||
AnsiConsole.MarkupLine(Theme.Faint("Add a \"toolchains\" section with \"python\" and/or \"node\" entries."));
|
AnsiConsole.MarkupLine(Theme.Faint("Add a \"toolchains\" section with \"python\" and/or \"node\" entries."));
|
||||||
AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back..."));
|
AnsiConsole.MarkupLine("\n" + Theme.Faint("Press any key to go back..."));
|
||||||
Console.ReadKey(intercept: true);
|
Console.ReadKey(intercept: true);
|
||||||
|
|||||||
@ -50,9 +50,9 @@ public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceR
|
|||||||
foreach (var proj in projects)
|
foreach (var proj in projects)
|
||||||
{
|
{
|
||||||
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
|
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
|
||||||
var devtoolPath = Path.Combine(absPath, "devtool.json");
|
var configPath = ConfigLoader.FindConfigPath(absPath);
|
||||||
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
|
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
|
||||||
var exists = File.Exists(devtoolPath);
|
var exists = configPath is not null;
|
||||||
|
|
||||||
var label = isCurrent
|
var label = isCurrent
|
||||||
? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/] [{Theme.GreenDim}](current)[/]"
|
? $"[bold {Theme.GreenBold}]► {Markup.Escape(proj.Name)}[/] [{Theme.GreenDim}](current)[/]"
|
||||||
@ -61,7 +61,7 @@ public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceR
|
|||||||
label += $" [{Theme.Amber}](disabled)[/]";
|
label += $" [{Theme.Amber}](disabled)[/]";
|
||||||
|
|
||||||
var desc = !exists
|
var desc = !exists
|
||||||
? $" [{Theme.Red}]devtool.json not found[/]"
|
? $" [{Theme.Red}]project config not found[/]"
|
||||||
: string.IsNullOrWhiteSpace(proj.Description)
|
: string.IsNullOrWhiteSpace(proj.Description)
|
||||||
? $" [{Theme.GreenDim}]{Markup.Escape(absPath)}[/]"
|
? $" [{Theme.GreenDim}]{Markup.Escape(absPath)}[/]"
|
||||||
: $" [{Theme.GreenDim}]{Markup.Escape(proj.Description)}[/]";
|
: $" [{Theme.GreenDim}]{Markup.Escape(proj.Description)}[/]";
|
||||||
@ -160,7 +160,7 @@ public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceR
|
|||||||
{
|
{
|
||||||
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
|
var absPath = WorkspaceLoader.ResolveProjectRoot(_workspaceRoot, proj);
|
||||||
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
|
var isCurrent = string.Equals(absPath, _currentProjectRoot, StringComparison.OrdinalIgnoreCase);
|
||||||
var hasConfig = File.Exists(Path.Combine(absPath, "devtool.json"));
|
var hasConfig = ConfigLoader.FindConfigPath(absPath) is not null;
|
||||||
var status = proj.Disabled
|
var status = proj.Disabled
|
||||||
? Theme.Warn("disabled")
|
? Theme.Warn("disabled")
|
||||||
: hasConfig ? Theme.Ok("ready") : Theme.Fail("no config");
|
: hasConfig ? Theme.Ok("ready") : Theme.Fail("no config");
|
||||||
@ -223,15 +223,17 @@ public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceR
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath = Path.Combine(absolutePath, "devtool.json");
|
var configPath = ConfigLoader.FindConfigPath(absolutePath);
|
||||||
if (!File.Exists(configPath))
|
if (configPath is null)
|
||||||
{
|
{
|
||||||
var create = AnsiConsole.Confirm(
|
var create = AnsiConsole.Confirm(
|
||||||
$"[{Theme.Amber}]No devtool.json found. Create a minimal template?[/]",
|
$"[{Theme.Amber}]No project config found. Create a minimal template?[/]",
|
||||||
defaultValue: true);
|
defaultValue: true);
|
||||||
if (!create)
|
if (!create)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var newConfigName = $"sdtconfig-{new DirectoryInfo(absolutePath).Name}.json";
|
||||||
|
configPath = Path.Combine(absolutePath, newConfigName);
|
||||||
File.WriteAllText(configPath, "{\n \"name\": \"SDT Project\",\n \"version\": \"0.1.0\",\n \"workflows\": []\n}\n");
|
File.WriteAllText(configPath, "{\n \"name\": \"SDT Project\",\n \"version\": \"0.1.0\",\n \"workflows\": []\n}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,14 +272,14 @@ public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceR
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath = Path.Combine(absolutePath, "devtool.json");
|
var configPath = ConfigLoader.FindConfigPath(absolutePath);
|
||||||
if (initializeConfig && !File.Exists(configPath))
|
if (initializeConfig && configPath is null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var scan = ConfigBootstrapper.Scan(absolutePath);
|
var scan = ConfigBootstrapper.Scan(absolutePath);
|
||||||
var generated = ConfigBootstrapper.BuildDefaultConfig(scan);
|
var generated = ConfigBootstrapper.BuildDefaultConfig(scan);
|
||||||
ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false);
|
configPath = ConfigBootstrapper.WriteDefaultConfig(scan.ProjectRoot, generated, overwrite: false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -287,9 +289,9 @@ public sealed class WorkspaceScreen(WorkspaceConfig workspace, string workspaceR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!File.Exists(configPath))
|
if (configPath is null)
|
||||||
{
|
{
|
||||||
AnsiConsole.MarkupLine(Theme.Warn("Candidate has no devtool.json yet. Use Add + initialize."));
|
AnsiConsole.MarkupLine(Theme.Warn("Candidate has no project config yet. Use Add + initialize."));
|
||||||
Thread.Sleep(900);
|
Thread.Sleep(900);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,6 +160,24 @@ public sealed class ConfigBootstrapperTests
|
|||||||
Assert.Equal("AppB", scan.DotnetWorkingDir, ignoreCase: true);
|
Assert.Equal("AppB", scan.DotnetWorkingDir, ignoreCase: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_PrefersTauriPackageRoot_ForNodeWorkingDir()
|
||||||
|
{
|
||||||
|
var root = CreateTempDir();
|
||||||
|
var workspacePkg = Path.Combine(root, "journal");
|
||||||
|
var appPkg = Path.Combine(workspacePkg, "Journal.App");
|
||||||
|
Directory.CreateDirectory(appPkg);
|
||||||
|
Directory.CreateDirectory(Path.Combine(appPkg, "src-tauri"));
|
||||||
|
|
||||||
|
File.WriteAllText(Path.Combine(workspacePkg, "package.json"), """{ "name": "workspace", "scripts": { "build": "echo build" } }""");
|
||||||
|
File.WriteAllText(Path.Combine(appPkg, "package.json"), """{ "name": "journal-app", "scripts": { "build": "echo build", "tauri": "echo tauri" } }""");
|
||||||
|
File.WriteAllText(Path.Combine(appPkg, "src-tauri", "tauri.conf.json"), "{}");
|
||||||
|
|
||||||
|
var scan = ConfigBootstrapper.Scan(root);
|
||||||
|
|
||||||
|
Assert.Equal(Path.Combine("journal", "Journal.App"), scan.NodeWorkingDir, ignoreCase: true);
|
||||||
|
}
|
||||||
|
|
||||||
private static string CreateTempDir()
|
private static string CreateTempDir()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(Path.GetTempPath(), "sdt-bootstrap-" + Guid.NewGuid().ToString("N"));
|
var path = Path.Combine(Path.GetTempPath(), "sdt-bootstrap-" + Guid.NewGuid().ToString("N"));
|
||||||
|
|||||||
@ -180,7 +180,7 @@ public sealed class HeadlessExecutionTests
|
|||||||
{
|
{
|
||||||
var root = Path.Combine(Path.GetTempPath(), "sdt-headless-" + Guid.NewGuid().ToString("N"));
|
var root = Path.Combine(Path.GetTempPath(), "sdt-headless-" + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(root);
|
Directory.CreateDirectory(root);
|
||||||
File.WriteAllText(Path.Combine(root, "devtool.json"), devtoolJson);
|
File.WriteAllText(Path.Combine(root, "sdtconfig-headless.json"), devtoolJson);
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public sealed class ScriptCommonTests
|
|||||||
var nested = Path.Combine(root, "src", "app");
|
var nested = Path.Combine(root, "src", "app");
|
||||||
Directory.CreateDirectory(nested);
|
Directory.CreateDirectory(nested);
|
||||||
await File.WriteAllTextAsync(Path.Combine(root, "sample.sln"), "");
|
await File.WriteAllTextAsync(Path.Combine(root, "sample.sln"), "");
|
||||||
await File.WriteAllTextAsync(Path.Combine(root, "devtool.json"), """
|
await File.WriteAllTextAsync(Path.Combine(root, "sdtconfig-demo.json"), """
|
||||||
{
|
{
|
||||||
"name": "demo",
|
"name": "demo",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@ -35,7 +35,7 @@ public sealed class ScriptCommonTests
|
|||||||
var nested = Path.Combine(root, "child", "leaf");
|
var nested = Path.Combine(root, "child", "leaf");
|
||||||
Directory.CreateDirectory(nested);
|
Directory.CreateDirectory(nested);
|
||||||
Directory.CreateDirectory(Path.Combine(root, ".git"));
|
Directory.CreateDirectory(Path.Combine(root, ".git"));
|
||||||
await File.WriteAllTextAsync(Path.Combine(root, "devtool.json"), """
|
await File.WriteAllTextAsync(Path.Combine(root, "sdtconfig-demo.json"), """
|
||||||
{
|
{
|
||||||
"name": "demo",
|
"name": "demo",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public sealed class WorkspaceDefaultsTests
|
|||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(projectRoot, "devtool.json"), """
|
File.WriteAllText(Path.Combine(projectRoot, "sdtconfig-project-a.json"), """
|
||||||
{
|
{
|
||||||
"name": "Project A",
|
"name": "Project A",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -97,7 +97,7 @@ public sealed class WorkspaceDefaultsTests
|
|||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(projectRoot, "devtool.json"), """
|
File.WriteAllText(Path.Combine(projectRoot, "sdtconfig-project-b.json"), """
|
||||||
{
|
{
|
||||||
"name": "Project B",
|
"name": "Project B",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public sealed class WorkspaceInventoryServiceTests
|
|||||||
Directory.CreateDirectory(b);
|
Directory.CreateDirectory(b);
|
||||||
Directory.CreateDirectory(a);
|
Directory.CreateDirectory(a);
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(c, "devtool.json"), """{"name":"c","version":"0.1.0","workflows":[]}""");
|
File.WriteAllText(Path.Combine(c, "sdtconfig-c.json"), """{"name":"c","version":"0.1.0","workflows":[]}""");
|
||||||
File.WriteAllText(Path.Combine(b, "B.slnx"), "");
|
File.WriteAllText(Path.Combine(b, "B.slnx"), "");
|
||||||
File.WriteAllText(Path.Combine(a, "A.csproj"), "<Project />");
|
File.WriteAllText(Path.Combine(a, "A.csproj"), "<Project />");
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ public sealed class WorkspaceInventoryServiceTests
|
|||||||
var excluded = Path.Combine(root, "node_modules", "Pkg");
|
var excluded = Path.Combine(root, "node_modules", "Pkg");
|
||||||
Directory.CreateDirectory(excluded);
|
Directory.CreateDirectory(excluded);
|
||||||
File.WriteAllText(Path.Combine(excluded, "Pkg.csproj"), "<Project />");
|
File.WriteAllText(Path.Combine(excluded, "Pkg.csproj"), "<Project />");
|
||||||
File.WriteAllText(Path.Combine(root, "devtool.json"), """{"name":"root","version":"0.1.0","workflows":[]}""");
|
File.WriteAllText(Path.Combine(root, "sdtconfig-root.json"), """{"name":"root","version":"0.1.0","workflows":[]}""");
|
||||||
|
|
||||||
var service = new WorkspaceInventoryService();
|
var service = new WorkspaceInventoryService();
|
||||||
var result = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig());
|
var result = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig());
|
||||||
@ -82,7 +82,7 @@ public sealed class WorkspaceInventoryServiceTests
|
|||||||
Directory.CreateDirectory(b);
|
Directory.CreateDirectory(b);
|
||||||
File.WriteAllText(Path.Combine(a, "a.csproj"), "<Project />");
|
File.WriteAllText(Path.Combine(a, "a.csproj"), "<Project />");
|
||||||
File.WriteAllText(Path.Combine(b, "b.csproj"), "<Project />");
|
File.WriteAllText(Path.Combine(b, "b.csproj"), "<Project />");
|
||||||
File.WriteAllText(Path.Combine(root, "devtool.json"), """{"name":"root","version":"0.1.0","workflows":[]}""");
|
File.WriteAllText(Path.Combine(root, "sdtconfig-root.json"), """{"name":"root","version":"0.1.0","workflows":[]}""");
|
||||||
|
|
||||||
var service = new WorkspaceInventoryService();
|
var service = new WorkspaceInventoryService();
|
||||||
var first = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig());
|
var first = service.Scan(root, root, new WorkspaceInventorySettings(), new WorkspaceConfig());
|
||||||
|
|||||||
@ -10,7 +10,7 @@ public sealed class WorkspaceLoaderTests
|
|||||||
{
|
{
|
||||||
var root = Path.Combine(Path.GetTempPath(), "sdt-workspace-" + Guid.NewGuid().ToString("N"));
|
var root = Path.Combine(Path.GetTempPath(), "sdt-workspace-" + Guid.NewGuid().ToString("N"));
|
||||||
Directory.CreateDirectory(root);
|
Directory.CreateDirectory(root);
|
||||||
File.WriteAllText(Path.Combine(root, "devtool.json"), """
|
File.WriteAllText(Path.Combine(root, "sdtconfig-legacy.json"), """
|
||||||
{
|
{
|
||||||
"name": "legacy",
|
"name": "legacy",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@ -72,7 +72,7 @@ public sealed class WorkspaceLoaderTests
|
|||||||
var markerOnly = Path.Combine(workspaceRoot, "marker");
|
var markerOnly = Path.Combine(workspaceRoot, "marker");
|
||||||
Directory.CreateDirectory(current);
|
Directory.CreateDirectory(current);
|
||||||
Directory.CreateDirectory(markerOnly);
|
Directory.CreateDirectory(markerOnly);
|
||||||
File.WriteAllText(Path.Combine(current, "devtool.json"), """{"name":"current","version":"0.1.0","workflows":[]}""");
|
File.WriteAllText(Path.Combine(current, "sdtconfig-current.json"), """{"name":"current","version":"0.1.0","workflows":[]}""");
|
||||||
File.WriteAllText(Path.Combine(markerOnly, "marker.csproj"), "<Project />");
|
File.WriteAllText(Path.Combine(markerOnly, "marker.csproj"), "<Project />");
|
||||||
|
|
||||||
var loaded = WorkspaceLoader.FindAndLoad(current);
|
var loaded = WorkspaceLoader.FindAndLoad(current);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user