@basein/runner 0.2.8 → 0.2.10
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/README.md +64 -21
- package/dist/auth/client.d.ts +40 -1
- package/dist/auth/client.js +77 -9
- package/dist/bin/bir-hooks.d.ts +18 -3
- package/dist/bin/bir-hooks.js +124 -38
- package/dist/bin/bir.d.ts +2 -0
- package/dist/bin/bir.js +362 -39
- package/dist/bin/setup.d.ts +72 -0
- package/dist/bin/setup.js +286 -0
- package/dist/config/adapters/claude-code.d.ts +90 -4
- package/dist/config/adapters/claude-code.js +164 -16
- package/dist/config/generate.d.ts +93 -1
- package/dist/config/generate.js +90 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +8 -0
- package/dist/control/daemon.d.ts +116 -0
- package/dist/control/daemon.js +339 -0
- package/dist/control/discovery.d.ts +26 -0
- package/dist/control/discovery.js +41 -9
- package/dist/control/ensure-hook.d.ts +39 -0
- package/dist/control/ensure-hook.js +98 -0
- package/dist/control/paths.d.ts +14 -0
- package/dist/control/paths.js +20 -0
- package/dist/control/server.d.ts +28 -0
- package/dist/control/server.js +15 -2
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +28 -6
- package/docs/calculatedReplayGuide.md +157 -70
- package/docs/installRun.md +457 -111
- package/docs/loginWeb.md +1 -1
- package/docs/quickstart.md +193 -158
- package/package.json +2 -1
- package/scripts/install.ps1 +669 -0
- package/scripts/install.sh +586 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Set up the BaseIn runner on Windows in one paste: a Node if needed, the
|
|
4
|
+
@basein/runner package, Claude Code, then `bir setup` for this folder.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Served by the API as https://api.<domain>/install.ps1 and meant to be run
|
|
8
|
+
from a terminal opened in the project you start Claude Code in:
|
|
9
|
+
|
|
10
|
+
$env:BIR_SETUP_TOKEN="<token>"; irm https://api.<domain>/install.ps1 | iex
|
|
11
|
+
|
|
12
|
+
It never prompts, never calls exit (under iex that would close the window),
|
|
13
|
+
never touches the execution policy, and is safe to re-run: re-running is
|
|
14
|
+
also the upgrade path. Everything lives under ~\.baseinstrunner.
|
|
15
|
+
|
|
16
|
+
Steps, in order (each prints a "==> " line):
|
|
17
|
+
|
|
18
|
+
0. the folder: the home directory and a drive root are refused before
|
|
19
|
+
anything is downloaded; the setup token is taken out of the
|
|
20
|
+
environment before anything else runs
|
|
21
|
+
1. Node: a Node >= 20 on PATH is used as long as its real binary is not
|
|
22
|
+
inside a version manager (fnm, nvm, Volta, Scoop, asdf, mise), else a
|
|
23
|
+
private, digest-checked Node goes to ~\.baseinstrunner\node\v<V>\ and
|
|
24
|
+
is never put on PATH
|
|
25
|
+
2. the runner: @basein/runner is installed into ~\.baseinstrunner\npm,
|
|
26
|
+
always through npm-cli.js (never the npm shim); bir.cmd and
|
|
27
|
+
bir-hooks.cmd are written to ~\.baseinstrunner\bin, which goes first
|
|
28
|
+
on the user Path
|
|
29
|
+
3. Claude Code, through its official installer, when `claude` is missing
|
|
30
|
+
4. `bir setup --auth-url <api> --project <folder>`: signs in, wires the
|
|
31
|
+
project, starts the recorder, and prints what is left to do
|
|
32
|
+
|
|
33
|
+
The API rewrites two placeholders in this file (the service address and
|
|
34
|
+
the runner version, marked TEMPLATED below). Read raw from the repository
|
|
35
|
+
they count as unset: BIR_AUTH_URL is then required, the version is latest.
|
|
36
|
+
|
|
37
|
+
Environment (all optional):
|
|
38
|
+
|
|
39
|
+
BIR_SETUP_TOKEN one-time setup token from the console; removed from the
|
|
40
|
+
environment at once and handed to `bir setup` only
|
|
41
|
+
BIR_AUTH_URL the API address, when this file was not served by it
|
|
42
|
+
BIR_PROJECT the project folder, instead of the current one
|
|
43
|
+
BIR_NODE_VERSION private Node version instead of the pinned one; its
|
|
44
|
+
digest then comes from nodejs.org's SHASUMS256.txt
|
|
45
|
+
BIR_NODE_ARCHIVE a local node-v<V>-win-<arch>.zip instead of the download
|
|
46
|
+
BIR_TARBALL a local @basein/runner .tgz instead of the registry
|
|
47
|
+
BIR_NO_CLAUDE=1 do not install Claude Code
|
|
48
|
+
BIR_DRY_RUN=1 print every action and perform none: no download, no
|
|
49
|
+
install, no registry write, no `bir setup`. For support:
|
|
50
|
+
it shows what the line would do on a given machine.
|
|
51
|
+
HTTPS_PROXY, HTTP_PROXY, NO_PROXY, NODE_EXTRA_CA_CERTS
|
|
52
|
+
used for the downloads and passed to every child
|
|
53
|
+
#>
|
|
54
|
+
|
|
55
|
+
function Invoke-BirInstall {
|
|
56
|
+
# The token first, before any other statement: copied out of the environment
|
|
57
|
+
# and removed from it, so npm, the Claude Code installer and the `claude` the
|
|
58
|
+
# person starts next never see it. It reaches `bir setup` through that
|
|
59
|
+
# child's environment only, set right before the spawn and removed after.
|
|
60
|
+
$setupToken = $env:BIR_SETUP_TOKEN
|
|
61
|
+
if ($setupToken) { Remove-Item -Path Env:BIR_SETUP_TOKEN -ErrorAction SilentlyContinue }
|
|
62
|
+
|
|
63
|
+
# Inside the function on purpose: under `irm | iex` these would otherwise
|
|
64
|
+
# change the session the person keeps working in.
|
|
65
|
+
Set-StrictMode -Version Latest
|
|
66
|
+
$ErrorActionPreference = 'Stop'
|
|
67
|
+
# Invoke-WebRequest repaints a progress bar per chunk; on a 30 MB zip that
|
|
68
|
+
# takes longer than the download itself.
|
|
69
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
70
|
+
|
|
71
|
+
# -- what the API templated in ----------------------------------------------
|
|
72
|
+
#
|
|
73
|
+
# TEMPLATED: the server rewrites the two values below. The comparison
|
|
74
|
+
# assembles the placeholder name at run time, so the comparison itself is
|
|
75
|
+
# never rewritten.
|
|
76
|
+
$templatedAuthUrl = '__BIR_AUTH_URL__'
|
|
77
|
+
$templatedVersion = '__BIR_VERSION__'
|
|
78
|
+
|
|
79
|
+
$packageName = '@basein/runner'
|
|
80
|
+
$pinnedNodeVersion = '24.21.0'
|
|
81
|
+
# Published digests for the pinned version, the same six as install.sh
|
|
82
|
+
# embeds; only the two Windows ones are used here. Anything else is checked
|
|
83
|
+
# against nodejs.org's SHASUMS256.txt, which is not pinned.
|
|
84
|
+
$pinnedDigests = @{
|
|
85
|
+
'node-v24.21.0-win-x64.zip' = '158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541'
|
|
86
|
+
'node-v24.21.0-win-arm64.zip' = '8779b1bde1d39f8d420e3b57aa657b39891af434d3de44a919044cec06785921'
|
|
87
|
+
'node-v24.21.0-darwin-arm64.tar.gz' = 'bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057'
|
|
88
|
+
'node-v24.21.0-darwin-x64.tar.gz' = '1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097'
|
|
89
|
+
'node-v24.21.0-linux-x64.tar.gz' = '6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff'
|
|
90
|
+
'node-v24.21.0-linux-arm64.tar.gz' = '724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
$dryRun = ($env:BIR_DRY_RUN -eq '1')
|
|
94
|
+
$birHome = Join-Path $env:USERPROFILE '.baseinstrunner'
|
|
95
|
+
$tlsHint = "Your network inspects TLS. Set NODE_EXTRA_CA_CERTS to your company's CA bundle (ask IT) and run this again. Offline: BIR_TARBALL=<file.tgz> and BIR_NODE_ARCHIVE=<archive>."
|
|
96
|
+
|
|
97
|
+
# -- output -----------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function Write-Step([string]$Message) { Write-Host "==> $Message" -ForegroundColor Cyan }
|
|
100
|
+
function Write-Note([string]$Message) { Write-Host " $Message" -ForegroundColor DarkGray }
|
|
101
|
+
function Write-Warn([string]$Message) { Write-Host " $Message" -ForegroundColor Yellow }
|
|
102
|
+
function Write-Would([string]$Message) { Write-Host " [dry-run] would $Message" -ForegroundColor Magenta }
|
|
103
|
+
|
|
104
|
+
# -- helpers ----------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
# Runs a program and returns its exit code with everything it printed.
|
|
107
|
+
# $ErrorActionPreference is 'Continue' in here on purpose: Windows PowerShell
|
|
108
|
+
# 5.1 turns each redirected stderr line into an ErrorRecord, and under 'Stop'
|
|
109
|
+
# the first one (an "npm warn", say) would end the whole run.
|
|
110
|
+
function Invoke-Tool([string]$Exe, [string[]]$Arguments, [switch]$Quiet) {
|
|
111
|
+
$ErrorActionPreference = 'Continue'
|
|
112
|
+
$lines = New-Object System.Collections.Generic.List[string]
|
|
113
|
+
# Reset first: a program that never started (access denied, a quarantined
|
|
114
|
+
# binary) leaves $LASTEXITCODE at the previous command's value, which
|
|
115
|
+
# after a successful npm is 0 - and 0 would read as "it worked".
|
|
116
|
+
$global:LASTEXITCODE = -1
|
|
117
|
+
& $Exe @Arguments 2>&1 | ForEach-Object {
|
|
118
|
+
if ($_ -is [System.Management.Automation.ErrorRecord]) { $line = $_.Exception.Message } else { $line = [string]$_ }
|
|
119
|
+
$lines.Add($line)
|
|
120
|
+
if (-not $Quiet) { Write-Host $line }
|
|
121
|
+
}
|
|
122
|
+
$code = $global:LASTEXITCODE
|
|
123
|
+
if ($code -eq -1) { $lines.Add("$Exe could not be started."); $code = 127 }
|
|
124
|
+
return @{ Code = $code; Lines = $lines.ToArray() }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# The version line among a program's output. stderr is merged into stdout
|
|
128
|
+
# above, so a warning node prints at startup (a NODE_OPTIONS note, an extra
|
|
129
|
+
# certs complaint) can come first; the version is the line that looks like one.
|
|
130
|
+
function Get-VersionLine([string[]]$Lines) {
|
|
131
|
+
foreach ($l in @($Lines)) { if ($l.Trim() -match '^v?\d+\.\d+\.\d+$') { return $l.Trim() } }
|
|
132
|
+
return ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# The first output line that names an existing file - for `node -p process.execPath`.
|
|
136
|
+
function Get-ExistingPathLine([string[]]$Lines) {
|
|
137
|
+
foreach ($l in @($Lines)) {
|
|
138
|
+
$t = $l.Trim()
|
|
139
|
+
if ($t -and (Test-Path -LiteralPath $t -PathType Leaf)) { return $t }
|
|
140
|
+
}
|
|
141
|
+
return ''
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# A command line as the dry run prints it: arguments with spaces quoted, so
|
|
145
|
+
# the line can be pasted as it is.
|
|
146
|
+
function Format-CommandLine([string]$Exe, [string[]]$Arguments) {
|
|
147
|
+
$parts = New-Object System.Collections.Generic.List[string]
|
|
148
|
+
foreach ($a in @($Exe) + @($Arguments)) {
|
|
149
|
+
if ($a -match '\s') { $parts.Add('"' + $a + '"') } else { $parts.Add($a) }
|
|
150
|
+
}
|
|
151
|
+
return ($parts -join ' ')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Trailing separators trimmed and case folded: "C:\Users\me\" and
|
|
155
|
+
# "c:\users\me" are the same folder.
|
|
156
|
+
function Get-ComparablePath([string]$Path) {
|
|
157
|
+
$trimmed = $Path.TrimEnd('\', '/')
|
|
158
|
+
if ($trimmed -eq '') { $trimmed = $Path }
|
|
159
|
+
return $trimmed.ToLowerInvariant()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# "v24.21.0" -> major, minor, and the bare text; $null when it is not a version.
|
|
163
|
+
function Get-NodeVersionParts([string]$Text) {
|
|
164
|
+
if ($Text.Trim() -match '^v?(\d+)\.(\d+)\.(\d+)') {
|
|
165
|
+
return @{ Major = [int]$matches[1]; Minor = [int]$matches[2]; Text = $Text.Trim().TrimStart('v') }
|
|
166
|
+
}
|
|
167
|
+
return $null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# --use-system-ca exists from Node 22.15; with it a corporate root in the
|
|
171
|
+
# Windows store is trusted without NODE_EXTRA_CA_CERTS.
|
|
172
|
+
function Get-NodeFlags([int]$Major, [int]$Minor) {
|
|
173
|
+
if (($Major -gt 22) -or ($Major -eq 22 -and $Minor -ge 15)) { return @('--use-system-ca') }
|
|
174
|
+
return @()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# A per-shell or per-version directory: fnm's multishell links vanish with
|
|
178
|
+
# the shell, and nvm, Volta, Scoop, asdf and mise switch or remove versions
|
|
179
|
+
# under the same path. The SessionStart hook pins an absolute node path, so
|
|
180
|
+
# such a Node must not be the one it pins.
|
|
181
|
+
function Test-ManagedNodePath([string]$Path) {
|
|
182
|
+
$markers = @('fnm_multishells', 'fnm', '.nvm', 'nvm', '.volta', 'volta', 'nodenv', '.nodenv', 'asdf', '.asdf', 'mise', '.mise', 'scoop')
|
|
183
|
+
foreach ($segment in $Path.Split([char[]]@('\', '/'))) {
|
|
184
|
+
if ($markers -icontains $segment) { return $true }
|
|
185
|
+
}
|
|
186
|
+
return $false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function Get-Sha256([string]$Path) {
|
|
190
|
+
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# The digest of one archive from nodejs.org's SHASUMS256.txt: an anchored,
|
|
194
|
+
# exact-filename match, and exactly one line or nothing.
|
|
195
|
+
function Get-PublishedDigest([string]$Version, [string]$FileName) {
|
|
196
|
+
$url = "https://nodejs.org/dist/v$Version/SHASUMS256.txt"
|
|
197
|
+
Write-Note "v$Version is not the pinned version; its digest comes from $url"
|
|
198
|
+
$text = (Invoke-WebRequest -UseBasicParsing -Uri $url @webOptions).Content
|
|
199
|
+
$pattern = '^([0-9a-fA-F]{64})\s+\*?' + [regex]::Escape($FileName) + '\s*$'
|
|
200
|
+
$found = New-Object System.Collections.Generic.List[string]
|
|
201
|
+
foreach ($line in ($text -split "`n")) {
|
|
202
|
+
if ($line.TrimEnd("`r") -match $pattern) { $found.Add($matches[1].ToLowerInvariant()) }
|
|
203
|
+
}
|
|
204
|
+
if ($found.Count -ne 1) { throw "SHASUMS256.txt for v$Version lists $FileName $($found.Count) times; expected exactly once." }
|
|
205
|
+
return $found[0]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# A private Node under ~\.baseinstrunner\node\v<V>\: versioned, so a running
|
|
209
|
+
# recorder keeps its files and nothing is extracted over a locked node.exe.
|
|
210
|
+
function Install-PrivateNode([string]$Version) {
|
|
211
|
+
$arch = $env:PROCESSOR_ARCHITEW6432
|
|
212
|
+
if (-not $arch) { $arch = $env:PROCESSOR_ARCHITECTURE }
|
|
213
|
+
switch ($arch) {
|
|
214
|
+
'AMD64' { $nodeArch = 'x64' }
|
|
215
|
+
'ARM64' { $nodeArch = 'arm64' }
|
|
216
|
+
default { throw "No Node build for a $arch Windows. Install Node 20 or newer from https://nodejs.org and paste the line again." }
|
|
217
|
+
}
|
|
218
|
+
$fileName = "node-v$Version-win-$nodeArch.zip"
|
|
219
|
+
$nodeDir = Join-Path $birHome "node\v$Version"
|
|
220
|
+
$exe = Join-Path $nodeDir 'node.exe'
|
|
221
|
+
|
|
222
|
+
if (Test-Path -LiteralPath $exe) {
|
|
223
|
+
$probe = Invoke-Tool $exe @('-v') -Quiet
|
|
224
|
+
if ($probe.Code -eq 0 -and (Get-VersionLine $probe.Lines) -eq "v$Version") {
|
|
225
|
+
Write-Note "node v$Version already at $exe"
|
|
226
|
+
return $exe
|
|
227
|
+
}
|
|
228
|
+
# Half an extraction or a foreign file: removed, never extracted over.
|
|
229
|
+
if ($dryRun) { Write-Would "remove the unusable $nodeDir" }
|
|
230
|
+
else {
|
|
231
|
+
try { Remove-Item -LiteralPath $nodeDir -Recurse -Force }
|
|
232
|
+
catch { throw "$nodeDir is unusable and cannot be removed (a recorder still using it?). Run 'bir down' and paste the line again." }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if ($Version -eq $pinnedNodeVersion -and $pinnedDigests.ContainsKey($fileName)) {
|
|
237
|
+
$expected = $pinnedDigests[$fileName]
|
|
238
|
+
}
|
|
239
|
+
elseif ($dryRun) {
|
|
240
|
+
Write-Would "fetch https://nodejs.org/dist/v$Version/SHASUMS256.txt for the digest of $fileName"
|
|
241
|
+
$expected = '<from SHASUMS256.txt>'
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
$expected = Get-PublishedDigest $Version $fileName
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
$url = "https://nodejs.org/dist/v$Version/$fileName"
|
|
248
|
+
$archive = $env:BIR_NODE_ARCHIVE
|
|
249
|
+
$downloaded = $false
|
|
250
|
+
if ($archive) {
|
|
251
|
+
if (-not (Test-Path -LiteralPath $archive -PathType Leaf)) { throw "BIR_NODE_ARCHIVE is not a file: $archive" }
|
|
252
|
+
$archive = (Resolve-Path -LiteralPath $archive).ProviderPath
|
|
253
|
+
Write-Note "using the local archive $archive (BIR_NODE_ARCHIVE)"
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
$archive = Join-Path $env:TEMP "bir-$fileName"
|
|
257
|
+
$downloaded = $true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
|
|
261
|
+
$useTar = Test-Path -LiteralPath $tar
|
|
262
|
+
if ($dryRun) {
|
|
263
|
+
if ($downloaded) { Write-Would "download $url to $archive" }
|
|
264
|
+
Write-Would "check its SHA-256 against $expected"
|
|
265
|
+
if ($useTar) { Write-Would "extract it with $tar --strip-components=1 into $nodeDir" }
|
|
266
|
+
else { Write-Would "extract it with Expand-Archive (slow in Windows PowerShell 5.1) into $nodeDir" }
|
|
267
|
+
return $exe
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if ($downloaded) {
|
|
271
|
+
Write-Note "downloading $url"
|
|
272
|
+
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $archive @webOptions
|
|
273
|
+
}
|
|
274
|
+
$actual = Get-Sha256 $archive
|
|
275
|
+
if ($actual -ne $expected) {
|
|
276
|
+
if ($downloaded) { Remove-Item -LiteralPath $archive -Force -ErrorAction SilentlyContinue }
|
|
277
|
+
throw "$fileName does not match its published digest (expected $expected, got $actual). Nothing was installed."
|
|
278
|
+
}
|
|
279
|
+
Write-Note "digest ok: $expected"
|
|
280
|
+
|
|
281
|
+
# Extracted into a sibling directory, then one rename into place: there
|
|
282
|
+
# is never a half-written v<V>, and the rename stays on one volume.
|
|
283
|
+
$staging = Join-Path (Split-Path -Parent $nodeDir) (".v$Version-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
284
|
+
New-Item -ItemType Directory -Path $staging -Force | Out-Null
|
|
285
|
+
try {
|
|
286
|
+
if ($useTar) {
|
|
287
|
+
# bsdtar reads zip files and strips the inner folder itself.
|
|
288
|
+
# Windows 10 1803+ ships it; Git's tar.exe is GNU tar and does not
|
|
289
|
+
# read zips, which is why the System32 one is named explicitly.
|
|
290
|
+
$r = Invoke-Tool $tar @('-xf', $archive, '-C', $staging, '--strip-components=1') -Quiet
|
|
291
|
+
if ($r.Code -ne 0) { throw "tar.exe could not extract $archive`: $($r.Lines -join ' ')" }
|
|
292
|
+
Move-Item -LiteralPath $staging -Destination $nodeDir
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
Expand-Archive -LiteralPath $archive -DestinationPath $staging -Force
|
|
296
|
+
$inner = @(Get-ChildItem -LiteralPath $staging -Directory)
|
|
297
|
+
if ($inner.Count -ne 1) { throw "$fileName did not unpack into one folder as expected." }
|
|
298
|
+
Move-Item -LiteralPath $inner[0].FullName -Destination $nodeDir
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue }
|
|
303
|
+
if ($downloaded) { Remove-Item -LiteralPath $archive -Force -ErrorAction SilentlyContinue }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
$probe = Invoke-Tool $exe @('-v') -Quiet
|
|
307
|
+
if (-not ($probe.Code -eq 0 -and (Get-VersionLine $probe.Lines) -eq "v$Version")) {
|
|
308
|
+
throw "$exe does not run as v$Version after extraction."
|
|
309
|
+
}
|
|
310
|
+
Write-Note "node v$Version at $exe (not on PATH; only the runner uses it)"
|
|
311
|
+
return $exe
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# %USERPROFILE% instead of the literal profile path in a .cmd file: cmd.exe
|
|
315
|
+
# reads it in the console code page, and a profile named after a person
|
|
316
|
+
# (an umlaut, say) would not survive that as ASCII.
|
|
317
|
+
function ConvertTo-LauncherPath([string]$Path) {
|
|
318
|
+
$profileDir = $env:USERPROFILE.TrimEnd('\')
|
|
319
|
+
if ($Path.Length -gt $profileDir.Length -and $Path.Substring(0, $profileDir.Length + 1) -ieq ($profileDir + '\')) {
|
|
320
|
+
return '%USERPROFILE%' + $Path.Substring($profileDir.Length)
|
|
321
|
+
}
|
|
322
|
+
return $Path
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function Write-Launcher([string]$Path, [string]$NodeExe, [string[]]$NodeFlags, [string]$Script) {
|
|
326
|
+
$flags = ''
|
|
327
|
+
if (@($NodeFlags).Count -gt 0) { $flags = ' ' + (@($NodeFlags) -join ' ') }
|
|
328
|
+
$content = '@"' + (ConvertTo-LauncherPath $NodeExe) + '"' + $flags + ' "' + (ConvertTo-LauncherPath $Script) + '" %*' + "`r`n"
|
|
329
|
+
if ($dryRun) {
|
|
330
|
+
Write-Would "write $Path`: $($content.Trim())"
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
$encoding = [System.Text.Encoding]::ASCII
|
|
334
|
+
foreach ($c in $content.ToCharArray()) {
|
|
335
|
+
if ([int]$c -gt 127) {
|
|
336
|
+
$encoding = [System.Text.Encoding]::GetEncoding([System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage)
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
[System.IO.File]::WriteAllText($Path, $content, $encoding)
|
|
341
|
+
Write-Note "$Path -> $($content.Trim())"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Explorer, and so every terminal started from the taskbar or Start, reads
|
|
345
|
+
# the user environment again only after this broadcast. setx sends it,
|
|
346
|
+
# Set-ItemProperty does not. Best effort: a host without a C# compiler still
|
|
347
|
+
# has the registry change, picked up at the next sign-in.
|
|
348
|
+
function Send-EnvironmentChange {
|
|
349
|
+
try {
|
|
350
|
+
if (-not ('BirInstall.Native' -as [type])) {
|
|
351
|
+
Add-Type -Namespace BirInstall -Name Native -MemberDefinition @'
|
|
352
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
353
|
+
public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
|
|
354
|
+
'@
|
|
355
|
+
}
|
|
356
|
+
$result = [UIntPtr]::Zero
|
|
357
|
+
[BirInstall.Native]::SendMessageTimeout([IntPtr]0xffff, 0x1A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result) | Out-Null
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
Write-Note 'could not tell Explorer about the new Path; terminals opened from Start see it after the next sign-in'
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Additive only: this runs inside the person's shell, and replacing
|
|
365
|
+
# $env:Path would drop what that shell added for itself (a venv, a tool's
|
|
366
|
+
# per-shell directory). Registry entries missing here are appended.
|
|
367
|
+
function Update-SessionPath {
|
|
368
|
+
$known = New-Object System.Collections.Generic.List[string]
|
|
369
|
+
foreach ($p in $env:Path.Split(';')) { $known.Add($p.Trim().TrimEnd('\').ToLowerInvariant()) }
|
|
370
|
+
$registry = [string][Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [string][Environment]::GetEnvironmentVariable('Path', 'User')
|
|
371
|
+
foreach ($p in $registry.Split(';')) {
|
|
372
|
+
$dir = $p.Trim()
|
|
373
|
+
if ($dir -eq '') { continue }
|
|
374
|
+
$key = $dir.TrimEnd('\').ToLowerInvariant()
|
|
375
|
+
if (-not $known.Contains($key)) {
|
|
376
|
+
$env:Path = $env:Path + ';' + $dir
|
|
377
|
+
$known.Add($key)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# `claude` on PATH, or the native installer's ~\.local\bin when the Path
|
|
383
|
+
# change has not reached this shell yet.
|
|
384
|
+
function Get-ClaudePath {
|
|
385
|
+
$found = @(Get-Command claude -CommandType Application -ErrorAction SilentlyContinue)
|
|
386
|
+
if ($found.Count -ge 1) { return $found[0].Source }
|
|
387
|
+
$native = Join-Path $env:USERPROFILE '.local\bin\claude.exe'
|
|
388
|
+
if (Test-Path -LiteralPath $native) { return $native }
|
|
389
|
+
return ''
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# ===========================================================================
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
if ($dryRun) { Write-Warn 'BIR_DRY_RUN=1: printing what would happen; nothing is downloaded, installed or written.' }
|
|
396
|
+
|
|
397
|
+
# -- 00. the folder, before anything is downloaded ----------------------
|
|
398
|
+
#
|
|
399
|
+
# A terminal opened from Start begins in the home folder, and that is
|
|
400
|
+
# where a first paste often lands. Nothing is fetched until the folder
|
|
401
|
+
# looks like a project; BIR_PROJECT names one explicitly.
|
|
402
|
+
$project = ''
|
|
403
|
+
if ($env:BIR_PROJECT) {
|
|
404
|
+
if (-not (Test-Path -LiteralPath $env:BIR_PROJECT -PathType Container)) {
|
|
405
|
+
throw "BIR_PROJECT is not a folder: $($env:BIR_PROJECT)"
|
|
406
|
+
}
|
|
407
|
+
$project = (Resolve-Path -LiteralPath $env:BIR_PROJECT).ProviderPath
|
|
408
|
+
# Tab completion leaves a trailing backslash, and `"C:\has space\proj\"`
|
|
409
|
+
# on a command line reads as a path ending in a quote. A root keeps its.
|
|
410
|
+
if ($project.Length -gt ([System.IO.Path]::GetPathRoot($project)).Length) { $project = $project.TrimEnd('\', '/') }
|
|
411
|
+
Set-Location -LiteralPath $project
|
|
412
|
+
Write-Step "Setting up in $project (BIR_PROJECT)"
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
$project = (Get-Location -PSProvider FileSystem).ProviderPath
|
|
416
|
+
$here = Get-ComparablePath $project
|
|
417
|
+
$reason = ''
|
|
418
|
+
foreach ($candidate in @($env:USERPROFILE, $HOME)) {
|
|
419
|
+
if ($candidate -and (Get-ComparablePath $candidate) -eq $here) { $reason = 'your home folder' }
|
|
420
|
+
}
|
|
421
|
+
if (-not $reason -and (Get-ComparablePath ([System.IO.Path]::GetPathRoot($project))) -eq $here) {
|
|
422
|
+
$reason = 'the root of a drive'
|
|
423
|
+
}
|
|
424
|
+
if ($reason) {
|
|
425
|
+
Write-Host "This is $reason. Open a terminal in the project you start Claude Code in (cd there) and paste the line again." -ForegroundColor Red
|
|
426
|
+
Write-Note 'Or name the project first: $env:BIR_PROJECT = "C:\path\to\your\project"'
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
Write-Step "Setting up in $project"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# -- the service address and the version ---------------------------------
|
|
433
|
+
$authUrl = $templatedAuthUrl
|
|
434
|
+
if (-not $authUrl -or $authUrl -eq ('__BIR_' + 'AUTH_URL__')) { $authUrl = $env:BIR_AUTH_URL }
|
|
435
|
+
if (-not $authUrl) {
|
|
436
|
+
throw 'No service address: this file was not served by the API. Set $env:BIR_AUTH_URL = "https://api.your-service" and run it again.'
|
|
437
|
+
}
|
|
438
|
+
$authUrl = $authUrl.Trim().TrimEnd('/')
|
|
439
|
+
$version = $templatedVersion
|
|
440
|
+
if (-not $version -or $version -eq ('__BIR_' + 'VERSION__')) { $version = 'latest' }
|
|
441
|
+
Write-Note "service $authUrl, runner $packageName@$version"
|
|
442
|
+
|
|
443
|
+
# TLS 1.2 for the downloads; PowerShell 5.1 on an older Windows 10 still
|
|
444
|
+
# offers 1.0 and 1.1 by default and nodejs.org refuses those.
|
|
445
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
446
|
+
$webOptions = @{}
|
|
447
|
+
$proxy = $env:HTTPS_PROXY
|
|
448
|
+
if (-not $proxy) { $proxy = $env:HTTP_PROXY }
|
|
449
|
+
if ($proxy) {
|
|
450
|
+
$webOptions['Proxy'] = $proxy
|
|
451
|
+
Write-Note "downloads go through $proxy"
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# -- 1. Node ------------------------------------------------------------
|
|
455
|
+
Write-Step 'Checking Node'
|
|
456
|
+
$nodeExe = ''
|
|
457
|
+
$nodeFlags = @()
|
|
458
|
+
$npmCli = ''
|
|
459
|
+
if (Get-Command node -ErrorAction SilentlyContinue) {
|
|
460
|
+
$probe = Invoke-Tool 'node' @('-v') -Quiet
|
|
461
|
+
$parts = $null
|
|
462
|
+
if ($probe.Code -eq 0) { $parts = Get-NodeVersionParts (Get-VersionLine $probe.Lines) }
|
|
463
|
+
if ($null -eq $parts) {
|
|
464
|
+
Write-Note 'the node on PATH does not answer -v; installing a private one'
|
|
465
|
+
}
|
|
466
|
+
elseif ($parts.Major -lt 20) {
|
|
467
|
+
Write-Note "node v$($parts.Text) on PATH is older than 20; installing a private one"
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
# The real binary: shims (Volta, Scoop, fnm, nvm) exec it, and
|
|
471
|
+
# that is where node_modules\npm lives.
|
|
472
|
+
$real = Invoke-Tool 'node' @('-p', 'process.execPath') -Quiet
|
|
473
|
+
$realPath = ''
|
|
474
|
+
if ($real.Code -eq 0) { $realPath = Get-ExistingPathLine $real.Lines }
|
|
475
|
+
if (-not $realPath -or -not (Test-Path -LiteralPath $realPath -PathType Leaf)) {
|
|
476
|
+
Write-Note 'the node on PATH does not say where its binary is; installing a private one'
|
|
477
|
+
}
|
|
478
|
+
elseif (Test-ManagedNodePath $realPath) {
|
|
479
|
+
Write-Note "node v$($parts.Text) at $realPath belongs to a version manager; installing a private one so the hook outlives this shell"
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
$candidateCli = Join-Path (Split-Path -Parent $realPath) 'node_modules\npm\bin\npm-cli.js'
|
|
483
|
+
if (-not (Test-Path -LiteralPath $candidateCli -PathType Leaf)) {
|
|
484
|
+
Write-Note "node v$($parts.Text) at $realPath has no npm next to it; installing a private one"
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
$nodeExe = $realPath
|
|
488
|
+
$npmCli = $candidateCli
|
|
489
|
+
# @(...) around the call: a one-element array unrolls to a
|
|
490
|
+
# string on assignment, and string + array is one string.
|
|
491
|
+
$nodeFlags = @(Get-NodeFlags $parts.Major $parts.Minor)
|
|
492
|
+
Write-Note "node v$($parts.Text) at $realPath"
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
Write-Note 'no node on PATH; installing a private one'
|
|
499
|
+
}
|
|
500
|
+
if (-not $nodeExe) {
|
|
501
|
+
$nodeVersion = $pinnedNodeVersion
|
|
502
|
+
if ($env:BIR_NODE_VERSION) {
|
|
503
|
+
$nodeVersion = $env:BIR_NODE_VERSION.Trim().TrimStart('v')
|
|
504
|
+
if ($nodeVersion -notmatch '^\d+\.\d+\.\d+$') { throw "BIR_NODE_VERSION must look like 24.21.0, not '$($env:BIR_NODE_VERSION)'." }
|
|
505
|
+
}
|
|
506
|
+
Write-Step "Installing Node $nodeVersion privately"
|
|
507
|
+
$nodeExe = Install-PrivateNode $nodeVersion
|
|
508
|
+
$npmCli = Join-Path (Split-Path -Parent $nodeExe) 'node_modules\npm\bin\npm-cli.js'
|
|
509
|
+
$pinnedParts = Get-NodeVersionParts $nodeVersion
|
|
510
|
+
$nodeFlags = @(Get-NodeFlags $pinnedParts.Major $pinnedParts.Minor)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
# -- 2. the runner ------------------------------------------------------
|
|
514
|
+
#
|
|
515
|
+
# One code path for every Node: always into the private prefix, never
|
|
516
|
+
# into the machine's npm prefix (per-version under nvm and Volta,
|
|
517
|
+
# shim-managed under Scoop) and always through npm-cli.js, never the npm
|
|
518
|
+
# shim (PowerShell resolves npm.ps1 first, and a Restricted policy blocks it).
|
|
519
|
+
Write-Step 'Installing the runner'
|
|
520
|
+
$npmPrefix = Join-Path $birHome 'npm'
|
|
521
|
+
$spec = "$packageName@$version"
|
|
522
|
+
if ($env:BIR_TARBALL) {
|
|
523
|
+
if (-not (Test-Path -LiteralPath $env:BIR_TARBALL -PathType Leaf)) { throw "BIR_TARBALL is not a file: $($env:BIR_TARBALL)" }
|
|
524
|
+
$spec = (Resolve-Path -LiteralPath $env:BIR_TARBALL).ProviderPath
|
|
525
|
+
Write-Note "from $spec (BIR_TARBALL)"
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
Write-Note "$spec from the registry"
|
|
529
|
+
}
|
|
530
|
+
$npmArgs = @($nodeFlags) + @($npmCli, 'install', '-g', '--prefix', $npmPrefix, '--no-fund', '--no-audit', $spec)
|
|
531
|
+
$runnerDir = Join-Path $npmPrefix ('node_modules\' + $packageName.Replace('/', '\'))
|
|
532
|
+
$birJs = Join-Path $runnerDir 'dist\bin\bir.js'
|
|
533
|
+
$hooksJs = Join-Path $runnerDir 'dist\bin\bir-hooks.js'
|
|
534
|
+
if ($dryRun) {
|
|
535
|
+
Write-Would "run: $(Format-CommandLine $nodeExe $npmArgs)"
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
New-Item -ItemType Directory -Path $npmPrefix -Force | Out-Null
|
|
539
|
+
$r = Invoke-Tool $nodeExe $npmArgs
|
|
540
|
+
if ($r.Code -ne 0) {
|
|
541
|
+
if (($r.Lines -join "`n") -match 'CERT|ISSUER|SELF_SIGNED') { Write-Warn $tlsHint }
|
|
542
|
+
throw "npm install failed (exit $($r.Code)); see above."
|
|
543
|
+
}
|
|
544
|
+
if (-not (Test-Path -LiteralPath $birJs -PathType Leaf)) { throw "npm reported success but $birJs is missing." }
|
|
545
|
+
$installed = (Get-Content -LiteralPath (Join-Path $runnerDir 'package.json') -Raw | ConvertFrom-Json).version
|
|
546
|
+
Write-Note "bir $installed at $runnerDir"
|
|
547
|
+
# `bir setup` arrived in 0.2.9. A registry whose `latest` is older
|
|
548
|
+
# would install cleanly and then fail on the next line with nothing
|
|
549
|
+
# to say; say it here.
|
|
550
|
+
if ([version]($installed -replace '[^0-9.].*$', '') -lt [version]'0.2.9') {
|
|
551
|
+
throw "@basein/runner $installed has no 'bir setup'. This service must serve RUNNER_VERSION >= 0.2.9 (or set BIR_TARBALL to a newer build)."
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Launchers first on the user Path: `bir` in PowerShell then resolves to
|
|
556
|
+
# bir.cmd, so no execution-policy change, ever. The private Node stays
|
|
557
|
+
# off PATH; the launchers name it.
|
|
558
|
+
Write-Step 'Putting bir on your PATH'
|
|
559
|
+
$binDir = Join-Path $birHome 'bin'
|
|
560
|
+
if (-not $dryRun) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
|
|
561
|
+
# The launchers carry the same node flags this run used, so a `bir setup`
|
|
562
|
+
# typed later - and everything it wires - trusts the same certificates.
|
|
563
|
+
Write-Launcher (Join-Path $binDir 'bir.cmd') $nodeExe $nodeFlags $birJs
|
|
564
|
+
Write-Launcher (Join-Path $binDir 'bir-hooks.cmd') $nodeExe $nodeFlags $hooksJs
|
|
565
|
+
|
|
566
|
+
# The user Path is read raw and written back as REG_EXPAND_SZ. Not
|
|
567
|
+
# [Environment]::SetEnvironmentVariable: it flattens the value to REG_SZ,
|
|
568
|
+
# and every %USERPROFILE% already in there stops expanding.
|
|
569
|
+
$pathEntry = '%USERPROFILE%\.baseinstrunner\bin'
|
|
570
|
+
$rawPath = [string](Get-Item -Path HKCU:\Environment).GetValue('Path', '', 'DoNotExpandEnvironmentNames')
|
|
571
|
+
$present = $false
|
|
572
|
+
foreach ($part in $rawPath.Split(';')) {
|
|
573
|
+
$p = $part.Trim().TrimEnd('\')
|
|
574
|
+
if ($p -ieq $pathEntry -or $p -ieq $binDir.TrimEnd('\')) { $present = $true }
|
|
575
|
+
}
|
|
576
|
+
if ($present) {
|
|
577
|
+
Write-Note "user Path already has $pathEntry"
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
$newPath = if ($rawPath.Trim() -eq '') { $pathEntry } else { $pathEntry + ';' + $rawPath }
|
|
581
|
+
if ($dryRun) {
|
|
582
|
+
Write-Would "prepend $pathEntry to the user Path in HKCU:\Environment (REG_EXPAND_SZ); it is now: $rawPath"
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
Set-ItemProperty -Path HKCU:\Environment -Name Path -Value $newPath -Type ExpandString
|
|
586
|
+
Send-EnvironmentChange
|
|
587
|
+
Write-Note "user Path: $pathEntry first; new terminals see it"
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
$sessionHas = $false
|
|
591
|
+
foreach ($part in $env:Path.Split(';')) {
|
|
592
|
+
if ($part.Trim().TrimEnd('\') -ieq $binDir.TrimEnd('\')) { $sessionHas = $true }
|
|
593
|
+
}
|
|
594
|
+
if (-not $sessionHas) {
|
|
595
|
+
if ($dryRun) { Write-Would "prepend $binDir to `$env:Path for this window" }
|
|
596
|
+
else { $env:Path = $binDir + ';' + $env:Path }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# -- 3. Claude Code -----------------------------------------------------
|
|
600
|
+
Write-Step 'Checking Claude Code'
|
|
601
|
+
Update-SessionPath
|
|
602
|
+
$claude = Get-ClaudePath
|
|
603
|
+
if ($claude) {
|
|
604
|
+
Write-Note "claude at $claude"
|
|
605
|
+
}
|
|
606
|
+
elseif ($env:BIR_NO_CLAUDE -eq '1') {
|
|
607
|
+
Write-Note 'not installed; skipping (BIR_NO_CLAUDE=1)'
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
Write-Step 'Installing Claude Code (this can take a minute)'
|
|
611
|
+
# A child process, so the installer's own policy switch and any exit
|
|
612
|
+
# it calls stay in that child.
|
|
613
|
+
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
614
|
+
if (-not (Test-Path -LiteralPath $powershell)) { $powershell = 'powershell.exe' }
|
|
615
|
+
$claudeArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', 'irm https://claude.ai/install.ps1 | iex')
|
|
616
|
+
if ($dryRun) {
|
|
617
|
+
Write-Would "run: $(Format-CommandLine $powershell $claudeArgs)"
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
$r = Invoke-Tool $powershell $claudeArgs
|
|
621
|
+
Update-SessionPath
|
|
622
|
+
$claude = Get-ClaudePath
|
|
623
|
+
if ($r.Code -ne 0 -or -not $claude) {
|
|
624
|
+
Write-Warn "Claude Code did not install (exit $($r.Code)). The runner is set up anyway; install it later with: irm https://claude.ai/install.ps1 | iex"
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
Write-Note "claude at $claude"
|
|
628
|
+
Write-Note 'Claude Code needs a Claude subscription and asks you to sign in the first time it starts.'
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
# -- 4. setup -----------------------------------------------------------
|
|
634
|
+
Write-Step 'Running bir setup'
|
|
635
|
+
$setupArgs = @($nodeFlags) + @($birJs, 'setup', '--auth-url', $authUrl, '--project', $project)
|
|
636
|
+
if ($dryRun) {
|
|
637
|
+
Write-Would "run: $(Format-CommandLine $nodeExe $setupArgs)"
|
|
638
|
+
if ($setupToken) { Write-Would 'hand it BIR_SETUP_TOKEN through its environment (set right before, removed right after)' }
|
|
639
|
+
else { Write-Would 'run it without a token: it uses the cached sign-in or opens the browser' }
|
|
640
|
+
Write-Step 'Dry run finished; nothing was changed.'
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
# The token goes to this one child through its environment: set right
|
|
644
|
+
# before the spawn, removed right after, never on a command line.
|
|
645
|
+
if ($setupToken) { $env:BIR_SETUP_TOKEN = $setupToken }
|
|
646
|
+
try {
|
|
647
|
+
$r = Invoke-Tool $nodeExe $setupArgs
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
Remove-Item -Path Env:BIR_SETUP_TOKEN -ErrorAction SilentlyContinue
|
|
651
|
+
}
|
|
652
|
+
if ($r.Code -ne 0) {
|
|
653
|
+
throw "bir setup ended with exit code $($r.Code). Fix what it reported above, then paste the line again or run: bir setup"
|
|
654
|
+
}
|
|
655
|
+
# This window only when it is the PowerShell the line was pasted into: the
|
|
656
|
+
# cmd variant runs this in a child powershell.exe, whose $env:Path dies
|
|
657
|
+
# with it, so a cmd window sees `bir` only once it is a new window.
|
|
658
|
+
Write-Note 'bir works in every new terminal (and in this window when it is PowerShell); `bir doctor` checks the setup.'
|
|
659
|
+
Write-Note 'From a cmd window: open a new one, or run set PATH=%USERPROFILE%\.baseinstrunner\bin;%USERPROFILE%\.local\bin;%PATH%'
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
$message = $_.Exception.Message
|
|
663
|
+
if ($message -match 'CERT|ISSUER|SELF_SIGNED|secure channel|trust relationship') { Write-Warn $tlsHint }
|
|
664
|
+
Write-Host "error: $message" -ForegroundColor Red
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
Invoke-BirInstall
|