@beeos-ai/cli 1.1.0 → 1.1.1

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.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "BeeOS CLI — run AI agents from your desktop",
6
6
  "bin": {
@@ -86,7 +86,12 @@ Describe "install.ps1 parse-time integrity" {
86
86
  # function for Linux. Listed here so a future cleanup
87
87
  # that drops the helper triggers a Pester failure
88
88
  # before reaching customers.
89
- "Test-VncServer"
89
+ "Test-VncServer",
90
+ # EACCES auto-recovery (Phase 3 of the install pipeline).
91
+ # install.sh has the parallel `recover_eacces_prefix`.
92
+ # Dropping this helper would silently regress the Phase 3
93
+ # self-healing path back to "hint + exit 3".
94
+ "Invoke-RecoverEaccesPrefix"
90
95
  )
91
96
 
92
97
  foreach ($name in $expected) {
@@ -167,6 +172,58 @@ Describe "install.ps1 stderr log file (P1-I)" {
167
172
  }
168
173
  }
169
174
 
175
+ # ── EACCES auto-recovery (Phase 3) ───────────────────────────
176
+ #
177
+ # Static / structural guards for the Phase 3 self-healing path.
178
+ # Mirrors the install.sh design: when `npm install -g` fails with
179
+ # EACCES on the global node_modules, swap the npm prefix to a
180
+ # user-owned directory and retry exactly once. We deliberately do
181
+ # NOT mock `npm` / `[Environment]::SetEnvironmentVariable` here —
182
+ # Pester mocks for static .NET methods are fragile, and the rest of
183
+ # this test file uses structural / regex assertions for the same
184
+ # reason. Behaviour is exercised end-to-end via the staging EACCES
185
+ # soak (see `.cursor/skills/deploy-staging/install-verify.md`).
186
+
187
+ Describe "install.ps1 EACCES auto-recovery" {
188
+ It "writes to User-scope PATH (never Machine, to avoid UAC)" {
189
+ $content = Get-Content -Raw -Path $script:ScriptPath
190
+ # The recovery MUST persist on User scope; Machine-scope writes
191
+ # would silently re-prompt UAC and undo the whole point of
192
+ # falling back to a user-owned prefix.
193
+ $content | Should -Match 'SetEnvironmentVariable\(\s*"PATH"\s*,[^,]+,\s*"User"\s*\)'
194
+ $content | Should -Not -Match 'SetEnvironmentVariable\(\s*"PATH"\s*,[^,]+,\s*"Machine"\s*\)'
195
+ }
196
+
197
+ It "honours BEEOS_NO_NPM_PREFIX_FIX=1 as an opt-out" {
198
+ $content = Get-Content -Raw -Path $script:ScriptPath
199
+ $content | Should -Match "BEEOS_NO_NPM_PREFIX_FIX"
200
+ }
201
+
202
+ It "matches the EACCES fingerprint in the npm log" {
203
+ $content = Get-Content -Raw -Path $script:ScriptPath
204
+ # Same fingerprint string as install.sh's `grep -qE` — keeps
205
+ # the two scripts' classifier behaviour aligned. If you tighten
206
+ # the regex on one side, tighten it here too (and on
207
+ # install.sh) so a real EACCES on either platform still
208
+ # triggers recovery.
209
+ $content | Should -Match 'EACCES.*permission denied.*node_modules'
210
+ }
211
+
212
+ It "emits install.bootstrap.npm_recovered on retry success" {
213
+ $content = Get-Content -Raw -Path $script:ScriptPath
214
+ $content | Should -Match 'install\.bootstrap\.npm_recovered'
215
+ $content | Should -Match 'eacces_prefix'
216
+ }
217
+
218
+ It "still falls through to exit 3 when recovery is skipped or fails" {
219
+ $content = Get-Content -Raw -Path $script:ScriptPath
220
+ # Regression guard: the original hint + exit 3 path MUST stay
221
+ # reachable for non-EACCES failure modes (ETIMEDOUT, EBADENGINE,
222
+ # ENOSPC ...) and for the case where recovery itself bails.
223
+ $content | Should -Match 'exit 3'
224
+ }
225
+ }
226
+
170
227
  # ── Dry-run hook ─────────────────────────────────────────────
171
228
 
172
229
  Describe "install.ps1 dry-run hook" {
@@ -31,6 +31,16 @@
31
31
  install-link refactor).
32
32
  $env:BEEOS_USE_NPX = "1" Power-user throwaway install (beeos
33
33
  won't persist on PATH).
34
+ $env:BEEOS_NO_NPM_PREFIX_FIX="1" Disable Phase 3 EACCES auto-recovery.
35
+ Recovery (default ON) detects
36
+ "EACCES on lib/node_modules" in the
37
+ npm log, switches the npm prefix
38
+ to %APPDATA%\npm, prepends it on
39
+ User-scope PATH (NEVER Machine —
40
+ that would re-trigger UAC), and
41
+ retries `npm install -g` once.
42
+ Set to 1 to fall back to the legacy
43
+ "hint + exit 3" behaviour.
34
44
 
35
45
  IMPORTANT (env inheritance):
36
46
  $env:BEEOS_API_URL / BEEOS_AGENT_GATEWAY_URL / BEEOS_DASHBOARD_URL
@@ -431,6 +441,80 @@ function Show-InstallHints {
431
441
  }
432
442
  }
433
443
 
444
+ # ── EACCES auto-recovery (Phase 3) ────────────────────────────
445
+ #
446
+ # Mirror of `install.sh::recover_eacces_prefix`. Phase 2 (Node install)
447
+ # already self-heals via winget → choco; Phase 3 (`npm install -g`)
448
+ # historically just printed a hint and exited. EACCES on Windows is
449
+ # rarer than on macOS — the default npm prefix is already user-scoped
450
+ # (`%APPDATA%\npm`) — but it does happen when an Administrator-
451
+ # installed Node has flipped the prefix to `C:\Program Files\nodejs\
452
+ # node_modules`. The fix is symmetric to the POSIX path: switch the
453
+ # prefix back to a user-owned directory and persist on User-scope
454
+ # PATH. We DELIBERATELY do not touch Machine-scope PATH — that would
455
+ # require UAC and silently re-trigger the elevation prompt the user
456
+ # already declined when winget/choco asked.
457
+ #
458
+ # Escape hatch: `$env:BEEOS_NO_NPM_PREFIX_FIX = "1"` short-circuits
459
+ # this function so power users / strict-policy operators can opt out.
460
+ function Invoke-RecoverEaccesPrefix {
461
+ if ($env:BEEOS_NO_NPM_PREFIX_FIX -eq "1") {
462
+ Write-BeeInfo "BEEOS_NO_NPM_PREFIX_FIX=1 — skipping auto npm prefix switch."
463
+ return $false
464
+ }
465
+
466
+ $appData = $env:APPDATA
467
+ if (-not $appData -or $appData.Length -eq 0) {
468
+ Write-BeeWarn "APPDATA env not set — cannot pick a user prefix. Falling back to hint."
469
+ return $false
470
+ }
471
+ $prefix = Join-Path $appData "npm"
472
+ Write-BeeInfo "Switching npm global prefix to $prefix (user-owned)."
473
+
474
+ try {
475
+ New-Item -ItemType Directory -Path $prefix -Force | Out-Null
476
+ } catch {
477
+ Write-BeeError "Could not create ${prefix}: $_"
478
+ return $false
479
+ }
480
+
481
+ try {
482
+ & npm config set prefix "$prefix" 2>&1 | Tee-Object -FilePath $BeeosInstallLog -Append | Out-Null
483
+ if ($LASTEXITCODE -ne 0) {
484
+ Write-BeeError "npm config set prefix failed (exit $LASTEXITCODE)."
485
+ return $false
486
+ }
487
+ } catch {
488
+ Write-BeeError "npm config set prefix threw: $_"
489
+ return $false
490
+ }
491
+
492
+ # Make $prefix usable for the upcoming retry IN this session.
493
+ # Persistence (next terminal) happens via SetEnvironmentVariable
494
+ # below.
495
+ $env:Path = "$prefix;" + $env:Path
496
+
497
+ # Persist to User-scope PATH so a new terminal sees it. Idempotent —
498
+ # re-running on an already-fixed host MUST NOT append a duplicate
499
+ # entry (the User PATH is global to the account; appending without
500
+ # a contains-check would grow it unboundedly across re-runs).
501
+ try {
502
+ $userPath = [Environment]::GetEnvironmentVariable("PATH", "User")
503
+ if (-not $userPath) { $userPath = "" }
504
+ if (($userPath -split ";") -notcontains $prefix) {
505
+ $newUserPath = if ($userPath.Length -gt 0) { "$prefix;$userPath" } else { "$prefix" }
506
+ [Environment]::SetEnvironmentVariable("PATH", $newUserPath, "User")
507
+ Write-BeeInfo "Persisted PATH to user environment. Set BEEOS_NO_NPM_PREFIX_FIX=1 to skip on re-runs."
508
+ } else {
509
+ Write-BeeInfo "User PATH already contains $prefix — leaving as-is."
510
+ }
511
+ } catch {
512
+ Write-BeeWarn "Could not persist user PATH: $_"
513
+ Write-BeeWarn " Add $prefix to PATH manually to keep beeos discoverable in new terminals."
514
+ }
515
+ return $true
516
+ }
517
+
434
518
  # ── Run CLI ──────────────────────────────────────────────────
435
519
 
436
520
  function Invoke-BeeosCli {
@@ -469,18 +553,49 @@ function Invoke-BeeosCli {
469
553
  & npm install -g $CliPackage 2>&1 | Tee-Object -FilePath $BeeosInstallLog -Append
470
554
  $npmExit = $LASTEXITCODE
471
555
  if ($npmExit -ne 0) {
472
- # P0-A of the install-link review: fail-after-bootstrap signal.
473
- # The `install.bootstrap.cli_installed` event is only emitted
474
- # AFTER npm reports success.
475
- Send-Telemetry -Event "install.bootstrap.npm_fail" -ErrorCode "npm_install_cli_failed" -Success $false
476
- Write-BeeError "npm install -g $CliPackage failed."
477
- Write-BeeError " Full log: $BeeosInstallLog"
478
- Write-BeeError ""
479
- Write-BeeError "Common fixes:"
480
- Write-BeeError " - EEXIST on beeos from another package:"
481
- Write-BeeError " npm uninstall -g @beeos-ai/cli # then re-run installer"
482
- Write-BeeError " - EACCES / permission error: run PowerShell as Administrator"
483
- exit 3
556
+ # ── EACCES auto-recovery ──────────────────────────────────
557
+ # Mirror of install.sh's failure-branch recovery. We grep the
558
+ # teed log for the EACCES fingerprint; if it matches AND the
559
+ # user hasn't disabled the auto-fix, we swap the npm prefix
560
+ # to %APPDATA%\npm + persist to User PATH and retry exactly
561
+ # once. Other npm error classes (ENOSPC / E401 / EBADENGINE
562
+ # / ...) fall through to the existing hint+exit path —
563
+ # rarer, and need user judgement we can't safely automate.
564
+ $recovered = $false
565
+ $logContent = if (Test-Path $BeeosInstallLog) {
566
+ Get-Content -Raw -Path $BeeosInstallLog
567
+ } else { "" }
568
+ if ($logContent -match 'EACCES.*permission denied.*node_modules') {
569
+ Write-BeeWarn "Detected EACCES on global node_modules — attempting auto-recovery."
570
+ if (Invoke-RecoverEaccesPrefix) {
571
+ Write-BeeInfo "Retrying npm install with user-owned prefix..."
572
+ & npm install -g $CliPackage 2>&1 | Tee-Object -FilePath $BeeosInstallLog -Append
573
+ if ($LASTEXITCODE -eq 0) {
574
+ Send-Telemetry -Event "install.bootstrap.npm_recovered" -ErrorCode "eacces_prefix"
575
+ $recovered = $true
576
+ }
577
+ }
578
+ }
579
+ # ── End recovery ──────────────────────────────────────────
580
+
581
+ if (-not $recovered) {
582
+ # P0-A of the install-link review: fail-after-bootstrap signal.
583
+ # The `install.bootstrap.cli_installed` event is only emitted
584
+ # AFTER npm reports success.
585
+ Send-Telemetry -Event "install.bootstrap.npm_fail" -ErrorCode "npm_install_cli_failed" -Success $false
586
+ Write-BeeError "npm install -g $CliPackage failed."
587
+ Write-BeeError " Full log: $BeeosInstallLog"
588
+ Write-BeeError ""
589
+ Write-BeeError "Common fixes:"
590
+ Write-BeeError " - EEXIST on beeos from another package:"
591
+ Write-BeeError " npm uninstall -g @beeos-ai/cli # then re-run installer"
592
+ Write-BeeError " - EACCES / permission error:"
593
+ Write-BeeError " auto-recovery already attempted; if it failed, manually"
594
+ Write-BeeError " run ``npm config set prefix `"`$env:APPDATA\npm`"`` and"
595
+ Write-BeeError " add `$env:APPDATA\npm to your User PATH, then re-run installer."
596
+ Write-BeeError " (Set `$env:BEEOS_NO_NPM_PREFIX_FIX = '1' to disable auto-recovery.)"
597
+ exit 3
598
+ }
484
599
  }
485
600
  # Device-agent suite is intentionally NOT installed here; `beeos
486
601
  # device attach` will auto-install it on first use via
@@ -29,7 +29,9 @@
29
29
  # 3 `npm install -g @beeos-ai/cli` failed (CLI didn't reach PATH)
30
30
  #
31
31
  # Automation can branch on these — `1` is "fix your Node install",
32
- # `2` is "wrong machine", `3` is "your npm prefix / proxy / disk".
32
+ # `2` is "wrong machine", `3` is "your npm prefix / proxy / disk
33
+ # (after EACCES auto-recovery already attempted, see
34
+ # `BEEOS_NO_NPM_PREFIX_FIX` below)".
33
35
  #
34
36
  # ── Optional env vars ─────────────────────────────────────────────
35
37
  # BEEOS_API_URL Public API base (this script uses it for
@@ -51,6 +53,22 @@
51
53
  # install.ps1's same-named env var.
52
54
  # BEEOS_USE_NPX=1 Power-user throwaway install (beeos won't
53
55
  # persist on PATH).
56
+ # BEEOS_NO_NPM_PREFIX_FIX=1 Disable Phase 3 EACCES auto-recovery.
57
+ # Recovery (default ON) detects "EACCES on
58
+ # lib/node_modules" in the npm log, switches
59
+ # the npm global prefix to ~/.npm-global,
60
+ # persists $HOME/.npm-global/bin on PATH via
61
+ # the user's shell rc (zsh -> ~/.zshrc, bash
62
+ # -> ~/.bash_profile on macOS or ~/.bashrc
63
+ # on Linux), and retries `npm install -g`
64
+ # exactly once. The shell rc edit is wrapped
65
+ # in a sentinel block (`# >>> beeos
66
+ # npm-prefix >>>` ... `# <<< beeos
67
+ # npm-prefix <<<`) so it can be reverted by
68
+ # removing the block AND running
69
+ # `npm config delete prefix`. Set this var
70
+ # to 1 to fall back to the legacy
71
+ # "hint + exit 3" behaviour.
54
72
  #
55
73
  # IMPORTANT (env inheritance):
56
74
  # BEEOS_API_URL / BEEOS_AGENT_GATEWAY_URL / BEEOS_DASHBOARD_URL must
@@ -507,6 +525,138 @@ test_vnc_server() {
507
525
  echo ""
508
526
  }
509
527
 
528
+ # ── EACCES auto-recovery (Phase 3) ────────────────────────────
529
+ #
530
+ # Phase 2 of the bootstrap pipeline (Node install) already has a
531
+ # multi-strategy auto-recovery loop in `try_install_node` (nvm → fnm
532
+ # → brew). Phase 3 (`npm install -g @beeos-ai/cli`) historically had
533
+ # none — it just printed a hint and exited 3. The single most common
534
+ # Phase 3 failure is EACCES on the system-owned global node_modules
535
+ # (typically `/usr/local/lib/node_modules` after a nodejs.org .pkg
536
+ # install or a sudo brew install). The two helpers below close that
537
+ # gap with the same shape Phase 2 uses: a small detect function +
538
+ # an idempotent recovery function. `run_cli` calls them only when
539
+ # the npm log carries the EACCES fingerprint; every other npm
540
+ # failure mode is rarer and needs user judgement we can't safely
541
+ # automate from a curl|bash one-liner.
542
+
543
+ # Detect the user's preferred shell init file so that an `export
544
+ # PATH=...` write survives a new terminal. We support zsh + bash
545
+ # (~95% of macOS / Linux users); fish / tcsh / nushell users get a
546
+ # printed hint instead of a silent edit to a file we don't fully
547
+ # understand. `$SHELL` is the login shell as recorded in /etc/passwd,
548
+ # NOT the currently-running interpreter — that's exactly what we want
549
+ # for "what does the user's NEXT terminal source?".
550
+ detect_shell_rc() {
551
+ local shell_name
552
+ shell_name="$(basename "${SHELL:-}")"
553
+ case "$shell_name" in
554
+ zsh)
555
+ # zsh's per-user interactive rc. macOS Terminal.app sources it
556
+ # for both login and non-login zsh sessions. We deliberately do
557
+ # NOT touch ~/.zprofile (login-only) — keeping all our writes
558
+ # in a single file makes "how do I revert?" a one-line answer.
559
+ printf '%s' "$HOME/.zshrc"
560
+ ;;
561
+ bash)
562
+ # macOS Terminal.app launches each window as a login bash, so
563
+ # the canonical sourced file is ~/.bash_profile. Linux GUI
564
+ # terminals run interactive non-login bash and source ~/.bashrc.
565
+ if [[ "$OS_KIND" == "darwin" ]]; then
566
+ printf '%s' "$HOME/.bash_profile"
567
+ else
568
+ printf '%s' "$HOME/.bashrc"
569
+ fi
570
+ ;;
571
+ *)
572
+ # Empty string ⇒ caller falls back to a manual-paste hint.
573
+ printf '%s' ""
574
+ ;;
575
+ esac
576
+ }
577
+
578
+ # Switch the npm global prefix to a user-owned directory and persist
579
+ # the resulting PATH change to the user's shell rc. Used as the EACCES
580
+ # auto-recovery in `run_cli`. Idempotent — running on an already-
581
+ # fixed host is a no-op (the sentinel block check below prevents
582
+ # duplicate appends).
583
+ #
584
+ # Why this approach (and not the alternatives):
585
+ #
586
+ # - sudo retry: would leave /usr/local/lib/node_modules/@beeos-ai/cli
587
+ # owned by root, breaking the next `npm update -g` and creating
588
+ # ownership inconsistencies between the CLI binary and ~/.beeos
589
+ # (which the user owns). It's the kind of "fix" that buries the
590
+ # real problem deeper.
591
+ #
592
+ # - force-install nvm: intrusive for a user who already has a
593
+ # working Node toolchain, and risks breaking other tools that
594
+ # depend on the system Node path.
595
+ #
596
+ # - just print a hint: the original behaviour. Pushes the fix onto
597
+ # the user, who has to switch contexts mid-install. Most users
598
+ # give up here.
599
+ #
600
+ # The chosen path mirrors npm's own documentation:
601
+ # https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally
602
+ #
603
+ # Escape hatch: `BEEOS_NO_NPM_PREFIX_FIX=1` short-circuits this
604
+ # function so power users / strict-dotfiles operators can opt out.
605
+ recover_eacces_prefix() {
606
+ if [[ "${BEEOS_NO_NPM_PREFIX_FIX:-}" == "1" ]]; then
607
+ info "BEEOS_NO_NPM_PREFIX_FIX=1 — skipping auto npm prefix switch."
608
+ return 1
609
+ fi
610
+
611
+ local prefix="$HOME/.npm-global"
612
+ info "Switching npm global prefix to ${prefix} (user-owned)."
613
+
614
+ if ! mkdir -p "$prefix" >> "$BEEOS_INSTALL_LOG" 2>&1; then
615
+ error "Could not create ${prefix} — falling back to hint."
616
+ return 1
617
+ fi
618
+ if ! npm config set prefix "$prefix" >> "$BEEOS_INSTALL_LOG" 2>&1; then
619
+ error "npm config set prefix failed — falling back to hint."
620
+ return 1
621
+ fi
622
+
623
+ # Make the new prefix usable for the upcoming retry IN this
624
+ # session. Persistence (next terminal) happens via shell rc below.
625
+ export PATH="$prefix/bin:$PATH"
626
+
627
+ local rc
628
+ rc="$(detect_shell_rc)"
629
+ if [[ -z "$rc" ]]; then
630
+ warn "Unsupported login shell ($(basename "${SHELL:-unknown}")) — could not"
631
+ warn " persist PATH automatically. Add this line to your shell init:"
632
+ warn " export PATH=\"$prefix/bin:\$PATH\""
633
+ return 0
634
+ fi
635
+
636
+ # Sentinel block — grep + sed can remove it cleanly. We check for
637
+ # the BEGIN marker so re-runs of the installer don't append the
638
+ # block twice (the file may have been edited by hand between runs).
639
+ local marker_begin="# >>> beeos npm-prefix >>>"
640
+ local marker_end="# <<< beeos npm-prefix <<<"
641
+ if [[ -f "$rc" ]] && grep -qF "$marker_begin" "$rc"; then
642
+ info "Sentinel block already present in ${rc} — leaving as-is."
643
+ return 0
644
+ fi
645
+
646
+ # `>>` never clobbers existing content. If the rc doesn't exist yet
647
+ # (fresh user account), the redirect creates it with 0644 perms.
648
+ {
649
+ printf '\n%s\n' "$marker_begin"
650
+ printf '# Added by https://beeos.ai/install on %s\n' "$(date -u +%FT%TZ)"
651
+ printf '# Reason: system npm prefix was not writable (EACCES).\n'
652
+ printf '# To revert: delete this block AND run `npm config delete prefix`.\n'
653
+ printf 'export PATH="%s/bin:$PATH"\n' "$prefix"
654
+ printf '%s\n' "$marker_end"
655
+ } >> "$rc"
656
+ info "Persisted PATH to ${rc}. Set BEEOS_NO_NPM_PREFIX_FIX=1 to skip on re-runs."
657
+ return 0
658
+ }
659
+
510
660
  # ── Main ─────────────────────────────────────────────────────
