@beeos-ai/cli 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beeos-ai/cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "type": "module",
5
5
  "description": "BeeOS CLI — run AI agents from your desktop",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "build": "tsup",
19
19
  "type-check": "tsc --noEmit",
20
20
  "lint": "eslint src/",
21
- "test": "vitest run --passWithNoTests",
21
+ "test": "vitest run",
22
22
  "prepublishOnly": "pnpm build"
23
23
  },
24
24
  "dependencies": {
@@ -0,0 +1,40 @@
1
+ {
2
+ "$comment": [
3
+ "P1-E of the install-link review (shared anchor file).",
4
+ "",
5
+ "Three call sites render the 'BeeOS is already installed — what now?'",
6
+ "menu with subtly different copy:",
7
+ "",
8
+ " 1. web/packages/cli/scripts/install.sh (bash, pre-Node)",
9
+ " 2. web/packages/cli/scripts/install.ps1 (PowerShell, pre-Node)",
10
+ " 3. web/packages/cli/src/commands/init.ts (TS, post-Node)",
11
+ "",
12
+ "Pre-Node sites can't import TypeScript constants, so we anchor the",
13
+ "wording with this manifest plus a vitest grep lint",
14
+ "(`web/packages/cli/src/__tests__/existing-install-actions.test.ts`).",
15
+ "If you change a label in any of the three files, update the",
16
+ "matching `keywords` here so the lint stays representative; if you",
17
+ "add a new action altogether, add a new entry below + render it",
18
+ "in all three sites."
19
+ ],
20
+ "actions": {
21
+ "upgrade": {
22
+ "$comment": "Reinstall the CLI to the latest version, then proceed. install.sh/install.ps1 wording differs slightly from init.ts which says 'Upgrade CLI & agents'; both must hit at least one keyword from `keywords`.",
23
+ "keywords": ["Reinstall", "Upgrade"]
24
+ },
25
+ "rerun": {
26
+ "$comment": "Reuse the existing install (no npm i -g). install.sh/install.ps1 say 'Re-run beeos init', init.ts shows the 'Re-bind' wording for the same conceptual action.",
27
+ "keywords": ["Re-run", "Re-bind", "Reusing"]
28
+ },
29
+ "skip": {
30
+ "$comment": "Do nothing. Stable wording across all three sites.",
31
+ "keywords": ["Skip"]
32
+ }
33
+ },
34
+ "$files_root_comment": "Paths below are relative to the `web/` workspace root (the test resolves them against that anchor).",
35
+ "files": [
36
+ "packages/cli/scripts/install.sh",
37
+ "packages/cli/scripts/install.ps1",
38
+ "packages/core/src/detect.ts"
39
+ ]
40
+ }
@@ -0,0 +1,169 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pester smoke test for install.ps1 (P2-2 of the install-link review).
4
+
5
+ .DESCRIPTION
6
+ The goal of this suite is NOT end-to-end coverage of the install
7
+ flow — that requires a real Windows host with elevated privileges
8
+ plus npm/winget/choco mocked, which we do not yet have in CI. The
9
+ smoke test instead asserts the script's structural integrity:
10
+
11
+ 1. install.ps1 parses with no syntax errors (caught by the
12
+ AST parser before any side effect).
13
+ 2. The required helper functions are defined at top level so
14
+ that future refactors don't accidentally drop them.
15
+ 3. The dry-run hook (`BEEOS_INSTALL_DRY_RUN=1`) exits before
16
+ performing any network or filesystem write.
17
+
18
+ These three properties together make sure that regressions of the
19
+ "install.sh / install.ps1 没有自动化测试" type from the
20
+ install-link review get caught at PR time on the
21
+ install-script-lint workflow.
22
+ #>
23
+
24
+ # ── Parse-time checks ────────────────────────────────────────
25
+
26
+ $scriptPath = Join-Path $PSScriptRoot "install.ps1"
27
+
28
+ Describe "install.ps1 parse-time integrity" {
29
+ It "exists at the expected path" {
30
+ Test-Path $scriptPath | Should -Be $true
31
+ }
32
+
33
+ It "parses without syntax errors (AST round-trip)" {
34
+ $tokens = $null
35
+ $errors = $null
36
+ [System.Management.Automation.Language.Parser]::ParseFile(
37
+ $scriptPath,
38
+ [ref]$tokens,
39
+ [ref]$errors
40
+ ) | Out-Null
41
+ $errors.Count | Should -Be 0
42
+ }
43
+
44
+ It "declares the expected helper functions" {
45
+ $tokens = $null
46
+ $errors = $null
47
+ $ast = [System.Management.Automation.Language.Parser]::ParseFile(
48
+ $scriptPath,
49
+ [ref]$tokens,
50
+ [ref]$errors
51
+ )
52
+
53
+ $functions = $ast.FindAll({
54
+ param($node)
55
+ $node -is [System.Management.Automation.Language.FunctionDefinitionAst]
56
+ }, $true) | ForEach-Object { $_.Name }
57
+
58
+ $expected = @(
59
+ "Write-BeeInfo",
60
+ "Write-BeeOk",
61
+ "Write-BeeWarn",
62
+ "Write-BeeError",
63
+ "Test-Platform",
64
+ "Test-NodeVersion",
65
+ "Install-NodeAuto",
66
+ "Show-InstallHints",
67
+ "Send-Telemetry",
68
+ "Read-ExistingInstallAction",
69
+ "Install-DeviceAgentSuite",
70
+ "Invoke-BeeosCli",
71
+ # P2-8: PSCore + RunAs auto-elevation helpers.
72
+ "Test-IsAdministrator",
73
+ "Request-Elevation"
74
+ )
75
+
76
+ foreach ($name in $expected) {
77
+ $functions | Should -Contain $name
78
+ }
79
+ }
80
+ }
81
+
82
+ # ── Static guards ────────────────────────────────────────────
83
+
84
+ Describe "install.ps1 platform guard (P2-8)" {
85
+ It "contains the non-Windows early-exit branch" {
86
+ $content = Get-Content -Raw -Path $scriptPath
87
+ # Look for the marker comment AND the exit branch so a
88
+ # superficial rename of the variable still trips the test.
89
+ $content | Should -Match "This installer is Windows-only"
90
+ $content | Should -Match "exit 2"
91
+ }
92
+
93
+ It "contains a UAC auto-elevation branch" {
94
+ $content = Get-Content -Raw -Path $scriptPath
95
+ $content | Should -Match "Start-Process .* -Verb RunAs"
96
+ }
97
+
98
+ It "honours BEEOS_NO_AUTO_ELEVATE=1 as an opt-out" {
99
+ $content = Get-Content -Raw -Path $scriptPath
100
+ $content | Should -Match "BEEOS_NO_AUTO_ELEVATE"
101
+ }
102
+ }
103
+
104
+ Describe "install.ps1 stdin / TTY handover (P1-J)" {
105
+ # When stdin is piped (`irm ... | iex`), the child `beeos init`'s
106
+ # prompts read from the exhausted pipe and silently take defaults.
107
+ # Mirrors install.sh's `</dev/tty` reattachment.
108
+ It "detects [Console]::IsInputRedirected" {
109
+ $content = Get-Content -Raw -Path $scriptPath
110
+ $content | Should -Match "\[Console\]::IsInputRedirected"
111
+ }
112
+
113
+ It "uses Start-Process with -NoNewWindow when stdin is redirected" {
114
+ $content = Get-Content -Raw -Path $scriptPath
115
+ $content | Should -Match "Start-Process .* -NoNewWindow"
116
+ }
117
+ }
118
+
119
+ Describe "install.ps1 stderr log file (P1-I)" {
120
+ # Mirrors install.sh's BEEOS_INSTALL_LOG: winget / choco stderr
121
+ # MUST land in a forensic file instead of `2>$null`. The lint
122
+ # below is structural — assert the variable + the redirect appear.
123
+ It "declares a `$BeeosInstallLog` variable" {
124
+ $content = Get-Content -Raw -Path $scriptPath
125
+ $content | Should -Match "BeeosInstallLog"
126
+ }
127
+
128
+ It "redirects winget stderr to the install log" {
129
+ $content = Get-Content -Raw -Path $scriptPath
130
+ # `2>>$BeeosInstallLog` is the canonical append-redirect syntax;
131
+ # `2>$null` here would be a regression to the silent
132
+ # behaviour the review called out.
133
+ $content | Should -Match "winget install [^`r`n]+2>>\$BeeosInstallLog"
134
+ }
135
+
136
+ It "redirects choco stderr to the install log" {
137
+ $content = Get-Content -Raw -Path $scriptPath
138
+ $content | Should -Match "choco install [^`r`n]+2>>\$BeeosInstallLog"
139
+ }
140
+
141
+ It "Show-InstallHints surfaces the log path on failure" {
142
+ $content = Get-Content -Raw -Path $scriptPath
143
+ $content | Should -Match "Installer log:"
144
+ }
145
+ }
146
+
147
+ # ── Dry-run hook ─────────────────────────────────────────────
148
+
149
+ Describe "install.ps1 dry-run hook" {
150
+ BeforeEach {
151
+ $script:savedDryRun = $env:BEEOS_INSTALL_DRY_RUN
152
+ }
153
+
154
+ AfterEach {
155
+ if ($null -eq $script:savedDryRun) {
156
+ Remove-Item Env:BEEOS_INSTALL_DRY_RUN -ErrorAction SilentlyContinue
157
+ } else {
158
+ $env:BEEOS_INSTALL_DRY_RUN = $script:savedDryRun
159
+ }
160
+ }
161
+
162
+ It "exits cleanly when BEEOS_INSTALL_DRY_RUN=1 is set" {
163
+ $env:BEEOS_INSTALL_DRY_RUN = "1"
164
+ # Dot-source so we exercise the param-block + helper definitions
165
+ # without invoking the main install body. The dry-run hook
166
+ # `return`s before any state-changing call.
167
+ { . $scriptPath } | Should -Not -Throw
168
+ }
169
+ }
@@ -65,6 +65,41 @@ param(
65
65
  $ErrorActionPreference = "Stop"
66
66
  $MinNodeVersion = 18
67
67
 
68
+ # ── PSCore + non-Windows guard (P2-8 of the install-link review) ─────
69
+ #
70
+ # The script header asks for `#Requires -Version 5.1`, but that only
71
+ # rejects Windows PowerShell 2.0/3.0/4.0 hosts — it does NOT block
72
+ # PowerShell Core (`pwsh`) running on macOS / Linux, where most of
73
+ # the cmdlets used below (`winget`, `choco`, `Start-Process -Verb
74
+ # RunAs`, `[Environment]::UserInteractive`) are either missing or
75
+ # behave very differently. Without an explicit check the user gets a
76
+ # stack of opaque errors midway through the script.
77
+ #
78
+ # The check happens BEFORE any helper is even defined so that the
79
+ # message is the first thing the user sees.
80
+ $script:IsWindowsHost = $true
81
+ if ($PSVersionTable.PSEdition -eq "Core") {
82
+ # PowerShell 6+ exposes the cross-platform `$IsWindows` automatic
83
+ # variable. Fall back to environment sniffing if some exotic host
84
+ # somehow strips it (e.g. an embedded runner without the standard
85
+ # automatic-variable provider — extremely rare but cheap to guard).
86
+ $autoIsWindows = Get-Variable -Name "IsWindows" -ErrorAction SilentlyContinue
87
+ if ($autoIsWindows) {
88
+ $script:IsWindowsHost = [bool]$autoIsWindows.Value
89
+ } else {
90
+ $script:IsWindowsHost = ($env:OS -eq "Windows_NT")
91
+ }
92
+ }
93
+ if (-not $script:IsWindowsHost) {
94
+ Write-Host ""
95
+ Write-Host " This installer is Windows-only." -ForegroundColor Red
96
+ Write-Host " On macOS or Linux, please use install.sh instead:" -ForegroundColor Yellow
97
+ Write-Host ""
98
+ Write-Host " curl -fsSL https://beeos.ai/install.sh | bash"
99
+ Write-Host ""
100
+ exit 2
101
+ }
102
+
68
103
  $CliPackage = if ($Version -eq "latest") { "@beeos-ai/cli@latest" } else { "@beeos-ai/cli@$Version" }
69
104
 
70
105
  # Sibling packages installed alongside the CLI so that `beeos device attach`
@@ -87,6 +122,22 @@ if (-not $env:BEEOS_API_URL -or $env:BEEOS_API_URL.Length -eq 0) {
87
122
  $script:BeeosApiUrl = $env:BEEOS_API_URL
88
123
  }
89
124
 
125
+ # P1-I of the install-link review: dump winget/choco stderr to a
126
+ # timestamped log so a Windows install failure leaves a forensic
127
+ # breadcrumb for support. Mirrors `install.sh`'s `BEEOS_INSTALL_LOG`.
128
+ # Honour `$env:BEEOS_INSTALL_LOG` if the caller supplied one (CI may
129
+ # want a deterministic path); otherwise default to a file under
130
+ # `$env:TEMP` named `beeos-install-<timestamp>.log`. The directory
131
+ # must exist before we redirect into it — `$env:TEMP` is guaranteed
132
+ # on Windows, so no `New-Item` ceremony.
133
+ if ($env:BEEOS_INSTALL_LOG -and $env:BEEOS_INSTALL_LOG.Length -gt 0) {
134
+ $script:BeeosInstallLog = $env:BEEOS_INSTALL_LOG
135
+ } else {
136
+ $logDir = if ($env:TEMP) { $env:TEMP } else { (Get-Location).Path }
137
+ $stamp = Get-Date -Format "yyyyMMdd-HHmmss"
138
+ $script:BeeosInstallLog = Join-Path $logDir ("beeos-install-{0}.log" -f $stamp)
139
+ }
140
+
90
141
  # ── Helpers ──────────────────────────────────────────────────
91
142
 
92
143
  function Write-BeeInfo { param([string]$Msg) Write-Host "[beeos] $Msg" -ForegroundColor Cyan }
@@ -97,8 +148,10 @@ function Write-BeeOk { param([string]$Msg) Write-Host "[beeos] $Msg" -Foregro
97
148
  # ── Script-level telemetry (mirror of install.sh send_telemetry) ──
98
149
  #
99
150
  # Fire-and-forget POST to `/api/v1/telemetry/install`, capped at 2 s,
100
- # error-swallowing. Matches `web/packages/cli/src/telemetry.ts` event
101
- # names so dashboard queries don't have to special-case Windows.
151
+ # error-swallowing. Event names + payload shape mirror
152
+ # `web/packages/cli/src/telemetry.ts` (the Node-side source of truth);
153
+ # add an event in BOTH places or the Windows install will silently
154
+ # fall off the dashboard.
102
155
  #
103
156
  # Opt-out: `$env:BEEOS_NO_TELEMETRY = "1"`.
104
157
  function Send-Telemetry {
@@ -203,15 +256,115 @@ function Test-NodeVersion {
203
256
 
204
257
  # ── Auto-install Node.js ─────────────────────────────────────
205
258
 
259
+ # Detect whether the current PowerShell session is running with
260
+ # administrator privileges. winget / choco both need elevation to
261
+ # install machine-wide packages; without this check the failure
262
+ # manifests as opaque "access denied" errors deep inside the package
263
+ # manager output.
264
+ function Test-IsAdministrator {
265
+ try {
266
+ $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
267
+ $principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
268
+ return $principal.IsInRole(
269
+ [System.Security.Principal.WindowsBuiltInRole]::Administrator
270
+ )
271
+ } catch {
272
+ return $false
273
+ }
274
+ }
275
+
276
+ # Re-exec the script in an elevated PowerShell window when the
277
+ # current process is not an administrator. The user sees a UAC
278
+ # prompt; on consent, a fresh `pwsh.exe` / `powershell.exe` runs the
279
+ # same script with the same parameters, and the caller exits cleanly.
280
+ #
281
+ # Returns:
282
+ # $true — elevation requested; caller MUST exit immediately.
283
+ # $false — already elevated, or elevation declined / unavailable.
284
+ # Caller continues in the original (non-elevated) session.
285
+ function Request-Elevation {
286
+ if (Test-IsAdministrator) { return $false }
287
+ if ($env:BEEOS_NO_AUTO_ELEVATE -eq "1") {
288
+ Write-BeeWarn "BEEOS_NO_AUTO_ELEVATE=1 — skipping auto-elevation."
289
+ return $false
290
+ }
291
+ if (-not [Environment]::UserInteractive) {
292
+ # Non-interactive shells (CI, piped `irm | iex` without a
293
+ # console) cannot show a UAC prompt — re-execing would just
294
+ # spawn a window the user never sees. Bail to the caller's
295
+ # manual-fix branch instead.
296
+ Write-BeeWarn "Non-interactive shell — cannot prompt for elevation."
297
+ return $false
298
+ }
299
+
300
+ Write-BeeInfo "Requesting administrator privileges (UAC prompt) ..."
301
+ $self = $PSCommandPath
302
+ if (-not $self) {
303
+ # `irm | iex` produces no script path; we can't re-exec a
304
+ # one-liner, so let the caller print manual instructions.
305
+ Write-BeeWarn "Auto-elevation unavailable when piping the script (irm | iex)."
306
+ Write-BeeWarn "Please open PowerShell as Administrator and re-run the installer."
307
+ return $false
308
+ }
309
+
310
+ $pwsh = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
311
+ if (-not $pwsh) {
312
+ $pwsh = (Get-Command powershell -ErrorAction SilentlyContinue).Source
313
+ }
314
+ if (-not $pwsh) {
315
+ Write-BeeWarn "Could not locate pwsh.exe / powershell.exe for elevation."
316
+ return $false
317
+ }
318
+
319
+ $argList = @(
320
+ "-NoProfile",
321
+ "-ExecutionPolicy", "Bypass",
322
+ "-File", $self
323
+ )
324
+ if ($Version -ne "latest") { $argList += @("-Version", $Version) }
325
+ if ($Device) { $argList += "-Device" }
326
+ if ($Passthrough) { $argList += $Passthrough }
327
+
328
+ try {
329
+ Start-Process -FilePath $pwsh -ArgumentList $argList -Verb RunAs | Out-Null
330
+ Write-BeeOk "Elevated installer launched in a new window."
331
+ return $true
332
+ } catch {
333
+ Write-BeeWarn "User declined elevation or UAC failed: $_"
334
+ return $false
335
+ }
336
+ }
337
+
206
338
  function Install-NodeAuto {
207
339
  Write-BeeInfo "Node.js not found. Attempting automatic installation..."
208
340
  Write-Host ""
209
341
 
342
+ # Both winget (machine scope) and Chocolatey require admin rights
343
+ # to write into Program Files and HKLM. Try to escalate once; if
344
+ # the user accepts UAC the elevated script does the install and
345
+ # the original (non-elevated) session exits. If they decline we
346
+ # still try the package managers — winget's user-scope mode and
347
+ # choco's portable installs work without admin in some cases.
348
+ if (-not (Test-IsAdministrator)) {
349
+ if (Request-Elevation) {
350
+ # Elevated process took over; exit cleanly so the user
351
+ # doesn't see two installs racing.
352
+ exit 0
353
+ }
354
+ Write-BeeWarn "Continuing without admin — winget/choco may fail with EACCES."
355
+ }
356
+
210
357
  $winget = Get-Command winget -ErrorAction SilentlyContinue
211
358
  if ($winget) {
212
359
  Write-BeeInfo "Installing Node.js LTS via winget..."
213
360
  try {
214
- winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements --silent 2>$null
361
+ # P1-I: route stderr to $BeeosInstallLog instead of $null
362
+ # so a failed winget run leaves a breadcrumb. The append
363
+ # operator (`2>>`) is intentional — we want every line of
364
+ # output across multiple `Install-NodeAuto` invocations
365
+ # within a single script run to land in the same file.
366
+ "[$(Get-Date -Format 'o')] winget install OpenJS.NodeJS.LTS" | Out-File -FilePath $BeeosInstallLog -Append -Encoding utf8
367
+ winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements --silent 2>>$BeeosInstallLog
215
368
  $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
216
369
  [System.Environment]::GetEnvironmentVariable("Path", "User")
217
370
  if (Test-NodeVersion) {
@@ -220,6 +373,7 @@ function Install-NodeAuto {
220
373
  }
221
374
  } catch {
222
375
  Write-BeeWarn "winget install failed: $_"
376
+ Write-BeeWarn " see log for details: $BeeosInstallLog"
223
377
  }
224
378
  }
225
379
 
@@ -227,7 +381,8 @@ function Install-NodeAuto {
227
381
  if ($choco) {
228
382
  Write-BeeInfo "Installing Node.js LTS via Chocolatey..."
229
383
  try {
230
- choco install nodejs-lts -y 2>$null
384
+ "[$(Get-Date -Format 'o')] choco install nodejs-lts" | Out-File -FilePath $BeeosInstallLog -Append -Encoding utf8
385
+ choco install nodejs-lts -y 2>>$BeeosInstallLog
231
386
  $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
232
387
  [System.Environment]::GetEnvironmentVariable("Path", "User")
233
388
  if (Test-NodeVersion) {
@@ -236,6 +391,7 @@ function Install-NodeAuto {
236
391
  }
237
392
  } catch {
238
393
  Write-BeeWarn "Chocolatey install failed: $_"
394
+ Write-BeeWarn " see log for details: $BeeosInstallLog"
239
395
  }
240
396
  }
241
397
 
@@ -261,6 +417,13 @@ function Show-InstallHints {
261
417
  Write-BeeInfo "Then run this script again:"
262
418
  Write-Host " irm https://beeos.ai/install.ps1 | iex"
263
419
  Write-Host ""
420
+ # P1-I: surface the install log so support requests include the
421
+ # exact winget / choco stderr that explains the failure.
422
+ if (Test-Path $BeeosInstallLog) {
423
+ Write-Host " Installer log:" -ForegroundColor White
424
+ Write-Host " $BeeosInstallLog"
425
+ Write-Host ""
426
+ }
264
427
  }
265
428
 
266
429
  # ── Existing install detection ───────────────────────────────
@@ -277,17 +440,32 @@ function Get-BeeosVersion {
277
440
  }
278
441
 
279
442
  function Read-ExistingInstallAction {
443
+ # Note on naming: "Reinstall + upgrade CLI" deliberately differs
444
+ # from the `beeos init` menu's "Upgrade CLI & agents" item. The
445
+ # script-side action also re-bootstraps Node / pnpm and re-applies
446
+ # this shell's env vars before exec'ing the new binary, which the
447
+ # in-CLI upgrade does NOT do — choosing a different verb keeps the
448
+ # two flows distinguishable. See `install.sh` for the matching
449
+ # docstring.
450
+ #
451
+ # P1-E of the install-link review: the labels rendered below are
452
+ # anchored against `install.sh::prompt_existing_install_action` +
453
+ # `init.ts::decideAction` via
454
+ # `web/packages/cli/scripts/_existing_install_actions.json` and
455
+ # the vitest grep lint at
456
+ # `web/packages/cli/src/__tests__/existing-install-actions.test.ts`.
457
+ # Keep at least one keyword from `keywords[action]` per option.
280
458
  $ver = Get-BeeosVersion
281
459
  if (-not $ver) { $ver = "unknown" }
282
460
  Write-BeeWarn "BeeOS CLI is already installed (v$ver)."
283
461
  Write-Host ""
284
- Write-Host " [1] Upgrade to the latest version and run beeos init"
285
- Write-Host " [2] Re-run beeos init without upgrading"
462
+ Write-Host " [1] Reinstall + upgrade CLI to the latest version, then run beeos init"
463
+ Write-Host " [2] Re-run beeos init without reinstalling"
286
464
  Write-Host " [3] Skip (do nothing)"
287
465
  Write-Host ""
288
466
 
289
467
  if (-not [Environment]::UserInteractive) {
290
- Write-BeeInfo "Non-interactive shell — defaulting to upgrade."
468
+ Write-BeeInfo "Non-interactive shell — defaulting to reinstall."
291
469
  return "upgrade"
292
470
  }
293
471
 
@@ -367,6 +545,12 @@ function Invoke-BeeosCli {
367
545
  Write-BeeInfo "Installing $CliPackage globally..."
368
546
  & npm install -g $CliPackage
369
547
  if ($LASTEXITCODE -ne 0) {
548
+ # P0-A of the install-link review: fail-after-bootstrap signal.
549
+ # The `install.bootstrap.cli_installed` event is only emitted
550
+ # AFTER npm + the device-agent suite report success, so npm
551
+ # failures are recorded as a distinct outcome (not as silent
552
+ # post-success).
553
+ Send-Telemetry -Event "install.bootstrap.npm_fail" -ErrorCode "npm_install_cli_failed" -Success $false
370
554
  Write-BeeError "npm install -g $CliPackage failed."
371
555
  Write-BeeError ""
372
556
  Write-BeeError "Common fixes:"
@@ -376,11 +560,63 @@ function Invoke-BeeosCli {
376
560
  exit 1
377
561
  }
378
562
  Install-DeviceAgentSuite
563
+ Send-Telemetry -Event "install.bootstrap.cli_installed"
564
+
565
+ # P1-J of the install-link review: when the user invokes the
566
+ # installer via `irm https://beeos.ai/install.ps1 | iex` the
567
+ # script's stdin is the pipe from `Invoke-RestMethod`, not the
568
+ # console. If we then `& beeos @args`, `beeos init`'s prompts read
569
+ # from that exhausted pipe and silently take their default values
570
+ # — exactly what install.sh's `</dev/tty` reattachment was added
571
+ # to prevent on POSIX. We mirror that behaviour here:
572
+ # `Start-Process -NoNewWindow -Wait` reuses the parent's window
573
+ # but gives the child a fresh stdin handle attached to the
574
+ # console, so prompts work as the user expects.
575
+ $stdinRedirected = $false
576
+ try {
577
+ $stdinRedirected = [Console]::IsInputRedirected
578
+ } catch {
579
+ # Older / restricted hosts (e.g. ConstrainedLanguage mode)
580
+ # lose access to [Console]; fall through to the legacy `&`
581
+ # path rather than blowing up here.
582
+ $stdinRedirected = $false
583
+ }
584
+
585
+ if ($stdinRedirected -and [Environment]::UserInteractive) {
586
+ Write-BeeInfo "Detected piped stdin — re-attaching console for interactive prompts."
587
+ $beeosCmd = Get-Command beeos -ErrorAction SilentlyContinue
588
+ if ($beeosCmd) {
589
+ # `-WorkingDirectory` keeps cwd predictable; without it
590
+ # `Start-Process` defaults to the user's home directory in
591
+ # some hosts, which surprises later steps.
592
+ Start-Process -FilePath $beeosCmd.Source `
593
+ -ArgumentList $args `
594
+ -NoNewWindow `
595
+ -Wait `
596
+ -WorkingDirectory (Get-Location).Path
597
+ return
598
+ }
599
+ # If `beeos` somehow isn't on PATH yet (race with PATH refresh
600
+ # under a brand-new install), fall through to `&` which will
601
+ # fail loudly — better than silently consuming a piped EOF.
602
+ }
603
+
379
604
  & beeos @args
380
605
  }
381
606
 
382
607
  # ── Main ─────────────────────────────────────────────────────
383
608
 
609
+ # Dry-run hook for the Pester smoke test (P2-2 of the install-link
610
+ # review). When `BEEOS_INSTALL_DRY_RUN=1` is set, exit BEFORE running
611
+ # any state-changing operation but AFTER all helper functions and
612
+ # `param`-block validation have completed. This lets CI assert
613
+ # "the script parses + every helper is callable" without spawning a
614
+ # real Node install or hitting the telemetry endpoint.
615
+ if ($env:BEEOS_INSTALL_DRY_RUN -eq "1") {
616
+ Write-Host "[beeos] BEEOS_INSTALL_DRY_RUN=1 — exiting early (smoke test mode)" -ForegroundColor Yellow
617
+ return
618
+ }
619
+
384
620
  Write-Host ""
385
621
  Write-Host " BeeOS CLI Installer" -ForegroundColor White
386
622
  Write-Host " https://beeos.ai" -ForegroundColor DarkGray
@@ -414,9 +650,10 @@ if ($beeosCmd) {
414
650
  exit 0
415
651
  }
416
652
  "upgrade" {
417
- Write-BeeInfo "Upgrading $CliPackage..."
653
+ Write-BeeInfo "Reinstalling $CliPackage..."
418
654
  & npm install -g $CliPackage
419
655
  if ($LASTEXITCODE -ne 0) {
656
+ Send-Telemetry -Event "install.bootstrap.npm_fail" -ErrorCode "npm_install_cli_failed_upgrade" -Success $false
420
657
  Write-BeeError "npm install -g $CliPackage failed."
421
658
  Write-BeeError ""
422
659
  Write-BeeError "Common fixes:"
@@ -436,7 +673,13 @@ if ($beeosCmd) {
436
673
  Write-BeeInfo "Running BeeOS CLI..."
437
674
  Write-Host ""
438
675
 
439
- Send-Telemetry -Event "install.bootstrap.success"
676
+ # P0-A of the install-link review: this event marks "Node bootstrap
677
+ # finished, about to dispatch into npm install + the CLI". The
678
+ # follow-up `install.bootstrap.cli_installed` (fired inside
679
+ # Invoke-BeeosCli after npm succeeds) is the real "user has working
680
+ # `beeos` on PATH" signal. The two are split so dashboards can tell
681
+ # bootstrap failures apart from npm/CLI failures.
682
+ Send-Telemetry -Event "install.bootstrap.bootstrap_done"
440
683
 
441
684
  if ($Device) {
442
685
  Invoke-BeeosCli -Subcommand "device" -ExtraArgs (@("attach") + $Passthrough)