@yeaft/webchat-agent 1.0.524 → 1.0.526

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.
Binary file
@@ -6,7 +6,7 @@
6
6
  <title>Yeaft</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%232c2c2c'/><path d='M16 22V13M16 13L10 8M16 13L22 8' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' fill='none'/><circle cx='10' cy='8' r='2.4' fill='white'/><circle cx='22' cy='8' r='2.4' fill='white'/><circle cx='16' cy='22' r='2.4' fill='white'/></svg>">
8
8
  <link rel="stylesheet" href="vendor/katex/katex.min.css?v=b0d91614">
9
- <link rel="stylesheet" href="style.bundle.css?v=71937127">
9
+ <link rel="stylesheet" href="style.bundle.css?v=7048bc52">
10
10
  <script src="vendor.bundle.js?v=b4d2c175"></script>
11
11
  <script defer src="jszip.min.js"></script>
12
12
  <script defer src="docx-preview.min.js"></script>
@@ -17,6 +17,6 @@
17
17
  </head>
18
18
  <body>
19
19
  <div id="app"></div>
20
- <script type="module" src="app.bundle.js?v=890160ee"></script>
20
+ <script type="module" src="app.bundle.js?v=63b1cae8"></script>
21
21
  </body>
22
22
  </html>
@@ -0,0 +1,264 @@
1
+ # Safe user-level installer for the Yeaft Agent.
2
+ [CmdletBinding()]
3
+ param(
4
+ [Parameter()][string]$Server,
5
+ [Parameter()][string]$Secret,
6
+ [switch]$Help
7
+ )
8
+
9
+ $ErrorActionPreference = 'Stop'
10
+ $NodeChannel = 24
11
+ $Registry = 'https://pkg.yeaft.com/'
12
+ $Package = '@yeaft/webchat-agent@latest'
13
+ $Prefix = $null
14
+ $NpmCli = $null
15
+ $OwnedPrefix = $false
16
+ $ServiceInstallAttempted = $false
17
+ $InstallFailed = $false
18
+ $OriginalPath = $env:PATH
19
+ $OriginalTls = [Net.ServicePointManager]::SecurityProtocol
20
+ $SavedEnvironment = @{}
21
+ foreach ($Key in @('AGENT_SECRET', 'SERVER_URL', 'AGENT_NAME', 'YEAFT_AGENT_INSTANCE', 'WORK_DIR', 'YEAFT_DIR', 'PM2_HOME')) {
22
+ $Item = Get-Item -LiteralPath "Env:$Key" -ErrorAction SilentlyContinue
23
+ $SavedEnvironment[$Key] = @{ Present = $null -ne $Item; Value = if ($Item) { $Item.Value } else { $null } }
24
+ }
25
+
26
+ function Show-Help {
27
+ @'
28
+ Usage: install.ps1 -Server <ws:// or wss:// URL> -Secret <agent secret>
29
+
30
+ Installs a new, independently named Yeaft Agent in
31
+ $HOME\.yeaft\installations\<hostname>-NNNN. It never elevates privileges,
32
+ changes global PATH or npm configuration, or modifies an existing Agent. Node
33
+ 24 is downloaded from nodejs.org only when installed Node/npm cannot satisfy
34
+ Node >=22.5.0.
35
+ '@
36
+ }
37
+ function Stop-Install([string]$Message) { throw $Message }
38
+ function Test-Node([string]$NodePath) {
39
+ # No embedded double quotes: Windows PowerShell 5.1 strips them in native argv.
40
+ & $NodePath --no-warnings -e 'const v=process.versions.node.split(String.fromCharCode(46)).map(Number);if(v[0]<22||(v[0]===22&&v[1]<5))process.exit(1);require(String.fromCharCode(110,111,100,101,58,115,113,108,105,116,101))' *> $null
41
+ return $LASTEXITCODE -eq 0
42
+ }
43
+ function Quote-PowerShellLiteral([string]$Value) { return "'" + $Value.Replace("'", "''") + "'" }
44
+
45
+ try {
46
+ if ($Help) { Show-Help; return }
47
+
48
+ $ParsedServer = $null
49
+ $ServerValid = -not [string]::IsNullOrWhiteSpace($Server) -and
50
+ -not ($Server -match "[\r\n]") -and
51
+ [Uri]::TryCreate($Server, [UriKind]::Absolute, [ref]$ParsedServer) -and
52
+ ($ParsedServer.Scheme -eq 'ws' -or $ParsedServer.Scheme -eq 'wss') -and
53
+ -not [string]::IsNullOrWhiteSpace($ParsedServer.Host) -and
54
+ [string]::IsNullOrEmpty($ParsedServer.UserInfo)
55
+ if (-not $ServerValid) { Stop-Install '-Server must be a valid ws:// or wss:// URL with a host' }
56
+ if ([string]::IsNullOrWhiteSpace($Secret)) { Stop-Install '-Secret must not be empty' }
57
+ if ($Secret -match "[\r\n]") { Stop-Install 'arguments must not contain line breaks' }
58
+ if (-not $env:APPDATA) { Stop-Install 'APPDATA is required to install the Windows user service' }
59
+ $UserHome = if ($env:USERPROFILE) { $env:USERPROFILE } elseif ($env:HOME) { $env:HOME } else { $HOME }
60
+ if (-not $UserHome) { Stop-Install 'HOME is required to allocate the Agent installation' }
61
+ if ($UserHome -match "[\r\n]" -or $env:APPDATA -match "[\r\n]") { Stop-Install 'arguments must not contain line breaks' }
62
+
63
+ $Node = $null
64
+ $Npm = $null
65
+ $DownloadedRuntime = $false
66
+ $NodeCommand = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue
67
+ if (-not $NodeCommand) { $NodeCommand = Get-Command node -CommandType Application -ErrorAction SilentlyContinue }
68
+ if ($NodeCommand -and (Test-Node $NodeCommand.Source)) {
69
+ # A PATH npm.cmd may use a different adjacent node.exe (nvm/Volta/shims).
70
+ # Resolve Node's actual executable and require its own npm JS entry point.
71
+ $CandidateNode = (& $NodeCommand.Source --no-warnings -p 'process.execPath' 2>$null | Out-String).Trim()
72
+ if ($LASTEXITCODE -eq 0 -and [IO.Path]::IsPathRooted($CandidateNode)) {
73
+ $NpmCandidate = Join-Path (Split-Path -Parent $CandidateNode) 'node_modules\npm\bin\npm-cli.js'
74
+ if ((Test-Path -LiteralPath $NpmCandidate) -and (Test-Node $CandidateNode)) {
75
+ & $CandidateNode $NpmCandidate --version *> $null
76
+ if ($LASTEXITCODE -eq 0) { $Node = $CandidateNode; $NpmCli = $NpmCandidate }
77
+ }
78
+ }
79
+ }
80
+
81
+ $Root = Join-Path $UserHome '.yeaft\installations'
82
+ $InstancesRoot = Join-Path $UserHome '.yeaft\instances'
83
+ $HostPart = ([Environment]::MachineName.ToLowerInvariant() -replace '[^a-z0-9._-]', '-').Trim('-')
84
+ if (-not $HostPart) { $HostPart = 'agent' }
85
+ New-Item -ItemType Directory -Force -Path $Root | Out-Null
86
+ New-Item -ItemType Directory -Force -Path $InstancesRoot | Out-Null
87
+
88
+ $Name = $null
89
+ for ($Attempt = 0; $Attempt -lt 100; $Attempt++) {
90
+ $Digits = '{0:D4}' -f (Get-Random -Minimum 0 -Maximum 10000)
91
+ $Candidate = "$HostPart-$Digits"
92
+ $CandidatePrefix = Join-Path $Root $Candidate
93
+ $CandidateData = Join-Path $InstancesRoot $Candidate
94
+ $CandidateConfig = Join-Path $env:APPDATA "yeaft-agent\instances\$Candidate"
95
+ $CandidateStartup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\yeaft-agent-$Candidate.bat"
96
+ if ((Test-Path -LiteralPath $CandidatePrefix) -or (Test-Path -LiteralPath $CandidateData) -or
97
+ (Test-Path -LiteralPath $CandidateConfig) -or (Test-Path -LiteralPath $CandidateStartup)) { continue }
98
+ try {
99
+ New-Item -ItemType Directory -Path $CandidatePrefix -ErrorAction Stop | Out-Null
100
+ $Prefix = $CandidatePrefix
101
+ $OwnedPrefix = $true
102
+ Set-Content -LiteralPath (Join-Path $Prefix '.installer-owned') -Value $PID -NoNewline
103
+ $Name = $Candidate
104
+ break
105
+ } catch {
106
+ if (Test-Path -LiteralPath $CandidatePrefix) { continue }
107
+ throw
108
+ }
109
+ }
110
+ if (-not $Name) { Stop-Install 'could not allocate a unique Agent name' }
111
+
112
+ # The directory was atomically created by this process. Do not alter ACLs on
113
+ # pre-existing directories. Disable inheritance and grant only the current
114
+ # user, SYSTEM, and Administrators full control.
115
+ $CurrentUserAcl = $null
116
+ try { $CurrentUserAcl = '*' + [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value } catch {}
117
+ if (-not $CurrentUserAcl) { $CurrentUserAcl = $env:USERNAME }
118
+ if (-not $CurrentUserAcl) { Stop-Install 'could not identify the current Windows user' }
119
+ & icacls.exe $Prefix '/inheritance:r' '/grant:r' "${CurrentUserAcl}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' *> $null
120
+ if ($LASTEXITCODE -ne 0) { Stop-Install 'could not restrict installation directory permissions' }
121
+
122
+ if (-not $Node) {
123
+ $Architecture = $null
124
+ try { $Architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } catch {}
125
+ if ([string]::IsNullOrWhiteSpace($Architecture)) {
126
+ $Architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
127
+ }
128
+ switch -Regex ($Architecture) {
129
+ '^(X64|AMD64|x86_64)$' { $NodeArch = 'x64'; break }
130
+ '^(Arm64|ARM64|aarch64)$' { $NodeArch = 'arm64'; break }
131
+ default { Stop-Install 'unsupported CPU architecture' }
132
+ }
133
+ $DownloadDir = Join-Path $Prefix '.download'
134
+ New-Item -ItemType Directory -Path $DownloadDir | Out-Null
135
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
136
+ $SumsPath = Join-Path $DownloadDir 'SHASUMS256.txt'
137
+ Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -Uri "https://nodejs.org/dist/latest-v$NodeChannel.x/SHASUMS256.txt" -OutFile $SumsPath
138
+ $Match = Get-Content -LiteralPath $SumsPath | Where-Object { $_ -match "^[0-9a-fA-F]{64}\s+node-v24\.[0-9.]+-win-$NodeArch\.zip$" } | Select-Object -First 1
139
+ if (-not $Match) { Stop-Install 'no supported Node.js archive was listed by nodejs.org' }
140
+ $Parts = $Match -split '\s+', 2
141
+ $Expected = $Parts[0].ToLowerInvariant()
142
+ $File = $Parts[1].Trim()
143
+ $Archive = Join-Path $DownloadDir $File
144
+ $VersionMatch = [regex]::Match($File, '^node-v([0-9.]+)-')
145
+ if (-not $VersionMatch.Success) { Stop-Install 'no supported Node.js archive was listed by nodejs.org' }
146
+ $NodeVersion = $VersionMatch.Groups[1].Value
147
+ Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -Uri "https://nodejs.org/dist/v$NodeVersion/$File" -OutFile $Archive
148
+ $Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Archive).Hash.ToLowerInvariant()
149
+ if ($Actual -ne $Expected) { Stop-Install 'Node.js checksum verification failed' }
150
+ $Extracted = Join-Path $DownloadDir 'extracted'
151
+ Expand-Archive -LiteralPath $Archive -DestinationPath $Extracted
152
+ $NodeFolder = Get-ChildItem -LiteralPath $Extracted -Directory | Select-Object -First 1
153
+ if (-not $NodeFolder) { Stop-Install 'could not unpack Node.js' }
154
+ $Runtime = Join-Path $Prefix 'runtime'
155
+ Move-Item -LiteralPath $NodeFolder.FullName -Destination $Runtime
156
+ Remove-Item -LiteralPath $DownloadDir -Recurse -Force
157
+ $Node = Join-Path $Runtime 'node.exe'
158
+ $Npm = Join-Path $Runtime 'npm.cmd'
159
+ $NpmCli = Join-Path $Runtime 'node_modules\npm\bin\npm-cli.js'
160
+ $DownloadedRuntime = $true
161
+ if (-not (Test-Node $Node)) { Stop-Install 'downloaded Node.js does not meet the minimum version' }
162
+ }
163
+
164
+ $NodeDir = Split-Path -Parent $Node
165
+ $LocalBin = Join-Path $Prefix 'node_modules\.bin'
166
+ $env:PATH = "$NodeDir;$Prefix;$LocalBin;$OriginalPath"
167
+ & $Node $NpmCli --version *> $null
168
+ if ($LASTEXITCODE -ne 0) { Stop-Install 'the selected Node.js does not provide a working npm' }
169
+ & $Node $NpmCli --prefix $Prefix --global=false install $Package pm2 --registry=$Registry --no-audit --no-fund --loglevel=error
170
+ if ($LASTEXITCODE -ne 0) { Stop-Install 'npm could not install the Yeaft Agent' }
171
+
172
+ $Cli = Join-Path $Prefix 'node_modules\@yeaft\webchat-agent\cli.js'
173
+ $Pm2 = Join-Path $Prefix 'node_modules\pm2\bin\pm2'
174
+ if (-not (Test-Path -LiteralPath $Cli) -or -not (Test-Path -LiteralPath $Pm2)) {
175
+ Stop-Install 'the installed package did not provide the required CLI and PM2 runtime'
176
+ }
177
+
178
+ $WorkDir = Join-Path $Prefix 'workspace'
179
+ $YeaftDir = Join-Path $Prefix 'data'
180
+ New-Item -ItemType Directory -Path $WorkDir | Out-Null
181
+ $Pm2Home = Join-Path $Prefix 'pm2'
182
+ $env:PM2_HOME = $Pm2Home
183
+ $env:SERVER_URL = $Server
184
+ $env:AGENT_SECRET = $Secret
185
+ $env:AGENT_NAME = $Name
186
+ $env:YEAFT_AGENT_INSTANCE = $Name
187
+ $env:WORK_DIR = $WorkDir
188
+ $env:YEAFT_DIR = $YeaftDir
189
+ $ServiceInstallAttempted = $true
190
+ & $Node $Cli install --name $Name --yeaft-dir $YeaftDir --work-dir $WorkDir *> $null
191
+ if ($LASTEXITCODE -ne 0) { Stop-Install 'the Agent service could not be installed' }
192
+
193
+ # The published Windows service writes a bare "pm2 resurrect" line. Repair
194
+ # only this newly allocated instance startup file, retaining its tray line.
195
+ $StartupPath = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\yeaft-agent-$Name.bat"
196
+ if (-not (Test-Path -LiteralPath $StartupPath)) { Stop-Install 'the Agent startup file was not created' }
197
+ $StartupLines = @(Get-Content -LiteralPath $StartupPath)
198
+ $TrayLines = @($StartupLines | Where-Object { $_ -match '^\s*start\s+.*agent-tray\.ps1' })
199
+ $BatchNode = $Node.Replace('%', '%%')
200
+ $BatchPm2 = $Pm2.Replace('%', '%%')
201
+ $BatchPath = "$NodeDir;$Prefix;$LocalBin".Replace('%', '%%')
202
+ $BatchHome = $Pm2Home.Replace('%', '%%')
203
+ $StartupContent = @('@echo off', 'setlocal DisableDelayedExpansion', 'chcp 65001 >nul', "set `"PATH=$BatchPath;%PATH%`"", "set `"PM2_HOME=$BatchHome`"", "`"$BatchNode`" `"$BatchPm2`" resurrect") + $TrayLines
204
+ [IO.File]::WriteAllText($StartupPath, ($StartupContent -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding($false)))
205
+
206
+ $Manager = Join-Path $Prefix 'yeaft-agent.ps1'
207
+ $NodeLiteral = Quote-PowerShellLiteral $Node
208
+ $CliLiteral = Quote-PowerShellLiteral $Cli
209
+ $NameLiteral = Quote-PowerShellLiteral $Name
210
+ $YeaftLiteral = Quote-PowerShellLiteral $YeaftDir
211
+ $WorkLiteral = Quote-PowerShellLiteral $WorkDir
212
+ $PathLiteral = Quote-PowerShellLiteral "$NodeDir;$Prefix;$LocalBin"
213
+ $Pm2HomeLiteral = Quote-PowerShellLiteral $Pm2Home
214
+ Set-Content -LiteralPath $Manager -Encoding UTF8 -Value @"
215
+ # Run this file in a child PowerShell, preserving the caller's environment.
216
+ if (`$args -contains 'upgrade') { Write-Error 'Use this Agent instance in the Web UI to upgrade safely.'; exit 1 }
217
+ `$oldPath = `$env:PATH
218
+ `$oldPm2Home = `$env:PM2_HOME
219
+ try {
220
+ `$env:PATH = $PathLiteral + ';' + `$oldPath
221
+ `$env:PM2_HOME = $Pm2HomeLiteral
222
+ & $NodeLiteral $CliLiteral @args --name $NameLiteral --yeaft-dir $YeaftLiteral --work-dir $WorkLiteral
223
+ `$code = `$LASTEXITCODE
224
+ } finally {
225
+ `$env:PATH = `$oldPath
226
+ `$env:PM2_HOME = `$oldPm2Home
227
+ }
228
+ exit `$code
229
+ "@
230
+ Set-Content -LiteralPath (Join-Path $Prefix 'node-path') -Value $Node
231
+ Remove-Item -LiteralPath (Join-Path $Prefix '.installer-owned') -Force
232
+ New-Item -ItemType File -Path (Join-Path $Prefix '.complete') | Out-Null
233
+ Write-Host "`nYeaft Agent $Name was installed."
234
+ Set-Content -LiteralPath (Join-Path $Prefix 'management-command') -Value $Manager
235
+ Write-Host "Manage it with:`n powershell -NoProfile -ExecutionPolicy Bypass -File $(Quote-PowerShellLiteral $Manager) status"
236
+ } catch {
237
+ $InstallFailed = $true
238
+ if ($OwnedPrefix -and -not $ServiceInstallAttempted -and $Prefix -and
239
+ (Test-Path -LiteralPath (Join-Path $Prefix '.installer-owned'))) {
240
+ Remove-Item -LiteralPath $Prefix -Recurse -Force -ErrorAction SilentlyContinue
241
+ }
242
+ $SafeMessages = @(
243
+ '-Server must be a valid ws:// or wss:// URL with a host','-Secret must not be empty',
244
+ 'arguments must not contain line breaks','APPDATA is required to install the Windows user service',
245
+ 'HOME is required to allocate the Agent installation','unsupported CPU architecture',
246
+ 'could not allocate a unique Agent name','could not identify the current Windows user',
247
+ 'could not restrict installation directory permissions',
248
+ 'no supported Node.js archive was listed by nodejs.org','Node.js checksum verification failed',
249
+ 'could not unpack Node.js','downloaded Node.js does not meet the minimum version',
250
+ 'npm could not install the Yeaft Agent','the selected Node.js does not provide a working npm',
251
+ 'the installed package did not provide the required CLI and PM2 runtime',
252
+ 'the Agent service could not be installed','the Agent startup file was not created'
253
+ )
254
+ $Message = if ($SafeMessages -contains $_.Exception.Message) { $_.Exception.Message } else { 'an unexpected operation failed; no existing Agent was changed' }
255
+ [Console]::Error.WriteLine("Yeaft Agent installation failed: $Message")
256
+ } finally {
257
+ $env:PATH = $OriginalPath
258
+ [Net.ServicePointManager]::SecurityProtocol = $OriginalTls
259
+ foreach ($Key in $SavedEnvironment.Keys) {
260
+ if ($SavedEnvironment[$Key].Present) { Set-Item -LiteralPath "Env:$Key" -Value $SavedEnvironment[$Key].Value }
261
+ else { Remove-Item -LiteralPath "Env:$Key" -ErrorAction SilentlyContinue }
262
+ }
263
+ }
264
+ if ($InstallFailed) { exit 1 }
@@ -0,0 +1,150 @@
1
+ #!/bin/sh
2
+ # 用户级一键安装:只创建新实例,不覆盖系统 Node/npm 或已有 Agent。
3
+ set -eu
4
+ umask 077
5
+
6
+ NODE_CHANNEL=24
7
+ REGISTRY=https://pkg.yeaft.com/
8
+ PACKAGE=@yeaft/webchat-agent@latest
9
+ SERVER=
10
+ SECRET=
11
+ PREFIX=
12
+ SERVICE_ATTEMPTED=false
13
+
14
+ usage() {
15
+ cat <<'EOF'
16
+ Usage: install.sh --server <ws:// or wss:// URL> --secret <agent secret>
17
+
18
+ Installs a new Yeaft Agent in ~/.yeaft/installations/<hostname>-NNNN.
19
+ Requires a Linux systemd user session or macOS launchd login session.
20
+ Reuses Node >=22.5.0 with npm, or downloads a private Node 24 runtime.
21
+ No sudo, system PATH changes, or upgrades/restarts of existing Agents.
22
+ EOF
23
+ }
24
+ fail() { printf '%s\n' "Yeaft Agent installation failed: $1" >&2; exit 1; }
25
+ cleanup() {
26
+ # PREFIX is assigned only AFTER successful exclusive creation. Once service
27
+ # installation starts, retain its runtime/config/logs for recovery.
28
+ if [ -n "$PREFIX" ] && [ "$SERVICE_ATTEMPTED" = false ]; then rm -rf "$PREFIX"; fi
29
+ }
30
+ trap cleanup EXIT
31
+ trap 'exit 130' INT
32
+ trap 'exit 143' HUP TERM
33
+ quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
34
+
35
+ while [ "$#" -gt 0 ]; do
36
+ case "$1" in
37
+ --server) [ "$#" -ge 2 ] || fail '--server requires a value'; SERVER=$2; shift 2 ;;
38
+ --secret) [ "$#" -ge 2 ] || fail '--secret requires a value'; SECRET=$2; shift 2 ;;
39
+ -h|--help) usage; exit 0 ;;
40
+ *) fail 'unknown argument (use --help)' ;;
41
+ esac
42
+ done
43
+ case "$SERVER" in ws://?*|wss://?*) ;; *) fail '--server must be a ws:// or wss:// URL' ;; esac
44
+ [ -n "$SECRET" ] || fail '--secret must not be empty'
45
+ CR=$(printf '\r')
46
+ case "$SERVER$SECRET$HOME" in *"
47
+ "*|*"$CR"*) fail 'arguments and home directory must not contain line breaks' ;; esac
48
+
49
+ OS=$(uname -s)
50
+ ARCH=$(uname -m)
51
+ case "$OS" in
52
+ Linux)
53
+ command -v systemctl >/dev/null 2>&1 || fail 'Linux requires systemctl and a systemd user session'
54
+ systemctl --user show-environment >/dev/null 2>&1 || fail 'Linux requires an available systemd user manager; log in with a systemd user session and retry'
55
+ PLATFORM=linux ;;
56
+ Darwin)
57
+ command -v launchctl >/dev/null 2>&1 || fail 'macOS requires launchd'
58
+ launchctl print "gui/$(id -u)" >/dev/null 2>&1 || fail 'macOS requires a graphical login session for launchd'
59
+ PLATFORM=darwin ;;
60
+ *) fail 'unsupported operating system (use PowerShell on Windows)' ;;
61
+ esac
62
+ case "$ARCH" in x86_64|amd64) NODE_ARCH=x64 ;; arm64|aarch64) NODE_ARCH=arm64 ;; *) fail 'supported CPU architectures are x64 and arm64' ;; esac
63
+ version_ok() {
64
+ "$1" -e 'const [a,b]=process.versions.node.split(".").map(Number);if(a<22||(a===22&&b<5))process.exit(1);require("node:sqlite")' >/dev/null 2>&1
65
+ }
66
+ NODE=
67
+ NPM=
68
+ if command -v node >/dev/null 2>&1; then
69
+ CANDIDATE_NODE=$(command -v node)
70
+ # Resolve relative PATH entries, but retain the bin directory that owns npm.
71
+ CANDIDATE_NODE=$(cd "$(dirname "$CANDIDATE_NODE")" && pwd)/node
72
+ CANDIDATE_NPM=$(dirname "$CANDIDATE_NODE")/npm
73
+ if [ -x "$CANDIDATE_NPM" ] && version_ok "$CANDIDATE_NODE" && PATH="$(dirname "$CANDIDATE_NODE"):$PATH" "$CANDIDATE_NPM" --version >/dev/null 2>&1; then
74
+ NODE=$CANDIDATE_NODE
75
+ NPM=$CANDIDATE_NPM
76
+ fi
77
+ fi
78
+
79
+ ROOT=$HOME/.yeaft/installations
80
+ HOST=$(hostname 2>/dev/null || printf agent)
81
+ HOST=$(printf '%s' "$HOST" | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c1-40)
82
+ [ -n "$HOST" ] || HOST=agent
83
+ mkdir -p "$ROOT"
84
+ tries=0
85
+ while [ "$tries" -lt 100 ]; do
86
+ tries=$((tries + 1))
87
+ DIGITS=$(od -An -N4 -tu4 /dev/urandom | awk '{printf "%04d", $1 % 10000}')
88
+ case "$DIGITS" in [0-9][0-9][0-9][0-9]) ;; *) fail 'could not generate an instance name' ;; esac
89
+ NAME=$HOST-$DIGITS
90
+ if [ -e "$HOME/.config/yeaft-agent/instances/$NAME" ] || [ -L "$HOME/.config/yeaft-agent/instances/$NAME" ] ||
91
+ [ -e "$HOME/.yeaft/instances/$NAME" ] || [ -L "$HOME/.yeaft/instances/$NAME" ] ||
92
+ [ -e "$HOME/.config/systemd/user/yeaft-agent@$NAME.service" ] || [ -L "$HOME/.config/systemd/user/yeaft-agent@$NAME.service" ] ||
93
+ [ -e "$HOME/Library/LaunchAgents/com.yeaft.agent.$NAME.plist" ] || [ -L "$HOME/Library/LaunchAgents/com.yeaft.agent.$NAME.plist" ]; then continue; fi
94
+ if mkdir "$ROOT/$NAME" 2>/dev/null; then PREFIX=$ROOT/$NAME; break; fi
95
+ done
96
+ [ -n "$PREFIX" ] || fail 'could not allocate a unique Agent name'
97
+
98
+ if [ -z "$NODE" ]; then
99
+ command -v curl >/dev/null 2>&1 || fail 'curl is required to download Node.js'
100
+ command -v tar >/dev/null 2>&1 || fail 'tar is required to unpack Node.js'
101
+ printf 'Downloading a private Node.js 24 runtime...\n'
102
+ STAGING=$PREFIX/.download
103
+ mkdir "$STAGING"
104
+ SUMS=$STAGING/SHASUMS256.txt
105
+ curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$SUMS" "https://nodejs.org/dist/latest-v$NODE_CHANNEL.x/SHASUMS256.txt" || fail 'could not download Node.js checksums'
106
+ FILE=$(awk -v suffix="-$PLATFORM-$NODE_ARCH.tar.gz" '$2 ~ /^node-v24\.[0-9]+\.[0-9]+-/ && substr($2,length($2)-length(suffix)+1)==suffix {print $2; exit}' "$SUMS")
107
+ [ -n "$FILE" ] || fail 'no supported Node.js archive was listed by nodejs.org'
108
+ VERSION=${FILE#node-}; VERSION=${VERSION%%-*}
109
+ curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$STAGING/$FILE" "https://nodejs.org/dist/$VERSION/$FILE" || fail 'could not download Node.js'
110
+ EXPECTED=$(awk -v f="$FILE" '$2==f {print $1; exit}' "$SUMS")
111
+ if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$STAGING/$FILE" | awk '{print $1}');
112
+ elif command -v shasum >/dev/null 2>&1; then ACTUAL=$(shasum -a 256 "$STAGING/$FILE" | awk '{print $1}');
113
+ else fail 'sha256sum or shasum is required to verify Node.js'; fi
114
+ [ "${#EXPECTED}" -eq 64 ] && [ "$ACTUAL" = "$EXPECTED" ] || fail 'Node.js checksum verification failed'
115
+ mkdir "$PREFIX/runtime"
116
+ tar -xzf "$STAGING/$FILE" -C "$PREFIX/runtime" --strip-components=1 || fail 'could not unpack Node.js'
117
+ NODE=$PREFIX/runtime/bin/node
118
+ NPM=$PREFIX/runtime/bin/npm
119
+ version_ok "$NODE" || fail 'downloaded Node.js cannot run on this OS or does not meet the minimum version'
120
+ PATH="$(dirname "$NODE"):$PATH" "$NPM" --version >/dev/null 2>&1 || fail 'downloaded Node.js did not provide a working npm'
121
+ rm -rf "$STAGING"
122
+ fi
123
+
124
+ YEAFT_INSTALL_SERVER="$SERVER" "$NODE" -e 'try {const u=new URL(process.env.YEAFT_INSTALL_SERVER);if(!["ws:","wss:"].includes(u.protocol)||!u.hostname||u.username||u.password)process.exit(1)}catch{process.exit(1)}' >/dev/null 2>&1 || fail 'invalid server URL'
125
+ NODE_DIR=$(dirname "$NODE")
126
+ printf 'Installing the Yeaft Agent in %s...\n' "$PREFIX"
127
+ PATH="$NODE_DIR:$PREFIX/bin:$PATH" "$NPM" --prefix "$PREFIX" --global install "$PACKAGE" \
128
+ --registry="$REGISTRY" --no-audit --no-fund --loglevel=error || fail 'npm could not install the Yeaft Agent'
129
+ CLI_JS=$PREFIX/lib/node_modules/@yeaft/webchat-agent/cli.js
130
+ [ -f "$CLI_JS" ] || fail 'the installed package did not provide yeaft-agent'
131
+
132
+ WRAPPER=$PREFIX/yeaft-agent
133
+ {
134
+ printf '#!/bin/sh\nPATH=%s:"$PATH"; export PATH\n' "$(quote "$NODE_DIR:$PREFIX/bin")"
135
+ printf '%s\n' 'for arg in "$@"; do if [ "$arg" = upgrade ]; then echo "Use this Agent instance in the Web UI to upgrade safely; standalone CLI upgrade targets a global npm installation." >&2; exit 1; fi; done'
136
+ printf 'exec %s %s "$@" --name %s --yeaft-dir %s\n' "$(quote "$NODE")" "$(quote "$CLI_JS")" "$(quote "$NAME")" "$(quote "$PREFIX/data")"
137
+ } > "$WRAPPER"
138
+ chmod 700 "$WRAPPER"
139
+ printf '%s\n' "$WRAPPER" > "$PREFIX/management-command"
140
+ # Use explicit roots rather than inheriting another Agent's WORK_DIR/YEAFT_DIR.
141
+ # Secret is a script argument, but is not passed through a shell to the CLI.
142
+ mkdir "$PREFIX/workspace"
143
+ SERVICE_ATTEMPTED=true
144
+ AGENT_SECRET="$SECRET" PATH="$NODE_DIR:$PREFIX/bin:$PATH" "$NODE" "$CLI_JS" install --server "$SERVER" --name "$NAME" \
145
+ --yeaft-dir "$PREFIX/data" --work-dir "$PREFIX/workspace" > "$PREFIX/service-install.log" 2>&1 || fail "service installation failed; inspect the private log at $PREFIX/service-install.log"
146
+ : > "$PREFIX/.complete"
147
+ printf '\nYeaft Agent %s was installed.\nManage it with:\n %s status --name %s\n' "$NAME" "$(quote "$WRAPPER")" "$NAME"
148
+ if [ "$PLATFORM" = linux ]; then
149
+ printf 'To keep the user service running after logout, ask an administrator to enable linger for your account.\n'
150
+ fi