1+ param (
2+ [switch ]$VerboseInstall
3+ )
4+
15$ErrorActionPreference = " Stop"
26
7+ # ---------------------------
8+ # Settings / Feature flags
9+ # ---------------------------
310$PackageName = if ($env: DEEPSTUDIO_PKG ) { $env: DEEPSTUDIO_PKG } else { " deepstudio-server" }
411
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+ # ---------------------------
30+ # Helpers
31+ # ---------------------------
32+ function Info ([string ]$msg ) { Write-Host $msg }
33+ function Warn ([string ]$msg ) { Write-Host $msg }
34+ function Fail ([string ]$msg ) { Write-Host $msg }
35+
536function Require-Npm {
637 if (-not (Get-Command npm - ErrorAction SilentlyContinue)) {
738 Write-Host " "
@@ -20,47 +51,223 @@ function Read-Secure([string]$msg) {
2051 finally { [Runtime.InteropServices.Marshal ]::ZeroFreeBSTR($bstr ) }
2152}
2253
23- function SetCfg ($k , $v ) { npm config set -- global $k $v | Out-Null }
24- function DelCfg ($k ) { try { npm config delete -- global $k | Out-Null } catch {} }
54+ function Mask-Token ([string ]$token ) {
55+ if ([string ]::IsNullOrEmpty($token )) { return " <empty>" }
56+ if ($token.Length -le 6 ) { return (" *" * $token.Length ) }
57+ $head = $token.Substring (0 , 3 )
58+ $tail = $token.Substring ($token.Length - 3 , 3 )
59+ return " $head " + (" *" * ($token.Length - 6 )) + " $tail "
60+ }
2561
26- Require- Npm
62+ function Ensure-Registry ([string ]$reg ) {
63+ if ([string ]::IsNullOrWhiteSpace($reg )) { throw " Registry URL is required." }
64+ $reg = $reg.Trim ().TrimEnd(" /" )
65+ try { [void ]([Uri ]$reg ) } catch { throw " Invalid registry URL: $reg " }
66+ return $reg
67+ }
68+
69+ function SetCfg ([string ]$k , [string ]$v ) {
70+ if ($DryRun ) { Info " DRYRUN: npm config set --global $k <value>" ; return }
71+ npm config set -- global $k $v | Out-Null
72+ if ($LASTEXITCODE -ne 0 ) { throw " npm config set failed ($k ). exit=$LASTEXITCODE " }
73+ }
74+
75+ function DelCfg ([string ]$k ) {
76+ if ($DryRun ) { Info " DRYRUN: npm config delete --global $k " ; return }
77+ try {
78+ npm config delete -- global $k | Out-Null
79+ } catch {}
80+ }
81+
82+ function Run-NpmInstall ([string []]$args ) {
83+ if ($DryRun ) {
84+ Info (" DRYRUN: npm " + ($args -join " " ))
85+ return
86+ }
87+
88+ if (-not $EnableLog ) {
89+ & npm @args
90+ if ($LASTEXITCODE -ne 0 ) {
91+ throw " npm failed with exit code $LASTEXITCODE . (Tip: set DEEPSTUDIO_LOG=1 to capture full logs.)"
92+ }
93+ return
94+ }
95+
96+ # Logging enabled: capture stdout/stderr to file AND show on console
97+ Info " Logging enabled. Log file: $LogPath "
98+
99+ $psi = New-Object System.Diagnostics.ProcessStartInfo
100+ $psi.FileName = " npm"
101+ $psi.Arguments = ($args -join " " )
102+ $psi.RedirectStandardOutput = $true
103+ $psi.RedirectStandardError = $true
104+ $psi.UseShellExecute = $false
105+ $psi.CreateNoWindow = $true
106+
107+ $p = New-Object System.Diagnostics.Process
108+ $p.StartInfo = $psi
109+ [void ]$p.Start ()
110+
111+ $outTask = $p.StandardOutput.ReadToEndAsync ()
112+ $errTask = $p.StandardError.ReadToEndAsync ()
113+
114+ $p.WaitForExit ()
115+
116+ $stdout = $outTask.Result
117+ $stderr = $errTask.Result
118+
119+ if ($stdout ) { Write-Host $stdout }
120+ if ($stderr ) { Write-Host $stderr }
121+
122+ $content = @ (
123+ " === DeepStudio install log ==="
124+ " Time: $ ( Get-Date - Format o) "
125+ " Package: $PackageName @latest"
126+ " VerboseInstall: $VerboseInstall "
127+ " Registry: $registry /"
128+ " Command: npm " + ($args -join " " )
129+ " "
130+ " ---- STDOUT ----"
131+ $stdout
132+ " "
133+ " ---- STDERR ----"
134+ $stderr
135+ " "
136+ " ExitCode: $ ( $p.ExitCode ) "
137+ ) -join " `r`n "
27138
28- $registry = Read-Host " Enter npm registry URL (for ex: https://xxx.pkgs.xxx.com/xxx/_packaging/xxx/npm/registry/)"
29- if ([string ]::IsNullOrWhiteSpace($registry )) {
30- throw " Registry URL is required."
139+ Set-Content - Path $LogPath - Value $content - Encoding UTF8
140+
141+ if ($p.ExitCode -ne 0 ) {
142+ throw " npm failed with exit code $ ( $p.ExitCode ) . See log: $LogPath "
143+ }
31144}
32145
33- # normalize registry
34- $registry = $registry.TrimEnd (" /" )
146+ # ---------------------------
147+ # Start
148+ # ---------------------------
149+ Require- Npm
150+
151+ Info " "
152+ Info " === DeepStudio npm installer ==="
153+ Info " Package: $PackageName @latest"
154+ Info (" VerboseInstall: " + $ (if ($VerboseInstall ) { " ON" } else { " OFF" }))
155+ Info (" DryRun: " + $ (if ($DryRun ) { " ON" } else { " OFF" }))
156+ Info (" LogToFile: " + $ (if ($EnableLog ) { " ON ($LogPath )" } else { " OFF" }))
157+ Info " "
35158
36- # derive auth prefix
159+ # Get registry: env > prompt
160+ $registryInput = $RegistryFromEnv
161+ if ([string ]::IsNullOrWhiteSpace($registryInput )) {
162+ $registryInput = Read-Host " Enter npm registry URL (ex: https://xxx.pkgs.xxx.com/xxx/_packaging/xxx/npm/registry/)"
163+ }
164+ $registry = Ensure- Registry $registryInput
165+
166+ # derive auth prefix (npmrc style)
37167$uri = [Uri ]$registry
38168$authPrefix = " //" + $uri.Host + $uri.AbsolutePath + " /"
39169
40- $pat = Read-Secure " Enter Azure DevOps PAT (Packaging:Read)"
41- if ([string ]::IsNullOrWhiteSpace($pat )) {
42- throw " PAT is empty."
43- }
170+ # Read PAT securely then TRIM it (fix common 401 due to whitespace/newlines)
171+ $patRaw = Read-Secure " Enter Azure DevOps PAT (Packaging:Read)"
172+ if ([string ]::IsNullOrWhiteSpace($patRaw )) { throw " PAT is empty." }
173+
174+ $pat = $patRaw.Trim ()
175+ $patRaw = $null
176+
177+ Info " "
178+ Info (" PAT (masked): " + (Mask- Token $pat ))
179+ Info " Tip: if masked prefix/suffix looks wrong, you likely pasted extra chars/spaces."
180+ Info " "
44181
45182$patB64 = [Convert ]::ToBase64String([Text.Encoding ]::UTF8.GetBytes($pat ))
46183$pat = $null
47184
48- try {
49- Write-Host " Installing $PackageName @latest"
185+ # npm install args
186+ $logLevel = if ($VerboseInstall ) { " verbose" } else { " notice" }
187+ $npmInstallArgs = @ (
188+ " install" , " -g" , " $PackageName @latest" ,
189+ " --registry" , " $registry /" ,
190+ " --loglevel" , $logLevel
191+ )
50192
193+ # Optional: extra debug signal for npm
194+ if ($VerboseInstall ) {
195+ $env: NPM_CONFIG_LOGLEVEL = " verbose"
196+ $env: NPM_CONFIG_PROGRESS = " false"
197+ }
198+
199+ try {
200+ Info " Configuring npm auth for this registry..."
51201 SetCfg " registry" " $registry /"
52202 SetCfg " ${authPrefix} :username" " ms"
53203 SetCfg " ${authPrefix} :_password" $patB64
54204 SetCfg " ${authPrefix} :email" " npm@example.com"
205+ SetCfg " always-auth" " true"
206+
207+ Info " "
208+ Info (" Command: npm " + ($npmInstallArgs -join " " ))
209+ Info " "
55210
56- npm install - g " $PackageName @latest" -- registry " $registry /"
57- Write-Host " ✅ Installed $PackageName @latest successfully."
58- Write-Host " You can now run $PackageName to launch it."
211+ Run- NpmInstall $npmInstallArgs
212+
213+ # Extra safety: verify it's actually installed
214+ if (-not $DryRun ) {
215+ npm list - g -- depth= 0 " $PackageName " | Out-Null
216+ if ($LASTEXITCODE -ne 0 ) {
217+ throw " Install command finished but package not found in global npm list. Something is wrong."
218+ }
219+ }
220+
221+ Info " "
222+ Info " ✅ Installed $PackageName @latest successfully."
223+ Info " You can now run: $PackageName "
224+ }
225+ catch {
226+ Info " "
227+ Fail " ❌ Installation failed."
228+ Fail (" Error: " + $_.Exception.Message )
229+
230+ # Helpful hints for common 401/403 issues
231+ Warn " "
232+ Warn " Common causes for 401/403:"
233+ Warn " - PAT missing Packaging:Read scope"
234+ Warn " - PAT pasted with extra whitespace (we trimmed, but double-check the masked value)"
235+ Warn " - Wrong registry URL (must be the npm/registry endpoint)"
236+ Warn " - No permission to the Azure Artifacts feed"
237+ Warn " - Corporate proxy/SSL interception issues"
238+
239+ if ($VerboseInstall ) {
240+ Info " "
241+ Info " ---- Full exception ----"
242+ Info $_.Exception.ToString ()
243+ Info " ------------------------"
244+ } else {
245+ Warn " "
246+ Warn " Tip: re-run with verbose:"
247+ Warn " `$ env:DEEPSTUDIO_VERBOSE='1'; irm <url> | iex"
248+ }
249+
250+ if ($EnableLog -and -not $DryRun ) {
251+ Warn " "
252+ Warn " Log saved to: $LogPath "
253+ }
254+
255+ throw
59256}
60257finally {
258+ Info " "
259+ Info " Cleaning up npm global config..."
61260 DelCfg " registry"
62261 DelCfg " ${authPrefix} :username"
63262 DelCfg " ${authPrefix} :_password"
64263 DelCfg " ${authPrefix} :email"
65- Write-Host " ✅ Cleanup complete."
264+ DelCfg " always-auth"
265+
266+ if ($VerboseInstall ) {
267+ Remove-Item Env:\NPM_CONFIG_LOGLEVEL - ErrorAction SilentlyContinue
268+ Remove-Item Env:\NPM_CONFIG_PROGRESS - ErrorAction SilentlyContinue
269+ }
270+
271+ Info " ✅ Cleanup complete."
272+ Info " "
66273}
0 commit comments