Skip to content

Commit 68ecca6

Browse files
authored
Update install.ps1
1 parent e071f39 commit 68ecca6

1 file changed

Lines changed: 159 additions & 51 deletions

File tree

‎deepstudio/xtester/install.ps1‎

Lines changed: 159 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# =============================================================================
1+
# =============================================================================
22
# X-Tester MCP installer
33
#
44
# Delivery model:
@@ -38,14 +38,33 @@ param(
3838

3939
[switch]$Force,
4040

41-
[switch]$SkipClientInstall
41+
[switch]$SkipClientInstall,
42+
43+
[switch]$Help
4244
)
4345

4446
$ErrorActionPreference = "Stop"
4547

46-
$Script:InstallerVersion = "1.12.1"
48+
$Script:InstallerVersion = "1.13.1"
4749
$Script:AgencyXTesterPluginPrompted = $false
4850

51+
if ($Help) {
52+
@"
53+
XTester installer
54+
55+
Parameters:
56+
-Source <private-feed|local> Install source. Default: private-feed
57+
-Version <version> Optional XTester package version
58+
-Client <name[]> copilot, claude-desktop, claude-code, vscode, or all
59+
-VenvPath <path> Managed MCP virtual environment
60+
-LocalPath <path> Package root for -Source local
61+
-Force Recreate the managed environment
62+
-SkipClientInstall Do not install missing client CLIs
63+
-Help Show this help without changing local state
64+
"@
65+
return
66+
}
67+
4968
try {
5069
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
5170
$OutputEncoding = [System.Text.Encoding]::UTF8
@@ -83,6 +102,21 @@ function Invoke-External([string]$FilePath, [string[]]$Arguments) {
83102
}
84103
}
85104

105+
function Get-XTesterReadinessStatePath {
106+
if ([string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
107+
throw "LOCALAPPDATA is required to locate XTester readiness state."
108+
}
109+
return Join-Path $env:LOCALAPPDATA "XTester\state\readiness-v1.json"
110+
}
111+
112+
function Invalidate-XTesterReadiness {
113+
$statePath = Get-XTesterReadinessStatePath
114+
if (Test-Path -LiteralPath $statePath) {
115+
Remove-Item -LiteralPath $statePath -Force -ErrorAction Stop
116+
Info "Invalidated previous readiness state: $statePath"
117+
}
118+
}
119+
86120
# winget renders its download spinner with `\r` redraws. When PowerShell wraps
87121
# stdout (any `2>&1 | ...` pipeline counts as a non-TTY consumer) every frame
88122
# is flushed with a newline, producing a vertical column of `-`,`\\`,`|`,`/`
@@ -279,7 +313,7 @@ function Write-JsonObject([string]$Path, $Value) {
279313

280314
function Resolve-LocalPackagePath {
281315
if (-not $LocalPath) {
282-
throw "-Source local requires -LocalPath pointing at the XTester package directory (e.g. -LocalPath C:\path\to\DeepTest\XTester)."
316+
throw "-Source local requires -LocalPath pointing at the XTester package directory (e.g. -LocalPath C:\path\to\xtester)."
283317
}
284318
if (-not (Test-Path -LiteralPath $LocalPath)) {
285319
throw "Local package path does not exist: $LocalPath"
@@ -749,6 +783,69 @@ function Get-LatestXTesterVersionFromFeed([string]$VenvPython) {
749783
return $null
750784
}
751785

786+
function Get-XTesterLauncherRepairVersion([string]$VenvPython) {
787+
# This probe must also work with older installed XTester versions, so it
788+
# uses Python's metadata rather than importing a new package helper. Single
789+
# quotes in Python survive Windows PowerShell 5.1 native argument passing.
790+
$probe = @'
791+
import os
792+
import re
793+
import sys
794+
import sysconfig
795+
from importlib.metadata import distribution
796+
from pathlib import Path
797+
798+
dist = distribution('xtester')
799+
version = dist.version
800+
if not re.fullmatch(r'[0-9][A-Za-z0-9.!+_-]*', version):
801+
raise SystemExit('Invalid installed XTester version metadata')
802+
names = {entry.name for entry in dist.entry_points if entry.group == 'console_scripts'}
803+
if not {'xtester', 'xtester-mcp'} <= names:
804+
raise SystemExit('XTester metadata is missing required console_scripts')
805+
if any(not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*', name) or name.endswith('.') for name in names):
806+
raise SystemExit('Invalid XTester console_scripts name')
807+
scripts = Path(sysconfig.get_path('scripts')).resolve()
808+
suffix = '.exe' if os.name == 'nt' else ''
809+
missing = []
810+
for name in sorted(names):
811+
launcher = scripts / (name + suffix)
812+
if launcher.resolve().parent != scripts or not launcher.is_file() or not os.access(launcher, os.X_OK):
813+
missing.append(name)
814+
if missing:
815+
print('Missing XTester launchers: ' + ', '.join(missing), file=sys.stderr)
816+
print(version)
817+
'@
818+
$repairVersion = & $VenvPython -I -c $probe
819+
if ($LASTEXITCODE -ne 0) {
820+
throw "Could not validate managed XTester launcher metadata (exit code $LASTEXITCODE). Client registration stopped."
821+
}
822+
return $repairVersion
823+
}
824+
825+
function Ensure-XTesterLaunchers([string]$VenvPython, [string]$WheelPath = "") {
826+
$repairVersion = Get-XTesterLauncherRepairVersion $VenvPython
827+
if ([string]::IsNullOrWhiteSpace($repairVersion)) { return }
828+
829+
Warn "Repairing missing XTester launchers at installed version $repairVersion without changing dependencies."
830+
$repairArgs = @(
831+
"-m", "pip", "install", "--progress-bar", "off", "--force-reinstall", "--no-deps",
832+
"xtester==$repairVersion"
833+
)
834+
if ($WheelPath) {
835+
$repairArgs += $WheelPath
836+
} else {
837+
$repairArgs += @("--index-url", $FeedUrl, "--extra-index-url", $ExtraIndexUrl)
838+
}
839+
# Do not include feed URLs in an exception: they may contain credentials.
840+
& $VenvPython @repairArgs
841+
if ($LASTEXITCODE -ne 0) {
842+
throw "XTester launcher repair failed (exit code $LASTEXITCODE). Client registration stopped."
843+
}
844+
if (Get-XTesterLauncherRepairVersion $VenvPython) {
845+
throw "XTester launchers are still missing after one repair attempt. Client registration stopped."
846+
}
847+
}
848+
752849
function Install-XTesterIntoVenv([string]$VenvPython) {
753850
Invoke-WithCleanPythonPath {
754851
Invoke-Step "Upgrading pip and installing private-feed auth helpers" {
@@ -758,12 +855,8 @@ function Install-XTesterIntoVenv([string]$VenvPython) {
758855

759856
if ($Source -eq "local") {
760857
$path = Resolve-LocalPackagePath
761-
# Build a wheel first, then pip install the wheel. Editable installs
762-
# (`pip install -e`) skip setup.py's build_py cmdclass, which means the
763-
# bundled_skills/ tree (daily-test-report and friends) never gets staged
764-
# into site-packages, and xtester-install-skills SKIPs the tree-skill
765-
# with "not staged; dev source?". Building the wheel first matches what
766-
# the private-feed path installs and keeps -Source local honest.
858+
# Build a wheel first so the local path exercises the same package-data
859+
# behavior as the private-feed installation.
767860
$distDir = Join-Path $path "dist"
768861
$buildDir = Join-Path $path "build"
769862
Invoke-Step "Building X-Tester wheel from local source: $path" {
@@ -783,6 +876,7 @@ function Install-XTesterIntoVenv([string]$VenvPython) {
783876
Invoke-External $VenvPython @("-m", "pip", "install", "--progress-bar", "off", "--force-reinstall", "--no-deps", $wheel.FullName)
784877
Invoke-External $VenvPython @("-m", "pip", "install", "--progress-bar", "off", "-U", $wheel.FullName)
785878
}
879+
Ensure-XTesterLaunchers $VenvPython $wheel.FullName
786880
return
787881
}
788882

@@ -857,27 +951,21 @@ function Install-XTesterIntoVenv([string]$VenvPython) {
857951
Success "Install succeeded with fallback extra index: $fallbackExtraIndexUrl"
858952
}
859953
}
954+
Ensure-XTesterLaunchers $VenvPython
860955
}
861956
}
862957

863958
function Resolve-XTesterMcpFromVenv {
864959
$exe = Join-Path $VenvPath "Scripts\xtester-mcp.exe"
865-
if (-not (Test-Path -LiteralPath $exe)) {
960+
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
866961
throw "xtester-mcp.exe was not installed in the managed venv: $exe"
867962
}
868963
return (Resolve-Path -LiteralPath $exe).Path
869964
}
870965

871-
function Resolve-XTesterMcpDoctorFromVenv {
872-
$exe = Join-Path $VenvPath "Scripts\xtester-mcp-doctor.exe"
873-
if (-not (Test-Path -LiteralPath $exe)) {
874-
throw "xtester-mcp-doctor.exe was not installed in the managed venv: $exe"
875-
}
876-
return (Resolve-Path -LiteralPath $exe).Path
877-
}
878-
879-
function Test-XTesterMcpProtocol([string]$DoctorPath, [string]$CommandPath) {
880-
Invoke-External $DoctorPath @("--command", $CommandPath, "--timeout-seconds", "10")
966+
function Test-XTesterMcpProtocol([string]$VenvPython, [string]$CommandPath) {
967+
# Ignore checkout modules and PYTHONPATH when loading the installed Doctor.
968+
Invoke-External $VenvPython @("-I", "-m", "xtester.mcp.doctor", "--command", $CommandPath, "--timeout-seconds", "10")
881969
}
882970

883971
function Ensure-XTesterCliPath([string]$VenvPython) {
@@ -995,21 +1083,13 @@ function Offer-AgencyXTesterPluginUpdate {
9951083
function Register-ClaudeCode([string]$CommandPath) {
9961084
$claude = Get-Command "claude" -ErrorAction SilentlyContinue
9971085
if (-not $claude) {
998-
Warn "claude CLI was not found; skipping Claude Code registration."
999-
Warn "Use Claude Desktop registration or install Claude Code, then re-run with -Client claude-code."
1000-
return
1086+
throw "claude CLI was not found; install Claude Code or select another client."
10011087
}
10021088

10031089
Offer-AgencyXTesterPluginUpdate
10041090

1005-
try {
1006-
Invoke-Step "Registering X-Tester MCP with Claude Code" {
1007-
Invoke-External $claude.Source @("mcp", "add", $ServerName, $CommandPath)
1008-
}
1009-
}
1010-
catch {
1011-
Warn "claude mcp add failed: $($_.Exception.Message)"
1012-
Warn "Run 'claude mcp --help' to confirm this Claude Code version supports MCP registration."
1091+
Invoke-Step "Registering X-Tester MCP with Claude Code" {
1092+
Invoke-External $claude.Source @("mcp", "add", $ServerName, $CommandPath)
10131093
}
10141094
}
10151095

@@ -1067,8 +1147,7 @@ function Register-VSCode([string]$CommandPath) {
10671147
Success "Rewrote VS Code user MCP config: $configPath"
10681148
}
10691149
catch {
1070-
Warn "Skipped VS Code registration: $($_.Exception.Message)"
1071-
Info "Re-run with -Client vscode after fixing $configPath to add it later."
1150+
throw "VS Code registration failed after config recovery: $($_.Exception.Message)"
10721151
}
10731152
}
10741153
}
@@ -1080,8 +1159,8 @@ function Expand-Clients {
10801159
return $Client | Select-Object -Unique
10811160
}
10821161

1083-
function Register-XTesterClients([string]$DoctorPath, [string]$CommandPath, [string[]]$Targets) {
1084-
Test-XTesterMcpProtocol $DoctorPath $CommandPath
1162+
function Register-XTesterClients([string]$VenvPython, [string]$CommandPath, [string[]]$Targets) {
1163+
Test-XTesterMcpProtocol $VenvPython $CommandPath
10851164
foreach ($target in $Targets) {
10861165
switch ($target) {
10871166
"copilot" { Register-Copilot $CommandPath }
@@ -1103,6 +1182,8 @@ Write-Host ""
11031182
Write-Host " X-Tester MCP installer v$Script:InstallerVersion" -ForegroundColor Magenta
11041183
Write-Host ""
11051184

1185+
Invalidate-XTesterReadiness
1186+
11061187
$pythonInfo = Resolve-Python
11071188
$pythonInvocation = @($pythonInfo.FilePath) + $pythonInfo.Arguments
11081189
Success "Using Python: $($pythonInfo.Version) ($($pythonInvocation -join ' '))"
@@ -1144,17 +1225,17 @@ $venvPython = New-ManagedVenv $pythonInfo
11441225
Install-XTesterIntoVenv $venvPython
11451226
Ensure-XTesterCliPath $venvPython
11461227
$xtesterMcp = Resolve-XTesterMcpFromVenv
1147-
$xtesterMcpDoctor = Resolve-XTesterMcpDoctorFromVenv
11481228
Success "Using xtester-mcp: $xtesterMcp"
11491229

1230+
$selectedClients = @(Expand-Clients)
11501231
Invoke-Step "Checking protocol and registering X-Tester MCP clients" {
1151-
Register-XTesterClients $xtesterMcpDoctor $xtesterMcp (Expand-Clients)
1232+
Register-XTesterClients $venvPython $xtesterMcp $selectedClients
11521233
}
1234+
$registeredClients = @($selectedClients)
11531235

11541236
# Opportunistic: if the user installed for copilot only and the Claude Code CLI
11551237
# is on PATH, offer to register there too. Skipped non-interactively (no host
11561238
# UI) and when the user already asked for claude-code or -SkipClientInstall.
1157-
$selectedClients = Expand-Clients
11581239
if (-not $SkipClientInstall `
11591240
-and ($selectedClients -contains 'copilot') `
11601241
-and (-not ($selectedClients -contains 'claude-code')) `
@@ -1171,6 +1252,7 @@ if (-not $SkipClientInstall `
11711252
}
11721253
elseif ([string]::IsNullOrWhiteSpace($answer) -or $answer -match '^(y|yes)$') {
11731254
Register-ClaudeCode $xtesterMcp
1255+
$registeredClients += "claude-code"
11741256
} else {
11751257
Info "Skipping Claude Code registration. Re-run with -Client claude-code to add it later."
11761258
}
@@ -1195,6 +1277,7 @@ if (-not $SkipClientInstall `
11951277
}
11961278
elseif ([string]::IsNullOrWhiteSpace($answer) -or $answer -match '^(y|yes)$') {
11971279
Register-VSCode $xtesterMcp
1280+
$registeredClients += "vscode"
11981281
} else {
11991282
Info "Skipping VS Code registration. Re-run with -Client vscode to add it later."
12001283
}
@@ -1203,16 +1286,30 @@ if (-not $SkipClientInstall `
12031286
# ---------------------------------------------------------------------------
12041287
# Bundled Copilot CLI skills -> ~/.copilot/skills/
12051288
# Only runs when copilot is among the selected clients (other hosts use their
1206-
# own skill mechanisms). The helper overwrites the four xtester-* slots.
1289+
# own skill mechanisms). The helper atomically replaces the unified slot.
12071290
# ---------------------------------------------------------------------------
12081291
if ($Client -contains "copilot" -or $Client -contains "all") {
1292+
$skillsRoot = & $venvPython -I -c "import os; from pathlib import Path; print(Path(os.environ.get('XTESTER_SKILLS_DEST') or Path.home() / '.copilot' / 'skills').expanduser().absolute())"
1293+
if ($LASTEXITCODE -ne 0) { throw "Could not resolve the Copilot skills destination." }
1294+
$retiredSkills = @(
1295+
foreach ($name in @("xtester-setup", "xtester-create", "xtester-run", "xtester-report")) {
1296+
$path = Join-Path $skillsRoot $name
1297+
if (Test-Path -LiteralPath $path -PathType Container) { $path }
1298+
}
1299+
)
1300+
$skillPrompt = "Install the bundled xtester Copilot CLI skill into ~/.copilot/skills/? [Y/n]"
1301+
if ($retiredSkills.Count -gt 0) {
1302+
Warn "These retired skills were replaced by the unified xtester skill:"
1303+
foreach ($path in $retiredSkills) { Info $path }
1304+
Warn "They no longer match the current MCP interface and may lead to incorrect calls. We strongly recommend Yes."
1305+
$skillPrompt = "Install the unified xtester skill and remove these retired skills? [Y/n]"
1306+
}
1307+
$proceed = $false
12091308
$cliSkillsBin = Join-Path $VenvPath "Scripts\xtester-install-skills.exe"
12101309
if (Test-Path -LiteralPath $cliSkillsBin) {
1211-
$proceed = $false
12121310
Write-Host ""
1213-
$answer = Read-OptionalPrompt `
1214-
"Install bundled xtester-* Copilot CLI skills into ~/.copilot/skills/? [Y/n]" `
1215-
"Non-interactive host; skipping bundled xtester-* skill install (re-run interactively to install)."
1311+
$answer = Read-OptionalPrompt $skillPrompt `
1312+
"Non-interactive host; skipping bundled xtester skill install (re-run interactively to install)."
12161313
if ($null -ne $answer) {
12171314
if ([string]::IsNullOrWhiteSpace($answer) -or $answer -match '^(y|yes)$') {
12181315
$proceed = $true
@@ -1222,23 +1319,34 @@ if ($Client -contains "copilot" -or $Client -contains "all") {
12221319
}
12231320
}
12241321
if ($proceed) {
1225-
Invoke-Step "Installing bundled xtester-* Copilot CLI skills into ~/.copilot/skills/" {
1226-
try {
1227-
Invoke-External $cliSkillsBin @()
1228-
}
1229-
catch {
1230-
Warn "xtester-install-skills failed: $($_.Exception.Message)"
1231-
}
1322+
Invoke-Step "Installing bundled xtester Copilot CLI skill into $skillsRoot" {
1323+
Invoke-External $cliSkillsBin @()
12321324
}
12331325
}
12341326
}
12351327
else {
12361328
Warn "xtester-install-skills not found at $cliSkillsBin; skipping CLI skill install."
12371329
}
1330+
if ($retiredSkills.Count -gt 0 -and -not $proceed) {
1331+
Warn "Skill migration incomplete; retired skills were not removed. Re-run interactively and choose Yes to migrate."
1332+
}
1333+
}
1334+
1335+
$readinessArguments = @(
1336+
"-m", "xtester.readiness", "mark-ready",
1337+
"--installer-version", $Script:InstallerVersion,
1338+
"--mcp-executable", $xtesterMcp
1339+
)
1340+
foreach ($registeredClient in ($registeredClients | Sort-Object -Unique)) {
1341+
$readinessArguments += @("--client", $registeredClient)
1342+
}
1343+
Invoke-Step "Recording validated XTester readiness" {
1344+
Invoke-External $venvPython $readinessArguments
12381345
}
12391346

12401347
Write-Host ""
12411348
Success "X-Tester MCP installation complete."
12421349
Info "Server name: $ServerName"
12431350
Info "Managed venv: $VenvPath"
12441351
Info "MCP command: $xtesterMcp"
1352+
Info "Reconnect or restart registered clients to refresh MCP and skill discovery."

0 commit comments

Comments
 (0)