diff --git a/setup/Install-EssAdk.Tests.ps1 b/setup/Install-EssAdk.Tests.ps1 index 743b89c66..af7d66ff6 100644 --- a/setup/Install-EssAdk.Tests.ps1 +++ b/setup/Install-EssAdk.Tests.ps1 @@ -34,12 +34,119 @@ Test 'script parses without syntax errors' { } } -Test 'script declares SkipMakerProfile parameter' { +Test 'script declares InstallMode parameter' { + if ($src -notmatch "\[string\]\s*\`$InstallMode\s*=\s*'prompt'") { + throw 'InstallMode parameter with prompt default not found' + } + if ($src -notmatch "ValidateSet\('maker',\s*'developer',\s*'prompt'") { + throw "InstallMode ValidateSet should start with the canonical 'maker','developer','prompt' triple" + } + # Legacy aliases 'lite'/'standard' remain in the ValidateSet so old + # bootstrap-lite invocations and any pinned CI script don't blow up + # under the rename. + if ($src -notmatch "ValidateSet\('maker',\s*'developer',\s*'prompt',\s*'lite',\s*'standard'\)") { + throw "InstallMode ValidateSet should also accept legacy 'lite','standard' values" + } +} + +Test 'script declares SkipMakerProfile parameter (back-compat)' { if ($src -notmatch '\[switch\]\s*\$SkipMakerProfile') { throw 'SkipMakerProfile switch parameter not found' } } +Test 'SkipMakerProfile forces InstallMode developer for back-compat' { + if ($src -notmatch 'if\s*\(\$SkipMakerProfile\)\s*\{\s*\$InstallMode\s*=\s*''developer''\s*\}') { + throw 'SkipMakerProfile back-compat coercion to InstallMode=developer not found' + } +} + +Test 'legacy InstallMode values (lite/standard) are coerced to maker/developer' { + if ($src -notmatch "if\s*\(\`$InstallMode\s+-eq\s+'lite'\)\s*\{\s*\`$InstallMode\s*=\s*'maker'\s*\}") { + throw "legacy 'lite' -> 'maker' coercion missing" + } + if ($src -notmatch "if\s*\(\`$InstallMode\s+-eq\s+'standard'\)\s*\{\s*\`$InstallMode\s*=\s*'developer'\s*\}") { + throw "legacy 'standard' -> 'developer' coercion missing" + } +} + +Test 'InstallMode=prompt fires a Maker/Developer prompt in the installer terminal' { + # The installer resolves the mode in the terminal before handing off + # to VS Code so essMaker.mode is written to settings.json before any + # editor UI appears - the answer never races with the theme picker + # or GitHub Copilot sign-in that VS Code renders on first launch. + if ($src -notmatch "if\s*\(\`$InstallMode\s+-eq\s+'prompt'\)") { + throw 'no prompt branch guarding InstallMode=prompt' + } + if ($src -notmatch 'Read-Host') { throw 'terminal prompt must use Read-Host' } + if ($src -notmatch 'Maker \(recommended\)') { throw 'Maker option label missing from prompt copy' } + if ($src -notmatch '\[2\] Developer') { throw 'Developer option label missing from prompt copy' } +} + +Test 'InstallMode=prompt defaults to maker under non-interactive stdin / CI' { + if ($src -notmatch 'IsInputRedirected') { + throw 'non-interactive detection (Console::IsInputRedirected) missing' + } + if ($src -notmatch '\$env:CI') { throw 'CI env-var non-interactive guard missing' } + if ($src -notmatch 'Defaulting to Maker mode') { + throw 'non-interactive branch should default to Maker mode with a visible message' + } +} + +Test 'Install-EssAdk.ps1 parses under Windows PowerShell 5.1' { + # F-1: the supported Windows path invokes Windows PowerShell 5.1 - if + # anything in the script uses a PS7-only operator (``??``, ``?.``, + # pipeline chain ``||`` / ``&&``, ternary) the 5.1 parser rejects the + # whole file before running a line. Testing under $PSVersionTable + # (which is PS7 in this suite) catches nothing; we have to invoke + # ``powershell.exe`` (5.1) explicitly with -NoProfile -Command and let + # its Language.Parser::ParseFile look at the file. + if (-not $IsWindows) { + # Non-Windows test host: skip (macOS / Linux CI). Windows CI hits + # the parser probe below. Guarding on $IsWindows before touching + # $env:SystemRoot avoids a Join-Path null-Path throw on Linux, + # where SystemRoot does not exist. + return + } + $pwsh5 = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + if (-not (Test-Path $pwsh5)) { + # Windows host without inbox PowerShell 5.1 (unusual): skip. + return + } + $scriptPath = $installerPath + $probe = @' +$errors = $null +$tokens = $null +[System.Management.Automation.Language.Parser]::ParseFile($args[0], [ref]$tokens, [ref]$errors) | Out-Null +if ($errors.Count -gt 0) { + $errors | ForEach-Object { Write-Output $_.Message } + exit 1 +} +exit 0 +'@ + $probeFile = Join-Path $env:TEMP 'ess-adk-ps51-parse-probe.ps1' + Set-Content -LiteralPath $probeFile -Value $probe -Encoding ASCII + try { + $result = & $pwsh5 -NoProfile -ExecutionPolicy Bypass -File $probeFile $scriptPath 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "PS 5.1 parser rejected Install-EssAdk.ps1: $($result -join '; ')" + } + } finally { + Remove-Item -LiteralPath $probeFile -ErrorAction SilentlyContinue + } +} + +Test 'installer answer-normalization is PS 5.1 compatible (no ??)' { + # Direct guard rail for F-1: fail loudly if a future edit reintroduces + # ``??`` in executable code. Strip PowerShell single-line comments + # first so the guard-rail commentary in the script itself does not + # trip this check. + $stripped = ($src -split "`n" | ForEach-Object { ($_ -replace '#.*$', '') }) -join "`n" + if ($stripped -match '\?\?') { + throw 'Install-EssAdk.ps1 must not use PS7-only ``??`` (breaks Windows PowerShell 5.1). Use ``if ($null -eq ...) { ... }`` instead.' + } +} + Test 'script declares SkipLaunch parameter' { if ($src -notmatch '\[switch\]\s*\$SkipLaunch') { throw 'SkipLaunch switch parameter not found' @@ -74,9 +181,12 @@ Test 'non-git directory detection exists' { } } -Test 'extension installs in both modes (writes essMaker.mode setting)' { - if ($src -notmatch "essMaker\.mode.*\`$modeLabel") { - throw 'Mode setting write logic not found' +Test 'extension installs in every mode (writes essMaker.mode setting from resolved $modeLabel)' { + if ($src -notmatch "essMaker\.mode.*\`$settingsModeValue") { + throw 'Mode setting write logic (uses $settingsModeValue) not found' + } + if ($src -notmatch '\$settingsModeValue\s*=\s*\$modeLabel') { + throw '$settingsModeValue should be assigned directly from the CLI-resolved $modeLabel (never "prompt" by this point)' } } @@ -92,9 +202,12 @@ Test 'bootstrap.ps1 parses without errors' { if ($errors.Count -gt 0) { throw "Parse errors: $($errors[0].Message)" } } -Test 'bootstrap.ps1 passes SkipMakerProfile = $true' { - if ($bootstrapSrc -notmatch 'SkipMakerProfile\s*=\s*\$true') { - throw 'SkipMakerProfile not set to $true in standard bootstrap' +Test 'bootstrap.ps1 does not pin SkipMakerProfile (consolidated installer resolves mode in the CLI)' { + if ($bootstrapSrc -match 'SkipMakerProfile\s*=\s*\$true') { + throw 'bootstrap.ps1 should not set SkipMakerProfile = $true after installer consolidation (ADO #7895603)' + } + if ($bootstrapSrc -match "InstallMode\s*=\s*'(maker|developer|lite|standard)'") { + throw "bootstrap.ps1 should not pin InstallMode; it should let the installer default to 'prompt' so VS Code asks the maker." } } @@ -109,11 +222,38 @@ Test 'bootstrap-lite.ps1 parses without errors' { if ($errors.Count -gt 0) { throw "Parse errors: $($errors[0].Message)" } } -Test 'bootstrap-lite.ps1 does NOT pass SkipMakerProfile as an argument' { - # It may mention SkipMakerProfile in a comment, but the actual args hash should not include it +Test 'bootstrap-lite.ps1 passes -InstallMode maker (back-compat shim for legacy URL)' { if ($liteSrc -match 'SkipMakerProfile\s*=\s*\$true') { throw 'bootstrap-lite.ps1 should not set SkipMakerProfile = $true' } + if ($liteSrc -notmatch "InstallMode\s*=\s*'maker'") { + throw "bootstrap-lite.ps1 should pin InstallMode = 'maker' so old links keep landing in maker mode (was 'lite' before the rename)" + } +} + +Write-Host "`nbootstrap-dev.ps1:" -ForegroundColor Cyan + +$devPath = Join-Path $PSScriptRoot 'bootstrap-dev.ps1' +$devSrc = if (Test-Path $devPath) { Get-Content $devPath -Raw } else { '' } + +Test 'bootstrap-dev.ps1 exists' { + if (-not (Test-Path $devPath)) { + throw "bootstrap-dev.ps1 should exist as the shortcut for makers who want the Developer (default VS Code) experience" + } +} + +Test 'bootstrap-dev.ps1 parses without errors' { + if (-not $devSrc) { return } + $tokens = $null; $errors = $null + $null = [System.Management.Automation.Language.Parser]::ParseFile($devPath, [ref]$tokens, [ref]$errors) + if ($errors.Count -gt 0) { throw "Parse errors: $($errors[0].Message)" } +} + +Test 'bootstrap-dev.ps1 passes -InstallMode developer' { + if (-not $devSrc) { throw 'bootstrap-dev.ps1 does not exist' } + if ($devSrc -notmatch "InstallMode\s*=\s*'developer'") { + throw "bootstrap-dev.ps1 should pin InstallMode = 'developer'" + } } # --------------------------------------------------------------------------- @@ -169,9 +309,16 @@ Test 'defines no-op telemetry stubs when the emitter is absent (fail-open)' { if ($src -notmatch 'function Initialize-EssInstallTelemetry') { throw 'no-op stub fallback not found' } } -Test 'derives installer mode (flightcheck | adk | lite)' { +Test 'derives installer mode (flightcheck | adk) - lite consolidated into adk' { if ($src -notmatch "FlightCheckOnly.*'flightcheck'") { throw 'flightcheck mode not derived' } - if ($src -notmatch "SkipMakerProfile.*'adk'") { throw 'adk mode not derived' } + if ($src -notmatch "\}\s*else\s*\{\s*'adk'\s*\}") { throw "post-consolidation installer identity should collapse to 'adk' for non-flightcheck installs" } + if ($src -match "SkipMakerProfile.*'adk'.*else.*'lite'") { throw 'lite installer identity should no longer be derived here (installMode dim carries the distinction)' } +} + +Test 'passes -InstallMode to Initialize-EssInstallTelemetry' { + if ($src -notmatch 'Initialize-EssInstallTelemetry\s+-Installer\s+\$essInstaller\s+-InstallMode\s+\$modeLabel') { + throw '-InstallMode $modeLabel must be threaded into telemetry init' + } } Test 'Write-Step is hooked to emit a per-step telemetry event' { @@ -239,21 +386,60 @@ Test 'envelope time is culture-invariant ISO-8601 (non-colon-separator locales)' [System.Threading.Thread]::CurrentThread.CurrentCulture = $orig } } -Test 'lite installer is not instrumented (no telemetry ready, no events)' { +Test 'installer telemetry emits an installMode dimension' { + Initialize-EssInstallTelemetry -Installer 'adk' -InstallMode 'maker' + try { + $data = Get-EssTelCommonData + if (-not $data.ContainsKey('installMode')) { throw 'installMode dimension not emitted' } + if ($data.installMode -ne 'maker') { throw "installMode wrong: $($data.installMode)" } + } finally { $script:EssTel.Ready = $false; $script:EssTel.Completed = $false } +} + +Test 'installer telemetry defaults installMode to prompt when unspecified' { + Initialize-EssInstallTelemetry -Installer 'adk' + try { + $data = Get-EssTelCommonData + if ($data.installMode -ne 'prompt') { throw "expected default prompt, got: $($data.installMode)" } + } finally { $script:EssTel.Ready = $false; $script:EssTel.Completed = $false } +} + +Test 'installer telemetry accepts legacy lite/standard values (back-compat)' { + # Old bootstrap-lite invocations may still pass -InstallMode lite until + # they resync. The emitter must accept those without validation errors. + Initialize-EssInstallTelemetry -Installer 'adk' -InstallMode 'lite' + try { + if ($script:EssTel.InstallMode -ne 'lite') { throw "legacy 'lite' should be accepted verbatim" } + } finally { $script:EssTel.Ready = $false; $script:EssTel.Completed = $false } + Initialize-EssInstallTelemetry -Installer 'adk' -InstallMode 'standard' + try { + if ($script:EssTel.InstallMode -ne 'standard') { throw "legacy 'standard' should be accepted verbatim" } + } finally { $script:EssTel.Ready = $false; $script:EssTel.Completed = $false } +} + +Test 'maker installer identity is instrumented (post-consolidation)' { + # Regression: pre-consolidation the emitter guarded out Installer='lite' + # because the lite installer was slated for removal. With bootstrap-lite.ps1 + # now a compat shim into the unified installer that passes -InstallMode maker, + # the guard would drop all shim events. It must be gone. $old = $env:ESS_ADK_TELEMETRY try { - $env:ESS_ADK_TELEMETRY = '' # ensure telemetry is otherwise enabled - Initialize-EssInstallTelemetry -Installer 'lite' - if ($script:EssTel.Ready) { throw 'lite installer should not be telemetry-ready' } - } finally { $env:ESS_ADK_TELEMETRY = $old } + $env:ESS_ADK_TELEMETRY = '' + Initialize-EssInstallTelemetry -Installer 'lite' -InstallMode 'maker' + if (-not $script:EssTel.Ready) { throw 'legacy lite installer identity should be telemetry-ready after consolidation' } + } finally { $env:ESS_ADK_TELEMETRY = $old; $script:EssTel.Ready = $false; $script:EssTel.Completed = $false } } -Test 'PowerShell emitter guards out the lite installer' { +Test 'PowerShell emitter no longer guards out the legacy lite installer' { $emitterSrc = Get-Content $psEmitter -Raw - if ($emitterSrc -notmatch "Installer\s+-eq\s+'lite'") { throw 'lite guard missing in PS emitter' } + if ($emitterSrc -match "if\s*\(\`$Installer\s+-eq\s+'lite'\)\s*\{\s*\`$script:EssTel\.Ready\s*=\s*\`$false") { + throw 'PS emitter still guards out lite installer; guard should be removed after consolidation (ADO #7895603)' + } } -Test 'bash emitter guards out the lite installer' { +Test 'bash emitter still guards out the legacy lite installer (macOS scope unchanged in this iteration)' { + # macOS consolidation is out of scope for the current PR. Until a follow-up + # US addresses macOS, bootstrap-lite-mac.sh remains a separate installer + # tagged as ess_tel_installer=lite and the bash emitter still gates it out. $shSrc = Get-Content $shEmitter -Raw - if ($shSrc -notmatch 'ESS_TEL_INSTALLER.*==.*"lite"') { throw 'lite guard missing in bash emitter' } + if ($shSrc -notmatch 'ESS_TEL_INSTALLER.*==.*"lite"') { throw 'lite guard missing in bash emitter (macOS scope not yet migrated)' } } # --- macOS installer + all bootstraps wiring ------------------------------- @@ -272,14 +458,62 @@ Test 'install-ess-adk.sh step() emits a per-step telemetry event' { if ($macInstaller -notmatch 'ess_tel_step' -or $macInstaller -notmatch 'ess_step_key') { throw 'step hook missing' } } -foreach ($bs in @('bootstrap.ps1', 'bootstrap-flightcheck.ps1', 'bootstrap-lite.ps1')) { +Test 'install-ess-adk.sh honors INSTALL_MODE (maker|developer|prompt) with legacy SKIP_MAKER_PROFILE alias' { + if ($macInstaller -notmatch 'INSTALL_MODE=') { throw 'INSTALL_MODE env var not consumed' } + if ($macInstaller -notmatch 'SKIP_MAKER_PROFILE.*==.*"true"[\s\S]{0,120}INSTALL_MODE="developer"') { + throw 'legacy SKIP_MAKER_PROFILE=true should map to INSTALL_MODE=developer' + } + if ($macInstaller -notmatch 'INSTALL_MODE.*==.*"lite"[\s\S]{0,80}INSTALL_MODE="maker"') { + throw "legacy 'lite' should be coerced to 'maker' on macOS" + } + if ($macInstaller -notmatch 'INSTALL_MODE.*==.*"standard"[\s\S]{0,80}INSTALL_MODE="developer"') { + throw "legacy 'standard' should be coerced to 'developer' on macOS" + } + if ($macInstaller -notmatch 'INSTALL_MODE"?\s*==\s*"developer"') { throw 'developer launch branch missing' } + # Maker is now the fall-through `else` branch of the launch block (prompt + # is resolved to maker|developer before any launch code runs), so we + # assert the maker log copy is present instead of the explicit == check. + if ($macInstaller -notmatch 'ESS Maker Profile will run /setup') { throw 'maker launch branch (fall-through else) missing' } +} + +Test 'install-ess-adk.sh INSTALL_MODE=prompt fires a terminal Maker/Developer prompt' { + if ($macInstaller -notmatch 'INSTALL_MODE"?\s*==\s*"prompt"') { throw 'no prompt branch guarding INSTALL_MODE=prompt' } + if ($macInstaller -notmatch 'read -r answer') { throw 'terminal read for maker/developer answer missing' } + if ($macInstaller -notmatch '/dev/tty') { + throw '/dev/tty read fallback required so the prompt works when the script is piped through bash from a curl one-liner' + } + if ($macInstaller -notmatch 'Maker \(recommended\)') { throw 'Maker option label missing from prompt copy' } + if ($macInstaller -notmatch '\[2\] Developer') { throw 'Developer option label missing from prompt copy' } +} + +Test 'install-ess-adk.sh INSTALL_MODE=prompt defaults to maker under non-interactive / CI' { + if ($macInstaller -notmatch '\$\{CI:-\}') { throw 'CI env-var non-interactive guard missing' } + if ($macInstaller -notmatch '! -t 0') { throw 'stdin-is-a-tty non-interactive guard missing' } + if ($macInstaller -notmatch 'Defaulting to Maker mode') { + throw 'non-interactive branch should default to Maker mode with a visible message' + } +} + +Test 'bootstrap-dev-mac.sh exists and pins INSTALL_MODE=developer' { + $devMacPath = Join-Path $PSScriptRoot 'bootstrap-dev-mac.sh' + if (-not (Test-Path $devMacPath)) { throw 'bootstrap-dev-mac.sh missing' } + $devMacSrc = Get-Content $devMacPath -Raw + if ($devMacSrc -notmatch 'INSTALL_MODE="developer"') { throw 'bootstrap-dev-mac.sh should pin INSTALL_MODE=developer' } +} + +Test 'bootstrap-lite-mac.sh pins INSTALL_MODE=maker (back-compat shim)' { + $liteMacSrc = Get-Content (Join-Path $PSScriptRoot 'bootstrap-lite-mac.sh') -Raw + if ($liteMacSrc -notmatch 'INSTALL_MODE="maker"') { throw 'bootstrap-lite-mac.sh should pin INSTALL_MODE=maker so legacy URL still lands in the chat-first experience' } +} + +foreach ($bs in @('bootstrap.ps1', 'bootstrap-flightcheck.ps1', 'bootstrap-lite.ps1', 'bootstrap-dev.ps1')) { $bsSrc = Get-Content (Join-Path $PSScriptRoot $bs) -Raw Test "$bs downloads the telemetry lib and sets ESS_INSTALL_TELEMETRY_LIB" { if ($bsSrc -notmatch 'install-telemetry\.ps1') { throw 'telemetry lib not downloaded' } if ($bsSrc -notmatch 'ESS_INSTALL_TELEMETRY_LIB') { throw 'env var not set' } } } -foreach ($bs in @('bootstrap-mac.sh', 'bootstrap-flightcheck-mac.sh', 'bootstrap-lite-mac.sh')) { +foreach ($bs in @('bootstrap-mac.sh', 'bootstrap-flightcheck-mac.sh', 'bootstrap-lite-mac.sh', 'bootstrap-dev-mac.sh')) { $bsSrc = Get-Content (Join-Path $PSScriptRoot $bs) -Raw Test "$bs downloads the telemetry lib and sets ESS_INSTALL_TELEMETRY_LIB" { if ($bsSrc -notmatch 'install-telemetry\.sh') { throw 'telemetry lib not downloaded' } diff --git a/setup/Install-EssAdk.ps1 b/setup/Install-EssAdk.ps1 index 99b5ff870..3f5e7285a 100644 --- a/setup/Install-EssAdk.ps1 +++ b/setup/Install-EssAdk.ps1 @@ -53,12 +53,21 @@ Prompts for your Dataverse environment URL and creates a minimal .local/config.json so FlightCheck can authenticate without running /setup. +.PARAMETER InstallMode + Selects the VS Code experience: 'maker' (chat-first, hidden developer + chrome - was 'lite'), 'developer' (default VS Code layout with /setup + injection - was 'standard'), or 'prompt' (default: this installer + asks the maker in the terminal, defaulting to 'maker' under a + non-interactive shell). The ESS Maker Profile extension is installed + in every mode; only the layout and /setup delivery differ. Explicit + values are respected without a prompt; 'prompt' is the recommended + default for new customers. The legacy values 'lite' and 'standard' are + still accepted and coerced to 'maker' and 'developer' respectively. + .PARAMETER SkipMakerProfile - Skip installing the bundled "ESS Maker Profile" VS Code extension. The - profile hides developer chrome (file tree, tabs, status bar, etc.) and - drops the user into a chat-first surface tailored to the HR/IT admin - persona. Use this switch to keep the stock VS Code layout - typically - only relevant for developers iterating on the kit itself. + Back-compat switch. Equivalent to -InstallMode developer. Retained so + existing bootstrap.ps1 invocations and CI scripts keep working; new + callers should use -InstallMode instead. .EXAMPLE # Default invocation. May fail on stock Windows due to PowerShell @@ -84,9 +93,74 @@ param( [switch] $SkipLaunch, [switch] $UseDsc, [switch] $FlightCheckOnly, + [ValidateSet('maker', 'developer', 'prompt', 'lite', 'standard')] + [string] $InstallMode = 'prompt', [switch] $SkipMakerProfile ) +# Back-compat: -SkipMakerProfile forces developer mode even when +# -InstallMode is passed. This preserves the old behaviour where the +# switch was the only way to say "no chat-first layout". +if ($SkipMakerProfile) { $InstallMode = 'developer' } + +# Back-compat: legacy value aliases from the pre-rename installer +# (bootstrap-lite.ps1 previously pinned 'lite'; -SkipMakerProfile alias +# previously coerced to 'standard'). Coerce to the new canonical names +# so every downstream reference sees maker|developer|prompt. +if ($InstallMode -eq 'lite') { $InstallMode = 'maker' } +if ($InstallMode -eq 'standard') { $InstallMode = 'developer' } + +# When the caller didn't pin a mode (the default one-liner path via +# bootstrap.ps1), prompt the maker in the terminal for their preference. +# Doing it here in the CLI, before we hand off to VS Code, makes the +# choice deterministic: the answer is applied to essMaker.mode before +# any editor UI appears, so there's no race with the theme picker or +# GitHub Copilot sign-in that VS Code renders on first launch. +if ($InstallMode -eq 'prompt') { + $nonInteractive = $env:CI -or $env:TF_BUILD -or $env:GITHUB_ACTIONS -or [Console]::IsInputRedirected + if ($nonInteractive) { + Write-Host "" + Write-Host "Non-interactive environment detected. Defaulting to Maker mode." -ForegroundColor Yellow + $InstallMode = 'maker' + } else { + Write-Host "" + Write-Host "==> Choose your ESS Maker experience" -ForegroundColor Cyan + Write-Host " [1] Maker (recommended)" + Write-Host " Chat-first layout; hides file tree, tabs, and status bar;" + Write-Host " big-button Quick Actions rail. Best if you mostly work in" + Write-Host " chat and want a focused HR/IT admin surface." + Write-Host "" + Write-Host " [2] Developer" + Write-Host " Default VS Code layout with GitHub Copilot Chat in the" + Write-Host " side panel. Best if you plan to inspect or edit files" + Write-Host " directly." + Write-Host "" + $choice = $null + while ($null -eq $choice) { + $answer = Read-Host "Enter 1 for Maker, 2 for Developer (default: 1)" + # ``$answer ?? ''`` would need PS7's null-coalescing operator, but + # the supported Windows path invokes Windows PowerShell 5.1 - the + # 5.1 parser rejects ``??`` before running a line of the installer. + if ($null -eq $answer) { $answer = '' } + $answer = $answer.Trim() + switch -Regex ($answer) { + '^(1|maker|m|)$' { $choice = 'maker' } + '^(2|developer|dev|d)$' { $choice = 'developer' } + default { Write-Host "Please enter 1 or 2." -ForegroundColor Yellow } + } + } + $InstallMode = $choice + Write-Host " Selected: $InstallMode" -ForegroundColor Green + Write-Host "" + } +} + +# Canonical mode label used throughout this script for both telemetry and +# the VS Code settings write. Kept in $modeLabel so all downstream +# references (5c extension install, launch, telemetry) share one source +# of truth. +$modeLabel = $InstallMode + $ErrorActionPreference = 'Stop' function Write-Step { param([string]$m) Write-Host "`n==> $m" -ForegroundColor Cyan; try { Write-EssInstallStep -Step (Get-EssStepKey $m) } catch {} } @@ -413,8 +487,8 @@ if (-not $essTelLoaded) { function Complete-EssInstallTelemetry { param($Outcome, $ErrorRecord) } } -$essInstaller = if ($FlightCheckOnly) { 'flightcheck' } elseif ($SkipMakerProfile) { 'adk' } else { 'lite' } -Initialize-EssInstallTelemetry -Installer $essInstaller +$essInstaller = if ($FlightCheckOnly) { 'flightcheck' } else { 'adk' } +Initialize-EssInstallTelemetry -Installer $essInstaller -InstallMode $modeLabel try { @@ -964,10 +1038,12 @@ if (-not $FlightCheckOnly) { # Skipped in FlightCheckOnly mode (no VS Code launch) and when the user # passes -SkipExtensions (IT-locked-down boxes that block VSIX installs). if (-not $FlightCheckOnly -and -not $SkipExtensions) { - # Install the ESS Maker Profile extension in both modes. In lite mode it - # applies the chat-first layout; in standard mode it only handles /setup - # injection after the welcome wizard closes (no visual changes). - $modeLabel = if ($SkipMakerProfile) { 'standard' } else { 'lite' } + # Install the ESS Maker Profile extension in every mode. In maker mode + # it applies the chat-first layout; in developer mode it only handles + # /setup injection after the welcome wizard closes (no visual + # changes). $modeLabel is always 'maker' or 'developer' by this + # point - the CLI prompt above resolves 'prompt' before we reach any + # of the install steps. Write-Step "Installing ESS Maker Profile ($modeLabel mode)" $code = Resolve-CodeCommand @@ -1022,12 +1098,13 @@ if (-not $FlightCheckOnly -and -not $SkipExtensions) { } # Write the mode setting so the extension knows whether to apply - # the lite layout or inject /setup (standard mode). + # the maker (chat-first) layout or inject /setup (developer mode). # Uses string manipulation to preserve JSONC comments in settings.json. $settingsDir = Join-Path $env:APPDATA 'Code\User' if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null } $settingsFile = Join-Path $settingsDir 'settings.json' - $modeEntry = "`"essMaker.mode`": `"$modeLabel`"" + $settingsModeValue = $modeLabel + $modeEntry = "`"essMaker.mode`": `"$settingsModeValue`"" if (Test-Path $settingsFile) { $raw = Get-Content $settingsFile -Raw if ($raw -match '"essMaker\.mode"\s*:') { @@ -1404,14 +1481,17 @@ if (-not $SkipLaunch) { $codePath = if ($code.Source) { $code.Source } elseif ($code.FullName) { $code.FullName } else { $null } if ($codePath) { # Launch strategy depends on mode: - # - Lite mode: just open the workspace. The ESS Maker Profile extension + # - Maker mode: just open the workspace. The ESS Maker Profile extension # handles layout + /setup injection after the welcome wizard closes. - # - Standard mode: use `code chat '/setup'` which opens Copilot Chat in + # - Developer mode: use `code chat '/setup'` which opens Copilot Chat in # the sidebar panel on the right (the standard chat experience). + # By the time we get here $modeLabel is always 'maker' or 'developer' + # (the CLI prompt above resolves 'prompt' before we reach any launch + # code), so there is no third fall-through branch to handle. Push-Location $workspace try { - if ($SkipMakerProfile) { - # Standard mode - use code chat to open /setup in sidebar panel + if ($modeLabel -eq 'developer') { + # Developer mode - use code chat to open /setup in sidebar panel Write-Step 'Opening workspace in VS Code and requesting /setup in Copilot Chat' $chatOutput = Invoke-Native { & $codePath chat '/setup' } $chatExit = $LASTEXITCODE @@ -1428,7 +1508,7 @@ if (-not $SkipLaunch) { Write-Host "If /setup does not start after trust/sign-in, open Copilot Chat manually and run /setup." -ForegroundColor Yellow } } else { - # Lite mode - extension handles /setup after welcome wizard + # Maker mode - extension handles /setup after welcome wizard Write-Step 'Opening workspace in VS Code' Start-Process -FilePath $codePath -ArgumentList @('.') | Out-Null Write-Ok "Launched VS Code at $workspace" diff --git a/setup/README.md b/setup/README.md index d70990656..8141ab9e6 100644 --- a/setup/README.md +++ b/setup/README.md @@ -14,32 +14,44 @@ iex (irm https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-mac.sh)" ``` -Once complete, VS Code opens at `solutions/ess-maker-skills/` and `/setup` is automatically requested in Copilot Chat. You'll be prompted to trust the workspace and sign in to GitHub/Copilot — accept these prompts and `/setup` will connect the workspace to an existing editable DA Dev agent. +Once complete, the installer asks in the terminal which experience you want — **Maker** (chat-first, big-button layout — recommended) or **Developer** (default VS Code view with Copilot Chat in the side panel). Under a non-interactive shell (CI, piped input) the installer silently picks Maker. VS Code then opens at `solutions/ess-maker-skills/` and `/setup` is automatically requested in Copilot Chat. You'll be prompted to trust the workspace and sign in to GitHub/Copilot — accept these prompts and `/setup` will connect the workspace to an existing editable DA Dev agent. > **GitHub Copilot subscription is required** for the in-editor maker experience. This script installs the toolchain and extension scaffolding; it does not grant the Copilot entitlement. -## Lite Mode (Chat-First Layout) +## Maker Mode (Chat-First Layout) and Developer Mode -For users who prefer a simplified, chat-first experience that hides developer chrome (file tree, tabs, status bar) and shows a "Quick Actions" button rail: +The one-shot installer (`bootstrap.ps1` / `bootstrap-mac.sh`) asks you to choose between **Maker** and **Developer** in the terminal before VS Code launches, so mode selection is a one-line choice from the main installer — no separate command needed. Maker was previously called "Lite" and Developer was previously called "Standard"; the old names still work for pinned scripts and previously-installed users. + +For scripts and docs that need to pin the choice up front (bypassing the terminal prompt), mode-specific shortcuts are available: **Windows** (PowerShell): ```powershell +# Maker mode (chat-first) iex (irm https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-lite.ps1) + +# Developer mode (default VS Code layout) +iex (irm https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-dev.ps1) ``` **macOS** (Terminal): ```bash +# Maker mode (chat-first) /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-lite-mac.sh)" + +# Developer mode (default VS Code layout) +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-dev-mac.sh)" ``` -This installs everything the standard installer does, plus the **ESS Maker Profile** extension which provides: +> `bootstrap-lite.ps1` / `bootstrap-lite-mac.sh` are kept at their original URLs for back-compat with existing docs and links, and now pin **Maker** mode (the renamed Lite mode). + +Maker mode is the same install as Developer mode plus the **ESS Maker Profile** extension applying: - A chat-only layout with all developer surfaces hidden - Big-button "Quick Actions" rail for common tasks (Connect, Customize landing page, Create, Scan, FlightCheck, Push) - A built-in tutorial explaining each button -You can switch between lite mode and standard VS Code at any time using the toggle buttons in the Quick Actions panel. +You can switch between Maker mode and Developer mode at any time using the toggle buttons in the Quick Actions panel. ## GitHub Codespaces (no local install) @@ -120,11 +132,13 @@ cd ~/source/Employee-Self-Service-Agent-Developer-Kit/solutions/ess-maker-skills |---|---| | `Install-EssAdk.ps1` | Windows orchestrator. Installs toolchain via winget, pip dependencies, clones repo, installs extensions, launches VS Code. With `-FlightCheckOnly`, installs minimal toolchain and runs FlightCheck. | | `install-ess-adk.sh` | macOS orchestrator. Same as above but uses Homebrew. Set `FLIGHTCHECK_ONLY=true` for FlightCheck-only mode. | -| `bootstrap.ps1` | Windows one-liner entry point (standard VS Code layout). | -| `bootstrap-lite.ps1` | Windows one-liner entry point (lite mode — chat-first layout). | +| `bootstrap.ps1` | Windows one-liner entry point (asks Maker vs Developer in the terminal before VS Code launches). | +| `bootstrap-lite.ps1` | Windows one-liner entry point (Maker mode - chat-first layout; previously named "Lite"). | +| `bootstrap-dev.ps1` | Windows one-liner entry point (Developer mode - default VS Code layout; previously named "Standard"). | | `bootstrap-flightcheck.ps1` | Windows one-liner entry point (FlightCheck only). | -| `bootstrap-mac.sh` | macOS one-liner entry point (standard VS Code layout). | -| `bootstrap-lite-mac.sh` | macOS one-liner entry point (lite mode — chat-first layout). | +| `bootstrap-mac.sh` | macOS one-liner entry point (asks Maker vs Developer in the terminal before VS Code launches). | +| `bootstrap-lite-mac.sh` | macOS one-liner entry point (Maker mode - chat-first layout; previously named "Lite"). | +| `bootstrap-dev-mac.sh` | macOS one-liner entry point (Developer mode - default VS Code layout; previously named "Standard"). | | `bootstrap-flightcheck-mac.sh` | macOS one-liner entry point (FlightCheck only). | | `ess-adk-setup.winget.yaml` | Declarative DSC config consumed by `winget configure` (optional Windows path). | | `telemetry/install-telemetry.ps1` | Installer telemetry emitter (PowerShell). Fail-open; emits install start/step/completion to Aria/1DS. | @@ -219,7 +233,9 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -SkipEx powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -SkipClone # skip git clone (toolchain only) powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -SkipLaunch # don't open VS Code at the end powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -FlightCheckOnly # minimal install for FlightCheck only -powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -SkipMakerProfile # standard VS Code (no lite mode) +powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -InstallMode maker # pin Maker mode (chat-first) +powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -InstallMode developer # pin Developer mode (default VS Code layout) +powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-EssAdk.ps1 -SkipMakerProfile # legacy alias: coerced to Developer mode ``` For air-gapped / locked-down environments, IT can mirror the files internally and serve them from an intranet URL by passing `-SourceBaseUrl`. @@ -229,11 +245,17 @@ For air-gapped / locked-down environments, IT can mirror the files internally an From this folder: ```bash -# Full installer: +# Full installer (asks Maker or Developer in the terminal before VS Code launches): bash install-ess-adk.sh -# Full installer with lite mode (chat-first layout): -SKIP_MAKER_PROFILE=false bash install-ess-adk.sh +# Pin Maker mode (chat-first layout): +INSTALL_MODE=maker bash install-ess-adk.sh + +# Pin Developer mode (default VS Code layout): +INSTALL_MODE=developer bash install-ess-adk.sh + +# Legacy alias (still accepted; coerced to INSTALL_MODE=developer): +SKIP_MAKER_PROFILE=true bash install-ess-adk.sh # FlightCheck only: FLIGHTCHECK_ONLY=true bash install-ess-adk.sh diff --git a/setup/bootstrap-dev-mac.sh b/setup/bootstrap-dev-mac.sh new file mode 100644 index 000000000..d4843c979 --- /dev/null +++ b/setup/bootstrap-dev-mac.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# ESS ADK - macOS Bootstrap (Developer Mode) +# +# One-liner entry point: +# /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-dev-mac.sh)" +# +# Installs the full maker kit and lands the maker in the default VS Code +# layout (activity bar, file explorer, status bar visible) with /setup +# injected into Copilot Chat. Shortcut for makers who already know they want +# the Developer experience and want to skip the terminal mode prompt. +# --------------------------------------------------------------------------- +set -euo pipefail + +# Developer mode: pin INSTALL_MODE so install-ess-adk.sh uses `code chat` +# to open /setup in the sidebar panel, skips the terminal mode prompt, and +# does not apply the chat-first layout. +export INSTALL_MODE="developer" + +# Parse optional --branch / --source-base-url arguments +BRANCH="main" +SOURCE_BASE_URL="" +while [[ $# -gt 0 ]]; do + case "$1" in + --branch) BRANCH="$2"; shift 2 ;; + --source-base-url) SOURCE_BASE_URL="$2"; shift 2 ;; + *) shift ;; + esac +done +SOURCE_BASE_URL="${SOURCE_BASE_URL:-${ESS_ADK_SOURCE_URL:-https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/$BRANCH/setup}}" + +TEMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TEMP_DIR"' EXIT + +echo "Fetching ESS ADK installer to $TEMP_DIR" +INSTALLER_URL="$SOURCE_BASE_URL/install-ess-adk.sh" +echo " $INSTALLER_URL" + +if ! curl -fsSL "$INSTALLER_URL" -o "$TEMP_DIR/install-ess-adk.sh"; then + echo " [ERR] Failed to download: $INSTALLER_URL" >&2 + echo " If raw.githubusercontent.com is blocked by your firewall/proxy," >&2 + echo " clone the repo manually and run: INSTALL_MODE=developer setup/install-ess-adk.sh" >&2 + exit 1 +fi + +# Verify the downloaded file looks like a valid script +if [[ ! -s "$TEMP_DIR/install-ess-adk.sh" ]] || ! head -1 "$TEMP_DIR/install-ess-adk.sh" | grep -q '^#!/'; then + echo " [ERR] Downloaded file appears invalid (empty or not a shell script)" >&2 + echo " A corporate proxy may be intercepting the request." >&2 + exit 1 +fi + +# Best-effort: fetch the installer telemetry emitter (fail-open - a telemetry +# download failure must never block the install). +if curl -fsSL "$SOURCE_BASE_URL/telemetry/install-telemetry.sh" -o "$TEMP_DIR/install-telemetry.sh" 2>/dev/null; then + export ESS_INSTALL_TELEMETRY_LIB="$TEMP_DIR/install-telemetry.sh" +fi + +# Run the downloaded installer in a subshell to avoid issues if it calls exit +export ESS_ADK_BRANCH="$BRANCH" +bash "$TEMP_DIR/install-ess-adk.sh" diff --git a/setup/bootstrap-dev.ps1 b/setup/bootstrap-dev.ps1 new file mode 100644 index 000000000..635fbb285 --- /dev/null +++ b/setup/bootstrap-dev.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS + One-liner bootstrap that pins the installer to Developer Mode. + +.DESCRIPTION + Downloads the installer and runs it with -InstallMode developer, giving + the maker the default VS Code layout (activity bar, file explorer, + status bar visible) plus automatic /setup injection into the Copilot + Chat side panel. This is the shortcut for makers who already know they + want the developer experience and want to skip the Maker/Developer + terminal prompt. + + New customers should use bootstrap.ps1, which asks in the terminal + which experience to install and defaults to Maker (chat-first) mode. + + iex (irm https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-dev.ps1) + + All real work happens in Install-EssAdk.ps1; this file just gets the bits + onto the customer's machine and pins the mode to developer. + +.PARAMETER InstallRoot + Forwarded to Install-EssAdk.ps1. See that script for details. + +.PARAMETER Branch + Forwarded to Install-EssAdk.ps1. Defaults to "main". + +.PARAMETER SourceBaseUrl + Where to fetch the installer files from. Defaults to the raw GitHub URL of + the setup folder. Override for testing. +#> + +[CmdletBinding()] +param( + [string] $InstallRoot, + [string] $Branch = 'main', + [string] $SourceBaseUrl +) + +$ErrorActionPreference = 'Stop' +[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +# Derive SourceBaseUrl from -Branch when not explicitly set, so `-Branch ` +# actually pulls the installer bits from that feature branch (not from main). +if (-not $SourceBaseUrl) { + $SourceBaseUrl = "https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/$Branch/setup" +} + +$tempDir = Join-Path $env:TEMP "ess-adk-bootstrap-$([Guid]::NewGuid().ToString('N').Substring(0,8))" +New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + +$files = @( + 'ess-adk-setup.winget.yaml', + 'Install-EssAdk.ps1' +) + +Write-Host "Fetching ESS ADK bootstrap files to $tempDir" -ForegroundColor Cyan +foreach ($f in $files) { + $url = "$SourceBaseUrl/$f" + $dst = Join-Path $tempDir $f + Write-Host " $url" + try { + Invoke-WebRequest -Uri $url -OutFile $dst -UseBasicParsing -TimeoutSec 60 + } catch { + Write-Host " [ERR] Failed to download: $url" -ForegroundColor Red + Write-Host " If raw.githubusercontent.com is blocked by your firewall/proxy," -ForegroundColor Yellow + Write-Host " download the repo manually and run:" -ForegroundColor Yellow + Write-Host " .\setup\Install-EssAdk.ps1 -InstallMode developer" -ForegroundColor Yellow + throw $_ + } +} + +# Best-effort: fetch the installer telemetry emitter (fail-open - a telemetry +# download failure must never block the install). +$telLib = Join-Path $tempDir 'install-telemetry.ps1' +try { + Invoke-WebRequest -Uri "$SourceBaseUrl/telemetry/install-telemetry.ps1" -OutFile $telLib -UseBasicParsing -TimeoutSec 30 + $env:ESS_INSTALL_TELEMETRY_LIB = $telLib +} catch { + Write-Host " [warn] Installer telemetry unavailable (continuing)" -ForegroundColor DarkYellow +} + +$installer = Join-Path $tempDir 'Install-EssAdk.ps1' + +# Run the installer in-memory (as a script block) so execution policy never +# applies - the script content is never "executed from disk". Read as UTF-8 +# explicitly: Windows PowerShell 5.1 otherwise decodes a no-BOM file as ANSI +# (CP1252), which mangles any non-ASCII byte and breaks ScriptBlock parsing. +$scriptContent = [System.IO.File]::ReadAllText($installer, [System.Text.Encoding]::UTF8) +$scriptBlock = [ScriptBlock]::Create($scriptContent) + +# Developer mode: pass -InstallMode developer so the installer's terminal +# mode prompt is skipped and the maker lands directly in the default VS Code +# layout with /setup requested via `code chat`. Same physical installer as +# the other bootstraps; just a different pinned mode. +$installerArgs = @{ Branch = $Branch; InstallMode = 'developer' } +if ($InstallRoot) { $installerArgs.InstallRoot = $InstallRoot } + +& $scriptBlock @installerArgs diff --git a/setup/bootstrap-lite-mac.sh b/setup/bootstrap-lite-mac.sh index e33e0bb93..359f8369a 100644 --- a/setup/bootstrap-lite-mac.sh +++ b/setup/bootstrap-lite-mac.sh @@ -1,16 +1,24 @@ #!/usr/bin/env bash # --------------------------------------------------------------------------- -# ESS ADK — macOS Bootstrap (Lite Mode) +# ESS ADK - macOS Bootstrap (Maker Mode - formerly "Lite Mode") # -# One-liner entry point: +# One-liner entry point (kept as-is for URL back-compat): # /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-lite-mac.sh)" # # Installs the full maker kit with the ESS Maker Profile extension enabled, -# giving users a chat-first, big-button experience. +# giving users a chat-first, big-button experience (Maker mode). This is the +# macOS equivalent of the Windows bootstrap-lite.ps1 compat shim: same +# unified install-ess-adk.sh script, mode pinned to 'maker' up front. # --------------------------------------------------------------------------- set -euo pipefail -# Lite mode: do NOT set SKIP_MAKER_PROFILE so the chat-first profile installs. +# Maker mode: pin INSTALL_MODE so install-ess-adk.sh skips the mode-prompt +# path and lands the maker in the chat-first layout. Legacy telemetry +# gating: the bash emitter still guards out ESS_TEL_INSTALLER=lite until +# macOS consolidation ships, so we opt this shim out of the new 'adk' +# installer identity by exporting the override. +export INSTALL_MODE="maker" +export ESS_TEL_INSTALLER_OVERRIDE="lite" # Parse optional --branch / --source-base-url arguments BRANCH="main" diff --git a/setup/bootstrap-lite.ps1 b/setup/bootstrap-lite.ps1 index 6e52777ae..a58290758 100644 --- a/setup/bootstrap-lite.ps1 +++ b/setup/bootstrap-lite.ps1 @@ -1,19 +1,24 @@ <# .SYNOPSIS - One-liner bootstrap for the ESS Maker Kit in Lite Mode. + Back-compat one-liner bootstrap that pins the installer to Maker Mode + (formerly known as "Lite Mode"). .DESCRIPTION - Downloads the installer script into a temp folder and runs it with the - ESS Maker Profile extension enabled. This gives users a chat-first, - big-button experience that hides developer chrome (file tree, tabs, - status bar, etc.) and surfaces a "Quick Actions" button rail. + Downloads the installer and runs it with -InstallMode maker. Kept so + existing links (docs, blog posts, share sheets) that point at + bootstrap-lite.ps1 keep working after the standard + lite installers + were merged into a single bootstrap.ps1 and the modes were renamed + from lite/standard to maker/developer. - Designed to be invoked from a single command: + New customers should use bootstrap.ps1, which asks in the terminal + which experience to install. This shim is documented as a redirect + only. iex (irm https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup/bootstrap-lite.ps1) All real work happens in Install-EssAdk.ps1; this file just gets the bits - onto the customer's machine and ensures the Maker Profile is installed. + onto the customer's machine and pins the mode to maker (the chat-first + experience). .PARAMETER InstallRoot Forwarded to Install-EssAdk.ps1. See that script for details. @@ -30,12 +35,18 @@ param( [string] $InstallRoot, [string] $Branch = 'main', - [string] $SourceBaseUrl = 'https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup' + [string] $SourceBaseUrl ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +# Derive SourceBaseUrl from -Branch when not explicitly set, so `-Branch ` +# actually pulls the installer bits from that feature branch (not from main). +if (-not $SourceBaseUrl) { + $SourceBaseUrl = "https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/$Branch/setup" +} + $tempDir = Join-Path $env:TEMP "ess-adk-bootstrap-$([Guid]::NewGuid().ToString('N').Substring(0,8))" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null @@ -79,8 +90,11 @@ $installer = Join-Path $tempDir 'Install-EssAdk.ps1' $scriptContent = [System.IO.File]::ReadAllText($installer, [System.Text.Encoding]::UTF8) $scriptBlock = [ScriptBlock]::Create($scriptContent) -# Lite mode: do NOT pass -SkipMakerProfile so the chat-first profile installs. -$installerArgs = @{ Branch = $Branch } +# Maker mode (was "Lite mode" before the rename): pass -InstallMode maker +# so the ESS Maker Profile applies the chat-first layout without asking the +# maker. Kept as a compat shim while the single bootstrap.ps1 becomes the +# recommended entry point. +$installerArgs = @{ Branch = $Branch; InstallMode = 'maker' } if ($InstallRoot) { $installerArgs.InstallRoot = $InstallRoot } & $scriptBlock @installerArgs diff --git a/setup/bootstrap-mac.sh b/setup/bootstrap-mac.sh index 08a0b7e39..bdae27bd9 100644 --- a/setup/bootstrap-mac.sh +++ b/setup/bootstrap-mac.sh @@ -7,8 +7,6 @@ # --------------------------------------------------------------------------- set -euo pipefail -export SKIP_MAKER_PROFILE="true" - # Parse optional --branch / --source-base-url arguments BRANCH="main" SOURCE_BASE_URL="" diff --git a/setup/bootstrap.ps1 b/setup/bootstrap.ps1 index 7763323ad..430d37688 100644 --- a/setup/bootstrap.ps1 +++ b/setup/bootstrap.ps1 @@ -26,12 +26,20 @@ param( [string] $InstallRoot, [string] $Branch = 'main', - [string] $SourceBaseUrl = 'https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/main/setup' + [ValidateSet('maker','developer','prompt','lite','standard','')] + [string] $InstallMode = '', + [string] $SourceBaseUrl ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 +# Derive SourceBaseUrl from -Branch when not explicitly set, so `-Branch ` +# actually pulls the installer bits from that feature branch (not from main). +if (-not $SourceBaseUrl) { + $SourceBaseUrl = "https://raw.githubusercontent.com/microsoft/Employee-Self-Service-Agent-Developer-Kit/$Branch/setup" +} + $tempDir = Join-Path $env:TEMP "ess-adk-bootstrap-$([Guid]::NewGuid().ToString('N').Substring(0,8))" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null @@ -75,7 +83,11 @@ $installer = Join-Path $tempDir 'Install-EssAdk.ps1' $scriptContent = [System.IO.File]::ReadAllText($installer, [System.Text.Encoding]::UTF8) $scriptBlock = [ScriptBlock]::Create($scriptContent) -$installerArgs = @{ Branch = $Branch; SkipMakerProfile = $true } +$installerArgs = @{ Branch = $Branch } +# Forward -InstallMode when the caller pinned one; otherwise leave the +# installer to fall back to its own default (prompt), which asks the +# maker to pick Maker or Developer in the terminal before VS Code launches. +if ($InstallMode) { $installerArgs.InstallMode = $InstallMode } if ($InstallRoot) { $installerArgs.InstallRoot = $InstallRoot } & $scriptBlock @installerArgs diff --git a/setup/install-ess-adk.sh b/setup/install-ess-adk.sh index 6ca8ed0af..302d4c44c 100644 --- a/setup/install-ess-adk.sh +++ b/setup/install-ess-adk.sh @@ -22,7 +22,83 @@ set -euo pipefail BRANCH="${ESS_ADK_BRANCH:-main}" INSTALL_ROOT="${ESS_ADK_INSTALL_ROOT:-$HOME/source}" FLIGHTCHECK_ONLY="${FLIGHTCHECK_ONLY:-false}" +# INSTALL_MODE: maker | developer | prompt (or legacy lite | standard). +# 'maker' (was 'lite') - chat-first layout, /setup after welcome wizard +# 'developer' (was 'standard') - default VS Code layout, /setup via `code chat` +# 'prompt' - ask the maker in the terminal (defaults to +# maker under a non-interactive shell) +# Legacy env var SKIP_MAKER_PROFILE=true is still accepted and, matching the +# Windows -SkipMakerProfile switch's precedence, always coerces to +# INSTALL_MODE=developer even when INSTALL_MODE is also set - so pinned CI +# scripts on either platform get the same answer. +INSTALL_MODE="${INSTALL_MODE:-}" SKIP_MAKER_PROFILE="${SKIP_MAKER_PROFILE:-false}" +if [[ "$SKIP_MAKER_PROFILE" == "true" ]]; then + INSTALL_MODE="developer" +fi +if [[ -z "$INSTALL_MODE" ]]; then + INSTALL_MODE="prompt" +fi +# Coerce legacy names to the new canonical values so every downstream +# reference works with maker|developer|prompt. +if [[ "$INSTALL_MODE" == "lite" ]]; then INSTALL_MODE="maker"; fi +if [[ "$INSTALL_MODE" == "standard" ]]; then INSTALL_MODE="developer"; fi +case "$INSTALL_MODE" in + maker|developer|prompt) ;; + *) + echo "WARNING: unknown INSTALL_MODE '$INSTALL_MODE'; defaulting to prompt" >&2 + INSTALL_MODE="prompt" + ;; +esac + +# When the caller didn't pin a mode (the default one-liner path via +# bootstrap-mac.sh), prompt the maker in the terminal for their preference. +# Doing it here in the CLI, before we hand off to VS Code, makes the +# choice deterministic: the answer is applied to essMaker.mode before +# any editor UI appears, so there's no race with the theme picker or +# GitHub Copilot sign-in that VS Code renders on first launch. +if [[ "$INSTALL_MODE" == "prompt" ]]; then + if [[ -n "${CI:-}" || -n "${TF_BUILD:-}" || -n "${GITHUB_ACTIONS:-}" ]] || [[ ! -t 0 ]]; then + echo "" + echo "Non-interactive environment detected. Defaulting to Maker mode." + INSTALL_MODE="maker" + else + echo "" + echo "==> Choose your ESS Maker experience" + echo " [1] Maker (recommended)" + echo " Chat-first layout; hides file tree, tabs, and status bar;" + echo " big-button Quick Actions rail. Best if you mostly work in" + echo " chat and want a focused HR/IT admin surface." + echo "" + echo " [2] Developer" + echo " Default VS Code layout with GitHub Copilot Chat in the" + echo " side panel. Best if you plan to inspect or edit files" + echo " directly." + echo "" + while true; do + printf "Enter 1 for Maker, 2 for Developer (default: 1): " + # Read from the terminal directly so this works even when the + # bootstrap piped install-ess-adk.sh through bash (stdin is the + # script, not the tty). + if [[ -r /dev/tty ]]; then + read -r answer /dev/null 2>&1; then ess_tel_complete() { :; } fi -# Installer identity: flightcheck | adk (full, maker profile skipped) | lite. +# Installer identity: flightcheck | adk (full, chosen mode carried by +# INSTALL_MODE dimension) | lite (legacy back-compat when the caller pinned +# SKIP_MAKER_PROFILE=false explicitly via the old bootstrap-lite-mac.sh). if [[ "$FLIGHTCHECK_ONLY" == "true" ]]; then _ess_installer=flightcheck -elif [[ "$SKIP_MAKER_PROFILE" == "true" ]]; then - _ess_installer=adk +elif [[ "${ESS_TEL_INSTALLER_OVERRIDE:-}" != "" ]]; then + # Explicit override from the legacy bootstrap-lite-mac.sh so its events + # keep the pre-consolidation `lite` installer tag (the bash emitter still + # gates that identity out until macOS consolidation ships). + _ess_installer="$ESS_TEL_INSTALLER_OVERRIDE" else - _ess_installer=lite + _ess_installer=adk fi -ess_tel_init "$_ess_installer" || true +ess_tel_init "$_ess_installer" "$INSTALL_MODE" || true # Emit a completion event on any exit (success on 0, cancelled on Ctrl+C/term, # failure otherwise). The FlightCheck-only path records success explicitly @@ -393,15 +474,12 @@ if [[ "$FLIGHTCHECK_ONLY" != "true" ]]; then fi done - # ESS Maker Profile — installs in both modes. In lite mode it - # applies the chat-first layout; in standard mode it only handles - # /setup injection (no visual changes). The mode is communicated - # via essMaker.mode in VS Code's user settings.json. - if [[ "$SKIP_MAKER_PROFILE" == "true" ]]; then - MODE_LABEL="standard" - else - MODE_LABEL="lite" - fi + # ESS Maker Profile - installs in every mode. In maker mode it + # applies the chat-first layout; in developer mode it only handles + # /setup injection (no visual changes). By this point MODE_LABEL + # is always 'maker' or 'developer' - the CLI prompt at the top of + # the script resolves 'prompt' before we reach any install step. + MODE_LABEL="$INSTALL_MODE" step "Installing ESS Maker Profile ($MODE_LABEL mode)" MAKER_VSIX_DIR="$REPO_PATH/tools/ess-maker-profile/extension" @@ -424,13 +502,14 @@ if [[ "$FLIGHTCHECK_ONLY" != "true" ]]; then fi # Write the mode setting so the extension knows whether to apply - # the lite layout or inject /setup (standard mode). + # the maker (chat-first) layout or inject /setup (developer mode). SETTINGS_DIR="$HOME/Library/Application Support/Code/User" if [[ "$(uname)" != "Darwin" ]]; then SETTINGS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/Code/User" fi mkdir -p "$SETTINGS_DIR" SETTINGS_FILE="$SETTINGS_DIR/settings.json" + SETTINGS_MODE_VALUE="$MODE_LABEL" if [[ -f "$SETTINGS_FILE" ]]; then # Merge into existing settings using Python (available from step 2) python3 -c " @@ -440,12 +519,12 @@ try: settings = json.load(f) except: settings = {} -settings['essMaker.mode'] = '$MODE_LABEL' +settings['essMaker.mode'] = '$SETTINGS_MODE_VALUE' with open('$SETTINGS_FILE', 'w') as f: json.dump(settings, f, indent=2) " 2>/dev/null || true else - echo "{\"essMaker.mode\": \"$MODE_LABEL\"}" > "$SETTINGS_FILE" + echo "{\"essMaker.mode\": \"$SETTINGS_MODE_VALUE\"}" > "$SETTINGS_FILE" fi else warn "VS Code 'code' CLI not found. Install extensions manually after launching VS Code." @@ -690,11 +769,14 @@ fi # --------------------------------------------------------------------------- if [[ -n "$CODE_CMD" ]]; then # Launch strategy depends on mode: - # - Standard mode (SKIP_MAKER_PROFILE=true): use `code chat` to open - # /setup in the sidebar panel (the standard chat experience). - # - Lite mode: just open the workspace. The ESS Maker Profile extension + # - Developer mode: use `code chat` to open /setup in the sidebar panel + # (the default Copilot Chat experience). + # - Maker mode: just open the workspace. The ESS Maker Profile extension # handles layout + /setup injection after the welcome wizard closes. - if [[ "$SKIP_MAKER_PROFILE" == "true" ]]; then + # By this point INSTALL_MODE is always 'maker' or 'developer' - the CLI + # prompt at the top of the script resolves 'prompt' before we reach any + # launch code. + if [[ "$INSTALL_MODE" == "developer" ]]; then step "Opening workspace in VS Code and requesting /setup in Copilot Chat" if (cd "$WORKSPACE_PATH" && "$CODE_CMD" chat "/setup"); then ok "Requested /setup in Copilot Chat at $WORKSPACE_PATH" diff --git a/setup/telemetry/install-telemetry.ps1 b/setup/telemetry/install-telemetry.ps1 index fad04cace..047443bfd 100644 --- a/setup/telemetry/install-telemetry.ps1 +++ b/setup/telemetry/install-telemetry.ps1 @@ -48,6 +48,7 @@ $script:EssTelSchemaVersion = '1.0' $script:EssTel = @{ Ready = $false Installer = 'adk' + InstallMode = 'prompt' Env = 'prod' IKey = '' InstanceId = '' @@ -208,6 +209,7 @@ function Get-EssTelCommonData { schemaVersion = $script:EssTelSchemaVersion env = $script:EssTel.Env installer = $script:EssTel.Installer + installMode = $script:EssTel.InstallMode invocationSource = 'installer' platform = $script:EssTel.Platform os = $script:EssTel.OsVersion @@ -220,17 +222,22 @@ function Get-EssTelCommonData { function Initialize-EssInstallTelemetry { <# .SYNOPSIS Begin installer telemetry: notice, identity, and the start event. - .PARAMETER Installer One of adk | lite | flightcheck. + .PARAMETER Installer One of adk | lite | flightcheck. 'lite' is retained + for the back-compat bootstrap-lite.ps1 shim; new callers should pass + 'adk' with -InstallMode maker instead. + .PARAMETER InstallMode The VS Code experience the maker will land in + after the installer completes: maker | developer | prompt (or the + legacy aliases 'lite' | 'standard'). 'prompt' means the installer + will ask in the terminal before VS Code launches. #> param( [ValidateSet('adk', 'lite', 'flightcheck')] - [string]$Installer = 'adk' + [string]$Installer = 'adk', + [ValidateSet('maker', 'developer', 'prompt', 'lite', 'standard')] + [string]$InstallMode = 'prompt' ) try { if (-not (Test-EssTelemetryEnabled)) { $script:EssTel.Ready = $false; return } - # The Lite-mode installer is being merged into the standard ADK installer - # (mode will become an onboarding prompt), so it is no longer instrumented. - if ($Installer -eq 'lite') { $script:EssTel.Ready = $false; return } Initialize-EssTelConfig $envName = "$env:ESS_ADK_ARIA_ENV".Trim().ToLowerInvariant() @@ -241,6 +248,7 @@ function Initialize-EssInstallTelemetry { $adkVer = "$env:ESS_ADK_VERSION".Trim(); if (-not $adkVer) { $adkVer = 'unknown' } $script:EssTel.Installer = $Installer + $script:EssTel.InstallMode = $InstallMode $script:EssTel.Env = $envName $script:EssTel.IKey = $script:EssTelIKeys[$envName] $script:EssTel.InstanceId = $inst.Id diff --git a/setup/telemetry/install-telemetry.sh b/setup/telemetry/install-telemetry.sh index 95606ea93..5b61fb731 100644 --- a/setup/telemetry/install-telemetry.sh +++ b/setup/telemetry/install-telemetry.sh @@ -135,7 +135,7 @@ ess_tel_send() { ms=$(( (10#$ns / 1000000) % 1000 )) ts="$(date -u +%Y-%m-%dT%H:%M:%S).$(printf '%03d' "$ms")Z" local data - data="{\"schemaVersion\":\"$ESS_TEL_SCHEMA\",\"env\":\"$ESS_TEL_ENV\",\"installer\":\"$ESS_TEL_INSTALLER\",\"invocationSource\":\"installer\",\"platform\":\"$ESS_TEL_PLATFORM\",\"os\":\"$(_ess_tel_jesc "$ESS_TEL_OS")\",\"instanceId\":\"$ESS_TEL_INSTANCE\",\"adkVersion\":\"$(_ess_tel_jesc "$ESS_TEL_ADKVER")\",\"firstRun\":$ESS_TEL_FIRSTRUN$extra}" + data="{\"schemaVersion\":\"$ESS_TEL_SCHEMA\",\"env\":\"$ESS_TEL_ENV\",\"installer\":\"$ESS_TEL_INSTALLER\",\"installMode\":\"$ESS_TEL_INSTALL_MODE\",\"invocationSource\":\"installer\",\"platform\":\"$ESS_TEL_PLATFORM\",\"os\":\"$(_ess_tel_jesc "$ESS_TEL_OS")\",\"instanceId\":\"$ESS_TEL_INSTANCE\",\"adkVersion\":\"$(_ess_tel_jesc "$ESS_TEL_ADKVER")\",\"firstRun\":$ESS_TEL_FIRSTRUN$extra}" local body="{\"ver\":\"4.0\",\"name\":\"$name\",\"time\":\"$ts\",\"iKey\":\"$envtoken\",\"data\":$data}" local uploadms; uploadms="$(( $(date +%s) * 1000 ))" # Circuit breaker: an unreachable/slow collector must not add its timeout to @@ -160,10 +160,16 @@ ess_tel_send() { # --- public API ------------------------------------------------------------ ess_tel_init() { # $1 = installer (adk|lite|flightcheck) + # $2 = install mode (maker|developer|prompt; or legacy lite|standard). + # Defaults to 'prompt' to match the Windows installer contract. ESS_TEL_INSTALLER="${1:-adk}" + ESS_TEL_INSTALL_MODE="${2:-prompt}" ess_tel_enabled || { ESS_TEL_READY=0; return 0; } - # The Lite-mode installer is being merged into the standard ADK installer - # (mode will become an onboarding prompt), so it is no longer instrumented. + # The legacy 'lite' installer identity (bootstrap-lite-mac.sh sets it + # explicitly via ESS_TEL_INSTALLER_OVERRIDE) is not instrumented yet; + # macOS consolidation is deferred to a follow-up US. The unified + # installer identity is 'adk' with the maker/developer/prompt mode + # carried by the installMode dimension. [[ "$ESS_TEL_INSTALLER" == "lite" ]] && { ESS_TEL_READY=0; return 0; } ess_tel_ensure_config local envname diff --git a/tools/ess-maker-profile/extension/CHANGELOG.md b/tools/ess-maker-profile/extension/CHANGELOG.md index 1fe4343b8..479a97e41 100644 --- a/tools/ess-maker-profile/extension/CHANGELOG.md +++ b/tools/ess-maker-profile/extension/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## 0.4.28 (POC) + +- **Mode prompt lives in the installer CLI.** The consolidated installer + (`setup/Install-EssAdk.ps1` on Windows, `setup/install-ess-adk.sh` on + macOS) asks the Maker/Developer question in the terminal before it + launches VS Code, and writes the resolved mode to `essMaker.mode` in + `settings.json`. The extension trusts that value: blank / `prompt` + values silently default to Maker on first activation, so a stray VS + Code window that never went through the installer still lands + deterministically without any first-launch modal. + +## 0.4.27 (POC) + +- **Modes renamed: Lite -> Maker (default), Standard -> Developer.** + User research showed "Lite" undersold the mode (it is a full, + streamlined chat-first UX, not a cut-down one) and "Standard" was not + really standard (it still opens Copilot Chat and runs `/setup` on + first launch). A new `setup/bootstrap-dev.ps1` shortcut mirrors the + existing `bootstrap-lite.ps1`. The `essMaker.mode` setting accepts + the new values (`maker`, `developer`) and still accepts the legacy + values (`lite`, `standard`) so pinned CI, docs, and previously- + installed users are not disrupted; legacy values are normalized on + read at the top of `firstInstallDispatch`. The + `essMaker.restoreStandardLayout` command ID is preserved (its + user-facing title is now "Restore Developer Layout"). The persisted + on-disk key `essMaker.liteMode.v1` is likewise preserved so existing + users do not lose their layout preference. macOS gets a matching + `bootstrap-dev-mac.sh` shortcut; `bootstrap-lite-mac.sh` continues + to work and pins Maker mode. The installer emits an additional + `installMode` telemetry dimension (maker | developer | prompt, or + legacy lite | standard). + +## 0.4.26 (POC) + +- **Windows installer consolidation.** One `bootstrap.ps1` drives both + experiences (see ADO #7895603) and forwards the chosen mode into the + extension via the `essMaker.mode` global setting so subsequent + launches skip any prompt. Legacy invocations that pin + `essMaker.mode` to `lite` or `standard` (including the back-compat + `bootstrap-lite.ps1` shim) bypass the terminal prompt. + ## 0.4.25 (POC) - **Customize landing page** is available in Quick Actions. The setup-gated action opens a guided Copilot chat for branding, quick links, starter prompts, and insight cards. diff --git a/tools/ess-maker-profile/extension/ess-maker-profile-0.4.25.vsix b/tools/ess-maker-profile/extension/ess-maker-profile-0.4.25.vsix deleted file mode 100644 index 8cf62342d..000000000 Binary files a/tools/ess-maker-profile/extension/ess-maker-profile-0.4.25.vsix and /dev/null differ diff --git a/tools/ess-maker-profile/extension/ess-maker-profile-0.4.28.vsix b/tools/ess-maker-profile/extension/ess-maker-profile-0.4.28.vsix new file mode 100644 index 000000000..9da05f9d4 Binary files /dev/null and b/tools/ess-maker-profile/extension/ess-maker-profile-0.4.28.vsix differ diff --git a/tools/ess-maker-profile/extension/extension.js b/tools/ess-maker-profile/extension/extension.js index a8a244db7..ee36320d5 100644 --- a/tools/ess-maker-profile/extension/extension.js +++ b/tools/ess-maker-profile/extension/extension.js @@ -19,6 +19,10 @@ function _log(msg) { const EXT_ID = 'microsoft-ess.ess-maker-profile'; const APPLIED_KEY = 'essMaker.chatOnlyApplied.v7'; +// Historical key name: stores whether the user wants the chat-only ("Maker +// mode") layout applied on activation. Kept as-is on disk (essMaker.liteMode.v1) +// to preserve existing users' persisted preference across the lite -> maker +// rename; renaming the storage key would silently reset everyone to the default. const LITE_MODE_KEY = 'essMaker.liteMode.v1'; const SETTINGS_BACKUP_KEY = 'essMaker.settingsBackup.v1'; @@ -216,7 +220,7 @@ function startPrereqWatcher(context) { const interval = setInterval(refresh, 10000); // Register disposables - context.subscriptions.push(stateWatcher, configWatcher, flightcheckWatcher, { + context.subscriptions.push(stateWatcher, flightcheckWatcher, { dispose: () => clearInterval(interval) }); @@ -255,7 +259,7 @@ async function tryRun(commandId, ...args) { catch (err) { console.warn(`[ess-maker] ${commandId} failed:`, err.message); return false; } } -function isLiteMode() { +function isMakerLayout() { const cfg = vscode.workspace.getConfiguration(); return cfg.get('workbench.activityBar.location') === 'hidden'; } @@ -821,7 +825,7 @@ async function restoreStandardLayout() { } const sel = await vscode.window.showInformationMessage( - 'ESS Maker: standard layout restored. Reload the window to see all changes.', + 'ESS Maker: Developer layout restored. Reload the window to see all changes.', 'Reload Window' ); if (sel === 'Reload Window') { @@ -894,7 +898,7 @@ class ActionsViewProvider { await tryRun('workbench.files.action.expandRecursively'); await this.refresh(); const sel = await vscode.window.showInformationMessage( - 'Standard layout restored. Reload the window for full effect.', + 'Developer layout restored. Reload the window for full effect.', 'Reload Window' ); if (sel === 'Reload Window') { @@ -905,7 +909,7 @@ class ActionsViewProvider { await applySettings(CHAT_ONLY_LAYOUT, vscode.ConfigurationTarget.Global); await this.refresh(); const sel = await vscode.window.showInformationMessage( - 'Lite mode applied. Reload the window for full effect.', + 'Maker mode applied. Reload the window for full effect.', 'Reload Window' ); if (sel === 'Reload Window') { @@ -937,9 +941,9 @@ class ActionsViewProvider { for (const a of ACTIONS) { states[a.id] = actionState(a, completed); } - const liteMode = isLiteMode(); + const makerLayout = isMakerLayout(); try { - await this._view.webview.postMessage({ type: 'state', states, liteMode }); + await this._view.webview.postMessage({ type: 'state', states, makerLayout }); } catch {} } @@ -1042,10 +1046,10 @@ class ActionsViewProvider {

Customize your ESS agent

${buttons}
- @@ -1063,10 +1067,10 @@ class ActionsViewProvider {
How each button works
- @@ -1116,14 +1120,14 @@ class ActionsViewProvider { } } // Show/hide mode-toggle buttons based on current layout. - const btnLite = document.getElementById('btn-lite'); - const btnStandard = document.getElementById('btn-standard'); - if (e.data.liteMode) { - btnLite.style.display = 'none'; - btnStandard.style.display = ''; + const btnMaker = document.getElementById('btn-maker'); + const btnDeveloper = document.getElementById('btn-developer'); + if (e.data.makerLayout) { + btnMaker.style.display = 'none'; + btnDeveloper.style.display = ''; } else { - btnLite.style.display = ''; - btnStandard.style.display = 'none'; + btnMaker.style.display = ''; + btnDeveloper.style.display = 'none'; } }); vscode.postMessage({ type: 'ready' }); @@ -1425,6 +1429,98 @@ async function maybePromptReinstall(repoRoot) { } } +// --- First-install dispatch (ADO #7895603 consolidated installer) --------- +// The consolidated installer (setup/Install-EssAdk.ps1 / install-ess-adk.sh) +// prompts the maker for maker vs developer in the terminal BEFORE VS Code +// launches, and writes the resolved mode to essMaker.mode in settings.json. +// So by the time this extension activates, essMaker.mode is always one of +// 'maker' | 'developer' | 'lite' (legacy) | 'standard' (legacy). +// +// If the installer left the value blank ('' or 'prompt') - e.g. a maker +// double-clicked the extension into a stray VS Code window without +// running the installer, or an older non-interactive install flow slipped +// through - we default to 'maker' silently rather than pop a modal on +// first launch. First-launch modals reliably lose the race against the +// theme picker and Copilot sign-in prompts and are never seen by makers. +// The mode can always be changed later via the Quick Actions toggle or +// the essMaker.mode setting. +// +// Legacy value migration: the pre-rename installer wrote 'lite'/'standard' +// to essMaker.mode. Reads here normalize those to the new 'maker'/'developer' +// values so existing users keep the mode they picked. + +// Normalize legacy essMaker.mode values ('lite', 'standard') written by +// the pre-rename installer to the new canonical names, so existing users +// don't get re-prompted after upgrading the extension. +function normalizeInstallerMode(mode) { + if (mode === 'lite') return 'maker'; + if (mode === 'standard') return 'developer'; + return mode; +} + +async function firstInstallDispatch(context, installerMode) { + let effectiveMode = normalizeInstallerMode(installerMode); + if (!effectiveMode || effectiveMode === 'prompt') { + // Installer didn't resolve a mode (blank or literal 'prompt'). Fall + // back to maker silently and persist so we don't fall through here + // on every activation. + _log(`firstInstallDispatch: installer mode was "${installerMode}"; defaulting to maker`); + effectiveMode = 'maker'; + try { + await vscode.workspace.getConfiguration().update( + 'essMaker.mode', + effectiveMode, + vscode.ConfigurationTarget.Global, + ); + } catch (err) { + _log(`firstInstallDispatch: failed to persist essMaker.mode: ${err && err.message}`); + } + } + const isDeveloperMode = effectiveMode === 'developer'; + _log(`firstInstallDispatch: effectiveMode=${effectiveMode}, isDeveloperMode=${isDeveloperMode}`); + context.globalState.update(LITE_MODE_KEY, !isDeveloperMode); + + // Check if the user already has a config file (returning user who + // re-ran the installer). Skip /setup if already configured. + let alreadyConfigured = false; + try { + const met = await checkPrerequisites(); + alreadyConfigured = met.has('setup'); + } catch (err) { + _log(`firstInstallDispatch: checkPrerequisites error: ${err && err.message}`); + } + _log(`firstInstallDispatch: alreadyConfigured=${alreadyConfigured}`); + + if (isDeveloperMode) { + // Developer mode: no layout changes. The installer already dispatched + // ``code chat "/setup"`` before launching VS Code (Install-EssAdk.ps1 + // and install-ess-adk.sh both do this for the developer branch), so + // the extension deliberately does NOT inject /setup again here - + // doing so would open two /setup chats on the fresh-install path. + context.globalState.update(APPLIED_KEY, true); + _log(`firstInstallDispatch: developer mode, alreadyConfigured=${alreadyConfigured} - installer owns /setup dispatch, extension no-ops`); + return; + } + + // Maker mode: apply layout. + applyChatOnlyLayout({ silent: false }) + .then(() => context.globalState.update(APPLIED_KEY, true)) + .catch(() => {}); + if (alreadyConfigured) { + _log('firstInstallDispatch: skipping /setup (already configured), opening chat'); + setTimeout(() => tryRun('workbench.action.chat.open').catch(() => {}), 3000); + } else { + waitForWelcomeWizard() + .then(() => { _log('firstInstallDispatch: wizard done (maker), waiting 3s...'); return new Promise(r => setTimeout(r, 3000)); }) + .then(() => { _log('firstInstallDispatch: calling injectSetup (maker)'); return injectSetup(); }) + .then(() => _log('firstInstallDispatch: injectSetup completed (maker)')) + .catch((err) => { + _log(`firstInstallDispatch: ERROR in maker wizard chain: ${err && err.message}`); + console.warn('[ess-maker] Welcome wizard wait timed out, skipping auto /setup'); + }); + } +} + function activate(context) { _extensionContext = context; _log(`activate: ENTRY. workspaceFolders=${JSON.stringify(vscode.workspace.workspaceFolders?.map(f => f.uri.fsPath))}`); @@ -1471,64 +1567,37 @@ function activate(context) { startPrereqWatcher(context); // First-run vs subsequent runs: - // - First run: determine mode (lite vs standard) from VS Code setting - // written by the installer. - // Lite mode: applies chat-only layout; user clicks Setup to run /setup. - // Standard mode: injects /setup into Copilot Chat automatically. - // - Subsequent lite activations: silently re-apply layout. + // - First run: determine mode (maker vs developer) from VS Code setting + // written by the installer. Legacy values 'lite'/'standard' are + // normalized to 'maker'/'developer' in firstInstallDispatch. + // Maker mode: applies chat-only layout; user clicks Setup to run /setup. + // Developer mode: the installer already dispatched /setup before VS Code + // launched, so the extension only records the mode and does not inject + // a second /setup here. + // Empty ("") / "prompt": installer left the choice unresolved (e.g. a + // maker double-clicked the extension into a stray VS Code window + // without running the consolidated installer). firstInstallDispatch + // silently defaults to maker; we never show a first-launch modal + // because that surface reliably loses the race against the theme + // picker and Copilot sign-in. + // - Subsequent maker-mode activations: silently re-apply layout. const alreadyApplied = context.globalState.get(APPLIED_KEY, false); - const userWantsLite = context.globalState.get(LITE_MODE_KEY, true); // default to lite + const userWantsMakerLayout = context.globalState.get(LITE_MODE_KEY, true); // default to maker layout const installerMode = vscode.workspace.getConfiguration().get('essMaker.mode', ''); - _log(`activate: alreadyApplied=${alreadyApplied}, userWantsLite=${userWantsLite}, installerMode="${installerMode}", workspaceFolders=${vscode.workspace.workspaceFolders?.length || 0}`); + _log(`activate: alreadyApplied=${alreadyApplied}, userWantsMakerLayout=${userWantsMakerLayout}, installerMode="${installerMode}", workspaceFolders=${vscode.workspace.workspaceFolders?.length || 0}`); if (vscode.workspace.workspaceFolders?.length) { if (!alreadyApplied) { - // First install. Check installer-provided mode setting. - const isStandardMode = installerMode === 'standard'; - _log(`activate: first install, isStandardMode=${isStandardMode}`); - context.globalState.update(LITE_MODE_KEY, !isStandardMode); - - // Check if the user already has a config file (returning user - // who re-ran the installer). Skip /setup if already configured. - checkPrerequisites().then(met => { - const alreadyConfigured = met.has('setup'); - _log(`activate: alreadyConfigured=${alreadyConfigured}`); - - if (isStandardMode) { - // Standard mode: no layout changes. The installer handles - // /setup injection via `code chat` which opens in the - // sidebar panel. Nothing to do here. - context.globalState.update(APPLIED_KEY, true); - _log('activate: standard mode — installer handles /setup via code chat'); - } else { - // Lite mode: apply layout. - applyChatOnlyLayout({ silent: false }) - .then(() => context.globalState.update(APPLIED_KEY, true)) - .catch(() => {}); - if (alreadyConfigured) { - _log('activate: skipping /setup (already configured), opening chat'); - // Returning user in lite mode — just open the chat panel - // so they can start working right away. - setTimeout(() => tryRun('workbench.action.chat.open').catch(() => {}), 3000); - } else { - // Wait for welcome wizard to finish, then inject /setup. - waitForWelcomeWizard() - .then(() => { _log('activate: wizard done (lite), waiting 3s...'); return new Promise(r => setTimeout(r, 3000)); }) - .then(() => { _log('activate: calling injectSetup (lite)'); return injectSetup(); }) - .then(() => _log('activate: injectSetup completed (lite)')) - .catch((err) => { - _log(`activate: ERROR in lite wizard chain: ${err && err.message}`); - console.warn('[ess-maker] Welcome wizard wait timed out, skipping auto /setup'); - }); - } - } - }).catch(err => _log(`activate: checkPrerequisites error: ${err && err.message}`)); - } else if (userWantsLite) { - // Subsequent lite mode launch: silently re-apply layout. + // First install. Silently resolve mode (defaulting to maker if + // the installer left essMaker.mode blank/"prompt"), then dispatch. + firstInstallDispatch(context, installerMode) + .catch(err => _log(`activate: firstInstallDispatch error: ${err && err.message}`)); + } else if (userWantsMakerLayout) { + // Subsequent maker-mode launch: silently re-apply layout. setTimeout(() => { applyChatOnlyLayout({ silent: true }).catch(() => {}); }, 1500); } - // If userWantsLite is false (standard mode), skip re-applying. + // If userWantsMakerLayout is false (developer mode), skip re-applying. // Auto-update nudge (ADO 7569528 / 7569530): check whether the local // clone is behind origin/main and, if so, offer a one-click pull. diff --git a/tools/ess-maker-profile/extension/extension.test.js b/tools/ess-maker-profile/extension/extension.test.js index 858e93a13..e82081823 100644 --- a/tools/ess-maker-profile/extension/extension.test.js +++ b/tools/ess-maker-profile/extension/extension.test.js @@ -200,6 +200,57 @@ test('exposes the essMaker.autoUpdateCheck opt-out setting', () => { assert.strictEqual(prop.default, true); }); +console.log('\nfirst-install mode dispatch (ADO #7895603):'); + +test('extension does not prompt for mode inside VS Code', () => { + // The mode is resolved in the installer CLI before VS Code launches + // (setup/Install-EssAdk.ps1 + install-ess-adk.sh prompt there), so + // essMaker.mode is already written to settings.json by the time the + // extension activates. If the extension prompted here on first + // launch, its picker would race the theme picker + Copilot sign-in + // that VS Code renders on first launch, so we guard against a mode + // picker regressing into the extension. + assert.ok(!/async function promptForInstallMode/.test(src), 'promptForInstallMode should not exist; the installer prompts in the CLI'); + assert.ok(!/showQuickPick\([\s\S]{0,200}?Maker \(recommended\)/.test(src), 'in-VS-Code Maker/Developer picker should not exist'); +}); + +test('firstInstallDispatch defaults blank / "prompt" installer values to maker', () => { + assert.ok(/if\s*\(!effectiveMode \|\| effectiveMode === 'prompt'\)/.test(src), 'fallback branch should still guard blank / prompt'); + assert.ok(/effectiveMode = 'maker'/.test(src), 'fallback should default to maker without a modal'); +}); + +test('firstInstallDispatch persists the resolved mode to global settings', () => { + assert.ok(/'essMaker\.mode',[\s\S]*?ConfigurationTarget\.Global/.test(src), 'resolved mode should be persisted with ConfigurationTarget.Global'); +}); + +test('developer mode does NOT inject /setup from the extension (installer owns dispatch)', () => { + // F-3 regression guard: the installer already runs ``code chat "/setup"`` + // for developer mode before launching VS Code, so injecting again in + // the extension opens two /setup chats on the fresh-install path. + // The isDeveloperMode branch of firstInstallDispatch must therefore + // not call injectSetup / waitForWelcomeWizard. + const devBranch = src.match(/if\s*\(isDeveloperMode\)\s*\{([\s\S]*?)\n\s*return;\s*\n\s*\}/); + assert.ok(devBranch, 'isDeveloperMode branch not found in firstInstallDispatch'); + assert.ok(!/injectSetup\s*\(/.test(devBranch[1]), 'developer branch must not call injectSetup - installer owns /setup dispatch'); + assert.ok(!/waitForWelcomeWizard\s*\(/.test(devBranch[1]), 'developer branch must not wait for welcome wizard to inject /setup'); +}); + +test('legacy essMaker.mode values are normalized (lite -> maker, standard -> developer)', () => { + assert.ok(/function normalizeInstallerMode/.test(src), 'normalizeInstallerMode helper missing'); + assert.ok(/if\s*\(mode === 'lite'\)\s*return 'maker'/.test(src), "'lite' should be normalized to 'maker'"); + assert.ok(/if\s*\(mode === 'standard'\)\s*return 'developer'/.test(src), "'standard' should be normalized to 'developer'"); + assert.ok(/normalizeInstallerMode\(installerMode\)/.test(src), 'firstInstallDispatch should normalize before checking fallback'); +}); + +test('essMaker.mode config schema accepts the new maker/developer values', () => { + const modeProp = pkg.contributes.configuration.properties['essMaker.mode']; + assert.ok(modeProp, 'essMaker.mode missing from configuration'); + assert.ok(modeProp.enum.includes('maker'), "enum should include 'maker'"); + assert.ok(modeProp.enum.includes('developer'), "enum should include 'developer'"); + assert.ok(modeProp.enum.includes('lite'), "enum should still include legacy 'lite'"); + assert.ok(modeProp.enum.includes('standard'), "enum should still include legacy 'standard'"); +}); + console.log('\nauto-update: parseLsRemoteSha:'); test('extracts sha from a ls-remote line', () => { diff --git a/tools/ess-maker-profile/extension/package.json b/tools/ess-maker-profile/extension/package.json index ee7ef39ca..399991ad3 100644 --- a/tools/ess-maker-profile/extension/package.json +++ b/tools/ess-maker-profile/extension/package.json @@ -2,7 +2,7 @@ "name": "ess-maker-profile", "displayName": "ESS Maker", "description": "Chat-only, big-button experience for customizing your Employee Self-Service agent. POC.", - "version": "0.4.25", + "version": "0.4.28", "publisher": "microsoft-ess", "private": true, "engines": { @@ -49,7 +49,7 @@ }, { "command": "essMaker.restoreStandardLayout", - "title": "ESS Maker: Restore Standard Layout", + "title": "ESS Maker: Restore Developer Layout", "category": "ESS Maker" }, { @@ -92,9 +92,9 @@ "properties": { "essMaker.mode": { "type": "string", - "enum": ["lite", "standard", ""], + "enum": ["maker", "developer", "lite", "standard", ""], "default": "", - "description": "Install mode set by the ESS installer. 'lite' applies chat-first layout; 'standard' uses default VS Code layout with /setup injection only." + "description": "Install mode set by the ESS installer. 'maker' (was 'lite') applies the chat-first layout; 'developer' (was 'standard') uses the default VS Code layout with /setup injection; empty string means the installer left the choice unresolved and the extension silently defaults to maker on first activation. The legacy 'lite'/'standard' values are still accepted and treated as 'maker'/'developer' so existing users keep their choice after the rename." }, "essMaker.autoUpdateCheck": { "type": "boolean",