511
661
 
512
662
  run_cli() {
@@ -553,6 +703,28 @@ run_cli() {
553
703
  # the time they go to copy-paste. `set -o pipefail` (file header)
554
704
  # ensures the pipe's exit status is `npm`'s, not `tee`'s.
555
705
  if ! npm install -g "$CLI_PACKAGE" 2>&1 | tee -a "$BEEOS_INSTALL_LOG"; then
706
+ # ── EACCES auto-recovery ──────────────────────────────────────
707
+ # The single most common Phase 3 failure: the system npm prefix
708
+ # is owned by root, EACCES on rename. Detect the fingerprint in
709
+ # the npm log we just teed; if it matches, swap the prefix to a
710
+ # user-owned dir (see `recover_eacces_prefix` for the rationale)
711
+ # and retry exactly once. Every other npm error class
712
+ # (ENOSPC / ETIMEDOUT / EBADENGINE / ...) falls through to the
713
+ # hint+exit path below — those are rarer in practice and need
714
+ # human judgement we can't safely automate.
715
+ if grep -qE 'EACCES.*permission denied.*node_modules' "$BEEOS_INSTALL_LOG"; then
716
+ warn "Detected EACCES on global node_modules — attempting auto-recovery."
717
+ if recover_eacces_prefix; then
718
+ info "Retrying npm install with user-owned prefix..."
719
+ if npm install -g "$CLI_PACKAGE" 2>&1 | tee -a "$BEEOS_INSTALL_LOG"; then
720
+ send_telemetry "install.bootstrap.npm_recovered" "eacces_prefix"
721
+ send_telemetry "install.bootstrap.cli_installed"
722
+ exec beeos "$subcmd" "$@" <"$stdin_src"
723
+ fi
724
+ fi
725
+ fi
726
+ # ── End recovery ──────────────────────────────────────────────
727
+
556
728
  # P0-A of the install-link review: separate "Node ready" from
557
729
  # "CLI is actually on PATH" so the dashboard never reports a
558
730
  # success that the user can't reproduce. `install.bootstrap.npm_fail`
@@ -565,7 +737,10 @@ run_cli() {
565
737
  error " - EEXIST on /bin/beeos from another package:"
566
738
  error " npm uninstall -g @beeos-ai/cli # then re-run installer"
567
739
  error " - EACCES permission error:"
568
- error " use a node version manager (nvm / fnm) or sudo"
740
+ error " auto-recovery already attempted; if it failed, manually"
741
+ error " run \`npm config set prefix \"\$HOME/.npm-global\"\` and"
742
+ error " add \$HOME/.npm-global/bin to PATH, then re-run installer."
743
+ error " (Set BEEOS_NO_NPM_PREFIX_FIX=1 to disable auto-recovery.)"
569
744
  exit 3
570
745
  fi
571
746
  # NPM succeeded — CLI is now on PATH. The device-agent suite is