mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 05:30:46 +08:00
Release 0.4.12: update/reinstall mode and post-install venv repair.
Interactive Setup asks update vs reinstall; elevated install relinks .venv so tray start works under Program Files. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
372138d9a1
commit
ccd75a9c09
7 changed files with 246 additions and 28 deletions
|
|
@ -56,15 +56,17 @@ Optional local zip only: `build_release.ps1 -CreateZip` (not for release upload)
|
|||
|
||||
1. Run `NetX-Setup-x.y.z.exe` (admin).
|
||||
2. **First install:** Database page — choose built-in (offline) or external PostgreSQL (host/port/user/password/db; connection must succeed to continue).
|
||||
3. **Already installed (upgrade):** if `%ProgramData%\NetX\.env` already has `NETX_DB_MODE`, Setup **skips** the Database page, stops running NetX, replaces program files, and **keeps** data / DB settings. Use Start Menu → **Reconfigure database** only when you need to change DB mode.
|
||||
3. **Already installed:** if `%ProgramData%\NetX\.env` already has `NETX_DB_MODE`, Setup shows an **Install mode** page:
|
||||
- **Update** (default) — skip Database page; stop NetX; replace program files; **keep** ProgramData / DB settings.
|
||||
- **Reinstall** — same Database wizard as first install (`ApplyDatabaseConfig`); does **not** delete ProgramData (use Uninstall → delete data for a wipe).
|
||||
4. Files → `%ProgramFiles%\NetX\` (program root).
|
||||
5. Data → `%ProgramData%\NetX\` (`.env`, `pgdata`, spool, secrets).
|
||||
6. Start menu: **Start NetX** / **Stop NetX** / **Open NetX UI** / **Reconfigure database**.
|
||||
|
||||
Silent first install (bundled default): `/SILENT /DbMode=bundled`
|
||||
Silent external: `/SILENT /DbMode=external /DbHost=... /DbPort=5432 /DbUser=... /DbPassword=... /DbName=...`
|
||||
Silent upgrade over existing data: `/VERYSILENT /NORESTART` (auto-detects `.env`; or force `/SkipDbPage=1`)
|
||||
Force DB wizard on an existing install: `/ForceDbPage=1`
|
||||
Silent update over existing data: `/VERYSILENT /NORESTART /InstallMode=update` (also `/SkipDbPage=1`)
|
||||
Silent reinstall (DB wizard via params): `/VERYSILENT /InstallMode=reinstall /DbMode=bundled` (or external `/DbHost`…) — also `/ForceDbPage=1`
|
||||
Optional credential key (reuse encrypted NE passwords from another install): `/CredentialSecretKey=...` — omit to auto-generate.
|
||||
|
||||
## Database modes
|
||||
|
|
@ -79,7 +81,7 @@ Existing Windows deploys that only set `NETX_DATABASE_URL` keep working: never s
|
|||
|
||||
## Manual update
|
||||
|
||||
Preferred: run a newer `NetX-Setup-*.exe` over the same machine (upgrade mode keeps ProgramData).
|
||||
Preferred: run a newer `NetX-Setup-*.exe` and choose **Update** (or silent `/InstallMode=update`) so ProgramData is kept. Setup (elevated) always relinks `.venv` → `python/runtime` after file copy so tray start works without write access under Program Files.
|
||||
|
||||
Legacy/dev zip (only if you built with `-CreateZip`):
|
||||
|
||||
|
|
|
|||
|
|
@ -404,8 +404,23 @@ function Repair-NetxShippedVenv {
|
|||
"version = $ver",
|
||||
"executable = $rtPyAbs"
|
||||
)
|
||||
Set-Content -LiteralPath $cfgPath -Value ($lines -join "`r`n") -Encoding ascii
|
||||
return $true
|
||||
$desired = ($lines -join "`r`n")
|
||||
# Skip write when already correct — Users cannot modify Program Files after Setup.
|
||||
try {
|
||||
$cur = ([IO.File]::ReadAllText($cfgPath) -replace "`r`n", "`n" -replace "`r", "`n").Trim()
|
||||
$want = ($desired -replace "`r`n", "`n" -replace "`r", "`n").Trim()
|
||||
if ($cur -eq $want) {
|
||||
return $true
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
Set-Content -LiteralPath $cfgPath -Value $desired -Encoding ascii -ErrorAction Stop
|
||||
return $true
|
||||
} catch {
|
||||
# Non-elevated tray/start: leave cfg as-is; caller may still run if paths happen to work.
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-NetxVenvRunnable {
|
||||
|
|
@ -446,7 +461,11 @@ function Ensure-NetxVenv {
|
|||
$rtPy = Join-Path (Get-NetxBundledPythonRoot -ProgramRoot $ProgramRoot) "python.exe"
|
||||
|
||||
if (Test-Path -LiteralPath $rtPy) {
|
||||
$null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot
|
||||
try {
|
||||
$null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot
|
||||
} catch {
|
||||
# Access denied under Program Files when not elevated — ignore if venv already runs.
|
||||
}
|
||||
}
|
||||
|
||||
if ((Test-Path -LiteralPath $venvPy) -and -not $ForcePip) {
|
||||
|
|
@ -455,11 +474,19 @@ function Ensure-NetxVenv {
|
|||
}
|
||||
Write-Host "[WARN] Shipped .venv exists but Python cannot start (broken pyvenv.cfg or missing runtime)." -ForegroundColor Yellow
|
||||
if (Test-Path -LiteralPath $rtPy) {
|
||||
$null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot
|
||||
if (Test-NetxVenvRunnable -VenvPython $venvPy) {
|
||||
$repaired = $false
|
||||
try {
|
||||
$repaired = [bool](Repair-NetxShippedVenv -ProgramRoot $ProgramRoot)
|
||||
} catch {
|
||||
$repaired = $false
|
||||
}
|
||||
if ($repaired -and (Test-NetxVenvRunnable -VenvPython $venvPy)) {
|
||||
Write-Host "==> Repaired .venv to use bundled python/runtime" -ForegroundColor Green
|
||||
return $venvPy
|
||||
}
|
||||
if (-not $repaired) {
|
||||
throw "venv_needs_admin_repair: pyvenv.cfg still points at the build PC. Re-run Setup (Update) as Administrator, or Start Menu → Reconfigure database once elevated."
|
||||
}
|
||||
}
|
||||
if (-not $ForcePip) {
|
||||
throw "venv_not_runnable: reinstall NetX Setup (ships python/runtime + .venv) or run repair as admin"
|
||||
|
|
|
|||
|
|
@ -404,15 +404,22 @@ try {
|
|||
|
||||
$env:NETX_UPDATE_ELEVATED = "1"
|
||||
if ($isSetup) {
|
||||
# Silent Setup over an existing configured install skips the DB wizard
|
||||
# (see installer/netx.iss GSkipDbPage) and preserves ProgramData.
|
||||
# Silent Setup update mode: skip DB wizard, keep ProgramData
|
||||
# (see installer/netx.iss /InstallMode=update).
|
||||
Write-Host "==> Applying update via silent Setup.exe"
|
||||
$setupArgs = "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /DIR=`"$prog`" /SkipDbPage=1"
|
||||
$setupArgs = "/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /DIR=`"$prog`" /InstallMode=update"
|
||||
$sp = Start-Process -FilePath $pkgPath -ArgumentList $setupArgs -Wait -PassThru
|
||||
if ($null -eq $sp.ExitCode -or $sp.ExitCode -ne 0) {
|
||||
$code = if ($null -eq $sp.ExitCode) { "null" } else { $sp.ExitCode }
|
||||
throw "setup_update_failed: exit $code"
|
||||
}
|
||||
# Setup post-install also repairs; belt-and-suspenders when running elevated apply.
|
||||
$repairPs1 = Join-Path $prog "packaging\repair_venv.ps1"
|
||||
if (Test-Path -LiteralPath $repairPs1) {
|
||||
Write-Host "==> Relinking .venv to bundled python/runtime"
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File $repairPs1 `
|
||||
-ProgramRoot $prog -DataRoot $data
|
||||
}
|
||||
Write-Host "==> Restarting NetX after Setup"
|
||||
& (Join-Path $PSScriptRoot "start_netx_app.ps1") `
|
||||
-ProgramRoot $prog -DataRoot $data -SkipBrowser
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#define MyAppName "NetX"
|
||||
#ifndef MyAppVersion
|
||||
#define MyAppVersion "0.4.11"
|
||||
#define MyAppVersion "0.4.12"
|
||||
#endif
|
||||
#define MyAppPublisher "NetX"
|
||||
#define MyAppURL "https://github.com/hansjone/netx"
|
||||
|
|
@ -78,6 +78,12 @@ english.DbBundledMissing=Built-in PostgreSQL files are missing from the install
|
|||
english.DbEnvMissing=Database configuration did not write %ProgramData%\NetX\.env.
|
||||
english.ReconfigureDb=Reconfigure database
|
||||
english.UpgradeKeepDb=Existing NetX data found - database settings will be kept (no wizard).
|
||||
english.ModePageCaption=Install mode
|
||||
english.ModePageDescription=An existing NetX installation was found. Choose update or reinstall.
|
||||
english.ModeUpdate=Update existing installation (keep database settings)
|
||||
english.ModeReinstall=Reinstall and reconfigure database (same as first install)
|
||||
english.ModeUpdateHint=Replaces program files only. ProgramData\.env and database data are kept.
|
||||
english.ModeReinstallHint=Shows the Database page again. Does not delete ProgramData unless you uninstall and choose to.
|
||||
chinesesimplified.CreateDesktopIcon=创建桌面快捷方式
|
||||
chinesesimplified.SetupOptions=安装完成后:
|
||||
chinesesimplified.StartTrayNow=立即启动 NetX 托盘
|
||||
|
|
@ -107,6 +113,12 @@ chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件
|
|||
chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。
|
||||
chinesesimplified.ReconfigureDb=重新配置数据库
|
||||
chinesesimplified.UpgradeKeepDb=检测到已有 NetX 数据 - 将保留数据库配置(跳过向导)。
|
||||
chinesesimplified.ModePageCaption=安装模式
|
||||
chinesesimplified.ModePageDescription=检测到已有 NetX 安装。请选择更新或重装。
|
||||
chinesesimplified.ModeUpdate=更新现有安装(保留数据库配置)
|
||||
chinesesimplified.ModeReinstall=重装并重新配置数据库(与首次安装相同)
|
||||
chinesesimplified.ModeUpdateHint=仅替换程序文件,保留 ProgramData\.env 与数据库数据。
|
||||
chinesesimplified.ModeReinstallHint=再次显示数据库页。不会删除 ProgramData(除非卸载时选择删除)。
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
|
|
@ -203,6 +215,11 @@ var
|
|||
GCredKey: String;
|
||||
GSkipDbPage: Boolean;
|
||||
GIsUpgrade: Boolean;
|
||||
GInstallMode: String;
|
||||
ModePage: TWizardPage;
|
||||
RbModeUpdate: TNewRadioButton;
|
||||
RbModeReinstall: TNewRadioButton;
|
||||
LblModeHint: TNewStaticText;
|
||||
GDbConfigured: Boolean;
|
||||
GPgToolsReady: Boolean;
|
||||
GDbConnOk: Boolean;
|
||||
|
|
@ -265,6 +282,64 @@ begin
|
|||
Result := EnvHasDbMode();
|
||||
end;
|
||||
|
||||
procedure SyncSkipDbFromInstallMode();
|
||||
begin
|
||||
GSkipDbPage := (GInstallMode = 'update');
|
||||
end;
|
||||
|
||||
procedure ApplyInstallModeFromChoice();
|
||||
begin
|
||||
if (RbModeReinstall <> nil) and RbModeReinstall.Checked then
|
||||
GInstallMode := 'reinstall'
|
||||
else if GIsUpgrade then
|
||||
GInstallMode := 'update'
|
||||
else
|
||||
GInstallMode := 'fresh';
|
||||
SyncSkipDbFromInstallMode();
|
||||
end;
|
||||
|
||||
procedure ModeChoiceClick(Sender: TObject);
|
||||
begin
|
||||
ApplyInstallModeFromChoice();
|
||||
if LblModeHint = nil then
|
||||
Exit;
|
||||
if GInstallMode = 'reinstall' then
|
||||
LblModeHint.Caption := ExpandConstant('{cm:ModeReinstallHint}')
|
||||
else
|
||||
LblModeHint.Caption := ExpandConstant('{cm:ModeUpdateHint}');
|
||||
end;
|
||||
|
||||
function ResolveInstallModeFromParams(): Boolean;
|
||||
{ Returns True if a CLI param forced the mode. }
|
||||
var
|
||||
Mode: String;
|
||||
begin
|
||||
Result := False;
|
||||
Mode := LowerCase(Trim(ExpandConstant('{param:InstallMode|}')));
|
||||
if (Mode = 'update') or (Mode = 'upgrade') then
|
||||
begin
|
||||
GInstallMode := 'update';
|
||||
Result := True;
|
||||
end
|
||||
else if (Mode = 'reinstall') or (Mode = 'fresh') then
|
||||
begin
|
||||
GInstallMode := 'reinstall';
|
||||
Result := True;
|
||||
end
|
||||
else if ParamIsTruthy('SkipDbPage') then
|
||||
begin
|
||||
GInstallMode := 'update';
|
||||
Result := True;
|
||||
end
|
||||
else if ParamIsTruthy('ForceDbPage') then
|
||||
begin
|
||||
GInstallMode := 'reinstall';
|
||||
Result := True;
|
||||
end;
|
||||
if Result then
|
||||
SyncSkipDbFromInstallMode();
|
||||
end;
|
||||
|
||||
procedure StopNetxBeforeFileReplace();
|
||||
var
|
||||
ResultCode: Integer;
|
||||
|
|
@ -538,18 +613,62 @@ begin
|
|||
GDbConfigured := False;
|
||||
GPgToolsReady := False;
|
||||
GDbConnOk := False;
|
||||
{ Upgrade / reinstall over a configured install: skip DB wizard and keep
|
||||
ProgramData\.env / pgdata. Fresh install still shows the Database page.
|
||||
Force wizard: /ForceDbPage=1 Force skip: /SkipDbPage=1 }
|
||||
{ Existing install: interactive Mode page (Update vs Reinstall).
|
||||
Silent: /InstallMode=update|reinstall (or /SkipDbPage=1 /ForceDbPage=1). }
|
||||
GIsUpgrade := DetectExistingConfiguredInstall();
|
||||
if ParamIsTruthy('ForceDbPage') then
|
||||
GSkipDbPage := False
|
||||
else if ParamIsTruthy('SkipDbPage') then
|
||||
GSkipDbPage := True
|
||||
else
|
||||
GSkipDbPage := GIsUpgrade;
|
||||
ModePage := nil;
|
||||
RbModeUpdate := nil;
|
||||
RbModeReinstall := nil;
|
||||
LblModeHint := nil;
|
||||
if not ResolveInstallModeFromParams() then
|
||||
begin
|
||||
if GIsUpgrade then
|
||||
GInstallMode := 'update'
|
||||
else
|
||||
GInstallMode := 'fresh';
|
||||
SyncSkipDbFromInstallMode();
|
||||
end;
|
||||
|
||||
DbPage := CreateCustomPage(wpSelectDir,
|
||||
if GIsUpgrade and (not WizardSilent) then
|
||||
begin
|
||||
ModePage := CreateCustomPage(wpSelectDir,
|
||||
ExpandConstant('{cm:ModePageCaption}'),
|
||||
ExpandConstant('{cm:ModePageDescription}'));
|
||||
|
||||
RbModeUpdate := TNewRadioButton.Create(ModePage);
|
||||
RbModeUpdate.Parent := ModePage.Surface;
|
||||
RbModeUpdate.Caption := ExpandConstant('{cm:ModeUpdate}');
|
||||
RbModeUpdate.Checked := (GInstallMode <> 'reinstall');
|
||||
RbModeUpdate.Top := ScaleY(8);
|
||||
RbModeUpdate.Left := ScaleX(0);
|
||||
RbModeUpdate.Width := ModePage.SurfaceWidth;
|
||||
RbModeUpdate.OnClick := @ModeChoiceClick;
|
||||
|
||||
RbModeReinstall := TNewRadioButton.Create(ModePage);
|
||||
RbModeReinstall.Parent := ModePage.Surface;
|
||||
RbModeReinstall.Caption := ExpandConstant('{cm:ModeReinstall}');
|
||||
RbModeReinstall.Checked := (GInstallMode = 'reinstall');
|
||||
RbModeReinstall.Top := RbModeUpdate.Top + ScaleY(28);
|
||||
RbModeReinstall.Left := ScaleX(0);
|
||||
RbModeReinstall.Width := ModePage.SurfaceWidth;
|
||||
RbModeReinstall.OnClick := @ModeChoiceClick;
|
||||
|
||||
LblModeHint := TNewStaticText.Create(ModePage);
|
||||
LblModeHint.Parent := ModePage.Surface;
|
||||
LblModeHint.Top := RbModeReinstall.Top + ScaleY(36);
|
||||
LblModeHint.Left := ScaleX(0);
|
||||
LblModeHint.Width := ModePage.SurfaceWidth;
|
||||
LblModeHint.Height := ScaleY(60);
|
||||
LblModeHint.AutoSize := False;
|
||||
LblModeHint.WordWrap := True;
|
||||
ModeChoiceClick(nil);
|
||||
|
||||
DbPage := CreateCustomPage(ModePage.ID,
|
||||
ExpandConstant('{cm:DbPageCaption}'),
|
||||
ExpandConstant('{cm:DbPageDescription}'));
|
||||
end
|
||||
else
|
||||
DbPage := CreateCustomPage(wpSelectDir,
|
||||
ExpandConstant('{cm:DbPageCaption}'),
|
||||
ExpandConstant('{cm:DbPageDescription}'));
|
||||
|
||||
|
|
@ -687,6 +806,12 @@ end;
|
|||
function ShouldSkipPage(PageID: Integer): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
if (ModePage <> nil) and (PageID = ModePage.ID) then
|
||||
begin
|
||||
{ Only interactive existing installs show the mode page. }
|
||||
Result := (not GIsUpgrade) or WizardSilent;
|
||||
Exit;
|
||||
end;
|
||||
if (DbPage <> nil) and (PageID = DbPage.ID) then
|
||||
Result := GSkipDbPage;
|
||||
end;
|
||||
|
|
@ -696,6 +821,11 @@ var
|
|||
ErrMsg: String;
|
||||
begin
|
||||
Result := True;
|
||||
if (ModePage <> nil) and (CurPageID = ModePage.ID) then
|
||||
begin
|
||||
ApplyInstallModeFromChoice();
|
||||
Exit;
|
||||
end;
|
||||
{ Only the DB page runs validation. Other pages must always proceed
|
||||
(Tasks/Ready used to appear "dead" when CloseApplications hung). }
|
||||
if (DbPage = nil) or (CurPageID <> DbPage.ID) or GSkipDbPage then
|
||||
|
|
@ -736,6 +866,14 @@ var
|
|||
begin
|
||||
Result := '';
|
||||
NeedsRestart := False;
|
||||
|
||||
if (ModePage <> nil) and (not WizardSilent) then
|
||||
ApplyInstallModeFromChoice();
|
||||
|
||||
{ Stop tray / API / bundled PG so Program Files can be replaced safely. }
|
||||
if GSkipDbPage or GIsUpgrade or DirExists(ExpandConstant('{app}\packaging')) then
|
||||
StopNetxBeforeFileReplace();
|
||||
|
||||
if GSkipDbPage then
|
||||
Exit;
|
||||
|
||||
|
|
@ -897,12 +1035,31 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
procedure RepairNetxVenvAfterInstall();
|
||||
var
|
||||
ResultCode: Integer;
|
||||
RepairScript: String;
|
||||
Params: String;
|
||||
begin
|
||||
RepairScript := ExpandConstant('{app}\packaging\repair_venv.ps1');
|
||||
if not FileExists(RepairScript) then
|
||||
Exit;
|
||||
Params :=
|
||||
'-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + RepairScript + '"' +
|
||||
' -ProgramRoot "' + ExpandConstant('{app}') + '"' +
|
||||
' -DataRoot "' + ExpandConstant('{commonappdata}\NetX') + '"';
|
||||
Exec('powershell.exe', Params, ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
UninstallKey: String;
|
||||
begin
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
{ Always relink .venv -> local python/runtime (Setup is elevated). }
|
||||
RepairNetxVenvAfterInstall();
|
||||
|
||||
{ Upgrade keeps existing .env / pgdata - do not re-run DB wizard. }
|
||||
if not GSkipDbPage then
|
||||
ApplyDatabaseConfig()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"channel": "stable",
|
||||
"latest": "0.4.11",
|
||||
"latest": "0.4.12",
|
||||
"min_compatible": "0.3.0",
|
||||
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.11",
|
||||
"notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.12",
|
||||
"windows": {
|
||||
"url": "https://github.com/hansjone/netx/releases/download/v0.4.11/NetX-Setup-0.4.11.exe",
|
||||
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.11/NetX-Setup-0.4.11.exe",
|
||||
"url": "https://github.com/hansjone/netx/releases/download/v0.4.12/NetX-Setup-0.4.12.exe",
|
||||
"setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.12/NetX-Setup-0.4.12.exe",
|
||||
"package": "setup",
|
||||
"sha256": "",
|
||||
"size": 0
|
||||
|
|
|
|||
25
packaging/repair_venv.ps1
Normal file
25
packaging/repair_venv.ps1
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
param(
|
||||
[string]$ProgramRoot = "",
|
||||
[string]$DataRoot = ""
|
||||
)
|
||||
|
||||
# Relink shipped .venv to local python/runtime (run elevated after Setup/update).
|
||||
$ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot\_common.ps1"
|
||||
|
||||
$prog = Get-NetxProgramRoot -Override $ProgramRoot
|
||||
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||||
|
||||
Write-Host "==> Repairing .venv under $prog"
|
||||
if (Repair-NetxShippedVenv -ProgramRoot $prog) {
|
||||
$venvPy = Join-Path $prog ".venv\Scripts\python.exe"
|
||||
if (Test-NetxVenvRunnable -VenvPython $venvPy) {
|
||||
Write-Host "==> .venv OK -> bundled python/runtime" -ForegroundColor Green
|
||||
Get-Content (Join-Path $prog ".venv\pyvenv.cfg")
|
||||
exit 0
|
||||
}
|
||||
Write-Host "[ERR] pyvenv.cfg written but venv still not runnable" -ForegroundColor Red
|
||||
exit 2
|
||||
}
|
||||
Write-Host "[ERR] repair failed (missing runtime/.venv or access denied)" -ForegroundColor Red
|
||||
exit 1
|
||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "netx-ops"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue