Skip to content

Commit 4c76227

Browse files
authored
Add private-install-v2.ps1 for DeepStudio installation
Implement a PowerShell script for DeepStudio installation with features like verbose logging, dry run, and Azure CLI token acquisition.
1 parent 5eb91b4 commit 4c76227

1 file changed

Lines changed: 334 additions & 0 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
param(
2+
[switch]$VerboseInstall
3+
)
4+
5+
$ErrorActionPreference = "Stop"
6+
7+
# ---------------------------
8+
# Settings / Feature flags
9+
# ---------------------------
10+
$PackageName = if ($env:DEEPSTUDIO_PKG) { $env:DEEPSTUDIO_PKG } else { "deepstudio-server" }
11+
12+
# Support both: param -VerboseInstall and env DEEPSTUDIO_VERBOSE=1
13+
$VerboseInstall = $VerboseInstall -or ($env:DEEPSTUDIO_VERBOSE -eq "1")
14+
15+
# Dry run: only print commands, do not modify npm config or install
16+
$DryRun = ($env:DEEPSTUDIO_DRY_RUN -eq "1")
17+
18+
# Save logs to file when DEEPSTUDIO_LOG=1
19+
$EnableLog = ($env:DEEPSTUDIO_LOG -eq "1")
20+
21+
# Registry can be provided by env var (best for CI / automation)
22+
$RegistryFromEnv = $env:DEEPSTUDIO_REGISTRY
23+
24+
# Optional override for log path
25+
$LogPath = if ($env:DEEPSTUDIO_LOG_PATH) { $env:DEEPSTUDIO_LOG_PATH } else {
26+
Join-Path (Get-Location) ("deepstudio-install-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".log")
27+
}
28+
29+
# Default registry for DeepStudio (stored as base64)
30+
$DefaultRegistryB64 = "aHR0cHM6Ly9taWNyb3NvZnQucGtncy52aXN1YWxzdHVkaW8uY29tL09TL19wYWNrYWdpbmcvRGVlcFN0dWRpby9ucG0vcmVnaXN0cnkv"
31+
$DefaultRegistry = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DefaultRegistryB64))
32+
33+
# ---------------------------
34+
# Helpers
35+
# ---------------------------
36+
function Info([string]$msg) { Write-Host $msg }
37+
function Warn([string]$msg) { Write-Host $msg }
38+
function Fail([string]$msg) { Write-Host $msg }
39+
40+
function Require-Npm {
41+
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
42+
Write-Host ""
43+
Write-Host "❌ Node.js / npm not found."
44+
Write-Host "Please install Node.js first:"
45+
Write-Host "https://nodejs.org/en/download"
46+
Write-Host ""
47+
exit 1
48+
}
49+
}
50+
51+
function Read-Secure([string]$msg) {
52+
$secure = Read-Host $msg -AsSecureString
53+
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
54+
try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) }
55+
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
56+
}
57+
58+
function Mask-Token([string]$token) {
59+
if ([string]::IsNullOrEmpty($token)) { return "<empty>" }
60+
if ($token.Length -le 6) { return ("*" * $token.Length) }
61+
$head = $token.Substring(0, 3)
62+
$tail = $token.Substring($token.Length - 3, 3)
63+
return "$head" + ("*" * ($token.Length - 6)) + "$tail"
64+
}
65+
66+
function Ensure-Registry([string]$reg) {
67+
if ([string]::IsNullOrWhiteSpace($reg)) { throw "Registry URL is required." }
68+
$reg = $reg.Trim().TrimEnd("/")
69+
try { [void]([Uri]$reg) } catch { throw "Invalid registry URL: $reg" }
70+
return $reg
71+
}
72+
73+
function New-TempNpmrc {
74+
$path = Join-Path $env:TEMP ("deepstudio-npmrc-" + [Guid]::NewGuid().ToString("N") + ".npmrc")
75+
return $path
76+
}
77+
78+
function Write-IsolatedNpmrc([string]$path, [string]$registry, [string]$authPrefix, [string]$pat) {
79+
# Use _authToken to avoid conflicts with user's existing .npmrc and avoid base64 encoding pitfalls.
80+
# NOTE: This writes token into a temporary file; we delete it in finally.
81+
$content = @"
82+
registry=$registry/
83+
${authPrefix}:_authToken=$pat
84+
"@
85+
86+
if ($DryRun) {
87+
Info "DRYRUN: write isolated npmrc to $path"
88+
Info ("DRYRUN: npmrc content (token masked):`nregistry=$registry/`n${authPrefix}:_authToken=$(Mask-Token $pat)")
89+
return
90+
}
91+
92+
# ASCII is safest for .npmrc formatting
93+
Set-Content -Path $path -Value $content -Encoding ASCII
94+
}
95+
96+
function Run-NpmViaCmd([string]$cmdLine) {
97+
if ($DryRun) {
98+
Info ("DRYRUN: " + $cmdLine)
99+
return
100+
}
101+
102+
if (-not $EnableLog) {
103+
& cmd.exe /d /s /c $cmdLine
104+
if ($LASTEXITCODE -ne 0) {
105+
throw "npm failed with exit code $LASTEXITCODE. (Tip: set DEEPSTUDIO_LOG=1 to capture full logs.)"
106+
}
107+
return
108+
}
109+
110+
Info "Logging enabled. Log file: $LogPath"
111+
Info ("Command via cmd.exe: " + $cmdLine)
112+
113+
$psi = New-Object System.Diagnostics.ProcessStartInfo
114+
$psi.FileName = "cmd.exe"
115+
$psi.Arguments = "/d /s /c " + $cmdLine
116+
$psi.RedirectStandardOutput = $true
117+
$psi.RedirectStandardError = $true
118+
$psi.UseShellExecute = $false
119+
$psi.CreateNoWindow = $true
120+
121+
$p = New-Object System.Diagnostics.Process
122+
$p.StartInfo = $psi
123+
[void]$p.Start()
124+
125+
$stdout = $p.StandardOutput.ReadToEnd()
126+
$stderr = $p.StandardError.ReadToEnd()
127+
$p.WaitForExit()
128+
129+
if ($stdout) { Write-Host $stdout }
130+
if ($stderr) { Write-Host $stderr }
131+
132+
$content = @(
133+
"=== DeepStudio install log ==="
134+
"Time: $(Get-Date -Format o)"
135+
"Package: $PackageName@latest"
136+
"VerboseInstall: $VerboseInstall"
137+
"Registry: $registry/"
138+
"Command: $cmdLine"
139+
""
140+
"---- STDOUT ----"
141+
$stdout
142+
""
143+
"---- STDERR ----"
144+
$stderr
145+
""
146+
"ExitCode: $($p.ExitCode)"
147+
) -join "`r`n"
148+
149+
Set-Content -Path $LogPath -Value $content -Encoding UTF8
150+
151+
if ($p.ExitCode -ne 0) {
152+
throw "npm failed with exit code $($p.ExitCode). See log: $LogPath"
153+
}
154+
}
155+
156+
# ---------------------------
157+
# Azure CLI token acquisition
158+
# ---------------------------
159+
function Get-AzAccessToken {
160+
# Try to obtain a temporary access token via Azure CLI for Azure DevOps.
161+
# The resource ID 499b84ac-1321-427f-aa17-267ca6975798 is the well-known
162+
# resource identifier for Azure DevOps (Azure Artifacts / Packaging).
163+
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
164+
Info "Azure CLI (az) not found. Will fall back to manual PAT entry."
165+
return $null
166+
}
167+
168+
Info "Azure CLI found. Checking for existing az login session..."
169+
170+
try {
171+
$tokenJson = & az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query "accessToken" -o tsv 2>&1
172+
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($tokenJson)) {
173+
if ($VerboseInstall -and $tokenJson) { Info "az output: $tokenJson" }
174+
Info "No valid az login session found. Will fall back to manual PAT entry."
175+
return $null
176+
}
177+
$token = $tokenJson.Trim()
178+
Info "✅ Obtained temporary access token from Azure CLI session."
179+
return $token
180+
}
181+
catch {
182+
Info "Failed to get token from Azure CLI: $($_.Exception.Message)"
183+
Info "Will fall back to manual PAT entry."
184+
return $null
185+
}
186+
}
187+
188+
# ---------------------------
189+
# Start
190+
# ---------------------------
191+
Require-Npm
192+
193+
Info ""
194+
Info "=== DeepStudio npm installer (v2) ==="
195+
Info "Package: $PackageName@latest"
196+
Info ("VerboseInstall: " + $(if ($VerboseInstall) { "ON" } else { "OFF" }))
197+
Info ("DryRun: " + $(if ($DryRun) { "ON" } else { "OFF" }))
198+
Info ("LogToFile: " + $(if ($EnableLog) { "ON ($LogPath)" } else { "OFF" }))
199+
Info ""
200+
201+
# Get registry: env > default
202+
$registryInput = $RegistryFromEnv
203+
if ([string]::IsNullOrWhiteSpace($registryInput)) {
204+
$registryInput = $DefaultRegistry
205+
}
206+
$registry = Ensure-Registry $registryInput
207+
208+
Info "Registry: $registry"
209+
Info ""
210+
211+
# derive auth prefix (npmrc style)
212+
$uri = [Uri]$registry
213+
$authPrefix = "//" + $uri.Host + $uri.AbsolutePath.TrimEnd("/") + "/"
214+
215+
# Try Azure CLI token first, fall back to manual PAT
216+
$pat = Get-AzAccessToken
217+
218+
if ([string]::IsNullOrWhiteSpace($pat)) {
219+
Info ""
220+
Info "Manual PAT required. You can create one at:"
221+
Info " https://dev.azure.com/ > User Settings > Personal Access Tokens"
222+
Info " Scope: Packaging > Read"
223+
Info ""
224+
225+
$patRaw = Read-Secure "Enter Azure DevOps PAT (Packaging:Read)"
226+
if ([string]::IsNullOrWhiteSpace($patRaw)) { throw "PAT is empty." }
227+
228+
$pat = $patRaw.Trim()
229+
$patRaw = $null
230+
} else {
231+
Info "Using temporary token from Azure CLI (no PAT creation needed)."
232+
}
233+
234+
Info ""
235+
Info ("Token (masked): " + (Mask-Token $pat))
236+
Info ""
237+
238+
# npm install args / loglevel
239+
$logLevel = if ($VerboseInstall) { "verbose" } else { "notice" }
240+
241+
# Create isolated npmrc to avoid user's existing ~/.npmrc conflicts
242+
$tmpNpmrc = New-TempNpmrc
243+
244+
# Optional: extra debug signal for npm
245+
if ($VerboseInstall) {
246+
$env:NPM_CONFIG_LOGLEVEL = "verbose"
247+
$env:NPM_CONFIG_PROGRESS = "false"
248+
}
249+
250+
try {
251+
Info "Preparing isolated npm config (ignores your existing .npmrc)..."
252+
Write-IsolatedNpmrc -path $tmpNpmrc -registry $registry -authPrefix $authPrefix -pat $pat
253+
254+
# Build command line (use cmd.exe to avoid PowerShell alias/function issues with npm)
255+
# IMPORTANT: Use --userconfig so npm only reads our isolated temp npmrc for this run.
256+
$installCmd = 'npm install -g "{0}@latest" --registry "{1}/" --loglevel {2} --userconfig "{3}"' -f $PackageName, $registry, $logLevel, $tmpNpmrc
257+
258+
Info ""
259+
Info ("Command: " + $installCmd)
260+
Info ""
261+
262+
Run-NpmViaCmd $installCmd
263+
264+
# Extra safety: verify it's actually installed (still using isolated userconfig is ok)
265+
if (-not $DryRun) {
266+
$verifyCmd = 'npm list -g --depth=0 "{0}" --userconfig "{1}"' -f $PackageName, $tmpNpmrc
267+
Run-NpmViaCmd $verifyCmd
268+
}
269+
270+
Info ""
271+
Info "✅ Installed $PackageName@latest successfully."
272+
Info ""
273+
274+
# Ask user whether to start deepstudio-server
275+
$startChoice = Read-Host "Start $PackageName now? [Y/n]"
276+
if ([string]::IsNullOrWhiteSpace($startChoice) -or $startChoice -match '^[Yy]') {
277+
Info ""
278+
Info "🚀 Launching $PackageName (press Ctrl+C to stop)..."
279+
Info ""
280+
if ($DryRun) {
281+
Info "DRYRUN: would run $PackageName"
282+
} else {
283+
& $PackageName
284+
}
285+
} else {
286+
Info ""
287+
Info "You can start it later by running: $PackageName"
288+
}
289+
}
290+
catch {
291+
Info ""
292+
Fail "❌ Installation failed."
293+
Fail ("Error: " + $_.Exception.Message)
294+
295+
Warn ""
296+
Warn "Common causes for 401/403:"
297+
Warn " - Azure CLI token expired (try: az login)"
298+
Warn " - PAT missing Packaging:Read scope"
299+
Warn " - PAT pasted with extra whitespace (we trimmed, but double-check the masked value)"
300+
Warn " - Wrong registry URL (must be the npm/registry endpoint)"
301+
Warn " - No permission to the Azure Artifacts feed"
302+
Warn " - Corporate proxy/SSL interception issues"
303+
304+
if ($VerboseInstall) {
305+
Info ""
306+
Info "---- Full exception ----"
307+
Info $_.Exception.ToString()
308+
Info "------------------------"
309+
} else {
310+
Warn ""
311+
Warn "Tip: re-run with verbose:"
312+
Warn " `$env:DEEPSTUDIO_VERBOSE='1'; `$env:DEEPSTUDIO_LOG='1'; irm <url> | iex"
313+
}
314+
315+
if ($EnableLog -and -not $DryRun) {
316+
Warn ""
317+
Warn "Log saved to: $LogPath"
318+
}
319+
320+
throw
321+
}
322+
finally {
323+
# Clean up temp npmrc and env vars
324+
if (-not $DryRun) {
325+
Remove-Item $tmpNpmrc -Force -ErrorAction SilentlyContinue
326+
}
327+
if ($VerboseInstall) {
328+
Remove-Item Env:\NPM_CONFIG_LOGLEVEL -ErrorAction SilentlyContinue
329+
Remove-Item Env:\NPM_CONFIG_PROGRESS -ErrorAction SilentlyContinue
330+
}
331+
Info ""
332+
Info "✅ Cleanup complete."
333+
Info ""
334+
}

0 commit comments

Comments
 (0)