@@ -26,13 +26,20 @@ $LogPath = if ($env:DEEPSTUDIO_LOG_PATH) { $env:DEEPSTUDIO_LOG_PATH } else {
2626 Join-Path (Get-Location ) (" deepstudio-install-" + (Get-Date - Format " yyyyMMdd-HHmmss" ) + " .log" )
2727}
2828
29+ # Installer version
30+ $InstallerVersion = " 2.6"
31+
2932# Default registry for DeepStudio (stored as base64)
3033$DefaultRegistryB64 = " aHR0cHM6Ly9taWNyb3NvZnQucGtncy52aXN1YWxzdHVkaW8uY29tL09TL19wYWNrYWdpbmcvRGVlcFN0dWRpby9ucG0vcmVnaXN0cnkv"
3134$DefaultRegistry = [System.Text.Encoding ]::UTF8.GetString([System.Convert ]::FromBase64String($DefaultRegistryB64 ))
3235
3336# Will be set later after registry is resolved
3437$script :ResolvedRegistry = $null
3538
39+ # Rolling in-memory log buffer (last N lines) — dumped on error
40+ $script :LogBuffer = [System.Collections.Generic.Queue [string ]]::new()
41+ $script :LogBufferMax = 100
42+
3643# ---------------------------
3744# Helpers
3845# ---------------------------
@@ -42,14 +49,32 @@ function Fail([string]$msg) { Write-Host " ✖ $msg" -ForegroundColor Red }
4249function Success ([string ]$msg ) { Write-Host " ✔ $msg " - ForegroundColor Green }
4350function Dim ([string ]$msg ) { Write-Host " $msg " - ForegroundColor DarkGray }
4451
52+ function Add-ToLogBuffer ([string ]$line ) {
53+ $script :LogBuffer.Enqueue ($line )
54+ while ($script :LogBuffer.Count -gt $script :LogBufferMax ) {
55+ [void ]$script :LogBuffer.Dequeue ()
56+ }
57+ }
58+
59+ function Dump-LogBuffer {
60+ if ($script :LogBuffer.Count -eq 0 ) { return }
61+ Write-Host " "
62+ Warn " Recent output (last $ ( $script :LogBuffer.Count ) lines):"
63+ Write-Host " ────────────────────────────────────────────────" - ForegroundColor DarkGray
64+ foreach ($line in $script :LogBuffer ) {
65+ Dim $line
66+ }
67+ Write-Host " ────────────────────────────────────────────────" - ForegroundColor DarkGray
68+ }
69+
4570function Show-Banner {
4671 $lines = @ (
4772 " ____ ____ _ _ _ "
4873 " | _ \ ___ ___ _ __ / ___|| |_ _ _ __| (_) ___ "
4974 " | | | |/ _ \/ _ \ '_ \\___ \| __| | | |/ _`` | |/ _ \ "
5075 " | |_| | __/ __/ |_) |___) | |_| |_| | (_| | | (_) |"
5176 " |____/ \___|\___| .__/|____/ \__|\__,_|\__,_|_|\___/ "
52- " |_| I n s t a l l e r v2.1 "
77+ " |_| I n s t a l l e r v $InstallerVersion "
5378 )
5479 $colors = @ (" Red" , " Yellow" , " Green" , " Cyan" , " Blue" , " Magenta" )
5580 Write-Host " "
@@ -237,12 +262,13 @@ function New-TempNpmrc {
237262function Write-IsolatedNpmrc ([string ]$path , [string ]$registry , [string ]$authPrefix , [string ]$pat ) {
238263 $content = @"
239264registry=$registry /
265+ always-auth=true
240266${authPrefix} :_authToken=$pat
241267"@
242268
243269 if ($DryRun ) {
244270 Info " DRYRUN: write isolated npmrc to $path "
245- Info (" DRYRUN: npmrc content (token masked):`n registry=$registry /`n ${authPrefix} :_authToken=$ ( Mask- Token $pat ) " )
271+ Info (" DRYRUN: npmrc content (token masked):`n registry=$registry /`n always-auth=true ` n${authPrefix} :_authToken=$ ( Mask- Token $pat ) " )
246272 return
247273 }
248274
@@ -291,23 +317,97 @@ function Show-ProxyDiagnostics {
291317 }
292318}
293319
320+ function Show-NpmConfigConflicts {
321+ Write-Host " "
322+ Info " 🔎 Checking for conflicting npm configurations..."
323+ $conflicts = 0
324+
325+ # 1. Check NPM_CONFIG_REGISTRY env var (overrides --registry in some npm versions)
326+ $envRegistry = $env: NPM_CONFIG_REGISTRY
327+ if (-not [string ]::IsNullOrWhiteSpace($envRegistry )) {
328+ $conflicts ++
329+ Warn " NPM_CONFIG_REGISTRY env var is set: $envRegistry "
330+ Warn " This can override --registry flag. Will be cleared during install."
331+ }
332+
333+ # 2. Check for project-level .npmrc in cwd and parent directories
334+ $searchDir = Get-Location
335+ $projectNpmrcs = @ ()
336+ while ($null -ne $searchDir -and $searchDir.Path.Length -gt 3 ) {
337+ $candidate = Join-Path $searchDir.Path " .npmrc"
338+ if (Test-Path $candidate ) {
339+ $projectNpmrcs += $candidate
340+ }
341+ $searchDir = Split-Path $searchDir.Path - Parent | Get-Item - ErrorAction SilentlyContinue
342+ }
343+ if ($projectNpmrcs.Count -gt 0 ) {
344+ foreach ($p in $projectNpmrcs ) {
345+ $conflicts ++
346+ Warn " Project .npmrc found: $p "
347+ # Check if it sets a registry
348+ $content = Get-Content $p - Raw - ErrorAction SilentlyContinue
349+ if ($content -match ' (?m)^\s*registry\s*=' ) {
350+ Warn " ↳ Contains 'registry=' — this WILL override the install registry!"
351+ }
352+ if ($content -match ' (?m)^\s*//.*:_auth' ) {
353+ Dim " ↳ Contains auth tokens"
354+ }
355+ }
356+ }
357+
358+ # 3. Check for global npmrc
359+ try {
360+ $globalNpmrc = & npm config get globalconfig 2> $null
361+ if ($LASTEXITCODE -eq 0 -and -not [string ]::IsNullOrWhiteSpace($globalNpmrc ) -and (Test-Path $globalNpmrc )) {
362+ $content = Get-Content $globalNpmrc - Raw - ErrorAction SilentlyContinue
363+ if ($content -match ' (?m)^\s*registry\s*=' ) {
364+ $conflicts ++
365+ Warn " Global .npmrc has 'registry=' set: $globalNpmrc "
366+ Warn " ↳ This can override the install registry!"
367+ }
368+ }
369+ } catch {}
370+
371+ # 4. Check the user-level npmrc for conflicting registry
372+ try {
373+ $userNpmrc = & npm config get userconfig 2> $null
374+ if ($LASTEXITCODE -eq 0 -and -not [string ]::IsNullOrWhiteSpace($userNpmrc ) -and (Test-Path $userNpmrc )) {
375+ $content = Get-Content $userNpmrc - Raw - ErrorAction SilentlyContinue
376+ if ($content -match ' (?m)^\s*registry\s*=' ) {
377+ Dim " User .npmrc has 'registry=' set: $userNpmrc (will be overridden by --userconfig)"
378+ }
379+ }
380+ } catch {}
381+
382+ # 5. Check current effective registry
383+ $effectiveRegistry = Get-NpmConfigValue " registry"
384+ if (-not [string ]::IsNullOrWhiteSpace($effectiveRegistry )) {
385+ Dim " Current effective registry: $effectiveRegistry "
386+ }
387+
388+ if ($conflicts -eq 0 ) {
389+ Success " No conflicting npm configurations found."
390+ } else {
391+ Write-Host " "
392+ Warn " $conflicts conflict(s) detected — the installer will attempt to override them."
393+ }
394+
395+ return $conflicts
396+ }
397+
294398function Run-NpmViaCmd ([string ]$cmdLine ) {
295399 if ($DryRun ) {
296400 Info (" DRYRUN: " + $cmdLine )
297401 return
298402 }
299403
300- if (-not $EnableLog ) {
301- & cmd.exe / d / s / c $cmdLine
302- if ($LASTEXITCODE -ne 0 ) {
303- throw " npm failed with exit code $LASTEXITCODE . (Tip: set DEEPSTUDIO_LOG=1 to capture full logs.)"
304- }
305- return
306- }
404+ Add-ToLogBuffer " [cmd] $cmdLine "
307405
308- Info " Logging enabled. Log file: $LogPath "
309- Info (" Command via cmd.exe: " + $cmdLine )
406+ if ($EnableLog ) {
407+ Info " Logging enabled. Log file: $LogPath "
408+ }
310409
410+ # Use streaming output so the user sees progress in real-time
311411 $psi = New-Object System.Diagnostics.ProcessStartInfo
312412 $psi.FileName = " cmd.exe"
313413 $psi.Arguments = " /d /s /c " + $cmdLine
@@ -316,38 +416,76 @@ function Run-NpmViaCmd([string]$cmdLine) {
316416 $psi.UseShellExecute = $false
317417 $psi.CreateNoWindow = $true
318418
419+ $stdoutLines = [System.Collections.Generic.List [string ]]::new()
420+ $stderrLines = [System.Collections.Generic.List [string ]]::new()
421+
319422 $p = New-Object System.Diagnostics.Process
320423 $p.StartInfo = $psi
321- [void ]$p.Start ()
424+ $p.EnableRaisingEvents = $true
425+
426+ # Stream stdout line-by-line
427+ $outAction = {
428+ if ($null -ne $EventArgs.Data ) {
429+ $Event.MessageData.OutLines.Add ($EventArgs.Data )
430+ Write-Host $EventArgs.Data
431+ }
432+ }
433+ $errAction = {
434+ if ($null -ne $EventArgs.Data ) {
435+ $Event.MessageData.ErrLines.Add ($EventArgs.Data )
436+ Write-Host $EventArgs.Data - ForegroundColor DarkYellow
437+ }
438+ }
439+
440+ $msgData = [PSCustomObject ]@ { OutLines = $stdoutLines ; ErrLines = $stderrLines }
441+ $outEvent = Register-ObjectEvent - InputObject $p - EventName OutputDataReceived - Action $outAction - MessageData $msgData
442+ $errEvent = Register-ObjectEvent - InputObject $p - EventName ErrorDataReceived - Action $errAction - MessageData $msgData
322443
323- $stdout = $p.StandardOutput.ReadToEnd ()
324- $stderr = $p.StandardError.ReadToEnd ()
444+ [void ]$p.Start ()
445+ $p.BeginOutputReadLine ()
446+ $p.BeginErrorReadLine ()
325447 $p.WaitForExit ()
326448
327- if ($stdout ) { Write-Host $stdout }
328- if ($stderr ) { Write-Host $stderr }
329-
330- $content = @ (
331- " === DeepStudio install log ==="
332- " Time: $ ( Get-Date - Format o) "
333- " Package: $PackageName @latest"
334- " VerboseInstall: $VerboseInstall "
335- " Registry: $ ( $script :ResolvedRegistry ) /"
336- " Command: $cmdLine "
337- " "
338- " ---- STDOUT ----"
339- $stdout
340- " "
341- " ---- STDERR ----"
342- $stderr
343- " "
344- " ExitCode: $ ( $p.ExitCode ) "
345- ) -join " `r`n "
346-
347- Set-Content - Path $LogPath - Value $content - Encoding UTF8
449+ # Give events a moment to flush
450+ Start-Sleep - Milliseconds 200
451+
452+ Unregister-Event - SourceIdentifier $outEvent.Name
453+ Unregister-Event - SourceIdentifier $errEvent.Name
454+
455+ # Feed output into rolling in-memory buffer
456+ foreach ($line in $stdoutLines ) {
457+ if ($line ) { Add-ToLogBuffer $line }
458+ }
459+ foreach ($line in $stderrLines ) {
460+ if ($line ) { Add-ToLogBuffer " [ERR] $line " }
461+ }
462+
463+ # Write to log file if enabled
464+ if ($EnableLog ) {
465+ $content = @ (
466+ " === DeepStudio install log ==="
467+ " Time: $ ( Get-Date - Format o) "
468+ " Package: $PackageName @latest"
469+ " VerboseInstall: $VerboseInstall "
470+ " Registry: $ ( $script :ResolvedRegistry ) /"
471+ " Command: $cmdLine "
472+ " "
473+ " ---- STDOUT ----"
474+ ($stdoutLines -join " `r`n " )
475+ " "
476+ " ---- STDERR ----"
477+ ($stderrLines -join " `r`n " )
478+ " "
479+ " ExitCode: $ ( $p.ExitCode ) "
480+ ) -join " `r`n "
481+
482+ Set-Content - Path $LogPath - Value $content - Encoding UTF8
483+ }
348484
349485 if ($p.ExitCode -ne 0 ) {
350- throw " npm failed with exit code $ ( $p.ExitCode ) . See log: $LogPath "
486+ $errMsg = " npm failed with exit code $ ( $p.ExitCode ) ."
487+ if ($EnableLog ) { $errMsg += " See log: $LogPath " }
488+ throw $errMsg
351489 }
352490}
353491
@@ -471,15 +609,12 @@ Write-Host " └─────────────────────
471609Write-Host " "
472610
473611Show-ProxyDiagnostics
612+ $configConflicts = Show-NpmConfigConflicts
474613
475- # Get registry
614+ # Get registry (always use default; org override only via DEEPSTUDIO_REGISTRY env var)
476615$registryInput = $RegistryFromEnv
477616if ([string ]::IsNullOrWhiteSpace($registryInput )) {
478- Write-Host " 🏢 " - NoNewline - ForegroundColor Cyan
479- $adoOrg = Read-Host " Enter ADO org name [microsoft]"
480- if ([string ]::IsNullOrWhiteSpace($adoOrg )) { $adoOrg = " microsoft" }
481- $registryInput = $DefaultRegistry -replace " microsoft\.pkgs" , " $adoOrg .pkgs"
482- Dim " Using org: $adoOrg "
617+ $registryInput = $DefaultRegistry
483618}
484619$registry = Ensure- Registry $registryInput
485620$script :ResolvedRegistry = $registry
@@ -525,11 +660,22 @@ if ($VerboseInstall) {
525660 $env: NPM_CONFIG_PROGRESS = " false"
526661}
527662
663+ # Save and clear env vars that can override --registry
664+ $script :SavedNpmConfigRegistry = $env: NPM_CONFIG_REGISTRY
665+ $env: NPM_CONFIG_REGISTRY = $null
666+
528667try {
529668 Info " 📄 Preparing isolated npm config..."
530669 Write-IsolatedNpmrc - path $tmpNpmrc - registry $registry - authPrefix $authPrefix - pat $pat
531670
532- $installCmd = ' npm install -g "{0}@latest" --registry "{1}/" --loglevel {2} --userconfig "{3}"' -f $PackageName , $registry , $logLevel , $tmpNpmrc
671+ # --userconfig overrides ~/.npmrc, --globalconfig overrides {prefix}/etc/npmrc
672+ # NUL for globalconfig so npm doesn't see a conflicting global config
673+ # NOTE: --registry is intentionally omitted from the CLI flags.
674+ # It is already set inside the isolated npmrc. Passing it on the CLI as well
675+ # puts the registry at CLI precedence while _authToken stays at userconfig
676+ # precedence, and npm 11 may refuse to send userconfig auth for a CLI-level
677+ # registry. Keeping both in the same npmrc avoids this precedence split.
678+ $installCmd = ' npm install -g "{0}@latest" --loglevel {1} --userconfig "{2}" --globalconfig NUL' -f $PackageName , $logLevel , $tmpNpmrc
533679
534680 if ($VerboseInstall ) {
535681 Dim (" Command: " + $installCmd )
@@ -541,7 +687,7 @@ try {
541687 Run- NpmViaCmd $installCmd
542688
543689 if (-not $DryRun ) {
544- $verifyCmd = ' npm list -g --depth=0 "{0}" --userconfig "{1}" ' -f $PackageName , $tmpNpmrc
690+ $verifyCmd = ' npm list -g --depth=0 "{0}"' -f $PackageName
545691 Run- NpmViaCmd $verifyCmd
546692 }
547693
@@ -554,13 +700,19 @@ try {
554700 Write-Host " 🚀 " - NoNewline - ForegroundColor Magenta
555701 $startChoice = Read-Host " Start $PackageName now? [Y/n]"
556702 if ([string ]::IsNullOrWhiteSpace($startChoice ) -or $startChoice -match ' ^[Yy]' ) {
703+ Write-Host " "
704+ Write-Host " 🏢 " - NoNewline - ForegroundColor Cyan
705+ $adoOrg = Read-Host " Enter your ADO org name [microsoft]"
706+ if ([string ]::IsNullOrWhiteSpace($adoOrg )) { $adoOrg = " microsoft" }
707+ $env: DEEPSTUDIO_ADO_ORG = $adoOrg
708+ Dim " ADO org: $adoOrg "
557709 Write-Host " "
558710 Write-Host " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - ForegroundColor Magenta
559711 Success " Launching $PackageName (press Ctrl+C to stop)..."
560712 Write-Host " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - ForegroundColor Magenta
561713 Write-Host " "
562714 if ($DryRun ) {
563- Dim " DRYRUN: would run $PackageName "
715+ Dim " DRYRUN: would run $PackageName (ADO org: $adoOrg ) "
564716 } else {
565717 & $PackageName
566718 }
@@ -584,6 +736,15 @@ catch {
584736 Dim " • Wrong registry URL"
585737 Dim " • No permission to the Azure Artifacts feed"
586738 Dim " • Corporate proxy/SSL interception issues"
739+ Dim " • npm 11+: built-in npmrc at <npm-prefix>/node_modules/npm/npmrc has conflicting auth"
740+ Dim " (check: npm config get prefix then inspect <prefix>/node_modules/npm/npmrc)"
741+
742+ Write-Host " "
743+ Warn " Common causes for 404:"
744+ Dim " • A project-level .npmrc in the current directory set a different registry"
745+ Dim " • NPM_CONFIG_REGISTRY env var overrode the --registry flag"
746+ Dim " • The ADO feed is missing the npmjs.org upstream source"
747+ Dim " • Try running from a clean directory (e.g. cd $env: TEMP )"
587748
588749 if ($VerboseInstall ) {
589750 Write-Host " "
@@ -599,11 +760,17 @@ catch {
599760 if ($EnableLog -and -not $DryRun ) {
600761 Write-Host " "
601762 Warn " Log saved to: $LogPath "
763+ } else {
764+ Dump- LogBuffer
602765 }
603766
604767 throw
605768}
606769finally {
770+ # Restore env var
771+ if ($script :SavedNpmConfigRegistry ) {
772+ $env: NPM_CONFIG_REGISTRY = $script :SavedNpmConfigRegistry
773+ }
607774 if (-not $DryRun ) {
608775 Remove-Item $tmpNpmrc - Force - ErrorAction SilentlyContinue
609776 }
0 commit comments